摘要:本文围绕操作系统中的进程这一核心概念展开,系统讲解了进程的基本定义与 PCB(Linux 中为 task_struct)数据结构,深入分析了进程的五种经典状态及其在 Linux 内核源码中的真实实现,并重点介绍了僵尸进程与孤儿进程的产生原因、验证方法及潜在危害,帮助读者建立对进程生命周期管理的完整认识。
一、进程的基本概念
1.1 什么是进程
进程是操作系统对运行中程序的抽象,是资源分配和调度的独立单位。
举个简单例子:运行磁盘上的 exe 文件时,系统会先在内核创建专属 PCB(内核用来描述进程的数据结构)。随后向内存申请空间,将程序的代码和数据载入内存。PCB 加上内存里的代码、数据,整体就叫做进程。
1.2 描述进程的数据结构--PCB
我们的进程信息被放在一个叫做进程控制块的数据结构PCB(process control block)中,可以将其理解为进程属性的集合。而PCB是相对于操作系统来说的广义概念,在Linux中,我们的PCB叫做task_struct
struct task_struct { pid_t pid; // PID 进程ID pid_t ppid; // 父进程PID volatile long state; // 进程状态(R/S/D/T/Z状态) int exit_code; // 子进程结束后存放退出码 int exit_signal; // 子进程结束后存放终止信号 struct mm_struct *mm; // 指向进程虚拟内存描述符(用户空间代码、堆、栈都由它管理) int prio; //优先级 struct files_struct *files; // 进程打开的文件描述符表 struct thread_struct thread; // 寄存器上下文(进程切换时保存CPU现场) };二、进程状态
2.1 进程的五种状态
状态可大概分为五种:新建、就绪、运行、阻塞(等待)、终止。
新建态(New)
操作系统刚创建进程,分配 PCB,还没加载完程序代码与数据,尚未准备好上 CPU。
就绪态(Ready)
进程一切资源准备好了,只差 CPU;一旦 CPU 空闲,调度器就可以选中它运行。
对应 Linux:R(TASK_RUNNING,就绪 + 运行都归为 R)
运行态(Running)
进程正在 CPU 上执行指令。
Linux 中,就绪和运行在state里统一标记为 R,靠调度器区分谁正在占用 CPU。
阻塞态/ 等待态(Waiting)
进程等待某个事件(IO、信号),主动让出 CPU,不再参与调度。
细分就是:S 可中断睡眠(TASK_INTERRUPTIBLE)
D 不可中断睡眠(TASK_UNINTERRUPTIBLE)
终止态(Terminated)
进程代码、数据资源全部释放,只剩 PCB 保存退出信息,也就是僵尸 Z 状态,等待父进程 wait 回收 PCB。
另外经典操作系统课本的五状态模型,是理论模型; Linux 真实实现把阻塞拆成 S/D,额外还有暂停 T、僵尸 Z、死亡 X,和理论模型不完全一一对应。
关于挂起:当我们的操作系统发现内存空间不足时,为了空出所用内存,会将结构体task_struct中的数据和代码交换到磁盘中的swap分区,最先交换的是处于阻塞状态的PCB,再接着是未被调用的运行状态的PCB,最后实在没有空间时,操作系统会选择的杀死特定的进程(可见尽量不要过度使用内存)。在交换时会进行多次内存和磁盘的I/O,而这多次的I/O会浪费很多时间,因此这是一个以时间换空间的方法。
2.2 源码中的状态
为了弄明白正在进行的进程是什么意思,我们需要知道进程的不用状态。一个进程可以有几个状态(在Linux内核中,进程有时也叫任务)
static const char *const task_state_array[] = { "R (running)", /*0 */ "S (sleeping)", /*1 */ "D (disk sleep)", /*2 */ "T (stopped)", /*4 */ "t (tracing stop)", /*8 */ "X (dead)", /*16 */ "Z (zombie)", /*32 */ };R 运行状态(running)
并不意味着进程⼀定在运⾏中,它表明进程要么是在运⾏中要么在运⾏队列⾥。
S 睡眠状态(sleeping)
意味着进程在等待事件完成(这⾥的睡眠有时候也叫做可中断睡眠)。
D 磁盘休眠状态(Disk sleep)
有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
T 停止状态(stopped)
可以通过发送 SIGSTOP 信号给进程来停⽌(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运⾏。
X 死亡状态(dead)
这个状态只是⼀个返回状态,你不会在任务列表⾥看到这个状态。
下面特别介绍两种状态:僵尸状态(Z)和孤儿状态
2.2.1 僵尸进程(Z zombie)
僵尸状态(Zombies)是一种比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。
下面我们进行验证:
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<sys/types.h> 4 5 int main() 6 { 7 pid_t id = fork(); 8 9 if(id == 0) 10 { 11 while(1) { 12 printf("I am 子进程,我的pid为:%d,ppid: %d\n",getpid(), getppid()); 13 sleep(1); 14 } 15 } 16 else { 17 while(1) { 18 printf("I am 父进程,我的pid为:%d,ppid: %d\n",getpid(), getppid()); 19 sleep(1); 20 } 21 } 22 return 0; 23 }先通过fork函数创建一个子进程:在这个程序中就同时存在父进程和子进程。
根据id的不同值,死循环的让父进程和子进程不间断的运行,同时在另一个终端中进行监控。
用skill -9 关闭子进程并不断打印列表进行监控。
while :;do ps axj|head -1 && ps axj|grep app.exe|grep -v grep; sleep 1;echo "-------------------";done可以发现子进程61857的状态变成了Z(僵尸状态)。
僵尸状态会以终止状态保持在进程表中,并且会⼀直在等待父进程读取退出状态代码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进⼊Z状态。
僵尸进程的危害:
僵尸进程会保留PCB来存储进程退出状态,用于向父进程汇报任务执行情况。如果父进程长期不调用wait系列函数读取子进程的退出信息,子进程将持续处于僵尸状态,对应的PCB就无法被内核释放。大量僵尸进程堆积时,大量PCB结构体持续占用内核内存,造成内核内存泄漏,消耗系统的进程资源,严重时会导致系统无法创建新进程。
2.2.2 孤儿状态
如果说僵尸状态是子进程结束没有被父进程读取,而一直以终止状态保存在PCB中,那么孤儿状态就是父进程新进行退出,让子进程变成“ 孤儿进程 ”,其会被1号的init/systemd(系统)进程自动领养,为孤儿进程的退出进行善后
三、小结
本文围绕进程这一操作系统核心概念展开,主要涵盖以下内容:
进程的基本概念:进程是操作系统对运行中程序的抽象,由 PCB(Linux 中为 task_struct)加上内存中的代码和数据构成;内核通过"先描述,再组织"的方式统一管理进程。
进程的五种状态:新建、就绪、运行、阻塞(等待)、终止;Linux 真实实现将阻塞细分为 S 可中断睡眠和 D 不可中断睡眠,并额外包含 T 停止、Z 僵尸、X 死亡等状态。
源码中的状态定义:通过 task_state_array 数组展示了 R、S、D、T、t、X、Z 等状态在内核中的具体定义与含义。
僵尸进程:子进程退出后父进程未读取退出码时产生,会长期占用 PCB 导致内核内存泄漏,危害系统资源。
孤儿进程:父进程先退出后,子进程被 1 号 init/systemd 进程领养,并自动转为后台进程。