深入理解计算机系统:从程序员视角看计算机
基于经典教材 CSAPP 的系统知识全面总结
引言
《深入理解计算机系统》(Computer Systems: A Programmer’s Perspective, CSAPP) 是一本从程序员视角审视计算机系统的经典教材。本书不仅仅教授理论知识,更强调理解计算机系统如何影响程序的正确性、性能和实用性 。
核心理念 :
程序员必须理解系统才能写出高效、可靠的代码
硬件和软件的边界越来越模糊
系统知识是成为优秀程序员的必备技能
第一部分:程序结构和执行
信息的表示和处理
1. 信息存储
计算机中的所有信息都以位 (bit) 的形式存储和处理。
关键概念 :
1 2 3 4 5 6 字节 (Byte) = 8 位 字 (Word) = 机器字长(32位或64位系统) 地址空间: - 32位系统:2^32 = 4GB - 64位系统:2^64 = 16EB (理论上)
字节序 (Endianness) :
1 2 3 4 5 6 7 大端序 (Big Endian): 高位字节存储在低地址 小端序 (Little Endian): 低位字节存储在低地址 示例:整数 0x01234567 地址: 0x100 0x101 0x102 0x103 大端: 01 23 45 67 小端: 67 45 23 01
graph LR
A[数据 0x01234567] --> B{字节序}
B -->|大端| C[01 23 45 67]
B -->|小端| D[67 45 23 01]
2. 整数表示
无符号编码 :
1 2 范围: [0, 2^w - 1] 示例 (8位): 0 ~ 255
补码编码 (最常用的有符号数表示):
1 2 3 4 5 6 7 8 9 范围: [-2^(w-1), 2^(w-1) - 1] 示例 (8位): -128 ~ 127 补码规则: - 正数:与无符号数相同 - 负数:按位取反加1 -1 的补码 (8位): 11111111 -128 的补码 (8位): 10000000
关键陷阱 :
1 2 3 4 5 6 7 8 9 10 11 int a = -1 ;unsigned int b = 1 ;if (a < b) { } int max = INT_MAX;int overflow = max + 1 ;
3. 浮点数表示
IEEE 754 标准 :
1 2 3 4 5 6 7 单精度 (32位): [符号位 1位][指数 8位][尾数 23位] 双精度 (64位): [符号位 1位][指数 11位][尾数 52位] 值 = (-1)^s × M × 2^E
特殊值 :
值
指数
尾数
0
全0
全0
∞
全1
全0
NaN
全1
非0
浮点数陷阱 :
1 2 3 4 5 6 7 8 9 float a = 0.1 + 0.2 ;if (a == 0.3 ) { } if (fabs (a - 0.3 ) < 1e-6 ) { }
程序的机器级表示
1. 汇编语言基础
x86-64 寄存器 :
1 2 3 4 5 6 7 8 9 10 通用寄存器 (64位): %rax - 返回值 %rbx - 被调用者保存 %rcx - 第4个参数 %rdx - 第3个参数 %rsi - 第2个参数 %rdi - 第1个参数 %rbp - 帧指针 %rsp - 栈指针 %r8~%r15 - 额外寄存器
AT&T 语法 vs Intel 语法 :
1 2 3 4 5 6 7 ; AT&T 语法 (GCC 默认) movq $5, %rax # 将立即数5移到rax movq %rax, %rbx # 源在前,目的在后 ; Intel 语法 (Windows 常用) mov rax, 5 ; 目的在前,源在后 mov rbx, rax
2. 函数调用约定
栈帧结构 :
1 2 3 4 5 6 7 8 9 10 11 高地址 +------------------+ | 参数7 | | 参数6 | | 返回地址 | <- 调用者的栈帧 +------------------+ | 保存的%rbp | <- %rbp 指向这里 | 局部变量 | | ... | +------------------+ <- %rsp 指向这里 低地址
graph TD
A[调用者] --> B[保存参数1-6到寄存器]
B --> C[额外参数压栈]
C --> D[call指令]
D --> E[被调用者]
E --> F[保存旧rbp]
F --> G[分配栈帧]
G --> H[执行函数]
H --> I[恢复栈帧]
I --> J[ret返回]
参数传递规则 :
1 2 3 前6个参数: %rdi, %rsi, %rdx, %rcx, %r8, %r9 第7+参数: 通过栈传递 返回值: %rax (整数), %xmm0 (浮点数)
3. 数组和结构体
数组访问 :
1 2 3 4 5 6 int array [10 ];int *ptr = &array [3 ];leaq (%rdi,%rsi,4 ), %rax # rax = rdi + rsi*4
结构体对齐 :
1 2 3 4 5 6 7 8 9 10 11 12 13 struct S1 { char c; int i; char d; }; struct S2 { char c; char d; int i; };
处理器体系结构
1. 指令集架构 (ISA)
CISC vs RISC :
特性
CISC (x86)
RISC (ARM, RISC-V)
指令复杂度
复杂,功能强大
简单,固定长度
寻址模式
多种
较少
寄存器数量
较少
较多
示例
x86, x86-64
ARM, MIPS, RISC-V
2. 流水线 (Pipeline)
5级流水线 :
1 2 3 4 5 1. 取指 (Fetch) 2. 译码 (Decode) 3. 执行 (Execute) 4. 访存 (Memory) 5. 写回 (Write-back)
graph LR
A[指令1] --> B[取指]
B --> C[译码]
C --> D[执行]
D --> E[访存]
E --> F[写回]
流水线冒险 :
结构冒险 :硬件资源冲突
数据冒险 :指令间存在数据依赖
控制冒险 :分支指令导致的不确定性
分支预测 :
1 2 3 4 5 6 7 8 9 for (int i = 0 ; i < n; i++) { if (data[i] >= 128 ) { sum += data[i]; } }
优化程序性能
1. 性能指标
CPU 性能公式 :
1 2 3 程序执行时间 = 指令数 × CPI × 时钟周期 CPI (Cycles Per Instruction): 每条指令的平均时钟周期数
关键优化原则 :
减少指令数
降低 CPI
提高时钟频率
2. 编译器优化
优化级别 :
1 2 3 4 gcc -O0 gcc -O1 gcc -O2 gcc -O3
常见优化技术 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 for (int i = 0 ; i < n; i++) { sum += a[i]; } for (int i = 0 ; i < n; i += 4 ) { sum += a[i] + a[i+1 ] + a[i+2 ] + a[i+3 ]; } for (int i = 0 ; i < n; i++) { result[i] = sqrt (x) * array[i]; } double temp = sqrt (x);for (int i = 0 ; i < n; i++) { result[i] = temp * array[i]; }
3. 内存性能优化
局部性原理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 for (int j = 0 ; j < N; j++) { for (int i = 0 ; i < N; i++) { sum += array[i][j]; } } for (int i = 0 ; i < N; i++) { for (int j = 0 ; j < N; j++) { sum += array[i][j]; } }
第二部分:在系统上运行程序
存储器层次结构
1. 存储器层次
graph TD
A[CPU寄存器] --> B[L1缓存]
B --> C[L2缓存]
C --> D[L3缓存]
D --> E[主存DRAM]
E --> F[本地磁盘]
F --> G[远程存储]
典型参数 :
层次
容量
访问时间
价格
寄存器
<1KB
0.25ns
最贵
L1缓存
32-64KB
1ns
很贵
L2缓存
256KB-1MB
4ns
贵
L3缓存
8-32MB
10-20ns
较贵
主存
4-16GB
100ns
适中
SSD
256GB-2TB
100μs
便宜
HDD
1-10TB
10ms
很便宜
2. 缓存原理
缓存映射方式 :
直接映射 :每个内存块只能映射到一个缓存行
全相联 :内存块可以映射到任意缓存行
组相联 :折中方案,最常用
缓存写策略 :
1 2 3 4 5 6 7 写命中: - 写直达 (Write-through): 同时写缓存和内存 - 写回 (Write-back): 只写缓存,延迟写回内存 写缺失: - 写分配 (Write-allocate): 加载到缓存再写 - 非写分配 (Not-write-allocate): 直接写内存
3. 缓存友好的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void transpose_bad (int src[N][N], int dst[N][N]) { for (int i = 0 ; i < N; i++) { for (int j = 0 ; j < N; j++) { dst[j][i] = src[i][j]; } } } void transpose_good (int src[N][N], int dst[N][N]) { int B = 8 ; for (int i = 0 ; i < N; i += B) { for (int j = 0 ; j < N; j += B) { for (int ii = i; ii < i + B; ii++) { for (int jj = j; jj < j + B; jj++) { dst[jj][ii] = src[ii][jj]; } } } } }
链接
1. 链接过程
graph LR
A[源文件.c] --> B[预处理]
B --> C[编译]
C --> D[汇编]
D --> E[目标文件.o]
E --> F[链接器]
G[库文件] --> F
F --> H[可执行文件]
目标文件格式 (ELF) :
1 2 3 4 5 6 7 8 ELF Header +------------------+ | .text | 代码段 | | .data | 数据段 | | .bss | 未初始化 | | .rodata | 只读数据 | | .symtab | 符号表 | +------------------+
2. 符号解析
符号类型 :
1 2 3 4 int global_var = 42 ; int global_uninit; static int local_var = 10 ; extern int external_var;
链接规则 :
多个强符号:错误
一个强符号,多个弱符号:选择强符号
多个弱符号:任选一个(危险!)
3. 静态库与动态库
静态库 (.a) :
1 2 3 4 5 ar rcs libmylib.a file1.o file2.o gcc main.c -L. -lmylib -o app
动态库 (.so / .dll) :
1 2 3 4 5 gcc -shared -fPIC -o libmylib.so file1.c file2.c gcc main.c -L. -lmylib -o app
对比 :
特性
静态库
动态库
链接时间
编译时
运行时
文件大小
大
小
更新
需重编译
只需替换库
启动速度
快
慢
异常控制流
1. 异常分类
graph TD
A[异常] --> B[中断]
A --> C[陷阱]
A --> D[故障]
A --> E[终止]
B --> F[异步IO设备]
C --> G[同步系统调用]
D --> H[可恢复错误]
E --> I[不可恢复错误]
异常示例 :
1 2 3 4 中断: 键盘输入,网络数据包 陷阱: 系统调用 (read, write, fork) 故障: 缺页异常,除零错误 终止: 硬件错误,非法指令
2. 进程控制
创建进程 :
1 2 3 4 5 6 7 8 9 10 11 12 13 pid_t pid = fork();if (pid == 0 ) { printf ("子进程 PID = %d\n" , getpid()); execlp("ls" , "ls" , "-l" , NULL ); exit (0 ); } else if (pid > 0 ) { printf ("父进程,子PID = %d\n" , pid); int status; waitpid(pid, &status, 0 ); }
3. 信号
常见信号 :
信号
默认行为
触发条件
SIGINT
终止
Ctrl+C
SIGKILL
终止
kill -9
SIGSEGV
终止
段错误
SIGCHLD
忽略
子进程终止
信号处理 :
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <signal.h> void handler (int sig) { printf ("捕获信号 %d\n" , sig); } int main () { signal(SIGINT, handler); while (1 ) { sleep(1 ); } return 0 ; }
虚拟内存
1. 虚拟地址空间
Linux 进程地址空间 :
1 2 3 4 5 6 7 8 9 10 11 12 13 高地址 +------------------+ | 内核空间 | +------------------+ | 栈 ↓ | | | | ↑ 堆 | +------------------+ | .bss 段 | | .data 段 | | .text 段 | +------------------+ 低地址
2. 页表
地址转换 :
1 2 3 4 5 虚拟地址 = [虚拟页号 VPN | 页内偏移] ↓ 页表转换 物理地址 = [物理页号 PPN | 页内偏移] 页大小: 4KB (2^12)
多级页表 :
graph LR
A[虚拟地址] --> B[一级页表]
B --> C[二级页表]
C --> D[三级页表]
D --> E[物理页]
3. 内存映射
mmap 系统调用 :
1 2 3 4 5 6 7 8 9 10 11 #include <sys/mman.h> int fd = open("data.txt" , O_RDONLY);void *ptr = mmap(NULL , file_size, PROT_READ, MAP_PRIVATE, fd, 0 ); char ch = ((char *)ptr)[100 ];munmap(ptr, file_size); close(fd);
第三部分:程序间的交互和通信
系统级I/O
1. Unix I/O 模型
文件描述符 :
1 2 3 4 5 6 7 8 9 10 0 - stdin 1 - stdout 2 - stderr int fd = open("file.txt" , O_RDONLY);char buf[1024 ];ssize_t n = read(fd, buf, sizeof (buf));write(fd, "hello" , 5 ); close(fd);
2. I/O 多路复用
select 示例 :
1 2 3 4 5 6 7 8 9 10 fd_set readfds; FD_ZERO(&readfds); FD_SET(sockfd, &readfds); struct timeval tv = {5 , 0 };int ready = select(sockfd + 1 , &readfds, NULL , NULL , &tv);if (ready > 0 && FD_ISSET(sockfd, &readfds)) { }
网络编程
1. 套接字编程
TCP 服务器 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int listenfd = socket(AF_INET, SOCK_STREAM, 0 );struct sockaddr_in addr ;addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(8080 ); bind(listenfd, (struct sockaddr*)&addr, sizeof (addr)); listen(listenfd, 5 ); int connfd = accept(listenfd, NULL , NULL );char buf[1024 ];read(connfd, buf, sizeof (buf)); write(connfd, "Hello" , 5 ); close(connfd); close(listenfd);
TCP 三次握手 :
sequenceDiagram
participant C as 客户端
participant S as 服务器
C->>S: SYN
S->>C: SYN+ACK
C->>S: ACK
Note over C,S: 连接建立
并发编程
1. 线程
创建线程 :
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <pthread.h> void *thread_func (void *arg) { printf ("线程运行中\n" ); return NULL ; } int main () { pthread_t tid; pthread_create(&tid, NULL , thread_func, NULL ); pthread_join(tid, NULL ); return 0 ; }
2. 同步机制
互斥锁 :
1 2 3 4 5 6 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_lock(&mutex); counter++; pthread_mutex_unlock(&mutex);
信号量 :
1 2 3 4 5 6 sem_t sem;sem_init(&sem, 0 , 1 ); sem_wait(&sem); sem_post(&sem);
3. 经典问题
生产者-消费者 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 sem_t empty, full;pthread_mutex_t mutex;void *producer (void *arg) { while (1 ) { sem_wait(&empty); pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex); sem_post(&full); } } void *consumer (void *arg) { while (1 ) { sem_wait(&full); pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex); sem_post(&empty); } }
总结
核心思想
抽象层次 :理解系统的多层抽象
性能优化 :缓存、局部性、并行性
正确性保证 :理解底层避免bug
系统思维 :程序在系统中运行