最新资讯

  • 从单线程到线程池:TCP服务器并发处理演进之路

从单线程到线程池:TCP服务器并发处理演进之路

2026-02-02 18:39:43 栏目:最新资讯 6 阅读

目录

一、单执行流服务器的弊端(单线程进程)

1、单线程服务器

2、为何客户端显示连接成功?

第一步:第一个客户端连接

第二步:第二个客户端连接

关键点

3、解决方案

二、多进程版TCP网络程序

1、多进程服务器基本原理

进程创建与服务分配

并发处理优势

2、文件描述符继承机制

描述符表特性

套接字继承示例

匿名管道通信案例

3、子进程管理策略

1. 僵尸进程问题

2. 阻塞式等待方案

3. 非阻塞式等待方案

4、优雅解决方案

1. 忽略SIGCHLD信号(捕捉SIGCHLD信号)

2. 双层进程模型(推荐)

5、补充知识

问题1:进程退出后,曾经打开的文件会怎么办?

问题2:父进程打开文件后创建子进程,子进程能使用父进程的fd吗?

以管道为例进行验证:像之前我们提到的“管道不就是吗?”完全正确!!!让我们拆解一下管道的创建和使用过程:

需要注意的地方:

总结表格

三、多线程版TCP网络程序设计

1、进程与线程创建成本对比

2、多线程服务器基本架构

2.1 连接处理流程

2.2 线程管理策略

3、文件描述符管理

3.1 共享文件描述符表

3.2 使用规范

4、参数结构体设计

5、文件描述符共享问题

6、将Service函数定义为静态成员函数的原因

静态成员函数要求

7、关键注意事项

8、扩展建议

9、代码测试

ps -aL 命令

ps -axj 命令

10、补充:问题分析

问题1:fd线程能看到吗?

问题2:线程敢不敢关闭自己不需要的fd?

popen/pclose 函数详解

函数原型

功能说明

执行流程

底层实现

关键知识点总结

四、线程池版的TCP网络程序

1、传统多线程模式的缺陷

线程创建与销毁开销大

高并发场景性能瓶颈

2、解决方案

线程池预创建机制

线程复用机制

资源管控机制

3、线程池的实现与应用

4、服务类新增线程池成员

5、任务类设计

6、设计Handler类

7、代码测试阶段


一、单执行流服务器的弊端(单线程进程)

正如之前所述,当单个客户端连接服务端时,该客户端可以正常接收服务端提供的服务。如下所示:

当第一个客户端正与服务端保持连接时,我们尝试让第二个客户端接入服务器,然后在客户端这边显示第二个客户端他自己连接成功,但是服务端那边却没有显示新加入的客户端:

然后我们在第二个客户端输入要发送的信息,然后发送给服务端,我们看到发送给服务端的消息既未在服务端显示,服务端也未将该消息回显给该客户端。如下所示:

我们此时可以尝试关闭第一个客户端进程,然后我们可以看到服务端会等待第一个客户端断开连接后,才会处理并回显第二个客户端发送的数据:

1、单线程服务器

  • 实验结果表明,该服务器只能依次处理客户端请求,必须完成当前客户端的服务(第一个客户端)后才能响应下一个连接(第二个客户端)。

  • 这是因为我们实现的是单线程版本服务器(当前服务端进程只有一个线程),同一时间仅能为一个客户端提供服务。

  • 当服务器通过accept函数建立连接后,会专注于服务该客户端(第一个客户端)。如下,可以验证当前服务端还在为第一个客户端进行服务:

  • 在此期间,虽然其他客户端可能发起连接请求,但由于服务器采用单线程架构,无法同时处理多个连接请求。

2、为何客户端显示连接成功?

  • 当服务器处理第一个客户端的请求时,第二个客户端的连接请求实际上已被系统接收。

  • 尽管服务器尚未调用accept函数获取该连接,但底层TCP协议会维护一个连接队列(回顾listen()函数将sockfd设置为监听状态,最多允许backlog个客户端处于连接等待队列。若超过该数量的连接请求将被自动忽略,通常建议将该值设置为较小的数值(如5)),未处理的连接会被暂存其中,而通过已建立的连接传输但未处理的连接,它的数据存放在内核接收缓冲区。

  • 一旦服务器调用 accept() 接受连接,就可以立即读取已到达的数据!!!

  • 这个队列的最大长度由listen函数的第二个参数决定,因此第二个客户端会立即收到连接成功的响应。(注意:意思是服务端监听listen完成三次握手之后,connect函数就相当于成功返回了,它不管是否完成accept函数操作!!!)

listen() 函数的第二个参数指的是「完整连接队列」的长度。TCP 连接的两种状态:

  • 半连接队列(SYN Queue):收到 SYN,但未完成三次握手

  • 完整连接队列(Accept Queue):已完成三次握手,等待 accept() 取出

在上面的这个场景中,第二个客户端会被阻塞在「完整连接队列」中

第一步:第一个客户端连接

客户端A: SYN → 半连接队列 → 完成握手 → 完整连接队列 → accept()取出
  • 连接建立成功,服务器开始处理客户端A的请求

第二步:第二个客户端连接

客户端B: SYN → 半连接队列 → 完成握手 → 完整连接队列 ⛔ 阻塞在这里!
  • 三次握手快速完成

  • 但服务器还在处理客户端A的请求,没有调用 accept()

  • 客户端B的连接在完整连接队列中等待

关键点

  1. 半连接队列:只在三次握手期间短暂存在

  2. 完整连接队列:连接建立后,等待 accept() 的地方

  3. 单线程问题:由于服务器忙于处理第一个请求,无法及时 accept() 第二个连接

总结:第二个客户端阻塞在完整连接队列,等待服务器调用 accept() 来处理它!

3、解决方案

  • 单线程服务器无法充分利用系统资源,实际应用中很少采用这种架构。

  • 要解决这个问题,需要将服务器改造为多线程或多进程架构,实现并发处理能力。


二、多进程版TCP网络程序

        在单进程TCP服务器模型中,服务器只能以串行方式依次处理客户端连接请求。当我们将单执行流服务器升级为多进程版本后,服务器可以同时处理多个客户端连接,显著提升并发处理能力。

1、多进程服务器基本原理

进程创建与服务分配

        多进程TCP服务器的核心思想是:当服务端通过accept()函数获取到新客户端连接后,不是由当前主执行流直接处理该连接,而是调用fork()函数创建子进程,将服务任务分配给新创建的子进程。具体流程如下:

  1. 主进程(父进程)创建监听套接字并绑定端口

  2. 进入循环,持续调用accept()等待新连接

  3. 当有新连接到达时,调用fork()创建子进程

  4. 在子进程中关闭监听套接字,专注于服务当前客户端(因为子进程继承了父进程的文件描述符,所以要关闭掉)

  5. 在父进程中关闭已分配的连接套接字,继续等待新连接(让创建的子进程拿到了连接套接字文件描述符后,这个连接套接字对父进程已经没有用了,所以要关闭掉)

并发处理优势

这种模型利用了操作系统进程调度的特性:

  • 父子进程作为独立的执行流,可以同时运行在不同CPU核心上

  • 父进程无需等待子进程完成服务即可继续接受新连接

  • 每个客户端连接由独立的子进程处理,避免相互干扰

2、文件描述符继承机制

描述符表特性

文件描述符表是进程级的资源,子进程创建时会继承父进程的所有打开文件描述符:

  • 继承的描述符具有相同的文件偏移量

  • 继承的描述符具有相同的文件状态标志(如O_NONBLOCK)

  • 父子进程对描述符的操作相互独立(关闭操作除外)

        文件描述符表是进程特有的资源。当父进程创建子进程时,子进程会继承父进程的文件描述符表。例如,若父进程打开了某个文件并获得描述符3,那么子进程的3号描述符也会指向同一个文件。这种继承关系会持续传递:子进程创建的新进程同样会继承这个文件描述符。

        当父进程创建子进程后,父子进程各自保持独立性,父进程文件描述符表的变更不会影响子进程。匿名管道就是一个典型例子:父进程调用pipe函数获取两个文件描述符(分别对应管道的读写端),子进程会继承这两个文件描述符。随后,父子进程分别关闭管道的不同端(一个关闭读端,另一个关闭写端),此时各自文件描述符表的修改互不影响。这样,父子进程就能通过该管道实现单向通信。

同理,对于套接字文件,子进程也会继承父进程的套接字文件描述符。这使得子进程能够直接对特定套接字进行读写操作,从而为对应的客户端提供服务。

套接字继承示例

int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(listen_fd, ...);
listen(listen_fd, 5);

int conn_fd = accept(listen_fd, ...);
pid_t pid = fork();

if (pid == 0) { // 子进程
    close(listen_fd); // 关闭监听套接字
    // 使用conn_fd与客户端通信
} else { // 父进程
    close(conn_fd); // 关闭已分配的连接套接字
    // 继续accept新连接
}

匿名管道通信案例

int pipefd[2];
pipe(pipefd); // 创建管道

pid_t pid = fork();
if (pid == 0) { // 子进程
    close(pipefd[1]); // 关闭写端
    // 从pipefd[0]读取数据
} else { // 父进程
    close(pipefd[0]); // 关闭读端
    // 向pipefd[1]写入数据
}

这个例子展示了:

  1. 父子进程继承相同的管道描述符

  2. 各自关闭不需要的端后形成单向通信

  3. 后续的文件描述符操作互不影响

3、子进程管理策略

当父进程创建子进程后,必须等待子进程退出,否则子进程会变成僵尸进程并导致内存泄漏。因此,服务端在创建子进程后需要调用wait或waitpid函数进行等待。

关于等待方式的选择:

阻塞式等待

  • 采用阻塞方式时,服务端必须等待当前客户端服务完成后才能处理下一个连接请求

  • 这种方式本质上仍然是串行处理模式

非阻塞式等待

  • 虽然可以在子进程服务期间继续接收新连接

  • 但需要保存所有子进程的PID并持续检测其退出状态

  • 会额外消耗系统资源

综上所述,无论是阻塞式还是非阻塞式等待,都存在明显缺陷。因此,更好的解决方案是让服务端不主动等待子进程退出。

实现父进程不等待子进程退出的方法

  1. 捕获SIGCHLD信号并将其处理方式设置为忽略

  2. 采用进程链的方式:父进程创建子进程后,子进程再创建孙子进程,最终由孙子进程处理实际服务

1. 僵尸进程问题

  • 当子进程退出时,操作系统会保留其退出状态直到父进程通过wait()waitpid()回收。

  • 如果父进程未及时回收,子进程将成为僵尸进程,占用系统资源。

2. 阻塞式等待方案

while(1) {
    int conn_fd = accept(...);
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程服务代码
        exit(0);
    } else {
        waitpid(pid, NULL, 0); // 阻塞等待子进程
    }
}

缺点:父进程必须等待当前子进程退出才能接受新连接,失去并发优势。

3. 非阻塞式等待方案

// 存储所有子进程PID
pid_t pids[MAX_CLIENTS];
int pid_count = 0;

while(1) {
    int conn_fd = accept(...);
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程服务代码
        exit(0);
    } else {
        pids[pid_count++] = pid;
        // 定期检查子进程状态
        for(int i=0; i 0) {
                // 子进程已退出,移除PID
                pids[i] = pids[--pid_count];
            }
        }
    }
}

缺点

  • 需要维护子进程列表

  • 定期检查消耗CPU资源

  • 实现复杂度较高

4、优雅解决方案

1. 忽略SIGCHLD信号(捕捉SIGCHLD信号)

#include 

void sigchld_handler(int sig) {
    while(waitpid(-1, NULL, WNOHANG) > 0); // 回收所有僵尸进程
}

int main() {
    struct sigaction sa;
    sa.sa_handler = sigchld_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGCHLD, &sa, NULL);
    
    // 主服务器循环...
}

或更简单的忽略方式(但可能不完全可靠):

signal(SIGCHLD, SIG_IGN); // 设置忽略SIGCHLD

        当子进程退出时,系统会向父进程发送SIGCHLD信号。使用 signal(SIGCHLD, SIG_IGN); 之后,父进程通常不需要等待子进程,可以立即继续执行,而且不会产生僵尸进程。但如果业务逻辑需要同步,父进程仍然可以选择等待子进程。

信号处理的三层次:(重点!!!)

// 层次1:默认处理 - 信号被传递,执行默认动作
signal(SIGCHLD, SIG_DFL);  // 子进程结束会产生僵尸进程

// 层次2:忽略处理 - 信号根本不会被传递
signal(SIGCHLD, SIG_IGN);  // 内核直接回收,进程不知情

// 层次3:捕获处理 - 信号被传递,执行自定义函数
signal(SIGCHLD, custom_handler);  // 进程收到信号并处理
class TcpServer
{
public:
	void Start()
	{
        std::cout << "Server start on port: " << _port << std::endl;
		signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信号
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			pid_t id = fork();
			if (id == 0){ //child
				//处理请求
				Service(sock, client_ip, client_port);
				exit(0); //子进程提供完服务退出
			}
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
};

记得加上对应必要的头文件:

#include     // 用于 signal(), SIGCHLD, SIG_IGN
#include     // 可选,如果需要 exit() 等函数

代码测试:完成程序重新编译并启动服务端后,可通过以下监控脚本实时跟踪服务进程状态。

while :; do ps axj | head -1 && ps axj | grep TcpServer | grep -v grep;echo "##############################################################";sleep 1;done

可以看到当前服务器状态显示尚无客户端连接,仅运行着一个主服务进程。该进程持续监听新连接请求,并在接收到连接后创建子进程为对应客户端提供服务。

当客户端连接服务器时,服务进程会调用fork函数创建一个子进程,专门为该客户端提供服务。如下监控所示新加入了一个进程:

当又有一个新的客户端连接服务器时,服务进程会再创建一个新的子进程来专门处理该客户端的请求。如下监控所示又新加入一个进程:

关键在于,这两个客户端由独立的执行流提供服务,因此能够同时获得响应。它们发送至服务端的数据都能被正常接收并处理,服务端也会及时作出反馈。

当客户端逐个断开连接时,服务端对应的子进程会随之终止。然而,服务端始终会保留至少一个主服务进程,其主要职责是持续监听并处理新的连接请求。

2. 双层进程模型(推荐)

我们也可以通过服务端创建的子进程再次fork,由孙子进程为客户端提供服务,这样就无需等待孙子进程退出。

进程命名说明:

  • 爷爷进程:服务端调用listen监听客户端的进程

  • 爸爸进程:由爷爷进程fork创建的子进程

  • 孙子进程:由爸爸进程fork创建的进程,负责调用Service函数提供服务

实现机制:

  1. 爸爸进程创建孙子进程后立即退出,此时孙子进程成为孤儿进程,然后直接被1号进程给领养!!!由init进程帮忙等待和释放孙子进程!!!

  2. 也就是说,服务进程(爷爷进程)通过wait/waitpid能立即完成对爸爸进程的等待

  3. 此后服务进程可继续调用accept处理其他客户端请求

孤儿进程处理:

  • 由于爸爸进程立即退出,孙子进程成为孤儿进程

  • 系统会自动接管孤儿进程

  • 服务进程无需等待孙子进程退出

文件描述符管理:

        当服务进程(祖父进程)通过listen函数获取新连接后,会由孙子进程负责处理该连接。在此过程中,服务进程首先将文件描述符表传递给父进程,随后父进程通过fork创建孙子进程并再次传递文件描述符表。

        由于fork创建的子进程拥有独立的文件描述符表,各进程间的操作互不影响。因此,服务进程在完成fork后可以安全地关闭通过listen获取的文件描述符。同理,父进程和孙子进程无需保留从服务进程继承的监听套接字,父进程可以将其关闭。

  • 服务进程在调用fork函数后,应当及时关闭从accept函数获取的文件描述符。这是因为服务进程会持续调用accept函数创建新的服务套接字,若不及时释放不再使用的文件描述符,将导致可用文件描述符数量逐渐减少。

  • 对于父进程和子进程,建议关闭从服务进程继承的监听套接字。虽然不关闭仅会造成单个文件描述符的泄漏,但关闭仍是更稳妥的做法。这可以避免子进程在提供服务时对监听套接字进行误操作,进而影响其中的数据。

优势

  • 中间进程无需处理子进程回收,而是直接退出,让1号进程成为子进程的父进程!!!也就是交给系统!!!

  • 中间进程退出时自动清理工作进程

class TcpServer
{
public:
	void Start()
	{
        std::cout << "Server start on port: " << _port << std::endl;
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			pid_t id = fork();
			if (id == 0){ //child
				close(_listen_sock); //child关闭监听套接字
				if (fork() > 0){
					exit(0); //爸爸进程直接退出
				}
				//处理请求
				Service(sock, client_ip, client_port); //孙子进程提供服务
				exit(0); //孙子进程提供完服务退出
			}
			close(sock); //father关闭为连接提供服务的套接字
			waitpid(id, nullptr, 0); //等待爸爸进程(会立刻等待成功)
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
};

这里也要记得加上对应的头文件!!!

#include 

服务器测试:重新编译并运行客户端程序后,继续通过监控脚本对服务进程进行实时状态监测。

while :; do ps axj | head -1 && ps axj | grep TcpServer | grep -v grep;echo "##############################################################";sleep 1;done

可以看到目前没有客户端连接到服务器,仅监测到一个服务进程正在等待客户端的连接请求。

        当我们启动一个客户端并连接到服务器时,服务进程会首先创建一个父进程。这个父进程随后会立即创建一个子进程(孙子进程)来为客户端提供服务,随后父进程会自动退出(这个退出速度很快,观察不到父进程的创建和退出过程)。此时系统中仅剩两个服务进程:最初用于接收连接的主服务进程,以及实际处理客户端请求的孙子进程。值得注意的是,孙子进程的PPID显示为1,这表明它已成为孤儿进程。

当第二个客户端连接服务器时,系统会再次创建一个孤儿进程来为其提供服务。

这里思考一下:为什么两个连接的客户端在服务端上输出显示的套接字都为4呢?(重点!!!)

进程类型监听套接字 _listen_sock连接套接字 sock状态
主进程保持打开短暂拥有后关闭持续监听
爸爸进程关闭保持打开短暂存在
孙子进程关闭保持打开处理具体请求

原因分析:文件描述符重用

关键机制

  1. 文件描述符是进程资源:每个进程有自己独立的文件描述符表

  2. 最小可用FD分配:Linux总是分配当前可用的最小文件描述符编号

  3. 描述符及时关闭:在fork过程中,父进程及时关闭了连接套接字

详细过程分析

第一个客户端连接

int sock = accept(_listen_sock, ...);  // sock = 4
// 主进程:sock=4
pid_t id = fork();  // 创建爸爸进程

// 爸爸进程中:
close(_listen_sock);  // 关闭监听套接字
if (fork() > 0) {     // 创建孙子进程
    exit(0);          // 爸爸进程退出
}

// 主进程中:
close(sock);  // 关闭sock=4,现在FD 4变为可用状态
waitpid(id, nullptr, 0);  // 回收爸爸进程

第二个客户端连接

// 此时FD 4已经可用(主进程关闭了上一个sock)
int sock = accept(_listen_sock, ...);  // 再次分配sock=4

进程间的文件描述符关系

主进程: _listen_sock=3 (始终打开)
       ↓
客户端A连接: sock=4 (创建) → fork() → 爸爸进程A → fork() → 孙子进程A(持有sock=4)
       ↓ 主进程关闭sock=4 (FD 4释放)
       ↓
客户端B连接: sock=4 (重新分配) → fork() → 爸爸进程B → fork() → 孙子进程B(持有sock=4)

为什么能看到相同的sock值?(这里超级重点!!!理解超级重要!!!)

  1. 主进程视角:sock=4被重复使用

  2. 孙子进程视角:每个孙子进程都认为自己持有"sock=4",但实际上:

    • 孙子进程A:持有指向客户端A连接的描述符4

    • 孙子进程B:持有指向客户端B连接的描述符4

    • 它们在不同的进程空间中,描述符编号相同但指向不同的网络连接

可以这样理解:就是爷爷进程监听到的客户端对应的信息不同(但同一时刻只有一个监听进程,也就是只有一个监听套接字),然后得到不同的监听套接字,后面通过accept函数接收的到不同的接收套接字(套接字都是使用文件描述符来访问的),所以得到的接收套接字本质是不同的,虽然都是用4号来表示,但第一个4和第二个4是不一样的!!!

1. 监听套接字是同一个

// 整个过程中只有一个监听套接字
_listen_sock  // 始终是同一个,比如fd=3

2. 接收套接字本质不同

// 第一个客户端连接
int sock = accept(_listen_sock, ...);  // 创建连接A → 内核对象A
// 此时 sock=4 指向客户端A的连接

// 第二个客户端连接  
int sock = accept(_listen_sock, ...);  // 创建连接B → 内核对象B
// 此时 sock=4 指向客户端B的连接

3. 关键区别

  • 第一个4号:指向客户端A的连接对象

  • 第二个4号:指向客户端B的连接对象

  • 虽然编号相同,但指向的内核对象完全不同

验证方法

我们可以在Service函数中添加进程信息来验证:

void Service(int sock, const std::string& client_ip, int client_port)
{
    std::cout << "Process " << getpid() << " handling sock " << sock 
              << " for client [" << client_ip << ":" << client_port << "]" << std::endl;
    // ... 处理请求
}

然后会看到这样的输出,通过输出我们可以更加直观的看到IP相同但是端口号不同!!!所以这就对应着不同的网络连接!!!虽然文件描述符一样!!!:

重要结论

  1. 这不是bug:这是Linux文件描述符管理的正常行为

  2. 不影响功能:每个进程独立维护自己的文件描述符表

  3. 资源管理良好:主进程及时关闭不需要的描述符,避免泄漏

  4. 并发安全:不同进程中的相同FD编号互不影响

这种现象正好证明了你的服务器代码在正确工作:主进程妥善管理资源,孤儿进程独立处理客户端请求。

关键概念:文件描述符是进程私有的,每个进程有独立的文件描述符表

第一阶段:第一个客户端连接后

// 主进程
int sock = accept(_listen_sock, ...);  // 分配fd=4
pid_t id = fork();  // 创建爸爸进程

// 爸爸进程(复制了主进程的fd表)
close(_listen_sock);
if (fork() > 0) {  // 创建孙子进程A
    exit(0);
}
// 孙子进程A继续运行,持有fd=4

// 回到主进程
close(sock);  // 主进程关闭自己的fd=4

此时的状态

  • 主进程:fd=4 被标记为"可用"

  • 孙子进程A:在自己的进程空间中持有fd=4(指向客户端A的连接)

  • 两个fd=4完全独立,互不影响

第二阶段:第二个客户端连接时

// 在主进程中
int sock = accept(_listen_sock, ...);  // 再次分配fd=4

为什么可以重复分配

  1. 主进程只关心自己的文件描述符表

  2. 孙子进程A的文件描述符表对主进程不可见

  3. 内核为主进程分配fd时,只检查主进程自己的fd表

类比理解:想象每个进程有自己的"钥匙串":

  • 主进程:有钥匙3(监听)、钥匙4(已归还)

  • 孙子进程A:有钥匙4(指向客户端A的连接)

  • 当主进程需要新钥匙时,它只看自己的钥匙串,发现4号位置空着,就再次使用

内核层面的实现

实际上,文件描述符只是进程文件描述符表的索引,真正重要的是背后的struct file结构:

主进程文件描述符表
fd[3] → struct file (监听socket)
fd[4] → struct file (客户端B连接)  ← 重新分配

孙子进程A文件描述符表  
fd[4] → struct file (客户端A连接)  ← 完全独立的对象

总结根本原因:文件描述符编号是进程局部的,不是系统全局的。每个进程维护自己独立的文件描述符表,内核在分配fd时只考虑当前进程的可用情况,不考虑其他进程的使用情况。

        诶???现在又疑惑了,为什么呢?因为你看这个总结的原因,如果是这样的话,我们之前使用的管道对应的文件描述符不是可以实现进程间通信吗?这不进一步说明是像全局那样可以访问的吗?为什么又是局部的呢?其实,更准确的说法应该是:文件描述符编号是进程局部的,但文件描述符指向的内核对象可以是系统全局共享的!!!

管道的情况分析

管道的创建和共享

int pipefd[2];
pipe(pipefd);  // 创建管道
// pipefd[0]=3 (读端), pipefd[1]=4 (写端)

pid_t pid = fork();
if (pid == 0) {
    // 子进程
    // 继承了相同的文件描述符:3→读端, 4→写端
    // 但这是在fork时复制的,每个进程有自己的fd表项
}

实际的内存结构

关键区别:继承 vs 独立分配

情况1:fork继承(管道、socketpair等)

// 父子进程共享相同的文件描述符编号
pipe(fd);
fork();
// 父子进程都有相同的fd[0]和fd[1]编号
// 指向同一个管道对象

情况2:独立分配(accept、open等)

// 主进程
int sock1 = accept(...);  // fd=4
close(sock1);

// 子进程(通过fork创建)
int sock2 = accept(...);  // 也可能得到fd=4
// 但指向不同的连接对象!

更精确的技术解释

1. 文件描述符表是进程私有的

struct task_struct {
    struct files_struct *files;  // 每个进程独有
};

struct files_struct {
    struct file **fd_array;  // 该进程的fd数组
};

2. 但struct file对象可以被共享

struct file {
    atomic_t f_count;    // 引用计数
    struct inode *f_inode;  // 指向实际的文件/管道/socket
};

内核层面的真实情况

// 内核数据结构示意
struct task_struct {          // 进程控制块
    struct files_struct *files;  // 每个进程独有的文件表
};

struct files_struct {
    struct file **fd_array;   // 文件描述符指针数组
};

// 实际的内存对象
孙子进程A: fd_array[4] → struct file对象A → TCP连接A
孙子进程B: fd_array[4] → struct file对象B → TCP连接B

这代表什么?

  1. 进程隔离的成功体现:每个进程有独立的内存空间和资源视图

  2. 正确的并发处理:两个客户端请求被完全独立地处理

  3. 没有资源冲突:虽然fd编号相同,但指向不同的网络连接

正确的理解

  1. 文件描述符编号分配是进程局部的:每个进程独立决定给新打开的文件什么编号

  2. 文件描述符可以指向共享的内核对象:通过fork继承或传递fd实现

  3. 相同编号不一定指向相同对象:不同进程中相同的fd编号可能指向完全不同的资源

  4. 相同对象可能有不同编号:通过dup2()可以让不同fd指向同一个对象

所以我的服务器代码中,两个孙子进程的fd=4指向不同的连接,这正是"文件描述符编号是进程局部"的体现!而管道的情况是"内核对象可以被多个进程共享"的体现。两者并不矛盾,只是展示了文件描述符系统的不同方面。

最后再思考一个问题:那像之前我们所说的套接字究竟是文件描述符还是文件描述符编号?

套接字不是文件描述符,也不是文件描述符编号;套接字是内核中的一种对象,而文件描述符是我们访问这个对象的句柄(前面使用错误的术语,现在我们改正一下,以这个为基准,这个是标准的)

正确的术语使用

  • ❌ 错误:"这个套接字是4号"

  • ✅ 正确:"这个套接字通过4号文件描述符访问"

  • ❌ 错误:"关闭套接字"

  • ✅ 正确:"关闭套接字的文件描述符"

类比理解:就像酒店房间:

  • 套接字 = 实际的客房(有床、卫生间、设施)

  • 文件描述符 = 房卡(让你访问客房)

  • 文件描述符编号 = 房卡上的房间号"401"

你可以:

  • 有多张房卡指向同一个房间(dup2)

  • 不同酒店的401房间完全不同(不同进程的相同fd编号)

  • 退还房卡但房间还在(close fd但套接字可能还被其他进程使用)

总结

  • 套接字是内核中的网络连接对象

  • 文件描述符是进程访问该对象的句柄

  • 文件描述符编号是句柄在进程内的标识符

在你的服务器代码中,当你说"sock=4"时,你指的是:使用编号为4的文件描述符来访问某个套接字对象

        分析完上面的问题,我们接着回到刚刚的测试中,此时,两个客户端分别由独立的孤儿进程提供服务,因此能够同时处理请求。客户端发送的数据均能在服务端正常输出,服务端也会及时给予响应。

当所有客户端退出后,为其提供服务的孤儿进程也将随之终止。这些进程会被系统自动回收,最终仅保留最初建立连接的服务进程。

5、补充知识

问题1:进程退出后,曾经打开的文件会怎么办?

核心答案:当一个进程正常或异常退出时,内核会负责回收该进程所占用的几乎所有资源。这包括清理其内存空间、取消内存映射、以及最关键地——关闭所有该进程打开的文件描述符

详细解释:

  1. 自动关闭机制

    • 进程控制块(PCB,在Linux中通常是 task_struct 结构体)中维护着一个“打开文件描述符表”。

    • 当进程退出时,作为进程终止流程的一部分,内核会遍历这个表,对每一个有效的文件描述符(fd)执行 close() 系统调用。

    • 这个操作是内核级别的,是强制性的,无论进程是以何种方式退出(main 函数返回、调用 exit()、接收到致命信号等)。

  2. close() 操作的具体影响

    • 释放文件描述符:该进程的fd号码被标记为空闲,可以再次被该进程(如果它还活着)或其后代进程使用。

    • 递减内核文件对象的引用计数:每个被打开的文件在内核中都有一个对应的 struct file 对象。close(fd) 会将这个对象的引用计数减1。

    • 检查引用计数:如果引用计数减为0,意味着没有任何进程再需要这个文件对象了。此时,内核会执行最终的清理工作:

      • 最终释放这个 struct file 对象,文件被真正关闭。

      • 如果文件被修改过并且处于写缓存模式,会将最后的脏数据刷写到磁盘。

      • 调用文件系统特定的 release(或 close)方法。

  3. 一个重要的推论:文件描述符是“每进程”的

    • 进程A打开文件 test.txt,得到fd 3。

    • 进程B打开同一个文件 test.txt,会得到它自己独立的fd(可能是3,也可能是其他数字)。

    • 当进程A退出时,它只关闭自己的fd 3,这只会影响进程A自己的访问。进程B的fd仍然有效,可以继续读写文件。内核中该文件的 struct file 对象的引用计数只是从2减为了1,并未归零,所以文件不会被彻底关闭。

总结:进程退出 → 内核自动关闭其所有打开的文件描述符 → 内核文件对象引用计数减1 → 若引用计数归零,则执行最终的文件关闭和资源释放。

问题2:父进程打开文件后创建子进程,子进程能使用父进程的fd吗?

核心答案:能,绝对可以。 这正是许多进程间通信(IPC)机制,如管道、匿名FIFO等的工作原理基础。

详细解释:

  1. 继承机制

    • 当父进程调用 fork() 创建子进程时,子进程会获得一份父进程地址空间的副本

    • 这个“副本”不仅仅包括代码、数据、堆栈,还包括了进程的“打开文件描述符表”

    • 这意味着,父进程在 fork() 之前打开的每一个文件描述符,在子进程中都有一个完全相同的副本。它们指向内核中同一个 struct file 对象

  2. 关键:共享同一个内核文件对象

    • 父进程的fd 3 和 子进程的fd 3,指向的是内核中完全相同的 struct file 结构

    • 因此,它们共享:

      • 同一个文件偏移量:如果父进程通过fd 3读取了100字节,文件偏移量前进了100。那么子进程再通过它的fd 3读取时,会从第100字节之后开始读。这个特性是管道、协同处理文件等能够正常工作的关键

      • 文件的打开状态和权限(读、写、追加等)。

      • 对文件的访问权限

  3. 引用计数的变化

    • 在 fork() 之前,假设父进程打开了一个文件,内核中该文件的 struct file 引用计数为1。

    • fork() 成功后,子进程也拥有了这个fd,引用计数变为2。

    • 现在,需要父进程和子进程都关闭了这个fd,引用计数才会从2减到0,文件才会被最终关闭。

以管道为例进行验证:像之前我们提到的“管道不就是吗?”完全正确!!!让我们拆解一下管道的创建和使用过程:
#include 
#include 

int main() {
    int pipefd[2];
    pid_t pid;
    char buf;

    // 1. 父进程创建一个管道。pipefd[0]用于读,pipefd[1]用于写。
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }

    // 2. 父进程调用 fork()
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    if (pid == 0) {     /*** 子进程 ***/
        close(pipefd[1]); // 子进程关闭不用的写端

        // 子进程从 pipefd[0] (继承自父进程) 读取数据
        while (read(pipefd[0], &buf, 1) > 0) {
            write(STDOUT_FILENO, &buf, 1);
        }
        write(STDOUT_FILENO, "
", 1);
        close(pipefd[0]); // 关闭读端
        _exit(EXIT_SUCCESS);

    } else {            /*** 父进程 ***/
        close(pipefd[0]); // 父进程关闭不用的读端

        // 父进程向 pipefd[1] (自己打开的) 写入数据
        const char *msg = "Hello from parent!";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]); // 关闭写端,发送EOF给子进程

        wait(NULL); // 等待子进程退出
        exit(EXIT_SUCCESS);
    }
}

在这个例子中:

  • pipe() 在父进程上下文中创建了管道,打开了两个文件描述符 pipefd[0] 和 pipefd[1]

  • fork() 后,子进程继承了这两个fd。

  • 父子进程通过关闭不需要的fd来建立正确的单向数据流(父写子读)。

  • 它们操作的是同一个管道对象,所以父进程写入的数据,子进程可以立即读到。

需要注意的地方:
  • 竞态条件:由于文件偏移量是共享的,如果父子进程同时对一个普通文件进行写入(没有使用 O_APPEND 标志),输出内容可能会混杂在一起。需要使用同步机制(如文件锁)来避免。

  • 正确的资源管理:正因为fd是共享的,所以必须由所有持有它的进程都关闭后,资源才会释放。在管道例子中,父进程关闭写端后,子进程的读端才能看到EOF。如果某个进程忘记关闭一个fd,即使其他进程都退出了,这个文件资源也可能一直无法释放。

总结表格

特性问题1:进程退出问题2:fork() 继承
核心行为内核自动关闭所有fd子进程继承父进程的所有fd
对内核文件对象的影响引用计数减1引用计数加1
最终关闭条件该进程的关闭操作使引用计数归零需要父和子都关闭fd才能使引用计数归零
实际应用确保进程不会永久占用文件资源管道、FIFO、重定向等IPC和资源共享的基础
文件偏移量-共享,导致协同操作和竞态条件

三、多线程版TCP网络程序设计

1、进程与线程创建成本对比

在实现多执行流的服务器时,选择多线程而非多进程的主要原因在于资源开销的显著差异:

  • 进程创建成本高:创建进程需要分配独立的进程控制块(task_struct)、进程地址空间(mm_struct)、页表等核心数据结构,这些操作涉及内核大量工作。

  • 线程创建成本低:线程作为进程内的执行单元,共享进程的大部分资源(如地址空间、文件描述符表等),只需创建线程控制块(TCB)和少量私有资源,开销通常比进程小10-100倍。

因此,在需要处理大量并发连接的服务器程序中,多线程架构是更高效的选择。

2、多线程服务器基本架构

2.1 连接处理流程

  1. 主线程(服务进程)在监听套接字上调用accept()获取新连接

  2. 为每个新连接创建一个专用服务线程

  3. 新线程使用该连接的文件描述符与客户端通信

  4. 主线程继续监听新连接,实现并发处理

2.2 线程管理策略

        当服务进程通过accept函数获取新连接时,可以立即创建新线程来处理客户端请求。需要注意的是,主线程(服务进程)需要等待新线程结束,否则可能产生类似僵尸进程的问题。若希望避免主线程等待,可通过pthread_detach函数将新线程设置为分离状态。这样当线程结束时,系统会自动回收其资源。如此一来,主线程就能继续调用accept获取新连接,而由分离线程独立处理客户端请求。

  • 线程分离:通过pthread_detach()使线程退出时自动回收资源,避免僵尸线程问题

  • 资源所有权:明确各线程对文件描述符的管理职责

3、文件描述符管理

3.1 共享文件描述符表

  • 文件描述符表用于维护进程与文件之间的映射关系,每个进程都拥有自己独立的文件描述符表。

  • 当主线程创建新线程时,由于这些线程同属一个进程,它们会共享该进程的文件描述符表,而不会为每个线程单独创建新的描述符表。

  • 所有线程共享同一文件描述符表这是POSIX线程的标准行为

  • 表项包含:文件描述符编号、打开文件标志、文件偏移量指针、v节点指针(指向内核文件结构)

3.2 使用规范

        当服务进程(主线程)通过accept函数获取文件描述符后,新创建的线程可以直接访问该描述符。需要注意的是,虽然新线程能够访问主线程获取的文件描述符,但它们无法自动识别各自服务的客户端对应的描述符。因此主线程在创建新线程时,必须明确告知每个线程需要操作的套接字文件描述符,以便新线程知道应该处理哪个客户端连接。

  • 主线程职责:创建监听套接字、调用accept()接受新连接、将返回的已连接套接字传递给服务线程

  • 服务线程职责:使用传入的套接字与客户端通信、通信完成后关闭套接字、绝对不要关闭监听套接字

4、参数结构体设计

        在实际业务中,新线程通过调用Service函数为客户端提供服务,该函数需要接收三个参数:客户端套接字、IP地址和端口号。然而,pthread_create函数在创建新线程时仅支持传入单个void*类型的参数。

        为此,我们设计了Param结构体来封装这三个必要参数。主线程创建新线程时,可实例化一个Param对象,将客户端套接字、IP地址和端口号存入其中,然后将该对象的地址作为参数传递给线程执行例程。

        在线程执行例程中,只需将传入的void* 参数强制转换为Param* 类型,即可获取所需的客户端信息,进而调用Service函数完成服务处理。由于pthread_create()只能传递一个void*参数,需要设计参数封装结构:

class Param {
public:
    Param(int sock, const std::string& ip, int port)
        : _sock(sock), _ip(ip), _port(port) {}
    
    ~Param() = default;

    // 禁止拷贝
    Param(const Param&) = delete;
    Param& operator=(const Param&) = delete;

public:
    int _sock;         // 已连接套接字
    std::string _ip;   // 客户端IP
    int _port;         // 客户端端口
};

参数传递流程

  1. 主线程创建Param对象(堆上分配)

  2. 将对象指针传递给pthread_create()

  3. 新线程获取指针并转换为Param*

  4. 使用参数后释放内存

5、文件描述符共享问题

由于所有线程共享同一份文件描述符表,因此在操作文件描述符时,必须考虑其对其他线程的影响。下面的注意事项全是基于这一点来进行的:

对于主线程通过accept获取的文件描述符:

  • 主线程不应主动关闭该描述符

  • 关闭操作应由服务线程执行

  • 服务线程需在处理完客户端请求后才能关闭

对于监听套接字:

  • 服务线程无需关注监听套接字

  • 禁止服务线程关闭监听套接字描述符

  • 否则将导致主线程无法接收新连接

6、将Service函数定义为静态成员函数的原因

  • pthread_create函数要求线程执行例程必须是一个接受void* 参数并返回void* 的函数。

  • 当我们将执行例程定义在类内部时,必须将其声明为静态成员函数,否则编译器会自动添加隐藏的this指针作为第一个参数。

  • 由于线程执行例程中需要调用Service函数,而静态成员函数不能直接调用非静态成员函数,因此必须将Service函数也声明为静态成员。

  • 考虑到Service函数的实现本身并不依赖类的实例成员,只需简单地在函数声明前添加static修饰符即可满足需求。

静态成员函数要求

由于线程入口函数必须符合void* (*)(void*)的签名:

  • 必须声明为static(消除this指针隐式传递)

  • 不能直接访问非静态成员变量

  • 通过参数传递所需数据

class TcpServer
{
public:
	static void* HandlerRequest(void* arg)
	{
		pthread_detach(pthread_self()); //分离线程
		//int sock = *(int*)arg;
		Param* p = (Param*)arg;

		Service(p->_sock, p->_ip, p->_port); //线程为客户端提供服务

		delete p; //释放参数占用的堆空间
		return nullptr;
	}
	void Start()
	{
        std::cout << "Server start on port: " << _port << std::endl;
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			Param* p = new Param(sock, client_ip, client_port);
			pthread_t tid;
			pthread_create(&tid, nullptr, HandlerRequest, p);
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
};

7、关键注意事项

  • 资源泄漏防护:确保线程参数内存被释放、确保文件描述符被正确关闭

  • 错误处理:系统调用失败后的恢复策略、线程创建失败的处理

  • 线程安全:避免多个线程同时操作共享资源、对于真正的共享数据,需要使用互斥锁等同步机制

  • 性能考虑:线程创建/销毁开销、考虑使用线程池优化频繁创建线程的场景

8、扩展建议

  • 线程池模式:对于高并发场景,预先创建一组线程比动态创建更高效

  • 连接超时处理:添加定时器机制管理空闲连接

  • 优雅退出:实现服务器的正常关闭流程

  • 日志系统:完善的日志记录便于问题排查

这种多线程架构适合中等规模的并发连接(数百到数千),对于更高并发需求,可考虑I/O多路复用(如epoll)结合线程池的混合模式。

9、代码测试

        我们客户端完全不用改变,只是改变了服务端的实现方式,使用多线程来实现!!!而不是使用进程了。现在重新编译服务端代码时需要注意:由于代码使用了多线程功能,编译时必须添加-lpthread选项。

#include 

另外,由于我们需要监控的是各个线程的运行状态,此时应该使用ps -aL命令而非之前的ps -axj命令来进行监控。

while :; do ps -aL|head -1&&ps -aL|grep TcpServer;echo "##########################################";sleep 1;done

ps -aL 命令

  • 功能:显示当前用户的所有进程,并包含线程信息(LWP - Light Weight Process)

  • 各字段含义

    • PID:进程ID

    • LWP:轻量级进程ID(线程ID)

    • TTY:终端设备

    • TIME:CPU占用时间

    • CMD:命令名称

  • 使用场景:查看一个进程包含哪些线程、分析多线程程序的线程情况、查找具体的线程ID

ps -axj 命令

  • 功能:以作业格式显示所有进程的详细信息

  • 各字段含义PPID:父进程ID、PID:进程ID、PGID:进程组ID、SID:会话ID、TTY:控制终端、TPGID:前台进程组ID、STAT:进程状态、UID:用户ID、TIME:CPU时间、COMMAND:完整命令

  • 进程状态(STAT)说明S:睡眠状态、R:运行状态、D:不可中断睡眠、T:停止状态、Z:僵尸进程、s:会话领导者、<:高优先级、N:低优先级、L:有页面锁在内存中

  • 使用场景:查看进程的父子关系、分析进程组和会话信息、排查僵尸进程、查看完整的进程树结构

  • 启动服务端后,监控显示当前仅有一个服务线程运行,即主线程,此时该线程正处于等待客户端连接的状态。

当客户端与服务端建立连接后,主线程会执行以下操作:

  1. 为该客户端创建参数结构体

  2. 生成新线程

  3. 将参数结构体地址传递给新线程

  4. 新线程从参数结构体中获取必要参数

  5. 调用Service函数处理客户端请求

因此,监控系统会显示两个线程同时运行。如下所示:

        当第二个客户端发起连接请求时,主线程会重复相同流程,创建新线程为该客户端提供服务。此时服务端已运行着三个线程。可以看到连接套接字4已经被第一个客户端占了,并且这是在一个进程中的线程资源,所以下一个客户端只能占连接套接字5了。

        这两个客户端由独立的执行流提供服务,因此可以同时接收服务端响应。服务端能正常打印来自两个客户端的消息,并将回显数据准确返回给各自对应的客户端。

        每当有客户端发起连接请求时,服务端都会创建对应的新线程来处理该客户端的请求。随着客户端逐个断开连接,这些服务线程也会依次终止运行。最终,服务端将仅保留最初的主线程,继续等待新的连接请求。

10、补充:问题分析

问题1:fd线程能看到吗?

答案:1(能)

原因:

  • 在同一个进程内的所有线程共享文件描述符表

  • 文件描述符是进程级别的资源,不是线程级别的

  • 一个线程打开文件获得的fd,同一进程内的其他线程都可以使用

问题2:线程敢不敢关闭自己不需要的fd?

答案:0(不敢)

原因:

  • 文件描述符是进程共享资源,关闭一个fd会影响整个进程

  • 其他线程可能正在使用该fd,突然关闭会导致:

    • 其他线程的读写操作失败

    • 数据丢失或程序崩溃

  • 正确的做法是通过线程间通信协调fd的使用

popen/pclose 函数详解

函数原型
#include 
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

功能说明

popen():创建管道并执行命令

  • command:要执行的shell命令

  • type

    • "r":从命令读取输出(父进程读,子进程写)

    • "w":向命令写入输入(父进程写,子进程读)

执行流程
// 示例:执行 "ls -a -l -n" 并读取输出
FILE *fp = popen("ls -a -l -n", "r");
if (fp != NULL) {
    char buffer[1024];
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        // 处理输出
    }
    pclose(fp);
}
底层实现
  1. 创建管道:建立父子进程间的通信通道

  2. fork()创建子进程

    • 子进程重定向标准输入/输出到管道

    • 子进程通过exec执行命令

  3. 父进程返回FILE指针用于读写

关键知识点总结

特性说明
文件描述符共享同一进程的所有线程共享fd表
fd关闭风险线程不应随意关闭fd,会影响其他线程
popen原理管道 + fork + exec + 标准流重定向
进程间通信popen使用管道实现进程间数据传递

这样的设计确保了进程内资源的统一管理,同时也要求线程在使用共享资源时要格外小心。


四、线程池版的TCP网络程序

1、传统多线程模式的缺陷

当前多线程服务器架构存在以下问题:

线程创建与销毁开销大

  • 每当新客户端连接时,主线程都需要动态创建服务线程

  • 服务结束后立即销毁该线程

  • 这种频繁的线程创建/销毁操作效率低下

高并发场景性能瓶颈

  • 面对大量并发请求时,需要为每个客户端创建独立线程

  • 线程数量激增导致CPU调度压力骤增

  • 线程切换开销显著增加系统负担

  • 线程调度周期延长,直接影响客户端响应速度

2、解决方案

针对上述问题,我们采用以下优化策略:

线程池预创建机制

  • 服务端预先初始化一组线程资源

  • 客户端请求到达时立即分配空闲线程提供服务

  • 避免临时创建线程带来的性能开销

线程复用机制

  • 服务线程完成任务后保持活跃状态

  • 无任务时线程进入休眠等待状态

  • 新请求到达时唤醒休眠线程继续服务

资源管控机制

  • 严格控制线程池大小,避免CPU过载

  • 当所有线程繁忙时,新请求进入全连接队列等待

  • 出现空闲线程时从队列获取待处理请求继续服务

该方案通过资源预分配、循环利用和智能调度,实现了服务能力与系统负载的平衡。

3、线程池的实现与应用

为了解决服务端的性能问题,我们引入了线程池机制(之前已经实现过)。线程池的核心价值在于:

  1. 避免频繁创建和销毁线程带来的性能损耗

  2. 提高CPU内核的利用率

  3. 防止系统资源的过度调度

线程池的工作机制包含以下关键组件:

  • 任务队列:接收并存储待处理任务

  • 工作线程:默认创建5个线程持续运行

  • 任务处理流程:

    • 线程持续轮询任务队列

    • 发现任务后立即取出并执行其Run方法

    • 队列为空时线程进入休眠状态

下面使用简易版线程池来实现,如下:

#define NUM 5

//线程池
template
class ThreadPool
{
private:
	bool IsEmpty()
	{
		return _task_queue.size() == 0;
	}
	void LockQueue()
	{
		pthread_mutex_lock(&_mutex);
	}
	void UnLockQueue()
	{
	    pthread_mutex_unlock(&_mutex);
	}
	void Wait()
	{
	    pthread_cond_wait(&_cond, &_mutex);
	}
	void WakeUp()
	{
	    pthread_cond_signal(&_cond);
	}
public:
	ThreadPool(int num = NUM)
		: _thread_num(num)
	{
		pthread_mutex_init(&_mutex, nullptr);
		pthread_cond_init(&_cond, nullptr);
	}
	~ThreadPool()
	{
	    pthread_mutex_destroy(&_mutex);
	    pthread_cond_destroy(&_cond);
	}
	//线程池中线程的执行例程
	static void* Routine(void* arg)
	{
	    pthread_detach(pthread_self());
	    ThreadPool* self = (ThreadPool*)arg;
	    //不断从任务队列获取任务进行处理
		while (true){
			self->LockQueue();
			while (self->IsEmpty()){
				self->Wait();
			}
			T task;
			self->Pop(task);
			self->UnLockQueue();
			
			task.Run(); //处理任务
		}
	}
	void ThreadPoolInit()
	{
		pthread_t tid;
		for (int i = 0; i < _thread_num; i++){
			pthread_create(&tid, nullptr, Routine, this); //注意参数传入this指针
		}
	}
	//往任务队列塞任务(主线程调用)
	void Push(const T& task)
	{
	    LockQueue();
	    _task_queue.push(task);
	    UnLockQueue();
	    WakeUp();
	}
	//从任务队列获取任务(线程池中的线程调用)
	void Pop(T& task)
	{
	    task = _task_queue.front();
	    _task_queue.pop();
	}
	
private:
	std::queue _task_queue; //任务队列
	int _thread_num; //线程池中线程的数量
	pthread_mutex_t _mutex;
	pthread_cond_t _cond;
};

4、服务类新增线程池成员

当前服务端引入了线程池机制,需要在服务类中添加一个线程池指针成员:

  1. 实例化服务器对象时,线程池指针初始化为空指针

  2. 服务器初始化完成后,构造实际的线程池对象

    • 可指定线程数量参数

    • 若不指定,默认创建5个工作线程

  3. 服务器启动前初始化线程池

    • 创建工作线程

    • 线程持续监听任务队列并执行任务

工作流程:

  • 服务进程通过accept获取连接请求后

  • 根据客户端套接字、IP和端口构建任务

  • 调用线程池的Push接口将任务加入队列

这是一个典型的生产者-消费者模型:

  • 生产者:服务进程(生成任务)

  • 消费者:线程池工作线程(处理任务)

  • 交易场所:线程池的任务队列

class TcpServer
{
public:
	TcpServer(int port)
		: _listen_sock(-1)
		, _port(port)
		, _tp(nullptr)
	{}
	void InitServer()
	{
		//创建套接字
		_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listen_sock < 0){
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
		//绑定
		struct sockaddr_in local;
		memset(&local, '', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;

		if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
			std::cerr << "bind error" << std::endl;
			exit(3);
		}
		//监听
		if (listen(_listen_sock, BACKLOG) < 0){
			std::cerr << "listen error" << std::endl;
			exit(4);
		}

		_tp = new ThreadPool(); //构造线程池对象
	}
	void Start()
	{
		_tp->ThreadPoolInit(); //初始化线程池
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
			
			Task task(sock, client_ip, client_port); //构造任务
			_tp->Push(task); //将任务Push进任务队列
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
	ThreadPool* _tp; //线程池
};

5、任务类设计

        任务类需要包含以下核心属性:客户端套接字、客户端IP地址、客户端端口号。这些属性用于标识任务对应的客户端连接信息。任务类需实现Run方法,该方法将被线程池中的线程调用以执行任务处理。实际业务逻辑通过服务类的Service函数实现。

实现方案建议:

  1. 直接将Service函数移植到任务类中作为Run方法(简单但不推荐,破坏分层架构)

  2. 更优方案:在任务类中添加仿函数成员,通过回调机制执行任务处理,既保持架构清晰性,又能灵活调用Service函数。

class Task
{
public:
	Task()
	{}
	Task(int sock, std::string client_ip, int client_port)
		: _sock(sock)
		, _client_ip(client_ip)
		, _client_port(client_port)
	{}
	~Task()
	{}
	//任务处理函数
	void Run()
	{
	    _handler(_sock, _client_ip, _client_port); //调用仿函数
	}
private:
	int _sock; //套接字
	std::string _client_ip; //IP地址
	int _client_port; //端口号
	Handler _handler; //处理方法
};

注意:当任务队列中存在任务时,线程池中的线程会先创建一个Task对象,然后将该对象作为输出参数传递给任务队列的Pop函数来获取任务。因此,Task类除了需要提供带参数的构造函数外,还必须包含无参构造函数,以便创建空对象。

6、设计Handler类

接下来需要设计Handler类,在该类中重载()运算符,使其执行逻辑等同于Service函数的代码。

class Handler
{
public:
	Handler()
	{}
	~Handler()
	{}
	void operator()(int sock, std::string client_ip, int client_port)
	{
		char buffer[1024];
		while (true){
			ssize_t size = read(sock, buffer, sizeof(buffer)-1);
			if (size > 0){ //读取成功
				buffer[size] = '';
				std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;

				write(sock, buffer, size);
			}
			else if (size == 0){ //对端关闭连接
				std::cout << client_ip << ":" << client_port << " close!" << std::endl;
				break;
			}
			else{ //读取失败
				std::cerr << sock << " read error!" << std::endl;
				break;
			}
		}
		close(sock); //归还文件描述符
		std::cout << client_ip << ":" << client_port << " service done!" << std::endl;
	}
};

        我们可以让服务器处理多种任务,当前它仅执行字符串回显功能。实际的任务处理逻辑完全由任务类中的handler成员决定。要扩展服务器功能,只需修改Handler类中的运算符重载函数即可。服务器的初始化、启动和线程池管理代码都无需改动,这种设计实现了通信功能与业务逻辑的软件解耦。

7、代码测试阶段

整理一下服务端的代码,下面是完整TcpServer.cc代码(服务端):

#include 
#include 
#include 
#include     // 用于 signal(), SIGCHLD, SIG_IGN
#include     // 可选,如果需要 exit() 等函数
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define BACKLOG 5
#define NUM 5

// 前向声明
class Handler;
class Task;

//线程池
template
class ThreadPool
{
private:
	bool IsEmpty()
	{
		return _task_queue.size() == 0;
	}
	void LockQueue()
	{
		pthread_mutex_lock(&_mutex);
	}
	void UnLockQueue()
	{
	    pthread_mutex_unlock(&_mutex);
	}
	void Wait()
	{
	    pthread_cond_wait(&_cond, &_mutex);
	}
	void WakeUp()
	{
	    pthread_cond_signal(&_cond);
	}
public:
	ThreadPool(int num = NUM)
		: _thread_num(num)
	{
		pthread_mutex_init(&_mutex, nullptr);
		pthread_cond_init(&_cond, nullptr);
	}
	~ThreadPool()
	{
	    pthread_mutex_destroy(&_mutex);
	    pthread_cond_destroy(&_cond);
	}
	//线程池中线程的执行例程
	static void* Routine(void* arg)
	{
	    pthread_detach(pthread_self());
	    ThreadPool* self = (ThreadPool*)arg;
	    //不断从任务队列获取任务进行处理
		while (true){
			self->LockQueue();
			while (self->IsEmpty()){
				self->Wait();
			}
			T task;
			self->Pop(task);
			self->UnLockQueue();
			
			task.Run(); //处理任务
		}
	}
	void ThreadPoolInit()
	{
		pthread_t tid;
		for (int i = 0; i < _thread_num; i++){
			pthread_create(&tid, nullptr, Routine, this); //注意参数传入this指针
		}
	}
	//往任务队列塞任务(主线程调用)
	void Push(const T& task)
	{
	    LockQueue();
	    _task_queue.push(task);
	    UnLockQueue();
	    WakeUp();
	}
	//从任务队列获取任务(线程池中的线程调用)
	void Pop(T& task)
	{
	    task = _task_queue.front();
	    _task_queue.pop();
	}
	
private:
	std::queue _task_queue; //任务队列
	int _thread_num; //线程池中线程的数量
	pthread_mutex_t _mutex;
	pthread_cond_t _cond;
};

class Handler
{
public:
	Handler()
	{}
	~Handler()
	{}
	void operator()(int sock, std::string client_ip, int client_port)
	{
		char buffer[1024];
		while (true){
			ssize_t size = read(sock, buffer, sizeof(buffer)-1);
			if (size > 0){ //读取成功
				buffer[size] = '';
				std::cout << client_ip << ":" << client_port << "# " << buffer << std::endl;

				write(sock, buffer, size);
			}
			else if (size == 0){ //对端关闭连接
				std::cout << client_ip << ":" << client_port << " close!" << std::endl;
				break;
			}
			else{ //读取失败
				std::cerr << sock << " read error!" << std::endl;
				break;
			}
		}
		close(sock); //归还文件描述符
		std::cout << client_ip << ":" << client_port << " service done!" << std::endl;
	}
};

class Task
{
public:
	Task()
	{}
	Task(int sock, std::string client_ip, int client_port)
		: _sock(sock)
		, _client_ip(client_ip)
		, _client_port(client_port)
	{}
	~Task()
	{}
	//任务处理函数
	void Run()
	{
	    _handler(_sock, _client_ip, _client_port); //调用仿函数
	}
private:
	int _sock; //套接字
	std::string _client_ip; //IP地址
	int _client_port; //端口号
	Handler _handler; //处理方法
};

class TcpServer
{
public:
	TcpServer(int port)
		: _listen_sock(-1)
		, _port(port)
		, _tp(nullptr)
	{}
	void InitServer()
	{
		//创建套接字
		_listen_sock = socket(AF_INET, SOCK_STREAM, 0);
		if (_listen_sock < 0){
			std::cerr << "socket error" << std::endl;
			exit(2);
		}
		//绑定
		struct sockaddr_in local;
		memset(&local, '', sizeof(local));
		local.sin_family = AF_INET;
		local.sin_port = htons(_port);
		local.sin_addr.s_addr = INADDR_ANY;

		if (bind(_listen_sock, (struct sockaddr*)&local, sizeof(local)) < 0){
			std::cerr << "bind error" << std::endl;
			exit(3);
		}
		//监听
		if (listen(_listen_sock, BACKLOG) < 0){
			std::cerr << "listen error" << std::endl;
			exit(4);
		}

		_tp = new ThreadPool(); //构造线程池对象
	}
	void Start()
	{
		_tp->ThreadPoolInit(); //初始化线程池
		for (;;){
			//获取连接
			struct sockaddr_in peer;
			memset(&peer, '', sizeof(peer));
			socklen_t len = sizeof(peer);
			int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
			if (sock < 0){
				std::cerr << "accept error, continue next" << std::endl;
				continue;
			}
			std::string client_ip = inet_ntoa(peer.sin_addr);
			int client_port = ntohs(peer.sin_port);
			std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;

			Task task(sock, client_ip, client_port); //构造任务
			_tp->Push(task); //将任务Push进任务队列
		}
	}
private:
	int _listen_sock; //监听套接字
	int _port; //端口号
	ThreadPool* _tp; //线程池
};

int main() {
    TcpServer server(8081); // 创建服务器实例,监听8081端口
    server.InitServer();    // 初始化服务器
    server.Start();         // 启动服务器
    return 0;
}

现在,我们重新编译服务端代码后,可以通过以下监控脚本来查看服务端各个线程的运行状态。

while :; do ps -aL|head -1&&ps -aL|grep TcpServer;echo "##########################################";sleep 1;done

        服务端启动后,即使没有客户端连接请求,系统默认会创建6个线程。其中1个线程(第一个)负责监听新连接,其余5个(后面五个)是线程池中预分配的服务线程,用于处理后续的客户端请求。

        当客户端连接到服务器时,主线程会接收连接请求,将其封装为任务对象并放入任务队列。随后,线程池中的5个工作线程之一会从队列中取出该任务,执行对应的处理函数为客户端提供服务。

        当第二个客户端发起连接请求时,服务端会将其封装为任务并加入队列。线程池中的线程随后从队列中获取并处理该任务。由于两个客户端分别由不同的执行流提供服务,因此可以同时享受服务。

        不同于之前的情况,现在无论有多少客户端请求,服务端始终仅由线程池中的5个线程提供服务。线程数量不会随客户端连接数增加而变化,这些线程也不会因客户端断开而终止。

本文地址:https://www.yitenyun.com/5055.html

搜索文章

Tags

#ios面试 #ios弱网 #断点续传 #ios开发 #objective-c #ios #ios缓存 #服务器 #python #pip #conda #远程工作 #人工智能 #微信 #kubernetes #笔记 #平面 #容器 #linux #学习方法 #Trae #IDE #AI 原生集成开发环境 #Trae AI 香港站群服务器 多IP服务器 香港站群 站群服务器 #运维 #分阶段策略 #模型协议 #学习 #华为云 #部署上线 #动静分离 #Nginx #新人首发 #银河麒麟高级服务器操作系统安装 #银河麒麟高级服务器V11配置 #设置基础软件仓库时出错 #银河麒高级服务器系统的实操教程 #生产级部署银河麒麟服务系统教程 #Linux系统的快速上手教程 #docker #github #git #harmonyos #鸿蒙PC #gemini #gemini国内访问 #gemini api #gemini中转搭建 #Cloudflare #hadoop #hbase #hive #zookeeper #spark #kafka #flink #科技 #深度学习 #自然语言处理 #神经网络 #大数据 #职场和发展 #程序员创富 #ARM服务器 # GLM-4.6V # 多模态推理 #开源 #fastapi #html #css #Conda # 私有索引 # 包管理 #低代码 #爬虫 #音视频 #物联网 #websocket #unity #c# #游戏引擎 #进程控制 #ci/cd #jenkins #gitlab #kylin #开发语言 #云原生 #iventoy #VmWare #OpenEuler #tcp/ip #网络 #qt #C++ #langchain #数据库 #ssh #数信院生信服务器 #Rstudio #生信入门 #生信云服务器 #经验分享 #安卓 #RTP over RTSP #RTP over TCP #RTSP服务器 #RTP #TCP发送RTP #diskinfo # TensorFlow # 磁盘健康 #后端 #内网穿透 #cpolar #vscode #mobaxterm #计算机视觉 #aws #云计算 #word #umeditor粘贴word #ueditor粘贴word #ueditor复制word #ueditor上传word图片 #node.js #MobaXterm #ubuntu #儿童书籍 #儿童诗歌 #童话故事 #经典好书 #儿童文学 #好书推荐 #经典文学作品 #语言模型 #大模型 #ai #ai大模型 #agent #arm #分布式 #华为 #sql #AIGC #agi #android #腾讯云 #自动化 #ansible #http #项目 #高并发 #flutter #java-ee #文心一言 #AI智能体 #ide #区块链 #测试用例 #生活 #前端 #nginx #serverless #FTP服务器 #flask #Dell #PowerEdge620 #内存 #硬盘 #RAID5 #windows #企业开发 #ERP #项目实践 #.NET开发 #C#编程 #编程与数学 #飞牛nas #fnos #缓存 #pytorch #Harbor #PyCharm # 远程调试 # YOLOFuse #mcu #java #jar #Reactor #javascript #vue上传解决方案 #vue断点续传 #vue分片上传下载 #vue分块上传下载 #mysql #多个客户端访问 #IO多路复用 #回显服务器 #TCP相关API #PyTorch #模型训练 #星图GPU #AI编程 #centos #svn #程序人生 #科研 #博士 #pycharm #jmeter #功能测试 #软件测试 #自动化测试 #鸿蒙 #网络协议 #c++ #数学建模 #2026年美赛C题代码 #2026年美赛 #uni-app #小程序 #notepad++ #安全 #内存治理 #django #iBMC #UltraISO #信息与通信 #算法 #散列表 #哈希算法 #数据结构 #leetcode #课程设计 #spring boot #架构 #牛客周赛 #dify #计算机网络 #spring cloud #spring #vue.js #json #golang #redis #个人开发 #蓝桥杯 #php #mvp #设计模式 #游戏 #京东云 #性能优化 #DeepSeek #服务器繁忙 #AI #rocketmq #阻塞队列 #生产者消费者模型 #服务器崩坏原因 #jvm #数据仓库 #vllm #Streamlit #Qwen #本地部署 #AI聊天机器人 #矩阵 #线性代数 #AI运算 #向量 #LLM #鸭科夫 #逃离鸭科夫 #鸭科夫联机 #鸭科夫异地联机 #开服 #vim #gcc #yum #mmap #nio #prometheus #ecmascript #elementui #开源软件 #我的世界 #Ansible # 自动化部署 # VibeThinker #Ubuntu服务器 #硬盘扩容 #命令行操作 #VMware #web安全 #web #webdav #网络安全 #风控模型 #决策盲区 #AI论文写作工具 #学术论文创作 #论文效率提升 #MBA论文写作 #es安装 #单片机 #stm32 #嵌入式硬件 #需求分析 #信息可视化 #claude code #codex #code cli #ccusage #数据集 #Ascend #MindIE #阿里云 #语音识别 #rabbitmq #protobuf #ModelEngine #DisM++ # 系统维护 #gpu算力 #设备驱动 #芯片资料 #网卡 #jetty #全能视频处理软件 #视频裁剪工具 #视频合并工具 #视频压缩工具 #视频字幕提取 #视频处理工具 #udp #企业微信 #Android #Bluedroid #智能手机 #everything #流程图 #论文阅读 #论文笔记 #毕业设计 #c语言 #MCP #MCP服务器 #SSH反向隧道 # Miniconda # Jupyter远程访问 #VS Code调试配置 #DS随心转 #asp.net #1024程序员节 #epoll #高级IO #adb #面试 #LoRA # RTX 3090 # lora-scripts #react.js #svm #amdgpu #kfd #ROCm #ddos #fiddler #压力测试 #microsoft #测试工具 #opencv #数据挖掘 #googlecloud #里氏替换原则 #银河麒麟 #系统升级 #信创 #国产化 #chatgpt #守护进程 #复用 #screen #n8n #arm开发 #Modbus-TCP #sizeof和strlen区别 #sizeof #strlen #计算数据类型字节数 #计算字符串长度 #编辑器 #金融 #mcp #金融投资Agent #Agent #正则 #正则表达式 #智能路由器 #AI写作 #研发管理 #禅道 #禅道云端部署 #iphone #中间件 #凤希AI伴侣 #深度优先 #DFS #fabric #postgresql #RAID #RAID技术 #磁盘 #存储 #流量监控 #架构师 #系统架构 #软考 #系统架构师 #unity3d #服务器框架 #Fantasy #elasticsearch #大模型学习 #transformer #MC #链表 #链表的销毁 #链表的排序 #链表倒置 #判断链表是否有环 #机器学习 #电脑 #grafana #todesk #java大文件上传 #java大文件秒传 #java大文件上传下载 #java文件传输解决方案 #asp.net大文件上传 #asp.net大文件上传下载 #asp.net大文件上传源码 #ASP.NET断点续传 #asp.net上传文件夹 #journalctl #openresty #lua #wordpress #雨云 #LobeChat #vLLM #GPU加速 #ping通服务器 #读不了内网数据库 #bug菌问答团队 #若依 #quartz #框架 #SMTP # 内容安全 # Qwen3Guard #数码相机 #openlayers #bmap #tile #server #vue #SSH #X11转发 #Miniconda #debian #改行学it #创业创新 #Coze工作流 #AI Agent指挥官 #多智能体系统 #openEuler #Hadoop #TCP #客户端 #嵌入式 #DIY机器人工房 #SSH Agent Forwarding # PyTorch # 容器化 #homelab #Lattepanda #Jellyfin #Plex #Emby #Kodi #autosar #nacos #银河麒麟aarch64 #uvicorn #uvloop #asgi #event #.net #大模型入门 #AI大模型 #信令服务器 #Janus #MediaSoup #tdengine #时序数据库 #制造 #涛思数据 #TensorRT # Triton # 推理优化 #zabbix #tensorflow #Jetty # CosyVoice3 # 嵌入式服务器 #claude #推荐算法 #log #YOLO #建筑缺陷 #红外 #scala #sqlserver #密码学 #进程 #apache #远程桌面 #远程控制 #信号处理 #目标跟踪 #银河麒麟操作系统 #openssh #华为交换机 #信创终端 #ssl #智能一卡通 #门禁一卡通 #梯控一卡通 #电梯一卡通 #消费一卡通 #一卡通 #考勤一卡通 #ms-swift # 一锤定音 # 大模型微调 #deepseek #risc-v #bash #求职招聘 #cpp #SSH公钥认证 # 安全加固 #ffmpeg #PowerBI #企业 #版本控制 #Git入门 #开发工具 #代码托管 #Qwen3-14B # 大模型部署 # 私有化AI #数模美赛 #matlab #screen 命令 #macos #个人博客 #vp9 #幼儿园 #园长 #幼教 #支付 #nas #fpga开发 #LVDS #高速ADC #DDR #驱动开发 #嵌入式编译 #ccache #distcc #azure #ida #迁移重构 #数据安全 #漏洞 #代码迁移 #目标检测 #蓝耘智算 #oracle #ONLYOFFICE #MCP 服务器 #SA-PEKS # 关键词猜测攻击 # 盲签名 # 限速机制 #模版 #函数 #类 #笔试 #tomcat #树莓派4b安装系统 #前端框架 #毕设 #WEB #STUN # TURN # NAT穿透 #我的世界服务器搭建 #minecraft #laravel #cursor #Playbook #AI服务器 #shell #CPU利用率 #simulink #操作系统 #进程创建与终止 #数组 #ollama #llm #流媒体 #NAS #飞牛NAS #监控 #NVR #EasyNVR #RustDesk #IndexTTS 2.0 #本地化部署 #几何学 #拓扑学 #tcpdump #embedding #AB包 #visual studio code #负载均衡 #ESXi #车辆排放 #Linux #线程 #线程池 #Shiro #反序列化漏洞 #CVE-2016-4437 #vuejs #paddleocr #运营 #生信 #eBPF #Spring AI #STDIO协议 #Streamable-HTTP #McpTool注解 #服务器能力 #pencil #pencil.dev #设计 #搜索引擎 #产品经理 #ui #团队开发 #墨刀 #figma #酒店客房管理系统 #论文 #sqlite #智慧校园解决方案 #智慧校园一体化平台 #智慧校园选型 #智慧校园采购 #智慧校园软件 #智慧校园专项资金 #智慧校园定制开发 #Triton # CUDA #selenium #webrtc #RAG #全链路优化 #实战教程 #wsl #HeyGem # 远程访问 # 服务器IP配置 #MS #Materials #SSH保活 #远程开发 #结构与算法 #Windows 更新 #ComfyUI # 推理服务器 #libosinfo #Android16 #音频性能实战 #音频进阶 #模拟退火算法 #虚拟机 #maven #扩展屏应用开发 #android runtime #CTF #微信小程序 #TLS协议 #HTTPS #漏洞修复 #运维安全 #SSE # AI翻译机 # 实时翻译 #钉钉 #机器人 #yolov12 #研究生life #文件传输 #电脑文件传输 #电脑传输文件 #电脑怎么传输文件到另一台电脑 #电脑传输文件到另一台电脑 # IndexTTS 2.0 # 远程运维 #性能 #优化 #RAM #无人机 #Deepoc #具身模型 #开发板 #未来 #mongodb #私有化部署 #windows11 #系统修复 #r-tree #聊天小程序 #NFC #智能公交 #服务器计费 #FP-增长 #其他 #RXT4090显卡 #RTX4090 #深度学习服务器 #硬件选型 #Proxmox VE #虚拟化 #neo4j #NoSQL #SQL #交互 #idm #GPU服务器 #8U #硬件架构 #NPU #CANN #万悟 #联通元景 #智能体 #镜像 #intellij-idea #idea #intellij idea #cnn #浏览器自动化 #python #cosmic #重构 #健身房预约系统 #健身房管理系统 #健身管理系统 #零售 #clickhouse #SSH免密登录 #代理 #集成测试 #微服务 #H5 #跨域 #发布上线后跨域报错 #请求接口跨域问题解决 #跨域请求代理配置 #request浏览器跨域 #gateway #Comate #scrapy #游戏机 #eclipse #servlet #JumpServer #堡垒机 #arm64 #UDP的API使用 #北京百思可瑞教育 #百思可瑞教育 #北京百思教育 #处理器 #select #上下文工程 #langgraph #意图识别 #ESP32 #传感器 #Python #MicroPython #RK3576 #瑞芯微 #硬件设计 #机器视觉 #6D位姿 #振镜 #振镜焊接 #UOS #海光K100 #统信 #teamviewer #数据采集 #浏览器指纹 #wpf #串口服务器 #Modbus #MOXA #CUDA #jupyter #Socket网络编程 # 目标检测 #pdf #edge #迭代器模式 #观察者模式 #硬件 #Fun-ASR # 语音识别 # WebUI #部署 #昇腾300I DUO #大模型教程 #UDP套接字编程 #UDP协议 #网络测试 #c++20 #SRS #直播 #mybatis #milvus #springboot #知识库 #web server #请求处理流程 #Host #渗透测试 #SSRF #运维开发 #opc ua #opc #chrome #程序员 #测试流程 #金融项目实战 #P2P #黑群晖 #无U盘 #纯小白 #政务 #音乐分类 #音频分析 #ViT模型 #Gradio应用 #指针 #鼠大侠网络验证系统源码 #单元测试 #SSH跳板机 # Python3.11 #东方仙盟 # GLM-TTS # 数据安全 #API限流 # 频率限制 # 令牌桶算法 #ip #游戏私服 #云服务器 #分类 #Gunicorn #WSGI #Flask #并发模型 #容器化 #性能调优 #powerbi #蓝湖 #Axure原型发布 #Clawdbot #个人助理 #数字员工 #状态模式 #mcp server #AI实战 #LabVIEW知识 #LabVIEW程序 #LabVIEW功能 #labview #llama #ceph #ambari #turn #ai编程 #微PE # GLM # 服务连通性 #源码 #闲置物品交易系统 #IPv6 #DNS #muduo库 #uv #uvx #uv pip #npx #Ruff #pytest #C# # REST API # GLM-4.6V-Flash-WEB #数据恢复 #视频恢复 #视频修复 #RAID5恢复 #流媒体服务器恢复 #910B #昇腾 #restful #ajax #视频去字幕 #flume #C语言 #vivado license #html5 #UDP #RSO #机器人操作系统 #prompt #glibc #Anaconda配置云虚拟环境 #YOLOv8 # Docker镜像 #MQTT协议 #openHiTLS #TLCP #DTLCP #商用密码算法 #esp32教程 #计算机 #OPCUA #集成学习 #https #pandas #matplotlib #mamba #可信计算技术 #winscp #双指针 # 双因素认证 #OSS #firefox #CMake #Make #C/C++ #rust #rustdesk #p2p #vps #连接数据库报错 #Docker #算力一体机 #ai算力服务器 #硬件工程 #青少年编程 # 高并发部署 #自动驾驶 #YOLOFuse # Base64编码 # 多模态检测 #Canal #SPA #单页应用 #webpack #web3.py #系统安全 #ipmitool #BMC #学术写作辅助 #论文创作效率提升 #AI写论文实测 #C #JAVA #Java #bootstrap #麒麟OS #swagger #IndexTTS2 # 阿里云安骑士 # 木马查杀 #reactjs #web3 #mariadb #Dify #ARM架构 #鲲鹏 #产品运营 #1panel #vmware #策略模式 #LangGraph #CLI #JavaScript #langgraph.json #说话人验证 #声纹识别 #CAM++ #Anything-LLM #IDC服务器 #ssm #5G #汇编 #raid #raid阵列 #typescript #npm #PTP_1588 #gPTP #模型上下文协议 #MultiServerMCPC #load_mcp_tools #load_mcp_prompt #电气工程 #PLC #gitea #database #Windows #海外服务器安装宝塔面板 #翻译 #开源工具 #边缘计算 #结构体 #TCP服务器 #开发实战 #rdp #能源 #网站 #截图工具 #批量处理图片 #图片格式转换 #图片裁剪 #多线程 #性能调优策略 #双锁实现细节 #动态分配节点内存 #esp32 arduino #markdown #建站 #HistoryServer #Spark #YARN #jobhistory #游戏美术 #技术美术 #游戏策划 #游戏程序 #用户体验 #大模型部署 #mindie #大模型推理 #业界资讯 #n8n解惑 #可撤销IBE #服务器辅助 #私钥更新 #安全性证明 #双线性Diffie-Hellman #区间dp #贪心算法 #二进制枚举 #图论 #L2C #勒让德到切比雪夫 #大模型开发 #elk #简单数论 #埃氏筛法 #平板 #交通物流 #智能硬件 #H5网页 #网页白屏 #H5页面空白 #资源加载问题 #打包部署后网页打不开 #HBuilderX #A2A #GenAI #abtest #内存接口 # 澜起科技 # 服务器主板 #VMWare Tool #数据分析 #ue5 # 显卡驱动备份 #心理健康服务平台 #心理健康系统 #心理服务平台 #心理健康小程序 #eureka #vue3 #天地图 #403 Forbidden #天地图403错误 #服务器403问题 #天地图API #部署报错 #广播 #组播 #并发服务器 #x86_64 #数字人系统 #插件 #VibeVoice # 语音合成 #企业存储 #RustFS #对象存储 #高可用 #TFTP #三维 #3D #三维重建 #NSP #下一状态预测 #aigc #文生视频 #CogVideoX #AI部署 #考研 #软件工程 #rtsp #转发 #DAG # ProxyJump # 跳板机 #服务器解析漏洞 #dubbo #海外短剧 #海外短剧app开发 #海外短剧系统开发 #短剧APP #短剧APP开发 #短剧系统开发 #海外短剧项目 #log4j #kmeans #聚类 #CVE-2025-61686 #路径遍历高危漏洞 #数字孪生 #三维可视化 #IntelliJ IDEA #Spring Boot #Llama-Factory # 大模型推理 # 代理转发 #随机森林 #echarts #经济学 #网路编程 #百万并发 # 服务器IP # 端口7860 #WinDbg #Windows调试 #内存转储分析 # 公钥认证 #逻辑回归 #计组 #数电 #Node.js #漏洞检测 #CVE-2025-27210 # GPU租赁 # 自建服务器 # 云服务器 #cascadeur #设计师 #web服务器 #Xshell #Finalshell #生物信息学 #组学 #memcache #MinIO服务器启动与配置详解 #大剑师 #nodejs面试题 #C2000 #TI #实时控制MCU #AI服务器电源 # 树莓派 # ARM架构 #OBC #遛狗 #DHCP #C++ UA Server #SDK #跨平台开发 #ranger #MySQL8.0 #统信UOS #服务器操作系统 #win10 #qemu #SSH复用 # 远程开发 #磁盘配额 #存储管理 #文件服务器 #形考作业 #国家开放大学 #系统运维 #ngrok #视觉检测 #visual studio #大语言模型 #长文本理解 #glm-4 #推理部署 # Connection refused #智能体来了 #智能体对传统行业冲击 #行业转型 #AI赋能 #注入漏洞 #nvidia #GATT服务器 #蓝牙低功耗 #iot #智能家居 #safari #Nacos #gRPC #注册中心 #Rust #Tokio #异步编程 #系统编程 #Pin #http服务器 #win11 # ControlMaster #chat #twitter #CosyVoice3 # IP配置 # 0.0.0.0 #智慧城市 #galeweather.cn #高精度天气预报数据 #光伏功率预测 #风电功率预测 #高精度气象 #memory mcp #Cursor #线性回归 #c #YOLO26 #muduo #TcpServer #accept #高并发服务器 #贴图 #材质 #vnstat #postman # 远程连接 #勒索病毒 #勒索软件 #加密算法 #.bixi勒索病毒 #数据加密 #超算服务器 #算力 #高性能计算 #仿真分析工作站 #LangFlow # 轻量化镜像 # 边缘计算 #Buck #NVIDIA #交错并联 #DGX #实时音视频 #知识 #WinSCP 下载安装教程 #SFTP #FTP工具 #服务器文件传输 #excel #copilot #硬盘克隆 #DiskGenius #媒体 #论文复现 #AutoDL #攻防演练 #Java web #红队 #手机h5网页浏览器 #安卓app #苹果ios APP #手机电脑开启摄像头并排查 #语音生成 #TTS #IO #anaconda #虚拟环境 #whisper #GB28181 #SIP信令 #SpringBoot #视频监控 #WT-2026-0001 #QVD-2026-4572 #smartermail #hibernate #ArkUI #ArkTS #鸿蒙开发 #TTS私有化 # IndexTTS # 音色克隆 #存储维护 #screen命令 #AI技术 #系统管理 #服务 #go #AITechLab #cpp-python #CUDA版本 #连锁药店 #连锁店 #puppeteer #KMS #slmgr #源代码管理 #宝塔面板部署RustDesk #RustDesk远程控制手机 #手机远程控制 #黑客技术 #网安应急响应 #管道Pipe #system V #ARM64 # DDColor # ComfyUI #Ubuntu #ESP32编译服务器 #Ping #DNS域名解析 # keep-alive #Fluentd #Sonic #日志采集 #POC #问答 #交付 #面向对象 #xlwings #Excel #SAP #ebs #metaerp #oracle ebs # 高并发 #SSH跳转 #Chat平台 #流量运营 #用户运营 # GPU集群 #框架搭建 #自由表达演说平台 #演说 #AI-native #dba #nfs #iscsi #国产化OS #clamav #文件管理 #国产开源制品管理工具 #Hadess #一文上手 #CVE-2025-68143 #CVE-2025-68144 #CVE-2025-68145 #范式 #weston #x11 #x11显示服务器 #计算几何 #斜率 #方向归一化 #叉积 #samba # 批量管理 #Karalon #AI Test #ASR #SenseVoice #命令模式 #环境搭建 #scanf #printf #getchar #putchar #cin #cout #图像处理 #yolo #ET模式 #非阻塞 #多模态 #微调 #超参 #LLamafactory #高品质会员管理系统 #收银系统 #同城配送 #最好用的电商系统 #最好用的系统 #推荐的前十系统 #JAVA PHP 小程序 #JNI #CPU #测评 #CCE #Dify-LLM #Flexus # 数字人系统 # 远程部署 #ipv6 #duckdb #cesium #可视化 # 硬件配置 #排序算法 #jdk #排序 #Linux多线程 #SMP(软件制作平台) #EOM(企业经营模型) #应用系统 #CSDN #spine #TRO #TRO侵权 #TRO和解 #aiohttp #asyncio #异步 #运维工具 #软件 #本地生活 #电商系统 #商城 #Discord机器人 #云部署 #程序那些事 #移动端h5网页 #调用浏览器摄像头并拍照 #开启摄像头权限 #拍照后查看与上传服务器端 #摄像头黑屏打不开问题 #.netcore #tornado # 模型微调 #服务器IO模型 #非阻塞轮询模型 #多任务并发模型 #异步信号模型 #多路复用模型 # 黑屏模式 # TTS服务器 #领域驱动 #项目申报系统 #项目申报管理 #项目申报 #企业项目申报 #文件IO #输入输出流 #Aluminium #Google # 大模型 # 模型训练 #Go并发 #高并发架构 #Goroutine #系统设计 #Tracker 服务器 #响应最快 #torrent 下载 #2026年 #Aria2 可用 #迅雷可用 #BT工具通用 #net core #kestrel #web-server #asp.net-core #Zabbix #语音合成 #FASTMCP #人脸识别 #人脸核身 #活体检测 #身份认证与人脸对比 #微信公众号 #企业级存储 #网络设备 #Smokeping #pve #联机教程 #局域网联机 #局域网联机教程 #局域网游戏 #EMC存储 #NetApp存储 #zotero #WebDAV #同步失败 #代理模式 #工具集 #大模型应用 #API调用 #PyInstaller打包运行 #服务端部署 #asp.net上传大文件 #土地承包延包 #领码SPARK #aPaaS+iPaaS #数字化转型 #智能审核 #档案数字化 #农产品物流管理 #物流管理系统 #农产品物流系统 #农产品物流 #xss #欧拉 #编程 #c++高并发 #Termux #Samba #SSH别名 #Langchain-Chatchat # 国产化服务器 # 信创 #麒麟 #信创国产化 #达梦数据库 # 水冷服务器 # 风冷服务器 # 自动化运维 #VoxCPM-1.5-TTS # 云端GPU # PyCharm宕机 #ShaderGraph #图形 #uip #儿童AI #图像生成 #pjsip #VMware Workstation16 #openclaw #AI生成 # outputs目录 # 自动化 #GPU ##租显卡 #进程等待 #wait #waitpid #全文检索 #2026AI元年 #年度趋势 #国产PLM #瑞华丽PLM #瑞华丽 #PLM #短剧 #短剧小程序 #短剧系统 #微剧 #nosql #ZooKeeper #ZooKeeper面试题 #面试宝典 #深入解析 #人形机器人 #人机交互 #汽车 #文件上传漏洞 #编程助手 #xml #Kylin-Server #国产操作系统 #服务器安装 #Kuikly #openharmony #门禁 #梯控 #智能梯控 #电梯 #电梯运力 #电梯门禁 #计算机毕业设计 #程序定制 #毕设代做 #课设 #vncdotool #链接VNC服务器 #如何隐藏光标 #HBA卡 #RAID卡 #DDD #tdd #大学生 #大作业 #FHSS #bond #服务器链路聚合 #网卡绑定 #CNAS #CMA #程序文件 # GPU服务器 # tmux #程序开发 #程序设计 #wireshark #网络安全大赛 #插入排序 #outlook #错误代码2603 #无网络连接 #2603 #算力建设 #gpu #nvcc #cuda #性能测试 #LoadRunner #智能制造 #供应链管理 #工业工程 #库存管理 #nodejs #云服务器选购 #Saas #具身智能 #RK3588 #RK3588J #评估板 #核心板 #嵌入式开发 #SSH密钥 #模块 #练习 #基础练习 #循环 #九九乘法表 #计算机实现 #VSCode # Qwen3Guard-Gen-8B #dynadot #域名 #ETL管道 #向量存储 #数据预处理 #DocumentReader #工厂模式 #群晖 #音乐 #esb接口 #走处理类报异常 #SQL注入主机 #树莓派 #N8N #Coturn #TURN #smtp #smtp服务器 #PHP #银河麒麟部署 #银河麒麟部署文档 #银河麒麟linux #银河麒麟linux部署教程 #声源定位 #MUSIC #SMARC #ARM #ThingsBoard MCP #AI+ #coze #AI入门 # 智能运维 # 性能瓶颈分析 #空间计算 #原型模式 #devops #resnet50 #分类识别训练 #PyTorch 特性 #动态计算图 #张量(Tensor) #自动求导Autograd #GPU 加速 #生态系统与社区支持 #与其他框架的对比 #戴尔服务器 #戴尔730 #装系统 #OpenManage #junit #AI视频创作系统 #AI视频创作 #AI创作系统 #AI视频生成 #AI工具 #AI创作工具 #fs7TF #bug #AI 推理 #NV #Spire.Office #npu #隐私合规 #网络安全保险 #法律风险 #风险管理 #React #Next #CVE-2025-55182 #RSC #ServBay #数据访问 # 服务器IP访问 # 端口映射 # 网络延迟 #远程软件 # OTA升级 # 黄山派 #内网 # IndexTTS2 #静脉曲张 #腿部健康 #ansys #ansys问题解决办法 #自动化运维 #lucene #分布式数据库 #集中式数据库 #业务需求 #选型误 #3d #WRF #WRFDA #公共MQTT服务器 #mssql #HarmonyOS #代理服务器 #Apple AI #Apple 人工智能 #FoundationModel #Summarize #SwiftUI #b树 #跳槽 #工作 #sql注入 #机器人学习 #雨云服务器 #Minecraft服务器 #教程 #MCSM面板 #路由器 #windbg分析蓝屏教程 #le audio #蓝牙 #低功耗音频 #通信 #连接 # 服务器配置 # GPU #appche #视觉理解 #Moondream2 #多模态AI #CA证书 #CS336 #Assignment #Experiments #TinyStories #Ablation #ICPC #量子计算 # 批量部署 #JT/T808 #车联网 #车载终端 #模拟器 #仿真器 #开发测试 #星际航行 # 键鼠锁定 #agentic bi #opc模拟服务器 #远程连接 #安全架构 #工程设计 #预混 #扩散 #燃烧知识 #层流 #湍流 #娱乐 #敏捷流程 #报表制作 #职场 #数据可视化 #用数据讲故事 #Keycloak #Quarkus #AI编程需求分析 #AI赋能盾构隧道巡检 #开启基建安全新篇章 #以注意力为核心 #YOLOv12 #AI隧道盾构场景 #盾构管壁缺陷病害异常检测预警 #隧道病害缺陷检测 #ARMv8 #内存模型 #内存屏障 #服务器线程 # SSL通信 # 动态结构体 #RWK35xx #语音流 #实时传输 #node #参数估计 #矩估计 #概率论 #lvs # ARM服务器 #canvas层级太高 #canvas遮挡问题 #盖住其他元素 #苹果ios手机 #安卓手机 #调整画布层级 #学术生涯规划 #CCF目录 #基金申请 #职称评定 #论文发表 #科研评价 #顶会顶刊 #LE Audio #BAP #视频 # child_process #超时设置 #客户端/服务器 #网络编程 #挖矿 #Linux病毒 #节日 #麦克风权限 #访问麦克风并录制音频 #麦克风录制音频后在线播放 #用户拒绝访问麦克风权限怎么办 #uniapp 安卓 苹果ios #将音频保存本地或上传服务器 #YOLO11 #安全威胁分析 #仙盟创梦IDE #GLM-4.6V-Flash-WEB # AI视觉 # 本地部署 #基础语法 #标识符 #常量与变量 #数据类型 #运算符与表达式 #地理 #遥感 #动态规划 #taro #AI应用编程 #dlms #dlms协议 #逻辑设备 #逻辑设置间权限 #scikit-learn #react native #主板 #总体设计 #电源树 #框图 #Minecraft #PaperMC #我的世界服务器 #前端开发 #EN4FE #Gateway #认证服务器集成详解 #服务器开启 TLS v1.2 #IISCrypto 使用教程 #TLS 协议配置 #IIS 安全设置 #服务器运维工具 #uniapp #合法域名校验出错 #服务器域名配置不生效 #request域名配置 #已经配置好了但还是报错 #uniapp微信小程序 #Archcraft #Claude #华为od #华为机试 #Linly-Talker # 数字人 # 服务器稳定性 #kong #Kong Audio #Kong Audio3 #KongAudio3 #空音3 #空音 #中国民乐 #零代码平台 #AI开发 #mtgsig #美团医药 #美团医药mtgsig #美团医药mtgsig1.2 #传统行业 #后端框架 #小艺 #搜索 #电子电气架构 #系统工程与系统架构的内涵 #Routine #证书 #gnu #工程实践 #就业 #百度 #ueditor导入word #MCP服务器注解 #异步支持 #方法筛选 #声明式编程 #自动筛选机制 #图像识别 #Java程序员 #Java面试 #后端开发 #Spring源码 #Spring #V11 #kylinos #导航网 #KMS激活 #gpt #API #sentinel #wps #寄存器 #网络攻击模型 #composer #symfony #java-zookeeper #pyqt #poll #coffeescript #r语言 #STDIO传输 #SSE传输 #WebMVC #WebFlux #个性化推荐 #BERT模型 #FaceFusion # Token调度 # 显存优化 #实体经济 #商业模式 #软件开发 #数智红包 #商业变革 #创业干货 #语义搜索 #嵌入模型 #Qwen3 #AI推理 #工业级串口服务器 #串口转以太网 #串口设备联网通讯模块 #串口服务器选型 #入侵 #日志排查 #挖漏洞 #攻击溯源 #blender #warp #因果学习 #RPA #影刀RPA #AI办公 #tcp/ip #网络 #人大金仓 #Kingbase #Spring AOP #Prometheus #交换机 #三层交换机 #多进程 #python技巧 #高斯溅射 #React安全 #漏洞分析 #Next.js #Puppet # TTS #租显卡 #训练推理 #云开发 #个人电脑 #AI智能棋盘 #Rock Pi S #高仿永硕E盘的个人网盘系统源码 #MC群组服务器 #VPS #搭建 #递归 #线性dp #漏洞挖掘 #numpy #unix #webgl #支持向量机 #CS2 #debian13 #BoringSSL # 权限修复 #ICE #Syslog #系统日志 #日志分析 #日志监控 #文本生成 #CPU推理 #Autodl私有云 #深度服务器配置 # 鲲鹏 #http头信息 # SSH #k8s #音诺ai翻译机 #AI翻译机 # Ampere Altra Max #sklearn #stl #IIS Crypto #温湿度监控 #WhatsApp通知 #IoT #MySQL # HiChatBox # 离线AI #人脸识别sdk #视频编解码 #统信操作系统 #决策树 #sglang #题解 #图 #dijkstra #迪杰斯特拉 #开关电源 #热敏电阻 #PTC热敏电阻 #数据报系统 #idc # 服务器迁移 # 回滚方案 #效率神器 #办公技巧 #自动化工具 #Windows技巧 #打工人必备 #实时检测 #卷积神经网络 #HarmonyOS APP #旅游 #AI电商客服 #spring ai #oauth2 #Cpolar #国庆假期 #服务器告警 #rtmp #dreamweaver #晶振 #hdfs #华为od机试 #华为od机考 #华为od最新上机考试题库 #华为OD题库 #华为OD机试双机位C卷 #od机考题库 #ROS # 局域网访问 # 批量处理 #运维 # 高温监控 #I/O模型 #并发 #水平触发、边缘触发 #多路复用 #Python3.11 #AI工具集成 #容器化部署 #分布式架构 #快递盒检测检测系统 #ue4 #DedicatedServer #独立服务器 #专用服务器 #FL Studio #FLStudio #FL Studio2025 #FL Studio2026 #FL Studio25 #FL Studio26 #水果软件 #智能电视 #clawdbot #vertx #vert.x #vertx4 #runOnContext #CMC #Matrox MIL #二次开发 #rsync # 数据同步 #密码 #网络配置实战 #Web/FTP 服务访问 #计算机网络实验 #外网访问内网服务器 #Cisco 路由器配置 #静态端口映射 #网络运维 #防火墙 #claudeCode #content7 #0day漏洞 #DDoS攻击 #漏洞排查 #单例模式 #懒汉式 #恶汉式 #odoo # 串口服务器 # NPort5630 #nmodbus4类库使用教程 #docker-compose #IFix #ftp #sftp #YOLO识别 #YOLO环境搭建Windows #YOLO环境搭建Ubuntu #OpenHarmony #Python办公自动化 #Python办公 #gerrit # 环境迁移 #cpu #超算中心 #PBS #lsf #反向代理 #AE #rag # ms-swift #xshell #host key #PN 结 #ossinsight #cocos2d #图形渲染 #adobe #数据迁移 #测速 #iperf #iperf3 #express #cherry studio #gmssh #宝塔 #Exchange #小智 #系统安装 #铁路桥梁 #DIC技术 #箱梁试验 #裂纹监测 #四点弯曲 #可再生能源 #绿色算力 #风电 #期刊 #SCI #游戏服务器断线 #外卖配送 #AI Agent #开发者工具 #okhttp #Socket #套接字 #I/O多路复用 #字节序 #计算机外设 #boltbot #边缘AI # Kontron # SMARC-sAMX8 #remote-ssh #glances #健康医疗 #人脸活体检测 #live-pusher #动作引导 #张嘴眨眼摇头 #苹果ios安卓完美兼容 #高考 #pxe #强化学习 #策略梯度 #REINFORCE #蒙特卡洛 #AI应用 #L6 #L10 #L9 #Beidou #北斗 #SSR #MinIO #free #vmstat #sar #阿里云RDS #软件需求 #信息安全 #信息收集 #vrrp #脑裂 #keepalived主备 #高可用主备都持有VIP #H3C # AI部署 #材料工程 #AI产品经理 #VMware创建虚拟机 #远程更新 #缓存更新 #多指令适配 #物料关联计划 #DooTask #防毒面罩 #防尘面罩 #Qwen3-VL # 服务状态监控 # 视觉语言模型 #m3u8 #HLS #移动端H5网页 #APP安卓苹果ios #监控画面 直播视频流 #UEFI #BIOS #Legacy BIOS #隐函数 #常微分方程 #偏微分方程 #线性微分方程 #线性方程组 #非线性方程组 #复变函数 #bigtop #hdp #hue #kerberos #KMS 激活 #轻量化 #低配服务器 #UDP服务器 #recvfrom函数 #身体实验室 #健康认知重构 #系统思维 #微行动 #NEAT效应 #亚健康自救 #ICT人 #高精度农业气象 #Ward #docker安装seata #思爱普 #SAP S/4HANA #ABAP #NetWeaver #云计算运维 #WAN2.2 #生产服务器问题查询 #日志过滤 #4U8卡 AI 服务器 ##AI 服务器选型指南 #GPU 互联 #GPU算力 #日志模块 #dash #银河麒麟服务器系统 #Arduino BLDC #核辐射区域探测机器人 #esp32 #mosquito #2025年 #FRP #AI教程 # DIY主机 # 交叉编译 #自动化巡检 #istio #服务发现 #基金 #股票 #jquery #fork函数 #进程创建 #进程终止 #分子动力学 #化工仿真 #运动 #session #JADX-AI 插件 #语义检索 #向量嵌入 #starrocks #OpenAI #故障 #tekton #二值化 #Canny边缘检测 #轮廓检测 #透视变换 #DuckDB #协议 #新浪微博 #传媒