Linux—网络通信04-IO多路复用-并发模型

1.核心目标

让服务器能够同时处理多个客户端的连接请求


2. 方案一:多进程模型 (Process-based)

利用 fork() 系统调用,为每一个新连接的客户端创建一个独立的子进程来处理。 关键知识点

  1. 僵尸进程的回收
    • 子进程结束后,如果父进程没有读取其退出状态,子进程会变成"僵尸进程",占用系统资源。
    • 解决方法 :使用信号机制。注册 SIGCHLD 信号处理函数,在函数中调用 wait(NULL) 自动回收子进程。
  2. 文件描述符的回收与继承
    • fork() 之后,子进程会复制 父进程的所有文件描述符(包括监听套接字 listfd 和通信套接字 conn)。
    • 原则
      • 父进程 :只负责监听 (listfd),不需要通信 (conn)。所以父进程收到 conn 后应立刻 close(conn)
      • 子进程 :只负责通信 (conn),不需要监听 (listfd)。所以子进程进入循环前应 close(listfd)
    • 后果:如果不关闭不需要的描述符,会导致文件描述符泄露,且可能导致端口无法释放。

💻 代码逻辑梳理

A. 信号处理 (防止僵尸进程)
bash 复制代码
// 定义信号处理函数
void handle(int num) 
{
    // wait(NULL) 会阻塞等待任意一个子进程结束,并回收其资源
    // 放在信号处理函数中,当子进程结束时,内核发送 SIGCHLD 信号触发此函数
    wait(NULL); 
}

// 在 main 函数中注册
signal(SIGCHLD, handle);
B. 主流程 (Main Loop)
bash 复制代码
int main()
 {
    // 1. 创建监听套接字 (socket, bind, listen) ... [省略]

    while (1) 
{
        // 2. 接受连接 (阻塞)
        int conn = accept(listfd, ...);
        if (conn == -1) { perror("accept"); continue; }

        // 3. 【核心】创建子进程
        pid_t pid = fork();

        if (pid > 0) 
{ 
            // --- 父进程区域 ---
            // 父进程不需要和这个特定的客户端通信,关闭 conn
            close(conn); 
            // 父进程继续循环,去 accept 下一个客户
            // 注意:这里不需要 wait(NULL),因为已经用信号处理了
        } 

        else if (pid == 0) 
{ 
            // --- 子进程区域 ---
            // 子进程不需要监听新连接,关闭 listfd (非常重要!)
            close(listfd); 

            // 4. 业务处理循环 (Echo Server 示例)
            while (1) 
{
                char buf[512] = {0};
                int rd_ret = recv(conn, buf, sizeof(buf), 0);
                
                if (rd_ret <= 0) 
{
                    printf("cli offline\n");
                    exit(0); // 子进程处理完毕或出错,直接退出
                }

                // 处理数据 (例如加上时间戳返回)
                time_t tm; time(&tm);
                sprintf(buf, "%s %s", buf, ctime(&tm));
                send(conn, buf, strlen(buf), 0);
            }
        } 
        else 
{
            // fork 失败
            perror("fork");
            close(conn);
        }
    }
    close(listfd);
    return 0;
}

3. 方案二:多线程模型 (Thread-based)

利用 pthread_create,为每一个新连接的客户端创建一个独立的线程来处理。相比进程,线程更轻量级,切换开销小,共享内存方便。

🔑 关键知识点

  1. 线程属性的设置 (分离属性)
    • 默认情况下,线程结束后需要其他线程调用 pthread_join 来回收资源,否则会变成"僵尸线程"。
    • 解决方法 :设置线程属性为 PTHREAD_CREATE_DETACHED (分离状态)。这样线程结束后,系统会自动回收资源,无需 join
  2. 参数传递的安全性
    • pthread_create 的第 4 个参数 void *arg 传递给线程函数。
    • 陷阱 :如果在主线程中直接传 &conn (局部变量的地址),由于主线程循环很快,conn 的值可能在子线程还没用到时就被下一次 accept 修改了。
    • 解决
      • 方法 A:malloc 一块内存存 conn,传给线程,线程用完 free
      • 方法 B (代码中使用):使用信号量 (sem_t) 进行同步,确保主线程在子线程保存好 conn 之前,不进行下一次循环修改 conn
  3. 互斥与同步 (信号量)
    • 代码中使用 sem_waitsem_post 来保证 conn 变量在多线程间传递时的安全性。

💻 代码逻辑梳理

A. 线程函数 (Worker Thread)
bash 复制代码
// 全局信号量,用于同步 conn 的传递
sem_t sem_arg; 

void *th(void *arg)
 {
    // 1. 获取传入的 conn 指针,并解引用拿到值
    // 注意:这里强转为 int* 是因为传入的是 &conn
    int conn = *(int*)arg; 
    
    // 2. 通知主线程:我已经拿到 conn 了,你可以继续循环了
    sem_post(&sem_arg); 

    // 3. 设置为分离线程 (可选,也可以在 create 时设置属性)
    pthread_detach(pthread_self());

    // 4. 业务处理循环
    while (1) 
{
        char buf[512] = {0};
        int rd_ret = recv(conn, buf, sizeof(buf), 0);
        
        if (rd_ret <= 0) 
{
            printf("cli offline\n");
            close(conn);
            break; // 跳出循环,线程函数结束,线程自动销毁
        }

        // 处理数据...
        time_t tm; time(&tm);
        sprintf(buf, "%s %s", buf, ctime(&tm));
        send(conn, buf, strlen(buf), 0);
    }
    return NULL;
}
B. 主流程 (Main Loop)
bash 复制代码
int main() 
{
    // 1. 初始化信号量 (初始值为 0)
    // 意味着 sem_wait 会阻塞,直到 sem_post 被调用
    sem_init(&sem_arg, 0, 0);

    // 2. 创建监听套接字 (socket, bind, listen) ... [省略]

    while (1)
 {
        int conn = accept(listfd, ...);
        if (conn == -1) { perror("accept"); continue; }

        pthread_t tid;
        
        // 3. 【核心】创建线程
        // 第 4 个参数传入 &conn (conn 变量的地址)
        pthread_create(&tid, NULL, th, &conn);

        // 4. 【同步】等待线程拿走 conn
        // 如果线程还没执行到 sem_post,这里主线程会阻塞
        // 防止主线程立刻进入下一次循环,修改了 conn 的值,导致线程拿到错误的 fd
        sem_wait(&sem_arg); 
        
        // 此时线程已经安全保存了 conn 的值,主线程可以继续 accept 下一个
    }
    
    sem_destroy(&sem_arg);
    close(listfd);
    return 0;
}

4. 两种模型对比总结

特性 多进程模型 (Process) 多线程模型 (Thread)
资源开销 。每个进程有独立的内存空间、页表等。 。线程共享进程内存,只有栈和寄存器独立。
切换速度 。涉及上下文切换,TLB 刷新等。 。只需切换栈和寄存器。
数据共享 。需要使用 IPC (管道、共享内存、消息队列)。 。直接读写全局变量即可 (但需加锁)。
稳定性 。一个进程崩溃不影响其他进程。 。一个线程崩溃 (如段错误) 可能导致整个进程挂掉。
适用场景 对稳定性要求极高,或需要利用多核 CPU 隔离任务。 高并发、IO 密集型任务 (如 Web 服务器、聊天室)。
代码关键点 fork(), wait(), SIGCHLD, 关闭多余 fd pthread_create, sem_t, 线程分离, 参数传递安全

常见坑点

  1. 多进程中 :为什么子进程要 close(listfd)
    • 答:如果不关闭,所有子进程都持有一份监听套接字的副本。当主进程想关闭服务器时,端口可能因为还有子进程占用而无法释放。此外,这也浪费了文件描述符资源。
  2. 多线程中 :为什么不能直接传 conn 而要传 &conn 还要加信号量?
    • 答:pthread_create 是异步的。如果直接传 conn 的值(通过 malloc),没问题。但如果传主线程栈上的 &conn,主线程循环极快,可能在子线程还没来得及读取 *arg 时,conn 的值就已经变成下一个客户的 fd 了。加信号量是为了强制主线程"等一等",确保数据同步。
  3. 僵尸进程/线程
    • 进程靠 signal(SIGCHLD, handler) + wait() 解决。
    • 线程靠 pthread_detach() 解决。
相关推荐
mpr0xy2 小时前
Linux操作系统安装nvidia-drives和nvidia-container-toolkit
linux·运维·服务器
minji...2 小时前
Linux 基础IO (三) (用户缓冲区/内核缓冲区深刻理解)
java·linux·运维·服务器·c++·算法
九天轩辕2 小时前
跨平台符号表生成规则详解:Windows/Linux/macOS/OHOS
linux·windows·macos
蜜獾云2 小时前
linux-磁盘挂载
linux·运维·服务器
白藏y2 小时前
【Linux】常见指令用法
linux
TG_yunshuguoji2 小时前
阿里云代理商:百炼如何查询账单明细和进行成本优化?
服务器·阿里云·云计算
c++之路2 小时前
Linux进程池与线程池深度解析:设计原理+实战实现(网盘项目架构)
java·linux·架构
Irissgwe2 小时前
Ext系列⽂件系统
linux·服务器·ext系统文件
翻斗包菜2 小时前
Nginx 四大核心功能实战:正向代理 + 反向代理 + 缓存 + Rewrite 正则
运维·nginx·缓存