【APUE】并发 — 信号

目录

一、异步与同步

二、信号的概念

三、signal 函数

3.1 函数原型 

3.2 代码示例 

四、信号的不可靠

五、可重入函数

反例1:函数内使用了静态数据

反例2:函数内使用了 malloc 或 free

反例3:函数内调用了标准 I/O 函数

六、标准信号的响应过程

6.1 信号屏蔽字与未决信号集 

6.2 时间片轮转

6.3 响应过程详解 

6.3.1 进程收到信号

6.3.2 进程响应信号 

6.3.3 响应完毕

6.4 思考

七、常用函数 

7.1 kill

7.2 raise

7.3 alarm

7.4 pause

7.5 综合运用

7.5.1 定时累加

7.5.2 漏桶实现

7.5.3 令牌桶实现 

拓展1:令牌桶封装成库

拓展2:多计时器封装成库

7.6 abort

7.7 system 

7.8 sleep 

7.9 nanosleep

7.10 usleep

7.11 select

八、信号集

8.1 概念

8.2 相关函数

九、信号屏蔽字/pending集的处理 

9.1 sigprocmask 

9.1.1 函数功能介绍

9.1.2 代码示例 

9.1.3 思考

9.2 sigpending 

9.2.1 函数功能介绍

9.2.2 思考

十、扩展函数

10.1 setitimer && setitimer

10.2 sigsuspend

10.2.1 函数功能介绍 

10.2.2 代码示例  

10.3 sigaction

10.3.1 signal 的缺陷

10.3.1.1 缺陷1 

10.3.1.2 缺陷2 

10.3.2 sigaction 功能介绍

10.3.3 代码示例 

10.3.3.1 定制信号处理过程中所屏蔽的信号 

10.3.3.2 仅响应 kernel 态的信号

十一、实时信号


一、异步与同步

概况:同步是阻塞模式,异步是非阻塞模式

同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回响应信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;

异步就相当于当客户端发送给服务端请求后,在等待服务端处理请求的时,客户端可以做其他的事情,这样节约了时间,提高了效率。

  • 比方说一个人边吃饭,边看手机,边说话,就是异步处理的方式
  • 同步处理就不一样了,说话后再吃饭,吃完饭后再看手机。必须等待上一件事完了,才执行后面的事情

如果是异步处理方式,那么客户端如何才能知道服务端请求处理完毕了查询法和通知法 

查询法:客户端时不时地去查询服务端工作进展,看看服务端工作完成没有

通知法:服务端工作完成后,主动通知客户端,这样客户端不需要时不时地去查询服务端


二、信号的概念

信号是软件中断,信号的响应依赖于中断

注意:软件中断不等于中断!软件中断是应用层的内容;而中断是硬件的、底层的内容


通过 kill -l 命令可以查看有哪些信号

【APUE】并发 — 信号_第1张图片

  • 编号 1-31 的信号都是标准信号
  • 编号 34 之后的信号都叫 SIGRTxxx,都是实时信号,RT 代表“实时”

这些信号有什么功能呢?看下表

【APUE】并发 — 信号_第2张图片

其中:

  • “•” 表示此种信号定义为基本 POSIX.1 规范部分,“XSI” 表示该信号定义在 XSI 扩展部分
  • “终止 + core” 表示在进程当前工作目录的 core 文件中复制了该进程的内存映像,可用于检查进程终止时的状态

三、signal 函数

3.1 函数原型 

man 2 signal 

#include 

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);


// 等同于:
void (*signal(int signum, void (*handler)(int)))(int);

功能:指定程序收到特定信号后,执行的行为

  • signum — 指定信号
  • handler — 用于指定行为。可以指向某类函数的入口地址,也可以设置成 SIG_IGN(表示“收到该信号后忽略”) 或 SIG_DFL(表示“收到该信号后执行默认行为”)
  • 调用成功返回信号原来的 handler;失败返回 SIG_ERR 并设置 errno

handler 指向的函数我们可以称之为信号处理函数 


3.2 代码示例 

首先让程序“收到信号后忽略该信号”

#include 
#include 
#include 
#include 

int main() {

        signal(SIGINT, SIG_IGN);    // SIGINT信号可通过ctrl+c发送给该进程

        for (int i = 0; i < 10; ++i) {
                write(1, "*", 1);       // 往终端打印一个字符
                sleep(1);    // 阻塞1s
        }
        write(1, "\n", 1);

        exit(0);
}

【APUE】并发 — 信号_第3张图片

可以看到,进程收到 ctrl+c 发出的信号 SIGINT 后,并没有终止,该信号被忽略了

然后让程序收到信号后,执行我们设定的信号处理函数

#include 
#include 
#include 
#include 

static void handler(int s) {
        write(1, "!", 1);
        return;
}

int main() {

        signal(SIGINT, handler);

        for (int i = 0; i < 10; ++i) {

                write(1, "*", 1);       // 往终端打印一个字符
                sleep(1); // 阻塞

        }

        write(1, "\n", 1);

        exit(0);
}

【APUE】并发 — 信号_第4张图片

可以看到,进程收到 ctrl+c 发出的信号 SIGINT 后,调用了 handler 信号处理函数打印了感叹号。上面截图的内容是因为我一直按着 ctrl+c 没放,然后程序刷的一下就跑完了跑完之后我还一直按着没来得及松开,所以多键入了几行 

唉?不是 sleep 阻塞了吗?怎么会那么快跑完呢?

答案:处理未被忽略的信号会打断阻塞的系统调用 

比如系统调用 open、read 等等,阻塞的时候都可能被信号打断。对于这些系统调用而言,被信号打断显然也算是一种错误,因为它们没有完成应该完成的任务。这种错误被称为“假错”,会设置 errno = EINTR


四、信号的不可靠

如果我们定义了一个需要花费较长时间调用的信号处理函数,当在运行该函数处理该信号的同时,如果又收到了别的信号,怎么处理?

信号的不可靠的定义:信号处理函数由内核调用,在调用该函数处理信号的过程中,会存在一个程序执行所依赖的现场。此时如果又来了一个信号,内核转入其他程序流可能会冲掉之前信号处理函数调用过程中所依赖的现场这样一来,从其他程序流再返回之前那个信号处理函数后,可能会导致一些问题

具体而言,如果信号处理函数的运行结果依赖于现场,那么如果该函数的执行现场被冲掉了,其运行结果也未知了......

打个比方......

【APUE】并发 — 信号_第5张图片

上面例子中,全局变量 a 的值就是我们所说的“现场”,信号处理函数的运行结果依赖于 a 的值。因为这个 a 有可能被改变成奇奇怪怪的东东,因此我们这个信号处理函数的运行结果也可能是奇奇怪怪的东东......,显然不太可靠


五、可重入函数

如果将可重入函数作为信号处理函数,能够避免上述介绍的信号不可靠问题

定义:又称可被中断的函数。如果一个函数在执行期间被中断后,到重新回中断点继续执行的过程中,函数所依赖的环境没有发生改变,那么这个函数就是可重入的,否则就不可重入


函数不可重入的条件

  • 函数内使用了静态的数据
  • 函数内使用了 malloc 或者 free 函数
  • 函数内调用了标准的 I/O 函数

下面一一介绍这几个不可重入函数


反例1:函数内使用了静态数据

char cTemp;    //全局变量
void SwapChar(char *lpcX, char *lpcY) {
    cTemp=*lpcX;
    *lpcX=*lpcY;
    lpcY=cTemp;    //访问了全局变量
}

在这里,函数原目标是交换两个指针所指空间的值。但是由于是通过全局变量 cTemp 作为媒介交换的,那么如果在 lpcY = cTemp 语句之前发生中断,会跳转到外部程序流执行。如果重新返回该函数后,cTemp 的值被外部的程序流程改变了,那么就无法完成交换。因此,该函数不可重入


反例2:函数内使用了 malloc 或 free

void mallocAndFree() {
    char *Ptr = NULL;
    Ptr = (char *)malloc(100 * sizeof(char));
    if (NULL == Ptr) {
        exit(1);
    }
    free(Ptr);
    Ptr = NULL;
}

在这里,函数只是简单地开辟堆内存然后释放堆内存。malloc 会开辟堆内存,假如在 malloc 后发生中断,重新返回该函数后,malloc 开辟的那片堆内存已经被外部的程序流程释放了,那么该函数后续的 free 语句就会有问题。因此,该函数不可重入


反例3:函数内调用了标准 I/O 函数

void printA() {
    printf("A");
    fflush(stdout);
    return;
}

在这里,函数原目标是在在标准输出上打印字母 A。printf 会将字母 A 加入缓冲区,并利用 fflush 刷新缓冲区并打印在设备上。假如在 fflush 前发生中断,重新返回该函数后,缓冲区中的值被奇奇怪怪的内容填充,那么那么该函数就会在设备上打印奇奇怪怪的内容。因此,该函数不可重入

所有的系统调用都是可重入的;一部分标准库函数也是可重入的,如:memcpy 


六、标准信号的响应过程

在这里我们暂时以进程为单位来看待标准信号的响应过程。可能随着后续知识深入,会改变我们看待的视角。不过先暂时这么理解就行了 

在介绍标准信号的响应过程之前,先铺垫一点儿基础知识。注意:下面讲的都是标准信号!!


6.1 信号屏蔽字与未决信号集 

【APUE】并发 — 信号_第6张图片

6.2 时间片轮转

宏观上,一个进程的正常运行过程如下:

微观上,进程其实是按照如下方式执行:

【APUE】并发 — 信号_第7张图片

由于时间片轮转,当一个进程耗完其时间片,操作系统便会让其陷入内核,并将该进程的运行现场保存起来,然后加入就绪队列等待调度。经过一段时间后,该进程被操作系统调度上 CPU,从而根据所保存的进程运行现场,恢复进程的继续执行

由于时间片及进程在就绪队列中等待调度的时间都很短,因此宏观上这个进程好像是在一直运行一样


6.3 响应过程详解 

6.3.1 进程收到信号

进程收到信号,只是将该信号所对应的 pending 位置 1,仅此而已

【APUE】并发 — 信号_第8张图片

6.3.2 进程响应信号 

进程只会在被调度的时候,去看 pending 并响应信号,即信号是在从 kernel 态回到 user 态的路上被响应的

【APUE】并发 — 信号_第9张图片

6.3.3 响应完毕

【APUE】并发 — 信号_第10张图片


6.4 思考

标准信号从收到到响应有一个不可避免的延迟,为什么?

答:收到信号只是让 pending 某位置 1,而响应需要等到进程被调度。这中间是会有一段时间延迟的 

如何屏蔽掉一个信号?

答:信号屏蔽字 mask 特定位置 0 即可。这样一来,在进程被调度时,通过 mask 按位与上 pending 时,即使 pending 特定位置 1了,与运算后结果也是 0,这样一来就相当于这个信号被屏蔽了

标准信号为什么会丢失?

答:如果在处理特定信号前收到多个相同信号,相当于不断对 pending 特定的位置 1,这在操作系统看来,和仅收到一个信号是等同的效果,它无法知道其实这是收到了多个信号的结果

标准信号的响应有没有严格的顺序? 

答:没有,如果 mask 和 pending 与运算后有多位为 1,则由操作系统决定标准信号的响应顺序

mask 和 pending 的变化情况如下

mask pending
1		0	// 初始化
			// 进程处理自己的任务...
1		1	// 某一时刻信号到来
            // 进程处理自己的任务...,直到重新被调度时
0		0	// 信号响应 内核态->用户态时
    		// 进入到信号处理函数中执行
1		0	// 信号响应完毕
     		// 进程继续处理自己的任务

这样变化的含义是什么? 

答:pending 变为 1 表示有未决信号,pending 变为 0 表示该信号被处理了;mask 变为 0 是为了避免在执行该信号的处理函数过程中,又收到相同信号,从而嵌套调用该函数(毕竟有些信号处理函数确实是不可重入的)

从信号处理函数中往外进行函数跳转会发生什么?

之前我们学过如下两个用于实现函数间跳转的函数

#include 
 
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

其基本思路是通过 env 保存现场,通过前往现场实现函数跳转。但是注意:在有的环境下,jum_buf 类型的 env 并不会保存 mask!

在之前图示中,信号处理函数执行完毕后会陷入内核,并将 mask 置 0 的恢复至 1。如果在信号处理函数执行过程中途跳出,则信号处理函数就永远不会顺利地执行完毕了,从而不会陷入内核恢复 mask,进而导致 mask 永远无法恢复

因此,如果想从信号处理函数往外跳,应该用下面两个函数

#include 

int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);

在这里 sigjum_buf 类型的 env 能够保存包括 mask 在内的现场。这样一来,即使从信号处理函数中往外跳(通过前往现场的方式),到达现场后也能够成功恢复 mask


七、常用函数 

7.1 kill

man 2 kill

#include 
#include 

int kill(pid_t pid, int sig);

功能:向进程发送信号

  • pid — 用于指定给什么哪些进程发信号。当 pid 取不同的值时,在这里有不同的意义
取值 含义
pid > 0 信号将会发送给 pid 所标识的某个进程
pid = 0 组内广播。信号将发送给和当前调用该函数进程同组的所有进程
pid = -1 全局广播。信号将发送给当前进程能发送给的所有进程(除 init 进程)
pid < -1 信号将发送给某个指定进程组中进程,这个进程组的 ID 等于 pid 的绝对值
  • sig — 指定发送什么信号。如果 sig = 0,则表示不会发出任何信号,常被用于检测某个进程或进程组是否存在(如果进程或进程组不存在,会设置 errno = ESRCH)
  • 成功返回 0;失败返回 -1 并设置 errno

7.2 raise

man 3 raise

#include 

int raise(int sig);

功能:向当前调用该函数的进程/线程发送信号(即向自己发送信号)

等价于:

kill(getpid(), sig);

在多线程程序中,等价于: 

pthread_kill(pthread_self(), sig);

7.3 alarm

man 2 alarm

#include 

unsigned int alarm(unsigned int seconds);

功能:设置以秒为单位的倒计时器

  • seconds — 设置一个倒计时器,表明在指定 seconds 后,内核会给当前进程发送 SIGALRM 信号(定时器超时)。进程收到该信号的默认动作为终止
  • 每个进程都有且只有唯一的一个倒计时器,所以多个 alarm 函数共同调用时,后面设置的倒计时器会覆盖掉前面的倒计时器,返回被覆盖倒计时器的剩余秒数
  • 若 seconds 设置为 0,则取消之前设置的倒计时器,返回被取消倒计时器的剩余秒数

代码示例

#include 
#include 
#include 

int main() {
        alarm(10);    // 10s后发送超时信号
        while(1);
        exit(0);
}

【APUE】并发 — 信号_第11张图片

上图中 CPU 提高利用率是因为不断执行 while(1),忙等


7.4 pause

man 2 pause

#include 

int pause(void);

功能:阻塞直到收到信号

代码示例 

#include 
#include 
#include 

int main() {
        alarm(10);
        pause()
        exit(0);
}  

【APUE】并发 — 信号_第12张图片

在这里,使用 pause 就不会出现忙等现象。阻塞进程主动放弃 CPU,不会占用 CPU 资源,相较于利用 while(1) 忙等,是种更好的方式 

注意:慎用 sleep,因为虽然在我们的环境下 sleep 能够实现我们想要的功能,但是 sleep 在很多别的环境下是用 alarm + pause 封装的,这肯定是有问题的:alarm 到时间后会让内核发送信号导致进程意外终止!在这些环境下,sleep 后的语句是执行不到的


7.5 综合运用

7.5.1 定时累加

功能需求:让程序在 5s 内不断执行累加操作 

实现1:通过时间戳实现

#include 
#include 
#include 
int main() {

        time_t end = time(NULL)+5;
        long long count = 0;

        while (time(NULL) <= end)
                count++;

        printf("%lld\n", count);

        exit(0);
}

【APUE】并发 — 信号_第13张图片

实现2:通过 alarm 发送信号实现 

#include 
#include 
#include 
#include 

// 循环跳出的标志
static int loop = 1;

// 信号处理函数
static void alrm_handler(int s) {
        loop = 0;
}

int main() {

        long long count = 0;

        //对信号SIGALRM注册信号处理函数
        //为保证在收到SIGALRM之前注册,因此写在alarm之前
        signal(SIGALRM, alrm_handler);
        alarm(5);

        while (loop)
                count++;

        printf("%lld\n", count);

        exit(0);
}

【APUE】并发 — 信号_第14张图片

可以看到,通过 alarm 发送信号定时的 5s 比通过时间戳定时的 5s 更精确,且因为不用频繁调用 time 函数,最终在 5s 内执行累加的次数更多 

实现3:试试优化

利用如下命令对实现 2 中的代码进行 O1 优化(可以理解为最低程度的优化),并执行

【APUE】并发 — 信号_第15张图片

我们等了很久很久,发现并没有结束程序。怎么回事呢?可在编译时加上 -S 选项生成汇编文件,通过汇编文件中的汇编语言进行分析

分析结果:编译优化时,gcc 看到循环体中没有改变 loop,所以 gcc 认为 loop 是不变的,因此将 loop 的值存储在高速缓存(CPU的寄存器)中,每次读取 loop 的值时,从高速缓存中读取,而不是在 loop 实际存放的内存地址中读取。因此每次读取到的值都是 loop = 1,从而死循环

解决方案:用 volatile 修饰 loop,此时编译器认为该变量易变,每次读取时从实际内存地址中读取

#include 
#include 
#include 
#include 

// 循环跳出的标志
static volatile int loop = 1;    // volatile修饰

// 信号处理函数
static void alrm_handler(int s) {
        loop = 0;
}

int main() {

        long long count = 0;

        //对信号SIGALRM注册信号处理函数
        signal(SIGALRM, alrm_handler);
        alarm(5);

        while (loop)
                count++;

        printf("%lld\n", count);

        exit(0);
}

【APUE】并发 — 信号_第16张图片


7.5.2 漏桶实现

定义:所谓漏桶,即流量控制,按固定的速率来处理请求

为什么叫漏桶?想一下这样的场景......请求先在桶中排队,系统或服务只以一个恒定的速度从桶中将请求取出进行处理

【APUE】并发 — 信号_第17张图片

上图画的是个漏斗。好吧,就想成漏斗就行 

需求:实现一个显示文件内容到标准输出的程序。要求每秒只往后显示 10 个字符,而不是一口气全部显示

提示:可在之前实现 cp 命令的程序基础上修改 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define CPS     10	// character per second
#define BUFSIZE CPS


static void alrm_handler(int s) {
    alarm(1);	// 为了实现隔一秒触发一次信号
		// 每当处理上一信号,则开始计时准备下一次信号
}

int main(int argc, char **argv) {
    int sfd, dfd = 1;
    char buf[BUFSIZE];
    int len, ret, pos;

    if(argc < 2) {
        fprintf(stderr, "Usage...\n");
        exit(1);
    }

    // 注意顺序,先注册,再alarm
    signal(SIGALRM, alrm_handler);
    alarm(1);
	
    do {
        if((sfd = open(argv[1], O_RDONLY)) < 0) {
            if(errno != EINTR) {
                perror("open()");
                exit(1);
            }
        }
    } while(sfd < 0);	// 确保成功打开文件
	
    while(1) {

        // 用pause,防止CPU空转
        pause();

        while((len = read(sfd, buf, BUFSIZE)) < 0) {
            if(errno == EINTR)
                continue;
            perror("read()");
            break;
        }    // 确保读取
		
        if(len == 0)
            break;

        pos = 0;
        while(len > 0) {
            if((ret = write(dfd, buf + pos, len)) < 0) {
                if(errno == EINTR)
                    continue;
                perror("write()");
                exit(1);
            }
            pos += ret;
            len -= ret;
        }    // 保证写入len字节
    }

    close(sfd);
    exit(0);
}


7.5.3 令牌桶实现 

定义:系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。当桶满时,新添加的令牌被丢弃或拒绝

如果不好理解,可以想想炉石传说 

没玩过可以查查游戏规则,反正下面是游戏界面

每一回合系统都会为玩家增加一个可用水晶。而如果需要打出卡牌,则需要从水晶栏中扣除对应的水晶数量。当水晶栏里没有水晶时,无法打出卡牌(这里不考虑 0 费卡)。当水晶栏满时,下一回合也不会再增加水晶数了

需求:实现一个显示文件内容到标准输出的程序。要求每显示 10 个字符需要消耗一个令牌,而每秒我们会往桶中增加一个令牌。令牌最多能储存 100 个

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define APS     10      // 每秒令牌增加的个数
#define BUFSIZE 10    // 每消耗一个令牌能够显示的字符个数
#define BURST 100       // 桶上限

static volatile int token = 0;  // 桶中令牌数


static void alrm_handler(int s) {
        alarm(1);       // 为了实现隔一秒触发一次信号
                        // 每当处理上一信号,则开始计时准备下一次信号
        token++;    // 每秒增加一个令牌
        if (token > BURST)
                token = BURST;    // 令牌最多储存100个
}

int main(int argc, char **argv) {
        int sfd, dfd = 1;
        char buf[BUFSIZE];
        int len, ret, pos;

        if(argc < 2) {
                fprintf(stderr, "Usage...\n");
                exit(1);
        }

        // 注意顺序,先注册,再alarm
        signal(SIGALRM, alrm_handler);
        alarm(1);

        do {
                if((sfd = open(argv[1], O_RDONLY)) < 0) {
                        if(errno != EINTR) {
                                perror("open()");
                                exit(1);
                        }
                }
        } while(sfd < 0);       // 确保成功打开文件

        while(1) {

                // 用pause,防止CPU空转
                while (token <= 0)
                        pause();
                token--;        // 用掉一个令牌

                while((len = read(sfd, buf, BUFSIZE)) < 0) {
                        if(errno == EINTR)
                                continue;
                        perror("read()");
                        break;
                }    // 确保读取

                if(len == 0)
                        break;

                pos = 0;
                while(len > 0) {
                        if((ret = write(dfd, buf + pos, len)) < 0) {
                                if(errno == EINTR)
                                        continue;
                                perror("write()");
                                exit(1);
                        }
                        pos += ret;
                        len -= ret;
                }    // 保证写入len字节
        }

        close(sfd);
        exit(0);
}

令牌桶与漏桶的主要区别在于漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。在令牌桶算法中,只要令牌桶中存在令牌,那么就允许突发地传输数据直到达到用户配置的门限,因此它适合于具有突发特性的流量


拓展1:令牌桶封装成库

上述介绍的令牌桶是种很好的流量控制方案,可能以后还需要用到,因此我们希望将其封装成库。未来通过这个库就能够实现流量控制 

库是给人调用的,当然要给人提供一些良好的接口

【APUE】并发 — 信号_第18张图片

此外,用户可能需要使用多个流量控制的方案,即需要用到多个桶。而不同的令牌桶参数就代表着不同的桶,也就代表着不同的流量控制方案。因此我们需要将多套不同的令牌桶参数储存下来,方便直接使用

【APUE】并发 — 信号_第19张图片

接下来分别进行代码实现

mytbf.h 

.h 文件主要声明了暴露给用户的接口 

#define MYTBF_MAX 1024
typedef void mytbf_t;

// 创建令牌桶,初始化令牌桶参数并返回。返回的一套令牌桶参数能够表征一个桶
mytbf_t * mytbf_init(int aps, int burst);

// 在特定令牌桶中中获取令牌token,返回实际获取到的个数
int mytbf_fetchtoken(mytbf_t *, int);

// 在特定令牌桶中归还令牌token,返回实际归还的个数
int mytbf_returntoken(mytbf_t*, int);

// 删除桶
int mytbf_destroy(mytbf_t *);

mytbf.c

.c 文件用于接口函数的具体实现 

#include 
#include 
#include 
#include 
#include 

#include "mytbf.h"

static struct mytbf_st* job[MYTBF_MAX]; // 用于存放令牌桶的数组。最多存放MYTBF_MAX个桶
                                        // 表示最多存在MYTBF_MAX套方案

static int inited = 0;  // 确保模块仅加载一次

typedef void (*sighandler_t)(int);
static sighandler_t alrm_handler_save;  // 保存SLGALRM的原行为

struct mytbf_st {

        int aps;        // 每秒增加的令牌数
        int burst;      // 每个桶中的令牌个数上限
        int token;      // 令牌个数
        int pos;        // 这个桶存放在数组的哪个位置

};

static int get_free_pos() {     // 工具函数,获取数组中的空位置来存放令牌桶

        for (int i = 0; i < MYTBF_MAX; ++i)
                if (job[i] == NULL)
                        return i;
        return -1;

}

static int min(int lhs, int rhs) {      // 工具函数,求最小值

        return lhs < rhs ? lhs : rhs;

}

static void alrm_handler(int s) {       // 信号处理函数

        alarm(1);       // 为了实现隔一秒触发一次信号
                        // 每当处理上一个信号,则开始计时准备下一次信号

        for (int i = 0; i < MYTBF_MAX; ++i) {   // 不同的桶共用一个倒计时器
                                                // 故处理一个信号应该对所有桶中的token都加aps
                if (job[i] != NULL) {
                        job[i]->token += job[i]->aps;
                        if (job[i]->token > job[i]->burst)
                                job[i]->token = job[i]->burst;
                }
        }

}

static void module_unload() {   // 模块卸载
                                // 为什么叫模块?一个好的编程习惯是,将我们写的代码看成一个软件项目下的一个子模块......
                                // 模块卸载是为了使调用我们的模块后,环境能够恢复到和调用模块前一样
        signal(SIGALRM, alrm_handler_save);     // 恢复SIGALRM的行为
        alarm(0);       // 关闭计时功能
        for (int i = 0; i < MYTBF_MAX; ++i)     // 释放空间。注意free(NULL)是合法的
                free(job[i]);

}

static void module_load(void) { // 模块加载
                                // 模块加载表示做进入我们的模块之前的预处理。只能调用一次
        alrm_handler_save = signal(SIGALRM, alrm_handler);      // 保存SIGALRM的原始行为,便于之后模块卸载
        alarm(1);       // 打开倒计时功能
        atexit(module_unload);  // 如果exit,也需要卸载模块,故将unload注册为钩子函数

}



// 创建令牌桶,返回一套令牌桶参数来表征一个桶
mytbf_t * mytbf_init(int aps, int burst) {

        struct mytbf_st * me;

        if (!inited) {  // 再次思考为什么只能加载模块一次?
                        // 因为这个函数是为了创建令牌桶
                        // 如果创建多个桶,加载多次会导致之前设置的倒计时器被覆盖
                module_load();
                inited = 1;
        }

        int pos = get_free_pos();
        if (pos < 0)
                return NULL;

        me = malloc(sizeof(*me));
        if (me == NULL)
                return NULL;
        me->token = 0;
        me->aps = aps;
        me->burst = burst;
        me->pos = pos;
        job[pos] = me;  // 将创建的桶放进数组的空位置
        return me;

}

// 在特定令牌桶中中获取令牌token
int mytbf_fetchtoken(mytbf_t *ptr, int size) {

        if (size <= 0)
                return -EINVAL; // 这样做的好处是为了:用户可以通过EINVAL获取对应的错误提示字符串
        struct mytbf_st *me = ptr;      // 别忘了mytbf_t*是void*,struct mytbf_st*才是结构体指针

        while (me->token <= 0)
                pause();
        int n = min(me->token, size);   // 希望获取size个令牌,但是桶中令牌个数是me->token
                                        // 能获取到的应该是size与me->token中的最小值
        me->token -= n;
        return n;

}

// 在特定令牌桶中归还令牌token
int mytbf_returntoken(mytbf_t *ptr, int size) {

        if (size <= 0)
                return -EINVAL;
        struct mytbf_st *me = ptr;

        me->token += size;
        if (me->token > me->burst)
                me->token = me->burst;

        return size;

}

// 删除桶
int mytbf_destroy(mytbf_t *ptr) {

        struct mytbf_st * me = ptr;
        job[me->pos] = NULL;
        free(ptr);
        return 0;

}

main.c 

main.c 主要是模拟用户,用于演示我们封装出的令牌桶的使用方法。这里我们对 7.5.3 中的代码进行重构 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include "mytbf.h"
#include 
#define APS     10      // 每秒令牌增加的个数
#define BUFSIZE 10      // 消耗一个令牌能够打印的字符数
#define BURST 100       // 桶上限


int main(int argc, char **argv) {
        int sfd, dfd = 1;
        char buf[BUFSIZE];
        int len, ret, pos;

        if(argc < 2) {
                fprintf(stderr, "Usage...\n");
                exit(1);
        }


        void *tbf = mytbf_init(APS, BURST);
        if (tbf == NULL)
        {
                fprintf(stderr, "mytbf_init() failed!\n");
                exit(1);
        }

        do {
                if((sfd = open(argv[1], O_RDONLY)) < 0) {
                        if(errno != EINTR) {
                                perror("open()");
                                exit(1);
                        }
                }
        } while(sfd < 0);       // 确保成功打开文件

        while(1) {

                int token = mytbf_fetchtoken(tbf, 1);   // 获取1个令牌
                if (token < 0)
                {
                        fprintf(stderr, "mytbf_fetchtoken():%s\n", strerror(-token));   // 用上了函数返回的errno
                        exit(1);
                }

                while((len = read(sfd, buf, BUFSIZE)) < 0) {
                        if(errno == EINTR)
                                continue;
                        perror("read()");
                        break;
                }    // 确保读取

                if(len == 0) {  // 一个字符没读到,归还令牌
                                // 如果读到字符了,哪怕不满BUFSIZE,也要消耗令牌
                        mytbf_returntoken(tbf, 1);
                        break;
                }

                pos = 0;

                while(len > 0) {
                        if((ret = write(dfd, buf + pos, len)) < 0) {
                                if(errno == EINTR)
                                        continue;
                                perror("write()");
                                exit(1);
                        }
                        pos += ret;
                        len -= ret;
                }    // 保证写入len字节
        }

        close(sfd);
        mytbf_destroy(tbf);

        exit(0);
}

我们设置的每秒增加 10 个令牌,消耗一个令牌能够打印 10 个字符。因此,最终的效果就是每秒打印 100 个字符,如下

【APUE】并发 — 信号_第20张图片

补充:之前代码里面,token 令牌的类型是 int,可以将 int 换成另一个数据结构:sig_atomic_t,这个类型是信号原子类型,操作系统能够保证这个类型变量的取值和赋值操作一定是由一条机器指令能够执行完毕的原子操作 


拓展2:多计时器封装成库

众所周知,使用 alarm 时只能有一个倒计时器。如果我们希望这样一个场景:在某个时刻,我们希望在过 3s 后,过 10s 后,过 15s 后分别均收到信号,该怎么实现?也就是说我们希望能够利用单一倒计时器实现任意数量倒计时器的功能。这个功能很有用,我们也准备将其封装成库

先看看我们希望的效果,用伪码表示

static void f1(void *p)
{
        printf("f1():%s", p);
}
static void f2(void *p)
{
        printf("f2():%s", p);
}

int main() {
        // 显示效果:
        // Begin!End!..f2():bbb...f1():aaa..f1():ccc..............

        puts("Begin!");

        /*
         * 在此处实现三个计时器的功能
         * 5秒后通过f1打印"aaa"
         * 2秒后通过f2打印"bbb"
         * 7秒后通过f1打印"ccc"
         */

        puts("End!");

        while (1) {
                write(1, ".", 1);
                sleep(1);
        }

        exit(0);
}

思考:如何实现上述功能呢?

可以把         

  • 5 秒后通过 f1 打印 "aaa"
  • 2 秒后通过 f2 打印 "bbb"
  • 7 秒后通过 f1 打印 "ccc"

看成三个不同的任务。每个任务需要维护自己的剩余秒数、调用的函数及传入函数的参数。每个任务的剩余秒数都会随着时间的推迟而不断减少。只要剩余秒数减到 0,就通过参数调用函数

【APUE】并发 — 信号_第21张图片

我们可以通过一个结构体来描述单个任务的剩余秒数、调用的函数、传入的参数。因为有多个任务,因此我们将描述这些任务的结构体的指针都存放在数组中

【APUE】并发 — 信号_第22张图片

下面仅给个接口的框架,具体实现暂略

anytimer.h(框架)


7.6 abort

man 3 abort

#include 

void abort(void);

功能:给当前进程发送 SIGABRT 信号

主要目的是人为制造一个异常,杀掉当前进程,得到一个进程的出错现场并存到 core 文件中 


7.7 system 

之前其实都讲过这个函数,可看作是 fork、execl、wait 的简单封装

我们在这里补充一下:

        During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored,  in  the process that calls system().

如何 ignore 一个信号我们知道(调用 signal 传入 SIG_IGN),那么如何 block 一个信号呢?将在后面进行讲解 


7.8 sleep 

man 3 sleep

#include 

unsigned int sleep(unsigned int seconds);

功能:以秒为单位的休眠 

之前说过应该慎用 sleep,因为有些环境下 sleep 是由 alarm + pause 封装而成的。不过在 LINUX 环境下,sleep 是通过 nanosleep 封装实现的


7.9 nanosleep

man 2 nanosleep

#include 

int nanosleep(const struct timespec *req, struct timespec *rem);

功能:以纳秒为单位的休眠


7.10 usleep

man 3 usleep

#include 

int usleep(useconds_t usec);

:以微秒为单位的休眠


7.11 select

man 2 select

通过适当设置这个函数传入的参数,也能实现安全可靠的微秒级休眠。但是这个函数本来的功能是同步多路复用,休眠只是副作用


八、信号集

8.1 概念

顾名思义,信号的集合

一个信号集能表示多个信号

某个信号集的数据类型是 sigset_t

问:类型为 sigset_t 的一个变量是如何表示多个信号的集合的?  

说白了,这个类型其实就是一个整型,不过要以位图的视角去看:其不同位表征了不同信号。某位置为 1 表示特定信号在该集合中;某位置为 0 表示特定信号不在该集合中

【APUE】并发 — 信号_第23张图片

8.2 相关函数

man 3 sigemptyset

#include 

// 将set所指定的信号集中的信号清空
int sigemptyset(sigset_t *set);

// 将所有信号都添加到set所指定的信号集中
int sigfillset(sigset_t *set);

// 将signum所指定的信号添加到set所指定的信号集中
int sigaddset(sigset_t *set, int signum);

// 从set所指定的信号集中删除signum所指定的信号
int sigdelset(sigset_t *set, int signum);

// 查看signum所指定的信号是否存在于set所指定的信号集当中
int sigismember(const sigset_t *set, int signum);

九、信号屏蔽字/pending集的处理 

这部分要回想一下之前标准信号响应过程那部分画的几幅图

这里只搬一张图过来,里面包含了我们需要关注的:mask 和 pending 集

【APUE】并发 — 信号_第24张图片

在这里,我们需要说明一点:可以把信号屏蔽字 mask 也看成一个信号集!这样一来,相当于内核维护了两个信号集

  • mask 信号集:未被屏蔽的信号构成的信号集
  • pending 信号集:接收到但是还未被处理的信号构成的信号集

9.1 sigprocmask 

9.1.1 函数功能介绍

man 2 sigprocmask

#include 

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能:设置信号屏蔽字 mask 信号集

  • how — 想做什么操作。其值及含义如下
含义

SIG_BLOCK

屏蔽信号集 set 中的信号(设置 mask 对应到信号集 set 中信号的位为 0

SIG_UNBLOCK

解除屏蔽信号集 set 中的信号(设置 mask 对应到信号集 set 中信号的位为 1

SIG_SETMASK

设置 mask 信号集为 set(设置后,仅 set 中的信号未被屏蔽
  • set — 指定一个信号集
  • oldset — 用于保存设置前的 mask 信号集

9.1.2 代码示例 

我们希望每秒打印一个 *,并排布成 20 乘 5 的 * 方阵

【APUE】并发 — 信号_第25张图片

  • 特殊要求1:我们希望定制我们自己的操作(打印一个感叹号)来处理 SIGINT 信号
  • 特殊要求2:我们希望在即将换行的时候才执行我们的信号处理函数,行间不响应信号
  • 实现方式 1:利用 SIG_UNBLOCK 手动解除屏蔽
#include 
#include 
#include 
#include 

static void handler(int s) {
        write(1, "!", 1);
        return;
}

int main() {

        signal(SIGINT, handler);    // SIGINT信号可通过ctrl+c发送给该进程

        sigset_t set;

        sigaddset(&set, SIGINT);    // 向集合中添加SIGINT信号

        for (int j = 0; j < 20; ++j) {
                sigprocmask(SIG_BLOCK, &set, NULL);    // 屏蔽set中的信号
                for (int i = 0; i < 5; ++i) {
                        write(1, "*", 1);       // 往终端打印一个字符
                        sleep(1);    // 阻塞1s
                }
                sigprocmask(SIG_UNBLOCK, &set, NULL);    // 解除屏蔽set中的信号
                write(1, "\n", 1);
        }

        exit(0);
}
  • 实现方式2:记录原 mask 信号集,通过恢复至原 mask 信号集解除屏蔽 
#include 
#include 
#include 
#include 

static void handler(int s) {
        write(1, "!", 1);
        return;
}

int main() {

        signal(SIGINT, handler);    // SIGINT信号可通过ctrl+c发送给该进程

        sigset_t set, oldmask;

        sigaddset(&set, SIGINT);

        for (int j = 0; j < 20; ++j) {
                sigprocmask(SIG_BLOCK, &set, &oldmask);    // 将原mask信号集记录下来
                for (int i = 0; i < 5; ++i) {
                        write(1, "*", 1);       // 往终端打印一个字符
                        sleep(1);    // 阻塞1s
                }
                sigprocmask(SIG_SETMASK, &oldmask, NULL);    // 将现mask信号集恢复至原mask信号集
                write(1, "\n", 1);
        }

        exit(0);
}

【APUE】并发 — 信号_第26张图片

9.1.3 思考

思考 1:哪种方式更好?

答:方式 2 更好。假如把我们写的代码看作是一个大项目里面的小模块,方式 2 能够确保进出我们的模块前后,mask 信号集未改变;

而方式 1 只能确保出我们的模块时,SIGINT 信号未被屏蔽。万一进入我们的模块之前,SIGINT 已经被屏蔽,那么调用我们的模块后,mask 信号集就被改变了

思考 2:为什么发送多个信号,换行时仅处理一次?

答:在处理特定信号之前,重复收到同一个信号相当于不断置 pending 的某位为 1,这在操作系统看来,和仅收到一个信号是等同的效果,它无法知道其实这是收到了多个信号的结果


9.2 sigpending 

9.2.1 函数功能介绍

man 2 sigpending

#include 

int sigpending(sigset_t *set);

功能:获取 pending 未决信号集

  • set —  用于保存获取到的 pending 集。该函数会进入内核,取出 pending 信号集的值,并将其回填至 set 所指向的位置

9.2.2 思考

这个函数很少用!主要有下面几个原因 

注意到函数会扎进内核取 pending 信号集,获取到 pending 之后必定存在从内核态返回用户态的这个过程,而这个过程会对信号进行响应处理。响应完信号后,才回到原程序控制流,此时之前获取到的 pending 信号集中的信号可能已经被处理了

即使在调用该函数的过程中屏蔽掉所有信号,成功获取到了处理响应之前的 pending 信号集,又有什么用呢?没有任何接口可以在内核中设置 pending!内核中的 pending 表示有哪些收到的信号尚未被处理,其值是由系统维护的,程序员不能随意设置


十、扩展函数

10.1 setitimer && setitimer

升级版的 alarm。不过相对于 alarm 而言,这组函数能够提供更高精度和多种计时方式,可以平替掉 alarm

还是先查看手册:man 2 setitimer 

#include 

int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value,
              struct itimerval *old_value);

功能: 获取或者设置倒计时器的值

  • which — 指定倒计时器的类型。which 的值及含义如下
含义

ITIMER_REAL

倒计时器一直运行(即关注的是真实世界中的时间),计时结束发出 SIGALRM

ITIMER_VIRTUAL

倒计时器仅当进程在用户态运行时运行(即仅关注用户态时间),计时结束发出 SIGVTALRM

ITIMER_PROF

倒计时器仅当进程运行时运行(即仅关注进程运行的时间),计时结束发出 SIGPROF
  • new_value — 用于设置倒计时器参数
  • old_value — 用于保存旧的倒计时器参数,旧的参数会回填至 old_value 所指结构体
  • curr_value — 用于储存获取到的倒计时器参数,获取的参数会填进 curr_value 所指结构体
  • 成功返回 0;失败返回 -1 并设置 errno

我们可以看到,倒计时器参数都是封装到结构体 struct itimerval 中的。这个结构体中到底保存了哪些倒计时器参数呢?

struct itimerval {
    struct timeval it_interval; /* Interval for periodic timer */
    struct timeval it_value;    /* Time until next expiration */
};
// 如何实现周期性发信号?
// 当递减it_value至0时,会原子性地将it_interval赋值给it_value,然后继续计时
// 这样一来,相当于每过it_interval就会发出一次信号

struct timeval {
    time_t      tv_sec;         /* seconds */
    suseconds_t tv_usec;        /* microseconds */
};
// 可以看到timeval就是代表了时间。注意其精度是微秒级别的,很精确

10.2 sigsuspend

10.2.1 函数功能介绍 

man 2 sigsuspend

#include 

int sigsuspend(const sigset_t *set);

功能:设置 mask 信号集,并立即阻塞等待信号

  • set - 设置进程当前 mask 信号集为参数 set 指定的信号集 
  • 设置 mask,并立即阻塞等待两步是原子操作 

为什么要有这个函数?下面通过代码示例讲解这个函数的使用场景 


10.2.2 代码示例  

需求:实现不可被 SIGINT 打断的字符打印过程

字符打印完毕后,如果打印字符过程中没收到过 SIGINT,则阻塞等待 SIGINT;否则跳过阻塞


一个初步想法是这样的:在字符打印前屏蔽 SIGINT,在字符打印完毕后解除屏蔽 SIGINT,并 pause 等待信号。这样想是因为以为程序会这样:解除屏蔽后,如果打印字符过程中没收到 SIGINT,则 pause 阻塞等待;如果收到过 SIGINT,pending 集应该有记录,pause 看到后就能跳过阻塞 

【APUE】并发 — 信号_第27张图片

哎?奇怪了,第二次运行过程中,在打印途中发送了 SIGINT 信号,照理来说打印完毕后应该跳过阻塞的。为什么运行结果显示还是发生阻塞了? 

wow,本质原因是因为解除屏蔽pause 等待中间可能穿插很多动作......两个操作非原子操作


怎么办?用上 sigsuspend 就行

#include 
#include 
#include 

static void handler(int s) {
        printf("收到过信号\n");
}

int main() {

        sigset_t set, oldmask;

        sigaddset(&set, SIGINT);
        signal(SIGINT, handler);

        sigprocmask(SIG_BLOCK, &set, &oldmask);

        for (int i = 0; i < 10; ++i) {
                write(1, "*", 1);
                sleep(1);
        }
        write(1, "\n", 1);

        // sigprocmask(SIG_SETMASK, &oldmask, NULL);
        // pause();
        // 下面的语句功能相当于上面两个语句的原子操作
        sigsuspend(&oldmask);

        return 0;
}

【APUE】并发 — 信号_第28张图片


10.3 sigaction

10.3.1 signal 的缺陷

sigaction 是升级版的 signal 函数。为什么需要升级?肯定是因为 signal 函数有缺陷


10.3.1.1 缺陷1 

 多个不同信号将同一不可重入函数作为信号处理函数时,会出问题

如上,在 main 中开辟了堆空间,然后进入长时间的阻塞。需要编写信号处理函数对程序进行必要的收尾工作,并将 SIGINT、SIGQUIT、SIGTERM 均绑定到这个信号处理函数上。这样一来,通过其中任一信号都能调用信号处理函数,然后在信号处理函数中进行必要的收尾工作,具体包括恢复信号之前的行为,以及释放内存。信号也能够打断长时间阻塞,从而使程序正常退出

下面看看有什么问题 

虽然在调用函数处理 SIGINT 的过程中,SIGINT 是被屏蔽了的(mask 对应 SIGINT 的那位为 0),但是 SIGQUIT 没有被屏蔽!又因为这些信号均绑定的同一信号处理函数,因此若此时发送 SIGQUIT,会嵌套调用该信号处理函数!因为该函数是不可重入的,嵌套调用会导致 free 两次堆内存直接逆天   


10.3.1.2 缺陷2 

一言以蔽之:无法控制信号来源。比如有一个小调皮,在电脑上乱敲一通,没准就给某个进程发送了不太妥当的信号......

signal 不能区分信号来源于 user 态(如通过 kill 函数发出信号来源于 user 态)还是来源于 kernel 态(如 alarm 函数发出的信号来源于 kernel 态)


10.3.2 sigaction 功能介绍

man 2 sigaction

#include 

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);

功能:设置程序收到特定信号后,执行的行为

  • signum — 指定某个信号
  • act — 将特定信号的行为设置成 act 所指的 sigaction 结构体
  • oldact — 用于保存调用该函数之前,该信号旧的行为。描述旧行为的 sigaction 结构体会被回填到 oldact 所指空间
  • 成功返回 0;失败返回 -1 并设置 errno

怎么通过 struct sigaction 来描述信号的行为?看看结构体里装了些啥

struct sigaction {

    // 用于指定信号处理函数(一参数版本),与函数signal的参数一样的作用
    void     (*sa_handler)(int);

    // 用于指定信号处理函数(三参数版本)
    void     (*sa_sigaction)(int, siginfo_t *, void *);

    // 信号集,用于指定:在响应当前信号的同时,还希望屏蔽哪些信号
    sigset_t   sa_mask;

    // 特殊要求
    int        sa_flags;

    // 基本不用,略
    void     (*sa_restorer)(void);

};

注意:在有些系统下,sa_handler 与 sa_sigaction 被封装为一个 union,故不要同时设置 sa_handler 与 sa_sigaction,二选一即可

如果不理解 union 是什么东东,请自行查阅以下知识:

  1. union 的特性
  2. 如何通过 union 判断大端小端存储

如何二选一?sa_handler 用法和 signal 函数参数的用法一模一样,使用场景我们很清楚。这里着重介绍一下 sa_sigaction 是什么玩意儿 

void     (*sa_sigaction)(int sig, siginfo_t * info, void *);

在调用该信号处理函数处理某个信号时:

  • 可通过 sig 了解到该信号是什么
  • 可通过 info 了解到该信号的更多信息
  • 第三个参数其实是 ucontext_t 类型的指针,用来保存跳转到信号处理函数之前的进程信号屏蔽字 mask、执行栈和机器寄存器,可结合 setcontext 函数恢复到处理信号前的执行现场。基本不用

具体能了解哪些信息呢?看看 siginfo_t 的内容 

siginfo_t {
        int      si_signo;     /* Signal number */
        int      si_errno;     /* An errno value */
        int      si_code;      /* Signal code */
        int      si_trapno;    /* Trap number that caused 
                                  hardware-generated signal 
                                  (unused on most architectures) */
        pid_t    si_pid;       /* Sending process ID */
        uid_t    si_uid;       /* Real user ID of sending process */
        int      si_status;    /* Exit value or signal */
        clock_t  si_utime;     /* User time consumed */
        clock_t  si_stime;     /* System time consumed */
        union sigval si_value; /* Signal value */
        int      si_int;       /* POSIX.1b signal */
        void    *si_ptr;       /* POSIX.1b signal */
        int      si_overrun;   /* Timer overrun count; 
                                  POSIX.1b timers */
        int      si_timerid;   /* Timer ID; POSIX.1b timers */
        void    *si_addr;      /* Memory location which caused fault */
        long     si_band;      /* Band event (was int in 
                                  glibc 2.3.2 and earlier) */
        int      si_fd;        /* File descriptor */
        short    si_addr_lsb;  /* Least significant bit of address
                                  (since Linux 2.6.32) */
        void    *si_lower;     /* Lower bound when address violation
                                  occurred (since Linux 3.19) */
        void    *si_upper;     /* Upper bound when address violation
                                  occurred (since Linux 3.19) */
        int      si_pkey;      /* Protection key on PTE that caused 
                                  fault (since Linux 4.6) */
        void    *si_call_addr; /* Address of system call instruction
                                  (since Linux 3.5) */
        int      si_syscall;   /* Number of attempted system call
                                  (since Linux 3.5) */
        unsigned int si_arch;  /* Architecture of attempted system call
                                  (since Linux 3.5) */
}          

注意,在某些系统下, siginfo_t 是一个 union,因为那些系统下的信号可能没这么多种字段的信息

 再在下面补一个 man 手册中对 sa_sigaction 的描述


10.3.3 代码示例 

10.3.3.1 定制信号处理过程中所屏蔽的信号 

需求:更改 10.3.1.1 中的代码,希望响应信号的同时,屏蔽 SIGINT、SIGQUIT、SIGTERM

#include 
#include 
#include 
#include 

typedef void (*sighandler_t)(int);	

static int * ptr;
static struct sigaction oldAction_SIGINT;
static struct sigaction oldAction_SIGQUIT;
static struct sigaction oldAction_SIGTERM;

void myexit(int s) {	// 这个s此时就派上用场了:在调用该函数处理信号时,可表征所处理的信号
	if (s == SIGINT)
		printf("\nSIGINT ");
	else if (s == SIGQUIT)
		printf("\nSIGQUIT ");
	else if (s == SIGTERM)
		printf("\nSIGTERM ");
	puts("terminating...");
	sleep(5);

	sigaction(SIGINT, &oldAction_SIGINT, NULL);	// 恢复信号之前的行为
	sigaction(SIGQUIT, &oldAction_SIGQUIT, NULL);
	sigaction(SIGTERM, &oldAction_SIGTERM, NULL);
	free(ptr);	// 操作了堆空间,不可重入
			
	return;
}

int main() {

//	oldHandler_SIGINT = signal(SIGINT, myexit);	// 三个信号绑定到了同一个信号处理函数
//	oldHandler_SIGQUIT = signal(SIGQUIT, myexit);
//	oldHandler_SIGTERM = signal(SIGTERM, myexit);
	
	// 用于指定行为,替换上述三行注释的内容
	struct sigaction sa;	
	sa.sa_handler = myexit;	// 指定信号处理函数
	sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGINT);	
    sigaddset(&sa.sa_mask, SIGQUIT);	
    sigaddset(&sa.sa_mask, SIGTERM);	// 指定响应信号的同时,还希望屏蔽哪些信号	
	sa.sa_flags = 0;	// 无特殊要求
	sigaction(SIGINT, &sa, &oldAction_SIGINT);
	sigaction(SIGQUIT, &sa, &oldAction_SIGQUIT);
	sigaction(SIGTERM, &sa, &oldAction_SIGTERM);

	ptr = (int*)malloc(sizeof(int));
	sleep(100000);	// 在此期间需要通过信号对程序进行收尾工作,信号也会打断阻塞的系统调用
	exit(0);
}

接下来查看效果,并和更改前对比

【APUE】并发 — 信号_第29张图片


10.3.3.2 仅响应 kernel 态的信号

需求:实现一个正计时器,每秒显示距离某个时刻过了多少秒

要求这个计时器不能被用户态发送的 SIGALRM 信号干扰计时的精度

先写一个计时器程序 

#include 
#include 
#include 
#include 
#include 
#include 

static struct sigaction oldAction;
static struct itimerval oldTimer;
static int timer = 0;

static void alrm_action(int sig, siginfo_t *info, void * unused) {
	if (info->si_code != SI_KERNEL)    // si_code表示信号来源
	{
		printf("signal from %d\n", info->si_code);
		return;
	}
	printf("%ds from start, signal from %d\n", ++timer, info->si_code);
}

static void myexit() {
	puts("\nEnd timing!");
	setitimer(ITIMER_REAL, &oldTimer, NULL);    // 恢复原计时器设置
	sigaction(SIGALRM, &oldAction, NULL);    // 恢复SIGALRM的原始行为
	exit(0);    // 正常退出
}

int main() {

	struct sigaction sa;
	sa.sa_sigaction = alrm_action;
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = SA_SIGINFO;    // SA_SIGINFO表示我们用的sa_sigaction而非sa_handler
	sigaction(SIGALRM, &sa, &oldAction);    // 为SIGALRM指定新的行为,保存旧的行为

	struct itimerval it;
	it.it_interval.tv_sec = 1;
	it.it_interval.tv_usec = 0;    // 第一个信号发出之后,再以周期性每秒发一次
	it.it_value.tv_sec = 1;
	it.it_value.tv_usec = 0;    // 1秒后发第一个信号
	printf("Start timing!(pid = %d)\n", getpid()); 
	setitimer(ITIMER_REAL, &it, &oldTimer);    // 开始计时    

	signal(SIGINT, myexit);

	while(1)
		pause();

	exit(0);
}

再写一个程序,能通过 kill() 向指定进程发送源自于 user 态的信号

#include 
#include 
#include 
#include 

int main(int argc, char * argv[]) {
        if (argc < 3) {
                fprintf(stderr, "Usage: %s  \n", argv[0]);
                exit(1);
        }
        if (kill(atoi(argv[1]), atoi(argv[2])) < 0) {
                perror("kill()");
                exit(1);
        }
        exit(0);
}

效果如下

【APUE】并发 — 信号_第30张图片

补充一下:除了 man 手册,哪里还可以看到详细的信号的介绍?

答:

/usr/include/bits/signum.h

十一、实时信号

之前讲的都是标准信号,标准信号可能丢失且标准信号的响应没有严格顺序

  • 丢失:短时间内收到多个相同信号,反映在 pending 集也是某一位重复置 1,最终这一批信号只会被处理一次
  • 无严格顺序:多位 pending 为 1,响应顺序不确定 

而实时信号不同,相比于标准信号,实时信号主要具有以下特点

  • 实时信号规定了处理顺序,signum 越小的信号就会被越先处理,相同信号则先到的先被处理
  • 信号不会丢失,哪怕短时间内收到多个相同信号,也会进入队列,然后排着队挨个挨个被处理



人麻了。。。。。。为什么一次比一次多。。。。。。日均输出3000字了!!甚至有部分代码还是截图的,字数都没考虑在内

现在的心情:テ_デ  ┭┮﹏┭┮  (;´༎ຶД༎ຶ`)

【APUE】并发 — 信号_第31张图片【APUE】并发 — 信号_第32张图片

虽然也不知道为啥要写,对我来说貌似也没啥用

就当为以后想找的那种需要写很多思想汇报材料的工作累积累积经验吧hhhhh



另外,今天祝老爸生日快乐!!

【APUE】并发 — 信号_第33张图片

画画真累。。。画得我想吃 

【APUE】并发 — 信号_第34张图片 【APUE】并发 — 信号_第35张图片


你可能感兴趣的:(UNIX环境高级编程,服务器,1024程序员节,linux,c++)