ch03.字符设备驱动程序
编写驱动程序的第一步就是定义驱动程序为用户程序提供的能力(机制)。接下来以scull(“Simple Character Utility for Loading Localities”区域装载的简单字符工具,scull是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个设备)为例说明。
主设备号和次设备号
对字符设备的访问时通过文件系统内的设备名称进行的。这些名称被称为特殊文件、设备文件或者简单称之为文件系统树的节点。它们通常位于/dev目录,字符设备驱动程序的设备文件可通过ls -l命令输出的第一列中的“c”来识别。
ls -l执行后,可在设备文件项的最后修改日期前看到两个数(用逗号隔开),这两个数就是对应设备的主次设备号。通常而言,主设备号标识设备对应的驱动程序。linux内核允许多个驱动程序共享主设备号。此设备号由内核使用,用于正确确定设备文件所指的设备。
内核中,dev_t类型用来保存设备编号----包括主次设备号。dev_t是一个32位的数,其中的12位用来表示主设备号,其余20位用来表示此设备号。要获得dev_t的主设备号或此设备号,使用:
获取主设备号:MAJOR(dev_t dev);
获取此设备号:MINOR(dev_t dev);
将主次设备号转换成dev_t类型:MKDEV(int major , int minor );
分配和释放设备编号
在建立字符设备之前,我们的驱动程序首先要做的就是获得一个或者多个设备编号。
int register_chrdev_region(dev_t first,unsigned int count,char *name); (静态分配设备号 <linux/fs.h>)
参数1:要分配的设备编号范围的起始值 (first的次设备号通常被设置为0) 参数2:所请求得连续设备号的个数 参数3:name是和该编号相关的设备名称(出现在/proc/devices和sysfs中)
返回值:成功 0
失败 负的错误码
如果提前知道所需要的设备编号,则register_chrdev_region会工作得很好。但是我们经常不知道设备将要使用哪些主设备号;因此,linux内核开发社区一直在努力转向设备编号的动态分配。
int alloc_chrdev_region(dev_t *dev ,unsigned int firstminor ,unsigned int count ,char *name); (动态分配所需要的主设备号)
参数1:&dev 参数2:要分配的第一个次设备号,通常为0 参数3和4与register_chrdev_region函数一样。
在不使用时释放这些设备编号:
void unregister_chrdev_region(dev_t first ,unsigned int count);
通常在模块清除函数中使用unregister_chrdev_region函数。
在用户空间程序可访问上述设备编号之前,驱动程序需要将设备编号和内部函数链接起来,这些内部函数用来实现设备的操作。
重要的数据结构
设备号的注册仅仅是驱动程序代码必须完成的许多工作中的第一件事情。大部分基本的驱动程序操作涉及到三个重要的内核数据结构,分别是file_operations、file和inode。
文件操作file_operations结构体
file_operations结构就是用来建立驱动程序操作和设备编号的连接的。(<linux/fs.h>)其中包含了一组函数指针。习惯上,file_operations结构体或者指向这类结构体的指针称为fops,这个结构体中的每一个字段都必须指向驱动程序中实现特定操作的函数,对于不支持的操作,对应的字段可置为NULL值。
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 *); //__user表示指针是一个用户空间地址,因此不能
//被直接引用
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 (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
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 *, loff_t, loff_t, 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 (*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 (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
struct file_operations Code
struct module *owner;
第一个file_operations字段并不是一个操作,相反,它是指向“拥有”该结构的模块的指针。内核使用这个字段以避免在模块的操作正在被使用时卸载该模块。几乎所有的情况下,该成员都会被初始化为THIS_MODULE。
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 *);
poll方法是poll、epoll和select这三个系统调用的后端实现。这三个系统调用可用来查询某个或者多个文件描述符的读取或写入是否会被阻塞。poll方法应该返回一个位掩码,用来指出非阻塞的读取或写入是否可能,并且也会向内核提供将调用进程置于休眠状态直到i/o变为可能时的信息。如果驱动程序将poll方法定义为NULL,则设备会被认为既可读也可写,并且不会被阻塞。
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
unlocked_ioctl提供了一种执行设备特定命令的方法。
int (*open)(struct inde*,struct file*);
尽管这始终是对设备文件执行的第一个操作,然而却并不要求驱动程序一定要声明一个相应的方法。如果这个入口为NULL,设备的打开操作永远成功,但系统不会通知驱动程序。
int (*release)(struct inode*,struct file*);
当file结构体被释放时,将调用这个操作。
struct file_operations scull_fops = {
.owner = THIS_MODULE,
.llseek = scull_llseek,
.read = scull_read,
.write = scull_write,
.unlocked_ioctl = scull_unlocked_ioctl,
.open = scull_open,
.release = scull_release,
};
常用的file_opreations初始化 Code
file结构
设备驱动程序使用的第二个最重要的内核数据结构是struct file。file结构代表一个打开的文件(它并不仅仅限定于设备驱动程序,系统中每个打开的文件在内核空间都有一个对应的file结构)。它由内核在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数,内核会释放这个数据结构。内核源码中,指向struct file的指针通常被称为filep。
mode_t f_mode;
文件模式
loff_t f_pos;
当前的读/写位置。loff_t是一个64位的数(用gcc的术语就是long long)。如果驱动程序需要直到文件中的当前位置,可以读取这个值,但不要去修改它。read/write会使用它们接收到的最后那个指针参数来更新这一位置,而不是直接对filp->f_pos进行操作。
struct file_opreations *f_op;
与文件相关的操作。
void *private_data;
私有数据。
inode结构
内核用inode结构在内部表示文件,因此它和file结构不同,后者表示打开的文件描述符。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但他们都指向单个inode结构(包含了大量的文件信息)。
为了鼓励编写可移植性更强的代码,内核开发者增加了两个新的宏,可用来从一个inode中获得主设备号和次设备号:
unsigned int iminor(struct inode *inode);
unsigned int imajor(struct inode *inode);
字符设备的注册
内核内部使用struct cdev结构来表示字符设备。在内核调用设备操作之前,必须分配并注册一个或者多个上述结构(<linux/cdev.h>)。
void cdev_init(struct cdev *cdev,struct file_operations *fops);
初始化已分配到的结构。(struct cdev也有一个所有者字段,应该被设置为THIS_MODULE)
在cdev结构体设置好之后,最后的步骤是通过cdev_add告诉内核该结构的信息(注册):
int cdev_add(struct cdev *cdev,dev_t num,unsigned int count);
参数1:cdev结构 参数2:该设备对应的第一个设备编号 参数3:和该设备关联的设备编号的个数 (通常取1)
注:在驱动程序还没有完全准备好处理设备上的操作时(初始化),就不能调用cdev_add。
cdev_del(struct cdev * dev);
从系统中移除(注销)一个字符设备。
scull中的设备注册
scull内部通过struct scull_dev的结构体来表示每个设备。
cdev结构体镶嵌在该结构体中,现在,我们的注意力在cdev上,即内核和设备间的接口struct cdev。该结构必须如上所述的被初始化并添加到系统中。
open和release
open方法
int (*open)(struct inode *inode,struct file *filp);
其中的inode参数在其i_cdev字段中包含可我们所需要的信息,及我们先前设置的cdev结构。现在的问题是,我们通常不需要cdev结构本身,而是希望得到包含cdev结构的scull_dev结构。内核中实现了一种技巧,通过定义在<linux/kernel.h>头文件中的
container_of宏函数实现:
container_of(pointer,container_type,container_field); ----已知结构体成员的指针反推结构体的指针
这个宏函数需要一个container_field字段的指针,该字段包含在container_type类型的结构中,然后返回包含该字段的结构指针。
struct scull_dev *dev; /*device information*/ dev = container_of(inode->i_cdev,struct scull_dev,cdev);
filp->private_data = dev; /*for other methods*/
container_of Code
一旦代码找到scull_dev结构之后,scull将一个指针保存到了file结构的private_data字段中,这样可以方便今后对该指针的访问。
==》经过些微简化的scull_open代码:
int scull_open(struct inode *inode,struct file *filp)
{
struct scull_dev *dev; /*device information*/
dev = container_of(inode->i_cdev,struct scull_dev,cdev);
filp->private_data = dev; /*for other methods*/
}
scull_open Code
release方法
release方法的作用正好和open相反。完成下面的任务:1.释放由open分配的、保存在filp->private_data中的所有内容。2.在最后一次关闭操作时关闭设备。
read和write
read:从内核拷贝数据到应用程序空间 对应调用copy_to_user
write:从应用程序空间拷贝数据到内核 对应调用copy_from_user
ssize_t read(struct file *filp,char __user *buff,size_t count,loff_t *offp);
ssize_t write(struct file *filp,const char __user *buff,size_t count,loff_t *offp);
参数1:filp文件流指针 参数2:指向用户空间的缓冲区 参数3:请求传输的数据长度 参数4:指针,指明用户在文件中进行存取操作的位置。
注:read和write方法的buff参数是用户空间的指针,因此,内核代码不能直接引用其中的内容。
原因:1.在内核模式中运行时,用户空间的指针可能是无效的,该地址可能根本无法被映射到内核空间,或者可能指向某些随机数据。2.用户空间的内存是分页的,对用户空间内存的直接引用将导致页错误,结果可能是‘oops’。3.如果我们的驱动程序盲目引用用户提供的指针,讲导致系统出现打开的后门,从而允许用户空间程序随意访问或覆盖系统中的内存。危及用户系统安全。
然而,驱动程序必须访问用户空间的缓冲区以便完成自己的工作。为了确保安全,这种访问应该始终通过内核提供的专用函数完成。
unsigned long copy_to_user(void __user *to,const void *from,unsigned long count);
unsigned long copy_from_user(void *to,const void __user *from,unsigned long count);
这两个函数的作用并不限于在内核空间和用户空间之间拷贝数据,它们还检查用户空间的指针是否有效。如果指针无效,就不会进行拷贝;如果在拷贝过程中遇到无效地址,则仅仅会复制部分数据。
返回值的问题:read和write方法在出错时都返回一个负值,大于等于0的返回值告诉调用程序成功传输了多少字节。如果在正确传输部分数据之后发生了错误,则返回值必须是成功传输的字节数,但这个错误只能在下一次函数调用时才会得到报告。