前言
本次主要是理清字符驱动的加载过程, 分析几个重要函数,以及文件系统与之对应关系。
字符设备开发基本步骤:
- 分配设备号
- 设备注册
- 创建设备节点 ( 用于和用户数据传输 )
- 注销设备相关信息
设备号详解
在内核设备号是32位变量,高12位是主设备号 , 低20位是次设备号。
分配设备号的方式有两种:静态和动态分配, 常用动态分配
函数:
设备ID = 高12位是主设备号 + 低20位是次设备号 ;
动态分配函数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev_t *dev 设备ID
unsigned baseminor 起始次设备号
unsigned count 设备数
const char *name 设备名
静态分配函数:
用于已知设备号的情况
int register_chrdev_region(dev_t from, unsigned count, const char *name)
dev_t from 已知的起始设备ID
unsigned count 设备数
const char *name 设备名
设备ID 和 主次设备号 相互转换宏:
#define MAJOR(dev) ((dev)>>8) // 从设备ID获取主设备号
#define MINOR(dev) ((dev) & 0xff) // // 从设备ID获取次设备号
#define MKDEV(ma,mi) ((ma)<<8 | (mi)) // // 将主设备和次设备号转换成 设备ID
内核管理的字符设备数据结构
原理
内核管理字符设备是通过哈希表进行的,哈希表其实可以简单理解为 数组串起来的链表:
图片来源: https://www.jianshu.com/p/f8ea1b33ed5b
内核字符设备的哈希表原型:
名字:chrdevs , 长度是255 个,但是有时会看到257的主设备号,这种内核直接模255得到2 ,会把257加载到chrdevs[2]的位置,然后按次设备号大小排在next里。
//linux-imx-4.1.15-2.1.0-g3dc0a4b-v2.7/fs/char_dev.c
#define CHRDEV_MAJOR_HASH_SIZE 255
static struct char_device_struct {
struct char_device_struct *next; // 同一个主设备号的链表
unsigned int major; // 主设备号
unsigned int baseminor; // 次设备号
int minorct; // 个数
char name[64];
struct cdev *cdev; /* 字符设备在文件系统的目录节点,以及各种操作接口 */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
在文件系统表现
如下图第一列是主设备号, 第二列是设备名,获取注册的所有字符设备cat /proc/devices ,将通过kgdb调试实时查看内核管理的字符设备哈希表。
kgdb调试打印内核哈希表:
在函数 chrdev_show 处打断点,当函数执行到这儿时开始打印哈希表
-
主设备号4
通过文件系统可知有两个设备 /dev/vc/0 和 、tty , 以下是动态打印的内核哈希表
baseminor = 0 的是第一个设备 ,baseminor = 1 的是第二个设备 tty, tty有设备63个 ,next等于0说明后面没有设备。
-
主设备号5
通过文件系统可知有3个设备 /dev/tty 、/dev/console 、/dev/ptmx , 以下是动态打印的内核哈希表
分配设备号及注册
/* 1、构建设备号 */
if (chrdev.major)
{
chrdev.devid = MKDEV(chrdev.major, 0);
register_chrdev_region(chrdev.devid, CHARDEV_CNT, "chrdev_register");
}
else
{
alloc_chrdev_region(&chrdev.devid, 5, CHARDEV_CNT, "chrdev_register");
chrdev.major = MAJOR(chrdev.devid);
}
/* 2、注册设备 */
cdev_init(&chrdev.cdev, &chrdev_ops);
cdev_add(&chrdev.cdev, chrdev.devid, CHARDEV_CNT);
如图分成两步, 第一步获取设备号,第二步正式注册设备。
注册的设备号在文件系统 /proc/devices 下可以查看 , 在内核注册设备实际就是申请 cdev 内存空间,然后将其挂载到字符设备节点下。
创建设备节点
源码:
/* 3、创建类 */
chrdev.class = class_create(THIS_MODULE, "chrdev_class");
if (IS_ERR(chrdev.class)) {
return PTR_ERR(chrdev.class);
}
/* 4、创建设备节点文件 */
chrdev.device = device_create(chrdev.class, NULL, chrdev.devid, NULL, "chrdev_device");
if (IS_ERR(chrdev.device))
{
return PTR_ERR(chrdev.device);
}
所谓的设备节点主要用来传输内核到用户空间的数据 , 节点存在于文件系统 /dev/ 下,创建节点前提还得有对应的类,所以以上先创建类在创建节点。
设备类:
设备节点:
注销设备
注销设备需按照以下方式进行, 主要是将分配的设备号从内核删除,将cdev 从文件系统删除, 以及删除设备节点。
cdev_del(&chrdev.cdev);/* 删除cdev */
unregister_chrdev_region(chrdev.devid, CHARDEV_CNT); /* 注销设备号 */
device_destroy(chrdev.class, chrdev.devid);
class_destroy(chrdev.class);