本文使用环境:
内核版本:Linux 5.4.31
硬件平台:armv7 / stm32mp157
编译环境:Ubuntu Linux 18.04.4 LTS / gcc version 8.4.0 (Buildroot 2020.02.3-00002-gee623e2fe0-dirty)
linux驱动程序运行于内核空间,应用程序运行于用户空间,如何在用户空间访问驱动程序?驱动加载时生成设备节点,如/dev/hello,应用程序可将这个节点当看成用户空间下的普通文件,应用程序使用open/read/write等IO接口操作节点文件时,驱动程序提供相应的hello_drv_open/hello_drv_read/hello_drv_write等驱动接口支持。
linux内核使用面向对象的编程思想,将多个驱动函数指针组织到一个file_operations中,驱动注册时将这个结构体传入内核,根据返回的设备号生成设备节点,这样就可以将驱动接口和设备节点相关联,因此驱动设计的核心是实现file_operations结构体中定义的函数。
本文实现一个驱动程序的雏形——hello驱动,提供:app调用write函数时,将用户空间数据写入内核,app调用read函数时,将内核中写入的数组返回给用户空间app。
hello驱动使用内核模块加载的方式运行,因此必须存在模块加载函数、模块卸载函数以及通用许可证声明。
/* 声明模块加载与卸载函数 */ module_init(hello_init); module_exit(hello_exit); /* 遵循GPL协议 */ MODULE_LICENSE("GPL");光有以上部分还不够,完整的程序还需要定义file_operations结构体变量并实现驱动函数填充file_operations结构体变量,最后需要完善模块加载函数和模块卸载函数。基于以上思路,驱动编写步骤可以分为:
确定主设备号,用于生成设备节点;定义file_operations结构体变量;实现file_operations结构体变量中的open/read/write/close等驱动函数,并填入file_operations结构体变量;完善模块加载函数,实现驱动注册、生成设备节点等操作,模块被加载时调用模块加载函数;完善模块卸载函数,实现驱动卸载、注销设备节点等操作,模块被卸载时调用模块卸载函数。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");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; }修改当前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.oMakefile修改完成后,在hello_drv.c源码路径下执行make命令编译模块,编译完成后将在hello_drv.c源码路径下产生hello_drv.ko文件和hello_drv_test app可执行文件,将这两个文件文件拷贝到开发板NFS目录下备用,这里文件在NFS目录下,因此无需拷贝。
开发板初始化完成后,执行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
执行insmod hello_drv.ko加载模块,通过ls -l /dev/hello查看设备节点
执行./hello_drv_test查看app用法
执行./hello_drv_test -w hello_drv_module_test写入数据
执行./hello_drv_test -r读取数据,输出为写入的数据
执行rmmod hello_drv卸载模块后hello_drv模块被注销
本文分析了hello驱动程序的基本组成与编写步骤,hello驱动模块包含模块加载函数、模块卸载函数以及通用许可证声明,在此基础上,仍需完善驱动程序:
确定主设备号,用于生成设备节点;定义file_operations结构体变量;实现file_operations结构体变量中的open/read/write/close等驱动函数,并填入file_operations结构体变量;完善模块加载函数,实现驱动注册、生成设备节点等操作,模块被加载时调用模块加载函数;完善模块卸载函数,实现驱动卸载、注销设备节点等操作,模块被卸载时调用模块卸载函数。最后,通过编写测试程序调用驱动,完成hello驱动的验证。