IO 杂记
1.用户空间 和 内核空间
Kernel space 是 Linux 内核的运行空间, User space 是用户程序的运行空间. 为了安全, 它们是隔离的, 即使用户的程序崩溃了, 内核也不受影响.
ni : niceness 的缩写, CPU 消耗在 nice 进程 (低优先级) 的时间百分比.
id : idle 的缩写, CPU 消耗在闲置进程的时间百分比, 这个值越低, 表示 CPU 越忙.
wa : wait 的缩写, CPU 等待外部 I/O 的时间百分比, 这段时间 CPU 不能干其他事, 但是也没有执行运算, 这个值太高就说明外部设备有问题.
hi : hardware interrupt 的缩写, CPU 响应硬件中断请求的时间百分比.
si : software interrupt 的缩写, CPU 响应软件中断请求的时间百分比.
st : stole time 的缩写, 该项指标只对虚拟机有效, 表示分配给当前虚拟机的 CPU 时间之中, 被同一台物理机上的其他虚拟机偷走的时间百分比.
2. PIO与DMA
? PIO 我们拿磁盘来说, 很早以前, 磁盘和内存之间的数据传输是需要CPU控制的, 也就是说如果我们读取磁盘文件到内存中, 数据要经过CPU存储转发, 这种方式称为PIO.
? DMA 后来, DMA(直接内存访问, Direct Memory Access) 取代了PIO, 它可以不经过CPU而直接进行磁盘和内存 (内核空间) 的数据交换. 在DMA模式下, CPU只需要向DMA控制器下达指令, 让DMA控制器来处理数据的传送即可, DMA控制器通过系统总线来传输数据, 传送完毕再通知CPU, 这样就在很大程度上降低了CPU占有率, 大大节省了系统资源.
3. 缓存IO 和 直接IO
3.1 缓存IO
? 在Linux的缓存I/O机制中, 数据先从磁盘复制到内核空间的缓冲区, 然后从内核空间缓冲区复制到应用程序的地址空间.
1. 读操作
? 操作系统检查内核的缓冲区有没有需要的数据, 如果已经缓存了, 那么就直接从缓存中返回;否则从磁盘中读取, 然后缓存在操作系统的缓存中.
2. 写操作
? 将数据从用户空间复制到内核空间的缓存中. 这时对用户程序来说写操作就已经完成, 至于什么时候再写到磁盘中由操作系统决定, 除非显示地调用了sync同步命令.
3. 优点
? 在一定程度上分离了内核空间和用户空间, 保护系统本身的运行安全; 可以减少读盘的次数, 从而提高性能.
4. 缺点
? 在缓存 I/O 机制中, DMA 方式可以将数据直接从磁盘读到页缓存中, 或者将数据从页缓存直接写回到磁盘上, 而不能直接在应用程序地址空间和磁盘之间进行数据传输. 数据在传输过程中需要在应用程序地址空间 (用户空间) 和缓存 (内核空间) 之间进行多次数据拷贝操作, 带来的CPU以及内存开销是非常大的.
3.2 直接IO
? 直接IO就是应用程序直接访问磁盘数据, 而不经过内核缓冲区, 也就是绕过内核缓冲区,自己管理I/O缓存区, 这样做的目的是减少一次从内核缓冲区到用户程序缓存的数据复制.
? 引入内核缓冲区的目的在于提高磁盘文件的访问性能, 因为当进程需要读取磁盘文件时, 如果文件内容已经在内核缓冲区中, 那么就不需要再次访问磁盘;而当进程需要向文件中写入数据时, 实际上只是写到了内核缓冲区便告诉进程已经写成功, 而真正写入磁盘是通过一定的策略进行延迟的.
? 数据库服务器, 它们为了充分提高性能, 希望绕过内核缓冲区, 由自己在用户态空间实现并管理I/O缓冲区, 包括缓存机制和写延迟机制等, 以支持独特的查询机制, 比如数据库可以根据更加合理的策略来提高查询缓存命中率. 另一方面, 绕过内核缓冲区也可以减少系统内存的开销, 因为内核缓冲区本身就在使用系统内存.
? Linux提供了对这种需求的支持, 即在open()系统调用中增加参数选项O_DIRECT, 用它打开的文件便可以绕过内核缓冲区的直接访问, 这样便有效避免了CPU和内存的多余时间开销.
4. IO 访问方式
4.1 磁盘IO
? 当应用程序调用read接口时, 操作系统检查在内核的高速缓存有没有需要的数据, 如果已经缓存了, 那么就直接从缓存中返回, 如果没有, 则从磁盘中读取, 然后缓存在操作系统的缓存中.
? 应用程序调用write接口时, 将数据从用户地址空间复制到内核地址空间的缓存中, 这时对用户程序来说, 写操作已经完成, 至于什么时候再写到磁盘中, 由操作系统决定, 除非显示调用了sync同步命令.
4.2 网络IO
普通的网络传输步骤如下:
? 1)操作系统将数据从磁盘复制到操作系统内核的页缓存中.
? 2)应用将数据从内核缓存复制到应用的缓存中.
? 3)应用将数据写回内核的Socket缓存中.
? 4)操作系统将数据从Socket缓存区复制到网卡缓存, 然后将其通过网络发出.
? 1.当调用read系统调用时, 通过DMA(Direct Memory Access) 将数据copy到内核模式.
? 2.然后由CPU控制将内核模式数据copy到用户模式下的 buffer中.
? 3.read调用完成后, write调用首先将用户模式下 buffer中的数据copy到内核模式下的socket buffer中
? 4.最后通过DMA copy将内核模式下的socket buffer中的数据copy到网卡设备中传送. 从上面的过程可以看出, 数据白白从内核模式到用户模式走了一圈, 浪费了两次copy, 而这两次copy都是CPU copy, 即占用CPU资源.
4.3 磁盘 IO vs 网络IO
? 磁盘IO主要的延时是由(以15000rpm硬盘为例) :
? 机械转动延时机械磁盘的主要性能瓶颈, 平均为2ms) + 寻址延时(2~3ms) + 块传输延时(一般4k每块, 40m/s的传输速度, 延时一般为0.1ms) 决定. (平均为5ms)
? 而网络IO主要延是由: 服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时 决定. (一般为几十到几千毫秒, 受环境干扰极大)
5. 同步IO 和 异步IO
? 同步和异步是针对 应用程序和内核的交互 而言的
? 同步指 用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪, 而异步指 用户进程触发IO操作以后便开始做自己的事情
? 用户空间要的数据, 必须等到内核空间给它才做其他事情; 用户空间要的数据, 不需要等到内核空间给它, 才做其他事情
6. 阻塞IO 和 非阻塞IO
? 阻塞方式下读取或者写入函数将一直等待, 而非阻塞方式下, 读取或者写入函数会立即返回一个状态值
? 用户和内核空间IO操作的方式
? 堵塞: 用户空间通过系统调用(systemcall)和内核空间发送IO操作时, 该调用是堵塞的
? 非堵塞: 用户空间通过系统调用 (systemcall) 和 内核空间发送IO操作时, 该调用是不堵塞的, 直接返回的, 只是返回时, 可能没有数据而已
7. IO 模型
IO模式 | BIO | NIO | AIO |
---|
| 同步阻塞 | 同步非阻塞 | 异步非阻塞 | 实现难度 | easy | hard | hard | 可靠性 | 差 | 好 | 好 | 吞吐量 | 低 | 高 | 高 |
-
同步阻塞IO (Blocking IO): 即传统的IO模型 -
同步非阻塞IO (Non-blocking IO): 默认创建的socket都是阻塞的, 非阻塞IO要求socket被设置为NONBLOCK. -
IO多路复用(IO Multiplexing) : 即经典的Reactor设计模式, 有时也称为异步阻塞IO -
异步IO(Asynchronous IO) : 即经典的Proactor设计模式, 也称为异步非阻塞IO
7.1 同步阻塞IO BIO
? 同步阻塞IO模型是最简单的IO模型, 用户线程在内核进行IO操作时被阻塞.
? 用户线程通过系统调用read发起IO读操作, 由用户空间转到内核空间. 内核等到数据包到达后, 然后将接收的数据拷贝到用户空间, 完成read操作
? 即用户需要等待read将socket中的数据读取到buffer后, 才继续处理接收的数据. 整个IO请求的过程中, 用户线程是被阻塞的, 这导致用户在发起IO请求时, 不能做任何事情, 对CPU的资源利用率不够
缺点
? IO代码里read操作是阻塞操作, 如果连接不做数据读写操作会导致线程阻塞, 浪费资源
? 如果线程很多, 会导致服务器线程太多, 压力太大, 比如C10K问题
7.2 同步非阻塞IO NIO
? 将socket设置为NONBLOCK. 这样做用户线程可以在发起IO请求后可以立即返回
? 由于socket是非阻塞的方式, 因此用户线程发起IO请求时立即返回. 但并未读取到任何数据, 用户线程需要不断地发起IO请求, 直到数据到达后, 才真正读取到数据, 继续执行
? 整个IO请求的过程中, 虽然用户线程每次发起IO请求后可以立即返回, 但是为了等到数据, 仍需要不断地轮询、重复请求, 消耗了大量的CPU的资源
8.3 IO多路复用
? 建立在内核提供的多路分离函数select基础之上的, 使用select函数可以避免同步非阻塞IO模型中轮询等待的问题
? 用户首先将需要进行IO操作的socket添加到select中, 然后阻塞等待select系统调用返回. 当数据到达时, socket被激活, select函数返回. 用户线程正式发起read请求, 读取数据并继续执行
? 使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求. 用户可以注册多个socket, 然后不断地调用select读取被激活的socket, 即可达到在同一个线程内同时处理多个IO请求的目的
? 其中while循环前将socket添加到select监视中, 然后在while内一直调用select获取被激活的socket, 一旦socket可读, 便调用read函数将socket中的数据读取出来
? 使用select函数的优点并不仅限于此. 虽然上述方式允许单线程内处理多个IO请求, 但是每个IO请求的过程还是阻塞的(在select函数上阻塞) 优化: 如果用户线程只注册自己感兴趣的socket或者IO请求, 然后去做自己的事情, 等到数据到来时再进行处理, 则可以提高CPU的利用率 ?
? 通过Reactor的方式, 可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理. 用户线程注册事件处理器之后可以继续执行做其他的工作(异步), 而Reactor线程负责调用内核的select函数检查socket状态. 当有socket被激活时, 则通知相应的用户线程(或执行用户线程的回调函数), 执行handle_event进行数据读取、处理的工作
? 由于select函数是阻塞的, 因此多路IO复用模型也被称为异步阻塞IO模型. 注意, 这里的所说的阻塞是指select函数执行时线程被阻塞, 而不是指socket.
void UserEventHandler::handle_event() {
if(can_read(socket)) {
read(socket, buffer);
process(buffer);
}
}
{
Reactor.register(new UserEventHandler(socket));
}
? 用户需要重写EventHandler的handle_event函数进行读取数据、处理数据的工作, 用户线程只需要将自己的EventHandler注册到Reactor即可.
Reactor::handle_events() {
while(1) {
sockets = select();
for(socket in sockets) {
get_event_handler(socket).handle_event();
}
}
}
8.4 异步IO
? 异步IO模型中, 当用户线程收到通知时, 数据已经被内核读取完毕, 并放在了用户线程指定的缓冲区内, 内核在IO完成后通知用户线程直接使用即可
?
? 用户线程直接使用内核提供的异步IO API发起read请求, 且发起后立即返回, 继续执行用户线程代码. 不过此时用户线程已经将调用的AsynchronousOperation和CompletionHandler注册到内核, 然后操作系统开启独立的内核线程去处理IO操作. 当read请求的数据到达时, 由内核负责读取socket中的数据, 并写入用户指定的缓冲区中. 最后内核将read的数据和用户线程注册的CompletionHandler分发给内部Proactor, Proactor将IO完成的信息通知给用户线程(一般通过调用用户线程注册的完成事件处理函数), 完成异步IO
void UserCompletionHandler::handle_event(buffer) {
process(buffer);
}
{
aio_read(socket, new UserCompletionHandler);
}
? 用户需要重写CompletionHandler的handle_event函数进行处理数据的工作, 参数buffer表示Proactor已经准备好的数据, 用户线程直接调用内核提供的异步IO API, 并将重写的CompletionHandler注册即可.
|