本章目的编写一个完整的字符设备驱动
一、主次编号
字符设备有主设备号和此设备号,主编号标识设备相连的驱动. 次编号被内核用来决定引用哪个设备。
设备编号内部表示:dev_t 在<linux/types.h>中定义,2.6.0内核dev_t是32位的量,12位做朱编号,20位用作次编号。
应当利用在<linux/kdev_t.h>中的宏定义来获得dev_t的主或者次编号
MAJOR(dev_t dev);
MINOR(dev_t dev);
如果有主次编号,需要将其转换为一个dev_t,使用:
MKDEV(int major, int minor);
1.2分配和释放设备编号
建立字符驱动时,要做的第一件事实获取一个或多个设备编号。在<linux/fs.h>中定义
int register_chrdev_region(dev_t first, unsigned int count, char *name); first:是你要分配的起始设备编号,次编号部分常常是0,但是没有要求是那个效果 count:你请求的连续设备编号的总数,如果count太大,你要求的范围可能溢出到下一个次编号。 name:是应当连接到这个编号范围的设备的名子,它会出现在/proc/devices和sysfs中
返回值:成功0,出错负的错误码
内核分配一个主编号
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char *name); dev:输出参数,函数成功完成时持有你的分配范围的第一个数 firstminor:应当是请求的第一个要用的次编号,常常是0 count:类似 name:类似
不使用时,需要释放设备编号
void unregister_chrdev_region(dev_t first, unsigned int count); 调用它通常是你模块clenaup函数
1.2 主编号的动态分配
对于新驱动,强烈建议使用动态分配获取你的主设备编号,而不是随机选取一个当前空闲的编号。
换句话说,使用alloc_cdrdev_region而不是register_cdrdev_region。
动态分配的缺点是你无法提前创建设备节点,因为模块主编号会变化。编号一旦分配,就可以从/proc/devices中读取
cat /proc/devices
用在scull源码中获取主编号的代码:
if(scull_major) { dev = MKDEV(scull_major, scull_minor); result = register_cdrdev_region(dev, scull_nr_devs, "scull"); } else { result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull"); scull_major = MAJOR(dev); } if(result < 0) { printk(KERN_WARNING"scull: can't get major %d ", scull_major); return result; }
二、一些重要数据结构
大部分基础性的驱动操作包括3个重要的内核数据结构,称为file_operations, file 和 inode
2.1 文件操作
虽然我们有了设备编号,但是还没有连接任何设备操作到这些设备上。
字符设备通过file_operation结构建立连接,定义在<linux/fs.h>中,它是一个函数指针的集合,每个打开文件与它自身的函数集合相关联。
通常,一个file_operation接哦股获取一个指针称为fops(或其他变体),结构中每个成员必须指向驱动中的函数。如果不支持则置为NULL。
struct module *owner 不是一个操作,指向拥有这个结构的模块指针。使用时阻止卸载。基本上简单初始化为THIS_MODULE,在<linux/module.h>中定义的宏 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 *, filedir_t); unsigned int (*poll)(struct file *, struct poll_table_struct *); int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long); 发出设备特定命令的方法 int (*mmap)(struct file *, struct vm_area_struct *); int (*open)(struct inode *, struct file *); 设备文件进行的第一个操作 int (*flush)(stuct file *); int (*release)(struct inode *, struct file *); 在问价年结构被释放时引用这个操作 int (*fsync)(struct file *, struct dentry *, int); int (*aio_fsync)(struct kiocb *, int); 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); int (*check_flags)(int); int (*dir_notify)(struct file *, unsigned long);
scull只实现最重要的设备方法,file_operations结构初始化如下
struct file_operations scull_fops = { .owner = THIS_MODULE, .llseek = scull_llseek, .read = scull_read, .write = scull_write, .ioctl = scull_ioctl, .open = scull_open, .release = scull_release, };
2.2 文件结构
struct file,定义于<linux/fs.h>是设备驱动中第二个重要的数据结构。
struct file代表一个打开的文件,它由内核在open时创建,并传递给在问价能上操作的任何函数,直到最后关闭。
struct file指针常称为file或者filp,它的成员如下:
mode_t f_mode; 文件模式是可读还是可写,FMODE_READ和FMODE_WRITE loff_t f_pos; 当前读写位置,loff_t在所有平台都是64位 unsigned int f_flags; 文件标志,所有标志在<linux/fcntl.h>中定义 struct file_operations *f_op; void *private_data; struct dentry *f_dentry; 关联到文件目录的入口
2.3 inode结构
inode结构由内核内部用来表示文件,不同于打开文件描述符,可能有有代表单个文件的多个打开描述符的许多文件结构,但是他们都指向一个单个inode结构。
作为一个通用的规则,这个结构只有2个成员对于编写驱动代码有用:
dev_t i_dev; 代表设备文件的结点,包含实际的设备编号 struct cdev *i_cdev; 内核内部结构,代表字符设备。
开发者增加了2个宏,用来从一个inode中获取主次编号
unsigned int iminor(struct inode *inode); unsigned int imajor(struct inode *inode);
三、字符设备注册
内核在内部使用类型struct cdev结构来代表字符设备,在内核调用你的设备操作前,你编写分配并注册一个或几个这些结构。<linux/cdev.h>这个结构和它的关联帮助函数定义在这里。
如果你想在运行时获得一个独立的cdev结构,可以使用这样的代码:
struct cdev *my_cdev = cdev_alloc(); my_cdev->ops = &my_fops;
偶尔想cdev结构嵌入你自己的设备特定的机构;这种情况,应当初始化已经分配的结构,使用:
void cdev_init(struct cdev *cdev, struct file_operations *fops); 初始化 int cdev_add(struct cdev *dev, dev_t num, unsigned int count); dev是cdev结构,num是这个设备响应的第一个设备号dev_t结构,count应当关联到设备的设备号数目,常常是1
此函数调用可能失败,会返回错误码。一旦增加内核就会调用它 void cdev_del(struct cdev *dev);
为系统去除一个字符设备
3.1 scull中设备的注册
scull使用一个struct scull_dev类型的结构表示每个设备。
struct scull_dev { struct scull_qset *data; /* Pointer to first quantum set */ int quantum; /* the current quantum size */ int qset; /* the current array size */ unsigned long size; /* amount of data stored here */ unsigned int access_key; /* used by sculluid and scullpriv */ struct semaphore sem; /* mutual exclusion semaphore */ struct cdev cdev; /* Char device structure */ };
我们设备与内核结构的struct cdev,这个结构必须初始化并且如上述添加到系统中。
static void scull_setup_cdev(struct scull_dev *dev, int index) { int err, devno = MKDEV(scull_major, scull_minor + index); cdev_init(&dev->cdev, &scull_fops); dev->cdev.owner = THIS_MODULE; dev->cdev.ops = &scull_fops; err = cdev_add(&dev->cdev, devno, 1); /* Fail gracefully if need be */ if(err) printk(KERN_NOTICE "Error %d adding scull%d", err, index); }
四、open和release
4.1open方法
open方法提供驱动来做任何初始化来准备后续的操作,open应当进行下面的工作:
- 检查设备特定的错误(设备是否准备,硬件错误)
- 如果第一次打开,初始化设备
- 如果需要,更新f_op指针
- 分配并填充要放进filp->private_data的任何数据结构
open方法的原型是:
int (*open)(struct inode *indoe, struct file *filp); inode参数有我们需要的信息,里面的i_cdev成员。包含我们建立的cdev结构
在<linux/kernel.h>中定义:
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 */
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 */ /* now trim to 0 the length of the device if open was write-only */ if((filp->f_flags & O_ACCMODE) == O_WRONLY) { scull_trim(dev); /* ignore errors */ } return 0; /* success */ }
4.2 release方法
release是open的反面,有时你会发现实现函数称为device_close,设备方法应当进行下面任务:
释放open分配在filp->private_data中的任何东西
在最后的close关闭设备,代码:
int scull_release(struct inode *inode, struct file *filp) { return 0; }
五、scull的内存使用
scull使用的内存区,称为一个设备,长度可变,写的越多,增长越多;通过使用段文件覆盖设备来进行修整。
scull驱动引入2个核心函数来管理linux内核汇总的内存,定义在<linux/slab.h>
void *kmalloc(size_t size, int flags); void kfree(void *ptr);
分配zise字节的内存,flags是内存如何分配,一般使用GFP_KERNEL
然后用kfree释放
struct scull_qset结构
struct sucll_qset { void **data; struct scull_qset *next; };
scull_trim函数负责释放整个数据区,scull_open文件为写而打开时调用。
int scull_trim(struct scull_dev *dev) { struct scull_qset *next, *dptr; int qset = dev->qset; /* "dev" is not-null */ int i; for(dptr = dev->data; dptr; dptr = next) { /* all the list time */ if(dptr->data) { for(i = 0; i < qset; i++) kfree(dptr->data[i]); kfree(dptr->data); dptr->data = NULL; } next = dptr->next; kfree(dptr); } dev->size = 0; dev->quantum = scull_quantum; dev->qset = scull_qset; dev->data = NULL; return 0; }
六、读和写
函数原型:
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); filp是文件指针 count是请求的传输数据大小 buff参数指向持有被写入数据的缓存
read和write方法的buff参数是用户空间指针。因此,不能被内核代码直接解引用,如下理由:
- 依赖你的驱动运行的体系,以及内核被如何配置的,用户空间指针当运行于内核模式可能根本是无效的。
- 就算这个指针在内核空间是同样的东西,用户空间内存是分页的,在做系统调用时这个内存可能没有在RAM中。
- 用户提供的指针,可能是错误或恶意的。盲目街引用,提供了一个打开的门路使用户孔家内存区或覆盖系统任何地方的内存。
定义于<asm/uaccess.h>中,有特殊的技巧确保内核和用户空间数据传输安全和正确。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count); unsigned long copy_from_user(void *to, const __user *from, unsigned long count);
这两个函数的角色不限于拷贝数据到和从用户空间:他们还检查用户空间指针是否有效。如果无效,不进行拷贝
6.1 read方法
read的返回值由调用的应用程序解释:
- 若果这个值等于传递给read系统调用的count参数,请求的字节数已经被传送,这是最好的情况
- 如果是正数,但小于count,只有部分被传送。可能由于几个原因,依赖于设备。
- 如果值为0,到达了文件末尾
- 一个负值表示有一个错误,错误在<linux/errno.h>中
还有一种情况是,“没有数据后来到达”,所以read需要阻塞。
ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) { struct scull_dev *dev = filp->private_data; struct scull_qset *dptr; /* the first listitem */ int quantum = dev->quantum, qset = dev->qset; int itemsize = quantum * qset; /* how many bytes in the listitem */ int item, s_pos, q_pos, rest; ssize_t retval = 0; if (down_interruptible(&dev->sem)) return -ERESTARTSYS; if (*f_pos >= dev->size) goto out; if (*f_pos + count > dev->size) count = dev->size - *f_pos; /* find listitem, qset index, and offset in the quantum */ item = (long)*f_pos / itemsize; rest = (long)*f_pos % itemsize; s_pos = rest / quantum; q_pos = rest % quantum; /* follow the list up to the right position (defined elsewhere) */ dptr = scull_follow(dev, item); if (dptr == NULL || !dptr->data || ! dptr->data[s_pos]) goto out; /* don't fill holes */ /* read only up to the end of this quantum */ if (count > quantum - q_pos) count = quantum - q_pos; if (copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) { retval = -EFAULT; goto out; } *f_pos += count; retval = count; out: up(&dev->sem); return retval; }
6.2 write方法
返回值的规则:
- 等于count,字节数已被传送
- 正直,小于count,部分被传送。冲虚最可能重试写入剩下的数据
- 如果为0,什么都没写,如果不是错误,没理由返回错误码。再一次,标准库重试写调用。
- 一个负值表示发生错误,定义于<linux/errno.h>
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos) { struct scull_dev *dev = filp->private_data; struct scull_qset *dptr; int quantum = dev->quantum, qset = dev->qset; int itemsize = quantum * qset; int item, s_pos, q_pos, rest; ssize_t retval = -ENOMEM; /* value used in "goto out" statements */ if(down_interruptible(&dev->sem)) return -ERSETARTSYS; /* find listitem, qset index and offset in the quantum */ item = (long)*f_pos / itemsize; rest = (long)*f_pos % itemsize; s_pos = rest / quantum; q_pos = rest % quantum; /* follow the list up to the right position */ dptr = scull_follow(dev, item); if(dptr == NULL) goto out; if(!dptr->data) { dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL); if(!dptr->data) goto out; memset(dptr->data, 0, qset *sizeof(char *)); } if(!dptr->data[s_pos]) { dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL); if(!dptr->data[s_pos]) goto out; } /* write only up to the end of this quantum */ if(count > quantum - q_pos) count = quantum - q_pos; if(copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) { retval = -EFAULT; goto out; } *f_pos += count; retval = count; /* update the size */ if(dev->size < *f_pos) dev->size = *f_pos; out: up(&dev->sem); return retval; }
6.3 readv和writev
readv和writev是read和write的矢量版本,使用一个结构数组,每个包含一个缓存指针和一个长度值。
多次调用read和write可以用readv和writev来替代,从而获得更大的效率
函数原型:
ssize_t (*readv)(struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos); ssize_t (*writev)(struct file *filp, const struct iovec *iov, unsigned long count, loff_t *ppos);
iovec结构,定义于<linux/uio.h>
struct iovc { void __user *iov_base; __kernel_size_t iov_len; };