• lv8 嵌入式开发-网络编程开发 16 多路复用poll函数


    目录

    1 多路复用的多种实现方式

    2 poll 

    2.1 poll 函数应用

    3 epoll 函数族(效率最高)

    3.1 epoll_create 创建epoll句柄

    3.2 epoll_ctl epoll句柄控制接口

    3.3 epoll_wait 等待 epoll 文件描述符上的 I/O 事件 

    3.4 epoll 函数应用


    1 多路复用的多种实现方式

    2 poll 

    1. int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    2. struct pollfd {
    3. int d; /* 文件描述符 */
    4. short events; /* 请求的事件 */
    5. short revents; /* 返回的事件 */
    6. };

     poll() 函数是一个系统调用,用于监视一组文件描述符,等待其中的一个或多个文件描述符变为就绪状态,从而进行读写操作。它与 select() 类似,但 poll() 没有最大文件描述符数量的限制,并且更加高效。以下是参数的含义:

    • fds:指向 pollfd 结构体数组的指针,其中存放了需要监视的文件描述符和每个文件描述符所关注的事件。
    • nfds:文件描述符的数量。
    • timeout:超时时间(单位是毫秒),如果为 0 表示立即返回,如果为 -1 表示永远等待。

    poll() 的返回值表示就绪的文件描述符数量,如果返回值为 0,则表示超时。如果返回值为 -1,则表示发生错误,此时可以使用 errno 查看具体的错误信息。

    事件类型 events:

    •         POLLIN:有数据可读
    •         POLLPRI:有紧急数据需要读取
    •         POLLOUT: 文件可写
    •          .....

    2.1 poll 函数应用

    server.c

    1. #include "net.h"
    2. #include
    3. #define MAX_SOCK_FD 1024
    4. int main(int argc, char *argv[])
    5. {
    6. int i, j, fd, newfd;
    7. nfds_t nfds = 1;
    8. struct pollfd fds[MAX_SOCK_FD] = {};
    9. Addr_in addr;
    10. socklen_t addrlen = sizeof(Addr_in);
    11. /*检查参数,小于3个 直接退出进程*/
    12. Argment(argc, argv);
    13. /*创建已设置监听模式的套接字*/
    14. fd = CreateSocket(argv);
    15. fds[0].fd = fd;
    16. fds[0].events = POLLIN;
    17. while(1){
    18. if( poll(fds, nfds, -1) < 0)
    19. ErrExit("poll");
    20. for(i = 0; i < nfds; i++){
    21. /*接收客户端连接,并生成新的文件描述符*/
    22. if(fds[i].fd == fd && fds[i].revents & POLLIN){ //判断服务端的fd是否有数据到来即第0个,并且文件描述符是数据可读,那么再执行把客户端接进来
    23. if( (newfd = accept(fd, (Addr *)&addr, &addrlen) ) < 0)
    24. perror("accept");
    25. fds[nfds].fd = newfd; //加入检查列表
    26. fds[nfds++].events = POLLIN;
    27. printf("[%s:%d][nfds=%lu] connection successful.\n",
    28. inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), nfds);
    29. }
    30. /*处理客户端数据*/
    31. if(i > 0 && fds[i].revents & POLLIN){ //服务端不算,需要1开始
    32. if(DataHandle(fds[i].fd) <= 0){
    33. if( getpeername(fds[i].fd, (Addr *)&addr, &addrlen) < 0)
    34. perror("getpeername");
    35. printf("[%s:%d][fd=%d] exited.\n",
    36. inet_ntoa(addr.sin_addr), ntohs(addr.sin_port), fds[i].fd);
    37. close(fds[i].fd);
    38. for(j=i; j-1; j++) //删除检查的目标,等于把后面的数组往前挪1
    39. fds[j] = fds[j+1];
    40. nfds--;
    41. i--;
    42. }
    43. }
    44. }
    45. }
    46. close(fd);
    47. return 0;
    48. }

    socket.c

    1. #include "net.h"
    2. void Argment(int argc, char *argv[]){
    3. if(argc < 3){
    4. fprintf(stderr, "%s\n", argv[0]);
    5. exit(0);
    6. }
    7. }
    8. int CreateSocket(char *argv[]){
    9. /*创建套接字*/
    10. int fd = socket(AF_INET, SOCK_STREAM, 0);
    11. if(fd < 0)
    12. ErrExit("socket");
    13. /*允许地址快速重用*/
    14. int flag = 1;
    15. if( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )
    16. perror("setsockopt");
    17. /*设置通信结构体*/
    18. Addr_in addr;
    19. bzero(&addr, sizeof(addr) );
    20. addr.sin_family = AF_INET;
    21. addr.sin_port = htons( atoi(argv[2]) );
    22. /*绑定通信结构体*/
    23. if( bind(fd, (Addr *)&addr, sizeof(Addr_in) ) )
    24. ErrExit("bind");
    25. /*设置套接字为监听模式*/
    26. if( listen(fd, BACKLOG) )
    27. ErrExit("listen");
    28. return fd;
    29. }
    30. int DataHandle(int fd){
    31. char buf[BUFSIZ] = {};
    32. Addr_in peeraddr;
    33. socklen_t peerlen = sizeof(Addr_in);
    34. if( getpeername(fd, (Addr *)&peeraddr, &peerlen) )
    35. perror("getpeername");
    36. int ret = recv(fd, buf, BUFSIZ, 0);
    37. if(ret < 0)
    38. perror("recv");
    39. if(ret > 0){
    40. printf("[%s:%d]data: %s\n",
    41. inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);
    42. }
    43. return ret;
    44. }

    net.h

    1. #ifndef _NET_H_
    2. #define _NET_H_
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. typedef struct sockaddr Addr;
    13. typedef struct sockaddr_in Addr_in;
    14. #define BACKLOG 5
    15. #define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)
    16. void Argment(int argc, char *argv[]);
    17. int CreateSocket(char *argv[]);
    18. int DataHandle(int fd);
    19. #endif

    3 epoll 函数族(效率最高)

    1. /*创建epoll句柄*/
    2. int epoll_create(int size); //size参数实际上已经被弃用
    3. /*epoll句柄的控制接口*/
    4. int epoll_ctl(int epfd, int op, int fd,
    5. struct epoll_event *event);
    6. /*等待 epoll 文件描述符上的 I/O 事件*/
    7. int epoll_wait(int epfd, struct epoll_event *events,
    8. int maxevents, int timeout);

    3.1 epoll_create 创建epoll句柄

    int epoll_create(int size);  //size参数实际上已经被弃用
    

    epoll_create() 函数用于创建一个 epoll 实例,并返回一个与该实例相关联的文件描述符。以下是参数的含义:

    • size:已经被弃用,原本用于指定 epoll 实例的大小,但在内核版本2.6.8之后不再使用。

    epoll_create() 的返回值为一个非负整数,表示与 epoll 实例相关联的文件描述符。如果返回值为 -1,则表示创建失败,此时可以使用 errno 查看具体的错误信息。

    调用成功后,可以使用返回的文件描述符进行后续的操作,如注册、修改、等待文件描述符上的事件等。

    3.2 epoll_ctl epoll句柄控制接口

    1. int epoll_ctl(int epfd, int op, int fd,
    2. struct epoll_event *event);

    epoll_ctl() 函数用于向 epoll 实例中注册、修改或删除文件描述符的事件。它是 epoll 系统调用的控制接口。以下是参数的含义:

    • epfd:epoll 实例的文件描述符,通过 epoll_create() 创建。
    • op:操作类型,可以是以下几种值之一:
      • EPOLL_CTL_ADD:将文件描述符 fd 添加到 epoll 实例中,并关联一个事件结构。
      • EPOLL_CTL_MOD:修改文件描述符 fd 在 epoll 实例中关联的事件结构。
      • EPOLL_CTL_DEL:从 epoll 实例中移除文件描述符 fd
    • fd:需要注册、修改或删除的文件描述符。
    • event:指向 struct epoll_event 结构体的指针,用于指定文件描述符关心的事件类型。

    epoll_ctl() 的返回值为 0 表示操作成功,-1 表示操作失败,此时可以使用 errno 查看具体的错误信息。

    epoll_event结构体

    1. typedef union epoll_data {
    2. void *ptr;
    3. int fd;
    4. __uint32_t u32;
    5. __uint64_t u64;
    6. } epoll_data_t;
    7. struct epoll_event {
    8. __uint32_t events; /* Epoll events 用来描述文件描述符上发生的事件*/
    9. epoll_data_t data; /* User data variable 可以用来存储与该事件相关的用户数据信息。*/
    10. };
    • EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);
    • EPOLLOUT:表示对应的文件描述符可以写;
    • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    • EPOLLERR:表示对应的文件描述符发生错误;
    • EPOLLHUP:表示对应的文件描述符被挂断;
    • EPOLLET :将 EPOLL 设为边缘触发(Edge Trigger)模式,这是相对于水平触发(Level Trigger)来说的。
    • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

    3.3 epoll_wait 等待 epoll 文件描述符上的 I/O 事件 

    1. int epoll_wait(int epfd, struct epoll_event *events,
    2. int maxevents, int timeout);

    epoll_wait() 函数用于等待 epoll 实例中的文件描述符上发生事件,它会一直阻塞直到至少有一个文件描述符发生就绪事件。以下是参数的含义:

    • epfd:epoll 实例的文件描述符,通过 epoll_create() 创建。
    • events:指向 struct epoll_event 结构体数组的指针,用于返回触发事件的文件描述符信息。
    • maxevents:表示 events 数组的最大大小,即最多可以返回多少个事件。
    • timeout:超时时间(单位是毫秒),如果为 0 表示立即返回,如果为 -1 表示永远等待。

    epoll_wait() 的返回值表示就绪的文件描述符数量,如果返回值为 0,则表示超时。如果返回值为 -1,则表示发生错误,此时可以使用 errno 查看具体的错误信息。在成功返回时,events 数组被填充了触发事件的文件描述符信息。

    3.4 epoll 函数应用

    server.c

    1. #include "net.h"
    2. #include
    3. #define MAX_SOCK_FD 1024
    4. int main(int argc, char *argv[])
    5. {
    6. int i, nfds, fd, epfd, newfd;
    7. Addr_in addr;
    8. socklen_t addrlen = sizeof(Addr_in);
    9. struct epoll_event tmp, events[MAX_SOCK_FD] = {};
    10. /*检查参数,小于3个 直接退出进程*/
    11. Argment(argc, argv);
    12. /*创建已设置监听模式的套接字*/
    13. fd = CreateSocket(argv);
    14. if( (epfd = epoll_create(1)) < 0) //参数1是无意义得
    15. ErrExit("epoll_create");
    16. tmp.events = EPOLLIN;
    17. tmp.data.fd = fd;
    18. if( epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &tmp) )
    19. ErrExit("epoll_ctl");
    20. while(1) {
    21. if( (nfds = epoll_wait(epfd, events, MAX_SOCK_FD, -1) ) < 0)
    22. ErrExit("epoll_wait");
    23. printf("nfds = %d\n", nfds);
    24. for(i = 0; i < nfds; i++) {
    25. if(events[i].data.fd == fd){
    26. /*接收客户端连接,并生成新的文件描述符*/
    27. if( (newfd = accept(fd, (Addr *)&addr, &addrlen) ) < 0)
    28. perror("accept");
    29. printf("[%s:%d] connection.\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port) );
    30. tmp.events = EPOLLIN;
    31. tmp.data.fd = newfd;
    32. if( epoll_ctl(epfd, EPOLL_CTL_ADD, newfd, &tmp) )
    33. ErrExit("epoll_ctl");
    34. }else{/*处理客户端数据*/
    35. if(DataHandle(events[i].data.fd) <= 0){
    36. if( epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL) )
    37. ErrExit("epoll_ctl");
    38. if( getpeername(events[i].data.fd, (Addr *)&addr, &addrlen) )
    39. perror("getpeername");
    40. printf("[%s:%d] exited.\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port) );
    41. close(events[i].data.fd);
    42. }
    43. }
    44. }
    45. }
    46. close(epfd);
    47. close(fd);
    48. return 0;
    49. }

    socket.c

    1. #include "net.h"
    2. void Argment(int argc, char *argv[]){
    3. if(argc < 3){
    4. fprintf(stderr, "%s\n", argv[0]);
    5. exit(0);
    6. }
    7. }
    8. int CreateSocket(char *argv[]){
    9. /*创建套接字*/
    10. int fd = socket(AF_INET, SOCK_STREAM, 0);
    11. if(fd < 0)
    12. ErrExit("socket");
    13. /*允许地址快速重用*/
    14. int flag = 1;
    15. if( setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag) ) )
    16. perror("setsockopt");
    17. /*设置通信结构体*/
    18. Addr_in addr;
    19. bzero(&addr, sizeof(addr) );
    20. addr.sin_family = AF_INET;
    21. addr.sin_port = htons( atoi(argv[2]) );
    22. /*绑定通信结构体*/
    23. if( bind(fd, (Addr *)&addr, sizeof(Addr_in) ) )
    24. ErrExit("bind");
    25. /*设置套接字为监听模式*/
    26. if( listen(fd, BACKLOG) )
    27. ErrExit("listen");
    28. return fd;
    29. }
    30. int DataHandle(int fd){
    31. char buf[BUFSIZ] = {};
    32. Addr_in peeraddr;
    33. socklen_t peerlen = sizeof(Addr_in);
    34. if( getpeername(fd, (Addr *)&peeraddr, &peerlen) )
    35. perror("getpeername");
    36. int ret = recv(fd, buf, BUFSIZ, 0);
    37. if(ret < 0)
    38. perror("recv");
    39. if(ret > 0){
    40. printf("[%s:%d]data: %s\n",
    41. inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port), buf);
    42. }
    43. return ret;
    44. }

    net.h

    1. #ifndef _NET_H_
    2. #define _NET_H_
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include
    11. #include
    12. typedef struct sockaddr Addr;
    13. typedef struct sockaddr_in Addr_in;
    14. #define BACKLOG 5
    15. #define ErrExit(msg) do { perror(msg); exit(EXIT_FAILURE); } while(0)
    16. void Argment(int argc, char *argv[]);
    17. int CreateSocket(char *argv[]);
    18. int DataHandle(int fd);
    19. #endif

    4 练习

    总结select,poll和epoll的优缺点

    select:

    优点:

    1. 可以处理的文件描述符数量没有限制,适用于低并发量的情况。
    2. 在一些平台上(如Windows),只有select是唯一可用的多路复用机制。

    缺点:

    1. select每次调用都需要将所有文件描述符从用户态到内核态的拷贝,性能较差。
    2. 在高并发量情况下,每次检查都需要轮询整个fd_set,占用大量系统资源。
    3. 不能监听文件描述符所在的位置是否被修改。

    poll:

    优点:

    1. 可以处理的文件描述符数量没有限制,适用于低并发量的情况。
    2. 相比select,poll不需要拷贝文件描述符,性能较好。

    缺点:

    1. 在高并发量情况下,每次检查都需要轮询整个pollfd数组,占用大量系统资源。
    2. 不能监听文件描述符所在的位置是否被修改。

    epoll:

    优点:

    1. 支持ET(边缘触发)模式,效率更高。
    2. 不会随着监听文件描述符的增加而变慢,适用于高并发量的情况。
    3. 支持文件描述符所在位置是否被修改的检测。

    缺点:

    1. 可以处理的文件描述符数量有限,但通常数量较大,一般不会对使用造成太大影响。

    综上所述,epoll是更为高效、灵活的I/O多路复用机制,可以更好地适应高并发量的场景。而select和poll则适用于低并发量的情况。

  • 相关阅读:
    SpringSecurity Oauth2实战 - 08 SpEL权限表达式源码分析及两种权限控制方式原理
    使用 OpenTelemetry 构建 .NET 应用可观测性(3):.NET SDK 概览
    SpringMVC数据格式化
    理解Lua中“元表和元方法“
    blender快捷键
    linux内核的reciprocal_value结构体
    7年阿里测试经验之谈 —— 用UI自动化测试实现元素定位
    Google codelab WebGPU入门教程源码<4> - 使用Uniform类型对象给着色器传数据(源码)
    (更新中)数据结构开发实战教程
    el-table :span-method合并单元格
  • 原文地址:https://blog.csdn.net/m0_60718520/article/details/133715964