Gitlib Gitlib
首页
  • 分类
  • 标签
  • 归档
  • Golang开发实践万字总结
  • MySQL核心知识汇总
  • Redis实践总结
  • MQ实践万字总结
  • Docker数据持久化总结
  • Docker网络模式深度解读
  • 常用游戏反外挂技术总结
  • 读书笔记
  • 心情杂货
  • 行业杂谈
  • 友情链接
关于我
GitHub (opens new window)

Ravior

以梦为马,莫负韶华
首页
  • 分类
  • 标签
  • 归档
  • Golang开发实践万字总结
  • MySQL核心知识汇总
  • Redis实践总结
  • MQ实践万字总结
  • Docker数据持久化总结
  • Docker网络模式深度解读
  • 常用游戏反外挂技术总结
  • 读书笔记
  • 心情杂货
  • 行业杂谈
  • 友情链接
关于我
GitHub (opens new window)
  • 操作系统

  • 计算机网络

    • 深入理解IO模型
    • DDOS攻击下多IP防御思路
    • 从TCP/IP协议谈Linux内核参数优化
    • IO多路复用之select、poll、epoll详解
    • 数据结构和算法

    • MySQL

    • Redis

    • Nginx

    • MongoDB

    • 其他

    • 计算机基础
    • 计算机网络
    Ravior
    2019-07-26
    目录

    IO多路复用之select、poll、epoll详解

    select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

    # select

    监视多个文件句柄的状态变化,程序会阻塞在select处等待,直到有文件描述符就绪或超时。

    int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    
    1

    select 函数监视的文件描述符分3类,writefds(写状态), readfds(读状态), exceptfds(异常状态)。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可)。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

    #include <stdio.h>
    #include <unistd.h>
    #include <sys/socket.h>
    #include <sys/select.h>
    #include <stdlib.h>
    #include <arpa/inet.h>
    #include <ctype.h>
    #include <string.h>
    
    #define SERVER_IP "127.0.0.1"
    #define SERVER_PORT 6666
    
    void doSocketSelect()
    {
        
        int ret = 0;
        struct sockaddr_in server_addr, client_addr;
    
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(SERVER_PORT);
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        int server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (server_sockfd < 0) {
            perror("Fail to create listen socket\n");
            goto failed;
        } else {
            printf("server socket created, fd: %d\n", server_sockfd);
        }
    
        ret = bind(server_sockfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
        if (ret != 0) {
            perror("Fail to bind socket\n");
            goto failed;
        }
        
        ret = listen(server_sockfd, 128);
    
        if (ret != 0) {
            perror("Fail to listen socket\n");
            goto failed;
        }
    
        printf("开启监听:%s:%d ....\n", SERVER_IP, SERVER_PORT);
    
        fd_set read_fds; // 读文件操作符
    
        while(1) {
            printf("服务器等待连接\n");
            // 清理句柄集合
            FD_ZERO(&read_fds);
            // 将监听socket放入集合
            FD_SET(server_sockfd, &read_fds);
    
            ret = select(FD_SETSIZE, &read_fds, NULL, NULL, NULL);
            printf("select ret:%d\n", ret);
    
            if (ret < 0) {
                perror("Fail to select!\n");
            } else if (ret == 0) {
                perror("Select timeout!!!\n");
                continue;
            } else {
                if (FD_ISSET(server_sockfd, &read_fds)) {
                    int client_len = sizeof(client_addr);
                    int client_sockfd = accept(server_sockfd, (struct sockaddr *) &client_addr, (socklen_t *) &client_len);
                    if (client_sockfd > 0) {
                        printf("有新客户端连接 fd: %d, %s:%d\n",client_sockfd, inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
                        char w[] = "welcome!\n";
                        write(client_sockfd, w, sizeof(w));
                        // 关闭客户端连接
                        // close(client_sockfd);
                    } else {
                        printf("accept error");
                    }
                } else {
                    
                }
            }
    
        }
    
        failed:
        close(server_sockfd);
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86

    缺陷:

    • 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024);
    • 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
    • select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

    以select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

    # poll

    与select轮询所有待监听的描述符机制类似,但poll使用pollfd结构表示要监听的描述符,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他select的缺点依然存在

    int poll (struct pollfd *fds, unsigned int nfds, int timeout);
    
    1

    pollfd结构:

    struct pollfd {
        int fd; /* file descriptor */
        short events; /* requested events to watch */
        short revents; /* returned events witnessed */
    };
    
    1
    2
    3
    4
    5

    pollfd结构包括了events(要监听的事件)和revents(实际发生的事件)。而且也需要在函数返回后遍历pollfd来获取就绪的描述符。

    # epoll

    epoll的实现机制与select/poll机制完全不同,相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

    epoll在Linux内核中申请了一个简易的文件系统,通过三个函数epoll_create、epoll_ctl, epoll_wait实现调度:

    int epoll_create(int size);
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    
    1
    2
    3

    调用过程如下:

    • 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源
    • 调用epoll_ctl向epoll对象中添加连接的套接字
    • 调用epoll_wait收集发生的事件的连接

    当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关,eventpoll结构体如下所示:

    IO

    每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

    而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

    在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示: IO

    当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

    IO

    通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

    如此一来,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制所有连接的句柄数据,内核也不需要去遍历全部的连接。

    #计算机网络
    上次更新: 2022/12/01, 11:09:34
    从TCP/IP协议谈Linux内核参数优化
    数据结构之单向链表

    ← 从TCP/IP协议谈Linux内核参数优化 数据结构之单向链表→

    最近更新
    01
    常用游戏反外挂技术总结
    11-27
    02
    Golang开发实践万字总结
    11-11
    03
    Redis万字总结
    10-30
    更多文章>
    Theme by Vdoing | Copyright © 2011-2022 Ravior | 粤ICP备17060229号-3 | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式