Linux 高级 IO
非阻塞 IO:fcntl
fcntl 是 Linux 系统编程中一个非常重要的函数,全称为 File Control,即文件控制。它提供了对文件描述符的广泛控制,包括复制文件描述符、获取/设置文件描述符标志、获取/设置文件锁以及获取/设置文件描述符的所有者等。
Linux 高级 IO 机制中 select 是经典的多路复用模型。文章介绍非阻塞 IO 实现及 select 函数原型、参数含义、socket 就绪条件。分析 select 特点与缺点,如 fd 数量限制、用户态内核态拷贝开销等。通过示例代码展示单进程多服务器消息交流的实现逻辑,说明 select 在并发处理中的优势与局限性,为后续学习 epoll 等机制奠定基础。

fcntl 是 Linux 系统编程中一个非常重要的函数,全称为 File Control,即文件控制。它提供了对文件描述符的广泛控制,包括复制文件描述符、获取/设置文件描述符标志、获取/设置文件锁以及获取/设置文件描述符的所有者等。
一个文件描述符默认都是阻塞 IO,函数原型如下:
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, .../* arg */);
后面追加的参数根据 cmd 的值的不同而产生不同。
fcntl 函数有 5 种功能:
我们现在只需要使用第三个功能,就能满足当前需要,将一个文件描述符设置为非阻塞。
基于 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;
}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 函数来实现多路复用输入/输出模型:select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在 select 这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
- 参数 nfds 是需要监视的最大的文件描述符值 +1
- rdset, wrset, exset 分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合
- 参数 timeout 为结构 timeval,用来设置 select() 的等待时间
参数 timeout 取值:
- NULL:则表示 select()没有 timeout,select 将一直被阻塞,直到某个文件描述符上发生了事件
- 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 的全部位
关于 timeval 结构: timeval 结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为 0。
函数返回值:
- 执行成功则返回文件描述词状态已改变的个数
- 如果返回 0 代表在描述词状态改变前已超过 timeout 时间,没有返回
- 当有错误发生时则返回 -1,错误原因存于 errno,此时参数 readfds,writefds, exceptfds 和 timeout 的值变成不可预测
错误值可能为:
- EBADF:文件描述词为无效的或该文件已关闭
- EINTR:此调用被信号所中断
- EINVAL:参数 n 为负值
- ENOMEM:核心内存不足
读就绪:
写就绪:
可监控的文件描述符个数取决于 sizeof(fd_set) 的值。我这边服务器上 sizeof(fd_set)=512,每 bit 表示一个文件描述符,则我服务器上支持的最大文件描述符是 512*8=4096
将 fd 加入 select 监控集的同时,还要再使用一个数据结构 array 保存放到 select 监控集中的 fd。一是用于再 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET 判断。二是 select 返回后会把以前加入的但并无事件发生的 fd 清空,则每次开始 select 前都要重新从 array 取得 fd 逐一加入 (FD_ZERO 最先),扫描 array 的同时取得 fd 最大值 maxfd,用于 select 的第一个参数。
每次调用 select,都需要手动设置 fd 集合,从接口使用角度来说也非常不便
每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
select 支持的文件描述符数量太小
讲了这么多,就让我们用用 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 = _rfds_array[i]->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"); break;
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 被我们放弃,但我们也不会损失什么,因为后面还有更厉害的工具等待着我们。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
暂无推荐文章,稍后可再来查看。
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online