I/O多路复用:
在多个网络连接中,共用少数几个进程或线程。
支持IO多路复用的进程,需要先通知内核进行监控,等待内核监控到指定的一个或者多个I/O条件就绪(输入准备好被读取,或者文件描述符已经能承受更多的输出),它就通知进程。
5种I/O模型基本区别
输入操作包括两个阶段:
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作,1. 等待数据从网络中到达。当所有等待分组到达时,它被复制到内核中的某个缓冲区。2. 将数据从内核缓冲区复制到应用进程缓冲区。
阻塞式I/O模型
阻塞式I/O模型(blocking I/O),默认情况下所有套接字都是阻塞的。
非阻塞式I/O模型
非阻塞式I/O模型(nonblocking I/O),进程将一个套接字设置成非阻塞是在通知内核,当所请求的I/O操作非得将本进程投入睡眠才能完成时,不要将本进程投入睡眠。
前三次调用recvfrom时没有数据返回,因此内核转而立即返回一个EWOULDBLOCK错误。第四次调用recvfrom时已有一个数据报准备好,它被复制到应用进程缓冲区,于是recvfrom成功返回。
轮询:一个应用进程对一个非阻塞描述符循环使用recvfrom。也就是,进程持续轮询内核,查看某个操作是否准备就绪。
I/O复用模型
I/O复用(I/O multiplexing),阻塞在系统调用select或者poll之上,而不是阻塞在真正的I/O系统调用之上。
阻塞select调用,等待数据报套接字变为可读。当select返回套接字可读时,调用recvfrom把所读数据报复制到应用进程缓冲区。
信号驱动式I/O模型
信号驱动式I/O(signal-driven I/O),让内核在描述符就绪时发送SIGIO信号通知给进程。
首先开启套接字的信号驱动式I/O功能,并且通过sigaction系统调用安装一个信号处理函数。此系统调用将立即返回,进程继续工作,没有阻塞情况发生。当数据报准备好读取时,内核就为进程产生一个SIGIO信号。随后,可以在信号处理中调用recvfrom读取数据报,并通知主循环数据已经准备好待处理,也可以立即通知主循环,让它读取数据报。
异步I/O模型
异步I/O(asynchronous I/O),告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到自己的缓冲区)完成后通知进程。与信号模型的主要区别是:信号驱动式I/O是由内核通知何时启动一个I/O操作,异步I/O模型是由内核通知操作I/O何时完成。
调用aio_read函数,给内核传递描述符,缓冲区指针,缓冲区大小和文件偏移,并通知内核当整个操作完成时如何通知进程。该系统调用立即返回,而且等待I/O期间,进程不会被阻塞。
各种I/O模型比较
前4中模型的主要区别在等待数据的处理中,因为在将数据从内核复制到用户空间的过程是相同的。在数据从内核复制到调用者的缓冲区期间,进程将阻塞于recvfrom调用。
同步I/O和异步I/O对比
- 同步I/O操作(synchronous I/O operation)导致请求进程阻塞,直到I/O操作完成;
- 异步I/O操作(asynchronous I/O operation)不导致请求阻塞。
select函数
select函数允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或者多个事件发生或者经历一段指定的时间后唤醒它。
select实际是通知内核,对某些描述符(读,写或者异常条件)感兴趣以及等待多长时间。描述符不仅限于socket,任何描述符select都可以使用
利用man 2 select
查看select
函数的具体应用。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, strcut timeval *timeout);
strcut timeval *timeout
timeout
告知啮合等待所指定描述符中的任何一个就绪可花多长时间。其中,timveval结构用于指定这段时间的秒数和微秒数。
struct timeval{
long tv_sec;
long tv_usec;
}
3种可能情况:
2. 永远等待下去:仅在一个描述符准备好I/O时返回,为此,可以将参数设置为空指针。
3. 等待一段时间。
4. 根本不等待:检查描述符后立即返回,称为轮询(polling)
readset
、 writeset
、exceptset
readset
、 writeset
、exceptset
分别为读,写,异常的描述符。??select使用描述符集,通常是一个整数数组,其中每个整数的每一位对应一个描述符。
具体地,使用细节为:
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
分配一个fd_set
数据类型的描述符集,并可以利用宏设置和测试集合中的每一位。首先,需要初始化自动变量分配的一个描述符集合。
fd_set rset;
FD_ZERO(&rset); // 初始化集合,每一位设置为off
FD_SET(1, &rset); // turn on bit for fd1
FD_SET(4, &rset); // turn on bit for fd4
FD_SET(5, &rset); // turn on bit for fd5
maxfdp1
maxfdp1
参数指定待测试的描述符个数,它的值是待测试的最大描述符加1。以上面1, 4和5为例,maxfdp1值就是6,是6而不是5的原因是:指定的是描述符的个数而不是最大值,描述符是从0开始的。
select函数修改由指针readset、writeset和exceptset所指向的描述符集合,所以这3个参数都是值-结果参数。调用此函数时,需要指定所关心的描述符的值,**函数返回时,将指示哪些描述符已经就绪。**该函数返回后,使用FD_ISSET宏来测试fd_set数据类型中的描述符。**描述符集合内任何与为就绪描述符对应的位返回时均置为0。**所以,每次重新调用select函数时,都得再次把所有描述符集合内所关心的位置为1。
描述符就绪条件
等待某个描述符准备好I/O:
对于普通文件的即是读写。
对于socket的“就绪”:
select的最大文件描述符
str_cli函数
客户端的套接字上的三个处理条件:
- 对端TCP 发送数据,那么套接字变为可读,并且read返回一个大于0的值(即读入数据的字节数)。
- 对端TCP 发送一个FIN(对端进程终止),那么套接字变为可读,并且read返回0(EOF)。
- 对端TCP 发送 一个RST (对端主机奔溃并重新启动),该套接字变为可读,并且read返回-1,而errno中含有确切的错误码。
str_cli函数摘录
#include "unp.h"
void str_cli(FILE *fd, int sockfd){
int maxfdp1;
fd_set rset;
char sendline[MAXLINE], recvline[MAXLINE];
/* 检查可读性的描述符集合。*/
FD_ZERO(&rset); // 描述符集合初始化
for( ; ; ) {
FD_SET(fileno(fp), &rset); // 打开标准I/O文件指针fp
FD_SET(sockfd, &rset); // 打开套接字sockfd
maxfd1 = max(fileno(fd), sockfd) + 1; // fileno将fd转换为文件描述符的值
select(maxfd1, &rset, NULL, NULL, NULL); // 调用select阻塞,直到某个文件描述符就绪
/ *处理可读的套接字* /
if (FD_ISSET(sockfd, &rset)) { // 如果socket可读
if (Readline(sockfd, recvline, MAXLINE) == 0) // readline读入回射文本行
err_quit("str_cli: server terminated prematurely");
Fputs(recvline, stdout); // fputs输出
}
/* 处理标准读入 */
// 如果标准读入可读, 将先用fgets读入一行文本, 再用writen将它写入到套接字中。
if (FD_ISSET(fileno(fd), &rset)) { /* input is readable */
if (Fgets(sendline, MAXLINE, fd) == NULL )
return; /* all done */
Writen(sockfd, sendline, strlen(sendline));
}
}
}
此处的代码是利用fgets
、writen
、readline
、fputs
。它们在本函数中的驱动流发生了变化,新版本的是由select
来调用驱动,而之前的是由fgets
调用来驱动。