前言
前面我们讲述了进程的基本概念以及如何描述、组织、查看进程,如何使用fork函数创建子进程等内容,本篇将讲述进程的各种状态。
先看看Linux内核源代码怎么说
下面的状态在kernel源代码里的定义:
/* * 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 */ };
认识进程状态
在我们讲述Linux内核中的进程状态之前,我们先在看一下网上是怎么个样子。
如下:
我们依次来解释一下它的含义
新建
新建:字面意思,就是创建以后还没有进入运行队列(如fork后)
阻塞
阻塞:我们都知道进程的代码数据在内存中,但是系统还有其他资源,如网卡、磁盘等…所以系统不止有一种队列,不同的队列速度不同,所以就会有时间差,阻塞状态就是CPU等待非CPU资源就绪的状态。
我们来验证一下:
比如有下面代码:
#include<stdio.h> int main() { int a = 0; scanf("%d",&a); return 0; }
这个等待输入的状态就是阻塞状态
运行
比如之前讲的task_struct结构在队列中排队就叫做运行态
挂起
和阻塞有点类似,但是它的区别是挂起状态会将进程的代码和数据换出到磁盘上,而阻塞时的进程代码数据仍在内存。
终止
它是X和Z两种状态的综合体,具体之后再讲~
进程状态查看命令
ps aux / ps axj 命令
Linux内核中的进程状态
R
R即表示运行态,并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
测试代码:
#include <stdio.h> #include <sys/types.h> #include <unistd.h> int main() { while(1) { } return 0; }
运行结果:
S
阻塞态,意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
我们都知道进程的代码数据在内存中,但是系统还有其他资源,如网卡、磁盘等…所以系统不止有一种队列,不同的队列速度不同,所以就会有时间差,阻塞状态就是CPU等待非CPU资源就绪的状态。
如下图:
这并不是真的S状态,为什么这么讲?因为你访问了外设,一旦访问外设的话,这个外设不一定说是立马给你准备好,即便是给你准备好了,其实你也要有一个让他那么获取你数据的过程。然后其实外设就比较慢,要等的话,其中你的进程状态大部分都是 s,所以你看到当前,它的状态就是 s 状态。
D
磁盘休眠状态(Disk sleep)有时候也叫不可中断睡眠状态(uninterruptible sleep),它不可被中断、不可被动唤醒,在这个状态的进程通常会等待IO的结束。
S与D的区别:
- 它们都是睡眠状态,几乎等价,但是S是可中断的,D不可中断。
那么D的意义是什么?
在操作系统中,当内存不足时,会自动杀死一些不用的进程,但是有些进程一旦终止,便可能造成数据丢失,操作系统就需要知道哪些不能被自动杀死,于是在S的基础上就有了D状态。
T
停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
要注意和阻塞态的区别,它不用等待资源,比如打断点时。
测试断点:
上图中的 t 就是暂停状态。
X
死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态,它的作用是供给操作系统回收的,因为操作系统不会每时每刻都回收,而是过一段时间回收一次来提高效率。
到现在为止,我们对进程的概念停留在两个方面:
一、必须把自己的可执行程序和代码加载到内存中。
二、操作系统为了管理众多进程,它必须给每个进程都创建对应的PCB。
接下来我们来讲解两种特殊的进程,僵尸进程和孤儿进程。
Z
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程(使用wait()系统调用,后面讲)没有读取到子进程退出的返回代码时就会产生僵死(尸)进程
僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程或者操作系统来进行回收。
只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态
我们可以这样理解它,僵尸进程可理解为他们的代码和数据已经被释放掉,但PCB没有释放。
eg:假如有一个恶意程序。运行后就是让子进程退出,但父进程不回收他这样就会导致僵尸问题,让操作系统速度卡顿
验证代码:
#include <stdio.h> #include <unistd.h> int main() { pid_t pid=fork(); if(pid<0) { perror("fork"); return 1; } else if(pid==0) { while(1) { printf("I am child,pid:%d,ppid:%d\n",getpid(),getppid()); sleep(3); break; } exit(0); } else { while(1) { printf("I am father,pid:%d,ppid:%d\n",getpid(),getppid()); sleep(1); } } printf("you can see me!\n"); return 0; }
运行结果:
查看状态:
图中的Z就是僵尸状态,子进程已退出,但父进程还在运行,处于等待回收的被检测状态。
R+/S+/T+中的+是什么意思?
- 带 + 为前台任务,一旦开始,就会占用bash对话框执行命令就没有任何效果了,而且可以被Ctrl + c 终止。
- 如果要让它在后台运行,则使用./mycode & [任务号] pid,此时bash就看不到它的运行了,变成了R状态,终止命令:kill -9 pid,暂停命令:kill -19 pid,继续命令:kill -18 pid
僵尸进程危害
进程的退出状态必须被维持下去,因为他要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。
可父进程如果一直不读取,那子进程就一直处于Z状态?
是的!
维护退出状态本身就是要用数据维护,也属于进程基本信息,所以保存在task_struct(PCB)中,
换句话说,Z状态一直不退出,PCB一直都要维护?是的!
那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?
是的!因为数据结构对象本身就要占用内存,想想C语言中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
是否会内存泄漏?是的!比如。 C语言写的程序中malloc后的资源不回收,此时导致的僵尸问题就是内存泄漏。
孤儿进程
父进程如果提前退出,那么子进程后退出,进入Z之后,那该如何处理呢?
父进程先退出,子进程就称之为“孤儿进程”
我们知道子进程一般由父进程回收,但是如果是上面这种情况,孤儿进程会被1号init进程领养,当然要由init进程回收喽。
为什么要被领养?
当子进程退出时,父进程已不在,需要由领养进程来进行回收。
验证一下:
#include<stdio.h> #include <unistd.h> int main() { pid_t id = fork(); if(id==0) { //child while(1){} } else { int cnt=5; while(cnt) { cnt--; sleep(1); } } return 0; }
ps axj | head -1 && ps ajx | grep mycode ps axj | head -1 && ps ajx | grep 2194
在一些较新的Linux发行版中,引入了--user选项,该选项会创建一个用户级的systemd实例,用于管理用户级别的服务和进程。
这个用户级的systemd实例的进程ID可能会出现在其他进程的PPID字段中,包括孤儿进程。
虽然孤儿进程的父进程通常是1号进程,但在这种情况下,用户级systemd实例的主进程可能会被记录为孤儿进程的父进程。
后记
本篇从操作系统常见的进程状态来引入到Linux内核中的进程状态,他们中的状态有所不同,但是其内核是一样的。比如R表示运行态、S表示阻塞态、D表示磁盘休眠态、 T表示暂停状态、X表示死亡状、 G表示相似状态、他们组合起来,分别对应的阻塞、运行、挂起、终止这些状态。我们还讲述了在我们查看进程状态时看到的加号是什么意思?以及讲述了两种特殊的进程状态,僵尸进程和孤儿进程。