1、一 前言在开源操作系统中,Linux 的发展最为显著,到目前为止,它在低端服务器市场已经占据了相当大的份额。从最新的 Linux 2.6系统来看,Linux 的发展方向主要有两个:嵌入式系统和高端计算领域。调度系统对于操作系统的整体性能有着非常重要的影响,嵌入式系统、桌面系统和高端服务器对于调度器的要求是很不一样的。Linux 调度器的特点主要有两个: 核心不可抢占; 调度算法简单有效。由于 Linux 适用于多种平台,本文所指缺省为 i386下的 SMP 系统。二 相关数据结构在 Linux 中,进程用 task_struct 表示,所有进程被组织到以 init_task 为表头的双向链表中
2、(见include/linux/sched.hSET_LINKS()宏) ,该链表是全系统唯一的。所有 CPU 被组织到以 schedule_data(对界后)为元素的数组之中。进程与所运行的 CPU 之间可以相互访问(详见下) 。所有处于运行态的进程(TASK_RUNNING )被组织到以 runqueue_head 为表头的双向链表之中,调度器总是从中寻找最适合调度的进程。runqueue_head 也是全系统唯一的。下面分别介绍这些与调度器工作相关的数据结构。1 init_tssTSS,Task State Segment,80x86平台特有的进程运行环境,尽管 Linux 并不使用 T
3、SS,但将 TSS 所需要描述的信息保存在以 cpu 号为索引的 tss_struct 数组 init_tss 中,进程切换时,其中的值将获得更新。2 task_struct在 Linux 中,线程、进程使用的是相同的核心数据结构,可以说,在2.4的内核里只有进程,其中包含轻量进程。一个进程在核心中使用一个 task_struct 结构来表示,包含了大量描述该进程的信息,其中与调度器相关的信息主要包括以下几个:i. stateLinux 的进程状态主要分为三类:可运行的(TASK_RUNNING,相当于运行态和就绪态) ;被挂起的(TASK_INTERRUPTIBLE 、TASK_UNINTE
4、RRUPTIBLE 和 TASK_STOPPED) ;不可运行的(TASK_ZOMBIE) ,调度器主要处理的是可运行和被挂起两种状态下的进程,其中 TASK_STOPPED 又专门用于 SIGSTP 等 IPC 信号的响应,而 TASK_ZOMBIE 指的是已退出而暂时没有被父进程收回资源的“僵尸“ 进程。ii. need_resched布尔值,在调度器中用于表示该进程需要申请调度(详见“调度器工作流程“ ) 。iii. policy在 Linux 2.4中,进程的调度策略可以有三种选择:SCHED_FIFO(先进先出式调度,除非有更高优先级进程申请运行,否则该进程将保持运行至退出才让出 C
5、PU) 、SCHED_RR(轮转式调度,该进程被调度下来后将被置于运行队列的末尾,以保证其他实时进程有机会运行) 、SCHED_OTHER(常规的分时调度策略) 。另外,policy 中还包含了一个 SCHED_YIELD 位,置位时表示主动放弃 CPU。iv. rt_priority用于表征实时进程的优先级,从1-99取值,非实时进程该项应该为0。这一属性将用于调度时的权值计算(详见“就绪进程选择算法 “) 。v. counter该属性记录的是当前时间片内该进程还允许运行的时间(以 CPU 时钟 tick 值为单位,每个进程的 counter 初值与 nice 值有关, nice 越小则 c
6、ounter 越大,即优先级越高的进程所允许获得的 CPU 时间也相对越多) ,并参与“ 就绪进程选择算法“ 。在 Linux 2.4中,每个(非SCHED_FIFO 实时)进程都不允许运行大于某一时间片的时间,一旦超时,调度器将强制选择另一进程运行(详见“调度器工作流程 “)vi. nice用户可支配的进程优先级,将参与“就绪进程选择算法“ ,同时该值也决定了该进程的时间片长度(详见下) 。vii. cpus_allowed以位向量的形式表示可用于该进程运行的 CPU(见“ 调度器工作流程“ ) 。viii. cpus_runnable以位向量的形式表示当前运行该进程的 CPU(相应位为1)
7、 。如果不在任何 CPU 上运行,则为全1。这一属性和 cpus_allowed 属性结合,可以迅速判断该进程是否能调度到某一CPU 上运行(位“ 与“) 。ix. processor本进程当前(或最近)所在 CPU 编号。x. thread用于保存进程执行环境(各个寄存器的值以及 IO 操作许可权映射表) ,内容与 TSS 相近。因为 TSS 以 CPU id 为索引,而 Linux 无法预测被替换下来的进程下一次将在哪个 CPU 上运行,所以这些信息不能保存在 TSS 中。3 current核心经常需要获知当前在某 CPU 上运行的进程的 task_struct,在 Linux 中用 cu
8、rrent 指针指向这一描述符。current 的实现采用了一个小技巧以获得高效的访问速度,这个小技巧与Linux 进程 task_struct 的存储方式有关。在 Linux 中,进程在核心级运行时所使用的栈不同于在用户级所分配和使用的栈。因为这个栈使用率不高,因此仅在创建进程时分配了两个页(8KB) ,并且将该进程的 task_struct安排在栈顶。 (实际上这两个页是在分配 task_struct 时申请的,初始化完 task_struct 后即将esp 预设为页尾作为进程的核心栈栈底,往 task_struct 方向延伸。 )因此,要访问本进程的 task_struct,只需要执行以
9、下简单操作:_asm_(“andl %esp,%0; “:“=r“ (current) : “0“ (8191UL);此句将 esp 与0x0ffffe0作“与“运算,获得核心栈的首页基址,此即为 task_struct 的地址。4 schedule_datatask_struct 是用于描述进程的数据结构,其中包含了指向所运行 CPU 的属性。在 Linux 中,另有一个数据结构对应于 CPU,可以利用它访问到某 CPU 上运行的进程,这个数据结构定义为 schedule_data 结构,包含两个属性: curr 指针,指向当前运行于该 CPU 上的进程的task_struct,通常用 cp
10、u_curr(cpu)宏来访问;last_schedule 时间戳,记录了上一次该 CPU 上进程切换的时间,通常用 last_schedule(cpu)宏来访问。为了使该数据结构的访问能与 CPU 的 Cache line 大小相一致,schedule_data 被组织到以SMP_CACHE_BYTES 为单位的 aligned_data 联合数组中,系统中每个 CPU 对应数组上的一个元素。5 init_tasks调度器并不直接使用 init_task 为表头的进程链表,而仅使用其中的 “idle_task“。该进程在引导完系统后即处于 cpu_idle()循环中(详见“ 其他核心应用的调
11、度相关部分 “之“IDLE 进程“) 。SMP 系统中,每个 CPU 都分别对应了一个 idle_task,它们的 task_struct 指针被组织到init_tasksNR_CPUS数组中,调度器通过 idle_task(cpu)宏来访问这些 “idle“进程(详见“ 调度器工作流程“) 。6 runqueue_head以 runqueue_head 为表头的链表记录了所有处于就绪态的进程(当前正在运行的进程也在其中,但 idle_task 除外) ,调度器总是从中选取最适合调度的进程投入运行。三 进程切换过程从一个进程的上下文切换到另一个进程的上下文,因为其发生频率很高,所以通常都是调度
12、器效率高低的关键。在 Linux 中,这一功能是以一段经典的汇编代码实现的,此处就着力描述这段代码。这段名为 switch_to()的代码段在 schedule()过程中调用,以一个宏实现:/* 节选自include/asm-i386/system.h */#define switch_to(prev,next,last) do asm volatile(“pushl %esint“ “pushl %edint“ “pushl %ebpnt“ 保存 esi、edi、ebp 寄存器“movl %esp,%0nt“ esp 保存到 prev-thread.esp 中“movl %3,%espnt“
13、 从 next-thread.esp 恢复 esp“movl $1f,%1nt“ 在 prev-thread.eip 中保存“1:“ 的跳转地址,当 prev被再次切换到的时候将从那里开始执行“pushl %4nt“ 在栈上保存 next-thread.eip,_switch_to()返回时将转到那里执行,即进入 next 进程的上下文“jmp _switch_ton“ 跳转到_switch_to(),进一步处理(见下)“1:t“ “popl %ebpnt“ “popl %edint“ “popl %esint“ 先恢复上次被切换走时保存的寄存器值,再从switch_to()中返回。:“=m“
14、 (prev-thread.esp), %0“=m“ (prev-thread.eip),%1“=b“ (last) ebx,因为进程切换后,恢复的栈上的 prev 信息不是刚被切换走的进程描述符,因此此处使用 ebx 寄存器传递该值给 prev:“m“ (next-thread.esp), %3“m“ (next-thread.eip), %4“a“ (prev), “d“ (next), eax,edx“b“ (prev); ebx while (0)进程切换过程可以分成两个阶段,上面这段汇编代码可以看作第一阶段,它保存一些关键的寄存器,并在栈上设置好跳转到新进程的地址。第二阶段在 swi
15、tch_to()中启动,实现在_switch_to()函数中,主要用于保存和更新不是非常关键的一些寄存器(以及 IO 操作许可权映射表 ioperm)的值: unlazy_fpu(),如果老进程在 task_struct 的 flags 中设置了 PF_USEDFPU 位,表明它使用了 FPU,unlazy_fpu() 就会将 FPU 内容保存在 task_struct:thread 中; 用新进程的 esp0(task_struct:thread 中)更新 init_tss 中相应位置的 esp0; 在老进程的 task_struct:thread 中保存当前的 fs 和 gs 寄存器,然后
16、从新进程的task_struct:thread 中恢复 fs 和 gs 寄存器; 从新进程的 task_struct:thread 中恢复六个调试寄存器的值; 用 next 中的 ioperm 更新 init_tss 中的相应内容switch_to()函数正常返回,栈上的返回地址是新进程的 task_struct:thread:eip,即新进程上一次被挂起时设置的继续运行的位置(上一次执行 switch_to()时的标号“1:“位置) 。至此转入新进程的上下文中运行。在以前的 Linux 内核中,进程的切换使用的是 far jmp 指令,2.4采用如上所示的手控跳转,所做的动作以及所用的时间均
17、与 far jmp 差不多,但更利于优化和控制。四 就绪进程选择算法Linux schedule()函数将遍历就绪队列中的所有进程,调用 goodness()函数计算每一个进程的权值 weight,从中选择权值最大的进程投入运行。进程调度权值的计算分为实时进程和非实时进程两类,对于非实时进程(SCHED_OTHER) ,影响权值的因素主要有以下几个:1. 进程当前时间片内所剩的 tick 数,即 task_struct 的 counter 值,相当于 counter 越大的进程获得 CPU 的机会也越大,因为 counter 的初值与(-nice)相关,因此这一因素一方面代表了进程的优先级,另
18、一方面也代表了进程的“欠运行程度“ ;(weight = p-counter;)2. 进程上次运行的 CPU 是否就是当前 CPU,如果是,则权值增加一个常量,表示优先考虑不迁移 CPU 的调度,因为此时 Cache 信息还有效;(weight += PROC_CHANGE_PENALTY;)3. 此次切换是否需要切换内存,如果不需要(或者是同一进程的两个线程间的切换,或者是没有 mm 属性的核心线程) ,则权值加1,表示(稍微)优先考虑不切换内存的进程;(weight += 1;)4. 进程的用户可见的优先级 nice,nice 越小则权值越大。 (Linux 中的 nice 值在-20 到
19、+19之间选择,缺省值为0,nice()系统调用可以用来修改优先级。 )(weight += 20 - p-nice;) 对于实时进程(SCHED_FIFO、SCHED_RR) ,权值大小仅由该进程的 rt_priority 值决定(weight = 1000 + p-rt_priority;) ,1000的基准量使得实时进程的权值比所有非实时进程都要大,因此只要就绪队列中存在实时进程,调度器都将优先满足它的运行需要。如果权值相同,则选择就绪队列中位于前列的进程投入运行。除了以上标准值以外,goodness()还可能返回-1,表示该进程设置了 SCHED_YIELD 位,此时,仅当不存在其他就
20、绪进程时才会选择它。如果遍历所有就绪进程后,weight 值为0,表示当前时间片已经结束了,此时将重新计算所有进程(不仅仅是就绪进程)的 counter 值,再重新进行就绪进程选择(详见“调度器工作流程“) 。五 调度器Linux 的调度器主要实现在 schedule()函数中。1调度器工作流程schedule()函数的基本流程可以概括为四步:1). 清理当前运行中的进程2). 选择下一个投入运行的进程3). 设置新进程的运行环境4). 执行进程上下文切换5). 后期整理其中包含了一些锁操作:就绪队列锁 runquque_lock,全局核心锁 kernel_flag,全局中断锁global_i
21、rq_lock,进程列表锁 tasklist_lock。下面先从锁操作开始描述调度器的工作过程。A. 相关锁 runqueue_lock,定义为自旋锁,对就绪队列进行操作之前,必须锁定; kernel_flag,定义为自旋锁,因为很多核心操作(例如驱动中)需要保证当前仅由一个进程执行,所以需要调用 lock_kernel()/release_kernel()对核心锁进行操作,它在锁定/解锁 kernel_flag 的同时还在 task_struct:lock_depth 上设置了标志,lock_depth 小于0表示未加锁。当发生进程切换的时候,不允许被切换走的进程握有 kernel_flag
22、 锁,所以必须调用 release_kernel_lock()强制释放,同时,新进程投入运行时如果lock_depth0,即表明该进程被切换走之前握有核心锁,必须调用reacquire_kernel_lock()再次锁定; global_irq_lock,定义为全局的内存长整型,使用 clear_bit()/set_bit()系列进行操作,它与 global_irq_holder 配合表示当前哪个 cpu 握有全局中断锁,该锁挂起全局范围内的中断处理(见 irq_enter()) ; tasklist_lock,定义为读写锁,保护以 init_task 为头的进程列表结构。B. prev在 s
23、chedule 中,当前进程(也就是可能被调度走的进程)用 prev 指针访问。对于 SCHED_RR 的实时进程,仅当该进程时间片结束( counter=0)后才会切换到别的进程,此时将根据 nice 值重置 counter,并将该进程置于就绪队列的末尾。当然,如果当前就绪队列中不存在其他实时进程,则根据前面提到的 goodness()算法,调度器仍将选择到该进程。如果处于 TASK_INTERRUPTIBLE 状态的进程有信号需要处理(这可能发生在进程因等待信号而准备主动放弃 CPU,在放弃 CPU 之前,信号已经发生了的情况) ,调度器并不立即执行该进程,而是将该进程置为就绪态(该进程还
24、未来得及从就绪队列中删除) ,参与紧接着的 goodness 选择。如果 prev 不处于就绪态,也不处于上面这种有信号等待处理的挂起态(prev 为等待资源而主动调用 schedule()放弃 CPU) ,那么它将从就绪队列中删除,此后,除非有唤醒操作将进程重新放回到就绪队列,否则它将不参与调度。被动方式启动调度器工作时,当前进程的 need_resched 属性会置位(见下“ 调度器工作时机“) 。在 schedule()中,该位会被清掉,表示该进程已经在调度器中得到了处理(当然,这一处理并不意味着该进程就一定获得了 CPU) 。C. goodness调度器遍历就绪队列中的所有进程,只要它
25、当前可被调度(cpus_runnable current-state = TASK_INTERRUPTIBLE;while (!list-ready) if (file-f_flags break;if (signal_pending(current) retval = -ERESTARTSYS;break;schedule();current-state = TASK_RUNNING; /* 这一句实际上可以省略,因为进程的状态在唤醒过程中就已经恢复到 TASK_RUNNING 了 */remove_wait_queue(其过程通常可分为四步: 将进程添加到事件等待队列中; 置进程状态为 T
26、ASK_INTERRUPTIBLE(或 TASK_UNINTERRUPTIBLE) ; 在循环中检查等待条件是否满足,不满足则调用 schedule(),满足了就退出循环; 将进程从事件等待队列中删除。从“ 调度器工作流程“ 中我们知道,调度器会将处于休眠状态的进程从就绪队列中删除,而只有就绪队列中的进程才有可能被调度到。将该进程重新放到就绪队列中的动作是在事件发生时的“唤醒 “过程中完成的。在以上所示的鼠标驱动中,鼠标中断将调用mousedev_event()函数,该函数的最后就会使用 wake_up_interruptible()唤醒等待鼠标事件的所有进程。wake_up_interrup
27、tible()将最终调用 try_to_wake_up()函数:/* 节选自kernel/sched.c */static inline int try_to_wake_up(struct task_struct * p, int synchronous)unsigned long flags;int success = 0;spin_lock_irqsave(p-state = TASK_RUNNING;if (task_on_runqueue(p)goto out;add_to_runqueue(p); /* 添加到就绪队列中 */if (!synchronous | !(p-cpus_a
28、llowed schedule();check_pgt_cache();初始化过程中第一次执行 cpu_idle(),因 need_resched 为1 ,所以直接启动 schedule()进行第一次调度。如上文所述,schedule()会清掉 need_resched 位,因此,之后本循环都将执行idle()函数,直至 need_resched 再被设置为非0(比如在 reschedule_idle()中,见上“调度器工作时机“) 。idle()函数有三种实现可能: default_idle(),执行 hlt 指令; poll_idle(),如果核心参数上定义了“idle=poll“,则 p
29、m_idle 会指向 poll_idle(),它将need_resched 设置为特殊的 -1,然后反复循环直到 need_resched 不等于-1。因为poll_idle()采用更高效的指令,所以运行效率比 default_idle()要高; 电源管理相关的 idle 过程,例如 APM 和 ACPI 模块中定义的 idle 过程。因为仅当就绪队列为空的时候才会调度到 idle 进程,所以,只有在系统完全空闲时才会执行 check_pgt_cache()操作,清理页表缓存。2. 进程创建系统中除了 init_task 是手工创建的以外,其他进程,包括其他 CPU 上的 idle 进程都是通
30、过do_fork()创建的,所不同的是,创建 idle 进程时使用了 CLONE_PID 标志位。在 do_fork()中,新进程的属性设置为: state:TASK_UNINTERRUPTIBLE pid:如果设置了 CLONE_PID 则与父进程相同(仅可能为0) ,否则为下一个合理的 pid cpus_runnable:全1;未在任何 cpu 上运行 processor:与父进程的 processor 相同;子进程在哪里创建就优先在哪里运行 counter:父进程 counter 值加1的一半;同时父进程自己的 counter 也减半,保证进程不能通过多次 fork 来偷取更多的运行时间
31、(同样,在子进程结束运行时,它的剩余时间片也将归还给父进程,以免父进程因创建子进程而遭受时间片的损失) 其他值与父进程相同子进程通过 SET_LINKS()链入进程列表,然后调用 wake_up_process()唤醒(见上“ 调度器工作时机“) 。3. smp 系统初始化init_task 在完成关键数据结构初始化之后,在进行硬件的初始化之前,会调用 smp_init()对SMP 系统进行初始化。smp_init()调用 smp_boot_cpus(),smp_boot_cpus()对每一个 CPU 都调用一次 do_boot_cpu(),完成 SMP 其他 CPU 的初始化工作。/* 节选
32、自arch/i386/kernel/smpboot.c do_boot_cpu() */if (fork_by_hand() processor = cpu;idle-cpus_runnable = 1 thread.eip = (unsigned long) start_secondary; /* 被调度到后的启动地址 */del_from_runqueue(idle); /* idle 进程不通过就绪队列调度 */unhash_process(idle);init_taskscpu = idle; /* 所有 idle 进程都可通过 init_tasks数组访问 */该进程被调度到时即执行
33、 start_secondary(),最终将调用 cpu_idle(),成为 IDLE 进程。七 Linux 2.4调度系统的一些问题1. 进程时间片2.4内核中进程缺省时间片是根据以下公式计算的:/* 节选自kernel/sched.c */#if HZ 2)#elif HZ 1)#elif HZ counter = (p-counter 1) + NICE_TO_TICKS(p-nice);如上所述,时钟中断将不断对当前运行的非 IDLE 进程进行时间片剩余值减 1的操作,直至所有就绪队列中的 counter 都减为0了,就在 schedule()中对每个进程(包括休眠进程)利用上述公式执
34、行时间片的更新。其中在include/asm-i386/param.h 中定义了 HZ 为100,而counter 通常初值为0,nice 缺省为 0(nice 在-20到19之间选择) ,所以,i386下 counter 的缺省值为6,也就是大约60ms(时钟中断大约每 10ms 一次) 。同时,对于休眠的进程而言,其参与计算的 counter 非0,因此实际上它的 counter 是在累加,构成一个等比数列 COUNTER=COUNTER/2+k,1k=11 ,其最大值趋近于2*k,也就是说,2.4系统中进程的时间片不会超过230ms。因为就绪进程选取算法中 counter 的值占很大比重
35、(见“就绪进程选择算法“) ,因此,这种对于休眠进程时间片叠加的做法体现了 Linux 倾向于优先执行休眠次数比较多,也就是 IO密集(IO-bound )的进程。Linux 设计者最初是希望因此而提高交互式进程的响应速度,从而方便终端用户,但 IO 密集的进程并不一定就是交互式进程,例如数据库操作需要频繁地读写磁盘,从而经常处于休眠状态,动态优先级通常较高,但这种应用并不需要用户交互,所以它反而影响了真正的交互动作的响应。时间片的长度对系统性能影响也很大。如果太短,进程切换就会过于频繁,开销很大;如果太长,系统响应就会太慢,Linux 的策略是在系统响应不至于太慢的前提下让时间片尽可能地长。
36、2. 内核不可抢占从上面的分析我们可以看到,schedule()是进行进程切换的唯一入口,而它的运行时机很特殊。一旦控制进入核心态,就没有任何办法可以打断它,除非自己放弃 cpu。一个最典型的例子就是核心线程中如果出现死循环(只要循环中不调用 schedule()) ,系统就会失去响应,此时各种中断(包括时钟中断)仍然在响应,但却不会发生调度,其他进程(包括核心进程)都没有机会运行。下面给出的是中断返回的代码:/* 节选自arch/i386/entry.S */ENTRY(ret_from_intr)GET_CURRENT(%ebx) #将 current 指针存到 ebx 寄存器中备用ret
37、_from_exception:movl EFLAGS(%esp),%eax #取 EFLAGS 中的 VM_MASK 位判断是否处于VM86模式movb CS(%esp),%al #取 CS 低两位判断是否处于用户态testl $(VM_MASK | 3),%eax jne ret_from_sys_call #如果处于 VM86模式或者处于用户态,就从ret_from_sys_call 入口返回,否则直接返回jmp restore_all这是此时唯一可能调用 schedule()的地方(通过 ret_from_sys_call,见“ 调度器工作时机“) ,但普通的核心线程不属于任何一种要求
38、的状态,它能响应中断,但不能导致调度。这个特点的表现之一就是,高优先级的进程无法打断正在核内执行系统调用(或者中断服务)的低优先级进程,这对于实时系统来说是致命的,但却简化了核心代码。内核中很多地方都利用了这一特点,能够不做过多保护地访问共享数据,而不用担心其他进程的打扰。3. 实时性能Linux 2.4通过就绪进程选择算法的设计区分实时进程和非实时进程,只要有实时进程可运行,非实时进程就不会获得运行机会。Linux 又将实时进程分为 SCHED_RR 和SCHED_FIFO 两类。SCHED_RR 时间片结束后会发生调度,并将自己置于就绪队列的末尾,从而给其他 rt_priority 相同(
39、或更高)的实时进程运行机会(见“调度器工作流程“ ) ,而 SCHED_FIFO 不会因时间片结束而放弃 CPU(见“ 调度器工作时机“ ) ,或者出现更高优先级的实时进程,或者主动放弃 CPU,否则 SCHED_FIFO 将运行到进程结束。尽管 Linux 2.4中区分了实时进程和非实时进程的调度优先权,但也仅此而已。不支持核心抢占运行的操作系统很难实现真正的实时性,因为实时任务的响应时间无法预测。有两种办法使系统的实时性更好,一种是采用设置类似抢占调度点的做法,一种就是使内核真正具备可抢占性。即使是内核可抢占的系统,也并不一定满足实时性要求,它仅仅解决了 CPU 资源的访问优先权问题,其他
40、资源也同样需要“被抢占“ ,例如实时进程应该能够从握有某个共享资源的普通进程手中夺得它所需要的资源,它使用完后再还给普通进程。但实际上,很多系统都无法做到这一点,Linux 的调度器更是不具备这种能力。4. 多处理机系统中的局限性Linux 的调度器原本是针对单处理机系统设计的,在内核发展过程中,不断通过补丁来提高多处理机系统(主要是 SMP 系统)的执行效率。这种开发方式一直持续到2.4版本,因此在2.4内核中,SMP 应用仍然有很多无法突破的障碍,例如全局共享的就绪队列。很多研究团体都在针对 Linux 调度器的多处理机扩展性作研究,参考文献中列举了其中两个56,但最权威的改进还是在2.6
41、内核中。对于超线程 CPU,Linux 调度器的支持有限,它可以区分同一物理 CPU 上的两个逻辑CPU,在两个逻辑 CPU 都空闲的时候,调度器可以优先考虑将进程调度到其中一个逻辑CPU 上运行(见“ 调度器工作流程“) 。从原理上说,超线程 CPU 是存在两个(或多个)执行现场的单 CPU,只有两个使用 CPU 不同部件(比如定点部件和浮点部件)的线程在其上运行的时候才有正的加速,否则,由于执行部件冲突以及 Cache miss,使用超线程技术甚至会带来一定程度上的性能损失。Linux 2.4的调度器并不能区分哪些进程是“类似“ 的,哪些进程会使用不同的执行部件,因此,实际上无法恰当使用超
42、线程 CPU。对于其他更复杂的多处理机系统,例如目前高端系统中占统治地位的 NUMA 结构机器,Linux 在调度器上基本未作考虑。例如进程(线程)总优先在创建它的 CPU 上运行(见“其他核心应用的调度相关部分“之“ 进程创建“) ,并倾向于保持在该 CPU 上(见“就绪进程选择算法“ ) ,整个CPU 选择过程没有做任何局部性优化。八 后记调度系统的表现关系到整个系统的性能,Linux 的应用目前主要集中在低端服务器系统和桌面系统,将来很可能向高端服务器市场和嵌入式系统发展,这就要求调度系统有大的改动。在新的 Linux 内核2.6版本中,调度器的改动是最引人注目的,它一方面提供了核心可抢占的支持,另一方面又对多处理机系统上的表现进行了优化。在熟悉了2.4的调度系统之后,我们将分析2.6中调度器的表现。