itop-3568开发板驱动学习笔记(8)高级字符设备(二)IO 多路复用
《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记
文章目录
- IO 多路复用简介
- IO 多路复用实验
- poll 机制
- select 机制
- epoll 机制
IO 多路复用简介
IO 多路复用可以实现一个进程监控多个文件描述符。一旦某个文件描述符准备就绪,就通知应用程序进行相应的读写操作。没有文件操作符就绪时就会阻塞应用程序,从而释放出 CPU 资源。
——原文
Linux 提供了三种实现 IO 多路复用的模型,分别是 select、poll 和 epoll,它们功能本质上相同:监听多个文件描述符,当指定的事件(如读、写事件)发生时,就通知线程事件到达。
IO 多路复用实验
poll 机制
poll()接受一个指向结构’struct pollfd’列表的指针,其中包括了你想测试的文件描述符和事件。事件由一个在结构中事件域的比特掩码确定。当前的结构在调用后将被填写并在事件发生后返回。在SVR4(可能更早的一些版本)中的 "poll.h"文件中包含了用于确定事件的一些宏定义。事件的等待时间精确到毫秒 (但令人困惑的是等待时间的类型却是int),当等待时间为0时,poll()函数立即返回,-1则使poll()一直挂起直到一个指定事件发生。
——百度百科
(应用层)poll 函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds 为要监听的文件描述符集合,是一个数组,该结构体定义见下文;nfds 为要监听的文件描述符数量;timeout 为超时时间,单位为 ms,timeout 大于 0,poll 等待时间直到指定的时间,timeout 为 0,poll 立即返回,timeout 为 -1,一直等待,直到时间发生。
poll 返回值小于等于 0 表示没有任何事件发生,大于 0 时为事件发生个数。
poll 结构体定义:
struct pollfd {
int fd;
short events; //事件
short revents; //返回事件
};
poll 支持的事件类型:
事件名 | 说明 |
---|---|
POLLIN | 普通数据可读 |
POLLRDNORM | 普通数据可读 |
POLLPRI | 高优先级数据可读 |
POLLOUT | 普通数据可写 |
POLLWRNORM | 普通数据可写 |
POLLERR | 发生错误 |
POLLHUP | 发生挂起 |
POLLNVAL | 描述符不是一个打开的文件 |
驱动层 poll 函数定义:
unsigned int (*poll)(struct file *filp, struct poll_table_struct *wait);
wait 对应用户层 poll 函数的 timeout 参数,在驱动中调用的休眠函数为 poll_wait(),wait 便是 poll_wait() 的一个参数。
该函数的返回值就是应用层 struct pollfd 的 revents 成员。
poll_wait() 定义如下,主要用于 poll 驱动函数中的休眠操作,
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
该函数的第二个参数 wait_queue_head_t 为上篇笔记提到的等待队列头,即 poll_wait 依靠等待队列来实现阻塞线程的操作。
实验程序
驱动核心代码
在上一份驱动代码的基础上添加 chrdev_poll() 函数,当驱动运行该函数时,调用 poll_wait() 函数,让线程进入阻塞,同时设定超时时间(由用户层程序决定超时时间),当等待队列 my_wait_queue 被唤醒或到达超时时间, poll 继续往下运行,如果驱动数据已经准备好(tmp_dev->condition 变为 1),则返回 POLLIN,表示数据可读。
// poll
static unsigned int chrdev_poll(struct file *file, struct poll_table_struct *wait)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
poll_wait(file, &my_wait_queue, wait); // 阻塞
if(tmp_dev->condition == 1)
{
return POLLIN; // 返回事件类型
}
return 0;
}
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = chrdev_open, //将 open 成员指向 chrdev_open()函数
.read = chrdev_read, //将 read 成员指向 chrdev_read()函数
.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
.poll = chrdev_poll, //将 poll 字段指向 chrdev_poll()函数
};
完整驱动代码
#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/errno.h>
#include <linux/io.h>
#include <linux/wait.h>
#include <linux/poll.h>
// 定义并初始化等待队列头
DECLARE_WAIT_QUEUE_HEAD(my_wait_queue);
// 定义一个私有数据结构体
struct my_device
{
dev_t dev_num; // 设备号
int major; // 主设备号
int minor; // 次设备号
struct cdev st_cdev;
struct class *st_class;
struct device *st_device;
char kbuf[32];
int condition; // 条件标志
};
// 定义一个全局私有数据结构体
struct my_device dev1;
// open()
static int chrdev_open(struct inode *inode , struct file *file )
{
file->private_data = &dev1; // 设置私有数据
printk("chrdev_open.\n");
return 0;
}
// close()
static int chrdev_release(struct inode *inode, struct file *file)
{
printk("chrdev_release.\n");
return 0;
}
// read()
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = 0;
// 如果 open() 的 flags 参数带 O_NONBLOCK
if(file->f_flags& O_NONBLOCK)
{
// 如果数据没就绪,直接退出 read()
if(tmp_dev->condition == 0)
return -EAGAIN;
}
// 可中断的阻塞等待,进程进入休眠状态
wait_event_interruptible(my_wait_queue, tmp_dev->condition);
tmp_dev->condition = 0; // 条件标志复位
// 向应用空间拷贝数据
ret = copy_to_user(buf, tmp_dev->kbuf, strlen(tmp_dev->kbuf));
if(ret != 0)
{
printk("copy_to_user error.\r\n");
return -1;
}
printk("chrdev_read.\n");
return 0;
}
// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据
if(ret != 0)
{
printk("copy_from_user error.\r\n");
return -1;
}
tmp_dev->condition = 1; // 将条件置 1
wake_up_interruptible(&my_wait_queue); // 唤醒等待队列中的休眠进程
return 0;
}
// poll
static unsigned int chrdev_poll(struct file *file, struct poll_table_struct *wait)
{
struct my_device *tmp_dev = (struct my_device*)file->private_data;
poll_wait(file, &my_wait_queue, wait); // 阻塞
if(tmp_dev->condition == 1)
{
return POLLIN; // 返回事件类型
}
return 0;
}
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = chrdev_open, //将 open 成员指向 chrdev_open()函数
.read = chrdev_read, //将 read 成员指向 chrdev_read()函数
.write = chrdev_write,//将 write 字段指向 chrdev_write()函数
.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
.poll = chrdev_poll, //将 poll 字段指向 chrdev_poll()函数
};
// 驱动入口函数
static int __init chrdev_init(void)
{
int ret;
// 自动获取设备号(只申请一个,次设备号从 0 开始)
ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");
if(ret < 0)
{
goto err_alloc;
}
printk("alloc chrdev region successfully.\n");
dev1.major = MAJOR(dev1.dev_num); // 获取主设备号
dev1.minor = MINOR(dev1.dev_num); // 获取次设备号
printk("major is %d.\nminor is %d\n", dev1.major, dev1.minor);
dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块
cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备
ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统
if(ret < 0)
{
goto err_cdev_add;
}
printk("cdev add successfully.\n");
dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类
if(IS_ERR(dev1.st_class))
{
ret = PTR_ERR(dev1.st_class); // 返回错误码
goto err_class_create;
}
dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备
if(IS_ERR(dev1.st_device))
{
ret = PTR_ERR(dev1.st_device); // 返回错误码
goto err_device_create;
}
return 0;
err_device_create:
class_destroy(dev1.st_class); // 删除类
err_class_create:
cdev_del(&dev1.st_cdev); // 删除 cdev
err_cdev_add:
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
err_alloc:
return ret; // 返回错误号
}
// 驱动出口函数
static void __exit chrdev_exit(void)
{
device_destroy(dev1.st_class, dev1.dev_num); // 删除设备
class_destroy(dev1.st_class); //删除设备类
cdev_del(&dev1.st_cdev); // 删除字符设备
unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号
printk("chrdev_exit.\n");
}
module_init(chrdev_init); //注册入口函数
module_exit(chrdev_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
“读”程序代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#define DEV_FILE "/dev/chrdev_device"
int main(int argc, char** argv)
{
int fd, tmp;
int ret = 0;
char buf[32] = {0};
struct pollfd fds[1];
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
//初始化 fbs[0]
fds[0].fd = fd;
fds[0].events = POLLIN; // 事件类型为可读事件
// 读数据
printf("app will read data.\n");
while(1)
{
ret = poll(fds, 1, 5000); // 监听文件是否有事件发生
if(ret <= 0)
{
printf("poll wait timeout.\n");
}
else if(fds[0].revents == POLLIN) // 如果返回事件为数据可读取
{
ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
if(ret == 0)
printf("app read data successfully\ndata: %s\n", buf);
else
printf("app read data failed.\n");
sleep(3);
}
}
// 关闭设备文件
close(fd);
return 0;
}
“写”程序代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define DEV_FILE "/dev/chrdev_device"
int main(int argc, char** argv)
{
int fd, tmp;
int ret = 0;
char buf[32] = "poll test";
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
// 读数据
printf("app will write data.\n");
ret = write(fd, buf, sizeof(buf)); // 向设备文件写数据
if(ret == 0)
printf("app write data successfully\n");
else
printf("app write data failed.\n");
// 关闭设备文件
close(fd);
return 0;
}
Makefile 文件
由于我打算在 X86 平台测试,所我屏蔽了交叉编译和平台的平台的环境变量,内核目录改为本机内核地址,
#目标文件,与驱动源文件同名,编译成模块
obj-m := chrdev_test.o
#架构平台选择
#export ARCH=arm64
#编译器选择
#export CROSS_COMPILE=aarch64-linux-gnu-
#内核目录
#KDIR := /home/topeet/Linux/rk356x_linux/kernel/
KDIR := /lib/modules/$(shell uname -r)/build
#编译模块
all:
make -C $(KDIR) M=$(shell pwd) modules
$(CROSS_COMPILE)gcc read.c -o read
$(CROSS_COMPILE)gcc write.c -o write
#清除编译文件
clean:
make -C $(KDIR) M=$(shell pwd) clean
rm read write
实验结果
先在后台运行 read,read 先通过 poll 监听设备文件是否有数据可读,每次监听最长等待 5 秒,超过 5 秒没有等到 POLLIN 事件,打印超时提示,再次循环监听,直到 POLLIN 事件发生(调用 write() 向驱动写数据后,数据就绪,poll 返回 POLLIN)。
select 机制
select 与 poll 机制类似,都是轮询监听,但 select 最大的文件描述符个数是有限制的,默认为 1024,而 poll 没有这个限制。
select 函数接口
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数:
- nfds: 被监控的三类文件描述符集合中最大的文件描述符 + 1
- readfds: 读事件文件描述符集合
- writefds: 写事件文件描述符集合
- exceptfds: 异常文件描述符集合
- timeout: 超时时间,为 NULL 时, select() 进入阻塞状态,一直等待,直到有事件发生;超时时间为 0 时,不阻塞,立刻返回;超时时间大于 0,select() 便会在超时时间内监测事件是否发生,超时后则退出 select()。
返回值:
返回值等于 0 表示没有任何事件发生,大于 0 时为事件发生个数,小于 0 表示出错。
struct timeval 结构体
struct timeval{
long tv_sec; //秒
long tv_usec; //毫秒
};
文件描述符集合的操作:
void FD_CLR(int fd, fd_set *set); // 将 set 中与 fd 对应的标志位清除
int FD_ISSET(int fd, fd_set *set); // 判断 set 里与 fd 对应的标志是否为 1
void FD_SET(int fd, fd_set *set); // 把 set 里的与 fd 对应的标志位置为 1
void FD_ZERO(fd_set *set); // 把 set 里所有文件描述符标志位置 0
【注意】:调用完 select() ,需要重新给 timeout 赋值(向下计数,计数值到 0 表示超时),同时也要调用 FD_ZERO() 将 3 种文件描述符数组清空。
实验程序
驱动文件、“写”程序、Makefile 都和 poll 实验相同,只有 “读“程序(监听程序)不同,
“读”程序代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#define DEV_FILE "/dev/chrdev_device"
#define FDS_NUM 5
int main(int argc, char** argv)
{
int fd, tmp, i;
int ret = 0;
char buf[32] = {0};
int fds[FDS_NUM] = {0};
int max_fd = 0;
fd_set readfds;
fd_set writefds;
fd_set errorfds;
struct timeval timeout;
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
// 初始化 fbs[0]
fds[0] = fd;
// 获取最大 fd
for(i = 0; i < FDS_NUM; i++)
{
max_fd = fds[i] > max_fd ? fds[i] : max_fd;
}
// 读数据
printf("app will read data.\n");
while(1)
{
// 将 fds[0] 添加到文件描述符集合
FD_SET(fds[0], &readfds);
FD_SET(fds[0], &writefds);
FD_SET(fds[0], &errorfds);
// 赋值超时时间
timeout.tv_sec = 3;
timeout.tv_usec = 1000;
// 监听文件是否有事件发生
ret = select(max_fd + 1, &readfds, &writefds, &errorfds, &timeout);
if(ret <= 0)
{
printf("select wait timeout.\n");
}
else if(FD_ISSET(fds[0], &readfds)) // 如果返回事件为可读
{
ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
if(ret == 0)
printf("app read data successfully\ndata: %s\n", buf);
else
printf("app read data failed.\n");
sleep(3);
}
else if(FD_ISSET(fds[0], &errorfds)) // 如果返回事件为异常事件
{
printf("excepted event.\n");
}
else if(FD_ISSET(fds[0], &writefds)) // 如果返回事件为可写
{
printf("writable event.\n");
}
// 清空集合里所有文件描述符
FD_ZERO(&readfds);
FD_ZERO(&writefds);
FD_ZERO(&errorfds);
}
// 关闭设备文件
close(fd);
return 0;
}
实验结果
seletc 实验结果与 poll 实验结果相同:
epoll 机制
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
——百度百科
epoll 相关变量
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常见事件类型
事件名 | 描述 |
---|---|
EPOLLIN | 文件描述符可读 |
EPOLLOUT | 文件描述符可写 |
EPOLLPRI | 文件描述符有紧急数据可读 |
EPOLLERR | 文件描述符产生错误 |
EPOLLHUP | 文件描述符被挂断 |
EPOLLET | 文件描述符有事务产生 |
epoll相关的系统调用有:epoll_create, epoll_ctl 和epoll_wait,头文件为 sys/epoll.h
epoll_create()
原型:
int epoll_create(int size);
功能:epoll_create() 用于生成一个 epoll 文件描述符,size 为 epoll 文件描述符可支持的文件描述符个数。
epoll_wait()
原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
功能:监听 epoll 文件描述符
参数:
- epfd:epoll_create 生成的 epoll 文件描述符
- epoll_event:用来回传成功检测的事件数组
- maxevents:每次监听的最大事件个数
- timeout:timeout 为 -1,表示一直阻塞监听;timeout 为 0,表示非阻塞监听;timeout 为正数时,代表监听超时时间(单位为毫秒)
返回值:成功时返回有事件发生的文件描述符个数,超时返回 0,发生错误时返回 -1 并设置 errno。
epoll_ctl()
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制 epoll 文件描述符上的事件,包括注册事件、修改事件和删除事件。
参数:
- epfd:epoll_create 生成的 epoll 文件描述符
- op:要进行的操作,可取值见下文
- fd:要操作的文件描述符
- event:epoll_event 指针
op | 值 | 说明 |
---|---|---|
EPOLL_CTL_ADD | 1 | 注册操作 |
EPOLL_CTL_DEL | 2 | 删除操作 |
EPOLL_CTL_MOD | 3 | 修改操作 |
返回值:成功返回 0,发生错误返回 -1 并设置 errno。
实验程序
驱动文件、“写”程序、Makefile 都和 poll 实验相同,只有 “读“程序(监听程序)不同,
“读”程序代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>
#define DEV_FILE "/dev/chrdev_device"
#define FDS_NUM 5
#define MAX_EVENTS 10
int main(int argc, char** argv)
{
int fd, tmp, i;
int ret = 0;
char buf[32] = {0};
int epollfd;
struct epoll_event event;
struct epoll_event events[MAX_EVENTS];
// 打开设备文件
fd = open(DEV_FILE, O_RDWR);
if(fd < 0)
{
printf("%s open failed.\n", DEV_FILE);
return 0;
}
printf("%s open successfully.\n", DEV_FILE);
// 创建一个 epollfd
epollfd = epoll_create(FDS_NUM);
if(epollfd == -1)
{
printf("epoll_create error.\n");
return 0;
}
// event 结构体初始化
event.events = EPOLLIN; // 读事件
event.data.fd = fd; // 要监听的文件描述符
// 设置 epollfd
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event) == -1)
{
printf("epoll_ctl error.\n");
return 0;
}
// 读数据
printf("app will read data.\n");
while(1)
{
// 等待 events
ret = epoll_wait(epollfd, events, MAX_EVENTS, 3000);
if(ret < 0)
{
printf("epoll_wait error.\n");
return 0;
}
else if(ret == 0)
{
printf("epoll wait timeout.\n");
}
else
{
// 根据监测到的事件作相应处理
for(i = 0; i < ret; i++)
{
if(events[i].data.fd == fd)
{
ret = read(fd, buf, sizeof(buf)); // 从设备文件读数据
if(ret == 0)
printf("app read data successfully\ndata: %s\n", buf);
else
printf("app read data failed.\n");
sleep(3);
}
}
}
}
// 关闭设备文件
close(fd);
return 0;
}
实验结果
epoll 实验结果与 poll 实验 和 select 实验结果相同: