APUE读书笔记—第10章 信号
1. 信号概念
- 信号是软件中断,信号提供了一种处理异步事件的方法。
- 信号名在 < signal.h > 都被定义为正整数常量。
- 不存在编号为0的信号。
1.1 产生信号的条件:
- 当用户按某写终端按键时,引发终端产生的信号。如:Ctrl+C产生SIGINT信号。
- 硬件异常产生信号。如除零错,无效的内存应用产生SIGSEGV信号。
- 进程调用kill函数可将任一信号发送给另一个进程或进程组。
- 用户可用kill命令将信号发送给其他进程。
- 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号。如SIGPIPE在管道的读进程已终止后,一个进程写此管道。
1.2 信号的处理
- 忽略此信号。SIGKILL和SIGSTOP不能被忽略
- 捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数
- 执行默认的动作。Linux对每种信号都规定了默认操作。默认的动作共分为5种情况:(man 7 singal)
- Term表示终止当前进程
- Core表示终止当前进程并且Core Dump(Core Dump 用于gdb调试).
- Ign表示忽略该信号.
- Stop表示停止当前进程.
- Cont表示继续执行先前停止的进程.
2. 信号集处理函数
2.1 信号集(signal set)
一个能表示多个信号的数据类型。
#include <signal.h>
int sigemptyset(sigset_t *set);
//初始化由set指向的信号集,清除其中所有信号
int sigfillset(sigset_t *set);
//初始化由set指向的信号集,使其包括所有信号。
int sigaddset(sigset_t *set, int signum);
//将一个信号添加到已有的信号集
int sigdelset(sigset_t *set, int signum);
//从信号集删除一个信号
//4个函数返回值:若成功,返回0,若出错,返回-1
int sigismember(const sigset_t *set, int signum);
//测试signum是否在set信号集中
//返回值:若真,返回0,若假,返回0
2.2 函数sigprocmask
调用sigprocmask可以检测或更改或同时进行检测和更改进程的信号屏蔽字。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
//返回值:若成功,返回0,若出错,返回-1
- oldset是非空指针,那么进程的当前信号屏蔽字通过oldset返回。
- set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。
- sigprocmask函数仅为单线程进程定义。
how | 说明 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
2.3 函数sigpending
sigpending函数返回一个信号集,对于调用进程而言,其中的各信号是阻塞不能传递的,因而也一定是当前未决的。
#include <signal.h>
int sigpending(sigset_t *set);
//返回值:若成功,返回0,若出错,返回-1
2.4 信号集处理函数的例子
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void printsigset(const sigset_t *set)
{
int i;
for(i = 1; i < 32; i++){
if(sigismember(set, i) == 1){ //测试set的第i为是否为1
putchar('1');
}else{
putchar('0');
}
}
puts(""); //换行
}
int main()
{
sigset_t s, p;
int i = 0;
sigemptyset(&s); //init
sigaddset(&s, SIGINT); //add SIGINT to sigset
sigaddset(&s, SIGQUIT); //add SIGQUIT to sigset
sigprocmask(SIG_BLOCK, &s, NULL); //block SIGINT
printf("64bit sigset_t类型的大小为:%ld\n", sizeof(s));
while(1){
sigpending(&p); //get pending signals
printsigset(&p); //print pending sigset
if(i == 10){
sigdelset(&s, SIGQUIT); //删除SIGQUIT=0信号,只有SIGINT=1信号
sigprocmask(SIG_UNBLOCK, &s, NULL);//不阻塞SIGINT信号,进程被SIGQUIT信号结束
}
i++;
sleep(1);
}
return 0;
}
首先构造一个信号屏蔽字,然后添加要阻塞的信号,读出当前进程的未决信号集,调用sigprocmask函数更改进程的信号屏蔽字
运行结果:
➜ SIGNAL ./sigset
64bit sigset_t类型的大小为:128
0000000000000000000000000000000
0000000000000000000000000000000 //未决信号集为空。
^C0100000000000000000000000000000 //Ctrl + C 发送SIGINT信号,SIGINT信号被阻塞,并打印未决信号集,下标为1的位被置1
0100000000000000000000000000000
0100000000000000000000000000000
^\0110000000000000000000000000000 //Ctrl + \ 发送SIGQUIT信号,SIGQUIT信号被阻塞,并打印未决信号集,下标为2的位被置1
0110000000000000000000000000000
0110000000000000000000000000000
0110000000000000000000000000000
0110000000000000000000000000000
0110000000000000000000000000000
//10s之后SIGQUIT信号被清除,SIGINT进程被阻塞,进程因SIGQUIT信号而退出
3. 信号捕捉设定
函数sigaction的功能是检查、修改与指定信号相关联的处理动作
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
//返回值:若成功,返回0,若出错,返回-1
- 参数signo是要检测或修改其具体动作的信号编号。
- oldact返回信号的上一个动作,act指定要修改的动作。
sigaction的结构如下:
struct sigaction {
void (*sa_handler)(int); //捕捉函数
void (*sa_sigaction)(int, siginfo_t *, void *); //新添加的捕捉函数,可以传参 , 和sa_handler互斥,两者通过sa_flags选择采用哪种捕捉函数
sigset_t sa_mask; //在执行捕捉函数时,设置阻塞其它信号,sa_mask | 进程阻塞信号集,退出捕捉函数后,还原回原有的
阻塞信号集
int sa_flags; //SA_SIGINFO 或者 0
};
3.1 信号捕捉例子
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void do_sig(int num)
{
int n = 5;
printf("I am do_sig\n");
while(n--) {
printf("num = %d\n", num);
sleep(1);
}
}
int main()
{
struct sigaction act;
// act.sa_handler = SIG_DFL; //SIG_DFL执行默认动作
// act.sa_handler = SIG_IGN; //SIG_IGN执行忽略动作
act.sa_handler = do_sig;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT, &act, NULL);
while(1){
printf("**********\n");
sleep(1);
}
return 0;
}
运行结果:
➜ SIGNAL ./sigaction
**********
^CI am do_sig //Ctrl + C 发送SIGINT信号
num = 2
^C^C^Cnum = 2 //连续多次发送SIGINT信号
^\num = 2 //Ctrl + \ 发送一次SIGQUIT信号
num = 2
num = 2
I am do_sig
num = 2
num = 2
num = 2
num = 2
num = 2
[1] 1485 quit (core dumped) ./sigaction
程序分析:
- 定义一个sigaction的变量,并且将信号捕捉函数的首地址赋值给sa_handler成员。
- 初始化一个信号集,并且添加SIGQUIT信号,因此如果在执行捕捉函数时会阻塞SIGQUIT信号,并设置捕捉的信号为SIGINT。
- 当程序运行起来,当按下Ctrl+C发送一个SIGINT信号时,系统从用户态进入内核态,当内核处理完异常
处理递送的信号时,需要进入用户态调用捕捉函数 - 在调用捕捉函数的执行n–的5秒期间,又发送三次SIGINT信号和一次SIGQUIT信号,但是在执行捕捉函数的期间因为之前设置的信号集会阻塞这些信号。
- 当执行完捕捉函数后,会执行特殊的系统调用sigreturn,再次陷入内核态,然后又从内核态返回上次被中断的地方(while(1)的死循环)继续执行,接着信号屏蔽字会恢复为原先值。
- 因为信号屏蔽字的恢复,上次阻塞的信号再次引发中断,但当同一个信号多次发生,通常不会加入队列当被解除阻塞后,信号处理函数只被调用一次。因此,在执行完第一个I am do_sig后即使多次发送SIGINT但也只会在执行一次I am do_sig。
- 当执行完第二次SIGINT后,又会以相同的方式处理SIGQUIT信号,因为信号集恢复为原先值,因此,进程被SIGQUIT信号终止。
4. C标准库信号处理函数
4.1 signal函数
signal函数由ISO C定义,不最好使用sigaction代替该函数。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
//返回值:若成功,返回以前的信号处理配置,若出错,返回SIG_ERR
- 第一个参数指定信号的值,第二个参数指定针对前面信号值的处理handler可以是SIG_IGN(忽略该信号)、SIG_DFL(系统默认动作)或者是接到此信号后要调用的函数地址。我们称这种处理为捕捉该信号,称此函数为信号处理程序或信号捕捉函数。
- 如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR
4.2 signal函数的使用
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void do_sig(int n)
{
printf("SIGINT\n");
}
int main(void)
{
signal(SIGINT, do_sig);
while(1) {
printf("**************\n");
sleep(1);
}
return 0;
}
运行结果:
➜ SIGNAL ./signal
**************
**************
^CSIGINT //Ctrl + C 发送SIGINT信号
**************
**************
^CSIGINT //Ctrl + C 发送SIGINT信号
**************
^\[1] 12618 quit (core dumped) ./signal //发送SIGQUIT退出程序
4.2system函数
system函数执行一条shell命令
#include <stdlib.h>
int system(const char *command);
5.可重入函数
进程捕捉的信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时终端,它首先执行该信号处理程序中的指令,如果从信号处理程序返回,则继续执行在捕捉的信号时进程正在执行的正常指令序列。但是在返回控制时不会出现任何错误,就叫可重入函数。而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
例如:
strtok_r函数
strtok_r函数是strtok函数的可重入版本,并且线程安全。用来分解一个字符串为一组字符串。
#include <string.h>
char *strtok(char *str, const char *delim);
char *strtok_r(char *str, const char *delim, char **saveptr);
- str是被分割的字符串地址
- delim函数的为分隔符字符串地址
- saveptr是指向char *类型的指针变量,用来保存strtok_r内部保存切分时的上下文,以应对连续调用分解相同源字符串。
#include <stdio.h>
#include <string.h>
int main(void)
{
char str[] = "everthing will be OK!";
char *saveptr = str, *p;
while((p = strtok_r(saveptr, " ", &saveptr)) != NULL)
printf("%s\n", p);
return 0;
}
运行结果:
➜ SIGNAL ./strtok_r
everthing
will
be
OK!
可重入函数见man 7 signal
6. 时序竟态
#include <unistd.h>
int pause(void);
//返回-1,并设置errno
- pause函数使调用进程挂起,直到有信号递达,如果递达信号是忽略,则继续挂起。
pause函数可以在逻辑上实现简易的sleep函数
1. signal(SIGALRM, do_sig);
2. alarm(n);
3. pause();
先设置一个SIGALRM信号,当alarm定时了n秒之后,pause接收到SIGALRM信号,然后返回。
但是,这存在着BUG,因为形成了竟态条件(race condition),会涉及时序竟态的问题。
竞态条件(race condition),从多进程间通信的角度来讲,是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。
当程序执行完第二步alarm函数,此时如果有优先级更高的进程抢占了CPU的使用权,此时该进程就会处于就绪状态,当该进程被拉起时,n秒已经过去,SIGALRM信号已经丢失,那么该进程则会一直处于pause状态。
为了使信号不丢失,就可是设置信号屏蔽字阻塞。
1. signal(SIGALRM, do_sig);
2. 阻塞SIGALRM
3. alarm(n);
4. 解除阻塞SIGALRM
5. pause();
但是在第4和第5步中间,仍然会形成竟态条件。因此需要一个原子操作先恢复信号屏蔽字,然后使进程休眠。而sigsuspend函数提供此功能,因此,就有了sigsuspend函数。
#include <signal.h>
int sigsuspend(const sigset_t *mask);
//返回-1,并将errno设置为EINTR
sigsuspend函数的行为如下(原子操作):
- 通过参数mask解除对某个信号的屏蔽
- 挂起进程
- 当被信号唤醒,sigsuspend返回,进程的信号屏蔽字恢复为原来的值。
6.1 实现sleep函数
使用sigaction代替signal,使用sigsuspend代替pause
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void sig_arm() { }
unsigned int mysleep(unsigned int nsecs)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
//设置信号捕捉函数,保存之前信号动作
newact.sa_handler = sig_arm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
//阻塞SIGALRM信号,保存当前信号屏蔽字
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
alarm(nsecs);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
//参数suspmask设置为取消阻塞SIGALRM信号,保证sigsuspend一定不阻塞SIGALRM,并且休眠进程,同时等待信号发生
sigsuspend(&suspmask);
//设置未睡眠够秒数,恢复之前的信号动作
unslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL);
//进程恢复为原来的信号屏蔽字
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return unslept;
}
int main(void)
{
int n = 5;
while(n--) {
mysleep(1);
printf("1 sec passed\n");
}
}
运行结果:
➜ SIGNAL ./mysleep1
1 sec passed
1 sec passed
1 sec passed
1 sec passed
1 sec passed
7. SIGCHLD信号处理
7.1 SIGCHLD信号的产生条件
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
7.2 使用信号机制通知父进程回收子进程
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
void do_sig_child(int signo)
{
pid_t pid;
int status;
while ((pid = waitpid(0, &status, WNOHANG)) > 0) {
if (WIFEXITED(status))
printf("child %d exit %d\n", pid, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("child %d signal %d\n", pid, WTERMSIG(status));
}
}
int main(void
{
pid_t pid;
int i;
for(i = 0; i < 10; i++) {
if ((pid = fork()) == 0) {
break;
} else if (pid < 0) {
perror("fork err");
exit(1);
}
}
if(pid == 0) {
int n = 8;
while(n--) {
printf("child ID:%d\n", getpid());
sleep(1);
}
} else if (pid > 0) {
struct sigaction act;
act.sa_handler = do_sig_child;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
sigaction(SIGCHLD, &act, NULL);
while(1) {
printf("Parent ID:%d\n\n\n", getpid());
sleep(1);
}
}
return 100;
}
//WIFEXITED(status):子进程正常exit终止,返回真
//WEXITSTATUS(status)返回子进程正常退出值
//WIFSIGNALED(status):子进程被信号终止,返回真
//WTERMSIG(status)返回终止子进程的信号值
运行结果:
child ID:25348
child ID:25347
child ID:25349
child 25340 exit 100 //子进程退出值为进程return 的 100
child 25345 exit 100
child 25346 exit 100
Parent ID:25339
child 25343 exit 100
child 25341 exit 100
Parent ID:25339
child 25344 exit 100
Parent ID:25339
child 25342 exit 100
Parent ID:25339
child 25347 exit 100
child 25348 exit 100
Parent ID:25339
child 25349 exit 100
Parent ID:25339
Parent ID:25339