文章目录
- 🌈 一、信号的概念
- ⭐ 1. 什么是信号
- ⭐ 2. 常见的信号
- ⭐ 3. 信号的管理
- 🌈 二、进程的运行
- ⭐ 1. 进程运行模式
- ⭐ 2. 查看后台进程
- ⭐ 3. 运行后台进程
- ⭐ 4. 终止后台进程
- 🌈 三、信号的产生
- ⭐ 1. 通过键盘产生信号
- ⭐ 2. 调用系统函数向进程发送信号
- 🌙 2.1 kill 给任意进程发送任意信号
- 🌙 2.2 raise 给进程本身发送任意信号
- 🌙 2.3 abort 使当前进程异常终止
- ⭐ 3. 硬件异常产生信号
- ⭐ 4. 软件条件产生信号
- 🌈 四、信号的捕捉
- ⭐ 1. 自定义捕捉
- ⭐ 2. 无法被捕捉的信号
- ⭐ 3. 内核如何实现信号的捕捉
- ⭐ 4. 用户态和内核态
- ⭐ 5. sigaction 检查并修改信号的处理动作
- 🌈 五、信号的阻塞
- ⭐ 1. 信号的状态
- ⭐ 2. 信号在内核中的表示
- ⭐ 3. sigset_t 信号集
- ⭐ 4. 信号集操作参数
- ⭐ 5. sigprocmask 设置阻塞信号集
- ⭐ 6. sigpending 获取未决信号集
- ⭐ 7. 无法被阻塞的信号
- 🌈 六、信号的补充
- ⭐ 1. volatile 保持内存的可见性
- ⭐ 2. SIGCHLD 信号
🌈 一、信号的概念
⭐ 1. 什么是信号
1. 信号的本质
- 信号本质是一种向目标进程发送通知的机制。
- 用户或操作系统 (OS) 通过发送指定的信号,通知进程某些事件已经发生了,进程可以在后续进行信号处理。
2. 进程为什么会认识信号
- 进程要处理信号,就必须要识别出对应的信号,并知道对应的信号意味着什么、要让进程执行什么操作。
- 进程能识别信号,是 OS 将常见的信号及信号处理动作内置到进程的代码和属性中。
3. 信号是异步产生的
- 信号的产生是不确定的,当信号产生时,进程可能正在处理某些任务,没办法立即去处理这个信号。信号的产生相对于进程正在做的工作,是异步产生的。
- 进程会暂时将产生的信号记录下来,等到合适的时候再去处理记录下来的信号所要执行的操作。
4. 进程要能够暂存到来的信号
- 进程在处理任务时,如果此时信号到来,进程要有能力将到来的信号记录下来。
⭐ 2. 常见的信号
1. 常见的信号
- 在命令行中输入
kill -l
指令可以查看系统定义的信号列表。 - 普通信号:编号 1 ~ 31 的信号,普通信号使用位图管理,当前只讨论普通信号。
- 实时信号:编号 34 ~ 64 的信号,需要 OS 立即处理的信号,一般不会出现信号丢失。
2. 缺失的信号
- 没有 0 号信号:进程的退出信号为 0,表示进程在执行期间没有收到任何信号,进程是正常退出的。0 号信号是专门用来标识进程正常结束的情况。
⭐ 3. 信号的管理
- 每个进程对于信号都持有两个东西:函数指针数组、信号位图。
1. 信号位图
- 信号编号在 1 ~ 31 的信号是普通信号。
- 在进程 PCB 中,存在一个位图,该位图比特位的位置决定信号编号;比特位的内容决定是否收到信号。
- 每个信号在位图中的位置都不同,可通过这种方式识别是几号信号。这样只需要在进程 PCB 中开一个 32 位的整形变量即可管理好普通信号集。
2. 信号的函数指针数组
- 每个进程都有属于自己的一个函数指针数组,数组的下标和信号编号一致。
- 当进程收到信号时,就会去执行对应下标处的函数指针指向的函数中的任务。
🌈 二、进程的运行
⭐ 1. 进程运行模式
- 进程在执行的时候,分别有前台执行和后台执行两种执行模式。
1. 前台执行
- 一般是以
./xxx
的方式将进程放到前台执行,前台进程在系统中只能有 1 个 (因为只有一个键盘)。
2. 后台执行
- 一般是以
./xxx &
的方式将进程放到后台执行, 一般将耗时较长的任务放到后台执行 (如下载),后台进程在系统中可以有多个。
⭐ 2. 查看后台进程
- 可以在命令行输入
jobs
指令查看在后台的进程。
⭐ 3. 运行后台进程
- 执行后台进程的方式有两种,主要看该进程在不在后台,如果在后台还要看是不是暂停状态。
1. 将进程放到后台执行
- 可以使用
./xxx &
的方式将进程放到后台执行,一般将耗时较长的任务放到后台执行 (如下载),后台进程在系统中可以有多个。- 如:将 ./process.exe & 进程放到后台执行。
2. 执行在后台暂停的进程
- 如果后台的进程处于暂停 (Stopped) 状态,可以使用
bg 后台进程编号
将暂停的进程运行起来。- 如:当前有一个处于暂停状态的 ./process.exe 后台进程,其后台进程编号为 1,现将其运行起来。
⭐ 4. 终止后台进程
1. 将进程移到前台后终止
- 在命令行使用
fg 后台进程编号
将后台进程提到前台,然后使用 ctrl + c 或别 kill 指令将该前台进程干掉即可。- 如:当前有一个处于运行状态的 ./process.exe >> log.txt & 后台进程,其后台进程编号为 1,现将其提起到前台后使用 ctrl + c 终止。
2. 使用 kill -9 pid 终止后台进程
- 不管进程在哪里执行,肯定有自己的 pid,只需要找到后台执行的进程的 pid 然后对其发送 kill -9 信号即可。
- 在使用 ./xxx & 执行后台进程时,会显示该后台进程的 pid,使用 kill -9 pid 将该进程干掉即可。或者使用 ps -al 指令找到想干掉的进程的 pid,然后再 kill 掉它。
- 如:当前有一个处于运行状态的 ./process.exe >> log.txt & 后台进程,其后台进程编号为 1,现在将其 kille 掉。
🌈 三、信号的产生
- 由于 OS 是进程的管理者,因此无论信号有多少种产生方式,永远只能由 OS 向目标进程发送。
⭐ 1. 通过键盘产生信号
1. 终止进程 ctrl + c
- 可以按下
ctrl + c
向前台进程发送 SIGINT (2 号) 信号终止进程本身 (只能终止在前台执行的进程)。- 例:当前有一个执行死循环打印的进程,现在通过 ctrl + c 将其终止。
2. 暂停进程 ctrl + z
- 可以按下
ctrl + z
向前台进程发送 SIGSTOP (19 号) 暂停信号。 - 由于不能暂停前台进程 (OS 会挂掉),因此 ctrl + z 只会将前台进程移到后台暂停。
- 因此 ctrl + z 更多时候是用来将前台执行的进程移到后台暂停。
3. 退出进程 ctrl + \
- 可以按下
ctrl + \
向前台进程发送 SIGQUIT (3 号) 信号,让进程退出。
⭐ 2. 调用系统函数向进程发送信号
🌙 2.1 kill 给任意进程发送任意信号
- kill 命令其实是通过调用系统调用 kill 函数实现的。
1. kill 函数原型
#include <signal.h>
#include <sys/types.h>
int kill(pid_t pid, int sig);
- 功能:向 pid 指定的进程发送 sig 所指定的信号。
- 参数:pid 表示的就是进程的 pid,表示要对哪个进程发送信号。sig 表示要对 pid 所指向的进程发送的信号编号。
- 返回:如果信号发送成功则返回 0;失败则返回 -1,并设置错误码。
2. kill 函数用例
- 实现一个向指定的进程发送信号的 mykill 程序。
- mkill.cpp 文件:用以实现 kill 指令的逻辑。
// 当前文件: mykill.cpp
#include <string>
#include <iostream>
#include <signal.h>
using std::cout;
using std::endl;
using std::stoi;
using std::string;
void usage(const string &process)
{
cout << "\nusage: " << process << " sig pid" << endl;
}
// 使用方式: mykill.exe -9 pid
int main(int argc, char *argv[])
{
if (3 != argc)
{
usage(argv[0]);
exit(0);
}
int sig = stoi(argv[1] + 1); // 获取信号编号
pid_t pid = stoi(argv[2]); // 获取进程 pid
int n = kill(pid, sig); // 向 pid 所指定的进程发送 sig 信号
return 0;
}
- mytest.cpp 文件:实现不停的打印内容,等待接收 mykill 发送过来的 9 号信号。
// 当前文件: mytest.cpp
#include <unistd.h>
#include <iostream>
using std::cout;
using std::endl;
int main()
{
while (true)
{
cout << "I am a process, pid: " << getpid() << endl;
sleep(1);
}
}
🌙 2.2 raise 给进程本身发送任意信号
- 进程本身可以使用
raise
函数向自己发送指定信号。
1. raise 函数原型
#include <signal.h>
int raise(int sig);
- 参数:sig 任意信号编号。
- 返回:成功返回 0,失败返回 -1 并设置错误码。
2. raise 函数用例
- 让进程自身不停的向自己发送 2 号信号,然后对其进行捕捉。
#include <unistd.h>
#include <signal.h>
#include <iostream>
using std::cout;
using std::endl;
void print(int sig)
{
cout << "这是一个专门捕捉 " << sig << " 号信号的实例" << endl;
}
int main()
{
signal(2, print); // 捕捉 2 号信号
while (true)
{
raise(2); // 不停的向进程自己发送 2 号信号
sleep(1);
}
}
🌙 2.3 abort 使当前进程异常终止
1. 函数原型
#include <stdlib.h>
void abort(void);
- 功能:向调用该函数的进程发送 SIGABRT (6 号) 信号引起异常终止。
2. 函数用例
#include <stdlib.h>
#include <iostream>
using std::cout;
using std::endl;
int main()
{
abort(); // 调用该函数的进程会发生异常终止
cout << "异常终止后走不到这里" << endl;
return 0;
}
⭐ 3. 硬件异常产生信号
- 当硬件发生异常时,OS 会将对应的溢出标志位在系统层面上解释成 kill(pid, sig) 向目标进程发送特定的信号。
- 例:当前进程执行了除零的指令时,CPU 的运算单元会产生异常,内核将这个异常解释为 SIGFPE (8 号) 信号并发送给进程。
- 例:当前进程访问非法内存地址时,内存管理单元 (MMU) 会产生异常,内核将这个异常解释为 SIGSEGV (11 号) 信号发送给进程。
⭐ 4. 软件条件产生信号
- 因为软件问题而向目标进程发送信号被称之为软件条件。
- SIGPIPE (13 号) 就是一种由软件条件产生的信号,在管道的读端关闭时,写端还一直在写入,OS 就会向写端进程发送该信号。
- 还有一种由软件产生信号的例子就是 alarm 函数。
1. alarm 函数原型
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
- 功能:设定一个闹钟,让 OS 在 seconds 指定的秒数后向当前进程发送 SIGALRM (14 号) 信号 (该信号默认为终止当前进程)。
- 参数:指定多少秒之后触发闹钟,seconds 如果是 0,则取消所有还未触发的闹钟。
- 返回:如果设定的是第一个闹钟则返回 0,如果不是则返回以前设定的闹钟时间还余下的秒数。
- 如果设了一个 30s 后响的闹钟 A,但在第 20s 又设了个闹钟 B,此时第二次 alarm 的返回值就是 10,表示上个闹钟 A 还有 10s 触发。
2. alarm 函数用例
- 设定一个 1 秒之后的闹钟,在这一秒内不停的向屏幕打印内容,看看能打印几行。
#include <unistd.h>
#include <iostream>
using std::cout;
using std::endl;
int main()
{
alarm(1); // 1 秒之后终止进程
for (size_t count = 0; true; count++)
cout << "count = " << count << endl;
return 0;
}
🌈 四、信号的捕捉
⭐ 1. 自定义捕捉
- 信号可以被自定义捕捉,进程接收到信号后可以不执行本来应该执行的任务,而是去执行自己定义的任务。
- 可以使用 signal 函数对信号进行捕捉。
1. signal 函数原型
#include <signal.h>
typedef void (*sighandler_t)(int); // 函数指针
sighandler_t signal(int signum, sighandler_t handler);
2. signal 函数参数
- signum:指定你要捕捉的是哪个信号,就盯着指定的这个信号看。
- handler:更改进程持有的函数指针数组中信号对应下标位置的函数指针所指向的函数为指定函数。捕捉到指定信号之后,去执行该函数指针所指向的函数,并且要将捕捉到的信号作为参数传递给该函数。
3. signal 函数用例
- 编写一个专门捕捉 2 号信号的进程,将进程收到 2 号信号要执行的任务从终止进程编程打印,使得 ctrl + c 无法终止该进程。
#include <iostream>
#include <signal.h>
using std::cout;
using std::endl;
void print(int sig)
{
cout << "这是一个专门捕捉 " << sig << " 号信号的实例" << endl;
}
int main()
{
// 只要发送 2 号信号就会被捕捉,然后去执行 print 函数
signal(2, print);
while(true);
return 0;
}
⭐ 2. 无法被捕捉的信号
- 如果所有的信号都能被自定义捕捉的话,某个进程将所有的信号全部给捉了去,该进程就无敌了,没有人能干掉它,OS 也不行。
- 显然 OS 是不允许这样的事发生的,因此 OS 设置了几个无法被捕获的信号。
无法被自定义捕捉的信号
- SIGKILL (9 号) 信号:这是一个用来立即结束程序的信号,不能被忽略、阻塞或捕捉。进程一旦接收到该信号,将被无条件终止。
- SIGSTOP (19 号) 信号:该信号用于停止 (挂起) 进程的执行,不能被忽略、阻塞或捕捉。
⭐ 3. 内核如何实现信号的捕捉
- 信号会在合适的时候被处理,那么什么时候合适的时候呢?
1. 信号处理的时机
- 进程从内核态返回到用户态的时候,会进行信号的检测和信号的处理。
- 系统调用背后,就包含了身份的变化。
2. 信号捕捉的过程
- 当进程从内核态返回用户态时 (如系统调用返回、中断处理完毕等),内核会检查 pending 表中是否有待处理的信号。
- 如果有信号待处理且该信号未被阻塞,内核会查找 handler 表以确定该信号的处理方式。
- 如果信号的处理方式是用户自定义的 (即捕捉信号),内核会创建一个新的堆栈帧,用于保存当前进程的上下文 (如寄存器状态、信号掩码等),并调用用户定义的信号处理函数。
⭐ 4. 用户态和内核态
- 用户态:一种受控的状态,能够访问的资源是有限的,只能访问自己的 [0, 3] GB 地址空间。
- 内核态:是一种操作系统的状态,能够访问大部分系统资源,能让用户以 OS 的身份访问 [3, 4] GB 的地址空间。
- 内核级页表在整个操作系统中只有一张,因此无论进程如何调度,CPU 都能直接找到 OS。
⭐ 5. sigaction 检查并修改信号的处理动作
1. sigaction 函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- 该函数可以读取和修改与指定信号相关联的处理动作。
- 调用成功则返回 0,出错则返回 -1。
2. sigaction 函数参数
- signum:想修改的信号的编号。
- act:如果 act 指针非空,则根据 act 修改 signum 所指定信号的处理动作。
- oldasct:如果 oldact 指针非空,则通过 oldact 获取 signum 所指定信号的原来的处理动作。
3. sigaction 函数用例
- 捕捉 2 号信号,并将 2 号信号的默认处理动作变成当前进程的 handler 函数中。
#include <iostream>
#include <signal.h>
#include <unistd.h>
using std::cout;
using std::endl;
void handler(int sig)
{
cout << "这是一个专门捕捉 " << sig << " 号信号的函数" << endl;
}
int main()
{
cout << "当前进程的 pid 为: " << getpid() << endl;
struct sigaction act;
struct sigaction oldact;
act.sa_handler = handler;
sigaction(2, &act, &oldact);
while (true)
sleep(1);
return 0;
}
🌈 五、信号的阻塞
⭐ 1. 信号的状态
- 信号有 3 种状态。
1. 信号递达
- 实际执行信号的处理动作称之为信号递达 (delivery)。信号递达之后的处理动作有 3 种:
- 执行信号的默认执行动作:
signal(信号编号, SIG_DFL);
对指定信号执行其默认动作。 - 忽略信号:
signal(信号编号, SIG_IGN);
忽略递达的信号,让该信号啥也干不了。 - 对信号的自定义捕捉处理:就是信号的捕捉。
- 执行信号的默认执行动作:
2. 信号未决
- 信号从产生到递达之间的状态称为信号未决 (pending)。
- 将信号保存在信号位图叫做信号未决。
3. 信号阻塞
- 进程可以选择阻塞 (block) 某个信号。被阻塞的信号将保持在 pending 状态,直到进程解除对该信号的阻塞,才去执行递达的动作。
- 信号阻塞和信号忽略不同:信号被阻塞时就不会被递达;信号忽略是在递达之后可选的一种处理动作。
⭐ 2. 信号在内核中的表示
1. 进程要维护信号的三种状态
- 信号的三种状态在 OS 中是要兑现的,OS 在进程中会维护三张表。
- 分别是:阻塞 (block) 表、未决位图 (pending) 表、handler 表。
- block 表:该表是个位图结构,比特位的位置表示信号的编号,比特位的内容表示否是对特定的信号进行阻塞。
- 对应位 n 上的值为 1 表示 n 号信号被阻塞,0 表示 n 号信号未被阻塞。
- pending 表:该表是用于保存信号的位图结构,比特位的位置表示信号的编号,比特位的内容表示否是收到特定编号的信号。
- 对应位 n 上的值为 1 表示收到了 n 号信号,0 表示未收到 n 号信号。
- handler 表:该表是个函数指针数组,信号编号是该数组的下标,该数组的内容是对应信号编号的处理方法。
2. 信号的识别
- 针对特定某个信号,横着看这 3 张表即可。
⭐ 3. sigset_t 信号集
- 由于 pending 和 block 表都是用位图表示的,因此,未决和阻塞表可以用相同的数据类型 sigset_t 来存储。
- sigset_t 数据类型被称为信号集,这个类型可以表示每个信号的 “有效” 或 “无效” 状态。
- 在阻塞信号集中: “有效” 和 “无效” 的含义是该信号是否处于阻塞状态;
- 在未决信号集中: “有效” 和 “无效” 的含义是该信号是否处于未决状态。
- sigset_t 说白了就是个结构体,结构体中有个数组,通过这个实现位图。
⭐ 4. 信号集操作参数
- 由于 sigset_t 操作集是个位图,不应该由用户对这个位图实施置 0 置 1 的操作,OS 提供了以下函数来操作 sigset_t 信号集。
#include <signal.h>
int sigemptyset(sigset_t *set); // 将信号集的全部位变成 0
int sigfillset(sigset_t *set); // 将信号集的全部位变成 1
int sigaddset(sigset_t *set, int signo); // 将指定信号添加到信号集中 (将特定比特位变为 1)
int sigdelset(sigset_t *set, int signo); // 将指定信号从信号集中删除 (将特定比特位变为 0)
int sigismember(const sigset_t *set, int signo); // 判断信号集的有效信号中是否包含指定信号 (判断特定比特位是否为 1)
- sigemptyset 函数:用于将信号集中的全部比特位变成 0,成功返回 0,失败则返回 -1.
#include <iostream>
#include <signal.h>
using std::cout;
using std::endl;
int main()
{
sigset_t set;
sigemptyset(&set);
for (auto val : set.__val)
cout << val << " ";
cout << endl;
return 0;
}
- sigfillset 函数:用于将信号集中的全部比特位变成 1,成功返回 0,失败则返回 -1.
#include <iostream>
#include <signal.h>
using std::cout;
using std::endl;
int main()
{
sigset_t set;
sigfillset(&set);
for (auto val : set.__val)
cout << val << " ";
cout << endl;
return 0;
}
- sigaddset 函数:将指定信号添加到信号集中 (将特定比特位变为 1),成功返回 0,失败则返回 -1.
#include <iostream>
#include <signal.h>
using std::cout;
using std::endl;
int main()
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, 5); // 将 5 号比特位置为 1
for (auto val : set.__val)
cout << val << " ";
cout << endl;
return 0;
}
- sigdelset 函数:将指定信号从信号集中删除 (将特定比特位变为 0),成功返回 0,失败则返回 -1.
#include <iostream>
#include <signal.h>
using std::cout;
using std::endl;
int main()
{
sigset_t set;
sigemptyset(&set);
sigaddset(&set, 5);
sigdelset(&set, 5); // 将 5 号比特位置位 0
for (auto val : set.__val)
cout << val << " ";
cout << endl;
return 0;
}
- sigismember 函数:判断信号集的有效信号中是否包含指定信号 (判断特定比特位是否为 1),若包含则返回 1,不包含则返回 0,出错返回-1。
#include <iostream>
#include <signal.h>
using std::cout;
using std::endl;
int main()
{
sigset_t set;
sigaddset(&set, 5);
// 判断 5 号信号是否在信号集中
cout << sigismember(&set, 5) << endl;
return 0;
}
⭐ 5. sigprocmask 设置阻塞信号集
- 前面的对 sigset_t 信号集的修改都只是对局部变量的修改,并没有修改进程本身的 block 表和 pending 表。
- 想要修改进程本身的 block 表还得借助
sigprocmask
函数。
1. sigprocmask 函数原型
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
// 成功返回 0,失败返回 -1
- how:表示准备如何设置当前进程的 block 表。
- set:如果该参数非空,则该参数是用来修改当前进程的 block 表的。
- oldset:如果该参数非空,则该参数是用来获取当前进程的 block 表的。
2. how 参数的可选项
选项 | 说明 |
---|---|
SIG_BLOCK | set 当中包含了希望添加到当前进程的 block 表的信号 |
SIG_UNBLOCK | set 当中包含了希望从当前进程的 block 表中解除阻塞的信号 |
SIG_SETMASK | 将当前进程的 block 表设置成 set 的内容 |
3. sigprocmask 函数用例
- 实现对 2 号信号的阻塞。
#include <iostream>
#include <signal.h>
#include <unistd.h>
using std::cout;
using std::endl;
void handler(int sig)
{
cout << "这是一个专门捕捉 " << sig
<< " 号信号的函数" << endl;
}
int main()
{
sigset_t block;
sigset_t oblock;
// 将信号集的全部比特位变成 0
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block, 2); // 将 block 变量的 2 号比特位置 1
sigprocmask(SIG_BLOCK, &block, &oblock); // 将 block 变量的内容设置到进程的 block 表中
while (true)
sleep(1);
return 0;
}
⭐ 6. sigpending 获取未决信号集
- 通过 sigset_t 信号集获取当前进程的 pending 表。
1. sigpending 函数原型
#include <signal.h>
int sigpending(sigset_t *set);
2. sigpending 函数用例
#include <iostream>
#include <signal.h>
using std::cout;
using std::endl;
int main()
{
sigset_t set;
sigpending(&set); // 获取当前进程的 pending 表的 0 1 序列
for (auto val : set.__val)
cout << val << " ";
cout << endl;
return 0;
}
⭐ 7. 无法被阻塞的信号
- 并非所有信号都可以被进程阻塞。有一些信号是出于系统安全和稳定性的考虑,被设计为不可被阻塞的。
无法被阻塞的信号
- SIGKILL (9 号):用于立即终止进程,不能被阻塞、忽略或捕获。一旦进程接收到SIGKILL信号,它将立即被终止,不进行任何清理操作。
- SIGSTOP (19):用于暂停进程的执行,同样不能被阻塞、忽略或捕获。进程接收到 SIGSTOP 信号后,会立即停止执行,直到接收到 SIGCONT 信号才继续执行。
🌈 六、信号的补充
⭐ 1. volatile 保持内存的可见性
- volatile 关键字功能:告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行。
1. 编译器会对代码进行优化
- 标准情况:正常编译。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
int flag = 0;
void handler(int sig)
{
cout << "sig: " << sig << endl;
flag = 1;
cout << "change flag to: " << flag << endl;
}
int main()
{
signal(2, handler);
cout << "pid: " << getpid() << endl;
while (0 == flag);
cout << "quit normal!" << endl;
return 0;
}
- 输入 ctrl + c 后,2号信号被捕捉,执行自定义动作,修改 flag为1,while 条件不满足,退出循环,进程退出。
- 优化情况:代码不变,在编译时调整一下优化级别。
- 输入 ctrl + c 后,2 号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!
- flag 此时已经被修改了,循环依旧在执行,因为 while 循环检查的 flag,并不是内存中最新的 flag,出现了二义性。
- 编译器没看见 main 函数中有谁修改 flag,因此直接将 flag 放进 CPU 寄存器中,whilie 检测的是寄存器中的 flag 而不是内存中的。
2. volatile 不准将变量放进寄存器
- 被 volatile 关键字修饰的变量,每次访问都必须从内存中读取取。
- 用 volatile 修饰了 flag 之后,while 每次访问都必须跑到内存中去读取 flag 然后进行判断。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using std::cout;
using std::endl;
// 不允许将该变量优化进寄存器
volatile int flag = 0;
void handler(int sig)
{
cout << "sig: " << sig << endl;
flag = 1;
cout << "change flag to: " << flag << endl;
}
int main()
{
signal(2, handler);
cout << "pid: " << getpid() << endl;
while (0 == flag);
cout << "quit normal!" << endl;
return 0;
}
⭐ 2. SIGCHLD 信号
- 子进程在终止时会给父进程发送 SIGCHLD (17 号) 信号,该信号的默认处理动作是忽略,父进程可以自定义 SIGCHLD 信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用 wait 清理子进程即可。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
using std::cout;
using std::endl;
void handler(int sig)
{
cout << "捕捉到一个 " << sig << " 号信号" << endl;
}
int main()
{
signal(SIGCHLD, handler);
pid_t id = fork();
// 子进程
if (0 == id)
{
cout << "子进程正在运行" << endl;
exit(1);
}
// 父进程等待子进程
wait(nullptr);
return 0;
}