操作系统 - wangsun39/Learning GitHub Wiki

0

蒋老师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 变量
  • 存在全局指令执行顺序的假设
    真实的内存模型往往 在不同处理器可能看到不同的共享内存,内存之间的同步并不是立即完成的,这给并发编程带来了很多麻烦

并发控制

Peterson算法

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的方式?
引出条件变量的概念

条件变量 API

  • 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

并发 bug

防御性编程

  • 增加一些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,上锁顺序依赖

  • 用程序(如:图方法)来检测数据竞争,死锁

防御性编程

assert

CHECK_INT

运行时检查

lockdep

现代复杂软件系统必备的支撑工具

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/

计算机reset的过程

CPU Reset (Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A/3B)

bootloader的原理

CPU reset -> Firmware -> Bootloader -> Kernal_start()
加载第一个程序systemd

状态机模型的应用

工具:

strace

gdb

gdb调试过程中
用!cmd可以执行shell命令
record full 模式,可以把状态机每步记录下来,然后返回去执行每个语句
reverse execute
有些syscall不能保证有效
info proc mappings 可以打印内存maps
gdb_python_script.py 自定义一个python脚本,定义一些自己的命令,可以辅助调试时打印一些内容\

perf

进程地址空间

可以通过pmap命令查看进程的内存地址空间
也可以通过/proc/pid/maps查看,两者内容类似
/proc/pid/maps 中能看到两个段:vvar和vdso是一些存放通过直接读取可读内存的系统调用的函数的数据,例如gettimeofday
因此 gettimeofday 虽然是系统调用,但它们可以很快的执行
vvar存在这些数据,而vdso存放的时代码段

如果想获取其他进程的内存地址空间的数据,可以通过

  • 打开/proc/pid/mem中的数据进行,具体的方法需要再查
  • 或者使用ptrace,这个也需要再看

软件动态更新

类似于热补丁,在进程执行过程中,把一个函数实现替换成另外一种
实验生存指南 课中有提到

libc

精简的libc库,musl
musl-gcc 静态编译

perror() 系统统一的报错打印

environ
环境变量也是通过参数放入函数条用栈中, 规范在Figure 3.9中 sysv-abi
extern int main ( int argc , char argv[ ] , char envp[ ] );

链接加载

可以自定义自己的加载器,ld.so

⚠️ **GitHub.com Fallback** ⚠️