先打个预防针:进程状态本质其实task_struct内部维护的一个int类型的整数,只不过不同的整数表示不同的状态。

1.操作系统的角度理解运行、阻塞、挂起状态

1.1 运行状态

        我们已经知道,PCB是由双向链表进行管理的,但实际上,它又属于调度队列,同时属于两个数据结构?这怎么理解呢?我们先谈一谈内核中链表的实现。

        预备知识:C语言中,对任何类型开辟空间的时候,变量的地址在数字上等于开辟空间中字节地址最小的那一串数字,然后通过偏移量来确定数据。

内核中链表的实现:

        双向链表实际上是在task_struct内部维护的,外部定义结点,内部定义结点对象,通过对象的前后指针指向串联起整个链表,如图:

        然后问题转化成了知道某个对象内部某个变量的地址,怎么求对象的起始地址?也就是该变量的偏移量,解决了这个问题就解决了管理进程时的访问数据问题。我们先定义一个简单的结构体方便说明问题。

struct obj
{
    int a = 0;
    int b = 1;
    int c = 2;
    itn d = 3;
}

int main()
{
    obj s;
    return 0;
}

解决办法:

        偏移量 :(long long)&((&(struct obj*)0) -> b)  

        起始地址 = (long long)&(s.b)  -  (long long)&((&(struct obj*)0) -> b)  

解释: (strcut obj *)0假设起始地址为0的结构体;->b,访问b这个变量,&再取它的地址,强转为long long后的结果就是偏移量,核心要理解的就是假设起始地址为0

问题来了:为什么内核要这么做,设计这么复杂?

答:其一,对内核对象进行管理更具通用性;其二,这样的设计保障了task_struct既可以属于双向链表,又可以属于调度队列,且有需求的话,未来还可以属于任何数据结构!!!

什么是调度队列?

        调度队列是操作系统内核中用于管理进程/线程执行顺序的数据结构;简单来说,就是操作系统的“待办清单”,也就是待CPU处理的。

说明:只要进程在调度队列内部,我们就认为是处于运行状态。

1.2 阻塞状态

        阻塞状态:简单来说,就是进程等待的资源未就绪,造成堵塞。比如当前运行的程序中有scanf,它要访问操作系统,然后访问外设,从键盘读取数据,此时是在调度队列中的,处于运行状态,但是如果迟迟没有等到键盘输入,即资源始终不就位,此时要出调度队列,进入等待队列,读取数据。(等待队列此时需要与访问的外设进行链接,等待资源就位,如下图)此时进程的管理依旧还是数据结构的增删擦查改,无非就是数据结构在变化而已。

所以判断当前是处于运行状态还是阻塞状态,看当前所处的队列即可。

1.3 挂起状态

        当内存资源严重不足时,进程会被“请出”调度列表,它的代码和数据会被换出(swap out)到磁盘上一个叫swap的分区,并且寄存器中的值以及运行道哪一行代码也会被保留进这个swap分区,此时的状态称作挂起状态,当内存资源足够时,再将这部分换入(swap in)内存,那如果换出操作后内存还是严重不足呢?此时系统就有可能杀进程了,直接关闭程序/进程。进程运行期间挂起叫做运行挂起,阻塞期间挂起叫做阻塞挂起。

        问题:swap分区多大最为合适? 一般认为是内存大小或者内存大小的一半最好,因为swap分区足够大的话,OS会依赖swap分区,swap in 和swap out 操作频率会增加,也就是IO操作增加,而IO操作效率是很低的,降低系统速度。所以挂起的本质就是:用时间换取空间

       阻塞状态和挂起状态的区分:阻塞状态是进程仍在内存区域,只不过是在等待某种资源,而没有运行,挂起状态的进程已经不在内存当中了。

2. Linux下看待进程状态

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 */
};

2.1 R

R:运行状态,程序不一定在运行,也可能在调度队列里。还有一种情况就是当死循环打印时,明明当前程序在一直运行,但状态却显示的是S,这是因为CPU太快了,它可能已经完成了十万次输出,但是外设(显示屏)只输出了几万次,此时就是在等待外设完成输出的过程。如下图:

 这里我们看到S的后面有一个'+',这个' + '表示当前程序是在前台运行。

理解一下前后台:

        试想一下,如果当前有很多程序正在执行,且大部分都需要从键盘读取数据,此时我们输入的数据究竟给那个进程呢?显然我们只能给一个进程输入数据,而能够接收这个数据的程序就叫做前台程序。所以:

        前台程序:能够从键盘当中读取数据的程序,有且只有一个。

        后台程序:也就是不能从键盘读取数据的,可以存在多个。

        那为什么要有前后台呢?那当然是为了提高效率,比如下载和安装软件的时候,你总不能傻傻的等着它下载完才能执行其他命令吧,此时我们就可以把下载和安装放在后台运行,如果有写入数据的话,先向文件中写入即可。要将程序放在后台运行只需在命令的最后加一个‘ & ’,如下:

        前台运行时:

此时输入的命令被忽略(bash被换到后台了,不能从键盘读取数据)但此时的ctrl + c还是能终止程序的。

        后台运行时:

程序后台运行的时候,命令可以被读取和执行,但此时的ctrl + c不再起作用。

kill -9 PID  #此时这个命令才能杀掉程序

      

2.2 S

S:休眠状态,这里的休眠状态也叫做可中断休眠状态,意味着进程在等待事件完成,例如scanf时,等待从键盘获取资源,或者等待从磁盘获取资源。

2.3 T

T:需要获取资源(如scanf)的程序放后台,此时这个程序的状态就是T(stop)

code.c

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 
  4 int main()
  5 {
  6     int a;
  7     while(1)
  8     {
  9         scanf("%d\n",&a);                                                                  
 10          printf("我是一个进程:我的pid是:%d\n a :%d",getpid(),a);
 11          sleep(1);
 12     }
 13     return 0;
 14 }
jobs  #用于查看后台运行的程序
fg [序号]  #将后台程序移至到前台

kill -19 [PID]   #暂停(挂起)一个进程
kill -18 [PID]   #恢复至运行状态  这两一般搭配使用

2. 4 t

t:进程被追踪时暂停。如gdb创建子进程进行调试,打断点,程序运行至断点处停下,此时的状态就是t。

2.5 D

D:磁盘休眠,是一种深度睡眠,不可中断睡眠,OS也杀不掉,只能等待进程自己醒来。这是一种保护措施。

        比如一个进程正在向磁盘写入1GB的数据,而此时内存刚好又严重不足,操作系统会开始杀程序,但是不会杀该进程,因为如果杀了的话,这些1GB的数据怎么处理,不好交代,所以只能等待当前进程完成。

2.6 Z和X

Z(zombie):进程退出,但是PCB还需要维持一段时间,直到父进程来读取,这段时间就是Z(僵尸进程)状态,Z是走向X的一个过度阶段。

        为什么要这么做呢?因为进程是需要被管理的!父进程通过getpid()子进程的PID来进行管理,如果一直不读取的话会造成内存泄漏!这对一直运行而不退出的程序(常驻程序)影响会非常大。另一种情况:父进程先结束的子进程被称作孤儿进程,此时子进程会被PID为1(就是bash)的进程领养,避免内存泄漏。

X:是一种瞬时状态,被释放的一瞬间。

完~

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐