I/O多路复用 epoll

1、阻塞I/O

while ((n = read(STDIN_FILENO, buf, BUFSIZ)) > 0) {
    if (write(STDOUT_FILENO, buf, n) != n) {
        perror("write error");
    }
}

       上面代码片段使用阻塞I/O,在很多场景下可以看到。比如复制文件,读一个文件然后写入到另一个文件。比如一些脚本对文件的简单处理,从某个文件中读让后对文本进行替换等操作再写入这个文件完成文本处理。再比如一些交互式场景,从终端的输入读取再输出。对网络套接字的处理,当read收到消息后,write回复。

       需要理解为什么这里的read,write被称为阻塞I/O。系统调用read在默认模式下,如果没有数据可读就会阻塞进程。对于文件如果读到结尾(EOF),read不会阻塞而是返回0。但是对于终端和网络套接字,当前没有数据可读时read将会阻塞进程直到输入数据(或者收到中止符,收到中止信号,关闭连接等等情况)。阻塞I/O能很好地处理从一个描述符中读,那么同时处理两个文件描述符呢?

2、并发模型

        考虑一个聊天室程序,它从用户终端中读取,然后马上写入到网络套接字。从网络套接字中读取,然后马上写入到终端。这个时候使用阻塞的read并循环调用两个read,当某个read操作阻塞时,程序就无法及时执行另一个read操作。可能会导致这样的现象,你发送了一条消息对方发送了三条消息,你只能看到对方发送的一条消息,想看到另外两条消息你必须再额外发送条消息(因为程序在阻塞等待你发送消息而处理不了对方发送的消息了)。

       一种解决这个问题的方法是创建多个进程或者线程。每个进程或者线程处理一个文件描述符,这样就能使用阻塞I/O处理多个文件描述符了。但是这种方案带来了很多问题,内存问题(每个进程都需要分配独立的内存空间),cpu消耗(当进程线程数多的时候你的cpu运行的大部分时间都在做上下文切换)。并发控制给开发,测试,维护带来了巨大的困难,如果你曾经写过支持多线程的程序你就能理解。对于那种文件描述符数量可能扩展到非常大的程序(比如聊天室服务器),我们必须找到更好的解决方案。

3、非阻塞I/O

       非阻塞I/O是一种I/O操作模式,在此模式下,系统调用不会因为等待某些操作而阻塞当前线程或进程。在非阻塞I/O模式中。如果数据已经准备好,调用会立即返回并处理数据。如果数据尚未准备好,调用会立即返回一个错误(通常是EWOULDBLOCKEAGAIN),表明现在没有数据可读。

对于给定的描述符有两种方法为其指定为非阻塞I/O。调用open获得描述符,指定O_NONBLOCK标志。对于已经打开的文件描述符,调用函数fcntl打开O_NONBLOCK文件状态标志。

3.1 轮询

        可以使用轮询加非阻塞I/O的方法同时处理多个描述符。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
​
#define BUF_SIZE 1024
​
void set_nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
​
void handle_io(int fd_read, int fd_write) {
    char buf[BUF_SIZE];
    ssize_t n;
    
    n = read(fd_read, buf, BUF_SIZE);
    if (n < 0) {
        if (errno != EAGAIN && errno != EWOULDBLOCK) {
            perror("read error");
            exit(EXIT_FAILURE);
        }
        return; // No data read, try again later
    }
    
    if (n > 0) {
        // Actual handling code for read data, e.g., write to another fd
        if (write(fd_write, buf, n) != n) {
            perror("write error");
            exit(EXIT_FAILURE);
        }
    }
}
​
int main() {
    int fd1, fd2, fd1_out, fd2_out; 
    // Assume fd1, fd2, fd1_out, fd2_out are valid file descriptors
    
    set_nonblock(fd1);
    set_nonblock(fd2);
​
    while (1) {
        // Continuously poll the file descriptors
        handle_io(fd1, fd1_out);
        handle_io(fd2, fd2_out);
        // Potentially add sleep or yield here to reduce CPU usage
        usleep(100); // Example: sleep for 100 microseconds
    }
​
    return 0;
}

       上面这段代码展示了如何将描述符设置为非阻塞的,且展示了如何使用轮询处理多个描述符。轮询的优点是实现起来比较简单也容易理解,简单场景下有足够的性能也无需考虑线程同步。

       轮询的方案会导致CPU效率低下和产生较高的时延。可以想象这样的场景,read操作读不到数据返回耗时10ms,read操作读到数据耗时100ms。每次轮询100个描述符只有5个有数据可读,CPU效率仅有34.5%,平均时延达到190ms。这个数据在I/O更加不活跃的情况下将会表现得更差。(100 * 5 / (5 * 100 + 95 * 10) = 0.345, (100 - 5) / 5 * 10 = 190)

3.2 I/O多路转接

       轮询未准备好的描述符将带来低下的CPU效率和高时延,I/O多路转接技术可以解决这个问题。可以想象如果我们使用一个类似信号的机制,在一个描述符准备好时发送信号到进程唤醒进程,进程再去read这个文件描述符。这样我们就不需要去轮询未准备好的描述符了!但是我们知道信号的数量是有限的(一般是64个),信号没有办法告诉我们哪些描述符已经准备好了当描述符的数量大于64时。Linux内核提供了内核事件通知机制用于高效地管理和监视I/O操作等事件。当有描述符就绪时,内核事件通知机制能通知我们哪些描述符已经就绪。C库提供的函数select、poll、epoll都使用了这种机制。

3.2.1 select

       select()是一个经典的 I/O 多路转接函数,用于监控多个文件描述符的状态变化。它允许程序判断一组文件描述符中是否有某个文件描述符已准备好进行读操作、写操作或异常状态检查。

#include <sys/select.h>
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:表示准备好的文件描述符的数量。

  • 等于 0:表示超时。

  • 小于 0:表示出错。

fd_set 是一个文件描述符集合,使用以下的宏来操作它:

  • FD_ZERO(fd_set *set):清空集合。

  • FD_SET(int fd, fd_set *set):将文件描述符 fd 加入集合。

  • FD_CLR(int fd, fd_set *set):将文件描述符 fd 从集合中移除。

  • FD_ISSET(int fd, fd_set *set):检查文件描述符 fd 是否在集合中。

使用示例

    例子关于创建多个描述符然后select,并查找是哪个描述符就绪

3.2.2 poll

    poll() 类似select(),只是接口不同。

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

参数

  • fds: 一个包含多个文件描述符的数组,每个文件描述符通过pollfd结构体来描述。

  • nfds: fds数组中的文件描述符的数量。

  • timeout: 等待的最大毫秒数,-1表示无限等待,0表示立即返回。

struct pollfd {
    int   fd;         /* 文件描述符 */
    short events;     /* 请求的事件 */
    short revents;    /* 返回的事件 */
};
  • fd: 要被检测或监视的文件描述符。

  • events: 你感兴趣的事件,例如POLLIN(数据可读), POLLOUT(数据可写)。

  • revents: 实际发生了的事件,由poll函数在返回时设置。

返回值

  • -1: 错误,errno会被设置

  • 0: 超时,没有文件描述符就绪

  • >0: 就绪的文件描述符的数量

简单例子:

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

int main(void) {
    struct pollfd fds[1];
    fds[0].fd = STDIN_FILENO;
    fds[0].events = POLLIN;
    int timeout = 5000;

    printf("Waiting for input...\n");
    int ret = poll(fds, 1, timeout);

    if (ret == -1) {
        perror("poll");
        exit(EXIT_FAILURE);
    } else if (ret == 0) {
        printf("No data within five seconds.\n");
    } else {
        if (fds[0].revents & POLLIN) {
            char buffer[100];
            ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);
            if (n > 0) {
                buffer[n] = '\0';
                printf("Read: %s", buffer);
            }
        }
    }
    return 0;
}

3.2.3 epoll

       epoll为Linux提供的更现代的多路复用技术,可以更高效得监视多个描述符上的事件。调用select得到内核返回的fd_set,我们仍然需要遍历所有的文件描述符找到就绪的部分(复杂度为O(n),但是相比于轮询已经有了优化不需要调用read)。epoll的内核实现使用红黑树维护描述符,并使用一个链表维护就绪的描述符。select返回fd_set需要复制所有描述符状态,epoll_wait仅返回链表不用将整个描述符表从内核空间复制到用户空间。epoll使用红黑树维护描述符表,增删查的复杂度都为O(logn)。select使用线性表增删查复杂度为O(n)。

int epoll_create1(int flags);

       创建一个 epoll 实例并返回一个文件描述符,用于唯一标识这个实例。flags参数通常设置为 0。以突破描述符上限设置。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

       控制 epoll 实例中文件描述符的注册、修改和删除。epfd 是由 epoll_create1返回的 epoll实例的文件描述符。op 指定操作类型(例如 EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL 等)。fd是要操作的目标文件描述符,event是要与 fd 关联的事件。往内核的红黑树增删改描述符。

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

        等待 epoll 实例 epfd上的事件。events 用于返回就绪的事件,maxevents表示 events数组能够容纳的最大事件数,timeout 是等待的毫秒数,-1表示无限等待。

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

        events: 表示事件类型,例如 EPOLLIN、EPOLLOUT、EPOLLERR等。data: 用户数据,通常用于存储与 epoll 事件相关的信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值