IO复用技术(1)——select/poll/epoll原理介绍及使用案例


原理:使用一个线程来检查多个文件描述符,委托内核进行检查,如果有一个文件描述符就绪,则返回,否则阻塞直到超时,大大减少需要的线程数量、内存开销和上下文切换的CPU开销(比如一个事用1000个线程去做,但如果使用IO复用,可以只用一个线程)。

1.Select

1.1 工作流程

需要进行IO操作的socket 添加到socket
阻塞直到select系统调用返回(委托内核进行操作)
用户线程发起read请求
内核进行数据拷贝,给用户线程,完成read

image.png

有一张图非常的形象
image.png

1.2 fd_set函数

#define __FD_SETSIZE 1024

typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

// 将文件描述符fd从set集合中删除 
void FD_CLR(int fd, fd_set *set); 

// 判断文件描述符fd是否在set集合中 
int  FD_ISSET(int fd, fd_set *set); 

// 将文件描述符fd添加到set集合中 
void FD_SET(int fd, fd_set *set); 

// 将set集合中, 所有文件描述符对应的标志位设置为0
void FD_ZERO(fd_set *set); 

主要用于将文件描述符fd与fd_set集合进行关联

1.3 select函数

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

readfds:内核检测该集合中的IO是否可读。
writefds:内核检测该集合中的IO是否可写
exceptfds:内核检测该集合中的IO是否异常
nfds:以上三个集合中最大的文件描述符数值 + 1,例如集合是{0,1,4},那么 maxfd 就是 5
timeout:用户线程调用select的超时时长
timeout = NULL,等待无限长时间
timeout = 0,不等待,立刻返回
timeout>0,等待指定时间
返回值
大于0:成功,返回集合中已就绪的IO总个数
等于-1:调用失败
等于0:没有就绪的IO

1.4 例程

先用FD_ZERO将位置0,然后使用FD_SET设置所监听的文件描述符到fd_set,select函数进行监听,当select返回大于0,则使用FD_ISSET遍历所有fd到maxfd,如果可操作,则去操作,操作完后需要用FD_CLR清除已产生的事件
假设fd = 1,fd = 2上发生事件,则select返回时,rset的值为0x0003,当处理完fd = 1的事件,调用FD_CLR后,则值变为0x0002,以此类推
服务端代码

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
 
int main() 
{
 
    // 创建socket
    int lFd = socket(PF_INET, SOCK_STREAM, 0);
    if (lFd < 0) 
    {
        printf("socket error\n");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
 
    int iRet = 0;
    // 绑定
    iRet = bind(lFd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (iRet < 0) 
    {
        printf("bind error\n");
        return -1;
    }
 
    // 监听
    iRet = listen(lFd, 8);
    if (iRet < 0) 
    {
        printf("listen error\n");
        return -1;
    }
 
    int maxFd = lFd;
    fd_set allFdSets, tmpFdSets;
    FD_ZERO(&allFdSets);
    FD_SET(lFd, &allFdSets);
 
    while(1) 
    {
        memcpy(&tmpFdSets, &allFdSets, sizeof(tmpFdSets));


        iRet = select(maxFd + 1, &tmpFdSets, NULL, NULL, 0);
        if (iRet == -1)
        {
            perror("select error:");
            continue;
        }
        else if (iRet == 0)
        {
            printf("select return no results\n");
            continue;
        }
        else
        {
            for (int i = lFd; i < maxFd + 1; i++)
            {
                if (i == lFd)
                {
                    /// new client
                    if (FD_ISSET(lFd, &tmpFdSets))
                    {
                        struct sockaddr_in addr = {0};
                        int iLen = sizeof(addr);
                        int clientFd = accept(lFd, (struct sockaddr *)&addr, &iLen);
                        FD_SET(clientFd, &allFdSets);

                        maxFd = clientFd > maxFd ? clientFd : maxFd;
                        FD_CLR(lFd, &tmpFdSets);
                    }
                }
                else
                {
                    /// msg
                    if (FD_ISSET(i, &tmpFdSets))
                    {
                        char acBuf[1024] = {0};
                        int iLen = read(i, acBuf, sizeof(acBuf));
                        if (iLen == -1)
                        {
                            printf("fd:%d error read ret:%d\n", i, iLen);
                            continue;
                        }
                        else if (iLen == 0)
                        {
                            printf("fd %d closed\n", i);
                        }
                        else
                        {
                            printf("fd %d, recv buf :%s, return ok\n", i, acBuf);
                            write(i, "ok", strlen("ok"));
                        }
                    }
                }

                FD_CLR(i, &tmpFdSets);
            }
        }
    }
    return 0;
}

客户端代码

#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
 
int main() {
 
    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }
 
    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);
 
    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
 
    if(ret == -1){
        perror("connect");
        return -1;
    }
 
    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "data%d", num++);
        printf("write buf:%s\n", sendBuf);
        write(fd, sendBuf, strlen(sendBuf) + 1);
 
        char recvBuf[1024] = {0};
        // 接收
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", recvBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
        sleep(1);
    }
 
    close(fd);
 
    return 0;
}

image.png

2.poll

2.1 poll函数

pol和select原理基本相同,使用起来稍微有点差别,它没有最大1024文件描述符限制,也不需要每次重置fd_set数组的值

struct pollfd {
    int fd; /* file descriptor */
    short events; /* events to look for */
    short revents; /* events returned */
};

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

fds:struct pollfd类型的数组, 存储了待检测的文件描述符,struct pollfd有三个成员
fd:委托内核检测的文件描述符
events:委托内核检测的fd事件(输入、输出、错误),每一个事件有多个取值
revents:这是一个传出参数,数据由内核写入,存储内核检测之后的结果
其中event的取值如下,不同事件对应不同值

nfds:描述的是数组 fds 的大小
timeout: 指定poll函数的阻塞时长 ,-1代表无限等待
返回值
-1:失败,并设置errno,可以用perror打印
大于0:检测的集合中已就绪的文件描述符的总个数

2.2 例程

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <poll.h>
#include <signal.h>
 
int main() 
{
 
    // 创建socket
    int lFd = socket(PF_INET, SOCK_STREAM, 0);
    if (lFd < 0) 
    {
        printf("socket error\n");
        return -1;
    }

    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
 
    int iRet = 0;
    // 绑定
    iRet = bind(lFd, (struct sockaddr *)&saddr, sizeof(saddr));
    if (iRet < 0) 
    {
        printf("bind error\n");
        return -1;
    }
 
    // 监听
    iRet = listen(lFd, 8);
    if (iRet < 0) 
    {
        printf("listen error\n");
        return -1;
    }
 

    int nFds = 0;
    struct pollfd fds[512] = {0};
    int maxFds = sizeof(fds)/ sizeof(fds[0]);
    for (int i = 0; i < maxFds; i++)
    {
        fds[i].fd = -1;
        fds[i].events = POLLIN;
    }

    fds[0].fd = lFd;
    fds[0].events = POLLIN;

    while(1) 
    {
        iRet = poll(fds, nFds + 1, -1);
        if (iRet == -1)
        {
            perror("poll error:");
            continue;
        }
        else if (iRet == 0)
        {
            printf("poll return no results\n");
            continue;
        }
        else
        {
            /// new client
            if (fds[0].revents & POLLIN)
            {
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lFd, (struct sockaddr *)&cliaddr, &len);

                printf("new client connect\n");
                for (int i = 1; i < maxFds; i++)
                {
                    if (fds[i].fd == -1)
                    {
                        fds[i].fd = cfd;
                        fds[i].events = POLLIN;
                        nFds = i > nFds ? i : nFds;
                        printf("new client connect success, fd:%d, nFds:%d\n", fds[i].fd, nFds);
                        break;
                    }
                }
            }

            /// client have data
            for (int i = 1; i <= nFds; i++)
            {
                if (fds[i].revents & POLLIN)
                {
                    char acBuf[1024] = {0};
                    int iLen = read(fds[i].fd, acBuf, sizeof(acBuf));
                    if (iLen == -1)
                    {
                        printf("fd:%d error read ret:%d\n", i, iLen);
                        continue;
                    }
                    else if (iLen == 0)
                    {
                        if (i == nFds)
                        {
                            nFds--;
                        }
                        printf("fd %d closed, relase source, nFds:%d\n", fds[i].fd, nFds);
                        fds[i].fd = -1;
                        fds[i].events = POLLIN;
                        
                    }
                    else
                    {
                        printf("fd %d, recv buf :%s, return ok\n", fds[i].fd, acBuf);
                        write(fds[i].fd, "ok", strlen("ok"));
                    }
                }
            }
            
        }

    }
 
    return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
 
int main() {
 
    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }
 
    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);
 
    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
 
    if(ret == -1){
        perror("connect");
        return -1;
    }
 
    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        sprintf(sendBuf, "data%d", num++);
        printf("write buf:%s\n", sendBuf);
        write(fd, sendBuf, strlen(sendBuf) + 1);
 
        char recvBuf[1024] = {0};
        // 接收
        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", recvBuf);
        } else {
            printf("server disconnect...\n");
            break;
        }
        sleep(1);
    }
 
    close(fd);
 
    return 0;
}

image.png

3.epoll

Epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,且epoll采用红黑树管理文件描述符,效率会更高

3.1 工作流程

正如这个图一样,epoll相比较select和poll一个比较大的优点就在于,它能够准确告知应用层是哪一个事件来了,而不需要去一个个遍历,减少很大一部分开销
image.png

epoll整体流程如下
在内核中创建了一个数据,这个数据中有两个比较重要的数据,一个是 需检测文件描述符信息(红黑树),还有一个是就绪列表,存放已改变的文件描述符信息(双向链表)
image.png

3.2 相关函数

创建epoll句柄

int epoll_create(int size);  
int epoll_create1(int flags);

控制epoll实例,主要是增加或删除需要监控的IO事件

struct epoll_event {
    __uint32_t events;
    epoll_data_t data;
};

union epoll_data {
 void     *ptr;
 int       fd;
 uint32_t  u32;
 uint64_t  u64;
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epfd:epoll句柄
op:操作选项
EPOLL_CTL_ADD: 向 epoll 句柄注册文件描述符对应的事件
EPOLL_CTL_DEL:向 epoll 句柄删除文件描述符对应的事件
fd:操作的文件描述符
event:注册的事件类型,并且可以通过这个结构体设置用户自定义数据
events:注册的事件类型
data:用户自定义数据

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

等待I/O事件就绪

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

**epfd:**epoll句柄
events:出参,代表发生变化的文件描述符信息,可能是多个
maxevents:events的结构体数组大小
timeout
-1,一直阻塞,直到有事件就绪后返回
0,不阻塞,函数马上返回
大于0:等待指定时间后返回

3.3 epoll的两种工作模式

epoll由两种工作模式,分别为LT模式(条件触发)、ET模式(边缘触发),默认为条件触发
条件触发:只要输入缓冲有数据,便一直触发事件
a. 用户不读数据,数据一直在缓冲区,epoll 会一直通知
b. 用户只读了一部分数据,epoll会通知
c. 缓冲区的数据读完了,不通知
边缘触发:只有描述符从未就绪变为就绪时,才会为文件描述符发送一次就绪通知,之后不再通知
减少了事件被重复触发的次数,效率比LT模式高,且可以分离接收数据和处理数据的时间点,工作在该模式必须要使用非阻塞等待
a. 用户不读数据,数据一直在缓冲区中,epoll下次检测的时候就不再次通知了
b. 用户只读了一部分数据,epoll不再次通知
c. 缓冲区的数据读完了,不再次通知

3.4 示例代码

条件触发模式

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/epoll.h>
 
int main() {
 
    // 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in saddr;
    saddr.sin_port = htons(9999);
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
 
    // 绑定
    bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
 
    // 监听
    listen(lfd, 8);
 
    // 调用epoll_create()创建一个epoll实例
    int epfd = epoll_create(100);
 
    // 将监听的文件描述符相关的检测信息添加到epoll实例中
    struct epoll_event epev;
    epev.events = EPOLLIN;
    epev.data.fd = lfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
 
    struct epoll_event epevs[1024];
 
    while(1) {
 
        int ret = epoll_wait(epfd, epevs, 1024, -1);
        if(ret == -1) {
            perror("epoll_wait");
            exit(-1);
        }
 
        printf("ret = %d\n", ret);
 
        for(int i = 0; i < ret; i++) {
 
            int curfd = epevs[i].data.fd;
 
            if(curfd == lfd) {
                // 监听的文件描述符有数据达到,有客户端连接
                struct sockaddr_in cliaddr;
                int len = sizeof(cliaddr);
                int cfd = accept(lfd, (struct sockaddr *)&cliaddr, &len);
 
                epev.events = EPOLLIN;
                epev.data.fd = cfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
            } else {
                if(epevs[i].events & EPOLLOUT) {
                    continue;
                }   
                // 有数据到达,需要通信
                char buf[1024] = {0};
                int len = read(curfd, buf, sizeof(buf));
                if(len == -1) {
                    perror("read");
                    exit(-1);
                } else if(len == 0) {
                    printf("client closed...\n");
                    epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
                    close(curfd);
                } else if(len > 0) {
                    printf("read buf = %s\n", buf);
                    write(curfd, buf, strlen(buf) + 1);
                }
 
            }
 
        }
    }
 
    close(lfd);
    close(epfd);
    return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
 
int main() {
 
    // 创建socket
    int fd = socket(PF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        return -1;
    }
 
    struct sockaddr_in seraddr;
    inet_pton(AF_INET, "127.0.0.1", &seraddr.sin_addr.s_addr);
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(9999);
 
    // 连接服务器
    int ret = connect(fd, (struct sockaddr *)&seraddr, sizeof(seraddr));
 
    if(ret == -1){
        perror("connect");
        return -1;
    }
 
    int num = 0;
    while(1) {
        char sendBuf[1024] = {0};
        // sprintf(sendBuf, "send data %d", num++);
        fgets(sendBuf, sizeof(sendBuf), stdin);
 
        write(fd, sendBuf, strlen(sendBuf) + 1);
 
        // 接收
        int len = read(fd, sendBuf, sizeof(sendBuf));
        if(len == -1) {
            perror("read");
            return -1;
        }else if(len > 0) {
            printf("read buf = %s\n", sendBuf);
        } else {
            printf("服务器已经断开连接...\n");
            break;
        }
    }
 
    close(fd);
 
    return 0;
}

4.总结

IO复用中epoll会更高效,内存拷贝次数少,时间复杂度低,且不受fd数量的限制

  • 15
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值