Linux学习之高级IO:I/O多路转接之poll(认识)与epoll(重点) + 非阻塞读取

poll函数(认识)

在这里插入图片描述

events和revents的取值:

在这里插入图片描述

poll的优点(poll不用位图,select用的是位图)

不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现.
1.pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select更方便
2.poll并没有最大数量限制 (但是数量过大后性能也是会下降).

poll的缺点

poll中监听的文件描述符数目增多时
1.和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符.
2.每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中.
3.同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

poll示例: 使用poll监控标准输入

#include <iostream>
#include <unistd.h>
#include <poll.h>

int main()
{
	//如果关心多个可以设置成数组即rfds[128]
    struct pollfd rfds;
    rfds.fd = 0;//我想关心0号文件描述符
    rfds.events = POLLIN;//我想关心0号文件描述符的写入事件
    rfds.revents = 0;//初始化

    while (true)
    {
        int n = poll(&rfds, 1, -1);
        switch (n)
        {
        case 0:
            std::cout << "time out ..." << std::endl;
            break;
        case -1:
            std::cerr << "poll error" << std::endl;
            break;
        default:
            std::cout << "有事件发生..." << std::endl;
            if(rfds.revents & POLLIN)
            {
                std::cout << rfds.fd << " 上面的读事件发生了" << std::endl;
                char buffer[128];
                ssize_t s = read(0, buffer, sizeof(buffer)-1);
                if(s > 0)
                {
                    std::cout << "有人说# " << buffer << std::endl;
                }
            }
            break;
        }
    }

    return 0;
}

epoll(重点!)

按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法

epoll相关系统调用

epoll_create

在这里插入图片描述

epoll_ctl

在这里插入图片描述
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL队列里

epoll_wait

在这里插入图片描述

epoll工作原理(配合两张图看)

在这里插入图片描述
在这里插入图片描述

总结,创建epoll模型就是创建三个东西:红黑树,就绪队列,回调机制 。epoll使用过程就是下面三部曲:

调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪

epoll示例: 使用epoll监控listen套接字

epoll_server.cpp

#include <iostream>
using namespace std;
#include <string>
#include <sys/epoll.h>
#include "Sock.hpp"

#define SIZE 128
#define NUM 64
void Usage()
{
    cout << "输入格式应该为:"
         << "./epoll_server + port" << endl;
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage();
        exit(1);
    }
    // 1.建立TCP,监听socket
    uint16_t port = atoi(argv[1]);
    int listen_sock = Sock::Socket();
    Sock::Bind(listen_sock, port);
    Sock::Listen(listen_sock);

    // 2.创建epoll模型,获得epoll文件描述符
    int epfd = epoll_create(SIZE);
    cout<<"listen_sock: "<<listen_sock<<" epfd : "<<epfd<<endl;

    // 3.先将listen_sock和它所关心的事件,添加到内核
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = listen_sock;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);

    // 4.事件循环
    struct epoll_event revs[NUM];
    while (true)
    {
        int timeout = 1000;
        //这里传入的数组,仅仅是尝试从内核中拿回来已经就绪的事件
        int n = epoll_wait(epfd, revs, NUM, timeout);
        switch (n)
        {
        case 0:
        cout<<"timeout ..."<<endl;
            break;
        case -1:
            cerr<<"epoll error"<<endl;
            break;
        default:
            cout<<"有事件就绪啦"<<endl;
            //5.处理就绪事件
            for(int i = 0 ; i < n ; i++)
            {
                int sock = revs[i].data.fd;
                if(revs[i].events & EPOLLIN)
                {
                    std::cout << "文件描述符: " << sock << " 读事件就绪" << std::endl;
                    //5.1处理连接事件
                    if(sock == listen_sock)
                    {
                        std::cout << "文件描述符: " << sock << " 链接数据就绪" << std::endl;
                        int fd = Sock::Accept(sock);
                        if(fd >= 0)
                        {
                            std::cout << "获取新链接成功啦: " << fd << std::endl;
                            //能不能立即读取呢??不能!
                            //因为有链接并不代表有数据,如果没有数据就读的话,就会被阻塞挂起
                            struct epoll_event _ev;
                            _ev.data.fd = fd;
                            _ev.events = EPOLLIN;
                            epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &_ev);//将新连接插入epoll维护的红黑树中
                            std::cout << "已经将" << fd << " 托管给epoll啦" << std::endl;
                        }
                        else{
                            //do nothing
                        }
                    }
                    else 
                    {
                        // 5.2 正常的读取处理
                        std::cout << "文件描述符: " << sock << "正常数据就绪" << std::endl;
                        char buffer[3] = {0};
                        int s = recv(sock, buffer, sizeof(buffer)-1, 0);//0表示阻塞等待,但走到这里一定有数据可读,不会阻塞
                        if(s > 0)
                        {
                            buffer[s] = 0;
                            std::cout << "client [" << sock << "]# " << buffer << std::endl;
                        }
                        else if(s == 0)
                        {
                            close(sock);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
                            std::cout << "sock: " << sock << "delete from epoll success" << std::endl;
                        }
                        else
                        {
                            //读取失败
                            std::cout << "recv error" << std::endl;
                            close(sock);
                            epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);
                            std::cout << "sock: " << sock << "delete from epoll success" << std::endl;
                        }
                    }

                }
                else if(revs[i].events & EPOLLOUT)
                {
                    //处理写事件
                }
                else
                {

                }
            }
            break;
        }
    }

    close(epfd);
    close(listen_sock);
    return 0;
}

Sock.cpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;

class Sock
{
public:
    static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
            cerr << "socket error" << endl;
            exit(2);
        }
        int opt = 1;
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        return sock;
    }

    static void Bind(int sock, uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            cerr << "bind error!" << endl;
            exit(3);
        }
    }

    static void Listen(int sock)
    {
        if (listen(sock, 5) < 0)
        {
            cerr << "listen error !" << endl;
            exit(4);
        }
    }

    static int Accept(int sock)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(sock, (struct sockaddr *)&peer, &len);
        if(fd >= 0){
            return fd;
        }
        return -1;
    }

    static void Connect(int sock, std::string ip, uint16_t port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));

        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        server.sin_addr.s_addr = inet_addr(ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
        {
            cout << "Connect Success!" << endl;
        }
        else
        {
            cout << "Connect Failed!" << endl;
            exit(5);
        }
    }
};

运行结果:
在这里插入图片描述

epoll的优点(和 select 的缺点对应)

1.接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
2.数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
3.事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
4.没有数量限制: 文件描述符数目无上限.

select和epoll的应用场景(重点!)

都知道epoll相对于select模型的优点,它的速度和并发量相对于select明显的优势,但是是不是epoll就可以完全代替select呢,在得出结论之前还是先要看看他们各自的实现原理。

1.select:在网络编程中统一的操作顺序是创建socket->绑定端口->监听->accept->write/read,当有客户端连接到来时,select会把该连接的文件描述符放到fd_set(一组文件描述符(fd)的集合),然后select会循环遍历它所监测的fd_set内的所有文件描述符,当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,fd_set是一个类似于数组的数据结构,由于它每次都要遍历整个数组,所有她的效率会随着文件描述符的数量增多而明显的变慢,除此之外在每次遍历这些描述符之前,系统还需要把这些描述符集合从内核copy到用户空间,然后再copy回去,如果此时没有一个描述符有事件发生(例如:read和write)这些copy操作和便利操作都是无用功,可见slect随着连接数量的增多,效率大大降低。可见如果在高并发的场景下select并不适用,况且select默认的最大描述符为1024,如果想要更多还要做响应参数的配置。

2.epoll:说到epoll都夸赞它的效率和并发量,那么她好在哪里呢。首先调用epoll_create时内核帮我们在epoll文件系统里建了个file结点;除此之外在内核cache里建立红黑树用于存储以后epoll_ctl传来的socket,当有新的socket连接来时,先遍历红黑书中有没有这个socket存在,如果有就立即返回,没有就插入红黑数,然后给内核中断处理程序注册一个回调函数,每当有事件发生时就通过回调函数把这些文件描述符放到事先准备好的用来存储就绪事件的链表中,调用epoll_wait时,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后检查这些socket。在LT模式下,如果这些socket上确实有未处理的事件时,该句柄会再次被放回到刚刚清空的准备就绪链表,保证所有的事件都得到正确的处理。如果到timeout时间后链表中没有数据也立刻返回。因此在并发需求量高的场景中我们即使要监控数百万计的句柄,大多数一次也只返回很少量的准备就绪句柄。由此可见epoll仅需要从内核态copy少量的句柄到用户态,这样就避免了select模型中的无效便利和用户和内核之间的copy操作。

说道这里,可以看到epoll的优势非常明显,几乎没有描述符数量的限制,并发支持完美,不会随着socket的增加而降低效率,也不用在内核空间和用户空间之间做无效的copy操作。 但是是不是所有的场景都适合epoll呢?看下面的例子。

一个游戏服务器,tcp server负责接收客户端的连接,dbserver负责处理数据信息,一个webserver负责处理服务器的web请求,gameserver负责游戏的逻辑处理,所有这些服务都和另外一个gateserver相连,gateserver负责服务器间的通信和转发(进程间通信),只要游戏服务器在服务状态,这些连接几乎不会断开(异常情况可能会断开),并且这些连接数量一般不会很多。这种情况,select还是epoll呢?很明显是select,因为每时每刻这些连接的socket都有事件发生(比如:服务期间的心跳信息,还有大型网络游戏的同步信息(一般每秒在20-30次)),最重要的是,这种场景下,并发量也不会很大。如果此时用epoll,为此所建立的文件系统,红黑书和链表对于此来说就是杀鸡用牛刀,效率反而不高。当然这里的tcp server负责大量的客户端的连接,毫无疑问epoll是首选,它接受大量的客户端连接,收到客户端的消息之后把消息转发发给select网络模型的gateserver,gateserver再转发给gameserver进行逻辑处理,最后返回给客户端就over了。因此在如果在并发量低,socket都比较活跃的情况下,select就不见得比epoll慢了(就像我们常常说快排比插入排序快,但是在特定情况下这并不成立)。

总结:epoll适用于大型并发,比如大量的客户端连接。select适用于连接数少且连接基本不会断开的情况,如服务器间通信

水平触发LT(Level Triggered)与边缘触发ET(发Edge Triggered)工作模式

LT(总结:直到用户把数据全部取走才停止通知)

epoll默认状态下就是LT工作模式.
当epoll检测到socket上事件就绪的时候, 可以不立刻进行处理. 或者只处理一部分.
如上面的例子, 由于只读了1K数据, 缓冲区中还剩1K数据, 在第二次调用 epoll_wait 时, epoll_wait仍然会立刻返回并通知socket读事件就绪.直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回. 支持阻塞读写和非阻塞读写

ET(总结:只有新增的时候提醒一次,之后不再提醒)必须搭配非阻塞读写才行!!!

如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式.
当epoll检测到socket上事件就绪时, 必须立刻处理.
如上面的例子, 虽然只读了1K的数据, 缓冲区还剩1K的数据, 在第二次调用 epoll_wait 的时候, epoll_wait 不会再返回了.也就是说, ET模式下, 文件描述符上的事件就绪后, 只有一次处理机会. ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll.

只支持非阻塞的读写

对比LT和ET

1.LT是 epoll 的默认行为. 使用 ET 能够减少 epoll 触发的次数. 但是代价就是强逼着程序猿一次响应就绪过程中就把所有的数据都处理完.
2.相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比 LT 更高效一些. 但是在 LT 情况下如果也能做到
每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的.
3.另一方面, ET 的代码复杂程度更高了

为什么ET模式下的fd,必须是非阻塞的原因!!!(重要的一批)

在这里插入图片描述

理解ET模式和非阻塞文件描述符

使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 "工程实践"上的要求.(意思就是想用ET要搭配非阻塞才有效)
假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求.

在这里插入图片描述
如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来,
参考 man 手册的说明, 可能被信号打断), 剩下的9k数据就会待在缓冲区中
在这里插入图片描述
在这里插入图片描述
所以, 为了解决上述问题(阻塞read不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,
保证一定能把完整的请求都读出来.
而如果是LT没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪

设置非阻塞

在这里插入图片描述

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

void SetNonBlock(int fd)
{
    //获取当前fd的标志位
    int fl = fcntl(fd, F_GETFL);
    if(fl < 0)
    {
        perror("fcntl");
        return;
    }
    //将标志位设置为非阻塞
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}


int main()
{
    SetNonBlock(0);
    while(1){
        //如过读成功,则errno = 0,如果失败errno = 11
        errno = 0;
        char buffer[10];
        //重点是read
        ssize_t s = read(0, buffer, sizeof(buffer)-1);
        if(s > 0)
        {
            buffer[s] = 0;
            write(1, buffer, strlen(buffer));
            printf("read success, s: %d, errno: %d\n", s, errno);
        }
        else{
            //对非阻塞socket而言,EAGAIN不是一种错误。在VxWorks和Windows上,EAGAIN的名字叫做EWOULDBLOCK
            if(errno == EAGAIN || errno == EWOULDBLOCK) 
            {
                printf("数据没有准备好,再试试吧!\n");
                printf("read failed, s: %d, errno: %d\n", s, errno);
                //做做其他事情
                sleep(3);
                continue;
            }
        }

    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值