zoukankan      html  css  js  c++  java
  • select&epoll&poll

    内核空间和用户空间

    现在操作系统都是采用虚拟存储器,那么对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程的最大地址空间为 4G。操作系统的核心是内核(kernel),它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。具实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,一部分为用户空间。针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。
    每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。换句话说就是, 最高 1G 的内核空间是被所有进程共享的!

     

     

    进程切换

    为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(content switch)。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

    从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:
    1. 保存处理机上下文,包括程序计数器和其他寄存器。
    2. 更新PCB信息。
    3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
    4. 选择另一个进程执行,并更新其PCB。
    5. 更新内存管理的数据结构。
    6. 恢复处理机上下文。

    注:总而言之进程间的切换就是很耗费系统资源

     

    进程的阻塞

    正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则系统将自动执行阻塞(Block),使自己由运行状态变为阻塞状态。所以进程的阻塞,是进程自身的一种主动行为,也只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的

     

    文件描述符fd

    文件描述符(File descriptor)是一个用于描述指向文件的引用。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核中为每一个进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

     

    缓存 I/O

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

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

     

    栗子: 一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
    1. 等待数据准备 (Waiting for the data to be ready)
    2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

    因此linux系统产生五种网络模式的方案。
    - 阻塞 I/O(blocking IO)
    - 非阻塞 I/O(nonblocking IO)
    - I/O 多路复用( IO multiplexing)
    - 信号驱动 I/O( signal driven IO)
    - 异步 I/O(asynchronous IO)

     

    阻塞:

    一个函数在等待某些事情的返回值的时候会被 阻塞. 函数被阻塞的原因有很多: 网络I/O,磁盘I/O,互斥锁等.事实上 每个 函数在运行和使用CPU的时候都或多或少都会被阻塞(举个极端的例子来说明为什么对待CPU阻塞要和对待一般阻塞一样的严肃: 比如密码哈希函数 bcrypt, 需要消耗几百毫秒的CPU时间,这已经远远超过了一般的网络或者磁盘请求时间了).

    异步:函数在会在某些事情完成之前就返回,仅需在函数中触发这个事情的调用即可,而不再关心执行结果如何

    IO多路复用

    IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。 多路复用最高效的是:它能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态, select()函数就可以返回。

    IO多路复用适用如下场合:

      (1)当客户端需要同时处理多个文件描述符的输入输出操作时(一般是交互式输入和网络套接口),必须使用I/O复用。

      (2)当程序需要同时进行多个套接字的操作的时候。

      (3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

      (4)如果一个服务器需要同时使用TCP和UDP协议。

      (5)如果一个服务器要处理多个服务或者多个协议的时候。

      与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

      select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限。而epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当fd就绪时,立即回调函数rollback。



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

     

    select

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

    select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

    select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

    1. select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.
    1. socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZESocket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。(如果能够给套接字注册某个回调函数,当他们活跃时自动完成相关操作,就避免了轮训,这正是epoll与kqueue做的。)

      3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

    简单版:

    1. 连接数受限
    2. 查找配对速度慢
    3. 数据由内核拷贝到用户态

     

    poll

    基本原理:poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

    它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

    1. 大量的fd数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

    2. poll还有一个特点是“水平触发”如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

    3. poll改善了select的第一个连接数受限的缺点

    注意:select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。




    epoll

    基本原理:在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epollepoll更加灵活,没有描述符限制,事先通过epoll_ctl()来注册一个文件描述符,使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)

    epoll的优点:

    1. 监视的描述符数量不受限制,没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口,具体数目可以 cat /proc/sys/fs/file-max察看)。

    2. 效率提升,不是采用轮询的方式,IO的效率不会随着监视fd的数量的增长而下降,只有活跃可用的FD才会调用callback函数。即:epoll最大的优点在于它只关心“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率就会远远高于select和poll。

    3. 内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少了复制的开销。

    epoll对文件描述符的操作:

    LT模式:默认模式,当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

         .socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件

         .socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件

         仅在缓冲区状态变化时触发事件,比如数据缓冲去从无到有的时候(不可读-可读)

    LT(level triggered)是缺省的工作方式,并同时支持block和no-block socket。此种模式下,内核会告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不做任何操作,内核还是会继续通知你。

    ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

         .socket接收缓冲区不为空,有数据可读,则读事件一直触发

         .socket发送缓冲区不满可以继续写入数据,则写事件一直触发

          epoll_wait返回的事件就是socket的状态

    ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从 未就绪变为就绪时,内核会通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd执行IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的 阻塞读/阻塞写操作 把处理多个文件描述符的任务阻塞。
     


    epoll解决的问题:
    • epoll没有fd数量限制,每个epoll监听一个fd,所以最大数量与能打开的fd数量有关,一个g的内存的机器上,能打开10万个左右

    • epoll不需要每次都从用户空间将fd set复制到内核空间,epoll在用epoll_ctl函数进行事件注册的时候,已经将fd复制到内核中,所以不需要每次都重新复制一次

    • select 和 poll 都是主动轮询机制,需要拜訪每一個 FD; epoll是被动触发方式,给fd注册了相应事件的时候,我们为每一个fd指定了一个回调函数,当数据准备好之后,就会把就绪的fd加入一个就绪的队列中,epoll_wait的工作方式实际上就是在这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。

    • 虽然epoll。poll。epoll都需要查看是否有fd就绪,但是epoll之所以是被动触发,就在于它只要去查找就绪队列中有没有fd,就绪的fd是主动加到队列中,epoll不需要一个个轮询确认。 换一句话讲,就是select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动轮询一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用轮询。

     

     select、poll、epoll区别

     selectepoll都是I/O多路复用的方式,但是select是通过不断轮询监听socket实现,epoll是当socket有变化时通过回掉的方式主动告知用户进程实现

    1. 支持一个进程所能打开的最大连接数
    输入图片说明
     
     
     
    1. FD剧增后带来的IO效率问题
    输入图片说明
     
     
     
    1. 消息传递方式
    输入图片说明
     
     

    使用场景:

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

    2. select低效是因为每次它都需要轮询,而epoll采用的是被动触发方式,只需要查看就绪队列中是否加入了新成员即可。

    3. select, poll是为了解決同时大量IO的情況(尤其网络服务器),但是随着连接数越多,性能越差
    4. epoll是select和poll的改进方案,在 linux 上可以取代 select 和 poll,可以处理大量连接的性能问题


     

  • 相关阅读:
    govalidators 验证
    go iris框架 获取url的两种方法
    tornado框架
    并发/并行,阻塞/非阻塞,同步/异步
    CSS选择器及优先级
    linux下压力测试命令ab
    asyncio
    linux网络原理及基础设置
    linux命令
    linux简介
  • 原文地址:https://www.cnblogs.com/hsmwlyl/p/10652503.html
Copyright © 2011-2022 走看看