文章目录
系统调用简介
用户空间进程和硬件设备之间的中间层,为用户空间提供一种硬件的抽象接口。
在linux中,系统调用是用户空间访问内核的唯一手段,除异常和陷入外,系统调用是内核的唯一合法入口。Linux提供的系统调用比大部分操作系统都少得多。
系统调用
访问系统调用,通常通过 访问C库中定义的函数 实现。系统调用通过返回long型值表示成功或者错误,系统调用出错时C库会把错误码写入全局变量errno,再通过调用perror()库函数把错误码翻译成用户可以理解的字符串。
系统调用在用户空间int和内核空间long有不同的返回值。
通过宏 定义系统调用。SYSCALL_DEFINEx (yyy) { },x表示参数个数,yyy表示在用户空间的函数名,再加上sys_yyy是在内核中定义的函数名(Linux所有系统调用遵守的命名规则)。
系统调用号
在Linux中每个系统调用被赋予一个独一无二的调用号。一旦分配不再变更,否则系统错乱。
有个未实现的系统调用sys_ni_syscall(),只返回-ENOSYS不做任何事,专门针对无效调用设计,罕用。
系统调用处理程序
用户空间的程序无法直接执行内核代码,不能直接调用内核空间的函数。
需要通知内核,其机制是软中断。通过引发异常使系统切换到内核态去执行异常处理程序(此时的异常处理程序就是系统调用处理程序)。
x86上预定义的软中断是中断号128。触发一个异常导致系统切换到内核态去执行128号异常处理程序,就是系统调用处理程序,system_call()
,与硬件体系结构紧密相关。
指定恰当的系统调用
x86上,系统调用号通过eax寄存器传递给内核。其他体系结构实现方法类似。system_call()
将根据调用号和NR_syscalls对比检查有效性,最终执行对应的系统调用。call *sys_call_table(, %rax, 8)
参数调用
x86上,参数也存放在寄存器上,返回值在eax寄存器中。
read()
—> read()
封装例程 |—> system_call()
—> sys_read()
应用 C库 |—> 系统调用处理程序 系统调用
用户空间 |—> 内核空间
系统调用的实现
给Linux添加新的系统调用比较容易,而设计和实现一个系统调用才是难度所在。
确保系统调用不对系统做错误的假设,否则可能崩溃。如机器字节长度和字节序等。
参数验证
系统调用必须检查他们所有的参数是否合法有效且正确。最重要的检查就是检查用户提供的指针是否合法有效。必须保证:
- 指针指向的内存区域属于用户空间
- 指针指向的内存区域在进程的地址空间
- 读/写/可执行 内存限制要明确,进程绝不能绕过内存访问限制
从用户空间读取数据copy_from_user()
,反之copy_to_user()
。三个参数,arg2位置的数据拷贝到arg1位置,arg3是拷贝数据长度(字节数)。成功返回0,失败返回没能完成拷贝的数据的字节数。失败则 系统调用返回-EFAULT。
Note:copy_from_user()
和copy_to_user()
都可能引起阻塞,当包含用户数据的页被换出到硬盘上而不是在物理内存上时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。
权能机制(capabilities)
检查针对特定资源的特殊权限。通过capable()
检查是否有权能对指定的资源进行操作,返回非0则有权,返回0则无权操作。如capable(CAP_SYS_NICE)
,capable(CAP_SYS_REBOOT)
。
默认情况下,属于超级用户的进程拥有所有权利。
系统调用上下文
内核在执行系统调用时处于进程上下文,current
宏指向引发系统调用的进程。
在进程上下文中内核可以休眠,并可被抢占。
可以休眠:比如,在系统调用阻塞或显式调用schedule()
的时候。
可以被抢占:当前进程同样可被其他进程抢占,其他进程也可使用该系统调用,因此要考虑系统调用的重入问题。
当系统调用返回时,控制权仍在system_call()
中,最终负责切换到用户空间,并让用户进程继续执行下去。
中断处理程序不能休眠,与系统调用不同。
绑定系统调用最后的步骤
编写完系统调用后,需要注册成正式的系统调用。
- 在系统调用表最后加入一个表项,对应位置就是系统调用号;
- 定义系统调用号于
<asm/unistd.h>
; - 系统调用必须被编译进内核镜像,不能编译成模块。放入
kernel/
的相关文件即可。
从用户空间访问系统调用
1. 通过C库支持
通常,系统调用靠C库支持,用户只需包含标准头文件并与C库链接就可以使用系统调用了。
2. 直接使用Linux提供的宏操作
Linux提供一组宏,也可以直接对系统调用进行访问,它会设置好寄存器并调用陷入指令,这些宏是_syscalln()
,n的范围从0到6,代表需要传递给系统调用的参数个数(该宏必须了解有多少参数按照什么顺序压入寄存器)。参数个数为2+2xn
。
举例:
系统调用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()
了。
用系统调用实现 的优缺点
建立新的系统调用 的优点
- 创建容易且使用方便
- Linux系统调用高性能
建立新的系统调用 的问题(缺点)
- 需要一个系统调用号,并且内核处于开发阶段时就要分配好
- 系统调用被固化到内核镜像,无法改动接口
- 需要将系统调用分别注册到各个支持的体系中去
- 在脚本中不容易使用系统调用,在文件系统中也无法直接访问系统调用
- 由于需要调用号,在主内核树之外很难维护和使用系统调用
- 如果仅仅是简单的信息交换,那又大材小用了
替代方法
实现一个设备节点,并对此实现read()
和write()
。