1 进程描述符 task_struct 和 线程描述符 thread_info
1.1 task_struct 简介
Linux 内核涉及进程和程序的所有算法都围绕一个名为 task_struct 的数据结构建立。 《深入 LINUX 内核架构》P32
task_struct 相对较大,在 32 位机器上,它大约有 1.7KB。但如果考虑到该结构内包含了内核管理一个进程所需的所有信息,那么它的大小也算相当小了。进程描述符中包含的数据能完整地描述一个正在执行的进程:它打开的文件,进程的地址空间,挂起的信号,进程的状态,还要其他很多信息。
《Linux 内核设计与实现》P21
1.2 current 分析
实际上,内核中大部分处理进程的代码都是直接通过 task_struct 进行的。因此,通过 current 宏查找到当前正在运行进程的进程描述符的速度就显得尤为重要。
硬件体系结构不同,该宏的实现也不同,它必须针对专门的硬件体系结构做处理。有的硬件体系结构可以拿出一个专门寄存器来存放指向当前进程 task_struct 的指针,用于加快访问速度。
而有些像 x86 这样的体系结构(其寄存器并不富余),就只能在内核栈的尾端创建 thread_info 结构,通过计算偏移间接地查找 task_struct 结构。
在 PPC(PowerPC,IBM 基于 RISC 的现代微处理器)上,current 宏只需把 r2 寄存器中的值返回就行了。与 x86 不一样,PPC 有足够多的寄存器。
《Linux 内核设计与实现》P23 《深入理解 LINUX 内核》P91
current 指针在原子模式下是没有任何意义的,也是不可用的,因为相关代码和被中断的进程没有任何管理。
current 在获得自旋锁的代码里是可用的,但禁止访问用户空间,因为这会导致调度的发生。
《LINUX 设备驱动程序》(第三版)P197
1.3 进程描述符(task_struct)的删除
在调用了 do_exit()之后,尽管线程已经僵死不能组运行了,但是系统还保留了它的进程描述符。前面说过,这样做可以让系统有办法在子系统终结后仍能获得它的信息。因此,进程终结时所需的清理工作和进程描述符的删除被分开执行。在父进程获得已终结的子进程的信息后,或者通知内核它并不关注那些信息后,子进程的 task_struct 结构才被释放。
《Linux 内核设计与实现》P32
2 进程标识符 / 线程组标识符 / 进程组标识符 / 会话标识符
2.1 进程标识符 PID
PID 存放在进程描述符的 pid 字段中。 PID 被顺序编号,新创建进程的 PID 通常是前一个进程的 PID 加 1 。 《深入理解 LINUX 内核》P88
为了与老版本的 Unix 和 Linux 兼容,PID 的最大值默认设置为 32768(short int 短整型的最大值),尽管这个值也可以增加到高达 400 万(这受<linux/threads.h>中所定义 PID 最大值的限制)。内核把每个进程的 PID 存放在它们各自的进程描述符中。
这个最大值很重要,因为它实际上就是系统中允许同时存在的进程的最大数目。这个值越小,转一圈就越快,本来数值大的进程比数值小的进程迟运行,但这样一来就破坏了这一原则。
系统管理员通过修改/proc/sys/kernel/pid_max 来提高上限。
《Linux 内核设计与实现》P23
PID 的值有一个上限,当内核使用的 PID 达到这个上限值的时候就必须循环使用已闲置的小 PID 号。在缺省情况下,最大的 PID 号是 32767(PID_MAX_DEFAULT-1)。
在 64 位体系结构中,系统管理员可以把 PID 的上限扩大到 4194303。
《深入理解 LINUX 内核》P88
2.2 线程组 / 线程组标识符 tgid
Unix 程序员希望同一组中的线程有共同的 PID。例如,应该可以把信号发送给指定 PID 的一组线程,这个信号会作用于该组中所有的线程。事实上,POSIX 1003.1c 标准规定一个多线程应用程序中的所有线程都必须有相同的 PID。
遵照这个标准,Linux 引入线程组表示。一个线程组中所有线程使用和该线程组的领头线程(thread group leader)相同的 PID,也就是该组中第一个轻量级进程的 PID,它被放在进程描述符的 tgid 字段中。
getpid()系统调用返回当前进程的 tgid 值而不是 pid 值。
《深入理解 LINUX 内核》P89
进程有两种特殊的形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
《Linux 内核深度解析》P17
多个共享用户虚拟地址空间的进程组成一个线程组,线程组中的主进程称为组长,线程组标识符就是组长的进程标识符。
当调用系统调用 clone 传入标志 CLONE_THREAD 以创建新进程时,新进程和当前进程属于一个线程组。
《Linux 内核深度解析》P20
2.3 进程组 / 进程组标识符 pgrp
独立进程可以合并成进程组(使用 setpgrp 系统调用)。该进程组成员的 task_struct 的 pgrp 属性值都是相同的,即进程组组长的 PID。进程组简化了向组的所有成员发送信号的操作,这对于各种系统程序设计应用是有用的。
请注意,用管道连接的进程包含在同一个进程组中。
《深入 LINUX 内核架构》P43
多个进程可以组成一个进程组,进程组标识符是组长的进程标识符。进程可以使用系统调用 setpid 创建或者加入一个进程组。
进程组简化了向进程组所有成员发送信号的操作。
《Linux 内核深度解析》P20
2.4 会话 / 会话标识符 session
几个进程组可以合并成一个会话。会话中的所有进程都有同样的会话 ID,保存在 task_struct 的 session 成员中。SID可以使用 setsid 系统调用设置。
《深入 LINUX 内核架构》P43
多个进程组可以组成一个会话。当进程调用系统调用 setsid 的时候,创建一个新的会话,会话标识符是该进程的进程标识符。创建会话的进程是会话的首进程。
Linux 是多用户操作系统,用户登录时会创建一个会话,用户启动的所有进程都属于这个会话。
《Linux 内核深度解析》P20
3 内核线程和用户线程
3.1 简介
线程机制是现代编程技术中常用的一种抽象概念。该机制提供了在同一程序内共享内存地址空间运行的一组线程。这些线程还可以共享打开的文件和其他资源。线程机制支持并发程序设计技术(concurrent programming),在多处理器系统上,它也能保证真正的并行处理(parallelism)。
Linux 实现线程的机制非常独特。从内核的角度说,它并没有线程这个概念。Linux 把所有的线程都当做进程来实现。内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的 task_struct,所以在内核中,它看起来就像是一个普通的进程(只是线程和其它一些进程共享某些资源,如地址空间)。
《Linux 内核设计与实现》P28
进程有两种特殊的形式:没有用户虚拟地址空间的进程称为内核线程,共享用户虚拟地址空间的进程称为用户线程。共享同一个用户虚拟地址空间的所有用户线程组成一个线程组。
《Linux 内核深度解析》P17
3.2 内核线程简介
内核经常需要在后台执行一些操作。这种任务可以通过内核线程(kernel thread)完成——独立运行在内核空间的标准进程。
内核线程和普通的进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的 mm 指针被设置为NULL)。它们只在内核空间运行,从来不切换到用户空间去。
内核线程和普通进程一样,可以被调度,也可以被抢占。
《Linux 内核设计与实现》P30
内核线程没有进程地址空间,也没有相关的内存描述符。所以内核线程对应的进程描述符中 mm 域为空。事实上这也是内核线程的真正含义——它们没有用户上下文。
《Linux 内核设计与实现》P250
3.3 内核线程注意点
内核线程启动后就一直运行直到调用 do_exit()退出,或者内核其它部分调用 kthread_stop()退出,传递给kthread_stop()的参数为 kthread_create()函数返回的 task_struct 结构的地址。
《Linux 内核设计与实现》P31
3.5 用户线程
一个进程由几个用户线程组成,每个线程都代表进程的一个执行流。现在,大部分多线程应用程序都是用pthread(POSIX thread)库的标准库函数集编写的。
《深入理解 LINUX 内核》P85
4 进程的地址空间
4.1 简介
内核除了管理本身内存外,还必须管理用户空间中进程的内存。我们称这个内存为进程地址空间,也就是系统中每个用户空间进程看到的内存。
《Linux 内核设计与实现》P247
进程的虚拟地址空间分为用户虚拟地址空间和内核虚拟地址空间,所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间。
《Linux 内核深度解析》P17
4.2 进程地址空间布局 / 内存描述符(struct mm_struct)
内核使用内存描述符结构体(struct mm_struct)表示进程的地址空间,该结构体包含了和进程地址空间有关的全部信息。
《Linux 内核设计与实现》P248
所有的 mm_struct 结构体都通过自身的 mmlist 域连接在一个双向链表中,该链表的首元素是 init_mm 内存描述符,它代表 init 进程的地址空间。另外要注意,操作该链表的时候需要使用 mmlist_lock 锁来防止并发访问。
在进程的进程描述符(task_struct)中,mm 域存放着进程使用的内存描述符,所以 current->mm 便指向当前进程的内存描述符。
《Linux 内核设计与实现》P249
下面给出内存描述符的结构和各个域的描述
kernel3.10
//include/linux/mm_types.h
struct mm_struct {
struct vm_area_struct * mmap; /* list of VMAs */ // 内存区域链表
struct rb_root mm_rb; // VMA形成的红黑树
struct vm_area_struct * mmap_cache; /* last find_vma result */ // 最近使用的内存区域
......
unsigned long free_area_cache; /* first hole of size cached_hole_size or larger */ //地址空间第一个空洞
pgd_t * pgd; //页全局目录
atomic_t mm_users; /* How many users with user space? */ //使用地址空间的用户数
atomic_t mm_count; /* How many references to "struct mm_struct" (users count as 1) */ //主使用计数器
int map_count; /* number of VMAs */ //内存区域的个数
spinlock_t page_table_lock; /* Protects page tables and some counters */ //页表锁
struct rw_semaphore mmap_sem; //内存区域的信号量
struct list_head mmlist; /* List of maybe swapped mm's. These are globally strung
* together off init_mm.mmlist, and are protected
* by mmlist_lock
*/ //所有mm_struct形成的链表
unsigned long start_code, end_code, start_data, end_data; //代码段的开始地址和结束结束地址,数据段的首地址和尾地址
unsigned long start_brk, brk, start_stack; //堆的首地址,堆的尾地址,进程栈的首地址
unsigned long arg_start, arg_end, env_start, env_end; //命令行参数首地址,命令行参数尾地址,环境变量首地址,环境变量尾地址
......
unsigned long total_vm; /* Total pages mapped */ //全部的页面数目
unsigned long locked_vm; /* Pages that have PG_mlocked set */ //上锁的页面数目
......
unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */ //保存的auxv
......
};
《Linux内核设计与实现》P248
4.3 栈
4.3.1 用户栈和内核栈
内核在创建进程的时候,在创建 task_struct 的同时,会为进程创建相应的堆栈。每个进程会有两个栈,一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。
当进程在用户空间运行时,cpu 堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;
当进程在内核空间运行时,cpu 堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。
当进程因为中断或者系统调用而陷入内核态之行时,进程所使用的堆栈也要从用户栈转到内核栈。
https://www.cnblogs.com/sixloop/p/8099613.html
用户空间能够奢侈地负担起非常大的栈,而且栈空间还可以动态增长,相反,内核却不能这么奢侈——内核栈小而且固定。
当给每个进程分配一个固定大小的小栈后,不但可以减少内存的消耗,而且内核也无须负担太重的栈管理任务。历史上,每个进程都有两页的内核栈。因为 32 位和 64 位体系结构的页面大小分别是 4KB 和 8KB,所以通常它们的内核栈的大小分别是 8KB 和 16KB。
《Linux 内核设计与实现》P203
内核栈有两种布局
<1> 结构体 thread_info 占用内核栈的空间,在内核栈顶部,成员 task 指向进程描述符。
<2> 结构体 thread_info 没有占用内核栈的空间,是进程描述符的第一个成员。
两种布局的区别是 thread_info 的位置不同。如果选择第二种布局,需要配置宏CONFIG_THREAD_INFO_IN_TASK。ARM64 架构使用第二种内核栈布局。第二种内核栈布局的好处是:thread_info 结构体作为进程描述符的第一个成员,它的地址和进程描述符的地址相同。当进程在内核模式运行时,ARM64 架构的内核使用用户栈指针寄存器 SP_EL0 存放当前进程的 thread_info 结构体地址,通过这个寄存器可以得到 thread_info 结构体的地址,也可以得到进程描述符的地址。
《Linux 内核深度解析》P26
内核栈的长度是 THREAD_SIZE,它由各种处理器架构自己定义,ARM64 架构定义的内核栈长度是 16KB。
《Linux 内核深度解析》P27
x86_64 的内核栈
x86_64 page size (PAGE_SIZE) is 4K.
Like all other architectures, x86_64 has a kernel stack for every active thread. These thread stacks are THREAD_SIZE
(2*PAGE_SIZE) big. These stacks contain useful data as long as a thread is alive or a zombie(僵尸). While the thread is in user space
the kernel stack is empty except for(除了) the thread_info structure at the bottom.
In addition to the per thread stacks, there are specialized stacks associated with each CPU. These stacks are only used while the
kernel is in control on that CPU; when a CPU returns to user space the specialized stacks contain no useful data. The main CPU
stacks are:
<1> Interrupt stack. IRQSTACKSIZE
<2> STACKFAULT_STACK. EXCEPTION_STKSZ (PAGE_SIZE).
<3> DOUBLEFAULT_STACK. EXCEPTION_STKSZ (PAGE_SIZE).
<4> NMI_STACK. EXCEPTION_STKSZ (PAGE_SIZE).
<5> DEBUG_STACK. DEBUG_STKSZ
<6> MCE_STACK. EXCEPTION_STKSZ (PAGE_SIZE).
《Documentation/x86/x86_64/kernel-stacks》(kernel3.10)
4.3.2 单页内核栈
但是,在 2.6 系列内核的早期,引入了一个选项设置单页内核栈。当激活这个选项时,每个进程的内核栈只有一页那么大。
这么做处于两个原因:首先,可以让每个进程减少内存消耗。其次,也是最重要的,随着机器运行时间的增加,寻找两个未分配的、连续的页变得越来越困难。物理内存渐渐变为碎片,因此,给一个新进程分配虚拟内存(VM)的压力也在增加。
每个进程的整个调用链必须放在自己的内核栈中。
《Linux 内核设计与实现》P203
4.3.3 用户栈
进程的用户虚拟地址空间的起始地址是 0 ,长度是 TASK_SIZE ,由每种处理器架构定义自己的宏TASK_SIZE 。 ARM64 架构定义的宏 TASK_SIZE 如下所示。
<1> 32 位用户空间程序: TASK_SIZE 的值是 TASK_SIZE_32 ,即 0x100000000 ,等于 4GB 。
<2> 64 位用户空间程序: TASK_SIZE 的值是 TASK_SIZE_64 ,即 2 的 VA_BITS 次方字节, VA_BITS 是编译内核时选择的虚拟地址位数。
《Linux 内核深度解析》P115
栈的起始地址是 STACK_TOP ,默认启用栈随机化,需要把起始地址减去一个随机值。
如果是 32 位用户空间程序, STACK_TOP 的值是异常向量的基准地址 0xFFFF0000 。
《Linux 内核深度解析》P118
内核线程仅运行在内核态,因此,它们永远不会访问低于 TASK_SIZE 的地址。与普通进程相反,内核线程不用线性区,因此,内存描述符的很多字段对内核线程是没有意义的。
《深入理解 LINUX 内核》P351
防止攻击的地址空间随机化(/proc/sys/kernel/randomize_va_space)
为了使缓冲区溢出攻击更加困难,内核支持为内存映射、栈和堆选择随机的起始地址。进程是否使用虚拟地址空间随机化的功能,由以下两个因素共同决定。
<1> 进程描述符成员 personality (个性化)是否设置 ADDR_NO_RANDOMIZE 。
<2> 全局变量 randomize_va_space : 0 关闭随机化, 1 表示内存映射区域和栈的起始地址随机化, 2 表示内存映射区域、栈和堆的起始地址随机化。可通过文件“ /proc/sys/kernel/randomize_va_space” 修改。
《Linux 内核深度解析》P118
如果全局变量 randomize_va_space 设置为 1 ,则启用地址空间随机化机制。通常情况下都是启用的,但是在Transmeta CPU 上会停用,因为该设置会降低此类计算机的速度。
《深入 LINUX 内核架构》P235
Linux 通过对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局,以免恶意程序通过计算访问栈、库函数等地址。
https://www.cnblogs.com/chyl411/p/4576769.html
4.3.4 节省栈资源
在任意一个栈资源中,你都必须尽量节省栈资源。这并不难,也没什么窍门,只需要在具体的函数中让所有局部变量所占空间之和不要超过几百字节。在栈上进行大量的静态分配是很危险的。要不然,在内核中和在用户空间中进行的栈分配就没什么差别了。
栈溢出时悄无声息,但势必会引起严重的问题。因为内核没有在管理内核栈上做足工作,因此,当栈溢出时,多出的数据就会直接溢出来,覆盖掉紧邻堆栈末端的东西。
首先面临考验的就是 thread_info 结构(这个结构就贴着每个进程内核堆栈的末端)。
在堆栈之外,任何内核数据都可能存在潜在的危险。当栈溢出时,最好的情况是时期宕机,最坏的情况是悄无声息地破坏数据。
《Linux 内核设计与实现》P203
4.4 BSS 段、数据段、代码段、堆和用户空间栈
4.4.1 BSS 段:存放程序中未初始化的全局变量
BSS 是英文 Block Started by Symbol 的简称。BSS 段属于静态内存分配。
https://blog.csdn.net/weixin_38233274/article/details/80321719
BSS(Block Started by Symbol)段中通常存放程序中以下符号:
<1> 未初始化的全局变量和静态局部变量
<2> 初始值为 0 的全局变量和静态局部变量(依赖于编译器实现)
<3> 未定义且初值不为 0 的符号(该初值即 common block 的大小)
https://www.cnblogs.com/chyl411/p/4576769.html
4.4.2 data 数据段:存放程序中已初始化的全局变量
数据段数据静态内存分配。
https://blog.csdn.net/weixin_38233274/article/details/80321719
数据段通常用于存放程序中已初始化且初值不为 0 的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。
https://www.cnblogs.com/chyl411/p/4576769.html
start_data 和 end_data 标记了包含已初始化数据的区域。
在 ELF 二进制文件映射到地址空间之后,这些区域的长度不再改变。
《深入 LINUX 内核架构》P232
4.4.3 text 代码段:存放程序执行代码
这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
https://blog.csdn.net/weixin_38233274/article/details/80321719
可执行代码占用的虚拟地址空间区域,其开始和结束分配通过 start_code 和 end_code 标记。
在 ELF 二进制文件映射到地址空间之后,这些区域的长度不再改变。
《深入 LINUX 内核架构》P232
4.4.4 堆(heap):存放程序运行中动态分配的内存段
它的大小并不固定,可动态扩张或缩减。当进程调用 malloc 等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用 free 等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)若程序员不释放,则会有内存泄漏,系统会不稳定,Windows 系统在该进程退出时由 OS 释放,Linux 则只在整个系统关闭时 OS 才去释放(参考 Linux 内存管理)。
https://blog.csdn.net/weixin_38233274/article/details/80321719
堆的起始地址保存在 start_brk,brk 表示堆区域当前的结束地址。尽管堆的起始地址在进程生命周期中是不变的,但堆的长度会发生改变,因而 brk 的值也会改变。
《深入 LINUX 内核架构》P233
4.4.5 用户空间栈(stack):又称堆栈,存放程序临时创建的局部变量
是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括 static 声明的变量,static 意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
它是由操作系统分配的,内存的申请与回收都由 OS 管理。
https://blog.csdn.net/weixin_38233274/article/details/80321719
栈起始于 STACK_TOP,如果设置了 PF_RANDOMIZE,则起始点会减少一个小的随机量。每个体系结构都必须定义 STACK_TOP,大多数都设置为 TASK_SIZE,即用户地址空间中最高的可用地址。
进程的参数列表和环境变量都是栈的初始数据。
《深入 LINUX 内核架构》P233
持续地重用栈空间有助于使活跃的栈内存保持在 CPU 缓存中,从而加速访问。进程中的每个线程都有属于自己的栈。向栈中不断压入数据时,若超出其容量就会耗尽栈对应的内存区域,从而触发一个页错误。此时若栈的大小低于堆栈最大值 RLIMIT_STACK(通常是 8M),则栈会动态增长,程序继续运行。映射的栈区扩展到所需大小后,不再收缩。
Linux 中 ulimit -s 命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高堆栈容量可能会增加内存开销和启动时间。
https://www.cnblogs.com/chyl411/p/4576769.html
4.4.6 保留区
每个体系结构都指定了一个特定的起始地址:IA-32 系统起始于 0x08048000,在 text 段的起始地址与最低的可用地址之间有大约 128MiB 的间距,用于捕获 NULL 指针。
《深入 LINUX 内核架构》P233
位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。
在 32 位 X86 架构的 Linux 系统中,用户进程可执行程序一般从虚拟地址空间 0x08048000 开始加载。该加载地址由ELF 文件头决定,可通过自定义链接器脚本覆盖链接器默认配置,进而修改加载地址。
https://www.cnblogs.com/chyl411/p/4576769.html
4.4.7 通过 objdump 命令查看可执行文件的不同的段
http://blog.chinaunix.net/uid-27018250-id-3867588.html
4.4.8 通过 size 命令查看可执行文件的 text、data 和 bss 段
# size a.out
text data bss dec hex filename
1365 568 8 1941 795 a.out
#
4.5 pmap 命令(report memory map of a process)
《Linux 内核设计与实现》P255
4.6 使用命令 readelf 查看 ELF 文件的信息
《Linux 内核深度解析》P46
5 父子进程之间的特殊处理
5.1 父进程是否关注子进程的退出事件
当进程退出的时候,根据父进程是否关注子进程的退出事件,处理存在如下差异。
<1> 如果父进程关注子进程退出事件,那么进程退出时释放各种资源,只留下一个空的进程描述符,变成僵尸进程,发送信号 SIGCHLD 通知父进程,父进程在查询进程终止的原因后回收子进程的进程描述符。
<2> 如果父进程不关注子进程退出事件,那么进程退出时释放各种资源,释放进程描述符,自动消失。
《Linux 内核深度解析》P48
5.2 父进程查询子进程终止的原因
waitpid();
waitid();
wait4(); 已经废弃
《Linux 内核深度解析》P48
《Linux 内核深度解析》P53
5.3 孤儿进程选择“领养者”的顺序
《Linux 内核深度解析》P49
6 “0”号进程(idle/swapper)、“1”号进程(init)和“2”号进程(kthreadd)
6.1 “0”号进程:idle(swapper)
所有进程的祖先叫做进程 0,idle 进程或因为历史的原因叫做 swapper 进程,它是 linux 的初始化阶段从无到有创建的一个内核线程。这个祖先进程使用静态分配的数据结构,所有其它进程的数据结构都是动态分配的。
《深入理解 LINUX 内核》P128
在多处理器系统中,每个 CPU 都有一个进程 0。
只要打开机器电源,计算机的 BIOS 就启动一个 CPU,同时禁用其它 CPU。运行在 CPU0 上的 swapper 进程初始化内核数据结构,然后激活其它的 CPU,并通过 copy_process()函数创建另外的 swapper 进程,把 0 传递给新创建的 swapper 进程作为它们的新 PID。此外,内核把适当的 CPU 索引赋给内核所创建的每个进程的 thread_info 描述符的 cpu 字段。
《深入理解 LINUX 内核》P129
6.2 “1”号进程:init
init 进程是用户空间的第一个进程,负载启动用户程序。Linux 系统常用的 init 程序有 sysinit、busybox init、upstart、systemd 和 procd。sysinit 是 UNIX 系统 5(System V)风格的 init 程序,启动配置文件是“/etc/inittab”,用来指定要执行的程序以及在哪些运行级别执行这些程序。
《Linux 内核深度解析》P15
6.3 “2”号进程:kthreadd(负责内核线程的调度和管理)
kthreadd 进程由 idle 通过 kernel_thread 创建,并始终运行在内核空间 , 负责所有内核线程的调度和管理。
它的任务就是管理和调度其他内核线程 kernel_thread, 会循环执行一个 kthread 的函数,该函数的作用就是运行 kthread_create_list 全局链表中维护的 kthread, 当我们调用 kernel_thread 创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以 kthreadd 为父进程。