
Linuxの信号
信号允许进程和内核中断其他进程,它是一种更高层的软件形式异常。
一个发出而没有被接收的信号叫做待处理信号(pendingsignal)。在任何时刻,一种类型至多只会有一个待处理信号。
如果一个进程有一个类型为k的待处理信号,那么任何接下来发送到这个进程的类型为k的信号都不会排队等待;它们只是被简单地丢弃。
一个进程可以有选择性地阻塞接收某种信号。当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞
一个待处理信号最多只能被接收一次。内核为每个进程在pending位向量中维护着待处理信号的集合,而在blocked位向量中维护着被阻塞的信号集合。只要传送了一个类型为k的信号,内核就会设置pending中的第k位,而只要接收了一个类型为k的信号,内核就会清除pending中的第k位。
进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。getpgrp函数返回当前进程的进程组ID:
#include <unistd.h>
pid_t getpgrp(void);
默认地,一个子进程和它的父进程同属于一个进程组。一个进程可以通过使用setpgid函数来改变自己或者其他进程的进程组:
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
setpgid函数将进程pid的进程组改为pgid。如果pid是0,那么就使用当前进程的PID。如果pgid是0,那么就用pid指定的进程的PID作为进程组ID
kill函数
进程通过调用kill函数发送信号给其他进程(包括它们自己)。
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
- 如果
pid大于零,那么kill函数发送信号号码sig给进程pid。 - 如果
pid等于零,那么kill发送信号sig给调用进程所在进程组中的每个进程,包括调用进程自己。 - 如果pid小于零,
kill发送信号sig给进程组|pid|(pid的绝对值)中的每个进程
接收信号
当内核把进程从内核模式切换到用户模式时(例如,从系统调用返回或是完成了一次上下文切换),它会检查进程p的未被阻塞的待处理信号的集合(pending&~blocked)。
如果这个集合为空(通常情况下),那么内核将控制传递到p的逻辑控制流中的下一条指令(In.x)。
然而,如果集合是非空的,那么内核选择集合中的某个信号k(通常是最小的k),并且强制p接收信号k。收到这个信号会触发进程采取某种行为。一旦进程完成了这个行为,那么控制就传递回p的逻辑控制流中的下一条指令。每个信号类型都有一个预定义的默认行为,是下面中的一种:
- 进程终止。
- 进程终止并转储内存。
- 进程停止(挂起)直到被SIGCONT信号重启。
- 进程忽略该信号。
比如,收到SIGKILL的默认行为就是终止接收进程。另外,接收到SIGCHLD的默认行为就是忽略这个信号。进程可以通过使用signal函数修改和信号相关联的默认行为。唯一的例外是SIGSTOP和SIGKILL,它们的默认行为是不能修改的。
signal函数
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
- 如果
handler是SIG_IGN,那么忽略类型为signum的信号。 - 如果
handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为。 - 否则,
handler就是用户定义的函数的地址,这个函数被称为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序。
信号处理程序可以被其他信号处理程序中断
阻塞信号
Linux提供阻塞信号的隐式和显式的机制:
- 隐式阻塞机制。内核默认阻塞任何当前处理程序正在处理信号类型的待处理的信号。例如,假设程序捕获了信号s,当前正在运行处理程序S。如果发送给该进程另一个信号s,那么直到处理程序S返回,s会变成待处理而没有被接收。
- 显式阻塞机制。应用程序可以使用
sigprocmask函数和它的辅助函数,明确地阻塞和解除阻塞选定的信号。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signum);int sigdelset(sigset_t *set, int signum);
int sigismember(const sigset_t *set, int signum)
sigprocmask函数改变当前阻塞的信号集合(前面所述blocked位向量)。具体的行为依赖于how的值:
- SIG_BLOCK: 把
set中的信号添加到blocked中(blocked=blocked | set) - SIG_UNBLOCK: 从
blocked中删除set中的信号(blocked=blocked &~set) - SIG_SETMASK:
block=set. - 如果oldset非空,那么blocked位向量之前的值保存在oldset中。
sigemptyset初始化set为空集合。sigfillset函数把每个信号都添加到 set 中。sigaddset函数把signum添加到 setsigdelset从set中删除signum- 如果
signum是 set 的成员,那么sigismember返回1,否则返回0.
信号处理程序
安全的信号处理
- 处理程序尽可能简单.处理程序可能只是简单地设置全局标志并立即返回;所有与接收信号相关的处理都由主程序执行,它周期性地检查(并重置)这个标志
- 在处理程序中只调用异步信号安全的函数。所谓异步信号安全的函数(或简称安全的函数)能够被信号处理程序安全地调用,原因有二:要么它是可重入的,要么它不能被信号处理程序中断。
- 保存和恢复errno。许多Linux异步信号安全的函数都会在出错返回时设置errno。在处理程序中调用这样的函数可能会干扰主程序中其他依赖于errno的部分。解决方法是在进入处理程序时把
errno保存在一个局部变量中,在处理程序返回前恢复它。注意,只有在处理程序要返回时才有此必要。如果处理程序调用exit终止该进程,那么就不需要这样做了。 - 阻塞所有的信号,保护对共享全局数据结构的访问。如果处理程序和主程序或其他处理程序共享一个全局数据结构,那么在访问(读或者写)该数据结构时,处理程序和主程序应该暂时阻塞所有的信号。这条规则的原因是从主程序访问一个数据结构d通常需要一系列的指令,如果指令序列被访问d的处理程序中断,那么处理程序可能会发现d的状态不一致,得到不可预知的结果。在访问d时暂时阻塞信号保证了处理程序不会中断该指令序列。
- 用volatile声明全局变量。考虑一个处理程序和一个main函数,它们共享一个全局变量g。处理程序更新g,main周期性地读g。对于一个优化编译器而言,main中g的值看上去从来没有变化过,因此使用缓存在寄存器中g的副本来满足对g的每次引用是很安全的。如果这样,main函数可能永远都无法看到处理程序更新过的值。可以用volatile类型限定符来定义一个变量,告诉编译器不要缓存这个变量。例如:
volatile int g;volatile限定符强迫编译器每次在代码中引用g时,都要从内存中读取g的值。一般来说,和其他所有共享数据结构一样,应该暂时阻塞信号,保护每次对全局变量的访问。 - 用
sigatomict声明标志。在常见的处理程序设计中,处理程序会写全局标志来记录收到了信号。主程序周期性地读这个标志,响应信号,再清除该标志。对于通过这种方式来共享的标志,C提供一种整型数据类型sig_atomic_t,对它的读和写保证会是原子的(不可中断的),因为可以用一条指令来实现它们:volatile sig_atomic_t flag;因为它们是不可中断的,所以可以安全地读和写sig_atomic_t变量,而不需要暂时阻塞信号。注意,这里对原子性的保证只适用于单个的读和写,不适用于像f1ag++或flag=flag+10这样的更新,它们可能需要多条指令
信号的一个与直觉不符的方面是未处理的信号是不排队的。因为pending位向量中每种类型的信号只对应有一位,所以每种类型最多只能有一个未处理的信号。因此,如果两个类型k的信号发送给一个目的进程,而因为目的进程当前正在执行信号k的处理程序,所以信号k被阻塞了,那么第二个信号就简单地被丢弃了;它不会排队。
示例程序如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
void handle(int sig)
{
int olderrno = errno;
const char *buf;
if(waitpid(-1,NULL,0) < 0)
{
buf = "waitpid err\r\n";
write(1,buf,strlen(buf));
}
buf = "handle reaped child\r\n";
write(1,buf,strlen(buf));
sleep(1);
errno = olderrno;
}
int main(int argc,char **argv)
{
int i;
int n;
char buf[128];
if(signal(SIGCHLD,handle) == SIG_ERR)
{
printf("signal init err\r\n");
return -1;
}
for(i = 0;i < 3;i++)
{
if(fork() == 0)
{
printf("child %d\r\n",(int)getpid());
exit(0);
}
}
if(n = read(0,buf,sizeof(buf)) < 0)
{
perror("read fail:");
}
printf("Parent processing input\r\n");
while(1)
;
exit(0);
}
当我们运行时会发现:
./sigTest
child 6598
child 6599
child 6600
handle reaped child
handle reaped child
aaa
Parent processing input
按下CTRL Z然后ps t查看进程
^Z
[1]+ Stopped ./sigTest
kidwjb@dshanpi-a1:~/sourceCode/OS_code/ECF_code$ ps t
PID TTY STAT TIME COMMAND
3421 ttyFIQ0 Ss 0:00 /bin/login -p --
4468 ttyFIQ0 S 0:00 -bash
6597 ttyFIQ0 T 0:23 ./sigTest
6600 ttyFIQ0 Z 0:00 [sigTest] <defunct>
6601 ttyFIQ0 R+ 0:00 ps t
kidwjb@dshanpi-a1:~/sourceCode/OS_code/ECF_code$
从输出中我们注意到,尽管发送了3个SIGCHLD信号给父进程,但是其中只有两个信号被接收了,因此父进程只是回收了两个子进程。如果挂起父进程,我们看到,实际上子进程6600没有被回收,它成了一个僵死进程(在ps命令的输出中由字符串“defunct”表明)
问题就在于我们的代码没有解决信号不会排队等待这样的情况。所发生的情况是:
父进程接收并捕获了第一个信号。当处理程序还在处理第一个信号时,第二个信号就传送并添加到了待处理信号集合里。然而,因为SIGCHLD信号被SIGCHLD处理程序阻塞了,所以第二个信号就不会被接收。此后不久,就在处理程序还在处理第一个信号时,第三个信号到达了。因为已经有了一个待处理的SIGCHLD,第三个SIGCHLD信号会被丢弃。一段时间之后,处理程序返回,内核注意到有一个待处理的SIGCHLD信号,就迫使父进程接收这个信号。父进程捕获这个信号,并第二次执行处理程序。在处理程序完成对第二个信号的处理之后,已经没有待处理的SIGCHLD信号了,而且也绝不会再有,因为第三个SIGCHLD的所有信息都已经丢失了
sigaction
sigaction 是 POSIX 标准中推荐用于处理信号的系统调用,相比 signal() 更强大、可移植性更好,行为也更明确,尤其在多线程环境中更可靠。
函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum:要处理的信号,比如SIGINT、SIGTERM等(不能是SIGKILL或SIGSTOP)。act:指向你设置的新信号处理方式的结构体。oldact:用于保存之前的处理方式(可为NULL,如果你不关心)。- 返回值:成功返回 0,失败返回 -1。
struct sigaction结构体关键成员
struct sigaction {
void (*sa_handler)(int); // 传统信号处理函数(简单场景)
void (*sa_sigaction)(int, siginfo_t *, void *); // 扩展处理函数(需 SA_SIGINFO)
sigset_t sa_mask; // 在信号处理期间要阻塞的其他信号
int sa_flags; // 控制信号行为的标志
void (*sa_restorer)(void); // 已废弃,不要用
};
常用用法:
- 使用
sa_handler设置一个简单的回调函数(如处理SIGINT)。 - 用
sigemptyset(&sa.sa_mask)初始化掩码。 sa_flags通常设为 0(除非你需要高级功能)。
代码编写
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
volatile sig_atomic_t keep_running = 1;
void sigint_handler(int sig) {
keep_running = 0; // 安全:sig_atomic_t 是 C 标准保证的原子类型
}
void* worker(void* arg) {
while (keep_running) {
printf("工作...\n");
sleep(1);
}
return NULL;
}
int main() {
struct sigaction sa;
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
while (keep_running) {
sleep(1);
}
pthread_join(t, NULL);
printf("程序正常退出。\n");
return 0;
}
并发编程现象
这里给出一个较为有趣的程序,父进程管理一个全局列表记录子进程,当父进程创建子进程后就将其添加入列表;当父进程在SIGCHLD处理程序中回收一个终止的子进程时从列表中删除对应项
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#define MAXNUM 5
typedef struct list {
int list[MAXNUM];
int index;
}list_t;
list_t processList;
void initList()
{
memset(&processList,0,sizeof(processList));
}
void addList(int pid)
{
processList.list[processList.index++] = pid;
}
void deleteList(int pid)
{
int i = 0;
while(processList.list[i] != pid)
i++;
processList.list[i] = 0;
}
void handler(int sig)
{
int oldErrno = errno;//确保用户进程errno不被干扰
sigset_t nowMask;
sigset_t prevMask;
pid_t pid;
sigfillset(&nowMask);
while((pid = waitpid(-1,NULL,0)) > 0) //回收子进程
{
sigprocmask(SIG_BLOCK,&nowMask,&prevMask);
deleteList(pid);
sigprocmask(SIG_SETMASK,&prevMask,NULL);
}
if(errno != ECHILD)
perror("waitpid:");
errno = oldErrno;
}
int main(int argc,char **argv)
{
int pid;
sigset_t nowMask;
sigset_t prevMask;
sigfillset(&nowMask);//先把所有信号放入nowMask里面
signal(SIGCHLD,handler);//注册信号处理函数
initList(); //初始化列表
while(processList.index < MAXNUM)
{
if(pid = fork() == 0)
{
execve("/bin/date",argv,NULL);
}
sigprocmask(SIG_BLOCK,&nowMask,&prevMask);
addList(pid);
sigprocmask(SIG_SETMASK,&prevMask,NULL);
}
return 0;
}
运行结果:
./listTest
Mon Mar 2 16:57:14 CST 2026
Mon Mar 2 16:57:14 CST 2026
Mon Mar 2 16:57:14 CST 2026
Mon Mar 2 16:57:14 CST 2026
Mon Mar 2 16:57:14 CST 2026
乍一看代码似乎没有问题,但是可能会发生这样的事件:
- 父进程执行
fork函数,内核调度新创建的子进程运行,而不是父进程 - 在父进程能够再次运行之前,子进程就终止,并且变成一个僵死进程,使得内核传递一个
SIGCHLD信号给父进程 - 接着,当父进程再次变成可运行但又在它执行之前,内核注意到有未处理的
SIGCHLD信号,并通过在父进程中运行处理程序接收这个信号 - 信号处理程序回收终止的子进程,并调用
deleteList,这个函数什么也不做,因为父进程还没有把该子进程添加到列表中。 5)在处理程序运行完毕后,内核运行父进程,父进程从fork返回,通过调用addList错误地把(不存在的)子进程添加到作业列表中。
正常流程:
┌─────────────────────────────────────────────────────┐
│ 父进程 main() │
├─────────────────────────────────────────────────────┤
│ 1. 初始化: sigfillset(&nowMask), 注册handler │
│ 2. while(index < MAXNUM): │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ fork() │ │
│ └────────┬────────┘ │
│ │ │
│ ┌─────┴─────┐ │
│ ▼ ▼ │
│ [子进程] [父进程] │
│ │ │ │
│ ▼ │ │
│ execve() │ │
│ /bin/date │ │
│ │ │ │
│ ▼ │ │
│ 执行结束 │ │
│ 退出(正常) │ │
│ │ │ │
│ │ ▼ │
│ │ 内核: 子进程→僵尸状态 │
│ │ 发送 SIGCHLD 给父进程 │
│ │ │ │
│ │ ┌──────┴──────┐ │
│ │ │ SIGCHLD是否被阻塞? │ │
│ │ └──────┬──────┘ │
│ │ │ │
│ │ ┌──────┴──────┐ │
│ │ │ 否: 立即递送 │ │
│ │ │ 是: 标记pending │ │
│ │ └──────┬──────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ handler(SIGCHLD)│ │
│ │ ├─────────────────┤ │
│ │ │ sigprocmask(BLOCK) │ │
│ │ │ while(waitpid>0): │ │
│ │ │ deleteList(pid) │ │
│ │ │ sigprocmask(SETMASK)│ │
│ │ └────────┬────────┘ │
│ │ │ │
│ │ ▼ │
│ │ handler返回,继续父进程 │
│ │ │ │
│ ▼ ▼ │
│ [退出] sigprocmask(BLOCK) │
│ addList(真实pid) ←✅ 正确添加 │
│ sigprocmask(SETMASK) │
│ 继续下一轮循环 │
└─────────────────────────────────────────────────────┘
竞态条件流程:
┌─────────────────────────────────────────────────────┐
│ 竞态条件: 内核调度顺序导致的问题 │
├─────────────────────────────────────────────────────┤
│ │
│ T0: 父进程执行 fork() │
│ ├─ 创建子进程(假设pid=1234) │
│ └─ ⚠️ 内核调度: 选择子进程先运行! │
│ │
│ ▼ │
│ T1: [子进程] 运行 │
│ ├─ execve("/bin/date") │
│ ├─ 立即执行完毕 │
│ ├─ exit() → 变成僵尸进程 │
│ └─ 内核: 向父进程发送 SIGCHLD │
│ SIGCHLD进入父进程的pending队列 🔴 │
│ │
│ ▼ │
│ T2: [父进程] 仍未从fork()返回! │
│ ├─ 内核检查: 父进程有pending的SIGCHLD │
│ ├─ 检查信号掩码: 此时父进程还未执行阻塞代码 │
│ │ → SIGCHLD未被阻塞! 🔴 │
│ └─ 内核: 中断父进程,先执行handler! ⚡ │
│ │
│ ▼ │
│ T3: [handler] 在父进程上下文中执行 │
│ ├─ sigprocmask(BLOCK, &nowMask) ← 阻塞所有 │
│ ├─ waitpid(-1) 返回 1234 ←✅ 成功回收 │
│ ├─ deleteList(1234) │
│ │ └─ 🔴 遍历list找1234 → 找不到! │
│ │ (因为父进程还没执行addList!) │
│ │ └─ 可能越界访问或静默失败 ⚠️ │
│ └─ sigprocmask(SETMASK, &prevMask) │
│ handler返回 │
│ │
│ ▼ │
│ T4: [父进程] 终于从fork()返回 │
│ ├─ 注意: 此时fork()在父进程中返回的是 1234 │
│ ├─ 执行: sigprocmask(BLOCK) │
│ ├─ 执行: addList(1234) ←🔴 错误! │
│ │ └─ 把"已回收的pid"添加到列表中 │
│ │ └─ list中记录了不存在的进程! │
│ └─ 执行: sigprocmask(SETMASK) │
│ │
│ ▼ │
│ 💥 后果: │
│ • list中存有无效的pid │
│ • 后续对该pid的操作(如kill/wait)可能失败 │
│ • 逻辑上: "添加了一个不存在的作业" │
│ │
└─────────────────────────────────────────────────────┘
这是一个竞争的经典同步错误示例,这种错误非常难以调试
下面是消除竞争的一种办法:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#define MAXNUM 5
typedef struct list {
int list[MAXNUM];
int index;
}list_t;
list_t processList;
void initList()
{
memset(&processList,0,sizeof(processList));
}
void addList(int pid)
{
processList.list[processList.index++] = pid;
}
void deleteList(int pid)
{
int i = 0;
while(processList.list[i] != pid)
i++;
processList.list[i] = 0;
}
void handler(int sig)
{
int oldErrno = errno;//确保用户进程errno不被干扰
sigset_t nowMask;
sigset_t prevMask;
pid_t pid;
sigfillset(&nowMask);
while((pid = waitpid(-1,NULL,0)) > 0) //回收子进程
{
sigprocmask(SIG_BLOCK,&nowMask,&prevMask);
deleteList(pid);
sigprocmask(SIG_SETMASK,&prevMask,NULL);
}
if(errno != ECHILD)
perror("waitpid:");
errno = oldErrno;
}
int main(int argc,char **argv)
{
int pid;
sigset_t nowMask;
sigset_t prevMask;
sigset_t oneMask;
sigemptyset(&oneMask);
sigaddset(&oneMask,SIGCHLD);//只设置一个SIGCHLD在掩码中
sigfillset(&nowMask);//先把所有信号放入nowMask里面
signal(SIGCHLD,handler);//注册信号处理函数
initList(); //初始化列表
while(processList.index < MAXNUM)
{
sigprocmask(SIG_BLOCK,&oneMask,&prevMask); //阻塞SIGCHLD信号,就算先是子进程执行完退出后也不会立即处理SIGCHLD,而是先挂起
if((pid = fork()) == 0)
{
sigprocmask(SIG_SETMASK,&prevMask,NULL);//子进程会完全继承父进程的信号掩码,包括被阻塞的信号,对于子进程来说不需要阻塞SIGCHLD,保持良好习惯
execve("/bin/date",argv,NULL);
}
sigprocmask(SIG_BLOCK,&nowMask,&prevMask);
addList(pid);
sigprocmask(SIG_SETMASK,&prevMask,NULL);
}
return 0;
}
通过在调用fork之前,阻塞SIGCHLD信号,然后在调用addList之后取消阻塞这些信号,我们保证了在子进程被添加到作业列表中之后回收该子进程。
注意,子进程继承了它们父进程的被阻塞集合,所以我们必须在调用execve之前,小心地解除子进程中阻塞的SIGCHLD信号,确保execve后的程序从干净的信号状态开始
显示等待信号
有时候父进程需要等待子进程终止才能进行下一步,代码如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
volatile sig_atomic_t pid;
void sigchldHandler(int sig)
{
int olderrno = errno;
pid = waitpid(-1,NULL,0);
errno = olderrno;
}
void sigintHandler(int sig)
{
}
int main(int argc,char **argv)
{
sigset_t nowMask;
sigset_t prevMask;
signal(SIGCHLD,sigchldHandler);
signal(SIGCHLD,sigintHandler);
sigemptyset(&nowMask);
sigaddset(&nowMask,SIGCHLD);
while(1)
{
sigprocmask(SIG_BLOCK,&nowMask,&prevMask);
if(fork() == 0)
exit(0);
pid = 0;
sigprocmask(SIG_SETMASK,&prevMask,NULL);
while (!pid)
{
;
//pause();
//sleep(1);
}
printf("child exit\r\n");
}
return 0;
}
当这段代码正确执行的时候,循环在浪费处理器资源。我们可能会想要修补这个问题,在循环体内插入pause: while (!pid) pause(; 注意,我们仍然需要一个循环,因为收到一个或多个SIGINT信号,pause会被中断。不过,这段代码有很严重的竞争条件**:如果在while测试后和pause之前收到SIGCHLD信号,pause会永远睡眠。** 另一个选择是用sleep替换pause: while (!pid) /* Too slow! */ sleep(1); 当这段代码正确执行时,它太慢了。如果在while之后pause之前收到信号,程序必须等相当长的一段时间才会再次检查循环的终止条件
sigsuspend
合适的解决办法是使用sigsuspend,它是原子的
#include <signal.h>
int sigsuspend(const sigset_t *mask);
sigsuspend函数暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号,其行为要么是运行一个处理程序,要么是终止该进程。如果它的行为是终止,那么该进程不从sigsuspend返回就直接终止。如果它的行为是运行一个处理程序,那么sigsuspend从处理程序返回,恢复调用 sigsuspend时原有的阻塞集合。
它等价于:
sigprocmask(SIG_BLOCK,&nowMask,&prevMask);
pause();
sigprocmask(SIG_SETMASK,&prevMask,NULL);
原子属性保证对sigprocmask和pause的调用总是一起发生的,不会被中断。这样就消除了潜在的竞争,即在调用sigprocmask之后但在调用pause之前收到了一个信号。
调用代码如下:
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
volatile sig_atomic_t pid;
void sigchldHandler(int sig)
{
int olderrno = errno;
pid = waitpid(-1,NULL,0);
errno = olderrno;
}
void sigintHandler(int sig)
{
}
int main(int argc,char **argv)
{
sigset_t nowMask;
sigset_t prevMask;
signal(SIGCHLD,sigchldHandler);
signal(SIGCHLD,sigintHandler);
sigemptyset(&nowMask);
sigaddset(&nowMask,SIGCHLD);
while(1)
{
sigprocmask(SIG_BLOCK,&nowMask,&prevMask);
if(fork() == 0)
exit(0);
pid = 0;
while (!pid)
{
sigsuspend(&prevMask);//暂时把SIGCHLD打开用于接收信号
}
sigprocmask(SIG_SETMASK,&prevMask,NULL);
printf("child exit\r\n");
}
return 0;
}
在每次调用sigsuspend之前,都要阻塞 SIGCHLD。sigsuspend 会暂时取消阻塞 SIGCHLD,然后休眠,直到父进程捕获信号。在返回之前,它会恢复原始的阻塞集合,又再次阻塞SIGCHLD。如果父进程捕获一个SIGINT信号,那么while成立继续循环,下一次迭代又再次调用sigsuspend。如果父进程捕获一个SIGCHLD,会退出循环。此时,SIGCHLD是被阻塞的,所以我们可以可选地取消阻塞SIGCHLD。