前言:
在上篇我们已经对有关体系结构的基本知识进行了详细的介绍,接下来我们将进入网络编程的第一个大块—— 有关进程相关的知识!!!
目录
前言
(一) 基本概念
1、描述进程-PCB
2、查看进程
1️⃣通过ps指令
2️⃣通过 /proc
3、通过系统调用获取进程标示符
4、通过系统调用创建进程-fork初识
1️⃣ fork函数
2️⃣fork 返回值
3️⃣ 发生写时拷贝
(二)进程状态
1、看看Linux内核源代码怎么说
2、进程状态查看
3、 Z(zombie)-僵尸进程
1️⃣解释
2️⃣退出码
3️⃣代码演示
4️⃣僵尸进程危害
4️⃣避免僵尸进程
总结
【承上启下】
在上一篇博客中,我们简单的介绍了关于计算机体系结构的基本知识,那在还没有学习进程之前,我先问大家,操作系统是怎么管理进行进程管理的呢?根据上节课我们学到的,其实很简单——先把进程描述起来,再把进程组织起来!
接下来,我们就开始对进程的相关学习,对于这方面的文字知识,我在《操作系统——进程》相关的文章中已经进行了纸面上的直观解释。今天我们将结合在Linux环境,带大家一起去学习有关进程相关的知识。
首先,给大家感性的介绍一下什么叫做进程。今天,当给我们写博客得时候,我不是直接打开电脑就开始写的。我需要先打开电脑--打开浏览器--打开CSDN--最后才开始写文章的。
又例如当我们在使用Linux的时候,我们是先双击登陆xshell,在连接远程的服务器之后才开始正常工作的。再此过程中,操作系统都会为其以进程的方式来进行相应的操作。
【小结】
在以前,不管是我们写的程序还是在指令行上进行相应的指令操作,最后的工作都是由操作系统在底层帮我们把编译起来的程序转化为相应的进程之后再去进行执行的。
通过以上种种,我们可以得出一个结论:我们做的任何关于启动程序并运行的行为,都是由操作系统帮助我们将程序转化为相应的进程再去完成相应的任务!!!
【分析】
【小结】
task_struct-PCB的一种
- 在Linux中描述进程的结构体叫做task_struct
- task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
- 标示符:描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
进程 = 内核关于进程相关的数据结构 + 当前进程的代码和数据
到此,我们就解决了为什么需要进行进程管理,以及需要pcb的原因
接下来,我们简单的通过代码来大家查看相应的进程。
【分析】
那么我们如何查看进程呢?接下来,我们需要用到相应的指令
1、
ps axj
【分析】
ps axj
是一个在Linux系统中常用的命令,用于查看当前运行的进程的详细信息。使用 ps axj
命令会按照进程的层次结构、内核线程和作业控制等方面展示进程信息,生成一个进程树。2、
ps axj | grep process
【分析】
ps axj | grep process` 是一个在 Linux 系统中常用的命令,用于查找并筛选特定进程的信息。它的具体含义如下:
因此,`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` 命令会先输出当前进程的表头信息,然后再将包含 "process" 字符串的全部进程信息输出。这个命令可以帮助我们快速查看进程列表的标题信息并筛选特定进程的信息。
5、
ps axj | head -1 && ps ajx | grep process | grep -v grep
【分析】
这是两个 Linux 命令连接在一起,使用了管道符号 `|`将它们串联起来。
第一个命令是 `ps axj | head -1`,它的作用是列出所有进程的详细信息,并将其输出的第一行传递给下一个命令。
第二个命令是 `ps ajx | grep process | grep -v grep`,它的作用是搜索所有正在运行的进程信息并输出包含关键字 "process" 的行,且同时过滤掉包含 "grep" 的行,防止输出结果包含当前查询的命令本身。
除了上述那样的方式之外,还有一种利用比较“传统”的方式,进程的信息可以通过 /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;
- 当我们删除进程之后,相应的进程的都会被删除掉,在当我们去查看时会发现没得了,同样的 /proc目录伴随着的是动态改变的。
- 进程id(PID)
- 父进程id(PPID)
1、PID
【代码演示】
2、PPID
【分析】
在Linux中,Bash是最常用的shell解释器之一,是一种命令解释器程序,用于执行用户输入的命令。它是GNU操作系统自带的默认shell程序,但也可以在其他操作系统中使用,比如macOS等。
Bash是一个强大的解释器,支持命令历史记录、命令自动补全、文件名通配符、重定向、管道等常用的 shell 功能。Bash还支持变量、数组、函数、条件语句、循环语句等高级编程特性,使得开发人员可以编写相对复杂的脚本来完成各种任务。
Bash解释器的执行过程如下:
从上图,我们则不难看出【bash】也是一个进程!!!
命令行启动的所有程序,最终都会变成进程,而该进程对应的父进程都是bash(对于如何做到的,后序再说)
接下来我们在简单的学个指令。在之前我们是通过【ctrl + c】来终止进程,那有没有其他的方式呢?其实还是有的,我们可以通过【kill】进程的方式来完成。
【小结】
- 运行 man fork
- 认识fork fork有两个返回值
- 父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)
在Linux中用于创建新进程的指令为【fork】,有别于传统的执行【./】来进行把操作,使用【fork】函数可以直接进行操作。
【分析】
1、概念
`fork()` 系统调用的语法如下:
```
#includepid_t fork(void);
```
2、返回值
3、调用
【代码演示】
接下来,我们通过带大家直观的感受是如何操作的。
首先,我们先改一下代码,今天教大家一个批量化注释和去注释的方法:
具体操作:
【分析】
【分析】
【分析】
那大家知道【10788】是什么吗?我们简单的去查看一下:
【分析】
1、首先给大家说一下,在操作系统中【fork】之后,父进程和子进程谁先运行是随机的,即不确定到底是谁先开始运行,取决于调度器先调度谁;
2、此时,大家是不是心里有“十万个为什么了呀!!”大家先别着急,对于第一个问题,在接下来我会为大家解答的,这里大家先接收这个概念。
【分析】
fork()
函数创建了一个新进程,并打印了父进程和子进程的 PID;fork()
函数的返回值来确定当前进程是哪一个进程;fork()
返回值为 0,则说明在子进程中,否则在父进程中。【小结】
a、fork之后,执行流会被分成两个;
b、fork之后,谁先调度由编译器决定;
c、fork之后的代码共享,通常我们通过 if 和 else if 来进行分流;
d、回答上述第一个问题,一个函数有两个返回值:
在 Linux 中,`fork()` 是一个系统调用,用于创建一个新的进程。这个系统调用的返回值有两个,父进程中返回子进程的进程 ID,子进程中返回值为0。而“一个函数有两个返回值”的情况,可以通过函数的返回值来区分当前运行的进程是父进程还是子进程。
当在父进程中调用 `fork()` 函数时,操作系统会创建一个新的进程作为当前进程的子进程。新的子进程是原来进程的一个独立副本,它有自己的地址空间,内存、堆栈和文件描述符等资源都被复制了一份。
在父子进程中,`fork()` 函数的返回值是不同的:
因此,`fork()` 函数在父进程和子进程中都会执行一次,且每个进程都有自己不同的返回值。父子进程的逻辑分别在不同的代码路径中处理,这样就实现了“一个函数有两个返回值”的情况,同时也达到了新建一个独立进程的目的。
在 Linux 中,创建子进程时会发生写时拷贝。
1、当父进程创建子进程时,子进程会继承父进程的内存映像,也就是父进程的地址空间,但是子进程并不直接共享父进程的内存,而是创建了一个副本。
2、在这个副本中,父子进程共享同一个物理页,直到其中一个进程尝试写入共享页时,才会将该物理页复制给这个进程,这就是写时拷贝的过程。
【分析】
在这个示例中,我们创建了一个整型变量 x,并使用 fork()
系统调用创建了一个子进程。在父进程中,我们对 x 变量进行了修改,并在父子进程中分别使用 printf()
打印了 x 的值。
因为父进程中修改的 x
变量是共享的,所以在打印父进程中的 x 的值时,你会发现它已经被修改了。但是,在子进程中打印 x 的值时,你会发现它没有被修改。这是因为写时拷贝机制的存在,在父子进程共享内存时,对共享内存的修改并不会立即生效。只有在相应的进程中修改了共享内存后,才会将页面复制。
为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux内核里,进程有时候也叫做任务)
/*
* 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 */
};
【分析】
接下来,我将为大家逐个的解释上述的各个运行状态,用代码给大家直观的呈现。
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信号,该信号是一种由系统或其他进程发送的信号,用于暂停进程的执行。
此时,我们需要学习一条新的命令,即【SIGSTOP】(暂停一个进程):
那有没有一种方法可以使得当前终止的进程重新跑起来呢?
此时就会有一个很奇怪的问题,当我们在使用【ctrl+c】想去终止进程的时候,发现此时终止不了:
【分析】
不知道,大家是否注意到当我们使用【SIGCONT】恢复进程后再去查看进程状态时,它显示的是【S】而不是之前那样显示的【S+】;
此时,我们又需要引入一个新的指令,即【SIGKILL】
【注意】
到此,关于【T】状态就为大家讲解完毕了!!!
- 僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲) 没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
- 僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。
- 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
首先给大家讲解一下关于退出码知识!!!
Linux中的退出码指的是在进程退出时,向父进程返回的一个整数值,用于告诉父进程该进程是否以及如何正常地结束。通常情况下,0表示进程正常退出,而其他的数值则表示进程以异常或错误的方式退出。
在Linux中,一个命令或程序的退出码是由其在退出时调用【exit】系统调用时的参数决定的。通常,0 被默认视为成功的退出码,其他非零值则表示程序以某种方式发生了错误。返回非零退出码能够让调用该程序的用户或其他程序获得有用的信息,以便于进行后续处理。
在shell中,可以通过特殊的变量【$?】获取上一个命令或程序的退出码。例如,在执行一个命令后,可以使用【echo】命令输出退出码,如下所示:
【分析】
如果一个进程退出了,立马就会有一个【X】状态,此时你作为父进程,还有没有机会拿到退出结果呢?
僵尸进程在操作系统中存在的时间很短暂,仅仅是在子进程终止后,父进程还没有处理该子进程的退出状态时。当父进程调用wait()或waitpid()等系统调用获取子进程的退出状态后,操作系统会回收僵尸进程所占用的资源,并将其释放掉。
此时,我们写出这样的一段代码:
僵尸进程对系统性能并没有直接的影响,因为它们不再占用CPU和内存资源。然而,如果大量的僵尸进程积累,可能会占用过多的系统进程表项,导致系统进程表不够用,从而影响系统的稳定性。
具体有以下几点:
下面是一个简单的代码示例,用于演示僵尸进程的产生和处理(在后面我还会具体的给大家讲解):
#include
#include
#include
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和进程状态便讲解结束了。接下来,我们简单的总结一下
以上便是本文的全部内容了,感谢大家的观看与支持!!!