从用户的角度来理解,进程就是程序的执行过程,例如我们在shell下敲入一个nonbuild-in的命令,我们的she'll就会fork出一个进程来执行这个任务。
(1) 什么是多任务
地球人都知道,我们现在使用的Linux都是多任务的操作系统。下面就唠一唠多任务的概念。
什么是多任务?执行完一个任务再接着执行另一个任务(传说中石器时代,计算机还真是这样处理任务的)就是多任务? NO,必须多个任务“同时”执行才能算是多任务操作系统。此处的“同时”只是指在用户角度感觉是同时执行。如果只有一个CPU,显然它是没有办法同时执行两个任务的。因此,让用户感到是多个任务在同时执行才是多任务操作系统的目的。
(2)从kernel看进程
如何才能够在单个CPU上运行多个进程呢?很自然的想法就是保存老进程的状态,然后加载新进程的状态。对,Linux就是这样子实现的。那么老进程需要保存那些信息呢?让我们来看看Linux是如何完成的。
在Linux下,进程切换通常存在以下情况:
(1)进程主动放弃继续执行,例如yield系统调用和一些阻塞型的系统调用会引发进程切换,某些异常的发生也会导致进程切换的发生。
(2)由内核主动暂停进程的执行,转而执行别的进程或中断。例如中断的发生和更高优先级进程的唤醒,或者进程消耗完时间片等等因素,都会导致内核暂停动迁进程的执行。
Linux 2.4(i386平台)使如下代码进行进程切换:
//sched.c schedule()函数的部分代码
{
prepare_to_switch(); //在i386体系下该函数什么也不执行
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;
if (!mm) {
BUG_ON(next->active_mm);
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next, this_cpu);
} else {
BUG_ON(next->active_mm != mm);
switch_mm(oldmm, mm, next, this_cpu);
}
if (!prev->mm) {
prev->active_mm = NULL;
mmdrop(oldmm);
}
}
switch_to(prev, next, prev);
__schedule_tail(prev);
}
此处应注意对内核线程的处理方法。判断线程是不是内核线程的有效方法就是判断PCB中的内存描述符是否为空。如果内存描述符为空,则就是内核线程;否则就是普通线程。下面函数为页表切换的程序:
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu)
{
if (prev != next) {
/* stop flush ipis for the previous mm */
clear_bit(cpu, &prev->cpu_vm_mask);
#ifdef CONFIG_SMP
cpu_tlbstate[cpu].state = TLBSTATE_OK;
cpu_tlbstate[cpu].active_mm = next;
#endif
set_bit(cpu, &next->cpu_vm_mask);
/* Re-load page tables */
load_cr3(next->pgd);
/* load_LDT, if either the previous or next thread
* has a non-default LDT.
*/
if (next->context.size+prev->context.size)
load_LDT(&next->context);
}
#ifdef CONFIG_SMP
else {
cpu_tlbstate[cpu].state = TLBSTATE_OK;
if(cpu_tlbstate[cpu].active_mm != next)
out_of_line_bug();
if(!test_and_set_bit(cpu, &next->cpu_vm_mask)) {
/* We were in lazy tlb mode and leave_mm disabled
* tlb flush IPI delivery. We must reload %cr3.
*/
load_cr3(next->pgd);
load_LDT(&next->context);
}
}
#endif
}
以上应注意对于具有相同内存描述符的管理措施,具有相同描述符只有两种情况:1. 至少其中之一是内核线程;2. 或者两个共享相同页表的普通进程。如果内存描述符相同,则页表切换不会发生。页表切换的代价是相当巨大的,TLB中的内容全部失效,这意味着更多次的访问内存甚至硬盘。
此处也给我们较好的提示:在应用程序开发中,能用线程则尽量用线程来解决,这样就能将不同执行流程之间的切换代价降至最小。
以下代码为386平台下的切换代码:
#define switch_to(prev,next,last) do { /
asm volatile("pushl %%esi/n/t" /
"pushl %%edi/n/t" /
"pushl %%ebp/n/t" /
"movl %%esp,%0/n/t" /* save ESP */ /
"movl %3,%%esp/n/t" /* restore ESP */ /
"movl $1f,%1/n/t" /* save EIP */ /
"pushl %4/n/t" /* restore EIP */ / //将返回地址压栈,__switch_to函数返回会自动执行1后的代码
"jmp __switch_to/n" /
"1:/t" / "popl %%ebp/n/t" /
"popl %%edi/n/t" /
"popl %%esi/n/t" /
:"=m" (prev->thread.esp),"=m" (prev->thread.eip), /
"=b" (last) /
:"m" (next->thread.esp),"m" (next->thread.eip), /
"a" (prev), "d" (next), /
"b" (prev)); /
} while (0)
// 进程切换,在Linux2.4中每个CPU只使用了一个TSS,因此当进程切换时,需要频繁的更改TSS。
void fastcall __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
struct thread_struct *prev = &prev_p->thread,
*next = &next_p->thread;
struct tss_struct *tss = init_tss + smp_processor_id();
unlazy_fpu(prev_p); //保存FPU,MMX,XMM寄存器
/*
* Reload esp0, LDT and the page table pointer:
*/
tss->esp0 = next->esp0;
/*
* Save away %fs and %gs. No need to save %es and %ds, as
* those are always kernel segments while inside the kernel.
*/
asm volatile("mov %%fs,%0":"=m" (prev->fs));
asm volatile("mov %%gs,%0":"=m" (prev->gs));
/*
* Restore %fs and %gs.
*/
loadsegment(fs, next->fs);
loadsegment(gs, next->gs);
/*
* Now maybe reload the debug registers
*/
if (next->debugreg[7]){
loaddebug(next, 0);
loaddebug(next, 1);
loaddebug(next, 2);
loaddebug(next, 3);
/* no 4 and 5 */
loaddebug(next, 6);
loaddebug(next, 7);
}
if (prev->ioperm || next->ioperm) {
if (next->ioperm) {
/*
* 4 cachelines copy ... not good, but not that
* bad either. Anyone got something better?
* This only affects processes which use ioperm().
* [Putting the TSSs into 4k-tlb mapped regions
* and playing VM tricks to switch the IO bitmap
* is not really acceptable.]
*/
memcpy(tss->io_bitmap, next->io_bitmap,
IO_BITMAP_BYTES);
tss->bitmap = IO_BITMAP_OFFSET;
} else
/*
* a bitmap offset pointing outside of the TSS limit
* causes a nicely controllable SIGSEGV if a process
* tries to use a port IO instruction. The first
* sys_ioperm() call sets up the bitmap properly.
*/
tss->bitmap = INVALID_IO_BITMAP_OFFSET;
}
}
此处会保存FPU,MMX,XMM等寄存器,内核堆栈切换终于发生。状态段加载IO允许位图。
参考文献:
1. 《Understanding Linux Kernel》
2. Linux 2.4 部分源码