前言
Linux 编程中,大多数的场景,数据的交换,不论读还是写都要经过两次数据拷贝过程:用户和内核,内核和硬件物理内存
如果数据的访问量比较小,两次的数据拷贝对系统性能影响几乎可以忽略不计
如果数据的访问比较大,两次的数据拷贝势必影响系统性能
数据的操作的规律是源要不是用户或者硬件,目的要不是硬件或者用户,而内核仅仅作为一个缓冲,所以用户到内核的数据拷贝是多余的,如果让用户在用户空间访问硬件设备的物理内存,即可将两次数据拷贝变成一次数据拷贝
以下设备的数据量的访问比较大:摄像头,声卡,显卡,LCD等
如何让用户在用户空间访问到硬件设备的物理地址呢?将数据的拷贝由2次变成1次,加快数据的访问速度呢?
利用大名鼎鼎的mmap机制。
作为驱动层,如果我们所涉及的驱动设备数据的量比较大,我们在驱动层,最好根据需要,去支持mmap机制,将设备物理地址映射到用户空间的虚拟地址上,一旦映射完毕,对设备的访问将来都放在用户空间来进行
回顾mmap系统调用的使用:
void *addr;
fd = open("a.txt", ...);
addr = mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
memcpy(addr, "hello,world", 12);
作用:将文件a.txt(硬盘物理信息)映射到用户3G虚拟地址空间中的MMAP虚拟内存映射区中,一旦映射完毕将来访问用户的这块虚拟内存就是在访问对应的物理地址(物理内存),不再经过内核的数据拷贝,加快文件的访问速度。
第一个实参0:告诉内核,请在当前进程的3G虚拟地址空间的MMAP内存映射区中找一块空闲的虚拟内存用来映射文件
第二个实参0x1000:空闲的用户虚拟内存的大小,必须是页面大小的整数倍
返回值addr:内核将内核帮你找的那块空闲虚拟内存区域的首地址返回到用户空间,将来用户在用户空间访问这块虚拟内存就是在访问硬件的物理地址
这样的操作,就会绕过用户到内核,内核到硬件,从而达到直接从 用户内存空间 到 硬件上,两次拷贝就缩减成一次拷贝了。
对应底层驱动的mmap接口:
struct file_operations {
// 指向内核创建的描述空闲虚拟内存区域的对象
int (*mmap)(struct file *file, struct vm_area_struct *vma);
};
mmap 系统调用过程
1. 应用程序调用mmap系统调用函数,首先调用GLIBC的mmap的函数实现
2. GLIBC的mmap函数将会做两件事:
保存的mmap的系统调用号到对应的寄存器,ARM架构下的R7寄存器。
调用svc触发的软中断异常。
3. 进入内核准备好的软中断处理入口
从寄存器去除之前保存的mmap系统调用号
以系统调用号为索引,在系统调用表中找到对应的内核函数sys_mmap(内核)
4. sys_mmap 内部完成两件事儿
a. 内核会在当前进程的3G 的mmap内存映射区中找一块空闲的虚拟内存,将来用来映射物理地址(物理内存)
b. 一旦内核找到这个空闲的内存区域,内核用 strcut vm_area_struct 结构体创建一个对象,描述这块虚拟内存。
c. 最终调用底层驱动的mmap接口,将创建是 struct vm_area_struct 对象的首地址传递给驱动的mmap接口
5. 底层驱动mmap接口利用第二个形参vma指针即可获取空闲虚拟内存的属性。
底层驱动的mmap需要做什么
函数功能
将物理地址映射到用户虚拟地址
参数
vma:指向内核创建描述空闲虚拟内存区域的对象
addr:空闲虚拟内存的起始地址vm_start
pfn:将物理地址右移12位
size:空闲虚拟内存的大小vm_end - vm_start
prot:空闲虚拟内存的访问权限vm_page_prot
切记:addr,pfn一定是页面大小的整数倍
切记:如果对设备进行输入和输出的操作(GPIO),一定要关闭cache功能
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
Linux 内核分离思想
Linux 内核分离思想依据内核的设备-总线-驱动编程模型来实现
一个完整的设备驱动,必须包含纯硬件 + 纯软件。
驱动在正式运行期间,分离的纯硬件、纯软件必须再次结合。
采用 设备 - 总线 - 驱动模型编程,驱动开发者只需关注,如下对象:
platform_device
name: 硬件节点的名称,用于匹配,十分重要。
id: 硬件节点的编号,如果只有一个硬件,id = -1; 如果有多个name相同的硬件设备,那么采用id 来进行区分,编号从0开始,1,2...
dev:需重点关注,此数据结构中重点关注 void * platform_data 字段,如下介绍。该字段是用于装载自定义的硬件的信息。
resource:
start,硬件的起始信息;
end, 硬件的结束信息;
flags,硬件信息的标识:
IORESOURCE_MEM: 内存资源,内存地址,寄存器地址等用此宏来表示。
IORESOURCE_IRQ: IO资源,GPIO编号,中断号等用此宏来表示。
这个结构很有意思,里面有三个指针,parent,sibling,child,是个tree-like,回头单独再看,此时先pass。
num_resources:resource类型的硬件信息的个数
以上的这些字段,都是需要特别注意,特别处理的,其中 dev.platform_data 装载自定义的硬件信息,resource 装载resource类型的硬件信息,二者可同时使用,也可单独使用。
具体的编程步骤
1. 定义并初始化描述信息对象
struct platform_device plat_dev = {
.name = "xxxx",
.id
.dev = {
.platform_data = 用户自定义硬件信息
}
.resource = resource 描述的硬件信息
.num_resources = resources
....
};
2. 向内核的 dev 链表注册硬件信息,通知内核进行遍历 drv 链表进行匹配工作。
extern int platform_device_register(struct platform_device *);
3. 卸载硬件信息
extern void platform_device_unregister(struct platform_device *);
platform_driver
这个数据结构的作用是描述纯软件信息
driver: 重点关注其中的 name字段,用于匹配。
probe:硬件和软件匹配成功,内核调用该接口。参数指向匹配成功的硬件节点。
remove:删除硬件节点或者软件节点,内核调用。参数指向匹配成功的硬件节点。
注意:probe至于做什么事,驱动开发者根据需求来实现,probe 与 remove 需对称。
纯软件编程步骤
1. 定义初始化软件节点
2. 注册软件节点到内核,内核遍历匹配
3. 卸载
总结
probe函数被调用,代表着一个完成的驱动的开始。
一般probe需要完成三个操作:
1. 通过形成 pdev,获取硬件信息
2. 处理硬件信息。该申请的申请,该注册的注册,该配置的配置,等等
3. 注册字符设备,或者混杂设备,目的是提供用户访问接口