第 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 提供详细信息。

      本章主旨

      本章是「进程」主题的开篇。核心:理解进程是什么、进程内存如何布局、虚拟内存如何工作。本章不展开 fork/exec 等进程创建细节(见第 24-27 章)、不展开调度(见第 35 章)、不展开凭证(见第 9 章)。掌握本章内容后,读者应能在脑中画出「进程虚拟地址空间」的布局图,并能解释「为什么局部变量是线程私有的」「为什么全局变量需要互斥」。

      一、核心概念

      本章围绕 6 个核心概念展开:从「什么是进程」到「进程内存如何布局」再到「虚拟内存如何工作」。

      概念 定义 + 重要性 实现提示

      进程 vs 程序

      程序是包含代码和元信息的文件;进程是程序的运行实例(内核抽象);一个程序可对应多个进程。

      §6.1;进程由「ELF 二进制 + 内核数据结构」构成;fork 创建子进程,exec 加载新程序。

      PID 与 PPID

      PID 是进程唯一标识符;PPID 是父进程 ID;进程 1 (init) 是所有进程的祖先。

      §6.2;getpid() 返回当前 PID;getppid() 返回 PPID。

      进程内存布局

      5 个段:text(只读代码)、data(初始化数据)、bss(零初始化)、heap(malloc 区域)、stack(局部变量)。

      §6.3;栈从高地址向低地址增长;堆从低地址向高地址增长。

      虚拟内存管理

      进程地址空间是虚拟的——按需分页到物理内存或 swap;进程间相互隔离;典型页大小 4096 字节。

      §6.4;sysconf(_SC_PAGESIZE) 查询页大小。

      命令行参数

      C 程序通过 main(int argc, char *argv[]) 访问命令行参数;argv[0] 是程序名。

      §6.5;argc 是参数个数(含程序名);参数以字符串形式存储。

      环境变量

      每个进程继承父进程的环境;通过 extern char **environgetenv() 访问;常见如 HOMEPATH

      §6.5;环境变量名=值形式;可被 exec 后的新程序继承或替换。

      二、详细笔记

      6.1 进程与程序

      What:程序 = 文件中包含的代码、元信息和数据;进程 = 程序在内核中运行的实例(一个动态抽象)。

      Why:区分「程序」和「进程」才能理解「为什么一份代码可以同时跑两次」「为什么 PID 与程序不是一一对应」。

      How:程序文件(典型 ELF 格式)包含:

      1. 二进制格式标识:ELF 标记(让内核知道如何解析)。

      2. 机器语言指令:程序的算法。

      3. 程序入口点:第一条指令的地址。

      4. 数据:初始化常量和变量值。

      5. 符号和重定位表:用于调试和动态链接。

      6. 共享库和动态链接信息:依赖的 .so 文件列表。

      7. 其他元信息

      进程 = 内核数据结构 + 用户态内存;内核维护进程的信息包括:

      • 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):

      • 空间局部性:访问附近的地址(顺序处理指令和数据)。

      • 时间局部性:短时间内重复访问同一地址(循环)。

      虚拟内存的优势:

      1. 进程隔离——一个进程无法访问另一个进程的内存。

      2. 只在内存中保留需要的部分——降低内存需求。

      3. 允许更多进程并发运行——提高 CPU 利用率。

      When

      • 查询页大小:sysconf(_SC_PAGESIZE)getpagesize()(旧)。

      • /proc/PID/status 中的 VmSizeVmRSSVmPeak 等字段报告虚拟内存统计。

      • 段错误(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

      • 程序需要可配置——读环境变量(HOMEPATHLANG)。

      • 程序需要可选参数——遍历 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 命令与进程内存查询

      Whatsize(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 + 实际时间测量。

      三、关键图表

      非可视化条目(进程内存结构速查)
      项目 描述

      程序 vs 进程

      程序是文件,进程是运行实例;一个程序可对应多个进程

      PID

      进程唯一标识;⇐pid_max(默认 32768,可调到 4M+)

      PPID

      父进程 ID;孤儿进程被 init(PID 1)收养

      内存 5 段

      text / data / bss / heap / stack

      text 段

      只读机器指令;可被多进程共享

      bss 段

      未初始化全局/静态变量;加载时清零;可执行文件中不占空间

      stack 段

      局部变量、函数参数、返回地址;从高地址向低地址增长

      heap 段

      malloc 分配;从低地址向高地址增长;通过 brk/sbrk 或 mmap 管理

      虚拟内存

      按需分页;swap 到磁盘;进程间隔离;典型页 4096 字节

      etext / edata / end

      标记段边界的符号

      四、思维导图

      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

      五、重点与易错点

      1. 「程序」vs「进程」:前者是文件,后者是运行实例;同一程序可产生多个独立进程。

      2. PID 会被复用:进程终止后,PID 可被后续进程使用;不要假设 PID 持久不变——需要稳定标识用 getpid() + time()

      3. 孤儿进程被 init 收养:父进程先于子进程终止时,子进程 PPID 变为 1。

      4. text 段是只读的:试图修改 text(如写入代码段)触发 SIGSEGV。

      5. bss 段加载时清零:未初始化的全局变量保证是 0;这是语言标准保证的。

      6. stack 从高地址向低地址增长:与 heap 相反——递归太深会导致 stack overflow(SIGSEGV)。

      7. heap 与 stack 之间的「未分配」区域:程序可动态扩展(mmap 或 sbrk)。

      8. etext/edata/end 不是 ANSI C 标准:是 Linux/BSD 扩展;但几乎所有 UNIX 都有。

      9. 环境变量被子进程继承:修改 environ 影响子进程;fork 后父子环境独立。

      10. 虚拟内存是按需分配:malloc 大块内存后,实际物理内存只在第一次写入时才分配(page fault)。

      11. 段错误的常见原因:解引用 NULL 指针、解引用未初始化指针、访问已 free 的内存、栈溢出、写入只读段。

      12. 跨章衔接:第 7 章展开 heap 管理(malloc/free/brk);第 9 章展开进程凭证;第 24-27 章展开 fork/exec/wait;第 35 章展开进程调度。