前言: Linux作为一个功能强大、灵活多变的操作系统,提供了丰富多样的I/O处理方式。从传统的阻塞I/O到非阻塞I/O,再到更加高效的异步I/O和内存映射I/O,每一种方式都有其独特的适用场景和性能特点。掌握这些高级I/O机制,不仅能够帮助我们更好地理解和优化系统性能,还能在开发高并发、高性能的应用程序时游刃有余。
select机制,则是Linux中处理多路复用I/O的一种经典方法。它允许一个进程同时监视多个文件描述符,以等待其中的任何一个变为可读、可写或有错误条件发生。这种机制极大地提高了I/O处理的灵活性和效率,特别是在处理大量并发连接时,select机制的优势更加明显。
让我们携手踏上这段探索之旅,一同揭开Linux高级I/O与select机制的神秘面纱。
非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射 IO(mmap),这些统称为高级IO,本篇我们则是重点讨论I/O多路转接
fcntl
是 Linux 系统编程中一个非常重要的函数,全称为 File Control,即文件控制。它提供了对文件描述符的广泛控制,包括复制文件描述符、获取/设置文件描述符标志、获取/设置文件锁以及获取/设置文件描述符的所有者等。fcntl 函数的灵活性使其成为处理文件 I/O 操作时不可或缺的工具
一个文件描述符, 默认都是阻塞IO,函数原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
后面追加的参数根据cmd的值的不同而产生不同
fcntl函数有5种功能:
获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)
我们现在只需要使用第三个功能,就能满足当前需要,将一个文件描述符设置为非阻塞
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞
void SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <cstdlib>
#include <fcntl.h>
void SetNoBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
std::cerr << "fcntl error" << std::endl;
exit(0);
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
SetNoBlock(0);
while (true)
{
char buffer[1024];
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << "echo# " << buffer << std::endl;
}
else if (s == 0)
{
std::cout << "end stdin" << std::endl;
break;
}
else
{
// 非阻塞等待,如果数据没有准备好就会按照错误返回,s == -1
// 那我们怎么知道出错的原因是数据没有准备好,还是真的出错了呢?s是怎么区分的?
// read, recv会以出错的形式告知上层,数据还没有准备好
if(errno == EWOULDBLOCK)
{
std::cout << "OS的底层数据还没有准备好, error: " << errno << std::endl;
// other
}
else if(errno == EINTR)
{
std::cout << "IO interrupted by signal, try again" << std::endl;
}
else
{
std::cout << "read error!" << std::endl;
break;
}
}
sleep(1);
}
return 0;
}
我们不断的去查看数据是否准备好,只要准备好我们就拿走,没有准备好,我们就去做其他事情
初识select:
系统提供select函数来实现多路复用输入/输出模型:
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
参数解释:
参数timeout取值:
关于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的全部位
关于timeval结构:
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0
函数返回值:
错误值可能为:
EBADF
文件描述词为无效的或该文件已关闭EINTR
此调用被信号所中断EINVAL
参数n 为负值。ENOMEM
核心内存不足读就绪:
写就绪:
讲了这么多,就让我们用用select正是操作一把,单进程实现多服务器消息交流,体现多路转接的真正实力
SelectServer:
#pragma once
#include <iostream>
#include <string>
#include <sys/select.h>
#include "Log.hpp"
#include "Socket.hpp"
using namespace Net_Work;
const static int gdefaultport = 8888;
const static int gbacklog = 8;
const static int num = sizeof(fd_set) * 8;
class SelectServer
{
private:
void HandlerEvent(fd_set &rfds)
{
for (int i = 0; i < num; i++)
{
if (_rfds_array[i] == nullptr)
continue;
// 合法的fd
// 读事件分两种,一类是新链接的到来,一类是新数据的到来
int fd = _rfds_array[i]->GetSocket();
if (FD_ISSET(fd, &rfds))
{
// 读事件就绪 -> 新链接的到来
if (fd == _listensock->GetSocket())
{
lg.LogMessage(Info, "get a new link\n");
std::string clientip;
uint16_t clientport;
// 这里不会阻塞,因为select已经检测到listensock就绪了
Socket *sock = _listensock->AcceptConnection(&clientip, &clientport);
if (!sock)
{
lg.LogMessage(Error, "accept error\n");
return;
}
lg.LogMessage(Info, "get a client, client info is# %s:%d, fd:%d\n", clientip.c_str(), clientport, sock->GetSocket());
// 获取成功了,但是我们不能直接读写,底层的数据不确定是否就绪
// 新链接fd到来时,要把新链接fd交给select托管 --- 只需要添加到数组_rfds_array中即可
int pos = 0;
for (; pos < num; pos++)
{
if (_rfds_array[pos] == nullptr)
{
_rfds_array[pos] = sock;
break;
}
}
if (pos == num)
{
sock->CloseSocket();
delete sock;
lg.LogMessage(Warning, "server is full ... !\n");
}
}
// 新数据的到来
else
{
std::string buffer;
bool res = _rfds_array[i]->Recv(&buffer, 1024);
if(res)
{
lg.LogMessage(Info, "client say# %s\n", buffer.c_str());
buffer.clear();
}
else
{
lg.LogMessage(Warning, "client quit, maybe close or error, close fd: %d\n", _rfds_array[i]->GetSocket());
_rfds_array[i]->CloseSocket();
delete _rfds_array[i];
_rfds_array[i] = nullptr;
}
}
}
}
}
public:
SelectServer(int port = gdefaultport)
: _port(port), _listensock(new TcpSocket())
{
}
void InitServer()
{
_listensock->BuildListenSocketMethod(_port, gbacklog);
for (int i = 0; i < num; i++)
{
_rfds_array[i] = nullptr;
}
_rfds_array[0] = _listensock.get();
}
void Loop()
{
_isrunning = true;
while (_isrunning)
{
// 不能直接accept新连接,而是要将selete交给selete, 只有selete有资格知道IO事件有没有就绪
fd_set rfds;
FD_ZERO(&rfds);
int max_fd = _listensock->GetSocket();
for (int i = 0; i < num; i++)
{
if (_rfds_array[i] == nullptr)
{
continue;
}
else
{
int fd = _listensock->GetSocket();
FD_SET(fd, &rfds); // 添加所有合法的fd到rfds集合中
if (max_fd < fd) // 更新最大fd
{
max_fd = fd;
}
}
}
// 遍历数组,1.找最大值 2.合法的fd添加到rfds集合中
// 定义时间
struct timeval timeout = {0, 0};
// rfds本质是一个输入输出型参数,rfds是在select调用返回的时候,不断被修改,所以每次都要重置
PrintDebug();
int n = select(max_fd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
switch (n)
{
case 0:
lg.LogMessage(Info, "select timeout ... last time: %u.%u\n", timeout.tv_sec, timeout.tv_usec);
break;
case -1:
lg.LogMessage(Error, "select error !!! \n");
default:
lg.LogMessage(Info, "select success, begin event handler, last time: %u.%u\n", timeout.tv_sec, timeout.tv_usec);
HandlerEvent(rfds);
break;
}
}
_isrunning = false;
}
void stop()
{
_isrunning = false;
}
void PrintDebug()
{
std::cout << "current select rfds list is: ";
for (int i = 0; i < num; i++)
{
if (_rfds_array[i] == nullptr)
continue;
else
std::cout << _rfds_array[i]->GetSocket() << " ";
}
std::cout << std::endl;
}
~SelectServer()
{
}
private:
std::unique_ptr<Socket> _listensock;
int _port;
bool _isrunning;
Socket *_rfds_array[num];
};
虽然说select实现了我们之前从未做到过的功能,select 只负责等待,可以等待多个fd,IO的时候,效率比较高一些,但是对于它的缺点来说,它还是不适合我们使用的
缺点:
int n = select(max_fd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
// max_fd + 1 表示的是
正因为这些缺点,select被我们放弃,但我们也不会损失什么,因为后面还有更厉害的工具等待着我们
随着我们一同走过这段关于Linux高级I/O与select机制的学习之旅,我们不难发现,这些技术不仅是系统编程中的关键要素,更是提升应用程序性能和稳定性的有力武器。从非阻塞I/O到异步I/O,从内存映射到文件锁定,再到select机制的多路复用处理,每一项技术都为我们打开了新的视角,让我们能够更加深入地理解和优化系统行为。
最后,让我们携手开启系统编程的新篇章,继续深入探索Linux的奥秘,共同推动技术的进步和发展。在未来的日子里,愿我们都能在技术的海洋中畅游,收获满满的知识与智慧。再见!
希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行! 谢谢大家支持本篇到这里就结束了,祝大家天天开心!