# 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

image-20210218184131092

# 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

有了基本的数据结构,我们来看字符驱动初始化进行流程:

image-20210218182429713

  • 获取设备号:这是注册驱动的基石

    • 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

demo.c
#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 进行查看

test.c
#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 :测试代码
  1. 编译驱动代码和测试代码
命令行提示符
make # 编译驱动代码
gcc test.c -o test --static  #编译测试代码
sudo chmod 777 test   #赋予文件执行权限
  1. 载入驱动并查看分配到的主设备号
命令行提示符
sudo insmod xx.ko
#使用任意一种查看设备号
cat /proc/devices #查看设备号 
dmesg # 查看主设备号

image-20200925155857177

  1. 创建操作节点并进行测试
命令行提示符
$ sudo mknod /dev/demodriver c 238 0 #创建设备节点
$ sudo ./test
$ dmesg   #查看消息

image-20200925160425610

  1. 完成测试退出
命令行提示符
#退出顺序
$ sudo rmmod _demoDriver
$ sudo rm /dev/demodriver
$ sudo make clean

# Summary

本文主要介绍了字符型驱动的知识相关,以及编写一个字符型驱动的全流程和测试过程

# Appendix

函数操作集的重载编写要注意的是:参数要和以下的统一,

file_fops
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;
更新于

请我喝[茶]~( ̄▽ ̄)~*

Junwide Xiao 微信支付

微信支付

Junwide Xiao 支付宝

支付宝