S02. 手写x86操作系统--内核实现(更新中)

手写x86操作系统学习笔记--S01. 手写x86操作系统--引导程序和加载器load的实现-CSDN博客

 一、虚拟内存管理

1、内存分页 

二级页表线性地址转换物理地址过程如下:

  1. 用虚拟地址的高10位乘以4,作为页目录表内的偏移地址,加上页目录表的物理地址,所得的和便是页目录项的物理地址。读取该页目录项,从中获取到页表的物理地址。
  2. 用虚拟地址的中间10位乘以4,作为页表内的偏移地址,加上在第1步中得到的页表物理地址,所得的和,便是页表项的物理地址。读取该页表项,从中获取到分配的物理页地址。
  3. 虚拟地址的高10位和中间10位分别是PDE(页目录项)和PTE(页表项)的索引值,所以它们需要乘以4。但低12位就不是索引值啦,其表示的范围是0-0xFFF,作为页内偏移最合适,所以虚拟地址的低12位加上第2步中得到的物理页地址,所得的和便是最终转换的物理地址。

100f0b070b434aa49e1825c0805a7c64.png

2、零碎知识点 

  • 对于段寄存器的入栈,即 cs、ds、es、fs、gs、ss,无论在哪种模式下,都是按当前模式的默认操作数大小压入的。例如,在16位模式下,CPU直接压入2字节,栈指针sp减2。在32位模式下,CPU直接压入4字节,栈指针esp减4。
  • 对于通用寄存器和内存,无论是在实模式或保护模式:如果压入的是16位数据,栈指针减2。如果压入的是32位数据,栈指针减4。
  • 一个段描述符,在CPU眼里分为两大类,要么描述的是系统段,要么描述的是数据段,这是由段描述符中的S位决定的,用它指示是否是系统段。无论是代码,还是数据,甚至包括栈,它们都作为硬件的输入,都是给硬件的数据而已,所以代码段在段描述符中也属于数据段(非系统段)。各种称为 的结构便是系统段,也就是硬件系统需要的结构,非软件使用的,如调用门任多门。
  • 段寄存器CS、DS、ES、FS、GS、SS,在实模式下时,段中存储的是段基地址,即内存段的起始地址。而在保护模式下时,由于段基址已经存入了段描述符中所以段寄存器中再存放段基址是没有意义的,在段寄存器中存入的是一个叫作选择子的东西--Selector。
  • GDT中的第0个段描述符是不可用的,原因是定义在GDT中的段描述符是要用选择子来访问的,如果使用的选择子忘记初始化,选择子的值便会是0这便会访问到第0个段描述符。为了避免出现这种因忘记初始化选择子而选择到第0个段描述符的情况,GDT中的第0个段描述符不可用。
  • 在保护模式中,我们采用Linux等主流操作系统的内存段----平坦模型平坦模型就是整个内存都在一个段里,不用再像实模式那样用切换段基址的式访问整个地址空间。在32位保护模式中,寻址空间是4G,所以,平坦模型在我们定义的描述符中,段基址是0,段界限*粒度等于4G。粒度我们选的是4k,故段界限是0xFFFF。

 二、中断与异常

 1、中断分类

把中断按事件来源分类,来自CPU外部的中断就称为外部中断,来自CPU内部的中断称为内部中断。外部中断按是否导致宕机来划分,可分为可屏蔽中断和不可屏蔽中断两种,而内部中断按中断是否正常来划分,可分为软中断(int 8位立即数)和异常(故障、陷入和终止)。 

CPU为大家提供了两条信号线。外部硬件的中断是通过两根信号线通知CPU的,这两根信号线就是INTR(INTeRrupt)和NMI(Non Maskable Interrupt)。可屏蔽中断是通过INTR引脚进入CPU的,外部设备如硬盘、网卡等发出的中断都是可屏蔽中断。可屏蔽的意思是此外部设备发出的中断,CPU可以不理会,因为它不会让系统宕机,所以可以通过eflags寄存器的IF位将所有这些外部设备的中断屏蔽。不可屏蔽中断是通过NMI引脚进入CPU的,它表示系统中发生了致命的错误,它等同于宣布:计算机的运行到此结束了。可屏蔽中断并不会导致致命问题,它的数量是有限的,所以每一种中断源都可以获得一个中断向量号。而不可屏蔽中断引起的致命错误原因有很多,每一种都是硬伤,出现了基本上可以认为用软件解决不了,所以不可屏蔽中断的中断向量号统一为2。

对于中断是否无视eflags中的IF位,可以这么理解:首先,只要是导致运行错误的中断类型都不受IF位的管束,如INM、异常。其次,由于int n型的软中断用于实现系统调用功能,不能因为IF位为0就不顾用户请求,所以为了用户功能正常,软中断必须无视IF位。

2、中断处理过程 

2.1 中断向量和中断门描述符表

为了统一中断管理,把来自外部设备、内部指令的各种中断类型统统归结为一种管理方式,即为每个中断信号分配一个整数,用此整数作ID,而这个整数就是所谓的中断向量,ID作为中断描述符表的索引,这样就能找到对应的表项,进而从中找到对应的中断处理程序。中断向量的作用和选择子类似,它们都用来在描述符表中索引一个描述符,只不过选择子用于在GDT或LDT中检索段描述符,而中断向量专用于中断描述符表,其中没有RPL字段。异常和不可屏蔽中断的中断向量号是由CPU自动提供的,而来自外部设备的可屏蔽中断号是由中断代理提供的(咱们这里的中断代理是8259A),软中断是由软件提供的。

中断门:包含了中断处理程序所在段的段选择和段内偏移地址。当通过此方式进入中断后,标志寄存器eflags中的IF位自动置0,也就是在进入中断后,自动把中断关闭,避免中断嵌套。Linux就是利用中断门实现的系统调用,就是那个著名的int 0x80。中断门允许存在于IDT中,描述符中中断门的ype值为二进制1110。

2874ab53b11f4a009effcc780c2f59fb.png

同加载GDTR一样,加载DTR也有个专门的指令--lidt,其用法是:lidt 48位内存数据,在这48位内在数据中,前16位是IDT表的界限,后32位是IDT线性基地址。 

406351a51d0e436a850e67af330e01ac.png

2.2 中断流程 

完整的中断过程分为CPU外和CPU内两部分。CPU外:外部设备的中断由中断代理芯片接收,处理后将该中断的中断向量号发送到CPU。CPU内:CPU执行该中断向量号对应的中断处理程序,处理器内的过程如下:

a5d8fd11c7794bdfbba9ac1d593eaee3.png4db2da1366764130aeceace046cebdcd.png

d2a1fa1d08184357b58ab0c89666b49f.png

8c334cd6612847fe8685c6eeb62e3afd.png

进入中断时要把NT位和TF位置为0。TF表示Trap Flag,也就是陷阱标志位,这用在调试环境中,当TF为0时表示禁止单步执行。

d2ebaa53b1424611890f8f42eb204077.png

a234e8970f004bbb958bddfe9608affc.png

3、中断发生时的压栈

 3.2 中断进入

973b41c9a4bf4175879096fcd243b85a.png

下面看看以上寄存器入栈情况及顺序,这里不再讨论有关特权检查的内容: 

  1. 处理器根据中断向量号找到对应的中断描述符后,拿CPL和中断门描述符中选择子对应的目标代码段的DPL比对,若CPL权限比DPL低,即数值上CPL>DPL,这表示要向高特权级转移,需要切换到高特叔级的栈。这也意味着当执行完中断处理程序后,若要正确返回到当前被中断的进程,同样需要将栈恢复为此时的旧栈。于是处理器先临时保存当前旧栈SS和ESP的值,记作SS_old和ESP_old,然后在TSS中找到同目标代码段DPL级别相同的栈加载到寄存器SS和ESP中,记作SS_new和ESP_new,再将之前临时保存的SS_old和ESP_old压入新栈备份。
  2. 在新栈中压入EFLAGS寄存器。
  3. 由于要切换到目标代码段,对于这种段间转移,要将CS和EIP保存到当前栈中备份,记作CS old和EIP_old,以便中断程序执行结束后能恢复到被中断的进程。当前栈是新栈,还是旧栈,取决于是否涉及到特权级转移。
  4. 某些异常会有错误码,此错误码用于报告异常是在哪个段上发生的,也就是异常发生的位置,所以错误码中包含选择子等信息。错误码会紧跟在EP之后入栈,记作ERROR_CODE。

如果未涉及到特权级转移,便不会到TSS中寻找新栈,而是继续使用当前旧栈,因此也谈不上恢复旧栈,此时中断发生时栈中数据不包括SS_old和ESP_old。比如中断发生时当前正在运行的是内核程序,这是0特权级到0特权级,无特权级变化。

bc91b2a7d2bd4e8fa76c180fee554b9c.png

428bca0e0633488d95c2c4d072d20336.png

 3.2 中断返回

中断处理程序返回时,假设在32位模式下,它从当前栈顶处依次弹出32位数据分别到寄存器EIP、CS、EFLAGS。iret指令并不清楚栈中数据的正确性,它只负责把栈顶处往上的数据,每次4字节,对号入座弹出到相关寄存器,所以在使用iret之前,一定要保证栈顶往上的数据是正确的。注意,如果中断有错误码,处理器并不会主动跳过它的位置,咱们必须手动将其跳过 

f417ff174eb14910a8921f45315a7081.png

// ***利用中断返回的方式,从内核切换至第一个进程运行***
void move_to_first_task(void) {
    // 不能直接用Jmp far进入,因为当前特权级0,不能跳到低特权级的代码
    // 在iret后,还需要手动加载ds, fs, es等寄存器值,iret不会自动加载
    task_t * curr = task_current();
    ASSERT(curr != 0);

    tss_t * tss = &(curr->tss);

    __asm__ __volatile__(
        // 模拟中断返回,切换入第1个可运行应用进程
        // 不过这里并不直接进入到进程的入口,而是先设置好段寄存器,再跳过去
        "push %[ss]\n\t"			// SS
        "push %[esp]\n\t"			// ESP
        "push %[eflags]\n\t"        // EFLAGS
        "push %[cs]\n\t"			// CS
        "push %[eip]\n\t"		    // ip
        "iret\n\t"::[ss]"r"(tss->ss),  [esp]"r"(tss->esp), [eflags]"r"(tss->eflags),
        [cs]"r"(tss->cs), [eip]"r"(tss->eip));
}

4. 外部中断

4.1 可编程中断控制器8259A 

 8259A是可屏蔽中断的代理,其作用是负责所有来自外设的中断,其中就包括来自时钟的中断,以后要通过它完成进程调度。Intel处理器共支持256个中断,但8259A只可以管理8个中断,所以为了多支持一些中断设备,提供了另一个解决方案,将多个8259A级联。有了级联这种组合后,每一个8259A就被称为1片。若采用级联方式,即多片8259A芯片串连在一起,最多可级联9个,也就是最多支持64个中断。n片8259A通过级联可支持7n+1个中断源,级联时只能有一片8259A为主片master,其余的均为从片slave。来自从片的中断只能传递给主片,再由主片向上传递给CPU,也就是说只有主片才会向CPU发送INT中断信号,如图7-11所示。e4fd39982e2d4ddfb638e2dfe96b2971.png

 可屏蔽中断响应流程:

  1. 当某个外设发出一个中断信号时,由于主板上已经将信号通路指向了8259A芯片的某个IRQ接口,所以该中断信号最终被送入了8259A。8259A首先检查IMR寄存器中是否已经屏蔽了来自该IRQ接口的中断信号。IMR寄存器中的位,为1,则表示中断屏蔽,为0,则表示中断放行。
  2. 如果该IRQ对应的相应位已经被置1,即表示来自该IRQ接口上的中断已经被屏蔽了,则将该中断信号丢弃,否则,将其送入IRR寄存器,将该IRQ接口所在IRR寄存器中对应的BIT置1。IRR寄存器的作用相当于待处理中断队列。在某个恰当时机,优先级仲裁器PR会从IRR寄存器中挑选一个优先级最大的中断,此处的优先级决判很简单,就是IRQ接口号越低,优先级越大,所以IRQ0优先级最大。
  3. 之后,8259A会在控制电路中,通过INT接口向CPU发送INTR信号。信号被送入了CPU的INTR接口后,这样CPU便知道有新的中断到来了,于是CPU将手里的指令执行完后,马上通过自己的INTA接口向8259A的INTA接口回复一个中断响应信号,表示已准备好。
  4. 8259A在收到这个信号后,立即将刚才选出来的优先级最大的中断在ISR寄存器中对应的BIT置1,此寄存器表示当前正在处理的中断,同时要将该中断从“待处理中断队列”寄存器IRR中去掉,也就是在IRR中将该中断对应的BIT置0。
  5. 接下来,CPU将再次发送INTA信号给8259A,这一次是想获取中断对应的中断向量号,就是我们前面所说的0~255的“整数”。由于大部分情况下8259A的起始中断向量号并不是0,所以用起始中断向量号+IRQ接口号便是该设备的中断向量号随后,8259A将此中断向量号通过系统数据总线发送给CPU。CPU从数据总线上拿到该中断向量号后,用它做中断向量表或中断描述符表中的索引,找到相应的中断处理程序并去执行。

8259A 中断控制器寄存器汇总表:

寄存器名称宽度描述
中断屏蔽寄存器 (IMR)8 位屏蔽或允许中断请求。每一位对应一个中断线。
中断请求寄存器 (IRR)8 位记录当前等待服务的中断请求。每一位对应一个中断线。
中断服务寄存器 (ISR)8 位记录当前正在服务的中断请求。每一位对应一个中断线。
初始化命令寄存器 1 (ICW1)8 位边沿触发,级联模式,需要 ICW4。
初始化命令寄存器 2 (ICW2)8 位设置中断向量偏移,即起始中断向量号。
初始化命令寄存器 3 (ICW3)8 位配置主从 8259A 连接关系,仅在级联的方式下才需要。
初始化命令寄存器 4 (ICW4)8 位用于设置8259A的工作模式,当ICW1中的IC4为1时才需要ICW4。
操作命令寄存器 1 (OCW1)8 位设置中断屏蔽寄存器(IMR)。
操作命令寄存器 2 (OCW2)8 位设置中断结束方式和优先级模式。
操作命令寄存器 3 (OCW3)8 位用于读取 IRR 或 ISR 的状态。

8259A的编程步骤:

既然8259A称为可编程中断控制器,就说明它的工作方式很多,咱们就要通过编程把它设置成需要的样子。对它的编程也很简单,就是对它进行初始化,设置主片与从片的级联方式,指定起始中断向量号以及设置各种工作模式。8259A的编程就是写入ICW和OCW,下面总结下写入的步骤 :

19ebd63d61984330a3608756e9321557.png

4.2 可编程定时器8253

Intel 8253 可编程定时器 (Programmable Interval Timer, PIT) 是一种广泛用于计算机系统中的计时和定时设备。它具有三个独立的计时器,每个计时器可以独立编程来执行各种计时任务。PIT芯片使用的振荡器运行频率(大约)为1.193182 MHz。

I/O port     Usage
0x40         Channel 0 data port (read/write)
0x41         Channel 1 data port (read/write)
0x42         Channel 2 data port (read/write)
0x43         Mode/Command register (write only, a read is ignored)

8253控制字寄存器

名称位数范围描述
BCD0BCD 模式选择。0:二进制计数,1:  BCD 计数。
M0, M1, M21-3定时器工作方式选择。
RW1, RW24-5设置待操作计数器的读写及锁存方式。
SC0, SC16-7选择计数器(0, 1, 2)。

 IRQ0引脚上的时钟中断信号频率是由8253的计数器0设置的,我们要使用计数器0。时钟发出的中断信号必须是周期性发出的,也就是我们要采取循环计数的工作方式,可选的工作方式为方式2和方式3,这里咱选择方式3。初始化8253的步骤如下:

cd35247716344fbf993c626cb522dfdf.png

5、利用中断门实现中断处理

// ***中断和异常初始化***
// ***1. 初始化中断描述符表,即分别初始化其中128个表项(特权级0);2. 基于给定的中断向量号安装中断处理程序***
// ***3. 将中断描述符表加载到IDTR寄存器;4. 初始化pic 控制器***
void irq_init(void) {
    // 初始化中断描述符表,即分别初始化其中128个表项(特权级0)
	for (uint32_t i = 0; i < IDT_TABLE_NR; i++) {
    	gate_desc_set(idt_table + i, KERNEL_SELECTOR_CS, (uint32_t) exception_handler_unknown,
                  GATE_P_PRESENT | GATE_DPL0 | GATE_TYPE_IDT);
	}
	// 安装异常处理接口(可屏蔽中断不在此处安装,在初始化对应硬件设备时安装)
    irq_install(IRQ0_DE, exception_handler_divider);
    ...
	lidt((uint32_t)idt_table, sizeof(idt_table));
	// 初始化可编程中断控制器8259A(中断代理),以接收外部中断
	init_pic();
}

// ***初始化硬件定时器
static void init_pit (void) {
    uint32_t reload_count = PIT_OSC_FREQ / (1000.0 / OS_TICK_MS);

    outb(PIT_COMMAND_MODE_PORT, PIT_CHANNLE0 | PIT_LOAD_LOHI | PIT_MODE3);
    outb(PIT_CHANNEL0_DATA_PORT, reload_count & 0xFF);   // 加载低8位
    outb(PIT_CHANNEL0_DATA_PORT, (reload_count >> 8) & 0xFF); // 再加载高8位

    irq_install(IRQ0_TIMER, (irq_handler_t)exception_handler_timer);
    irq_enable(IRQ0_TIMER);
}

// ***安装中断处理程序,输入:中断号 irq_num,中断处理程序函数指针 handler
// ***将中断号为irq_num的中断描述符的处理程序设置为  handler
int irq_install(int irq_num, irq_handler_t handler) {
	if (irq_num >= IDT_TABLE_NR) {
		return -1;
	}
    gate_desc_set(idt_table + irq_num, KERNEL_SELECTOR_CS, (uint32_t) handler,
                  GATE_P_PRESENT | GATE_DPL0 | GATE_TYPE_IDT);
	return 0;
}

void do_handler_unknown (exception_frame_t * frame) {
	do_default_handler(frame, "Unknown exception.");
}
// ***中断发生时相应的栈结构,暂时为无特权级发生的情况***
typedef struct _exception_frame_t {
    // 结合压栈的过程,以及pusha指令的实际压入过程
    int gs, fs, es, ds;
    int edi, esi, ebp, esp, ebx, edx, ecx, eax;
    int num;
    int error_code;
    int eip, cs, eflags;
    int esp3, ss3;
}exception_frame_t;
// 中断发生时,会自动切换到特权级0对应的栈中去执行
// 并且只保存ss,esp,cs,eip,flags寄存器
// 所以需要在中断中自行保存其它寄存器
.text
.macro exception_handler name num with_error_code
	    .extern do_handler_\name
		.global exception_handler_\name
	exception_handler_\name:
		// 如果没有错误码,压入一个缺省值
		// 这样堆栈就和有错误码的情形一样了
		.if \with_error_code == 0
			push $0
		.endif

		// 压入异常号
		push $\num

		// 保存所有寄存器,保护中断调用前的现场环境
		pushal
		push %ds
		push %es
		push %fs
		push %gs

		// 调用中断处理函数
		push %esp // 通过压栈的方式,将exception_frame_t的地址传给 do_handler_\name,函数调用传参就是这样
		call do_handler_\name
		add $(1*4), %esp //弹出esp

		// 恢复保存的寄存器
		pop %gs
		pop %fs
		pop %es
		pop %ds
		popal

		// 跳过压入的异常号和错误码
		add $(2*4), %esp
		iret
.endm

// 软件中断
exception_handler unknown, -1, 0
...

// 硬件中断
exception_handler timer, 0x20, 0
exception_handler kbd, 0x21, 0
exception_handler ide_primary, 0x2E, 0
static uint32_t sys_tick; // 系统启动后的tick数量

// ***定时器中断处理函数
void do_handler_timer (exception_frame_t *frame) {
    sys_tick++;

    // 先发EOI,而不是放在最后
    // 放最后将从任务中切换出去之后,除非任务再切换回来才能继续噢应
    pic_send_eoi(IRQ0_TIMER);

    // ***时间片轮转处理,该函数在中断处理函数中调用***
    // ***1. 将时间片用完的任务放到就绪队列尾部,然后进行任务切换***
    // ***2. 将睡眠时间到达的任务从睡眠队列移除,加入就绪队列尾部***
    task_time_tick();
}

三、进程间的同步与互斥

/**
 * 互斥锁
 */

typedef struct _mutex_t {
    task_t * owner;
    int locked_count;
    list_t wait_list;
}mutex_t;

#include "cpu/irq.h"
#include "ipc/mutex.h"

/**
 * 锁初始化
 */
void mutex_init (mutex_t * mutex) {
    mutex->locked_count = 0;
    mutex->owner = (task_t *)0;
    list_init(&mutex->wait_list); //初始化等待队列
}

// 申请锁
//如果申请不到锁,着进行任务切换进入到等待队列
//如果申请到了锁,则继续执行
void mutex_lock (mutex_t * mutex) {
    irq_state_t  irq_state = irq_enter_protection();

    task_t * curr = task_current();
    if (mutex->locked_count == 0) {
        // 没有任务占用,占用之
        mutex->locked_count = 1;
        mutex->owner = curr; //设置锁的拥有者
    } else if (mutex->owner == curr) {
        // 已经为当前任务所有,只增加计数
        mutex->locked_count++;
    } else { //如果申请不到锁,则进行任务切换进入到等待队列
        // 有其它任务占用,则进入队列等待
        task_t * curr = task_current(); //获取当前运行的任务
        task_set_block(curr); //将当前任务移除就绪队列
        list_insert_last(&mutex->wait_list, &curr->wait_node); //将当前任务插入等待队列
        task_dispatch(); //切换到其它任务运行
    }

    irq_leave_protection(irq_state);
}

// 释放锁
// 如果等待队列不为空,则取出等待队列的第一个任务,立即唤醒并占用锁
void mutex_unlock (mutex_t * mutex) {
    irq_state_t  irq_state = irq_enter_protection();

    // 只有锁的拥有者才能释放锁
    task_t * curr = task_current();
    if (mutex->owner == curr) {
        if (--mutex->locked_count == 0) {
            // 减到0,释放锁
            mutex->owner = (task_t *)0; //置空锁的拥有者

            // 如果队列中有任务等待,则立即唤醒并占用锁
            if (list_count(&mutex->wait_list)) { //如果等待队列不为空
                list_node_t * task_node = list_remove_first(&mutex->wait_list); //获取等待队列的第一个节点
                task_t * task = list_node_parent(task_node, task_t, wait_node); //根据节点定位到任务
                task_set_ready(task); //将任务插入到就绪队列

                // 在这里占用,而不是在任务醒后占用,因为可能抢不到
                mutex->locked_count = 1; //设置锁的计数为1
                mutex->owner = task;

                task_dispatch(); //任务切换
            }
        }
    }

    irq_leave_protection(irq_state);
}

/**
 * 计数信号量
 */

// ***进程同步用的计数信号量
typedef struct _sem_t {
    int count;				// 信号量计数
    list_t wait_list;		// 等待的进程列表
}sem_t;

#include "cpu/irq.h"
#include "core/task.h"
#include "ipc/sem.h"

/**
 * 信号量初始化
 */
void sem_init (sem_t * sem, int init_count) {
    sem->count = init_count;
    list_init(&sem->wait_list);
}

/**
 * 申请信号量
 */
void sem_wait (sem_t * sem) {
    irq_state_t  irq_state = irq_enter_protection(); //关中断,保护临界资源,这里临界资源为信号量sem

    if (sem->count > 0) { //如果信号量大于0,则不用等待
        sem->count--;
    } else {
        // 从就绪队列中移除,然后加入信号量的等待队列
        task_t * curr = task_current();
        task_set_block(curr); // 将任务从就绪队列中移除
        list_insert_last(&sem->wait_list, &curr->wait_node); //将任务加入sem等待队列
        task_dispatch(); //任务分配与切换
    }

    irq_leave_protection(irq_state);
}

/**
 * 释放信号量
 */
void sem_notify (sem_t * sem) {
    irq_state_t  irq_state = irq_enter_protection(); //关中断,保护临界资源,这里临界资源为信号量sem

    if (list_count(&sem->wait_list)) {
        // 有进程等待,则唤醒加入就绪队列
        list_node_t * node = list_remove_first(&sem->wait_list); //取出等待队列的第一个节点
        task_t * task = list_node_parent(node, task_t, wait_node); //基于节点找到需要唤醒的任务
        task_set_ready(task); //将需要唤醒的任务加入就绪队列

        task_dispatch(); //任务分配与切换,不是一定必须切换
    } else { // 没有进程等待,则信号量sem记数值加一
        sem->count++;
    }

    irq_leave_protection(irq_state);
}

/**
 * 获取信号量的当前值
 */
int sem_count (sem_t * sem) {
    irq_state_t  irq_state = irq_enter_protection();
    int count = sem->count;
    irq_leave_protection(irq_state);
    return count;
}

四、特权级 & 系统调用

1、特权级简介

CPL:在CPU中运行的是指令,故而代码段猫述符中的DPL,便是当前CPU所处的特权级,这个特权级称为当前特权级,即CPL(Current Privilege Level),它表示处理器正在执行的代码的特权级别。当处理器特权级检查的条件通过后,新代码段的DPL就变成了处理器的CPL,并且且标代码段描述符的DPL将保存在代码段寄存器CS中的RPL位。注意,只是代码段寄存器CS中的RPL是CPL,其他段寄存器中选择子的RPL与CPL无关。在任意时刻,当前特权级CPL保存在CS选择子中的RPL部分。

RPL:由于指令存放在代码段中,所以,就用代码段寄存器CS中选择的RPL位表示代码请求别人资源能力的等级。代码段寄存器CS和指令指针寄存器EP中指向的指令便是当前在处理器中正在运行的代码。CPL是对当前正在运行的程序而言的,而RPL有可能是正在运行的程序,也可能不是。在一般情况下,如果低特权级不向高特权级程序提供自己特权级下的选择子,也就是不涉及向高特权级程序委托办事的话,CPL和RPL都来自同一程序。但凡涉及委托,进入0特权级后,CPL是指代理人(即内核),RPL则是委托者。

特权级转移分为两类,一类是由中断门、调用门等手段实现低特权级转向高特权级,另一类则相反,是由调用返回指令从高特权级返回到低特叔级,这是唯一能让处理器降低特权级的情况。  

访问者任何时候都不允许访问比自己特权更高的资源,无论受访资源是数据,还是代码。对于受访者为数据段来说,只有访问者的权限大于等于该DPL表示的最低权限才能够继续访问,否则连这个门槛都迈不过去。对于受访者为代码段来说,只有访问者的权限等于该DPL表示的最低权限才能够继续访问,即只能平级访问。处理器能够通过以下两方式转向执行高特权级的代码段:

  1. 基于一致性代码段实现。一致性代码段也称为依从代码段,用来实现从低特权级的代码向高特权级的代码转移。一致性代码段是指如果自己是转移后的目标段,自己的特权级(DPL)一定要大于等于转移前的CPL,即数值上CPL≥DPL。一致性代码段的一大特点是转移后的特权级不与自己的特权级(DPL)为主,而是与转移前的低特权级一致,也就是说处理器遇到目标段为一致性代码段时,并不会将CPL用该目标段的DPL替换。代码段可以有一致性和非一致性之分,但所有的数据段总是非一致的,即数据段不允许被比本数据段特权级更低的代码段访问。(本实现全程不采用一致性代码段)
  2. 通过门结构(中断门或者调用门)实现由低特权级转移到高特权级。

不通过门结构,直接访问一般数据和代码时的特权检查规则:

ebcb80e21cca48a597958e8a60a693cd.png

2、调用门

calljmp指令后接调用门选择子为参数以调用函数例程的形式实现从低特权向高特权转移,可用来实现系统调用。call指令使用调用门可以实现向高特权代码转移,jmp指令使用调用门只能实现向平级代码转移。门的门槛是访问者特权级的下限,访问者的特权级再低也不能比门描述符的特权级DPL低,即数值上CPL≤门的DPL。门的门框是访问者特权级的上限,访问者的特权级再高也不能比门描述符中且标程序所在代码段的DPL高,即数值上CPL≥目标代码段DPL

66e06f34a47844c980d0f8e94b2ce978.png

1d710910a9f946369af27dc2644aec3d.png

假设用户进程要调用某个调用门,该门描述符中参数的个数是2,调用前的当前特权级为3,调田后的新特权级为0。用户进程通过cal指令调用调用门的完整过程:

  1. 现为此调用门提供2个参数,这是在使用调用门前完成的,目前是在3特权级,所以要3特权级栈中压入参数,分别是参数1和参数2。
  2. 在这一步骤中要确定新特权级使用的栈,根据门描述符中选择子对应的目标代码段的DPL(等于0),处理器自动在TSS中找到合适的栈段选择子SS和栈指针ESP,它们作为转移后新的栈。为方便叙述,将它们记作SS_new、ESP_new。
  3. 检查新栈段选择子对应的描述符的DPL和TYPE,如果未通过检查则处理器引发异常。
  4. 如果转移后的目标代码段DPL比CPL要高,说明栈段选择子SS_new是特权级更高的栈,这说明需要特权级转换,需要切换到新栈,由于转移前的旧栈段选择子SS_old及指针ESP_old得保存到新栈中,这样在高特权级的目标程序执行完成后才能通过retf指令恢复旧栈。处理器先找个地方临时保存SS_old和ESP_old,之后将SS_new加载到栈段寄存器SS,esp_new加载到栈指针寄存器esp,这样便启用了新栈。
  5. 在使用新栈后,将上一步中临时保存的SS_old和ESP_old压入到当前新栈中,也就是0特权级栈。由于讨论的是32位模式,故栈操作数也是32位,SS_old只是16位数据,将其高16位用0填充后入栈保存。
  6. 在这一步中要将用户栈中的参数复制到转移后的新栈中,根据调用门描述符中的参数个数决定复制几个参数。
  7. 由于调用门描述符中记录的是目标程序所在代码段的选择子及偏移地址,这意味着代码段寄存器CS要用该选择子重新加载,只要段寄存器被加载,段描述符缓冲寄存器就会被刷新,从而相当于切换到了新段上运行,这是段间远转移。所以需要将当前代码段CS和EIP都备份在栈中,这两个值分别记作CS_old和EIP_old,这两个值是将来恢复用户进程的关键。
  8. 把门描述符中的代码段选择子装载到代码段寄存器CS,把偏移量装载到指令指针寄存器EP。至此,处理器终于从用户程序转移到了内核程序上,实现了特权级由3到0的转移,开始执行门描述符中对应的内核服务程序。

d24101cdbef44e4f9f01b18c70b5b00c.png

00b72df030e844a4885e152ab680fbdf.png

下面是利用retf指令丛调用门返回的过程: 

  1. 当处理器执行到retf指令时,它知道这是远返回,所以需要从栈中返回旧栈的地址及返回到低特权级的程序中。这时候它要进行特权级检查。先检查栈中CS选择子,根据其RPL位,即未来的CPL,判断在返回过程中是否要改变特权级。
  2. 此时栈顶应该指向栈中的EIP_old,在此步骤中获取栈中CS_old和EIP_old,根据该CS_old选择子对应的代码段的DPL及选择子中的RPL做特权级检查,如果检查通过,先从栈中弹出32位数据,即EIP_old到寄存器EP,然后再弹出32位数据CS_old,此时要临时处理一下,由于所有的段寄存器都是16位的,当然包括CS,所以丢弃CS old的高16位,将低16位加载到CS寄存器。此时栈指针ESP_new指向最后一个参数。
  3. 如果返回指令retf后面有参数,则增加栈指针ESP_new的值,以跳过栈中参数,如参数1和参数2,所以retf后面的参数应该等于参数个数*参数大小。此时,栈指针ESP_new便指向ESP_old。
  4. 如果在第1步中判断出需要改变特权级,从栈中弹出32位数据ESP_old到寄存器ESP。同样寄存器SS也是16位的,故再弹出32位的SS_old,只将其低16位加载到寄存器SS,此时恢复了旧栈。

9cd122530d654615b5eb56032f3f51ac.png

8bb4095775664675afb3056a015beef4.png

3、通过创建单个调用门实现系统调用

int msleep (int ms) {
    if (ms <= 0) {
        return 0;
    }
    syscall_args_t args;
    args.id = SYS_msleep;
    args.arg0 = ms;
    return sys_call(&args);
}
// ***执行系统调用
static inline int sys_call (syscall_args_t * args) {
    const unsigned long sys_gate_addr[] = {0, SELECTOR_SYSCALL | 0};  // 使用特权级0
    int ret;
    // 采用调用门, 这里只支持5个参数
    // 用调用门的好处是会自动将参数复制到内核栈中,这样内核代码很好取参数
    // 而如果采用寄存器传递,取参比较困难,需要先压栈再取
    __asm__ __volatile__(
            "push %[arg3]\n\t"
            "push %[arg2]\n\t"
            "push %[arg1]\n\t"
            "push %[arg0]\n\t"
            "push %[id]\n\t"
            "lcalll *(%[gate])\n\n"
            :"=a"(ret)
            :[arg3]"r"(args->arg3), [arg2]"r"(args->arg2), [arg1]"r"(args->arg1),
    [arg0]"r"(args->arg0), [id]"r"(args->id),
    [gate]"r"(sys_gate_addr));
    return ret;
}
// ***初始化GDT表***
// ***1. 将表中所有段描述符清零;2. 设置一个数据段描述符(特权级0);3. 设置一个代码段(非一致,特权级0)描述符;***
// ***4. 为系统调用设置一个调用门描述符(特权级3);5. 加载GDT到GDTR寄存器***
void init_gdt(void) {
	// 全部清空
    for (int i = 0; i < GDT_TABLE_SIZE; i++) {
        segment_desc_set(i << 3, 0, 0, 0);
    }

    //数据段
    segment_desc_set(KERNEL_SELECTOR_DS, 0x00000000, 0xFFFFFFFF,
                     SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_DATA
                     | SEG_TYPE_RW | SEG_D | SEG_G);

    // 只能用非一致代码段,以便通过调用门更改当前任务的CPL执行关键的资源访问操作
    segment_desc_set(KERNEL_SELECTOR_CS, 0x00000000, 0xFFFFFFFF,
                     SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_CODE
                     | SEG_TYPE_RW | SEG_D | SEG_G);

    // 调用门
    gate_desc_set((gate_desc_t *)(gdt_table + (SELECTOR_SYSCALL >> 3)),
            KERNEL_SELECTOR_CS,
            (uint32_t)exception_handler_syscall,
            GATE_P_PRESENT | GATE_DPL3 | GATE_TYPE_SYSCALL | SYSCALL_PARAM_COUNT);

    // 加载gdt
    lgdt((uint32_t)gdt_table, sizeof(gdt_table));
}
exception_handler_syscall:
	// 保存前一任务的状态
	pusha
	push %ds
	push %es
	push %fs
	push %gs
	pushf

	// 使用内核段寄存器,避免使用应用层的
	mov $(KERNEL_SELECTOR_DS), %eax
	mov %eax, %ds
	mov %eax, %es
	mov %eax, %fs
	mov %eax, %gs

    // 调用处理函数
    mov %esp, %eax
    push %eax
	call do_handler_syscall
	add $4, %esp

    // 再切换回来
	popf
	pop %gs
	pop %fs
	pop %es
	pop %ds
	popa
	
	// 5个参数,加上5*4,不加会导致返回时ss取不出来,最后返回出现问题
    retf $(5*4)    // CS发生了改变,需要使用远跳转
// 系统调用表
static const syscall_handler_t sys_table[] = {
	[SYS_msleep] = (syscall_handler_t)sys_msleep,
};

// ***处理系统调用,该函数由系统调用函数调用***
void do_handler_syscall (syscall_frame_t * frame) {
	// 超出边界,返回错误
    if (frame->func_id < sizeof(sys_table) / sizeof(sys_table[0])) {
		// 查表取得处理函数,然后调用处理
		syscall_handler_t handler = sys_table[frame->func_id];
		if (handler) {
			int ret = handler(frame->arg0, frame->arg1, frame->arg2, frame->arg3);
			frame->eax = ret;  // 设置系统调用的返回值,由eax传递
            return;
		}
	}
	// 不支持的系统调用,打印出错信息
	task_t * task = task_current();
	log_printf("task: %s, Unknown syscall: %d", task->name,  frame->func_id);
    frame->eax = -1;  // 设置系统调用的返回值,由eax传递
}
// ***让当前任务进入睡眠状态,该函数调用 task_set_block() 和 task_set_sleep 实现***
void sys_msleep (uint32_t ms) {
    // 至少延时1个tick
    if (ms < OS_TICK_MS) {
        ms = OS_TICK_MS;
    }

    irq_state_t state = irq_enter_protection();

    // 从就绪队列移除,加入睡眠队列
    task_set_block(task_manager.curr_task);
    task_set_sleep(task_manager.curr_task, (ms + (OS_TICK_MS - 1))/ OS_TICK_MS);
    
    // 进行一次调度
    task_dispatch();

    irq_leave_protection(state);
}

4、通过0x80中断实现系统调用

除了使用调用门之外,还有其它的方式可以使用从特权级3转移特权级3。我们可以自行主动执行int 序号来触发一个中断门对应的处理程序执行。这样就同时可实现从特权级3到特权级0的转换。这个过程类似于在boot中执行bios中断来读取磁盘,相应的参数可通过寄存器传递。

6e0c4441375849cd96fcfbec6d06ae30.png

如同Linux,使用向量号为0x80的中断门,当需要执行系统调用时,执行int 0x80指令。参数也是通过寄存器传递,课程中使用了eax, ebx, ecx, edx, esi传递。在0x80对应的中断处理程序中,从exception_frame_t中取出这些值,然后从中取出相应的参数,再查找sys_table执行相应的系统调用函数。 

320e4a0761df49f09e1ee3b28d7d281b.png

五、进程管理

 1、TSS简介

TSS,即 Task State Segment,意为任务状态段。TSS是每个任务都有的结构,它用于一个任务的标识,相当于任务的身份证,程序拥有此结构才能运行,这是处理器硬件上用于任务管理的系统结构,处理器能够识别其中每一个字段。

TSS是硬件支持的系统数据结构,它和GDT等一样,由软件填写其内容,由硬件使用。GDT要加载到寄存器GDTR中才能被处理器找到,TSS也是一样,它是由TR(Task Register)寄存器加载的,每次处理器执行不同任务时,将TR寄存器加载成不同任务的TSS就成了。 

d8cd8fb2372d4aa488b24aa6d5218161.png

TSS同其他普通段一样,是位于内存中的区域,因此可以把TSS理解为TSS段,只不过TSS中的数据并不像其他普通段那样散乱,TSS中的数据是按照固定格式来存储的。TSS中的字段基本上全是寄存器名称,这些寄存器就是任务运行中的最新状态可见TSS的主要作用就是保存任务的快照,也就是CPU执行该任务时,寄存器当时的瞬时值。除了一般的寄存器外,TSS中还有I/O位图上一个任务的TSS指针。I/O位图在单个端口的粒度上进行IO特权控制。另外,CPU在不同特权级下用不同的栈,这三组栈是用来由低特权级往高特权级跳转时用的,最低的特权级是3,没有更低的特权级会跳入3特权级,因此TSS中没有SS3和esp3。 

TSS和LDT一样,必须要在GDT中注册才行,这也是为了在用描述符的阶段做安全检查,因此TSS是通过选择子来访问的,将tss加载到TR的指今是ltr,其指令格式为:

0855072b54424defb03e7c0801026ed5.png

有了TSS后,任务在被换下CPU时,由CPU自动地把当前任务的资源状态(所有寄存器、必要的内存结构,如栈等)保存到该任务对应的TSS中(由寄存器TR指定)。CPU通过新任务的TSS选择子加载新任务时,会把该TSS中的数据载入到CPU的寄存器中,同时用此TSS描述符更新寄存器TR。以上动作是CPU自动完成的,不需要人工千预,这就是前面所说的硬件一级的原生支持。不过,第一个任务的TSS是需要手工加载的,否则第一个任务的状态该没有地方保存了。 在系统初始化时,必须向Task Register写入一个有效的TSS描述符对应的选择子。在本课程中,写入的则是init_main()对应的描述选选择子,即其为最开始运行的任务。

4fa4415a3db54984bd00fded82ef08b8.png

为了支持多任务,CPU厂商提供了LDT及TSS这两种原生支持,他们要求为每一个任多分别配一个LDT及TSS,LDT中保存的是任务自己的实体资源,也就是数据和代码TSS中保存的是任务的上下文状态及三种特权级的栈指针、I/O位图等信息。既然LDT和TSS用来表示一个任务,那么任务切换就是换这两个结构:将新任务对应的LDT信息加载到LDTR寄存器,对应的TSS信息加载到TR寄存器。任务的段放在GDT,还是LDT中,无非就是在用选择子选择它们时有区别,任务私有的实体资源不是必须放在它白已的LDT中。综上所述,LDT是可有可无的,真正用于区分一个任务的标志是TSS。

2、任务切换 

在CPU眼里,一个TSS就代表一个任务,TSS才是任务的标志,CPU区分任多就是靠TSS。因此,只要TR寄存器中的TSS信息不换,无论执行的是哪里的指令,也无论指令是否跨越特权级(从用户态到内核态),CPU都认为还是在同一个任务中。 进行任务切换的方式有中断+任务门call或jmp+任务门iretd。

2.1 call、jmp切换任务

b1c14fcb52ea4470b4815be3176b385a.png

2.2 现代操作系统采用的任务切换方式 

df6c1432e6f444ffb14defe1c2204704.png

Linux为每个CPU创建一个TSS,在各个CPU上的所有任务共享同个TSS,各CPU的TR寄存器保存各CPU上的TSS,在用ltr指令加载TSS后,该TR寄存器永远指向同一个TSS,之后再也不会重新加载TSS。在进程切换时,只需要把TSS中的SS0及esp0更新为新任务的内核栈的段地址及栈指针。 当CPU由低特权级进入高特权级时,CPU会自动从TSS中获取对应高特权级的栈指针(TSS是CPU内部框架原生支持的,当然是直动从中获取新的栈指针)。具体地,Linux只用到了特权3级和特权0级,因此CPU从3特权级的用户态进入0特权级的内核态时(比如从用户进程进入中断),CPU自动从当前任务的TSS中获取SS0和esp0字段的值作为0特权级的栈,然后Linux手动执行一系列的push指令将任务的状态的保存在0特权级栈中,也就是TSS中esp0所指向的栈。

3、采用jmp实现任务的切换(需要给每个任务分配独立的TSS)

具体来说,一个程序在运行起来后,首先会为程序代码(.text)、使用到的数据(rodata、.data、.bss等)分配存储空间,即如之前看到的,loader将kernel加载到内存中运行时,需要将相应的代码和数据从文件中拷贝到特定的内存位置。这些内存会区域将在程序运行起来后被程序独自占有,不会被其它程序运行修改,所以这些内存中的当前内容虽然体现了当前程序的执行状态,但是并不需要保存。而程序运行时在访问这些内存时,需要使用到CS/SS/ES/DS/FS/GS这些段寄存器,而这些寄存器在CPU中只有一份,是被多个程序所共享的。所以,为了在程序恢复运行时,这些段寄存器要和之前相同,TSS中要有相应的字段,来保存该程序使用了哪些段。 

// ***进行一次任务调度,从就绪队列选择一个任务,然后采用ljmpl切换过去
void task_dispatch (void) {
    task_t * to = task_next_run();
    if (to != task_manager.curr_task) {
        task_t * from = task_manager.curr_task;

        task_manager.curr_task = to;
        task_switch_from_to(from, to);
    }
}
// ***切换至指定任务,采用ljmpl指令跳转到 *to 任务的tss段
void task_switch_from_to (task_t * from, task_t * to) {
     switch_to_tss(to->tss_sel);
}
// ***切换至TSS,即跳转实现任务切换
void switch_to_tss (uint32_t tss_selector) {
    far_jump(tss_selector, 0);
}

static inline void far_jump(uint32_t selector, uint32_t offset) {
	uint32_t addr[] = {offset, selector };
	__asm__ __volatile__("ljmpl *(%[a])"::[a]"r"(addr));
}

4、更简单高效的任务切换方法

b8c56c54237b40aa9ac8ea09dc220526.png

eda160c0322e4b1dac659161fbd35bab.png

// ***任务控制块结构(PCB)***
typedef struct _task_t {
	uint32_t * stack;

	tss_t tss;				// 任务的TSS段
	uint16_t tss_sel;		// tss选择子
}task_t;

// ***此种方式的初始化任务***
int task_init (task_t *task, uint32_t entry, uint32_t esp) {
/*	
	由于加载下一任务是通过ret指令在栈中找到相应入口地址,故而esp必须初始化为entry,
	然后再压入4个占位符与pop操作对应,保证ret能正确找到函数入口
	// pop %edi
	// pop %esi
	// pop %ebx
	// pop %ebp
  	// ret
*/
	//如果不采用intel建议的方式,便不需要再创建TSS,一个cpu共用一个TSS,但需要在task结构中创建一个栈以保存任务切换的上下文
	// 此时任务切换所需的上下文环境保存在tss->stack中,通过ret进行任务切换
	// tss_init(task, entry, esp); //此为采用intel建议的方式,每个任务都有一个独立的tss
    uint32_t * pesp = (uint32_t *)esp; // 将传入的 esp 地址转换为指向 uint32_t 的指针
    if (pesp) {
        *(--pesp) = entry;
		// 压入4个占位符,通常用于保存通用寄存器的值
		// *(--pesp)操作实现指针 pesp 向前移动一个位置,然后访问并修改这个新位置的值。
        *(--pesp) = 0;
        *(--pesp) = 0;
        *(--pesp) = 0;
        *(--pesp) = 0;
		// 保存新的栈指针到任务结构中
        task->stack = pesp;
    }
    return 0;
}

// 对 init_task 的初始化
task_init(&init_task, (uint32_t)init_task_entry, (uint32_t)&init_task_stack[1024]); //1024即可,压栈会先将栈顶指针下移,再压入

void task_switch_from_to (task_t * from, task_t * to) {
    // switch_to_tss(to->tss_sel); //此为采用intel建议的方式,每个任务都有一个独立的tss
    simple_switch(&from->stack, to->stack); //此为liunx做法
}
// eax, ecx, edx由调用者自动保存
// ebx, esi, edi, ebp需要由被调用者保存和恢复
// cs/ds/es/fs/gs/ss不用保存,因为都是相同的
// esp不用保存,只需要让esp保存在调用之前的状态
// eflags
// simple_switch(&from->stack, to->stack);
	.text
	.global simple_switch
simple_switch:
	movl 4(%esp), %eax   // 取&from->stack
	movl 8(%esp), %edx   // 取to->stack

	// 保存前一任务的状态
	push %ebp
	push %ebx
	push %esi
	push %edi

	//  %eax:指的是 eax 寄存器本身,操作的是寄存器中的值
	// (%eax):指的是 eax 寄存器中的值作为内存地址,操作的是该地址指向的内存中的值
	mov %esp, (%eax)    // from->stack = esp 保存原始栈到stack
  	mov %edx, %esp      // esp = to->stack 切换栈

	// 加载下一任务的栈
	pop %edi
	pop %esi
	pop %ebx
	pop %ebp
  	ret

 六、文件系统

1、MBR & DBR

在磁盘存储中,MBR(Master Boot Record,主引导记录)和 DBR(DOS Boot Record,DOS 引导记录)是两个重要的引导区结构,它们在磁盘启动和文件系统管理中发挥关键作用。

MBR(主引导记录)位于硬盘的第一个扇区,即逻辑块地址(LBA)0。MBR 包含启动引导代码、分区表和引导签名,是 BIOS 加载操作系统的第一步。它负责将控制权交给分区中的操作系统引导程序。

偏移量大小描述
0x000446 字节引导代码
0x1BE64 字节分区表(4 个分区,每个 16 字节)
0x1FE2 字节引导签名(0x55AA)

DBR(DOS 引导记录)DBR 位于每个分区的第一个扇区,即每个分区的逻辑块地址 0。DBR 包含文件系统的引导代码和基本信息。它负责加载操作系统引导程序并启动操作系统。DBR 结构(以 FAT 文件系统为例)如下:

偏移量大小描述
0x0003 字节跳转指令
0x0038 字节OEM 标识符
0x00B25 字节BPB(BIOS 参数块)
0x02426 字节扩展 BPB
0x03E448 字节引导代码
0x1FE2 字节引导签名(0x55AA)

MBR 与 DBR 的关系

引导过程:计算机启动时,BIOS 加载 MBR 并执行其中的引导代码。MBR 引导代码根据分区表信息找到活动分区,并将控制权交给该分区的 DBR。DBR 执行文件系统的引导代码,进一步加载操作系统引导程序。

分区管理:MBR 负责管理整个硬盘的分区信息,每个分区都有一个对应的 DBR。DBR 负责管理分区内的文件系统信息和引导操作系统。

11ef0cdc62a14529b46a9b40a9c60c7c.png

2786f0337bea4474a8045fbfbe56a8df.png

a5f61b44e6254f77b853bd51fbdd351f.png

da115f7e5559499eb0ba2d04e166e024.png

b02cd739c71448f9b8c70a0110964d9c.png

317fa530078444f4a9e23be2d662de92.png

21459bc9af194ae19e1e0e237c006c3f.png 971739005392499aa12a6130d25ee424.png

2、部分代码 

fs.h & fs.c

// ***文件系统操作接口***
typedef struct _fs_op_t {
	int (*mount) (struct _fs_t * fs,int major, int minor);
    void (*unmount) (struct _fs_t * fs);

    int (*open) (struct _fs_t * fs, const char * path, file_t * file);
    int (*read) (char * buf, int size, file_t * file);
    int (*write) (char * buf, int size, file_t * file);
    void (*close) (file_t * file);

    int (*seek) (file_t * file, uint32_t offset, int dir);
    int (*stat)(file_t * file, struct stat *st); 
    int (*ioctl) (file_t * file, int cmd, int arg0, int arg1);

    int (*opendir)(struct _fs_t * fs,const char * name, DIR * dir);
    int (*readdir)(struct _fs_t * fs, DIR* dir, struct dirent * dirent);
    int (*closedir)(struct _fs_t * fs,DIR *dir);
    int (*unlink) (struct _fs_t * fs, const char * path);
}fs_op_t;

#define FS_MOUNTP_SIZE      512

// 文件系统类型
typedef enum _fs_type_t {
    FS_FAT16,
    FS_DEVFS,
}fs_type_t;

// ***文件系统结构体***
typedef struct _fs_t {
    char mount_point[FS_MOUNTP_SIZE];  // 挂载点路径
    fs_type_t type;                    // 文件系统类型

    fs_op_t * op;                      // 文件系统操作接口
    void * data;                       // 文件系统的相关数据
    int dev_id;                        // 所属的设备
    list_node_t node;                  // 链表结点
    mutex_t * mutex;                   // 文件系统操作互斥信号量

    union {
        fat_t fat_data;                // FAT文件系统相关数据
    };
}fs_t;
#define FS_TABLE_SIZE		10		// 最大支持挂载的文件系统数量

static list_t mounted_list;			// 已挂载的文件系统链表
static list_t free_list;			// 空闲文件系统链表
static fs_t fs_tbl[FS_TABLE_SIZE];	// 文件系统列表
static fs_t * root_fs;				// 根文件系统

extern fs_op_t devfs_op;            // devfs中定义的设备文件系统
extern fs_op_t fatfs_op;            // fatfs中定义的磁盘文件系统

// ***文件系统初始化***
// ***1. 初始化挂载链表和文件描述符;2. 挂载FS_DEVFS和FS_FAT16文件系统***
void fs_init (void) {
	mount_list_init(); // 初始化挂载链表
    file_table_init(); // 初始化文件描述表

	disk_init();       // 磁盘初始化

	// 挂载设备文件系统,挂载点名称可随意,文件类型:FS_DEVFS,路径:"/dev"
	fs_t * fs = mount(FS_DEVFS, "/dev", 0, 0);
	ASSERT(fs != (fs_t *)0);

	// 挂载根文件系统
	root_fs = mount(FS_FAT16, "/home", ROOT_DEV);
	ASSERT(root_fs != (fs_t *)0);
}
// ***挂载文件系统,输入:待挂载文件系统的类型 type,挂载点路径mount_point,主设备号与次设备号***
// ***1. 从空闲文件系统链表分配文件系统结构,初始化该文件系统结构(会调用具体文件系统的挂载函数),将该文件系统加入挂载链表
static fs_t * mount (fs_type_t type, char * mount_point, int dev_major, int dev_minor) {
	fs_t * fs = (fs_t *)0;

	log_printf("mount file system, name: %s, dev: %x", mount_point, dev_major);

	// 查找传入的文件系统是否已经挂载
 	list_node_t * curr = list_first(&mounted_list);
	while (curr) {
		fs_t * fs = list_node_parent(curr, fs_t, node);
		if (kernel_strncmp(fs->mount_point, mount_point, FS_MOUNTP_SIZE) == 0) {
			log_printf("fs alreay mounted.");
			goto mount_failed;
		}
		curr = list_node_next(curr);
	}

	// 从空闲文件系统链表分配新的文件系统结构
	list_node_t * free_node = list_remove_first(&free_list);
	if (!free_node) {
		log_printf("no free fs, mount failed.");
		goto mount_failed;
	}
	fs = list_node_parent(free_node, fs_t, node); //取出分配的文件系统结构首地址

	// 根据文件系统类型type获取对应的接口操作
	fs_op_t * op = get_fs_op(type, dev_major);
	if (!op) {
		log_printf("unsupported fs type: %d", type);
		goto mount_failed;
	}

	// 给定数据一些缺省的值
	kernel_memset(fs, 0, sizeof(fs_t));
	kernel_strncpy(fs->mount_point, mount_point, FS_MOUNTP_SIZE);
	fs->op = op;
	fs->mutex = (mutex_t *)0;

	// 针对文件系统类型执行特定的挂载操作(如文件系统类型的字段的初始化)
	if (op->mount(fs, dev_major, dev_minor) < 0) {
		log_printf("mount fs %s failed", mount_point);
		goto mount_failed;
	}
	list_insert_last(&mounted_list, &fs->node); //将fs插入mounted_list
	return fs;
mount_failed:
	if (fs) {
		list_insert_first(&free_list, &fs->node); //插回空闲链表
	}
	return (fs_t *)0;
}

// ***根据文件系统的类型获取指定文件系统的操作接口
// ***返回:&fatfs_op 或者 &devfs_op 或者 (fs_op_t *)0
static fs_op_t * get_fs_op (fs_type_t type, int major) {
	switch (type) {
	case FS_FAT16:
		return &fatfs_op;
	case FS_DEVFS:
		return &devfs_op;
	default:
		return (fs_op_t *)0;
	}
}

devfs.h & devfs.c

// ***设备类型描述结构
typedef struct _devfs_type_t {
    const char * name;
    int dev_type;
    int file_type;
}devfs_type_t;
// ***设备文件系统中支持的设备(目前只支持tty)
static devfs_type_t devfs_type_list[] = {
    {
        .name = "tty",
        .dev_type = DEV_TTY, //设备类型(主设备号)
        .file_type = FILE_TTY,
    }
};

// ***设备文件系统操作接口***
fs_op_t devfs_op = {
    .mount = devfs_mount,
    .unmount = devfs_unmount,
    .open = devfs_open,
    .read = devfs_read,
    .write = devfs_write,
    .seek = devfs_seek,
    .stat = devfs_stat,
    .close = devfs_close,
    .ioctl = devfs_ioctl,
};

fatfs.c & fatfs.c

#pragma pack(1)    // 千万记得加这个

#define FAT_CLUSTER_INVALID 		    0xFFF8      	    // 无效的簇号
#define FAT_CLUSTER_FREE          	    0x00     	        // 空闲或无效的簇号

#define DIRITEM_NAME_FREE               0xE5                // 目录项空闲名标记
#define DIRITEM_NAME_END                0x00                // 目录项结束名标记

#define DIRITEM_ATTR_READ_ONLY          0x01                // 目录项属性:只读
#define DIRITEM_ATTR_HIDDEN             0x02                // 目录项属性:隐藏
#define DIRITEM_ATTR_SYSTEM             0x04                // 目录项属性:系统类型
#define DIRITEM_ATTR_VOLUME_ID          0x08                // 目录项属性:卷id
#define DIRITEM_ATTR_DIRECTORY          0x10                // 目录项属性:目录
#define DIRITEM_ATTR_ARCHIVE            0x20                // 目录项属性:归档
#define DIRITEM_ATTR_LONG_NAME          0x0F                // 目录项属性:长文件名

#define SFN_LEN                    	 	11                  // sfn文件名长

// ***FAT目录项结构体(不属于其它结构体成员)***
typedef struct _diritem_t {
    uint8_t DIR_Name[11];                   // 文件名
    uint8_t DIR_Attr;                       // 属性
    uint8_t DIR_NTRes;
    uint8_t DIR_CrtTimeTeenth;              // 创建时间的毫秒
    uint16_t DIR_CrtTime;                   // 创建时间
    uint16_t DIR_CrtDate;                   // 创建日期
    uint16_t DIR_LastAccDate;               // 最后访问日期
    uint16_t DIR_FstClusHI;                 // 簇号高16位
    uint16_t DIR_WrtTime;                   // 修改时间
    uint16_t DIR_WrtDate;                   // 修改时期
    uint16_t DIR_FstClusL0;                 // 簇号低16位
    uint32_t DIR_FileSize;                  // 文件字节大小
} diritem_t;

// ***完整的DBR(DOS 引导记录)类型(FAT文件系统的配置信息,读取后用于初始化FAT结构)***
typedef struct _dbr_t {
    uint8_t BS_jmpBoot[3];                 // 跳转代码
    uint8_t BS_OEMName[8];                 // OEM名称
    uint16_t BPB_BytsPerSec;               // 每扇区字节数
    uint8_t BPB_SecPerClus;                // 每簇扇区数
    uint16_t BPB_RsvdSecCnt;               // 保留区扇区数
    uint8_t BPB_NumFATs;                   // FAT表项数
    uint16_t BPB_RootEntCnt;               // 根目录项目数
    uint16_t BPB_TotSec16;                 // 总的扇区数
    uint8_t BPB_Media;                     // 媒体类型
    uint16_t BPB_FATSz16;                  // FAT表项大小
    uint16_t BPB_SecPerTrk;                // 每磁道扇区数
    uint16_t BPB_NumHeads;                 // 磁头数
    uint32_t BPB_HiddSec;                  // 隐藏扇区数
    uint32_t BPB_TotSec32;                 // 总的扇区数

	uint8_t BS_DrvNum;                     // 磁盘驱动器参数
	uint8_t BS_Reserved1;				   // 保留字节
	uint8_t BS_BootSig;                    // 扩展引导标记
	uint32_t BS_VolID;                     // 卷标序号
	uint8_t BS_VolLab[11];                 // 磁盘卷标
	uint8_t BS_FileSysType[8];             // 文件类型名称
} dbr_t;
#pragma pack()

// ***FAT结构(为DOS 引导记录中的有用信息,便于操作FAT分区)***
typedef struct _fat_t {
    // fat文件系统本身信息
    uint32_t tbl_start;                     // FAT表起始扇区号
    uint32_t tbl_cnt;                       // FAT表数量
    uint32_t tbl_sectors;                   // 每个FAT表的扇区数
    uint32_t bytes_per_sec;                 // 每扇区大小
    uint32_t sec_per_cluster;               // 每簇的扇区数
    uint32_t root_ent_cnt;                  // 根目录的项数
    uint32_t root_start;                    // 根目录起始扇区号
    uint32_t data_start;                    // 数据区起始扇区号
    uint32_t cluster_byte_size;             // 每簇字节数

    // 与文件系统读写相关信息
    uint8_t * fat_buffer;             		// FAT表项缓冲
    int curr_sector;                        // 当前缓存的扇区号

    struct _fs_t * fs;                      // 所在的文件系统
    mutex_t mutex;                          // 互斥信号量
} fat_t;

typedef uint16_t cluster_t; //簇表中的簇类型(两个字节)
// ***FAT文件系统操作接口***
fs_op_t fatfs_op = {
    .mount = fatfs_mount,
    .unmount = fatfs_unmount,
    .open = fatfs_open,
    .read = fatfs_read,
    .write = fatfs_write,
    .seek = fatfs_seek,
    .stat = fatfs_stat,
    .close = fatfs_close,

    .opendir = fatfs_opendir,
    .readdir = fatfs_readdir,
    .closedir = fatfs_closedir,
    .unlink = fatfs_unlink,
};

tty.h & tty.c

#define TTY_NR						8		// 最大支持的tty设备数量

// tty终端设备循环队列结构体
typedef struct _tty_fifo_t {
	char * buf;				// 缓冲区
	int size;				// 最大字节数
	int read, write;		// 当前读写位置
	int count;				// 当前已有的数据量
}tty_fifo_t;


// ***tty终端设备结构体***
typedef struct _tty_t {
	char obuf[TTY_OBUF_SIZE];       // 输出缓存池,用于初始化ofifo的buf
	tty_fifo_t ofifo;				// 输出缓存队列
	sem_t osem;                     // 用于输出的信号量
	char ibuf[TTY_IBUF_SIZE];       // 输入缓存池,用于初始化ififo的buf
	tty_fifo_t ififo;				// 输入缓存队列
	sem_t isem;						// 用于输入的信号量
	int iflags;						// 输入标志
    int oflags;						// 输出标志
	int console_idx;				// 控制台索引号
}tty_t;

int tty_fifo_get (tty_fifo_t * fifo, char * c);  // 从tty_fifo循环队列缓冲区读取一字节数据,成功返回0
int tty_fifo_put (tty_fifo_t * fifo, char c);    //向tty_fifo循环队列缓冲区写入一字节数据,成功返回0

void tty_select (int tty); // 选择当前的tty,一共支持8个tty设备
// 向tty输入缓存队列ififo写入一字符,然后通过信号量通知其等待队列上的进程有数据到达
// 该函数被键盘中断调用,每当有键盘输入,则会通知其等待队列上的进程
void tty_in (char ch);
static tty_t tty_devs[TTY_NR];
static int curr_tty = 0; //当前运行的tty设备id,一共支持8台tty设备

// ***定义并初始化一个tty类型的设备驱动接口描述符 dev_tty_desc: 描述一个tty设备所具备的特性***
dev_desc_t dev_tty_desc = {
	.name = "tty",
	.major = DEV_TTY,
	.open = tty_open,
	.read = tty_read,
	.write = tty_write,
	.control = tty_control,
	.close = tty_close,
};

disk.h  & disk.h 

#pragma pack(1)

// ***MBR(主引导记录)的分区表项类型(用于初始化)***
typedef struct _part_item_t {
    uint8_t boot_active;               // 活动分区标志(0x80表示活动分区,可引导;0表示非活动分区)
	uint8_t start_header;              // 起始header(
	uint16_t start_sector : 6;         // 起始扇区
	uint16_t start_cylinder : 10;	   // 起始磁道
	uint8_t system_id;	               // 文件系统类型
	uint8_t end_header;                // 结束header
	uint16_t end_sector : 6;           // 结束扇区
	uint16_t end_cylinder : 10;        // 结束磁道
	uint32_t relative_sectors;	       // 相对于该驱动器开始的相对扇区数
	uint32_t total_sectors;            // 总的扇区数
}part_item_t;

#define MBR_PRIMARY_PART_NR	    4   // 磁盘中的分区表项数量(4个)

// ***MBR区域描述结构(引导代码,分区表,引导标志)***
typedef  struct _mbr_t {
	uint8_t code[446];                          // 引导代码区
    part_item_t part_item[MBR_PRIMARY_PART_NR];
	uint8_t boot_sig[2];                        // 引导标志
}mbr_t;

#pragma pack()

struct _disk_t;

// ***分区结构,和磁盘分区表项对应***
typedef struct _partinfo_t {
    char name[PART_NAME_SIZE];  // 分区名称
    struct _disk_t * disk;      // 所属的磁盘

    enum {
        FS_INVALID = 0x00,      // 无效文件系统类型
        FS_FAT16_0 = 0x06,      // FAT16文件系统类型
        FS_FAT16_1 = 0x0E,
    }type;                      //分区类型

	int start_sector;           // 本分区的起始扇区
	int total_sector;           // 本分区占用的总扇区数
}partinfo_t;

// ***磁盘结构***
typedef struct _disk_t {
    char name[DISK_NAME_SIZE];                  // 磁盘名称

    enum {
        DISK_DISK_MASTER = (0 << 4),            // 主设备
        DISK_DISK_SLAVE = (1 << 4),             // 从设备
    }drive;

    uint16_t port_base;                         // 端口起始地址
    int sector_size;                            // 本磁盘的扇区大小
    int sector_count;                           // 本磁盘的扇区数量
	partinfo_t partinfo[DISK_PRIMARY_PART_CNT];	// 磁盘分区表, 包含描述整个磁盘的分区信息(通过读取磁盘上的分区表得到)
    mutex_t * mutex;                            // 访问该磁盘的互斥锁
    sem_t * op_sem;                             // 读写命令操作的同步信号量
}disk_t;

void disk_init (void);

void exception_handler_ide_primary (void);
static disk_t disk_buf[DISK_CNT];  // 磁盘列表
static mutex_t mutex;              // 磁盘互斥锁
static sem_t op_sem;               // 磁盘操作的信号量
static int task_on_op;             // 标志位 指示当前进程对磁盘进行了读写操作

// ***磁盘设备驱动接口描述符***
dev_desc_t dev_disk_desc = {
	.name = "disk",
	.major = DEV_DISK,
	.open = disk_open,
	.read = disk_read,
	.write = disk_write,
	.control = disk_control,
	.close = disk_close,
};

 七、相关图例

 618aa46f5c0942178a90993783482d44.png

八、其它

source/kernel/init/init.c 

// ***内核入口***
void kernel_init (boot_info_t * boot_info) {
    init_boot_info = boot_info;

    cpu_init();             // CPU初始化
    irq_init();             // 中断初始化
    log_init();             // 日志输出初始化

    memory_init(boot_info); // 内存初始化要放前面一点,因为后面的代码可能需要内存分配
    fs_init();              // 文件系统初始化

    time_init();            // 可编程定时器8253初始化

    task_manager_init();    //任务管理器初始化
}


void init_main(void) {
    log_printf("Kernel is running....");

    task_first_init(); //init进程初始化
    move_to_first_task(); //切换到int进程
}

// ***切换至第一个进程运行***
void move_to_first_task(void) {
    // 不能直接用Jmp far进入,因为当前特权级0,不能跳到低特权级的代码
    // 在iret后,还需要手动加载ds, fs, es等寄存器值,iret不会自动加载
    task_t * curr = task_current();
    ASSERT(curr != 0);

    tss_t * tss = &(curr->tss);

    __asm__ __volatile__(
        // 模拟中断返回,切换入第1个可运行应用进程
        // 不过这里并不直接进入到进程的入口,而是先设置好段寄存器,再跳过去
        "push %[ss]\n\t"			// SS
        "push %[esp]\n\t"			// ESP
        "push %[eflags]\n\t"        // EFLAGS
        "push %[cs]\n\t"			// CS
        "push %[eip]\n\t"		    // ip
        "iret\n\t"::[ss]"r"(tss->ss),  [esp]"r"(tss->esp), [eflags]"r"(tss->eflags),
        [cs]"r"(tss->cs), [eip]"r"(tss->eip));
}

 source/kernel/cpu/cpu.c

static segment_desc_t gdt_table[GDT_TABLE_SIZE]; //定义GDT表,全局静态变量
static mutex_t mutex; //定义互斥锁

// ***设置段描述符,输入:段选择子 selector,段基地址 base,段界限 limit,段属性 attr
// ***用段基地址 base,段界限 limit,段属性 attr初始化选择子为selector的段描述符
void segment_desc_set(int selector, uint32_t base, uint32_t limit, uint16_t attr) {
    // 获取对应的段描述符指针,即GDT表的起始地址加基于索引的偏移
    segment_desc_t * desc = gdt_table + (selector >> 3);

	// 如果界限比较长,将长度单位换成4KB
	if (limit > 0xfffff) {
		attr |= 0x8000;
		limit /= 0x1000;
	}
	desc->limit15_0 = limit & 0xffff;
	desc->base15_0 = base & 0xffff;
	desc->base23_16 = (base >> 16) & 0xff;
	desc->attr = attr | (((limit >> 16) & 0xf) << 8);
	desc->base31_24 = (base >> 24) & 0xff;
}

// ***设置门描述符,输入:门描述符指针 * desc,目标代码段的选择子 selector、偏移量 offset,相关属性 attr
// ***用目标代码段的选择子 selector、偏移量 offset,相关属性 attr 初始化 *desc指向的门描述符
void gate_desc_set(gate_desc_t * desc, uint16_t selector, uint32_t offset, uint16_t attr) {
	desc->offset15_0 = offset & 0xffff;
	desc->selector = selector;
	desc->attr = attr;
	desc->offset31_16 = (offset >> 16) & 0xffff;
}

// ***释放选择子 sel 指向的GDT描述符***
void gdt_free_sel (int sel) {
    mutex_lock(&mutex);
    gdt_table[sel / sizeof(segment_desc_t)].attr = 0;
    mutex_unlock(&mutex);
}

// ***分配一个GDT描述符,成功则返回:新GDT描述符相对GDT表基址的偏移量(以字节为单位)***
int gdt_alloc_desc (void) {
    int i;
    // 跳过第0项
    mutex_lock(&mutex);
    for (i = 1; i < GDT_TABLE_SIZE; i++) {
        segment_desc_t * desc = gdt_table + i;
        if (desc->attr == 0) {
            desc->attr = SEG_P_PRESENT;     // 标记为占用状态
            break;
        }
    }
    mutex_unlock(&mutex);
    return i >= GDT_TABLE_SIZE ? -1 : i * sizeof(segment_desc_t);
}

// ***初始化GDT表***
// ***1. 将表中所有段描述符清零;2. 设置一个数据段描述符(特权级0);3. 设置一个代码段(非一致,特权级0)描述符;***
// ***4. 为系统调用设置一个调用门描述符(特权级3);5. 加载GDT到GDTR寄存器***
void init_gdt(void) {
	// 全部清空
    for (int i = 0; i < GDT_TABLE_SIZE; i++) {
        segment_desc_set(i << 3, 0, 0, 0);
    }

    //数据段
    segment_desc_set(KERNEL_SELECTOR_DS, 0x00000000, 0xFFFFFFFF,
                     SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_DATA
                     | SEG_TYPE_RW | SEG_D | SEG_G);

    // 只能用非一致代码段,以便通过调用门更改当前任务的CPL执行关键的资源访问操作
    segment_desc_set(KERNEL_SELECTOR_CS, 0x00000000, 0xFFFFFFFF,
                     SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_CODE
                     | SEG_TYPE_RW | SEG_D | SEG_G);

    // 调用门
    gate_desc_set((gate_desc_t *)(gdt_table + (SELECTOR_SYSCALL >> 3)),
            KERNEL_SELECTOR_CS,
            (uint32_t)exception_handler_syscall,
            GATE_P_PRESENT | GATE_DPL3 | GATE_TYPE_SYSCALL | SYSCALL_PARAM_COUNT);

    // 加载gdt
    lgdt((uint32_t)gdt_table, sizeof(gdt_table));
}

// ***切换至TSS,即跳转实现任务切换
void switch_to_tss (uint32_t tss_selector) {
    far_jump(tss_selector, 0);
}

// ***CPU初始化***
// ***1. 初始化互斥锁***
// ***2. 初始化GDT表(初始化代码段、数据段和调用门三个描述符,然后加载GDT到GDTR寄存器中)***
void cpu_init (void) {
    
    mutex_init(&mutex);
    init_gdt();
}

source/kernel/include/cpu/irq.h & irq.c 

typedef struct _exception_frame_t {
    // 结合压栈的过程,以及pusha指令的实际压入过程
    int gs, fs, es, ds;
    int edi, esi, ebp, esp, ebx, edx, ecx, eax;
    int num;
    int error_code;
    int eip, cs, eflags;
    int esp3, ss3;
}exception_frame_t;

typedef void(*irq_handler_t)(void); //中断处理函数指针
#define IDT_TABLE_NR			128			 // IDT表项数量

static gate_desc_t idt_table[IDT_TABLE_NR];	 // 中断描述表


void do_handler_unknown (exception_frame_t * frame) {
	do_default_handler(frame, "Unknown exception.");
}
...

// ***安装中断处理程序,输入:中断号 irq_num,中断处理程序函数指针 handler
// ***将中断号为irq_num的中断描述符的处理程序设置为  handler
int irq_install(int irq_num, irq_handler_t handler) {
	if (irq_num >= IDT_TABLE_NR) {
		return -1;
	}

    gate_desc_set(idt_table + irq_num, KERNEL_SELECTOR_CS, (uint32_t) handler,
                  GATE_P_PRESENT | GATE_DPL0 | GATE_TYPE_IDT);
	return 0;
}
...

// ***中断和异常初始化***
// ***1. 初始化中断描述符表,即分别初始化其中128个表项(特权级0);2. 基于给定的中断向量号安装中断处理程序***
// ***3. 将中断描述符表加载到IDTR寄存器;4. 初始化pic 控制器***
void irq_init(void) {
    // 初始化中断描述符表,即分别初始化其中128个表项(特权级0)
	for (uint32_t i = 0; i < IDT_TABLE_NR; i++) {
    	gate_desc_set(idt_table + i, KERNEL_SELECTOR_CS, (uint32_t) exception_handler_unknown,
                  GATE_P_PRESENT | GATE_DPL0 | GATE_TYPE_IDT);
	}

	// 安装异常处理接口
    irq_install(IRQ0_DE, exception_handler_divider);
	...
	irq_install(IRQ20_VE, exception_handler_virtual_exception);

	// 将中断描述符表加载到IDTR寄存器
	lidt((uint32_t)idt_table, sizeof(idt_table));

	//初始化可编程中断控制器8259A(中断代理)
	init_pic();
}

 source/kernel/core/memory.h & memory.c

// ***地址分配结构***
typedef struct _addr_alloc_t {
    mutex_t mutex;              // 地址分配的互斥锁
    bitmap_t bitmap;            // 辅助分配用的位图

    uint32_t page_size;         // 页大小,4kb
    uint32_t start;             // 能够管理的内存区域的起始地址,1M
    uint32_t size;              // 能够分配的存储空间的总大小
}addr_alloc_t;

// ***虚拟地址到物理地址之间的映射关系表
typedef struct _memory_map_t {
    void * vstart;     // 虚拟地址
    void * vend;
    void * pstart;     // 物理地址
    uint32_t perm;     // 访问权限
}memory_map_t;
// *** 将虚拟页映射到物理页
// *** page_dir 页目录表指针,vaddr 虚拟起始地址,paddr 需要映射到的物理起始地址,count 映射页数,perm 映射权限
int memory_create_map (pde_t * page_dir, uint32_t vaddr, uint32_t paddr, int count, uint32_t perm) {
    for (int i = 0; i < count; i++) {
		// 根据页目录起始地址和虚拟地址vaddr,找到对应的二级页表项(1表示不存在则进行分配)
		// 放回对应二级页表项的起始地址 pte(4k对齐)
        pte_t * pte = find_pte(page_dir, vaddr, 1);
        if (pte == (pte_t *)0) {
            return -1;
        }
        // 创建映射的时候,这条二级页表项 pte应当是不存在的。
        // 如果存在,说明可能有问题
        ASSERT(pte->present == 0);
        //设置二级页表项,将此页映射到物理地址 paddr处,映射权限为 perm(代码段,数据段),存在位 置1
        pte->v = paddr | perm | PTE_P;

        vaddr += MEM_PAGE_SIZE; //虚拟地址递增
        paddr += MEM_PAGE_SIZE; //物理地址递增,如果vaddr=paddr则是创建恒等映射
    }

    return 0;
}

// ***根据内存映射表,构造内核页表***
void create_kernel_table (void) {
    // 声明了一组外部变量,这些变量表示不同的内核段的起始和结束地址,采用数组形式是为了能够直接取到地址
    extern uint8_t s_text[], e_text[], s_data[], e_data[];
    extern uint8_t kernel_base[];

    // 地址映射表, 用于建立内核级的地址映射
    // 地址不变,但是添加了属性, 虚拟起始地址,虚拟结束地址,物理起始地址,访问权限
    static memory_map_t kernel_map[] = {
        {kernel_base,   s_text,         0,              PTE_W},         // 内核栈区
        {s_text,        e_text,         s_text,         0},         // 内核代码区
        {s_data,        (void *)(MEM_EBDA_START - 1),   s_data,        PTE_W},      // 内核数据区
        {(void *)CONSOLE_DISP_ADDR, (void *)(CONSOLE_DISP_END - 1), (void *)CONSOLE_VIDEO_BASE, PTE_W},

        // 扩展存储空间一一映射,方便直接操作,这里只进行了映射,没有分配实际的物理内存
		// MEM_EXT_START 为 1M,先将1M以上的虚拟地址恒等映射到物理地址, MEM_EXT_END为128M
        {(void *)MEM_EXT_START, (void *)MEM_EXT_END,     (void *)MEM_EXT_START, PTE_W},
    };

    // 清空页目录表
    kernel_memset(kernel_page_dir, 0, sizeof(kernel_page_dir));

    // 清空后,然后依次根据映射关系创建映射表
    for (int i = 0; i < sizeof(kernel_map) / sizeof(memory_map_t); i++) {
        memory_map_t * map = kernel_map + i;

        // 可能有多个页,建立多个页的配置
        int vstart = down2((uint32_t)map->vstart, MEM_PAGE_SIZE);
        int vend = up2((uint32_t)map->vend, MEM_PAGE_SIZE);
        // 注意,s_data 地址需要4kb对齐,这需要在链接脚本中进行设置,如果不进行页边界对齐
		// 如果在这将 s_text 和 s_data进行4kb对齐,则可能这两个段会重叠,而造成页的重复映射
        int page_count = (vend - vstart) / MEM_PAGE_SIZE;

		// 将vstart的地址空间映射到 pstart的位置,映射的page_count页,访问权限设置为 perm
		// 内核页目录表 kernel_page_dir一共有1024个页目录项
        memory_create_map(kernel_page_dir, vstart, (uint32_t)map->pstart, page_count, map->perm);
    }
}

// *** 为进程创建一个页目录表***
// *** 1. 在物理内存中为进程分配一个页目录表(4kb);
// *** 2. 将虚拟地址0x80000000以下的页目录项设置为和为内核页目录表 kernel_page_dir的一样***
// *** 3.  用户空间的内存映射暂不处理,等加载程序时创建***
uint32_t memory_create_uvm (void) {
    pde_t * page_dir = (pde_t *)addr_alloc_page(&paddr_alloc, 1);
    if (page_dir == 0) {
        return 0;
    }
    kernel_memset((void *)page_dir, 0, MEM_PAGE_SIZE);

    // 复制整个内核空间的页目录项,以便与其它进程共享内核空间
    // 用户空间的内存映射暂不处理,等加载程序时创建
    uint32_t user_pde_start = pde_index(MEMORY_TASK_BASE);
    for (int i = 0; i < user_pde_start; i++) {
        page_dir[i].v = kernel_page_dir[i].v;
    }

    return (uint32_t)page_dir;
}

// ***根据父页目录表复制其所有的内存空间***
// ***1. 创建一个新页目录表(需分配4k空间);2. 内核空间的映射,所有进程都一样,不需要额外开辟空间(只有一份物理存储,相当于浅拷贝)
// ***3. 拷贝父页目录表中用户空间所映射的内容,需要独立创建相应的二级页表以及分配物理空间,
// ******新页目录表和父页目录表映射的物理内存中存储的内容一样,不过物理内存是独立的(相当于深拷贝)
uint32_t memory_copy_uvm (uint32_t page_dir) {
    // 复制基础页表
    uint32_t to_page_dir = memory_create_uvm();
    if (to_page_dir == 0) {
        goto copy_uvm_failed;
    }
    // 再复制用户空间的各项
    uint32_t user_pde_start = pde_index(MEMORY_TASK_BASE);
    pde_t * pde = (pde_t *)page_dir + user_pde_start;

    // 遍历用户空间页目录项
    for (int i = user_pde_start; i < PDE_CNT; i++, pde++) {
        if (!pde->present) {
            continue;
        }
        // 遍历页表
        pte_t * pte = (pte_t *)pde_paddr(pde);
        for (int j = 0; j < PTE_CNT; j++, pte++) {
            if (!pte->present) {
                continue;
            }
            // 分配物理内存
            uint32_t page = addr_alloc_page(&paddr_alloc, 1);
            if (page == 0) {
                goto copy_uvm_failed;
            }
            // 建立映射关系
            uint32_t vaddr = (i << 22) | (j << 12);
            int err = memory_create_map((pde_t *)to_page_dir, vaddr, page, 1, get_pte_perm(pte));
            if (err < 0) {
                goto copy_uvm_failed;
            }
            // 复制内容
            kernel_memcpy((void *)page, (void *)vaddr, MEM_PAGE_SIZE);
        }
    }
    return to_page_dir;

copy_uvm_failed:
    if (to_page_dir) {
        memory_destroy_uvm(to_page_dir);
    }
    return -1;
}

// *** 初始化内存管理系统
 // *** 1、初始化物理内存分配器:将所有物理内存管理起来,在1MB内存中分配物理位图
 // *** 2、创建内核页表(原loader中创建的页表已经不再合适),在0 - 128M内存范围建立虚拟内存与物理内存的恒等映射
 // *** 3、切换到当前页表,将内核页目录表kernel_page_dir写入到cr3
void memory_init (boot_info_t * boot_info) {
     // 1MB内存空间空闲区域的起始地址,在链接脚本中定义
    extern uint8_t * mem_free_start;

    log_printf("mem init.");
    show_mem_info(boot_info);

    // 在内核数据后面放物理页位图
	// 1MB内存空间空闲区域的起始地址,在链接脚本中定义
    uint8_t * mem_free = (uint8_t *)&mem_free_start; 

    // 计算1MB以上空间的空闲内存容量,并对齐的页边界
    uint32_t mem_up1MB_free = total_mem_size(boot_info) - MEM_EXT_START;
    mem_up1MB_free = down2(mem_up1MB_free, MEM_PAGE_SIZE);    // 向下对齐到4KB页
    log_printf("Free memory: 0x%x, size: 0x%x", MEM_EXT_START, mem_up1MB_free);

    // 4GB大小需要总共4*1024*1024*1024/4096/8=128KB的位图, 使用低1MB的RAM空间中足够
    // 该部分的内存仅跟在mem_free_start开始放置
    addr_alloc_init(&paddr_alloc, mem_free, MEM_EXT_START, mem_up1MB_free, MEM_PAGE_SIZE);
    mem_free += bitmap_byte_count(paddr_alloc.size / MEM_PAGE_SIZE);

    // 到这里,mem_free应该比EBDA地址要小
    // MEM_EBDA_START为0x80000,为显存等保留区域,不可用,检查mem_free是否已经覆盖该区域,覆盖便报错
    ASSERT(mem_free < (uint8_t *)MEM_EBDA_START);

    // 创建内核页表并切换过去
    create_kernel_table();

    // 切换到当前页表,将内核页目录表kernel_page_dir写入到cr3(页目录基址寄存器)
    mmu_set_page_dir((uint32_t)kernel_page_dir);
}

 source/kernel/core/task.c

static task_manager_t task_manager;                  // 任务管理器
static task_t task_table[TASK_NR];                   // 用户进程表
static mutex_t task_table_mutex;                     // 进程表互斥访问锁

// ***任务状态段初始化,输入:指向任务PCB的指针 *task、flag代表是内核任务(1)还是应用任务(0)、任务入口 entry 、任务栈的栈顶指针esp***
// ***1. 为TSS分配一个GDT描述符(特权级设置为0,每个任务都有一个独立的tss,tss在pcb结构体中);
// ***2. 分配内核栈(每个任务都有一个独立的内核栈),即0特权级栈 ss0,esp0,(得到的是物理地址)
// ***3. 根据flag为数据段和代码段选择不同的段选择子(内核段或应用段);4. 初始化tss中的各字段
static int tss_init (task_t * task, int flag, uint32_t entry, uint32_t esp) {
    // 为TSS分配一个GDT描述符(特权级设置为0)
    int tss_sel = gdt_alloc_desc();
    if (tss_sel < 0) {
        log_printf("alloc tss failed.\n");
        return -1;
    }

    segment_desc_set(tss_sel, (uint32_t)&task->tss, sizeof(tss_t),
            SEG_P_PRESENT | SEG_DPL0 | SEG_TYPE_TSS);

    // tss段初始化
    kernel_memset(&task->tss, 0, sizeof(tss_t));

    // 分配4kb的(一页)内核栈(得到内核栈顶界限的起始物理地址)
    uint32_t kernel_stack = memory_alloc_page();
    if (kernel_stack == 0) {
        goto tss_init_failed;
    }
    
    // 根据不同的权限选择不同的访问选择子
    int code_sel, data_sel;
    if (flag & TASK_FLAG_SYSTEM) {
        code_sel = KERNEL_SELECTOR_CS;
        data_sel = KERNEL_SELECTOR_DS;
    } else {
        // 注意加了RP3,不然将产生段保护错误
        code_sel = task_manager.app_code_sel | SEG_RPL3;
        data_sel = task_manager.app_data_sel | SEG_RPL3;
    }

    task->tss.eip = entry;
    task->tss.cs = code_sel; 
    task->tss.ss0 = KERNEL_SELECTOR_DS;
    task->tss.esp0 = kernel_stack + MEM_PAGE_SIZE;

    task->tss.esp = esp ? esp : kernel_stack + MEM_PAGE_SIZE;  // 未指定栈则用内核栈,即运行在特权级0的进程
    task->tss.eflags = EFLAGS_DEFAULT| EFLAGS_IF;
    task->tss.es = task->tss.ss = task->tss.ds = task->tss.fs 
            = task->tss.gs = data_sel;   // 全部采用同一数据段
    task->tss.iomap = 0;

    // 页表初始化
    uint32_t page_dir = memory_create_uvm();
    if (page_dir == 0) {
        goto tss_init_failed;
    }
    task->tss.cr3 = page_dir;

    task->tss_sel = tss_sel;
    return 0;
tss_init_failed:
    gdt_free_sel(tss_sel);

    if (kernel_stack) {
        memory_free_page(kernel_stack);
    }
    return -1;
}

// ***初始化任务,输入:指向任务PCB的指针 *task、flag代表是内核任务还是应用任务、任务名称 name, 任务入口 entry,任务的栈顶指针esp***
// ***1.初始化任务状态段tss(包含于PCB中);2. 初始化任务PCB中的各字段;3. 将任务加入包含全部任务的任务队列
int task_init (task_t *task, const char * name, int flag, uint32_t entry, uint32_t esp) {
    ASSERT(task != (task_t *)0);

    int err = tss_init(task, flag, entry, esp);
    if (err < 0) {
        log_printf("init task failed.\n");
        return err;
    }

    // 任务字段初始化
    kernel_strncpy(task->name, name, TASK_NAME_SIZE);
    task->state = TASK_CREATED;
    task->sleep_ticks = 0;
    task->time_slice = TASK_TIME_SLICE_DEFAULT;
    task->slice_ticks = task->time_slice;
    task->parent = (task_t *)0;
    task->heap_start = 0;
    task->heap_end = 0;
    list_node_init(&task->all_node);
    list_node_init(&task->run_node);
    list_node_init(&task->wait_node);

    // 文件相关
    kernel_memset(task->file_table, 0, sizeof(task->file_table));

    // 插入所有的任务队列中
    irq_state_t state = irq_enter_protection();
    task->pid = (uint32_t)task;   // 使用地址,能唯一
    list_insert_last(&task_manager.task_list, &task->all_node);
    irq_leave_protection(state);
    return 0;
}

/** init进程的初始化,init进程是单独编译,将其和内核连接为了一个elf文件,加载时将其加载到用户空间运行
 * 没有采用从磁盘加载的方式,因为需要用到文件系统,并且最好是和kernel绑在一定,这样好加载。
 * 当然,也可以采用将init的源文件和kernel的一起编译。此里要调整好kernel.lds,在其中将init加载地址设置成和内核一起的,运行地址设置成用户进程运行的高处
 * 不过,考虑到init可能用到newlib库,如果与kernel合并编译,在lds中很难控制将newlib的,代码与init进程的放在一起,有可能与kernel放在了一起
 * 综上,最好是分离
 */
void task_first_init (void) {
    void first_task_entry (void);

    // 以下获得的是bin文件在内存中的物理地址
    extern uint8_t s_first_task[], e_first_task[];

    // 分配的空间比实际存储的空间要大一些,多余的用于放置栈
    uint32_t copy_size = (uint32_t)(e_first_task - s_first_task);
    uint32_t alloc_size = 10 * MEM_PAGE_SIZE;
    ASSERT(copy_size < alloc_size);

    uint32_t first_start = (uint32_t)first_task_entry;

    // 第一个任务代码量小一些,好和栈放在1个页面
    // 这样就不要立即考虑还要给栈分配空间的问题
    task_init(&task_manager.first_task, "first task", 0, first_start, first_start + alloc_size);
    task_manager.first_task.heap_start = (uint32_t)e_first_task; 
    task_manager.first_task.heap_end = task_manager.first_task.heap_start;
    task_manager.curr_task = &task_manager.first_task;

    // 更新页表地址为自己的
    mmu_set_page_dir(task_manager.first_task.tss.cr3);

    // 分配一页内存供代码存放使用,然后将代码复制过去
    memory_alloc_page_for(first_start,  alloc_size, PTE_P | PTE_W | PTE_U);
    kernel_memcpy((void *)first_start, (void *)&s_first_task, copy_size);

    // 启动进程
    task_start(&task_manager.first_task);

    // 写TR寄存器,指示当前运行的第一个任务
    write_tr(task_manager.first_task.tss_sel);
}

// ***任务管理器初始化***
// 1. 清空用户进程表(表项为PCB);2.分配并初始化用户进程数据段和代码段描述符(所有用户进程共用相同的数据段和代码段描述符);
// 3. 初始化管理器中的就绪、睡眠和总体队列;4. 初始化空闲任务(内核任务,运行在特权级0);5. 将当前任务切换为空闲任务
void task_manager_init (void) {
    kernel_memset(task_table, 0, sizeof(task_table));
    mutex_init(&task_table_mutex);

    // 数据段和代码段,使用DPL3,所有用户进程共用同一个
    int sel = gdt_alloc_desc();
    segment_desc_set(sel, 0x00000000, 0xFFFFFFFF,
                     SEG_P_PRESENT | SEG_DPL3 | SEG_S_NORMAL |
                     SEG_TYPE_DATA | SEG_TYPE_RW | SEG_D);
    task_manager.app_data_sel = sel;

    sel = gdt_alloc_desc();
    segment_desc_set(sel, 0x00000000, 0xFFFFFFFF,
                     SEG_P_PRESENT | SEG_DPL3 | SEG_S_NORMAL |
                     SEG_TYPE_CODE | SEG_TYPE_RW | SEG_D);
    task_manager.app_code_sel = sel;

    // 各队列初始化
    list_init(&task_manager.ready_list);
    list_init(&task_manager.task_list);
    list_init(&task_manager.sleep_list);

    // 空闲任务初始化,内核任务,运行在特权级0,无需指定特权级3的栈
    task_init(&task_manager.idle_task,
                "idle task",
                TASK_FLAG_SYSTEM,
                (uint32_t)idle_task_entry,
                0); 
    task_manager.curr_task = (task_t *)0;
    task_start(&task_manager.idle_task);
}

syscall

/ ***创建进程的副本***
// ***1. 从用户进程表中为子进程分配一个任务结构(PCB);2. 初始化子进程的PCB(tss等字段,一部分拷贝父进程的)***
// ***3. 复制父进程的内存空间到子进程;4. 将子进程加入就绪队列并返回子进程的pid
int sys_fork (void) {
    task_t * parent_task = task_current();

    // 分配任务结构
    task_t * child_task = alloc_task();
    if (child_task == (task_t *)0) {
        goto fork_failed;
    }

    syscall_frame_t * frame = (syscall_frame_t *)(parent_task->tss.esp0 - sizeof(syscall_frame_t)); 

    // 对子进程进行初始化,并对必要的字段进行调整
    // 其中esp要减去系统调用的总参数字节大小,因为其是通过正常的ret返回, 而没有走系统调用处理的ret(参数个数返回)
    int err = task_init(child_task,  parent_task->name, 0, frame->eip, 
                        frame->esp + sizeof(uint32_t)*SYSCALL_PARAM_COUNT);
    if (err < 0) {
        goto fork_failed;
    }

    // 拷贝打开的文件
    copy_opened_files(child_task);

    // 从父进程的栈中取部分状态,然后写入tss。
    // 注意检查esp, eip等是否在用户空间范围内,不然会造成page_fault
    tss_t * tss = &child_task->tss;
    tss->eax = 0; // 子进程返回0
    tss->ebx = frame->ebx;
    tss->ecx = frame->ecx;
    tss->edx = frame->edx;
    tss->esi = frame->esi;
    tss->edi = frame->edi;
    tss->ebp = frame->ebp;

    tss->cs = frame->cs;
    tss->ds = frame->ds;
    tss->es = frame->es;
    tss->fs = frame->fs;
    tss->gs = frame->gs;
    tss->eflags = frame->eflags;

    child_task->parent = parent_task;

    // 复制父进程的内存空间到子进程
    if ((child_task->tss.cr3 = memory_copy_uvm(parent_task->tss.cr3)) < 0) {
        goto fork_failed;
    }

    // 创建成功,将子进程加入就绪队列并返回子进程的pid
    task_start(child_task);
    return child_task->pid;
fork_failed:
    if (child_task) {
        task_uninit (child_task);
        free_task(child_task);
    }
    return -1;
}
// *** 调整堆的内存分配,返回堆之前的指针***
// *** 需要分配的字节数(向上增长)***
char * sys_sbrk(int incr) {
    task_t * task = task_current();
    char * pre_heap_end = (char * )task->heap_end;
    int pre_incr = incr;

    ASSERT(incr >= 0);

    // 如果地址为0,则返回有效的heap区域的顶端
    if (incr == 0) {
        log_printf("sbrk(0): end = 0x%x", pre_heap_end);
        return pre_heap_end;
    } 
    
    uint32_t start = task->heap_end;// 获取当前堆增长的起始地址
    uint32_t end = start + incr; //计数增长后的末端地址

    // 起始偏移非0
    int start_offset = start % MEM_PAGE_SIZE;
    if (start_offset) { //如果当前分配的起始地址不是页边界对齐
        // 不超过1页,只调整
        if (start_offset + incr <= MEM_PAGE_SIZE) { //判断是否超过一页
            task->heap_end = end;
            return pre_heap_end;
        } else {
            // 超过1页,先只调本页的
            uint32_t curr_size = MEM_PAGE_SIZE - start_offset;
            start += curr_size;
            incr -= curr_size;
        }
    }
    // 处理其余的,起始对齐的页边界的
    if (incr) {
        uint32_t curr_size = end - start;
        int err = memory_alloc_page_for(start, curr_size, PTE_P | PTE_U | PTE_W);
        if (err < 0) {
            log_printf("sbrk: alloc mem failed.");
            return (char *)-1;
        }
    }

    task->heap_end = end;
    return (char * )pre_heap_end;        
}

// *** 加载一个进程
// *** 这个比较复杂,argv/name/env都是原进程空间中的数据,execve中涉及到页表的切换
// *** 在对argv和name进行处理时,会涉及到不同进程空间中数据的传递。
int sys_execve(char *name, char **argv, char **env) {
    task_t * task = task_current();

    // 后面会切换页表,所以先处理需要从进程空间取数据的情况
    kernel_strncpy(task->name, get_file_name(name), TASK_NAME_SIZE);

    // 现在开始加载了,先准备应用页表,由于所有操作均在内核区中进行,所以可以直接先切换到新页表
    uint32_t old_page_dir = task->tss.cr3;
    uint32_t new_page_dir = memory_create_uvm();

    // 加载elf文件到内存中。要放在开启新页表之后,这样才能对相应的内存区域写
    uint32_t entry = load_elf_file(task, name, new_page_dir);    // 暂时置用task->name表示

    // 准备用户栈空间,预留环境环境及参数的空间
    uint32_t stack_top = MEM_TASK_STACK_TOP - MEM_TASK_ARG_SIZE;    // 预留一部分参数空间
    int err = memory_alloc_for_page_dir(new_page_dir,
                            MEM_TASK_STACK_TOP - MEM_TASK_STACK_SIZE,
                            MEM_TASK_STACK_SIZE, PTE_P | PTE_U | PTE_W);

    // 复制参数,写入到栈顶的后边
    int argc = strings_count(argv);
    err = copy_args((char *)stack_top, new_page_dir, argc, argv);

    // 加载完毕,为程序的执行做必要准备
    // 注意,exec的作用是替换掉当前进程,所以只要改变当前进程的执行流即可
    // 当该进程恢复运行时,像完全重新运行一样,所以用户栈要设置成初始模式
    // 运行地址要设备成整个程序的入口地址
    syscall_frame_t * frame = (syscall_frame_t *)(task->tss.esp0 - sizeof(syscall_frame_t));
    frame->eip = entry;
    frame->eax = frame->ebx = frame->ecx = frame->edx = 0;
    frame->esi = frame->edi = frame->ebp = 0;
    frame->eflags = EFLAGS_DEFAULT| EFLAGS_IF;  // 段寄存器无需修改

    // 内核栈不用设置,保持不变,后面调用memory_destroy_uvm并不会销毁内核栈的映射。
    // 但用户栈需要更改, 同样要加上调用门的参数压栈空间
    frame->esp = stack_top - sizeof(uint32_t)*SYSCALL_PARAM_COUNT;

    // 切换到新的页表
    task->tss.cr3 = new_page_dir;
    mmu_set_page_dir(new_page_dir);   // 切换至新的页表。由于不用访问原栈及数据,所以并无问题

    // 调整页表,切换成新的,同时释放掉之前的
    // 当前使用的是内核栈,而内核栈并未映射到进程地址空间中,所以下面的释放没有问题
    memory_destroy_uvm(old_page_dir);            // 再释放掉了原进程的内容空间

    // 当从系统调用中返回时,将切换至新进程的入口地址运行,并且进程能够获取参数
    // 注意,如果用户栈设置不当,可能导致返回后运行出现异常。可在gdb中使用nexti单步观察运行流程
    return  0;
}


// ***加载elf文件到内存中,并进行解析***
static uint32_t load_elf_file (task_t * task, const char * name, uint32_t page_dir) {
    Elf32_Ehdr elf_hdr;
    Elf32_Phdr elf_phdr;

    // 以只读方式打开
    int file = sys_open(name, 0);  
    if (file < 0) {
        log_printf("open file failed.%s", name);
        goto load_failed;
    }

    // 先读取文件头
    int cnt = sys_read(file, (char *)&elf_hdr, sizeof(Elf32_Ehdr));

    // 然后从中加载程序头,将内容拷贝到相应的位置
    uint32_t e_phoff = elf_hdr.e_phoff;
    for (int i = 0; i < elf_hdr.e_phnum; i++, e_phoff += elf_hdr.e_phentsize) {

        // 读取程序头后解析,这里不用读取到新进程的页表中,因为只是临时使用下
        cnt = sys_read(file, (char *)&elf_phdr, sizeof(Elf32_Phdr));

        // 加载当前程序头
        int err = load_phdr(file, &elf_phdr, page_dir);

        // 简单起见,不检查了,以最后的地址为bss的地址
        task->heap_start = elf_phdr.p_vaddr + elf_phdr.p_memsz;
        task->heap_end = task->heap_start;
   }
    sys_close(file);
    return elf_hdr.e_entry;
}


// ***加载一个程序头表的一个条目到内存中,输入:打开的文件描述符 file,程序头表地址phdr,为新任务创建的页目录表page_dir***
// ***1. 为该程序段分配内存,并在其页目录表中建立映射;2. 将该程序段复制到分配的内存中
static int load_phdr(int file, Elf32_Phdr * phdr, uint32_t page_dir) {
    // 生成的ELF文件要求是页边界对齐的
    ASSERT((phdr->p_vaddr & (MEM_PAGE_SIZE - 1)) == 0);

    // 分配空间
    int err = memory_alloc_for_page_dir(page_dir, phdr->p_vaddr, phdr->p_memsz, PTE_P | PTE_U | PTE_W);
    if (err < 0) {
        log_printf("no memory");
        return -1;
    }

    // 调整当前的读写位置
    if (sys_lseek(file, phdr->p_offset, 0) < 0) {
        log_printf("read file failed");
        return -1;
    }

    // 为段分配所有的内存空间.后续操作如果失败,将在上层释放
    // 简单起见,设置成可写模式,也许可考虑根据phdr->flags设置成只读
    // 因为没有找到该值的详细定义,所以没有加上
    uint32_t vaddr = phdr->p_vaddr;
    uint32_t size = phdr->p_filesz;
    while (size > 0) {
        int curr_size = (size > MEM_PAGE_SIZE) ? MEM_PAGE_SIZE : size;

        uint32_t paddr = memory_get_paddr(page_dir, vaddr);

        // 注意,这里用的页表仍然是当前的
        if (sys_read(file, (char *)paddr, curr_size) <  curr_size) {
            log_printf("read file failed");
            return -1;
        }

        size -= curr_size;
        vaddr += curr_size;
    }

    // bss区考虑由crt0和cstart自行清0,这样更简单一些
    // 如果在上边进行处理,需要考虑到有可能的跨页表填充数据,懒得写代码
    // 或者也可修改memory_alloc_for_page_dir,增加分配时清0页表,但这样开销较大
    // 所以,直接放在cstart哐crt0中直接内存填0,比较简单
    return 0;
}


// ***复制进程参数到栈中。注意argv指向的空间在另一个页表(新的页表)里***
static int copy_args (char * to, uint32_t page_dir, int argc, char **argv) {
    // 在stack_top中依次写入argc, argv指针,参数字符串
    task_args_t task_args;
    task_args.argc = argc;
    task_args.argv = (char **)(to + sizeof(task_args_t));

    // 复制各项参数, 跳过task_args和参数表
    // 各argv参数写入的内存空间
    char * dest_arg = to + sizeof(task_args_t) + sizeof(char *) * (argc + 1);   // 留出结束符
    
    // argv表
    char ** dest_argv_tb = (char **)memory_get_paddr(page_dir, (uint32_t)(to + sizeof(task_args_t)));
    ASSERT(dest_argv_tb != 0);

    for (int i = 0; i < argc; i++) {
        char * from = argv[i];

        // 不能用kernel_strcpy,因为to和argv不在一个页表里
        int len = kernel_strlen(from) + 1;   // 包含结束符
        int err = memory_copy_uvm_data((uint32_t)dest_arg, page_dir, (uint32_t)from, len);
        ASSERT(err >= 0);

        dest_argv_tb[i] = dest_arg;

        // 记录下位置后,复制的位置前移
        dest_arg += len;
    }

    // 可能存在无参的情况,此时不需要写入
    if (argc) {
        dest_argv_tb[argc] = '\0';
    }

     // 写入task_args
    return memory_copy_uvm_data((uint32_t)to, page_dir, (uint32_t)&task_args, sizeof(task_args_t));
}

// ***在不同的进程空间中拷贝字符串,page_dir为目标页表,当前仍为老页表
int memory_copy_uvm_data(uint32_t to, uint32_t page_dir, uint32_t from, uint32_t size) {
    char *buf, *pa0;

    while(size > 0){
        // 获取输入的虚拟地址 to 的物理地址
        uint32_t to_paddr = memory_get_paddr(page_dir, to);
        if (to_paddr == 0) {
            return -1;
        }

        // 计算当前可拷贝的大小
        uint32_t offset_in_page = to_paddr & (MEM_PAGE_SIZE - 1);
        uint32_t curr_size = MEM_PAGE_SIZE - offset_in_page;
        if (curr_size > size) {
            curr_size = size;       // 如果比较大,超过页边界,则只拷贝此页内的
        }

        kernel_memcpy((void *)to_paddr, (void *)from, curr_size);

        size -= curr_size;
        to += curr_size;
        from += curr_size;
  }
  return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值