《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())
时间: 2024-08-27 20:41:56

《Linux内核设计与实现》第三章学习笔记的相关文章

《Linux命令行与shell脚本编程大全》 第三章 学习笔记

第三章:基本的bash shell命令 bash程序使用命令行参数来修改所启动shell的类型 参数 描述 -c string 从string中读取命令并处理他们 -r 启动限制性shell,限制用户在默认目录下活动 -i 启动交互性shell,允许用户输入 -s 从标准输入读取命令 环境变量PS1.PS2 PS1:控制默认命令行提示符格式 PS2:控制后续命令行提示符格式   bash shell提示符字符串中使用的特殊字符 字符 描述 \a 报警字符 \d “日 月 年”格式显示的日期 \e

linux第三章学习笔记

第三章 进程管理 进程是Unix操作系统抽象概念中最基本的一种. 进程管理是所有操作系统的心脏所在. 一.进程 1. 进程是处于执行期的程序.除了可执行程序代码,还包括打开的文件.挂起的信号.内核内部数据.一个或者多个执行线程等多种资源 线程是在进程活动中的对象:内核调度的对象是线程而不是进程 在Linux系统中,并不区分线程和进程 可能存在两个或者多个进程执行的是同一个程序:甚至N个进程共享打开的文件.地址空间之类的资源 2. 线程:是进程中活动的对象.每个线程都有一个独立的程序计数器,进程栈

《Java从入门到精通》第三章学习笔记

第3章 Java语言基础 一.标识符和关键字 1.Java中的标识符用于标识类名.变量名.方法名.数组名.文件名. 2.标识符的命名规则:由字母.数字.下划线和美元符号组成,但第一个字符不能是数字.同时标识符不能选用Java的关键字和保留字. 3.一般的约定命名规则: (1)类和接口名:每个词首字母大写,如MyClass,HelloWorld: (2)方法名:第一个词的首字母小写,其余词首字母大写,尽量少用下划线,如myMethod,myGetData: (3)常量名:基本数据类型的常量名全部用

《程序员的自我修养》第三章学习笔记

1,  编译器编译源代码生成的文件叫做目标文件. 从结构上说,是编译后的可执行文件,只不过还没有经过链接 3.1 目标文件的格式 1,可执行文件的格式: Windows下的PE  和   Linux下的ELF 2,从广义上说,目标文件与可执行文件的格式几乎是一样的,所以广义上可以将目标文件与可执行文件看成是一种类型的文件. 3,可执行文件,动态链接库,静态链接库都按照可执行文件格式存储(Windows下是 PE-COFF格式,Linux下是ELF格式). 4,Linux下命令: $: file 

第三章学习笔记

一.进程 1.进程就是出于执行期的程序 2.执行线程,简称线程,是在进程中活动的对象 3.进程提供两种虚拟机制:虚拟处理器和虚拟内存 二.进程描述符和任务结构 1.内核把进程的列表存放在焦作任务队列的双向循环链表中,其中每一项都是类型为task_struct.称为进程描述符的结构 2.进程描述符的分配和存放: (1)目的:Linux通过slab分配task_struct结构,以达到对象复用以及和缓存着色的目的(避免资源动态分配和释放带来的资源消耗) (2)分配:每个任务的堆栈尾端(比如,对于向上

《从问题到程序》第三章学习笔记

知识总结 pan_area(3.24)是函数调用表达式,表示用实参3.24去调用函数pan_area.double pan_area(double r)表示本函数的名字是pan_area,其返回值类型是double.这个函数只有一个参数,所以参数表里只有一对类型描述和参数名,说明参数的类型是double,名字是r. int main() { ...... return 0; } 以main为名字的函数的地位特殊,它表示这个程序的执行起点和整个过程.在一个C程序启动时,其执行就从它的main函数的

Scala第三章学习笔记

换行后的左大括号造成的问题: class FooHolder { def foo() { println("foo was called") } } Scala认为def foo()这行代码定义了一个抽象方法.这是因为它没有捕捉到后面的大括号,认定def foo()是完整的一行语句.当编译时,它认为这是一个洗呢匿名代码块,应该在类构建过程中执行. 解决办法:加一条新的编码规定,要求所有的方法定义使用"="语法. trait FooHolder2{ def foo()

《Linux内核设计与实现》第一、二章学习笔记

<Linux内核设计与实现>第一.二章学习笔记 姓名:王玮怡  学号:20135116 第一章 Linux内核简介 一.关于Unix ——一个支持抢占式多任务.多线程.虚拟内存.换页.动态链接和TCP/IP网络的现代化操作系统 1.主要发展过程   1969年,贝尔实验室的程序员们设计了一个文件系统原型,最终发展演化成了Unix 1971年,Unix被移植到PDP-11型机中 1973年,整个Unix系统使用C语言进行重写,为后来Unix系统的广泛移植铺平了道路 Unix第六版(V6)被贝尔实

Android深度探索——第三章读书笔记及心得

了解Git ——第三章读书笔记及心得 对于Android的理解是从这学期才开始,所以不可谓说是了解的太少太少.对于Linux虽然经过了一学期的学习.经过一次紧张的实训,但是了解的也不是很多.不过我终究是知道Android和Linux是开源的,这是很多老师都曾经告诉我们的.虽然Git并不是学习Android和Linux开发必须掌握的技术,但是对于想要认真学习好这门技术的我们来说应该要努力掌握好这门技术.就像书上说的学习新技术的方式不是一味的读书,只会纸上谈兵.更应该深入的理解自己感兴趣的源代码,通