原文出处:
http://blog.youkuaiyun.com/21cnbao/article/details/7526159
宋老师的排版不太好,我整理了一下 ,转载到自己的博客空间。
第六章 字符设备驱动
本章导读
在整个Linux设备驱动的学习中,字符设备驱动较为基础。本章将展示Linux字符设备驱动程序的结构,并解释其主要组成部分的编程方法。6.1节讲解了Linux字符设备驱动的关键数据结构cdev及file_operations结构体的操作方法,并分析了Linux字符设备的整体结构,给出了简单的设计模板。
6.2节描述了本章及后续各章节所基于的globalmem虚拟字符设备,6~9章都将基于该虚拟设备实例进行字符设备驱动及并发控制等知识的讲解。
6.3节依据6.1节的知识讲解globalmem设备的驱动编写方法,对读写函数、seek()函数和IO控制函数等进行了重点分析。该节的最后改造globalmem的驱动程序以利用文件私有数据。
6.4节给出了6.3的globalmem设备驱动在用户空间的验证。
6.1 Linux字符设备驱动结构
6.1.1 cdev结构体
在Linux 2.6内核中,使用 cdev 结构体描述一个字符设备,cdev 结构体的定义如代码清单6.1。代码清单 6.1 cdev 结构体
struct cdev {
struct kobject kobj; /* 内嵌的kobject对象 */
struct module *owner; /* 所属模块 */
struct file_operations *ops; /* 文件操作结构体 */
struct list_head list;
dev_t dev; /* 设备号 */
unsigned int count;
};
cdev结构体的dev_t成员定义了设备号,为32位,其中12位主设备号,20位次设备号。使用下列宏可以从dev_t获得主设备号和次设备号:
MAJOR(dev_t dev)
MINOR(dev_t dev)
而使用下列宏则可以通过主设备号和设备号生成 dev_t:
MKDEV(int major, int minor)
cdev 结构体的另一个重要成员 file_operations 定义了字符设备驱动提供给虚拟文件系统的接口函数。
Linux 2.6 内核提供了一组函数用于操作 cdev 结构体:
void cdev_init (struct cdev *, struct file_operations *);
struct cdev *cdev_alloc(void);
void cdev_put (struct cdev *p);
int cdev_add (struct cdev *, dev_t, unsigned);
void cdev_del (struct cdev *);
cdev_init() 函数用于初始化 cdev 的成员,并建立 cdev 和 file_operations 之间的连接,其源代码如清单6-2。
代码清单6.2 cdev_init() 函数
void cdev_init(struct cdev *cdev, struct file_operations *fops) {
memset(cdev, 0, sizeof *cdev);
INIT_LIST_HEAD(&cdev->list);
kobject_init(&cdev->kobj, &ktype_cdev_default);
cdev->ops = fops; /* 将传入的文件操作结构体指针赋值给 cdev 的 ops */
}
cdev_alloc() 函数用于动态申请一个 cdev 内存,其源代码如清单6-3。
代码清单6.3 cdev_alloc() 函数
struct cdev *cdev_alloc(void) {
struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
if (p) {
INIT_LIST_HEAD(&p->list);
kobject_init(&p->kobj,&ktype_cdev_dynamic);
}
return p;
}
cdev_add() 函数和 cdev_del() 函数分别向系统添加和删除一个cdev,完成字符设备的注册和注销。对 cdev_add() 的调用通常发生在字符设备驱动模块加载函数中,而对 cdev_del() 函数的调用则通常发生在字符设备驱动模块卸载函数中。
6.1.2分配和释放设备号
在调用 cdev_add() 函数向系统注册字符设备之前,应首先调用 register_chrdev_region() 或 alloc_chrdev_region() 函数向系统申请设备号,这两个函数的原型为:int register_chrdev_region(dev_t from, unsigned count, const char *name);
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);
register_chrdev_region()函数用于已知起始设备的设备号的情况,而 alloc_chrdev_region() 用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数 dev 中。
alloc_chrdev_region() 与 register_chrdev_region() 对比的优点在于它会自动避开设备号重复的冲突。
相反地,在调用 cdev_del() 函数从系统注销字符设备之后,unregister_chrdev_region() 应该被调用以释放原先申请的设备号,这个函数的原型为:
void unregister_chrdev_region(dev_t from, unsigned count);
6.1.3 file_operations结构体
file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行 Linux 的 open()、write()、read()、close() 等系统调用时最终被调用。file_operations 结构体目前已经比较庞大,它的定义如代码清单6.4。
代码清单6.4 file_operations 结构体
struct file_operations {
struct module *owner; /* 拥有该结构的模块的指针,一般为THIS_MODULES */
loff_t (*llseek) (struct file *, loff_t, int); /* 用来修改文件当前的读写位置 */
ssize_t (*read) (struct file *, char __user*, size_t, loff_t*); /* 从设备中同步读取数据 */
ssize_t (*write) (struct file *, const char__user *, size_t, loff_t*); /* 向设备发送数据 */
ssize_t (*aio_read) (struct kiocb *, char__user *, size_t, loff_t); /* 初始化一个异步的读取操作*/
ssize_t (*aio_write) (struct kiocb *, constchar __user *, size_t, loff_t); /* 初始化一个异步的写入操作 */
int (*readdir) (struct file *, void *,filldir_t); /* 仅用于读取目录,对于设备文件,该字段为 NULL */
unsigned int (*poll) (struct file *, struct poll_table_struct *); /* 轮询函数,判断目前是否可以进行非阻塞的读取或写入 */
int (*ioctl) (struct inode *, struct file *,unsigned int, unsigned long); /* 执行设备IO控制命令 */
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); /* 不使用BLK的文件系统,将使用此种函数指针代替ioctl */
long (*compat_ioctl) (struct file *, unsignedint, unsigned long); /* 在64位系统上,32位的ioctl调用,将使用此函数指针代替 */
int (*mmap) (struct file *, structvm_area_struct*); /* 用于请求将设备内存映射到进程地址空间 */
int (*open) (struct inode *, struct file *); /* 打开 */
int (*flush) (struct file *);
int (*release) (struct inode *, struct file *); /* 关闭 */
int (*fsync) (struct file *, struct dentry *, int datasync); /* 刷新待处理的数据 */
int (*aio_fsync) (struct kiocb *, intdatasync); /* 异步fsync */
int (*fasync) (int, struct file *, int); /* 通知设备FASYNC标志发生变化 */
int (*lock) (struct file *, int, structfile_lock*);
ssize_t (*sendpage) (struct file *, structpage *, int, size_t, loff_t *, int); /* 通常为NULL */
unsigned long(*get_unmapped_area)(structfile *,unsigned long, unsigned long, unsigned long, unsigned long); /* 在当前进程地址空间找到一个未映射的内存段 */
int (*check_flags) (int); /* 允许模块检查传递给fcntl(F_SETEL...)调用的标志 */
int (*dir_notify) (struct file *filp, unsignedlong arg); /* 对文件系统有效,驱动程序不必实现 */
int (*flock) (struct file *, int, structfile_lock *);
ssize_t (*splice_write) (struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); /* 由VFS调用,将管道数据粘接到文件 */
ssize_t (*splice_read) (struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); /* 由VFS调用,将文件数据粘接到管道 */
int (*setlease) (struct file *, long, struct file_lock **);
};
下面我们对 file_operations 结构体中的主要成员进行分析:
llseek() 函数用来修改一个文件的当前读写位置,并将新位置返回,在出错时,这个函数返回一个负值。
read() 函数用来从设备中读取数据,成功时函数返回读取的字节数,出错时返回一个负值。
write() 函数向设备发送数据,成功时该函数返回写入的字节数。如果此函数未被实现,当用户进行 write() 系统调用时,将得到 -EINVAL 返回值。
readdir() 函数仅用于目录,设备节点不需要实现它。
ioctl() 提供设备相关控制命令的实现(既不是读操作也不是写操作),当调用成功时,返回给调用程序一个非负值。
mmap() 函数将设备内存映射到进程内存中,如果设备驱动未实现此函数,用户进行 mmap() 系统调用时将获得 -ENODEV 返回值。这个函数对于帧缓冲等设备特别有意义。
当用户空间调用 Linux API 函数 open() 打开设备文件时,设备驱动的 open() 函数最终被调用。
驱动程序可以不实现这个函数,在这种情况下,设备的打开操作永远成功。
与 open() 函数对应的是 release() 函数。
poll() 函数一般用于询问设备是否可被非阻塞的立即读写。当询问的条件未触发时,用户空间进行 select() 和 poll() 系统调用将引起进程的阻塞。
aio_read() 和 aio_write() 函数分别对与文件描述符对应的设备进行异步读、写操作。
设备实现这两个函数后,用户空间可以对该设备文件描述符调用 aio_read()、aio_write() 等系统调用进行读写。
6.1.4 Linux 字符设备驱动的组成
在 Linux 中,字符设备驱动由如下几个部分组成:字符设备驱动模块加载与卸载函数
在字符设备驱动模块加载函数中应该实现设备号的申请和cdev的注册,而在卸载函数中应实现设备号的释放和cdev的注销。
工程师通常习惯为设备定义一个设备相关的结构体,其包含该设备所涉及到的 cdev 、私有数据及信号量等信息。常见的设备结构体、模块加载和卸载函数形式如代码清单6.5。
代码清单6.5 字符设备驱动模块加载与卸载函数模板
/* 设备结构体 */
struct xxx_dev_t {
struct cdev cdev;
...
} xxx_dev;
/* 设备驱动模块加载函数
static int __init xxx_init(void)
{
...
cdev_init(&xxx_dev.cdev, &xxx_fops); /* 初始化 cdev */
xxx_dev.cdev.owner = THIS_MODULE;
/* 获取字符设备号 */
if (xxx_major) {
register_chrdev_region(xxx_dev_no, 1, DEV_NAME);
} else {
alloc_chrdev_region(&xxx_dev_no, 0, 1, DEV_NAME);
}
ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); /* 注册设备 */
...
}
/* 设备驱动模块卸载函数 */
static void __exit xxx_exit(void)
{
unregister_chrdev_region(xxx_dev_no, 1); /* 释放占用的设备号 */
cdev_del(&xxx_dev.cdev); /* 注销设备 */
...
}
字符设备驱动的 file_operations 结构体中成员函数
file_operations 结构体中成员函数是字符设备驱动与内核的接口,是用户空间对 Linux 进行系统调用最终的落实者。大多数字符设备驱动会实现 read()、write() 和 ioctl() 函数,常见的字符设备驱动这3个函数的形式如代码清单6.6。
代码清单6.6 字符设备驱动读、写、IO控制函数模板
/* 读设备 */
ssize_t xxx_read(struct file *filp, char__user *buf, size_t count, loff_t *f_pos) {
...
copy_to_user(buf, ..., ...);
...
}
/* 写设备 */
ssize_t xxx_write(struct file *filp, const char__user *buf, size_t count, loff_t *f_pos) {
...
copy_from_user(..., buf, ...);
...
}
/* ioctl函数 */
int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg) {
...
switch(cmd) {
case XXX_CMD1:
...
break;
caseXXX_CMD2:
...
break;
default:
/* 不能支持的命令 */
return - ENOTTY;
}
return0;
}
设备驱动的写函数中,filp 是文件结构体指针,buf 是用户空间内存的地址,该地址在内核空间不能直接读写,count 是要写的字节数,f_pos 是写的位置相对于文件开头的偏移。
由于内核空间与用户空间的内存不能直接互访,因此借助了函数 copy_from_user() 完成用户空间到内核空间的拷贝,以及 copy_to_user() 完成内核空间到用户空间的拷贝,见代码第6行和第14行。
完成内核空间和用户空间内存拷贝的 copy_from_user() 和 copy_to_user() 的原型分别为:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long count);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long count);
上述函数均返回不能被复制的字节数,因此,如果完全复制成功,返回值为0。
如果要复制的内存是简单类型,如 char、int、long 等,则可以使用简单的 put_user() 和 get_user() ,如:
intval; /* 内核空间整型变量 */
...
get_user(val, (int *)arg); /* 用户->内核,arg 是用户空间的地址 */
...
put_user(val, (int *)arg); /* 内核->用户,arg是用户空间的地址 */
读和写函数中的 __user 是一个宏,表明其后的指针指向用户空间,这个宏定义为:
#ifdef __CHECKER__
#define __user __attribute__((noderef, address_space(1)))
#else
#define __user
#endif
IO 控制函数的 cmd 参数为事先定义的 IO 控制命令,而 arg 为对应于该命令的参数。譬如对于串行设备,如果 SET_BAUDRATE 是一道设置波特率的命令,那后面的 arg 就应该是波特率值。
在字符设备驱动中,需要定义一个 file_operations 的实例,并将具体设备驱动的函数赋值给 file_operations 的成员,如代码清单6.7。
代码清单6.7 字符设备驱动文件操作结构体模板
struct file_operations xxx_fops = {
.owner = THIS_MODULE,
.read = xxx_read,
.write = xxx_write,
.ioctl = xxx_ioctl,
...
};
上述 xxx_fops 在代码清单6.5第10行的 cdev_init(&xxx_dev.cdev, &xxx_fops) 的语句中被建立与 cdev 的连接。
图6.1描述了字符设备驱动的结构,字符设备驱动与字符设备以及字符设备驱动与用户空间访问该设备的程序之间的关系。

6.2 globalmem 虚拟设备实例描述
从本章开始,后续的数章都将基于虚拟的 globalmem 设备进行字符设备驱动的讲解。globalmem 意味着“全局内存”,在 globalmem 字符设备驱动中会分配一片大小为 GLOBALMEM_SIZE(4KB)的内存空间,并在驱动中提供针对该片内存的读写、控制和定位函数,以供用户空间的进程能通过 Linux 系统调用访问这片内存。实际上,这个虚拟的 globalmem 设备几乎没有任何实用价值,仅仅是一种为了讲解问题的方便而凭空制造的设备。当然,它也并非百无一用,由于 globalmem 可被2个或2个以上的进程同时访问,其中的全局内存可作为用户空间进程进行通信的一种蹩脚的手段。
本章下面的一节将给出 globalmem 设备驱动的雏形,而后续章节会在这个雏形的基础上初步添加并发与同步控制等复杂功能。
6.3 globalmem设备驱动
6.3.1头文件、宏及设备结构体
在 globalmem 字符设备驱动中,应包含它要使用的头文件,并定义 globalmem 设备结构体及相关宏。代码清单6.8 globalmem 设备结构体和宏
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#define GLOBALMEM_SIZE 0x1000 /* 全局内存大小:4K字节 */
#define MEM_CLEAR 0x1 /* 清0全局内存 */
#define GLOBALMEM_MAJOR 250 /* 预设的 globalmem 的主设备号 */
static int globalmem_major = GLOBALMEM_MAJOR;
/* globalmem 设备结构体 */
struct globalmem_dev {
struct cdev cdev; /* cdev 结构体 */
unsigned char mem[GLOBALMEM_SIZE]; /* 全局内存 */
};
struct globalmem_dev dev; /* 设备结构体实例 */
从第19~22行代码可以看出,定义的 globalmem_dev 设备结构体包含了对应于 globalmem 字符设备的 cdev、使用的内存 mem[GLOBALMEM_SIZE]。当然,程序中并不一定要把 mem[GLOBALMEM_SIZE] 和 cdev 包含在一个设备结构体中,但这样定义的好处在于,它借用了面向对象程序设计中“封装”的思想,体现了一种良好的编程习惯。
6.3.2加载与卸载设备驱动
globalmem 设备驱动的模块加载和卸载函数遵循代码清单6.5的类似模板,其实现的工作与代码清单6.5完全一致,如代码清单6.9。代码清单6.9 globalmem 设备驱动模块加载与卸载函数
/* globalmem 设备驱动模块加载函数 */
int globalmem_init(void)
{
int result;
dev_t devno = MKDEV(globalmem_major, 0);
/* 申请字符设备驱动区域 */
if (globalmem_major)
result = register_chrdev_region(devno, 1,"globalmem");
else {
/* 动态获得主设备号 */
result = alloc_chrdev_region(&devno,0, 1, "globalmem");
globalmem_major = MAJOR(devno);
}
if (result < 0)
return result;
globalmem_setup_cdev();
return 0;
}
/* globalmem 设备驱动模块卸载函数 */
void globalmem_exit(void)
{
cdev_del(&dev.cdev); /* 删除 cdev 结构 */
unregister_chrdev_region(MKDEV(globalmem_major, 0), 1); /* 注销设备区域 */
}
第18行调用的 globalmem_setup_cdev() 函数完成 cdev 的初始化和添加,如代码清单6.10。
代码清单6.10 初始化并添加 cdev 结构体
/*初始化并添加 cdev 结构体*/
static void globalmem_setup_cdev(void) {
int err, devno = MKDEV(globalmem_major, 0);
cdev_init(&dev.cdev, &globalmem_fops);
dev.cdev.owner = THIS_MODULE;
err = cdev_add(&dev.cdev, devno, 1);
if (err)
printk(KERN_NOTICE "Error %d addingglobalmem", err);
}
在 cdev_init() 函数中,与 globalmem 的 cdev 关联的 file_operations 结构体如代码清单6.11。
代码清单6.11 globalmem 设备驱动文件操作结构体
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.ioctl = globalmem_ioctl,
};
6.3.3读写函数
globalmem 设备驱动的读写函数主要是让设备结构体的 mem[] 数组与用户空间交互数据,并随着访问的字节数变更返回给用户的文件读写偏移位置。读和写函数的实现分别如代码清单6.12和6.13。代码清单6.12 globalmem 设备驱动读函数
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret = 0;
/* 分析和获取有效的读长度 */
if (p >= GLOBALMEM_SIZE) /* 要读的偏移位置越界 */
return 0;
if (count > GLOBALMEM_SIZE - p) /* 要读的字节数太大 */
count = GLOBALMEM_SIZE - p;
/* 内核空间->用户空间 */
if (copy_to_user(buf, (void *)(dev.mem + p), count))
ret = - EFAULT;
else {
*ppos += count;
ret = count;
printk(KERN_INFO "read %d bytes(s)from %d\n", count, p);
}
return ret;
}
代码清单6.13 globalmem设备驱动写函数
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
int ret = 0;
/* 分析和获取有效的写长度 */
if (p >= GLOBALMEM_SIZE) /* 要写的偏移位置越界 */
return 0;
if (count > GLOBALMEM_SIZE - p) /* 要写的字节数太多
count = GLOBALMEM_SIZE - p;
/*用户空间->内核空间*/
if (copy_from_user(dev.mem + p, buf, count))
ret = - EFAULT;
else {
*ppos += count;
ret= count;
printk(KERN_INFO "written %d bytes(s)from %d\n", count, p);
}
return ret;
}
6.3.4 seek 函数
seek() 函数对文件定位的起始地址可以是文件开头(SEEK_SET, 0)、当前位置 (SEEK_CUR, 1) 和文件尾 (SEEK_END, 2) ,globalmem 支持从文件开头和当前位置相对偏移。在定位的时候,应该检查用户请求的合法性,若不合法,函数返回 - EINVAL,合法时返回文件的当前位置,如代码清单6.14。
代码清单6.14 globalmem 设备驱动 seek() 函数
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) {
loff_t ret;
switch (orig) {
case 0: /* 从文件开头开始偏移 */
if (offset < 0) {
ret = - EINVAL;
break;
}
if ((unsigned int)offset > GLOBALMEM_SIZE) {
ret = - EINVAL;
break;
}
filp->f_pos = (unsigned int)offset;
ret = filp->f_pos;
break;
case 1: /* 从当前位置开始偏移 */
if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
ret = - EINVAL;
break;
}
if ((filp->f_pos + offset) < 0) {
ret = - EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = - EINVAL;
}
return ret;
}
6.3.5 ioctl 函数
1、globalmem 的 ioctl() 函数globalmem 的 ioctl() 函数接受 MEM_CLEAR 命令,这个命令会将全局内存的有效数据长度清0,对于设备不支持的命令,ioctl()函数应该返回- EINVAL,如代码清单6.15。
代码清单6.15 globalmem 设备驱动 IO 控制函数
static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case MEM_CLEAR:
/* 清除全局内存 */
memset(dev->mem, 0, GLOBALMEM_SIZE);
printk(KERN_INFO "globalmem is set tozero\n");
break;
default:
return - EINVAL; /* 其他不支持的命令 */
}
return 0;
}
在上述程序中,MEM_CLEAR 被宏定义为0x01,实际上并不是一种值得推荐的方法,简单地对命令定义为0x0、0x1、0x2等类似值会导致不同的设备驱动拥有相同的命令号。如果设备A、B都支持0x0、0x1、0x2这样的命令,假设用户本身希望给A发0x1命令,可是不经意间发给了B,这个时候B因为支持该命令,它就会执行该命令。因此,Linux内核推荐采用一套统一的ioctl()命令生成方式。
2、ioctl() 命令
Linux 建议以如图6.2所示的方式定义 ioctl() 的命令。
图6.2 IO控制命令的组成
设备类型 | 序列号 | 方向 | 数据尺寸 |
8 bit | 8 bit | 2 bit | 13/14 bit |
命令码的序列号也是8位宽。
命令码的方向字段为2位,该字段表示数据传送的方向,可能的值是_IOC_NONE(无数据传输)、 _IOC_READ(读)、_IOC_WRITE(写)和_IOC_READ|_IOC_WRITE(双向)。数据传送的方向是从应用程序的角度来看的。
命令码的数据长度字段表示涉及到的用户数据的大小,这个成员的宽度依赖于体系结构,通常是13 或者14 位。
内核还定义了_IO()、_IOR()、_IOW()和_IOWR()这4个宏来辅助生成命令,这4个宏的通用定义如代码清单6.16。
代码清单6.16 _IO()、_IOR()、_IOW()和_IOWR()宏定义
#define _IO(type, nr) _IOC(_IOC_NONE, (type), (nr), 0)
#define _IOR(type, nr, size) _IOC(_IOC_READ, (type), (nr), (_IOC_TYPECHECK(size)))
#define _IOW(type, nr, size) _IOC(_IOC_WRITE, (type), (nr), (_IOC_TYPECHECK(size)))
#define _IOWR(type, nr, size) _IOC(_IOC_READ|_IOC_WRITE, (type), (nr), (_IOC_TYPECHECK(size)))
/* _IO、_IOR等使用的_IOC宏 */
#define _IOC(dir,type,nr,size) (((dir) << _IOC_DIRSHIFT) | ((type) << _IOC_TYPESHIFT) | ((nr) << _IOC_NRSHIFT) | ((size) << _IOC_SIZESHIFT))
由此可见,这几个宏的作用是根据传入的type(设备类型字段)、nr(序列号字段)和size(数据长度字段)和宏名隐含的方向字段移位组合生成命令码。
由于 globalmem 的 MEM_CLEAR 命令不涉及数据传输,因此它可以定义为:
#define GLOBALMEM_MAGIC ...
#define MEM_CLEAR _IO(GLOBALMEM_MAGIC, 0)
3、预定义命令
内核中预定义了一些 IO 控制命令,如果某设备驱动中包含了与预定义命令一样的命令码,这些命令会被当作预定义命令被内核处理而不是被设备驱动处理,预定义命令包括:
FIOCLEX : 即 File IOctl Close on Exec,对文件设置专用标志,通知内核当 exec() 系统调用发生时自动关闭打开的文件。
FIONCLEX : 即 File IOctl Not CLose on Exec,与 FIOCLEX 标志相反,清除由 FIOCLEX 命令设置的标志。
FIOQSIZE : 获得一个文件或者目录的大小,当用于设备文件时,返回一个 ENOTTY 错误。
FIONBIO : 即 File IOctl Non-Blocking I/O,这个调用修改在 filp->f_flags 中的 O_NONBLOCK 标志。
FIOCLEX、FIONCLEX、FIOQSIZE 和 FIONBIO 这些宏的定义为:
#define FIONCLEX 0x5450
#define FIOCLEX 0x5451
#define FIOQSIZE 0x5460
#define FIONBIO 0x5421
6.3.6 使用文件私有数据
6.3.1~6.3.5节给出的代码完整地实现了预期的 globalmem 雏形,在其代码中,为 globalmem 设备结构体 globalmem_dev 定义了全局实例 dev(见代码清单6.7第25行),而 globalmem 的驱动中 read()、write()、ioctl()、llseek() 函数都针对dev进行操作。实际上,大多数 Linux 驱动工程师遵循一个“潜规则”,那就是将文件的私有数据 private_data 指向设备结构体,在 read()、write()、ioctl()、llseek() 等函数通过 private_data 访问设备结构体。
这个时候,我们要将各函数进行少量的修改,为了让读者朋友建立字符设备驱动的全貌视图,代码清单6.17列出了完整的使用文件私有数据的 globalmem 的设备驱动,本程序位于虚拟机/home/lihacker/develop/svn/ldd6410-read-only/training/kernel/drivers/globalmem/ch6目录。
代码清单6.17 使用文件私有数据的 globalmem 的设备驱动
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#define GLOBALMEM_SIZE 0x1000 /* 全局内存最大4K字节 */
#define MEM_CLEAR 0x1 /* 清0全局内存 */
#define GLOBALMEM_MAJOR 250 /* 预设的globalmem的主设备号 */
static int globalmem_major = GLOBALMEM_MAJOR;
/*globalmem设备结构体*/
struct globalmem_dev {
structcdev cdev; /* cdev 结构体 */
unsignedchar mem[GLOBALMEM_SIZE]; /* 全局内存 */
};
struct globalmem_dev *globalmem_devp; /*设备结构体指针*/
/*文件打开函数*/
int globalmem_open(struct inode *inode, struct file *filp) {
/*将设备结构体指针赋值给文件私有数据指针*/
filp->private_data =globalmem_devp;
return0;
}
/*文件释放函数*/
int globalmem_release(struct inode *inode, structfile *filp)
{
return0;
}
/* ioctl设备控制函数 */
static int globalmem_ioctl(struct inode *inodep, struct file *filp, unsigned intcmd, unsigned long arg) {
struct globalmem_dev *dev = filp->private_data; /* 获得设备结构体指针 */
switch(cmd) {
caseMEM_CLEAR:
memset(dev->mem,0, GLOBALMEM_SIZE);
printk(KERN_INFO"globalmem is set to zero\n");
break;
default:
return - EINVAL;
}
return0;
}
/* 读函数 */
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos) {
unsignedlong p = *ppos;
unsignedint count = size;
intret = 0;
struct globalmem_dev *dev = filp->private_data; /* 获得设备结构体指针 */
/*分析和获取有效的写长度*/
if (p >= GLOBALMEM_SIZE)
return0;
if (count > GLOBALMEM_SIZE - p)
count= GLOBALMEM_SIZE - p;
/*内核空间->用户空间*/
if (copy_to_user(buf, (void *)(dev->mem + p), count)) {
ret= - EFAULT;
} else {
*ppos += count;
ret = count;
printk(KERN_INFO"read %u bytes(s) from %lu\n", count, p);
}
return ret;
}
/* 写函数 */
static ssize_t globalmem_write(struct file *filp, const char __user *buf, size_tsize, loff_t *ppos) {
unsigned long p = *ppos;
unsigned int count = size;
int ret = 0;
struct globalmem_dev *dev = filp->private_data; /* 获得设备结构体指针 */
/*分析和获取有效的写长度*/
if (p >= GLOBALMEM_SIZE)
return 0;
if (count > GLOBALMEM_SIZE - p)
count= GLOBALMEM_SIZE - p;
/* 用户空间->内核空间 */
if (copy_from_user(dev->mem +p, buf, count))
ret = - EFAULT;
else {
*ppos += count;
ret = count;
printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
}
return ret;
}
/* seek 文件定位函数 */
static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig) {
loff_t ret = 0;
switch (orig) {
case 0: /*相对文件开始位置偏移*/
if (offset < 0) {
ret= - EINVAL;
break;
}
if ((unsigned int)offset > GLOBALMEM_SIZE) {
ret= - EINVAL;
break;
}
filp->f_pos =(unsigned int)offset;
ret = filp->f_pos;
break;
case 1: /*相对文件当前位置偏移*/
if ((filp->f_pos+ offset) > GLOBALMEM_SIZE) {
ret= - EINVAL;
break;
}
if ((filp->f_pos+ offset) < 0) {
ret= - EINVAL;
break;
}
filp->f_pos += offset;
ret = filp->f_pos;
break;
default:
ret = - EINVAL;
break;
}
return ret;
}
/* 文件操作结构体 */
static const struct file_operations globalmem_fops = {
.owner = THIS_MODULE,
.llseek = globalmem_llseek,
.read = globalmem_read,
.write = globalmem_write,
.ioctl = globalmem_ioctl,
.open = globalmem_open,
.release = globalmem_release,
};
/* 初始化并注册 cdev */
static void globalmem_setup_cdev(struct globalmem_dev *dev, int index) {
int err, devno = MKDEV(globalmem_major, index);
cdev_init(&dev->cdev,&globalmem_fops);
dev->cdev.owner =THIS_MODULE;
err = cdev_add(&dev->cdev, devno, 1);
if (err)
printk(KERN_NOTICE"Error %d adding globalmem %d", err, index);
}
/* 设备驱动模块加载函数 */
int globalmem_init(void) {
int result;
dev_t devno = MKDEV(globalmem_major, 0);
/* 申请设备号 */
if (globalmem_major)
result = register_chrdev_region(devno, 1, "globalmem");
else {
/* 动态申请设备号 */
result =alloc_chrdev_region(&devno, 0, 1, "globalmem");
globalmem_major = MAJOR(devno);
}
if (result < 0)
return result;
/* 动态申请设备结构体的内存 */
globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
/* 申请失败 */
if (!globalmem_devp) {
result = - ENOMEM;
goto fail_malloc;
}
memset(globalmem_devp, 0,sizeof(struct globalmem_dev));
globalmem_setup_cdev(globalmem_devp,0);
return 0;
fail_malloc:
unregister_chrdev_region(devno,1);
return result;
}
/* 模块卸载函数 */
void globalmem_exit(void) {
cdev_del(&globalmem_devp->cdev); /* 注销 cdev */
kfree(globalmem_devp); /* 释放设备结构体内存 */
unregister_chrdev_region(MKDEV(globalmem_major,0), 1); /* 释放设备号 */
}
MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>");
MODULE_LICENSE("Dual BSD/GPL");
module_param(globalmem_major, int, S_IRUGO);
module_init(globalmem_init);
module_exit(globalmem_exit);
除了在 globalmem_open() 函数中通过 filp->private_data = globalmem_devp 语句(见第29行)将设备结构体指针赋值给文件私有数据指针并在 globalmem_read() 、globalmem_write()、globalmem_llseek()和globalmem_ioctl()函数中通过struct globalmem_dev *dev = filp->private_data语句获得设备结构体指针并使用该指针操作设备结构体外,代码清单6.17与代码清单6.7~6.15的程序并无二致。
读者朋友们,这个时候,请您翻回到本书的第1章,再次阅读代码清单1.4,即 Linux下LED 的设备驱动,是否豁然开朗?
代码清单6.17仅仅作为使用 private_data 的范例,实际上,在这个程序中使用 private_data 没有任何意义,直接访问全局变量 globalmem_devp 来的更加结构清晰。如果 globalmem 不只包括1个设备,而是同时包括2个或2个以上的设备,采用private_data的优势就会集中显现出来。
在不对代码清单6.17中的 globalmem_read()、globalmem_write()、globalmem_ioctl()等重要函数及 globalmem_fops 结构体等数据结构进行任何修改的前提下,只是简单的修改 globalmem_init()、globalmem_exit()和globalmem_open(),就可以轻松地让globalmem驱动中包含2个同样的设备(次设备号分别为0和1),如代码清单6.18。
代码清单6.18 支持2个 globalmem 设备的 globalmem 驱动
/* 文件打开函数 */
int globalmem_open(struct inode *inode, struct file *filp) {
/* 将设备结构体指针赋值给文件私有数据指针 */
struct globalmem_dev *dev;
dev = container_of(inode->i_cdev, structglobalmem_dev, cdev);
filp->private_data = dev;
return 0;
}
/* 设备驱动模块加载函数 */
int globalmem_init(void) {
int result;
dev_t devno = MKDEV(globalmem_major, 0);
/* 申请设备号 */
if (globalmem_major)
result = register_chrdev_region(devno, 2, "globalmem");
else {
/* 动态申请设备号 */
result = alloc_chrdev_region(&devno, 0, 2, "globalmem");
globalmem_major = MAJOR(devno);
}
if (result < 0)
return result;
/* 动态申请2个设备结构体的内存 */
globalmem_devp = kmalloc(2*sizeof(structglobalmem_dev), GFP_KERNEL);
if (!globalmem_devp) {
/*申请失败*/
result = - ENOMEM;
goto fail_malloc;
}
memset(globalmem_devp, 0, 2*sizeof(structglobalmem_dev));
globalmem_setup_cdev(&globalmem_devp[0], 0);
globalmem_setup_cdev(&globalmem_devp[1], 1);
return 0;
fail_malloc:
unregister_chrdev_region(devno, 1);
return result;
}
/* 模块卸载函数 */
void globalmem_exit(void) {
cdev_del(&(globalmem_devp[0].cdev));
cdev_del(&(globalmem_devp[1].cdev)); /* 注销cdev */
kfree(globalmem_devp); /* 释放设备结构体内存 */
unregister_chrdev_region(MKDEV(globalmem_major, 0), 2); /* 释放设备号 */
}
/* 其它代码同清单6.16 */
6.4 globalmem 驱动在用户空间的验证
在对应目录通过 make 命令编译 globalmem 的驱动,得到 globalmem.ko 文件。运行:$sudo su
#insmod globalmem.ko
命令加载模块,通过 lsmod 命令,发现 globalmem 模块已被加载。再通过 cat /proc/devices 命令察看,发现多出了主设备号为250的 globalmem 字符设备驱动:
#cat /proc/devices
Characterdevices:
1 mem
4 /dev/vc/0
4 tty
4 ttyS
5 /dev/tty
5 /dev/console
5 /dev/ptmx
6 lp
7 vcs
10 misc
13 input
14 sound
21 sg
29 fb
99 ppdev
108ppp
116alsa
128ptm
136pts
180usb
188ttyUSB
189usb_device
216rfcomm
226drm
250globalmem
接下来,通过命令
#mknod /dev/globalmem c 250 0
创建 /dev/globalmem 设备节点,并通过 echo 'hello world' > /dev/globalmem 命令和 cat/dev/globalmem 命令分别验证设备的写和读,结果证明 hello world 字符串被正确地写入 globalmem 字符设备:
#echo "hello world" > /dev/globalmem
#cat /dev/globalmem
helloword
如果启用了 sysfs 文件系统,将发现多出了 /sys/module/globalmem 目录,该目录下的树型结构为:
|--refcnt
`--sections
|--.bss
|-- .data
|-- .gnu.linkonce.this_module
|-- .rodata
|-- .rodata.str1.1
|-- .strtab
|-- .symtab
|-- .text
`-- __versions
refcnt 记录了 globalmem 模块的引用计数,sections 下包含的数个文件则给出了 globalmem 所包含的 BSS、数据段和代码段等的地址及其它信息。
对于代码清单6.18给出的支持2个 globalmem 设备的驱动,在加载模块后需创建2个设备节点,/dev/globalmem0 对应主设备号 globalmem_major,次设备号0,/dev/globalmem1 对应主设备号 globalmem_major,次设备号1。分别读写/dev/globalmem0和/dev/globalmem1,发现都读写到了正确的对应的设备。