Linux网络编程IO管理
网络 IO 涉及到两个系统对象,一个是用户空间调用 IO 的进程或者线程,一个是内核空间的内核系统,比如发生 IO 操作 read 时,它会经历两个阶段:
- 等待内核协议栈的数据准备就绪;
- 将内核中的数据拷贝到用户态的进程或线程中。
由于在以上两个阶段产出的不同情况,就出现了多种网络 IO 管理方法,即网络 IO 模型。
五种网络 IO 模型
阻塞 IO(blocking IO)
在 Linux 中,默认情况下所有 socket 都是 blocking,一个典型的读操作流程如下:

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

//接受缓冲区大小
#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() 创建新进程。
我们假设对上述服务器/客户机模型提出更高的要求,即让服务器同时为多个客户机提供服务,就有了以下模型。

#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 执行读操作时,流程如下:

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 有数据到达,就通知用户进程。流程如下:

当用户进程调用了 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)

当用户进程发起 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 处理。
推荐课程:https://xxetb.xetslk.com/s/3oyV5o
相关文章:
Linux网络编程IO管理
网络 IO 涉及到两个系统对象,一个是用户空间调用 IO 的进程或者线程,一个是内核空间的内核系统,比如发生 IO 操作 read 时,它会经历两个阶段: 等待内核协议栈的数据准备就绪;将内核中的数据拷贝到用户态的…...
SpringCloud集成ELK
1、添加依赖 <dependency><groupId>net.logstash.logback</groupId><artifactId>logstash-logback-encoder</artifactId><version>6.1</version> </dependency>2、在logback-spring.xml中添加配置信息(logback-sp…...
【卷起来】VUE3.0教程-06-组件详解
各位看官,点波关注和赞吧 组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构: 这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的…...
JS Web
Web API 元素通用属性 元素自身属性 事件处理...
【Linux】传输层协议——UDP
零、传输层的作用是负责数据能够从发送端传输到接收端 一、再来认识一下端口号 端口号(Port)标识了一个主机进行通信的不同的应用程序。在TCP/IP协议中,用“源IP”,“源端口号”,“目的IP”,“目的端口号”…...
算法学习攻略总结 : 入门至进阶,通关之路指南
❃博主首页 : <码到三十五> ☠博主专栏 : <mysql高手> <elasticsearch高手> <源码解读> <java核心> <面试攻关> ♝博主的话 : <搬的每块砖,皆为峰峦之基;公众号搜索(码到…...
《卷积神经网络 CNN 原理探秘》
CNN基本原理详解 卷积神经网络(Convolutional Neural Network,简称CNN),是一种前馈神经网络,人工神经元可以响应周围单元,可以进行大型图像处理。卷积神经网络包括卷积层和池化层。 卷积神经网络是受…...
C#获取计算机信息
目录 效果 项目 代码 下载 效果 项目 代码 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Management; n…...
派遣函数 - 通过设备链接打开设备
利用文件IO相关的Wn32API对设备进行“打开”和“关闭”操作。要打开设备,必须通过设备的名字才能得到该设备的柄。前面介绍过,每个设备都有设备名称,如HelloDDK驱动程序的设备名为“Device\\MyDDKDevice”,但是设备名无法被用户模…...
Vue 2 中的 `$set` 方法详解
在 Vue 2 中,响应式数据的更新非常重要,因为它确保了当数据改变时,视图能够自动更新。Vue 使用一套高效的机制来追踪依赖并在数据变化时更新视图。然而,在某些情况下,直接修改对象的属性可能不会触发视图更新。这时&am…...
掌握Hive函数[2]:从基础到高级应用
目录 高级聚合函数 多进一出 1. 普通聚合 count/sum... 2. collect_list 收集并形成list集合,结果不去重 3. collect_set 收集并形成set集合,结果去重 案例演示 1. 每个月的入职人数以及姓名 炸裂函数 概述 案例演示 1. 数据准备 1)表…...
水壶问题记录
https://leetcode.cn/problems/water-and-jug-problem/description/?envTypestudy-plan-v2&envId2024-spring-sprint-100...
spring综合性利用工具-SpringBootVul-GUI(五)
项目地址 https://github.com/wh1t3zer/SpringBootVul-GUI 0x01简介 本着简单到极致的原则,开发了这么一款半自动化工具(PS:这个工具所包含了20个漏洞,开发不易,有任何问题可提issue) 尽管是一个为懒人量…...
2024年9月12日(k8s环境及测试 常用命令)
一、环境准备及测试 1、报错处理: kube-system calico-node-5wvln 0/1 Init:0/3 0 16h kube-system calico-node-d7xfb 0/1 Init:0/3 0 16h ku…...
卫生间漏水原因很多,切莫病急乱投医
有位业主说他家卫生间背面的墙湿了,邻居家正好在装修,把家具拆掉以后发现墙面上有一片已经湿了。 和业主相约去现场看看,去楼下业主家看了看,顶面是干燥的,这就说明不往楼下漏水。 这就奇怪了&#…...
IEEE 802.11a OFDM系统的仿真(续)
(内容源自详解MATLAB/SIMULINK 通信系统建模与仿真 刘学勇编著第九章内容,有兴趣的读者请阅读原书) clear all %%%%%%%参数设计部分%%%%%%%Nsp52;%系统子载波数(不包括直流载波) Nfft64;%FFT长度 Ncp16;…...
Linux cut命令详解使用:掌握高效文本切割
cut 是 Linux 中一个用于从文本文件或标准输入中提取指定字段的命令。它根据分隔符或者字符位置来裁剪文本,是处理文本文件中的字段、列和子字符串的常用工具。 基本语法 cut [选项] 文件或 命令 | cut [选项]常用选项 -b:按字节位置切割(…...
c++11新特性——endable_shared_from_this
文章目录 一.解决场景代码示例原因 二.解决办法代码 三.底层原理 一.解决场景 一个share_ptr管理的类,如果从类的函数里返回类对象(this指针),导致share_ptr引用计数错误,析构时异常问题 代码示例 #include <mem…...
小程序的右侧抽屉开关动画手写效果
<template><view><button click"openDrawer">打开抽屉</button><view v-if"showDrawer" class"drawer" :style"{ backgroundColor: bgColor }" click"closeDrawer"><view class"draw…...
vue3中el-table中点击图片放大时,被表格覆盖
问题:vue3中el-table中点击图片放大时,被表格覆盖。 解决方法:el-image 添加preview-teleported <el-table-column label"封面图" prop"coverUrl"><template #default"scope"><el-imagestyle&q…...
Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...
工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...
定时器任务——若依源码分析
分析util包下面的工具类schedule utils: ScheduleUtils 是若依中用于与 Quartz 框架交互的工具类,封装了定时任务的 创建、更新、暂停、删除等核心逻辑。 createScheduleJob createScheduleJob 用于将任务注册到 Quartz,先构建任务的 JobD…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
基于Java Swing的电子通讯录设计与实现:附系统托盘功能代码详解
JAVASQL电子通讯录带系统托盘 一、系统概述 本电子通讯录系统采用Java Swing开发桌面应用,结合SQLite数据库实现联系人管理功能,并集成系统托盘功能提升用户体验。系统支持联系人的增删改查、分组管理、搜索过滤等功能,同时可以最小化到系统…...
视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
Linux 内存管理实战精讲:核心原理与面试常考点全解析
Linux 内存管理实战精讲:核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用,还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
手机平板能效生态设计指令EU 2023/1670标准解读
手机平板能效生态设计指令EU 2023/1670标准解读 以下是针对欧盟《手机和平板电脑生态设计法规》(EU) 2023/1670 的核心解读,综合法规核心要求、最新修正及企业合规要点: 一、法规背景与目标 生效与强制时间 发布于2023年8月31日(OJ公报&…...
针对药品仓库的效期管理问题,如何利用WMS系统“破局”
案例: 某医药分销企业,主要经营各类药品的批发与零售。由于药品的特殊性,效期管理至关重要,但该企业一直面临效期问题的困扰。在未使用WMS系统之前,其药品入库、存储、出库等环节的效期管理主要依赖人工记录与检查。库…...
