🔑🔑博客主页:阿客不是客
🍓🍓系列专栏:深入代码世界,了解掌握 Linux
欢迎来到泊舟小课堂
😘博客制作不易欢迎各位👍点赞+⭐收藏+➕关注
一、进程的概念
1.1 什么是进程
进程是一个运行起来的程序。
这句话在很多教科书上出现,但是这说了跟没说一样,什么是运行起来的程序呢,跑或没跑?跑起来的程序,和没跑起来的程序?我们不放首先来思考一个问题:
❓ 思考:程序是文件吗?
我们之前讲过,计算机的一切皆文件,文件在磁盘。
本章一开始讲的冯诺依曼,磁盘就是外设,和内存与 CPU 打交道,它们之间有数据交互。你的程序最后要被 CPU 运行,所以要运行起来必须先从磁盘外设加载到内存中。因此,当可执行文件被加载到内存中时,该程序就成为了一个进程。
1.2 先描述再组织
我们还是首先思考一个问题,通过问题去引出我们的知识点。
❓ 思考:操作系统中可能存在多个进程吗?
操作系统里面可能同时存在大量的进程!
既然如此,那操作系统要不要将所以后的进程管理起来呢?当然要,不要不就乱套了?当前想调用哪个进程,想让哪个进程占用 CPU 资源,
想执行哪个资源,数据一大你不管怎么行?所以我们刚才再次讲解了操作系统管理的概念:被管理对象的管理本质上是对数据的管理。那么 对进程的管理,本质上就是对进程数据的管理。
所以还是那句话 —— 我们需要 先描述再组织。
所以,当一个程序加载到内存时,操作系统做的不仅仅只是把代码和数据加入到内存,还要管理进程,创建对应的数据结构。linux 操作系统的内核是 C 语言写的,所以我们管理进程,就要先描述再组织,那描述一个事物我们当然是要用 —— struct。
1.3 进程控制块(PCB)
struct task_struct
{
进程的所有属性数据
};
在操作系统中,我们把描述进程的结构体称为 (Process Ctrl Block) 。在很多教材中,会把
称为 进程控制块。
❓ 为什么每个进程都要有
呢 (task_struct)?
💡 因为操作系统要管理我们的进程,想要管理就必须要 "先描述再组织" 。
进程信息被放在一个叫做进程控制块(PCB)的数据结构中,可以理解为进程属性的集合。PCB是进程存在的唯一标识。在Linux环境下,PCB就是task_struct,一个包含进程属性信息的结构体。
然后我们就可以将进程理解为:被进程控制块PCB所管理的可执行程序。一旦可执行程序被执行加载到内存,操作系统就会创建对应的PCB将其管理,最后就形成了进程。
❓ 为什么我们的 task_struct 每个进程都要有呢?
💡 因为这是为了管理进程而描述进程所设计的结构体类型,将来当有一个进程加载到内存时,操作系统在内核中一定要为该进程创建 task_struct 结构体变量,并且要将该变量链入到全局的链表当中。通过双向链表的形式将各个进程控制块 联系起来,方便管理。
操作系统如果新创建一个进程就将其对应的 链接入双向链表中,要删掉一个进程,实际上就是遍历所有的链表结点,把对应进程的
和代码都释放掉,这就叫对链表做管理。
终你会发现,操作系统对进程的管理,最终变成了对链表的增删查改。
什么是进程?目前为止我们可以总结成:进程 = 可执行程序 + 该进程对应的内核数据结构
1.4 task_struct的内容
task_struct是Linux当中的进程控制块,主要包含以下信息:
- 标示符(PID): 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器(pc指针): 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- 进程的代码是不可能在很短时间运行完的,规定每个进程的时间片(单次运行的最长时间),用户感受到的多个进程同时运行,本质上是CPU的快速切换。CPU只有一套寄存器,为了保护上下文,进程的这些临时数据被写入在PCB中,再来执行时,恢复上下文。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 进程创建出来,CPU要执行它对应的代码,然而CPU很少,进程很多。因此OS内有一个调度模块,负责较为均衡的调度每一个进程,较为公平的获得CPU资源。让每个进程都能获得CPU资源,让每个进程都能被执行。
- 其他信息。
二、查看进程
2.1 通过指令查看进程
我们历史上执行的所有指令工具,自己的程序,运行起来都是进程
我们先创建一个 mytest.c 文件,然后写上一个死循环,每隔1秒就打印一句话:
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
sleep(1);
printf("这是一个进程!\n");
}
return 0;
}
接下来我们 ./code 去运行它,此时这个程序就变成了一个进程:
那么此时,我们可以再开一个xshell,使用 ps 查看进程,我们这里就先用 ps aux 来做个演示:
此时他就会将你系统中所有的进程显示出来,这些都是系统中所对应的相关启动进程。我们刚才直接使用 ps aux 打出来的都是以行为单位,如何我想查看我们刚才的 code 进程呢?
ps aux | grep 'code'
看到这里,你应该能发现了,其实没有什么神奇的,就相当于所有的指令是进程而已。Windows 下的任务管理器:
2.2 进程 ID(pid)
每一个进程在系统中,都会存在一个惟一的标识符!这就如同每个人都有身份证号一样,进程也需要标号的,所以每个进程都存在有一个 。
我们的 code 现在还在后台欢快的跑着呢,此时我们可以把所有的 title 列名称显示出来:
ps aux | head -1
此时我们成功把属性提取出来了,我们使用 && 进行下一步操作(逻辑与,前面指令成功再执行下面的指令)
ps aux | head -1 && ps aux | grep 'code' | grep -v grep
2.3 获取 pid(getpid 函数)
下面我们隆重介绍下获取 的函数 —— getpid() 想要查看进程
,一定是这个进程得运行起来。我们不妨先问问 Linux 手册中的那个男人,getpid 的下落:
man 2 getpid
💬 我们修改一下刚才的 mytest.c 代码:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(1)
{
sleep(1);
printf("这是一个进程!我的pid:%d\n",getpid());
}
return 0;
}
🚩 运行结果如下:
启动后,我们发现我们的 mytest 可执行程序的 为 5399。是否果真如此?我们还是用 ps aux 验证一下看看:
ps aux | head -1 && ps aux | grep 'code' | grep -v grep
2.4 杀进程
我们再来回忆一下我们是如何杀掉一个进程的……
这是我们之前讲的,在 Linux 命令行中的热键,遇到问题解决不了可以用它来中止。所谓的 就是用来杀进程的。除此之外,你也可以选择在另一个终端中使用 kill 命令:
kill -9 PID
当前你只需要知道可以通过 kill -9 命令杀掉进程就行了,至于这个 号信号,我们会放在后面的信号章节去讲!
比如我们现在想杀掉刚才运行的, 打出进程 的 mytest 进程,其
为4181
2.5 通过 proc 目录查看进程信息
上面我们讲述了查看进程的第一种方式,即最常用的 ps aux 。下面我们要来讲解第二种方式,在讲解之前我们先来探讨一下 "当前路径"
ls /
proc:内存文件系统,里面放的是当前系统实时的 进程信息。既然如此,现在我们就用 ls /proc 看一下我们的 process 进程信息:
这就是当前进程的 ,刚才我们说了 /proc 里保存的是内存当中实时的进程信息。那我们在 /proc 目录下找这个:
既然是实时的,那我们把跑的正欢的 mytest 进程 ctrl+ c 干掉,看看这个文件夹是否还健在:
我们已经证明了实时的概念,现在我们再去研究一下进程的信息,我们再把进程启动起来。启动之后再查 5399,发现还是没有:
那是当然的,原因很简单,因为重开了嘛!我们在用指令去查看新的 :
ps aux | head -1 && ps aux | grep 'code' | grep -v grep
进程 发生了变化:5399
5424,我们再来查看它的属性:
这里面的东西很多,目前想搞懂里面都是做什么的还为时尚早,我们先 -l 看看细节:
ls /proc/[pid] -l
我们重点去关注 exe 和 cwd:
- exe:指出进程对应的可执行程序的磁盘文件
- cwd:指出进程当前的工作路径
下面我们先终止进程,修改一下 mytest.c 文件的内容,给它加一个文件操作:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
FILE* fp = fopen("test.txt", "w");
while(1)
{
sleep(1);
printf("这是一个进程!我的pid:%d\n",getpid());
}
return 0;
}
成功运行,此时我们 ls 就能发现当前路径下多出一个 test.txt 文件,这就是我们自己创建的:
我们在C语言中讲到:fopen 后面如果不带路径,那么会默认在当前路径。所谓的当前路径,其本质!也浮现出来了 —— 当前进程所在的路径,进程会自己维护,进程会知道自己的工作路径在哪里:
cwd
/home/sakura/进程
那我们想在进入之后修改当前路径呢?要用到 chdir:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
chdir("/home/sakura");
FILE* fp = fopen("test.txt", "w");
while(1)
{
sleep(1);
printf("这是一个进程!我的pid:%d\n",getpid());
}
return 0;
}
cwd 结果如下:
2.6 父进程 ID(ppid)
(parent process id) 其实就是父进程
。
可以通过 getpid() 函数获取,其实
也有与之对应的函数,那就是 getppid() 。我们还是从 code.c 下手,刚才我们加入了 getpid, 现在我们再给句子后面加入 getppid:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
chdir("/home/sakura");
FILE* fp = fopen("test.txt", "w");
while(1)
{
sleep(1);
printf("这是一个进程!我的pid:%d, 我的ppid:%d\n",getpid(), getppid());
}
return 0;
}
🚩 代码运行结果:
我们还是验证一下,这里要看 ,刚才的 ps aux 是显示不到的,这里介绍一下 ps ajx :
ps ajx | head -1 && ps ajx | grep 'code' | grep -v grep
ps ajx 就能把 和
同时显示出来了。我们刚才发觉到
在每次启动都会重新分配,但是好像这里的
似乎恒定不变啊。
❓ 思考:我的父进程为什么不变?是谁呢?
ps axj | head -1 && ps axj | grep 2723
我们的父进程是一个叫 的东西!这个现象,我们可以推导出一个假设:几乎我们在命令行上所执行的所有指令包括你自己定义的 cmd,都是
进程的子进程。
2.7 使用 fork() 创建子进程
我们可以使用fork
函数创建一个子进程。 它有两个返回值。父进程返回子进程的 ,给子进程返回 0。
💬 代码演示:我们来看看会发生什么:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
printf("父进程开始,pid:%d\n", getpid());
pid_t id = fork();
printf("这是一个进程!我的pid:%d, 我的ppid:%d\n",getpid(), getppid());
return 0;
}
🚩 代码运行结果:
第一行数据是该进程的PID
和PPID
,第二行数据是代码中通过调用fork
函数创建的子进程的PID
和PPID
。其中该进程的PID
就是子进程的父进程PID
,所以我们可以说这两个进程是父子关系。而该进程的父进程就是bash
,一般而言,在命令行上运行的指令,父进程基本都是bash
。
- 如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0。
- 如果子进程创建失败,则在父进程中返回 -1。
所以我们可以通过 fork 的返回值,让父子进程分别去执行不同的过程。代码示例如下:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
while (1)
{
printf("我是子进程,我的pid: %d,我的父进程是 %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
// parent
while (1)
{
printf("我是父进程,我的pid: %d,我的父进程是 %d\n", getpid(), getppid());
sleep(1);
}
}
return 0;
}
🚩 运行结果如下:
我们发现,这两块代码是可以同时执行的。
原因:fork 之后,子进程是继承于父进程的,父进程和子进程会共享代码和数据,一般都会执行后续的代码。这也是为什么刚才的 printf 会打印两次的原因。fork 之后,父进程和子进程返回值不同,所以可以通过不同的返回值去判断,让父子执行不同的代码块
❓ 问题1:父进程返回子进程的
,给子进程返回 0,为什么?
一个父进程有多个子进程,而一个子进程只有一个父进程,父进程必须要知道 fork 产生的子进程的 pid,父进程需要通过 pid 来区分不同的子进程。
子进程最重要的是要知道自己被创建成功了,而子进程因为只有一个父进程,子进程找父进程的成本非常低。如果想获取,直接 getppid() 即可。
❓ 问题2:为什么 fork 会返回两次?
调用一个函数,当这个函数准备 return 的之后,那么这个函数的核心功能完成了吗?
当我们函数准备执行 return 的时候,函数的核心功能已经完成:
① 子进程已经被创建了
② 将子进程放入运行队列
最后,return 是代码吗?是的!所以当我们走到 return 时父进程有了,子进程也已经在运行队列了,fork 后代码共享,父子进程当然会执行后续被共享的 return 代码。因此,父进程执行一次 return,子进程执行一次 return,最后就是两个返回值了。
❓ 问题2:为什么一个变量既 ==0,又>0?让 if,else 同时执行?
我们目前还没有能力完全解决这个问题,我们先简单地了解一点。
进程之间具有独立性! 即使一个进程中途异常退出,也不会影响其他进程。
fork 函数,OS syscall call,fork 之后,OS 做了什么?是不是系统多了一个进程?
- task_struct + 进程代码和数据
- task_struct + 子进程的代码和数据
子进程的 task_struct 对象内部的数据基本是从父进程继承下来的。子进程执行代码,计算数据的,子进程的代码从哪里来呢?和父进程执行同样的代码,fork 之后,因为代码对于进程来说是只读的,父子进程代码共享,数据也同样共享。
但数据又要各自独立!
对于父子任意一方,如果想要对数据进行修改,OS会把数据在底层拷贝一份,让对应目标进程修改这个拷贝,这个技术叫做写时拷贝!
三、进程状态
3.1 什么是进程状态
进程状态在 Linux 内核中就是个 整数,这个整数在进程的 task_stuct 中:
int status
在Linux
中,一共有七种状态:
其中最重要的三种状态是:运行、阻塞和挂起。
3.2 运行状态
📚 运行态:进程在运行队列中,代表我已经准备好了,随时可以调度。
进程只要在运行队列中,就叫做 运行态。
每一个 task_struct 都能找到对应的代码和数据,让进程排队。并不意味着进程一定在运行中,一个进程处于R
状态,它只是表明进程要么是在运行中要么在运行队列里,随时可以被 CPU 调度。
3.3 阻塞状态
📚 阻塞:进程等待某种资源(非CPU),资源没有就绪的时候,进程需要在该资源的等待队列中进行排队,此时进程的代码并没有运行,此时进程所处的状态就叫做阻塞。
为了讲解进程阻塞,我们先了解两个知识点:
- 一个进程使用资源的时候,可不仅仅是在申请 CPU 资源
- 进程可能会申请其它资源:磁盘、网卡、显卡,显示器资源……
外设速度慢,CPU 太快了,所以才会有内存这样的设备。操作系统的核心工作叫做先描述再组织,通过这样的方式来对软硬件资源作管理。我们所看到的软件在系统中一定会存在数据结构维护描述,每一个都有对应的资源。
假设现在有一个进程正在被 CPU 调度,它要读 1G 的数据到内存,此时 CPU 就开始执行它读数据的代码,这种情况传统意义上就是 IO 读取时,可是磁盘数据没有就绪,进程该怎么办?
难道就眼睁睁地看着该进程占 CPU,在 CPU 上傻等吗?
不!不可能,绝对不可能!
- 如果磁盘没就绪,我就把你这个进程丢到磁盘的等待队列当中,在你等待的期间,我们的 CPU 正在同时处理其他任务。
- 如果此时又有个读网卡的进程,操作系统还在忙着呢,就直接丢到网卡的等待队列当中。
所以,当访问某些资源(磁盘,网卡等),如果该资源暂时没有准备好,或者正在给其他进程提供服务,那么此时:
- 当前进程要从 runqueue 中逐出。
- 将当前进程放入对应设备的描述结构体中的等待队列。
当我们对应的设备就绪,在硬件层面上准备完毕,一定会通过某种方式让操作系统知道。如果已经可以入场了,会把该进程的 从等待队列放入运行队列中。放到运行队列中,CPU 就可以去处理这个进程了。
当我们的进程此时在等待外部资源的时(处于等待队列),该进程的代码不会被执行。当前进程去等待某种资源就绪而导致并不运行时所处的状态,就叫做 进程阻塞。
3.4 进程挂起
(挂起)
进程挂起:一个进程对应的代码和数据被操作系统因为资源不足而导致操作系统将该进程的代码和数据临时地置换到磁盘当中,此时叫做进程挂起。
挂起和阻塞很像,最终挂起也是卡住,但是挂起和阻塞在操作系统的定义上是不一样的。我们还是带着大家理解一下挂起,挂起就要换另一种讲法了:
如果内存不足了怎么办?操作系统就要帮我们进行 辗转腾挪 。短期内不会调度(你等的资源,短期内不会就绪)进程,它的代码和数据依旧在内存中,那岂不是在白白的浪费空间?
操作系统就会把该进程的代码和数据置换到磁盘上的交换分区,这样的进程就是 进程挂起。
在等待序列中被挂起被称为阻塞挂起。如果内存实在是不够用,操作系统还有可能将正在运行队列末端的程序挂起甚至杀死(闪退),等到调度的时候再拿回来,这种状态叫做运行挂起。
往往内存不足的时候,伴随着磁盘被高频率访问,就可能是因为操作系统一直在做辗转操作。
四、linux 的进程状态
4.1 linux 进程源码
文章开头说的 ——
" 所谓的进程状态,本质上其实就是个整数。"
那这些 整数 究竟是什么?我们现在就来研究一下。为了研究 Linux 进程状态,我们把源码先拿出来看看:
其中 就是运行态
4.2 CPU 运行速度
我们还是用上一章学的 ps 方式,当时是为了看 的。这里我们也能拿 ps 指令来看进程的状态,我们开另一个xshell输入:
ps axj | head -1 && ps axj | grep code| grep -v grep
这个 栏记录的就是该 process 可执行程序进程的状态了:
怎么是 ,我们的 process 不是在运行吗,怎么不是
?
首先需要声明一点,状态后面跟加号,表示是一个 前台进程,你只需要知道的是,能够在键盘上 Ctrl+c 暂停的都可以叫前台进程。
我们 process.c 里的代码值得执行的也就一个 printf 输出语句而已,很快时间就完了。所以大部分时间你都在 sleep(1) !
好,既然如此,那我们把 sleep(1) 给注释掉,这下总该是一直在运行了吧?
while(1)
{
//sleep(1);
printf("这是一个进程!我的pid:%d, 我的ppid:%d\n",getpid(), getppid());
}
我们在用 ps 指令看看状态 栏是个什么:
啊这,还是 状态啊,这是为什么呢?
💡 真相:显示器本身是个外设,它非常慢,即便它闲着呢,准备好刷新它也要花时间的,所以这个进程它看起来像死循环地进行 printf 打印,实际上这个进程 90% 的情况都在等所对应的显示器就绪进行打印,因为显示器太慢了!只是因为打印的东西很快一瞬间就完成,所以我们 ps 查看到的这个进程,大部分情况在内核中都处于
状态。
我们让程序跑一个空循环呢?
这个代码也没有访问其他外设资源,页没有读文件也没有读磁盘也没打印,就纯纯的死循环。所以这个进程不访问任何资源,只等你 CPU,只要你被运行期间不访问外设,就不会被阻塞。不访问外设,那么死也会在等待队列里,一直在等待队列中,这就让 process 达成 状态。
但这还有个 号,代表程序在前台运行,阻塞了其他的程序,让我们在运行时加个 &:
./code &
成功让其在后台执行:
4.3 S 状态(阻塞状态)
若一个进程是 状态,那么它也能称作是 阻塞状态,这也意味着它一定是在等待某种资源。
我们目前等待的是硬件,刚才举的 code.c 代码例子中 sleep 其实就是等待软件。 是阻塞状态,其实是一种 休眠状态,S 代表 Sleep!睡大觉!
我们一般把 状态叫作 浅度睡眠,也叫做 可中断睡眠。
- 顾名思义,当进程处于
状态,它可以随时被唤醒。
- 不仅仅是操作系统可以唤醒,你也可以唤醒,甚至你想杀掉它都行。
kill -9 [pid]
4.4 T 状态(暂停状态)
4.5 D 状态(阻塞状态)
下面我们来看 状态,
状态也是一种阻塞状态,它也是要我们得进程等待某种资源。资源不就绪,就处于某种等待状态。那么与
状态有什么区别呢?我们细看:
S (sleeping)
D (disk sleep)
💭 场景例子:
某进程在等磁盘干完活期间挂出 状态,此时系统繁忙 OS 发现该进程不干活,就把它干掉了。此时磁盘写入失败了,这 500 MB 数据该如何处理?雇用磁盘的进程被杀死了,这个货该交给谁?那就直接丢掉?如果这 500 MB 是转账信息呢?是银行系统呢?是重要数据呢?
OS(狂暴):嗷!我有权力杀掉进程,我杀掉进程有错吗?我在执行一个管理者的权利,怎么能怪我呢?
进程(委屈):从事件开始到事件结束,我这个进程好像没有做过任何事情吧?你叫我给数据给磁盘,我也给了,给的时候我等磁盘结果有什么问题?我怎么就偷懒了,就算偷懒,我也什么都没做,我就在这等磁盘给我结果,过了一会家里就来了个大汉(OS)带着一群人拿着各种斧子砍刀把我做掉了,我是受害者啊!这种事情发生你怎么能怪我啊!我真的是 "人在家中坐,锅从天上来啊。"
磁盘:看我干啥?这个故事当中,我就是个跑腿的,你进程让我干啥我就干啥,你这个进程既然在那里等,你不就是在等我写入的结果吗?早就走了你还用等?我写入就是有失败的情况,要是每次都 100% 成功你还用在这等?你既然在这等,你就是默许我能失败。况且我还不一定失败呢,我是写完了发现你进程人没了!OS 你也是,你没事杀它什么!
此时 OS 想了想,我作为管理者,我应该给自己立个法则,创造一个 OS 都杀不死的:
"disk sleep —— gif.latex?gif.latex? 状态就诞生了!"
我们一般把 状态叫做 深度睡眠,也叫 不可中断睡眠。OS 无权唤醒或杀死之。此时通过
甚至 kill -9 都没有任何卵用,只有等到磁盘读写完毕将数据交付给该进程后,此时
状态的该进程才会醒来,其状态或将变为
。
但这也并不意味着 状态进程真的天下无敌,高枕无忧了!"关机重启" 和 "拔电源" 就能干掉它。
这个 状态我们就不模拟了……可能会把我的机子磁盘打满(害怕.jpg)
4.6 X 状态(死亡状态)
状态很简单,我们刚才也介绍过了,我们先说说
状态:dead 代表死亡,所以
状态对应的就是 死亡状态。这个没有什么好说的,
状态的进程就代表死亡了。
4.7 Z 状态(僵尸状态)
我们下面重点来说一下 状态,
表示 Zombie(僵尸),
状态就称之为 僵尸状态。
僵尸状态:当一个 Linux 中的进程退出的时候,一般不会直接进入
状态(死亡,资源可以立马被回收),而是进入
状态。
状态是一种已经死亡的状态,僵尸,什么是僵尸?僵尸又称为活死人,是一种半死不活的东西。就是一个进程死了之后,我们等一等,不要它立马把资源释放,阻止
立刻进入
状态。
❓为什么要先进入 ?
进程为什么被创建出来呢?一定是因为要有任务让这个进程执行,当该进程退出的时候,我们怎么知道这个进程把任务给我们完成的如何了呢?当然要了!一般需要将进程的执行结果告知给父进程或OS。进程为
状态,就是为了维护退出信息,可以让父进程或者 OS 读取的,退出信息会写入 test_struct。
五、僵尸进程和孤儿进程
5.1 僵尸进程
处于僵尸状态的进程,我们就称之为僵尸进程。
僵尸进程存在的意义:表征进程退出时是因为什么原因而退出的。
现在我们来模拟一下僵尸进程,很简单:
如果创建子进程,子进程退出了,父进程不退出也不等待子进程(回收),此时子进程退出之后所处的状态就是 状态。
💬 代码演示:模拟一个僵尸进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 5;
while (cnt)
{
printf("我是子进程,我还剩下 %ds,我的pid:%d,我的ppid:%d\n", cnt--, getpid(), getppid());
sleep(1);
}
printf("我是子进程,我已经变僵尸了,等待被检测,我的pid:%d,我的ppid:%d\n", getpid(), getppid()));
}
else
{
// father
while (1)
{
printf("我是父进程,我正在运行...\n");
sleep(1);
}
}
return 0;
}
用 ps 检测一下看看,我们每隔一秒检测一次,然后换行,写一个监控脚本:
while :; do ps axj | head -1 && ps axj | grep code| grep -v grep; sleep 1; echo "";done
🚩 运行结果:
❓ 思考:长时间僵尸,会引发什么问题?
如果没有人收尸,该状态会一直维护,该进程的相关资源 (task_struct) 不会被释放!会有内存泄露的风险!一般必须要求父进程进行回收,如何回收的问题我们会在进程控制章节讲解。
没人收那我 kill 掉它可以吗?他本来就是死掉的进程,你还 kill 它有什么用……
5.2 孤儿进程
我们顺便再讲解一下孤儿进程,孤儿进程顾名思义,就是父亲先退出了,孩子还在的情况。我们来模拟一下孤儿进程的情况,模拟让子进程一直不退出,父进程倒计时很快退出即可:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
while(1)
{
printf("我是子进程,我的pid:%d,我的ppid:%d\n", getpid(), getppid());
sleep(1);
}
}
else
{
// father
int cnt = 5;
while (cnt)
{
printf("我是父进程,我还剩下 %ds,我的pid:%d,我的ppid:%d\n", cnt--, getpid(), getppid());
sleep(1);
}
}
return 0;
}
还是用我们刚才写的监控脚本,监控一下:
while :; do ps axj | head -1 && ps axj | grep code| grep -v grep; sleep 1; echo "";done
🚩 运行结果:
❓ 疑问:父进程退出,为什么父进程没有变成僵尸?我们怎么没有看到父进程 为
?
那是因为父进程的父进程是
,它会自动回收它的子进程,也就是这里的父进程。这里之所以没有看到父进程变成僵尸,是因为被
回收了,
的状态很快,所以你没看到。
那为什么刚才我自己代码中的父进程创建的子进程,父进程没有回收子进程呢?那是因为你的代码压根就没有写回收,所以你的子进程就没有回收。既然子进程需要父进程回收,如果父进程先提前退出了,那孤儿进程如何处理?
细心的读者应该发现了,上图中父进程退出后,子进程 变为了 1,其实就是被 "领养" 了。也就是说,如果父进程 提前退出,子进程还在运行,子进程会被 1 号进程领养。
1 号进程是何方神圣?其实就是操作系统!1 号进程,即 为 1 的进程,Linux 系统启动后,第一个被创建的用户态进程。
我们把被 1 号进程领养的进程,称之为 孤儿进程 。
前面我们说了, 后有加号的属于前台进程,只要能被 Ctrl + c 干掉的都是前台进程。而我们的孤儿进程似乎
都无可奈何:
刚才变成孤儿进程后 也是从
变为了
,这就叫做 后台进程。
后台进程其实还是在运行,只是会影响命令行输入,既然 Ctrl + c 都无法奈其何,kill 算了!诶,我们刚才说的是僵尸进程 kill 杀不死,但是这是孤儿进程,孤儿还是人!因为……
"人被杀,就会死。"