🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022
文章目录
深入理解 Linux 进程管理
一、管理哲学/本质:先描述,再组织(校长如何管理学校?)
一个操作系统不仅仅只能运行一个进程,可以同时运行多个进程。操作系统的进程管理:先描述,在组织 → 任何一个进程。在操作系统中,管理任何对象,最终都可以转化成为对某种数据结构的增删查改。想象你是一所大学的校长,管理数万名学生。你不需要认识每个学生,只需通过 学生档案系统 管理:
- 描述:每个学生有专属档案(学号、姓名、专业、成绩、宿舍号…)
- 组织:档案按学院 → 专业 → 班级形成 链表结构
- 管理:调整专业只需修改档案中的“专业”字段,开除学生只需删除对应档案(增删查改)
操作系统管理进程同理:
- 描述:为每个进程创建
task_struct
(进程的“档案”) - 组织:通过链表、队列等数据结构管理所有
task_struct
- 控制:调整优先级、终止进程等操作只需修改对应结构体
数据结构的作用
- 学校:学生档案链表 → 管理学生 增删查改(如入学、转班、退学)。
- 操作系统:进程 PCB 链表 → 管理进程的创建、调度、终止等。
二、进程的定义与核心概念
1. 什么是进程?
- 进程 = 程序员自己写的代码和数据 + 内核 PCB 数据结构对象(描述进程的所有数据,process ctrl block → 进程控制块)
- 程序:程序员编写的代码(如
hello.c
编译后的可执行文件hello
)。 - 进程:程序被加载到内存中运行的实例(如运行
./hello
后生成的进程)。
- 程序:程序员编写的代码(如
- 关键点:
- 进程是动态的:程序本身是静态文件,进程是运行时的实体。
- 每个进程都有自己的 PCB(进程控制块),存储进程的所有信息。
2. 进程管理的核心:进程控制块(PCB)
- PCB 的作用:记录进程的所有属性,帮助内核管理进程。
- Linux 中的 PCB:名为
task_struct
的结构体(C 语言中的结构体)。 - 类比:就像学生的学籍档案,记录了学号、成绩、班级、状态等。
三、Linux 的 task_struct
详解(进程控制块的实现)
1. task_struct
的结构(进程的 DNA)
在 Linux 内核中,进程属性的集合 task_struct
是一个庞大的结构体(约有数百个字段),task_struct
是 Linux 内核的一种数据结构,它会被装载到 RAM(内存)里并且包含着进程的信息。
// 简化版 task_struct 结构(Linux内核真实结构复杂得多)
struct task_struct
{
long pid; // 身份证号(进程唯一标识)
volatile long state; // 当前状态(睡觉/跑步/待机)
int priority; // VIP等级(优先级)
struct mm_struct *mm; // 家庭住址(内存分配情况)
struct files_struct *files; // 随身物品(打开的文件)
// 相关指针信息:
structPCB *next
struct PCB *queue
struct PCB *xxxxx
// ... 上百个字段
};
关键字段解析(下面详解):
字段 | 类比 | 作用 |
---|---|---|
pid | 学号 | 唯一标识进程(ps -ef 查看) |
state | 学生状态(上课/请假) | 进程状态:运行(R)、睡眠(S)、僵尸(Z)等 |
priority | 奖学金等级 | 优先级高的进程优先使用 CPU(nice 命令调整) |
mm_struct | 宿舍分配表 | 记录进程使用的内存空间(代码段、数据段、堆栈等) |
files_struct | 书包里的书本 | 记录进程打开的所有文件(lsof -p <pid> 查看) |
上下文数据 | 课堂笔记 | 保存 CPU 寄存器的值,用于中断后恢复现场 |
1. 标识符(Identity)
- pid(进程 ID):唯一标识符,类似学生的学号。每个进程有唯一的 PID,通过
ps
命令可见。 - ppid(父进程 ID):记录该进程的父进程 PID,形成父子进程关系。类比:学生档案中记录班主任的工号。
2. 状态(State)
进程状态:进程当前所处的阶段,包括:
- 运行态(Running):正在 CPU 上执行。
- 就绪态(Runnable):等待调度到 CPU 上运行。
- 阻塞态(Blocked):等待 I/O(如读写文件)、等待信号等。
- 终止态(Zombie):进程已结束,但父进程未回收其资源(成为“僵尸”)。
字段示例:
volatile long state; // 进程状态(如TASK_RUNNING, TASK_INTERRUPTIBLE) int exit_code; // 进程退出时的返回值
3. 优先级(Priority)
调度优先级:决定进程获得 CPU 时间的“权重”。
- nice 值:用户可调整的优先级(值越低,优先级越高)。
- 实时优先级:用于需要严格实时性的进程(如音频播放)。
字段示例:
int nice; // nice值(-20到19,默认0) unsigned long policy; // 调度策略(如SCHED_OTHER普通进程)
4. 程序计数器(Program Counter)
- 寄存器状态:记录进程执行到的位置。
- pc(程序计数器):指向当前指令的下一条指令的地址。
- 其他寄存器:如栈指针(
rsp
)、基指针(rbp
)等,保存进程执行现场。
- 作用:当进程被中断(如切换到其他进程),内核会保存这些寄存器的值,以便恢复执行。
5. 上下文(Context)
- 上下文数据:进程执行时的所有硬件寄存器的值。
- 类比:学生临时离开教室前,记录自己写到哪页笔记、用哪支笔。
- 保存场景:当进程被中断(如 I/O 请求),内核将寄存器值保存到
task_struct
。 - 恢复场景:当进程重新调度到 CPU 时,内核恢复这些寄存器值,继续执行。
6. 内存与资源信息
- 内存指针:记录进程的代码、数据、堆栈的内存地址。
- 代码段(text 段):程序指令的内存区域。
- 数据段(data 段):全局变量、堆栈等。
- 共享内存:与其他进程共享的内存块指针。
- 资源列表:
- 打开的文件描述符(如
/dev/sda
)。 - 分配的 I/O 设备(如 USB 设备)。
- 打开的文件描述符(如
7. I/O 状态
- I/O 请求队列:记录进程等待的 I/O 操作(如读取硬盘)。
- 分配的设备:进程当前占用的硬件设备。
8. 记账信息(Accounting)
- CPU 时间统计:
utime
:用户模式下消耗的 CPU 时间。stime
:内核模式下消耗的 CPU 时间。
- 时间限制:如进程允许的最大运行时间。
- 记账用户:记录进程属于哪个用户(如
root
或普通用户)。
9. 其他信息
- 信号处理:记录进程如何处理信号(如
SIGTERM
终止信号)。 - 线程信息:如果是多线程程序,记录线程组信息。
- 调度队列指针:指向进程所在的就绪队列或阻塞队列。
四、组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以 task_struct
链表等非常复杂的数据结构的形式存在内核里。
五、查看进程
1. 查看进程方法
在 Linux 中,进程(Process)是正在运行的程序实例。查看进程的常用命令有:
ps
:最常用的进程查看工具,适合快速查看进程快照(静态结果输出)。top
:动态监控进程,适合实时观察系统状态(动态查看进程变化,默认 5 秒更新)。htop
:增强版top
,界面更友好,适合交互式操作。- 其他命令:如
pgrep
、pidof
等,适合精准定位进程。
1. ps
相关命令
当直接输入 ps
而不带任何参数时,命令会:
- 仅显示当前终端会话中的进程,即与当前终端关联的进程。
- 输出内容较为简洁,仅包含以下字段:
- PID:进程 ID。
- TTY:进程关联的终端(如
pts/0
表示伪终端)。 - TIME:进程占用的 CPU 时间。
- CMD:启动进程的命令名及参数。
命令/选项 | 说明 | 示例 | 常用度 |
---|---|---|---|
ps aux |
显示所有用户的进程,包含详细信息(用户、CPU、内存等) | ps aux |
⭐⭐⭐⭐⭐ |
ps -ef |
显示全格式进程信息(更详细的进程信息,常用于系统管理和脚本,含 PPID) | ps -ef |
⭐⭐⭐⭐⭐ |
ps -e |
显示所有进程 | ps -e |
⭐⭐⭐ |
ps -u <用户> |
显示指定用户的进程 | ps -u root |
⭐⭐⭐ |
ps -p <PID> |
显示指定 PID 的进程信息 | ps -p 1234 |
⭐⭐⭐⭐ |
ps -o |
按需展示特定字段,常与 -p 结合使用(如 PID、PPID、CMD 等) |
ps -o pid,ppid,cmd,%mem,%cpu |
⭐⭐⭐⭐ |
ps aux --sort |
按指定字段排序(如内存、CPU) | ps aux --sort=-%mem |
⭐⭐⭐⭐ |
ps auxf |
以树状结构显示进程 | ps auxf |
⭐⭐⭐⭐ |
2. top
相关命令(按 q
退出)
命令/选项 | 说明 | 示例 | 常用度 |
---|---|---|---|
top |
实时动态查看进程状态(类似任务管理器) | top |
⭐⭐⭐⭐⭐ |
top -c |
显示完整命令路径 | top -c |
⭐⭐⭐⭐ |
top -p <PID> |
监控指定 PID 的进程 | top -p 1234 |
⭐⭐⭐ |
top -u <用户> |
监控指定用户的进程 | top -u root |
⭐⭐⭐ |
top -b |
以批处理模式运行,输出到文件 | top -b -n 1 > top.log |
⭐⭐⭐ |
top -n <次数> |
指定刷新次数后退出 | top -n 3 |
⭐⭐⭐ |
3. htop
相关命令
命令/选项 | 说明 | 示例 | 常用度 |
---|---|---|---|
htop |
增强版 top,支持鼠标操作和颜色高亮(需安装:yum install htop ) |
htop |
⭐⭐⭐⭐⭐ |
htop -u <用户> |
显示指定用户的进程 | htop -u root |
⭐⭐⭐⭐ |
htop -p <PID> |
显示指定 PID 的进程 | htop -p 1234 |
⭐⭐⭐ |
htop -d <秒> |
设置刷新间隔时间 | htop -d 5 |
⭐⭐⭐ |
4. 其他进程相关命令
命令/选项 | 说明 | 示例 | 常用度 |
---|---|---|---|
pgrep |
根据名称查找 PID(支持模糊匹配) | pgrep sshd |
⭐⭐⭐⭐ |
pgrep -l |
显示进程名和 PID | pgrep -l ssh |
⭐⭐⭐⭐ |
pidof |
根据精确进程名查找 PID | pidof nginx |
⭐⭐⭐ |
pstree |
以树状结构显示进程关系(需安装:yum install psmisc ) |
pstree -p |
⭐⭐⭐ |
kill <PID> |
终止指定进程 | kill 1234 |
⭐⭐⭐ |
kill -9 <PID> |
强制杀死进程 | kill -9 1234 |
⭐⭐⭐⭐ |
5. /proc
文件系统
命令/路径 | 说明 | 示例 | 常用度 |
---|---|---|---|
/proc/[PID]/status |
查看进程的详细状态信息 | cat /proc/1234/status |
⭐⭐⭐⭐ |
/proc/[PID]/cmdline |
查看进程的启动命令 | cat /proc/1234/cmdline |
⭐⭐⭐ |
/proc/[PID]/exe |
查看进程对应的可执行文件 | ls -l /proc/1234/exe |
⭐⭐⭐ |
/proc/[PID]/fd |
查看进程打开的文件描述符 | ls -l /proc/1234/fd |
⭐⭐⭐ |
2. 通过系统调用获取进程标识符
- C 程序示例
#include <unistd.h> // 提供 getpid(), getppid(), 和 sleep()
#include <stdio.h>
int main()
{
while (1)
{
// 获取当前进程 ID
pid_t pid = getpid();
printf("当前进程 ID (PID): %d\n", pid);
// 获取父进程 ID
pid_t ppid = getppid();
printf("父进程 ID (PPID): %d\n", ppid);
// 睡眠 1 秒
sleep(1);
}
return 0;
}
- 编译
gcc -o getpid getpid.c
- 运行
./getpid
- 示例输出
当前进程 ID (PID): 1195
父进程 ID (PPID): 481
3. fork()
创建进程详解
摘自鸟哥的
Linux
私房菜基础学习篇(第四版)517页:
Linux
的程序调用通常成为fork-and-exec
的流程。进程都会借父进程以复制(fork
)的方式产生一个一模一样的子进程,然后被复制出来的子进程再以exec
的方式来执行实际要执行的进程,最终就成为一个子进程。
- 基础示例代码
#include <unistd.h>
#include <stdio.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
fprintf(stderr, "创建子进程失败"); // 将标准错误输出的信息改为中文
return 1;
}
else if (pid == 0)
{
printf("子进程PID=%d, 父进程PPID=%d\n", getpid(), getppid()); // 子进程中打印的信息改为中文
}
else
{
printf("父进程PID=%d, 创建的子进程PID=%d\n", getpid(), pid); // 父进程中打印的信息改为中文
}
return 0;
}
- 示例输出:
父进程PID=2744, 创建的子进程PID=2745
子进程PID=2745, 父进程PPID=2744
注意:输出顺序可能不固定,取决于系统调度,再看下面一个示例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("begin: 我是一个进程,pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
// 子进程
while (1)
{
printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else if (id > 0)
{
// 父进程
while (1)
{
printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
}
else
{
//error
}
return 0;
}
示例输出:
begin: 我是一个进程,pid: 3721, ppid: 481
我是父进程,pid: 3721, ppid: 481
我是子进程,pid: 3722, ppid: 3721
我是父进程,pid: 3721, ppid: 481
我是子进程,pid: 3722, ppid: 3721
我是子进程,pid: 3722, ppid: 3721
……
[!IMPORTANT]
1. 为什么
fork()
给子进程返回0
,给父进程返回子进程的PID
?
fork()
的目的是创建一个 几乎完全相同 的子进程(一般而言fork
之后的代码父子共享)。让父子进程执行不同的代码分支。通过返回值区分父子进程的身份:
- 父进程:需要知道子进程的
PID
,以便后续管理(如等待子进程退出、发送信号等)。- 子进程:无需知道自己的
PID
(可通过getpid()
获取),但需要明确自己是子进程(返回0
是最直接的方式)。实现意义:
- 父进程通过子进程
PID
可以追踪、控制子进程(如waitpid()
)。- 子进程通过返回值
0
知道自己是被创建的一方,从而执行子进程独有的逻辑。总结:父进程通过返回值区分子进程,子进程通过返回值确认自己是子进程。
2.
fork()
作为一个函数为什么能返回两次?如何理解?
fork()
本质上只调用一次,但返回两次!fork()
调用后,操作系统会复制父进程,创建一个新的子进程。
父进程:继续从
fork()
返回,得到子进程的 PID。子进程:从
fork()
返回,得到 0。示意图:
+----------------+ | 父进程执行 fork() | --> 子进程诞生 +----------------+ | | v v 返回子进程 PID 返回 0
看似一次调用返回两次,实际是两个独立的进程分别执行了
fork()
后的代码。可以把fork()
想象成 一颗分叉的树:调用时是同一条路径,但执行后分成了两条路,各自独立运行。
3. 为什么同一个变量在父子进程里内容不同?
调用
fork()
时,子进程不会立即复制父进程的内存,而是共享同一份物理内存(标记为只读)。当父子进程 修改内存时,操作系统才会复制被修改的页(内存页的最小单位)。具体来说就是当fork()
被调用时:
- 代码段(Code Segment): 父子进程共享这部分内存,不需要复制。毕竟代码是静态的,读操作不会影响对方。
- 数据段、堆、栈(Data, Heap, Stack): 父子进程“看起来”各自独立,但实际上,系统不会立刻复制这部分内存!取而代之的是:
- COW 机制: 父子进程最开始共享同一片物理内存,这些内存页被标记为 只读(Read-Only)。
- 写时复制: 当父或子进程试图写入这片内存时,操作系统才会触发“页面复制”,分配新的内存页,供写操作独享。
这种机制大幅优化了
fork()
的性能,因为如果父子进程不修改内存,根本就不用复制,节省了大量资源。综上:fork()
之后,父子进程共享代码段,但数据、堆、栈在写操作时会触发“写时复制”。举个例子:
#include <stdio.h> #include <unistd.h> int main() { int x = 100; pid_t pid = fork(); if (pid == 0) { // 子进程 printf("子进程 x = %d\n", x); // 共享内存,输出 100 x = 200; // 写操作,触发写时复制,子进程独享这片内存 printf("子进程修改后 x = %d\n", x); } else { // 父进程 sleep(1); // 让子进程先跑 printf("父进程 x = %d\n", x); // 父进程的 x 仍然是 100,未受子进程影响 } return 0; } 输出: 子进程 x = 100 子进程修改后 x = 200 父进程 x = 100
解释:
fork()
之后,父子进程共享内存,x
的初始值对父子都可见(100)。- 子进程写入
x
时,触发 COW:
- 系统为子进程分配新内存页,把原来内存页的值(100)复制过来。
- 子进程修改自己的副本,父进程继续使用原来的内存页。
- 所以,父子进程的
x
从此各自独立,互不干扰。因为它们的内存空间已经分开,各玩各的!注:如果想让父子进程共享数据,得用专门的共享内存机制(如
mmap()
)。
4.
fork()
究竟在干什么?系统中多了一个进程吗?是的,
fork()
让系统里多了一个新进程!可通过ps
命令观察到子进程的存在:ps -ef | grep <程序名>
。它做了这几件事:
复制父进程的内存空间: 代码段、数据段、堆栈等全部复制,子进程看起来和父进程几乎一模一样。
- 复制父进程的 进程控制块(PCB),生成子进程的 PCB。
- 复制父进程的 虚拟地址空间(通过写时复制优化)。
分配新的进程 ID(PID): 子进程有独立的 PID,同时保留父进程的 PPID(Parent PID)。
独立调度: 子进程作为独立的进程被调度,和父进程各自执行,互不干扰。
所以,
fork()
之后,系统里确实多了一个进程!但它们起点一致,只是接下来的执行路径可能不同。
5. 为什么要创建子进程?
创建子进程的目的是 并行执行不同的任务,常见用途有:
- 后台任务: 如守护进程、Web 服务器,父进程处理请求,子进程负责具体工作。
- 任务分解: 父进程分配工作,子进程完成任务。
- 隔离风险: 子进程失败不会影响父进程,避免整个程序崩溃。
简单来说,父进程管理,子进程干活,井井有条!
6.
fork()
后,谁先运行?这个是 不确定的!谁先运行完全取决于操作系统的调度策略:
- 父子进程创建完成后,由操作系统的调度器决定执行顺序。
- 可能父进程先运行,也可能子进程先运行,甚至交替执行
所以,谁先跑?随缘!操作系统说了算,让操作系统的调度器决定,它才是“调度大哥”!
六、扩展思考
Q1:为什么进程不需要直接与内核交互?
答:内核通过 task_struct
间接管理进程,就像校长通过学生档案管理学生。进程只需执行代码,内核通过数据结构记录状态并决策。
Q2:如何查看进程的 task_struct
?
答:用户态无法直接访问 task_struct
,但可以通过以下命令间接查看:
ps
:显示 PID、状态、CPU/内存使用。/proc/[pid]/stat
:虚拟文件系统,如cat /proc/1234/stat
包含进程的详细信息(映射task_struct
字段)。
Q3:多个进程如何共享 CPU?
答:通过调度算法在 task_struct
间切换。
- 完全公平调度(CFS):基于虚拟运行时间(
vruntime
)决定进程优先级。 - 实时调度策略:如
SCHED_FIFO
(先进先出)和SCHED_RR
(时间片轮转)。 - 调度相关字段:
task_struct
中的policy
(调度策略)、prio
(动态优先级)、static_prio
(静态优先级)。
共勉