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)的文件描述符集合,核心宏:cFD_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 多路复用):cstruct 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(套接字缓冲区):内核网络栈的核心数据结构,封装网络数据包,贯穿整个协议栈:cstruct 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:内核表示网络设备(网卡)的核心结构体:cstruct 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 三次握手与四次挥手
-
三次握手(建立连接):
- 客户端 → 服务端:SYN(同步序列号);
- 服务端 → 客户端:SYN+ACK(确认客户端序列号,同步服务端序列号);
- 客户端 → 服务端:ACK(确认服务端序列号);
核心目的:同步序列号,确保双方收发能力正常。

-
四次挥手(关闭连接):
- 主动关闭方 → 被动关闭方:FIN(无数据发送);
- 被动关闭方 → 主动关闭方:ACK(确认 FIN);
- 被动关闭方 → 主动关闭方:FIN(无数据发送);
- 主动关闭方 → 被动关闭方: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),不阻塞进程; -
设置方式:
cint flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 开启非阻塞 -
优点:单进程可轮询多个 fd,适合高并发;
-
缺点:需循环轮询,CPU 占用高(通常与 epoll 配合使用)。
3.4 内核态数据包处理流程
用户态发送/接收数据的背后,内核协议栈的核心处理流程:
(1)发送数据(用户态 → 网卡)
- 用户态调用
send(),数据从用户缓冲区拷贝到内核sk_buff; - 传输层(TCP/UDP)封装协议头(如 TCP 头:序列号、确认号、端口);
- 网络层(IP)封装 IP 头(源/目的 IP、协议类型、TTL);
- 链路层封装以太网帧头(源/目的 MAC、帧类型);
- 网卡驱动通过 DMA 将
sk_buff数据写入网卡硬件缓冲区,网卡发送数据。
(2)接收数据(网卡 → 用户态)
- 网卡接收数据后,通过 DMA 将数据写入内核
sk_buff,触发中断; - 内核中断处理函数(上半部)标记数据到达,触发下半部处理;
- 链路层解封装以太网帧头,校验帧类型;
- 网络层解封装 IP 头,查找路由表,转发或本地处理;
- 传输层解封装 TCP/UDP 头,将数据投递到对应套接字的接收缓冲区;
- 用户态调用
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 内核态调试方法
-
printk 日志 :内核模块中通过
printk(KERN_INFO "xxx")打印协议栈/sk_buff 信息,通过dmesg查看; -
ftrace 跟踪 :跟踪内核网络函数调用(如
tcp_v4_connect、ip_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 -
perf 性能分析 :分析网络函数的执行时间和 CPU 占用:
bashperf record -g -p <进程PID> # 记录进程调用栈 perf report # 查看分析结果
6. 常见问题
问题1:TCP 粘包/拆包问题如何解决?
答:TCP 是字节流协议,无"数据包"边界,粘包/拆包的核心原因是内核缓冲区/MTU 限制,解决思路:
- 固定长度包:每次发送固定长度数据,接收方按固定长度读取;
- 分隔符分隔 :在数据包末尾添加特殊分隔符(如
\n),接收方按分隔符拆分; - 消息头+消息体:数据包前添加长度字段(如 4 字节表示消息长度),接收方先读长度再读数据;
- 应用层协议:使用成熟协议(如 HTTP、Protobuf),自带消息边界。
问题2:epoll 相比 select/poll 的核心优势?
答:核心优势是效率 和扩展性:
- 时间复杂度:epoll 是 O(1)(仅处理就绪 fd),select/poll 是 O(n)(轮询所有 fd);
- fd 数量限制:select 受 FD_SETSIZE 限制(默认 1024),epoll 无限制;
- 内存拷贝:select/poll 每次调用需将 fd 集合拷贝到内核,epoll 只需注册一次,无需重复拷贝;
- 触发方式:epoll 支持水平触发(LT)和边缘触发(ET),select/poll 仅支持水平触发。
问题3:TCP 关闭后的 TIME_WAIT 状态是什么?如何优化?
答:TIME_WAIT 是 TCP 四次挥手后主动关闭方的状态(默认 2MSL 时间,约 1-4 分钟),核心目的是确保被动关闭方收到最后一个 ACK,避免旧数据包干扰新连接。
优化方式:
- 设置套接字选项
SO_REUSEADDR:允许端口快速复用; - 设置
SO_LINGER:控制 close() 行为(慎用,可能导致数据丢失); - 调整内核参数
net.ipv4.tcp_tw_reuse = 1:允许 TIME_WAIT 端口复用; - 调整
net.ipv4.tcp_tw_timeout:缩短 TIME_WAIT 超时时间(不建议全局修改)。
问题4:阻塞与非阻塞套接字的核心区别?
答:核心区别在于IO 函数的返回行为:
- 阻塞套接字:IO 函数(recv/send/accept/connect)未就绪时,进程睡眠,直到事件就绪;
- 非阻塞套接字:IO 函数未就绪时,立即返回
-EAGAIN,进程不阻塞; - 非阻塞套接字必须配合循环/IO 多路复用使用,否则会频繁返回错误,浪费 CPU。
问题5:内核态 sk_buff 操作的注意事项?
答:sk_buff 是内核网络栈的核心,操作时需注意:
- 避免直接修改
data/head/tail/end,使用内核提供的 API(如skb_put()、skb_pull()); - 分配 sk_buff 后必须检查是否为 NULL,避免空指针;
- 中断上下文操作 sk_buff 时,需使用自旋锁保护;
- 及时释放 sk_buff(
kfree_skb()),避免内存泄漏。
7. 总结
关键点回顾
- 核心架构:Linux 网络通信基于 TCP/IP 分层模型,内核协议栈实现底层协议逻辑,用户态通过套接字(文件描述符)提供统一操作接口,遵循"一切皆文件"思想;
- 核心机制 :
- TCP 是面向连接、可靠的字节流协议,依赖三次握手/四次挥手、滑动窗口、拥塞控制;
- UDP 是无连接、不可靠的数据包协议,速度快,适合实时性要求高的场景;
- IO 多路复用(epoll)是高并发网络编程的核心,相比 select/poll 效率提升显著;
- 调试与问题 :
- 用户态调试优先使用 tcpdump/ss/strace,内核态调试用 printk/ftrace;
- TCP 粘包需通过应用层定义消息边界解决,TIME_WAIT 可通过套接字选项/内核参数优化;
- 套接字的阻塞/非阻塞模式需根据并发需求选择,非阻塞模式需配合 epoll 使用。