目录
- 五种IO模型
- 引入
- IO模型
- 阻塞IO
- 非阻塞IO
- 信号驱动IO
- O多路转接
- 异步IO
- IO重要概念
- 同步通信 vs 异步通信
- 阻塞 vs 非阻塞
- 其他高级IO
- 非阻塞IO
- fcntl
- 基于fcntl将文件描述符设置为非阻塞
- 轮询方式读取标准输入
- I/O多路转接之select
- 初识select
- select函数
- 原型
- 参数timeout取值
- fd_set结构及输入输出型参数
- select编写需要第三方数组
- socket就绪条件
- select优缺点
- 代码
- I/O多路转接之poll
- poll函数接口
- 参数说明
- 参数fds及poll优缺点
- 代码
- I/O多路转接之epoll
- epoll的相关系统调用
- epoll_create
- epoll_ctl
- epoll_wait
- epoll工作原理
- 原理
- 细节理解
- epoll的优点(和 select 的缺点对应)
- 注意
- epoll工作方式
- epoll的使用场景
- epoll中的惊群问题
- 代码
五种IO模型
引入
IO模型
阻塞IO
阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
非阻塞IO
非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用.
信号驱动IO
信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作
O多路转接
IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态.
异步IO
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
IO重要概念
同步通信 vs 异步通信
同步和异步关注的是消息通信机制.
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
- 异步则是相反, 调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后, 被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
多进程多线程, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概念.
- 进程/线程同步也是进程/线程之间直接的制约关系
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候.
阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
- 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.
其他高级IO
非阻塞IO,纪录锁,系统V流机制, I/O多路转接(也叫I/O多路复用) ,readv和writev函数以及存储映射IO(mmap),这些统称为高级IO
非阻塞IO
fcntl
一个文件描述符, 默认都是阻塞IO.
基于fcntl将文件描述符设置为非阻塞
bool SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
if (fl < 0)
return false;
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
return true;
}
- 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
- 然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
轮询方式读取标准输入
bool SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
if (fl < 0)
return false;
fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
return true;
}
int main()
{
// 将0号文件描述符设置为非阻塞
SetNonBlock(0); //只要设置一次,后续就都是非阻塞了
char buffer[1024];
while (true)
{
sleep(1);
errno = 0;
// 非阻塞的时候,我们是以出错的形式返回,告知上层数据没有就绪:通过errno来辨别出错类型
// 0号设置为非阻塞的时候,read不再阻塞等待键盘输入内容,而是会直接返回
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
// s > 0 代表读取到数据了,否则代表出错了,出错包含两种,一种代表正常continue再次循环,一种代表异常
// 出错,不仅仅是错误返回值,errno变量也会被设置,表明出错原因,可以通过errno来确定出错类型
if (s > 0)
{
buffer[s-1] = 0;
std::cout << "echo# " << buffer << " errno[---]: " << errno << " errstring: " << strerror(errno) << std::endl;
}
else
{
// 如果失败的errno值是11,代表其实没错,只不过是底层数据没就绪,非阻塞直接返回而已
// EWOULDBLOCK 和 EAGAIN 就是 11
if(errno == EWOULDBLOCK || errno == EAGAIN)
{
std::cout << "当前0号fd数据没有就绪, 请下一次再来试试吧" << std::endl;
continue;
}
// EINTR 也是代表正常
else if(errno == EINTR)
{
std::cout << "当前IO可能被信号中断,在试一试吧" << std::endl;
continue;
}
else
{
//进行差错处理
}
}
}
return 0;
}
I/O多路转接之select
初识select
系统提供select函数来实现多路复用输入/输出模型.
- select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
- 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
select函数
原型
参数timeout取值
fd_set结构及输入输出型参数
select编写需要第三方数组
socket就绪条件
读就绪
- socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
- socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
- 监听的socket上有新的连接请求;
- socket上有未处理的错误;
写就绪
- socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记
- SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
- socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
- socket使用非阻塞connect连接成功或失败之后;
- socket上有未读取的错误;
异常就绪
- socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(TCP协议头中, 有一个紧急指针的字段),
select优缺点
代码
I/O多路转接之poll
poll函数接口
参数说明
参数fds及poll优缺点
代码
I/O多路转接之epoll
按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
被公认为Linux2.6下性能最好的多路I/O就绪通知方法.
epoll的相关系统调用
epoll_create
创建一个epoll的句柄.
- 自从linux2.6.8之后, size参数是被忽略的.
- 用完之后, 必须调用close()关闭.
epoll_ctl
epoll的事件注册函数.
- 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
- 第一个参数是epoll_create()的返回值(epoll的句柄).
- 第二个参数表示动作,用三个宏来表示.
- 第三个参数是需要监听的fd.
- 第四个参数是告诉内核需要监听什么事.
第二个参数的取值:
- EPOLL_CTL_ADD :注册新的fd到epfd中;
- EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL :从epfd中删除一个fd;
struct epoll_event结构如下:
events可以是以下几个宏的集合:
- EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
- EPOLLOUT : 表示对应的文件描述符可以写;
- EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
- EPOLLERR : 表示对应的文件描述符发生错误;
- EPOLLHUP : 表示对应的文件描述符被挂断;
- EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
- EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里.
epoll_wait
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在epoll监控的事件中已经发送的事件.
- 参数events是分配好的epoll_event结构体数组.
- epoll将会把发生的事件赋值到events数组中 (events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存).
- maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size.
- 参数timeout是超时时间 (毫秒, 0会立即返回, -1是永久阻塞).
- 如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.
epoll工作原理
原理
- 当某一进程调用epoll_create方法时, Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关.
细节理解
epoll的优点(和 select 的缺点对应)
注意
epoll中使用了内存映射机制
内存映射机制: 内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销.
这种说法是不准确的. 我们定义的struct epoll_event是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中的
epoll工作方式
epoll有2种工作方式-水平触发(LT)和边缘触发(ET)
对比LT和ET
epoll的使用场景
epoll中的惊群问题
参考 http://blog.csdn.net/fsmiy/article/details/36873357