一、字符设备驱动框架

1. file_operations结构体

在Linux中应用程序运行在用户空间,而驱动程序属于内核的一部分,在内核空间运行。用户需要通过​系统调用​陷入到内核空间,才能实现对底层驱动的操作。

i.MX6ULL驱动开发 | 02-字符设备驱动框架_驱动开发

以open函数为例,当用户在C语言程序中调用​​open​​函数时,调用关系链如下图所示:

i.MX6ULL驱动开发 | 02-字符设备驱动框架_设备号_02

这就意味着,驱动程序必须提供一些必要的函数,来与open、read、write、close这些函数相对应,确实,这套函数定义在 ​​file_operations​​结构体中。

file_operations结构体中定义了Linux内核驱动操作函数的集合​,在Linux内核文件​​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 *);
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 (*mremap)(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
};

这些函数的作用如下:

i.MX6ULL驱动开发 | 02-字符设备驱动框架_linux_03

2. cdev结构体

Linux操作系统将字符设备统一用一个cdev结构体进行管理,该结构体对字符设备进行了详细的描述​。

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;
};

成员

作用

kobj

字符设备驱动中的一个内核对象

owner

设备的拥有者,一般为THIS_MODULE

ops

file_operations结构体指针

list

双向链表节点,用于挂载到设备链表

dev

设备信息

count

记录了相同主设备号中次设备号的总数

Linux内核提供了一组函数用来操作cdev结构体, 声明在文件​​<include/linux/cdev.h>​​​中,实现在文件​​fs/char_dev.c​​中:

void cdev_init(struct cdev *, const 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 *);

void cd_forget(struct inode *);

(1)cdev_init函数用来​初始化cdev结构体的成员​,并建立cdev和file_operations之间的连接:

/**
* 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;
}

(2)cdev_alloc函数用来​动态申请一个cdev内存​:

/**
* 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;
}

(3)cdev_add函数用来向系统中添加一个cdev,完成字符设备的注册:

/**
* 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;
}

(4)cdev_del函数用于从系统中删除一个cdev,完成字符设备的注销:

/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*/
void cdev_del(struct cdev *p)
{
cdev_unmap(p->dev, p->count);
kobject_put(&p->kobj);
}

3. 设备号的描述

每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成。主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。

这么重要的信息,当然在cdev结构体中的dev成员中,dev_t其实就是一个u32类型,在​​include/linux/types.h​​文件中,定义如下:

typedef __u32 __kernel_dev_t;

typedef __kernel_dev_t dev_t;

在文件​​include/linux/kdev_t.h​​中提供了关于设备号的操作宏,定义如下:

#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))

可以看到,​主设备号占据高12位、范围是0-4095​,次设备号占据低20位。

4. 主设备号的分配

主设备号分配有两种方式:静态分配和动态分配。

4.1. 静态分配主设备号

有一些设备号已经被linux内核开发者给分配了,分配的内容在文件​​Documentation/devices.txt​​中,但是这些设备号我们依然可以强行使用,只是不规范而已。

如果系统中已经使用了的设备号,可以使用下面的命令查看,那么我们就不能强行使用了,否则会造成冲突。

cat /proc/devices

i.MX6ULL驱动开发 | 02-字符设备驱动框架_设备节点_04

避开系统已经使用的,只要在范围之内,喜欢哪个用哪个都行​。

4.2. 动态分配设备号

静态分配设备号简单粗暴,但是很容易造成冲突。

Linux社区推荐使用动态分配设备号,在注册字符设备之前先申请一个设备号,系统会自动分配一个没有使用的设备号,这样就避免了冲突。卸载驱动的时候释放掉这个设备号即可。

(1)​申请设备号API

设备号申请的函数声明在头文件​​include/linux/fs.h​​中,声明如下:

extern int alloc_chrdev_region(dev_t *, unsigned, unsigned, const char *);

实现在​​fs/char_dev.c​​文件中:

/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
const char *name)
{
struct char_device_struct *cd;
cd = __register_chrdev_region(0, baseminor, count, name);
if (IS_ERR(cd))
return PTR_ERR(cd);
*dev = MKDEV(cd->major, cd->baseminor);
return 0;
}

该函数有四个参数:

  • dev:用于保存申请到的设备号
  • baseminor:次设备号起始地址,一般为0
  • count:要申请的设备数量
  • name:设备名字

(2)​释放设备号API

该函数同样声明在​​include/linux/fs.h​​文件中,声明如下:

extern void unregister_chrdev_region(dev_t, unsigned);

实现在文件``中:

/**
* unregister_chrdev_region() - return a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count)
{
dev_t to = from + count;
dev_t n, next;

for (n = from; n < to; n = next) {
next = MKDEV(MAJOR(n)+1, 0);
if (next > to)
next = to;
kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
}
}

该函数有两个参数:

  • from:要释放的设备号
  • count:要释放的设备号数量

5. 设备节点

用户程序要实现对设备的操作,还需要一个在 /dev 目录下的设备节点​,那么如何来创建设备节点呢?

5.1. 手动创建设备节点

加载驱动到内核后,可以通过mknod命令,根据设备号手动创建一个设备节点:

mknod <设备目录> <设备类型> <主设备号> <次设备号>

比如:

mknod /dev/hello_drv c 200 0

这样就会在/dev目录下创建一个名为hello_drv的设备节点,显然这样不太方便,如果在驱动加载的时候,自动的创建一个设备节点该有多好。

5.2. 自动创建设备节点

Linux内核提供了自动创建设备节点的机制,具体使用如下,这些API在​​include/linux/device.h​​文件中声明。

(1)创建一个设备类

/* This is a #define to keep the compiler from merging different
* instances of the __key variable */
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})

该函数会在​​/sys/class​​目录下创建一个该类​。

(2)创建一个设备

struct device *device_create(struct class *cls, struct device *parent,
dev_t devt, void *drvdata,
const char *fmt, ...);

该函数会在​​/dev​​目录下创建该设备节点。

(3)删除设备

extern void device_destroy(struct class *cls, dev_t devt);

(4)删除类

extern void class_destroy(struct class *cls);

注意,在设备驱动注销的时候,需要自动删除设备,也要一起删除设备节点和相关的设备类。

6. 总结

6.1. 字符设备驱动框架架构

i.MX6ULL驱动开发 | 02-字符设备驱动框架_linux_05

6.2. 字符设备驱动调用关系

i.MX6ULL驱动开发 | 02-字符设备驱动框架_linux_06

6.3. 设备节点和设备号之间的关系

i.MX6ULL驱动开发 | 02-字符设备驱动框架_linux_07

二、字符设备驱动实例

1. 模块源码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/types.h>
#include <linux/cdev.h>
#include <linux/device.h>

#define HELLO_DRV_NAME "hello drv"
#define HELLO_DRV_CLASS "hello_drv_class"
#define HELLO_DRV_DEVICE "hello_drv0"

#define MEM_SIZE 0x1000

static dev_t hello_drv_dev;
static struct cdev *hello_drv_cdev;
static struct class *hello_drv_class;
static struct device *hello_drv0;

static int hello_drv_open(struct inode *inode, struct file *filp)
{
printk("hello drv open!\n");
return 0;
}

ssize_t hello_drv_read(struct file *filp, char __user *buf, size_t len, loff_t *off)
{
printk("hello drv read!\n");
return 0;
}

ssize_t hello_drv_write(struct file *filp, const char __user *buf, size_t len, loff_t *off)
{
printk("hello drv write!\n");
return 0;
}

static int hello_drv_release(struct inode *inode, struct file *filp)
{
printk("hello drv release!\n");
return 0;
}

static struct file_operations hello_drv_fops = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_release,
};

static int __init hello_drv_init(void)
{
int ret;

// 动态申请设备号
ret = alloc_chrdev_region(&hello_drv_dev, 0, 1, HELLO_DRV_NAME);
if (ret = 0) {
printk(KERN_WARNING"alloc_chrdev_region failed!\n");
return -1;
}

// 动态申请cdev
hello_drv_cdev = cdev_alloc();
if (!hello_drv_cdev) {
printk(KERN_WARNING"cdev_alloc failed!\n");
return -1;
}

// 初始化cdev结构体
hello_drv_cdev->owner = THIS_MODULE;
hello_drv_cdev->ops = &hello_drv_fops;

// 将设备添加到内核中
cdev_add(hello_drv_cdev, hello_drv_dev, 1);

// 创建设备类
hello_drv_class = class_create(THIS_MODULE, HELLO_DRV_CLASS);
if (!hello_drv_class) {
printk(KERN_WARNING"class_create failed!\n");
return -1;
}

// 创建设备节点
hello_drv0 = device_create(hello_drv_class, NULL, hello_drv_dev, NULL, HELLO_DRV_DEVICE);
if (IS_ERR(hello_drv0)) {
printk(KERN_WARNING"device_create failed!\n");
return -1;
}

printk("hello drv init success!\n");
return 0;
}

static void __exit hello_drv_exit(void)
{
// 将设备从内核删除
cdev_del(hello_drv_cdev);

// 释放设备号
unregister_chrdev_region(hello_drv_dev, 1);

// 删除设备节点
device_destroy(hello_drv_class, hello_drv_dev);

// 删除设备类
class_destroy(hello_drv_class);

printk("hello drv exit!\n");
}

module_init(hello_drv_init);
module_exit(hello_drv_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Mculover666");

2. 编译

KERNEL_DIR := /home/mculover666/imx6ull/kernel/linux-imx6ull
obj-m := hello_drv.o

build: kernel_modules

kernel_modules:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean

3. 加载卸载测试

(1)驱动加载测试

i.MX6ULL驱动开发 | 02-字符设备驱动框架_驱动开发_08

加载之后查看系统当前使用的设备号:


查看驱动自动创建的设备类:

i.MX6ULL驱动开发 | 02-字符设备驱动框架_设备号_09

查看驱动自动创建的设备节点:

i.MX6ULL驱动开发 | 02-字符设备驱动框架_linux_10

(2)驱动卸载测试

i.MX6ULL驱动开发 | 02-字符设备驱动框架_linux_11

查看设备节点,已经被删除:

i.MX6ULL驱动开发 | 02-字符设备驱动框架_设备节点_12

查看设备类,已经被删除:

i.MX6ULL驱动开发 | 02-字符设备驱动框架_设备节点_13

查看设备号:

i.MX6ULL驱动开发 | 02-字符设备驱动框架_linux_14

这里虽然没有被删除,但是注意到,之前加载后查看的时候是有两个(之前已经卸载了一次),此时只有一个了,所以设备号不会被立即释放,应该是过一会被系统释放,有待进一步验证学习。

4. 应用程序测试

编写一个普通的C语言测试程序:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv)
{
int fd;
char *filename = NULL;
int tmp;

// 检查参数
if (argc != 2) {
printf("usage: ./test_hello_drv [device]\n");
}

// 打开设备文件
filename = argv[1];
fd = open(filename, O_RDWR);
if (fd < 0) {
printf("open %s error!\n", filename);
return 0;
}

// 读文件
read(fd, &tmp, 4);

// 写文件
write(fd, &tmp, 4);

// 关闭文件
close(fd);

return 0;
}

交叉编译:

arm-linux-gnueabihf-gcc test_hello_drv.c -o test_hello_drv

测试:

i.MX6ULL驱动开发 | 02-字符设备驱动框架_linux_15

至此,字符设备驱动框架学习完毕。

参考

  • 《I.MX6ULL嵌入式Linux驱动开发指南》,正点原子,第40、42章
  • 《Linux驱动开发指南》,李山文,第2章
  • 《Linux设备驱动开发详解》,宋宝华,第6章