01-Linux IO模型

Linux IO模型

一、IO模型分类

1.1 分类标准

  • 一次IO可以分为两个阶段,等待数据准备阶段和数据拷贝阶段。第一阶段将数据加载到内核空间。但是有可能数据没有准备好,比如等待对方的数据或者等待缓冲区被写满,第二阶段就是将准备好的数据从内核空间拷贝到用户进程(用户进程是不能直接访问硬件对应缓冲区的,比如网络IO缓冲区,因此需要将数据从内核拷贝到用户进程)
  • 第一阶段:如果等待期间数据没有准备好能够直接返回,就是非阻塞的,如果需要等待直到数据准备好那就是阻塞的。
  • 第二阶段:如果这个数据拷贝阶段是用户程序自己完成,那就是同步的,如果这个拷贝过程是操作系统完成,拷贝完成之后给用户程序一个通知,那就是异步的。
  • 按照这两个阶段,理论上有四种模型,阻塞同步、阻塞异步、非阻塞同步和非阻塞异步。不过Linux系统下有5种网络模型,前面的四种并非每一种类型都有实现,对应如下:
1.阻塞 I/O(blocking IO)  -- 阻塞同步
2.非阻塞 I/O(nonblocking IO)-- 非阻塞同步
3.I/O 多路复用( IO multiplexing) -- 非阻塞同步
4.信号驱动 I/O( signal driven IO) -- 非阻塞同步
5.异步 I/O(asynchronous IO)-- 非阻塞异步

1.2 blocking和non-blocking的区别

  • 从前面非分类标准可以看出,阻塞IO和非阻塞IO的区别在于,在内核准备数据阶段(可能数据尚未到达,或者等待缓冲区被写满),阻塞IO会阻塞等待直到数据准备好(其实后面的数据拷贝到用户进程的阶段也是阻塞的),非阻塞IO在准备数据阶段则会直接返回。

1.3 同步IO和异步IO的区别

  • 区别在于IO操作的时候是否被阻塞,这个IO操作是指第二阶段将数据从内核拷贝到用户程序,在Linux中是recvfrom这个系统调用完成这个步骤。对于同步IO在这个拷贝阶段用户进程是阻塞的,而异步IO则不会阻塞,异步IO发起操作之后直接返回就不管了,直到内核拷贝数据完成之后返回一个通知告诉进程完成了,整个过程用户进程不被阻塞。

二、几种IO模型对比

2.1 Blocking IO(使用广泛)

  • 在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:

在这里插入图片描述

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

2.2 非阻塞 I/O(nonblocking IO,浪费 CPU使用很少)

  • linux下,可以通过设置socket使其变为non-blocking。就是告诉内核,当请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误。当对一个non-blocking socket执行读操作时,流程是这个样子:

在这里插入图片描述

  • 当用户进程发出recvfrom操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个recvfrom操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送recvfrom操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,non-blocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
  • 这种IO模式用户线程需要不断的去探测数据是否准备好 ,比较浪费CPU资源

2.3 I/O 多路复用( IO multiplexing,广泛使用)

  • IO multiplexing就是我们说的select,poll,epoll,他们是实现IO多路复用的基础。这几个函数也是会使进程阻塞的,但是和阻塞IO不同的是,他们不会阻塞在某一个IO操作,可以阻塞多个IO操作,可以同时对多个读写的IO函数进行检测,直到有数据读写才调用系统的IO调用。比如前面的阻塞IO,它只能检测一个IO操作,如果数据没准备好就只能等着。但是这几个函数它可以检测N个,如果N个IO操作都没有准备好,那么就会阻塞,但是N个IO里面只要有至少一个可以进行读写,则会返回。有些地方也称这种IO方式为event driven IO。
  • select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

在这里插入图片描述

  • 当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。所以,I/O多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
  • 这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用
    multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
  • 在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

2.4 信号驱动I/O

  • 允许SOCKET接口进行信号驱动I/O,并注册一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据

在这里插入图片描述

2.5 异步 I/O(asynchronous IO)

  • Linux下的asynchronous IO其实用得很少。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的步骤在完成后,通过状态、通知和回调来通知调用者的输入输出操作。先看一下它的流程:

在这里插入图片描述

  • 用户进程发起aio_read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
  • 异步操作整个过程都不需要用户进程参与,发出信号之后做自己的事情,等到内核把数据拷贝到用户空间之后,给自己一个信号就完事了

2.6 对比

在这里插入图片描述

  • 通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

三、I/O 多路复用之select、poll、epoll详解

  • select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,让一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

int poll (struct pollfd *fds, unsigned int nfds, int timeout);


int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

3.1 select

  • select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except)或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

  • 优点:良好跨平台支持;select目前几乎在所有的平台上支持

  • 缺点:单个进程能监视的FD数量存在最大限制,Linux上一般为1024,可通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

3.2 poll

  • pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,poll fd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询poll fd来获取就绪的描述符。
  • 从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

3.3 epoll

  • epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
  • epoll是通过回调的方式激活某个有事件的文件描述符,而select和poll需要遍历所有的文件描述符来找到具有事件的文件描述符,因此epoll比select和poll有非常大的性能提示,select和pool在文件描述符过多的时候性能会下降明显,epoll则不会。
  • 优点:epol最大的优点是监视的FD数量不受限制,不过如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,
    但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll,也就是在FD很多,但是同一时间活跃的FD并不多的时候,epoll的效率会大大高于select和poll。

四、参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值