网站信任 用户转化,建设网站选什么地方的主机,wordpress主题太臃肿,做公司网站的平台致前行的人#xff1a; 要努力#xff0c;但不着急#xff0c;繁花锦簇#xff0c;硕果累累都需要过程#xff01; 目录 1.信号概念 1.1生活角度的信号 2. 技术应用角度的信号 3.Linux操作系统中查看信号 4.常用信号发送 4.1通过键盘发送信号 4.2调用系统函数发送信号 4.3…致前行的人 要努力但不着急繁花锦簇硕果累累都需要过程 目录 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 查看所有的信号如图所示 【131】称为称为普通信号【3464】称为实时信号
4.常用信号发送
4.1通过键盘发送信号
1. 用户输入命令,在Shell下启动一个前台进程。 用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断被OS获取解释成信号发送给目标前台进程前台进程因为收到信号进而引起进程退出
#includeiostream
#includeunistd.h
using namespace std;int main()
{while(true){cout 我是一个进程 getpid() endl;}return 0;
} 按Ctrl c时本质上就是给进程发送2号信号操作系统从键盘上捕捉到信号 然后终止前台正在运行的进程。
验证ctrl c是2号信号
下面介绍一个函数是专门进行捕捉信号signal 函数原型 #include signal.htypedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler);参数介绍 signum:要进行捕捉的信号 handler:捕捉信号之后采用回调的方式执行要采取的动作 #includeiostream
#includesignal.h
#includeunistd.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获取解释成信号发送给目标前台进程前台进程因为收到信号进而引起进程退出
#includeiostream
#includeunistd.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 sigismemberconst 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.hint sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 7.7实例代码
#includeiostream
#includevector
#includesignal.h
#includeunistd.h
#define MAX_SIGNUM 31
using namespace std;static vectorint 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函数调用,而是被系统所调用
#includeiostream
#includesignal.h
#includeunistd.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是实时信号的处理函数,本章不详细解释这两个字段。
#includeiostream
#includesignal.h
#includeunistd.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
#includestdio.hint 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;
}
标准情况下键盘输入CTRLc,2号信号被捕捉执行自定义动作修改quit 1,while(条件不满足)退出循环进程退出。
运行截图 将该进程进行优化为-O3
mysignal:mysignal.cgcc -o $ $^ -O3 #-stdc11
.PHONY:clean
clean:rm -rf mysignal 运行截图 优化情况下键入 CTRL-C ,2号信号被捕捉执行自定义动作修改 quit1 但是 while 条件依旧满足,进程继续运行但是很明显quit肯定已经被修改了但是为何循环依旧执行很明显 while 循环检查的quit并不是内存中最新的quit这就存在了数据二异性的问题。 while 检测的quit其实已经因为优化被放在了CPU寄存器当中。
CPU处理数据的过程分为取指令分析指令执行指令将结果写回到内存中
未优化前 优化后因为编译器识别到在main函数内部中并没有对quit进行修改而是每次做一个判断所以直接将quit变量中的内容写入到寄存器中下一次再判断的时候直接使用寄存器变量中的值所以while()条件一直为假进程也就没有退出 如何解决呢很明显需要 volatile 运行截图 volatile的作用保持内存的可见性告知编译器被该关键字修饰的变量不允许被优化对该变量的任何操作都必须在真实的内存中进行操作
11.SIGCHLD信号
进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。 其实,子进程在终止时会给父进程发SIGCHLD17信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
#includestdio.h
#includestdlib.h
#includesignal.h
#includeunistd.hvoid 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秒过后子进程自动被回收了