Linux IO多路复用:select poll epoll

目录

1.先说说什么是IO多路复用

2.select

3.poll

4.epoll

5.总结


1.先说说什么是IO多路复用

IO多路复用指的是除了多进程、多线程以外用来解决并发的手段。

以网络IO为例子:当大量网络链接建立时,不需要创建多线程来消耗大量系统资源。

以一个进程作为入口,用一个监视器(select poll epoll)来监视链接,当链接发来数据时,select会发生响应,没有数据时我们可以进行其他正常操作。

这样,我们就节省了等待的时间,当发生IO时就立即处理。

那么IO多路复用的使用场景也就很明确了:可以维护大量IO,但同时IO不能过于密集


2.select

函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,

struct timeval *timeout);

fd_set是一个位图结构,用位来标识要监视的fd。

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位

int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真

void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位

void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

nfds表示最大的fd,readfds表示需要关注读的fd,writedfs表示需要关注写的fd。

exceptfds表示出错的fd,timeout用来定时,超时返回0。

错误返回-1,正确返回改变的fd个数

来段使用select思想的socket网络代码:

#pragma once

#include "sock.hpp"
#include "log.hpp"

#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>

using namespace std;

#define BITS 8
#define NUM (sizeof(fd_set) * BITS)
#define FD_NONE -1

class SelectServer
{
public:
    SelectServer(uint16_t port = 8080)
    {
        _port = port;
        //socket套接字编程,直接调的系统接口
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        //需要使用一个数组来存储需要关注的fd
        for (int i = 0; i < NUM; i++)
            _fd_set_array[i] = FD_NONE;
        //listensock存在0下标
        _fd_set_array[0] = _listensock;
    }
    ~SelectServer()
    {
        if (_listensock >= 0)
            close(_listensock);
    }
    void Start()
    {
        while (true)
        {
            //只关心读
            fd_set rfds;             // fd集合
            FD_ZERO(&rfds);          // 先初始化

            int maxfd = _listensock; // 找最大fd
            for (int i = 0; i < NUM; i++)
            {
                if (_fd_set_array[i] == FD_NONE)
                    continue;
                //非空就进行设置
                FD_SET(_fd_set_array[i], &rfds);
                //找到最大fd
                if (maxfd < _fd_set_array[i])
                    maxfd = _fd_set_array[i];
            }

            //监视rfds,这里只关心读事件
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case 0:
                //日志,不用在意具体实现
                logMessage(DEBUG, "%s", "time out...");
                break;
            case -1:
                logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
                break;
            default:
                logMessage(DEBUG, "get a new link event...");
                HandEvent(rfds);
                break;
            }
        }
    }
private:
    void HandEvent(const fd_set &rfds)
    {
        for (int i = 0; i < NUM; i++)
        {
            if (_fd_set_array[i] == FD_NONE)
                continue;
            //判断rfds中是否存在_fd_set_array[i]
            if (FD_ISSET(_fd_set_array[i], &rfds))
            {
                //是listensock,表示需要accept创建接收sock
                if (_fd_set_array[i] == _listensock)
                    Accepter();
                else
                //表示接收sock
                    Recver(i);
            }
        }
    }
    void Accepter()
    {
        string clientip;
        uint16_t clientport;
        //accept接收
        int sock = Sock::Accept(_listensock, &clientip, &clientport);
        logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
        int pos = 1;
        for (; pos < NUM; pos++)
        {
            if (_fd_set_array[pos] == FD_NONE)
                break;
        }
        if (pos == NUM)
        {
            logMessage(WARNING, "%s:%d", "select server already full , close: %d", sock);
            close(sock);
        }
        else
        {
            //找到一个位置放上接收sock
            _fd_set_array[pos] = sock;
        }
    }
    void Recver(int pos)
    {
        logMessage(DEBUG, "message in, get IO event: %d", _fd_set_array[pos]);
        char buffer[1024];
        //从接收sock中接收数据 暂且不考虑粘包问题
        int s = recv(_fd_set_array[pos], buffer, sizeof(buffer) - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fd_set_array[pos], buffer);
        }
        else if (s == 0)
        {
            //读端关闭
            logMessage(DEBUG, "client[%d] quit, me too...", _fd_set_array[pos]);
            //关闭sock
            close(_fd_set_array[pos]);
            _fd_set_array[pos] = FD_NONE;
        }
        else
        {
            logMessage(WARNING, "%d sock recv error, %d : %s", _fd_set_array[pos], errno, strerror(errno));
            close(_fd_set_array[pos]);
            _fd_set_array[pos] = FD_NONE;
        }
    }

private:
    int _listensock;
    int _fd_set_array[NUM];
    uint16_t _port;
};

select的缺点:

每次使用select都需要设定fd集合,将fd集合从用户态拷贝到内核态。

需要使用一个数组来存储fd,需要频繁的修改。

fd_set大小有限,支持存储fd大小有限。


3.poll

函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);

pollfd结构:

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

fd表示需要关注的文件描述符

events表示这个描述符关注的事件,常见的有三个:

POLLIN:可读        POLLOUT:可写        POLLERR:错误

revents表示返回的标记

nfds同样表示关注的最大文件描述符

timeout表示时间限制,单位是毫秒

改写上面select版的socket通信,改成poll版

#pragma once

#include"sock.hpp"
#include"log.hpp"

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

#define FD_NONE -1

using namespace std;

class PollServer
{
public:
    const static int nfds=100; 
    const static int timeout=1000; //1秒
    PollServer(uint16_t port=8080)
    :_port(port)
    {
        _nfds=nfds;
        _timeout=timeout;
        _listensock=Sock::Socket();
        Sock::Bind(_listensock,_port);
        Sock::Listen(_listensock);
        //创建pollfd组
        _fds=new struct pollfd[_nfds];
        //初始化
        for(int i=0;i<_nfds;i++)
        {
            _fds[i].fd=FD_NONE;
            _fds[i].events=_fds[i].revents=0;
        }
        //listensock只关注读
        _fds[0].fd=_listensock;
        _fds[0].events=POLLIN;
    }
    void Start()
    {
        while(true)
        {
            int n=poll(_fds,_nfds,_timeout); 
            //帮用户检测文件描述符是否就绪
            switch(n)
            {
                case 0:
                logMessage(DEBUG, "%s", "time out...");
                break;

                case -1:
                logMessage(WARNING, "poll error: %d : %s", errno, strerror(errno));
                break;

                default:
                HandEvent();
                break;
            }
        }
    }
    void HandEvent()
    {
        for(int i=0;i<_nfds;i++)
        {
            if(_fds[i].fd == FD_NONE)
            continue;
            //只关注读
            if(_fds[i].revents & POLLIN)
            {
                //需要创建读取的sock
                if(i == 0)
                Accepter();
                else
                //从读取sock读取数据
                Recver(i);
            }
        }
    }
    void Accepter()
    {
        string clientip;
        uint16_t clientport;
        int sock=Sock::Accept(_listensock,&clientip,&clientport);
        logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
        int pos=1;
        for(;pos<_nfds;pos++)
        {
            if(_fds[pos].fd == FD_NONE)
            break;
        }
        //不够用了,可以通过增加nfds的值来搞
        if(pos == _nfds)
        {
            logMessage(WARNING, "%s:%d", "poll server already full ,close: %d", sock);
            close(sock);
        }
        else
        {
            //设置进pollfd中
            _fds[pos].fd=sock;
            //只关注读
            _fds[pos].events=POLLIN;
        }
    }
    void Recver(int pos)
    {
        char buffer[1024];
        //读取数据
        int s=recv(_fds[pos].fd,buffer,sizeof buffer-1,0);
        if(s == 0)
        {
            //读端关闭
            logMessage(DEBUG, "client[%d] quit, me too...", _fds[pos].fd);
            close(_fds[pos].fd);
            //不要忘记设置pos位置无效
            _fds[pos].fd=FD_NONE;
            _fds[pos].events=0;
        }
        else if(s < 0)
        {
            logMessage(WARNING, "%d sock recv error, %d : %s", _fds[pos].fd, errno, strerror(errno));
            _fds[pos].fd=FD_NONE;
            _fds[pos].events=0;
        }
        else
        {
            buffer[s]=0;
            logMessage(DEBUG, "client[%d]# %s", _fds[pos].fd, buffer);
        }
    }
private:
    int _listensock;
    uint16_t _port;
    struct pollfd* _fds;
    int _nfds;
    int _timeout;
};

poll相比select,编写更容易。

但是,同样需要对pollfd进行遍历,需要不断地从用户态和内核态之间转换


4.epoll

epoll相比与select和poll而言,最大的区别是不需要用户自己来维护fd数组了。底层可以通过epoll_ctl将你关心的fd都挂载到一颗红黑树上,同时设置一个回调函数,当sock对应事件发生时,回调开始。回调最终会产生一段就绪队列,就绪队列可以通过epoll_wait返回。

函数原型:

int epoll_create(int size);
创建一个epoll模型,size已经被弃用了,返回一个fd表示epoll模型

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op可以取以下值:

EPOLL_CTL_ADD:添加fd
EPOLL_CTL_MOD:修改fd
EPOLL_CTL_DEL:删除fd

epoll_event结构内容:

struct epoll_event
{
    uint32_t events; //需要关系的事件
    epoll_data_t data; //里面有fd,其他不重要
    /*****/
}

events可以取的值

*EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);

*EPOLLOUT : 表示对应的文件描述符可以写;

EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);

EPOLLERR : 表示对应的文件描述符发生错误;

EPOLLHUP : 表示对应的文件描述符被挂断;

EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.

EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里

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

这里的events是输出型参数,返回就绪队列长度。

再用epoll重写一个socket网络代码(只关注读)

epoll.hpp

#pragma once
#include<sys/epoll.h>
#include<unistd.h>
#include<iostream>

class Epoll
{
public:
    const static int g_num=256;
    static int CreateEpoll() //创建epoll模型
    {
        int epfd=epoll_create(g_num);
        if(epfd > 0)
        return epfd;
        else
        exit(1);
    } 

    //依据不同方式(传参)来实现对特定fd的特定功能的插入删除或修改
    static bool CtlEpoll(int epfd,int op,int sock,uint32_t events) 
    {
        struct epoll_event ev;
        //也需要在events中设置一下sock
        ev.data.fd=sock;
        ev.events=events;
        int n=epoll_ctl(epfd,op,sock,&ev);
        return n==0;
    }

    //OS进行等待事件发生,返回值表示n个fd准备就绪
    //同时所有结果存放至ev中,从下标0到n-1
    static int WaitEpoll(int epfd,struct epoll_event ev[],int num,int timeout)
    {
        return epoll_wait(epfd,ev,num,timeout);
    }
};

epollserver.hpp

#pragma once
#include "sock.hpp"
#include "log.hpp"
#include "epoll.hpp"

#include <unistd.h>
#include <assert.h>
#include <sys/epoll.h>
#include <functional>

using namespace std;

const static int g_num = 64;
class EpollServer
{
public:
    using func_t = function<void(string)>;
    EpollServer(func_t hander, const uint16_t &port = 8080)
        : _port(port), _func(hander), _revsnum(g_num)
    {
        //申请空间
        _revs = new struct epoll_event[_revsnum];

        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);

        // 创建ep模型_epfd 并将_listensock以读等待的方式进行ctl
        _epfd = Epoll::CreateEpoll();
        logMessage(DEBUG, "init success, listensock: %d, epfd: %d", _listensock, _epfd);
        if (!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, _listensock, EPOLLIN))
            exit(2);
        logMessage(DEBUG, "add listensock to epoll success.");
    }
    void Accepter()
    {
        string clientip;
        uint16_t clientport;
        int sock = Sock::Accept(_listensock, &clientip, &clientport);
        Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, sock, EPOLLIN);
        logMessage(DEBUG, "add new sock : %d to epoll success", sock);
    }
    void Recver(int sock)
    {
        char buffer[1024];
        int s = recv(sock, buffer, sizeof buffer - 1, 0);
        if (s > 0)
        {
            buffer[s] = 0;
            _func(buffer);
        }
        else if (s == 0)
        {
            bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
            assert(res);
            (void)res;
            close(sock);
            logMessage(NORMAL, "client %d quit, me too...", sock);
        }
        else
        {
            bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
            assert(res);
            (void)res;
            close(sock);
            logMessage(NORMAL, "client recv %d error, close error sock", sock);
        }
    }
    void HandlerEvents(int n)
    {
        assert(n > 0);
        for (int i = 0; i < n; i++)
        {
            uint32_t revents = _revs[i].events;
            int sock = _revs[i].data.fd;
            if (revents & EPOLLIN)
            {
                if (sock == _listensock)
                    Accepter();
                else
                    Recver(sock);
            }
        }
    }
    void LoopOnce(int timeout)
    {
        //_revs返回就绪的events
        //返回就绪fd数组的长度
        int n = Epoll::WaitEpoll(_epfd, _revs, _revsnum, timeout);
        switch (n)
        {
        case 0:
            logMessage(DEBUG, "timeout...");
            break;
        case -1:
            logMessage(WARNING, "epoll wait error: %s", strerror(errno));
            break;
        default:
            logMessage(DEBUG, "get a event");
            HandlerEvents(n);
            break;
        }
    }
    void Start()
    {
        int timeout = -1; // 阻塞等
        // int timeout=1000;
        while (true)
        {
            //处理一次 
            LoopOnce(timeout);
        }
    }
    ~EpollServer()
    {
        if (_listensock >= 0)
            close(_listensock);
        if (_epfd >= 0)
            close(_epfd);
        if (_revs)
            delete[] _revs;
    }

private:
    int _listensock;
    uint16_t _port;
    int _epfd;
    struct epoll_event *_revs;
    int _revsnum;
    func_t _func;
};


5.总结

epoll是IO多路复用的最佳解决方案。通过一套模型,不会像select那样fd过多就无法满足需求,也不会出现大量循环导致用户内核频繁切换导致的性能消耗(添加fd,使用epoll_ctl直接ADD即可)。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会敲代码的运气选手^

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值