linux开发各种I/O操作简析,以及select、poll、epoll机制的对比

作者:良知犹存

转载授权以及围观:欢迎添加微信公众号:羽林君


IO 概念区分

四个相关概念:

  • 同步(Synchronous)

  • 异步( Asynchronous)

  • 阻塞( Blocking )

  • 非阻塞( Nonblocking)

阻塞I/O

 阻塞,就是调用我(函数),我(函数)没有接收完数据或者没有得到结果之前,我不会返回。

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

当用户进程调用了 read()/recvfrom() 等系统调用函数,它会进入内核空间中,当这个网络I/O没有数据的时候,内核就要等待数据的到来,而在用户进程这边,整个进程会被阻塞,直到内核空间返回数据。当内核空间的数据准备好了,它就会将数据从内核空间中拷贝到用户空间,此时用户进程才解除阻塞的的状态,重新运行起来。

所以,阻塞I/O的特点就是在IO执行的两个阶段(用户空间与内核空间)都被阻塞了。

非阻塞I/O

    非阻塞,就是调用我(函数),我(函数)立即返回。阻塞调用是指调用结果返回之前,当前线程会被挂起(线程进入非可执行状态,在这个状态下,cpu不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果之后才会返回。

 

有人也许会把阻塞调用和同步调用等同起来,实际上他是不同的。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回,它还会抢占CPU去执行其他逻辑,也会主动检测I/O是否准备好。

执行的模型如下:

能看到,非阻塞I/O的特点是用户进程需要不断的 主动询问 内核空间的数据准备好了没有。

同步I/O

    在操作系统中,程序运行的空间分为内核空间和用户空间,用户空间所有对io操作的代码(如文件的读写、socket的收发等)都会通过系统调用进入内核空间完成实际的操作。

    而且我们都知道CPU的速度远远快于硬盘、网络等I/O。在一个线程中,CPU执行代码的速度极快,然而,一旦遇到I/O操作,如读写文件、发送网络数据时,就需要等待 I/O 操作完成,才能继续进行下一步操作,这种情况称为同步 I/O

其实所谓同步,就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。

    实际工作中我们却很少使用同步I/O,因为当你读写某个文件,进行I/O操作时候,如果数据没有及时回应到,那么系统就会将当前执行读写的线程挂起来等待数据的读取完成,而其他需要CPU执行的代码就无法被当前线程执行,这就是同步I/O的弊端。仅仅因为一个I/O操作就会阻塞当前线程,导致其他代码无法执行,当然我们遇到这样时候会选择用多线程或者多进程来并发执行代码。

       但是多线程和多进程也无法根除这种阻塞问题,因为系统内存大小的限制,所以系统不能无限的增加线程和进程。此外过多的线程和进程,就会导致系统切换线程和进程的开销变大,真正运行代码时间就会变少,这样子系统性能也会严重下降。

异步I/O

    简单来说就是,用户不需要等待内核完成实际对io的读写操作就直接返回了。

    当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者

I/O过程主要分两个阶段:

1.数据准备阶段

2.内核空间复制回用户进程缓冲区空间

无论阻塞式IO还是非阻塞式IO,都是同步IO模型,区别就在与第一步是否完成后才返回,但第二步都需要当前进程去完成,异步IO呢,就是从第一步开始就返回直到第二步完成后才会返回一个消息,也就是说,异步能够让你在第一步时去做其它的事情。

同步IO和异步IO的区别就在于:数据拷贝的时候进程是否阻塞

 

阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回

因为异步IO把IO的操作给了内核,让内核去操作,同步IO的话,需要等待IO操作从内核态的数据缓冲区拷贝到用户态的数据缓冲区,所以此时的同步IO是阻塞的。

多路复用I/O

    多路复用I/O就是我们说的 select,poll,epoll 等操作,复用的好处就在于 单个进程 就可以同时处理 多个 网络连接的I/O,能实现这种功能的原理就是 select、poll、epoll 等函数会不断的 轮询 它们所负责的所有 socket ,当某个 socket 有数据到达了,就通知用户进程。

一般在Linux下我们会有以下几种的字符设备读写方式,下面是一个使用的对比:

1、查询方法:一直在查询,不断去查询是否有事件发生,整个过程都是占用CPU资源,非常消耗CPU资源。

2、中断方式:当有事件发生时,就去跳转到相应事件去处理,CPU占用时间少。

3、poll方式: 中断方式虽然占用CPU资源少,但是在应用程序上需要不断在死循环里面执行读取函数,应用程序不能去做其它事情。poll机制解决了这个问题,当有事件发生时,才去执行读read函数,按键事件没有按下时<如果规定了时间,超过时间后返回无按键信息>,去执行其它的处理函数。

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

    我们再说一下select,poll和epoll这几个IO复用方式,这时你就会了解它们为什么是同步IO了,以epoll为例,在epoll开发的服务器模型中,epoll_wait()这个函数会阻塞等待就绪的fd,将就绪的fd拷贝到epoll_events集合这个过程中也不能做其它事(虽然这段时间很短,所以epoll配合非阻塞IO是很高效也是很普遍的服务器开发模式--同步非阻塞IO模型)。有人把epoll这种方式叫做同步非阻塞(NIO),因为用户线程需要不停地轮询,自己读取数据,看上去好像只有一个线程在做事情,也有人把这种方式叫做异步非阻塞(AIO),因为毕竟是内核线程负责扫描fd列表,并填充事件链表的,个人认为真正理想的异步非阻塞,应该是内核线程填充事件链表后,主动通知用户线程,或者调用应用程序事先注册的回调函数来处理数据,如果还需要用户线程不停的轮询来获取事件信息,就不是太完美了,所以也有不少人认为epoll是伪AIO,还是有道理的。

select函数

  该函数准许进程指示内核等待多个事件中的任何一个发送,并只在有一个或多个事件发生或经历一段指定的时间后才唤醒。select的调用过程如下所示:

select的几大缺点:

(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

(2)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

(3)select支持的文件描述符数量太小了,默认是1024

    poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

    epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

    epoll既然是对selectpoll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此之前,我们先看一下epoll和select和poll的调用接口上的不同, select和poll都只提供了一个函数select或者poll函数。而epoll提供了三个函数, epoll create,epoll cti和epoll wait , epoll create是创建一个epol句柄 ; epoll ctl是注册要监听的事件类型; epoll wait则是等待事件的产生。

    对于第一-个缺点, epoll的解决方案在epoll ctl函数中。每次注册新的事件到epoll句柄中时(在epoll ctI中指定EPOLL CTL ADD) ,会把所有的fd拷贝进内核,而不是在epoll wait的时候重复拷贝。epoll保证 了每个fd在整个过程中只会拷贝一次。

    对于第二个缺点, epoll的解决方案不像select或poll- -样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一-个回调函数 ,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入-一个就绪链表)。epoll wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd (利用schedule_ timeout0实现睡一会,判断一会的效果 ,和select实现中的第7步是类似的)。

    对于第三个缺点, epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目, 这个数字-般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。

总结:

1 、 select ,poll实现需要自 己不断轮询所有fd集合,直到设备就绪 ,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着 ”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪 链表是否为空就行了,这节省 了的CPU时间。这就是回调机制带来的性能提升。

2 、 select , poll每次调用都要把fd集合从用户态往内核态拷贝一-次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。

1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善

这就是我分享的select,poll,epoll,其中参考了很多人的文章,如果大家有什么更好的思路,也欢迎分享交流哈。

 

END

推荐阅读

【1】C++的智能指针你了解吗?

【2】嵌入式底层开发的软件框架简述 
【3】CPU中的程序是怎么运行起来的 必读
【4】C++的匿名函数(lambda表达式)
【5】阶段性文章总结分析

本公众号全部原创干货已整理成一个目录,回复[ 资源 ]即可获得

 

更多分享,扫码关注我

参考链接:

https://blog.csdn.net/Crazy_Tengt/article/details/79225913

https://tutorial.linux.doc.embedfire.com/zh_CN/latest/system_programing/socket_io.html

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

posted @ 2020-11-24 13:46  良知犹存  阅读(256)  评论(0编辑  收藏  举报