目录
mmap设备文件操作
定位操作
习题
mmap设备文件操作
显卡一类的设备有一片很大的显存,驱动程序将这片显存映射到内核的地址空间,方便进行操作。如果用户想要在屏幕上进行绘制操作,将要在用户空间开辟出一片至少一样大的内存,将要绘制的图像数据填充在这片内存空间中,然后调用 write 系统调用,将数据复制到内核空间的显存中,从而进行图像绘制。不难发现,在这个过程中有大量的数据要复制,对于显卡这类对性能要求非常高的设备,这种复制带来的性能损耗显然是不可接受的。
要消除这个复制操作就需要应用程序能够直接访问显存,但是显存被映射在内核空间,应用程序没有这个访问权限。字符设备驱动提供了一个 mmap 接口,可以把内核空间中的那片内存所对应的物理地址空间再次映射到用户空间,这样一个物理内存就有了两份映射,或者说有两个虚拟地址,一个在内核空间,一个在用户空间。这样就可以通过直接操作用户空间的这片映射之后的内存来直接访问物理内存,从而提高了效率。下面是一个虚拟的帧缓存设备的驱动程序,其实现了 mmap 接口
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define VFB_MAJOR 256
#define VFB_MINOR 1
#define VFB_DEV_CNT 1
#define VFB_DEV_NAME "vfbdev"
struct vfb_dev {
unsigned char *buf;
struct cdev cdev;
};
static struct vfb_dev vfbdev;
static int vfb_open(struct inode * inode, struct file * filp)
{
return 0;
}
static int vfb_release(struct inode *inode, struct file *filp)
{
return 0;
}
static int vfb_mmap(struct file *filp, struct vm_area_struct *vma)
{
if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(vfbdev.buf) >> PAGE_SHIFT, \
vma->vm_end - vma->vm_start, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
ssize_t vfb_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
int ret;
size_t len = (count > PAGE_SIZE) ? PAGE_SIZE : count;
ret = copy_to_user(buf, vfbdev.buf, len);
return len - ret;
}
static struct file_operations vfb_fops = {
.owner = THIS_MODULE,
.open = vfb_open,
.release = vfb_release,
.mmap = vfb_mmap,
.read = vfb_read,
};
static int __init vfb_init(void)
{
int ret;
dev_t dev;
unsigned long addr;
dev = MKDEV(VFB_MAJOR, VFB_MINOR);
ret = register_chrdev_region(dev, VFB_DEV_CNT, VFB_DEV_NAME);
if (ret)
goto reg_err;
cdev_init(&vfbdev.cdev, &vfb_fops);
vfbdev.cdev.owner = THIS_MODULE;
ret = cdev_add(&vfbdev.cdev, dev, VFB_DEV_CNT);
if (ret)
goto add_err;
addr = __get_free_page(GFP_KERNEL);
if (!addr)
goto get_err;
vfbdev.buf = (unsigned char *)addr;
memset(vfbdev.buf, 0, PAGE_SIZE);
return 0;
get_err:
cdev_del(&vfbdev.cdev);
add_err:
unregister_chrdev_region(dev, VFB_DEV_CNT);
reg_err:
return ret;
}
static void __exit vfb_exit(void)
{
dev_t dev;
dev = MKDEV(VFB_MAJOR, VFB_MINOR);
free_page((unsigned long)vfbdev.buf);
cdev_del(&vfbdev.cdev);
unregister_chrdev_region(dev, VFB_DEV_CNT);
}
module_init(vfb_init);
module_exit(vfb_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <email>");
MODULE_DESCRIPTION("This is an example for mmap");
代码第75 行使用__get_free_page 动态分配了一页内存(关于内存分配的知识我们在后面会较为细致的学习),内核空间按页来管理内存,在进行映射时,地址要按照页大小对齐。代码第 98 行在模块卸载的时候使用 free_page 来释之前分配的内存。
代码第 50 行至第 56 行是操作方法集合的结构,为了说明问题,我们只实现了open、release、 mmap 和 read,其中 open 和 release 接口只是简单地返回 0表示操作成功。而read接口函数,即 vfb_read 在第13 行首先判断了读取的字节数是否超过了分配内存的大小(PAGE_SIZE 是页大小的宏,通常是 4096 字节),如果超过了则限定多只能读一页数据。代码第 45 行使用 copy_to_user 内的据复制到用户间。第 47 行返回实际读取的字节数。注意,copy_to_user 返回未复制成功的字节数,全部复制成功则返回0。
代码第 32行至第 38 行是 mmap 接口的实现,在这里主要是调用了 remap_pfn_range,该函数的原型如下。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot);
第一个参数 vma 是用来描述一片映射区域的结构指针,一个进程有很多片映射的域,每一个区域都有这样对应的一个结构,这些结构通过链表和红黑树组织在一起。该结构描述了这片映射区域虚拟的起始地址、结束地址和访问的权限等信息。第二个参数addr 是用户指定的映射之后的虚拟起始地址,如果用户没有指定,那么由内核来指定该地址。第三个参数是物理内存所对应的页框号,就是将物理地址除以页大小得到的值.第四个参数是想要映射的空间的大小。最后一个参数 prot 是该内存区域的访问权限。过该函数后,一片物理内存区域将会被映射到用户空间,而这片物理内存本身在之前》被映射到了内核空间,所以这片物理内存区域被映射了两次,在用户空间和内核空间都可以被访问。图4.3 是对应的示意图。
代码第 34 行中的 virt_to_phys(vfbdev.buf)> >PAGE_SHIFT就是首先把在内核空间的虚拟地址 vfbdev.buf 通过 virt_to_phys 转换成对应的物理地址,然后将该物理地址右移PAGE_SHIFT 比特位(其实就是除以页的大小)得到了物理页框号。其他的实参都是vma 中的成员来指定的,而其中的起始地址、大小和权限都可以由用户在系统调用函数中指定,struct vm_area_struct 结构由内核来构造。
下面是对该驱动的测试程序。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
int main(int argc, char * argv[])
{
int fd;
char *start;
int i;
char buf[32];
fd = open("/dev/vfb0", O_RDWR);
if (fd == -1)
goto fail;
start = mmap(NULL, 32, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (start == MAP_FAILED)
goto fail;
for (i = 0; i < 26; i++)
*(start + i) = 'a' + i;
*(start + i) = '\0';
if (read(fd, buf, 27) == -1)
goto fail;
puts(buf);
munmap(start, 32);
return 0;
fail:
perror("mmap test");
exit(EXIT_FAILURE);
}
代码第 19 行调用了 mmap 系统调用,第一个参数是想要映射的起始地址,通常设置为 NULL,表示由内核来决定该起始地址。第二个参数 32 是要映射的内存空间的大小。第三个参数 PROT_READ | PROT_WRITE 表示映射后的空间是可读、可写的。第四个参数 MAP_SHARED 是指映射是多进程共享。最后一个参数是位置偏移,为0表示从头开始。
代码第23 行至第 25行是直接对映射之后的内存进行操作。代码第27 行则读出之前操作的内容,可对比判断操作是否成功。下面是编译、测试用的命令。
定位操作
支持随机访问的设备文件,访问的文件位置可以由用户来指定,并且对于读写这类操作,下一次访问的文件位置将会紧接在上一次访问结束的位置之后,上面模拟的虚拟显卡设备并不支持这一类操作。首先每次读取的位置都是从文件最开头的位置开始的,也就是说形参 pos 没有使用上;其次是没有 llseek 系统调用所对应的接口函数。
要让驱动支持定位操作,首先来看看 pos 形参的作用。文件对用户的抽象是一段线性存储的数据,那么可以把文件看成一个数组,每个数组元素占一个字节,那么 pos参数就是访问这个数组的下标的地址。例如,虚拟显卡分配了一页的内存,即文件的内容,如果一页是 4096字节,那么*pos 的值就可以为 0~4095,*pos 就指定了要访问的数据的地址相对于起始地址偏移的字节数。不同于普通文件的是,这个设备文件的大小是固定的。而且,虚拟显卡设备在每次读取后,驱动应该负责更新*pos 的值。
和lIseek 对应的驱动接口是 file_operations 结构中 llseek 函数指针指向的函数,其类型如下。
loff_t (*llseek) (struct file *, loff_t, int);
指针指向的函数有三个参数,第一个参数指向代表打开文件的 file 结构;第二个参数是偏移量;第三个参数是位置,分别是 SEEK_SET、SEEK_CUR 和 SEEK_END。llseek接口要做的事情就是根据传入的参数来调整保存在 file 结构中的文件位置值。
下面分别是加入文件定位操作后的驱动代码和应用程序代码。
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#define VFB_MAJOR 256
#define VFB_MINOR 1
#define VFB_DEV_CNT 1
#define VFB_DEV_NAME "vfbdev"
struct vfb_dev {
unsigned char *buf;
struct cdev cdev;
};
static struct vfb_dev vfbdev;
static int vfb_open(struct inode * inode, struct file * filp)
{
return 0;
}
static int vfb_release(struct inode *inode, struct file *filp)
{
return 0;
}
static int vfb_mmap(struct file *filp, struct vm_area_struct *vma)
{
if (remap_pfn_range(vma, vma->vm_start, virt_to_phys(vfbdev.buf) >> PAGE_SHIFT, \
vma->vm_end - vma->vm_start, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
ssize_t vfb_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
int ret;
size_t len = (count > PAGE_SIZE) ? PAGE_SIZE : count;
if (*pos + len > PAGE_SIZE)
len = PAGE_SIZE - *pos;
ret = copy_to_user(buf, vfbdev.buf + *pos, len);
*pos += len - ret;
return len - ret;
}
static loff_t vfb_llseek(struct file * filp, loff_t off, int whence)
{
loff_t newpos;
switch (whence) {
case SEEK_SET:
newpos = off;
break;
case SEEK_CUR:
newpos = filp->f_pos + off;
break;
case SEEK_END:
newpos = PAGE_SIZE + off;
break;
default: /* can't happen */
return -EINVAL;
}
if (newpos < 0 || newpos > PAGE_SIZE)
return -EINVAL;
filp->f_pos = newpos;
return newpos;
}
static struct file_operations vfb_fops = {
.owner = THIS_MODULE,
.open = vfb_open,
.release = vfb_release,
.mmap = vfb_mmap,
.read = vfb_read,
.llseek = vfb_llseek,
};
static int __init vfb_init(void)
{
int ret;
dev_t dev;
unsigned long addr;
dev = MKDEV(VFB_MAJOR, VFB_MINOR);
ret = register_chrdev_region(dev, VFB_DEV_CNT, VFB_DEV_NAME);
if (ret)
goto reg_err;
cdev_init(&vfbdev.cdev, &vfb_fops);
vfbdev.cdev.owner = THIS_MODULE;
ret = cdev_add(&vfbdev.cdev, dev, VFB_DEV_CNT);
if (ret)
goto add_err;
addr = __get_free_page(GFP_KERNEL);
if (!addr)
goto get_err;
vfbdev.buf = (unsigned char *)addr;
memset(vfbdev.buf, 0, PAGE_SIZE);
return 0;
get_err:
cdev_del(&vfbdev.cdev);
add_err:
unregister_chrdev_region(dev, VFB_DEV_CNT);
reg_err:
return ret;
}
static void __exit vfb_exit(void)
{
dev_t dev;
dev = MKDEV(VFB_MAJOR, VFB_MINOR);
free_page((unsigned long)vfbdev.buf);
cdev_del(&vfbdev.cdev);
unregister_chrdev_region(dev, VFB_DEV_CNT);
}
module_init(vfb_init);
module_exit(vfb_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <E-mail>");
MODULE_DESCRIPTION("This is an example for mmap");
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
int main(int argc, char * argv[])
{
int fd;
char *start;
int i;
char buf[32];
fd = open("/dev/vfb0", O_RDWR);
if (fd == -1)
goto fail;
start = mmap(NULL, 32, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (start == MAP_FAILED)
goto fail;
for (i = 0; i < 26; i++)
*(start + i) = 'a' + i;
*(start + i) = '\0';
if(lseek(fd, 3, SEEK_SET) == -1)
goto fail;
if (read(fd, buf, 10) == -1)
goto fail;
buf[10] = '\0';
puts(buf);
munmap(start, 32);
return 0;
fail:
perror("mmap test");
exit(EXIT_FAILURE);
}
驱动代码第 45 行和第46 行判断文件访问是否超过了边界,如果是则调整访问的长度。代码第48 行在复制时考虑到了偏所带来的影响,代码第 49 行则是更新位置值。代码第54行至第78行是文件定位操作的实现,根据 whence 的不同,设置了新的文件位置值,代码第72行和第 73 行则是判断新的位置值是否合法,代码第 75 行将新的文件位
置值更新到fe结构的f_pos成员中。
测试程序相对于之前的变化是在读操作之前首先使用 lseek 将文件位置定位为 3,那么之后的操作都将从文件的第3 个字节开始读取。下面是编译、测试用的命令
习题
1、 ioctl接口函数的命令不包含哪个部分(B )。
[A] 幻数 [C] 参数传输方向 [D] 参数大小 [B] 权限
2、关于 proc 文件系统说法不正确的是(D )。
[B] 通常挂载在/proc 目录下 [A] 是一种伪文件系统 [C] 包含了进程相关的信息 [D] 硬件信息主要输出到该文件系统
(更新为sysfs系统)
3.关于阻塞型 IO 说法不正确的是( D)。
[A] 当资源不可用时,进程主动睡眠
[B] 当资源可用时,由其他内核执行路径唤醒
[C] 可以设置超时后被自动唤醒
[D] 只会唤醒一个进程
4.IO多路复用在 ( D)发生阻塞。
[B] 驱动的read 接口函数中[A]驱动的poll 接口函数中
[D]select、poll 或epoll 系统调用中[C]驱动的write 接口函数中
5.关于异步IO 说法正确的是 (C)
[A] 在IO完成后系统调用才会返回
[B] 在 IO完成后不会通知调用者
[C] 在一次异步操作中,可以将多个 IO 请求合并
[D] 在 IO完成后,设备驱动直接调用调用者注册的回调函数
6.关于异步通知说法错误的是 (D )。
[A] 类似于中断
[B] 可以获取资源的具体状态是可读还是可写
[C] 是由驱动来启动信号的发送的
[D] 当打开一个字符设备文件后,异步通知是默认使能的
7.mmap 的最大优点是 ( B)。
[A] 将用户空间的一片内存映射到内核空间,从而提高效率
[B] 将内核空间的一片内存映射到用户空间,从而提高效率
[C] 使字符设备可以实现随机访问
[D] 额外分配了物理内存,从而提高了效率
8,关于文件定位操作说法正确的是( D)。
[A] 当一个进程定位到文件的一个位置后,另一个打开同样文件的进程所访问的文件位置也随之变化
[B] 串口这类字符设备是必须要实现文件定位操作的
[C] 在字符设备驱动中实现了 lseek 后,可以让所有的字符设备都随机访问
[D] 大多数字符设备都不支持文件定位操作