Linux--I/O模型分析

来源:互联网 发布:淘宝怎么导出订单信息 编辑:程序博客网 时间:2024/04/30 01:47

注:本文是对网上资料和书本的总结,有错误的地方请指出,谢谢。

假如有一天,你想访问你读你/tmp目录下的hello文件。会经历什么?

首先你向操作系统发起你想读的请求,然后操作系统就会将数据返回给你,你就会可以看见hello文件的内容。真的这么简单吗?其实不是的。

我们先需要了解用户空间和内核空间:

现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

(by the way:一个进程的堆的大小也是虚拟内存的大小-1G的内核空间大小,基本就是约等于3G(4G-1G),而通常栈是由编译器 决定的,基本是1M)


缓存I/O:

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 I/O 的缺点:
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

所以对于一次I/O的访问,比如你刚才想访问hello文件,首先数据被拷贝到内核的缓冲区中,在才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
1. 等待数据准备 (Waiting for the data to be ready)
2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

过程如下:


(这个时候你可能会说为什么要这么麻烦,不直接从让应用程序访问磁盘。当然这个是可以的,这种称为直接I/O。但是大多数文件的默认操作都是缓存I/O。这个不是本文的重点,大家可以查资料了解)


Linux产生了5种I/O模型:

- 阻塞 I/O(blocking IO)
- 非阻塞 I/O(nonblocking IO)
- I/O 多路复用( IO multiplexing)
- 信号驱动 I/O( signal driven IO)
- 异步 I/O(asynchronous IO)


阻塞/O模型:

常用的I/O操作比如read和write,以及套接字上的accept、recvfrom都是阻塞I/O。在linux中,默认情况下所有的socket都是blocking,以数据报套接字的典型例子:



也就是当用recvfrom时,

kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

     这样,当服务器需要处理1000个连接的的时候,而且只有很少连接忙碌的,那么会需要1000个线程或进程来处理1000个连接,而1000个线程大部分是被阻塞起来的。由于CPU的核数或超线程数一般都不大,比如4,8,16,32,64,128,比如4个核要跑1000个线程,那么每个线程的时间槽非常短,而线程切换非常频繁。这样是有问题的:线程是有内存开销的,1个线程可能需要512K(或2M)存放栈,那么1000个线程就要512M(或2G)内存。线程的切换,或者说上下文切换是有CPU开销的,当大量时间花在上下文切换的时候,分配给真正的操作的CPU就要少很多。


非阻塞/O模型:

   那么,我们就要引入非阻塞I/O的概念,非阻塞IO很简单,通过fcntl(POSIX)或ioctl(Unix)设为非阻塞模式,这时,当你调用read时,如果有数据收到,就返回数据,如果没有数据收到,就立刻返回一个错误,如EWOULDBLOCK。这样是不会阻塞线程了,但是你还是要不断的轮询来读取或写入。

(阻塞时是不占用CPU的,运行态的线程才占用CPU,这样进程或者线程的切换开销比较大。非阻塞就是为了减少大量的上下文切换)


于是,我们需要引入IO多路复用的概念。多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)。

I/O复用模型:


这样在处理1000个连接时,只需要1个线程监控就绪状态,对就绪的每个连接开一个线程处理就可以了,这样需要的线程数大大减少,减少了内存开销和上下文切换的CPU开销。

使用select函数的方式如下图所示:





select比fork高效的地方:

select是内核会用更高效的方式去做,而用户空间的代码每一次系统调用都要包含一次用户空间到内核空间的转换,以及内核再转换回来,这样就很浪费机器周期。而且内核中的poll接口实现会根据操作文件类型的不同有不一样的选择,竭尽全力去节省时间。

select/epoll的作用是,(相比传统的fork/thread模式)让你的系统资源更专注地用在I/O和数据处理上,而不是用于 thread context switch上。


epoll为什么这么快

epoll是多路复用IO(I/O Multiplexing)中的一种方式,但是仅用于linux2.6以上内核,在开始讨论这个问题之前,先来解释一下为什么需要多路复用IO.

以一个生活中的例子来解释.

假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但是不知道你具体住在哪里,于是你们约好了在A号楼门口见面.

如果你使用的阻塞IO模型来处理这个问题,那么你就只能一直守候在A号楼门口等待朋友的到来,在这段时间里你不能做别的事情,不难知道,这种方式的效率是低下的.

如果你使用的非阻塞IO模型来处理这个问题,你创建30个分身,每个在A号楼附近地方等待朋友的到来。 这种类似于为每一个用户创建一个进程或者线程处理连接。

现在时代变化了,开始使用多路复用IO模型来处理这个问题.你告诉你的朋友来了A号楼找楼管大妈,让她告诉你该怎么走.这里的楼管大妈扮演的就是多路复用IO的角色.
进一步解释select和epoll模型的差异.

select版大妈做的是如下的事情:比如同学甲的朋友来了,select版大妈比较笨,她带着朋友挨个房间进行查询谁是同学甲,你等的朋友来了。
epoll版大妈就比较先进了,她记下了同学甲的信息,比如说他的房间号,那么等同学甲的朋友到来时,只需要告诉该朋友同学甲在哪个房间即可,不用自己亲自带着人满大楼的找人了.

别小看了这些效率的提高,在一个大规模并发的服务器中,轮询IO是最耗时间的操作之一.再回到那个例子中,如果每到来一个朋友楼管大妈都要全楼的查询同学,那么处理的效率必然就低下了,过不久楼底就有不少的人了.

对比最早给出的阻塞IO的处理模型, 可以看到采用了多路复用IO之后, 程序可以自由的进行自己除了IO操作之外的工作, 只有到IO状态发生变化的时候由多路复用IO进行通知, 然后再采取相应的操作, 而不用一直阻塞等待IO状态发生变化了.

从上面的分析也可以看出,epoll比select的提高实际上是一个用空间换时间思想的具体应用.


 参考文章:  http://www.cppblog.com/converse/archive/2008/10/12/63836.aspx

             https://www.zhihu.com/question/19732473

             http://blog.csdn.net/tennysonsky/article/details/45745887

             https://segmentfault.com/a/1190000003063859

            《unix网络编程一》


0 0
原创粉丝点击