免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 799 | 回复: 0
打印 上一主题 下一主题

Linux系统调用 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2005-09-19 15:38 |只看该作者 |倒序浏览
摘要:本期重点和大家讨论系统调用机制。其中涉及到了一些及系统调用的性能、上下文深层问题,同时也穿插着讲述了一些内核调试方法。并且最后试验部分我们利用系统调用与相关内核服务完成了一个搜集系统调用序列的特定任务,该试验具有较强的实用和教学价值。
 
什么是系统调用
   顾
名思意,系统调用说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通
过文件系统相关的调用请求系统打开文件、关闭文件或读写文件,可以通过时钟相关的系统调用获得系统时间或设置系统时间等。
从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间。
系统服务之所以需要通过系统调用提供给用户空间的根本原因是为了对系统“保护”,因为我们知道Linux的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中,逻辑上相互隔离。所以用户进程在通常情况下不允许访问内核数据,也无法使用内核函数,它们只能在用户空间操作用户数据,调用户用空间函数。比如我们熟悉的“hello world”程序(执行时)就是标准的户空间进程,它使用的打印函数printf就属于用户空间函数,打印的字符“hello word”字符串也属于用户空间数据。

是很多情况下,用户进程需要获得系统服务(调用系统程序),这时就必须利用系统提供给用户的“特殊”接口——系统调用了,它的特殊性主要在于规定了用户进
程进入内核的具体位置;换句话说用户访问内核的路径是事先规定好的,只能从规定位置进入内核,而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限
制才能保证内核安全无虞。我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园,但你必须老老实实的坐在观光车上,按照规定的路线观
光游览。当然,不准下车,因为那样太危险,不是让你丢掉小命,就是让你吓坏了野生动物。
 
Linux的系统调用
     对于现代操作系统,系统调用是一种内核与用户空间通讯的普遍手段,Linux系统也不例外。但是Linux系统的系统调用相比很多Unix和windows等系统具有一些独特之处,无处不体现出Linux的设计精髓——简洁和高效。
     Linux系统调用很多地方继承了Unix的系统调用(但不是全部),但Linux相比传统Unix的系统调用做了很多扬弃,它省去了许多Unix系统冗余的系统调用,仅仅保留了最基本和最有用的系统调用,所以Linux全部系统调用只有250个左右(而有些操作系统系统调用多达1000个以上)。
这些系统调用按照功能逻辑大致可分为“进程控制”、“文件系统控制”、“系统控制”、“存管管理”、“网络管理”、“socket控制”、“用户管理”、“进程间通信”几类,详细情况可参阅文章
[color="#006699"]系统调用列表
如果你想详细看看系统调用的说明,可以使用man 2 syscalls 命令查看,或干脆到 内核源码目录>/include/asm-i386/unistd.h源文件种找到它们的原本。
熟练了解和掌握上面这些系统调用是对系统程序员的必备要求,但对于一个开发内核者或内核开发者来
[1]
说死记硬背下这些调用还远远不够。如果你仅仅知道存在的调用而不知道为什么它们会存在,或只知道如何使用调用而不知道这些调用在系统中的主要用途,那么你离驾驭系统还有不小距离。
要弥补这个鸿沟,第一,你必须明白系统调用在内核里的主要用途。虽然上面给出了数种分类,不过总的概括来讲系统调用主要在系统中的用途无非以下几类:
l        控制硬件——系统调用往往作为硬件资源和用户空间的抽象接口,比如读写文件时用到的write/read调用。
l        设置系统状态或读取内核数据——因为系统调用是用户空间和内核的唯一通讯手段
[2]
,所以用户设置系统状态,比如开/关某项内核服务(设置某个内核变量),或读取内核数据都必须通过系统调用。比如getpgid、getpriority、setpriority、sethostname
l        进程管理——一系列调用接口是用来保证系统中进程能以多任务,在虚拟内存环境下得以运行。比如 fork、clone、execve、exit等
第二,什么服务应该存在于内核;或者说什么功能应该实现在内核而不是在用户空间。这个问题并不没有明确的答案,有些服务你可以选择在内核完成,也可以在用户空间完成。选择在内核完成通常基于以下考虑:
l        服务必须获得内核数据,比如一些服务必须获得中断或系统时间等内核数据。
l        从安全角度考虑,在内核中提供的服务相比用户空间提供的毫无疑问更安全,很难被非法访问到。
l        从效率考虑,在内核实现服务避免了和用户空间来回传递数据以及保护现场等步骤,因此效率往往要比实现在用户空间高许多。比如,httpd等服务。
l        如果内核和用户空间都需要使用该服务,那么最好实现在内核空间,比如随机数产生。
   理解上述道理对掌握系统调用本质意义很大,希望网友们能从使用中多总结,多思考。
 
系统调用、用户编程接口(API)、系统命令、和内核函数的关系
系统调用并非直接和程序员或系统管理员打交道,它仅仅是一个通过软中断机制(我们后面讲述)向内核提交请求,获取内核服务的接口。而在实际使用中程序员调用的多是用户编程接口——API,而管理员使用的则多是系统命令。
用户编程接口其实是一个函数定义,说明了如何获得一个给定的服务,比如read()、malloc()、free()、abs()等。它有可能和系统调用形式上一致,比如read()接口就和read系统调用对应,但这种对应并非一一对应,往往会出现几种不同的API内部用到统一个系统调用,比如malloc()、free()内部利用brk( )系统调用来扩大或缩小进程的堆;或一个API利用了好几个系统调用组合完成服务。更有些API甚至不需要任何系统调用——因为它不必需要内核服务,如计算整数绝对值的abs()接口。
另外要补充的是Linux的用户编程接口遵循了在Unix世界中最流行的应用编程界面标准——POSIX标准,这套标准定义了一系列API。在Linux中(Unix也如此)这些API主要是通过C库(libc)实现的,它除了定义的一些标准的C函数外,一个很重要的任务就是提供了一套封装例程(wrapper routine)将系统调用在用户空间包装后供用户编程使用。
不过封装并非必须的,如果你愿意直接调用,Linux内核也提供了一个syscall()函数来实现调用,我们看个例子来对比一下通过C库调用和直接调用的区别。
 
#include
#include
#include
#include
int main(void) {
long ID1, ID2;
/*-----------------------------*/
/* 直接系统调用*/
/* SYS_getpid (func no. is 20) */
/*-----------------------------*/
ID1 = syscall(SYS_getpid);
printf ("syscall(SYS_getpid)=%ld
", ID1);
/*-----------------------------*/
/* 使用"libc"封装的系统调用 */
/* SYS_getpid (Func No. is 20) */
/*-----------------------------*/
ID2 = getpid();
printf ("getpid()=%ld
", ID2);
return(0);
}
 
系统命令相对编程接口更高了一层,它是内部引用API的可执行程序,比如我们常用的系统命令ls、hostname等。Linux的系统命令格式遵循系统V的传统,多数放在/bin和/sbin下(相关内容可看看shell等章节)。
有兴趣的话可以通过strace ls或strace hostname 命令查看一下它们用到的系统调用,你会发现诸如open、brk、fstat、ioctl 等系统调用被用在系统命令中。
下一个需要解释一下的问题是内核函数和系统调用的关系,内核函数大家不要想像的过于复杂,其实它们和普通函数很像,只不过在内核实现,因此要满足一些内核编程的要求
[color="#006699"][3]
。系统调用是一层用户进入内核的接口,它本身并非内核函数,进入内核后,不同的系统调用会找到对应到各自的内核函数——换个专业说法就叫:系统调用服务服务例程。实际对请求服务的是内核函数而非调用接口。
比如系统调用 getpid实际就是调用内核函数sys_getpid。
asmlinkage long sys_getpid(void)
{
       return current->tpid;
}
Linux系统种存在许多的内核函数,有些是内核文件种自己使用的,有些则是可以export出来供内核其他部分共同使用的,具体情况自己决定。
内核公开的内核函数——export出来的——可以使用命令ksyms 或 cat /proc/ksyms来查看。另外网上还有一本归纳分类内核函数的书叫作《The Linux Kernel API Book》,有兴趣的读者可以去看看。
    总而言之,从用户角度向内核看,依次是系统命令、编程接口、系统调用和内核函数。再讲述了系统调用实现后,我们会回过头来看看整个执行路径。
系统调用实现
Linux中实现系统调用利用了0x86体系结构中的软件中断
[color="#006699"][4]
。软件中断和我们常说的中断(硬件中断)不同之处在于——它是通过软件指令触发而并非外设,也就是说又编程人员出发的一种异常,具体的讲就是调用int $0x80汇编指令,这条汇编指令将产生向量为128的编程异常。
之所以系统调用需要借助异常实现,是因为当用户态的进程调用一个系统调用时,CPU便被切换到内核态执行内核函数
[5]
,而我们在i386体系结构部分已经讲述过了进入内核——进入高特权级别——必须经过系统的门机制,这里异常实际上就是通过系统门陷入内核(除了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上,这个传递动作是通过在执行int0x80前把调用号装入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宏把这些寄存器的值保存在内核态堆栈中。
 
 
有始便有终,当服务例程结束时,system_call( ) 从eax获得系统调用的返回值,并把这个返回值存放在曾保存用户态 eax寄存器栈单元的那个位置上。然后跳转到ret_from_sys_call( ),终止系统调用处理程序的执行。
当进程恢复它在用户态的执行前,RESTORE_ALL宏会恢复用户进入内核前被保留到堆栈中的寄存器值。其中eax返回时会带回系统调用的返回码。(负数说明调用错误,0或正数说明正常完成)
 
我们可以通过分析一下getpid系统调用的真是过程来将上述概念具体化,分析getpid系统调用一个办法是查看entry.s中的代码细节,逐步跟踪源码来分析运行过程,另外就是可借助一些内核调试工具,动态跟踪运行路径。
假设我们的程序源文件名为getpid.c,内容是:
#include
#include
#include
#include
int main(void) {
long ID;
ID = getpid();
printf ("getpid()=%ld
", ID);
return(0);
}
将其编译成名为getpid的执行文件”gcc –o getpid 路径>/getpid.c”, 我们使用KDB来产看它进入内核后的执行路径。
l         激活KDB (按下pause键,当然你必须已经给内核打了KDB补丁);设置内核断点 “bp sys_getpid” ;退出kdb “go”;然后执行./getpid 。瞬间,进入内核调试状态,执行路径停止在断点sys_getpid处。
l         在KDB>提示符下,执行bt命令观察堆栈,发现调用的嵌套路径,可以看到在sys_getpid是在内核函数system_call中被嵌套调用的。
l         在KDB>提示符下,执行rd命令查看寄存器中的数值,可以看到eax中存放的getpid调用号——0x00000014(=20).
l         在KDB>提示符下,执行ssb(或ss)命令跟踪内核代码执行路径,可以发现sys_getpid执行后,会返回system_call函数,然后接者转入ret_from_sys_call例程。(再往后还有些和调度有关其他例程,我们这里不说了它们了。)
 
结合用户空间的执行路径,大致该程序可归结为一下几个步骤:
1  该程序调用libc库的封装函数getpid。该封装函数中将系统调用号_NR_getpid(第20个)压入EAX寄存器,
2  调用软中断 int 0x80 进入内核。
(以下进入内核态)
3  在内核中首先执行system_call,接着执行根据系统调用号在调用表中查找到对应的系统调用服务例程sys_getpid。
4.执行sys_getpid服务例程。
5.执行完毕后,转入ret_from_sys_call例程,系统调用中返回。
 
   内核调试是一个很有趣的话题,方法多种多样,我个人认为比较好用的是UML(user mode linux+gdb)和 KDB 这两个工具。尤其KDB对于调试小规模内核模块或查看内核运行路径很有效,对于它的使用方法可以看看
[color="#006699"]Linux 内核调试器内幕
这片文章。
系统调用思考
    系统调用的内在过程并不复杂,我们不再多说了,下面这节我们主要就系统调用所涉及的一些重要问题作一些讨论和分析,希望这样能更有助了解系统调用的精髓。
调用上下文分析
系统调用虽说是要进入内核执行,但它并非一个纯粹意义上的内核例程。首先它是代表用户进程的,这点决定了虽然它会陷入内核执行,但是上下文仍然是处于进程上下文中,因此可以访问进程的许多信息(比如current结构——当前进程的控制结构),而且可以被其他进程抢占(在从系统调用返回时,由system_call函数判断是否该再调度),可以休眠,还可接收信号
[color="#006699"][6]
等等。

有这些特点都涉及到了进程调度的问题,我们这里不做深究,只要大家明白系统调用完成后,再回到或者说把控制权交回到发起调用的用户进程前,内核会有一次调
度。如果发现有优先级别更高的进程或当前进程的时间片用完,那么就会选择高优先级的进程或重新选择进程运行。除了再调度需要考虑外,再就是内核需要检查是
否有挂起的信号,如果发现当前进程有挂起的信号,那么还需要先返回用户空间处理信号处理例程(处于用户空间),然后再回到内核,重新返回用户空间,有些麻
烦但这个反复过程是必须的。
 
调用性能问题
系统调用需要从用户空间陷入内核空间,处理完后,又需要返回用户空间。其中除了系统调用服务例程的实际耗时外,陷入/返回过程和系统调用处理程序(查系统调用表、存储恢复用户现场)也需要花销一些时间,这些时间加起来就是一个系统调用的响应速度。系统调用不比别的用户程序,它对性能要求很苛刻,因为它需要陷入内核执行,所以和其他内核程序一样要求代码简洁、执行迅速。幸好Linux具有令人难以置信的上下文切换速度,使得其进出内核都被优化得简洁高效;同时所有<span lang="EN-US" style="font-

本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/9747/showart_48318.html
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP