文章目录
- 前言
- 孤儿进程
- 僵尸进程
- 守护进程
- 总结
前言
孤儿进程、僵尸进程和守护进程是操作系统中的概念,它们分别表示不同的进程状态和特性。孤儿进程和僵尸进程了解了解(都是为守护进程做铺垫),但是对于守护进程大家还是可以好好学习学习,相信以后会用得到~~~~~~
【本博客的实例代码用的是C/C++多进程高并发框架分享【内附可执行源码注释完整】中的代码】
孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程
所收养,并由init进程对它们完成状态收集工作。
想想我们如何模仿一个孤儿进程?👇
1、kill 父进程
2、父进程不wait自然退出
这里进行模拟的场景是父进程不wait自然退出,看代码👇
multip_process1.cpp
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/wait.h>
#include <sys/types.h>
typedef void (*spawn_proc_pt) (void *data);
static void worker_process_cycle(void *data);
static void start_worker_processes(int n);
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name);
int main(int argc,char **argv){
start_worker_processes(4);//启动四个工作进程
//管理子进程,等待子进程都挂了
//wait(NULL);
printf("parent is over!!\n");
}
void start_worker_processes(int n){
int i=0;
for(i = n - 1; i >= 0; i--){
spawn_process(worker_process_cycle,(void *)(intptr_t) i, "worker process");//生产四个子进程(处理任务的过程worker_process_cycle,传入i的地址,名字是work_process)
}
}
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name){
pid_t pid;
pid = fork();
switch(pid){
case -1:
fprintf(stderr,"fork() failed while spawning \"%s\"\n",name);
return -1;
case 0://如果是子进程
proc(data);
return 0;
default:
break;
}
printf("start %s %ld\n",name,(long int)pid);//打印父进程的pid
return pid;
}
//设置cpu亲缘关系,把进程绑定在一个cpu的核上面
static void worker_process_init(int worker){
cpu_set_t cpu_affinity;//cpu亲缘
//worker = 2;
//多核高并发处理 4core 0 - 0 core 1 - 1 2 -2 3 -3
CPU_ZERO(&cpu_affinity);//将结构体清零
CPU_SET(worker % CPU_SETSIZE,&cpu_affinity);// 0 1 2 3,将核与掩码进行绑定
//sched_setaffinity
//0:绑定自己
if(sched_setaffinity(0,sizeof(cpu_set_t),&cpu_affinity) == -1){
fprintf(stderr,"sched_setaffinity() failed\n");
}
}
void worker_process_cycle(void *data){
int worker = (intptr_t) data;//worker就是3210
//初始化
worker_process_init(worker);
//干活,子进程该干什么就写在这下面
for(;;){
sleep(10);
printf("pid %ld ,doing ...\n",(long int)getpid());
}
}
main方法中可以看到,没有wait
,所以当创建完4个进程以后,父进程就自然结束了,这样就导致4个子进程交由init进程进行托管。
1、如果父进程有wait👇
2、如果父进程没有了wait👇
僵尸进程
僵尸进程是指子进程已经退出,但是其父进程没有调用wait或者waitpid等函数来回收子进程的资源,导致子进程的进程控制块(PCB)仍然保留在系统中,此时子进程成为僵尸进程。僵尸进程不会消耗系统资源,但是如果父进程一直不回收它的资源,就会占用系统的进程号,导致系统进程号不足,从而影响系统的正常运行。
Linux进程的状态中,僵尸进程是非常特殊的一种,它已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。它需要它的父进程来为它收尸
,如果他的父进程没安装 SIGCHLD信号处理函数调用wait或waitpid()等待子进程结束
,又没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时父进程结束了, 那么init进程自动会接手这个子进程,为它收尸,它还是能被清除的。但是如果如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是 为什么系统中有时会有很多的僵尸进程。
1.怎么查看僵尸进程:
利用命令ps -ef,可以看到有标记为<defunct>的进程就是僵尸进程。
2.怎么清理僵尸进程:
①改写父进程,在子进程死后要为它收尸。具体做法是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行waitpid()函数为子进程收尸。这是基于这样的原理:就算父进程没有调用 wait,内核也会向它发送SIGCHLD消息,尽管对默认处理是忽略,如果想响应这个消息,可以设置一个处理函数。
②把父进程杀掉。父进程死后,僵尸进程成为"孤儿进程",过继给1号进程init,init始终会负责清理僵尸进程。它产生的所有僵尸进程也跟着消失。
来看看一串代码就知道什么意思了👇
模拟的场景是父进程创建完四个子进程以后,父进程没有wait,而四个子进程相继退出,父进程还在循环之中,这样就会让四个子进程变成僵尸进程
multip_process2.cpp
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/wait.h>
#include <sys/types.h>
typedef void (*spawn_proc_pt) (void *data);
static void worker_process_cycle(void *data);
static void start_worker_processes(int n);
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name);
int main(int argc,char **argv){
start_worker_processes(4);//启动四个工作进程
//管理子进程,等待子进程都挂了
//wait(NULL);
for(;;) sleep(1);
return 0;
}
void start_worker_processes(int n){
int i=0;
for(i = n - 1; i >= 0; i--){
spawn_process(worker_process_cycle,(void *)(intptr_t) i, "worker process");//生产四个子进程(处理任务的过程worker_process_cycle,传入i的地址,名字是work_process)
}
}
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name){
pid_t pid;
pid = fork();
switch(pid){
case -1:
fprintf(stderr,"fork() failed while spawning \"%s\"\n",name);
return -1;
case 0://如果是子进程
proc(data);
return 0;
default:
break;
}
printf("start %s %ld\n",name,(long int)pid);//打印父进程的pid
return pid;
}
//设置cpu亲缘关系,把进程绑定在一个cpu的核上面
static void worker_process_init(int worker){
cpu_set_t cpu_affinity;//cpu亲缘
//worker = 2;
//多核高并发处理 4core 0 - 0 core 1 - 1 2 -2 3 -3
CPU_ZERO(&cpu_affinity);//将结构体清零
CPU_SET(worker % CPU_SETSIZE,&cpu_affinity);// 0 1 2 3,将核与掩码进行绑定
//sched_setaffinity
//0:绑定自己
if(sched_setaffinity(0,sizeof(cpu_set_t),&cpu_affinity) == -1){
fprintf(stderr,"sched_setaffinity() failed\n");
}
}
void worker_process_cycle(void *data){
int worker = (intptr_t) data;//worker就是3210
//初始化
worker_process_init(worker);
//干活,子进程该干什么就写在这下面
// for(;;){
// sleep(10);
// printf("pid %ld ,doing ...\n",(long int)getpid());
// }
exit(1);
}
守护进程
守护进程是在后台运行的一种特殊进程,通常不与任何终端关联
,也不接受用户输入。它通常用于执行一些周期性任务或者长期运行的服务,例如web服务器、数据库服务器等。守护进程通常在系统启动时自动启动
,并且一般具有特定的权限和身份,以便访问一些系统资源和服务。守护进程需要注意的是,在运行时需要注意不要产生僵尸进程
,否则会影响系统的稳定性。
以僵尸进程的代码为例(没有制作守护进程),如果在终端一中运行代码
在终端二中查看进程状态,有
如果现在将终端一进行关闭后在使用终端二进行查看进程状态,会发现几个进程都会消失【这就是不采用守护进程的后果,因为不采用守护进程,那么在这个终端启动的进程就是属于终端,终端关闭,那么同时几个进程就一起挂了
】
那如何成为一个守护进程呢,步骤如下:
1.
调用fork(),创建新进程,它会是将来的守护进程.
2.
在父进程中调用exit,保证子进程不是进程组长【这样就可以创建新会话】
保证子进程不是进程组长可以避免它接收到终端的信号,但父进程退出后,子进程自动成为孤儿进程,会被init进程
接管。
3.
调用setsid()创建新的会话区
通过setsid()函数创建新的会话,并使子进程成为新会话的首进程,从而与控制终端脱离关联。
4.
将当前目录改成根目录
将当前工作目录改为根目录,确保守护进程的工作目录不会影响到其他程序的工作。
5.
将标准输入,标准输出,标准错误重定向到/dev/null
.
避免守护进程输出到终端或者接收终端的输入,/dev/null是一个特殊的文件,它不占用任何磁盘空间,任何写入该文件的数据都会被直接丢弃。在Unix/Linux系统中,/dev/null被广泛应用于重定向程序的输出,是一个非常有用的工具,在守护进程中,通常不需要从标准输入读取数据
,因此重定向标准输入的常见做法是将其重定向到/dev/null。这样,程序就无法从标准输入读取任何数据,从而避免了一些潜在的安全问题。
经过上述步骤,进程就已经成为一个守护进程了,可以执行它需要执行的任务了。这些任务可能包括网络服务、定时任务等。
构建守护进程关键代码如下👇
#include <fcntl.h>
#include <unistd.h>
int daemon(int nochdir, int noclose)
{
int fd;
switch (fork()) {
case -1:
return (-1);
case 0:
break;
default:
//父进程exit
_exit(0);
}
//子进程继续进行操作
//创建一个新的会话
if (setsid() == -1)
return (-1);
//改变工作目录
if (!nochdir)
(void)chdir("/");
//重定向文件句柄
//fd=....,打开这个文件句柄
if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) {
(void)dup2(fd, STDIN_FILENO);//将stdin重定向到fd
(void)dup2(fd, STDOUT_FILENO);//将stdout重定向到fd
(void)dup2(fd, STDERR_FILENO);//将stderr重定向到fd
if (fd > 2)
(void)close (fd);
}
return (0);
}
来看正经完整的示例代码吧👇
multip_process3.cpp
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <sys/wait.h>
#include <sys/types.h>
typedef void (*spawn_proc_pt) (void *data);
static void worker_process_cycle(void *data);
static void start_worker_processes(int n);
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name);
#include <fcntl.h>
#include <unistd.h>
int daemon(int nochdir, int noclose)
{
int fd;
switch (fork()) {
case -1:
return (-1);
case 0:
break;
default:
_exit(0);
}
if (setsid() == -1)
return (-1);
if (!nochdir)
(void)chdir("/");
//fd=....,打开这个文件句柄
if (!noclose && (fd = open("/dev/null", O_RDWR, 0)) != -1) {
(void)dup2(fd, STDIN_FILENO);//将stdin重定向到fd
(void)dup2(fd, STDOUT_FILENO);//将stdout重定向到fd
(void)dup2(fd, STDERR_FILENO);//将stderr重定向到fd
if (fd > 2)
(void)close (fd);
}
return (0);
}
int main(int argc,char **argv){
daemon(0,0);
start_worker_processes(4);//启动四个工作进程
//管理子进程,等待子进程都挂了
wait(NULL);
}
void start_worker_processes(int n){
int i=0;
for(i = n - 1; i >= 0; i--){
spawn_process(worker_process_cycle,(void *)(intptr_t) i, "worker process");//生产四个子进程(处理任务的过程worker_process_cycle,传入i的地址,名字是work_process)
}
}
pid_t spawn_process(spawn_proc_pt proc, void *data, char *name){
pid_t pid;
pid = fork();
switch(pid){
case -1:
fprintf(stderr,"fork() failed while spawning \"%s\"\n",name);
return -1;
case 0://如果是子进程
proc(data);
return 0;
default:
break;
}
printf("start %s %ld\n",name,(long int)pid);//打印父进程的pid
return pid;
}
//设置cpu亲缘关系,把进程绑定在一个cpu的核上面
static void worker_process_init(int worker){
cpu_set_t cpu_affinity;//cpu亲缘
//worker = 2;
//多核高并发处理 4core 0 - 0 core 1 - 1 2 -2 3 -3
CPU_ZERO(&cpu_affinity);//将结构体清零
CPU_SET(worker % CPU_SETSIZE,&cpu_affinity);// 0 1 2 3,将核与掩码进行绑定
//sched_setaffinity
//0:绑定自己
if(sched_setaffinity(0,sizeof(cpu_set_t),&cpu_affinity) == -1){
fprintf(stderr,"sched_setaffinity() failed\n");
}
}
void worker_process_cycle(void *data){
int worker = (intptr_t) data;//worker就是3210
//初始化
worker_process_init(worker);
//干活,子进程该干什么就写在这下面
for(;;){
sleep(10);
printf("pid %ld ,doing ...\n",(long int)getpid());
}
}
1、"终端一"运行可执行程序
2、在"终端二"查看进程信息:
3、把"终端一"进行关闭后再在"终端二"查看进程信息【看相应的进程是否还会关闭】:
总结
1、孤儿进程:指父进程退出或异常结束,而其子进程继续运行的情况。孤儿进程会被init进程接管,其PPID会被设置为1。
2、僵尸进程:指子进程结束,但是父进程没有处理其退出状态信息,造成该进程处于僵尸状态,占用系统资源。可以通过wait()或waitpid()等函数来处理僵尸进程。
3、守护进程:是在后台运行的一种特殊进程,一般不与任何控制终端相连。创建守护进程的过程包括:fork()创建子进程,setsid()创建新会话,改变工作目录和文件访问权限,关闭不需要的文件描述符等。
在编写孤儿进程、僵尸进程和守护进程时,需要注意一些技巧和注意事项,例如:合理的处理子进程的退出状态信息,保证子进程不会成为进程组组长,及时关闭不需要的文件描述符等。
总之,孤儿进程、僵尸进程和守护进程是进程管理中的重要概念,在实际编程中需要充分了解它们的概念、特点和实现方式,以避免出现一些常见的问题,同时也需要注意代码实现的可靠性和安全性。