第 1 章 Linux 设备驱动概述及开发环境构建
1. 设备驱动的定义
设备驱动充当了硬件和应用软件之间的纽带,应用软件时只需要调用系统软件的API就可让硬件去完成要求的工作。
无操作系统时硬件、驱动和应用软件的关系如下图所示。
当设备驱动引入操作系统后,驱动变成了连接硬件和内核的桥梁,应用程序将可使用统一的系统调用接口来访问各种设备,具有较好的通用性。硬件、驱动、操作系统和应用程序的关系如下图所示。
2. Linux 设备驱动的重难点
- 内存的读写方式、通信接口的原理以及总线接口的工作方式;
- 能够灵活使用C语言的结构体、指针、函数指针以及内存动态申请和释放;
- 有一定的Linux 内核基础;
- 有一定的多任务并发控制和同步的基础。
3. linux 源代码阅读和编辑
- Windows阅读工具: Source Insight
- Linux主机阅读工具: vim + cscope、vim + ctags
第 2 章 驱动设计的硬件基础
1. 处理器
中央处理器的体系架构分为冯 • 诺依曼结构和哈佛结构两类。
从指令集的角度,中央处理器分为RISC(精简指令集计算机)和CISC(复杂指令集计算机)两类。
DSP 一般采用改进的哈佛架构,如下图所示,地址总线和数据总线由程序存储器和数据存储器分时共用。
2. 储存器
- CAM:内容寻址 RAM
CAM的输入与输出如图所示,在数据检索方面有较大的优势,极大地提升系统性能。
- FIFO:先进先出队列
用于数据缓冲,两边的端口不对等等,某一时刻只能被设置为一边作为输入,一边作为输出。
3. 接口与总线
- 以太网接口
组成一个以太网接口的硬件原理如下图所示。
- PCI和PCI-E
PCI-E兼容 PCI,支持PCI设备和内存模组的初始化。
4. 芯片数据手册阅读方法
快速而准确地定位有用信息,重点阅读这些信息,忽略无关内容。
5. 仪器仪表使用
逻辑分析仪最主要的作用是用于时序的判定,具有超强的逻辑跟踪分析功能。
第 3 章 Linux 内核及内核编程
Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program. —— Linus Torvalds
1. Linux内核的组成部分
2. Linux内核的编译
推荐命令:make menuconfig
TTY_PRINTK 配置项的三态:
- Y:可编译入内核;
- N:可不编译;
- M:可编译为内核模块。
3. Linux下的C编程特点
- 如果使用
“-ansi –pedantic”
编译选项,则会告诉编译器不使用 GNU 扩展语法; - do{} while(0) 的使用完全是为了保证宏定义的使用者能无编译错误地使用宏,它不对其使用者做任何假设;
- goto 用于错误处理的用法实在是简单而高效。
第 4 章 Linux 内核模块
1. Linux 内核模块简介
内核模块中用于输出的函数是printk(),和printf()作用相似,但是printk()可定义输出级别。
- 模块加载函数:insmod、modprobe(同时加载依赖)
- 模块卸载函数:rmmod、modprobe -r(同时卸载依赖)
Linux 内核不能使用非GPL许可权
第 5 章 Linux 文件系统与设备文件
1. 文件操作系统调用
数字代表的权限:
- 1:执行权限
- 2:写权限
- 4:读权限
- 0:无
2. Linux 文件系统目录结构
根目录运行“ls -l”
命令,获得如下目录。
序号 | 命令 | 功能 |
---|---|---|
1 | /bin | 基础命令 |
2 | /sbin | 系统命令 |
3 | /dev | 设备文件存储目录 |
4 | /etc | 系统配置文件的所在地 |
5 | /lib | 系统库文件存放目录 |
6 | /mnt | 挂载储存设备的挂载目录 |
7 | /opt | “可选”,有些软件包会被安装在这里 |
8 | /proc | 进程及内核信息存放在这里 |
9 | /tmp | 存放临时文件 |
10 | /usr | 系统存放程序的目录 |
11 | /var | 变化,目录的内容经常变动 |
12 | /sys | sysfs文件系统被映射在此目录 |
3. Linux 文件系统与设备驱动
应用程序和 VFS 之间的接口是系统调用,而 VFS 与磁盘文件系统以及普通设备之间的接口是 file_operations 结构体成员函数,这个结构体包含对文件进行打开、关闭、读写、控制的一系列成员函数。
第 6 章 Linux 字符设备驱动结构
1. cdev 结构体
函数 | 功能 |
---|---|
cedv_init()函数 | 初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接 |
cdev_alloc()函数 | 用于动态申请一个cdev 内存 |
cdev_add()和cdev_del()函数 | 分别向系统添加和删除一个cdev,完成字符设备的注册和注销 |
register_chrdev_region() 或 alloc_chrdev_region() 函数 | 向系统申请设备号 |
2.file_operations 结构体
函数 | 功能 |
---|---|
llseek() 函数 | 修改一个文件的当前读写位置,并将新位置返回 |
read() 函数 | 从设备中读取数据 |
write() 函数 | 向设备发送数据 |
readdir() 函数 | 仅用于目录,设备节点不需要实现它 |
ioctl() 函数 | 提供设备相关控制命令的实现(既不是读操作也不是写操作) |
mmap() 函数 | 将设备内存映射到进程内存中 |
open() 函数 | 打开设备文件 |
release() 函数 | 释放设备文件 |
poll()函数 | 一般用于询问设备是否可被非阻塞地立即读写 |
aio_read() 和 aio_write() 函数 | 分别对与文件描述符对应的设备进行异步读、写操作 |
copy_from_user() 函数 | 完成用户空间到内核空间的拷贝 |
copy_to_user() 函数 | 完成内核空间到用户空间的拷贝 |
如下图所示,是字符设备驱动的结构、字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系。
- container_of()函数:通过结构体成员的指针找到对应结构体的指针;
- seek()函数:定位文件的起始地址。
第 7 章 Linux 字符设备驱动结构
1. 并发与竞态
并发指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。
在单CPU 范围内避免竞态的一种简单而省事的方法是在进入临界区之前屏蔽系统的中断。长时间屏蔽中断是很危险的,有可能造成数据丢失乃至系统崩溃等后果。
原子操作指的是在执行过程中不会被别的代码路径所中断的操作。
自旋锁是一种典型的对临界资源进行互斥访问的手段,使用自旋锁需要注意如下几个问题。
- 自旋锁实际上是忙等锁,仅仅需要等待;
- 自旋锁可能导致系统死锁;
- 自旋锁锁定期间不能调用可能引起进程调度的函数。
RCU 可以看作读写锁的高性能版本,相比读写锁,RCU 的优点在于既允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。
信号量是用于保护临界区的一种常用方法,与自旋锁不同的是,当获取不到
信号量时,进程不会原地打转而是进入休眠等待状态。
第 8 章 Linux 设备驱动中的阻塞与非阻塞 I/O
阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再 进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。 而非阻塞操作的进程在不能进行设备操作时,并不挂起,它或者放弃,或者不停地查询,直至可 以进行操作为止。
所谓死锁,就是多个进程循环等待它方占有的资源而无限期地僵持下去的局面。如果没有外力的作用,那么死锁涉及的各个进程都将永远处于封锁状态。
第 9 章 Linux 设备驱动中的异步通知与异步 I/O
异步通知的意思是:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上“中断”的概念,比较准确的称谓是“信号驱动的异步 I/O”。
第 10 章 中断与时钟
1. 中断
所谓中断是指 CPU 在执行程序的过程中,出现了某些突发事件急待处理,CPU 必须暂停执行当前的程序,转去处理突发事件,处理完毕后 CPU 又返回原程序被中断的位置并继续执行。
2. 中断处理机制
- 顶半部:执行速度很快,用于完成尽量少的比较紧急的功能;
- 底半部:不是非常紧急的任务,相对比较耗时,不在硬件中断服务程序中执行,用它来完成中断事件的绝大多数任务。
第 11 章 内存与 I/O 访问
1. 内存空间与 I/O 空间
在 X86 处理器中存在着 I/O 空间的概念,I/O 空间是相对于内存空间而言的,它通过特定的指令 in、out 来访问。
内存空间是必须的,而I/O 空间是可选的。
高性能处理器一般会提供一个内存管理单元(MMU),该单元辅助操作系统进行内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护和Cache 缓存控制等硬件支持。
进程的 4GB 内存空间被分为两个部分 —— 用户空间与内核空间。用户空间地址一般分布为0~3GB(即PAGE_OFFSET,在0x86 中它等于0xC0000000),这样,剩下的3~4GB 为内核空间,如图所示。
2. 内存存储
在用户空间动态申请内存的函数为malloc(),这个函数在各种操作系统上的使用是一致的,malloc()申请的内存的释放函数为free()。
3. kmalloc() 和 vmalloc() 的区别
- kmalloc():分配的内存虚拟地址和物理地址都是连续的;
- vmalloc():分配的内存虚拟地址是连续的,而物理地址则无须连续。
出于性能的考虑,很多内核代码都用 kmalloc() 来获得内存,而不是 vmalloc()。
- kmalloc() 函数
相对于 malloc() 函数,kmalloc() 函数多了一个 flags 参数,用它可以获得以字节为单位的一块内核内存,对申请的内存大小有限制,不能超过2^10 * 4K = 4MB。
kmalloc() 在
void * kmalloc(size_t size, gfp_t flags)
- kfree() 函数
kfree() 函数释放由 kmalloc() 分配出来的内存块,分配和回收要注意配对使用,避免内存泄漏和其他bug。其中,kfree(NULL) 是安全的。
kfree() 在
void kfree(const void *ptr)
举例:假定存在一个 dog 结构体,现在为他动态地分配足够的空间,不在需要这个内存时,需要释放它。
struct dog *p;
p = kmalloc(sizeof(struct dog), GFP_KERNEL);
if (!p)
/* 处理错误... */
kfree(p);
- kzalloc() 函数
kzalloc() 是 kmalloc() 和 memset() 的结合,也就是先是用 kmalloc() 申请空间, 然后用 memset() 来初始化 ,所有申请的元素都被初始化为 0。
kzalloc() 对应的内存释放函数也是 kfree()。
- vmalloc() 函数
工作方式类似于 kmalloc(),为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项。
由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。
vmalloc() 在
void * vamlloc(unsigned long size)
- vfree() 函数
vfree() 函数 用于释放 vmalloc() 获得的内存,使用下面的函数:
void vfree(const void *addr)
4. DMA
DMA 是一种无需CPU 的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。使用DMA 可以使系统CPU 从实际的I/O 数据传输过程中摆脱出来,从而大大提高系统的吞吐率。
第 12 章 Linux 设备驱动的软件架构思想
1. 软件工程的基本原则
- 高内聚、低耦合
- 信息隐蔽