首页 > 代码库 > 《内核设计与实现》读书笔记(三)- 进程管理

《内核设计与实现》读书笔记(三)- 进程管理

进程是所有操作系统的核心概念,同样在linux上也不例外。

主要内容:

  • 进程和线程
  • 进程的创建
  • 进程的终止

1. 进程和线程

1.1 进程

进程是处于执行期的程序以及相关的资源的总称。

线程是进程中活动的对象。内核调度的对象是线程,而不是进程。

进程和线程的管理操作(比如创建和销毁)都是由内核来实现的。

Linux中的进程于Windows相比是很轻量级的,而且不严格区分进程和线程,线程不过是一种特殊的进程。

所以下面只讨论进程,只有当线程与进程存在不一样的地方时才提一下线程。

 

进程提供2中虚拟机制:虚拟处理器和虚拟内存

每个进程有独立的虚拟处理器和虚拟内存,

每个线程有独立的虚拟处理器,同一个进程内的线程有可能会共享虚拟内存。

1.2 进程描述符和任务结构

内核把进程的列表存放在叫做任务队列(task list)的双向循环链表中。链表中的每一项都是类型为task_struct称为进程描述符(process descriptor)的结构中(include/linux/sched.h)

进程描述符中包含的数据能完整地描述一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信息,进程的状态,还有其它很多信息。

内核通过一个唯一的进程标识值或PID来标识每个进程。

在内核中,访问任务通常需要获得指向其task_struct的指针。因为,内核中大部分处理进程的代码都是直接通过task_struct进行的。因此,通过current宏查找到当前正在运行进程的进程描述符的速度尤为重要。

1.3 进程状态

进程描述符中的state域描述了进程的当前状态。系统中的每个进程必然处于五种进程状态的一种。

TASK_RUNNING:运行

TASK_INTERRUPTIBLE:可中断的

TASK_UNINTERRUPTIBLE:不可中断的

TASK_TRACED:被其他进程跟踪的进程

TASK_STOPPED:停止

1.3.1 进程状态转换

技术分享

1.3.2 设置当前进程状态

内核经常要调整某个进程状态。这时最好使用set_task_state(task, state)函数:

set_task_state(task, state);  /* 将任务task的状态设置为state */

1.4 进程上下文

可执行程序代码是进程中的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。

一般程序在用户空间执行,但当一个程序执行了系统调用或处罚了某个异常,它就陷入了内核空间。这时,我们称内核“代表进程执行”并处于进程上下文中。

系统调用和异常处理程序是对内核明确定义的接口。进程只有通过这些接口才能陷入内核执行---对内核的所有访问都必须通过这些接口。

1.5 进程家族树

Linux系统的进程之间存在一个明显的继承关系。所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。该进程读取系统的初始化脚本(initscript)并执行其他的相关程序,最终完成系统启动的整个过程。

系统中的每个进程必有一个父进程,相应的,每个进程也可以拥有另个或多个子进程。拥有同一个父进程的所有进程称为兄弟。

进程间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程task_struct叫做parent的指针,还包含一个称为children的子进程链表。

对于当前进程:

获得父进程的进程描述符:struct task_struct *my_parent = current -> parent;

依次访问子进程:struct task_struct *task;

        struct list_head *list;

        list_for_each(list, &current -> children) {

          task = list_entry(list, struct task_struct, sibling);  /* task现在指向当前的某个子进程*/

        };

init进程的进程描述符是作为init_task静态分配的。

2. 进程的创建

Linux系统的进程的创建于其他操作系统的进程创建(spawn机制)不同,Linux系统的进程创建分两个步骤执行:fork()和exec()

fork:通过拷贝当前进程创建一个子进程

exec:读取可执行文件并将其载入地址空间开始运行

2.1 写时拷贝

传统的fork()系统调用直接把所有的资源复制给新建的进程,这种实现效率低,因为它拷贝的数据也许并不共享。

为了提高效率,Linux的fork()使用写时拷贝(copy-on-write),只有在需要写入的时候,数据才会被复制,即资源的复制只有在需要写入的时候才进行,在此之前只是以只读方式共享。

fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的进程描述符。

2.2 fork()

Linux通过clone()系统调用实现fork()。clone()这个系统调用通过一系列的参数标志来指明父、子进程需要共享的资源。fork()、vfork()和__clone()库函数都根据各自需要的参数去调用clone(),然后由clone()去调用do_fork().

do_fork()完成创建中的大部分工作。该函数调用copy_process()函数。

 

copy_process()创建的流程:

 

  1. 调用dup_task_struct()为新进程分配内核栈、thread_info结构和task_struct等,其中的内容与父进程相同(描述符完全相同)。
  2. 检查并确保新建的子进程(进程数目是否超出上限等)
  3. 清理新进程的信息(比如PID置0等),使之与父进程区别开。
  4. 新进程状态置为 TASK_UNINTERRUPTIBLE
  5. 调用copy_flags()以更新task_struct的flags成员。
  6. 调用alloc_pid()为新进程分配一个有效的PID
  7. 根据clone()的参数标志,拷贝或共享相应的信息
  8. 做一些扫尾工作并返回一个指向子进程的指针

copy_process()函数成功返回后,回到do_fork(),新创建的子进程被唤醒并让其投入运行。

2.3 线程在Linux中的实现

线程机制提供了同一程序内共享内存的地址空间运行的一组线程。线程机制支持并发程序设计技术。

Linux实现线程机制是把所有的线程都当做进程来实现。

2.3.1 创建线程

创建线程和进程的步骤一样,只不过在调用clone()的时候需要传递一些参数标志来指明需要共享的资源。

比如,通过一个普通的fork来创建进程,相当于:clone(SIGCHLD, 0)

创建一个和父进程共享地址空间,文件系统资源,文件描述符和信号处理程序的进程,即一个线程:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)

2.3.2 内核线程

内核经常需要在后台执行一些操作,这种任务可以通过内核线程(kernel thread)完成。

在内核中创建的内核线程与普通的进程之间还有个主要区别在于:内核线程没有独立的地址空间,它们只能在内核空间运行。

而内核进程和普通进程一样,可以被调度,也可以被抢占。

3.进程终止

 

《内核设计与实现》读书笔记(三)- 进程管理