拼一个自己的操作系统(SnailOS 0.03的实现)

本文介绍了如何构建一个名为SnailOS的操作系统的早期版本,包括添加空闲线程以防止系统宕机,实现线程的休眠功能,以及设计系统调用的中断处理程序。通过这些机制,系统能够更有效地管理资源和调度任务。
摘要由CSDN通过智能技术生成

拼一个自己的操作系统 SnailOS 0.03的实现

拼一个自己的操作系统SnailOS0.03源代码-Linux文档类资源-CSDN下载

操作系统SnailOS学习拼一个自己的操作系统-Linux文档类资源-CSDN下载

SnailOS0.00-SnailOS0.00-其它文档类资源-CSDN下载

实现简单的系统调用

 

https://pan.baidu.com/s/19tBHKyzOSKACX-mGxlvIeA?pwd=i889

 

附带功能,为系统添加一个空闲线程

不知道大家有没有想过,当系统中仅有一个主线程的情况下会出现什么现象。是啊,如果真的时这样的话,系统将宕机。原因是,主线程中存在阻塞(休眠)自己的函数,虽然中断会唤醒主线程,但仅仅就在那个十分短暂的睡觉的间隙,系统就出大事了。因为现在线程或进程队列中一个可供调度函数调度的程序都没有了。这样只能宕机了。就是下图这样。

db1b01247f5c9c8e4929262845146c2f.png

 

 

当然我们可以不让主线程休眠,可是那样的话,其他线程或者进程就得不到运行的机会了。这主要是主线程运行在最高的运行级别上,如果它不休眠,按照我们的调度算法,其他线程或进程就得不到运行。其实这也没有什么难得,《30天自制操作系统》已经给我们指名了方向。我们只要在最低级,也就是第7级加上一个空闲线程就好了,平时在一般的情况下,系统中一定会有好多的线程和进程在运行者,因此能够等到空闲线程露脸的机会不多。极特殊的情况下,空闲线程无奈上场,力挽狂澜。当中断来临时,主线程被叫醒,系统安然无恙。这个线程我们就放在主函数中,别提有多简单了。

【kernel.c 节选】

(上面省略)

void idle(void* arg);

(中间省略)

// thread_start("k_thread_a", 2, k_thread_a, " argA ", 1);

// thread_start("k_thread_b", 2, k_thread_b, " argB ", 1);

// thread_start("k_thread_c", 3, k_thread_c, " argC ", 1);

/*

创建空闲线程,把空闲线程放在运行级别的最低级上,则当除了主线程

之外没有其他线程或者进程运行时,空闲线程粉墨登场,从而避免了由于

主线程睡眠而造成的无进程运行的调试异常。

*/

thread_start("Idle", 1, idle, "idle ", 7);

/*

创建3个用户级进程,它们都运行在1的运行级上,且优先级为2(优先级

的设置归init_thread函数管)。

*/

// process_execute(ua, "UA", 1);

// process_execute(ub, "UB", 1);

// process_execute(uc, "Uc", 1);

(中间省略)

void idle(void* arg) {

unsigned char* s = arg;

while(1) {

printf_(s);

__asm__ __volatile__ ("sti; hlt;");

}

}

 

decad30727c178710d7ede83fc11ab52.png

 

 

大家看到了吧,由于创建的其他几个进程或线程都被注释掉了,因此只有idle()函数冲锋在前了,而其他几个线程运行的时候,idle()是得不到运行的,这个请大家自行实验。

 

休眠函数的实现

其实休眠一点都不难,它只是让当前线程在指定的时间段内反复地进入就绪态,直到该时间段为0,只不过计算时间段,用了个小技巧,那就是通过一个临时变量和全局变量的ticks的差值来计算。好了,让我们用代码来实现。

【thread.c 节选】

(上面省略)

/*

这是使线程或进程主动让出处理器使用权的函数,难道还有这么傻的进程,

其实这也不是线程傻,主要是有时候,线程还真有这样的需求,就比如

我们的线程在缓冲区显示数据,就需要让自己等待那么一段时间,从而让

我们能够真正看出显示的是什么信息。

*/

void yield1(void) {

unsigned int old_status = intr_disable();

struct task* cur = running_thread();

/*

将当前线程加入到就绪队列尾处,并直接置该线程为就绪态。而后调度。

当应该保证这一切是原子操作,这里通过关中断来完成。

*/

ASSERT(!double_linked_list_find(thread_ready_list, &cur->general_tag));

thread_ready_list = &ready_list[cur->level];

double_linked_list_append(thread_ready_list, &cur->general_tag);

cur->status = READY;

schedule();

set_intr_status(old_status);

asm("sti");

}

 

void ticks_to_sleep1(unsigned int sleep_ticks) {

/*

如何粗略的测定这种所谓的休眠的时间呢?我们在线程运行的时候,初始

调用该函数,则生成一个临时变量start_ticks,这时把全局变量ticks

的值赋给start_ticks,因为这时的差值为0,所以它一定小于我们事先指定

的一个正整数值。所以进入循环,线程进入就绪态。随着时间的推移,

ticks的值被时钟中断处理程序逐渐增大,而该线程也会在某个时刻再次被

调度,这时,该线程是从循环中的某个地方开始运行的,由于上次循环条件

成立,因此再次判断循环条件。直到某次条件不成立,线程才退出此函数。。

*/

unsigned int short start_ticks = ticks;

while(ticks - start_ticks < sleep_ticks) {

yield1();

}

}

 

 

/*

这个仅仅是封装了上面是函数。没有什么好讲的。

*/

void mtime_sleep1(unsigned int msecond) {

unsigned sleep_ticks = msecond / 10 + 1;

ASSERT(sleep_ticks > 0);

ticks_to_sleep1(sleep_ticks);

}

 

在主函数中要把线程的注释都去掉,这样让我们看看线程在信息区显示代码的速度就知道,休眠函数的作用了。这次终于在是跑的没边没沿的了。由于看图也没什么效果,还是请大家自行观察。

 

系统调用就是让用户应用程序可以运行特权指令的方法。当进程(用户级的任务)处于3级用户态的时候,系统的特权指令几乎都不能使用。如果强行使用就会触发一般保护异常。其实这也没有什么大惊小怪的,毕竟用户程序要是能轻易使用控制系统的指令话,系统也就没有什么安全性可言了。因此,那些运行特权指令的事情都是由操作系统来做的,也就是说,那些苦活累活都留给了操作系统。用户程序以某种方式调用系统提供的功能,就是系统调用了。这在某种程度上保证了系统的安全,毕竟,你要使用系统的功能,必须按照实现约定的方式来操作,否则就不能调用。下面我们就上代码。

【system.asm 节选】

(上面省略)

; 系统调用的中断处理程序,本来系统调用是可以用嵌入式汇编实现的,

; 不过笔者觉得那样的语法稍微复杂了些,所以改用汇编过程调用的

; 方法。

global _syscall

extern _syscall_table

align 8

_syscall:

; 一个系统调用的中断向量号。

push 0x88

; 这个语句是多余的,只是仿照中断和异常的处理保留了它。

jmp _syscall_entry

_syscall_entry:

push 0

pusha

push ds

push es

push fs

push gs

 

; 用寄存器传递参数,由于寄存器的数量有限,所以不可能传递过多

; 的参数。

push ebx

push ecx

push edx

call [_syscall_table + eax * 4]

; 为实际的系统调用传递参数后,一定要丢弃栈中的参数。

add esp, 3 * 4

; 按照约定,eax为返回值,所以这里要改变中断栈中的eax的值。

mov [esp + 11 * 4], eax

; 中断返回。

jmp _exit

 

; 一个参数的系统调用,因为我们不使用寄存器传递参数,所以这里

; 只是简单的举了一个例子。

global _syscall1

align 8

_syscall1: ; unsigned int syscall1(unsigned int syscall_nr, unsigned int arg1);

; eax在调用前放入功能号,edx是固定的传递第一个参数。

mov eax, [esp + 1 * 4]

mov edx, [esp + 2 * 4]

int 0x88

ret

 

; 系统调用的中断处理程序2。

; 用用户栈来传递参数,这样参数的个数就不受限制了。

global __syscall

align 8

__syscall:

; 为了便于大家查看相对与栈指针的偏移量是怎么推算来的,

; 笔者在这里标注了压栈的顺序。

; 下面这5个是特权级变化时,处理器自动压入进程的0级栈的。

; push ss

; push esp ; 17

; pushf ; 16

; push cs ; 15

; push eip ; 14

; 下面这两个是为了保持栈的一致和平衡刻意压栈的。

push 0x80 ; 13

jmp __syscall_entry

__syscall_entry:

push 0 ; 12

; 这个则是pusha压栈的细节。

; push eax 11

; push ecx 10

; push edx 9

; push ebx 8

; push esp 7

; push ebp 6

; push esi 5

; push edi 4

pusha

; 压栈4个段寄存器。从而完成了整个进程上下文的保护,当然与中断和

; 异常处理的方式如出一辙。

push ds ; 3

push es ; 2

push fs ; 1

push gs ; 0

 

; 通过掰着手指数数,可以发现栈中高处第17个4字节地址处是用户栈指针。

mov ebx, [esp + 17 * 4]

 

; 因此ebx也就是用户栈指针了。反方向的压入进程0级栈中,从而向系统

; 调用表中的函数传递正确顺序的参数。

push dword [ebx + 7 * 4]

push dword [ebx + 6 * 4]

push dword [ebx + 5 * 4]

push dword [ebx + 4 * 4]

push dword [ebx + 3 * 4]

push dword [ebx + 2 * 4]

push dword [ebx + 1 * 4]

push dword [ebx + 0 * 4]

call [_syscall_table + eax * 4]

; 丢弃参数,恢复栈。

add esp, 8 * 4

; 栈中第11个地址处是用户系统调用前eax的值,但这个值约定作为返回值。

mov [esp + 11 * 4], eax

 

jmp _exit

 

 

; 下面是最多可以传递8个参数的系统调用汇编部分。我们仅解释第二个。

global __syscall0

align 8

__syscall0: ; unsigned int _syscall0(unsigned int syscall_nr);

mov eax, [esp + 1 * 4]

int 0x80

ret

 

; 当用户使用一个参数的系统调用时,按照约定,它需要把系统调用功能号

; 写入eax中,而第一个参数压入到栈的最低处,依此类推,最后的参数应该

; 在用户栈的最高处,由于栈是向下生长,所以最后参数应该先压栈。

global __syscall1

align 8

__syscall1: ; unsigned int _syscall1(unsigned int syscall_nr, unsigned int arg0);

mov eax, [esp + 1 * 4]

push dword [esp + 2 * 4]

int 0x80

; 在系统调用完成后,也一定要还原栈。

add esp, 1 * 4

ret

 

global __syscall2

align 8

__syscall2:

mov eax, [esp + 1 * 4]

push dword [esp + 3 * 4]

push dword [esp + 2 * 4]

int 0x80

add esp, 2 * 4

ret

 

global __syscall3

align 8

__syscall3:

mov eax, [esp + 1 * 4]

push dword [esp + 4 * 4]

push dword [esp + 3 * 4]

push dword [esp + 2 * 4]

int 0x80

add esp, 3 * 4

ret

 

global __syscall4

align 8

__syscall4:

mov eax, [esp + 1 * 4]

push dword [esp + 5 * 4]

push dword [esp + 4 * 4]

push dword [esp + 3 * 4]

push dword [esp + 2 * 4]

int 0x80

add esp, 4 * 4

ret

 

global __syscall5

align 8

__syscall5:

mov eax, [esp + 1 * 4]

push dword [esp + 6 * 4]

push dword [esp + 5 * 4]

push dword [esp + 4 * 4]

push dword [esp + 3 * 4]

push dword [esp + 2 * 4]

int 0x80

add esp, 5 * 4

ret

 

global __syscall6

align 8

__syscall6:

mov eax, [esp + 1 * 4]

push dword [esp + 7 * 4]

push dword [esp + 6 * 4]

push dword [esp + 5 * 4]

push dword [esp + 4 * 4]

push dword [esp + 3 * 4]

push dword [esp + 2 * 4]

int 0x80

add esp, 6 * 4

ret

 

global __syscall7

align 8

__syscall7:

mov eax, [esp + 1 * 4]

push dword [esp + 8 * 4]

push dword [esp + 7 * 4]

push dword [esp + 6 * 4]

push dword [esp + 5 * 4]

push dword [esp + 4 * 4]

push dword [esp + 3 * 4]

push dword [esp + 2 * 4]

int 0x80

add esp, 7 * 4

ret

 

 

global __syscall8

align 8

__syscall8:

mov eax, [esp + 1 * 4]

push dword [esp + 9 * 4]

push dword [esp + 8 * 4]

push dword [esp + 7 * 4]

push dword [esp + 6 * 4]

push dword [esp + 5 * 4]

push dword [esp + 4 * 4]

push dword [esp + 3 * 4]

push dword [esp + 2 * 4]

int 0x80

add esp, 8 * 4

ret

 

【syscall.h】

// syscall.h 创建者:至强 创建时间:2022年8月

#ifndef __SYSCALL_H

#define __SYSCALL_H

 

extern struct mem_man kernel_phy, user_phy, kernel_vir;

 

extern unsigned int syscall1(unsigned int syscall_nr, unsigned int arg1);

void syscall_init(void);

unsigned int sys_getpid(void);

unsigned int getpid(void);

unsigned int sys_write(char* s);

unsigned int write(char* s);

unsigned int sys_sleep(unsigned int msecond);

unsigned int msleep(unsigned int msecond);

 

unsigned int sys_get_ticks(void);

unsigned int get_ticks(void);

void* sys_malloc(unsigned int pg_cnt);

void sys_free(void* ptr);

unsigned int malloc_(unsigned int pg_cnt);

unsigned int free_(void* ptr);

 

extern unsigned int _syscall8(unsigned int syscall_nr, unsigned int arg0,

unsigned int arg1, unsigned int arg2, unsigned int arg3,

unsigned int arg4, unsigned int arg5, unsigned int arg6,

unsigned int arg7);

extern unsigned int _syscall7(unsigned int syscall_nr, unsigned int arg0,

unsigned int arg1, unsigned int arg2, unsigned int arg3,

unsigned int arg4, unsigned int arg5, unsigned int arg6);

extern unsigned int _syscall6(unsigned int syscall_nr, unsigned int arg0,

unsigned int arg1, unsigned int arg2, unsigned int arg3,

unsigned int arg4, unsigned int arg5);

extern unsigned int _syscall5(unsigned int syscall_nr, unsigned int arg0,

unsigned int arg1, unsigned int arg2, unsigned int arg3,

unsigned int arg4);

extern unsigned int _syscall4(unsigned int syscall_nr, unsigned int arg0,

unsigned int arg1, unsigned int arg2, unsigned int arg3);

extern unsigned int _syscall3(unsigned int syscall_nr, unsigned int arg0,

unsigned int arg1, unsigned int arg2);

extern unsigned int _syscall2(unsigned int syscall_nr, unsigned int arg0,

unsigned int arg1);

extern unsigned int _syscall1(unsigned int syscall_nr, unsigned int arg0);

extern unsigned int _syscall0(unsigned int syscall_nr);

 

#endif

 

【syscall.c】

// syscall.c 创建者:至强 创建时间:2022年8月

#include "syscall.h"

#include "thread.h"

#include "screen.h"

#include "string.h"

#include "intr.h"

#include "sync.h"

#include "memory.h"

#include "global.h"

#include "debug.h"

 

extern unsigned int ticks;

/*

系统调用的个数暂定为64个。这个根据系统的实际情况,按需而变。

*/

unsigned int syscall_table[64];

 

/*

获得进程id的系统调用函数。

*/

unsigned int sys_getpid(void) {

return running_thread()->pid;

}

 

/*

包装系统调用为一个c函数。

*/

unsigned int getpid(void) {

return _syscall0(0);

}

 

/*

信息区输出字符串的系统调用函数。

*/

unsigned int sys_write(char* s) {

screen_put_str_white(s);

return strlen_(s);

}

 

unsigned int write(char* s) {

return _syscall1(1, (unsigned int)s);

}

 

unsigned int sys_sleep(unsigned int msecond) {

mtime_sleep1(msecond);

return msecond;

}

 

/*

使进程休眠的系统调用函数。

*/

unsigned int msleep(unsigned int msecond) {

return _syscall1(2, msecond);

}

 

/*

获得滴答值的系统调用函数。

*/

unsigned int sys_get_ticks(void) {

return ticks;

}

 

unsigned int get_ticks(void) {

return _syscall0(3);

}

 

/*

进程中分配用户内存的系统调用函数。

*/

void* sys_malloc(unsigned int pg_cnt) {

struct mem_man* mm_vir, *mm_phy;

struct task* cur = running_thread();

if(cur->pgdir == NULL) {

mm_vir = &kernel_vir;

mm_phy = &kernel_phy;

} else {

mm_vir = &cur->user_vir;

mm_phy = &user_phy;

}

malloc_page(mm_vir, pg_cnt);

}

 

unsigned int malloc_(unsigned int pg_cnt) {

return _syscall1(4, pg_cnt);

}

 

/*

释放内存的系统调用函数。

*/

void sys_free(void* ptr) {

struct mem_man* mm_vir, *mm_phy;

struct task* cur = running_thread();

if(cur->pgdir == NULL) {

mm_vir = &kernel_vir;

mm_phy = &kernel_phy;

} else {

mm_vir = &cur->user_vir;

mm_phy = &user_phy;

}

mfree_page(mm_vir, (unsigned int)ptr);

}

 

unsigned int free_(void* ptr) {

return _syscall1(5, (unsigned int)ptr);

}

 

/*

系统调用初始化,在系统调用表中,手工注册每一个实际运行的函数。

*/

void syscall_init(void) {

syscall_table[0] = (unsigned int)sys_getpid;

syscall_table[1] = (unsigned int)sys_write;

syscall_table[2] = (unsigned int)sys_sleep;

syscall_table[3] = (unsigned int)sys_get_ticks;

syscall_table[4] = (unsigned int)sys_malloc;

syscall_table[5] = (unsigned int)sys_free;

}

 

 

【intr.c 节选】

(上面省略)

void idt_init(void) {

(中间省略)

/*

在中断描述表中添加我们的系统调用汇编函数的地址和中断向量号,在属性上

加上0x6000就是可供用户使用的。这里我们占用了两个,没有什么特殊的想法,

只是想实验一下用寄存器传递参数的方法。

*/

create_gate(0x88, (unsigned int)syscall, 1 * 8, 0x8e00 + 0x6000);

create_gate(0x80, (unsigned int)_syscall, 1 * 8, 0x8e00 + 0x6000);

(下面省略)

 

【intr.h 节选】

(上面省略)

extern void syscall(void);

extern void _syscall(void);

(下面省略)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_39410618

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值