# Kernel Module — Char Device Driver
Goal
上文我们基本了解了什么是内核模块和加载类型,并编写自己的第一个模块。而本文开始介绍 Linux 中内核模块中的字符设备驱动,了解字符设备驱动的特点和编写简单的字符驱动。
Preconditions
# What is “Device Driver”
设备驱动指的是操作系统和输入输出设备间的粘合剂,驱动负责将操作系统的请求传输,转化成特定物理设备控制器能够理解的命令。而内核模块就是驱动的载体。Linux 系统内核将设备对象的特征进行了抽象,总结除了以下三种模型:
- Char Device Driver:
- 特点:是以字节为单位的 I/O 传输,这种字符流的传输率比较低
- 对象:鼠标,键盘,触摸屏
- Block Device Driver:
- 特点:是以块为单位传输,这种的传输率比较高
- 对象:磁盘,闪存
- Network Device Driver
- 涉及到网络协议层
# Basic Char Device
# Data Structure
字符设备驱动管理的核心对象是以字符为传输流的设备,在 Linux 内核中,使用了一个 struct cdev
来进行描述其属性
struct cdev { | |
struct kobject kobj; // 这是用于设备驱动模型管理的结构 | |
struct module *owner; // 字符设备驱动的内核模块对象指针,包含了模块的详细信息 | |
const struct file_operations *ops; // 字符设备驱动的函数操作集 | |
struct list_head list; // 一个 List_head 的链表结构,将所有设备驱动串起来 | |
dev_t dev; // 字符设备的设备号 | |
unsigned int count; // 同属一个主设备号的次设备号个数 | |
} __randomize_layout; |
__randomize_layout:随机化重排内存排列,这是一种内核安全保护手段
这里有两个重点理解的的内容
dev_t
:字符设备的设备号(是由主设备和次设备号的组成)主设备号:用来区分是什么类型的设备
次设备号:用来驱分同一类型内的多个设备
两个对应的宏:
MAJOR
:用来获取主设备号MINOR
:用来获取次设备号宏操作 #define MINORBITS 20 // 主设备和次设备的分界(后 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)) // 创建设备号的宏
file_operations *
:字符设备驱动的函数操作集- 这是一个结构体指针,这个结构体中包含了常用的函数操作定义
- 这是内核面向对象的体现,统一了上层的调用接口。而我们可以通过对函数指针指向来实现具体功能
这两个是字符设备驱动最核心部分,因为设备号的分配表示内核是否还有相关的资源。同时我们创建操作节点是也是需要依靠设备号来定位具体的设备,因为多个同类设备时使用统一的设备驱动,而之间主要的区分和定位方式就是利用设备号。而函数操作集则是我们实现具体驱动行为的关键,这部分的实现就要依据具体设备的数据手册来实现。
# Init. Framework
有了基本的数据结构,我们来看字符驱动初始化进行流程:
获取设备号:这是注册驱动的基石
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
(推荐使用这个)- dev:分配到的设备号输出到 dev
- baseminor:请求主设备号范围开始的第一个,也就是最小值
- count:次设备数目
- name:驱动名
- 返回值:0 为成功,负数为失败
int register_chrdev_region(dev_t from, unsigned count, const char *name)
- from:手动分配设备号的其实设备号(需要自己查阅,系统没有分配的)
- count:次设备的数量
- name:驱动名
- 返回值:0 为成功,负数为失败
- 以上两个 API 均能分配设备号,前者自动分配,后者需要手动
- 两个都通过调用:
__register_chrdev_region
来完成分配
分配字符设备的数据结构:这是驱动的核心内容
cdev_alloc()
:向内核申请字符驱动的数据结构- 如果分配失败,那么整个后续无法进行,因此会释放到之前申请到设备号
调用:
unregister_chrdev_region(dev,count)
来释放回系统cdev_init(struct cdev *cdev, const struct file_operations *fops)
:初始化cdev:字符设备对象的数据
fops:对应的操作函数集,这里相当于函数重载的意思
fops static const struct file_operations demo_fops = // 对应的操作函数集
{
.owner = THIS_MODULE,
.open = demo_open,
.release = demo_release,
.read = demo_read,
.write = demo_write
};
这个函数将字符设备的数据和函数操作关联起来
cdev_add(struct cdev *p, dev_t dev, unsigned count)
:将字符设备驱动加到系统中- p:字符设备对象的指针
- dev:设备号
- count:次设备数量
- 返回值:0 为整个,负数为失败
- 失败处理:
- 如果失败了,后续操作也无法执行,所以需要跳转到释放字符设备对象的数据
- 调用:
cdev_del(cdev * p)
将 cdev 从系统中移除,并 freeing cdev 的数据结构 - 调用:
unregister_chrdev_region(dev,count)
来释放回系统
使用完的释放:遵循栈法则,后进先注销
- 先处理:字符设备对象结构
- 后处理:给字符设备分配的设备号
# Source Code
#include <linux/module.h> | |
#include <linux/init.h> | |
#include <linux/fs.h> | |
#include <linux/cdev.h> | |
#include <linux/uaccess.h> | |
#define DEMO_NAME "demodriver" | |
static unsigned int count =1 ; // 在一个主设备号下,次设备数量统计 | |
struct demoDriver_cdev | |
{ | |
dev_t dev ; // 字符设备的设备号 | |
struct cdev *pdev; // 字符设备的数据结构 | |
/* data 可以自定义数据 */ | |
} demo_ctx = { | |
.dev = 0, | |
.pdev = NULL, | |
}; | |
static int demo_open(struct inode * inode,struct file * file) | |
{ | |
// 我们将驱动做成一个设备节点,所以可以用 inode 结构来访问,驱动信息 | |
int major = MAJOR(inode->i_rdev); | |
int minor = MINOR(inode->i_rdev); | |
printk(KERN_EMERG"%s: Major Num: %d , Minor Num: %d",__func__,major,minor); | |
return 0; | |
} | |
static int demo_release(struct inode * inode,struct file * file) | |
{ | |
return 0; | |
} | |
static ssize_t demo_read(struct file * file, char __user *buf, size_t lbuf,\ | |
loff_t *ppos) | |
{ | |
printk(KERN_EMERG"%s enter",__func__); | |
return 0; | |
} | |
static ssize_t demo_write(struct file * file, const char __user *buf, size_t count,\ | |
loff_t *f_pos) | |
{ | |
printk(KERN_EMERG"%s enter",__func__); | |
return 0; | |
} | |
static const struct file_operations demo_fops = | |
{ | |
.owner = THIS_MODULE, | |
.open = demo_open, | |
.release = demo_release, | |
.read = demo_read, | |
.write = demo_write | |
}; | |
static int __init demo_init(void) | |
{ | |
int ret; | |
// 系统自动分配一个主设备号,使用这个接口避免冲突,这里需要 4 个参数 cdev,baseminor ,count ,name | |
ret = alloc_chrdev_region(&demo_ctx.dev, 0 ,count,DEMO_NAME); | |
if(ret) | |
{ | |
printk(KERN_EMERG"Faild in alloc char device region"); | |
return ret; | |
} | |
demo_ctx.pdev = cdev_alloc(); // 申请设备数据结构 | |
if (!demo_ctx.pdev) | |
{ | |
printk(KERN_EMERG"cdev_alloc faild"); | |
goto unregister_chrdev; | |
} | |
cdev_init(demo_ctx.pdev,&demo_fops); // 结构体和操作函数的指针建立联系 | |
ret = cdev_add(demo_ctx.pdev,demo_ctx.dev,count); // 将设备添加到系统中,传入主设备号和次设备数目 | |
if(ret) | |
{ | |
printk(KERN_EMERG"cdev_add faild"); | |
goto cdev_fail; | |
} | |
printk(KERN_EMERG"success register char device: %s\n",DEMO_NAME); | |
printk(KERN_EMERG"Major Num: %d , Minor Num: %d",MAJOR(demo_ctx.dev),MINOR(demo_ctx.dev)); | |
return 0; | |
// 如果在注册驱动过程失败,则使用进行注销 | |
cdev_fail: | |
cdev_del(demo_ctx.pdev); | |
unregister_chrdev: | |
unregister_chrdev_region(demo_ctx.dev,count); // 卸载掉,释放回系统 | |
return ret; | |
} | |
static void __exit demo_exit(void) | |
{ | |
printk("remove Device\n"); | |
if(demo_ctx.pdev) | |
cdev_del(demo_ctx.pdev); | |
unregister_chrdev_region(demo_ctx.dev,count); | |
} | |
module_init(demo_init); | |
module_exit(demo_exit); | |
// 模块可选信息 | |
MODULE_LICENSE("GPL");// 许可证声明 | |
MODULE_AUTHOR("junwide");// 作者声明 | |
MODULE_DESCRIPTION("This module is a char device");// 模块描述 | |
MODULE_VERSION("V1.0");// 模块别名 | |
MODULE_ALIAS("Char Modules");// 模块别名 |
# Build Code
使用和之前一样的 Build Code 通用模板,不在赘述。
# Usage
命令 | 解释 |
---|---|
make | 编译 Modules 工程 |
sudo make install | 将编译后的 Modules 放进系统目录,建立依赖关系,探测启动 modules |
sudo make boot | 将这个 modules 设置为开机自动加载 |
sudo make rboot | 将这个 modules 的开机启动删除 |
sudo make clean | 清空这个 modules 编译出来的所有东西 |
# User Code
用户测试代码,其原理就是打开字符设备,读取操作(实际没有读出内容),这里验证的是是否有跳进响应和函数中,
如果有跳进,可以通过 dmesg
进行查看
#include <stdio.h> | |
#include <fcntl.h> | |
#include <unistd.h> | |
#define DEMO_DEV_NAME "/dev/demodriver" | |
int main() | |
{ | |
char buffer[64]; | |
int fd; | |
fd = open(DEMO_DEV_NAME, O_RDONLY); | |
if (fd < 0) { | |
printf("open device %s failded\n", DEMO_DEV_NAME); | |
return -1; | |
} | |
read(fd, buffer, 64); | |
close(fd); | |
return 0; | |
} |
# Test
这篇我们讨论的驱动需要总共有 3 个文件
demo.c
:驱动模块代码Makefiele
:构建代码Test.c
:测试代码
- 编译驱动代码和测试代码
make # 编译驱动代码 | ||
gcc test.c -o test --static #编译测试代码 | ||
sudo chmod 777 test #赋予文件执行权限 |
- 载入驱动并查看分配到的主设备号
sudo insmod xx.ko | ||
#使用任意一种查看设备号 | ||
cat /proc/devices #查看设备号 | ||
dmesg # 查看主设备号 |
- 创建操作节点并进行测试
$ sudo mknod /dev/demodriver c 238 0 #创建设备节点 | ||
$ sudo ./test | ||
$ dmesg #查看消息 |
- 完成测试退出
#退出顺序 | ||
$ sudo rmmod _demoDriver | ||
$ sudo rm /dev/demodriver | ||
$ sudo make clean |
# Summary
本文主要介绍了字符型驱动的知识相关,以及编写一个字符型驱动的全流程和测试过程
# Appendix
函数操作集的重载编写要注意的是:参数要和以下的统一,
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 (*iopoll)(struct kiocb *kiocb, bool spin); | |
int (*iterate) (struct file *, struct dir_context *); | |
int (*iterate_shared) (struct file *, struct dir_context *); | |
__poll_t (*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 *); | |
unsigned long mmap_supported_flags; | |
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 (*setfl)(struct file *, unsigned long); | |
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); | |
loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in, | |
struct file *file_out, loff_t pos_out, | |
loff_t len, unsigned int remap_flags); | |
int (*fadvise)(struct file *, loff_t, loff_t, int); | |
} __randomize_layout; |