一:进程管理
进程、轻量级进程和线程
通常定义:进程是程序执行时的一个实例。 这个很像类和实例对象的关系。
从内核来看:进程的目的就是担当分配系统资源(CPU,内存等)的实体。
当进程创建时,它几乎和父进程相同。父子进程区别是他们都有各自独立的堆和栈,所以子进程对内存单元的修改对父进程是不可见的。
在多线程系统中,一个进程由几个用户线程(简单说就是线程)组成,每个线程代表进程的一个执行流。现在大多数多线程应用都是pthread(POSIX thread)库的标准库函数编写的。2个轻量级进程基本可以共享一些资源,诸如地址空间、打开的文件等等。其中一个修改了,另外一个能立即查看这种修改。2个线程访问共同资源时必须同步他们。
那么实现多线程就是把轻量级进程与每个线程关联起来。
Linux早期没有提供多线程的版本支持。从内核来看,多线程应用程序仅仅是一个普通的进程。
Linux使用轻量级进程(lightweight proccess)对多线程应用程序提供更好支持。
实际上,进程与线程的区分也不是十分严格,一般讲到进程时常常也包括了线程。
线程也有“pid”,也有task_struct结构。
进程描述符
为了管理进程,内核得搞明白进程啥时啥地干了啥事情,所以内核用一个结构来记录进程做的事情,task_struct,比如内核知道进程的优先级,它分配了什么样的地址空间,它访问了哪个文件,它在CPU上运行还是被阻塞了等等。这也可以叫做一个进程描述符process descriptor。
task_struct 包含很多成员,将进程和各个内核子系统联系起来。
标识一个进程
进程和进程描述符之间是非常严格的一一对应关系。
类unix的系统允许用户使用一个叫做进程标识符process ID(或PID)的数类标识进程。PID存放在进程描述符的pid字段中。PID被顺序编号,创建新进程的PID通常是前一个进程的PID加1。但是PID也有一个上限,内核使用的PID达到上限时必须循环使用已闲置的小PID号。在缺省情况下,最大PID号时32767(PID_MAX_DEFAULT-1).
系统管理员可以设置这个值,在 /proc/sys/kernel/pid_max 文件中。
linux把不同的PID与系统中的每个进程或轻量级进程相关联。
另一方面,Unix程序员希望同一组中的线程有共同的PID。例如,把指定PID的信号发送给组中的所有线程。 所以,Linux引入了线程组的表示。一个线程组中的所有线程使用和该线程组的领头线程(thread group leader)相同的PID,也就是该组中的第一个轻量级进程的PID。它被存入进程描述符tgid字段中。getpid() 系统调用返回当前进程的tgid值而不是pid值,因此,一个多线程应用的所有线程共享相同的PID。绝大多数进程都属于一个线程组,包含单一的成员;线程组的领头线程其tgid的值与pid的值相同,因而 getpid() 系统调用对这类进程所起的作用和一般进程是一样的。
进程状态
- 可运行状态 TASK_RUNNING
要么在CPU上执行,那么准备执行
- 可中断的等待状态 TASK_INTERRUPTIBLE
进程被挂起(睡眠),直到某个时候变为真。产生一个硬中断,释放进程正等待的系统资源,或传递一个信号都可以唤醒进程的条件
- 不可中断的等待状态 TASK_UNITERRUPTIBLE
与可中断的等待状态类似,但有一个例外,把信号传递到睡眠进程不能改变它的状态。这种状态很少用,但在一些特定情况下
- 暂停状态TASK_STOPPED
进程执行被暂停。当进程接收到SIGSTOP、SIGSTP、SIGTTIN或SIGTTOU 信号后,进入暂停状态
- 跟踪状态 TASK_TRACED
进程的执行已由debugger程序暂停。当一个进程被另外一个进程监控时,例如debugger执行ptrace()系统调用监控一个测试程序。 任何信号都可以把这个状态置于TASK_TRACED 状态。
还有2个进程状态是既可以存放进程描述符state字段中,也可以存放在exit_state 字段中。从名称可以看出,只有当进程的执行被终止时,进程的状态才会变为这2中状态:
- 僵死状态 EXIT_ZOMBIE
进程的执行被终止了,但是,父进程还没又发布wait4() 或者 waitpid() 系统调用来返回有关死亡进程的信息。
- 僵死撤销状态 EXIT_DEAD
最终状态:由于父进程刚发出wait4() 或者 waitpid() 系统调用,因为进程由系统删除。为了防止其他执行线程在同一个进程上也执行wait() 类系统调用,而把进程的僵死状态改为僵死撤销状态EXIT_DEAD
state 字段可以用一个简单的复制语句设置:
p->state = TASK_RUNNING;
内核也使用 set_task_state
和 set_current_state
宏:他们分别设置执行进程的状态和当前执行进程的状态。
二:进程调度
调度简介
在一个多进程的操作系统中,进程调度是一个关键性的问题,它对系统性能有着绝对的影响。
一般来说,操作系统是应用程序和可用资源之间的“中间件”。 计算机的可用资源有内存,CPU,其他硬件等。CPU也是一种资源,调度器可以临时分配一个执行的任务在CPU上运行。调度器使得同时执行多个程序成为可能,各种各样的进程就可以同时共享使用CPU了。
内核必须提供一种方法,使各种进程任务能公平的共享cpu时间,又能考虑不同任务的优先级,能有效的分配CPU的时间。也要防止CPU尚有能力且有进程执行,但是由于某种原因而长时间得不到执行的情况。一旦这些情况发生,调度机制要能识别与化解。
调度器满足的目标
为了满足上面的目标,所以设计一个调度器要考虑的为题有:
1、 调度的时机
也就是说在什么情况下,什么时候进行调度
2、调度的策略
根据什么准则挑选下一个进程进入cpu运行
3、调度的方式
是可剥夺,还是不可剥夺。当运行的进程不愿意资源放弃CPU的使用权,是否可以强制性的剥夺使用权。等等问题。
调度时机
首先,自愿的调度随时都可以进行。在内核里,一个进程可以通过 schedule()
启动一次调度,也可以在 schedule()
之前将本进程的状态设置为TASK_INTERRUPTIBLE 或者 TASK_UNINTERRUPTIBLE, 暂时放弃运行而进入睡眠。
在用户空间中,则可以通过系统调用 pause()
来达到同样的目的。也可以为这种自愿放弃时间运行加上时间限制。在内核中有个 schedule_timeout()
来达到这个目的。 在用户空间则是 nanosleep()
。
从应用角度看,只有在用户空间自愿放弃运行着一主动是可见的,在内核主动放弃运行时不可见的,它隐藏在其他可能受阻的系统调用中。几乎所有涉及到外设的系统调用,如open(), read(), write(), select() 等,都是可能受阻。
除此之外,调度还可以是非自愿的,即强制发生在每次从系统调用返回前夕,以及每次从中断或者异常处理返回到用户空间的前夕。
注意:这里“返回到用户空间”是几个关键字,因为这意味着只有在用户空间(当CPU在用户空间运行)发生的中断或者异常才会引起调度。
进程的分类
1、一种分类
- IO密集型
- 计算密集型
2、 另外一种分类
- 交互式进程
- 批处理进程
- 实时进程
实时进程与普通进程
在linux中, 调度算法可以明确的确认所有实时进程的身份, 但是没办法区分交互式程序和批处理程序(统称为普通进程), linux2.6的调度程序实现了基于进程过去行为的启发式算法, 以确定进程应该被当做交互式进程还是批处理进程. 当然与批处理进程相比, 调度程序有偏爱交互式进程的倾向。
根据进程的不同分类Linux采用不同的调度策略.
实时进程:采用FIFO 或者 Round Robin的调度策略
普通进程:则要区分交互式和批处理的不同。传统Linux调度器提高交互式应用的优先级,使得它们能更快地被调度。而CFS和RSDL等新的调度器的核心思想是”完全公平”。这个设计理念不仅大大简化了调度器的代码复杂度,还对各种调度需求的提供了更完美的支持.
linux进程的调度算法其实经过了很多次的演变, 但是其演变主要是针对与普通进程的, 因为前面我们提到过根据进程的不同分类Linux采用不同的调度策略.实时进程和普通进程采用了不同的调度策略, 更一般的普通进程还需要启发式的识别批处理进程和交互式进程.
Linux调度器的演进
一开始的调度器是复杂度为O(n)的始调度算法(实际上每次会遍历所有任务,所以复杂度为O(n)), 这个算法的缺点是当内核中有很多任务时,调度器本身就会耗费不少时间,所以,从linux2.5开始引入赫赫有名的O(1)调度器。
然而,linux是集全球很多程序员的聪明才智而发展起来的超级内核,没有最好,只有更好,在O(1)调度器风光了没几天就又被另一个更优秀的调度器取代了,它就是CFS调度器Completely Fair Scheduler.
这个也是在2.6内核中引入的,具体为2.6.23,即从此版本开始,内核使用CFS作为它的默认调度器,O(1)调度器被抛弃了, 其实CFS的发展也是经历了很多阶段,最早期的楼梯算法(SD), 后来逐步对SD算法进行改进出RSDL(Rotating Staircase Deadline Scheduler), 这个算法已经是”完全公平”的雏形了, 直至CFS是最终被内核采纳的调度器, 它从RSDL/SD中吸取了完全公平的思想,不再跟踪进程的睡眠时间,也不再企图区分交互式进程。它将所有的进程都统一对待,这就是公平的含义。CFS的算法和实现都相当简单,众多的测试表明其性能也非常优越。
字段 | 版本 |
---|---|
O(n)的始调度算法 | linux-0.11~2.4 |
O(1)调度器 | linux-2.5 |
CFS调度器 | linux-2.6~至今 |
Linux调度器框架
2种方法激活调度:
- 一种是直接的,比如进程打算睡眠或者处于其他原因放弃CPU
- 另外一种是通过周期性的机制,以固定频率运行,不时的检查是否有必要
因此当前linux的调度程序由两个调度器组成:主调度器,周期性调度器(两者又统称为通用调度器(generic scheduler)或核心调度器(core scheduler))
并且每个调度器包括两个内容:调度框架(其实质就是两个函数框架)及调度器类
**
6种调度策略
linux内核目前实现了6中调度策略(即调度算法), 用于对不同类型的进程进行调度, 或者支持某些特殊的功能。
比如 SCHED_NORMAL
和 SCHED_BATCH
调度普通的非实时进程, SCHED_FIFO
和 SCHED_RR
和 SCHED_DEADLINE
则采用不同的调度策略调度实时进程, SCHED_IDLE
则在系统空闲时调用idle进程.
字 段 | 描 述 | 所在调度器类 |
---|---|---|
SCHED_NORMAL | (也叫SCHED_OTHER)用于普通进程,通过CFS调度器实现。SCHED_BATCH用于非交互的处理器消耗型进程。SCHED_IDLE是在系统负载很低时使用 | CFS |
SCHED_BATCH | SCHED_NORMAL普通进程策略的分化版本。采用分时策略,根据动态优先级(可用nice()API设置),分配CPU运算资源。注意:这类进程比上述两类实时进程优先级低,换言之,在有实时进程存在时,实时进程优先调度。但针对吞吐量优化, 除了不能抢占外与常规任务一样,允许任务运行更长时间,更好地使用高速缓存,适合于成批处理的工作 | CFS |
SCHED_IDLE | 优先级最低,在系统空闲时才跑这类进程(如利用闲散计算机资源跑地外文明搜索,蛋白质结构分析等任务,是此调度策略的适用者) | CFS-IDLE |
SCHED_FIFO | 先入先出调度算法(实时调度策略),相同优先级的任务先到先服务,高优先级的任务可以抢占低优先级的任务 | RT |
SCHED_RR | 轮流调度算法(实时调度策略),后者提供 Roound-Robin 语义,采用时间片,相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性,同样,高优先级的任务可以抢占低优先级的任务。不同要求的实时任务可以根据需要用sched_setscheduler() API设置策略 | RT |
SCHED_DEADLINE | 新支持的实时进程调度策略,针对突发型计算,且对延迟和完成时间高度敏感的任务适用。基于Earliest Deadline First (EDF) 调度算法 | DL |
linux内核实现的6种调度策略, 前面三种策略使用的是cfs调度器类,后面两种使用rt调度器类, 最后一个使用DL调度器类
5个调度器类
而依据其调度策略的不同实现了5个调度器类, 一个调度器类可以用一种种或者多种调度策略调度某一类进程, 也可以用于特殊情况或者调度特殊功能的进程.
调 度 器 类 | 描述 | 对应调度策略 |
---|---|---|
stop_sched_class | 优先级最高的线程,会中断所有其他线程,且不会被其他任务打断 作用 1.发生在cpu_stop_cpu_callback 进行cpu之间任务migration 2.HOTPLUG_CPU的情况下关闭任务 | 无, 不需要调度普通进程 |
dl_sched_class | 采用EDF最早截至时间优先算法调度实时进程 | SCHED_DEADLINE |
rt_sched_class | 采用提供 Roound-Robin算法或者FIFO算法调度实时进程 具体调度策略由进程的task_struct->policy指定 | SCHED_FIFO, SCHED_RR |
fair_sched_clas | 采用CFS算法调度普通的非实时进程 | SCHED_NORMAL, SCHED_BATCH |
idle_sched_class | 采用CFS算法调度idle进程, 每个cup的第一个pid=0线程:swapper,是一个静态线程。调度类属于:idel_sched_class,所以在ps里面是看不到的。一般运行在开机过程和cpu异常的时候做dump | SCHED_IDLE |
其所属进程的优先级顺序为
stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class
3个调度实体
调度器不限于调度进程, 还可以调度更大的实体, 比如实现组调度: 可用的CPUI时间首先在一半的进程组(比如, 所有进程按照所有者分组)之间分配, 接下来分配的时间再在组内进行二次分配.
这种一般性要求调度器不直接操作进程, 而是处理可调度实体, 因此需要一个通用的数据结构描述这个调度实体,即seched_entity结构, 其实际上就代表了一个调度对象,可以为一个进程,也可以为一个进程组.
linux中针对当前可调度的实时和非实时进程, 定义了类型为seched_entity的3个调度实体
调度实体 | 名称 | 描述 | 对应调度器类 |
---|---|---|---|
sched_dl_entity | DEADLINE调度实体 | 采用EDF算法调度的实时调度实体 | dl_sched_class |
sched_rt_entity | RT调度实体 | 采用Roound-Robin或者FIFO算法调度的实时调度实体 | rt_sched_class |
sched_entity | CFS调度实体 | 采用CFS算法调度的普通非实时进程的调度实体 | fair_sched_class |
调度器类的就绪队列
另外,对于调度框架及调度器类,它们都有自己管理的运行队列,调度框架只识别rq(其实它也不能算是运行队列),而对于cfs调度器类它的运行队列则是cfs_rq(内部使用红黑树组织调度实体),实时rt的运行队列则为rt_rq(内部使用优先级bitmap+双向链表组织调度实体), 此外内核对新增的dl实时调度策略也提供了运行队列dl_rq
调度器整体框架
本质上, 通用调度器(核心调度器)是一个分配器,与其他两个组件交互.
- 调度器用于判断接下来运行哪个进程.
内核支持不同的调度策略(完全公平调度, 实时调度, 在无事可做的时候调度空闲进程,即0号进程也叫swapper进 程,idle进程), 调度类使得能够以模块化的方法实现这些侧露额, 即一个类的代码不需要与其他类的代码交互
当调度器被调用时, 他会查询调度器类, 得知接下来运行哪个进程
- 在选中将要运行的进程之后, 必须执行底层的任务切换.
这需要与CPU的紧密交互. 每个进程刚好属于某一调度类, 各个调度类负责管理所属的进程. 通用调度器自身不涉 及进程管理, 其工作都委托给调度器类.
每个进程都属于某个调度器类(由字段task_struct->sched_class标识), 由调度器类采用进程对应的调度策略调度(由task_struct->policy )进行调度, task_struct也存储了其对应的调度实体标识