文章目录
- 进程
- 进程常见的状态
- 进程调度
- 进程状态变化关系
- 进程标识
- 示例--进程标识的使用以及简介
- 进程创建
- fork函数
- vfork函数
- 示例--使用`fork`函数创建子进程,并了解进程之间的关系
- 创建进程时发生的变化
- 虚拟内存空间的变化
- 示例--验证fork函数创建进程时的操作
- 对文件IO的影响
- 进程创建对文件IO的影响
- 示例--代码演示fork创建子进程前后内核中有关文件的变化
- 进程创建的应用
- 进程链和进程扇
- 示例--使用fork函数创建进程链
- 示例--使用fork函数创建进程扇
- 守护进程
- 孤儿进程
- 示例--使用fork函数创建一个孤儿进程
- 僵尸进程
- 避免僵尸进程
进程
进程常见的状态
-
运行状态:在
STAT
中显示为R
,表示进程正在CPU上执行 -
等待状态:表示进程暂时停止执行,等待某个时间的发生或者某个资源,例如
wait
函数- 可中断的等待:程序可以被某些信号中断,如
SIGINT、SIGSTP
,在STAT
中显示为S
- 不可中断的等待:不受信号的干扰,可能直到某些硬件信息发生改变才中断运行,在
STAT
中显示为D
- 可中断的等待:程序可以被某些信号中断,如
-
停止状态:一般是在做一些调试(
gdb
调试)或者给进程发送SIGSTOP
信号的时候会处于停止状态,在STAT
中显示为T
-
僵尸状态:进程已经结束了,但是在内存中还留有一些记录没有被释放掉,在进程表项中仍然有记录。在
STAT
中显示为Z
进程调度
目前的操作系统都是多用户、多任务的,多用户的意思就是同一时间允许多个用户进行登录,多任务就是允许一个用户启动很多的进程来执行很多的任务。进程想要运行就要获取CPU,那么哪个进程先获取到CPU哪个进程就先执行,其他的进程就要阻塞等待。
操作系统可以分为抢占式和非抢占式。非抢占式的操作系统中进程的执行除非它自己主动停止,要不然别的进程无法获取到CPU从而执行,由此可以看出这种操作系统发的运行效率是非常低的,那么显然这种方式是不可取的。现在的操作系统都是抢占式的操作系统,根据进程的调度策略、进程的优先级,谁先获取到CPU谁就先执行。不过在抢占式操作系统中即使是一个进程获取到了CPU,那么它也不会一直执行,内核会给每一个进程分配一个时间片,系统会通过内核分配的时间片从而去调度进程。当时间片用完的时候,进程就会由运行状态转变为就绪状态转而去执行别的进程,这就是现在应用很广泛的分时操作系统。
由于后台有很多的进程,那么这就涉及到了哪个进程先执行,哪个进程的优先级比较高。当进程启动以后,在内核空间的进程表项有一个专门的算法来管理进程运行的先后顺序,就是进程调度。通过进程调度就能够保证优先级较高的进程能够先执行,而优先级低的进行后执行,以及每一个进程时间片的分配。
在进程调度中还涉及到一个操作就是进程交换。当一个进程将要结束而另一个进程将要运行的时候就会涉及到进程交换。经过前边的内容可知当一个进程在运行起来的时候会分配一个远大于实际物理内存的一个虚拟内存。(虚拟内存包括用户空间和内核空间两部分,一般用户的操作是在用户空间,当通过系统调用的时候会切换到内核空间,但最后会返回到用户空间,总的来说开销比较小)最后通过MMU(内存管理单元)
映射到实际的物理内存上。那么当一个进程结束转而去运行一个新的进程的时候就会将原先物理内存中的内容进行替换,并且此时还涉及到寄存器等内容的替换,所以这个开销是非常大的。(由于开销非常大,所以后边就会引入线程这一操作)
进程状态变化关系
如图所示:介绍一下进程的状态
- 正在运行态:此时这个进程已经获取到了CPU,CPU给它分配一个时间片,已经处于运行状态,如果时间片用完以后就会转变为就绪状态直到下一次进程调度给它分配时间片。
- 就绪态:进程要想获得CPU就必须要进入到就绪态,在就绪态内部实际上有一个等待队列,进行调度会通过进程的优先级等条件将某个进程从就绪态转变为运行态。
- 僵尸态:正在运行的进程突然终止,虽然它终止了,但是在进程表项里仍留有它的记录,它的信息没有完全释放。
- 停止态:在
gdb
调试的时候会进入到停止态,当想要从停止态变为运行态的时候必须先变为就绪态,然后从就绪态根据CPU的调度变为运行态。 - 等待状态:等待状态一般是未申请到所需资源会变为等待状态,例如后边要用到的父进程等待子进程退出用到的
wait
函数,只有当子进程结束的时候给父进程发送信号它才会继续执行,否则它只能处于挂起或者阻塞状态。
这里有一点需要注意的是:通过上边的内容可以发现实际上同一时间只有一个进程在执行,只不过这个时间间隔非常短,所以我们从宏观上看感觉是好多进程同时在运行。从微观上看,如果是一个单核的CPU,那么同一时间只能够运行一个进程,然后通过时间片轮转的方式运行其他进程。如果是双核或者多核的CPU能够同时运行多个进程,那个才是真正的同时运行。
进程标识
在Linux系统中,每一个进程都有一个唯一的标识,这个标识叫做进程的id,即PID
(process id)。在shell环境中可以使用ps
指令后边加一列的选项查看进程的标识符,那么在程序中如何查看进程的标识?
有关进程标识的函数
#include <unistd.h>
#include <sys/types.h>
pid_t getpid(void); //获取当前进程ID
pid_t getppid(void); //获取当前进程父进程的ID
uid_t getuid(void); //获取当前进程的实际用户ID
uid_t geteuid(void); //获取当前进程的有效用户ID
git_t getgid(void); //获取当前进程的实际用户的组ID
gid_t getegid(void); //获取当前进程的有效用户的组ID
pid_t getpgrp(void); //获取当前进程所在的进程组的ID
pid_t getpgid(pid_t pid); //获取指定进程所在的jin'cehn
示例–进程标识的使用以及简介
#include "header.h"
int main(void)
{
printf("pid: %d\n",getpid());
printf("ppid: %d\n",getppid());
printf("user id: %d\n",getuid());
printf("effective user id: %d\n",geteuid());
printf("user group id: %d\n",getgid());
printf("effective user group id: %d\n",getegid());
printf("process group id: %d\n",getpgrp());
printf("process number %d belongs to process group id is %d\n",getpid(),getpgid(getpid()));
printf("process's father number %d belongs to process group id is %d\n",getppid(),getpgid(getppid()));
while(1);
return 0;
}
这里为了能够使用指令执行查找当前进程,所以使用while
循环把它卡住。
然后通过getpid()
函数能够获取到当前进程的PID
并且使用getppid()
函数获取到当前进程的父进程的pid
,使用ps -aux|grep process_id |grep -v grep
查找现在这个正在运行的进程进行对比可以发现它们的进程标识符是一样的。
如图所示,R
代表它此时的状态STAT
,表明它此时正处于运行状态;6995
表示当前进程的进程标识符,6500
表示当前进程的父进程的进程标识符;当前进程的父进程是bash
,bash
进程是Linux系统中的一种命令行的解释器,用于执行用户输入的命令或脚本文件。在用户编写的脚本中,通常第一行会写#/bin/bash
就是通过指定bash
作为脚本的解释器来执行的。并且在Linux系统中也能够查找到bash
这个进程它的进程号和当前进程的父进程的标识符是对的上的。实际上在shell
终端运行的可执行程序它们的父进程都是bash
这个进程。
getuid
函数是用来获取当前进程的实际用户id,geteuid
函数是用来获取当前进程的有效用户id。这里详细解释一下这两个实际用户和有效用户的概念:实际用户就是当登录Linux系统的时候所用的用户,如下图,这里登录系统用户的是dx
,所以它的uid
就是dx
这个用户的id,在终端直接敲id
就能够获取到相关的信息。这里的uid
和程序里通过getuid
的结果一样。而有效用户指的是启动这个进程的用户,也就是说可以使用一个和当前登录这个系统不同的用户来执行这个可执行程序,那么它的euid
就会不一样(后边展示一下)。这个一般在Linux系统中除非特别需要,否则实际用户和有效用户是相同的。
getgid
函数和getegid
函数的作用与上边的getuid
和geteuid
的作用类似。由于在Linux系统中,每一个用户都有所属的组,所以这个的getgid
函数就是实际用户所属的组id,而getgid
函数就是有效用户所属的组id。与上边的getuid
和geteuid
函数正好一一对应。由于这里的有效用户和实际用户它们使用的是同一个用户,所以它们的所属组id也是一样的。
getpgrp
函数是用来查询当前进程所属的进程组id,进程也有一个进程组,要注意和上边的用户组区分开。
getpgid
函数是用来查询指定进程的所属的进程组id,如果指定的进程是当前进程,其执行结果和getpgrp
函数的执行结果一致。
下边简单介绍一下怎么使一个程序的实际用户和有效用户不一样
chown
指令是用来设置文件的所有者和文件关联组,通过ls -l
指令可以查看当前process_id
可执行文件的所有者和文件关联组都是dx
,通过sudo chown
将可执行文件的所有者和文件关联组都设置为root
用户。然后通过chmod
改变文件的权限,通过u+s g+s
意味着当该文件被执行时,它将以文件所有者的身份和文件所属组的身份运行。通过这个操作就可以发现实际用户和有效用户的区别,实际用户指的是登录系统所用的用户,而有效用户指的是执行可执行程序的用户。这里的euid
和egid
都是0是因为root
用户的uid
和gid
都是0。
进程创建
fork函数
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
//功能:创建一个与当前进程(父进程)完全相同的一个进程(子进程)
//参数:无参
//返回值:fork函数的返回值有三种情况,在父进程中如果成功调用后返回的是一个大于0的子进程的id,在子进程中如果成功调用后返回值是等于0,当执行失败后,父进程会返回-1,并设置errno为错误码
vfork函数
#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
//功能:vfork创建子进程,但是子进程先运行且不复制父进程的内存空间(很少用)
示例–使用fork
函数创建子进程,并了解进程之间的关系
#include "header.h"
int main()
{
pid_t pid;
pid = fork();
if(pid > 0)
{
int i;
for(i=0;i<10;i++)
{
printf("this is parent process, parent's pid is %d, parent's father's pid is %d, pid is %d\n",getpid(),getppid(),pid);
sleep(1);
}
}
else if(pid == 0)
{
int i;
for(i=0;i<10;i++)
{
printf("this is child process, child's pid is %d, child's father's pid is %d, pid is %d\n",getpid(),getppid(),pid);
sleep(1);
}
}
else
{
perror("fork error");
exit(EXIT_FAILURE);
}
return 0;
}
通过执行结果可知,在当前代码中,父子进程是交替运行的,它们的先后顺序遵从进程调度。父进程的父进程是bash
,在父进程中fork
函数的返回值是子进程的标识符。子进程的父进程是执行fork
函数的进程,它执行fork
函数的返回值是0,通过fork
函数的返回值就能够确定当前正在执行的是哪一个进程。
如图所示,当运行一个进程P1后,那么系统就会给这个进程分配一个虚拟内存,在虚拟内存中的用户空间里包括代码段、数据段、堆、栈等详细信息。虚拟内存通过MMU(内存管理单元)
和实际的物理内存进行映射。当父进程通过使用fork
函数创建子进程以后会将整个虚拟内存拷贝一份给子进程,在子进程中除代码段以外的数据段、堆、栈都会有和实际的物理内存发生映射,而父进程和子进程的代码段是共享同一片内存空间的。
创建进程时发生的变化
虚拟内存空间的变化
父进程在通过fork
函数创建了一个子进程后,子进程会继承父进程的一些属性,也会有一些特有属性。
-
子进程的继承属性
-
用户信息和权限、目录信息、信号信息、环境、共享存储段、资源限制、堆、栈和数据段,共享代码段。
父进程通过
fork
函数创建了一个子进程,在这个过程中由于每一个进程都有一个虚拟内存空间,所以父进程会将内存空间整个复制给子进程。所以父子进程的代码段、数据段、堆、栈等内容是完全一样的。在传统的进程创建方式中:父子进程都有一个独立的虚拟内存空间,而它们的虚拟内存空间分别映射到不同的物理内存上,由此可见父子进程有相同但是却互相独立的虚拟内存空间和不同的物理内存空间。但是后来由于每通过fork
函数创建一个进程就给子进程分配一片物理内存空间,而当子进程的生命周期非常短的时候,尤其是在执行exec
函数的时候就会十分的浪费资源,所以后边就诞生了写时拷贝技术。写时拷贝技术的原理是**父进程在通过fork
函数创建子进程的时候会复制它的虚拟内存空间和它的虚拟内存映射(页表)。那么父子进程拥有相同的虚拟内存映射所以就会指向同一片物理内存空间,也就是说刚开始父子进程共用一片物理内存空间,并且这片物理内存空间是只读的。当其中有任意一个进程想要修改数据的时候,系统才会为尝试写入这片空间的进程分配物理内存,并将之前内存中的数据拷贝一份到这片内存中。**通过写时拷贝的操作能够保证进程之间的独立性,同时能够避免资源的浪费。
-
示例–验证fork函数创建进程时的操作
#include "header.h"
int g_v = 30;
int main(void)
{
int a_v = 30;
static int s_v = 30;
pid_t pid;
printf("parent pid is %d\n",getpid());
pid = fork();
if(pid > 0)
{
g_v = 40; a_v = 40; s_v = 40;
printf("this is parent process, getpid: %d getppid: %d pid: %d\n",getpid(),getppid(),pid);
printf("ag_v:%p aa_v:%p as_v:%p\n",&g_v,&a_v,&s_v);
}
else if(pid == 0)
{
g_v = 50; a_v = 50; s_v = 50;
printf("this is child process,getpid: %d getppid: %d pid: %d\n",getpid(),getppid(),pid);
printf("ag_v:%p aa_v:%p as_v:%p\n",&g_v,&a_v,&s_v);
}
else
{
perror("fork error");
}
printf("pid is %d g_v:%d a_v:%d s_v:%d\n",getpid(),g_v,a_v,s_v);
return 0;
}
通过代码的执行结果可以看到全局变量、静态变量、局部变量它们在父子进程中的地址是一样的。这里有一点比较奇怪,既然地址一样,那为什么它们的数据回不一样呢?这其实就涉及到了进程创建的原理。父进程通过fork
函数创建子进程后会将父进程的虚拟内存空间和虚拟内存映射一并复制给子进程(虚拟内存的用户空间包含代码段、数据段、堆、栈等)。所以这里的父子进程拥有相同的但是却各自独立的虚拟内存空间,所以这里打印出来的地址是一样的。由于目前采用的是写时拷贝技术,刚开始的时候父子进程指向的是同一片物理内存,所以只有当某一个进程对虚拟内存空间发生修改的时候系统会创建一片新的物理内存空间,因为父子进是通过虚拟内存映射到同一片只读的物理内存。这里父子进程都对这片内存空间发生了修改,所以这时候就会开辟一块新的内存空间。它们对虚拟内存数据的修改最终都会通过MMU(内存管理单元)
映射到物理内存上,而后边的打印变量就是物理内存又通过MMU(内存管理单元)
映射到虚拟内存上的数据,所以即使父子进程的地址一样,但是它们映射的却是不同的物理内存,那么它们的数据不一样也就不奇怪了。
对文件IO的影响
进程创建对文件IO的影响
#include "header.h"
int g_v = 30;
int main(void)
{
int a_v = 30;
static int s_v = 30;
pid_t pid;
//fopen是标准C库函数,所以在操作文件的时候会有缓存,所以当向文件写入或者从文件中读取的时候都会经过缓存,缓存的类型为全缓存
//open函数是系统提供的系统调用函数,所以不带缓存功能,它在向文件写入或者读取的时候能够直接拿到
FILE *fp = fopen("fp.txt","w");
int fd = open("fd.txt", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU | S_IRWXG); //如果文件不存在就创建文件,创建文件的权限为文件的拥有者和同组拥有rwx的权限
char *str = "hello world";
char buffer[48] = {'\0'};
fprintf(fp, "pid:%d, content:%s",getpid(),str); //fprinf函数向文件写入的时候会写入到缓存里,必须刷新缓存或者关闭文件才能够将内容写入到文件里去
sprintf(buffer,"pid: %d str:%s\n",getpid(),str);
write(fd,buffer,sizeof(buffer)); //write函数会直接写入到文件里而不经过缓存
printf("parent pid is %d\n",getpid());
pid = fork();
if(pid > 0)
{
g_v = 40; a_v = 40; s_v = 40;
printf("this is parent process, getpid: %d getppid: %d pid: %d\n",getpid(),getppid(),pid);
printf("ag_v:%p aa_v:%p as_v:%p\n",&g_v,&a_v,&s_v);
}
else if(pid == 0)
{
g_v = 50; a_v = 50; s_v = 50;
printf("this is child process,getpid: %d getppid: %d pid: %d\n",getpid(),getppid(),pid);
printf("ag_v:%p aa_v:%p as_v:%p\n",&g_v,&a_v,&s_v);
}
else
{
perror("fork error");
}
sprintf(buffer,"pid: %d str:%s\n",getpid(),str);
write(fd,buffer,sizeof(buffer));
//printf("pid is %d g_v:%d a_v:%d s_v:%d\n",getpid(),g_v,a_v,s_v);
fprintf(fp,"pid is %d g_v:%d a_v:%d s_v:%d\n",getpid(),g_v,a_v,s_v);
fclose(fp);
close(fd);
return 0;
}
缓存区一般位于虚拟内存中用户空间的堆区,缓存区存放在此可以自由的通过
malloc、calloc、realloc
开辟空间,也可以通过free
函数将不用的空间释放,总的来说在堆区开辟缓存区的方式相对自由,如果缓存区放在栈区,那么栈空间很快就会被用光。
如图所示,经过fopen
函数打开的文件具有缓存功能,后续的fprintf
函数都是在操作这个缓存区。因为缓存区位于堆中,所以当父进程使用fork
函数去创建子进程的时候,会将整个内存空间全部复制给子进程,这也就解释了为什么在fp.txt
文件里会有两句父进程的pid
加hello world
,因为这两句话是父进程刚开始执行的,子进程并不会执行。而open
函数是系统调用,它打开的文件不具有缓存功能,所以使用write
函数向fd.txt
文件写入数据的时候会立马写入到文件里,所以后边的fork
创建子进程并不会将它的数据拷贝给子进程。这也就是为什么fd.txt
文件为什么只有三句内容的原因。
-
子进程特有属性
- 进程ID、锁信息、运行时间、未决信号
-
操作文件时的内核结构变化
- 子进程只继承父进程的文件描述符表,不继承但共享文件表项和i节点
- 父进程创建一个子进程后,文件表项中的引用计数器加1变成2,当父进程使用
close
关闭文件描述符后,计数器减1,子进程还是可以使用文件表项,只有当计数器为0时才会释放文件表项。
在之前文件那一章有提到过在内核空间中有三个关于文件的结构体分别是文件描述符表、文件表项、i节点(内核中文件数据结构)。当父进程使用
fork
函数创建子进程以后会复制文件描述符表给子进程,但是文件表项和i节点是共用的。也就是说此时父子进程共有两个文件描述符表指向同一个文件表项,在文件表项中有一个引用计数器就是专门来统计有几个文件描述符表指向了文件表项。所以当父进程打开一个文件以后,再通过fork
函数创建子进程,此时引用计数器的数值为2,这个用法和硬链接的原理很相似(硬链接的原理)。所以当想要关闭文件的时候必须将引用计数器清零,即打开此文件的所有进程全部调用close
函数关闭文件。从用户角度来看,当定义一个文件指针或者文件描述符都属于局部变量,那么这个局部变量位于用户空间的栈区,所以当父进程通过fork
函数创建子进程的时候,会将文件描述符或者文件指针复制一份给子进程。
示例–代码演示fork创建子进程前后内核中有关文件的变化
#include "header.h"
int main(int argc, char **argv)
{
if(argc < 2)
{
fprintf(stderr,"usage:%s [filepath]\n",argv[0]);
exit(EXIT_FAILURE);
}
int fd = open(argv[1],O_WRONLY);
if(fd < 0)
{
perror("open");
exit(EXIT_FAILURE);
}
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(pid > 0)
{
if(lseek(fd,0,SEEK_END) < 0) //父进程通过lseek函数将文件位置偏移到文件的末尾,然后子进程去操作文件
{
perror("lseek");
exit(EXIT_FAILURE);
}
}
else
{
char *str = "hello MAKABAKA";
ssize_t size = strlen(str);
sleep(3);
if(write(fd,str,size) != size) //虽然父子进程操作的都是fd,但是子进程的fd是父进程将文件描述符表复制了一份给子进程
//所以它们操作的不是同一个fd,但是操作的是同一个文件。
{
perror("write error");
exit(EXIT_FAILURE);
}
}
printf("pid:%d finished\n",getpid());
close(fd); //这里的close函数会被父子进程执行两次,每执行一次就会将引用计数器的数字减1,直到将引用计数器减为0文件才会关闭
return 0;
}
在代码中,先在父进程中打开一个文件,然后创建一个子进程,在内核中会为子进程复制一个文件描述符表,然后父子进程的两个文件描述符表分别指向同一个文件表项,所以它们实际上操作的是同一个文件。当父进程将文件的位置偏移到最后的时候,子进程也会同步的看到文件此时的偏移量,然后从当前位置即文件的末尾开始写入。最后两个进程都要关闭文件描述符,确保引用计数器归零,然后才能释放这个已经被打开文件的资源。·
进程创建的应用
进程链和进程扇
进程链就是的结构是父进程创建子进程,子进程创建孙子进程,以此类推…在外边看来就像是链式结构,所以称为进程链。进程扇的结构是父进程一直在创建子进程,并不会让子进程再创建孙子进程,这种结构称为进程扇。进程链和进程扇都是具有血缘关系的进程,进程链属于父子之间的血缘关系,进程扇属于兄弟之间的血缘关系。
示例–使用fork函数创建进程链
#include "header.h"
int main(int argc, char **argv)
{
int i,cnt;
pid_t pid;
if(argc < 2)
cnt = 2;
else
cnt = atoi(argv[1]);
for(i=1;i<cnt;i++)
{
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(EXIT_FAILURE);
}
else if(pid > 0) //进程链的结构是父创子,子创孙,所以要中间衍生出来的父进程要退出循环,不让父进程再创建子进程
break;
}
printf("pid is %d and the parent pid is %d\n",getpid(),getppid());
while(1);
return 0;
}
如图所示,创建了一个进程链,它们的每一个子进程的父进程都是上一个子进程,一直到最开始运行peocess_link3
这个进程它的父进程是bash
。可以使用pstree
指令后边加-h
选项来查看进程链。
示例–使用fork函数创建进程扇
#include "header.h"
int main(int argc, char **argv)
{
int i,cnt;
pid_t pid;
if(argc < 2)
cnt = 2;
else
cnt = atoi(argv[1]);
for(i=1;i<cnt;i++)
{
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(EXIT_FAILURE);
}
else if(pid == 0) //进程扇的结构是父进程一直在创子进程,所以要保证循环中子进程不会再创建子进程
break;
}
printf("pid is %d and the parent pid is %d\n",getpid(),getppid());
while(1);
return 0;
}
进程扇最明显的特点就是子进程它们的父进程都是一样的,这里的进程号为3362的是bash
进程。
守护进程
- 守护进程(daemon)是生存周期长的一种进程。它们常常在系统启动时启动,在系统关闭时终止
- 所有的守护进程都以超级用户(用户ID为0)的优先权运行
- 守护进程没有控制端
- 守护进程的父进程都是
init
进程
关于守护进程的代码实现放到后边几章。
孤儿进程
孤儿进程是指父进程在子进程之前退出,导致子进程被init
进程或systemd
(通常是PID
为1)收养的进程。
示例–使用fork函数创建一个孤儿进程
#include "header.h"
int main()
{
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(EXIT_FAILURE);
}
else if(pid > 0)
{
printf("parent's pid is %d and the son's pid is %d\n",getpid(),pid);
exit(EXIT_SUCCESS);
}
else
{
sleep(3); //休眠3秒,保证父进程先退出,使子进程变成孤儿进程,从而被init进程收留
printf("son's pid is %d, and the parent pid is %d\n",getpid(),getppid());
}
return 0;
}
如图所示,当父进程由于子进程先退出,那么子进程就会变成一个孤儿进程,而Linux系统为了防止系统中产生过多的孤儿进程,进程号为1的init
的进程就会收留孤儿进程从而变成孤儿进程的父进程,在孤儿进程执行完相应的操作后,将孤儿进程的资源释放。
但是在现代的Linux系统中,/lib/systemd/systemd
是进程号为1的进程,在这种情况下,systemd
替代了传统的init
系统进程,成为了系统启动过程的管理者。所以这里看到收养孤儿进程的父进程是systemd
这个进程而不是init
进程。
僵尸进程
僵尸进程指的是子进程优于父进程先退出,其中子进程的大部分资源会释放。例如用户空间的代码段、数据段、堆、栈,但是内核空间的进程表项会被保留下来用来记载其进程状态,不会被调度器再次调度。
示例–使用fork函数创建一个子进程并观察它的运行状态
#include "header.h"
int main()
{
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("fork error");
exit(EXIT_FAILURE);
}
else if(pid == 0)
{
printf("pid is %d and the parent pid is %d\n",getpid(),getppid()); //子进程优于父进程先退出导致子进程仍保留了进程表项用来记载进程的状态
exit(EXIT_FAILURE);
}
else //父进程一在在做死循环
while(1);
return 0;
}
通过编译执行可以看到此时父进程的pid
为2905,子进程的pid
为2906,后边的状态那一栏父进程显示的是R+
表明此时父进程此时处于正在运行的状态,而子进程的状态是Z+
表明它是一个僵尸进程。后边的defunct
的意思是它是一个已经死的进程。如果将代码中父进程这里修改让它一直睡眠,则它的状态会变成S+
表明是一种可以使用信号中断的等待状态,而如果是D+
表明是以一种不可使用信号中断的等待状态,这种情况除非是在硬件条件改变的前提下才能够退出等待状态。
避免僵尸进程
虽然僵尸进程释放了大部分资源,但是仍然保留了进程表项来记录进程的状态,如果系统中产生大量的僵尸进程会造成资源的浪费,所以还是要将僵尸进程的进程表项所占有的资源释放掉。下边介绍几种解决僵尸进程的方法:
-
将父进程终止,使得僵尸进程也就是子进程变成孤儿进程,然后由
systemd
进程将孤儿进程收养,最后将孤儿进程的资源释放掉。虽然说子进程已经退出,但是它仍然保留了进程表项,所以它还是能够变成孤儿进程。-
结束父进程的方法
-
要将父进程终止可以直接按
Ctrl+C
来结束父进程的运行,然后让僵尸进程变成孤儿进程进而被systemd
进程收养,将孤儿进程的资源释放。 -
当父进程运行的时候不是一个前台进程,变成了一个后台进程,那么就不能使用
Ctrl+C
结束父进程了,因为Ctrl+C
只针对前台进程。那么就要通过下一章节的信号处理机制使用kill
发送信号,将进程杀死。这里只要在可执行程序后边加
&
符号,前台进程就变成一个在后台运行的程序了,使用kill
发送kill -9 pid
就可以结束对应的进程。其实上边的Ctrl+C
发送的也是信号,它的信号是SIGINT
,是用来中断程序运行的,kill -9
发送的信号是SIGKILL
直接杀死进程。
-
-
-
让僵尸进程的父进程来回收,父进程每隔一段时间来查询子进程是否结束并回收,调用
wait()
或waitpid()
,通知内核释放僵尸进(下边介绍) -
采用信号
SIGCHLD
通知处理,并在信号处理程序中调用wait
函数(下一章节介绍)