当前位置: 代码迷 >> 综合 >> linux内核笔记(五)高级I/O操作(三)
  详细解决方案

linux内核笔记(五)高级I/O操作(三)

热度:59   发布时间:2024-02-12 11:25:29.0

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");

__get_free_page动态分配了一页内存,内核空间按页来管理内存,在进行映射时,地址要按照页大小对齐。

模块卸载时使用free_page来释放之前分配的内存。

read接口函数即vfb_read,首先判断了读取的字节数是否超过了分配内存的大小(PAGE_SIZE是页大小的宏,通常为4096字节),如果超了,则限定最多只能读一页的数据。然后用copy_to_user返回未成功复制的字节数,全部复制成功返回0。

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:用户指定的映射之后的虚拟起始地址,如果没有指定,那么由内核来指定

pfn:物理内存所对应的页框号,就是物理地址除以页大小得到的值

size:想要映射的空间的大小

prot:该内存区域的访问权限

通过该函数一片物理内存区域将被映射到用户空间,而这边物理内存本身在之前又被映射到内核空间,所以这片物理内存区域被映射了两次,在用户空间和内核空间都可以访问。

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;//调用了mmap系统调用,第一个参数是映射的起始地址(通常为NULL由内核来决定),第二个参数32是要映射的内存空间的大小,第三个参数 PROT_READ | PROT_WRITE表示映射后的空间是可读、可写的,第四个参数是多进程共享,最后一个参数是位置偏移量,0表示从头开始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);
}

定位操作

支持随机访问的设备文件,访问的文件位置可以由用户来指定,并且对于读写这类操作,下一次访问的文件位置将会紧接在上一次访问结束的位置之后,上面的模拟的虚拟显卡设备并不支持这一操作。首先每次读取的位置都是从文件最开头的位置开始的,形参pos没有用上,没有llseek系统调用所对应的接口函数。

要让驱动支持定位操作,首先看pos形参的作用。文件对用户的抽象是一段线性储存的数据,可以把文件看出一个数组,每个数组元素占用一个字节,那么pos参数就是访问这个数组的下标的地址。

例如,虚拟显卡分配了一页内存,即文件的内容,如果一页是4096字节,那么*pos的值可以为0~4095,*pos就指定了要访问的数据的地址相对于起始地址偏移的字节数。不同于普通文件的是,这个设备的大小是固定的。而且,虚拟显卡设备在每次读取后,驱动应该负责更新*pos值。

和llseek对应的驱动接口是file_operations结构体中llseek函数指针指向的函数。

loff_t (*llseek) (struct fo;e *, loft_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;//将新的文件位置值更新到file结构的f_pos成员中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");

 

#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);
}