xv6操作系统问答式梳理(MIT6.S081)

本文详细阐述了xv6系统的关键特性,包括系统调用流程、sysinfo和getpid的加速、页表机制、alarm功能、上下文切换、用户线程和文件系统设计,以及内存管理、锁策略和死锁预防。
摘要由CSDN通过智能技术生成

xv6系统调用的流程?

  • 拿trace函数举例,首先在用户态执行系统调用trace

  • 函数使用CPU提供的ecall指令,跳转到内核态

  • 到达内核态统一系统调用处理函数 syscall(),所有系统调用都会跳到这里来处理

  • syscall() 根据跳板传进来的系统调用编号,查询 syscalls[] 表,找到对应的内核函数并调用

  • 到达 sys_trace() 函数,执行具体内核操作

添加的系统调用sysinfo是如何实现的?

sysinfo的功能是返回空闲内存和已经创建进程的数量,具体实现上,由于xv6使用链表管理空闲页面,通过遍历链表即可计算空闲内存的大小,遍历所有进程,其中状态为未使用的进程数即为已经创建进程的数量

你是如何加速系统调用getpid的?

pid存在进程控制块中,进程控制块在内核里,用户需要通过系统调用才可以读取内核。这里可以创建一个共享页面,记录虚拟地址USYSCALL到pid所在页面物理地址的映射,这样用户态可以通过访问共享页面获得pid,无需进行系统调用,因此加速了getpid的过程

xv6的页表机制是什么样的?你做了什么样的修改?

xv6采用了三级页表,每一级有512个页表项。实验中对三级页表进行了打印,并为页表项增添标志位,以实现系统调用sys_pgaccess(获知哪个页面被访问过)

alarm是如何实现的?

alarm是报警函数,进程使用 cpu 时,每隔一个特定的时间发送信号sigalarm通知内核,进程陷入,随后,内核保存各种寄存器和状态,并执行中断处理函数,执行完后内核恢复保存的状态并返回,从中断处继续执行,alarm函数限制进程使用cpu的时间

介绍一下函数调用栈

栈帧是因函数运行而临时开辟的空间,每调用一次函数,便会在栈上创建一个独立栈帧,其中存放函数中的必要信息,如局部变量、函数传参、返回值等。当函数运行完毕栈帧将会销毁。

栈从高地址向低地址增长,sp指向栈顶(当前栈帧的位置,会移动),fp指向栈底,创建新的栈帧时对指针做减法

栈帧大小不同,但return address、指向前一个栈帧的指针在固定位置

栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。
在 xv6 中,使用一个页(4KB)来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底。

backtrace是如何实现的?

backtrace的作用是打印栈帧,首先获取当前的fp,指向当前栈帧的起始地址(fp 虽然是帧开始地址,但是地址比 sp 高),随后打印fp-8位置处的地址,即为栈帧的返回地址,再将指针移动到fp-16,指向上一个栈帧,直到遍历完成。

xv6如何分配内存?

  • 首先malloc维护了一个内存池,当申请的内存超过内存池可以提供的内存

  • 进程可以通过kalloc动态申请内存,xv6有一个空闲页面组成的链表,分配页面的时候会将空闲页面移出,随后可以通过kfree释放内存,内存释放后需要将空闲页加入链表

写时复制的原理、如何实现、优缺点

  • 原理:推迟分配物理内存,直到真正使用的时候,再分配物理内存

  • 实现:首先,父进程页表项设置为只读,父进程的物理内存映射给子进程(页表项仍为只读),同时标记为COW页;子进程想要向物理页写数据时,CPU引发页错误;随后,分配新物理页,把原始页中的数据复制到新分配的页中,修改子进程页表项,指向新分配的内存,把新页面的页表项标记为可写PTE_W;需要释放页面时,先确保物理页没有进程使用(引用计数为0),随后释放(借用一个数组实现,索引代表页,值代表引用个数)

  • 优点:需要的时候再复制页面,减少不必要的资源分配和延时

  • 缺点:如果写页面频繁,会出现大量的页错误

介绍一下上下文切换?

  • 上下文指的是通用寄存器的值和栈的状态,上下文切换发生在进程调度的时候,会保存当前进程的相关信息,将另一进程的相关信息存入到相关寄存器中

  • 对于线程,如果多个线程属于同一进程,虚拟内存共享,只需要切换一些私有数据,虚拟内存保持不动

xv6如何实现用户线程?

  • 每个用户进程只有一个用户线程,每个进程一张页表,用户线程不会共享内存

  • 每个用户线程对应一个内核线程,所有内核线程使用同一张内核页表,内核线程会共享内存

xv6如何进行线程切换?

  • 由于处理器数量有限,xv6需要在多个进程之间切换共享CPU资源

  • 每个用户进程只有一个用户线程,每个用户线程对应一个内核线程,每个CPU有一个调度器线程(内核中)

  • xv6通过swtch函数进行用户线程的切换,该函数被调用时会保存上下文到内核栈中,再把控制权转交给调度器线程,调度器线程选择下一个要运行的进程,并通过swtch恢复所选进程的上下文,继续执行

  • 同时,在调用swtch函数之前,必须加锁,以防上下文切换过程中发生状态变化

介绍一下xv6中有哪些锁

  • xv6实现了自旋锁和休眠锁

  • 自旋锁是一种独占锁,加锁失败后线程会忙等待(可能会消耗大量CPU资源),使用整型变量表示,0表示未被占用,线程想要获取锁,会把该值设置为1,这是一个原子操作,等线程完成对共享资源的访问,则会将变量设置为0,表示锁已经释放

  • 休眠锁:进程对资源加锁失败后会进入休眠状态,释放CPU资源,等待条件满足时才被唤醒。在xv6中睡眠锁通过自旋锁+循环+sleep实现

自旋锁和休眠锁有哪些应用场景?

  • 自旋锁适合持有锁时间比较短的场景,因为在持有锁的过程中线程会持续占用CPU

  • 睡眠锁适合那些持有锁时间比较长的场景

加锁的优缺点?

  • 锁的作用保证了临界区内操作的序列化,但也会使得编程复杂、带来死锁问题,同时,锁的粒度应当尽可能的小,大锁安全但是会降低并行性

  • xv6中的kalloc就用了一把大锁,如果多个CPU想要申请内存,运行会变得很慢,而文件系统使用的是细粒度的锁,每个文件都有一把锁

xv6如何实现线程安全的哈希表

  • 起初,我为哈希表的put和get操作整体加锁,这样保证每个时刻只有一个线程在操作哈希表,但是发现性能不佳,实际上等同于将哈希表的操作变回单线程

  • 后面,我对锁进行了优化。只需要确保两个线程不会同时操作同一个 bucket 即可,并不需要确保不会同时操作整个哈希表,所以可以将加锁的粒度,从整个哈希表一个锁降低到每个 bucket 一个锁

你是怎么降低lock condition的?

  • xv6所有缓存用一条链表连接,并为空闲页面链表添加了一把大锁,这样做性能不高

  • 我的应对方式是为每个空闲页面分别设置一个锁,从而降低了每个锁的锁征用

加锁的原则

  • 优先加大锁,实在不行大锁化小锁,减小锁粒度

  • 只在有线程安全要求的代码处加锁

  • 锁分离,如读写锁

  • 对于不可能被共享的对象,消除对这些对象的加锁操作

xv6的文件系统是怎么实现的?

  • xv6文件系统采用混合索引的方式,每个文件所占用的前 12 个盘块的盘块号是直接记录在 inode 中的(每个盘块 1024 字节),

  • 大于 12 个盘块的部分,会分配一个额外的一级索引表(一盘块大小,1024Byte,包含256个盘块号),用于存储这部分数据

  • 实验中我为文件系统加上了二级索引页,第14号盘块指向一级索引页,一级索引页指向二级索引页,再指向数据页

  • 所有文件的inode都会被存储在磁盘上。系统和进程需要使用某个inode时,这个inode会被加载到inode缓存里。

软链接与硬链接的区别?

  • 软链接也是一种文件,保存另一个文件的路径名,在打开文件的时候根据保存的路径名再去查找实际文件,硬链接则指向一个存在文件的inode

  • xv6中我将inode中第一个直接块存储文件路径,需要打开文件时,递归跟随符号链中存储的路径

mmap函数是如何实现的?

  • 函数的作用是将文件直接映射到内存当中,之后对文件的读写就可以直接通过对内存进行读写来进行,而对文件的同步则由操作系统来负责完成

  • 使用mmap可以避免对文件大量 read 和 write 操作带来的内核缓冲区和用户缓冲区之间的频繁的数据拷贝

  • mmap系统调用使用懒加载(延迟加载),因为需要读取的文件内容大小很可能要比可使用的物理内存要大

  • 文件不再需要的时候需要调用munmap解除映射

  • 具体实现上,首先要在用户的地址空间内找到一片空闲的区域,为了防止和其他进程的地址空间产生冲突,将其映射到trapframe的下面,向下生长

  • 在进程结构体里添加数组,数组元素个数为想要同时支持的文件个数

  • mmap调用时,找到一个没被使用的用于映射文件的虚拟地址空间vma,增加文件的引用计数,并把空间插入到进程vma链表中

什么是懒分配?

  • 分配内存时,只增大进程的内存空间字段值,但并不实际进行内存分配;当该内存段需要使用时,会发现找不到内存页,抛出 page fault 中断,这时再进行物理内存的分配,然后重新执行指令

遇到什么困难?你是怎么解决的?

  • 加速系统调用的那个实验实在是看不懂,我就去看他的文档、手册,理解原理,然后对照提示和源码梳理流程

  • 有时候文档写的很晦涩,我就去参考Linux中的设计,比如cow和mmap

  • 优化锁的时候加大锁性能不佳,我就尝试把大锁转换成小锁,最终调试通过

有什么收获?

  • 通过这个lab,我更深入的了解了操作系统进程切换、内存管理、系统调用、锁机制,锻炼了分析问题、解决问题的能力,养成阅读源码和文档的习惯,并能够调试诊断错误

其它相关问题

  • 为什么要有虚拟内存?

    • 内存隔离:使得各个进程拥有独立的地址空间,互不干扰

    • 更大的地址空间:组合物理内存和外存,使得用户拥有比实际上大得多的地址空间

    • 文件共享:可以将文件映射到进程的虚拟地址空间,使得多个进程共享一个文件的虚拟内存副本

    • 优化内存管理:可以将不常用的数据页移动到硬盘上

  • 为什么虚拟内存之和可以超过物理内存?

    • 基于局部性原理,内存有重复访问的倾向性,不经常访问的页面,可以将其换出内存

    • 操作系统基于缺页故障,实现了fork写时复制等延迟分配的机制

  • 内存管理有哪些模块?

    • 内存的分配与回收:管理内存池,为进程分配内存块,回收不再使用的内存块

    • 虚拟内存管理(扩充内存空间):地址转换、页表机制、页面置换、共享内存

    • 内存保护:确保进程不能越界访问其他进程的内存,一般设置上下限地址寄存器、限长寄存器

  • 页表有什么作用?

    • 页表存的是虚拟地址到物理地址的映射,作用是实现地址转换,系统中的每个进程都有一张页表
  • 页表项中还有哪些位?

    • 有效位:将地址空间中没有使用的页面标记为无效,就不需要为其分配物理帧,从而节省内存

    • 存在位:标识页面是否在内存中,还是说已经被换出

    • 脏位:页面是否被修改过

    • 访问位:页面是否被访问过

  • 三级页表相比于一级页表的优劣?

    • 优势:

    • 对于一级页表,页表项过多的话,整张页表会过大,且需要为其分配完整的物理内存空间,这是一种内存上的浪费,引入多级页表后,线性的页表转换成树状结构,页面可以稀疏存储;

    • 每个层级的页表分配给不同进程,可以更好的实现内存隔离

    • 缺点:

    • 访问多级页表,速度会变慢

    • 如果页面没有命中,需要加载多次才能获得正确的地址转换信息

    • 地址映射比较复杂

  • TLB快表的作用和原理?

    • TLB是一种高速缓存,作用是加速虚拟地址到物理地址的转换,减少访存的次数

    • 基于局部性原理,记录最近访问过的映射关系

  • 局部性原理?

    • 时间局部性:近期访问过的页面不久之后可能会再次访问

    • 空间局部性:访问过的页面,它附近的页面也会被访问

  • 地址转换的流程?

    • CPU发出一个虚拟地址访问请求,虚拟地址被发送到内存管理单元MMU

    • MMU查询TLB,看是否访问过,如果TLB命中,直接进行地址转换,TLB没有命中,则查询页表,页表中存在该页面,则进行地址转换,访问物理内存,页表中不存在该页面,发生缺页中断,从外存中调页

  • 发生中断的场景有哪些?

    • 程序执行系统调用

    • 程序出现缺页中断、运算除0之类的错误

    • 硬件触发中断

  • 缺页中断物理内存不足会怎样?

    • 操作系统会根据页面置换算法,如:最近最久未使用、先入先出等方式选择一页换出,再换入一页
  • 什么是线程池?

    • 在服务器启动之初就创建的一组子线程,所有线程运行相同的代码,当新任务到来,从线程池中选择某个子线程为之服务,线程池中线程的数量应当和CPU数量差不多
  • 堆和栈的区别?

    • 堆空间大、栈空间小

    • 栈空间操作系统自动分配释放,存储静态变量、函数参数等,使用较安全;栈空间由程序员手动申请释放,容易发生内存泄漏

    • 栈的生命周期更短,不适合长期存储数

  • 进程、线程、协程的区别

    • 进程是资源分配的最小单位,线程是任务调度的基本单位

    • 协程可以理解为用户态线程,内核视角无法察觉协程存在

  • 线程有哪些实现方式?

    • 用户线程:线程的TCB存在用户态,线程的管理对操作系统透明,多个用户级线程对应一个内核级线程

      • 优点:线程切换比较快,不需要内核态和用户态的切换

      • 缺点:一个用户线程阻塞了,整个进程都会阻塞;时间片分配给进程,多线程执行时,每个线程得到的时间片较少

    • 内核线程:由内核管理线程,一个用户线程对应一个内核线程

      • 优点:某个线程被阻塞,不会影响其它线程的运行

      • 缺点:线程的创建、终止、切换都是通过系统调用,开销比较大

    • 轻量级线程

  • 如何实现多线程?

    • 并行,多个CPU,每个CPU运行一个线程

    • 并发:分时复用CPU,一个CPU在多个线程间来回切换

  • 什么是死锁?

    • ≥2个线程并行执行,由于争夺资源相互等待,没有外力的作用下,线程会一直相互等待,没办法继续运行
  • 死锁的四个条件?

    • 互斥:多个线程不能同时使用同一个资源

    • 请求保持:线程在等待资源时并不会释放已经持有的资源

    • 不可剥夺:线程已经持有了资源,在自己使用完之前不能被其他线程获取

    • 循环等待:两个线程获取资源的顺序形成了环路链

  • 怎样避免死锁问题?

    • 避免一个线程同时获取多个锁

    • 使用锁超时,线程等待锁超过一段时间后放弃

    • 顺序资源分配

    • 银行家算法(一种死锁避免的算法)

  • 24
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值