深入剖析Linux——进程信号
致前行的人:
要努力,但不着急,繁花锦簇,硕果累累都需要过程!
目录
1.信号概念
1.1生活角度的信号
2. 技术应用角度的信号
3.Linux操作系统中查看信号
4.常用信号发送
4.1通过键盘发送信号
4.2调用系统函数发送信号
4.3硬件异常产生信号
4.4软件条件产生信号
5.核心转储
6.不同信号的意义
7.阻塞信号
7.1信号常见概念:
7.2信号在内核中的表示
7.3sigset_t
7.4信号操作函数
7.5sigprocmask
7.6sigpending
7.7实例代码
8.捕捉信号
8.1如何实现信号的捕捉
8.2sigaction
9.可重入函数
10.volatile
11.SIGCHLD信号
1.信号概念
1.1生活角度的信号
你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
2. 技术应用角度的信号
如何将上面的概念,迁移到进程中呢?
1.首先我们都知道,信号是给进程发的,那进程是如何识别信号的呢?因为进程本身就是程序员编写的属性的逻辑的集合,所以进程识别信号是通过程序员编码完成的;2.当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理;3.进程必须对信号具有保存能力;4.进程在处理信号的时候,会有三种动作(默认动作,自定义动作,忽略动作),当进程收到信号时就称之为信号捕捉
上面我们知道进程必须对信号具有保存能力,而进程要保存,应该保存在哪里呢?通过之前的学习我们知道,当一个进程被加载到内存的时候,操作系统会创建PCB对象,保存进程的相关属性,其中就一个字段是用来保存进程是否收到信号,当进程收到信号的时候就将该字段的对应信号的比特位置为1,所以发送信号并保存信号的本质是修改PCB中的信号位图。
PCB的管理者是OS,既然是要修改PCB中的内容,只能由操作系统自己进行修改,所以在上层我们进行发送信号一定要调用操作系统提供的对应接口,所以在使用kill命令进行发送信号的时候一定在底层调用了对应的系统调用接口
3.Linux操作系统中查看信号
kill-l 查看所有的信号:如图所示
【1,31】称为称为普通信号【34,64】称为实时信号
4.常用信号发送
4.1通过键盘发送信号
1. 用户输入命令,在Shell下启动一个前台进程。
用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
#include<iostream>
#include<unistd.h>
using namespace std;int main()
{while(true){cout << "我是一个进程" << getpid() << endl;}return 0;
}
按Ctrl + c时本质上就是给进程发送2号信号,操作系统从键盘上捕捉到信号 ,然后终止前台正在运行的进程。
验证ctrl + c是2号信号
下面介绍一个函数是专门进行捕捉信号:signal
函数原型:
#include <signal.h>typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);
参数介绍:
signum:要进行捕捉的信号
handler:捕捉信号之后采用回调的方式执行要采取的动作
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std;void handler(int signao) {cout<< "捕捉了一个信号,信号编号是:"<< signao << endl; } int main() {signal(2,handler);while(true){cout << "我是一个进程" << getpid() << endl;sleep(1);}return 0; }
2. 用户输入命令,在Shell下启动一个前台进程。
用户按下 Ctrl-\ ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
#include<iostream>
#include<unistd.h>
using namespace std;int main()
{while(true){cout << "hello world" << getpid() <<endl;sleep(1);}return 0;
}
Ctrl + \对应的就是3号信号
4.2调用系统函数发送信号
1.调用函数kill
参数介绍:
第一个参数:要终止进程的pid
第二个参数:要发送的信号
返回值:On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set appropriately.
使用介绍:调用kill函数终止掉任意进程
//mysignal.cc
static void Usage(const string& proc)
{cout << "\nUsage: " << proc << " pid signo\n" << endl;
}
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 n = kill(pid,signo);if(n != 0){perror("kill fail:");}return 0;
}
//mytest.ccint main()
{while(true){cout << "我是一个正在运行的进程,pid:" << getpid() << endl;sleep(1);}return 0;
}
2.调用函数raise
参数介绍:
sig:要发送的信号
返回值:returns 0 on success, and nonzero for failure.
使用说明:给自己发送任意信号
//给自己发送任意信号:
int main()
{int count = 0;while(count <= 10){printf("%d\n",count++);if(count >= 5)raise(9);}return 0;
}
3.调用函数abort
使用说明:给自己发送指定信号
int main()
{int count = 0;while(count <= 10){printf("%d\n",count++);if(count >= 5)abort();}return 0;
}
调用abort函数终止了该进程,但是给进程发送的是几号信号呢?下面我们来验证一下:
通过验证发现调用abort函数是给进程发送了6号信号
4.3硬件异常产生信号
信号产生不一定是用户显示的发信号,也有可能是硬件异常,操作系统主动发送信号
1.如下面这段代码:
int main()
{printf("运行中.....\n");sleep(1);int a = 10;a /= 0;return 0;
}
当在代码出现除0的操作时,报了一个浮点数错误,并且终止掉了该进程
那为什么会终止进程呢?
这里是因为收到了OS系统发的信号,那操作系统发的是哪个信号呢?通过验证发现操作系统发的是8号信号:SIGFPE
那8号信号是什么呢?
通过调用signal函数验证8号信号:
void catchSig(int signo)
{cout << "获取到一个信号,信号编号是:" << signo << endl;sleep(1);
}
int main()
{signal(SIGFPE,catchSig);while(true){printf("运行中.....\n");sleep(1);int a = 10;a /= 0;}return 0;
}
通过验证也发现,当除0的时候,操作系统确实给该进程发送8号信号
此时就有了新的疑问,当进行一次/=0的时候,为什么会一直收到信号呢?,除此之外,操作系统是如何得知我/0了呢?
关于上面的两个问题,其实是跟硬件有关系的,下面我们具体来看看:
此时我们就回答了上面的一个问题,操作系统如何得知我/0了,对于另外一个问题/0一次,会一直捕捉
这是因为收到信号不一定会引起进程退出,没有退出,就有可能被调到,CPU内部的寄存器只有一份,但是寄存器中的内容,是属于进程的上下文,当进程被切换的时候,就有无数次寄存器被保存和恢复的过程,所以每一次恢复的时候,让OS系统识别到了CPU内部状态寄存器的溢出标志位为1,所以就出现了上面的现象,在/一次0之后,就会一直捕捉到8号信号
2.如下面的这段代码:
int main()
{while(true){printf("运行中.....\n");sleep(1);int* p = nullptr;*p = 100; //野指针错误}return 0;
}
当出现野指针问题的时候,操作系统发的是几号信号呢?
void catchSig(int signo)
{cout << "获取到一个信号,信号编号是:" << signo << endl;exit(1);
}
int main()
{signal(SIGFPE,catchSig);while(true){printf("运行中.....\n");sleep(1);int* p = nullptr;*p = 100; //野指针错误}return 0;
}
通过验证发现,当出现野指针错误的时候操作系统发送的是11号信号
还是和上面相同的问题,操作系统是如何得知我写的代码有空指针的问题呢?
当空指针解引用从虚拟地址映射到物理空间的时候,CPU中有一个MMU的内存管理单元,当遇到空指针解引用的时候MMU就会出现异常,然后被操作系统得知,操作系统向进程发送信号
4.4软件条件产生信号
1.在管道通信的时候,当读端关闭,写端一直写的时候,OS系统就会向写端发送13号信号SIGPIPE终止掉写端,把这种产生信号的方式就被称为由软件条件产生的信号
2.调用函数alarm
使用说明:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
int main()
{alarm(1);int cnt = 0;while(true){printf("%d\n",cnt++);}return 0;
}
验证alarm给进程发送SIGALRM信号:
int cnt = 0;
void catchSig(int signo)
{cout << "获取到一个信号" << cnt << endl;exit(1);
}
int main()
{signal(SIGALRM,catchSig);alarm(1);while(true){cnt++;}return 0;
}
注:alarm(0)可以取消闹钟
如何理解“闹钟”是软件条件产生的信号:
任意一个进程,都可以通过alarm系统调用在内核中设置闹钟,OS系统内可能存在着很多闹钟,那操作系统该如何管理这些闹钟:
所以说闹钟是软件条件产生的信号。
信号产生总结:
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
OS是进程的管理者
信号的处理是否是立即处理的?在合适的时候信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
需要被暂时记录下来,记录在进程的PCB中
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
知道,就如同在没有看到红灯之前就已经知道,看到红灯之后应该如何处理
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
当操作系统向进程发送信号时,本质上是在修改进程PCB的位图。
5.核心转储
概念:当进程异常退出的时候,进程在对应的时刻,将内存中的有效数据转储到磁盘中。
现象:
进程在退出的时候,一般会有两种情况,一种是Term表示正常退出,一种是core表示异常退出:
如图所示:
在云服务器上,默认进程如果是以core退出的,暂时看不到明显的现象,如果想要看到需要打开core file选项:
int main()
{int a[10] = { 0 };a[10000] = 100;return 0;
}
数组越界访问时进程会异常退出:
当打开core file选项的时候就会产生一个临时文件:文件名=core + 进程的pid
该文件是可被调试的,那如何进行调试呢?
如图所示:
通过这种方式就能快速定位进程异常的位置了
6.不同信号的意义
通过上面的学习,我们已经了解了信号产生的几种方式,虽然产生信号的方式有所不同,但是大部分信号,默认处理动作都是终止进程,既然默认处理动作都是终止进程,那为什么还需要分那么多的信号呢?这是因为信号不同,代表不同的时间,当进程被终止的时候,能够更快的定位到进程是因为什么而终止的
注:kill -9信号是管理员信号,即使所有的信号被捕捉时,无法终止进程,9号信号也能终止掉该进程:
例:
void catchSig(int signo)
{cout << "获取到一个信号" << signo << endl;
}
int main()
{for(int signo = 1; signo <= 31; signo++){signal(signo,catchSig);}while(true){printf("我正在运行>>%d\n",getpid());sleep(1);}return 0;
}
7.阻塞信号
7.1信号常见概念:
实际执行信号的动作称为信号递达
信号从产生到递达之间的状态称为信号未决
进程可以阻塞某个信号
被阻塞的信号在产生时将保持未决状态,直到信号解除对此信号的阻塞,才执行递达的动作
注:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是信号是在递达之后的一种可选的处理动作
7.2信号在内核中的表示
信号在内核的表示示意图:
执行逻辑:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作,信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志,当信号产生时,信号被阻塞,所以暂时不递达,当信号没有被阻塞,并且该信号处于未决状态,该信号被递达
7.3sigset_t
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
7.4信号操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
7.5sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于 maks = mask | set
SIG_UNBLOCK:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~ set
SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask = set
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
7.6sigpending
#include <signal.h>int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
7.7实例代码
#include<iostream>
#include<vector>
#include<signal.h>
#include<unistd.h>
#define MAX_SIGNUM 31
using namespace std;static vector<int> sigarr = {2};
static void show_pending(const sigset_t &pending)
{for(int signo = MAX_SIGNUM; signo >= 1; signo--){if(sigismember(&pending, signo)){cout << "1";}else cout << "0";}cout << "\n";
}
static void myhandler(int signo)
{cout << signo <<"号信号被递达" << endl;
}
int main()
{//执行自定义动作for(const auto& sig : sigarr){signal(sig,myhandler);}//屏蔽指定信号:sigset_t block,oblock,pending;//初始化:sigemptyset(&block);sigemptyset(&oblock);sigemptyset(&pending);//添加要屏蔽的信号:for(const auto& sig : sigarr){sigaddset(&block,sig);}//开始屏蔽,设置进入内核:sigprocmask(SIG_SETMASK,&block,&oblock);//遍历打印pending信号集:int cnt = 10;while(true){//初始化:sigemptyset(&pending);//获取:sigpending(&pending);//打印:show_pending(pending);sleep(1);if(cnt-- == 0){//对特定信号解除屏蔽,让操作系统递达该信号sigprocmask(SIG_SETMASK,&oblock,&block);cout << "恢复对信号的屏蔽,不屏蔽任何信号\n";}}return 0;
}
运行截图:
8.捕捉信号
8.1如何实现信号的捕捉
信号在产生的时候,不会立即被处理,而是在合适的时候进行处理,所谓合适的时候就是从内核态返回到用户态的时候进行处理。
对于内核态和用户态又该如何理解呢?
如何标识当前进程的身份是用户态还是内核态:
进程是如何进入操作系统执行对应的方法呢?
每一个进程都有自己的地址空间,内核空间被映射到了[3,4]G的空间,当进程想要访问操作系统的接口时,只需要在自己的地址空间进行跳转就可以了
注:进入内核态的方式,第一种是在调用系统调用接口,第二种是进程切换的时候
捕捉信号的具体过程:
逻辑抽象:
8.2sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;void handler(int signo)
{cout<< "get a signo" << signo << endl;
}
int main()
{struct sigaction act,oact;act.sa_handler = handler;act.sa_flags = 0;sigemptyset(&act.sa_mask);//捕捉2号信号sigaction(SIGINT,&act,&oact);while(true){sleep(1);}return 0;
}
运行截图:
当某个信号处理的函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,本章的代码都把sa_flags设为0,sa_sigaction是实时信号的处理函数,本章不详细解释这两个字段。
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;void Count(int cnt)
{while(cnt){printf("cnt:%2d\r",cnt);fflush(stdout);cnt--;sleep(1);}printf("\n");
}
void handler(int signo)
{cout<< "get a signo" << signo<< "正在处理中……"<< endl;Count(20);
}
int main()
{struct sigaction act,oact;act.sa_handler = handler;act.sa_flags = 0;sigemptyset(&act.sa_mask);//当正在处理某一种信号,也想屏蔽其它信号的时候,可以将屏蔽的信号加入sa_mask中sigaddset(&act.sa_mask,3);//捕捉2号信号sigaction(SIGINT,&act,&oact);while(true){sleep(1);}return 0;
}
运行截图:
9.可重入函数
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
如果在main函数和在handler函数中insert函数被重复进入,出现问题,该函数就被称为是不可重入函数
如果在main函数和在handler函数中insert函数被重复进入,没有出现问题,该函数就被称为是可重入函数
如果一个函数符合以下条件之一则是不可重入的:
1.调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2.调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
10.volatile
#include<stdio.h>int quit = 0;
void handler(int signo)
{printf("%d号信号正在被捕捉\n",signo);printf("quit:%d\n",quit);quit = 1;printf("->%d\n",quit);
}
int main()
{signal(2,handler);while(!quit);printf("我是正常退出的!!\n");return 0;
}
标准情况下,键盘输入CTRL+c,2号信号被捕捉,执行自定义动作,修改quit = 1,while(条件不满足),退出循环,进程退出。
运行截图:
将该进程进行优化为-O3
mysignal:mysignal.cgcc -o $@ $^ -O3 #-std=c++11
.PHONY:clean
clean:rm -rf mysignal
运行截图:
优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 quit=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显quit肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的quit,并不是内存中最新的quit,这就存在了数据二异性的问题。 while 检测的quit其实已经因为优化,被放在了CPU寄存器当中。
CPU处理数据的过程分为:取指令,分析指令,执行指令,将结果写回到内存中
未优化前:
优化后:因为编译器识别到在main函数内部中并没有对quit进行修改,而是每次做一个判断,所以直接将quit变量中的内容写入到寄存器中,下一次再判断的时候直接使用寄存器变量中的值,所以while()条件一直为假,进程也就没有退出
如何解决呢?很明显需要 volatile
运行截图:
volatile的作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
11.SIGCHLD信号
进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD(17)信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait,清理子进程即可。
#include<stdio.h>
#include<stdlib.h>
#include<signal.h>
#include<unistd.h>void Count(int cnt)
{while(cnt){printf("cnt:%2d\r",cnt);fflush(stdout);cnt--;sleep(1);}printf("\n");
}
void handler(int signo)
{//1.有非常多的子进程,在同一时刻退出了//处理方案:waitpid(-1) while(1) 采用循环的方式等待任意一个进程//2.有非常多的子进程,在同一时刻退出了一部分://处理方案:采用非阻塞式的等待方式等待任意一个进程// while(1)// {// pid_t ret = waitpid(-1,NULL,WNOHANG);// if(ret == 0) break;// }printf("pid:%d,%d号信号正在被捕捉\n",getpid(),signo);}
int main()
{signal(17,handler);printf("我是父进程,pid:%d,ppid:%d\n",getpid(),getppid());pid_t id = fork();if(id == 0){printf("我是子进程,pid:%d,ppid:%d,我要退出了\n",getpid(),getppid());Count(5);exit(1);}while(1) sleep(1);return 0;
}
运行截图:
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction函数自定义的忽略 通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
运行截图:5秒过后子进程自动被回收了
相关文章:

深入剖析Linux——进程信号
致前行的人: 要努力,但不着急,繁花锦簇,硕果累累都需要过程! 目录 1.信号概念 1.1生活角度的信号 2. 技术应用角度的信号 3.Linux操作系统中查看信号 4.常用信号发送 4.1通过键盘发送信号 4.2调用系统函数发送信号 4.3…...

API-Server的监听器Controller的List分页失效
前言 最近做项目,还是K8S的插件监听器(理论上插件都是通过API-server通信),官方的不同写法居然都能出现争议,争议点就是对API-Server的请求的耗时,说是会影响API-Server。实际上通过源码分析两着有差别&am…...
jupyter notebook 进阶使用:nbextensions,终极避坑
jupyter notebook 进阶使用:nbextensions,终极避坑吐槽安装 jupyter_contrib_nbextensions1. Install the python package(安装python包)方法一,PIP:方法二,Conda(推荐)&…...
C 语言编程 — Doxygen + Graphviz 静态项目分析
目录 文章目录目录安装配置解析Project related configuration optionsBuild related configuration optionsConfiguration options related to warning and progress messagesConfiguration options related to the input filesConfiguration options related to source brows…...

Mybatis报BindingException:Invalid bound statement (not found)异常
一、前言 本文的mybatis是与springboot整合时出现的异常,若使用的不是基于springboot,解决思路也大体一样的。 二、从整合mybatis的三个步骤排查问题 但在这之前,我们先要知道整合mybatis的三个重要的工作,如此才能排查&#x…...

HttpRunner3.x(1)-框架介绍
HttpRunner 是一款面向 HTTP(S) 协议的通用测试框架,只需编写维护一份 YAML/JSON 脚本,即可实现自动化测试、性能测试、线上监控、持续集成等多种测试需求。主要特征继承的所有强大功能requests ,只需以人工方式获得乐趣即可处理HTTP…...

pytest学习和使用20-pytes如何进行分布式测试?(pytest-xdist)
20-pytes如何进行分布式测试?(pytest-xdist)1 什么是分布式测试?2 为什么要进行分布式测试?2.1 场景1:自动化测试场景2.2 场景2:性能测试场景3 分布式测试有什么特点?4 分布式测试关…...

三、Python 操作 MongoDB ----非 ODM
文章目录一、连接器的安装和配置二、新增文档三、查询文档四、更新文档五、删除文档一、连接器的安装和配置 pymongo: MongoDB 官方提供的 Python 工具包。官方文档: https://pymongo.readthedocs.io/en/stable/ pip安装,命令如下࿱…...
求最大公约数和最小公倍数---辗转相除法(欧几里得算法)
目录 一.GCD和LCM 1.最大公约数 2.最小公倍数 二.暴力求解 1.最大公约数 2.最小公倍数 三.辗转相除法 1.最大公约数 2.最小公倍数 一.GCD和LCM 1.最大公约数 最大公约数(Greatest Common Divisor,简称GCD)指的是两个或多个整数共有…...

音视频开发_获取媒体文件的详细信息
一、前言 做音视频开发过程中,经常需要获取媒体文件的详细信息。 比如:获取视频文件的总时间、帧率、尺寸、码率等等信息。 获取音频文件的的总时间、帧率、码率,声道等信息。 这篇文章贴出2个我封装好的函数,直接调用就能获取媒体信息返回,copy过去就能使用,非常方便。…...

Springboot集成Swagger
一、Swagger简介注意点! 在正式发布的时候要关闭swagger(出于安全考虑,而且节省内存空间)之前开发的时候,前端只用管理静态页面, http请求到后端, 模板引擎JSP,故后端是主力如今是前…...

Vue全新一代状态管理库 Pinia【一篇通】
文章目录前言1. Pinia 是什么?1.1 为什么取名叫 Pinia?1.2. 为什么要使用 Pinia ?2. 安装 Pinia2.1.创建 Store2.1.1. Option 类型 Store2.1.2 Setup 函数类型 Store2.1.3 模板中使用3. State 的使用事项(Option Store )3.1 读取 State3.2 …...

STM32 -4 关于STM32的RAM、ROM
一 stm32 的flash是什么、有什么用、注意事项、如何查看 一 、说明 它主要用于存储代码,FLASH 存储器的内容在掉电后不会丢失,STM32 芯片在运行的时候,也能对自身的内部 FLASH 进行读写,因此,若内部 FLASH 存储了应用…...

第一个 Qt 程序
第一个 Qt 程序 “hello world ”的起源要追溯到 1972 年,贝尔实验室著名研究员 Brian Kernighan 在撰写 “B 语言教程与指导(Tutorial Introduction to the Language B)”时初次使用(程序),这是目前已 知最早的在计算机著作中将…...

Spring注解驱动开发--AOP底层原理
Spring注解驱动开发–AOP底层原理 21. AOP-AOP功能测试 AOP:【动态代理】 指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的编程方式; 1、导入aop模块:Spring AOP,(Spring-aspects) 2、定义一个业务逻辑类(Ma…...
对象的动态创建和销毁以及对象的复制,赋值
🐶博主主页:ᰔᩚ. 一怀明月ꦿ ❤️🔥专栏系列:线性代数,C初学者入门训练,题解C,C的使用文章,「初学」C 🔥座右铭:“不要等到什么都没有了,才…...

JVM调优,调的是什么?目的是什么?
文章目录前言一、jvm是如何运行代码的?二、jvm的内存模型1 整体内存模型结构图2 堆中的年代区域划分3 对象在内存模型中是如何流转的?4 什么是FULL GC,STW? 为什么会发生FULL GC?5 要调优,首先要知道有哪些垃圾收集器及哪些算法6 调优不是盲目的,要有依据,几款内…...

docker部署zabbix监控
docker部署zabbix监控 1、环境说明 公有云ubuntu22.04 系统->部署docker环境zabbix-server 6.4 2、准备docker环境 更新apt以及安装一些必要的系统工具 sudo apt-get update sudo apt-get -y install apt-transport-https ca-certificates curl software-properties-co…...

C语言刷题(6)(猜名次)——“C”
各位CSDN的uu们你们好呀,今天,小雅兰还是在复习噢,今天来给大家介绍一个有意思的题目 题目名称: 猜名次 题目内容: 5位运动员参加了10米台跳水比赛,有人让他们预测比赛结果: A选…...
两年外包生涯,感觉自己废了一半....
先说一下自己的情况。大专生,17年通过校招进入湖南某软件公司,干了接近2年的点点点,今年年上旬,感觉自己不能够在这样下去了,长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了五年的功能测试…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...
STM32+rt-thread判断是否联网
一、根据NETDEV_FLAG_INTERNET_UP位判断 static bool is_conncected(void) {struct netdev *dev RT_NULL;dev netdev_get_first_by_flags(NETDEV_FLAG_INTERNET_UP);if (dev RT_NULL){printf("wait netdev internet up...");return false;}else{printf("loc…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...

ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...

算法笔记2
1.字符串拼接最好用StringBuilder,不用String 2.创建List<>类型的数组并创建内存 List arr[] new ArrayList[26]; Arrays.setAll(arr, i -> new ArrayList<>()); 3.去掉首尾空格...

HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...

Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...