(1) epoll模型是基于事件的通知方式,事先为每个建立连接的文件描述符注册事件,一旦该描述符就绪,内核会采用类似callback的回调机制,将文件描述符加入epoll指定的文件描述符集中。虽然epoll机制中返回的同样是就绪文件描述符的数量,但epoll中的文件描述符集只存储了就绪的文件描述符,服务器进程无需再扫描所有连接的文件描述符。
(2) epoll 没有对fd描述符有限制,理论上取决于系统内存大小,可以通过cat/proc/sys/fs/file-max查看,大概1G内存创建10w个连接
(3) epoll的具体实现使用mmap(内存映射机制)加速内核与用户空间消息传递,不必再将内核中的文件描述符复制到内存空间。
SYNOPSIS: #include
FUNCTION: epoll_create
DESCRIPTION:
epoll_create() returns a file descriptor referring to the new epoll instance
(创建一个epoll实例与文件描述符)。
PARAMETER: 参数size 为该epoll中可监听的文件描述符的最大个数。
RETURN VALUE:
函数调用成功,返回一个非负的文件描述符;否则失败,返回-1并设置错误
On success, these system calls return a nonnegative file descriptor. On error, -1 is returned, and errno is set to indicate the error.
ERRORS:EINVAL size is not positive(失败并报参数size 不是正数的错误)
SYNOPSIS: #include
FUNCTION: epoll_ctl
DESCRIPTION:用于添加,修改,删除要监听的event事件。
Valid values for the op argument are:
EPOLL_CTL_ADD:将新的fd注册到epfd(epoll实例对应得文件符)中
EPOLL_CTL_MOD:修改已注册的fd
EPOLL_CTL_DEL:删除epfd中的fd
PARAMETER:
(1)Op参数:
EPOLL_CTL_ADD:将新的fd注册到epfd(epoll实例对应得文件符)中
EPOLL_CTL_MOD:修改已注册的fd
EPOLL_CTL_DEL:删除epfd中的fd
(2)参数event: 用于设置要监听的事件,event是一个epoll_event类型的指针,其结构体定义如下:
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
struct epoll_event中成员events表示要监控的事件,该成员由一些单一事件组成的位集,宏定义如下:
事件宏
• EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
• EPOLLOUT: 表示对应的文件描述符可以写;
• EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
• EPOLLERR: 表示对应的文件描述符发生错误;
• EPOLLHUP: 表示对应的文件描述符被挂断;
• EPOLLET: 将 EPOLL设为边缘触发(Edge Triggered)模式(默认为水平触发),这是相对于水平触发(Level Triggered)来说的。
• EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
struct epoll_event中成员data的数据类型是共用体epoll_data_t,其定义如下
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
RETURN VALUE:函数调用成功,返回0;否则失败,返回-1并设置错误
ERRORS:
(1)EBADF: epfd or fd is not a valid file descriptor.
EEXIST: op was EPOLL_CTL_ADD, and the supplied file descriptor fd is already registered with this epoll instance.
(2)EINVAL: epfd is not an epoll file descriptor, or fd is the same as epfd, or the requested operation op is not supported by this interface.
(3)ENOENT:op was EPOLL_CTL_MOD or EPOLL_CTL_DEL, and fd is not registered with this epoll instance.
(4)ENOMEM:There was insufficient memory to handle the requested op control operation.
(5)ENOSPC:The limit imposed by /proc/sys/fs/epoll/max_user_watches was encountered while trying to register (EPOLL_CTL_ADD) a new file descriptor on an epoll instance. See epoll for further details.
(6) EPERM:The target file fd does not support epoll. This error can occur if fd refers to, for example, a regular file or a directory.
SYNOPSIS: #include
FUNCTION: epoll_wait
DESCRIPTION:
用于等待epoll句柄中所监控的事件的发生,当有一个或者多个事件发生或等待超时后,epoll_wait返回。
PARAMETER: The memory
(1) events: 指向发生epoll_create()调用时系统事先预备的空间,当有监听事件发生时,内核将该事件复制到此空间中
(2) events:表示events事件的大小,不能超过epoll_create()调用时所传入参数的大小
(3) timeout: 超时时间,用于设置epoll_wait的工作方式。若设置为0,则立刻返回;若设置为-1,则无限等待;否则表示等待一定时长。
RETURN VALUE:
函数调用成功,返回就绪文件描述符的数量;若等待超时后并无就绪文件描述符则返回0;若调用失败则返回-1并设置错误。
ERRORS:
(1) EBADF: epfd is not a valid file descriptor.
(2) EFAULT: The memory area pointed to by events is not accessible with write permissions.
(3) EINTR: The call was interrupted by a signal handler before either any of the requested events occurred or the timeout expired; see signal
(4) EINVAL:epfd is not an epoll file descriptor, or maxevents is less than or equal to zero.
二者的差异在于 level-trigger (LT) 模式下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger (ET) 模式下只有某个 socket 从 unreadable 变为 readable 或从unwritable 变为 writable 时,epoll_wait 才会返回该 socket。
从socket读数据:
从socket写数据:
所以, 在epoll的ET模式下, 正确的读写方式为:
读: 只要可读, 就一直读, 直到返回0, 或者 errno = EAGAIN
写: 只要可写, 就一直写, 直到数据发送完, 或者 errno = EAGAIN
Epoll的工作模式在调用注册函数epoll_ctl()时确定,
由该函数的参数event的成员events指定。默认情况下epoll的工作模式为水平触发,若要将其设置为边缘触发模式,需使用宏EPOLLET对event进行设置,具体示例如下:
event.events=EPOLLIN|EPOLLET
之后需要在循环中不断调用,保证将文件描述符中的数据全部读出。
下面给出具体案例,来展示epoll在边缘触发模式下如何实现双端通讯。ET模式只能工作在非阻塞模式下,否则单纯使用epoll(单进程)无法同时出来多个文件描述符。在实现案例之前先介绍一下设置文件描述符状态的方法,linux系统中可使用fcntl()函数来设置文件描述符的属性。
int fcntl(int fd,int cmd,…/*arg*/);
/**********************************************************
SYNOPSIS: #include < fcntl.h>
FUNCTION: epoll_ fcntl
DESCRIPTION: 获取或修改已打开文件的属性
PARAMETER:
(1) fd:为被操作文件描述符
(2) cmd:为操作fd的命令
(3) 用来接收命令cmd所需要的参数,该值可以为空
**********************************************************/
案例:
flag = fcntl(fd,F_GETFL);
flag |= O_NONBLOCK;
fcntl(fd,F_SETFL,flag);
使用epoll模型搭建多路I/O转接服务器,服务器可接受客户端数据并将接收的数据转换为大写,写回客户端;客户端可向服务端发送数据,并将服务端的数据打印在终端。
(1)服务端为LT模式时
使用epoll模型搭建多路I/O转接服务器,服务器可接受客户端数据并将接收的数据转换为大写,写回客户端;客户端可向服务端发送数据,并将服务端的数据打印在终端。
#include <cstdio>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<ctype.h>
#include<errno.h>
#define MAXLINE 80 //缓冲区数组大小
#define SERV_PORT 58000 //端口号
#define OPEN_MAX 1024 //最大打开文件描述符的数量
int main()
{
int listenfd, connfd, sockfd;
int nready, efd;
int res, maxi;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
struct epoll_event tep, ep[OPEN_MAX];
listenfd = socket(PF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
{
perror("socket");
exit(EXIT_FAILURE);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) == -1)
{
perror("bind!");
exit(EXIT_FAILURE);
}
if (listen(listenfd, 20) == -1)
{
perror("listen error");
exit(EXIT_FAILURE);
}
//初始化client集合
for (int i = 0; i < OPEN_MAX; i++)
{
client[i] = -1;
}
maxi = -1; //初始化maxi
efd = epoll_create(OPEN_MAX); //创建epoll句柄
if (efd == -1)
{
perror("epoll_create ");
exit(EXIT_FAILURE);
}
//初始化
tep.events = EPOLLIN;
tep.data.fd = listenfd;
//为服务器进程注册事件(listenfd)
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
if (res == -1)
{
perror("epool_ctl");
exit(EXIT_FAILURE);
}
for (;;)
{
nready = epoll_wait(efd, ep, OPEN_MAX, -1); //阻塞监听
if (nready == -1)
{
perror("epoll_wait");
exit(EXIT_FAILURE);
}
printf("epoll_wait\n");
for (int i = 0; i < nready; i++)
{
//若fd为listenfd,表示有连接请求到达
if (ep[i].data.fd == listenfd)
{
clilen = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen);
printf("received from %s at port %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),
ntohs(cliaddr.sin_port));
//将accpet获取的文件描述符保存到cient[]数组中
int j = 0;
for (j = 0; j < OPEN_MAX; j++)
{
if (client[j] < 0)
{
client[j] = connfd;
break;
}
}
if (j == OPEN_MAX)
{
perror("too many clients");
exit(EXIT_FAILURE);
}
if (j > maxi)
{
maxi = j; //更新最大文件描述符
}
tep.events = EPOLLIN;
tep.data.fd = connfd;
//为新建立连接的进程注册事件
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
if (res == -1)
{
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
}
else
{
sockfd = ep[i].data.fd;
int n = read(sockfd, buf, MAXLINE);
if (0 == n)
{
int j = 0;
for (j = 0; j < maxi; j++)
{
if (client[j] == sockfd)
{
client[i] = -1;
break;
}
}
//取消监听
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if (-1 == res)
{
perror("epoll_ctl");
exit(EXIT_FAILURE);
close(sockfd);
printf("client [%d] closed connection\n", j);
}
}
else
{
for (int j = 0; j < n; j++)
{
buf[j] = toupper(buf[j]);
}
write(sockfd, buf, n);
}
}
}
}
close(listenfd);
close(efd);
return 0;
}
(2) 服务端为ET模式时
搭建边沿触发模式下的epoll服务器,使服务器可接收并处例客户端发送的数据
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/wait.h>
#include<sys/epoll.h>
#include<sys/socket.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
#define MAXLINE 10
#define SERV_PORT 58000
#define EPOLL_SIZE 50
void error_handling(const char* buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
int main()
{
int listenfd, connfd;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clntaddr_len;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
int i, flag;
struct epoll_event ep_events[EPOLL_SIZE];
struct epoll_event event;
int epfd,event_cnt;
listenfd = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(SERV_PORT);
if (bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
{
error_handling("bind error!");
}
if (listen(listenfd,20)== -1)
{
error_handling("listen() error");
}
printf("Accepting connection ...\n");
clntaddr_len = sizeof(clnt_addr);
connfd = accept(listenfd, (struct sockaddr*)&clnt_addr, &clntaddr_len);
printf("received from %s at port %d\n", inet_ntop(AF_INET, &clnt_addr.sin_addr, str, sizeof(str)), ntohs(clnt_addr.sin_port));
flag=fcntl(connfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(connfd, F_SETFL, flag);
epfd = epoll_create(EPOLL_SIZE);
if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event) == -1)
{
error_handling("epoll_ctl error!");
}
event.events = EPOLLIN | EPOLLET;
event.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &event);
int len = 0;
//获取数据
while (1)
{
printf("epoll_wait begin\n");
int res = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
printf("epoll_wait end res=%d\n", res);
if (ep_events[0].data.fd==connfd)
{
while ((len=read(connfd,buf,MAXLINE/2))>0)
{
printf("%s", buf);
write(STDOUT_FILENO, buf, len);
}
}
}
return 0;
}
(3)客户端
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<errno.h>
#include<arpa/inet.h>
#define MAXLINE 80 //缓冲区数组大小
#define SERV_PORT 58000 //端口号
int main()
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
printf("create socket failed\n");
exit(EXIT_FAILURE);
}
memset(&servaddr, 0,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(SERV_PORT);
if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) != 0)
{
printf("connect failed\n");
exit(EXIT_FAILURE);
}
while (fgets(buf, MAXLINE, stdin) != NULL)
{
write(sockfd, buf, strlen(buf));
n = read(sockfd, buf, MAXLINE);
if (n == 0)
{
printf("the other side has been closed.\n");
}
else
{
write(STDOUT_FILENO, buf, n);
}
}
close(sockfd);
return 0;
}
[1]: linux 手册
[2]: linux编程基础书籍
[3]: https://www.jianshu.com/p/ed1f9e9a1982