Linux网络编程踩坑:多线程并发服务器中文件描述符的正确传递方式
在编写多线程并发服务器(Multi-threaded Server)时,最容易让新手“翻车”的一个细节就是如何将主线程接收到的文件描述符(File Descriptor, fd)正确地传递给子线程。
很多初学者会发现,明明代码逻辑看起来没问题,但当多个客户端同时连接时,消息却发错乱了(比如客户端A发的消息被服务器回传给了客户端B)。本文将基于课堂笔记,深度解析这个经典的“内存共享陷阱”,并提供正确的解决方案。
一、 问题背景:为什么不能直接传地址?
1. 典型错误场景
在主线程的 while 循环中,我们通常会这样做:
- 调用
accept()阻塞等待,返回一个新的文件描述符cfd。 - 调用
pthread_create()创建子线程。 - 错误操作:将
cfd的地址(&cfd)作为参数传递给子线程。
2. 原因深度剖析
这涉及到了进程与线程的内存模型差异:
- 进程(Process):拥有独立的虚拟地址空间。修改一个进程的变量不会影响另一个进程(写时复制)。
- 线程(Thread):共享同一个进程的虚拟地址空间(堆、全局变量等)。
灾难发生的流程如下:
- 时刻 T1:主线程
accept成功,cfd变量被赋值为 3(代表客户端 A)。 - 时刻 T2:主线程调用
pthread_create,传入&cfd。 - 时刻 T3:主线程继续循环,再次
accept成功,cfd变量被更新为 4(代表客户端 B)。 - 时刻 T4:子线程 A 开始运行,通过传入的地址
&cfd去读取数据。此时它读到的是被修改后的值 4。
结果:子线程 A 本该服务客户端 A(fd=3),结果却拿到了 fd=4,导致它错误地与客户端 B 进行了通信,或者发生“串台”现象。
二、 解决方案:构建描述符管理数组
为了避免上述竞态条件(Race Condition),我们需要保证每个子线程拿到的文件描述符是存储在独立内存区域中的,互不干扰。
核心思路:
- 定义一个全局结构体数组(或整型数组)。
- 主线程
accept成功后,遍历数组找到一个“空闲位置”。 - 将
cfd存入该位置。 - 将该数组元素的地址传递给子线程。
这样,即使主线程的 cfd 局部变量变了,数组中存储的值也不会变。
三、 代码实战
下面是一个完整的、可运行的多线程并发服务器代码案例。
1. 代码实现 (server_fixed.c)
#include
#include
#include
#include
#include
#include
#include
#define MAX_CONN 1024
#define PORT 8888
// 定义一个结构体来存储每个连接的信息
struct SockInfo {
int fd; // 通信文件描述符
struct sockaddr_in addr; // 客户端地址信息(可选,方便日志打印)
};
// 全局数组,用于管理所有连接
// 初始化时需要将fd设为-1,表示空闲
struct SockInfo infos[MAX_CONN];
// 子线程的工作函数
void* worker(void* arg) {
// 1. 将参数强转回结构体指针
struct SockInfo* pinfo = (struct SockInfo*)arg









