LINUX设备驱动程序笔记

一、设备驱动程序简介

1.内核功能划分
进程管理,内存管理,文件系统,设备控制,网络。
设备的分类:字符设备,块设备,网络接口。

二、构造和运行模块

单个源文件编译模块:obj-m := hello.o
多个源文件编译模块:obj-m := module.o
module-objs := file1.o file2.o
make -C ~/kernel-2.6 M=‘pwd’ modules
首先进入到kernel目录执行make,(即执行内核顶层的Makefile文件)。M=选项让该Makefile在构造modules目标之前返回到模块源码目录。然后,modules目标指向obj-m变量中设定的模块。即上面的hello.o或者module.o。

三、字符设备驱动程序

1.主设备号和次设备号
对字符设备的访问是通过文件系统内的设备名称进行的。主设备号标识设备对应的驱动,但多个驱动有可能共享主设备号。次设备号确定设备文件所代表的设备。
2.设备编号的表达
设备编号用dev_t类型表示,要提取主次设备编号,应使用:
MAJOR(dev_t dev);
MINOR(dev_t dev);
主次设备号转换为dev_t,使用:MKDEV(int major, int minor);
3. 分配和释放设备编号
静态分配设备编号必须先知道系统中哪些主设备号是未被使用的,优点是可以提前创建设备节点,以供用户态访问。缺点是移植性差。
动态分配设备编号缺点是无法预先创建设备节点,不过只要分配成功之后,就可以从/proc/devices中读取到主设备号。
分配以及获取主设备号代码如下:
if (scull_major){
dev = MKDEV(scull_major, sucll_minor);
result = register_chrdev_region(dev, scull_nr_dev, “scull”); //静态分配
}
else{
result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, “scull”); //动态分配
scull_major = MAJOR(dev);
}
其中,scull是和该编号范围关联的设备名称,会出现在/proc/devices 和 sysfs中。
创建设备节点示例如下:
mknod /dev/scull0 c major 0
major代表主设备号,0是次设备号。
设备编号的释放如下:
void unregister_chrdev_region(dev_t first, unsigned int count);
4.一些重要的数据结构
file 结构
struct file是一个内核结构,它代表一个打开的文件,由内核在open的时候创建,并且传递给在该文件操作上的所有函数。直到最后一个close调用,即关闭对应打开的最后一个文件,内核会释放此数据结构。
文件操作(file_operations结构)
每个打开的文件和一组函数关联。这些操作主要用来实现系统调用,命名为open、read等等。这个结构中的每一个字段都必须指向驱动程序中实现的特定操作的函数,对于不支持的操作,对应的字段可设置为NULL。
inode结构
内核用inode结构在内部表示文件,和file结构的区别是file表示打开的文件描述符,同一个文件可以被打开多次,就有多个打开的文件描述符。但inode只有一个,多个file指向同一个inode。
dev_t i_rdev; 表示设备文件的设备编号。
struct cdev *i_cdev; 指向字符设备的内核结构 struct cdev的指针。
字符设备注册
静态方式:
struct cdev my_cdev;
cdev_init(&my_cdev, &fops);
my_cdev.owner = THIS_MODULE;
动态内存定义初始化:
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &fops;
my_cdev->owner = THIS_MODULE;
初始化 cdev 后,需要把它添加到系统中去。为此可以调用 cdev_add() 函数。传入 cdev 结构的指针,起始设备编号,以及设备编号范围。
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
p->dev = dev;
p->count = count;
return kobj_map(cdev_map, dev, count, NULL, exact_match, exact_lock, p);
}

四、并发和竞态

1.并发及其管理
并发来源:
(1)正在运行的多个用户空间进程可能会造成内核驱动程序的并发。
(2)SMP系统可在不同处理器上同时执行驱动程序代码。
(3)内核是可抢占的,驱动的程序代码可能在任何时候丢失对处理器的独占。
(4)异步中断事件,也会导致代码的并发执行。
(5)内核的可延迟代码执行机制,比如工作队列,tasklet以及timer等,随时可以打断当前进程。
对共享资源的并发访问产生竞态。确保一次只有一个执行线程可操作共享资源。使用锁定机制。
信号量
信号量通常被用作互斥模式。
静态初始化一个互斥体:
DECLARE_MUTEX(name); //一个称为name的信号量被初始化为1
DECLARE_MUTEX_LOCKED(name); //一个称为name的信号量被初始化为0
动态分配互斥体:
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
P函数,减小信号量的值。
void down(struct semaphore *sem); //减小信号量的值,并在必要时一直等待。
int down_interruptible(struct semaphore *sem); //同down的操作,但可以被中断。
int down_trylock(struct semaphore *sem); //不会休眠,若信号量在调用时不可获得,立即返回一个非零值。
V函数,增加信号量的值。
void up(struct semaphore *sem); //调用之后,不再拥有信号量。
自旋锁
自旋锁和信号量的不同,可在不能休眠的代码中使用,比如中断处理例程。
静态初始化:
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
动态初始化:
void spin_lock_init(spinlock_t *lock);
在进入到临界区之前,获取需要的锁:
void spin_lock(spinlock_t *lock); //自旋锁本质不可中断。若调用了spin_lock,在获得锁之前将一直处于自旋状态。
释放已经获取的锁:
void spin_unlock(spinlock_t *lock);
如果一个获得了自旋锁的驱动程序,在临界区开始工作。这个时候驱动程序进入休眠,或者发生了内核抢占,这样我们的代码将拥有自旋锁。如果被调度的线程刚好也要获得相同的锁,或者抢占内核的线程也要获得相同的锁,这时候该线程就会等待(在处理上自旋)很长时间直到处于临界区的驱动程序重新被调度完成释放锁的操作。
休眠是驱动程序主动让出处理器,抢占和中断会是被动让出处理器,但只要让出处理器就有可能处理器会做很长时间无用功。最坏的情况是运行在相同处理器上一个要获得相同锁的中断处理例程,会一直自旋,处于临界区的驱动程序不会被再次调度,这样就会造成死锁。
所以,拥有自旋锁的代码必须是原子的,不能休眠。并且自旋锁的实现本身是禁止抢占的。为了解决驱动程序在拥有锁的时候被本处理器上的中断处理例程中断去获得相同的自旋锁而造成死锁的情况,内核提供了几个禁止中断的自旋锁:
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_lock_irq(spinlock_t *lock);
void spin_lock_bh(spinlock_t *lock);
以上都是仅在本地处理器上禁止中断,irqsave会将先前的中断状态保存在flags中。bh是禁止软中断。
拥有自旋锁的时间越短越好。因为长时间的拥有,其他处理器就必须自旋等待更长的时间。对当前处理器而言,会阻止调度,对于真正高优先级的进程它不得不等待。


版权声明:本文为weixin_44285803原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。