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

1.TCP并发之多线程模型

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

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

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

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

2.非阻塞IO

普通socket默认是阻塞模式。调用recvaccept时,如果没有数据,函数会挂起等待。 非阻塞模式下,没有就绪数据时函数不会阻塞,会立刻返回,同时设置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主流实现:selectpollepoll。 一个线程可以同时监视大量文件描述符;当任意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。


相关推荐
斑马1391 小时前
Linux软件编程学习笔记(十二)——TCP并发服务器模型
java·服务器·网络
学着改变2751 小时前
2026便携式超声波流量计性能白皮书 户外巡检适用性横评
大数据·网络·人工智能·科技·产品运营·量子计算
rcms152702692182 小时前
Novellus Systems 02-033134-00晶圆底座加热器
网络
C+ 安口木2 小时前
Alibaba Cloud Linux 3 服务器环境搭建实战记录
linux·运维·服务器
聚搜云——JuSouClouD2 小时前
杭州腾讯云代理商:腾讯云2核4G服务器够用吗?网站、API和开发测试怎么选
服务器·云计算·腾讯云
马立杰2 小时前
IE241210_路由安全技术(三层安全)
网络·安全
马立杰2 小时前
IE241211_路由安全技术(三层安全)
网络·安全·智能路由器
迪康Defender2 小时前
公用电脑责任追溯难?迪康端点安全一体化管理系统用户模式详解
java·运维·开发语言·网络·其他·安全
迪康Defender3 小时前
迪康端点安全一体化管理系统文档安全:构建企业文档资产全链路防护体系
运维·网络·安全·web安全·终端安全管理