【Linux高级IO(一)】明白五种IO模型
目录1、五种IO模型:
2、非阻塞IO
3、多路转接之select
3.1、明白select的执行过程
1、应用层read&write的时候,本质把数据从用户写给OS ---- 本质就是拷贝
2、IO = 等待 + 拷贝(大部分时间在等待,比及有数据了才举行拷贝)
要举行拷贝,必须先判断条件成立。 这个条件就是读写事件 读事件停当,就是读缓冲区有足够的数据可以读 写事件停当就是发送缓冲区中要有足够的空间
什么叫做高效IO呢? 单元时间内,IO过程中,等的比重越小,IO服从就越高!
实在险些所有提高IO服从的计谋,本质就是让等的比重变小。
1、五种IO模型:
五种IO模型:
1、张三:钓鱼的时候,专心钓鱼,不做任何其他的事,反面任何人说话。这叫阻塞式IO。
2、李四:钓鱼的时候,在等鱼咬勾的时候还会一边看书。这是非阻塞轮询IO。
3、王五:等鱼咬钩的时候,在鱼竿上面放一个铃铛,当鱼咬钩的时候,就会给他提醒。这是信号驱动式IO。
4、赵六:他有100个鱼竿,全都放在水里,自己在岸边巡查,有鱼咬钩立马去检察。这是多路复用、多路转接式IO。
5、田七:他是老板,他把钓鱼这件事委托给他的助理小王,田七只是钓鱼行为的发起者,他要的只是鱼。这是异步IO。
阻塞IO vs 非阻塞IO 它们的IO服从实在是雷同的,因为IO = 等待 + 拷贝 (等待的时间实在是一样的,只是等的方式差别)
同步IO vs 异步IO 同步IO实在是当前这个人有没有参与IO,参与了就是同步,没参与就是异步。异步IO不参与IO只是发起IO,最后拿效果就行。 同步IO和线程同步实在没有任何关系。
因为多路复用式IO服从最高 我们后面重点阐明多路复用式IO和非阻塞IO
2、非阻塞IO
文件描述符本质就是一个数组下标,每一个数组下标指向的都是一个内核中的文件对象,文件对象中有文件的flags,用fcntl设置一个文件描述符的属性,实在就是设置这个文件在底层struct file中的标志位。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd,... /* arg */ );
https://i-blog.csdnimg.cn/direct/84395399709e49eba860cffd2fd9c2f4.png
将文件描述符设置为非阻塞,以后在利用read、write、recv、send...都是利用非阻塞的方式举行IO的。
void SetNoBlock(int fd) {
int fl = fcntl(fd, F_GETFL);//获得指定文件描述符的标记位
if (fl < 0)//小于0,获取失败
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
//为指定的文件描述符设置标记位 在老的标记位f1的基础上添加 O_NONBLOCK(非阻塞)
}1、将文件描述符设置成为非阻塞,假如底层fd数据没有停当,recv、read、write、send返回值会以出错的形式返回(因为返回值大于0就是读到了,等于0就是关闭了)
2、有两种情况: 我真的出错了、底层没有停当
3、我要怎么区分这两种情况??? 通过errno错误码区分(11是底层数据没有停当)
else if(errno == EWOULDBLOCK)
{
cout << "0 fd data not ready, try again!" << endl;
}#include <fcntl.h>
#include <unistd.h>
#include<errno.h>
#include <cstdlib>
#include <iostream>
using namespace std;
//对指定的fd设置非阻塞
void SetNonBlock(int fd) {
int fl = fcntl(fd, F_GETFL);
if (fl < 0) {
cerr << "fcntl error" << endl;
exit(1);
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main() {
SetNonBlock(0);
while (1) {
char buffer;
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if (s > 0) {
buffer = 0;
cout << buffer << endl;
} else if (s == 0) {
cout << "读到文件结尾了" << endl;
break;
}
else
{
//1. 数据没用准备好 2. 真的出错了. 都以-1的返回值返回
// 数据没有准备好,不算出错. 需要区分这两种情况
if(errno == EWOULDBLOCK || errno == EAGAIN)
{
cout<<"os底层数据还没就绪"<<endl;
cout<<errno<<endl;
}
//被信号中断, 也不算read出错
else if(errno == EINTR)
{
cout<<"IO interrupted by signal"<<endl;
}
else
{
cout<<"read error"<<endl;
break;
}
}
sleep(1);
}
}
3、多路转接之select
IO = 等 + 拷贝
select:只负责举行等待。一次可以等待多个fd
停当事件通常分为可读事件,可写事件和非常事件
#inclde <sys/select>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
第一个参数:要等的最大的文件描述符+1
第二个:关心读 fd_set是内核提供的一种数据类型,它是位图 输入输出型参数
输入时:用户告诉内核,我给你一个或多个fd,你要帮我关心fd上面的读事件哦,假如读事件停当了,你要告诉我。
输出时:内核告诉用户,用户你要我关心的多个fd,有哪些停当了,用户你赶紧读取吧。
输入时:比特位的位置(从右向左数),表示文件描述符编号,比特位的内容,0/1,表示是否需要内核关心。
输出时:比特位为0/为1,表示哪些用户关心的fd,上面的读事件已经停当了。
fd_set是一张位图,让用户和内核传递fd是否停当的信息的。
第三个: 关心写
这就涉及到许多对位图修改的动作,系统提供了接口。
https://i-blog.csdnimg.cn/direct/0b9bc4d2666540e8b7c6761a9b0abd10.png
fd_set类型参数, 输入你想要关心的fd聚集, 输出时, 此结构中存放, 已经事件停当的fd聚集. 比如你想要关心0~10号文件描述符的读事件, 函数返回时, 此聚集中可能只有1.3.5号fd被返回了, 也就是说只有1.3.5号fd的事件停当了
第五个参数:
struct timeval:结构体 时间结构体:
{5,0}:每隔5s,timeout一次。5s阻塞式等待,这5s没有文件描述符停当,就返回,再重新进入,重复(我们需要重复设置)。假如等待5s期间有文件描述符停当了,就会立刻返回
timeval类型参数: 假如你设置阻塞时间为5秒, 但是等待了三秒后就有事件停当, 函数就返回了, 那么timeval类型参数的值会被设置成为2秒.
他是输入输出参数,可能要举行周期的重复设置
{0,0}:非阻塞等待。立马返回
NULL:阻塞等待
这个参数是输入输出型参数
select返回值和错误码:
大于0,有n个fd停当了
等于0,超时,没有出错,但是也没有fd停当
小于0,等待出错
https://i-blog.csdnimg.cn/direct/4a4480e6faf34b7da40d0a0e0c625618.png
3.1、明白select的执行过程
假如事件停当,上层不处置惩罚,select会一直关照你!
select的缺点:
1、等待的fd是有上限的
2、输入输出型参数比较多,数据拷贝的频率比较高(用户到内核,内核到用户)
3、输入输出型参数比较多,每次都要对关心的fd举行事件重置
4、用户层,利用第三方数组管理用户的fd,用户层需要许多次遍历,内核中检测fd事件停当也要遍历
#pragma once
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport) : _port(port)
{
for(int i = 0; i < fd_num_max; i++)
{
fd_array = defaultfd;
std::cout <<"fd_array[" << i << "]" << " : " << fd_array << std::endl;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter()
{
// 连接事件就绪了
std::string clientip;
uint16_t clientport = 0;
int sock = _listensock.Accept(&clientip, &clientport); // 获取连接不会阻塞在这里
if (sock < 0)
{
return;
}
lg(Info, "accept success, %s:%d", clientip.c_str(), clientport);
// 以前走到这里可以直接读取数据,可是,现在这里不可以
// 以前是多线程、多进程, 文件描述符其实是托管给执行流的 他阻塞是不影响的
// 我们现在是单进程,不能建立完连接,立马就进行读如果不发,当前的进程就阻塞了
// select里面只有一个listen套接字 要将sock设置进select里这样读文件描述符集里的文件描述符就会变得越来越多
// accept获取新连接, 不能直接读,因为不清楚是否就绪,selcet清楚是否就绪,所以要将新获取的连接 添加到辅助数组里
// select把数据处理完之后,下次循环时会重新再进行把文件描述符添加到rfds里,再交给select由他来监听
int pos = 1;
for (; pos < fd_num_max; pos++)
{
if (fd_array != default)
continue; // 被占用
else
break;
}
if (pos == fd_num_max)
{
// 全部被占用
lg(Warning, "server is full, close %d now!", sock);
close(sock);
}
else // 提前break说明由-1位置(即没有被占用)
{
fd_array = sock; // 新获取的连接往数组中添加
}
}
void Recver(int fd, int pos)
{
char buffer;
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer = 0; // 把它当字符串
cout << "get a message: " << buffer << endl;
}
else if (n == 0) // 读失败
{
lg(Info, " client quit,me too, close fd is : ", fd);
close(fd);
fd_array = defaultfd; // 这里本质是从select中移除
}
}
void Dispatchar(fd_set &rfds) // 读事件就绪 //事件派发器
{
for (int i = 0; i < fd_num_max; i++)
{
int fd = fd_arry;
if (fd == dafaultfd)
conitnue;
if (FD_ISSET(fd, &rfds)) // 判断listen套接字是否在集合里,即是否就绪
{
if(fd == _listensock.Fd())
{
Accepter(); //连接管理器
}
else //不是listenfd
{
Recver(fd, i);
}
}
}
}
void Start()
{
int listensock = _listensock.Fd();
fd_array = listensock;
for (;;)
{
Fd_set rfds;
Fd_ZERO(rfds);
int maxfd = fd_array;
for(int i = 0; i < fd_num_max; i++)
{
if(fd_arry == default)
continue; //没有被设置过的
Fd_SET(fd_array, &rfds); // 将文件描述符添加到集合里
if(maxfd < fd_array)
{
maxfd = fd_array;
}
}
// accept 的本质是检测listensocket上面有没有连接事件 即底层有三次握手 他一次只能等一个文件描述符 所以要交给select
// 新连接到来,对于select来讲就是读事件就绪
// 读文件描述符集,他是一个位图
struct timeval timeout = {5, 0}; //需要被重复设置 因为如果不重复设置就 会剩余的时间替换
//输入输出参数,每次从内核返回的时候值可能就被改过了 ,所以需要重复设置
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout); // 告诉OS关心这个文件描述符上的读事件
//select 是如果事件就绪,上层不处理,select会一直通知你
//select告诉你就绪了,接下来的一次读取,fd不会阻塞
switch(n)
{
case 0:
//等待超时,在等待期间任何事情都没有发生
break;
case -1:
//异常
break;
default:
//有事件就绪
HandlerEvent(rfds);//就绪的事件在rfds里
break;
}
}
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array{defaultfd};//设置辅助数组
};
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]