其他分享
首页 > 其他分享> > 写个操作系统吧!

写个操作系统吧!

作者:互联网

写个操作系统吧!

参考书籍:

  • 《操作系统真相还原》
  • 《x86汇编语言:从实模式到保护模式》

中断

中断的分类:

image-20220128140016976

操作系统对中断的处理方式:

把中断处理程序划分为上半部和下半部。

上半部:处理一些紧急且重要的事情。(会关闭中断,处理完成后再开启中断)

下半部:处理一些重要但不紧急的事情,由操作系统在适合的时候执行。(允许中断发生)

image-20220128141002087


IDT中断描述符表

保护模式下用于存储中断处理程序入口的表。

实模式下存储中断处理程序入口的表称为中断向量表(IVT)

门:

image-20220128141608699

image-20220128142406762


中断向量表和中断描述符表的区别:

中断向量表:实模式

存在于低端内存中,即0x0 ~ 0x3ff,大小为1024个字节,每个中断向量大小为4个字节,因此中断向量表可以容纳256个中断向量

中断描述符表:保护模式

中断描述符表寄存器(IDTR),该寄存器分为两部分,0~15位是表界限,16~47位为IDT的基地址,最多容纳8192个描述符。


中断处理过程:

  1. CPU根据中断向量号定位到中断描述符。
  2. CPU进行特权级检查
  3. 执行中断处理程序

两个中断有关的命令


中断处理过程中栈的变化:

image-20220128143748192


中断错误码:

image-20220128144122764

参数解释:


中断控制器8259A

构造:

image-20220128144554701

image-20220128145101989

外部设备发起中断到CPU处理中断的流程:

  1. 外设发起中断,该中断信号被送入到8259A的某个IRQ接口
  2. 8259A收到该信号后,根据IMR寄存器判断该信号是否被屏蔽。(1屏蔽,0放行)
  3. 8259A将IRQ接口对应的该IRR寄存器的bit置为1。
  4. 优先仲裁器PRIRR寄存器中挑选一个优先级最大的中断(即IRQ的位置越小,优先级越大),通过INT接口发送INTR信号CPU
  5. CPU收到该信号后,通过INTA接口回复一个INTA信号,表示CPU准备完成可以接收中断向量号。
  6. 8259A收到INTA信号后,将对应的IRR寄存器上的位设置为0。
  7. CPU再次发送INTA信号8259A8259A发送\(起始向量号 + IRQ接口号 = 该设备的中断向量号\)给CPU
  8. CPU根据该中断向量号找到对应中断程序入口,执行对应的中断处理程序。
  9. 当中断处理程序结束后,如果8259AEOI通知(End Of Interrupt)设置为非自动模式,则中断处理程序需要在结束处向8259A发送EOI通知8259A收到EOI通知ISR寄存器中对应的bit设置为0。

如果EOI通知设置为自动模式,则8259A会在收到第二次INTA信号后就自动将对应的ISR寄存器的bit设置为0。


可编程的计数器/定时器8253

8253提供了三个计数器,分别对应端口0x40 ~ 0x42(16位)。

20计数器在计时到期后就会发出时钟中断信号,中断代理8259A就可以收到。

image-20220128210544784


进程和线程的运行机制

线程创建

由内核进行管理,不具备属于自己的虚拟地址空间,线程创建时,通过get_kernel_pages函数向内核内存池申请一页的内存空间作为PCB。

注意:线程栈处于该内存页的顶端,线程相关信息处于内存页的低端。

线程创建时通过thread_start函数创建,该函数主要的职责是从内核内存池中申请1页的内存空间,作为PCB,即struct task_struct*结构体。再初始化线程相关信息,如:线程名称、优先级、线程状态等,再初始化线程的内核栈,即将分配到的内存页的顶端作为栈的起始位置。thread->self_kstack = (uint32_t*) ((uint32_t) thread + PG_SIZE)。然后,再把线程的PCB块纳入thread_ready_listthread_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函数的步骤

  1. 先通过running_thread获取当前线程。
  2. 检查线程是否栈溢出,即查看struct task_struct*结构体的stack_magic属性是否被篡改。
  3. 将线程运行的总时间片数加一。
  4. timer.c中的全局变量ticks加一,表示从操作系统内核加载到现在所运行过的时间片数。
  5. 判断线程的可用时间片ticks是否为0,如果为0,则表示时间片用完,进行调度,即调用thread.c文件中的schedule函数。否则,将可用时间片减一,结束中断。(结束中断后,回返回当前线程之前正在运行的函数,并恢复其上下文(寄存器))

说了这么多,调度的所有关键点都在schedule函数上。

schedule函数的步骤:

  1. 通过running_thread函数获取当前运行的线程cur
  2. 判断当前线程是否处于TASK_RUNNING状态,如果是,则表明该线程是因为时间片用完,则进行线程调度的,那么将cur放入就绪队列thread_ready_list,重置该线程的可用时间片cur->ticks = cur->priority,设置线程的状态为TASK_READY
  3. 从就绪队列thread_ready_list的队头pop出一个线程,更新线程状态为TASK_RUNNING
  4. 调用process_activate函数,判断当前PCB是用户进程还是内核线程?如果是用户进程,则调用page_dir_activate函数修改cr3寄存器,即修改页目录表的起始地址为用户虚拟地址空间。否则修改页目录表的起始地址为0x100000,即为内核虚拟地址空间。(因为在调度时,前一个PCB有可能是用户进程,所有也需要更新cr3寄存器)。如果是用户进程,则需要修改tssesp0值,即修改0特权级栈为内核栈,即(uint32_t *)((uint32_t)pthread + PG_SIZE)
  5. 最后调用switch_to函数,该函数位于switch.s文件中,保存当前线程上下文,即将寄存器的值压入线程栈中,同时恢复即将调度的线程的上下文,最后通过ret指令,获取栈中的值,跳转到kernel_thread函数中去执行。

补充:

当跳转到kernel_thread函数后,会先开启中断,在调用线程栈中的thread_stack::function函数。从而实现从一个指令流跳转到另一个指令流。

进程创建

进程相比于线程,多出了一个3特权级栈虚拟地址空间

步骤:

  1. 通过process_execute函数传入调用的函数指针(void*)和进程名称,在从内核内存池中申请一个页面作为内核栈,进行线程相关的初始化,即:init_thread -> (create_user_vaddr_bitmap) -> init_stack
  2. create_user_vaddr_bitmap是初始化进程的虚拟地址空间的bitmap,将虚拟地址空间的起始地址设置为0x8048000,同时在内核内存池中分配出几个页来作为bitmap
  3. 创建用户进程的页目录表,先向内核内存池申请一个页面来作为页目录表,同时将内核的页目录表的第768项到1023项都复制到用户进程的页目录表中。
  4. 关闭中断。
  5. 将该PCB加入thread_all_listthread_ready_list队列
  6. 开启中断。

进程调度

进程的调度相比于线程的不同之处,即进程需要在调度后,需要初始化用户进程的上下文,即恢复寄存器原先的值,并且将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函数的主要流程:

  1. 获取当前PCB,并设置PCB的中断栈intr_stack结构。主要就是把栈中的eip值改为用户给的启动函数,同时修改栈中csss的值为用户态下的段选择子,修改esp指针为3特权级栈(到这里才实际分配用户栈空间)。
  2. 修改esp寄存器的值为PCB中断栈的起始地址,通过jmp指令跳转到kernel.s文件的intr_exit函数中。
  3. intr_exit这个函数主要就是恢复esp寄存器指向的栈中值到gs、fs、es、ds寄存器中,同时通过iretd指令退出中断模式(欺骗CPU,来跳转到3特权级栈
  4. 由于eip寄存器指向用户给定的启动函数,那么进程就从用户给的函数开始执行。

系统调用

中断发生后,处理器从低特权进入高特权,它会把ss3、esp3、eflag、cs、eip寄存器依次压入栈中,共20字节。

系统调用流程

在用户进程中导入syscall.h头文件。

里面有对应的系统调用函数,具体实现在syscall.c文件中。

write系统调用为例:

  1. 用户调用write函数,传入一个对应的字符串参数,这是函数内部会调用对应的宏_syscall1
  2. _syscall1宏会传入系统调用子功能号和参数,子功能号对应的就是SYSCALL_NR枚举(枚举会被转化为整数),_syscall1宏的功能就是,调用asm volatile宏定义(C语言内联汇编),把参数和子功能号压入栈中,并发起中断,即int $0x80指令,再将中断处理后的结果从eax寄存器取出来放入到ret_val变量中,并返回。
  3. 0x80中断的具体实现在kernel.s文件,该中断处理函数在interrupt.c文件的完成注册,即在中断描述符表中加入该函数的中断描述符。具体路径:idt_init() -> idt_desc_init() -> make_idt_desc(&idt[0x80], IDT_DESC_DPL3, syscall_hanlder)
  4. 现在看下syscall_handler函数,该函数的主要作用就是保存当前线程上下文,然后从栈中取出esp3指针,即用户栈指针,因为系统调用是在用户态下调用的,当CPU特权级发生变化时,CPU会负责将esp3等寄存器的值压入栈中,然后从栈中获取子功能号和系统调用参数,回调syscall.c文件中定义的全局数组syscall_table,该数组每一个元素都是对应子功能号(子功能号对应数组下标)的系统调用处理函数,当回调返回后,再将eax寄存器的值压入栈中,调用intr_exit函数退出中断。

内存管理

分配内存的步骤:

  1. 在虚拟内存池中分配虚拟地址,相关函数是vaddr_get,此函数会操作内核的虚拟内存位图kernel_vaddr.vaddr_bitmap或用户虚拟内存位图pcb->user_program_vaddr.vaddr_bitmap
  2. 在物理内存池中分配物理地址,相关函数是palloc,此函数会操作内核的物理内存池位图kernel_pool->pool_bitmap或用户物理内存池位图user_pool->pool_bitmap
  3. 在页表中完成虚拟地址到物理地址的映射,相关函数是page_table_add

以上三个步骤封装在malloc_page函数中。

释放内存的步骤:

  1. 在物理内存池中释放物理页地址,相关函数是pfree
  2. 在页表中去掉虚拟地址的映射,原理是将页表项的P位设置为0(即表示对应的数据不在内存中),相关函数是page_table_pte_remove
  3. 在虚拟内存池中释放虚拟地址,相关函数是vaddr_remove,操作的位图同vaddr_get函数。

以上三个步骤封装在mfree_page函数中。

标签:写个,操作系统,thread,中断,调用,线程,寄存器,函数
来源: https://www.cnblogs.com/Lht1/p/15893699.html