瑞芯微RK3568芯片是一款定位中高端的通用型SOC,采用22nm制程工艺,搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码,支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU,可用于轻量级人工智能应用。RK3568 支持安卓 11 和 linux 系统,主要面向物联网网关、NVR 存储、工控平板、工业检测、工控盒、卡拉 OK、云终端、车载中控等行业。
【公众号】迅为电子
【粉丝群】824412014(加群获取驱动文档+例程)
【视频观看】嵌入式学习之Linux驱动(第四篇-高级字符设备进阶_全新升级)_基于RK3568
【购买链接】迅为RK3568开发板瑞芯微Linux安卓鸿蒙ARM核心板人工智能AI主板
第28章 IO多路复用实验
在上俩个章节中,我们对阻塞IO和非阻塞IO进行了学习,本章节将学习第三种IO模型-多路复用IO。
28.1 IO多路复用简介
IO多路复用是一种同步的IO模型。IO多路复用可以实现一个进程监视多个文件描述符。一旦某个文件描述符准备就绪,就通知应用程序进行相应的读写操作。没有文件描述符就绪时就会阻塞应用程序,从而释放出CPU资源。
在第25章中,我们以钓鱼为例,对IO多路复用有了一个简单的认识。下面对钓鱼例子进行回顾:小李同时放置了十个鱼竿,并把十个鱼竿连在了一个铃铛上。这样小李就不必在岸边等待。当铃铛响了就表示有鱼上钩,再回来挨个检查到底是哪个鱼竿有鱼上钩即可。接着进一步体会IO多路复用。
在应用层Linux提供了三种实现IO多路复用的模型,分别是select、poll 和 epoll。在本驱动手册中主要偏重于对驱动的讲解,所以应用层中select、poll 和 epoll函数的使用在这里不做重点讲解。
首先来学习下select、poll 和 epoll函数有什么区别呢?poll函数和seslect函数都可以监听多个文件描述符,通过轮询来获取已经准备好的文件描述符。但是epoll函数将主动轮询变成了被动通知,当事件发生时被动接收通知。为了方便理解,举个形象的例子。假如poll和select是公司的前台,某天一位客户来公司找硬件工程师-小李,请求前台帮忙找人。于是poll和select前台带着这位客户挨个屋子寻找小李,直到找到小李为止。假如epoll是公司的前台,他提前统计了公司每个员工的工位。当客户来找小李的时候,不必像poll select一样,可以直接带着客户到硬件部门去找小李。从上面的俩个例子,明显对比epoll的效率更高。假如公司园区很大,那么poll select需要花费很长时间寻找小李,而epoll已经提前知道小李坐在哪个工位了,直接带客户去找小李即可。
select,poll,epoll有什么区别呢?在单个线程中,select函数最大可以监视1024个文件描述符,而poll函数和select函数并没有什么区别,只是poll函数没有最大文件描述符的限制。在本章节的实验中,以poll为例进行实验。在Linux应用程序中poll函数如下所示:
函数原型:
int poll(struct pollfd *fds,nfds_t nfds,int timeout);
函数功能:
监视并等待多个文件描述符的属性变化
函数参数:
第一个参数fds: 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体pollfd类型,pollfd结构体如下所示:
struct pollfd {
int fd; //被监视的文件描述符
short events; //等待的事件
short revents; //实际发生的事件
};
在pollfd结构体中,第一个成员fd是被监视的文件描述符。第二个成员events是要监视的事件,可监视的事件类型如下所示:
- POLLIN 有数据可以读取
- POLLPRI 有紧急的数据需要读取
- POLLOUT 可以写数据
- POLLERR 指定的文件描述符发生错误
- POLLHUP 指定的文件描述符挂起
- POLLNVAL 无效的请求
- POLLRDNORM 等同于POLLIN
第三个成员是返回事件,由Linux内核设置具体的返回事件。
第二个参数nfds: poll函数要监视的文件描述符数量
第三个参数timeout:指定等待的时间,单位是ms。无论I/O是否准备好,时间到POLL就会返回。如果timepoll大于0 等待指定的时间,如果timeout等于0,立即返回。如果timeout等于-1,事件发生以后才返回。
函数返回值:
失败返回-1,成功返回revents不为0的文件描述符个数。
当应用程序使用select或者poll函数对驱动程序进行非阻塞访问时,驱动程序中file_operations操作集的poll函数会执行。所以需要完善驱动中的poll函数。驱动中的poll函数原型如下所示:
unsigned int (*poll)(struct file *filp,struct poll_table_struct
*wait);
函数参数:
filp:要打开的文件描述符
wait: 结构体poll_table_struct类型指针,此参数是由应用程序中传递的。一般此参数要传递给poll_wait函数。
返回值:
向应用程序返回资源状态,可以返回的资源状态如下:
- POLLIN 有数据可以读取
- POLLPRI 有紧急的数据需要读取
- POLLOUT 可以写数据
- POLLERR 指定的文件描述符发生错误
- POLLHUP 指定的文件描述符挂起
- POLLNVAL 无效的请求
- POLLRDNORM 等同于POLLIN,普通数据可读。
函数功能:
这个函数要进行下面两项工作。首先,对可能引起设备文件状态变化的等待队列调用poll_wait(),将对应的等待队列头添加到poll_table.然后返回表示是否能对设备进行无阻塞读写访问的掩码。
驱动程序的poll函数中调用poll_wait函数,注意!poll_wait函数是不会引起阻塞的。poll_wait函数原型如下所示:
void poll_wait(struct file *filp,wait_queue_head_t *queue,poll_table *wait);
参数queue是要添加到poll_table中的等待队列头,参数wait是poll_table,也就是file_operations中poll函数的wait参数。
实验程序编写
28.2.1 编写测试 APP
本实验对应的应用程序网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\22\app。
在应用层Linux提供了三种API函数,分别是select poll和epoll。本次实验使用poll函数进行实验,如果对select 和epoll函数感兴趣,可以查找一些系统编程课程学习。
编写好的应用程序read.c如下所示:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>
int main(int argc, char *argv[])
{
int fd; //要监视的文件描述符
char buf1[32] = {0};
char buf2[32] = {0};
struct pollfd fds[1];
int ret;
fd = open("/dev/test", O_RDWR); //打开/dev/test设备,阻塞式访问
if (fd < 0)
{
perror("open error \n");
return fd;
}
//构造结构体
fds[0] .fd =fd;
fds[0].events = POLLIN; //监视数据是否可以读取
printf("read before \n");
while (1)
{
ret = poll(fds,1,3000); //轮询文件是否可操作,超时3000ms
if(!ret){ //超时
printf("time out !!\n,");
}else if(fds[0].revents == POLLIN) //如果返回事件是有数据可读取
{
read(fd,buf1,sizeof(buf1)); //从/dev/test文件读取数据
printf("buf is %s \n,",buf1); //打印读取的数据
sleep(1);
}
}
printf("read after\n");
close(fd); //关闭文件
return 0;
}
上述代码第16行,在打开设备节点时不使用非阻塞方式,要使用阻塞方式,所以改为O_RDWR。
在上述代码的第28行,使用poll函数监视并等待多个文件描述符的属性变化。poll函数第一个参数是被监视的文件描述符,是pollfd结构体类型的数组,所以在14行定义了pollfd结构体类型的数组fds。poll函数第2个参数是要监视的文件描述符数量,这里监视的文件描述符为1个。poll函数第3个参数是指定等待的时间 3000ms。
然后编写应用程序write.c,实现向设备文件写入数据的功能,编写好的write.c如下所示:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd;
char buf1[32] = {0};
char buf2[32] = "nihao";
fd = open("/dev/test",O_RDWR); //打开/dev/test设备
if (fd < 0)
{
perror("open error \n");
return fd;
}
printf("write before \n");
write(fd,buf2,sizeof(buf2)); //向/dev/test文件写入数据
printf("write after\n");
close(fd); //关闭文件
return 0;
}
28.2.2 驱动程序编写
本实验对应的驱动程序网盘路径为:iTOP-RK3568开发板【底板V1.7版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux驱动配套资料\04_Linux驱动例程\22\module。
IO多路复用实验需要应用程序和驱动程序进行配合,接下来编写驱动程序。编写好的驱动程序如下所示:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/wait.h>
#include <linux/poll.h>
struct device_test{
dev_t dev_num; //设备号
int major ; //主设备号
int minor ; //次设备号
struct cdev cdev_test; // cdev
struct class *class; //类
struct device *device; //设备
char kbuf[32];
int flag; //标志位
};
struct device_test dev1;
DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头
/*打开设备函数*/
static int cdev_test_open(struct inode *inode, struct file *file)
{
file->private_data=&dev1;//设置私有数据
printk("This is cdev_test_open\r\n");
return 0;
}
/*向设备写入数据函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev=(struct device_test *)file->private_data;
if (copy_from_user(test_dev->kbuf, buf, size) != 0) // copy_from_user:用户空间向内核空间传数据
{
printk("copy_from_user error\r\n");
return -1;
}
test_dev->flag=1;
wake_up_interruptible(&read_wq);
return 0;
}
/**从设备读取数据*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev=(struct device_test *)file->private_data;
if(file->f_flags & O_NONBLOCK ){
if (test_dev->flag !=1)
return -EAGAIN;
}
wait_event_interruptible(read_wq,test_dev->flag);
if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0) // copy_to_user:内核空间向用户空间传数据
{
printk("copy_to_user error\r\n");
return -1;
}
printk("This is cdev_test_read\r\n");
return 0;
}
static int cdev_test_release(struct inode *inode, struct file *file)
{
printk("This is cdev_test_release\r\n");
return 0;
}
static __poll_t cdev_test_poll(struct file *file, struct poll_table_struct *p){
struct device_test *test_dev=(struct device_test *)file->private_data; //设置私有数据
__poll_t mask=0;
poll_wait(file,&read_wq,p); //应用阻塞
if (test_dev->flag == 1)
{
mask |= POLLIN;
}
return mask;
}
/*设备操作函数*/
struct file_operations cdev_test_fops = {
.owner = THIS_MODULE, //将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = cdev_test_open, //将open字段指向chrdev_open(...)函数
.read = cdev_test_read, //将open字段指向chrdev_read(...)函数
.write = cdev_test_write, //将open字段指向chrdev_write(...)函数
.release = cdev_test_release, //将open字段指向chrdev_release(...)函数
.poll = cdev_test_poll, //将poll字段指向chrdev_poll(...)函数
};
static int __init chr_fops_init(void) //驱动入口函数
{
/*注册字符设备驱动*/
int ret;
/*1 创建设备号*/
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "alloc_name"); //动态分配设备号
if (ret < 0)
{
goto err_chrdev;
}
printk("alloc_chrdev_region is ok\n");
dev1.major = MAJOR(dev1.dev_num); //获取主设备号
dev1.minor = MINOR(dev1.dev_num); //获取次设备号
printk("major is %d \r\n", dev1.major); //打印主设备号
printk("minor is %d \r\n", dev1.minor); //打印次设备号
/*2 初始化cdev*/
dev1.cdev_test.owner = THIS_MODULE;
cdev_init(&dev1.cdev_test, &cdev_test_fops);
/*3 添加一个cdev,完成字符设备注册到内核*/
ret = cdev_add(&dev1.cdev_test, dev1.dev_num, 1);
if(ret<0)
{
goto err_chr_add;
}
/*4 创建类*/
dev1. class = class_create(THIS_MODULE, "test");
if(IS_ERR(dev1.class))
{
ret=PTR_ERR(dev1.class);
goto err_class_create;
}
/*5 创建设备*/
dev1.device = device_create(dev1.class, NULL, dev1.dev_num, NULL, "test");
if(IS_ERR(dev1.device))
{
ret=PTR_ERR(dev1.device);
goto err_device_create;
}
return 0;
err_device_create:
class_destroy(dev1.class); //删除类
err_class_create:
cdev_del(&dev1.cdev_test); //删除cdev
err_chr_add:
unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
err_chrdev:
return ret;
}
static void __exit chr_fops_exit(void) //驱动出口函数
{
/*注销字符设备*/
unregister_chrdev_region(dev1.dev_num, 1); //注销设备号
cdev_del(&dev1.cdev_test); //删除cdev
device_destroy(dev1.class, dev1.dev_num); //删除设备
class_destroy(dev1.class); //删除类
}
module_init(chr_fops_init);
module_exit(chr_fops_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("topeet");
首先在第9行代码添加<linux/poll.h>头文件。然后在第94行将poll字段指向chrdev_poll(…)函数,最后在73行到84行编写这个函数。
28.3 运行测试
28.3.1 编译驱动程序
在上一小节中的poll.c代码同一目录下创建 Makefile 文件,Makefile 文件内容如下所示:
export ARCH=arm64#设置平台架构
export CROSS_COMPILE=aarch64-linux-gnu-#交叉编译器前缀
obj-m += poll.o #此处要和你的驱动源文件同名
KDIR :=/home/topeet/Linux/linux_sdk/kernel #这里是你的内核目录
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules #make操作
clean:
make -C $(KDIR) M=$(PWD) clean #make clean操作
对于Makefile的内容注释已在上图添加,保存退出之后,来到存放poll.c和Makefile文件目录下,如下图(图 28-1)所示:
然后使用命令“make”进行驱动的编译,编译完成如下图(图 28-2)所示:
编译完生成poll.ko目标文件,如下图(图 28-3)所示:
至此驱动模块就编译成功了,下面进行应用程序read.c和write.c的编译。
28.3.2 编译应用程序
来到存放应用程序read.c和write.c的文件夹下,使用以下命令对read.c和write.c进行交叉编译,编译完成如下图(图 28-4)所示:
aarch64-linux-gnu-gcc -o read read.c -static
aarch64-linux-gnu-gcc -o write write.c -static
生成的read write文件就是之后放在开发板上运行的可执行文件,至此应用程序的编译就完成了。
28.3.3 测试
开发板启动之后,使用以下命令进行驱动模块的加载,如下图(图 28-5)所示:
在加载驱动程序之后,会生成如下图(图 28-6)所示的设备节点,在应用程序中也是操作这个设备节点。
首先运行read可执行程序,如下(图 28-7)所示,在三秒钟以后打印“time out”。
然后运行write可执行程序写入数据,如下(图 28-8)所示:
接着可以看到read读取到了数据,如下(图 28-9)所示: