APUE读书笔记---第10章 信号

    xiaoxiao2021-03-25  89

    APUE读书笔记—第10章 信号

    1. 信号概念

    信号是软件中断,信号提供了一种处理异步事件的方法。信号名在 < signal.h > 都被定义为正整数常量。不存在编号为0的信号。

    1.1 产生信号的条件:

    当用户按某写终端按键时,引发终端产生的信号。如:Ctrl+C产生SIGINT信号。硬件异常产生信号。如除零错,无效的内存应用产生SIGSEGV信号。进程调用kill函数可将任一信号发送给另一个进程或进程组。用户可用kill命令将信号发送给其他进程。当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。如SIGPIPE在管道的读进程已终止后,一个进程写此管道。

    1.2 信号的处理

    忽略此信号。SIGKILL和SIGSTOP不能被忽略捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数执行默认的动作。Linux对每种信号都规定了默认操作。默认的动作共分为5种情况:(man 7 singal) Term表示终止当前进程Core表示终止当前进程并且Core Dump(Core Dump 用于gdb调试).Ign表示忽略该信号.Stop表示停止当前进程.Cont表示继续执行先前停止的进程.

    2. 信号集处理函数

    2.1 信号集(signal set)

    一个能表示多个信号的数据类型。

    #include <signal.h> int sigemptyset(sigset_t *set); //初始化由set指向的信号集,清除其中所有信号 int sigfillset(sigset_t *set); //初始化由set指向的信号集,使其包括所有信号。 int sigaddset(sigset_t *set, int signum); //将一个信号添加到已有的信号集 int sigdelset(sigset_t *set, int signum); //从信号集删除一个信号 //4个函数返回值:若成功,返回0,若出错,返回-1 int sigismember(const sigset_t *set, int signum); //测试signum是否在set信号集中 //返回值:若真,返回0,若假,返回0

    2.2 函数sigprocmask

    调用sigprocmask可以检测或更改或同时进行检测和更改进程的信号屏蔽字。

    #include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); //返回值:若成功,返回0,若出错,返回-1 oldset是非空指针,那么进程的当前信号屏蔽字通过oldset返回。set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。sigprocmask函数仅为单线程进程定义。 how说明SIG_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=maskSIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~setSIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

    2.3 函数sigpending

    sigpending函数返回一个信号集,对于调用进程而言,其中的各信号是阻塞不能传递的,因而也一定是当前未决的。

    #include <signal.h> int sigpending(sigset_t *set); //返回值:若成功,返回0,若出错,返回-1

    2.4 信号集处理函数的例子

    #include <stdio.h> #include <signal.h> #include <unistd.h> void printsigset(const sigset_t *set) { int i; for(i = 1; i < 32; i++){ if(sigismember(set, i) == 1){ //测试set的第i为是否为1 putchar('1'); }else{ putchar('0'); } } puts(""); //换行 } int main() { sigset_t s, p; int i = 0; sigemptyset(&s); //init sigaddset(&s, SIGINT); //add SIGINT to sigset sigaddset(&s, SIGQUIT); //add SIGQUIT to sigset sigprocmask(SIG_BLOCK, &s, NULL); //block SIGINT printf("64bit sigset_t类型的大小为:%ld\n", sizeof(s)); while(1){ sigpending(&p); //get pending signals printsigset(&p); //print pending sigset if(i == 10){ sigdelset(&s, SIGQUIT); //删除SIGQUIT=0信号,只有SIGINT=1信号 sigprocmask(SIG_UNBLOCK, &s, NULL);//不阻塞SIGINT信号,进程被SIGQUIT信号结束 } i++; sleep(1); } return 0; }

    首先构造一个信号屏蔽字,然后添加要阻塞的信号,读出当前进程的未决信号集,调用sigprocmask函数更改进程的信号屏蔽字 运行结果:

    ➜ SIGNAL ./sigset 64bit sigset_t类型的大小为:128 0000000000000000000000000000000 0000000000000000000000000000000 //未决信号集为空。 ^C0100000000000000000000000000000 //Ctrl + C 发送SIGINT信号,SIGINT信号被阻塞,并打印未决信号集,下标为1的位被置1 0100000000000000000000000000000 0100000000000000000000000000000 ^\0110000000000000000000000000000 //Ctrl + \ 发送SIGQUIT信号,SIGQUIT信号被阻塞,并打印未决信号集,下标为2的位被置1 0110000000000000000000000000000 0110000000000000000000000000000 0110000000000000000000000000000 0110000000000000000000000000000 0110000000000000000000000000000 //10s之后SIGQUIT信号被清除,SIGINT进程被阻塞,进程因SIGQUIT信号而退出

    3. 信号捕捉设定

    函数sigaction的功能是检查、修改与指定信号相关联的处理动作

    #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); //返回值:若成功,返回0,若出错,返回-1 参数signo是要检测或修改其具体动作的信号编号。oldact返回信号的上一个动作,act指定要修改的动作。

    sigaction的结构如下:

    struct sigaction { void (*sa_handler)(int); //捕捉函数 void (*sa_sigaction)(int, siginfo_t *, void *); //新添加的捕捉函数,可以传参 , 和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数 sigset_t sa_mask; //在执行捕捉函数时,设置阻塞其它信号,sa_mask | 进程阻塞信号集,退出捕捉函数后,还原回原有的 阻塞信号集 int sa_flags; //SA_SIGINFO 或者 0 };

    3.1 信号捕捉例子

    #include <stdio.h> #include <unistd.h> #include <signal.h> void do_sig(int num) { int n = 5; printf("I am do_sig\n"); while(n--) { printf("num = %d\n", num); sleep(1); } } int main() { struct sigaction act; // act.sa_handler = SIG_DFL; //SIG_DFL执行默认动作 // act.sa_handler = SIG_IGN; //SIG_IGN执行忽略动作 act.sa_handler = do_sig; sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask, SIGQUIT); act.sa_flags = 0; sigaction(SIGINT, &act, NULL); while(1){ printf("**********\n"); sleep(1); } return 0; }

    运行结果:

    ➜ SIGNAL ./sigaction ********** ^CI am do_sig //Ctrl + C 发送SIGINT信号 num = 2 ^C^C^Cnum = 2 //连续多次发送SIGINT信号 ^\num = 2 //Ctrl + \ 发送一次SIGQUIT信号 num = 2 num = 2 I am do_sig num = 2 num = 2 num = 2 num = 2 num = 2 [1] 1485 quit (core dumped) ./sigaction

    程序分析:

    定义一个sigaction的变量,并且将信号捕捉函数的首地址赋值给sa_handler成员。初始化一个信号集,并且添加SIGQUIT信号,因此如果在执行捕捉函数时会阻塞SIGQUIT信号,并设置捕捉的信号为SIGINT。当程序运行起来,当按下Ctrl+C发送一个SIGINT信号时,系统从用户态进入内核态,当内核处理完异常 处理递送的信号时,需要进入用户态调用捕捉函数在调用捕捉函数的执行n–的5秒期间,又发送三次SIGINT信号和一次SIGQUIT信号,但是在执行捕捉函数的期间因为之前设置的信号集会阻塞这些信号。当执行完捕捉函数后,会执行特殊的系统调用sigreturn,再次陷入内核态,然后又从内核态返回上次被中断的地方(while(1)的死循环)继续执行,接着信号屏蔽字会恢复为原先值。因为信号屏蔽字的恢复,上次阻塞的信号再次引发中断,但当同一个信号多次发生,通常不会加入队列当被解除阻塞后,信号处理函数只被调用一次。因此,在执行完第一个I am do_sig后即使多次发送SIGINT但也只会在执行一次I am do_sig。当执行完第二次SIGINT后,又会以相同的方式处理SIGQUIT信号,因为信号集恢复为原先值,因此,进程被SIGQUIT信号终止。

    4. C标准库信号处理函数

    4.1 signal函数

    signal函数由ISO C定义,不最好使用sigaction代替该函数。

    #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); //返回值:若成功,返回以前的信号处理配置,若出错,返回SIG_ERR 第一个参数指定信号的值,第二个参数指定针对前面信号值的处理handler可以是SIG_IGN(忽略该信号)、SIG_DFL(系统默认动作)或者是接到此信号后要调用的函数地址。我们称这种处理为捕捉该信号,称此函数为信号处理程序或信号捕捉函数。如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR

    4.2 signal函数的使用

    #include <stdio.h> #include <signal.h> #include <unistd.h> void do_sig(int n) { printf("SIGINT\n"); } int main(void) { signal(SIGINT, do_sig); while(1) { printf("**************\n"); sleep(1); } return 0; }

    运行结果:

    ➜ SIGNAL ./signal ************** ************** ^CSIGINT //Ctrl + C 发送SIGINT信号 ************** ************** ^CSIGINT //Ctrl + C 发送SIGINT信号 ************** ^\[1] 12618 quit (core dumped) ./signal //发送SIGQUIT退出程序

    4.2system函数

    system函数执行一条shell命令

    #include <stdlib.h> int system(const char *command);

    5.可重入函数

    进程捕捉的信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时终端,它首先执行该信号处理程序中的指令,如果从信号处理程序返回,则继续执行在捕捉的信号时进程正在执行的正常指令序列。但是在返回控制时不会出现任何错误,就叫可重入函数。而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

    例如:

    strtok_r函数

    strtok_r函数是strtok函数的可重入版本,并且线程安全。用来分解一个字符串为一组字符串。

    #include <string.h> char *strtok(char *str, const char *delim); char *strtok_r(char *str, const char *delim, char **saveptr); str是被分割的字符串地址delim函数的为分隔符字符串地址saveptr是指向char *类型的指针变量,用来保存strtok_r内部保存切分时的上下文,以应对连续调用分解相同源字符串。 #include <stdio.h> #include <string.h> int main(void) { char str[] = "everthing will be OK!"; char *saveptr = str, *p; while((p = strtok_r(saveptr, " ", &saveptr)) != NULL) printf("%s\n", p); return 0; }

    运行结果:

    SIGNAL ./strtok_r everthing will be OK!

    可重入函数见man 7 signal

    6. 时序竟态

    #include <unistd.h> int pause(void); //返回-1,并设置errno pause函数使调用进程挂起,直到有信号递达,如果递达信号是忽略,则继续挂起。

    pause函数可以在逻辑上实现简易的sleep函数

    1. signal(SIGALRM, do_sig); 2. alarm(n); 3. pause();

    先设置一个SIGALRM信号,当alarm定时了n秒之后,pause接收到SIGALRM信号,然后返回。 但是,这存在着BUG,因为形成了竟态条件(race condition),会涉及时序竟态的问题。

    竞态条件(race condition),从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。

    当程序执行完第二步alarm函数,此时如果有优先级更高的进程抢占了CPU的使用权,此时该进程就会处于就绪状态,当该进程被拉起时,n秒已经过去,SIGALRM信号已经丢失,那么该进程则会一直处于pause状态。

    为了使信号不丢失,就可是设置信号屏蔽字阻塞。

    1. signal(SIGALRM, do_sig); 2. 阻塞SIGALRM 3. alarm(n); 4. 解除阻塞SIGALRM 5. pause();

    但是在第4和第5步中间,仍然会形成竟态条件。因此需要一个原子操作先恢复信号屏蔽字,然后使进程休眠。而sigsuspend函数提供此功能,因此,就有了sigsuspend函数。

    #include <signal.h> int sigsuspend(const sigset_t *mask); //返回-1,并将errno设置为EINTR

    sigsuspend函数的行为如下(原子操作):

    通过参数mask解除对某个信号的屏蔽挂起进程当被信号唤醒,sigsuspend返回,进程的信号屏蔽字恢复为原来的值。

    6.1 实现sleep函数

    使用sigaction代替signal,使用sigsuspend代替pause

    #include <stdio.h> #include <unistd.h> #include <signal.h> void sig_arm() { } unsigned int mysleep(unsigned int nsecs) { struct sigaction newact, oldact; sigset_t newmask, oldmask, suspmask; unsigned int unslept; //设置信号捕捉函数,保存之前信号动作 newact.sa_handler = sig_arm; sigemptyset(&newact.sa_mask); newact.sa_flags = 0; sigaction(SIGALRM, &newact, &oldact); //阻塞SIGALRM信号,保存当前信号屏蔽字 sigemptyset(&newmask); sigaddset(&newmask, SIGALRM); sigprocmask(SIG_BLOCK, &newmask, &oldmask); alarm(nsecs); suspmask = oldmask; sigdelset(&suspmask, SIGALRM); //参数suspmask设置为取消阻塞SIGALRM信号,保证sigsuspend一定不阻塞SIGALRM,并且休眠进程,同时等待信号发生 sigsuspend(&suspmask); //设置未睡眠够秒数,恢复之前的信号动作 unslept = alarm(0); sigaction(SIGALRM, &oldact, NULL); //进程恢复为原来的信号屏蔽字 sigprocmask(SIG_SETMASK, &oldmask, NULL); return unslept; } int main(void) { int n = 5; while(n--) { mysleep(1); printf("1 sec passed\n"); } }

    运行结果:

    ➜ SIGNAL ./mysleep1 1 sec passed 1 sec passed 1 sec passed 1 sec passed 1 sec passed

    7. SIGCHLD信号处理

    7.1 SIGCHLD信号的产生条件

    子进程终止时子进程接收到SIGSTOP信号停止时子进程处在停止态,接受到SIGCONT后唤醒时

    7.2 使用信号机制通知父进程回收子进程

    #include <stdio.h> #include <signal.h> #include <stdlib.h> #include <sys/wait.h> #include <sys/types.h> #include <unistd.h> void do_sig_child(int signo) { pid_t pid; int status; while ((pid = waitpid(0, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) printf("child %d exit %d\n", pid, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) printf("child %d signal %d\n", pid, WTERMSIG(status)); } } int main(void { pid_t pid; int i; for(i = 0; i < 10; i++) { if ((pid = fork()) == 0) { break; } else if (pid < 0) { perror("fork err"); exit(1); } } if(pid == 0) { int n = 8; while(n--) { printf("child ID:%d\n", getpid()); sleep(1); } } else if (pid > 0) { struct sigaction act; act.sa_handler = do_sig_child; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGCHLD, &act, NULL); while(1) { printf("Parent ID:%d\n\n\n", getpid()); sleep(1); } } return 100; } //WIFEXITED(status):子进程正常exit终止,返回真 //WEXITSTATUS(status)返回子进程正常退出值 //WIFSIGNALED(status):子进程被信号终止,返回真 //WTERMSIG(status)返回终止子进程的信号值

    运行结果:

    child ID:25348 child ID:25347 child ID:25349 child 25340 exit 100 //子进程退出值为进程return 的 100 child 25345 exit 100 child 25346 exit 100 Parent ID:25339 child 25343 exit 100 child 25341 exit 100 Parent ID:25339 child 25344 exit 100 Parent ID:25339 child 25342 exit 100 Parent ID:25339 child 25347 exit 100 child 25348 exit 100 Parent ID:25339 child 25349 exit 100 Parent ID:25339 Parent ID:25339
    转载请注明原文地址: https://ju.6miu.com/read-24288.html

    最新回复(0)