Ⅰ. 信号的基本概念

  1. 用户输入命令,在Shell下启动一个前台进程。
  2. 用户按下Ctrl-C,这个键盘输入产生一个硬件中断。
  3. 如果CPU当前正在执行这个进程的代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。
  4. 终端驱动程序将Ctrl-C解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。
  5. 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。

kill -l命令可以察看系统定义的信号列表。
这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明。
Term表示终止当前进程, Core表示终止当前进程并且Core Dumplgn表示忽略该信号, Stop表示停止当前进程, Cont表示继续执行先前停止的进程

产生信号的条件主要有:

  • 用户在终端按下某些键时,终端驱动程序会发送信号给前台进程,例如Ctrl-C产生SIGINT信号,Ctrl-Z产生SIGTSTP信号。
  • 硬件异常产生信号,这些条件由硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
  • 一个进程调用kill(2)函数可以发送信号给另一个进程。
  • 可以用kill(1)命令发送信号给某个进程,kill(1)命令也是调用kill(2)函数实现的,如果不明确指定信号则发送SIGTERM信号,该信号的默认处理动作是终止进程。当内核检测到某种软件条件发生时也可以通过信号通知进程,比如闹钟超时产生SIGALRM信号,或向读端已关闭的管道写数据时产生SIGPIPE信号

Ⅱ. 如何产生信号

1. Core Dump

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。可以通过ulimit -c命令对core文件的大小进行设置。

ulimit -c 1024
/**
 * @brief floating point exception (core dumped)
 *        SIGFPE
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    int a = 3 / 0;

    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/**
 * @brief free(): double free detected in tcache 2
 *        abort (core dumped)
 *        SIGABRT 
 *
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    void *p = malloc(16);

    free(p);
    free(p);

    return 0;
}

2.调用系统函数向进程发信号

#include <stdio.h>
#include <unistd.h>

/**
 * @brief 死循环,可以使用系统函数向该进程发信号,如
 *        kill -3 pid       SIGQUIT
 *        kill -9 pid       SIGKILL
 *        kill -11 pid      SIGSEGV
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    while (1) {
        sleep(1);
    }

    return 0;
}
  1. 使用命令kill -3 pid (SIGQUIT)杀死上面的进程会生成core文件:quit (core dumped)
  2. 使用命令kill -11 pid (SIGSEGV)杀死上面的进程会生成core文件:segmentation fault (core dumped)

使用gdb a.out core + bt命令可以查看进程退出之前的一些信息

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)abort函数使当前进程接收到SIGABRT信号而异常终止。

#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

int kill(pid_t pid, int sig);
int raise(int sig);
void abort(void);
  • kill:发信号
  • raise:向自己发信号
  • abort:向自己发SIGABRT信号

(1)kill

int kill(pid_t pid, int sig);
  1. pid:如果为正数,将信号sig发送给对应pid的进程;
    如果为0,则将信号sig发送给进程组中的所有进程;
    如果为-1,则将信号sig广播发送给系统内所有的进程(需要有权限) 
    如果小于-1,则将信号sig发送到ID-pid的进程组中每个进程。
  2. sig:将要发送的信号。如果sig0,则不发送信号,但仍会执行存在和权限检查,因此可用于检查允许调用者发出信号的进程ID或进程组ID是否存在。
  3. 返回值:成功(至少有一个信号被发送出去)则返回0,失败则返回-1并设置errno
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
/**
 * @brief 父进程在3s之后向子进程发送SIGSEGV信号
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork\n");
        exit(-1);
    }

    if (pid) {
        sleep(3);
        if (kill(pid, SIGSEGV) < 0) {
            perror("kill\n");
            exit(-2);
        }
        int stat;
        wait(&stat);
        if (WIFSIGNALED(stat)) {
            printf("child process was terminated by signal %d\n", WTERMSIG(stat));
        } else {
            printf("child exit with other reason\n");
        }

    } else {
        while (1) {
            printf("child process sleep 1s.\n");
            sleep(1);
        }
    }

    return 0;
}

(2)raise

int raise(int sig);

发送信号给当前调用的进程或线程,它是对kill函数的进一步封装,在单线程程序下相当于kill(getpid(), sig),在多线程程序下相当于pthread_kill(pthread_self(), sig)

  1. 返回值:成功返回0,失败返回非0
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/**
 * @brief 等待2s,之后向自己发送SIGSEGV信号
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    sleep(2);
    raise(SIGSEGV);
    return 0;
}

(3)abort

void abort(void);

abort首先会将SIGABRT信号解除阻塞(unblock),然后对调用它的进程发送该信号。这将导致进程异常终止,除非捕获到SIGABRT信号并且信号处理程序不返回。如果abort函数导致进程终止,那么所有打开的流浆被closeflush

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/**
 * @brief 等待2s,之后向自己发送SIGABRT信号
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    sleep(2);
    abort();
    return 0;
}

3.由软件条件产生信号

(1)SIGPIPE

关闭管道的所有读端之后,如果进程继续往该管道写数据,它将会收到终止信号

/**
 * @brief 关闭管道的所有读端之后,如果进程继续往该管道写数据,它将会收到终止信号
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    int pipefd[2];
    if (pipe(pipefd) < 0) {
        perror("pipe\n");
        exit(-1);
    }
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork\n");
        exit(-2);
    }
    if (pid) {
        //父进程,关闭管道的读端和写端
        close(pipefd[0]);
        close(pipefd[1]);

        int stat;
        wait(&stat);
        if (WIFSIGNALED(stat)) {
            printf("child process was terminated by signal %d\n", WTERMSIG(stat));
        } else {
            printf("child process exit with other reason\n");
        }
    } else {
        //子进程
        sleep(3);
        close(pipefd[0]);
        //当父进程的读端被关闭后,子进程继续往管道写数据,就会收到终止的信号
        write(pipefd[1], "hello world\n", 12);
    }

    return 0;
}

(2)alarm

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前 进程发SIGALRM信号。
如果seconds0,那么所有代办的闹钟都会被取消。
在任何情况下,任何先前设置的闹钟都会被取消。

  1. 返回值:闹钟被取消后,将返回剩下的时间。如果之前没有设置过闹钟,那么返回0
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/**
 * @brief 先设置一个5s的闹钟,然后经过2s后再设一个5秒的闹钟
 *        闹钟生效后将向调用它的进程发SIGALRM信号
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    alarm(5);
    sleep(3);
    unsigned int left = alarm(5);  // left==2
    printf("left=%d\n", left);
    while (1) {
        printf("sleep 1s\n");
        sleep(1);
    }

    return 0;
}

Ⅲ. 阻塞信号

实际执行信号的处理动作称为信号递达(Delivery) ,信号从产生到递达之间的状态,称为信号未决(Pending) 。进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

Linux系统编程笔记(8)——信号-萤火
当信号的pending为1,同时block为0时,信号会立即被处理

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针(handler)表示处理信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才该标志。

如果再进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
Linux是这样实现的:常规信号在递达之前产生多从只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)

1.信号集(set)操作函数

sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_t变量

#include <signal.h>

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);

这些函数允许操作 POSIX 信号集。

  • sigemptyset函数将set信号集初始化并清空
  • sigfillset函数将set信号集初始化并用所有信号填满它
  • sigaddset函数将signum代表的信号添加到set信号集中
  • sigdelset函数将signum代表的信号从set信号集中删除
  • sigismember函数用来测试signum代表的信号是否在set信号集中
  • 返回值:
    • sigemptysetsigfillsetsigaddsetsigdelset成功则返回0,失败则返回-1并设置errno
    • sigismember如果返回1则说明ignum代表的信号在set信号集中,如果返回0则代表不在,失败则返回-1并设置errno

2.信号屏蔽字(signal mask)读写函数

调用函数sigprocmask可以读取或更改进程的信号屏蔽字

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

sigprocmask用于获取和/或更改调用线程的信号屏蔽字(signal mask),信号屏蔽字是一组当前被阻塞着的未决的信号集。sigprocmask的行为取决于参数how

  • howset:参数how决定了sigprocmask的行为,有以下几种取值:
    • SIG_BLOCK:新的阻塞信号集是目前信号集与参数set的并集合,即在原先的基础上,增加参数set信号集到信号屏蔽字中(mask = mask | set)
    • SIG_UNBLOCK:将参数set信号集从原先的信号屏蔽字中移除(mask = mask & (~set))
    • SIG_SETMASK:信号屏蔽字由参数set来设置(mask = set)
  • oldset:如果参数oldset不为NULL,则可以用来存储原先的信号屏蔽字(备份,可以用来做还原操作);如果参数setNULL,虽然经过sigprocmask之后不会改变原先的信号屏蔽字,但是仍会将信号屏蔽字返回给oldset参数
  • 返回值:成功返回0,失败返回-1并设置errno

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其一个信号递达。(先递达信号,再返回)

不能阻塞SIGKILLSIGSTOP信号。

一个进程中的各个线程都有它们自己的signal mask

除非SIGBUSSIGFPESIGILLSIGSEGV信号是由kill(2)sigqueue(3)raise(3)发出的,否则如果当它们被阻塞后又通过其它途径产生了,那么结果是不可预料的。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char const *argv[]) {
    sigset_t set, oldset;
    // 初始化并清空set
    if (sigemptyset(&set) < 0) {
        perror("sigemptyset\n");
        exit(-1);
    }
    if (sigaddset(&set, SIGINT) < 0) {  //SIGINT:Ctrl+C
        perror("sigaddset\n");
        exit(-2);
    }
    //修改signal mask
    if (sigprocmask(SIG_BLOCK, &set, &oldset) < 0) {
        perror("change signal mask by function sigprocmask\n");
        exit(-3);
    }
    int n = 10;
    while (n > 0) {
        sleep(1);
        printf("you can not terminate this process by Ctrl+C now\n");
        n--;
    }
    //改回去,如果在之前有发送SIGINT信号,那么未决的SIGINT信号将会被立即处理,也就是进程会立即被终止
    if (sigprocmask(SIG_SETMASK, &oldset, NULL) < 0) {
        perror("change signal mask back by function sigprocmask\n");
        exit(-4);
    }

    return 0;
}

3.未决(pending)信号集

#include <signal.h>

int sigpending(sigset_t *set);

sigpending读取当前进程的未决(pending)信号集,通过set参数传出。

  • 返回值:成功返回0,出错返回-1并设置errno
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/**
 * @brief 打印信号集set
 * 
 * @param set 
 */
void print_sig(const sigset_t *set) {
    for (int i = 1; i < 32; i++) {
        sigismember(set, i) ? putchar('1') : putchar('0');
    }
    putchar(10);
}

/**
 * @brief 先将SIGINT和SIGQUIT信号加入到signal mask中,之后打印pending set,
 *        如果在10s内发送SIGINT或者SIGQUIT信号可以看到输出结果
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    sigset_t set, oldset, pset;
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    sigprocmask(SIG_BLOCK, &set, &oldset);
    int n = 10;
    while (n > 0) {
        //10s内发送SIGINT或者SIGQUIT信号可以看到输出结果
        sigpending(&pset);
        print_sig(&pset);
        sleep(1);
        n--;
    }
    sigprocmask(SIG_SETMASK, &oldset, NULL);

    return 0;
}

Ⅳ. 捕捉信号

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间(但是独立的)的,处理过程比较复杂

Linux系统编程笔记(8)——信号-萤火
注意这里的信号处理函数与主函数是处于不同的用户空间的

1. sigaction

sigaction函数可以读取和修改与指定信号相关联的处理动作。

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  1. signum:指定信号,除了信号SIGKILLSIGSTOP
  2. 如果act不为NULL,那么在指定的signum信号递达时将去执行act,如果oldact不为NULL,那么原先的信号抵达执行动作将被备份在oldact中(可用于恢复现场)

其中actoldact指向sigaction结构体:

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
  1. sa_handler:用于指向指定信号处理动作地址,可以设置为SIG_DEL,代表直线默认的处理动作,可以为SIG_IGN,代表忽略这个信号的处理动作,也可以指向自定义的信号处理函数,这个函数仅有一个唯一参数,那就是信号编号signum
  2. sa_flags:一般设置0
  3. sa_mask:在指向信号处理函数时,可以使用该参数阻塞其它信号的递达

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

/**
 * @brief 修改SIGINT信号的处理动作handler为自定义的函数
 * 
 * @param signum 
 */
void undead(int signum) {
    printf("get signum=%d, I'm alive.\n", signum);
}

int main(int argc, char const *argv[]) {
    struct sigaction act, oldact;
    act.sa_handler = undead;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(SIGINT, &act, &oldact);  //将SIGINT信号的处理动作改为自定义的undead函数

    int n = 10;
    while (n > 0) {
        sleep(1);
        n--;
    }

    sigaction(SIGINT, &oldact, NULL);  //还原SIGINT信号的处理动作

    return 0;
}

2.pause

pause函数使调用进程挂起直到有信号递达。

#include <unistd.h>

int pause(void);
  • 如果信号的处理动作是终止进程,则进程终止, pause函数没有机会返回;
  • 如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;
  • 如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1, errno设置为EINTR,表示“被信号中断” ,所以pause只有出错的返回值。
#include <signal.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

/**
 * @brief 自定义的信号处理函数sighandler
 * 
 * @param signum 
 */
void sig_alarm(int signum) {
    return;
}

/**
 * @brief 实现sleep函数
 * 
 * @param sec 
 * @return unsigned int 
 */
unsigned int mysleep(unsigned int sec) {
    struct sigaction act, oldact;
    act.sa_handler = sig_alarm;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(SIGALRM, &act, &oldact);

    alarm(sec);
    pause();
    int unslept = alarm(0);  // 再用一次alarm函数,获取上一个alarm被终止时剩余时间

    sigaction(SIGALRM, &oldact, NULL);

    return unslept;
}

int main(int argc, char const *argv[]) {
    int n = 5;
    while (n > 0) {
        printf("mysleep 1s\n");
        mysleep(1);
        n--;
    }

    return 0;
}

3.可重入函数

可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。

4.竞态条件与sigsuspend函数

当捕捉到信号时,不论进程的主控制流程当前执行到哪儿,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。引入了信号处理函数使得一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局变量、硬件资源等) ,就有可能出现冲突:

Linux系统编程笔记(8)——信号-萤火
main调用insert的同时发生了信号需要处理,信号处理函数sighandler也要操作insert函数,发生了冲突

sigsuspend包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对时序要求严格的场合下都应该调用sigsuspend而不是pause

#include <signal.h>

int sigsuspend(const sigset_t *mask);

sigsuspend会临时性地使用mask参数替换掉调用它的进程的信号屏蔽字(signal mask),之后会挂起这个进程,直到有动作是调用信号处理(signal handler)程序或者终止进程的信号递达为止。
如果该信号终止了这个进程,sigsuspend将不会返回;如果信号被捕获,那么sigsuspend将在信号处理(signal handler)程序执行完之后返回,最后信号屏蔽字(signal mask)将会被复位为调用sigsuspend之前的样子。

无法阻塞信号SIGKILLSIGSTOP

pause一样,sigsuspend没有成功情况的返回值,只有执行了一个信号处理函数之后 sigsuspend才返回,返回值为-1errno置为EINTR

#include <signal.h>
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

/**
 * @brief 自定义的信号处理函数sighandler
 * 
 * @param signum 
 */
void sig_alarm(int signum) {
    return;
}
/**
 * @brief 实现sleep函数
 * 
 * @param sec 
 * @return unsigned int 
 */
unsigned int mysleep(unsigned int sec) {
    struct sigaction act, oldact;
    sigset_t new_mask, old_mask, su_mask;
    //阻塞闹钟信号
    sigemptyset(&new_mask);
    sigaddset(&new_mask, SIGALRM);
    //将SIGALRM加入signal mask
    sigprocmask(SIG_BLOCK, &new_mask, &old_mask);

    //替换信号SIGALRM的信号处理函数
    act.sa_handler = sig_alarm;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(SIGALRM, &act, &oldact);

    //开启闹钟
    alarm(sec);
    //读取原先的的signal mask情况
    su_mask = old_mask;
    //将SIGALRM信号从su_mask集合中移除
    sigdelset(&su_mask, SIGALRM);
    //临时设置信号屏蔽字为su_mask, 然后挂起程序等待信号(含SIGALRM)的到来
    sigsuspend(&su_mask);
    //再用一次alarm函数,获取上一次alarm被终止时的剩余时间(可能是设置得时间到了或者是被其他信号终止了)
    int unslept = alarm(0);

    //先还原signal handler再还原signal mask
    //防止先还原signal mask的话有信号递达,处理了我们自定义的动作
    sigaction(SIGALRM, &oldact, NULL);
    sigprocmask(SIG_SETMASK, &old_mask, NULL);

    return unslept;
}

int main(int argc, char const *argv[]) {
    int n = 5;
    while (n > 0) {
        printf("mysleep 1s\n");
        mysleep(1);
        n--;
    }

    return 0;
}

5.关于SIGCHLD信号

waitwaitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

void sig_child(int signo) {
    int stat;
    pid_t pid = wait(&stat);
    if (WIFEXITED(stat)) {
        printf("chid process(pid=%d) exit with code %d\n", pid, WEXITSTATUS(stat));
    } else {
        printf("666 code %d\n", WEXITSTATUS(stat));
    }

    return;
}

/**
 * @brief 父进程fork出子进程,子进程调用exit终止,
 * 	  父进程自定义SIGCHLD信号处理函数,在其中调用wait
 * 	  获得子进程的退出状态并打印
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork\n");
        exit(-1);
    }
    if (pid) {
        //父进程
        struct sigaction act, old_act;
        act.sa_handler = sig_child;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;

        sigaction(SIGCHLD, &act, &old_act);

        int n = 10;
        while (n--) {
            printf("work~\n");
            sleep(1);
        }

    } else {
        //子进程
        sleep(3);
        exit(2);
    }

    return 0;
}

事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigactionSIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。此方法对于 Linux可用,但不保证在其它UNIX系统上都可用。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/**
 * @brief 父进程fork出子进程,子进程调用exit终止,
 * 	      父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,
 *        这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,
 *        也不会通知父进程
 * 
 * 
 * @param argc 
 * @param argv 
 * @return int 
 */
int main(int argc, char const *argv[]) {
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork\n");
        exit(-1);
    }
    if (pid) {
        //父进程
        struct sigaction act, old_act;
        act.sa_handler = SIG_IGN;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;

        sigaction(SIGCHLD, &act, &old_act);

        int n = 20;
        while (n--) {
            printf("work~\n");
            sleep(1);
        }

    } else {
        //子进程
        sleep(3);
        exit(2);
    }

    return 0;
}