【Linux】网络编程 —— 五种IO模型

🌈欢迎来到Linux专栏 ~~ 网络编程

网络编程

🌏关于IO的理解

🔥IO是什么?本质是什么?

Input / Output:外设 ------ 内存 :网络通信本质属于IO范畴!

所以IO的本质:等 + 拷贝 。如果我们感觉到IO慢,其实是在单位时间内等待的时间占比太大了!因为拷贝的效率是和硬件强相关的,如果我们要提高IO效率,不能从拷贝出下功夫👊

🔥什么叫做高效的IO? 如何设计高效IO

所谓高效的IO ,就是在单位时间内,IO中等待的比重越低,那么IO的效率就高了

🌏五种IO模型

我们以钓鱼为例 :钓鱼 = 等待 + 钓;那么是钓鱼效率高是钓的快还是等待的少呢?

张三:一直关注鱼漂,鱼上钩立马识别,完成钓鱼。如果鱼漂不动,什么都不做

李四:轮询式的关注鱼漂,不会因为鱼漂没有鱼咬钩而卡住自己,边玩手机哈哈

王五:把铃铛🔔挂在鱼竿上,自己做别的事,铃铛来通知自己

赵六: 把一百根鱼缸都插到海边,同时检测多个鱼竿的鱼漂事件

田七: 让秘书小王帮他去钓鱼,自己高高挂起

以上就是五种钓鱼方式,那么上面哪个人的钓鱼效率最高呢?

因为钓鱼 = 等待 + 钓起。但是赵六有100根鱼竿,其他人就一根

  • 鱼咬赵六的概率就是100/104 ,其他人就通通是1/104 也就是赵六等待上钩的时间永远比其他人要低,所以赵六的钓鱼概率是最高的(赵六一次追100个女孩哈哈)

张三 = 阻塞IO

李四 = 非阻塞IO

王五 = 信号驱动IO

赵六 = 多路复用 or 多路转接

田七 = 异步IO

就是对应的五种IO模型,最常使用的是阻塞、非阻塞、多路复用

🔥阻塞IO

阻塞IO :在内核将数据准备好之前,系统调用会⼀直等待。所有的套接字,默认都是阻塞方式

阻塞IO和非阻塞IO:差异在于等待事件就绪的方式不同 ------ 不就是二者都要等待吗 ------ 💥二者效率对于钓鱼这件事来说 效率是一样的

但是我们怎么感觉非阻塞IO 可以做更多的事情呢?阻塞和非阻塞在钓鱼(IO)上效率是一样的没错,非阻塞可以利用等待的时间去做其他事情啊 ,那么这个其他事情可以是读取另外一个文件描述符呢? 这样说非阻塞的效率高是可以的

  • 🔥非阻塞在单位时间内可以做更多事情!

但是我们现在只讨论一个文件描述符的情况下讨论效率高低 阻塞和非阻塞效率是一样的

🔥非阻塞IO

非阻塞IO :如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码

非阻塞IO往往需要程序员循环的方式反复尝试读写无件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用

🔥信号驱动IO

信号驱动IO :内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作

cpp 复制代码
signal(SIGIO, handler);

          ① 注册 SIGIO
应用程序 -----------------> 内核
   |
   | 做其他事情
   |
   |                     socket 数据到达
   |                            |
   |                            v
   | <------------------- ② SIGIO 信号
   |
   | ③ 信号处理函数执行
   v
recv()/read()

🔥多路复用

IO多路转接 :虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个多件描述符的就绪状态

  • select 去等待数据,同时等待多个文件描述符,一旦一个数据就绪了就拷贝数据
  • recvfrom 拷贝数据

💥同步IO 🆚 异步IO

IO分为两种:同步IO 和异步IO,上述的都是同步IO

同步和异步关注的是消息通信机制

  • 同步IO :不仅要参与发起IO,还要参与IO的过程,其中等待和钓鱼参与其中一个就是同步IO

  • 异步IO :只参与发起IO,但是不参与IO的任何细节

  • 所谓同步 ,就是在发出一个调用时,在没有得到结果之前,该调用就不返回 。但是一旦调用返回,就得到返回值了;换句话说,就是由调用者主动等待这个调用的结果;

  • 异步 则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果 ;换句话说,当⼀个异步过程调用发出后,调用者不会立刻得到结果;而是在调用发出后,被调用者通过状态、通知来通知调用者 ,或通过回调函数处理这个调用。田七给小王发布钓鱼任务后就不管了,直接回家,后续有结果也是小王打电话回来告诉田七

🔥异步IO

只参与发起IO,但是不参与IO的任何细节

由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

  • OS负责等待数据与拷贝

🌏探究 非阻塞IO

c 复制代码
int recv(int sockfd, void *buf, size_t len, int flags);

对于recv来说,flags参数代表的是接收方式

flags 作用
0 默认接收
MSG_PEEK 偷看数据,但不把数据从接收缓冲区移走
MSG_DONTWAIT 本次 recv() 使用非阻塞方式
MSG_WAITALL 尽量等到指定长度的数据全部到齐再返回
MSG_OOB 接收带外数据

也就是说recv可以设置成非阻塞解锁,同理open文件也可以用O_NDELAY来设置

这里想说明的意思是设置成非阻塞的方式可以有很多! 但有没有一种通用的方式来设置呢??

🔥fcntl

一个文件描述符,默认都是阻塞IO ,后续无论是read、write操作此文件都是非阻塞的

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, .../* arg */ );

传入的cmd的值不同,后面追加的参数也不相同

fcntl函数有5种功能:

  • 复制⼀个现有的描述符(cmd=F_DUPFD)
  • 获得/设置文件描述符标记 (cmd=F_GETFD或F_SETFD)
  • 💥获得/设置文件状态标记 (cmd=F_GETFL或F_SETFL)
  • 获得/设置异步I/O所有权 (cmd=F_GETOWN或F_SETOWN)
  • 获得/设置记录锁 (cmd=F_GETLK,F_SETLK或F_SETLKW)

我们此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞

实现函数SetNoBlock

基于fcntl,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞

cpp 复制代码
void SetNoBlock(int fd) {
	int fl = fcntl(fd, F_GETFL);
	if (fl < 0) {
		perror("fcntl");
		return;
	}
	fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

使用 F_GETFL 将当前的文件描述符的属性取出来(这是一个位图)

然后再使用 F_SETFL 将文件描述符设置回去。设置回去的同时,加上一个O_NONBLOCK参数

轮询方式读取标准输入

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <system_error> 
#include <unistd.h>
#include <fcntl.h>

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()
{
    char inbuffer[1024];
    SetNoBlock(0);
    while(1)
    {
        ssize_t n = read(0, inbuffer, sizeof(inbuffer) - 1);
        if(n > 0)
        {
            inbuffer[n-1] = 0;
            std::cout << "echo # " << inbuffer << std::endl;
        }
        else {
            //可以干其他事情
            std::cerr << "error ..." << std::endl;
        }
        sleep(1);
    }
}

其实上面代码有一个逻辑上错误:当我们读到文件结尾 的时候呢?if(n == 0)

当我们 读取失败 和 读取文件fd时,数据还没有就绪的时候都是以出错形式来进行返回的

  • 所以我们必须区分开这两种情况,总不可能说数据没有就绪也算出错吧!

怎么区分出示读取出错 还是数据没就绪呢?

库函数、系统调用(本质也是库函数,C封装)会有一个全局变量errno:错误码

也有可能出现,某个进程接受到了某个信号导致提前唤醒!EINTR:系统调用被信号中断

cpp 复制代码
进程阻塞在 read()
       ↓
突然收到信号
       ↓
执行信号处理函数
       ↓
read() 被提前打断
       ↓
返回 -1
errno = EINTR
cpp 复制代码
else {
    //数据还没就绪
    if(errno == EAGAIN)
    {
        //操作被阻塞
        std::cout << "data is not ready" << std::endl;
    }
    else if (errno == EINTR)
    {
        std::cout << "read 被信号中断" << std::endl;
        continue;
    }
    else 
    {
        //读取失败
        std::cout << "read error" << errno << std::endl;
        std::cout << "read error" << strerror(errno) << std::endl;
    }
}

📢写在最后

接下来登场的是 epoll

相关推荐
程序员-Benothing1 小时前
Shell脚本调试与最佳实践:set -eux、shellcheck实战
linux·运维·服务器
qq_369173631 小时前
一句话将 Word、PPT、PDF 发布成链接
人工智能·pdf·word·powerpoint·效率工具·ai 办公
invicinble1 小时前
让AI生成一套系统的完整提示词
人工智能
Stark-C1 小时前
Obsidian轻量化替代,MCP原生AI加持!NAS部署现代化私有知识库
人工智能
yi0111 小时前
DAY19: LeetCode 28 找出字符串中第一个匹配项的下标
人工智能·笔记·python·算法·leetcode
海宇AI1 小时前
零信任架构实战:基于海宇活体识别V步骤1构建自动化直播开播鉴权网关
运维·人工智能·架构·自动化
ESDWAN1 小时前
跨境电商网络专线怎么选?从带宽、延迟到SD-WAN部署与带宽管理的完整落地指南
开发语言·网络·php
数商思语行1 小时前
Palantir Study 27|Global Branching:安全修改生产 Ontology
人工智能·ai·供应链·商业分析·ontology·本体·fde
️公子1 小时前
决策模型开战:Jev / Decisions API / Strands Decider 的 ClosedSet、置信度闸门与混合 Agent
人工智能·大模型·软件工程·agent·决策模型