信号的阻塞、未达:
linux中进程1向进程2发送信号,要经过内核,内核会维护一个进程对某个信号的状态,如下图所示:
当进程1向进程2发送信号时,信号的传递过程在内核中是有状态的,内核首先要检查这个信号是不是处于阻塞状态,然后检查这个信号是不是处于未决状态,最后检查是不是忽略该信号。
更详细的信号传递过程如下:
一个信号送到进程2时,先检查这个进程的信号屏蔽字block,如果该信号对应位是1,表示进程把这个信号是屏蔽(阻塞)了,然后内核就将pending状态字的相应位置为1,表示信号未抵达,当我们在进程2中调用一个函数将block中的相应位置为0时,pending中的对应位就会被置为0,这时候刚才未达的信号就可以继续往后走了,然后检查进程2对这个信号是不是忽略,如果不是忽略就调用相应的信号处理函数。
下面介绍几个操作信号集的函数:
int sigemptyset(sigset_t *set) 把信号集(64bit)全部清零
int sigfillset(sigset_t *set) 把信号集全部置为1
int sigaddset(sigset_t *set, int signo) 根据signo,把信号集中的相应位置为1
int sigdelset(sigset_t *set, int signo) 根据signo,把信号集中相应的位清0
int sigismember(const sigset_t *set, int signo) 判断signo是否在信号集中
获取block状态字状态的函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oset) 读取或者更改进程的信号屏蔽状态字(block)
若成功则返回0,若出错则返回-1
如果oset是非空指针,则读取进程的当前信号屏蔽状态字通过oset传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的状态字备份到oset里,然后根据set和how参数更改信号屏蔽字,假设当前的信号屏蔽字为mask,下表说明了how的可选值。
int sigpending(sigset_t *set) 获取进程没有抵达的状态字
信号阻塞、未达示例程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> void my_handler(int num)
{
if(SIGINT == num)
{
printf("recv signal SIGINT\n");
}
else if(SIGQUIT == num)
{
sigset_t uset;
sigemptyset(&uset);
sigaddset(&uset, SIGINT);
sigprocmask(SIG_UNBLOCK, &uset, NULL);
printf("recv signal num = %d\n", num);
} } void printsigset(sigset_t *set)
{
int i = ;
for(i = ; i < ; i++)
{
if(sigismember(set, i))
{
putchar('');
}
else
{
putchar('');
}
} printf("\n");
} int main()
{
sigset_t bset;
sigset_t pset; sigemptyset(&bset);
sigaddset(&bset, SIGINT); if(signal(SIGINT, my_handler) == SIG_ERR)
{
perror("signal error");
exit();
} if(signal(SIGQUIT, my_handler) == SIG_ERR)
{
perror("signal error");
exit();
} sigprocmask(SIG_BLOCK, &bset, NULL); for(;;)
{
sigpending(&pset);
printf("bset : \n");
printsigset(&bset);
printf("pset : \n");
printsigset(&pset); sleep();
} return ;
}
在主函数中,我们将SIGINT设置为阻塞,执行程序时,当没有按下ctrl+c(产生SIGINT)时,pending状态字为全0,说明没有未达信号,当按下ctrl+c时,由于block中将SIGINT设置为了阻塞,所以当产生SIGINT时,pending中的第2位(SIGINT对应的位,信号从第1位开始算起)被置为了1。执行程序,结果如下:
ctrl+c产生SIGINT, ctrl+\产生SIGQUIT。按下ctrl+\触发信号处理函数,产生SIGQUIT信号,解除对SIGINT的阻塞,而刚刚那个未达的SIGINT被送达,再一次触发信号处理函数,打印出recv signal SIGINT。SIGINT被送达后,相应的pending位被清0了。我们在信号处理函数中将block中的阻塞位清除,但是并没有起作用(原因未知,在信号处理函数中设置block,只是临时起作用,例如:现在有一个未达信号SIGINT,我们产生SIGQUIT进入信号处理函数,临时将block中的阻塞位解除,然后处理这个未达信号,处理完之后,将阻塞位恢复原样,然后继续执行。 从执行结果也可以看出,是先打印出recv signal SIGINT,又打印出recv signal num = 3)。
block中设置SIGINT屏蔽字,按下ctrl+c,pending相应位置为1,按下ctrl+\,SIGQUIT被送达一次,信号处理函数被调用,SIGINT又被送达一次(因为刚才处于pending),SIGQUIT被送达的那次,在信号处理函数中将block中SIGINT位清0,并恢复SIGINT的处理函数为默认。再次按下ctrl+c,应该退出程序,但是没有退出,而是pending中又被置为1,再次按下ctrl+\,则信号处理函数中SIGQUIT相关的打印被输出,然后程序退出。
sigaction注册信号处理函数:
sigaction也用来做信号的安装,该函数功能比signal更强大。函数原型如下:
int sigaction(int signum, const struct sigaction *act, const struct sigaction *old)
该函数的第一个参数为信号的值,可以为除SIGKILL和SIGSTOP外的任何一个有效信号值(为这两个信号定义自己的处理函数,将导致安装错误)。
第二个参数为指向sigaction的一个实例的指针,在结构sigaction中指定了对特定信号的处理,可以为空,进程会以缺省方式对信号进行处理。
第三个参数oldact指向的对象用来保存原来对相应信号的处理,可指定为NULL。
第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等。
struct sigaction
{
void (*sa_handler)(int) //信号处理程序,不接受额外数据,老的处理函数
void (*sa_sigaction)(int, siginfo_t *, void *) // 信号处理程序,能接受额外数据,和sigqueue配合使用
sigset_t sa_mask;
int sa_flags; //影响信号的行为,SA_SIGINFO表示能接受数据,如果进程想要接收额外数据,则应设置该位
void (*sa_restorer)(void) //废弃
}
sa_handler和sa_sigaction不能同时存在,两个都赋值的话,优先调用sa_sigaction。
示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> void my_handler(int num)
{
printf("recv signal num = %d\n", num);
} int main()
{
struct sigaction act = {}; act.sa_handler = my_handler; sigaction(SIGINT, &act, NULL); for(;;)
{
sleep();
} return ;
}
执行结果如下:
赋值sa_sigaction的示例程序:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> void my_handler(int num)
{
printf("recv signal num = %d\n", num);
} void my_sa_sigaction(int num, siginfo_t *info, void *p)
{
printf("recv sig : %d\n", num);
} int main()
{
struct sigaction act = {}; act.sa_handler = my_handler;
act.sa_sigaction = my_sa_sigaction; sigaction(SIGINT, &act, NULL); for(;;)
{
sleep();
} return ;
}
执行结果如下:
sigqueue函数:
新的发送信号的系统调用,主要是针对实时信号提出的信号带有参数,与函数sigaction()配合使用。比kill函数强大,函数原型如下:
int sigqueue(pid_t pid, int sig, const union sigval value)
第一个参数指定接收信号的进程id,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。
sigval联合体如下:
typedef union sigval
{
int sival_int;
void *sival_ptr;
}sigval_t;
下面我们编写带有额外数据的信号发送处理函数,先给出信号处理函数中第二个参数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 */
sigval_t 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 */
int si_band; /* Band event */
int si_fd; /* File descriptor */
}
程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> void my_handler(int num)
{
printf("recv signal num = %d\n", num);
} void my_sa_sigaction(int num, siginfo_t *info, void *p)
{
int myintnum = ;
myintnum = info->si_value.sival_int;
printf("%d %d \n", myintnum, info->si_int);
} int main()
{
pid_t pid;
struct sigaction act = {}; act.sa_sigaction = my_sa_sigaction;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO; if (sigaction(SIGINT, &act, NULL) < )
{
perror("sigaction error");
exit();
} pid = fork(); if(pid == -)
{
perror("fork error");
} if(pid == )
{
union sigval usig;
usig.sival_int = ;
int n = ;
while(n > )
{
sigqueue(getppid(), SIGINT, usig);
n--;
sleep();
}
exit();
} for(;;)
{
sleep();
} return ;
}
执行结果如下:
如果子进程不睡眠,而是一直发信号,则可能造成信号丢失,因为SIGINT是不可靠信号。
实时信号与非实时信号示例程序如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h> void my_sigaction(int signum, siginfo_t *info, void *p)
{
if(SIGINT == signum)
{
printf("recv SIGINT, num = %d\n", signum);
}
else if(SIGRTMIN == signum)
{
printf("recv SIGRTMIN num = %d\n", signum);
}
else if(SIGUSR1 == signum)
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
sigaddset(&set, SIGRTMIN);
sigprocmask(SIG_UNBLOCK, &set, NULL);
}
else
{
printf("recv else\n");
}
} int main()
{
pid_t pid;
int ret = ;
struct sigaction act = {};
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO;
act.sa_sigaction = my_sigaction; if (sigaction(SIGINT, &act, NULL) < )
{
perror("sigaction error");
exit();
} if(sigaction(SIGRTMIN, &act, NULL) < )
{
perror("sigaction error");
exit();
} if(sigaction(SIGUSR1, &act, NULL) < )
{
perror("sigaction error");
exit();
} sigset_t bset; sigemptyset(&bset);
sigaddset(&bset, SIGINT);
sigaddset(&bset, SIGRTMIN); sigprocmask(SIG_BLOCK, &bset, NULL); pid = fork(); if(pid == -)
{
perror("fork error");
exit();
} if(pid == )
{
int i = ;
union sigval v;
v.sival_int = ; for(i = ; i < ; i++)
{
ret = sigqueue(getppid(), SIGINT, v);
if(ret != )
{
printf("sent SIGINT failed\n");
}
else
{
printf("sent SIGINT success\n");
} } v.sival_int = ;
for(i = ; i < ; i++)
{
ret = sigqueue(getppid(), SIGRTMIN, v);
if(ret != )
{
printf("sent SIGRTMIN failed\n");
}
else
{
printf("sent SIGRTMIN success\n");
}
} v.sival_int = ;
kill(getppid(), SIGUSR1); exit();
} while()
{
sleep();
} printf("end main ...\n"); return ;
}
我们注册了非实时信号SIGINT和实时信号SIGRTMIN,还有一个用户自定义信号SIGUSR1,在本例中负责发送解除命令。它们为同一个处理程序,只是有不同的分支,在子进程中发送了3次SIGINT和三次SIGRTMIN,一开始,这两个信号都是阻塞的,因此发送完成后它们都处于未决状态,当发送SIGUSR1后,临时解除阻塞,未决信号重新被发送,但是非实时信号只被发送了一次,属于不可靠信号。而实时信号发送原来的次数,属于可靠信号。执行结果如下所示:
实时的信号会存储在进程的结构中,所以不会丢失,会缓存到内核中,但是存储的数量也是有上限的,超过缓存上限后,再到来的信号会直接扔掉而不会将之前的冲掉,具体上限可以使用ulimit -a查看,pending signals如下所示:
而非实时信号,linux内核是不缓存的,发送多少也无所谓,最终只缓存一条。