目录
函数:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
参数timeout的取值:
返回值:
poll调用失败时,错误码可能被设置为:
(1)struct pollfd结构

(2)events和revents的取值
| 事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
|---|---|---|---|
| POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
| POLLRDNORM | 普通数据可读 | 是 | 是 |
| POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
| POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
| POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
| POLLWRNORM | 普通数据可写 | 是 | 是 |
| POLLWRBAND | 优先级带数据可写 | 是 | 是 |
| POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入 | 是 | 是 |
| POLLERR | 错误 | 否 | 是 |
| POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件 | 否 | 是 |
| POLLNVAL | 文件描述符没有打开 | 否 | 是 |

- #pragma once
-
- #include
- #include
- #include"sock.hpp"
-
- #define BACK_LOG 5
- #define NUM 1024
- #define DFL_FD -1
-
- namespace ns_poll{
-
- class Poll_Server{
- private:
- int listen_sock; //监听套接字
- int port; //端口号
- public:
- Poll_Server(int _port):port(_port){};
- ~Poll_Server()
- {
- if(listen_sock >=0 ){
- close(listen_sock);
- }
- }
-
- void InitPollServer()
- {
- listen_sock = ns_sock::Sock::Socket();
- ns_sock::Sock::Bind(listen_sock,port);
- ns_sock::Sock::Listen(listen_sock,BACK_LOG);
- }
-
- void Run()
- {
- struct pollfd fds[NUM];
- ClearPollfds(fds,NUM);
- SetPollfds(fds,NUM,listen_sock);
- for(;;){
- switch(poll(fds,NUM,-1)){ //阻塞式等待
- case 0:
- std::cout << "timeout ... " << std::endl;
- break;
- case -1:
- std::cerr << "poll error" << std::endl;
- break;
- default:
- //正常处理事件
- //std::cout << "有事件发生 ... " << std::endl;
- HandlerEvent(fds,NUM);
- break;
-
- }
- }
- }
-
-
- void HandlerEvent(struct pollfd* fds,int num)
- {
- for(int i = 0; i < num ; ++i){
- if(fds[i].fd == DFL_FD){
- continue;
- }
-
- //有效位置
- if(fds[i].fd == listen_sock && (fds[i].revents&POLLIN)){ //连接事件就绪
- struct sockaddr_in peer;
- memset(&peer, 0, sizeof(peer));
- socklen_t len = sizeof(peer);
- int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
- if(sock < 0){
- std::cerr << "accept error" << std::endl;
- continue;
- }
-
- std::string _ip = inet_ntoa(peer.sin_addr);
- int _port = ntohs(peer.sin_port);
- std::cout << "get a new link [ " << _ip << " : " << _port << " ]" << std::endl;
-
- //将获取到的套接字添加到fds数组中,并关心其读事件
- if(!SetPollfds(fds,num,sock)){
- close(sock);
- std::cout << "poll server is full, close fd: " << sock << std::endl;
- }
-
- }
- else if(fds[i].revents&POLLIN){ //读事件就绪
- char buffer[1024];
- ssize_t size = read(fds[i].fd,buffer,sizeof(buffer)-1);
- if(size > 0){
- buffer[size-1] = 0; //清除 /n
- std::cout << "echo# " << buffer << std::endl;
- }
- else if(size == 0){ //对方关闭
- std::cout << "client quit" << std::endl;
- close(fds[i].fd);
- UnSetPollfds(fds,i);
- }
- else{ //读取错误
- std::cerr << "read error" << std::endl;
- close(fds[i].fd);
- UnSetPollfds(fds,i);
- }
-
- }
- else{ //保证代码逻辑
- //TODO
- }
- }
- }
-
-
- private: //内部函数使用
- void ClearPollfds(struct pollfd* fds,int num)
- {
- for(int i = 0 ; i < num ; ++i){
- fds[i].fd = DFL_FD;
- fds[i].events = 0;
- fds[i].revents = 0;
- }
- }
-
- void UnSetPollfds(struct pollfd* fds,int pos)
- {
- fds[pos].fd = DFL_FD;
- fds[pos].events = 0;
- fds[pos].revents = 0;
- }
-
- bool SetPollfds(struct pollfd* fds,int num,int sock)
- {
- for(int i = 0 ; i < num ; ++i){
- if(fds[i].fd == DFL_FD){ //该位置没被使用
- fds[i].fd = sock;
- fds[i].events |= POLLIN; //添加读事件
- return true;
- }
- }
-
- return false; //fds数组已满
- }
-
- };
-
- }
(1)初始化服务器
(2)Run函数
(3)HandlerEvent函数
当poll检测到有文件描述符的读事件就绪,就会在其对应的struct pollfd结构中的revents成员中添加读事件并返回,接下来poll服务器就应该对就绪事件进行处理了,事件处理过程如下:
(4)poll_server.cc
- #include "poll_server.hpp"
- #include
- #include
-
- static void Usage(std::string proc)
- {
- std::cerr << "Usage:" << "\n\t" << proc << " port" << std::endl;
- }
-
- int main(int argc, char *argv[])
- {
- if(argc != 2){
- Usage(argv[0]);
- exit(4);
- }
-
- unsigned short port = atoi(argv[1]);
-
- ns_poll::Poll_Server* poll_svr = new ns_poll::Poll_Server(port);
- //server一般运行起来就不停止了,是否delete不影响
-
- poll_svr->InitPollServer();
- poll_svr->Run();
-
- return 0;
- }


struct pollfd结构当中包含了events和revents,相当于将select的输入输出型参数进行分离,因此在每次调用poll之前,不需要像select一样重新对参数进行设置。
poll可监控的文件描述符数量没有限制。
当然,poll也可以同时等待多个文件描述符,能够提高IO的效率。
说明:
epoll系统调用也可以让我们的程序同时监视多个文件描述符上的事件是否就绪,与select和poll的定位是一样的,适用场景也相同。
epoll在命名上比poll多了一个e,这个e可以理解成是extend,epoll就是为了同时处理大量文件描述符而改进的poll。
epoll在2.5.44内核中被引进,它几乎具备了select和poll的所有优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。
(1) epoll_create函数
epoll_create函数用于创建一个epoll模型 :
参数:
返回值:
注意: 当不再使用时,必须调用close函数关闭epoll模型对应的文件描述符,当所有引用epoll实例的文件描述符都已关闭时,内核将销毁该实例并释放相关资源。
(2)epoll_ctl函数
epoll_ctl函数用于向指定的epoll模型中注册事件:
参数:
返回值:
①第二个参数op的取值有以下三种:
②第四个参数对应的struct epoll_event结构:

③events的常用取值如下:
④events取值实际也是以宏的方式进行定义的,它们的二进制序列当中有且只有一个比特位是1,且为1的比特位是各不相同的。

(3)epoll_wait 函数
epoll_ctl函数用于收集监视的事件中已经就绪的事件:
参数:
参数timeout的取值:
返回值:
epoll_wait调用失败时,错误码可能被设置为:
(4)epoll模型

①所谓的epoll模型是通过进程的方式,指针的方式找到这—套机制。调用epoll_create 返回值是—个fd,就是说我们可以创建很多epoll模型彼此之间是独立的,有不同的fd指向自己的epoll模型
②回调机制:我们在网卡驱动层面注册的一个数据就绪的方法/函数,比如 read就绪,write就绪函数;当网卡当中/OS内部识别到了你有数据或者空间时会自动完成回调对应的方法。
③用户通过epoll_ctl填写到底层fd:event,即告诉OS关系哪些fd上面的哪些事件
④当红黑树中某个节点中的fd事件就绪, 在OS层面使用驱动层的功能完成某些回调功能 ;回调功能执行 :OS在系统层帮我们生成一个新的节点并且链接到我们的就绪队列当中
⑤如果这里我们调用epoll_ctl 的时候,我们设置了fd3,4.5没有设置6号fd,回调的时候一旦6号fd就绪会不会通知上层 ?
⑥epoll模型自己去设计可以把它们封装成类,写在一起,这里的epoll模型不就是个对象,这些东西各自一个结构然后用一个统一的结构包含在一起就可以用这个类定义对象,就是一个epoll模型;然后把这个对象放到文件的struct file里面,就可以通过fd找到这个epoll模型
⑦ epoll底层采用回调机制,来检测事件就绪;底层事件就绪的时候将 fd:event 拷贝到就绪队列,不断地通过epoll_wait,从队列中取节点。
⑧ select,poll这两个函数承担两种职责,而epoll通过接口就将两个职责分开了
⑨当一个数据从网络中来的时候OS怎么知道网卡里面有数据来了? – 中断机制

(1)当某一进程调用epoll_create函数时,Linux内核会创建一个eventpoll结构体,也就是我们所说的epoll模型,eventpoll结构体当中的成员rbr和rdlist与epoll的使用方式密切相关。
- struct eventpoll{
- ...
- //红黑树的根节点,这棵树中存储着所有添加到epoll中的需要监视的事件
- struct rb_root rbr;
- //就绪队列中则存放着将要通过epoll_wait返回给用户的满足条件的事件
- struct list_head rdlist;
- ...
- }
(2)在epoll中,对于每一个事件都会有一个对应的epitem结构体,红黑树和就绪队列当中的节点分别是基于epitem结构中的rbn成员和rdllink成员的,epitem结构当中的成员ffd记录的是指定的文件描述符值,event成员记录的就是该文件描述符对应的事件。
- struct epitem{
- struct rb_node rbn; //红黑树节点
- struct list_head rdllink; //双向链表节点
- struct epoll_filefd ffd; //事件句柄信息
- struct eventpoll *ep; //指向其所属的eventpoll对象
- struct epoll_event event; //期待发生的事件类型
- }
(3)说明
(4)回调机制
所有添加到红黑树当中的事件,都会与设备(网卡)驱动程序建立回调方法,这个回调方法在内核中叫ep_poll_callback。
说明一下:
- #pragma once
-
- #include
- #include
- #include"sock.hpp"
-
- #define BACK_LOG 5
- #define SIZE 256
- #define MAX_NUM 64
-
- namespace ns_epoll{
-
- class Epoll_Server{
- private:
- int listen_sock; //监听套接字
- int port; //端口号
- int epfd; //epoll模型
- public:
- Epoll_Server(int _port):port(_port){};
- ~Epoll_Server()
- {
- if(listen_sock >=0 ){
- close(listen_sock);
- }
-
- if(epfd >=0 ){
- close(epfd);
- }
- }
-
- void InitEpollServer()
- {
- listen_sock = ns_sock::Sock::Socket();
- ns_sock::Sock::Bind(listen_sock,port);
- ns_sock::Sock::Listen(listen_sock,BACK_LOG);
-
- //创建epoll模型
- epfd = epoll_create(SIZE);
- if(epfd < 0){
- std::cerr << "epoll_create error" << std::endl;
- exit(5);
- }
- }
-
- void Run()
- {
- //将监听套接字添加到epoll模型中,并关心读事件
- AddEvent(listen_sock,EPOLLIN);
- for(;;){
- struct epoll_event revs[MAX_NUM];
- int num = epoll_wait(epfd,revs,MAX_NUM,-1);
- if(num < 0){
- std::cerr << "epoll_wait error ..." << std::endl;
- continue;
- }
- else if(num == 0){
- std::cout << "timeout ..." << std::endl;
- continue;
- }
- else{
- //有事件就绪
- HandlerEvent(revs,num);
- }
-
- }
- }
-
-
- void HandlerEvent(struct epoll_event* revs,int num)
- {
- for(int i = 0; i < num ; ++i){
- int fd = revs[i].data.fd;
-
-
- //有效位置
- if(fd == listen_sock && (revs[i].events&EPOLLIN)){ //连接事件就绪
- struct sockaddr_in peer;
- memset(&peer, 0, sizeof(peer));
- socklen_t len = sizeof(peer);
- int sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
- if(sock < 0){
- std::cerr << "accept error" << std::endl;
- continue;
- }
-
- std::string _ip = inet_ntoa(peer.sin_addr);
- int _port = ntohs(peer.sin_port);
- std::cout << "get a new link [ " << _ip << " : " << _port << " ]" << std::endl;
-
- //将获取到的套接字添加到epoll模型中,并关心其读事件
- AddEvent(sock,EPOLLIN);
-
- }
- else if(revs[i].events&EPOLLIN){ //读事件就绪
- char buffer[64];
- ssize_t size = recv(fd,buffer,sizeof(buffer)-1,0);
- if(size > 0){
- buffer[size-1] = 0; //清除 /n
- std::cout << "echo# " << buffer << std::endl;
- }
- else if(size == 0){ //对方关闭
- std::cout << "client quit" << std::endl;
- close(fd);
- DelEvent(fd); //将fd从epoll模型中移出
- }
- else{ //读取错误
- std::cerr << "recv error" << std::endl;
- close(fd);
- DelEvent(fd);//将fd从epoll模型中移出
- }
-
- }
- else{ //保证代码逻辑
- //TODO
- }
- }
- }
-
-
- private: //内部使用函数
- void AddEvent(int sock,uint32_t event)
- {
- struct epoll_event ev;
- ev.events = event;
- ev.data.fd = sock;
-
- epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev);
- }
-
- void DelEvent(int sock)
- {
- epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
- }
-
- };
-
(1)服务器初始化
Epoll_Server类当中除了需要包含监听套接字和端口号两个成员变量之外,最好将epoll模型对应的文件描述符也作为一个成员变量。
(2)Run函数
说明:
(3)HandlerEvent函数
如果底层就绪队列当中有就绪事件,那么调用epoll_wait函数时就会将底层就绪队列中的事件拷贝到用户提供的revs数组当中,接下来epoll服务器就应该对就绪事件进行处理了,事件处理过程如下:
(4)server.cc
- #include "epoll_server.hpp"
- #include
- #include
-
- static void Usage(std::string proc)
- {
- std::cerr << "Usage:" << "\n\t" << proc << " port" << std::endl;
- }
-
- int main(int argc, char *argv[])
- {
- if(argc != 2){
- Usage(argv[0]);
- exit(4);
- }
-
- unsigned short port = atoi(argv[1]);
-
- ns_epoll::Epoll_Server* epoll_svr = new ns_epoll::Epoll_Server(port);
- //server一般运行起来就不停止了,是否delete不影响
-
- epoll_svr->InitEpollServer();
- epoll_svr->Run();
-
- return 0;
- }

用ls /proc/PID/fd命令,查看当前epoll服务器的文件描述符的使用情况。


注意:
select和poll,epoll的不同之处
如果有海量的fd需要被关心,大量的fd已经就绪,这个基于epoll的服务器的效率会不会受影响?
水平触发(LT,Level Triggered)

边缘触发(ET,Edge Triggered)

(1)epoll是怎么做到的事件如果没有处理一直在通知?
所谓的事件处理并不是你把事件读上去就行了,你要对就绪事件的空间有数据读数据/有空间写数据。底层引起事件就绪本质是OS内部有数据你不读,有空间你不写。
(2)ET工作模式下应该如何进行读写
(3)总结,为什么ET模式下read/write必须是非阻塞?
(4)ET和LT的对比