系统调用在用户空间进程和硬件设备之间添加了一个中间层。作用:
应用程序通过用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。
Unix最流行的应用编程接口是基于POSIX标准的,POSIX定义的API函数和系统调用之间有着直接关系。
C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。
getpid()系统调用的实现:
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
SYSCALL_DEFINE0是一个宏,定义一个无参数(0)的系统调用,展开后如下:
asmlinkage long sys_getpid(void)
asmlinkage是一个编译指令,通知编译器仅从栈中提取该函数的参数。所有系统调用都使用这个限定词。sys_name()是Linux系统系统调用的命名规则。
Linux中,每个系统调用被赋予一个系统调用号。
系统调用号分配后不能变更。如果一个系统调用被删除,它所占用的系统调用号不允许被回收利用。
sys_ni_syscall()表示未实现系统调用,只返回-ENOSYS,为无效的系统调用而设。
系统调用的列表存储在sys_call_table中。
Linux系统调用比其他操作系统执行的快。因为Linux上下文切换时间很短,系统调用处理程序和每个系统调用本身非常简洁。
用户空间的程序无法直接执行内核代码,不能直接调用内核空间的函数,因为内核驻留在受保护的地址空间上。
应用程序通过软中断通知内核执行系统调用,引发一个异常来使系统切换到内核态去执行异常处理程序(系统调用处理程序)。
X86系统预定义的软中断为中断号128,通过int $0x80
指令触发,执行异常处理程序system_call()。x86处理器新增指令sysenter
,更快、更专业地陷入内核。
x86系统上,系统调用号通过eax寄存器传递给内核。system_call()
将系统调用号与NR_syscalls
比较,大于或等于NR_syscalls
返回-ENOSYS
。执行系统调用代码:
call *sys_call_table(, %rax, 8);
系统调用表中表项以64位(8字节)类型存放,x86-32系统上用4代替8。
x86-32系统上,ebx、ecx、edx、esi和edi按照顺序存放前五个参数。
给用户空间的返回值存放在eax寄存器上。
系统调用的接口应该简洁,参数尽可能少,尽量为将来多做考虑。
标志不是用来让单个系统调用具有多个不同的行为,而是为了即使增加新的功能和选项,也不破坏向后兼容或不需要的增加新的系统调用。
系统调用必须仔细检查所有的参数是否合法有效。
例如在接收一个用户空间的指针之前,内核必须保证:
向用户空间写入数据,内核提供了copy_to_user()
方法。第一个参数是进程空间中的目的内存地址,第二个是内核空间内的源地址,最后一个是需要拷贝的数据长度。
从用户空间读取数据,内核提供了copy_from_user()
方法。把第二个参数指定位置上的数据拷贝到第一个参数指定的位置上。
执行失败,函数返回没能完成拷贝的数据的字节数。成功返回0。函数可能会引起阻塞。
内核会检查是否有合法权限。使用capable()
函数来检查是否有权对指定的资源进行操作,返回非0有权操作。
内核在执行系统调用的时候处于进程上下文。current指针指向引发系统调用的进程。
进程上下文中,内核可以休眠并且可以被抢占。能够休眠说明系统调用可以使用内核提供的绝大部分功能;当前的进程可以被其他进程抢占,使用相同的系统调用时要保证系统调用时可重入的。
注册正式的系统调用:
- 在系统调用表的最后加入一个表项。系统调用号从0开始计算。
- 对于所支持的各种体系结构,系统调用号都必须定义于
Linux使用_syscalln()
宏调用系统调用,其中n的范围从0到6,代表需要传递给系统调用的参数个数:
//open()系统调用定义
long open(const char *filename, int flags, int mode)
//使用系统调用的宏的形式
#define NR_open 5
_syscall113(long, open, const char*, filename, int, flags, int, mode)
第一个参数是系统调用的返回值类型,第二个参数是系统调用的名称,之后是系统调用每个参数的类型和名称。
通常不通过系统调用的方式实现。
好处:
问题:
替代方法: