您的位置:首页 > 运维架构 > Linux

Linux核心模式下的用户进程研究

2006-06-27 14:22 489 查看
Linux核心模式下用户进程研究
作者sleepjeep



摘要...3
ABSTRACT.4
第一章.................引言...5
第二章.................相关知识...7
2.1Linux相关内容...7
2.1.1内核体系结构...8
2.1.2用户模式和内核模式...8
2.1.3地址空间...9
2.1.4上下文...10
2.1.5系统调用...10
2.1.6模块的概念...13
2.2几种解决方案...13
2.2.1动态加载模块的方案...13
2.2.2修改内核源码的方案...14
2.2.3切换时修改段选择子的方案...14
2.3现实意义...14
第三章方案比较...15
3.1动态加载模块的方案...15
3.1.1基本原理...15
3.1.2适用环境...18
3.1.3优点...18
3.1.4问题与不足...18
3.1.5进一步讨论...19
3.2修改内核源码的方案...19
3.2.1基本原理...19
3.2.2适用环境...25
3.2.3优点...25
3.2.4问题与不足...25
3.3切换时修改段选择子的方案...26
3.3.1基本原理...26
3.3.2适用环境...27
3.3.3优点...27
3.3.4问题与不足...28
3.4综述...28
第四章方案实施...29
结束语...35
参考文献...36

摘要

在传统的操作系统中,用户程序必须承受因系统调用时用户模式和核心模式切换所带来的性能损失。然而,通过采用一些方法这部分的性能损失也可以被节省下来。在这片论文中,我们介绍了三种不同的方法,并且选择了其中的一种方法加以了实施。
系统调用是系统的一个功能,它在用户程序向受保护的内核请求服务时被触发。在传统的操作系统中,系统调用检查用户程序提供的参数,建立一个数据结构将这些参数传递到内核,并且执行一个被称为软中断的指令。接着,CPU的中断管理机构保存用户进程的状态并且将运行级别切换到内核模式。最后,系统进入内核函数去执行系统调用。因此,在传统的操作系统中,用户进程需要承担系统调用准备工作的额外开销,因为他们需要付出软中断和进程上下文切换的开销。
在本论文中,介绍了三种使用户进程得以在内核模式运行的方法。分别介绍了各个方案的基本原理,比较了各自的优缺点和适用环境。同时,在方案实施篇介绍了其中一种方案的具体实施样例。其中,方案一主要是采用动态加载内核模块的原理实现;方案二采用修改内核源码并配合TAL的方法实现;方案三采用在模式切换是修改进程段选择子的方案,辅助以模块实施。方案的具体实施采用了动态加载内核模块的方法。
论文介绍的方案将为编写Linux内核驻留程序和开发嵌入式系统等具体应用提供有建设性的参考。方案实施可以为具体的开发提供参考。

关键词:Linux核心模式用户进程系统调用

ABSTRACT

Intraditionaloperatingsystems,userprogramssufferformtheoverheadofsystemcallsbecauseoftransitionsbetweentheusermodeandthekernelmodeacrosstheirprotectionboundary.However,theoverheadcanbeeliminatedbyseveralmethods.Inthisdissertation,Iintroducedthreedifferentmethodsandimplementedasampleprogramusingoneofthemethods.
Asystemcallisfunctionthatiscalledbyuserprogramstoinvokeaserviceoftheprotectedkernel.Intraditionaloperatingsystems,thesystemcallcheckstheargumentsgivenbytheuserprograms,buildsadatastructuretoconveytheargumentstothekernel,andexecutesaspecialinstructioncalledasoftwareinterrupt.Then,theinterrupthardwareofaCPUsavesthestateoftheuserprogramsandswitchestheprivilegeleveltothekernelmode.Finally,Itdispatchestotheinnerfunctionthatimplementsthesystemcall.Thus,intraditionaloperationsystems,userprogramssufferfromtheoverheadofsystemcallsbecausetheyneedthecostlysoftwareinterruptionsandcontextswitches.
Inthisdissertation,threeschemestomakeuserprogramsruninKernelModewerediscussed.Itrespectivelyintroducedthebasicprincipalofeachschemeandcomparedtheiradvantagesanddisadvantagesandtheirapplicableenvironment.Moreover,adetailedimplementsampleofoneschemewasgiveninthechapterofprojectimplement.Forthesethreeschemes,SchemeⅠwascarriedoutwithLoadableKernelModules;SchemeⅡcameupwithmodifyingkernelsourcecodeaccompanyingwithTAL;SchemeⅢwasimplementedthroughalteringprogramsegmentselectorwhenpatternswitchingandassisteditwithmoduleimplement.ThepracticeoftheseschemesadoptedthemethodofSchemeⅠ.
Thedissertationwillserveasaconstructivereferenceforbothwritingkernel-residentapplicationforLinuxanddevelopingapplicationsuchasEmbeddedSystem.Theschemesofimplementinthisdissertationcanprovidereferenceforpracticalimplement.

KeywordsLinux,KernelMode,UserPrograms,SystemCalls

第一章引言

在计算机软硬件飞速发展的当今社会,操作系统作为应用软件的载体,得到了很大的发展。从Unix,Dos到Windows,操作系统逐渐的揭开了它的神秘面纱,随着硬件价格的降低走向了大众。在主流操作系统上,Unix类操作系统在大型机和服务器领域占有十分重要的地位;Windows操作系统则在微型机领域占据着统治地位。由于年代的久远和研究的深入,对于Unix系统的研究和开发已经硕果累累,继续的进行Unix系统层面的开发没有强烈的需求。而由于Windows不公开源代码的限制,同时,由于Windows主要用于个人机的事实,以及微软对于Windows版权的限制,乃至Windows操作系统微内核的保护机制,使得对于Windows的操作系统层面的开发也就不是非常的必要而且比较困难。而在另一方面,一个新兴的操作系统Linux正在服务器和个人计算机领域异军突起,有着诱人的发展潜力。由于Linux对Unix的良好继承性、良好的兼容性、可靠的稳定性和安全性等许多优点,以及其开放源代码和完全免费的特点,使其在未来操作系统的竞争中占据着很大的优势,未来将得到非常广泛的应用。由于其短暂的发展历程,Linux系统还不十分完美,而由于其开放源代码的特点,使得在系统层面的开发相对容易,基于这些,对于Linux系统层面的开发变得既有很大的应用前景又非常的便捷。因此,本课题采用Linux系统作为研究的对象。
现代的操作系统一般都有核心模式和用户模式之分,操作系统核心代码运行在核心模式下,具有较高的运行级别,具有较高的运行效率和较强的底层控制权力,系统硬件为其提供了尽可能多的内存保护以及其他措施,而用户进程则一般运行在用户模式下,其运行级别较低。
在传统的操作系统中,核心对于用户程序是由特权级工具和CPU的MMU提供保护的。第一,用户进程和核心是由CPU的不同运行级别分开的:用户进程在用户模式中运行,拥有最低的优先级;核心在核心模式运行,拥有最高的优先级。第二,核心被映射到只有在内核模式程序才可访问的一块内存地址上,由CPU中MMU的一个页表控制,因此,用户程序是不能直接存取内核的。用户进程通过系统调用来获取核心服务,这样的设计保证了系统核心的安全,
系统调用是系统的一个功能,它在用户程序向受保护的内核请求服务时被触发。在传统的操作系统中,系统调用检查用户程序提供的参数,建立一个数据结构将这些参数传递到内核,并且执行一个被称为软中断的指令。接着,CPU的中断管理机构保存用户进程的状态并且将运行级别切换到内核模式。最后,系统进入内核函数去执行系统调用。因此,在传统的操作系统中,用户进程需要承担系统调用准备工作的额外开销,因为他们需要付出软中断和进程上下文切换的开销。
用户进程不能直接存取系统核心,但同时也因为频繁的核心模式和用户模式的切换降低了系统的性能。如果用户进程在核心模式下运行,因为不需要用户模式和内核模式的切换,系统调用前期的额外开销就可以节省下来。因此,在某些对执行性能要求比较高的环境,一些原属于用户模式的程序,需要运行到核心模式下以提高性能,典型的如一些web服务程序。但是,简单的让用户进程在核心模式下运行是很危险的,因为核心模式下的进程可以无限制的作用于整个操作系统的资源。
目前有几种使用户程序在核心模式运行的方法,本论文试图通过比较各种使用户进程运行到核心模式下的方法,分析各自的原理和实现方案,找出各自的适用范围和优缺点,并提供其中一种的详细实现方案,以验证程序在核心模式下运行所带来的好处,为提高程序运行性能服务。
为了性能的检验,编写用于验证程序运行效率的程序,通过使其运行在核心模式下,得出比在用户模式下节省约1/3时间的结论。
论文的其余部分是这样组织的:在第二章,我们讨论了使用户程序在核心模式运行需要的相关知识。在第三章,我们介绍了目前使用的几种方案,介绍了其基本原理、适用环境并比较了它们的问题和不足。在第四章,我们对于其中的一个方案详细介绍了方案的实施。

第二章相关知识

2.1Linux相关内容

本节介绍了在几种方案中涉及到的一些Linux知识。在Linux中,有很多和用户程序在核心模式运行有联系的工作。当然,我们的研究和一般的工作区别是宗旨不同,我们的目标是使用户程序在内核模式下安全的执行而以前的工作着重于如何去安全的扩展内核。


2.1.1内核体系结构

Figure1.1Linux详细的内核体系结构图
Linux出于清晰性、兼容性、可移植性、健壮性和安全性以及速度方面的考虑,采用单内核的设计,但为了方便移植和维护,加入了微内核的设计概念,具体的是采用了模块的方法。单内核是一个很大的进程。它的内部又可以被分为若干模块(或者称为层次或其他)。但是在运行的时候,它是一个独立的二进制大映象。其模块间的通讯是通过直接调用其他模块中的函数实现的,而不是消息传递。图1-1是具有普遍性的Linux内核结构视图,用户程序通过系统调用来使用核心提供的服务,其内核的动作对于应用程序是不可知的。实际上用户程序并不直接和内核通讯――这样做毫无意义,而是通过libc(标准c库)和内核通信的,这样更容易而且易于程序编写。由于这种内核体系,Linux的进程由系统调用接口为界,分为了用户态的运行和内核态的运行(内核进程除外)。

2.1.2用户模式和内核模式

IntelCPU有4个优先级,ring0-ring3。操作系统运行在ring0,用户程序运行在ring3。运行在ring0的程序可以对所有硬件资源进行控制,而运行在ring3的对资源控制收到一些限制。内核模式驱动运行在ring0,而用户模式运行在ring3。系统硬件为核心模式提供了尽可能的内存保护以及其他安全措施,而用户进程不能直接存取系统核心,保证了核心的安全。在x86平台下,核心态和用户态的区分主要是通过段选择子确定的,具体来说,在Linux环境下,核心态的代码段选择子为0x10,数据段选择子为0x18;而用户态的代码段选择子为0x23,数据段选择子为0x2B,因此核心态程序工作在ring0,而用户态程序工作在ring3。这里有两个术语是内核空间(kernelspace)和用户空间(userspace),它们分别对应内核保留的内存和用户进程保留的内存。当然,多进程用户也经常同时运行,而且各个进程之间通常不会共享他们的内存,但是,任何一个用户进程使用的内存都称为用户空间。内核在某一个时刻通常只和一个用户进程交互,因此实际上不会引起任何混乱。由于这些内存空间是相互独立的,用户进程根本不能直接访问内核空间,内核也只能通过put_user和get_user宏和类似的宏才可以访问用户空间。用户空间和核心空间的交互通过系统调用来频繁的实施。

2.1.3地址空间

采用特权模式进行保护的根本目的是对地址空间的保护,用户进程不应该能够访问所有的地址空间:只有通过系统调用这种受严格限制的接口,进程才能进入核心态并访问到受保护的那一部分地址空间的数据,这一部分通常是留给操作系统使用。另外,进程与进程之间的地址空间也不应该随便互访。这样,就需要提供一种机制来在一片物理内存上实现同一进程不同地址空间上的保护,以及不同进程之间地址空间的保护。
Unix/Linux中通过虚存管理机制很好的实现了这种保护,在虚存系统中,进程所使用的地址不直接对应物理的存储单元。每个进程都有自己的虚存空间,每个进程有自己的虚拟地址空间,对虚拟地址的引用通过地址转换机制转换成为物理地址的引用。正因为所有进程共享物理内存资源,所以必须通过一定的方法来保护这种共享资源,通过虚存系统很好的实现了这种保护:每个进程的地址空间通过地址转换机制映射到不同的物理存储页面上,这样就保证了进程只能访问自己的地址空间所对应的页面而不能访问或修改其它进程的地址空间对应的页面。
虚拟地址空间分为两个部分:用户空间和系统空间。在用户模式下只能访问用户空间而在核心模式下可以访问系统空间和用户空间。系统空间在每个进程的虚拟地址空间中都是固定的,而且由于系统中只有一个内核实例在运行,因此所有进程都映射到单一内核地址空间。内核中维护全局数据结构和每个进程的一些对象信息,后者包括的信息使得内核可以访问任何进程的地址空间。通过地址转换机制进程可以直接访问当前进程的地址空间(通过MMU),而通过一些特殊的方法也可以访问到其它进程的地址空间。
尽管所有进程都共享内核,但是系统空间是受保护的,进程在用户态无法访问。进程如果需要访问内核,则必须通过系统调用接口。进程调用一个系统调用时,通过执行一组特殊的指令(这个指令是与平台相关的,每种系统都提供了专门的trap命令,基于x86的Linux中是使用int指令)使系统进入内核态,并将控制权交给内核,由内核替代进程完成操作。当系统调用完成后,内核执行另一组特征指令将系统返回到用户态,控制权返回给进程。

2.1.4上下文

一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
用户级上下文:正文、数据、用户栈以及共享存储区;
寄存器上下文:程序寄存器(IP),即CPU将执行的下条指令地址,处理机状态寄存器(EFLAGS),栈指针,通用寄存器;
系统级上下文:进程表项(proc结构)和U区,在Linux中这两个部分被合成task_struct,区表及页表(mm_struct,vm_area_struct,pgd,pmd,pte等),核心栈等。
全部的上下文信息组成了一个进程的运行环境。当发生进程调度时,必须对全部上下文信息进行切换,新调度的进程才能运行。进程就是上下文的集合的一个抽象概念。

2.1.5系统调用

每个操作系统在内核中都有一些最为基本的函数给系统的其他操作调用。在Linux系统中这些函数就被称为系统调用(SystemCall)。他们代表了一个从用户级别到内核级别的转换。在用户级别中打开一个文件在内核级别中是通过sys_open这个系统调用实现的。在/usr/include/sys/syscall.h中有一个完整的系统调用列表。以下是我的系统中syscall.h的一部分:

#ifndef_SYS_SYSCALL_H

#define_SYS_SYSCALL_H

#defineSYS_setup0/*只被init使用,用来启动系统的*/

#defineSYS_exit1

#defineSYS_fork2

#defineSYS_read3

#defineSYS_write4

#defineSYS_open5

#defineSYS_close6

#defineSYS_waitpid7

#defineSYS_creat8

#defineSYS_link9

#defineSYS_unlink10

#defineSYS_execve11

#defineSYS_chdir12

#defineSYS_time13

…………

#defineSYS_poll168

#defineSYS_syscall_pollSYS_poll

#endif/**/

每个系统调用都有一个预定义的数字(见上表),那实际上是用来进行这些调用的。内核通过中断0x80来控制每一个系统调用。这些系统调用的数字以及任何参数都将被放入某些寄存器(比如说,eax是用来放那些代表系统调用的数字)。那些系统调用的数字是一个被称之为sys_call_table[]的内核中的数组结构的索引值。这个结构把系统调用的数字映射到实际使用的函数。
系统调用发生在用户进程(比如emacs)通过调用特殊函数(例如open)以请求内核提供服务的时候。在这里,用户进程被暂时挂起。内核检验用户请求,尝试执行,并把结果反馈给用户进程,接着用户进程重新启动。
系统调用负责保护对内核所管理的资源的访问,系统调用中的几个大类主要有:处理I/O请求(open、close、read、write、poll等等)、进程(fork、execve、kill等等)、时间(time、settimeofday等等)以及内存(mmap、brk等等)的系统调用。几乎所有的系统调用都可以归入这几类。从根本上说,系统调用和表面并不完全相同,不同系统调用并不区分很细,有些系统调用是建立在别的系统调用的基础上的。
系统调用必须返回int的值,并且也只能返回int值。返回值为零或者为正说明调用成功,为负则说明发生了错误(最近的内核调用成功也可能返回负值,具体还需查errno)。
系统调用过程:

Figure1.2read()函数执行过程
以read为例,其功能是从一个资源中复制数据到进程所申请的空间。如图1.2所示,如果不考虑系统调用的触发机制而只考虑系统调用本身,read()系统调用的流程是:参数压栈->EAX置_NR_READ(系统调用号,在<asm/unistdh>中定义)->其他寄存器或连续储存区域存储参数(取决于参数长度)->INT0x80指令->转入内核运行->检查错误,返回->出栈,用户进程恢复运行。对于具体读文件的过程中是在内核中运行的,用户程序并不清楚所有在KernelSpace运行的过程,它只是提供了参数,然后发出了read命令。因此,对于x86平台,系统调用的主要过程是:用户进程进行必要的寄存器参数设置后,执行INT0x80指令,系统硬件切换用户堆栈到核心堆栈,并将SS、ESP、EFLAGS、CS、EIP压入到核心堆栈中,然后执行system_call调用,该调用保存寄存器参数值,并根据EAX寄存器的值确定sys_call_table表中对应的系统调用函数,然后执行该函数,函数执行完毕后返回到system_call,system_call恢复寄存器(部分寄存器可能已修改),然后执行IRET指令,该指令根据保存到核心栈的SS和ESP切换到用户栈,并恢复原有的代码段选择子和程序执行指针,从而回到用户进程继续运行。
系统调用一般有两种激活方法:system_call函数和lcall7调用门(callgate)(syscall函数是通过lcall7实现的,不能算独立的一种方法)。其中一般使用的是system_call函数,由于本文并不涉及激活方法研究,故使用system_call函数说明。

2.1.6模块的概念

整个内核并不需要同时转入内存,为了保证系统正常运行,一些特定的内核应该驻留在内存中,例如进程调度代码。而另外一些,则应该在内核需要的时候再装载,例如许多设备驱动程序。Linux为了在完成内核的可扩展同时不带来庞大的驻留内核,采用了一种可装载内核模块的概念。所谓模块,就是指在系统运行时可以增减的部分内核。需要是装载,不需要时卸载。
模块作为内核的一部分,具有内核的运行特征,即它是在内核模式下运行的,这一点,对于我们解决用户进程在内核模式的运行问题很重要。

2.2几种解决方案

对于将用户进程放入核心模式运行,目前主要有以下三种方案。

2.2.1动态加载模块的方案

从原理上说,用户进程的主要性能损失来自于用户模式和核心模式的切换开销,而用户模式和核心模式的切换则主要来自于系统调用(中断和例外同样导致核心模式和用户模式切换,但核心模式下也是常有的,故不考虑)。因此,为提高系统性能,希望系统调用运行到核心模式下,如Linux2.4核心提供的khttpd一样。
作为用户空间和内核空间的区别之一是其地址空间的不同,具体来讲用户进程的虚拟地址空间范围是0-3G,而内核进程为3-4G(注:在较新的内核版本中,内核虚拟地址空间为0-4G)。这对于用户进程的正常运行没有问题,但当把用户进程放到内核模式运行的时候,问题产生了。当进程触发系统调用的时候,对于用户进程提供的指针,系统调用必须检查提供的缓存是否是一个有效的地址空间,一个位于用户地址空间的地址被认为是合法的,而处于内核地址空间的则不是,所以当一个在内核模式运行的进程试图触发系统调用时是无法通过这个检查的。
该方案采用了阻止这种错误检测的方法,通过get_fs和set_fs宏来重新定义虚拟范围,从而使进程在内核状态也可触发系统调用。对于其他代码,由于可动态加载模块是在内核模式运行的,故采用动态加载模块的方法将其放入内核模式运行。

2.2.2修改内核源码的方案

方案通过使用类型化汇编语言(TAL)[MWCG98,MCG+99]让用户进程在核心模式安全运行的方法。
TAL是一种借助它的类型检查来保证程序内存和流程安全的汇编语言。一个程序的内存安全表示程序只作用于允许其访问的内存区域。程序的控制流安全表示程序只执行允许它执行的指令。在传统的操作系统中,程序的内存安全和控制流安全是由优先级和CPU的MMU控制的。
在这种方案中,用户进程使用TAL编写的,并且他们的安全性在加载时得到验证,这是在运行以前完成的。因此,用户进程可以在核心模式下安全而高效的运行,因为运行时安全检查已经不再需要了。
通过修改Linux内核,依照此方案,用TAL编写的用户进程得以在核心中安全的运行,系统调用的前期准备工作得到了节省。而且,它和原来的Linux核心提供了一样的接口(例如文件系统的控制接口),这是通过使用原来Linux核心系统调用的内部函数作为用户进程的接口来实现的。

2.2.3切换时修改段选择子的方案

在x86平台下,核心态和用户态的区分主要是通过段选择子来确定的。具体的来说,在Linux环境下,核心态的代码段选择子0x10,数据段选择子为0x18;而用户态的代码段选择子为0x23,数据段选择子0x2B。如果试图使用户程序工作在核心模式,即Ring0,一个简单的实现方案就是将用户程序的代码段和数据段选择子修改为核心态的代码段和数据段选择子。但在用户态下直接改变选择子显然是不允许的。由于模块在内核模式运行,故本方案采用动态加载模块的方式来修改选择子。
由于用户程序在系统调用过程中是在核心模式运行的,而在系统调用结束时由于选择子的重新恢复而转换回用户态继续运行,故本方案采取的方法是在系统调用中修改核心栈中保存的选择子寄存器值,使其在寄存器恢复时采用修改后的核心态的值,这样进程就得以停留在内核模式运行。

2.3现实意义

方案的顺利实施,将使不同环境不同状况的用户进程得以在内核模式运行,对于在程序执行性能上有特殊需要的进程,如能在内核模式运行,将大幅度的提高程序运行效率。通过比较分析各种方案的原理和优缺点,可以找出各自的应用范围,为程序的编写和运行提供帮助,而一些能够实用的程序将能使符合条件的进程在适当的时候在内核运行以提高运行性能,这对于嵌入式服务器等环境非常重要。故对于用户进程在核心模式下运行方案的分析研究对于网络服务器等应用领域具有十分显著的应用前景,对于进一步提升Linux系统下进程的执行性能具有非常重要的意义。

第三章方案比较

3.1动态加载模块的方案

3.1.1基本原理

在Unix类操作系统最为著名的特点中有一项就是其对于内核空间和用户空间的明确区分。明确的说,用户空间只能通过系统调用访问内核空间;系统调用作为内核空间和用户空间的接口只能由用户空间进程访问,内核空间进行的。由于用户进程包含了大量的系统调用,故将用户进程放入核心模式运行时必须解决核心模式下的系统调用问题,这是因为进行这样的操作时大批量的弃用系统调用而改用直接函数调用是不现实的,何况这还要牵扯到用户态的参数传递到核心态的问题。至于将用户态进程放到核心态运行,如果不牵涉到系统调用,简单的把他们编译为模块加载即可。因为根据Linux的设计,可动态加载模块是在内核模式运行的。由此可见,本方案着重需解决的问题是在内核模式的系统调用问题。
为了理解从内核模式进行系统调用,首先我们来分析一下正常系统调用的步骤。
以read为例,其功能是从一个资源中复制数据到进程所申请的空间。图1.2显示了从用户空间引发一个read系统调用的步骤。一个系统调用是由一个软中断将控制权传递给内核的。在x86系统中是0x80软中断(调用门)。中断码保存在EAX寄存器中,而其参数保存在其他寄存器中,在我们的例子中,read所关联的代码是__NR_read,在<asm/unistd.h>中定义。在切换到内核模式后,处理器必须把所有它的寄存器值保存到正确的内核函数,首先检查EAX是否越界,我们正在讨论的系统调用发生在sys_read函数,它必须发送到一个文件对象。根据用户进程传递给系统调用的文件描述信息,首先要做的是查找出文件,文件的打开方式最终作用于数据传递。所有以上的步骤对引发系统调用的用户程序是不可知的。图1.2中的每个箭头都表现为CPU的一次跳转。每次跳转可能要求刷新准备队列并且可能是一个缓存失败的事件。在用户空间和内核空间的转换非常重要,因为他们占用最多的处理器时间和数据预存取的过程。
现在让我们考虑在内核空间执行相同的系统调用。如果已经定义了KERNEL_SYSCALLS的话,最简便的方法是通过使用<asm/unistd.h>中的读或其他系统调用,否则,我们可能需要导出syscall_table。在系统调用之前,有些准备工作是必须要做的。对于用用户程序提供的指针从或者向用户空间传递数据的进程,系统调用必须检查提供的指针指向的空间是否是一个有效的地址。在正常运行时,一个位于用户地址范围的地址(0-3G)被认为是合法的,而一个位于内核地址范围(3-4G)的地址被认为是非法的。如果系统调用是由内核空间触发的,则我们必须阻止通常的错误检测,因为我们的目的缓存地址是在内核空间的,在3G以上。这就是本方案需解决的核心问题。
本方案采用了get_fs和set_fs宏解决地址检查的问题。Task_struck结构中的addr_limit域是用来定义合法的最高虚拟地址范围的,get_fs和set_fs宏可以用来读和写值。当以内核空间进行系统调用时,这个虚拟地址的限制是由get_ds宏返回的。所以,内核对内核的系统调用必须由以下代码包含:

mm_segment_tfs;


fs=get_fs();/*savepreviousvalue*/

set_fs(get_ds());/*usekernellimit*/


/*systemcallscanbeinvoked*/


set_fs(fs);/*restorebeforereturningtouserspace*/]

在程序编写过程中没有必要去包含每个单独的系统调用,因为几个系统调用可以执行在set_fs()……set_fs()之间。
故而,本方案的具体做法是将需要放入核心态运行的进程进行以下修改:在系统调用前后分别用get_fs和set_fs设置,将程序放入模块的初始化函数中(init_module(),若程序需要多次执行,init_module()可返回-EINVAL,不需卸载模块即可反复运行),编译模块。这样,在需要运行程序的时候用insmod命令加载模块即可做到程序在核心模式下的运行。

get_fs()和set_fs()

Linux使用了Intel的寄存器。
一个段寄存器在386的保护模式下,表现为到一个虚拟地址描述表的索引,表现为形容标志表。所有的内存空间使用这些段寄存器中的一个(CS、DS、ES、FS)作为它的虚拟地址寄存器。CS是代码段,并且用作缺省内存指示标志。DS是数据段寄存器,并用作大部分的数据操作指示器。ES和FS是附加的段寄存器,可以被应用程序和操作系统用作创造性的用途。
最初的Linux内核空间的内存映像图采用和物理地址一一对应的方式。用户空间的内存映像图被以二进制的形式指示给可执行程序。并且所有的文件均使用低端虚拟地址空间。正在执行的系统调用需要切换一个完全不同于用户空间的内存映像图。这由通过在用户空间和内核空间采用不同的描述器来使内存映像图关联到代码和数据段寄存器。因为许多系统调用需要使用用户地址空间。FS寄存器被保留以在内核模式的时候保存用户内存映像表。
几个宏名称的解释:
lget_fs返回了保存在FS中的当前段描述符。
lget_ds返回关联了内核模式当前保存在DS中的段描述符。
lset_fs保存一个描述符到FS,所以它将被用于指示数据交换。
这种内存虚拟地址的管理方案一直用到2.0版本的内核。版本2.1引进的最大革新是采用了一个不同的方法。这和其他平台正在采用的方法是一致的。用户空间和内核空间的内存映像图现在共享了虚拟地址的低3G,使得从内核空间对用户空间的访问更加迅捷了。
FS段寄存器现在结束了使命。现在用户空间内存也是由DS寄存器控制,和内核空间一样。FS现在仅仅在一些宏的名称上还有用(包括get_fs和set_fs),这些宏仍然具有一样的功能,但和FS寄存器已经无关了。

3.1.2适用环境

本方案适用与自己编写高运行效率应用程序或者已知程序代码的情况,例如编写一个内核驻留的web服务器。掌握了内核模式下的系统调用,对于编写内核驻留的高性能程序有很大的用途。

3.1.3优点

本方案的有点在于简单易行,无太大的技术风险。同时,本方案提供了非常好的系统调用性能,对进程运行效率的提高具有明显的作用。另外,在保证代码安全的情况下,本方案对系统的稳定性不会造成不良影响。

3.1.4问题与不足

本方案目前依然存在许多不足,比较突出的有以下几点:1、必须掌握程序源代码,任何无源代码的程序不能放入内核模式运行;2、需要掌握模块的编写和调试,内核模块的编写和调试与普通程序有许多不同;3、程序的编写需要得到安全保证,由于标准的Linux内核是不可抢占的,一旦出现死循环,将导致系统无法切换到其他进程执行,进程也无法被中断和结束;4、安全性问题,由于进程在核心态运行具有无限的控制权,任何修改或者获取内核其他进程资源的行为都将危害系统的稳定性和安全性,例如嗅探器的开发;5、编程人员必须熟悉系统核心编程和内核函数,并且无法调用一些属于用户进程的函数,如printf函数。

3.1.5进一步讨论

通过内核模式下的系统调用可以节省一部分的系统调用切换开销,这一点我们将在方案实施时给予验证,然而,系统调用在内核中触发并不是仅仅为了使速度最快,另一方面也是为了内核开发的方便。对于在内核模式下运行的进程,如能在执行系统调用时直接跳转到sys_read(或其他等值函数)是很令人感兴趣的,因为这样可以跳过图1.2中第三列的首部。这将很大程序上提高程序的运行效率。对于那些非常在意运行效率的程序最好的方法是在得到文件描述提供的指向文件结构的指针直接调用读函数。这对于核心至核心的调用来说是可能的最快的:系统调用将只执行图1.2中最后一列上面所关联的部分(即唯一实际数据传送的操作)。

3.2修改内核源码的方案

3.2.1基本原理

本方案由两部分组成,第一部分是程序的编写和执行,第二部分是内核的修改。
用户模式程序在内核模式运行的一个隐患是一旦程序访问错误的数据或者执行错误的指令,将可能影响系统中其他进程甚至使系统崩溃。本方案采用TAL来解决这个问题。
图3.1显示了一个程序编写的总体流程。在本方案中,一个程序员用TAL或其他程序语言编写程序。如果程序员用其他语言编写程序,则用一个编译器将这个语言转换为TAL。接着,汇编连接程序把它转化为二进制代码和类型注释。类型注释是用来在程序执行阶段检查程序在机器代码级别的安全性的。

图3.2显示了程序执行阶段的大概流程。核心通过类型检查机制检查类型注释,以此确定用户程序的内存和控制流安全。如果类型检查能确定用户程序的安全性,系统就可以将它放到内核中高效而安全的执行。
在本方案中,我们需要将TAL的类型检查加入到核心的可信计算基(TCB)中。然而,由于TAL的类型检查机制很小导致这并不成为一个问题。事实上,TAL的类型检查复杂性仅仅是O(n),n是一个程序的长度。

这里有必要介绍一下TAL的大致情况。
TAL是一种类型化的汇编语言。它通过类型机制确保内存和控制流的安全性。除了“类型”,它和平常使用的汇编语言是完全一样的。
图3.3展示了传统的IA-32汇编语言进行加运算的汇编代码。代码把EAX中的值和EBX指向的地址的值相加,然后代码跳到EDX所指向的内容继续执行。
图.3.3的程序在某些特定的环境下并不安全。举个例子,请看图.3.4,它在EBX中保存了一个无效的地址(行7)。导致了一个错误的内存操作(行2)。因此,这并没做到内存安全。

举另外一个例子,看图.3.5。它在EDX中保存了一个指令地址(行7)导致了一个有问题的指令执行(行3)。因此,这也不能做到控制流安全。

TAL通过对寄存器,内存和标号的类型检查阻止了这种错误的内存操作和指令执行。
图3.6显示了把图3.3中程序包装为TAL后的程序执行过程。类型注释被加在了第一行和第二行中间。通常情况下,当程序员用TAL写程序时,由程序员加入类型注释,当程序员用其他语言写程序而用编译器转换为汇编代码时,由编译器加入类型注释。事实上,TAL用于许多各种各样的类型注释,例如函数堆栈结构的类型,全局类型和已经存在的类型。为了论文简洁,我们在这里略过了这些介绍。
图3.6中,行1和行2间的类型注释的意义是这样的:“<”和“>”表示注释时代码标志类型。代码标志类型描述程序只有在满足“<”和“>”描述的情况时才可以跳转到标号的代码处。
lEAX保存了一个整型值。
lEBX保存了一个指向整型值的指针。
lEDX保存了一个指向已标志代码的指针,只有在满足如下两个条件时才可以跳转:
EAX保存了一个整型值。
EBX保存了一个指向整型值的指针。

这些类型注释在TAL编译器把程序编译为二进制文件后依然有效,因为TAL的编译器不仅生成了二进制文件,它也生成了类型注释。因此,通过产生类型注释,我们可以检查二进制代码的安全性。
下面将讲述如何类用TAL的类型检查来确保内存和控制流安全。
确保内存安全
图3.7显示了图3.4中程序类型化后的代码,程序在EBX保存了一个无效的内存地址(行10),在第4行,程序试图去进行一个错误的内存访问操作,这和图3.4中的一样。

程序不能通过TAL的类型检查。程序试图跳转到已经标志的代码“sum”(行11)。但是,因为EBX的类型变成了整型(行10),程序并不满足让跳转得以顺利进行必须的三个条件之一:“EBX保存一个指向整型值的指针”。像这样,TAL程序通过类型检查可以保证不发生错误的内存操作。
确保控制流安全
图3.8显示了图3.5中代码类型化后的代码。程序在EDX中存储了一个无效的指令地址(行10),并且试图去执行一个错误指令(行5),和图3.5一样。
程序不能通过TAL的类型检查。程序试图跳转到已经标志的代码“sum”(行10),但是,因为EDX的类型变成了整型(行10),程序并不满足让跳转得以顺利进行必须的三个条件之一:“EDX保存一个指向只有满足了下面两个条件才可以跳转的标志代码的指针”:
EAX保存一个整型值。
EBX保存一个指向整型值的指针。

像这样,TAL程序通过类型检查可以保证不执行错误的指令。
由上可知,通过使用TAL可以确保程序在内核空间运行时不发生危害系统安全的事件,从而可以保证普通用户空间进程在内核中安全的运行。
下一部分介绍内核的修改。

#ifndef_ASM_SEGMENT_H
#define_ASM_SEGMENT_H
#define__KERNEL_CS0x10
#define__KERNEL_DS0x18
#define__USER_CS0x23
#define__USER_DS0x2B
#endif

在x86平台下,LINUX建立进程的时候建立了两套段描述符,在文件在include/asm-i386/segment.h有说明。
一个用于内核代码,一个用于用户代码。运行内核代码的时候用内核的段描述符号就可以直接访问用户空间,但运行用户代码的时候用户段描述符不能访问内核空间,这是用的保护模式一些机制。
本方案中采用的方法是在进程建立的时候修改其段选择子的值,即将用户程序的代码段选择子改为0x10,数据段选择子改为0x18,这样进程建立后就能够在核心模式下运行。具体的,在2.4x核心下为修改start_thread宏,在include/asm-i386/processor.h中有说明:

#definestart_thread(regs,new_eip,new_esp)do{/
__asm__(“movl%0,%%fs;movl%0,%%gs”::”r”(0));
set_fs(USER_DS);
regs->xds=__USER_DS;
regs->xes=__USER_DS;
regs->xss=__USER_DS;
regs->xcs=__USER_CS;
regs->eip=new_eip;
regs->esp=new_esp;
}while(0)

这里regs->xds是段寄存器DS的映象,其次类推。
故在这里把它修改为核心态的段选择子即可。
本方案已得到具体实现,详见文献。
故而,本方案采用的方法是修改系统建立进程的程序,把用户态程序放到核心模式下运行,同时采用TAL来保证程序不进行危害系统安全的操作。

3.2.2适用环境

本方案因为涉及到对内核的修改,其针对不同的内核需要不同的修改方式,不能作为一种通用的手段,适用与对整个系统运行效率有特殊要求的情况,例如嵌入式系统等衍生系统。

3.2.3优点

本方案的优点在于其用户程序的编译和执行和通常的非核心模式的用户进程基本上没什么差别,用户无需掌握系统核心编程和内核参数,同时可以调用专属于用户进程的函数,解决了方案一存在的问题。

3.2.4问题与不足

本方案虽然可以使一个普通的用户程序完全工作在核心模式下,用户进程和内核进程具有完全相同的执行级别,同时该用户进程和通常的非核心模式的用户进程一样,其编译和执行基本上没什么区别。但由于方案采用修改Linux核心源码的方式,因此和核心版本有关,针对不同版本的核心需要重新编码,而且,方案对核心的修改,由于代码的增加降低了系统的整体性,而事实上系统内大量的进程仍然是普通用户模式进程,不需要运行到核心模式下。另外,由于整个进程完全工作在核心模式下,削弱了整个系统的问题性。修改内核源代码的方法在部署上也有相当的风险,任何不完善的代码都可能造成系统的崩溃。

3.3切换时修改段选择子的方案

3.3.1基本原理

在x86平台下,核心态与用户态的区分主要是通过段选择子来确定的。具体来说,在Linux环境下,核心态的代码段选择子是0x10,数据段选择子是0x18;而用户态的代码段选择子为0x23,数据段选择子为0x2B,因此核心态程序工作在Ring0,而用户态程序工作在Ring3。如果试图使用户程序工作在核心模式,即Ring0,一个简单的实现方案就是将用户程序的代码段和数据段选择子修改为核心态的代码段和数据段选择子,但在用户态下直接修改选择子显然使不允许的。因此方案2采用的方法修改核心代码中加载用户态选择子的程序,将在核心模式下运行的用户程序的选择子改为核心态的选择子。但为了不改动核心源码,这里采用了另一种方式去修改选择子,这里利用了一些用户态和核心态切换的基本原理。
在Linux操作系统下,用户态和核心态的切换主要依赖于系统调用。对于x86平台,系统调用的方式是采用INT0x80软中断。其主要过程为:用户进程进行必要的寄存器参数设置后,执行INT0x80指令,系统硬件切换用户堆栈到核心堆栈,并将SS、ESP、EFLAGS、CS、EIP压入核心堆栈中,然后执行system_call调用,该调用保存寄存器参数值,并根据EAX寄存器中的值确定执行系统调用表中的对应系统调用函数,然后执行该函数,函数执行完毕返回到system_call,system_call恢复寄存器,然后执行IRET指令,该指令根据保存到核心栈的SS和ESP切换到用户栈,并恢复原有的代码段选择子和程序执行指针,从而回到用户进程继续执行。由此,由于选择子的重新恢复,进程从核心态又回到了用户态。为保证进程仍在核心态运行,一个简单的方法是修改栈中的选择子寄存器值,将其改为核心态选择子,这样当寄存器恢复的时候,系统仍停留在核心态。
根据上面的分析,本方案的主要工作在于修改核心栈中的选择子寄存器,然而修改核心栈的值在用户态是不可行的,而作为访问内核空间的捷径之一,模块是经常被用到的工具,本方案采用的就是可加载核心模块的方式,由该模块来修改用户进程的核心栈。由于模块可同时为多个进程提供服务,故该模块可以保证多个进程工作在核心模式下,具有一定通用性。具体方案如下:为了代码简捷,采用字符设备核心模块,在其open函数中实现对进程栈的修改,主要代码如下:

structpt_regs*childregs

childregs=((structpt_regs*)(THREAD_SIZE+(unsignedlong)current))-1
childregs->xds=childregs->xes=__KERNEL_DS
childregs->xcs=__KERNEL_CS
上述代码修改了当前进程(current)核心栈中的CS、DS和ES选择子,从而当用户进程从核心调用返回时仍保持在核心栈中。
对于需要运行在核心模式的用户程序,只要执行open系统调用,打开上述模块对应的字符设备即可。但由于用户进程仍停留在核心态,因此不会发生堆栈切换,故应直接使用int指令打开模块对应的字符设备文件。则当用户程序open调用返回时,程序仍停留在核心态,并可执行特权级代码和核心函数。如果需要回到用户态,可以采用直接给选择子赋值的方式实现。这里,我们可以建立新的系统调用来实现。
这样就实现了用户进程在核心模式下的运行,并且可以在核心态和用户态之间自由转换。同时,由于核心模块和用户程序的分离,从而对用户程序的编码、编译和调试不产生影响。

3.3.2适用环境

本方案适合需要使不限数目的用户进程在内核模式运行而且需要较高的稳定性的情况。并且,由于采用模块的方式,适合大多数的内核版本,具有良好的通用性和应用前景。

3.3.3优点

本方案的有点有以下几条:1、不修改系统核心代码,既考虑了不同内核的兼容性,同时也保证了整个系统的性能不受损失。2、核心模式的用户进程与普通用户进程编译和执行基本一致,编程和调试相对简单。3、核心模式的用户进程可以非常自由的在核心模式和用户模式下互相转换,既减少了对系统稳定性的影响又提高了性能。

3.3.4问题与不足

本方案在实践中依然有一些问题和不足,现列举如下:1、用户堆栈和核心堆栈的问题。由于该方案系统调用返回是仍然出于核心态,系统仍工作在核心栈下,故对用户堆栈变量的存取可能会存在问题。另外,由于核心堆栈目前只有8K,故过深的循环嵌套或过大的局部变量将可能使堆栈溢出进而导致系统崩溃。2、稳定性问题。由于进程在核心态运行,一旦出现死循环将使系统无法切换到其他进程运行,该进程也无法被中断结束。3、安全性问题。由于绝大部分用户进程不允许运行在核心模式下,否则将带来较大的安全隐患。因此必须考虑保证只有合法权限的进程才能获得核心模式运行权限。另一个问题是代码执行的安全性保证,内核模式的代码必须保证内存和指令安全。

3.4综述

标准的操作系统区分核心模式和用户模式以实现最大程度的内核安全保护。一般情况下并不需要尝试超越这样的机制,但在某些必要的情况下将用户进程运行到核心模式下是可行的。这主要适用与以下几种应用:①.用于提高用户进程运行性能。将用户进程置于核心态运行,可明显提高用户进程的运行性能,尤其是针对某些核心调用的特点进行优化后,其性能的提高是非常显著的,这点可以通过第四章验证。如果一个用户进程执行频繁的核心调用,且每次调用存取的数据量都不大,放入核心运行将获得较大程度的性能提升,许多软件系统,如web服务器、窗口系统都具有这样的需求。②.使得某些用户程序存取核心提供的服务。Linux核心提供了大量的服务和通用函数,一般的用户程序仅通过系统调用来获取部分的服务,而许多服务对用户程序是不开放的,典型如slab内存分配器,某些标准C函数等。在某些特殊系统中,如能使用这些服务,将很大程度上提高性能。③.用于内核和用户进程的分析以及内核数据的存取。对于普通用户进程而言内核是不可见的,因此在用户模式下我们无法获取当前进程的许多信息,如GDT表,如虚拟内存等。采用在核心运行的方法,对获取这些信息有很大帮助。
本文的三种方案中,第一种比较适合与自己编写内核驻留程序,这类程序对运行性能有特殊的需求,而对在内核中运行的用户程序的数目没有要求。这是一种既能很大的提高应用程序性能又无需复杂处理的方法。而且它对于整个系统的稳定性安全性无影响,但需要对内核较了解和不能调用某些用户函数是其缺陷。第二种比较适合开发衍生或变形系统,例如对某些方面有特殊需求的嵌入式系统,这类系统无需运行普通系统那么复杂多样的程序,许多没有对安全性着重的要求,因为应用的专业性,其受外界的影响也比较小。这类系统可以采用第二种方案来提高性能。但由于修改内核源码的复杂性,限制了这种方案的通用性。第三种有较宽的适用范围,能满足大部分需要将用户程序放在核心下运行的情况。能提供比第二种更好的稳定性,可以为多个进程服务。这种方案有许多优点,然而其缺陷也是显著的。相对于第一种方案,它的运行效率较低,系统的稳定性较低;相对于第二种方案,其需要修改每个调用进程的代码,不适合大量进程使用这项功能。故这三种方案各有各的适用范围,在不同的情况下建议选择不同的方法加以实施。

第四章方案实施

为了直观的表现进程在核心模式下运行所带来的好处,我们需要以实际的程序作为示例。由于Linux系统大部分程序仍属于用户模式程序,直接放入内核运行可能影响系统的稳定性甚至导致系统崩溃,在分析比较三种方案后,决定选择第一种方案加以具体实施。这主要是因为第二种方案在调试上比较困难,容易导致系统崩溃;第三种方案运行过程中稳定性不能保证,并且,一部分也是出于第一种方案比较适合当个进程、运行效率较高、对系统稳定性无影响和比较容易实现的特点。
在具体实施方案前,有两点内容需要介绍:其一是内核模块,其二是rdtsc指令的使用。
LKMs就是可卸载的内核模块(LoadableKernelModules)。这些模块本来是Linux系统用于扩展他的功能的。使用LKMs的优点有:他们可以被动态的加载,而且不需要重新编译内核。由于这些优点,他们常常被特殊的设备(或者文件系统),例如声卡等使用。
每个LKM至少由两个基本的函数组成:

intinit_module(void)/*用于初始化所有的数据*/

{
...
}
voidcleanup_module(void)/*用于清除数据从而能安全地退出*/
{
...
}
一个内核模块至少包括两个函数:init_module,在这个模块插入内核时调用;cleanup_module,在模块被移出时调用。典型情况下,init_module为内核中的某些东西注册一个句柄,或者把内核中的程序提换成它自己的代码(通常是进行一些工作以后再调用原来工作的代码)。clean_module模块要求撤销init_module进行的所有处理工作,使得模块可以被安全的卸载。
加载一个模块(常常只限于root能够使用)的命令是:
#insmodmodule.o
这个命令让系统进行了如下工作:
加载可执行的目标文件(在这儿是module.o)
调用create_module这个系统调用(见2.1.5)来分配内存。
一些不能解决问题的引用由系统调用get_kernel_syms进行查找引用。
在此之后系统调用init_module将会被调用用来初始化LKM->执行intinti_module(void)等等。
一个内核模块不是一个可以独立执行的文件,而是需要在运行时刻连接入内核的目标文件。所以,它们需要用-c选项进行编译。而且,所有的内核模块都必须包含特定的标志:
l__KERNEL__——这个标志告诉头文件此代码将在内核模块中运行,而不是作为用户进程。
lMODULE——这个标志告诉头文件要给出适当的内核模块的定义。
lLINUX——从技术上讲,这个标志不是必要的。但是,如果你希望写一个比较正规的内核模块,在多个操作系统上编译,这个标志将会使你感到方便。它可以允许你在独立于操作系统的部分进行常规的编译。
还有其它的一些可被选择包含标志,取决于编译模块是的选项。如果你不能明确内核怎样被编译,可以在in/usr/include/linux/config.h中查到。

rdtsc是汇编的指令。为了精确的测量比较程序在用户模式和核心模式的不同表现,我们需要高精度的计时单位,在这里我们使用了系统的时间戳计数器。这是一个64位的寄存器,它在每个时钟周期加1,可以提供一个高质量的计时器。
<asm/msr.h>头文件包含了readtsc(lowhigh)宏指令。它可以将计时器的值读入由调用程序提供两个32位的值中。readtsc(low)只检索低32位的计时器,足以满足我们的需求。

下面介绍我们的具体方案。
首先,我们需要编写一个在用户空间检验运行性能的程序。
为了使误差最小化,我们只使用了一个空的read()系统调用,通过它在不同模式运行的时间来比较程序运行效率的变化。
在程序中,我们首先需要计算rdtsc指令本身花费的时间,则通过如下代码可以得到:

rdtscl(ini);/

/*nocode*/
rdtscl(end);/
time=end-ini/*justrdtsctakes*/
由于需要将代码包含在两个rdtscl之间,同时为了多次执行取平均值,我们采用宏来包装它:

#definemeasure_time(code)/

for(i=0;i<NTRIALS;i++)/*trymanytimes*/
{
rdtscl(ini);/
code;/
rdtscl(end);/
now=end-ini;/
if(now<best)best=now;/
}
则我们验证rdtsc本身的程序变为:

best=~0;

measure_time(0);/*nocode*/
tsc=best;/*justrdtsctakes*/
而我们验证read空系统调用的程序为:

measure_time(read(STDIN_FILENO,buffer,0));

之后减去rdtsc本身的时间即为read系统调用的时间。

主要代码如下:

intmain()

{
unsignedlongini,end,now,best,tsc;
inti;
charbuffer[4];

#definemeasure_time(code)/
for(i=0;i<NTRIALS;i++){/
rdtscl(ini);/
code;/
rdtscl(end);/
now=end-ini;/
if(now<best)best=now;/
}

/*timerdtsc(nocode)*/
best=~0;
measure_time(0);/*nocode*/
tsc=best;/*justrdtsctakes*/

/*timeanemptyread()call*/
best=~0;
measure_time(read(STDIN_FILENO,buffer,0));/*alltime*/

/*report*/
printf("systime:--rdtsc:%liticks/nsystime:--read():%liticks/n",tsc,best-tsc);
return0;
}
程序在我的机器上执行报告为:

systime:――rdtsc:80ticks

systime:――read():2048ticks
下面我们把它放到内核模式下去运行:

首先,我们需要把我们的程序放入到模块的初始化程序中,为了可以多次执行而不用卸载模块,我们在模块的初始化函数中返回-EINVAL,即模块加载失败的信息。

staticint__initread_init(void)

{
mm_segment_tfs;
unsignedlongini,end,now,best,tsc;
inti;
charbuffer[4];

#definemeasure_time(code)/
for(i=0;i<NTRIALS;i++){/
rdtscl(ini);/
code;/
rdtscl(end);/
now=end-ini;/
if(now<best)best=now;/
}

/*timerdtsc(i.e.nocode)*/
best=~0;
measure_time({});
tsc=best;
ksys_print("tsc",tsc);

/*timeanemptyread()*/
best=~0;
measure_time(read(0,buffer,0));
/*reportdata*/
ksys_print("read()",best-tsc);

return-EINVAL;
}
之后,按照方案一,我们在read系统调用前后加上fs=get_fs()、set_fs(get_ds())和set_fs(fs)。

模块已经完成,下面,我们用gcc-c-O2-I/usr/src/linux-2.4.20-8/include-DMODULE-D__KERNEL__-DLINUXread_modify.c(内核模式的编译和普通程序不同,需要加上-DMODULE-D__KERNEL__-DLINUX)命令编译模块,编译完成后在控制台用rmmod加载模块即可验证程序运行效果。
程序在我的系统上运行效果如下:

ksystime:tsc――80ticks

ksystime:read()――1502ticks
很显然,程序在内核模式的运行比用户模式大幅度的提高了运行效率。

结束语

经过两个多月的时间,论文终于写完了,在这段时间里,绝大部分时间放在了学习Linux系统上,终于从一个门外汉变得略知一二了。但是,研究的越深入,越感觉到自己知识的浅薄,现代操作系统的博大精深是难以想象的,一个人开发一个系统的年代已经一去不复返了。Linux就像一个深闺中的少女,对她了解的越多,越喜爱她的秉性。她的干练的代码和Open的理念给我留下了不可磨灭的印象。从Linux中学到的东西,对我的整个程序人生都有很大的益处。这里,也祝愿Linux的未来能够得到更广阔的应用,同时,希望北信能够拥有许多的Linux人才。
在此要郑重的感谢郁红英老师,没有她两个月以来对我的指导,这篇论文是难以顺利完成的。同时,感谢彭兴国和卫涛等同学,他们为我的论文和程序提出宝贵的建议,没有我们平常毫无保留的讨论,就没有这篇文章的产生。谢谢大家!

参考文献

[1]ToshiyukiMaeda.SafeExecutionofUserProgramsinKernelModeUsingTypedAssemblyLanguage.2002
[2]姜新汪秉文瞿坦.Linux核心模式下的用户进程研究.计算机工程与应用,2004.4118-120页
[3]ScottMaxwell著冯锐刑飞刘隆国陆丽娜译.Linux源代码分析.北京:机械工业出版社,20000-628页
[4]Gary.Nutt.KernelProjectsForLinux.PearsonEducation,20020-239页

Trackback:http://tb.blog.csdn.net/TrackBack.aspx?PostId=837714
[点击此处收藏本文] 野草发表于 2006年06月26日22:55:00

functionhide(){showComment();}

document.write("");



rjchen发表于2006-06-2707:55:00IP:219.236.60.*
好文章,感谢野草!

CSDNBLOG编辑发表于2006-06-2710:21:00IP:218.247.0.*
评论
野草网友,经过CSDNBlog专家顾问团的合议,您的文章:我的毕业论文-Linux核心模式下的用户进程研究,成为6月26日的“每日一帖”!请参考下面的提示和我们联系领取奖品事宜,谢谢你向广大CSDN网友分享你的原创文章。

提示:
1.评选时间为周一至周五!
2.请您及时查看和关注您的个人Blog,我们会以评论的形式告诉您获奖的信息。
3.本次活动奖品是由第二书店提供的100个D币,请访问第二书店(www.dearbook.com.cn)激活您的D币帐号;激活后请将您[Dearbook的注册邮件]/[Blog的帐号]//[获奖文章名称]/[获奖日期]回复邮件给我们的编辑gaocao(AT)csdn.net;
4.请各位网友及时查收您的D币帐号,有问题及时和我们联系,或者在本栏目评论。
5.[每日一帖]不断更新中……明天将评选出今天的每日一帖
6.详细请参考:http://blog.csdn.net/todaypost/
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: