嵌入式Linux之字符设备驱动
本文档从Linux字符设备入手,描述一个字符设备在内核编译产生,进而在应用层被调用的过程。通过对字符设备的研究,一窥Linux设备驱动程序的工作机制。
Linux设备分为三类,依次为:字符设备,块设备,网络接口设备。
字符设备(char)是能通过字节流一样访问的设备,串口和键盘就是典型的字符设备。如果一个设备能以字符流的方式被访问,那就可以将其归结为字符设备。块设备是指那些可以被随机访问的固定大小数据片(chuck,通常512字节)的设备,例如硬盘。字符设备和块设备的区别如下:
- 最主要差别:字符设备只能被连续访问,而块设备可以被随机访问
- 块设备通过系统缓存进行读取,不是直接从物理磁盘读取;字符设备直接从物理磁盘读取(例如键盘,直接进相应中断)
本文档以一个简单的字符设备驱动scull为例,阐述一个字符设备驱动程序的编写过程。scull是一个操作内存区域的字符设备驱动程序,这片内存区域相当于一个设备,scull设备的优点在于其与硬件无关,因而可操作性强。
1. 主设备号和次设备号
一个设备在在应用层程序中被分为主设备号和从设备号,主设备号表示设备对应的驱动程序,从设备号供内核使用,用于正确确定设备文件所确定的设备。
主从设备号相应的结构定义在内核源代码头文件中的,主要的结构有两个:
typedef __u32 __kernel_dev_t;
typedef __kernel_fd_set fd_set;
typedef __kernel_dev_t dev_t;
Linux中用dev_t表示设备的主设备号和从设备号。可以看出他是一个32位无符号类型,12位表示主设备,20位表示从设备。
在linux/kdev_t.h文件中,定义了dev_t类型向主,从设备号转化的宏:
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
从中可以看出,dev_t的高12为被定义成主设备号,低20位被定义为从设备号。
2. 设备号的分配
在明确了一个设备的主从设备号之后,需要做的工作时获得设备编号。这主要是通过声明在linux/fs.h文件中的设备注册函数实现的。
extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);
extern int register_chrdev_region(dev_t, unsigned, const char *);
在明确了要分配的主从设备号之后,可以通过register_chrdev_region函数进行设备号的注册,其中的const char*型变量指定了与该设备号相关联的设备名,这个设备名将出现在/proc/devices和sysfs中。
当不明确要分配的设备号时,可以通过alloc_chrdev_region函数进行动态分配,注意其第一个参数为dev_t *,指针类型,可以再该函数体内被修改,最终得到一个被动态分配的设备号。
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;
}
分析这段代码,如果指定了对应的主设备号,则采用静态分配的方式分配设备号,如果没有指定对应的主设备号,则采用动态分配的方式分配设备号。
3. 三个重要的数据结构
设备号的注册仅仅是驱动程序代码完成的第一步工作,后面的驱动程序设计中,涉及到三个重要的数据结构,file_opertions,file和inode。
3.1 文件操作结构file_opertions
前面所述及的,为设备驱动注册的了设备编号,文件操作则将驱动程序连接到这些编号。内核通过file_opertions建立这种连接。这个结构定义在、linux/fs.h中,包含了一组函数指针。每一个打开的文件(通过file结构表示),均通过文件操作执行。通常情况下,针对编写的驱动程序类型,定义相应的file_opertions类型的结构体,经常将其命名为fops,意为文件操作。在该结构体中,针对那些该设备驱动需要的函数进行初始化,对于那些不需要支持的操作,可以将其赋值为NULL。以scull驱动为例,定义了scull_fops结构体,其成员初始化如下:
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,
};
上述结构体中,owner,llseek,read,write等均是file_opertion结构体内部的成员函数。在该字符设备驱动程序中,仅仅针对需要的成员函数进行了初始化,其他一些函数因为该驱动并未支持,故没有进行初始化。这里仅仅介绍用到的成员函数。
struct module *owner
第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在
<linux/module.h>
中定义的宏.
loff_t (*llseek) (struct file *, loff_t, int);
llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个”long offset”, 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在”file 结构” 一节中描述).
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL(“Invalid argument”) 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 “signed size” 类型, 常常是目标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.
int (*ioctl) (struct inode\ , struct file\ , unsigned int, unsigned long);
ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, “设备无这样的 ioctl”), 系统调用返回一个错误.
int (*open) (struct inode *, struct file *);
尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.
int (*release) (struct inode\ *, struct file *);
在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL。
3.2 file结构
struct file 结构是设备驱动程序所使用的第二个重要的数据结构,此处的file和用户空间程序中的FILE没有任何关系。FILE定义在C库且不会出现在内核代码中,而file结构体定义在linux/fs.h文件中,可被内核调用。
file结构表示一个打开的文件(不仅仅是设备驱动程序,系统中每一个打开的文件都在内核空间有一个对应的一个file文件)。该file文件在open时创建,并传递给在该文件上进行操作的所有函数,直到最后的close函数。在内核源代码中,指向file的指针被命名为filp。
至于file结构体内部的成员,简要的列举,后面会看到对这些成员的初始化,到时在对这些成员做详细解释。
mode_t f_mode:文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE。
loff_t f_pos:当前读写位置。
unsigned int f_flags:文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC。
struct file_operations *f_op:
内核在执行open操作时对这个指针进行赋值,以后需要处理文件操作时就取这个指针。
void *private_data;
open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 可以使用这个成员或者忽略它; 活着可以使用这个成员来指向分配的数据, 但是必须在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间可以用来保留状态信息。
struct dentry *f_dentry;
关联到文件的目录入口( dentry )结构。
3.3 inode结构
内核用inode结构在内部表示文件,因此它和file结构不同,后者表示打开的文件描述符,对单个文件,可能会有多个表示打开的文件描述符file结构,但它们都指向同一个inode。
inode结构体中包含了大量的信息,但针对驱动程序有用的数据只有两个:
dev_t i_rdev;
对于表示设备文件的inode结构,这个字段包含了真正的设备编号。
struct cdev *i_cdev;
struct cdev是表示字符设备内核的内部结构,当inode指向一个字符设备文件时,该字段包含了指向struct cdev结构的指针。struct cdev结构定义在linux/Cdev.h文件中,结构如下:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
可以看出其中包含了文件操作和设备编号。
4. 字符设备的注册
上面提到,内核内部使用struct cdev结构体来表示字符设备。字符设备的注册,简单的理解,就是该结构体的初始化。其中最重要的就是对文件操作ops的初始化。
例如:
struct cdev *my_cdev =cdev_alloc(); //分配空间
my_cdev->ops=&my_fops; //初始化文件操作
当然,可以将cdev结构内嵌到自定义的设备结构中,之后可以用如下代码初始化已分配到的结构。
void cdev_init(struct cdev *cdev,struct file_opertions *fops);
在cdev结构设置好之后,通过下面的调用告诉内核该结构的信息:
int cdev_add(struct cdev *dev,dev_t num,unsigned int count);
以设计的scull结构为例,定义scull设备结构体:
struct scull_dev{
struct scull_qset *data;
int quantum;
int qset;
unsigned long size;
unsigned int access_key;
struct semaphore sem;
struct cdev cdev;
};
scull的初始化代码如下:
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);
if (err)
printk(KERN_NOTICE “Error %d adding scull%d”, err, index);
}
5. open和release函数
open方法提供给驱动程序以初始化的能力,为以后的操作作准备。应完成的工作如下:
- 检查设备特定的错误(如设备未就绪或硬件问题)
- 如果设备是首次打开,则对其进行初始化
- 如有必要,更新f_op指针
- 分配并填写置于filp->private_data里的数据结构
int (*open)(struct inode *inode, struct file *filp);
inode 参数有我们需要的信息,以它的 i_cdev 成员的形式, 里面包含我们之前建立的 cdev 结构. 唯一的问题是通常我们不想要 cdev 结构本身, 我们需要的是包含 cdev 结构的 scull_dev 结构.内以 container_of 宏的形式实现这种转换, 在 <linux/kernel.h>
中定义。
而根据scull的实际情况,他的open函数只要完成第四步(将初始化过的struct scull_dev dev的指针传递到filp->private_data里,以备后用)就好了,所以open函数很简单。但是其中用到了定义在<linux/kernel.h>
中的container_of宏,源码如下:
#define container_of(ptr, type, member) ({
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char\ *)__mptr - offsetof(type,member) );})
这个宏使用一个指向 container_field 类型的成员的指针, 它在一个 container_type 类型的结构中, 并且返回一个指针指向包含结构. 在 scull_open, 这个宏用来找到适当的设备结构:
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方法提供释放内存,关闭设备的功能。应完成的工作如下:
(1)释放由open分配的、保存在file->private_data中的所有内容;
(2)在最后一次关闭操作时关闭设备。
由于前面定义了scull是一个全局且持久的内存区,所以他的release什么都不做。
int scull_release(struct inode *inode, struct file *filp) { return 0; }
6.read和write函数
read()函数用于从已打开的文件读取数据
表头文件: #include<unistd.h>
定义函数: ssize_t read(int fd,void * buf ,size_t count);
函数说明: read()会把参数fd 所指的文件传送count个字节到buf指针所指的内存中。若参数count为0,则read()不会有作用并返回0。返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。
附加说明 如果顺利read()会返回实际读到的字节数,最好能将返回值与参数count 作比较,若返回的字节数比要求读取的字节数少,则有可能读到了文件尾、从管道(pipe)或终端机读取,或者是read()被信号中断了读取动作。当有错误发生时则返回-1,错误代码存入errno中,而文件读写位置则无法预期。
write()函数用于将数据写入已打开的文件内。
表头文件: #include<unistd.h>
定义函数: ssize_t write (int fd,const void * buf,size_t count);
函数说明: write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。当然,文件读写位置也会随之移动。
返回值 如果顺利write()会返回实际写入的字节数。当有错误发生时则返回-1,错误代码存入errno中。