系统调用

1、什么是系统调用?

系统调用是在用户空间进程和硬件设备之间添加了一个中间层。主要作用有:

1、为用户空间提供了一种硬件的抽象接口。用户只需要关心要实现的功能,而不需要关系底层是怎么样实现的。

2、保证了系统的安全和稳定。作为中间层,系统调用可以基于权限、用户类型和其他的一些规则对需要进行的访问进行裁决。

3、每个进程都运行在虚拟系统中,而在用户空间和系统的其他部分提供这样的一层公共接口,也是出于这样的考虑。如果应用程序可以随意访问硬件而内核又一无所知,那么几乎就没有办法实现多任务和虚拟内存。也就没法谈安全性和稳定性。

在Linux中,系统调用是用户空间访问内核的唯一的手段了;除了异常和陷入之外,系统调用是内核唯一的合法入口。

 

2、API、POSIX和C库的关系

1

 

在Unix的接口设计中,有这么一句话“提供体制(mechanism)而不是策略(policy)”,换句话就是说,Unix的系统调用抽象出来用与完成某种特定的目的的函数,至于这些函数怎么使用,则不需要内核去关心。。

3、系统调用

系统调用(Linux下为syscall),通过C库中定义的函数调用来进行。通常都需要定义零个、一个或者几个参数(输入)

如getpid()的系统调用,其定义的示例:

SYSCALL_DEFINE(getpid)
{
   return task_tgid_vnr(current); //return current->tgid
}

定义中并不需要规定其是如何实现的,SYSCALL_DEFINE0为一个宏,定义了一个无参数的系统调用(数字为0),展开之后的代码为

asmlinkage long sys_getpid(void)

分析定义:

1) asmlinkage为限定词,为一个编译指令,通知编译器从栈中提取函数的参数。所有的系统调用都需要用到该限定词。

2)函数的返回值为long型,是出于兼容32位机器和64位机器的考虑。系统调用在用户空间和内核空间有不同的返回值类型,在用户空间其返回值的类型为int,在内核空间其返回值的类型为long。

3)最终,get_pid()在内核中被定义为 sys_getpid()。这是Linux中所有系统都遵守的命名规则。

1、系统调用号

在Linux中每一个都有一个系统调用号,在用户空间的进程执行系统调用的时候,会通过这个系统调用号找到到底需要执行的是哪一个系统调用;用户空间的进程不会提及用户调用的名称。

系统调用号一旦被分配,则不能被修改,否则编译好的代码就会运行错误。同时,如果一个系统调用被删除,那么也不被允许被回收利用。(这两个注意到 地方的理解就在于,系统使用系统调用号来区别不同的系统调用的,如果可以的话,就会出现混乱)。在Linux中还有一个为实现的系统调用sys_ni_syscall(),除了返回-ENOSYS外不做任何工作,专门用来补缺。

内核将系统注册过的系统调用放在sys_call_table中,arch/x86/kernel/syscall_table_32.S 可以查看到x86体系结构下的系统调用表。

2、系统调用的性能

Linux的系统调用的执行时间要比其他系统执行的快,原因:

1、较短的上下文切换的时间。

2、系统调用处理程序和每个系统调用本身都非常整洁。

4、系统调用处理程序

由于内核是在受保护的地址空间上,因此用户空间是无法直接执行内核代码的,也不能直接调用内核空间的程序,所有Linux系统调用的实现是通过告知内核自己需要执行一个系统调用,然后切换到内核态,然后内核就可以代表应用程序在内核空间执行系统调用。

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

1、指定恰当的系统调用

所有的系统调用陷入内核态的方法都是一样的,通过引发一个异常,是系统切换到内核态上去处理。所有还需要把系统调用号传送给内核,在X86中,系统调用好通过eax寄存器传送给内核。其他的体系结构也都是类似的。

system_call()程序通过给定的系统号,与NR_syscalls做比较检查其有效性。如果其大于或者等于NR_syscalls,则返回-ENOSYS。否则继续执行相应的系统调用。

2、参数传递

参数的传递类似于系统号的传递,都是放在寄存器中,给用户空间的返回值也是类似。

5、怎么实现系统调用

1、实现系统调用:

1)明确系统调用的功能:在Linux中不提倡多用途的系统调用(同一个系统调用号,通过不同的传递的参数值来完成不同的工作)

2)明确系统调用的接口:新的系统调用的参数、返回值和错误码都是什么?系统调用接口应该追求 简洁、参数尽可能少。比如需要考虑功能多次改变会怎么样?新的功能能否可以追加到系统调用?是否可以容易的修订错误并且不会破坏向后兼容。

系统调用的标志,不是用来实现单个系统调用有多个不同的行为(如前所述,这是不允许的),而是为了即使增加新的功能和选项的时候,也不会破坏向后兼容或者不需要增加新的系统调用。

在设计系统调用的时候,比如要考虑的,尽量为将来做多考虑:1)是不是对函数做了不必要的限制?系统调用设计的越通用越好,不能假设说,系统调用以后也会这么用,系统调用的目的不变,但用法可能会发生改变。2)系统调用是不是可移植的?不要对机器的字节长度和字节序做假设。

2、参数验证

<1>系统调用必须仔细检查传入的参数是否合法有效的。由于系统调用是在内核空间中运行,如果将不合法的参数传入内核,将会对系统的稳定性和安全性都产生极大的破坏。

e.g 与I/O相关的,需要检查文件描述符是否正确。与进程相关的,需要检查PID是否正确。

<2>最重要的检查是检查指针是否合法有效。系统调用在接收用户空间的指针之前,内核需要保证:

        1、指针区域指向的内存区域为用户空间。进程绝对不能哄骗内核去读取内核空间的内容。

        2、指针区域指向的内存区域是在进程的地址空间里面。进程不能哄骗内核去读取其他进程的数据。

        3、如果是读,需要标记内存区域是可读;如果写,则需要标志可写;如果可以执行,则内存被标记为可执行。进程不能够绕过内存内存的访问限制。

内核提供了两个放完来完成必须的检查,以及完成内核空间与用户空间的数据的来回拷贝。copy_to_user() 和 copy_from_user()

copy_to_user() 向用户空间写入大的数据,3个参数:进程空间的目的内存地址;内核空间内的源地址;需要拷贝的数据长度(字节数)。

copy_from_user() 从用户空间读取数据

如果执行失败,这两个函数返回的都是没能够完成的拷贝的数据的字节数。如果成功,则返回0。如果出现错误,则返回的是标准 -EFFAULT。

范例:演示了 copy_to_user() copy_from_user()用法

SYSCALL_DEFINE3(silly_copy, unsigned long * src, unsigned long * dest, unsinged long len)
{
    unsinged long buf;
    if (copy_from_user(&buf , src, len))
         return -EFAULT;
    if (copy_to_user(dst, &buf, len))
         return -EFAULT;
    return len;
}

其中 copy_to_user() copy_from_user()都可能会引起系统阻塞。

<3>检查是否具有合法的权限。在老版本使用suser()来完成,该版本只能检查用户是否为超级用户,现在被新版本更细粒度的取代。新的系统允许检查针对特定资源的特殊权限。可以通过调用capable()检查是否有权能够对指定的资源进行操作,如果返回非0,则可以操作,如果返回0,则无权操作。

比如说,使用capable(CAP_SYS_NICE)可以检查调用者是否具有更改其他进程nice的值的权限。

6、系统调用上下文

内核在执行系统调用的时候,出于进程上下文。

current指针指向当前任务,即引发系统调用的那个进程。在进程上下文中,内核可以休眠,并且内核可以被其他进程抢占。

说明:1、能够休眠说明系统调用可以使用内核提供的绝大部分的功能。(中断不能休眠,所有绝大部分的工作都推迟到底半部执行。) 2、因为是在进程上下文中,进程可以被抢占,因此是新抢占的进程也可以调用该系统调用,因此要保证系统调用是可重入的。

1、绑定系统调用的其他操作

2、从用户空间访问系统调用

7、为什么尽量不通过系统调用的方式实现

系统调用的优点:

1、系统调用容易创建且使用方便

2、Linux系统调用性能比较高

问题:

1、需要一个系统调用号,该调用号需要在开发阶段由官方分配

2、系统调用被加入稳定内核之后就固话了,其接口不允许改动。

3、需要将系统调用注册到每一个支持的体系中去。

4、脚本中不容易调用系统调用,也不能从文件系统直接访问系统调用。

。。。

替代方法:

实现一个设备节点,并实现write()和read()。使用ioctl()来对特定设备进行操作或者对特定信息进行检索。

About: happyhls


发表评论

电子邮件地址不会被公开。 必填项已用*标注