字符设备通过字符(一个接一个的字符)以流方式向用户程序传递数据,就像串行端口那样。字符设备驱动通过/dev目录下的特殊文件公开设备的属性和功能,通过这个文件可以在设备和用户应用程序之间交换数据,也可以通过它来控制实际的物理设备。这也是Linux的基本概念,一切皆文件。字符设备驱动程序是内核源码中最基本的设备驱动程序。字符设备在内核中表示为struct cdev
的实例,struct cdev
定义在include/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; /* 隶属于同一主设备号的次设备号的个数.*/
} __randomize_layout;
开头为c的代表字符设备文件,开头为b的代表块设备文件,日期左边的第五列、第六列用
内核用dev_t类型变量维持设备号,该变量是u32。主设备号仅占12位,次设备号占20位。
typedef __kernel_dev_t dev_t;
typedef __u32 __kernel_dev_t;
typedef unsigned int __u32;
dev_t类型定义在include/linux/kdev_t.h中,可以通过如下两个宏定义来获取主、次设备号:
MAJOR(dev_t dev);
MINOR(dev_t dev);
如果有主设备和次设备号,也可以通过宏MKDEV(int major,int minor)
来构建dev_t。
设备注册时,必须使用主设备号和次设备号,前者标识一个特定的驱动程序,后者用作标识使用该驱动程序的各设备(设备列表中的数组索引),因为同一个驱动可处理多个设备,而不同的驱动程序可以处理相同类型的不同设备。
设备号在系统范围内标识设备文件,有两种不同的方法分配设备号。
下面两个函数都在fs/char_dev.c实现
静态方法是调用register_chardev_region()函数,该方法必须事先知道所需的设备号
int register_chrdev_region(dev_t from, unsigned count, const char *name)
这个函数成功返回0,失败返回错误码。from是由我们所需的主设备号和合理范围内的次设备号组成,可由MKDEV构建。count是所需的连续设备号数目,name是相关设备或者驱动程序的名字。
使用alloc_chardev_region()函数,使内核自动分配设备号,建议采用这种方法获得有效的设备号。
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
这个函数成功返回0,失败返回错误码。dev获取分配的设备号,baseminor代表申请的次设备号范围内的第一个数字,count代表次设备的数目,name代表相关设备或者驱动程序的名字。
这两种分配方法的区别在于,第一种方法必须事先知道所需的设备号,这就是注册制:把所需的设备号告诉内核。这可能在教学中使用,只有自己使用该驱动程序时,才会这样选择,如果在其他机器上加载该驱动程序,就无法保证所选择的设备号在这台机器未被占用,这会引起设备号的冲突和麻烦。第二种更安全,因为内核帮助获取一个合适的设备号,所以我们甚至不需要关心在其他机器上加载该模块所出现的问题,内核将根据具体情况来自动分配。
可以在文件上执行的操作取决于管理文件的设备驱动程序。这样的操作在内核中定义为struct file_operations
的实例。struct file_operations
定义了一组回调函数,用于处理文件上的所有用户空间的系统调用。举个例子,如果想让用户在设备文件上执行write操作,必须在驱动中实现write函数对于的回调函数,并把它添加到绑定在设备上的struct file_operations
中,struct file_operations
定义在include/linux/fs.h中。
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 *);
int (*iterate_shared) (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 (*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
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
};
其中的每一个函数都和系统调用链接在一起,它们都不是必需的。当用户代码在指定文件上调用与文件相关的系统调用时,内核会查找负责这个文件的驱动程序,定位它的struct file_operations结构,并检查和该系统调用匹配的方法是否已经定义。如果已经定义了,就运行它。如果未定义,则根据系统调用不同返回不同的错误码。
struct inode表示一个具体文件。一个设备或者驱动会由struct inode的实例表示。在该结构体中,我们需要注意以下几个域。
struct inode {
...
const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
union {
struct pipe_inode_info *i_pipe; /* 如果是Linux管道,则设置并使用 */
struct block_device *i_bdev; /* 如果是块设备,则设置并使用 */
struct cdev *i_cdev; /* 如果是字符设备,则设置并使用 */
char *i_link;
unsigned i_dir_seq;
};
....
};
struct inode
里面也有struct file_operations
,但是i_fop
指向的是默认的索引节点操作,如果struct inode
代表的是字符设备,则i_cdev会指向一个struct cdev
结构,对文件进行操作时,使用的是cdev中file_operations中定义的文件操作方法。
struct file代表的是一个进程打开的文件,其里面也有struct file_operations
struct file {
...
const struct file_operations *f_op;
...
};
当我们在应用层使用open函数打开一个文件时,会创建struct file
对象,初始化struct file
对象时,struct file
对象中的file_operations将指向struct inode
的file_operations(准备的来说,struct inode
如果没有定义文件的具体操作,将指向默认的file_operations,如果定义了,比如字符设备,将指向字符设备的file_operations)
比如我们使用open打开两个字符设备
fd0 = open("/dev/com0",O_RDWR);fd1 = open("/dev/com1",O_RDWR);
如下图
struct inode
使用struct cdev
中的file_operations,struct file
也指向struct cdev
中的file_operations,当对com0或者com1操作时,直接调用struct file
的file_operations。
如何将file_operations里面定义的操作和struct cdev
结构绑定到一起呢?我们可以使用cdev_init函数,将struct cdev
中的ops指向第二个参数指向的内容
void cdev_init(struct cdev *, const struct file_operations *)
字符设备在内核中表示为struct cdev的实例。在编写字符设备驱动程序时,目标是最终创建并注册与struct file_operations关联的结构实例,为用户空间提供一组可以在该设备上执行的操作函数,为了实现这个目标,必须执行以下几个步骤:
宏class_create()用于动态创建设备的逻辑类,并完成部分字段的初始化,然后将其添加进Linux内核系统中。此函数的执行效果就是在目录/sys/class下创建一个新的文件夹,此文件夹的名字为此函数的第二个输入参数,但此文件夹是空的。宏class_create()在实现时,调用了函数__class_create(),作用和函数__class_create()基本相同。
class_create在include/linux/device.h中被定义
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
返回值为创建的逻辑类。
此宏需要与函数class_destroy()配对使用,不能单独使用,当单独使用时,第一次不会出现错误,但当第二次插入模块时就会出现错误。
函数cdev_add()用于向Linux内核系统中添加一个新的cdev结构体变量所描述的字符设备,并且使这个设备立即可用。
在文件linux/cdev.h中定义:
int cdev_add(struct cdev *, dev_t, unsigned)
函数 cdev_add()有三个输入参数,第一个输入参数代表即将被添加入Linux内核系统的字符设备;第二个输入参数是dev_t类型的变量,此变量代表设备的设备号,其中包括主设备号和次设备号;第三个输入参数是无符号的整型变量,代表想注册设备的设备号的范围,用于给struct cdev中的字段count赋值
函数device_create()用于动态地创建逻辑设备,并对新的逻辑设备类进行相应的初始化,将其与此函数的第一个参数所代表的逻辑类关联起来,然后将此逻辑设备加到Linux内核系统的设备驱动程序模型中。函数能够自动地在/sys/devices/virtual目录下创建新的逻辑设备目录,在/dev目录下创建与逻辑类对应的设备文件。
该函数定义在linux/device.h中
struct device *device_create(struct class *cls, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...);
函数device_create()的第一个输入参数代表与即将创建的逻辑设备相关的逻辑类,也就是class_create
第二个输入参数代表即将创建的逻辑设备的父设备的指针,子设备与父设备的关系是:当父设备不可用时,子设备不可用,子设备依赖父设备,父设备不依赖子设备。
第三个输入参数是逻辑设备的设备号
第四个输入参数是void类型的指针,代表回调函数的输入参数。
第五个输入参数是逻辑设备的设备名,即在目录/sys/devices/virtual创建的逻辑设备目录的目录名。
返回值是struct device结构体类型的指针,指向新创建的逻辑设备,
device_create创建了设备文件,我们就可以根据该设备文件来和驱动或设备交互了。
注意:函数device_create()必须和函数device_destroy()配对使用,这样才不会出现错误
#include
#include
#include
#include
#include
#include
static unsigned int major; /* major number for device */
static struct class *dummy_class;
static struct cdev dummy_cdev;
int dummy_open(struct inode * inode, struct file * filp)
{
pr_info("Someone tried to open me\n");
return 0;
}
int dummy_release(struct inode * inode, struct file * filp)
{
pr_info("Someone closed me\n");
return 0;
}
ssize_t dummy_read (struct file *filp, char __user * buf, size_t count,
loff_t * offset)
{
pr_info("Nothing to read guy\n");
return 0;
}
ssize_t dummy_write(struct file * filp, const char __user * buf, size_t count,
loff_t * offset)
{
pr_info("Can't accept any data guy\n");
return count;
}
struct file_operations dummy_fops = {
open: dummy_open,
release: dummy_release,
read: dummy_read,
write: dummy_write,
};
static int __init dummy_char_init_module(void)
{
struct device *dummy_device;
int error;
dev_t devt = 0;
/* Get a range of minor numbers (starting with 0) to work with */
error = alloc_chrdev_region(&devt, 0, 1, "dummy_char");
if (error < 0) {
pr_err("Can't get major number\n");
return error;
}
major = MAJOR(devt);
pr_info("dummy_char major number = %d\n",major);
/* Create device class, visible in /sys/class */
dummy_class = class_create(THIS_MODULE, "dummy_char_class");
if (IS_ERR(dummy_class)) {
pr_err("Error creating dummy char class.\n");
unregister_chrdev_region(MKDEV(major, 0), 1);
return PTR_ERR(dummy_class);
}
/* Initialize the char device and tie a file_operations to it */
cdev_init(&dummy_cdev, &dummy_fops);
dummy_cdev.owner = THIS_MODULE;
/* Now make the device live for the users to access */
cdev_add(&dummy_cdev, devt, 1);
dummy_device = device_create(dummy_class,
NULL, /* no parent device */
devt, /* associated dev_t */
NULL, /* no additional data */
"dummy_char"); /* device name */
if (IS_ERR(dummy_device)) {
pr_err("Error creating dummy char device.\n");
class_destroy(dummy_class);
unregister_chrdev_region(devt, 1);
return -1;
}
pr_info("dummy char module loaded\n");
return 0;
}
static void __exit dummy_char_cleanup_module(void)
{
unregister_chrdev_region(MKDEV(major, 0), 1);
device_destroy(dummy_class, MKDEV(major, 0));
cdev_del(&dummy_cdev);
class_destroy(dummy_class);
pr_info("dummy char module Unloaded\n");
}
module_init(dummy_char_init_module);
module_exit(dummy_char_cleanup_module);
MODULE_AUTHOR("John Madieu " );
MODULE_DESCRIPTION("Dummy character driver");
MODULE_LICENSE("GPL");
在/dev目录下创建了字符设备