信号的原理
生活中存在信号的场景,例如信号弹、下课上课铃声、求偶、红绿灯、快递发短信取件码、旗语、狼烟、发令枪、军训哨子、闹钟等等。根据生活中的例子,可以明白信号是有人告知的,即便是没有信号产生,但是当信号产生之后,我们知道应该做些什么事情;其次,当信号产生时,我们并不会立即处理这个信号,而是在合适的时候去做这件事情,因为此时存在更重要的事情。
信号产生后------存在时间窗口------信号处理时------在这个时间窗口内必须记住信号的到来。
信号的原理:
- 进程必须识别并处理信号,并且信号没有产生,也要具备处理信号的能力------信号的处理能力,属于进程内置功能的一部分。
- 进程即便没有收到信号,也能够知道如何处理信号
- 当进程真的收到信号的时候,进程可以立即处理,也可能并不会立即处理这个信号,而是选择合适的时候处理信号。
- 一个进程必须当信号产生的时候,到信号开始被处理,就一定会有时间窗口,此时要求进程具有临时保存信号已经发生了的能力。
cpp
int main()
{
while(true)
{
cout << "I am a crazy process" << endl;
sleep(1);
}
return 0;
}

问题:ctrl + c 为什么能够杀掉前台进程?
【解释】Linux中,一次登录中,一个终端,一般会配上一个bash,每一个登录,只允许一个进程是前台进程,可以允许多个进程是后台进程。
一般来讲,谁来获取键盘输入,谁就是前台进程。当执行上面的代码时,会将bash进程改变为后台进程,此时执行ls、pwd命令时,无法显现出来内容。当执行可执行程序的时候,在可执行程序后面加上&,可以将该程序在后台执行。
所以,ctrl + c是键盘输入,只有前台进程可以获取到;同时ctrl+c本质是被进程解释成为了2号信号。

指令:kill -l
【功能】查看信号列表
这其中,1~30号信号是普通信号,31~64是实时信号。
信号的处理方式:
- 默认动作
- 忽略
- 自定义动作------信号的捕捉
就比如,进程收到2号信号的默认动作是终止自己进程。
接口:设置自定义信号的处理方法,修改特定进程对信号的处理动作的。参数signum:表示信号列表里面的参数。
参数handler:表示对信号进行自定义动作。
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
// 参数表示收到哪一个信号
void myhandler(int signo)
{
cout << "process get a signal: " << signo << endl;
}
int main()
{
signal(SIGINT, myhandler);
while(true)
{
cout << "I am a crazy process" << endl;
sleep(1);
}
return 0;
}

通过修改自定义信号的处理方式,将2号信号捕捉,修改成自定义信号(打印内容),现在只能通过kill -9 [pid]来杀掉进程。
【说明】有些信号可以自定义动作,有效信号不可以自定义动作。
【注意】signal只需要设置一次,后续的代码在运行的时候都会有效。如果在后续的代码中,产生信号才会执行自定义动作,如果没有信号产生,则后续就不会出现自定义动作。
++问题:键盘数据是如何输入给内核的,ctrl+c是如何变成信号的(硬件)++
【解释】键盘被输入,肯定是操作系统先得知的。键盘属于外设,键盘可以给CPU发送硬件中断,来让操作系统读取键盘的数据,每一个外设都会给CPU发送硬件中断,每一个硬件中的都有自己的中断号,硬件通过中断单元发送给CPU。软件中,开机时在操作系统中形成中断向量表,中断向量表存在方法的地址(直接访问外设的方法------主要是磁盘、显示器、键盘等),读取键盘的方法被保存在向量表中。一旦CPU中触发中断号,操作系统会识别CPU中的中断号,操作系统会以中断号为索引,来执行这个方法。信号就是通过软件方式对进程模拟的硬件中断。操作系统会通过分析从键盘获取到的是数据还是控制信号(例如ctrl+c),如果是控制信号,就会发送给进程。
总的来讲,就是用户按ctrl+c这个键盘输入产生一个硬件中断,被操作系统获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
【总结】
- ctrl+c产生的信号只能发给前台进程。一个命令后面加&可以放到后台运行,这样shell不必等等进程结束就可以接受新的命令,启动新的进程。
- shell可以同时运行一个前台进程和多个后台进程,只有前台进程才能接到像ctrl+c这种控制键产生的信号。
前台进程在运行过程中用户随时可能按下ctrl+c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。
信号是进程之间事件异步通知的一种方式,属于软中断。
信号的产生
键盘组合键
可以通过键盘组合键的方式(例如ctrl+c产生2号信号,ctrl+\产生3号信号,ctrl+z产生19号信号)
2号信号SIGINT的默认处理动作是终止进程,3号信号SIGQUIT的默认处理动作是终止进程并且Core Dump。
相较于2、3号信号,19号信号SIGSTOP并没有会被捕捉,所以并不是所有的信号都可以被捕捉的。

在0~31号进程中,只有9号信号和19号信号不能被捕捉,其他均可以被捕捉。
使用kill命令
指令:kill -[signal] [pid]
【功能】执行一个信号
使用系统调用接口
kill接口

cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;
void Usage(string proc)
{
cout << "Usage:\n\t" << proc << " signum pid\n\n";
}
// mykill signum pid
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signum = stoi(argv[1]);
pid_t pid = stoi(argv[2]);
int n = kill(pid, signum);
if(n == -1)
{
perror("kill");
exit(2);
}
return 0;
}

raise系统调用

cpp
int main()
{
int cnt = 5;
while(cnt)
{
cout << "process pid : " << getpid() << endl;
if(cnt == 2)
{
raise(9);
}
cnt--;
sleep(1);
}
return 0;
}
abort接口

abort也是一个封装后的函数,给自己的进程发送6号信号。abort函数除了执行6号信号,在函数内部也执行了退出进程的操作。
异常条件
信号产生无论是如何产生都是通过操作系统来发送给进程的,因为操作系统是进程的管理者。
cpp
#include <signal.h>
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
cout << "div befer" << endl;
sleep(1);
int a = 10;
a /= 0;
cout << "div after" << endl;
sleep(1);
return 0;
}
执行上述代码的结果是:

进程收到8号信号。

没有修改8号信号的默认动作时,当出现异常,触发8号信号打印""并退出进程;当自定义8号信号的动作只是打印,进程并没有退出,而是一直出发8号信号并执行打印动作。
同样的例子还有:野指针问题,一旦发生段错误,进程就会一直触发11号信号,直到进程退出。

当进程出现异常问题时,进程并不会直接退出,而是触发某一个信号,然后执行该信号的默认动作。
【问题】为什么除零或者野指针会给进程发送信号呢?
解释:当出现除零或者野指针问题,操作系统会给进程发送对应的信号。CPU中存在一种状态寄存器,状态寄存器中有一个溢出标志位,当出现除零错误时,状态寄存器中的溢出标志位由0变成1出异常(虽然修改的时CPU内部的状态寄存器,但是只会影响自己的进程)。CPU在调度该进程时出异常,操作系统得知异常后会向该进程发送信号。
CPU是硬件,操作系统是硬件的管理者!
当出现异常的信号被捕捉后,进程不退出,当前进程会一直被调度,而CPU中的寄存器一直处于异常状态,操作系统又会触发信号。
软件条件
异常并不是只是由硬件产生的,管道中读端关闭写端一直在写,此时操作系统会发送13号信号SIGPIPE,这种情况是软件异常。
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
【功能】调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。

cpp
int n = alarm(5);
while(1)
{
cout << "proc is running..." << endl;
sleep(1);
}

alarm的特点:设置一次闹钟只会响一次闹钟
cpp
void handler(int signo)
{
cout << "get a signo: " << signo << endl;
alarm(5);
}
// 设置一次闹钟后再次设置一次闹钟
int main()
{
signal(SIGALRM, handler);
int n = alarm(5);
while(1)
{
cout << "proc is running..." << endl;
sleep(1);
}
return 0;
}


cpp
#include <signal.h>
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
int cnt = 500;
while(cnt--)
{
cout << "I am a child process, pid: " << getpid() << " cnt: " << cnt << endl;
sleep(1);
}
exit(0);
}
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
cout << "child quit info, rid: " << rid
<< " exit code: " << ((status >> 8) & 0xFF)
<< " exit signal: " << (status & 0x7F)
<< " core dump: " << ((status >> 7) & 1) << endl;
}
return 0;
}

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这就叫做Core Dump。
指令:ulimit -c 1024
【功能】打开core dump限制,允许产生core文件。
打开系统的core dump功能,一旦进程出现异常,操作系统会将进程在内存中的运行信息,给dump(转储)到进程的当前目录(磁盘)形成core pid文件,这种功能称为核心转储。进程异常终止通常是因为有Bug,就比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清楚错误原因,这叫做Post-mortem Debug(事后调试)。
信号的保存
对于普通信号而言,信号是发送给进程的PCB结构体中使用位图操作管理信号的。
- 位图中比特位的内容是0还是1,表明是否收到信号
- 比特位的位置(第几个),表示信号的编号
- 所谓的"发送信号",本质就是操作系统修改目标进程task_struct信号位图对应的比特位置。
因为操作系统是进程的管理者,只有它有资格去修改task_struct内部的属性。
进程在收到信号之后,可能不会立即处理这个信号。而信号不会被立即处理,就需要有一个时间窗口。
信号的相关概念
- 实际执行信号的处理动作称为信号递达。
- 信号从产生到递达之间的状态称之为信号未决。
- 进程可以选择阻塞某一个信号。
信号的范围是[1, 31],每一种信号都要有自己的一种处理方法。这种处理方法在底层都是函数指针数组。

进程可以通过指针查找三张表,pening表可以查看是否收到信号(位图结构),handler表里保存信号指导的方法的函数指针,block表同样是利用位图的思想来选择是否选择阻塞一个信号。通过三张表可以对信号进行记录保存相关的工作。

- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
信号被阻塞,但是信号依旧会被发送,直到进程信号解除阻塞,才可以执行递达之后的动作。
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号在内存的表示
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

- 在上图中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所有暂时不能递达。虽然其处理动作是忽略,但在没有接触阻塞之前不能忽略这个信号,因为进程仍然有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
问题:在Linux中如果再进程接触对某个信号的阻塞之前这种信号产生过多次,将会如何处理?
【答】在Linux中常规信号在递达之前产生多次,只计一次,而实时信号在递达之前产生多次可以依次放在放在一个队列中。
sigset_t
sigset_t称为信号集,这个类型可以表示每个信号的"有效"或者"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。
信号集操作函数
sigset_t类型对于每种信号用一个bit位来表示"有效"或者"无效"状态,通过调用以下函数来操作sigset_t变量。
- 头文件#include<signal.h>
- int sigemptyset(sigset_t *set);
函数sigempty初始化set所指向的信号集,使其中所有信号的对应bit位置清零,表示该信号集不包含任何有效信号。
- int sigfillset(sigset *set);
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit位置置1,表示该信号集的有效信号包括系统支持的所有信号。
- int sigaddset(sigset *set, int signo);
在信号集中添加一个信号
- int sigdelset(sigset_t *set, int signo);
在特定的信号集中删除一个信号
- int sigismember(const sigset_t *set, int signo);
一个信号是否存在于信号集中。
【注意】在使用sigset_t类型的变量之前,一定要调用sigemptyset或者sigfillset进行初始化,使得信号集处于确定的状态。
sigprocmask接口
通过调用函数sigprocmask可以读取或者更改进程的信号屏蔽字(阻塞信号集),也就是读取或者修改block信号集。
- #include<signal.h>
- int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 返回值:若成功则为0,若出错则为-1。
- how参数有三种可选参数,这三个选项只能三选一。

- set参数是一个输入型参数,用于修改进程block表中的字段。
- oset参数是一个输出型参数,用于保存上一次的block表的字段,以便恢复。
sigpending函数接口
- #include<signal.h>
- int sigpending(sigset_t *set);
- 读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1.

cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
void PrintPending(sigset_t &pending)
{
for(int signo = 31; signo >= 1; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
cout << "catch a signo: " << signo << endl;
}
int main()
{
// 0. 对2号信号进行自定义捕捉
signal(2, handler);
// 1. 先对2号信号进行屏蔽------数据预备
sigset_t bset; // 在用户栈上的,属于用户区
sigemptyset(&bset);
sigaddset(&bset, 2); // 仅仅只是修改bset变量的值,并没有设置到进程的task_struct中
// 1.2 调用系统调用
sigset_t oset; // 用于保存上一次信号集
sigemptyset(&oset);
sigprocmask(SIG_SETMASK, &bset, &oset);
// 2. 重复打印当前进程的pending 00000000 00000000 00000000 00000000
sigset_t pending;
int cnt = 0;
while(1)
{
// 2.1 获取
int n = sigpending(&pending);
if(n < 0) continue;
// 2.2 打印
PrintPending(pending);
sleep(1);
cnt++;
// 2.3 解除阻塞
if(cnt == 20)
{
cout << "unblock 2 signo" << endl;
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
// 3. 发送2号信号 00000000 00000000 00000000 00000010
return 0;
}
【注意】信号不能都进行屏蔽,就比如9、19号进程不可以被屏蔽
cpp
void PrintPending(sigset_t &pending)
{
for(int signo = 31; signo >= 1; signo--)
{
if(sigismember(&pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handler(int signo)
{
cout << "catch a signo: " << signo << endl;
}
int main()
{
// 测试
sigset_t bset, oset;
sigemptyset(&bset);
sigemptyset(&oset);
for(int i = 0; i <= 31; ++i)
{
sigaddset(&bset, i);
}
sigprocmask(SIG_SETMASK, &bset, &oset);
sigset_t pending;
while(true)
{
int n = sigpending(&pending);
if(n < 0) continue;
PrintPending(pending);
sleep(1);
}
return 0;
}

信号的捕捉
当进程从内核态时返回到用户态的时候,进行信号的检测与处理。
当执行系统调用的时候,操作系统会将用户身份转换成内核身份,在内核态中处理对应的函数,然后再返回到用户态进而实行后续代码------也就是说操作系统是自动去做身份切换的"从用户到内核"。
谈一谈进程地址空间

问题:当系统中存在多个进程的时候,用户页表有几份、内核页表有几份?
【答】:用几个进程就存在几份用户级页表,因为进程具有独立性;内核级页表只有一份,每一个进程看到的内核空间都是一样的,也就意味着整个系统中,进程不管如何切换,内核空间都是不变的。在进程视角查看时,当调用系统调用接口时,就是在自己的进程地址空间中进行执行的。站在操作系统角度查看时,任何一个时刻都会有进程在执行,想要执行操作系统的代码,就可以随时访问。
操作系统的本质,是基于时钟中断的一个死循环。在计算机硬件中,有一个时钟芯片,会每隔很短的时间,都会向计算机发送时钟中断。
内核态:允许访问操作系统的代码和数据。
用户态:只能访问用户自己的代码,不能访问操作系统的代码和数据。


如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
【例子】当用户程序设置SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或者异常切换到内核态,在中断处理完毕后要返回用户态的main函数之前会检查是否有信号递达,发现SIGQUIT信号递达后,内核决定返回用户态后不是恢复main函数的上下文执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
信号捕捉的方法
信号捕捉除了signal函数,还有下面的函数接口:
sigaction

- act是输入型参数,oldact是输出型参数,这两个参数都是结构体。
该函数既可以捕捉普通信号也可以捕捉实时信号,对于需要捕捉普通信号而言,只需要了解两个内容:
- void (*sa_handler)(int);------捕捉信号需要的处理方法。
- sigset_t sa_mask;
简单使用sigaction函数:
cpp
#include <cstring>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "catch a signal, signal number : " << signo << endl;
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
sigaction(2, &act, &oact);
while(true)
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}
问题:pending位图是什么时候从1被修改成0的?
cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintPending()
{
sigset_t set;
sigpending(&set);
for(int signo = 1; signo <= 31; ++signo)
{
if(sigismember(&set, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << "\n";
}
void handler(int signo)
{
PrintPending(); // 在这里打印pending表,来测试pending位图是处理handler信号之前修改还是处理之后修改的。
cout << "catch a signal, signal number : " << signo << endl;
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
sigaction(2, &act, &oact);
while(true)
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}

通过实验时发现,操作系统是先将pending位图由1变成0,再去调用handler方法,也就是在执行捕捉方法之前,先清0,再调用。
问题:当处理2号信号,可以再次触发2号信号吗?
cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintPending()
{
sigset_t set;
sigpending(&set);
for (int signo = 1; signo <= 31; ++signo)
{
if (sigismember(&set, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << "\n";
}
void handler(int signo)
{
cout << "catch a signal, signal number : " << signo << endl;
while (true) // 使用死循环,测试当执行2号信号时,再次发送2号信号的情况
{
PrintPending(); // 在这里打印pending表,来测试pending位图是处理handler信号之前修改还是处理之后修改的。
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
sigaction(2, &act, &oact);
while (true)
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}

不可以!当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(也就是block表),当信号处理函数返回时自动恢复原来的信号屏蔽字。这样也就保证了在处理某个信号的时候,如果这种信号再次产生,那么它会被阻塞当前处理结束为止。
关于sigaction结构体中的sigset_t sa_mask变量:
- sa_mask的类型是sigset_t。
- 当处理2号信号时,2号信号会自动屏蔽2号信号,但是如果还想屏蔽更多信号,
cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
using namespace std;
void PrintPending()
{
sigset_t set;
sigpending(&set);
for (int signo = 1; signo <= 31; ++signo)
{
if (sigismember(&set, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << "\n";
}
void handler(int signo)
{
cout << "catch a signal, signal number : " << signo << endl;
while (true) // 使用死循环,测试当执行2号信号时,再次发送2号信号的情况
{
PrintPending(); // 在这里打印pending表,来测试pending位图是处理handler信号之前修改还是处理之后修改的。
sleep(1);
}
}
int main()
{
struct sigaction act, oact;
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
// 屏蔽其他(1,3,4)信号
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 1);
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
sigaction(2, &act, &oact);
while (true)
{
cout << "I am a process: " << getpid() << endl;
sleep(1);
}
return 0;
}
sa_mask会处理当处理一个信号时屏蔽其他信号。
可重入函数

main函数调用inset函数像一个链表head中插入节点node1,插入操作分为两步,当刚结束完第一步的时候,因为硬件中断使进程切换到内核,再次回到用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都完成后从sighandler返回到内核态,再次回到用户态继续执行main函数的insert函数的后续代码,现在执行第二步,此时main函数和sighandler先后向链表中插入两个节点,最后只有一个节点被真正插入,而另外一个节点丢失,造成了内存泄漏。
【现象】insert函数被main和handler执行流重复进入
【问题】节点丢失,内存泄漏
- 如果一个函数被重复进入的情况下,出错或者可能出错称之为不可重入函数,否则叫做可重入函数。
目前的大部分函数都是不可重入函数
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或者free,因为malloc也是用全局链表来管理堆的。
- 调用了标志库I/O库函数。标志I/O库的很多实现都以不可重入的方式使用全局数据结构的。
volatile
cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
using namespace std;
int flag = 0;
void handler(int signo)
{
cout << "catch a signal: " << signo << endl;
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag); // 当flag=0,!flag为真,当flag=1, !flag为假。
cout << "Process quit normal" << endl;
return 0;
}

执行该程序时,当发送2号信号时,循环结束,程序退出。
但是优化条件下,flag变量可能被直接优化到CPU内的寄存器中,在编译期间可以携带-O0选项(没有优化),-O1选项(优化级别1),-O3选项(优化级别3).



不同的优化级别会产生不同的效果,根据优化导致CPU会直接读取寄存器中的内容,而不会选择读取内存中的内容。
- 例如register是一个建议型关键字,意思是能把变量放在CPU中就把变量放在寄存器中。
volatine关键字可以防止变量过度优化,也就是一直存放在内存中,而不会选择放在CPU中的寄存器中。与register相当于是相反的两个方向。

volatile int flag = 0;防止编译器过度优化,保持内存的可见性。
SIGHLD信号
子进程退出时,不是悄悄退出,而是在退出的时候是会主动的向父进程发送SIGCHLD(17)号信号。

cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signo)
{
cout << "I am process: " << getpid() << " catch a signo: " << signo << endl;
}
int main()
{
signal(17, handler);
pid_t id = fork();
if(id == 0)
{
while(true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(1);
}
exit(0);
}
// father
while(true)
{
cout << "I am father process: " << getpid() << endl;
sleep(1);
}
return 0;
}

子进程在进行等待的时候,我们可以采用基于信号的方法等待。
进程等待的好处:
- 获取子进程的退出状态
- 释放子进程的僵尸状态
- 虽然不知道父子谁先运行,但是一定是父进程最后退出。
子进程退出时,还是需要调用wait/waitpid这样的接口,也就是说父进程必须保证自己是一定在运行的。
将子进程等待写入到信号捕捉函数中:
cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
using namespace std;
void handler(int signo)
{
pid_t rid = waitpid(-1, nullptr, 0);
cout << "I am process: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl;
}
int main()
{
signal(17, handler);
pid_t id = fork();
if(id == 0)
{
while(true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
// father
while(true)
{
cout << "I am father process: " << getpid() << endl;
sleep(1);
}
return 0;
}

问题:如果存在10个进程,需要同时退出或者退出一半进程?
同时退出10个进程的代码:
cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <ctime>
using namespace std;
void handler(int signo)
{
pid_t rid;
while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
{
cout << "I am process: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl;
}
}
int main()
{
signal(17, handler);
for (int i = 0; i < 10; ++i)
{
pid_t id = fork();
if (id == 0)
{
while (true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
}
// father
while (true)
{
cout << "I am father process: " << getpid() << endl;
sleep(1);
}
return 0;
}
退出部分进程的代码(使用不同时间):
cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <ctime>
using namespace std;
void handler(int signo)
{
pid_t rid;
while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0)
{
cout << "I am process: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl;
}
}
int main()
{
srand(time(nullptr));
signal(17, handler);
for (int i = 0; i < 10; ++i)
{
pid_t id = fork();
if (id == 0)
{
while (true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
sleep(rand() % 5 + 3);
}
// father
while (true)
{
cout << "I am father process: " << getpid() << endl;
sleep(1);
}
return 0;
}
事实上,由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。此方法对于Linux可以使用,但是不保证在其它类UNIX系统上都可用。
cpp
#include <iostream>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <ctime>
using namespace std;
int main()
{
signal(17, SIG_IGN);
for (int i = 0; i < 10; ++i)
{
pid_t id = fork();
if (id == 0)
{
while (true)
{
cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;
sleep(5);
break;
}
exit(0);
}
sleep(1);
}
// father
while (true)
{
cout << "I am father process: " << getpid() << endl;
sleep(1);
}
return 0;
}



