基于驱动代码分离的设备驱动体系结构的研究

2015-07-03 07:46白璐翟高寿
软件 2015年1期

白璐 翟高寿

摘要:在对Linux操作系统的网络驱动进行了研究后,设计和实现了一种基于驱动代码分离的驱动框架,将驱动程序分为在用户态执行的部分和在内核态运行的内核模块部分,用户态的驱动部分主要是调用频率较低的性能无关的代码,如设备的状态信息获得等;在内核态运行的驱动内核模块包括驱动代码中的关键部分,如中断响应、数据传输等。以PCnet32网络驱动为例,实现了驱动程序代码在用户空间和内核空间的分离,在一定程度上实现了驱动的隔离,减少了由驱动引起的漏洞破坏操作系统的可能性,同时减少了内核中运行的驱动代码,也满足了驱动性能的要求,达到了保护操作系统可靠性的目的。

关键词:操作系统安全;驱动隔离;用户空间;内核空间;Linux

中图分类号:TP311

文献标识码:A

0 引言

随着计算机科学技术的发展以及硬件性能的提升,操作系统的稳定性及安全性日益成为现今面临的最主要的挑战。尽管操作系统日益成熟,但它依然面临着对更高的安全性要求的挑战。Linux如今已广泛的应用于个人电脑的操作系统和服务器上,近年来,随着嵌入式系统的发展,Linux也开始普遍应用于嵌入式设备。而Linux设备驱动通常都是作为内核的一部分来实现,通常是以内核模块的形式实现。驱动程序运行在内核地址空间,因此其拥有很高的特权,同时对系统资源有完全的访问权限。随着设备种类和数量的日益增加,Linux内核源码中设备驱动程序的数量也在增加。超过50%的内核代码是设备驱动程序代码。而设备驱动往往是操作系统内核中的漏洞的主要来源。驱动程序需要正确处理在运行时引起的错误,同时驱动程序必须运行在受限制的内核环境。内核崩溃一般由驱动里的漏洞引起,攻击者可能也会利用这些安全漏洞来破坏系统。在隔离设备驱动方面已经有很多尝试。许多研究型操作系统可以支持完全的设备驱动隔离。但是,在商用操作系统方面,例如Linux主要焦点在以错误隔离方式来防止传统的设备驱动漏洞。

研究表明,内核中设备驱动程序的缺陷往往是内核其他部分的三到七倍。其原因除了驱动程序代码本身的复杂性,另外一个原因就是很多驱动程序是由不是非常熟悉操作系统结构的人写的。以内核模块运行的驱动程序如果崩溃,往往会造成操作系统的崩溃。同时,驱动程序实现在内核,如果驱动程序有错误或漏洞将引起其他不相关的内核部分的错误。

Microkernel是将驱动程序在用户空间实现,达到驱动隔离的目的从而保护了操作系统的安全。用户空间驱动程序的优点是易于开发,如果一个驱动程序是普通的用户进程,它就可以像其他用户程序一样被调试。在用户空间实现驱动可以加强系统的稳定性。当用户空间进程崩溃,系统不会因为它的影响而不稳定,因为系统可以把用户空间的驱动程序强行杀死或重新启动它,这与杀死或重启普通进程是一样的。但是若将驱动程序完全移到用户空间实现,又会影响操作系统的性能,例如一些对实时性要求较高的操作将受到影响。

所以本文提出了驱动代码分离的想法,即一部分设备驱动实现在用户空间另一部分实现在内核空间。而我们对于如何分离驱动代码的原则是将性能相关的且执行频率高的操作留在内核空间,而把性能相关度低的执行频率较低的操作在用户空间实现。

分离设备驱动代码的方法既实现了用户空间驱动的优点又满足了内核设备驱动对性能的要求。因为设备驱动程序通常不是由对内核非常熟悉的人写的,所以出现漏洞是难免的。同时驱动程序都是以内核模块的形式存在于内核,内核驱动程序代码就对整个内核地址空间有完全的访问权限,并且由于拥有特权而能进行所有的访问操作。所以在设备驱动中的漏洞很容易引起内核崩溃或挂起。而当由此引起的内核崩溃发生后我们只能重启系统来恢复。有研究显示85%的漏洞都是由于操作系统的驱动程序的漏洞引起。分离设备驱动则可以将驱动程序中的漏洞从内核隔离。虽然仅仅靠驱动代码分离不能隔离依然留在用户空间的内核的代码的漏洞,但是基本上留在内核里的代码将是经过我们严格测试少有出现漏洞的代码。所以,分离设备驱动意味着减少驱动导致的系统崩溃的几率,同时也可以减少内核中的代码量。

1 设计

设备驱动程序将被分为两部分,一部分以守护进程的形式运行在用户空间,守护进程(Daemon)是运行在后台的一种特殊进程,它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件,驱动的另一部分还是以内核模块的形式运行在内核。在此过程中使用请求-回复模式在内核和用户空间进行通讯。当内核中有向已经移到用户空间的函数的请求到来时,通过内核与用户空间的这种通讯方式把这个来自内核的请求委派给用户空间进程处理。而用户空间进程通过ioct1()调用陷入内核,在内核中以内核线程的形式等待内核对用户空间函数的请求到来。当在内核中等待的用户线程被到来的内核请求唤醒时,控制就返回到用户空间。用户空间进程继续处理这个来自内核的请求,例如,执行内核所请求的已经移动到用户空间的函数,并且返回执行结果给内核,与此同时准备处理下一个请求。如果接下来还没有请求到来,则用户空间进程继续在内核中等待请求。图1,2,3给出了驱动分离框架设计的概览,展示了分离代码后的驱动运行中各个阶段的状态。

内核驱动,也就是以内核模块形式运行在内核的这部分驱动程序内包含所有在内核实现了的函数。通常,它有控制I/O、设备的打开和关闭以及probe这些功能,还有中断处理和初始化以及退出模块。当一个向用户空间函数的请求到来时,保留在内核以内核模块运行的驱动部分就把这个请求入队并且把这个请求通知给用户空间进程。这一通知包含所请求的函数名和该函数执行所需要的相关参数。

2 实现

本论文主要是针对PCnet32网络驱动程序,以pcnet32为例实现了这一分离驱动代码的设计。PCnet32这个驱动程序是在VMware虚拟机上所支持的网络驱动程序,之所以选择它是因为这样就可以在VMware虚拟机环境下实现对网络驱动程序的分离。使用VMware可以防止修改内核代码后系统崩溃带来的麻烦,可以比较简单的从内核异常中重新启动或是恢复到最初的状态。本文使用的实验环境是:Intel Corei5-3230M CPU,主频2.6GHz,Linux 2.6.30内核,VMware Workstation 9.0.2。虚拟机有512MB内存,20GB的硬盘容量以及配置NAT-configured的以太网卡。之所以选择Linux进行实验是因为它支持可加载内核模块形式的驱动程序,并且它已经存在一些对用户空间驱动程序的支持。

2.1已有的支持

为了把一些函数移到用户空间,分离驱动的设计利用了一些在Linux中已存在的支持,如下面所论及的两方面。

2.1.1系统调用

首先,需要用户空间进程通过调用陷入内核,从而表明它已经准备好处理内核对用户空间函数的请求,同时表示它也可以在执行完内核所请求的函数后向内核传回执行的结果。这种通讯的实现主要是依靠ioct1()系统调用,使用ioct1()函数可以通过socket这种特殊的设备文件执行指定的设备控制操作。在这种通过ioct1()进行的内核与用户空间通讯中定义了三种控制操作命令(command),这些命令可以在PCnet32中的ioct1()函数中得到处理,同时用户空间进程也使用这些命令来通过ioct1()系统调用陷入内核,对内核数据进行相对应的操作。为此设定了三种命令,分别是PCNET32UD COPY、PCNET32UD INVOKE和PCNET32UD RETURN,这三个命令对应的作用是:拷贝所需的内核数据、在内核中等待一个调用请求和在用户空间处理了一个内核所请求的函数后传回执行结果。

2.1.2内核API

因为分离驱动的设计需要在内核和用户空间之间来回传输数据,一方面有向用户空间发送内核请求的函数所需要的参数,另一方面是在用户空间执行所请求的函数后,将得到的结果返回内核。因此使用了Linux内核API,copy_to_user()和copy_from_user()来完成这样的通讯。

同时,用户线程通过ioct1操作陷入内核,内核线程通过内核请求而被唤醒,使用信号量同步这两者的操作顺序。信号量操作通过Linux的sema_init()、up()、down()操作来完成。

2.2用户空间的接口

在这个设计中需要一些特定的内核数据结构,以及需要驱动的一些常量在用户空间可用,原因是这些参数将被移动到用户空间的驱动函数所访问。因此增加了pcnet32.h头文件,包含了在/usr/include/linux中的一些数据结构。这个头文件也包含了新增的'user_fn_args'结构,这个结构体包含所要请求的函数标识符和不同内核请求所请求的函数需要的不同的参数的数据类型,在把这个数据结构传给用户空间函数之前它就需要被初始化赋值。

2.3实现架构

图4表明了代码的框架,实现了对分离后的驱动代码的控制。整个控制的流程可以总结如图4。

(1)首先,用户空间进程发起PCNET32UD COPY命令的ioct1()调用,通过这个调用拷贝所需的数据结构到用户空间,例如设备的私有数据结构。

(2)接下来,用户空间进程调用PCNET32UD REQUEST命令,这将导致用户空间线程在内核中等待信号量直到有内核请求的到来而唤醒它。

(3)当收到一个内核请求的时候,内核函数把这个请求入队并且唤醒等待的用户线程。然后它在申请获得第二个信号量时睡眠,等待从用户空间函数发回的响应结果。

(4)使在请求队列队头的请求出队,由此唤醒用户空间进程,在拷贝完user fn args数据结构和一些所需的参数到用户空间后,将引起PCNET32UD INVOKE命令的ioct1控制返回到用户空间。

(5)用户空间进程根据传回的函数标识符调用相应的函数。在执行完函数后,用户空间进程调用PCNET32 RETURN命令的ioct1()系统调用,通过ioct1()调用中的指针参数将结果返回内核空间。

(6)内核通过PCNET32 RETURN命令的ioct1()系统调用拷贝返回的结果到内核缓冲区,并且唤醒等待着第二个信号量的内核函数的线程,这个线程再将结果返回给原始调用函数。

3 评价与测试

3.1分离到用户空间的函数

PCnet32驱动程序有54个函数,其中包括初始化和退出模块,在分离驱动的设计中,将其中pcnet32_get_drvinfo,pcnet32_get_ringparam,pcnet32_get_msgleve1,pcnet32_set_msglevel移到用户空间。这四个函数可以归为‘get'和'set'方法,这些方法是通过ethtool接口调用来获得或改变设备设置的,例如rx/tx ring参数,信息级别等。比起收发操作,ethtool命令执行的频率并没有那么高,所以这些函数是很适合移动到用户空间的。这些ethtool函数必须被所有的网络驱动实现,所以把它们移动到用户空间也适用于其他的驱动程序。

3.2验证分离代码的驱动程序运行的正确性

通过执行ethtool命令来验证分离驱动架构的正确性,所对应的执行命令如表1。

对比使用原驱动程序与分离后的驱动程序运行后的结果,发现结果所获得的信息结果一致,所以将驱动程序分为一部分运行在用户空间一部分运行在内核并不影响驱动函数的执行功能。

3.3测试性能

为了衡量分离驱动程序后的驱动性能,这里主要是测试了在内核运行的函数的性能以及移动到用户空间的函数的性能。这些测试是在虚拟机环境下进行的,所以也受到虚拟机的开销影响,如果将这个分离了的设备驱动运行在实际的硬件环境下会有更准确的测量结果。

表2给出了在原有驱动模块和在分离后的设备驱动上执行这些ethtool命令所需要的时间,可以看到相对原有驱动来说,分离后的设备驱动在时间消耗上有较大的增加。

当在用户空间执行时,进程拥有的只是普通的权限不能作一些特定的操作,当进程在内核态执行时拥有很高的特权,几乎可以做任何事情。所以,任何在用户空间执行的操作必须要与内核相交互,因此分离后的设备驱动会比原来以一个内核模块运行在内核中的驱动程序的执行效率低。不过,因为移动到用户空间的这些内核函数通常不会被频繁的调用,所以对分离后的驱动程序运行的效率不会有很大的影响,并且即使分离驱动后开销有所增加,但也是在可承受范围内。

4 结论

本文给出了以PCnet32驱动为例的分离设备驱动的方法,证明了将没有被频繁调用的驱动程序代码移到用户空间不会对性能造成大的影响。这意味着内核可以使用这个方法隔离驱动漏洞,从而避免其导致的系统崩溃,而依然保留在内核的驱动关键代码也不会因为分离驱动代码而受到性能方面的影响。