C库、API和系统调用
一般情况下,应用程序通过用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程。
LInux系统调用像大多数Unix系统一样,作为C库的一部分提供。
C库实现了Unix系统的主要API,包括标准C库函数和系统调用接口。
Unix中最流行的API是基于POSIX标准的。
示例与说明
getpid()系统调用:
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
SYSCALL_DEFINE0是一个宏,定义了一个无参数的系统调用,它是一系列宏中的一个:
#define SYSCALL_DEFINE0(name) asmlinkage long sys_##name(void)
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)
展开后的定义其实是这样的:
asmlinkage long sys_getpid(void)
这里:
asmlinkage是编译指令,用于通知编译器仅从栈中提取该函数的参数。
long是为了保证兼容性,在用户空间返回的是int,在内核空间为long。
sys_xx是Linux中所有系统调用都应该遵守的命名规则。
系统调用列表
Linux有一张系统调用表记录所有已注册过的系统调用,并为每一个系统调用分配一个系统调用号来关联系统调用。
比如(arch\x86\kernel\syscall_table_32.S):
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long ptregs_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
.long sys_unlink /* 10 */
.long ptregs_execve
.long sys_chdir
.long sys_time
// 后面略
如果有些系统调用,没有实现,对应的位置(从0开始,就是系统调用号)也不会被覆盖,还是用sys_ni_syscall()来替代,表示一个没有实现的系统调用,它只会返回-ENOSYS:
/*
* Non-implemented system calls get redirected here.
*/
asmlinkage long sys_ni_syscall(void)
{
return -ENOSYS;
}
触发系统调用
软中断:通过引发一个异常来促使系统切换到内核去执行异常处理程序,而这个异常处理程序就是系统调用处理程序(对应中断号128,system_call()),下面是x86_64平台的部分实现代码:
arch\x86\kernel\entry_64.S:
ENTRY(system_call)
CFI_STARTPROC simple
CFI_SIGNAL_FRAME
CFI_DEF_CFA rsp,KERNEL_STACK_OFFSET
CFI_REGISTER rip,rcx
/*CFI_REGISTER rflags,r11*/
SWAPGS_UNSAFE_STACK
/*
* A hypervisor implementation might want to use a label
* after the swapgs, so that it can do the swapgs
* for the guest and jump here on syscall.
*/
ENTRY(system_call_after_swapgs)
movq %rsp,PER_CPU_VAR(old_rsp)
movq PER_CPU_VAR(kernel_stack),%rsp
/*
* No need to follow this irqs off/on section - it's straight
* and short:
*/
ENABLE_INTERRUPTS(CLBR_NONE)
SAVE_ARGS 8,1
movq %rax,ORIG_RAX-ARGOFFSET(%rsp)
movq %rcx,RIP-ARGOFFSET(%rsp)
CFI_REL_OFFSET rip,RIP-ARGOFFSET
GET_THREAD_INFO(%rcx)
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%rcx)
jnz tracesys
system_call_fastpath:
cmpq $__NR_syscall_max,%rax
ja badsys
movq %r10,%rcx
call *sys_call_table(,%rax,8) # XXX: rip relative
movq %rax,RAX-ARGOFFSET(%rsp)
另一种触发系统调用的是指令:sysenter。
系统调用号也需要传递给内核,在x86上通过eax寄存器来传递。
除了系统调用号,可能还需要额外的外部参数。
关于外部参数,不同平台应该不同,且书中的说法也跟代码中的有差别,也不必特别关注。
系统调用上下文
内核在执行系统调用的时候处于进程上下文。
current指针指向当前任务,即引发系统调用的那个进程。
内核在执行系统调用的时候也可以休眠(相对应的,中断处理程序不能休眠),因此需要保证系统调用是可重入的。
避免实现新的系统调用
如果仅仅进行简单的信息交换,可以使用替代方法:实现设备节点,并对此实现read()和write()。使用ioctl()对特定的设置进行操作或者对特定的信息进行检索。
像信号量这样的接口,可以用文件描述符来表示。
把增加的信息作为文件放在sysfs的合适位置。