信号的概念

  • 信号是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
  • 发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下:
    • 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。比如输入 Ctrl+C 通常会给进程发送一个中断信号。
    • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令,诸如被 0 除,或者引用了无法访问的内存区域。
    • 系统状态变化,比如 alarm 定时器到期将引起 SIGALRM 信号,进程执行的 CPU 时间超限,或者该进程的某个子进程退出。
    • 运行 kill 命令或调用 kill() 函数。
  • 使用信号的两个主要目的是:
    • 进程知道已经发生了一个特定的事情。
    • 强迫进程执行它自己代码中的信号处理程序。
  • 信号的特点:
    • 操作简单
    • 不能携带大量信息
    • 满足某个特定条件才发送
    • 优先级比较高
  • 查看系统定义的信号列表:kill–l
  • 前 31 个信号为常规信号,其余为实时信号。

信号的默认处理动作和状态

1. 信号的五种默认处理动作

  1. Term:Terminate,终止进程
  2. Core:终止进程 + 生成 core 转储文件(崩溃调试时可查看)
  3. Ign:Ignore,默认忽略该信号
  4. Stop:暂停进程运行
  5. Cont:恢复已暂停的进程继续执行

2. 信号的三种状态

  1. 产生
    信号触发生成的过程,只要满足条件就会生成信号,常见触发场景:
    • 用户操作:Ctrl+C 产生 SIGINT、Ctrl+\ 产生 SIGQUIT、Ctrl+Z 产生 SIGTSTP
    • 硬件异常:除 0、非法内存访问、非法指令,内核自动生成对应信号;
    • 代码调用:kill() / raise() / sigqueue() / alarm() / abort() 主动发送信号;
    • 系统事件:子进程退出触发 SIGCHLD、管道无读端触发 SIGPIPE、终端关闭触发 SIGHUP
  2. 未决
    信号已经产生,但目标线程阻塞了该信号(信号屏蔽字 mask 屏蔽此信号),内核暂时无法把信号交给进程处理,该信号会被内核标记为未决状态,存放在未决信号集合里
    关键特性
    • 标准信号不排队:同一信号多次产生、持续阻塞时,只会保留 1 次未决记录;
    • 实时信号支持排队:多次发送都会全部保存,解除阻塞后依次投递;
    • 调用 sigpending() 可以查询当前线程所有未决信号。
  3. 递送
    内核切换回用户态时,检查线程未决信号:如果信号没有被屏蔽,就把信号交给进程处理,这个动作叫递送。信号一旦递送完成,就会从未决集合中清除,结束整个生命周期。
    递送后有三种处理方式:
    • 执行系统默认动作(终止、暂停、忽略、生成 core);
    • 主动忽略该信号;
    • 执行自定义信号处理函数捕获信号。

进程实现信号捕捉的过程

在这里插入图片描述

core 文件的介绍

1. 访问野地址试错 (触发 SIGSEGV 信号)

(1) 创建一个 core.c 文件,该程序访问了野地址

#include <stdio.h>
#include <string.h>

int main()
{
    char * buf;
    strcpy(buf, "hello");

    return 0;
}

(2) 编译并运行该程序

gcc core.c
./a.out

在这里插入图片描述
终端显示发生了段错误,也就是说 Linux 内核向 a.out 进程发送了 SIGSEGV 信号。

2. 查看当前终端会话所有资源软限制

ulimit -a

在这里插入图片描述
从终端输出信息可以看到:
core file size (blocks, -c) 0 这代表核心转储文件最大允许大小为 0 块,系统不会生成任何 core 转储文件。

3. 查看 core 文件的硬限制

查看 core 文件硬限制上限

ulimit -Hc

在这里插入图片描述
这里的 1024 单位是 block,Linux 默认 1 block = 512 字节,因此 1024 blocks = 1024 × 512 = 524288 字节 = 512KB,也就是说单个崩溃程序生成的那一份 core 转储文件的上限为 512 KB

可以看到我电脑的硬限制上限1024 blocks ,普通用户不能调高硬限制,只能把软限制调低,无法改成 unlimited

4. 开启 core 文件生成

由于普通用户无法将 core 文件设置为无限制大小,所以切换到管理员终端。
(1) 切换 root shell

sudo bash

这里补充一下,如果要退出 root shell,输入 exit 即可。

(2) 开启 core 文件生成,且大小无限制 (仅当前终端会话生效)

ulimit -c unlimited
ulimit -a

在这里插入图片描述

5. 解决 core 文件生成问题

(1) 再次运行 a.out 并查看当前目录文件

./a.out
ls

在这里插入图片描述
这时突然发现并没有生成 core 文件,但是明明已经打开对于 core 文件的限制了呀。

(2) 读取内核里控制 core 核心转储文件 的配置规则

cat /proc/sys/kernel/core_pattern

在这里插入图片描述
图中的占位符为各种进程崩溃信息,他说明了本地运行程序的目录不会生成 core 文件,全部交给 Ubuntu 自带的崩溃收集工具 apport。apport 接管后,core 镜像统一存在 /var/lib/apport/coredump/

(3) 重新运行 a,out,并寻找 core 文件
在这里插入图片描述
可以发现 core 文件确实生成在了 /var/lib/apport/coredump/ 目录下,并且内存不小,因此调试后要记得执行 rm /var/lib/apport/coredump/core.* 删除 core 文件哦!

(4) 改为本地输出core文件,带 PID 后缀保证 core 文件不覆盖

echo "core.%p" > /proc/sys/kernel/core_pattern

(5) 允许root运行的程序生成 core 文件 (如果是普通用户则不需要执行此命令)

echo 2 > /proc/sys/fs/suid_dumpable

在这里插入图片描述
可以发现在当前目录下成功生成了 core 文件,不过以上修改在重启系统后则会消失。

6. 通过 gdb 交互界面加载 core 信息

(1) 删掉这些文件,重新编译运行生成 core

rm a.out core.3749
gcc core.c -g -o a.out
./a,out

刚才对于 core.c 的编译没有加 -g ,虽然也能加载 core 文件、看到函数调用地址,但是看不到源码、变量名、行号;加 -g 才能完整调试,定位到代码哪一行出错。

(2) 进入 gdb 调试

gdb ./a.out

(3) 在 gdb 交互界面加载 core 文件

core-file core.3800

在这里插入图片描述
由图片中描述的:不仅打印了报错信息,还说明了触发信号为 SIGSEGV

Linux 标准信号

1. 查看信号的详细信息

man 7 signal

2. Linux 标准信号表

编号 信号名称 对应事件 默认动作
1 SIGHUP 用户退出 shell 时,由该shell启动的所有进程将收到这个信号;控制终端断开/控制进程退出 终止进程(Term)
2 SIGINT 用户按下 <Ctrl+C> 组合键,终端向当前运行程序发送中断信号 终止进程(Term)
3 SIGQUIT 用户按下 <Ctrl+\> 组合键,终端向程序发送退出信号 终止进程并生成 core 文件(Core)
4 SIGILL CPU 检测到进程执行非法指令 终止进程并生成 core 文件(Core)
5 SIGTRAP 断点指令 / 调试 trap 指令触发,用于程序调试 终止进程并生成 core 文件(Core)
6 SIGABRT 调用 abort 函数主动触发终止信号;别名 SIGIOT 终止进程并生成 core 文件(Core)
7 SIGBUS 总线错误,非法内存访问、内存对齐异常 终止进程并生成 core 文件(Core)
8 SIGFPE 致命运算错误:浮点异常、除0、数值溢出等算法错误 终止进程并生成 core 文件(Core)
9 SIGKILL 强制杀死进程信号,不可捕获、阻塞、忽略 终止进程(Term)
10 SIGUSR1 用户自定义信号1,业务自定义使用 终止进程(Term)
11 SIGSEGV 段错误,非法内存引用、越界访问内存 终止进程并生成 core 文件(Core)
12 SIGUSR2 用户自定义信号2,业务自定义使用 终止进程(Term)
13 SIGPIPE 管道断裂:向无读端的管道、socket 写入数据 终止进程(Term)
14 SIGALRM alarm 定时器超时触发 终止进程(Term)
15 SIGTERM 软件正常终止信号,kill 默认发送该信号 终止进程(Term)
16 SIGSTKFLT 协处理器栈错误,现已废弃 终止进程(Term)
17 SIGCHLD 子进程暂停/退出;别名SIGCLD,默认自动忽略 忽略信号(Ign)
18 SIGCONT 恢复被 SIGSTOP / SIGTSTP 暂停的进程 恢复运行(Cont)
19 SIGSTOP 强制暂停进程,不可捕获、阻塞、忽略 暂停进程(Stop)
20 SIGTSTP 用户按下<Ctrl+Z>,终端发起前台进程暂停 暂停进程(Stop)
21 SIGTTIN 后台进程尝试读取终端输入 暂停进程(Stop)
22 SIGTTOU 后台进程尝试向终端输出内容 暂停进程(Stop)
23 SIGURG Socket 收到带外紧急数据 忽略信号(Ign)
24 SIGXCPU 进程超出 CPU 使用时间资源限制 终止进程并生成core文件(Core)
25 SIGXFSZ 进程创建文件超出文件大小资源限制 终止进程并生成core文件(Core)
26 SIGVTALRM 虚拟定时器超时,仅统计用户态 CPU 时间 终止进程(Term)
27 SIGPROF 性能分析定时器超时,统计用户 + 内核 CPU 时间 终止进程(Term)
28 SIGWINCH 终端窗口尺寸发生变化 忽略信号(Ign)
29 SIGIO I/O就绪异步通知;别名 SIGPOLL(System V) 终止进程(Term)
30 SIGPWR 电源故障、系统断电预警;别名 SIGINFO 终止进程(Term)
31 SIGSYS 非法系统调用;别名 SIGUNUSED 终止进程并生成 core 文件(Core)
34~64 SIGRTMIN ~ SIGRTMAX Linux 的实时信号,它们没有固定的含义 (可以由用户自定义) 终止进程(Term)

SIGKILLSIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。

kill 函数

1.头文件

#define _GNU_SOURCE
#include <sys/types.h>
#include <signal.h>

2.函数原型

int kill(pid_t pid, int sig);

参数

  • pid
    • pid > 0 :将信号 sig 发送给 PID 等于该值的单个进程。
    • pid = 0:将信号 sig 发送至调用者所属进程组内的所有进程。
    • pid = -1:将信号 sig 发送给调用者有权限发送信号的全部进程,但1 号进程(init 初始化进程)除外。
    • pid < -1:取 -pid 作为进程组 ID,将信号 sig 发送给该进程组内所有进程。
  • sig需要发送的信号的编号或者是宏值,0 表示不发送任何信号,但仍会执行进程存在性校验与信号发送权限校验;该用法可用来检测目标 PID / 进程组 ID 是否存在,且当前程序是否具备向其发信号的权限。

返回值

  • 成功(至少成功发送了一个信号):返回 0 。
  • 失败:返回 -1,并会设置对应的错误码 errno 。

作用:可用于向任意进程或进程组发送任意信号。

3. 样例 (子进程死循环,父进程发送 SIGQUIT 阻止)

创建一个 kill.c 文件

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

int main()
{
    int pid = fork();
    if(pid > 0)
    {
        printf("我是父进程\n");
        sleep(2);

        int ret = kill(pid, SIGQUIT);  // 退出信号
        if(ret == -1)
        {
            perror("kill");
            exit(0);
        }
    }
    else if(pid == 0)
    {
        while(1)
        {
            printf("我是子进程\n");
            sleep(1);
        }
    }
    else
    {
        perror("fork");
        exit(0);
    }

    return 0;
}

编译并运行程序

gcc kill.c -o kill
./kill

在这里插入图片描述
可以看到子进程在输出两行后就被父进程退出了。

raise 函数

1. 头文件

#include <signal.h>

2. 函数原型

int raise(int sig);

参数

  • sig需要发送的信号的编号或者是宏值,0 表示不发送任何信号,但仍会执行进程存在性校验与信号发送权限校验;该用法可用来检测目标 PID / 进程组 ID 是否存在,且当前程序是否具备向其发信号的权限。

返回值

  • 成功:返回 0。
  • 失败:返回非 0 数值。

作用:该函数用于向调用自身的进程或线程发送信号。

  1. 在单线程程序中,它等价于:kill(getpid(), sig);
  2. 在多线程程序中,它等价于:pthread_kill(pthread_self(), sig);
    若该信号触发了自定义信号处理函数,raise() 会等到信号处理函数执行完毕返回后,自身才会返回。

3. 样例 (进程自己发送 SIGQUIT 信号退出)

创建一个 raise.c 文件

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

int main()
{
    printf("hello, Linux!\n");
    int ret = raise(SIGQUIT);
    if(ret != 0)
    {
        perror("raise");
        exit(0);
    }

    return 0;
}

编译并运行程序

gcc raise.c -o raise
./raise

在这里插入图片描述

abort 函数

1. 头文件

#include <stdlib.h>

2. 函数原型

void abort(void);

参数:该函数无参数。

返回值:该函数无返回值。

作用:发送SIGABRT信号给当前的进程,杀死当前进程。

完整内部步骤 (对比 kill((getpid(), SIGABRT))

  1. 强制解除 SIGABRT 的信号阻塞。
    • 如果你之前用 sigprocmaskSIGABRT 屏蔽了,kill(getpid(), SIGABRT) 会把信号挂起、暂时不触发;但 abort() 上来第一步就清空 SIGABRT 的阻塞标记,保证信号一定能送达。
    • 发送第一次 SIGABRT(等价于 raise / kill (getpid (), SIGABRT)
  2. 判断走向:
    • 情况 1:信号处理函数里 exit() / longjmp 直接跑路,不再回到 abort,到此结束;
    • 情况 2:忽略信号 / 处理函数正常 return 回到 abort 代码里:
      • ① 将 SIGABRT 恢复成系统默认处理方式;
      • ② 再发一次 SIGABRT,这次一定会杀死进程,无法拦截。

3. 样例

创建一个 abort.c 文件

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

int main()
{
    printf("hello!\n");

    abort();

    printf("Linux\n");

    return 0;
}

编译并运行程序

gcc abort.c -o abort
./abort

在这里插入图片描述
可以看到终端只输出了 “hello” 这个字符串,在 abort 函数后的字符串因为程序中断则没有显示了。

alarm 函数

1. 头文件

#include <unistd.h>

2. 函数原型

unsigned int alarm(unsigned int seconds);

参数

  • seconds:倒计时的时长 (单位为秒)。如果参数 seconds 传 0:取消当前所有还未触发的定时器定时任务,定时器无效 (不进行倒计时,不发信号),因此可通过 alarm(0) 取消上一个定时器。

返回值

  • 之前设置了定时器:返回上一次闹钟剩余的倒计时秒数。
  • 之前没有设置定时器:返回 0 。

作用alarm() 函数用于设置定时器:经过 seconds 秒后,向调用该函数的进程发送 SIGALRM 定时器信号。

  • 无论传入的 seconds 是多少,调用 alarm 时,上一次设置的定时器都会直接作废。
  • 该函数是不阻塞的。

3. 样例1 (探究 alarm 返回值)

创建一个 alarm.c 文件

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

int main()
{
    int seconds = alarm(10);
    printf("alarm(10)的返回值是: %d\n", seconds);

    sleep(2);

    seconds = alarm(2);
    printf("alarm(2)的返回值是: %d\n", seconds);

    return 0;
}

编译并运行程序

gcc alarm.c -o alarm
./alarm

在这里插入图片描述
通过终端显示发现,第一次调用 alarm 函数,由于前面没有定时器返回值为 0 ,第二次调用 alarm 函数,在 sleep(2) 睡眠了两秒之后,输出的返回值是 10-8=2 ,这符合上面对于 alarm 函数返回值的描述。

4. 样例2 (粗略感受计算机一秒钟识别的数字量)

创建一个 alarm1.c 文件

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

int main()
{
    alarm(1);

    int i = 1;
    while(1)
    {
        printf("i = %d\n", i++);
    }

    return 0;
}

编译并运行程序

gcc alarm1.c -o alarm
./alarm

在这里插入图片描述
但其实这个数字距离 cpu 一秒钟对于数字的处理量误差极大,因为 printf 要操作终端输出, IO 操作的速度要远低于 cpu 的计算速度。这个例子只是大概了解下 alarm 函数的应用场景。

setitimer 函数

1. 头文件

#include <sys/time.h>

2. 函数原型

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

// 间隔定时器结构体
struct itimerval {
    struct timeval it_interval; // 周期性定时的间隔时长
    struct timeval it_value;    // 延迟多久执行定时器
};

// 时间单位结构体
struct timeval {
    time_t      tv_sec;         // 秒
    suseconds_t tv_usec;        // 微秒
};

参数

  • which:定时器类型。下面三类定时器相互独立,互不干扰,但进程要求每种最多只能拥有一个实例。

    • ITIMER_REAL
      按照真实时间倒计时。每次超时触发 SIGALRM 信号。
    • ITIMER_VIRTUAL
      仅统计进程用户态 CPU 耗时(包含进程内所有线程的用户 CPU 时间)。每次超时触发 SIGVTALRM 信号。
    • ITIMER_PROF
      统计进程总 CPU 耗时(用户态 + 内核态,包含进程所有线程)。每次超时触发 SIGPROF 信号。
      可配合 ITIMER_VIRTUAL 对进程的用户态、内核态 CPU 耗时做性能采样分析。
  • new_value:传入参数,新定时器的参数。

  • old_value:传出参数,内核将返回修改前原本的旧定时器参数,如果填 NULL 将不会返回。

返回值

  • 成功返回 0。
  • 失败返回 -1,同时全局变量 errno 。

作用setitimer() 根据 new_value 设置 / 关闭 which 指定的间隔定时器。所谓间隔定时器,就是:定时器先等待一段时间;时间到了以后触发一次;然后如果设置了周期,就继续重复触发。

  • old_value 不为空指针,函数会把定时器修改前的旧状态存入该缓冲区(等价于先调用一次 getitimer 获取旧值)。
  • new_value.it_value 任意一个字段非 0:启用定时器,倒计时至该时长后第一次超时;
  • new_value.it_value 秒、微秒全为 0:关闭定时器。
  • new_value.it_interval 用于设置循环间隔;若该字段全 0,则定时器仅触发一次。

signal 函数

1. 头文件

#include <signal.h>

2. 函数原型

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

解析一下 typedef void (*sighandler_t)(int)
原始类型:返回 void、参数 int 的函数指针:void (*)(int);typedef 给它取别名:sighandler_t

sighandler_t 是一个指针,指向满足下面规则的函数:

  • 函数返回值:void
  • 函数接收 1 个 int 类型参数(对应信号编号 signum

参数

  • signum:信号编号,建议写信号宏值。
  • handler:捕捉信号后的处理方式。
    • SIG_IGN:忽略该信号。
    • SIG_DFL:使用该信号的系统默认处理行为。
    • 自定义函数地址:用户自己编写的信号处理函数。

特殊限制
SIGKILL(强制杀死进程)与 SIGSTOP(暂停进程)这两个信号无法捕获,也不能被忽略。

返回值

  • 成功:返回该信号修改前旧的处理函数指针。
  • 失败:返回 SIG_ERR,同时全局变量 errno 被赋值,标识错误原因。

作用:用于设置信号 signum 的处理方式。

3. handler 为自定义函数地址时,不同系统的处理分歧

执行处理函数前,要么会把该信号的处理方式重置回 SIG_DFL,要么会临时阻塞当前信号(不同系统实现不同);
随后调用自定义处理函数,并把信号编号 signum 作为入参传入。
如果执行处理函数时阻塞了该信号,则函数返回后自动解除阻塞。

分两种分支:
分支 1:老式 BSD 前系统(重置机制)
1. 信号抵达进程;
2. 内核先将该信号处置重置为 SIG_DFL
3. 调用我们传入的 handler
4. handler 返回后,处置已经是默认值,下次信号直接走默认行为。

例如以下代码,在老式 Unix 上运行:第一次按 Ctrl+C → 进入 handler 打印信息;内核自动把 SIGINT 改回默认处理(终止程序);再按一次 Ctrl+C,程序直接退出,不会再进 handler

void handler(int sig) {
    printf("收到 Ctrl+C\n");
}

int main() {
    signal(SIGINT, handler);
    while(1);
}

分支 2:现代 Linux/glibc(阻塞机制)
1. 信号抵达进程;
2. 内核临时阻塞当前信号,不会重置 handler
3. 调用我们传入的 handler
4. handler 执行期间,同信号被屏蔽;
5. handler return 后,自动取消阻塞,handler 依旧有效,可重复触发。

两种分支的原因:早期系统没有信号自动屏蔽机制,设计者担心:handler 内部执行耗时逻辑时,同一个信号连续多次触发、递归调用 handler,造成栈溢出、逻辑混乱。于是简单粗暴设计:触发一次就还原默认,避免递归嵌套。但带来巨大弊端:想要持续捕获信号,必须在 handler 内部重新调用 signal() 注册一遍函数。

setitimer 函数和 signal 函数的样例

创建一个 test.c 文件

#include <stdio.h>
#include <sys/time.h>
#include <signal.h>
#include <stdlib.h>

void itimer_handel(int signum)
{
    printf("触发了信号%d\n", signum);
}

void ctrl_c_handel(int signum)
{
    printf("用户按了Ctrl+C键, 即将中断程序...\n");
    exit(0);
}

int main()
{
    signal(SIGALRM, &itimer_handel);  // 注册捕捉SIGALRM信号处理动作
    signal(SIGINT, &ctrl_c_handel);   // 注册捕捉SIGINT信号处理动作

    // 设置间隔时间为2秒,在4秒后执行定时器
    struct itimerval new_val;
    new_val.it_interval.tv_sec = 2;
    new_val.it_interval.tv_usec = 0;
    new_val.it_value.tv_sec = 4;
    new_val.it_value.tv_usec = 0;

    struct itimerval old_val;

    setitimer(ITIMER_REAL, &new_val, &old_val);

    // 测试是否阻塞
    printf("间隔定时器开始工作...\n");  

    // 查看传出参数old_val
    printf("修改前旧定时器间隔秒:%ld, 微秒: %ld\n", old_val.it_interval.tv_sec, old_val.it_interval.tv_usec);
    printf("修改前旧定时器首次延时秒:%ld, 微秒: %ld\n", old_val.it_value.tv_sec, old_val.it_value.tv_usec);

    while(1);

    return 0;
}

编译并运行程序

gcc test.c -o test
./test

在这里插入图片描述

信号集的概念

1. 概念

  • 许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t
  • 在 PCB 中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为 “未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
  • 信号的 “未决” 是一种状态,指的是从信号的产生到信号被处理前的这一段时间。
  • 信号的 “阻塞” 是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
  • 信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号,所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感的操作。
  • 可以将阻塞信号集与未决信号集看作因果关系,例如阻塞信号集阻塞某个信号,一些行为发送对应信号时因为被阻塞,内核不执行信号处理函数,将该信号在未决集对应位标记为 1 ,始终保持未决态,直到阻塞解除。
    在这里插入图片描述

2. 举例说明

  1. 用户通过键盘 Ctrl + C, 产生2号信号 SIGINT (信号被创建)
  2. 信号产生但是没有被处理 (未决)
    • 在内核中将所有的没有被处理的信号存储在一个集合中 (未决信号集)
    • SIGINT 信号状态被存储在第二个标志位上
      • 这个标志位的值为0, 说明信号不是未决状态
      • 这个标志位的值为1, 说明信号处于未决状态
  3. 这个未决状态的信号,需要被处理,处理之前需要和另一个信号集(阻塞信号集),进行比较
    • 阻塞信号集默认不阻塞任何的信号
    • 如果想要阻塞某些信号需要用户调用系统的 API
  4. 在处理的时候和阻塞信号集中的标志位进行查询,看是不是对该信号设置阻塞了
    • 如果没有阻塞,这个信号就被处理
    • 如果阻塞了,这个信号就继续处于未决状态,直到阻塞解除,这个信号就被处理

信号集相关的函数

SIGKILLSIGSTOP 信号无法被捕捉、阻塞或忽略。若仅在本地 sigset_t 信号集中将二者对应标志位置设置为 1 ,仅会修改程序内存中的位图标记;只有调用 sigprocmask 将该信号集设置为进程阻塞掩码时,内核才会自动忽略这两个信号的置位,不会将其加入进程真实屏蔽字。若要判断进程当前是否阻塞信号,仍需通过 sigismember 查询从内核获取的真实屏蔽掩码。

#define _GNU_SOURCE
#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);

1. sigemptyset 函数

参数

  • set:传出参数,需要操作的信号集。

返回值

  • 成功:返回 0 。
  • 失败:返回 -1 。

作用:清空信号集中的数据,将信号集中的所有标志位置设置为 0 。

2. sigfillset 函数

参数

  • set:传出参数,需要操作的信号集。

返回值

  • 成功:返回 0 。
  • 失败:返回 -1 。

作用:将信号集中的所有的标志位置设置为 1 。

3. sigaddset 函数

参数

  • set:传出参数,需要操作的信号集。

返回值

  • 成功:返回 0 。
  • 失败:返回 -1 。

作用:设置信号集中的某一个信号对应的标志位为 1 ,表示阻塞这个信号。

4. sigdelset 函数

参数

  • set:传出参数,需要操作的信号集。
  • signum:需要设置为不阻塞的信号。

返回值

  • 成功:返回 0 。
  • 失败:返回 -1 。

作用:设置信号集中的某一个信号对应的标志位为 0 ,表示不阻塞这个信号。

5. sigismember 函数

参数

  • set:需要操作的信号集。
  • signum:需要判断是否在信号集内的信号。

返回值

  • 成功:如果信号 signum 在信号集合内,返回 1 ;如果不在集合内,返回 0 。
  • 失败:返回 -1 。

作用:判断某个信号是否阻塞。

6. 样例

创建一个 sigset.c 文件

#define _GNU_SOURCE
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>

int main()
{
    // 创建一个信号集
    sigset_t set;

    // 清空信号集的内容
    sigemptyset(&set);

    // 判断 SIGINT 是否在信号集 set 里
    int ret = sigismember(&set, SIGINT);
    if(ret == 1)
    {
        printf("SIGINT信号阻塞\n");
    }
    else if(ret == 0)
    {
        printf("SIGINT信号不阻塞\n");
    }
    else
    {
        perror("sigismember");
        exit(0);
    }

    printf("-------------------------\n");

    // 将 SIGINT 信号和 SIGQUIT 信号加入信号集
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);
    printf("已将SIGINT和SIGQUIT加入信号集\n");

    // 判断 SIGINT 是否在信号集 set 里
    ret = sigismember(&set, SIGINT);
    if(ret == 1)
    {
        printf("SIGINT信号阻塞\n");
    }
    else if(ret == 0)
    {
        printf("SIGINT信号不阻塞\n");
    }
    else
    {
        perror("sigismember");
        exit(0);
    }

    // 判断 SIGQUIT 是否在信号集 set 里
    ret = sigismember(&set, SIGQUIT);
    if(ret == 1)
    {
        printf("SIGQUIT信号阻塞\n");
    }
    else if(ret == 0)
    {
        printf("SIGQUIT信号不阻塞\n");
    }
    else
    {
        perror("sigismember");
        exit(0);
    }

    printf("-------------------------\n");

    // 将 SIGQUIT 信号移出信号集并再次判断
    sigdelset(&set, SIGQUIT);
    printf("已将SIGQUIT信号移出信号集\n");
    ret = sigismember(&set, SIGQUIT);
    if(ret == 1)
    {
        printf("SIGQUIT信号阻塞\n");
    }
    else if(ret == 0)
    {
        printf("SIGQUIT信号不阻塞\n");
    }
    else
    {
        perror("sigismember");
        exit(0);
    }

    printf("-------------------------\n");

    // 将所有信号加入信号集
    sigfillset(&set);
    printf("所有信号都已被加入信号集\n");

    // 判断 SIGPIPE 是否在信号集 set 里
    ret = sigismember(&set, SIGPIPE);
    if(ret == 1)
    {
        printf("SIGPIPE信号阻塞\n");
    }
    else if(ret == 0)
    {
        printf("SIGPIPE信号不阻塞\n");
    }
    else
    {
        perror("sigismember");
        exit(0);
    }

    return 0;
}

编译并运行程序

gcc sigset.c -o sigset
./sigset

在这里插入图片描述

sigprocmask 函数

1. 头文件

#define _GNU_SOURCE
#include <signal.h>

2. 函数原型

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

参数

  • how:如何对内核阻塞信号集进行处理。

    • SIG_BLOCK
      计算表达式:mask = mask | set
      新阻塞信号集 = 当前阻塞集 与 set 参数信号集 的并集(新增阻塞指定信号)。
    • SIG_UNBLOCK
      计算表达式:mask = mask & ~set
      从当前阻塞信号集中移除 set 里的信号;即使目标信号本就未被阻塞,调用也合法、不会报错。
    • SIG_SETMASK
      计算表达式:mask = set
      直接将线程阻塞信号掩码完整替换为 set 参数指定的信号集。
  • set:已经初始化好的用户自定义的信息集。如果填 NULL ,阻塞信号集不会发生任何修改(how 参数会被忽略),但仍会把当前掩码写入非空的 oldset

  • oldset:传出参数,保存设置之前的内核中的阻塞信号集,可以是 NULL 。

返回值

  • 成功:返回 0 。
  • 失败:返回 -1,同时全局变量 errno 会被赋值以标记错误类型。

作用:将自定义的信号集中的数据设置到内核中的阻塞信号集中(设置阻塞,解除阻塞,替换)。

sigpending 函数

1. 头文件

#define _GNU_SOURCE
#include <signal.h>

2. 函数原型

int sigpending(sigset_t *set);

参数

  • set:传出参数,保存的是内核中的未决信号集中的信息。

返回值

  • 成功:返回 0 。
  • 失败:返回 -1,同时全局变量 errno 会赋值标识错误类型。

作用:获取内核中的未决信号集。

sigprocmask 函数和 sigpending 函数的样例

1. 形象体现阻塞信号集与未决信号集的关系

创建一个 sigprocmask.c 文件

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

int main()
{
    // 创建自定义信号集
    sigset_t set;
    sigemptyset(&set);

    // 对自定义信号集设置 Ctrl+C 和 Ctrl + \ 触发信号的标准位为1
    sigaddset(&set, SIGINT);
    sigaddset(&set, SIGQUIT);

    // 操作当前内核阻塞信号集(与自定义信号集取并集)
    sigprocmask(SIG_BLOCK, &set, NULL);

    int cnt = 1;
    while(1)
    {
        cnt++;

        sigset_t pendingset;
        sigemptyset(&pendingset);

        // 获取未决信号集
        sigpending(&pendingset);
        for(int i = 1; i <= 31; i++)
        {
            int ret = sigismember(&pendingset, i);
            if(ret == 1)
            {
                printf("1");
            }
            else if(ret == 0)
            {
                printf("0");
            }
            else
            {
                perror("sigismember");
                exit(0);
            }
        }
        printf("\n");

        sleep(1);


        if(cnt >= 7)
        {
            sigprocmask(SIG_UNBLOCK, &pendingset, NULL);
        }
    }

    return 0;
}

通过代码可以看到,程序将不断获取内核的未决信号集,并在七秒后,内核的阻塞信号集就会移出获取的未决信号集里的信号。因此在七秒前,即使触发了 SIGINT 或者 SIGQUIT 信号,由于被阻塞,也只会将前 31 号的未决信号集状态输出到终端,等到七秒后,信号不再被阻塞,进程也会终止。

编译并运行程序

gcc sigprocmask.c -o sigprocmask
./sigprocmask

在这里插入图片描述

2. 尝试阻塞 SIGKILL 信号

创建一个 sigpending.c 文件

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

int main()
{
    // 创建自定义信号集
    sigset_t set;
    sigemptyset(&set);

    // 设置自定义信号集中 SIGKILL 信号的标志位为1
    sigaddset(&set, SIGKILL);

    // 尝试阻塞SIGKILL信号
    sigprocmask(SIG_BLOCK, &set, NULL);

    // 发送SIGKILL信号
    kill(getpid(), SIGKILL);

    // 获取内核未决信号集
    sigset_t pendingset;
    sigpending(&pendingset);
    
    // 如果SIGKILL信号被阻塞,那一定会出现在未决信号
    if(sigismember(&pendingset, SIGKILL) == 1)
    {
        printf("阻塞SIGKILL信号成功\n");
    }
}

编译并运行该程序

gcc sigpending.c -o sigpending
./sigpending

在这里插入图片描述
可以发现 SIGKILL 信号无法被阻塞,忽略或捕捉。

SIGCHLD 信号

1. 产生的条件

  • 子进程终止时
  • 子进程接收到 SIGSTOP 信号停止时
  • 子进程处在停止态,接受到 SIGCONT 后唤醒时

以上三种条件都会给父进程发送 SIGCHLD 信号,父进程默认会忽略该信号 (SIG_DFL)。

2. 父进程默认行为与显式忽略的区别

虽然默认情况下父进程确实会 “忽略” SIGCHLD 信号,但这个 “忽略” 和使用 signal(SIGCHLD, SIG_IGN) 进行显式忽略是完全不同的两个概念。

  • 默认状态(SIG_DFL):
    父进程对 SIGCHLD 的默认处理动作是 SIG_DFL(忽略)。在这种情况下,子进程死亡后,内核会将子进程的状态置为僵尸态(Zombie),并保留其 PCB 信息,直到父进程调用 wait()waitpid()。如果父进程一直不调用等待函数,这些僵尸进程就会永久占用系统资源。
  • 显式忽略(signal(SIGCHLD, SIG_IGN)):
    这是一个非常特殊的 POSIX 特性。当你通过 signal(SIGCHLD, SIG_IGN) 设置 SIG_IGN 时,实际是在让内核自动清理死掉的子进程,不要把它们变成僵尸进程。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/time.h>

int main()
{
    signal(SIGCHLD, SIG_IGN);

    pid_t pid = fork();
    if(pid > 0)
    {
        printf("我是父进程, pid: %d\n", getpid());
        while(1);
    }
    else if(pid == 0)
    {
        printf("我是子进程, pid: %d\n", getpid());
    }
    else
    {
        perror("fork");
        exit(0);
    }
}

以上代码对父进程进行死循环,在 ps aux 验证后,发现确实没有僵尸进程的存在。

sigaction 函数

1. 头文件

#define _GNU_SOURCE
#include <signal.h>

2. struct 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);
};

部分架构中 sa_handlersa_sigaction 共用联合体空间,二者只能赋值其中一个,不可同时设置,具体定义如下。

struct sigaction {
    union {
        void (*sa_handler)(int);
        void (*sa_sigaction)(int, siginfo_t *, void *);
    } __sigaction_handler;
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};

// 头文件提供宏,对外屏蔽联合体细节
#define sa_handler     __sigaction_handler.sa_handler
#define sa_sigaction   __sigaction_handler.sa_sigaction
  • sa_restorer
    不使用。应用程序不应使用此字段,POSIX 标准也未定义该成员;它是库内部用于信号返回跳转的跳板函数
  • sa_handler
    捕捉信号后的处理方式。与 signal 函数的参数 handler 一致。
    • SIG_IGN:忽略该信号。
    • SIG_DFL:使用该信号的系统默认处理行为。
    • 自定义函数地址:用户自己编写的信号处理函数。
  • sa_sigaction
    仅当 sa_flags 开启 SA_SIGINFO 时生效,替代 sa_handler 作为处理函数;回调接收三个参数,后文详述。
  • sa_mask
    临时阻塞信号集 (仅信号处理函数执行期间生效):信号处理函数执行期间,该集合内所有信号会被阻塞(加入当前线程信号屏蔽字)。
    另外:触发本次处理函数的信号本身,默认也会被阻塞;若开启 SA_NODEFER 则不会自动阻塞。
  • sa_flags
    行为控制标志位,多个标志按位或组合,可用值如下:
    • SA_NOCLDSTOP
      仅对 SIGCHLD 生效:子进程暂停(SIGSTOP / SIGTSTP / SIGTTIN / SIGTTOU)或恢复运行(SIGCONT)时,不再发送 SIGCHLD 通知父进程。
    • SA_NOCLDWAIT(Linux 2.6 起支持)
      • 仅 SIGCHLD 专用:子进程终止后不会变成僵尸进程,无需父进程调用 wait / waitpid 回收。
      • 此标志仅在为 SIGCHLD 信号建立处理函数,或将该信号的处置方式设置为 SIG_DFL 时才有意义。
      • 若设置该标志,子进程退出时是否产生 SIGCHLD 信号 POSIX 未强制规定;Linux 会正常发送 SIGCHLD,其他系统不一定。
    • SA_NODEFER
      处理函数运行时,不自动屏蔽触发自身的信号(只有 sa_mask 里写的信号才阻塞)。
      意味着处理函数执行途中,同一信号可以再次递达、嵌套执行。
    • SA_ONSTACK
      信号处理函数在备用信号栈上执行(备用栈由 sigaltstack (2) 分配);若无备用栈则使用进程默认栈。
    • SA_RESETHAND
      信号处理函数执行一次后,自动把该信号恢复为默认动作 SIG_DFL 。
    • SA_RESTART
      兼容 BSD 信号语义:被信号中断的阻塞系统调用(read / write / accept 等),在处理函数返回后自动重试,不会直接返回 EINTR 错误。
    • SA_RESTORER
      应用不可使用,C 库内部标记 sa_restorer 为信号跳转函数地址。
    • SA_SIGINFO(Linux 2.2+)
      启用三参数版信号处理函数,此时必须填 sa_sigaction 而非 sa_handler。
    • SA_UNSUPPORTED(Linux 5.11+)
      用于动态检测内核是否支持指定标志位:
      流程:sigaction 设置此标志 + 待检测标志 → 再调用一次 sigaction 获取 oldact;若 oldact 中 SA_UNSUPPORTED 被清空,则剩余置位的标志都是内核支持的。
    • SA_EXPOSE_TAGBITS(Linux 5.11+)
      默认情况下内核会清空 siginfo_t->si_addr 里架构相关的标签位;开启该标志则保留部分标签位。
      兼容旧内核程序需要配合 SA_UNSUPPORTED 做兼容性检测。

3. SA_SIGINFO 模式下的三参数处理函数

开启 SA_SIGINFO 时,回调函数格式:

void handler(int sig, siginfo_t *info, void *ucontext)
{
    // 处理逻辑
}
  • sig:触发回调的信号编号
  • infosiginfo_t 结构体指针,存放信号详细来源、错误信息
  • ucontextucontext_t 上下文结构体(强转 void*),内核保存的进程寄存器上下文;多数业务场景不会使用。

siginfo_t 结构体关键字段说明:

siginfo_t {
    int      si_signo;     // 信号编号
    int      si_errno;     // 错误码(Linux下基本无用)
    int      si_code;      // 信号产生原因码(核心字段)
    int      si_trapno;    // 硬件异常陷阱号,多数架构未使用
    pid_t    si_pid;       // 发送方进程PID
    uid_t    si_uid;       // 发送方真实用户ID
    int      si_status;    // 子进程退出状态 / 触发信号值
    clock_t  si_utime;     // 子进程用户态CPU耗时
    clock_t  si_stime;     // 子进程内核态CPU耗时
    union sigval si_value; // sigqueue 附带的自定义数值
    int      si_int;       // POSIX实时信号整型数据
    void    *si_ptr;       // POSIX实时信号指针数据
    int      si_overrun;   // 定时器溢出次数
    int      si_timerid;   // 内核定时器ID(≠timer_create返回值)
    void    *si_addr;      // 内存访问出错地址(段错误、总线错误等)
    long     si_band;      // IO事件掩码(SIGIO/SIGPOLL)
    int      si_fd;        // 触发IO事件的文件描述符
    short    si_addr_lsb;  // 地址最低有效位,用于标记损坏内存大小
    void    *si_lower;     // 地址越界下限(边界检查错误)
    void    *si_upper;     // 地址越界上限
    int      si_pkey;      // 内存保护键ID
    void    *si_call_addr; // 触发seccomp的系统调用指令地址
    int      si_syscall;   // 触发seccomp的系统调用号
    unsigned int si_arch;  // 系统调用对应CPU架构
}

结构体内部大量字段是联合体,仅对应信号类型的字段才有有效数据,其余不可读取。

不同信号对应的有效字段

  1. kill () / sigqueue () 发送信号
    • kill:填充 si_pid、si_uid
    • sigqueue:额外填充 si_int /si_ptr(发送时传入的自定义数据)
  2. POSIX 定时器信号
    • si_timerid、si_overrun 有效,记录定时器溢出次数
  3. 消息队列通知 SIGEV_SIGNAL
    • si_int/si_ptr(创建通知时传入的值)、si_pid、si_uid
  4. SIGCHLD(子进程状态变更)
    • si_pid(子进程 PID)、si_uid、si_status(退出码 / 终止信号)、si_utime/si_stime(子进程 CPU 时间)
  5. SIGILL / SIGFPE / SIGSEGV / SIGBUS / SIGTRAP(硬件异常信号)
    • si_addr 保存出错内存地址;部分错误填充 si_addr_lsb、si_lower、si_upper、si_pkey
  6. SIGIO / SIGPOLL IO 事件信号
    • si_fd(事件 fd)、si_band(poll 事件掩码,POLLIN / POLLOUT 等)
  7. SIGSYS(seccomp 拦截非法系统调用)
    • si_call_addr、si_syscall、si_arch 记录被拦截的调用信息

4. 函数原型

int sigaction(int signum, const struct sigaction *_Nullable restrict act, struct sigaction *_Nullable restrict oldact);

参数

  • sigsum:信号编号,建议写信号宏值。不能是 SIGKILLSIGSTOP,这两个信号无法自定义处理。
  • act:非空指针时,将 act 中的配置设置为该信号新的处理规则。
  • oldact:传出参数,非空指针时,调用结束后会把该信号旧的处理配置存入该结构体。

返回值

  • 成功:返回 0
  • 失败:返回 -1,并设置全局变量 errno 标记错误类型

作用:该函数用于修改进程收到指定信号时执行的处理动作。

5. 样例1 (间隔定时器)

创建一个 sigaction.c 文件

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

void myhandel(int signum)
{
    printf("触发了SIGALRM信号, 编号为%d\n", signum);
}

int main()
{
    // 创建sigaction结构体
    struct sigaction sigact;
    sigemptyset(&sigact.sa_mask);       // 清空临时阻塞信号集
    sigact.sa_handler = myhandel;     
    sigact.sa_flags = 0;
    
    // 注册信号处理动作
    sigaction(SIGALRM, &sigact, NULL);
    
    // 创建一个 延迟4秒后启动,定时间隔为2秒 的间隔定时器
    struct itimerval new_val;
    new_val.it_interval.tv_sec = 2;
    new_val.it_interval.tv_usec = 0;
    new_val.it_value.tv_sec = 4;
    new_val.it_value.tv_usec = 0;
    setitimer(ITIMER_REAL, &new_val, NULL);

    while(1);

    return 0;
}

编译并运行该程序

gcc sigaction.c -o sigaction
./sigaction

在这里插入图片描述
可以发现间隔定时器的处理函数正常触发。

6. 样例2 (解决防止僵尸进程时阻塞父进程问题)

无论是 wait 还是 waitpid ,如果将防止子进程变成僵尸进程的处理用普通死循环的形式实现,会阻塞父进程的功能实现。
创建一个 sigzombie.c 文件

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

int cnt = 0;

void sigzombie(int signum)
{
    printf("触发信号, 编号: %d\n", signum);

    while(1)
    {
        int ret = waitpid(-1, NULL, WNOHANG);  // 不阻塞父进程
        if(ret > 0)    
        {
            printf("一个子进程死亡, cnt: %d\n", ++cnt);
        }
        else if(ret == 0)
        {
            printf("仍有子进程存在........\n");
            break;
        }
        else if(ret == -1)  // 没有子进程存在了
        {
            break;
        }
    }
}

int main()
{
    // 先阻塞SIGCHLD信号,防止注册动作还未完成,子进程就已经死亡变成僵尸进程
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set, SIGCHLD);

    sigprocmask(SIG_BLOCK, &set, NULL);

    // 创建15个子进程
    pid_t pid;
    for(int i = 1; i <= 15; i++)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }

    if(pid > 0)   // 父进程
    {
        // 注册SIGCHLD信号的处理动作
        struct sigaction act;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        act.sa_handler = sigzombie;
        sigaction(SIGCHLD, &act, NULL); 

        // 注册操作完成后解开内核对SIGCHLD信号的阻塞
        sigprocmask(SIG_UNBLOCK, &set, NULL);

        while(1)
        {
            printf("我是父进程, pid: %d\n", getpid());
            sleep(1);
        }
    }
    else if(pid == 0)
    {
        printf("我是子进程, pid: %d\n", getpid());
    }
    else if(pid == -1)
    {
        perror("fork");
        exit(0);
    }

    return 0;
}

编译并运行程序

gcc sigzombie.c -o sigzombie
./sigzombie

在这里插入图片描述

  • 可能会有疑惑 SIGCHLD 属于标准信号(不可靠信号):内核信号队列里同一个信号只会保存一份。如果连续多个子进程同时死亡、批量产生 SIGCHLD,但是内核只会给父进程发送 1 个 SIGCHLD 标记,会不会导致僵尸进程的出现?
    其实并不会,虽然 waitpid 函数一次只能回收一个子进程,但所幸它作用于一个 while(1) 中,可以持续释放死亡的子进程资源。
  • 那会不会有子进程趁着 SIGCHLD 信号的处理动作未注册前死亡,又因为该信号父进程默认忽略,导致僵尸进程的出现?
    其实并不会,在 main 函数的开头就对 SIGCHLD 信号进行阻塞,在信号处理动作注册完毕后解除阻塞,保证一定可以对这些信号进行处理,也就保证了不会有漏掉的僵尸进程。
  • 为什么不直接把信号处理动作的注册放在 main 函数最开头呢?
    • fork 函数的读时共享此昂,写时复制机制将会复制提前注册的信号,但是子进程完全不需要处理该信号。
    • 如果那样的话,注册操作和 fork 是割裂的。在 fork 的过程中,信号处理函数是随时可能被触发的(如果此时有子进程死了)。如果信号处理函数被触发,父进程的 for 循环会被中断,执行完 sigzombie 后再回到 for 循环。这种随机中断在复杂的系统编程中,极容易导致 errno 被修改、全局变量冲突等难以调试的 Bug。

7. 样例3 (尝试用 struct sigaction 结构体中的 sa_flags 自动回收僵尸进程)

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

int main()
{
    // 尝试 flag 参数中 SA_NOCLDWAIT 的威力
    struct sigaction act;
    sigemptyset(&act.sa_mask);
    act.sa_flags = SA_NOCLDWAIT;
    act.sa_handler = SIG_DFL;
    sigaction(SIGCHLD, &act, NULL); 

    // 创建15个子进程
    pid_t pid;
    for(int i = 1; i <= 15; i++)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }

    if(pid > 0)   // 父进程
    {
        while(1)
        {
            printf("我是父进程, pid: %d\n", getpid());
            sleep(1);
        }
    }
    else if(pid == 0)
    {
        printf("我是子进程, pid: %d\n", getpid());
    }
    else if(pid == -1)
    {
        perror("fork");
        exit(0);
    }

    return 0;
}
Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐