1.信号的概念
在这里要给出一个信号的准确概念感觉很困难,可以这么说,信号就是进程之间或者内核与进程间异步通信的一种机制,有点类似于中断的性质。在 linux 系统中有 31 种信号,每一种信号都以 SIG 三个字母开头,例如 SIGABRT 是夭折信号,就是调用 abort 函数产生的信号,SIGALRM 是调用 alarm 函数定时时间溢出后产生的信号。
对每一个信号,系统有三种处理方式:
1.忽略这种信号,就是对这种信号不做任何处理。但是,对于 SIGKILL 和 SIGSTOP 信号,却不能忽略。因为这两种信号,通常被操作系统用来终止失去控制的进程。 2.捕捉该信号。当一个进程收到该信号时,就调用相应的信号处理函数,来对该信号做出相应的动作。 3.按系统默认方式处理信号,一般的默认动作是终止程序。
2.signal函数
当需要捕捉某种信号时,需要用到 signal 函数来注册处理该信号的函数。这个 signal 函数比较特殊,它的返回值是一个函数指针,这个函数指针指向之前处理该信号的函数。它有两个参数,第一个参数用来指定信号,第二个参数用来指定处理该信号的函数。它的函数原型如下:
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
举一个简单的 signal 的使用例子如下:
#include <signal.h> #include <stdio.h> static void sig_usr(int signo); int main(void) { if(signal(SIGUSR1,sig_usr)==SIG_ERR) { printf("signal(SIGUSR1) error\n"); return -1; } if(signal(SIGUSR2,sig_usr)==SIG_ERR) { printf("signal(SIGUSR2) error\n"); return -1; } for(;;) { pause(); } return 0; } static void sig_usr(int signo) { if(signo==SIGUSR1) { printf("received SIGUSR1\n"); } else if(signo==SIGUSR2) { printf("received SIGUSR2\n"); } else { printf("received signal:%d\n",signo); } }将程序编译为 a.out 之后,用下面的命令在后台运行:
./a.out &
在终端上输出程序的PID如下:
[1] 6212我们输入如下命令:
kill -SIGUSR1 6212
可以看到程序输出
received SIGUSR1
3.可再入函数
在信号处理的过程中会产生一些问题,例如,当在 main 函数中正在调用 malloc 函数在堆上动态分配内存空间的时候,产生了一个信号,需要去执行这个信号的信号处理函数。而在这个信号处理函数中,也需要调用 malloc 函数,这时候就有可能发生问题。因为 malloc 为它分配的存储区保存一个链接表,而如果在main函数处理这张链接表时,在信号处理函数调用了 malloc,就使进程遭到了破坏。
再举一个例子,如果在 main 函数中,刚刚调用完 getpwnam 函数,这时候来了一个信号,需要去执行信号处理函数,在这个函数中也许要调用 getpwnam 函数,这就造成了 main 函数调用 getpwnam 得到的信息丢失。
通过上面的两个例子产生的问题,linux 规定了一个可再入函数表,这个表中列举了在信号处理函数中可以调用的函数。当然这张表中肯定没有包括 malloc 函数和 getpwnam 函数,这些函数有另一个名字,叫不可再入函数。
就算对于可再入函数,我们知道每一个进程只有一个 errno ,如果在 main 函数中设定了 errno 的值以后,如果在信号处理程序中调用的某个可再入函数也可能会修改了 errno 的值。所以要求我们在信号处理程序前保存现场,在信号处理程序要结束时,恢复现场。
4.kill和raise以及几个术语
kill函数用来向指定的进程或者进程组发送信号,raise函数用来向进程自己发送信号。进程只能向和它所有者相同的进程发送信号,当然超级用户进程可以向各个进程发送信号。
信号的产生:当造成信号的事件发生时,为进程产生一个信号。
当产生了信号以后,内核会在进程表中设置某种形式的标记,这个过程称为信号递送。
在信号产生和信号递送这个时间,称为信号未决。
5.alarm函数和pause函数
alarm 函数用来定时一段时间,当定时时间溢出时,会产生 SIGALRM 信号。pause 函数用来使进程挂起,直到捕捉到一个信号,并且等待执行完信号处理函数之后,pause 函数才会返回 -1 。它们的函数原型如下:
#include <unistd.h> unsigned int alarm(unsigned int seconds); int pause(void);alarm 函数的参数单位是秒,它的返回值是上一次调用 alarm 时剩余的时间。当参数 seconds 为 0 时,如果上次的定时时间还未到,则取消上次的定时,这是一个很常用的功能。
alarm 和 pause 函数可以用来实现sleep函数,如下:
#include <signal.h> #include <stdio.h> static void sig_alrm(int signo) { return; } unsigned int sleep1(unsigned int nsecs) { if(signal(SIGALRM,sig_alrm)==SIG_ERR) { printf("signal(SIGALRM) error\n"); return nsecs; } alarm(nsecs); pause(); return (alarm(0)); } int main(void) { printf("before sleep1\n"); sleep1(2); printf("after sleep1\n"); return 0; }
在这个实现的函数中存在着一个竞争条件,当执行完 alarm(nsecs) 之后,可能还没有执行 pause 时,定时时间已经到了。这时执行 sig_alrm 信号处理函数,之后再执行 pause,如果系统中再没有信号产生,这就会使进程一直挂起。虽然这种情况极少发生,但是这总是一个 bug,当它发生错误时,会很难查找。可以用 setjmp 和 longjmp 来消除这个潜在的bug:
#include <stdio.h> #include <signal.h> #include <setjmp.h> static jmp_buf env_buf; static void sig_alrm(int signo) { longjmp(env_buf,1); } unsigned int sleep2(unsigned int nsecs) { if(signal(SIGALRM,sig_alrm)==SIG_ERR) { printf("signal(SIGALRM) error\n"); return nsecs; } if(setjmp(env_buf)==0) { alarm(nsecs); pause(); } return (alarm(0)); } int main(void) { printf("before sleep1\n"); sleep2(2); printf("after sleep1\n"); return 0; }
这样,即使在还没有执行 pause 时,时间就已经溢出了,也不会发上上面的情况。alarm 函数还用来为会阻塞的操作定时,防止它们进入永久阻塞的状态。这个功能类似于看门狗的功能,主要防止程序跑飞。