五种IO模型

任何IO都包含两个步骤:等待和拷贝。在实际应用场景中,等待消耗的时间远远高于拷贝的时间,让IO更高效的核心办法是让等待的时间尽量少。
IO重要概念
同步VS异步通信
同步:由调用者主动等待调用结果。
异步:调用发出后,调用者通过状态,信号等被通知,或通过回调函数处理这个调用。
阻塞VS非阻塞
阻塞调用指调用结果返回之前当前进程会被挂起,调用的线程只有在得到结果之后才会返回。
非阻塞调用指得到结果之前不会阻塞当前进程。
实现函数SetNoBlock
cpp
void SetNonBlockOrDie(int fd)
{
int f1=fcntl(fd,F_GETFL);
if(f1<0)
{
perror("fcntl");
return;
}
fcntl(fd,F_SETFL,f1|O_NONBLOCK);
}
轮询方式读取标准输入:
cpp
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
using namespace std;
void SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
SetNoBlock(0);
while (1)
{
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) - 1);
if (read_size < 0)
{
perror("read");
sleep(1);
continue;
}
printf("input:%s\n", buf);
}
return 0;
}

select介绍

参数:
参数nfds是需要监视的最大的文件描述符值+1;
readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合.
参数timeout为结构timeval,用来设置select()的等待时间。
timeout取值:
NULL:一直等,select被阻塞,直到某个 fd 就绪
0,0:仅检测描述符集合的状态,立即返回,不阻塞
其他值:等待指定时间,若在指定的时间段里没有事件发生,select将超时返回。

fd_set本质是一个位图,使用对应的位来表示要监视的文件描述符。
一组接口来操作位图:
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
函数返回值:
1.执行成功则返回文件描述词状态已改变的个数.
2.如果返回0代表在描述词状态改变前已超过timeout时间,没有返回.
3.当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测。
select执行过程举例
1.执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
2.若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) .
3.若再加入fd=2,fd=1,则set变为0001,0011
4.执行select(6,&set,0,0,0)阻塞等待
5.若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。若没有事件发生,则fd=5被清空。
select特点
1.可监控的文件描述符取决于sizeof(fd_set)的值,即位图的大小是一定的,服务器上支持的最大文件描述符是sizeof(fd_set)*8.
-
将fd加入到select监控集的同时。还要再用一个数据结构array来保存放入select监控集中的fd。一是用于在select 返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入,扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
缺点:1.每次调用select, 都需要手动设置fd集合, 非常不便.
2.每次调用select,都需要把fd集合从用户态拷贝到内核态,开销在fd很多时会很大
3.同时每次调用select都需要在内核遍历传递进来的所有fd,开销在fd很多时很大
4.select支持的文件描述符数量太小.
select使用示例
检测标准输入输出:
cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>
int main()
{
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
for (;;)
{
printf("> ");
fflush(stdout);
int ret = select(1, &read_fds, NULL, NULL, NULL);
if (ret < 0)
{
perror("select");
continue;
}
if (FD_ISSET(0, &read_fds))
{
char buf[1024] = {0};
read(0, buf, sizeof(buf) - 1);
printf("input: %s", buf);
}
else
{
printf("error! invaild fd\n");
continue;
}
FD_ZERO(&read_fds);
FD_SET(0, &read_fds);
}
return 0;
}
SelectServer.hpp
cpp
#pragma once
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8888;
static const int fd_num_max = sizeof(fd_set) * 8;
int defaultfd = -1;
class SelectServer
{
public:
SelectServer(const uint16_t &port = defaultport)
: _port(port)
{
for (int i = 0; i < fd_num_max; i++)
{
fd_array[i] = defaultfd;
}
}
void Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
}
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,sockfd:%d", clientip.c_str(), clientport, sock);
int pos = 1;
for (; pos < fd_num_max; pos++)
{
if (fd_array[pos] != defaultfd)
continue;
else
break;
}
// 找到一个可填入的下标
if (pos == fd_num_max)
{
lg(Warning, "server is full,close %d now!", sock);
close(sock);
}
else
{
fd_array[pos] = sock; // 填入
PrintFd(); //
}
}
void Recver(int fd, int pos)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
cout << "get a message: " << buffer << endl;
}
else if (n == 0)
{
lg(Info, "client quit,me too,close fd: %d", fd);
close(fd);
fd_array[pos] = defaultfd; // 本质是从select中移除。
}
else
{
lg(Warning, "recv error,fd is: %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
}
void Dispather(fd_set &rfds)
{
for (int i = 0; i < fd_num_max; i++)
{
int fd = fd_array[i];
if (fd == defaultfd)
continue;
if (FD_ISSET(fd, &rfds))
{
if (fd == _listensock.Fd())
{
Accepter(); // 连接管理器
}
else
{
Recver(fd, i);
}
}
}
}
void PrintFd()
{
cout << "online fd list: ";
for (int i = 0; i < fd_num_max; i++)
{
if (fd_array[i] == defaultfd)
continue;
cout << fd_array[i] << " ";
}
cout << endl;
}
void Start()
{
int listensock = _listensock.Fd();
fd_array[0] = listensock;
while (true)
{
fd_set rfds;
FD_ZERO(&rfds); // 读位图置零
int maxfd = fd_array[0];
for (int i = 0; i < fd_num_max; i++)
{
if (fd_array[i] == defaultfd)
continue;
FD_SET(fd_array[i], &rfds);
if (maxfd < fd_array[i])
{
maxfd = fd_array[i];
lg(Info, "max fd update,max fd is: %d", maxfd);
}
} // 这个循环为了每次重新设置要等待的文件描述符到位图中,并更新maxfd的值以便后续调用。
struct timeval timeout = {1, 0};
// 如果事件就绪,上层不处理。select就会一直通知。
int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
switch (n)
{
case 0:
cout << "time out,timeout: " << timeout.tv_sec << " " << timeout.tv_usec << endl;
break;
case -1:
cerr << "select error" << endl;
break;
default:
// 有事情就绪
cout << "get a new link!!!" << endl;
Dispather(rfds);
break;
}
}
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_num_max];
};