I/O多路复用详解

I/O 多路复用技术

几种常见的I/O多路复用技术,包括:select、poll、epoll

1、select函数

select是一种I/O多路复用技术,它可以同时监视多个文件描述符的状态变化,包括可读、可写和异常等。
select是最早的I/O多路复用技术之一,它使用fd_set数据结构来管理文件描述符集合,并通过select函数来监视这些文件描述符的状态变化。select函数会阻塞,直到有文件描述符就绪或超时。

select的缺点是,它使用的fd_set数据结构有大小限制,通常为1024个文件描述符,而且每次调用select都需要将fd_set数据结构从用户态复制到内核态,效率较低。

select函数的原型如下:

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

参数说明:

  • nfds:需要监视的最大文件描述符值加1
  • readfds:指向可读文件描述符集合的指针。
  • writefds:指向可写文件描述符集合的指针。
  • exceptfds:指向异常文件描述符集合的指针。
  • timeout:超时时间,可以设置为NULL表示永远等待,或者设置为一个时间值,表示等待指定时间后超时。

select函数的返回值表示就绪文件描述符的数量,如果返回0表示超时,如果返回-1表示出错。

使用select函数的步骤如下:

  1. 创建并初始化fd_set数据结构,用于存储需要监视的文件描述符集合。

  2. 将需要监视的文件描述符添加到对应的fd_set数据结构中。

  3. 调用select函数,等待文件描述符的状态变化。

  4. 检查select函数的返回值,判断是否有文件描述符就绪。

  5. 遍历文件描述符集合,检查每个文件描述符的状态,进行相应的处理。

下面是一个简单的示例代码,演示了如何使用select函数实现I/O复用:

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

int main() {
    fd_set readfds;
    FD_ZERO(&readfds);

    int fd1 = 0; // 标准输入
    int fd2 = 1; // 标准输出

    FD_SET(fd1, &readfds);
    FD_SET(fd2, &readfds);

    int maxfd = std::max(fd1, fd2) + 1;

    while (true) {
        fd_set tmpfds = readfds;
        int ret = select(maxfd, &tmpfds, NULL, NULL, NULL);
        if (ret == -1) {
            std::cerr << "select error" << std::endl;
            break;
        } else if (ret == 0) {
            std::cout << "timeout" << std::endl;
            continue;
        }

        if (FD_ISSET(fd1, &tmpfds)) {
            // fd1有数据可读
            char buf[1024];
            ssize_t n = read(fd1, buf, sizeof(buf));
            if (n > 0) {
                std::cout << "read from fd1: " << std::string(buf, n) << std::endl;
            } else if (n == 0) {
                std::cout << "fd1 closed" << std::endl;
                break;
            } else {
                std::cerr << "read error" << std::endl;
                break;
            }
        }

        if (FD_ISSET(fd2, &tmpfds)) {
            // fd2可写
            std::string message = "Hello, world!";
            ssize_t n = write(fd2, message.c_str(), message.size());
            if (n > 0) {
                std::cout << "write to fd2: " << message << std::endl;
            } else {
                std::cerr << "write error" << std::endl;
                break;
            }
        }
    }

    return 0;
}

在上述示例中,我们使用select函数同时监视标准输入和标准输出。当标准输入有数据可读时,我们从标准输入读取数据并输出;当标准输出可写时,我们向标准输出写入数据。通过使用select函数,我们可以实现同时处理多个文件描述符的I/O操作,提高程序的效率。

2、poll函数

类似于select,它可以同时监视多个文件描述符的状态变化。

poll是select的改进版本,它使用pollfd数据结构来管理文件描述符集合,并通过poll函数来监视这些文件描述符的状态变化。poll函数不再有fd_set的大小限制,并且只需要将pollfd数据结构从用户态复制到内核态一次,效率较高。但是,poll仍然需要遍历整个文件描述符集合来查找就绪的文件描述符,效率较低。

poll函数的原型如下:

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

参数说明:

  • fds:指向pollfd结构体数组的指针,每个pollfd结构体表示一个文件描述符及其关注的事件。
  • nfds:pollfd结构体数组的大小。
  • timeout:超时时间,可以设置为-1表示永远等待,或者设置为一个非负数,表示等待指定时间后超时。

poll函数的返回值表示就绪文件描述符的数量,如果返回0表示超时,如果返回-1表示出错。

使用poll函数的步骤如下:

  1. 创建并初始化pollfd结构体数组,用于存储需要监视的文件描述符及其关注的事件。
  2. 将需要监视的文件描述符及其关注的事件设置到对应的pollfd结构体中。
  3. 调用poll函数,等待文件描述符的状态变化。
  4. 检查poll函数的返回值,判断是否有文件描述符就绪。
  5. 遍历pollfd结构体数组,检查每个文件描述符的revents字段,进行相应的处理。

下面是一个简单的示例代码,演示了如何使用poll函数实现I/O复用:

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

int main() {
    struct pollfd fds[2];

    int fd1 = 0; // 标准输入
    int fd2 = 1; // 标准输出

    fds[0].fd = fd1;
    fds[0].events = POLLIN;
    fds[1].fd = fd2;
    fds[1].events = POLLOUT;

    while (true) {
        int ret = poll(fds, 2, -1);
        if (ret == -1) {
            std::cerr << "poll error" << std::endl;
            break;
        } else if (ret == 0) {
            std::cout << "timeout" << std::endl;
            continue;
        }

        if (fds[0].revents & POLLIN) {
            // fd1有数据可读
            char buf[1024];
            ssize_t n = read(fd1, buf, sizeof(buf));
            if (n > 0) {
                std::cout << "read from fd1: " << std::string(buf, n) << std::endl;
            } else if (n == 0) {
                std::cout << "fd1 closed" << std::endl;
                break;
            } else {
                std::cerr << "read error" << std::endl;
                break;
            }
        }

        if (fds[1].revents & POLLOUT) {
            // fd2可写
            std::string message = "Hello, world!";
            ssize_t n = write(fd2, message.c_str(), message.size());
            if (n > 0) {
                std::cout << "write to fd2: " << message << std::endl;
            } else {
                std::cerr << "write error" << std::endl;
                break;
            }
        }
    }

    return 0;
}

在上述示例中,我们使用poll函数同时监视标准输入和标准输出。当标准输入有数据可读时,我们从标准输入读取数据并输出;当标准输出可写时,我们向标准输出写入数据。

3、epoll函数

epoll是一种高效的I/O多路复用技术,它是Linux特有的,相比于select和poll,epoll在性能和可扩展性上有很大的优势。

它使用epoll_ctl函数来注册文件描述符,并使用epoll_wait函数来等待文件描述符的状态变化。epoll使用红黑树和双链表数据结构来管理文件描述符集合,可以高效地处理大量的文件描述符。epoll提供了三种工作模式:LT(水平触发)、ET(边缘触发)和EPOLLONESHOT(一次性触发),可以根据需要选择不同的工作模式。

三种工作模式:边缘触发(EPOLLET)、水平触发(EPOLLIN)和一次性触发(EPOLLONESHOT)。

  1. 边缘触发(EPOLLET):

    • 当文件描述符上有新的数据可读或可写时,epoll会通知应用程序。
    • 边缘触发模式下,应用程序需要一次性读取或写入所有的数据,否则下次epoll通知时不会再次触发。
    • 边缘触发模式适用于需要高效处理大量数据的场景,因为它只在数据状态发生变化时通知应用程序。
  2. 水平触发(EPOLLIN):

    • 当文件描述符上有新的数据可读时,epoll会通知应用程序。
    • 水平触发模式下,应用程序可以多次读取数据,直到没有数据可读为止。
    • 水平触发模式适用于需要按需读取数据的场景,因为它可以在每次epoll通知时读取一部分数据。
  3. 一次性触发(EPOLLONESHOT):

    • 当文件描述符上有新的数据可读或可写时,epoll会通知应用程序,并将该文件描述符设置为一次性触发模式。
    • 一次性触发模式下,应用程序需要重新设置文件描述符的触发模式,以便下次再次触发。
    • 一次性触发模式适用于需要对每个事件进行独立处理的场景,因为它可以确保每个事件只触发一次。

epoll的核心是epoll_ctl和epoll_wait两个系统调用。

  1. epoll_ctl函数用于注册文件描述符和事件,其原型如下:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数说明:

  • epfd:epoll实例的文件描述符,通过epoll_create函数创建。
  • op:操作类型,可以是EPOLL_CTL_ADD(添加文件描述符)、EPOLL_CTL_MOD(修改文件描述符)或EPOLL_CTL_DEL(删除文件描述符)。
  • fd:需要注册的文件描述符。
  • event:指向epoll_event结构体的指针,用于指定事件类型和数据。
  1. epoll_wait函数用于等待文件描述符的事件就绪,其原型如下:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明:

  • epfd:epoll实例的文件描述符。
  • events:指向epoll_event结构体数组的指针,用于存储就绪的事件。
  • maxevents:events数组的大小,表示最多可以存储多少个就绪的事件。
  • timeout:超时时间,可以设置为-1表示永远等待,或者设置为一个非负数,表示等待指定时间后超时。

epoll_event结构体用于描述事件类型和数据,其定义如下:

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t events;    // 事件类型
    epoll_data_t data;  // 事件数据
};

使用epoll的步骤如下:

  1. 创建epoll实例,通过epoll_create函数获取一个epoll实例的文件描述符。
  2. 创建并初始化epoll_event结构体,用于存储文件描述符和关注的事件。
  3. 使用epoll_ctl函数注册文件描述符和事件。
  4. 调用epoll_wait函数等待文件描述符的事件就绪。
  5. 检查epoll_wait函数的返回值,判断是否有事件就绪。
  6. 遍历就绪的事件,根据事件类型进行相应的处理。

下面是一个简单的示例代码,演示了如何使用epoll实现I/O复用:

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

int main() {
    int epollfd = epoll_create(1);
    if (epollfd == -1) {
        std::cerr << "epoll_create error" << std::endl;
        return -1;
    }

    struct epoll_event event;
    event.events = EPOLLIN;
    event.data.fd = 0; // 标准输入

    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &event) == -1) {
        std::cerr << "epoll_ctl error" << std::endl;
        return -1;
    }

    struct epoll_event events[10];

    while (true) {
        int nfds = epoll_wait(epollfd, events, 10, -1); // 返回事件个数
        if (nfds == -1) {
            std::cerr << "epoll_wait error" << std::endl;
            break;
        }

        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == 0) {
                // 标准输入有数据可读
                char buf[1024];
                ssize_t n = read(0, buf, sizeof(buf));
                if (n > 0) {
                    std::cout << "read from stdin: " << std::string(buf, n) << std::endl;
                } else if (n == 0) {
                    std::cout << "stdin closed" << std::endl;
                    break;
                } else {
                    std::cerr << "read error" << std::endl;
                    break;
                }
            }
        }
    }

    close(epollfd);

    return 0
  }

4、select、poll、epoll总结

  1. select:select是最古老的I/O复用方法,它使用一个位图来表示文件描述符的状态,每次调用select时,需要将所有待监视的文件描述符复制到内核中,这个过程会带来一定的开销。此外,select的位图大小有限,通常为1024,因此在处理大量文件描述符时,需要使用循环调用select来处理。这种方式会导致性能下降。

  2. poll:poll是select的改进版本,它使用一个结构体数组来表示文件描述符的状态,可以处理更多的文件描述符。与select相比,poll的性能有所提升,但在处理大量文件描述符时,仍然需要使用循环调用poll来处理。

  3. epoll:epoll是Linux特有的I/O复用方法,它使用一个事件驱动的方式来处理文件描述符。epoll通过将文件描述符添加到内核事件表中,当有事件发生时,内核会通知应用程序。与select和poll相比,epoll在处理大量文件描述符时具有更好的性能,因为它使用了事件驱动的方式,不需要循环调用。

总的来说,epoll在处理大量文件描述符时具有更好的性能,而select和poll在处理少量文件描述符时可能更加简单和方便。因此,在选择I/O复用方法时,需要根据具体的应用场景和需求来进行选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值