OS

OS

图形化界面都是通过发消息给某个可以直接与操作系统图形层通信的进程交互。

程序状态的两种表现:runtime/source code

宏内核:OS 运行在管理者模式,不需要考虑操作系统哪一个部分需要什么硬件特权,但错误是致命的(可能会导致所有用户程序崩溃)。

微内核:最小化 OS 在管理者模式下的运行,在用户模式执行 OS 的大部分。

进程的结构(xv6)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum procstate { UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE };

struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  int xstate;                  // Exit status to be returned to parent's wait
  int pid;                     // Process ID

  // wait_lock must be held when using this:
  struct proc *parent;         // Parent process

  // these are private to the process, so p->lock need not be held.
  uint64 kstack;               // Virtual address of kernel stack
  uint64 sz;                   // Size of process memory (bytes)
  pagetable_t pagetable;       // User page table
  struct trapframe *trapframe; // data page for trampoline.S
  struct context context;      // swtch() here to run process (register
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

开机

进入 boot loader,boot loader 将 xv6 内核加载到内存。CPU 在机器模式下,从_entry 执行 xv6,RISC-V 启动时,分页硬件未启动,虚拟地址直接映射到物理地址。loader 将 xv6 内核加载到物理地址 0x80000000 的内存上 (从 0x0 到 0x80000000 这段地址包含了 I/O 设备)。

xv6 系统调用

  1. 操作系统初始化时注册系统调用
  2. 系统调用函数将对应的调用存到寄存器
  3. 保存用户空间的状态
  4. 执行 ECALL 指令,提升权限
  5. 执行 trampoline 里面的 uservec() ,保存现有的用户寄存器到 trapframe,切换页表
  6. 执行 usertrap(),判断陷入类型并处理
  7. 执行 syscall(),进行系统调用
  8. 执行 usertrapret() ,设置 trapframe
  9. 执行 trampoline 里面的 userret,恢复寄存器,返回用户空间 reference: NJUOS, 6.828

页表

SATP 寄存器保存页表

MMU 不会保存页表,只会在读取 SATP 寄存器的值以进行页表的读取

多级页表

在三级页表中,结构如下 pte: page table entry ppn: Physical page number

|pte|pte|pte|page| |9 |9 |9 |12 |

可以将 pte 看做是一个指针数组,指向下一个 pte 或 page

虚拟地址的 3 个 9 bit 索引到对应的 ppn

每个进程有自己的虚拟地址,同时进程会维护一个 SATP(Supervisor Address Translation and Protection)寄存器,包含了指向页表的物理地址。

缓存

因为需要三级映射,所以加一个直接映射的缓存会很有用(但在切换 page table 的时候需要清空 TLB)

Translation Lookside Buffer(页表缓存)TLB

物理内存

xv6_memory

0x1000 是 Boot Loader,用于加载操作系统

虚拟内存从 0x02000000 开始,到 PHYSTOP,与物理地址是一样的

低于 0x80000000 的地址空间不存在与 DRAM,访问这些地址的时候,指令会走向其他的硬件

将内核对象映射到用户内存

  1. 初始化页表时添加映射(和寄存器一样)
  2. 初始化内存时正常请求内存以及初始化
  3. 回收页表时取消映射

Page faults

copy-on-write:写时复制,fork 的时候先不复制内存,等到修改时再复制 lazy allocation:懒加载,申请时先不分配空间,使用时再分配 zero fill on demand:请求分页,创建进程并进行大变量的初始化分配时,仅初始化一个 page,将虚拟地址空间全 0 的 page 都 map 在这一页上面 demand paging:需要的时候再加载 text 和 data paging to disk:硬盘分页,内存不够时驱逐一些页到物理内存,重新访问时再调回 automatically extending stacks:自动扩展栈空间,当栈空间不足时,在运行时自动扩展栈大小 memory-mapped files:内存映射文件,将文件映射到虚拟地址空间,使得文件内容可以像访问内存一样被访问

sleep & wakeup

xv6 线程切换时需要用到线程自己的锁,而不能占有其他任何锁

在涉及到硬件的实现时,因为硬件响应的时间和 CPU 时间不对等,可以在执行完一段操作之后释放 CPU 资源,等待硬件响应完再获取 CPU 进行下一步的处理

network

多个 LAN(Local Area Networks) 可以通过 Routine 连接

局域网通信由以太网协议决定,局域网则由 Internet Protocol 决定

多个锁需要调用时,需要进行排序。比如“哲学家吃饭”问题,圆桌上的奇数椅子的哲学家先拿起右边的筷子;偶数椅子的哲学家先拿起左边的筷子就不会死锁。

自旋锁

aquire

  • 关闭中断(防止锁持有的过程中触发中断调用的中断函数获取锁导致死锁)
  • 检查死锁(是否本 CPU 已经持有了相同的锁)
  • while(atomicSwap(lock,1)!=0); // 如果 lock 本来就是 1,会返回 1
  • 关闭编译器优化(保证持锁期间代码不被编译器重排),设置持锁的 CPU

release

  • 检查死锁(是否本 CPU 并未持有相同的锁)
  • 开启编译器优化
  • atomicSwap(lock,0);
  • 开启中断
updatedupdated2024-04-122024-04-12