卡车司机模拟器

Abstraction:多级的语言体系。代码语言经过 compiler 得到汇编语言 (RISC-V),经过 assembler 得到机器语言,经过 linker 得到可执行文件,经过 loader 与内存交互。另一方面,也有多级的系统硬件架构。缺点是效率不会很高;如果要提效,一个方法是打破语言体系,使用更底层的操作。Locality:多级内存系统。SRAM->DRAM->SSD->Hard Disk->NAS.Parallelism:Amdahl’s Law, \(\text{speedup}=\dfrac1{1-f+\dfrac fS}\),其中 \(f\) 是并行比例,\(S\) 是核数。Make common things fast, but no over-optimize,因为如果 common (bottleneck) 的东西很快,那瓶颈就转到别的东西上面,此时那些东西才是真正 common 的。Redundancy: 为处理 dependency,需要 redundancy 处理意外。

C++:1b sign+8b exp+23b frac; 1b sign+11b exp+52b frac。


结构体储存时,对齐到其中最长的一个成员。相反,union 则是把所有东西压在一块。Bigendian:LSB 有最高的地址 (MIPS, Internet);Litten endian:MSB 有最高的地址 (x86, ARM, RISC-V)。注意:BE/LE 仅仅代表字节内存储方式,字节外的方式都是全体一致的。

just-in-time (JIT) compilation: 在 runtime 时 compile,因此可以直接执行而非还需要解释。Compilation 运行时表现更好,但是通过 import 库,解释型语言也可以使用这些东西。Compiled files (.o, exe) are processor- and OS-specific, not portable Interpreters are easier to implement than compilers o Interpreters are closer to higher, human-readable levels。


处理器结构:Program Counter,memory。ALU:计算单元。Controller:parse instructions and decide next PC。这些东西都需要与外界交互,很慢。因此,使用 register。

Pipelining is a form of instruction-level parallelism。 Multiple instructions simultaneously exist in the processor, but at different stages。Superscalar is also a form of instruction-level parallelism Multiple instructions simultaneously exist in the processor, and at the same stage。Instructions execute out of-order, but this is OK because dependent instructions remain sequential

Multiple Cores: PC, Registers 私有;Memory 共有。 通讯通过 memory 中的 load 和 store.


RISC: reduced instruction set computer (RISC-V, ARM, MIPS);CISC: complex instruction set computer (x86, x86-64)。<op> rd, rs1, rs2:将 rs1 和 rs2 运算结果写到 rd。<op>i rd, rs1, imm:与数值的运算结果。lw rd, offset(rs1)(load)。sw rs2, offset(rs1)(save)。【牢记:一个变量是 4 byte 的。因此 a[2]8(x1),其中 x1 指向 *abeq rs1, rs2, L #branch to L if rs1==rs2 Also have bne, blt, bge, …j L # jumpto L。Test and set: t&s reg, addrreg = *addr; /* test */ if (reg == 0) *addr = 1; /* set */ Compare and swap;Fetch and add/sub。


Code/text, static data, …: set up at program loading Heap: dynamic data allocation, managed by OS Stack: to support (recursive) procedure calls in hardware。Primary storage, a.k.a., main memory, or simply memory.Fast but volatile (lose contents after powered off)Implemented with DRAMand optimized bySRAMcaches Secondary storage, a.k.a., external storage, Slow but non-volatile; Implemented with disks and/or SSDs.SRAM: 2D array,使用 \(n+m\) bit 的 address:前 \(n\) 位是 wordline,后 \(m\) 位是 bitline。


Cache 的大小被称作 cache size。Cache 被均分成数块,每一块被称作一个 cacheline,块的大小被称作 cacheline size。Cacheline 是 cache 与 memory 间数据传输的最小单位。这意味着,在 load 的时候,就算只需要一个 byte 的数据,如果发生 cache miss,CPU 也会自动 load 满一整个 cacheline。对于 memory 中的一段地址,其被从高位向低位切成三段:tag 段、index 段、offset 段。其中,index 指认该地址对应哪个 cacheline;offset 指认该地址对应 cacheline 中的哪个 bit;tag 用于在对应到相应位置后,检查该位置上存储的信息是否与需要的地址吻合。

一个 cacheline 中存储的信息主要包括三个:全 cache 一致的地址 tag;用于存储数据的 data;以及一个额外的 bit,valid bit,用于标记该 cache 中数据是否合法。在开机时,所有的 valid bit 都会被设为 false。只有在 valid bit 为 true 的场合,才会进一步比较 tag 以裁定是否 miss。

如果通过了 valid bit 和 tag 的核验,即可直接从 cache 中返回数据,此时称作 cache hit;反之则为 cache miss,此时可能覆盖 cache 中的一个位置。

miss 时的额外消耗可以不计入覆盖 cache 的消耗:因为 replace 不耽误你回传需要的信息,因此可以在背地里慢慢 replace。average memory access time (AMAT) = hit latency + miss rate × miss penalty。hr: 80%-90%. hl: 1-2ns. mp: 100-200ns。

offset 永远是 cacheline 的 byte 位数。index:

  • fully associative:每个 data 都可以被存储入任何 cacheline。此时 hit rate 高但 hit latency 也高。实现方式是将 index 段长设为 0,这样就必须枚举所有 cacheline 了。
  • direct mapped:每个 data 只能被唯一存储入某个 cacheline。此时 hit rate 低但 hit latency 也低。实现是 index 段长等于 cacheline 数目的位数。
  • set-associative:每个地址有 \(n\) 个备选项。\(n\) 被称作 associativity。访问时所有备选项都要被扫一遍。比较折衷。实现是 index 段长等于 \(n\) 的位数。

Replacement Policy:miss 时要不要覆盖原有数据?如果覆盖,覆盖哪一个?random:随机干掉一位幸运观众。Least Recently Used (LRU):维护时间戳。找到所有 associativity 位中最近一次访问最古老的位。Least Frequently Used (LFU):维护寿命指针,在 hit 时增加,定期减半。当数据超过 cache capacity 时,LRU 可能爆炸!

write policy:通写:当内存中数据被更新时,将更新的数据同步到 cache。显然,这很慢。回写:先将数据写入 cache,然后当 cache 数据被替换时再写入内存。在使用该策略时,需要额外维护一个 dirty bit,标记着是否需要将 cache 数据回写。通写队列:维护队列,慢慢写。

cache miss 的原因:compulsory (cold):第一次必然 miss。capacity:cache 不够了。开大 cache 即可。conflict:被 replace 了。开大 associativity 即可。因此,cache 有三重要参数:Associativity、Blocksize、Capacity。

多级 cache 中,每一层的 AMAT 仍然等于 hit latency + miss rate × miss penalty。区别是,这里的 miss rate 是 local 的:为本层中 miss 数目除以本层中 access 数目,而 penalty 是下一层的 AMAT。因此,递归地计算 AMAT。现代处理器一般有三级 cache:L1、L2 是每个核私有的,而 L3 是公有的。L1 分成两组:L1-D 存储数据,可读可写;L1-I 存储指令,只读不写。既然 L3 是所有核公有的,那么就需要确保核间的数据一致性。两种策略:update-based:单核更新后,手动推送给其它核。实现很麻烦,所以不用。invalidate-based:更新后,把其它核宣布为不合法。


一个程序不一定只会有一个进程:它可以同时被多个母程序调用,创建多个并行的进程实例。多个实例之间的内存和处理器都是共用的,只能以短间隔周期性地将处理器供给给每个进程,造成所有进程被同步处理的假象。因此,在切换进程前,有必要将处理器中信息存储,再次切换前再载入。只有多核才能真正地同步进行。每个线程有其私有的寄存器、PC 和栈,但与同进程中的其它线程分享内存。Base & Bound 是两个 register,它划分了一段进程能访问的区域,使得进程间不会互相干涉。但是问题在于,每个进程都只能访问一段连续的区域,且很难扩张某个进程占有的区域,更难以多进程间共享内存。Process Control Block 是一个内存中的区域,储存了进程的相关信息。内核调度器 (kernel scheduler) 决定了下一个被调用的进程,这需要相应的 scheduling algorithm。


硬件总是支持至少两种模式:内核模式(系统模式),高权限;用户模式,低权限。硬件专门实现了一个调控模式的 mode bit,只有在内核模式下一些指令才可以被调用。而操作系统则决定两者间的切换。三种用户至内核的切换:system call (syscall):调用了关闭进程、内存分配、网络上传等指令。是 proactive,即被用户要求的。interrupt:外界程序打断(计时器/输入输出)。是 reactive,即难以预料的。exception & trap:内生的命令,例如出 bug 了。是 reactive 的。例如,出 bug 的时候,系统会自动进入 kernel mode 并执行 kill 进程等操作。


一个进程会以三种方式告终:从主函数 return;调用了 exit 的 syscall;收到了终止信号。

父进程可以通过 fork 指令创造一个几乎与之等价的子进程,唯一区别在于它访问另一块内存。父子进程之间并没有明确的先后关系,它们可以同时进行。fork 的效果是,在子线程中会返回 0、在父线程中会返回子线程的 pid。在进程告终后,它仍然占据着系统资源,并成为了僵尸进程。父进程可以使用 wait 函数或 waitpit 函数“收割” (reap) 子进程。

  • wait:等待某个子进程告终。需要输入一个地址,在函数执行完后该地址中会存储子进程的 return 值(而该函数会 return 告终的子进程;这种实现方式是因为 c 中每个程序只能有一个输出);waitpid:等待某个特定的子进程告终。

特别地,如果父进程在收割子进程之前亟告终,会有一个特殊的初始化进程将孤儿进程手动干掉。因此,只有在长期执行的程序中,才需要手动收割子进程。

exec 函数族可以调用其它程序。一般来说,父进程不会直接调用其它函数:而是应该 fork 子进程并在子进程中再调用。


信号 (signal) 被用于通知程序,某些事件发生了。发送信号:kernel 可以检测系统事件并发生信号;kill 指令除了终结函数(发送 SIGKILL 这个指令以外),还可以发送其它信号。收到信号:可以选择终结进程、使用 signal handler 抛出一个类似 exception 的东西,或者啥也不干。shell:自动执行的一系列 user 命令。它本身就是一个进程。


两种进程:non-preemptive,一旦开始就不能中途停止;preemptive,可以随时停止。如何优化 scheduling?可以按照 waiting time、response time (=waiting+excution)、throughput(单位时间内完成的工作数目)、fairness(所有 job 以某种相同的方式使用资源)等为尺度。

FCFS:先来先执行。简单,但是除此之外一无是处。RR (Round Robin):所有工作都会被周期性执行一段。更加公平,但是切换工作会增加额外开销,并且不好选择量化的事件段。【具体而言,维护队列,如果有人执行完了或者新来了就扔到队尾】其它模式(需要首先知道所有 job 的时长):SJF (shortest job come first);SRTF (shortest remaining time first)【支持插队,如果新来的 job 比老 job 的剩余时间还要短,就优先执行之】。最优化平均 response time,但是不公平,如果有大量 short job,则 long job 就会 starve。并且,鉴于我们难以预料 execution time,有时不太 pratical。


. 任何非换行字符。*之前 的一个量被重复 \(0\) 或任意多次。+ 被重复 \(1\) 或任意多次。? 被重复 \(0\)\(1\) 次。^ 匹配一行的起始。如,^a 匹配以 a 开始的行。$ 匹配一行的结束。如,a$ 匹配以 a 结束的行。空格是字面量。| 匹配前或后的整个东西。例如,ab|cd 匹配 abcd。特别地,可以连 |,即 ab|bc|cd 等。[...] 匹配中括号中任何一个字符。例如,[abc] 匹配 abc

方括号中获得新意义的字符:[^...] 匹配不在括号中的任何字符。[...-...] 匹配两字面量之间的东西。例如,[b-e] 就是 [bcde][4-8] 就是 [45678]。需要注意的是,此处是 ASCLL 符中,介于二者之间的东西。因此,[?-f] 是 ASCLL 符中介于 ?f 之间的东西。

方括号中需要特殊讨论的字符:\ 是转义符。一般而言,只对特殊意义的字符 \-]^ 有效。右方括号 ],是方括号的结束符。如果想匹配字面量 ],则要么放在方括号开头,要么用转义符。例如,可以用 []f] 匹配 ]f,也可以用 [f\]] 表达同一个意思。连字符 -,出现在两个字面量之间则匹配二者之间的东西,出现在开头结尾则匹配字面量 -。这意味着一些奇怪的用法,例如 [\--\\] 表示字面量 - 和字面量 \ 间的所有字符。脱字符 ^,出现在开头是特殊字符,出现在其它位置是字面量。当需要特殊字符的字面量时,一律加 \ 最省心。其它所有在非中括号环境中是特殊字符的字符,如 +* 等,在中括号中是字面量。

{n}之前 一个量重复恰 \(n\) 次。{n,} 重复至少 \(n\) 次。{n,m} 重复至少 \(n\) 次,至多 \(m\) 次。默认的“重复”是贪婪的:希望匹配尽量长的内容。而在其后加上 ?,即 *?+???{n,}?{n,m}? 等,则转为匹配尽量短的内容。(...) 可以获取括号中的东西的匹配结果,在外界(如 grep 中)使用 \1$1 等(视环境而定)可以调用它们。不过,其最常见的一个用法或许是字面意义上的括号:将里面的表达式视作一个整体。(?...) 更像是纯粹的括号:它不获取匹配结果,一定程度上节省了内存等。(?=...) 是正向先行断言:它要求后面必须匹配括号中的内容(但并非真正消耗它们)。(?!...) 是负向先行。注意,使用它的时候要警惕是否排除了所有 前缀 为括号中内容的串,因此有时需格外警惕。\d 数字。\D 非数字。\s 空白。\S 非空白。\f 换页。\n 换行。\r 回车。\w 单词字符(包括下划线),=[A-Za-z0-9_]\W 非单词。


date 输出当前时间time 检测其后包含的命令运行时间,包括 realusersysuptime:显示系统上次启动以来持续运行的总时间、当前用户数和平均负载。manmanmanual(手册) 的缩写,用于查看 Linux 命令、函数或配置文件的详细使用说明。按 空格 翻页,Enter 逐行滚动,q 退出。搜索:输入 /关键词(如 /option)按回车查找,n 跳转到下一个匹配项。

uname:显示系统信息(内核版本、硬件架构等)。 -a:显示所有信息(内核名称、主机名、内核版本、系统时间、硬件架构等)。 -s:内核名称(默认输出,如 Linux)。 -r:内核版本(如 5.4.0-91-generic)。 -m:硬件架构(如 x86_64)。

whoami:显示当前登录用户的用户名。使用 id 查看 uidroot 是一个特殊的 user,它的 uid=0sudo:以 root 身份干活。

mkdir:创建新目录(-p 自动创建父目录,如 mkdir -p dir1/dir2)。 cat:查看文件内容(如 cat file.txt),或合并文件(cat file1 file2 > merged)。创建新文件(需配合重定向) cat > newfile.txt # 从键盘输入内容,按 Ctrl+D 保存;追加内容 cat file1.txt >> file2.txt # 将file1内容追加到file2末尾。文件不存在时会报错。

lessmore:分页查看文件内容。more 较基础,逐页显示文件内容,适合快速浏览。 只能向前翻页(空格键下一页,Enter 下一行)。 到达文件末尾会自动退出。 支持基础搜索(/关键词,但无法高亮或反向搜索)。 空格:下一页 Enter:下一行 q:退出 less:可向前/向后翻页Page Up/Downb/空格)。 支持搜索高亮/关键词 搜索,n/N 跳转匹配项)。 不会自动退出,需手动按 q 退出。 支持查看压缩文件(如 less file.gz)。 less filename.txt # 分页查看文件(推荐)less +F logfile.log # 实时跟踪文件更新(类似 tail -f,按 Ctrl+C 退出跟踪) 。| 空格f 下一页 || b上一页 || /关键词 向前搜索 || ?关键词向后搜索 || n/N 跳转到下一个/上一个匹配项 || G/g 跳转到文件末尾/开头 || q 退出 |

head:快速查看文件的前几行(默认 10 行),适用于日志、配置文件等场景。

| -n <行数> | 指定显示的行数(如 -n 20) || -c <字节数> | 显示前N字节(如 -c 100) || -q | 安静模式(不显示文件名头) || -v 始终显示文件名头 |

  • 文件不存在时会报错,可先用 ls 确认。 大文件处理高效(只读取指定行,不加载全部内容)。 tail:查看文件末尾内容(如 tail -f 实时跟踪日志)。 less/more:交互式分页查看完整文件。

wc -l:计算文件或输入流的行数(常用于日志分析、代码统计等)。

wc -l filename.txt      # 统计文件行数(输出格式:行数 文件名)  
cat file.txt | wc -l    # 通过管道统计行数(仅输出数字)  
  • 统计日志文件错误次数:grep "ERROR" log.txt | wc -l
  • 检查代码行数:wc -l *.py

df -kh:查看磁盘空间使用情况(人类可读格式)

作用:显示文件系统的磁盘容量、已用空间、剩余空间(自动转换为 KB/MB/GB 单位)。 | -h | 以易读单位显示(如 KB/MB/GB) || -k | 强制以 KB 为单位(默认行为) || -T | 显示文件系统类型(如 ext4/nfs) |

  • du -sh:统计目录的实际磁盘占用(而非分区整体)。

路径和相关操作:~:根目录。.:当前目录。..:父目录。$PWD 是存储当前路径的环境变量,echo $PWD 等于 pwdtouch 摸一摸文件,将其访问时间和修改时间更新为当前时间。如果文件不存在,会创建之,因此常被用于新建文件夹。

find
find 是 Linux 下强大的文件搜索工具,可以递归遍历目录结构,根据文件名、类型、大小、权限、时间等条件查找文件。基本语法是 find [路径] [选项] [操作]

按名称查找find /home -name "*.txt" 查找 /home 下所有 .txt 文件。 按类型查找find /var -type d 查找 /var 下的所有目录。 按大小查找find / -size +10M 查找大于 10MB 的文件。 按时间查找find /tmp -mtime -7 查找 7 天内修改过的文件。 执行操作find /var/log -name "*.log" -exec rm {} \; 删除所有 .log 文件。

awk
awk 是一种文本处理工具,擅长按列提取、分析和转换数据。它逐行读取输入,按字段(默认以空格或制表符分隔)处理,并支持条件判断和计算。

打印特定列awk '{print $1, $3}' file.txt 输出第 1 列和第 3 列。 条件过滤awk '$3 > 100 {print $0}' data.csv 输出第 3 列大于 100 的行。 计算统计awk '{sum += $1} END {print sum}' numbers.txt 计算第 1 列的总和。 字段分隔符awk -F',' '{print $2}' file.csv 使用逗号作为分隔符。 内置变量NR(行号)、NF(字段数)、FS(输入分隔符)、OFS(输出分隔符)。

sed
sed(流编辑器)用于对文本进行查找、替换、删除等操作,支持正则表达式,适合批量编辑文件或流数据。

替换文本sed 's/old/new/g' file.txt 将所有 "old" 替换为 "new"。 删除行sed '/pattern/d' file.txt 删除包含 "pattern" 的行。 行范围操作sed '2,5d' file.txt 删除第 2 到第 5 行。 原地编辑sed -i 's/foo/bar/g' file.txt 直接修改文件(谨慎使用)。 正则匹配sed -n '/error/p' log.txt 打印包含 "error" 的行(类似 grep)。 sed -nE 's/.*Age: ([0-9]+).*/\1/p' data.txt

综合示例

  • 查找所有 .log 文件并统计行数: find /var/log -name "*.log" -exec wc -l {} \; ;提取日志中的 IP 地址: awk '{print $1}' access.log | sort | uniq -c ;批量替换配置文件中的路径: sed -i 's|/old/path|/new/path|g' *.conf

这三个命令常结合使用,比如先用 find 定位文件,再用 sedawk 处理内容,是 Linux 文本处理的经典组合。


连接符:

;:顺序执行,不论前一个指令是否成功执行。&&:只有前一个命令执行成功(返回状态码 0)才继续。||:只有前一个失败(返回非零状态码)才继续。优先级:&& > || > ;(也即,优先解析 &&$?: 前一条指令的状态码。$$:当前指令的进程码(pid)。!!:整个前一条指令。(sudo !! 强制执行上一行)

权限      文件数目(非文件夹则为1) 所有者 集群
-rwxr-xr-x             1           xtx  xtx     78 Feb 28 14:04 sshlinker.sh

$PS1:决定终端输出命令时提示符的样式。$PATH:环境变量们。使用 echo $PATH 输出,export PATH=$PATH:/home/ubuntu 修改(为什么没有空格?不然会被当成命令!)>:输出重定向。>>:输出追加。<:输入重定向。2>(没有空格):将 stderr 重定向。|:管道(pipe):将前一个命令的输出作为后一个命令的输入。grep:文本搜索工具。

grep [选项] "搜索模式" [文件...]
  • "搜索模式":可以是普通字符串或正则表达式。
  • [文件...]:可以是一个或多个文件,如果不指定文件,则从标准输入(stdin)读取数据。
  • -i 忽略大小写(case-insensitive) || -v 反向匹配,输出 不包含 模式的行 || -n 显示匹配行的 行号 || -c 只统计匹配的行数(count) || -l只显示 包含匹配项的文件名 || -r-R 递归搜索 目录下的所有文件 || -w 匹配 整个单词(word) || -A n显示匹配行及其后 n 行(After) || -B n显示匹配行及其前 n 行(Before) || -C n显示匹配行及其前后 n 行(Context || -e 指定多个模式(grep -e "pat1" -e "pat2") || -E 使用 扩展正则表达式(等同于 egrep) || -F 按字面字符串匹配(不解析正则表达式,等同于 fgrep) |
ps aux | grep "nginx"          # 查找 nginx 进程
cat /var/log/syslog | grep -i "error"
grep "^start" file.txt        # 匹配以 "start" 开头的行
grep "end$" file.txt          # 匹配以 "end" 结尾的行
grep "[0-9]{3}" file.txt      # 匹配 3 位数字(需 `-E` 或 `egrep`)
grep -E "error|warning" log.txt  # 匹配 "error" 或 "warning"

sort 排序,unix 去重(需要先排序)。

ps 用于 静态查看当前进程状态(快照式查询),默认只显示当前终端的进程。

  • | ps 仅显示当前终端相关的进程 || ps -eps -A 显示 所有进程 || ps -f 显示完整格式(UID, PID, PPID, CMD 等)|| ps -u 用户名 显示指定用户的进程 || ps -aux 显示所有进程(BSD 风格,包含 CPU、内存占用) || ps -ef 显示所有进程(标准 UNIX 风格) || ps -p PID 查看指定 PID 的进程 || ps --forest 以树状结构显示进程层级关系 |

输出字段:

  • PID:进程 ID ;PPID:父进程 ID ;USER:进程所有者 ;%CPU:CPU 占用率 ;%MEM:内存占用率 ;VSZ:虚拟内存占用(KB) ;RSS:物理内存占用(KB) ;TTY:进程关联的终端 ;STAT:进程状态(R=运行, S=睡眠, Z=僵尸等) ;START:进程启动时间 ;TIME:占用 CPU 时间 ;COMMAND:启动进程的命令

top 用于 动态实时监控系统进程,类似于 Windows 的任务管理器,默认按 CPU 使用率排序。

  • Shift + PCPU 使用率 排序 Shift + M内存使用率 排序 || Shift + T运行时间 排序 || k 杀死进程(输入 PID 后回车) || q 退出 top || h 显示帮助 || 1 显示所有 CPU 核心的使用情况 || u + 用户名 只显示指定用户的进程 || r 调整进程的优先级(renice |

结合 grep 查找进程

ps -aux | grep nginx      # 查找 nginx 相关进程

<tab>:自动补全;<Ctrl-R>:搜索曾经执行过的命令。<Ctrl-C>:杀死进程。<Ctrl-Z>:将进程暂停并挂起到后台。<Ctrl-D>EOF。如果当前正在输入,则结束输入,否则退出终端。<Ctrl-A/E>Home/End<U/K/W/L>:删到行首/行尾/前一个以空格分割的单词/清屏(等于 clear)。

fg:将暂停的进程恢复到前台继续运行。bg:让暂停的进程在后台继续运行。jobs:查看所有后台/暂停的任务。


Shell Globbing 是 shell 中使用的一种特殊模式,它并非 regexp。* 匹配任意数量的字符,? 匹配任何单个字符,[...] 匹配其中任何一个,[^...][!...] 匹配非其中任何一个,{...,...} 匹配其中任何一个串。mv *{.py,.sh} new_folder 将所有 .py .sh 转移。


为什么需要线程协作? 资源共享:例如多个用户共享一台计算机,或多个ATM共享同一银行账户。 加速:通过重叠I/O和计算,或利用多处理器并行处理任务。 模块化:将复杂问题分解为多个简单部分,例如编译器调用多个工具链(如cpp | cc1 | cc2 | as | ld)。

基础原子操作通常包括单字的加载(load)和存储(store),但复杂操作(如双精度浮点存储)可能需要额外注意。使用原子操作,可以实现如下功能:Synchronization:确保线程之间的配合。Critical Section:一段代码,在其被执行时,仅能有一个活跃线程。Mutual Exclusion:确保只能有一个线程去执行 Critical Section 的机制。lock 操作有时会过于粗粒度。底层:原子操作——Load/Store Disable Ints Test&Set Comp&Swap;中层: Locks Semaphores Monitors Send/Receive;高层:Shared Programs


P():不断等待直到 semaphore 回正后,将其减一。V():将 semaphore 加一。可能唤起等待的其它线程。 General rule of thumb: Use a separate semaphore for each constraint。因此,为互斥操作(写入/输出)、检测是否满、检测空位分别建立变量。P() 的顺序很重要,否则会引起 deadlock;V()的顺序不重要,但是可能影响效率(例如,先唤醒 emptyslots 后,因为 mutex 还在所以又睡回去了)


使用 lock 实现 mutual exclusion、condition variables 实现调度。lock 和 cv 二者共同组成了一个 monitor。Condition Variable:在 critical section 中 等待什么东西。通过如下方式调用:

  • Wait(&lock): 自动 release lock 然后休眠。在唤醒后,自动 acquire lock。Signal():唤醒一个休眠的线程。Broadcast(): 唤醒所有休眠线程。

为什么要 while(...)?这涉及到两种不同的 monitor 风格:Hoare monitor:在 signal 后,会立刻切换到被唤起的线程并挂起当前线程,此时 可以 保证被唤醒的线程条件成立,因此可以用 if 代替 while优点:逻辑更直观,减少不必要的唤醒检查。缺点:实现复杂,需要额外的上下文切换。Mesa monitor:``signal()仅将等待线程移到**就绪队列**,当前线程继续执行。被唤醒的线程**必须重新检查条件**(必须用while`)。优点:实现简单,性能更高(减少上下文切换)。缺点:可能因竞争导致条件再次失效(需循环检查)。教科书中往往使用 hoare,但是 mesa 在实际中使用。


在上锁的函数中,中途报错跑掉,是不行的:因为外层可以 catch 这个错并继续干活,但是锁还挂着呢!同理,中途调其它(可能会报错的)函数后,要记得 catch 并解锁。

C++ 中,推荐使用 {} 包裹的代码块,意外退出时自动解锁。这是好的:显式的锁有助于提高运行效率,但是更难去分析。Java 中,已经高度集成化了。Go 采取另一种策略:不共享内存,在需要共享的时候,发消息!

Most concurrency bugs (98%) are one of:Atomicity violations (not protecting shared resources) ;Order violations;Deadlocks。Type 1. problems are caused by under-protecting shared resources, type 3. often caused by over-protection. Fixes to type 3. bugs often create type 1. bugs. 😦

Good news: Most non-deadlock bugs involve only one variable Most (97%) of deadlocks involve two threads which access at most two resources . Not-so-good news: concurrency bugs seem to be a small fraction of all reported bugs, but consume a large fraction of debugging time (days per bug instead of hours).


CPU时间片、内存页、网络带宽等资源是可共享的,也即非竞争性 (non-preemptive) 的。这些东西不需要手动释放。与之相反的,磁盘空间、锁等东西是竞争性的,需要手动释放。此外,另一个属性是资源是否需要排他性的 access。例如,只读文件往往是可共享的;打印机在打印时是不可共享的。

Starvation:线程无限期地等待。例如,低优先级线程总是为高优先级让路。Deadlock:两个线程互相等。“逻辑学家和五根筷子”Starvation 可以(但不一定)结束,但是死锁没有外界干涉不会自行终结。DL 也不一定确定:换一个顺序就可能突然不 DL 了。四种会导致 DL 的场合:

  1. Mutual Exclusion Only one thread at a time can use a resource. Hold and Wait A thread holding at least one resource is waiting to acquire additional resources held by other threads. No Preemption Resources are released only voluntarily by the thread holding the resource, after the thread is finished with it. Circular Wait There exists a set of waiting threads such that dependency forms circles.

三种 handling 死锁的方法:允许系统陷入死锁后自行恢复。需要死锁探测算法,和强制 terminate 的技巧;确保系统永远不会陷入死锁。需要对 lock 占用保持监视;忽视死锁!我们不妨假装死锁不会出现。很遗憾的是,最后一种是最常见的解决方案。不过,还是有些能用的技巧:分配足够多的资源;让线程之间不再共享内存(不太现实);不允许等待。打电话:做出尝试,如果失败,返回“忙线中”,或者:所有人同时通话,如果撞车,retry;一开始就分配好。但是我们无法预知未来;以某种固定顺序申请资源。先统一申请磁盘,再是内存,再是……这样可以避免循环死锁。

posted @ 2025-04-21 17:05  Troverld  阅读(94)  评论(1)    收藏  举报