个人主页:Lei宝啊
愿所有美好如期而遇
IO本质
我们常说IO就是input,output,也就是输入和输出,但是,他的本质是什么?站在OS角度,站在进程的角度,IO是什么?我们想,输入输出,数据都是保存在内存中,从硬件输入,再输出到硬件上,像scanf,printf,recv,read,write等函数,他们实际上做了什么?其实是等待加拷贝,什么叫等待加拷贝,其实就是说,等待硬件条件满足。我们用recv举例,他就在等待接收缓冲区中有数据,然后将接收缓冲区中的数据拷贝到用户缓冲区中。所以我们说,其实IO的本质就是等待+拷贝。
那么什么叫做高效的IO?像我们计算机计算一个简单的加法时,一秒可以计算数亿,但是如果说我们要将每一次计算的结果打印到显示器上,那么可能只有几万,这样的IO显然并不高效,为什么不高效,因为他降低了计算效率,即使结果已经计算出来,但是我们仍然看不到,本质就是因为等待,打印在等待显示器这个硬件资源就绪,而光计算,并不需要等待,所以效率就高,所以高效的IO就是降低等待的时间,等待时间占的比重越低,IO效率越高。
那么什么叫做高效的代码?就是减少IO的比重,因为IO就注定了需要等待硬件资源,注定了会影响效率,第二个就是将等待的时间利用起来,这样也能提高效率。
五种IO模型
- 1. 阻塞IO,阻塞式等待,资源就绪进行IO。
- 2. 非阻塞IO, 资源不就绪就直接返回。
- 3. 信号驱动IO,收到信号进行IO。
- 4. 多路复用,多路转接。
- 5. 异步IO。
1和2的区别就在于等待方式不同,什么是多路复用?简单来说,我们传统的IO是等+拷贝,并且一次只能等待一个文件描述符,而多路复用就可以通过系统调用同时等待多个文件描述符,将所有文件描述符等待的时间重叠起来,拷贝工作仍然由我们传统的接口如recv等来完成。异步IO就是说,这些操作全部由内核来完成,用户不用进行IO,我们称用户只要参与了IO,就是同步IO,也就是说,只要用户参与了等或者拷贝,就是同步IO。
这里我们和线程的同步IO做一下区分,线程的同步指的是线程之间的执行具有顺序性,而这里的同步IO指的是只要用户参与了IO就是同步IO。
五种IO方式中最为高效的就是多路复用,当然,因为阻塞IO比较简单,所以使用的还是比较多。
代码实现
接下来我们就简单举个例子来实现一下多路复用。
select系统调用
第一个参数:填写最大fd值+1,中间三个参数,都是fd集合,表示我们想让操作系统监管的fd,最后一个参数是一个结构体指针,这个结构体有两个属性,秒和毫秒,表示select系统调用等待监管的fd集合的时长,当有fd就绪时,这个参数返回,被设置为剩余时长。select返回值为就绪的fd数目。
fd_set类型的变量,实际上是位图,用来标识fd的位置和状态,下面的四个宏,用来对fd_set类型的变量做操作,第一个是移除,第二个是判断存在,第三个是设置,第四个是清空。
#include "Socket.hpp"
#include <vector>
using namespace socket_ns;
const static int N = sizeof(fd_set) * 8;
const static int defaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port)
: _port(port), _listensock(make_unique<TcpSocket>()),
assist(N, defaultfd)
{
Analyze addr("0", _port);
_listensock->BuildTcpServerSocket(addr);
assist[0] = _listensock->getfd();
}
~SelectServer() {}
void Loop()
{
while (true)
{
fd_set rfds;
FD_ZERO(&rfds);
// 将所有合法的fd设置进rfds中
int maxfd = assist[0];
for (int i = 0; i < N; i++)
{
if (assist[i] == defaultfd)
continue;
FD_SET(assist[i], &rfds);
if (maxfd < assist[i])
maxfd = assist[i];
}
timeval timeout = {1, 0};
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
Log(Debug, "timeout...");
break;
case -1:
Log(Error, "select error");
break;
default:
Log(Info, "select fd num: %d", n);
HandlerEvent(rfds);
break;
}
}
}
void HandlerEvent(fd_set &rfds)
{
for (int i = 0; i < N; i++)
{
// 这里判断守监管的fd是否就绪,监管的fd在assist中
if (assist[i] == defaultfd)
continue;
if (FD_ISSET(assist[i], &rfds))
{
if (assist[i] == _listensock->getfd())
{
AcceptClientRequest();
// listen套接字获取普通套接字加入辅助数组
}
else
{
HandlerNormalRequest(i); // 处理普通套接字的请求
}
}
}
}
// 参数:已就绪的普通fd的位置
void HandlerNormalRequest(int pos)
{
char buffer[N];
int n = recv(assist[pos], buffer, sizeof(buffer) - 1, 0);
if (n > 0)
{
cout << buffer << endl;
string rest = "response: ";
rest += buffer;
send(assist[pos], rest.c_str(), rest.size(), 0);
}
else
{
close(assist[pos]);
assist[pos] = defaultfd;
}
}
void AcceptClientRequest()
{
Analyze an("0", _port);
sock_addr addr = _listensock->Accept(an);
// 通过返回值,我们可以拿到普通socketfd
int socketfd = addr->getfd();
if (socketfd < 0)
return;
int pos = 1;
for (; pos < N; pos++)
{
if (assist[pos] == defaultfd)
break;
}
if (pos == N)
{
close(socketfd);
Log(Warning, "FULL");
return;
}
else if (pos < N)
{
assist[pos] = socketfd;
Log(Info, "AcceptClientRequest Set Success");
}
}
private:
uint16_t _port;
unique_ptr<Socket> _listensock;
vector<int> assist;
};
select实现的多路复用,是有缺陷的,因为它能够等待的fd是有上限的,在博主的ubuntu下,他是1024个bit位,也就是说,最多能够同时等待1024个fd。
再一个,每一次,对select的参数来说,都需要重新设置,而poll解决了这两个问题,但是这里我们不再多谈,后面我们会讲解epoll。