当进程A用信号给进程B发送信号时,进程B一旦收到信号,就会停下正在执行的进程转去处理信号,处理完信号会继续回来执行刚才的进程,可见信号的优先级比较高。
信号有三种状态,分别是产生、未决、递达。信号的产生可以通过按键 ctrl + \ 、ctrl +c ……等方式产生,或者通过系统调用(kill raise abort 后面会说到),未决从字面意思上理解就是未被处决,也就是没有被处理的意思,处于产生和递达的中间状态,递达就是递送并且已经到达进程,已经被处理。
执行默认的处理动作(大部分是终止),忽略信号(丢掉不处理),捕捉信号(注册自定义信号处理函数)
信号的编号,信号的名字,信号的默认处理动作,信号的产生条件(这些都可以在man 7 signal中查看)
阻塞信号集中存放的都是被当前进程阻塞的信号。若当前进程收到的是阻塞信号,这些信号需要被阻塞,不予处理。信号产生后由于某种原因没有被处理,那么这类信号的集合就被称为未决信号集。在屏蔽解除前,信号一直处于未决状态,如果信号从阻塞信号集中解除阻塞,则该信号会被处理,并从未决信号集中去除。
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函数的原型如上,其第一个参数是要注册的信号的值,一般都传入相应的宏,第二个参数是一个回调函数,也就是自己写的处理信号的函数。下面看一个关于signal函数的简单例子。
#include
#include
#include
#include
#include
void handler(int signo)
{
printf("signo=[%d]\n",signo);
}
int main()
{
signal(SIGPIPE,handler);
int fd[2];
int pip=pipe(fd);
if(pip<0)
{
perror("pipe error\n");
return -1;
}
close(fd[0]);
while(1)
{
write(fd[1],"hello",sizeof("hello"));
}
return 0;
}
这个signal注册代码在上次进程之间的通信就已经简单介绍过了,这里是用signal捕捉管道破裂信号,我们关闭了管道的读端,让其一直写入,就会让管道破裂,然后signal函数就会捕捉到这个信号,这个信号的整型值是13,使用的时候还是用SIGPIPE.
屏幕上会一直打印这个,因为设置了循环写,管道破裂后依然继续执行。这里需要注意回调函数的写法,回调函数是有一个参数的。
kill函数的作用是给指定进程发送信号。
第一个参数就是传入要杀死的进程的pid,具体的可以去查看手册,我们最常用的是传入进程的pid,第二个参数就是信号的编号。
利用kill函数可以给进程自己发送一个终止的信号SIGKILL.
如果后面这句话没有执行就说明进程已经收到信号结束运行了 。
使用kill函数的时候需要注意传入不同的参数带来的影响不一样,有些参数需要慎重使用,一不小心可能会将整个控制台程序都给关闭了。
abort函数给当前进程发送SIGABRT信号,并终止当前进程。
#include
#include
#include
#include
#include
void handler(int signo)
{
printf("signo=[%d]\n",signo);
}
int main()
{
signal(SIGABRT,handler);
abort();
printf("----\n");
return 0;
}
我们注册一个信号捕捉函数来捕捉SIGABRT信号,并且abort函数执行后,后面的printf就不会执行了。这个abort函数就相当于kill(getpid(),SIGABRT);
raise函数是给当前进程发送指定信号,相当于kill(getpid(),signo);成功返回0,失败返回非0值。
#include
#include
#include
#include
void handler(int signo)
{
printf("signo=[%d]\n",signo);
}
int main()
{
signal(SIGPIPE,handler);
int ret=raise(SIGPIPE);
printf("----\n");
return 0;
}
和上面的一样注册一个信号捕捉函数来捕捉当前信号,我们就随便给当前进程发送一个SIGPIPE信号。
alarm函数是用来设置定时器的,就可以理解成设置闹钟,在指定seconds之后,内核会给当前进程发送一个SIGALRM信号,进程收到信号后,默认终止。每个进程都有且仅有唯一的一个定时器。函数返回0或者是还剩多少秒。这个函数第一次调用返回0,第二次返回剩余的秒数。
int alarm (int seconds);
下面看一段测试代码:
#include
#include
#include
#include
#include
void handler(int signo)
{
printf("signo=[%d]\n",signo);
}
int main()
{
signal(SIGALRM,handler);
int ret=alarm(9);
printf("%d\n",ret);
sleep(2);
ret=alarm(1);
printf("%d\n",ret);
//alarm(0);//取消设置时钟后面的这句话不会输出
//printf("%d\n",ret);
sleep(10);
return 0;
}
说明一下最后一个sleep的作用,如果不sleep的话程序执行完了直接推出了然后定时器还没触发。这里我们设置了一个9s的定时器,第一次的返回值应该是0,所以第一次打印的值应该是0,第二次返回的就是定时器还剩下多久了,上面休眠了两秒,因此这里输出的是7.
答案和我们想象的一样。注意alarm(0)是取消时钟的意思,取消时钟的话就不会发出SIGALRM信号,因此也不会被阻塞。有一个有趣的测试,可以用来测试你的电脑能在一秒中数数多少次。
#include
#include
#include
#include
#include
#include
int main()
{
int i=0;
alarm(1);
while(1)
{
printf("%d\n",i++);
}
return 0;
}
好像电脑运算速度越快,一秒内会数更多数。
函数原型如下:
int setitimer(int which, const struct itimerval *new_value,
struct itimerval *old_value);
第一个参数which表示指定定时方式,有三个方式:
ITIMER_REAL 计算自然时间,并在最后返回SIGALRM信号。
ITIMER_VIRTUAL 虚拟空间计时,只计算进程占cpu的时间,返回SIGVTALRM信号。
ITIMER_PROF 运行时计时,计算占用cpu和执行系统调用的时间,在最后返回SIGPROF信号。
第二个参数new_value是结构体:
struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
it_interval 表示闹钟出发周期 it_value 表示出发时间 it_sec 秒 it_usec 微秒
最后一个参数传入NULL即可。下面举个例子说明setitimer函数如何使用:
#include
#include
#include
#include
#include
#include
void handler(int signo)
{
printf("HELLO WORLD %d\n",signo);
}
int main()
{
signal(SIGALRM,handler);
struct itimerval tm;
tm.it_interval.tv_sec=1;//设置周期
tm.it_interval.tv_usec=0;
tm.it_value.tv_sec=2;
tm.it_value.tv_usec=0;
setitimer(ITIMER_REAL,&tm,NULL);
while(1)
{
sleep(1);//防止上面打印太快
}
return 0;
}
上面我们设置了一个周期为一的定时器,从第二秒开始,每秒打印一次HELLO WORLD,结构体记不住可以查手册。
上面已经说过了阻塞信号集就当前进程要阻塞的信号的集合,未决信号集就是当前进程暂时没有处理的信号的集合。这连个集合都存在进程的PCB中。就用刚才SIGINT信号来解释一下这俩信号集:当进程收到了SIGINT信号时,首先会将这个进程存在未决信号集中,表示该信号处于未决状态,同时会将当时未决信号集中这个位置置为1,在这个信号需要被处理的时候,首先会查询阻塞信号集中对应的位置是不是1,如果是1,那么这个信号就会被阻塞,这个信号就不会被处理,所以其未决信号集对应的位置也会保持为1,如果不是1,那么当前信号就会被处理,执行默认处理动作,并将未决信号集对应位置置为0。当SIGINT信号从阻塞信号集中解除阻塞后,就会被处理。
下面看看信号集相关函数:首先先了解一下阻塞信号集的创建--sigset_t set 这就创建了一个阻塞信号集,
int sigemptyset(sigset_t *set)把集合中的所有位置为0;初始化
int sigfillset(sigset_t *set)把集合中的所有位置全部都置为1
int sigaddset(sigset_t *set,int sognum) 将某个信号添加到信号集中
int sigdelset(sigset_t *set,int signum) 将某个信号从集合中删除
int sigmismember(sigset_t *set ,int signum) 查看某个信号是否在集合中
int sigprocmask(int how,const sigset_t *set,*oldset)设置阻塞信号集
int sigpending(sigset_t *set) 读取当前进程的未决信号集
最后一个设置阻塞信号集函数中的how,可以传入SIG_BLOCK 把某个信号设置为阻塞,SIG_UNBLOCK 将某个信号解除阻塞 ,SIG_SETMASK 直接进行设置,包括上面两个的功能,具体使用方法看后面的例子。下面看一个实例我们将SIGINT信号加入阻塞信号信号集,然后通过键盘的ctrl+c来产生SIGINT信号,判断这个信号是否在未决信号集中,如果在,就输出1,否则输出0,程序如下:
#include
#include
#include
#include
#include
void handler(int signo)
{
printf("signo=[%d]\n",signo);
}
int main()
{
signal(SIGINT,handler);
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGINT);//将信号加入set中
sigprocmask(SIG_BLOCK,&set,NULL);//将信号加入阻塞信号集
sigset_t pending;
sigset_t oldset;
int i=0;
int j=0;
while(1)
{
sigemptyset(&pending);
sigemptyset(&oldset);
sigpending(&pending);//获取未决信号集中的信号
for(i=1;i<32;i++)
{
if(sigismember(&pending,i)==1)//判断信号是否在那个范围之中
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
//循环十次就解除阻塞,执行信号处理函数
if(j++%10==0)
{
//sigprocmask(SIG_UNBLOCK,&set,NULL);
sigprocmask(SIG_SETMASK,&oldset,NULL);//解除阻塞
}
else
{
sigprocmask(SIG_BLOCK,&set,NULL);
}
sleep(1);
}
return 0;
}
前面将其加入到阻塞信号集中后,后面每循环十次就将其解解除阻塞一次,解除阻塞后,就会处理我们注册的信号处理函数。
观察运行结果我们可以发现,尽管我们在前面输入了很多次信号,但是最终这个信号只会被处理一次,这也就证明了信号不支持排队。
sigaction和signal的作用是差不多的,都是注册信号处理函数,就可以把sigaction当成signal的升级版,sigaction的函数原型如下:
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
第一个参数是你要注册的信号,一般都是传入相应的宏,第二个参数是一个传入参数,一个结构体:
struct sigaction {
void (*sa_handler)(int);//信号处理函数
void (*sa_sigaction)(int, siginfo_t *, void *);//基本不用
sigset_t sa_mask;//信号处理函数执行期间要进行阻塞的函数,就看成一个阻塞信号集
int sa_flags;//通常为0,表示默认标识
void (*sa_restorer)(void);//这个参数几乎用不到
};
下面看一个例子来看一下sigaction的具体用法 :
#include
#include
#include
#include
#include
void handler(int signo)
{
printf("signo=[%d]\n",signo);
sleep(3);
}
int main()
{
struct sigaction act;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);//不阻塞信号就把他设置为空
sigaddset(&act.sa_mask,SIGQUIT);
act.sa_flags=0;
sigaction(SIGINT,&act,NULL);
signal(SIGQUIT,handler);
while(1)
{
sleep(1);
}
return 0;
}
对于sigaction结构体的第sa_mask成员,如果不需要阻塞信号,就直接调用sigemptyset函数将其初始化为全0即可,如果要阻塞信号,那么就需要调用sigaddset函数来将其加入到阻塞信号集。在本例中我们将SIGQUIT信号在act属性中设置为阻塞,那么只有当SIGINT信号处理函数执行期间,如果收到了SIGQUIT 信号,那么其将会被阻塞,当SIGINT信号处理函数执行完了,就会为SIGQUIT解除阻塞,并执行其相应的信号处理函数。
值得注意的是,在sleep(3)期间,两个信号同时产生了很多次,但是最终只会被执行一次。这也应证了上面的信号不支持排队这个观点。同时这里需要注意,这里的程序终止需要通过重新拉一个窗口利用kill命令才能将其杀死,
产生:每个进程运行结束都会给其父进程发送SIGCHLD信号。
#include
#include
#include
#include
#include
#include
void sighandler(int signo)
{
printf("signo=%d\n",signo);
}
int main()
{
pid_t pid=fork();
signal(SIGCHLD,sighandler);
if(pid<0)
{
perror("fork error\n");
return -1;
}
else if(pid==0)
{
printf("child process pid=%d,ppid=%d\n",getpid(),getppid());
while(1)
{sleep(1);}
}
else if(pid>0)
{
printf("father process pid=%d,ppid=%d\n",getpid(),getppid());
while(1)
{sleep(1);}
}
return 0;
}
借用前面父子进程的代码,在里面为SIGCHLD信号注册一个处理函数,然后我们手动杀死子进程,观察SIGCHLD信号的捕捉。
这里我们使用SIGCHLD信号回收子进程,首先来看一下SIGCHLD信号的产生条件,当一个进程运行结束时,或者是收到SIGSTOP SIGCONT信号,就会给其父进程发送一个SIGCHLD信号,父进程在收到这个信号后,从而对开始对子进程进行回收。下面看一个使SIGCHILD信号回收子进程的实例,首先是循环创建三个子进程和之前的方式一样,注意在子进程中要有break,防止子进程反复创建,后面要在父进程中注册信号处理函数,使用sigaction函数,在信号处理函数中使用waitpid函数对子进程进行回收,当然在这个过程中会有很多情况会产生僵尸进程,首先,如果你的信号处理函数还没有完成注册,三个子进程都已经退出了,此时父进程没有完成对子进程的回收,会产生三个僵尸进程:
将程序跑起来,通过观察进程运行情况会发现三个子进程:
对于这种情况,我们可以在信号注册函数完成前先将SIGCHILD信号设置为阻塞,当完成信号处理函数的注册后,再将其解除阻塞;当然在信号处理函数中也存在问题,因为信号是不支持排队的,连续发送多次信号这个信号只会被处理一次,因此我们就需要在收到一个信号就把所有的子进程就全部回收了,就需要在信号处理函数中设置循环回收子进程;以上的这两点是容易忽视的小细节,值得注意!下面是全部的代码:
#include
#include
#include
#include
#include
#include
void handler(int signo)
{
while(1)
{
pid_t pid=waitpid(-1,NULL,WNOHANG);
if(pid>0)
{
printf("回收了%d\n",pid);
}
else if(pid==0)
{
continue;
}
else if(pid==-1)
{
printf("recycle over\n");
break;
}
}
}
int main()
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set,SIGCHLD);
sigprocmask(SIG_BLOCK,&set,NULL);
int i=0;
for(i=0;i<3;i++)
{
pid_t pid=fork();
if(pid<0)
{
perror("fork errror\n");
return -1;
}
else if(pid==0)//子进程
{
printf("child process pid=[%d],ppid=[%d]\n",getpid(),getppid());
break;//当检查到是子进程时候就会跳出,子进程就不会循环创建孙子进程
}
else if(pid>0)//父进程
{
printf("father process pid=[%d],ppid=[%d]\n",getpid(),getppid());
}
}
if(i==0)//第一个子进程
{
printf("child 1 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
}
if(i==1)//第二个子进程
{
printf("child 2 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
}
if(i==2)//第三个子进程
{
printf("child 3 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
}
if(i==3)//在父进程中回收子进程
{
printf("father :pid = [%d],ppid = [%d]\n",getpid(),getppid());
struct sigaction act;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sleep(5);
sigaction(SIGCHLD,&act,NULL);
sigprocmask(SIG_UNBLOCK,&set,NULL);
while(1)
{
sleep(1);
}
}
return 0;
}
理清整个处理逻辑后再看这段代码会简单许多,同时还有主要waitpid函数的使用,其相关参数的设置。