背景:怎么构建一个最简单的字符设备驱动并且可以使用app进行操作
2.4和2.6不同其实也就是构造设备节点的时候 一个是手动构造,一个是kobj构造
名称 | 大致意思 |
---|---|
设备 | proc/devices/设备名称 insmode驱动 |
设备节点 | /dev/xxx 对这个设备进行操作的文件 mknode使用主次设备号对设备关联 |
设备文件 | /sys/设备文件/设备属性 另一种操控驱动的方案,使用设备属性操作 设备文件关联 kobj |
设备属性 | sys/设备文件/设备属性 一个设备文件有多个设备属性 设备属性关联 kobj_attr |
大致方法
1.写驱动文件(file_operation),构造对应的read write 函数
2.分配设备名称(/proc/devices/设备名称)
3.注册进内核 使用主次设备号,设备名称 ,insmode 注册后 在/proc/devices/设备名称 就能找到对应设备
4.对这个设备进行设备文件注册,mknode 命令,生成 /dev/设备文件名称
5.应用程序打开 设备文件 获得设备的 file_operation 结构体 对设备进行操作
linux中万物皆为文件
图中的一个设备会有各种调用参数 open read write 来组成这个设备的 file_operation结构体
内核层中 在驱动程序里面 各种调用参数来对底层的硬件进行操作 作为驱动程序注册进内核变成一个设备 也会有 open read write等操作接口
开始有了驱动程序的 file_operation结构体
把 主次设备号 和 file_operation 结构体进行内核中的注册
注册到内核中 变成一个设备驱动程序
major = register_chrdev(DEV_MAJOR, DEV_NAME, &led_fops); //注册到内核
注册到内核后 有了 proc/devices/DEV_NAME
mknod [选项]… 名称 类型 [主设备号 次设备号]
其实在驱动层中是构造了一个char_device_struct 结构体,char_device_struct 结构体继承了驱动结构体
char_device_struct 是驱动的基本对象, 把这个对象保存在内核中
static struct char_device_struct {
//指向下一个链表节点
struct char_device_struct *next;
//主设备号
unsigned int major;
//次设备号
unsigned int baseminor;
//次设备号的数量
int minorct;
//设备的名称
char name[64];
//内核字符对象(已废弃)
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];
上一步已经有proc/devices/DEV_NAME
对这个DEV_NAME设备构建出对应的设备文件进行操作
使用mkmod /dev/test c 2 0 命令 构建对应设备的设备文件,通过查找内核设备所有的主次设备号挑选出对应的 设备
/dev/test 设备文件的文件名
c 文件为字符设备文件
2 主设备号
0 次设备号
应用程序是一个进程
进程有自己的结构体 task_srtuct
task_srtuct->files_struct->fd_array[]->file_operations
//里面保存了打开的文件 inode 节点 对这个节点 进行 write read 操作
使用 open(/dev/xxx) 打开这个文件 把这个文件的file_operation 保存在 inode 数组里面
后续使用 read write 调用 inode节点的各种操作函数 对文件 /dev/xxx 这个设备文件进行操作
总结 : 写驱动程序 就是为了让app操作 驱动程序中的file_operation接口
所以需要把驱动程序中的file_operation结构体注册到内核 内核有两个哈希表chrdevs[256],和cdev_map->probe[256]表
chrdevs哈希表 记录 char_device_struct结构体指针 新增一个字符设备,需要查询哈希表里面有没有
cdev_map->probe哈希表 记录probe结构体指针 用于记录驱动里file_operattion 所以我们根据主次设备号就能找到 要操作的file_operation结构体
app使用open打开的文件 抽象为inode结构体 对这个文件的操作接口file_operation也记录在 inode结构体上
这样就能通过app进行操作了
下面用几个问答尽量说明白
把file_operations文件操作接口注册到内核,内核通过主次设备号来登记记录它
struct cdev 记录操作接口 ,继承kobj
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
//这个函数初始化cdev,赋值驱动里自己写的file_operation结构体
cdev_init(struct cdev *cdev, const struct file_operations *fops)
用 file_operation 的时候通过两个哈希表来找到 自己写的file_operation
这时候需要引入两个全局变量 chrdevs 和 struct kobj_map *cdev_map
里面保存了两个哈希表chrdevs[255],cdev_map->probe[255]
第一个哈希表 chrdevs[255]:登记设备号
注册方法 register_chrdev() 每次新增一个字符设备,需要查询哈希表里面有没有
__register_chrdev_region()
存在意义 : 保存字符设备,新增字符设备看放哪个地方,方便查询
第二个 cdev_map->probe:保存驱动基本对象struct cdev
注册方法 cdev_add()
存在意义:根据主次设备号,找到cdev,找到file_operation结构体
通过两个哈希表已经保存了cdev 保存了主次设备号 还有自己file_operation结构体
这时候需要 mknod指令+主从设备号 建立设备节点
把自己file_operation结构体和设备节点绑定
通过主次设备号在cdev_map中找到cdev->file_operations
把cdev->file_operations绑定到新的设备节点中
register_chrdev() cdev_add() 注册设备号后
已经注册了的设备号 可以使用cat /proc/device 个人感觉把哈希表1列出来
看见对应的名字和主设备号
内核希望一个驱动file_opera 占一个主设备号 多个次设备号
char_device_struct 关键数据结构 作为linux哈希表1的链表组成
static struct char_device_struct {
//指向下一个链表节点
struct char_device_struct *next;
//主设备号
unsigned int major;
//次设备号
unsigned int baseminor;
//次设备号的数量
int minorct;
//设备的名称
char name[64];
//内核字符对象(已废弃)
struct cdev *cdev; /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; //初始化 指针数组 256的数组 都存了结构体指针 struct char_device_struct
//下面是往表中注册新设备 根据主次设备号 名字 新增char_device_struct
register_chrdev()
__register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name)
//保存新注册的设备号到chrdevs哈希表中,防止设备号冲突
//这四个函数参数都用来构建 新char_device_struct 结构体 存入这个结构体到哈希表
struct char_device_struct *cd, **cp;
cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);//申请内存
mutex_lock(&chrdevs_lock);//增加互斥锁,这个锁也是全局变量
if (major == 0)
major = find_dynamic_major();//为0在chrdevs表中找一个空闲位置
//保存主次设备号和设备个数
cd->major = major;
cd->baseminor = baseminor;
cd->minorct = minorct;
//保存设备名称
strlcpy(cd->name, name, sizeof(cd->name));
//计算主设备号对应哈希表的位置
i = major_to_index(major);
//遍历链表元素指针 如果主设备号是0,遍历的就是chrdevs[0]->char_device_struct链表
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
//主设备号相等,看次设备号是不是冲突
/* */
if (*cp && (*cp)->major == major) {
int old_min = (*cp)->baseminor;
int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
int new_min = baseminor;
int new_max = baseminor + minorct - 1;
//存入新的char_device_struct指针到哈希表中
cd->next = *cp;
*cp = cd;
如果以后想使用fie_operation接口,可以通过设备号在probes哈希表(哈希表2)中
找到probe->data指向的cdev结构体,再找到file_operation
//struct cdev字符设备管理对象 继承kobj
struct cdev {
//内核驱动基本对象
struct kobject kobj;
//相关内核模块,通过内核模块加载驱动,防止驱动还在工作,内核模块卸载
struct module *owner;
//设备驱动接口
const struct file_operations *ops;
//链表节点
struct list_head list;
//设备号
dev_t dev;
//次设备号的数量
unsigned int count;
} __randomize_layout;
//哈希表probes
struct kobj_map {
struct probe { //内嵌结构体
//指向下一个链表节点
struct probe *next;
//设备号
dev_t dev;
//次设备号的数量
unsigned long range;
struct module *owner;
kobj_probe_t *get;
int (*lock)(dev_t, void *);
//空指针,内核常用技巧,用来保持sruct cdev 指针 就能拿到file_operation 结构体
void *data;
} *probes[255]; //这里也就是probes哈希表,第二个哈希表,用来记录file_operattion
struct mutex *lock;
};
//往哈希表2增加新的设备
//这个函数初始化cdev,赋值file_operation结构体
cdev_init(struct cdev *cdev, const struct file_operations *fops)
kobject_init(&cdev->kobj, &ktype_cdev_default); //初始化一个kobj(仅仅初始化,没有绑定目录等)
cdev->ops = fops;//给cdev的ops指针赋值 自己的file_operation接口
//继续初始化一下cdev,把cdev放入probes哈希表
cdev_add(struct cdev *p, dev_t dev, unsigned count) //接受cdev结构体,设备号,数量
p->dev = dev; //保存设备号
p->count = count; //保存设备号和设备号的数量
error = kobj_map(cdev_map, dev, count, NULL, //把cdev结构体填充到probes哈希表上
exact_match, exact_lock, p);
unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1; //判断次设备号是否溢出
if (n > 255) //判断主设备号是否大于255
p = kmalloc_array(n, sizeof(struct probe), GFP_KERNEL); //申请内存
//申请完后给probe结构体赋值
p->owner = module;
p->get = probe;
p->lock = lock;
p->dev = dev; //保存设备号
p->range = range; //保存本次设备号数量
p->data = data; //这里保存了cdev结构体
struct probe **s = &domain->probes[index % 255]; //保存在probes哈希表中
kobject_get(p->kobj.parent);
新版本中使用kobj创建设备节点
这里说老版本的 linux指令 mkmod /dev/test c 2 0
图呢? 结构体呢!!!
用户空间 : mknod命令->glibc库mknode()->sys_mkonde
内核空间 : sys_mknodat()->vfs_mknod()->dir->i_op->mknod(ext4_mknod)->init_special_inode()
init_special_inode()
inode类型 如果是字符设备类型 def_chr_fops作为该文件的操作接口
设备号记录在inode->i_rdev
自己构建的file_operation等在应用程序调用open函数之后,才会绑定在文件上
所以目前自己的设备节点连接的file_operation是 通用的def_chr_fops
struct inode {
xxx......
dev_t i_rdev;
struct inode_operations *i_op;
struct cdev *i_cdev;
}
一个文件对应着一个inode 设备节点中的字符设备通用file_operation对inode里面的设备号查找
找出驱动程序真正的 file_operation结构体 结构体进行替换
再把真正的结构体保存在进程的数组 直接调用
fd = open("/dev/xxx",O_RDWR)
驱动层
sys_open()
do_sys_open()
get_unused_fd_flags()
do_dentry_open(struct file *f,struct inode *inode,int (*open)(struct inode *, struct file *))
/*把inode的i_fop赋值给struct file的f_op*/
f->f_op = fops_get(inode->i_fop);
open = f->f_op->open; //这里的f_op 是上面的def_chr_fops
error = open(inode, f);
def_chr_fops->chrdev_open()!!!!
const struct file_operations *fops;
struct cdev *p;
struct kobject *kobj;
struct cdev *new = NULL;
/* 内核哈希表 cdev_map 中,根据设备号查找自己注册的sturct cdev,获取cdev中的file_operation接口
不懂怎么保存在哈希表的回去看 保持哈希表 */
kobj = kobj_lookup(cdev_map, inode>i_rdev,&idx);//cdev_map全局变量,能知道内核哈希表
现在得到cdev里面的kobj成员
new = container_of(kobj, struct cdev, kobj);//从kobj成员得到cdev变量
inode->i_cdev = p = new;//把cdev赋值给inode节点
fops = fops_get(p->ops);//获取到自己设计的file_o
peration结构体
/*把cdev中的file_operation接口赋值给struct file的f_op*/
replace_fops(filp, fops);
/*调用自己实现的file_operation接口中的open函数*/
filp->f_op->open(inode, filp);
/* 后续对inode节点的操作,都是调用自己的file_operation结构体了 */
fd_install()
class_create()创建目录 /sys/class/xxx 同时返回class对象
device_create()创建目录 创建/sys/class/xxx/yyy目录 同时返回device对象
虽然class_create() 和 device_create() 都能创建目录项
但device_create()不止创建了 /sys/class/xxx/yyy 还在yyy目录下创建了属性文件(/sys/class/xxx/yyy/dev)
属性文件记录硬件设备的设备号
创建完属性文件后 继续使用kobject_uevent()发送驱动添加的消息 给用户空间的udev守护进程
这时udev会找到创建的yyy 读取dev这个设备属性文件夹 获得硬件设备的设备号
udev 调用mknod()函数 来创建/dev/yyy的设备节点
新旧本质 到最后都调用了mknode()函数
只不过新方法的函数参数 放在(/sys/class/xxx/yyy/dev)