【RTOS学习】源码分析(信号量和互斥量 事件组 任务通知)
🐱作者:一只大喵咪1201
🐱专栏:《RTOS学习》
🔥格言:你只管努力,剩下的交给时间!
目录
- 🍓信号量和互斥量
- 🍅创建
- 🍅Take
- 🍅Give
- 🍓事件组
- 🍅设置事件
- 🍅等待事件
- 🍅同步点
- 🍓任务通知
- 🍅发通知
- 🍅等待通知
- 🍓总结
🍓信号量和互斥量
信号量和互斥量几乎一模一样:
创建:

如上图所示,创建时使用的都是xSemaphoreCreateXXX函数,只是后面的XXX不一样,其他都非常类似,而且本质上都是调用的xQueueGenericCreate函数来创建通用队列,只是传入的参数不一样。
Give:

如上图所示,Give时,使用的都是xSemaphoreGiveXXX,本质上也是在调用xQueueGenericSend向通用队列中写数据,只是参数不同。
Take:

如上图所示,Take时,使用的都是xSemaphoreTakeXXX,本质上也是在调用xQueueSemaphoreTake这个函数,只是参数不同,注意,这里并不是调用xQueueGenericReceive函数,和队列的操作不同。
可以看到,信号量和互斥量在创建,释放,申请信号量时的函数非常类似,所以本喵只需要讲解一种即可,就介绍一下最复杂的递归互斥量吧。
🍅创建

如上图代码所示,调用xSemaphoreCreateRecursiveMutex创建递归互斥量,这是一个宏函数,会调用xQueueCreateMutex,在该函数中会调用xQueueGenericCreate创建通用队列。
但在这之前先让创建的队列长度为1,队列中每个数据的大小是0字节,然后再调用xQueueGenericCreate。
根据前面创建队列时的分析我们知道,此时只会在堆区上创建一个队列结构体Queue_t,并不会分配环形缓冲区。
然后再调用prvInitialiseMutex函数初始化互斥量:

如上图所示prvInitialiseMutex函数,将Queue_t中联合体中xSemaphore成员的xMutexHolder用来表示锁的所有者设置为NULL,再将uxRecursiveCallCount递归锁计数次数设置为0。
最后再调用xQueueGenericSend向队列中写数据,写入数据是NULL,这里仅仅是让uxMessagesWaiting = 1,好让互斥量有初始值。

如上图,所以此时得到的Queue_t队列是这样的,没有存放数据的环形缓冲区,只有一个队列头,其中的成员也被赋予了合适的初始值。
🍅Take

如上图所示,使用xSemaphoreTakeRecursive申请一把递归锁,该函数会调用xQueueTakeMutexRecursive函数,在这个函数中,先判断锁的持有者身份xMutexHolder,如果是当前任务pxCurrentTCB,则说明此时是递归上锁,则将递归上锁次数uxRecursiveCallCount加一。
如果持有者身份是NULL或者不是当前任务,调用xQueueSemaphoreTake申请锁,申请成功后让uxRecursiveCallCount加一。

如上图xQueueSemaphoreTake函数,先判断可否申请锁,也就是队列中的有效数据是否大于0,如果可以申请则将有效数据uxMessagesWaiting的数值减一,然后通过pvTaskIncrementMutexHeldCount函数记录持锁人身份到pxQueue->u.xSemaphore.xMutexHolder成员中。

如上图所示,如果队列中的有效数据小于等于0,说明此时无法申请锁,如果该任务不愿意等待的话就直接错误返回,如果愿意等待,则调用vTaskInternalSetTimeOutState记录当前时刻。
在确认要阻塞后,调用xTaskPriorityInherit函数进行优先级继承,然后将该任务放入等待读取数据的xTasksWaitingToReceive链表中,再主动发起调度,让当前任务阻塞。

如上图xTaskPriorityInherit函数,如果被阻塞任务的优先级大于持锁者的优先级,并且持锁者在就绪链表中,则交换双方的位置,也就是将二者的优先级交换,并且放入对应的就绪链表中。如果持锁者不在就绪链表中,则直接将当前阻塞任务的优先级给它即可。
如果被阻塞任务的优先级小于等于持锁者的优先级,则不需要进行优先级继承。

如上图所示,当这个申请锁的任务再次被唤醒时,也是有两种情况,如果是有人释放了锁,那么for循环中再次判断操作时会成功申请到锁,成功返回。
如果是超时被唤醒,则会先调用prvGetDisinheritPriorityAfterTimeout将被继承的优先级恢复原样,然后错误返回。
🍅Give

如上图所示xSemaphoreGiveRecursive,用来释放递归锁,最后会调用xQueueGiveMutexRecursive函数,在该函数中,首先判断释放锁的是否是锁的持有者,如果是持有者,则先将递归次数减一,当该次数为0时,说明不是递归释放,则向队列中写数据,就是让有效数据uxMessagesWaiting加一。
如果不是持有者,则直接错误返回,并不会阻塞。
- 在释放锁的过程中,并不会阻塞,如果失败就直接返回。
总的来说,使用锁的过程分为如下几步:
- 创建互斥锁并进行初始化,让锁有初始值,但是此时持锁者为
NULL。 - 申请锁时:
- 如果不是持有者申请锁,则看有效数据个数
uxMessagesWaiting是否大于0,如果大于0说明可以申请,如果小于等于0,说明不可以申请,此时会将申请者放入到锁的管理读取数据的事件链表中。 - 如果是锁的持有者,对于递归锁则会让递归次数增加,非递归锁和不是持有者的待遇一样。
- 如果不是持有者申请锁,则看有效数据个数
- 释放锁时:
- 如果不是持有者,直接错误返回,因为锁不能被其他任务随意释放。
- 如果是持有者,对于递归锁,则让递归次数减少,当递归次数减少为0时,则向队列中写数据,让有效数据的个数
uxMessagesWaiting重新变为1。
🍓事件组

如上图所示事件组结构体EventGroup_t的定义,包含两个成员:
uxEventBits:这是一个32位的变量,用来存放事件,只是用低24位,每一个比特位代表一个事件。xTasksWaitingForBits:这是一个链表头,该链表用来存放因等待事件就绪而阻塞的任务TCB。

如上图xEventGroupCreate函数所示,在创建事件组时,先在堆区上开辟一块存放EventGroup_t结构体的空间,然后将结构体中表示事件值的uxEventBits清0,再初始化一下链表xTasksWaitingForBits。

如上图所示,每个任务TCB里的xEventListItem事件链表中,每个链表项中的xItemValue,该32位的变量也可以用来表示事件。
- 高8位用来表示控制位:比如等待后是否清除事件等等。
- 低24位用来表示事件:每一位表示一个事件和
EventGroup_t中的事件位对应。
🍅设置事件

如上图xEventGroupSetBits函数所示,在该函数中,先获取事件组中的链表里的第一个链表项,这里可能管理着因正在等待事件就绪而处于阻塞状态的任务。
然后将要设置的事件值,使用或等的方式赋值给EventGroup_t中的uxEventBits。

如上图所示代码,在将事件值设置好以后,需要判断是否有任务可以唤醒,使用while循环遍历链表中的所有任务。
在判断过程中,先拿到事件控制位uxControlBits,也就是在等待事件的任务对事件的态度,如果不是eventWAIT_FOR_ALL_BITS,说明当前遍历的任务不要求所有等待是事件都就绪,只要uxBitsWaitedFor & pxEventBits->uxEventBits != 0,说明只有一个或者多个事件就绪,此时将xMatchFound = pdTRUE,表示可以唤醒链表中等待的任务。
如果控制位表示要等待所有事件就绪,则只有uxBitsWaitedFor & pxEventBits->uxEventBits == uxBitsWaitedFor时,也就是所有等待事件都就绪时,才可以唤醒链表中等待的任务。
接下来如果有任务可以唤醒时,从正在遍历的当前任务中uxControlBits拿到是否要清除已经就绪的事件所在的bit。
之后再调用vTaskRemoveFromUnorderedEventList将要唤醒的任务从事件组的链表中移除,并且设置标志eventUNBLOCKED_DUE_TO_BIT_SET,表示该任务被唤醒是由于等待事件成功。
最后返回事件组中的事件值pxEventBits->uxEventBits。
在设置事件值时主要分为三大步:
- 设置要等待的事件值。
- 唤醒事件组链表中正在等待事件就绪的任务:
- 等待事件的任务有要求全部事件就绪时才会被唤醒。
- 等待事件的任务也有只要求有事件就绪时就可以被唤醒。
- 在退出函数时,根据控制位决定是否清除事件组中已经就绪的事件。
🍅等待事件

如上图xEventGroupWaitBits函数,该函数是由某个任务调用的,用来等待事件组中要等待的事件。
首先就是获取事件组中的事件值uxCurrentEventBits,然后判断其与等待任务要等待的事件uxBitsToWaitFor是否相等,要根据xWaitForAllBits决定是所有事件都就绪才符合等待要求还是有事件就绪就符合等待要求。
当符合等待要求时,根据控制位决定是否在退出该函数前将事件组中已经就绪的事件值清除,如果不符合等待要求,且该任务不愿意等待,则超时返回,如果愿意等待,则将该任务放入到事件组用来维护等待事件的任务链表中。
然后主动发起调度,当前任务就阻塞在这里了。
当该任务再次被唤醒时,有可能是等待的事件就绪了被唤醒,也有可能是因为超时而被唤醒:

如上图所示,当等待事件的任务再次被唤醒时,根据eventUNBLOCKED_DUE_TO_BIT_SET判断一下是否因为事件就绪被唤醒,如果不是,则说明是超时,再判断一次事件是否就绪,没有就绪则超时返回。
如果是事件就绪而被唤醒,等待成功返回。
等待过程中注意分为三步:
- 判断要等待的事件和事件组中的事件值,根据任务指定的控制值决定是所有事件都就绪才算等待成功还是只要有事件就绪就算等待成功。
- 如果等待成功,则成功返回,等待不成功,则根据是否愿意等待决定是错误返回还是放入事件组的阻塞链表中。
- 处于阻塞状态再次被唤醒后,根据
eventUNBLOCKED_DUE_TO_BIT_SET位判断是事件就绪被唤醒还是超时唤醒。
🍅同步点

如上图xEventGroupSync函数,调用该函数可以实现同步点,在函数内部,首先将要等待的事件设置到事件组中:
- 如果在设置过程中,其他等待同步点的事件产生,则唤醒其他任务
- 并且判断自己要等待的事件也全部发生,自己不会被阻塞。
如果自己要等待的事件没有全部就绪,说明其他任务也没有被唤醒,此时将当前任务继续添加到事件组的阻塞等待链表中。
如果当前任务愿意等待,则主动发起调度,阻塞到这里。
再次被唤醒后,如果eventUNBLOCKED_DUE_TO_BIT_SET不为1,说明是超时唤醒,则错误返回,如果该位为1,说明是事件被设置唤醒。
多个等待同步的任务都会从这里被唤醒,开始一起继续执行。
🍓任务通知

如上图所示,使用队列、信号量、事件组时,我们都要事先创建对应的结构体,双方通过中间的结构体通信。

如上图所示,使用任务通知时,任务结构体TCB中就包含了内部对象,可以直接接收别人发过来的"通知"。
使用任务通知时,可以明确指定:通知哪个任务。

如上图所示,每个任务都有一个结构体:TCB(Task Control Block),里面有2个成员:
- 一个是uint8_t类型的
ucNotifyState数组,用来表示通知状态。 - 一个是uint32_t类型
ulNotifiedValue数组,用来表示通知值。
数组的大小#define configTASK_NOTIFICATION_ARRAY_ENTRIES 1为1,可以将数组看成一个变量。
通知状态有3种取值:
taskNOT_WAITING_NOTIFICATION:任务没有在等待通知taskWAITING_NOTIFICATION:任务在等待通知taskNOTIFICATION_RECEIVED:任务接收到了通知,也被称为pending(有数据了,待处理)
🍅发通知

如上图所示xTaskNotify,调用该函数项目标任务发出通知时,会调用xTaskGenericNotify,在该函数中,首先获取被通知任务的状态值ucNotifyState,然后将该值设置为taskNOTIFICATION_RECEIVED,表示已经对该任务发出了通知。
根据eAction对被通知的任务通知值ulNotIfiedValue进行操作,可以赋值,可以加加,可以覆盖,也可以不做任何操作等等。
操作完毕后,判断一下被通知之前目标任务的状态,如果是taskWAITING_NOTIFICATION,说明目标任务在等待通知而处于阻塞状态,所以此时将目标任务放入到就绪链表中,如果被通知任务的优先级更高,则主动发起调度。
向任务发起通知总的来说就三步:
- 设置通知状态为
taskNOTIFICATION_RECEIVED。 - 根据
eACction操作通知值ulNotIfiedValue。 - 任务如果原本阻塞,则将其放入就绪链表中。
🍅等待通知

如上图xTaskNotifyWait函数,任务调用该函数等待任务通知,最后会调用xTaskGenericNotifyWait函数,在该函数内,首先判断当前任务的任务状态ucNotifyState。
- 如果状态值不是
taskNOTIFICATION_RECEIVED,说明没有接到通知:- 根据控制位决定是否在入口处清除指定事件。
- 将当前任务状态设置为
taskWAITING_NOTIFICATION,表示正在等待通知。 - 如果愿意等待,则将当前任务放入到延时链表中,然后发起调度,当前任务阻塞。
- 被唤醒或者第一次调用就接收到任务通知:
- 再次判断任务状态,如果不是
taskNOTIFICATION_RECEIVED,说明是超时唤醒,直接错误返回。 - 如果是
taskNOTIFICATION_RECEIVED,说明接收到任务通知被唤醒,根据控制位决定是否在出口处清除事件。
- 再次判断任务状态,如果不是
🍓总结
虽然互斥量信号量,事件组名字中不包含队列,但其本质上还是使用的通用队列,只是该队列中不存放数据本身,只靠Queue_t中的成员。
对于任务通知,不如说是通知任务,更是没有用于两个任务间通信的结构,直接从一个任务指定通知另一个任务,改变的是TCB中的任务通知状态和任务通知值。
相关文章:
【RTOS学习】源码分析(信号量和互斥量 事件组 任务通知)
🐱作者:一只大喵咪1201 🐱专栏:《RTOS学习》 🔥格言:你只管努力,剩下的交给时间! 目录 🍓信号量和互斥量🍅创建🍅Take🍅Give &#x…...
1316:【例4.6】数的计数(Noip2001) 代码+解析
1316:【例4.6】数的计数(Noip2001) 【题目描述】 我们要求找出具有下列性质数的个数(包括输入的自然数n )。先输入一个自然数n(n≤1000),然后对此自然数按照如下方法进行处理:不作任何处理;在它的左边加上一…...
征集倒计时 | 2023年卓越影响力榜单-第四届中国产业创新奖报名即将截止
第四届「ISIG中国产业智能大会」将于2024年3月16日在上海举办。2024 ISIG 以“与科技共赢,与产业共进”为主题,共设立RPA超自动化、 低代码、AIGC大模型、流程挖掘四大主题峰会。届时,大会组委会将颁发2023年度卓越影响力榜单—第四届中国产业…...
vue的语法模板与数据绑定的说明
vue的两大模板语法: 1.插值语法 2.指定语法 插值语法:{{}} 功能:用于解析标签体的内容 写法:{{xxx}},xxx是js表达式,且可以直接读取到data中的所有属性 指定语法: 功能:用于解析标签(包括:标签属性、标…...
VueCron使用方法
1)什么是vueCron Vue Cron 是基于 Vue.js 的定时任务管理组件,它提供了一种简单易用的方式来设定和管理定时任务。Vue Cron 提供了一个类似于 Linux crontab 的界面,用户可以通过它来创建、编辑和删除定时任务。 2)安装依赖及应…...
SpringBlade export-user SQL 注入漏洞复现
0x01 产品简介 SpringBlade 是一个由商业级项目升级优化而来的 SpringCloud 分布式微服务架构、SpringBoot 单体式微服务架构并存的综合型项目。 0x02 漏洞概述 SpringBlade v3.2.0 及之前版本框架后台 export-user 路径存在安全漏洞,攻击者利用该漏洞可通过组件customSqlS…...
结构体的一些补充知识
1、结构体后面分号前面的名字是什么意思。 在C中,结构体的定义格式为: struct <结构体名> {// 成员变量和成员函数 };在这个定义中,<结构体名>就是结构体的名称,而这个名称位于结构体定义的末尾,分号之前…...
20V升26V 600mA升压型LED驱动芯片,PWM调光芯片-AH1160
AH1160是一个功能强大的升压型LED驱动芯片,专为需要精确控制LED亮度的PWM调光应用而设计。它可将20V输入电压升压至26V,同时提供稳定的600mA电流输出,适用于各种LED照明设备。 芯片特点: 1. 输入电压范围:AH1160可在…...
如何在Go中制作HTTP服务器
引言 许多开发人员至少会花一些时间创建服务器,以便在互联网上分发内容。HTTP (Hypertext Transfer Protocol,超文本传输协议)提供了大部分这些内容,无论是请求一张猫的图片还是请求加载你正在阅读的教程。Go标准库为创建HTTP服务器以提供web内容或向这些服务器发出HTTP请求…...
Linux笔记---系统信息
🍎个人博客:个人主页 🏆个人专栏:Linux学习 ⛳️ 功不唐捐,玉汝于成 目录 前言 命令 1. uname - 显示系统信息 2. hostname - 显示或设置系统主机名 3. top - 显示系统资源使用情况 4. df - 显示磁盘空间使用情…...
最新版android stuido加上namespace
每个 Android 模块都有一个命名空间,此命名空间用作其生成的 命名空间由模块的 build.gradle 文件中的 namespace 属性定义,如以下代码段所示。namespace 最初会设为您在创建项目时选择的软件包名称。 Kotlin Groovy android {namespace "com.ex…...
Wireshark基础及捕获技巧
第一章:Wireshark基础及捕获技巧 1.1 Wireshark基础知识回顾 1.2 高级捕获技巧:过滤器和捕获选项 1.3 Wireshark与其他抓包工具的比较 第二章:网络协议分析 2.1 网络协议分析:TCP、UDP、ICMP等 2.2 高级协议分析:HTTP…...
Windows下Navicat15.0连接Oracle11g报ORA-28547解决
目录 背景 一、相关环境 1、操作系统 2、Navicat版本 3、ORACLE连接 4、默认连接 二、问题分析 1、默认dll配置 三、修改配置 1、下载匹配的client 2、替换相应目录 总结 背景 最近在项目中需要使用Oracle数据库,当前很多应用系统的数据都存储在MySQL或者Pos…...
21 Vue3中使用v-for遍历对象数组
概述 使用v-for遍历对象数组在真实的开发中也属于非常常见的用法,需要重点掌握。 因为目前流行的是前后端分离开发,在前后端分离开发中,最常需要处理的就是对象数组类型的数据了。 比如,将员工信息渲染到表格中。 这节课我们就…...
深入理解Java自定义异常与全局异常处理 @RestControllerAdvice
异常主要是包括编译时的异常和运行时的异常。编译时的异常可以通过捕获异常获取,运行时候的异常主要是通过代码规范,或者测试。 Spring Boot提供了两种异常处理方式来统一处理和维护异常信息。 第一种方式是使用RestControllerAdvice注解与ExceptionHand…...
h5页面跳转微信小程序(最简单的方法|URL Scheme)
文章目录 导文实现获取 URL Scheme加密 URL Scheme获取方式拼接参数 明文 URL Scheme获取方式 实际项目展示:频率限制注意事项开放范围示例代码包 导文 H5页面跳转微信小程序的需求是普遍存在的。由于微信小程序是一种只能在微信内部访问的应用程序,而H5…...
智能优化算法应用:基于非洲秃鹫算法3D无线传感器网络(WSN)覆盖优化 - 附代码
智能优化算法应用:基于非洲秃鹫算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用:基于非洲秃鹫算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.非洲秃鹫算法4.实验参数设定5.算法结果6.…...
持续集成交付CICD:Jenkins使用GitLab共享库实现前端项目镜像构建
目录 一、实验 1. GitLab修改项目文件与Harbor环境确认 2.Jenkins使用GitLab共享库实现前端项目镜像构建 3.优化CI流水线封装Harbor账户密码 4.Jenkins再次使用GitLab共享库实现前端项目镜像构建 一、实验 1. GitLab修改项目文件与Harbor环境确认 (1…...
SQL server 数据库 SQL语句高级用法
1、表的高级查询 use student select * from stuinfo1 -- 使用 in 的子查询 select * from stuinfo where stu_age in ( select stu_age from stuinfo where cla_id 12345 ) select * from stuinfo where stu_age in ( 19 , 20 , 21 , 25 , 23 , 1…...
wavlink 路由器 多处前台RCE漏洞复现
0x01 产品简介 WAVLINK是中国睿因科技(WAVLINK)公司开发的一款路由器。 0x02 漏洞概述 WAVLINK路由器mesh.cgi、nightled.cgi、live-api.cgi等接口处存在命令执行漏洞,攻击者可通过该漏洞获取服务器权限。包含型号WN530HG4、WN531G3、WN572HG3、WN535G3、WN575A4等。 0x…...
绝美辛夷花海!九皇山春日限定,羌族古寨里的粉色浪漫
九皇山位于四川省绵阳市北川羌族自治县桂溪镇,是国家4A级旅游景区,地处四川盆地西北边缘与川西高原的过渡地带,属典型喀斯特地貌,核心风貌兼具丰富的自然生态景观与深厚的羌族人文底蕴,景区占地面积25平方公里…...
AcousticSense AI作品分享:识别不同音乐流派的频谱图展示
AcousticSense AI作品分享:识别不同音乐流派的频谱图展示 1. 当AI学会"看"音乐:频谱图里的流派密码 你有没有想过,AI是如何像人类一样理解音乐的?传统方法往往依赖复杂的音频特征提取,而AcousticSense AI选…...
【RK3588 NPU性能调优实战】多线程异步推理YOLOv5,榨干6TOPS算力
1. 为什么你的RK3588 NPU跑不满6TOPS? 第一次在RK3588上跑YOLOv5时,我也被官方宣称的6TOPS算力唬住了。直到亲眼看到npu-smi显示的实际利用率——好家伙,不到30%!这就像买了辆跑车却只能挂一档开。经过两周的折腾,终于…...
告别复杂配置!Qwen-Image-2512-SDNQ一键部署,打造专属AI绘画网站
告别复杂配置!Qwen-Image-2512-SDNQ一键部署,打造专属AI绘画网站 1. 为什么选择Qwen-Image-2512-SDNQ镜像? 在AI绘画领域,模型部署往往意味着复杂的配置和环境搭建。Qwen-Image-2512-SDNQ-uint4-svd-r32镜像彻底改变了这一现状&…...
React-PDF自定义字体粗细终极指南:实现精确文本字重控制的完整教程
React-PDF自定义字体粗细终极指南:实现精确文本字重控制的完整教程 【免费下载链接】react-pdf 📄 Create PDF files using React 项目地址: https://gitcode.com/gh_mirrors/re/react-pdf React-PDF是一个功能强大的库,允许开发者使用…...
从手忙脚乱到从容不迫:DouyinLiveRecorder如何用智能代理池解决多平台直播录制难题
从手忙脚乱到从容不迫:DouyinLiveRecorder如何用智能代理池解决多平台直播录制难题 【免费下载链接】DouyinLiveRecorder 项目地址: https://gitcode.com/gh_mirrors/do/DouyinLiveRecorder 你是否曾经为了录制不同平台的直播内容而疲于奔命?当抖…...
Avalonia预览器罢工了?别慌,手把手教你排查和修复‘无法加载axaml预览’的坑
Avalonia预览器崩溃自救指南:从错误日志到配置优化的全链路解决方案 当你正沉浸在Avalonia跨平台UI开发的流畅体验中,突然发现预览窗口变成一片空白,右下角弹出"无法加载axaml预览"的红色警告——这种突如其来的开发中断࿰…...
家庭实验室:树莓派控制OpenClaw调用远程Qwen3-32B
家庭实验室:树莓派控制OpenClaw调用远程Qwen3-32B 1. 为什么选择树莓派OpenClaw组合 去年冬天,我在整理家庭实验室设备时发现一个闲置的树莓派4B。这台信用卡大小的电脑曾经用来跑Home Assistant控制智能家居,但后来换了NUC主机就被束之高阁…...
Html2Pdf高性能转换引擎:PHP 7.2-8.4全版本兼容的企业级HTML转PDF解决方案
Html2Pdf高性能转换引擎:PHP 7.2-8.4全版本兼容的企业级HTML转PDF解决方案 【免费下载链接】html2pdf OFFICIAL PROJECT | HTML to PDF converter written in PHP 项目地址: https://gitcode.com/gh_mirrors/ht/html2pdf 在当今企业数字化转型浪潮中…...
部署开源的Minecraft服务器智能运维管理系统 Minecraft-Rcon-Manage 自存简易教程
项目地址:Minecraft-Rcon-Manage 前言 笔者最近寻找一款能实现Minecraft服务器RCON远程访问的工具,找到了这个目前正在持续更新、功能丰富的开源项目Minecraft-Rcon-Manage,但实际部署过程中发现作者提供的教程博客无法正常访问,…...

