《计算机系统概论》学习笔记

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 位机上,longpointer 都是 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)

只有 lwsw 访问内存。

Accessing Arrays

用两个寄存器的和表示地址。

slli x28, x1, 2 # x28 = 4 * i

Changing Program Control Flow

条件跳转:

beq rs1, rs2, L # branch to L if rs1 == rs2

还有 bnebltbge

无条件跳转:

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。进程的执行顺序不可预测。

当进程终止时,仍然占用少量系统资源,因为父进程需要子进程的退出信息。waitwaitpid 给出信息,然后 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。用 waitnotify 等待和通知。

Go 语言 make(chan string) 创建了无缓冲 channel,c <- "ping" 之后会阻塞,直到 msg := <- c

Dead Locks

常见错误:违反原子操作是缺少保护,死锁是过度保护。

大部分非死锁错误只涉及一个变量,大部分死锁错误只包含两个线程互相需要两个资源。

要求:互斥,拿取并等待,非抢占(只能等线程释放),循环等待。

处理:detection,prevention,ignore。

如何避免:所需资源无限,不允许共享资源(不太现实),不允许等待(立刻返回,需要重新请求),按照固定顺序访问(破坏循环等待),让所有线程在一开始获得所有资源。

posted @ 2025-04-16 10:26  qAlex_Weiq  阅读(363)  评论(0)    收藏  举报