小结:
系统调用连锁反应:
陷入内核⇒ 传递系统调用号和参数 ⇒ 执行正确的系统调用函数 ⇒ 返回值带回用户空间
内核必须提供系统调用所希望完成的功能,但它完全可以按照自己预期的方式去实现。
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文件,这个表为每一个有效的系统调用指定了唯一的系统调用号。通知内核的机制是通过软中断(与后面下半部的软中断不同)实现的:通过引发一个异常来促使系统切换到内核态去执行异常处理程序,此时的异常处理程序实际上就是系统调用处理程序。 x86上预定义的软中断是中断号128,通过int $0x80
触发。第128号异常处理程序是system_call(),与硬件体系结构密切相关
eax寄存器
传递给内核的。
call *sys_call_table(,%rax,8)
ebx、ecx、edx、esi和edi
按照顺序放前5个参数,如果需要6个或以上的参数,则应该用一个单独的寄存器指向所有的这些参数在用户空间地址的指针。eax
寄存器中linux中不提倡采用多用途的系统调用(一个系统调用通过传递不同的参数值来选择完成不同的工作)
设计时考虑:
(1). 参数是什么?返回值?错误码?
(2). 是否可以容易地修订错误?不会破坏向后兼容性?
(3). 考虑将来:越通用越好,是否可移植?
(4). 可移植性和健壮性
检查:
用户提供的指针是否有效
内核提供2个方法来完成必须检查的内核空间和用户空间的数据来回拷贝
copy_to_user()
copy_from_user()
capable()
函数检查是否有权对指定的资源进行操作,如capable(CAP_SYS_NICE)是否有权改变nice值。(内核在执行系统调用时处于进程上下文,在进程上下文中,内核可以休眠并且可以被抢占。由于可被抢占,所以新的进程可能会使用相同的系统调用,必须小心,保证该系统调用是可重入的。
当系统调用返回时,控制权仍在system_call()中,它最终负责切换到用户空间。
系统调用的注册:
(1). 在系统调用表的最后加入一个表项。从0开始,系统调用在表中位置就是他的系统调用号。
(2). 系统调用号都必须定义于
(3). 系统调用必须被编译进内核映像(不能被编译为模块).只要把它放进kernel/下的一个相关文件即可。
步骤(sys_foo为例):
/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
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
#include
asmlinkage long sys_foo(void){//返回每个进程的内核栈大小
return THREAD_SIZE;
}
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();
}
优点:容易且方便;linux系统调用性能高
缺点:(1). 系统调用号需官方分配;(2). 系统调用加入稳定内核后被固话,接口不允许改动;(3). 需要为每个体系结构注册系统调用;(4). 脚本不容易调用系统调用;(5). 在内核主内核树之外难以维护和使用
替代方法:实现一个设备节点,对此实现read()和write(),使用ioctl()对特定的设置进行操作或对特定的信息进行检索。