Re:Linux系统篇(五十五)线程篇 · 八:为什么需要条件变量?从生活比喻、API 梳理到 pthread_cond 实战源码探究
◆ 博主名称 小此方-CSDN博客大家好欢迎来到小此方的博客。⭐️Linux系列个人专栏 【主题曲】Linux⭐️此方的GitHub github_此方⭐️Re系列专栏我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)文章目录概要序論一、从互斥到同步为什么仅有互斥是不够的1.1 纯互斥场景下的极端故事超级自习室1.1.1 争夺钥匙与高效释放1.1.2 极端循环与“饥饿问题”Tips:其他线程获取临界资源的契机1.2 问题的解决方案与临时调优1.2.1 临时调优方案在循环中加入微小休眠1.2.2 制度层面重塑管理者规则与队列机制1.3 引入线程同步Thread Synchronization1.3.1 什么是线程同步1.3.2 互斥与同步的互补关系二、理解同步的概念2.1 一个游戏拿苹果与放苹果2.1.1 阶段一没有铃铛与队列纯互斥逻辑2.1.2 阶段二引入“铃铛与队列”同步机制2.2 专业抽象的概念定义2.2.1 条件变量Condition Variable2.2.2 同步概念与竞态条件三、快速梳理条件变量的调用接口3.1 条件变量的初始化与销毁3.1.1 静态初始化3.1.2 动态初始化3.1.3 销毁条件变量3.2 在条件变量下等待pthread_cond_wait3.2.1在条件变量中等待的线程被唤醒后pthread_cond_wait内部做了什么工作3.3 唤醒在条件变量下等待的线程3.3.1 唤醒单个线程pthread_cond_signal3.3.2 广播唤醒所有线程pthread_cond_broadcast四、第一个测试 demo4.1 测试代码展示4.2 Demo 核心逻辑拆解与原理探究4.2.1 为什么一定要先加锁再调用 pthread_cond_wait4.2.2 pthread_cond_wait 内部隐藏的自动解锁机制4.2.3 线程被唤醒后的重新竞争锁概要序論Hello大家好我是此方上两篇我们讲到了互斥量的概念、接口、原理和封装。本文开始讲解同步和条件变量相关内容。一、从互斥到同步为什么仅有互斥是不够的引入新的技术必然是为了解决旧技术带来的新问题。在上一篇文章中我们学习了互斥锁Mutex的基本概念与使用。互斥成功解决了多线程并发访问临界资源时的“数据不一致”与“竞争条件”问题确保了同一时刻只有一个执行流能够进入临界区。然而单纯依靠“纯互斥”真的就完美无缺了吗1.1 纯互斥场景下的极端故事超级自习室为了更好地理解纯互斥带来的隐患我们来看一个生活中“超级自习室”的例子。假设有一个“超级自习室”里面只有一张桌子且自习室的门锁只能由一把钥匙打开。自习室相当于临界资源。钥匙相当于互斥锁。想进去自习的人相当于各个线程执行流。1.1.1 争夺钥匙与高效释放当一个人线程 A成功拿到了钥匙并进入自习室后其他人就只能在门外等待。过了一段时间线程 A 准备离开自习室。他打开门、走出自习室并把钥匙归还到了门口的钥匙挂钩上释放锁。但此时存在一个极其特殊的物理现象由于线程 A 离钥匙挂钩距离最近刚刚把钥匙挂上去他又可以瞬间再次伸手将钥匙拿下来此时门外排队的其他人在做什么呢其他的线程离钥匙挂钩相对较远或者正处于“准备唤醒”、“休眠恢复”等各种微小的系统开销与延迟中。因此线程 A 刚出来又立马申请到了钥匙重新关上门进入自习室。1.1.2 极端循环与“饥饿问题”在自习室内部线程 A 发现自己其实并没有什么实质性的自习任务可做没有做有效动作于是他又开门出来挂上钥匙。挂上钥匙的瞬间他又再次抢到了钥匙……如此循环往复申请锁进入临界区但未进行有效操作或高频重复操作释放锁凭借距离优势瞬间重新申请锁从互斥规则来看纯互斥有错吗没有任何错因为任何时刻确实都只有一个人进入了自习室数据安全得到了绝对保证。但是从系统整体的运行效率与公平性来看不高效线程 A 在频繁申请和释放锁却没能进行有效的业务处理。不太公平其他想要自习的人其他线程长时间在门外等待永远抢不过距离挂钩最近的线程 A。在多线程编程中这种其他线程长时间获取不到临界资源、导致无法继续推进的现象就称为线程饥饿问题Starvation。在 Ubuntu 或 CentOS 等不同的 Linux 发行版/内核调度环境下如果不加任何干预纯互斥锁的这种弊端往往表现得非常明显。Tips:其他线程获取临界资源的契机既然活跃线程每次都能近水楼台先得月其他线程有没有可能获取道临界资源呢我让GPT帮我整理了三种情况1.2 问题的解决方案与临时调优既然纯互斥可能会带来“饥饿问题”和“效率低下”我们该如何改进1.2.1 临时调优方案在循环中加入微小休眠在编写多线程代码时如果在循环体内释放锁之后加上一行极小的休眠指令如usleep(100);// 释放锁后给其他线程一些调度和申请的时间当线程 A 释放锁后主动休眠哪怕只有 100 微秒就能将CPU的使用权和抢锁机会让渡出来给其他线程留出反应和唤醒的时间。这种做法能够让各个线程更加平均地申请到锁显著缓解饥饿现象。但这仅仅是一种通过“人为硬编码延时”进行的妥协式调优并没有从根本的机制层面彻底解决“访问顺序”问题。1.2.2 制度层面重塑管理者规则与队列机制要从根本上解决自习室的秩序问题自习室管理员提出了两项严格的新规定不能立即申请第二次刚从自习室出来释放锁的人不能当场重新伸手抢钥匙。按顺序排队所有想进入自习室的人必须先在一个指定的队列末尾进行排队。刚出来的人如果还想再次进入必须跑回到队列的尾部重新排队等待下一次轮到自己。通过这种“先来后到”的排队约束所有人都有了明确且可预测的进入顺序。1.3 引入线程同步Thread Synchronization通过给纯互斥加上“按顺序排队/访问”的约束规则我们就正式引出了操作系统中的线程同步Thread Synchronization机制。1.3.1 什么是线程同步所谓的线程同步在保证临界资源安全互斥的前提下让所有的执行流按照某种一定的顺序来访问临界资源。1.3.2 互斥与同步的互补关系互斥解决的是“安全”问题确保数据不被破坏即“不能同时进”。同步解决的是“高效与公平”问题避免线程饥饿并提高整体系统的协同吞吐量即“有序轮流进”。只有将“互斥”与“同步”结合起来多线程对共享资源的协同访问才算真正走向了成熟与高效。二、理解同步的概念在深入操作系统底层接口之前我们先回顾一个之前接触过的类似机制管道Pipe。在进程间通信的管道中当管道为空时读端会被阻塞不能继续读取当管道被写满时写端会被阻塞不能继续写入。读端与写端之间这种“有资源才读、有空间才写”的配合本质上就是一种典型的同步机制。先讲抽象的概念容易劝退我们先来做一个游戏。2.1 一个游戏拿苹果与放苹果假设桌子上有一个盘子盘子里最多只能放一个苹果。现在有两类角色放苹果的人拿苹果的人由于盘子属于共享资源为了防止多人同时抢夺盘子上方挂着一把锁每次只能有一个人拿到锁并靠近盘子。2.1.1 阶段一没有铃铛与队列纯互斥逻辑在这个阶段桌子上只有一把锁。拿苹果的人因为蒙着眼睛在不拿到锁、伸手摸盘子之前根本不知道盘子里到底有没有苹果。此时的情况如下拿苹果的人先申请锁。拿到锁后伸手摸盘子发现盘子是空的。他什么也拿不到只能释放锁离开。由于他距离锁最近刚释放锁他又顺手重新申请了锁再摸一次盘子发现还是空的又释放锁……这种模式带来的危害拿苹果的人在没有苹果时疯狂地“申请锁→ \rightarrow→发现没苹果→ \rightarrow→释放锁→ \rightarrow→再申请锁”。极大地消耗了精力占用 CPU 资源却没有做出任何有效动作。而放苹果的人想过来放苹果却根本抢不到锁。2.1.2 阶段二引入“铃铛与队列”同步机制为了解决上述危害游戏规则进行了升级在盘子旁边放置了一个铃铛并在旁边设立了一条排队队列。现在的互动约定如下拿苹果的人申请锁后摸盘子如果摸到了苹果直接拿走释放锁离开。如果发现盘子为空他不再盲目重新抢锁而是主动释放锁并跑到队列末尾排队休眠。放苹果的人申请锁后将苹果放入盘子放完苹果后他去按一下铃铛。铃声响起唤醒在队列头部等待的第一个人。被唤醒的人直接从队头出来去拿苹果拿完后再回到队尾排队。引入后的好处拿苹果的人不再进行无意义的重复抢锁盘子为空时就安静地在队列中等待放苹果的人放完资源后通过“按铃”精准通知等待者。整个过程井然有序资源得到了最充分的利用。故事中的**“铃铛 队列”** 组合在 Linux 多线程编程中对应的正是条件变量Condition Variable2.2 专业抽象的概念定义看懂了上面的故事我们再来看操作系统中关于同步与条件变量的专业定义就会一目了然2.2.1 条件变量Condition Variable当一个线程互斥地访问某个变量时它可能发现在其它线程改变状态之前它什么也做不了。例如一个线程访问队列时发现队列为空它只能等待直到其它线程将一个节点添加到队列中。这种情况下就需要用到条件变量。2.2.2 同步概念与竞态条件同步在保证数据安全互斥的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题叫做同步。竞态条件Race Condition由于执行时序的问题而导致程序运行出现异常或结果不符合预期我们称之为竞态条件。三、快速梳理条件变量的调用接口条件变量的调用接口与我们前面讲过的互斥量调用接口十分相似在使用时都需要引入头文件pthread.h。在 POSIX 线程库中条件变量的数据类型为pthread_cond_t。下面我们对条件变量的核心 API 进行快速梳理。3.1 条件变量的初始化与销毁与互斥锁类似条件变量也分为静态初始化与动态初始化两种方式。3.1.1 静态初始化如果条件变量被定义为全局变量或static静态变量可以使用宏进行快速静态初始化无需手动销毁pthread_cond_tcondPTHREAD_COND_INITIALIZER;3.1.2 动态初始化如果条件变量是局部变量或在堆上动态分配的则需要调用pthread_cond_init函数进行动态初始化intpthread_cond_init(pthread_cond_t*restrict cond,constpthread_condattr_t*restrict attr);cond指向需要初始化的条件变量的指针。attr条件变量的属性通常设置为NULL表示使用默认属性。3.1.3 销毁条件变量对于通过pthread_cond_init动态初始化的条件变量在不再使用时必须调用pthread_cond_destroy进行销毁和资源释放intpthread_cond_destroy(pthread_cond_t*cond);3.2 在条件变量下等待pthread_cond_wait当线程访问临界资源前发现条件不满足时需要调用等待接口使自身挂起休眠进入该条件变量的等待队列中。intpthread_cond_wait(pthread_cond_t*restrict cond,pthread_mutex_t*restrict mutex);cond指定要在哪个条件变量下进行等待。mutex互斥锁指针。关键疑问这里可以注意到一个很特殊的细节——pthread_cond_wait的第二个参数接收了一把互斥锁mutex。为什么在条件变量下等待时必须把互斥锁作为参数传进去3.2.1在条件变量中等待的线程被唤醒后pthread_cond_wait内部做了什么工作脱离条件变量队列重新竞争互斥锁关键步骤成功获取锁函数准备返回函数返回继续向下执行代码3.3 唤醒在条件变量下等待的线程当其它线程改变了状态例如产生了新的资源、放下了苹果就需要去唤醒在条件变量队列中等待的线程。POSIX 提供了两种唤醒方式3.3.1 唤醒单个线程pthread_cond_signalintpthread_cond_signal(pthread_cond_t*cond);功能唤醒在指定条件变量cond下等待的一个线程相当于按一下铃铛唤醒队列头部的线程。3.3.2 广播唤醒所有线程pthread_cond_broadcastintpthread_cond_broadcast(pthread_cond_t*cond);功能唤醒在指定条件变量cond下等待的所有线程相当于大喇叭广播让队列中所有等待的线程同时醒来重新竞争资源。四、第一个测试 demo4.1 测试代码展示#includeiostream#includepthread.h#includevector#includeunistd.hintticket100000;pthread_mutex_t _mutexPTHREAD_MUTEX_INITIALIZER;pthread_cond_t _condPTHREAD_COND_INITIALIZER;void*RunThread(void*args){char*namestatic_castchar*(args);while(true){{pthread_mutex_lock(_mutex);pthread_cond_wait(_cond,_mutex);if(ticket0){ticket--;std::cout线程name抢到票: ticketstd::endl;}pthread_mutex_unlock(_mutex);}}}intmain(){intcnt5;std::vectorpthread_tpvr;while(cnt){pthread_t tid;pvr.push_back(tid);char*namenewchar[64];intnsnprintf(name,64,thread-%d,cnt);if(n0)perror(snprintf error);pthread_create(tid,nullptr,RunThread,name);cnt--;}while(true){std::cout唤醒一个线程std::endl;usleep(1000);pthread_cond_signal(_cond);}for(autoe:pvr){pthread_join(e,nullptr);}return0;}4.2 Demo 核心逻辑拆解与原理探究在这段代码中主线程创建了 5 个工作线程它们都在执行RunThread函数。我们可以通过以下三个关键问题来深入理解这段 Demo 的运行机制。4.2.1 为什么一定要先加锁再调用 pthread_cond_wait观察RunThread函数内部逻辑pthread_mutex_lock(_mutex);pthread_cond_wait(_cond,_mutex);判断条件本身就是访问临界资源线程之所以要等待是因为“某种条件不满足”如票数不足或资源未就绪。而要检查条件是否满足就必须读取共享变量ticket。检查共享变量的操作必须在加锁保护的临界区内部进行。休眠必须在临界区内发起当判定条件不满足需要休眠时线程此时已经处于临界区内部手握互斥锁。4.2.2 pthread_cond_wait 内部隐藏的自动解锁机制这就引出了一个严密的逻辑冲突如果线程拿着锁直接挂起休眠了其他线程不就再也无法申请到锁去改变条件了吗这就是为什么pthread_cond_wait必须传入第二个参数_mutex的根本原因当线程执行pthread_cond_wait时操作系统会在将该线程挂入_cond等待队列的同时自动释放传入的互斥锁_mutex线程执行链路申请锁成功进入临界区→ \rightarrow→判定条件不满足→ \rightarrow→调用pthread_cond_wait→ \rightarrow→线程挂入条件变量等待队列→ \rightarrow→自动释放锁→ \rightarrow→允许新线程进入临界区。这样一来其他线程如主线程或其他生产者才能顺利拿到互斥锁去修改共享变量或发出唤醒信号。4.2.3 线程被唤醒后的重新竞争锁在主线程中通过一个死循环定期调用pthread_cond_signal(_cond);运行表现与底层动作有序被唤醒主线程每发出一次pthread_cond_signal就会从_cond等待队列的队头唤醒一个线程。从终端打印输出可以清晰看到各个线程被唤醒的顺序完全遵循入队列的顺序避免了线程饥饿。唤醒后重新竞争锁当被唤醒的线程从pthread_cond_wait函数返回时它并不是直接继续往下执行而是必须在函数内部重新去竞争_mutex锁安全退出临界区只有重新成功抢到锁的线程才会从pthread_cond_wait内部返回出来接着执行后续的ticket--操作并在最后通过pthread_mutex_unlock(_mutex)释放锁。如果将主线程中的pthread_cond_signal替换为广播唤醒pthread_cond_broadcast虽然所有等待线程会在瞬间全部被唤醒但由于重新竞争锁的机制存在它们依然会排队依次获取锁进入后续逻辑绝对不会出现多个线程同时在临界区内修改数据导致的混乱现象。好的本期内容就到这里如果对你有帮助还不要忘记点赞三联支持。我是此方我们下期再见。bye! Linux、C、算法持续连载中欢迎关注WeChat Official Account 【此方的技术栈】。

相关新闻

多模态数据库SurrealDB:简介、实战

多模态数据库SurrealDB:简介、实战

概述 关系型、nosql、向量数据库、嵌入式、 简介 官网,用Rust写的开源(GitHub,32.8K Star,1.3K Fork)多模型数据库,把文档、图、关系、时序、地理空间和向量全塞进同一个引擎里;对外暴露Surr…

2026/9/25 6:48:22 阅读更多 →
Nginx安全配置实战:从基础代理到多层拉黑策略

Nginx安全配置实战:从基础代理到多层拉黑策略

1. 项目概述:从“拉黑”需求看Nginx在前端架构中的核心价值 最近在部署一个基于Nuxt.js的服务端渲染应用时,我遇到了一个挺典型的运维需求:如何在前端服务器层面,高效、精准地拦截恶意请求?这个需求在社区里常被简称为…

2026/9/25 13:34:31 阅读更多 →
金融级AI应用实战:从零构建可复现的信用评分预测服务

金融级AI应用实战:从零构建可复现的信用评分预测服务

在实际金融科技项目中,银行和金融机构对人工智能(AI)的投入早已超越概念验证阶段,进入如何将AI能力安全、合规、高效地融入核心业务流程的深水区。无论是用于风险控制的智能风控模型,还是提升客户体验的智能客服&#…

2026/9/25 13:34:27 阅读更多 →

最新新闻

SpringBoot+Vue 实现办公用品管理系统|计算机毕设源码讲解

SpringBoot+Vue 实现办公用品管理系统|计算机毕设源码讲解

💖💖作者:计算机毕业设计小明哥 💙💙个人简介:曾长期从事计算机专业培训教学,本人也热爱上课教学,语言擅长Java、微信小程序、Python、Golang、安卓Android等,开发项目包…

2026/9/25 22:07:44 阅读更多 →
Python Assert 语句

Python Assert 语句

我们要去搞明白, 到底什么叫做断言。断言是程序里用来坚定地声明或表明某个事实的语句。比如在编一个除法的函数时, 你内心非常确定, 那个除数是不应该等于零的, 所以你就发出了断言, 说明这个除数不是零。断言仅仅只是一个布尔表达式, 它的作用是用来检查某个具体的条件有没有…

2026/9/25 22:07:44 阅读更多 →
阿里云 300万美金加入 Linux 基金会 Alibaba Cloud joins as a Founding Corporate Patron with $3 million

阿里云 300万美金加入 Linux 基金会 Alibaba Cloud joins as a Founding Corporate Patron with $3 million

阿里巴巴云正式加入 Omacom 基金会,成为创始企业赞助人,承诺每年出资 100 万美元,连续三年!这意味着总计 300 万美元的投入,与 DigitalOcean 的赞助金额持平,将全部用于 Omarchy 的开发、维护与推广。 但这…

2026/9/25 22:06:44 阅读更多 →
云服务器怎么搭建python环境变量管理系统

云服务器怎么搭建python环境变量管理系统

要搭建一个系统用来管理环境变量这事儿, 它并不是简简单单就能弄好的, 你首先得具备一定的基础知识储备, 并且还要有一定的编程实际操作经验才行;接下来这儿有一个非常基础的系统框架可以摆在你的面前供你看一看, 这个框架可不是固定不变的死规矩, 它是可以根据你自…

2026/9/25 22:06:44 阅读更多 →
提示词实测:剩菜太多不知道吃什么,让 AI 直接决定今晚菜单

提示词实测:剩菜太多不知道吃什么,让 AI 直接决定今晚菜单

冰箱里剩下一堆食材、又不想专门买菜时,晚上吃什么最头疼。我实测了一组提示词,把人数、食材、口味和时间限制一次性告诉 AI,让它直接决定菜单,而不是列一堆菜让我自己选。提示词的关键要求 提示词要求 AI 优先使用现有食材、根据…

2026/9/25 22:05:43 阅读更多 →
init_rootfs / shmem_init / init_ramfs_fs 函数

init_rootfs / shmem_init / init_ramfs_fs 函数

init_rootfs1. init_rootfs 函数1.1 shmem_init 函数1.2 init_ramfs_fs 函数1. init_rootfs 函数 通过 register_filesystem 函数,将新的rootfs文件系统插入到全局链表file_systems中 通过 init_ramfs_fs()->register_filesystem 函数,将一个新的ram…

2026/9/25 22:05:43 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/25 19:27:14 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/25 11:15:26 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/25 20:29:09 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/25 20:29:43 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/25 20:29:31 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/25 19:27:26 阅读更多 →