本文使用环境:

内核版本:Linux 5.4.31

硬件平台:armv7 / stm32mp157

编译环境:Ubuntu Linux 18.04.4 LTS / gcc version 8.4.0 (Buildroot 2020.02.3-00002-gee623e2fe0-dirty)

0. 引言

linux驱动程序运行于内核空间,应用程序运行于用户空间,如何在用户空间访问驱动程序?驱动加载时生成设备节点,如/dev/hello,应用程序可将这个节点当看成用户空间下的普通文件,应用程序使用open/read/writeIO接口操作节点文件时,驱动程序提供相应的hello_drv_open/hello_drv_read/hello_drv_write等驱动接口支持。

驱动基础——hello驱动_模块加载

linux内核使用面向对象的编程思想,将多个驱动函数指针组织到一个file_operations中,驱动注册时将这个结构体传入内核,根据返回的设备号生成设备节点,这样就可以将驱动接口和设备节点相关联,因此驱动设计的核心是实现file_operations结构体中定义的函数。

本文实现一个驱动程序的雏形——hello驱动,提供:app调用write函数时,将用户空间数据写入内核,app调用read函数时,将内核中写入的数组返回给用户空间app

1. hello驱动

1.1 驱动组成与编写步骤

hello驱动使用内核模块加载的方式运行,因此必须存在模块加载函数模块卸载函数以及通用许可证声明

/* 声明模块加载与卸载函数 */
module_init(hello_init);
module_exit(hello_exit);

/* 遵循GPL协议 */
MODULE_LICENSE("GPL");

光有以上部分还不够,完整的程序还需要定义file_operations结构体变量并实现驱动函数填充file_operations结构体变量,最后需要完善模块加载函数模块卸载函数。基于以上思路,驱动编写步骤可以分为:

  1. 确定主设备号,用于生成设备节点;
  2. 定义file_operations结构体变量;
  3. 实现file_operations结构体变量中的open/read/write/close等驱动函数,并填入file_operations结构体变量;
  4. 完善模块加载函数,实现驱动注册、生成设备节点等操作,模块被加载时调用模块加载函数;
  5. 完善模块卸载函数,实现驱动卸载、注销设备节点等操作,模块被卸载时调用模块卸载函数。

1.2 hello驱动代码

hello_drv.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

/* 函数声明 */
static ssize_t hello_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset);
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset);
static int hello_drv_open(struct inode *node, struct file *file);
static int hello_drv_close(struct inode *node, struct file *file);

/* 1. 确定主设备号 */
static int major = 0;
static char kernel_buf[1024];/* 保存用户空间传入的字符 */
static struct class *hello_class;

#define MIN(a, b) ((a)<(b)?(a):(b))

/* 2. 定义自己的file_operations结构体变量,填入驱动函数指针 */
static struct file_operations hello_drv = 
{
    /* data */
    .owner  = THIS_MODULE,
    .open   = hello_drv_open,
    .read   = hello_drv_read,
    .write  = hello_drv_write,
    .release= hello_drv_close,
};


/* 3. 实现对应的open/read/write等函数,用于填入file_operations结构体 */
static ssize_t hello_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	int err;
	
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);

	/* 用户空间不能直接访问内核空间buf,需要copy_to_user */
	err = copy_to_user(buf, kernel_buf, MIN(size, 1024));
	return MIN(size, 1024);
}

static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	int err;
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
    /* 内核空间不能直接访问用户空间buf,需要copy_from_user */
	err = copy_from_user(kernel_buf, buf, MIN(1024, size));
	return MIN(1024, size);
}

static int hello_drv_open(struct inode *node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

static int hello_drv_close(struct inode *node, struct file *file)
{
    printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	return 0;
}

/* 4. 模块加载函数入口,模块被加载时,hello_init被调用 */
static int __init hello_init(void)
{
	int err;

	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 返回主设备号,第一个参数为0将由系统分配主设备号 */
	major = register_chrdev(0, "hello", &hello_drv);

	/* 应用程序要访问驱动程序,需要设备节点,如:/dev/hello */
	/* 1. 创建class,用于创建device */
	hello_class = class_create(THIS_MODULE, "hello_class");
	err = PTR_ERR(hello_class);
	if(IS_ERR(hello_class))
	{
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		/* return -1 前先卸载 */
		unregister_chrdev(major, "hello");
		return -1;
	}
	/* 2. 创建device,自动创建设备节点、/dev/hello */
	device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
	
	return 0;
}

/* 5. 模块卸载函数入口,模块被卸载时,hello_exit被调用 */
static void __exit hello_exit(void)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	/* 1. 销毁device */
	device_destroy(hello_class, MKDEV(major, 0));
	
	/* 2. 销毁class */
	class_destroy(hello_class);
	
	/* 3. 释放主设备号 */
	unregister_chrdev(major, "hello");
}


/* 声明模块加载与卸载函数 */
module_init(hello_init);
module_exit(hello_exit);

/* 遵循GPL协议 */
MODULE_LICENSE("GPL");

1.3 hello app代码

hello_drv_test.c

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

/*
 * ./hello_drv_test -w abc
 * ./hello_drv_test -r
 */
int main(int argc, char **argv)
{
	int fd;
	char buf[1024];
	int len;
	
	/* 1. 判断参数 */
	if (argc < 2) 
	{
		printf("Usage: %s -w <string>\n", argv[0]);
		printf("       %s -r\n", argv[0]);
		return -1;
	}

	/* 2. 打开文件 */
	fd = open("/dev/hello", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/hello\n");
		return -1;
	}

	/* 3. 写文件或读文件 */
	if ((0 == strcmp(argv[1], "-w")) && (argc == 3))
	{
		len = strlen(argv[2]) + 1;
		len = len < 1024 ? len : 1024;
		write(fd, argv[2], len);
	}
	else
	{
		len = read(fd, buf, 1024);		
		buf[1023] = '\0';
		printf("app read : %s\n", buf);
	}
	
	close(fd);
	
	return 0;
}

1.4 修改Makefile

修改当前hello_drv.c源码路径下Makefile文件,该Makefile将调用内核目录中的Makefile完成hello_drv模块与hello_drv_test app编译。

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册

KERN_DIR = /home/ryan/100ask_stm32mp157_pro-sdk/Linux-5.4

all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o hello_drv_test hello_drv_test.c 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
	rm -f hello_drv_test

obj-m	+= hello_drv.o

1.5 模块编译

Makefile修改完成后,在hello_drv.c源码路径下执行make命令编译模块,编译完成后将在hello_drv.c源码路径下产生hello_drv.ko文件和hello_drv_test app可执行文件,将这两个文件文件拷贝到开发板NFS目录下备用,这里文件在NFS目录下,因此无需拷贝。

驱动基础——hello驱动_设备节点_02

1.6 hello驱动测试

开发板初始化完成后,执行mount -t nfs -o nolock,vers=3,port=2049,mountport=9999 192.168.3.2:/home/ryan/nfs_rootfs/ /mnt命令挂载NFS目录,通过NFS目录进入驱动二进制文件目录/drv_code/01_hello_drv

驱动基础——hello驱动_#include_03

执行insmod hello_drv.ko加载模块,通过ls -l /dev/hello查看设备节点

驱动基础——hello驱动_设备节点_04

执行./hello_drv_test查看app用法

驱动基础——hello驱动_用户空间_05

执行./hello_drv_test -w hello_drv_module_test写入数据

驱动基础——hello驱动_#include_06

执行./hello_drv_test -r读取数据,输出为写入的数据

驱动基础——hello驱动_设备节点_07

执行rmmod hello_drv卸载模块后hello_drv模块被注销

驱动基础——hello驱动_设备节点_08

2. 小结

本文分析了hello驱动程序的基本组成与编写步骤,hello驱动模块包含模块加载函数模块卸载函数以及通用许可证声明,在此基础上,仍需完善驱动程序:

  1. 确定主设备号,用于生成设备节点;
  2. 定义file_operations结构体变量;
  3. 实现file_operations结构体变量中的open/read/write/close等驱动函数,并填入file_operations结构体变量;
  4. 完善模块加载函数,实现驱动注册、生成设备节点等操作,模块被加载时调用模块加载函数;
  5. 完善模块卸载函数,实现驱动卸载、注销设备节点等操作,模块被卸载时调用模块卸载函数。

最后,通过编写测试程序调用驱动,完成hello驱动的验证。

参考
  • 《嵌入式Linux应用完全开发手册_韦东山全系列视频文档全集v2.8》第5篇