序言
当我们执行程序的时候,操作系统只是将该程序的代码以及相关的数据加载到内存中吗?如果这样的话,当多个程序都需要执行时,操作系统又该怎样确保程序能够高效、有序地执行呢?所以,只是这样处理的话,肯定是不够的!
一个学校的校长需要管理整个学校的学生,那他不可能将所有学生都认识一遍吧!但那个校长幸好是一个程序员,他将需要管理的信息(身高,体重,性别,绩点等等)包装为一个结构体 struct
,现在校长对学生的管理也就变成了对学生信息的管理,最后他利用指针将所有学生的信息相连接起来。校长的目的已经达到了,他对全校学生的管理,变成了对学生信息链表的增删查改。
操作系统对大量需要执行程序的管理,从简单的看,又何尝不是这个简单的道理呢?
1. 进程的概念
1.1 什么是进程
进程是程序在处理机上的一次执行过程,是程序的一个执行周期,是正在执行的程序。它是系统进行资源分配和运行调度的一个独立单位。
1.2 进程的组成
我们在上面提到,操作系统为了高效地管理程序,将程序的信息包装为一个结构体,然后将所有信息相连接,最后 对所有程序的管理,也就变成了对该链表的增删查改。所以一个程序进入内存后,不仅仅只是包含自己的代码和数据,还有一个包含了自身信息的结构体,叫做 PCB 进程控制块
。所以说,进程 = PCB + 代码数据
:
- 代码:这是进程要执行的指令集合,是程序的具体实现。
- 数据:进程在执行过程中需要访问和操作的数据,包括输入数据、输出数据以及中间结果等。
- PCB(进程控制块):PCB是操作系统用于存储进程相关信息的数据结构,它包含进程的标识符、状态、优先级等信息
PCB 内的信息包括:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- …
2. ps
指令 和 fork
函数
在介绍进程的状态之前,先介绍在 Linux 的一个指令 ps
和 C/C++
中的 fork
函数,以便在之后更好的理解。
2.1 ps
指令
该指令用于获取当前环境下进程的详细信息,他的选项常用的包含:
a
:显示所有终端的进程,包括其他用户的进程。u
:显示详细的进程信息,包括进程的所有者、CPU使用率、内存使用量等。x
:显示没有控制终端的进程。e
:显示所有进程,而不仅仅是当前终端会话的进程。j
:采用工作控制的格式显示程序状况。f
:显示完整的进程信息,包括父进程ID(PPID)、进程状态、CPU使用率(%CPU)、内存使用率(%MEM)等。l
:显示长格式的进程信息,包括进程命令行、进程状态(如S表示休眠、R表示运行等)、进程的会话ID(SID)等。p
:指定要显示的进程ID(PID),可以通过此选项查看特定进程的详细信息。o
:自定义输出格式,可以指定要显示的列和排序方式。
在使用时我们常用的配合有 ps ajx
:
ps aux
:
2.2 fork
函数
该函数用于 创建一个新的进程,称为子进程。 使用时需要包含 <unistd.h>
头文件,fork
函数的一个显著特点是它 调用一次,返回两次
。具体来说,fork函数在父进程中返回新创建的子进程的PID,在子进程中返回 0。如果出现错误,则返回 -1。
使用举例:
// getpid 函数用于获取当前程序的 pid
1 #include <iostream>
2 #include <stdio.h>
3 #include <unistd.h>
4 using namespace std;
5
6 int main(){
7 int ret = fork();
8 // 创建失败
9 if(ret == -1){
10 printf("Failed to create child process!");
11 }
12 // 子进程
13 else if(ret == 0){
14 while(1){
15 printf("I am child process, my pid is %d.\n", getpid());
16 sleep(1);
17 }
18 }
19 // 父进程
20 else{
21 while(1){
22 printf("I am parent process, my pid is %d.\n", getpid());
23 sleep(1);
24 }
25 }
26
27 return 0;
28 }
在这里我们创建了一个子进程,并且父进程和子进程都以间隔一秒的时间打印相应的内容:
3. 进程的状态
进程的状态是操作系统中描述进程执行过程变化的重要概念,它们反映了进程在生命周期中的不同阶段。进程的状态通常可以分为以下几种:
- 创建态(新建态)
当进程刚被创建时,它处于创建态。此时,操作系统正在为进程分配资源,初始化进程控制块(PCB)等。在创建态下,进程还没有被加载到内存中执行。 - 就绪态
当进程已经准备好运行,但还没有被CPU调度执行时,它处于就绪态。在就绪态下,进程已经具备了运行的条件,等待CPU调度执行。 - 运行态
当CPU调度器选择了一个就绪态的进程,并开始执行它时,该进程处于运行态。在运行态下,进程正在被CPU执行,执行其指令。 - 阻塞态(等待态)
当进程由于某些原因无法继续执行时(如等待I/O操作完成、等待某个事件发生等),它会进入阻塞态。在阻塞态下,进程暂时停止执行,等待条件满足后重新进入就绪态。 - 终止态
当进程执行完成或者被终止时,它进入终止态。在终止态下,进程释放占用的资源,操作系统回收PCB等,进程的生命周期结束。
我们在这里着重介绍 运行态,以及上面没有包括的挂起状态
:
3.1 运行状态
当我们 CPU
执行一个程序时,是将该程序执行完成才结束吗?肯定不是的,就比如,我们写了一个死循环的程序,CPU
难道会一直执行该程序?CPU
会采用一种更为公正的方式,为每个程序分配固定的时间运行。
时间片轮转调度算法
CPU
会采用时间片轮转调度算法,该算法的系统中,每个进程被分配一个固定的时间片来执行。当进程的时间片用完时,系统会将其从运行状态转换为就绪状态,以便让其他进程获得CPU的使用权。
没执行完的程序咋办
如果你的进程在该时间内没有执行完成,会 保留现场。会将 CPU
中寄存器相应的数据存入进程的 PCB
的上下文信息中。当你下一次继续执行时,会 复原现场
。
3.2 挂起状态
大家都知道,我们内存的空间使用是非常紧张的,当我们的内存工作负荷较重时,系统可能会把一些不重要的进程挂起,以保证系统能正常运行并优先处理实时任务。
什么是挂起
如果对自己的电脑较为了解的话,会知道在自己的磁盘上有一块叫做 swap
空间,该空间的作用就是:当内存工作负荷较重时,会将暂时不会被 CPU
所调度的进程的代码和数据放入该块空间上,省出相应的内存空间,这就是挂起。
这是操作系统利用时间(效率)获取空间的手段。
4. Linux 上进程的状态
有同学可能就有疑问了,我们不是已经说过了 进程的状态
了吗?为什么还要把 Linux
单独拿出来讲,是因为他更特殊吗?哈哈不是这样的,前面我们提到的只是操作系统中的概念,只是一个理论框架,不是具体的实现。在实际情况下,可能会将进程状态划分得更加细致,以便更好地管理进程的生命周期和调度。
4.1 R(Running)状态
状态描述:进程 PCB
被调度到 CPU
运行队列中且已被分配 CPU
资源,正在执行指令,可以访问 CPU
和其他系统资源。
注意:运行状态并不意味着进程一直占用CPU,因为存在时间片的概念,进程在用完时间片后会被放回队列尾部,等待再次调度。
4.2 S(Sleeping)状态
状态描述:: 进程处于可中断的睡眠状态。这通常意味着进程正在等待某个事件(如I/O操作完成、接收到信号等)来唤醒它。
以我们上述举例子的程序为例,我们的程序创建了个子进程,并且每隔一秒打印相应的信息。现在我们通过命令:
while true;do ps ajx | head -1 && ps ajx | grep a.out | grep -v grep; sleep 1; done
每隔一秒查看该进程的状态:
可以看到,我们该程序一共产生了两个进程,一个父进程,一个子进程,但是很奇怪,我们的程序一直在执行,为什么还会是 S
状态?
请问大家,是操作系统执行我们的程序快,还是显示屏打印结果的速度快?毋庸置疑,显示器硬件的速度根本和操作系统不是一个量级的。所以说,当我我们查看该进程的状态时,绝大部分时间都是在等待显示器打印结果,操作系统的执行只是一刹那的瞬间。
4.3 T(Stopped)状态
进程描述:进程已被停止。这通常是因为接收到了SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号。
例:正在执行的程序,我们使用 ctrl + Z
来中断他:
这时他的状态就是 T
:
4.3 D(disk sleep)状态
进程描述:不可中断的睡眠状态。这通常发生在进程正在等待I/O操作(如磁盘读写)完成,且不能被信号中断。
在上面我们介绍了 S
状态,该状态可以被中断,现在的 T
状态不可被中断。
4.4 Z(Zombie)状态
状态描述:这是一个已经结束的进程,但其父进程尚未通过调用 wait()
或 waitpid()
来读取其退出状态。
我们这里改变一下代码的逻辑:
1 #include <iostream>
2 #include <stdio.h>
3 #include <unistd.h>
4 using namespace std;
5
6 int main(){
7 int ret = fork();
8 // 创建失败
9 if(ret == -1){
10 printf("Failed to create child process!");
11 }
12 // 子进程
13 else if(ret == 0){
14 printf("I am child process, my pid is %d.\n", getpid());
15 }
16 // 父进程
17 else{
18 while(1){
19 printf("I am parent process, my pid is %d.\n", getpid());
20 sleep(1);
21 }
22 }
23
24 return 0;
25 }
我们将子进程的不断打印改变成为了只打印一次,而父进程不变。
之后我们启动程序,并输出该进程的状态:
可以看到,子进程变成了 Z
状态,其代码和数据部分已经被回收,进程的进程描述符就始终保持在系统,造成资源的浪费。
该进程的起因是因为:当子进程比父进程先结束时,父进程没有读取子进程的退出状态。
为什么需要父进程读取子进程的信息呢?你执行完毕了,直接推出不好吗?
之所以这样做,是因为子进程帮助你父进程完成一个任务,你不需要确定一下他完成的情况吗?这会导致父进程就无法知道子进程的执行结果,这可能会导致程序逻辑上的错误或不一致。
4.5 孤儿进程
状态描述:孤儿进程指的是一类特殊的进程,即其父进程已经终止(退出),但该进程本身仍在运行。
这和 僵尸进程
恰巧相反,孤儿是父亲结束了,但我还在执行;而僵尸是我结束了,但父亲还在执行。
被init进程收养
:在 Linux 系统中,当一个进程成为孤儿进程时,操作系统内核会自动将该进程的父进程ID(PID)更改为1,即 bash
进程。该进程充当了所有孤儿进程的“监护者”,负责管理和回收这些进程的资源。
总结
在这篇文章中,我们介绍了进程的概念以及进程在 Linux 系统上详细的状态,希望大家有所收获。