进程概念以及进程的状态

序言

 当我们执行程序的时候,操作系统只是将该程序的代码以及相关的数据加载到内存中吗?如果这样的话,当多个程序都需要执行时,操作系统又该怎样确保程序能够高效、有序地执行呢?所以,只是这样处理的话,肯定是不够的!
 一个学校的校长需要管理整个学校的学生,那他不可能将所有学生都认识一遍吧!但那个校长幸好是一个程序员,他将需要管理的信息(身高,体重,性别,绩点等等)包装为一个结构体 struct,现在校长对学生的管理也就变成了对学生信息的管理,最后他利用指针将所有学生的信息相连接起来。校长的目的已经达到了,他对全校学生的管理,变成了对学生信息链表的增删查改。
 操作系统对大量需要执行程序的管理,从简单的看,又何尝不是这个简单的道理呢?


1. 进程的概念

1.1 什么是进程

 进程是程序在处理机上的一次执行过程,是程序的一个执行周期,是正在执行的程序。它是系统进行资源分配和运行调度的一个独立单位。

1.2 进程的组成

 我们在上面提到,操作系统为了高效地管理程序,将程序的信息包装为一个结构体,然后将所有信息相连接,最后 对所有程序的管理,也就变成了对该链表的增删查改。所以一个程序进入内存后,不仅仅只是包含自己的代码和数据,还有一个包含了自身信息的结构体,叫做 PCB 进程控制块。所以说,进程 = PCB + 代码数据

  • 代码:这是进程要执行的指令集合,是程序的具体实现。
  • 数据:进程在执行过程中需要访问和操作的数据,包括输入数据、输出数据以及中间结果等。
  • PCB(进程控制块):PCB是操作系统用于存储进程相关信息的数据结构,它包含进程的标识符、状态、优先级等信息

PCB 内的信息包括:

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据: 进程执行时处理器的寄存器中的数据。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

2. ps 指令 和 fork 函数

 在介绍进程的状态之前,先介绍在 Linux 的一个指令 psC/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 系统上详细的状态,希望大家有所收获。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值