Linux device driver 的概念
系统调用是操作系统内核和应用程序之间的接口,设备驱动程序是操作系统内核和机器硬件之间的接口。设备驱动程序为应用程序屏蔽了硬件的细节,这样在应用程序看来,硬件设备只是一个设备文件,应用程序可以象操作普通文件一样对硬件设备进行操作。设备驱动程序是内核的一部分,它完成以下的功能:
1、对设备初始化和释放;
2、把数据从内核传送到硬件和从硬件读取数据;
3、读取应用程序传送给设备文件的数据和回送应用程序请求的数据;
4、检测和处理设备出现的错误。
在Linux操作系统下有三类主要的设备文件类型,一是字符设备,二是块设备,三是网络设备。字符设备和块设备的主要区别是:在对字符设备发出读/写请求时,实际的硬件I/O一般就紧接着发生了,块设备则不然,它利用一块系统内存作缓冲区,当用户进程对设备请求能满足用户的要求,就返回请求的数据,如果不能,就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备设计的,以免耗费过多的CPU时间来等待。
已经提到,用户进程是通过设备文件来与实际的硬件打交道。每个设备文件都都有其文件属性(c/b),表示是字符设备还是块设备?另外每个文件都有两个设备号,第一个是主设备号,标识驱动程序,第二个是从设备号,标识使用同一个设备驱动程序的不同的硬件设备,比如有两个软盘,就可以用从设备号来区分他们。设备文件的的主设备号必须与设备驱动程序在登记时申请的主设备号一致,否则用户进程将无法访问到驱动程序。
最后必须提到的是,在用户进程调用驱动程序时,系统进入核心态,这时不再是抢先式调度。也就是说,系统必须在你的驱动程序的子函数返回后才能进行其他的工作。如果你的驱动程序陷入死循环,不幸的是你只有重新启动机器了,然后就是漫长的fsck。
字符设备是指在I/O传输过程中以字符为单位进行传输的设备,例如键盘,打印机等。请注意,以字符为单位并不一定意味着是以字节为单位,因为有的编码规则规定,1个字符占16比特,合2个字节。
在编写Linux内核设备驱动时,需要使用内核提供的设备驱动接口,向内核提供具体设备的操作方法。应用程序可以像操作普通文件一样操作字符设备。常见的串口、调制解调器都是字符设备。编写字符设备驱动需要使用内核提供的register_chrdev()函数注册一个字符设备驱动。函数定义如下:
int register_chrdev(unsigned int major, const char *name, struct file_operations *fops);
参数major是主设备号,name是设备名称,fops是指向函数指针数组的结构指针,驱动程序的入口函数都是包括在这个指针内部。该函数的返回值如果小于0表示注册设备驱动失败,如果设置major为0,表示由内核动态分配主设备号,函数返回值是主设备号。
内核提供了unregister_chrdev()函数卸载设备驱动,定义如下:
int unregister_chrdev(unsigned int major, const char *name);
major是想要卸载的主设备号,name是欲卸载的设备驱动名称。内核会比较设备驱动名称和设备号是否相同,不同则返回-EINVAL。错误地卸载设备驱动可能会带来严重后果,因此在卸载驱动的时候应该对函数返回值进行判断。
register_chrdev()函数中有一个fops参数,该参数指向一个file_operations结构,该结构包含了驱动上所有操作。随着内核功能的不断增加,file_operations结构的定义也越来越复杂,内核2.6.18版本的定义如下:
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 (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, 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 *, fl_owner_t id);
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);
…
};
字符设备驱动程序的基本步骤
一.设备号
对字符设备的访问是通过文件系统内的设备名称来访问的,设备名称位于目录/dev下.为了便于系统管理,设置了和设备名称一一对应的设备号,它分为主设备号和次设备号.通常来说,主设备号标示了设备对应的驱动程序,次设备号则用来分辨拥有同一个主设备号的的各个不同设备.
在内核中,设备号使用类型dev_t来保存,它包括了主设备号和次设备号.dev_t是一个32位的整数,其中的12位用来标示主设备号,其余的20位用来标示次设备号.我们可以使用两个宏来获得设备的主设备号及次设备号:
MAJOR(dev_t dev_id);
MINOR(dev_t dev_id);
将主设备号和次设备号转换为dev_t类型,则可以使用下面的宏:
MKDEV(int major, int minor);
其中,major为主设备号,minor为次设备号.
二.分配设备号
在建立一个字符设备之前.首先要申请设备号,完成该功能的函数有两个,都包含在头文件中.下面分别来看这两个文件:
1.int register_chrdev_region(dev_t first, unsigned int count, char *name);
其中, first为要分配的设备编号范围的起始值,经常被置零.count则是所请求的连续设备编号的个数,这意味着只能申请连续的设备编号.
2.int alloc_chrdev_region(dev_t *dev, unsigned firstminor, int count, char *name);
其中dev用于保存申请成功后动态分配的第一个设备号, firstminor则是请求使用的第一个次设备号.其余与上个函数相同.
三.定义并初始化file_operations结构体.
file_operations结构体用于连接设备号和驱动程序的操作.在该结构体的内部包含了一组函数指针,这些函数用来实现系统调用.通常情况下,要注册如下的几个函数:
1.struct module *owner:用来指向拥有该结构体的模块.
2.ssize_t read(struct file *filp, char __user *buf, size_t count, loff_t *f_ops):用来从设备中读取数据.其中:
filp为文件属性结构体指针.
buf为用户态函数使用的字符内存缓冲.
count为要读取的数据数.
f_ops为文件指针的偏移量.
2.ssize_t write(struct file *filp, const char __user *buf, size_t count, loff_t *f_ops):用来向设备输入数据.各函数的含义与上个函数相同.
3.int open(struct inode *inode, struct file *):该函数用来打开一个设备文件.
4.int release(struct inode *inode, struct file *):该函数用来关闭一个设备文件.
该结构体的初始化形式如下例:
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.read = read,
.write = write,
.open = open,
.release = release,
}
四.字符设备的注册.
内核内部使用struct cdev结构来表示字符设备.在内核调用设备的操作之前,必须分配或注册一个或者多个该结构体.该结构体包含在头文件中.一般的步骤如下:
首先定义该结构体:
struct cdev my_cdev;
然后即可以初始化该结构,使用如下的函数初始化:
int cdev_init(struct cdev *dev, struct file_operations *fops).
然后定义该结构体中的一个所有者字段:
my_cdev.owner = THIS_MODULE;
最后即可以向模块添加该结构体:
int cdev_add(struct cdev *dev, dev_t dev_num, usigned int count).其中dev是cdev结构体,dev_num是该设备对应的第一个设备编号, count则是与该设备关联的设备编号数量.
五.移除字符设备
void cdev_del(struct cdev *dev);
六.注销设备号.
unregister_chrdev_region(dev_t first, unsigned int count);
以上两个函数一般用于模块出口函数中.