进程切换:
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行,这种行为被称为进程切换、任务切换或上下文切换。
硬件上下文:
尽管每个进程可以拥有属于自己的地址空间,但所有进程必须共享CPU寄存器。因此,在恢复一个进程的执行之前,内核必须确保每个寄存器装入了挂起进程时的值。
进程恢复执行前必须装入寄存器的一组数据称为硬件上下文。硬件上下文是进程可执行上下文的一个子集,因为可执行上下文包含进程执行时需要的所有信息。在Linux中,进程硬件上下文的一部分存放在任务状态(TSS)段,而剩余部分存放在内核态堆栈中。进程切换只发生在内核态。
执行进程切换:
从本质上说,每个进程切换由两步组成:
1、切换页全局目录以安装一个新的地址空间
2、切换内核态堆栈和硬件上下文
创建进程:
Unix操作系统依赖进程创建来满足用户的需求。
现代Unix内核引入了三种不同机制解决进程创建问题:
1、写时复制技术,允许子进程读相同的物理页。只要两者中有一个试图写一个物理页,内核就把这个页的内容拷贝到一个新的物理页,并把这个新的物理页分配给正在写的进程。
2、轻量级进程允许父子进程共享每个进程在内核的很多数据结构
3、vfork()系统调用创建的进程能共享其父进程的内存地址空间。为了防止父进程重写子进程需要的数据,阻塞父进程的执行,一直到子进程退出或执行一个新的程序为止。
Clone()、fork()及vfork()系统调用:
在Linux中,轻量级进程是由名为clone()的函数创建的。clone()是在C语言库中定义的一个封装函数,它负责建立新的轻量级进程的堆栈,并调用对编程者隐藏的clone()系统调用。
传统的fork()以及vfork()系统调用在Linux中也是用clone()实现的。
do_fork()函数负责处理clone()、fork()和vfork()系统调用。
内核线程:
现代操作系统将一些重要的任务,如刷新磁盘高速缓存,交换出不用的页框,维护网络连接等,委托给内核线程,内核线程不受不必要的用户态上下文拖累。
在Linux中,内核线程在以下几方面不同于普通进程:
1、内核线程只运行在内核态,而普通进程既可以运行在内核态,也可以运行在用户态。
2、因为内核线程只运行在内核态,它只使用大于PAGE_OFFSET的线性地址空间,而不管在用户态还是内核态,普通进程可以使用4GB的线性地址空间。
kernel_thread()函数创建一个新的内核线程,它接受的参数有:所要执行的内核函数的地址、要传递给函数的参数、一组clone标志。该函数本质上调用do_fork()。
所有的进程的祖先叫做进程0,idle进程,或者因为历史原因叫做swapper进程,它是在Linux初始化阶段从无到有创建的一个内核线程。stark_kernel()函数初始化内核需要的所有数据结构,激活中断,创建另一个叫进程1的内核线程(init进程)。新创建内核线程的PID为1,并与进程0共享进程所有的内核数据结构。
创建init进程后,进程0执行cpu_idle()函数,该函数本质上是在开中断的情况下重复执行hlt汇编语言指令。只有当没有其它进程处于TASK_RUNNING状态时,调度程序才选择进程0。
在多处理器系统中,每个CPU都有一个进程0.只要打开电源,计算机的BIOS就会启动某一个CPU,同时禁用其它CPU。运行在CPU0上的swapper进程初始化内核数据结构,然后激活其它CPU,并通过copy_process()函数创建另外的swapper进程,把0传递给新创建的swapper进程作为它们的新PID。此外,内核把适当的CPU索引赋给内核所创建的每个进程的thread_info描述符的cpu字段。
由进程0创建的内核线程执行init()函数,init()依次完成内核初始化。init()调用execve()系统调用装入可执行程序init,结果,init内核线程变为一个普通进程,且拥有自己的每进程内核数据结构。在系统关闭之前,init进程一直存活,因为它创建和监控在操作系统外层执行的所有进程的活动。
撤消进程:
进程终止的一般方式是调用exit()库函数,该函数释放C函数库所分配的资源,执行编程者所注册的每个函数,并结束从系统回收进程的那个系统调用。
进程终止:
在Linux2.6中有两个终止用户态应用的系统调用:
1、exit_group()系统调用,它终止整个线程组,即整个基于多线程的应用。do_group_exit()是实现这个系统调用的主要内核函数。
2、exit()系统调用,它终止某一个线程,而不管该线程所属线程组中的所有其它进程。do_exit()是实现这个系统调用的主要内核函数。
进程删除:
Unix允许进程查询内核以获得其父进程的PID,或者其任何子进程的执行状态,包括终止状态。因此,不允许Unix内核在进程一终止后就丢弃包含在进程描述符字段中的数据,只有父进程发出了与被终止进程相关的wait()类系统调用之后,才允许这样做。这就是引入僵死状态的原因:尽管从技术上来说进程已死,但必须保存它的描述符,直到父进程得到通知。
如果发生父进程在子进程结束之前结束的情况,系统中会到处是僵死的进程,而且它们的进程描述符会永久占据着RAM,因此,必须强迫所有的孤儿进程成为init进程的子进程。这样,当init进程用wait()类系统调用检查其合法的子进程终止时,就会撤消僵死进程