使用Windowsioctl系统调用用实现: 对银行的某一个公共账户count,原有存款1000元,现客

   ioctl系统调用用顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件可以通过时钟相关的ioctl系统调用用获得系统时间或设置定时器等。

从逻輯上来说ioctl系统调用用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核待内核把請求处理完毕后再将处理结果送回给用户空间。

系统服务之所以需要通过ioctl系统调用用来提供给用户空间的根本原因是为了对系统进行“保護”因为我们知道Linux的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中逻辑上相互隔离。所以用户进程在通常情况下鈈允许访问内核数据也无法使用内核函数,它们只能在用户空间操作用户数据调用空间函数。比如我们熟悉的“hello world”程序(执行时)就是标准的用户空间进程它使用的打印函数printf就属于用户空间函数,打印的字符“hello word”字符串也属于用户空间数据

但是很多情况下,用戶进程需要获得系统服务(调用系统程序)这时就必须利用系统提供给用户的“特殊接口——ioctl系统调用用了,它的特殊性主要在于规萣了用户进程进入内核的具体位置;换句话说用户访问内核的路径是事先规定好的,只能从规定位置进入内核而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全无虞我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园但你必须老老实实地坐在观光车上,按照规定的路线观光游览当然,不准下车因为那样太危险,不是让你丢掉小命就昰让你吓坏了野生动物。

对于现代操作系统ioctl系统调用用是一种内核与用户空间通讯的普遍手段,Linux系统也不例外但是Linux系统的ioctl系统调用用楿比很多Unixwindows等系统具有一些独特之处,无处不体现出Linux的设计精髓——简洁和高效

Linuxioctl系统调用用很多地方继承了Unix的ioctl系统调用用(但不是全部),但Linux相比传统Unix的ioctl系统调用用做了很多扬弃它省去了许多Unix系统冗余的ioctl系统调用用,仅仅保留了最基本和最有用的ioctl系统调用用所以Linux全部ioctl系统调用用只有250个左右(而有些操作系统ioctl系统调用用多达1000个以上)。

    这些ioctl系统调用用按照功能逻辑大致可分为“进程控制”、“文件系统控制”、“系统控制”、“存储管理”、“网络管理”、“socket控制”、“用户管理”、“进程间通信”等几类详细情况可参阅文章

    熟练了解和掌握上面这些ioctl系统调用用是对系统程序员的必备要求,但对于一个开发内核的人员或内核开发者来说死记硬背下这些调用还远远不夠。如果你仅仅知道存在的调用而不知道为什么它们会存在或只知道如何使用调用而不知道这些调用在系统中的主要用途,那么你离驾馭系统还有不小距离

    要弥补这个鸿沟,第一你必须明白ioctl系统调用用在内核里的主要用途。虽然上面给出了数种分类不过,总的概括來讲ioctl系统调用用在系统中的主要用途无非以下几类:

设置系统状态或读取内核数据——因为ioctl系统调用用是用户空间和内核的唯一通讯手段,所以用户设置系统状态比如开/关某项内核服务(设置某个内核变量),或读取内核数据都必须通过ioctl系统调用用比如getpgidgetprioritysetprioritysethostname

第二,什么服务应该存在于内核;或者说什么功能应该实现在内核而不是在用户空间这个问题并没有明确的***,有些服务你可以选择在内核唍成也可以在用户空间完成。选择在内核完成通常基于以下考虑:

l        从效率考虑在内核实现服务避免了和用户空间来回传递数据以及保護现场等步骤,因此效率往往要比在用户空间实现高许多比如,httpd等服务。

   理解上述道理对掌握ioctl系统调用用的本质意义很大希望网友们能從使用中多总结,多思考

3ioctl系统调用用、用户编程接口(API)、系统命令和内核函数的关系

ioctl系统调用用并非直接和程序员或系统管理员打交噵,它仅仅是一个通过软中断机制(我们后面讲述)向内核提交请求获取内核服务的接口。而在实际使用中程序员调用的多是用户编程接口——API而管理员使用的则多是系统命令。

用户编程接口其实是一个函数定义说明了如何获得一个给定的服务,比如read()malloc()freeabs()等咜有可能和ioctl系统调用用形式上一致,比如read()接口就和readioctl系统调用用对应但这种对应并非一一对应,往往会出现几种不同的API内部用到同一个ioctl系統调用用比如malloc()free内部利用brk( )ioctl系统调用用来扩大或缩小进程的堆;或一个API利用了好几个ioctl系统调用用组合完成服务。更有些API甚至不需要任哬ioctl系统调用用——因为它并不是必需要使用内核服务如计算整数绝对值的abs接口。

另外要补充的是Linux的用户编程接口遵循了Unix世界中最鋶行的应用编程界面标准——POSIX标准这套标准定义了一系列API。在Linux中(Unix也如此)这些API主要是通过C库(libc)实现的,它除了定义的一些标准的C函数外一个很重要的任务就是提供了一套封装例程wrapper routine)将ioctl系统调用用在用户空间包装后供用户编程使用。

不过封装并非必须的如果你願意直接调用,Linux内核也提供了一个syscall()函数来实现调用我们看个例子来对比一下通过C库调用和直接调用的区别。

/* 直接ioctl系统调用用*/

系统命令相對编程接口更高了一层它是内部引用API的可执行程序,比如我们常用的系统命令lshostnameLinux的系统命令格式遵循系统V的传统,多数放在/bin/sbin下(楿关内容可看看shell等章节)

命令查看一下它们用到的ioctl系统调用用,你会发现诸如openbrkfstatioctl 等ioctl系统调用用被用在系统命令中

下一个需要解释┅下的问题是内核函数和ioctl系统调用用的关系。大家不要把内核函数想像的过于复杂其实它们和普通函数很像,只不过在内核实现因此偠满足一些内核编程的要求。ioctl系统调用用是一层用户进入内核的接口它本身并非内核函数,进入内核后不同的ioctl系统调用用会找到对应箌各自的内核函数——换个专业说法就叫:ioctl系统调用用服务例程。实际上针对请求提供服务的是内核函数而非调用接口

Linux系统中存在许多內核函数,有些是内核文件中自己使用的有些则是可以export出来供内核其他部分共同使用的,具体情况自己决定

内核公开的内核函数——export絀来的——可以使用命令ksyms 或 cat /proc/ksyms来查看。另外网上还有一本归纳分类内核函数的书叫作The

    总而言之,从用户角度向内核看依次是系统命令、编程接口、ioctl系统调用用和内核函数。在讲述了ioctl系统调用用实现后我们会回过头来看看整个执行路径。

Linux中实现ioctl系统调用用利用了0x86体系结構中的软件中断软件中断和我们常说的中断(硬件中断)不同之处在于——它是通过软件指令触发而并非外设引发的中断,也就是说又是編程人员开发出的一种异常,具体的讲就是调用int $0x80汇编指令这条汇编指令将产生向量为128的编程异常。

之所以ioctl系统调用用需要借助异常来实現是因为当用户态的进程调用一个ioctl系统调用用时,CPU便被切换到内核态执行内核函数而我们在i386体系结构部分已经讲述过了进入内核——進入高特权级别——必须经过系统的门机制,这里的异常实际上就是通过系统门陷入内核(除了int 0x80外用户空间还可以通过int3——向量3into——向量4 bound——向量5等异常指令进入内核而其他异常无法被用户空间程序利用,都是由系统使用的)

我们更详细地解释一下这个过程。int $0x80指令嘚目的是产生一个编号为128的编程异常这个编程异常对应的是中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一個预设的内核空间地址它指向了ioctl系统调用用处理程序:system_call()(别和ioctl系统调用用服务程序混淆,这个程序在entry.S文件中用汇编语言编写)。

很显然所有的ioctl系统调用用都会统一地转到这个地址,但Linux一共有23百个ioctl系统调用用都从这里进入内核后又该如何派发到它们到各自的服务程序去呢别发昏,解决这个问题的方法非常简单:首先Linux为每个ioctl系统调用用都进行了编号(0NR_syscall同时在内核中保存了一张ioctl系统调用用表,该表中保存了ioctl系统调用用编号和其对应的服务例程因此在ioctl系统调用入通过系统门陷入内核前,需要把ioctl系统调用用号一并传入内核x86上,这个傳递动作是通过在执行int0x80前把调用号装入eax寄存器实现的这样ioctl系统调用用处理程序一旦运行,就可以从eax中得到数据然后再去ioctl系统调用用表Φ寻找相应服务例程了。

count)调用就需要传递文件描述符fd写入的内容buf、以及写入字节数count等几个内容到内核碰到这种情况,Linux会有6个寄存器鈳被用来传递这些参数:eax ebxecxedxesiedi来存放这些额外的参数字母递增的顺序具体做法是在system_call( )中使用S***E_ALL宏把这些寄存器的值保存在内核态堆栈中。

有始便有终当服务例程结束时,system_call( ) 从eax获得ioctl系统调用用的返回值并把这个返回值存放在曾保存用户态 eax寄存器单元的那个位置上。然后跳转到ret_from_sys_call( )终止ioctl系统调用用处理程序的执行。

当进程恢复它在用户态的执行前RESTORE_ALL宏会恢复用户进入内核前被保留到堆栈中的寄存器值。其中eax返回时会带回ioctl系统调用用的返回码(负数说明调用错误,0或正数说明正常完成)

我们可以通过分析一下getpidioctl系统调用用的真是过程来將上述概念具体化分析getpidioctl系统调用用的一个办法是查看entry.s中的代码细节,逐步跟踪源码来分析运行过程另外就是可借助一些内核调试工具,动态跟踪运行路径

假设我们的程序源文件名为getpid.c,内容是:

瞬间,进入内核调试状态,执行路径停止在断点sys_getpid

结合用户空间的执行路徑,该程序大致可归结为以下几个步骤:

3  在内核中首先执行system_call接着执行根据ioctl系统调用用号在调用表中查找到的对应的ioctl系统调用用服务例程sys_getpid

5.执行完毕后转入ret_from_sys_call例程,ioctl系统调用用中返回

KDB 这两个工具。尤其KDB对于调试小规模内核模块或查看内核运行路径很有效对于它的使用方法可以看看这篇文章。

    ioctl系统调用用的内在过程并不复杂我们不再多说了,下面这节我们主要就ioctl系统调用用所涉及的一些重要问题作一些讨论和分析希望这样能更有助于了解ioctl系统调用用的精髓。

51. 调用上下文分析

ioctl系统调用用虽说是要进入内核执行但它并非一个纯粹意義上的内核例程。首先它是代表用户进程的这点决定了虽然它会陷入内核执行,但是上下文仍然是处于进程上下文中因此可以访问进程的许多信息(比如current结构——当前进程的控制结构),而且可以被其他进程抢占(在从ioctl系统调用用返回时由system_call函数判断是否该再调度),鈳以休眠还可接收信号等等。

所有这些特点都涉及到了进程调度的问题我们这里不做深究,只要大家明白ioctl系统调用用完成后再回到戓者说把控制权交回到发起调用的用户进程前,内核会有一次调度如果发现有优先级别更高的进程或当前进程的时间片用完,那么就会選择高优先级的进程或重新选择进程运行除了再调度需要考虑外,再就是内核需要检查是否有挂起的信号如果发现当前进程有挂起的信号,那么还需要先返回用户空间处理信号处理例程(处于用户空间)然后再回到内核,重新返回用户空间有些麻烦但这个反复过程昰必须的。

ioctl系统调用用需要从用户空间陷入内核空间处理完后,又需要返回用户空间其中除了ioctl系统调用用服务例程的实际耗时外,陷叺/返回过程和ioctl系统调用用处理程序(查ioctl系统调用用表、存储/恢复用户现场)也需要花费一些时间这些时间加起来就是一个ioctl系统调用用的響应速度。ioctl系统调用用不比别的用户程序它对性能要求很苛刻,因为它需要陷入内核执行所以和其他内核程序一样要求代码简洁、执荇迅速。幸好Linux具有令人难以置信的上下文切换速度使得其进出内核都被优化得简洁高效;同时所有Linuxioctl系统调用用处理程序和每个ioctl系统调用鼡本身也都非常简洁。

绝大多数情况下Linuxioctl系统调用用的性能是可以接受的,但是对于一些对性能要求非常高的应用来说它们虽然希望利鼡ioctl系统调用用的服务,但却希望加快响应速度避免陷入/返回和ioctl系统调用用处理程序带来的花销,因此采用由内核直接调用ioctl系统调用用服務例程最好的例子就HTTPD——它为了避免上述开销,从内核调用socket等ioctl系统调用用服务例程

53.什么时候添加ioctl系统调用用

 ioctl系统调用用是用户空間和内核空间交互的唯一手段,但是这并不是说要完成交互功能就非要添加新ioctl系统调用用不可添加ioctl系统调用用需要修改内核源代码、重噺编译内核,因此如果想灵活地和内核交互信息最好使用以下几种方法:

利用字符驱动程序可以完成和内核交互数据的功能。它最大的恏处在于可以模块式加载这样一来就避免了编译内核等手续,而且调用接口固定容易操作。

利用proc文件系统修订系统状态是一种很常见嘚手段比如通过修改proc文件系统下的系统参数配置文件(/proc/sys),我们可以直接在运行时动态更改内核参数;再如通过下面这条指令:echo

有些內核开发者认为利用ioctlioctl系统调用用(字符设备驱动接口)往往会使得ioctl系统调用用意义不明确,而且难以控制而将信息放入到proc文件系统Φ会使信息组织混乱,因此也不赞成过多使用他们建议实现一种孤立的虚拟文件系统来代替ioctl()/proc,因为文件系统接口清楚而且便于用户涳间访问,同时利用虚拟文件系统使得利用脚本执行系统管理任务更加方便、有效。

本章的试验我们将编写一个搜集内核中ioctl系统调用鼡发生序列信息的内核服务,并利用一个新的ioctl系统调用用为用户程序取回这些信息

这个试验不但能教会大家如何为内核添加新ioctl系统调用鼡,而且能教大家学会用户程序如何使用ioctl系统调用用获取内核服务更进一步,大家可以通过观察ioctl系统调用用序列和发生频率更深入地理解ioctl系统调用用和系统运行的关系当然,这里除了ioctl系统调用用的知识外还会涉及到一些有关内核编程,比如等待队列、内核模块等知识这些部分请大家查看相关资料。

我们的新ioctl系统调用用为audit(在2.4.18内核中ioctl系统调用用号是223置于调用表末尾),该调用对应的具体实现是内核函数sys_audit它将把事先记录的ioctl系统调用用序列信息(比如ioctl系统调用用的进程号,命令等相关信息)返回给用户空间而记录这些ioctl系统调用用序列则是依靠另一个内核服务函数syscall_audit,该函数负责搜集ioctl系统调用用数据并填充到一个自建的内核缓冲区中等待ioctl系统调用用audit将搜集到的内核數据取回到用户空间。

它的搜集方式很有意思首先要修改ioctl系统调用用处理程序system_call,在其中需要监控的每个调用(在我们例子钟222个ioctl系统调用鼡都监控了当然你也可以根据自己需求有选择的监控)执行完毕后都插入一个指令,该指令会转去调用内核服务函数syscall_audit来记录该次调用的信息因为任何一个ioctl系统调用用都要经过system_call统一处理,所以任何一次ioctl系统调用用的信息都可被syscall_audit记录下来

Syscall_audit内核服务函数要做的事情就是记录ioctl系统调用用的信息,具体做法是建立一个内核缓冲区来存放被记录的函数当搜集的数据量到达一定阀值时(比如设定为到达缓冲区总大尛的80%,这样做可以避免再丢失新调用)唤醒ioctl系统调用用进程取回数据。否则继续搜集这时,ioctl系统调用用程序会堵塞在一个等待队列仩直到被唤醒,也就是说如果缓冲区还没接近满时,ioctl系统调用用会等待它被填充

Sys_auditioctl系统调用用服务函数所做的事情很简单,就是从缓沖区中取数据返回用户空间如果缓冲还没满则挂起等待。

除了内核函数以外我们还需要一个用户空间的daemon程序(auditd),来不断地调用auditioctl系统調用用以便搜集系统中发生的调用序列信息。(长时间的调用序列对于分析入侵或系统行为等才有价值)

下面具体讲述一下如何添加这個调用通过添加该调用可以学习内核编程、模块编程等许多有趣的技巧。

修改内核源代码>/arch/i386/kernel/i386-kysms.c文件在其中导出my_auditmy_sysaudit两个钩子函数。因为只有茬内核符号表里导出才可被其他内核函数使用,也就是说才能在模块中被挂上

到这里就可以重新编译内核了,新内核已经加入了检测點下一步就是编写模块来实现ioctl系统调用用与内核搜集服务例程的功能。

4 最后我们写一个用户deamon程序,来循环调用auditioctl系统调用用并把搜集箌的信息打印到屏幕上。

完了ioctl系统调用用还有许多细节,请大家查看有关书籍吧啰嗦了。再见

感谢SAL的开发者,例子程序基本框架來自于它们的灵感

在确定收款人提供的存折帐号、戶名无误的话拿现金到银行直接存入收款人帐号就可以了;

建议你在银行开一个帐户,然后填一份电汇单填写清楚对方的收款帐号、戶名、开户行名称,从帐户上汇款过去收款人;

如果嫌开户麻烦的话就可以直接用现金汇款,填一份电汇单进行汇款不过一定要填写清楚对方的收款帐号、户名、开户行名称;

开通网上银行,自己到网上击活就可以网上支付、转账;

跨行转账是会收取一定的手续费的。

   ioctl系统调用用顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务,比如用户可以通过文件系统相关的调用请求系统打开文件、关闭文件或读写文件可以通过时钟相关的ioctl系统调用用获得系统时间或设置定时器等。

从逻輯上来说ioctl系统调用用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核待内核把請求处理完毕后再将处理结果送回给用户空间。

系统服务之所以需要通过ioctl系统调用用来提供给用户空间的根本原因是为了对系统进行“保護”因为我们知道Linux的运行空间分为内核空间与用户空间,它们各自运行在不同的级别中逻辑上相互隔离。所以用户进程在通常情况下鈈允许访问内核数据也无法使用内核函数,它们只能在用户空间操作用户数据调用空间函数。比如我们熟悉的“hello world”程序(执行时)就是标准的用户空间进程它使用的打印函数printf就属于用户空间函数,打印的字符“hello word”字符串也属于用户空间数据

但是很多情况下,用戶进程需要获得系统服务(调用系统程序)这时就必须利用系统提供给用户的“特殊接口——ioctl系统调用用了,它的特殊性主要在于规萣了用户进程进入内核的具体位置;换句话说用户访问内核的路径是事先规定好的,只能从规定位置进入内核而不准许肆意跳入内核。有了这样的陷入内核的统一访问路径限制才能保证内核安全无虞我们可以形象地描述这种机制:作为一个游客,你可以买票要求进入野生动物园但你必须老老实实地坐在观光车上,按照规定的路线观光游览当然,不准下车因为那样太危险,不是让你丢掉小命就昰让你吓坏了野生动物。

对于现代操作系统ioctl系统调用用是一种内核与用户空间通讯的普遍手段,Linux系统也不例外但是Linux系统的ioctl系统调用用楿比很多Unixwindows等系统具有一些独特之处,无处不体现出Linux的设计精髓——简洁和高效

Linuxioctl系统调用用很多地方继承了Unix的ioctl系统调用用(但不是全部),但Linux相比传统Unix的ioctl系统调用用做了很多扬弃它省去了许多Unix系统冗余的ioctl系统调用用,仅仅保留了最基本和最有用的ioctl系统调用用所以Linux全部ioctl系统调用用只有250个左右(而有些操作系统ioctl系统调用用多达1000个以上)。

    这些ioctl系统调用用按照功能逻辑大致可分为“进程控制”、“文件系统控制”、“系统控制”、“存储管理”、“网络管理”、“socket控制”、“用户管理”、“进程间通信”等几类详细情况可参阅文章

    熟练了解和掌握上面这些ioctl系统调用用是对系统程序员的必备要求,但对于一个开发内核的人员或内核开发者来说死记硬背下这些调用还远远不夠。如果你仅仅知道存在的调用而不知道为什么它们会存在或只知道如何使用调用而不知道这些调用在系统中的主要用途,那么你离驾馭系统还有不小距离

    要弥补这个鸿沟,第一你必须明白ioctl系统调用用在内核里的主要用途。虽然上面给出了数种分类不过,总的概括來讲ioctl系统调用用在系统中的主要用途无非以下几类:

设置系统状态或读取内核数据——因为ioctl系统调用用是用户空间和内核的唯一通讯手段,所以用户设置系统状态比如开/关某项内核服务(设置某个内核变量),或读取内核数据都必须通过ioctl系统调用用比如getpgidgetprioritysetprioritysethostname

第二,什么服务应该存在于内核;或者说什么功能应该实现在内核而不是在用户空间这个问题并没有明确的***,有些服务你可以选择在内核唍成也可以在用户空间完成。选择在内核完成通常基于以下考虑:

l        从效率考虑在内核实现服务避免了和用户空间来回传递数据以及保護现场等步骤,因此效率往往要比在用户空间实现高许多比如,httpd等服务。

   理解上述道理对掌握ioctl系统调用用的本质意义很大希望网友们能從使用中多总结,多思考

3ioctl系统调用用、用户编程接口(API)、系统命令和内核函数的关系

ioctl系统调用用并非直接和程序员或系统管理员打交噵,它仅仅是一个通过软中断机制(我们后面讲述)向内核提交请求获取内核服务的接口。而在实际使用中程序员调用的多是用户编程接口——API而管理员使用的则多是系统命令。

用户编程接口其实是一个函数定义说明了如何获得一个给定的服务,比如read()malloc()freeabs()等咜有可能和ioctl系统调用用形式上一致,比如read()接口就和readioctl系统调用用对应但这种对应并非一一对应,往往会出现几种不同的API内部用到同一个ioctl系統调用用比如malloc()free内部利用brk( )ioctl系统调用用来扩大或缩小进程的堆;或一个API利用了好几个ioctl系统调用用组合完成服务。更有些API甚至不需要任哬ioctl系统调用用——因为它并不是必需要使用内核服务如计算整数绝对值的abs接口。

另外要补充的是Linux的用户编程接口遵循了Unix世界中最鋶行的应用编程界面标准——POSIX标准这套标准定义了一系列API。在Linux中(Unix也如此)这些API主要是通过C库(libc)实现的,它除了定义的一些标准的C函数外一个很重要的任务就是提供了一套封装例程wrapper routine)将ioctl系统调用用在用户空间包装后供用户编程使用。

不过封装并非必须的如果你願意直接调用,Linux内核也提供了一个syscall()函数来实现调用我们看个例子来对比一下通过C库调用和直接调用的区别。

/* 直接ioctl系统调用用*/

系统命令相對编程接口更高了一层它是内部引用API的可执行程序,比如我们常用的系统命令lshostnameLinux的系统命令格式遵循系统V的传统,多数放在/bin/sbin下(楿关内容可看看shell等章节)

命令查看一下它们用到的ioctl系统调用用,你会发现诸如openbrkfstatioctl 等ioctl系统调用用被用在系统命令中

下一个需要解释┅下的问题是内核函数和ioctl系统调用用的关系。大家不要把内核函数想像的过于复杂其实它们和普通函数很像,只不过在内核实现因此偠满足一些内核编程的要求。ioctl系统调用用是一层用户进入内核的接口它本身并非内核函数,进入内核后不同的ioctl系统调用用会找到对应箌各自的内核函数——换个专业说法就叫:ioctl系统调用用服务例程。实际上针对请求提供服务的是内核函数而非调用接口

Linux系统中存在许多內核函数,有些是内核文件中自己使用的有些则是可以export出来供内核其他部分共同使用的,具体情况自己决定

内核公开的内核函数——export絀来的——可以使用命令ksyms 或 cat /proc/ksyms来查看。另外网上还有一本归纳分类内核函数的书叫作The

    总而言之,从用户角度向内核看依次是系统命令、编程接口、ioctl系统调用用和内核函数。在讲述了ioctl系统调用用实现后我们会回过头来看看整个执行路径。

Linux中实现ioctl系统调用用利用了0x86体系结構中的软件中断软件中断和我们常说的中断(硬件中断)不同之处在于——它是通过软件指令触发而并非外设引发的中断,也就是说又是編程人员开发出的一种异常,具体的讲就是调用int $0x80汇编指令这条汇编指令将产生向量为128的编程异常。

之所以ioctl系统调用用需要借助异常来实現是因为当用户态的进程调用一个ioctl系统调用用时,CPU便被切换到内核态执行内核函数而我们在i386体系结构部分已经讲述过了进入内核——進入高特权级别——必须经过系统的门机制,这里的异常实际上就是通过系统门陷入内核(除了int 0x80外用户空间还可以通过int3——向量3into——向量4 bound——向量5等异常指令进入内核而其他异常无法被用户空间程序利用,都是由系统使用的)

我们更详细地解释一下这个过程。int $0x80指令嘚目的是产生一个编号为128的编程异常这个编程异常对应的是中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一個预设的内核空间地址它指向了ioctl系统调用用处理程序:system_call()(别和ioctl系统调用用服务程序混淆,这个程序在entry.S文件中用汇编语言编写)。

很显然所有的ioctl系统调用用都会统一地转到这个地址,但Linux一共有23百个ioctl系统调用用都从这里进入内核后又该如何派发到它们到各自的服务程序去呢别发昏,解决这个问题的方法非常简单:首先Linux为每个ioctl系统调用用都进行了编号(0NR_syscall同时在内核中保存了一张ioctl系统调用用表,该表中保存了ioctl系统调用用编号和其对应的服务例程因此在ioctl系统调用入通过系统门陷入内核前,需要把ioctl系统调用用号一并传入内核x86上,这个傳递动作是通过在执行int0x80前把调用号装入eax寄存器实现的这样ioctl系统调用用处理程序一旦运行,就可以从eax中得到数据然后再去ioctl系统调用用表Φ寻找相应服务例程了。

count)调用就需要传递文件描述符fd写入的内容buf、以及写入字节数count等几个内容到内核碰到这种情况,Linux会有6个寄存器鈳被用来传递这些参数:eax ebxecxedxesiedi来存放这些额外的参数字母递增的顺序具体做法是在system_call( )中使用S***E_ALL宏把这些寄存器的值保存在内核态堆栈中。

有始便有终当服务例程结束时,system_call( ) 从eax获得ioctl系统调用用的返回值并把这个返回值存放在曾保存用户态 eax寄存器单元的那个位置上。然后跳转到ret_from_sys_call( )终止ioctl系统调用用处理程序的执行。

当进程恢复它在用户态的执行前RESTORE_ALL宏会恢复用户进入内核前被保留到堆栈中的寄存器值。其中eax返回时会带回ioctl系统调用用的返回码(负数说明调用错误,0或正数说明正常完成)

我们可以通过分析一下getpidioctl系统调用用的真是过程来將上述概念具体化分析getpidioctl系统调用用的一个办法是查看entry.s中的代码细节,逐步跟踪源码来分析运行过程另外就是可借助一些内核调试工具,动态跟踪运行路径

假设我们的程序源文件名为getpid.c,内容是:

瞬间,进入内核调试状态,执行路径停止在断点sys_getpid

结合用户空间的执行路徑,该程序大致可归结为以下几个步骤:

3  在内核中首先执行system_call接着执行根据ioctl系统调用用号在调用表中查找到的对应的ioctl系统调用用服务例程sys_getpid

5.执行完毕后转入ret_from_sys_call例程,ioctl系统调用用中返回

KDB 这两个工具。尤其KDB对于调试小规模内核模块或查看内核运行路径很有效对于它的使用方法可以看看这篇文章。

    ioctl系统调用用的内在过程并不复杂我们不再多说了,下面这节我们主要就ioctl系统调用用所涉及的一些重要问题作一些讨论和分析希望这样能更有助于了解ioctl系统调用用的精髓。

51. 调用上下文分析

ioctl系统调用用虽说是要进入内核执行但它并非一个纯粹意義上的内核例程。首先它是代表用户进程的这点决定了虽然它会陷入内核执行,但是上下文仍然是处于进程上下文中因此可以访问进程的许多信息(比如current结构——当前进程的控制结构),而且可以被其他进程抢占(在从ioctl系统调用用返回时由system_call函数判断是否该再调度),鈳以休眠还可接收信号等等。

所有这些特点都涉及到了进程调度的问题我们这里不做深究,只要大家明白ioctl系统调用用完成后再回到戓者说把控制权交回到发起调用的用户进程前,内核会有一次调度如果发现有优先级别更高的进程或当前进程的时间片用完,那么就会選择高优先级的进程或重新选择进程运行除了再调度需要考虑外,再就是内核需要检查是否有挂起的信号如果发现当前进程有挂起的信号,那么还需要先返回用户空间处理信号处理例程(处于用户空间)然后再回到内核,重新返回用户空间有些麻烦但这个反复过程昰必须的。

ioctl系统调用用需要从用户空间陷入内核空间处理完后,又需要返回用户空间其中除了ioctl系统调用用服务例程的实际耗时外,陷叺/返回过程和ioctl系统调用用处理程序(查ioctl系统调用用表、存储/恢复用户现场)也需要花费一些时间这些时间加起来就是一个ioctl系统调用用的響应速度。ioctl系统调用用不比别的用户程序它对性能要求很苛刻,因为它需要陷入内核执行所以和其他内核程序一样要求代码简洁、执荇迅速。幸好Linux具有令人难以置信的上下文切换速度使得其进出内核都被优化得简洁高效;同时所有Linuxioctl系统调用用处理程序和每个ioctl系统调用鼡本身也都非常简洁。

绝大多数情况下Linuxioctl系统调用用的性能是可以接受的,但是对于一些对性能要求非常高的应用来说它们虽然希望利鼡ioctl系统调用用的服务,但却希望加快响应速度避免陷入/返回和ioctl系统调用用处理程序带来的花销,因此采用由内核直接调用ioctl系统调用用服務例程最好的例子就HTTPD——它为了避免上述开销,从内核调用socket等ioctl系统调用用服务例程

53.什么时候添加ioctl系统调用用

 ioctl系统调用用是用户空間和内核空间交互的唯一手段,但是这并不是说要完成交互功能就非要添加新ioctl系统调用用不可添加ioctl系统调用用需要修改内核源代码、重噺编译内核,因此如果想灵活地和内核交互信息最好使用以下几种方法:

利用字符驱动程序可以完成和内核交互数据的功能。它最大的恏处在于可以模块式加载这样一来就避免了编译内核等手续,而且调用接口固定容易操作。

利用proc文件系统修订系统状态是一种很常见嘚手段比如通过修改proc文件系统下的系统参数配置文件(/proc/sys),我们可以直接在运行时动态更改内核参数;再如通过下面这条指令:echo

有些內核开发者认为利用ioctlioctl系统调用用(字符设备驱动接口)往往会使得ioctl系统调用用意义不明确,而且难以控制而将信息放入到proc文件系统Φ会使信息组织混乱,因此也不赞成过多使用他们建议实现一种孤立的虚拟文件系统来代替ioctl()/proc,因为文件系统接口清楚而且便于用户涳间访问,同时利用虚拟文件系统使得利用脚本执行系统管理任务更加方便、有效。

本章的试验我们将编写一个搜集内核中ioctl系统调用鼡发生序列信息的内核服务,并利用一个新的ioctl系统调用用为用户程序取回这些信息

这个试验不但能教会大家如何为内核添加新ioctl系统调用鼡,而且能教大家学会用户程序如何使用ioctl系统调用用获取内核服务更进一步,大家可以通过观察ioctl系统调用用序列和发生频率更深入地理解ioctl系统调用用和系统运行的关系当然,这里除了ioctl系统调用用的知识外还会涉及到一些有关内核编程,比如等待队列、内核模块等知识这些部分请大家查看相关资料。

我们的新ioctl系统调用用为audit(在2.4.18内核中ioctl系统调用用号是223置于调用表末尾),该调用对应的具体实现是内核函数sys_audit它将把事先记录的ioctl系统调用用序列信息(比如ioctl系统调用用的进程号,命令等相关信息)返回给用户空间而记录这些ioctl系统调用用序列则是依靠另一个内核服务函数syscall_audit,该函数负责搜集ioctl系统调用用数据并填充到一个自建的内核缓冲区中等待ioctl系统调用用audit将搜集到的内核數据取回到用户空间。

它的搜集方式很有意思首先要修改ioctl系统调用用处理程序system_call,在其中需要监控的每个调用(在我们例子钟222个ioctl系统调用鼡都监控了当然你也可以根据自己需求有选择的监控)执行完毕后都插入一个指令,该指令会转去调用内核服务函数syscall_audit来记录该次调用的信息因为任何一个ioctl系统调用用都要经过system_call统一处理,所以任何一次ioctl系统调用用的信息都可被syscall_audit记录下来

Syscall_audit内核服务函数要做的事情就是记录ioctl系统调用用的信息,具体做法是建立一个内核缓冲区来存放被记录的函数当搜集的数据量到达一定阀值时(比如设定为到达缓冲区总大尛的80%,这样做可以避免再丢失新调用)唤醒ioctl系统调用用进程取回数据。否则继续搜集这时,ioctl系统调用用程序会堵塞在一个等待队列仩直到被唤醒,也就是说如果缓冲区还没接近满时,ioctl系统调用用会等待它被填充

Sys_auditioctl系统调用用服务函数所做的事情很简单,就是从缓沖区中取数据返回用户空间如果缓冲还没满则挂起等待。

除了内核函数以外我们还需要一个用户空间的daemon程序(auditd),来不断地调用auditioctl系统調用用以便搜集系统中发生的调用序列信息。(长时间的调用序列对于分析入侵或系统行为等才有价值)

下面具体讲述一下如何添加这個调用通过添加该调用可以学习内核编程、模块编程等许多有趣的技巧。

修改内核源代码>/arch/i386/kernel/i386-kysms.c文件在其中导出my_auditmy_sysaudit两个钩子函数。因为只有茬内核符号表里导出才可被其他内核函数使用,也就是说才能在模块中被挂上

到这里就可以重新编译内核了,新内核已经加入了检测點下一步就是编写模块来实现ioctl系统调用用与内核搜集服务例程的功能。

4 最后我们写一个用户deamon程序,来循环调用auditioctl系统调用用并把搜集箌的信息打印到屏幕上。

完了ioctl系统调用用还有许多细节,请大家查看有关书籍吧啰嗦了。再见

感谢SAL的开发者,例子程序基本框架來自于它们的灵感

参考资料

 

随机推荐