Linux 网络IO

Linux 网络IO是一个非常庞大的话题,但在学习网络编程的路上是一道绕不够去的坎

但是不要把磁盘IO和网络IO记混了。

 

目录

 

IO操作的过程

 I/O操作中的阻塞、非阻塞、同步、异步:

linux五种网络IO模型

1、阻塞I/O模型

2、非阻塞I/O模型

3、I/O复用模型

4、信号驱动I/O模型

5、异步I/O模型

 

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

select

poll

epoll

总结:


IO操作的过程

通过系统给我们的API函数调用I/O操作(此时在用户态),这个函数先等待I/O缓存中有数据,操作系统会监测I/O缓存(此时在内核态),当I/O缓存中接收到数据时,操作系统先通知这个函数调用(进入用户态),然后这个函数的实现中会调用一些已经包装好的函数,“ 这些 ”函数会把数据从内核中拷贝到用户缓存区(先到内核态,再到用户态),然后这个函数就会返回。

 

所以,整体上看I/O操作是由两个过程组成,一是等待数据,二是把数据从内核中搬到用户空间。

即将介绍的I/O模型的不同之处是在等待数据的方式上,在第二个过程中是一样的,都是把数据从内核中搬到用户空间,然后进行相关操作。

 I/O操作中的阻塞、非阻塞、同步、异步:

(1)同步:同步就是在一个功能调用时,在这个调用没有得到结果之前,这个调用不会返回。

         在I/O操作中,如调用read函数,这个函数会被阻塞在read函数调用处,但内核一直在做与read相关的事情,也就是这个函数是激活的(会占用cpu),只是从表面上看函数还没有返回而已,而write操作是异步的,数据从用户缓冲区写到内核之后就返回了,内核向磁盘同步的过程是异步进行的。

(2)阻塞:阻塞调用就是调用一个函数,这个调用结果没有得到结果之前,执行这个函数的线程会被挂起(这个状态下,cpu不会给该函数分配时间片),直到得到结果后才返回。

(3)非阻塞:是指如果一个函数不能立刻得到结果,这个函数不会阻塞该线程而立即返回。

(4)异步:是指在一个功能调用时,这个调用不会立即得到结果,也不会阻塞该线程,这个调用所在的线程会继续执行其它事情,而这个功能是由其它执行部件来完成,当这个部件执行完该功能时,通过状态,通知来通知调用者,或通过回调函数处理这个调用。

执行部件和调用者可以通过三种途径返回结果:

a. 如果执行部件用状态来通知,那么调用者就需要每隔一定时间检查一次,效率就很低,例如轮询。

b. 如果是使用通知的方式,效率则很高,因为执行部件几乎不需要做额外的操作。

c. 回调函数,利用函数是指针的本质特性,将最后的操作指向需要回调的函数,操作完成后根据指向关系来调用。

linux五种网络IO模型
 

1、阻塞I/O模型

最流行的I/O模型是阻塞I/O(blocking I/O)模型。

以数据报套接口作为例子,我们有下图所示的情形:进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回。最常见的错误是系统调用被信号中断。我们说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据。

 

 

2、非阻塞I/O模型

进程把一个套接口设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。下图展示了非阻塞I/O模型。

前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有数据报准备好,它被拷贝到应用进程缓冲区,recvfrom于是成功返回。我们接着处理数据。

当一个应用进程像这样对一个非阻塞描述字循环调用recvfrom时,我们称之为轮询(polling)。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间,不过这种模型偶尔也会遇到,通常是在只专门提供某种功能的系统中才有。

 

3、I/O复用模型

有了I/O复用(I/O multiplexing),我们就可以调用select或poll,阻塞在这两个系统调用中的某一个之上,而不是阻塞在真正的I/O系统调用上。下图展示了I/O复用模型。

为select设置一组套接字,如果这些套接字有一个以上出现了可读、可写、或者异常,select都会返回,使用select的好处是:能够等待多个套接字准备好。我们阻塞于select调用,等待数据报套接口变为可读。当select返回套接口可读这一条件时,我们调用recvfrom把所读数据报拷贝到应用进程缓冲区。通常程序的做法是,对不同的套接字,进行分开处理。

使用select需要使用两个系统调用:使用select的可以等待多个描述字就绪,如果有任何一个fd就绪,select或poll就会返回,这个时候用户进程再调用recvfrom,将数据从内核缓冲区拷贝到用户进程空间。

但是select仅仅知道有I/O事件发生,但却并不知道是那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长.Linux还提供了一个epoll系统调用,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的(复杂度降低到了O(1))。文章后面我们会介绍。

所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)

4、信号驱动I/O模型

我们也可以用信号,让内核在描述字就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动I/O(signal-driven I/O),如

首先开启套接口的信号驱动I/O功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用立即返回,进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。

无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间,进程不被阻塞。主循环可以继续执行,只等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。

5、异步I/O模型

异步I/O的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到我们自己的缓冲区)完成后通知我们。这种模型与信号驱动模型的主要区别在于:信号驱动I/O是由内核通知我们何时启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。常见的使用场景例如:在Java的AIO中,采用callback回调函数的方式,数据读取完成后调用提前写好的回调函数去处理数据。

 

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

Linux 的内核将所有外部设备都看做一个文件来操作(一切皆文件),对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有响应的描述符,称为socketfd(socket文件描述符),描述符就是一个数字,指向内核中的一个结构体(文件路径,数据区等一些属性)。

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

select

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

select的调用过程如下所示:

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(schedule_timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间。

select的几大缺点:

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

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

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

poll

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,其他的都差不多。

epoll

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

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

  对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和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内部定义的等待队列)。这也能节省不少的开销。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值