进程状态:
❓假设我们在上课,在B站上上课,请问我们的B站是不是一直运行呢?💡不是的!
❓假设我们同时打开了B站和PDF阅读器时,是怎么运行的呢?
💡每一个进程在CPU跑一会,再从CPU拿下来放上另外一个上去,周而复始,这种叫做
分时操作系统
❓那就有一个问题,假设先用B站,为什么先将B站这个进程放上去,而不是PDF阅读器呢?
💡这就取决于进程状态!
思维导图:
1.进程的状态
1.1三种基本运行状态
- 运行状态
🔥R运行状态(running): 并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里
。
❓一个进程只要把自己放到CPU上开始运行了,是不是一直要到执行完毕,才把自己放下来?
💡不是,每一个进程都有一个叫做:时间片的概念! 其时间大概是在10 ms左右。所以并不是一个进程一直在执行,而是这多个进程在一个时间段内所有代码都会被执行 —— 这就叫做【并发执行】
所以呢这就一定会存在大量的进程被CPU放上去、拿下来的动作 —— 这就叫做【进程切换】
- 并发and并行
🔥并行:指的是两个或者多个事件同一时刻发生(可以类比为一个公路,三台车不同道同向行驶)
🔥并发:指的是一段时间内有多个程序同时运行
- 就绪状态
🔥指的是进程已经处于准备好执行的状态,但是CPU没有空闲的,
- 阻塞状态和挂起状态
有两个概念:阻塞和挂起!
阻塞:进程因为等待某种条件就绪 ,而导致的一种不推进的状态——进程卡住了——阻塞一定在等待某种资源
就相当于进程太多了,卡着一个进程,在这个进程来看就是在等待CPU调度(某种资源)
🔥阻塞:进程等待某种资源就绪的过程
❓为什么阻塞呢?
💡进程要通过等待的方式,等具体的资源被别人是用使用完之后,再被自己使用。
❓什么是资源呢?💡资源=磁盘或者网卡显卡等各种外设(这是硬件,软件也有)
因为操作系统是通过先描述,后组织的方式来进行管理,假设用struct来代表这些外设用链表连接起来,
❓可以存在大量的进程吗?💡可以的❓要管理吗?💡先描述再组织
假设我们有一个进程task_struct要加载到cpu中,它在等待一个网卡资源
CPU说你不能在我上面跑了,所以task_struct就添加到这个队列的尾部等待网卡这个资源
也就是我们的task_struct在我们的列入到某种资源的队列当中,它就不会被CPU调度了吗,这不就是卡住了吗,不就是阻塞了吗!!!!
🔥pcb可以被维护在不同的队列中的!
阻塞:阻塞就是不被调度——一定是因为当前进程需要等待某种资源就绪——一定是进程task_struct结构体需要再某种被OS管理的资源下排队
挂起:
📚例子
假设task_struct是个下载任务,这里突然没网了,CPU说这个task_struct就不能跑了,task_struct就链接在网卡这个队列的中,这里就是阻塞了
操作系统通过自己的一套算法,把占有内存的,不被调度的进程的数据和代码交换到磁盘当中
磁盘那边放了一份交换后的代码和数据,这里的内存的代码和数据被释放,
在被再次调度前再把代码和数据换回来就可以了
🔥其中代码和数据由操作系统暂时将我们的从内存换到磁盘的过程中,此时这个进程就是挂起状态,全称叫做阻塞挂起状态
2.Linux进程状态
task_struct是一个结构体,内部会包含各种属性,包括状态(status)
为了研究 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):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
用0表示R,用1表示S,用2表示D,用4表示T,用8表示t,用16表示X,用32表示Z
- 运行状态
❓进程只要是在R状态,就一定是在CPU上运行吗?💡不一定!
所有运行的进程只要在运行的队列中排队就可以了,所以CPU调度进程只要去我们的队列中去挑进程就可以了
❓进程是什么状态?💡一般也看这个进程在哪里排队
进程状态查看我们可以用ps
这里我们也能拿 ps 指令来看进程的状态,我们开小窗输入:
ps axj | head -1 && ps axj | grep process | grep -v grep
显然这里S是休眠状态
🔥首先需要声明一点,状态后面跟加号,表示是一个 **前台进程,**你只需要知道的是,能够在键盘上 Ctrl+c 暂停的都可以叫前台进程。
其实我们CPU的运行速度都很快,我们看到这里它一直在打印,其实是显示器(外设)一直在打印,CPU一瞬间就完成了,所以状态是S
我们将test.c中printf注释掉,再试一下
我们发现这里变成了R状态了
所以这个进程不访问任何资源,只等你 CPU,只要你被运行期间不访问外设,就不会被阻塞。
不访问外设,那么死也会在等待队列里,一直在等待队列中,这就让 process 达成 R状态!
🔥这里printf的本质就是向外设(网卡、显示器)打印消息,进程只要是R状态,并不直接代表进程正在运行,而代表该进程在运行队列中排队
🔥队列由操作系统自己维护
- 睡眠状态
S状态——休眠状态,可中断休眠,它一直在等待某种资源,其实就是阻塞状态
🔥我们一般把 状态叫作 浅度睡眠,也叫做 可中断睡眠。
- 顾名思义,当进程处于 S 状态,它可以随时被唤醒。
- 不仅仅是操作系统可以唤醒,你也可以唤醒,甚至你想杀掉它都行。
$ kill -9 [pid]
我们用另外一个例子来更直观感受S状态
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int a=0;
printf("输入a的值:");
scanf("%d",&a);
printf("a=%d\n",a);
return 0;
}
将该进程运行起来我们可以看到其是出于S+
的状态,因为【shell】此时正在等待用户的输入,这个就对应到了我们上面所讲到的 阻塞状态
🔥不仅如此,像我们一直在使用的bash
,也可以算是一种【阻塞状态】,一直等待着我们去输入命令行,一旦有了的话它就进行读取
- 深度睡眠模式
这也是一种阻塞模式
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
这里D模式比S模式多一个disk,磁盘?那肯定跟磁盘有关系
一般而言,在 Linux 中,如果我们等待的是磁盘资源,我们进程阻塞所处的状态就是 D 状态。
与S状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。
绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发现,kill -9
竟然杀不死一个正在睡眠的进程了!于是我们也很好理解,为什么ps命令看到的进程几乎不会出现D状态,而总是S状态。
🔥 而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。
- 暂停状态
T状态,暂停状态
我们先通过这个指令来了解一下,我们要使用的就是下面的18和19信号
kill -l
进程暂停与进程休眠(阻塞) 没有关系,只是单纯不想让这个进程跑了。
比如有些进程在执行任务时,用户想让这个进程暂停一下,这其实很好理解。
比如看视频,听音乐,下载,都会有暂停。当你点击暂停的时候下载对应的代码就不跑了,此时这个进程你就可以认为是暂停状态。
kill -19 进程pid 暂停该进程
kill -18 进程pid 解除暂停
kill -18 pid解除一下
- 死亡状态
X状态,死亡状态
kill -9 PID
dead 代表死亡,所以 X状态对应的就是 死亡状态。
这个没有什么好说的,X 状态的进程就代表死亡了,可以随时等待 OS 来收尸了。
- 僵尸状态
当一个 Linux 中的进程退出的时候,一般不会直接进入X状态(死亡,资源可以立马被回收),而是进入Z状态
❓当进程退出后不直接变成X状态而是变成Z状态这是为什么呢?
💡当一个进程退出的时候,那最关心它的便是【父进程】。因为这个父进程费了很大的劲才将这个子进程
fork
出来,此时呢它突然挂掉了,那么此时父进程就必须去关心一下对应的子进程退出时的原因
在这个退出过程中,进程占有的所有资源将被回收,除了task_struct结构(以及少数资源)以外。于是进程就只剩下task_struct这么个空壳,故称为僵尸。
之所以保留task_struct,是因为task_struc t里面保存了进程的退出码、以及一些统计信息。而其父进程很可能会关心这些信息。
我们来造一个僵尸出来看看:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) {
pid_t id = fork();
if (id == 0) {
// child
int cnt = 5;
while (cnt) {
printf("我是子进程,我还剩下 %ds\n", cnt--);
sleep(1);
}
printf("我是子进程,我已经变僵尸了,等待被检测\n");
exit(0);
}
else {
// father
while (1) {
sleep(1);
}
}
}
❓如果长时间处于僵尸状态,会有什么结果呢?
💡如果没有人收尸,该状态会一直维护,该进程的相关资源 (task_struct) 不会被释放!内存泄露!一般必须要求父进程进行回收,如何回收的问题我们会在进程控制章节讲解。
❓为什么要main()的返回值呢?💡return 0;这个是进程退出码
🔥如果-个进程退出了,立马X状态,立马退出,你作为父进程,有没有机会拿到退出结果呢? ? ? Linux当进程退出的时候,- -般进程不会立即彻底退出,而是要维持一个状态叫做Z, 也叫做僵尸状态— 方便后续父进程(OS)读取该子进程退出的退出结果!
只要父进程不退出,这个僵尸状态的子进程就一直存在。那么如果父进程退出了呢,谁又来给子进程“收尸”?
当进程退出的时候,会将它的所有子进程都托管给别的进程(使之成为别的进程的子进程)。托管给谁呢?可能是退出进程所在进程组的下一个进程(如果存在的话),或者是1号进程。所以每个进程、每时每刻都有父进程存在。除非它是1号进程。
1号进程,pid为1的进程,又称init进程。
linux系统启动后,第一个被创建的用户态进程就是init进程。它有两项使命:
1、执行系统初始化脚本,创建一系列的进程(它们都是init进程的子孙);
2、在一个死循环中等待其子进程的退出事件,并调用waitid系统调用来完成“收尸”工作;
init进程不会被暂停、也不会被杀死(这是由内核来保证的)。它在等待子进程退出的过程中处于S状态,“收尸”过程中则处于R状态。
- 孤儿进程
如果父进程 一直存在,子进程退出了,父进程对子进程不管不顾,子进程退出后就要进入僵尸状态
如果有父子两个进程,因为一些问题父进程不存在了,子进程还没有退出呢?
myproc.c
1 #include<stdio.h>
2 #include<unistd.h>
3 int main()
4 {
5 pid_t id=fork();
6 if(id==0)
7 {
8 //child
9 while(1)
10 {
11 printf("我是子进程:pid:%d,ppid:%d\n",getpid(),getppid());
12 sleep(1);
13 }
14 }
15 else
16 {
17 //parent
18 int cnt=10;
19 while(1)
20 {
21 printf("我是父进程:pid:%d,ppid:%d\n",getpid(),getppid());
22 sleep(1);
23 if(cnt--<=0)break;
24 }
25 }
26 return 0;
27 }
make一下,没有问题
这里的$@
和$^
$@代表冒号左侧的,红色的,目标文件
$^代表冒号右侧的,蓝色的,依赖文件
while :;do ps ajx | head -1&&ps ajx |grep myproc | grep -v grep;sleep 1;echo "----------";done
我们在旁边把程序运行起来
我们发现我们的父进程是5388 ppid 7666
子进程 5389 ppid5388
后面父进程中止了,只剩下子进程
子进程是5389 ppid变成了1
父进程这时候退出了那是不是应该处于僵尸状态呢?
这里我们为什么没有看到父进程处于僵尸状态呢?
我们在命令行中启动一个进程时,它的父进程是bash
父进程退出之后,它的父进程是bash(命令行解释器),所以这个父进程退出的时候,它的父亲帮他回收了
相当于是我死了,我爹bash帮我回收了
我们发现这个子进程的父进程死掉后,它给自己又找了个爹(1号进程),1好进程可以叫为操作系统
如果不领养会发生什么呢?
如果有一天这个子进程挂了,并且他没有父进程,他就会被操作系统遗忘,就一直处于一种占用操作系统内存空间的情况,也就是内存泄漏