操作系统进程相关-以Linux0.11和MINIX为代表

二、linux操作系统

2.1、操作系统的启动

  • (1) 开机时,系统复位,CS:IP被复位为0xFFFF0,于是跳转到0xFFFF0处执行命令。此处为jmp指令。跳转到段地址0xF000:0xE05B处
    (0xF000段地址均在ROM中,也即bios存储区域)。执行开机自检等工作后,加载主引导扇区到0x7C00处。

  • (2)主引导扇区再将自己复制到0x90000处,并跳转到此处执行代码。(为什么不直接加载到此处,还需要多一步复制的过程?为了兼容之前的机器)

  • (3)利用 BIOS中断(0x13) 将setup(四个扇区)加载到0x90200处,利用BIOS中断(0x10)显示开机界面,将操作系统system加载到0x10000处。
    (后续还要移动,之所以现在不移动是为了不覆盖BIOS中断表,setup还需要用到BIOS中断)

    • 读操作系统时尽量一次将整个磁道的所有扇区读入,保证每次不超过64kb(不跨段)。
    • 检查使用哪个根设备,跳转到setup中。
  • (4)setup利用BIOS中断读取系统数据到0x90000处,覆盖掉Bootsect。(光标位置、扩展内存大小等)

    • 将system向下移动到0x00000处。

    • 加载中断描述符表寄存器(IDTR)和全部描述符表寄存器(GDTR)、开启A20地址线,设置中断控制芯片等。全局描述符表暂放在0x9xxxx处
      说明:IDTR中存放的是IDT在内存中的基地址和其限长。
      在这里插入图片描述

    • 修改控制寄存器CR0,进入32位保护模式
      进入保护模式后,段寄存器保存的为描述符在描述符表中的偏移位置,也即jmp的段值是指段选择符
      段选择符
      低2位标识特权级,第3位0/1为全局/局部描述符,其余为索引。

    • 转到system中的head

  • (5)system中的head

    • 重新设置中断描述符表(IDT)、全局段描述符表(GDT)以及描述符表寄存器,放在内核数据段,位置在head最后处。
      中断描述符表中的项被称为中断门描述符,初始都设置为哑中断函数
    • 判断A20地址线是否开启
      ------------------------------------------------上面为分段机制,下面为分页机制-------------------------
    • 页目录表放在绝对物理地址0处,CR3保存其地址
      保护模式下使用GNU汇编,从左向右赋值
    • 设置页表项
      每个页表包含1024个表项,每个表项需要4个字节,每个页表本身需要4KB空间,第一个页表放在0x1000处。
      每个表项代表内存4KB,若想寻址16MB,则需要4个页表。(根据CSAPP,现代页表是分级机制,是不断往下细分的方式)
    • 转到main函数执行

BIOS中断表也没必要放在0x00000处,这样可以避免操作系统的移动(从0x10000移动到0x00000)。

2.2、c语言内嵌汇编

输入输出共用同一个寄存器时,使用“0”约束修饰输入变量

此部分参考:link

在此需要强调的是,在指明input operands的情况下,即使指令不会产生output operands,其:也需要给出。例如asm (“sidt %0\n” : :“m”(loc));

2.3、系统调用的实现

为了防止应用程序改变操作系统,硬件提供特权级保护。通过比较当前特权级(CPL)和目标特权级(DPL),只有满足 C P L ≤ D P L CPL\le DPL CPLDPL (内核为0,用户为3)的代码才能执行,而操作系统为了给用户程序提供服务就需要提供一些接口:系统调用。
系统调用是通过中断实现的,具体而言是:
(1) 通过库函数调用int 0x80(通过宏来展开系统调用函数write(),宏中内嵌汇编实现此功能),需要传递的参数为系统调用功能号
(2) 在中断描述符表中找到对应中断处理函数system_call (此部分为特意设置该表项的DPL=3,使得用户程序可以调用)
(3) 在system_call中根据系统调用号在sys_call_table[]中找到并执行相应的处理函数 (通过函数指针数组实现)

在系统调用期间,ds、es指向内核数据空间,fs指向用户数据空间,因此可以完成内核和用户之间的数据交换。

2.3.1、实验部分

添加一个新系统调用的流程就为:
(1) 为新系统调用设置新的系统调用号,并注意用相应的宏展开该系统调用
(2) 在系统调用函数指针数组中,添加新的处理函数
(3) 在内核中实现新的处理函数
在linux0.11实验三系统调用中,在内核下使用printk是个大坑,并不能使用printf的%s等,否则将提示段错误。最好仅仅用来输出一部分写好的字符串。

2.3.2、fork

linux 0.11中定义每个进程最大可用虚拟内存空间为64MB,最大任务数为64,共计4GB。
(1) 在主内存区中申请一页内存p,复制任务数据结构(PCB,task_struct)到p开始处
(2) 设置任务状态段(TSS)各寄存器的值,设置内核堆栈esp0开始位置位于p末尾处,ss0被设置为内核数据段选择符(0x10)。TSS也在PCB中,其中保存的为进程运行的所有信息(CPU各寄存器的值,IP的值等),当前正在执行进程的TSS由任务寄存器(TR)指示。

为什么tss.ss0被设置为0x10?
linux0.11最多能管理16MB内存,而内核数据段长度为16MB,因此内核代码可以寻址整个物理内存的任何位置。

2.3.3、进程切换

linux0.11使用TSS和ljmp完成进程的切换,但这个指令耗时较长。
利用内核堆栈切换的方式更高效。

1、子进程堆栈的构造;
在fork过程中,主要的函数是copy_process,其利用父进程信息创建子进程。堆栈中的内容需要参考其执行过程。

  • (1)用户执行中断函数过程(进入内核):
    • 利用内核栈切换其实是借用了iret指令内核栈与用户栈联系起来。因此在构造新进程内核栈时需要效仿,在内核栈中压入相应内容ss、sp、eflags,cs,ip,有了这些我们便可以回到用户代码处以及找到用户堆栈。
  • (2) 执行系统调用过程(在内核中执行系统调用):
    • a.肯定是系统调用sys_call引起的进程的切换,因此后续要考虑ret_from_sys_call中弹出的寄存器,以便恢复到用户调用系统调用前的状态接着执行。
    • b.调用sys_fork创建新进程,还需考虑 sys_fork函数中压入的堆栈。(sys_fork会首先调用find_empty_process获得子进程号,保存在eax中,因此父进程返回子进程号);
    • c. 最后执行copy_process,其中a.b.可以合为一步,因为本质上最终压栈的参数全部传给了copy_process(子进程的eax设置为0,其代表最终的返回值,因此子进程返回0),只需利用这些信息构造即可。
  • (3)执行schedule调度过程(执行完sys_fork后,会判断任务状态、时间片等信息,可能会执行调度):
    • 主要涉及函数为switch_to。因此最后要考虑的是,切换是在switch_to函数中进行的,内核切换后仍返回该函数,需考虑switch_to函数中压栈的情况。执行switch_to时
      • 若调度到父进程,判断条件满足直接返回结束,父进程会一级级的返回;
      • 若调度到子进程,执行下述2过程,子进程执行完switch_to后,返回一个特意构造的函数,在其中弹栈,执行iret直接返回到用户程序

2、switch_to具体切换过程;
在C语言中调用switch_to时,已经把其参数压入栈中,压入顺序为从右向左,最后压入返回地址。
在switch_to函数中,由于是C语言调用该函数,因此首先保存栈帧,之后需要完成的内容是:切换进程PCB、保存新进程的内核栈指针到TSS中(此处的tss为全局共享变量,改变此变量后仍然和Intel中断处理兼容,current和tss的初值在sched.c),切换内核栈,切换LDT。

3.需要注意的问题:

  • (1).在sched.c中schedule函数调用switch_to时,需要参数pnext,我们想让pnext表示下一个进程的PCB结构,需要注意的是,此变量一定要设置初值(进程0的PCB结构),否则在系统刚开始运行时会出现问题,next默认为0,即没有进程需要调度时,运行进程0,如果不设置初值,将会调度一个随机值,就会出现问题。
  • (2). 在switch_to函数中,切换内核栈后,仍有一步操作为movl 12(%ebp),%ecx,此处的问题是既然内核栈已经切换,而12(%ebp)对应内容位于原进程堆栈上,在新进程内核栈中并没有此参数。
    解答:此步之所以能够执行成功,是因为内核栈切换只改变%esp,此时%ebp仍是指向原进程堆栈,因此就可以用原先参数。
2.3.4、execve

基于内核栈的切换的原理我们已经获知,此函数只不过是改变代码及用户栈。因此只需改变ss、sp、eflags,cs,ip中的ip和sp即可。

2.4、内存管理

物理内存的使用共计分为内核模块区,高速缓冲区,虚拟盘(如果有的话),主内存区;
高速缓冲区是为了应对块设备,对相应设备的读写均需要在此中转一次;
内核可以直接访问高速缓冲区,但需要通过mm访问主内存区;

三、minix中驱动程序实现方式

minix为微内核设计方式,共分为四层。从上到下依次为用户进程、服务器进程、驱动进程、内核进程(包含时钟和系统任务)

只有系统任务(运行在内核模式下)有权限访问各内存位置,有能力完成拷贝操作。

典型调用过程为:用户进程读文件,转换为信号方式,向文件系统(服务器进程)发送信号,文件系统再调用系统任务访问IO接口,当IO过程完成后,其向驱动程序发送中断信号,驱动完成拷贝后再向文件系统发送信号,文件系统再向用户进程发送信号。

(0)主程序

驱动程序的执行方式为:进行硬件相关的初始化,进入循环等待接收中断信号,接收到中断信号后执行后续操作(读/写/更改设备参数等操作)。这个流程对很多驱动都适用,是一个通用的程序。具体是通过一个结构体实现,结构体中的元素全为函数指针,这样具体的程序实现函数后便可通过这种方式创建结构体,再传给此主程序即可。

(1)RAM盘驱动程序

实际上为一段内存区域。分为若干设备。
init:初始化各设备。
prepare:判断索引是否在此区域,不在则返回空指针(设备结构体类型)。
transfer:根据次设备号,调用系统任务sys_vircopy/sys_phycopy进行拷贝。

参考

《操作系统设计与实现》
《Linux内核完全注释》

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值