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)
我们目前认为:T
和 t
状态没有区别!
在进程状态中的 R
状态中,我们学会了用 kill
命令杀掉一个进程!现在我们再来学习如何使用 kill
命令暂停和运行一个进程!
kill -l #列出所有信号
(信号部分的原理我们后期会详细讲解,这里学会怎么使用就行啦)这里面有两个信号:SIGSTOP
和 SIGCONT
分别用来暂停,和运行一个暂停的进程!一个进程被暂停之后,我们就能看到 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
就会一直占用内存资源,造成内存泄漏。
父进程获取子进程退出信息的过程称为进程等待!这个知识点后面会详解!
父进程直接创建了子进程嘛,子进程要退出了,父进程肯定要关心关心撒!父进程创建子进程就是让子进程来办事的嘛!
我们可以写一个代码来验证,子进程退出的时候,父进程不获取子进程的退出状态,那么子进程就会处于僵尸状态的结论:让子进程循环打印 pid
和 ppid
三秒之后退出循环,退出循环之后结束子进程。父进程打印自己的 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 进程领养了子进程,但是系统中没有爷爷进程等待孙子进程的逻辑,即使领养了也没用。但是操作系统就不一样了,操作系统是所有进程的管理者,能够直接在内核层面回收!