select, poll, epoll使用介绍与区别浅析

一、背景介绍

用户空间与内核空间

Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间,两者不能简单地使用指针传递数据。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,现在操作系统都是采用虚拟存储器,针对32位linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

文件描述符

在Linux通用I/O模型中,I/O操作系列函数(系统调用)都是围绕一个叫做文件描述符的整数展开。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

进程阻塞

进程阻塞一般是被动的,正在执行的进程,在抢占资源中得不到资源,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,就会被动的挂起在内存,等待某种资源或信号量将其唤醒。当进程进入阻塞状态,是不占用CPU资源的。

缓存IO

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

二、IO模型

对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待的分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。

阻塞式IO(blocking IO)

这是最流行的IO模型,默认情形下,所有的套接字都是阻塞的。

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

非阻塞式IO(non-blocking IO)

进程把一个套接字设置成非阴霾是在通知内核,当所请求的IO操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。

例如,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的系统调用,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,阻塞式IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

IO复用模型(IO multiplexing Model)

有了IO复用,我们就可以调用select/poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的IO系统调用上。

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

这里和blocking IO其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个系统调用(select 和 recvfrom),而blocking IO只调用了一个系统调用(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。

实际中,在IO multiplexing Model中,对于每一个socket,一般都设置成为non-blocking,但是,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

信号驱动式IO模型

我们也可以用信号让内核在描述符就绪时发送SIGIO信号通知我们,即信号驱动式IO。

这种方式首先开启套接字的信号驱动式IO功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用立即返回,进程继续工作。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理。也可以立即通知主循环,让它读取数据报。

异步IO模型

这种函数的工作机制是,告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到缓冲区)完成后通知调用进程。

这种模型与信号驱动模型的主要区别是,信号驱动式IO是由内核通知进程何时可以启动一个IO操作,而异步IO操作是由内核通知进程IO操作何时完成。进程调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小和文件偏移,并告诉内核当整个操作完成时如何通知进程。该系统调用立即返回,而且在等待IO完成埋单,进程不被阻塞。

三、IO多路复用之select, poll, epoll

select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

select 简介

函数原型

#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

/* 返回:若有就绪描述符则为其数目,超时为0,出错为-1 */

最后一个参数timeout告知内核等待所指定描述符中的任何一个就绪可花多长时间。timeval结构用于指定这段时间的秒数的微秒数。

struct timeval {
    long tv_sec;
    long tv_usec;
};

等待的时间有以下三种:

  • 永远等待下去:仅在有一个描述符准备好IO时才返回,把该参数设为空指针;
  • 等待一段固定时间:在有一个描述符准备好IO时返回,但不超过由该参数所指向的timeval结构中指定的秒数和微秒数;
  • 根本不等待:检查描述符后立即返回,即轮询。该参数指向一个timeval结构,且其中的定时器值必须为0。

注意,尽管timeval结构允许指定一个微秒级的分辨率,然而内核支持的真实分辨率往往粗糙的多。另外还有调度延迟,即定时器时间到后,内核还要一定的时间调度相应进程运行。

中间三个参数readset, writeset, exceptset指定要让内核测试读、写和异常条件的描述符。select使用描述符集,通常是一个整数数组,其中每个整数中的每一位对应一个描述符。

如果我们对某一个条件不感兴趣,就可以把它设为空指针。事实上,如果这三个指针均为空,我们就有了一个比unix的sleep函数更为精确的定时器。

maxfd1参数指定待测试的描述符个数,为待测试的最大描述符加1,此时,描述符0,1,2……直到maxfd1-1均将被测试。

调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当sselect函数返回后,可以 通过遍历fdset,来找到就绪的描述符。

需要注意的两点:

  • 头文件<sys/selet.h>中定义的FD_SETSIZE常值是数据类型fd_set中描述符总数,其值通常是1024。
  • select的第一个参数别忘了对最大描述符加1.
poll 简介

函数原型

#include <poll.h>
int poll(strut pollfd *fdarray, unsigned long nfds, int timeout);

/* 返回:若有就绪描述符则为其数目,超时为0,出错为-1 */

第一个参数指向一个结构数组第一个元素的指针。第个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。

struct pollfd {
    int fd;
    short events;
    short revents;
};

pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。

epoll 简介

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。

epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll操作需要三个接口,分别陈述如下。

int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大

这个参数不同于select中的第一个参数,它并不是限制了epoll能监听的描述符最大个数,只是对内核初始分配的内部数据结构的一个建议。

注意,epoll创建句柄后,它就会占用一个fd值,可以在/proc/pid/fd/下查看到。所以使用完epoll后务必调用close关闭它,否则可能导致fd被耗尽。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//对指定描述符fd执行op操作。
  • epfd:是epoll_create()的返回值。
  • op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
  • fd:是需要监听的fd(文件描述符)
  • epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);//等待epfd上的io事件,最多返回maxevents个事件

参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

工作模式
LT模式

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

ET模式

ET(edge-triggered)是高速工作方式,只支持no-block socket。

在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

总结

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

如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值