网络通信 --- 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 就绪(返回就绪数)
相关推荐
大母猴啃编程1 小时前
Socket编程UDP
linux·网络·c++·网络协议·udp
kessy11 小时前
LKT4304加密芯片在工业PLC控制器中的安全应用案例
网络
悟凡爱学习2 小时前
Linux 操作系统&消息队列
linux·运维·服务器
大Mod_abfun2 小时前
AntdUI教程#1ChatList交互(vb.net)
服务器·前端·ui·交互·antdui·聊天框
嵌入式×边缘AI:打怪升级日志2 小时前
2.3.2 目录与文件操作命令(保姆级详解)
linux·运维·服务器
艾莉丝努力练剑2 小时前
MySQL查看命令速查表
linux·运维·服务器·网络·数据库·人工智能·mysql
哈__2 小时前
Index-TTS 声音克隆搭载cpolar内网穿透,随时随地生成专属语音!
网络
皮皮哎哟2 小时前
Linux网络最终篇:TCP并发服务器
linux·服务器·select·epoll·poll·tcp并发
sbjdhjd2 小时前
RHCE | Linux 例行性工作(定时任务)从入门到精通
linux·运维·服务器·华为·云计算