I/O-Bound Process vs. Processor-Bound Process
Process可以被分类为两种:一种是I/O密集型的,一种是处理器密集型
I/O密集型主要是在等待I/O的请求与响应上
处理器密集型,主要是在CPU的运算上
Process Priority
Linux采用动态priority-based调度。当一个程序是I/O密集的,则Linux倾向于把这个地为较高优先级,而把处理器密集型的定为较低优先级。(我猜是因为1.io需要尽快的响应;2.io process经常会被block的时候给换出去,不影响cpu密集process的运行时间)
Linux采用两个优先级的设置,一个叫做nice值,范围在-20到+19之间,缺省是0.更大的nice值表示更低的优先级,因为这看起来这个process对其他processes非产的Nice。如果一个process的nice值是-19则这个process会得到最大的时间片。
另一个,叫做实时优先级,这个优先级是可配置的,从0至99,所有的实时processes的优先级都要高于普通的process。 (这部分需要阅读UTLK来进行确认,我记得不是这样滴)
Timeslice
时间片,是一个经验值,太长的时间片,会导致系统的响应不是那么的顺畅;而太短的时间片又会使系统浪费太多的时间在进程切换上。
Linux中,倾向于给CPU密集型process分配更少的时间片,而给IO密集型分配更长的时间片。
当一个process用光了自己的时间片的时候,它会被标注成expired,一个过期的process只有当其他所有的process时间片耗尽后才会重新被调度。
Process Preemption
进程抢占的意思是:
当一个process进入TASK_RUNNING状态的时候,内核将检查这个process是否比当前运行的process更高优先级,如果是的话,调度器就将用这个新执行的process抢占当前进程。当一个process的时间片用光的时候,这个process将被一个新的process抢占
The Linux Scheduling Algorithm
Linux定义每个CPU有一个runqueue,类型是struct runqueue
有一组宏可以方便的操作runqueue
cpu_rq(processor)返回一个指定cpu的runqueue的指针
task_rq(task)返回一个指向这个task所属的runqueue的指针
每个runqueue包含两个priority array是struct prio_array类型, 一个是active arrary, 一个是expired array
用array的目的是为了使复杂度实现O(1),我觉得这里有必要列出这个特殊的结构
struct prio_array {
};
struct list_head queue[MAX_PRIO]; 代表每个优先级有一个list头
bitmap数组加起来统共160个二进制位,当某个优先级的task被置为TASK_RUNNING的时候,对应的二进制位会标记成1,于是,找到目前需要运行的最高优先级的task被简化为了找到 bitmap最高位的1。Linux定义了一个函数来进行这个操作sched_find_first_bit()。
Recalculating Timeslices
上面提到了active array和expired array,Linux用这两个array来缓解重新计算的工作量
当一个task用光了它的时间片的时候,就要被塞进expired array,在塞进expired array之前,它的时间片会被重新计算出来。这样重新计算时间片的工作就被简化为了交换active array和expired array的指针。
调度器的一个主要函数是:schedule()
这个函数将会在有内核代码要sleep或者是某个task被抢占的时候被调起,schedule()是独立运行于每个CPU上的
schedule()的功能,简单的说就是挑出最高优先级的task并运行它,这段代码表明了这个功能
struct task_struct *prev, *next;
struct list_head *queue;
struct prio_array *array;
int idx;
prev = current;
array = rq->active;
idx = sched_find_first_bit(array->bitmap);
queue = array->queue + idx;
next = list_entry(queue->next, struct task_struct, run_list);
当prev不等于next的时候,就说明要进行一次上下文切换了,切换至next的上下文中,由context_switch()函数完成。
effective_prio()函数返回一个task的动态优先级,这个优先级是由nice值加上一个bonus或者是惩罚值得来的。这个值是由task的交互性的来的
(ULK这里有个公式,回头补上)
为了实现这个能力,Linux统计每个process在TASK_RUNNABLE的时候的sleep时间,保存在task_struct.sleep_avg成员中。据说这个方法是异常的精确。
Linux调度器还为高交互性task提供了一点点帮助,当一个高交互的task用光时间片的时候,它不会被放到expired array中,而是放回到active array。
生命的意义就是在于它的变幻,你永远不会知道明天会发生什么事。 珍惜生命。。。。。
进程
linux中,进程有自己的独立的用户空间,和自己独立的内核空间(堆栈),使用一个task_struct结构来表示一个进程。一般用fork,vfork等函数建立。
线程:用户线程,内核线程
linux中,用户进程没有自己独立的用户空间,需要和别的线程共享用户空间。有自己独立的内核空间(堆栈),使用一个task_struct结构来表示。 在用户空间的应用程序中使用pthread_creat建立。
linux中,内核线程没有用户空间,他只能运行在内核空间,使用一个task_struct结构来表示。由于没有用户空间故mm_struct *mm==NULL。在内核使用kernel_thread创建。
另外: linux中,将线程称作轻量级进程(lightweight process),内核可以直接进行调度。
在《深入linux内核》书中有个比喻:假设一个象棋程序使用两个线程,其中一个控制图形化棋盘,等待选手移动并显示在屏幕上,另一个考虑棋的下一步移动。 由此可见,当第一个进程在等待的时候,第二个进程应该继续运行。但是如果象棋程序仅是一个单独的进程,第一个线程就不能简单的发出等待用户行为的堵塞系统调用;否则第二个进程也会被堵塞。 所以linux需要内核为别对线程进行单独的调度。
计算机语言中说: 进程是系统分配的最少单元,线程是程序运行的最小单元。
进程描述符:
每个一个进程,线程都会用一个task_struct结构来描述的,变量很多,挑重要的介绍:
当生成一个进程时候,内核分配进程的数据结构,给一个8k的内核空间。其中thread_info结构在最下面,stack是内核空间的栈(用户空间的栈视进程类型来确定)。内核可以简单通过thread_info结构来找到该进程的task_struct结构。(2.6内核和2.4内核的进程内存结构不同)。有个current来表示当前进程的task_struct结构。在kernel中有 #define current (get_current())。
进程的调度:
参考国嵌视频教程中的方法,进程的调度研究步骤:
1.调度策略
2.调度时机
3.调度步骤
调度的策略:
进程的调度的策略就是按照进程的类型在 task_struct中的unsigned int policy; 变量来表示的。
调度的时机:
调度采用的是调用schedule()函数。调度的时机就是指 调用schedule的时机。
1.主动式:
当进程运行中需要某些资源等时候,资源无法使用,此时进程会suspend起自己并自动放弃CPU,进行调度。
eg:current->state = TASK_INTERRRUPTIBLE;
schedule();
2被动式(抢占式)
被动式即为进程正常的运行,被另一个更高优先级的进城抢占了CPU
抢占分为: 用户抢占和内核抢占。 2.4版本中 只支持用户抢占, 2.6的版本中新添加了内核抢占
1. 用户抢占
用户抢占发生在
- 从系统调用返回用户空间的时候
- 从中断处理程序返回用户空间的时候
总结为: 当从 内核空间 返回 用户空间 的时候,就会运行schedule()函数,来进行一次进程调度。
但只有 用户中断 有个问题,当进程进到内核空间后,如果一直不出来,那么就不会进行调度(只有在从内核空间返回时候才调度),就会影响系统的实时性。所以增加了内核抢占。
2。 内核抢占
内核抢占当有进程运行在内核空间的时候,发生:
- 内核中断,返回到内核空间时候
- 使能内核抢占的时候(锁,使能中断)
就会进行内核抢占。
但是在内核中,也有几种情况是不允许内核抢占的:
- 中断中
- 软中断中
- spinlock后
- 正在执行schedule时
在以上情况下是不可以进行内核抢占的。
在thread_info结构中,有个preempt_count变量,当进入以上情况不允许抢占的时候,preempt_count就加1,当退出时候就减一。在减一的同时进行判定,如果为0,就会调用调度函数。
内核中断时候的调度:
有个_TIF_NEED_RESCHED 标志,当某个进程的时间片用完,或者 当有个更高优先级的进程进入就绪的时候,会置位这个标志。 由此可见,当需要调度切换进程的时候才会进行调度。
解锁等使能内核抢占调度:
spin_unlock > raw_spin_unlock > _raw_spin_unlock > _raw_spin_unlock
在enable的时候进行check TIF_NEED_RESCHED是否需要调度,并调度。
调度的步骤:
看schedule函数的code即可。
主要步骤:
1.清理当前运行中的进程
2.选择下一个要运行的进程。pick_next_task函数
3.设置新的进程的运行环境
4.进程上下文切换
总结:
只有以上的时机才会进行进程调度,其他的不会。如:try_to_wake_up函数,仅仅是将一个进程加入到run_queue中去,并在需要时置位need_resched,其实并不会进行任务的调度
未完待续,只是做了个 国嵌视频的笔记,等以后慢慢学习慢慢润色吧。
与进程相比,线程中所带的资源很少,因此,创建线程和撤消线程的开销就比进程小.线程也称为“轻进程.在系统调度中,线程的切换开销也比进程步,但是不同任务中的线程切换会引起任务的切换,在这种情况下,线程和进程的调度开销就变成一样了.为了优化系统效率,减步由于线程切换而弓I起的任务切换,在调度算法中加入了以下代码: IF (所选中的线程和当前运行的城程属于同一十任务) THEN 不做任务切换} ELSE进行任务切换操作} 显然,这种方法在某种情况下会对系统性能有所帮助,但是这种方法在很大程度上属于一种“被动的,或者说是一种“碰运气”的方法.另外,单纯以线程为主的调度算法对用户任务有失公平性,以线程为主的调度算法是完全参照传统操作系统中的调度算法设计而成的.当线程投入运行时,系统为它分配周定大小的时间片,系统中线程按时间片轮转.这样,就产生了公平性问题:如果一个任务中有两个线程,那么,从理论上讲,它将比只用一个线程实现的任务多获得近1倍的处理机时间.在传统的进程调度系统中,一个用户可以通过创建多个进程来获得更多的处理机调度机会,但是,它是建立在增加了创建进程和进程间通讯的系统开销代价的基础上的相比之下,创建线程的开销非常小,同一任务间的线程之闭通讯开销也很小为了解决上述问题,我们提出并实现了一种将传统的任务和新的线程调度机翩相结合的方法:以任务为单位分配时间片(这样可以保证调度的公平性),在线程调度时,当一个线程不是由于任务时间片用完的原因而放弃处理机时,只要系统中没有高优先级线程,就从本任务中选取线程,从而使得由线程切换而引起的任务切换操作开销达到最小. 从目前的发展来看,用户任务的并行粒度越来越小,即用户任务中的线程越来越多,而每个线程所执行的操作会越来越步.因此,使用线程+任务的方法可以有效地减少单纯的以线程为主的系统调度所引起的系统开销.
进程切换分两步
1.切换页目录以使用新的地址空间
2.切换内核栈和硬件上下文。
对于linux来说,内核不区分进程和线程, 线程和进程的最大区别就只在于地址空间。
对于一个进程内的线程切换,第1步是不需要做的,第2是进程和线程切换都要做的
抢占式内核
使用抢占式内核可以保证系统响应时间。最高优先级的任务一旦就绪,总能得到CPU的使用权。当一个运行着的任务使一个比它优先级高的任务进入了就绪态,当前任务的CPU使用权就会被剥夺,或者说被挂起了,那个高优先级的任务立刻得到了CPU的控制权。如果是中断服务子程序使一个高优先级的任务进入就绪态,中断完成时,中断了的任务被挂起,优先级高的那个任务开始运行。抢占式内核如下图所示。
2.Linux下的用户态抢占和内核态抢占
Linux除了内核态外还有用户态。用户程序的上下文属于用户态,系统调用和中断处理例程上下文属于内核态。在2.6 kernel以前,Linux kernel只支持用户态抢占。
2.1 用户态抢占(User Preemption)
在kernel返回用户态(user-space)时,并且need_resched标志为1时,scheduler被调用,这就是用户态抢占。当kernel返回用户态时,系统可以安全的执行当前的任务,或者切换到另外一个任务。当中断处理例程或者系统调用完成后,kernel返回用户态时,need_resched标志的值会被检查,假如它为1,调度器会选择一个新的任务并执行。中断和系统调用的返回路径(return path)的实现在entry.S中(entry.S不仅包括kernel entry code,也包括kernel exit code)。
2.2 内核态抢占(Kernel Preemption)
在2.6 kernel以前,kernel code(中断和系统调用属于kernel code)会一直运行,直到code被完成或者被阻塞(系统调用可以被阻塞)。在 2.6 kernel里,Linux kernel变成可抢占式。当从中断处理例程返回到内核态(kernel-space)时,kernel会检查是否可以抢占和是否需要重新调度。kernel可以在任何时间点上抢占一个任务(因为中断可以发生在任何时间点上),只要在这个时间点上kernel的状态是安全的、可重新调度的。
3.内核态抢占的设计
3.1 可抢占的条件
要满足什么条件,kernel才可以抢占一个任务的内核态呢?
·没持有锁。锁是用于保护临界区的,不能被抢占。
·Kernel code可重入(reentrant)。因为kernel是SMP-safe的,所以满足可重入性。
如何判断当前上下文(中断处理例程、系统调用、内核线程等)是没持有锁的?Linux在每个每个任务的thread_info结构中增加了preempt_count变量作为preemption的计数器。这个变量初始为0,当加锁时计数器增一,当解锁时计数器减一。
3.2 内核态需要抢占的触发条件
内核提供了一个need_resched标志(这个标志在任务结构thread_info中)来表明是否需要重新执行调度。
3.3 何时触发重新调度
set_tsk_need_resched():设置指定进程中的need_resched标志
clear_tsk need_resched():清除指定进程中的need_resched标志
need_resched():检查need_ resched标志的值;如果被设置就返回真,否则返回假
什么时候需要重新调度:
·时钟中断处理例程检查当前任务的时间片,当任务的时间片消耗完时,scheduler_tick()函数就会设置need_resched标志;
·信号量、等到队列、completion等机制唤醒时都是基于waitqueue的,而waitqueue的唤醒函数为default_wake_function,其调用try_to_wake_up将被唤醒的任务更改为就绪状态并设置need_resched标志。
·设置用户进程的nice值时,可能会使高优先级的任务进入就绪状态;
·改变任务的优先级时,可能会使高优先级的任务进入就绪状态;
·新建一个任务时,可能会使高优先级的任务进入就绪状态;
·对CPU(SMP)进行负载均衡时,当前任务可能需要放到另外一个CPU上运行;
3.4 抢占发生的时机(何时检查可抢占条件)
·当一个中断处理例程退出,在返回到内核态时(kernel-space)。这是隐式的调用schedule()函数,当前任务没有主动放弃CPU使用权,而是被剥夺了CPU使用权。
·当kernel code从不可抢占状态变为可抢占状态时(preemptible again)。也就是preempt_count从正整数变为0时。这也是隐式的调用schedule()函数。
·一个任务在内核态中显式的调用schedule()函数。任务主动放弃CPU使用权。
·一个任务在内核态中被阻塞,导致需要调用schedule()函数。任务主动放弃CPU使用权。
3.5 禁用/使能可抢占条件的操作
对preempt_count操作的函数有add_preempt_count()、sub_preempt_count()、inc_preempt_count()、dec_preempt_count()。
使能可抢占条件的操作是preempt_enable(),它调用dec_preempt_count()函数,然后再调用preempt_check_resched()函数去检查是否需要重新调度。
禁用可抢占条件的操作是preempt_disable(),它调用inc_preempt_count()函数。
在内核中有很多函数调用了preempt_enable()和preempt_disable()。比如spin_lock()函数调用了preempt_disable()函数,spin_unlock()函数调用了preempt_enable()函数。
3.6 什么时候不允许抢占
preempt_count()函数用于获取preempt_count的值,preemptible()用于判断内核是否可抢占。
有几种情况Linux内核不应该被抢占,除此之外,Linux内核在任意一点都可被抢占。这几种情况是:
·内核正进行中断处理。在Linux内核中进程不能抢占中断(中断只能被其他中断中止、抢占,进程不能中止、抢占中断),在中断例程中不允许进行进程调度。进程调度函数schedule()会对此作出判断,如果是在中断中调用,会打印出错信息。
·内核正在进行中断上下文的Bottom Half(中断的下半部)处理。硬件中断返回前会执行软中断,此时仍然处于中断上下文中。
·内核的代码段正持有spinlock自旋锁、writelock/readlock读写锁等锁,处干这些锁的保护状态中。内核中的这些锁是为了在SMP系统中短时间内保证不同CPU上运行的进程并发执行的正确性。当持有这些锁时,内核不应该被抢占,否则由于抢占将导致其他CPU长期不能获得锁而死等。
·内核正在执行调度程序Scheduler。抢占的原因就是为了进行新的调度,没有理由将调度程序抢占掉再运行调度程序。
·内核正在对每个CPU“私有”的数据结构操作(Per-CPU date structures)。在SMP中,对于per-CPU数据结构未用spinlocks保护,因为这些数据结构隐含地被保护了(不同的CPU有不一样的per-CPU数据,其他CPU上运行的进程不会用到另一个CPU的per-CPU数据)。但是如果允许抢占,但一个进程被抢占后重新调度,有可能调度到其他的CPU上去,这时定义的Per-CPU变量就会有问题,这时应禁抢占。
Linux是一个多用户,多任务的系统,可以同时运行多个用户的多个程序,就必然会产生很多的进程,而每个进程会有不同的状态。
1.R (TASK_RUNNING)状态,可执行状态。
只有在该状态的进程才可能在CPU上运行。而同一时刻可能有多个进程处于可执行状态,这些进程的task_struct结构(进程控制块)被放入对应CPU的可执行队列中(一个进程最多只能出现在一个CPU的可执行队列中)。进程调度器的任务就是从各个CPU的可执行队列中分别选择一个进程在该 CPU上运行。
很多操作系统教科书将正在CPU上执行的进程定义为RUNNING状态、而将可执行但是尚未被调度执行的进程定义为READY状态,这两种状态在linux下统一为 TASK_RUNNING状态。
2.S (TASK_INTERRUPTIBLE)状态,可中断的睡眠状态。
处于这个状态的进程因为等待某某事件的发生(比如等待socket连接、等待信号量),而被挂起。这些进程的task_struct结构被放入对应事件的等待队列中。当这些事件发生时(由外部中断触发、或由其他进程触发),对应的等待队列中的一个或多个进程将被唤醒。
通过ps命令我们会看到,一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态(除非机器的负载很高)。毕竟CPU就这么一两个,进程动辄几十上百个,如果不是绝大多数进程都在睡眠,CPU又怎么响应得过来。
3.D (TASK_UNINTERRUPTIBLE)状态,不可中断的睡眠状态。
与TASK_INTERRUPTIBLE状态类似,进程处于睡眠状态,但是此刻进程是不可中断的。不可中断,指的并不是CPU不响应外部硬件的中断,而是指进程不响应异步信号。
绝大多数情况下,进程处在睡眠状态时,总是应该能够响应异步信号的。否则你将惊奇的发现,kill -9竟然杀不死一个正在睡眠的进程了!于是我们也很好理解,为什么ps命令看到的进程几乎不会出现TASK_UNINTERRUPTIBLE状态,而总是 TASK_INTERRUPTIBLE状态。
而TASK_UNINTERRUPTIBLE状态存在的意义就在于,内核的某些处理流程是不能被打断的。如果响应异步信号,程序的执行流程中就会被插入一段用于处理异步信号的流程(这个插入的流程可能只存在于内核态,也可能延伸到用户态),于是原有的流程就被中断了。在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。
linux系统中也存在容易捕捉的TASK_UNINTERRUPTIBLE状态。执行vfork系统调用后,父进程将进入TASK_UNINTERRUPTIBLE状态,直到子进程调用exit或exec