Linux 进程状态

操作系统学科的进程状态

请添加图片描述

  • 新建态:刚刚创建的进程, 操作系统还未把它加入可执行进程组, 它通常是进程控制块已经创建但还未加载到内存中的新进程。
  • 就绪态:进程做好了准备,只要有机会就开始执行。
  • 阻塞态:进程在某些事件发生前不能执行,如 I/O 操作完成。
  • 执行/运行态:进程正在执行,假设计算机中只有一个处理器,因此最多只有一个进程处于这个状态。
  • 终止/退出态:操作系统从可执行进程组中释放出的进程,要么它自身已停止,要么它因某种原因被取消。

上面的就是操作系统学科中,可能会提到的进程状态!当然你还可能看到诸如:就绪挂起,阻塞挂起等概念!
我们要学习的是一款具体的操作系统:linux 操作系统对进程状态的定义和实现。

linux 进程状态

为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。一个进程可以有几个状态(在 Linux 内核里,进程有时候也叫做任务)。
下面的状态在 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 */
};
  • R 运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
  • S 睡眠状态(sleeping): 意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠
    (interruptible sleep))
  • D 磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
  • T 停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
  • X 死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。

运行状态,R

在 linux 中,进程控制块 task_struct 是用双向链表链接起来的!操作系统维护了一个运行队列,凡是在运行队列中的进程就都处于运行态!被放在操作系统维护的运行队列中的是进程的控制块,即 task_struct,当轮到某个进程的代码被 cpu 执行时,我们能够通过运行队列中的 task_strcut 找到该进程对应的代码和数据!然后开始执行!
在这里插入图片描述
一个正在 cpu 上运行的进程是不是一直要等到该进程的代码执行完毕才把自己从 cpu 上扒下来呢?显然这是不可能的!每一个进程都有一个叫做时间片的概念,当某个进程的时间片消耗完了,就会脱离 cpu,换下一个进程到 cpu 上执行!由一个进程切换到另一个进程,叫做进程切换
linux 中进程的时间片大约是:5~800ms,这就意味着一个进程每次在 cpu 上执行的时间是有限的!加上 cpu 来回地切换进程!我们就能够看到多个进程在同一时间同时运行的现象!


我们能够尝试看到运行状态嘛?因为每个进程在 cpu 上执行的时间都非常短,看到这个状态也是不容易的!

  • 首先我们得学会查看进程的状态嘛!在进程的概念部分我们讲解了查看进程部分属性的方法,其中 ps axj 就能查看进程的状态!

  • 写程序:

    #include<stdio.h>
    
    int main()
    {
          
          
    	while(1) printf("hello linux\n");
    	return 0;
    }
    //下面是 makefile 文件
    test:test.c
    	gcc -o $@ $^ -std=c99
    .PHONY:clean
    	clean:
    rm -f test
    

    我们一直在循环打印 “hello linux”。

  • 通过监控脚本查看可执行程序(进程)的状态:

    while :; do ps axj | head -1 && ps axj | grep test | grep -v grep; echo "-------------------------------------------------"; sleep 1; done;
    

    在这里插入图片描述
    我们看到尽管显示器上一直在打印 “hello linux” 但是我们看到的进程状态依旧是:S+,即阻塞状态!
    这个 S+S 的区别就是:

    • S+:加号代表前台进程,S+ 就是这个前台进程正处于阻塞状态。
    • S:表示后台进程正在阻塞中!

    这里前后台的概念就跟手机上的前后台概念差不多!
    我们运行可执行程序的时候加上 & 就能在后台运行啦:./test &
    在这里插入图片描述
    这个时候我们就能看到:S 而不是 S+。在后台运行之后,你发现使用:ctrl + c 结束不掉这个进程了!那是因为 ctrl + c 只能结束前台进程!那要结束该怎么办呢?我们可以使用信号(信号我们后面会详细讲解):

    kill -9 1322154
    

    看上图,这个进程的 pid 是:1322154。我们给 pid 为 1322154 的进程发送九号信号就能将这个进程给杀掉了!

可这并不符合我们的预期哇!我们想看到的是 R 状态哇!怎么办呢?我们尝试将 printf 去掉试试!

#include<stdio.h>

int main()
{
    
    
    while(1) ;
    return 0;
}

在这里插入图片描述
我们看到也是成功看到 R 状态啦!这是因为加上 printf 这个进程会等待显示器资源就绪,从而使得进程大部分时间处于阻塞状态!主要是 cpu 执行的速度太快了!

阻塞状态,S

操作系统中为正在运行的进程维护了运行队列!同样也会为正在阻塞的进程维护阻塞队列!一个进程处于阻塞状态的场景很多,比如:等待外设资源就绪(包括,键盘,网卡,鼠标,显示器等等)。
操作系统中的大部分进程都是处于阻塞状态的!


下面的代码中,正在等待键盘的输入,那么这个进程的状态就是阻塞状态呢!

#include<stdio.h>

int main()
{
    
    
    int a;
    scanf("%d", &a);
    return 0;
}

在这里插入图片描述

深度睡眠/磁盘休眠,D

我们假设一个场景:在很久很久以前,有一个进程 A,正在向磁盘中写入数据!进程 A 正在欢快的写着。突然,操作系统发现,自己的内存空间严重不足了!当他路过进程 A 身边时,发现 A 正在向磁盘中慢吞吞的些数据呢!操作系统看他不顺眼,直接将进程 A 给他杀掉了!(操作系统为了保证自己的正常运行,完全可能杀进程的,类比手机开很多应用程序,操作系统杀后台的例子)。磁盘正写着呢,突然发现这个进程没了,这数据才写道一半呢?怎么办呢?磁盘心想,不完整的数据还是丢了吧!(这是大部分情况的结果!)。
这个时候上层用户发现对自己非常重要的数据没了!一纸诉状,将操作系统,进程,磁盘一并告上了法庭!请听被告的辩词:

  • 操作系统:亲爱的用户,您赋予我管理软硬件资源的权力,为的就是向您提供一个安全,流畅,的运行环境!当时我正处于危机时刻,如果不通过杀死进程来释放内存,我就会崩溃的!导致电脑直接挂掉!
  • 磁盘:我一直在认真完成自己的工作哇,数据写到一半,进程突然没了!我只能将残缺的数据丢掉了!因为我的设定本就是如此!如果我有罪,那么其他的磁盘是不是也同样有罪!
  • 进程:我可是受害者哇!我是被杀掉的,怎么能有罪呢?

如果您是法官大人,您觉得是谁的错呢?显然他们都没有错!
从那以后,当进程正在向磁盘中写数据的时候,他的状态就会被修改为 D 状态,这个 D 状态就是一块免死金牌,操作系统无法杀掉这个进程!
操作系统中 D 状态的进程很短,很少,用户一般都察觉不到。我们无法演示出来 D 状态!
如果真被用户察觉到,那么操作系统肯定要挂了!

暂停状态,T(t)

我们目前认为:Tt 状态没有区别!
在进程状态中的 R 状态中,我们学会了用 kill 命令杀掉一个进程!现在我们再来学习如何使用 kill 命令暂停和运行一个进程!

kill -l #列出所有信号

在这里插入图片描述

(信号部分的原理我们后期会详细讲解,这里学会怎么使用就行啦)这里面有两个信号:SIGSTOPSIGCONT 分别用来暂停,和运行一个暂停的进程!一个进程被暂停之后,我们就能看到 T 状态的进程啦!

#include<stdio.h>
#include<unistd.h>

int main()
{
    
    
    for(int i = 0; ; i++)
    {
    
    
        printf("hello linux: %d, pid: %d\n", i, getpid());
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
我们看到,在给 pid 为 1613503 的进程发送 19 号信号之后,进程就暂停下来了,查看这个进程的状态就是 T 状态!
我们在给他发送 18 号信号就能让他从暂停状态变为运行状态!
在这里插入图片描述
T 状态与 S 状态有区别,T 状态可以理解为阻塞状态,只不过 T 状态可能是控制一个进程,或者等待!


T/t 状态有什么运用场景嘛?我们之前是不是学习过 gdb,让程序在代码的某个位置停下来(调试),不就是 T/t 状态的运用嘛!
在这里插入图片描述

终止态,X

终止态的进程是真的就看不到了,一个进程死亡了,被放入垃圾回收的队列中,回收资源!
终止态是一个瞬时状态!

僵尸状态,Z

一个进程退出了,进程的退出信息会被维护一段时间,父进程获取了进程的退出状态之后,该进程由 Z 状态变为 X 状态
进程退出信息被维护的这一段时间,进程就处于 Z 状态!
一般情况下,进程退出的时候,如果父进程没有主动回收子进程的退出消息,子进程会一直处于 Z 状态,子进程相关的资源,尤其是 task_struct 结构体不能被释放,那么子进程的 task_struct 就会一直占用内存资源,造成内存泄漏。

父进程获取子进程退出信息的过程称为进程等待!这个知识点后面会详解!
父进程直接创建了子进程嘛,子进程要退出了,父进程肯定要关心关心撒!父进程创建子进程就是让子进程来办事的嘛!

我们可以写一个代码来验证,子进程退出的时候,父进程不获取子进程的退出状态,那么子进程就会处于僵尸状态的结论:让子进程循环打印 pidppid 三秒之后退出循环,退出循环之后结束子进程。父进程打印自己的 pid

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>

int main()
{
    
    
    pid_t id = fork();
    if(id == 0) //子进程
    {
    
    
        int cnt = 0;
        while(1)
        {
    
    
            printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
            cnt++;
            if(cnt == 3)
                break;
        }
        exit(0);
    }
    else if(id > 0)
    {
    
    
        while(1)
        {
    
    
            printf("I am parent, pid: %d\n", getpid());
            sleep(1);
        }
    }
    else
    {
    
    
        perror("fork(): ");
    }
    return 0;
}

使用监控脚本监控进程的状态:
在这里插入图片描述
我们看到在子进程退出之后,子进程的状态变成了 Z 状态,也就是这个进程变成了僵尸进程
至于怎么获取子进程的退出信息,我们会在进程等待的章节详细讲解!


你有没有想过,如果我们的父进程先退出,结果又是怎么样的呢?

孤儿进程

好的,我们就来写代码看看父进程比子进程先退出是怎么个事儿!
我们让父进程运行三秒之后退出,子进程一直运行,通过监控监本看看会出现什么现象!

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>

int main()
{
    
    
    pid_t id = fork();
    if(id == 0) //子进程
    {
    
    
        while(1)
        {
    
    
            printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
    }
    else if(id > 0)
    {
    
    
        int cnt = 0;
        while(1)
        {
    
    
            printf("I am parent, pid: %d\n", getpid());
            sleep(1);
            cnt++;
            if(cnt == 3)
                break;
        }
    }
    else
    {
    
    
        perror("fork(): ");
    }
    return 0;
}

我们看到,父进程退出之后子进程的确一直再跑,只不过子进程的父进程好像发生了变化!
在这里插入图片描述
子进程的父进程变成了 1,我们来看看这个 1 号进程是啥啊!
在这里插入图片描述
一号进程是:systemd,不就是操作系统嘛!
这个现象说明:父进程先于子进程退出,这样的子进程被称为孤儿进程!孤儿进程会被操作系统领养!


为什么操作系统要领养他呢?
因为孤儿进程也要释放资源哇,之前是通过父进程获取子进程的退出状态之后,由操作系统释放资源!父进程提前退出了,那么就直接让操作系统释放不就行了!操作系统有这个能力做到!


bash 进程不是我们自己写的进程的父进程吗? bash 进程能领养孤儿进程嘛?
说到底就是 bash 没有这个能力,即使 bash 进程领养了子进程,但是系统中没有爷爷进程等待孙子进程的逻辑,即使领养了也没用。但是操作系统就不一样了,操作系统是所有进程的管理者,能够直接在内核层面回收!


猜你喜欢

转载自blog.csdn.net/m0_73096566/article/details/134765576