unix下的五种io模型

1.阻塞式io
该io模型使得调用方阻塞等待数据到达,直到数据从内核拷贝到用户空间后才返回。
2.非阻塞式io
该io模型不会阻塞,当内核没有可读的数据时,调用该函数会返回一个错误。当内核有数据可读时,会等待数据从内核拷贝到用户空间然后返回。
3.io复用
该io模型下进程阻塞在select/poll上,select/pool本身持有多个io描述符,当任何一个描述符触发了你注册的事件时,返回进程,我们可以通过select/poll获得当前需要处理的io描述符,进行处理。
注:io复用一般和非阻塞io一起使用,原因是我们不希望在处理任何一个io描述符时进入阻塞状态,而如果使用阻塞io很难避免这一点。(考虑当io可读时,通过循环读取io中所有的数据,阻塞io总会阻塞在最后一次read上)与此对应的,我们可以使用多线程+阻塞io模型来处理多个io,这两种模型的优劣与合适的场景见后。
4.信号驱动式io
利用unix系统的信号机制通知进程有数据可读。
5.异步io
异步io模型将数据从内核复制到用户空间后才通知用户。

前四种模型是同步io模型,进程需要阻塞等待数据从内核拷贝到用户空间。异步io模型不需要阻塞等待数据拷贝。

描述符读就绪条件:

1.套接字接收缓冲区中的字节数大于等于套接字接收缓冲区低水位标记的当前大小。低水位标记可以通过SO_RCVLOWAT套接字选项设置该套接字的低水位标记,对于TCP和UDP而言该值默认为1。
2.连接半关闭,及接收到了FIN。这时read会返回0。
3.监听套接字已完成的连接数不为0。(但对该套接字调用accept可能阻塞。原因如下:在监听套接字为阻塞模式下,accept调用在已完成队列为空时会阻塞。如果一已完成连接的套接字出发了监听套接字可读,但是在accept之前客户端发送RST报文导致服务器内核从已完成队列里删除了这一项,此时已完成队列为空,accept阻塞。因此io复用中监听套接字也要是非阻塞的。)
4.套接字上有错误待处理,这时read返回-1,并将errno设置为对应错误。

描述符写就绪条件:

1.套接字发送缓冲区可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,如果套接字是非阻塞的,我们调用write函数则会返回一正值。低水位标记可以通过SO_SNDLOWAT套接字选项设置套接字的低水位标记,对TCP和UDP而言该值通常默认为2048。
2.该连接的写半部关闭,对这样的套接字写会产生SIGPIPE信号。(一般我们将SIGPIPE信号设置为忽略,这样写操作将会返回-1并将errno设置为EPIPE。)
3.非阻塞模式的connect套接字完成连接或者产生错误。
4.该套接字上有错误待处理。

描述符异常就绪条件:

我们仅关注带外数据到达。

io复用模型之间的比较

linux上最常见的几种io复用模型有:select,poll和epoll。
缺点1:select能够监视的文件描述符数量存在最大限值,通常是1024。
缺点2:采用轮询方式扫描文件描述符,文件描述符越多,性能越差。
缺点3:每次select都需要将句柄数据结构从用户空间复制到内核空间,开销很大。
缺点4:select采用数组记录监视的文件描述符,每次返回之后需要遍历查找触发的事件。
select采用水平触发模式。

poll采用链表结构保存文件描述符,因此没有了最大监视限制,解决了缺点1。但其他缺点依然存在。

epoll解决了以上问题,每次注册新的事件时,epoll会将fd拷贝进内核,而不是在epoll_wait时候重新拷贝。解决了缺点1,3。
epoll在内核中维护了一颗红黑树结构以存储监视的fd,并且为fd读写事件注册回调函数,当fd读写事件发生时调用回调函数,会将对应的fd加入到一个双向链表中。因此epoll_wait不需要轮询fd,只需要定时去查看就绪双向链表,当有触发fd时就将对应的结构拷贝到用户空间并返回。因此解决了缺点2,4。

下面详细说说epoll的ET和LT模式。
LT模式及水平触发模式,只要fd当前处于可读/可写状态,则每次epoll_wait都会触发对应的事件,因此一旦我们没有要写的数据,就要从epoll中取消关注写事件,防止无效触发。
ET模式及边沿触发模式,对于读来说,---update---,如果在这次你没有读完所有数据,就只有等下次有新数据到来才能继续读。这在业务层面可能会产生bug,假如客户发过来一个请求,等得到响应后才继续发送数据,这时由于这个请求在本次读取过程中没有读取完毕,服务器无法识别本请求,故无法产生响应,这样服务器和客户端停滞在这里。对于写来说,---update---。可以看出来,ET模式应该使用非阻塞套接字,在每次读时读完所有数据,每次写时要么填满发送缓冲区,要么写完当前所有数据。ET模式的写事件触发像是一种惰性触发,不需要你在没有要写的数据时取消关注写事件。
这里看下面这个问题:https://www.zhihu.com/questio...
说明:无论是ET模式还是LT模式,epoll内部维护的可读/可写状态是一样的,只是触发的准则不同。(这里的触发可以理解为将fd相关结构加入就绪双向列表)因此当一个可读事件被触发,其返回的状态可能是可读可写的,但是本次fd相关结构加入就绪列表这一动作不是由于可写事件而发生。对方断开连接,可读事件被触发是预料之内的,但是返回的状态是可读可写,为什么此时可写呢?请看描述符写就绪条件第2/4条,此时它的状态确实是可写,尽管本次返回不是由于其可写而触发。
2020年3月11日更新:发现对ET模式何时触发的理解还是不够,准备查询资料或者源代码。

再看第二个问题:LT模式可以使用阻塞套接字么?
见:https://www.zhihu.com/questio...
LT模式下也不能使用阻塞套接字,原因在上述问题的回答中很多人都回答了。io复用返回的可读不是确定性的。(监听套接字完成队列非空,返回可读,结果在读事件处理之前客户端发送RST导致该队列中的连接关闭,这时候用accept去接收阻塞的监听套接字就会阻塞。另外套接字接收缓冲区有数据到达,触发可读,结果在读事件处理之前,通过校验的方式发现接收缓冲区的数据有误,于是丢弃该数据,这时候read一个阻塞套接字仍旧会阻塞。)对于写来说倒是可以根据接收缓冲区的低水位标记来发送数据,只要发送的数据小于该标记的值,理论上就不会阻塞,但是有没有什么其他的坑目前不知道。
综上,LT模式下也要使用非阻塞套接字。

api

头文件:<sys/epoll.h>
int epoll_create(int size);
Linux 2.6.8之后size参数被忽略(但是必须被设置为大于0)。返回值是一个文件描述符,该描述符持有一个epoll实例,之后的epoll函数都依据于本描述符使用该epoll实例。当不需要时需要调用close函数关闭该文件描述符。
返回值:成功则返回非负文件描述符,失败则返回-1并设置errno。
注:kernel 2.6.27加入了一个新函数epolL_create1,该函数可以对返回的文件描述符设置O_CLOEXEC选项。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event\* 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;
};

向epfd持有的epoll实例添加,修改或删除关注的文件描述符,第二个参数op指示了本次要做的操作:
EPOLL_CTL_ADD 添加
EPOLL_CTL_MOD 修改
EPOLL_CTL_DEL 删除
第三个参数是对应的文件描述符,第四个参数记录了具体关注的事件以及用户定义的数据。具体关注的事件见:http://www.man7.org/linux/man...
重点看这几个:
EPOLLIN 可读

EPOLLOUT 可写
这两个最常用,我们不多介绍。

EPOLLRDHUP(since Linux 2.6.17)
当本端收到FIN字节或者本端调用shutdown(SHUT_RD)触发该事件,这个情况可以用read返回0检测出来。同时也会触发EPOLLIN。据说不是所有内核都支持。

EPOLLPRI
带外数据到达触发该选项。

EPOLLET
触发ET模式,epoll默认是LT模式的。

EPOLLHUP
文件描述符绑定的套接字被挂断,有以下几种情况:
1.本地调用shutdown(SHUT_RDWR)
2.本地调用shutdown(SHUT_WR)并且收到了FIN。
3.接收到了对端发送的RST。
注:被epoll处理的套接字不能被close掉,不然会被epoll识别出来并返回错误。
本项的说明来自链接:https://blog.csdn.net/zhouguo... 不保证正确性,需进一步测试。

EPOLLERR
套接字出错,例如向一个远端close的套接字写数据。
linux manual 建议可以不将EPOLLHUP和EPOLLERR加入监听,因为可以根据读写返回值以及其他信息判断出这些标注的情况。

返回值:成功则返回0,出错返回-1并设置errno。

BUGS
在kernel 2.6.9之前,调用epoll_ctl解除文件描述符的监视时,第三个参数不可被设置为nullptr,尽管这种情况下这个参数没有意义。

int epoll_wait(int epfd, struct epoll_event\* events, int maxevents, int timeout);
阻塞等待注册在epfd绑定的epoll上的事件,参数timeout表示最长阻塞的时间,单位为ms,在这之前有时间发生则立刻返回,如果阻塞时间达到timeout且没有事件发生,也会返回。timeout设置为-1表示不设置此项,默认永远阻塞。
发生事件对应的epoll_event会被填充在events中。
epoll_wait返回有三种情况:
1.有事件发生。
2.timeout被设置且时间到达。
3.被信号中断。
注:使用epoll_pwait可以设置阻塞期间阻塞的信号。

返回值:无出错返回值大于等于0,出错则返回-1并设置errno。

example
#include <iostream>
#include <sys/epoll.h>
#include <cassert>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
#include <vector>
#include <string>
#include <unistd.h>
#include <errno.h>

struct tconnection {
    int fd;
    std::string recv_content;
};

int main() {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(listenfd > 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    int result = inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr);
    assert(result > 0);
    server_addr.sin_port = htons(12345);
    server_addr.sin_family = AF_INET;

    socklen_t len = sizeof(server_addr);
    result = bind(listenfd, (struct sockaddr*)&server_addr, len);
    assert(result == 0);
    result = listen(listenfd, 1024);
    assert(result == 0);

    int epfd = epoll_create(10);
    assert(epfd >= 0);
    epoll_event ev;
    ev.events = EPOLLIN;
    tconnection* ptr = new tconnection();
    ptr->fd = listenfd;
    ev.data.ptr = ptr;
    result = epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
    assert(result == 0);

    std::vector<epoll_event> evs;
    evs.resize(1024);
    while(true) {
      int ready = epoll_wait(epfd, &*evs.begin(), evs.size(), 5000);
      std::cout << "epoll wake up." <<std::endl;
      assert(ready != -1);
      for(int i = 0; i < ready; ++i) {
        tconnection* ptr = (tconnection*)evs[i].data.ptr;
        if(ptr->fd == listenfd) {
          int clientfd = accept(listenfd, nullptr, nullptr);
          std::cout << "new connection.\n";
          assert(clientfd > 0);
          tconnection* ptr = new tconnection();
          ptr->fd = clientfd;
          ev.data.ptr = ptr;
          result = epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
          assert(result == 0);
          continue;
        }
        if(evs[i].events & (EPOLLIN)) {
          char buf[1025];
          int n = -1;
          while((n = read(ptr->fd, buf, 1024)) > 0) {
            buf[n] = '\0';
            ptr->recv_content.append(buf);
            std::cout << "fd : " << ptr->fd << " get " << buf << std::endl;
          }
          assert(n >= 0);
          if(n == 0) {
            result = write(ptr->fd, ptr->recv_content.c_str(), ptr->recv_content.size());
            assert(result >= 0);
            result = epoll_ctl(epfd, EPOLL_CTL_DEL, ptr->fd, &ev);
            assert(result == 0);
            result = close(ptr->fd);
            assert(result == 0);
            delete ptr;
            std::cout << "connection close." <<std::endl;
          }
        }
      }
    }
    close(epfd);
    return 0;
}

注:这个例子仅为了说明epoll api的使用,严格意义上并不是正确的程序(可能阻塞在很多地方)。本例中使用了阻塞套接字(非阻塞套接字需要额外提供输入和输出缓冲,比较麻烦),且没有关注可写事件,直接在可读事件中读数据并缓存,当读到0时将缓冲的数据write回对端。


p__n
491 声望10 粉丝

科学告诉你什么是不可能的;工程则告诉你,付出一些代价,可以把它变成可行,这就是科学和工程不同的魅力。


引用和评论

0 条评论