本人不才,只能从表面理解epoll函数的机制,看了很多博客,由于缺乏基础知识,所以对内核中的实现和其数据结构理解不到位,粗浅地来认识一下。
系统打开的最大文件描述符 也是有限制的,并且这个最大量和内存有关
cat /proc/sys/fs/file-max
194720
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);\\添加套接字 typedef union epoll_data{ void *ptr; int fd; _uint32_t u32; _uint64_t u64; }epoll_data_t; struct epoll_event{ _uint32_t events; epoll_data_t data;\\epoll高效的原因 };
.........................
看一下select低效的原因int nready=select(maxfd+1,&rset,NULL,NULL,NULL);//select的实现其实是对maxfd+1个描述符全部进行遍历,没发生事件的位被清除并且同时将发生事件描述符拷贝至rset。全部遍历maxfd+1检测到有I\O事件后,将发生事件的个数返回给nready,从这里可以看出,select经历了一次遍历和拷贝过程。时间复杂度为O(n)
同理poll也一样是O(n).
接着看epoll:
epoll_create(int size)//这个函数是创建一个哈希表,size表示哈希表的容量 epoll_create1(int flags)//这个是实现了一棵红黑树,不需要指定容量
#include<stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <errno.h> #include<iostream> #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> #include<sys/select.h> #include<errno.h> #include<sys/poll.h> #include<vector> #include<algorithm> #include<sys/signal.h> #include<fcntl.h> #include<sys/epoll.h> using namespace std; typedef std::vector<struct epoll_event> EventList; #define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); }while(0) void activate_nonblock(int fd){ int ret; int flags=fcntl(fd,F_GETFL); if(flags==-1) ERR_EXIT("FCNTL"); flags|=O_NONBLOCK; ret=fcntl(fd,F_SETFL,flags); if(ret==-1) ERR_EXIT("fcntl"); } int main() { signal(SIGPIPE,SIG_IGN); //signal(SIGPIPE,handler); int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) { perror("socket() err"); return -1; } int on = 1; if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { perror("setsockopt() err"); return -1; } int conn; vector<int> client; struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(8888); addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (bind(sockfd, (struct sockaddr *) &addr, sizeof(addr)) == -1) { perror("bind() err"); return -1; } if (listen(sockfd, SOMAXCONN) == -1) { perror("bind() err"); return -1; } struct sockaddr_in peeraddr; int nready; int epollfd; struct epoll_event event; event.data.fd=sockfd; event.events=EPOLLIN|EPOLLET; epollfd=epoll_create1(EPOLL_CLOEXEC); epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);//将sockfd和event加入epollfd进行管理 EventList Events(16); int count=0; while(1){ int i;socklen_t peerlen; nready=epoll_wait(epollfd,&*Events.begin(),static_cast<int>(Events.size()),-1); if((size_t)nready==Events.size()) Events.resize(Events.size()*2); for( i=0;i<nready;i++){ if(Events[i].data.fd==sockfd){ peerlen=sizeof(peeraddr); conn=accept(sockfd,(struct sockaddr *)&peeraddr,&peerlen); printf("ip=%s port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port)); printf("count=%d\n",++count); client.push_back(conn); activate_nonblock(conn); event.data.fd=conn; event.events=EPOLLIN|EPOLLET; epoll_ctl(epollfd,EPOLL_CTL_ADD,conn,&event); } else if(Events[i].events&EPOLLIN){ conn=Events[i].data.fd; if(conn<0) continue; char recvbuf[1024]={0}; int ret=read(conn,recvbuf,1024); if(ret==-1) ERR_EXIT("read"); if(ret==0){ printf("client close\n"); close(conn); event=Events[i]; epoll_ctl(epollfd,EPOLL_CTL_DEL,conn,&event); client.erase(std::remove(client.begin(),client.end(),conn),client.end()); } fputs(recvbuf,stdout); write(conn,recvbuf,strlen(recvbuf)); } } } }
epoll_serverepoll
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
通过epoll_ctl函数添加进来的事件都会被放在红黑树的某个节点内,所以,重复添加是没有用的。当把事件添加进来的时候时候会完成关键的一步,那就是该事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,就会调用这个回调函数,该回调函数在内核中被称为:ep_poll_callback,这个回调函数其实就所把这个事件添加到rdllist这个双向链表中。一旦有事件发生,epoll就会将该事件添加到双向链表中。那么当我们调用epoll_wait时,epoll_wait只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观。这里也需要将发生了的事件复制到用户态内存中即可。
也就是说我们得到epoll_wait返回准备好的时间并不需要遍历完整个描述符。这和底层实现有关,由于能力不足,只能理解到这。以后再慢慢补充
原文地址:https://www.cnblogs.com/illfuckingkyzb/p/10152623.html
时间: 2024-10-29 19:59:54