本节是本书中《高级字符驱动程序操作》章节的第三节内容。本节主要涉及到的是多路复用IO接口 poll
、select
和 epoll
。
本文主要的内容有以下:
-
poll
、select
和epoll
的作用; - 驱动中的
poll
操作; -
epoll
实例
1. poll、select和epoll的作用
在非阻塞IO的应用程序中,经常会使用到 poll
、select
和 epoll
系统调用,这三个系统调用的功能本质上是一样的:都允许进程决定是否可以对一个或多个打开的文件做非阻塞的读取或写入。
这些调用会阻塞进程,直到给定的文件描述符集合中的任何一个可读取或者写入,因此,它们常常用于那些要使用多个输入或者输出流,而又不会被其中某个流阻塞的进程中。
下面说一下这三个系统调用的关系:poll
和 select
由两个Unix团体几乎同时实现,所以同时存在这两个系统调用,而 epoll
是在2.5.45中引入的,扩展了 poll
函数使其能够处理数千个文件描述符。
要实现上面系统调用的功能,需要驱动代码通过 poll
函数提供相应的支持。驱动的 poll
函数在之前的文章《字符设备驱动(上)》 中有说过,是 poll
、epoll
、select
这三个系统调用的后端实现,用于实现IO的多路复用。
2. 驱动中的poll操作
驱动中的poll函数原型如下:
__poll_t (*poll) (struct file *, struct poll_table_struct *);
当用户空间程序在驱动关联的文件描述符上执行 poll
、select
或 epoll
系统调用时,该驱动程序的方法将被调用。
传递给 poll
的第二个参数是 poll_table_struct
,它的声明在 <linux/poll.h>
中,驱动程序源代码必须包含这个头文件。使用过程中我们可以不用了解这个结构体的细节,把它当做一个不透明的对象使用即可。
poll
的返回值描述哪个操作可以立即执行的位掩码,这些掩码包括:
-
POLLIN
:当前设备可以无阻塞读取 -
POLLRDNORM
:与POLLIN
相同 -
POLLOUT
:当前设备可以无阻塞地写入 -
POLLWRNORM
:与POLLOUT
相同 -
POLLERR
:设备发生了错误 -
POLLHUP
:当前读取设备的进程达到文件尾
在 poll
方法中,通过 poll_wait
函数,驱动程序向 poll_table_struct
结构体中添加一个等待队列头,poll_wait函数的原型如下:
void poll_wait(struct file *, wait_queuea_head_t *, poll_table *);
3. epoll驱动实例
我们基于之前scull_sleep设备进行修改,之前的设备是在读取过程中休眠,直到有数据写入且达到数据量要求后才唤醒读取进程继续读取数据。使用epoll后,设备将会在写入的数据达到要求后才会开始读取数据。
全部代码位于:
https://gitee.com/Quehehe/LinuxDeviceDriver
首先说明一下驱动代码,驱动中,最主要的就是实现了poll函数,具体实现如下:
static __poll_t scull_poll (struct file *filp, struct poll_table_struct *poll_table)
{
struct scull_lock_dev *dev = filp->private_data;
unsigned int mask = 0;
poll_wait(filp, &dev->read_wait_queue, poll_table);
if(dev->data_length >= DATA_SIZE_LIMIT) {
mask |= POLLIN | POLLRDNORM;
}
return mask;
}
static struct file_operations fops = {
......
.poll = scull_poll,
};
其实现也很简单,主要就是调用了 poll_wai()
函数,将读取数据的等待队列头加入到了poll_table中。
然后看一下用户空间的代码,代码位于 scull_poll_test.c
文件中,其部分代码如下:
int main(int argc, char **argv)
{
int fd = open("/dev/scull_poll0", O_RDWR);
int epoll_fd = epoll_create(MAXEVENTS);
struct epoll_event scull_epoll_event;
struct epoll_event temp_event[MAXEVENTS];
char buf[100];
int i = 0;
int n = 0;
if(fd<0) {
printf("can't open \n");
return -1;
}
scull_epoll_event.events = EPOLLIN;
scull_epoll_event.data.fd = fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &scull_epoll_event);
printf("start wait data!\n");
n = epoll_wait(epoll_fd, temp_event, MAXEVENTS, -1);
if(-1 == n) {
printf("Failed to wait!\n");
return -1;
}
printf("data ready!\n");
for(i = 0; i < n; i++) {
if((temp_event[i].data.fd == fd) && (temp_event[i].events & EPOLLIN)) {
n = read(fd, buf, 100);
if(n < (sizeof(buf) - 1)) {
buf[n] = 0;
}
if(n < 0) {
printf("read data ERR!\n");
}
printf("read data length = %d, data is:\n%s\n", n, buf);
}
}
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, &scull_epoll_event);
return 0;
}
其中主要的几个函数是:
int epoll_create(int size) //创建一个epoll句柄
创建一个 epoll_ctl
用于注册 epoll
事件,其注册的类型有三种,通过以下三个宏来表示:
-
EPOLL_CTL_ADD
:注册新的fd到epfd中; -
EPOLL_CTL_MOD
:修改已经注册的fd的监听事件; -
EPOLL_CTL_DEL
:从epfd中删除一个fd;
注册完事件之后,调用 epoll_wait()
函数,此时,当前线程会阻塞,直到注册的 epoll
事件中,某个事件有唤醒操作,则此线程会被唤醒,继续执行。
通过对比读取到的epoll事件相关参数,就能够知道是哪个设备唤醒了当前线程,同时可以知道可以对该设备进行什么操作。
scull_poll_test.c
文件需要使用以下命令编译生成可执行文件:
gcc -o scull_poll_test scull_poll_test.c
可能读者对于这个操作还是不太了解,不知道和之前的休眠唤醒有什么区别。让我们考虑这样一种情况,当前有1000个设备或文件需要我们监听,当其有变化值,我们要读取其中的数据。这时,如果采用休眠唤醒的方法来监听,则我们针对每个文件或设备都要创建一个线程,这样就需要创建1000个线程;如果采用epoll的方法,则将所有设备或文件注册为epoll事件,使用一个线程进行监听,当某些设备或文件有更新时,唤醒这个线程进行数据读取操作,这样就只需要一个线程即可。
测试方法和之前的类似,这边就不再进行演示了。