Linux驱动开发 / 字符设备驱动内幕 (1)
哈喽,我是老吴,继续记录我的学习心得。
一、保持专注的几个技巧
将最重要的事放在早上做。
待在无干扰环境下,比如图书馆。
意识到刚坐下开始投入工作前,有点负面小情绪是特别正常的现象。
让“开心一刻”成为计划的一部分。
拥有合情合理的日计划和周计划。
二、Linux 字符设备驱动内幕 (1)
正文目录:
1. 什么是字符设备驱动?
2. 快速体验字符设备驱动和应用程序 (超简单的 demo)
3. 字符设备在内核里的抽象3.1 字符设备核心代码概览3.2 对字符设备进行抽象: struct cdev3.3 对字符设备的操作进行抽象:struct file_operations
4. 更多值得学习的知识点
5. 相关参考
写作目的:
探索 Linux 字符设备驱动。
测试环境:
Ubuntu 16.04
Gcc 5.4.0
1. 什么是字符设备驱动?
现实世界中存在着成千上万的硬件设备,这些设备在硬件特性和使用方式上都各不相同。Linux 系统的大牛们从这些形形色色的设备中提取出共性,将它们抽象为三大类:字符设备、块设备和网络设备。
基于代码质量和复用性的考虑,Linux 内核针对每一类硬件设备都提供了对应的驱动模型框架,一般包括基本的内核设施和文件系统接口。开发人员在写某类设备驱动程序时,能一套完整的驱动模型框架可以使用,从而将精力放在硬件设备本身的控制上。
简单的 Linux 设备驱动程序结构图:
详细一点的 Linux 设备驱动程序结构图:
2. 快速体验字符设备驱动和应用程序 (超简单的 demo)
1) 字符设备驱动 (chrdev_drv.c):
字符设备的打开和读函数:
static struct cdev chr_dev; // 字符设备抽象
static dev_t ndev; // 设备号
static int chr_open(struct inode *nd, struct file *filp)
{printk("chr_open, major=%d, minor=%d\n", MAJOR(nd->i_rdev), MINOR(nd->i_rdev));return 0;
}static ssize_t chr_read(struct file *filp, char __user *u, size_t sz, loff_t *off)
{printk("In chr_read()\n");return 0;
}static int chr_release(struct inode *nd, struct file *filp)
{printk("In chr_release()\n");return 0;
}
文件操作函数集:
struct file_operations chr_ops =
{.owner = THIS_MODULE,.open = chr_open,.read = chr_read,.release = chr_release,
};
模块加载和卸载:
static int demo_init(void)
{int ret;cdev_init(&chr_dev, &chr_ops);ret = alloc_chrdev_region(&ndev, 0, 1, "chr_dev");if(ret < 0)return ret;printk("demo_init():major=%d, minor=%d\n", MAJOR(ndev), MINOR(ndev));ret = cdev_add(&chr_dev, ndev, 1);if(ret < 0)return ret;return 0;
}
static void demo_exit(void)
{printk("demo_exit...\n");cdev_del(&chr_dev);unregister_chrdev_region(ndev, 1);
}
2) 访问字符设备的应用程序:
int main()
{int ret;char buf[32];int fd = open("/dev/chr_dev", O_RDONLY|O_NDELAY);if(fd < 0){printf("open file %s failed!\n", CHR_DEV_NAME);return -1;}read(fd, buf, 32);close(fd);return 0;
}
3) 不用完全理解代码的含义,直接看运行效果:
# 编译驱动程序
$ make KERNELDIR=XXX/linux ARCH=arm CROSS_COMPILE=arm-linux-# 编译应用程序
$ arm-linux-gcc chrdev_app.c -o chrdev_app# 加载驱动模块
$ insmod chrdev_drv.ko
demo_init():major=242, minor=0# 手动创建字符设备文件节点
$ mknod /dev/chr_dev c 242 0# 运行应用程序
$ ./chrdev_app
chr_open, major=242, minor=0
In chr_read()
In chr_release()
从上面测运行结果可知,应用程序调用 chrdev_app.c / open() 会导致驱动程序 chrdev_drv.c / struct file_operations chr_ops->open() 被调用,read 操作也是类似。
内核是如何实现上述功能的?
带着这个困惑来了解字符设备驱动的框架,才不会迷失在内核里各种复杂的代码细节里。
3 字符设备在内核里的抽象
3.1 字符设备核心代码概览
在深入阅读各种代码之前,先整体地概览一遍将会涉及到的程序文件,找出核心主干,能有效地避免陷入繁杂的代码细节中。
1) 分解 C source 文件,fs/char_dev.c (Linux-4.14):
作用:
char_dev.c 是字符设备驱动框架的核心实现文件,它位于 fs 目录中,说明了字符设备驱动和文件系统是紧密联系在一起的。
内容(以重要性排序):
1> public 函数:
// 1. 字符设备子系统初始化
void __init chrdev_init(void) // 2. struct cdev 管理相关
void chrdev_show(struct seq_file *f, off_t offset)
void cdev_put(struct cdev *p)
void cd_forget(struct inode *inode)
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
void cdev_set_parent(struct cdev *p, struct kobject *kobj)
int cdev_device_add(struct cdev *cdev, struct device *dev)
void cdev_device_del(struct cdev *cdev, struct device *dev)
void cdev_del(struct cdev *p)
struct cdev *cdev_alloc(void)
void cdev_init(struct cdev *cdev, const struct file_operations *fops)// 3. 设备号管理相关
__register_chrdev_region(unsigned int major, unsigned int baseminor,
__unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)
int register_chrdev_region(dev_t from, unsigned count, const char *name)
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
int __register_chrdev(unsigned int major, unsigned int baseminor,
void unregister_chrdev_region(dev_t from, unsigned count)
void __unregister_chrdev(unsigned int major, unsigned int baseminor,
在 Linux 代码中,双下划线(__)开始的函数名表示这是一个低层接口, 应当小心使用。如果你调用这个函数,确信你知道你在做什么。也就是说,阅读代码时,双下划线开头的函数可以暂时放一边。
struct cdev 管理相关:cdev_add() / cdev_del() / cdev_init(),需重点关注。
设备号管理相关:alloc_chrdev_region() / register_chrdev_region(),需重点关注。
2> public 变量:
const struct file_operations def_chr_fops = {.open = chrdev_open,.llseek = noop_llseek,
};
类似于高级语言的多态机制,在 Linux 各个子系统的驱动框架中一般用一个统一的 file_operations->open 函数关联到各个具体的硬件驱动的 struct file_operations,需要重点关注。
3> private 变量:
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];static struct kobj_map *cdev_map;
static struct kobj_type ktype_cdev_default;
static struct kobj_type ktype_cdev_dynamic;
struct char_device_struct *chrdevs[] 数组是字符设备驱动框架的核心数据结构,需重点关注。
4> private 函数:
static inline int major_to_index(unsigned major)
static int find_dynamic_major(void)
static struct kobject *cdev_get(struct cdev *p)
static int chrdev_open(struct inode *inode, struct file *filp)
static void cdev_purge(struct cdev *cdev)
static struct kobject *exact_match(dev_t dev, int *part, void *data)
static int exact_lock(dev_t dev, void *data)
static void cdev_unmap(dev_t dev, unsigned count)
static void cdev_default_release(struct kobject *kobj)
static void cdev_dynamic_release(struct kobject *kobj)
static struct kobject *base_probe(dev_t dev, int *part, void *data)
helper 类函数,不太重要。
2) 分解 C header 文件,include/linux/cdev.h (Linux-4.14):
作用:
包含字符设备驱动相关结构体的定义、以及一些字符设备核心 API 的声明。
内容:
1> struct cdev 结构体:
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;
struct cdev 是字符设备驱动的核心抽象,需重点关注。
3.2 对字符设备进行抽象: struct cdev
编写字符设备驱动程序就是为了管理和控制字符设备,Linux 内核将字符设备抽象为一 个数据结构:struct cdev。这个结构体似乎从 Linux-2.6 到现在 Linux-5.8 都没有发生变化。
1) struct cdev 成员简介:
目前没必要完全理解这些成员的作用,有个大概印象就好:
struct kobject kobj: 内嵌的内核对象,与 Linux 设备驱动模型相关,后续需专门写一篇文章来介绍。
struct module *owner: 指向拥有这个结构体的模块的指针,这个成员用来在它的操作还在被使用时阻止模块被卸载,一般初始化为 THIS_MODULE;
struct file_operations *ops: 在Linux通用文件模型下,字符设备的文件操作函数集,后续详解。
struct list_head list:用于管理 struct cdev 的链表,后续需专门写一篇文章来介绍。
字符设备的设备号,由主设备号和次设备号构成,后续需专门写一篇文章来介绍。
2) 两种方式创建 struct cdev 对象:
这里用对象一词,是为了引导大家用面向对象的思维来看待 Linux 内核的设计。
面向过程还是面向对象,不应该和语言绑定在一起,应该理解为 2 种不同的编程思维。人脑是很美妙的,在不同的场景,只要你愿意,它就能应用不同的思维方式来解决问题。设计 Linux 内核代码的神牛们,可谓是各个都是面向对象编程的大牛,练习编程就应该练习对事物的抽象能力,C 程序员如果觉得自己缺乏这方面的能力,不如学习一下 Java 编程。
静态定义:
static struct cdev chr_dev;
动态分配:
struct cdev *my_cdev = cdev_alloc();
cdev_alloc() 不仅会为struct cdev对象分配内存空间,还会对该对象进行必要的初始化:
struct cdev *cdev_alloc(void)
{struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);if (p) {INIT_LIST_HEAD(&p->list);kobject_init(&p->kobj, &ktype_cdev_dynamic);}return p;
}
一个值得注意的点:
我搜索了一下内核源码,发现大多数驱动都选择静态定义 struct cdev,这样更简单省事。
3) 某个真实字符硬件的抽象:
数据结构 struct cdev 作为字符设备的抽象,仅仅是为了满足 Linux 内核对字符设备驱动程序框架结构设计的需要。
现实中,一个具体的字符硬件设备的数据结构的抽象往往要复杂得多,在这种情况下 struct cdev 常常作为一种内嵌的成员变量出现在实际设备的数据结构中。
例如 drvier/watchdog/watchdog_dev.c:
struct watchdog_core_data {struct kref kref;struct cdev cdev;struct watchdog_device *wdd;struct mutex lock;unsigned long last_keepalive;unsigned long last_hw_keepalive;struct delayed_work work;unsigned long status; /* Internal status bits */
};
4) 初始化 cdev 对象:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{memset(cdev, 0, sizeof *cdev);INIT_LIST_HEAD(&cdev->list);kobject_init(&cdev->kobj, &ktype_cdev_default);cdev->ops = fops;
}
cdev_init() 最重要的作用就是将 struct cdev 对象和 struct file_operations 对象绑定在一起。
一些值得注意的点:
cdev_init() 和 cdev_alloc() 中有一部分功能是重叠的(例如 kobject_init()),所以 cdev_init() 只能搭配静态定义 struct cdev 的方式来使用。
如果采用 cdev_alloc() 动态分配 struct cdev 对象,则需自行 cdev->ops = fops 。
3.3 对字符设备的操作进行抽象:struct file_operations
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);[...]int (*mmap) (struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);[...];} __randomize_layout;
目前没必要完全理解这些成员的作用,有个大概印象就好。
字符设备驱动程序中一个极其关键的数据结构,字符设备驱动程序的编写,就是是围绕着如何实现 struct file_operations 中的那些函数指针成员而展开的。
通过内核文件系统组件在其间的穿针引线,应用程序中对文件类函数(open、read、write)的调用,将最终被转接到 struct file_operations 中对应函数指针的具体实现上,后续需专门写一篇文章来介绍。
鉴于大多数人的注意力无法在一篇文章里上集中太久,更多的内容将放在后面的文章里。建议大家可以先自行阅读相关书籍,不是自己理解到的东西是消化不了的。
4. 更多值得学习的知识点
字符设备号的构成与管理
字符设备的注册
生成字符设备文件的方式有哪些?
创建字符设备文件是发生了什么?
字符设备文件是如何关联到字符设备驱动的?
分析一些真实的字符设备驱动
5. 相关参考
《Linux 内核文档》
《Linux设备驱动程序》(ldd) / 第 3 章节
《深入Linux设备驱动程序内核机制》(ildd) / 第 2 章节
《精通Linux设备驱动程序开发》(eldd) / 第 2 章节
《Linux设备驱动开发详解》(ldds) / 第 6 章节
《深入Linux内核架构》(plka) / 第 6 章节
《嵌入式应用开发完全手册》
三、思考技术,也思考人生
学习技术,更要学习如何生活。
你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。
对 嵌入式系统 (Linux、RTOS、OpenWrt、Android) 和 开源软件 感兴趣,想和更多人互相交流学习,请关注公众号:嵌入式Hacker,一起来学习吧。
无论是关注或转发,还是打赏,都是对作者莫大的支持。觉得文章对你有价值的话,不妨点个 在看和点赞 哦。
欢迎加入我的微信群:先加我,我拉你进群,暗号(我最棒)。
祝各位工作顺利,家庭幸福,财源滚滚~~~
Linux驱动开发 / 字符设备驱动内幕 (1)相关推荐
- <Linux开发>--驱动开发-- 字符设备驱动(3) 过程详细记录
<Linux开发>–驱动开发-- 字符设备驱动(3) 过程详细记录 驱动开发是建立再系统之上的,前面作者也记录了系统移植的过程记录,如果有兴趣,可进入博主的主页查看相关文章,这里就不添加链 ...
- linux的驱动开发——字符设备驱动
1.字符设备驱动 \qquad字符设备驱动是最基本,最常用的设备.它将千差万别的硬件设备采用统一的接口封装起来,屏蔽了硬件的差异,简化了应用层的操作. 2.描述所有字符设备的结构体 \qquad描述所 ...
- 正点原子-驱动开发-字符设备驱动
Linux中的三大类驱动:字符设备.块和网络设备驱动 I2C.SPI.音频等都属于字符设备驱动 的类型 EMMC.NAND.SD卡和 U盘等存储都属于块设备 网卡,WIFI等都属于网络驱动 一个设备可 ...
- linux open函数_Linux驱动开发 / 字符设备驱动内幕 (1)
哈喽,我是老吴,继续记录我的学习心得. 一.保持专注的几个技巧 将最重要的事放在早上做. 待在无干扰环境下,比如图书馆. 意识到刚坐下开始投入工作前,有点负面小情绪是特别正常的现象. 让"开 ...
- _Linux驱动开发 / 字符设备驱动内幕 (1)
哈喽,我是老吴,继续记录我的学习心得. 一.保持专注的几个技巧 将最重要的事放在早上做. 待在无干扰环境下,比如图书馆. 意识到刚坐下开始投入工作前,有点负面小情绪是特别正常的现象. 让"开 ...
- STM32MP157驱动开发——字符设备驱动
一.简介 字符设备是 Linux 驱动中最基本的一类设备驱动,字符设备就是一个一个字节,按照字节 流进行读写操作的设备,读写数据是分先后顺序的.比如我们最常见的点灯.按键. IIC. SPI, LCD ...
- 【linux驱动之字符设备驱动基础】
linux驱动之字符设备驱动基础 文章目录 linux驱动之字符设备驱动基础 前言 一.开启驱动学习之路 二.驱动预备知识 三.什么是驱动? 3.1 驱动概念 3.2 linux 体系架构 3.3 模 ...
- Linux 设备驱动开发 —— platform设备驱动应用实例解析
前面我们已经学习了platform设备的理论知识Linux 设备驱动开发 -- platform 设备驱动 ,下面将通过一个实例来深入我们的学习. 一.platform 驱动的工作过程 platfor ...
- Linux驱动开发|块设备驱动
块设备驱动 块设备驱动是 Linux 三大驱动类型之一,块设备驱动比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,下面介绍块设备驱动框架及使用 一.块设备介绍 块设备是针对存储设备的 ...
- linux用户空间flash驱动,全面掌握Linux驱动框架——字符设备驱动、I2C驱动、总线设备驱动、NAND FLASH驱动...
原标题:全面掌握Linux驱动框架--字符设备驱动.I2C驱动.总线设备驱动.NAND FLASH驱动 字符设备驱动 哈~ 这几天都在发图,通过这种方式,我们希望能帮大家梳理学过的知识,全局的掌握Li ...
最新文章
- linux下的makefile编程
- idea git 在文件上点了revert怎么复原_在 IntelliJ IDEA 中使用 Git,太方便了
- 高程数据处理_珠峰长高了吗?新高程怎么算出来的?揭秘
- 【渝粤教育】国家开放大学2018年春季 0273-21T中国现代文学 参考试题
- mybatis学习(14):log4j:ERROR Category option 1 not a decimal integer.
- php 9000 端口没起来,ubuntu fpm-php 未监听9000端口问题
- 数据结构之外部排序:最佳归并树
- MyCat 数据库分片极简体验
- 激战 5G:国内外技术的分水岭
- 计算机一级考试题库字处理题库,全国计算机一级考试题库试题
- python3 numpy教程_Python Numpy 教程
- JSP从入门到精通_课堂实战视频教程
- 应用交付学习笔记三-BIG-IP LTM健康检查
- ssh-keygen 常用命令与参数
- App、H5、PC应用多端开发框架Flutter 2发布
- 爱尔兰卫生部遭Conti勒索软件攻击说明与建议
- “国家自主贡献亚洲交通倡议项目-中国部分”启动:实现交通运输零排放,中国至关重要...
- python五子棋单机版源代码_Python基于pygame实现单机版五子棋对战
- 替换word中手动换行(软回车)为段落标记(硬回车)
- 夏天快到了,热不热?下雪啦
热门文章
- php 怎么判断月份最后一天_PHP基础案例三:判断学生星座
- python取列表前几个元素_从Python列表中获取前n个唯一元素
- 百度测试linux面试题,【百度百度Linux面试题】面试问题:Linux查看… - 看准网
- dom控制html元素编号,JavaScript DOM对象控制HTML元素详解
- StanfordDB class自学笔记 (12) Constraints and Triggers
- 学python前端需要哪些基础知识_前端基础知识整理回顾~~
- python如何使用多线程_python实现多线程教程
- http和https简介、区别以及客户端到服务器https通讯步骤
- java如何实现容器_Java的容器都有哪些,怎么实现的
- mycat 读写分离 处理延时