《自己动手写操作系统》 第六章 系统调用的实现

时间:2021-07-20 04:33:19
在学习系统调用之前,我们有必要理清几个问题:什么是系统调用?为什么要使用系统调用?如何来实现一个系统调用。

1.理论知识

    所谓系统调用,就是内核提供的、功能十分强大的一系列的函数。这些系统调用是在内核中实现的,再通过一定的方式把系统调用给用户,一般都通过门(gate)陷入(trap)实现。系统调用是用户程序和内核交互的接口。 系统服务之所以需要通过系统调用来提供给用户空间的根本原因是为了对系统进行“保护”。
    比如,用户去旅馆住宿,需要申请一个房间,有两种策略:用户自己查表,然后选择一个房间;向前台递交住宿请求,由前台查找到空的房间号,然后交给用户对应的钥匙。显然,为了保护其他乘客的隐私,方便管理,避免冲突等,我们都会采用后一种策略,这里,就是我们的系统调用。可以说,系统调用是应用程序和内核之间的桥梁。

2.实现一个简单的系统调用

    我们现在来实现一个get_tricks函数,来统计当前已经发生了多少次时钟中断。首先,需要明白的是,我们可以在用户态实现这个函数。这里将它放到内核态度,仅仅是为了说明系统调用的写法。我们采用中断来实现系统调用,用寄存器来传递系统调用的参数。

2.1.利用系统调用实现的get_tricks函数:

get_tricks代码如下:kernel/syscall.asm;
 
 8 %include "sconst.inc"
9
10 _NR_get_ticks equ 0 ; 要跟 global.c 中 sys_call_table 的定义相对应!
11 INT_VECTOR_SYS_CALL equ 0x90
12
13
14 ; 导出符号
15 global get_ticks
16
17
18 bits 32
19 [section .text]
20
21 ; ====================================================================================
22 ; get_ticks
23 ; ====================================================================================
24 get_ticks:
25 mov eax, _NR_get_ticks;用eax传递系统调用参数
26 int INT_VECTOR_SYS_CALL
27 ret

2.2定义中断门:

61 PUBLIC void init_prot()
62 {
63 init_8259A();
64
65 // 全部初始化成中断门(没有陷阱门)
......
98 init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);
}

2.3修改save

因为我们使用了eax来传递系统调用的参数,但是eax又被用于保存进程上下文中的运算,所以我们修改save函数,其中的eax换成esi:

save:
...
325 mov esi, esp ; esi = 进程表起始地址
326
327 inc dword [k_reenter] ; k_reenter++;
328 cmp dword [k_reenter], 0 ; if(k_reenter ==0)
329 jne .1 ; {
330 mov esp, StackTop ; mov esp, StackTop <-- 切换到内核栈
331 push restart ; push restart
332 jmp [esi + RETADR - P_STACKBASE] ; return;
333 .1: ; } else { 已经在内核栈,不需要再切换
334 push restart_reenter ; push restart_reenter
335 jmp [esi + RETADR - P_STACKBASE] ; return;
336 ; }
...
ret


2.4系统调用的过程:

342 sys_call:
343 call save
344
345 sti
346
347 call [sys_call_table + eax * 4]
348 mov [esi + EAXREG - P_STACKBASE], eax
349
350 cli
351
352 ret

可以看出,和中断过程相比,同样需要开启中断(因为进入系统调用以后,中断也会被默认关闭),另外,我们无需关闭或者开启对应的时钟中断。
    另外,我们需要如下定义:  
28 PUBLIC  t_sys_call      sys_call_table[NR_SYS_CALL] = {sys_get_ticks}; 23 typedef void*   t_sys_call;注意,这是一个指针,不是一个函数指针

2.5核心函数

sys_get_tricks:
另外,我们增加proc.c文件,sys_get_tricks放在其中。
 
45 PUBLIC int sys_get_ticks()
46 {
disp_str("+");
47 return ticks;
48 }

2.6测试我们的系统调用

我们在proc.h中添加下列函数申明:
 47 /* proc.c */ 48 PUBLIC  int sys_get_ticks();    /* t_sys_call */
49
50 /* syscall.asm */
51 PUBLIC void sys_call(); /* t_pf_int_handler */
52 PUBLIC int get_ticks();
    我们来理清调用关系:get_tricks >>sys_call(eax=!!) >>  sys_get_tricks
    在进程中调用get_tricks:
 

70 void TestA() 71 {
72 while(1){
get_tricks();
73 disp_str("A");
74 disp_int(i++);
75 disp_str(".");
76 delay(1);
77 }
78 }
   然后编译运行即可。


3.改进系统调用

    这里,我们需要定义一个全局变量,初始化成0,在时钟中断处理程序中将tricks +1,然后让get_tricks函数返回tricks,最后修改testA(),打印当前tricks。
 

4.get_tricks的应用

    我们知道,时钟中断发生的时间是固定的,所以完全可以用get_tricks函数来书写一个判断时间的函数。

4.1 8253/8254PIT

    时钟中断控制器采用的intel的芯片 8253,它有三个16位的计数器,其中counter0对应端口地址是0x40h,控制寄存器对应0x43h。其中counter0输出到IRQ0,每隔一段时间产生依次时钟中断。计数器的原理是这样的,有一个输入频率1193180HZ,每次输入将导致计数器-1,当计数到0时,产生中断。而16b的循环需要65535次,所以时钟中断的默认发生频率是1193180/65536=18.2HZ.
    现在我们可以通过改变计数器的值来改变时钟中断的发生频率,因为defaultH/count=userH(用户得到的中断频率=默认频率/计数器数 值),从而可以利用userH得到需要设定的count。接下来,就是如何设置count的问题了。
    我们省略相关的原理,直接给出结论:如果想要设定count,需要先将0x43写入ox43h端口;然后再将count写入0x40端口。有一点需要注意的是,依次只能写入一个字节,按照规定,对于count(16b),我们先写低字节,再写高字节。
 59     /* 初始化 8253 PIT */ 60     out_byte(TIMER_MODE, RATE_GENERATOR);
61 out_byte(TIMER0, (t_8) (TIMER_FREQ/HZ) );
62 out_byte(TIMER0, (t_8) ((TIMER_FREQ/HZ) >> 8));
63 /* 初始化 8253 PIT 完毕 */

4.2测试并使用我们的延迟函数

定义一个延迟10ms等级的函数:
 40 PUBLIC void milli_delay(int milli_sec) 41 {
42 int t = get_ticks();
43
44 while(((get_ticks() - t) * 1000 / HZ) < milli_sec) {}
45 }

在TestA中,调用这个函数:
 76 void TestA() 77 {
78 while(1){
79 disp_str("A");
80 disp_int(get_ticks());
81 disp_str(".");
82 milli_delay(1000);
83 }
84 }

    通过实际的测试,我们可以发现实际的延迟并非是10ms。为什么?因为进程多余一个,while(((get_ticks() - t) * 1000 / HZ) < milli_sec) {}在执行的时候,如果发生了时钟中断,那么循环条件就不能得到有效判断,等到回来重新判定的时候,必然已经超过了10ms。

    这里,比较巧妙的是,我们的计时函数放在用户态,如果将进程数量控制在1,那么就能得到一个比较精确的时间控制。

5.总结

5.1系统调用的由来:

在linux类型的系统中,系统调用是通过中断来实现的,软件中断。系统调用的过程如下:

系统调用使用方式:

因为中断向量数目十分有限,显然不能让一个系统调用仅仅对应一个中断号码,我们采用了中断号对应系统调用;而不同的系统调用,对应不同的调用号码的方式。这里,我们用eax传输系统调用号码。

比如,一个传统的调用方式:mov   eax,3;int 80h(假设80h是系统调用对应的中断号)

但是,这里有一个问题,系统调用号码都不相同,而且参数不等,上层应用程序如何来使用这些系统调用呢。很显然,我们需要针对每个系统调用来起一个名字,给一个更加人性化的调用接口,这就是系统调用的由来。

5.2系统调用的添加方式:


如果是从0开始添加,我们参考上面的过程即可。我们总结一下如何在上面的基础上,添加新的系统调用:

1)NR_SYS_CALL 加一 :const.h

2)给sys_call_table 增加一个新成员sys_foo:global.h

3)sys_foo 函数体:where

4)sys_foo函数申明:proto.h

foo的函数申明:proto.h

5)_NR_foo的定义:syscall.asm

6)foo的函数体:syscall.asm

7)添加global foo:syscall.asm

8)如果参数个数发生变化,有所增加,需要修改sys_call:kernel.asm