系统调用在用户空间进程和硬件设备之间添加了一个中间层,主要作用是:
系统调用是用户空间访问内核的唯一手段,除了异常和陷入之外,它是内核唯一的合法入口
应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来变成,一个API定义了一组应用程序使用的编程接口,可以实现为一个或多个系统调用,或者完全不使用任何系统调用
在Linux中,每个系统调用被赋予了一个系统调用号。
系统调用号的特点:
sys_ni_syscall()
来填补这种空缺,它除了返回-ENOSYS
外不做任何工作系统调用表sys_call_table,为每一个有效的系统调用指定了唯一的系统调用号
Linux系统调用比其他许多操作系统都要快,原因是:
通知内核的机制通过软中断实现:通过引发一个异常来促使系统切换到内核态去执行异常处理程序
在x86系统上预定义的软中断是中断号128,通过int $0x80指令触发中断,这条指令触发一个异常导致系统切换到内核态并执行第128号异常处理程序(这个异常处理程序就是系统调用处理程序),它的名字是system_call()
因为所有系统调用陷入内核的方式都一样,所以需要把系统调用号传给内核用于区分每种系统调用。
在x86上,系统调用号通过eax寄存器传递给内核,在陷入内核之前,用户空间把相应系统调用号放入eax中,system_call()函数将给定的系统调用号与NR_syscalls作比较来检查其有效性,如果大于或等于NR_syscalls,就返回-ENOSYS,否则,执行相应的系统调用
call *sys_call_table(, %rax, 8)
系统调用额外的参数也是存放在寄存器传递给内核。在x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前5个参数,如果超过5个参数,需要用单独的寄存器存放所有指向这些参数在用户空间地址的指针
给用户空间的返回值也通过寄存机传递,在x86系统中,它存放在eax寄存器
实现的几个原则:
由于系统调用在内核空间执行,为了保证系统的安全和稳定,系统调用必须仔细检查所有的参数是否合法
其中,最重要的一项就是检查用户提供的指针是否有效。在接收一个用户空间的指针之前,内核必须保证:
内核提供两个方法完成必须的检查和内核空间与用户空间之间数据的来回拷贝,分别为copy_to_user()
和copy_from_user()
如果执行失败,这两个函数返回没能完成拷贝的数据的字节数;如果成功,返回0。两个方法都有可能阻塞,当包含用户数据的页被换出到磁盘而不再物理内存上时,就可能发生,此时进程休眠,知道缺页异常程序将改页换入物理内存
注意:内核无论何时都不能轻率地接受来自用户空间的指针!
最后一项检查:进程是否有对应系统调用的合法权限,如reboot()
系统调用,需要确保进程拥有CAP_SYS_REBOOT功能
内核在执行系统调用事处于进程上下文,current指针指向当前任务,即引发系统调用的那个进程
在进程上下文中,内核可以休眠(比如在系统调用阻塞或者调用schedule()时),这说明了:
注册一个正式的系统调用的过程为:
例如,一个虚构的系统调用foo()注册的过程:
.long sys_foo
加入表的末尾然后在该表中加入一行
#define __NR_foo 338
asmlinkage long sys_foo()
{
return THREAD_SIZE
}
用户程序除了通过标准头文件和C库链接来使用系统调用之外,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+2*n个参数,第一个参数对应系统调用的返回类型,第二个参数是系统调用的名称,之后就是系统调用参数顺序排列的每个参数的类型和名称。该宏会扩展成内嵌汇编的C函数,汇编语言执行将系统调用号压入寄存器并触发软中断陷入内核的过程