什么是系统调用
系统调用是用户空间与内核空间之间交互的接口,用户空间不能直接访问内核空间,而必须通过系统调用才可访问,这是为了保证内核空间的稳定性和安全性。
系统调用与用户编程接口API
用户空间的程序通常不直接使用系统调用,而是通过API间接调用系统调用。API封装了系统调用,但不是每一个API接口都会用到系统调用。API和系统调用并没有严格对应关系,一个API可能恰好只对应一个系统调用,比如read()API和read()系统调用;一个API也可能由多个系统调用实现;有时候,一个API的功能可能并不需要内核提供的服务,那么此时这个API也就不需要任何的系统调用,比如abs()。另外,一个系统调用可能还被多个不同的API内部调用。
Linux的用户编程接口遵循了在Unix世界中最流行的应用编程界面标准——POSIX标准,这套标准定义了一系列API。在Linux中(Unix也如此),这些API主要是通过C库(libc)实现的,它除了定义的一些标准的C函数外,一个很重要的任务就是提供了一套封装例程(wrapper routine)将系统调用在用户空间包装后供用户编程使用。
不过封装并非必须的,如果你愿意直接调用,Linux内核也提供了一个syscall()函数来实现调用
getpid直接系统调用与C库调用示例
#include <syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
int main(void) {
long ID1, ID2;
/*-----------------------------*/
/* 直接系统调用*/
/* SYS_getpid (func no. is 20) */
/*-----------------------------*/
ID1 = syscall(SYS_getpid);
printf ("syscall(SYS_getpid)=%ld\n", ID1);
/*-----------------------------*/
/* 使用"libc"封装的系统调用 */
/* SYS_getpid (Func No. is 20) */
/*-----------------------------*/
ID2 = getpid();
printf ("getpid()=%ld\n", ID2);
return(0);
}
系统调用的实现
Linux中实现系统调用利用了0x86体系结构中的软件中断。软件中断和我们常说的中断(硬件中断)不同之处在于——它是通过软件指令触发而并非外设引发的中断,也就是说,又是编程人员开发出的一种异常,具体的讲就是调用int $0x80汇编指令,这条汇编指令将产生向量为128的编程异常。
之所以系统调用需要借助异常来实现,是因为当用户态的进程调用一个系统调用时,CPU便被切换到内核态执行内核函数,而进入内核——进入高特权级别——必须经过系统的门机制,这里的异常实际上就是通过系统门陷入内核(除了int 0x80外用户空间还可以通过int3——向量3、into——向量4 、bound——向量5等异常指令进入内核,而其他异常无法被用户空间程序利用,都是由系统使用的)。
我们更详细地解释一下这个过程。int $0x80指令的目的是产生一个编号为128的编程异常,这个编程异常对应的是中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一个预设的内核空间地址,它指向了系统调用处理程序:system_call()(别和系统调用服务程序混淆,这个程序在entry.S文件中用汇编语言编写)。
很显然,所有的系统调用都会统一地转到这个地址,但Linux一共有2、3百个系统调用都从这里进入内核后又该如何派发到它们到各自的服务程序去呢?别发昏,解决这个问题的方法非常简单:首先Linux为每个系统调用都进行了编号(0—NR_syscall),同时在内核中保存了一张系统调用表,该表中保存了系统调用编号和其对应的服务例程,因此在系统调入通过系统门陷入内核前,需要把系统调用号一并传入内核,在x86上,这个传递动作是通过在执行int 0x80前把调用号装入eax寄存器实现的。这样系统调用处理程序一旦运行,就可以从eax中得到数据,然后再去系统调用表中寻找相应服务例程了。
除了需要传递系统调用号以外,许多系统调用还需要传递一些参数到内核,比如sys_write(unsigned int fd, const char * buf, size_t count)调用就需要传递文件描述符fd、要写入的内容buf、以及写入字节数count等几个内容到内核。碰到这种情况,Linux会有6个寄存器可被用来传递这些参数:eax (存放系统调用号)、 ebx、ecx、edx、esi及edi来存放这些额外的参数(以字母递增的顺序)。具体做法是在system_call( )中使用SAVE_ALL宏把这些寄存器的值保存在内核态堆栈中。
getpid系统调用的实现示例
系统调用表中arch/x86/kernel/syscall_table_32.S,sys_getpid在sys_call_table中的位置就是其系统调用号,这儿是20
.long sys_getpid /* 20 */
在支持的体系结构(如x86)中定义系统调用号:
arch/x86/include/asm/unistd_64.h:这儿的系统调用号不需要和上面系统调用表中的一致
#define __NR_getpid 39
__SYSCALL(__NR_getpid, sys_getpid)
在kernel/timer.c中实现该系统调用:注意,这儿的名字getpid会在SYSCALL_DEFINE0中拼接成sys_getpid
/**
* sys_getpid - return the thread group id of the current process
*
* Note, despite the name, this returns the tgid not the pid. The tgid and
* the pid are identical unless CLONE_THREAD was specified on clone() in
* which case the tgid is the same in all threads of the same group.
*
* This is SMP safe as current->tgid does not change.
*/
SYSCALL_DEFINE0(getpid)
{
return task_tgid_vnr(current);
}
上面SYSCALL_DEFINE0定义在include/linux/syscalls.h中:
#define SYSCALL_DEFINE0(sname) \
SYSCALL_TRACE_ENTER_EVENT(_##sname); \
SYSCALL_TRACE_EXIT_EVENT(_##sname); \
static struct syscall_metadata __used \
__syscall_meta__##sname = { \
.name = "sys_"#sname, \
.syscall_nr = -1, /* Filled in at boot */ \
.nb_args = 0, \
.enter_event = &event_enter__##sname, \
.exit_event = &event_exit__##sname, \
.enter_fields = LIST_HEAD_INIT(__syscall_meta__##sname.enter_fields), \
}; \
static struct syscall_metadata __used \
__attribute__((section("__syscalls_metadata"))) \
*__p_syscall_meta_##sname = &__syscall_meta__##sname; \
asmlinkage long sys_##sname(void)
#else
#define SYSCALL_DEFINE0(name) asmlinkage long sys_##name(void)
#endif
根据上面的定义,其实很容易在内核中添加一个系统调用,其方法见http://edsionte.com/techblog/archives/2086。
系统调用的性能问题
系统调用需要从用户空间陷入内核空间,处理完后,又需要返回用户空间。其中除了系统调用服务例程的实际耗时外,陷入/返回过程和系统调用处理程序(查系统调用表、存储/恢复用户现场)也需要花费一些时间,这些时间加起来就是一个系统调用的响应速度。系统调用不比别的用户程序,它对性能要求很苛刻,因为它需要陷入内核执行,所以和其他内核程序一样要求代码简洁、执行迅速。幸好Linux具有令人难以置信的上下文切换速度,使得其进出内核都被优化得简洁高效;同时所有Linux系统调用处理程序和每个系统调用本身也都非常简洁。
绝大多数情况下,Linux系统调用的性能是可以接受的,但是对于一些对性能要求非常高的应用来说,它们虽然希望利用系统调用的服务,但却希望加快响应速度,避免陷入/返回和系统调用处理程序带来的花销,因此采用由内核直接调用系统调用服务例程,最好的例子就HTTPD——它为了避免上述开销,从内核调用socket等系统调用服务例程。
使用systemtap追踪进程的系统调用次数
global syscalllist
probe begin {
printf("System Call Monitoring Started (10 seconds)...\n")
}
probe syscall.*
{
syscalllist[pid(), execname(), name]++
}
probe timer.ms(10000) {
foreach ( [pid, procname, name] in syscalllist ) {
printf("%s[%d] calls %s %d times\n", procname,
pid,
name,
syscalllist[pid, procname, name] )
}
exit()
}
输出示例:
# stap system_call_count.stp
System Call Monitoring Started (10 seconds)...
stapio[2328] calls fcntl 82 times
stapio[2328] calls read 92 times
stapio[2328] calls nanosleep 40 times
mongod[2099] calls select 1978 times
mongod[2099] calls gettimeofday 400 times
mongod[2099] calls nanosleep 298 times
compiz[8661] calls read 253 times
compiz[8661] calls clock_gettime 2466 times
compiz[8661] calls poll 555 times
compiz[8661] calls writev 168 times
Xorg[2242] calls clock_gettime 733 times
Xorg[2242] calls setitimer 340 times
Xorg[2242] calls read 340 times
Xorg[2242] calls writev 177 times
Xorg[2242] calls select 170 times
compiz[8661] calls recvfrom 1187 times
redis-server[3600] calls gettimeofday 400 times
redis-server[3600] calls epoll_wait 100 times
本文参考:
http://edsionte.com/techblog/archives/2071
http://www.kerneltravel.net/journal/iv/syscall.htm