linux驱动学习笔记(1)——字符设备驱动开发

(一)驱动程序介绍

(a)Linux驱动程序学习

知识结构:

1. Linux驱动程序设计模式(40%)

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

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

(b)驱动分类:

①字符设备:

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

②块设备:

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

③网络接口:

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

(c)驱动程序使用:

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

(二)字符设备驱动程序

(a)设备号:

①主次设备号:

  字符设备通过字符设备文件来存取。字符设备文件由使用 ls -l 的输出的第一列的“c”标识。如果使用 ls -l 命令, 会看到在设备文件项中有 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)

④分配主设备号:

静态申请:

方法:

根据Documentation/devices.txt,确定一个没有使用的主设备号

使用 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个设备号

(b)创建设备文件

①使用mknod 命令手工创建
  mknod 用法:mknod filename type major minor

    • filename:设备文件名
    • type: 设备文件类型
    • major: 主设备号
    • minor: 次设备号

  例: mknod serial0 c 100 0

②动创建

(c)重要结构:

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

Struct File:

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

    • loff_t f_pos /*文件读写位置*/
    • struct file_operations *f_op

Struct Inode:

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

    • dev_t i_rdev:设备号

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,
}

(d)设备注册

在linux 2.6内核中,字符设备使用 struct cdev 来描述。字符设备的注册可分为如下3个步骤:

    • 分配cdev
    • 初始化cdev
    • 添加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方法是驱动程序用来为以后的操作完成初始化准备工作的。在大部分驱动程序中,open完成如下工作:

      • 初始化设备。
      • 标明次设备号。

Release方法:
  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 参数是用户空间指针。因此, 它不能被内核代码直接引用,理由如下:

用户空间指针在内核空间时可能根本是无效的---没有那个地址的映射

内核提供了专门的函数用于访问用户空间的指针,例如:

    • int copy_from_user(void *to, const void __user *from, int n)
    • int copy_to_user(void __user *to, const void *from, int n)

⑤设备注消

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

    • p: 要注销的字符设备结构

(三)字符驱动实例分析


(四)驱动调试技术

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

    1. 打印调试
    2. 调试器调试
    3. 查询调试

(五)并发控制

(a)并发与竞态

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

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。
      • 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,即锁在创建时就处在已锁状态。

3. 获取信号量

void down(struct semaphore * sem)

获取信号量sem,可能会导致进程睡眠,因此不能在中断上下文使用该函数。该函数将把sem的值减1,如果信号量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) 该宏用于初始化自旋锁x,自旋锁在使用前必须先初始化。
    • spin_lock(lock) 获取自旋锁lock,如果成功,立即获得锁,并马上返回,否则它将一直自旋在那里,直到该自旋锁的保持者释放。
    • spin_trylock(lock)试图获取自旋锁lock,如果能立即获得锁,并返回真,否则立即返回假。它不会一直等待被释放
    • spin_unlock(lock) 释放自旋锁lock,它与spin_trylock或spin_lock配对使用。

③信号量PK自旋锁

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

(六)高级字符设备驱动程序

(1)设备Ioctl控制

功能: 

  大部分驱动除了需要具备读写设备的能力外,还需要具备对硬件控制的能力。例如,要求设备报告错误信息,改变波特率,这些操作常常通过 ioctl方法来实现。

用户使用方法:

在用户空间,使用ioctl 系统调用来控制设备,原型如下:
int ioctl(int fd,unsigned long cmd,...)
原型中的点表示这是一个可选的参数,存在与否依赖于控制命令(第 2 个参数 )是否涉及到与设备的数据交互。

驱动ioctl方法:

ioctl 驱动方法有和用户空间版本不同的原型:
int (*ioctl)(struct inode *inode,struct file *filp,unsigned int cmd,unsigned long arg)
cmd 参数从用户空间传下来,可选的参数 arg 以一个
unsigned long 的形式传递,不管它是一个整数或一个指针。如果cmd命令不涉及数据传输,则第 3 个参数arg的值无任何意义。

Ioctl实现:

步骤:

(a)定义命令

  在编写ioctl代码之前,首先需要定义命令。为了防止对错误的设备使用正确的命令,命令号应该在系 统范围内是唯一的。ioctl 命令编码被划分为几个位段,include/asm/ioctl.h中定义了这些位字段:类型(幻数),序号,传送方向,参数的大小。Documentation/ioctl-number.txt文件中罗列了在内核中已经使用了的幻数。

定义 ioctl 命令的正确方法是使用 4 个位段, 这个列表中介绍的符号定义在<linux/ioctl.h>中:

    • Type 幻数(类型): 表明哪个设备的命令,在参考了 ioctlnumber.txt之后选出,8 位宽。
    • Number 序号,表明设备命令中的第几个,8 位宽。
    • Direction 数据传送的方向,可能的值是 _IOC_NONE(没有数据传输),_IOC_READ, _IOC_WRITE。 数据传送是从应用程序的观点来看待的,_IOC_READ 意思是从设备读。
    • Size 用户数据的大小。(13/14位宽,视处理器而定)

内核提供了下列宏来帮助定义命令:

    • _IO(type,nr)
    • 没有参数的命令
    • _IOR(type,nr,datatype)
    • 从驱动中读数据
    • _IOW(type,nr,datatype)
    • 写数据到驱动
    • _IOWR(type,nr,datatype)
    • 双向传送,type 和 number 成员作为参数被传递

定义命令(示例)

#define MEM_IOC_MAGIC ‘m’ //定义幻数

#define MEM_IOCSET
_IOW(MEM_IOC_MAGIC, 0, int)

#define MEM_IOCGQSET
_IOR(MEM_IOC_MAGIC, 1, int)

(b)实现命令

  定义好了命令,下一步就是要实现Ioctl函数了,Ioctl函数的实现包括如下3个技术环节:
返回值
  Ioctl函数的实现通常是根据命令执行的一个switch语句。但是,当命令号不能匹配任何一个设备所支持的命令时,通常返回-EINVAL(“非法参数”)
参数使用

如果是一个整数,可以直接使用。如果是指针,我们必须确保这个用户地址是有效的,因此使用前需进行正确的检查.
不需要检测:

    • copy_from_user
    • copy_to_user
    • get_user
    • put_user

需要检测:

    • __get_user
    • __put_user

int access_ok(int type, const void *addr, unsigned long size)
第一个参数是 VERIFY_READ 或者 VERIFY_WRITE,用来表明是读用户内存还是写用户内存。addr 参数是要操作的用户内存地址,size 是操作的长度。如果 ioctl 需要从用户空间读一个整数,那么size参数等于 sizeof(int)。
access_ok 返回一个布尔值: 1 是成功(存取没问题)和 0 是失败(存取有问题),如果该函数返回失败, 则Ioctl应当返回–EFAULT 。

if (_IOC_DIR(cmd) & _IOC_READ)
{
    err = !access_ok(VERIFY_WRITE, (void __user *)arg,_IOC_SIZE(cmd));
}    
else if (_IOC_DIR(cmd) & _IOC_WRITE)//why _IOC_READ 对应 VERIFY_WRITE 
{
    err = !access_ok(VERIFY_READ, (void __user *)arg,_IOC_SIZE(cmd));
}

if (err)
{
    return -EFAULT;
}

     命令操作

switch(cmd)
{
    case MEM_IOCSQUANTUM: /* Set: arg points to the value */
    retval = __get_user(scull_quantum, (int *)arg);
    break;

    case MEM_IOCGQUANTUM: /* Get: arg is pointer to result */
    retval = __put_user(scull_quantum, (int *)arg);
    break;

    default:
    return –EINVAL;
}

(2)内核等待队列

 等待队列

在Linux驱动程序设计中,可以使用等待队列来实现进程的阻塞,等待队列可看作保存进程的容器,在阻塞进程时,将进程放入等待队列,当唤醒进程时,从等待等列中取出进程。

Linux 2.6内核提供了如下关于等待队列的操作:
1、定义等待队列
wait_queue_head_t my_queue

2、初始化等待队列
init_waitqueue_head(&my_queue)

3、定义并初始化等待队列
DECLARE_WAIT_QUEUE_HEAD(my_queue)

4、有条件睡眠
wait_event(queue,condition)
当condition(一个布尔表达式)为真时,立即返回;否则让进程进入TASK_UNINTERRUPTIBLE模式的睡眠,并挂在queue参数所指定的等待队列上。

wait_event_interruptible(queue,condition)
当condition(一个布尔表达式)为真时,立即返回;否则让进程进入TASK_INTERRUPTIBLE的睡眠,并挂在queue参数所指定的等待队列上。

int wait_event_killable(wait_queue_t queue, condition)
当condition(一个布尔表达式)为真时,立即返回;否则让进程进入TASK_KILLABLE的睡眠,并挂在queue参数所指定的等待队列上。

5、无条件睡眠(老版本,建议不再使用)
sleep_on(wait_queue_head_t *q) 
让进程进入不可中断的睡眠,并把它放入等待队列q。

interruptible_sleep_on(wait_queue_head_t *q)
让进程进入可中断的睡眠,并把它放入等待队列q

6、从等待队列中唤醒进程
wake_up(wait_queue_t *q)
从等待队列q中唤醒状态为TASK_UNINTERRUPTIBLE,TASK_INTERRUPTIBLE,TASK_KILLABLE 的所有进程。

wake_up_interruptible(wait_queue_t *q)
从等待队列q中唤醒状态为TASK_INTERRUPTIBLE 的进程。

(3)阻塞型字符设备驱动

功能

前一节我们在设计简单字符驱动程序时,跳过了一个重要的问题:当一个设备无法立刻满足用户的读写请求时应当如何处理? 例如:调用read时没有数据可读, 但以后可能会有;或者一个进程试图向设备写入数据,但是设备暂时没有准备好接收数据。应用程序通常不关心这种问题,应用程序只是调用 read 或 write 并得到返回值。驱动程序应当(缺省地)阻塞进程,使它进入睡眠,直到请求可以得到满足。

阻塞方式

在阻塞型驱动程序中,Read实现方式如下:
如果进程调用read,但设备没有数据或数据不足,进程阻塞。当新数据到达后,唤醒被阻塞进程。

在阻塞型驱动程序中,Write实现方式如下:
如果进程调用了write,但设备没有足够的空间供其写入数据,进程阻塞。当设备中的数据被读走后,缓冲区中空出部分空间,则唤醒进程。

非阻塞方式

阻塞方式是文件读写操作的默认方式,但应用程序员可通过使用O_NONBLOCK标志来人为的设置读写操作为非阻塞方式(该标志定义在<linux/fcntl.h>中,在打开文件时指定)。

如果设置了O_NONBLOCK标志,read和write的行为是不同的。如果进程在没有数据就绪时调用了read,或者在缓冲区没有空间时调用了write,系统只是简单地返回-EAGAIN,而不会阻塞进程。

(4)Poll设备操作

(a)Select系统调用(功能)

Select系统调用用于多路监控,当没有一个文件满足要求时,select将阻塞调用进程。


int select(int maxfd, fd_set *readfds, fd_set *writefds, fe_set *exceptfds, const struct timeval *timeout)

参数:

    • Maxfd: 文件描述符的范围,比待检测的最大文件描述符大1
    • Readfds: 被读监控的文件描述符集
    • Writefds: 被写监控的文件描述符集
    • Exceptfds:被异常监控的文件描述符集;
    • Timeout: 定时器
    1. Timeout取不同的值,该调用有不同的表现:
    2. Timeout值为0,不管是否有文件满足要求,都立刻返回,无文件满足要求返回0,有文件满足要求返回一个正值。
    3. Timeout为NULL,select将阻塞进程,直到某个文件满足要求
    4. Timeout 值 为 正 整 数 , 就 是 等 待 的 最长时 间 , 即select在timeout时间内阻塞进程。

返回值:

Select调用返回时,返回值有如下情况:

    1. 正常情况下返回满足要求的文件描述符个数;
    2. 经过了timeout等待后仍无文件满足要求,返回值为0;
    3. 如果select被某个信号中断,它将返回-1并设置errno为EINTR。
    4. 如果出错,返回-1并设置相应的errno。

使用方法:

    1. 将要监控的文件添加到文件描述符集
    2. 调用Select开始监控
    3. 判断文件是否发生变化

系统提供了4个宏对描述符集进行操作:

      • #include <sys/select.h>
      • void FD_SET(int fd, fd_set *fdset)
      • void FD_CLR(int fd, fd_set *fdset)
      • void FD_ZERO(fd_set *fdset)
      • void FD_ISSET(int fd, fd_set *fdset)

宏FD_SET将文件描述符fd添加到文件描述符集fdset中;
宏FD_CLR从文件描述符集fdset中清除文件描述符fd;
宏FD_ZERO清空文件描述符集fdset;
在调用select后使用FD_ISSET来检测文件描述符集fdset中的文件fd发生了变化。

FD_ZERO(&fds); //清空集合
FD_SET(fd1,&fds); //设置描述符
FD_SET(fd2,&fds); //设置描述符
maxfdp=fd1+1; //描述符最大值加1,假设fd1>fd2
switch(select(maxfdp,&fds,NULL,NULL,&timeout))
case -1: 
exit(-1);
break; //select错误,退出程序
case 0:
break;
default:
if(FD_ISSET(fd1,&fds)) //测试fd1是否可读

(b)Poll方法:

应用程序常常使用select系统调用,它可能会阻塞进程。这个调用由驱动的 poll 方法实现,原型为:

unsigned int (*poll)(struct file *filp,poll_table *wait)

Poll设备方法负责完成:

    1. 使用poll_wait将等待队列添加到poll_table中。
    2. 返回描述设备是否可读或可写的掩码。

位掩码:

    • POLLIN   设备可读
    • POLLRDNORM 数据可读
    • POLLOUT  设备可写
    • POLLWRNORM 数据可写
    • 设备可读通常返回(POLLIN|POLLRDNORM )
    • 设备可写通常返回(POLLOUT|POLLWRNORM )
static unsigned int mem_poll(struct file *filp,poll_table *wait)
{
    struct scull_pipe *dev =filp->private_data;
    unsigned int mask =0;
    /* 把进程添加到等待队列 */
    poll_wait(filp,&dev->inq,wait);
    /*返回掩码*/
    if (有数据可读)
    mask = POLLIN |POLLRDNORM;/*设备可读*/
    return mask;
}

(5)自动创建设备文件

    devfs_register (devfs_handle_t dir,const char *name,unsigned int flags,unsigned int major,unsigned int minor,umode_t mode,void *ops,void *info)
在指定的目录中创建设备文件。

  • dir:目录名,为空表示在/dev/目录下创建;
  • name:文件名;
  • flags:创建标志;
  • major:主设备号;
  • minor:次设备号;
  • mode:创建模式;
  • ops:操作函数集;
  • info:通常为空

从Linux 2.6.13开始,devfs不复存在,udev成为devfs的替代。相比devfs,udev(mdev)存在于应用层。利用udev(mdev)来实现设备文件的自动创建很简单,

在驱动初始化的代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

  • struct class *myclass = class_create(THIS_MODULE,“my_device_driver”);
  • device_create(myclass, NULL, MKDEV(major_num, 0), NULL,“my_device”);

当驱动被加载时,udev( mdev )就会自动在/dev下创建my_device设备文件

(七)中断处理 

中断概念

为什么需要中断?

    1. 外设的处理速度一般慢于CPU
    2. CPU不能一直等待外部事件

所以设备必须有一种方法来通知CPU它的工作进度,这种方法就是中断。

中断实现

在Linux驱动程序中,为设备实现一个中断包含两个步骤:

    1. 向内核注册中断
    2. 实现中断处理函数

中断注册

request_irq用于实现中断的注册功能:
int request_irq(unsigned int irq,void (*handler)(int, void*, struct pt_regs *),unsigned long flags,const char *devname,void *dev_id)
返回0表示成功,或者返回一个错误码

  • unsigned int irq 中断号
  • void (*handler)(int,void *,struct pt_regs *)中断处理函数。
  • unsigned long flags 与中断管理有关的各种选项。
    • IRQF_DISABLED(SA_INTERRUPT)
    • 如果设置该位,表示是一个“快速”中断处理程序;如果没有设置这位,那么是一个“慢速”中断处理程序。
    • IRQF_SHARED(SA_SHIRQ)
    • 该位表明中断可以在设备间共享
  • const char * devname 设备名
  • void *dev_id 共享中断时使用

快速/慢速中断

这两种类型的中断处理程序的主要区别在于:快速中断保证中断处理的原子性(不被打断),而慢速中断则不保证。换句话说,也就是“开启中断”标志位(处理器IF)在运行快速中断处理程序时是关闭的,因此在服务该中断时,不会被其他类型的中断打断;而调用慢速中断处理时,其它类型的中断仍可以得到服务

共享中断

共享中断就是将不同的设备挂到同一个中断信号线上。Linux对共享的支持主要是为PCI设备服务
共享中断也是通过request_irq函数来注册的,
但有三个特别之处:

    1. 申请共享中断时,必须在flags参数中指定 IRQF_SHARED位
    2. dev_id参数必须是唯一的。
    3. 共享中断的处理程序中,不能使用disable_irq(unsigned int irq)如果使用了这个函数,共享中断信号线的其它设备将同样无法使用中断,也就无法正常工作了。

中断处理程序

什么是中断处理程序,有何特别之处?
中断处理程序就是普通的C代码。特别之 处在于中断处理程序是在中断上下文中运行的,它的行为受到某些限制:

    1. 不能向用户空间发送或接受数据
    2. 不能使用可能引起阻塞的函数
    3. 不能使用可能引起调度的函数

中断处理函数流程
void short_sh_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{

/* 判断是否是本设备产生了中断(为什么要做这样的检测?) */
value = inb(short_base);
if (!(value & 0x80)) return;
/* 清除中断位(如果设备支持自动清除,则不需要这步) */
outb(value & 0x7F, short_base);
/* 中断处理,通常是数据接收 */
。。。。。。。。。
/* 唤醒等待数据的进程 */
wake_up_interruptible(&short_queue);

}

释放中断

当设备不再需要使用中断时(通常在驱动卸载时), 应当把它们返还给系统,
使用:void free_irq(unsigned int irq, void *dev_id)

版权说明:

      内容由网上找到的一套国嵌的培训视频整理而来

发布了164 篇原创文章 · 获赞 229 · 访问量 62万+

猜你喜欢

转载自blog.csdn.net/li_wen01/article/details/103898990
今日推荐