Linux驱动学习
1. 设备总线模型
bus 负责维护 注册进来的devcie 与 driver ,每注册进来一个device 或者 driver 都会调用 Bus->match 函数 将device 与 driver 进行配对,并将它们加入链表,如果配对成功,调用Bus->probe或者driver->probe函数, 调用 kobject_uevent 函数设置环境变量,mdev进行创建设备节点等操作。
在总线设备驱动模型中,需关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。当系统向内核注册每一个驱动程序时,都要通过调用 platform_driver_register 函数将驱动程序注册到总线,并将其放入所属总线的 drv链表中,注册驱动的时候会调用所属总线的match函数寻找该总线上与之匹配的每一个设备,如果找到与之匹配的设备则会调用相应的probe函数将相应的设备和驱动进行绑定; 同样的当系统向内核注册每一个设备时,都要通过调用platform_device_register 函数将设备注册到总线,并将其放入所属总线的dev链表中,注册设备的时候同样也会调用所属总线的match函数寻找该总线上与之匹配的每一个驱动程序,如果找到与之匹配的驱动程序时会调用相应的probe函数将相应的设备和驱动进行绑定;而这一匹配的过程是由总线自动完成的。
1.3 参考文档
2. DTS
2.1 设备树概念
Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):
CPU的数量和类别
内存基地址和大小
总线和桥
外设连接
中断控制器和中断使用情况
GPIO控制器和GPIO使用情况
Clock控制器和Clock使用情况
这种以树状节点的方式描述一个设备的各种硬件信息:CPU、GPIO、时钟、中断、内存等,形成类似文本文件,很好的解决了这些问题。
它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
通常由.dts文件以文本方式对系统设备树进行描述,经过Device Tree Compiler(dtc)将dts文件转换成二进制文件binary device tree blob(dtb),.dtb文件可由Linux内核解析,有了device tree就可以在不改动Linux内核的情况下,对不同的平台实现无差异的支持,只需更换相应的dts文件,即可满足。
2.2 参考文档
3. 字符设备驱动
3.1 字符设备、字符设备驱动与用户空间访问该设备的程序三者之间的关系
如图,在Linux内核中:
a – 使用cdev结构体来描述字符设备;
b – 通过其成员dev_t来定义设备号(分为主、次设备号)以确定字符设备的唯一性;
c – 通过其成员file_operations来定义字符设备驱动提供给VFS的接口函数,如常见的open()、read()、write()等;
在Linux字符设备驱动中:
a – 模块加载函数通过 register_chrdev_region( ) 或 alloc_chrdev_region( )来静态或者动态获取设备号;
b – 通过 cdev_init( ) 建立cdev与 file_operations之间的连接,通过 cdev_add( ) 向系统添加一个cdev以完成注册;
c – 模块卸载函数通过cdev_del( )来注销cdev,通过 unregister_chrdev_region( )来释放设备号;
用户空间访问该设备的程序:
a – 通过Linux系统调用,如open( )、read( )、write( ),来“调用”file_operations来定义字符设备驱动提供给VFS的接口函数;
3.2 设备注册以及使用方法
3.2.1 设备号的分配
设备号是一个数字,用来标示设备。设备文件的创建要指明主设备号和次设备号。主设备号表明设备的类型,与一个确定的驱动程序对应,次设备号通常用来标明不同属性,例如不同的使用方法,不同的位置等,它标志某个具体的物理设备。
在2.6中,用dev_t类型来描述设备号,它是一个32位类型,高12为主设备号,后20为次设备号。使用MAJOR和MINOR取主次设备号,使用MKDEV来合并得到设备号。
设备号的分配分为两种:静态和动态。
静态分配知道主设备号,通过参数指定第一个设备号,通常次设备号为0,所以可以这么得到MKDEV(主设备号, 0)。
函数为int register_chrdev_region(dev_t first, unsigned int count, char* name)
count为设备号数目,name为设备名
动态分配通过参数仅需要第一个次设备号,通常为0,和分配的数目
int alloc_chrdev_region(dev_t dev, unsigned int fristminor, char name)
动态的时候需要注意,需要保存分配到的主设备号,不卸载设备的时候有麻烦。
释放已经分配的设备号使用unregister_chrdev_region(dev_t first, unsigned int count),不管动态还是静态。
3.2.2 字符设备驱动模型
3.2.3 如何使用字符设备驱动
3.3 参考文档
- Linux字符设备驱动实现 - Neptune15 - 博客园 (cnblogs.com)
- (49条消息) Linux 字符设备驱动结构(一)—— cdev 结构体、设备号相关知识解析_知秋一叶-优快云博客
- Linux驱动篇(五)–字符设备驱动(一) - 知乎 (zhihu.com)
4. misc设备驱动
4.1 什么是MISC设备
(1)misc中文名就是杂项设备\杂散设备,因为现在的硬件设备多种多样,有好些设备不好对他们进行一个单独的分类,所以就将这些设备全部归属于
杂散设备,也就是misc设备,例如像adc、buzzer等这些设备一般都归属于misc中。
(2)需要注意的是,虽然这些设备归属于杂散设备中,但是其实你也可以不把设备放在这个类中,这都是驱动工程师按照自己的想法做的,你想把他们写在
misc类设备中也可以,自己单独建立一个类也是可以的,只不过是否标准化而已,因为人家既然建立了这个类,那你就把这个设备放在这个类下,不是很好吗?
你还自己单独搞一个类,虽然这也没错,只不过是说你不按套路出牌。
(3)所有的misc类设备都是字符设备,也就是misc类设备其实是字符设备中分出来的一个小类。
(4)misc类设备在应用层的操作接口:/dev/xxxx, 设备类对应在 /sys/class/misc
(5)misc类设备有自己的一套驱动框架,所以我们写一个misc设备的驱动直接利用的是内核中提供的驱动框架来实现的。misc驱动框架是对内核提供的原始的字符设备
注册接口的一个类层次的封装,很多典型的字符设备都可以归于misc设备,都可以利用misc提供的驱动框架来编写驱动代码,通过misc驱动框架来进行管理。
4.2. 与字符设备的对比
注册
设置好miscdevice结构体后使用misc_register向系统注册一个MISC设备
int misc_register(struct miscdevice *misc);
原先我们需要
alloc_chrdev_region(); /* 申请设备号 */
cdev_init(); /* 初始化cdev */
cdev_add(); /* 添加cdev */
class_create(); /* 创建类 */
device_create(); /* 创建设备 */
卸载
当我们卸载驱动的时候调用misc_deregister来注销MISC设备
int misc_deregister(struct miscdevice *misc);
原先我们需要
cdev_del(); /* 删除cdev */
unregister_chrdev_region(); /* 注销设备号 */
device_destroy(); /* 删除设备 */
class_destroy(); /* 删除类 */
4.3 参考文档
- Linux驱动框架之misc类设备驱动框架 - 涛少& - 博客园 (cnblogs.com)
- (49条消息) Linux misc设备(一)misc驱动框架_JT同学的博客-优快云博客
- Linux驱动之Misc子系统剖析 | 客舍青青 - 江湖厮杀,刀剑无情君且随意 - 客舍青青,菜菜,MySQL,C语言,C++,java,python,嵌入式,JavaScript (caiyifan.cn)
5. 块设备驱动
5.1 块设备概念
是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。
块设备不能向字符设备那样访问,而是要先将请求放入队列,优化调整顺序后再执行,这种访问方式称为"电梯调度算法"。
5.3 参考文档
- (49条消息) LINUX驱动之块设备驱动_勇士后卫头盔哥的博客-优快云博客
- (49条消息) 十六、Linux驱动之块设备驱动_墨、白的博客-优快云博客
- Linux驱动篇(九)——块驱动简述 - 知乎 (zhihu.com)
6. 网络设备驱动
6.1 网咯设备描述
网络设备是计算机体系结构中必不可少的一部分,处理器如果想与外界通信,通常都会选择网络设备作为通信接口
7. 同步与互斥
7.1 线程同步(互斥锁、信号量、条件量)
7.1.1 互斥锁
互斥量(也称为互斥锁)出自POSIX线程标准,可以用来同步同一进程中的各个线程。当然如果一个互斥量存放在多个进程共享的某个内存区中,那么还可以通过互斥量来进行进程间的同步。
互斥量,从字面上就可以知道是相互排斥的意思,它是最基本的同步工具,用于保护临界区(共享资源),以保证在任何时刻只有一个线程能够访问共享的资源。
互斥量类型声明为pthread_mutex_t数据类型,在<bits/pthreadtypes.h>中有具体的定义。
在Linux环境下,类型pthread_mutex_t其本质是一个结构体。但是为了简化理解,应用时可忽略其实现细节,简单当成整数看待。mutex一般以下面方式定义:
pthread_mutex_t mutex;
变量mutex只有两种取值1、0。
/* 初始化一个互斥锁(互斥量)mutex,初值可视为1;*/
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
/* 销毁一个互斥锁 */
int pthread_mutex_destroy(pthread_mutex_t *mutex);
/* Try locking a mutex. */
int pthread_mutex_trylock (pthread_mutex_t *__mutex);
/* Lock a mutex. */
int pthread_mutex_lock (pthread_mutex_t *__mutex);
/* Unlock a mutex. */
int pthread_mutex_unlock (pthread_mutex_t *__mutex);
这几个函数都很简单,通过pthread_mutex_lock()函数获得访问共享资源的权限,如果已经有其他线程锁住互斥量,那么该函数会是线程阻塞指定该互斥量解锁为止。 pthread_mutex_trylock()是对应的非阻塞函数,如果互斥量已被占用,它会返回一个EBUSY错误。访问完共享资源后,一定要通过pthread_mutex_unlock() 函数,释放占用的互斥量。允许其他线程访问该资源。
互斥量的使用流程:线程占用互斥量,然后访问共享资源,最后释放互斥量。
7.1.2 信号量
信号量是包含一个非负整数型的变量,并且带有两个原子操作wait和signal。Wait还可以被称为down、P或lock,signal还可以被称为up、V、unlock或post。在UNIX的API中(POSIX标准)用的是wait和post。
对于wait操作,如果信号量的非负整形变量S大于0,wait就将其减1,如果S等于0,wait就将调用线程阻塞;对于post操作,如果有线程在信号量上阻塞(此时S等于0),post就会解除对某个等待线程的阻塞,使其从wait中返回,如果没有线程阻塞在信号量上,post就将S加1.
由此可见,S可以被理解为一种资源的数量,信号量即是通过控制这种资源的分配来实现互斥和同步的。如果把S设为1,那么信号量即可使多线程并发运行。另外,信号量不仅允许使用者申请和释放资源,而且还允许使用者创造资源,这就赋予了信号量实现同步的功能。可见信号量的功能要比互斥量丰富许多。
信号量的创建/销毁:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_destroy(sem_t *sem);
- sem:信号量
- pshared:0:线程同步 1:进程同步
- value:信号量初始值
- 返回值:成功返回0,错误返回错误码
信号量的访问/释放:
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
wait会对信号量减1,如果信号量大于0,就直接减1。如果信号量等于0,就会等待。
post会对信号量加1。
7.1.3 条件量
7.1.3.1 创建条件变量
Pthreads 用 pthread_cond_t 类型的变量来表示条件变量。程序必须在使用 pthread_cond_t变量之前对其进行初始化。
(1) 静态初始化
对于静态分配的变量可以简单地将 PTHREAD_COND_INITIALIZER 赋值给变量来初始化默认行为的条件变量。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
(2)动态初始化
对动态分配或者不使用默认属性的条件变量来说可以使用 pthread _cond_init()来初始化。函数原型如下:
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
参数 cond 是一个指向需要初始化 pthread_cond_t 变量的指针,参数 attr 传递 NULL 值时, pthread_cond_init()将 cond 初始化为默认属性的条件变量。
函数成功将返回 0;否则返回一个非 0 的错误码。
静态初始化程序通常比调用 pthread_cond_init()更有效,而且在任何线程开始执行之前,确保变量被执行一次。
以下代码示例了条件变量的初始化。
pthread_cond_t cond;
int error;
if (error = pthread_cond_init(&cond, NULL));
fprintf(stderr, "Failed to initialize cond : %s\n", strerror(error));
7.1.3.2 销毁条件变量
函数 pthread_cond_destroy()用来销毁它参数所指出的条件变量,函数原型如下:
int pthread_cond_destroy(pthread_cond_t *cond);
函数成功调用返回 0,否则返回一个非 0 的错误码。以下代码演示了如何销毁一个条件变量。
pthread_cond_t cond;
int error;
if (error = pthread_cond_destroy(&cond))
fprintf(stderr, "Failed to destroy cond : %s\n", strerror(error));
7.1.3.3 等待与通知
- 等待
条件变量是与条件测试一起使用的,通常线程会对一个条件进行测试,如果条件不满足就会调用条件等待函数来等待条件满足。
条件等待函数有 pthread_cond_wait()pthread_cond_timedwait()和两个,函数原型如下:
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
pthread_cond_wait()函数在条件不满足时将一直等待, 而 pthread_cond_timedwait()将只等待一段时间。
参数 cond 是一个指向条件变量的指针,参数 mutex 是一个指向互斥量的指针,线程在调用前应该拥有这个互斥量,当线程要加入条件变量的等待队列时,等待操作会使线程释放这个互斥量。 pthread_timedwait()的第三个参数 abstime 是一个指向返回时间的指针,如果条件变量通知信号没有在此等待时间
之前出现,等待将超时退出, abstime 是个绝对时间,而不是时间间隔。
以上函数成功调用返回 0,否则返回非 0 的错误码,其中 pthread_cond_timedwait() 函数如果 abstime 指定的时间到期,错误码为 ETIMEOUT。
以下代码使得线程进入等待,直到收到通知并且满足 a 大于等于 b 的条件。
pthread_mutex_lock(&mutex)
while(a < b)
pthread_cond_wait(&cond, &mutex)
pthread_mutex_unlock(&mutex)
- 通知
当另一个线程修改了某参数可能使得条件变量所关联的条件变成真时,它应该通知一个或者多个等待在条件变量等待队列中的线程。
条件通知函数有 pthread_cond_signal()和 pthread_cond_broadcast()函数,其中 pthread_cond_signal 函数可以唤醒一个在条件变量等待队列等待的线程,而 pthread_cond_broadcast函数可以所有在条件变量等待队列等待的线程。函数原型如下:
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
参数 cond 是一个指向条件变量的指针。函数成功返回 0,否则返回一个非 0 的错误码。