Post

阶段性总结-1(Claude)

阶段性总结-1(Claude)

这是一个很好的时机做一次”知识融合”。我不会重新讲每个概念,而是带你看:这些东西如何共同构成一个运行中的计算机系统。

你学的这些东西,本质上是同一个问题的不同侧面:操作系统如何把”静止的程序”变成”活的进程”,并让多个进程安全共存。

我们先用一张图把所有概念串起来,然后再讲逻辑。

image-20260408223126162

现在我们沿着这张图,讲清楚每一层之间的因果逻辑


主线:一个程序如何变成活的进程

整个知识体系可以用一条因果链串起来:

1
2
3
4
5
6
7
8
你写的代码(碎片)
  → 链接(缝合碎片)
  → ELF 文件(静止的完整程序)
  → execve(触发变身)
  → 进程(活的执行单元)
    ├── 虚拟地址空间(隔离的内存视图)
    ├── 线程(执行流)
    └── 文件描述符(I/O 句柄)

每一个箭头,都是一个你学过的机制在工作。


第一条主线:程序如何”活起来”

链接和加载解决的是同一个问题的两个阶段:把静止的代码变成可运行的进程。

你已经知道 ELF 里有两套视图——section 给链接器用,segment 给加载器用。这里补充一个关键连接:

execve() 是整个过程的触发点。

execve() 是一个系统调用,它做的事情是:

  1. 丢弃当前进程的地址空间(彻底清空)
  2. 让 OS 读取新 ELF 文件的 segment,用 mmap() 重新建立地址空间
  3. 如果有 INTERP segment,先把控制权交给 ld-linux.so
  4. 最终跳转到 _start

注意:execve() 不创建新进程,它”替换”当前进程的内容。 这就是为什么它几乎总是和 fork() 配合出现——fork() 创建新进程,execve() 把那个新进程的内容换成目标程序。

这就是著名的 fork-exec 模式,是 Unix 进程模型的核心设计。


第二条主线:进程内部的结构

进程活起来之后,它的内部是什么?

虚拟地址空间是进程最重要的属性。它不是真实的内存,而是操作系统给进程的一个”幻觉”——每个进程都以为自己独占整个地址空间。

mmap() 是建立这个幻觉的工具:

  • 加载 ELF 时,OS 调用 mmap() 把文件内容映射进来——这就是 segment 被加载的方式
  • 动态链接时,每个 .so 也通过 mmap() 映射进地址空间
  • 你自己调用 mmap() 可以映射任意文件或匿名内存

所以你地址空间里的每一块内存,本质上都是某个 mmap() 的结果。

文件描述符(FD)*是进程访问 OS 管理对象的统一接口。无论是文件、管道、socket、还是设备,进程都通过一个小整数(fd)来操作它们。这是操作系统最重要的抽象之一——*“一切皆文件”

FD 在内核里对应一张 open file table,里面存着真正的 inode 指针和读写位置。


第三条主线:进程如何繁殖

fork() 做的事情:复制当前进程,产生一个几乎完全相同的子进程。

但”复制”是昂贵的,所以现代操作系统用 Copy-on-Write(CoW)

  • fork() 后,父子进程共享同一批物理内存页
  • 只有当某一方试图写入某页时,OS 才真正复制那一页

这个机制和 mmap() 深度相关——CoW 本质上是页表层面的懒惰复制。

fork() 会继承什么?

继承不继承
虚拟地址空间(CoW)父进程的线程
文件描述符表(共享同一 fd)未决的信号
环境变量锁的状态(危险!)

第四条主线:线程和进程的关系

线程是进程内的执行流。一个进程可以有多个线程,它们:

  • 共享:虚拟地址空间、文件描述符表、代码段、数据段
  • 独占:栈、寄存器、程序计数器(PC)

这里有一个深刻的连接:线程共享地址空间,意味着一个线程可以访问另一个线程的栈——但它通常不应该这么做。 这就是为什么多线程编程需要同步原语(mutex 等)。

fork() 有一个著名的陷阱:如果多线程程序调用 fork(),子进程里只有调用 fork() 的那个线程被复制,其他线程消失了,但它们持有的锁可能还在数据结构里——子进程因此可能死锁。


一切如何通过 syscall 到达 OS

你学的所有”应用视角的操作系统”,本质上是:进程通过系统调用这扇门,请求内核帮它操作那些它无权直接碰触的资源。

1
2
3
4
5
6
7
8
进程想要:                  对应 syscall:
─────────────────────────────────────────
访问文件          →   open() / read() / write()
分配内存          →   mmap() / brk()
创建新进程        →   fork()
运行新程序        →   execve()
退出              →   _exit()
创建线程          →   clone()(Linux 下线程的底层实现)

_exit() 单独拿出来说:它是直接进入内核终止进程的,不走 C 标准库的清理流程(不调用 atexit(),不 flush stdio)。对应的 exit() 是 C 库函数,会先做清理再调用 _exit()。这个区别在 fork() 后的子进程里很重要。


总结:一张因果网络

用最精简的方式把所有概念的关系说清楚:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.o 文件(编译产物,有符号空洞)
    ↓  [链接器:符号解析 + 重定位]
ELF 文件(完整但静止)
    ↓  [fork + execve:进程替换]
进程(活的执行单元)
    │
    ├─ 虚拟地址空间 ←── mmap() 构建(包括加载 ELF segment)
    │       └─ 每个 .so 也 mmap 进来(动态链接)
    │
    ├─ 线程 ←── 共享地址空间,独占栈和寄存器
    │
    └─ 文件描述符 ←── 内核对象的用户态句柄
    
    ↓  [syscall:穿越用户态/内核态边界]
内核(调度、内存管理、文件系统)

你学的每一个概念,都是这张网络里的一个节点或一条边。它们不是孤立的知识点,而是同一个系统的不同层次。

This post is licensed under CC BY 4.0 by the author.

Trending Tags