Linuxの信号

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函数修改和信号相关联的默认行为。唯一的例外是SIGSTOPSIGKILL,它们的默认行为是不能修改的。

signal函数

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:

  • 如果handlerSIG_IGN那么忽略类型为 signum的信号
  • 如果handlerSIG_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添加到 set
  • sigdelset从set中删除 signum
  • 如果 signum是 set 的成员,那么 sigismember返回1,否则返回0.

信号处理程序

安全的信号处理

  1. 处理程序尽可能简单.处理程序可能只是简单地设置全局标志并立即返回;所有与接收信号相关的处理都由主程序执行,它周期性地检查(并重置)这个标志
  2. 在处理程序中只调用异步信号安全的函数。所谓异步信号安全的函数(或简称安全的函数)能够被信号处理程序安全地调用,原因有二:要么它是可重入的,要么它不能被信号处理程序中断。
  3. 保存和恢复errno。许多Linux异步信号安全的函数都会在出错返回时设置errno。在处理程序中调用这样的函数可能会干扰主程序中其他依赖于errno的部分。解决方法是在进入处理程序时把errno保存在一个局部变量中,在处理程序返回前恢复它。注意,只有在处理程序要返回时才有此必要。如果处理程序调用exit终止该进程,那么就不需要这样做了。
  4. 阻塞所有的信号,保护对共享全局数据结构的访问。如果处理程序和主程序或其他处理程序共享一个全局数据结构,那么在访问(读或者写)该数据结构时,处理程序和主程序应该暂时阻塞所有的信号。这条规则的原因是从主程序访问一个数据结构d通常需要一系列的指令,如果指令序列被访问d的处理程序中断,那么处理程序可能会发现d的状态不一致,得到不可预知的结果。在访问d时暂时阻塞信号保证了处理程序不会中断该指令序列。
  5. 用volatile声明全局变量。考虑一个处理程序和一个main函数,它们共享一个全局变量g。处理程序更新g,main周期性地读g。对于一个优化编译器而言,main中g的值看上去从来没有变化过,因此使用缓存在寄存器中g的副本来满足对g的每次引用是很安全的。如果这样,main函数可能永远都无法看到处理程序更新过的值。可以用volatile类型限定符来定义一个变量,告诉编译器不要缓存这个变量。例如:volatile int g;volatile限定符强迫编译器每次在代码中引用g时,都要从内存中读取g的值。一般来说,和其他所有共享数据结构一样,应该暂时阻塞信号,保护每次对全局变量的访问。
  6. 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:要处理的信号,比如 SIGINTSIGTERM 等(不能是 SIGKILLSIGSTOP)。
  • 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

乍一看代码似乎没有问题,但是可能会发生这样的事件:

  1. 父进程执行fork函数,内核调度新创建的子进程运行,而不是父进程
  2. 在父进程能够再次运行之前,子进程就终止,并且变成一个僵死进程,使得内核传递一个SIGCHLD信号给父进程
  3. 接着,当父进程再次变成可运行但又在它执行之前,内核注意到有未处理的SIGCHLD信号,并通过在父进程中运行处理程序接收这个信号
  4. 信号处理程序回收终止的子进程,并调用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之前,都要阻塞 SIGCHLDsigsuspend 会暂时取消阻塞 SIGCHLD,然后休眠,直到父进程捕获信号在返回之前,它会恢复原始的阻塞集合,又再次阻塞SIGCHLD。如果父进程捕获一个SIGINT信号,那么while成立继续循环,下一次迭代又再次调用sigsuspend。如果父进程捕获一个SIGCHLD,会退出循环。此时,SIGCHLD是被阻塞的,所以我们可以可选地取消阻塞SIGCHLD

上一篇
下一篇