Linux 系统编程学习笔记 - 信号
信号的基本概念
终端启动前台进程后,按Ctrl-C执行过程:
- 用户输入命令,shell启动前台进程 `$ ./a.out';
- 用户按下Ctrl-C,产生硬件中断;
- 如果CPU正在执行该进程代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断;
- 终端驱动程序将Ctrl-C解释成一个SIGINT信号,发送一个SIGINT信号给进程,记录在PCB中;
- 当从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而该信号默认处理动作是终止进程,所以直接终止进程而不是再返回它的用户空间执行代码。
前台进程,后台进程
Ctrl-C产生的信号只能发送给前台进程。
前台进程和后台进程启动方式:
$ ./a.out # 启动前台进程方式
$ ./a.out & # 启动后台进程方式
系统定义的信号列表
$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
每个信号都有一个编号和一个宏定义名称,位于signal.h
- Signal一栏表示宏定义对应信号名;
- Value表示宏定义对应值;
- Action是默认的处理动作,各种含义:
Term表示终止当前进程,Core表示终止当前进程并且Core Dump,Ign表示忽略该信号,Stop表示停止当前进程,Cont表示继续执行先前停止的进程。 - Comment说明说明条件下产生该信号
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGHUP 1 Term Hangup detected on controlling terminal
or death of controlling process
SIGINT 2 Term Interrupt from keyboard
SIGQUIT 3 Core Quit from keyboard
SIGILL 4 Core Illegal Instruction
SIGABRT 6 Core Abort signal from abort(3)
SIGFPE 8 Core Floating-point exception
SIGKILL 9 Term Kill signal
SIGSEGV 11 Core Invalid memory reference
SIGPIPE 13 Term Broken pipe: write to pipe with no
readers; see pipe(7)
SIGALRM 14 Term Timer signal from alarm(2)
SIGTERM 15 Term Termination signal
SIGUSR1 30,10,16 Term User-defined signal 1
SIGUSR2 31,12,17 Term User-defined signal 2
SIGCHLD 20,17,18 Ign Child stopped or terminated
SIGCONT 19,18,25 Cont Continue if stopped
SIGSTOP 17,19,23 Stop Stop process
SIGTSTP 18,20,24 Stop Stop typed at terminal
SIGTTIN 21,21,26 Stop Terminal input for background process
SIGTTOU 22,22,27 Stop Terminal output for background process
...
产生信号的主要条件:
- 按键,终端驱动程序发送信号给前台进程,如Ctrl-C产生SIGINT信号, Ctrl-\ : SIGQUIT, Ctrl-Z : SIGTSTP;
- 硬件异常产生信号,由硬件检测并通知内核,内核向当前进程发送信号;
- 进程调用kill(2),发送信号给另一个进程;
- kill(1)命令发送信号给进程,默认发SIGTERM终止进程;
- 内核检测到某种软件条件产生,发信号通知进程。如闹钟超时产生SIGALRM,向读端关闭的管道写数据产生SIGPIPE;
如何改变信号默认处理动作?
调用sigaction(2)函数,告诉内核如何处理信号,可选动作:
- 忽略该信号;
- 执行信号的默认处理动作;
- 提供一个信号处理函数,内核处理该信号时切换到用户态处理该函数。---- 捕捉信号(catch)
产生信号
通过终端按键产生信号
什么是Core Dump?
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这个过程就叫Core Dump(核心转储)。
异常终止原因,通常是bug,如非法内存访问。
允许产生多大core取决于进程PCB的Resource Limit。默认不允许产生core文件,因为可能包含用户密码等敏感信息。开发调试阶段,可以用ulimit命令改变该限制。
产生Core Dump的例子
- ulimit命令改变Shell进程的Resource Limit,允许core最大1024K
$ ulimit -c 1024
- 编写死循环程序
#include <stdio.h>
int main() {
while(1);
return 0;
}
- 前台运行死循环程序,终端键入Ctrl-C或Ctrl-\
$ ./a.out
按Ctrl-C
$ ./a.out # a.out 复制Shell进程,因此拥有一样的Resource Limit
按Ctrl-\退出(核心已转储)
$ ls -l core # 查看core文件访问权限等信息
-rw------- 1 martin martin 245760 3月 30 16:28 core
调用系统函数向进程发信号
后台执行死循环程序,用kill命令发送SIGSEGV信号。
$ ./a.out &
[2] 16527
$ kill -SIGSEGV 16527 # 调用kill命令向pid=16527进程(也就是死循环进程)发送SIGSEGV信号
回车
[2]- 段错误 (核心已转储) ./a.out
kill -SIGSEGV 16527等价于kill -11 16527,因为11是SIGSEGV的编号。
发送信号函数:kill和raise函数
kill命令是调用kill函数实现。kill函数可以给指定进程发送指定的信号,raise函数可以给当前进程发送指定的信号。
kill或许理解成成send更合适,因为是发送信号,而非杀死信号、进程。
raise(signo)相当于kill(getpid(), signo)
#include <signal.h>
// 成功返回0,错误-1
int kill(pid_t pid, in signo);
int raise(int signo);
abort终止进程
abort函数使当前进程接收到SIGABRT信号异常终止。类似于exit,abort总会成功。
#include <stdlib.h>
void abort(void);
由软件条件产生信号
SIGPIPE :当读端都关闭时,还有进程向管道写端write,进程会收到SIGPIPE信号。
SIGALRM :当alarm指定时间到后,内核发送SIGALRM信号给进程。
alarm函数
#include <unistd.h>
// 设定闹钟,告诉内核在seconds秒后给当前进程发送SIGALRM信号
// 返回值是0或之前设定的闹钟剩余秒数
unsigned int alarm(unsigned int seconds);
示例:循环打印计数器值,1秒后收到SIGALRM信号终止(进程)
#include <unistd.h>
#include <stdio.h>
int main() {
int counter;
alarm(1);
for (counter = 0; 1; ++counter)
printf("counter=%d\n", counter);
return 0;
}
阻塞信号
信号在内核中的表示
信号的状态:
实际执行信号的处理动作称为信号递达 Delivery,信号从产生到递达之间的状态,称为信号未决 Pending。
进程可以选择阻塞Block 某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达动作。
阻塞信号 vs 忽略信号
阻塞 ≠ 忽略
阻塞信号不会递达,忽略信号是递达之后的一种处理策略。
信号在内核中的表示示意图
每个信号都有2个标志位分别表示Block和Pending,还有一个函数指针表示处理动作。
信号产生时,内核在PCB设置Pending标志,信号递达时清除。
上图中,3个信号说明:
- SIGHUP 未Block,也未产生过,递达时执行默认处理动作;
- SIGINT 产生过,正Block,暂时不能递达。默认处理动作是忽略;
- SIGQUIT 未产生过,一旦产生将Block,处理动作是用户自定义函数sighandler;
进程解除信号阻塞前,如果该信号产生过多次,如何处理?
POSIX.1允许系统递送给信号一次或多次。Linux实现:常规信号在递达之前产生多次只计一次,实时信号递达之前产生多次可以依次放在一个队列里。
每个信号分别有一个bit存储pending flag和block flag,不记录产生次数。
pengding flag, block flag可以用相同数据类型sigset_t 存储,sigset_t称为信号集,该类型可以表示每个信号的“有效”或“无效”状态。简而言之,每个bit,对应一个信号集的开关。
阻塞信号集也叫当前进程的信号屏蔽字Signal Mask。
信号屏蔽字专门用于屏蔽信号,值为0:表示信号是打开的,进程立即处理信号;值为1:表示信号是关闭的,进程暂不处理信号;
信号集操作函数
sigset_t类型内部如何存储bit,依赖于系统实现,使用者无需关心。使用者可以使用下面的函数来操作sigset_t变量。
#include <signal.h>
// 初始化set所指向的信号集,所有信号对应bit清0
int sigemptyset(sigset_t *set);
// 初始化set所指向的信号集,所有信号对应bit置1
int sigfillset(sigset_t *set);
// 添加指定信号,某个信号对应bit置1
int sigaddset(sigset_t *, int signo);
// 删除指定信号,某个信号对应bit清0
int sigdelset(sigset_t *set, int signo);
// 用于判断信号集中是否包含指定信号,包含返回1,否则返回0,出错返回-1
int sigismember(const sigset_t *set, int signo);
sigprocmask
读取或更改进程的信号屏蔽字Signal Mask。通过设置Signal Mask可以让信号产生后block。
注意:无法block SIGNKILL或SIGSTOP,通过sigprocmask block 二者的尝试会被忽略。
#include <signal.h>
// 如果oset != NULL,则读取当前进程的信号屏蔽字,通过oset读出
// 如果set != NULL,则更改进程的信号屏蔽字,参数how指示如何修改
// 如果oset != NULL && set != NULL,则先用oset备份原来的信号屏蔽字,再用set更改
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
how参数含义(假设当前信号屏蔽字为mask)
how值 | 作用描述 |
---|---|
SIG_BLOCK | 添加指定信号到信号屏蔽字,相当于mask = mask | set |
SIG_UNBLOCK | 清除指定信号到信号屏蔽字,可以解除阻塞,相当于mask = mask &~set |
SIG_SETMASK | 设置信号屏蔽字,相当于mask = set |
如果调用sigprocmask()解除当前进程若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
sigpending
读取当前进程pending信号
#include <signal.h>
// 读取当前进程pending信号,通过set读出
// 成功返回0;出错-1
int sigpending(sigset_t *set);
例程,
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
void printsigset(const sigset_t *set) {
int i;
for (i = 0; 1; i++) {
if (sigismember(set, i) == 1) // 读取信号集第i个信号的状态
putchar('1');
else putchar('0');
}
puts("");
}
int main() {
// 1. 定义信号集
sigset_t s, p; // 定义信号集s, p
// 2. 初始化信号集
sigemptyset(&s); // 初始化信号集s
// 3. 为信号集添加/清除信号
sigaddset(&s, SIGINT); // 为信号集s添加SIGINT信号, SIGINT -- 键盘中断信号
// 4. 阻塞/不阻塞 信号集对应信号
sigprocmask(SIG_BLOCK, &s, NULL); // 写信号屏蔽字mask = mask | s,该函数决定是否允许阻塞指定信号
while (1) {
sigpending(&p); // 读取当前进程的pending信号集
printsigset(&p); // 打印信号集
}
reutrn 0;
}
程序运行时,每秒打印一次pending信号集。因为阻塞了SIGINT信号,按下Ctrl-C会让硬件驱动程序给当前进程发送SIGINT信号,进而导致SIGINT信号处于peding状态。按Ctrl-\可以终止进程,因为发送的是SIGQUIT信号,而SIGQUIT信号未阻塞。
$ ./a.out
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
^\退出 (核心已转储)
捕捉信号
内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,称为捕捉信号
一个信号捕捉、执行的示例:
- 用户程序注册了SIGQUIT信号处理函数sighandler;
- 当前正在执行main函数,此时发送中断或异常切换到内核态;
- 在中断处理完毕后,返回用户态main之前,检查到有SIGQUIT递达;
- 内核决定返回用户态后,不是恢复main继续执行,而是执行sighandler处理信号;
- sighandler返回后,自动执行特殊系统调用sigreturn再次进入内核态;
- 如果没有新信号要递达,再返回用户态就是恢复main继续执行;
信号捕捉过程示意图
sigaction
读取、修改信号对应handler。
#include <signal.h>
// 读取和修改与指定信号相关联的处理动作
// 成功返回0,出错-1
// signo 信号编号
// act 要修改的信号的处理动作
// 通过oact传出该信号原来的处理动作
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
// sigaction 结构体
struct {
void (*sa_handler)(int); /* addr of signal handler */
sigset_t sa_mask; /* additional signals to block */
int sa_flags; /* signal options */
/* alternate handler */
void (*sa_sigaction)(int, siginfo_t *, void *);
};
-
sa_handler
sa_handler 是一个函数指针,指向信号处理的回调函数
SIG_IGN, 忽略该信号;
SIG_DFL,执行系统默认动作;
函数指针,用自定义函数捕捉信号; -
sa_mask
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,保证了在处理某个信号时,如果这种信号再次产生,那么会被阻塞到当前处理结束为止。如果在信号处理时,处理当前信号被自动屏蔽(阻塞)外,还希望自动屏蔽另外一些信号,就可以用sa_mask说明这些需要额外屏蔽的信号。当信号处理函数返回时,会自动恢复原来的信号屏蔽字。 -
sa_flags
用来设置信号处理的其他相关操作
选项 | 含义 |
---|---|
SA_RESETHAND | 当调用信号处理函数时,将信号的处理函数重置为缺省的SIG_DFL |
SA_RESTART | 如果信号中断了进程某个系统调用,则系统自动启动该函数调用 |
SA_NODEFER | 一般情况下,当信号处理函数运行时,内核将阻塞该信号。但如果启用了SA_NODEFER,运行处理函数时,系统将不会阻塞该信号 |
SA_NOCLDSTOP | 使得父进程在它的子进程暂停或继续运行时,不会收到SIGCHILD信号 |
SA_NOCLDWAIT | 使得父进程在子进程退出时,不会收到SIGCHLD信号,此时子进程如果退出也不会成为僵尸进程 |
SA_SIGINFO | 使用sa_sigaction成员而不是默认的sa_handler作为信号处理函数 |
SA_ONESTACK | 调用由signaltstack函数设置的可选信号栈上的信号处理函数 |
SA_INTERRUPT | 中断系统调用 |
SA_ONESHOT | 同SA_RESETHAND |
SA_NOMASK | 同SA_NODEFER |
SA_SATCK | 同SA_ONESHOT |
注意:sa_restorer已经过时,不建议使用。
- sa_sigaction
备用信号处理函数,有3个参数,可以获得关于信号的详细信息。
当sa_flags & SA_SIGINFO != 0时,将使用sa_sigaction作为信号处理函数;否则使用sa_handler。
pause
挂起进程。
pause无返回,只有出错返回-1。
如果信号的处理动作是捕捉,则调用信号处理函数后pause返回-1,同时errno = EINTR(表示被信号中断)
#include <unistd.h>
// 使当前进程挂起直到有信号递达
int pause(void);
例子,利用pause和alarm实现sleep函数功能
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
// empty function
void sig_alarm() {
}
unsigned int mysleep(unsinged int nsecs) {
struct sigaction newact, oldact; // 保存新、旧sigaction信息
unsigned int uslept; // 剩余休眠时间
newact.sa_handler = sig_alarm; // 设置newact对应sa_handler为sig_alarm
sigemptyset(&newact.sa_mask); // 初始化signal mask (all block flag bit = 0)
newact.sa_flags = 0; // 清除 sa_flags
sigaction(SIGALRM, &newact, &oldact); // 针对SIGALRM设置当前进程的sigaction,当进程收到SIGALRM信号时,根据newact信息调用sig_alarm进行处理,同时保存原来的sigaction到oldact
alarm(nsecs); // 设定闹钟,nescs秒后向进程发送SIG_ALRM信号
pause(); // 调用pause等待,内核切换到别的进程运行
uslept = alarm(0); // 获取上一次alarm剩余时间,如果没有则为0
sigaction(SIGALRM, &oldact, NULL); // 恢复与SIGALRM信号关联的处理动作为原来的oldact
return uslept;
}
// client
int main(void) {
while (1) {
mysleep(2);
printf("Two seconds passed\n");
}
}
如果上一次alarm设置的闹钟时间未到,信号未产生,此时调用alarm(0)会取消上一次的alarm,刷新闹钟定时器。
参见 alarm(0)函数的作用
思考问题:
-
信号处理函数sig_alrm什么都没干,为什么还要注册它作为SIGALRM的处理函数?不注册信号处理函数可以吗?
因为默认的SIGALRM信号处理是终止当前进程,不重新注册,让其指向自定义函数,alarm到预期时间内核发送SIGNALRM信号给当前进程,导致进程终止。 -
为什么在mysleep函数返回前要恢复SIGALRM信号原来的sigaction?
因为mysleep函数让SIGALRM的sigaction信号处理函数指向了自定义的函数,而后面别的函数、模块或子进程可能并不知道,而且也不一定需要自定义函数处理SIGALRM信号,因此需要恢复。 -
mysleep函数的返回值表示什么含义?什么情况下返回非0值?
如果上一次alarm闹钟设置的时间还未到,返回值就表示时间;如果已经到了,就返回0;。
可重入函数
信号处理函数是一个单独的控制流程,跟主控制流是异步的,二者不存在调用和被调用关系,并且使用不同的堆栈空间。如果信号处理的控制流程和主流程都访问相同的全局资源(如全局变量,硬件等),就可能出现冲突。
重入:有函数可能在调用还没返回,就再次进入,叫做重入。
一个函数访问一个全局的数据结构,可能因为重入而造成错乱,这样的函数称为不可重入函数。
一个函数如果只访问自己的局部变量或参数,称为可重入(Reentrant)函数。
如果一个函数符合以下条件之一,则不可重入:
- 调用了malloc/free,因为malloc也是用全局链表来管理的;
- 调用了标准I/O库函数,因为标准I/O库很多实现都以不可重入的方式使用全局数据结构;
sig_atomic_t类型与volatile限定符
C语言里面对一个变量赋值是一条语句,但是对应底层语言可能有多条指令。如果变量赋值的代码块重入,就有可能产生混乱。
示例,将对变量a的赋值程序,反汇编
long long a;
int main(void) {
a = 5;
return 0;
}
生成带源码的反汇编
$ gcc main.c -g
$ objdump -dS a.out
main函数汇编指令
a=5;
8048352: c7 05 50 95 04 08 05 movl $0x5,0x8049550
8048359: 00 00 00
804835c: c7 05 54 95 04 08 00 movl $0x0,0x8049554
8048363: 00 00 00
虽然C代码只有一条,但是32bit机器上对64bit long long类型变量赋值需要2条指令,因此不是原子操作。读操作也类似,需要读2次32bit寄存器。
如何解决这个重入带来的变量错乱问题?
C标准定义类型sig_atomic_t,来确保对变量的读写都是原子操作。
不同平台C库可能取不同类型,如32位机上sig_atomic_t定义为int类型。
sig_atomic_t 能避免不同执行流程同时访问同一个变量,但是不能解决CPU从缓存读取数据,而不从变量实际内存位置取值的问题。如下面的例子,由于a初值0,循环体内没有写a值,编译器会对其进行优化,用0来替代while条件中的a,这样循环条件永远为真,也就是死循环。即使sighandler改变了a值,也不会退出循环。
#include <signal.h>
sig_atomic_t a = 0;
int main() {
// 注册sighandler
while(!a) ; // 等待sighandler改变a以退出循环
return 0;
}
解决办法:使用volatile修饰变量a,告诉编译器不优化掉变量a的内存的读写。
使用volatile关键字的场景:
- 程序中多个执行流访问同一全局变量;
- 变量的内存单元中的数据不需要读写操纵就可以自己发生变化,如与硬件相关的寄存器值(可能位于RAM),或者由别的进程更新的变量源,每次读取的值可能不一样;
- 多次向变量的内存单元写数据,只写不读,如喂狗操作(序列),不是做无用功,而是有特殊含义;
经验:sig_atomic_t类型的变量应该总是加上volatile。
竞态条件与sigsuspend函数
对于前面的mysleep例程,正常期望alarm(nsec)函数后,立即调用pause挂起进程,过了nsec秒,内核发送SIGALRM信号给进程。
有种跟时序有关的情况,可能导致意外:
如果帧执行alarm(nsec)后,由于优先级更高的进程要执行,超过nsec秒后,内核发送SIGALRM信号给进程,处于pending状态。
等优先级更改进程执行完,内核先执行sig_alrm处理SIGALRM信号,再进入内核。
等再返回进程主控制流时,alarm(nsec)返回,调用pause。但是SIGALRM处理完毕,再调用pause已经没有意义。
由于异步事件任何时候都有可能发生,写程序不考虑完善,可能由于时序问题导致错误,这叫做竞态条件(Race Condition)。
解决办法:
使用sigsuspend,用于在接收到某个信号之前,临时用sigmask替换进程的信号掩码(Signal Mask,也是Block信号集),并暂停进程执行,直到收到信号为止。
sigsuspend 会阻塞进程,进入休眠状态(不占用CPU时间),将调用进程的signal mask(阻塞信号)替换为参数sigmask指向的mask。也就是说,如果sigmask指向一个空的mask(sigemptyset(&set)),那么接收到任何信号将打断进程休眠,恢复进程原来的signal mask。
sigsuspend = 1.解除阻塞信号;2.挂起等待信号。
这是一个原子操作,能避免在“解除阻塞信号”到“挂起等待信号”之间的执行流中断,而导致错乱数据。
适用场景:
适合用于等待信号的时候,希望进程休眠(挂起)的场景。
#include <signal.h>
// 和pause一样没有返回值,出错返回-1,errno=EINTR
int sigsuspend(const sigset_t *sigmask);
用sigsuspend重新实现mysleep函数:
unsigned int mysleep(unsigned int nsecs) {
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
// 设置SIGALRM信号的hanlder,保存之前的状态信息
newact.sa_handler = sig_alrm; // 自定义SIGALRM信号处理函数,实际为空
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact); // 绑定SIGALRM信号和信号集newact包含自定义handler,将原有的信号集保存到oldact
// 阻塞SIGALRM,并且保存当前signal mask
sigemptyset(&newmask); // 初始化newmask(清 0)
sigaddset(&newmask, SIGALRM); // 为newmask添加SIGALRM信号有效标志位
sigprocmask(SIG_BLOCK, &newmask, &oldmask); // 阻塞newmask中置位的信号,并保存原来的signal mask到oldmask
// 因为此时SIGALRM处于阻塞状态,进程即使收到SIGALRM信号,也不会处理,需要等到解除阻塞
alarm(nsecs); // 设置闹钟,nsecs秒后,内核将向进程发送SIGALRM信号
// 默认的oldmask一般是不阻塞SIGALRM信号的,为了安全起见,主动删除suspmask对SIGALRM的阻塞设置
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM); // 确保SIGALRM不阻塞
sigsuspend(&suspmask); // 原子操作:挂起进程(pause),等待捕捉任意信号(enable noblock for SIGALRM and other noblock signal)
uslept = alarm(0);
sigaction(SIGALRM, &oldact, NULL);
// 恢复signal mask
sigprocmask(SIG_SETMASK, &oldmask, NULL); // 用oldmask恢复signal mask
return unslept;
}
注意:sigsuspend的核心是 确保进程挂起和等待捕捉信号是一个原子操作。
SIGCHLD信号
如何避免僵尸进程?
子进程终止时,父进程利用wait/waitpid清理僵尸进程。或者终止父进程,子进程过继给init清理。
但是,父进程如何知道子进程终止?
方式一:使用wait,阻塞等待子进程终止;
方式二:使用waitpid,轮询子进程状态,查询到终止时清理;
还有一种方式,就是子进程结束时,向父进程发送SIGCHLD信号通知父进程。
子进程终止时,会给父进程发送SIGCHLD信号,默认处理动作是忽略(SIG_IGN)。
需要将对SIGCHLD信号的处理,通过父进程调用sigaction绑定SIGCHLD信号的为自定义处理函数,在自定义处理函数中,调用wait/waitpid清理子进程。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
void newact() {
int stat_val;
pid_t pid;
pid = wait(&state);
printf("child pid = %d terminated\n", pid);
if (WIFEXITED(stat_val)) {// 进程正常退出
printf("Child exit with exit code %d\n", WEXITSTATUS(stat_val));
}
else if(WIFSIGNALED(stat_val)) {// 进程异常终止
printf("Child terminate abnormally, signal %d\n" , WTERMSIG(stat_val));
}
}
int main() {
struct sigaction newact, oldact;
newact.sa_handler = sig_procchld;
sigemptyset(&newact.sa_mask);
newact.flags = 0;
sigaction(SIGCHLD, &newact, &oldact);
pid_t pid = fok();
if (pid < 0) {
perror("fork failed");
exit(1);
}
else if (pid == 0) { // child
while (1) {
}
}
else { // parent
}
return 0;
}