Linux中进程的属性:状态
一、通用OS进程中的各种状态与相关概念
1.1通用进程中的状态
CPU执行进程代码,不是把进程执行完才开始执行下一个,而是给每个进程预分配一个“时间片”, CPU基于时间片进行轮转调度(每个CPU分别进行)
其中发涉及到的几个进程的状态在不同操作系统中各不相同,但可以把他们归类为图中几种:
①创建/就绪/运行状态(对于一个进程来说往往没有明显区别)
②阻塞状态
③中止状态
1.2并行与并发的概念
CPU的功能十分强大,即使有多个进程,也可以在CPU数量仅有几个的情况下,通过并行和并发来执行
①并发:多个进程在一个CPU下采用进程切换的方式,在一段时间内让多个进程同时推进
②并行:多个进程在多个CPU下分别,同时进行
1.3时间片的概念与分时操作系统,实时操作系统
时间片是应用于分时操作系统当中的,分时操作系统的特点是:
①给每一个任务分别分配一个时间片
②调度任务尽量公平的分配(即时间片尽量贴近)
类似于Linux和Windows这种民用级别操作系统,都采用分时操作系统
与之相对的是实时操作系统:
如汽车上的操作系统,在播放音乐和紧急刹车这两个任务间,紧急刹车必须要是最高优先级,不能再尽量公平了
1.4运行状态
1.4.1分时操作系统的大致原理
首先操作系统在启动电脑时就会加载到内存中
每个CPU都会对应一个runqueue队列的对象,对象中存有PCB的头节点(这里以Linux中的task_struct为例),在不考虑优先级问题时,遵循FIFO算法
1.4.2FIFO算法的具体实现
当一个新的进程进入的时候,会形成一个task_struct,之后链入到runqueue对象的task_struct* head中
CPU在调用时,每次把head直接指向的task_struct对象拿出来,让head指向它的next,它本身进入CPU中进行运行,运行时间片结束后链入到最后一个task_struct的下一个
1.4.3什么时候进程是运行状态
通过FIFO算法说明:当对应进程在运行队列当中,该进程的状态就叫运行状态
可以理解为已经准备好了,可以随时被CPU调度
1.5阻塞状态
1.5.1操作系统对于底层硬件的管理:device链表
OS要管理底层硬件,依旧是遵循“先描述,后组织”的原则,在OS内部创建一个名为device的结构体对象,用来表示每个硬件的情况
在内存中,各个device像带头链表一样连接起来,其属性中的 type标识了是何种硬件,status则表示当前硬件是否正常工作,如此一来,再间隔一层驱动层就可以对硬件进行直接管理
1.5.2什么时候进程处于阻塞状态
当一个进程task_struct的代码中出现了scanf()等需要从键盘或其他硬件读取数据的语句,CPU会停止运行,并且不会把它链入runqueue队列中,取而代之的是链入device的一个成员变量task_struct* wait_queue下(根据1.5.1中图示device的第四个成员变量)
如果有多个需要读取的进程,会按照队列继续向下链入
这些在硬件的wait_queue队列中等待的进程,称之为阻塞状态
1.5.3阻塞状态回归运行状态的过程
在OS接收到如键盘等硬件输入的信息后,就会把这一进程的阻塞状态改为运行状态,并且将其从waitqueue头删,链入到runqueue队列中
在该进程再次进入CPU中时,就已经获取了键盘的写入数据,并继续接下来的程序
运行和阻塞的本质:让不同进程处在不同的队列中
1.5.4阻塞(又名等待)的本质是什么
连入目标外部设备,CPU不调度
1.6挂起状态
1.6.1出现的契机
处于阻塞状态的进程不会被CPU调度,但是他的代码和数据还留在内存中占用空间;
此时如果内存空间严重不足,那就可以考虑暂时将这一部分代码和数据移到硬盘中管辖,需要使用时再调回来
1.6.2意义
在内存资源严重不足的时候,给操作系统一些空间,本质上是用时间换空间(因为换入换出的本质是IO,速度很慢,实际应用时不多见)
1.6.3阻塞挂起状态
所谓挂起状态,就是在磁盘中一块称为“swap分区”的位置,把内存中的代码和数据换出到磁盘的相应位置,换出后,因为一定是在阻塞状态下才有机会进入挂起状态,此时进程的状态称为“阻塞挂起状态”
回归运行:
当硬件中获取到数据,OS会先把状态改为运行,再从内存中换出代码和数据,然后再连入运行队列
二、Linux中进程的状态
2.1底层中状态定义的方式与各类状态
在Linux中,是用task_struct中的属性int status
通过一个类似于#define的方式将整形定义为为各类状态
R(running):0
S(sleeping):1
D(disk sleep):2
T(stopped):4
t(tracing stop):8
X(dead):16
Z(zombie):32
①R
就是普通的运行状态
②S
是sleeping的缩写,为阻塞等待状态(可中断睡眠;又称浅睡眠,可以被kill杀进程)
③D
是disk sleeping磁盘睡眠的缩写,为阻塞等待状态的一种(不可中断睡眠;又称深睡眠)
④T
是stopping的缩写,也属于阻塞
⑤t
是tracing stop追踪暂停的缩写,当进程被追踪的时候(如gdb中断点停下,进程状态为t)也属于阻塞
⑥X
是dead的缩写,死亡状态
⑦Z
是zombie的缩写,僵尸状态
2.2D状态
disk sleeping,即磁盘睡眠
因为磁盘负责存取数据,这很重要,所以操作系统中的进程在磁盘当中等待的状态比较特殊,它的出现是因为要向磁盘当中写入数据是做IO操作,需要花很多时间
而且这段时间进程不能出任何问题,否则会造成数据的丢失,所以单独设置了一个深睡眠防止被杀进程
注:D状态大多的情况下是瞬时的状态,不会被查到;因为如果查到,那么大概率是磁盘出现了一些问题,如老化,空间不足等。
2.3S状态与S+状态的区别
他们的区别主要在是否是后台进程上:一般一个可执行程序启动后默认都是前台程序,状态对应S+;后台进程则对应状态S
实例:
假如运行起来一个可执行程序mycode
我们可以通过
./+[可执行程序] &
来启动后台进程
效果
查询结果:
后台进程的特点是什么?
无法被ctrl+C杀进程,只能通过kill -9来结束程序
前台进程:
后台进程:
用kill -9杀进程后:
2.4T状态的主动设置与取消以及其影响
T状态的设置有两种方式:
①当进程做了非法但是不致命的操作,被OS暂停
②可以利用kill的选项指定一个进程进入T状态
第19号选项SIGSTOP,可以使用第18号选项SIGCONT将进程继续
用法:
kill -19 +[PID]
2.4补:主动设置T状态后取消会影响到进程的前后台属性
主动设置T状态后再继续,会自动将前台程序切换到后台
2.5X状态与进程死亡
2.5.1进程退出时的返回信息
要了解进程退出时的返回顺序,那么必须要明确:进程为什么会被创建
进程被创建是为了完成用户的任务
既如此,在进程退出时,我们必须要明确任务是否被完成,那么该怎么做呢?
实际上,我们通过进程的执行结果,告知父进程/OS任务的完成情况
那么我们该如何查看进程的执行结果呢?
可以通过指令
echo $?
查看最近程序退出时的退出信息(例如ll之后运行,就会展示ll指令是否成功)
注:程序正常完成任务返回0,其余情况返回非0
2.5.1补:“程序正常完成任务返回0,其余情况返回非0”这种说法的实例
我们在写main函数的时候,会在最后一行写上return 0; 其实这一个返回值就是返回给OS进程的执行结果,返回0就是程序正常执行完毕
如果我们刻意main函数写返回10,$?结果也会变为10
2.6死亡状态X与僵尸状态Z
2.6.1通过现实例子理解二者对于进程结束的影响
正如在苹果从树上掉下腐烂后,我们研究它落地时间最后扔掉它的过程
苹果在树上成熟,它落地后腐烂,直到我们发现并对它进行检测研究结束,它的腐烂时间这一数据就被记录了下来,最后他才会被扔掉
这个过程类比就是:
在树上生长就相当于进程在运行
从树上落下相当于运行结束
结束后被我们发现并研究出腐烂时间,类比于“维持退出信息,方便父进程/操作系统来进行查询”
从掉下到研究完毕,即从进程结束到维持完退出信息,称之为僵尸状态
扔掉后,即进程死亡状态
2.6.2进程退出时发生的事情
进程=内核数据结构(task_struct)+代码和数据
①进程退出时,代码就不会再被执行了,首先可以立即释放的就是程序代码和数据
②进程退出时需要有退出信息,这些信息就保存在自己的task_struct中
③虽然退出,但它的task_struct依旧要被OS维护起来,方便用户未来获取进程退出信息
2.6.3退出信息的存储位置:task_struct中
task_struct中存储内容之一就是“进程退出信息”,对应的形式是int类型,名为exit_code
以及一些其他的信息
2.6.4进程创建/释放时,内核数据结构和代码数据出现的先后顺序
进程创建:先创建内核数据结构task_struct(没加载代码数据之前称为新建状态),再创建代码和数据
进程释放:先释放代码和数据,此时称之为僵尸状态,task_struct暂时被维护起来
2.6.5结合例子观察僵尸状态(死亡状态为瞬时状态,无法观察)及系统层面的内存泄漏
首先需要一个代码:可以展示父子进程同时进行,且子进程进行十次以后停止,但父进程一直运行
4 int main()5 {6 printf("父进程开始执行;我的PID:%d,我的PPID:%d\n",getpid(),getppid());7 8 pid_t pi=fork();9 if(pi==0)10 {11 int cnt=10;12 while(cnt > 0)13 {14 printf("我是子进程,我的PID:%d,我的PPID:%d,当前cnt:%d\n",getpid(),getppid(),cnt);15 sleep(1);16 cnt--;17 } 18 }19 else {20 while(1)21 {22 printf("我是父进程,我的PID:%d,我的PPID:%d\n",getpid(),getppid()); 23 sleep(1);24 25 }26 }27 28 29 return 0;30 }
我们编辑出一个名为“status”的可执行程序
为了可以实时监控进程的状态,我们需要使用“循环执行指令”的方式(暂不展开讲述循环方法)
while :;do ps axj |head -1;ps axj | grep status;sleep 1;done
执行结果:
在刚开始执行./status程序时,进程的状态一直是“S+”
在子进程循环十次以后,状态发生了改变,成为了“Z+”
从这个结果我们可以看出:子进程执行完了以后,程序进入僵尸状态;这个状态是为了维护自己的task_struct,方便未来父进程读取退出状态(父进程不读,子进程PCB不退),对于此时的子进程来说,OS和父进程都不会直接回收他
其中“defunct”意为“失效的”,即表示进程已经开始退出
系统层面的内存泄漏:
在例子中,如果没有人管子进程,他会一直处于僵尸状态,而task_struct是一个不小的对象,他一直在消耗内存,这就是内存泄漏(系统层面)
解决:
一般需要父进程主动读取子进程退出信息,子进程就会自动退出
2.6补:我们malloc/new出来的空间会随着进程结束而自动释放吗(语言层面内存泄漏)
会的,malloc/new出来的空间属于“代码和数据”中的“数据”这部分,进程退出,代码和数据自动释放
所以内存泄漏(语言层面)主要怕一直不退的常驻进程,这样new出来的空间一直不还
2.7孤儿进程
2.7.1对比2.6.5中的例子,理解孤儿进程的出现
父在子退会出现僵尸进程,而父退子在就会出现孤儿进程
修改例子中的代码:
4 int main()5 {6 printf("父进程开始执行;我的PID:%d,我的PPID:%d\n",getpid(),getppid());7 8 pid_t pi=fork();9 if(pi==0)10 {11 int cnt=10;12 while(cnt > 0)13 {14 printf("我是子进程,我的PID:%d,我的PPID:%d,当前cnt:%d\n",getpid(),getppid(),cnt);15 sleep(1);16 17 }18 }19 else {20 int cnt=10;21 while(cnt > 0)22 { 23 printf("我是父进程,我的PID:%d,我的PPID:%d,当前cnt:%d\n",getpid(),getppid(),cnt);24 sleep(1);25 cnt--;26 }27 }28 29 30 return 0;31 }
我们让父进程只执行10次,而子进程无限循环
效果:
我么不难发现:子进程的PPID变为了1,那么这里的1究竟是什么呢?
2.7.2利用top可以快速查看当前所以进程,寻找PID为1的是谁
从中可以读出:PID为1对应的是“systemd”,即内存中加载的操作系统
父进程退出后,子进程会被“系统”(部分OS下名为initd)自动领养
2.7.3孤儿进程的影响:转到后台
被“领养”的子进程会自动装到后台去运行,它不能被ctrl+c杀进程,需要kill -9来进行中止