操作系统为用户态的进程与硬件设备进行交互提供了一组接口,在应用程序和硬件之间设置一个额外的层有诸多优点:
1. 使得编程更容易,不需要用户学习硬件设备的低级编程
2. 提高了系统的安全性,内核在满足某个请求之前可以检查请求的正确性
3. 添加的接口使得程序具有可移植性
Unix系统通过向内核发送系统调用(system call)实现用户空间进程和硬件设备之间的大部分接口。 这里使用wiki的一幅图片简单说明:
从图中我们可以看到,无论是在用户空间中调用GNU C库函数后由GNU 的C库与系统调用通信还是应用程序直接调用函数触发系统调用,Linux的内核部分总是通过系统调用接口与外部交流。在当初学习操作系统课程时,我们也知道系统调用是应用程序访问内核空间的唯一手段。从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。比如,用户程序中使用 open() 函数打开一个文件,gnu C库 glibc 将会调用系统调用sys_open(这也是系统调用名称的一般规律,xyz() 函数对应的系统调用名字一般为 sys_xyz ,而在较新内核比如3.10上,绝大多数系统调用的名字都改以“SyS_”开头,也即 open() 函数对应的为 SyS_open 系统调用 )。
换句话说,用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全无虞。我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实地坐在观光车上,按照规定的路线观光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。
实现系统调用需要涉及包括架构相关的特性的一个控制传输方法,典型的实现就是使用软中断(software interrupt)或者陷入(trap)。
Linux 系统调用的可以通过两种不同的方式调用:
* 第一种实现机制是一个多路汇聚以及分解的过程,该汇聚点就是 0x80 中断这个入口点(X86 系统结构)。也就是说,所有系统调用都从用户空间中汇聚到 0x80 中断点,同时保存具体的系统调用号。当 0x80 中断处理程序运行时,将根据系统调用号对不同的系统调用分别处理(调用不同的内核函数处理)。
* 第二种实现为SYSCALL / SYSRET ,SYSENTER / SYSEXIT 汇编语言指令(独立于AMD和intel建立,本质上一样的)。
如果我们使用 ctags 或者 cscope 在Linux 内核源代码中查找系统调用的话(如 “ vim -t sys_open ”或者在 cscope界面下查找 sys_open 的函数定义),插件会告诉我们源代码中并没有对于 sys_open 这个系统调用的定义,所以我们只能简介查找。
我们都知道 sys_open 系统调用的实际操作基本上都是在 do_sys_open 函数中完成的,当然,如果你不知道,你可以使用 ftrace 追踪一会系统调用,然后在追踪结果中进行筛选并找出 sys_open 系统调用的大致调用路径,下面是我的一次追踪:
0) 1-1.tes-21282 | | SyS_open() {
0) 1-1.tes-21282 | | do_sys_open() {
0) 1-1.tes-21282 | | getname() {
0) 1-1.tes-21282 | | ...
0) 1-1.tes-21282 | 1.134 us | }
0) 1-1.tes-21282 | 1.695 us | }
0) 1-1.tes-21282 | 2.214 us | }
0) 1-1.tes-21282 | | get_unused_fd_flags() {
0) 1-1.tes-21282 | | __alloc_fd() {
0) 1-1.tes-21282 | | ...
0) 1-1.tes-21282 | 1.644 us | }
0) 1-1.tes-21282 | 6.201 us | }
0) 1-1.tes-21282 | 6.691 us | }
0) 1-1.tes-21282 | | do_filp_open() {
0) 1-1.tes-21282 | | path_openat() {
0) 1-1.tes-21282 | | ...
0) 1-1.tes-21282 | | do_last() {
0) 1-1.tes-21282 | + 57.283 us | ...
0) 1-1.tes-21282 | | path_put() {
0) 1-1.tes-21282 | 5.327 us | ...
0) 1-1.tes-21282 | 6.043 us | }
0) 1-1.tes-21282 | | putname() {
0) 1-1.tes-21282 | 1.631 us | ...
0) 1-1.tes-21282 | 2.124 us | }
0) 1-1.tes-21282 | ! 274.155 us | } /* do_sys_open */
0) 1-1.tes-21282 | ! 274.725 us | } /* SyS_open */
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
#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__)
#define SYSCALL_DEFINEx(x, sname, ...) \
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...) \
asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)); \
asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__)) \
{ \
long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__)); \
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return ret; \
} \
SYSCALL_ALIAS(sys##name, SyS##name); \
static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))
其实我等菜鸟一看到这样的宏定义肯定瞬间被秒杀了,而这样的宏定义在内核中使用颇为频繁,在这里我们简单熟悉一下这个庞大的宏定义中的一些语法或者关键字:
从Linux文档[5]中我们可以得知,asmlinkage是一种gcc 的标签,它告诉编译器不要去寄存器上寻找参数,而是去CPU的堆栈上寻找。
其实asmlinkage 本身也是一个宏,使用 ctags 我们可以很轻松的寻找到在 "arch/x86/include/asm/linkage.h" 中的宏定义:
#ifdef CONFIG_X86_32
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
呵呵,貌似又惹上了新麻烦阿:-),那么CPP_ASMLINKAGE和__attribute__ (( regparm(0))) 又是什么?
在解释这两个新概念之前,我们还是把asmlinkage 简单介绍清楚: 函数定义前加宏asmlinkage,表示这些函数通过堆栈而不是通过寄存器传递参数。gcc编译器在汇编过程中调用c语言函数时传递参数有两种方法:一种是通过堆栈,另一种是通过寄存器。缺省时采用寄存器,假如你要在你的汇编过程中调用c语言函数,并且想通过堆栈传递参数,你定义的c函数时要在函数前加上宏asmlinkage。
使用ctags 可以很轻松的找到CPP_ASMLINKAGE的定义:(在 include/linux/linkage.h中)
#ifdef __cplusplus
#define CPP_ASMLINKAGE extern "C"
#else
#define CPP_ASMLINKAGE
#endif
所以CPP_ASMLINKAGE就是被定义为 extern "C" ,而 extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。
(1) 被extern "C"限定的函数或变量是extern类型的extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
(2) 被extern "C"修饰的变量和函数是按照C语言方式编译和连接的。
__attribute__是关键字,是gcc的c语言扩展。__attribute__机制是GNU C的一大特色,它可以设置函数属性、变量属性和类型属性等。可以通过它们向编译器提供更多数据,帮助编译器执行优化等。
__attribute__((regparm(0))):告诉gcc编译器该函数不需要通过任何寄存器来传递参数,参数只是通过堆栈来传递。
__attribute__((regparm(3))):告诉gcc编译器这个函数可以通过寄存器传递多达3个的参数,这3个寄存器依次为EAX、EDX 和 ECX。更多的参数才通过堆栈传递。这样可以减少一些入栈出栈操作,因此调用比较快。
关于__attribute__ 更多可以参阅<1><2>。
其实知道asmlinkage 是什么意思并不好玩,要知道它为什么这么用才比较好玩!上文已经说过,系统调用是用户空间向内核空间发出请求来完成某些动作的,所以这些函数是非传统的,你也别指望它能和正常的函数一样,正常的函数通常都是将参数写进寄存器而系统调用这是写入程序堆栈。在用户空间调用系统调用需要将确定值写入寄存器(系统调用号永远被写入eax中,其他的参数被写入ebx,exc等寄存器),这些在系统调用时都需要被翻译。举个例子sethostname,声明为int sethostname(char *name, size_t len),会是这样:
mov ecx, len ; 名字的字节数
mov ebx, name ; 名字字符串的地址
mov eax, 170 ; 系统调用号(syscall number)
int 0x80 ; 通过0x80号中断触发系统调用
0x80号终端出发软中断并将CPU切至内核模式,然后执行系统调用sys_sethostname。具体步骤为:首先它会现将所有寄存器中的数据存入CPU堆栈,然后检查一些其他的东西(比如验证参数),如果一切正常就会调用相应的系统调用。,比如这个例子中的sys_sethostname,定义为:
SYSCALL_DEFINE2(sethostname, char __user *, name, int, len)
{
...
}
因为用户空间传进的参数都已经被保存在栈中,所以编译器必须被提示从栈中提取参数,所以我们需要asmlinkage。
注意:即使一个系统调用不需要参数(比如fork()),也还是需要向eax寄存器中填写系统调用号的。
__VA_ARGS__ 是在C99中定义的一个可变参数的宏(目前只有GCC支持,vc6.0不支持的哦。此外,在很多使用到__VA_ARGS__的场合都会在其前面添加##,作用在于当可变参数的个数为0时,这个##会把前面多余的“,”去掉)。
当程序中调用一个函数时,程序跳到存储器中保存函数的位置开始读取代码执行,执行完后再返回。为了提高速度,C定义了inline函数,告诉编译器把函数代码在编译时直接拷到程序中,这样就不用执行时另外读取函数代码。
Static函数告诉编译器其他文件看不到这个函数,因此该函数只能在当前文件中被调用。Static inline 函数只能在当前文件中被调用,同时执行速度快,几个文件中都可以使用同样的函数名。Static inline的内联函数,一般情况下不会产生函数本身的代码(函数本身不编译),而是全部被嵌入在被调用的地方。如果不加static,则表示该函数有可能会被其他编译单元所调用,所以一定会产生函数本身的代码。所以加了static,一般可令可执行文件变小。内核里一般见不到只用inline的情况,而都是使用static inline。
代码中还有一些其他的宏定义,比如__SC_DECL,这些都是在 include/linux/syscall.h 文件中定义的,但是目前看不太懂 =_=,这里先预留着~
====================
引用:
[1] 《深入理解Linux内核》 第三版 - 第十章:系统调用
[2] Wiki -- system call http://en.wikipedia.org/wiki/Syscall
[3] 《系统调用原理》 http://hi.baidu.com/aniufngrxmhlrxe/item/c7ee284fd1de67e0bdf45198
[4] 《read 系统调用剖析》http://www.ibm.com/developerworks/cn/linux/l-cn-read/#ibm-pcon
[5] http://kernelnewbies.org/FAQ/asmlinkage
[6] http://blog.csdn.net/ce123_zhouwei/article/details/8446520
[7] http://www.quora.com/Linux-Kernel/What-does-asmlinkage-mean-in-the-definition-of-system-calls
[8] http://wenku.baidu.com/link?url=1s--morYW4p8CD0lirJg4cZUCT2Pf1EuVcxSGPu4FF4CZM7woQa7mjBLYKXB4tAJWoTvC33_JDnJ_rWzJvWG1PibGNlB7jLGJGlg-7LGLeq
====================