高权重网站发外链,上海市电话黄页本,上海家政公司排名,全国造价工程师管理系统官网本文目录 一、poll二、epoll2.1 相对poll和select的优点2.2 epoll的api2.3 epoll的demo实现2.5 epoll的工作模式 一、poll
poll是对select的一个改进#xff0c;我们先来看看select的缺点。 我们来看看poll的实现。
struct pollfd {int fd; /* 委托内核检测的文件描述符 */s… 本文目录 一、poll二、epoll2.1 相对poll和select的优点2.2 epoll的api2.3 epoll的demo实现2.5 epoll的工作模式 一、poll
poll是对select的一个改进我们先来看看select的缺点。 我们来看看poll的实现。
struct pollfd {int fd; /* 委托内核检测的文件描述符 */short events; /* 委托内核检测文件描述符的什么事件 */short revents; /* 文件描述符实际发生的事件 */
};struct pollfd myfd;
myfd.fd 5;
myfd.events POLLIN | POLLOUT;int poll(struct pollfd *fds, nfds_t nfds, int timeout);参数说明如下。 fds是struct pollfd结构体数组这是一个需要检测的文件描述符集合。
当内核检测到有变动之后有需要修改的会直接修改revents不需要修改events了相对select来说就不需要每次重置fds集合。
除此之外相对于select来说并没有1024的限制。 nfds是第一个参数数组中最后一个有效元素的下标1。
timeout注意这个是int类型的当为0时代表不阻塞当为-1时表示阻塞当检测到需要检测的文件描述符发生了变化解除阻塞。0表示阻塞的时长。
poll函数返回值为-1时表示失败0会返回n表示检测到集合中有n个描述符发生了变化。 poll的服务端实现代码如下。
#include stdio.h
#include arpa/inet.h
#include unistd.h
#include stdlib.h
#include string.h
#include poll.hint main() {// 创建socketint lfd socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port htons(9999);saddr.sin_family AF_INET;saddr.sin_addr.s_addr INADDR_ANY;// 绑定bind(lfd, (struct sockaddr *)saddr, sizeof(saddr));// 监听listen(lfd, 8);// 初始化检测的文件描述符数组struct pollfd fds[1024];for(int i 0; i 1024; i) {fds[i].fd -1;fds[i].events POLLIN;}fds[0].fd lfd;int nfds 0;while(1) {// 调用poll系统函数让内核帮检测哪些文件描述符有数据int ret poll(fds, nfds 1, -1);if(ret -1) {perror(poll);exit(-1);} else if(ret 0) {continue;} else if(ret 0) {// 说明检测到了有文件描述符的对应的缓冲区的数据发生了改变if(fds[0].revents POLLIN) {// 表示有新的客户端连接进来了struct sockaddr_in cliaddr;int len sizeof(cliaddr);int cfd accept(lfd, (struct sockaddr *)cliaddr, len);// 将新的文件描述符加入到集合中for(int i 1; i 1024; i) {if(fds[i].fd -1) {fds[i].fd cfd;fds[i].events POLLIN;break;}}// 更新最大的文件描述符的索引nfds nfds cfd ? nfds : cfd;}for(int i 1; i nfds; i) {if(fds[i].revents POLLIN) {// 说明这个文件描述符对应的客户端发来了数据char buf[1024] {0};int len read(fds[i].fd, buf, sizeof(buf));if(len -1) {perror(read);exit(-1);} else if(len 0) {printf(client closed...\n);close(fds[i].fd);fds[i].fd -1;} else if(len 0) {printf(read buf %s\n, buf);write(fds[i].fd, buf, strlen(buf) 1);}}}}}close(lfd);return 0;
}
对应的客户端代码我们继续沿用之前的即可。
#include stdio.h
#include arpa/inet.h
#include stdlib.h
#include unistd.h
#include string.hint main() {// 创建socketint fd socket(PF_INET, SOCK_STREAM, 0);if(fd -1) {perror(socket);return -1;}struct sockaddr_in seraddr;inet_pton(AF_INET, 127.0.0.1, seraddr.sin_addr.s_addr);seraddr.sin_family AF_INET;seraddr.sin_port htons(9999);// 连接服务器int ret connect(fd, (struct sockaddr *)seraddr, sizeof(seraddr));if(ret -1){perror(connect);return -1;}int num 0;while(1) {char sendBuf[1024] {0};sprintf(sendBuf, send data %d, num);write(fd, sendBuf, strlen(sendBuf) 1);// 接收int len read(fd, sendBuf, sizeof(sendBuf));if(len -1) {perror(read);return -1;}else if(len 0) {printf(read buf %s\n, sendBuf);} else {printf(服务器已经断开连接...\n);break;}// sleep(1);usleep(1000);}close(fd);return 0;
}
二、epoll
首先调用epoll_create实现一个epoll的实例这个epoll实例是在内核区是结构体类型可以理解成一块数据。返回值是一个文件描述符那我们就可以通过这个文件描述符来操作这块内核当中的epoll数据通过epoll提供的一些api来进行操作。
eventpoll中有两个最关键的数据就是rbr和rdlist也就是红黑树和双向就绪链表。
rbr记录需要检测的文件描述符。之前需要把一些表从用户态拷贝到内核态现在是直接在内核态效率高了很多。另外现在是红黑树之前是链表红黑树的遍历效率也高很多。
rdlist是检测文件描述符当中哪些是有数据发生改变的。
在函数epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,ev)中最后一个参数ev是struct epoll_event类型需要设置events和ev.data.fd。
调用epoll_wait之后内核就会去检测rbr里面的文件描述符是否有对应的数据改变。如果有改变的就绪的就会把文件描述符的信息放到rdlist中然后会把这个list拷贝到用户区这样用户区直接遍历这几个fd就可以进行对应的读写操作了。 2.1 相对poll和select的优点
在时间复杂度方面每次调用 select 或 poll 时内核需要遍历所有被监控的文件描述符检查它们的状态。select 和 poll 的时间复杂度是 O(n)。当文件描述符数量很大时例如成千上万个这种线性扫描的效率会非常低。
epoll 的时间复杂度是 O(1)epoll 使用红黑树和双向链表来管理文件描述符。当文件描述符的状态发生变化时内核会将其加入到就绪链表中用户程序只需要检查就绪链表即可而不需要遍历所有文件描述符。
在文件描述符数量限制方面默认情况下select 只能监控最多 1024 个文件描述符由 FD_SETSIZE定义。如果需要监控更多的文件描述符需要修改内核参数并重新编译程序。poll 使用数组来存储文件描述符理论上可以监控任意数量的文件描述符。但当文件描述符数量很大时遍历整个数组的效率会非常低。
epoll 可以轻松支持数万个甚至更多的文件描述符。它使用红黑树来存储文件描述符查找和插入的效率很高。
在 用户态和内核态的数据拷贝方面每次调用 select 或 poll 时都需要将文件描述符集合从用户态拷贝到内核态当文件描述符数量很大时这种拷贝操作会带来较大的开销。
对于epoll文件描述符只需要通过 epoll_ctl 添加到内核事件表中一次后续不需要重复拷贝。当文件描述符状态变化时内核会直接将事件放入就绪链表中用户程序通过 epoll_wait 获取就绪事件。
在 事件触发模式方面select 和 poll 只支持水平触发Level-TriggeredLT模式如果文件描述符的状态满足条件例如有数据可读select 和 poll 会一直通知用户程序直到状态发生变化。 epoll 支持水平触发LT和边缘触发Edge-TriggeredET模式 水平触发LT与 select 和 poll 的行为相同只要文件描述符的状态满足条件就会一直通知用户程序。边缘触发ET只有当文件描述符的状态发生变化时才会通知用户程序。这种模式可以减少重复通知的次数提高效率。但是需要用户程序一次性处理完所有数据否则可能会丢失数据。
在内核实现机制方面select 和 poll 是基于轮询的机制每次调用时内核需要遍历所有文件描述符检查它们的状态。这种机制在大规模并发场景下效率较低。
epoll 是基于事件回调的机制内核会为每个文件描述符注册回调函数当文件描述符的状态发生变化时内核会调用回调函数将其加入到就绪链表中。这种机制避免了不必要的遍历效率更高。
2.2 epoll的api
头文件如下。
#include sys/epoll.h/创建一个新的epoll实例。在内核中创建了一个数据这个数据中有两个比较重要的数据一个是需要检测的文件描述符的信息红黑树还有一个是就绪列表存放检测到数据发送改变的文件描述符信息双向链表。【从linux内核2.6.8开始size这个参数已经被忽略了但是必须大于0。】
int epoll_create(int size);
- 参数size : 目前没有意义了。随便写一个数必须大于0
- 返回值-1 : 失败 0 : 文件描述符操作epoll实例的对epoll实例进行管理添加文件描述符信息删除信息修改信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- 参数- epfd : epoll实例对应的文件描述符- op : 要进行什么操作EPOLL_CTL_ADD: 添加EPOLL_CTL_MOD: 修改比如从读事件改成写事件EPOLL_CTL_DEL: 删除- fd : 要检测的文件描述符- event : 检测文件描述符什么事情epoll_event是检测事件的结构体定义如下。
struct epoll_event {uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
};
常见的Epoll检测事件- EPOLLIN - EPOLLOUT - EPOLLERR- EPOLLET 设置边沿触发在其中又有一个联合体epoll_data_t定义如下。通过联合体用户可以选择存储不同类型的数据如指针、文件描述符、32 位或 64 位整数。
ptr是一个指向任意类型的指针。用户可以将与事件相关的任意数据存储在这个指针中例如指向某个结构体的指针。这种方式非常灵活可以存储用户自定义的数据结构。 fd这是 epoll 最常用的用途之一直接存储与事件相关的文件描述符。 u32是一个 32 位的无符号整数。用户可以存储一些简单的整数值作为用户数据。u64同理。
联合体 epoll_data_t 的设计允许用户根据需要选择存储不同类型的数据。联合体的特性是所有成员共享同一块内存因此在任何时刻联合体中只有一个成员是有效的。用户可以根据实际需求选择存储哪种类型的数据。
typedef union epoll_data {void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;检测函数如下。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, inttimeout);- 参数- epfd : epoll实例对应的文件描述符- events : 传出参数保存了发送了变化的文件描述符的信息- maxevents : 第二个参数结构体数组的大小- timeout : 阻塞时间- 0 : 不阻塞- -1 : 阻塞直到检测到fd数据发生变化解除阻塞
- 0 : 阻塞的时长毫秒- 返回值- 成功返回发送变化的文件描述符的个数 0- 失败 -1这里有个问题是在使用 epoll 时epoll_ctl 函数确实已经将文件描述符fd注册到了 epoll 实例中但 epoll_event 结构体中的 data.fd 仍然需要存储文件描述符的原因主要有以下几点
首先epoll_ctl是用于将文件描述符注册到 epoll 实例中并设置相关的事件类型如 EPOLLIN、EPOLLOUT 等。它的作用是告诉 epoll 哪些文件描述符需要被监控以及监控哪些类型的事件。
而epoll_event 中 用于在 epoll_wait 调用时返回检测到的事件。 它的作用是告诉用户哪些文件描述符发生了事件以及发生了哪些类型的事件。epoll_event 中的 data.fd 是为了方便用户在 epoll_wait 返回后能够直接获取到发生事件的文件描述符。
当 epoll_wait 返回时它会返回一个 epoll_event 数组每个 epoll_event 表示一个发生事件的文件描述符及其事件类型。通过在 epoll_event 中存储 fd用户可以直接从 epoll_event 中获取到发生事件的文件描述符而无需额外查找。
2.3 epoll的demo实现
#include stdio.h
#include arpa/inet.h
#include unistd.h
#include stdlib.h
#include string.h
#include sys/epoll.hint main() {// 创建socketint lfd socket(PF_INET, SOCK_STREAM, 0);struct sockaddr_in saddr;saddr.sin_port htons(9999);saddr.sin_family AF_INET;saddr.sin_addr.s_addr INADDR_ANY;// 绑定bind(lfd, (struct sockaddr *)saddr, sizeof(saddr));// 监听listen(lfd, 8);// 调用epoll_create()创建一个epoll实例int epfd epoll_create(100);// 将监听的文件描述符相关的检测信息添加到epoll实例中struct epoll_event epev;epev.events EPOLLIN;epev.data.fd lfd;epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, epev);struct epoll_event epevs[1024];while(1) {int ret epoll_wait(epfd, epevs, 1024, -1);if(ret -1) {perror(epoll_wait);exit(-1);}printf(ret %d\n, ret);for(int i 0; i ret; i) {int curfd epevs[i].data.fd;if(curfd lfd) {// 监听的文件描述符有数据达到有客户端连接struct sockaddr_in cliaddr;int len sizeof(cliaddr);int cfd accept(lfd, (struct sockaddr *)cliaddr, len);epev.events EPOLLIN | EPOLLOUT; //监听的事件比较多所以每一种事件在下方都需要进行对应的处理。epev.data.fd cfd;epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, epev);} else {if(epevs[i].events EPOLLOUT) {continue;} // 有数据到达需要通信char buf[1024] {0};int len read(curfd, buf, sizeof(buf));if(len -1) {perror(read);exit(-1);} else if(len 0) {printf(client closed...\n);epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);close(curfd);} else if(len 0) {printf(read buf %s\n, buf);write(curfd, buf, strlen(buf) 1);}}}}close(lfd);close(epfd);return 0;
}2.5 epoll的工作模式
epoll 有两种工作模式LT水平触发模式和 ET边沿触发模式。在 LT 模式中当内核检测到文件描述符fd的读缓冲区中有数据时会通知用户。如果用户没有读取数据数据会一直保留在缓冲区中epoll 会持续通知用户。即使用户只读取了一部分数据epoll 也会继续通知直到缓冲区的数据被完全读走。LT 模式同时支持阻塞block和非阻塞non-block的 socket它是一种缺省的工作方式内核会持续告知用户文件描述符是否就绪并允许用户对这个就绪的 fd 进行 I/O 操作。如果用户不进行任何操作内核会继续发送通知。
相比之下ET 模式是一种高速工作方式仅支持非阻塞 socket。在这种模式下内核仅在文件描述符从未就绪变为就绪时通过 epoll 通知用户一次。一旦通知内核会假定用户知道文件描述符已经就绪并且不会再为该文件描述符发送更多的就绪通知除非用户执行了某些操作导致文件描述符不再处于就绪状态。在 ET 模式中如果用户不对 fd 执行 I/O 操作从而使得它再次变为未就绪状态内核不会再次发送通知。这种模式显著减少了 epoll 事件被重复触发的次数因此比 LT 模式更高效。在 ET 模式下工作时必须使用非阻塞套接字以避免由于单个文件句柄的阻塞读/写操作导致处理多个文件描述符的任务饿死。
需要特别注意的是ET模式中如果用户不读数据数据一直在缓冲区中epoll下次检测的时候就不会再通知了。
如果使用了ET模式那么在监听到有客户端连接之后对cfd的属性需要设置非阻塞。