从用户的角度来理解,进程就是程序的执行过程,例如我们在shell下敲入一个nonbuild-in的命令,我们的shell就会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_SMPelse { 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 部分源码