Linux驱动程序概述
Linux设备驱动程序是为特定的硬件提供给应用程序的一组标准化接口,它隐藏了设备工作的细节。用户程序通过标准化系统调用,这些调用和特定的硬件是无关的,再由Linux内核调用特定的设备驱动程序操作和控制特定的实际的硬件设备。其中,Linux系统设备分为三种类型:
- 字符设备(character device)
- 块设备(block device)
- 网络接口(network interface)
本篇文章中,主要谈论字符设备驱动程序的设计。
字符设备是能够像字节流一样被访问的设备,一般不使用缓存技术。字符设备驱动程序最少应实现open、close、read和write系统调用。典型的字符设备例子是终端设备(/dev/console)和串口(/dev/ttyS0)。
字符驱动程序的调用过程
如上图,用户想要调用Linux设备驱动函数对设备进行操作时,由Linux系统调用来间接调用设备驱动函数,此时系统由用户态陷入内核态,完成对设备的操作后(即完成内核态数据和用户态数据的交换),操作系统又返回用户态。
驱动程序的一些基本知识
主设备号和次设备号
Linux系统为每一个设备分配了一个主设备号和次设备号,主设备号标识一类设备对应的驱动程序,次设备号用来标识一类设备中的某个具体设备。比如说:现在有打印机这个设备,主设备号就只用来标识打印机这类设备,区别于其他如鼠标,传真机等设备;而次设备号是用来区别不同打印机,如打印机A和打印机B,它俩主设备号一样,但次设备号不同。
设备文件的操作
Linux系统访问设备就像访问文件一样,例如打开设备使用系统调用open(),关闭设备使用系统调用close()。读写设备使用系统调用read()和write()。在Linux内核中,字符设备使用struct file_operations结构来定义设备的各种操作集合,结构中的各个函数分别响应同名或类似的名称的系统调用。struct file_operations结构在Linux系统下的定义如下:
struct file_operations
{
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, char __user *, size_t, loff_t);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_write) (struct kiocb *, const char __user *, size_t, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t*);
ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t*);
ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void*);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
};
主要函数的作用
ioctl函数
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ioctl 函数提供用户程序对设备执行特定的命令的方法,比如设置设备驱动程序内部参数,控制设备操作特性,或其他不是读也不是写的操作等。调用成功返回非负值。
open函数
int (*open) (struct inode *, struct file *);
open 函数用来打开设备。如果该函数没有实现,系统调用 open 总是成功,但驱动程序得不到任何打开设备的通知。
read/write函数
ssize_t (*read) (struct file * filp, char __user * buffer, size_t size, loff_t * opps);
ssize_t (*write) (struct file * filp, const char __user * buffer, size_t size, loff_t * opps);
(指针参数 filp 为进行读取(写)信息的目标文件,指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址),参数size为要读取的信息长度,参数 opps 为读(写)的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)。
如果返回值为非负,则操作成功。
llseek函数
loff_t (*llseek) (struct file * filp , loff_t p, int orig);
指针参数filp为进行读取信息的目标文件结构体指针;参数 p 为文件定位的目标偏移量;参数orig为对文件定位的起始地址,其中,orig=0,则表示从文件头开始;orig=1,则表示从当前位置开始;origin=2,表示从文件末尾开始。
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值.
loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽。
编写字符设备驱动程序,主要是实现struct file_operations结构中的各个函数。当然,驱动程序并不是要实现所有的这些函数,可以根据实际设备需要实现必要的函数即可。
模块注册和卸载的函数调用
在 Linux 系统中注册设备时,通常需要传递三个重要参数:主设备号,设备名称,设备文件结构。
主设备号是在系统内是唯一标识设备类型的定义,向内核申请设备注册或卸载时,都需要传递主设备号。主设备号的定义有两种方法:
- 指定一常数作为主设备号,只要不与已有设备号冲突,即可把该设备号唯一地指派给该设备;
- 以设备号0进行注册申请,由系统返回一个可用设备号作为主设备号。
设备注册或卸载时还包括传递一个设备名称,用户程序使用这个名称来打开设备。
设备是以文件的形式存在的,所以设备注册时需要使用一个文件结构struct file_operations定义,内核使用此结构定位驱动程序的操作函数。
字符设备的注册通过调用register_chrdev来注册,通过unregister_chrdev_region来卸载:
/* 字符设备注册函数 */
int register_chrdev (unsigned int major, const char *name, struct file_operations *fops);
/*字符设备卸载函数*/
static inline void unregister_chrdev(unsigned int major, const char *name);