mit是宏内核还是微内核?※分别说一下这两个的优缺点?你有了解混合内核吗,混合内核跟前面两个有什么区别?※
- 828是宏内核,宏内核是把所有核心功能都装载到内核当中,例如内存管理、进程管理等待 这样子实现简单,调用方便,缺点就是功能全部糅杂到一起,如果内核中一个功能崩溃那么整体内核就不可用,维护困难。
- 微内核有点类似微服务的思想,将多个功能分散开来形成独立的功能,通过最小化内核,将大部分功能实现在用户态,提高系统的可靠性和安全性,更有利于维护和管理。缺点就是:当功能繁多的时候,各个功能直接需要频繁的通信,会导致性能较差、复杂度提升。
- 混合内核顾名思义就是从宏内核和微内核中做折中方案,例如windows 将重要的内存管理 等核心功能实现在内核中,将另外的功能以可插拔的形式实现为内核模块 运行在用户空间。
bios到kernel的启动过程是什么样的?,实模式和保护模式所做的操作有哪一些,转换后mmu的作用。
bios到kernel的启动过程为:
- 接通电源后,CPU开始执行BIOS的代码
- BIOS执行硬件的初始化,从设备中寻找可启动的设备,所以会找到磁盘
- 因为磁盘的第一个扇区装载的是
引导类加载器
,所以会把硬盘头一个扇区加载到内存0x7c00的地址处并对引导类加载器
初始化 - 之后BIOS将CPU资源交给引导类加载器负责,引导类加载器就将内核的镜像(存在第二个扇区)加载至内存指定位置当中
- 此时CPU控制权继续转移到内核代码上,从此开始内核就真正被运行起来了
实模式和保护模式所做的操作:
- 实模式中负责初始化GDT表和IDT表(主要通过lgdt加载GDT表、lidt加载IDT表)
- GDT是一张存储在内存中的表,用于描述操作系统的各种内存段的基址以及长度等信息,为了防止程序恶意的使用其他段,在保护内存中,每个进程存在一份LDT表,而LDT包含的段描述符又存在于GDT中。
- IDT是一张存储在内存中的表,用于描述操作系统的各种中断以及处理函数,当发生中断时,CPU即可通过这张表找到对应的处理函数
- 将CS和DS段基址分别指向GDT中的 代码段 和 数据段
- 将CR0寄存器的PE(保护模式使能,也就是第1bit位 )位设置为1,表示CPU进入保护模式
- 在保护模式后,由于访问地址需要通过MMU的转换,所以必须要先设置好页表以及开启分页机制,此后便可真正访问到虚拟空间的内存
- MMU的作用:用于处理CPU中的虚拟地址和物理地址之间的转换
MMU是如何实现内存地址转换的?
分页机制:内存会被划分为等大小的页(6.828是4KB),每个页都有单独对应的物理地址
地址转换:CPU执行命令时,会将虚拟地址发送到MMU当中,由MMU通过CR3
寄存器获取当前进程的页表,然后通过高10位找到页目录中的页表,再通过中间10位找到页表中对应的页表项,接着利用最后的12位来查询页表项中的虚拟地址,虚拟地址上存着的是物理地址,从而获取到物理地址,若是不存在物理地址,则会触发page Fault,这时候会跳转到内核态中,执行对应的page_fault_handler
函数并判断是否是在用户态的缺页异常:
- 如果是,则在页错误处理函数中通过
pgdir_walk()
函数,分配到一个物理页到错误虚拟地址上,接着更新页表。此时页表上导致缺页错误的虚拟内存
已经成功映射上物理地址
,则会将tf->eip
(即中断返回地址)修改为对应的刚刚分配的物理地址,然后返回用户态。 - 如果不是,则直接报错,结束进程的运行。
※是如何进行虚拟内存管理的?虚拟内存的整体布局,你可以简略的描述一下吗?※
-
是通过二级页表将虚拟内存空间映射到物理内存空间上,从而实现了进程内存隔离、内存保护等功能
-
内存空间布局如下:
/* 虚拟内存分布图: * Virtual memory map: Permissions 权限 * kernel/user 内核/用户 * * 4 Gig --------> +------------------------------+ --+ * | | RW/-- | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | * : . : | * : . : | * : . : 4Gig-256MB * |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/-- | * | | RW/-- | * | Remapped Physical Memory | RW/-- | * | | RW/-- | * KERNBASE, ----> +------------------------------+ 0xf0000000 --+ * KSTACKTOP | CPU0's Kernel Stack | RW/-- KSTKSIZE | * | - - - - - - - - - - - - - - -| | * | Invalid Memory (*) | --/-- KSTKGAP | * | 无效内存 (*) | * +------------------------------+ | * | CPU1's Kernel Stack | RW/-- KSTKSIZE | * | - - - - - - - - - - - - - - -| PTSIZE 4MB * | Invalid Memory (*) | --/-- KSTKGAP | * | 无效内存 (*) | * +------------------------------+ | * : . : | * : . : | * MMIOLIM ------> +------------------------------+ 0xefc00000 --+ * | Memory-mapped I/O | RW/-- PTSIZE 4MB * ULIM, MMIOBASE --> +------------------------------+ 0xef800000 上部由内核控制 下部由用户控制 * | Cur. Page Table (User R-) | R-/R- PTSIZE * UVPT ----> +------------------------------+ 0xef400000 * | RO PAGES | R-/R- PTSIZE * UPAGES ----> +------------------------------+ 0xef000000 * | RO ENVS | R-/R- PTSIZE * UTOP,UENVS ------> +------------------------------+ 0xeec00000 * UXSTACKTOP -/ | User Exception Stack | RW/RW PGSIZE * | 用户异常栈 | * +------------------------------+ 0xeebff000 * | Empty Memory (*) | --/-- PGSIZE * USTACKTOP ---> +------------------------------+ 0xeebfe000 * | Normal User Stack | RW/RW PGSIZE * +------------------------------+ 0xeebfd000 * | | * | | * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * . . * . . * . . * |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| * | Program Data & Heap | * UTEXT --------> +------------------------------+ 0x00800000 --+ * PFTEMP -------> | Empty Memory (*) | PTSIZE * | | | * UTEMP --------> +------------------------------+ 0x00400000 --+ * | Empty Memory (*) | | * | - - - - - - - - - - - - - - -| | * | User STAB Data (optional) | PTSIZE * USTABDATA ----> +------------------------------+ 0x00200000 | * | Empty Memory (*) | | * 0 ------------> +------------------------------+ --+
简述:虚拟内存空间主要分为两大部分,一部分是
(ULIM,4Gig)
这块内存空间,是只允许内核态读写的,不允许用户态有权限操作。而剩下的[0,ULIM]
有980,992个PGSIEZ
,专门为用户准备的- 第一个
PGSIZE(MMIOLIM,KSTACKTOP]
主要用于CPU的内核栈,每个CPU都拥有一个内核栈,只允许内核读写 - 第二个
PGSIZE(ULIM,MMIOLIM]
保留I/O设备映射使用的内存区域 - 第三个
PGSIZE(UVPT,MMIOBASE]
是当前使用的页表的区域,内核用户都只读不可写,MIT6.828一个页表正好4MB大小。 - 第四个
PGSIZE(UPAGES,UVPT]
是页面结构的只读区域,内核和用户都只读不可写 - 第五个
PGSIZE[UTOP,UPAGES]
是进程结构的只读区域,内核和用户都只读不可写 - 第六、七个
PGSIZE[USTACKTOP,UXSTACKTOP]
是用户异常栈,内核和用户都可以读写,当超过USTACKTOP之后就代表栈溢出了,注意:栈是向下延伸的 (USTABDATA,UXSTACKTOP]
的空间都是用户存放数据的地方,这个地方简称为:堆(可以类比Java的堆)- 而
[0,USTABDATA]
之间的代表NULL值,因为内存低区都默认为不可用的状态。
- 第一个
你的内存管理粒度是多大,如何对进程进行内存分配和管理。
MIT6.828中的内存粒度是页,一个页的大小为4KB
内存分配通过page_alloc
函数,这个函数从page_free_list
取出一个物理页,并返回页面起始地址
内存管理通过一个页结构(PageInfo
)中的pp_ref
计数为0时,代表已经没有引用指向这个物理页了,那么就可以通过page_free
函数进行回收
还有page_insert
函数是负责将一个物理页面插入到页表中,建立于虚拟地址映射,而page_lookup
函数则是从页表中进行寻找对应的页表项,并返回相应的物理页。
通过以上函数来实现了内核对内存进行分配、回收、查询等操作
进程和线程有什么区别?※ 你进程中和线程中切换过程可以说一下吗?他们有什么区别?哪个开销更大,为什么?※
进程和线程区别:进程是操作系统分配的基本单位,而线程是操作系统调度的基本单位。多个线程共享同一个地址空间和其他资源。因此,创建和销毁进程的开销通常比创建和销毁线程的开销大。但是,由于线程共享同一个地址空间和其他资源,线程之间的切换开销比进程之间的切换开销要小。同时,多线程程序也可以更好地利用多核CPU的并行性能,提高程序的执行效率。
很遗憾,MIT6.828中没有提到线程的概念,只有进程。在6.828中实现进程的调度方式主要是轮询的方式,轮循到每一个进程都会分配对应的时间片。
你引入了多cpu,是如何进行调度? ※可以说一下你了解到其他的调度算法吗,它们各自的优缺点是什么?※
是时间片调度的。在MIT6.828中,每个CPU都有一个本地APIC,用于处理处理器间和处理器本地的中断。定时器中断是通过本地APIC的计时器定期产生的。当定时器中断发生时,本地APIC会发送一个中断给当前CPU,当前CPU会在中断处理程序中检查就绪队列,并根据调度算法选择新的进程开始执行。
其他的调度算法:
- 先来先服务调度算法:按照顺序执行,**优点:**简单直接,**缺点:**如果长作业在短作业前面,那么短作业无法及时得到反馈,效率低下,平均等待时间长。
- 短作业调度算法:按照执行的时间的长短执行,**优点:**短作业能被优先处理 **缺点:**很可能会造成饿死现象
- 优先级调度算法:按照作业的优先级大小执行,**优点:**适合对付紧急情况 **缺点:**同样会造成饿死现象
- 时间片调度算法:每一个作业被分配一个时间片,当使用完毕后放入队列中进行等待,**优点:**可以保证每一个作业都能被执行, **缺点:**时间片大 浪费性能,时间片小,作业之间的切换也会浪费性能,对于长作业而言尤其明显
- 多级反馈队列调度算法:分为多个队列,每个队列具备不同的优先级,作业被放入哪一个队列中主要取决于执行时间和等待时间。**优点:**每一个作业都能被很好的处理,性能利用得均衡,不会有饿死现象 **缺点:**实现起来复杂
文件系统的管理是如何实现的
MIT6.828文件系统结构把硬盘分成大小相等的块,每个块都有对应的块号且块大小为4KB。
磁盘文件系统结构:
-
块0不使用,因为上面存放着引导类加载器和分区表。
-
块1为超级块,存放着文件系统属性的元数据,例如:块大小、磁盘大小等
-
块2为块位图,因为每个块都有块号并单调递增的,所以通过位图上的bit位来判断是否为空闲状态。1为空闲,0为被使用
-
此后所有的块是用于存储文件数据或目录的。
文件系统常用的操作有:磁盘块创建、磁盘块删除、磁盘块查找、文件查找
分配是通过alloc_block
函数实现的,通过轮询的查找位图,获取第一个空闲的块,并返回对应的块号
删除是通过free_block
函数实现,通过位图的方式,将对应bit位置为 1
查找是通过file_get_block
函数实现,通过调用了file_block_walk
函数,获取对应块号的地址后装入传参的**blk。
文件查找是通过dir_lookup
函数实现,通过调用了file_get_block
函数获取到传参dir中的所有块,然后去比对块中file.f_name是否与传参的name一样
你使用的是时间片调度,具体的流程是什么样的?你知道时间片是如何校准时间的吗。
流程:时间片是通过时钟中断来实现的。在MIT6.828中当时钟中断发生时,会调用对应的中断处理函数。具体实现在trap.c
文件下的trap_dispatch()
,这个函数就是负责当时间片用完后,调度下一个可运行的进程。
校准时间主要是通过kern/lapic.c中的
lapicw(TICR, 10000000);, 表示设置 lapic[TICR]
为10000000。TICR是 APIC 定时器计数器寄存器,当CPU将lapic[TICR]减至为0后,就会触发定时器中断。
解释一下fork,有实现COW(写时复制)吗?这个你是怎么实现的?※怎么判断那一内存区域是需要COW的?COW的好处是什么※
fork是创建出子进程,该子进程与父进程共享同一个进程空间(虚拟内存空间),父进程中返回子进程的PID,而子进程会返回0。
有实现COW,子进程被创建之后,首先的操作就是把父进程的页表完全映射过来并不会完全复制。如果某个地址中权限位表示为可写(PTE_W)
或可写时复制(PTE_COW)
,那么就可以将此虚拟地址标记上写时复制并映射到子进程页表上。
COW 技术的好处在于节省了内存空间,因为父进程和子进程之间共享页面,而不是将整个空间复制一份,这样可以节省内存空间,提高系统的效率和性能
MIT6.828的fork
函数流程:
首先会将页错误的处理程序设置为:异常处理函数pgfault
,如果当发生写的情况,那么就会中断进行这个函数,pgfault
会分配出一个页面专门对应父/子进程进行写入
接着,会调用sys_exofork
来创建一个新的进程。
接下来sys_exofork
中就会返回不同的值,0代表目前的进程是子进程,而>0的代表目前的进程是父进程
如果当前进程是子进程,那么则会将当前的进程thisenv
首先标识为子进程
如果是父进程,那么就会将自身页表中所有的地址,通过duppage
函数进行映射。具体映射操作:
会检查当前页表项是否存在,即PTE_P
存在,不存在直接panic
否则继续:
- 会判断此页表项是否为共享,即
PTE_SHARE
存在,则直接映射 - 会判断此页表项是否为可写或者写时复制,即
PTE_W 或PTE_COW
存在,则将PTE_COW
映射到子进程的页表项上同时重新映射到自己的页表项上(因为父子进程必须都要标识PTE_COW
位,不然可能无法触发写时复制!这里我在实验中就踩坑了)
页表映射完后,则还会为子进程在UXSTACKTOP - PGSIZE
创建一个页面作为异常栈。
最后同样将pgfault
设置到子进程中的页错误处理函数,并将子进程设置为可运行的状态。
进程间如何通信?讲一讲其中最简单的管道通信的原理
操作系统中进程间的通信有以下几种方式:
-
匿名/有名管道,匿名管道会将数据存在内存当中,而有名管道会将数据存在硬盘当中。匿名管道只用于父子进程,而有名管道用于任意进程之间
- 原理:父进程创建匿名管道,通过
pipe()
函数返回两个文件描述符,其中fd[0]
用于读端,fd[1]
用于写端。父进程调用fork()
函数之后会创建出子进程,子进程会继承父进程的所有内存空间,同样也拥有了管道的读写端,但父进程会关闭读端,子进程会关闭写端。这样父进程就可以通过写端写入数据到管道中,而子进程就可以通过读端获取父进程的数据了。当数据传输完毕后,父进程会关闭管道的写端,那么此时子进程通过读端读取到返回值0,代表已经没有数据了,同样会关闭管道的读端,于是子进程结束。如果往后父进程再想通过写端进行写入时,那么就会收到SIGPIPE的信号,表示管道已经关闭了。
- 原理:父进程创建匿名管道,通过
-
消息队列,由于管道是半双工方式管道的消息传输的效率是很低的,必须要建立两个管道才能实现全双工,所以引出了消息队列,消息队列可以实现全双工模式。但是消息是需要双方规定好消息格式的,并且消息的发送需要从用户态拷贝到内核态的缓冲区,再从内核态的缓冲区拷贝到用户态,会产生额外的开销。
-
共享内存,多个进程共享同一片物理内存区域,若要发生多进程同时写入场景,可以采用信号量或者写时复制解决。
- MIT6.828当中是会通过
sys_page_map()
来映射物理页面的,这样多个进程都能跟共享同一份物理页面。
- MIT6.828当中是会通过
-
信号量,用来同步多个进程访问同一个内存区域。
- 原理:本质是一个计数器,当一个进程要访问共享资源时,>0时代表此内存区域可访问,当<=0时代表无法访问。
-
信号,是一种软件中断,主要用在异常情况下,它是可以异步的,不需要接收方和放松方有相互作用。
-
socket,不同主机间的进程通讯。
接下来是高端内存区域是什么地方?用来干什么的?访问高端内存区域会发生什么?(题主答:触发异常进内核态然后找对应函数去处理。为什么能够触发异常,想了想觉得应该是由页表项的后12位中有具体的权限门,锁死了用户态的访问。)
高端内存区域是[ULIM, 2^32)
,此256MB的内存区域只允许内核读写,用于内核存放数据,相当于内核专用的堆。如果当用户进程去访问这些高端区域,那么会发生异常,紧接着会进入对应的异常处理函数当中。
你项目中哪里有使用到锁?用到了哪种锁?※它们在这个项目中作用是什么?你可以讲讲项目种用到的锁的实现原理吗?※**
当进程进出内核态时需要使用内核锁。
项目中用到了内核锁kernel_lock
,底层是通过自旋锁spinlock
实现的,而自旋锁采用了CAS的思想,通过xchg
指令实现,可以保证原子性。
内核锁,lock_kernel()
函数是 MIT 6.828 操作系统中用于获取内核全局锁的函数。它的作用是防止多个 CPU 同时修改内核数据结构和内存信息,保证内核代码的原子性和正确性。
xchg()的实现原理是通过总线锁定(Bus Lock)来保证原子性,总线锁定会在多处理器环境下把总线锁住,防止其它处理器访问共享内存,从而保证了对共享内存的原子操作。
XV6思维导图
启动流程:
中断流程:
文件系统流程:
如有错误,欢迎指正!