内核零内存拷贝策略

本文介绍了一种在Linux环境下实现MPEG4视频编码模块零拷贝的技术方案,通过巧妙利用mmap系统调用和内核内存管理机制,避免了数据在用户空间与内核空间之间的复制,有效提升了系统的整体效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Linux驱动和用户程序之间内存零拷贝实现


本文的方法在Linux 2.4.19内核上实现。
假设我们有一个流设备(比如IME6400 MPEG4编码的视频编码器),那么常理驱动程序设计一定会采用循环缓冲区来控制mpeg4数据流,而且数据流会从内核空间拷贝到用户空间,这存在一些不 足之处。因为采用一般的设备驱动设计方法,驱动看起来会按照如下实现:在用户空间,用户程序先调用open系统调用打开MPEG4编码板设备,即执行驱动 的open方法,然后驱动会申请一块内核存储空间作为读取编码数据的缓冲区。该缓冲区同样设计为循环缓冲区,中断函数向里面写入编码数据,用户进程调用 read方法时从其中读取数据,缓冲区的同步仍然需要在中断函数和read方法中协调完成。如果用户进程“消费”数据过快,则它调用read时缓冲区将无 数据可读,这时,当前进程就睡眠在驱动定义的等待队列(wait queue)里(称为阻塞型IO操作),此后等待中断函数有数据写入缓冲区时唤醒进程。需要注意的是当进程睡眠被唤醒并不能保证等待的事件已经发生了,有 可能是其他原因如收到信号被唤醒,此时进程发现缓冲区仍然没有数据将继续睡眠。所以,等待队列实现起来效率是十分低的,比较适用于低速设备。
Linux为了保护内核关键数据,区分内核空间和进程空间,这给内核空间和用户空间的数据交换带来一定的制约。由于许多原因,用户进程无法直接访问内核存 储空间,驱动程序需要调用copy_to_user函数将编码数据拷贝给用户进程,这样,数据流向是像这样的:MPEG4编码设备-> 中断读取到驱动缓冲区 ->用户进程缓冲区 -> 播放或者发送至网络,因此所有数据都将被拷贝了两次,先从外部设备到内核缓冲区,然后从内核缓冲区到用户空间缓冲区。这种方法所占用的存储资源也较多,因 为内核驱动和用户进程都需要申请存储空间。还有,频繁的调用系统调用本身也带来了额外的开销。


(一)、零拷贝策略
用户进程和驱动共享同一块物理内存,使得它们之间不需要数据拷贝,这就是零拷贝策略。零拷贝策略的关键是mmap系统调用,也就是把内核存储空间映射到用 户进程空间的方法,它使应用程序和内核空间共享物理内存成为可能。实现的方法是,在内核空间申请一片内存,即用作这里所说的循环缓冲区,然后用户进程调用 设备的mmap函数来将内核的虚拟地址空间映射到进程的地址空间。因此我们的设备驱动程序需要编写mmap方法,但是在下面我们会看到,因为我们采取了十 分巧妙的内存分配措施,驱动不需要mmap方法,而只需要调用/dev/mem的mmap方法。
实现零内存拷贝后,用户程序和驱动程序之间的循环缓冲区同步就变得比较简单了,我们取消了一般驱动设计的阻塞型IO(当然任何时候都可以实现非阻塞型IO 操作),即不需睡眠等待队列,并编写设备驱动的ioctl方法为应用程序提供前移head指针的操作。如前所述,中断往缓冲区写入编码数据,并移动 tail指针,应用程序直接将缓冲区的数据发送到网络,它调用ioctl方法前移内核中缓冲区的head指针,控制循环缓冲区的大小。而且,由于每次中断 写入的一定是1024个字节(IME6400产生1024字节数据时产生一次中断),所以只要缓冲区有数据,我们每次移动head指针时也移动1024个 字节,即相当于每次“读取”1024个字节,简化了缓冲区管理。这样,我们的设备方法file_operations除了open、release只需要 ioctl方法,因为零拷贝根本就不需要read函数。
struct file_operations IME6410_fops = {
ioctl:    ioctl_IME6410,
open:    open_IME6410,
release:    release_IME6410,
};
(二)、如何获取大量连续物理内存
采用零拷贝方法以后,我们考虑获取内存的问题。依我的系统为例,总的物理内存(SDRAM)为8M,除去内核以及文件系统占用的约3M多空间,分配了1M 的物理内存空间作为循环缓冲区是比较合理的,这样既不影响系统的稳定性又最大化MPEG4编码模块驱动的最大缓冲区,剩余还有2M多的内存对运行其他程序 已经足够。
那么,该如何得到这1M地址空间呢?在Linux内核中,可以有多种分配物理内存的方法,第一种是kmalloc函数,该函数类似于C语言标准库中的 malloc函数,而且它分配的区域在物理内存中也是连续的,但是在任何情况下,kmalloc可以分配的最大内存是128k,虽然我们可以分配多个缓冲 区加以管理,但这无疑提高了驱动开发的复杂度而且增加了缓冲管理的执行开销,故它不适合采纳。我们再来看看第二种方案,__get_free_page函 数,__get_free_page是直接对物理页面进行操作的函数,可以直接分配调用者指定的物理页面并且最大可达2M。 __get_free_page可以实现我们的目标,但用__get_free_page动态分配大块内存的问题在于,对内存十分有限的系统,在某些情况 下有可能会运行不正常,驱动程序需要冒分配失败的风险。对于我们的MPEG4编码采集应用,稳定是最重要的,而且不管在什么情况下,系统最重要的进程任务 就是编码采集的进程。设计的最优目标就是在系统运行比较苛刻的情况下编码采集进程的内存资源仍然能够得到满足。
本文采用了一个十分巧妙的方法达到这个设计目的。从内核“偷”1M内存过来,前面已经提到,Linux内核编译时的配置参数CMDLINE,此参数告诉内 核内存大小、控制台设备、root设备等等信息。我们的主控平台有8M大小SDRAM,但是我们设置CMDLINE时只设置了7M,因此得到了最后那1M 大小(即从物理地址0×20700000-0x207FFFFF)的物理内存,用它来作为循环缓冲区。当然,这1M内存仍然需要建立页表。
这样做的可行性在哪里?内核只知道自己有7M的内存,它当然不会去碰剩余的1M内存,我们的数据在里面是十分安全的。而且内核不会在处理某个地址时判断它 是否处于自己的存储空间范围内,Linux系统相信自己的内核,认为内核代码是可靠的,因此内核在处理0×20700000-0x207FFFFF的地址 段时,只要我们建立好页表映射,内核可以读写这段地址空间,但是它本身却不知道这段空间的存在!那么如何建立这1M内存的页表呢?我们这里不深入展开讲 Linux的内核虚拟内存管理,简单地来看ARM体系结构的Linux内核在启动时如何建立内核页表。与ARM内存管理相关的代码在../arch /arm/mm/mm-armv.c文件里面,内核初始化建立内核页表的函数是void __init memtable_init(),该函数又是通过create_mapping(struct map_desc *md)来建立页表的。仔细分析memtable_init()的代码,可以发现前面的7M内存被看做一个struct map_desc建立了页表,我们需要做的是构造一个自己的struct map_desc描述符,它的信息需要设置成后1M内存,然后调用create_mapping()建立我们所要的页表:
struct map_desc *init_maps;
……
// add by pwj, system mem 8M
init_maps->physical   = 0×20000000+0×00700000; 
printk(“trick bank start 0x%x\n”, init_maps->physical);
init_maps->virtual    = __phys_to_virt(init_maps->physical);
printk(“trick bank virtual start 0x%x\n”, init_maps->virtual);
init_maps->length     = 0×100000;    // 大小1M
// tricky
printk(“trick bank size 0x%x\n”, init_maps->length);
init_maps->domain     = DOMAIN_KERNEL;
init_maps->prot_read  = 1;    // 可读
init_maps->prot_write = 1;     // 可写
init_maps->cacheable  = 0;    // 这块1M区域相当于外设存储空
// 间,故设置成
init_maps->bufferable = 0;     // 不能cacheable和不能bufferable
create_mapping(init_maps);
建立这1M内存的页表以后,再来看用户程序的操作,用户程序需要将内存的最后这1M空间映射到自己的地址空间,它可以通过调用/dev/mem的mmap 方法来实现。通过/dev/mem设备文件的mmap系统调用,可以将线性地址描述的物理内存映射到进程的地址空间,然后就可以直接访问这段内存了。
#define STARTMEM 0×20700000     // 物理地址从0×20700000开始
fd = open(“/dev/mem”, O_RDWR);
addr = mmap(0, 1024*1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, STARTMEM);
以上这段代码很容易理解,通过调用/dev/mem设备的mmap方法,用户进程将物理内存STARTMEM开始的1024*1024字节映射到自己的地 址空间,得到的虚拟起始地址就是addr。完成了这些工作之后,MPEG4视频编码模块的驱动程序和用户进程就享用了1M的循环缓冲区,缓冲区的物理起始 地址是0×20700000,在用户进程中的虚拟起始地址就是addr。
最后实现的完整的零拷贝驱动结构如图:
内核零内存拷贝策略

通过以上的方法,我们实现了MPEG4视频编码模块的编码数据零拷贝,提高了嵌入式系统的效率。

http://www.jb51.net/LINUXjishu/86937.html


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值