简单进程调度

目录

实验要求

一、采用 nasm 汇编语言实现一个实验性质的 OS 内核并部署到 MBR,主要功能如下:

二、采用 nasm 汇编与 C 语言混合编程,实现一个 API 库

三、采用 C 语言实现一个示范应用,并部署到磁盘

实验环境

实验原理

实验过程

在 kernel 中完成 write 函数

在 kernel 中完成 sleep 函数

在 kernel 中修改 int 80h

完成 C 语言头文件命名为 myos.h

在 api 中写入两个函数的具体调用 (传入 ax 的功能号,调用 int 80)

完成 C 语言测试程序,在 makefile 中进行统一地编译和链接,在 qemu 虚拟机运行

加载磁盘

实验结果

总结与思考

参考文献

流程图

实验要求

本次实验分为 OS 内核、API 库、示范应用三个部分,为综合性实验项目(建议合理构造代码目录结 构,采用 makefile 组织和管理整个实验项目的编译、部署和运行):

一、采用 nasm 汇编语言实现一个实验性质的 OS 内核并部署到 MBR,主要功能如下:

1)实现 write 内核函数,向屏幕输出指定长度的字符串(尽量保证每次输出另起一行);

2)实现 sleep 内核函数,按整型参数以秒为单位延迟(以 PA3 为基础实现,包括设置定时器和时钟 中断处理例程);

3)以系统调用的形式向外提供 write 和 sleep 两个系统服务(建议使用 int 80h,也可分别以不同的 用户自定义中断实现不同的系统服务,提供系统调用中断处理例程);

4)内核完成初始化后,从 1 号逻辑扇区开始加载示范性应用程序并运行(期间当应用进程 sleep 时, 需切换至内核的无限循环处执行,当应用进程 sleep 结束时,需切换回应用进程执行)。

二、采用 nasm 汇编与 C 语言混合编程,实现一个 API 库

1)提供一个包含 write 和 sleep 函数原型的 C 语言头文件 (建议名称 myos.h);

2)用 nasm 汇编语言实现对应的 API 库(封装 OS 内核提供的两个系统调用服务,注意在此处妥善 处理示范应用 32 位指令与 OS 内核 16 位指令的对接!!!)。

三、采用 C 语言实现一个示范应用,并部署到磁盘

1)采用 C 语言编写示范应用,用于测试 OS 内核功能,示范应用大致内容如下图所示(仅供参考!!!);

2)采用 GCC 编译生成 ELF32 格式代码,并用 objcopy 等工具提取出相应的代码段和数据段,最后 装入 1 号逻辑扇区开始的连续磁盘空间(由示范应用的大小确定)。

实验环境

操作系统:Windows 11

编程语言:汇编与 C 语言

使用工具:qemu, vscode Hex Editor

虚拟系统版本:Windows 10,linux-20-desktop

实验原理

第一部分: 采用 nasm 汇编语言实现一个实验性质的 OS 内核并部署到 MBR 实模式下,向屏幕输出字符涉及直接写入显存,使用 ASCII 字符编码及其属性信息,并通过控制光 标位置实现换行。清屏操作可以提升输出的可读性。 通过 8253/8254 硬件定时器和修改 int 8h 中断来实现程序中的延迟功能。应用进程执行 sleep 时,操 作系统会保存当前进程的状态(如寄存器、栈指针),然后切换到其他进程或内核线程执行。sleep 结束后, 通过恢复保存的上下文,实现回切到原进程继续执行,这是操作系统管理进程和实现并发的基础。 系统调用允许用户态程序请求操作系统服务,通过 int 80h 触发,操作系统根据中断号执行相应服务, 并在完成时返回用户态。此过程涉及复杂的系统交互,如栈切换、参数传递和错误处理。 从磁盘读取数据到内存通常利用 BIOS 中断或直接硬件控制,对于启动过程特别重要,如加载 1 号逻 辑扇区的数据,正确设置加载地址是保证程序正确执行的前提。

第二部分:采用 nasm 汇编与 C 语言混合编程,实现一个 API 库 API 库的汇编实现需要正确地设置系统调用的参数。这涉及将 C 语言函数调用时传递的参数转移到 适当的寄存器或栈上,确保它们符合操作系统规定的系统调用参数传递规则。执行系统调用后,可能还需 要从寄存器中获取并处理返回值。

第三部分:采用 C 语言实现一个示范应用,并部署到磁盘 GCC 将源代码编译成目标代码,生成 ELF32 格式的可执行文件。ELF 是一种广泛使用的文件格式, 它包含了程序的代码、数据、动态链接信息等,是操作系统理解和加载程序的标准方式。 使用 objcopy 等工具,可以从 ELF 文件中提取出代码段和数据段。去除 ELF 文件中的其他元数据和 辅助信息,以便适合特定目的的部署,如制作引导扇区。 将提取出的代码段和数据段写入到磁盘的 1 号逻辑扇区开始的连续空间。1 号逻辑扇区通常是操作系 统启动流程中的首个读取位置,因此将程序放于此处是为了让计算机在启动时能够自动执行这段代码。这 一步骤对于创建自定义引导加载器、操作系统或实现低级系统编程任务至关重要。

实验过程

实验步骤主要分为以下几个步骤:

1. 在 kernel 中完成 write 函数,

2. 在 kernel 中完成 sleep 函数,定时器在专栏中的另一篇文章中有所实现,修改 int 8h 中断

3. 在 kernel 中修改 int 80h,并根据 ax 的功能号判断是执行 write 还是 sleep

4. 完成 C 语言头文件,命名为 myos.h

5. 在 api 中写入两个函数的具体调用 (传入 ax 的功能号,调用 int 80)

6. 完成 C 语言测试程序,在 makefile 中进行统一地编译和链接,在 qemu 虚拟机运行。

在 kernel 中完成 write 函数

初始化段寄存器和堆栈, 设置段寄存器(DS, ES, SS)为 0,以指向实模式下的内存低端。首先将堆栈 指针(SP)设置到 0x9000,用于存储一些初始化信息,如重复执行点的栈指针,并将 SP 移动到 0x2000, 作为用户栈的起点。

再进行清屏,通过 BIOS 中断 0x10 调用,设置了屏幕的清屏操作和光标位置的重置。这里利用了 BIOS 中断的功能号 0x06(设置光标位置)和 0x10(设置视频模式和属性)。定义了一个简单的 write_function, 用于向屏幕输出字符串。它使用 BIOS 中断 0x10 的功能号 0xE 来逐字符打印字符串,直到遇到字符串结 尾。在打印完一行后,还会调整光标位置到下一行。

[ BITS 16]
[ ORG 0 x7C00 ]
TIMER_FREQ equ 1193182
TARGET_FREQ equ 50
DIVISOR equ TIMER_FREQ / TARGET_FREQ
; sp 先 初 始 化 指 向 repeat 的 栈
mov sp ,0 x9000
pushf
push 0 x0000
push repeat
mov [ repeat_stack_pos ] ,sp
; 当 前 是 在 0x2000 的 用 户 栈 上 做 的
mov sp ,0 x2000
xor ax ,ax
mov ss ,ax
mov es ,ax
mov ds ,ax
clear : ; 清 屏
mov eax , 0 x600 ; ah 中 输 入 功 能 号
mov ebx , 0 x700 ; 设 置 上 卷 行 属 性 , 0x70 表 示 用 黑 底 白 字 的 属 性 填 充 空 白 行
mov ecx , 0 ; 左 上 角 : (0 , 0)
mov edx , 0 x184f ; 右 下 角 : (80 ,25)
int 0 x10 ; int 0 x10
mov ah , 2 ; 输 入 : 2 号 子 功 能 是 设 置 光 标 位 置 , 需 要 存 入 ah 寄 存 器
mov bh , 0 ; bh 寄 存 器 存 储 的 是 待 设 置 光 标 的 页 号
mov dh , 0 ; dh 寄 存 器 存 储 的 是 行 号
mov dl , 0 ; dl 寄 存 器 存 储 的 是 列 号
int 0 x10 ; 输 出 : 无
write_function :
mov ah ,0 x0E
write_loop :
lodsb
int 0 x10 ; 调 用 BIOS 中 断 打 印 字 符
loop write_loop
write_done :
mov ah ,0 x03
mov bh ,0 x00
int 0 x10
inc dh
mov ah ,0 x02
xor dl ,dl
int 0 x10
iret

在 kernel 中完成 sleep 函数

实现一个简易的 sleep 功能,通过控制硬件定时器(8253/8254 PIT)来实现延时,并通过修改中断处 理例程来计数,直到达到预定的延时时间后恢复程序执行。这在另外文章中有实现,在此不赘述。

set_sleep : ; 初 始 化 int 8h 中 断
cli ; 关 中 断
mov ax , 0
mov es , ax
mov ax , handle_interrupt_assembly
mov [es :0 x8 *4] , ax ; 0 x1C 处 为 向 量 表 中 定 时 器 有 关 中 断 处 理 例 程 的 位 置
mov ax ,0 x0 ; 分 别 将 偏 移 地 址 ( 先 ) 和 段 地 址 ( 后 ) 写 入 对 应 位 置
mov [es :0 x8 *4+2] , ax
sti ; 开 中 断
init_timer :
mov al , 0 x36 ; 控 制 字 到 0x43
out 0x43 , al
mov ax , DIVISOR ; 加 载 除 数 值
out 0x40 , al ; 通 道 0 , 用 于 产 生 时 钟 中 断
mov al , ah
out 0x40 , al
handle_interrupt_assembly :
INC ecx
cmp ecx , 50
je return_to_normal
mov al , 0 x20 ; AL = 0 x20 表 示 发 送 EOI 信 号 给 PIC
out 0x20 , al ; 发 送 EOI 信 号
iret
sleep_function :
mov eax , ecx
imul eax ,50
mov ecx , eax
mov [ app_stack_segment ], ss
mov [ app_stack_pos ],sp
mov ss , [ repeat_stack_segment ] ; 设 置 新 的 栈 段 寄 存 器
mov sp ,[ repeat_stack_pos ]
sti
jmp repeat
repeat :
jmp repeat
return_to_normal :
xor ecx , ecx
mov [ repeat_stack_segment ],ss ; 设 置 新 的 栈 段 寄 存 器
mov [ repeat_stack_pos ],sp
mov ss ,[ app_stack_segment ]
mov sp ,[ app_stack_pos ]
mov al , 0 x20
out 0x20 , al
iret
repeat_stack_segment dw 0
repeat_stack_pos dw 0
app_stack_segment dw 0
app_stack_pos dw 0
TIMES 510 -($-$$) DB 0
DW 0 xAA5

需要特别说明的是如下四行,这是我定义的 idle 程序和应用程序的各自的 sp 和 ss 存储的地方,这一 部分在进程切换中尤为重要。

repeat_stack_segment dw 0
repeat_stack_pos dw 0
app_stack_segment dw 0
app_stack_pos dw 0

在 kernel 中修改 int 80h

禁用中断(CLI),然后修改中断向量表中的第 80 号中断(int 0x80)的入口,指向自定义的系统调用 处理程序(syscall_handler)。完成后,重新启用中断(STI)。通过比较 EAX 寄存器的内容来决定执行哪 个功能。如果 EAX 为 0,则跳转到 write_function. 如果 EAX 为 1,则跳转到 sleep_function.

set_interrupt_handler_assembly : ; 初 始 化 int 80 中 断
cli ; 关 中 断
mov ax , 0
mov es , ax
mov ax , syscall_handler
mov [es :0 x80 *4] , ax ; 0 x1C 处 为 向 量 表 中 定 时 器 有 关 中 断 处 理 例 程 的 位 置
mov ax ,0 x0 ; 分 别 将 偏 移 地 址 ( 先 ) 和 段 地 址 ( 后 ) 写 入 对 应 位 置
mov [es :0 x80 *4+2] , ax
sti ; 开 中 断
syscall_handler :
cmp eax ,0
je write_function
cmp eax ,1
je sleep_function

完成 C 语言头文件命名为 myos.h

声明了两个函数头。

extern void my_write ( char *buf , short count );
extern void my_sleep ( short seconds );

在 api 中写入两个函数的具体调用 (传入 ax 的功能号,调用 int 80)

在这一部分,需要进行参数的传递以及将 eax 的调用号调整好,int 80 时跳转到对应的中断处理例程当中去。

[ BITS 16]
section .text
global my_write
global my_sleep
my_write :
push ebp
mov ebp , esp ; 设 置 栈 帧 指 针
mov cx , [ ebp +12] ; count 从 第 二 个 参 数 位 置 获 取
mov si , [ ebp +8] ; buf 从 第 一 个 参 数 位 置 获 取
push ebx
xor eax , eax
int 0 x80 ; 触 发 软 中 断 进 行 系 统 调 用
pop ebx
pop ebp ; 恢 复 栈 帧 指 针
ret ; 返 回 结 果 在 eax 中 , 通 常 为 写 入 的 字 节 数

my_sleep :
push eax
push ecx
mov eax , 1
mov ecx , [ esp + 12]
int 0 x80
pop ecx
pop eax
ret

完成 C 语言测试程序,在 makefile 中进行统一地编译和链接,在 qemu 虚拟机运行

makefile 当中需要将汇编文件用 nasm 编译,将 C 语言程序用 gcc 编译成 elf32 文件,用 objcopy 等 工具提取出相应的代码段和数据段,最后装入 1 号逻辑扇区开始的连续磁盘空间

# include " myos.h "
int main ( void )
{
    while (1)
    {
        my_write (" hello !", 6);
        my_sleep (1) ;
    }
}

加载磁盘

用 LBA 方式读取磁盘,并将磁盘九个扇区的内容读到 0x00:0x8000。因为 app 大小是 4Kb,所以需 要八个扇区,但是要加上数据段,所以就要加一个扇区。

load :
mov ax ,0
mov ds ,ax
mov si ,0 x7DE8
; 设 置 DAP 详 细 信 息 , 直 接 读 到 0x00 :0 x8000 ; 读 取 9 个 扇 区 ( 代 码 加 数 据 段 )
mov ax ,0 x8000
mov [si +0 x4],ax
xor ax ,ax
inc ax
mov [si -0 x1],al
mov ax ,0 x9
mov [si +0 x2],ax
mov word [si ] ,0 x10
mov ebx ,0 x00000001
mov [si +0 x8], ebx
mov ebx ,0 x00000000
mov [si +0 xc], ebx
mov word [si +0 x6 ] ,0 x0000
; 使 用 INT 0 x13 读 取 硬 盘
mov dl ,0 x80
mov ah ,0 x42
int 13h
jmp 0 x00 :0 x8000

最后在终端 make 成功之后,就在 CMD 里面运行 qemu 和生成的 mbr.bin 文件。

运行结果如下,会每隔一定时间,输出一个字符串并换行:

实验结果

用汇编语言实现一个实验性质的 OS 内核并部署到 MBR,以 int 80 系统调用的形式向外提供 write 和 sleep 两个系统服务, 并用汇编语言实现了对应的 API 库。在测试的 C 语言函数中,可以在执行 my_sleep 时候进行栈的切换,跳转到 idle 进程来,在 my_sleep 结束后再跳 转回 APP 进程。

总结与思考

(1). 因为这一次的 write 是有一个字符串长度这样子的参数的,我原本拿到这个参数觉得没什么用, 后来我查阅资料,使用了 loop 来进行循环控制, 相应的我就需要将循环的次数传入到寄存器 ecx 中

(2). 磁盘 load 我一开始使用的是 CHS 寻址方式,但是我发现这样子好像是读不进去的,最后我换成 了 LBA 寻址方式,将硬盘上的所有扇区视为一个连续的线性地址空间。

(3). 一开始我对于栈的切换不是很明白,参考老师上课讲解的进程切换的时候需要将进程的 IP,CS, flags 保存起来,我原本认为是需要将这三个参数显式 push 到栈当中,查询资料之后才知道在 int 中断的 时候就已经可以自动将这几个自动 push 进去,而我只需要将当前栈保存,并跳转到另外一个栈去即可。 等到 idle 进程结束时候,再跳转回原来的栈,用 iret 自动 pop 这三个状态量,并从状态返回。

(4). 原先我只保存了 sp,后来在助教的提示下,为了保证严谨,所以需要将段寄存器 ss 也一并保存 起来。ss 寄存器指示了当前栈段的位置,而 sp 指针则指向栈顶。如果不保存 ss,当程序从中断处理或任 务切换返回时,可能会错误地使用了不正确的栈段,导致栈操作混乱,比如错误的函数返回、局部变量访 问错误等。

(5). 这次的任务理解起来是没有什么困难的,实际上实现也不是很复杂。但是对于 gcc 编译器,makefile 的使用,以及通过栈切换实现进程切换是需要学习的。

参考文献

https://blog.csdn.net/Ghjhfssfgk/article/details/104392688 https://blog.csdn.net/lijiewen2017/article/details/124574991

《操作系统真相还原》(郑刚)(Z-library)

流程图

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值