在用户空间中编写驱动程序

在传统的模式下,包处理程序和数据链路程序运行在内核空间中,基于内核协议栈来进行业务处理。例如网络设备驱动和NETFILTER框架提供一些挂载接口供包处理程序调用,应用程序可以在内核中实现某些功能。


然而,从另外一个角度来说,在用户空间运行数据链路程序上下文的需求也是可能会发生的。Linux 在用户空间为应用程序提供了一些很方便的接口,包含更强大和更灵活的进程管理,标准的系统调用接口,简单的资源管理,丰富的XML处理库和正则表达式解析等。同时,内存隔离和独立重启技术使得调试起来更为方便。此外,内核中的程序需要遵循GPL原则,用户空间程序不用被这些要求约束。


用户层数据链路程序拥有自己的开销。网络设备驱动运行在内核上下文中,将接收到的网络包存放在内核空间的内存中,当用户程序需要使用这些数据包的内容时,需要将内核内存中的数据拷贝到用户空间中,反之亦然。同样的,在内核态和用户态来回切换,需要很大的性能开销,从而违反了数据链路程序低时延、高通量的原则。


下面,介绍一些如何降低这些开销,从而实现从用户空间数据链路程序的方法。




一、向用户空间映射内存


为了代替传统的I/O模型,Linux内核为用户层程序提供了直接在内核内存范围中进行内存映射的功能。在用户层中通过设备驱动上下文可以直接访问设备驱动的设备内存,设备驱动上下文中包含了注册配置和I/O描述。用户层应用程序访问分配内存将直接作用于设备内存上。


一些Linux系统调用允许这种内存映射,最简单的就是mmap()调用,mmap()调用允许用户空间程序映射一页或者连续的多页的物理设备地址范围。其他的系统调用包含splice()/vmsplice(),这两个函数允许用户空间读写随意的内核缓存。tee()允许用户程序在内核空间中对两块缓存进行拷贝。


物理内存和用户空间内存映射人物通常使用转换后备缓存或者TLB,对于一款特定的处理起来说,内核用来作为缓存使用的TLB表项的数量是有限的,表项的大小通常决定于处理器支持的最小页的大小,通常为4kb。


linux在初始化的时候使用固定的一小部分TLB表项来映射内核内存。然而对于应用程序来说,TLB表项的数量是一个限制并且TLB失误可能导致命中效率的减低。为了避免这些问题,Linux提出了一个Huge-TLB(大页表)的概念,允许用户空间应用程序映射大于4kb大小的内存页,这个映射不仅可以用于应用数据,也可以用于文本段。


Linux中很多高效的机制都支持在内核空间和用户空间之间的零拷贝机制,通过内存映射或者其他的技术来实现,这些同样可以用于数据链路程序中。但是这些机制还是会占用CPU周期,并且系统准备数据包仍然消耗较高的资源。用户层进程直接访问硬件可以消除任何机制中的内核空间与用户空间的数据与状态的转换,这样就可以减少系统准备数据包的消耗。


二、UIO驱动


linux提供了标准的UIO框架来开发用户空间的设备驱动,UIO框架定义了一个小的内核组件来执行以下两个关键任务:
a.为用户空间指明设备内存的区域范围。
b.注册设备中断和为用户空间提供中断指示。


内核空间UIO组件通过一组类似/dev/uioxx的系统项来暴露出来,用户空间程序找到这些系统项,读设备地址范围冰鞋映射到用户空间内存中。


用户空间程序可以执行所有设备管理的I/O操作功能。对于中断,需要在设备的系统项上执行一个块的read()操作,根据结果可以实现内核组件让用户进程休眠或者唤醒。








三、用户空间网络驱动


网络设备驱动需要的内存有以下三类:


a.配置空间:这指的是设备配置注册的内容。
b.I/O描述空间:这指的是设备用来访问设备数据的描述。
c.I/O数据空间:这指的是设备实际的I/O访问数据。


用网卡设备来举例的话,以上三个可以指通常的设备配置(包含  MAC 配置),缓冲区描述环和数据包缓冲区。


对于传统的内核空间网络驱动,所有的三个区域都映射在内核空间,用户空间对它的任何访问通过VFS对网卡的抽象接口的ioctl()或者 read()/write()系统调用,通过拷贝来实现内核空间与用户空间的数据交互。如图1



用户空间的网络驱动,采用其他的方法,将以上三个区域直接映射到用户空间内存上,这就允许用户空间程序直接控制描述环缓冲。应用程序直接数据缓冲来避免拷贝带来的开销。如图2.




1.用户空间驱动面临的问题:
直接访问网络设备带来了一些问题,这些本来是由内核协议栈或者是系统调用隐藏的:
a.多个应用程序共享一个网络设备。
b.阻塞访问网络数据
c.缺乏网络堆栈服务,如TCP / IP协议
d.数据包的内存管理
e.应用重启时的资源管理。
f.缺乏标准的驱动访问接口。




共享设备


 和Linux系统的套接层允许多个应用程序打开socket--TCP,UDP,raw IP不同,用户空间驱动只允许一个一个应用程序通过接口访问。然而,大多数网络接口时下在接收和发送方向提供多个缓冲区描述符环。此外,这些接口还提供了某种形式的硬件分级机制来传入流量分流到这些多个环。这样的机制可以用于单个缓冲区描述符环映射到不同的应用程序。这又限制了其应用在单一接口硬件设备支持环的数目的数目。另一种情况是开发一个调度框架,在用户空间的驱动器,将处理多个应用程序。




阻塞访问数据


与传统的基于socket的访问不同,用户空间程序的套接字可以阻塞到数据已经完全准备好,或者通过select()/poll()去等待多个输入。用户空间程序需要经常地
去轮询缓存描述环,以确定数据是否到来。这可以用阻塞的read()调用UIO设备项来解决,这种方式使得用户程序在网卡设备接收中断时被阻塞,同时应用程序提供了突破需要被中断通知的限制。
换句话说,替代了每个包到来时的中断。这样当缓冲区描述项返回给其他执行任务时,可以采用轮询机制来处理一系列的数据缓存。当所有的缓存描述项数据被消费完后,在其他数据到来之前,
应用程序可以执行read()进行阻塞。




协议栈服务


Linux的协议栈和socket接口是基于网络服务抽象的来的,例如路由查找和ARP协议。由于缺少这些服务,应用程序必须用有两项功能,类似于内核中的网络协议栈
或者本地路由转发和邻居表。




内存管理
用户空间程序同样需要处理网络设备的缓冲区,需要提供数据的存储和检索功能。除了分配和释放内存,同样需要为网络设备提供用户空间虚拟地址和物理地址的转换能力。
但是在运行时进行这样的转换是非常需要消耗资源的。同样,处理器的TLB数量被限制,性能可能会受到比较大的影响。另一个方法是使用Huge-TLB分配一大块内存,在这个内存块上
进行数据的操作。




应用程序重启
应用程序负责分配和管理设备资源和当前的设备状态。当程序出现异常或者在不受控制和未执行清理的情况下重启。
设备会处于一种不稳定状态。一种解决方法是,内核空间的UIO组件跟踪应用程序的状态和重启事件,重新
设置设备和应用程序创建的内存映射。


用户标准接口
当前用户空间网络驱动提供一个底层的编程接口,规定了设备的操作规范,而不是整合标准的系统调用API,例如
open()/close(), read()/write() 和send()/receive()。设备特殊的API意味着应用程序需要移植每个用到的网络设备。


带有限制的自由


UIO框架提供了用户空间程序直接访问网络设备的自由,但是这个过程中也存在一定的限制,如资源和内存管理。当前的用户空间网络驱动
在一些特定的网络设备上被单进程访问的时候有较好的表现。然而,未来的这些驱动必须解决这些限制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值