Linux2.6设备驱动常用的接口函数

Linux2.6设备驱动常用的接口函数(一)

----字符设备

 

刚开始,学习linux驱动,觉得linux驱动很难,有字符设备,块设备,网络设备,针对每一种设备其接口函数,驱动的架构都不一样。这么多函数,要每一个的熟悉,那可多难啦!可后来发现linux驱动有很多规律可循,驱动的基本框架都差不多,再就是一些通用的模块。

基本的架构里包括:加载,卸载,常用的读写,打开,关闭,这是那种那基本的咯。利用这些基本的功能,当然无法实现一个系统。比方说:当多个执行单元对资源进行访问时,会引发竞态;当执行单元获取不到资源时,它是阻塞还是非阻塞?当突然间来了中断,该怎么办?还有内存管理,异步通知。而linux针对这些问题提供了一系列的接口函数和模板框架。这样,在实际驱动设计中,根据具体的要求,选择不同的模块来实现其功能需求。

 

觉得能熟练理解,运用这些函数,是写号linux设备驱动的第一步。因为是设备驱动,是与最底层的设备打交道,就必须要熟悉底层设备的一些特性,例如字符设备,块设备等。系统提供的接口函数,功能模块就像是工具,能够根据不同的底层设备的的一些特性,选择不同的工具,方能在linux驱动中游刃有余。

 

最后就是调试,这可是最头疼的事。在调试过程中,总会遇到这样,那样的问题。怎样能更快,更好的发现并解决这些问题,就是一个人的道行咯!我个人觉得:

发现问题比解决问题更难!

时好时坏的东西,最纠结!

看得见的错误比看不见的错误好解决!

 

一:Fops结构体中函数:

①ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).

②ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数

③loff_t (*llseek) (struct file *, loff_t, int);

llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).

④int (*open) (struct inode *, struct file *);

尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.

⑤int (*release) (struct inode *, struct file *);

在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.

⑥int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.

⑦int (*mmap) (struct file *, struct vm_area_struct *);

mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.

⑧unsigned int (*poll) (struct file *, struct poll_table_struct *);

poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写。

 

二:驱动的基本架构

1:模块加载

①创建设备号:

MAJOR(dev_t dev):根据设备号dev获得主设备号;
MINOR(dev_t dev):根据设备号dev获得次设备号;
MKDEV(int major, int minor):根据主设备号major和次设备号minor构建设备号。

可以通过以下方法来创建设备号:

dev_t mydev; 

mydev=MKDEV(50,0); 

我们也可以由mydev得到major 和minor number. 

int major,minor; 

major=MAJOR(mydev); 

minor=MINOR(mydev); 

dev_t类型:
  在内核中,dev_t类型(定义在<linux/types.h>中)用来保存设备编号——包括主设备号和次设备号。在内核2.6.0中,dev_t是一个32位的数,其中高12位表示主设备号,低20位表示次设备号。

②申请设备号

内核提供了三个函数来注册一组字符设备编号,这三个函数分别是 register_chrdev_region()、alloc_chrdev_region() 。这三个函数都会调用一个共用的 __register_chrdev_region() 函数来注册一组设备编号范围(即一个 char_device_struct 结构)。

静态申请:

register_chrdev_region(dev_t first,unsigned int count,char *name)
First :要分配的设备编号范围的初始值(次设备号常设为0);
Count:连续编号范围.
Name:编号相关联的设备名称. (/proc/devices);

成功时返回 0 ,失败时返回负数。

动态分配:
alloc_chrdev_region(dev_t *dev,unsigned int firstminor,unsigned int count,char*name); 

*dev:存放返回的设备号;

Firstminor : 通常为0;
Count:连续编号范围.
Name:编号相关联的设备名称. (/proc/devices);

③初始化设备结构体

一个 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 结构的指针,起始设备编号,以及设备编号范围。
cdev_add(struct cdev *p, dev_t dev, unsigned count)

 

2:模块卸载:

注销设备:cdev_del(struct cdev *p)

释放设备号:unregister_chrdev_region(dev_t from, unsigned count); 

 

三:中断

1:申请中断:int request_irq(unsigned int irq,
void (*handler)(int irq, void *dev_id, struct pt_regs *regs),

           unsigned long irqflags,
       const char * devname,
       oid *dev_id  );
irq: 要申请的硬件中断号。在Intel平台,范围是0~15。
handler: 向系统登记的中断处理函数。这是一个回调函数,中断发生时,系统掉用这个函数,传入的参数包括硬件中断号,device id,寄存器值。dev_id就是下面的request_irq时传递给系统的参数dev_id。
irqflags: 中断处理的一些属性。比较重要的有SA_INTERRUPT,标明中断处理程序是快速处理程序(设置SA_INTERRUPT)还是慢速处理程序(不设置SA_INTERRUPT)。快速处理程序被调用时屏蔽所有中断。慢速处理程序不屏蔽。还有一个 SA_SHIRQ属性,设置了以后运行多个设备共享中断。
dev_id:  中断共享时会用到。一般设置为这个设备的device结构本身或者NULL。中断处理程序可以用dev_id找到相应的控制这个中断的设备,或者用irq2dev_map找到中断对应的设备。

2:释放中断:void free_irq(unsigned int irq, void *dev_id);
irq: 是将要注销掉的中断服务函数的中断号;
dev_id: 指定与 request_irq() 函数中使用的 dev_id 值相同的值。

 

3:中断处理程序的架构

为了在中断执行时间尽可能短和中断处理需要完成尽可能多的工作间寻找一个平衡点,linux将中断处理程序分为两个半部:顶半部和底半部。顶半部完成尽可能少的比较紧急的任务,而底半部通常做了中断处理程序中所有工作,而且可以被新的中断打断。尽管系统将中断处理程序分为两部分,但并可以僵化的认为中断处理程序中一定要分为上下两半部。

 

通常底半部机制主要有tasklet、工作队列和软中断
tasklet

 

②工作队列

 

 

四:阻塞与轮询

阻塞,执行单元在不能获得资源时便挂起,直到获得可操作的条件后才进行操作。非阻塞,执行单元在不能获得资源时便直接返回。

 

那么这儿就有一个问题:如何将进程挂起,挂起的进程又如何唤醒?

定义等待队列头

Wait_queue_head_t my_queue;

初始化等待队列头

Init_waitqueue_head(&my_queue);

DECLARE_WAIT_QUEUE_HEAD(name);

定义等待队列

DECLARE_WAITQUEUE(name, tsk);

添加/移除等待队列

Void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

Void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

等待事件

Wait_event(queue, condition);

Wait_event_interruptible(queue, condition);

Wait_event_timeout(queue, condition, timeout);

Wait_event_interruptible_timeout(queue, condition, timeout);

唤醒队列

Void wake_up(wait_queue_head_t *queue);

Void wake_up_interruptible(wait_queue_head_t *queue);

在等待队列上睡眠

Sleep_on(wait_queue_head_t *q);

Interruptible_sleep_on(wait_queue_head_t *q);

 

轮询:在应用程序中使用非阻塞IO,来查询设备是否可以进行无阻塞访问,通常会用到系统调用函数select()/poll函数,而这两个函数最终回去调用设备驱动中的poll函数。

设备驱动中poll函数原型:

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

这个函数要进行下面两项工作。首先,对可能引起设备文件状态变化的等待队列调用poll_wait(),将对应的等待队列头添加到poll_table.然后,返回表示是否能对设备进行无阻塞读写访问的掩码。在上面提到了一个poll_wait()函数,它的原型:

void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);

它的作用就是把当前进程添加到wait参数指定的等待列表(poll_table)中。需要注意的是这个函数是不会引起阻塞的。

 

五:内存/IO

清楚几个概念:IO空间,内存空间。IO端口,IO内存

①寄存器和内存的区别

寄存器和内存都是可以用来读写的,但寄存器的操作时有副作用,称之为(side effect 边际效果)

读取一个寄存器可能导致寄存器中的内容发生变化,比如在一些设备的中断状态寄存器中,读取了寄存器后会自动清零

 

②IO空间和内存空间

并不是所有的体系结构都有IO空间这个定义的,我所了解的只有X86体系上有,而ARM体系结构就没有这种区别,

在X86上,IO空间和内存是独立的,他们各自有各自的总线,并且IO空间一般是64K,即16位,内存空间为4G,可见他们的差别是很大。

 

③IO 端口和IO内存

在有了IO空间的概念后,就有IO端口和IO内存

当一个寄存器或内存位于IO空间时候,称之为IO端口

当一个寄存器或内存位于内存空间时候,称之为IO内存

 

一:申请内存

1:用户空间申请内存

void *malloc(size_t size) //返回类型为空指针类型
void free(void *ptr);

Malloc()函数用来在堆中申请内存空间,free()函数释放原先申请的内存空间。Malloc()函数是在内存的动态存储区中分配一个长度为 size 字节的连续空间.

2:内核空间申请内存

在内核空间中,设计到申请内存的函数有kmalloc(),__get_free_page()和vmalloc函数。kmalloc()和__get_free_page()申请的内存位于物理内存映射区域,在物理上是连续的,而且与真实的物理地址只有一个固定的偏移。而vmalloc是在虚拟内存空间中申请一片连续的区域,而这片区域在物理地址上,不一定连续。

Void *kmalloc(size_t size, int flags);

__get_free_pages(unsigned int flags, unsigned int order);

Void *vmalloc(unsigned long size);

释放内存,分别对应为:

Void kfree(void *ptr);

Void free_page(unsigned long addr);

Void vfree(void *ptr);

 

二:IO端口和IO内存的访问

对于IO端口,和IO内存给如何访问呢?

以下是IO内存的访问流程,IO端口的访问流程和IO内存,原理上是相通的,只是接口函数不一样。

 

六:并发

当多个执行单元,同时并行执行,在获取共享资源时,可能发生冲突,而这种冲突,就叫竞态。可是该如何处理这种冲突呢?而解决这种冲突的途径是:让个执行单元对共享资源的互斥访问。主要方法有:信号量,自旋锁,另外还用到中断屏蔽,原子操作,互斥体。

 

1:信号量:

①定义信号量:

Struct semaphore sem;

②初始化信号量:

Void sema_init(struct semaphore *sem, int val);

Void init_MUTEX(struct semaphore *sem);

初始化一个互斥信号量,把sem的值设置为1.

Void init_MUTEX_LOCKED(struct semaphore *sem);

初始化一个互斥信号量,把sem的值设置为0.

以下宏是定义并初始化信号量的快捷方式。

DECLARE_MUTEX(name);

DECLARE_MUTEX_LOCKED(name);

③获得信号量:

Void down(struct semaphore *sem);//不可中断

Void down_interruptible(struct semaphore *sem);//可中断

Int down_trylock(struct semaphore *sem);

如果能立刻得到,则获取信号量并返回0,否则,返回非0值,它不会导致调用者休眠。

④释放信号量:

Int up(struct semaphore *sem);

释放信号量,唤醒等待者

2:自旋锁:

①定义自旋锁:

Spinlock_t lock;

②初始化自旋锁:

Spin_lock_init(&lock);

③获得自旋锁:

Spin_lock(lock);

Spin_trylock(lock);

④释放自旋锁:

Spin_unlock(&lock);

 

利用这些函数,可以很好的解决的竞态,但是有一问题:如果在实际设计过程中,的确存在多个执行单元同时对共享资源读操作时(同时写操作,不现实),那该怎么办呢?觉得linux系统的整个架构真是完善,考虑到了问题的方方面面,着实佩服这些始作俑者。

读写自旋锁,读写信号量就解决了这个问题,其中读自旋锁,读信号量为共享的,也就是说,一个执行单元访问这段共享资源时,另一个执行单元也要访问时,是被允许的;而写自旋锁,写信号量,则是互斥的!

暂时所写的驱动程序中,还没用到这个,先了解一下的。

 

还有一个问题:既然自旋锁和信号量都是用来解决竞态的,那么他们有什么区别,什么时候该用自旋锁?什么时候该用信号量呢?

自旋锁:可以理解为,当执行单元访问不到共享资源时,它就会一直在等待,自旋,原地打转的意思,一直处于忙状态。通常适用于访问共享资源的时间比较短。

信号量:当执行单元访问不到共享资源时,它就会休眠,直到获取信号量时,被唤醒。通常是用于访问共享资源的时间比较长。

 

七:异步

区分异步和同步:简单的说,同步,就是当发出某个请求时,如果请求没有响应,我一直等你;异步,就是在发出某个请求后,如果没有回应,我可以做自己的事情,当请求响应后,才开始相关处理。

在驱动的测试中,还没有用到异步。就先了解下。

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值