课程: 宋宝华老师的进程课程
kernel version | arch |
---|---|
v4.4.0 | arm32 |
进程
首先需要明确的是:进程是资源分配的基本单位,线程是调度的基本单位。同一进程里的线程之间共享进程的资源。
进程的表示
在Linux内核中,使用结构体(PCB) task_struct
(位于 include/linux/sched.h)来表明一个进程,其中不仅包括了此进程的资源,还有其状态、优先级等参数。
1 | struct task_struct { |
此结构体中就包含了内存资源、文件系统路径、打开的文件资源等:
1 | struct fs_struct { |
进程的限制
系统中可以创建的进程总数是有限的,同理单个用户可以创建的进程数也是有限的。
- 用户可以使用
ulimit -u
查看限制的进程数,也可以使用getrlimit(),setrlimit()
来得到或设置资源
1 |
|
fork bomb
由于系统的进程是有限的,如果无限制的创建进程,那最终将导致进程被耗光,也就相当于系统资源被消耗完而出现系统死掉的现象。
fork bomb 就是基于这个原理做出来的。
android 提权漏洞
提权漏洞 就是因为pid会被恶意消耗完,而代码没有检查自己降权成功而导致的root权限问题。
进程的链接
Linux内核以三种数据结构来链接进程PCB:
- 链表 : 用于遍历所有进程
- 树 : 用于查看进程的继承关系
- 使用命令
pstree
可以查看进程的树形结构
- 使用命令
- 哈希表 : 用于快速查找出进程
进程的树形结构
进程是以树的形式创建的,也是基于这个关系使得父进程可以监控子进程。
- 当子进程意外退出后,父进程可以获取其退出的原因并且重新启动它。
进程的状态

注意:
- Linux的调度算法仅针对就绪态和运行态的调度!
- 内核以
task_struct
为单位进行调度!
理解僵死态
僵死态就是进程已经退出, 其占有的资源已经被释放,但父进程还没有清理其PCB时的一个状态 。
当父进程清理子进程PCB后(通过 waitpid
实现),那么对于该进程的所有痕迹都被清除了。
- 只要进程一退出,其所占有的所有资源都被释放了,所以不用担心代码里面动态申请的内存还未来得及释放
- 如果父进程没有清理子进程,那么其最后的PCB就代表它的尸体存在。
- 可以通过
ps -aux
命令来查看其状态。
- 可以通过
可以通过以下代码来理解:
1 |
|
可以看到:
- 当父进程使用
waitpid()
时,外部使用kill
命令后,使用ps -aux
看不到子进程的任何痕迹 - 当父进程没有使用
waitpid()
来清除子进程的僵死态时,使用ps -aux
看到其状态是Z+
。- 当父进程被终止后,其僵死态也消失了。
理解内存泄露
根据上面对僵死态的理解,可以知道 只要进程退出,就会释放其所占的资源,也就没有所谓的内存泄露
内存泄露指的是: 在进程运行时 其所占用的内存随着时间的推移在震荡的上升。
- 正常的进程所占用的内存应该是在一个平均值周围震荡。
理解停止态
停止态用于 主动暂停进程 ,有点类似于给这个进程打了一个断点(此进程已经不占用CPU资源)。在需要其运行的时候,又可以让其继续运行。
- 睡眠是进程没有获取到资源而 主动让出CPU
- 在shell中可以使用
Ctrl + Z
来让一个进程进入停止状态,使用fg
来让其再次前台运行,bg
进入后台运行- 也可以使用
cpulimit
命令来限制某个进程的利用率,其内部就是在让进程间歇性的进入停止态以控制其CPU利用率
- 也可以使用
理解睡眠
当一个进程在等待资源时便会进入睡眠态,一般情况下都会设置为浅度睡眠,只有在读写块设备这种情况才会深度睡眠。
睡眠的底层实现,是将 task_struct
放入等待队列中,然后在接收到信号或资源可用来唤醒此队列中的一个进程。
fork()
fork()的作用是在一个进程的基础上为其分裂出一个子进程,其内部是为子进程单独分配了一个 task_struct
的PCB。
此时两个进程分别通过fork()来返回,父进程中fork()返回子进程的pid,子进程中的fork()返回0。
父进程与子进程资源->fork()
当父进程通过 fork() 创建子进程时,子进程除了拥有一个PCB外,也具有与父进程 一样的资源 (内存、文件系统、文件、信号等)。
在接下来的过程中,父子进程可以分别单独的修改自己的资源,二者并不会冲突。
在实现逻辑的过程中,内存资源的分离是基于 具有MMU支持的COW技术 来实现的。
- 在 fork() 前,内存资源是可读可写的
- 在 fork() 后二者的内存资源都 变为只读 的,此时父子进程对应内存的虚拟地址和物理地址都是一致的
- 父或子进程的其中一个修改内存时,便会触发MMU的 pagefault
- 然后内核会为此进程访问的内存重新申请页表,让其对应到另一个物理地址
- 最后父子进程虽然虚拟地址一样,但它们对应的物理地址就不一样了,并且它们的内存资源权限又恢复为可读可写了
- 最终此进程的内存修改才正式生效
所有在内存分离时,最开始的操作是比较耗时的!
验证内存分离的代码如下:
1 |
|
父进程与子进程资源 -> vfork()
当硬件中没有MMU支持时,父进程通过vfork()来创建子进程,子进程拥有一个新的PCB,此时二者是具有 完全一样的内存资源(但文件系统、文件、信号等资源是分离的) ,且 无法完成内存分离 。
- 所以,无论是父还是子修改了内存,这些修改对于另一方是可见的
- 如果子进程没有退出,父进程是无法运行的。
父进程与子进程资源 -> clone()
父进程通过clone()来创建子进程,子进程拥有一个新PCB,此时二者是具有 完全一样的所有资源,也就是共享所有资源 , 那就是一个线程了!
- 子进程的资源指针直接指向父进程的资源
- pthread_create()的底层就是由clone()所支持的
孤儿
实例展示
1 |
|
通过上面的代码查看,当kill掉父进程以后,子进程的 parent pid 会变为另外一个进程的pid。
- 此父进程有可能是init进程,也可能是具有subreaper属性的进程。
- 这要根据子进程是否挂接在各自的链表中

subreaper
subrepaer 是在3.4后引入的,当将进程设置为 repaer 时需要注意wait子进程,以回收它的PCB。
1 |
|
根进程
Linux在启动的过程会创建进程0,此进程0会创建init进程1,此后所有的进程都是挂接在init进程下的。
进程0在完成创建init进程后,会设置自己的优先级为最低,也就是将自己退化成了idle进程。 当其他进程都不占有CPU时,idle进程会运行,并将CPU置为低功耗模式。 当接收到中断后,如果有其他进程调度便又会将CPU让给其他进程。
线程
由上面的 clone()
可以看出:在Linux中创建线程实际上在内核也会位置分配一个 task_struct
,但它们的资源都指向同一个地址。
而 task_struct
中具有pid,这样在创建线程的同时也创建了多个pid。
tgid
为了符合操作系统中关于线程的要求:一个进程中的多个线程所访问的pid都是一致的。
Linux内核使用 TGID(thread group ID),来使得上层调用 getpid()
时获取的pid,都是最初此进程的pid,而其他的pid被掩盖了。
- 可以在shell中访问
/proc/[pid]/task/
中看到几个被掩盖的pid- 也可以使用
top -H
来查看各个线程对应的Pid
- 也可以使用
- 在编程时,使用系统调用
syscall(__NR_gettid)
来获取自己真实的pid
1 |
|