1. poll详解
intpoll(struct pollfd *fd, nfds_t nfds, int timeout);
structpollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */
};
结构体红各项含义如下:
2. epoll详解
(1)API介绍
intepoll_create(int size);
intepoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
structepoll_event {/* 该结构体主要存放和fd有关的信息 */
uint32_t events;
epoll_data_t data;
};
typedefunion epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_data_t是一个联合体union,四个成员共用同一块内存,也就是说四个成员我们只能用一个,一般情况下我们用fd,这个fd实际上就是epoll_ctl()函数的第三个参数fd。
如果我们想在epoll树上挂载更多信息,而不仅仅是fd文件描述符的话,我们可以把更多信息封装在结构体中,并把该结构体传给epoll_data_t结构体的ptr指针,这样就可以在epoll树上挂载和fd有关的更多信息。
structsockInfo
{
int fd;
structsockaddr_inaddr;
};
比如说,要获取发生变化的fd对应的client的IP和port,就可以利用指针ptr,这样的话联合epoll_data_t中的fd就不能用了,我们把文件描述符传给sockInfo的fd即可完成fd信息的挂载。
intepoll_wait(
int epfd,
struct epoll_event* events, /* 结构体数组 */
int maxevents,
int timeout
);
(2)epoll树
(3)epoll模型
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<sys/epoll.h>
intmain(int argc, constchar* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
structsockaddr_inserv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]); //字符串转整形值
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
structsockaddr_inclient_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
structepoll_eventev;
ev.events = EPOLLIN;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
//存放发生变化的fd对应的树节点
structepoll_eventall[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
// 遍历all数组中的前ret个元素 //ret表示有几个变化的fd,变化的fd都存在all数组中
for(int i=0; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求 // accept不阻塞,因为已经有连接
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
// 将新得到的cfd挂到树上
structepoll_eventtemp;
temp.events = EPOLLIN; //检测cfd对应的读缓冲区,是否有数据传入
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN) //只处理读事件
{
continue;
}
/*
假如说client发送过了100个数据,也就是serve的read缓冲区有100个数据,
但是调用recv函数的时候只能读50个数据,而本次循环只调用了一次recv,
那么只能下次循环再读剩余的50个数据,所以下次循环检测的时候,
epoll_wait还是会返回,因为缓冲区还是剩余数据。这就是水平触发模式。
这样的话虽然client只发了1次,但是epoll_wait会通知两次server去读数据。
*/
// 读数据
char buf[1024] = {0};
int len = recv(fd, buf, sizeof(buf), 0);
if(len == -1)
{
perror("recv error");
exit(1);
}
elseif(len == 0)
{
printf("client disconnected ....\n");
//close(fd);
// fd从epoll树上删除
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
// 挂树的时候需要ev,把ev挂在树上删除写NULL就行了
if(ret == -1)
{
perror("epoll_ctl del error");
exit(1);
}
close(fd);
}
else
{
printf(" recv buf: %s\n", buf);
write(fd, buf, len);
}
}
}
}
close(lfd);
return0;
}
epoll维护的红黑树是存在一个共享内存中,内核和用户都可以通过操作这个共享内存来操作树,不需要内核态和用户态的切换,也不需要两种状态之间的数据拷贝,所以效率更高。
(4)epoll的三种工作模式
对于epoll_wait()来说,epoll_wait 调用次数越多, 系统的开销越大。
水平触发模式会多次返回,只要server的read缓冲区有数据,epoll_wait就返回,也就会通知server去读数据,那么在循环检测的时候,只要server的read缓冲区有数据,epoll_wait就会多次调用,多次返回,并通知server去读数据;假如说client发送过了100个数据,也就是serve的read缓冲区有100个数据,但是调用recv函数的时候只能读50个数据,而本次循环只调用了一次recv,那么只能下次循环再读剩余的50个数据,所以下次循环检测的时候,epoll_wait还是会返回,因为缓冲区还是剩余数据。这就是水平触发模式。这样的话虽然client只发了1次,但是epoll_wait会通知两次server去读数据。—— (printf函数是标准C库函数,C库函数都有一个默认缓冲区,printf的大小是8K。printf函数是行缓冲,使用printf函数的时候,如果不加 \n 会默认等到写满的时候才打印内容,加 \n 会强制把缓冲区的内容打印出来。另外 \0 表示结束,不加 \0 就会一直输出直到遇到 \0,用write(STDOUT_FILENO)替代printf函数就可以解决这些问题。)
边沿触发模式,client发一次数据epoll_wait只返回一次,也就只读一次,这样的话server的read缓冲区可能会有很多数据堆积,server读数据的时候可能读到的是上一次剩余的数据,并且只有client发的时候,epoll_wait才会通知server去读数据,边沿触发模式尽可能减少了epoll_wait的调用次数,缺点是数据有可能读不完导致堆积;
while(recv() > 0)
{
printf();
}
示例
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<ctype.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<errno.h>
intmain(int argc, constchar* argv[])
{
if(argc < 2)
{
printf("eg: ./a.out port\n");
exit(1);
}
structsockaddr_inserv_addr;
socklen_t serv_len = sizeof(serv_addr);
int port = atoi(argv[1]);
// 创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
// 初始化服务器 sockaddr_in
memset(&serv_addr, 0, serv_len);
serv_addr.sin_family = AF_INET; // 地址族
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听本机所有的IP
serv_addr.sin_port = htons(port); // 设置端口
// 绑定IP和端口
bind(lfd, (struct sockaddr*)&serv_addr, serv_len);
// 设置同时监听的最大个数
listen(lfd, 36);
printf("Start accept ......\n");
structsockaddr_inclient_addr;
socklen_t cli_len = sizeof(client_addr);
// 创建epoll树根节点
int epfd = epoll_create(2000);
// 初始化epoll树
structepoll_eventev;
// 设置边沿触发
ev.events = EPOLLIN; //监听的文件描述符没必要边沿触发,主要是通信的cfd
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
structepoll_eventall[2000];
while(1)
{
// 使用epoll通知内核fd 文件IO检测
int ret = epoll_wait(epfd, all, sizeof(all)/sizeof(all[0]), -1);
printf("================== epoll_wait =============\n");
// 遍历all数组中的前ret个元素
for(int i=0; i<ret; ++i)
{
int fd = all[i].data.fd;
// 判断是否有新连接
if(fd == lfd)
{
// 接受连接请求
int cfd = accept(lfd, (struct sockaddr*)&client_addr, &cli_len);
if(cfd == -1)
{
perror("accept error");
exit(1);
}
// 设置文件cfd为非阻塞模式
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
// 将新得到的cfd挂到树上
structepoll_eventtemp;
// 设置边沿触发
temp.events = EPOLLIN | EPOLLET;
temp.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &temp);
// 打印客户端信息
char ip[64] = {0};
printf("New Client IP: %s, Port: %d\n",
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip, sizeof(ip)),
ntohs(client_addr.sin_port));
}
else
{
// 处理已经连接的客户端发送过来的数据
if(!all[i].events & EPOLLIN)
{
continue;
}
// 读数据
char buf[5] = {0};
int len;
// 循环读数据
while( (len = recv(fd, buf, sizeof(buf), 0)) > 0 )
{
// 数据打印到终端
//不要用printf,因为printf如果找不到 \0 \n 字符会出现乱码,打印不出来等问题
write(STDOUT_FILENO, buf, len);
// 发送给客户端
send(fd, buf, len, 0);
}
if(len == 0)
{
printf("客户端断开了连接\n");
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
elseif(len == -1)
{
//数据已经被读完了,因为是非阻塞,所以在while循环中recv还要继续读,导致返回-1
if(errno == EAGAIN)
{
printf("缓冲区数据已经读完\n");
}
else
{
//这才是真正的recv错误
printf("recv error----\n");
exit(1);
}
}
#if 0
if(len == -1)
{
perror("recv error");
exit(1);
}
elseif(len == 0)
{
printf("client disconnected ....\n");
// fd从epoll树上删除
ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
if(ret == -1)
{
perror("epoll_ctl - del error");
exit(1);
}
close(fd);
}
else
{
// printf(" recv buf: %s\n", buf);
write(STDOUT_FILENO, buf, len);
write(fd, buf, len);
}
#endif
}
}
}
close(lfd);
return0;
}
5)文件描述符1024限制
对于select来说,无法突破文件描述符1024上限,因为select是通过数组实现的。poll和epoll可以突破1024限制,poll是内部链表实现,而epoll是红黑树实现。
查看受计算机硬件限制的文件描述符上限可以通过下面命令
cat /proc/sys/fs/file-max
同样,我们也可以通过修改配置文件来修改这个上限,但是,我们在程序中设置的时候不能超过硬件限制的上限
vim /etc/security/limits.conf
- soft nofile 8000 —— 也可以通过命令ulimit -n 2000来修改为2000
- hard nofile 8000 —— 硬件资源限制
修改后重启系统即可起效。