简述
Linux设备驱动有三种:字符设备驱动、块设备驱动、网络设备驱动。其中字符设备驱动最为基础。
Linux设备驱动目前有三种开发架构:原始架构、平台总线架构、设备树架构。三种架构理解难度依次上升,但开发难度依次下降。本篇文章介绍原始架构。
字符设备驱动开发最关键的两个数据结构是cdev
和file_operations
,如果想要自动生成设备节点,还要用到class
结构。
cdev结构体用来描述一个字符设备,内容如下
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
file_operations中存放着与操作设备相关的函数。模块加载与卸载函数分别负责将cdev添加到内核与从内核中删除。加载完字符设备驱动模块后,/proc/devices
文件中会生成对应的设备条目。
添加设备后,还需要生成设备节点文件,应用程序是通过设备节点文件来操控设备的,设备节点文件在/dev/
目录下。
整体框架如下所示:
![](https://img-blog.csdnimg.cn/20200422212608588.png)
其中的的红字是我们进行原始驱动开发必需要做的几件预备工作。做完之后才能进行file_operations成员的开发。
本文将按如下顺序介绍字符设备驱动开发方法:
- 主次设备号介绍
- cdev相关函数介绍
- 案例演示
- 生成设备节点
- file_operations成员列举
主次设备号
设备号由cdev结构体中的dev定义,一共32位,其中主设备号12位,次设备号20位,主设备号标识设备类型,次设备号标识同一设备类型中的不同设备。分解与合成设备号的方法如下:
MAJOR(dev_t dev)
MINOR(dev_t dev)
MKDEV(int major, int minor)
设备号需要申请才能获得,有静态和动态两种申请方法,分别入下:
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);
模块卸载后要将设备号释放,释放方法如下:
void unregister_chrdev_region(dev_t from, unsigned count);
cdev相关函数
Linux提供了一组函数用于操作cdev结构体:
- void cdev_init(struct cdev*, struct file_operations *);
cdev_init用于初始化cdev成员,并建立与file_operations之间的联系
- struct cdev *cdev_alloc(void);
cdev_alloc用于动态申请cdev内存
-
void cdev_put(struct cdev *p);
-
int cdev_add(struct cdev *, dev_t, unsigned);
cdev_add用于向系统添加一个cdev
- void cdev_del(struct cdev *);
cdev_del用于从系统删除cdev
案例演示
该案例只是为了说明字符设备的添加方法,没有实现file_operations的任何函数
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/stat.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Shijia Yin");
#define DEV_NAME "test"
#define DEV_MIN_NUM 2
#define DEV_MAJOR 0
#define DEV_MINOR 0
int dev_major = DEV_MAJOR;
int dev_minor = DEV_MINOR;
struct test_dev_t {
struct cdev cdev;
} test_dev;
struct file_operations test_fops = {
.owner = THIS_MODULE,
};
static int test_init(void)
{
int err;
int ret = 0;
dev_t dev_num;
if(dev_major) {
dev_num = MKDEV(dev_major, dev_minor);
ret = register_chrdev_region(dev_num, DEV_MIN_NUM, DEV_NAME);
} else {
ret = alloc_chrdev_region(&dev_num, dev_minor, DEV_MIN_NUM, DEV_NAME);
dev_major = MAJOR(dev_num);
printk(KERN_EMERG "major number is %d !\n", dev_major);
}
if(ret < 0) {
printk(KERN_EMERG "register_chrdev_region req %d is failed!\n", dev_major);
}
cdev_init(&test_dev.cdev, &test_fops);
test_dev.cdev.ops = &test_fops;
test_dev.cdev.owner = THIS_MODULE;
err = cdev_add(&test_dev.cdev, dev_num, 1);
if(err) {
printk(KERN_EMERG "cdev_add %d is fail! %d\n", dev_minor, err);
} else {
printk(KERN_EMERG "cdev_add %d is success! \n", dev_minor);
}
printk(KERN_ALERT "Hello, world\n");
return 0;
}
static void test_exit(void)
{
printk(KERN_EMERG "test exit!\n");
cdev_del(&test_dev.cdev);
unregister_chrdev_region(MKDEV(dev_major, dev_minor), DEV_MIN_NUM);
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(test_init);
module_exit(test_exit);
出于linux内核编码习惯,通常会为设备定义一个相关的结构体,该结构体包含设备所涉及的cdev、私有数据以及锁等信息。该案例中的设备结构体为test_dev_t。设备条目可以在/proc/devices中查看。
生成设备节点
在Linux中,万物皆文件,设备也要被抽象成文件,设备文件叫做设备节点,生成设备节点有两种方法:手动添加、自动生成。设备节点文件可以在/dev/目录下看到。
手动添加
手动添加指的是,模块加载完成后,在命令行下输入下面的命令来生成设备节点:
mknod /dev/test c 230 0
其中c用来标识字符设备,230是主设备号,0是次设备号,执行完该指令后会在/dev下生成test设备节点文件,应用程序可以像操作普通文件一样操作该文件。
自动添加
自动添加指的是在模块加载的时候生成设备节点,源码如下所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/stat.h>
#include <linux/slab.h>
#include <linux/device.h>
#include <linux/moduleparam.h>
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Shijia Yin");
#define DEV_NAME "test"
#define DEV_MIN_NUM 2
#define DEV_MAJOR 0
#define DEV_MINOR 0
int dev_major = DEV_MAJOR;
int dev_minor = DEV_MINOR;
struct test_dev_t {
struct cdev cdev;
} test_dev;
struct file_operations test_fops = {
.owner = THIS_MODULE,
};
//定义test_class
static struct class *test_class;
static int test_init(void)
{
int err;
int ret = 0;
dev_t dev_num;
if(dev_major) {
dev_num = MKDEV(dev_major, dev_minor);
ret = register_chrdev_region(dev_num, DEV_MIN_NUM, DEV_NAME);
} else {
ret = alloc_chrdev_region(&dev_num, dev_minor, DEV_MIN_NUM, DEV_NAME);
dev_major = MAJOR(dev_num);
printk(KERN_EMERG "major number is %d !\n", dev_major);
}
if(ret < 0) {
printk(KERN_EMERG "register_chrdev_region req %d is failed!\n", dev_major);
}
cdev_init(&test_dev.cdev, &test_fops);
test_dev.cdev.owner = THIS_MODULE;
test_dev.cdev.ops = &test_fops;
err = cdev_add(&test_dev.cdev, dev_num, 1);
//创建设备节点
test_class = class_create(THIS_MODULE, DEV_NAME);
device_create(test_class, NULL, dev_num, NULL, DEV_NAME);
if(err) {
printk(KERN_EMERG "cdev_add %d is fail! %d\n", dev_minor, err);
} else {
printk(KERN_EMERG "cdev_add %d is success! \n", dev_minor);
}
printk(KERN_ALERT "Hello, world\n");
return 0;
}
static void test_exit(void)
{
printk(KERN_EMERG "test exit!\n");
cdev_del(&test_dev.cdev);
//删除设备节点
device_destroy(test_class, MKDEV(dev_major, dev_minor));
//删除test_class
class_destroy(test_class);
unregister_chrdev_region(MKDEV(dev_major, dev_minor), DEV_MIN_NUM);
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(test_init);
module_exit(test_exit);
与生成设备节点相关的代码已用注释标出。
file_operations成员列举
file_operations结构体内容如下,实际上,驱动开发的主要时间用于下面函数的实现:
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
int (*readdir) (struct file *, void *, filldir_t);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/* end remove */
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 *, 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 **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
/* end add */
};
总结
整个原始设备驱动编写流程大致可分为如下几步:
- 定义设备结构体xxx_dev_t与file_operations
- 实现file_operations中的各个成员
- 申请设备号
- 初始化cdev,即将cdev与file_operations绑定
- 向系统添加cdev,即将cdev与前面申请的设备号相绑定
- 创建设备节点
- 编写模块退出函数:释放设备号、删除cdev、删除类、销毁设备节点等
引用
[1] 讯为驱动开发资料
[2] Linux内核源码
[3] 《Linux设备驱动程序开发详解》宋宝华 第一版