scull ,即“simple character utility for loading localities, 区域装载的简单字符工具’”。
是一个操作内存区域的字符设备驱动程序,这片内存区域就相当于一个字符设备。
获取设备号 -> 注册设备 -> 关联File operations结构 -> open(打开设备) -> write ->read -> release资源 -> close(关闭设备)
主设备号标识设备对应的驱动程序,次设备号由内核使用,用于正确确定设备文件所指的设备。
在内核中,dev_t类型用来保存设备编号,在
#include <linux/kedv_t.h> MAJOR(dev_t dev); MINOR(dev_t dev);如果需要将主设备号和次设备号转换成dev_t类型,则使用:
MKDEV(int major, int minor);在建立字符设备之前,驱动程序首先要做的是获得一个或多个设备的编号。完成该工作的必要函数是register_chrdev_region,该函数在
int register_chrdev_region(dev_t first, unsigned int count, char *name);first是要分配的设备编号范围起始值 ,其次设备号经常被置为0。count是所有请求的连续设备编号的个数,name是和该编号范围关联的设备名称,它将出现在/proc/devices和sysfs中。该函数分配成功时返回0,错误返回负的错误码。
以上的固定分配,有时不明确所需要的设备编号,故通常采用动态分配,函数如下:
int alloc_chrdev_region(dev_t, unsigned int firstminor, unsigned int count, char *name);dev 是一个只输出的参数, 它在函数成功完成时持有你的分配范围的第一个数。 fisetminor 应当是请求的第一个要用的次编号;它常常是 0。count 和 name 参数如同给 request_chrdev_region 的一样。
为了防止选定的设备号出现冲突和麻烦,驱动程序应尽量使用动态分配而不是静态分配。
不管哪种方法,不使用时需要释放设备号,函数如下:
void unregister_chrdev_region(dev_t first, unsigned int count);通常我们会在模块的清除函数中调用unregister_chrdev_region函数。
由于动态分配的主设备号无法预先创建设备节点,所以需要从/proc/devices中读取得到。可以将对insmod的调用替换成一个脚本,该脚本在调用insmod之后读取/proc/devices以获得分配的主设备号,然后创建对应的设备文件。其脚本为:
#!/bin/sh module="scull" device="scull" mode="664" # 使用传入到该脚本的所有参数调用insmod,同时使用路径名来指定具体模块位置 # 这是因为新的modutiles默认不会在当前目录中查找模块 /sbin/insmod ./$module.ko $* || exit 1 # 删除原有节点 rm -f /dev/${device}[0-3] major=$(awk "\\$2==\"$module\" {print \\$1}" /proc/devices) mknod /dev/${device}0 c $major 0 mknod /dev/${device}1 c $major 1 mknod /dev/${device}2 c $major 2 mknod /dev/${device}3 c $major 3 # 给予适当的组属性许可,并修改组 # 并非所有的发行版都具有staff组,有些有wheel组 group="staff" grep -q '^staff:' /etc/group || group="wheel" chgrp $group /dev/${device}[0-3] chmod $mode /dev/${device}[0-3]分配主设备号的最佳方式是:默认采用动态分配, 同时保留在加载甚至是编译时批定设备号的余地。常用的获取主设备号的代码为:
if (scull_major) { dev = MKDEV(scull_major, scull_minor); result = register_chrdev_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\n", scull_major); return result; }这种结构体初始化定义是C语言最新C99标准,称为指定初始化(designated initializer)。采用这种方式的优势就在于由此初始化不必严格按照定义时的顺序。这带来了极大的灵活性。
此file结构不是用户空间中的FILE,两者无关系,FILE是在C库中定义的,不会出现在内核代码中,而struct file是一个内核结构,它不会出现在用户程序中。file结构代表一个打开的文件,同open创建,直到最后的close之后才释放这个结构,指向struct file的指针为file和filp,其中file指的是结构本身,filp指向该结构的指针。
内核用inode结构在内部表示文件,和file结构不同,file表示打开的文件描述符,对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向单个inode结构。
struct cdev结构表示字符设备,获取一个独立的cdev结构并嵌入到自己的设备特定结构中,其代码如下:
struct cdev *my_cdev = cdev_alloc(); my_cdev->ops = &my_fops;之后初始化已分配到的结构:
void cdev_init(struct cdev *cdev, struct file_operations *fops);cdev的字段需有初始化,它有一个所有者字段,应该设置成THIS_MODULE。
cdev设置好后,通过下面代码告诉内核该结构的消息:
int cdev_add(struct cdev *dev, dev_t num, unsigned int count);dev 是 cdev 结构,num 是这个设备对应的第一个设备号,count 是应当关联到设备的设备号的数目.通常是1, 但在某些情况下,会有多个设备号对应于一个特定的设备。
如果要移除一个字符设备,做如下调用:
void cdev_del(struct cdev *dev);注册实例,scull:
struct scull_dev { //quantum,量子,一个内存区称为一个量子,而这个指针数组称为量子集 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原型:
int (*open)(struct inode *inode, struct file *filp);其作用为:
检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误如果它第一次打开, 初始化设备如果需要, 更新 f_op 指针.分配并填充要放进 filp->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 */ /* 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 */ }release方法作用与open相反,该方法实现被称为device_close或device_release,作用如下:
释放 open 分配在 filp->private_data 中的任何东西在最后的 close 关闭设备scull中的release代码:
int scull_release(struct inode *inode, struct file *filp) { return 0; }并不是每个close系统调用都会引起release方法的调用,只有真正释放设备数据的close调用才会调用这个方法,所以scull中的release代码这么少。
read和write原型相似,如下 :
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 参数指向用户空间的缓存区, 最后, offp 是一个指针指向一个”long offset type,长偏移类型”对象, 它指出用户正在存取的文件位置. 返回值是一个”signed size type,有符号的尺寸类型”;
大多数read和write方法实现的核心部分如下,用于拷贝任意的一段字节序列。
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);这两个函数的作用并不限于在内核空间和用户空间之间拷贝数据,它们还检查用户空间的指针是否有效。