1.前言
本文的内容是基于Linux 2.6的源码,深入分析进程模型。
- 什么是进程
- 操作系统是怎么组织进程的
- 进程状态如何转换
- 进程是如何调度的
- 谈谈自己对该操作系统进程模型的看法
2.什么是进程
进程是处于执行期的程序以及它所包含的所有资源的总称,包括虚拟处理器,虚拟空间,寄存器,堆栈,全局数据段等。
Windows10进程如图所示
3.操作系统是如何组织进程的
一、描述进程——PCB
进程信息被放在一个叫做进程控制块的结构中,可以理解为进程属性的集合,称之为:PCB,在Linux下,PCB是一个叫做task_struct的结构体,这个结构体里面存放了进程的有关信息。
task_struct结构体的内容分类:
①标识符(PID):描述本进程的唯一的标识符,用来区别其他进程
获取pid的方法有很多,最推荐的一种就是通过系统调用getpid()来获取进程的pid,
②状态:任务状态,退出码,退出信号等
状态分类:R运行状态(runing) S睡眠状态(sleeping) D磁盘休眠状态(Disk sleeping) T停止状态(stopped) X死亡状态(dead) Z僵尸状态(zombie)
③优先级:相对于其他进程的优先级
PRI:进程可执行的优先级,值越小优先级越高
NI:代表nice值,表示进程可被执行的优先级的修正数值
④程序计数器:程序中即将被执行的下一条指令的地址
⑤内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
⑥上下文数据:进程执行时处理器的寄存器中的数据
⑦I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
⑧记账信息:可能包括处理器的时间总和,使用的时钟数总和,时间限制,记账号等
⑨其他信息
二、组织进程
因为进程需要不断的关闭和开启,和数据结构中链表的结构很相似,所以运行在系统的进程都以task_struct链表的形式存在内核里面,此时我们就把进程组织起来了。
三、查看进程
现在进程已经被描述和组织起来,那么有些方法可以让我们查看进程呢?
①进程信息可以被 /proc 文件查看; 比如:要查看PID(进程的标识符)为1的进程,就需要查看 /proc/1 这个文件夹
②通过系统调用获取进程标识符:
getppid()函数是获取父进程的标识符,getpid()函数是获取子进程的标识符
四、创建进程
通过系统调用创建进程,也就是使用fork()函数
#include<stdio.h> #include<sys/types.h> #include<unistd.h> int main(){ pid_t ret = fork(); printf("hello proc:%d,ret = %d\n",getpid(),ret); return 0; }
五、进程的状态:
R运行状态(running):表明进程要么在运行中,要么在运行队列里面
S睡眠状态(sleeping):意味着进程在等待事件的完成,这个睡眠也叫做可中断睡眠
D磁盘休眠状态(Disk sleep):这个状态的睡眠通常在等待I/O的结束,也叫做不可中断睡眠
T停止状态(stopped):可以发送SIGSTOP信号给进程,让进程停止。也可以发送SIGCONT信号让进程继续运行。
X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里面看到这个状态
Z僵尸状态(zombie):子进程退出,父进程没有读取子进程的退出码,子进程进入Z状态
僵尸进程:
#include<stdio.h> #include<sys/types.h> #include<unistd.h> #include<stdlib.h> int main(){ pid_t id = fork(); if(id<0){ perror("fork failed!\n"); return 1; } else if (id>0){//这个循环里面,我们让父进程sleep30s printf("father pid:%d\n",getpid()); sleep(30); } else {//这个循环里面,我们让子进程sleep5s,之后退出 printf("child pid:%d\n",getpid()); sleep(5); exit(EXIT_SUCCESS); } return 0; }
僵尸进程的危害:
①因为进程的状态必须被维护,退出状态也属于进程的基本信息,所以就保存在PCB中,在Z状态中,子进程不退出,PCB就要一直被维护
②子进程进入僵尸状态之后会一直占用内存,造成内存的资源浪费
③内存的泄漏
4.进程状态如何转换
1. Linux进程状态有:
TASK_RUNNING : 就绪态或者运行态,进程就绪可以运行,但是不一定正在占有CPU,对应进程状态的R
TASK_INTERRUPTIBLE:睡眠态,但是进程处于浅度睡眠,可以响应信号,一般是进程主动sleep进入的状态,对应进程状态S
TASK_UNINTERRUPTIBLE:睡眠态,深度睡眠,不响应信号,典型场景是进程获取信号量阻塞,对应进程状态D
TASK_ZOMBIE:僵尸态,进程已退出或者结束,但是父进程还不知道,没有回收时的状态,对应进程状态Z
TASK_STOPED:停止,调试状态,对应进程状态T
2. 进程调度时机:
进程调度会引起进程状态转换,由上图可知如下情况会触发调度,进程终止或进程睡眠时主动exit或sleep释放CPU;浅度睡眠的进程被CFS调度选中唤醒,深度睡眠进程由于信号量,锁等的释放而被唤醒;进程收到信号量等;还有一种最常见的中断,异常。
5.进程是如何调度的
Linux进程调度的目标
1.高效性:高效意味着在相同的时间下要完成更多的任务。调度程序会被频繁的执行,所以调度程序要尽可能的高效;
2.加强交互性能:在系统相当的负载下,也要保证系统的响应时间;
3.保证公平和避免饥渴;
4.SMP调度:调度程序必须支持多处理系统;
5.软实时调度:系统必须有效的调用实时进程,但不保证一定满足其要求;
Linux进程优先级
进程提供了两种优先级,一种是普通的进程优先级,第二个是实时优先级。前者适用SCHED_NORMAL调度策略,后者可选SCHED_FIFO或SCHED_RR调度策略。任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占,同级实时进程之间是按照FIFO(一次机会做完)或者RR(多次轮转)规则调度的。
实时进程的调度
实时进程,只有静态优先级,因为内核不会再根据休眠等因素对其静态优先级做调整,其范围在0~MAX_RT_PRIO-1间。默认MAX_RT_PRIO配置为100,也即,默认的实时优先级范围是0~99。而nice值,影响的是优先级在MAX_RT_PRIO~MAX_RT_PRIO+40范围内的进程。
不同与普通进程,系统调度时,实时优先级高的进程总是先于优先级低的进程执行。知道实时优先级高的实时进程无法执行。实时进程总是被认为处于活动状态。如果有数个 优先级相同的实时进程,那么系统就会按照进程出现在队列上的顺序选择进程。假设当前CPU运行的实时进程A的优先级为a,而此时有个优先级为b的实时进程B进入可运行状态,那么只要b<a,系统将中断A的执行,而优先执行B,直到B无法执行(无论A,B为何种实时进程)。
不同调度策略的实时进程只有在相同优先级时才有可比性:
1. 对于FIFO的进程,意味着只有当前进程执行完毕才会轮到其他进程执行。由此可见相当霸道。
2. 对于RR的进程。一旦时间片消耗完毕,则会将该进程置于队列的末尾,然后运行其他相同优先级的进程,如果没有其他相同优先级的进程,则该进程会继续执行。
,对于实时进程,高优先级的进程就是大爷。它执行到没法执行了,才轮到低优先级的进程执行。等级制度相当森严啊。
非实时进程调度
Linux对普通的进程,根据动态优先级进行调度。而动态优先级是由静态优先级(static_prio)调整而来。Linux下,静态优先级是用户不可见的,隐藏在内核中。而内核提供给用户一个可以影响静态优先级的接口,那就是nice值,两者关系如下:
static_prio=MAX_RT_PRIO +nice+ 20
nice值的范围是-20~19,因而静态优先级范围在100~139之间。nice数值越大就使得static_prio越大,最终进程优先级就越低。
ps -el 命令执行结果:NI列显示的每个进程的nice值,PRI是进程的优先级(如果是实时进程就是静态优先级,如果是非实时进程,就是动态优先级)
而进程的时间片就是完全依赖 static_prio 定制的,见下图,摘自《深入理解linux内核》,
我们前面也说了,系统调度时,还会考虑其他因素,因而会计算出一个叫进程动态优先级的东西,根据此来实施调度。因为,不仅要考虑静态优先级,也要考虑进程的属性。例如如果进程属于交互式进程,那么可以适当的调高它的优先级,使得界面反应地更加迅速,从而使用户得到更好的体验。Linux2.6 在这方面有了较大的提高。Linux2.6认为,交互式进程可以从平均睡眠时间这样一个measurement进行判断。进程过去的睡眠时间越多,则越有可能属于交互式进程。则系统调度时,会给该进程更多的奖励(bonus),以便该进程有更多的机会能够执行。奖励(bonus)从0到10不等。
系统会严格按照动态优先级高低的顺序安排进程执行。动态优先级高的进程进入非运行状态,或者时间片消耗完毕才会轮到动态优先级较低的进程执行。动态优先级的计算主要考虑两个因素:静态优先级,进程的平均睡眠时间也即bonus。计算公式如下,
dynamic_prio = max (100, min (static_prio - bonus + 5, 139))
在调度时,Linux2.6 使用了一个小小的trick,就是算法中经典的空间换时间的思想,使得计算最优进程能够在O(1)的时间内完成。
Linux进程状态机