Linux Kernel Development Notes - 3

3 进程管理

进程是处于执行期的程序以及它所包含的资源的总称。线程是在一个进程中活动的对象。内核调度的对象是线程。

进程提供两种虚拟机制:虚拟处理器和虚拟内存。

fork() –> exec*() –> exit()

fork()创建新进程(由clone()系统调用实现);接着调用exec*()创建新的地址空间,并把新的程序载入;最后exit()系统调用退出执行(终结进程释放资源)。

父进程可通过wait4()系统调用查询子进程是否终结。进程退出执行后被设置为僵死状态,直到父进程调用wait()waitpid()为止。

 

  3.1 进程描述符及任务结构

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

      进程描述符中包含的数据完整的描述了一个正在执行的程序:它打开的文件,进程的地址空间,挂起的信号,进程的状态等。

 

  3.1.1 分配进程描述符

      Linux通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的(?_11章)。通过预先分配和重复使用task_struct,可以避免动态分配和释放所带来的资源消耗,使进程创建迅速。

      x86为例参考。

  3.1.2 进程描述符的存放

      内核通过唯一的进程标识值PID(process identification value)来标识每个进程(pid_t类型,int in fact)。

      内核中大部分处理进程的代码都是直接通过task_struct进行的。通过current宏(实现基于硬件)来查找当前正在运行进程的进程描述符。

  3.1.3 进程状态

      进程描述符中的state域描述了进程的当前状态。为一下五中状态标志之一:

      TASK_RUNNING(运行) -- 进程是可执行的;它或者在执行,或者在运行队列中等待执行。这是进程在用户空间中执行唯一可能的状态;也可以应用到内核空间中正在执行的进程。

      TASK_INTERRUPTIBLE(可中断) -- 进程正在睡眠(被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒并投入运行。

      TASK_UNINTERRUPTIBLE(不可中断) -- 除了不会因为接收到信号而被唤醒从而投入运行外,此状态与可中断状态相同。此状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。由于处于此状态的任务对信号不做响应,所以较之可中断状态,使用的较少。

      TASK_ZOMBIE(僵死) -- 该进程结束了,但是其父进程还没有调用wait4()系统调用。为了父进程能够获知它的消息,子进程描述符仍被保留。一旦父进程调用了wait4(),进程描述符就会被释放。

      TASK_STOPPED(停止) -- 进程停止执行;进程没有投入运行也不能投入运行。通常此状态发生在接收到SIGSTOP、SIGTSTP、SIGTTIN、SIGTTOU等信号的时候。此外,在调试期间接收到任何信号,都会使进程进入此状态。

1

  3.1.4 设置当前进程状态

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

      set_current_state(state);

      除SMP系统(this function provides a memory barrier to force ordering on other processors),其等价于:task->state = state;

  3.1.5 Process Context

      One of the most important parts of a process is the executing program code. This code is read in from an executable file and executed within the program's address space(从可执行文件载入到进程的地址空间执行). Normal program execution occurs in user-space. When a program executes a system call or triggers an exception, it enters kernel-space. At this point, the kernel is said to be "executing on behalf of the process" and is in process context. When in process context, the current macro is valid. Upon exiting the kernel, the process resumes execution in user-space, unless a higher-priority process has become runnable in the interim, in which case the scheduler is invoked to select the higher priority process.

      System calls and exception handlers are well-defined interfaces into the kernel. A process can begin executing in kernel-space only through one of these interfacesall access to the kernel is through these interfaces.

  3.1.6 The Process Family Tree

      所有的进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。

      每个进程必须有一个父进程。Likewise,每个进程亦可有零个或多个子进程。进程间的关系存放在进程描述符(task_struct)中。每个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 现在指向当前的某个子进程 */  
      
 

      其中:

 
      struct list_head { 
          struct list_head *next, *prev; 
      }; 

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

  3.2进程的创建

    fork()exec*()

    fork() 通过拷贝当前进程创建子进程。子、父进程的区别仅在于PID、PPID、某些资源和统计量(不必要继承)。

    exec*()负责读取可执行文件并将其载入地址空间开始执行。

    3.2.1 Copy-on-Write

      In Linux, fork() is implemented through the use of copy-on-write pages.

      The duplication of resources occurs only when they are written; until then, they are shared read-only.

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

 

    3.2.2 fork()

      Linux通过clone()系统调用实现fork().

      fork()、vfork()、__clone() –> clone() –> do_fork() –> copy_process()

    3.2.3 vfork()

 

  3.3 线程在Linux中的实现

    Linux把线程都当作进程来实现。线程仅仅被视为一个与其他进程共享某些资源的进程。它只是一种进程间共享资源的手段。

    线程的创建和普通的进程创建类似,只不过在调用clone()的时候需要传递一些参数标志来指明要共享的资源。传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共享资源的种类。

    内核线程:独立运行在内核空间的标准进程。内核线程只能由其他内核线程创建。

    在现有内核线程中创建一个新的内核线程的方法:

   int kernel_thread(int (*fn)(void *), void * arg, unsigned long flags)

    上面的函数返回时,父线程退出,并返回一个指向子线程task_struct的指针。子线程开始运行fn指向的函数。

  3.4 进程终结

    一个进程终结时,内核必须释放它所占有的资源并告知父进程。

    一般,进程的析构发生在它调用exit()之后。由do_exit()完成释放资源和告知父进程等工作。资源被释放,进程不可运行并处于TASK_ZOMBIE状态。

    此时,进程所占有的资源是内核栈、thread_info结构和task_struct结构。该进程还存在,存在的唯一目的就是向它的父进程提供信息。

    After the parent retrieves the information, or notifies the kernel that it is uninterested, the remaining memory held by the process is freed and returned to the system for use.

    3.4.1 删除进程描述符

      在调用do_exit()之后,线程已经僵死,但系统还保留了它的进程描述符。这样做可以让系统有办法在子进程终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的task_struct结构才被释放。

      wait()这一族函数通过唯一的一个wait4()系统调用实现--挂起调用它的进程,直到其中的一个子进程退出,此时函数返回该子进程的PID。

      当最终需要释放进程描述符时,release_task()会被调用。进程描述符和进程独享的所有资源全部释放。

    3.4.2 The Dilemma of the Parentless Task

      若父进程在子进程之前退出,必须有机制保证子进程能找到一个新的父亲,否则这些成为孤儿的进程就会在退出时永远处于僵死状态,耗费内存。

      解决方法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init作为它们的父进程。

  3.5 进程小结

    进程的存放和表示:task_struct和thread_info;

    进程的创建:通过clone()和fork();

    把新的执行映像装入到地址空间:exec*();

    父进程收集其后代的信息:wait();

    终结进程:exit()。

posted @ 2010-10-05 17:20  yangzd  阅读(307)  评论(0编辑  收藏  举报