2.1.1 网络I_O与select、poll、epoll

文章详细介绍了Linux中的五种I/O模式:阻塞I/O、非阻塞I/O、I/O多路复用(包括select、poll、epoll)、异步I/O和信号驱动I/O。重点讨论了I/O多路复用的select、poll和epoll的区别,包括它们的工作原理、API和性能特点。最后提供了这三种机制的代码示例。
摘要由CSDN通过智能技术生成

1、Linux的I/O模式

网络I/O,会涉及两个系统对象,用户空间的进程调用IO和内核空间的内核系统,每次进行I/O都要进行两个阶段,当进行read操作时,两个阶段为:

  1. 等待内核准备数据就绪
  2. 将数据从内核拷贝到进程

正因为这两个阶段,linux系统产生了五种IO方式:

  1. 阻塞I/O
  2. 非阻塞I/O
  3. I/O多路复用
  4. 异步I/O
  5. 信号驱动IO(少见、不常用)

1.1 阻塞I/O(blocking I/O)

在linux系统中,默认情况下所有socket都是阻塞的,典型的read操作流程:
image.png
流程中可以看出,blocking I/O 的特点是在I/O的两个阶段(等待数据和拷贝数据)都是被阻塞了。

1.2 非阻塞I/O(non-blocking I/O)

linux系统中,socket可以设置为非阻塞,当一个non-blocking socket进行读操作时,流程如下:
请添加图片描述

从上面的流程中可以看出,当我们read数据时,如果kernel中数据还没有准备完成,read也会立即返回一个error,而不会阻塞我们的进程。当kernel数据准备完成,当我们read时,kernel会拷贝数据到我们的进程内存中,read会成功读取数据。
非阻塞recv函数返回值代表不同含义:

  • recv() 返回值 > 0,读取数据成功,返回值为读取数据得长度;
  • recv() 返回值 = -1 && error = EAGAIN,表示kernel数据没有准备完,read没有读到数据,正常返回。
  • recv() 返回值 = -1 && error != EAGAIN,表示socket故障,无法正常通信,具体故障可以参考error值。

1.3 IO多路复用(IO multiplexing)

多路复用需要把socket设置为non-blocking状态,基本流程如下:
图片.png
I/O多路复用的特点是通过一种机制让一个进程可以同事监控多个socket状态,这些socket集合中任意一个socket进入读写就绪状态,该状态就会被返回。实现多路复用常用的有select、poll以及epoll。

1.4 异步I/O(Asynchronous I/O)

Linux 下的 asynchronous I/O 用在磁盘 I/O 读写操作,不用于网络 I/O,从内核 2.6 版本才开始引入。先看一下它的流程
图片.png

2、I/O多路复用之 select、poll、epoll

select、poll、epoll都是I/O多路复用机制,它们都需要在读写事件就绪后自己负债读写。

2.1 select 流程和API


从上述流程来看,使用select函数进行I/O请求和阻塞I/O模型没有太大区别,甚至还增加了监视socket,以及额外的socket函数的额外操作,效率更差了。但是使用select最大的优势是我们可以在一个线程中同时处理多个socket的I/O请求。我们可以添加多个socket,然后不断通过select函数读取发生事件的socket,即可达到同一个线程内同时处理多个I/O请求的目的(I/O多路复用)。
select的API:

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

FD_ZERO(fd_set* fds); 	//一个 fd_set类型变量的所有位都设为 0
FD_SET(int fd, fd_set* fds);	//设置变量的某个位置
FD_ISSET(int fd, fd_set* fds);	//测试某个位置是否被置位	
FD_CLR(int fd, fd_set* fds);	//清除某个位时可以使用
//maxfds:被监听socket的总数,它比所有socket集合中的最大socket大 1,因为socket是从 0 开始的。
//rfds、wfds、efds分别指向可读、可写、异常事件对应的集合。timeout用于设置select函数的超时时间。
int select(int maxfds, fd_set *rfds, fd_set *wfds, fd_set *efds,struct timeval *timeout);
struct timeval
{
	long tv_sec;	//秒
	long tv_usec;	//微秒
}

select函数的返回值:超时返回:= 0; 失败返回: = -1; 成功返回: > 0。
fd_set的理解:fd_set查看源码可以发现fd_set为长度为1024个bit位数组,fd_set中的每个bit都对应一个fd,最多支持1024个fd。select返回的时真个fd_set,需要我们遍历整个数组才只能知道发生了哪些变化。

2.2 poll 流程和API


poll 就是把 select 中的 fd_set 数组换成了链表,其他和 select 没什么不同。
poll的API

#include <sys/poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

fds:列出了我们需要poll()来检查的文件描述符们。该参数为结构体数组,每个数组元素都是一个struct pollfd结构。可以传递多个结构体,指示 poll() 监视多个文件描述符。
nfds:表示fds结构体数组的长度,简单说,就是向 poll 申请的事件检测的个数。
在 select 里面,文件描述符的个数已经随着 fd_set 的实现而固定,没有办法对此进行配置
而在 poll 函数里,我们可以控制 pollfd 结构的数组大小,这意味着我们可以突破原来 select 函数最大描述符的限制
timeout:描述了 poll 的行为,单位是毫秒
如果是一个 <0 的数,表示在有事件发生之前永远等待;
如果是 0,表示不阻塞进程,立即返回;
如果是一个 >0 的数,表示 poll 调用方等待指定的毫秒数后返回。

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

2.3 epoll 流程和API

image.png
epoll是基于事件驱动的I/O模式,它没有fd个数的限制,epoll对fd采用红黑树的结构进行管理,并且该红黑树是放在内核中的。所以epoll模式的优点有:

  • 支持进程打开fd的数据理论没有上限
  • I/O效率不会随fd个数增加而线性下降
  • 使用mmap加速内核和用户的数据交互

epoll的有两种工作模式

  • LT(level triggered 水平触发,默认工作模式,支持block和non-blocking),当fd就绪,内核会一直通知你fd就绪,可以进行I/O操作了,如果你不做任何操作,内核会连续通知你。
  • ET(edge triggered 边缘触发,高速工作模式,只支持non-blocking模式),当fd就绪,内核只会通知你一次,如果你不处理该事件,内核都不会为该事件通知第二遍,当有新的数据来到时,内核会才会再次发出就绪通知。

epoll的API:

#include <sys/epoll.h>
/* 创建一个epoll的句柄,size用来告诉内核需要监听的数目一共有多大。当创建好epoll句柄后,
它就是会占用一个fd值,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。*/
int epoll_create(int size);
/*epoll的事件注册函数*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*等待事件的到来,如果检测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll的事件注册函数epoll_ctl,第一个参数是epoll_create的返回值,第二个参数表示动作,使用下面三个宏来表示:

EPOLL_CTL_ADD    //注册新的fd到epfd中;
EPOLL_CTL_MOD    //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL    //从epfd中删除一个fd;
typedef union epoll_data
{
    void        *ptr;
    int          fd;
    __uint32_t   u32;
    __uint64_t   u64;
} epoll_data_t;

struct epoll_event 
{
    __uint32_t events; /* Epoll events */
    epoll_data_t data; /* User data variable */
};

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

2.4 总结3种多路复用

系统调用selectpollepoll
事件集合用户通多3个参数分别传入感兴趣的可读、可写以及异常事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数。统一处理所有事件类型,因此只需要一个事件集参数。用户通过pollfd.events传入感兴趣的事件,内核通过修复pollfd.revents反馈其中就绪的事件。内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无需反复传入用户感兴趣的事件。epoll_wait系统调用的参数events近用了反馈就绪的事件。
时间负债度O(n)O(n)O(1)
支持最多fd数据量系统默认1024理论没有上限理论没有上限
工作模式LTLTLT/ET
内核实现采用轮询方式检测I/O就绪事件采用轮询方式检测I/O就绪事件采用回调方式检测就绪事件

3、代码实例之 select、poll、epoll

3.1 select 服务demo

#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/select.h>
#include <netinet/in.h>

int init_svr(int port) {
    int svr_fd = -1;
    struct sockaddr_in svr_addr;
    svr_fd = socket(AF_INET, SOCK_STREAM, 0);

    svr_addr.sin_family = AF_INET;
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    svr_addr.sin_port = htons(port);
    if (-1 == bind(svr_fd, (struct sockaddr *)&svr_addr, sizeof (svr_addr))){
        close(svr_fd);
        printf("bind error:%d", errno);
        return -1;
    }
    if (-1 == listen(svr_fd, 100)){
        close(svr_fd);
        printf("listen error:%d", errno);
        return -1;
    }
    return svr_fd;
}


int main() {
    const int buf_len = 1024;
    int svr_fd = init_svr(9999);
    
    fd_set rfds;
    FD_ZERO(&rfds);
    
    while(true) {
        FD_SET(svr_fd, &rfds);
        struct timeval tv;//每次while循环,tv都会被清空
        tv.tv_sec = 3;
        tv.tv_usec = 0;
        switch (select(FD_SETSIZE, &rfds, nullptr, nullptr, &tv)) {
            case -1:
                printf("select error:%d\n", errno);
                break;
            case 0:
                printf("select time out\n");
                break;
            default:
                for (int fd = 0; fd < FD_SETSIZE; ++fd) {
                    if (FD_ISSET(fd, &rfds)) {
                        if (fd == svr_fd){
                            struct sockaddr clt_addr;
                            int addr_len = sizeof(clt_addr);
                            int clt_fd = accept(svr_fd, (struct sockaddr *) &clt_addr,reinterpret_cast<socklen_t *>(&addr_len));
                            int flags = fcntl(clt_fd, F_GETFL, 0);
                            flags |= O_NONBLOCK;
                            fcntl(clt_fd, F_SETFL, flags);
                            FD_SET(clt_fd, &rfds);
                            printf("svr rcv client :%d\n", clt_fd);
                        } else {
                            char buf[buf_len] = {0};
                            int ret = recv(fd, buf, buf_len, 0);
                            if (0 == ret) {
                                FD_CLR(fd,&rfds);
                                close(fd);
                            }else if (ret > 0){
                                printf("client rcv data len:%d\n", ret);
                                FD_SET(fd, &rfds);
                            }
                        }
                    }
                }
                break;
        }
    }
    return 0;
}

3.2 poll 服务demo

#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <poll.h>

int init_svr(int port) {
    int svr_fd = -1;
    struct sockaddr_in svr_addr;
    svr_fd = socket(AF_INET, SOCK_STREAM, 0);

    svr_addr.sin_family = AF_INET;
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    svr_addr.sin_port = htons(port);
    if (-1 == bind(svr_fd, (struct sockaddr *)&svr_addr, sizeof (svr_addr))){
        close(svr_fd);
        printf("bind error:%d", errno);
        return -1;
    }
    if (-1 == listen(svr_fd, 100)){
        close(svr_fd);
        printf("listen error:%d", errno);
        return -1;
    }
    return svr_fd;
}


int main() {
    const int buf_len = 1024;
    int svr_fd = init_svr(9999);

    struct pollfd pfd[1024];
    int max_size = sizeof (pfd)/sizeof (pfd[0]);
    for (int i = 0; i <max_size; ++i) {
        pfd[i].fd = -1;
    }
    pfd[0].fd = svr_fd;
    pfd[0].events = POLLIN;
    int nfds = 1;
    while (true){
        switch (poll(pfd, nfds, 1000*3)) {
            case -1:
                printf("poll error:%d\n", errno);
                break;
            case 0:
                printf("poll time out\n");
                break;
            default:
                if (pfd[0].revents & POLLIN){
                    struct sockaddr_in clt_addr;
                    socklen_t len = sizeof(clt_addr);
                    int clt_fd = accept(svr_fd,(struct sockaddr*)&clt_addr,&len);
                    printf("accept finish %d\n",clt_fd);
                    int index = 0;
                    for (; index < max_size; ++index) {
                        if (pfd[index].fd < 0){
                            pfd[index].fd = clt_fd;
                            break;
                        }
                    }
                    if (max_size == index){
                        close(clt_fd);
                        printf("to many client\n");
                    }
                    pfd[index].events = POLLIN;
                    nfds = nfds>index+1?nfds:index+1;
                }

                for (int i = 1; i < nfds; ++i) {
                    if (pfd[i].revents & POLLIN){
                        char buf[buf_len]={0};
                        int ret = recv(pfd[i].fd, buf, sizeof(buf)-1, 0);
                        if (0 == ret){
                            printf("client close:%d\n", pfd[i].fd);
                            close(pfd[i].fd);
                            pfd[i].fd = -1;
                        } else if (ret > 0){
                            printf("client rcv size:%d\n", ret);
                        }
                    }

                    if (pfd[i].revents & POLLOUT){
                        //send data
                    }
                }
                break;
        }
    }
    return 0;
}

3.3 epoll 服务demo

#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>

int init_svr(int port) {
    int svr_fd = -1;
    struct sockaddr_in svr_addr;
    svr_fd = socket(AF_INET, SOCK_STREAM, 0);

    svr_addr.sin_family = AF_INET;
    svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    svr_addr.sin_port = htons(port);
    if (-1 == bind(svr_fd, (struct sockaddr *)&svr_addr, sizeof (svr_addr))){
        close(svr_fd);
        printf("bind error:%d", errno);
        return -1;
    }
    if (-1 == listen(svr_fd, 100)){
        close(svr_fd);
        printf("listen error:%d", errno);
        return -1;
    }
    return svr_fd;
}

int main() {
    const int buf_len = 1024;
    int svr_fd = init_svr(9999);

    int epfd = epoll_create(1);
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = svr_fd;
    if (epoll_ctl(epfd, EPOLL_CTL_ADD, svr_fd, &ev) < 0){
        printf("epoll_ctrl error:%d\n", errno);
        close(svr_fd);
        return -1;
    }
    const int ev_nums = 128;
    struct epoll_event evs[ev_nums];
    while (true){
        switch (epoll_wait(epfd, evs, ev_nums, 3 * 1000)) {
            case -1:
                printf("epoll_wait error:%d\n", errno);
                break;
            case 0:
                printf("epoll_wait time out\n");
                break;
            default:
                for (int i = 0; i < ev_nums; ++i) {
                    struct sockaddr_in clt_addr;
                    int addr_len = sizeof (clt_addr);
                    if (evs[i].data.fd == svr_fd && evs[i].events & EPOLLIN){
                        int clt_fd = accept(svr_fd, (struct sockaddr *) &clt_addr,reinterpret_cast<socklen_t *>(&addr_len));
                        ev.data.fd = clt_fd;
                        ev.events = EPOLLIN;
                        epoll_ctl(epfd,EPOLL_CTL_ADD,clt_fd,&ev);
                        printf("recv client:%d\n", clt_fd);
                    } else if (evs[i].data.fd != svr_fd && evs[i].events & EPOLLIN){
                        char buf[buf_len]={0};
                        int ret = recv(evs[i].data.fd, buf, sizeof(buf)-1, 0);
                        if (0 == ret){
                            printf("client close:%d\n", evs[i].data.fd);
                            close(evs[i].data.fd);
                            evs[i].data.fd = -1;
                            epoll_ctl(epfd, EPOLL_CTL_DEL, evs[i].data.fd, nullptr);
                        } else if (ret > 0){
                            printf("client rcv size:%d\n", ret);
                            ev.data.fd = evs[i].data.fd;
                            ev.events = EPOLLOUT | EPOLLIN;
                            epoll_ctl(epfd,EPOLL_CTL_MOD,evs[i].data.fd,&ev);
                        }
                    }else if (evs[i].data.fd != svr_fd && evs[i].events & EPOLLOUT){
                        //send data
                    }
                }
                break;
        }
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值