C信号处理的基础
信号是软件中断所提供的用于处理异步时间的一种机制,它有一个非常精确的生命周期。首先是一个时间引发一个信号,然后内核将该信号存储起来,直到被传递出去,最后内核在适当的时候处理该信号。内核处理信号有三种方式:
- 忽略,但是SIGKILL和SIGSTOP是不能被忽略的。
- 捕获并处理,内核暂停当前程序的执行,跳跃到一个先前注册的一个函数,进程执行该函数,然后返回到暂停的地方。
- 执行默认操作,具体的操作取决有信号类型,默认操作通常是终止进程。
static void sigwinch_handler(int signo){
printf("window size has been changed\n");
}
int main(){
if(signal(SIGWINCH, sigwinch_handler) == SIG_ERR){
printf("signal error.\n");
exit(EXIT_FAILURE);
}
for(;;)
pause();
return 0;
}
上面这段代码是一个简单的信号的例子:在终端窗口的大小发生变化的时候就会输出"window size has been changed",也就是说把SIGWINCH信号交给sigwinch_handler来处理了。
bool is_child;
bool has_fork;
static void sigwinch_handler(int signo){
if(is_child == true)
printf("child:\n");
else
printf("parent:\n");
printf("window size has been changed\n");
}
static void sigint_handler(int signo){
has_fork = true;
if(is_child == true)
printf("child:");
else
printf("parent:");
printf("int\n");
}
int main(){
has_fork = false;
if(signal(SIGWINCH, sigwinch_handler) == SIG_ERR){
printf("signal error.\n");
exit(EXIT_FAILURE);
}
if(signal(SIGINT, sigint_handler) == SIG_ERR){
printf("signal error.\n");
exit(EXIT_FAILURE);
}
while(has_fork == false){}
int pid = fork();
if(pid == 0)
is_child = true;
else
is_child = false;
for(;;)
pause();
return 0;
}
从上面的这段代码中可以看到fork对信号处理的影响。fork之后子进程也会接受到内核发给父进程的信号,但是对于父进程已经接受过的信号子进程是不会重复地去处理一次。
static void sigint_heandler(int signo){
printf("i get a signal: SIGINT\n");
}
int main(){
if(signal(SIGINT, sigint_heandler) == SIG_ERR){
printf("signal fail.\n");
exit(-1);
}
int pid = fork();
if(pid != 0){
if(kill(pid, SIGINT) == 0){
printf("send SIGINT success.\n");
}else{
printf("send fail.\n");
}
}
for(;;)
pause();
return 0;
}
信号的引发方式很多,不仅仅是在程序运行的时候我们在控制台按下"Ctrl+c"才能引发SIGINT,还可以在程序运行的时候通过程序来发送,上面的代码就是一个小例子。通过fork产生一个子进程后向它发送SIGINT信号。当然这个是有权限问题的,只有具有CAP_KILL能力的进程才能随意地给其他进程发送各种信号,如果不具备该能力的进程可以给相同的用户的进程发送信号。
如果在kill调用的时候pid=0,则signo被发送给它所属的进程组。如果pid=-1,那范围就更大了,除了它本身和init除外,能发送的进程都发送一次。而如果pid<-1,那么就会给进程组-pid发送signo。signo=0为空信号,虽然什么都不会做,但是仍然在发送的时候会进行错误检测。所以可以通过它来检测进程的权限。
在想要给自己发送信号的时候kill当然是能做到的,不过还有另外一个选择:raise。而给进程组发送信号也可以通过killg代替。
如果就像刚才的信号处理程序那样随意的使用全局变量(当然在这个程序中是没什么大问题的)很可能就会引发大问题,因为内核发送一个信号的时候根本不知道进程在执行什么样的代码。所以在信号处理程序中运行的代码必须保证做进行的操作或调用的函数不会出现多个并行运行的时候引发问题,也就是要可重入。
static void sigint_handler(int signo){
printf("sigint_handler get a signal.\n");
}
void printset(sigset_t *sset){
for(int i = 0, *p = (int*)sset; i < sizeof(*sset)/sizeof(int); i++)
printf("%08x%c", *(p+i), (i%8!=7)?'':'\n');
printf("\n");
}
int main(){
if(signal(SIGINT, sigint_handler) == SIG_ERR){
printf("signal fail.\n");
exit(-1);
}
sigset_t *sset = (sigset_t*)malloc(sizeof(sigset_t));
sigfillset(sset);
printset(sset);
sigemptyset(sset);
printset(sset);
sigaddset(sset, SIGINT);
printset(sset);
int ret = sigismember(sset, SIGINT);
if(ret == 1){
printf("SIGINT is in set.\n\n");
}
ret = sigprocmask(SIG_SETMASK, sset, NULL);
if(ret == 0){
printf("set success.\n\n");
}
sleep(5);
ret = sigpending(sset);
printset(sset);
sigdelset(sset, SIGINT);
ret = sigprocmask(SIG_SETMASK, sset, NULL);
if(ret == 0)
printf("change success.\n");
return 0;
}
那在处理临界区的代码的时候不想让信号打断应该怎么处理呢?上面的代码可以看出大概的方法:首先用sigprocmask来阻挡一组信号的接受,在离开临界区的时候再对这些信号解除阻塞,那么这些信号就会在这时候被处理。
void do_sth(int signo, siginfo_t *info, void *context){
printf("do_sth.\n");
}
int main(){
struct sigaction sig;
sig.sa_sigaction = do_sth;
sig.sa_flags = SA_SIGINFO;
sigemptyset(&(sig.sa_mask));
int ret = sigaction(SIGINT, &sig, NULL);
while(true){
pause();
}
return 0;
}
与signal相比,sigaction有更大的灵活性,不过在一起的一篇aio的博客中已经介绍过该函数的用法了,所以在这里就不重复了。而如果想给指定的进程发送信号可以用sigqueue来完成,同样比kill更强大。
----------------------------
个人理解,欢迎拍砖。