01. Linux设备驱动概述

1. 设备驱动的作用

核心:充当硬件和应用软件之间的纽带
具体任务:
① 读写设备寄存器(实现控制的方式)
② 完成设备的轮询、中断处理、DMA通信(CPU与外设通信的方式)
③ 进行物理内存向虚拟内存的映射(在开启硬件MMU的情况下)

2. 有无操作系统时的设备驱动

1)无操作系统

A. 硬件、驱动和应用程序的关系
01. Linux设备驱动概述

说明1:无操作系统特点
① 驱动包含的接口函数直接与硬件功能吻合,没有任何附加功能(向下)
② 设备驱动的接口被直接提交给应用软件工程师,应用软件直接访问设备驱动的接口(向上)

说明2:无操作系统情况下,两种不合理的驱动架构
01. Linux设备驱动概述
缺点:设备驱动和应用软件平等,驱动中包含了业务层面的处理,不符合高内聚,低耦合的要求。

01. Linux设备驱动概述
缺点:应用软件直接操作硬件寄存器,不单独设计驱动模块,代码不可复用

B. 单任务软件典型架构
在一个无限循环中夹杂着对设备中断的检测或者对设备的轮询。

2)有操作系统

A. 硬件、驱动、操作系统和应用软件的关系
01. Linux设备驱动概述

说明1:设备驱动的2个任务
① 操作硬件(向下)
② 将驱动融于内核,需要设计面向操作系统内核的接口,这些接口由操作系统定义(向上)
在有操作系统的情况下,驱动的架构由相应的操作系统定义,必须按照相应的架构设计驱动
结果:驱动成为连接硬件内核的桥梁 !!!

说明2:操作系统通过给驱动制造麻烦来给上层应用提供便利
由于驱动都按照操作系统给出独立于设备的接口设计,应用程序可以使用统一的系统调用接口来访问各种设备
e.g. 使用write和read函数可以访问各种字符设备和块设备,而不论设备的具体类型和工作方式

3. Linux设备分类

1)常规分类法

A. 字符设备
特点:
① 以串行顺序依次进行访问的设备
② 字符设备不经过系统的快速缓冲
e.g. 触摸屏、鼠标

B. 块设备
特点:
① 可以用任意顺序进行访问,以块为单位进行操作
② 块设备经过系统的快速缓冲
③ 在块设备上可以构建文件系统
e.g. 硬盘、SD卡

说明:块设备以块为最小传输单位,不能按字节处理数据。而Linux则允许块设备传送任意数量的字节,因此块 & 字符设备的区别区别仅在于内核内部管理数据的方式不同,即内核和驱动之间的软件接口不同

C. 网络设备
特点:
① 网络设备面向数据包的接收和发送设计
网络设备不对应于文件系统的结点

说明:与字符 & 块设备一样,网络设备也可以是一个纯粹的软件设备(e.g. 回环网卡)

2)总线分类法

示例:I2C驱动 / USB驱动 / PCI驱动 / LCD驱动
这些驱动都可以归入常规分类法的3个基础类别,但由于这些设备比较复杂,Linux为其定义了各自的驱动体系结构(即内核提供了给定类型设备的附加层我们编写的驱动是和这些附加层一起工作
所谓给定类型设备的附加层,其实就是内核开发者实现了整个设备类型的共有特性,并提供给驱动程序实现者
e.g. USB设备由USB模块驱动,而USB模块和USB子系统一起工作。但USB设备本身在系统中可以表现为一个字符设备(e.g. USB串口)/ 块设备(e.g. USB读卡器)/ 网络设备(e.g. USB网卡)

4. Linux设备驱动在整个软硬件系统中的位置

01. Linux设备驱动概述

说明1:除网络设备外,字符设备和块设备都被映射为Linux文件系统中的文件,可以通过文件系统的系统调用接口(open / close / read / write)访问

说明2:对块设备的2种访问方式
① 原始块访问(e.g. dd命令)
② 构建文件系统通过文件访问

说明3:Linux块子系统 & MTD子系统
① MTD子系统面向Nor & Nand Flash工作,在其上可建立Yaffs等文件系统
② Linux块子系统面向磁盘 & MMC/SD工作,在其上可建立FAT/EXT等文件系统

5. 内核空间与用户空间

1)硬件基础:CPU支持不同的运行模式
ARM:支持usr / fiq / irq / svc / sys / und / abt 七种模式
X86:拥有ring 0 ~ ring 3四种特权等级

2)Linux利用CPU的这一特性实现内核态和用户态,但他只使用两级
ARM:内核态(svc模式)、用户态(usr模式)
X86:内核态(ring 0)、用户态(ring 3) //ring 1 现在被用于实现虚拟化

说明1:ARM Linux 的系统调用实现原理是采用swi软中断从用户态切换至内核态
说明2:X86是通过int 0x80中断进入内核态

3)内核态与用户态
内核态:可以进行任何操作
用户态:禁止对硬件的直接访问和对内存的未授权访问

说明:内核态和用户态使用不同的地址空间(即有自己的内存映射),Linux 只能通过系统调用硬件中断从用户空间进入内核空间。

补充:在进入内核态时,系统调用和硬件中断的不同
A. 执行系统调用的内核代码运行在进程上下文中,他代表调用进程执行操作,因此能够访问进程地址空间的所有数据。
B. 处理硬件中断的内核代码运行在中断上下文中,他和进程是异步的,与任何一个特定进程无关。
通常,一个驱动程序模块中的某些函数作为系统调用的一部分,而其他函数负责中断处理。

6. GNU C对ANSI C的常见扩展

1)零长度数组

struct var_data
{
int len;
char data[0];
};

说明1:由于没有为data数组分配内存,因此sizeof(struct var_data) = sizeof(int)

说明2:char data[0] 意味着通过var_data结构体类型变量的data[i]成员可以访问len之后的第i个地址中的内容
e.g. 假设struct var_data的数据域就保存在struct var_data紧接着的内存区域,那么可以通过如下方式遍历这些数据。
struct var_data s;
...
for (i = 0; i < s.len; ++i) //此时s.len 中保存的就是实际的数据域字节数
printf("%x\n", s.data[i]);

典型应用场景:定义变长对象的头结构(e.g. 802.11帧头部,由于IE 的存在,帧长度可变~~)
补充:其实只有在数据域紧接着struct var_data时,零长度数组才有意义

上机测试:成员数组data 的起始地址
01. Linux设备驱动概述

01. Linux设备驱动概述

2)case范围

GNU C 支持case x...y 语法

switch (ch)
{
case '0'...'9':
ch -= '0';
break;
case 'a'...'f'
ch -= 'a' - 10;
break;
case 'A'...'F':
ch -= 'A' - 10;
break;
}

3)语句表达式

其实就是花括号中的复合语句

在这个topic下主要想讨论一种避免副作用的宏定义方式:
#definf MIN(x, y) ((x) < (y) ? (x) : (y))
这种宏定义方式已经考虑得比较全面,但不能避免调用时的副作用,比如,
int x = 10;
int y = 20;
int z = MIN(x++, y++); //int z = ((x++) < (y++) ? (x++) : (y++)),由于副作用变量错误地
//累加了2次
注意:在实际使用中要避免在调用宏时带副作用

改进方式:在复合语句中定义局部变量
#define MIN(type, x, y) \
({type _x = (x); type _y = (y); _x < _y ? _x : _y})

int z = MIN(int, x++, y++); //int z = {int _x=x++; int _y=y++; _x < _y ? _x : _y};

4)typeof关键字

typeof(x)可以获得x的类型,借助这个宏,可以重写上面的MIN 宏(内核代码中的实现)

#define MIN(x, y) ({ \
const typeof(x) _x = (x); \
const typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x < _y ? _x : _y})

说明:(void)(&_x == &_y) 的作用是判断参与比较的两个值类型是否一致
_x 和_y 的地址值当然不可能相同,但是如果两个变量的类型不同,此处进行地址比较就会使得编译器警告:comparison of distinct pointer types lacks a cast。这是因为C语言中的指针包含 地址值 + 基类型 这2个属性

5)可变参数宏

#define pr_debug(fmt, arg...)
printk(fmt, ##arg)
说明:pre_debug宏中的arg表示其余的参数,可以是零个或多个。

pre_debug("%s:%d\n", filename, line); ==>
printk("%s:%d\n", filename, line);

使用##是为了处理arg参数个数为零个的情况,此时前面的逗号变得多余,使用##后,GNU C 处理器会丢弃前面的逗号。
pre_debug("success!\n"); ==>
printk("success!\n");
而不是
printk("success!\n",);

6)当前函数名宏

GNU C 中使用宏__FUNCTION__ 保存函数在源代码中的名字,C99 中新增了__func__ 宏表示当前函数名。
目前建议在Linux 编程中使用__func__ 宏

7)特殊属性声明__attribute__

用途:声明函数变量类型的特殊属性,以便进行手工的代码优化定制代码检查的方法。
语法:在需要修饰的声明后面添加__attribute__((ATTRIBUTE)),其中ATTRIBUTE为属性说明,如果存在多个属性,以逗号分隔。
下面列举几个常用的属性:

A. noreturn

用于函数,表示函数从不返回。编译器可以据此优化代码(e.g. 不为函数返回准备寄存器),并消除不必要的警告信息

B. unused

用于函数和变量,表示该函数或变量可能不会被用到,可避免编译器产生的警告。

C. aligned

用于变量、结构体或联合体,指定变量、结构体或联合体的对齐方式,以字节为单位
struct example_struct
{
char a;
int b;
long c;
}__attribute__((aligned(4))); //以4字节对齐

下面通过一个实例来了解下aligned 的作用:
aligned(4)
01. Linux设备驱动概述

aligned(16)
01. Linux设备驱动概述

分析:如果将__attribute__((aligned(n)))作用于一个类型(n必须为2的幂次方),那么该类型变量在分配地址空间时,其存放的地址一定按照n字节对齐;并且其占用的空间也是n的整数倍

D. packed

用于变量和类型,用于变量或结构体时表示使用最小可能的对齐;用于枚举、结构体或联合体类型是表示该类型使用最小的内存。
01. Linux设备驱动概述

01. Linux设备驱动概述
说明:可见对整个结构体类型使用packed属性后,不再对齐和补齐(但实际编程时不建议使用,因为非对齐的内存访问效率较低)

E. section

用于函数或数据,表示将其链接到指定的段
__attribute__((section("section_name"))) //内核中非常常用

F. format

用于函数,表示函数使用printf / scanf风格的参数,指定format属性可以让编译器根据格式串检查参数类型
01. Linux设备驱动概述
该属性说明printk的第1个参数是格式串,从第2个参数开始会根据printf函数的格式串规则检查参数

8)内建函数

GNU C 提供了大量的内建函数,其中大部分是标准C库函数的GNU C编译器内建版本(如memcpy等),他们与对应的标准C 库函数功能相同。
不属于库函数的其他内建函数的命名通常以__builtin开始

A. __builtin_constant_p

__builtin_constant_p(EXP)用于判断一个值是否为编译时常数,如果参数EXP的值是常数该函数返回1,否则返回0
01. Linux设备驱动概述

B. __builtin_expect

__builtin_expect(EXP, C)用于为编译器提供分支预测信息,其返回值是整数表达式EXP的值,C的值必须是编译时常数
由于代码中的分支语句会中断流水线,所以可以使用likely & unlikely宏暗示分支容易成立还是不容易成立
01. Linux设备驱动概述

说明:可以使用-ansi -pedantic编译选项禁用GNU C语法

7. 内核编程其他主题

1)do {} while(0)

使用场景:宏定义
示例:
#define SAFE_FREE(p) do{free(p); p = NULL;} while(0) //此处没有分号哦~~
if (p)
SAFE_FREE(p);
else
... //do something

说明:此时的宏展开不会有问题。如果仅仅使用花括号,仍可能有潜在风险
#define SAFE_FREE(p) {free(p); p = NULL;}
if (p)
SAFE_FREE(p); //调用后添加分号,是通常的使用习惯
else
... //do something
会被展开为,
if (p)
{free(p); p = NULL;} ;
else
... //do something
由于分号的存在,{free(p); p = NULL} ; 表示2 条语句(复合语句 + 空语句),所以else 无法配对,导致编译失败。
问题根源:C 语言中规定语句以分号结束,但有一点例外,就是复合语句是以右花括号结束(})

2)goto语句的使用

goto 语句在Linux内核源码中一般只用于错误处理
关键:在错误处理时,注销 / 释放资源的顺序和注册 / 申请资源的顺序相反

if (register_a() != 0)
goto err;
if (register_b() != 0)
goto err1;
if (register_c() != 0)
goto err2;

//错误处理书写技巧:从err 写起,逐层向上~~
err2:
unregister_b();
err1:
unregister_a();
err:
return ret;

3)内核中的并发

理解关键:内核代码几乎始终不能假定在给定代码段中能够独占CPU

并发原因:
① Linux 中的并发进程,可能同时要使用我们的驱动程序
② 中断处理程序
③ 其他异步事件(e.g. 内核定时器)
④ SMP
⑤ 内核抢占

对内核代码的要求:
① 必须可重入,能够同时运行在多个上下文中
② 内核数据结构要保证多个线程能分开执行
③ 访问共享数据的代码必须避免破坏共享数据

4)当前进程的获取

如果当前执行的内核操作由某个进程发起,那么可以通过全局项current来获得当前进程
全局项current的实现方式如下,
A. before 2.6
实现为指向struct task_struct 的指针(include/sched.h)
01. Linux设备驱动概述

01. Linux设备驱动概述

B. from 2.6
① 不再是一个全局变量
② 为支持SMP,开发了一种能找到运行在相关CPU 上的当前进程的机制
③ 实现时不依赖特定架构,将指向task_struct 结构的指针隐藏在内核栈中
01. Linux设备驱动概述

01. Linux设备驱动概述
说明:从实现方式看,确实是从内核栈中获取了当前进程的信息

5)浮点工具链

Linux的浮点处理有3种方式,其对应的编译选项如下,
① 完全软浮点:-mfloat-abi=soft
② 与软浮点兼容,但是使用FPU硬件:-mfloat-abi=softfp
③ 完全硬浮点:-mfloat-abi=hard
说明:由于目前主流的ARM芯片都自带VFP或者NEON等浮点处理单元(FPU),所以对硬浮点的需求更加强烈。在工具链前缀中包含"hf"的为支持完全硬浮点的工具链,比如arm-linux-gnueabihf-gcc

6)一些细节

① 内核API中以双下划线开头的函数,是接口的底层组件,应该谨慎使用
② 内核代码不能实现浮点运算(当然内核代码中也不需要~~)