申请分配主设备号
为特定设备相关的数据结构分配内存
将入口函数(open、read等)与字符驱动程序的cdev抽象相关联
将主设备号与cdev相关联
在/dev和/sys下创建节点
初始化硬件
对字符设备(c)的访问是通过文件系统内的设备名称进行的,那些名称被称为特殊文件、设备文件、或者文件系统树的节点,它们通常位于/dev目录
1 设备编号
主设备号:高12位,用来区分设备类型,标识设备对应的驱动程序
例如:/dev/null 和/dev/zero由驱动程序1管理
次设备号:由内核使用,区分同类型的不同设备,用于正确确定设备文件所指的设备。可以通过次设备号获得一个指向内核设备的直接指针。也可以将次设备号当做设备本地数组的索引。

内核用dev_t类型来保存设备编号,
1.1 dev_t
设备编号的内部表达
在内核中,dev_t类型用来保存设备编号:包括主设备号和次设备号
dev_t :主设备号12位 + 次设备号20位
1.2 生成设备号
linux提供相应的宏实现dev_t 与设备号之间的转换。
#include<linux/kdev.h>
MAJOR(dev) // 从dev_t中获取主设备号
MINOR(dev) // 获取次设备号
MKDEV(ma,mi) // 通过主设备号ma和次设备号mi生成dev_t
1.3 分配设备编号
建立一个字符设备之前,驱动程序首先要做的事情就是获得一个或多个设备编号
用户提前知道设备编号---register_chrdev_region()
用户不知道设备编号---alloc_chrdev_region()
1.3.1 register_chrdev_region()
register_chrdev_region(dev_t first,unsigned int count,char *name)
参数:
firet:要分配的设备编号范围的初始值(first的次设备号常设为0)
count:连续编号的个数
name:和该范围相关的设备名称,将出现在/proc/devices 和sysfs中
返回值:成功:0
失败:-EFAULT
缺点:驱动被广泛应用时,随机选定的主设备号可能会导致设备号冲突,而使得驱动程序无法注册
1.3.2 alloc_chrdev_region()
int alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char *name);
函数功能:动态分配
参数:
dev:用于输出,在成功完成调用之后,将保存已分配范围的第一个编号
firstminor:要使用的被请求的第一个次设备号,通常为0
count:所请求的连续设备编号的个数
name:和该编号关联的设备名称,它将出现在/proc/devices和 sysfs中
缺点:分配的主设备号不能保证始终一致,所以无法预先创建设备节点
1.4 unregist_chrdev_region()
释放设备编号:删除设备链表中的元素
void unregist_chrdev_region(dev_t from,unsigned int count);
参数:
from:设备号
count:要卸载的个数
unregister_chrdev()
2 数据结构
2.1 struct file_operation
/* 指针:指向模块拥有者,一般为THIS_MODULE 驱动程序入口地址,知晓结构拥有者的身份可以让内核帮助管理*/
struct module *owner;
/* mmap就是建立内核空间映射到用户空间虚拟地址上,之后,应用程序直接访问映射后虚拟地址,实际是在访问内核空间 */
int (*mmap) (struct file *, struct vm_area_struct *);
/* 打开设备 */
int (*open) (struct inode *, struct file *);
/* 关闭设备 */
int (*release) (struct inode *, struct file *);
__user 表示指针是用户空间指针,不能被直接引用。
2.2 struct file
内核结构,不会出现在用户程序中。代表一个打开的文件(不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间中都有一个对应的file结构)
指向struct file的指针通常被称为filp
2.3 struct inode
内核用inode结构在内部表示文件。
struct file表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但是它们都指向单个inode结构
常规,通常只有
dev_t i_rdev;// 对表示设备文件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_dev;// 表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
2.4 struct cdev
描述字符设备的抽象。
//#include<cdev.h>
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
对应API
file_operation:cdev_init
dev_t : cdev_add()
3 字符设备注册
3.1 cdev_init()
建立cdev和file_operations之间的连接
void cdev_init(struct cdev *cdev, struct file_operations *fops);
my_cdev->owner = THIS_MODULE;
函数原型:

3.2 cdev_alloc()
// 分配
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;
3.3 cdev_add()
将主/次设备号和cdev关联
// 通知内核cdev的消息
int cdev_add(struct cdev *cdev, dev_t num, unsigned int count);
参数:num是设备对应的第一个设备编号
count和该设备关联的设备编号的数量,通常取1
注意:在驱动程序上没有完全准备好处理设备上的操作时,不能调用cdev_add();
3.4 移除设备
void cdev_del(struct cdev *dev);
示例:

4 类
版本:2.6.26之后
驱动中加入对sysfs + udev的支持
头文件 /include/linux/device.h
流程:在驱动初始化代码里
调用class_create()为该设备创建一个class
为每个设备调用devide_create()
属于应用层,不要试图在内核的配置选项里找到它,加入对udev的支持很简单,以字符设备驱动为例,在驱动初始化的代码里调用class_create()为该设备创建一个class,再
4.1 struct class
用来创建类,存放于sysfs下面
4.2 class_create()
为设备创建sysfs入口点
struct class shm_class;//共享内存类
shm_class = class_create(THIS_MODULE,"shm_class");
参数1:指定类的所有者是哪个模块
参数2:指定类型名
返回值:
4.3 class_destroy()
void class_destroy(struct class *cls)
5 设备节点
5.1 device_create()
在/dev目录下创建相应的设备节点。
加载模块时,要用户空间中的udev会自动响应device_create()函数,去/sysfs目录下寻找对应的类,从而创建设备节点。
函数功能:
寻找对应的类从而创建设备节点
extern struct device *device_create(struct class *cls, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...)
参数:
cls:该设备从属的类
parent:该设备的父设备,NULL:表示无
devt:设备号
drvdata:设备名称
fmt:从
5.2 device_destory()
void device_destroy(struct class *dev, dev_t devt);
3.2 device_create_file()
建立设备属性文件。
函数功能:
在/sys/class/下创建一个属性文件,从而通过这个属性进行读写就能完成对应的数据操作
函数原型:
int device_create_file(struct device *, struct device_attribute *)
原理:
DEVICE_ATTR宏创建一个名为dev_attr_##_name的属性结构,dev_attr_##_name用于device_create_file()
#include </linux/device.h>
#define DEVICE_ATTR(_name, _mode, _show, _store)
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)
- 初始化device_attribute结构体的宏
#include </linux/sysfs.h>
#define __ATTR(_name,_mode,_show,_store) {
.attr = {.name = __stringify(_name), .mode = _mode },
.show = _show,
.store = _store,
}
-
struct device_attribute的定义
#include </linux/device.h>
/* interface for exporting device attributes */
struct device_attribute {
struct attribute attr;
// 函数指针
ssize_t (*show)(struct device *dev, struct device_attribute *attr,
char *buf);
ssize_t (*store)(struct device *dev, struct device_attribute *attr,
const char *buf, size_t count);
};
3.3 device_remove_file()
4 数据交互
fop的.read和.write是负责在用户空间和设备指教交换数据的主要字符驱动函数。
驱动write: 数据下传
驱动read:数据上送
读写函数系列扩展:
fsync()
aio_read() / aio_write()
mmap()
需要注意的问题:
访问时是否需要等待设备I/O结束?------》阻塞和非阻塞操作
很多驱动程序的数据访问函数依靠中断来获取数据,并且需要通过等待队列等数据结构和中断上下文代码来通信
-
内核不能直接访问用户空间的缓冲区,反之亦然.
read()需要借助copy_to_user()
write()需要借助copy_from_user()
4.1 设备I/O
原理:对于只支持内存映射的I/O寄存器的计算机体系架构:<mark style="box-sizing: border-box; background: rgb(255, 255, 0); color: rgb(0, 0, 0); text-indent: 0px;">通过把I/O端口地址重新映射到内存地址来伪装端口I/O</mark>
访问I/O端口的内联函数:
单数据传输
一次传输一个数据
/*8bit,字节读写端口*/
void outb(unsigned char byte,unsigned port);
// port: I/O地址,虚拟地址
unsigned inb(unsigned port);
inw();//字,16bit
outw()
inl();
outl();//双字, 32bit
即使是在64位的体系架构上,端口地址空间也只使用最大32bit的数据通路
在用户空间访问I/O端口的前提:
- 编译程序时必须带-O 选项来强制内联函数的展开
串操作
作为补充,有些处理器上实现一次传输一个数据序列的特殊指令,序列中的数据单位可以是字节、字或双字。这些指令称为串操作指令。

4.2 数据同步
4.2.1 fsync()
驱动程序完成数据下传,并不能保证数据成功写入设备中,如果应用程序要确保成功,可以调用fsync()。
fsync()驱动程序确保数据从驱动程序缓冲区中排出,并且写到设备。
4.2.2 异步I/O(AIO)
用户数据存储在多个缓冲区并需要发送至设备,驱动需要从不同缓冲区手机数据,并将其分发至设备。

struct iovec
包含数据缓冲区的地址和长度
4.3 mmap
将设备内存和用户的虚拟内存关联,应用程序可以调用相应的系统调用,也可以调用mmap(),直接在返回的内存区操作,以访问设备驻留的内存。
4.4 ioctl
当应用程序需要请求某些设备特定的动作时,这个例程接收并实现应用程序的命令。
4.4.1 示例-CRC
CRC:循环冗余校验
调整校验和:CMOS内容被修改后重新计算CRC,计算出的校验和被存储在CMOS存储体预先指定的偏移上
验证校验和:用于检查CMOS内容是否完好。通过比较针对当前内容计算出的CRC和以前存储的CRC来完成
5 轮询
当设备上有数据到来,或者驱动程序准备好接收新数据时,系统最好能够采用同步或异步的方式通知
5.1 同步-poll()
poll_table结构
内核数据结构,表示等待队列,由被轮询等待数据的设备驱动程序所拥有
poll_wait()
为内核poll_table添加一个等待队列后休眠。
5.2 异步-fasync()
考虑性能,一些应用程序需要以异步方式获得设备驱动程序的通知
5 程序结构
(1)初始化例程
(2)入口函数集——file_operation
(3)中断例程、底半部例程、定时器处理例程、内核辅助线程以及其他组成部分(对用户空间透明)
字符设备驱动初始化工作
申请分配主设备号
为特定设备相关的数据结构分配内存
将入口函数(open、read等)与字符驱动程序的cdev抽象相关联
将主设备号与cdev相关联
在/dev和/sys下创建节点
-
初始化硬件
主要是硬件资源的申请与配置,主要涉及地址映射,寄存器读写等相关操作。
ioremap()将物理地址映射成虚拟地址。

在linux内核中,采用cdev描述字符设备,成员dev_t 来定义设备号,以确定字符设备的唯一性,通过成员file_operations来定义字符设备驱动提供给VFS的接口函数
在字符设备驱动中,模块加载函数(register_chrdev_region()或者alloc_chrdev_region())来静态或者动态获取设备号,通过cdev_init()建立cdev与file_operations之间的连接,通过cdev_add()向系统添加一个cdev来完成注册。模块卸载函数cdev_del()来注销cdev,通过unregister_chrdev_region()来释放设备号。
用户空间访问该设备的程序通过linux系统调用,同名调用file_operations。