• 《Linux内核设计与实现》第三章学习笔记


    第三章  进程管理

    姓名:王玮怡  学号:20135116

    一、进程

    1、进程的含义

      进程是处于执行期的程序以及相关资源的总称,程序本身并不是进程,实际上就是正在执行的代码的实时结果。Linux内核通常把进程也叫“任务”。

    2、线程的含义

      执行线程简称线程,是在进程中互动的对象。内核调度的对象是线程而不是进程。Linux系统不区分线程和进程,线程只是一种特殊的进程。

    3、进程的执行过程

    (1)clone()调用fork(),通过复制一个现有进程来创建一个全新的进程,进程开始存活。其中调用fork()的进程为父进程,新产生的进程为子进程。在该系统调用结束时,在返回点这个相同位置上,父进程回复执行,子进程开始执行。其中,fork()系统调用从内核返回两次:一次回到父进程,另一次返回到新产生的子进程。子进程和父进程的区别在于PID(每个进程唯一)、PPID(父进程的进程号,子进程将其设置为被拷贝进程的PID)和某些资源和统计量。

    (2)新的进程调用exec()这组函数,创建新的地址空间,并把新的程序载入其中。

    (3)程序通过exit()系统调用推出执行,终结进程并将其占用的资源释放,进程退出执行后为僵死状态,直到父进程调用wait()或waitpid()为止。其中父进程可以通过wait4()系统调用查询子进程是否终结。

    二、进程描述符及任务结构

      内核把进程的列表存放在“任务队列”的双向循环链表中。链表中的每一项都是类型为task_struct、称为进程描述符的结构,该结构定义在<linux/sched.h>文件中,进程描述符包含了一个具体进程的所有信息。

                               

    1、分配进程描述符

      Linux通过slab分配器动态分配task_struct结构,这样能达到对象复用和缓存着色(cache coloring)的目的。只需在栈底(向下增长的栈)或栈顶(向上增长的栈)创建一个新的结构struct thread_info。

      每个任务的thread_info结构在它的内核栈的尾端分配。结构域中task域存放的是指向该任务实际task_struct的指针。

    2、进程描述符的存放

      内核通过一个唯一的进程标识值或PID来标识每个进程。PID是一个数,表示为pid_t隐含类型,实际上就是一个int类型,最大默认值设置为32768(short int短整型的最大值)。最大默认值表示系统中允许同时存在的进程的最大数目,这个值越小,转一圈的速度越快。

      在内核中访问任务通常需要获得指向其task_struct的指针。实际上,内核中大部分处理进程的代码都是直接通过task_struct进行的。

    3、进程状态

      进程描述符中的state域描述了进程的当前状态。

    (1)进程的五种状态

    • TASK RUNNING(运行):进程是可执行的,表示正在执行或者在运行队列中等待执行。
    • TASK_INTERRUPTIBLE(可中断):进程正在休眠(被阻塞),等待某种条件达成。
    • TASK_UNINTERRUPTIBLE(不可中断):除了就算接收信号也不会被唤醒或准备投入运行外,这个状态与可打断状态一样,通常在进程必须等待时不受干扰或等待时间很快就会发生时出现。
    • __TASK_REACED:被其他进程跟踪的进程,例如,通过ptrace对调试程序进行跟踪。
    • __TASK_STOPPED:进程停滞执行;进程没有投入运行也不能投入运行

    (2)进程状态转化

    4、设置当前进程状态

      使用set_task_state(task,state)函数将制定的进程设置为制定的状态。

    *注:set_current_state(state)和set_task_state(task,state)含义是等同的。

    5、进程上下文

      当一个程序调用执行了系统调用或触发了某个异常,它就陷入了内核空间,此时,我们称内核“代表进程执行”并处于进程上下文中。在此上下文中current宏是有效的。

      进程只有通过某些明确定义的接口才能陷入内核执行——对内核的所有访问都必须是必须通过这些接口的。

    6、进程家族树

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

    三、进程创建

    1、写时拷贝

    (1)Linux的fork()使用写时拷贝(copy-on-write)页实现,内核并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝,而fork()的实际开销就是复制父进程的页表以及给子进程创建唯一地进程描述符。

    (2)资源的复制只有在需要写入时才进行,在此之前,只是以只读方式共享。

    (3)在一般情况下,进程创建后都会马上运行一个可执行的文件,这种优化可以避免拷贝大量冗余数据。

    2、fork()

    (1)fork()、vfork()、__clone()库函数都会根据各自需要的参数标志去调用clone(),然后由clone()去调用do_fork()。

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

    (3)关于copy_process()函数:

    • 调用dup_task_struct()为新进程创建一个内核栈、thread_info结构和task_struct,这些值与当前值一致
    • 检查并确保新创建这个子进程后,当前用户所拥有的进程数目没有超出给它分配的资源的限制。
    • 子进程着手使自己与父进程区分开来,而task_struct中的大多数数据都依然未被修改。
    • 子进程的状态被设置为TASK_UNINTERRUPTIBLE,以保证它不会投入运行。
    • copy_process()调用copy_flags()以更新task_struct的flags成员。
    • 调用alloc_pid()为新进程分配一个有效的PID。
    • 根据传递给clone()的参数标志,copy_process()拷贝或共享打开的文件、文件系统信息、信号处理函数、进程地址空间和命名空间等。
    • 最后,copy_process()做扫尾工作并返回一个指向子进程的指针。

    3、vfork()

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

    (2)理想情况下,系统最好不要调用vfork(),内核也不用实现它。

    (3)vfork()系统调用的实现是通过向clone()系统调用传递一个特殊标志来进行的。

    • 在调用copy_process()时, task_struct 的vfor_done 成员被设置为NULL
    • 在执行do_fork()时,如果给定特别标志,则vfork_done 会指向一个特定地址
    • 子进程先开始执行后,父进程不是马上恢复执行,而是一直等待,直到子进程通过vfork_done 指针向它发送信号。
    • 在调用mm_release()时,该函数用于进程退出内存地址空间,并且检查vfork_done 是否为空,如果不为空,则会向父进程发送信号。
    • 回到do_fork(),父进程醒来并返回。

    四、线程在Linux中的实现

      每个线程都拥有唯一隶属于自己的task_struct,所以在内核中,它看起来就像是一个普通的进程。

    1、创建线程

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

                         

      传递给clone()的参数标志决定了新创建进程的行为方式和父子进程之间共辜的资源种类。

    2、内核线程

    (1)内核线程和普通的进程阔的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm 指针被设置为NULL),它们只在内核空间运行,从来不切换到用户空间去。

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

    (3)内核钱程也只能囱其他内核钱程创建

    • 新的任务是由kthread内核进程通过clone()系统调用而创建的
    • 新创建的进程处于不可运行状态,如果不通过调用wake_up _process()明确地唤醒它,它不会主动运行。
    • 创建一个进程并让它运行起来,可以通过调用ktbread_run()
    • 内核钱程启动后就一直运行直到调用do_exit()退出,或者内核的其他部分调用kthread_stop()退出。

    五、进程终结

      一般来说,进程的析构是自身引起的。它发生在进程调用exit()系统调用时,既可能显式地调用这个系统调用,也可能隐式地从某个程序的主函数返回(其实C 语言编译器会在main()函数的返回点后面放置惆用exit()的代码)。

      进程的终结,大部分依靠do_exit():

    • 将tast_struct 中的标志成员设置为PF_EXITING。
    • 调用del_timer_ sync()删除任一内核定时器。根据返回的结果,它确保没有定时器在排队,也设有定时器处理程序在运行。
    • 如果BSD 的进程记账功能是开启的, do_exit()调用acct_update_ integrals()来输出记账信息。
    • 然后调用exit_mm()函数释放进程占用的mm_struct,如果没有别的进程使用它们,就彻底释放它们。
    • 接下来调用sem_ exit()函数。如果进程排队等候IPC信号,它则离开队列。
    • 调用exit_files()和exit_fs(),以分别递藏文件描述符、文件系统数据的引用计数。
    • 接着把存放在task_struct 的exit_code成员中的任务退出代码置为由exit()提供的退出代码,或者去完成任何其他由内核机制规定的退出动作。
    • 调用exit_notify()向父进程发送信号,给子进程重新找养父(线程组中的其他线程或者为init进程),并把进程状态(存放在task_struct 结构的exit_state 中)设成EXIT_ZOMBLE。
    • do_exit()调用schedule()切换到新的进程。

      至此,与进程相关联的所有资源都被释放掉了,线程不可运行(实际上也没有地址空间让它运行)并处于EXIT_ZOMBIE退出状态。

    1、删除进程描述符

      wait()这一族函数都是通过唯一(但是很复杂)的一个系统调用wait4()来实现的。它的标准动作是挂起调用它的进程,直到其中的一个子进程退出,此时函数会返回该子进程的PID。

      当最终需要释放进程描述符时,release_task()会被调用,用以完成以下工作:

    2、孤儿进程造成的进退维谷

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

    *解决方法:

      给子进程在当前线程组内找一个线程作为父亲,如果不行,就让init做它们的父进程.在do_exit()中会调用exit_notify(),该函数会调用forget_original_parent(),而后者会调用find_new _reaper() 来执行寻父过程。

      当一个进程被跟踪时,它的临时父亲设定为调试进程寻找一个新的父进程的办法:在一个单独的被ptrace 跟踪的子进程链表中搜索相关的兄弟进程一一用两个相对较小的链袭减轻了遍历带来的消耗。

    本章总结:

    • Linux 如何存放和表示进程(用task_ struct 和thread_info )
    • 如何创建进程(通过fork(),实际上最终是clone())
    • 如何把新的执行映像装入到地址空间(通过execO 系统调用族)
    • 如何表示进程的层次关系,父进程又是如何收集其后代的信息(通过wait()系统调用族)
    • 进程最终如何消亡(强制或自愿地调用exit())
  • 相关阅读:
    底部菜单栏之Fragment的详细介绍和使用方法
    Warm up 2
    如何做好一位资深的web前端工程师
    使用 HTML5 canvas 绘制精美的图形
    计算元素距离浏览器左边的距离
    [JSOI2016]独特的树叶
    【SDOI2009】Elaxia的路线
    【SCOI2009】最长距离
    【SCOI2009】围豆豆
    【AHOI2005】穿越磁场
  • 原文地址:https://www.cnblogs.com/wwy-20135116/p/5332170.html
Copyright © 2020-2023  润新知