需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)
在生活中,我们会有各种各样的信号出现,比如发令枪、红绿灯、闹钟等等。我们拿红绿灯举例:
人为什么能够识别红绿灯信号?
首先有一个前提条件是识别包括认识和行为产生,通俗来说就是能够认识什么是红灯什么是绿灯,行为产生的意思就是知道红灯的时候要做什么,绿灯的时候要做什么。
我们为什么能识别红绿灯呢?
曾经有人教育过,让我们在大脑中记住了红绿灯的属性和对应的行为
当信号到来的时候,我们不一定立马处理这个信号
由于信号可以随时产生(异步的),此时我们可能做着更重要的事情
信号到来的时候我们需要记住这个信号(时间窗口),确保将来信号会被处理
处理信号会有一些处理动作,这些动作一般分为三个
默认动作、忽略动作、自定义动作
首先明确一个共识:信号是发给进程的
我们在刚接触进程的时候,了解过一个
kill
命令,可以通过kill命令向指定pid的进程发送9号信号,杀死进程kill -9 $pid
所以我们知道,信号是发送给进程的
那么,如何将我们在上面讲的生活中的信号的概念对应到进程中?
进程是如何识别信号的? 认识+动作,进程需要知道每个信号是什么,知道每个信号对应的行为是什么
进程本身是由程序员编写的属性和逻辑的集合,是由程序员编码完成的,所以进程对信号和认识和对应的动作都是由程序员编码完成的
进程本身要对信号具有保存能力 在收到信号的时候,进程可能在执行更重要的任务,不一定立刻处理信号,所以进程本身要对信号具有保存能力。**那么信号被保存在哪里呢?**我们知道每个进程都有对应的PCB,在Linux下就是task_struct
结构体中。**如何保存?**在task_struct中有位图结构保存了收到的信号,用比特位的位置代表信号的编号,比特位对应的值1,0分别代表是否收到该信号。
进程对信号的处理 进程对信号的处理有三种动作:默认、忽略、自定义 处理信号也可也被称作信号的捕捉
如何理解信号的发送 我们知道信号是保存在task_struct中的信号位图结构中的,所谓的发送信号,实际上就是修改进程task_struct中的信号位图结构的内容,我们知道task_struct是OS内核数据结构,只有OS有修改权限,所以只有OS有向进程发送信号的能力。所以无论我们未来学习多少种发送信号的方式,其本质都是通过OS向目标进程发送信号!
这里说的是只有OS有向进程发送信号的能力,不代表只有OS能够操作信号的发送。可以理解成我附庸的附庸不是我的附庸,虽然我们没有办法越过OS直接向进程发送信号,但是我们能控制OS向进程发送信号,这个控制的方式就是系统调用,因此我们可以推测出之前使用的kill命令也是调用了对应的系统调用
我们可以使用kill -l
指令查看系统定义的信号列表
OS一共定义了61个信号,没有0、32、33号信号
可以使用man 7 signal
查看每个信号对应的含义
Linux提供了61个信号,其中[1,31]被称为普通信号,[34,64]被称为实时信号,其中实时信号这里不做讲解
31个普通信号的具体含义已经做了讲解,具体见博文:
信号的生命周期主要分为三个部分:信号产生、信号保存、信号处理
接下来,将会按照信号的生命周期的顺序来讲解信号
我们知道,在命令行下,如果一个前台程序出现了问题,可以使用CTRL+C
终止掉这个程序,这实际上就是向这个前台程序发送2号信号。
在man 7 signal中的解释
Signal Value Action Comment
──────────────────────────────────────────────────────────────────────
SIGINT 2 Term Interrupt from keyboard
是由键盘产生的信号,值是2,信号是SIGINT
,表示从键盘发送的中断的信号
所以当我们ctrl+c的时候该进程直接进入结束状态
键盘是硬件,通过组合键按下给OS识别,OS将组合键解释成信号,向目标进程发信号,目标进程在合适的时候处理这个信号
我们在上面说到,只有OS有向进程发送信号的权限,所以所有外部向进程发送的信号都要通过系统调用,这里提供三个系统调用
使用kill指令向任意进程发送任意信号
头文件:
#include
#include
函数原型:
int kill(pid_t pid, int sig);
参数解释:
pid:表示发送信号的对象进程的pid
sig:表示要发送的信号
返回值:
调用成功返回0,失败返回-1同时设置错误码
实验:使用kill系统调用模拟实现kill指令
#include
#include
#include
static void Usage(const char* cmd)
{
std::cout << "\nUsage:" << cmd << " pid signo" << std::endl;
}
// ./mykill pid signo
int main(int argc, char* argv[])
{
if(argc != 3) // 处理输入参数不对的情况
{
Usage(argv[0]);
exit(1);
}
//转换第二个和第三个参数
pid_t pid = atoi(argv[1]);
int signo = atoi(argv[2]);
int ret = kill(pid, signo);
if(ret != 0)
{
perror("kill:");
}
return 0;
}
发送了9号信号,进程被杀死
使用raise给进程自己发送任意信号。raise是C语言提供的一个接口
头文件:
#include
函数原型:
int raise(int sig);
参数解释:
sig:表示向自己发送的信号
返回值:
调用成功返回0,否则返回一个非0的值
可以使用kill模拟实现
// 下面两条代码含义相同
kill(getpid(), signo);
raise(signo);
使用abort给进程自己发送6号信号
头文件:
#include
函数原型:
void abort(void);
返回值:由于调用之后就会发送6号信号,中断进程,返回值无意义,所以返回值类型为void
可以使用kill或raise模拟实现
// 下面三条代码含义相同
kill(getpid(), SIGABRT);
raise(SIGABRT);
abort();
关于的行为的理解:有很多的情况进程收到大部分的信号,默认处理动作都是终止进程。
信号的意义:信号的不同代表不同的事件,都是对事件发生之后的处理动作是可以一样的。
信号的产生处理用户显示发送之外,在计算机内部也会自动产生,我们看一个例子
这里出现了除0错误,在CPU的运算器中有一个状态寄存器保存了运算结果的状态,当发生除0运算的时候,运算器产生了一个非常大的结果,这个结果会出现溢出,所以状态寄存器的溢出标记为就会被标记,向OS发送SIGFPE
信号,表示浮点数运算异常
signal接口可以捕捉到任意信号,并将这个信号对应的行为自定义
头文件: #include
typedef void(*sighandler_t)(int); 函数原型: sighandler_t signal(int signum, sighandler_t handler); 参数解释: sighandler_t:这是一个被typedef的类型,表示一个函数指针,这个函数的返回值是void,有一个int类型的参数 signum: 表示要操作的信号编号 handler:signum信号要绑定的行为的函数指针
捕捉信号的证明:
#include
#include
#include
void catchSig(int signo)
{
std::cout << "捕捉到一个信号,编号是:" << signo << std::endl;
exit(-1);
}
int main()
{
signal(SIGFPE, catchSig); // 把SIGFPE信号
while(true)
{
std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl;
sleep(1);
int a = 10;
a /= 0;
}
return 0;
}
如果我们定义的catchSig
函数中没有进程退出,那么会出现什么情况呢?
这个情况是因为我们说信号的发送是OS改变进程PCB中的信号位图,那么在我们自己实现的catchSig
中没有让OS将这个信号位图恢复,所以OS在调度进程的过程中,会将这个进程重复调度,所以每次都会处理这个信号,就导致了刷屏这个现象
除了除0异常之外,还有空指针解引用等一系列的一场,我们可以再做一个实验来看看
#include
#include
#include
void catchSig(int signo)
{
std::cout << "捕捉到一个信号,编号是:" << signo << std::endl;
exit(-1);
}
int main()
{
signal(SIGSEGV, catchSig); // 把SIGFPE信号
while(true)
{
std::cout << "我是一个正在运行的进程,pid: " << getpid() << std::endl;
sleep(1);
int* p = nullptr;
*p = 10;
}
return 0;
}
1. 管道信号——SIGPIPE
我们在之前的文章中说到,进程间通信的管道,如果把所有的读端关闭,那么写端的所有写入都是没有意义的,所以如果所有读端都被关闭,那么OS就会向写端发送信号SIGPIPE
,让写端关闭这个对应管道.
2. 定时器——14号信号SIGALRM
我们调用alarm
系统调用,可以给OS设定一个闹钟,在时间到了之后,OS会给当前进程发送SIGALRM信号
头文件:
#include
函数原型:
unsigned int alarm(unsigned int seconds);
参数解释:
seconds:表示设定的闹钟时间,单位是秒
返回值:
若调用alarm函数前,进程已经设置了闹钟,则返回上一个闹钟时间的剩余时间,并且本次闹钟的设置会覆盖上一次闹钟的设置。如果调用alarm函数前,进程没有设置闹钟,则返回值为0。
例如:我们可以使用这个系统调用辅助,来测试我们的机器1s能够进行多少次加法运算
#include
#include #include int count = 0; void catchSig(int signo) { std::cout << "捕捉到一个信号,编号是:" << signo << std::endl; std::cout << "运算次数:" << count << std::endl; exit(-1); } int main() { signal(SIGALRM, catchSig); alarm(1); while(true) { count++; } return 0; }
进程在接收到绝大多数信号之后的行为都是终止进程,出现不同信号的原因是为了使对应信号产生不同的行为。在man 7 signal
中看到信号对应的Action
大多是Term和Core两种,那么这两种的区别是什么?
但是,在云服务器上,如果进程是core退出的,我们暂时看不到明显现象,如果想看到的话,需要打开一个选项
可以看到,云服务器默认关闭了core file
,可以使用ulimit -c 1024
指令,在当前系统中形成最大可以为1024个blocks的数据块,保存core file文件。
在开启之前,我们运行下面一段程序:
int main()
{
while(true)
{
int a[10];
a[10000] = 10;
}
return 0;
}
出现段错误,没有其他任何显示
开启corefile之后:多的文件的文件名为core,后缀名为引起core的进程的pid
所谓的核心转储就是当进程出现异常的时候,我们在进程对应的时刻,在内存中的有效箱数据转储到磁盘中
为什么要有核心转储?
为了把问题保存下来,支持调试
如何支持调试?
使用gdb
这种调试方法叫做事后调试
所以core和term的区别就是**通过core结束的进程可以支持生成核心转储文件**
信号的一些相关概念:
我们知道信号是需要被保存的,根据我们现有知识,可以推测出信号是保存在task_struct中的。实际上,在Linux下,有三个数据结构用于保存信号相关数据。
1. pending位图
pending位图结构的理解:信号被OS发送,被当前进程接收之后不会被立刻处理,所以需要进行保存,这里就使用pending位图保存。
pending位图的结构的定义是unsigned int pending
,这个位图结构是一个32位的无符号整型。这个位图结构包括两个部分:1.每个数据位的位置2.每个数据位的内容0/1
其中位置表示的是不同的信号,数据位的内容0表示没有接收到该信号,1表示接收到该信号
2. block位图
block位图的理解:进程可以选择阻塞某个信号,阻塞的实现就是通过block位图来实现的,block的结构和pending的结构是一样的,是一个32位的无符号整型变量unsigned int block
。只是对于位图结构的定义不同
1.每个数据的位置:表示的是不同信号
2.每个数据位的内容0/1。0表示该信号没有被阻塞,1表示该信号被阻塞
3. handler_t数组
handler_t数组的理解:每一个信号都需要有一个对应的行为(也就是对应的函数)这个数组有32个元素,从1开始的每个元素都代表了一个回调函数。
handler_t类型:typedef void(*handler_t)(int signo)
handler_t数组的定义:handler_t handler[32] = {0};
当进程准备处理信号的时候,block位图对应位置位0,pending位图对应位置位1,就调用对应的handler数组中的回调函数
我们在4.1.1中讲了pendign和block的数据类型是unsigned int
,但是实际上他们的类型被封装过,封装后的体现是sigset_t
类型,这是因为在Linux中除了我们今天讲到的31个普通信号之外,还有31个实时信号,为了支持这些内容和提供扩展,所以Linux封装了sigset_t类型,这个类型的实现最终是一个结构体
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
typedef __sigset_t sigset_t;
上文中说到了PCB中保存信号的结构,那么对应的,这些数据结构也需要对应的操作方法。同时,上述的数据结构是内核的数据结构,程序员没办法直接操作这些数据结构,所以OS必定需要提供一些接口/系统调用让程序员能够操作这些数据。
头文件:
#include
函数原型:
int sigemptyset(sigset_t *set); // 将set位图的所有位置0
int sigfillset(sigset_t *set); // 将set位图的所有位置1
int sigaddset(sigset_t *set, int signum); // 在set位图中吧signum信号对应的比特位置1
int sigdelset(sigset_t *set, int signum); // 在set位图中吧signum信号对应的比特位置0
int sigismember(const sigset_t *set, int signum); // 判断signum信号对应的比特位内容是0还是1
参数解释:
set:需要操作的位图
signum:需要操作的信号编号
返回值:
调用成功返回0,否则返回-1,对于sigismember函数,若包含signum信号则返回1,不包含则返回0,出错返回-1。
头文件:
#include
函数原型:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
函数描述:
读取/更改进程的信号屏蔽字,也就是block位图结构
参数解释:
how:要如何操作信号屏蔽字,有三个模式:
SIG_BLOCK,将set中包含的信号添加到当前信号屏蔽字中(block|set)。
SIG_UNBLOCK,将set中包含的信号从当前信号屏蔽字中移除(block&~set)。
SIG_SETMASK,将当前信号屏蔽字重置为set。(block=set)
set:需要操作的信号位图
oldset:执行操作前的信号屏蔽字结构的保存,是一个输出型参数
返回值:
操作成功返回0,失败返回-1同时设置错误码
注意:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
头文件:
#include
函数原型:
int sigpending(sigset_t *set);
函数描述:
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。程 序如下
参数解释:
输出型参数,获取到的pending位图
返回值:
如果调用成功就返回0,否则返回-1,同时设置错误码
在讲解信号捕捉流程之前需要加一点补充知识:这里对内核态和用户态进行一个简单的讲解
内核空间和用户空间
我们在进程地址空间的时候讲过对于任意一个进程,都有一个虚拟的进程地址空间大小为4G(在32位OS下)其中的高地址1G空间(0xC0000000到0xFFFFFFFF)保存内核数据。这个1G大小的空间就是内核空间,低地址的3G称为用户空间
内核级页表:我们之前同样说到,物理内存和进程地址空间通过页表联系起来,实际上页表也分为内核级页表和用户级页表,内核级页表就是进程地址空间的内核空间和物理内存的映射。
内核级页表是一个全局的页表,它用来维护操作系统的代码与进程之间的关系。因此,在每个进程的进程地址空间中,用户空间是属于当前进程的,每个进程看到的代码和数据是完全不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是一样的内容。
内核态和用户态
我们自己写的代码都是用户级代码,但是用户级代码难免会需要访问OS数据和OS管理的资源,比如getpid访问OS内的task_struct内容,printf访问OS管理的显示器资源,,用户自己写的代码为了访问资源必须直接或间接访问OS提供的接口,必须通过系统调用来完成访问。
系统在执行用户级代码的时候身份就是用户态,在执行内核代码的时候,身份就是内核态。
从进程地址空间的角度来看,当执行系统内代码或访问系统内数据的时候,就是在进程地址空间中进行上下文查找。但是此时执行身份时用户态,所以在执行内核代码的时候需要首先进行身份转换,将用户态转变为内核态。
那么当前处于内核态还是用户态是怎么分辨的呢?
在CPU内部有一个CR3寄存器,保存当前运行身份,是用户态(11)还是内核态(00)
如何从用户态切换到内核态?
在x86下通过一个汇编指令:
int $0x80
,在arm下SWI
将执行身份从用户态转换为内核态,这个过程我们称为陷入内核
注意:系统陷入内核的成本非常高
我们之前说过信号在被发送给进程之后不会被立刻处理,而是由OS在合适的时候进行处理,这个合适的时候实际上就是OS陷入内核之后处于内核态的时候,只有此时处于内核态,才有权力查看当前进程的信号保存的相关数据结构。信号捕捉的流程按照对应的信号处理动作分为两种情况
1. 信号处理动作是默认的或者忽略的
2. 信号的处理动作是自定义的
小技巧:可以使用数学中无穷的表达符号来记忆
为什么执行自定义信号处理函数的时候要转变身份为用户态呢?不是说身份转换成本很大吗?
当前如果处于内核态,当时是有权限执行自定义的信号处理函数的,但是OS不信任任何人,如果自定义的信号函数中存在恶意代码,OS是无法识别的。由于内核态的权限非常高,一旦出现恶意代码将会导致不可预知的后果,所以要从根本上杜绝这种情况的发生,就要让OS在执行用户级代码的时候一定不能是内核态
小实验:
#include
#include
#include
#include
static std::vector<int> blockSigs = { 2 };
const int MAX_SIGNUM = 31;
void show_pending(sigset_t pending)
{
for(int signo = MAX_SIGNUM; signo >= 1; --signo)
{
if(sigismember(&pending, signo))
{
std::cout << "1";
}
else
{
std::cout << "0";
}
}
std::cout << std::endl;
}
void myhandler(int signo)
{
std::cout << signo << "号信号已经被递达" << std::endl;
}
int main()
{
signal(2, myhandler);
// 1.先尝试屏蔽指定信号集
sigset_t block, oblock, pending;
// 1.1 初始化信号集
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
// 1.2 添加要屏蔽的信号
for(const auto& sig : blockSigs)
{
sigaddset(&block, sig);
}
// 1.3 开始屏蔽(设置进内核)
sigprocmask(SIG_SETMASK, &block, &oblock);
// sigprocmask(SIG_BLOCK, &block, &oblock);
// 2. 遍历打印pending信号集
int cnt = 0;
while(true)
{
cnt++;
// 2.1 初始化(这里初始化可以不做,因为获取是覆盖式的)
sigemptyset(&pending);
// 2.2 获取pending
sigpending(&pending);
// 2.3 打印结果
show_pending(pending);
sleep(1);
if(cnt == 10)
{
std::cout << "取消屏蔽" << std::endl; // 一旦对特定信号进行屏蔽解除,那么一般OS至少要立马递达一个信号
sigprocmask(SIG_SETMASK, &oblock, &block);
}
}
}
1. 使用系统调用signal捕捉任意信号,设置任意的回调函数,当捕捉到设定的信号之后,会调用指定的回调函数
signal函数的使用方法我们在上文的硬件信号的证明的时候讲解使用过,这里就不过多赘述
2. 使用系统调用sigaction捕捉,对特定信号设置特定的回调方法
头文件:
#include
函数原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数解释:
signum:需要设置信号编号
act:输入型参数,被设置信号对应的相关信息结构体
oldact:输出型参数,保存之前的信息
返回值:
调用成功返回0,否则返回-1同时设置错误码
这里出现了一个结构体struct sigaction
struct sigaction {
void (*sa_handler)(int); // 自定义的信号处理行为
void (*sa_sigaction)(int, siginfo_t *, void *); // 跟普通信号无关,我们暂时不关心
sigset_t sa_mask; // 当前信号被屏蔽的同时需要屏蔽的信号
int sa_flags; // 这里我们不关心,设置为0即可
void (*sa_restorer)(void); // 跟普通信号无关,我们暂时不关心
};
一些补充:
- 对于sa_handler参数,可以为
SIG_IGN
表示忽略该信号,可以为SIG_DFL
表示执行系统默认动作,可以是一个函数指针,表示自定义的捕捉行为- 对于sa_mask参数:首先需要说明的是,当某个信号的处理函数被调用,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时,自动恢复原来的信号屏蔽字。
例如,下面我们用sigaction函数对2号信号进行了捕捉,将2号信号的处理动作改为了自定义的打印动作,并在执行一次自定义动作后将2号信号的处理动作恢复为原来默认的处理动作。
#include
#include
#include
#include
struct sigaction act, oact;
void handler(int signo)
{
printf("get a signal:%d\n", signo);
sigaction(2, &oact, NULL);
}
int main()
{
memset(&act, 0, sizeof(act));
memset(&oact, 0, sizeof(oact));
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2, &act, &oact);
while (1){
printf("I am a process...\n");
sleep(1);
}
return 0;
}
运行代码后,第一次向进程发送2号信号,执行我们自定义的打印动作,当我们再次向进程发送2号信号,就执行该信号的默认处理动作了,即终止进程。
一般而言,我们认为main执行流和信号捕捉执行流是两个执行流!
如果在main中和在handler中,该函数被重复进入,此时出问题,则该函数(比如insert)称为不可重入函数
如果在main中和在handler中,该函数被重复进入,此时不出问题,则该函数(比如insert)称为可重入函数
我们遇到的绝大多数函数都是不可重入函数
main函数调用insert,向链表head插入Node1,insert只做了第一步,然后就被中断(或者因为信号原因执行信号捕捉),此时进程挂起,然后唤醒在次回到用户态检查有信号待处理,于是切换到sighandler方法,sighandler也调用了insert函数,要把Node2头插到链表里,Node2的next结点指向下一个结点位置,下一步就是head指向Node2,完成Node2的头插,信号捕捉完之后就成功把Node2插入,接下来回到main执行流,对Node1完成插入的第二步动作,此时把head指向Node1,最后只有Node1真正插入到链表之中,而Node2结点找不到了,发生内存泄漏,出现问题。
不可重入函数的判定方式
如果一个函数符合下列条件之一的,就是不可重入函数
1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2. 调用了标准I/O库 函数。标准I/O库的汗多实现都以不可重入的方式使用全局数据结构。
子进程退出时,会向父进程发送17号信号SIGCHLD
证明:
#include
#include
#include
#include
void handler(int signo)
{
printf("pid:%d, %d 号信号,正在被捕捉!\n",getpid(),signo);
}
int main()
{
signal(SIGCHLD,handler);//17号信号
printf("我是父进程:%d,ppid:%d\n",getpid(),getppid());
pid_t id = fork();
if(id==0)
{
printf("我是子进程:%d,ppid:%d,我要退出了\n",getpid(),getppid());
exit(1);
}
while(1)
sleep(1);
return 0;
}
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。
signal(SIGCHLD,SIG_IGN);
sigaction(SIGCHLD,act,oldact);
注意:虽然SIGCHLD的默认动作就是忽略,但是与手动设置表现的不一样,默认是收到信号就进行处理,该等还得等,而如果我们手动设置了SIG_IGN,子进程退出时发送给父进程的信号会被父进程忽略,但是子进程会被OS回收,这是有所区别的。含义不一样
本节完…
gno)
{
printf(“pid:%d, %d 号信号,正在被捕捉!\n”,getpid(),signo);
}
int main()
{
signal(SIGCHLD,handler);//17号信号
printf(“我是父进程:%d,ppid:%d\n”,getpid(),getppid());
pid_t id = fork();
if(id==0)
{
printf(“我是子进程:%d,ppid:%d,我要退出了\n”,getpid(),getppid());
exit(1);
}
while(1)
sleep(1);
return 0;
}
[外链图片转存中...(img-Ml3visS3-1706210780067)]
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。
~~~c
signal(SIGCHLD,SIG_IGN);
sigaction(SIGCHLD,act,oldact);
注意:虽然SIGCHLD的默认动作就是忽略,但是与手动设置表现的不一样,默认是收到信号就进行处理,该等还得等,而如果我们手动设置了SIG_IGN,子进程退出时发送给父进程的信号会被父进程忽略,但是子进程会被OS回收,这是有所区别的。含义不一样
本节完…