1、设备号的作用
主设备号用来标识与设备文件相连的驱动程序。次设备号被驱动用来辩别操作的是那个设备
主设备号用来反映设备类型
次设备号用来区分通类型的设备
2、内核中如何描述设备号?
dev_t //其实质为unsigned int 32为整数,其中高12位为主设备号,低20位为次设备号
如何从dev_t中分解出主设备号?
MAJOR(dev_t dev)
如何从dev_t中分解出设备号?
MINOR(dev_t dev)
linux内核如何给设备分配主设备号?
可以采用静态申请,动态申请两种方法
3、静态申请
方法:1、根据Documentation/devices.txt,确定一个没有使用的主设备号
2、使用register_chrdev_region函数注册设备号
优点:简单
缺点:一旦驱动被广泛使用,这个随机选定的设备号可能会导致设备号冲突,
而使驱动程序无法注册
int register_chrdev_region(dev_t from, unsigned count, const char *name)
功能:
注册从from开始的count个设备号(主设备号不变,次设备号增加,如果次设备号益处,主设备号加1)
参数:
from: 要注册的第一个设备号
count:要注册的设备号个数
name: 设备名(体现在/proc/devices)
4、动态分配
方法: 使用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)
5、注销设备号
不论使用何种方法分配设备号,都应该在不再使用它们的时候释放这些设备号。
void unregister_chrdev_region(dev_t from, unsigned count)
功能:释放从from开始的count个设备号
6、创建设备文件
6.1、手动创建:
使用mknod: mknod filename type major monor
filename: 设备文件名
type: 设备文件类型
major: 主设备号
minor: 次设备号
例:mknod serial0 c 100 0
6.2、动态创建:
7、重要结构
在Linux字符设备驱动程序设计中,有3种非常重要的数据结构:
struct file struct inode struct file_operations
7.1、struct file
代表一个打开的文件。系统中每个打开的文件在内核空间都有一个关联的
struct file。它由内核再打开文件时创建,在文件关闭后释放。
重要成员:
loff_t f_pos //文件读写位置
struct file_operations *f_op
7.2、struct inode
用来记录文件的物理上的信息。因此,它和代表打开文件的file结构是不同的。
一个文件可以对应多个file结构,但只有一个inode结构。
重要成员:
dev_t i_rdev:设备号
7.3、struct file_operations
一个函数指针的集合,定义能在设备上进行的操作。结构中的成员指向驱动中的函数,
这些函数实现一个特别的操作,对于不支持的操作保留为NULL
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,
};
8、设备注册
在Linux2.6内核中,字符设备使用struct cdev来描述。
字符设备的注册可分为如下3个步骤:
1、分配cdev
2、初始化cdev
3、添加cdev
8.1、struct cdev的初始化使用cdev_init函数来完成。
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数:
cdev:初始化的cdev结构
fops:设备对应的操作函数集
8.2、struct cdev的注册使用cdev_add函数来完成。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数:
p:待添加到内核的字符设备结构
dev:设备号
count:添加的设备个数
9、设备操作
int (*open)(struct inode *, struct file *)
在设备文件上的第一个操作,并不要求驱动程序一定要
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)
控制设备
10、读和写
读和写方法都完成类似的工作:从设备中读取数据到用户空间;将数据传递给驱动程序。
它们的原型也相当相似:
ssize_t xxx_read(struct file *file, char __user *buff, size_t count, loff_t *offp);
ssize_t xxx_read(struct file *file, char __user *buff, size_t count, loff_t *offp);
对于2个方法,file是文件指针,count是请求传输的数据量。buff参数指向数据缓存。
最后,offp指出文件当前的访问位置。
10.2、read和write方法的buff参数是用户空间指针。因此,它不能被内核代码直接引用,理由如下:
用户空间指针在内核空间时可能根本是无效的---没有那个地址的映射。
内核提供了专门的函数用于访问用户空间的指针
int copy_from_user(void *to, const void __user *from, int n)
int copy_to_suser(void __user *to, const void *from, int n)
11、设备注销
字符设备的注销使用cdev_del函数来完成。
int cdev_del(struct cdev *p)
参数:p要注销的字符设备结构
12、并发与竞态
并发:多个执行单元同时被执行。
竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量等)的访问导致的竞争状态。
处理并发的常用技术是加锁或者互斥,即确保在任何时间只有一个执行单元可以操作
共享资源。在Linux内核中主要通过semaphore机制和spin_lock机制实现。
12.1、信号量
信号量的实现也是与体系结构相关的,定义在<asm/semaphore.h>中,
struct semaphore 类型用来表示信号量。
定义信号量
struct semaphore sem;
初始化信号量
void sema_init(struct semaphore *sem, int val)
该函数用于初始化设置信号量的初值,它设置信号量sem的值为val。
void init_MUTEX(struct semaphore *sem)
该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1。
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的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,
直到别的任务释放该信号量才能继续运行。
void down_interruptible(struct semaphore *sem)
获取信号量sem。如果信号量不可用,进程将被置为TASK_INTERRUPTIBLE类型的睡眠装填。
该函数由返回值来区分是正常返回还是被信号中断返回,如果返回0,表示获得信号量正常返回,
如果被信号打断,返回-EINTER。
void down_killable(struct semaphore *sem)
获取信号量sem。如果信号量不可用,进程将被设置为TASK_LILLABLE类型的睡眠状态。
注:down()函数现已不建议继续使用。建议使用down_killable()或down_interruptible()
void up(struct semaphore *sem)
该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,
因此唤醒这些等待者。
13、自旋锁
自旋锁最多只能被一个可执行单元持有。自旋锁不会引起调用者睡眠,如果一个执行线程试
图获得一个已经被只有的自旋锁,那么线程就会一直进行忙循环,一直等待下去,在那里看看
是否该自旋锁的保持者已经释放了锁,“自旋”就是这个意思。
信号量是一种睡眠锁,自旋锁是忙等。
spin_lock_init(x)
试图获取自旋锁lock,如果能立即获得锁,并返回真,否则立即返回假。它不会一直等待被释放。
spin_unlock(lock)
释放自旋锁lock,它与spin_trylock或者spin_lock配对使用。
14、信号量PK自旋锁
信号量可能允许多个持有者,而自旋锁在任何时候只能允许一个持有者。当然也有信号量叫互斥
信号量(只能一个持有者),允许有多个持有者的信号量叫计数信号量。
信号量适合于保持时间较长的情况;而自旋锁适合于保持时间非常短的情况,在实际应用中自旋
锁控制的代码只有几行,而持有自旋锁的时间也一般不会超过两次上下文切换的时间,因为线程
一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我
们就应该选择信号量。
Linux字符设备驱动学习
最新推荐文章于 2025-04-06 22:47:45 发布