现在的位置: 首页 > 综合 > 正文

深入浅出Hello World 5

2014年09月05日 ⁄ 综合 ⁄ 共 3086字 ⁄ 字号 评论关闭


 
 

系统调用实现

参考文章:

http://docs.huihoo.com/joyfire.net/6-1.html

 

操作系统为在用户态运行的进程与硬件的通信提供了一组接口,然而用户进程是不能直接和硬件交互的,而是通过系统调用来实现和硬件的交互。

 

首先来说明的是用户态内核态

http://blog.csdn.net/fatsandwich/archive/2008/02/29/2131707.aspx

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。 

内核态与用户态是操作系统的两种运行级别,intel cpu没有必然的联系, intel cpu提供Ring0-Ring3三种级别的运行模式,Ring0级别最高,Ring3最低。Linux使用了Ring3级别运行用户态,Ring0作为 内核态,没有使用Ring1Ring2Ring3状态不能访问Ring0的地址空间,包括代码和数据。Linux进程的4GB地址空间,3G-4G 分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块,以及内核所维护

的数据。用户运行一个程序,该程序所创建的进程开始是运 行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过writesend等系统调用,这些系统调用会调用内核中的代码来完成操作,这时,必 须切换到Ring0,然后进入3GB-4GB中的内核地址空间去执行这些代码完成操作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能 随意操作内核地址空间,具有一定的安全保护作用。
   
至于说保护模式,是说通过内存页表操作等机制,保证进程间的地址空间不会互相冲突,一个进程的操作不会修改另一个进程的地址空间中的数据。

 

总结上面的:由于intel cpu的硬件保护机制的存在,在特权级低的情况下,进程是不能访问一些操作系统资源的,所以linux采用了系统调用的机制来允许应用进程来访问一些硬件资源。所有的进程是共享的一个内核空间的实际物理ram

 

http://tmdnet.nothave.com/man/aka/Lecture-4/index.html

中断通常是被定义为一个事件,该事件改编程序执行的顺序,这是与cpu的硬件是相关的。在每种平台上,都有特定的指令可以使进程的执行由用户态转换为核心态,这种指令称作操作系统陷入。进程通过陷入指令便进入了内核态。

 

linux系统的中断的硬件基础首先是idt表。中断描述符表idt是一个系统表,它与每个中断异常向量相联系,在intelcpu中使用寄存器idtr来保存idt表在内存中的位置。

 

当执行了一条指令之后,cseip寄存器会指向下一条将要执行的指令的逻辑地址,在执行那条指令之前,cpu会检查时候发生了中断或者是异常,如果发生了中断或者是异常的话。

 

1.确定中断或者是异常相关联的i

2.读取idt中的第i项(其中保存的是中断描述符)

3.gdt中查找gdt基址,在gdt中查找,读取idt表项中的选择描述符所标识的段描述符,这个描述符指定了中断处理程序所在的段的基址。事实上

idt的初始化在系统启动时,对INT 0x80进行一定的初始化。

 

使用汇编子程序setup_idtlinux/arch/i386/kernel/head.S)初始化idt表(中断描述符表),这时所有的入口函数偏移地址都被设为ignore_int

setup_idt:

lea ignore_int,%edx

movl $(__KERNEL_CS << 16),%eax

movw %dx,%ax /* selector = 0x0010 = cs */

movw $0x8E00,%dx /* interrupt gate - dpl=0, present */

lea SYMBOL_NAME(idt_table),%edi

mov $256,%ecx

rp_sidt:

movl %eax,(%edi)

movl %edx,4(%edi)

addl $8,%edi

dec %ecx

jne rp_sidt

ret

selector = __KERNEL_CS, DPL = 0, TYPE = E, P = 1;

 

Start_kernel()(linux/init/main.c)调用trap_init()(linux/arch/i386/kernel/trap.c)函数设置中断描述符表。在该函数里,实际上是通过调用函数set_system_gate(SYSCALL_VECTOR,&system_call)来完成该项的设置的。其中的SYSCALL_VECTOR就是0x80,而system_call则是一个汇编子函数,它即是中断0x80的处理函数,主要完成两项工作:a. 寄存器上下文的保存;b. 跳转到系统调用处理函数。

 

 

4.确定中断源的特权级能够发出这个中断

5.检查是否发生了特权级的变化,如果需要的话,改变特权级。

6.保存相关寄存器

7.现在装载cseip,从而指定了中断的处理程序的第一条指令

 

现在程序已经执行到了system_call

8.保存相关的寄存器

9.判断当前调用是否是合法系统调用(EAX是系统调用号,它应该小于NR_syscalls

10.在系统调用表sys_call_table中查找处理函数入口地址,并跳转到该入口地址。 

于是开始中断处理函数。

 

现在中断处理程序开始运行。。。。

 

当中断处理程序执行完毕之后,开始中断返回。其中最主要的就是:使用用RESTORE_ALL来弹出所有被SAVE_ALL压入核心栈的内容并且使用iret返回用户态。

 

当然上面的过程中忽略了中断中一个比较重要的部分“参数的传递”:

SAVE_ALL是系统调用参数的传入过程,当执行完SAVE_ALL并且再由CALL指令调用其处理函数时,堆栈的结构应该如上图所示。这时的堆栈结构看起来和执行一个普通带参数的函数调用是一样的,参数在堆栈中对应的顺序是(arg1 ebx),(arg2, ecx,arg3, edx. . . . . .,这正是SAVE_ALL压栈的反顺序,这些参数正是用户在使用系统调用时试图传送给核心的参数。

 

总结上面:显然在内核态中的中断处理函数使用“寄存器”来传递参数。现在来看一个例子:

int sys_write (unsigned int fd, const char* buf, unsigned int count)

显然在执行这个函数时(运行在内核态),该函数需要在栈顶的位置寻找函数的参数。所以需要在SAVE_ALL宏之后,还需要保存其他的寄存器,

http://blog.csdn.net/TopEmbedded/archive/2009/02/24/3933683.aspx

这样就意味着我们只要巧妙地设置好堆栈,让sys_fork()产生一种错觉,好像是上层的一个函数直接调用sys_fork()一样。事实上sys_fork()也的确可以在内核态直接调用。”

 ”通过巧妙的构造堆栈,达到调用内核函数的目的。“

 

好吧整个中断就算完成了。。。

抱歉!评论已关闭.