在前面的三章里我们完成了驱动的框架、应用程序的编写,但是并没有实现文件的实际读写功能(只是通过内核打印出了调试信息)。这一章我们着重实现文件实际的读写效果。
由于没有实际数据IO,我们只是在驱动中定义一个数据传递给应用程序;在应用程序中定义个用户数据用来传递给内核(驱动)。
内核空间和用户空间的数据交互
因为用户和驱动之间的数据是分别存在在内核空间和用户空间中,我们需要在内核空间和数据空间之间做数据交互。这就要用到下面内核里的两个函数(uaccess.h中):
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n) { if (access_ok(VERIFY_READ, from, n)) n = __copy_from_user(to, from, n); else /* security hole - plug it */ memset(to, 0, n); return n; } static inline unsigned long __must_check copy_to_user(void __user *to, const void *from, unsigned long n) { if (access_ok(VERIFY_WRITE, to, n)) n = __copy_to_user(to, from, n); return n; }
从函数名字就可以看出来,一个是从用户空间(user)拷贝数据(copy_from_user)另一个是向用户空间复制数据(copy_to_user)。通过这两个函数,就可以实现数据在内核空间和用户空间进行交互。
驱动程序编写
驱动程序的编写是在上一章的驱动上完成的,只是修改了dev_write和dev_read两个函数,还新增了两个变量用来存储数据。
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/fs.h> #include <linux/uaccess.h> #define DEV_MAJOR 200 //设备号 #define DEV_NAME "DEVICE_TEST" //设备名称 static char kerneldata[] = {"test data from kernel!"}; //测试用内核数据 static char writebuf[100]; //写缓存 // /** // * @brief 打开设备文件 // * // * @return int // */ static int dev_open(struct inode *inode, struct file *filp) { printk("dev open!\r\n"); return 0; } /** * @brief 关闭设备文件 * * @return int */ static int dev_release(struct inode *inode, struct file *filp) { printk("dev release!\r\n"); return 0; } /** * @brief 读设备文件数据 * * @param filp * @param buf * @param count * @param ppos * @return ssize_t */ static ssize_t dev_read(struct file *filp, __user char *buf, size_t count, loff_t *ppos) { int ret = 0; printk("dev read data!\r\n"); ret = copy_to_user(buf,kerneldata,sizeof(kerneldata)); //向用户空间写入数据 if (ret == 0){ return 0; } else{ printk("kernel read data error!"); return -1; } } /** * @brief 设备文件数据写入 * * @param file * @param buf * @param count * @param ppos * @return ssize_t */ static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { int ret = 0; printk("dev write data!\r\n"); ret = copy_from_user(writebuf,buf,count); //从用户空间获取数据写入内核空间(writebuf) if (ret == 0){ printk("get data from APP:%s\r\n",writebuf); return 0; } else{ printk("kernelwrite err!\r\n"); return -1; } } /** * @brief 文件操作结构体 * */ static struct file_operations testDev_fops= { .owner = THIS_MODULE, .open = dev_open, .release = dev_release, .read = dev_read, .write = dev_write, }; /** * @brief 初始化 * * @return int */ static int __init dev_init(void) { int ret = 0; printk("device init!\r\n"); //字符设备注册 ret = register_chrdev(DEV_MAJOR, DEV_NAME, &testDev_fops); if(ret < 0 ){ printk("device init failed\r\n"); } return 0; } /** * @brief 卸载 * */ static void __exit dev_exit(void) { //字符设备注销 unregister_chrdev(DEV_MAJOR,DEV_NAME); printk("device exit\r\n"); } module_init(dev_init); //模块加载 module_exit(dev_exit); //模块卸载 MODULE_LICENSE("GPL"); MODULE_AUTHOR("ZeqiZ");
整个过程先放上面,在后面结合应用程序来大概讲一下。主要就是看下读和写两个函数。
应用程序
应用程序修改的地方大一些
/** * @file testAPP.c * @author your name (you@domain.com) * @brief * @version 0.1 * @date 2022-04-02 * * @copyright Copyright (c) 2022 * */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> /** * @brief * * @param argc //参数个数 * @param argv //参数 * @return int */ int main(int argc,char *argv[]) { char *filename; //文件名 filename = argv[1]; //文件名为命令行后第二个参数(索引值为1) int ret = 0; //初始化操作返回值 int f = 0; //初始化文件句柄 int action = atoi(argv[2]); //读写标志:0为读,1为写 char readbuf[100]; //初始化读数据缓存 char writebuf[100]; //初始化写数据缓存 char testdata[] = {"data from user Application!"}; //测试数据,准备写入内核的数据 if(argc != 3){ //输入参数数量!=3,提示输入格式错误! printf("input format error!"); } f = open(filename, O_RDWR); //打开文件 if(f < 0){ printf("open file %s failed!\r\n", filename); } /** * @brief 参数为0,进行读操作 * */ if(action == 0){ ret = read(f, readbuf , 100); if (ret < 0) { printf("read err!"); return -1; } else{ printf("read data from kernel:%s\r\n",readbuf); /*sleep1秒,等待printf函数打印完成*/ sleep(1); ret = close(f); return 0; } } /** * @brief 参数为1,进行写操作 * */ else if(action == 1){ ret = write(f, testdata, sizeof(testdata)); if(ret <0){ printf("write err!"); return -1; } else{ close(f); return 0; } } else{ close(f); return 0; } }
因为我们通过应用文件操作驱动文件主要的方式就是读和写,所以方便起见命令的格式为
./APPtest /dev/testDev 0
一共3个参数,第一个是应用程序名称,第二个是设备节点文件,第三个是读写标志位。在函数一开始会判断参数数量是否和要求格式一致(3个),不一致时提示格式错误。正常的话打开文件进行后续操作。
文件读写操作
这一章主要就是讲一下怎么实现文件的读写操作。
读数据
当我们需要获取硬件的数据时(例如获取传感器信息)就要使用到读操作。在挂载模块并加载设备文件节点以后通过命令行输入如下命令
./testAPP /dev/testDev 0
第三个参数为0(argv[2]),在APP里action变量值就为0。通过if据判断调用read函数。read函数中的参数为文件句柄f,读取缓存readbuf,读取的数据长度100。在执行read函数时,内核执行文件操作结构体内read对应的函数dev_read。dev_read里最主要的功能就是调用下面的函数
ret = copy_to_user(buf,kerneldata,sizeof(kerneldata)); //向用户空间写入数据
就是将kerneldata变量里的值传递给用户空间。用户空间在拿到值以后通过printf函数打印出来。
这里有个bug:
printf函数和printk函数的优先级不知道是不是有什么关系,在打印readbuf里的内容时,在打印了一半的信息后关闭文件的提示信息会被printk打印出来。然后再继续打印(类似中断的效果)。所以我在这里加了个sleep,等了1秒钟,等消息被打印完全了再关闭文件。
写数据
写数据和读数据的过程基本一致,在命令行内输入下面的命令
./testAPP /dev/testDev 1
通过第3个参数1,程序调用if判断执行write函数,在write函数里我们将testdata给到buf内,内核中使用
ret = copy_from_user(writebuf,buf,count); //从用户空间获取数据写入内核空间(writebuf) if (ret == 0){ printk("get data from APP:%s\r\n",writebuf); return 0; }
copy_from_user函数,将数据读出至writebuf内,如果无异常就将writebuf里的数据通过printk打印出来,而这里的打印信息和关闭的打印信息都是通过内核的printk打印的,所以不存在前面的bug。这样整个驱动就完成了!