首页 > 技术文章 > 设备驱动

yuankaituo 2015-04-08 16:42 原文

字符设备

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

块设备

在大部分的 Unix 系统, 块设备不能按字节处理数
,只能一次传送一个或多个长度是512字节( 或
一个更大的 2 次幂的数 )的整块数据
而Linux则允许块设备传送任意数目的字节。因
此, 块和字符设备的区别仅仅是驱动的与内核的接
口不同

 

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

 

驱动程序的安装

1.直接编译进内核。

2.模块方式。

 

驱动程序的使用:




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

 

v设备号
v创建设备文件
v设备注册
v重要数据结构
v设备操作

 

主次设备号

字符设备(键盘,显示器等)通过字符设备文件(c)来存取。字符设
备文件由使用 ls -l 的输出的第一列的“c”标
识。如果使用 ls -l 命令, 会看到在设备文件
项中有 2 个数(由一个逗号分隔) 这些数字
就是设备文件的主次设备编号。(举例察
看/dev)

crw--w---- 1 root tty 4, 0 4月 8 14:57 tty0

设备号的作用:

字符设备文件与字符设备驱动之间的联系。

主设备号用来标识设备文件与设备文件相连的驱动
程序。次编号被设备驱动程序用来辨别操作的
是哪个设备文件。

主设备号用来反映设备类型

次设备号用来区分同类型的设备

 

 内核中如何描述设备号?
dev_t
**其实质为unsigned int 32位整数,其中高12位为主
设备号,低20位为次设备号。
 如何从dev_t中分解出主设备号?
MAJOR(dev_t dev)
 如何从dev_t中分解出次设备号?
 MINOR(dev_t dev)

 

分配主设备号
Linux内核如何给设备分配主设备号?
可以采用静态申请,动态分配两种方法。

 

静态申请
方法:
1、根据Documentation/devices.txt,确定一个没有
使用的主设备号
2、使用 register_chrdev_region 函数注册设备号
 优点:
简单
缺点:
一旦驱动被广泛使用, 这个随机选定的主设备号可能
会导致设备号冲突,而使驱动程序无法注册

 

int register_chrdev_region(dev_t from, unsigned
count, const char *name)
功能:
申请使用从 from 开始的 count 个设备号(主设备号
不变,次设备号增加)
参数:
from:希望申请使用的设备号
count:希望申请使用设备号数目
name:设备名(体现在/proc/devices)

 

动态分配

方法:
使用 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: 设备文件类型
major: 主设备号
minor: 次设备号
例: mknod serial0 c 100 0

 

自动创建

 

重要结构

在Linux字符设备驱动程序设计
中,有3种非常重要的数据结构:
Struct file
Struct inode
Struct file_operations

 

Struct File

代表一个打开的文件。系统中每个打开的文件
在内核空间都有一个关联的 struct file。它由
内核在打开文件时创建, 在文件关闭后释放。
重要成员:
loff_t f_pos /*文件读写位置*/
struct file_operations *f_op

 

Struct Inode

用来记录文件的物理上的信息。因此, 它和代
表打开文件的file结构是不同的。一个文件可
以对应多个file结构, 但只有一个inode 结构。
v重要成员:
dev_t i_rdev:设备号

 

Struct file_operations

一个函数指针的集合,定义能在设备
上进行的操作。结构中的成员指向驱
动中的函数, 这些函数实现一个特别的
操作, 对于不支持的操作保留为 NULL。

 

例:mem_fops
struct file_operations mem_fops = {
.owner = THIS_MODULE,
.llseek = mem_seek,
.read = mem_read,
.write = mem_write,
.ioctl = mem_ioctl,
.open = mem_open,
.release = mem_release,
};

 

应用-驱动模型

内核代码导读
应用程序如何访问驱动程序?
(Read_write.c )

 

设备注册

在linux 2.6内核中,字符设备使用 struct
cdev 来描述。
字符设备的注册可分为如下3个步骤:
1. 分配cdev
2. 初始化cdev
3. 添加cdev

 

设备注册 (分配)

Struct cdev的分配可使用cdev_alloc函数
来完成。
struct 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)
参数:
p: 待添加到内核的字符设备结构
dev: 设备号
count: 添加的设备个数

完成了驱动程序的
注册,下一步该做什么呢??
实现设备所支持的操作

 int (*open)(struct inode *, struct file *)
在设备文件上的第一个操作,并不要求驱动程序
一定要实现这个方法。如果该项为NULL,设备
的打开操作永远成功。
void (*release)(struct inode *, struct file *)
当设备文件被关闭时调用这个操作。与open相
仿,release也可以没有。

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *)
从设备中读取数据。
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *)
向设备发送数据。
unsigned int (*poll) (struct file *, struct poll_table_struct *)
对应select系统调用
 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long)
控制设备


int (*mmap) (struct file *, struct vm_area_struct *)
将设备映射到进程虚拟地址空间中。
off_t (*llseek) (struct file *, loff_t, int)
修改文件的当前读写位置,并将新位置作
为返回值。

Open方法是驱动程序用来为以后的操作完
成初始化准备工作的。在大部分驱动程序
中,open完成如下工作:
初始化设备。
标明次设备号。

Release方法的作用正好与open相反。
这个设备方法有时也称为close,它应
该:
关闭设备。

读和写方法都完成类似的工作:从设备中读取数据到用户空间;将数据传递
给驱动程序。它们的原型也相当相似:
ssize_t xxx_read(struct file * filp, char __user * buff, size_t count, loff_t *
offp);
ssize_t xxx_write(struct file *filp, char __user * buff, size_t count, loff_t
*offp);
对于 2 个方法, filp是文件指针, count是请求传输的数据量。buff 参数指向
数据缓存。最后, offp 指出文件当前的访问位置。

Read 和 Write 方法的 buff 参数是用户空间指
针。因此, 它不能被内核代码直接引用,理由
如下:
用户空间指针在内核空间时可能根本是无效
的---没有那个地址的映射。

内核提供了专门的函数用于访问用户空间
的指针,例如:
v int copy_from_user(void *to, const void __user *from, int n)
v int copy_to_user(void __user *to, const void *from, int n)

设备注消

字符设备的注销使用cdev_del函数来完成。
int cdev_del(struct cdev *p)
参数:
p: 要注销的字符设备结构

字符设备驱动程序
memdev.c

 

虚拟文件系统VFS

Linux系统的文件并不局限于普通的磁盘文件,
I/O设备、套接字都属于文件范畴,具体分类有:
• 普通文件
• 目录文件
• 链接文件
• 设备文件
• Socket文件
• 管道文件

文件系统是对存储设备上的文件进行存储和组织
的机制,Linux支持多种文件系统,可以分为:
• 磁盘文件系统,如ext2
• Flash文件系统,如jffs2,yaffs2
• 网络文件系统,如NFS
• 特殊文件系统,如/sys


VFS(虚拟文件系统)隐藏各种文件系统的具体
细节,为文件操作提供统一的接口。


   进程每打开一个文件,就会有一个file结构与之对应。
同一个进程可以多次打开同一个文件而得到多个不
同的file结构,file结构描述了被打开文件的属性,
读写的偏移指针等信息。
两个不同的file结构可以对应同一个dentry结构。进
程多次打开同一个文件时,对应的只有一个dentry
结构。Dentry结构存储目录项和对应文件(inode)
的信息。

 在存储介质中,每个文件对应唯一的inode结点,但
是,每个文件又可以有多个文件名(通过ln 命令建
立文件链接),即可以通过不同的文件名访问同一
个文件。这里多个文件名对应一个文件的关系在数
据结构中表示就是dentry和inode的关系。
 Inode中不存储文件的名字,它只存储节点号;而
dentry则保存有名字和与其对应的节点号,所以就
可以通过不同的dentry访问同一个inode。

应用程序通过VFS访问设备文件

读文件(vfs_read)
打开文件(do_sys_open)

 

调试技术分类

对于驱动程序设计来说,核心问题之一就
是如何完成调试。当前常用的驱动调试技
术可分为:
• 打印调试
• 调试器调试
• 查询调试

• 打印调试
在调试应用程序时,最常用的调试技
术是打印,就是在应用程序中合适的
点调用printf。当调试内核代码的时
候,可以用printk完成类似任务。

在驱动开发时,printk 非常有助于调试。但当正
式发行驱动程序时, 应当去掉这些打印语句。但
你有可能很快又发现,你又需要在驱动程序中实
现一个新功能(或者修复一个bug),这时你又要
用到那些被删除的打印语句。这里介绍一种使用
printk 的合理方法,可以全局地打开或关闭它
们,
而不是简单地删除。


合理使用Printk
#ifdef PDEBUG
#define PLOG(fmt,args...) printk(KERN_DEBUG
"scull:"fmt,##args)
#else
#define PLOG(fmt,args...)
/*do nothing */
#endif

 


合理使用Printk
Makefile作如下修改:
DEBUG =y
ifeq ($(DEBUG),y)
DEBFLAGS =-O2 -g -DPDEBUG
else
DEBFLAGS =-O2
endif
CFLAGS +=$(DEBFLAGS)

 

并发与竞态

并发:多个执行单元同时被执行。
竞态:并发的执行单元对共享资源
(硬件资源和软件上的全局变量等)
的访问导致的竞争状态

例:
if (copy_from_user(&(dev->data[pos]), buf, count))
ret = -EFAULT;
goto out;
假设有 2 个进程试图同时向一个设备的相
同位置写入数据,就会造成数据混乱。

处理并发的常用技术是加锁或者互
斥,即确保在任何时间只有一个执行
单元可以操作共享资源。在Linux内核
中主要通过semaphore机制和
spin_lock机制实现。

Linux内核的信号量在概念和原理上与用户态的
信号量是一样的,但是它不能在内核之外使用,
它是一种睡眠锁。如果有一个任务想要获得已经
被占用的信号量时,信号量会将这个进程放入一
个等待队列,然后让其睡眠。当持有信号量的进
程将信号释放后,处于等待队列中的任务将被唤
醒,并让其获得信号量。

信号量在创建时需要设置一个初始值,表示允许
有几个任务同时访问该信号量保护的共享资源,
初始值为1就变成互斥锁(Mutex),即同时只能
有一个任务可以访问信号量保护的共享资源。
当任务访问完被信号量保护的共享资源后,必须
释放信号量,释放信号量通过把信号量的值加1实
现,如果释放后信号量的值为非正数,表明有任
务等待当前信号量,因此要唤醒等待该信号量的
任务。

信号量的实现也是与体系结构相关的,定义在
<asm/semaphore.h>中,struct semaphore类型用
来表示信号量。
1. 定义信号量
struct semaphore sem;
2. 初始化信号量
void sema_init (struct semaphore *sem, int val)
该函用于数初始化设置信号量的初值,它设置信号量
sem的值为val。

v void init_MUTEX (struct semaphore *sem)
该函数用于初始化一个互斥锁,即它把信号量
sem的值设置为1。
v void init_MUTEX_LOCKED (struct semaphore *sem)
该函数也用于初始化一个互斥锁,但它把信号量
sem的值设置为0,即一开始就处在已锁状态。

定义与初始化的工作可由如下宏一步完成:
vDECLARE_MUTEX(name)
定义一个信号量name,并初始化它的值为1。
vDECLARE_MUTEX_LOCKED(name)
定义一个信号量name,但把它的初始值设置为0
,即锁在创建时就处在已锁状态。

3. 获取信号量
vvoid down(struct semaphore * sem)
获取信号量sem,可能会导致进程睡眠,因此不
能在中断上下文使用该函数。该函数将把sem的
值减1,如果信号量sem的值非负,就直接返回,
否则调用者将被挂起,直到别的任务释放该信号
量才能继续运行。

v int down_interruptible(struct semaphore * sem)
获取信号量sem。如果信号量不可用,进程将被
置为TASK_INTERRUPTIBLE类型的睡眠状态。
该函数由返回值来区分是正常返回还是被信号中
断返回,如果返回0,表示获得信号量正常返回,
如果被信号打断,返回-EINTR。

v 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)
该宏用于初始化自旋锁x,自旋锁在使用前必
须先初始化。
spin_lock(lock)
获取自旋锁lock,如果成功,立即获得锁,并
马上返回,否则它将一直自旋在那里,直到该
自旋锁的保持者释放。

spin_trylock(lock)
试图获取自旋锁lock,如果能立即获得锁,
并返回真,否则立即返回假。它不会一直等
待被释放。
spin_unlock(lock)
释放自旋锁lock,它与spin_trylock或
spin_lock配对使用。

信号量PK自旋锁

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

 

推荐阅读