Linux 进程状态(R, S, D, T(t), X, Z)

目录

进程状态

R-- 运行状态

S-- 睡眠状态(可中断睡眠状态)

D-- 磁盘休眠状态(不可中断睡眠)

T-- 停止状态& t-- (跟踪状态)

X-- 死亡状态(退出状态)

Z-- 僵死状态&僵尸进程

僵尸进程

僵尸进程危害

如何避免僵尸进程

孤儿进程


进程状态

进程的先描述,再组织 戳链接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103213364

在进程的组织中我们以学校组织管理学生为例子. 那么 在学校中,  学生有着不同的状态,  比如说, 休学, 请假, 毕业, 退学,  学业警

告等各种表示学生学习生活状态的信息 .进程就像学校里的学生一样, 也有着不同的状态. 我们来看 内核原码中对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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
  • Z-- 僵尸状态 (zombie) : 已经死了, 依然占着某些资源

R-- 运行状态

只有该状态下的进程才可能在CPU中执行, 为什么说是可能呢? 因为在Linux下, 正在运行和就绪(在运行队列中, 拿到时间片就能

运行)的进程都视作运行状态. Linux 下 R 状态的进程的task_struct结构 (PCB) 被放入对应的CPU的可执行队列中(一个进程最多

只能出现在一个CPU的可执行队列中). 进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该CPU上行 .

如下面代码运行会进入死循环, 会一直占用CPU, 处于R状态

#include<unistd.h>
int main(){
    while(1);
    return 0;
}

S-- 睡眠状态(可中断睡眠状态)

处于这个状态的进程因为等待某个事件的发生, 比如被wait()阻塞,而被挂起. 这些进程的task_struct被放入对应事件的等待

队列中. 当等待的事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的这个睡眠状态的进程被唤醒.

通过ps命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于S 状态(除非机器正处于高负荷状态下. 毕竟CPU就那

么几个,而进程动辄就是几十上百个,如果不是绝大多数进程都在睡眠,那对于CPU来说, CPU简直太难了 .

如下代码, 子进程由于sleep进入S状态, 父进程由于wait阻塞等待子进程退出, 也处于S状态

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
int main(){
    pid_t pid = fork();
    if(pid == 0){
        sleep(30);
        _exit(0);
    }
    wait(NULL);
    return 0;
}

 

D-- 磁盘休眠状态(不可中断睡眠)

与R 状态类似, D状态进程也处于 "睡眠状态", 但是此刻的睡眠时不可中断的.  不可中断,指的并不是CPU不响应外部硬件的中

断,而是指进程不响应异步信号 . 绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发

现,kill -9竟然杀不死一个正在睡眠的进程了!于是我们也很好理解,为什么ps命令看到的进程几乎不会出现D 状态,而总是S状

态.  而 D 状态存在的意义就在于,内核的某些处理流程是不能被打断的. 如果响应异步信号,程序的执行流程中就会被插入一段

用于处理异步信号的流程(这个插入流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程被中断了. 在进程对某

些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,

并与对应的物理设备进行交互), 可能需要使用 D 状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不

可控的状态 .  这种情况下的 D 状态总是非常短暂的,通过ps命令基本上不可能捕捉到 .

很难捕捉, 但还是可以捕捉的, 让grep 在根目录下搜索字符, grep会对磁盘进行扫描, 即处于不可中断的D状态, 直到IO结束.

T-- 停止状态& t-- (跟踪状态)

向进程发送一个SIGSTOP(停止信号)信号,它就会因响应信号而进入T状态(除非该进程本身处于 D 状态而不响应信号 ).

(SIGSTOP与SIGKILL(无条件终止)信号一样,是非强制的. 不允许用户进程通过signal系统的系统调用重新设置对应的信号处理

函数)向进程发送一个SIGCONT(继续信号)信号,可以让其从 T 状态恢复到 R 状态。

当进程正在被跟踪时,它处于 t 这个特殊的状态。“正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。比如在

gdb中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于 t 状态。而在其他时候,被跟踪的进程还是处于前面提到

的那些状态。对于进程本身来说,T 和 t 状态很类似,都是表示进程暂停下来。而 t 状态相当于在 T 之上多了一层保护,处于 t

状态的进程不能响应SIGCONT信号而被唤醒. 只能等到调试进程通过ptrace系统调用执行PTRACE_CONT、PTRACE_DETACH

等操作 (通过ptrace系统调用的参数指定操作), 或调试进程退出,被调试的进程才能恢复 R 状态 .

如下代码:

#include<unistd.h>
int main(){
    while(1);
    return 0;
}

进入死循环后是 R 状态 , 用 kill -STOP PID 来暂停, 如下

如下代码 

#include<stdio.h>
int main(){
    int flag = 10;
    if(flag == 10){
        flag = 0;
    }
    return 0;
}

当执行到断点暂停, 进程此时处于 t 状态

X-- 死亡状态(退出状态)

进程即将被销毁时的状态, 所以X状态是非常短暂的,几乎不可能通过ps命令捕捉到 .

Z-- 僵死状态&僵尸进程

在进程退出过程中, 进程占有的所有资源将被回收, 除了task_struct结构 (以及少数资源) 以外不立即释放.

但当一个进程退出, 但其所占用的资源没有释放回收时, 这个进程就变成了僵尸进程, 处于僵死状态.

例如当子进程先于父进程退出时, 为了保存自身的退出状态(退出码或异常信号), task_struct 以及少数资源不会立即释放, 一直等

待父进程接收其退出状态, 这时操作系统会通知父进程获取子进程的退出状态, 当父进程获取到子进程的退出状态后, 释放子进程

未释放完的资源. 但若父进程没有关注到子进程退出, 一直没有获取子进程退出状态. 这时, 子进程就成为了一个只有着一个空

task_struct(只保存着退出状态, 以及一些统计信息). 而没有真正的灵魂, "有的进程活着, 但它已经死了", 此时这个子进程就处于僵

死状态.  

当然,内核也可以将这些信息保存在别的地方,而将task_struct释放掉,以节省一些空间。但是使用task_struct结构更为方

便,因为内核中已经建立了从pid到task_struct查找关系,还有进程间的父子关系。释放掉task_struct,则需要建立一些新的数据

结构,以便让父进程找到它的子进程的退出信息。父进程可以通过wait系列的系统调用(如wait4,waitid)来等待某个或某些子进

程的退出,并获取它的退出信息. 然后wait系列的系统调用会顺便将子进程的尸体(task_struct)也释放掉. 前面说到, 子进程在退出

时操作系统会通知父进程获取退出码, 即内核会给其父进程发送一个信号,通知父进程来收尸. 这个信号默认是SIGCHLD,但是

在通过clone系统调用(fork()和vfork()就是封装了clone)创建子进程时,可以设置这个信号 .

僵尸进程

处于僵死状态的进程称之为僵尸进程.

下面代码

#include <stdio.h>
#include <stdlib.h> 
#include<unistd.h>
int main() {
    pid_t pid = fork();
    if(pid < 0){ 
        perror("fork"); 
        return 1; 
    }
    else if(pid == 0){
        printf("child[%d] is begin Z...\n", getpid());
        sleep(5); 
        exit(EXIT_SUCCESS);  
    }
    else{
        printf("parent[%d] is sleeping...\n", getpid());
        sleep(10); 
    }
    return 0; 
}

 

可以看到,  在父进程处于S状态时, 没有获取子进程退出状态, 子进程就变成了僵尸进程.

僵尸进程危害

  • 子进程先于父进程终止,而父进程又没有调用wait或waitpid, 此种情况子进程进入僵死状态,并且会一直保持下去直到系统重启. 内核只保存进程的一些必要信息在task_struct中以备父进程所需.
  • 那一个父进程创建了很多子进程,要是都不回收,就会造成内存资源的浪费, 因为task_struct(PCB)数据结构对象本身就要占用内存,要在内存的某个位置进行开辟空间, 这样就会造成资源泄漏.
  • Linux操作系统最大进程程数是有限制的, 如果僵尸进程过多, 会导致进程数过多, 创建不了新的进程.

如何避免僵尸进程

  1. 让要退出的进程的父进程用wait()阻塞等待子进程退出并回收,  或用waitpid() 隔段时间来查询子进程是否结束并回收 .
     
  2. 采用信号SIGCHLD通知处理,并在信号处理程序中调用wait()函数
    具体方法 戳链接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103659853
  3. 让僵尸进程变成孤儿进程,由init进程回收,就是让父进程先退出

wait()和waitpid()详解在另一篇中,戳链接( ̄︶ ̄)↗https://blog.csdn.net/qq_41071068/article/details/103302883

还是上面的代码. 稍作修改, 如下 :

#include <stdio.h>
#include <stdlib.h> 
#include <unistd.h>
#include<sys/wait.h>
int main() {
    pid_t pid = fork();
    if(pid < 0){ 
        perror("fork"); 
        return 1; 
    }
    else if(pid == 0){
        sleep(5); 
        exit(EXIT_SUCCESS);  
    }
    printf("父进程被wait()阻塞, 等待子进程退出并回收\n");
    wait(NULL);
    return 0; 
}

孤儿进程

那么如果父进程先退出了呢, 谁又来给子进程“收尸”?当一个父进程进程退出的时候,会将它的所有子进程都托管给别的进程(使

之成为别的进程的子进程. 托管给谁呢?可能是退出进程所在进程组的下一个进程 (如果存在的话 ), 或者是1号进程。所以每个进

程每时每刻都有父进程存在. 除非它是1号进程. 

1号进程: pid为1的进程, 又称init进程. linux系统启动后, 第一个被创建的用户态进程就是init进程.

它有两项使命:

1、执行系统初始化脚本,创建一系列的进程(它们都是init进程的子孙)

2、在一个死循环中等待其子进程的退出事件,并调用waitid系统调用来完成“收尸”工作 ;
     init进程不会被暂停, 也不会被杀死(这是由内核来保证的). 它在等待子进程退出的过程中处于S状态,  “收尸”过程
     中则处于R状态.

如下代码

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
    pid_t pid = fork();
    if(pid < 0){
        perror("fork");
        exit(-1);
    }
    else if(pid == 0){
        printf("子进程睡一会\n");
        sleep(10);
        printf("子进程退出\n");
    }
    else{
        printf("父进程先走一步\n");
        exit(0);
    }
    return 0;
}

 

在子进程睡眠的这10秒内, 可以看到子进程被1号进程收养, 子进程退出后, 由1号进程回收.

发布了223 篇原创文章 · 获赞 639 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/qq_41071068/article/details/103231310