Linux 之网络通信

Linux 之网络通信

1. 宏观理解

Linux 网络通信的核心是内核网络协议栈 + 用户态套接字(Socket)接口,是实现跨进程、跨主机、跨网络数据交互的基础能力,遵循"一切皆文件"的设计理念:

  • 向下(内核态协议栈):Linux 内核实现了完整的 TCP/IP 协议栈,负责数据包的封装/解封装、路由转发、拥塞控制、网卡硬件交互等核心逻辑,是网络通信的底层支撑;
  • 向上(用户态接口):将网络通信抽象为套接字描述符(fd),为用户空间提供统一的文件类操作接口(socket/connect/bind/listen/recv/send/close),屏蔽底层协议(TCP/UDP)、网卡硬件、网络拓扑的差异;
  • 中间(核心机制):处理网络 IO 的阻塞/非阻塞、并发复用(select/poll/epoll)、数据包的内核态-用户态拷贝、协议栈的分层处理等核心逻辑。

2. 网络通信基础组成

2.1 网络通信分层架构

Linux 网络栈遵循 TCP/IP 分层模型,从用户态到硬件层职责清晰,层间通过固定接口交互:

层级(TCP/IP) 对应 Linux 实现 核心职责 关键接口/数据结构
应用层 用户态进程 业务逻辑实现(如 HTTP/SSH) 套接字 API(socket/send/recv)
传输层 内核 TCP/UDP 模块 端到端通信(可靠传输/无连接传输) struct sock、sk_buff、TCP 控制块
网络层 内核 IP 模块 跨网络路由、IP 地址封装 路由表、IP 头、icmp 协议处理
链路层 内核链路层模块 + 网卡驱动 局域网帧传输、MAC 地址封装 以太网帧、net_device、sk_buff
物理层 网卡硬件 二进制数据的电/光信号传输 网卡寄存器、DMA 通道

同时,Linux 网络栈在用户态与内核态的分层如下:

层级 运行空间 核心作用
用户态网络层 用户态 调用套接字 API 实现业务通信,无需关注协议栈细节
内核套接字层 内核态 管理套接字生命周期,衔接用户态 API 与内核协议栈
内核协议栈层 内核态 实现 TCP/UDP/IP 等协议的核心逻辑
设备驱动层 内核态 管理网卡硬件,处理数据包的物理收发

2.2 核心标识体系

Linux 网络通信通过"地址 + 端口 + 协议"唯一标识一个网络连接,是跨主机通信的基础:

(1)网络地址标识
  • IP 地址 :标识网络中的主机,Linux 支持 IPv4(32 位)和 IPv6(128 位),核心结构体:

    c 复制代码
    // IPv4 地址结构体
    struct in_addr {
        uint32_t s_addr; // 网络字节序(大端)的 IP 地址
    };
    // 通用套接字地址结构体(兼容 IPv4/IPv6)
    struct sockaddr {
        sa_family_t sa_family; // 地址族(AF_INET/AF_INET6)
        char        sa_data[14]; // 地址数据(IP+端口)
    };
    // IPv4 专用套接字地址结构体(常用)
    struct sockaddr_in {
        sa_family_t    sin_family; // AF_INET
        in_port_t      sin_port;   // 端口号(网络字节序)
        struct in_addr sin_addr;   // IP 地址
        char           sin_zero[8]; // 填充字段
    };
  • MAC 地址 :标识局域网内的网卡设备,6 字节(48 位),存储在 net_device 结构体的 dev_addr 字段,用于链路层帧传输。

(2)端口号
  • 16 位整数(0-65535),标识主机内的特定进程,核心分类:
    • 知名端口(0-1023):系统保留(如 80=HTTP、22=SSH、443=HTTPS);
    • 动态端口(1024-65535):用户进程可随机使用。
  • 端口号与 IP 地址组合(IP:端口)唯一标识网络中的一个"通信端点"。
(3)套接字描述符(socket fd)
  • 用户态通过文件描述符 操作套接字(与文件 fd 统一管理),由 socket() 函数创建,核心特性:
    • 遵循文件描述符的通用操作(如 close(fd) 关闭套接字);
    • 可通过 fcntl() 设置非阻塞、O_NONBLOCK 等属性;
    • 支持 IO 多路复用(select/poll/epoll)监听可读/可写事件。
(4)协议族与协议类型
协议族(AF_*) 协议类型(SOCK_*) 对应传输层协议 核心特点
AF_INET(IPv4) SOCK_STREAM TCP 面向连接、可靠、字节流、拥塞控制
AF_INET(IPv4) SOCK_DGRAM UDP 无连接、不可靠、数据报、速度快
AF_INET(IPv4) SOCK_RAW 原始 IP 包 绕过传输层,直接操作 IP/ICMP 包(需 root 权限)

2.3 核心数据结构

(1)用户态核心结构体
  • struct sockaddr_in:IPv4 套接字地址(见上文),用于绑定(bind)、连接(connect)等操作;

  • fd_set:IO 多路复用(select)的文件描述符集合,核心宏:

    c 复制代码
    FD_ZERO(fd_set *set); // 清空集合
    FD_SET(int fd, fd_set *set); // 将 fd 加入集合
    FD_ISSET(int fd, fd_set *set); // 判断 fd 是否在集合中
    FD_CLR(int fd, fd_set *set); // 从集合移除 fd
  • struct epoll_event:epoll 事件结构体(IO 多路复用):

    c 复制代码
    struct epoll_event {
        uint32_t events; // 监听的事件(EPOLLIN/EPOLLOUT/EPOLLERR)
        epoll_data_t data; // 关联数据(fd/指针)
    };
    typedef union epoll_data {
        void *ptr;
        int fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
(2)内核态核心结构体
  • struct sk_buff(套接字缓冲区):内核网络栈的核心数据结构,封装网络数据包,贯穿整个协议栈:

    c 复制代码
    struct sk_buff {
        unsigned char *data; // 数据包有效数据起始地址
        unsigned char *head; // 缓冲区起始地址
        unsigned char *tail; // 数据包有效数据结束地址
        unsigned char *end;  // 缓冲区结束地址
        struct net_device *dev; // 关联的网络设备(网卡)
        struct sk_buff *next; // 链表指针(用于数据包队列)
        __be32 saddr; // 源 IP 地址(网络字节序)
        __be32 daddr; // 目的 IP 地址(网络字节序)
        __be16 sport; // 源端口号
        __be16 dport; // 目的端口号
        // 协议相关字段(TCP/UDP/IP 头指针)
    };

    核心作用:内核通过 sk_buff 管理数据包的分配、拷贝、分片、重组,避免频繁内存申请/释放。

  • struct net_device:内核表示网络设备(网卡)的核心结构体:

    c 复制代码
    struct net_device {
        char name[IFNAMSIZ]; // 设备名(如 eth0、lo)
        unsigned char dev_addr[ETH_ALEN]; // MAC 地址
        unsigned int mtu; // 最大传输单元(默认 1500 字节)
        struct net_device_ops *netdev_ops; // 设备操作接口
        struct sk_buff_head rx_queue; // 接收数据包队列
        // 状态字段(UP/DOWN/运行状态)
    };

3. 网络通信核心机制

3.1 套接字生命周期(TCP)

TCP 套接字(SOCK_STREAM)是最常用的网络通信方式,完整生命周期分为服务端和客户端两个维度:

(1)服务端

核心特点:

  • listen() 后套接字进入"监听态",内核维护半连接队列(SYN 队列)和全连接队列(ACCEPT 队列);
  • accept() 从全连接队列取出已完成三次握手的连接,返回新的套接字 fd(与客户端通信),原监听 fd 继续监听。
(2)客户端

核心特点:

  • connect() 触发 TCP 三次握手,成功返回后连接建立;
  • 客户端无需显式 bind(),内核会自动分配随机端口和本地 IP。
(3)TCP 三次握手与四次挥手
  • 三次握手(建立连接):

    1. 客户端 → 服务端:SYN(同步序列号);
    2. 服务端 → 客户端:SYN+ACK(确认客户端序列号,同步服务端序列号);
    3. 客户端 → 服务端:ACK(确认服务端序列号);
      核心目的:同步序列号,确保双方收发能力正常。
  • 四次挥手(关闭连接):

    1. 主动关闭方 → 被动关闭方:FIN(无数据发送);
    2. 被动关闭方 → 主动关闭方:ACK(确认 FIN);
    3. 被动关闭方 → 主动关闭方:FIN(无数据发送);
    4. 主动关闭方 → 被动关闭方:ACK(确认 FIN);
      核心目的:确保双方数据都已传输完成,避免数据丢失。

3.2 IO 多路复用

单进程/线程同时管理多个套接字 fd 的核心机制,解决"一个 fd 阻塞导致所有 fd 无法处理"的问题,核心方案对比:

方案 核心原理 性能 最大 fd 限制 适用场景
select 轮询 fd_set 集合,拷贝整个集合到内核 低(O(n)) FD_SETSIZE(默认 1024) 低并发(fd < 1024)
poll 轮询 pollfd 数组,无 fd 数量限制 低(O(n)) 无(受系统资源限制) 中低并发
epoll 内核事件表 + 回调通知,仅返回就绪 fd 高(O(1)) 无(受系统资源限制) 高并发(如百万连接)
epoll 核心使用步骤
c 复制代码
// 1. 创建 epoll 实例
int epfd = epoll_create1(0);
if (epfd == -1) { perror("epoll_create1"); exit(1); }

// 2. 定义事件结构体
struct epoll_event ev, events[1024];
ev.events = EPOLLIN; // 监听可读事件
ev.data.fd = listen_fd; // 关联监听 fd

// 3. 将 fd 加入 epoll 实例
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

// 4. 循环监听事件
while (1) {
    // 阻塞等待就绪事件(超时时间 -1 表示永久阻塞)
    int nfds = epoll_wait(epfd, events, 1024, -1);
    for (int i = 0; i < nfds; i++) {
        if (events[i].data.fd == listen_fd) {
            // 监听 fd 就绪:接受新连接
            int conn_fd = accept(listen_fd, NULL, NULL);
            // 将新连接 fd 加入 epoll
            ev.data.fd = conn_fd;
            epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
        } else {
            // 普通 fd 就绪:读取数据
            char buf[1024];
            int n = read(events[i].data.fd, buf, sizeof(buf));
            if (n <= 0) {
                // 连接关闭/出错,移除 fd
                epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                close(events[i].data.fd);
            } else {
                // 处理数据(如回显)
                write(events[i].data.fd, buf, n);
            }
        }
    }
}

3.3 阻塞/非阻塞网络 IO

套接字默认是阻塞模式 ,可通过 fcntl() 设置为非阻塞模式,核心区别:

(1)阻塞 IO
  • 特点:调用 recv()/send()/accept()/connect() 后,若事件未就绪(如无数据可读、无连接可接受),进程进入睡眠态(TASK_INTERRUPTIBLE),直到事件就绪被唤醒;
  • 优点:编程简单,无需轮询;
  • 缺点:单进程只能处理一个 fd,并发差。
(2)非阻塞 IO
  • 特点:调用 IO 函数后,若事件未就绪,立即返回 -EAGAIN(或 -EWOULDBLOCK),不阻塞进程;

  • 设置方式:

    c 复制代码
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 开启非阻塞
  • 优点:单进程可轮询多个 fd,适合高并发;

  • 缺点:需循环轮询,CPU 占用高(通常与 epoll 配合使用)。

3.4 内核态数据包处理流程

用户态发送/接收数据的背后,内核协议栈的核心处理流程:

(1)发送数据(用户态 → 网卡)
  1. 用户态调用 send(),数据从用户缓冲区拷贝到内核 sk_buff
  2. 传输层(TCP/UDP)封装协议头(如 TCP 头:序列号、确认号、端口);
  3. 网络层(IP)封装 IP 头(源/目的 IP、协议类型、TTL);
  4. 链路层封装以太网帧头(源/目的 MAC、帧类型);
  5. 网卡驱动通过 DMA 将 sk_buff 数据写入网卡硬件缓冲区,网卡发送数据。
(2)接收数据(网卡 → 用户态)
  1. 网卡接收数据后,通过 DMA 将数据写入内核 sk_buff,触发中断;
  2. 内核中断处理函数(上半部)标记数据到达,触发下半部处理;
  3. 链路层解封装以太网帧头,校验帧类型;
  4. 网络层解封装 IP 头,查找路由表,转发或本地处理;
  5. 传输层解封装 TCP/UDP 头,将数据投递到对应套接字的接收缓冲区;
  6. 用户态调用 recv(),数据从内核接收缓冲区拷贝到用户缓冲区。

4. 典型网络通信实现

4.1 TCP 服务端-客户端(基础版)

(1)TCP 服务端(阻塞模式)
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 8888
#define BUF_SIZE 1024

int main() {
    // 1. 创建 TCP 套接字
    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置套接字选项(避免端口占用)
    int opt = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

    // 3. 绑定 IP + 端口
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET; // IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
    server_addr.sin_port = htons(PORT); // 端口(网络字节序)

    if (bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }

    // 4. 监听连接(最大等待队列 5)
    if (listen(listen_fd, 5) == -1) {
        perror("listen failed");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }
    printf("TCP server listening on port %d...\n", PORT);

    // 5. 接受客户端连接(阻塞)
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);
    int conn_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_len);
    if (conn_fd == -1) {
        perror("accept failed");
        close(listen_fd);
        exit(EXIT_FAILURE);
    }
    printf("Client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

    // 6. 收发数据
    char buf[BUF_SIZE];
    while (1) {
        // 读取客户端数据(阻塞)
        ssize_t n = read(conn_fd, buf, BUF_SIZE - 1);
        if (n <= 0) {
            if (n == 0) printf("Client disconnected\n");
            else perror("read failed");
            break;
        }
        buf[n] = '\0';
        printf("Received from client: %s\n", buf);

        // 回显数据给客户端
        write(conn_fd, buf, n);
        if (strcmp(buf, "exit") == 0) break;
    }

    // 7. 关闭套接字
    close(conn_fd);
    close(listen_fd);
    return 0;
}
(2)TCP 客户端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 8888
#define BUF_SIZE 1024

int main() {
    // 1. 创建 TCP 套接字
    int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (sock_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 连接服务端
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(SERVER_PORT);

    if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("connect failed");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    printf("Connected to TCP server %s:%d\n", SERVER_IP, SERVER_PORT);

    // 3. 收发数据
    char buf[BUF_SIZE];
    while (1) {
        printf("Enter message (exit to quit): ");
        fgets(buf, BUF_SIZE, stdin);
        // 去掉换行符
        buf[strcspn(buf, "\n")] = '\0';

        // 发送数据到服务端
        write(sock_fd, buf, strlen(buf));
        if (strcmp(buf, "exit") == 0) break;

        // 读取服务端回显
        ssize_t n = read(sock_fd, buf, BUF_SIZE - 1);
        if (n <= 0) {
            perror("read failed");
            break;
        }
        buf[n] = '\0';
        printf("Received from server: %s\n", buf);
    }

    // 4. 关闭套接字
    close(sock_fd);
    return 0;
}
编译&运行
bash 复制代码
# 编译服务端
gcc tcp_server.c -o tcp_server
# 编译客户端
gcc tcp_client.c -o tcp_client

# 运行服务端
./tcp_server
# 新开终端运行客户端
./tcp_client

4.2 UDP 服务端-客户端(无连接)

(1)UDP 服务端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define PORT 9999
#define BUF_SIZE 1024

int main() {
    // 1. 创建 UDP 套接字
    int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 绑定 IP + 端口
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);

    if (bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(sock_fd);
        exit(EXIT_FAILURE);
    }
    printf("UDP server listening on port %d...\n", PORT);

    // 3. 收发数据(无连接,无需 accept)
    char buf[BUF_SIZE];
    struct sockaddr_in client_addr;
    socklen_t client_len = sizeof(client_addr);

    while (1) {
        // 接收客户端数据(阻塞)
        ssize_t n = recvfrom(sock_fd, buf, BUF_SIZE - 1, 0, (struct sockaddr *)&client_addr, &client_len);
        if (n <= 0) {
            perror("recvfrom failed");
            continue;
        }
        buf[n] = '\0';
        printf("Received from %s:%d: %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buf);

        // 回显数据给客户端
        sendto(sock_fd, buf, n, 0, (struct sockaddr *)&client_addr, client_len);
        if (strcmp(buf, "exit") == 0) break;
    }

    // 4. 关闭套接字
    close(sock_fd);
    return 0;
}
(2)UDP 客户端
c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 9999
#define BUF_SIZE 1024

int main() {
    // 1. 创建 UDP 套接字
    int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 定义服务端地址(无需 connect)
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
    server_addr.sin_port = htons(SERVER_PORT);

    // 3. 收发数据
    char buf[BUF_SIZE];
    socklen_t server_len = sizeof(server_addr);

    while (1) {
        printf("Enter message (exit to quit): ");
        fgets(buf, BUF_SIZE, stdin);
        buf[strcspn(buf, "\n")] = '\0';

        // 发送数据到服务端
        sendto(sock_fd, buf, strlen(buf), 0, (struct sockaddr *)&server_addr, server_len);
        if (strcmp(buf, "exit") == 0) break;

        // 接收服务端回显
        ssize_t n = recvfrom(sock_fd, buf, BUF_SIZE - 1, 0, (struct sockaddr *)&server_addr, &server_len);
        if (n <= 0) {
            perror("recvfrom failed");
            break;
        }
        buf[n] = '\0';
        printf("Received from server: %s\n", buf);
    }

    // 4. 关闭套接字
    close(sock_fd);
    return 0;
}

4.3 内核态简单网络数据包捕获(sk_buff 示例)

c 复制代码
#include <linux/module.h>
#include <linux/netfilter.h>
#include <linux/netfilter_ipv4.h>
#include <linux/ip.h>
#include <linux/tcp.h>

// 定义 netfilter 钩子(捕获所有 IPv4 数据包)
static struct nf_hook_ops nf_hook;

// 数据包处理函数
unsigned int hook_func(unsigned int hooknum, struct sk_buff *skb,
                       const struct net_device *in, const struct net_device *out,
                       int (*okfn)(struct sk_buff *)) {
    struct iphdr *ip_header;
    struct tcphdr *tcp_header;

    // 检查 sk_buff 是否有效
    if (!skb) return NF_ACCEPT;

    // 获取 IP 头
    ip_header = ip_hdr(skb);
    if (!ip_header) return NF_ACCEPT;

    // 仅处理 TCP 数据包
    if (ip_header->protocol == IPPROTO_TCP) {
        // 获取 TCP 头
        tcp_header = tcp_hdr(skb);
        if (!tcp_header) return NF_ACCEPT;

        // 打印源/目的 IP 和端口
        printk(KERN_INFO "TCP Packet: %pI4:%d -> %pI4:%d\n",
               &ip_header->saddr, ntohs(tcp_header->source),
               &ip_header->daddr, ntohs(tcp_header->dest));
    }

    // 放行数据包(返回 NF_DROP 则丢弃)
    return NF_ACCEPT;
}

// 模块初始化
static int __init net_capture_init(void) {
    nf_hook.hook = hook_func;
    nf_hook.hooknum = NF_INET_PRE_ROUTING; // 路由前捕获
    nf_hook.pf = PF_INET; // IPv4
    nf_hook.priority = NF_IP_PRI_FIRST; // 最高优先级

    // 注册钩子
    nf_register_net_hook(&init_net, &nf_hook);
    printk(KERN_INFO "Net capture module loaded\n");
    return 0;
}

// 模块退出
static void __exit net_capture_exit(void) {
    // 注销钩子
    nf_unregister_net_hook(&init_net, &nf_hook);
    printk(KERN_INFO "Net capture module unloaded\n");
}

module_init(net_capture_init);
module_exit(net_capture_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Linux Net Capture Module");

5. 网络通信调试方法

5.1 常用用户态调试工具

工具 核心作用 常用命令示例
netstat 查看网络连接、端口监听 netstat -tulnp(查看 TCP/UDP 监听端口) netstat -an(查看所有连接)
ss 替代 netstat,性能更高 ss -tulwp(查看监听端口及关联进程) ss -s(查看网络统计)
tcpdump 抓包分析(命令行) tcpdump -i eth0 port 8888(捕获 eth0 端口 8888 数据包) tcpdump -w capture.pcap(保存抓包到文件)
wireshark 图形化抓包分析 打开 capture.pcap 分析 TCP 三次握手/数据内容
strace 跟踪进程的系统调用 strace -e socket,connect,recv,send ./tcp_client(跟踪客户端系统调用)
ping 测试网络连通性(ICMP) ping 127.0.0.1(本地回环测试) ping -c 4 www.baidu.com(发送 4 个包)
telnet 测试端口是否可达 telnet 127.0.0.1 8888(测试 8888 端口是否监听)

5.2 内核态调试方法

  1. printk 日志 :内核模块中通过 printk(KERN_INFO "xxx") 打印协议栈/sk_buff 信息,通过 dmesg 查看;

  2. ftrace 跟踪 :跟踪内核网络函数调用(如 tcp_v4_connectip_rcv):

    bash 复制代码
    # 启用 ftrace
    echo function > /sys/kernel/debug/tracing/current_tracer
    echo tcp_v4_connect > /sys/kernel/debug/tracing/set_ftrace_filter
    echo 1 > /sys/kernel/debug/tracing/tracing_on
    # 执行网络操作后查看日志
    cat /sys/kernel/debug/tracing/trace
  3. perf 性能分析 :分析网络函数的执行时间和 CPU 占用:

    bash 复制代码
    perf record -g -p <进程PID> # 记录进程调用栈
    perf report # 查看分析结果

6. 常见问题

问题1:TCP 粘包/拆包问题如何解决?

答:TCP 是字节流协议,无"数据包"边界,粘包/拆包的核心原因是内核缓冲区/MTU 限制,解决思路:

  1. 固定长度包:每次发送固定长度数据,接收方按固定长度读取;
  2. 分隔符分隔 :在数据包末尾添加特殊分隔符(如 \n),接收方按分隔符拆分;
  3. 消息头+消息体:数据包前添加长度字段(如 4 字节表示消息长度),接收方先读长度再读数据;
  4. 应用层协议:使用成熟协议(如 HTTP、Protobuf),自带消息边界。

问题2:epoll 相比 select/poll 的核心优势?

答:核心优势是效率扩展性

  1. 时间复杂度:epoll 是 O(1)(仅处理就绪 fd),select/poll 是 O(n)(轮询所有 fd);
  2. fd 数量限制:select 受 FD_SETSIZE 限制(默认 1024),epoll 无限制;
  3. 内存拷贝:select/poll 每次调用需将 fd 集合拷贝到内核,epoll 只需注册一次,无需重复拷贝;
  4. 触发方式:epoll 支持水平触发(LT)和边缘触发(ET),select/poll 仅支持水平触发。

问题3:TCP 关闭后的 TIME_WAIT 状态是什么?如何优化?

答:TIME_WAIT 是 TCP 四次挥手后主动关闭方的状态(默认 2MSL 时间,约 1-4 分钟),核心目的是确保被动关闭方收到最后一个 ACK,避免旧数据包干扰新连接。

优化方式:

  1. 设置套接字选项 SO_REUSEADDR:允许端口快速复用;
  2. 设置 SO_LINGER:控制 close() 行为(慎用,可能导致数据丢失);
  3. 调整内核参数 net.ipv4.tcp_tw_reuse = 1:允许 TIME_WAIT 端口复用;
  4. 调整 net.ipv4.tcp_tw_timeout:缩短 TIME_WAIT 超时时间(不建议全局修改)。

问题4:阻塞与非阻塞套接字的核心区别?

答:核心区别在于IO 函数的返回行为

  1. 阻塞套接字:IO 函数(recv/send/accept/connect)未就绪时,进程睡眠,直到事件就绪;
  2. 非阻塞套接字:IO 函数未就绪时,立即返回 -EAGAIN,进程不阻塞;
  3. 非阻塞套接字必须配合循环/IO 多路复用使用,否则会频繁返回错误,浪费 CPU。

问题5:内核态 sk_buff 操作的注意事项?

答:sk_buff 是内核网络栈的核心,操作时需注意:

  1. 避免直接修改 data/head/tail/end,使用内核提供的 API(如 skb_put()skb_pull());
  2. 分配 sk_buff 后必须检查是否为 NULL,避免空指针;
  3. 中断上下文操作 sk_buff 时,需使用自旋锁保护;
  4. 及时释放 sk_buff(kfree_skb()),避免内存泄漏。

7. 总结

关键点回顾

  1. 核心架构:Linux 网络通信基于 TCP/IP 分层模型,内核协议栈实现底层协议逻辑,用户态通过套接字(文件描述符)提供统一操作接口,遵循"一切皆文件"思想;
  2. 核心机制
    • TCP 是面向连接、可靠的字节流协议,依赖三次握手/四次挥手、滑动窗口、拥塞控制;
    • UDP 是无连接、不可靠的数据包协议,速度快,适合实时性要求高的场景;
    • IO 多路复用(epoll)是高并发网络编程的核心,相比 select/poll 效率提升显著;
  3. 调试与问题
    • 用户态调试优先使用 tcpdump/ss/strace,内核态调试用 printk/ftrace;
    • TCP 粘包需通过应用层定义消息边界解决,TIME_WAIT 可通过套接字选项/内核参数优化;
    • 套接字的阻塞/非阻塞模式需根据并发需求选择,非阻塞模式需配合 epoll 使用。
相关推荐
hweiyu002 小时前
Linux 命令:patch
linux·运维·服务器
ID_180079054732 小时前
Python调用淘宝评论API:从入门到首次采集全流程
服务器·数据库·python
Web极客码2 小时前
宝塔面板后台突然显示“IO延迟非常高”
linux·服务器·数据库
IDC02_FEIYA2 小时前
Windows资源管理器未响应怎么处理?
运维·服务器·windows
遇见火星2 小时前
Linux 服务可用性监控实战:端口、进程、接口怎么监控?
android·linux·运维
claider2 小时前
Vim User Manual 阅读笔记 usr_22.txt Finding the file to edit 多文件编辑浏览
笔记·编辑器·vim
AI视觉网奇2 小时前
ue 导出 fbx
笔记·学习·ue5
tod1132 小时前
IP分片和组装的具体过程
运维·服务器·网络
野犬寒鸦2 小时前
从零起步学习并发编程 || 第三章:JMM(Java内存模型)详解及对比剖析
java·服务器·开发语言·分布式·后端·学习·spring