网络通信 --- TCP并发服务器/IO模型/多路复用IO相关函数接口 --- Linux

1、TCP并发服务器

1.1TCP并发服务器问题

服务端需要同时处理多个客户端连接时,核心矛盾

  • accept():阻塞等待新客户端的三次握手,若无新连接则一直阻塞;
  • recv()/send():阻塞等待对应客户端的读写数据,若无数据则一直阻塞。单线程下,阻塞 IO 会导致服务器只能处理一个客户端,无法响应其他客户端的连接 / 数据请求,程序逻辑无法满足并发需求。

解决方法:

  • TCP并发服务器线程/进程模型:
    • 优点:实现简单(每个新连接创建一个线程 / 进程处理),逻辑直观;
    • 缺点:资源消耗大,连接数过多时会耗尽系统资源,且线程 / 进程切换开销大;
  • TCP并发服务器多路复用模型(select/poll/epoll)
    • 优点:单线程 / 少量线程即可处理大量连接,资源消耗低,切换开销小;
    • 缺点:编程逻辑稍复杂,select/poll 存在性能瓶颈(epoll 无此问题)。

1.2 Linux系统的4种IO模型

阻塞IO:

数据没来时,进程/线程阻塞等待,不占用CPU资源

  • 优点:CPU利用率高,编程简单
  • 缺点:一个线程只能处理一个fd

非阻塞IO:

系统调用立即返回,用户程序需要不断轮询检查数据

  • 优点:一个线程可管理多个fd
  • 缺点:大量占用CPU资源(空轮询)

多路复用IO:

用一个函数(select/poll/epoll)监听多个fd是否产生IO事件,

只要有fd就绪就返回,用户再处理对应fd

  • 优点:一个线程可管理大量连接,CPU利用率高
  • 缺点:编程逻辑复杂

信号驱动IO:

当fd数据就绪时,内核主动发SIGIO信号通知应用,

应用在信号处理函数中读取数据

异步IO:

应用发起调用IO接口操作后立即返回,内核完成全部IO操作

(包括数据拷贝到用户缓冲区)后才通知应用

IO模型 特点 CPU占用 并发能力 编程复杂度
阻塞IO 数据未到阻塞 简单
非阻塞IO 轮询检查 中等
多路复用 统一监听 复杂
信号驱动 信号通知就绪 较复杂
异步IO 内核完成所有 最复杂
  • 信号驱动 I/O:内核只负责"通知你可以做了"
  • 异步 I/O:内核直接把 I/O 操作做完,完成后再通知你

1.3 函数接口

1.3.1 fcntl(设置文件描述符属性)(文件描述符控制)

  • 函数原型:
cpp 复制代码
int fcntl(int fd, int cmd, ... /* arg */ );
  • 功能:对已打开的文件描述符fd执行指定的控制操作(如修改属性、设置所有权、获取状态等)
  • 参数:
    • fd:要操作的文件描述符
    • cmd:操作命令(核心常用值如下)
      • F_GETFL:获取文件描述符的状态标志(如 O_NONBLOCK、O_ASYNC)
      • F_SETFL:设置文件描述符的状态标志(仅能修改 O_APPEND、O_NONBLOCK、O_ASYNC 等)
      • F_SETOWN:设置接收 SIGIO/SIGURG 信号的进程 ID / 进程组 ID
      • F_GETOWN:获取接收 SIGIO/SIGURG 信号的进程 ID / 进程组 ID
    • arg:可选参数,根据 cmd 决定(如 F_SETFL 时传新的标志位,F_SETOWN 时传进程 ID)
  • 返回值:
    • 执行 F_GETFL:成功返回文件描述符的状态标志值(整数),失败返回 - 1
    • 执行 F_SETFL/F_SETOWN:成功返回 0,失败返回 - 1
    • 其他 cmd(如 F_DUPFD):成功返回新的文件描述符,失败返回 - 1
    • 通用规则:失败时均会设置errno
  • eg:
cpp 复制代码
// 某些系统需要定义 _GNU_SOURCE 才能看到 F_SETOWN
#define _GNU_SOURCE
#include <fcntl.h>

#include <unistd.h>
#include <signal.h>
#include <stdio.h>

// 示例:将fd设置为信号驱动IO
void sigio_handler(int signo) {
    printf("收到IO事件通知\n");
}

void set_async_io(int fd) {
    int flags;
    // 1. 获取当前fd的属性
    flags = fcntl(fd, F_GETFL);
    if (flags == -1) {
        perror("fcntl F_GETFL error");
        return;
    }
    // 2. 设置异步IO属性(O_ASYNC)
    flags |= O_ASYNC;
    if (fcntl(fd, F_SETFL, flags) == -1) {
        perror("fcntl F_SETFL error");
        return;
    }
    // 3. 设置当前进程接收该fd的SIGIO信号
    if (fcntl(fd, F_SETOWN, getpid()) == -1) {
        perror("fcntl F_SETOWN error");
        return;
    }
}
复制代码
// F_SETOWN 的作用:设置哪个进程(或进程组)接收该fd的SIGIO信号
fcntl(fd, F_SETOWN, getpid());  // 设置当前进程接收信号

关键注意

  • O_ASYNC仅对 "支持异步的文件描述符" 有效(如套接字、终端),普通文件不支持;
  • 异步 IO 的核心是 "内核主动通知",但O_ASYNC实现的是信号驱动 IO(非真正的异步 IO)。
cpp 复制代码
//常用示例:

// 1. 获取 fd 的状态标志	
int flags = fcntl(fd, F_GETFL);	
if (flags == -1) { 
    perror("fcntl F_GETFL"); 
    return -1;
}


// 2. 设置 fd 为非阻塞IO
flags |= O_NONBLOCK;
if (fcntl (fd, F_SETFL, flags) == -1) { 
    perror ("fcntl F_SETFL"); 
    return -1;
}


// 3. 设置 fd 的 SIGIO 信号归属当前进程
if (fcntl (fd, F_SETOWN, getpid ()) == -1) { 
    perror ("fcntl F_SETOWN"); 
    return -1;
}


| 注意事项 | 
1. `F_SETFL`仅能修改**可修改的标志位**
(如O_NONBLOCK、O_ASYNC),无法修改O_RDONLY、O_WRONLY等打开时的基础标志;
2. `O_ASYNC`(异步IO标志)仅对套接字、终端等"面向字符"的文件描述符有效,普通文件不支持。

1.3.2 select 函数(多路复用IO)

  • 函数原型:
cpp 复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds, 
            fd_set *exceptfds, struct timeval *timeout);
  • 功能:监听多个文件描述符的读、写、异常事件,直到有事件发生或超时
  • 参数:
    • nfds:需要监听的最大文件描述符 + 1(select 会从 0 遍历到 nfds-1,因此必须传最大值 + 1)
    • readfds:读文件描述符(事件)集合(监听是否有数据可读,如 recv、accept),传 NULL 表示不监听
    • writefds:写事件集合(监听是否可写,如 send),传 NULL 表示不监听
    • exceptfds:异常事件集合(如套接字带外数据:可以理解为"紧急数据"或"加急数据",它在正常数据流之外传输,即使接收缓冲区满了也能被立即处理。),传 NULL 表示不监听
    • timeout:超时时间,结构体定义如下:
cpp 复制代码
struct timeval {
    long tv_sec; // 秒
    long tv_usec; // 微秒
};
  • 返回值:
    • 成功:返回就绪(产生事件)的文件描述符总数(读+写+异常)
    • 失败:返回-1,设置errno(如被信号中断时errno=EINTR)
    • 超时:返回0(无任何事件就绪
  • 注意事项 :
    • select返回后,未就绪的文件描述符会被从集合中清除,因此每次调用前需重新初始化集合;
      1. 监听的文件描述符数量受限于FD_SETSIZE(默认1024),无法突破;
    • 需遍历所有监听的fd才能找到就绪的fd,连接数越多效率越低。

1.3.3 fd_set相关辅助宏(操作文件描述符集合)

cpp 复制代码
void FD_CLR(int fd, fd_set *set);

**功能:**将文件描述符fd从集合set中移除

**注意:**fd需是有效的、已加入集合的描述符

cpp 复制代码
int FD_ISSET(int fd, fd_set *set);

**功能:**判断fd是否在就绪的集合set中

**返回值:**在集合中返回非0值,不在返回0

**核心用途:**select返回后,遍历判断哪个fd就绪

cpp 复制代码
void FD_SET(int fd, fd_set *set);

**功能:**将文件描述符fd加入集合set

**注意:**fd不能超过FD_SETSIZE-1,否则行为未定义

cpp 复制代码
void FD_ZERO(fd_set *set);

功能: 将集合set的所有位清0(初始化集合);必须在使用集合前调用,否则集合内数据随机

cpp 复制代码
//常用示例:

fd_set read_fds;

// 1. 初始化集合
FD_ZERO(&read_fds);

// 2. 将监听fd加入集合
FD_SET(listen_fd, &read_fds);

// 3. 调用select
select(listen_fd+1, &read_fds, NULL, NULL, NULL);

// 4. 判断fd是否就绪
if (FD_ISSET(listen_fd, &read_fds)) {
  // 监听fd有事件(新连接)
}

// 5. 移除fd
FD_CLR(listen_fd, &read_fds);
集合状态 timeout=NULL timeout=0 timeout>0
空集合 进程会一直阻塞,直到手动发送信号(如 Ctrl+C)中断,否则不会返回。 立即返回0 等待超时后返回0
有fd但无事件 阻塞直到有事件就绪/被信号中断 立即返回0 等待至超时(返回 0)或有 fd 就绪(返回就绪数)
相关推荐
聚搜云——JuSouClouD2 分钟前
2026找阿里云代理商采购GPU服务器,有没有额外折扣?
服务器·阿里云·云计算
Lynne3095 分钟前
工业边缘计算网关到底解决什么问题?从设备接入到AI推理的技术链路
网络·数据分析·数据
鲁邦通物联网20 分钟前
海外设备怎么远程维护?工业路由器远程运维架构设计
运维·网络·智能路由器·工业路由器·工业级路由器·5g路由器·5g工业路由器
wangxiaojie668822 分钟前
一键自动认证校园网,告别手动配置
服务器
张宇Joaquin39 分钟前
基于鲲鹏服务器的金仓数据库调优参考实践
服务器
艾芯微科技40 分钟前
ESDUNL24VC1 单向 ESD 静电保护二极管参数、电路设计与 PCB 防护布局
网络·单片机·嵌入式硬件·集成测试·51单片机
国际云,接待1 小时前
华为云 ELB 如何配出真正高可用:跨可用区 ECS、健康检查和灰度摘流实战
服务器
❁满城风絮*1 小时前
Nginx—从默认页面到高性能网
网络·nginx
牢姐与蒯1 小时前
Linux文件(三).Ext系列文件系统
linux·运维·服务器
java_nnnn1 小时前
面试题(网络部分前传)-13问
服务器·网络·网络协议·tcp/ip·面试