应用程序在发起某个系统调用时,是先调用标准库中的同名函数,标准库再根据不同体系结构来选择特定的方式陷入内核。
以socket库函数为例(以下均为uClibc中的实现),来说明标准库是如何一步步解析系统调用名并进入内核的:
socket系统调用号:
#define __NR_socket (4000 + 183)
socket()函数的实现就是下面的函数:
_syscall3(int, socket, int, family, int, type, int, protocol)
这个函数调用下面的宏:
SYSCALL_FUNC(3, int, socket, int, family, int, type, int, protocol)
宏展开就成了:
int socket(C_DECL_ARGS_3(int, family, int, type, int, protocol)) { \
return (type)INLINE_SYSCALL(socket, 3, C_ARGS_3(int, family, int, type, int, protocol)); \
}
这里会把类型和参数名之间的逗号去掉,参数名增加两个:系统调用名字和参数个数,于是进一步展开得到:
int socket(int family, int type, int protocol) { \
return (int)INLINE_SYSCALL(socket, 3, family, type, protocol); \
}
INLINE_SYSCALL的定义仍是一个宏:
INLINE_SYSCALL_NCS(__NR_socket, 3, family, type, protocol)
宏展开,系统调用名改为系统调用号,增加一个err参数:
INTERNAL_SYSCALL_NCS(__NR_socket, err, 3, family, type, protocol)
根据参数个数3找到internal_syscall3,其定义如下,前三个参数用在内嵌汇编里所以看起来有点奇怪,实际上就是直接替换:
internal_syscall3( = __NR_socket, ,"r" (__v0), err, family, type, protocol)
!
({ \
long _sys_result; \
\
{ \
register ARG_TYPE __v0 __asm__("$2") = __NR_socket; \
register ARG_TYPE __a0 __asm__("$4") = (ARG_TYPE) arg1; \
register ARG_TYPE __a1 __asm__("$5") = (ARG_TYPE) arg2; \
register ARG_TYPE __a2 __asm__("$6") = (ARG_TYPE) arg3; \
register ARG_TYPE __a3 __asm__("$7"); \
__asm__ __volatile__ ( \
".set\tnoreorder\n\t" \
"syscall\n\t" \
".set\treorder" \
: "=r" (__v0), "=r" (__a3) \
: input, "r" (__a0), "r" (__a1), "r" (__a2) \
: __SYSCALL_CLOBBERS); \
err = __a3; \
_sys_result = __v0; \
} \
_sys_result; \
})
上面的过程就是保存参数,传入系统调用号,执行syscall指令,进入内核态,并准备接收两个返回值:错误码和结果。这里陷入内核的过程是体系结构相关的,上述为MIPS的做法,而ARM中是通过swi指令陷入软中断来完成的。
进入到内核之后的工作以及如何返回到用户态,可以参考MIPS中的系统调用这篇文章中的介绍。
如果在标准库中没有定义某个系统调用的同名函数,可以直接通过syscall(),例如获取线程tid的系统调用gettid,其系统调用号为224,但uClibc中并没有定义gettid()库函数,则可以通过syscall(224)使用该系统调用。而对于用户自定义的系统调用(当然不建议这样做),也只能用syscall()。