文章目录
前言
IO过程总体分为两步,第一步等:接收端->接受缓冲区等待数据的递达,发送端->等待发送缓冲区有可用空间,将发送数据放入发送缓冲区;第二步拷贝:接收端->将递达的数据从内核空间拷贝到用户空间的接受缓冲区,发送端->将发送的数据从用户空间的发送缓冲区拷贝至内核缓冲区;
所以高效IO本质是单位时间内拷贝的频率很高,重点是缩短等待的时间
一、五种IO模型
1.1 阻塞IO
阻塞IO是最常见的IO模型(在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式)
阻塞:本质是由OS发起,由OS执行,进程状态R->(S or T or D),进入等待队列当中,数据就绪后,进程被操作系统唤醒,再去执行
recvfrom
1.2 非阻塞IO
如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用
非阻塞轮询的本质是:由用户发起,OS执行,做事件就绪(OS有数据)的检测工作
1.3 信号驱动IO
内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
1.4 异步IO
由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)
1.5 IO多路转接
虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件
描述符的就绪状态
二、同步通信与异步通信
同步与异步关注的是消息通信机制
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,就得到返回值;换句话说,就是由调用者主动等待这个调用的结果;拷贝需要用户参与
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所有没有返回结果;换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者,或者通过回调函数处理这个调用。拷贝不需要用户参与,操作系统拷贝
这里的同步通信和进程之间的同步是完全不相干的概念
三、非阻塞IO
非阻塞IO可以open打开时设置flag
为非阻塞,这里主要介绍打开后的fd如何设置为非阻塞
3.1 fcntl
一个文件描述符,默认都是阻塞IO,fcntl函数用于已经打开的fd
该函数是一个系统调用函数
man 2 fcntl
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl
函数有5种功能:
- 复制一个现有的描述符 ---- cmd=F_DUPFD
- 获得/设置文件描述符标记–cmd=F_GETFD或F_SETFD
- 获得/设置文件状态标记-----cmd=F_GETFL或F_SETFL
- 获得/设置异步IO所有权-----cmd=F_GETOWN或F_SETOWN
- 获得/设置记录锁--------------cmd=F_GETLK,F_SETLK或F_SETLKW
传入的cmd的值不同,后面追加的参数也不相同
目前只使用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞
3.2 实现函数SetNoBlock
基于fcntl
,实现一个SetNoBlock函数,将文件描述符设置为非阻塞
void SetNoBlock(int fd){
int fl = fcntl(fd,F_GETFL); //获取fd的状态标记
if(fl < 0){
perror("fcntl");
return ;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK); //将fl状态标记设置为非阻塞
}
3.3 如何使用函数SetNoBlock
以轮询的方式读取标准输入
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
bool SetNonBlock(int fd){
int fl = fcntl(fd,F_GETFL);
if(fl < 0){
std::cerr << "fcntl error" << std::endl;
return false;
}
fcntl(fd,F_SETFL, fl | O_NONBLOCK);
return true;
}
int main()
{
#define NUM 1024
SetNonBlock(0);
while(true){
char buffer[NUM];
ssize_t size = read(0,buffer,sizeof(buffer)-1);
if(size < 0){
//不一定是出错了,有可能是底层没有数据
if(errno == EAGAIN || errno == EWOULDBLOCK ){
std::cout << "底层的数据没有就绪,你在轮询检测一下,try again " << std::endl;
continue;
}
if(errno == EINTR){
std::cout << "底层数据就绪未知,被信号中断 " << std::endl;
continue;
}
else{
std::cerr << "read error: " << size << " errno: "<< errno << std::endl;
break;
}
}
buffer[size] = 0; //主动给字符串最后添加'\0'
std::cout << "#echo: " <<buffer << std::endl;
}
return 0;
}
如果以非阻塞读取数据时,如果数据没有就绪,read是以出错的形式返回
如何判断是read真的出错?or 底层数据没有就绪 or read被信号中断
- errno == EAGAIN || errno == EWOULDBLOCK 时,说明数据没有就绪
- errno == EINTR ,说明被信号中断
grep -ER 'EAGAIN | EWOULDBLOCK' /usr/include/ 查看宏定义