Linux进程-上

目录

1. 冯诺依曼体系

1.1. 冯诺依曼体系是什么?

1.2. 为什么还要有一个存储器呢?

原因一:

原因二:

1.3. 冯诺依曼体系的理解

1.4. 冯诺依曼体系的总结:

2.认识OS及相关概念

2.1. OS是什么 ?

2.2. 为什么要有OS ?

2.3. 如何理解管理

2.4. 如何理解管理理念

2.5. 如何理解系统调用接口

3.进程概念

3.1. 为什么要有PCB

3.2. 可执行程序和进程的区别 

3.3. 什么是PCB

3.4. task_struct的分类

3.4.1. 上下文数据的理解

总结:上下文数据的理解

3.5. 查看进程

方式一: 利用ps命令查看进程

方式二:利用top命令

方式三:通过/proc这个系统目录查看

3.6. 通过系统调用获取进程标识符

补充:pid_t这个类型究竟是什么呢?

补充:简单介绍一下kill命令

3.7. fork()系统调用

1. fork()的理解

2. fork()的一般使用

3. fork()的两个返回值为什么是这样

4. fork()为什么会有两个返回值呢

5. fork()后父子进程的执行顺序

3.8. 操作系统进程的状态

3.8.1. 新建:

3.8.2. 运行

3.8.3. 阻塞

3.8.4. 挂起

3.8.5. 挂起阻塞

3.9. Linux操作系统具体的进程状态

3.9.1. R状态:       

3.9.2. S状态:   

演示一: 

演示二:

3.9.3. D状态:   

3.9.4. T状态:   

3.9.5. t状态:   

3.9.6. X状态:

3.9.7. Z状态:

僵尸状态是什么?

僵尸状态为什么?

3.9.8. 孤儿进程

为什么要被领养呢?


1. 冯诺依曼体系

1.1. 冯诺依曼体系是什么?

冯诺依曼体系是一种计算机体系结构或计算模型。以匈牙利裔数学家冯·诺依曼(John Von Neumann)的名字命名。它是现代计算机体系结构的基础。它包含了五大组件。输入设备、输出设备、存储器、运算器、控制器;而运算器和控制器又统称为CPU。

输入设备是计算机系统用于接收外部数据或命令的硬件设备。它们允许用户将信息输入到计算机系统中,使其能够进行处理和操作;常见的一些输入设备有:键盘、摄像头、鼠标、磁盘、网卡等等。

输出设备是计算机系统用于向用户呈现处理结果或转移数据的硬件设备。它们将计算机处理后的数据、图像、声音等信息展示给用户或其他设备。常见的一些输入设备有:显示器、音响、磁盘、网卡等等。

中央处理器(CPU):CPU有两个单元:算术逻辑单元(运算器)和控制单元(控制器)。

运算器: 可以进行算术运算、逻辑运算;

控制器:CPU是可以借助控制器响应外部事件的,比如将数据从外设Load到内存。

1.2. 为什么还要有一个存储器呢?

假如我是一个计算机体系的设计者,那么我可能最简单粗暴的想法就是这样:

我们的想法就是:不要中间的存储器,直接让CPU和外设进行交互。 但是,我们知道,冯诺依曼是在外设和CPU之间构造一个"缓冲"区域也就是存储器,并且被世界人民所接受,必有他之道理。我们就来分析一下,为什么?

原因一:

首先我们需要知道,输入设备的本质是为了产生数据;输出设备的本质是保存或者显示数据。而在一般的体系结构中,硬件处理数据的效率:CPU || 寄存器  > 内存  >  磁盘/SSD > 光盘 > 磁带;我们也可以给个大概的概念,以便理解:CPU || 寄存器处理数据的量级在纳秒(10^-9s)级别;内存处理数据的量级在微妙(10^-6s)级别;外设处理数据的量级在毫秒(10^-3s)级别。

而在生活中,我们可能听说过一个原理 --- 木桶原理

什么叫做木桶原理呢?就是对于一个木桶来说,其盛水的能力取决于最短的那块木板。

如果我们将木桶的每一块木板看作为一个元素,那么由众多木板组成的这个木桶就可以被看作为一个系统,我们将这个系统类比到我们的计算机的体系结构,也就是说,计算机处理数据的能力并不取决于最快的那一个硬件,而是由最慢的硬件所决定的。

例如,对于一台计算机系统,它的性能取决于最弱的环节,无论是CPU、内存、硬盘存储、网络连接还是其他组件。如果其中任何一个部分的性能较低或存在瓶颈,整个系统的效能都会受到限制。

如果计算机体系由我们之前认为的那样,即CPU直接和外设进行交互,那么带来的一个结果便是:该体系处理数据的能力被严重的限制,从而导致整机的效率低下。

因此,冯诺依曼为了考虑整机的效率,设计出了存储器(内存);有了存储器,我们就可以利用一些软件策略,比如OS;OS会预先从外设中将数据装载到存储器中,当CPU需要处理数据时,只和存储器进行数据层面上的交互,此时的短板就是存储器,而非外设了,提高了整机效率。

原因二:

可能我们会有这样的疑问,即为什么不把所有的存储设备都用内存呢?或者用更快的寄存器呢?这样不就可以提高效率了吗?

首先,这种做法并不太适合。

原因一:无论你是内存亦或者寄存器,你们的存储方式都是带电存储。也就是说,你们的存储是有限制的,断电或者计算机重启,如果数据还在内存中或者寄存器里,此时就会导致数据丢失。

原因二:我们假设电脑永远正常运行且不断电,如果此时将所有存储设备换成内存或者寄存器,效率可能没有提高多少,但会带来另一个问题,那就是成本爆炸式的增长。我们知道寄存器或者内存与硬盘相比成本是非常高的。此时就会导致这种产品的适用人群急剧减少。因为对于广大人民群众来说,他们所选择的产品往往是价格偏低,质量良好的产品。因此,冯诺依曼的这种体系更好的符合了人民的需求。

因此,存储器作为外设和CPU在数据层面上的一个"缓存结构",使得整机处理数据的效率大大提高且降低了成本。故,冯诺依曼体系被广泛接受并运用。

1.3. 冯诺依曼体系的理解

 1、CPU读取数据(数据 + 代码),都是要从内存中读取,站在数据的角度,我们认为CPU不和外设直接交互。

2、如果CPU要处理数据,需要先将外设中的数据Load内存。站在数据的角度,外设只和内存直接交互。

结论:

输入设备将数据Load到内存,我们称之为Input,例如scanf

内存将数据刷新到输出设备,我们称之为Output,例如printf

输入设备 到 输出设备 我们称之为 IO过程。

这样我们也就可以理解为什么我们之前说,一个程序要运行起来,必须先Load到内存中。

这是因为冯诺依曼体系结构的特点决定的。

1. CPU处理数据,只和内存进行交互;

2. 内存的处理数据的效率快,提高了快速的访问速度;

3. 有效管理系统资源以及确保程序的隔离性;

1.4. 冯诺依曼体系的总结:

1.这里的存储器指的是内存,并非磁盘。

2. 不考虑缓存情况,这里的CPU能且只能对存储器其进行交互,不能访问外设(输入或输出设备)

3.外设要进行输入/输出数据,只能先写入内存或者先从内存读取数据

4.总而言之,所有设备(外设/CPU) 只和存储器交互。

2.认识OS及相关概念

操作系统:任何计算机系统都包含一个基本的程序集合,称之为操作系统。大概的理解:

OS包括: 1.内核(进程管理、内存管理、文件管理、驱动管理); 2. 其他程序(shell程序,函数库等等);

在整个计算机软硬件框架中:  OS的定位是: 一款纯正的搞管理的软件

2.1. OS是什么 ?

操作系统是一款软件,专门对软硬件资源进行管理工作的软件;

2.2. 为什么要有OS ?

对下:管理好软硬件资源 (方式)

对上:给用户提供高效的、稳定的、安全的、简单的运行环境 (目的)

2.3. 如何理解管理

在现实世界中,一个学校里面的人充当着不同的角色。在这里我们把学校体系简化一下:校长、辅导员、学生。

而要做到管理,前提首先要有管理者被管理者,以及执行者

对应到上面,那么校长就是管理者;学生就是被管理者;辅导员就是执行者。

而在现实世界中,我们做的事情无非分为两种,做执行做决策

管理者在管理这件事情扮演的角色就是做决策。执行者扮演的角色就是执行者。

现实生活中,校长管理学生是如何管理的呢?是直接走到你面前告诉你,你今天高数作业没交?下午的大物没去听课?答案是,肯定不是这样的。现实是,在大学里,除了开学典礼和毕业典礼我们可能见上校长一面,其他时间,我们几乎见不到他,那校长是如何管理学生的呢?

答案是:校长是通过各项数据管理学生的,通过对某位学生的各项数据进行分析处理,以做出相应的决策,也就是说,做决策的依据就是数据。

例如:李四这学期上了6门课,挂了5门课,校长通过教务系统的数据得出李四这个学生是一个不认真对待学习的人,给予留级处理。这就是通过数据做出相应决策的处理方案。

那么,现在问题又来了,管理者如何得到被管理者的数据的呢

此时就需要执行者去获取被管理者的数据。

我们此时就可以类比到计算机:

管理者:操作系统

执行者:硬件驱动

被管理者:底层的硬件

操作系统通过硬件驱动获得底层硬件的各种数据为依据,做出各种决策,让硬件驱动去执行对应决策,以达到管理的目的。

上面的过程都有一个潜在的前提:被管理者的数据是已经存在的

那么现在有一个问题,这些数据是如何产生的呢?

校长管理学生,难道说,把所有学生的各种数据胡乱打包在一起?不做特殊处理吗?那管理的效率是不是太低了?而事实上,管理肯定是需要对数据进行特殊处理的;虽然学生有很多,但它们有一个共同的特征:他们都是学生啊,因此他们一定存在着诸多的共同点,那么我们可以用统一的视角去看待他们,例如以结构体的手段描述每个学生,以达到用统一的视角去看待这些"数据"的目的

struct Student
{
  char name[20];
  char sex[10];
  char addr[20];
  char tele[20];
  int age;
  int score[6];
};

操作系统也是这样,例如操作系统要管理进程,管理文件等等,都需要对它们进行先描述。把这些需要被管理的对象描述之后。我们就可以以统一的视角去看待它们。然而单单描述是不够的,如果我们把这些学生描述后,不经过特定的组织形式组织起来,那么大量的数据就是一种混乱状态,管理者无法有效及时的对数据做出相应的决策。此时我们就需要利用特定的数据结构将这些数据进行组织起来,例如顺序表、链表等等数据结构。

struct Student[1000];   // 结构体数组

struct Node            //链表
{
  struct Node* next;
  struct Student data;
};

经过上面的分析:我们得出一个结论:管理者要管理好被管理的对象,那么首先要进行描述被管理的对象。其次,我们还需要利用特定的数据结构,将这些被管理的对象组织起来。即先描述再组织

管理是一种手段,通过管理这种手段,我们就可以达到一种目的:对被管理者的管理工作转变为了对特定数据结构的增删改查

而我们也知道,Linux内核是用C语言写的,那么再C语言中,如何描述一个被管理的对象的呢? 答案是:struct;虽然目前我们没有看过任何的OS内核代码,但我们能够提前预测到OS内部一定存在着大量的数据结构和算法。因此我们说数据结构是操作系统理解的核心技术。

2.4. 如何理解管理理念

经过我们上面对管理的分析,我们应该有一个大概的认识,即如果要管理好一个对象,那么就是先描述在组织。可是光有这样的理解还是不太充分,因此我们要谈谈管理理念。

那什么叫做理念呢?

理念:本质是一种指导思想,是对所有具体事务的共同规律的抽象

在以前学习C的过程中,我相信各位和我一样,写过管理系统,例如:学生管理系统、通讯录等等。而无论是什么管理系统,它们都有一个共同特征,那这个共同特征是什么呢:即一个管理系统首先要写一个struct 结构体把要管理的对象先描述起来;

例如:通讯录,通讯录的本质是对人的数据进行管理;我们首先会写一个struct 结构体描述一个人, 而之所以用struct结构体,那是因为方便我们用统一的视角去描述人的各种数据,以便于收集它们的数据。此时我们就可以将所有对象利用特定的数据结构组织起来。此时我们就可以达到一个目的:将对象的管理工作转变成了对特定数据结构的增删查改。而这套模式我们就称之为管理理念。

例如:在C++中,我们学习过STL,我们知道STL为我们提供了一系列的容器(数据结构和算法),而这样就可以达到一种目的:即描述对象的业务教给了我们,组织对象的业务交给了STL。而这也就是我们学习STL的原因之一,因为它可以帮助我们提高管理业务的效率。

此时,只要理解了这套理念,无论以后我们遇到什么项目,什么问题。无外乎就是被管理对象是谁,如何描述被管理对象;再利用特定数据结构将这些对象组织起来。而这我们就称之为管理理念,有了这份理念,此时我们对于问题的分析和解决就具备了一个充分的指导思想。

2.5. 如何理解系统调用接口

在现实世界中,各位都会经历到银行办理各种业务。我们思考一下,银行是如何为我们提供各种服务的呢?

假如我现在要去取钱,难道说,我直接跑到银行的钱库去取吗?这显然是不现实的。因为银行的管理人员不信任我。你别给我说,什么你这个人是一个非常正直善良的人,不会做非法行为。然而银行是没有时间也没有精力去调查你的。银行的体系就是认为:所有人都不值得信任。那么它是如何为我提供各种服务的呢?它是以提供各种柜台的方式为我们提供各种服务的。正因为银行不信任任何人,因此它会以柜台得形式为我们提供服务。不然那门口的保安是拦谁的?那柜台的防弹玻璃又是防谁的?哪又有人要说,既然你银行不信任我,那你干脆弄个铁门把银行焊死。这不就安全了吗?虽然你此时是安全了,但是你银行要为客户提供服务啊。你焊死了,咋提供服务,因此银行采用了柜台的方式,即把一切风险降到最低的同时为客户提供各种稳定的业务服务

我们将银行的这套体系类比到操作系统。

操作系统里面有各种核心数据(数据结构和算法),例如:进程管理、内存管理、驱动管理、文件系统等等,操作系统不会让我们直接访问这些数据,因为它也不信任我。操作系统也假设所有人都不值得信任。但同样的,OS也需要我们提供各种功能或者业务。那么既然不能直接访问,那么操作系统如何提供这些功能的呢?答案是:OS为我们提供各种功能是以接口的形式提供的,这个接口我们称之为系统调用接口(System Call)

而我们也知道,Linux 操作系统是由C语言编写的。这里的系统调用接口本质上就是用C语言实现的各种函数

例如银行,我们知道银行为客户提供服务的方式:通过柜台的形式。然而即便有银行柜台,其操作流程还是很繁琐的(例如:填写各种信息、各种业务单子、还要跟柜台的工作人员进行各种交互,以及其他的一些操作),尤其是一些年纪很大的老年人,其交互成本是很高的。

因此,银行为用户提供了新的服务:例如在柜台之外的一些工作人员,这些工作人员是为了帮助那些不能很好和银行柜台工作人员进行交互的。 有了这些工作人员,就可以让那些不善于和柜台工作人员交互的用户也可以处理业务。而在银行柜台之外的这一层我们称之为服务层。之所以有这一层服务层,是因为某些用户不善于和柜台这一层进行各种交互。

将银行体系类比到操作系统上

银行柜台就好比是OS的系统调用接口;

然而系统调用的使用成本是很高的,类比到银行体系,有些用户不善于和柜台这一层进行交互,因此有些厉害的人在OS这一层之上(即系统调用接口这一层)有人设计出了shell外壳、图形化界面;也有人对各种系统调用接口进行了各种封装,形成了各种库:C库、C++库、boost库等等,还有我们的各种语言、各种组件。此时,经过上面的各种对系统调用接口的封装,我们达到了一个目的:可以使用OS的各种功能的同时降低了使用成本。

而我们现在所处的阶段就类似于现实世界中的那些老年人,我们不善于和银行柜台直接打交道,而需要银行柜台之外的一层 --- 服务层帮助我们和银行柜台进行各种交互。也就是说,我们现在不善于直接访问系统调用接口,因此只能借助语言、库的形式访问OS系统。而要访问操作系统,那么这些语言、库必然要对OS的系统调用接口进行封装。

而我们的目标:成为直接访问银行柜台的人;即可以直接通过系统调用接口访问OS。

3.进程概念

其实,当我们在Windows下启动一个软件,本质其实就是创建了一个进程;而在Linux下,运行一条命令或者一个可执行程序,运行的时候,本质上也是在系统层面创建了一个进程。

因此,当一个可执行程序被加载到内存的时候,此时我们就不能再称之为程序了,而应该称之为一个进程。

3.1. 为什么要有PCB

Linux是可以同时加载多个进程的,那么是不是意味着在Linux环境下,内存是可能同时存在大量的进程的。而既然有这么多的进程,那么请问,Linux操作系统需不需要管理这些进程呢?

答案是:当然且必须要管理!

那么如何管理这些进程呢?

答案是:先描述在组织

先描述:先将进程的各种属性用struct 结构体描述起来。

在组织:通过特定的数据结构将这些结构体对象组织起来。

此时操作系统对进程这些对象的管理工作转变为了对特定数据结构的增删改查。

具体来说,OS为了管理每一个进程,OS会为每一个进程创建一个PCB(PCB是一个结构体,通过PCB描述每一个进程),其中包含了进程的所有属性,在Linux下具体为struct task_struct;再通过特定的数据结构组织起来,例如:

struct task_struct
{    
    // 先描述:
    // 包含了进程的所有属性信息

    // 在组织
    // 以双向链表的形式进行组织起来
    struct task_struct* _prev;
    struct task_struct* _next;
};

在这里有一个结论:

有了进程控制块PCB,所有的进程管理任务与进程对应的可执行程序毫无关系 !!!与进程的(由内核(OS)创建的)PCB强相关!!!

3.2. 可执行程序和进程的区别 

可执行程序本质上是一个在磁盘上的二进制文件,可执行程序 = 代码 + 数据;

那么进程是什么呢?

有的教科书上说的是,将一个可执行程序加载到内存就是一个进程。但这种说法是不准确的。一个可执行程序要变成一个进程,那么首先要将可执行程序的代码和数据加载到内存中,可是单单这样就够了吗?答案是:不够;因为OS需要管理进程,那么必然要有描述一个进程的结构体,这个结构体(PCB)包含了进程的所有属性信息(例如:这个进程的ID、进程的代码和数据所在的位置、进程状态等等);因此当一个可执行程序变成进程的时候,不仅会将代码和数据Load到内存中,与此同时,OS会为该进程创建与之对应的PCB结构体等数据结构。

因此在这里我们有一个简单的定义:进程 = 可执行程序的代码 + 数据 + 与进程强相关的数据结构(PCB等等);

3.3. 什么是PCB

PCB(Process Control Block)是一个struct 结构体,这个结构体包含着进程的所有属性信息。

而在Linux操作系统下,PCB具体为task_struct,task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的属性信息。

3.4. task_struct的分类

  1. 标示符:描述该进程的唯一标示符,用来区别其他进程,就是一个有符号整数。
  2. 状态:任务状态,退出代码,退出信号等。
  3. 优先级:相对于其他进程的优先级。
  4. 程序计数器:程序中即将被执行的下一条指令的地址。
  5. 内存指针:包括可执行程序的代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  6. 上下文数据:进程被调度时,CPU内部的寄存器中的临时数据 。
  7. I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  8. 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  9. 其他信息。

3.4.1. 上下文数据的理解

如何理解上下文的数据呢?

我们知道,当进程位于run_queue时,此时就可以被CPU调度,而CPU里面是会存在大量的寄存器的!

如果某一个进程正在被CPU调度的时候,那么CPU内的寄存器的里面,一定保存了该进程的临时数据!

时间片

OS规定每个进程单次运行时间的最大值称之为时间片;

对于OS来说,难道进程的执行策略:是一个进程执行完了才会去执行下一个进程吗?答案是:绝对不可能;为什么,因为OS如果这样做了,那么OS就非常容易出问题,例如,我写一个死循环的程序,当这个程序运行起来的时候,那么该进程永远都不会结束,那么其他进程就无法得以推进了!

也就是说一个进程的代码可能不是很短时间内就会执行完的!因此OS规定了一个进程单次运行时间的最大值,也就是时间片;一个进程在被调度的时候,不管你有没有执行完代码!一旦你到达了时间片,那么对不起,OS就不会让你继续跑了,它会让下一个进程跑,你这个进程就会停止运行,直到下一次时间片到来,你才能继续运行; 因此,在单CPU的情况下,用户感受到的多个进程"同时在运行",本质是通过CPU的快速切换完成的。

实际上,对于单CPU来说,在某一个时刻,只有一个进程在被调度;在一段时间内,通过CPU的快速切换,使得多个进程得以推进,这种现象我们称之为并发!

OK,你说了CPU会快速切换被调度的进程,我接受,但是你之前不是告诉我,CPU内部的寄存器保存的是被调度的进程的临时数据吗?CPU的快速切换,会不会导致这些临时数据丢失呢?

首先,我们说答案:这些临时数据就不应该被丢弃或者覆盖,也不会被丢弃或者覆盖!

这些临时数据的确保存在CPU内部的寄存器中,并且CPU只有一套寄存器,那么OS如何处理的呢?

虽然寄存器硬件只有一套,但是寄存器中的临时数据是你这个进程的啊!当发生CPU调度进程完的时候,被切换的进程会将对应的临时数据保存到自己的PCB里(保护),这个过程我们称之为为保护上下文

保存的目的就是为了将来便于恢复!

下一次继续被CPU调度的时候,首先会把曾经运行的临时数据加载到CPU,继续之前的状态继续被CPU调度(就如同没有被中断过!),以达到多个进程同时被CPU调度且不丢失各自运行数据的目的(恢复),这个过程我们称之为恢复上下文

进程被调度的时候 ,CPU内部寄存器中与进程强相关的临时数据,就叫做上下文数据。有了对上下文数据的理解,我们可以感受到进程是被切换的!

总结:上下文数据的理解
  1. 进程被创建出来是为了让CPU去执行它对应的代码的;

  2. CPU很少,而进程很多,我们必须以某种算法保证谁先谁后,也需要OS提供一个负责进程调度的调度管理模块;

  3. 当CPU在调度的时候,不是说把一个进程的代码跑完,才跑下一个进程,而是说给每一个进程基本的执行时间片;时间片一旦到达,即便你这个进程没有跑完,也必须马上让出CPU资源,调度下一个进程;

  4. 进程在实际执行的时候,一旦在执行过程中,进程被CPU剥离,一定会导致进程产生大量的中间临时数据需要被保存;

  5. CPU本身内部的寄存器保存的就是进程被调度产生的临时数据(不是所有数据,是核心数据),而这些数据,我们就称之为上下文数据。

3.5. 查看进程

#include <stdio.h>
#include <unistd.h>

int main()
{
  while(1)
  {
    printf("cowsay hello\n");
    sleep(1);
  }
  return 0;
}

我们所看到的现象是,当运行一个可执行程序的时候,此时就会变成一个进程。其内核操作时:将可执行程序的代码 + 数据 Load到内存中,同时OS会为该进程创建一个PCB用于描述该进程。

那么如何查看一个进程呢?在这里用三种方式查看进程:

方式一: 利用ps命令查看进程

ps   // 会显示当前终端下的进程:

bash就是我们的外壳程序,即命令行解释器,它也是一个进程;ps是一条命令,同样的它也是一个进程。 

  1. -a 显示所有用户的进程(包括其他用户的进程)
  2. -j 使用更详细的格式显示进程信息,包括进程的进程 ID(PID)、 父进程 ID(PPID)、进程组 ID(PGID)、会话 ID(SID)、控制终端、状态等
  3. -x 显示没有控制终端的进程 
//综合起来,"ps ajx" 命令将以详细的格式显示所有用户的进程信息,包括没有控制终端的进程
ps ajx // 显示所有进程   

// 将所有进程信息通过管道传输给grep进程,并过滤出 my_test
ps ajx | grep "my_test"  

但我们发现,grep也是一个进程,因此我们也可以将grep过滤掉,即:

ps ajx | grep "my_test" | grep -v grep  //过滤出my_test的同时过滤掉grep

有时候,我们也需要ps ajx命令的头部信息,例如:

// 显示ps ajx命令的头部信息 && 过滤出my_test的同时过滤掉grep
// && 的意思就类似于逻辑与, 当左边这条命令正常运行后,才会去运行右边的命令
ps ajx | head -1 && ps ajx | grep "my_test" | grep -v grep

方式二:利用top命令

top 命令: 显式系统中所有进程,类似于Windows下的任务管理器

q:退出top

方式三:通过/proc这个系统目录查看

 /proc,即位于根目录下的一个系统目录,这个内存级别的文件目录可以将系统中所有进程以文件系统展现出来。

proc就是process的简写,即可以用文件的形式查看进程。

ls /proc  // 查看这个系统目录

可以看到这个目录下里面大多数都是一个目录,而且这些目录大部分都很有特点,它们的名字是一个整数,那么这个数字代表着什么呢?其实,这个数字就代表某一个进程的PID,PID是一个进程的唯一标识符。例如我要获取10127号进程的信息,那么可以这样查看:

ll /proc/10127  // 查看10127号进程的信息

在这里我们说一下,cwd是什么?cwd全称为current work directory,即当前进程的工作目录。而在我们以前学习过一个C函数,即fopen,原型如下:

FILE* fopen(const char* filename,const char* mode)

以前我们是如何调用这个函数的呢?如下:

FILE* fp = fopen("log.txt","w");

我们以前的说法是,这个函数会默认在当前路径下打开这个log.txt文件;而我们现在应该理解了,打开一个文件,那么是谁打开呢?当然是进程打开一个文件。既然是一个进程,那么它必然有CWD(current work directory),即当前进程的工作目录。因此,当调用fopen的时候,如果没有显示声明文件路径,那么默认在当前进程的工作目录下创建文件。

3.6. 通过系统调用获取进程标识符

刚刚我们说了,一个进程的标识符是PID,那么如何获取这个标识符呢?OS为我们提供了一个接口:getpid,以及getppid;

getpid接口可以获取当前进程的标识符PID

getppid接口可以获取当前进程的父进程的标识符PID

// man 2 getpid   --- man的二号手册
#include <sys/types.h>
#include <unistd.h>

// pid_t 是由OS提供的一个有符号的整数
pid_t getpid(void);  // 返回当前进程的PID
pid_t getppid(void); // 返回当前进程的父进程的PID

可以看到,这两个函数与我们之前所包含的头文件完全不一样,并且这两个函数在man的二号手册,而我们之前说过,二号手册是查找系统调用的,而这两个函数就是两个系统调用。可以这样说,这两个函数就是我们最先学到的两个系统调用了。那么如何使用呢?

void Test2(void)
{
  while(1)
  {
    printf("i am a process,PID: %d,PPID:%d\n",getpid(),getppid());
    sleep(1);
  }
}

这两个函数非常简单,getpid返回当前进程的PID,getppid返回当前进程的PPID(即当前进程的父进程的PID)。根据上面的现象,我们也看到了,的确是两个进程的PID,并且这两个进程的关系是父子关系吗?是的!那么这个PID好理解,就是我这个可执行程序变成进程的PID,那么这个PPID即父进程是谁呢?

根据上面的现象,我想说的就是:在命令行上启动的命令、可执行程序其父进程都是bash。而我们之前说过,shell是命令行解释器,在Linux具体为bash。

补充:pid_t这个类型究竟是什么呢?

前面说了,这个pid_t是一个有符号的整形,但它到底是一个什么类型呢?如果有兴趣,可以往下看:

首先这个pid_t类型是在/usr/include/目录下的sys/types.h头文件中,其中有这样的定义

grep -ER "pid_t" /usr/include/sys/types.h

具体如下:

可以看到,pid_t是__pid_t这个类型的重命名,那么这个__pid_t有是什么类型呢? __pid_t这个类型是在/usr/include/bits/这个目录下的types.h文件中:

grep -ER "__pid_t" /usr/include/bits/types.h

具体如下:

可以看到, __STD_TYPE就是typedef,而__pid_t就是__PID_T_TYPE的重命名;那么这个__PID_T_TYPE有是什么类型呢?__PID_T_TYPE该类型在/usr/include/bits目录下的typesizes.h这个头文件中,具体如下:

grep -ER "__PID_T_TYPE" /usr/include/bits/typesizes.h

可以看到,__PID_T_TYPE又被定义为__S32_TYPE这个符号,那么__32_TYPE到底是什么类型呢?__32_TYPE在/usr/include/bits/这个目录下的types.h这个头文件中,具体如下:

grep -ER "__S32_TYPE" /usr/include/bits/types.h 

可以看到,pid_t本质上就是一个int(针对我的机器,在这里仅供参考);

补充:简单介绍一下kill命令

kill命令 可以向目标进程发送信号,具体信号如下:

在这里就简单说一下9号信号,kill -9 可以给一个进程发送9号信 号,杀掉该进程。例如:

3.7. fork()系统调用

1. fork()的理解

#include <unistd.h>
// fork --- create a child process
pid_t fork(void)

return val:
on failure: -1 is returned in the parent process,  no child process is created, and errno is set appropriately;
on success: the PID of the child process is returned in the parent process,and 0 is returned in the child process.

通过上面的描述,即失败,给调用fork()函数的进程返回-1;成功,给父进程返回子进程的PID,给子进程返回0;

接下来,我们将用代码分析分析fork():

void Test3(void)
{
  printf("i am a process\n");
  
  printf("cowsay hello\n");
}

这个Test3函数调用的结果,那么自然就是两条打印语句。

符合我们的预期。

void Test3(void)
{
  printf("i am a process\n");
  fork();
  printf("cowsay hello\n");
}

可以看到,我此时只不过在上面的基础之上增加了一个fork函数调用,那么这会带来什么差异呢?

上面的结果令人非常诧异!什么情况?我明明只调用了两条printf,但此时却给我打印了三条信息!并且我们发现,fork()函数之前的打印只打印了一次,fork()后的打印却打印了两次!这就是fork()的作用了,fork()之后,会创建子进程,此时的子进程和父进程会共享这段代码,并且子进程也会从fork之后执行代码,也就是说,当fork()之后,此时就从一个执行流变成了两个执行流,那么自然也就打印了两条语句。

经过上面的分析,我承认你说的是对的,fork之后会创建一个子进程;但是你还说过,fork()是有返回值的,on success,给父进程返回子进程的PID,给子进程返回0;那我就想看看是否如此呢?

void Test4(void)
{
  printf("i am a process: %d\n",getpid());

  pid_t id = fork();

  printf("id = %d\n",id);
}

代码很简单,我们看看结果:

各位,在我们以前学习C/C++的时候,从来没有遇到过一个函数返回一个整形时会有两个返回值!但是我们现在真真实实地见到了,fork()的确有两个返回值,当fork()成功之后,给父进程返回子进程的PID,给子进程返回0;上面的结果也符合预期;并且我们也发现,每次fork()后,创建的子进程也不相同,甚至父进程自身每次也不一致!

2. fork()的一般使用

各位,fork()是帮助我们创建一个子进程,但如果我们让这个子进程与对应的父进程执行相同的代码其实是没有太大意义的,我们的愿景是:fork()成功之后,形成两个执行流(具有父子关系),并让这两个执行流执行不同的业务逻辑,这样才是有意义的!OK,你这样说,我可以接受,但是现在又有一个问题,如何区分这两个执行流呢?那么自然是用fork()成功后的返回值了,给子进程返回0,给父进程返回子进程的PID,那么我们完全可以根据返回值让父子进程进行的不同业务逻辑! 例如:

void Test1(void)
{

  pid_t id = fork();

  if(id < 0) 
  {
    // fork failure
    perror("fork");
    exit(1);
  }

  else if(id == 0)
  {
    //id == 0 ---> child process
    while(1)
    {
      printf("i am a child process,PID: %d,PPID: %d\n",getpid(),getppid());
      sleep(1);
    }
  }
  else
  {
    // id > 0 ---> parent process
    while(1)
    {
      printf("i am a parent process,PID: %d,PPID: %d\n",getpid(),getppid());
      sleep(1);
    }
  }

}

在这里上说一下,各位,在我们以前学习C或C++的过程中,有没有遇到代码即执行if代码块又执行else的代码块呢? 答案是:没有,但是现在,我们却遇到了;我们也说过,fork()成功后,会创建一个子进程,导致从一个执行流变为两个执行流,并且这两个父子进程共享代码,此时根据不同的返回值让父子进程执行各自的业务逻辑!而上面也应证了我们的预期。当然,为了更好地观察现象,我们也可以用一个命令行监控脚本,如下:

while :; do ps ajx | head -1 && ps ajx | grep "my_test" | grep -v grep; sleep 1; echo "#########################"; done

 总结,一般情况下,我们是根据fork()成功后的两个返回值让父子进程执行不同的业务逻辑。

3. fork()的两个返回值为什么是这样

经过上面对fork函数文档的阅读,以及我们自己观察到的现象,我现在可以接受fork是有两个返回值的,并且给父进程返回子进程的PID,给子进程返回0;但是我就是想知道为什么这样呢??我们也知道,fork()是一个系统调用,是由Linux操作系统内部实现的,但是我就是想知道操作系统为什么这样做?

对于上面这个问题,我们可以感性的认识这个问题,就在现实生活中,一个父亲是有可能存在多个孩子的,而一个孩子他的父亲是唯一的!那么对于父亲来说,如果他有很多小孩,他是不是应该给每个孩子起个名字,以作区分,来标识这个孩子!而对于某一个孩子来说,他的父亲一定是唯一的!因此没有必要在给父亲起一个特殊的名字,以作标识!

同理:父进程可以有很多个子进程,而对于一个子进程来说,它的父进程是唯一的!那么对于父进程来说,由于它可能是存在多个子进程的,因此父进程是需要一个标识符来标识它的某一个孩子的,因此父进程的返回值就是子进程的PID,而对于子进程来说,它的父进程是唯一的,因此并不需要在对父进程做特殊标识,因此子进程的返回值是0

4. fork()为什么会有两个返回值呢

这个问题该如何理解呢?就是,你说fork()的时候,自始至终你都是再告诉我它成功会有两个返回值,给父进程返回子进程的PID,给子进程返回0;可是,为什么呢?为什么fork()成功会返回两个返回值呢?

我们知道,Linux操作系统是由C写的,而fork是由Linux提供的系统调用接口,那么也就是说fork()也是由C实现的?并且这个特殊的C函数存在两个返回值。而要理解这个问题,我们首先要搞懂下面的问题:

就是对于一个C函数来说,如果走到了return 这条语句,它核心的代码逻辑是否执行完了呢?

pid_t fork(void)
{
    // 核心代码逻辑 --- 创建子进程的代码逻辑

    return val; // 这里是方便举例
}

答案是:如果一个函数走到了return,那么也就意味着这个函数的核心代码逻辑走完了

而我们知道,fork()函数的功能是:创建一个子进程!虽然我们不知道fork底层实现具体是什么,但有一点我们很清楚,那就是在return之前,fork()就已经完成了子进程的创建!既然是创建子进程,那么也就意味着系统多了一个进程,也就意味着多了一个执行流,即此时存在着两个执行流!那么这两个执行流分别调用return,也就是说return会被执行两次,那么自然会产生两个返回值。

5. fork()后父子进程的执行顺序

通过我们上面的分析,我们现在已经知道了fork后会有两个进程,即有两个执行流,但现在我想知道这两个进程谁先运行谁后运行呢?

答案是:fork()后父子进程谁先谁后运行是不确定的,这个是由OS的调度器所决定的。

3.8. 操作系统进程的状态

一般的操作系统对于进程的状态有这些专有名词:新建、运行、阻塞、挂起、挂起阻塞等等, 那么如何理解这些状态呢?

我们之前说过,OS为了管理进程,会为每一个进程都会创建一个task_struct结构体用于描述一个进程的属性信息,这样便达到了一个目的:OS对进程的管理转变成了对特定数据结构的增删查改!而当CPU要去执行一个进程的时候,会存在一个运行队列(run_queue)和等待/阻塞队列(wait_queue),那么如何理解呢?

3.8.1. 新建:

按字面意思理解,就是新建一个进程,包含相关的数据结构 + 代码和数据。

3.8.2. 运行

什么叫做运行态呢? task_struct结构体在运行队列(run_queue)中排队,就叫做运行状态!此状态不一定正在被CPU调度,但是此状态随时可以被CPU调度!运行队列是进程正在等待CPU资源就绪

3.8.3. 阻塞

首先我们要有这样的认知,系统中是存在各种资源的!并不是只有CPU资源,还有可能有网卡、磁盘、显卡等其他资源,也就是说,一个进程有可能等待CPU资源就绪,也有可能等待其他资源就绪,而在OS中,也不仅存在着run_queue,还存在着wait_queue(等待/阻塞队列);而我们将task_struct结构体在阻塞队列(wait_queue)中排队,称之为阻塞状态;阻塞队列是进程正在等待非CPU资源!

3.8.4. 挂起

首先,并不是所有的状态都需要维护队列,例如,挂起状态就不需要;

在操作系统中,有一个swap分区,当内存块不足的时候,将长时间不执行的进程代码和数据从内存换出到磁盘,此时内存只有这个PCB结构体(与之对应的代码和数据被换出到磁盘里了 )在内存里,当内存不足的时候,OS通过适当的置换进程的代码和数据到磁盘,此时的进程的状态就叫做挂起状态;

从挂起状态 ----> 运行状态 ,并不是仅仅把PCB里面的状态由挂起 ---> 运行,还要把可执行程序的代码和数据重新从磁盘加载到内存!

注意:挂起和阻塞都和CPU资源没有关系!

3.8.5. 挂起阻塞

当某一个进程在等待非CPU资源的同时,内存也不够了,那么此时会将这个进程的代码和数据从内存换出到磁盘上,那么此时这个进程的状态就是挂起阻塞!

注意:挂起的成本是很高的,因为它要将代码和数据从内存写入磁盘,如果此时又要将该进程从挂起 ---> 运行,那么又要将代码和数据从磁盘Load到内存中,实际上这就是一个IO过程,其成本是很高的。

3.9. Linux操作系统具体的进程状态

下面的状态在kernel源代码里定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"X (dead)", /* 16 */
"Z (zombie)", /* 32 */
};

3.9.1. R状态:       

R状态就是运行态,R状态的进程可能正在被运行,也有可能正在运行队列中进行等待,随时可以被CPU调度! 演示如下:

void Test1(void)
{
  while(1)
  {

  }
}

 利用一个死循环演示R状态,通过命令行监控脚本可以看到结果就是一个R状态。

3.9.2. S状态:   

S状态称之为可中断睡眠(interruptible sleep)又称之为浅度睡眠;对应的就是阻塞状态;S状态的进程处于等待队列中!该进程正在等待某种非CPU资源;演示如下:

演示一: 
void Test2(void)
{
  while(1)
  {
    printf("haha\n");
  }
}

令人奇怪的是,我们的代码明明是一个死循环打印,按道理说,难道不应该是一个R状态吗?可是我们通过监控脚本看到此时进程的状态是S,为什么呢?
首先S状态对应的就是阻塞状态!那么既然是阻塞状态,那么该进程一定是在等待某非CPU资源就绪;而我们知道,printf本质是:默认是内存向显示器文件写入数据,即是一个output的过程。而我们知道CPU处理数据的速度与外设处理数据的数据差距非常大!后者较前者相比非常非常慢!也就是说,这个进程在运行的时候,极大多数的时间都在等待外设资源就绪,只有极少的时间是CPU在处理数据!故我们看到的大部分情况都是S状态,其根本原因就是因为外设太慢了,而CPU太快了!

演示二:
void Test3(void)
{
  int num = 0;
  scanf("%d",&num);
  while(1)
  {

  }

可以看到,当这个进程在运行的时候,由于我调用了scanf这个函数,那么这个进程就会等待用户从键盘输入数据,也就是等待一个非CPU资源的进程,那么就是一个阻塞状态,通过监控脚本,我们也可以看到此时就是一个S状态,即阻塞状态!

3.9.3. D状态:   

D状态也是一种睡眠状态,不过更确切的说法它是一种深度睡眠,也称之为磁盘睡眠,这个状态,不可以被中断,也不可以被被动唤醒!OS也无法杀掉这种状态的进程!这种进程只有两种方式可以解决:第一种方式:该进程自动苏醒;第二种方式:计算机重启;

如何理解不可被中断

要理解这个问题,我们只能感性的认识:

现在有一个进程A,它的工作:向磁盘写入大量的数据,而我们知道,CPU处理数据的效率远大于磁盘处理数据的效率,因此该进程在大部分时间都会等待磁盘资源就绪,既然是等待磁盘资源就绪,那么就是等待一种非CPU资源,也就是说该进程此时处于阻塞状态;然而我们知道OS是会对进程做管理的,假设当前OS的内存空间严重不足的时候,OS会通过一定的手段,杀掉一些进程,来起到节省空间的目的;

那么场景就是如下:

进程A对磁盘说:磁盘啊,我需要你帮我保存一批数据!

磁盘说:当然没问题!那你把数据给我吧;

此时进程A就陆续的将数据写入磁盘,但由于磁盘的速度太慢,因此进程A大部分时间都处于一种阻塞状态;

就在此时,OS这个管理者就路过进程A,由于此时OS的内存空间压力非常大,什么挂起,什么换出,都试了个遍,内存空间压力还是很大,而它正好看到进程A还在休眠,气不打一处来,说我都忙成啥了,你还在这里休眠,OS就立马把进程A杀掉了!

然而此时问题就来了:

这个进程A将数据交给磁盘使其保存起来,磁盘是不是应该告诉这个进程,你交给我的任务,我完成得怎么样了,是写入成功了?还是写入失败了?如果失败了,那么原因是什么呢?是不是应该返回一个错误码,表明错误原因呢?

可是,正当磁盘想将写入的结果告诉该进程A时,它发现进程A已经没见了,只剩它一人在风中凌乱,可是此时另一个进程又想向磁盘写入数据,那么此时磁盘只好将写入结果丢弃,那么带来的问题就是:此时用户不知道写入的结果啊,写入成功还好,那写入失败了呢?如果写入失败了,那么用户不知道,是不是就导致了结果误判。

那么请问:此时这个问题是谁来背锅呢?

是磁盘?是进程A?还是OS呢?

磁盘说:我冤枉啊,我就是按照进程A的需求完成的啊,你让我写我就去写,最后我想把写入结果告诉你,可是你不见了啊,这咋能怪我呢?

那难道是进程A的问题?

进程A:我也是醉了,这咋能怪我呢,我是受害者啊,我正在那里休眠的好好的,突然一个彪形大汉来把我干了,我都不知道发生了啥,这也能怪我?冤枉啊!

那难道是OS的问题?

OS无辜地说:我的设计者给予我的权力就是:当操作系统内存空间不足的时候,可以杀掉一些进程,以达到起到节省空间的作用,不然如果我自己都出问题了,那么整个系统都会出问题!

OK,听完它们各自的描述,你会发现它们说的都有道理,那么问题出在哪里呢?问题的根本就是处于这种状态的进程(向磁盘进行读写的进程),不可以被杀掉OS系统都没有权力杀掉这种进程,这就是D状态的进程,处于D状态的进程不可被杀掉。

对于D状态的进程只有两种方式可以解决:第一种方式:得到读写结果,进程自动苏醒;第二种方式:计算机重启;

我们也可以理解,如果一个OS中存在着大量的D状态的进程,是一种挺危险的状态,因为此时OS都无法处理这种进程。如果内存压力过大,是存在着宕机的风险的。

3.9.4. T状态:   

彻底的暂停状态,此时这个进程不会有任何的数据更新;

 可以通过发送 SIGSTOP 信号给目标进程,使其成为暂停状态

这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。 

不知知道各位注意到没有,当我们给一个进程发送信号的时候,原先进程的状态是带一个+的,例如S+、R+等等,但是经过发送信号,此时进程的状态变为了S、R等等;那这两个有什么区别呢?

前者我们称之为前台进程,后者我们称之为后台进程;

前台进程运行起来时,会影响我们和bash进行交互,运行的同时,不能进行指令操作;

后台进程运行起来时,不影响我们和bash交互,可以继续进行指令操作

前台进程退出:直接Ctrl + c结束即可

后台进程退出:需要先fg,将后台进程转换为前台进程,然后Ctrl + c;或者直接给后台进程发送9号信号,直接干掉该进程。

3.9.5. t状态:   

T和t统称为一种暂停状态,只不过t是在调试的时候表现出来的,例如:

 以前我们所说的,给一个进程打一个断点,会让这个进程停止下来,现在我们就知道了,本质是该进程暂停罢了,该进程处于一种t状态;

3.9.6. X状态:

X状态 : 标识该进程是一个终止进程 , 瞬时性非常强(一般不好观察到),意味着当前进程的资源可以被OS回收。

3.9.7. Z状态:

Z状态称之为僵尸状态; 相信很多人听到这个状态的名字,感觉很奇怪,为什么OS需要定义一种这样的状态呢? 接下来我们就来分析分析僵尸状态:

僵尸状态是什么?

当一个进程已经退出,但是此时还不能回收这个进程的资源,而处于一个检测的状态,我们将这个状态称之为僵尸状态。

僵尸状态为什么?

很多人不理解,为什么一个进程都退出了,还不允许回收其资源,OS究竟为什么这么设计呢?

要理解这个问题,我们需要借助现实的例子帮助理解:

情景:

李四是一个热爱跑步的程序员,一天早上,太阳还没起来,你先起来了,在小区的路上挥洒汗水,正在此时,你的旁边呼啸而过一个名叫赵三的程序员,赵三跑得非常快,一溜烟的功夫,就跑了三四十米远,突然,你听见一声怪响,定睛一看,原来是赵三躺在地上了,经过乐于助人思想的熏陶 ,你连忙过去查看,发现赵三的呼吸停止了,心跳也停止了;此时你非常惶恐,立马拨打了急救电话和警察的电话;

这个情景稍显怪异,但为了让我们更好的理解僵尸状态,只好这样说。

警察叔叔和医生相继到场,医生经过一番检查,发现人的确不行了,此时,难道警察会说那行,直接联系家属,让他们过来处理后事吧?答案是:绝对不可能!现实是,警察一过来就会将周围拉警戒线,封锁起来,然后让法医辨别人的死亡原因,判断究竟是意外死亡还是他杀;与此同时,会根据目击证人,监控,还原案发场景;当得到法医的判断结果、目击证人的供词、监控等有力证据才能证明这个人的死亡原因!如果是正常死亡,那么才会联系家属,处理后事;如果是其他形式的非正常死亡,那么就会继续侦测,抓捕犯罪嫌疑人!

而对于进程而言,也是一样的道理,当一个进程终止了,那么此时OS不会直接去回收它的资源,而是会检测它的退出原因,判断是正常退出,还是异常退出;

那么一般是谁去检测僵尸进程退出的原因呢?一般是父进程或者OS去检测僵尸进程退出的原因。

因此,之所以维持僵尸状态,是为了为了让父进程或者OS来进行检测进程的退出原因,检测完后将该状态由Z ---> X,进而才会去回收资源;那么换言之,如果一个进程一直处于僵尸状态,是不是就导致了内存泄漏呢?是的!至于解决方案:我们在进程---下的进程等待详说!

 僵尸进程的演示:

void Test6(void)
{
  pid_t id = fork();
  if(id == 0)
  {
    int i = 3;
    while(i--)
    {
      printf("i am a child process,PID: %d, PPID: %d\n",getpid(),getppid());
      sleep(1);
    }
  }
  else
  {
    while(1)
    {
      printf("i am a parent process,PID: %d,PPID: %d\n",getpid(),getppid());
      sleep(1);
    }
  }
}

上面逻辑的预期:子进程运行三秒,进程退出(状态由S--->Z) ;父进程一直死循环运行;

通过监视窗口查看,符合我们的预期!子进程运行三秒,进程退出,状态变为僵尸状态,此时需要等待父进程读取子进程的退出状态。

3.9.8. 孤儿进程

孤儿进程和进程的状态没关系,它只是一种特殊的进程;我们将父进程提前退出,子进程没退出,将这个子进程称之为孤儿进程,这个孤儿进程会被1号进程领养,这个1号进程也就是操作系统。演示如下:

void test7(void)
{
  pid_t id = fork();
  if(id == 0)
  {
    //child process
    while(1)
    {
      printf("i am a child process,PID: %d, PPID: %d\n",getpid(),getppid());
      sleep(1);
    }
  }
  else
  {
    //parent process
    int i = 3;
    while(i--)
    {
      printf("i am a parent process,PID: %d, PPID: %d\n",getpid(),getppid());
      sleep(1);
    }
  }
}

代码逻辑:子进程一直运行,父进程运行三次,进程退出!

预期:当父进程退出时,子进程就没父进程了,此时就需要OS去领养这个子进程,因此我们应该看到的结果就是:子进程的PPID成为1(1号进程就是OS),即被1号进程领养!

从命令行的监控脚本结果我们可以知道:当子进程运行三次时,父进程退出,此时这个子进程需要被OS进行领养,即此时这个子进程的父进程就是1号进程,也就是OS。

为什么要被领养呢?

我们知道,对于一个子进程来说,正常情况下,当子进程退出的时候,需要父进程获取子进程的退出结果,使其从Z--->X(僵尸状态 ---> 死亡状态) ,而如果没有父进程,那么子进程的退出结果谁去读取呢?因此,如果一个子进程的父进程提前退出,那么此时就需要OS去领养这个进程,以便获取它的退出结果,从而使其从Z--->X,进而OS才会去回收它的资源。因此之所以领养的主要原因: 为了获取子进程的退出结果,使其退出时,可以从Z ---> X,以便于回收其资源,避免了资源泄露!

 进程上的内容到此结束!

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值