# Kernel Module — Char Device Basic II

image-20210420235054817

Goal

上一篇介绍了一般字符驱动的开发逻辑,而这一篇主要是讲一般的字符型驱动以外的,其他类字符型驱动设备的开发逻辑,以及内核里非常巧妙的内核无锁环形缓存区 KFIFO 的使用和具体的实现逻辑。

Preconditions

# MISC Char Device

Miscellaneous Devices:其他设备,也就是指没有在 Linux Kernel Document 中写明的驱动类型,都可以用 Misc 机制来注册和管理驱动。因为有时人们需要编写 “小型” 设备驱动程序,以支持自定义的 hack(硬件或软件),而 Misc 的好处就是简化了我们对 Module 开发的步骤,从注册到创建设备节点都一站式完成,使得整个流程非常顺滑。但是 Misc 驱动也有自己缺陷,就是无法注册多个设备节点

  • MISC 机制的内容

    • miscdevice :管理着所有信息

    • struct miscdevice  {
      	int minor;                     // 标志为动态分配
      	const char *name;             // 模块名
      	const struct file_operations *fops;  // 对应我们的函数集
      	struct list_head list;
      	struct device *parent;
      	struct device *this_device;  // 这是注册完返回的设备
      	const struct attribute_group **groups;
      	const char *nodename;
      	umode_t mode;
      };
    • 注册和注销

      • int misc_register(struct miscdevice *misc) :注册一个 misc 设备
      • void misc_deregister(struct miscdevice *misc) :注销一个 misc 设备
    • 赋值:

      • static struct miscdevice mydemo_device_misc_d = {
            .minor = MISC_DYNAMIC_MINOR,  
            .name = DEMO_NAME,
            .fops = &demo_fops
        };

# Kernel Data Struct

  • KFIFO
    • 内核无锁环形缓冲区
    • 原理:实现一个读指针(指向可读区域),一个写指针(指向可写区域)在一个环形的缓冲区中操作
    • 实现:能够在一个读进程和一个写进程并发的场景下,无须加锁来保证数据的安全,当然还是提供了加锁的操作的(例如加了自旋锁的 in 和 out)
    • 操作:
      • 注册:
        • DEFINE_KFIFO(fifo, type, size) :声明和初始化一个 FIFO
        • kfifo_init(fifo, buffer, size) :声明和初始化一个 FIFO
        • kfifo_alloc(fifo, size, gfp_mask) :请求一个 FIFO 的 buffer
      • 操作:
        • kfifo_is_full :是否满
        • kfifo_is_empty : 是否空
        • kfifo_len :获得 fifo 已经使用的元素个数
        • kfifo_avail :获得 fifo 没有使用的个数
        • kfifo_size :获得 fifo 的大小
        • kfifo_reset :重置 fifo 的内容
      • 数据迁移:
        • kfifo_from_user(fifo, from, len, copied)
          • fifo :缓存区的对象
          • from :读取的地方
          • len :读取的数量
          • copied :读取成功的数量,会赋值到这个变量上
        • kfifo_to_user(fifo, to, len, copied)
          • fifo :缓存区的对象
          • to :存放的地方
          • len :读取的数量
          • copied :读取成功的数量,会赋值到这个变量上
      • 释放: kfifo_free(fifo) 释放一个 fifo 空间
  • I/O 非阻塞
    • 非阻塞:就是不等待,直接操作,要么操作成功,要么返回错误码
    • 阻塞:就是会等待,等待条件满足后唤醒
    • FLAG:O_NONBLOCK

# Source Code

demo.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/miscdevice.h> //misc 机制的头文件
#include <linux/uaccess.h>  
#include <linux/kfifo.h>    // 内核无环缓冲区
#define DEMO_NAME "kfifodriver"
static struct device * mydemo_device;
DEFINE_KFIFO(demo_fifo, char, 64);
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;
}
//loff_t 表示当前读写位置的 offset  
static ssize_t demo_read(struct file * file, char __user *buf, size_t buf_count,\
                            loff_t *ppos)
{    
    int ret;
    int readed_;
    if(kfifo_is_empty(&demo_fifo))
    {
        if(file->f_flags & O_NONBLOCK)
            return -EAGAIN;
    }
    ret = kfifo_to_user(&demo_fifo,buf, buf_count,&readed_); 
    if(ret)
       return -EIO;
    printk(KERN_EMERG"%s readed = %d, pos = %lld \n",__func__,readed_,*ppos);
    return readed_;
}
static ssize_t demo_write(struct file * file, const char __user *buf, size_t buf_count,\
                            loff_t *ppos)
{
    int ret;
    int writed_ ;
    if(kfifo_is_full(&demo_fifo))
    {
        if(file->f_flags & O_NONBLOCK)
            return -EAGAIN;
    }
    ret = kfifo_from_user(&demo_fifo,buf, buf_count,&writed_); // 返回没有拷贝成功的字节数
    if  (ret)
        return -EIO;
    printk(KERN_EMERG"%s writed = %d, pos = %lld \n",__func__,writed_,*ppos);
    return writed_;
}
static const struct file_operations demo_fops =
{
    .owner = THIS_MODULE,
    .open  = demo_open,
    .release = demo_release,
    .read = demo_read,
    .write = demo_write
};
static struct miscdevice mydemo_device_misc_d = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = DEMO_NAME,
    .fops = &demo_fops
};
static int __init demo_init(void)
{
    int ret;
    ret = misc_register(&mydemo_device_misc_d);
    if(ret) 
    {
        printk(KERN_EMERG"Faild in register misc device");
        return ret;
    }
    mydemo_device = mydemo_device_misc_d.this_device;
    printk(KERN_EMERG"success register misc device: %s\n",DEMO_NAME);
    return 0;
}
static void  __exit demo_exit(void)
{
    printk("remove Device\n");
    misc_deregister(&mydemo_device_misc_d);
}
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

Makefile

和 Single Module 的 Makefile 一样,不在赘述

# Test

test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define DEMO_DEV_NAME "/dev/kfifodriver"
int main()
{
	char buffer[64];
	int fd;
    int ret;
    char message[80] = "we come from china";
    char *r_buffer;
    size_t len = sizeof(message);
     
	fd = open(DEMO_DEV_NAME, O_RDWR | O_NONBLOCK);
	if (fd < 0) {
		printf("open device %s failded\n", DEMO_DEV_NAME);
		return -1;
	}
    r_buffer = malloc(len*2);
    memset(r_buffer,0,2*len);
    ret = read(fd,r_buffer,2*len);
    printf("read %d bytes\n",ret);
    printf("Context: %s\n",r_buffer);
    ret = write(fd,message,len);
    if( ret != len)
    {
        printf("can not write on device %d ret: %d \n",fd,ret);
        return -1;
    }
    ret = write(fd,message,len);
    if( ret != len)
        printf("can not write on device %d ret: %d \n",fd,ret);
    memset(r_buffer,0,2*len);
    ret = read(fd,r_buffer,2*len);
    printf("read %d bytes\n",ret);
    printf("Context: %s\n",r_buffer);
    close(fd);
	return 0;
}

编译和安装

Commad命令行
$ gcc test.c -o test --static
$ make #编译
$ sudo make install # 安装并启动
$ sudo ./test

image-20200925200044281

已经 KFIFO 满了,却没阻塞,再次写入引发错误

# Summary

这一篇主要内容是实现一个简单的 Misc 的驱动,同时能够调用 KFIFO 进行数据在用户态和内核态的传输。具体的 Misc 在内核实现的例子,可以参考内核中硬件看门狗的驱动实现。后面的拓展内容主要是介绍 KFIFO 的原理和实现的。

# More About KFIFO

KFIFO 全称为 Kernel First In First Out ,是内核无锁环形缓冲区,作为内核非常重要的数据结构,它有以下几个非常重要的特点,而这几个特点也将作为我们的切入点来了解这个设计巧妙的数据结构。无锁循环队列的应用范围很广泛,例如:在低频单片机中,串口接收数据(中断模式)可以将 ISR 分为 top half 和 bottom half,top half 负责接收数据并将数据存放到循环队列中,而 bottom half 负责从队列中取出数据并处理数据。用类似以上的实现,可以减少一个锁,从而实现更高的并发度和资源利用率。

  • 高效编程理念

  • 巧用环形缓冲

  • 并行无锁技术

KFIFO数据结构
struct __kfifo {
	unsigned int	in; /*  (in &= mask) 就是缓存区的入队索引 */
	unsigned int	out; /* (out &= mask) 缓存区的出队索引 */
	unsigned int	mask; /* 获取偏移的掩码 */
	unsigned int	esize; /* 每个元素所占的字节数 */
	void		*data; /* 指针:指向存放缓存的数据 */
};

# 高效编程理念

让我们先来看一代码,主要观察高亮的部分的写法。

KFIFO初始化参考链接
int __kfifo_alloc(struct __kfifo *fifo, unsigned int size,size_t esize, gfp_t gfp_mask)
{
	/*
	 * round up to the next power of 2, since our 'let the indices
	 * wrap' technique works only in this case.
	 */
	size = roundup_pow_of_two(size);
	fifo->in = 0;
	fifo->out = 0;
	fifo->esize = esize;
	if (size < 2) {
		fifo->data = NULL;
		fifo->mask = 0;
		return -EINVAL;
	}
	fifo->data = kmalloc_array(esize, size, gfp_mask);
	if (!fifo->data) {
		fifo->mask = 0;
		return -ENOMEM;
	}
	fifo->mask = size - 1;
	return 0;
}

这里使用了一个 roundup_pow_of_two() 的函数来完成大小的确定,这个函数主要的是作用有:

  • 找出与用户给定 size 最近的2n2^n 的数(例如: Size (用户) 7 , 找出的数是 8 (232^3))
  • 尽可能快的完成上一步的操作
找数参考链接
#define roundup_pow_of_two(n)			\
(						\
	__builtin_constant_p(n) ? (		\
		((n) == 1) ? 1 :		\
		(1UL << (ilog2((n) - 1) + 1))	\
				   ) :		\
	__roundup_pow_of_two(n)			\
 )

这里用到了两个方法来实现了高效的操作:

  1. ilog2() 函数调用的是汇编的 bsr 用来找到 n-1 的最高有效的 1 的位置
  2. 找到最高有效的 1 的位置后使用位移操作直接获取到有效的大小(2n2^n

bsr :bsr %0 %1 x86 汇编的一个指令,用于扫描一个 %1 中数的最高有效的 1 的位置索引,并存到 %0

例如:bsr % a #8 那么 a 中就会存 3

# 巧用环形缓冲

采用环形缓冲区的好处为,当一个数据元素被用掉后,其余数据元素不需要移动其存储位置,从而减少拷贝提高效率。

入队参考链接
static void kfifo_copy_in(struct __kfifo *fifo, const void *src,
		unsigned int len, unsigned int off)
{
	unsigned int size = fifo->mask + 1;
	unsigned int esize = fifo->esize;
	unsigned int l;
	off &= fifo->mask;
	if (esize != 1) {    // 这一部分主要是处理数据占大小的,这会影响指针寻址
		off *= esize;
		size *= esize;
		len *= esize;
	}
	l = min(len, size - off);
	memcpy(fifo->data + off, src, l);
	memcpy(fifo->data, src + l, len - l);
	/*
	 * make sure that the data in the fifo is up to date before
	 * incrementing the fifo->in index counter
	 */
	smp_wmb();
}
unsigned int __kfifo_in(struct __kfifo *fifo,
		const void *buf, unsigned int len)
{
	unsigned int l;
	l = kfifo_unused(fifo);
	if (len > l)
		len = l;
	kfifo_copy_in(fifo, buf, len, fifo->in);
	fifo->in += len;
	return len;
}

我们先看 kfifo_unused() 这个函数。这个函数主要计算出 kfifo 中还有多少空用空间,主要通过以下两点实现:

  • inout 都是 unsigned int
    • 这就使得无论两个的位置如何,相减总能得到两个数之间的差,即 kfifo 中的元素数量
  • (mask+1) 是整个 kfifo 的大小
    • (mask+1) 减去已经使用的元素数量,就能得到未使用的元素数量。
计算空间
static inline unsigned int kfifo_unused(struct __kfifo *fifo)
{
	return (fifo->mask + 1) - (fifo->in - fifo->out);
}

然后看 L31 ,这里做一个取小值,如果剩余的多则要入队的都可以加,如果剩余的少则要入队只能是剩下的个数。

然后就开始到入队的正式过程,也是环形缓冲区的真正体现。我们这里为了便于理解,我们进行以下的假设

  • 数据类型为 char
  • 数据类型所占的字节数 esize 为 1
  • 数据的 size 为 8
  • 已经使用的数量为 2

image-20210422140928997

我们先跳过 L8off &= fifo->mask; ,先假设我们已经知道 in/off 为 6,然后看 L14

l = min(len, size - off) ,这里对应的图中的 Step 1

  • (size-off) 计算的是偏移后面剩下的个数(包括使用的和未使用的)(这里是 2)
  • len 就是能够写入数量(这里是 6),这里有一个恒等关系 len >= (size - off)
  • 取小值,是获得写在数据右边的数量。

然后使用 memcpy(fifo->data + off, src, l); 把数据写入到右侧

然后再使用 memcpy(fifo->data, src + l, len - l); 把数据写入到左侧 这里对应图中的 Step 2

L35 这里直接更新 in 的位置(这里是 10)这里对应图中的 Step 3。然后现在会过头来看我们刚才跳过的 L8

如果下一次继续入队两个元素,那么先执行 L8 . off &= fifo->mask;

  • in = 10 (1010b)
  • mask = 7 (0111b)
  • off = 2 (0010b)

这样就完成了偏移的环形,而且不需要额外的判断。如果能够理解入队操作,那么出队操作也是类似的。

# 并行无锁技术

实际上是使用了内存屏障,我们知道 编译器编译源代码时,会将源代码进行优化,将源代码的指令进行重排序,以适合于 CPU 的并行执行。然而,内核同步必须避免指令重新排序,优化屏障(Optimization barrier)避免编译器的重排序优化操作,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。具体关于内存屏障的讨论,我们会在缓存和内存相关内容时进行讨论,这里只是简单的理解。

软件可通过读写屏障强制内存访问次序。读写屏障像一堵墙,所有在设置读写屏障之前发起的内存访问,必须先于在设置屏障之后发起的内存访问之前完成,确保内存访问按程序的顺序完成。Linux 内核提供的内存屏障 API 函数说明如下表。内存屏障主要用于多处理器系统,使用 smp_xxx 函数。

指令作用
smp_rmb()适用于多处理器的读内存屏障。
smp_wmb()适用于多处理器的写内存屏障。
smp_mb()适用于多处理器的内存屏障。
更新于

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

Junwide Xiao 微信支付

微信支付

Junwide Xiao 支付宝

支付宝