进程创建与退出的原理
目录
一、task_struct的创建与链接
二、内核处理进程退出的主要步骤
在所有的操作系统中,都遵循着一个“先描述,后管理”的原则。所谓描述就是把一个抽象的状态(如硬件资源、运行状态等)用一个结构体来表示,以后只要状态发生了改变就必须先修改这个结构体中对应的成员;而管理则是说有着许许多多的硬件状态,不能孤立的存在而应该放在一个统一的容器中存放,想要查询、修改哪一个硬件只需要到这个“档案室”中找到其结构体,并进行修改。
本篇文章我们只讨论进程的描述结构体,即进程控制块(Process Control Block),在不同的操作系统中PCB有着不同的叫法,而在Linux中我们称之为task_struct。
一、task_struct的创建与链接
既然我们说所有的东西都要先描述再管理,那么进程也不例外。当使用fork函数创建一个进程的时候,操作系统内核首先就会创建一个task_struct结构体。这个结构体由于需要操作系统管理,则必定是在内核空间中的。即全局的进程链表。
当task_struct已经被链入到内核的全局链表中了,则会开始填充骨肉。
最后会把该task_struct链入运行队列中,让调度器能够管理其运行。
二、内核处理进程退出的主要步骤
进程退出通常分为正常退出(进程本身通过return或exit函数退出)和异常退出(运行时收到了外部的终止信号、或运行错误)
- (1)释放资源:
- 内存资源:进程占用的用户空间内存(包括堆、栈、数据段等 )会被释放。内核会根据进程
task_struct
中记录的内存描述符(mm_struct
)信息,回收分配给该进程的虚拟内存区域和对应的物理内存页框。如果存在共享内存,会检查引用计数,当引用计数为 0 时,释放共享内存资源。- 文件描述符:关闭进程打开的所有文件描述符。内核会遍历进程
task_struct
中的文件描述符表,调用相应的文件系统操作来关闭文件,释放相关的文件资源,如文件锁、缓存等 。- 其他资源:例如进程创建的管道、信号量、消息队列等进程间通信资源,也会根据情况进行释放或清理。
- (2)更新父进程信息:子进程需要向父进程报告自己的退出状态。如果父进程调用了
wait
或waitpid
函数等待子进程结束,那么父进程会获取子进程的退出状态。同时,内核会在父进程的task_struct
中更新子进程的相关信息,比如从父进程记录子进程的链表中移除该子进程节点 。- (3)从运行队列移除:如果进程在退出前处于运行队列(就绪态 ),内核会将其
task_struct
从运行队列中移除。以 Linux 内核的 CFS(完全公平调度器 )为例,会操作运行队列对应的红黑树,将代表该进程的调度实体(sched_entity
,在task_struct
中 )从红黑树中删除,这样调度器就不会再调度该进程运行。- (4)从等待队列移除:若进程处于阻塞态,在等待某个事件(如 I/O 操作完成、信号量获取等 ),它会在相应的等待队列中。内核会找到该进程所在的等待队列,将其
task_struct
从队列中移除,避免后续该进程被错误唤醒。- (5)从全局进程链表移除:Linux 内核使用双向循环链表管理所有进程,以
init_task
作为链表头。当进程退出时,会将其task_struct
中的tasks
节点(用于连接到全局进程链表的节点 )从链表中摘除,具体操作是修改前后节点的next
和prev
指针,使链表中不再包含该进程的task_struct
。- (6)处理僵尸状态(Zombie State):如果父进程没有调用
wait
或waitpid
来获取子进程的退出状态,子进程会进入僵尸状态,此时子进程的task_struct
不会立即被释放,而是会保留在内存中,等待父进程获取其退出状态。不过,即使处于僵尸态,它也已经从运行队列、等待队列等功能性队列中移除,只是还在全局进程链表中,直到父进程调用wait
系列函数后,内核才会彻底释放该task_struct
占用的内存。
值得注意的是:当一个进程退出后,会被标记为僵尸状态,且从运行队列、阻塞队列中移除。但是并未移出全局进程链表以及父进程的子进程链表,只有当父进程使用wait或waitpid后才会在子进程链表中找到该pcb,获取退出信息后才销毁。