前言:
在上篇我们已经对有关体系结构的基本知识进行了详细的介绍,接下来我们将进入网络编程的第一个大块—— 有关进程相关的知识!!!
目录
前言
【承上启下】
在上一篇博客中,我们简单的介绍了关于计算机体系结构的基本知识,那在还没有学习进程之前,我先问大家,操作系统是怎么管理进行进程管理的呢?根据上节课我们学到的,其实很简单——先把进程描述起来,再把进程组织起来!
接下来,我们就开始对进程的相关学习,对于这方面的文字知识,我在《操作系统——进程》相关的文章中已经进行了纸面上的直观解释。今天我们将结合在Linux环境,带大家一起去学习有关进程相关的知识。
首先,给大家感性的介绍一下什么叫做进程。今天,当给我们写博客得时候,我不是直接打开电脑就开始写的。我需要先打开电脑--打开浏览器--打开CSDN--最后才开始写文章的。
又例如当我们在使用Linux的时候,我们是先双击登陆xshell,在连接远程的服务器之后才开始正常工作的。再此过程中,操作系统都会为其以进程的方式来进行相应的操作。
【小结】
在以前,不管是我们写的程序还是在指令行上进行相应的指令操作,最后的工作都是由操作系统在底层帮我们把编译起来的程序转化为相应的进程之后再去进行执行的。
💨 通过以上种种,我们可以得出一个结论:我们做的任何关于启动程序并运行的行为,都是由操作系统帮助我们将程序转化为相应的进程再去完成相应的任务!!!
(一) 基本概念
1、描述进程-PCB
【分析】
- 1、假定在底层有这样的三个模块,在磁盘中存放着各种各样的文件——包括普通文件和目录等,当经过编译之后便形成了可执行程序,而可执行程序的本质就是--普通二进制文件。既然是文件,那么我们就有对其进行增删查改的权利;
- 2、因此对于磁盘文件也是属于文件,在我们之前说过文件=内容+属性。内容是属于我们写的代码和数据;属性则是文件的拥有者和所述者是谁,能不能被执行以及创建时间等;
- 3、紧接着当我们【./】加载到内存之后,就需要把程序跑起来,即意味着要我们的代码和数据此时要加载到内存中,然后cpu才能访问我们的代码和数据;
- 4、当我们只有一个进程加载到进程,CPU再去对其访问,此时情况尚可乐观,而现在当我们的磁盘有成百上千了进程需要加载到内存中时,操作系统此时就面临一个问题?“那就是如何把这些加载到内存中的进程管理起来” ,即操作系统应对加载到内存的进程做管理。
- 5、此时问题又来了,那么如何对其进行相应的管理操作呢?其实很简单,上节课我们说过管理的本质就是——先描述,在组织。为了解决上诉这个问题,操作系统会在内存中开辟一个数据结构来存放相应的进程,即此时就需要引出关于程序控制块PCB;
- 6、而对于PCB结构体,我们即可为其命名为【task/pcb struct】,里面存放了进程相关的所有属性。有了这个之后,上述的情况即可描写为—— 既然是结构体,就可以有结构体指针的概念,此时操作系统说“进程你只管来吧”,等到加载到内存之后,在内存中会以指针的方式进行链接,将来操作系统只需遍历所有进程的PCB即可。
【小结】
task_struct-PCB的一种
- 在Linux中描述进程的结构体叫做task_struct
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
💨 进程 = 内核关于进程相关的数据结构 + 当前进程的代码和数据
👉 到此,我们就解决了为什么需要进行进程管理,以及需要pcb的原因 👈
2、查看进程
1️⃣通过ps指令
接下来,我们简单的通过代码来大家查看相应的进程。
- 首先,我们需要创建一个【makefile】和一个【process.c】文件(这两个大家应该都知道什么意思)
【分析】
- 我们简单的写了一个【proces.c】文件,里面的代码即为循环打印【hello world】,每次打印之后 sleep 一下。
那么我们如何查看进程呢?接下来,我们需要用到相应的指令
1、
ps axj
【分析】
ps axj
是一个在Linux系统中常用的命令,用于查看当前运行的进程的详细信息。使用ps axj
命令会按照进程的层次结构、内核线程和作业控制等方面展示进程信息,生成一个进程树。
2、
ps axj | grep process
【分析】
ps axj | grep process` 是一个在 Linux 系统中常用的命令,用于查找并筛选特定进程的信息。它的具体含义如下:
- - `ps axj` 命令用于列出当前所有进程的信息并生成进程树。
- - `|` 管道符用于将前一个命令的输出结果作为后一个命令的输入。
- - `grep process` 命令用于在 `ps axj` 命令的输出结果中查找包含 "process" 字符串的行。
因此,`ps axj | grep process` 命令会先列出当前所有进程的详细信息,然后将包含 "process" 字符串的行筛选出来并输出。这个命令可以帮助我们快速找到正在运行的特定进程,并且只显示与该进程相关的信息。
3、
ps axj | head -1
【分析】
head -1
命令用于筛选出ps axj
命令输出的第一行信息。
因此,ps axj | head -1
命令会先列出当前所有进程的详细信息,然后只输出第一行信息。由于第一行通常包含表头信息,因此这个命令可以帮助我们快速查看进程列表的标题信息。
4、
ps axj | head -1 && ps axj | grep process
【分析】
`ps axj | head -1 && ps axj | grep process` 是一个在 Linux 系统中常用的命令组合,用于查看当前运行的进程的摘要信息和包含 "process" 字符串的进程信息。
具体含义如下:
- - `ps axj` 命令用于列出当前所有进程的信息并生成进程树。
- - `|` 管道符用于将前一个命令的输出结果作为后一个命令的输入。
- - `head -1` 命令用于筛选出 `ps axj` 命令输出的第一行信息。
- - `&&` 逻辑运算符用于将两个命令连接起来,只有前一个命令执行成功后才会执行后一个命令。
- - `grep process` 命令用于在 `ps axj` 命令的输出结果中查找包含 "process" 字符串的行。
因此,`ps axj | head -1 && ps axj | grep process` 命令会先输出当前进程的表头信息,然后再将包含 "process" 字符串的全部进程信息输出。这个命令可以帮助我们快速查看进程列表的标题信息并筛选特定进程的信息。
- 此时,我们需要分屏来看其输出显示,分屏操作如下:
5、
ps axj | head -1 && ps ajx | grep process | grep -v grep
【分析】
这是两个 Linux 命令连接在一起,使用了管道符号 `|`将它们串联起来。
第一个命令是 `ps axj | head -1`,它的作用是列出所有进程的详细信息,并将其输出的第一行传递给下一个命令。
- 具体实现上,`ps axj` 命令会显示当前系统上所有进程的详细信息,显示每个进程的父进程ID、用户ID、CPU 使用率等信息;
- `|` 管道符号表示将前一个命令的输出作为后一个命令的输入,`head -1` 表示只显示第一行输出。
第二个命令是 `ps ajx | grep process | grep -v grep`,它的作用是搜索所有正在运行的进程信息并输出包含关键字 "process" 的行,且同时过滤掉包含 "grep" 的行,防止输出结果包含当前查询的命令本身。
- 具体实现上,`ps ajx` 命令会列出所有正在运行的进程,显示每个进程的父进程ID、用户ID、CPU 使用率等信息;
- |` 管道符号表示将前一个命令的输出作为后一个命令的输入;
- `grep process`会搜索所有包含关键字 "process" 的行;
- `grep -v grep`会过滤掉包含字符串 "grep" 的行,最后输出结果给用户。
2️⃣通过 /proc
💨 除了上述那样的方式之外,还有一种利用比较“传统”的方式,进程的信息可以通过 /proc 系统文件夹查看
【分析】
`ls /proc`是一个Linux命令,用于列出`/proc`目录下的所有文件和子目录(如果有的话)
`/proc`是一个虚拟文件系统(Virtual File System),它是Linux内核提供的一种机制,用于在运行时向用户空间提供有关系统和进程的信息。在`/proc`目录中,每个进程都有一个以其进程ID(PID)命名的子目录。
`/proc`目录下的文件和子目录对于诊断和调试Linux中的系统和进程非常有用。例如:
- 使用`/proc/cpuinfo`文件查看你的CPU信息;
- 使用`/proc/meminfo`文件查看你的内存信息;
- 使用`/proc/$PID/status`文件查看特定进程的状态等等。
总之,`ls /proc`用于显示所有可用目录和文件的列表,可以让用户方便地访问系统和进程的许多有用信息。
- 如:要获取PID为1的进程信息,你需要查看 /proc/1 这个文件夹。
- 例如,当我们要获取刚才我们创建的进程信息,你需要查看 /proc/12682 这个文件夹。
- 此时,我们【pwd】一下,可以看到当前所在的路径:
- 此时,我终止相应的进程,看结果是如何的:
【小结】
以上便是关于查看进程的全部内容了
- 我们可以发现进程没创建一个进程都有相应的进程标识符PID;
- 当我们删除进程之后,相应的进程的都会被删除掉,在当我们去查看时会发现没得了,同样的 /proc目录伴随着的是动态改变的。
3、通过系统调用获取进程标示符
👇
- 进程id(PID)
- 父进程id(PPID)
1、PID
- 在上面我们已经学习了如何查看进程的PID,接下来我们要学习的是如何获取进程的PID。这就需要引入一个系统调用函数【getpid】
【代码演示】
- process.c
- 紧接着我们跑程序跑起来:
- 当我们删除掉上述进程之后,再重新去运行程序,我们会发现一个事情,那就是上述的PID和PPID 与本次运行起来的进程的 PID和PPID 是不同的。具体如下:
2、PPID
- 当我们再次运行起来,我们观察一下输出的现象:
【分析】
- 从上图我们可以发现,当我们终止进程之后再去运行,虽然PID每次是不一样的,但是父进程(PPID)始终保持不变。
接下来,我们去查看【28195】PPID
【分析】
在Linux中,Bash是最常用的shell解释器之一,是一种命令解释器程序,用于执行用户输入的命令。它是GNU操作系统自带的默认shell程序,但也可以在其他操作系统中使用,比如macOS等。
Bash是一个强大的解释器,支持命令历史记录、命令自动补全、文件名通配符、重定向、管道等常用的 shell 功能。Bash还支持变量、数组、函数、条件语句、循环语句等高级编程特性,使得开发人员可以编写相对复杂的脚本来完成各种任务。
Bash解释器的执行过程如下:
- 1. 用户通过终端输入命令;
- 2. 终端将命令传递给Bash;
- 3. Bash解析命令,并将解析后的命令分为多个单独的词法单元(token);
- 4. Bash根据命令中的重定向、管道和后台运行符等特殊符号来设置相应的标志,并执行相应的操作;
- 5. Bash将词法单元传递给相应的命令处理程序执行,如系统命令、内置命令或脚本程序等;
- 6. 执行完毕后,Bash返回一个状态码给终端。
从上图,我们则不难看出【bash】也是一个进程!!!
命令行启动的所有程序,最终都会变成进程,而该进程对应的父进程都是bash(对于如何做到的,后序再说)
接下来我们在简单的学个指令。在之前我们是通过【ctrl + c】来终止进程,那有没有其他的方式呢?其实还是有的,我们可以通过【kill】进程的方式来完成。
- 具体如下:
【小结】
- 到此,关于如何通过系统调用的方式获取进程标识符的方法便讲解完毕了;
- 上述我们已经知道了如何查看进程的PID,接下来我们要讨论的便是我们要如何获取进程的PID
4、通过系统调用创建进程-fork初识
- 运行 man fork
- 认识fork fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
1️⃣ fork函数
💨 在Linux中用于创建新进程的指令为【fork】,有别于传统的执行【./】来进行把操作,使用【fork】函数可以直接进行操作。
- 首先,还是在【man】手册中查一下:
【分析】
1、概念
- 在 Linux 操作系统中,fork() 是一个系统调用,它创建一个新进程作为当前进程的副本;
- 新进程(子进程)拥有与原始进程(父进程)相同的代码,数据和堆栈空间副本,并且从父进程中继承其他重要的属性,例如用户 ID,组 ID 和文件描述符等。
`fork()` 系统调用的语法如下:
```
#include <unistd.h>pid_t fork(void);
```
2、返回值
- 该函数返回新创建的子进程的 PID(进程标识符),如果返回值为 0,则说明该函数调用成功并且当前正在运行的进程是子进程;
- 如果返回值大于 0,则该函数调用成功并且返回当前正在运行的进程的 PID,也就是父进程的 PID;如果返回值小于 0,则该函数调用失败。
- 在调用 `fork()` 之后,父进程和子进程都会从函数返回,但是可以通过返回值来判断进程是父进程还是子进程;
- 通常,在父进程中,返回值是子进程的 PID;而在子进程中,返回值是 0。(后序解释)
3、调用
- `fork()` 调用成功后,子进程会继承父进程的内存资源,例如数据段、堆栈段和其他内存映射;
- 子进程与父进程是相互独立的,它们之间没有内部通信机制,可以使用 `exec()` 或 `system()` 系统调用来运行新程序或命令。
【代码演示】
接下来,我们通过带大家直观的感受是如何操作的。
首先,我们先改一下代码,今天教大家一个批量化注释和去注释的方法:
- 批量化注释:在命令模式下,按【ctrl + v】,此时左下角显示出相应的图示,紧接着按【j】选择要注释的段落,然后切换到大写模式下,按下【i】,之后输入【//】,最后按下【ESC】即可;
- 批量化去注释:输入法切换为小写,直接按下【u】即可。
具体操作:
- process.c
- 输出如下:
- 但是当我们在A和B之间添加一条【fork】之后,结果如何呢?
- 输出如下:
【分析】
- 此时当我们写入【fork】函数之后,打印输出了两行的B,怎么回事呢?接下来,我们试着去打印对应的【PID】和【PPID】查看一下。
- 接下来,我们试着打印B的【PID和PPID】,看结果是如何的:
- 输出显示
【分析】
- 从上图我们可以看出,B的【PPID】正是A的【PID】,因此说明了一件事,那就是我们创建出了一个子进程;
- 接着,大家认为A的【PID】打印是如何的呢?
- 接下来,我们在试着打印A的【PID和PPID】,看结果是如何的:
- 输出显示
【分析】
- 从上图,我们可以得出是【22474】打印的A;
💨 那大家知道【10788】是什么吗?我们简单的去查看一下:
- 因此,我们可以得出整个调用链为:当你运行你的程序时,你就成了【bash】的子进程,而你的程序又创建了子进程为【22475】,即输出的【22474】和【22475】为父子关系,而【22475】和【10788】则为爷孙关系,所以就相当于树状结构一样。
2️⃣fork 返回值
- 我们直接通过代码的方式来进行感受:
- 输出如下:
【分析】
1、首先给大家说一下,在操作系统中【fork】之后,父进程和子进程谁先运行是随机的,即不确定到底是谁先开始运行,取决于调度器先调度谁;
- ❓ 而根据上述输出显示,我们可以发现一个比较奇怪的事情,一个函数怎么会有两个返回值呢❓
- ❔ 其次,一个变量里面明明地址是完全一样的,但是里面的内容却不是一样的呢❔
2、此时,大家是不是心里有“十万个为什么了呀!!”大家先别着急,对于第一个问题,在接下来我会为大家解答的,这里大家先接收这个概念。
- 因此,上述的用法其实是不准确的,在 【fork 】之后通常要用 if 进行分流
- 输出如下:
【分析】
- 在上面的代码中,我们使用
fork()
函数创建了一个新进程,并打印了父进程和子进程的 PID; - 在这个例子中,父进程和子进程使用两种不同的代码路径执行,并且根据
fork()
函数的返回值来确定当前进程是哪一个进程; - 如果
fork()
返回值为 0,则说明在子进程中,否则在父进程中。
【小结】
a、fork之后,执行流会被分成两个;
b、fork之后,谁先调度由编译器决定;
c、fork之后的代码共享,通常我们通过 if 和 else if 来进行分流;
👉 d、回答上述第一个问题,一个函数有两个返回值:👈
在 Linux 中,`fork()` 是一个系统调用,用于创建一个新的进程。这个系统调用的返回值有两个,父进程中返回子进程的进程 ID,子进程中返回值为0。而“一个函数有两个返回值”的情况,可以通过函数的返回值来区分当前运行的进程是父进程还是子进程。
当在父进程中调用 `fork()` 函数时,操作系统会创建一个新的进程作为当前进程的子进程。新的子进程是原来进程的一个独立副本,它有自己的地址空间,内存、堆栈和文件描述符等资源都被复制了一份。
💨 在父子进程中,`fork()` 函数的返回值是不同的:
- 在父进程中,`fork()` 返回子进程的 PID。父进程可以通过这个值来标识新创建的子进程,并跟踪、控制它们的行为。如果返回值是 -1,则说明创建进程失败,错误代码保存在 `errno` 变量中;
- 在子进程中,`fork()` 返回0。子进程可以通过这个值来判断当前运行的进程是子进程,然后执行自己的逻辑。
因此,`fork()` 函数在父进程和子进程中都会执行一次,且每个进程都有自己不同的返回值。父子进程的逻辑分别在不同的代码路径中处理,这样就实现了“一个函数有两个返回值”的情况,同时也达到了新建一个独立进程的目的。
3️⃣ 发生写时拷贝
在 Linux 中,创建子进程时会发生写时拷贝。
1、当父进程创建子进程时,子进程会继承父进程的内存映像,也就是父进程的地址空间,但是子进程并不直接共享父进程的内存,而是创建了一个副本。
2、在这个副本中,父子进程共享同一个物理页,直到其中一个进程尝试写入共享页时,才会将该物理页复制给这个进程,这就是写时拷贝的过程。
- 下面是一个简单的示例代码,展示了如何创建子进程,并在子进程中修改共享内存:
- 输出如下
【分析】
在这个示例中,我们创建了一个整型变量 x,并使用 fork()
系统调用创建了一个子进程。在父进程中,我们对 x 变量进行了修改,并在父子进程中分别使用 printf()
打印了 x 的值。
因为父进程中修改的 x
变量是共享的,所以在打印父进程中的 x 的值时,你会发现它已经被修改了。但是,在子进程中打印 x 的值时,你会发现它没有被修改。这是因为写时拷贝机制的存在,在父子进程共享内存时,对共享内存的修改并不会立即生效。只有在相应的进程中修改了共享内存后,才会将页面复制。
(二)进程状态
1、看看Linux内核源代码怎么说
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 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 */
};
【分析】
- R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里;
- S睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep));
- D磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的 进程通常会等待IO的结束。
- T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
2、进程状态查看
接下来,我将为大家逐个的解释上述的各个运行状态,用代码给大家直观的呈现。
1、R运行状态(running)
- 首先,我们先写出这样的一个程序,大家先猜想一下这个程序跑起来之后是在运行吗?
- 首先,我们都知道这个跑起来肯定是循环打印“我在运行吗?”这句话的,但是这个程序现在所处的状态是什么呢?
- 接下来,我把代码改一下,把里面的输出注释掉后再去运行程序,看此时输出的状态是什么:
- 改完之后,程序输出如下:
【分析】
此时大家是不是觉得又有点奇怪了呀,刚开始时的循环打印难道就不是所处【R】状态了吗?输出也很明显,代码明明跑的很正常呀?
- 这里的输出打印时循环打印,即意味着要频繁的访问显示器设备,更重要的此时我使用的机器因为是买的服务器,可能背后它是处于深圳,浙江等距离我是千里之外的,此时需要往显示器上输出就一定有访问外设的行为;
- 因此,这里打印的本质其实就是向外设(要么就是网卡,或者显示器)打印消息。今天,我们不考虑网络的因素,大家经常使用的打印接口不就是往显示器上输出吗?根据体系结构,显示器是个外设,所以你是在往外设进行打印;
- 可是,当设备在执行这里的 “输出”代码时,在底层是不是就要访问外设,在频繁打印的情况下外设的状态可能不是随时处于就绪状态的,因此当前的进程就可能处在外设上进行排队等待操作而不是在内存中进行排队等待。
结论:
因此,综上我们得出上述输出案例的【S】状态属于阻塞状态的一种,是以休眠状态进行阻塞,当前的进程并没有一直在CPU的就绪队列中进行等待,而因为【print】而导致去等待某种资源了。
而注释掉输出后状态就变为【R】的原因是因为:当前代码中没有任何访问资源的代码,只有循环,因此当前代码则是一个纯计算的代码,所以它在它自己整个进程调度的周期里面只会用【CPU】资源,只要被调度就一定是【R】状态。
👉 到此,关于【R】状态就为大家讲解完毕了!!!👈
2、S睡眠状态(sleeping)
- 解释
在Linux系统中,S睡眠状态一般指系统进入Suspend-to-RAM模式,也就是将计算机的内存内容保存到RAM中,并将整个计算机系统进入低功耗状态;
这种睡眠状态可以快速唤醒系统,并且能够节省电力。因此也称为—— 可中断休眠。
- 💨 接下来,我们还是以代码的方式带大家直观的感受:
- 紧接着我们再去查询当前所处的状态是什么:
【分析】
首先,此时的进程正在【scanf】处阻塞着,它在等我们从键盘中输入数据;
因此,此时这个进程并没有被调度,因为它当前等待的资源并没有就绪,因此并没有在运行队列中等待,而是在键盘上等,一旦输入数据就可以运行了。因此,这里的状态就为【S】——即在等待键盘资源的响应。
- 可中断演示:
👉 到此,关于【S】状态就为大家讲解完毕了!!!👈
3、D磁盘休眠状态(Disk sleep)
- 解释
在Linux系统中,D磁盘休眠状态一般指硬盘进入休眠模式,也称为硬盘省电模式;
在这种模式下,硬盘会停止转动,磁头也会停止工作,以节约电能。当需要从硬盘中读取数据时,硬盘会重新启动并恢复正常的工作状态。
硬盘休眠模式一般由内核进行控制,内核可以向硬盘发送命令,让硬盘进入休眠状态;
当需要重新访问硬盘时,内核会向硬盘发送一条唤醒命令,让硬盘恢复正常工作状态。因此也称为—— 不可中断休眠。
【注意】
- 需要注意的是,硬盘休眠模式可能会影响系统性能,因为当硬盘休眠时,系统需要等待硬盘重新启动才能访问数据;
- 因此,在需要频繁读写硬盘的场景中,需要谨慎使用硬盘休眠模式。所以在此就不演示了。
👉 到此,关于【D】状态就为大家讲解完毕了!!!👈
4、T停止状态(stopped)
- 解释
在Linux系统中,T停止状态一般指系统进入Suspend-to-Disk(Suspend-to-disk也叫做Hibernation,即休眠到磁盘)模式,也是一种系统休眠模式;
此时操作系统将系统的状态保存到硬盘的一个特定的分区中,然后将整个系统关闭。当系统需要重新启动时,操作系统会重新读取保存在硬盘中的状态,并恢复系统到先前的工作状态。
- 原理
当一个进程被暂时挂起或被其他进程发送一个SIGSTOP信号时,它就会进入T状态;
进程的状态在Linux中可以通过/proc文件系统中的状态文件来查看。该文件于/proc/[PID]/status,其中[PID]是进程的ID,进程的行来表示会 T (stopped)"。
T不会执行任何指完全暂停了它的执行。这是因为它接收到了一个SIGSTOP信号,该信号是一种由系统或其他进程发送的信号,用于暂停进程的执行。
- 💨 以下是一个简单的代码示例,演示如何将进程暂停并使其进入T状态:
- 进程查看:
此时,我们需要学习一条新的命令,即【SIGSTOP】(暂停一个进程):
- 演示过程
那有没有一种方法可以使得当前终止的进程重新跑起来呢?
- 其实是有的,此时有需要另外一条指令【SIGCONT】
- 过程:
此时就会有一个很奇怪的问题,当我们在使用【ctrl+c】想去终止进程的时候,发现此时终止不了:
【分析】
不知道,大家是否注意到当我们使用【SIGCONT】恢复进程后再去查看进程状态时,它显示的是【S】而不是之前那样显示的【S+】;
- 【S】:后台进程。当使用【ctrl+c】后还可以正常的执行shell指令,在后台还可以执行它自己的代码。
- 【S+】:前台进程。表面此时是在前台运行的,使用【ctrl+c】可以终止掉;
此时,我们又需要引入一个新的指令,即【SIGKILL】
- 过程:
【注意】
- 并且,在进入T停止状态后,系统需要花费一定的时间将内存中的内容保存到磁盘中,因此,比起进入Suspend-to-RAM模式的速度会比较慢。
👉 到此,关于【T】状态就为大家讲解完毕了!!!👈
3、🔥 Z(zombie)-僵尸进程
1️⃣解释
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
2️⃣退出码
首先给大家讲解一下关于退出码知识!!!
Linux中的退出码指的是在进程退出时,向父进程返回的一个整数值,用于告诉父进程该进程是否以及如何正常地结束。通常情况下,0表示进程正常退出,而其他的数值则表示进程以异常或错误的方式退出。
在Linux中,一个命令或程序的退出码是由其在退出时调用【exit】系统调用时的参数决定的。通常,0 被默认视为成功的退出码,其他非零值则表示程序以某种方式发生了错误。返回非零退出码能够让调用该程序的用户或其他程序获得有用的信息,以便于进行后续处理。
在shell中,可以通过特殊的变量【$?】获取上一个命令或程序的退出码。例如,在执行一个命令后,可以使用【echo】命令输出退出码,如下所示:
- 输出:
- 💨 紧接着我们再把代码改一下:
- 输出:
【分析】
如果一个进程退出了,立马就会有一个【X】状态,此时你作为父进程,还有没有机会拿到退出结果呢?
- 因此,在Linux中当进程退出时,一般进程不会立即彻底退出,而是要维持一个状态叫做-- Z,也叫僵尸进程,目的是方便父进程(OS) 读取子进程退出的退出结果。
僵尸进程在操作系统中存在的时间很短暂,仅仅是在子进程终止后,父进程还没有处理该子进程的退出状态时。当父进程调用wait()或waitpid()等系统调用获取子进程的退出状态后,操作系统会回收僵尸进程所占用的资源,并将其释放掉。
3️⃣代码演示
此时,我们写出这样的一段代码:
- 过程:
- 查询展示:
4️⃣僵尸进程危害
僵尸进程对系统性能并没有直接的影响,因为它们不再占用CPU和内存资源。然而,如果大量的僵尸进程积累,可能会占用过多的系统进程表项,导致系统进程表不够用,从而影响系统的稳定性。
具体有以下几点:
- 进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎 么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?—— 是的!
- 维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,换句话 说,Z状态一直不退出,PCB一直都要维护?—— 是的!
- 那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?是的!
- 因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
- 内存泄漏?—— 是的!
4️⃣避免僵尸进程
- 父进程调用wait()或waitpid()等系统调用来获取子进程的退出状态,及时回收僵尸进程。
- 父进程可以忽略SIGCHLD信号,由操作系统自动回收子进程的资源。
- 使用双亲死亡法(Parent Death)来处理僵尸进程,即父进程在创建子进程后立即退出,将子进程交由init进程接管。
- 如果父进程已经退出,可以通过编写一个守护进程来定期扫描系统中的僵尸进程,并将其退出。
下面是一个简单的代码示例,用于演示僵尸进程的产生和处理(在后面我还会具体的给大家讲解):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
// 创建子进程失败
fprintf(stderr, "Fork failed\n");
exit(1);
}
else if (pid == 0)
{
// 子进程
printf("Child process executing\n");
sleep(5); // 模拟子进程执行一段时间
printf("Child process exiting\n");
exit(0);
}
else
{
// 父进程
printf("Parent process executing\n");
// 等待子进程退出并获取其退出状态
wait(NULL);
printf("Parent process exiting\n");
exit(0);
}
}
【分析】
- 上述代码中,父进程通过调用
wait(NULL)
等待子进程退出并获取其退出状态。在子进程执行期间,父进程会一直阻塞在这个地方,直到子进程终止。
💨 运行上述代码,你会看到输出类似于:
- 这表明子进程先于父进程退出,父进程成功回收了子进程的资源,因此没有产生僵尸进程
【小结】
- 总之,僵尸进程是由于父进程没有及时处理子进程的退出状态而导致的一种进程状态。及时回收僵尸进程对于系统的稳定性和资源利用非常重要。
总结
到此,关于进程PCB和进程状态便讲解结束了。接下来,我们简单的总结一下
- 1、首先,我们先通过电脑上运行的进程的例子带大家直观的感受了什么叫做进程;
- 2、其次,我们围绕Linux环境下,对进程进行了展开的学习。我先给大家引出什么叫做管理,最直观的解释就是--“先描述,在组织”,并举例说明了这六个字的由来;接下来,带大家了解了对于PCB结构体,我们可为其命名为【task/pcb struct】,这里面存放了进程相关的所有属性;
- 3、接下来,给大家介绍了两种查看进程的方法,分别是【ps】和【/proc】去进程查看;
- 4、紧接着带大家学习了如何通过系统调用查看进程标识符以及创建进程【fork】;
- 5、最后就是关于几种进程状态的理解,分别是【R】、【S】、【D】、【T】、【Z】。
以上便是本文的全部内容了,感谢大家的观看与支持!!!