Linux系统调用
1. 系统调用
(1) 系统调用
-
访问系统调用(syscall)通常通过C库中定义的函数调用来进行,通常用一个负的返回值表明错误,0表明成功。为了保证32位和64位系统的兼容,系统调用在用户空间和内核空间有不同的返回值类型,在用户空间为int,在内核空间为long。
-
所有的系统调用都需要限定词
asmlinkage
,这是一个编译指令,通知编译器仅从栈中提取该函数的参数。 -
系统调用在出现错误的时候C库会把错误码写入
errno
全局变量,通过调用perror()
库函数,可以把该变量翻译成用户可以理解的错误字符串。
(2) 系统调用号
-
每个系统调用被赋予一个系统调用号。系统调用号一旦分配就不能变更。如果一个系统调用被删除,它所占用的系统调用号也不允许被回收利用。Linux有一个“未实现"系统调用
sys_ni_syscall()
,它除了返回-ENOSYS
外不做任何其他工作,这个错误号就是专门针对无效的系统调用而设的。如果一个系统调用被删除,或者变得不可用,这个函数就要负责“填补空觖”。 -
内核记录了系统调用表中的所有已注册过的系统调用的列表,存储在
sys_call_table
中。 每一种体系结构中都明确定义了这个表,在x86-64中,它定义于arch/i386/kernel/syscall_64.c 文件中。这个表为每一个有效的系统调用指定了唯一的系统调用号。
2. 系统调用处理程序
(1) 指定恰当的系统调用
- 用户空间的程序不能直接调用内核空间中的函数。通知内核的机制是靠软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序,此异常处理程序就是系统调用处理程序。在x86上,在陷入内核之前,用户空间就把相应的系统调用号放入
eax
寄存器中传递给内核。 - 在x86系统上预定义的软中断是中断号128,通过
int $0x80
指令触发该中断。这条指令会触发一个异常导致系统切换到内核态并执行第128号异常处理程序system_call()
,它与硬件体系结构紧密相关,x86-64的系统上在entry_64.S中编写。与int中断指令相比,sysenter
指令提供了更快、更专业的陷入内核执行系统调用的方式。 system_call()
函数通过将给定的系统调用号与NR_syscalls
做比较来检查其有效性。如果它大于或者等于NR syscalls
,该函数就返回-ENOSYS
;否则,就执行相应的系统调用:
call *sys_call_table(,%rax,8)
系统调用表中的表项以64位(8字节)类型存放,内核需要将给定的系统调用号乘4,然后用所得的结果在该表中查询其位置。在x86-32系统上,代码类似,只是用4代替8。
(2) 参数传递
除了系统调用号以外,大部分系统调用都还需要一些外部的参数输入。在x86-32系统上,ebx
、ecx
、edx
、esi
和edi
按顺序存放前五个参数。需要六个或六个以上参数时用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针。给用户空间的返回值也通过寄存器传递,在x86系统上,它存放在eax
寄存器中。
3. 系统调用的实现
(1) 实现系统调用
- 在Linux中不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来选择完成不同的工作)。
- 系统调用的接口参数尽可能少。很多系统调用提供了标志参数以确保向前兼容。
- 系统调用不对机器的字节长度和字节序做假设。
(2) 参数验证
-
系统调用必须检查所有参数是否合法有效。进程不应当让内核去访问那些无权访问的资源。在接收一个用户空间的指针之前,内核必须保证:指针指向的内存区域属于用户空间;指针指向的内存区域在进程的地址空间里;进程不能绕过内存访问限制。如果是读,该内存应被标记为可读;如果是写,该内存应被标记为可写;如果是可执行,该内存被标记为可执行。
-
为了向用户空间写入数据,内核提供了
copy_to_user()
。它需要三个参数:第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个参数是需要拷贝的数据长度(字节数);为了从用户空间读取数据,内核提供了copy_from_user()
。如果执行成功,则返回0;如果失败,返回没能完成拷贝的数据的字节数,出现错误时,系统调用返回标准-EFAULT
。copy_to_user()
和copy_from_user()
都可能引起阻塞。当包含用户数据的页被换出到硬盘上而不是在物理内存上的时,进程就会休眠,直到缺页处理程序将该页从硬盘重新换回物理内存。 -
最后检查是否有合法权限。使用
capable()
来检查是否有权能对指定的资源进行操作,如果返回非0值,调用者就有权进行操作,返回0则无权操作。比如capable(CAP_SYS_NICE)
可以检查调用者是否有权改变其他进程的nice值,默认情况下,属于超级用户的进程拥有所有权利而非超级用户没有任何权利。<linux/capability.h>中包含一份所有这些权能和其对应的权限的列表。
4. 系统调用上下文
内核在执行系统调用的时候处于进程上下文,current
指针指向当前任务,即引发系统调用的那个进程。在进程上下文中,内核可以休眠并且可以被抢占。当系统调用返回的时候,控制权仍然在system_call()中,它最终会负责切换到用户空间,并让用户进程继续执行下去。
(1) 绑定一个系统调用的最后步骤
当编写完一个系统调用后,就要把它注册成一个正式的系统调用:
-
在系统调用表的最后加入一个表项。从0算起,系统调用在该表中的位置就是它的系统调用号。对于大多数体系结构来说,该表位于entry.S文件中。每种体系结构不需要对应相同的系统调用号,系统调用号是专属于体系结构ABI(应用程序二进制接口)的部分。
-
在<asm/unistd.h>中定义系统调用号。
-
将系统调用编译进内核映象(不能被编译成模块)。这只要把它放进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)
- 对于每个宏来说,都有2+2xn个参数,第一个参数对应系统调用的返回值类型;第二个参数是系统调用的名称;再以后是按照参数顺序排列的每个参数的类型和名称。
_NR_open
在<asm/unistd.h>中定义,是系统调用号,该宏会被扩展成为内嵌汇编的C函数,调用open()
系统调用直接把上面的宏放置在应用程序中就可以了。
(3) 系统调用的替代方法
建立一个新的系统调用很容易但是有弊端,通常不直接建立一个新的系统调用,而是采取替代方法:实现一个设备节点,并对此实现read()
和write()
。使用ioctl()
对特定的设置进行操作或者对特定的信息进行检索。
-
像信号量这样的某些接口,可以用文件描述符来表示。
-
把增加的信息作为一个文件放在sysfs的合适位置。