拼一个自己的操作系统 SnailOS 0.03的实现
拼一个自己的操作系统SnailOS0.03源代码-Linux文档类资源-CSDN下载
操作系统SnailOS学习拼一个自己的操作系统-Linux文档类资源-CSDN下载
SnailOS0.00-SnailOS0.00-其它文档类资源-CSDN下载
实现简单的系统调用
附带功能,为系统添加一个空闲线程
不知道大家有没有想过,当系统中仅有一个主线程的情况下会出现什么现象。是啊,如果真的时这样的话,系统将宕机。原因是,主线程中存在阻塞(休眠)自己的函数,虽然中断会唤醒主线程,但仅仅就在那个十分短暂的睡觉的间隙,系统就出大事了。因为现在线程或进程队列中一个可供调度函数调度的程序都没有了。这样只能宕机了。就是下图这样。
当然我们可以不让主线程休眠,可是那样的话,其他线程或者进程就得不到运行的机会了。这主要是主线程运行在最高的运行级别上,如果它不休眠,按照我们的调度算法,其他线程或进程就得不到运行。其实这也没有什么难得,《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;");
}
}
大家看到了吧,由于创建的其他几个进程或线程都被注释掉了,因此只有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);
(下面省略)