欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Linux设备驱动开发详解——学习笔记-设备驱动来联系。在没有操作系统的情况下,工程师可以根据硬件设备的特点自行定义接口。而在有操作系统的情况下,驱动的架构则由相应的操作系统来定义。驱动存在的意义就是给上层应用提供便利。 驱动针对的对象是存储器和外设。Linux将存储器和外设分为 3 个基础大类:字符设备、块设备、网络设备。 字符设备和块设备都被 Linux 映射到文件系统的文件和目录中,通过文件系统的接口(open、read、write、close等)来访问。其中,块设备可以通过类似 dd 命令对应的原始块设备来访问,也可以通过建立文件系统,以文件路径来访问。 学习 Linux 设备驱动,要求非常好的硬件基础、非常好的软件基础、一定的 Linux 内核基础和非常好的多任务并发控制和同步的基础。学习 Linux 设备驱动要将学习的函数、数据结构等放到整体架构中去理解,才能理清驱动中各组成部分之间的关系。 驱动设计的硬件基础 驱动工程师需要掌握 处理器、存储器、接口和总线、可编程门电路、原理图、硬件

最编程 2024-01-14 12:18:34
...

常规会接触到的仪器包括万用表、示波器、逻辑仪等。

Linux 内核及内核编程

【Linux 内核的子系统】

Linux 也是一种类 Unix 系统,由 Linus 参照 Minix 系统开发而来。Linux 受 GNU 计划的广大开源程序以及互联网上广大开发者的支持,不断根据 POSIX 标准优化自身设计,已经成为风靡世界的操作系统。驱动编程的本质是内核编程,Linux 内核包括进程调度内存管理虚拟文件系统网络接口进程间通信这五大子系统。

  • 进程调度:使进程在就绪、执行、睡眠、暂停等几个状态之间切换。Linux 内核通过 task_struct 结构体描述进程并管理各种资源。当用户进程有访问底层资源或硬件的需求时,则通过系统调用进入内核空间。对于需要并发支持的进程,还可以启动内核线程。
  • 内存管理:将低地址空间作为用户空间,高地址空间作为内核空间。(如 4G 内存则按 3G 用户空间及 1G 内存空间来划分)内存管理主要对页面管理、内核空间 slab、用户空间 C 库二次管理提供支持。
  • 虚拟文件系统:对硬件系统进行抽象,对上层应用提供统一的操作接口,由底层文件系统,或者设备驱动中实现的 operations 结构体的操作函数提供支持。
  • 网络接口包括网络协议和网络驱动程序,协议规定了通信方式,驱动程序则完成具体的通信工作。
  • 进程间通信:Linux 提供了多种 IPC 方式,如信号量、共享内存、消息队列、管道、UNIX 域套接字 等。Android 中则还提供了 Binder 的 IPC 方式。

Linux 内核的配置系统包括 3 个部分:Makefile、Kconfig 配置文件、配置工具。

【Linux 内核的引导】

对于 RAM Linux 来说,Soc 一般都内置了 bootrom ,上电后 CPU0 去执行 bootrom,再由 bootrom 引导 bootloader。其他 CPU 则会进入 WFI 状态等待 CPU0 的唤醒。bootloader 会去引导内核启动,在这个启动阶段 CPU0 使得用户空间的 init 程序被调用,派生出其他进程。启动的过程中,CPU0 还会唤醒其他 CPU 均衡负载。

zImage 内核镜像实际上包括未被压缩的解压算法被压缩的内核组成。程序从 bootloader 跳入 zImage 后,调用 zImage 中的解压算法,将内核解压出来。

Linux 内核模块

要想将自己的功能包含到内核中,可以有两种方式:直接编入内核、编译为模块导入。编译为模块的方式,能够使内核更加简洁,而且使系统更加灵活。模块通常会包括:模块加载函数、模块卸载函数、模块许可证声明、模块参数(可选)、模块导出符号(可选)、模块作者等信息声明(可选)等。

模块参数可以在 insmod 时传递:insmod <param_name> = <val>。也可以由 bootloader 通过 bootargs 进行传递。

模块导出符号是指将本模块的符号导出到内核符号表中,供其他模块使用。

Linux 文件系统与设备文件

Linux 系统的字符设备和块设备都体现 “一切皆文件” 的设计思想。Linux 系统对文件的基本操作包括:创建、打开、读写、定位、关闭等。系统调用和 C 库文件在文件操作 API 的定义上有些许不同。

Linux 的目录中,包括真实存在的文件系统,以及虚拟文件系统。

虚拟文件系统是应用程序和驱动程序之间的桥梁,二者之间的沟通就是通过 VFS 提供的设备节点来实现的。VFS 和 驱动之间则通过驱动程序提供的 file_operations 来沟通。对于块设备,可以有两种访问方法:一是跨过文件系统直接访问裸设备,通过 Linux 统一的 def_blk_fops 这一 operations 来实现;二是借助文件系统访问块设备。文件系统会把对文件的读写转换为对块设备原始扇区的读写。

驱动程序设计中,最关心与文件相关的两个结构体:file、inode。file 结构由内核创建,其成员与各个文件操作相关。inode 结构包含了访问权限等许多文件信息,对于设备文件,该包括了设备编号(高12位主设备号+低20位次设备号)。主设备号则对应一类驱动,次设备号则描述一个具体的设备。

在 Linux 2.4 版本的内核中引入了 devfs 来管理设备驱动,它能够自动在驱动初始化时创建节点,在卸载时删除节点,自动分配主设备号,并且为用户空间提供修改驱动所有者和权限的方法。而在 Linux 2.6 版本的内核中,udev 取代了 devfs。取代的理由是,devfs 的灵活性不符合内核设计中对于机制固定的要求,应当到用户空间中去实现对设备驱动的管理。

udev 工作在用户空间,在设备加入或移除时通过 netlink 发送热插拔事件 uevent,根据内核反馈的信息来完成节点创建等内容。与 devfs 在访问设备的时候采取加载驱动的方式不同,udev 在发现设备的时候就会去加载对应的驱动。

sysfs 文件系统也是一种虚拟文件系统,产生所有硬件的层级视图,其下包括块设备、总线类型、设备类型、所有设备等目录。所有设备和驱动都会挂到总线上,由总线负责匹配。在 Linux 内核中,具体使用 bus_type、device_driver、device 来描述总线、驱动和设备。驱动和设备分开来注册,注册时并要求另一方已经存在。当设备或驱动在内核中注册时,内核都会借助 bus_type 的 match( ) 成员来对两者进行绑定。

在配置 udev 时,每一行代表一个配置规则,包括匹配部分和赋值部分。匹配部分理解为固定项选择,赋值部分理解为用户传递值(如设备文件名称等)。udev 的赋值功能使得内核能够根据设备的序列号或其他固定信息来进行确定的映射,避免了 devfs 中的不确定映射带来的用户无法直接确定物理设备对应的设备文件这一方面的困扰。

Android 中没有采取 udev,而是使用了 vold,它们的机制是一样的。在 vold 中,也是监听基于 netlink 的套接字(NetlinkManager.cpp 中实现),解析收到的消息。

字符设备驱动

内核通过 cdev 结构描述一个字符设备,cdev 结构包含两个重要的成员:驱动设备的设备号(12位主设备号+20位次设备号)、file_operations 接口。字符设备最先经历 init 阶段,然后按照用户程序的调用对设备文件节点进行各种操作,最后在需要的时候 exit 设备。下面围绕 globalmem 字符设备来描述驱动的一般结构。

globalmem 驱动在内核中使用 globalmem_dev 结构来描述,它包含两个成员:设备号、用于数据交换的 char 数组。globalmem 设备文件的 file 句柄中的 private_data 成员将指向 globalmem_dev 结构。

在驱动设备的 init 入口函数中,将会通过 register_chardev_region 或 alloc_chrdev_region 去申请设备号。register_chardev_region 是使用指定的设备号去申请,alloc_chrdev_region 则是由系统自动分配并返回结果。两者在程序中可以结合起来用,先指定申请,失败后让系统分配。init 入口还进行了 file_operations 结构的绑定。最后,要调用 cdev_add 函数,向内核注册这个字符设备

在 exit 出口函数中,则是要先通过 cdev_del 让内核注销此设备,再释放占用的设备号

file_operations 中的各个成员是与文件操作对应的回调函数,由驱动程序实现与文件操作对应的具体的驱动操作函数。驱动程序运行于内核空间,在操作用户空间缓冲区时需要使用 access_ok 函数来判断传输的地址参数是否属于用户空间,避免因传入地址错误而错误地向内核空间写入数据。

驱动设备的 ioctl 函数,一般通过对接收的 cmd 参数进行 switch 分支判断,来根据命令执行不同的操作。对于 ioctl 命令格式,ioctl 有推荐的命令格式:数据类型(8 位)+ 序列号(8 位)+ 方向(2 位)+ 数据尺寸(13/14位)。内核中也提供了特殊的宏来生成 iotcl 命令。

Linux 设备驱动中的并发控制

在 Linux 内核中,广泛存在多个执行单元对共享资源的同时访问,这些并发让 Linux 系统陷入竞态。Linux 内核中的竞态主要发生在:多 CPU 核之间单 CPU 核的进程之间中断与进程之间。解决竞态问题的方法是保证对共享资源的互斥访问,Linux 中将访问共享资源的代码区域称为临界区,提供了中断屏蔽、原子操作、自旋锁、信号量、互斥体等互斥访问方式。

Linux 内核的锁机制是针对编译器的编译乱序和处理器的执行乱序去实现的。

编译乱序是由于 CPU 不考虑线程安全,仅以单线程视角去优化指令排序。解决编译乱序的方法是在内嵌汇编中使用编译屏障 barrier( ) ,以阻止编译器仅 barrier( ) 之后的代码进行优化。而与此功能相似的 volatile 关键字只是避免内存访问行为的合并,不具备保护临界资源的作用。

执行乱序是指处理器在执行指令时,根据资源的实际情况,调整了访存指令的执行顺序。解决执行乱序的方法是内存屏障:DMB(数据内存屏障)、DSB(数据同步屏障)、ISB(指令同步屏障)。

中断屏障是指通过关闭 CPU 对中断的响应,避免了 CPU 进程和中断之间的并发。但长时间屏蔽中断在 Linux 系统中是非常危险的,这就要求屏蔽中断后要尽可能快地执行完临界区代码。屏蔽中断的方法只能屏蔽本 CPU 内的中断,而无法解决多 CPU 引发的竞态。中断屏蔽适合与自旋锁一起使用。

local_irq_diable()  /* 屏蔽中断 */
...
critical section  /* 临界区 */
...
local_irq_enable()  /* 开中断 */

原子操作由内核提供,可以对位或整型进行排他性修改,依赖于 CPU 自身的原子操作,在 ARM 处理器中是 LDREX 和 STREX 指令。LDREX 指令设置访问标志,STREX 则用于取消访问标志并执行存储行为。重点在于 STREX 只在存在访问标志时才能执行成功并完成存储操作。否则就会重新进入 LDREX 到 STREX 的循环尝试。原子操作能够同时满足多核之间并发和单核内部并发的竞态。

自旋锁依赖于硬件上的原子操作访存实现(测试并设置某内存变量),保证只有一个 CPU 能够获得锁,无法正在请求此锁的 CPU 则会因获取失败而循环地进行获取锁的尝试。多核 CPU 系统中,一个拿到自旋锁的 CPU 会禁止本 CPU 上的抢占调度。这就是基础自旋锁的实现原理。为了确保自旋锁不被中断和底半部影响,基础自旋锁还需要配合中断屏蔽,构成衍生的自旋锁。对于衍生的自旋锁,单 CPU 占用的特性避免了核间并发,补充的中断屏蔽则避免了核内的并发。

自旋锁的自旋等待特性要求使用者不能长时间占用一个自旋锁,并且要避免诸如对一个自旋锁二次请求之类的局面,防止造成死锁。

/* 基础自旋锁的使用 */
spinlock_t lock;
spin_lock_init(&lock);

spin_lock(&lock);
... /* critical section */
spin_unlock(&lock);

/* 衍生的自旋锁 */
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()

spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_lock_irqrestore() = spin_unlock() + local_irq_restore()

spin_lock_bp() = spin_lock() + local_bh_disable()
spin_unlock_bp() = spin_unlock() + local_bh_enable()

为了以更细的粒度管理读和写,衍生出了读写自旋锁。读写自旋锁允许读操作的同时进行,但拒绝同时同时写以及边读边写的操作。

顺序锁在读写锁的基础上,放宽了对于写时读的限制,但依旧禁止同时写的操作。在读取被顺序锁保护的资源后,要检查读期间是否有过写操作,如果有,则要重新读。

RCU (Read-Copy-Update)不使用互斥访问方法,而是在修改共享资源时,先拷贝一份副本,在副本上进行修改,再用修改后的副本替代原始资源。在修改完成之前原始资源的程序仍旧能够访问原始资源,直到原始资源不再被使用,系统就会自动释放。这个过程实际上和 Java 的垃圾回收机制很相似。

信号量的操作对应操作系统的 PV 概念,信号量的值可以是 0. 1, N 。在信号量为 0 时,进程等待状态,等待资源可用。

互斥体类似于信号量,其实现依赖于自旋锁。互斥体的机制是以进程的角度实现的,当进程无法获取到资源时,则发生上下文切换。从 CPU 的角度来说,上下文切换造成的开销也是很大的,所以互斥体只适宜在临界区较大的时候使用,在临界区较小的时候还是更适合使用自旋锁。

原文地址:https://www.cnblogs.com/zenonx/p/linux_device_driver_dev_note.html