SiYu

积少成多 聚沙成塔

欢迎来到我的个人站~


Linux进程管理

进程

   进程就是处于执行期的程序(目标码存放在某种存储介质上)。但进程并不仅仅局限于一段可执行程序代码。通常进程还要包括其他资源,像打开的文件,挂起的信号,内核内部数据,处理器状态,一个或多个具有内存映射的内存地址控件及一个或多个执行线程(thread of execution),当然还包括用来存放全局变量的数据段等。实际上,进程就是正在执行的程序代码的实时结果。内核需要有效而又透明地管理所有细节。

  执行线程,简称线程,是在进程中活动的对象。每个线程都拥有一个盾立的程序计数器、进程栈、和一组进程寄存器。内核调度的对象时线程,而不是进程。在传统的Unix系统中,一个进程只包含一个线程,但现在的系统中,包含多个线程的多线程程序司空见惯。Linux系统的线程实现非常特别:它对线程和进程并不特别区分。对Linux而言,线程只不过时一种特殊的进程罢了。

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

  程序本身并不是进程,进程是处于执行期的程序以及相关的资源的总称。实际上,完全可能存在两个或多个不同的进程执行的时同一个程序。并且两个或两个以上并存的进程还可以共享许多诸如打开的文件、地址空间之类的资源。

  Linux中,通常调用fork()系统的结果,该系统调用用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。在该调用结束时,在返回点这个相同的位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。

  通常,创建新的进程都是为了立即执行新的或不同的程序,而接着调用exec()这组函数就可以创建新的地址空间,并把新的程序载入其中。在现在Linux内核中,fork()实际上是由clone()系统实现的。

  最终,程序通过exit()系统调用退出执行。这个函数会终结进程并将其占用的资源释放掉。父进程可以通过wait4()系统调用查询子进程是否中介,这其实使得进程呢拥有了等待特定进程执行完毕的能力。进程退出执行后被设置为僵死状态, 知道它的父进程调用wait()或waitpid()为止。

进程描述符及任务结构

  内核把进程的列表存放在任务队列中,它是双向循环链表。链表中的每一项都是类型为task_struck、称为进程描述符的结构。包含一个具体进程的所有信息。

  例如:它打开的文件。进程的地址控件,挂起的信号。进程的状态,还有其他更多信息。

分配进程描述符

  通过slab分配器分配task_struct结构,这样能达到对象复用和缓存着色的目的。现用slab分配器动态生成task_struct,所以只需在栈底或栈顶创建一个新的结构struct thread_info。

  在x86上,struct thread_info在文件<asm/thread_info.h>中定义如下:

struct thread_info{
	struct task_struct    *task;
    struct exec_domain    *exec_domain;
    _u32                  flags;
    _u32                  status;
    _u32                  cpu;
    int                   preempt_count;
    mm_setment_t          addr_limit;
    struct restart_block  restart_block;
    void                  *sysenter_return;
    int                   uaccess_err;
}
进程描述符的存放

  内核通过一个唯一的进程标识值或PID(max:32768)来标识每个进程。在内核中,访问任务通常需要获得指向其taks_struct的指针。实际上,内核大部分处理进程的代码都是直接通过task_struct进行的。

进程状态

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

  1.TASK_RUNNING(运行)—进程时可执行的:它或者正在执行,或者在运行队列中等待执行。这时进程在用户控件中执行的唯一可能的状态。这种状态也可以应用到内核控件中正在执行的进程。

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

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

  4._TASK_TRACED—被其他进程跟踪的进程,例如通过ptrace对调试程序进行跟踪。

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

设置当前进程状态

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

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

该函数将指定的进程设置为指定的状态。

进程上下文

  可执行程序代码时进程的重要组成部分。这些代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行。当一个程序调执行了系统调用或者出发了某个异常,它就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中current宏时有效的。除非在此间隙有更高优先级的进程需要执行并由调度器做出了相应调整。否则,在内核退出的时候,程序恢复在用户空间会继续执行。

进程家族树

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

  系统中每个进程必有一个父进程,相应的,每个进程也可以拥有零个或多个子进程。拥有同一个父进程的所有进程被称为兄弟。进程间的关系存放在进程描述符中。每个task_struct都包含一个指向其父进程tast_struct、叫做parent的指针,还包含一个称为children的子进程链表。所以,对于当前进程,可以通过下面代码获得其父进程的进程描述符

sturct 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静态分配的。下面的代码可以很好地演示所有进程之间的关系:

struct task_struct *task;
for (task = current; task != &init_task; task = task ->parent)
/* task 现在指向init */
进程创建

  其他操作系统提供了产生(spawn)进程的机制:首先的新的地址控件创建进程,读入可执行文件,最后开始执行。 Unix采用了与众不同的实现方式,它把上述步骤分解到两个单独的函数中去执行:fork()和exec()。首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量。exec()函数负责读取可执行文件并将其载入地址控件开始运行。

写时拷贝

  Linux的fork()使用写时拷贝(copuy-on-write)页实现,时一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。   fork()的时机开销时复制父进程的页表以及给子进程创建唯一的进程描述符。在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量根本就不会被使用的数据。

fork()

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

  do_fork完成了创建中的大部分工作,它的定义在kernel/fork.c文件中。该函数调用copy_process()函数,然后让进程开始运行。copy_process()函数完成的工作很有意思:

  1)调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前进程的值相同。此时,子进程和父进程的描述符时完全相同的。

  2)检查并确保新创建这个子进程后,当前用户所拥有的进程数没有超过给它分配的资源的限制。

  3)子进程着手使自己与父进程区别开来。进程描述符内的许多成员都要被清0huo设为初始值。

  4)子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。

  5)copy_process()调用copy_flags()以更新task_struct的flags成员。表明进程是否拥有超级用户权限的PF_SUPERPRIV标志被清0。表明进程还没有调用exec()函数的PF_FORKNOEXEC标志被设置。

  6)调用alloc_pid()为新进程分配一个有效的PID。

  7)根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址控件和命名控件等。

  8)最后,copy_process()做扫尾工作并返回一个指向子进程的指针。

  再回到do_fork()函数,如果copy_process()函数成功返回,新创建的子进程被唤醒并让其投入运行。内核有意选择子进程首先执行。

vfork()

  除了不拷贝父进程的页表项外,vfork()系统调用和fork()的功能相同。子进程作为父进程的一个单独的线程在它的地址控件里运行,父进程被阻塞,直到子进程退出或执行exec()。子进程不能向地址控件写入。

线程在linux中的实现

  线程机制时现代编程技术中常用的一种抽象概念。该机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。线程机制支持并发程序设计技术,在多处理器系统上,它也能表征真正的并行处理。

  从内核角度,linux没有线程这个概念。它把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属自己的task_struct,所以在内核中,它看起来就像一个普通的进程。

创建线程

  需要传递一些参数标志来指明需要共享的资源:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);
内核线程

  内核经常需要在后台执行一些操作。这种任务可以通过内核线程完成–独立运行在内核空间的标准进程。内核线程和普通的进程间的区别在于内核线程没有独立的地址空间。它们只在内核空间运作,从来不切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。

进程终结

   不管进程时怎么终结的,该任务大部分都要靠do_exit来完成,它要做如下工作:

  1)将task_struct中的标志成员设置为PF_EXITING

  2)调用del_timer_sync()删除任一内核定时器。

  3)如果BSD的进程记账功能时开启的,do_exit()调用acct_update_integrals()来输出记账信息。

  4)然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们,就彻底释放它们。

  5)接下来调用sem_exit()函数。如果进程排队等候IPC信号,它则离开队列。

  6)调用exit_files()和exit_fs(),以分别递减文件描述符、文件系统数据的引用计数。如果其中某个引用计数的数值降为0,那么就代表没有进程在使用相应的资源,此时可以释放。

  7)接着把存放在task_struct的exit_code成员中的任务代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。退出代码存放在这里供父进程随时检索。

  8)调用exit_notify()向父进程发送信号,给子进程重新找养父,养父为线程组中的其他线程或者init线程,并把进程状态设为EXIT_ZOMBIE

  9)do_exit()调用schedule()切换到新的进程。因此处于EXIT_ZOMBIE状态的进程不会再被调度,所以这是进程所执行的最后一段代码。do_exit()永不返回。

删除进程描述符

调用release_task(),释放当前进程描述符。

孤儿进程造成的进退维谷

  如果父进程在子进程之前退出,必须有机制来保证子进程能找到一个新的父亲,否则这些进程会称为孤儿进程并永远处于僵死状态,白白耗费内存。解决方法:给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程。在do_exit()中会调用exit_notify(),该函数会调用forget_original_parent(),而后者会调用find_new_reaper()来执行寻父过程。