UML软件工程组织

Linux核心模式下用户进程研究
作者:sleepjeep

摘要
在传统的操作系统中,用户程序必须承受因系统调用时用户模式和核心模式切换所带来的性能损失。然而,通过采用一些方法这部分的性能损失也可以被节省下来。在这片论文中,我们介绍了三种不同的方法,并且选择了其中的一种方法加以了实施。

系统调用是系统的一个功能,它在用户程序向受保护的内核请求服务时被触发。在传统的操作系统中,系统调用检查用户程序提供的参数,建立一个数据结构将这些参数传递到内核,并且执行一个被称为软中断的指令。 接着,CPU的中断管理机构保存用户进程的状态并且将运行级别切换到内核模式。最后,系统进入内核函数去执行系统调用。因此,在传统的操作系统中,用户进程需要承担系统调用准备工作的额外开销,因为他们需要付出软中断和进程上下文切换的开销。

在本论文中,介绍了三种使用户进程得以在内核模式运行的方法。分别介绍了各个方案的基本原理,比较了各自的优缺点和适用环境。同时,在方案实施篇介绍了其中一种方案的具体实施样例。其中,方案一主要是采用动态加载内核模块的原理实现;方案二采用修改内核源码并配合TAL的方法实现;方案三采用在模式切换是修改进程段选择子的方案,辅助以模块实施。方案的具体实施采用了动态加载内核模块的方法。

论文介绍的方案将为编写Linux内核驻留程序和开发嵌入式系统等具体应用提供有建设性的参考。方案实施可以为具体的开发提供参考。

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

ABSTRACT

In traditional operating systems, user programs suffer form the overhead of system calls because of transitions between the user mode and the kernel mode across their protection boundary. However, the overhead can be eliminated by several methods. In this dissertation, I introduced three different methods and implemented a sample program using one of the methods.

A system call is function that is called by user programs to invoke a service of the protected kernel. In traditional operating systems, the system call checks the arguments given by the user programs, builds a data structure to convey the arguments to the kernel, and executes a special instruction called a software interrupt. Then, the interrupt hardware of a CPU saves the state of the user programs and switches the privilege level to the kernel mode. Finally, It dispatches to the inner function that implements the system call. Thus, in traditional operation systems, user programs suffer from the overhead of system calls because they need the costly software interruptions and context switches.

In this dissertation, three schemes to make user programs run in Kernel Mode were discussed. It respectively introduced the basic principal of each scheme and compared their advantages and disadvantages and their applicable environment. Moreover, a detailed implement sample of one scheme was given in the chapter of project implement. For these three schemes, SchemeⅠwas carried out with Loadable Kernel Modules; Scheme Ⅱ came up with modifying kernel source code accompanying with TAL; Scheme Ⅲ was implemented through altering program segment selector when pattern switching and assisted it with module implement. The practice of these schemes adopted the method of Scheme Ⅰ.

The dissertation will serve as a constructive reference for both writing kernel-resident application for Linux and developing application such as Embedded System. The schemes of implement in this dissertation can provide reference for practical implement.

Keywords:Linux,Kernel Mode,User Programs,System Calls

第一章 引言

在计算机软硬件飞速发展的当今社会,操作系统作为应用软件的载体,得到了很大的发展。从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.1 Linux相关内容

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


2.1.1内核体系结构

Figure 1.1 Linux 详细的内核体系结构图

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

2.1.2用户模式和内核模式

Intel CPU有4个优先级,ring0-ring3。操作系统运行在ring0,用户程序运行在ring3。运行在ring0的程序可以对所有硬件资源进行控制,而运行在ring3的对资源控制收到一些限制。内核模式驱动运行在ring0,而用户模式运行在ring3。系统硬件为核心模式提供了尽可能的内存保护以及其他安全措施,而用户进程不能直接存取系统核心,保证了核心的安全。在x86平台下,核心态和用户态的区分主要是通过段选择子确定的,具体来说,在Linux环境下,核心态的代码段选择子为0x10,数据段选择子为0x18;而用户态的代码段选择子为0x23,数据段选择子为0x2B,因此核心态程序工作在ring0,而用户态程序工作在ring3。这里有两个术语是内核空间(kernel space)和用户空间(user space),它们分别对应内核保留的内存和用户进程保留的内存。当然,多进程用户也经常同时运行,而且各个进程之间通常不会共享他们的内存,但是,任何一个用户进程使用的内存都称为用户空间。内核在某一个时刻通常只和一个用户进程交互,因此实际上不会引起任何混乱。由于这些内存空间是相互独立的,用户进程根本不能直接访问内核空间,内核也只能通过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系统中这些函数就被称为系统调用(System Call)。他们代表了一个从用户级别到内核级别的转换。在用户级别中打开一个文件在内核级别中是通过sys_open这个系统调用实现的。在/usr/include/sys/syscall.h中有一个完整的系统调用列表。以下是我的系统中syscall.h的一部分:


#ifndef _SYS_SYSCALL_H
    #define _SYS_SYSCALL_H
    #define SYS_setup 0 /* 只被init使用,用来启动系统的 */
    #define SYS_exit 1
    #define SYS_fork 2
    #define SYS_read 3
    #define SYS_write 4
    #define SYS_open 5
    #define SYS_close 6
    #define SYS_waitpid 7
    #define SYS_creat 8
    #define SYS_link 9
    #define SYS_unlink 10
    #define SYS_execve 11
    #define SYS_chdir 12
    #define SYS_time 13
    …………
    #define SYS_poll 168
    #define SYS_syscall_poll SYS_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)。

系统调用过程:


Figure 1.2 read()函数执行过程

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

系统调用一般有两种激活方法:system_call函数和lcall7调用门(call gate)(syscall函数是通过lcall7实现的,不能算独立的一种方法)。其中一般使用的是system_call函数,由于本文并不涉及激活方法研究,故使用system_call函数说明。

2.1.6 模块的概念

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

模块作为内核的一部分,具有内核的运行特征,即它是在内核模式下运行的,这一点,对于我们解决用户进程在内核模式的运行问题很重要。

2.2 几种解决方案

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

2.2.1 动态加载模块的方案

从原理上说,用户进程的主要性能损失来自于用户模式和核心模式的切换开销,而用户模式和核心模式的切换则主要来自于系统调用(中断和例外同样导致核心模式和用户模式切换,但核心模式下也是常有的,故不考虑)。因此,为提高系统性能,希望系统调用运行到核心模式下,如Linux 2.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。如果试图使用户程序工作在核心模式,即Ring 0,一个简单的实现方案就是将用户程序的代码段和数据段选择子修改为核心态的代码段和数据段选择子。但在用户态下直接改变选择子显然是不允许的。由于模块在内核模式运行,故本方案采用动态加载模块的方式来修改选择子。

由于用户程序在系统调用过程中是在核心模式运行的,而在系统调用结束时由于选择子的重新恢复而转换回用户态继续运行,故本方案采取的方法是在系统调用中修改核心栈中保存的选择子寄存器值,使其在寄存器恢复时采用修改后的核心态的值,这样进程就得以停留在内核模式运行。

2.3 现实意义

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

参考文献

[1] Toshiyuki Maeda.Safe Execution of User Programs in Kernel Mode Using Typed Assembly Language.2002

[2] 姜新 汪秉文 瞿坦.Linux核心模式下的用户进程研究.计算机工程与应用,2004.4 118-120页

[3] Scott Maxwell著 冯锐 刑飞 刘隆国 陆丽娜 译.Linux源代码分析.北京:机械工业出版社,2000 0-628页

[4] Gary.Nutt. Kernel Projects For Linux. Pearson Education,2002 0-239页


版权所有:UML软件工程组织