目录
一. 同步与阻塞
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五位考生现场答题。你的操作就是走到每位考生面前去收试卷,而每位考生的答题情况(例如是否答完试卷)是不同的,有的可能在你走到的时候很快就交卷了,有的可能需要很长时间才能完成。
到了收卷的时间,你要依次去收A,B,C,D,E考生的试卷。假说收到C考生的时候,他还未能答完试卷且他想答完题了再交给你,你又必须得等C交卷了才能去收D和E考生的试卷,那么这时候就是同步阻塞的。
对应Java中BIO(Block IO)。
到了收卷的时间,你要依次去收A,B,C,D,E考生的试卷。假说收到C考生的时候,他还未能答完试卷,你跳过C直接去收 D、E 的试卷,那么这时候就是同步非阻塞的。
暂时没听说有这种场景。
你收卷的时候不用刻意去等某个学生交卷,学生交卷又迅速。
select/poll:考生做完了试卷,大喊了一声“我要交卷”,但是你不知道谁喊的,这时候你需要一个个地去询问,这个就是select/poll。
epoll:考生做完了试卷,大喊了一声“我要交卷”而且还举手了,你直接去收卷。
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:把数据从内核缓冲区复制到应用进程的缓冲区,这个过程虽然阻塞的,但是这个内存的拷贝是及其快的。
由上图可知,应用进程从调用recvfrom()到它返回的这整段时间内都是阻塞的,主要阻塞在wait for data这个过程。当应用进程有返回值(return OK)的时候,应用进程已经读完数据了。
当然也可能因系统调用被信号终端导致调用发生错误。
由上图可知,应用进程调用recvfrom的时候没有数据可返回则立即返回EWOULDBLOCK,而不是一直等着。
IO多路复用也是阻塞式的,应用进程阻塞在select调用,等待数据报套接字变为可读后,应用进程才会立即调用recvfrom读取数据。
从这里看I/O多路复用还不如BIO,因为它比BIO多一次select的系统调用,BIO只有一次recvfrom的系统调用。从后面的IO多路复用的进一步描述可知select的优势在于可以等待多个fd描述符就绪。
非阻塞式IO。应用进程通过sigaction的系统调用告知内核在数据就绪发送SIGIO信号来通知下,这个sigaction调用完就立马返回了。等内核数据就绪后,内核在通过信号告知应用进程来取数据。
AIO和SIGIO一样也是非阻塞的,与SIGIO的差别在于:SIGIO是由内核通知应用进程什么时候去启动recvfrom这个IO操作,而AIO是由内核通知应用进程I/O操作何时完成(即这时候数据已经从内核拷贝到了用户态),可以仔细对比下两张图。
以TCP socket通信为例来介绍从"BIO"到"I/O多路复用"的引进过程。
如上示例为"单线程+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多路复用。
(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 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 集合每次调用都需要重置 |
(TODO)
(TODO)
1.《UNIX Network Programming Volume 1, Third Edition: The Sockets Networking API》
I/O Models章节