:从操作系统管理到PCB、fork、进程状态、僵尸与孤儿进程)
Linux进程基础一从操作系统管理到PCB、fork、进程状态、僵尸与孤儿进程 星恒随风个人主页❄️ 个人专栏《指针合集》《C语言基础》《数据结构》《机器学习导论》《前端基础》《python基础》《C从入门到入土》《Linux的学习之旅》✨ 数据即知识压缩即智能文章目录Linux进程基础一从操作系统管理到PCB、fork、进程状态、僵尸与孤儿进程前言学习进程之前为什么要先理解操作系统1. 进程不是孤立的知识点一、从冯诺依曼体系开始理解计算机1. 计算机由哪些基本部分组成1.1 输入设备1.2 输出设备1.3 存储器1.4 CPU二、冯诺依曼体系数据流1. 真正要理解的是数据流2. 为什么CPU不能直接“运行硬盘里的程序”三、用聊天软件理解数据流1. 发送一条消息四、什么是操作系统1. 操作系统的位置2. 操作系统的两个主要目标2.1 对下管理硬件资源2.2 对上提供良好的运行环境五、操作系统的核心任务管理1. 什么叫管理2. 第一步描述3. 第二步组织六、系统调用与库函数1. 系统调用是什么2. 为什么还要有库函数3. 一个简单层次七、什么是进程1. 课本上的定义2. 但“程序”和“进程”并不相同3. 同一个程序可以产生多个进程八、从Linux内核角度理解进程1. 进程不仅只有代码和数据九、PCB是什么1. Process Control Block2. Linux中的PCB十、task_struct中保存什么1. 标识符2. 进程状态3. 调度信息4. CPU上下文5. 内存信息6. I/O信息十一、为什么PCB如此重要十二、操作系统如何组织进程1. 一个task_struct远远不够2. 不同目的使用不同数据结构十三、如何查看Linux中的进程1. /proc目录2. 查看状态3. ps4. top十四、PID和PPID1. PID2. PPID3. 通过系统调用获取十五、为什么进程会有父子关系十六、fork创建子进程1. 基本使用十七、fork最神奇的地方一次调用两次返回1. 返回值规则十八、为什么fork会返回两次1. fork之前2. 内核创建一个新的进程3. 父子进程都从fork之后继续执行十九、fork之后为什么通常必须if分流二十、父进程和子进程谁先运行1. 不确定二十一、fork会把父进程整个内存复制一遍吗1. 逻辑上父子进程拥有独立地址空间2. 但Linux不会立即把所有物理内存复制一份3. 写时拷贝的大致思想4. 当某一方尝试修改二十二、一个验证父子数据独立的实验二十三、进程到底有哪些状态1. 为什么需要“状态”二十四、Linux常见进程状态1. RRunning / Runnable2. SInterruptible Sleep3. DUninterruptible Sleep4. TStopped5. ZZombie6. XDead二十五、如何查看进程状态1. ps2. STAT中的加号是什么意思二十六、什么是僵尸进程1. 子进程退出以后不能直接什么都不留2. 父进程需要“回收”子进程3. 如果父进程一直不回收呢二十七、自己制造一个僵尸进程1. 另开一个终端二十八、僵尸进程到底有什么危害1. 它不是一个还在疯狂运行的进程2. 但内核不能完全删除它3. 大量僵尸会造成资源问题二十九、wait(NULL)到底是什么意思1. 基本代码2. 如果想取得退出状态3. 为什么wait可以消灭僵尸三十、什么是孤儿进程1. 正常情况2. 如果父进程提前退出三十一、孤儿进程会没人管吗三十二、模拟孤儿进程三十三、僵尸进程和孤儿进程不要混淆三十四、为什么会有进程竞争1. CPU数量远少于进程数量三十五、进程独立性三十六、并发和并行1. 并发 Concurrent2. 并行 Parallel3. 二者关系前言学习进程之前为什么要先理解操作系统1. 进程不是孤立的知识点初学 Linux 时我们经常会看到psauxtopkill然后又接触fork();getpid();getppid();很容易把它们理解成一堆Linux命令 几个系统调用实际上它们背后都围绕着同一个核心问题操作系统如何管理正在运行的程序为了真正理解进程需要先建立下面这条知识链冯诺依曼体系 ↓ 计算机硬件 ↓ 操作系统管理硬件 ↓ 操作系统管理程序 ↓ 描述进程 ↓ PCB / task_struct ↓ 组织进程 ↓ 调度、切换、创建、退出一、从冯诺依曼体系开始理解计算机1. 计算机由哪些基本部分组成1.1 输入设备例如键盘 鼠标 网卡 麦克风 摄像头负责向计算机输入数据1.2 输出设备例如显示器 打印机 网卡 音响负责把计算结果输出给外界1.3 存储器这里首先应该理解成内存 RAM而不是硬盘 SSD内存是 CPU 与各种程序数据之间非常重要的中间层。1.4 CPUCPU 中最核心的两类功能运算器 控制器负责执行指令 处理数据 控制程序运行二、冯诺依曼体系数据流1. 真正要理解的是数据流在一个简化模型下输入设备 ↓ 内存 ↕ CPU ↓ 内存 ↓ 输出设备核心思想CPU执行程序时主要围绕内存中的指令和数据进行工作。2. 为什么CPU不能直接“运行硬盘里的程序”假设hello这个可执行程序存放在磁盘。执行./hello不是CPU直接去SSD一条条读取指令更合理的抽象过程是磁盘上的可执行程序 ↓ 操作系统加载 ↓ 映射/装入内存 ↓ CPU取得指令 ↓ 执行因此程序运行和程序文件存在完全是两种状态。这也为后面的程序 vs 进程埋下伏笔。三、用聊天软件理解数据流1. 发送一条消息假设你输入hello简化后的数据流键盘 ↓ 设备驱动 ↓ 内存 ↓ 聊天程序读取 ↓ 程序处理 ↓ 操作系统网络协议栈 ↓ 网卡 ↓ 网络 ↓ 对方网卡 ↓ 对方操作系统 ↓ 对方聊天程序 ↓ 内存 ↓ 显示器可以看到硬件 程序 操作系统 内存一直在协作。因此软件开发到最后一定会遇到谁来协调这么多软件和硬件资源答案操作系统四、什么是操作系统1. 操作系统的位置可以把计算机软件栈简化成用户 ↓ 应用程序 ↓ C/C库 ↓ 系统调用接口 ↓ ┌──────────────┐ │ 操作系统内核 │ └──────────────┘ ↓ ↓ ↓ CPU 内存 外设也就是2. 操作系统的两个主要目标2.1 对下管理硬件资源例如CPU 内存 磁盘 网卡 键盘 显示器2.2 对上提供良好的运行环境应用程序不需要自己直接操作磁盘控制器 直接控制CPU调度 直接控制内存芯片而是通过操作系统提供的接口完成工作。五、操作系统的核心任务管理1. 什么叫管理可以把管理抽象成两个步骤先描述 ↓ 再组织2. 第一步描述假设操作系统要管理一个进程。它必须先记录PID是多少 当前状态是什么 优先级是多少 用了哪些内存 打开了哪些文件 CPU执行到哪里 父进程是谁那么在 C 语言实现的内核中最自然的方法就是创建一个结构体structProcess{intpid;intstate;intpriority;// ...};即结构体描述对象3. 第二步组织系统中可能有100个进程 1000个进程 数万个任务不能让这些结构体散落在那里。必须用链表 队列 树 哈希结构 位图等数据结构组织。因此操作系统管理问题最终经常变成数据结构 算法问题。六、系统调用与库函数1. 系统调用是什么操作系统虽然管理硬件但应用程序不能随意进入内核修改数据。操作系统会提供一组受控制的入口System Call 系统调用例如fork read write open close2. 为什么还要有库函数系统调用通常比较底层。于是 C 标准库、glibc 等会进行进一步封装。例如我们写printf(hello\n);并不是说printf 本身就是Linux系统调用它是C库函数内部最终可能借助write等系统调用完成输出。3. 一个简单层次应用程序 ↓ printf() ↓ C Library ↓ write() ↓ System Call ↓ Kernel ↓ 设备所以库函数 ! 系统调用七、什么是进程1. 课本上的定义常见定义进程是程序的一次执行实例。或者进程是正在执行的程序。这两个说法都可以帮助入门。2. 但“程序”和“进程”并不相同程序静态例如磁盘/home/user/test只是一个可执行文件进程动态它包含正在执行的代码 数据 CPU上下文 内存空间 打开的文件 状态 PID 其他内核管理信息3. 同一个程序可以产生多个进程例如./test./test./test虽然磁盘上只有一个test但系统中可能存在Process A Process B Process C它们代码来源相同但是PID不同 状态不同 地址空间不同 执行进度不同因此程序 ≠ 进程八、从Linux内核角度理解进程1. 进程不仅只有代码和数据对于 Linux 来说必须有内核对象描述这个进程。教学上可以写成进程 进程对应的代码和数据 描述进程的内核数据结构Linux 中非常核心的数据结构就是task_struct九、PCB是什么1. Process Control BlockPCBProcess Control Block中文进程控制块可以理解成操作系统用于描述和管理一个进程的数据结构。2. Linux中的PCBLinux 内核中task_struct承担了 PCB 的核心角色。可以粗略想象structtask_struct{pid_t pid;longstate;intpriority;// 内存信息// CPU上下文// 文件信息// 信号信息// 父子关系// 调度信息// ...};实际结构远比这里复杂得多。十、task_struct中保存什么1. 标识符例如PID用于区分不同任务。2. 进程状态例如Running Sleeping Stopped Zombie3. 调度信息例如优先级 调度策略 CPU时间4. CPU上下文CPU运行程序会不断改变寄存器 程序计数器 栈指针当进程被切走时这些信息必须能够恢复。5. 内存信息例如代码区域 数据区域 堆 栈 内存映射6. I/O信息例如打开的文件 使用的设备 文件描述符十一、为什么PCB如此重要假设 CPU 正在执行Process A突然需要切换Process A ↓ Process B那么以后重新运行 A 时CPU必须知道A之前执行到了哪里这些信息不能凭空出现。所以需要 PCB 保存进程上下文这就是进程存在不仅意味着代码还在还意味着描述进程的内核信息仍然存在十二、操作系统如何组织进程1. 一个task_struct远远不够系统中有task_struct A task_struct B task_struct C task_struct D ...内核必须把它们组织起来。2. 不同目的使用不同数据结构教学时经常画成task_struct ↕ task_struct ↕ task_struct帮助理解进程被内核组织管理实际现代 Linux 内核中一个任务会参与多种组织结构例如任务关系链 调度队列 PID索引结构 等待队列 红黑树 链表具体使用哪种结构取决于我们想解决什么管理问题这再次体现先描述 再组织十三、如何查看Linux中的进程1. /proc目录Linux 提供/proc伪文件系统。例如 PID 为1234可以ls/proc/1234里面能看到大量进程信息。2. 查看状态例如cat/proc/1234/status3. ps常见psaux或者psaxj4. toptop可以动态观察PID CPU利用率 内存 状态 运行时间等信息。十四、PID和PPID1. PIDPIDProcess ID即进程标识符2. PPIDPPIDParent Process ID即父进程PID3. 通过系统调用获取#includestdio.h#includeunistd.hintmain(void){printf(pid %d\n,getpid());printf(ppid %d\n,getppid());return0;}编译gcc process.c-oprocess运行./process十五、为什么进程会有父子关系在 Unix/Linux 中一个非常重要的进程创建方式是fork()当前进程Parent创建Child于是系统中的进程自然形成了某种父子关系十六、fork创建子进程1. 基本使用需要#includeunistd.h执行pid_t idfork();如果成功原来一个执行流 ↓ 变成两个执行流分别是父进程 子进程十七、fork最神奇的地方一次调用两次返回1. 返回值规则pid_t idfork();if(id0){// fork失败}elseif(id0){// child}else{// parent}其中父进程 fork返回子进程PID 子进程 fork返回0 失败 返回-1十八、为什么fork会返回两次1. fork之前只有Parent执行pid_t idfork();2. 内核创建一个新的进程可以粗略理解Parent task_struct ↓ 创建 Child同时建立子进程需要的内核管理信息 地址空间信息 文件表引用 执行上下文3. 父子进程都从fork之后继续执行于是fork() | ┌───┴───┐ ↓ ↓ Parent Child | | return PID return 0所以不是一个C函数在同一个进程里神秘地return两次而是fork之后已经存在两个不同的执行流每个执行流各自从fork返回。十九、fork之后为什么通常必须if分流例如#includestdio.h#includeunistd.hintmain(void){pid_t idfork();if(id0){perror(fork);return1;}elseif(id0){printf(child: pid%d, ppid%d\n,getpid(),getppid());}else{printf(parent: pid%d, child%d\n,getpid(),id);}return0;}因为父子进程从同一份代码继续运行我们需要通过fork返回值告诉它们你们之后分别应该做什么二十、父进程和子进程谁先运行1. 不确定例如if(id0){printf(child\n);}else{printf(parent\n);}不能保证parent child或者child parent哪一个先输出。因为父子进程都处于可运行状态最终谁先获得CPU由操作系统调度器决定。所以不要通过sleep猜进程执行顺序。真正需要同步时应使用wait 管道 信号量 互斥机制 条件变量 IPC等机制。二十一、fork会把父进程整个内存复制一遍吗1. 逻辑上父子进程拥有独立地址空间fork 后Parent和Child在语义上拥有彼此独立的内存空间子进程修改自己的普通内存不会直接修改父进程相应变量2. 但Linux不会立即把所有物理内存复制一份这会非常浪费。Linux 通常采用Copy-On-Write 写时拷贝3. 写时拷贝的大致思想fork 刚完成父虚拟页 ─┐ ├──→ 同一物理页 子虚拟页 ─┘只读时可以暂时共享4. 当某一方尝试修改例如子进程g_val100;此时触发写操作 ↓ 操作系统发现页面共享 ↓ 复制页面 ↓ 建立新的映射变成父虚拟页 ───→ 物理页A 子虚拟页 ───→ 物理页B之后互不影响二十二、一个验证父子数据独立的实验#includestdio.h#includeunistd.hintg_val10;intmain(void){pid_t idfork();if(id0){g_val100;printf(child: g_val%d\n,g_val);}elseif(id0){sleep(2);printf(parent: g_val%d\n,g_val);}return0;}典型结果child: g_val100 parent: g_val10说明父子进程地址空间彼此独立二十三、进程到底有哪些状态1. 为什么需要“状态”CPU数量有限。一个系统可能有几百 几千 甚至更多任务。任何时刻不可能所有进程都真正占用CPU所以进程需要不同状态描述现在能不能运行 在等什么 是否停止 是否已经退出二十四、Linux常见进程状态1. RRunning / RunnableR不一定意味着这一瞬间正在CPU上运行也可能表示已经具备运行条件 正在运行队列中等待CPU所以更准确地理解Running / Runnable2. SInterruptible SleepS表示可中断睡眠通常进程正在等待某个事件例如等待输入 等待定时器 等待某些资源3. DUninterruptible SleepD常见于等待某些I/O也叫不可中断睡眠4. TStoppedT表示进程被停止例如kill-STOPPID恢复kill-CONTPID5. ZZombieZ即僵尸进程它已经结束执行但内核还保留部分退出信息。6. XDead这是死亡状态生命周期非常短一般很难通过普通ps观察到。二十五、如何查看进程状态1. ps例如psaux或者更适合实验ps-o\pid,ppid,stat,cmd输出可能是PID PPID STAT CMD 3210 3000 S ./test2. STAT中的加号是什么意思例如S R其中S / R表示主状态。表示进程位于前台进程组因此R可以粗略理解成可运行 属于当前终端前台进程组二十六、什么是僵尸进程1. 子进程退出以后不能直接什么都不留子进程结束时可能产生退出码 退出原因 资源使用统计父进程可能需要这些信息。因此内核不能在子进程执行exit(...)之后立刻把所有信息彻底删除。2. 父进程需要“回收”子进程典型系统调用wait()或者waitpid()用于取得子进程退出状态并完成回收。3. 如果父进程一直不回收呢那么子进程已经退出 ↓ 但父进程没有wait ↓ 内核必须保存退出信息 ↓ 子进程进入Z状态这就是僵尸进程二十七、自己制造一个僵尸进程#includestdio.h#includestdlib.h#includeunistd.hintmain(void){pid_t idfork();if(id0){perror(fork);return1;}elseif(id0){printf(child pid%d exit...\n,getpid());exit(0);}else{printf(parent pid%d, child%d\n,getpid(),id);sleep(30);}return0;}编译gcc zombie.c-ozombie运行./zombie1. 另开一个终端ps-o\pid,ppid,stat,cmd可能看到PID PPID STAT CMD ... 1235 1234 Z [zombie] defunct其中Z defunct就是典型僵尸状态。二十八、僵尸进程到底有什么危害1. 它不是一个还在疯狂运行的进程僵尸进程已经停止执行所以并不会继续占CPU做计算2. 但内核不能完全删除它因为仍然需要保存PID 退出码 部分task信息 统计信息供父进程获取。3. 大量僵尸会造成资源问题如果父进程不断创建子进程 ↓ 子进程退出 ↓ 永远不wait就可能积累大量Zombie持续占用PID 进程表项 内核记账信息最终可能影响系统继续创建新进程。严格来说把它简单称为普通的堆内存泄漏并不够准确。更合适的说法是父进程没有及时回收子进程导致内核为已经退出的子进程维护的资源无法释放。二十九、wait(NULL)到底是什么意思1. 基本代码#includesys/wait.hwait(NULL);意思可以简单理解成等待一个子进程结束并将它回收但我不关心它具体的退出状态。2. 如果想取得退出状态intstatus0;pid_t retwait(status);后面可以使用WIFEXITED WEXITSTATUS等宏分析。3. 为什么wait可以消灭僵尸因为child退出 ↓ 退出信息被内核保存 ↓ parent调用wait ↓ 读取退出信息 ↓ 内核可以彻底清理子进程三十、什么是孤儿进程1. 正常情况Parent | └── Child2. 如果父进程提前退出Parent ↓ exit Child 仍然运行此时子进程就称为孤儿进程三十一、孤儿进程会没人管吗不会。传统 Unix 教学中通常说孤儿进程 ↓ 被PID 1进程接管现代 Linux 中说得更严谨一点孤儿后代可能被一个subreaper接管如果没有更近的子进程收割者通常最终由PID 1承担相应角色。在采用 systemd 的系统中 PID 1 通常就是systemd三十二、模拟孤儿进程#includestdio.h#includestdlib.h#includeunistd.hintmain(void){pid_t idfork();if(id0){perror(fork);return1;}elseif(id0){for(inti0;i10;i){printf(child: pid%d, ppid%d\n,getpid(),getppid());sleep(1);}}else{printf(parent: pid%d\n,getpid());sleep(3);exit(0);}return0;}观察子进程的PPID可能在父进程退出前后发生变化。三十三、僵尸进程和孤儿进程不要混淆对比僵尸进程孤儿进程自己是否已经结束是通常没有父进程是否存在通常存在原父进程已退出是否继续执行否可以核心问题父进程没回收父进程先结束处理wait/waitpid被subreaper/PID1接管一句话Zombie 子先死父没收尸 Orphan 父先走子还活着三十四、为什么会有进程竞争1. CPU数量远少于进程数量例如系统中 500个可运行任务 CPU 8个核心不可能同时真正运行500个任务所以进程之间会竞争CPU时间这就是竞争性三十五、进程独立性不同进程原则上拥有相对独立的地址空间 独立PCB 独立执行上下文一个普通进程不能随便直接读写另一个进程的内存这种隔离极大提高了系统稳定性 安全性三十六、并发和并行1. 并发 Concurrent假设只有一个CPU核心运行Process A Process B Process C可能是时间 → A A | B B | C | A | B | C C任意一个极短瞬间可能只有一个进程在CPU上执行但一段时间来看三个进程都在推进这就是并发2. 并行 Parallel如果有多个 CPU 核心CPU0 → Process A CPU1 → Process B CPU2 → Process C同一时刻真的有多个任务执行并行3. 二者关系并发 强调 多个任务在一段时间内共同推进 并行 强调 多个任务在同一时刻真正执行多核系统中并发与并行可以同时存在