《计算机系统概论》学习笔记
1. Introduction
Abstraction:抽象程度和效率的平衡。
Locality:内存大小和访问速度的平衡。
Parallelism:优化常见情况,但不要过度优化。
Redundancy:规模大的时候,任何事情都有可能发生。系统设计还要考虑安全性。
*Number Representation
补码:实际表示的数为 \(-a_{n - 1} \times 2 ^ {n - 1} + \sum_{i = 0} ^ {n - 2} a_i \times 2 ^ i\),会溢出。负数加 \(2 ^ n\) 得到补码的二进制表示。补码各位取反之后再加 \(1\),得到绝对值的二进制表示。
补码最高位(MSB)称为符号位。无符号整数向前填充 0(zero-extension),有符号整数向前填充符号位(sign-extension)。
C 或 C++ 同时涉及无符号和有符号的运算,有符号会隐式转换为无符号,可能导致 bug。
浮点数有一位符号位,若干指数位和若干分数位(1-8-23 或 1-11-52)。浮点数分为规格数,非规格数(指数部分为 0)和特殊数(质数部分为 1,表示无穷大和 NaN)。
在 32 位机上,long 和 pointer 都是 4 字节。
Bit Manipulation
左移负数也能得到正确的结果(神奇吧)。
右移分成逻辑右移(用 0 填充)和代数右移(用符号位填充)。
Data Layout in Memory
& 取地址,* 解地址。
多维数组(multi-dimentional)采用行优先,偏移量为 (i * C + j) * size。
多层数组(multi-layer)是指针序列,每个指针指向一个数组,这些数组可以不连续。
结构体对齐:内部每个元素的首地址是其大小的倍数,整个结构体对齐到最大数据类型的倍数。结构体内部元素的顺序可以节省空间。
联合体:所有变量的首地址相同。
在大端字节序(big endian)中,LSB 在较大地址。在小端字节序中,LSB 在较小地址。
Program Representation
用编译器转成汇编语言,汇编器转成机器语言。
C++ 是编译语言,编译器每次生成机器代码运行,生成文件依赖于系统。Python 是解释语言,解释器每次运行一行。
2. Processors and Assembly Code
Von Neumann 架构:处理器和内存分开,程序放在内存中。
*Instruction Set Architecture
处理器的运行顺序:根据 PC 在内存中获取指令,在处理器内部进行计算和存储(寄存器),最后更新 PC。
五个模块:fetch,decode,execute,load/store,write back。
优化:pipelining(有先后顺序的并行),superscalar(独立指令的并行),multi-core(多个处理器的并行)。
RISC(reduced)和 CISC(complex),课程讨论第五代 RISC 32 位整数:RV32I。
x0 总是零。
Arithmetic / Logic Instructions
<op> rd, rs1, rs2
<op>i rd, rs1, imm
rd 表示运算,imm 表示常数。
Data Transfer Instructions
load: ls rd, offset(rs1)
store: sw rs2, offset(rs1)
例如 *a = b + c[2]:
lw x28, 8(x3) -> multiply the size of element
add x28, x2, x28
s2 x28, 0(x1)
只有 lw 和 sw 访问内存。
Accessing Arrays
用两个寄存器的和表示地址。
slli x28, x1, 2 # x28 = 4 * i
Changing Program Control Flow
条件跳转:
beq rs1, rs2, L # branch to L if rs1 == rs2
还有 bne,blt,bge。
无条件跳转:
j L
一个指令同时处理多个数据:SIMD。
Data Race and Synchronization
当不同处理器访问相同地址时,出现数据竞争(data race)的问题。ISA 支持原子操作,例如 test and set t&s,compare and swap,fetch and sub。可以用来构造数据锁。
lock: t&s reg, addr # reg = *addr; /* test */ if (reg == 0) *addr = 1; /* set */
bne reg, zero, lock
unlock: st addr, 0
cas cmp_reg, swap_reg, addr # if (*addr == cmp_reg) swap(*addr, swap_reg);
AMOADD rd, rs2, (rs1) # read data from address rs1; apply the operation with rs2 and store back; leave the original value (data from address rs1) in rd.
*Memory Layout of a Program
地址从小到大依次为预留内存,文本和代码,静态数据(全局变量),堆(动态数据分配,申请的内存),空内存,栈(函数调用及其内部变量),操作系统与内核。
栈符合函数后进先出的调用顺序 LIFO。进入时将申请内存并入栈,返回时将内存释放并出栈。栈顶是高内存,所以是向下增长的。
函数调用的过程中会改变寄存器的值,所以调用函数时需要记录寄存器的值。要么是 caller 记录,要么是 callee 记录。ISA 对每个寄存器定义了是 caller 还是 callee 记录它。
传递控制:
jal L # jump to L and save the return position to register ra(x1)
jr ra # jump to the address stored in ra
传递数据:用寄存器传递参数和返回值。
3. Memory Hierarchy
Memory Technology
初级存储:内存。速度快但断电后数据不保存。通过 load 和 store 读取。DRAM 和 SRAM cache。
次级存储:外存。速度慢但数据不丢失。用磁盘和 SSD 存储。
SRAM:二维阵列,需要 n + m bit 表示行和列的地址。访问非常快,体积大,容量小。用电压表示 01。
DRAM: 访问较慢,访问具有破坏性,体积小,容量大,和处理器工艺不同。用充电表示 01。
内存的发展比处理器更慢。
Program Locality
含义:程序在一个时刻只需要很小一部分数据,且这些数据有很大概率相邻。
时间:一个数据很有可能再被访问到。
空间:在附近的数据有可能被访问到。
算法效率和系统效率的平衡。
*Caches
思想:把附近的数据放到内存,再将更小区域的数据放在多级缓存,以此类推。
processor <-> cache <-> memory。
优化:写缓存友好的代码,俗称卡常。
数据以块传输。cacheline 是管理数据的最小单元。
hit latency:访问缓存的时间。
miss penalty:从缓存访问内存的时间加上将数据带回缓存的时间(在 hit latency 基础上,所以称为 ”惩罚“)。不需要加入替换数据的时间,和返回数据并行。
AMAT:hit latency + miss rate * miss penalty
- L1 命中率 80-95,hit latency 1-2ns,miss penalty 100-200ns。
*Cache Organization
fully associative:一个块可以放在任何位置。低 miss rate,高 hit latency。
direct-mapped:一个块只能放在对应的位置。块下标是块地址模块数,当块数是 2 的幂次时即低位。低 hit latency,高 miss rate。
- 块地址是数据地址除以块大小。注意区分 data address,block address,index 和 block offset(每个地址在块内部的偏移)。
N-way set-associative:一个块可以放在部分位置。集合下标是块地址模集合数量。N 指的是集合大小,所以集合数量是块数除以 N。
- direct-mapped 就是 1-way,所以集合数量是块数。fully associative 就是一个集合,集合大小是块数。
需要使用 valid bit 表示是否有数据。
tag 表示块地址。
缓存只要存 tag 和 valid bit,不用 set index 和 block offset,因为可以根据 data address 直接算。
为什么会有缓存未命中?
- compulsory / cold:第一次被访问。
- capacity:没有足够的容量。
- conflict:不同的块之间撞了。fully associative 没有这个问题。
*Replacement Policy
当缓存获得一个新的块时,它需要决定是否替换某个块,以及替换哪个块。
- direct:没有选择
- set/fully associative:使用 replacement policy。
一些经典的策略:随机;最早被访问(可能导致缓存抖动,1 2 3 4 5 1 2 3 4 5 -> 零命中率);最少被访问(访问次数周期减半)。
一个好的策略和程序的局部性相匹配,但需要更复杂的硬件设计。
write:用 dirty bit 表示在被替换时是否需要写回内存(和内存是否一致)。直接写也可以,但是 dirty bit 可以减少写的次数。
write-miss:在 cache 里面没有,当成 read-miss。先 fetch 整个块过来再写。要 fetch 是因为块的大小大于 1。
总结:
- associativity -> conflict miss, block size -> compulsory miss, capacity -> capacity miss。
- Tag bit: total - log(C/A) (48-bit address, cap = 32k, 4-way -> tag = 48 - log(32k/4) = 35)。
Multi-level Cache
每一层的 miss rate 是前一层的 miss 除以这一层的 miss。miss penalty 是下一层的 AMAT。
L1 缓存在每个核上分成数据(L1D)和指令(L1I)。主要考虑 hit latency 而不是 miss rate。32K ~ 64K。
L2 缓存在每个核上只有一个,256K ~ 1M。
L3 缓存由每个核共享,由互联层(interconnect)和每个核链接。主要考虑 miss rate 而不是 hit latency。
因为 L2 缓存在每个核上,但 L3 只有一个,所以会出现数据一致性的问题。常见处理:
- update-based:更新其它块。
- invalidate-based:让其它所有 cache 这部分数据无效。主要在用这个。
针对缓存进行优化:利用程序的局部性。常见优化方法有循环重排,循环分块和循环展开。
4. Processes and Threads
为了支持多个用户和多个程序,需要操作系统。计科基讲过这些,xs。
*Processes
进程 process
一个进程是一个正在运行的程序的实例。
多进程看起来是每个进程有自己的处理器和内存,但实际上是因为 svae and restore。
线程 thread
一个线程是一个执行环境,一个进程可以有多个线程。process = one or multiple threads + an address space。
一个线程的状态是运行 executing 或挂起 suspended。
线程之间的切换称为 上下文切换 context switch。由进程的 system call 或者 timer 中断引起(接下来提到)。
每个线程有自己的寄存器,PC 和栈,但是它们共享对应进程的内存。
base & bound:每个线程的存储地址有上下界。缺点:难以动态扩展和共享数据。更好的解决方案:虚拟内存。
进程不能访问其它进程。用户进程不能访问系统进程。
进程控制块 PCB:每个进程的编号,状态,寄存器,地址空间,用户,对应文件,优先级,运行时间等信息。
硬件提供用户态与内核态。一些指令只有在内核态才可以执行。
在由用户态转为内核态时,需要设置 mode bit,保存用户进程的 PC,然后跳转至内核态。由内核态跳转回来是类似的。
一些可能的情况:system call(用户调用系统功能的接口),中断 interrupt,异常 exception & trap。
处理中断和异常:停止进程,储存数据和起因,控制权交给内核,根据起因选择内核异常处理器修复问题,返回用户进程或终止 abort。
*Process Management
进程终止:从 main 函数返回,或调用 exit,或收到信号强制退出。
void exit(int status) 以 status 为返回值退出,等价于 main 函数的 return。非零值表示异常。
int fork() 创建新进程。在 fork 的地方像叉子一样分开。对子进程返回 0,父进程返回子进程的 PID。进程的执行顺序不可预测。
当进程终止时,仍然占用少量系统资源,因为父进程需要子进程的退出信息。wait 和 waitpid 给出信息,然后 OS 杀掉僵尸进程。
pid_t wait(int* wstatus) 挂起当前进程,直到某个子进程终止。返回值是子进程的 PID。如果 wstatus 不是空指针,存入返回状态。
pid_t waitpid(pid_t pid, int* wstatus, int options) 等待指定子进程终止。
僵尸进程在父进程终止时会被 PID = 1 的 init 清理。
int execve(char* filename, char* argv[], char* envp[]) 运行 filename,给定参数列表和环境变量。调用后保留 PID,但是执行新程序。只有在发生错误时才会返回。
常见:fork-then-exec。
signals
信号:告诉程序一些事情发生了。2 SIGINT Ctrl-C 杀死;9 SIGKILL 系统杀死;11 SIGSEGV 段错误;14 SIGALRM:定时器杀死;17 SIGCHLD:子进程终止。
用户进程发送信号:int kill(pid_t pid, int sig)。
进程收到信号时,要么终止,要么运行用户层面的信号处理器(用户可以自己写),要么忽略。
shell:用于管理进程的进程。
前台任务:shell 等待并收割,即长时间占用终端。
后台任务:不会被 shell 收割,一般也不会被 init 收割(shell 不终止),不占终端。用户自行处理。可以带回前台杀死。
Scheduling policies
非抢占式 non-preemptive:一旦给一个任务一段时间运行,在这段时间内就不能将处理器拿回。
waiting time:在 queue 里面等待的时间总和。
response time:waiting time + execution time。
throughput:单位时间内完成的任务。
FCFS 先到先得;round robin RR 切割成小块并轮流执行;SJF 短时间优先;SRTF(STCF)短剩余时间优先。SJF 和 SRTF 能最小化平均响应时间。
5. Practical Skills in Computer Systems
Basics
- date 日期。
- time:real 真实用时,user 用户态(计算等),sys 内核态(IO 等)。
- uptime:在线时间,用户数量,最近 1,5,15 分钟的平均负载。
- clear:清屏。
- Man:manual 说明书。
- uname:输出一些系统信息。
- whoami:用户名。
- sudo:以最高权限运行(root,uid 为 0)。
管道:|,用前者的输出作为后者的输入。
Files
- ls:列出所有文件。-a 隐藏文件。-l 详细信息(permission,owner,group)。chmod 改权限。
- cd:change directory。
- mkdir:新建 directory。
- pwd:当前目录,print work directory。
- cat:concatenate,显示
cat file,合并cat file1 file2 > file3,创建cat file、追加cat >> file。 - less / more:查看文件。
- head / tail:前十行,后十行。tail -f 实时更新。
- wc:显示总行数(-l),非空行数,字符数(包括回车)。
- df:显示文件系统的磁盘空间使用情况,-k 是以 k 为单位,-h 是 human readable 单位。
- . 是当前目录,.. 是上级目录,~ 是根目录。
当只有前一个执行成功时再执行后一个,用 && 连接。类似有 || 和 ;。
- stdin 0,stdout 1 和 stderr 2 是在进程一开始就打开的文件。因为 fork 继承打开的文件,所以这些文件也继承。
- tee:同时在文件和终端显示。
环境变量 env。PS1 是命令行开头的样式,export PATH=$PATH:/home/ubuntu 在 PATH 加入新的路径,永久更新:~/.bashrc。
Processes and Environment
- grep:找到一些文件里含有某个子串的行并输出。一般配合文件重定向。
- ps:显示和当前终端有关的进程,-ef 显示所有正在运行的进程。
- top:ps -ef 的动态实时监控。
- tab 自动补全,R 搜索历史命令,C SIGINT,Z SIGSTP(移到后台,bg 重新运行,fg 拉到前台),D EOF,A/E 移动到开头或最后,\ SIGQUIT。
- kill:SIGTERM,默认终止信号。-9 SIGKILL 强制杀死,不可被捕获。
- nohup:忽略 SIGHUP 信号,在挂起时不停止运行。一般在最后加入 & 放到后台,不然会卡住终端。
后台的进程无法向终端输出,可能会卡住,所以需要重定向。
在 shell 当中,? 匹配一个字符,* 匹配任意多个字符,{} 匹配一个集合,[a-c] 匹配区间,[abc] 匹配单个字符的集合。
- dmesg:查看内核日志。
- ifconfig 和 ip:查看网络配置。
- ip route:路由表。
- top & htop:进程监控。
Regular Expressions
子串匹配。
- . 匹配除了 \n 以外的任意单个字符。
- * / + / ? / {n} / {n,} / {n, m} 匹配前一个字符 >= 0,>= 1,0 或 1,恰好 n,至少 n,n 到 m 次。
- \d 匹配数字。
- [abc] 匹配 a b 或 c。
- (RX1 | RX2) 匹配 RX1 或 RX2。
- ^ 匹配行首,$ 匹配行尾(
^A.*B$)。
Other Tools
-
find 找文件
find root_path ...。- 按扩展名
-name '*.ext' - 满足多个条件
-path '**/path/**/*.ext' -or -name '*pattern*'。 - 不区分大小写,查找目录
-type d -iname '*lib*'。 - 排除指定目录
-name '*.py' -not -path '*/site-packages/*' - 指定大小范围
-size +500k -size -10M。 - 对匹配文件执行命令
-name '*.ext' -exec wc -l {} \;; 表示命令结束。 - 指定修改时间并删除
-daystart -mtime -7 -delete。+7 是七天之前。 - 查找空文件并删除
-type f -empty -delete。
- 按扩展名
-
sort 排序,-n 按数值,
-
unique 只对有序文件有用。
-
sed
s/a/b/将 a 替换为 b,\n 是第 n 个括号的内容。sed -E 's/.*Disconnected from (invalid |authenticating )?user (.*) [^ ]+ port [0-9]+( \[preauth\])?$/\2/'。 -
awk $n 是每一行的第 n 列,$0 是行编号。
awk '$1 == 1 && $2 ~ /^c[^ ]*e$/ { print $2 }'。 -
tar 压缩或解压文件。`tar -czf - /path/to/files | ssh user@host2 "cd /path/to/destination && tar -xzf -"。
-
ssh-keygen 生成密钥。无需密钥?
cat .ssh/id_ed25519.pub | ssh foobar@remote 'cat >> ~/.ssh/authorized_keys'。 -
scp 复制本地文件到远程服务器。
-
rsync 仅传输修改过的文件。
Makefile
用于在修改文件后重新生成 project file。只会重新编译修改过的部分,很方便。Tab 缩进。
Bash Scripting
设置变量 foo=bar,不能有空格。$foo 使用变量。echo '$foo' 会输出 $foo,要用双引号。
可以做表达式求值 $(("50-34/17*8"))。
-
$0 名称。
-
$1 到 $9 参数。
-
$@ 所有参数。
-
$# 参数数量。
-
$? 上一个指令的返回值。
-
$$ 当前 script 的 PID。
-
!! 整个前一个命令行。
-
(CMD) 可以捕获一个命令的返回结果作为字符串(stdout,不能是 stderr)。检查两个文件夹下的文件名是否完全相同:
diff <(ls foo) <(ls bar)。 -
if elif else fi 条件跳转。
-
case $VAR in 1);; 2);; *);; esac 多分支选择。
-
for ... in ... 或者 while [condition] 或 until [condition],加上 do ... done 循环。
-
列表 arr:0-indxed。
- arr=() create。
- arr=(1 2 3) initialize。
- ${arr[2]} 获得第三个位置。
- ${arr[@]} 获得所有位置。
- ${!arr[@]} 获得所有下标。
- ${#arr[@]} 获得大小。
- arr[0]=3 overwrite。
- arr+=(4) append。
- str=$(ls) 保存 ls 的结果作为字符串。
- arr=($(ls)) 作为序列。
- ${arr[@]😒:n} 从下标 s 开始获得 n 个位置。
-
可以写 function。
-
python
t = subprocess.run(["ls", "-l"], capture_output=True) >>> print(t.stdout)。
Docker
一个轻量级的虚拟机,有自己的工作环境,适用于处理不同的任务需要不同的环境(软件版本,依赖关系等)的情况。
6. Concurrency and Synchronization (1)
Cooperating Threads
让线程分工合作:并发 concurrency。
当多个线程访问相同的数据时,会出现问题,因为线程的顺序是任意的,导致并发问题难以复现。
原子操作:一些不可分割的操作。在大部分机器上,load 和 store 是 atomic 的。
- synchronization:用原子操作保证线程间的合作。
- 临界区 critical section:一段访问共享资源的操作,且资源一次只能被一个线程使用。为了防止多个线程同时访问,需要确保每个线程的访问是互斥 mutual exclusion 的。
线程锁:任何同步都需要等待。被锁住时,等待。在 acquire 和 release 之间就是临界区。
买牛奶问题:让一个人不停地等待。
如何实现锁:用硬件的原子操作,如 test & set,comp & swap。
Semaphore
信号量。
- P():等待信号量变成正数,然后减 1。
- V():加 1,然后唤醒任意 P()。
初始值为 1 的信号量可以用于互斥。
初始值为 0 的信号量可以用于描述进程的先后执行顺序。
经典例子:用三个 semaphore 实现 producer -> buffer -> consumer。
Monitors and Conditional Variables
动机:不能抱着 lock 睡觉,但信号量既用于互斥,也用于表示调度限制。
monitor:一个锁和任意多个条件变量。
条件变量:允许在临界区等待。释放互斥锁 .wait(&lock),等到被另一个进程唤醒 .signal() 或 .broadcast(),此后会回到等待 lock 的队列。尽量用 while(Mesa)而非 if(Hoare,TA 1980,快排 1960)条件是否满足,因为别的线程可以导致条件再次不满足。
Butler Lampson:TA 1992,Xerox PARC 1970,co-inventor of laser printer,Ethernet。
7. Concurrency and Synchronization (2)
Reader & Writer Problem
writer 优先。在更改表示状态的变量时,需要用互斥锁保护。
Language Support
可能的问题:在 acquire 之后提前返回或调用函数时 throw exception 导致忘记 release。用 C++ 的 destructor 解决。
Java 的锁就是 class 实例本身。含有 synchronized 关键字的函数在调用一开始 acquire,返回时 release。用 wait 和 notify 等待和通知。
Go 语言 make(chan string) 创建了无缓冲 channel,c <- "ping" 之后会阻塞,直到 msg := <- c。
Dead Locks
常见错误:违反原子操作是缺少保护,死锁是过度保护。
大部分非死锁错误只涉及一个变量,大部分死锁错误只包含两个线程互相需要两个资源。
要求:互斥,拿取并等待,非抢占(只能等线程释放),循环等待。
处理:detection,prevention,ignore。
如何避免:所需资源无限,不允许共享资源(不太现实),不允许等待(立刻返回,需要重新请求),按照固定顺序访问(破坏循环等待),让所有线程在一开始获得所有资源。

浙公网安备 33010602011771号