diff --git a/study/kernel/process/create/README.md b/study/kernel/process/create/01-/README.md similarity index 75% rename from study/kernel/process/create/README.md rename to study/kernel/process/create/01-/README.md index 7da2c1c..f7c2515 100644 --- a/study/kernel/process/create/README.md +++ b/study/kernel/process/create/01-/README.md @@ -1,122 +1,122 @@ -Linux下进程的创建过程 -======= - - -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------------- |:-------------:|:-------------:|:-------------:|:-------------:|:-------------:| -| 2016-05-12 | [Linux-4.5](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux-进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | - - -http://www.linuxidc.com/Linux/2013-07/87011.htm -http://www.linuxeye.com/Linux/1827.html -http://bbs.csdn.net/topics/390872515 -http://blog.csdn.net/yjzl1911/article/details/5613569 -http://blog.csdn.net/dagouaofei/article/details/5644119 -http://blog.chinaunix.net/uid-23769728-id-3129443.html -http://baike.baidu.com/link?url=sCsQDvMUaAikV5W_eKrEL3RVijNHJtOJk8nsCnjlxtnU7yoJ9svp6cwaerQ6Dqc0I-kdoAYrOtMcocCUnzyggK -http://blog.chinaunix.net/uid-21718047-id-3070635.html -http://blog.163.com/boneshunter_1234/blog/static/340762320084472122207/ -http://blog.sina.com.cn/s/blog_626aed8b0100hws6.html - -#Linux下进程的创建流程 -------- - -##进程的复制fork和加载execve -------- - -我们在Linux下进行进行编程,往往都是通过fork出来一个新的程序,fork从化字面意义上理解就是说"分叉", 这其实就意味着我们的fork进程并不是真正从无到有被创建出来的。 - - -一个进程,包括代码、数据和分配给进程的资源,它其实是从现有的进程(父进程)复制出的一个副本(子进程),fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,然后如果我们通过execve为子进程加载新的应用程序后,那么新的进程将开始执行新的应用 - -简单来说,新的进程是通过fork和execve创建的,首先通过fork从父进程分叉出一个基本一致的副本,然后通过execve来加载新的应用程序镜像 - -* fork生成当前进程的的一个相同副本,该副本成为子进程 - -> 原进程(父进程)的所有资源都以适当的方法复制给新的进程(子进程)。因此该系统调用之后,原来的进程就有了两个独立的实例,这两个实例的联系包括:同一组打开文件, 同样的工作目录, 进程虚拟空间(内存)中同样的数据(当然两个进程各有一份副本, 也就是说他们的虚拟地址相同, 但是所对应的物理地址不同)等等。 - -* execve从一个可执行的二进制程序镜像加载应用程序, 来代替当前运行的进程 - -> 换句话说, 加载了一个新的应用程序。因此execv并不是创建新进程 - -所以我们在linux要创建一个应用程序的时候,其实执行的操作就是 - -1. 首先使用fork复制一个旧的进程 - -2. 然后调用execve在为新的进程加载一个新的应用程序 - - - -#写时复制技术 -------- - -有人认为这样大批量的复制会导致执行效率过低。其实在复制过程中,linux采用了写时复制的策略。 - - -写入时复制(Copy-on-write)是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。 - -第一代Unix系统实现了一种傻瓜式的进程创建:当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,这种创建地址空间的方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容。在大多数情况下,这样做常常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。 - -现在的Linux内核采用一种更为有效的方法,称之为写时复制(Copy On Write,COW)。这种思想相当简单:父进程和子进程共享页帧而不是复制页帧。然而,只要页帧被共享,它们就不能被修改,即页帧被保护。无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写。原来的页帧仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。 - -当进程A使用系统调用fork创建一个子进程B时,由于子进程B实际上是父进程A的一个拷贝, - -因此会拥有与父进程相同的物理页面.为了节约内存和加快创建速度的目标,fork()函数会让子进程B以只读方式共享父进程A的物理页面.同时将父进程A对这些物理页面的访问权限也设成只读. - -这样,当父进程A或子进程B任何一方对这些已共享的物理页面执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来解决这个异常. - -do_wp_page()会对这块导致写入异常中断的物理页面进行取消共享操作,为写进程复制一新的物理页面,使父进程A和子进程B各自拥有一块内容相同的物理页面.最后,从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续执行下去. - -一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值(比如PID)不同。相当于克隆了一个自己。 - - - -#0号进程与1号进程 -------- - -前面我们了解到linux下的进程创建式通过父进程复制自身分叉出的一个副本, 那么我们的系统中就必然存在一个进程是所有进程的祖先,要不然我们从哪里fork分叉出一个个子进程呢,linux下这个进程就是init进程 - -但是问题也来了,我们的祖先进程init进程,是从哪里来的? - -他总不能也是被分叉来的吧? - -如果是,那么分叉它的进程是谁,那么它为什么没有成为祖先进程 - -如果不是, 那么好了, 它是怎么创建出来的(真正的从无到有) - - - -Linux下有两个特殊的进程,idel进程($PID = 0$)和init进程($PID = 1$) - - -* idel进程由系统自动创建, 运行在内核态 - - idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork()产生的进程。完成加载系统后,演变为进程调度、交换 - - -* init进程由idel创建, - - 由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程 - Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。 - - - - -系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个linux系统的所有进程也是一个树形结构。**树根是系统自动构造的(或者说是由内核黑客手动创建的)**,即在内核态下执行的0号进程,它是所有进程的远古先祖。 - -由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程,即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号...的若干终端注册进程getty。 - -每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。 - - -上述过程可描述为:0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程 - -注意,上述过程描述中提到:1号内核进程调用执行init函数并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下: - -1. init()函数在内核态运行,是内核代码 - -2. init进程是内核启动并运行的第一个用户进程,运行在用户态下。 - -3. 一号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。 - - +Linux下进程的创建过程 +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------------- |:-------------:|:-------------:|:-------------:|:-------------:|:-------------:| +| 2016-05-12 | [Linux-4.5](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux-进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | + + +http://www.linuxidc.com/Linux/2013-07/87011.htm +http://www.linuxeye.com/Linux/1827.html +http://bbs.csdn.net/topics/390872515 +http://blog.csdn.net/yjzl1911/article/details/5613569 +http://blog.csdn.net/dagouaofei/article/details/5644119 +http://blog.chinaunix.net/uid-23769728-id-3129443.html +http://baike.baidu.com/link?url=sCsQDvMUaAikV5W_eKrEL3RVijNHJtOJk8nsCnjlxtnU7yoJ9svp6cwaerQ6Dqc0I-kdoAYrOtMcocCUnzyggK +http://blog.chinaunix.net/uid-21718047-id-3070635.html +http://blog.163.com/boneshunter_1234/blog/static/340762320084472122207/ +http://blog.sina.com.cn/s/blog_626aed8b0100hws6.html + +#Linux下进程的创建流程 +------- + +##进程的复制fork和加载execve +------- + +我们在Linux下进行进行编程,往往都是通过fork出来一个新的程序,fork从化字面意义上理解就是说"分叉", 这其实就意味着我们的fork进程并不是真正从无到有被创建出来的。 + + +一个进程,包括代码、数据和分配给进程的资源,它其实是从现有的进程(父进程)复制出的一个副本(子进程),fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,然后如果我们通过execve为子进程加载新的应用程序后,那么新的进程将开始执行新的应用 + +简单来说,新的进程是通过fork和execve创建的,首先通过fork从父进程分叉出一个基本一致的副本,然后通过execve来加载新的应用程序镜像 + +* fork生成当前进程的的一个相同副本,该副本成为子进程 + +> 原进程(父进程)的所有资源都以适当的方法复制给新的进程(子进程)。因此该系统调用之后,原来的进程就有了两个独立的实例,这两个实例的联系包括:同一组打开文件, 同样的工作目录, 进程虚拟空间(内存)中同样的数据(当然两个进程各有一份副本, 也就是说他们的虚拟地址相同, 但是所对应的物理地址不同)等等。 + +* execve从一个可执行的二进制程序镜像加载应用程序, 来代替当前运行的进程 + +> 换句话说, 加载了一个新的应用程序。因此execv并不是创建新进程 + +所以我们在linux要创建一个应用程序的时候,其实执行的操作就是 + +1. 首先使用fork复制一个旧的进程 + +2. 然后调用execve在为新的进程加载一个新的应用程序 + + + +#写时复制技术 +------- + +有人认为这样大批量的复制会导致执行效率过低。其实在复制过程中,linux采用了写时复制的策略。 + + +写入时复制(Copy-on-write)是一个被使用在程式设计领域的最佳化策略。其基础的观念是,如果有多个呼叫者(callers)同时要求相同资源,他们会共同取得相同的指标指向相同的资源,直到某个呼叫者(caller)尝试修改资源时,系统才会真正复制一个副本(private copy)给该呼叫者,以避免被修改的资源被直接察觉到,这过程对其他的呼叫只都是通透的(transparently)。此作法主要的优点是如果呼叫者并没有修改该资源,就不会有副本(private copy)被建立。 + +第一代Unix系统实现了一种傻瓜式的进程创建:当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,这种创建地址空间的方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容。在大多数情况下,这样做常常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。 + +现在的Linux内核采用一种更为有效的方法,称之为写时复制(Copy On Write,COW)。这种思想相当简单:父进程和子进程共享页帧而不是复制页帧。然而,只要页帧被共享,它们就不能被修改,即页帧被保护。无论父进程还是子进程何时试图写一个共享的页帧,就产生一个异常,这时内核就把这个页复制到一个新的页帧中并标记为可写。原来的页帧仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页帧的唯一属主,如果是,就把这个页帧标记为对这个进程是可写的。 + +当进程A使用系统调用fork创建一个子进程B时,由于子进程B实际上是父进程A的一个拷贝, + +因此会拥有与父进程相同的物理页面.为了节约内存和加快创建速度的目标,fork()函数会让子进程B以只读方式共享父进程A的物理页面.同时将父进程A对这些物理页面的访问权限也设成只读. + +这样,当父进程A或子进程B任何一方对这些已共享的物理页面执行写操作时,都会产生页面出错异常(page_fault int14)中断,此时CPU会执行系统提供的异常处理函数do_wp_page()来解决这个异常. + +do_wp_page()会对这块导致写入异常中断的物理页面进行取消共享操作,为写进程复制一新的物理页面,使父进程A和子进程B各自拥有一块内容相同的物理页面.最后,从异常处理函数中返回时,CPU就会重新执行刚才导致异常的写入操作指令,使进程继续执行下去. + +一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值(比如PID)不同。相当于克隆了一个自己。 + + + +#0号进程与1号进程 +------- + +前面我们了解到linux下的进程创建式通过父进程复制自身分叉出的一个副本, 那么我们的系统中就必然存在一个进程是所有进程的祖先,要不然我们从哪里fork分叉出一个个子进程呢,linux下这个进程就是init进程 + +但是问题也来了,我们的祖先进程init进程,是从哪里来的? + +他总不能也是被分叉来的吧? + +如果是,那么分叉它的进程是谁,那么它为什么没有成为祖先进程 + +如果不是, 那么好了, 它是怎么创建出来的(真正的从无到有) + + + +Linux下有两个特殊的进程,idel进程($PID = 0$)和init进程($PID = 1$) + + +* idel进程由系统自动创建, 运行在内核态 + + idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork()产生的进程。完成加载系统后,演变为进程调度、交换 + + +* init进程由idel创建, + + 由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程 + Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程。在系统启动完成完成后,init将变为守护进程监视系统其他进程。 + + + + +系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个linux系统的所有进程也是一个树形结构。**树根是系统自动构造的(或者说是由内核黑客手动创建的)**,即在内核态下执行的0号进程,它是所有进程的远古先祖。 + +由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程,即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号为1号、2号...的若干终端注册进程getty。 + +每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可输入注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。 + + +上述过程可描述为:0号进程->1号内核进程->1号用户进程(init进程)->getty进程->shell进程 + +注意,上述过程描述中提到:1号内核进程调用执行init函数并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下: + +1. init()函数在内核态运行,是内核代码 + +2. init进程是内核启动并运行的第一个用户进程,运行在用户态下。 + +3. 一号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。 + + diff --git a/study/kernel/process/create/01-idel/README.md b/study/kernel/process/create/02-idel/README.md similarity index 100% rename from study/kernel/process/create/01-idel/README.md rename to study/kernel/process/create/02-idel/README.md diff --git a/study/kernel/process/create/01-idel/images/init_thread_union.png b/study/kernel/process/create/02-idel/images/init_thread_union.png similarity index 100% rename from study/kernel/process/create/01-idel/images/init_thread_union.png rename to study/kernel/process/create/02-idel/images/init_thread_union.png diff --git a/study/kernel/process/create/01-idel/images/ps-aux.jpg b/study/kernel/process/create/02-idel/images/ps-aux.jpg similarity index 100% rename from study/kernel/process/create/01-idel/images/ps-aux.jpg rename to study/kernel/process/create/02-idel/images/ps-aux.jpg diff --git a/study/kernel/process/create/02-init/README.md b/study/kernel/process/create/03-init/README.md similarity index 100% rename from study/kernel/process/create/02-init/README.md rename to study/kernel/process/create/03-init/README.md diff --git a/study/kernel/process/task/README.md b/study/kernel/process/task/01-task_struct/README.md similarity index 97% rename from study/kernel/process/task/README.md rename to study/kernel/process/task/01-task_struct/README.md index 8358b00..441ce7e 100644 --- a/study/kernel/process/task/README.md +++ b/study/kernel/process/task/01-task_struct/README.md @@ -1,1166 +1,1169 @@ -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------------- |:-------------:|:-------------:|:-------------:|:-------------:|:-------------:| -| 2016-05-12 | [Linux-4.5](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux-进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | - - -进程是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称。注意,程序并不是进程,实际上两个或多个进程不仅有可能执行同一程序,而且还有可能共享地址空间等资源。 - - -Linux内核通过一个被称为进程描述符的[task_struct](http://lxr.free-electrons.com/source/include/linux/sched.h#L1389)结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在[include/linux/sched.h](http://lxr.free-electrons.com/source/include/linux/sched.h)文件中。 - -谈到task_struct结构体,可以说她是linux内核源码中最复杂的一个结构体了,成员之多,占用内存之大。 - -鉴于她的复杂,我们不能简单的亵渎,而是要深入“窥探”. - -下面来慢慢介绍这些复杂成员 - -#[进程状态](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1390) -------- -```c - volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ -``` - -[state](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1390)成员的可能取值如下 - ->参见http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L207 - -``` - - /* - * Task state bitmask. NOTE! These bits are also - * encoded in fs/proc/array.c: get_task_state(). - * - * We have two separate sets of flags: task->state - * is about runnability, while task->exit_state are - * about the task exiting. Confusing, but this way - * modifying one set can't modify the other one by - * mistake. - */ - #define TASK_RUNNING 0 - #define TASK_INTERRUPTIBLE 1 - #define TASK_UNINTERRUPTIBLE 2 - #define __TASK_STOPPED 4 - #define __TASK_TRACED 8 - -/* in tsk->exit_state */ - #define EXIT_DEAD 16 - #define EXIT_ZOMBIE 32 - #define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD) - -/* in tsk->state again */ - #define TASK_DEAD 64 - #define TASK_WAKEKILL 128 /** wake on signals that are deadly **/ - #define TASK_WAKING 256 - #define TASK_PARKED 512 - #define TASK_NOLOAD 1024 - #define TASK_STATE_MAX 2048 - - /* Convenience macros for the sake of set_task_state */ -#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) -#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) -#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) - -``` - -##5个互斥状态 -------- - -state域能够取5个互为排斥的值(通俗一点就是这五个值任意两个不能一起使用,只能单独使用)。系统中的每个进程都必然处于以上所列进程状态中的一种。 - -| 状态| 描述 | -| ------------- |:-------------:| -| TASK_RUNNING | 表示进程要么正在执行,要么正要准备执行(已经就绪),正在等待cpu时间片的调度 | -| TASK_INTERRUPTIBLE | 进程因为等待一些条件而被挂起(阻塞)而所处的状态。这些条件主要包括:硬中断、资源、一些信号……,一旦等待的条件成立,进程就会从该状态(阻塞)迅速转化成为就绪状态TASK_RUNNING | -| TASK_UNINTERRUPTIBLE | 意义与TASK_INTERRUPTIBLE类似,除了不能通过接受一个信号来唤醒以外,对于处于TASK_UNINTERRUPIBLE状态的进程,哪怕我们传递一个信号或者有一个外部中断都不能唤醒他们。只有它所等待的资源可用的时候,他才会被唤醒。这个标志很少用,但是并不代表没有任何用处,其实他的作用非常大,特别是对于驱动刺探相关的硬件过程很重要,这个刺探过程不能被一些其他的东西给中断,否则就会让进城进入不可预测的状态 | -| TASK_STOPPED | 进程被停止执行,当进程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信号之后就会进入该状态 | -| TASK_TRACED | 表示进程被debugger等进程监视,进程执行被调试程序所停止,当一个进程被另外的进程所监视,每一个信号都会让进城进入该状态 | - -##2个终止状态 -------- - -其实还有两个附加的进程状态既可以被添加到state域中,又可以被添加到[exit_state](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1461)域中。只有当进程终止的时候,才会达到这两种状态. - -```c -/* task state */ -int exit_state; -int exit_code, exit_signal; -``` - -| 状态| 描述 | -| ------------- |:-------------:| -| EXIT_ZOMBIE | 进程的执行被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息,此时进程成为僵尸进程 | -|EXIT_DEAD | 进程的最终状态 | - -而int exit_code, exit_signal;我们会在后面进程介绍 - -##新增睡眠状态 -------- - ->参见 -> ->[TASK_KILLABLE:Linux 中的新进程状态](http://www.ibm.com/developerworks/cn/linux/l-task-killable/index.html) - - -如前所述,进程状态 TASK_UNINTERRUPTIBLE 和 TASK_INTERRUPTIBLE 都是睡眠状态。现在,我们来看看内核如何将进程置为睡眠状态。 - -###内核如何将进程置为睡眠状态。 -------- - -Linux 内核提供了两种方法将进程置为睡眠状态。 - -将进程置为睡眠状态的**普通方法**是将进程状态设置为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 并调用调度程序的 schedule() 函数。这样会将进程从 CPU 运行队列中移除。 - -* 如果进程处于可中断模式的睡眠状态(通过将其状态设置为 TASK_INTERRUPTIBLE),那么可以通过显式的唤醒呼叫(wakeup_process())或需要处理的信号来唤醒它。 - -* 但是,如果进程处于非可中断模式的睡眠状态(通过将其状态设置为 TASK_UNINTERRUPTIBLE),那么只能通过显式的唤醒呼叫将其唤醒。除非万不得已,否则我们建议您将进程置为可中断睡眠模式,而不是不可中断睡眠模式(比如说在设备 I/O 期间,处理信号非常困难时)。 - -当处于可中断睡眠模式的任务接收到信号时,它需要处理该信号(除非它已被屏弊),离开之前正在处理的任务(此处需要清除代码),并将 -EINTR 返回给用户空间。再一次,检查这些返回代码和采取适当操作的工作将由程序员完成。 - -因此,懒惰的程序员可能比较喜欢将进程置为不可中断模式的睡眠状态,因为信号不会唤醒这类任务。 - -但需要注意的一种情况是,对不可中断睡眠模式的进程的唤醒呼叫可能会由于某些原因不会发生,这会使进程无法被终止,从而最终引发问题,因为惟一的解决方法就是重启系统。一方面,您需要考虑一些细节,因为不这样做会在内核端和用户端引入 bug。另一方面,您可能会生成永远不会停止的进程(被阻塞且无法终止的进程)。 - -现在,我们在内核中实现了一种**新的睡眠方法** - -Linux Kernel 2.6.25 引入了一种新的进程睡眠状态, - -| 状态| 描述 | -| ------------- |:-------------:| -| TASK_KILLABLE | 当进程处于这种可以终止的新睡眠状态中,它的运行原理类似于 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号 | - -它定义如下: - -```c -#define TASK_WAKEKILL 128 /** wake on signals that are deadly **/ - -/* Convenience macros for the sake of set_task_state */ -#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) -#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) -#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) -``` - -换句话说,TASK_UNINTERRUPTIBLE + TASK_WAKEKILL = TASK_KILLABLE。 - -而TASK_WAKEKILL 用于在接收到致命信号时唤醒进程 - -新的睡眠状态允许 TASK_UNINTERRUPTIBLE 响应致命信号 - - - -进程状态的切换过程和原因大致如下图 - -![进程状态转换图](./images/state.gif) - -#[进程标识符(PID)](http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1492) -------- - -``` -pid_t pid; -pid_t tgid; -``` - -Unix系统通过pid来标识进程,linux把不同的pid与系统中每个进程或轻量级线程关联,而unix程序员希望同一组线程具有共同的pid,遵照这个标准linux引入线程组的概念。一个线程组所有线程与领头线程具有相同的pid,存入tgid字段,getpid()返回当前进程的tgid值而不是pid的值。 - - - -在CONFIG_BASE_SMALL配置为0的情况下,PID的取值范围是0到32767,即系统中的进程数最大为32768个。 - - - -``` -#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000) -``` - ->参见 http://lxr.free-electrons.com/source/include/linux/threads.h#L27 - -在Linux系统中,一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的PID,并被存放在tgid成员中。只有线程组的领头线程的pid成员才会被设置为与tgid相同的值。注意,getpid()系统调用返回的是当前进程的tgid值而不是pid值。 - - -#[进程内核栈](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1391) -------- - -```c -void *stack; -``` - -##内核栈与线程描述符 -------- - -对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中 - -* 一个是内核态的进程堆栈, - -* 另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。 - -Linux把thread_info(线程描述符)和内核态的线程堆栈存放在一起,这块区域通常是8192K(占两个页框),其实地址必须是8192的整数倍。 - -在linux/arch/x86/include/asm/page_32_types.h中, -```c -#define THREAD_SIZE_ORDER 1 -#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER) -``` - -出于效率考虑,内核让这8K空间占据连续的两个页框并让第一个页框的起始地址是213的倍数。 - -内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。 - -用户态进程所用的栈,是在进程线性地址空间中; - -而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。 - ->需要注意的是,**内核态堆栈**仅用于内核例程,Linux内核另外为中断提供了单独的**硬中断栈**和**软中断栈** - - -下图中显示了在物理内存中存放两种数据结构的方式。线程描述符驻留与这个内存区的开始,而栈顶末端向下增长。 下图摘自ULK3,进程内核栈与进程描述符的关系如下图: - -![thread_info](./images/thread_info.gif) - -但是较新的内核代码中,进程描述符task_struct结构中没有直接指向thread_info结构的指针,而是用一个void指针类型的成员表示,然后通过类型转换来访问thread_info结构。 - -相关代码在[include/linux/sched.h](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L2812)中 - -```c -#define task_thread_info(task) ((struct thread_info *)(task)->stack) -``` - -在这个图中,esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端。一旦数据写入堆栈,esp的值就递减。 - -##内核栈数据结构描述thread_info和thread_union -------- - -thread_info是体系结构相关的,结构的定义在[thread_info.h](http://lxr.free-electrons.com/ident?v=4.5;i=thread_info)中 - -| 架构 | 定义链接 | -| ------------- |:-------------:| -| x86 | [linux-4.5/arch/x86/include/asm/thread_info.h, line 55](http://lxr.free-electrons.com/source/arch/x86/include/asm/thread_info.h?v=4.5#L55) | -| arm | [linux-4.5arch/arm/include/asm/thread_info.h, line 49](http://lxr.free-electrons.com/source/arch/arm/include/asm/thread_info.h#L49) -| arm64 | [linux/4.5/arch/arm64/include/asm/thread_info.h, line 47](http://lxr.free-electrons.com/source/arch/arm64/include/asm/thread_info.h#L47) | - -Linux内核中使用一个联合体来表示一个进程的线程描述符和内核栈: -```c -union thread_union -{ - struct thread_info thread_info; - unsigned long stack[THREAD_SIZE/sizeof(long)]; -}; -``` - -##获取当前在CPU上正在运行进程的thread_info -------- -下面来说说如何通过esp栈指针来获取当前在CPU上正在运行进程的thread_info结构。 - - -实际上,上面提到,thread_info结构和内核态堆栈是紧密结合在一起的,占据两个页框的物理内存空间。而且,这两个页框的起始起始地址是213对齐的。 - - -早期的版本中,不需要对64位处理器的支持,所以,内核通过简单的屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址了。 - - - -我们在下面对比了,获取正在运行的进程的thread_info的实现方式 - -| 架构 | 版本 | 定义链接 | 实现方式 | 思路解析 | -| ------------- |:-------------:|:-------------:|:-------------:|:-------------:| -| x86 | [3.14](http://lxr.free-electrons.com/ident?v=3.14;i=current_thread_info) | [current_thread_info(void)](http://lxr.free-electrons.com/source/arch/x86/include/asm/thread_info.h#L164) |return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); | 屏蔽了esp的低十三位,最终得到的是thread_info的地址 | -| x86 | [3.15](http://lxr.free-electrons.com/ident?v=3.15;i=current_thread_info) | [current_thread_info(void)](http://lxr.free-electrons.com/source/arch/x86/include/asm/thread_info.h?v=3.15#L163) | ti = (void *)(this_cpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE); | -| x86 | [4.1](http://lxr.free-electrons.com/ident?v=4.1&i=current_thread_info) | [current_thread_info(void)](http://lxr.free-electrons.com/source/arch/x86/include/asm/thread_info.h?v=4.1#L182) | (struct thread_info *)(current_top_of_stack() - THREAD_SIZE); - ->**早期版本** -> ->当前的栈指针(current_stack_pointer == sp)就是esp, -> ->THREAD_SIZE为8K,二进制的表示为0000 0000 0000 0000 0010 0000 0000 0000。 -> ->~(THREAD_SIZE-1)的结果刚好为1111 1111 1111 1111 1110 0000 0000 0000,第十三位是全为零,也就是刚好屏蔽了esp的低十三位,最终得到的是thread_info的地址。 - - -进程最常用的是进程描述符结构task_struct而不是thread_info结构的地址。为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏,由于task_struct *task在thread_info的起始位置,该宏本质上等价于current_thread_info()->task,在[include/asm-generic/current.h](http://lxr.free-electrons.com/source/include/asm-generic/current.h?v=4.5#L6)中定义: -```c -#define get_current() (current_thread_info()->task) -#define current get_current() -``` - -这个定义是体系结构无关的,当然linux也为各个体系结构定义了更加方便或者快速的current - ->>请参见 :http://lxr.free-electrons.com/ident?v=4.5;i=current - - -##分配和销毁thread_info -------- - -进程通过[alloc_thread_info_node](http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5;#L161)函数分配它的内核栈,通过[free_thread_info](http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5#L170)函数释放所分配的内核栈。 - -```c -# if THREAD_SIZE >= PAGE_SIZE -static struct thread_info *alloc_thread_info_node(struct task_struct *tsk, - int node) -{ - struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP, - THREAD_SIZE_ORDER); - - return page ? page_address(page) : NULL; -} - -static inline void free_thread_info(struct thread_info *ti) -{ - free_kmem_pages((unsigned long)ti, THREAD_SIZE_ORDER); -} -# else -static struct kmem_cache *thread_info_cache; - -static struct thread_info *alloc_thread_info_node(struct task_struct *tsk, - int node) -{ - return kmem_cache_alloc_node(thread_info_cache, THREADINFO_GFP, node); -} - -static void free_thread_info(struct thread_info *ti) -{ - kmem_cache_free(thread_info_cache, ti); -} -``` - -其中,[THREAD_SIZE_ORDER](http://lxr.free-electrons.com/ident?v=4.5;i=THREAD_SIZE_ORDER)宏的定义请查看 - -| 架构 | 版本 | 定义链接 | 实现方式 | 思路解析 | -| ------------- |:-------------:|:-------------:|:-------------:|:-------------:| -| x86 | 4.5 | [arch/x86/include/asm/page_32_types.h, line 20](http://lxr.free-electrons.com/source/arch/x86/include/asm/page_32_types.h?v=4.5#L20) | #define THREAD_SIZE_ORDER 1 | __get_free_pages函数分配2个页的内存(它的首地址是8192字节对齐的)| -| x86_64 | 4.5 | [arch/x86/include/asm/page_64_types.h, line 10](http://lxr.free-electrons.com/source/arch/x86/include/asm/page_64_types.h?v=4.5#L10)|#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER) - - - - - -#[进程标记](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1393) -------- - -``` -unsigned int flags; /* per process flags, defined below */ -``` - -反应进程状态的信息,但不是运行状态,用于内核识别进程当前的状态,以备下一步操作 - -flags成员的可能取值如下,这些宏以PF(ProcessFlag)开头 - ->参见 -> ->http://lxr.free-electrons.com/source/include/linux/sched.h?v4.5#L2083 -> ->例如 ->PF_FORKNOEXEC 进程刚创建,但还没执行。 ->PF_SUPERPRIV 超级用户特权。 ->PF_DUMPCORE dumped core。 ->PF_SIGNALED 进程被信号(signal)杀出。 ->PF_EXITING 进程开始关闭。 -> -```c - /* -* Per process flags -*/ -#define PF_EXITING 0x00000004 /* getting shut down */ -#define PF_EXITPIDONE 0x00000008 /* pi exit done on shut down */ -#define PF_VCPU 0x00000010 /* I'm a virtual CPU */ -#define PF_WQ_WORKER 0x00000020 /* I'm a workqueue worker */ -#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */ -#define PF_MCE_PROCESS 0x00000080 /* process policy on mce errors */ -#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */ -#define PF_DUMPCORE 0x00000200 /* dumped core */ -#define PF_SIGNALED 0x00000400 /* killed by a signal */ -#define PF_MEMALLOC 0x00000800 /* Allocating memory */ -#define PF_NPROC_EXCEEDED 0x00001000 /* set_user noticed that RLIMIT_NPROC was exceeded */ -#define PF_USED_MATH 0x00002000 /* if unset the fpu must be initialized before use */ -#define PF_USED_ASYNC 0x00004000 /* used async_schedule*(), used by module init */ -#define PF_NOFREEZE 0x00008000 /* this thread should not be frozen */ -#define PF_FROZEN 0x00010000 /* frozen for system suspend */ -#define PF_FSTRANS 0x00020000 /* inside a filesystem transaction */ -#define PF_KSWAPD 0x00040000 /* I am kswapd */ -#define PF_MEMALLOC_NOIO 0x00080000 /* Allocating memory without IO involved */ -#define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */ -#define PF_KTHREAD 0x00200000 /* I am a kernel thread */ -#define PF_RANDOMIZE 0x00400000 /* randomize virtual address space */ -#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */ -#define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_allowed */ -#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */ -#define PF_MUTEX_TESTER 0x20000000 /* Thread belongs to the rt mutex tester */ -#define PF_FREEZER_SKIP 0x40000000 /* Freezer should not count it as freezable */ -#define PF_SUSPEND_TASK 0x80000000 /* this thread called freeze_processes and should not be frozen */ -``` - -#[表示进程亲属关系的成员](http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1499) -------- -```c -/* - * pointers to (original) parent process, youngest child, younger sibling, - * older sibling, respectively. (p->father can be replaced with - * p->real_parent->pid) - */ -struct task_struct __rcu *real_parent; /* real parent process */ -struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */ -/* - * children/sibling forms the list of my natural children - */ -struct list_head children; /* list of my children */ -struct list_head sibling; /* linkage in my parent's children list */ -struct task_struct *group_leader; /* threadgroup leader */ -``` - - 在Linux系统中,所有进程之间都有着直接或间接地联系,每个进程都有其父进程,也可能有零个或多个子进程。拥有同一父进程的所有进程具有兄弟关系。 - - -| 字段 | 描述 | -| ------------- |:-------------:| -| real_parent | 指向其父进程,如果创建它的父进程不再存在,则指向PID为1的init进程 | -| parent | 指向其父进程,当它终止时,必须向它的父进程发送信号。它的值通常与real_parent相同 | -| children | 表示链表的头部,链表中的所有元素都是它的子进程 | -| sibling | 用于把当前进程插入到兄弟链表中 | -| group_leader | 指向其所在进程组的领头进程 | - -#[ptrace系统调用](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1394) -------- -Ptrace 提供了一种父进程可以控制子进程运行,并可以检查和改变它的核心image。 - -它主要用于实现断点调试。一个被跟踪的进程运行中,直到发生一个信号。则进程被中止,并且通知其父进程。在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。 - - -```c -unsigned int ptrace; - ptraced is the list of tasks this task is using ptrace on. -* This includes both natural children and PTRACE_ATTACH targets. -* p->ptrace_entry is p's link on the p->parent->ptraced list. -*/ -struct list_head ptraced; -struct list_head ptrace_entry; - -unsigned long ptrace_message; -siginfo_t *last_siginfo; /* For ptrace use. */ -``` - -成员ptrace被设置为0时表示不需要被跟踪,它的可能取值如下: - ->参见 -> ->http://lxr.free-electrons.com/source/include/linux/ptrace.h?v=4.5#L20 - -```c -/* - * Ptrace flags - * - * The owner ship rules for task->ptrace which holds the ptrace - * flags is simple. When a task is running it owns it's task->ptrace - * flags. When the a task is stopped the ptracer owns task->ptrace. - */ - -#define PT_SEIZED 0x00010000 /* SEIZE used, enable new behavior */ -#define PT_PTRACED 0x00000001 -#define PT_DTRACE 0x00000002 /* delayed trace (used on m68k, i386) */ -#define PT_PTRACE_CAP 0x00000004 /* ptracer can follow suid-exec */ - -#define PT_OPT_FLAG_SHIFT 3 -/* PT_TRACE_* event enable flags */ -#define PT_EVENT_FLAG(event) (1 << (PT_OPT_FLAG_SHIFT + (event))) -#define PT_TRACESYSGOOD PT_EVENT_FLAG(0) -#define PT_TRACE_FORK PT_EVENT_FLAG(PTRACE_EVENT_FORK) -#define PT_TRACE_VFORK PT_EVENT_FLAG(PTRACE_EVENT_VFORK) -#define PT_TRACE_CLONE PT_EVENT_FLAG(PTRACE_EVENT_CLONE) -#define PT_TRACE_EXEC PT_EVENT_FLAG(PTRACE_EVENT_EXEC) -#define PT_TRACE_VFORK_DONE PT_EVENT_FLAG(PTRACE_EVENT_VFORK_DONE) -#define PT_TRACE_EXIT PT_EVENT_FLAG(PTRACE_EVENT_EXIT) -#define PT_TRACE_SECCOMP PT_EVENT_FLAG(PTRACE_EVENT_SECCOMP) - -#define PT_EXITKILL (PTRACE_O_EXITKILL << PT_OPT_FLAG_SHIFT) -#define PT_SUSPEND_SECCOMP (PTRACE_O_SUSPEND_SECCOMP << PT_OPT_FLAG_SHIFT) - -/* single stepping state bits (used on ARM and PA-RISC) */ -#define PT_SINGLESTEP_BIT 31 -#define PT_SINGLESTEP (1<参见 -> ->http://lxr.free-electrons.com/source/include/uapi/linux/sched.h?v=4.5#L36 - -```c -/* -* Scheduling policies -*/ -#define SCHED_NORMAL 0 -#define SCHED_FIFO 1 -#define SCHED_RR 2 -#define SCHED_BATCH 3 -/* SCHED_ISO: reserved but not implemented yet */ -#define SCHED_IDLE 5 -#define SCHED_DEADLINE 6 -``` - -| 字段 | 描述 | 所在调度器类 | -| ------------- |:-------------:|:-------------:| -| SCHED_NORMAL | (也叫SCHED_OTHER)用于普通进程,通过CFS调度器实现。SCHED_BATCH用于非交互的处理器消耗型进程。SCHED_IDLE是在系统负载很低时使用 | CFS | -| SCHED_BATCH | SCHED_NORMAL普通进程策略的分化版本。采用分时策略,根据动态优先级(可用nice()API设置),分配 CPU 运算资源。注意:这类进程比上述两类实时进程优先级低,换言之,在有实时进程存在时,实时进程优先调度。但针对吞吐量优化 | CFS | -| SCHED_IDLE | 优先级最低,在系统空闲时才跑这类进程(如利用闲散计算机资源跑地外文明搜索,蛋白质结构分析等任务,是此调度策略的适用者)| CFS | -| SCHED_FIFO | 先入先出调度算法(实时调度策略),相同优先级的任务先到先服务,高优先级的任务可以抢占低优先级的任务 | RT | -| SCHED_RR | 轮流调度算法(实时调度策略),后 者提供 Roound-Robin 语义,采用时间片,相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性,同样,高优先级的任务可以抢占低优先级的任务。不同要求的实时任务可以根据需要用sched_setscheduler()API 设置策略 | RT | -| SCHED_DEADLINE | 新支持的实时进程调度策略,针对突发型计算,且对延迟和完成时间高度敏感的任务适用。基于Earliest Deadline First (EDF) 调度算法| - -##调度类 -------- - -sched_class结构体表示调度类,目前内核中有实现以下四种: - -```c -extern const struct sched_class stop_sched_class; -extern const struct sched_class dl_sched_class; -extern const struct sched_class rt_sched_class; -extern const struct sched_class fair_sched_class; -extern const struct sched_class idle_sched_class; -``` - - -| 调度器类 | 描述 | -| ------------- |:-------------:| -| idle_sched_class | 每个cup的第一个pid=0线程:swapper,是一个静态线程。调度类属于:idel_sched_class,所以在ps里面是看不到的。一般运行在开机过程和cpu异常的时候做dump | -| stop_sched_class | 优先级最高的线程,会中断所有其他线程,且不会被其他任务打断。作用:1.发生在cpu_stop_cpu_callback 进行cpu之间任务migration;2.HOTPLUG_CPU的情况下关闭任务。| -| rt_sched_class | RT,作用:实时线程 | -| fair_sched_class | CFS(公平),作用:一般常规线程 | - -目前系統中,Scheduling Class的优先级顺序为StopTask > RealTime > Fair > IdleTask - -开发者可以根据己的设计需求,來把所属的Task配置到不同的Scheduling Class中. - -#[进程地址空间](http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1453) -------- - -```c -/* http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1453 */ -struct mm_struct *mm, *active_mm; -/* per-thread vma caching */ -u32 vmacache_seqnum; -struct vm_area_struct *vmacache[VMACACHE_SIZE]; -#if defined(SPLIT_RSS_COUNTING) -struct task_rss_stat rss_stat; -#endif - -/* http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1484 */ -#ifdef CONFIG_COMPAT_BRK -unsigned brk_randomized:1; -#endif -``` - -| 字段 | 描述 | -| ------------- |:-------------:| -| mm | 进程所拥有的用户空间内存描述符,内核线程无的mm为NULL | -| active_mm | active_mm指向进程运行时所使用的内存描述符, 对于普通进程而言,这两个指针变量的值相同。但是内核线程kernel thread是没有进程地址空间的,所以内核线程的tsk->mm域是空(NULL)。但是内核必须知道用户空间包含了什么,因此它的active_mm成员被初始化为前一个运行进程的active_mm值。| -| brk_randomized| 用来确定对随机堆内存的探测。参见[LKML]( http://lkml.indiana.edu/hypermail/linux/kernel/1104.1/00196.html)上的介绍 | -| rss_stat | 用来记录缓冲信息 | ->因此如果当前内核线程被调度之前运行的也是另外一个内核线程时候,那么其mm和avtive_mm都是NULL - - -#判断标志 -------- -```c -int exit_code, exit_signal; -int pdeath_signal; /* The signal sent when the parent dies */ -unsigned long jobctl; /* JOBCTL_*, siglock protected */ - -/* Used for emulating ABI behavior of previous Linux versions */ -unsigned int personality; - -/* scheduler bits, serialized by scheduler locks */ -unsigned sched_reset_on_fork:1; -unsigned sched_contributes_to_load:1; -unsigned sched_migrated:1; -unsigned :0; /* force alignment to the next boundary */ - -/* unserialized, strictly 'current' */ -unsigned in_execve:1; /* bit to tell LSMs we're in execve */ -unsigned in_iowait:1; -``` -| 字段 | 描述 | -| ------------- |:-------------:| -| exit_code | 用于设置进程的终止代号,这个值要么是_exit()或exit_group()系统调用参数(正常终止),要么是由内核提供的一个错误代号(异常终止)。| -| exit_signal | 被置为-1时表示是某个线程组中的一员。只有当线程组的最后一个成员终止时,才会产生一个信号,以通知线程组的领头进程的父进程。| -| pdeath_signal | 用于判断父进程终止时发送信号。| -| personality | 用于处理不同的ABI,参见[Linux-Man](http://man7.org/linux/man-pages/man2/personality.2.html) | -| in_execve | 用于通知LSM是否被do_execve()函数所调用。详见补丁说明,参见[LKML](http://lkml.indiana.edu/hypermail/linux/kernel/0901.1/00014.html) | -| in_iowait | 用于判断是否进行iowait计数 | -| sched_reset_on_fork | 用于判断是否恢复默认的优先级或调度策略 | - - -#[时间](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1530) -------- - -```c -cputime_t utime, stime, utimescaled, stimescaled; -cputime_t gtime; -struct prev_cputime prev_cputime; -#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN -seqcount_t vtime_seqcount; -unsigned long long vtime_snap; -enum { - /* Task is sleeping or running in a CPU with VTIME inactive */ - VTIME_INACTIVE = 0, - /* Task runs in userspace in a CPU with VTIME active */ - VTIME_USER, - /* Task runs in kernelspace in a CPU with VTIME active */ - VTIME_SYS, -} vtime_snap_whence; -#endif -unsigned long nvcsw, nivcsw; /* context switch counts */ -u64 start_time; /* monotonic time in nsec */ -u64 real_start_time; /* boot based time in nsec */ -/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ -unsigned long min_flt, maj_flt; - -struct task_cputime cputime_expires; -struct list_head cpu_timers[3]; - -/* process credentials */ -const struct cred __rcu *real_cred; /* objective and real subjective task - * credentials (COW) */ -const struct cred __rcu *cred; /* effective (overridable) subjective task - * credentials (COW) */ -char comm[TASK_COMM_LEN]; /* executable name excluding path - - access with [gs]et_task_comm (which lock - it with task_lock()) - - initialized normally by setup_new_exec */ -/* file system info */ -struct nameidata *nameidata; -#ifdef CONFIG_SYSVIPC -/* ipc stuff */ -struct sysv_sem sysvsem; -struct sysv_shm sysvshm; -#endif -#ifdef CONFIG_DETECT_HUNG_TASK -/* hung task detection */ -unsigned long last_switch_count; -#endif -``` - -| 字段 | 描述 | -| ------------- |:-------------:| -| utime/stime | 用于记录进程在用户态/内核态下所经过的节拍数(定时器)| -| prev_utime/prev_stime | 先前的运行时间,请参考[LKML](http://lkml.indiana.edu/hypermail/linux/kernel/1003.3/02431.html)的补丁说明 | -| utimescaled/stimescaled | 用于记录进程在用户态/内核态的运行时间,但它们以处理器的频率为刻度 | -| gtime | 以节拍计数的虚拟机运行时间(guest time) | -| nvcsw/nivcsw | 是自愿(voluntary)/非自愿(involuntary)上下文切换计数 | -| last_switch_count | nvcsw和nivcsw的总和 | -| start_time/real_start_time | 进程创建时间,real_start_time还包含了进程睡眠时间,常用于/proc/pid/stat,补丁说明请参考[LKML](http://lkml.indiana.edu/hypermail/linux/kernel/0705.0/2094.html) | -| cputime_expires | 用来统计进程或进程组被跟踪的处理器时间,其中的三个成员对应着cpu_timers[3]的三个链表 | - -#[信号处理]() -------- - -```c -/* signal handlers */ -struct signal_struct *signal; -struct sighand_struct *sighand; -1583 -sigset_t blocked, real_blocked; -sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */ -struct sigpending pending; -1587 -unsigned long sas_ss_sp; -size_t sas_ss_size; -``` - - -| 字段 | 描述 | -| ------------- |:-------------:| -| signal | 指向进程的信号描述符 | -| sighand | 指向进程的信号处理程序描述符 | -| blocked | 表示被阻塞信号的掩码,real_blocked表示临时掩码 | -| pending | 存放私有挂起信号的数据结构 | -| sas_ss_sp | 是信号处理程序备用堆栈的地址,sas_ss_size表示堆栈的大小 | - - -#其他 -------- - (1)、用于保护资源分配或释放的自旋锁 - -``` -/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed, - * mempolicy */ - spinlock_t alloc_lock; -``` - (2)、进程描述符使用计数,被置为2时,表示进程描述符正在被使用而且其相应的进程处于活动状态 - -```c -atomic_t usage; -``` - (3)、用于表示获取大内核锁的次数,如果进程未获得过锁,则置为-1。 - -```c -int lock_depth; /* BKL lock depth */ -``` - (4)、在SMP上帮助实现无加锁的进程切换(unlocked context switches) - -```c -#ifdef CONFIG_SMP -#ifdef __ARCH_WANT_UNLOCKED_CTXSW - int oncpu; -#endif -#endif -``` - (5)、preempt_notifier结构体链表 - -```c -#ifdef CONFIG_PREEMPT_NOTIFIERS - /* list of struct preempt_notifier: */ - struct hlist_head preempt_notifiers; -#endif -``` - (6)、FPU使用计数 - -```c -unsigned char fpu_counter; -``` - (7)、 blktrace是一个针对Linux内核中块设备I/O层的跟踪工具。 - -```c -#ifdef CONFIG_BLK_DEV_IO_TRACE - unsigned int btrace_seq; -#endif -``` - (8)、RCU同步原语 - -```c -#ifdef CONFIG_PREEMPT_RCU - int rcu_read_lock_nesting; - char rcu_read_unlock_special; - struct list_head rcu_node_entry; -#endif /* #ifdef CONFIG_PREEMPT_RCU */ -#ifdef CONFIG_TREE_PREEMPT_RCU - struct rcu_node *rcu_blocked_node; -#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */ -#ifdef CONFIG_RCU_BOOST - struct rt_mutex *rcu_boost_mutex; -#endif /* #ifdef CONFIG_RCU_BOOST */ -``` - (9)、用于调度器统计进程的运行信息 - -```c -#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT) - struct sched_info sched_info; -#endif -``` - (10)、用于构建进程链表 - -```c -struct list_head tasks; -``` - (11)、to limit pushing to one attempt - -```c -#ifdef CONFIG_SMP - struct plist_node pushable_tasks; -#endif -``` - 补丁说明请参考:http://lkml.indiana.edu/hypermail/linux/kernel/0808.3/0503.html - - (12)、防止内核堆栈溢出 - -```c -#ifdef CONFIG_CC_STACKPROTECTOR - /* Canary value for the -fstack-protector gcc feature */ - unsigned long stack_canary; -#endif -``` - 在GCC编译内核时,需要加上-fstack-protector选项。 - - (13)、PID散列表和链表 - -```c -/* PID/PID hash table linkage. */ -struct pid_link pids[PIDTYPE_MAX]; -struct list_head thread_group; //线程组中所有进程的链表 -``` - (14)、do_fork函数 - -```c -struct completion *vfork_done; /* for vfork() */ -int __user *set_child_tid; /* CLONE_CHILD_SETTID */ -int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */ -``` - 在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特殊地址。 - - 如果copy_process函数的clone_flags参数的值被置为CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID,则会把child_tidptr参数的值分别复制到set_child_tid和clear_child_tid成员。这些标志说明必须改变子进程用户态地址空间的child_tidptr所指向的变量的值。 - - (15)、缺页统计 - -```c -/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ - unsigned long min_flt, maj_flt; -``` - (16)、进程权能 - -```c -const struct cred __rcu *real_cred; /* objective and real subjective task - * credentials (COW) */ -const struct cred __rcu *cred; /* effective (overridable) subjective task - * credentials (COW) */ -struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */ -``` - (17)、相应的程序名 - -```c -char comm[TASK_COMM_LEN]; -``` - (18)、文件 - -```c -/* file system info */ - int link_count, total_link_count; -/* filesystem information */ - struct fs_struct *fs; -/* open file information */ - struct files_struct *files; -``` - fs用来表示进程与文件系统的联系,包括当前目录和根目录。 - - files表示进程当前打开的文件。 - - (19)、进程通信(SYSVIPC) - -```c -#ifdef CONFIG_SYSVIPC -/* ipc stuff */ - struct sysv_sem sysvsem; -#endif -``` - (20)、处理器特有数据 - -```c -/* CPU-specific state of this task */ - struct thread_struct thread; -``` - (21)、命名空间 - -```c -/* namespaces */ - struct nsproxy *nsproxy; -``` - (22)、进程审计 - -```c - struct audit_context *audit_context; -#ifdef CONFIG_AUDITSYSCALL - uid_t loginuid; - unsigned int sessionid; -#endif -``` - (23)、secure computing - -```c -seccomp_t seccomp; -``` - (24)、用于copy_process函数使用CLONE_PARENT 标记时 - -```c -/* Thread group tracking */ - u32 parent_exec_id; - u32 self_exec_id; -``` - (25)、中断 - -```c -#ifdef CONFIG_GENERIC_HARDIRQS - /* IRQ handler threads */ - struct irqaction *irqaction; -#endif -#ifdef CONFIG_TRACE_IRQFLAGS - unsigned int irq_events; - unsigned long hardirq_enable_ip; - unsigned long hardirq_disable_ip; - unsigned int hardirq_enable_event; - unsigned int hardirq_disable_event; - int hardirqs_enabled; - int hardirq_context; - unsigned long softirq_disable_ip; - unsigned long softirq_enable_ip; - unsigned int softirq_disable_event; - unsigned int softirq_enable_event; - int softirqs_enabled; - int softirq_context; -#endif -``` - (26)、task_rq_lock函数所使用的锁 - -```c -/* Protection of the PI data structures: */ -raw_spinlock_t pi_lock; -``` - (27)、基于PI协议的等待互斥锁,其中PI指的是priority inheritance(优先级继承) - -```c -#ifdef CONFIG_RT_MUTEXES - /* PI waiters blocked on a rt_mutex held by this task */ - struct plist_head pi_waiters; - /* Deadlock detection and priority inheritance handling */ - struct rt_mutex_waiter *pi_blocked_on; -#endif -``` - (28)、死锁检测 - -```c -#ifdef CONFIG_DEBUG_MUTEXES - /* mutex deadlock detection */ - struct mutex_waiter *blocked_on; -#endif -``` - (29)、lockdep,参见内核说明文档linux-2.6.38.8/Documentation/lockdep-design.txt - -```c -#ifdef CONFIG_LOCKDEP -# define MAX_LOCK_DEPTH 48UL - u64 curr_chain_key; - int lockdep_depth; - unsigned int lockdep_recursion; - struct held_lock held_locks[MAX_LOCK_DEPTH]; - gfp_t lockdep_reclaim_gfp; -#endif -``` - (30)、JFS文件系统 - -```c -/* journalling filesystem info */ - void *journal_info; -``` - (31)、块设备链表 - -```c -/* stacked block device info */ - struct bio_list *bio_list; -``` - (32)、内存回收 - -```c -struct reclaim_state *reclaim_state; -``` - (33)、存放块设备I/O数据流量信息 - -```c -struct backing_dev_info *backing_dev_info; -``` - - (34)、I/O调度器所使用的信息 - -```c -struct io_context *io_context; -``` - (35)、记录进程的I/O计数 - -```c -struct task_io_accounting ioac; -if defined(CONFIG_TASK_XACCT) -u64 acct_rss_mem1; /* accumulated rss usage */ -u64 acct_vm_mem1; /* accumulated virtual memory usage */ -cputime_t acct_timexpd; /* stime + utime since last update */ -endif -``` -在Ubuntu 11.04上,执行cat获得进程1的I/O计数如下: - -![获得进程1的I/O计数](./images/cat_proc_io.png) - -输出的数据项刚好是task_io_accounting结构体的所有成员。 - - (36)、CPUSET功能 - -```c -#ifdef CONFIG_CPUSETS - nodemask_t mems_allowed; /* Protected by alloc_lock */ - int mems_allowed_change_disable; - int cpuset_mem_spread_rotor; - int cpuset_slab_spread_rotor; -#endif -``` - (37)、Control Groups - -```c -#ifdef CONFIG_CGROUPS - /* Control Group info protected by css_set_lock */ - struct css_set __rcu *cgroups; - /* cg_list protected by css_set_lock and tsk->alloc_lock */ - struct list_head cg_list; -#endif -#ifdef CONFIG_CGROUP_MEM_RES_CTLR /* memcg uses this to do batch job */ - struct memcg_batch_info { - int do_batch; /* incremented when batch uncharge started */ - struct mem_cgroup *memcg; /* target memcg of uncharge */ - unsigned long bytes; /* uncharged usage */ - unsigned long memsw_bytes; /* uncharged mem+swap usage */ - } memcg_batch; -#endif -``` - (38)、futex同步机制 - -```c -#ifdef CONFIG_FUTEX - struct robust_list_head __user *robust_list; -#ifdef CONFIG_COMPAT - struct compat_robust_list_head __user *compat_robust_list; -#endif - struct list_head pi_state_list; - struct futex_pi_state *pi_state_cache; -#endif -``` - (39)、非一致内存访问(NUMA Non-Uniform Memory Access) - -```c -#ifdef CONFIG_NUMA - struct mempolicy *mempolicy; /* Protected by alloc_lock */ - short il_next; -#endif -``` - (40)、文件系统互斥资源 - -```c -atomic_t fs_excl; /* holding fs exclusive resources */ -``` - (41)、RCU链表 - -```c -struct rcu_head rcu; -``` - (42)、管道 - -```c -struct pipe_inode_info *splice_pipe; -``` - (43)、延迟计数 - -```c -#ifdef CONFIG_TASK_DELAY_ACCT - struct task_delay_info *delays; -#endif -``` - (44)、fault injection,参考内核说明文件linux-2.6.38.8/Documentation/fault-injection/fault-injection.txt - -```c -#ifdef CONFIG_FAULT_INJECTION - int make_it_fail; -#endif -``` - (45)、FLoating proportions - -```c -struct prop_local_single dirties; -``` - (46)、Infrastructure for displayinglatency - -```c -#ifdef CONFIG_LATENCYTOP - int latency_record_count; - struct latency_record latency_record[LT_SAVECOUNT]; -#endif -``` - (47)、time slack values,常用于poll和select函数 - -```c -unsigned long timer_slack_ns; -unsigned long default_timer_slack_ns; -``` - (48)、socket控制消息(control message) - -```c -struct list_head *scm_work_list; -``` - (49)、ftrace跟踪器 - -```c -#ifdef CONFIG_FUNCTION_GRAPH_TRACER - /* Index of current stored address in ret_stack */ - int curr_ret_stack; - /* Stack of return addresses for return function tracing */ - struct ftrace_ret_stack *ret_stack; - /* time stamp for last schedule */ - unsigned long long ftrace_timestamp; - /* - * Number of functions that haven't been traced - * because of depth overrun. - */ - atomic_t trace_overrun; - /* Pause for the tracing */ - atomic_t tracing_graph_pause; -#endif -#ifdef CONFIG_TRACING - /* state flags for use by tracers */ - unsigned long trace; - /* bitmask of trace recursion */ - unsigned long trace_recursion; -#endif /* CONFIG_TRACING */ -``` + Linux进程描述符task_struct结构体详解--Linux进程的管理与调度(一) + ======= + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------------- |:-------------:|:-------------:|:-------------:|:-------------:|:-------------:| +| 2016-05-12 | [Linux-4.5](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux-进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | + + +进程是处于执行期的程序以及它所管理的资源(如打开的文件、挂起的信号、进程状态、地址空间等等)的总称。注意,程序并不是进程,实际上两个或多个进程不仅有可能执行同一程序,而且还有可能共享地址空间等资源。 + + +Linux内核通过一个被称为进程描述符的[task_struct](http://lxr.free-electrons.com/source/include/linux/sched.h#L1389)结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在[include/linux/sched.h](http://lxr.free-electrons.com/source/include/linux/sched.h)文件中。 + +谈到task_struct结构体,可以说她是linux内核源码中最复杂的一个结构体了,成员之多,占用内存之大。 + +鉴于她的复杂,我们不能简单的亵渎,而是要深入“窥探”. + +下面来慢慢介绍这些复杂成员 + +#[进程状态](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1390) +------- +```c + volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */ +``` + +[state](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1390)成员的可能取值如下 + +>参见http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L207 + +``` + + /* + * Task state bitmask. NOTE! These bits are also + * encoded in fs/proc/array.c: get_task_state(). + * + * We have two separate sets of flags: task->state + * is about runnability, while task->exit_state are + * about the task exiting. Confusing, but this way + * modifying one set can't modify the other one by + * mistake. + */ + #define TASK_RUNNING 0 + #define TASK_INTERRUPTIBLE 1 + #define TASK_UNINTERRUPTIBLE 2 + #define __TASK_STOPPED 4 + #define __TASK_TRACED 8 + +/* in tsk->exit_state */ + #define EXIT_DEAD 16 + #define EXIT_ZOMBIE 32 + #define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD) + +/* in tsk->state again */ + #define TASK_DEAD 64 + #define TASK_WAKEKILL 128 /** wake on signals that are deadly **/ + #define TASK_WAKING 256 + #define TASK_PARKED 512 + #define TASK_NOLOAD 1024 + #define TASK_STATE_MAX 2048 + + /* Convenience macros for the sake of set_task_state */ +#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) +#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) +#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) + +``` + +##5个互斥状态 +------- + +state域能够取5个互为排斥的值(通俗一点就是这五个值任意两个不能一起使用,只能单独使用)。系统中的每个进程都必然处于以上所列进程状态中的一种。 + +| 状态| 描述 | +| ------------- |:-------------:| +| TASK_RUNNING | 表示进程要么正在执行,要么正要准备执行(已经就绪),正在等待cpu时间片的调度 | +| TASK_INTERRUPTIBLE | 进程因为等待一些条件而被挂起(阻塞)而所处的状态。这些条件主要包括:硬中断、资源、一些信号……,一旦等待的条件成立,进程就会从该状态(阻塞)迅速转化成为就绪状态TASK_RUNNING | +| TASK_UNINTERRUPTIBLE | 意义与TASK_INTERRUPTIBLE类似,除了不能通过接受一个信号来唤醒以外,对于处于TASK_UNINTERRUPIBLE状态的进程,哪怕我们传递一个信号或者有一个外部中断都不能唤醒他们。只有它所等待的资源可用的时候,他才会被唤醒。这个标志很少用,但是并不代表没有任何用处,其实他的作用非常大,特别是对于驱动刺探相关的硬件过程很重要,这个刺探过程不能被一些其他的东西给中断,否则就会让进城进入不可预测的状态 | +| TASK_STOPPED | 进程被停止执行,当进程接收到SIGSTOP、SIGTTIN、SIGTSTP或者SIGTTOU信号之后就会进入该状态 | +| TASK_TRACED | 表示进程被debugger等进程监视,进程执行被调试程序所停止,当一个进程被另外的进程所监视,每一个信号都会让进城进入该状态 | + +##2个终止状态 +------- + +其实还有两个附加的进程状态既可以被添加到state域中,又可以被添加到[exit_state](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1461)域中。只有当进程终止的时候,才会达到这两种状态. + +```c +/* task state */ +int exit_state; +int exit_code, exit_signal; +``` + +| 状态| 描述 | +| ------------- |:-------------:| +| EXIT_ZOMBIE | 进程的执行被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息,此时进程成为僵尸进程 | +|EXIT_DEAD | 进程的最终状态 | + +而int exit_code, exit_signal;我们会在后面进程介绍 + +##新增睡眠状态 +------- + +>参见 +> +>[TASK_KILLABLE:Linux 中的新进程状态](http://www.ibm.com/developerworks/cn/linux/l-task-killable/index.html) + + +如前所述,进程状态 TASK_UNINTERRUPTIBLE 和 TASK_INTERRUPTIBLE 都是睡眠状态。现在,我们来看看内核如何将进程置为睡眠状态。 + +###内核如何将进程置为睡眠状态。 +------- + +Linux 内核提供了两种方法将进程置为睡眠状态。 + +将进程置为睡眠状态的**普通方法**是将进程状态设置为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 并调用调度程序的 schedule() 函数。这样会将进程从 CPU 运行队列中移除。 + +* 如果进程处于可中断模式的睡眠状态(通过将其状态设置为 TASK_INTERRUPTIBLE),那么可以通过显式的唤醒呼叫(wakeup_process())或需要处理的信号来唤醒它。 + +* 但是,如果进程处于非可中断模式的睡眠状态(通过将其状态设置为 TASK_UNINTERRUPTIBLE),那么只能通过显式的唤醒呼叫将其唤醒。除非万不得已,否则我们建议您将进程置为可中断睡眠模式,而不是不可中断睡眠模式(比如说在设备 I/O 期间,处理信号非常困难时)。 + +当处于可中断睡眠模式的任务接收到信号时,它需要处理该信号(除非它已被屏弊),离开之前正在处理的任务(此处需要清除代码),并将 -EINTR 返回给用户空间。再一次,检查这些返回代码和采取适当操作的工作将由程序员完成。 + +因此,懒惰的程序员可能比较喜欢将进程置为不可中断模式的睡眠状态,因为信号不会唤醒这类任务。 + +但需要注意的一种情况是,对不可中断睡眠模式的进程的唤醒呼叫可能会由于某些原因不会发生,这会使进程无法被终止,从而最终引发问题,因为惟一的解决方法就是重启系统。一方面,您需要考虑一些细节,因为不这样做会在内核端和用户端引入 bug。另一方面,您可能会生成永远不会停止的进程(被阻塞且无法终止的进程)。 + +现在,我们在内核中实现了一种**新的睡眠方法** + +Linux Kernel 2.6.25 引入了一种新的进程睡眠状态, + +| 状态| 描述 | +| ------------- |:-------------:| +| TASK_KILLABLE | 当进程处于这种可以终止的新睡眠状态中,它的运行原理类似于 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号 | + +它定义如下: + +```c +#define TASK_WAKEKILL 128 /** wake on signals that are deadly **/ + +/* Convenience macros for the sake of set_task_state */ +#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE) +#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED) +#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED) +``` + +换句话说,TASK_UNINTERRUPTIBLE + TASK_WAKEKILL = TASK_KILLABLE。 + +而TASK_WAKEKILL 用于在接收到致命信号时唤醒进程 + +新的睡眠状态允许 TASK_UNINTERRUPTIBLE 响应致命信号 + + + +进程状态的切换过程和原因大致如下图 + +![进程状态转换图](./images/state.gif) + +#[进程标识符(PID)](http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1492) +------- + +``` +pid_t pid; +pid_t tgid; +``` + +Unix系统通过pid来标识进程,linux把不同的pid与系统中每个进程或轻量级线程关联,而unix程序员希望同一组线程具有共同的pid,遵照这个标准linux引入线程组的概念。一个线程组所有线程与领头线程具有相同的pid,存入tgid字段,getpid()返回当前进程的tgid值而不是pid的值。 + + + +在CONFIG_BASE_SMALL配置为0的情况下,PID的取值范围是0到32767,即系统中的进程数最大为32768个。 + + + +``` +#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000) +``` + +>参见 http://lxr.free-electrons.com/source/include/linux/threads.h#L27 + +在Linux系统中,一个线程组中的所有线程使用和该线程组的领头线程(该组中的第一个轻量级进程)相同的PID,并被存放在tgid成员中。只有线程组的领头线程的pid成员才会被设置为与tgid相同的值。注意,getpid()系统调用返回的是当前进程的tgid值而不是pid值。 + + +#[进程内核栈](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1391) +------- + +```c +void *stack; +``` + +##内核栈与线程描述符 +------- + +对每个进程,Linux内核都把两个不同的数据结构紧凑的存放在一个单独为进程分配的内存区域中 + +* 一个是内核态的进程堆栈, + +* 另一个是紧挨着进程描述符的小数据结构thread_info,叫做线程描述符。 + +Linux把thread_info(线程描述符)和内核态的线程堆栈存放在一起,这块区域通常是8192K(占两个页框),其实地址必须是8192的整数倍。 + +在linux/arch/x86/include/asm/page_32_types.h中, +```c +#define THREAD_SIZE_ORDER 1 +#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER) +``` + +出于效率考虑,内核让这8K空间占据连续的两个页框并让第一个页框的起始地址是213的倍数。 + +内核态的进程访问处于内核数据段的栈,这个栈不同于用户态的进程所用的栈。 + +用户态进程所用的栈,是在进程线性地址空间中; + +而内核栈是当进程从用户空间进入内核空间时,特权级发生变化,需要切换堆栈,那么内核空间中使用的就是这个内核栈。因为内核控制路径使用很少的栈空间,所以只需要几千个字节的内核态堆栈。 + +>需要注意的是,**内核态堆栈**仅用于内核例程,Linux内核另外为中断提供了单独的**硬中断栈**和**软中断栈** + + +下图中显示了在物理内存中存放两种数据结构的方式。线程描述符驻留与这个内存区的开始,而栈顶末端向下增长。 下图摘自ULK3,进程内核栈与进程描述符的关系如下图: + +![thread_info](./images/thread_info.gif) + +但是较新的内核代码中,进程描述符task_struct结构中没有直接指向thread_info结构的指针,而是用一个void指针类型的成员表示,然后通过类型转换来访问thread_info结构。 + +相关代码在[include/linux/sched.h](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L2812)中 + +```c +#define task_thread_info(task) ((struct thread_info *)(task)->stack) +``` + +在这个图中,esp寄存器是CPU栈指针,用来存放栈顶单元的地址。在80x86系统中,栈起始于顶端,并朝着这个内存区开始的方向增长。从用户态刚切换到内核态以后,进程的内核栈总是空的。因此,esp寄存器指向这个栈的顶端。一旦数据写入堆栈,esp的值就递减。 + +##内核栈数据结构描述thread_info和thread_union +------- + +thread_info是体系结构相关的,结构的定义在[thread_info.h](http://lxr.free-electrons.com/ident?v=4.5;i=thread_info)中 + +| 架构 | 定义链接 | +| ------------- |:-------------:| +| x86 | [linux-4.5/arch/x86/include/asm/thread_info.h, line 55](http://lxr.free-electrons.com/source/arch/x86/include/asm/thread_info.h?v=4.5#L55) | +| arm | [linux-4.5arch/arm/include/asm/thread_info.h, line 49](http://lxr.free-electrons.com/source/arch/arm/include/asm/thread_info.h#L49) +| arm64 | [linux/4.5/arch/arm64/include/asm/thread_info.h, line 47](http://lxr.free-electrons.com/source/arch/arm64/include/asm/thread_info.h#L47) | + +Linux内核中使用一个联合体来表示一个进程的线程描述符和内核栈: +```c +union thread_union +{ + struct thread_info thread_info; + unsigned long stack[THREAD_SIZE/sizeof(long)]; +}; +``` + +##获取当前在CPU上正在运行进程的thread_info +------- +下面来说说如何通过esp栈指针来获取当前在CPU上正在运行进程的thread_info结构。 + + +实际上,上面提到,thread_info结构和内核态堆栈是紧密结合在一起的,占据两个页框的物理内存空间。而且,这两个页框的起始起始地址是213对齐的。 + + +早期的版本中,不需要对64位处理器的支持,所以,内核通过简单的屏蔽掉esp的低13位有效位就可以获得thread_info结构的基地址了。 + + + +我们在下面对比了,获取正在运行的进程的thread_info的实现方式 + +| 架构 | 版本 | 定义链接 | 实现方式 | 思路解析 | +| ------------- |:-------------:|:-------------:|:-------------:|:-------------:| +| x86 | [3.14](http://lxr.free-electrons.com/ident?v=3.14;i=current_thread_info) | [current_thread_info(void)](http://lxr.free-electrons.com/source/arch/x86/include/asm/thread_info.h#L164) |return (struct thread_info *)(sp & ~(THREAD_SIZE - 1)); | 屏蔽了esp的低十三位,最终得到的是thread_info的地址 | +| x86 | [3.15](http://lxr.free-electrons.com/ident?v=3.15;i=current_thread_info) | [current_thread_info(void)](http://lxr.free-electrons.com/source/arch/x86/include/asm/thread_info.h?v=3.15#L163) | ti = (void *)(this_cpu_read_stable(kernel_stack) + KERNEL_STACK_OFFSET - THREAD_SIZE); | +| x86 | [4.1](http://lxr.free-electrons.com/ident?v=4.1&i=current_thread_info) | [current_thread_info(void)](http://lxr.free-electrons.com/source/arch/x86/include/asm/thread_info.h?v=4.1#L182) | (struct thread_info *)(current_top_of_stack() - THREAD_SIZE); + +>**早期版本** +> +>当前的栈指针(current_stack_pointer == sp)就是esp, +> +>THREAD_SIZE为8K,二进制的表示为0000 0000 0000 0000 0010 0000 0000 0000。 +> +>~(THREAD_SIZE-1)的结果刚好为1111 1111 1111 1111 1110 0000 0000 0000,第十三位是全为零,也就是刚好屏蔽了esp的低十三位,最终得到的是thread_info的地址。 + + +进程最常用的是进程描述符结构task_struct而不是thread_info结构的地址。为了获取当前CPU上运行进程的task_struct结构,内核提供了current宏,由于task_struct *task在thread_info的起始位置,该宏本质上等价于current_thread_info()->task,在[include/asm-generic/current.h](http://lxr.free-electrons.com/source/include/asm-generic/current.h?v=4.5#L6)中定义: +```c +#define get_current() (current_thread_info()->task) +#define current get_current() +``` + +这个定义是体系结构无关的,当然linux也为各个体系结构定义了更加方便或者快速的current + +>>请参见 :http://lxr.free-electrons.com/ident?v=4.5;i=current + + +##分配和销毁thread_info +------- + +进程通过[alloc_thread_info_node](http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5;#L161)函数分配它的内核栈,通过[free_thread_info](http://lxr.free-electrons.com/source/kernel/fork.c?v=4.5#L170)函数释放所分配的内核栈。 + +```c +# if THREAD_SIZE >= PAGE_SIZE +static struct thread_info *alloc_thread_info_node(struct task_struct *tsk, + int node) +{ + struct page *page = alloc_kmem_pages_node(node, THREADINFO_GFP, + THREAD_SIZE_ORDER); + + return page ? page_address(page) : NULL; +} + +static inline void free_thread_info(struct thread_info *ti) +{ + free_kmem_pages((unsigned long)ti, THREAD_SIZE_ORDER); +} +# else +static struct kmem_cache *thread_info_cache; + +static struct thread_info *alloc_thread_info_node(struct task_struct *tsk, + int node) +{ + return kmem_cache_alloc_node(thread_info_cache, THREADINFO_GFP, node); +} + +static void free_thread_info(struct thread_info *ti) +{ + kmem_cache_free(thread_info_cache, ti); +} +``` + +其中,[THREAD_SIZE_ORDER](http://lxr.free-electrons.com/ident?v=4.5;i=THREAD_SIZE_ORDER)宏的定义请查看 + +| 架构 | 版本 | 定义链接 | 实现方式 | 思路解析 | +| ------------- |:-------------:|:-------------:|:-------------:|:-------------:| +| x86 | 4.5 | [arch/x86/include/asm/page_32_types.h, line 20](http://lxr.free-electrons.com/source/arch/x86/include/asm/page_32_types.h?v=4.5#L20) | #define THREAD_SIZE_ORDER 1 | __get_free_pages函数分配2个页的内存(它的首地址是8192字节对齐的)| +| x86_64 | 4.5 | [arch/x86/include/asm/page_64_types.h, line 10](http://lxr.free-electrons.com/source/arch/x86/include/asm/page_64_types.h?v=4.5#L10)|#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER) + + + + + +#[进程标记](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1393) +------- + +``` +unsigned int flags; /* per process flags, defined below */ +``` + +反应进程状态的信息,但不是运行状态,用于内核识别进程当前的状态,以备下一步操作 + +flags成员的可能取值如下,这些宏以PF(ProcessFlag)开头 + +>参见 +> +>http://lxr.free-electrons.com/source/include/linux/sched.h?v4.5#L2083 +> +>例如 +>PF_FORKNOEXEC 进程刚创建,但还没执行。 +>PF_SUPERPRIV 超级用户特权。 +>PF_DUMPCORE dumped core。 +>PF_SIGNALED 进程被信号(signal)杀出。 +>PF_EXITING 进程开始关闭。 +> +```c + /* +* Per process flags +*/ +#define PF_EXITING 0x00000004 /* getting shut down */ +#define PF_EXITPIDONE 0x00000008 /* pi exit done on shut down */ +#define PF_VCPU 0x00000010 /* I'm a virtual CPU */ +#define PF_WQ_WORKER 0x00000020 /* I'm a workqueue worker */ +#define PF_FORKNOEXEC 0x00000040 /* forked but didn't exec */ +#define PF_MCE_PROCESS 0x00000080 /* process policy on mce errors */ +#define PF_SUPERPRIV 0x00000100 /* used super-user privileges */ +#define PF_DUMPCORE 0x00000200 /* dumped core */ +#define PF_SIGNALED 0x00000400 /* killed by a signal */ +#define PF_MEMALLOC 0x00000800 /* Allocating memory */ +#define PF_NPROC_EXCEEDED 0x00001000 /* set_user noticed that RLIMIT_NPROC was exceeded */ +#define PF_USED_MATH 0x00002000 /* if unset the fpu must be initialized before use */ +#define PF_USED_ASYNC 0x00004000 /* used async_schedule*(), used by module init */ +#define PF_NOFREEZE 0x00008000 /* this thread should not be frozen */ +#define PF_FROZEN 0x00010000 /* frozen for system suspend */ +#define PF_FSTRANS 0x00020000 /* inside a filesystem transaction */ +#define PF_KSWAPD 0x00040000 /* I am kswapd */ +#define PF_MEMALLOC_NOIO 0x00080000 /* Allocating memory without IO involved */ +#define PF_LESS_THROTTLE 0x00100000 /* Throttle me less: I clean memory */ +#define PF_KTHREAD 0x00200000 /* I am a kernel thread */ +#define PF_RANDOMIZE 0x00400000 /* randomize virtual address space */ +#define PF_SWAPWRITE 0x00800000 /* Allowed to write to swap */ +#define PF_NO_SETAFFINITY 0x04000000 /* Userland is not allowed to meddle with cpus_allowed */ +#define PF_MCE_EARLY 0x08000000 /* Early kill for mce process policy */ +#define PF_MUTEX_TESTER 0x20000000 /* Thread belongs to the rt mutex tester */ +#define PF_FREEZER_SKIP 0x40000000 /* Freezer should not count it as freezable */ +#define PF_SUSPEND_TASK 0x80000000 /* this thread called freeze_processes and should not be frozen */ +``` + +#[表示进程亲属关系的成员](http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1499) +------- +```c +/* + * pointers to (original) parent process, youngest child, younger sibling, + * older sibling, respectively. (p->father can be replaced with + * p->real_parent->pid) + */ +struct task_struct __rcu *real_parent; /* real parent process */ +struct task_struct __rcu *parent; /* recipient of SIGCHLD, wait4() reports */ +/* + * children/sibling forms the list of my natural children + */ +struct list_head children; /* list of my children */ +struct list_head sibling; /* linkage in my parent's children list */ +struct task_struct *group_leader; /* threadgroup leader */ +``` + + 在Linux系统中,所有进程之间都有着直接或间接地联系,每个进程都有其父进程,也可能有零个或多个子进程。拥有同一父进程的所有进程具有兄弟关系。 + + +| 字段 | 描述 | +| ------------- |:-------------:| +| real_parent | 指向其父进程,如果创建它的父进程不再存在,则指向PID为1的init进程 | +| parent | 指向其父进程,当它终止时,必须向它的父进程发送信号。它的值通常与real_parent相同 | +| children | 表示链表的头部,链表中的所有元素都是它的子进程 | +| sibling | 用于把当前进程插入到兄弟链表中 | +| group_leader | 指向其所在进程组的领头进程 | + +#[ptrace系统调用](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1394) +------- +Ptrace 提供了一种父进程可以控制子进程运行,并可以检查和改变它的核心image。 + +它主要用于实现断点调试。一个被跟踪的进程运行中,直到发生一个信号。则进程被中止,并且通知其父进程。在进程中止的状态下,进程的内存空间可以被读写。父进程还可以使子进程继续执行,并选择是否是否忽略引起中止的信号。 + + +```c +unsigned int ptrace; + ptraced is the list of tasks this task is using ptrace on. +* This includes both natural children and PTRACE_ATTACH targets. +* p->ptrace_entry is p's link on the p->parent->ptraced list. +*/ +struct list_head ptraced; +struct list_head ptrace_entry; + +unsigned long ptrace_message; +siginfo_t *last_siginfo; /* For ptrace use. */ +``` + +成员ptrace被设置为0时表示不需要被跟踪,它的可能取值如下: + +>参见 +> +>http://lxr.free-electrons.com/source/include/linux/ptrace.h?v=4.5#L20 + +```c +/* + * Ptrace flags + * + * The owner ship rules for task->ptrace which holds the ptrace + * flags is simple. When a task is running it owns it's task->ptrace + * flags. When the a task is stopped the ptracer owns task->ptrace. + */ + +#define PT_SEIZED 0x00010000 /* SEIZE used, enable new behavior */ +#define PT_PTRACED 0x00000001 +#define PT_DTRACE 0x00000002 /* delayed trace (used on m68k, i386) */ +#define PT_PTRACE_CAP 0x00000004 /* ptracer can follow suid-exec */ + +#define PT_OPT_FLAG_SHIFT 3 +/* PT_TRACE_* event enable flags */ +#define PT_EVENT_FLAG(event) (1 << (PT_OPT_FLAG_SHIFT + (event))) +#define PT_TRACESYSGOOD PT_EVENT_FLAG(0) +#define PT_TRACE_FORK PT_EVENT_FLAG(PTRACE_EVENT_FORK) +#define PT_TRACE_VFORK PT_EVENT_FLAG(PTRACE_EVENT_VFORK) +#define PT_TRACE_CLONE PT_EVENT_FLAG(PTRACE_EVENT_CLONE) +#define PT_TRACE_EXEC PT_EVENT_FLAG(PTRACE_EVENT_EXEC) +#define PT_TRACE_VFORK_DONE PT_EVENT_FLAG(PTRACE_EVENT_VFORK_DONE) +#define PT_TRACE_EXIT PT_EVENT_FLAG(PTRACE_EVENT_EXIT) +#define PT_TRACE_SECCOMP PT_EVENT_FLAG(PTRACE_EVENT_SECCOMP) + +#define PT_EXITKILL (PTRACE_O_EXITKILL << PT_OPT_FLAG_SHIFT) +#define PT_SUSPEND_SECCOMP (PTRACE_O_SUSPEND_SECCOMP << PT_OPT_FLAG_SHIFT) + +/* single stepping state bits (used on ARM and PA-RISC) */ +#define PT_SINGLESTEP_BIT 31 +#define PT_SINGLESTEP (1<参见 +> +>http://lxr.free-electrons.com/source/include/uapi/linux/sched.h?v=4.5#L36 + +```c +/* +* Scheduling policies +*/ +#define SCHED_NORMAL 0 +#define SCHED_FIFO 1 +#define SCHED_RR 2 +#define SCHED_BATCH 3 +/* SCHED_ISO: reserved but not implemented yet */ +#define SCHED_IDLE 5 +#define SCHED_DEADLINE 6 +``` + +| 字段 | 描述 | 所在调度器类 | +| ------------- |:-------------:|:-------------:| +| SCHED_NORMAL | (也叫SCHED_OTHER)用于普通进程,通过CFS调度器实现。SCHED_BATCH用于非交互的处理器消耗型进程。SCHED_IDLE是在系统负载很低时使用 | CFS | +| SCHED_BATCH | SCHED_NORMAL普通进程策略的分化版本。采用分时策略,根据动态优先级(可用nice()API设置),分配 CPU 运算资源。注意:这类进程比上述两类实时进程优先级低,换言之,在有实时进程存在时,实时进程优先调度。但针对吞吐量优化 | CFS | +| SCHED_IDLE | 优先级最低,在系统空闲时才跑这类进程(如利用闲散计算机资源跑地外文明搜索,蛋白质结构分析等任务,是此调度策略的适用者)| CFS | +| SCHED_FIFO | 先入先出调度算法(实时调度策略),相同优先级的任务先到先服务,高优先级的任务可以抢占低优先级的任务 | RT | +| SCHED_RR | 轮流调度算法(实时调度策略),后 者提供 Roound-Robin 语义,采用时间片,相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性,同样,高优先级的任务可以抢占低优先级的任务。不同要求的实时任务可以根据需要用sched_setscheduler()API 设置策略 | RT | +| SCHED_DEADLINE | 新支持的实时进程调度策略,针对突发型计算,且对延迟和完成时间高度敏感的任务适用。基于Earliest Deadline First (EDF) 调度算法| + +##调度类 +------- + +sched_class结构体表示调度类,目前内核中有实现以下四种: + +```c +extern const struct sched_class stop_sched_class; +extern const struct sched_class dl_sched_class; +extern const struct sched_class rt_sched_class; +extern const struct sched_class fair_sched_class; +extern const struct sched_class idle_sched_class; +``` + + +| 调度器类 | 描述 | +| ------------- |:-------------:| +| idle_sched_class | 每个cup的第一个pid=0线程:swapper,是一个静态线程。调度类属于:idel_sched_class,所以在ps里面是看不到的。一般运行在开机过程和cpu异常的时候做dump | +| stop_sched_class | 优先级最高的线程,会中断所有其他线程,且不会被其他任务打断。作用:1.发生在cpu_stop_cpu_callback 进行cpu之间任务migration;2.HOTPLUG_CPU的情况下关闭任务。| +| rt_sched_class | RT,作用:实时线程 | +| fair_sched_class | CFS(公平),作用:一般常规线程 | + +目前系統中,Scheduling Class的优先级顺序为StopTask > RealTime > Fair > IdleTask + +开发者可以根据己的设计需求,來把所属的Task配置到不同的Scheduling Class中. + +#[进程地址空间](http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1453) +------- + +```c +/* http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1453 */ +struct mm_struct *mm, *active_mm; +/* per-thread vma caching */ +u32 vmacache_seqnum; +struct vm_area_struct *vmacache[VMACACHE_SIZE]; +#if defined(SPLIT_RSS_COUNTING) +struct task_rss_stat rss_stat; +#endif + +/* http://lxr.free-electrons.com/source/include/linux/sched.h?V=4.5#L1484 */ +#ifdef CONFIG_COMPAT_BRK +unsigned brk_randomized:1; +#endif +``` + +| 字段 | 描述 | +| ------------- |:-------------:| +| mm | 进程所拥有的用户空间内存描述符,内核线程无的mm为NULL | +| active_mm | active_mm指向进程运行时所使用的内存描述符, 对于普通进程而言,这两个指针变量的值相同。但是内核线程kernel thread是没有进程地址空间的,所以内核线程的tsk->mm域是空(NULL)。但是内核必须知道用户空间包含了什么,因此它的active_mm成员被初始化为前一个运行进程的active_mm值。| +| brk_randomized| 用来确定对随机堆内存的探测。参见[LKML]( http://lkml.indiana.edu/hypermail/linux/kernel/1104.1/00196.html)上的介绍 | +| rss_stat | 用来记录缓冲信息 | +>因此如果当前内核线程被调度之前运行的也是另外一个内核线程时候,那么其mm和avtive_mm都是NULL + + +#判断标志 +------- +```c +int exit_code, exit_signal; +int pdeath_signal; /* The signal sent when the parent dies */ +unsigned long jobctl; /* JOBCTL_*, siglock protected */ + +/* Used for emulating ABI behavior of previous Linux versions */ +unsigned int personality; + +/* scheduler bits, serialized by scheduler locks */ +unsigned sched_reset_on_fork:1; +unsigned sched_contributes_to_load:1; +unsigned sched_migrated:1; +unsigned :0; /* force alignment to the next boundary */ + +/* unserialized, strictly 'current' */ +unsigned in_execve:1; /* bit to tell LSMs we're in execve */ +unsigned in_iowait:1; +``` +| 字段 | 描述 | +| ------------- |:-------------:| +| exit_code | 用于设置进程的终止代号,这个值要么是_exit()或exit_group()系统调用参数(正常终止),要么是由内核提供的一个错误代号(异常终止)。| +| exit_signal | 被置为-1时表示是某个线程组中的一员。只有当线程组的最后一个成员终止时,才会产生一个信号,以通知线程组的领头进程的父进程。| +| pdeath_signal | 用于判断父进程终止时发送信号。| +| personality | 用于处理不同的ABI,参见[Linux-Man](http://man7.org/linux/man-pages/man2/personality.2.html) | +| in_execve | 用于通知LSM是否被do_execve()函数所调用。详见补丁说明,参见[LKML](http://lkml.indiana.edu/hypermail/linux/kernel/0901.1/00014.html) | +| in_iowait | 用于判断是否进行iowait计数 | +| sched_reset_on_fork | 用于判断是否恢复默认的优先级或调度策略 | + + +#[时间](http://lxr.free-electrons.com/source/include/linux/sched.h?v=4.5#L1530) +------- + +```c +cputime_t utime, stime, utimescaled, stimescaled; +cputime_t gtime; +struct prev_cputime prev_cputime; +#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN +seqcount_t vtime_seqcount; +unsigned long long vtime_snap; +enum { + /* Task is sleeping or running in a CPU with VTIME inactive */ + VTIME_INACTIVE = 0, + /* Task runs in userspace in a CPU with VTIME active */ + VTIME_USER, + /* Task runs in kernelspace in a CPU with VTIME active */ + VTIME_SYS, +} vtime_snap_whence; +#endif +unsigned long nvcsw, nivcsw; /* context switch counts */ +u64 start_time; /* monotonic time in nsec */ +u64 real_start_time; /* boot based time in nsec */ +/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ +unsigned long min_flt, maj_flt; + +struct task_cputime cputime_expires; +struct list_head cpu_timers[3]; + +/* process credentials */ +const struct cred __rcu *real_cred; /* objective and real subjective task + * credentials (COW) */ +const struct cred __rcu *cred; /* effective (overridable) subjective task + * credentials (COW) */ +char comm[TASK_COMM_LEN]; /* executable name excluding path + - access with [gs]et_task_comm (which lock + it with task_lock()) + - initialized normally by setup_new_exec */ +/* file system info */ +struct nameidata *nameidata; +#ifdef CONFIG_SYSVIPC +/* ipc stuff */ +struct sysv_sem sysvsem; +struct sysv_shm sysvshm; +#endif +#ifdef CONFIG_DETECT_HUNG_TASK +/* hung task detection */ +unsigned long last_switch_count; +#endif +``` + +| 字段 | 描述 | +| ------------- |:-------------:| +| utime/stime | 用于记录进程在用户态/内核态下所经过的节拍数(定时器)| +| prev_utime/prev_stime | 先前的运行时间,请参考[LKML](http://lkml.indiana.edu/hypermail/linux/kernel/1003.3/02431.html)的补丁说明 | +| utimescaled/stimescaled | 用于记录进程在用户态/内核态的运行时间,但它们以处理器的频率为刻度 | +| gtime | 以节拍计数的虚拟机运行时间(guest time) | +| nvcsw/nivcsw | 是自愿(voluntary)/非自愿(involuntary)上下文切换计数 | +| last_switch_count | nvcsw和nivcsw的总和 | +| start_time/real_start_time | 进程创建时间,real_start_time还包含了进程睡眠时间,常用于/proc/pid/stat,补丁说明请参考[LKML](http://lkml.indiana.edu/hypermail/linux/kernel/0705.0/2094.html) | +| cputime_expires | 用来统计进程或进程组被跟踪的处理器时间,其中的三个成员对应着cpu_timers[3]的三个链表 | + +#[信号处理]() +------- + +```c +/* signal handlers */ +struct signal_struct *signal; +struct sighand_struct *sighand; +1583 +sigset_t blocked, real_blocked; +sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */ +struct sigpending pending; +1587 +unsigned long sas_ss_sp; +size_t sas_ss_size; +``` + + +| 字段 | 描述 | +| ------------- |:-------------:| +| signal | 指向进程的信号描述符 | +| sighand | 指向进程的信号处理程序描述符 | +| blocked | 表示被阻塞信号的掩码,real_blocked表示临时掩码 | +| pending | 存放私有挂起信号的数据结构 | +| sas_ss_sp | 是信号处理程序备用堆栈的地址,sas_ss_size表示堆栈的大小 | + + +#其他 +------- + (1)、用于保护资源分配或释放的自旋锁 + +``` +/* Protection of (de-)allocation: mm, files, fs, tty, keyrings, mems_allowed, + * mempolicy */ + spinlock_t alloc_lock; +``` + (2)、进程描述符使用计数,被置为2时,表示进程描述符正在被使用而且其相应的进程处于活动状态 + +```c +atomic_t usage; +``` + (3)、用于表示获取大内核锁的次数,如果进程未获得过锁,则置为-1。 + +```c +int lock_depth; /* BKL lock depth */ +``` + (4)、在SMP上帮助实现无加锁的进程切换(unlocked context switches) + +```c +#ifdef CONFIG_SMP +#ifdef __ARCH_WANT_UNLOCKED_CTXSW + int oncpu; +#endif +#endif +``` + (5)、preempt_notifier结构体链表 + +```c +#ifdef CONFIG_PREEMPT_NOTIFIERS + /* list of struct preempt_notifier: */ + struct hlist_head preempt_notifiers; +#endif +``` + (6)、FPU使用计数 + +```c +unsigned char fpu_counter; +``` + (7)、 blktrace是一个针对Linux内核中块设备I/O层的跟踪工具。 + +```c +#ifdef CONFIG_BLK_DEV_IO_TRACE + unsigned int btrace_seq; +#endif +``` + (8)、RCU同步原语 + +```c +#ifdef CONFIG_PREEMPT_RCU + int rcu_read_lock_nesting; + char rcu_read_unlock_special; + struct list_head rcu_node_entry; +#endif /* #ifdef CONFIG_PREEMPT_RCU */ +#ifdef CONFIG_TREE_PREEMPT_RCU + struct rcu_node *rcu_blocked_node; +#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */ +#ifdef CONFIG_RCU_BOOST + struct rt_mutex *rcu_boost_mutex; +#endif /* #ifdef CONFIG_RCU_BOOST */ +``` + (9)、用于调度器统计进程的运行信息 + +```c +#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT) + struct sched_info sched_info; +#endif +``` + (10)、用于构建进程链表 + +```c +struct list_head tasks; +``` + (11)、to limit pushing to one attempt + +```c +#ifdef CONFIG_SMP + struct plist_node pushable_tasks; +#endif +``` + 补丁说明请参考:http://lkml.indiana.edu/hypermail/linux/kernel/0808.3/0503.html + + (12)、防止内核堆栈溢出 + +```c +#ifdef CONFIG_CC_STACKPROTECTOR + /* Canary value for the -fstack-protector gcc feature */ + unsigned long stack_canary; +#endif +``` + 在GCC编译内核时,需要加上-fstack-protector选项。 + + (13)、PID散列表和链表 + +```c +/* PID/PID hash table linkage. */ +struct pid_link pids[PIDTYPE_MAX]; +struct list_head thread_group; //线程组中所有进程的链表 +``` + (14)、do_fork函数 + +```c +struct completion *vfork_done; /* for vfork() */ +int __user *set_child_tid; /* CLONE_CHILD_SETTID */ +int __user *clear_child_tid; /* CLONE_CHILD_CLEARTID */ +``` + 在执行do_fork()时,如果给定特别标志,则vfork_done会指向一个特殊地址。 + + 如果copy_process函数的clone_flags参数的值被置为CLONE_CHILD_SETTID或CLONE_CHILD_CLEARTID,则会把child_tidptr参数的值分别复制到set_child_tid和clear_child_tid成员。这些标志说明必须改变子进程用户态地址空间的child_tidptr所指向的变量的值。 + + (15)、缺页统计 + +```c +/* mm fault and swap info: this can arguably be seen as either mm-specific or thread-specific */ + unsigned long min_flt, maj_flt; +``` + (16)、进程权能 + +```c +const struct cred __rcu *real_cred; /* objective and real subjective task + * credentials (COW) */ +const struct cred __rcu *cred; /* effective (overridable) subjective task + * credentials (COW) */ +struct cred *replacement_session_keyring; /* for KEYCTL_SESSION_TO_PARENT */ +``` + (17)、相应的程序名 + +```c +char comm[TASK_COMM_LEN]; +``` + (18)、文件 + +```c +/* file system info */ + int link_count, total_link_count; +/* filesystem information */ + struct fs_struct *fs; +/* open file information */ + struct files_struct *files; +``` + fs用来表示进程与文件系统的联系,包括当前目录和根目录。 + + files表示进程当前打开的文件。 + + (19)、进程通信(SYSVIPC) + +```c +#ifdef CONFIG_SYSVIPC +/* ipc stuff */ + struct sysv_sem sysvsem; +#endif +``` + (20)、处理器特有数据 + +```c +/* CPU-specific state of this task */ + struct thread_struct thread; +``` + (21)、命名空间 + +```c +/* namespaces */ + struct nsproxy *nsproxy; +``` + (22)、进程审计 + +```c + struct audit_context *audit_context; +#ifdef CONFIG_AUDITSYSCALL + uid_t loginuid; + unsigned int sessionid; +#endif +``` + (23)、secure computing + +```c +seccomp_t seccomp; +``` + (24)、用于copy_process函数使用CLONE_PARENT 标记时 + +```c +/* Thread group tracking */ + u32 parent_exec_id; + u32 self_exec_id; +``` + (25)、中断 + +```c +#ifdef CONFIG_GENERIC_HARDIRQS + /* IRQ handler threads */ + struct irqaction *irqaction; +#endif +#ifdef CONFIG_TRACE_IRQFLAGS + unsigned int irq_events; + unsigned long hardirq_enable_ip; + unsigned long hardirq_disable_ip; + unsigned int hardirq_enable_event; + unsigned int hardirq_disable_event; + int hardirqs_enabled; + int hardirq_context; + unsigned long softirq_disable_ip; + unsigned long softirq_enable_ip; + unsigned int softirq_disable_event; + unsigned int softirq_enable_event; + int softirqs_enabled; + int softirq_context; +#endif +``` + (26)、task_rq_lock函数所使用的锁 + +```c +/* Protection of the PI data structures: */ +raw_spinlock_t pi_lock; +``` + (27)、基于PI协议的等待互斥锁,其中PI指的是priority inheritance(优先级继承) + +```c +#ifdef CONFIG_RT_MUTEXES + /* PI waiters blocked on a rt_mutex held by this task */ + struct plist_head pi_waiters; + /* Deadlock detection and priority inheritance handling */ + struct rt_mutex_waiter *pi_blocked_on; +#endif +``` + (28)、死锁检测 + +```c +#ifdef CONFIG_DEBUG_MUTEXES + /* mutex deadlock detection */ + struct mutex_waiter *blocked_on; +#endif +``` + (29)、lockdep,参见内核说明文档linux-2.6.38.8/Documentation/lockdep-design.txt + +```c +#ifdef CONFIG_LOCKDEP +# define MAX_LOCK_DEPTH 48UL + u64 curr_chain_key; + int lockdep_depth; + unsigned int lockdep_recursion; + struct held_lock held_locks[MAX_LOCK_DEPTH]; + gfp_t lockdep_reclaim_gfp; +#endif +``` + (30)、JFS文件系统 + +```c +/* journalling filesystem info */ + void *journal_info; +``` + (31)、块设备链表 + +```c +/* stacked block device info */ + struct bio_list *bio_list; +``` + (32)、内存回收 + +```c +struct reclaim_state *reclaim_state; +``` + (33)、存放块设备I/O数据流量信息 + +```c +struct backing_dev_info *backing_dev_info; +``` + + (34)、I/O调度器所使用的信息 + +```c +struct io_context *io_context; +``` + (35)、记录进程的I/O计数 + +```c +struct task_io_accounting ioac; +if defined(CONFIG_TASK_XACCT) +u64 acct_rss_mem1; /* accumulated rss usage */ +u64 acct_vm_mem1; /* accumulated virtual memory usage */ +cputime_t acct_timexpd; /* stime + utime since last update */ +endif +``` +在Ubuntu 11.04上,执行cat获得进程1的I/O计数如下: + +![获得进程1的I/O计数](./images/cat_proc_io.png) + +输出的数据项刚好是task_io_accounting结构体的所有成员。 + + (36)、CPUSET功能 + +```c +#ifdef CONFIG_CPUSETS + nodemask_t mems_allowed; /* Protected by alloc_lock */ + int mems_allowed_change_disable; + int cpuset_mem_spread_rotor; + int cpuset_slab_spread_rotor; +#endif +``` + (37)、Control Groups + +```c +#ifdef CONFIG_CGROUPS + /* Control Group info protected by css_set_lock */ + struct css_set __rcu *cgroups; + /* cg_list protected by css_set_lock and tsk->alloc_lock */ + struct list_head cg_list; +#endif +#ifdef CONFIG_CGROUP_MEM_RES_CTLR /* memcg uses this to do batch job */ + struct memcg_batch_info { + int do_batch; /* incremented when batch uncharge started */ + struct mem_cgroup *memcg; /* target memcg of uncharge */ + unsigned long bytes; /* uncharged usage */ + unsigned long memsw_bytes; /* uncharged mem+swap usage */ + } memcg_batch; +#endif +``` + (38)、futex同步机制 + +```c +#ifdef CONFIG_FUTEX + struct robust_list_head __user *robust_list; +#ifdef CONFIG_COMPAT + struct compat_robust_list_head __user *compat_robust_list; +#endif + struct list_head pi_state_list; + struct futex_pi_state *pi_state_cache; +#endif +``` + (39)、非一致内存访问(NUMA Non-Uniform Memory Access) + +```c +#ifdef CONFIG_NUMA + struct mempolicy *mempolicy; /* Protected by alloc_lock */ + short il_next; +#endif +``` + (40)、文件系统互斥资源 + +```c +atomic_t fs_excl; /* holding fs exclusive resources */ +``` + (41)、RCU链表 + +```c +struct rcu_head rcu; +``` + (42)、管道 + +```c +struct pipe_inode_info *splice_pipe; +``` + (43)、延迟计数 + +```c +#ifdef CONFIG_TASK_DELAY_ACCT + struct task_delay_info *delays; +#endif +``` + (44)、fault injection,参考内核说明文件linux-2.6.38.8/Documentation/fault-injection/fault-injection.txt + +```c +#ifdef CONFIG_FAULT_INJECTION + int make_it_fail; +#endif +``` + (45)、FLoating proportions + +```c +struct prop_local_single dirties; +``` + (46)、Infrastructure for displayinglatency + +```c +#ifdef CONFIG_LATENCYTOP + int latency_record_count; + struct latency_record latency_record[LT_SAVECOUNT]; +#endif +``` + (47)、time slack values,常用于poll和select函数 + +```c +unsigned long timer_slack_ns; +unsigned long default_timer_slack_ns; +``` + (48)、socket控制消息(control message) + +```c +struct list_head *scm_work_list; +``` + (49)、ftrace跟踪器 + +```c +#ifdef CONFIG_FUNCTION_GRAPH_TRACER + /* Index of current stored address in ret_stack */ + int curr_ret_stack; + /* Stack of return addresses for return function tracing */ + struct ftrace_ret_stack *ret_stack; + /* time stamp for last schedule */ + unsigned long long ftrace_timestamp; + /* + * Number of functions that haven't been traced + * because of depth overrun. + */ + atomic_t trace_overrun; + /* Pause for the tracing */ + atomic_t tracing_graph_pause; +#endif +#ifdef CONFIG_TRACING + /* state flags for use by tracers */ + unsigned long trace; + /* bitmask of trace recursion */ + unsigned long trace_recursion; +#endif /* CONFIG_TRACING */ +``` diff --git a/study/kernel/process/task/01-namespace/Makefile b/study/kernel/process/task/02-namespace/Makefile similarity index 100% rename from study/kernel/process/task/01-namespace/Makefile rename to study/kernel/process/task/02-namespace/Makefile diff --git a/study/kernel/process/task/01-namespace/README.md b/study/kernel/process/task/02-namespace/README.md similarity index 98% rename from study/kernel/process/task/01-namespace/README.md rename to study/kernel/process/task/02-namespace/README.md index cf10b2f..ec07e7d 100644 --- a/study/kernel/process/task/01-namespace/README.md +++ b/study/kernel/process/task/02-namespace/README.md @@ -1,171 +1,175 @@ -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------------- |:-------------:|:-------------:|:-------------:|:-------------:|:-------------:| -| 2016-05-12 | [Linux-4.5](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux-进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | - ->Linux Namespaces机制提供一种资源隔离方案。 - - -PID,IPC,Network等系统资源不再是全局性的,而是属于特定的Namespace。每个Namespace里面的资源对其他Namespace都是透明的。要创建新的Namespace,只需要在调用clone时指定相应的flag。Linux Namespaces机制为实现基于容器的虚拟化技术提供了很好的基础,LXC(Linux containers)就是利用这一特性实现了资源的隔离。不同Container内的进程属于不同的Namespace,彼此透明,互不干扰。下面我们就从clone系统调用的flag出发,来介绍各个Namespace。 -命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全局属性。该机制类似于Solaris中的zone或 FreeBSD中的jail。对该概念做一般概述之后,我将讨论命名空间框架所提供的基础设施。 - -#命名空间概念 -------- - -传统上,在Linux以及其他衍生的UNIX变体中,许多资源是全局管理的。 - -例如,系统中的所有进程按照惯例是通过PID标识的,这意味着内核必须管理一个**全局的PID列表**。而且,所有调用者通过uname系统调用返回的系统相关信息(包括系统名称和有关内核的一些信息)都是相同的。用户ID的管理方式类似,即各个用户是通过一个全局唯一的UID号标识。 - -全局ID使得内核可以有选择地允许或拒绝某些特权。虽然UID为0的root用户基本上允许做任何事,但其他用户ID则会受到限制。例如UID为n 的用户,不允许杀死属于用户m的进程(m≠ n)。但这不能防止用户看到彼此,即用户n可以看到另一个用户m也在计算机上活动。只要用户只能操纵他们自己的进程,这就没什么问题,因为没有理由不允许用户看到其他用户的进程。 - -但有些情况下,这种效果可能是不想要的。如果提供Web主机的供应商打算向用户提供Linux计算机的全部访问权限,包括root权限在内。传统上,这需要为每个用户准备一台计算机,代价太高。使用KVM或VMWare提供的虚拟化环境是一种解决问题的方法,但资源分配做得不是非常好。计算机的各个用户都需要一个独立的内核,以及一份完全安装好的配套的用户层应用。 - -**命名空间**提供了一种不同的解决方案,所需资源较少。在虚拟化的系统中,一台物理计算机可以运行多个内核,可能是并行的多个不同的操作系统。而命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器的成员与其他容器毫无关系。但也可以通过允许容器进行一定的共享,来降低容器之间的分隔。例如,容器可以设置为使用自身的PID集合,但仍然与其他容器共享部分文件系统。 - -本质上,命名空间建立了系统的不同视图。此前的每一项全局资源都必须包装到容器数据结构中,只有资源和包含资源的命名空间构成的二元组仍然是全局唯一的。虽然在给定容器内部资源是自足的,但无法提供在容器外部具有唯一性的ID。 - -考虑系统上有3个不同命名空间的情况。命名空间可以组织为层次,我会在这里讨论这种情况。一个命名空间是父命名空间,衍生了两个子命名空间。假定容器用于虚拟主机配置中,其中的每个容器必须看起来像是单独的一台Linux计算机。因此其中每一个都有自身的init进程,PID为0,其他进程的PID 以递增次序分配。两个子命名空间都有PID为0的init进程,以及PID分别为2和3的两个进程。由于相同的PID在系统中出现多次,PID号不是全局唯一的。 - - - -虽然子容器不了解系统中的其他容器,但父容器知道子命名空间的存在,也可以看到其中执行的所有进程。图中子容器的进程映射到父容器中,PID为4到 9。尽管系统上有9个进程,但却需要15个PID来表示,因为一个进程可以关联到多个PID。至于哪个PID是"正确"的,则依赖于具体的上下文。 - -如果命名空间包含的是比较简单的量,也可以是非层次的,例如下文讨论的UTS命名空间。在这种情况下,父子命名空间之间没有联系。 -请注意,Linux系统对简单形式的命名空间的支持已经有很长一段时间了,主要是chroot系统调用。该方法可以将进程限制到文件系统的某一部分,因而是一种简单的命名空间机制。但真正的命名空间能够控制的功能远远超过文件系统视图。 - - -#Linux内核命名空间描述 -------- - -在Linux内核中提供了多个namespace,其中包括fs (mount), uts, network, sysvipc, 等。一个进程可以属于多个namesapce,既然namespace和进程相关,那么在task_struct结构体中就会包含和namespace相关联的变量。在task_struct 结构中有一个指向namespace结构体的指针nsproxy。 -```c -struct task_struct -{ -…….. -/* namespaces */ - struct nsproxy *nsproxy; -……. -} -``` - - -再看一下[nsproxy](http://lxr.free-electrons.com/source/include/linux/nsproxy.h#L29)是如何定义的,在[include/linux/nsproxy.h](http://lxr.free-electrons.com/source/include/linux/nsproxy.h)文件中,这里一共定义了5个各自的命名空间结构体,在该结构体中定义了5个指向各个类型namespace的指针,由于多个进程可以使用同一个namespace,所以nsproxy可以共享使用,count字段是该结构的引用计数。 - -```c -/* 'count' is the number of tasks holding a reference. - * The count for each namespace, then, will be the number - * of nsproxies pointing to it, not the number of tasks. - * The nsproxy is shared by tasks which share all namespaces. - * As soon as a single namespace is cloned or unshared, the - * nsproxy is copied -*/ -struct nsproxy -{ - atomic_t count; - struct uts_namespace *uts_ns; - struct ipc_namespace *ipc_ns; - struct mnt_namespace *mnt_ns; - struct pid_namespace *pid_ns_for_children; - struct net *net_ns; -}; -``` -1. UTS命名空间包含了运行内核的名称、版本、底层体系结构类型等信息。UTS是UNIX Timesharing System的简称。 - -2. 保存在struct ipc_namespace中的所有与进程间通信(IPC)有关的信息。 - -3. 已经装载的文件系统的视图,在struct mnt_namespace中给出。 - -4. 有关进程ID的信息,由struct pid_namespace提供。 - -5. struct net_ns包含所有网络相关的命名空间参数。 - -系统中有一个默认的`nsproxy`,[init_nsproxy](http://lxr.free-electrons.com/source/include/linux/init_task.h#L232),该结构在task初始化是也会被初始,定义在[include/linux/init_task.h](http://lxr.free-electrons.com/source/include/linux/init_task.h#L190) - -```c -#define INIT_TASK(tsk) \ -{ -…….. - .nsproxy = &init_nsproxy, -…….. -} -``` - -其中[init_nsproxy](http://lxr.free-electrons.com/source/kernel/nsproxy.c#L31)的定义为: - -``` -struct nsproxy init_nsproxy = { - .count = ATOMIC_INIT(1), - .uts_ns = &init_uts_ns, -#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC) - .ipc_ns = &init_ipc_ns, -#endif - .mnt_ns = NULL, - .pid_ns_for_children = &init_pid_ns, -#ifdef CONFIG_NET - .net_ns = &init_net, -#endif -}; -``` -对于.mnt_ns没有进行初始化,其余的namespace都进行了系统默认初始 - - -#命名空间的创建 -------- - -新的命名空间可以用下面两种方法创建。 - -1. 在用fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。 - -2. unshare系统调用将进程的某些部分从父进程分离,其中也包括命名空间。更多信息请参见手册页unshare(2)。 - -在进程已经使用上述的两种机制之一从父进程命名空间分离后,从该进程的角度来看,改变全局属性不会传播到父进程命名空间,而父进程的修改也不会传播到子进 程,至少对于简单的量是这样。而对于文件系统来说,情况就比较复杂,其中的共享机制非常强大,带来了大量的可能性。 - -命名空间的实现需要两个部分:每个子系统的命名空间结构,将此前所有的全局组件包装到命名空间中;将给定进程关联到所属各个命名空间的机制。 - -在用fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。这些选项如下 - -* CLONE_NEWPID 进程命名空间。空间内的PID 是独立分配的,意思就是命名空间内的虚拟 PID 可能会与命名空间外的 PID 相冲突,于是命名空间内的 PID 映射到命名空间外时会使用另外一个 PID。比如说,命名空间内第一个 PID 为1,而在命名空间外就是该 PID 已被 init 进程所使用。 - -* CLONE_NEWIPC 进程间通信(IPC)的命名空间,可以将 SystemV 的 IPC 和 POSIX 的消息队列独立出来。 - -* CLONE_NEWNET 网络命名空间,用于隔离网络资源(/proc/net、IP 地址、网卡、路由等)。后台进程可以运行在不同命名空间内的相同端口上,用户还可以虚拟出一块网卡。 - -* CLONE_NEWNS 挂载命名空间,进程运行时可以将挂载点与系统分离,使用这个功能时,我们可以达到 chroot 的功能,而在安全性方面比 chroot 更高。 - -* CLONE_NEWUTS UTS 命名空间,主要目的是独立出主机名和网络信息服务(NIS)。 - -* CLONE_NEWUSER 用户命名空间,同进程 ID 一样,用户 ID 和组 ID 在命名空间内外是不一样的,并且在不同命名空间内可以存在相同的 ID。 - - -##PID Namespace -------- - -当调用clone时,设定了CLONE_NEWPID,就会创建一个新的PID Namespace,clone出来的新进程将成为Namespace里的第一个进程。一个PID Namespace为进程提供了一个独立的PID环境,PID Namespace内的PID将从1开始,在Namespace内调用fork,vfork或clone都将产生一个在该Namespace内独立的PID。新创建的Namespace里的第一个进程在该Namespace内的PID将为1,就像一个独立的系统里的init进程一样。该Namespace内的孤儿进程都将以该进程为父进程,当该进程被结束时,该Namespace内所有的进程都会被结束。PID Namespace是层次性,新创建的Namespace将会是创建该Namespace的进程属于的Namespace的子Namespace。子Namespace中的进程对于父Namespace是可见的,一个进程将拥有不止一个PID,而是在所在的Namespace以及所有直系祖先Namespace中都将有一个PID。系统启动时,内核将创建一个默认的PID Namespace,该Namespace是所有以后创建的Namespace的祖先,因此系统所有的进程在该Namespace都是可见的。 - -##IPC Namespace -------- - -当调用clone时,设定了CLONE_NEWIPC,就会创建一个新的IPC Namespace,clone出来的进程将成为Namespace里的第一个进程。一个IPC Namespace有一组System V IPC objects 标识符构成,这标识符有IPC相关的系统调用创建。在一个IPC Namespace里面创建的IPC object对该Namespace内的所有进程可见,但是对其他Namespace不可见,这样就使得不同Namespace之间的进程不能直接通信,就像是在不同的系统里一样。当一个IPC Namespace被销毁,该Namespace内的所有IPC object会被内核自动销毁。 -PID Namespace和IPC Namespace可以组合起来一起使用,只需在调用clone时,同时指定CLONE_NEWPID和CLONE_NEWIPC,这样新创建的Namespace既是一个独立的PID空间又是一个独立的IPC空间。不同Namespace的进程彼此不可见,也不能互相通信,这样就实现了进程间的隔离。 - -##mount Namespace -------- - -当调用clone时,设定了CLONE_NEWNS,就会创建一个新的mount Namespace。每个进程都存在于一个mount Namespace里面,mount Namespace为进程提供了一个文件层次视图。如果不设定这个flag,子进程和父进程将共享一个mount Namespace,其后子进程调用mount或umount将会影响到所有该Namespace内的进程。如果子进程在一个独立的mount Namespace里面,就可以调用mount或umount建立一份新的文件层次视图。该flag配合pivot_root系统调用,可以为进程创建一个独立的目录空间。 - -##Network Namespace -------- - -当调用clone时,设定了CLONE_NEWNET,就会创建一个新的Network Namespace。一个Network Namespace为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口,IPv4和IPv6协议栈,IP路由表,防火墙规则,sockets等等。一个Network Namespace提供了一份独立的网络环境,就跟一个独立的系统一样。一个物理设备只能存在于一个Network Namespace中,可以从一个Namespace移动另一个Namespace中。虚拟网络设备(virtual network device)提供了一种类似管道的抽象,可以在不同的Namespace之间建立隧道。利用虚拟化网络设备,可以建立到其他Namespace中的物理设备的桥接。当一个Network Namespace被销毁时,物理设备会被自动移回init Network Namespace,即系统最开始的Namespace。 - -##UTS Namespace -------- - -当调用clone时,设定了CLONE_NEWUTS,就会创建一个新的UTS Namespace。一个UTS Namespace就是一组被uname返回的标识符。新的UTS Namespace中的标识符通过复制调用进程所属的Namespace的标识符来初始化。Clone出来的进程可以通过相关系统调用改变这些标识符,比如调用sethostname来改变该Namespace的hostname。这一改变对该Namespace内的所有进程可见。CLONE_NEWUTS和CLONE_NEWNET一起使用,可以虚拟出一个有独立主机名和网络空间的环境,就跟网络上一台独立的主机一样。 -以上所有clone flag都可以一起使用,为进程提供了一个独立的运行环境。LXC正是通过clone时设定这些flag,为进程创建一个有独立PID,IPC,FS,Network,UTS空间的container。一个container就是一个虚拟的运行环境,对container里的进程是透明的,它会以为自己是直接在一个系统上运行的。一个container就像传统虚拟化技术里面的一台安装了OS的虚拟机,但是开销更小,部署更为便捷。 -Linux Namespaces机制本身就是为了实现 container based virtualizaiton开发的。它提供了一套轻量级、高效率的系统资源隔离方案,远比传统的虚拟化技术开销小,不过它也不是完美的,它为内核的开发带来了更多的复杂性,它在隔离性和容错性上跟传统的虚拟化技术比也还有差距。 - -##[user_namespace](http://lxr.free-electrons.com/source/include/linux/user_namespace.h#L25) -------- - -CLONE_NEWUSER指定子进程拥有新的用户空间 - +Linux的命名空间详解--Linux进程的管理与调度(二) +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------------- |:-------------:|:-------------:|:-------------:|:-------------:|:-------------:| +| 2016-05-12 | [Linux-4.5](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux-进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | + +>Linux Namespaces机制提供一种资源隔离方案。 + + +PID,IPC,Network等系统资源不再是全局性的,而是属于特定的Namespace。每个Namespace里面的资源对其他Namespace都是透明的。要创建新的Namespace,只需要在调用clone时指定相应的flag。Linux Namespaces机制为实现基于容器的虚拟化技术提供了很好的基础,LXC(Linux containers)就是利用这一特性实现了资源的隔离。不同Container内的进程属于不同的Namespace,彼此透明,互不干扰。下面我们就从clone系统调用的flag出发,来介绍各个Namespace。 +命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全局属性。该机制类似于Solaris中的zone或 FreeBSD中的jail。对该概念做一般概述之后,我将讨论命名空间框架所提供的基础设施。 + +#命名空间概念 +------- + +传统上,在Linux以及其他衍生的UNIX变体中,许多资源是全局管理的。 + +例如,系统中的所有进程按照惯例是通过PID标识的,这意味着内核必须管理一个**全局的PID列表**。而且,所有调用者通过uname系统调用返回的系统相关信息(包括系统名称和有关内核的一些信息)都是相同的。用户ID的管理方式类似,即各个用户是通过一个全局唯一的UID号标识。 + +全局ID使得内核可以有选择地允许或拒绝某些特权。虽然UID为0的root用户基本上允许做任何事,但其他用户ID则会受到限制。例如UID为n 的用户,不允许杀死属于用户m的进程(m≠ n)。但这不能防止用户看到彼此,即用户n可以看到另一个用户m也在计算机上活动。只要用户只能操纵他们自己的进程,这就没什么问题,因为没有理由不允许用户看到其他用户的进程。 + +但有些情况下,这种效果可能是不想要的。如果提供Web主机的供应商打算向用户提供Linux计算机的全部访问权限,包括root权限在内。传统上,这需要为每个用户准备一台计算机,代价太高。使用KVM或VMWare提供的虚拟化环境是一种解决问题的方法,但资源分配做得不是非常好。计算机的各个用户都需要一个独立的内核,以及一份完全安装好的配套的用户层应用。 + +**命名空间**提供了一种不同的解决方案,所需资源较少。在虚拟化的系统中,一台物理计算机可以运行多个内核,可能是并行的多个不同的操作系统。而命名空间则只使用一个内核在一台物理计算机上运作,前述的所有全局资源都通过命名空间抽象起来。这使得可以将一组进程放置到容器中,各个容器彼此隔离。隔离可以使容器的成员与其他容器毫无关系。但也可以通过允许容器进行一定的共享,来降低容器之间的分隔。例如,容器可以设置为使用自身的PID集合,但仍然与其他容器共享部分文件系统。 + +本质上,命名空间建立了系统的不同视图。此前的每一项全局资源都必须包装到容器数据结构中,只有资源和包含资源的命名空间构成的二元组仍然是全局唯一的。虽然在给定容器内部资源是自足的,但无法提供在容器外部具有唯一性的ID。 + +考虑系统上有3个不同命名空间的情况。命名空间可以组织为层次,我会在这里讨论这种情况。一个命名空间是父命名空间,衍生了两个子命名空间。假定容器用于虚拟主机配置中,其中的每个容器必须看起来像是单独的一台Linux计算机。因此其中每一个都有自身的init进程,PID为0,其他进程的PID 以递增次序分配。两个子命名空间都有PID为0的init进程,以及PID分别为2和3的两个进程。由于相同的PID在系统中出现多次,PID号不是全局唯一的。 + + + +虽然子容器不了解系统中的其他容器,但父容器知道子命名空间的存在,也可以看到其中执行的所有进程。图中子容器的进程映射到父容器中,PID为4到 9。尽管系统上有9个进程,但却需要15个PID来表示,因为一个进程可以关联到多个PID。至于哪个PID是"正确"的,则依赖于具体的上下文。 + +如果命名空间包含的是比较简单的量,也可以是非层次的,例如下文讨论的UTS命名空间。在这种情况下,父子命名空间之间没有联系。 +请注意,Linux系统对简单形式的命名空间的支持已经有很长一段时间了,主要是chroot系统调用。该方法可以将进程限制到文件系统的某一部分,因而是一种简单的命名空间机制。但真正的命名空间能够控制的功能远远超过文件系统视图。 + + +#Linux内核命名空间描述 +------- + +在Linux内核中提供了多个namespace,其中包括fs (mount), uts, network, sysvipc, 等。一个进程可以属于多个namesapce,既然namespace和进程相关,那么在task_struct结构体中就会包含和namespace相关联的变量。在task_struct 结构中有一个指向namespace结构体的指针nsproxy。 +```c +struct task_struct +{ +…….. +/* namespaces */ + struct nsproxy *nsproxy; +……. +} +``` + + +再看一下[nsproxy](http://lxr.free-electrons.com/source/include/linux/nsproxy.h#L29)是如何定义的,在[include/linux/nsproxy.h](http://lxr.free-electrons.com/source/include/linux/nsproxy.h)文件中,这里一共定义了5个各自的命名空间结构体,在该结构体中定义了5个指向各个类型namespace的指针,由于多个进程可以使用同一个namespace,所以nsproxy可以共享使用,count字段是该结构的引用计数。 + +```c +/* 'count' is the number of tasks holding a reference. + * The count for each namespace, then, will be the number + * of nsproxies pointing to it, not the number of tasks. + * The nsproxy is shared by tasks which share all namespaces. + * As soon as a single namespace is cloned or unshared, the + * nsproxy is copied +*/ +struct nsproxy +{ + atomic_t count; + struct uts_namespace *uts_ns; + struct ipc_namespace *ipc_ns; + struct mnt_namespace *mnt_ns; + struct pid_namespace *pid_ns_for_children; + struct net *net_ns; +}; +``` +1. UTS命名空间包含了运行内核的名称、版本、底层体系结构类型等信息。UTS是UNIX Timesharing System的简称。 + +2. 保存在struct ipc_namespace中的所有与进程间通信(IPC)有关的信息。 + +3. 已经装载的文件系统的视图,在struct mnt_namespace中给出。 + +4. 有关进程ID的信息,由struct pid_namespace提供。 + +5. struct net_ns包含所有网络相关的命名空间参数。 + +系统中有一个默认的`nsproxy`,[init_nsproxy](http://lxr.free-electrons.com/source/include/linux/init_task.h#L232),该结构在task初始化是也会被初始,定义在[include/linux/init_task.h](http://lxr.free-electrons.com/source/include/linux/init_task.h#L190) + +```c +#define INIT_TASK(tsk) \ +{ +…….. + .nsproxy = &init_nsproxy, +…….. +} +``` + +其中[init_nsproxy](http://lxr.free-electrons.com/source/kernel/nsproxy.c#L31)的定义为: + +``` +struct nsproxy init_nsproxy = { + .count = ATOMIC_INIT(1), + .uts_ns = &init_uts_ns, +#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC) + .ipc_ns = &init_ipc_ns, +#endif + .mnt_ns = NULL, + .pid_ns_for_children = &init_pid_ns, +#ifdef CONFIG_NET + .net_ns = &init_net, +#endif +}; +``` +对于.mnt_ns没有进行初始化,其余的namespace都进行了系统默认初始 + + +#命名空间的创建 +------- + +新的命名空间可以用下面两种方法创建。 + +1. 在用fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。 + +2. unshare系统调用将进程的某些部分从父进程分离,其中也包括命名空间。更多信息请参见手册页unshare(2)。 + +在进程已经使用上述的两种机制之一从父进程命名空间分离后,从该进程的角度来看,改变全局属性不会传播到父进程命名空间,而父进程的修改也不会传播到子进 程,至少对于简单的量是这样。而对于文件系统来说,情况就比较复杂,其中的共享机制非常强大,带来了大量的可能性。 + +命名空间的实现需要两个部分:每个子系统的命名空间结构,将此前所有的全局组件包装到命名空间中;将给定进程关联到所属各个命名空间的机制。 + +在用fork或clone系统调用创建新进程时,有特定的选项可以控制是与父进程共享命名空间,还是建立新的命名空间。这些选项如下 + +* CLONE_NEWPID 进程命名空间。空间内的PID 是独立分配的,意思就是命名空间内的虚拟 PID 可能会与命名空间外的 PID 相冲突,于是命名空间内的 PID 映射到命名空间外时会使用另外一个 PID。比如说,命名空间内第一个 PID 为1,而在命名空间外就是该 PID 已被 init 进程所使用。 + +* CLONE_NEWIPC 进程间通信(IPC)的命名空间,可以将 SystemV 的 IPC 和 POSIX 的消息队列独立出来。 + +* CLONE_NEWNET 网络命名空间,用于隔离网络资源(/proc/net、IP 地址、网卡、路由等)。后台进程可以运行在不同命名空间内的相同端口上,用户还可以虚拟出一块网卡。 + +* CLONE_NEWNS 挂载命名空间,进程运行时可以将挂载点与系统分离,使用这个功能时,我们可以达到 chroot 的功能,而在安全性方面比 chroot 更高。 + +* CLONE_NEWUTS UTS 命名空间,主要目的是独立出主机名和网络信息服务(NIS)。 + +* CLONE_NEWUSER 用户命名空间,同进程 ID 一样,用户 ID 和组 ID 在命名空间内外是不一样的,并且在不同命名空间内可以存在相同的 ID。 + + +##PID Namespace +------- + +当调用clone时,设定了CLONE_NEWPID,就会创建一个新的PID Namespace,clone出来的新进程将成为Namespace里的第一个进程。一个PID Namespace为进程提供了一个独立的PID环境,PID Namespace内的PID将从1开始,在Namespace内调用fork,vfork或clone都将产生一个在该Namespace内独立的PID。新创建的Namespace里的第一个进程在该Namespace内的PID将为1,就像一个独立的系统里的init进程一样。该Namespace内的孤儿进程都将以该进程为父进程,当该进程被结束时,该Namespace内所有的进程都会被结束。PID Namespace是层次性,新创建的Namespace将会是创建该Namespace的进程属于的Namespace的子Namespace。子Namespace中的进程对于父Namespace是可见的,一个进程将拥有不止一个PID,而是在所在的Namespace以及所有直系祖先Namespace中都将有一个PID。系统启动时,内核将创建一个默认的PID Namespace,该Namespace是所有以后创建的Namespace的祖先,因此系统所有的进程在该Namespace都是可见的。 + +##IPC Namespace +------- + +当调用clone时,设定了CLONE_NEWIPC,就会创建一个新的IPC Namespace,clone出来的进程将成为Namespace里的第一个进程。一个IPC Namespace有一组System V IPC objects 标识符构成,这标识符有IPC相关的系统调用创建。在一个IPC Namespace里面创建的IPC object对该Namespace内的所有进程可见,但是对其他Namespace不可见,这样就使得不同Namespace之间的进程不能直接通信,就像是在不同的系统里一样。当一个IPC Namespace被销毁,该Namespace内的所有IPC object会被内核自动销毁。 +PID Namespace和IPC Namespace可以组合起来一起使用,只需在调用clone时,同时指定CLONE_NEWPID和CLONE_NEWIPC,这样新创建的Namespace既是一个独立的PID空间又是一个独立的IPC空间。不同Namespace的进程彼此不可见,也不能互相通信,这样就实现了进程间的隔离。 + +##mount Namespace +------- + +当调用clone时,设定了CLONE_NEWNS,就会创建一个新的mount Namespace。每个进程都存在于一个mount Namespace里面,mount Namespace为进程提供了一个文件层次视图。如果不设定这个flag,子进程和父进程将共享一个mount Namespace,其后子进程调用mount或umount将会影响到所有该Namespace内的进程。如果子进程在一个独立的mount Namespace里面,就可以调用mount或umount建立一份新的文件层次视图。该flag配合pivot_root系统调用,可以为进程创建一个独立的目录空间。 + +##Network Namespace +------- + +当调用clone时,设定了CLONE_NEWNET,就会创建一个新的Network Namespace。一个Network Namespace为进程提供了一个完全独立的网络协议栈的视图。包括网络设备接口,IPv4和IPv6协议栈,IP路由表,防火墙规则,sockets等等。一个Network Namespace提供了一份独立的网络环境,就跟一个独立的系统一样。一个物理设备只能存在于一个Network Namespace中,可以从一个Namespace移动另一个Namespace中。虚拟网络设备(virtual network device)提供了一种类似管道的抽象,可以在不同的Namespace之间建立隧道。利用虚拟化网络设备,可以建立到其他Namespace中的物理设备的桥接。当一个Network Namespace被销毁时,物理设备会被自动移回init Network Namespace,即系统最开始的Namespace。 + +##UTS Namespace +------- + +当调用clone时,设定了CLONE_NEWUTS,就会创建一个新的UTS Namespace。一个UTS Namespace就是一组被uname返回的标识符。新的UTS Namespace中的标识符通过复制调用进程所属的Namespace的标识符来初始化。Clone出来的进程可以通过相关系统调用改变这些标识符,比如调用sethostname来改变该Namespace的hostname。这一改变对该Namespace内的所有进程可见。CLONE_NEWUTS和CLONE_NEWNET一起使用,可以虚拟出一个有独立主机名和网络空间的环境,就跟网络上一台独立的主机一样。 +以上所有clone flag都可以一起使用,为进程提供了一个独立的运行环境。LXC正是通过clone时设定这些flag,为进程创建一个有独立PID,IPC,FS,Network,UTS空间的container。一个container就是一个虚拟的运行环境,对container里的进程是透明的,它会以为自己是直接在一个系统上运行的。一个container就像传统虚拟化技术里面的一台安装了OS的虚拟机,但是开销更小,部署更为便捷。 +Linux Namespaces机制本身就是为了实现 container based virtualizaiton开发的。它提供了一套轻量级、高效率的系统资源隔离方案,远比传统的虚拟化技术开销小,不过它也不是完美的,它为内核的开发带来了更多的复杂性,它在隔离性和容错性上跟传统的虚拟化技术比也还有差距。 + +##[user_namespace](http://lxr.free-electrons.com/source/include/linux/user_namespace.h#L25) +------- + +CLONE_NEWUSER指定子进程拥有新的用户空间 + diff --git a/study/kernel/process/task/01-namespace/uts_name.c b/study/kernel/process/task/02-namespace/uts_name.c similarity index 100% rename from study/kernel/process/task/01-namespace/uts_name.c rename to study/kernel/process/task/02-namespace/uts_name.c diff --git a/study/kernel/process/task/02-pid/README.md b/study/kernel/process/task/03-pid/README.md similarity index 97% rename from study/kernel/process/task/02-pid/README.md rename to study/kernel/process/task/03-pid/README.md index 9f64eec..ebdf332 100644 --- a/study/kernel/process/task/02-pid/README.md +++ b/study/kernel/process/task/03-pid/README.md @@ -1,720 +1,724 @@ -| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | -| ------------- |:-------------:|:-------------:|:-------------:|:-------------:|:-------------:| -| 2016-05-12 | [Linux-4.5](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux-进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | - - -Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一。 - -该数据结构在内核文件[include/linux/sched.h](http://lxr.free-electrons.com/source/include/linux/sched.h#L1389)中定义,在目前最新的Linux-4.5(截至目前的日期为2016-05-11)的内核中,该数据结构足足有 380 行之多,在这里我不可能逐项去描述其表示的含义,本篇文章只关注该数据结构如何来组织和管理进程ID的。 - -#进程ID概述 -------- - -##进程ID类型 -------- - -要想了解内核如何来组织和管理进程ID,先要知道进程ID的类型: - -内核中进程ID的类型用[pid_type](http://lxr.free-electrons.com/source/include/linux/pid.h#L6)来描述,它被定义在[include/linux/pid.h](http://lxr.free-electrons.com/source/include/linux/pid.h)中 - -```c -enum pid_type -{ - PIDTYPE_PID, - PIDTYPE_PGID, - PIDTYPE_SID, - PIDTYPE_MAX -}; -``` - -* **PID** 内核唯一区分每个进程的标识 - - -pid是 Linux 中在其命名空间中唯一标识进程而分配给它的一个号码,称做进程ID号,简称PID。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值 - - -这个pid用于内核唯一的区分每个进程 - ->注意它并不是我们用户空间通过getpid( )所获取到的那个进程号,至于原因么,接着往下看 - -* **TGID** 线程组(轻量级进程组)的ID标识 - -在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程(即轻量级进程,Linux其实没有严格的进程概念),它们处于一个线程组, - - -该线程组的所有线程的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID,但是由于他们是不同的进程,因此其pid各不相同;线程组组长(也叫主线程)的TGID与其PID相同;一个进程没有使用线程,则其TGID与PID也相同。 - - -* **PGID** - -另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作 - -例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。 - -* **SID** - -几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID,保存在task_struct的session成员中 - - -##PID命名空间 -------- - -###pid命名空间概述 -------- - -命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为mount命名空间、UTS命名空间、IPC命名空间、用户命名空间、PID命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。 - ->关于命名空间的详细信息,请参见 - -命名空间有的还有层次关系,如PID命名空间 - -![命名空间的层次关系图](./images/namespace-level.jpg) - - - - ->在上图有四个命名空间,一个父命名空间衍生了两个子命名空间,其中的一个子命名空间又衍生了一个子命名空间。以PID命名空间为例,由于各个命名空间彼此隔离,所以每个命名空间都可以有 PID 号为 1 的进程;但又由于命名空间的层次性,父命名空间是知道子命名空间的存在,因此子命名空间要映射到父命名空间中去,因此上图中 level 1 中两个子命名空间的六个进程分别映射到其父命名空间的PID 号5~10。 - -###局部ID和全局ID -------- - - -命名空间增加了PID管理的复杂性。 - - -回想一下,PID命名空间按层次组织。在建立一个新的命名空间时,该命名空间中的所有PID对父命名空间都是可见的,但子命名空间无法看到父命名空间的PID。但这意味着某些进程具有多个PID,凡可以看到该进程的命名空间,都会为其分配一个PID。 这必须反映在数据结构中。我们必须区分**局部ID**和**全局ID** - - - -全局PID和TGID直接保存在[`task_struct`](http://lxr.free-electrons.com/source/include/linux/sched.h#L1389)中,分别是task_struct的`pid`和`tgid`成员: - - -* **全局ID** 在内核本身和初始命名空间中唯一的ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个PID,叫全局ID,保证在整个系统中唯一。 - -* **局部ID** 对于属于某个特定的命名空间,它在其命名空间内分配的ID为局部ID,该ID也可以出现在其他的命名空间中。 - -```c - -struct task_struct -{ - //... - pid_t pid; - pid_t tgid; - //... -} -``` - -两项都是pid_t类型,该类型定义为__kernel_pid_t,后者由各个体系结构分别定义。通常定义为int,即可以同时使用232个不同的ID。 - -会话session和进程group组ID不是直接包含在task_struct本身中,但保存在用于信号处理的结构中。 - - - ->task_ struct->signal->__session表示全局SID, -> ->而全局PGID则保存在task_struct->signal->__pgrp。 -> ->辅助函数set_task_session和set_task_pgrp可用于修改这些值。 - - -除了这两个字段之外,内核还需要找一个办法来管理所有命名空间内部的局部量,以及其他ID(如TID和SID)。这需要几个相互连接的数据结构,以及许多辅助函数,并将在下文讨论。 - -下文我将使用ID指代提到的任何进程ID。在必要的情况下,我会明确地说明ID类型(例如,TGID,即线程组ID)。 - -一个小型的子系统称之为PID分配器(pid allocator)用于加速新ID的分配。此外,内核需要提供辅助函数,以实现通过ID及其类型查找进程的task_struct的功能,以及将ID的内核表示形式和用户空间可见的数值进行转换的功能。 - - - -###PID命名空间数据结构pid_namespace - -------- - - - -在介绍表示ID本身所需的数据结构之前,我需要讨论PID命名空间的表示方式。我们所需查看的代码如下所示: - -[pid_namespace](http://lxr.free-electrons.com/source/include/linux/pid_namespace.h#L24)的定义在[include/linux/pid_namespace.h](http://lxr.free-electrons.com/source/include/linux/pid_namespace.h#L24)中 - -命名空间的结构如下 - -```c -struct pid_namespace -{ - - struct kref kref; - struct pidmap pidmap[PIDMAP_ENTRIES]; - int last_pid; - struct task_struct *child_reaper; - struct kmem_cache *pid_cachep; - unsigned int level; - struct pid_namespace *parent; -}; -``` - ->我们这里只关心其中的child_reaper,level和parent这三个字段 - - -| 字段| 描述 | -| ------------- |:-------------:| -| kref | 表示指向pid_namespace的个数 | -| pidmap | pidmap结构体表示分配pid的位图。当需要分配一个新的pid时只需查找位图,找到bit为0的位置并置1,然后更新统计数据域(nr_free) | -| last_pid | 用于pidmap的分配。指向最后一个分配的pid的位置。(不是特别确定)| -| child_reaper | 指向的是当前命名空间的init进程,每个命名空间都有一个作用相当于全局init进程的进程 | -| pid_cachep | 域指向分配pid的slab的地址。| -| level | 代表当前命名空间的等级,初始命名空间的level为0,它的子命名空间level为1,依次递增,而且子命名空间对父命名空间是可见的。从给定的level设置,内核即可推断进程会关联到多少个ID。| -| parent | 指向父命名空间的指针 | - -![PID命名空间.png](./images/pid-namespace.png) - - -实际上PID分配器也需要依靠该结构的某些部分来连续生成唯一ID,但我们目前对此无需关注。我们上述代码中给出的下列成员更感兴趣。 - -每个PID命名空间都具有一个进程,其发挥的作用相当于全局的init进程。init的一个目的是对孤儿进程调用wait4,命名空间局部的init变体也必须完成该工作。 - -#pid结构描述 -------- - - -##pid与upid -------- - -PID的管理围绕两个数据结构展开: - -* [struct pid](http://lxr.free-electrons.com/source/include/linux/pid.h#L57)是内核对PID的内部表示, - -* [struct upid](http://lxr.free-electrons.com/source/include/linux/pid.h#L50)则表示特定的命名空间中可见的信息。 - - - -两个结构的定义在[include/linux/pid.h](include/linux/pid.h)中 - - - -```c -struct upid -{ - /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ - int nr; - struct pid_namespace *ns; - struct hlist_node pid_chain; -}; -``` - -| 字段| 描述 | -| ------------- |:-------------:| -| nr | 表示ID具体的值 | -| ns | 指向命名空间的指针 | -| pid_chain | 指向PID哈希列表的指针,用于关联对于的PID | - - ->所有的upid实例都保存在一个散列表中,稍后我们会看到该结构。 - -```c -struct pid -{ - atomic_t count; - /* 使用该pid的进程的列表, lists of tasks that use this pid */ - struct hlist_head tasks[PIDTYPE_MAX]; - int level; - struct upid numbers[1]; -}; -``` - -| 字段| 描述 | -| ------------- |:-------------:| -| count | 是指使用该PID的task的数目;| -| level | 表示可以看到该PID的命名空间的数目,也就是包含该进程的命名空间的深度 | -| tasks[PIDTYPE_MAX] | 是一个数组,每个数组项都是一个散列表头,分别对应以下三种类型 -| numbers[1] | 一个upid的实例数组,每个数组项代表一个命名空间,用来表示一个PID可以属于不同的命名空间,该元素放在末尾,可以向数组添加附加的项。| - ->tasks是一个数组,每个数组项都是一个散列表头,对应于一个ID类型,PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID( PIDTYPE_MAX表示ID类型的数目)这样做是必要的,因为一个ID可能用于几个进程。所有共享同一给定ID的task_struct实例,都通过该列表连接起来。 -> ->这个枚举常量PIDTYPE_MAX,正好是pid_type类型的数目,这里linux内核使用了一个小技巧来由编译器来自动生成id类型的数目 - - - -此外,还有两个结构我们需要说明,就是pidmap和pid_link - -* pidmap当需要分配一个新的pid时查找可使用pid的位图,其定义如下 - -* 而pid_link则是pid的哈希表存储结构 - -##pidmap用于分配pid的位图 -------- - -```c -struct pidmap -{ - atomic_t nr_free; - void *page; -}; -``` - -| 字段| 描述 | -| ------------- |:-------------:| -| nr_free | 表示还能分配的pid的数量 | -| page | 指向的是存放pid的物理页 | - - - ->pidmap[PIDMAP_ENTRIES]域表示该pid_namespace下pid已分配情况 - -##pid_link哈希表存储 -------- - -pids[PIDTYPE_MAX]指向了和该task_struct相关的pid结构体。 -pid_link的定义如下 -```c -struct pid_link -{ -struct hlist_node node; -struct pid *pid; -}; -``` - - - -##task_struct中进程ID相关数据结构 -------- - -##task_struct中的描述符信息 -------- -```c -struct task_struct -{ - //... - pid_t pid; - pid_t tgid; - struct task_struct *group_leader; - struct pid_link pids[PIDTYPE_MAX]; - struct nsproxy *nsproxy; - //... -}; -``` -| 字段| 描述 | -| ------------- |:-------------:| -| pid | 指该进程的进程描述符。在fork函数中对其进行赋值的 | -| tgid | 指该进程的线程描述符。在linux内核中对线程并没有做特殊的处理,还是由task_struct来管理。所以从内核的角度看, 用户态的线程本质上还是一个进程。对于同一个进程(用户态角度)中不同的线程其tgid是相同的,但是pid各不相同。 主线程即group_leader(主线程会创建其他所有的子线程)。如果是单线程进程(用户态角度),它的pid等于tgid。| -| group_leader | 除了在多线程的模式下指向主线程,还有一个用处, 当一些进程组成一个群组时(PIDTYPE_PGID), 该域指向该群组的leader | -| nsproxy | 指针指向namespace相关的域,通过nsproxy域可以知道该task_struct属于哪个pid_namespace | - ->对于用户态程序来说,调用getpid()函数其实返回的是tgid,因此线程组中的进程id应该是是一致的,但是他们pid不一致,这也是内核区分他们的标识 - - - -1. 多个task_struct可以共用一个PID - -2. 一个PID可以属于不同的命名空间 - -3. 当需要分配一个新的pid时候,只需要查找pidmap位图即可 - -那么最终,linux下进程命名空间和进程的关系结构如下: - -![进程命名空间和进程的关系结构](./images/pidnamespace-and-process.png) - -可以看到,多个task_struct指向一个PID,同时PID的hash数组里安装不同的类型对task进行散列,并且一个PID会属于多个命名空间。 - - - -#内核是如何设计task_struct中进程ID相关数据结构的 -------- - ->本部内容较多的采用了[Linux 内核进程管理之进程ID](http://www.cnblogs.com/hazir/p/linux_kernel_pid.html) - -Linux 内核在设计管理ID的数据结构时,要充分考虑以下因素: - -1. 如何快速地根据进程的 task_struct、ID类型、命名空间找到局部ID - -2. 如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct - -3. 如何快速地给新进程在可见的命名空间内分配一个唯一的 PID - -如果将所有因素考虑到一起,将会很复杂,下面将会由简到繁设计该结构。 - -##一个PID对应一个task时的task_struct设计 -------- - -一个PID对应一个`task_struct`如果先不考虑进程之间的关系,不考虑命名空间,仅仅是一个PID号对应一个`task_struct`,那么我们可以设计这样的数据结构 - -```c -struct task_struct -{ - //... - struct pid_link pids; - //... -}; - -struct pid_link -{ - struct hlist_node node; - struct pid *pid; -}; - -struct pid -{ - struct hlist_head tasks; //指回 pid_link 的 node - int nr; //PID - struct hlist_node pid_chain; //pid hash 散列表结点 -}; -``` -每个进程的 task_struct 结构体中有一个指向 pid 结构体的指针,pid结构体包含了PID号。 - -结构示意图如图 - -![一个task_struct对应一个PID](./images/per-task_struct-per-pid.png) - - -#如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct -------- -图中还有两个结构上面未提及: - ->* pid_hash[] -> ->这是一个hash表的结构,根据pid的nr值哈希到其某个表项,若有多个 pid 结构对应到同一个表项,这里解决冲突使用的是散列表法。 - -这样,就能解决开始提出的第2个问题了,根据PID值怎样快速地找到task_struct结构体: - -1. 首先通过 PID 计算 pid 挂接到哈希表 pid_hash[] 的表项 - -2. 遍历该表项,找到 pid 结构体中 nr 值与 PID 值相同的那个 pid - -3. 再通过该 pid 结构体的 tasks 指针找到 node - -4. 最后根据内核的 container_of 机制就能找到 task_struct 结构体 - -#如何快速地给新进程在可见的命名空间内分配一个唯一的 PID -------- - ->* pid_map -> ->这是一个位图,用来唯一分配PID值的结构,图中灰色表示已经分配过的值,在新建一个进程时,只需在其中找到一个为分配过的值赋给 pid 结构体的 nr,再将pid_map 中该值设为已分配标志。这也就解决了上面的**第3个问题——如何快速地分配一个全局的PID** - -至于上面的**第1个问题*就更加简单,已知 task_struct 结构体,根据其 pid_link 的 pid 指针找到 pid 结构体,取出其 nr 即为 PID 号。 - -##带进程ID类型的task_struct设计 -------- - -如果考虑进程之间有复杂的关系,如线程组、进程组、会话组,这些组均有组ID,分别为 TGID、PGID、SID,所以原来的 task_struct 中pid_link 指向一个 pid 结构体需要增加几项,用来指向到其组长的 pid 结构体,相应的 struct pid 原本只需要指回其 PID 所属进程的task_struct,现在要增加几项,用来链接那些以该 pid 为组长的所有进程组内进程。数据结构如下: - ->定义在http://lxr.free-electrons.com/source/include/linux/sched.h#L1389 - - -```c -enum pid_type -{ - PIDTYPE_PID, - PIDTYPE_PGID, - PIDTYPE_SID, - PIDTYPE_MAX -}; - -struct task_struct -{ - //... - pid_t pid; //PID - pid_t tgid; //thread group id - //.. - struct pid_link pids[PIDTYPE_MAX]; - struct task_struct *group_leader; // threadgroup leader - //... - struct pid_link pids[PIDTYPE_MAX]; - struct nsproxy *nsproxy; -}; - -struct pid_link -{ - struct hlist_node node; - struct pid *pid; -}; - -struct pid -{ - struct hlist_head tasks[PIDTYPE_MAX]; - int nr; //PID - struct hlist_node pid_chain; // pid hash 散列表结点 -}; -``` - -上面 ID 的类型 PIDTYPE_MAX 表示 ID 类型数目。之所以不包括线程组ID,是因为内核中已经有指向到线程组的 task_struct 指针 group_leader,线程组 ID 无非就是 group_leader 的PID。 - - -假如现在有三个进程A、B、C为同一个进程组,进程组长为A,这样的结构示意图如图 - -![增加ID类型的结构](./images/task_struct-with-pidtype.png) - - - -关于上图有几点需要说明: - -图中省去了 pid_hash 以及 pid_map 结构,因为第一种情况类似; - -进程B和C的进程组组长为A,那么 pids[PIDTYPE_PGID] 的 pid 指针指向进程A的 pid 结构体; - -进程A是进程B和C的组长,进程A的 pid 结构体的 tasks[PIDTYPE_PGID] 是一个散列表的头,它将所有以该pid 为组长的进程链接起来。 - -再次回顾本节的三个基本问题,在此结构上也很好去实现。 - -#进一步增加进程PID命名空间的task_struct设计 -------- - -若在第二种情形下再增加PID命名空间 - -一个进程就可能有多个PID值了,因为在每一个可见的命名空间内都会分配一个PID,这样就需要改变 pid 的结构了,如下: -```c -struct pid -{ - unsigned int level; - /* lists of tasks that use this pid */ - struct hlist_head tasks[PIDTYPE_MAX]; - struct upid numbers[1]; -}; - -struct upid -{ - int nr; - struct pid_namespace *ns; - struct hlist_node pid_chain; -}; -``` - -在 pid 结构体中增加了一个表示该进程所处的命名空间的层次level,以及一个可扩展的 upid 结构体。对于struct upid,表示在该命名空间所分配的进程的ID,ns指向是该ID所属的命名空间,pid_chain 表示在该命名空间的散列表。 - -举例来说,在level 2 的某个命名空间上新建了一个进程,分配给它的 pid 为45,映射到 level 1 的命名空间,分配给它的 pid 为 134;再映射到 level 0 的命名空间,分配给它的 pid 为289,对于这样的例子,如图4所示为其表示: - - -![增加PID命名空间之后的结构图](./images/task_struct-with-namespace.png) - - -图中关于如果分配唯一的 PID 没有画出,但也是比较简单,与前面两种情形不同的是,这里分配唯一的 PID 是有命名空间的容器的,在PID命名空间内必须唯一,但各个命名空间之间不需要唯一。 -至此,已经与 Linux 内核中数据结构相差不多了。 - - -#进程ID管理函数 -------- - -有了上面的复杂的数据结构,再加上散列表等数据结构的操作,就可以写出我们前面所提到的三个问题的函数了: - -##pid号到struct pid实体 -------- - -很多时候在写内核模块的时候,需要通过进程的pid找到对应进程的task_struct,其中首先就需要通过进程的pid找到进程的struct pid, -然后再通过struct pid找到进程的task_struct - -我知道的实现函数有三个。 - -```c -struct pid *find_pid_ns(int nr, struct pid_namespace *ns) -struct pid *find_vpid(int nr) -struct pid *find_get_pid(pid_t nr) -``` - - - -find_pid_ns获得 pid 实体的实现原理,主要使用哈希查找。内核使用哈希表组织struct pid,每创建一个新进程,给进程的struct pid都会插入到哈希表中,这时候就需要使用进程 -的进程pid和命名ns在哈希表中将相对应的struct pid索引出来,现在可以看下find_pid_ns的传入参数,也是通过nr和ns找到struct pid。 - -根据局部PID以及命名空间计算在 pid_hash 数组中的索引,然后遍历散列表找到所要的 upid, 再根据内核的 container_of 机制找到 pid 实例。 - -代码如下: - -```c -struct pid *find_pid_ns(int nr, struct pid_namespace *ns) -{ - struct hlist_node *elem; - struct upid *pnr; - - hlist_for_each_entry_rcu(pnr, elem, - &pid_hash[pid_hashfn(nr, ns)], pid_chain) - if (pnr->nr == nr && pnr->ns == ns) - return container_of(pnr, struct pid, - numbers[ns->level]); - - return NULL; -} -``` - -而另外两个函数则是对其进行进一步的封装,如下 - -```c -struct pid *find_vpid(int nr) -{ - return find_pid_ns(nr, current->nsproxy->pid_ns); -} -struct pid *find_get_pid(pid_t nr) -{ - struct pid *pid; - - rcu_read_lock(); - pid = get_pid(find_vpid(nr)); - rcu_read_unlock(); - - return pid; -} -``` - -三者之间的调用关系如下 - -![调用关系](./images/pid-to-struct_pid.png) - -由图可以看出,find_pid_ns是最终的实现,find_vpid是使用find_pid_ns -实现的,find_get_pid又是由find_vpid实现的。 - -由原代码可以看出find_vpid和find_pid_ns是一样的,而find_get_pid和find_vpid有一点差异,就是使用find_get_pid将返回的struct pid中的字段count加1,而find_vpid没有加1。 - - -##获得局部ID -------- - -根据进程的 task_struct、ID类型、命名空间,可以很容易获得其在命名空间内的局部ID - -获得与task_struct 关联的pid结构体。辅助函数有 task_pid、task_tgid、task_pgrp和task_session,分别用来获取不同类型的ID的pid 实例,如获取 PID 的实例: - -```c -static inline struct pid *task_pid(struct task_struct *task) -{ - return task->pids[PIDTYPE_PID].pid; -} -``` - -获取线程组的ID,前面也说过,TGID不过是线程组组长的PID而已,所以: -```c -static inline struct pid *task_tgid(struct task_struct *task) -{ - return task->group_leader->pids[PIDTYPE_PID].pid; -} -``` - -而获得PGID和SID,首先需要找到该线程组组长的task_struct,再获得其相应的 pid: - -```c -static inline struct pid *task_pgrp(struct task_struct *task) -{ - return task->group_leader->pids[PIDTYPE_PGID].pid; -} - -static inline struct pid *task_session(struct task_struct *task) -{ - return task->group_leader->pids[PIDTYPE_SID].pid; -} -``` - -获得 pid 实例之后,再根据 pid 中的numbers 数组中 uid 信息,获得局部PID。 - - -```c -pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns) -{ - struct upid *upid; - pid_t nr = 0; - if (pid && ns->level <= pid->level) - { - upid = &pid->numbers[ns->level]; - if (upid->ns == ns) - nr = upid->nr; - } - return nr; -} -``` - -这里值得注意的是,由于PID命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能,因此,函数中需要确保当前命名空间的level 小于等于产生局部PID的命名空间的level。 - -除了这个函数之外,内核还封装了其他函数用来从 pid 实例获得 PID 值,如 pid_nr、pid_vnr 等。在此不介绍了。 -结合这两步,内核提供了更进一步的封装,提供以下函数: - -```c -pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); -pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); -pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); -pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); -``` -从函数名上就能推断函数的功能,其实不外于封装了上面的两步。 - -##根据PID查找进程task_struct -------- - -* 根据PID号(nr值)取得task_struct 结构体 - -* 根据PID以及其类型(即为局部ID和命名空间)获取task_struct结构体 - -如果根据的是进程的ID号,我们可以先通过ID号(nr值)获取到进程struct pid实体(局部ID),然后根据局部ID、以及命名空间,获得进程的task_struct结构体 - - -可以使用pid_task根据pid和pid_type获取到进程的task - -```c -struct task_struct *pid_task(struct pid *pid, enum pid_type type) -{ - struct task_struct *result = NULL; - if (pid) { - struct hlist_node *first; - first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]), - lockdep_tasklist_lock_is_held()); - if (first) - result = hlist_entry(first, struct task_struct, pids[(type)].node); - } - - return result; -} -``` - -那么我们根据pid号查找进程task的过程就成为 -```c -pTask = pid_task(find_vpid(pid), PIDTYPE_PID); -``` - -内核还提供其它函数用来实现上面两步: - -```c -struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns); -struct task_struct *find_task_by_vpid(pid_t vnr); -struct task_struct *find_task_by_pid(pid_t vnr); -``` - - ->由于linux进程是组织在双向链表和红黑树中的,因此我们通过遍历链表或者树也可以找到当前进程,但是这个并不是我们今天的重点 - - - -##生成唯一的PID -------- - -内核中使用下面两个函数来实现分配和回收PID的: -```c -static int alloc_pidmap(struct pid_namespace *pid_ns); -static void free_pidmap(struct upid *upid); -``` - -在这里我们不关注这两个函数的实现,反而应该关注分配的 PID 如何在多个命名空间中可见,这样需要在每个命名空间生成一个局部ID,函数 alloc_pid 为新建的进程分配PID,简化版如下: -```c -struct pid *alloc_pid(struct pid_namespace *ns) -{ - struct pid *pid; - enum pid_type type; - int i, nr; - struct pid_namespace *tmp; - struct upid *upid; - tmp = ns; - pid->level = ns->level; - // 初始化 pid->numbers[] 结构体 - for (i = ns->level; i >= 0; i--) - { - nr = alloc_pidmap(tmp); //分配一个局部ID - pid->numbers[i].nr = nr; - pid->numbers[i].ns = tmp; - tmp = tmp->parent; - } - // 初始化 pid->task[] 结构体 - for (type = 0; type < PIDTYPE_MAX; ++type) - INIT_HLIST_HEAD(&pid->tasks[type]); - - // 将每个命名空间经过哈希之后加入到散列表中 - upid = pid->numbers + ns->level; - for ( ; upid >= pid->numbers; --upid) - { - hlist_add_head_rcu(&upid->pid_chain, &pid_hash[pid_hashfn(upid->nr, upid->ns)]); - upid->ns->nr_hashed++; - } - - return pid; -} -``` +Linux进程ID号--Linux进程的管理与调度(三) +======= + + +| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN | +| ------------- |:-------------:|:-------------:|:-------------:|:-------------:|:-------------:| +| 2016-05-12 | [Linux-4.5](http://lxr.free-electrons.com/source/?v=4.5) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux-进程管理与调度](http://blog.csdn.net/gatieme/article/category/6225543) | + + +Linux 内核使用 task_struct 数据结构来关联所有与进程有关的数据和结构,Linux 内核所有涉及到进程和程序的所有算法都是围绕该数据结构建立的,是内核中最重要的数据结构之一。 + +该数据结构在内核文件[include/linux/sched.h](http://lxr.free-electrons.com/source/include/linux/sched.h#L1389)中定义,在目前最新的Linux-4.5(截至目前的日期为2016-05-11)的内核中,该数据结构足足有 380 行之多,在这里我不可能逐项去描述其表示的含义,本篇文章只关注该数据结构如何来组织和管理进程ID的。 + +#进程ID概述 +------- + +##进程ID类型 +------- + +要想了解内核如何来组织和管理进程ID,先要知道进程ID的类型: + +内核中进程ID的类型用[pid_type](http://lxr.free-electrons.com/source/include/linux/pid.h#L6)来描述,它被定义在[include/linux/pid.h](http://lxr.free-electrons.com/source/include/linux/pid.h)中 + +```c +enum pid_type +{ + PIDTYPE_PID, + PIDTYPE_PGID, + PIDTYPE_SID, + PIDTYPE_MAX +}; +``` + +* **PID** 内核唯一区分每个进程的标识 + + +pid是 Linux 中在其命名空间中唯一标识进程而分配给它的一个号码,称做进程ID号,简称PID。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值 + + +这个pid用于内核唯一的区分每个进程 + +>注意它并不是我们用户空间通过getpid( )所获取到的那个进程号,至于原因么,接着往下看 + +* **TGID** 线程组(轻量级进程组)的ID标识 + +在一个进程中,如果以CLONE_THREAD标志来调用clone建立的进程就是该进程的一个线程(即轻量级进程,Linux其实没有严格的进程概念),它们处于一个线程组, + + +该线程组的所有线程的ID叫做TGID。处于相同的线程组中的所有进程都有相同的TGID,但是由于他们是不同的进程,因此其pid各不相同;线程组组长(也叫主线程)的TGID与其PID相同;一个进程没有使用线程,则其TGID与PID也相同。 + + +* **PGID** + +另外,独立的进程可以组成进程组(使用setpgrp系统调用),进程组可以简化向所有组内进程发送信号的操作 + +例如用管道连接的进程处在同一进程组内。进程组ID叫做PGID,进程组内的所有进程都有相同的PGID,等于该组组长的PID。 + +* **SID** + +几个进程组可以合并成一个会话组(使用setsid系统调用),可以用于终端程序设计。会话组中所有进程都有相同的SID,保存在task_struct的session成员中 + + +##PID命名空间 +------- + +###pid命名空间概述 +------- + +命名空间是为操作系统层面的虚拟化机制提供支撑,目前实现的有六种不同的命名空间,分别为mount命名空间、UTS命名空间、IPC命名空间、用户命名空间、PID命名空间、网络命名空间。命名空间简单来说提供的是对全局资源的一种抽象,将资源放到不同的容器中(不同的命名空间),各容器彼此隔离。 + +>关于命名空间的详细信息,请参见 + +命名空间有的还有层次关系,如PID命名空间 + +![命名空间的层次关系图](./images/namespace-level.jpg) + + + + +>在上图有四个命名空间,一个父命名空间衍生了两个子命名空间,其中的一个子命名空间又衍生了一个子命名空间。以PID命名空间为例,由于各个命名空间彼此隔离,所以每个命名空间都可以有 PID 号为 1 的进程;但又由于命名空间的层次性,父命名空间是知道子命名空间的存在,因此子命名空间要映射到父命名空间中去,因此上图中 level 1 中两个子命名空间的六个进程分别映射到其父命名空间的PID 号5~10。 + +###局部ID和全局ID +------- + + +命名空间增加了PID管理的复杂性。 + + +回想一下,PID命名空间按层次组织。在建立一个新的命名空间时,该命名空间中的所有PID对父命名空间都是可见的,但子命名空间无法看到父命名空间的PID。但这意味着某些进程具有多个PID,凡可以看到该进程的命名空间,都会为其分配一个PID。 这必须反映在数据结构中。我们必须区分**局部ID**和**全局ID** + + + +全局PID和TGID直接保存在[`task_struct`](http://lxr.free-electrons.com/source/include/linux/sched.h#L1389)中,分别是task_struct的`pid`和`tgid`成员: + + +* **全局ID** 在内核本身和初始命名空间中唯一的ID,在系统启动期间开始的 init 进程即属于该初始命名空间。系统中每个进程都对应了该命名空间的一个PID,叫全局ID,保证在整个系统中唯一。 + +* **局部ID** 对于属于某个特定的命名空间,它在其命名空间内分配的ID为局部ID,该ID也可以出现在其他的命名空间中。 + +```c + +struct task_struct +{ + //... + pid_t pid; + pid_t tgid; + //... +} +``` + +两项都是pid_t类型,该类型定义为__kernel_pid_t,后者由各个体系结构分别定义。通常定义为int,即可以同时使用232个不同的ID。 + +会话session和进程group组ID不是直接包含在task_struct本身中,但保存在用于信号处理的结构中。 + + + +>task_ struct->signal->__session表示全局SID, +> +>而全局PGID则保存在task_struct->signal->__pgrp。 +> +>辅助函数set_task_session和set_task_pgrp可用于修改这些值。 + + +除了这两个字段之外,内核还需要找一个办法来管理所有命名空间内部的局部量,以及其他ID(如TID和SID)。这需要几个相互连接的数据结构,以及许多辅助函数,并将在下文讨论。 + +下文我将使用ID指代提到的任何进程ID。在必要的情况下,我会明确地说明ID类型(例如,TGID,即线程组ID)。 + +一个小型的子系统称之为PID分配器(pid allocator)用于加速新ID的分配。此外,内核需要提供辅助函数,以实现通过ID及其类型查找进程的task_struct的功能,以及将ID的内核表示形式和用户空间可见的数值进行转换的功能。 + + + +###PID命名空间数据结构pid_namespace + +------- + + + +在介绍表示ID本身所需的数据结构之前,我需要讨论PID命名空间的表示方式。我们所需查看的代码如下所示: + +[pid_namespace](http://lxr.free-electrons.com/source/include/linux/pid_namespace.h#L24)的定义在[include/linux/pid_namespace.h](http://lxr.free-electrons.com/source/include/linux/pid_namespace.h#L24)中 + +命名空间的结构如下 + +```c +struct pid_namespace +{ + + struct kref kref; + struct pidmap pidmap[PIDMAP_ENTRIES]; + int last_pid; + struct task_struct *child_reaper; + struct kmem_cache *pid_cachep; + unsigned int level; + struct pid_namespace *parent; +}; +``` + +>我们这里只关心其中的child_reaper,level和parent这三个字段 + + +| 字段| 描述 | +| ------------- |:-------------:| +| kref | 表示指向pid_namespace的个数 | +| pidmap | pidmap结构体表示分配pid的位图。当需要分配一个新的pid时只需查找位图,找到bit为0的位置并置1,然后更新统计数据域(nr_free) | +| last_pid | 用于pidmap的分配。指向最后一个分配的pid的位置。(不是特别确定)| +| child_reaper | 指向的是当前命名空间的init进程,每个命名空间都有一个作用相当于全局init进程的进程 | +| pid_cachep | 域指向分配pid的slab的地址。| +| level | 代表当前命名空间的等级,初始命名空间的level为0,它的子命名空间level为1,依次递增,而且子命名空间对父命名空间是可见的。从给定的level设置,内核即可推断进程会关联到多少个ID。| +| parent | 指向父命名空间的指针 | + +![PID命名空间.png](./images/pid-namespace.png) + + +实际上PID分配器也需要依靠该结构的某些部分来连续生成唯一ID,但我们目前对此无需关注。我们上述代码中给出的下列成员更感兴趣。 + +每个PID命名空间都具有一个进程,其发挥的作用相当于全局的init进程。init的一个目的是对孤儿进程调用wait4,命名空间局部的init变体也必须完成该工作。 + +#pid结构描述 +------- + + +##pid与upid +------- + +PID的管理围绕两个数据结构展开: + +* [struct pid](http://lxr.free-electrons.com/source/include/linux/pid.h#L57)是内核对PID的内部表示, + +* [struct upid](http://lxr.free-electrons.com/source/include/linux/pid.h#L50)则表示特定的命名空间中可见的信息。 + + + +两个结构的定义在[include/linux/pid.h](include/linux/pid.h)中 + + + +```c +struct upid +{ + /* Try to keep pid_chain in the same cacheline as nr for find_vpid */ + int nr; + struct pid_namespace *ns; + struct hlist_node pid_chain; +}; +``` + +| 字段| 描述 | +| ------------- |:-------------:| +| nr | 表示ID具体的值 | +| ns | 指向命名空间的指针 | +| pid_chain | 指向PID哈希列表的指针,用于关联对于的PID | + + +>所有的upid实例都保存在一个散列表中,稍后我们会看到该结构。 + +```c +struct pid +{ + atomic_t count; + /* 使用该pid的进程的列表, lists of tasks that use this pid */ + struct hlist_head tasks[PIDTYPE_MAX]; + int level; + struct upid numbers[1]; +}; +``` + +| 字段| 描述 | +| ------------- |:-------------:| +| count | 是指使用该PID的task的数目;| +| level | 表示可以看到该PID的命名空间的数目,也就是包含该进程的命名空间的深度 | +| tasks[PIDTYPE_MAX] | 是一个数组,每个数组项都是一个散列表头,分别对应以下三种类型 +| numbers[1] | 一个upid的实例数组,每个数组项代表一个命名空间,用来表示一个PID可以属于不同的命名空间,该元素放在末尾,可以向数组添加附加的项。| + +>tasks是一个数组,每个数组项都是一个散列表头,对应于一个ID类型,PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID( PIDTYPE_MAX表示ID类型的数目)这样做是必要的,因为一个ID可能用于几个进程。所有共享同一给定ID的task_struct实例,都通过该列表连接起来。 +> +>这个枚举常量PIDTYPE_MAX,正好是pid_type类型的数目,这里linux内核使用了一个小技巧来由编译器来自动生成id类型的数目 + + + +此外,还有两个结构我们需要说明,就是pidmap和pid_link + +* pidmap当需要分配一个新的pid时查找可使用pid的位图,其定义如下 + +* 而pid_link则是pid的哈希表存储结构 + +##pidmap用于分配pid的位图 +------- + +```c +struct pidmap +{ + atomic_t nr_free; + void *page; +}; +``` + +| 字段| 描述 | +| ------------- |:-------------:| +| nr_free | 表示还能分配的pid的数量 | +| page | 指向的是存放pid的物理页 | + + + +>pidmap[PIDMAP_ENTRIES]域表示该pid_namespace下pid已分配情况 + +##pid_link哈希表存储 +------- + +pids[PIDTYPE_MAX]指向了和该task_struct相关的pid结构体。 +pid_link的定义如下 +```c +struct pid_link +{ +struct hlist_node node; +struct pid *pid; +}; +``` + + + +##task_struct中进程ID相关数据结构 +------- + +##task_struct中的描述符信息 +------- +```c +struct task_struct +{ + //... + pid_t pid; + pid_t tgid; + struct task_struct *group_leader; + struct pid_link pids[PIDTYPE_MAX]; + struct nsproxy *nsproxy; + //... +}; +``` +| 字段| 描述 | +| ------------- |:-------------:| +| pid | 指该进程的进程描述符。在fork函数中对其进行赋值的 | +| tgid | 指该进程的线程描述符。在linux内核中对线程并没有做特殊的处理,还是由task_struct来管理。所以从内核的角度看, 用户态的线程本质上还是一个进程。对于同一个进程(用户态角度)中不同的线程其tgid是相同的,但是pid各不相同。 主线程即group_leader(主线程会创建其他所有的子线程)。如果是单线程进程(用户态角度),它的pid等于tgid。| +| group_leader | 除了在多线程的模式下指向主线程,还有一个用处, 当一些进程组成一个群组时(PIDTYPE_PGID), 该域指向该群组的leader | +| nsproxy | 指针指向namespace相关的域,通过nsproxy域可以知道该task_struct属于哪个pid_namespace | + +>对于用户态程序来说,调用getpid()函数其实返回的是tgid,因此线程组中的进程id应该是是一致的,但是他们pid不一致,这也是内核区分他们的标识 + + + +1. 多个task_struct可以共用一个PID + +2. 一个PID可以属于不同的命名空间 + +3. 当需要分配一个新的pid时候,只需要查找pidmap位图即可 + +那么最终,linux下进程命名空间和进程的关系结构如下: + +![进程命名空间和进程的关系结构](./images/pidnamespace-and-process.png) + +可以看到,多个task_struct指向一个PID,同时PID的hash数组里安装不同的类型对task进行散列,并且一个PID会属于多个命名空间。 + + + +#内核是如何设计task_struct中进程ID相关数据结构的 +------- + +>本部内容较多的采用了[Linux 内核进程管理之进程ID](http://www.cnblogs.com/hazir/p/linux_kernel_pid.html) + +Linux 内核在设计管理ID的数据结构时,要充分考虑以下因素: + +1. 如何快速地根据进程的 task_struct、ID类型、命名空间找到局部ID + +2. 如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct + +3. 如何快速地给新进程在可见的命名空间内分配一个唯一的 PID + +如果将所有因素考虑到一起,将会很复杂,下面将会由简到繁设计该结构。 + +##一个PID对应一个task时的task_struct设计 +------- + +一个PID对应一个`task_struct`如果先不考虑进程之间的关系,不考虑命名空间,仅仅是一个PID号对应一个`task_struct`,那么我们可以设计这样的数据结构 + +```c +struct task_struct +{ + //... + struct pid_link pids; + //... +}; + +struct pid_link +{ + struct hlist_node node; + struct pid *pid; +}; + +struct pid +{ + struct hlist_head tasks; //指回 pid_link 的 node + int nr; //PID + struct hlist_node pid_chain; //pid hash 散列表结点 +}; +``` +每个进程的 task_struct 结构体中有一个指向 pid 结构体的指针,pid结构体包含了PID号。 + +结构示意图如图 + +![一个task_struct对应一个PID](./images/per-task_struct-per-pid.png) + + +#如何快速地根据局部ID、命名空间、ID类型找到对应进程的 task_struct +------- +图中还有两个结构上面未提及: + +>* pid_hash[] +> +>这是一个hash表的结构,根据pid的nr值哈希到其某个表项,若有多个 pid 结构对应到同一个表项,这里解决冲突使用的是散列表法。 + +这样,就能解决开始提出的第2个问题了,根据PID值怎样快速地找到task_struct结构体: + +1. 首先通过 PID 计算 pid 挂接到哈希表 pid_hash[] 的表项 + +2. 遍历该表项,找到 pid 结构体中 nr 值与 PID 值相同的那个 pid + +3. 再通过该 pid 结构体的 tasks 指针找到 node + +4. 最后根据内核的 container_of 机制就能找到 task_struct 结构体 + +#如何快速地给新进程在可见的命名空间内分配一个唯一的 PID +------- + +>* pid_map +> +>这是一个位图,用来唯一分配PID值的结构,图中灰色表示已经分配过的值,在新建一个进程时,只需在其中找到一个为分配过的值赋给 pid 结构体的 nr,再将pid_map 中该值设为已分配标志。这也就解决了上面的**第3个问题——如何快速地分配一个全局的PID** + +至于上面的**第1个问题*就更加简单,已知 task_struct 结构体,根据其 pid_link 的 pid 指针找到 pid 结构体,取出其 nr 即为 PID 号。 + +##带进程ID类型的task_struct设计 +------- + +如果考虑进程之间有复杂的关系,如线程组、进程组、会话组,这些组均有组ID,分别为 TGID、PGID、SID,所以原来的 task_struct 中pid_link 指向一个 pid 结构体需要增加几项,用来指向到其组长的 pid 结构体,相应的 struct pid 原本只需要指回其 PID 所属进程的task_struct,现在要增加几项,用来链接那些以该 pid 为组长的所有进程组内进程。数据结构如下: + +>定义在http://lxr.free-electrons.com/source/include/linux/sched.h#L1389 + + +```c +enum pid_type +{ + PIDTYPE_PID, + PIDTYPE_PGID, + PIDTYPE_SID, + PIDTYPE_MAX +}; + +struct task_struct +{ + //... + pid_t pid; //PID + pid_t tgid; //thread group id + //.. + struct pid_link pids[PIDTYPE_MAX]; + struct task_struct *group_leader; // threadgroup leader + //... + struct pid_link pids[PIDTYPE_MAX]; + struct nsproxy *nsproxy; +}; + +struct pid_link +{ + struct hlist_node node; + struct pid *pid; +}; + +struct pid +{ + struct hlist_head tasks[PIDTYPE_MAX]; + int nr; //PID + struct hlist_node pid_chain; // pid hash 散列表结点 +}; +``` + +上面 ID 的类型 PIDTYPE_MAX 表示 ID 类型数目。之所以不包括线程组ID,是因为内核中已经有指向到线程组的 task_struct 指针 group_leader,线程组 ID 无非就是 group_leader 的PID。 + + +假如现在有三个进程A、B、C为同一个进程组,进程组长为A,这样的结构示意图如图 + +![增加ID类型的结构](./images/task_struct-with-pidtype.png) + + + +关于上图有几点需要说明: + +图中省去了 pid_hash 以及 pid_map 结构,因为第一种情况类似; + +进程B和C的进程组组长为A,那么 pids[PIDTYPE_PGID] 的 pid 指针指向进程A的 pid 结构体; + +进程A是进程B和C的组长,进程A的 pid 结构体的 tasks[PIDTYPE_PGID] 是一个散列表的头,它将所有以该pid 为组长的进程链接起来。 + +再次回顾本节的三个基本问题,在此结构上也很好去实现。 + +#进一步增加进程PID命名空间的task_struct设计 +------- + +若在第二种情形下再增加PID命名空间 + +一个进程就可能有多个PID值了,因为在每一个可见的命名空间内都会分配一个PID,这样就需要改变 pid 的结构了,如下: +```c +struct pid +{ + unsigned int level; + /* lists of tasks that use this pid */ + struct hlist_head tasks[PIDTYPE_MAX]; + struct upid numbers[1]; +}; + +struct upid +{ + int nr; + struct pid_namespace *ns; + struct hlist_node pid_chain; +}; +``` + +在 pid 结构体中增加了一个表示该进程所处的命名空间的层次level,以及一个可扩展的 upid 结构体。对于struct upid,表示在该命名空间所分配的进程的ID,ns指向是该ID所属的命名空间,pid_chain 表示在该命名空间的散列表。 + +举例来说,在level 2 的某个命名空间上新建了一个进程,分配给它的 pid 为45,映射到 level 1 的命名空间,分配给它的 pid 为 134;再映射到 level 0 的命名空间,分配给它的 pid 为289,对于这样的例子,如图4所示为其表示: + + +![增加PID命名空间之后的结构图](./images/task_struct-with-namespace.png) + + +图中关于如果分配唯一的 PID 没有画出,但也是比较简单,与前面两种情形不同的是,这里分配唯一的 PID 是有命名空间的容器的,在PID命名空间内必须唯一,但各个命名空间之间不需要唯一。 +至此,已经与 Linux 内核中数据结构相差不多了。 + + +#进程ID管理函数 +------- + +有了上面的复杂的数据结构,再加上散列表等数据结构的操作,就可以写出我们前面所提到的三个问题的函数了: + +##pid号到struct pid实体 +------- + +很多时候在写内核模块的时候,需要通过进程的pid找到对应进程的task_struct,其中首先就需要通过进程的pid找到进程的struct pid, +然后再通过struct pid找到进程的task_struct + +我知道的实现函数有三个。 + +```c +struct pid *find_pid_ns(int nr, struct pid_namespace *ns) +struct pid *find_vpid(int nr) +struct pid *find_get_pid(pid_t nr) +``` + + + +find_pid_ns获得 pid 实体的实现原理,主要使用哈希查找。内核使用哈希表组织struct pid,每创建一个新进程,给进程的struct pid都会插入到哈希表中,这时候就需要使用进程 +的进程pid和命名ns在哈希表中将相对应的struct pid索引出来,现在可以看下find_pid_ns的传入参数,也是通过nr和ns找到struct pid。 + +根据局部PID以及命名空间计算在 pid_hash 数组中的索引,然后遍历散列表找到所要的 upid, 再根据内核的 container_of 机制找到 pid 实例。 + +代码如下: + +```c +struct pid *find_pid_ns(int nr, struct pid_namespace *ns) +{ + struct hlist_node *elem; + struct upid *pnr; + + hlist_for_each_entry_rcu(pnr, elem, + &pid_hash[pid_hashfn(nr, ns)], pid_chain) + if (pnr->nr == nr && pnr->ns == ns) + return container_of(pnr, struct pid, + numbers[ns->level]); + + return NULL; +} +``` + +而另外两个函数则是对其进行进一步的封装,如下 + +```c +struct pid *find_vpid(int nr) +{ + return find_pid_ns(nr, current->nsproxy->pid_ns); +} +struct pid *find_get_pid(pid_t nr) +{ + struct pid *pid; + + rcu_read_lock(); + pid = get_pid(find_vpid(nr)); + rcu_read_unlock(); + + return pid; +} +``` + +三者之间的调用关系如下 + +![调用关系](./images/pid-to-struct_pid.png) + +由图可以看出,find_pid_ns是最终的实现,find_vpid是使用find_pid_ns +实现的,find_get_pid又是由find_vpid实现的。 + +由原代码可以看出find_vpid和find_pid_ns是一样的,而find_get_pid和find_vpid有一点差异,就是使用find_get_pid将返回的struct pid中的字段count加1,而find_vpid没有加1。 + + +##获得局部ID +------- + +根据进程的 task_struct、ID类型、命名空间,可以很容易获得其在命名空间内的局部ID + +获得与task_struct 关联的pid结构体。辅助函数有 task_pid、task_tgid、task_pgrp和task_session,分别用来获取不同类型的ID的pid 实例,如获取 PID 的实例: + +```c +static inline struct pid *task_pid(struct task_struct *task) +{ + return task->pids[PIDTYPE_PID].pid; +} +``` + +获取线程组的ID,前面也说过,TGID不过是线程组组长的PID而已,所以: +```c +static inline struct pid *task_tgid(struct task_struct *task) +{ + return task->group_leader->pids[PIDTYPE_PID].pid; +} +``` + +而获得PGID和SID,首先需要找到该线程组组长的task_struct,再获得其相应的 pid: + +```c +static inline struct pid *task_pgrp(struct task_struct *task) +{ + return task->group_leader->pids[PIDTYPE_PGID].pid; +} + +static inline struct pid *task_session(struct task_struct *task) +{ + return task->group_leader->pids[PIDTYPE_SID].pid; +} +``` + +获得 pid 实例之后,再根据 pid 中的numbers 数组中 uid 信息,获得局部PID。 + + +```c +pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns) +{ + struct upid *upid; + pid_t nr = 0; + if (pid && ns->level <= pid->level) + { + upid = &pid->numbers[ns->level]; + if (upid->ns == ns) + nr = upid->nr; + } + return nr; +} +``` + +这里值得注意的是,由于PID命名空间的层次性,父命名空间能看到子命名空间的内容,反之则不能,因此,函数中需要确保当前命名空间的level 小于等于产生局部PID的命名空间的level。 + +除了这个函数之外,内核还封装了其他函数用来从 pid 实例获得 PID 值,如 pid_nr、pid_vnr 等。在此不介绍了。 +结合这两步,内核提供了更进一步的封装,提供以下函数: + +```c +pid_t task_pid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); +pid_t task_tgid_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); +pid_t task_pigd_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); +pid_t task_session_nr_ns(struct task_struct *tsk, struct pid_namespace *ns); +``` +从函数名上就能推断函数的功能,其实不外于封装了上面的两步。 + +##根据PID查找进程task_struct +------- + +* 根据PID号(nr值)取得task_struct 结构体 + +* 根据PID以及其类型(即为局部ID和命名空间)获取task_struct结构体 + +如果根据的是进程的ID号,我们可以先通过ID号(nr值)获取到进程struct pid实体(局部ID),然后根据局部ID、以及命名空间,获得进程的task_struct结构体 + + +可以使用pid_task根据pid和pid_type获取到进程的task + +```c +struct task_struct *pid_task(struct pid *pid, enum pid_type type) +{ + struct task_struct *result = NULL; + if (pid) { + struct hlist_node *first; + first = rcu_dereference_check(hlist_first_rcu(&pid->tasks[type]), + lockdep_tasklist_lock_is_held()); + if (first) + result = hlist_entry(first, struct task_struct, pids[(type)].node); + } + + return result; +} +``` + +那么我们根据pid号查找进程task的过程就成为 +```c +pTask = pid_task(find_vpid(pid), PIDTYPE_PID); +``` + +内核还提供其它函数用来实现上面两步: + +```c +struct task_struct *find_task_by_pid_ns(pid_t nr, struct pid_namespace *ns); +struct task_struct *find_task_by_vpid(pid_t vnr); +struct task_struct *find_task_by_pid(pid_t vnr); +``` + + +>由于linux进程是组织在双向链表和红黑树中的,因此我们通过遍历链表或者树也可以找到当前进程,但是这个并不是我们今天的重点 + + + +##生成唯一的PID +------- + +内核中使用下面两个函数来实现分配和回收PID的: +```c +static int alloc_pidmap(struct pid_namespace *pid_ns); +static void free_pidmap(struct upid *upid); +``` + +在这里我们不关注这两个函数的实现,反而应该关注分配的 PID 如何在多个命名空间中可见,这样需要在每个命名空间生成一个局部ID,函数 alloc_pid 为新建的进程分配PID,简化版如下: +```c +struct pid *alloc_pid(struct pid_namespace *ns) +{ + struct pid *pid; + enum pid_type type; + int i, nr; + struct pid_namespace *tmp; + struct upid *upid; + tmp = ns; + pid->level = ns->level; + // 初始化 pid->numbers[] 结构体 + for (i = ns->level; i >= 0; i--) + { + nr = alloc_pidmap(tmp); //分配一个局部ID + pid->numbers[i].nr = nr; + pid->numbers[i].ns = tmp; + tmp = tmp->parent; + } + // 初始化 pid->task[] 结构体 + for (type = 0; type < PIDTYPE_MAX; ++type) + INIT_HLIST_HEAD(&pid->tasks[type]); + + // 将每个命名空间经过哈希之后加入到散列表中 + upid = pid->numbers + ns->level; + for ( ; upid >= pid->numbers; --upid) + { + hlist_add_head_rcu(&upid->pid_chain, &pid_hash[pid_hashfn(upid->nr, upid->ns)]); + upid->ns->nr_hashed++; + } + + return pid; +} +``` diff --git a/study/kernel/process/task/02-pid/images/namespace-level.jpg b/study/kernel/process/task/03-pid/images/namespace-level.jpg similarity index 100% rename from study/kernel/process/task/02-pid/images/namespace-level.jpg rename to study/kernel/process/task/03-pid/images/namespace-level.jpg diff --git a/study/kernel/process/task/02-pid/images/per-task_struct-per-pid.png b/study/kernel/process/task/03-pid/images/per-task_struct-per-pid.png similarity index 100% rename from study/kernel/process/task/02-pid/images/per-task_struct-per-pid.png rename to study/kernel/process/task/03-pid/images/per-task_struct-per-pid.png diff --git a/study/kernel/process/task/02-pid/images/pid-namespace.png b/study/kernel/process/task/03-pid/images/pid-namespace.png similarity index 100% rename from study/kernel/process/task/02-pid/images/pid-namespace.png rename to study/kernel/process/task/03-pid/images/pid-namespace.png diff --git a/study/kernel/process/task/02-pid/images/pid-to-struct_pid.png b/study/kernel/process/task/03-pid/images/pid-to-struct_pid.png similarity index 100% rename from study/kernel/process/task/02-pid/images/pid-to-struct_pid.png rename to study/kernel/process/task/03-pid/images/pid-to-struct_pid.png diff --git a/study/kernel/process/task/02-pid/images/pidnamespace-and-process.png b/study/kernel/process/task/03-pid/images/pidnamespace-and-process.png similarity index 100% rename from study/kernel/process/task/02-pid/images/pidnamespace-and-process.png rename to study/kernel/process/task/03-pid/images/pidnamespace-and-process.png diff --git a/study/kernel/process/task/02-pid/images/task_struct-with-namespace.png b/study/kernel/process/task/03-pid/images/task_struct-with-namespace.png similarity index 100% rename from study/kernel/process/task/02-pid/images/task_struct-with-namespace.png rename to study/kernel/process/task/03-pid/images/task_struct-with-namespace.png diff --git a/study/kernel/process/task/02-pid/images/task_struct-with-pidtype.png b/study/kernel/process/task/03-pid/images/task_struct-with-pidtype.png similarity index 100% rename from study/kernel/process/task/02-pid/images/task_struct-with-pidtype.png rename to study/kernel/process/task/03-pid/images/task_struct-with-pidtype.png