IO复用(模型/阻塞/同步/异步)
second60 20180209
IO5种模型?
什么是阻塞/非阻塞?
什么是轮询?
什么是同步和异步?
套接字就绪的条件?
拒绝攻型服务?
IO复用:内核一旦发现进程指定的一个或多个IO条件就绪,就通知进程。
使用场景:
1. 当客户处理多个描述符(交互输入和网络套接字)时,必须使用IO复用。
2. 一个客户同时处理多个套接字
3. 如果一个TCP服务器既要处理监听套接字,又要处理已连接套接字。
4. 如果一个服务器即要处理TCP,又要处理UDP
5. 如果一个服务器要处理多个服务或多个协议。
---------------------------------------------------------------------------------------------
I/O模型
阻塞式IO
非阻塞式IO
IO复用
信号驱动式IO
异步IO
一个输入操作的两个阶段:(必记,很重要,5种IO模型理解靠这个)
1. 等待数据准备好
2. 从内核向进程复制数据
对于一个套接字的输入操作:
1. 等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区
2. 把数据从内核缓冲区复制到应用进程缓冲区
---------------------------------------------------------------------------------------------
阻塞式IO模型(重点)
blocking IO模型,默认的情形下,所有套接字都是阻塞的。
对UDP而言:要么整个数据报已接收,要么还没有。
对TCP而言:套接字低水位标记low-water mark等额外变量开始起作用。
系统调用:一般都会从应用进程空间中运行切换在内核空间中运行,一段时间之后再切换回来。
进程调用recvfrom,其系统调用直到数据报到达且被复制到应用进程的缓冲区或者发生错误才返回。进程从调用recvfrom开始到它返回的整段时间内是被阻塞的。recvfrom成功返回后,应用进程开始处理数据块。
---------------------------------------------------------------------------------------------
非阻塞式IO模型(重点)
进程把一个套接字设置成非阻塞是在通知内核:当所请求的IO操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。nonblocking IO.
设置成非阻塞后,前三次调用recvfrom时没有数据可返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。
当一个应用进程像这样对一个非阻塞描述符循环调用recvfrom时,我们称之为轮询polling。应用进程持续轮询内核,以查看某个操作是否就绪。这么做往往耗费大量CPU时间。
---------------------------------------------------------------------------------------------
IO复用模型(重点)
有了IO复用IO multiplexing, 就可以调用select/poll/epoll,阻塞在这两个系统调用上,而不是阻塞在真正的IO系统调用上。
我们阻塞于select调用,等待数据报套接字变为可读。当select返回套接字可读这一条件时,我们调用recvfrom把所读数据报复制到应用进程缓冲区。
使用select需要两个而不是单个系统调用,IO复用还稍有劣势。使用select的优势在于我们可以等待多个描述符就绪。
---------------------------------------------------------------------------------------------
信号驱动式IO模型
我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式IO(signal-driven IO)。
首先开启套接字的信号驱动式IO功能,并通过sigaction系统调用安装一个信号处理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。
无论如何处理SIGIO信号,优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被读取。
---------------------------------------------------------------------------------------------
异步IO模型(重点)
异步IO(asynchronous IO)工作机制:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到进程缓冲区)完成后通知我们。
与信号驱动区别:
信号驱动IO是由内核通知我们何时可以启动一个IO操作,而异步IO模型是内核通知我们IO操作何时完成。
我们调用aio_read函数(POSIX异步IO函数以aio_或lio_开头), 给内核传递描述符,缓冲区指针,缓冲区大小(与read相同)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。
该系统调用立即返回,而且在等待IO完成期间,我们的进程不被阻塞。本例中假设要求内核在操作完成时产生某个信号。该信号直到数据已复制到就用进程缓冲区才产生,这一点不同于信号驱动IO。
---------------------------------------------------------------------------------------------
各种IO模型的比较
前四种都是同步IO模型
最后一种是异常IO模型。
前四种的区别在于第一阶段,第二阶段都一样:在数据从内核复制到调用者的缓冲区期间,阻塞。
同步和异步的区别在于:同步会阻塞,异步不会阻塞。
同步IO操作(synchronous IO operation)导致请求进程阻塞,直到IO操作完成。
异步IO操作(asynchronous IO operation)不导致请求进程阻塞。
同步IO模型:阻塞IO模型,非阻塞IO模型,IO复用模型,信号驱动模型。
异步IO模型:异步IO模型
-------------------------------------------------------------------------------------------
select函数
该函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个事件发生或经历一段指定时间后才唤醒它。
调用select告知内核对哪些描述符(可读,可写或异常条件)感兴趣以及等待多长时间。
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set *readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);
若有就续描述符则返其数目,若超时返回0,若出错返回-1
timeout,它告知内核等待所指定描述符中的任何一个就绪可花多长时间。
struct timeval{
long tv_sec;
long tv_usec;
};
这个参数有三种可能:
1. 永远等待下去:仅在有一个描述符准备好IO时才返回。设为NULL。
2. 等待一段固定时间:在有一个描述符准备好IO时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
3. 根本不等待:检查描述符后立即返回,这称为轮询polling. 传0。
中间的三个参数readset, writeset,exceptset指定我们要让内核测试读,写和异常的条件描述符。如果不感兴趣,可以设为空指针,如果三个都为空,可以做精确定时器。
void FD_ZERO(fd_set* fdset);
void FD_SET(int fd, fd_set *fdset);
void FD_CLR(int fd,fd_set *fdset);
void FD_ISSET(int fd, fd_set* fdset);
fd_set数据类型的描述符集,并用这些宏设置或测试该集合中的每一位。
maxfdp1迫使我们计算出所关心的最大描述符并告知内核该值。
---------------------------------------------------------------------------------------------
引起套接字就绪的条件(必记)
1. 满足下列四个条件中的任何一个时,一个套接字准备好读。
a. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水平位标记的当前大小。对这样的套接字执行读操作不会阻塞并将返回一个大于0的值(准备好读入的数据)
b. 该连接的读半部关闭(即接收了FIN分节的TCP连接)。对这样的套接字的读操作不阻塞并返回0(也就是返回EOF)
c. 该套接字是一个监听连接字且已完成连接数不为0。(accept不阻塞,返回连接)
d. 其上有一个套接字错误待处理。对这样的套接字的读操作将不阻塞并返回-1(返回一个错误),同时把errno设置成确切的错误的条件。这些待处理错误也可以通过指定SO_ERROR选项调用getsockopt获取并清除。
2.下列四个条件中任何一个满足,一个套接字准备好写
a. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或该套接字不需要连接(UDP)。这意味着如果我们把套接字调置成非阻塞,写操作将不阻塞并返回一个正值(传输层接受的字节数)。可使用SO_SNDLOWAT选项设置该套接字的低水位标记。对TCP/UDP来说默认2048.
b. 该连接的写半部关闭。对这样的套接字的写操作将产生SIGPIPE信号。shutdown
c. 使用非阻塞式connect套接字已建立连接,或connect已经以失败告终。
d. 其上有一个套接字错误待处理。对这样的套接字写操作不阻塞并返回-1,并设置errno,可指定SO_ERROR选项调用getsockopt获取并清除.
3. 异常条件:如果一个套接字存在带外数据或仍处理带外标记。
接收低水位标记和发送低水位标记的目的在于:允许应用进程控制在select返回可读或可写条件之前有多少数据可读或有多大空间可用于写。
任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小(默认总是这关系)就总是可写的,这是因为UDP套接字不需要连接。
select的最大描述符数:<sys/types.h>
内核的FD_SETSIZE作为上限
---------------------------------------------------------------------------------------------
shutdown函数
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
成功返0,失败返-1
howto参值:
SHUT_RD: 关闭读,套接字中不再有数据可接收,且接收缓冲区现有的数被丢弃.不能再读.
SHUT_WR: 关闭写,半关闭half-close.当前留在套接字发送缓冲区的数据被发送掉,后跟TCP正常连接终止序列。不管套接字引用计数是否为0,不再可写。
SHUT_RDWR: 先调SHUT_RD,再调SHUT_WR
终止网络连接通常用close函数。不过close有两个限制,却可使用shutdown来避免.
1. close把描述符的引用计数减1,仅在该计数为0时才关闭套接字。而shutdown是不管引用计数就激激发TCP的正常连接终止序列止。
2. close终止读和写两个方向的数据传送。而shutdown可以半关闭
close的操作取决于SO_LINGER套接字选项的值。
---------------------------------------------------------------------------------------------
拒绝服务型攻击
当一个服务器在处理多个客户时,它绝于不能阻塞于只与单个客户相关的某个函数调用。否则可能导致服务器被持起,拒绝为所有其他客户提供服务。这是所谓的拒绝服务(denial of service)型攻击。
它就是针对服务器做些动作,导致服务器不再能为其他合法客户提供服务。
解决办法:
1. 使用非阻塞式IO
2. 让每个客户由单独的控制线程提供服务(如创建一个子进程或线程来服务每个客户)
3. 对IO操作设置一个超时
---------------------------------------------------------------------------------------------
poll函数
与select类似。
#include <poll.h>
int poll(struct polldf *fdarray, unsigned long nfds, int timeout);
成功返回就绪描述符数目,超时返回0, 出错返回-1
第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述符fd的条件。
struct pollfd{
int fd;
short events; // 调用值
short revents; // 返回结果
};
(注:本章内容出自《unix网络编程卷一》第六章,温故而知新,方便后面查阅)