进程(下)

参考:进程(上)

三. 进程切换

为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这称为进程切换。

3.1 硬件上下文

尽管每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此,在恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。
进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在Linux中,(intel)进程硬件上下文存放在TSS段,而剩余部分存放在内核态堆栈中。
进程切换只发生在内核态。在执行进程切换之前,用户态进程使用的所有寄存器内容都已保存在内核态堆栈上,这也包括SS和ESP这对寄存器的内容。

3.2 任务状态段(TSS)

TSS是用来存放硬件上下文的。但是linux没有使用TSS来做硬件上下文的切换。
tss_struct结构描述TSS的格式。init_tss数组为系统上每个不同的CPU存放一个TSS。在每次进程切换时,内核都更新TSS的某些字段以便相应的CPU控制单元可以安全地检索到它需要的信息。因此,TSS反映了CPU上当前进程的特权级,但不必为没有运行的进程保留TSS。
每个TSS有一个8字节的任务状态段描述符(TSSD)。由Linux创建的TSSD存放在全局描述符标(GDT)中,GDT的基地址存放在gdtr寄存器中,tr寄存器存放TSSD选择符。

3.2.1 thread字段

linux中,被替换的进程的硬件上下文没有保存在TSS中,他们保存在task_struct的thread字段中。这个数据结构包含的字段涉及大部分CPU寄存器,但不包括诸如eax、ebx等通用寄存器,它们保留在堆栈中。

3.3 执行进程切换

进程切换可能只发生在精心定义的点:schedule()函数(后边会详细讲到)。这里,我们仅关注内核如何执行一个进程切换:

  • 切换页全局目录以安装一个新的地址空间(后续详细讲解);
  • 切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器。

3.3.1 switch_to宏

硬件上下文的切换,由switch_to宏开始。这个宏跟体系结构相关。这个宏的最后一个参数last用来保存被切换出去的进程的值,宏的prev和next两个参数分别表示正在执行的进程和即将执行的进程,prev和next都是task_struct指针类型。x86下switch_to步骤如下:

  • 在两个通用寄存器eax、edx中保存prev和next的值;
  • 保存eflags和ebp寄存器(栈底)到prev内核栈中;
  • 保存esp(栈顶)的内容到prev->thread.esp;
  • 将next->thread.esp装入esp。此时,内核开始在next的内核栈上操作。这条指令实际完成了从prev到next进程的切换。此时current宏的返回值变指向next;
  • 将prev进程下一条要被执行的指令地址保存到prev->thread.eip(实际被保存的指令并发进程B的指令,而是switch_to宏的切换指令);
  • 将next->thread.eip的值压入next的内核栈;
  • 执行switch_to函数;
  • 进程prev再次获得CPU,esp指向prev进程。将eflags和ebp(栈低)压栈;
  • 将eax的值保存到第三个参数last中。

3.3.2 __switch_to()函数

__switch_to函数也是体系结构相关的。这个函数跟普通函数的参数传递区别是:普通函数通过栈传递参数,此函数使用switch_to宏保存的eax和edx作为prev和next的参数,实现方式是通过gcc的扩张编译器宏
函数执行过程:

  • 有选择地保存FPU、MMX及XMM寄存器的内容;
  • 将next->thread.esp0装入本地CPU的TSS的esp0字段;
  • 将next进程使用的线程局部存储(TLS)段装入本地CPU的全局描述符表;
  • 将fs,gs寄存器的内容分别存放在prev->thread.fs和prev->thread.gs中(fs/gs/ds/es都是数据段);
  • 如果fs或者gs被prev和next中任一使用(值非0),就将next->thread中存放的fs和gs装入寄存器fs和gs;
  • 如果next进程正在被调试,则装入next->thread中的debugreg数组的内容到对应寄存器;
  • 根据需要,更新TSS的I/O bitmap;

四 创建进程

传统的Unix操作系统以统一的方式对待所有的进程:子进程复制父进程所拥有的资源。 这种方式使进程的创建非常慢且效率低,因为子进程需要拷贝父进程的整个地址空间。而且,很多时候子进程几乎不必读或修改父进程拥有的任何资源。
现代Unix内核通过引入以下机制解决了这个问题:

  • 写时拷贝技术。允许父子进程读相同的物理页。任何一个进程试图写一个物理页时,内核就把这个页的内容拷贝到一个新的物理页,并把这个物理页映射给正在写的进程。
  • 轻量级进程允许父子进程共享各进程在内核的很多数据结构,如页表(也就是整个用户态地址空间)、打开文件表及信号处理。
  • vfork()系统调用创建的进程能共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,直到子进程退出或执行新的程序。

4.1 clone()、fork()及vfork()系统调用

Linux中,轻量级进程是由clone()函数创建的。

int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, void *newtls, pid_t *ctid */ );
fn:新进程执行的函数, 函数的返回值表示子进程的退出代码。
arg: fn()函数的参数
flags: 控制子进程创建或退出等的某些行为,例如结束时候发送的信号代码,子进程创建完是否立即执行,是否共享页表等。
child_stack:表示把用户态堆栈指针赋给子进程的esp寄存器。父进程应该总是为子进程分配新的堆栈。
tls: 表示线程局部存储段(TLS)数据结构的地址,该结构是为新轻量级进程定义的。设置flags标志CLONE_SETTLS有效

传统的fork() 系统调用在Linux中是用clone()实现的,其中flags参数只设置了SIGCHLD信号,child_stack参数是父进程当前的堆栈指针。当父子进程之一试图入栈的时候,写时拷贝机制会产生一份栈的拷贝,之后两个进程都使用各自独立的栈。

4.1.1 do_fork()函数

内核中的do_fork()函数负责处理clone()系统调用。

/*
 *  Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 */
long do_fork(unsigned long clone_flags,
	      unsigned long stack_start,
	      struct pt_regs *regs,
	      unsigned long stack_size,
	      int __user *parent_tidptr,
	      int __user *child_tidptr)
clone_flags:即clone()系统调用的flags
stack_start:即clone()系统调用参数child_stack
regs: 指向通用寄存器值的指针,通用寄存器的值是在用户态切换内核态时保存到内核态堆栈中的。
stack_size:未使用

do_fork()利用辅助函数copy_process()来创建进程描述符以及子进程执行所需要的所有其他内核数据结构。下面是do_fork()执行的主要步骤:

  • 通过查找pid_map_array位图,为子进程分配新的PID。
  • 检查自己是否可被跟踪,如果可以,并且需要被跟踪,设置CLONE_PTRACE标志。
  • 调用copy_process()复制进程描述符。如果所有必须的资源都是可用的,该函数返回刚创建的task_struct描述符的地址。这是创建过程的关键步骤,后文详述。
  • 如果flags设置了CLONE_STOPPED,或者必须跟踪子进程;则将进程状态设置成TASK_STOPPED,并为子进程增加挂起的信号SIGSTOP信号。
  • 如果flags没有设置CLONE_STOPPED标志,则调用wake_up_new_task()函数以执行下述操作:
    – 调整父进程和子进程的调度参数,以后详述
    – 如果子进程将和父进程运行在同一个CPU上,而且父进程和子进程不能共享同一组页表(flags标志CLONE_VM未设置),那么,就把子进程插入父进程运行队列,插入时,子进程刚好在父进程前面,这样可以是子进程先于父进程运行。如果子进程刷新其地址空间(使用新的页表),这样可以获得更好的性能。如果让父进程先运行,那么写时复制将会造成不必要的页面复制。
    – 如果子进程和父进程运行在不同的CPU上,或者父子进程共享页表,就将子进程插入父进程运行队列的队尾。
  • 如果CLONE_STOPPED标志被设置,就把子进程设置为TASK_STOPPED状态。
  • 进程被跟踪的处理
  • 如果flags设置了CLONE_VFORK标志,则把父进程插入等待队列,并挂起父进程直到子进程释放自己的内存空间(即子进程结束或执行新的程序)。
  • 结束并返回子进程的PID。

4.1.2 copy_process()函数

4.2 内核线程

4.2.1 创建一个内核线程

4.2.2 进程0

4.2.3 进程1

五 撤销进程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章