English | 中文版
[TOC]
阻塞式I/O(blocking I/O)在I/O执行的两个阶段都被阻塞了
非阻塞式I/O(nonblocking I/O)用户进程不断的主动询问kernel数据是否准备好
I/O复用(I/O multiplexing,select和poll)内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。
信号驱动式I/O(signal driven I/O,SIGIO)信号驱动用户进程读写,不需要用户不断轮询kernel。
异步I/O(asynchronous I/O)
前4种模型的主要区别在于第一阶段,因为他们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型。
- 同步
I/O操作(synchronous I/O opetation)导致请求进程阻塞,直到I/O操作完成。 - 异步
I/O操作(asynchronous I/O opetation)不导致请求进程阻塞。
TODO
#include <sys/select.h>
int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout)-
maxfdp1指定待测试的描述符最大值+1 -
readset让内核测试读描述符 -
writeset让内核测试写描述符 -
exceptset让内核测试异常描述符 -
timeout:超时时间(精确度10ms左右)空永远等待下去非空等待一段固定时间0根本不等待
-
返回值描述符数量:有就绪描述符
0:超时
-1:出错
多路复用(允许进程指示内核等待多个事件中的任何一个发生,并只在有一个或多个时间发生或经历一段指定的时间后才唤醒它)。
select支持的异常条件:
- 某个套接字的带外数据的到达;
- 某个已置为分组模式的伪终端存在可从其主端读取的控制状态信息。
套接字准备好读的条件(满足其一即可):
- 接收缓冲区中的数据字节数大于等于套接字接收缓冲区低水位标记的当前大小
- 连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作将不阻塞并返回0(也就是返回EOF)
- 是一个监听套接字且已完成的连接数不为0
- 其上有一个套接字错误待处理
套接字准备好写的条件(满足其一即可):
- 发送缓冲区中的可用空间字节数大于等于发送缓冲区低水位标记的当前大小,并且或者该套接字已连接,或者该套接字不需要连接(如UDP套接字)
- 该连接的写半部关闭
- 使用非阻塞connect的套接字已建立连接,或者connect已经以失败告终
- 其上有一个套接字错误待处理
注意:当某个套接字上发生错误时,它将由select标记为即可读又可写。
任何UDP套接字只要其发送低水位标记小于等于发送缓冲区大小(默认应该总是这种关系)就总是可写的,这是因为UDP套接字不需要连接。
select返回某个套接字就绪的条件:
| 条件 | 可读? | 可写? | 异常? |
|---|---|---|---|
| 有数据可读 关闭连接的读一半 给监听套接口准备好新连接 |
Y Y Y |
||
| 有可用于写的空间 关闭连接的写一半 |
Y Y |
||
| 待处理错误 | Y | Y | |
| TCP带外数据 | Y |
select的最大描述符数定义在<sys/types.h>或<sys/select.h>中:
#ifndef FD_SETSIZE
#define FD_SETSIZE 256
#endif- 每次调用
select都需要把fd_set从用户态拷贝到内核,开销大 - 同时每次调用
select都需要在内核遍历传递进来的所有fd_set,开销大 - select支持的文件描述符数量太小了
#include <sys/select.h>
#include <signal.h>
#include <time.h>
int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec *timeout, const sigset_t *sigmask);-
maxfdp1` 最大描述符数 + 1
-
readset可读描述符 -
writeset可写描述符 -
exceptset异常描述符 -
timeout超时pselect使用timespec结构,而不使用tiemval。
struct timespec { time_t tv_sec; long tv_nsec; }
-
sigmask一个指向信号掩码的指针,用于select阻塞期间时处理信号。 -
返回值有描述符就绪:n
超时:0
失败:-1
多路转接(允许禁止某些信号)
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
int kqueue(void);
int kevent(int kq, const struct kevent *changelist, int nchanges,
struct kevent *evenlist, int nevents,
const struct timespec *timeout);
void EV_SET(struct kevent *kev, uintptr_t ident, short filter,
u_short flags, u_int fflags, intptr_t data, vlid *udata);
struct kevent {
uintptr_t ident;
short filter;
u_short flags;
u_int fflags;
intptr_t data;
void *udata;
};| flags | 说明 | 更改 | 返回 |
|---|---|---|---|
| EV_ADD EV_CLEAR EV_DELETE EV_DISABLE EV_ENABLE EV_ONESHOT |
增设事件;自动启用,除非同时指定EV_DISABLE 用户获取后复位事件状态 删除事件 禁用事件但不删除 重新启用先前禁用的事件 触发一次后删除事件 |
Y Y Y Y Y Y |
|
| EV_EOF EV_ERROR |
发生EOF条件 发生错误:errno值在data成员中 |
Y Y |
| filter | 说明 |
|---|---|
| EVFILT_AIO | 异步I/O事件 |
| EVFILT_PROC | 进程exit,fork或exec事件 |
| EVFILT_READ | 描述符可读,类似select |
| EVFILT_SIGNAL | 收到信号 |
| EVFILT_TIMER | 周期性或一次性的定时器 |
| EVFILT_VNODE | 文件修改和删除事件 |
| EVFILT_WRITE | 描述符可写,类似select |
poll的机制与select类似,与select在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。
poll改变了文件描述符集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制远大于select的1024。
#include <poll.h>
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);-
fdarraypollfd数组struct pollfd { int fd; short events; // 要测试的描述符 short revents; // 返回的描述符的状态 }
poll函数的输入events和返回revents:
常值 说明 POLLIN
POLLRDNORM
POLLRDBAND
POLLPRI普通或优先级带数据可读
普通数据可读
优先级带数据可读
高优先级数据可读POLLOUT
POLLWRNORM
POLLWRBAND普通数据可写
普通数据可写
优先级带数据可写POLLERR
POLLHUP
POLLNVAL发生错误
发生挂起
描述符不是一个打开的文件 -
nfdspollfd数组长度 -
timeout超时时长超时值 说明 INFTIM 永远等待(POSIX规范要求在头文件 <poll.h>中定义INFTIM,不过许多系统仍然把它定义在头文件<sys/stropts.h>中)。0 立即返回,不阻塞进程。 >0 等待指定数目的毫秒数。 -
返回值成功:就绪描述符个数
定时器到时之前没有任何描述符就绪:0
失败:-1
多路复用
就TCP和UDP套接字而言,以下条件引起poll返回特定的revent:
- 所有正规TCP数据和所有UDP数据都被认为是普通数据
- TCP的带外数据被认为是优先级带数据
- 当TCP连接的读半部关闭时,也被认为是普通数据,随后的读操作将返回0
- TCP连接存在错误即可认为是普通数据,也可认为是错误(POLLERR)。无论哪种情况,随后的读操作将返回-1,并把errno设置成合适的值。这可用于处理诸如接收到RST或发生超时等条件。
- 在监听套接字上有新的连接可用即可认为是普通数据,也可认为是优先级数据。大多数实现视之为普通数据。
- 非阻塞式connect的完成被认为是使相应套接字可写。
- 每次调用
poll都需要把pollfd从用户态拷贝到内核,开销大 - 同时每次调用
poll都需要在内核遍历传递进来的所有pollfd,开销大
epoll是linux内核的可扩展I/O事件通知机制,于linux 2.5.44首次登场,基于事件驱动的I/O方式,是之前的select和poll的增强版本。
相对于select和poll来说,epoll没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
#include <sys/epoll.h>
int epoll_create(int size);size无意义,兼容以前的代码返回值epoll句柄
创建epoll句柄;流程如下:
- 在内核cache里建立了一个红黑树用于存储
epoll_ctl传来的socket; - 在内核cache建立一个
rdllist双向链表(就序列表),用于存储准备就绪的事件。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);-
epfdepoll句柄 -
op操作op值 说明 EPOLL_CTL_ADD 添加事件 EPOLL_CTL_MOD 修改监听的事件 EPOLL_CTL_DEL 删除监听的事件 -
fd套接字 -
event事件事件 说明 EPOLLIN 描述符处于可读状态 EPOLLOUT 描述符处于可写状态 EPOLLET 将 epoll event通知模式设置成edge triggeredEPOLLONESHOT 第一次进行通知,之后不在监测 EPOLLHUP 本段描述符产生一个挂断事件,默认监测事件 EPOLLRDHUP 对端描述符产生一个挂断事件 EPOLLPRI 由带外数据触发 EPOLLERR 描述符产生错误时触发,默认监测事件 -
返回值成功:0
失败:-1
向epfd对应的内核epoll实例添加,修改或删除对fd上事件event的监听。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);-
epfdepoll句柄 -
events用来记录被触发的events,其大小应该和maxevents一致 -
maxevents返回的events的最大个数 -
timeout超时时间(ms)timeout值 说明 0 立即返回。 -1 一直阻塞到已注册的事件变为就绪。 >0 阻塞直到时间结束或已注册的时间变为就绪。
检测有没有数据,有数据就返回,没有数据就sleep,等到timeout时间到后返回;示意图如下:
| 触发模式 | 说明 |
|---|---|
| LT | (Level Triggered,水平触发)默认模式,只要有数据都会触发,缓冲区剩余未读尽的数据会导致epoll_wait返回。 |
| ET | (Edge Triggered,边缘触发) 高速模式,只有数据到来才触发,不管缓存区中是否还有数据,缓冲区剩余未读尽的数据不会导致epoll_wait返回。Nginx用的ET,在以下情况推荐使用ET:1. read或write系统调用返回EAGAIN2.非阻塞的文件描述符 |
服务端:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/resource.h>
#define MAXEPOLL 10000
#define MAXLINE 1024
#define PORT 6000
#define MAXBACK 1000
int setnonblocking(int fd)
{
if(fcntl(fd, F_SETFL, fcntl(fd, F_GETFD, 0)|O_NONBLOCK) == -1)
{
printf("Set blocking error:%d\n", errno);
return -1;
}
return 0;
}
int main(int argc, char** argv)
{
int listen_fd;
int conn_fd;
int epoll_fd;
int nread;
int cur_fds;
int wait_fds;
int i;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
struct epoll_event ev; // 注意这里
struct epoll_event evs[MAXEPOLL]; // 注意注意
struct rlimit rlt; // 设置连接数
char buf[MAXLINE];
socklen_t len = sizeof(struct sockaddr_in);
// 设置每个进程允许打开的最大文件数
rlt.rlim_max = rlt.rlim_cur = MAXEPOLL;
if(setrlimit(RLIMIT_NOFILE, &rlt) == -1)
{
printf("Setrlimit Error : %d\n", errno);
exit(EXIT_FAILURE);
}
// 建立套接字
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(PORT);
if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("Socket Error...\n", errno);
exit(EXIT_FAILURE);
}
// 设置非阻塞模式
if(setnonblocking(listen_fd) == -1)
{
printf("Setnonblocking Error:%d\n", errno);
exit(EXIT_FAILURE);
}
// 绑定
if(bind(listen_fd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)) == -1)
{
printf("Bind Error : %d\n", errno);
exit(EXIT_FAILURE);
}
// 监听
if(listen(listen_fd, MAXBACK) == -1)
{
printf("Listen Error : %d\n", errno);
exit(EXIT_FAILURE);
}
// epoll操作(重点!!!)
epoll_fd = epoll_create(MAXEPOLL);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listen_fd; // 加入listen_fd
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0)
{
printf("Epoll Error : %d\n", errno);
exit(EXIT_FAILURE);
}
cur_fds = 1;
// 监听epoll事件
while(1)
{
if((wait_fds = epoll_wait(epoll_fd, evs, cur_fds, -1)) == -1)
{
printf("Epoll Wait Error : %d\n", errno);
exit(EXIT_FAILURE);
}
for(i = 0; i < wait_fds; i++)
{
if(evs[i].data.fd == listen_fd && cur_fds < MAXEPOLL)
{
if((conn_fd = accept(listen_fd, (struct sockaddr *)&cliaddr, &len)) == -1)
{
printf("Accept Error : %d\n", errno);
exit(EXIT_FAILURE);
}
printf("Server get from client!\n");
ev.events = EPOLLIN | EPOLLET; // 边缘触发,高速模式
ev.data.fd = conn_fd; // 放入连接描述符
if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev) < 0) // 添加事件
{
printf("Epoll Error : %d\n", errno);
exit(EXIT_FAILURE);
}
++cur_fds; // 文件描述符数+1
continue;
}
// 读数据
nread = read(evs[i].data.fd, buf, sizeof(buf));
if(nread <= 0)
{
close(evs[i].data.fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, evs[i].data.fd, &ev); // 读取失败,删除事件
--cur_fds;
continue;
}
// 写数据
write(evs[i].data.fd, buf, nread);
}
}
close(listen_fd); // 关闭监听
return 0;
}客户端:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#define MAXLINE 1024
#define SERV_PORT 6000
void send_and_recv(int connfd)
{
FILE* fp = stdin;
int lens;
char send[MAXLINE];
char recv[MAXLINE];
fd_set rset;
FD_ZERO(&rset);
int maxfd = (fileno(fp) > connfd ? fileno(fp) : connfd + 1);
int n;
while(1)
{
FD_SET(fileno(fp), &rset);
FD_SET(connfd, &rset);
if(select(maxfd, &rset, NULL, NULL, NULL) == -1)
{
printf("Client Select Error..\n");
exit(EXIT_FAILURE);
}
// 连接端口有信息
if(FD_ISSET(connfd, &rset))
{
printf("client get from server...\n");
memset(recv, 0, sizeof(recv));
n = read(connfd, recv, MAXLINE);
if(n == 0)
{
printf("Recv ok...\n");
break;
}
else if(n == -1)
{
printf("Recv error...\n");
break;
}
else
{
lens = strlen(recv);
recv[lens] = '\0';
write(STDOUT_FILENO, recv, MAXLINE);
printf("\n");
}
}
if(FD_ISSET(fileno(fp), &rset))
{
memset(send, 0, sizeof(send));
if(fgets(send, MAXLINE, fp) == NULL)
{
printf("End...\n");
exit(EXIT_FAILURE);
}
else
{
lens = strlen(send);
send[lens-1] = '\0'; // 不要回车字符
if(strcmp(send, "q") == 0)
{
printf("Bye..\n");
return;
}
printf("Client send : %s\n", send);
write(connfd, send, strlen(send));
}
}
}
}
int main(int argc, char** argv)
{
char buf[MAXLINE];
int connfd;
struct sockaddr_in servaddr;
if(argc != 2)
{
printf("Input server ip!\n");
exit(EXIT_FAILURE);
}
// 建立套接字
if((connfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
printf("Socket Error...\n", errno);
exit(EXIT_FAILURE);
}
// 套接字信息
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
// 链接server
if(connect(connfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
{
printf("Connect error..\n");
exit(EXIT_FAILURE);
}
send_and_recv(connfd);
close(connfd);
printf("Exit\n");
return 0;
}#include <sys/socket.h>
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int sockfd, const void *buff, size_t nbytes, int flags);-
sockfd套接字 -
buff缓冲区 -
nbytes字节数 -
flags标志flags 说明 recv send MSG_DONTROUTE 绕过路由表查找 Y MSG_DONTWAIT 仅本操作非阻塞 Y Y MSG_OOB 发送或接收带外数据 Y Y MSG_PEEK 窥看外来消息 Y MSG_WAITALL 等待所有数据 Y -
MSG_DONTROUTE告知内核目的主机在某个直接连接的本地网络上,因而无需执行路由表查找。 -
MSG_DONTWAIT在无需打开相应套接字的非阻塞标志的前提下,把单个I/O操作临时指定为非阻塞,接着执行I/O操作,然后关闭非阻塞标志。 -
MSG_OOB对于send,本标志指明即将发送带外数据;对于recv,本标志指明即将读入的是带外数据而不是普通数据。 -
MSG_PEEK适用于recv和recvfrom,它允许我们查看已可读取的数据,而且系统不在recv或recvfrom返回后丢弃这些数据。 -
MSG_WAITALL告知内核不要再尚未读入请求数目的字节之前让一个读操作返回。如果系统支持本标志,我们就可以省掉readn函数,替之以宏:#define readn(fd, ptr, n) recv(fd, ptr, n, MSG_WAITALL)
即使指定了MSG_WAITALL,如果发生下列情况之一:
- 捕获一个信号;
- 连接被终止;
- 套接字发生一个错误,相应的读函数仍有可能返回比所请求字节数要少的数据。
-
-
返回值成功:读/写的字节数
失败:-1
读/写 套接字
#include <sys/uio.h>
ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);
ssize_t writev(int filedes, const struct iovec *iov, int iovcnt);-
filedes文件描述符 -
iov指向iovec结构数组的指针POSIX要求在头文件
<sys/uio.h>中定义IOV_MAX常值来标识结构数组的容量,至少要为16;但是其实际值取决于具体实现:4.3BSD和Linux最多允许1024个,HP-UX最多允许2100个。iovec的定义如下:
struct iovec { void *iov_base; size_t iov_len; }
-
iovcntiovec结构数组的元素数量 -
返回值- 成功:读入/写出的字节数;
- 失败:-1。
读入/写出一个或多个缓冲区,这些操作分别称为分散读(scatter read)和集中写(gather write),因为来自读操作的输入数据被分散到多个应用缓冲区中,而来自多个应用缓冲区的输出数据则被集中提供给单个写操作。
#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags);-
sockfd套接字 -
msg消息结构struct msghdr { void *msg_name; // 接收者/发送者的协议地址 socklen_t msg_namelen; // 协议地址长度 struct iovec *msg_iov; // 输入/输出缓冲区数组 int msg_iovlen; // 缓冲区数组长度 void *msg_control; // 辅助数据的位置(可选) socklen_t msg_controllen; // 辅助数据的大小(可选) int msg_flags; // 消息标志 };
-
flags标志标志 由内核检查send, sendto或sendmsg函数的flags参数 由内核检查recv, recvfrom或recvmsg函数的flags参数 由内核通过recvmsg函数的msg_flags结构参数成员返回 MSG_DONTROUTE
MSG_DONTWAIT
MSG_PEEK
MSG_WAITALLY
Y
Y
Y
YMSG_EOR
MSG_OOBY
Y
YY
YMSG_BCAST
MSG_MCAST
MSG_TRUNC
MSG_CTRUNC
MSG_NOTIFICATIONY
Y
Y
Y
Y -
返回值成功:读/写字节数
失败:-1
读/写 套接字
| select | poll | epoll | |
|---|---|---|---|
| 操作方式 | 遍历 | 遍历 | 回调 |
| 底层实现 | 数组 | 链表 | 红黑树 |
| IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
| 最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
| fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
| 函数 | 任何描述符 | 仅套接字描述符 | 单个读/写缓冲区 | 分散/集中 读/写 | 可选标志 | 可选对端地址 | 可选控制信息 |
|---|---|---|---|---|---|---|---|
| read/write | Y | Y | |||||
| readv/writev | Y | Y | |||||
| recv/send | Y | Y | Y | ||||
| recvfrom/sendto | Y | Y | Y | Y | |||
| recvmsg/sendmsg | Y | Y | Y | Y | Y |







