目录
一、进程状态
1.一般操作系统学科的进程状态
二、Linux操作系统的进程状态
运行状态(R)
睡眠状态(S)
深度睡眠状态(D)
暂停状态(T)
追踪暂停状态(t)
僵尸状态(Z)
死亡状态(X)
总结:
三、两种特殊的进程
僵尸进程
孤儿进程
四、僵尸进程的处理回收
五、进程的优先级
什么是优先级
为什么要有优先级
Linux中优先级的特点
top命令更改nice
六、进程切换(并发性)
七、其他概念
前言
本文用于详解linux中进程状态、僵尸进程/孤儿进程的回收处理、进程优先级、进程切换和进程的几种特性。
一、进程状态
1.一般操作系统学科的进程状态
什么是进程状态:我们知道,一个程序被加载到内存变成进程之后,操作系统要对该进程进行管理,即为其创建对应的PCB对象;而进程状态,本质上就是PCB内部的一个整形变量,不同的整形值就对应不同的进程状态。
在普适的操作系统层面,即站在操作系统学科的角度来说,进程状态可能有如下几种:运行、挂起、阻塞、新建、就绪、等待、挂机、死亡;
其中最重要也是最难理解的几种状态分别是:运行、阻塞、挂起。
运行状态
操作系统为了合理分配CPU以及各种硬件资源,也为了更好的调度各个进程,会为CPU创建一个进程队列,为每一个硬件都创建一个运行队列/等待队列,而进程在不同的队列中,就会表示不同的状态。所以让某一个进程处于运行状态本质上就是将该进程对应的PCB放入CPU的运行队列中,然后再将PCB中维护进程状态的变量修改为相应的值。
因为进程PCB里面有进程的各种属性,以及进程对应的代码和数据的地址,所以CPU从运行队列中取出PCB后,可以根据该PCB来得到进程的各种数据和指令,然后执行相应运算;
所以进程处于运行状态并不一定意味着该进程此刻正在被运行,只要该进程处于CPU的运行队列中即可。(注:CPU是纳秒级的芯片,运算速度非常快,所以只要进程处于CPU的运行队列中,我们就可以认为该进程正在被运行)
阻塞状态
和CPU一样,我们计算机中的各种硬件也是十分有限的,但是需要使用这些硬件资源的进程却有很多,比如很多进程都需要向磁盘中写入数据,又或者要通过网卡发送数据;但是一个磁盘或者一个网卡在同一个时刻只能为一个进程提供服务,那么如果此时有其他运行中的进程需要使用该硬件资源,操作系统就会将该进程的PCB放入硬件的等待队列中,等待硬件来为我提供服务。
上面这种由于访问某种硬件需要进行等待的状态就被称为阻塞状态,阻塞状态本质上就是将进程的PCB从CPU的运行队列中剥离出来,放入硬件的等待队列中,然后将PCB中维护进程状态的变量修改为相应的值,比如1;待该进程获得对应的对应的硬件资源以后,再将该进程放入CPU的运行队列中。
通过上述做法我们能解决,CPU和硬件的速度差异。
注:并不是只有等待硬件资源进程才会处于阻塞状态,一个进程等待另一个进程就绪、一个进程等待某种软件资源就绪等都会处于阻塞状态。
挂起状态
硬件的速度较慢,但是大量的进程需要访问硬件,势必会产生较多的阻塞进程,这些阻塞进程的代码和数据在短期内不会被执行,如果全部存在于内存中将会导致内存占用。
这种由于内存空间不足,操作系统将在等待资源的进程对应的代码数据放到磁盘中以节省内存空间的状态就被称为挂起状态;挂起状态不会移动进程的PCB,只会移动进程对应的代码和数据。
注:挂起进程并不是释放进程,因为该进程对应的PCB仍然处于某硬件的等待队列中,当该进程获得对应的资源以后,操作系统仍然可以将该进程对应的代码和数据从磁盘加载到内存中来继续运行,其本质是对内存数据的唤入唤出;同时阻塞不一定挂起,挂起也不一定阻塞,也可能是新建挂起、就绪挂起,甚至是运行挂起。
总结:进程状态改变的本质是进程对应的 PCB (task_struct 对象) 处于不同设备的运行队列/等待队列中。
二、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 */
};
可以看到,Linux中进程一共有七种状态,分别是运行、睡眠、深度睡眠 (磁盘休眠)、暂停、追踪暂停、死亡、僵尸;
运行状态(R)
#include <stdio.h>
int main()
{ while(1); return 0;
}
R运行状态并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
状态后面有+号的表示前台进程,没有+号表示后台进程。前台进程在执行时,用户无法继续输入指令除非ctrl+c终止程序;后台进程在执行的过程中,用户可以输入指令且ctrl+c无法杀掉该进程。可以使用kill -9 PID的指令杀掉该进程。
睡眠状态(S)
进程的浅度睡眠状态,阻塞状态的一种,进程需要等待某种资源:
1 #include <stdio.h>2 #include <unistd.h> 3 int main() 4 { 5 int a = 0; 6 while(1) 7 { 8 printf("i am a process, %d\n", a++);9 sleep(1); 10 } 11 return 0; 12 }
注:我们使用 ps axj 指令查看进程状态只能查看进程某一时刻的状态,而外设的速度是要远远低于CPU的,所以我们可以发现,虽然 process 也在执行加法运算,但是我们每次查询时进程基本都处于阻塞状态,因为进程99%的时间都在等待硬件资源就绪,只有1%的时间在进行加法运算以及执行打印代码。
深度睡眠状态(D)
它是磁盘休眠状态,也叫不可中断睡眠状态,是阻塞状态的一种,只有在高IO的情况下才会发生(Linux中有一个dd命令可以模拟高IO的状态,可以动手试逝)。
上面我们提到,当内存空间不足的时候,操作系统会将一部分进程挂起来节省资源;但是如果内存空间严重不足,挂起已经解决不了问题的时候,操作系统就会主动杀掉某些进程;为了防止这种情况的发生,Linux设计出了深度睡眠 (D) 状态,处于深度睡眠状态的进程既不能被用户杀掉,也不能被操作系统杀掉,只能通过断电,或者等待进程自己醒来。
暂停状态(T)
可以通过发送 SIGSTOP 信号给进程来停止(T)进程,(kill -19 PID)。
这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。(kill -18 PID)
细心的同学可能注意到了一个细节:我们将 process 暂停或者 continue 之后,进程状态前面的 + 号消失了;其实,进程状态后面的 + 号代表着一个进程是前台进程,没有 + 号就代表是后台进程;对于前台进程,我们可以使用 Ctrl + c 将其终止,也可以用 kill 命令杀死它;但是对于后台进程来说,我们只能通过 kill 命令来杀死它。
追踪暂停状态(t)
追踪暂停状态是一种特殊的暂停状态,进程处于此状态表示该进程正在被追踪,比如 gdb 调试进程。
僵尸状态(Z)
我们创建一个进程的目的是为了让其帮我们完成某种任务,而既然是完成任务,进程在结束前就应该返回任务执行的结果,供父进程或者操作系统读取。
于是就出现了这种僵尸状态,僵尸状态就是进程在退出时等待父进程或者操作系统来读取退出状态代码,然后释放PCB的一种状态。
那么进程怎样进入僵尸状态?
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <stdlib.h> 5 int main() 6 { 7 8 pid_t id=fork(); 9 if(id==0) 10 { 11 //子进程 12 while(1) 13 { 14 printf("子进程, pid=%d, ppid=%d, fork_val=%d\n", getpid(), getppid(), id); 15 sleep(1); 16 exit(1);17 }18 }19 else if(id>0)20 {21 //父进程 22 while(1)23 {24 printf("父进程, pid=%d, ppid=%d, fork_val=%d\n", getpid(), getppid(), id);25 sleep(1); 26 //程序不会立即终止27 }28 }29 else30 {31 perror("fork");32 exit(-1);33 }34 return 0;35 }
在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。
当调用 exit (0) ,表示程序正常结束执行,并返回退出码 0 给操作系统。
(两个函数的详细用法,可查看下main两篇文章:
fork函数 --- 进程与PCB的关系,进程的基本操作 --- 含fork函数
exit函数 --- exit函数详解 )
在Z状态的进程被回收后,进程状态变为X死亡状态(dead):父进程读取完子进程的返回信息后,收尸速度太快了,我们看不到,进程死亡状态立马被它的父进程回收。
死亡状态(X)
死亡状态代表着一个进程结束运行,该进程对应的PCB以及代码和数据全部被操作系统回收。
总结:
可以看到,具体的Linux操作系统下的进程状态和普适的操作系统学科上进程的状态是不同的,比如Linux操作系统没有阻塞和挂起状态,阻塞状态通过睡眠、深度睡眠、暂停、追踪暂停等状态表现出来,而进程处于这些状态时是否会被调整为挂起状态,用户是不可得知的,因为操作系统没必要将挂起状态暴露给用户,用户也不关心一个进程是否会处于挂起状态。
三、两种特殊的进程
僵尸进程
上面我们提到一个进程的资源在被全部释放之前,需要由父进程或者操作系统来读取退出状态代码,而如果父进程不读取子进程的退出状态代码,该进程的PCB就一直得不到释放,此时该进程就会变成僵尸进程。
我们下面举例说明:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>int main()
{int id = fork();if(id > 0){while(1){printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}else if(id == 0){while(1){printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}}else {perror("fork fail");exit(-1);}return 0;
}
可以看到,当我们 kill 掉子进程之后,由于父进程中没有对子进程的退出状态代码进行读取,所以子进程变成了 Z 状态,并且子进程后面还提示了 defunct (失效的,不再使用的),此时,如果父进程一直不对不对子进程进行读取,那么子进程就会变成僵尸进程。
僵尸进程的危害:
所以,一个进程在退出的时候,不会立即释放全部资源 -–- 由于该进程已经不会再被执行了,所以操作系统可以释放进程的代码和数据,但是该进程的PCB会保留,因为PCB中存放着该进程的各种状态代码,特别是退出状态代码。如果父进程不回收这块资源,那么会造成系统的内存泄漏。那我能不能手动杀掉这个僵尸进程来手动释放僵尸资源?不可以,因为僵尸进程已经死亡,无法手动杀掉进程。
我们是如何避免的呢?--- 请看第四部分。
孤儿进程
孤儿进程是指父进程提前退出后,子进程被操作系统领养的一种情况,被操作系统领养的进程就被称为孤儿进程;
监控脚本命令:
while :; do ps axj | head -1 && ps axj | grep test | grep -v grep; sleep 1; done
这里还有两个细节:第一,父进程退出后并没有变成Z状态,因为父进程的父进程是bash,bash会读取父进程的退出状态;第二,子进程被领养后变成了后台进程。
四、僵尸进程的处理回收
前面提到过,在 Linux 环境中,我们是通过 fork
函数来创建子进程的。创建完毕之后,父子进程独立运行,父进程无法预知子进程什么时候结束。
通常情况下,子进程退出后,父进程会使用 wait
或 waitpid
函数进行回收子进程的资源,并获得子进程的终止状态。
如果子进程先于父进程退出,同时父进程太忙了,无瑕回收子进程的资源,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。(僵尸进程中的例子)。
但是,如果父进程先于子进程结束,则子进程成为孤儿进程。孤儿进程将被 init 进程(进程号为1)领养,并由 init 进程 对孤儿进程完成状态收集工作。(孤儿进程中的例子)。
如下图所示:
总结:
父进程终止↓
所有子进程的父 PID 被设为 1(init 接管)↓
init 进程调用 wait() 回收子进程↓
若子进程是僵尸 → 立即回收,解除僵尸状态
若子进程在运行 → 终止后由 init 回收,避免成为僵尸
五、进程的优先级
什么是优先级
优先级和权限不同,权限决定的是一件事情能不能做;优先级是在权限允许的前提下,该事情先做还是后做。
为什么要有优先级
资源是有限的,内存中有很多进程都要占用资源,但是资源是有限的,所以我们需要指定优先级来合理的分配资源。
Linux中优先级的特点
Linux 中优先级的表示与维护通过两个变量 PRI (priority) 和 NI (nice) 来完成,每个进程默认的 PRI 都是 80,NI 都是 0;我们可以通过修改 NI 的值来调整进程的优先级,NI 的改动范围为 [-20, 19];PRI 与 NI 的和越小,进程的优先级就越高。
我们通过 ps -l 来查看:
进程最终优先级=默认优先级(固定80)+nice值。
Linux支持进程在运行过程中调整优先级,调整的方式是修改nice值。
注意:每次top+r都是设置新的值,从默认值80开始的,而不是对旧值进行增减
top命令更改nice
步骤1:sudo top
步骤2:进入top后输入r然后在输入进程的pid(进入进程)输入要修改nice的值
进程优先级PRI的修改
进程优先级不要人为的调整,如果一个进程的优先级较高或较低,可能会造成其他进程获取操作系统资源不均,造成操作系统自身的调度失衡。
六、进程切换(并发性)
我们的笔记本只有一个CPU,代表着同一时刻只能有一个进程正在被运行,但是我们平时在使用电脑的时候,同一时间通常会运行着很多程序,并且这些程序都能被正常运行,这其实是进程切换的效果;
进程切换:CPU同一时刻只能运行一个进程,但是CPU的运算速度非常快,所以位于CPU运行队列中的每一个进程都只运行一个时间片,每个进程运行完一个时间片后被都被放到运行队列尾部,等待下次运行;这样使得在一个时间段中多个进程都能被运行。
上下文保护与上下文恢复:
CPU在进行进程切换时要进行上下文保护与上下文恢复。我们的进程在运行时会产生非常多的临时数据,同时CPU中存在一套寄存器硬件,当进程运行时,进程的PCB会被放入CPU内的寄存器中,此时CPU就可以通过进程PCB得到进程代码数据的地址;CPU在运行进程时所产生的大量的临时数据也都会被保存在寄存器中;
那么,我们在进行进程切换时需要进行进程的上下文保护与上下文恢复,即进程停止运行时将寄存器里面的数据保存起来,进程重新运行时将保存的数据再放入到寄存器中。cpu中有一个eip寄存器(PC指针),指向下一条指令的地址,通过PC指针继续运行下一行代码。以便我们能够接着上次运行的地方接着运行。
注:CPU寄存器硬件被所有进程共享,但是当CPU在具体运行某一进程时,CPU寄存器中的数据只属于该进程;同时,我们进行上下文保护时保存的是寄存器中的数据,而不是寄存器硬件。
七、其他概念
竞争性: 系统进程数目众多,而CPU资源只有少量,甚至1个,所以进程之间是具有竞争属性的。为了高效完成任务,更合理竞争相关资源,便具有了优先级 。
独立性: 多进程运行,需要独享各种资源,多进程运行期间互不干扰 。
并行: 多个进程在多个CPU下分别,同时进行运行,这称之为并行 。
并发: 多个进程在一个CPU下采用进程切换的方式,在一段时间之内,让多个进程都得以推进,称之为并发。