字符设备驱动程序
主设备号和次设备号
-
主设备号表示设备对应的驱动程序。驱动程序在初始化时,会注册它的驱动及对应主设备号到系统中,可以通过/proc/devices文件来获取驱动系统设备的主设备号。
-
次设备号由内核使用,用于正确确定设备文件所指的设备。(可以通过次设备号获得一个指向内核设备的直接指针,也可将次设备号当作设备本地数组的索引)表示使用同一设备驱动程序的不同硬件设备。有两个LED指示灯,LED灯需要独立的打开或关闭。那么,可以写一个LED灯的字符设备驱动程序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。
驱动程序遍历设备时,每发现一个它能驱动的设备,就创建一个设备对象,并为其分配一个次设备号以区分不同的设备。
分配和释放设备编号
int register_chrdev_region(dev_t first, unsigned int count, char *name);
first:要分配的设备编号范围的起始值。fitst的次设备号经常被置为0,但对函数来讲并不是必需的。
count: 所请求的连续设备编号的个数。
name:和该编号范围关联的设备名称,它将出现在/proc/drivers和sysfs中
该函数多用于开发者提前明确知道所需要的设备编号。
int alloc_chrdev_region(dev_t *dev, unsigned int firstminor, unsigned int count, char* name);
dev是仅用于输出的参数,在成功完成调用后将保存已分配范围的第一个编号。
firstminor是要使用的被请求的第一个次设备号,它通常是0。
其他参数与第一个函数相同。
void unregister_chrdev_region(dev_t first, unsigned int count);
通常在模块的清除函数中调用该函数。
动态分配主设备号
问题:若驱动程序被广泛使用,则选定一个编号的方法可能造成冲突,因此,驱动程序应该使用动态分配机制获取主设备号。
动态分配主设备号的缺点是:
- 由于分配的主设备号不能保证始终一致,所以无法预先创建设备节点。
- 分配主设备号的最佳方式:默认采用动态分配,同时保留在加载甚至时编译时指定主设备号的余地。例如scull的实现采用:
使用一个全局变量scull_major,用来保存所选择的设备号。该变量的初始化值是SCULL_MAJOR,该宏定义在scull.h中。默认取0,即“选择动态分配”。代码如下
if(scull_major){//scull_major默认是0,如果改了就走这一条,所以调用register_chrdev_region()来分配
dev=MKDEV(scull_major,scull_minor);
result=register_chrdev_region(dev,scull_nr_devs,"scull");
}else{//因为scull_major仍然为0,所以证明没有自定义scull_major值,所以调用alloc_chrdev_region()来分配
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;
}
一些重要的数据结构
文件操作(file_operations
)
file_operations
结构用来建立设备编号和驱动程序的连接。
这个结构定义在<linux/fs.h>中,其中包含了一组函数指针。
每个打开的文件(在内部由一个file结构表示)和一组函数关联(通过包含指向一个file_operations结构的f_op字段)。这些操作主要用来实现系统调用,命名为open、read等等。我们可以认为文件是一个“对象”,而操作它的函数是“方法”,如果采用面向对象编程的术语来表达就是,对象声明的动作将作用于其本身。
file_operations
结构或者指向这类结构的指针称为fops(或者是与此相关的其他叫法)。这个结构中的每一个字段,都必须指向驱动程序中实现特定操作的函数,对于不支持的操作,对应的字段可置为NULL。
-
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 *, char __user *, size_t, loff_t *);
-
ssize_t (*aio_write) (struct kiocb *, char __user *, size_t, loff_t);
-
int (*readdir) (struct file *, void *, filldir_t);
对于设备文件来说,这个字段应该为NULL。它仅用于读取目录,支队文件系统有用。
- unsigned int (*poll) (struct file *, struct poll_table_struct *);
poll方法是poll、epoll和select这三个系统调用的后端实现。这三个系统调用可用来查询某个或多个文件描述符上的读取或写入是否会被阻塞。poll方法应该返回一个位掩码,用来指出非阻塞的读取或写入是否可能,并且也会向内核提供将调用进程置于休眠状态直到I/O变为可能时的信息。如果驱动程序将poll方法定义为NULL,则设备会被认为既可读也可写,并且不会被阻塞。(io多路复用)
-
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
-
int (*mmap) (struct file *, struct vm_area_struct *);
mmap用于请求将设备内存映射到进程地址空间。如果设备没有实现这个方法,那么mmap系统调用将返回-ENODEV。
-
int (*open) (struct inode *, struct file *);
-
int (*flush) (struct 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 *);
-
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
-
int (*check_flags) (int )
-
int (*dir_notify)(struct file *, unsigned long);
inode结构
内核用inode结构在内部表示文件。对单个文件,可能会有许多个表示打开的文件描述符的file结构,但它们都指向耽搁inode结构。
dev_t i_rdev;
对表示设备文件的inode结构,该字段包含了真正的设备编号。
struct cdev *i_cdev;
struct cdev是表示字符设备的内核的内部结构。当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。
字符设备的注册
内核内部使用struct cdev结构来表示字符设备。
Scull中的设备注册
struct scull_dev{
struct scull_qset *data; //指向第一个量子级指针
int quantum;//当前量子的大小
int qset;//当前数组的大小
unsigned long size;//保存在其中的数据总量
unsigned int access_key;//由sculluid和scullpriv使用
struct semaphore sem;//互斥信号量
struct cdev 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;
error=cdev_add(&dev->cdev,devno,1);
/* Fail gracefully if need be*/
printk(KERN_NOTICE "Error %d adding scull%d",error,index);
}
cdev_init的分析
在内核的角度来看,一个cdev结构体就是一个字符设备。
struct cdev{
struct kobject kobj;//内嵌的内核对象
struct module *owner;//该字符设备所在的内核模块的对象指针
const struct file_operations *ops;//描述了字符设备所能实现的方法
struct list_head list;//用来将已经向内核注册的所有字符设备形成链表
dev_t dev;//字符设备的设备号,由主设备号和次设备号构成
unsigned int count;//隶属于同一主设备号的次设备号的个数
}__randomize_layout;
/**
* cdev_init() - 初始化一个cdev结构体
* @cdev: 将要被初始化的结构体指针
* @fops: 该设备对应的文件操作函数
*/
void cdev_init(struct cdev *cdev, struct file_operations *fops)
{
memset(cdev,0,sizeof(cdev));//首先将cdev对应的空间进行清0操作
INIT_LIST_HEAD(&cdev->list);//初始化list变量为双向环形链表的头节点
cdev->kobj.ktype=&ktype_cdev_default;
kobject_init(&cdev->kobj);
cdev->ops=fops;
}
//初始化list域的next指针和prev指针指向自己
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next=list;
list->prev=list;
}
struct kref{
atomic_t refcount;
};
// 内核对象
struct kobject {
/**
* 指向容器名称。
*/
char * k_name;
/**
* 如果容器名称不超过20个字符,就存在这里。
*/
char name[KOBJ_NAME_LEN];
/**
* 容器的引用计数。
*/
struct kref kref;
/**
* 用于将kobject插入某个链表。
*/
struct list_head entry;
/**
* 指向父kobject
*/
struct kobject * parent;
/**
* 指向包含的kset,kset是同类型的kobject结构的一个集合体。
*/
struct kset * kset;
/**
* 指向kobject的类型描述符。
*/
struct kobj_type * ktype;
/**
* 指向与kobject对应的sysfs文件的dentry数据结构。
*/
struct dentry * dentry;
};
/**
* 部分初始化kobject对象。
*/
void kobject_init(struct kobject * kobj)
{
kref_init(&kobj->kref); // 引用置1
INIT_LIST_HEAD(&kobj->entry); // 初始化双向链表
kobj->kset = kset_get(kobj->kset); // 找到其所对应的内核对象集合
}
static inline void kref_init(struct kref *kref)
{
atomic_set(&kref->refcount, 1);
}
read和write
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);
返回值是还需要拷贝的内存数量值。