写个操作系统吧!
写个操作系统吧!
参考书籍:
- 《操作系统真相还原》
- 《x86汇编语言:从实模式到保护模式》
中断
中断的分类:
-
外部中断:外部设备提供的中断信号
- INTR(INTeRrupt):可被屏蔽的中断,例如:网卡接收到数据等。
- NMI(Non Maskable Interrupted):无法被屏蔽的中断,例如:电源掉电等。
-
内部中断:
-
软中断:由软件发起,即执行了对应的中断指令。
int 8位立即数:根据中断向量表或中断描述符表调用对应的中断处理程序。int3:断点调式命令,触发中断向量号3。into:中断溢出指令,触发中断向量号4。bound:检查数组索引越界指令,触发中断向量号5。ud2:未定义指令,触发中断向量号6。
-
异常:指令执行期间CPU内部发生错误引起的,无法被屏蔽。
按照异常的严重程度,可分为以下三种:
- Fault,故障:可被修复的错误类型。
- Trap,陷阱:通常用在调试中。
- Abort,终止:一旦出现则无法修复,操作系统会将该程序抹掉。
-

操作系统对中断的处理方式:
把中断处理程序划分为上半部和下半部。
上半部:处理一些紧急且重要的事情。(会关闭中断,处理完成后再开启中断)
下半部:处理一些重要但不紧急的事情,由操作系统在适合的时候执行。(允许中断发生)

IDT中断描述符表
保护模式下用于存储中断处理程序入口的表。
实模式下存储中断处理程序入口的表称为中断向量表(IVT)。
门:
-
任务门
与任务状态段(TSS)配合使用,是Intel在硬件一级提供的任务切换机制。
任务门可以存在全局描述符表GDT、局部描述符表LDT、中断描述符表IDT中。
-
中断门
包含中断处理程序所在段的段选择子和段内偏移地址。通过此方式进入中断后,标志寄存器eflags中的IF位自动设置为0,也就是自动关闭中断,避免中断嵌套。
中断门只允许存在于中断描述符表IDT中。
-
陷阱门
与中断门一样,但是通过此方式进入中断,不会自动关闭中断。
-
调用门
提供给用户进入特权0级的方式,其DPL为3。
调用门记录了例程的地址,不能通过
int指令调用,只能通过call和jmp指令调用。可以安装在全局描述符表GDT和局部描述符表LDT中。


中断向量表和中断描述符表的区别:
中断向量表:实模式
存在于低端内存中,即
0x0 ~ 0x3ff,大小为1024个字节,每个中断向量大小为4个字节,因此中断向量表可以容纳256个中断向量
中断描述符表:保护模式
中断描述符表寄存器(IDTR),该寄存器分为两部分,
0~15位是表界限,16~47位为IDT的基地址,最多容纳8192个描述符。
中断处理过程:
- CPU根据中断向量号定位到中断描述符。
- CPU进行特权级检查
- 执行中断处理程序
两个中断有关的命令
cli:关中断,把eflags寄存器的IF位设置为0。sti:开中断,把eflags寄存器的IF位设置为1。
中断处理过程中栈的变化:

中断错误码:

参数解释:
- EVT:用来指明中断源是否来自外部设备,1是,0否。
- IDT:表示选择子是否指向中断描述符表,1是,0否。
- TI:表示是选择子使用GDT还是LDT,1LDT,0GDT。
中断控制器8259A
构造:


外部设备发起中断到CPU处理中断的流程:
- 外设发起中断,该中断信号被送入到
8259A的某个IRQ接口。 8259A收到该信号后,根据IMR寄存器判断该信号是否被屏蔽。(1屏蔽,0放行)8259A将IRQ接口对应的该IRR寄存器的bit置为1。优先仲裁器PR从IRR寄存器中挑选一个优先级最大的中断(即IRQ的位置越小,优先级越大),通过INT接口发送INTR信号给CPU。CPU收到该信号后,通过INTA接口回复一个INTA信号,表示CPU准备完成可以接收中断向量号。8259A收到INTA信号后,将对应的IRR寄存器上的位设置为0。CPU再次发送INTA信号给8259A,8259A发送\(起始向量号 + IRQ接口号 = 该设备的中断向量号\)给CPU。CPU根据该中断向量号找到对应中断程序入口,执行对应的中断处理程序。- 当中断处理程序结束后,如果
8259A的EOI通知(End Of Interrupt)设置为非自动模式,则中断处理程序需要在结束处向8259A发送EOI通知,8259A收到EOI通知将ISR寄存器中对应的bit设置为0。
如果
EOI通知设置为自动模式,则8259A会在收到第二次INTA信号后就自动将对应的ISR寄存器的bit设置为0。
可编程的计数器/定时器8253
8253提供了三个计数器,分别对应端口0x40 ~ 0x42(16位)。
20计数器在计时到期后就会发出时钟中断信号,中断代理8259A就可以收到。

进程和线程的运行机制
线程创建
由内核进行管理,不具备属于自己的虚拟地址空间,线程创建时,通过
get_kernel_pages函数向内核内存池申请一页的内存空间作为PCB。注意:线程栈处于该内存页的顶端,线程相关信息处于内存页的低端。
线程创建时通过
thread_start函数创建,该函数主要的职责是从内核内存池中申请1页的内存空间,作为PCB,即struct task_struct*结构体。再初始化线程相关信息,如:线程名称、优先级、线程状态等,再初始化线程的内核栈,即将分配到的内存页的顶端作为栈的起始位置。thread->self_kstack = (uint32_t*) ((uint32_t) thread + PG_SIZE)。然后,再把线程的PCB块纳入thread_ready_list和thread_all_list队列进行管理。
线程调度
线程的调度完全依赖于时钟中断函数。具体代码在
timer.c文件中。在
init.c中会暴露出一个init_all函数,作为main.c文件中的第一个函数调用。该函数负责初始化所有内核模块,也包括定时器的初始化,即timer_init函数。这个函数是在timer.c文件中。
timer_init函数主要设置了PIT8253定时器的定时周期,即以一定的频率向CPU发出中断信号,再将intr_timer_handler中断处理函数注册到中断描述符表中,对应的中断向量号为0x20(注册函数:register_handler),即interrupt.c文件中的idt_table数组。当CPU收到一个中断信号时,就会调用
kernel.s文件中的intr_entry_table数组,而该数组中的中断处理函数,都是使用同一个模板,具体逻辑就是:保存当前中断上下文(相关寄存器),再调用interrupt.c文件中注册好的idt_table数组。再说回
intr_timer_handler函数,该函数的主要逻辑如下:
intr_timer_handler函数的步骤
- 先通过
running_thread获取当前线程。- 检查线程是否栈溢出,即查看
struct task_struct*结构体的stack_magic属性是否被篡改。- 将线程运行的总时间片数加一。
- 将
timer.c中的全局变量ticks加一,表示从操作系统内核加载到现在所运行过的时间片数。- 判断线程的可用时间片
ticks是否为0,如果为0,则表示时间片用完,进行调度,即调用thread.c文件中的schedule函数。否则,将可用时间片减一,结束中断。(结束中断后,回返回当前线程之前正在运行的函数,并恢复其上下文(寄存器))说了这么多,调度的所有关键点都在
schedule函数上。
schedule函数的步骤:
- 通过
running_thread函数获取当前运行的线程cur。- 判断当前线程是否处于
TASK_RUNNING状态,如果是,则表明该线程是因为时间片用完,则进行线程调度的,那么将cur放入就绪队列thread_ready_list,重置该线程的可用时间片cur->ticks = cur->priority,设置线程的状态为TASK_READY。- 从就绪队列
thread_ready_list的队头pop出一个线程,更新线程状态为TASK_RUNNING。- 调用
process_activate函数,判断当前PCB是用户进程还是内核线程?如果是用户进程,则调用page_dir_activate函数修改cr3寄存器,即修改页目录表的起始地址为用户虚拟地址空间。否则修改页目录表的起始地址为0x100000,即为内核虚拟地址空间。(因为在调度时,前一个PCB有可能是用户进程,所有也需要更新cr3寄存器)。如果是用户进程,则需要修改tss的esp0值,即修改0特权级栈为内核栈,即(uint32_t *)((uint32_t)pthread + PG_SIZE)。- 最后调用
switch_to函数,该函数位于switch.s文件中,保存当前线程上下文,即将寄存器的值压入线程栈中,同时恢复即将调度的线程的上下文,最后通过ret指令,获取栈中的值,跳转到kernel_thread函数中去执行。补充:
当跳转到
kernel_thread函数后,会先开启中断,在调用线程栈中的thread_stack::function函数。从而实现从一个指令流跳转到另一个指令流。
进程创建
进程相比于线程,多出了一个
3特权级栈和虚拟地址空间。步骤:
- 通过
process_execute函数传入调用的函数指针(void*)和进程名称,在从内核内存池中申请一个页面作为内核栈,进行线程相关的初始化,即:init_thread -> (create_user_vaddr_bitmap) -> init_stack。create_user_vaddr_bitmap是初始化进程的虚拟地址空间的bitmap,将虚拟地址空间的起始地址设置为0x8048000,同时在内核内存池中分配出几个页来作为bitmap。- 创建用户进程的页目录表,先向内核内存池申请一个页面来作为页目录表,同时将内核的页目录表的第
768项到1023项都复制到用户进程的页目录表中。- 关闭中断。
- 将该PCB加入
thread_all_list和thread_ready_list队列- 开启中断。
进程调度
进程的调度相比于线程的不同之处,即进程需要在调度后,需要初始化用户进程的上下文,即恢复寄存器原先的值,并且将
esp修改为3特权级栈的地址值。线程调度就不需要,通过
switch_to函数的ret指令跳转到kernel_thread函数中执行给定的thread_func函数即可。进程调度,则同样通过
switch_to函数的ret指令跳转到kernel_thread函数中执行给定的thread_func函数,但是这个thread_func函数,在process_execute函数里调用init_stack时,已经指定为start_process函数。
start_process函数的主要流程:
- 获取当前PCB,并设置PCB的中断栈
intr_stack结构。主要就是把栈中的eip值改为用户给的启动函数,同时修改栈中cs和ss的值为用户态下的段选择子,修改esp指针为3特权级栈(到这里才实际分配用户栈空间)。- 修改
esp寄存器的值为PCB中断栈的起始地址,通过jmp指令跳转到kernel.s文件的intr_exit函数中。intr_exit这个函数主要就是恢复esp寄存器指向的栈中值到gs、fs、es、ds寄存器中,同时通过iretd指令退出中断模式(欺骗CPU,来跳转到3特权级栈)- 由于
eip寄存器指向用户给定的启动函数,那么进程就从用户给的函数开始执行。
系统调用
中断发生后,处理器从低特权进入高特权,它会把
ss3、esp3、eflag、cs、eip寄存器依次压入栈中,共20字节。
系统调用流程
在用户进程中导入
syscall.h头文件。里面有对应的系统调用函数,具体实现在
syscall.c文件中。以
write系统调用为例:
- 用户调用
write函数,传入一个对应的字符串参数,这是函数内部会调用对应的宏_syscall1。_syscall1宏会传入系统调用子功能号和参数,子功能号对应的就是SYSCALL_NR枚举(枚举会被转化为整数),_syscall1宏的功能就是,调用asm volatile宏定义(C语言内联汇编),把参数和子功能号压入栈中,并发起中断,即int $0x80指令,再将中断处理后的结果从eax寄存器取出来放入到ret_val变量中,并返回。0x80中断的具体实现在kernel.s文件,该中断处理函数在interrupt.c文件的完成注册,即在中断描述符表中加入该函数的中断描述符。具体路径:idt_init() -> idt_desc_init() -> make_idt_desc(&idt[0x80], IDT_DESC_DPL3, syscall_hanlder)。- 现在看下
syscall_handler函数,该函数的主要作用就是保存当前线程上下文,然后从栈中取出esp3指针,即用户栈指针,因为系统调用是在用户态下调用的,当CPU特权级发生变化时,CPU会负责将esp3等寄存器的值压入栈中,然后从栈中获取子功能号和系统调用参数,回调syscall.c文件中定义的全局数组syscall_table,该数组每一个元素都是对应子功能号(子功能号对应数组下标)的系统调用处理函数,当回调返回后,再将eax寄存器的值压入栈中,调用intr_exit函数退出中断。
内存管理
分配内存的步骤:
- 在虚拟内存池中分配虚拟地址,相关函数是
vaddr_get,此函数会操作内核的虚拟内存位图kernel_vaddr.vaddr_bitmap或用户虚拟内存位图pcb->user_program_vaddr.vaddr_bitmap。 - 在物理内存池中分配物理地址,相关函数是
palloc,此函数会操作内核的物理内存池位图kernel_pool->pool_bitmap或用户物理内存池位图user_pool->pool_bitmap。 - 在页表中完成虚拟地址到物理地址的映射,相关函数是
page_table_add。
以上三个步骤封装在
malloc_page函数中。
释放内存的步骤:
- 在物理内存池中释放物理页地址,相关函数是
pfree。 - 在页表中去掉虚拟地址的映射,原理是将页表项的P位设置为0(即表示对应的数据不在内存中),相关函数是
page_table_pte_remove。 - 在虚拟内存池中释放虚拟地址,相关函数是
vaddr_remove,操作的位图同vaddr_get函数。
以上三个步骤封装在
mfree_page函数中。

浙公网安备 33010602011771号