目前打算写一下 Linux 驱动开发入门系列的博客,一方面是为了方便日后复习,另一方面也是希望能够捋顺一下自己的思路。这是此系列的第一篇文章,目的是简单概述一下驱动程序在 Linux 内核中扮演的角色。后续文章计划从字符设备驱动框架、设备树、pinctrl 和 GPIO 子系统、IO模型、内核锁机制、中断等几个主题梳理字符驱动程序的编写方法。
驱动概念
驱动与底层硬件直接打交道,充当了硬件与应用软件中间的桥梁。
- 具体任务
- 读写设备寄存器(实现控制的方式)
- 完成设备的轮询、中断处理、DMA通信(CPU与外设通信的方式)
- 进行物理内存向虚拟内存的映射(在开启硬件MMU的情况下)
- 说明:设备驱动的两个任务方向
- 操作硬件(向下)
- 将驱动程序通入内核,实现面向操作系统内核的接口内容,接口由操作系统实现(向上) (驱动程序按照操作系统给出的独立于设备的接口设计,应用程序使用操作系统统一的系统调用接口来访问设备)
Linux系统主要部分:内核、shell、文件系统、应用程序
- 内核、shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序、管理文件并使用系统
- 分层设计的思想让程序间松耦合,有助于适配各种平台
- 驱动的上面是系统调用,下面是硬件
驱动分类
Linux驱动分为三个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
字符设备(Char Device)
- 字符(char)设备是个能够像字节流(类似文件)一样被访问的设备。
- 对字符设备发出读/写请求时,实际的硬件I/O操作一般紧接着发生。
- 字符设备驱动程序通常至少要实现open、close、read和write系统调用。
- 比如我们常见的 lcd、触摸屏、键盘、led、串口等等,他们一般对应具体的硬件都是进行出具的采集、处理、传输。
块设备(Block Device)
- 一个块设备驱动程序主要通过传输固定大小的数据(一般为512或1k)来访问设备。
- 块设备通过buffer cache(内存缓冲区)访问,可以随机存取,即:任何块都可以读写,不必考虑它在设备的什么地方。
- 块设备可以通过它们的设备特殊文件访问,但是更常见的是通过文件系统进行访问。
- 只有一个块设备可以支持一个安装的文件系统。
- 比如我们常见的电脑硬盘、SD卡、U盘、光盘等。
网络设备(Net Device)
- 任何网络事务都经过一个网络接口形成,即一个能够和其他主机交换数据的设备。
- 访问网络接口的方法仍然是给它们分配一个唯一的名字(比如eth0),但这个名字在文件系统中不存在对应的节点。
- 内核和网络设备驱动程序间的通信,完全不同于内核和字符以及块驱动程序之间的通信,内核调用一套和数据包传输相关的函(socket函数)而不是read、write等。
- 比如我们常见的网卡设备、蓝牙设备。
驱动程序的功能
- 对设备初始化和释放
- 把数据从内核传送到硬件和从硬件读取数据
- 读取应用程序传送给设备文件的数据和回送应用程序请求的数据
- 检测和处理设备出现的错误
驱动开发前提知识
内核态和用户态
- Kernel Mode(内核态)
- 内核模式下(执行内核空间的代码),代码具有对硬件的所有控制权限。可以执行所有CPU指令,可以访问任意地址的内存
- User Mode(用户态)
- 在用户模式下(执行用户空间的代码),代码没有对硬件的直接控制权限,也不能直接访问地址的内存。
- 只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址。
- 程序是通过调用系统接口(System Call APIs)来达到访问硬件和内存
Linux利用CPU实现内核态和用户态
- arm:内核态(svc模式),用户态(usr模式)
- x86: 内核态(ring 0 ),用户态(ring 3)( x86有ring 0 - ring3四种特权等级)
Linux实现内核态和用户态切换
- ARM Linux的系统调用实现原理是采用
swi
软中断从用户态切换至内核态 - X86是通过
int 0x80
中断进入内核态
Linux只能通过系统调用和硬件中断从用户空间进入内核空间
- 执行系统调用的内核代码运行在进程上下文中,他代表调用进程执行操作,因此能够访问进程地址空间的所有数据
- 处理硬件中断的内核代码运行在中断上下文中,他和进程是异步的,与任何一个特定进程无关通常,一个驱动程序模块中的某些函数作为系统调用的一部分,而其他函数负责中断处理
Linux下应用程序调用驱动程序流程
Linux下进行驱动开发,完全将驱动程序与应用程序隔开,中间通过C标准库函数以及系统调用完成驱动层和应用层的数据交换。
驱动加载成功以后会在“/dev”目录下生成一个相应的文件,应用程序通过对“/dev/xxx” (xxx 是具体的驱动文件名字) 的文件进行相应的操作即可实现对硬件的操作。
用户空间不能直接对内核进行操作,因此必须使用一个叫做 “系统调用”的方法 来实现从用户空间“陷入” 到内核空间,这样才能实现对底层驱动的操作
每一个系统调用,在驱动中都有与之对应的一个驱动函数,在 Linux 内核文件
include/linux/fs.h
中有个叫做 file_operations 的结构体,此结构体就是 Linux 内核驱动操作函数集合。
打开设备文件流程
- 加载一个驱动模块,产生一个设备文件,有唯一对应的inode结构体
- 应用层调用open函数打开设备文件,对于上层open调用到内核时会发生一次软中断,从用户空间进入到内核空间。
- open会调用到sys_open(内核函数),sys_open根据文件的地址,找到设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。
- 根据struct inode结构体里面记录的主设备号和次设备号,在驱动链表(管理所有设备的驱动)里面,根据找到字符设备驱动
- 每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口
- 找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。
- 执行xxx_open驱动函数。