第 6 章 进程 (Processes)
核心结论
-
进程 = 程序的运行实例:进程是内核用来分配资源的抽象实体;一个程序可对应多个进程;进程包含用户态内存(代码+变量)和内核数据结构(PID、内存表、fd 表、信号表等)。
-
PID 与 PPID:每个进程有唯一 PID(
⇐pid_max,默认 32768,可调至 4M+);PPID 标识创建它的进程;进程 1 (init) 是所有进程的祖先。 -
进程内存布局:text(代码,只读)、data(初始化数据)、bss(未初始化数据,零初始化)、heap(malloc 区域)、stack(局部变量)。
-
虚拟内存管理:进程地址空间是虚拟的——按需分页、swap 到磁盘、内存保护;典型页大小 4096 字节;进程间虚拟地址空间相互隔离。
-
栈帧:每个函数调用在栈上分配一个 frame,存储局部变量、参数、返回地址;栈从高地址向低地址增长。
-
命令行参数与进程初始化:C 程序通过
int main(int argc, char *argv[])访问命令行参数;环境变量通过extern char **environ访问;etext/edata/end标记段边界。 -
进程内存大小查询:
size(1)命令显示 text/data/bss 段大小;getrusage()查询运行时统计;/proc/PID/status提供详细信息。
|
本章主旨
本章是「进程」主题的开篇。核心:理解进程是什么、进程内存如何布局、虚拟内存如何工作。本章不展开 |
一、核心概念
本章围绕 6 个核心概念展开:从「什么是进程」到「进程内存如何布局」再到「虚拟内存如何工作」。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
进程 vs 程序 |
程序是包含代码和元信息的文件;进程是程序的运行实例(内核抽象);一个程序可对应多个进程。 |
§6.1;进程由「ELF 二进制 + 内核数据结构」构成;fork 创建子进程,exec 加载新程序。 |
PID 与 PPID |
PID 是进程唯一标识符;PPID 是父进程 ID;进程 1 ( |
§6.2; |
进程内存布局 |
5 个段:text(只读代码)、data(初始化数据)、bss(零初始化)、heap(malloc 区域)、stack(局部变量)。 |
§6.3;栈从高地址向低地址增长;堆从低地址向高地址增长。 |
虚拟内存管理 |
进程地址空间是虚拟的——按需分页到物理内存或 swap;进程间相互隔离;典型页大小 4096 字节。 |
§6.4; |
命令行参数 |
C 程序通过 |
§6.5; |
环境变量 |
每个进程继承父进程的环境;通过 |
§6.5;环境变量名=值形式;可被 exec 后的新程序继承或替换。 |
二、详细笔记
6.1 进程与程序
What:程序 = 文件中包含的代码、元信息和数据;进程 = 程序在内核中运行的实例(一个动态抽象)。
Why:区分「程序」和「进程」才能理解「为什么一份代码可以同时跑两次」「为什么 PID 与程序不是一一对应」。
How:程序文件(典型 ELF 格式)包含:
-
二进制格式标识:ELF 标记(让内核知道如何解析)。
-
机器语言指令:程序的算法。
-
程序入口点:第一条指令的地址。
-
数据:初始化常量和变量值。
-
符号和重定位表:用于调试和动态链接。
-
共享库和动态链接信息:依赖的
.so文件列表。 -
其他元信息。
进程 = 内核数据结构 + 用户态内存;内核维护进程的信息包括:
-
PID、PPID。
-
虚拟内存表(页表)。
-
打开文件描述符表。
-
信号传递与处理信息。
-
进程资源使用与限制。
-
当前工作目录。
-
其他。
When:
-
写一个程序——它是文件(程序)。
-
每次运行——内核创建一个新实例(进程);多个实例可同时存在(多进程)。
-
同一进程可以先后执行不同程序——
fork+exec模式。
Example:
// 摘自《The Linux Programming Interface》第 6 章
// 一个简单的程序文件 —— 静态的、可被多次执行的代码
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Hello from PID %d\n", getpid());
return 0;
}
// 运行多次产生多个进程,PID 不同,但程序代码相同
6.2 PID 与进程 ID
What:每个进程有唯一 PID(正整数);PPID 标识父进程;init(PID 1)是所有进程的祖先。
Why:PID 是进程间通信、信号发送、唯一标识的基础;理解 PID 复用才能避免「依赖 PID 持久不变」的错误。
How:
// 摘自《The Linux Programming Interface》第 6 章
#include <unistd.h>
pid_t getpid(void); // 当前进程 PID
pid_t getppid(void); // 父进程 PID
PID 约束(§6.2):
-
Linux 2.4 及更早:PID 上限 32767(
PID_MAX)。 -
Linux 2.6+:默认 32768,可通过
/proc/sys/kernel/pid_max调整;64 位系统最高 2^22 ≈ 4M。 -
PID 达到上限后——内核从 300 开始重新分配(避免与系统进程冲突)。
-
终止进程的 PID 会被后续新进程重用。
孤儿进程:父进程终止后,子进程被 init(PID 1)收养,PPID 变为 1。
When:
-
需要唯一标识当前进程——用
getpid()+ 当前时间戳(避免 PID 复用)。 -
需要父进程 ID——
getppid()。 -
想看进程树——
pstree(1)命令。
Example:
// 摘自《The Linux Programming Interface》第 6 章
pid_t pid = getpid();
pid_t ppid = getppid();
printf("PID=%ld PPID=%ld\n", (long) pid, (long) ppid);
6.3 进程内存布局
What:进程的虚拟内存分为若干段——text、data、bss、heap、stack。
Why:理解内存布局是理解 C 变量存储类别(全局/静态/局部/动态)的关键;是理解 malloc/free、栈溢出、段错误的基础。
How:5 段布局(§6.3 Figure 6-1):
| 段 | 内容 | 特点 |
|---|---|---|
text |
机器指令 |
只读;可被多进程共享 |
data |
显式初始化的全局/静态变量 |
可读写 |
bss |
未初始化的全局/静态变量 |
程序加载时清零;可执行文件中不占空间 |
heap |
动态分配的内存(malloc) |
从低地址向高地址增长;通过 brk/sbrk 或 mmap 管理 |
stack |
局部变量、函数参数、返回地址 |
从高地址向低地址增长 |
布局示意(地址从低到高):
0x00000000
...
(unallocated memory)
...
0x08048000 ← &etext (text 结束)
text
...
data ← &edata
bss ← &end
heap (grows up)
...
(unallocated)
...
stack (grows down)
argv, environ at top
0xC0000000 ← kernel space(不可访问)
特殊符号(§6.3):
-
extern char etext, edata, end;——分别标记 text、data、bss 段结束地址;可由程序访问。
When:
-
全局变量 → data/bss。
-
静态变量 → data/bss。
-
局部变量 → stack(函数帧)。
-
malloc→ heap。 -
函数参数 → stack(x86-32)或寄存器(x86-64 ABI)。
Example:
// 摘自《The Linux Programming Interface》第 6 章 proc/mem_segments.c
char globBuf[65536]; /* Uninitialized data segment (bss) */
int primes[] = { 2, 3, 5, 7 }; /* Initialized data segment */
static int key = 9973; /* Initialized data segment */
static char mbuf[10240000]; /* Uninitialized data segment */
static int square(int x) { /* 函数代码在 text 段 */
int result; /* 局部变量在 stack */
result = x * x;
return result;
}
int main() {
char *p = malloc(1024); /* p 在 stack,1024 字节在 heap */
...
}
6.4 虚拟内存管理
What:进程地址空间是虚拟的——按需分页到物理内存或 swap;进程间相互隔离。
Why:虚拟内存让多个进程可以共享有限的物理内存(通过 swap),同时保持地址空间隔离(一个进程不能访问另一个进程的内存)。
How:
-
页(page):固定大小的内存单元;典型 4096 字节。
-
页框(page frame):物理内存中等大小的块。
-
驻留集(resident set):进程当前在物理内存中的页。
-
swap 区:磁盘上保留区域,用于保存暂时不用的页。
-
页表(page table):内核为每个进程维护的虚拟地址 → 物理地址/swap 映射。
-
缺页(page fault):进程访问不在物理内存中的页时触发;内核从 swap 或磁盘加载。
locality of reference(§6.4):
-
空间局部性:访问附近的地址(顺序处理指令和数据)。
-
时间局部性:短时间内重复访问同一地址(循环)。
虚拟内存的优势:
-
进程隔离——一个进程无法访问另一个进程的内存。
-
只在内存中保留需要的部分——降低内存需求。
-
允许更多进程并发运行——提高 CPU 利用率。
When:
-
查询页大小:
sysconf(_SC_PAGESIZE)或getpagesize()(旧)。 -
/proc/PID/status中的VmSize、VmRSS、VmPeak等字段报告虚拟内存统计。 -
段错误(SIGSEGV)——访问未映射的虚拟地址或权限错误。
Example:
// 摘自《The Linux Programming Interface》第 6 章
long pagesize = sysconf(_SC_PAGESIZE);
printf("Page size: %ld bytes\n", pagesize);
// 输出: "Page size: 4096 bytes"
6.5 命令行参数与环境变量
What:进程启动时接收命令行参数(argc/argv)和环境变量(environ/getenv)。
Why:理解参数与环境是写「接收配置」的程序的基础;理解参数从 exec 调用方传递到被调用方。
How:
// 摘自《The Linux Programming Interface》第 6 章
#include <unistd.h>
extern char **environ; // 全局环境变量指针数组
char *getenv(const char *name); // 查找环境变量
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);
int clearenv(void); // 清空环境(非标准)
参数与环境位于栈上方(argv、environ 区):
+----------+ ← top of stack
| stack | char *argv[argc+1]
+----------+ char *environ[n+1]
| ... |
+----------+
| heap |
+----------+
| bss |
+----------+
| data |
+----------+
| text |
+----------+ 0x00000000 (low address)
When:
-
程序需要可配置——读环境变量(
HOME、PATH、LANG)。 -
程序需要可选参数——遍历
argv[1]到argv[argc-1]。 -
修改子进程的环境——
execve()时指定envp。
Example:
// 摘自《The Linux Programming Interface》第 6 章
int main(int argc, char *argv[]) {
printf("Program: %s\n", argv[0]);
printf("Arguments (%d):\n", argc - 1);
for (int i = 1; i < argc; i++)
printf(" argv[%d] = %s\n", i, argv[i]);
char *home = getenv("HOME");
char *path = getenv("PATH");
printf("HOME=%s\n", home);
printf("PATH=%s\n", path);
return 0;
}
6.6 size 命令与进程内存查询
What:size(1) 命令显示程序二进制中 text/data/bss 段的大小;getrusage() 查询运行时统计。
Why:理解进程内存结构对调试(栈溢出、内存泄漏)和优化(减小二进制大小)都很重要。
How:
# 摘自《The Linux Programming Interface》第 6 章
$ size /bin/ls
text data bss dec hex filename
93524 5472 1344 100340 187f4 /bin/ls
getrusage():
// 摘自《The Linux Programming Interface》第 6 章
#include <sys/resource.h>
int getrusage(int who, struct rusage *usage);
// who: RUSAGE_SELF(当前进程)/ RUSAGE_CHILDREN(已终止子进程)
// 返回 CPU 时间、内存使用、I/O 次数等
/proc/PID/status 提供详细信息:
-
VmPeak/VmSize:虚拟内存峰值/当前。 -
VmRSS:常驻内存大小。 -
VmData/VmStk/VmExe:各段大小。 -
Threads:线程数。 -
voluntary_ctxt_switches/nonvoluntary_ctxt_switches:上下文切换次数。
When:
-
优化二进制大小——用
size看哪段大。 -
调试内存问题——看
/proc/PID/status。 -
性能分析——
getrusage+ 实际时间测量。
三、关键图表
|
非可视化条目(进程内存结构速查)
|
四、思维导图
mindmap
root((第 6 章 进程))
进程程序
程序 文件
进程 实例
ELF 格式
PID PPID
唯一标识
init PID 1
pid_max
内存布局
text 只读
data 初始化
bss 零初始化
heap 动态
stack 局部
虚拟内存
页 4096 字节
页表
swap
按需分页
locality
命令行参数
argc argv
argv 0 程序名
参数传递
环境变量
environ 全局
getenv setenv
HOME PATH LANG
查询工具
size 命令
getrusage
proc PID status
五、重点与易错点
-
「程序」vs「进程」:前者是文件,后者是运行实例;同一程序可产生多个独立进程。
-
PID 会被复用:进程终止后,PID 可被后续进程使用;不要假设 PID 持久不变——需要稳定标识用
getpid() + time()。 -
孤儿进程被 init 收养:父进程先于子进程终止时,子进程 PPID 变为 1。
-
text 段是只读的:试图修改 text(如写入代码段)触发 SIGSEGV。
-
bss 段加载时清零:未初始化的全局变量保证是 0;这是语言标准保证的。
-
stack 从高地址向低地址增长:与 heap 相反——递归太深会导致 stack overflow(SIGSEGV)。
-
heap 与 stack 之间的「未分配」区域:程序可动态扩展(mmap 或 sbrk)。
-
etext/edata/end 不是 ANSI C 标准:是 Linux/BSD 扩展;但几乎所有 UNIX 都有。
-
环境变量被子进程继承:修改 environ 影响子进程;fork 后父子环境独立。
-
虚拟内存是按需分配:malloc 大块内存后,实际物理内存只在第一次写入时才分配(page fault)。
-
段错误的常见原因:解引用 NULL 指针、解引用未初始化指针、访问已 free 的内存、栈溢出、写入只读段。
-
跨章衔接:第 7 章展开 heap 管理(malloc/free/brk);第 9 章展开进程凭证;第 24-27 章展开 fork/exec/wait;第 35 章展开进程调度。