Linux内核学习笔记-2.进程管理

时间:2022-05-28 11:13:00

原创文章,转载请注明:Linux内核学习笔记-2.进程管理) By Lucio.Yang

部分内容来自:Linux Kernel Development(Third Edition),Robert Love,陈莉君等译。

1.进程

  进程是正在执行的程序代码的实时结果,包含打开的文件、挂起的信号等。线程是进程中的活动的对象,内核调度的对象是线程。在Linux内核对线程与进程并不加以区分,线程只不过是一种特殊的进程。  

2.进程描述符

  内核把进程的信息存放在task list的双向循环链表中,链表中的每一项都是类型为task_struct、成为进程描述符的的结构,包含一个具体进程的所有信息。Linux通过slab分配器分配task_struct结构,达到对象复用和缓存着色的目的,此时则只需要在栈顶或者栈底创建一个thread_info结构,里面保存task_struct指针。

  内核通过一个唯一的进程标识符PID(表示为pid_t隐含类型)来标识每个进程,这个值的类型为int,为了与老版本的Unix和Linux兼容,最大值默认设置为32768(short int的最大值),该上限可以设置。

  在内核中,访问任务通常需要获得指向task_struct的指针,通过current宏查找到当前正在运行的进程描述符的的指针(current宏只在内核空间,即内核处于进程上下文时有效)。current宏的实现与硬件体系结构有关,有些硬件体系结构的有专门的寄存器保存当前task_struct指针,但是如x86,并没有这样的寄存器,就只能在内核栈的尾端创建thread_info结构,通过计算偏移简介的查找task_struct指针。方法是,把栈指针的后13个有效位屏蔽掉(假定栈为8KB,):

    movl $-8192,%eax

    andl %esp,%eax

3.进程状态、上下文、家族树

  task_struct中的state描述了进程的当前状态,值必为下列五种状态之一:

    1.TASK_RUNNING

      运行:正在执行或者在运行队列中等待执行,这是进程在用户空间唯一可能的状态。

    2.TASK_INTERRUPTIBLE

      可中断:进程阻塞,当进程的某些条件达成或者接收到信号而提前被唤醒,进程可以随时投入运行。

    3.TASK_UNINTERRUPTIBLE

      不可中断:进程阻塞,但是进程接收信号不会被唤醒,必须在等待时不受干扰或者等待事件很快就会发生。

    4.TASK_TRACED

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

    5.TASK_STOPPED

      停止:进程停止运行。

  内核通过set_task_state(task,state)设置进程状态。

  Linux的进程之间存在明显的继承关系,都是PID为1的init进程的后代。每个task_struct都包含一个指向其父进程名为parent的指针,还包含一个名为children的子进程链表。

4.进程创建

  Unix将进程的创建工作分解到两个单独的函数执行:fork()和exec()。

  首先,fork()通过拷贝当前进程创建一个子进程,子进程和父进程的区别仅仅在于PID、PPID和某些资源的统计量(对于父进程, fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零)。

  exec函数族的作用是根据指定的文件名找到可执行文件,并用它来取代调用进程的内容。一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号。

  在Linux中使用exec函数族主要有两种情况:

    1.当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数族让自己重生。

    2.如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec,这样看起来就好像通过执行应用程序而产生一个新进程。(这种情况非常普遍)。

  Linux的fork()使用写时拷贝(copy-on-write)实现。内核并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享。在页根本不会被写入的情况下(fork()后立即调用exec()),就无须复制了。

5.线程

  从内核的角度来说,它并没有线程的概念,Linux把所有的线程当作进程处理,而指定他们共享某些资源。

5.进程终结

  进程的析构发生在进程调用exit()系统调用时,极可能显式地调用这个系统调用,也可能从某个程序的主函数返回(c语言编译器会在main函数的返回点后面放之调用exit()的代码)。终结任务大部分靠do_exit()完成。do_exit()完成一系列繁琐的工作,执行后,该进程并非马上消失。

  父进程在子进程之前退出,需要保证这些子进程找到一个新的父亲。解决办法是给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init进程领养。

  进程终结时的清理工作和进程描述符的删除被分开执行。进程描述符的删除,发生在父进程已经获得已终结的子进程的信息后,或者通知内核它不在关注那些信息后。

  wait()系统调用族是通过唯一的系统调用wait4()实现的,标准动作是挂起调用它的进程,由wait()分析当前进程的某个子进程是否退出,wait就会收集这个进程的信息,并把它销毁后返回;如果没有找到,父进程会一直阻塞,直至有一个出现为止。