• 正文
  • 相关推荐
申请入驻 产业图谱

【Linux内核设计思想】六、系统调用

09/24 08:50
177
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

系统调用

在Linux中,系统调用是除了异常和陷入外,用户空间访问内核的唯一手段,也是内核的唯一合法入口。它提供了用户空间上运行的进程和内核交互的接口,通过这些接口,应用程序可以访问硬件和操作系统的其它资源。系统调用作为用户空间和硬件设备之间的中间层,起到了一个使者的作用,主要作用包括:

为用户空间提供了一种硬件的抽象接口。比如说,应用程序在读取文件的时候根本无需关心磁盘类型和文件系统的类型。

保证系统的稳定和安全,作为硬件和应用程序的中间层,内核可以基于一定的规则对一些访问进行裁决。

Linux进程都运行在虚拟系统中,而系统调用则是一层公共的接口。

API、POSIX、C库

一般来说,应用程序是面向应用编程接口API来编程的,而不是直接面向系统调用编程。一个API定义了一组应用程序使用的编程接口,API和系统调用的关系既可以一对一,也可以一对多,甚至是不调用系统调用也没有关系。API在各种操作系统上的实现不尽相同,但是对应用程序来说都提供了相同的接口。

在Unix系统中,应用程序基本都是基于POSIX标准的,Linux与POSIX是兼容的。应用程序、C库函数、系统调用的调用关系如下,以printf()函数的调用为例:

从程序员的角度来看,程序员只需要关系API就可以了,不需要和系统调用打交道。而内核的角度来看,内核只和系统调用打交道,不关心库函数和应用程序是怎么调用系统调用的。

系统调用的规范

在Linux中,系统调用通常叫做syscalls。所有的系统调用在函数声明时都要加一个asmlinkage限定词,它同于通知编译器仅从栈中提取该函数的参数。另外,Linux中的所有系统调用在命名时都遵守一个规则,在函数名的前面加一个sys前缀,比如sys_getpid()等。系统调用还会通过一个long类型的返回值来表示成功或者错误,Linux系统调用在出现错误的时候会把错误码写入errno全局变量,通过调用perror()库函数可以把该全局变量翻译成用户可以理解的错误字符串。

系统调用号

在Linux中,每个系统调用都对应一个独一无二的系统调用号,当用户空间的进程执行一个系统调用时并不会提及系统调用的名称,而是通过系统调用号来指明要执行的是哪个系统调用。

系统调用号一旦分配就不能再有任何变动,并且如果一个系统调用被删除了,它所占用的系统调用号也不许再回收利用,否则的话以前编译的程序会调用这个系统调用号,但是调用的函数却已经不再是原先的函数了。Linux针对无效的系统调用,设立了一个sys_ni_syscall()系统调用,它不做任何工作仅仅返回-ENOSYS,当一个系统调用不可用或者被删除,这个函数就会填补空位。

Linux内核通过一个系统调用表sys_call_table来记录所有已注册过的系统调用的列表,该表为每一个有效的系统调用指定了唯一的系统调用号。

系统调用处理程序

我们知道,内核驻留在受保护的地址空间上,用户空间程序无法直接执行内核代码,也无法直接调用内核空间中的函数。所以,应用程序应该通过某种机制来通知内核自己需要执行系统调用,请求切换到内核态,以便于内核来代表应用程序执行相应的系统调用。而这个通知机制就是软中断实现的,通过引发一个异常来促使系统切换到内核态并执行异常处理程序。此异常处理程序实际上就是所谓的系统调用处理程序。

在x86系统上的软中断由int $0x80指令产生,该指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序,也就是系统调用处理程序system_call()。

在陷入内核时,还应该传递给内核系统调用号。在x86平台上,在陷入内核之前,用户空间会把相应的系统调用号放入eax寄存器,这样,系统调用号就会通过eax寄存器传递给内核,并陷入内核空间。

system_call()函数通过比较给定的系统调用号和NR_syscalls来检查有效性,如果大于或等于NR_syscalls,system_call()函数返回-ENOSYS,反之执行相应系统调用。

call*sys_call_table(, %eax, 4)

在系统调用表中,表项是以32位的类型存放的,内核需要将给定的系统调用号乘以4,通过结果在系统调用表中查询位置。

除了系统调用号之外,大部分系统调用还需要参数输入。所以,在陷入内核的时候,还应该把参数从用户空间传递给内核。同系统调用号一样,参数也是通过寄存器传递给内核的。在x86平台上,ebx,ecx,edx,esi,edi用于存放前五个参数。如果参数超过5个,将通过一个单独的寄存器存放指向所有这些参数在用户空间中地址的指针。

在x86平台上,返回到用户空间的返回值通过寄存器eax寄存器。

参数检查

系统调用是在内核空间中执行的,所以必须严格仔细地检查系统调用所传递的参数的合法和有效性,来保证系统安全稳定。比如,与文件IO相关的系统调用必须检查文件描述符是否正确有效,而与进程相关的系统调用必须检查进程描述符的有效性。这里最重要的一种参数检查就是对指针的有效性检查,绝不能让内核去访问非法的空间。在接受用户空间的指针时,内核必须保证:

指针指向的区域属于用户空间,进程不能让内核去访问内核空间数据。

指针指向的内存区域必须在进程的地址空间内,一个进程不能让内核去访问它进程的数据。

读/写相应的内存时,必须保证相应的内存空间被标记为读/写,也就是说,必须具有相应的权限。

内核通过两个函数可以实现向用户空间写数据以及从内存空间读数据。

copy_to_user();
copy_from_user();

系统调用上下文

内核在执行系统调用的时候处于进程上下文,此时,current指针指向引发系统调用的当前进程。在进程上下文中,内核可以休眠并可以被抢占。内核可以休眠说明系统调用可以使用内核提供的绝大部分功能,在进程上下文中能被抢占,说明当前进程也能够被其他进程抢占,而新的进程又可以使用和原进程相同的系统调用,所以这就要求系统调用必须是可重入的。

系统调用的注册要求如下:

编写完成系统调用函数,功能、参数、返回值、错误码等。

在系统调用表的最后加一个表项,该表从0开始算起,系统调用在表中的位置就是它的系统调用号。

对于所支持的各种体系结构,系统调用号必须定义在<asm/unistd.h>中。对于每种需要支持的体系结构,都必须将系统调用加入到其系统调用表中,每种体系结构不需要对应相同的系统调用号(系统调用号是专属于体系结构ABI应用程序二进制接口的部分)。

系统调用必须被编译进内核映像,而不是编译为模块。即放到kernel/sys.c文件中。

#undef __SYSCALL
#define __SYSCALL(nr, call) [nr] = (call),

void*sys_call_table[NR_syscalls] = {
[0 ... NR_syscalls-1] =sys_ni_syscall,
#include <asm/unistd.h>
};

从用户空间访问系统调用

系统调用是通过来支持的,用户程序通过包含标准头文件并和C库链接来使用系统调用。同时,Linux本身还提供了一组宏,可以直接访问系统调用而不必通过C库。这组宏_syscalln()其中n要传递给系统调用的参数个数,范围为0~6的数字,该宏会设置好寄存器并调用陷入指令。比如open()系统调用

longopen(constchar*filename, intflags, intmode)

不通过库函数,直接调用open()系统调用的方法如下

#defiine _NR_open 5
_syscall3(long, open, constchar*, filename, int, flags, int, mode)

_NR_open是系统调用号,宏_syscalln()有2+2*n个参数,第一个参数表示系统调用的返回值类型,第二个参数是系统调用名,后面的参数则是系统调用的参数类型和名称。该宏将系统调用号和参数压入寄存器并触发软中断来陷入内核。

相关推荐

Linux、C、C++、Python、Matlab,机器人运动控制、多机器人协作,智能优化算法,贝叶斯滤波与卡尔曼滤波估计、多传感器信息融合,机器学习,人工智能。