Linux网络编程IO管理

news2024/12/25 9:51:16

网络 IO 涉及到两个系统对象,一个是用户空间调用 IO 的进程或者线程,一个是内核空间的内核系统,比如发生 IO 操作 read 时,它会经历两个阶段:

  1. 等待内核协议栈的数据准备就绪;
  2. 将内核中的数据拷贝到用户态的进程或线程中。
    由于在以上两个阶段产出的不同情况,就出现了多种网络 IO 管理方法,即网络 IO 模型。

五种网络 IO 模型

阻塞 IO(blocking IO)

在 Linux 中,默认情况下所有 socket 都是 blocking,一个典型的读操作流程如下:

image.png

当用户进程调用了 read 这个系统调用,kernel 就开始了 IO 的第一个阶段:准备数据。对于 network io 来说,很多时候的数据是没有就绪的(比如很多时候还没有收到一个完整的数据包),那么整个进程就会被阻塞;当内核将数据准备好了,才会将数据从内核空间拷贝到用户态内存,然后 kernel 返回结果,用户态进程才会解除阻塞继续运行。
所以,block io 在 io 执行的两个阶段都被 block 了(数据准备和数据拷贝)。所有程序员解除网络编程都是从 listen recv send,开始的,这些都是阻塞型接口。可以很方便地构建一个服务器-客户机模型,下面是一个简单的模型结构:

image.png

//接受缓冲区大小
#define BUFFER_LENGTH       1024

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(struct sockaddr_in));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
        return -1;
    }

    listen(sockfd, 10);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
    printf("accept\n");

    while(1)
    {
        char buffer[BUFFER_LENGTH] = {0};
        int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
        printf("ret: %d, buffer: %s\n", ret, buffer);
        send(clientfd, buffer, ret, 0);
    }
}

大部分的 socket 接口都是阻塞型的。所谓的阻塞型接口是指系统调用不返回调用结果并让当前线程一直阻塞,只有当该系统调用获得结果或超时出错时才返回。
这些阻塞的接口给网络编程带来了很大的问题,如在调用 send() 的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或相应任何网络请求。
一个简单的改进方案就是在服务器端使用多线程(或多进程)。让每个连接都有独立的线程/进程,这样任何一个链接的阻塞都不会影响他的连接。具体使用多进程还是多线程没有一个特定的模式。传统意义上,进程的开销要远大于线程,所以要同时为较多的客户机提供服务,则不推荐多进程;如果单个服务执行体需要消耗较多的 CPU 资源,比如需要进行大规模或长时间的数据运算或文件访问,则进程较为安全。通常,使用 pthread_create() 创建新线程,fork() 创建新进程。
我们假设对上述服务器/客户机模型提出更高的要求,即让服务器同时为多个客户机提供服务,就有了以下模型。

image.png

#define BUFFER_LENGTH       1024

//线程函数
void *client_thread(void *arg)
{
    int clientfd = *(int*)arg;

    while(1)
    {
        char buffer[BUFFER_LENGTH] = {0};
        int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);

        if(ret == 0)
        {
            close(clientfd);
            break;
        }
        printf("ret: %d, buffer: %s\n", ret, buffer);

        send(clientfd, buffer, ret, 0);
    }
}

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(struct sockaddr_in));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
        return -1;
    }

    listen(sockfd, 10);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    while(1)
    {
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        pthread_t threadid;
        //将clientfd昨晚参数传入线程
        pthread_create(&threadid, NULL, client_thread, &clientfd);
    }
}

在上面的模型中,主线程持续等待客户端的连接请求,如果有连接,则创建新线程,并在新线程中提供和前面相同的服务。
很多初学者可能不明白为何一个 socket 可以 accept 多次。实际上 socket 的设计者
可能特意为多客户机的情况留下了伏笔,让 accept () 能够返回一个新的 socket。下面是
Accept 接口的原型:

int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

输入参数 s 是从 socket(),bind() 和 listen() 中沿用下来的 socket() 句柄值。执行完 bind() 和 listen() 后,操作系统会在指定的端口处监听所有连接请求,如果有请求,则将该连接请求加入请求队列。调用 accept() 接口正是从 socket s 的请求队列抽取第一个连接信息,创建一个与 s 同类的新 socket 返回句柄。新的 socket 句柄即后续 read() 和 recv() 的输入参数。如果请求队列当前没有请求,则 accept() 将进入阻塞状态直到有请求进入队列。
上述多线程服务器模型几乎完美解决了多个客户机提供问答服务的要求,但其实并不尽然。如果要同时相应成百上千的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界相应效率,而线程与进程本省也容易进入假死状态。
对于可能面临的同时出现的上千次次甚至上万次的客户端请求,“线程池”和“连接池”等池化组件或许可以缓解部分压力,但是不能解决所有问题。总之,多线程模型可以方便高效的解决小规模服务请求,但是对面大规模的服务请求,多线程模型也会遇到瓶颈,可以用非阻塞接口来解决这个问题。

非阻塞 IO(non-blocking IO)

Linux 下,可以通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程如下:

image.png

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    memset(&servaddr, 0, sizeof(struct sockaddr_in));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if(-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s", strerror(errno));
        return -1;
    }

    listen(sockfd, 10);
    // sleep(10);

    printf("sleep\n");
    int flags = fcntl(sockfd, F_GETFL, 0);
    flags |= O_NONBLOCK;
    fcntl(sockfd, F_SETFL, flags);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

    while(1)
    {
        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
        printf("accept\n");
    }
}

从图中看出,当用户发出 read 操作时,如果 kernel 中的数据还没有准备好,那么它并不会 block 用户进程,而是立刻返回一个 error。从用户的角度来讲,它发起一个 read 操作后,并不需要等待,而是马上得到一个结果。用户进程判断结果是一个 error 时,他就知道数据还没准备好,于是它可以再次发送 read 操作。一旦 kernel 中的数据准备好了,并且再次收到用户进程的 system call,那么它马上就将数据拷贝到用户内存,然后返回,所以,在非阻塞 IO 中,用户进程其实是不需要主动询问 kernel 数据准备好了没有。
在非阻塞状态下,recv() 接口在被调用后立刻返回,返回值代表了不同的含义,如在上面的例子中:

  • recv()返回值大于 0 ,表示接受数据完毕,返回值即是接受到的字节数;
  • recv()返回 0 ,表示连接已经正常断开;
  • recv()返回 1 ,且 errno 等于 EAGAIN ,表示 recv 操作还没执行完成;
  • recv()返回 1 ,且 errno 不等于 EAGAIN ,表示 recv 操作遇到系统错误 errno 。

非阻塞的接口相比阻塞接口的显著差异在于,在被调用之后立刻返回。使用如下的函数可以将某句柄 fd 设为非阻塞状态。

fcntl(fd, F_SETFL, O_NONBLOCK);

多路复用 IO(IO multiplexing)

这种模型的好处在于,单个 process 可以同时处理多个网络连接的 IO。他的基本原理就是 select/epoll 这个 function 会不断轮询所负责的所有 socket,当某个 socket 有数据到达,就通知用户进程。流程如下:

image.png

当用户进程调用了 select,那么整个进程就会被 block,而同时,kernel 会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从 kernel 拷贝到用户进程。
在多路复用模型中,对于每一个 socket,一般都会设置成 non-blocking,但是,如上图所示,其实整个用户的 process 都是 block 的。只不过 process 是被 select 这个函数 block,而不是被 socket IO 给 block。因此 select() 与非阻塞 IO 类似。
大部分 Unix/Linux 都支持 select 函数,该函数用于探测多个文件句柄的状态变化。下面给出 select 接口的原型:

FD_ZERO(int fd, fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

这里,fd_set 类型可以简单的理解为按 bit 位标记的句柄队列,例如要在某 fd_set 中标记一个值为 16 的句柄,则该 fd_set 的第 16 个 bit 位被标记为 1。具体的位置、验证可使用 FD_SET、FD_ISSET 等宏实现。在 select() 函数中,readfd、writefds 和 exceptfds 同时作为输入参数和输出参数。如果输入的 readfds 标记了 16 号句柄,则 select() 将检测 16 号句柄是否可读。在 select() 返回后,可以通过检查 readfds 是否标记 16 号句柄来判断“可读”事件是否发生。另外,用户可以设置 timeout 时间。
这里需要指出的是,客户端的一个 connect() 操作,将在服务器端激发一个“可读事件”,所以 select() 可能探测来自客户端的 connect() 行为。
上述模型中,最关键的地方是如何动态维护 select() 的三个参数,readfds、writefds 和 exceptfds。作为输入参数,readfds 应该标记所有的需要探测的“可读事件”的句柄,其中永远包括那个探测 connect() 的那个“母”句柄(使用 FD_SET() 标记)。
作为输出参数,readfds、writefds 和 exceptfds 中中保存了所有 select 捕捉到的所有事件的句柄值。程序员需要检查的所有的标记位(使用 FD_ISSET() 检查),以确定到底那些句柄发生了事件。
在上面的一问一答模式中,如果 select() 发现某句柄捕捉到可“可读事件”,服务器程序应及时做 recv() 操作,并且根据接收到的数据准备好发送数据,并将对应的句柄值加入 writefds,准备下一次“可写事件”的 select() 探测。探测。同样,如果 select() 发现某句柄捕捉到“可写事件”,则程序应及时做 send() 操作,并准备好下一次的可读事件探测准备。
这种模型的特征在于每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应。我们可以将这种模型叫做“事件驱动模型”。
但这个模型依旧有着很多问题。首先 Select () 接口并不是实现事件驱动的最好选择。因为当需要探测的句柄值较大时, select () 接口本身需要消耗大量时间去轮询各个句柄。很多操作系统提供了更为高效的接口,如 linux 提供了 epoll BSD 提供了 kqueue Solaris 提供了 /dev/poll 。如果需要实现更高效的服务器程序,类似 epoll 这样的接口更被推荐。遗憾的是不同的操作系统特供的 epoll 接口有很大差异,所以使用类似于 epoll 的接口实现具有较好跨平台能力的服务器会比较困难。

异步 IO(Asynchronous IO)

image.png

当用户进程发起 read 操作后,就立刻做其他的事情。另一方面,从 kernel 的角度,当他收到一个 asynchronous read 后,它会立刻返回,所以不会对用户进程产生任何 block。然后,kernel 会等数据准备好之后,将数据拷贝到用户空间内存,当一切都完成后,kernel 会给用户进程发送一个 signal 告诉他 read 操作完成了。

到目前为止,已经介绍了四个 IO 模型。现在来回答最初的几个问题:blocking 和 non-blocking 的区别在哪里?synchronous IO 和 asynchronous IO 的区别在哪里?

先回答简单的:blocking 和 non-blocking。调用 blocking IO 会一直 block 进程直到操作完成,而 non-blocking IO 在 kernel 还在准备数据的情况下会直接返回。
synchronous 和 asynchronous 的区别在于 synchronous 在进行 IO opration 的时候回将 process block 但是 asynchronous 不会。所以前面介绍的 blocking IO,non-blocking IO 和 IOmultiplexing 都是 synchronous。但是这时候就会有人问,non-blocking 不是不会 block process 吗。这里有一个需要注意的地方,non-blocking 只是在执行 read 这个系统调用的情况下 kernel 会直接返回,但是在 kernel 准备好数据拷贝到 application 的时候,依然会对 process block。所以她在 IO 操作上依然有阻塞的部分。而 asynchronous IO 不一样,当进程发起 IO 操作信号后直接返回不理睬,直到 kernel 发出 IO 操作完成的信号,中间没有任何阻塞。

信号驱动 IO(signal driven IO, SIGIO)

在我们安装信号函数之后,看进程继续运行并不阻塞。数据准备好之后,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 IO 操作函数处理数据。我们可以在信号处理函数中调用 read 读取数据,并通知主循环数据准备好;也可以立刻通知主循环让它读取数据。这种模型的优势在于等待数据包到达器件,可以继续执行,不被阻塞。免去了 select 的阻塞与轮询,当有 socket 活跃时,由 handler 处理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2109456.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue3 json格式化显示数据(vue3-json-viewer) 对比修改前后数据

需求:对比变更前后数据 npm包下载 npm install vue3-json-viewer --savemain.ts中全局引用 // json可视化 import JsonViewer from "vue3-json-viewer" import "vue3-json-viewer/dist/index.css";app.use(JsonViewer).mount("#app&quo…

鸿蒙界面开发——组件(6):属性字符串(StyledString)文本输入

属性字符串StyledString/MutableStyledString MutableStyledString继承于StyledString,以下统一简称StyledString。 是功能强大的标记对象,可用于字符或段落级别设置文本样式。 通过将StyledString附加到文本组件, 可以通过多种方式更改文本…

深度学习-用神经网络NN实现足球大小球数据分析软件

文章目录 前言一、 数据收集1.1特征数据收集代码实例 二、数据预处理清洗数据特征工程: 三、特征提取四、模型构建五、模型训练与评估总结 前言 预测足球比赛走地大小球(即比赛过程中进球总数是否超过某个预设值)的深度学习模型是一个复杂但有…

霍尼韦尔、书客、米家护眼大路灯怎么样?终极测评对比和护眼灯王者机型

霍尼韦尔、书客、米家护眼大路灯怎么样?护眼大路灯的重要性不容忽视,它是我们日常生活中用眼的必备工具,也是眼睛能够得到保护重要一环。近年来,护眼大路灯市场呈现出国际大牌的垄断局面,但这也带来了一些问题。为了争…

油猴插件录制请求,封装接口自动化参数

参考:如何使用油猴插件提高测试工作效率 一、背景 在酷家乐设计工具测试中,总会有许多高频且较繁琐的工作,比如: 查询插件版本:需要打开Chrome控制台,输入好几个命令然后过滤出版本信息。 查询模型商品&…

java设计模式day03--(结构型模式:代理模式、适配器模式、装饰者模式、桥接模式、外观模式、组合模式、享元模式)

5,结构型模式 结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。 由于组合关系或聚合关系比继承关系耦合度低,满足“…

SpingBoot中使用Swagger快速生成接口文档

目录 一.Swagger快速上手 二.Swagger中的基本注解 三.使用Swagger进行测试 一.Swagger快速上手 Swagger是⼀个接⼝⽂档⽣成⼯具,它可以帮助开发者⾃动⽣成接⼝⽂档。当项⽬的接⼝发⽣变更时,Swagger可以实时更新⽂档,确保⽂档的准确性和时…

【神经网络系列(高级)】神经网络Grokking现象的电路效率公式——揭秘学习飞跃的秘密【通俗理解】

【通俗理解】神经网络Grokking现象的电路效率公式 论文地址: https://arxiv.org/abs/2309.02390 参考链接: [1]https://x.com/VikrantVarma_/status/1699823229307699305 [2]https://pair.withgoogle.com/explorables/grokking/ 关键词提炼 #Grokkin…

组合优化与凸优化 学习笔记3 凸函数

目前学到了73页 凸函数的定义: 人话:函数f的定义域是凸集(在一般的情况下就是不能是断开的定义域(一般的情况是1维的嘛),假如x是什么多维向量的话就是说x的取值范围是一个凸集内),并…

基于云原生向量数据库 PieCloudVector 的 RAG 实践

近年来,人工智能生成内容(AIGC)已然成为最热门的话题之一。工业界出现了各种内容生成工具,能够跨多种模态产生多样化的内容。这些主流的模型能够取得卓越表现,归功于创新的算法、模型规模的大幅扩展,以及海…

XXL-JOB调度中心与执行器

XXL-JOB是一个轻量级的分布式任务调度平台,主要由调度中心和执行器两部分组成。下面详细讲解调度中心与执行器的功能和作用。 调度中心 调度中心是XXL-JOB的核心组件,负责任务的调度管理。其主要功能包括: 任务管理:调度中心提供…

计算组合数:scipy.special.comb()

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 计算组合数: scipy.special.comb() 选择题 以下代码两次输出的结果是? from scipy.special import comb print("【执行】print(comb(3,2))") print(comb(3…

011. Oracle-约束

我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈 入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈 虚 拟 环 境 搭 建 :👉&…

小白学装修 之 硬装阶段

在准备阶段 了解了 装修的基本概念 顺利收房 进行了需求和预算的大致规划 并且完成了简单的自主设计接下来就是带着自己的设计图 预算和想法 去找公司或者施工方了 找施工方 可以是 设计师和施工方分开找 也可以找有设计的装修公司 或者 有施工能力的设计室都行 但不 管哪…

【#第三期实战营闯关作业 ## 茴香豆:企业级知识库问答工具】

今天学习了《 茴香豆:企业级知识库问答工具》这一课,对大模型的应用有了更深得认识。以下是记录本课实操过程及截图: 搭建茴香豆虚拟环境: 输入以下命令 studio-conda -o internlm-base -t huixiangdou 成功安装虚拟环境截图 …

OpenAI gym CarRacing-v2 episode termination

题意:OpenAI Gym CarRacing-v2 赛道终止处理 问题背景: I am using gym0.26.0 library and am trying to understand what means that an episode is finished/done in the CarRacing-v2 environment. In the documentation is written this. 我正在使…

用Python实现时间序列模型实战——Day 12: 状态空间模型

一、学习内容 1. 状态空间模型的基本概念 状态空间模型是一种用于时间序列分析的强大工具,能够描述具有潜在状态动态变化的系统。该模型通过显式地建模时间序列中的潜在状态(即隐藏变量),能够捕捉复杂的动态结构,适用…

如何选择合适的变压吸附制氧设备

在选择合适的变压吸附(Pressure Swing Adsorption, PSA)制氧设备时,需要综合考虑多个因素以确保设备能够高效、稳定地运行,满足特定应用场景的需求。以下是一些关键步骤和考虑因素,帮助您做出明智的决策。 1. 明确应用需求 明确您的制氧需求至…

GNU_HASH确定函数地址

前言: 最近看了以下pwntoos的DynELF方法,对其中是如何获取到函数地址的过程很感兴趣,就研究了一下,对通过DT_GNU_HASH获取函数地址的过程有了比较清晰的了解 漏洞: 我这里使用2013-PlaidCTF进行测试,首先…

DeepDFA: 受控制流分析驱动的有效深度漏洞检测

目前基于深度学习的漏洞检测中性能最高的方法使用的是基于 token 的 transformer 模型,这对于捕捉漏洞检测所需的代码语义来说并不是最有效的。文中设计了一个受数据流分析启发的图学习框架 DeepDFA,以及一种能让图学习模拟数据流计算的嵌入技术。其训练…