网络编程之IO多路复用

Posted Chris_166

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络编程之IO多路复用相关的知识,希望对你有一定的参考价值。

目录

一. 同步与阻塞

1.1 同步阻塞

1.2 同步非阻塞

1.3 异步阻塞

1.4 异步非阻塞

1.5 I/O多路

二.多路复用的技术

2.1 UNIX I/O Models

2.1.1 blocking I/O

2.1.2 nonblocking I/O

2.1.3 I/O Multiplexing Model

2.1.4 SIGIO

2.1.5 asynchronous I/O

2.2 IO多路复用

2.2.1 从同步阻塞到同步非阻塞

2.2.2 select

2.2.3 poll

2.2.4 epoll

REF


一. 同步与阻塞

同步是针对调用者的操作行为来说的,阻塞是针对这个行为所使用的接口来说的。

例如你是某场竞赛的主考官,需要监考考生A、B、C、D、E五位考生现场答题。你的操作就是走到每位考生面前去收试卷,而每位考生的答题情况(例如是否答完试卷)是不同的,有的可能在你走到的时候很快就交卷了,有的可能需要很长时间才能完成。

1.1 同步阻塞

到了收卷的时间,你要依次去收A,B,C,D,E考生的试卷。假说收到C考生的时候,他还未能答完试卷且他想答完题了再交给你,你又必须得等C交卷了才能去收D和E考生的试卷,那么这时候就是同步阻塞的。

对应Java中BIO(Block IO)。

1.2 同步非阻塞

到了收卷的时间,你要依次去收A,B,C,D,E考生的试卷。假说收到C考生的时候,他还未能答完试卷,你跳过C直接去收 D、E 的试卷,那么这时候就是同步非阻塞的。

1.3 异步阻塞

暂时没听说有这种场景。

1.4 异步非阻塞

你收卷的时候不用刻意去等某个学生交卷,学生交卷又迅速。

1.5 I/O多路

select/poll:考生做完了试卷,大喊了一声“我要交卷”,但是你不知道谁喊的,这时候你需要一个个地去询问,这个就是select/poll。

epoll:考生做完了试卷,大喊了一声“我要交卷”而且还举手了,你直接去收卷。

二.多路复用的技术

2.1 UNIX I/O Models

https://masterraghu.com/subjects/np/introduction/unix_network_programming_v1.3/ch06lev1sec2.html

  • blocking I/O:阻塞式I/O -- BIO

  • nonblocking I/O:非阻塞式I/O  -- NIO,AIO(AIO是在BIO的包里)

  • I/O multiplexing (select and poll):I/O复用

  • signal driven I/O (SIGIO):信号驱动式I/O

  • asynchronous I/O (the POSIX aio_functions):异步I/O  --AIO
     

UNIX I/O Models阻塞/非阻塞对应JAVA IO说明
blocking I/O阻塞BIO
nonblocking I/O非阻塞NIO
I/O multiplexing阻塞AIO和NIO的底层都是用epoll,这是JDK又进行了一层封装使之成为了非阻塞式的。
asynchronous I/O非阻塞AIO
SIGIO非阻塞

在介绍I/O models前,先对Socket的读取操作做简单说明,通常来说包括如下两个操作:

1. wait for data:等待数据从网络中到达,当数据到达后就会将器复制到内核中的某个缓冲区;

2. copy data from kernel to user:把数据从内核缓冲区复制到应用进程的缓冲区,这个过程虽然阻塞的,但是这个内存的拷贝是及其快的

2.1.1 blocking I/O

 

 由上图可知,应用进程从调用recvfrom()到它返回的这整段时间内都是阻塞的,主要阻塞在wait for data这个过程。当应用进程有返回值(return OK)的时候,应用进程已经读完数据了。

当然也可能因系统调用被信号终端导致调用发生错误。

2.1.2 nonblocking I/O

由上图可知,应用进程调用recvfrom的时候没有数据可返回则立即返回EWOULDBLOCK,而不是一直等着。

2.1.3 I/O Multiplexing Model

IO多路复用也是阻塞式的,应用进程阻塞在select调用,等待数据报套接字变为可读后,应用进程才会立即调用recvfrom读取数据。

从这里看I/O多路复用还不如BIO,因为它比BIO多一次select的系统调用,BIO只有一次recvfrom的系统调用。从后面的IO多路复用的进一步描述可知select的优势在于可以等待多个fd描述符就绪。

2.1.4 SIGIO

 非阻塞式IO。应用进程通过sigaction的系统调用告知内核在数据就绪发送SIGIO信号来通知下,这个sigaction调用完就立马返回了。等内核数据就绪后,内核在通过信号告知应用进程来取数据。

2.1.5 asynchronous I/O

AIO和SIGIO一样也是非阻塞的,与SIGIO的差别在于:SIGIO是由内核通知应用进程什么时候去启动recvfrom这个IO操作,而AIO是由内核通知应用进程I/O操作何时完成(即这时候数据已经从内核拷贝到了用户态),可以仔细对比下两张图。

2.2 IO多路复用

以TCP socket通信为例来介绍从"BIO"到"I/O多路复用"的引进过程。

2.2.1 从同步阻塞到同步非阻塞

如上示例为"单线程+BIO"。由于accept()和read()都会阻塞,所以当client1在与server交互的过程中,client2就会被阻塞住。例如client1在connect()连接握手耗时或者是client1一直在write发数据到server,这时候client2就会一直阻塞等待。

Q:那么是否可以通过多线程来解决多client被阻塞的问题呢?

A:可以但不完全可以。因为多线程可能会存在线程浪费,线程调度也是个麻烦事。例如来一个client连接就建立一个线程,如果有1000个client就得创建1000个线程,但是实际上可能只有两三个client和server端在通信,这时候就会浪费很多线程资源。

 那么我们再来看看,不阻塞会怎样呢?

 不阻塞accept(),client1调用connect()的时候client2也可以调用,当他们connect()成功了就将socket fd放到fd_list集合里,后面再去轮询这个list集合,通过系统调用去读每个fd看是否有数据到达(在上面“UNIX I/O Models”中介绍过socket数据读取的两个主要过程)。

这种不阻塞的方式虽然能解决“单个 socket 阻塞影响其他socket的问题”,但是不断的遍历,不断的进行系统调用是会有一定的开销的,特别是在没有数据到来却一直在进行系统调用的时候,这种方式的缺点表现地尤为明显。

如何优雅的解决呢?这时候就引入了I/O多路复用。

2.2.2 select

(TODO)

// 返回值 > 0,已就续的文件描述符;等于0,超时;小于0,出错 

int select(int __fd_count, fd_set* __read_fds, fd_set* __write_fds, fd_set* __exception_fds, struct timeval* __timeout);

select获取就绪事件的几个参数说明如下:

1. __fd_count:3个监听集合的文件描述符最大值+1,告诉内核查询的fd就__fd_count;

2. fd_set* __read_fds:要监听的可读文件描述符集合;

3. fd_set* __write_fds:要监听的可写文件描述符集合;

4. fd_set* __exception_fds:要监听的异常文件描述符集合

5. struct timeval* __timeout:本次调用的超时时间。如果大于0,则表示超时等待的时间;如果等于0,则表示立即去判断是否有就绪事件到来;如果小于0,则表示一直等待,直到有就绪事件到来。

select阻塞调用的实现过程:

(1) 将fds从用户态拷贝到内核空间。

(2) 内核遍历一遍fds集合。

  • 如果有就绪的fd就会把就绪的fd数目返回给用户空间;
  • 如果发现没有就绪的fd就会将当前的用户进程给阻塞起来。当客户端向服务端发送的数据到达服务端的网卡后,服务端网卡会通过DMA的方式将这个数据写到指定的内存中,处理完成之后会通过中断信号告诉CPU有新的数据到达,CPU收到中断信号后会进行响应中断处理。根据数据包中的ip/端口找到对应的socket,再将数据保存到对应的socket接收队列,然后再检查这个socket队列里是否有用户进程在阻塞等待中,如果有的话就会唤醒这个进程。用户进程唤醒之后就会再检查一遍fds集合,如果有就绪的fd,就会给这个fd打上标记然后结束阻塞返回给用户空间。

fd_set

select的返回值是int,表示有几个fd是就绪的,那么哪些fd是就绪的呢,这时候就需要fd_set了。fd_set的本质是一个使用long类型数组实现的位图。数组的size为__FD_SETSIZE / (8 * sizeof(long)) = 16,即16个long,每个long为64位,因此这个fd_set的位图最大可以表示16 * 64 = 1024位。

同一个fd_set表达了两种意思,例如:

// 入参时传入的 fd_set* __read_fds:如下表 fd为2和4的fd是传入者感兴趣想监听的

0 0 0 0 0 1 0

// 回参时这个 fd_set* __read_fds表示fd为2的已经就绪

0 0 0 0 0 0 1 0

// include/linux/types.h
typedef __kernel_fd_set		fd_set;

// include/uapi/linux/posix_types.h
#undef __FD_SETSIZE
#define __FD_SETSIZE	1024

typedef struct 
	unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))]; 
 __kernel_fd_set;
Select将socket是否就绪检查逻辑下沉到操作系统层面,避免大量系统调用。只能知道事件已就绪,但是不知道具体是哪个fd,得遍历找到这些就绪的fd
优点不需要对每个fd都进行一次系统调用,解决了频繁的用户态内核态切换问题
缺点

(1) 单进程监听的fd存在限制,默认1024;

(2) 每次调用需要将FD从用户态拷贝到内核态不知道具体是哪个文件描述符就绪,需要遍历全部文件描述符;

(3) 入参的3个fdset 集合每次调用都需要重置

2.2.3 poll

(TODO)

2.2.4 epoll

(TODO)

REF

1.《UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API》

I/O Models章节

以上是关于网络编程之IO多路复用的主要内容,如果未能解决你的问题,请参考以下文章

python 网络编程 IO多路复用之epoll

IO模型以及多路复用基本原理

socket编程:多路复用I/O服务端客户端之poll

IO多路复用之selectpollepoll

IO多路复用之select总结

unix网络编程——I/O多路复用之epoll