作者雷镇
本文是Linux系统调用系列文章的第一篇对Linux系统调用的定义基本原理使用方法和注意事项大概作了一个介绍以便读者对Linux系统调用建立一个大致的印象
什么是系统调用?
Linux内核中设置了一组用于实现各种系统功能的子程序称为系统调用用户可以通过系统调用命令在自己的应用程序中调用它们从某种角度来看系统调用和普通的函数调用非常相似区别仅仅在于系统调用由操作系统核心提供运行于核心态而普通的函数调用由函数库或用户自己提供运行于用户态二者在使用方式上也有相似之处在下面将会提到
随Linux核心还提供了一些C语言函数库这些库对系统调用进行了一些包装和扩展因为这些库函数与系统调用的关系非常紧密所以习惯上把这些函数也称为系统调用
Linux中共有多少个系统调用?
这个问题可不太好回答就算让Linus Torvaldz本人也不见得一下子就能说清楚
在版内核中狭义上的系统调用共有个你可以在<内核源码目录>/include/asmi/unistdh中找到它们的原本也可以通过命令man syscalls察看它们的目录(man pages的版本一般比较老可能有很多最新的调用都没有包含在内)广义上的系统调用也就是以库函数的形式实现的那些它们的个数从来没有人统计过这是一件吃力不讨好的活新内核不断地在推出每一个新内核中函数数目的变化根本就没有人在乎至少连内核的修改者本人都不在乎因为他们从来没有发布过一个此类的声明
随本文一起有一份经过整理的列表它不可能非常全面但常见的系统调用基本都已经包含在内那里面只有不多的一部分是你平时用得到的本专栏将会有选择的对它们进行介绍
为什么要用系统调用?
实际上很多已经被我们习以为常的C语言标准函数在Linux平台上的实现都是靠系统调用完成的所以如果想对系统底层的原理作深入的了解掌握各种系统调用是初步的要求进一步若想成为一名Linux下编程高手也就是我们常说的Hacker其标志之一也是能对各种系统调用有透彻的了解
即使除去上面的原因在平常的编程中你也会发现在很多情况下系统调用是实现你的想法的简洁有效的途径所以有可能的话应该尽量多掌握一些系统调用这会对你的程序设计过程带来意想不到的帮助
系统调用是怎么工作的?
一般的进程是不能访问内核的它不能访问内核所占内存空间也不能调用内核函数CPU硬件决定了这些(这就是为什么它被称作保护模式)系统调用是这些规则的一个例外其原理是进程先用适当的值填充寄存器然后调用一个特殊的指令这个指令会跳到一个事先定义的内核中的一个位置(当然这个位置是用户进程可读但是不可写的)在Intel CPU中这个由中断x实现硬件知道一旦你跳到这个位置你就不是在限制模式下运行的用户而是作为操作系统的内核所以你就可以为所欲为
进程可以跳转到的内核位置叫做sysem_call这个过程检查系统调用号这个号码告诉内核进程请求哪种服务然后它查看系统调用表(sys_call_table)找到所调用的内核函数入口地址接着就调用函数等返回后做一些系统检查最后返回到进程(或到其他进程如果这个进程时间用尽)如果你希望读这段代码它在<内核源码目录>/kernel/entrySEntry(system_call)的下一行
如何使用系统调用?
先来看一个例子
#include /*定义宏_syscall*/
#include /*定义类型time_t*/
_syscall(time_ttimetime_t *tloc) /*宏展开后得到time()函数的原型*/
main()
{
time_t the_time;
the_time=time((time_t *)); /*调用time系统调用*/
printf(The time is %ld
the_time);
}
系统调用time返回从格林尼治时间年月日:开始到现在的秒数
这是最标准的系统调用的形式宏_syscall()展开来得到一个函数原型稍后我会作详细解释但事实上如果把程序改成下面的样子程序也可以运行得同样的结果
#include
main()
{
time_t the_time;
the_time=time((time_t *)); /*调用time系统调用*/
printf(The time is %ld
the_time);
}
这是因为在timeh中实际上已经用库函数的形式实现了time这个系统调用替我们省掉了调用_syscall宏展开得到函数原型这一步
大多数系统调用都在各种C语言函数库中有所实现所以在一般情况下我们都可以像调用普通的库函数那样调用系统调用只在极个别的情况下我们才有机会用到_syscall*()这几个宏
_syscall*()是什么?
在unistdh里定义了个宏分别是
_syscall(typename)
_syscall(typenametypearg)
_syscall(typenametypeargtypearg)
_syscall(typenametypeargtypeargtypearg)
_syscall(typenametypeargtypeargtypeargtypearg)
_syscall(typenametypeargtypeargtypeargtypeargtypearg)
_syscall(typenametypeargtypeargtypeargtypeargtypeargtypearg)
它们看起来似乎不太像宏但其实质和
#define MAXSIZE
里面的MAXSIZE没有任何区别
它们的作用是形成相应的系统调用函数原型供我们在程序中调用我们很容易就能发现规律_syscall后面的数字和typeNargN的数目一样多事实上_syscall后面跟的数字指明了展开后形成函数的参数的个数让我们看一个实例就是刚刚用过的time系统调用
_syscall(time_ttimetime_t *tloc)
展开后的情形是这样
time_t time(time_t * tloc)
{
long __res;
__asm__ volatile(int $x : =a (__res) : ()b ((long)(tloc)));
do {
if ((unsigned long)(__res) >= (unsigned long)()) {
errno = (__res);
__res = ;
}
return (time_t) (__res);
} while () ;
}
可以看出_syscall(time_ttimetime_t *tloc)展开成一个名为time的函数原参数time_t就是函数的返回类型原参数time_t *和tloc分别构成新函数的参数事实上程序中用到的time函数的原型就是它
errno是什么?
为防止和正常的返回值混淆系统调用并不直接返回错误码而是将错误码放入一个名为errno的全局变量中如果一个系统调用失败你可以读出errno的值来确定问题所在
errno不同数值所代表的错误消息定义在errnoh中你也可以通过命令man errno来察看它们
需要注意的是errno的值只在函数发生错误时设置如果函数不发生错误errno的值就无定义并不会被置为另外在处理errno前最好先把它的值存入另一个变量因为在错误处理过程中即使像printf()这样的函数出错时也会改变errno的值
系统调用兼容性好吗?
很遗憾答案是不好但这决不意味着你的程序会三天两头的导致系统崩溃因为系统调用是Linux的内核提供的所以它们工作起来非常稳定对于此点无需丝毫怀疑在绝大多数的情况下系统调用要比你自己编写的代码可靠而高效的多
但是在Linux的各版本内核之间系统调用的兼容性表现得并不像想象那么好这是由Linux本身的性质决定的Linux是一群程序设计高手利用业余时间开发出来的他们中间的大部分人没有把Linux当成一个严肃的商业软件(现在的情况有些不同了随着Linux商业公司和以Linux为生的人的增长不少人的脑筋发生了变化)结果就是如果新的方案在效率和兼容性上发生了矛盾他们往往捨弃兼容性而追求效率就这样如果他们认为某个系统调用实现的比较糟糕他们就会毫不犹豫的作出修改有些时候甚至连接口也一起改掉了更可怕的是很多时候他们对自己的修改连个招呼也不打在任何文档里都找不到关于修改的提示这样每当新内核推出的时候很可能都会悄悄的更新一些系统调用用户编制的应用程序也会跟着出错
说到这里你是不是感觉前途一片昏暗呢?呵呵不用太紧张如前面所说随着越来越多的人把Linux当成自己的饭碗不兼容的情况也越来越罕见从版本以后的Linux内核已经非常稳定了不过尽管如此你还是有必要在每个新内核推出之后对自己的应用程序进行兼容性测试以防止意外的发生
该如何学习使用Linux系统调用呢?
你可以用man 系统调用名称的命令来查看各条系统调用的介绍但这首先要求你要有不错的英语基础其次还得有一定的程序设计和系统编程的功底man pages不会涉及太多的应用细节因为它只是一个手册而非教程如果man pages所提