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

LINUX系统调用

2018年05月03日 ⁄ 综合 ⁄ 共 4571字 ⁄ 字号 评论关闭
一,系统调用简介
为了和用户空间上运行的进程进行交互,内核提供了一组接口.这组接口即是系统调用.通过该接口,应用程序可以访问硬件设备和其他操作系统资源.
系统调用层主要有三个作用:
1,它为用户空间提供了一种硬件的抽象接口.
2,系统调用保证了系统的稳定和安全.
3,每个进程都运行在虚拟系统中,而在用户空间和内核间提供这样一层公共接口,也是处于这种考虑.因为如果应用程序可以随意访问硬件而内核对此一无所知的话,几乎就没法实行爱你多任务和虚拟内存.
注意:在linux中,系统调用是用户空间访问内核的唯一手段.异常和陷入外,系统调用是内核唯一的合法入口.
下面通过一个简单的例子来简单的接触下系统调用.如getpid()系统调用,在内核中的实现如下:

asmlinkage long sys_getpid(void){

    return current->tgid;

}

这里有两点需要注意:
1,函数类型前有个限定词asmlinkage.它的作用是:通知编译器仅从栈中提取该函数的参数.所有的系统调用都需要这个限定词.
2,注意,系统调用getpid()在内核中被定义成sys_getpid().这是linux中所有系统调用都应该遵守的命名规则.
系统调用号:
下面来说说系统调用号.在linux中,每个系统调用都被赋予一个系统调用号.这样,通过这个独一无二的号就可以关联系统调用.
注意:系统调用号相当关键.一旦分配就不能再有任何变更,否则编译好的应用程序就会崩溃.此外,如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用,否则以前编译好的代码会调用这个系统调用,但实事上却调用的另一个系统调用.
内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在sys_call_table中.这个表为每一个有效的系统调用指定了唯一的系统调用号.

二,API,POSIX和系统调用.
一般情况下,应用程序通过API(应用编程接口)而不是系统调用来编程.而内核开发人员一般则需跟系统调用打交道.所以对于像我这种想搞内核的初级菜鸟来说,了解系统调用是很有必要的.如果你和我一样菜并且想投身于内核,那我们一起来学习吧...
API的实现可以调用一个系统调用,或者多个系统调用,或者跟系统调用毫无关系都可以.但在大多数UNIX系统上,根据POSIX标准定义的API函数和系统调用之间都有着直接关系.
API和系统调用的关系见下图:

在UNIX程序设计中有一句格言"提供机制而不是策略",即,Unix的系统调用抽象出了用于完成某种确定目的的函数.至于这些函数怎么用完全不用内核去关心.


三,系统调用处理程序

用户空间的程序不能直接执行内核代码,因为内核驻留在受保护的空间上,所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了.

通知内核的机制是靠软中断来实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序,此时的异常处理程序实际上就是系统调用处理程序.

因为所有的系统调用陷入内核的方式都是一样的,所以单单的陷入内核是不足以找出对应的系统调用的,所以还需要把系统调用号一起传递给内核.在x86上,系统调用号是通过eax寄存器传递给内核的.在陷入内核之前,用户空间就把相应的系统调用所对应的系统调用号放在eax中了,这样系统调用处理程序一旦运行,就可以从eax中得到数据,调用相应的系统调用了.

除了系统调用号以外,大部分系统调用还需要一些外部的参数输入,所以在发生异常的时候,应该把这些参数从用户空间传给内核.最简单的办法就是像传递系统调用号一样,把这些参数也放在寄存器里.在x86系统上,ebx,ecx,edx,esi和edi按照顺序存放前五个参数,一般情况下五个参数足够.

调用系统调用处理程序以执行一个系统调用的一个简单示例如下:

也就是说,进程通过软中断陷入内核,调用中断处理程序system_call,然后根据传进来的系统调用号,找到对应的系统调用.


四,系统调用的实现

给linux添加一个新的系统调用是件相对容易的工作.怎样设计和实现一个系统调用是难题所在.实现一个新的系统调用的第一步是决定它的用途.每个系统调用都应该有一个明确的用途.然后要考虑新系统调用的参数,返回值和错误码又该是什么.系统调用设计的越通用越好,不要假设这个系统调用现在怎么用,将来也一定怎么用.要确保不对系统调用做错误的假设,否则将来这个系统调用就可能会崩溃.

系统调用必须检查它所有的参数是否有效合法.这点很重要.最重要的一种检查就是检查用户提供的指针是否有效.在接收一个用户空间的指针之前,内核必须保证:

1,指针指向的内存区域属于用户空间.进程决不能哄骗内核去读内核空间的数据.

2,指针指向的内存区域在进程自己的地址空间里.进程决不能哄骗内核去读其他进程的数据.

3,如果是读,该内存应被标记为可读.如果是写,该内存应该被标记为可写.进程决不能绕过内核访问权限.

内核提供了两个方法来完成必须的检查和内核空间与用户空间之间数据的来回拷贝.copy_to_user()和copy_from_user().

下面来看一个示例silly_copy,即演示了一个系统调用的写法,又演示了上面两个函数的使用方法.

/*
*silly_copy:一个简单的系统调用,演示了copy_to_user()和copy_from_user()的用法.
*函数作用:没有实际价值的系统调用,它把len字节的数据从src拷贝到dst,毫无理由的让内核空间作为中转站.
*/

asmlinkage long sys_silly_copy(unsigned
long *src,unsigned
long *dst,unsigned
long len)
{
     unsigned long buf;
     /*如果内核字长与用户字长不匹配,则失败*/
     if(sizeof(len)
!=
sizeof
(buf)
) /*此处书上是if(len != sizeof(buf) ),个人认为写错了,len应该是用户传递的参数,所以sizeof(len)代表了用户字长,而系统调用是在用户空间执行的,所以sizeof(buf)应该是内核字长*/
         return
-
EINVAL;
        
      /*将用户地址空间的src拷贝进buf*/
      if( copy_from_user(&buf,src,len)
)
          return -EFAULT;
         
      /*将buf拷贝到用户地址空间dst*/
      if( copy_to_user(dst,&buf,len)
)
          return -EFAULT;
         
      /*返回拷贝的数据量*/
      return len;
}

这个和书上的代码不一样,请看注释中红色字的部分.


五,系统调用上下文
内核在执行系统调用的时候处于进程上下文(对于进程上下文不明白的朋友可以参考另一篇笔记<进程上下文 VS 中断上下文>).current指针指向当前任务,即引发系统调用的那个进程.在进程上下文中,内核可以休眠和抢占.
当系统调用返回时,控制权仍然在system_call()中,它最终会负责切换到用户空间并让用户进程继续执行下去.

六,如何注册系统调用并从用户空间进行访问
1,系统调用的注册.
ok,在上面的笔记中,我们已经知道如何编写一个系统调用了(其实很简单).接下来我们要做的就是把它注册到系统中.
1),在系统调用表的最后加入一个表项.每种支持该系统调用的硬件体系都必须做这样的工作.对于大多数体系结构来说,该表位于entry.S中.比如i386的位于/usr/src/linux-VERSION/arch/i386/kernel/entry.S中.
2),对于所支持的各种体系结构,系统调用号都必须定义于<asm/unistd.h>中.
3),系统调用必须被编译进内核映象(不能被编译成模块).比如可以将系统调用代码放在kernel/sys.c中,或者把它放在与其关系最紧密的文件中.如和调度相关,则可以放在/kernel/sched.c中.
2,从用户空间访问系统调用.
通常来讲,系统调用靠c库的支持.用户程序通过包含标准头文件并和c库链接,就可以使用系统调用,或者调用库函数再由库函数实际调用.如果自己仅仅写出系统调用,则glibc库不提供支持.
在这种情况下,linux提供了一组宏,用于直接对系统调用进行访问.该宏为_syscalln().其中n的范围从0到6,代表需要传递给系统调用的参数个数.
下面通过open系统调用来说明一下上面的宏.
long open (const char *filename, int flags, int mode)
如果不靠库支持,直接调用此系统调用的宏的形式为:

#define _NR open 5

_syscall3(long, open, const char*, filename, int, flags, int, mode)

这样就可以直接调用open()了.
对于每个_syscalln宏来说,都有2+2*n个参数.第一个参数为返回值类型,第二个参数为系统调用的名称.再以后是按照系统调用的顺序排列的每个参数的类型和名称.
比如我们自己编写一个系统调用my_system_call().代码如下:

#include
<asm/thread_info.h>

asmlinkage long sys_my_system_call(void)
{
    return THREAD_SIZE;
}

进行了1),2)两步的操作后,假设my_system_call()的系统调用号为100,则可以如下直接使用该系统调用.

#define _NR_my_system_call 100
_syscall0(long,my_system_call)

int main(void)
{
    long stack_size;
   
    stack_size = my_system_call();
    printf("the kernel stack size is %ld\n",stack_size);
   
    return 0;
}


七,总结

创建一个新的系统调用有其优点,但也有很多不便之处.

比如创建一个系统调用,需要一个系统调用号,而这需要在一个内核在处于开发版本的时候由官方分配给你.(单从这点,就很难,除非你是那些维护并开发源代码的高手们,并不适合像我这样的菜鸟)再者还需要将系统调用分别注册到每个需要支持的体系结构中去.而且系统调用被加入稳定内核后就被固化了,为了避免应用程序的崩溃,它不允许做改动.等等...

所以,虽然创建一个系统调用是如此的容易,但是绝对不提倡这么做.

ps:不太明白调用软中断后是怎么陷入内核,从而执行系统调用处理函数system_call()的?比如说第一个图中: printf-->c库的printf-->c库的write-->write系统调用,调用到c库的write后是如何陷入内核的?但这并不在本篇笔记的范围内,这里只要知道用户程序需要陷入内核空间后才能调用系统调用就可以了.如果以后能碰到这个问题,要好好研究下.

【上篇】
【下篇】

抱歉!评论已关闭.