Linux内核设计与实现:进程管理

Linux内核设计与实现:进程管理

进程

  1. 进程时处于执行期的程序。进程拥有其他资源,如打开的资源,挂起的信号内核内部数据,处理器状态,一个或多个具有内存映射的内存地址空间及一个或多个执行线程(thread of execution),存放全局数据量的数据段。
  2. 线程时进程中活动的对象。是内核调度的最小单位。内核调度的对象是线程,而不是的进程。线程拥有一个独立的程序计数器、进程栈和一组进程寄存器。
  3. 进程提供两种虚拟机制:虚拟处理器和虚拟内存。同一个进程内的线程相互可以共享内存。

进程描述符及任务结构

  1. 内核把进程列表存放在双向循环链表中(task list)。链表中的每一项都是类型为task_struct的进程描述符(process descriptor),在32位机器上大约有1.7KB 。

进程描述符及任务队列

  1. 内核通过PID(process identification value)来标识每个进程。PID是一个int类型,为了兼容老版本Unix和Linux,PID最大值是32768 。不过可以通过修改/proc/sys/kernel/pid_max来修改限制。

  2. 进程状态

    1. TASK_RUNNING(运行) 进程是可执行的;它正在执行,或者在运行队列中执行。这是进程在用户空间中执行的唯一可能状态。这种状态也可以在应用到内核空间中正在执行的进程。
    2. TASK_INTERRUPTIBLE(可中断)进程正在睡眠(被阻塞),等待某种条件的达成。一旦达成条件,内核就把进程设置为运行。此状态的进程也可能因为接收到信号而提前被唤醒并随时投入运行。
    3. TASK_UNNITERRUPTIBLE(不可中断)接收到信号不做响应。其他和可终端状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时候出现。
    4. __TASK_TRACED(被跟踪) 被其他进程跟踪的进程。
    5. __TASK_STOPPED(停止)进程停止执行。通常在接收到SIGSTOP/SIGTSTP/SIGTTIN/SUGTTOU等信号时候发生

    进程状态转化

  3. 可以通过set_task_state(task,state)函数来设置进程状态。等价于task->state = state。

  4. 应用程序启动时代码通过可执行文件载入到地址空间(一般时用户空间)中执行。当一个程序执行了系统调用或触发了某个异常时,它就陷入内核空间。一般情况下,在内核退出时,程序会恢复到用户空间继续执行。

  5. Linux/Unix系统在内核启动的最后阶段启动PID为1的init进程。此进程时所有进程的父进程(祖进程)。每个进程必须有一个父进程,拥有同一个父进程的所有进程互称为兄弟进程。进程间关系存放在进程描述符中,每个task_struct都包含一个指向其父进程task_struct的parent指针和一个children的子进程链表。

进程创建

  1. 进程创建

    1. 其他操作系统一般提供进程创建机制(spawn),现在新的地址空间船舰进程,读入可执行文件,最后开始执行。
    2. Unix/Linux通过两个单独的函数fork和exec分步创建和执行。fork通过拷贝当前线程创建一个子进程,子进程与父进程的区别仅仅在于PID/PPID和某些资源和统计量不同。exec负责读取可执行文件并将其载入到地址空间执行。

    2.Linux在fork时使用写时拷贝技术(copy-on-write)实现。在父进程fork子进程时并非把父进程的数据全部拷贝一份,而是引用了父进程的数据副本,以只读方式共享,只有在子进程需要写入数据的时候才开始复制父进程的副本。如果子进程执行代码无需写入父进程共享的数据,那么不需要拷贝父进程数据,可以大量节省资源和时间。

    3.Linux通过clone()系统调用实现fork(),clone()实际调用的是定义在kernel/fork.c 的do_fork(),该函数调用copy_process()函数,copy_process函数主要完成以下工作:

    1. 调用dup_task_struct()为新进程创建一个内核栈、thead_info结构和task_struct,这些值与当前线程的值相同。此时子进程与父进程的描述符是完全相同的。
    2. 检查并确保新创建这个子线程后,当前用户所拥有的进程数目没有超出给它分配的资源限制。
    3. 子进程左手清理进程描述符内的许多成员,主要是统计信息。task_struct的大多成员依然包吃不变。
    4. 子进程状态被设置为TASK_UNINTERRUPTIABLE,以保证它不会投入运行。
    5. copy_process()调用copy_flags()更新task_sturct的flags成员。
    6. 调用alloc_pid()为新进程分配一个有效的pid。
    7. 根据传递给clone的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。在一般情况下,这些资源会被给定进程的所有线程共享,否则这些资源对每个进程是不同的。
    8. 最后,做一些扫尾工作,并返回子进程的指针。

线程在Linux中的实现

  1. 线程机制支持并发程序设计技术(concurrent programming),在多处理器上能保证真正的并行处理(parallelism)。

  2. Linux线程实现非常独特。从内核角度上并没有线程概念。Linux把所有的线程都当作进程实现。内核并没有准备特别的调度算法或定义特别的数据结构来标识线程,相反,线程仅仅被视作为一个与其他进程共享某些资源的进程

  3. Windows或Solaris在内核中提供了专门支持线程的机制。这些系统常常把线程称为轻量级进程(lightweight processes)。相对于其他系统的进程,Linux进程本身已经很轻量了。

  4. 使用clone()创建线程时传递一些参数标志位指明需要共享的资源:

    参数标志 含义
    CLONE_FILES 父子进程共享打开的文件
    CLONE_FS 父子进程共享文件系统信息
    CLONE_IDLETASK 将PID设置为0(只供idel进程使用)
    CLONE_NEWS 为子进程创建新的命名空间
    CLONE_PARENT 指定子进程与父进程拥有同一个父进程
    CLONE_PTRACE 继续调试子进程
    CLONE_SETTID 将TID写回至用户空间
    CLONE_SETTLS 为子进程创建新的TLS(thread-local-storage)
    CLONE_SIGHAND 父子进程共享信号处理函数及被阻断的信号
    CLONE_SYSVSEM 父子进程共享System V SEM_UNDO语义
    CLONE_THREAD 父子进程放入相同的线程组
    CLONE_VFORK 调用vfork(),父进程准备睡眠等待子进程将其唤醒
    CLONE_UNTRACED 防止跟踪进程在紫禁城上强制执行CLONE_PTRACE
    CLONE_STOP 以TASK_STOPEDD状态开始进程
    CLONE_CHILD_CLEARTD 清除子进程的TID
    CLONE_CHILD_SETTD 设置子进程的TID
    CLONE_PARENT_SETTLD 设置父进程的TID
    CLONE_VM 父子进程共享地址空间
  5. 内核线程与普通进程的区别在于内核线程没有独立的地址空间。内核线程只在内核中运行。和普通线程类似,内核线程可以被调度和抢占。内核线程只能由其他内核线程创建。

进程终结

  1. 进程终结一般发生在进程调用exit()系统调用时,或者隐式的调用从程序的主函数(main)返回时放置在其后的exit(),或者进程接收到它不能处理也不能忽略的信号或异常时,进程会被动的被总结。进程终结需要系统调用do_exit()完成。
  2. 孤儿进程和僵尸进程
    1. 孤儿进程是指父进程在子进程还未终结的时候终结了,子进程就是孤儿进程。孤儿进程会被init(PID=1)的进程收养。
    2. 僵尸进程是指子进程在终结之后,父进程没有调用wait4()接收子进程描述符并进行清理,子进程就变成了僵尸进程。
    3. 孤儿进程被init进程收养,最终资源的回收由init进程完成,所以不影响性能。 僵尸进程影响系统性能,可以通过kill父进程强制把僵尸进程变成孤儿进程被init进程收养并回收来解决。