IO总结

2019-03-09  本文已影响0人  简书徐小耳

1.目前很多博客说的五种模型都是从read角度来描述的。

2.我们也常会说Direct IO,或者其他文件IO。他们主要是从write角度,即是否经过pagecache来区分。

3.引用一个网友的交流--人可以分为男人和女人,也可以分为好人和坏人。

java io和操作系统io的区别

理解底层的send(sendto)和recv(recvFrom)方法

int send( SOCKET s, const char FAR *buf, int len, int flags );

| MSG_DONTROUTE | 不查找表 | 是send函数使用的标志.这个标志告诉IP.目的主机在本地网络上面,没有必要查找表.这个标志一般用网络诊断和路由程序里面.
| MSG_OOB | 接受或者发送带外数据 |表示可以接收和发送带外的数据.关于带外数据我们以后会解释的.
| MSG_PEEK | 查看数据,并不从系统缓冲区移走数据 |是recv函数的使用标志,表示只是从系统缓冲区中读取内容,而不清除系统缓冲区的内容.这样下次读的时候,仍然是一样的内容.一般在有多个进程读写数据时可以使用这个标志.
| MSG_WAITALL | 等待所有数据 |L是recv函数的使用标志,表示等到所有的信息到达时才返回.使用这个标志的时候recv回一直阻塞,直到指定的条件满足,或者是发生了错误. 1)当读到了指定的字节时,函数正常返回.返回值等于len 2)当读到了文件的结尾时,函数正常返回.返回值小于len 3)当操作发生错误时,返回-1,且设置错误为相应的错误号(errno)

同步socket的send方法

int recv( SOCKET s, char FAR *buf, int len, int flags );

同步socket的recv方法

我们要做的就是 将数据在用户态(即上述方法中参数buf)和内核状态(socket内部的buffer或者页缓存)进行复制和拷贝

IO分类(一)

阻塞IO模型

非阻塞IO模型

IO复用模型

Selector(多路复用器的实现方式)

select

poll

epoll

epoll 具体的过程如下

epfd: 由 epoll_create() 生成的 Epoll 专用的文件描述符;
epoll_event: 用于回传代处理事件的数组;
maxevents: 每次能处理的事件数;
timeout: 等待 I/O 事件发生的超时值

水平触发和边沿触发

DEMO

有两个socket的fd——fd1和fd2。我们设定监听f1的“水平触发读事件“,监听fd2的”边沿触发读事件“。我们使用在时刻t1,使用epoll_wait监听他们的事件。
在时刻t2时,两个fd都到了100bytes数据,于是在时刻t3, epoll_wait返回了两个fd进行处理。在t4,我们故意不读取所有的数据出来,只各自读50bytes。
然后在t5重新注册两个事件并监听。在t6时,只有fd1会返回,因为fd1里的数据没有读完,仍然处于“被触发”状态;而fd2不会被返回,因为没有新数据到达。

水平触发

边沿触发

边沿触发的优缺点

区别总结

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,epoll 通过 mmap 把内核空间和用户空间映射到同一块内存,省去了拷贝的操作。

信号驱动IO模型

异步IO模型。

IO分类(二)

  • 上述把io分为五种类型,对应于我们java中其实就是bio,nio,aio。个人对于selector理解其不属于io的一种,只是一种更好管理io的方式。
  • 上述在分析io模型的时候把输入和输出都抽象化了,如果具体到现实中则大抵可以分为如下两类:磁盘io和网络io
  • 这里的分类主要讲解与BIO,NIO,AIO和磁盘IO以及网络IO之间的联系。
  • IO Blcok只有在对端是socket的情况下才有block,因为这个时候会去检测socket buffer 如果没有数据只能等待。
  • 对于磁盘的read,linux总认为不是BLock,就算中间有磁盘抖动等原因都不认为是block。
  • 基于上述原因虽然nio和io多路复用器对于标准输入输出描述符、管道和FIFO也都是有效的。但是像磁盘IO这种效果不大,所以一般这两种都是用于讨论网络IO。

磁盘IO

簇(sector)和块(block)

pageCache

用户空间的buffer

通过mmap和sendfile避免了两次copy

mmap

sendFile

传统io的读写顺序 ,read =从磁盘的读取到pagecache(DMA) 从pageCache到用户空间(cpu copy)。write=用户空间到pageCache(cpu copy),从socket缓冲到磁盘(DMA)

noremal的sendfile的流程=DMA copy 从磁盘到页缓存,然后才有cpu copy 将页缓存中数据拷贝到目标scoket的缓冲区,然后socket缓冲区到 磁盘(DMA)。

优化后的sendFile流程=DMA copy 从磁盘到页缓存,然后才有cpu copy 将页缓存中的文件描述符(件位置和长度信息的缓冲区描述符添加socket缓冲区去)拷贝到目标scoket的缓冲区然后socket缓冲区到 磁盘(DMA)。

DIO

BIO

NIO

AIO

POSIX AIO

这套接口没有得到广泛的使用,原因是其有很大的局限性——这套接口并不能算是"真・AIO"。这套接口是完全在用户态实现的(libc),完全没有深入到操作系统内核中。

此外,用信号做AIO的触发在工程中有很多问题。信号是一个“数字”,而且是全局有效的。所以比如你用POSIX AIO实现了一个lib,选用数字M做信号;但是你无法阻止其他人用POSIX AIO实现另外一个lib,也选用数字M做信号。这样如果一个程序同时用了两套lib,就会彼此干扰。POSIX AIO无法实现类似于epoll中可以创建多个epoll fd,彼此隔离的使用方式。

此外POSIX AIO因为是POSIX指定的标准,所以其存在的一个重要意义是不同操作系统的实现要一致,便于跨平台使用。但实际上各个操作系统对此标准实现的相当不一致(尤其是MacOS)

Linux AIO

aio_context_t ctx;
struct iocb cb;
struct iocb *cbs[1];
char data[4096];
struct io_event events[1];
int ret;
int fd = /* 打开一个文件,获得fd */;
ctx = 0;

ret = io_setup(128, &ctx); // 初始化一个同时处理最大128个fd的aio ctx
    
/* 初始化 IO control block */
memset(&cb, 0, sizeof(cb));
cb.aio_fildes = fd;
cb.aio_lio_opcode = IOCB_CMD_PWRITE; // 设置要“写入”
cb.aio_buf = (uint64_t)data; // aio用的buffer
cb.aio_offset = 0; // aio要写入的offset
cb.aio_nbytes = 4096; // aio要写入的字节个数

cbs[0] = &cb;
ret = io_submit(ctx, 1, cbs); // 提交io进行异步处理

/* 等待aio完成 */
ret = io_getevents(ctx, 1, 1, events, NULL);
/* 对events进行处理 */
io_destroy(ctx);

它只支持Direct IO的IO操作意味着选择使用了Linux AIO就无法享受Page Cache带来的好处;此外,只要使用Linux AIO,就意味着必须自己做块对齐.

这套接口支持的功能有限,比如对于fsync,stat等API,压根就不能真的做到异步

第三个问题是io_getevents,它和epoll一起使用会让程序有两个阻塞点。这样程序就没法写了。Linux提供了eventfd解决这个问题。

使用eventfd协调epoll和Linux AIO

问题抛出:同时用到epoll和Linux AIO。但是epoll_wait和io_getevents就会引入两个阻塞点,这样,等待文件IO的时候,网络请求就会被延迟

总结

BIO这一套接口非常完备,文件IO除了read,write,还有stat,fsync,rename等接口在现实中也是经常需要”异步“的;
编程容易。看看上面的例子,是不是非常容易晕。而这些已经是非常简化的例子了,现实中的代码要处理相当多的细节;
不用在AIO和Buffered IO中做取舍。BIO天然可以利用Page Cache来提高性能;
容易跨平台。不同操作系统的线程实现和BIO的实现基本上完备一致,不会像AIO那样细节差异相当巨大。

网络IO

BIO

NIO

void setnonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

AIO

上一篇 下一篇

猜你喜欢

热点阅读