TCP 并发服务器模型:4 种 IO 模型完整梳理

1.TCP并发之多线程模型

多线程是实现TCP并发最基础的方案。

  1. 主线程阻塞accept获取新连接,每来一个客户端就创建子线程专门处理该连接读写,以此实现并发。
  2. 缺点:每个线程拥有独立栈空间,系统可创建线程总数有限。客户端并发量很大时,大量线程会带来内存占用、上下文切换开销,严重时会导致系统崩溃。
  3. 优化方案 :实际开发一般搭配线程池使用,复用线程,避免频繁创建销毁线程。

Linux下IO模型一共分为4类:

  1. 阻塞IO
  2. 非阻塞IO
  3. 信号驱动IO
  4. IO多路复用

2.非阻塞IO

普通socket默认是阻塞模式。调用recv、accept时,如果没有数据,函数会挂起等待。 非阻塞模式下,没有就绪数据时函数不会阻塞,会立刻返回,同时设置errno为EAGAIN / EWOULDBLOCK。

设置非阻塞三种写法:

复制代码
#include <fcntl.h>

//方式1:fcntl修改已有fd,追加非阻塞标志
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

//方式2:创建socket直接指定非阻塞
int sockfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);

//方式3:Linux特有 accept4,直接得到非阻塞客户端fd
int client_fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK);

注意:单纯非阻塞IO需要自己循环轮询,CPU空耗很高,一般不会单独使用,大多配合多路复用一起工作。


3.信号驱动IO

核心思路:文件描述符IO就绪之后,内核给进程发送SIGIO信号,程序在信号处理函数中完成读写,主线程可以做别的业务,不需要阻塞等待IO。

使用三步:

  1. 使用signal()注册SIGIO信号捕获函数;
  2. fcntl设置fd开启异步标志O_ASYNC;
  3. F_SETOWN告诉内核:IO就绪时把信号发给当前进程。

关键示例代码 read.c

复制代码
#include "head.h"
int fd = 0;

void handler(int signo)
{
    char tmpbuff[4096] = {0};
    memset(tmpbuff, 0, sizeof(tmpbuff));
    read(fd, tmpbuff, sizeof(tmpbuff));
    printf("FIFO:%s\n", tmpbuff);
    return;
}

int main(void)
{
    int flags = 0;
    char tmpbuff[4096] = {0};
    signal(SIGIO, handler);

    mkfifo("/tmp/myfifo", 0664);
    fd = open("/tmp/myfifo", O_RDONLY);
    if (-1 == fd)
    {
        perror("fail to open");
        return -1;
    }
    //开启异步IO O_ASYNC
    flags = fcntl(fd, F_GETFL);
    flags |= O_ASYNC;
    fcntl(fd, F_SETFL, flags);
    //指定内核将SIGIO发送给当前进程
    fcntl(fd, F_SETOWN, getpid());

    while (1)
    {
        memset(tmpbuff, 0, sizeof(tmpbuff));
        gets(tmpbuff);
        printf("STDIN:%s\n", tmpbuff);
    }
    close(fd);
    return 0;
}

write.c

复制代码
#include "head.h"
int main(void)
{
    int fd = 0;
    char tmpbuff[4096] = {0};
    mkfifo("/tmp/myfifo", 0664);
    fd = open("/tmp/myfifo", O_WRONLY);
    if (-1 == fd)
    {
        perror("fail to open");
        return -1;
    }
    while (1)
    {
        gets(tmpbuff);
        write(fd, tmpbuff, strlen(tmpbuff));
        if (0 == strcmp(tmpbuff, ".quit"))
        {
            break;
        }
    }
    close(fd);
    return 0;
}

局限:信号驱动IO在网络编程中使用较少。信号处理函数内部有很多函数调用限制,高并发场景下信号会大量频发,容易丢失事件。


4.IO多路复用

Linux主流实现:select、poll、epoll。 一个线程可以同时监视大量文件描述符;当任意fd可读、可写、异常就绪,函数返回,用户层再去处理对应fd。是TCP高并发服务器最核心技术。

select

头文件

复制代码
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

函数原型

复制代码
int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

功能:同时监视多个文件描述符。阻塞直到至少一个fd就绪,或者超时。

重要特性:readfds/writefds/exceptfds属于传入传出参数。内核会把没有事件发生的fd从集合清除,下一轮循环需要重新复原fd集合。

参数说明:

  • nfds:要监视最大fd数值 + 1
  • readfds:监听可读事件集合
  • writefds:监听可写事件集合,一般传NULL
  • exceptfds:监听异常事件集合,一般传NULL
  • timeout:超时时间;传NULL代表永久阻塞;0代表不阻塞

返回值:

  • 0:就绪fd总数量

  • 0:超时,没有事件就绪
  • -1:出错

fd集合操作宏:

复制代码
void FD_ZERO(fd_set *set);         //清空集合
void FD_SET(int fd, fd_set *set);  //fd加入集合
void FD_CLR(int fd, fd_set *set);  //fd移除集合
void FD_ISSET(int fd, fd_set *set);//判断fd是否就绪

select缺点:fd集合大小有上限;每次调用需要把fd集合从用户态拷贝内核态;返回后需要遍历全部fd找就绪事件。

epoll

epoll是Linux独有的多路复用方案,解决select/poll性能短板,高并发场景首选。

epoll_create
复制代码
int epoll_create(int size);

功能:创建epoll实例,在内核维护事件监听表,返回epoll专用fd。 参数:size在新版本Linux已经被忽略,但参数必须大于0。 返回值:成功返回epfd;失败返回‑1。

epoll_ctl
复制代码
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

功能:对epoll内核事件表执行增、删、改操作。

参数:

  • epfd:epoll_create返回的句柄

  • op操作类型

    • EPOLL_CTL_ADD:添加fd到内核监听表
    • EPOLL_CTL_MOD:修改fd注册事件
    • EPOLL_CTL_DEL:从内核移除fd
  • fd:要监控的目标文件描述符

  • event:事件结构体指针

    struct epoll_event {
    uint32_t events; //关注事件掩码
    epoll_data_t data; //用户自定义数据
    };
    typedef union epoll_data {
    void *ptr;
    int fd; //最常用,保存待监控fd
    uint32_t u32;
    uint64_t u64;
    } epoll_data_t;

events常用掩码:

  • EPOLLIN:可读事件
  • EPOLLOUT:可写事件
  • EPOLLERR:错误事件(自动监控,无需手动注册)
  • EPOLLHUP:对端关闭(自动监控)
  • EPOLLET:边缘触发,默认水平触发(LT)
  • EPOLLONESHOT:一次性触发

返回值:成功返回0;失败返回‑1。

epoll_wait
复制代码
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

功能:阻塞等待事件就绪,将就绪事件填充到events数组返回。

参数:

  • epfd:epoll实例fd
  • events:输出数组,存放就绪事件
  • maxevents:数组最大长度
  • timeout:毫秒;‑1无限阻塞;0非阻塞立即返回

返回:

  • 0:就绪fd个数

  • 0:超时无事件
  • -1:调用失败

epoll优势:只返回就绪fd,不需要遍历全部连接;内核维护事件表,不用每次调用拷贝全部fd;百万并发场景性能远高于select。


相关推荐
大哥您好4 小时前
QEMU 11.1.2 多架构编译指南(x86_64 / aarch64 / riscv64)
linux·服务器
Gauss松鼠会6 小时前
高斯数据库(openGauss)的物理存储结构
运维·服务器·数据库·sql·opengauss·经验总结
Android系统攻城狮6 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_prepare_read调用流程与实战(七十四)
linux·运维·服务器·gstreamer音视频·音视频进阶
杨云龙UP6 小时前
PostgreSQL 18.4 数据库整库迁移实战:pg_dump 备份与 pg_restore 恢复完整操作手册
运维·服务器·数据库·postgresql·pg_dump·pg_restore
我科绝伦(Huanhuan Zhou)6 小时前
Linux 通用压力测试脚本合集:磁盘 + 网络
linux·网络·压力测试
聚美智数7 小时前
网站HTTP状态查询-SEO优化
网络·网络协议·http
treesforest8 小时前
数据包是如何找到回家路的?
网络·智能路由器·nat·ip归属地查询
土星云SaturnCloud8 小时前
土星云智慧服务区方案:AI视觉+边缘计算+视频存管一体化的全场景智能化落地解析
服务器·人工智能·ai·边缘计算
有毒的教程8 小时前
Linux底层存储路由配
linux·运维·网络
是真的小外套9 小时前
第9章:网络层安全协议 IPSec 学习笔记
网络·网络协议·tcp/ip