操作系统 - wangsun39/Learning GitHub Wiki
蒋老师B站视频
课件
教科书 OSTEP
https://jyywiki.cn/OS/OS_References
https://github.com/NJU-ProjectN/
AbstractMachine
轻量级libc
https://musl.libc.org/
newlib
轻量级osxv6
AI prompt: 我在做 [X]。如果你是一位专业人士,有更好的方法和建议吗?尽可能全面。
线程有自己独立的栈空间,默认是8M,在堆上用mmap申请
对于2个线程对一个全局变量进行N次的++操作,结果不一定是2N,甚至可能<N
- 单条指令的原子性 便我们强制使用一条汇编指令完成 sum++,依然无法保证最终得到 2N 的结果
- 语句执行顺序可能被破坏 设置屏障
asm volatile ("" ::: "memory");
violate 变量 - 存在全局指令执行顺序的假设
真实的内存模型往往 在不同处理器可能看到不同的共享内存,内存之间的同步并不是立即完成的,这给并发编程带来了很多麻烦
Atomic load & store 读/写单个全局变量是 “原子不可分割” 的 但这个假设在现代多处理器上并不成立 所以实际上按照模型直接写 Peterson 算法应该是错的?
“实现正确的 Peterson 算法” 是合理需求,它一定能实现
Compiler barrier/volatile 保证不被优化的前提下 处理器提供特殊指令保证可见性 编译器提供 __sync_synchronize() 函数 x86: mfence; ARM: dmb ish; RISC-V: fence rw, rw 同时含有一个 compiler barrier
自旋锁 (线程直接共享 locked)
更快的 fast path xchg 成功 → 立即进入临界区,开销很小 更慢的 slow path xchg 失败 → 浪费 CPU 自旋等待
互斥锁 (通过系统调用访问 locked)
更经济的 slow path 上锁失败线程不再占用 CPU 更慢的 fast path 即便上锁成功也需要进出内核 (syscall)
在 xchg 的假设下简化实现
包含一个原子指令
包含一个 compiler barrier
包含一个 memory fence
sum-spinlock demo
int locked = 0;
void lock() {
while (xchg(&locked, 1));
}
void unlock() {
xchg(&locked, 0);
}生产者和消费者
问题:一定是某个合法括号序列的前缀,括号嵌套的深度不超过n,
((())())((( 合法,
(((()))), (())) 不合法
同步:
等到有空位再打印左括号
等到能配对时再打印右括号
互斥锁的方法
#include "thread.h"
#include "thread-sync.h"
int n, count = 0;
mutex_t lk = MUTEX_INIT();
void Tproduce() {
while (1) {
retry:
mutex_lock(&lk); // lock - unlock 类似于 spinlock,性能较低
if (count == n) {
mutex_unlock(&lk);
goto retry;
}
count++;
printf("(");
mutex_unlock(&lk);
}
}
void Tconsume() {
while (1) {
retry:
mutex_lock(&lk);
if (count == 0) {
mutex_unlock(&lk);
goto retry;
}
count--;
printf(")");
mutex_unlock(&lk);
}
}
int main(int argc, char *argv[]) {
assert(argc == 2);
n = atoi(argv[1]);
setbuf(stdout, NULL);
for (int i = 0; i < 8; i++) {
create(Tproduce);
create(Tconsume);
}
}性能不高,因为lock - unlock,使得占用了CPU,能不能采用 sleep - wakeup的方式?
引出条件变量的概念
-
wait(cv, mutex) 💤
-
- 调用时必须保证已经获得 mutex
-
- 释放 mutex、进入睡眠状态
-
signal/notify(cv) 💬 私信:走起
-
- 如果有线程正在等待 cv,则唤醒其中一个线程
-
broadcast/notifyAll(cv) 📣 所有人:走起
-
- 唤醒全部正在等待 cv 的线程
互斥锁与条件变量的区别
互斥锁在unlock之后,会立即尝试lock,即使条件不发生任何变化,这次的操作可能就是白跑一趟,浪费了CPU。
而条件变量被唤醒时,是有其他线程通知(有可能就是条件发生变化,才通知的),当然之后还是要去尝试lock,并判断条件是否成立,才能进行操作
条件变量 - 例子:生者和消费者要使用不同的条件变量
信号量实现生产者消费者
#include "thread.h"
#include "thread-sync.h"
sem_t fill, empty;
void Tproduce() {
while (1) {
P(&empty);
printf("("); // 没有加锁
V(&fill);
}
}
void Tconsume() {
while (1) {
P(&fill);
printf(")"); // 没有加锁
V(&empty);
}
}
int main(int argc, char *argv[]) {
assert(argc == 2);
SEM_INIT(&fill, 0);
SEM_INIT(&empty, atoi(argv[1]));
for (int i = 0; i < 8; i++) {
create(Tproduce);
create(Tconsume);
}
}蒋老师课程中的例子,用信号量实现生产者,消费者,同样也可以用信号量实现条件变量和互斥锁,不过用信号量实现条件变量是比较麻烦的 上面的例子中,与OSTEP中例子有个差别,printf(")");前后没有加一个互斥锁,原因是这里printf没有操作共享数据
信号量比较适合的应用场景:
- happens before
- 计数型同步
线程的优点,可以利用多处理器
线程的缺点,线程占用资源多,CPU调度的开销大
协程的优点,占用资源小
协程的缺点,一个协程block,会阻塞整个线程\
Weak memory model 允许不同观测者看到不同结果
Since C11 data race is undefine behavior, 可以参考cppreference中的定义
https://en.cppreference.com/w/cpp/language/memory_model
防御性编程
- 增加一些assert保护一些场景不应该出现
- 增加一个check
代码动态检查工具:sanitizer
预分配内存在头尾增加一些额外的空间,存储一些特殊字符,定时检查这些字符是否被覆盖
lockdep:固定上锁的顺序关系,输出日志,工具分析日志
低配版lockdep:统计lock次数,lock 就++,unlock 就--, assert短时间内lock次数不会非常大
死锁
-
AA deadlock: spinlock - 关中断(需要增加临界区中锁个数计数器,计数器为0时,才能关)
-- spinlock-xv6.c
-- if (holding(lk)) panic(); -
ABBA deadlock: 哲学家吃饭问题
-- 为每种锁排序,需要加锁的时候(可能只对部分锁上锁),总按这个顺序加锁 -
尽量只上一把锁
-
lock-dep,上锁顺序依赖
-
用程序(如:图方法)来检测数据竞争,死锁
现代复杂软件系统必备的支撑工具
AddressSanitizer (asan); (paper): 非法内存访问
Buffer (heap/stack/global) overflow, use-after-free, use-after-return, double-free, ...
Linux Kernel 也有 kasan
ThreadSanitizer (tsan): 数据竞争
MemorySanitizer (msan): 未初始化的读取
UBSanitizer (ubsan): undefined behavior
Misaligned pointer, signed integer overflow, ...
Kernel 会带着 -fwrapv 编译
abstract Machine
https://jyywiki.cn/AbstractMachine/
CPU Reset (Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A/3B)
CPU reset -> Firmware -> Bootloader -> Kernal_start()
加载第一个程序systemd
gdb调试过程中
用!cmd可以执行shell命令
record full 模式,可以把状态机每步记录下来,然后返回去执行每个语句
reverse execute
有些syscall不能保证有效
info proc mappings 可以打印内存maps
gdb_python_script.py 自定义一个python脚本,定义一些自己的命令,可以辅助调试时打印一些内容\
可以通过pmap命令查看进程的内存地址空间
也可以通过/proc/pid/maps查看,两者内容类似
/proc/pid/maps 中能看到两个段:vvar和vdso是一些存放通过直接读取可读内存的系统调用的函数的数据,例如gettimeofday
因此 gettimeofday 虽然是系统调用,但它们可以很快的执行
vvar存在这些数据,而vdso存放的时代码段
如果想获取其他进程的内存地址空间的数据,可以通过
- 打开/proc/pid/mem中的数据进行,具体的方法需要再查
- 或者使用ptrace,这个也需要再看
类似于热补丁,在进程执行过程中,把一个函数实现替换成另外一种
实验生存指南 课中有提到
精简的libc库,musl
musl-gcc 静态编译
perror() 系统统一的报错打印
environ
环境变量也是通过参数放入函数条用栈中, 规范在Figure 3.9中
sysv-abi
extern int main ( int argc , char argv[ ] , char envp[ ] );
可以自定义自己的加载器,ld.so