第五章 系统调用
小结:
系统调用连锁反应:
陷入内核⇒ 传递系统调用号和参数 ⇒ 执行正确的系统调用函数 ⇒ 返回值带回用户空间
- 与内核通信
系统调用是用户空间访问内核的唯一手段;除异常和陷入之外,他们是内核唯一的合法入口 - API、POSIX、C库
API: 定义一组应用程序使用的编程接口,可以由0、1、多个系统调用组成。
POSIX:最流行的应用编程接口
C库:包括了标准C库函数和系统调用接口。
5.3 系统调用
内核必须提供系统调用所希望完成的功能,但它完全可以按照自己预期的方式去实现。
-
SYSCALL_DEFINE0(getpid)
,定义了一个无参数的系统调用(因为数字为0),展开后的代码为asmlinkage long sys_getpid(void)
,这里asmlinkage限定词,这是一个编译指令,通知编译器仅从栈中提取该函数的参数;且get_pid()在内核中被定义为sys_getpid() - 系统调用号
每个系统调用都有一个独一无二的系统调用号。
内核记录了系统调用表中所有已注册过的系统调用的列表,存储在sys_call_table
中。每种体系结构都明确定义这个表,x86-64中定义于arch/i386/kernel/syscall_64.c文件,这个表为每一个有效的系统调用指定了唯一的系统调用号。
5.4 系统调用处理程序
通知内核的机制是通过软中断实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序,此时的异常处理程序实际上就是系统调用处理程序。 x86上预定义的软中断是中断号128,通过int $0x80
触发。第128号异常处理程序是system_call(),与硬件体系结构密切相关
-
指定恰当的系统调用
仅仅陷入内核空间是不够的,还需要把系统调用号传给内核。在X86上,系统调用号是通过eax寄存器
传递给内核的。- system_call()函数通过将给定的系统调用号与NR_syscalls做比较检查其有效性,如果小于等于NR_syscalls则执行相应的系统调用:
call *sys_call_table(,%rax,8)
- system_call()函数通过将给定的系统调用号与NR_syscalls做比较检查其有效性,如果小于等于NR_syscalls则执行相应的系统调用:
-
参数传递
输入:系统调用还需要一些外部的参数输入,最简单的方法类似传递系统调用号,将参数也放在寄存器中。在x86-32中,ebx、ecx、edx、esi和edi
按照顺序放前5个参数,如果需要6个或以上的参数,则应该用一个单独的寄存器指向所有的这些参数在用户空间地址的指针。
返回值:也是寄存器,x86放在eax
寄存器中
5.5 系统调用的实现
5.5.1 实现系统调用
linux中不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来选择完成不同的工作)
设计时考虑:
(1). 参数是什么?返回值?错误码?
(2). 是否可以容易地修订错误?不会破坏向后兼容性?
(3). 考虑将来:越通用越好,是否可移植?
(4). 可移植性和健壮性
5.5.2 参数验证
检查:
用户提供的指针是否有效
内核提供2个方法来完成必须检查的内核空间和用户空间的数据来回拷贝
- 数据写入用户空间:
copy_to_user()
- 数据从用户空间读入:
copy_from_user()
注意:上述2个函数可能引起阻塞,当包含用户数据的页被换出到硬盘时,此时,进程会休眠,直到缺页处理完成后。 - 是否有合法权限:
capable()
函数检查是否有权对指定的资源进行操作,如capable(CAP_SYS_NICE)是否有权改变nice值。(<linux/capability.h>)
5.6 系统调用上下文
内核在执行系统调用时处于进程上下文,在进程上下文中,内核可以休眠并且可以被抢占。由于可被抢占,所以新的进程可能会使用相同的系统调用,必须小心,保证该系统调用是可重入的。
当系统调用返回时,控制权仍在system_call()中,它最终负责切换到用户空间。
5.6.1 绑定一个系统调用的最后步骤
系统调用的注册:
(1). 在系统调用表的最后加入一个表项。从0开始,系统调用在表中位置就是他的系统调用号。
(2). 系统调用号都必须定义于<asm/unistd.h>
(3). 系统调用必须被编译进内核映像(不能被编译为模块).只要把它放进kernel/下的一个相关文件即可。
步骤(sys_foo为例):
-
将sys_foo加入系统调用表中,对大多数体系结构,该表位于entry.s文件,但是我在4.18.0上看,表已经移动到
/arch/x86/entry/syscalls/syscall_64.tbl
中
0 common read __x64_sys_read
1 common write __x64_sys_write
2 common open __x64_sys_open
3 common close __x64_sys_close
4 common stat __x64_sys_newstat
xxx common foo __x64_sys_foo
-
将系统调用号加入<asm/unistd.h>中,我看的linux4.18.0内核在
usr/include/asm-generic/unistd.h
格式如下:
/* fs/xattr.c */
#define __NR_setxattr 5
__SYSCALL(__NR_setxattr, sys_setxattr)
#define __NR_lsetxattr 6
__SYSCALL(__NR_lsetxattr, sys_lsetxattr)
#define __NR_fsetxattr 7
__SYSCALL(__NR_fsetxattr, sys_fsetxattr)
#define __NR_getxattr 8
#define __NR_foo 338
- 最后实现foo()系统调用。系统调用必须编译到内核中去。可以把源码放入kernel/sys.c文件或者kernel/sched.c中.
#include <asm/page.h>
asmlinkage long sys_foo(void){//返回每个进程的内核栈大小
return THREAD_SIZE;
}
5.6.2 从用户空间访问系统调用
- 定义系统调用的宏
linux本身提供了一组宏,用于直接对系统调用进行访问,它会设好寄存器并调用陷入指令。这些宏是_syscalln()
,其中n的范围是0到6,代表需要传给系统调用的参数个数。
不依赖于库,直接调用系统调用的宏的形式为:
#define NR_open 5 // unistd.h中定义的系统调用号
_syscall3(long, open, const char*, filename, int ,flags, int ,mode)
调用long open(const char* filename, int flags, int mode)
这个宏会被扩展成为内嵌汇编的C函数,由汇编执行陷入内核的操作,如将系统调用号、参数压入寄存器并触发软中断。
2. 应用程序
在要使用open()系统调用的程序中把上述宏方进去即可。
#define __NR_foo 283
__syscall0(long, foo)
int main(){
....
stack_size = foo();
}
5.6.3 为什么不通过系统调用的方式实现
优点:容易且方便;linux系统调用性能高
缺点:(1). 系统调用号需官方分配;(2). 系统调用加入稳定内核后被固话,接口不允许改动;(3). 需要为每个体系结构注册系统调用;(4). 脚本不容易调用系统调用;(5). 在内核主内核树之外难以维护和使用
替代方法:实现一个设备节点,对此实现read()和write(),使用ioctl()对特定的设置进行操作或对特定的信息进行检索。