Linux字符设备驱动程序

时间:2023-01-03 22:44:54

驱动程序介绍

Linux驱动程序学习:

知识结构:(1)Linux驱动程序设计模式(40%)

  (2)内核相关知识(30%)

  (3)硬件相关知识(30%)

学习方法:理论->实验(疑问)->理论->实验.......



驱动程序:使硬件工作的软件。

Linux字符设备驱动程序

Linux字符设备驱动程序


驱动分类:(1)字符设备驱动;

  (2)网络接口驱动;

  (3)块设备驱动;


字符设备:字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现open,close,read和write的系统调用。


块设备:在大部分的Unix系统,块设备不能按字节处理数据,只能一次传送一个或多个长度是512字节(或一个更大的2次幂的数)的整块数据。   而Linux则允许块设备传送任意数目的字节。因此,块和字符设备的区别仅仅是驱动的与内核的接口不同。   块设备和字符设备还有一点区别是,在于访问顺序上面不同,一个是可以随机访问,一个不能随机访问,块设备可以随机存取,而字符设备不能随机存取。


网络接口:任何网络事务都通过一个接口来进行,一个接口通常是一个硬件设备(eth0),但是它也可以是一个纯粹的软件设备,比如回环接口。一个网络接口负责发送和接收数据报文。


驱动程序安装:(1)模块方式(已知);(2)直接编译进内核:Kconfig(修改配置菜单)Makefile?,例:将helloworld编译进内核

Linux字符设备驱动程序把文件拷贝到内核中去,

修改Kconfig,Linux字符设备驱动程序Linux字符设备驱动程序


之后,make menuconfig ARCH=armLinux字符设备驱动程序


下一步修改Makefile:代码放在哪个目录下修改哪个目录下的Makefile,vi drivers/char/MakefileLinux字符设备驱动程序


下一步编译内核:make uImage ARCH=arm CROSS_COMPILE=arm-linux-


驱动程序使用,Linux用户如何使用驱动程序?

图4


驱动程序使用:

A:Linux用户程序通过设备文件(又名:设备节点)来使用驱动程序操作字符设备和块设备。

Q:设备(字符,块)文件在何处?放在dev目录下。

Linux字符设备驱动程序




字符设备驱动程序

知识点:设备号,创建设备文件,设备注册,重要数据结构,设备操作。
主次设备号:字符设备通过字符设备文件来读取,字符设备文件由使用ls -l的输出的第一列的‘c’标识。如果使用ls -l命令,会看到在设备文件项中有两个数(由一个逗号分隔),这些数字就是设备文件的主次设备编号(举例查看/dev)。 Linux字符设备驱动程序,字符设备文件和字符设备驱动通过主设备号建立联系 主设备号的作用:(1)主设备号用来标识与设备文件相连的驱动程序;(2)次设备号被驱动程序用来辨别操作的是哪个设备。 ********主设备号用来反映设备类型************** ********次设备号用来区分同类型的设备********
Q:内核中如何描述设备号? A:dev_t,其实质为unsigned int 32位整数,其中高12位为主设备号,低20位为次设备号。
Q:如何从dev_t中分解出主设备号? A:MAJOR(dev_t dev)
Q:如何从dev_t中分解出次设备号? A:MINOR(dev_t dev)
静态申请: 方法:(1)根据Documentation.txt,确定一个没有使用的主设备号;(2)使用register_chrdev_region函数注     册设备号。 优点:简单; 缺点:一旦驱动被广泛使用,这个随机选定的主设备号可能会导致设备号冲突,而使驱动程序无法注册。
动态分配 方法:使用alloc_chrdev_region分配设备号 优点:简单易于驱动推广 缺点:无法再安装驱动前创建设备文件(因为安装前还没有分配到主设备号) 解决办法:安装驱动后,从/proc/devices中查询设备号  int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name) 功能:请求内核动态分配count个设备号,且次设备号从baseminor开始。 参数:dev,分配到的设备号    baseminor,起始次设备号    count,需要分配的设备号数目    name,设备名(体现在/proc/devices)
注销设备号:无论使用何种设备号,都应该在不再使用它们时释放这些设备号。 void unregister_chrdev_region(dev_t from,unsigned count) 功能:释放从from开始的count个设备号

创建设备文件,2种方法:(1)使用mknod命令手工创建;(2)自动创建;
手工创建: mknod用法:mknod filename type major minor filename:设备文件名 type:设备文件类型,c和b major:主设备号 minor:次设备号 例:mknod serial0 c 100 0
自动创建:后面介绍


重要结构:在Linux字符设备驱动程序设计中,有三种非常重要的数据结构:struct file   struct inode   struct file_operations
struct file,代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,在文件关闭后释放。 重要成员:loff_t f_pos   /*文件读写位置*/   struct file_operation *f_op
Struct Inode:用来记录文件的物理上的信息。因此,它和代表打开文件的file结构是不同的。一个文件可以对应多个file结构,但只有一个Inode结构 重要成员:dev_t i_rdev:设备号 
struct file_operations:一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数,这些函数实现一个特别的操作,对于不支持的操作保留为NULL。 Linux字符设备驱动程序


把应用程序对设备的操作转化为驱动程序中相应的函数
应用驱动模型,内核代码导读,应用程序如何访问驱动程序(Read_write.c)

设备注册:在Linux2.6内核中,字符设备使用struct cdev来描述。 字符设备的注册可分为如下3个步骤: 1.分配cdev 2.初始化cdev 3.添加cdev
分配:struct cdev的分配可使用cdev_alloc函数来完成。    struc cdev *cdev_alloc(void)
初始化:struct cdev的初始化使用cdev_init函数来完成。 void cdev_init(struct cdev *cdev,const struct file_operations *fops) 参数: cdev:待初始化的cdev结构 fops:设备对应的操作函数集
添加:struct cdev的注册表使用cdev_add函数来完成。 int cdev_add(struct cdev *p,dev_t dev,unsigned count) 参数:待添加到内核的字符设备结构 dev:设备号 count:添加设备的个数

设备操作实现: 设备操作:Linux字符设备驱动程序
Linux字符设备驱动程序
Linux字符设备驱动程序
Linux字符设备驱动程序
Linux字符设备驱动程序
Linux字符设备驱动程序
Linux字符设备驱动程序



Linux字符设备驱动程序

Linux字符设备驱动程序

Linux字符设备驱动程序

Linux字符设备驱动程序

MKDEV是将主设备好和此设备组成设备号。

#define MKDEV(major,minor) (((major) << MINORBITS) | (minor))

Linux字符设备驱动程序


驱动调试技术

调试技术分类:对于驱动程序设计来说,核心问题之一就是如何完成调试。当前常用的驱动调试技术可分为(1)打印调试(2)调试器调试(3)查询调试;
打印调试:在调试应用程序时,最常用的调试技术是打印,就是在应用程序中合适的点调用printf。当调试内核代码的时候,可以用printk完成类似的调试任务。
合理的使用printk:在驱动开发时,printk非常有助于调试。但当正式发行程序时,应当去掉这些打印语句。但你有可能很快又发现,你又需要在驱动程序中实现一个新功能(或者修复一个bug),这时你又要用到哪些被删除的打印语句。这里介绍一种使用printk的合理方法,可以全局的打开或者关闭它们,而不是简单的删除。
#ifdef PDEBUG
#define PLOG(fmt,args...) printk(KERN_DEBUG"scull:"fmt,##args)
#else
#define PLOG(fmt,args...)   /*do nothing*/
#endif
Makefile作如下修改: DEBUG=y ifneq($(DEBUG),y) DEBFLAGS=-O2 -g -DPDEBUG else DEBFLAGS=-O2 endif CFLAGS +=$(DEBFLAGS)


并发与竞态
并发:多个执行单元同时被执行。 竞态:并发的执行单元对共享资源(硬件上的资源和软件上的全局变量等)的访问导致的竞争状态。
信号量:Linux内核的信号量在概念上和原理上与用户态的信号量是一样的,但是它不能再内核之外使用,它是一种睡眠锁。如果有一个任务想要获得已知被占有的信号量时,信号量会将这个进程放入一个等待队列,然后让其睡眠。当持有信号量的进程将信号释放之后,处于等待队列中的任务将被唤醒,并让其获得信号量。
信号量在创建时需要设置一个初始值,表示允许有几个任务同时访问该信号量保护的共享资源,初始值为1就变成互斥锁(Mutex),即同时只能有一个任务可以访问信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果释放后信号量的值为非正数,表明有任务等待当前信号量,因此要唤醒等待该信号量的任务。
信号量的实现也是与体系结构相关的,定义在<asm/semaphore.h>中,struct semaphore类型用来表示信号量。 (1)定义信号量:struct semaphore sem; (2)初始化信号量:void sema_init(struct semaphore *sem,int val),该函数用于初始化设置信号量的初值,它设置信号量sem的值为val。 (3)void init_MUTEX(struct semaphore *sem),该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。 (4)void init_MUTEX_LOCKED(struct semaphore *sem)该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,即一开始就处于已锁状态。
定义与初始化的工作可由如下宏一步完成: DECLARE_MUTEX(name)定义一个信号量name,并初始化它的值为1。 DECLARE_MUTEX_LOCKED(name),定义一个信号量name,但把它的初始值设置为0,即锁在创建时就处于已锁状态。
获取信号量:void down(struct semaphore *sem),获取信号量sem,可能会导致进程休眠,因此不能再中断上下文使用该函数。该函数将把sem的值减一,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续执行。
int down_interruptible(struct semaphore *sem),获取信号量sem。如果信号量不可用,进程将被置为TASK_INTERRUPTIBLE类型的睡眠状态。该函数由返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。
down_killable(struct semaphore *sem),获取信号量sem,如果信号量不可用,进程将被置为TASK_KILLABLE类型的睡眠状态。注:down()现已不建议使用。建议使用down_killable()或down_interruptible()函数。
4.释放信号量,void up(struct semaphore *sem)该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

自旋锁:自旋锁最多只能被一个可执行单元持有。自旋锁不会引起调用者睡眠,如果一个执行线程试图获得一个已经被持有的自旋锁,那么线程就会一直进行忙循环,一直等待下去,在那里看是否该自旋锁的保持者,已经释放了锁,“自旋”就是这个意思。 spin_lock_init(x),该宏用于初始化自旋锁,自旋锁在使用前必须先初始化。 spin_lock(lock),获取自旋锁lock,如果成功,立即获得锁,并马上返回,否则它将一直自旋在那里,直到该自旋锁的保持者释放。 spin_trylock(lock),试图获取自旋锁lock,如果能立即获得锁,并返回真,否则一直返回假。它不会一直等待被释放。 spin_unlock(lock),释放自旋锁lock,它与spin_trylock或spin_lock配对使用。

信号量可能允许有多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥信号量(只能一个持有者),允许有多个持有者的信号量叫互斥信号量。
信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换时间,因为线程一旦进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长与两次上下问切换,我们就应该选择信号量。