linux I/O多路复用:select、poll、信号驱动I/O、epoll等技术使用、以及性能分析详解
1.1 整体性能分析
-
使用I/O多路复用、信号驱动I/O以及epoll的目标:同时检查多个文件描述符,看他们是否准备好了执行I/O操作(准确来说是看I/O操作是否可以以非阻塞的形式执行)
-
文件描述符就绪状态转换的一些可能的触发原因:
- 输入数据到达
- 套接字连接建立完成
- 满载的套接字发送缓冲区有了剩余空间
-
I/O模型的一些方案:
- I/O多路复用允许进程检查同时检查多个文件描述符的状态,比如select()和poll()系统调用
- 信号驱动I/O是指当输入或者输出数据可以写到指定的文件描述符上的时候,内核向请求数据的进程发送一个信号,在此之前进程可以处理其它任务,当I/O操作可以执行时通过接受信号来获得通知
- epoll API是linux专有的特性,拥有和select()和信号驱动I/O的优点
-
三种方案的比较:
- select()和poll()在UNIX中存在时间很久,可移植性很强,但是对大量的文件描述符的检查性能不是很好
- epoll API的关键优势在于它可以高效的检查大量的文件描述符,缺点是是在于它是专属于linux系统的API,其次它相比于信号驱动I/O还有一些优点:
- 避免信号处理的复杂性
- 可以指定想要检查的事件类型
- 可以以水平触发或者边缘触发的形式来通知进程
-
Libevent库就是一个软件层,它提供了检查文件描述符I/O事件的抽象,可以移植到多个UNIX系统中,底层采用select()、poll()、或者信号驱动I/O或者epoll()都可以
1.1.1 水平触发或者边缘触发
-
模型:
-
水平触发:当我们输入1024bytes数据的时候,此时采用select,这时候select了解到有数据的到来,返回文件描述符就绪的状态,然后我们执行I/O的读取操作,假如我们只读取了500bytes字节数据,然后又回到select函数,因为select是水平触发,因为还剩下有数据,所以select又会返回文件描述符的就绪信息,然后又可以开始执行I/O操作了
-
边缘触发:当我们输入1024bytes数据的时候,此时采用epoll的边缘触发模式,epoll检测到有数据到来,然后不在阻塞,马上返回,然后我们可以执行I/O的读取操作,假如还是只读取了500bytes数据,程序又返回到epoll的时候,由于是边缘触发,就算现在还有剩余数据,但是也必须等到下一次的输入数据(I/O请求)到来,epoll才会被触发,所以在新的I/O请求到来之前,epoll会阻塞,所以就是为什么在边缘触发模式下,执行一次I/O读取操纵的时候,尽量采取非阻塞并且循环读取尽可能多的数据,直到相应的系统调用产生EAGAIN或者EWOULDBLOCK错误码的时候退出,返回到epoll
-
EAGAIN或者EWOULDBLOCK错误码:通过I/O系统调用在非阻塞的文件描述符上循环读取数据的时候,如果文件描述符指向的缓冲区没有数据的话,就返回EAGAIN或者EWOULDBLOCK错误码
-
从电子角度:
- 水平触发:低电平或者高电平触发事件
- 边缘触发:低电平到高电平或者高电平到低电平的跳变产生一次触发事件
1.2 I/O多路复用
1.2.1 select()系统调用
/* According to POSIX.1-2001 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
/*****************
函数功能:系统提供select函数来实现多路复用输入/输出模型。select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会根据timeout参数的设置,阻塞或者不阻塞在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
返回值:执行成功则返回文件描述词状态已改变的个数,如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,exceptfds和timeout的值变成不可预测
*/
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
/*参数说明:
nfds:需要监视的最大文件描述符加1;
readfds:指向需要检测的可读文件描述符集合
writefds:指向需要检测的可写的文件描述符的集合
exceptfds:指向需要检测的异常的文件描述符的集合
timeout:提供了一个时间长度,如果在这个时间内没有任何就绪的文件描述符,就返回0
*/
//timeout结构体定义如下:
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
/*
如果timeout设置为:
NULL:select()会一直阻塞,直到某一个文件描述符就绪或者被一个信号中断
0:检测一次文件描述符集合的状态,就立刻返回,不等待
特定的时间值:在指定的时间段里面没有事件发生,函数就会返回,在指定的时间内也可以被中断
*/
//下面的宏提供了处理这三种类型文件描述符集合的方式
void FD_CLR(int fd, fd_set *set); //清楚set指向的文件描述符集合中的fd的位
int FD_ISSET(int fd, fd_set *set); //测试set指向的文件描述符集合中的fd的位是否就绪
void FD_SET(int fd, fd_set *set); //设置set指向的文件描述符集合中的fd的位
void FD_ZERO(fd_set *set); //清楚set指向的文件描述符集合的所有位为0
-
fd_set(文件描述符集合):一个文件描述集保存在 fd_set 类型中。fd_set类型变量每一位代表了一个描述符。我们也可以认为它只是一个由很多二进制位构成的数组。如下图所示:
-
select模型介绍:
-
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd
-
执行fd_set set;FD_ZERO(&set),set指向的文件描述符集合就是0000 0000
-
若fd=5,执行FD_SET(fd,&set); 后set变为0000,0100(第5位置为1)
-
若再加入fd=2,fd=1,则set变为0110 0100
-
执行select(5+1,&set,NULL,NULL,NULL)阻塞等待
-
若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0110,0000。注意:没有事件发生的fd=5被清空
-
-
-
select模型的特点:
- 可监控的最大文件描述符个数与sizeof(fd_set)有关,我的系统下其值为128byte,对应可以监视128*8=1024个文件描述符
- 文件描述符集合可以就是存放在一个array数组中,select返回后,FD_ISSET()检测会依靠array来完成,其次array中没有事件发生的文件描述符会被清除,其次重新开始select之前,需要重新初始化array
-
实例:
/*************************************************************************
> File Name: select.c
> Author:
> Mail:
> Created Time: Thu 18 Aug 2016 10:59:50 PM PDT
************************************************************************/
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <strings.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#define PORT 8888
#define backlog 1024
int main(int argc, char *argv[])
{
int ret;
char buff[1024];
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
inet_aton(argv[1], &addr.sin_addr);
//1.创建套接字
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
if (listen_fd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
//2.绑定地址信息
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
perror("bind");
exit(EXIT_FAILURE);
}
//3.监听
if (listen(listen_fd, backlog) == -1)
{
perror("listen");
exit(EXIT_FAILURE);
}
printf("listening ...\n");
fd_set read_fds, tmp_fds;
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
FD_SET(listen_fd, &read_fds);
int maxfd = listen_fd;
while (1)
{
struct timeval tv = {3, 0};
tmp_fds = read_fds;
ret = select(maxfd+1, &tmp_fds, NULL, NULL, &tv);
if (ret == -1)
{
perror("select");
exit(EXIT_FAILURE);
}
else if (ret == 0)
{
printf("timeout...\n");
}
//有文件描述符准备好
else
{
for (int i = 0; i <= maxfd; i++)
{
if (FD_ISSET(i, &tmp_fds))
{
//标准输入准备好
if (i == STDIN_FILENO)
{
fgets(buff, sizeof(buff), stdin);
printf("%s", buff);
}
//监听套接字准备好
else if (i == listen_fd)
{
//4.接受客户端链接请求
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
int connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
if (connect_fd == -1)
{
perror("accept");
exit(EXIT_FAILURE);
}
printf("accept from:%s:%hu\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
FD_SET(connect_fd, &read_fds);
maxfd = maxfd > connect_fd ? maxfd : connect_fd;
}
//以前添加进去的connect_fd准备好
else
{
ret = recv(i, buff, sizeof(buff), 0);
if (ret == -1)
{
perror("recv");
break;
}
else if (ret == 0)//对方关闭套接字
{
printf("client shutdown!\n");
FD_CLR(i, &read_fds);
close(i);
break;
}
else
{
printf("recv:%s", buff);
}
}
}
}
}
}
close(listen_fd);
return 0;
}
1.2.2 poll()系统调用
/*****************
函数功能:系统提供poll函数来实现多路复用输入/输出模型。poll系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会根据timeout参数的设置,阻塞或者不阻塞在poll这里等待,直到被监视的文件描述符有一个或多个发生了状态改变
返回值:执行成功则返回数组fds中拥有非零revents字段的pollfd结构体的数量,如果返回0代表在描述词状态改变前已超过timeout时间,没有返回;当有错误发生时则返回-1,错误原因存于errno
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*参数说明:
fds:指向一个结构体数组
nfds:结构体数组当中结构体的数量
timeout:poll函数阻塞的时间
*/
//fds结构体定义如下:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
/*
fd为指定的文件描述符
events:初始化为需要为fd检查的事件
revents:poll()返回时候把在fd上真正发生的事件存储在revent中
*/
/*timeout设定:
-1:poll()会一直阻塞到fds指向的结构体数组中存在就绪事件
0:不阻塞:只检查一次
非0:阻塞这个值大小的时间
*/
- 如果对文件描述符上面的事件不敢兴趣,可以将events设置为0,并将fd字段设置为0,或者原来值的相反数,然后将导致events被忽略,revents总是返回0
- POLLRDHUP必须定义_GNU_SOURCE测试宏
- POLLRDNORM、POLLRDBAND、POLLWRNORM、POLLWRBAND必须定义_XOPEN_SOURCE宏
/*************************************************************************
> File Name: poll.c
> Author:
> Mail:
> Created Time: Thu 18 Aug 2016 10:59:50 PM PDT
************************************************************************/
#include<stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <strings.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>
#include <poll.h>
#define PORT 8888
#define backlog 1024
int main(int argc, char *argv[])
{
int ret;
char buff[1024];
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
inet_aton(argv[1], &addr.sin_addr);
//1.创建套接字
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
if (listen_fd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
//2.绑定地址信息
if (bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1)
{
perror("bind");
exit(EXIT_FAILURE);
}
//3.监听
if (listen(listen_fd, backlog) == -1)
{
perror("listen");
exit(EXIT_FAILURE);
}
printf("listening ...\n");
struct pollfd events[1024];
bzero(events, sizeof(events));
events[0].fd = STDIN_FILENO;
events[0].events = POLLIN;
events[1].fd = listen_fd;
events[1].events = POLLIN;
int count = 2;
while (1)
{
ret = poll(events, count, 3*1000);
if (ret == -1)
{
perror("poll");
exit(EXIT_FAILURE);
}
else if (ret == 0)
{
printf("timeout...\n");
}
//有文件描述符准备好
else
{
for (int i = 0; i < count; i++)
{
if (events[i].revents & POLLIN)
{
//标准输入准备好
if (events[i].fd == STDIN_FILENO)
{
fgets(buff, sizeof(buff), stdin);
printf("%s", buff);
}
//监听套接字准备好
else if (events[i].fd == listen_fd)
{
//4.接受客户端链接请求
struct sockaddr_in client_addr;
int len = sizeof(client_addr);
int connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
if (connect_fd == -1)
{
perror("accept");
exit(EXIT_FAILURE);
}
printf("accept from:%s:%hu\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
events[connect_fd-listen_fd+1].fd = connect_fd;
events[connect_fd-listen_fd+1].events = POLLIN;
count = count > (connect_fd-listen_fd+2) ? count : (connect_fd-listen_fd+2);
}
//以前添加进去的connect_fd准备好
else
{
ret = recv(events[i].fd, buff, sizeof(buff), 0);
if (ret == -1)
{
perror("recv");
break;
}
else if (ret == 0)//对方关闭套接字
{
printf("client shutdown!\n");
bzero(&events[i], sizeof(events[0]));
close(events[i].fd);
break;
}
else
{
printf("recv:%s", buff);
}
}
}
}
}
}
close(listen_fd);
return 0;
}
1.2.3 文件描述符何时就绪
-
普通文件:代表普通文件的文件描述符总是被select()标记为可读或者可写,对于poll()来说,在revent中总是标记为POLLIN或者POLLOUT,原因如下:
- read()总是立刻返回数据、文件结尾或者错误
- write()总是立刻传输数据或者出现错误
-
管道或者FIFO:
-
读端:events假设已经指定POLLIN标记:
-
写端:events假设已经指定POLLOUT标记:
-
-
套接字:events字段已经指定了POLLIN|POLLOUT|POLLPRI
1.2.4 比较select()和poll()
- 在linux内核层面,两者都调用了相同的内核poll历程集合
- API之间区别:
- 当一个文件描述符关闭了,poll()的revents字段会返回POLLNVAL,而select()会返回-1将错误码设置为EBADF
- select检查的文件描述符数量有限制,linux下一般是1024,poll本质上没有限制
- select每次调用之前都需要重新初始化文件描述符集合
- 可移植性
- 两者使用都很广泛
- 性能:
- 待检查的文件描述符范围小,即最大文件描述符值较小
- 有大量的文件描述符,但是分布密集,满足这两条,两者性能差距不大
- 当最大文件描述符N非常大,但是0到N之间只有几个文件描述符,select会检查N个文件描述符,而poll只会检查我们在struct pollfd结构体数组中指定了的文件描述符,此时两者性能差距巨大
- 两者都存在的问题:
- 两者在调用时侯都会检查所有被指定的文件描述符,看是否处于就绪状态,当文件描述符密集存在时候,大大消耗CPU时间
- 检查时候,select和poll都会和内核交换某种数据结构,并且poll随着检查的文件描述符数量增加,数据结构大小会增加
- 调用完成之后,程序必须检查返回的数据结构中的所有元素,来查明是哪个文件描述符处于就绪状态,然后该做什么事情
1.3 信号驱动I/O
#include <unistd.h>
#include <fcntl.h>
/*************************
函数功能:系统调用对打开的文件描述符执行一系列控制操作
返回值:For a successful call, the return value depends on the cmd,On error, -1 is returned, and errno is set appropriately
*************************/
int fcntl(int fd, int cmd, ... /* arg */ );
/*参数
fd:打开的文件描述符
cmd:操作命令
...:依靠cmd命令来填写
*/
-
例子
/*************************************************************************\ * Copyright (C) Michael Kerrisk, 2015. * * * * This program is free software. You may use, modify, and redistribute it * * under the terms of the GNU General Public License as published by the * * Free Software Foundation, either version 3 or (at your option) any * * later version. This program is distributed without any warranty. See * * the file COPYING.gpl-v3 for details. * \*************************************************************************/ /* Listing 63-3 */ /* demo_sigio.c A trivial example of the use of signal-driven I/O. */ #include <signal.h> #include <ctype.h> #include <fcntl.h> #include <termios.h> #include "tty_functions.h" /* Declaration of ttySetCbreak() */ #include "tlpi_hdr.h" static volatile sig_atomic_t gotSigio = 0; /* Set nonzero on receipt of SIGIO */ static void sigioHandler(int sig) { gotSigio = 1; } int main(int argc, char *argv[]) { int flags, j, cnt; struct termios origTermios; char ch; struct sigaction sa; Boolean done; /*建立信号例程*/ sigemptyset(&sa.sa_mask); sa.sa_flags = SA_RESTART; sa.sa_handler = sigioHandler; if (sigaction(SIGIO, &sa, NULL) == -1) errExit("sigaction"); /*设定文件描述符属主*/ if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) errExit("fcntl(F_SETOWN)"); /* Enable "I/O possible" signaling and make I/O nonblocking for file descriptor */ flags = fcntl(STDIN_FILENO, F_GETFL); if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1) errExit("fcntl(F_SETFL)"); /* Place terminal in cbreak mode */ if (ttySetCbreak(STDIN_FILENO, &origTermios) == -1) errExit("ttySetCbreak"); for (done = FALSE, cnt = 0; !done ; cnt++) { for (j = 0; j < 100000000; j++) continue; /* Slow main loop down a little */ if (gotSigio) { /* Is input available? */ gotSigio = 0; /* Read all available input until error (probably EAGAIN) or EOF (not actually possible in cbreak mode) or a hash (#) character is read */ while (read(STDIN_FILENO, &ch, 1) > 0 && !done) { printf("cnt=%d; read %c\n", cnt, ch); done = ch == '#'; } } } /* Restore original terminal settings */ if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &origTermios) == -1) errExit("tcsetattr"); exit(EXIT_SUCCESS); }
-
因为SIGIO信号默认是终止进程,所以我们应该在启动信号驱动I/O之前安装好SIGIO信号的处理程序
-
指定某个会发生I/O就绪的文件描述符的属主,属主可以是进程或者该进程所属的进程组,当I/O就绪的时候,信号就会发送到指定的进程或者进程组
/**************************** 函数功能某个文件描述符就绪之后,返回接收到信号的进程或者进程组ID ****************************/ int id = fcntl(STDIN_FILENO, F_GETOWN); if (-1 == id) { perror("fcntl"); return -1; }
1.3.1 何时发送“I/O就绪”信号
- 终端和伪终端:产生新的输入时就会生成一个信号,即使之前的输入还没有被读取也是如此
- 管道和FIFO:
- 对于读取端口:
- 数据写入管道的时候
- 管道的写端关闭
- 对于写端口
- 读端口操作导致管道剩余空间增加,因此可以写入PIPE_BUF个字节
- 管道的读端关闭
- 对于读取端口:
- 套接字:
- 一个输入数据报到达套接字
- 套接字上面发生异步错误
- 监听套接字收到心得连接
- connect请求完成,也就是TCP连接的主动端进入ESTABLISHED状态,对于UNIX域套接字则不会发生
- 套接字上有新的输入
- 对端调用shutdown
- 套接字输出有绪
- 套接字发生异步错误
1.3.2 优化信号驱动I/O
-
目的:可以同时检测大量的文件描述符,相比于select和poll,它可以记住要检查的文件描述符,且在该文件描述符上发生I/O事件的时候才会发送信号给相应的进程或者进程组
-
两个必须执行的步骤:
-
用linux专用的fcntl()F_SETSIG操作为文件描述符指定一个信号,否则默认就发SIGIO信号
/*为文件描述符指定一个信号*/ if (fcntl(fd, F_SETSIG, sig) == -1) { perror("fcntl"); } /*取回当前文件描述符指定的信号*/ sig = fcntl(fd, F_GETSIG) if (-1 == sig) perror("fcntl"); /*使用这两个标志的时候,必需定义_GNU_SOURCE_*/
-
使用sigaction安装信号处理例程的时候,设定SA_SIGINFO标记
-
-
执行以上两个步骤的理由:
- SIGIO是标准的非排队信号,当一个SIGIO信号被阻塞了,后续的SIGIO信号会被丢弃,但是这里我们需要知道每个信号的到来,所以我们需要用F_SETSIG指定一个实时信号,他是排队信号,就不会造成信号丢失的问题
- 其次,sa.sa_flags字段指定了SA_SIGINFO标记,那么结构体siginfo_t就会作为第二个参数传递到信号处理进程中去,该结构体包含的字段可以识别在哪个文件描述符上发生了什么事件
- 注意,只有同时使用F_SETSIG和SA_SIGINFO两个标记,siginfo_t结构体才能合法传递过去
-
信号队列的溢出:
- 可以排队的实时信号数量是有限的,达到这个上限,内核对于I/0就绪的通知将恢复默认的SIGIO信号,此外SIGIO信号处理例程不接受siginfo_t结构体参数
- 解决办法:
- 增加可排队的实时信号数量的限制
- 为SIHIO也建立信号处理例程,获取队列中全部实时信号,然后临时切换到select或者poll
-
多线程中使用信号驱动I/O:通过在F_SETOWN_EX和F_GETOWN_EX标志
-
fcntl第三个擦参数为指向以下的结构体的指针:
struct f_owner_ex { int type; pid_t pid; } /*参数: type:用于表明pid当中的内容是表示进程还是进程组或者是线程的ID信息 */
1.4 epoll编程接口
- epoll编程接口的优点:
- epoll即支持水平触发,也支持边缘触发
- 避免复杂的信号处理流程
- 指定检查套接字文件描述符的读就绪或者写就绪或者两者同时指定
- linux系统专有
- epollAPI核心数据结构被称作为epoll实例,和一个打开的文件描述符关联起来,这个文件描述符用来指向epoll实例这个数据结构的,不是用来进行I/O操作的,epoll内核实例实现的两个主要目的:
- 记录了进程中声明过感兴趣的文件描述符列表
- 维护了处于I/O就绪的文件描述符列表
- 每一个文件描述符,我们都可以指定一个位掩码来表示我们感兴趣的事件
1.4.1 创建epoll实例:epoll_create()
/***********************
函数功能:创建一个epoll实例,并且返回一个指向该epoll实例的文件描述符
返回值:
On success, these system calls return a nonnegative file descriptor.
On error, -1 is returned, and errno is set to indicate the error.
************************/
#include <sys/epoll.h>
int epoll_create(int size);
/*参数
size:指定epoll实例要检测的文件描述符个数的最大值,但是自linux2.6.8以来这个参数被忽略了,一般是当前epoll实例暂时分配的个数,一般小于内核实际分配的总数目
*/
int epoll_create1(int flags);
/*参数
取消了无用的参数size,而是增加了可以更改系统调用功能的flags参数,它只有一个标志:EPOLL_CLOEXEC,使得内核在新的文件描述符上面启动了执行在关闭标志
*/
- 返回文件描述符不在使用的时候,一定要用close关闭,当一个epoll实例相关的文件描述都被关闭的时候,实例被销毁
1.4.2 修改epoll的兴趣列表:epoll_ctl()
/************************
函数功能:This system call performs control operations on the epoll instance
referred to by the file descriptor epfd. It requests that the opera‐
tion op be performed for the target file descriptor, fd.
返回值:When successful, epoll_ctl() returns zero. When an error occurs,
epoll_ctl() returns -1 and errno is set appropriately.
************************/
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
参数:
epfd:创建的epoll实例的文件描述符
fd:修改epoll实例兴趣列表中的哪一个文件描述符
op:和fd有关的选项
EPOLL_CTL_ADD:把fd添加到epfd中的兴趣列表,添加一个存在的fd会造成EEXIST错误
EPOLL_CTL_MOD:修改fd上面设定的事件,具体需要用到event指向的结构体,修改不在兴趣列表中的文件描述符,会发生 ENOENT错误
EPOLL_CTL_DEL:将文件描述符fd从epfd中删除,可以忽略event参数,删除不存在的fd会造成ENOENT错误
event:指向一个结构体
*/
//struct epoll_event结构体如下:
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
/*描述符fd就绪后,联合体成员用来指定传回给调用进程的信息*/
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
/*一个例子*/
int epfd;
struct epoll_event ev;
epfd = epoll_create(5);
if (-1 == epfd)
{
perror("epoll_create");
exit(-1);
}
ev.data.fd = fd;
ev.event = EPOLLIN;
if (epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev))
/*epoll实例上限*/
//max_user_watches:每个用户可以注册到epoll实例上面的文件描述符总数
1.4.3 事件等待:epoll_wait()
/***********************************
函数功能:返回epoll实例处于就绪态的文件描述符信息
返回值:When successful, epoll_wait() returns the number of file descriptors
ready for the requested I/O, or zero if no file descriptor became ready
during the requested timeout milliseconds. When an error occurs,
epoll_wait() returns -1 and errno is set appropriately.
***********************************/
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
/*
参数:
epfd:创建的epoll实例的文件描述符
events:指向的结构体数组返回的是有关就绪态文件描述符的信息此时结构体events字段包含在描述符上面已经发生的事件掩码,data字段返回我们当时用epoll_ctl注册在event.data中指定的值,因为data字段是我们唯一可以获得与这个事件相关文件描述符号的途径,所以我们在调用epoll_ctl注册fd的时候,一定要吧events.data.fd设定文件描述符,或者events.data.ptr指向包含文件描述符号的结构体
maxevents:events指向的结构体数组的个数
timeout:
-1:epoll一直阻塞,直到文件描述符就绪,或者有信号产生
0;执行一次非阻塞检查
>0:等待大于0的这一段时间,直到有fd就绪,或者捕获到一个信号
*/
-
struct epoll_event结构体中的events字段的掩码:
-
EPOLLONESHOT:指定了这一个字段,那么epoll_wait只会通知我们某个fd一次的就绪态,下次在调用epoll_wait时候,就算fd就绪了,epoll_wait也不会通知我们,会被标记为非激活态
1.4.4深入探究epoll的语义
- 文件描述:表示的是一个打开文件的各种信息,可以比喻为一个抽屉,内核监视的是这个抽屉里面的内容,文件描述符相当于抽屉的把手,这是内核给用户分配的一个操作抽屉里面内容的一个方法,内核当中的监视的兴趣列表其实也是抽屉的内容,当抽屉里面的内容就绪时候,内核就会通知用户,返回这部分的信息到events结构体,用户只是通过把手(文件描述符)来操作这块数据而已,一个抽屉可以有多个把手,就算关闭其中一个,当内核中数据就绪的时候,被关闭的文件描述符也会得到通知
1.4.5 epoll同I/O多路复用性能对比
-
性能对比:
-
不管是poll()、select()和epoll()就绪的文件句柄返回后,我们都需要一个一个的判断是哪一个文件句柄发生率什么事件,但是这点时间想比如内核中数据结构的传递,时间消耗很少,我们可以忽略不记
-
select()和poll()除了轮询的缺点之外,还会两次在用户空间和内核空间交换数据结构,而epoll只交换一次,然后内核最后把就绪事件记录到内核中的就绪列表中,通过epoll_wait直接把文件描述符返回给用户,而前者除了创建兴趣列表会交换数据结构到内核,就绪之后,还会把数据结构传递到用户空间
1.4.6 边缘触发通知
- epoll设置边缘触发模式:
-
边缘模式下如何避免文件描述符处于饥饿状态:
- 文件描述符处于饥饿状态定义:有多个就绪的文件描述符从epoll_wait返回,加入输入流很长,当在一个就绪的文件描述上面,通过循环读取的时候,由于输入流很长,就可能造成其它就绪的文件描述符无法得到处理,从而处于饥饿状态,这种状态我们应该避免,解决方法:
- 应用程序维护一张表,把处于就绪状态的文件描述符加入进去,同时从新设置监视时间为0或者很小,epoll_wait监视没有新的就绪文件描述符的时候,就可以快速的处理那些准备好的文件描述符号
- 应用程序维护一张表中,每一个文件描述符的操作,才有轮转调度,即每个文件描述符上执行I/O操作一段是时间,如果出现EAGAIN就移除应用程序维护的那张表
- 文件描述符处于饥饿状态定义:有多个就绪的文件描述符从epoll_wait返回,加入输入流很长,当在一个就绪的文件描述上面,通过循环读取的时候,由于输入流很长,就可能造成其它就绪的文件描述符无法得到处理,从而处于饥饿状态,这种状态我们应该避免,解决方法: