Linux系统I/O模型及select、poll、epoll原理和应用

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux系统I/O模型及select、poll、epoll原理和应用相关的知识,希望对你有一定的参考价值。

参考技术A

理解Linux的IO模型之前,首先要了解一些基本概念,才能理解这些IO模型设计的依据

操作系统使用虚拟内存来映射物理内存,对于32位的操作系统来说,虚拟地址空间为4G(2^32)。操作系统的核心是内核,为了保护用户进程不能直接操作内核,保证内核安全,操作系统将虚拟地址空间划分为内核空间和用户空间。内核可以访问全部的地址空间,拥有访问底层硬件设备的权限,普通的应用程序需要访问硬件设备必须通过 系统调用 来实现。

对于Linux系统来说,将虚拟内存的最高1G字节的空间作为内核空间仅供内核使用,低3G字节的空间供用户进程使用,称为用户空间。

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

这种方式很明显的缺点就是数据传输过程中需要再应用程序地址空间和内核空间进行多次数据拷贝操作,这些操作带来的CPU以及内存的开销是非常大的。

由于Linux系统采用的缓存I/O模式,对于一次I/O访问,以读操作举例,数据先会被拷贝到内核缓冲区,然后才会从内核缓冲区拷贝到应用程序的缓存区,当一个read系统调用发生的时候,会经历两个阶段:

正是因为这两个状态,Linux系统才产生了多种不同的网络I/O模式的方案

Linux系统默认情况下所有socke都是blocking的,一个读操作流程如下:

以UDP socket为例,当用户进程调用了recvfrom系统调用,如果数据还没准备好,应用进程被阻塞,内核直到数据到来且将数据从内核缓冲区拷贝到了应用进程缓冲区,然后向用户进程返回结果,用户进程才解除block状态,重新运行起来。

阻塞模行下只是阻塞了当前的应用进程,其他进程还可以执行,不消耗CPU时间,CPU的利用率较高。

Linux可以设置socket为非阻塞的,非阻塞模式下执行一个读操作流程如下:

当用户进程发出recvfrom系统调用时,如果kernel中的数据还没准备好,recvfrom会立即返回一个error结果,不会阻塞用户进程,用户进程收到error时知道数据还没准备好,过一会再调用recvfrom,直到kernel中的数据准备好了,内核就立即将数据拷贝到用户内存然后返回ok,这个过程需要用户进程去轮询内核数据是否准备好。

非阻塞模型下由于要处理更多的系统调用,因此CPU利用率比较低。

应用进程使用sigaction系统调用,内核立即返回,等到kernel数据准备好时会给用户进程发送一个信号,告诉用户进程可以进行IO操作了,然后用户进程再调用IO系统调用如recvfrom,将数据从内核缓冲区拷贝到应用进程。流程如下:

相比于轮询的方式,不需要多次系统调用轮询,信号驱动IO的CPU利用率更高。

异步IO模型与其他模型最大的区别是,异步IO在系统调用返回的时候所有操作都已经完成,应用进程既不需要等待数据准备,也不需要在数据到来后等待数据从内核缓冲区拷贝到用户缓冲区,流程如下:

在数据拷贝完成后,kernel会给用户进程发送一个信号告诉其read操作完成了。

是用select、poll等待数据,可以等待多个socket中的任一个变为可读,这一过程会被阻塞,当某个套接字数据到来时返回,之后再用recvfrom系统调用把数据从内核缓存区复制到用户进程,流程如下:

流程类似阻塞IO,甚至比阻塞IO更差,多使用了一个系统调用,但是IO多路复用最大的特点是让单个进程能同时处理多个IO事件的能力,又被称为事件驱动IO,相比于多线程模型,IO复用模型不需要线程的创建、切换、销毁,系统开销更小,适合高并发的场景。

select是IO多路复用模型的一种实现,当select函数返回后可以通过轮询fdset来找到就绪的socket。

优点是几乎所有平台都支持,缺点在于能够监听的fd数量有限,Linux系统上一般为1024,是写死在宏定义中的,要修改需要重新编译内核。而且每次都要把所有的fd在用户空间和内核空间拷贝,这个操作是比较耗时的。

poll和select基本相同,不同的是poll没有最大fd数量限制(实际也会受到物理资源的限制,因为系统的fd数量是有限的),而且提供了更多的时间类型。

总结:select和poll都需要在返回后通过轮询的方式检查就绪的socket,事实上同时连的大量socket在一个时刻只有很少的处于就绪状态,因此随着监视的描述符数量的变多,其性能也会逐渐下降。

epoll是select和poll的改进版本,更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll_create()用来创建一个epoll句柄。
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个就绪链表中管理。
epoll_wait() 可以从就绪链表中得到事件完成的描述符,因此进程不需要通过轮询来获得事件完成的描述符。

当epoll_wait检测到描述符IO事件发生并且通知给应用程序时,应用程序可以不立即处理该事件,下次调用epoll_wait还会再次通知该事件,支持block和nonblocking socket。

当epoll_wait检测到描述符IO事件发生并且通知给应用程序时,应用程序需要立即处理该事件,如果不立即处理,下次调用epoll_wait不会再次通知该事件。

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

【segmentfault】 Linux IO模式及 select、poll、epoll详解
【GitHub】 CyC2018/CS-Notes

Linux I/O复用中select poll epoll模型的介绍及其优缺点的比較

关于I/O多路复用:

I/O多路复用(又被称为“事件驱动”),首先要理解的是。操作系统为你提供了一个功能。当你的某个socket可读或者可写的时候。它能够给你一个通知。这样当配合非堵塞的socket使用时,仅仅有当系统通知我哪个描写叙述符可读了,我才去运行read操作。能够保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作相似。操作系统的这个功能通过select/poll/epoll之类的系统调用来实现。这些函数都能够同一时候监视多个描写叙述符的读写就绪状况,这样。**多个描写叙述符的I/O操作都能在一个线程内并发交替地顺序完毕,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

一、I/O复用之select

1、介绍:
select系统调用的目的是:在一段指定时间内。监听用户感兴趣的文件描写叙述符上的可读、可写和异常事件。poll和select应该被归类为这种系统调用,它们能够堵塞地同一时候探測一组支持非堵塞的IO设备,直至某一个设备触发了事件或者超过了指定的等待时间——也就是说它们的职责不是做IO,而是帮助调用者寻找当前就绪的设备。
以下是select的原理图:
技术分享图片

2、select系统调用API例如以下:

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

fd_set结构体是文件描写叙述符集,该结构体实际上是一个整型数组,数组中的每一个元素的每一位标记一个文件描写叙述符。fd_set能容纳的文件描写叙述符数量由FD_SETSIZE指定。普通情况下,FD_SETSIZE等于1024,这就限制了select能同一时候处理的文件描写叙述符的总量。

3、以下介绍一下各个參数的含义:
1)nfds參数指定被监听的文件描写叙述符的总数。

通常被设置为select监听的全部文件描写叙述符中最大值加1;
2)readfds、writefds、exceptfds分别指向可读、可写和异常等事件相应的文件描写叙述符集合。这三个參数都是传入传出型參数,指的是在调用select之前,用户把关心的可读、可写、或异常的文件描写叙述符通过FD_SET(以下介绍)函数分别加入进readfds、writefds、exceptfds文件描写叙述符集,select将对这些文件描写叙述符集中的文件描写叙述符进行监听,假设有就绪文件描写叙述符,select会重置readfds、writefds、exceptfds文件描写叙述符集来通知应用程序哪些文件描写叙述符就绪。这个特性将导致select函数返回后。再次调用select之前,必须重置我们关心的文件描写叙述符,也就是三个文件描写叙述符集已经不是我们之前传入 的了。
3)timeout參数用来指定select函数的超时时间(以下讲select返回值时还会谈及)。

struct timeval
{
    long tv_sec;        //秒数
    long tv_usec;       //微秒数
};

4、以下几个函数(宏实现)用来操纵文件描写叙述符集:

void FD_SET(int fd, fd_set *set);   //在set中设置文件描写叙述符fd
void FD_CLR(int fd, fd_set *set);   //清除set中的fd位
int  FD_ISSET(int fd, fd_set *set); //推断set中是否设置了文件描写叙述符fd
void FD_ZERO(fd_set *set);          //清空set中的全部位(在使用文件描写叙述符集前。应该先清空一下)
    //(注意FD_CLR和FD_ZERO的差别,一个是清除某一位,一个是清除全部位)

5、select的返回情况:
1)假设指定timeout为NULL,select会永远等待下去,直到有一个文件描写叙述符就绪,select返回。
2)假设timeout的指定时间为0,select根本不等待,马上返回;
3)假设指定一段固定时间,则在这一段时间内,假设有指定的文件描写叙述符就绪,select函数返回,假设超过指定时间,select同样返回。
4)返回值情况:
a)超时时间内,假设文件描写叙述符就绪,select返回就绪的文件描写叙述符总数(包含可读、可写和异常),假设没有文件描写叙述符就绪,select返回0;
b)select调用失败时,返回 -1并设置errno。假设收到信号。select返回 -1并设置errno为EINTR。

6、文件描写叙述符的就绪条件:
在网络编程中,
1)下列情况下socket可读:
a) socket内核接收缓冲区的字节数大于或等于其低水位标记SO_RCVLOWAT;
b) socket通信的对方关闭连接,此时该socket可读,可是一旦读该socket。会马上返回0(能够用这种方法推断client端是否断开连接);
c) 监听socket上有新的连接请求。
d) socket上有未处理的错误。


2)下列情况下socket可写:
a) socket内核发送缓冲区的可用字节数大于或等于其低水位标记SO_SNDLOWAT;
b) socket的读端关闭。此时该socket可写。一旦对该socket进行操作。该进程会收到SIGPIPE信号。
c) socket使用connect连接成功之后;
d) socket上有未处理的错误。

二、I/O复用之poll

1、poll系统调用的原理与原型和select基本相似。也是在指定时间内轮询一定数量的文件描写叙述符。以測试当中是否有就绪者。

2、poll系统调用API例如以下:

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

3、以下介绍一下各个參数的含义:
1)第一个參数是指向一个结构数组的第一个元素的指针,每一个元素都是一个pollfd结构,用于指定測试某个给定描写叙述符的条件。

struct pollfd
{
    int fd;             //指定要监听的文件描写叙述符
    short events;       //指定监听fd上的什么事件
    short revents;      //fd上事件就绪后,用于保存实际发生的时间
}。

待监听的事件由events成员指定,函数在相应的revents成员中返回该描写叙述符的状态(每一个文件描写叙述符都有两个事件,一个是传入型的events,一个是传出型的revents。从而避免使用传入传出型參数。注意与select的差别),从而告知应用程序fd上实际发生了哪些事件。events和revents都能够是多个事件的按位或。
2)第二个參数是要监听的文件描写叙述符的个数,也就是数组fds的元素个数;
3)第三个參数意义与select同样。

4、poll的事件类型:
技术分享图片
在使用POLLRDHUP时,要在代码開始处定义_GNU_SOURCE

5、poll的返回情况:
与select同样。

三、I/O复用之epoll

1、介绍:
epoll 与select和poll在使用和实现上有非常大差别。

首先,epoll使用一组函数来完毕,而不是单独的一个函数。其次。epoll把用户关心的文件描写叙述符上的事件放在内核里的一个事件表中。无须向select和poll那样每次调用都要反复传入文件描写叙述符集合事件集。

2、创建一个文件描写叙述符,指定内核中的事件表:

#include<sys/epoll.h>
int epoll_create(int size);
    //调用成功返回一个文件描写叙述符。失败返回-1并设置errno。

size參数并不起作用。仅仅是给内核一个提示。告诉它事件表须要多大。

该函数返回的文件描写叙述符指定要訪问的内核事件表,是其它全部epoll系统调用的句柄。

3、操作内核事件表:

#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    //调用成功返回0,调用失败返回-1并设置errno。

epfd是epoll_create返回的文件句柄。标识事件表。op指定操作类型。

操作类型有以下3种:

a)EPOLL_CTL_ADD, 往事件表中注冊fd上的事件;
b)EPOLL_CTL_MOD, 改动fd上注冊的事件;
c)EPOLL_CTL_DEL, 删除fd上注冊的事件。

event參数指定事件,epoll_event的定义例如以下:

struct epoll_event
{
    __int32_t events;       //epoll事件
    epoll_data_t data;      //用户数据
};

typedef union epoll_data
{
    void *ptr;
    int  fd;
    uint32_t u32;
    uint64_t u64;
}epoll_data;

在使用epoll_ctl时,是把fd加入、改动到内核事件表中,或从内核事件表中删除fd的事件。

假设是加入事件到事件表中,能够往data中的fd上加入事件events。或者不用data中的fd,而把fd放到用户数据ptr所指的内存中(由于epoll_data是一个联合体。仅仅能使用当中一个数据),再设置events。

3、epoll_wait函数
epoll系统调用的最关键的一个函数epoll_wait,它在一段时间内等待一个组文件描写叙述符上的事件。

#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    //函数调用成功返回就绪文件描写叙述符个数,失败返回-1并设置errno。

timeout參数和select与poll同样。指定一个超时时间;maxevents指定最多监听多少个事件。events是一个传出型參数。epoll_wait函数假设检測到事件就绪,就将全部就绪的事件从内核事件表(epfd所指的文件)中复制到events指定的数组中。

这个数组用来输出epoll_wait检測到的就绪事件,而不像select与poll那样。这也是epoll与前者最大的差别,下文在比較三者之间的差别时还会说到。

四、三组I/O复用函数的比較

同样点:
1)三者都须要在fd上注冊用户关心的事件。
2)三者都要一个timeout參数指定超时时间。
不同点:
1)select:
a)select指定三个文件描写叙述符集,各自是可读、可写和异常事件,所以不能更加仔细地区分全部可能发生的事件。
b)select假设检測到就绪事件,会在原来的文件描写叙述符上改动,以告知应用程序,文件描写叙述符上发生了什么时间,所以再次调用select时,必须先重置文件描写叙述符
c)select採用对全部注冊的文件描写叙述符集轮询的方式,会返回整个用户注冊的事件集合,所以应用程序索引就绪文件的时间复杂度为O(n)。
d)select同意监听的最大文件描写叙述符个数通常有限制。通常是1024。假设大于1024,select的性能会急剧下降;
e)仅仅能工作在LT模式。

2)poll:
a)poll把文件描写叙述符和事件绑定,事件不但能够单独指定。并且能够是多个事件的按位或。这样更加细化了事件的注冊,并且poll单独採用一个元素用来保存就绪返回时的结果,这样在下次调用poll时。就不用重置之前注冊的事件;
b)poll採用对全部注冊的文件描写叙述符集轮询的方式。会返回整个用户注冊的事件集合。所以应用程序索引就绪文件的时间复杂度为O(n)。
c)poll用nfds參数指定最多监听多少个文件描写叙述符和事件,这个数能达到系统同意打开的最大文件描写叙述符数目。即65535。
d)仅仅能工作在LT模式。

3)epoll:
a)epoll把用户注冊的文件描写叙述符和事件放到内核当中的事件表中。提供了一个独立的系统调用epoll_ctl来管理用户的事件,并且epoll採用回调的方式。一旦有注冊的文件描写叙述符就绪,讲触发回调函数,该回调函数将就绪的文件描写叙述符和事件复制到用户空间events所管理的内存。这样应用程序索引就绪文件的时间复杂度达到O(1)。
b)epoll_wait使用maxevents来制定最多监听多少个文件描写叙述符和事件,这个数能达到系统同意打开的最大文件描写叙述符数目,即65535。
c)不仅能工作在LT模式,并且还支持ET高效模式(即EPOLLONESHOT事件,读者能够自己查一下这个事件类型,对于epoll的线程安全有非常好的帮助)。

select/poll/epoll总结:
技术分享图片

















































以上是关于Linux系统I/O模型及select、poll、epoll原理和应用的主要内容,如果未能解决你的问题,请参考以下文章

vSocket模型详解及select应用详解

Linux系统I/O操作与零拷贝

Linux磁盘I/O子系统

linux下select函数详解及实例

系统调优

Linux系统I/O模型详解