信号(下)
(一)时序竞态
在信号(上)里面讲解了信号基础的用法。但是考虑一下这样的场景,比如我用alarm函数定时3秒,但是在定时完成后,cpu调度去执行其它的进程了,过了4秒,才回到之前执行到的地方,但是alarm定时的时间已经过了,那么还没等到执行下一条语句,信号就先被处理了,可能导致程序的逻辑出现问题。这里用一个例子说明。
首先介绍一个函数
函数原型:int pause(void)
返回值:只有执行了一个信号处理程序并从其返回时,pause才返回。并且只会返回-1,并设置errno为EINTR。也就是说,没有失败的情况。
参数:无
头文件:#include <unistd.h>
这下我们就可以借助alarm()
和pause()
完成一个我们自己编写的sleep
函数了。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
typedef void (*sighandler_t)(int);
void sig_catch(int signo) //处理函数
{
printf("-----------catch\n");
}
unsigned int mysleep(unsigned int seconds)
{
sighandler_t reback;
reback = signal(SIGALRM, sig_catch); //注册捕捉函数
if(reback == SIG_ERR) //调用失败
{
return seconds; //返回剩余的秒数
}
alarm(seconds); //定时
pause(); //挂起等待
signal(SIGALRM, reback); //恢复signal原来的默认处理动作
return alarm(0);
}
int main()
{
mysleep(5);
return 0;
}
这段代码,在一个负载不是很严重的系统上运行一般不会出现问题。但是请注意之前说到的一个现象,当执行到定时那一行代码,准备执行pause()
函数时,cpu调度到了另一个进程,用了6s,那么此时的定时期alarm定时的时间已经过了,信号一发送,就马上被处理掉,这个时候回到本进程,pause()
函数就再也等不到alarm()
函数发送的SIGALRM信号了,就会一直处于挂起状态。
既然信号是提前发送并处理了,那们我们可以用屏蔽信号的方法来阻止信号被预先处理,意思就是在执行pause
函数之前把SIGALRM信号屏蔽了,然后在即将执行pause
函数时,再解除屏蔽。
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void sig_catch(int signo) //处理函数
{
printf("-------catch\n");
}
int main()
{
struct sigaction act, old_act; //act代表新的处理动作,oldact代表旧的动作
int ret;
sigemptyset(&act.sa_mask); //清空act中的信号屏蔽字
act.sa_flags = 0; //使用默认属性
act.sa_handler = sig_catch; //指定处理函数
ret = sigaction(SIGALRM, &act, &old_act); //注册捕捉
if(ret == -1)
{
perror("sigaction error");
exit(1);
}
sigset_t mask, old_mask; //old_mask用来记录进程之前的信号屏蔽字
sigemptyset(&mask); //清空masl
sigaddset(&mask, SIGALRM); //将SIGALRM信号加入mask中
sigprocmask(SIG_BLOCK, &mask, &old_mask); //设置屏蔽SIGALRM
alarm(5); //定时开始
sigprocmask(SIG_UNBLOCK, &mask, NULL); //解除屏蔽
pause();
sigaction(SIGALRM, &old_act, NULL);
sigprocmask(SIG_SETMASK, &old_mask, NULL); //恢复进程之前的屏蔽字
return 0;
}
仔细一看会发现一个问题,在解除屏蔽SIGALRM信号完成后,然后cpu就调度到了其它进程,这样的结果和之前一样,都会导致pause()接收不到任何信号,造成一直挂起。问题就出在解除屏蔽和挂起之间,只要能把这两个操作合成一个操作,那就不会导致有这样的现象出现了。而Unix中就有这样的一个函数。
函数原型:int sigsuspend(const sigset_t *mask)
//挂起等待信号
返回值:总是返回-1,如果正常返回,会设置errno为EINTR
参数:mask是一个信号屏蔽字,里面屏蔽了需要等待的信号
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
void sig_catch(int signo) //处理函数
{
printf("-----------catch\n");
}
int main()
{
/* 前面的步骤都一样 */
struct sigaction act, old_act;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
act.sa_handler = sig_catch;
int ret = sigaction(SIGALRM, &act, &old_act);
if(ret < 0)
{
perror("sigaction error");
exit(1);
}
/* 和上段代码开始出现不同 */
sigset_t mask, old_mask, sus_mask; //sus_mask是一个临时的信号屏蔽字,只在sigsuspend函数期间生效,并且该函数调用完之后,信号屏蔽字恢复为调用之前的值
sigemptyset(&mask);
sigaddset(&mask, SIGALRM);
sigprocmask(SIG_BLOCK, &mask, &old_mask); //屏蔽SIGALRM信号
alarm(5);
sus_mask = old_mask; //将旧的信号屏蔽字赋给sus_mask
sigdelset(&sus_mask, SIGALRM); //为了防止之前的信号屏蔽字已经屏蔽了SIGALRM,所以保险起见,删掉SIGALRM
sigsuspend(&sus_mask); //传入sus_mask,等待信号
sigaction(SIGALRM, &old_act, NULL); //恢复为默认的动作
sigprocmask(SIG_SETMASK, &old_mask, NULL); //恢复为之前的信号屏蔽字
return 0;
}
这样就有效的避免了由于时序竞态造成的问题,要切记使用sigsuspend
函数时传入的信号屏蔽字只是临时的,调用完毕后进程的信号屏蔽字会自动恢复成调用之前的样子。
(二).可/不可重入函数
在《UNIX环境高级编程》中,解释的比较清楚。以下是原话
进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回(例如没有调用exit或longjmp),则继续执行在捕捉到信号时进程正在执行的正常指令序列(这类似于发生硬件中断时所做的)。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处。如果进程正在执行malloc在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc,这时会发生什么。。。。。。。。在malloc例子中,可能会对进程造成破坏,因为malloc通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时,进程可能正在更改此链表。
也就是说,可重入函数要求在执行该函数的过程中,即使被中断了之后,再次回来继续执行,也不会造成异常,这就叫可重入函数。书中还列出了大部分可重入函数,并提出了三点是不可重入函数的特征:
1.使用了静态数据结构
2.调用了malloc或free
3.是标准I/O函数(标准I/O函数的很多实现都以不可重入方式使用全局数据结构)
所以我们需要避免捕捉到信号调用的函数是不可重入函数。
(三).SIGCHLD信号
这个信号会在1).子进程终止时。2).子进程接收到SIGSTOP信号停止时。3).子进程处在停止态,接收到SIGCONT后唤醒时。这几种情况产生。
而在子进程结束时,它的父进程会收到SIGCHLD信号,而该信号的默认处理动作就是忽略,所以我们可以利用这一点,对该信号进行捕捉,然后完成子进程的回收。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void sys_err(char *str)
{
perror(str);
exit(1);
}
void do_sig_child(int signo) //处理函数
{
int status;
pid_t pid;
while((pid = waitpid(0, &status, WNOHANG)) > 0) //关键之处,应该使用while而不是if
{
if(WIFEXITED(status))
printf("---------child %d exit %d\n", pid, WEXITSTATUS(status));
else if(WIFSIGNALED(status))
printf("child %d cancel signal %d\n", pid, WTERMSIG(status));
}
}
int main(void)
{
pid_t pid;
int i;
struct sigaction act;
act.sa_handler = do_sig_child;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL); //注册捕捉函数
for(i = 0; i < 10; i++) //循环创建10个子进程
{
if((pid = fork()) == 0)
break;
else if(pid < 0)
sys_err("fork");
}
if(pid == 0) //子进程执行代码
{
int n = 1;
while(n--)
{
printf("child ID %d\n", getpid());
}
return i+1;
}
else if(pid > 0) //父进程执行代码
{
while(1)
{
printf("Parent Id %d\n", getpid());
sleep(1);
}
}
return 0;
}
这段代码最关键的地方就是do_sig_child
函数中为什么使用的是while而不是if,在上一篇博客中我们提到过,信号不支持排队,也就是说,假如有一个子进程死亡之后,正在处理该信号时,突然又有多个子进程死亡了,这样就会造成子进程的回收不完全,会遗漏,导致僵尸进程的产生,用while可以多次调用waitpid
函数,让多个处于死亡的子进程统一进行回收,这样就不会造成遗漏了。
(四).信号传参
函数原型:int sigqueue(pid_t pid, int sig, const union sigval value)
返回值:成功返回0.失败返回-1,并设置errno
参数:pid:代表要发送给哪个进程,填进程号;sig:要发送的信号;value:携带的数据
union sigval{
int sival_int;
void *sival_ptr;
}
需要注意,不同的进程拥有不同的虚拟空间,所以传地址是没有意义的。
如果我们想要接收到传递的参数,那就应该使用sigaction
函数中struct sigaction
结构体中的另外一个成员,也就是void (*sa_sigaction)(int, siginfo_t *, void *)
,并且此时的sa_flags
的值应为SA_SIGINFO
而不是默认属性。
(五).中断系统调用
系统调用分为两类:慢速系统调用和其它系统调用
1.慢速系统调用:可能会使进程永久阻塞。例如wait read pause等
2.其它系统调用:比如fork getpid
慢速系统调用被中断的行为就如同之前测试的pause
一样,必须接收到信号,并且该信号是被捕捉,而不是默认或者忽略。并且中断后返回-1,设置errno为EINTR。