Linux网络编程IO管理

网络 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       1024int 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.mzph.cn/pingmian/53375.shtml

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

相关文章

Kafka【八】如何保证消息发送的可靠性、重复性、有序性

【1】消息发送的可靠性保证 对于生产者发送的数据,我们有的时候是不关心数据是否已经发送成功的,我们只要发送就可以了。在这种场景中,消息可能会因为某些故障或问题导致丢失,我们将这种情况称之为消息不可靠。虽然消息数据可能会…

Spring框架基础介绍2.0

目录 AOP概述 面向切面思想 优点: 核心原理: 使用案例: AOP 的基本概念 springAOP 实现 AspectJ 中常用的通知 Spring事物管理 数据库事务管理? spring 事务管理? Spring中的事物管理分为两种形式: 1、编程式事物管理 2、声明…

低空经济如此火爆,新手如何分一杯羹?

低空经济的火爆为新手提供了诸多参与和分一杯羹的机会。以下是一些具体的建议,帮助新手在这一领域找到切入点: 1. 了解行业概况与趋势 定义与范围:低空经济是指在3000米以下空域内进行各种有人和无人驾驶航空器活动的经济形态,涉…

dubbo的SPI机制

一.dubbo的SPI机制 SPI机制是一个服务发现机制,通过接口的全限定名找到指定目录下对应的文件,然后加载对应的实现类注册到系统中进行使用。 在Java原生跟mysql的驱动加载也使用了这个机制,但是他们只能进行全部实现类的加载(遍历…

最新HTML5中的文件详解

第5章 HTML5中的文件 5.1选择文件 可以创建一个file类型的input,添加multiple属性为true,可以实现多个文件上传。 5.1.1 选择单个文件 1.功能描述 创建file类型input元素,页面中不再有文本框,而是 选择文件 按钮,右侧是上次文件的名称&a…

数据分析面试题:客户投保问题分析

目录 0 场景描述 1 数据准备 2 问题分析 2.1 计算小微公司的平均经营时长 2.2 计算小微公司且角色为投保人,保险起期在18年的总保费 2.3 假设,DWD_CUSTOMER_REL客户关联关系表中,存在部分客户保单数很多,部分客户保单数很少的情况,此时DWD_CUSTOMER_BASE表关联,程序…

百度智能云向量数据库创新和应用实践分享

本文整理自第 15 届中国数据库技术大会 DTCC 2024 演讲《百度智能云向量数据库创新和应用实践分享》 在 IT 行业,数据库有超过 70 年的历史了。对于快速发展的 IT 行业来说,一个超过 70 年历史的技术,感觉像恐龙一样,非常稀有和少…

Anaconda Prompt 安装paddle2.6报错

bug描述 python 3.11.9 通过 pip install paddlepaddle2.6.1 安装后,运行 paddle.utils.run_check() 则出现下面的错误: 解决办法 方法一:使用paddle 3的版本 这里要注意我的python版本 方法二:使用低版本的python python3.9…

Lombok jar包引入和用法

大家好,今天分享一个在编写代码时的快捷方法。 当我们在封装实体类时,会使用set、get等一些方法。如下图,不但费事还影响代码的美观。 那么如何才能减少代码的冗余呢,首先lib中导入lombok的jar包并添加库。 此处我已导入&#xf…

Jenkins+Svn+Vue自动化构建部署前端项目(保姆级图文教程)

目录 介绍 准备工作 配置jenkins 构建部署任务 常见问题 介绍 在平常开发前端vue项目时,我们通常需要将vue项目进行打包构建,将打包好的dist目录下的静态文件上传到服务器上,但是这种繁琐的操作是比较浪费时间的,可以使用jenkins进行自动化构建部署前端vue 准备工作 准备…

《粮食科技与经济》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答 问:《粮食科技与经济》是不是核心期刊? 答:不是,是知网收录的第一批认定学术期刊。 问:《粮食科技与经济》级别? 答:省级。主管单位: 湖南省粮食和物资储备局 …

bat批处理实现从特定文件夹中提取文件内容并以父文件夹名存储

1、需求分析 标题是bat批处理实现从特定文件夹中提取文件内容并以父文件夹名存储。这里面我们要做的工作是: ①、批处理脚本使用的是bat文件; ②、文件夹下面有很多子文件夹,然后子文件夹下仍然有相同的文件结构,我们需要从三级…

halcon 自定义距离10的一阶导数幅图,摆脱sobel的3掩码困境

一,为什么要摆脱3的掩码 在处理图像的过程中,会用到平滑算子,很容易破坏边际,所谓的一阶导数sobel只计算掩码为3的差分,在幅度图分割中,往往是很难把握的。 举个例子-现在图像头平滑好了,缺陷…

模具要不要建设3D打印中心

随着3D打印技术的日益成熟与广泛应用,模具企业迎来了自建3D打印中心的热潮。这一举措不仅为企业带来了前所未有的发展机遇,同时也伴随着一系列需要克服的挑战,如何看待企业引进增材制造,小编为您全面分析。 机遇篇: 加…

Codeforces Round (Div.3) C.Sort (前缀和的应用)

原题: time limit per test:5 seconds memory limit per test:256 megabytes You are given two strings a and b of length n. Then, you are (forced against your will) to answer q queries. For each query, you are given a range …

FPGA开发:Verilog数字设计基础

EDA技术 EDA指Electronic Design Automation,翻译为:电子设计自动化,最早发源于美国的影像技术,主要应用于集成电路设计、FPGA应用、IC设计制造、PCB设计上面。 而EDA技术就是指以计算机为工具,设计者在EDA软件平台上…

240907-Gradio渲染装饰器Render-Decorator

A. 最终效果 B. 示例代码 import gradio as gr import gradio as grwith gr.Blocks() as demo:input_text gr.Textbox()gr.render(inputsinput_text)def show_split(text):if len(text) 0:gr.Markdown("## No Input Provided")else:# for letter in text:for lett…

代码随想录训练营day37|52. 携带研究材料,518.零钱兑换II,377. 组合总和 Ⅳ,70. 爬楼梯

52. 携带研究材料 这是一个完全背包问题&#xff0c;就是每个物品可以无限放。 在一维滚动数组的时候规定了遍历顺序是要从后往前的&#xff0c;就是因为不能多次放物体。 所以这里能多次放物体只需要把遍历顺序改改就好了 # include<iostream> # include<vector>…

2024/9/6黑马头条跟学笔记(四)

D4内容介绍 阿里三方安全审核 分布式主键 异步调用 feign 熔断降级 1.自媒体文章自动审核 1.1审核流程 查文章——调接口文本审核——minio下载图片图片审核——审核通过保存文章——发布 草稿1&#xff0c;失败2&#xff0c;人工3&#xff0c;发布9 1.2接口获取 注册阿…

How can I load the openai api configuration through js in html?

题意&#xff1a;怎样在HTML中通过JavaScript加载OpenAI API配置 问题背景&#xff1a; I am trying to send a request through js in my html so that openai analyzes it and sends a response, but if in the js I put the following: 我正在尝试通过HTML中的JavaScript发…