深入理解 Linux 阻塞IO与Socket数据结构

news2025/5/13 20:41:42

一、阻塞IO的直观演示

示例代码:最简单的阻塞接收程序

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main() {
    // 创建TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 绑定地址端口
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    
    // 开始监听
    listen(sockfd, 5);
    printf("等待客户端连接...\n");
    
    // 阻塞点1:接受连接
    struct sockaddr_in client_addr;
    socklen_t len = sizeof(client_addr);
    int clientfd = accept(sockfd, (struct sockaddr*)&client_addr, &len);
    
    printf("客户端已连接!\n");
    
    // 阻塞点2:接收数据
    char buf[1024];
    int ret = recv(clientfd, buf, sizeof(buf), 0);
    printf("收到数据:%s\n", buf);
    
    close(clientfd);
    close(sockfd);
    return 0;
}

以下是等效的 Java 版本实现,保留了阻塞 IO 的特性并添加了详细注释:

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class BlockingIOServer {
    public static void main(String[] args) {
        // 创建TCP套接字并绑定端口(对应C的socket+bind+listen)
        try (ServerSocket serverSocket = new ServerSocket(8080)) {
            System.out.println("等待客户端连接...");
            
            // 阻塞点1:接受客户端连接(对应C的accept)
            Socket clientSocket = serverSocket.accept();
            System.out.println("客户端已连接!");

            // 阻塞点2:接收数据(对应C的recv)
            InputStream inputStream = clientSocket.getInputStream();
            byte[] buffer = new byte[1024];
            
            // read()方法会阻塞直到有数据到达
            int bytesRead = inputStream.read(buffer); 
            System.out.println("收到数据:" + new String(buffer, 0, bytesRead));

            // 自动关闭资源(Java 7+ try-with-resources)
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

其实本质上java的代码和C语言代码是一样的,都是调用对应的系统函数。

阻塞行为分析
  1. accept() 调用阻塞:直到有客户端连接才会继续执行
  2. recv() 调用阻塞:当连接建立后,如果客户端不发送数据,进程会一直挂起

二、Socket全生命周期

2.1 创建阶段:从用户态到内核态的旅程

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
流程详解(附内核调用链):
用户空间调用 socket()
↓
系统调用 sys_socket() → 陷入内核
↓
sock_create() → 创建通用socket对象
↓
查找协议族 net_families[AF_INET] → 获取IPv4协议族操作表
↓
调用 inet_create() → 创建TCP/UDP专用socket
│   ↓
分配 struct sock 结构 → 初始化连接状态为 TCP_CLOSE
↓
注册协议操作函数 → 设置 tcp_prot(TCP协议处理引擎)
↓
关联文件描述符 → 通过 file_operations 绑定读写方法

关键步骤深度解析

  1. 协议族选择

    // net/socket.c
    static int __init sock_init(void) {
        // 初始化时注册协议族
        rc = netlink_kernel_create(&init_net, NETLINK_ROUTE, ...);
        rc = inet_add_protocol(&icmp_protocol, IPPROTO_ICMP);
        ...
    }
    
    • net_families[AF_INET] 指向 ipprot 结构,包含IPv4协议处理函数
  2. 传输层协议绑定

    // net/ipv4/af_inet.c
    int inet_create(struct net *net, struct socket *sock, int protocol) {
        // 根据协议类型选择处理引擎
        if (protocol == IPPROTO_TCP)
            sock->ops = &inet_stream_ops;  // TCP操作集
        else if (protocol == IPPROTO_UDP)
            sock->ops = &inet_dgram_ops;   // UDP操作集
        
        // 分配TCP专用数据结构
        sk = sk_alloc(net, PF_INET, GFP_KERNEL, &tcp_prot, 0);
    }
    
  3. 资源预分配

    • 预分配接收队列缓存:sk->sk_receive_queue(基于内存池的sk_buff分配)
    • 初始化等待队列:sk->sk_sleep(后续阻塞操作的基础设施)

2.2 连接建立阶段:TCP三次握手的微观视角

connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
状态转换全景图:
TCP_CLOSE → TCP_SYN_SENT → TCP_ESTABLISHED
       ↑           ↑            ↑
      SYN        SYN-ACK       ACK
内核处理流水线:
用户调用 connect()
↓
tcp_connect() → 设置状态为 TCP_SYN_SENT
↓
生成SYN报文 → 调用 ip_queue_xmit() 发送
↓
进入状态机等待 → sk->sk_state = TCP_SYN_SENT
↓
等待ACK到达 → 触发 sk_state_change 回调

关键机制详解

  1. SYN报文发送

    // net/ipv4/tcp_output.c
    int tcp_connect(struct sock *sk) {
        // 构建SYN报文
        struct sk_buff *skb = alloc_skb(sizeof(struct tcphdr) + ...);
        tcp_init_nondata_skb(skb, tcp_current_seq(sk), TCPHDR_SYN);
        
        // 发送队列管理
        skb_queue_tail(&sk->sk_write_queue, skb);
        ip_queue_xmit(sk, NULL, skb);
    }
    
  2. 等待队列机制

    // 当前进程进入等待状态
    set_current_state(TASK_INTERRUPTIBLE);
    add_wait_queue_exclusive(&sk->sk_sleep, &wait);
    
    while (sk->sk_state != TCP_ESTABLISHED) {
        if (signal_pending(current))
            return -EINTR;
        schedule();
    }
    
  3. 状态机驱动

    // net/ipv4/tcp_states.h
    static const struct tcp_state_trans tcp_established_transitions = {
        .transitions = {
            [TCP_ESTABLISHED] = {
                .event = TCP_EARLY_DATA,
                .next_state = TCP_ESTABLISHED,
                .action = tcp_rcv_established,
            },
            // 处理ACK包的回调注册
        },
    };
    

阻塞行为本质
当调用 connect() 时:

  1. 若端口不可用/连接被拒绝 → 立即返回错误
  2. 若正常发送SYN → 进程进入 TASK_UNINTERRUPTIBLE 等待
  3. 当收到SYN-ACK后 → 内核完成握手 → 触发 sk->sk_data_ready
  4. 最终唤醒进程 → 返回成功

2.3 生命周期全景图

socket()
bind()+listen()
accept()收到SYN
收到ACK
close()
收到ACK
收到FIN
2MSL超时
CLOSED
LISTEN
SYN_RCVD
ESTABLISHED
FIN_WAIT_1
FIN_WAIT_2
TIME_WAIT

各阶段内存管理

  • 创建阶段:预分配接收缓冲区(sk->sk_rmem_alloc
  • 传输阶段:动态调整发送窗口(sk->snd_wnd
  • 关闭阶段:释放关联的skb队列

三、核心数据结构详解

3.1 socket结构体家族树

struct socket
├── struct file (VFS层对象)
└── struct sock (协议无关层)
    ├── struct tcp_sock (TCP协议私有数据)
    └── struct udp_sock (UDP协议私有数据)

请添加图片描述
具体解释见下面

3.2 关键数据结构详解

3.2.1 struct socket:用户态与内核态的桥梁
struct socket {
    const struct proto_ops *ops;  // 协议操作函数表(TCP/UDP/SCTP等)
    struct sock *sk;              // 核心协议栈对象(传输层控制块)
    struct file *file;            // 关联的文件描述符(VFS接口)
};

核心功能解析:

  • ops指向协议族操作表(如inet_stream_ops),代码如下:
const struct proto_ops inet_stream_ops = {
    .family    = PF_INET,
    .recvmsg   = inet_recvmsg,   // 接收消息入口
    .sendmsg   = inet_sendmsg,    // 发送消息入口
    .accept    = inet_accept,     // 接受新连接
    .bind      = inet_bind,       // 绑定端口
    ...
};

通过函数指针实现协议无关接口,支持多协议扩展,类比Java接口实现:就像Java中DataSource接口可以有不同的实现类(MySQL/Oracle),C语言通过proto_ops函数指针数组实现协议多态。当用户调用read()系统调用时,最终会通过socket->ops->recvmsg调用具体协议的接收函数。例如udp协议的接收消息,这里的recvmsg会指向udp的接收实现,如果是tcp协议,recvmsg那么就指向tcp的接收实现(经过三次握手后,可以接收消息)。

  • *sk,核心协议栈对象,下面会有解释,此处略
  • *file,关联的文件描述符,file指针将socket映射到文件系统,实现read()/write()等文件操作语义
3.2.2 struct sock
struct sock {
   const struct proto *sk_prot;     // 协议处理函数(如 tcp_prot)
    struct sk_buff_head sk_receive_queue; // 接收队列
    wait_queue_head_t    sk_sleep;    // 进程等待队列头 sk_wq
   void (*sk_data_ready)(struct sock *sk);  // 数据就绪通知回调
    // ...其他字段
};
  • 协议处理引擎 sk_prot
// TCP协议处理结构体
struct proto tcp_prot = {
    .name                  = "TCP",
    .err_handler           = tcp_err,          // 错误处理
    .recvmsg               = tcp_recvmsg,      // 数据接收(含流量控制)
    .sendmsg               = tcp_sendmsg,      // 数据发送(含拥塞控制)
    //...其它字段
};

工作流程示例:当应用层调用send()时,数据会经过以下路径:

用户空间缓冲区 → socket->ops->sendmsg → tcp_sendmsg → 协议栈处理 → 驱动程序发送
  • 接收队列 sk_receive_queue,即网络数据包
  • 等待队列 sk_sleep阻塞行为本质:当接收队列为空时,进程会被挂入sk_sleep队列,直到有数据到达触发唤醒。
  • sk_data_ready 负责在数据就绪时触发进程唤醒,是阻塞IO模型的核心机制。具体看下文。

四、阻塞IO唤醒机制

4.1 sk_data_ready 的本质

定义
sk_data_readystruct sock 中定义的一个函数指针,其类型为 void (*sk_data_ready)(struct sock *sk)。它是 协议栈向应用层传递数据就绪通知的核心机制,所有传输层协议(TCP/UDP)都需要实现此回调。

代码定位

// net/core/sock.h
struct sock {
    ...
    void (*sk_data_ready)(struct sock *sk);  // 数据就绪通知回调
    ...
};

4.2 工作流程:从网卡到应用层

以下是 sk_data_ready 被触发的完整链路(以TCP为例):

graph TD
    A[网卡接收数据包] --> B[硬件中断]
    B --> C[软中断(napi_schedule)]
    C --> D[napi_poll处理]
    D --> E[ip_rcv() → tcp_v4_rcv()]
    E --> F[协议处理(tcp_rcv)]
    F --> G[调用sk_data_ready(sk)]
    G --> H[唤醒等待队列(sk_sleep)]
    H --> I[应用层recv()返回数据]

关键步骤解析

  1. 中断阶段:网卡收到数据包触发硬件中断,注册的中断处理函数标记 NAPI 结构体。
  2. 软中断阶段:内核的 ksoftirqd 线程执行 napi_poll(),开始处理接收队列。
  3. 协议栈处理:数据包经过IP层、TCP层解析,最终进入 tcp_v4_rcv()
  4. 触发回调:在协议处理完成后,内核调用 sk->sk_data_ready(sk)
  5. 唤醒进程sk_data_ready 的默认实现会调用 sk_wake_async(),最终通过 wake_up_interruptible(&sk->sk_sleep) 唤醒等待队列中的进程。

4.3、sk_data_ready 与阻塞IO的阻塞/唤醒机制

4.3 1. 阻塞IO的核心逻辑

当应用层调用阻塞型 recv() 时:

// 系统调用入口 sys_recvfrom
if (skb_queue_empty(&sk->sk_receive_queue)) {
    // 数据未就绪,进程进入睡眠
    set_current_state(TASK_INTERRUPTIBLE);
    add_wait_queue(&sk->sk_sleep, &wait);
    schedule();  // 主动让出CPU
    remove_wait_queue(...);
}
4.3.2. sk_data_ready 的触发时机
  • 数据就绪时:当 sk_data_ready 被调用时,会触发以下动作:
    // 默认实现(net/core/sock.c)
    static inline void sock_def_readable(struct sock *sk, int len) {
        if (!sock_flag(sk, SOCK_DEAD)) {
            // 唤醒所有在sk_sleep队列中等待的进程
            wake_up_interruptible(&sk->sk_sleep);
            // 触发异步通知(如信号)
            sk_wake_async(sk, SOCK_WAKE_IO, POLL_IN);
        }
    }
    
4.3.3. 阻塞IO的唤醒本质
  • 等待队列(Wait Queue):进程在调用阻塞型 recv() 时会被加入 sk->sk_sleep 队列,并设置为不可中断状态(TASK_INTERRUPTIBLE)。
  • 唤醒条件:只有当 sk_data_ready 被调用时,才会触发队列唤醒。这意味着:
    • 数据必须通过协议栈处理完成(如TCP三次握手完成、数据包校验通过)。
    • 内核协议栈确认数据已准备好被用户空间读取。

在这里插入图片描述

请添加图片描述

总结

通过本文的学习,你应该已经掌握:

  1. 阻塞IO的底层行为模式
  2. Socket从创建到数据传输的完整生命周期
  3. 核心数据结构(socket/sock/sk_buff)的协作关系
  4. 系统调用到内核处理的完整链路

参考文档

  • 深入理解Linux网络: 修炼底层内功,掌握高性能原理 (张彦飞)

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

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

相关文章

如何修改进程优先级?

文章目录 1. 摘要2. 命令实现2.1 使用 renice&#xff08;调整普通进程的优先级&#xff09;​2.2 使用 chrt&#xff08;调整实时进程的优先级&#xff09; 3. 代码实现 1. 摘要 在实际开发中&#xff0c;我们经常会遇到创建进程的场景&#xff0c;但是往往并不关心它的优先级…

stm32week15

stm32学习 十一.中断 2.NVIC Nested vectored interrupt controller&#xff0c;嵌套向量中断控制器&#xff0c;属于内核(M3/4/7) 中断向量表&#xff1a;定义一块固定的内存&#xff0c;以4字节对齐&#xff0c;存放各个中断服务函数程序的首地址&#xff0c;中断向量表定…

QSFP+、QSFP28、QSFP-DD接口分别实现40G、100G、200G/400G以太网接口

常用的光模块结构形式&#xff1a; 1&#xff09;QSFP等效于4个SFP&#xff0c;支持410Gbit/s通道传输&#xff0c;可通过4个通道实现40Gbps传输速率。与SFP相比&#xff0c;QSFP光模块的传输速率可达SFP光模块的四倍&#xff0c;在部署40G网络时可直接使用QSFP光模块&#xf…

PXE安装Ubuntu系统

文章目录 1. 服务器挂载Ubuntu镜像2. 修改dhcp配置文件3. 修改tftp配置文件4.复制网络驱动文件和其他配置文件5. http目录下配置文件6. 踩坑记录6.1 Failed to load ldlinux.c326.2 no space left on device6.3 为啥用pxe安装系统时&#xff0c;客户端需要较大的内存&#xff1…

uniapp tabBar 中设置“custom“: true 在H5和app中无效解决办法

uniapp小程序自定义底部tabbar&#xff0c;但是在转成H5和app时发现"custom": true 无效&#xff0c;原生tabbar会显示出来 解决办法如下 在tabbar的list中设置 “visible”:false 代码如下&#xff1a;"tabBar": {"custom": true,//"cust…

ABP-Book Store Application中文讲解 - 前期准备 - Part 2:创建Acme.BookStore + Angular

ABP-Book Store Application中文讲解-汇总-CSDN博客 因为本系列文章使用的.NET8 SDK&#xff0c;此处仅介绍如何使用abp cli .NET 8 SDK SQL sevrer 2014创建Angular模板的Acme.BookStore。 目录 1. ABP cli创建项目 1.1 打开cmd.exe 1.2 创建项目 2. ABP Studio创建项…

基于k8s的Jenkins CI/CD平台部署实践(三):集成ArgoCD实现持续部署

基于k8s的Jenkins CI/CD平台部署实践&#xff08;三&#xff09;&#xff1a;集成ArgoCD实现持续部署 文章目录 基于k8s的Jenkins CI/CD平台部署实践&#xff08;三&#xff09;&#xff1a;集成ArgoCD实现持续部署一、Argocd简介二、安装Helm三、Helm安装ArgoCD实战1. 添加Arg…

控制台打印带格式内容

1. 场景 很多软件会在控制台打印带颜色和格式的文字&#xff0c;需要使用转义符实现这个功能。 2. 详细说明 2.1.转义符说明 样式开始&#xff1a;\033[参数1;参数2;参数3m 可以多个参数叠加&#xff0c;若同一类型的参数&#xff08;如字体颜色&#xff09;设置了多个&…

外网访问内网海康威视监控视频的方案:WebRTC + Coturn 搭建

外网访问内网海康威视监控视频的方案&#xff1a;WebRTC Coturn 需求背景 在仓库中有海康威视的监控摄像头&#xff0c;内网中是可以直接访问到监控摄像的画面&#xff0c;由于项目的需求&#xff0c;需要在外网中也能看到监控画面。 实现这个功能的意义在于远程操控设备的…

Linux系统下的延迟任务及定时任务

1、延迟任务 概念&#xff1a; 在系统中我们的维护工作大多数时在服务器行对闲置时进行 我们需要用延迟任务来解决自动进行的一次性的维护 延迟任务时一次性的&#xff0c;不会重复执行 当延迟任务产生输出后&#xff0c;这些输出会以邮件的形式发送给延迟任务发起者 在 RH…

【网络原理】数据链路层

目录 一. 以太网 二. 以太网数据帧 三. MAC地址 四. MTU 五. ARP协议 六. DNS 一. 以太网 以太网是一种基于有线或无线介质的计算机网络技术&#xff0c;定义了物理层和数据链路层的协议&#xff0c;用于在局域网中传输数据帧。 二. 以太网数据帧 1&#xff09;目标地址 …

相或为K(位运算)蓝桥杯(JAVA)

这个题是相或为k&#xff0c;考察相或的性质&#xff0c;用俩个数举例子&#xff0c;011001和011101后面的数不管和哪个数相或都不可能变成前面的数&#xff0c;所以利用这个性质我们可以用相与运算来把和k对应位置的1都积累起来&#xff0c;看最后能不能拼起来k如果能拼起来k那…

AI汽车时代的全面赋能者:德赛西威全栈能力再升级

AI汽车未来智慧出行场景正在描绘出巨大的商业图景&#xff0c;德赛西威已经抢先入局。 在2025年上海车展开幕前夕&#xff0c;德赛西威发布2030年全新使命愿景——“创领安全、愉悦和绿色的出行生活”&#xff0c;并推出全栈式智慧出行解决方案Smart Solution3.0、车路云一体式…

学习Python的第四天之网络爬虫

30岁程序员学习Python的第四天之网络爬虫的Scrapy库 Scrapy库的基本信息 Scrapy库的安装 在windows系统中通过管理员权限打开cmd。运行pip install scrapy即可安装。 通过命令scrapy -h可查看scrapy库是否安装成功. Scrapy库的基础信息 scrapy库是一种爬虫框架库 爬虫框…

5、开放式PLC梯形图编程组件 - /自动化与控制组件/open-plc-programming

76个工业组件库示例汇总 开放式PLC编程环境 这是一个开放式PLC编程环境的自定义组件&#xff0c;提供了一个面向智能仓储堆垛机控制的开放式PLC编程环境。该组件采用苹果科技风格设计&#xff0c;支持多厂商PLC硬件&#xff0c;具有直观的界面和丰富的功能。 功能特点 多语…

linux中常用的命令(三)

目录 1- ls(查看当前目录下的内容) 2- pwd (查看当前所在的文件夹) 3- cd [目录名]&#xff08;切换文件夹&#xff09; 4- touch [文件名] &#xff08;如果文件不存在&#xff0c;新建文件&#xff09; 5- mkdir[目录名] &#xff08;创建目录&#xff09; 6-rm[文件名]&…

Java 中 AQS 的实现原理

AQS 简介 AQS(全称AbstractQueuedSynchronizer)即抽象同步队列&#xff0c;它是实现同步器的基础组件&#xff0c;并发包中锁的底层就是使用AQS实现的。 由类图可以看到&#xff0c;AQS是一个FIFO的双向队列&#xff0c;其内部通过节点head和tail记录队首和队尾元素&#xff0…

『Python学习笔记』ubuntu解决matplotlit中文乱码的问题!

ubuntu解决matplotlit中文乱码的问题&#xff01; 文章目录 simhei.ttf字体下载链接&#xff1a;http://xiazaiziti.com/210356.html将字体放到合适的地方 sudo cp SimHei.ttf /usr/share/fonts/(base) zkfzkf:~$ fc-list | grep -i "SimHei" /usr/local/share/font…

鸿蒙知识总结

判断题 1、 在http模块中&#xff0c;多个请求可以使用同一个httpRequest对象&#xff0c;httpRequest对象可以复用。&#xff08;错误&#xff09; 2、订阅dataReceiverProgress响应事件是用来接收HTTP流式响应数据。&#xff08;错误&#xff09; 3、ArkTS中变量声明时不需要…

C++23 新特性:深入解析 std::views::join_with(P2441R2)

文章目录 std::views::join_with 基本用法处理字符串集合std::views::join_with 与其他视图的结合使用总结 随着C23标准的逐步推进&#xff0c;我们迎来了许多令人兴奋的新特性&#xff0c;其中之一就是 std::views::join_with。这个新特性是C23中引入的视图适配器&#xff0c…