恐惧让你沦为囚犯,
希望让你重获自由。
--- 《肖申克的救赎》---
通过网络通信的学习,我们能够理解网络通信的本质是进程间通信,而进程间通信的本质就是IO。
IO就是input与output
,站在进程角度,IO就是将数据从进程间输入输出数据;站在软件层面,IO就是与操作系统进行交互!以IO中常用的接口:read为例,当底层没有数据时,就会阻塞 。这种阻塞的本质是等待事件就绪。write写入数据时,会将数据拷贝到缓冲区中,当缓冲区满了之后,也会进行阻塞等待。
所以我们平时使用的IO都可以总结为等 + 拷贝
!所以什么叫做高效的IO呢?IO中可以理解为等 + 拷贝
,拷贝的效率是很快的,那么高效的IO就是"等"的效率高!如果调用read,write不需要等待,那么效率自然而然的就高了!
针对这个高效的IO,我们介绍一下五种IO模型
五种IO模型是程序员们经过长时间的使用总结出来的常用情况。接下来我们使用钓鱼的例子来介绍这五种IO模型
上面五个人就是经典的五种IO模型,每个人都代表一种系统调用,鱼竿就是文件描述符,鱼就是数据,河是操作系统内部,鱼漂浮动就代表数据就绪,收杆就代表进行拷贝:
阻塞IO和非阻塞IO的区别就是等待的方式不同,拷贝数据时一模一样的!上面五种钓鱼效率最高的是赵六:多路复用IO,毕竟人家鱼竿多,成功等到数据的概率就高!
而非阻塞IO的高效是与阻塞IO进行对比的,张三李四一天钓的鱼最终可能差不多,但李四看完一本书,追了4集电视剧…非阻塞IO的高效体现在可以在等待IO的同时处理其他事情!
异步IO就是两件事情互不影响,等待数据与获取数据就是异步进行!同步IO与异步IO的区别就是是否自身参与IO。
阻塞 IO 是最常见的 IO 模型:
select
就是一个专门用来等
的接口!
任何 IO 过程中, 都包含两个步骤: 第一是等待,第二是拷贝。 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间。让 IO 更高效, 最核心的办法就是让等待的时间尽量少。 这里强调两组概念:
同步通信 vs 异步通信(synchronous communication / asynchronouscommunication)同步和异步关注的是消息通信机制。
以后在看到 “同步” 这个词, 一定要先搞清楚大背景是什么。 这个同步, 是同步通信异步通信的同步, 还是线程同步与互斥的同步.
阻塞 vs 非阻塞
实现非阻塞IO的方式有很多种:
当使用open打开一个文件时,可以传入一个标志位O_NONBLOCK or O_NDELAY
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
这样读取时就是非阻塞的进行读取。
同样的recv和send系列接口也有对应的非阻塞标志位MSG_DONTWAIT
:
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
但是这些方式都不太通用,我们可以通过fcntl
接口将文件描述符设置为非阻塞的文件描述符:
Linux Programmer's Manual
FCNTL(2)
NAME
fcntl - manipulate file descriptor
SYNOPSIS
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl 函数有 5 种功能:
通过这个系统调用,可以写一个demo来看看效果。 这是j经典的阻塞IO:
#include <iostream>
#include <unistd.h>
#include<cstdio>
// 阻塞IO
int main()
{
char buffer[1024];
// 读取标准输入中的数据
while (true)
{
printf("Enter#");
fflush(stdout);
int n = read(0, buffer, sizeof(buffer) - 1);
if (n == 0)
{
// 说明没有读取到
continue;
}
else if(n > 0)
{
//读取到了数据
buffer[n] = 0;
std::cout << buffer ;
}
else
{
//读取出现错误了
perror("error!\n");
}
}
return 0;
}
读取效果是这样的:
如果没有数据输入就会阻塞等待数据输入。 那么如何更改为非阻塞IO呢?我们设置一个接口函数
#pragma once
#include <unistd.h>
#include <fcntl.h>
#include <iostream>
//int fcntl(int fd, int cmd, ... /* arg */);
void SetNonBlock(int fd)
{
//首先获取原来标志位
int fl = ::fcntl(fd , F_GETFL);
if(fl < 0)
{
std::cout<< "fcntl error " << std::endl;
return ;
}
//设置非阻塞标志位
int n = ::fcntl(fd , F_SETFL , fl | O_NONBLOCK);
if(n < 0)
{
perror("fcntl error \n");
return ;
}
return ;
}
通过这个接口可以快速将文件描述符设置为非阻塞。我们将标准输入设置为非阻塞我们再来运行一下:
那么如何区分是真的出错了还是底层不就绪的非阻塞IO返回呢?仅仅通过返回值是无法区分的了,但是read接口中的返回值是这么描述的:
RETURN VALUE
On success, the number of bytes read is returned (zero indicates end of file), and the file position is advanced by this number. It is not an error if this number is
smaller than the number of bytes requested; this may happen for example because fewer bytes are actually available right now (maybe because we were close to end-of-
file, or because we are reading from a pipe, or from a terminal), or because read() was interrupted by a signal. See also NOTES.
On error, -1 is returned, and errno is set appropriately. In this case, it is left unspecified whether the file position (if any) changes.
出错时,read接口会设置全局的errno!同样的recv,send…IO系列接口都是会设置errno:
可以看到errno被设置为了11
,那11代表什么呢?11就是EWOULDBLOCK
错误:
#define EAGAIN 11 /* Try again */
...
#define EWOULDBLOCK EAGAIN /* Operation would block */
这就表示底层数据不就绪,可以try again
,如果真的出错了,就会被设置成其他格式。
// 非阻塞IO
int main()
{
char buffer[1024];
SetNonBlock(0);
// 读取标准输入中的数据
while (true)
{
printf("Enter#");
fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer) - 1);
if (n == 0)
{
printf("read done\n");
break;
}
else if (n > 0)
{
// 读取到了数据
buffer[n] = 0;
printf("echo# %s" , buffer);
}
else
{
// 读取出现错误了
if(errno == EWOULDBLOCK)
{
std::cout << "底层数据没有就绪,开始轮询检测" <<std::endl;
//do other thing
//可以做其他事情!
continue;
}
else
{
//真的出错了!
perror("read error!\n");
break;
}
}
//sleep(1);
}
return 0;
}
这样就可以进行正常的非阻塞轮询了!
注意:操作系统的两个缓冲区:输入与输出,在我们键盘进行输入时会到操作系统的输入缓冲区,然后再到进程的输入缓冲区。而键盘输入时,操作系统会判断是否需要回显,回显就会将输入缓冲区的数据拷贝到输出缓冲区一份,这里就是可以回显的原因。
当我们进行IO拷贝时,如果突然接收到一个信号,导致IO拷贝中断了,那么这个读取的返回可能并没有读取完毕!这种情况的错误码是EINTR
,我们可以进行判断!