一、背景
字符设备驱动是三种基本驱动类型(others:块设备,网络设备)之一,其基本特点是一个字节一个字节的以流式的形式进行设备节点的访问,如串口(/dev/ttys0)、字符终端(/dev/console)等等。
在之后的章节介绍字符设备中相互关联的重要的数据结构和一些重要的API接口。同时,总结出一个通用的基本字符设备驱动模板。
二、设备号与分配
三、重要数据结构
1.file_operations
file_operations 结构体定义如下。可以看到的是,这些函数指针对应于open、read、write等系统调用。当我们对设备文件节点进行相应的应用层调用时,该节点对应的如下文件操作函数指针就会被调用到,从而完成设备的操作。
struct file_operations {
struct module *owner;
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 (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
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, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
};
2.file
当前,我们只关注其中的f_op和private_date 这两个成员。
对于file_operations指针来说,内核在执行open系统调用时对这个指针赋值,从而完成内核file 句柄和f_op之间的绑定。之后相关的系统调用则可以通过该指针进行调用。
private_data 这个指针供驱动程序本身自行使用,通常它会保存一些在跨系统调用中非常有用的状态信息或者数据结构,之后会看到例子。
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;
#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
} __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
3.inode
inode 结构对应于设备文件节点。对于当个文件,可能会有多个打开的文件描述符对应一个设备文件,但它们都指向单个的如下所示的inode结构,当前我们只要关注i_rdev和i_cdev这个两个元素。
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;
/* ... */
dev_t i_rdev; /*<---*/
/* ... */
union {
struct pipe_inode_info *i_pipe;
struct block_device *i_bdev;
struct cdev *i_cdev; /*<---*/
char *i_link;
};
/* ... */
};
其中,i_rdev对应该节点的设备编号,可以通过如下的宏以兼容的方式获得主/次设备号:
static inline unsigned iminor(const struct inode *inode)
{
return MINOR(inode->i_rdev);
}
static inline unsigned imajor(const struct inode *inode)
{
return MAJOR(inode->i_rdev);
}
i_cdev则表示字符设备在内核中的具体内部结构,来一窥究竟,该结构体会在第四节碰到:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
好了,来总结下这几个结构体之间的对应关系:

四、重要API
1. 字符设备注册类
静态初始化函数,用来填充cdev结构体的fops。其中的kobject设备模型的基础结构,之后会涉及。对应着cdev_alloc为动态分配
/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const 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_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
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结构体之后,通过调用cdev_add 来激活该设备。注意到注释中有提到,调用该函数后会立即激活该设备,所以在调用该函数之前需要确保用户已经可以正常的操作设备。对应着有cdev_del API。
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{
int error;
p->dev = dev;
p->count = count;
error = kobj_map(cdev_map, dev, count, NULL,
exact_match, exact_lock, p);
if (error)
return error;
kobject_get(p->kobj.parent);
return 0;
}
结合上面的API,设备注册的示例代码如下:
struct scull_dev {
/* ... */
struct cdev cdev; /* Char device structure */
};
/*
* Set up the char_dev structure for this device.
*/
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);
}
2. 文件操作类
2.1 open方法
当向系统注册了cdev 之后,当用户想要访问设备文件时,open 系统调用会回调cdev->ops->open,并以当前的inode与filp为入口参数。从而设备就有机会做初始化工作。
在open 中,需要完成如下的工作:
1.检测设备是否已准备好,如果没有准备好,返回错误码,方便用户检测错误
2.如果设备是首次打开,则对其进行初始化(比如可以查看相应的指针是否已设置)
3.如有必要,更新f_op指针(不常用)
4.分配并填写filp->private_data,至于为什么,因为其他的文件操作接口只存在filp,而没有了inode结构
作为一个例子,参考scull_open。其中通过container_of获得我们需要的外围大结构体(container_of的说明放在六、补充里,但其实很常用奥~)
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 */
/* add your code here*/
return 0; /* success */
}
2.2 release方法
对应于open,存在着release方法来执行清理工作,其完成如下工作:
1.释放open分配的,存在于filp->private里动态分配的内容
2.在最后一次关闭操作时关闭设备。
ldd3提到了几个算是常识点,
1.dup或者fork并不会调用open系统调用而获得filp副本(指针),当对应的进程退出调用close系统调用时,
并不会真正的调用设备release方法。原因是内核在获得副本时,仅仅是增加了filp的引用计数。
只有在引用计数归0,才会真正调用release 方法;
2.在linux 中,进程会杀死时,系统会自动调用打开的file的close系统调用
2.3 write and read 方法
write 和read 方法主要实现用户数据和设备数据之间的交互,内核提供了如下的两个inline函数来进行数据交互:
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n)
{
if (access_ok(VERIFY_READ, from, n))
n = __copy_from_user(to, from, n);
else /* security hole - plug it */
memset(to, 0, n);
return n;
}
static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n)
{
if (access_ok(VERIFY_WRITE, to, n))
n = __copy_to_user(to, from, n);
return n;
}
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;
/* ... */
if (copy_from_user(dptr->data[s_pos]+q_pos, buf, count)) {
retval = -EFAULT;
goto out;
}
/* ... */
}
五、模板
/*cdev_template.h*/
/*______________________________________________*/
#ifndef TEMPLATE_MAJOR
#define TEMPLATE_MAJOR0 /* dynamic major by default */
#endif
#ifndef TEMPLATE_NR_DEVS
#define TEMPLATE_NR_DEVS4 /* scull0 through scull3 */
#endif
struct template_dev {
/*
add your data here
*/
struct cdev cdev; /* Char device structure */
};
/*cdev_template.c*/
/*______________________________________________*/
/*Include file*/
#include <linux/config.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/proc_fs.h>
#include <linux/fcntl.h> /* O_ACCMODE */
#include <linux/seq_file.h>
#include <linux/cdev.h>
#include <asm/system.h> /* cli(), *_flags */
#include <asm/uaccess.h> /* copy_*_user */
/*
* Our parameters which can be set at load time.
*/
int template_major = SCULL_MAJOR;
int template_minor = 0;
int template_nr_devs = SCULL_NR_DEVS; /* number of bare scull devices */
module_param(template_major, int, S_IRUGO);
module_param(template_minor, int, S_IRUGO);
module_param(template_nr_devs, int, S_IRUGO);
MODULE_AUTHOR("Alessandro Rubini, Jonathan Corbet");
MODULE_LICENSE("Dual BSD/GPL");
struct template_dev *template_devices; /* allocated in template_init_module */
/*
* Open and close
*/
int template_open(struct inode *inode, struct file *filp)
{
struct template_dev *dev; /* device information */
dev = container_of(inode->i_cdev, struct template_dev, cdev);
filp->private_data = dev; /* for other methods */
/*
add your code here
*/
return 0; /* success */
}
int template_release(struct inode *inode, struct file *filp)
{
/*
add your code here
*/
}
/*
* Data management: read and write
*/
ssize_t template_read(struct file *filp, char __user *buf, size_t count,
loff_t *f_pos)
{
/*
add your code here
*/
}
ssize_t template_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
/*
add your code here
*/
}
/*
* The ioctl() implementation
*/
int template_ioctl(struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg)
{
/*
add your code here
*/
}
struct file_operations template_fops = {
.owner = THIS_MODULE,
.read = template_read,
.write = template_write,
.ioctl = template_ioctl,
.open = template_open,
.release = template_release,
};
/*
* The cleanup function is used to handle initialization failures as well.
* Thefore, it must be careful to work correctly even if some of the items
* have not been initialized
*/
void scull_cleanup_module(void)
{
int i;
dev_t devno = MKDEV(template_major, template_minor);
/* Get rid of our char dev entries */
if (template_devices) {
for (i = 0; i < template_nr_devs; i++) {
/*
add your clean code here
*/
cdev_del(&template_devices[i].cdev);
}
kfree(template_devices);
}
/* cleanup_module is never called if registering failed */
unregister_chrdev_region(devno, scull_nr_devs);
}
/*
* Set up the char_dev structure for this device.
*/
static void template_setup_cdev(struct template_dev *dev, int index)
{
int err, devno = MKDEV(template_major, template_minor + index);
cdev_init(&dev->cdev, &template_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &template_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);
}
int template_init_module(void)
{
int result, i;
dev_t dev = 0;
/*
* Get a range of minor numbers to work with, asking for a dynamic
* major unless directed otherwise at load time.
*/
if (template_major) {
dev = MKDEV(template_major, template_minor);
result = register_chrdev_region(dev, template_nr_devs, "template");
} else {
result = alloc_chrdev_region(&dev, template_minor, template_nr_devs,
"template");
scull_major = MAJOR(dev);
}
if (result < 0) {
printk(KERN_WARNING "scull: can't get major %d\n", template_major);
return result;
}
/*
* allocate the devices -- we can't have them static, as the number
* can be specified at load time
*/
template_devices = kmalloc(template_nr_devs* sizeof(struct template_dev), GFP_KERNEL);
if (!template_devices) {
result = -ENOMEM;
goto fail; /* Make this more graceful */
}
memset(template_devices , 0, template_nr_devs* sizeof(struct template_dev));
/*
add your code here before cdev register
*/
/* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
template_setup_cdev(&template_devices[i], i);
}
return 0; /* succeed */
fail:
template_cleanup_module();
return result;
}
module_init(template_init_module);
module_exit(template_cleanup_module);
六、补充
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})