2024年7月

解锁 SQL Server 2022的时间序列数据功能

SQL Server2022在处理时间序列数据时,SQL Server 提供了一些优化和功能,比如 DATE_BUCKET 函数、窗口函数(如 FIRST_VALUE 和 LAST_VALUE)以及其他时间日期函数,以便更高效地处理时间序列数据。


GENERATE_SERIES函数

SQL Server 2022 引入了一个新的函数 GENERATE_SERIES,它用于生成一个整数序列。
这个函数非常有用,可以在查询中生成一系列连续的数值,而无需创建临时表或循环。

GENERATE_SERIES ( start, stop [, step])
start:序列的起始值。
stop:序列的终止值。
step:每次递增或递减的步长(可选)。如果省略,默认为1。

使用场景包括快速生成一系列数据用于测试或填充表或者结合日期函数生成一系列日期值。

示例

生成的结果集将包含 20 行,每行显示从 '2019-02-28 13:45:23' 开始,按分钟递增的时间。

SELECT DATEADD(MINUTE, s.value, '2019-02-28 13:45:23') AS [Interval]
FROM GENERATE_SERIES(0, 20, 1) ASs;

对于每一个 s.value,DATEADD 函数将基准日期时间增加相应的分钟数。


DATE_BUCKET函数

SQL Server 2022 引入了一个新的函数 DATE_BUCKET,用于将日期时间值按指定的时间间隔分组(即分桶)。

这个函数在时间序列分析、数据聚合和分段分析等场景中非常有用。

DATE_BUCKET ( bucket_width, datepart, startdate, date )
bucket_width:时间间隔的大小,可以是整数。
datepart:时间间隔的类型,例如 year, month, day, hour, minute, second 等。
startdate:起始日期,用于定义时间间隔的起点。
date:需要分组的日期时间值。


使用 DATE_BUCKET 函数时,指定的时间间隔单位(如 YEAR、QUARTER、MONTH、WEEK 等)以及起始日期(origin)决定了日期时间值被分配到哪个存储桶。这种方式有助于理解时间间隔的计算是如何基于起始日期来进行的。

示例

DECLARE @date DATETIME = '2019-09-28 13:45:23';DECLARE @origin DATETIME = '2019-01-28 13:45:23';SELECT 'Now' AS [BucketName], @date AS [DateBucketValue]
UNION ALL
SELECT 'Year', DATE_BUCKET (YEAR, 1, @date, @origin)UNION ALL
SELECT 'Quarter', DATE_BUCKET (QUARTER, 1, @date, @origin)UNION ALL
SELECT 'Month', DATE_BUCKET (MONTH, 1, @date, @origin)UNION ALL
SELECT 'Week', DATE_BUCKET (WEEK, 1, @date, @origin)


--假如日期时间值如下:
Now:
2019-09-28 13:45:23--按年分组:
DATE_BUCKET(
YEAR, 1, @date, @origin)
2019-01-28 13:45:23 开始的年度存储桶,2019-09-28 落入 2019-01-282020-01-28的存储桶中。
结果:
2019-01-28 13:45:23--按季度分组:
DATE_BUCKET(QUARTER,
1, @date, @origin)
2019-01-28 13:45:23 开始的季度存储桶,每个季度 3个月。2019-09-28 落入第三个季度存储桶(即从 2019-07-28 13:45:232019-10-28 13:45:23)。
结果:
2019-07-28 13:45:23--按月分组:
DATE_BUCKET(
MONTH, 1, @date, @origin)
2019-01-28 13:45:23开始的月度存储桶,每个月一个存储桶。2019-09-28 落入第九个存储桶(即从 2019-09-28 13:45:232019-10-28 13:45:23)。
结果:
2019-09-28 13:45:23--按周分组:
DATE_BUCKET(WEEK,
1, @date, @origin)
2019-01-28 13:45:23开始的每周存储桶。2019-09-28 落入从 2019-09-23 13:45:232019-09-30 13:45:23的存储桶。
结果:
2019-09-23 13:45:23

SELECT 'Now' AS [BucketName], GETDATE() AS [BucketDate]
UNION ALL
SELECT '5 Minute Buckets', DATE_BUCKET (MINUTE, 5, GETDATE())UNION ALL
SELECT 'Quarter Hour', DATE_BUCKET (MINUTE, 15, GETDATE());



Now:
BucketName: Now
BucketDate:
2024-07-26 16:14:11.030这是当前时间,即GETDATE() 返回的系统当前时间。5Minute Buckets:
BucketName:
5Minute Buckets
BucketDate:
2024-07-26 16:10:00.000这是将当前时间按5 分钟间隔进行分组的结果。DATE_BUCKET(MINUTE, 5, GETDATE()) 返回当前时间所在的 5 分钟区间的起始时间。在这个例子中,16:14:11 落在 16:10:0016:15:00 之间,因此返回 16:10:00



Quarter Hour:
BucketName: Quarter Hour
BucketDate:
2024-07-26 16:00:00.000这是将当前时间按15 分钟间隔进行分组的结果。DATE_BUCKET(MINUTE, 15, GETDATE()) 返回当前时间所在的 15 分钟区间的起始时间。在这个例子中,16:14:11 落在 16:00:0016:15:00 之间,因此返回 16:00:00

更多实际场景示例

按自定义起始日期分组
假设我们有一系列事件时间 EventTime,希望从'2023-01-01'日期开始,每周进行分组统计事件数量。

--创建表 Events:

USE [testdb]
GO

CREATE TABLEEvents (
EventID
INT PRIMARY KEY,
EventTime
DATETIME);INSERT INTO Events (EventID, EventTime) VALUES(1, '2023-01-02 14:30:00'),
(
2, '2023-01-08 09:15:00'),
(
3, '2023-01-09 17:45:00'),
(
4, '2023-01-15 12:00:00'),
(
5, '2023-01-16 08:00:00'),
(
6, '2023-01-22 19:30:00'),
(
7, '2023-01-29 11:00:00');--从'2023-01-01'起始日期开始,每周进行分组统计事件数量。 DECLARE @origin DATETIME = '2023-01-01';SELECTDATE_BUCKET(WEEK,1, EventTime, @origin) ASWeekStart,COUNT(*) ASEventCountFROMEventsGROUP BYDATE_BUCKET(WEEK,1, EventTime, @origin)ORDER BYWeekStart;


按自定义时间间隔分组
假设我们有一个传感器数据表 SensorReadings

USE [testdb]
GO


CREATE TABLESensorReadings (
ReadingID
INT PRIMARY KEY, --唯一标识 ReadingTime DATETIME, --读数的时间 Value FLOAT --读数的值 );INSERT INTO SensorReadings (ReadingID, ReadingTime, Value) VALUES(1, '2023-07-26 10:03:00', 23.5),
(
2, '2023-07-26 10:05:00', 24.1),
(
3, '2023-07-26 10:09:00', 22.8),
(
4, '2023-07-26 10:15:00', 25.0),
(
5, '2023-07-26 10:20:00', 23.9),
(
6, '2023-07-26 10:27:00', 24.3),
(
7, '2023-07-26 10:29:00', 24.5);--我们希望按 10 分钟的间隔将数据分组,并计算每个间隔的平均读数值。 SELECTDATE_BUCKET(MINUTE,10, ReadingTime) ASBucketStartTime,ROUND(AVG(Value),4) ASAverageValueFROMSensorReadingsGROUP BYDATE_BUCKET(MINUTE,10, ReadingTime)ORDER BYBucketStartTime;

如果是传统方法需要使用公用表表达式CTE才能完成这个需求

    
--查询:按 10 分钟间隔分组并计算平均值
WITH TimeIntervals AS(SELECTReadingID,
ReadingTime,
Value,
--将分钟数归约到最近的 10 分钟的整数倍, 从2010年到现在有多少个10分钟区间 DATEADD(MINUTE, (DATEDIFF(MINUTE, '2000-01-01', ReadingTime) / 10) * 10, '2010-01-01') ASBucketStartTimeFROMSensorReadings
)
SELECTBucketStartTime,ROUND(AVG(Value), 4) ASAverageValueFROMTimeIntervalsGROUP BYBucketStartTimeORDER BYBucketStartTime;


WITH TimeIntervals AS (...)公共表表达式(CTE)用于计算每条记录的 BucketStartTime。
DATEDIFF(MINUTE, '2000-01-01', ReadingTime) / 10 计算 ReadingTime 到基准时间 '2000-01-01' 的分钟数,然后除以 10,得到当前时间点所在的 10 分钟区间的索引。
DATEADD(MINUTE, ..., '2000-01-01') 将该索引转换回具体的时间点,即区间的起始时间。


查询主部分:
选择 BucketStartTime 和相应区间内读数值的平均值。
使用 GROUP BY 按 BucketStartTime 分组,并计算每个分组的平均值。
ORDER BY 用于按照时间顺序排列结果。


FIRST_VALUE 和 LAST_VALUE 窗口函数

在 之前版本的SQL Server 中,FIRST_VALUE 和 LAST_VALUE 是窗口函数,用于在一个分区或窗口中返回第一个或最后一个值。

SQL Server 2022 引入了新的选项 IGNORE NULLS 和 RESPECT NULLS 来处理空值(NULL)的方式,从而增强了这些函数的功能。

基本语法

FIRST_VALUE
返回指定窗口或分区中按指定顺序的第一个值。
FIRST_VALUE (
[scalar_expression])OVER ( [partition_by_clause] order_by_clause [rows_range_clause])



LAST_VALUE
返回指定窗口或分区中按指定顺序的最后一个值。
LAST_VALUE (
[scalar_expression])OVER ( [partition_by_clause] order_by_clause [rows_range_clause])



新功能:IGNORE NULLS 和 RESPECT NULLS
IGNORE NULLS: 忽略分区或窗口中的
NULL值。
RESPECT NULLS: 默认行为,包含分区或窗口中的
NULL 值。

示例

假设我们有一个表 MachineTelemetry,包含以下数据:

CREATE TABLEMachineTelemetry ([timestamp] DATETIME,
SensorReading
FLOAT);INSERT INTO MachineTelemetry ([timestamp], SensorReading) VALUES('2023-07-26 10:00:00', 23.5),
(
'2023-07-26 10:00:15', 24.1),
(
'2023-07-26 10:00:30', NULL),
(
'2023-07-26 10:00:45', 25.0),
(
'2023-07-26 10:01:00', NULL),
(
'2023-07-26 10:01:15', 23.9),
(
'2023-07-26 10:01:30', NULL),
(
'2023-07-26 10:01:45', 24.3);


默认行为(包含 NULL 值)

--使用 FIRST_VALUE 和 LAST_VALUE 进行差距分析--默认行为(包含 NULL 值)
SELECT 
    [timestamp],
DATE_BUCKET(MINUTE,
1, [timestamp]) AS [timestamp_bucket],
SensorReading,
FIRST_VALUE(SensorReading)
OVER(
PARTITION
BY DATE_BUCKET(MINUTE, 1, [timestamp])ORDER BY [timestamp]ROWSBETWEEN UNBOUNDED PRECEDING ANDUNBOUNDED FOLLOWING
)
AS [Default_FIRST_VALUE (RESPECT NULLS)],
LAST_VALUE(SensorReading)
OVER(
PARTITION
BY DATE_BUCKET(MINUTE, 1, [timestamp])ORDER BY [timestamp]ROWSBETWEEN UNBOUNDED PRECEDING ANDUNBOUNDED FOLLOWING
)
AS [Default_LAST_VALUE (RESPECT NULLS)] FROMMachineTelemetryORDER BY [timestamp];

忽略 NULL 值

--忽略 NULL 值
SELECT 
    [timestamp],
DATE_BUCKET(MINUTE,
1, [timestamp]) AS [timestamp_bucket],
SensorReading,
FIRST_VALUE(SensorReading) IGNORE NULLS
OVER(
PARTITION
BY DATE_BUCKET(MINUTE, 1, [timestamp])ORDER BY [timestamp]ROWSBETWEEN UNBOUNDED PRECEDING ANDUNBOUNDED FOLLOWING
)
AS [First_Reading (IGNORE NULLS)],
LAST_VALUE(SensorReading) IGNORE NULLS
OVER(
PARTITION
BY DATE_BUCKET(MINUTE, 1, [timestamp])ORDER BY [timestamp]ROWSBETWEEN UNBOUNDED PRECEDING ANDUNBOUNDED FOLLOWING
)
AS [Last_Reading (IGNORE NULLS)] FROMMachineTelemetryORDER BY [timestamp];


总结

实际上,对于时间序列我们一般使用专业的时间序列数据库,例如InfluxDB 。

它使用 TSM(Time-Structured Merge Tree)作为存储引擎称,这是 LSM 树的一种变体,专门优化用于时间序列数据的写入和查询性能。

另外,SQL Server 的时间序列功能是使用行存储引擎(Row Store)作为其存储引擎,这意味着数据是按行进行存储和处理的。

在大部分场景下面,如果性能不是要求非常高,其实SQL Server 存储时间序列数据性能是完全足够的,而且额外使用InfluxDB数据库需要维护多一个技术栈,对运维要求更加高。

特别是现在追求数据库一体化的趋势背景下,无论是时间序列数据,向量数据,地理数据,json数据都最好在一个数据库里全部满足,减轻运维负担,复用技术栈,减少重复建设成本是比较好的解决方案。

参考文章

https://sqlbits.com/sessions/event2024/Time_Series_with_SQL_Server_2022

https://www.microsoft.com/en-us/sql-server/blog/2023/01/12/working-with-time-series-data-in-sql-server-2022-and-azure-sql/

https://www.mssqltips.com/sqlservertip/6232/load-time-series-data-with-sql-server/


本文版权归作者所有,未经作者同意不得转载。

两种方法定义在Django模型中的
save
方法有不同的参数处理方式。

第一种方法:

def save(self, *args, **kwargs):
super().save(
*args, **kwargs)

特点:

  • 使用
    *args

    **kwargs
    来捕获所有位置参数和关键字参数。
  • 这样的方法可以灵活地接收任何传递给
    save
    方法的参数,并将它们传递给父类的
    save
    方法。
  • 适用于需要在保存模型实例时捕获和处理所有可能的参数场景。

第二种方法:

def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
super().save(force_insert
=force_insert, force_update=force_update, using=using, update_fields=update_fields)

特点:

  • 明确列出
    save
    方法的所有参数,并为每个参数提供默认值。
  • 参数列表包括
    force_insert

    force_update

    using

    update_fields
    ,这些是Django模型
    save
    方法常见的参数。
  • 这种方法定义更清晰,并且对于需要传递特定参数的调用者更加直观。

总结:

  • 第一种方法更灵活,可以接收和传递任意数量和类型的参数。
  • 第二种方法更明确,适合在需要使用特定参数时提供清晰的接口。

在选择使用哪种方法时,考虑到代码的可读性和未来的维护性是很重要的。如果你不需要捕获所有参数,通常推荐使用第二种方法,因为它更加清晰和明确。

在Django模型中,
save
方法有许多常见的用法和扩展。以下是一些常见的用法和示例:

1. 自动填充字段

在保存模型实例时,自动填充或修改某些字段的值。

from django.db importmodelsfrom django.utils importtimezoneclassMyModel(models.Model):
name
= models.CharField(max_length=100)
created_at
= models.DateTimeField(editable=False)
updated_at
=models.DateTimeField()def save(self, *args, **kwargs):if notself.id:
self.created_at
=timezone.now()
self.updated_at
=timezone.now()
super().save(
*args, **kwargs)

2. 数据验证

在保存之前对数据进行自定义验证。

classMyModel(models.Model):
name
= models.CharField(max_length=100)
age
=models.IntegerField()def save(self, *args, **kwargs):if self.age <0:raise ValueError("Age cannot be negative")
super().save(
*args, **kwargs)

3. 创建关联对象

在保存模型实例时,创建或更新关联对象。

classProfile(models.Model):
user
= models.OneToOneField(User, on_delete=models.CASCADE)
bio
=models.TextField()classMyModel(models.Model):
user
= models.ForeignKey(User, on_delete=models.CASCADE)
name
= models.CharField(max_length=100)def save(self, *args, **kwargs):
super().save(
*args, **kwargs)
Profile.objects.get_or_create(user
=self.user)

4. 条件保存

根据特定条件决定是否调用父类的
save
方法。

classMyModel(models.Model):
name
= models.CharField(max_length=100)
is_active
= models.BooleanField(default=True)def save(self, *args, **kwargs):ifself.is_active:
super().save(
*args, **kwargs)else:raise ValueError("Inactive objects cannot be saved")

5. 防止重复保存

防止对象在某些情况下被多次保存。

classMyModel(models.Model):
name
= models.CharField(max_length=100)
counter
= models.IntegerField(default=0)def save(self, *args, **kwargs):if self.counter ==0:
super().save(
*args, **kwargs)else:raise ValueError("Object has already been saved")

6. 发送信号或触发其他操作

在保存模型实例时,发送信号或触发其他操作。

from django.db.models.signals importpost_savefrom django.dispatch importreceiverclassMyModel(models.Model):
name
= models.CharField(max_length=100)def save(self, *args, **kwargs):
super().save(
*args, **kwargs)#触发某些操作,例如发送信号 post_save.send(sender=self.__class__, instance=self)

@receiver(post_save, sender
=MyModel)def post_save_handler(sender, instance, **kwargs):print(f"Instance of {sender} saved with name: {instance.name}")

这些示例展示了如何在自定义的
save
方法中扩展和增强Django模型的保存逻辑。根据具体需求,你可以组合和调整这些技术来实现更复杂的功能。

前言

​ 为什么配置这样的一个环境呢?鄙人受够了Keil5那个简陋的工作环境了,实在是用不下去,调试上很容易跟CubeMX的代码产生不协调导致调试——发布代码不一致造成的一系列问题。CubeIDE虽说不错,但是它的代码辅助功能和构建系统实在不敢恭维,经常出现Makefile未同步导致符号定义冲突,亦或者是埋下了潜在的程序bug。

​ 也有人尝试使用Keil Assistance + VSCode + Keil分工写代码与烧录,这听起来不错,但是调试并不方便,常常要来回奔波,实在是有些麻烦。这里我们尝试新兴的PlatformIO来辅助我们进行嵌入式的开发。

介绍

PlatformIO

Your Gateway to Embedded Software Development Excellence — PlatformIO latest documentation

​ Platform是基于VScode文本编辑器,使用了vscode强大的扩展extension功能,使得开发者可以在vscode中直接调用gcc、jlink、gdb等进行开发、调试。PlatformIO只是一个集成开发环境,其本身几乎不包括任何实质性功能,但是其集成了很多了例如编译器、调试器等,主要包括以下:

  • vscode
  • gcc编译器,版本:arm-none-eabi-gcc (针对arm内核嵌入式设备的专属c语言编译器)
  • arm-none-eabi-gdb调试器
  • jlink 驱动
  • Scons构建工具、

​ 也就是说,PlatformIO就是一个类似于Keil, STMCudeIDE那样的工作环境,它本身不负责进行编译,上传,调试测试等,但是它封装了我们常见的arm-noeabi-系列工具,省去我们学习纷繁复杂的配置语法,统一成PlatformIO的配置语言,这样我们就可以通过写PlatformIO的配置语言来辅助构建我们的嵌入式程序。

STM32CubeMX

STM32CubeMX - STM32Cube initialization code generator - STMicroelectronics

​ STM32CubeMX 是用于 32 位 ARM Cortex STM32 微控制器的图形工具。它允许对 STM32 微控制器和微处理器进行非常简单的配置,生成初始化 C 代码,还可以通过一步一步的操作为 Arm Cortex-M 内核或 Arm Cortex-A core 生成部分 Linux 设备树。它是 STMCube 生态系统的一部分,且可以作为独立应用程序或作为 Eclipse 插件集成在开发环境(STM32CubeIDE)中。
​ 它能做到的事情很简单了:那就是将我们配置嵌入式设备的流程从代码配置走向GUI配置,点点摁扭,就可以生成一系列初始化的代码辅助我们完成繁琐的初始化任务

Visual Studio Code

Visual Studio Code - Code Editing. Redefined

Visual Studio Code 是一个轻量级功能强大的源代码编辑器,支持语法高亮、代码自动补全(又称 IntelliSense)、代码重构、查看定义功能,并且内置了命令行工具和 Git 版本控制系统。适用于 Windows、macOS 和 Linux。它内置了对 JavaScript、TypeScript 和 Node.js 的支持,并为其他语言和运行时(如 C++、C#、Java、Python、PHP、Go、.NET)提供了丰富的扩展生态系统。为了不影响读者的沉浸式阅读学习,如需使用目录请在左侧使用即可。

正文

​ 我们下面来以STM32F103ZET6为目标单片机,实现一个简单的点灯任务!来看看我们如何进行工程配置。

使用STM32CubeMX生成PlatformIO可以支持的Makefile文件

​ 我们的初始化代码是由STM32CubeMX生成的,这里我们完成这些配置:

关于如何下载安装STM32CubeMX不是本文的重点,请看官另行百度

点击画圈的地方选择目标嵌入式设备

锁定目标板子(↓)

这里我收藏了常用的板子,所以可以很快找到,没有的看官搜索板子的型号就好

点击目标板子的栏目,就看到我们熟悉的界面了。

1. 选择目标引脚并配置

​ 我们需要选择目标引脚然后完成相关的配置,笔者这里选择了我这块开发板上的PE5引脚来驱动连接上的单片机,引脚的配置需要结合硬件电路进行选择

​ 在这里,本人设置了:上拉输出,高速模式

2. 配置时钟树

​ 我选择了使用外部晶振时钟,配置为72MHz

3.
[关键!调试则需要:]
选择调试器类型

​ Serial Wire是笔者选择的,我的手头只有STLink可以使用,如果看官有的是JLink调试器,请根据

STM32-HAL库CubeMX中的SYS配置选项_cubmx不配置sys debug-CSDN博客

​ 做出相关的选择!这一步相当关键!

4. 选择对应的目标IDE配置代码与文件

​ 看官自行
设置好项目名称

项目存储的文件夹
,两者将会在后面的PIO配置中使用到!请高度关注!

TIPS: 如果看官不想拷贝所有的库文件,可以在Code Generator那一栏中选择相应的配置:

下面我们生成代码


​ 可以看到所有的必要底层库文件都得到了拷贝,下面我们转战VSCode + PIO

使用PlatformIO搭建我们的STM32开发环境

​ 打开VSCode,PlatformIO在Plugin Market里下载。

​ 看官先点击下载之,泡上一杯咖啡吧,下载很慢的!(笔者这里下载过了)安装结束之后,你的左侧工具栏会出现这个

​ 点击一下:

​ 这就是PlatformIO!我们下面可以将我们的STM32生成的代码跟PIO对接。点击新建工程:

里面需要填写的项目我一个个说:

  1. 工程名称:跟CubeMx你填写的项目名称要完全一致
  2. 目标板子:跟CubeMx型号一致
  3. 框架:这个是说的是如何进行读取行为,我们使用的是CubeMX的生成代码,所以目标框架我们采用的是STM32Cube
  4. 位置Location:先把Default Location给删掉,我们需要选择在刚刚在CubeMx下选择的文件夹,可以看到我们在刚刚笔者选择在了
    D:\MDK5 Project\CubeMx Trainingg\demo_
    下,所有的代码都继续生成在了
    D:\MDK5 Project\CubeMx Trainingg\demo_\${ProjectName}\
    下方,
    我们在PIO下的目录就选择到
    D:\MDK5 Project\CubeMx Trainingg\demo_
    这个位置

    ,这是因为他也会生成
    ${ProjectName}\...
    文件夹,我们这样做是为了让PIO生成的文件和CubeMx的文件生成在用以项目根目录下,方便我们的书写和文件读取

​ 点击Finish到这里之后,第一次下载的朋友将会下载相关的工具链,笔者当时下载了长达小半个小时,请各位看官稍有耐心,可以出去散散步!之后等到工具链配置结束之后,生成工程就是一刹那的事情。

​ 现在生成结束了!

​ 我们下面来看看platformio.ini文件。

; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html


[env:genericSTM32F103ZE]
platform = ststm32
board = genericSTM32F103ZE
framework = stm32cube

​ 这是PlatformIO为我们默认生成好的模板,但是仅靠这个完全没有办法编译我们的项目,我们需要自行修改。

​ 经过笔者的简单探索,笔者整合出了如下的配置:

使用笔者的配置

; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html


; src_dir就是告知我们的项目的大源文件地址在何处,它隶属于platformio模块的匹配
; https://docs.platformio.org/en/latest/projectconf/sections/platformio/options/directory/src_dir.html
[platformio]
src_dir = ./Core/

;目标板子: STM32F103ZE
[env:genericSTM32F103ZE]
platform = ststm32
board = genericSTM32F103ZE
framework = stm32cube

; -Idir是include文件目录, 这里随了GCC的语法
; build_flag本质上就是给arm-noeabi-gcc加上了编译参数,直接添加的,所以直接随的是GCC的语法
build_flags =     
    -ICore/Inc    

;选择链接文件,我们的STM32上电后要执行一段启动脚本
board_build.ldscript = ./STM32F103ZETx_FLASH.ld

; 下面是上传工具的配置
; PlatformIO默认使用的上传协议就是stlink,默认的调试协议还是stlink, 这是笔者并没有写出的原因
; 如果看官手头有的调试器是JLink的话,则需要显示的指定: [看官自行注释]
; upload_protocol=jlink
; debug_tool=jlink

; 手头只有USB < - > TTL的话, 则是
; upload_protocol=serial

; 值得注意的是: 一些板子是不支持某一些协议的,这跟PlatformIO使用到的开源库有关系,请前往
; https://docs.platformio.org/en/latest/boards/index.html#boards
; 搜索自己使用的板子查看支持的上传,调试协议

​ 还有另一份是广为传播的一种配置:

; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

; 在配置文件中分号开头为注释
[platformio]
; 将源代码的编译目录移到项目目录下,否则默认src下
src_dir = ./

[env:genericSTM32F103ZE]
platform = ststm32
board = genericSTM32F103ZE
; framework = stm32cube(不用framework了)
; 编译配置-D是宏定义,-Idir是include文件目录,读者可按自己项目结构更改
; 这里笔者锐评一下: 这种方式就是会十分繁琐!必须依次指定所有的包含目录!
; 在已经存在支持的框架下,请优先使用platformIO已经支持的框架!
build_flags =         
  -D STM32F103xE	; 预定义宏, 看官可以理解为在一切源文件的开头加上了#define STM32F103xE
  -ICore/Inc		; 包含了源文件的路径
  -IDrivers/CMSIS/Include
  -IDrivers/CMSIS/Device/ST/STM32F1xx/Include
  -IDrivers/STM32F1xx_HAL_Driver/Inc
  -IDrivers/STM32F1xx_HAL_Driver/Inc/Legacy

;选择编译文件的所在路径,这里包含了源文件路径,启动文件,驱动库和rtos路径。如果+<*>便是路径下的所以文件,-<.git/>便是忽略.git路径下的文件
build_src_filter = +<Core/Src> +<startup_stm32f103xe.s> +<Drivers/> +<Middlewares/>
;选择链接文件
board_build.ldscript = ./STM32F103ZETx_FLASH.ld

​ 下面我们直接点灯就好,在Core/Src/main.c下:

  // 笔者选择的是上拉模式,亮灯要拉低GPIO电平
  HAL_GPIO_WritePin(LED1_GPIO_Port, LED1_Pin, GPIO_PIN_RESET);
  HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);

​ 我们下面编译运行,
您既可以Ctrl + Shift + P输入PlatformIO:Build
开始构建,也可以的点击VSCode下方工具栏上的:

从小房子的图标开始,依次是:

  • PlatformIO主页面
  • 构建
  • 上传
  • 清理
  • 测试(单元测试)
  • 串口监视器
  • 终端
  • 项目
  • 上传的端口设置(默认不动就好)

​ 余下的是笔者写其他语言的插件,请不予理会。

​ 笔者下面演示的是构建工程,点击

Processing genericSTM32F103ZE (platform: ststm32; board: genericSTM32F103ZE; framework: stm32cube)
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/ststm32/genericSTM32F103ZE.html
PLATFORM: ST STM32 (17.4.0) > STM32F103ZE (64k RAM. 512k Flash)
HARDWARE: STM32F103ZET6 72MHz, 64KB RAM, 512KB Flash
DEBUG: Current (blackmagic) External (blackmagic, cmsis-dap, jlink, stlink)
PACKAGES:
 - framework-stm32cubef1 @ 1.8.4
 - tool-ldscripts-ststm32 @ 0.2.0
 - toolchain-gccarmnoneeabi @ 1.70201.0 (7.2.1)
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 29 compatible libraries
Scanning dependencies...
No dependencies
Building in release mode
Compiling .pio\build\genericSTM32F103ZE\FrameworkHALDriver\Src\stm32f1xx_hal.o
... 
Compiling .pio\build\genericSTM32F103ZE\FrameworkCMSISDevice\gcc\startup_stm32f103xe.o
Compiling .pio\build\genericSTM32F103ZE\FrameworkCMSISDevice\system_stm32f1xx.o
Archiving .pio\build\genericSTM32F103ZE\libFrameworkCMSISDevice.a
Indexing .pio\build\genericSTM32F103ZE\libFrameworkCMSISDevice.a
Linking .pio\build\genericSTM32F103ZE\firmware.elf
Checking size .pio\build\genericSTM32F103ZE\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [          ]   0.1% (used 44 bytes from 65536 bytes)
Flash: [          ]   0.5% (used 2724 bytes from 524288 bytes)
Building .pio\build\genericSTM32F103ZE\firmware.bin
================================================================= [SUCCESS] Took 4.93 seconds =================================================================

​ 出现
[SUCCESS]
字样,说明我们的项目构建顺利,否则,请依次查看自己的项目配置问题。常见的是重定义,请保证自己不要重复包含源文件导致符号重定义!

​ 下面我们点击
->
上传项目到板子上!

​ 请在上传之前,确保您已经连接好调试器(我的是STLink),同时板子已经上好电了!

Processing genericSTM32F103ZE (platform: ststm32; board: genericSTM32F103ZE; framework: stm32cube)
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/ststm32/genericSTM32F103ZE.html
PLATFORM: ST STM32 (17.4.0) > STM32F103ZE (64k RAM. 512k Flash)
HARDWARE: STM32F103ZET6 72MHz, 64KB RAM, 512KB Flash
DEBUG: Current (blackmagic) External (blackmagic, cmsis-dap, jlink, stlink)
PACKAGES:
 - framework-stm32cubef1 @ 1.8.4
 - tool-dfuutil @ 1.11.0
 - tool-dfuutil-arduino @ 1.11.0
 - tool-ldscripts-ststm32 @ 0.2.0
 - tool-openocd @ 3.1200.0 (12.0)
 - tool-stm32duino @ 1.0.2
 - toolchain-gccarmnoneeabi @ 1.70201.0 (7.2.1)
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 29 compatible libraries
Scanning dependencies...
No dependencies
Building in release mode
Checking size .pio\build\genericSTM32F103ZE\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [          ]   0.1% (used 44 bytes from 65536 bytes)
Flash: [          ]   0.5% (used 2724 bytes from 524288 bytes)
Configuring upload protocol...
AVAILABLE: blackmagic, cmsis-dap, dfu, jlink, serial, stlink
CURRENT: upload_protocol = stlink
Uploading .pio\build\genericSTM32F103ZE\firmware.elf
xPack Open On-Chip Debugger 0.12.0-01004-g9ea7f3d64-dirty (2023-01-30-15:04)
Licensed under GNU GPL v2
For bug reports, read
        http://openocd.org/doc/doxygen/bugs.html
debug_level: 1

hla_swd
[stm32f1x.cpu] halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08000bf8 msp: 0x20010000
** Programming Started **
Warn : Adding extra erase range, 0x08000c90 .. 0x08000fff
** Programming Finished **
** Verify Started **
** Verified OK **
** Resetting Target **
shutdown command invoked
================================================================= [SUCCESS] Took 2.75 seconds =================================================================

​ 如愿,小灯亮起,到这里,我们的教程结束!

一些在上传中可能出现的问题

  1. 板子没上电↓

  1. STLink没插好!或者STLink异常,导致OpenOCD无法初始化,请检查端口

  1. 使用USB转TTL接口上传程序出现的问题

​ 感谢SourceForge大佬的解答,我随后马上意识到自己犯了一件很蠢的事情,我忘记改板子为bootloader烧录模式了,导致板子根本不接受usb烧录。

stm32flash / Tickets / #134 STM32L462 "Failed to init device" problem (sourceforge.net)

​ 请各位看官参考自己的手册关于BOOT引脚的配置!

​ 所以,我需要改BOOT0引脚接到3.3V上去,上载好程序后,恢复BOOT引脚全部接地运行程序,果然:

Processing genericSTM32F103ZE (platform: ststm32; board: genericSTM32F103ZE; framework: stm32cube)
----------------------------------------------------------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/ststm32/genericSTM32F103ZE.html
PLATFORM: ST STM32 (17.4.0) > STM32F103ZE (64k RAM. 512k Flash)
HARDWARE: STM32F103ZET6 72MHz, 64KB RAM, 512KB Flash
DEBUG: Current (blackmagic) External (blackmagic, cmsis-dap, jlink, stlink)
PACKAGES:
 - framework-stm32cubef1 @ 1.8.4
 - tool-dfuutil @ 1.11.0
 - tool-dfuutil-arduino @ 1.11.0
 - tool-ldscripts-ststm32 @ 0.2.0
 - tool-openocd @ 3.1200.0 (12.0)
 - tool-stm32duino @ 1.0.2
 - toolchain-gccarmnoneeabi @ 1.70201.0 (7.2.1)
LDF: Library Dependency Finder -> https://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 29 compatible libraries
Scanning dependencies...
No dependencies
Building in release mode
Checking size .pio\build\genericSTM32F103ZE\firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [          ]   0.1% (used 44 bytes from 65536 bytes)
Flash: [          ]   0.5% (used 2716 bytes from 524288 bytes)
Configuring upload protocol...
AVAILABLE: blackmagic, cmsis-dap, dfu, jlink, serial, stlink
CURRENT: upload_protocol = serial
Looking for upload port...
Auto-detected: COM8
Uploading .pio\build\genericSTM32F103ZE\firmware.bin
stm32flash 0.4

http://stm32flash.googlecode.com/

Using Parser : Raw BINARY
Interface serial_w32: 115200 8E1
Version      : 0x22
Option 1     : 0x00
Option 2     : 0x00
Device ID    : 0x0414 (High-density)
- RAM        : 64KiB  (512b reserved by bootloader)
- Flash      : 512KiB (sector size: 2x2048)
- Option RAM : 16b
- System RAM : 2KiB
Write to memory
Erasing memory

Wrote address 0x08000100 (7.98%) 
Wrote address 0x08000200 (15.96%) 
...
Wrote address 0x08000b00 (87.78%) 
Wrote address 0x08000c00 (95.76%) 
Wrote address 0x08000c88 (100.00%) Done.

Starting execution at address 0x08000000... done.

调试

​ 这里请只有串口烧录的朋友止步了。

​ 调试极其容易,我们只需要按照经典的点击:

​ 确保自己的板子和调试器都上好电之后,就可以愉快调试了!

本文介绍在
Linux
服务器中,通过
PBS
(Portable Batch System)作业管理系统脚本的方式,提交任务到
服务器
队列,并执行任务的方法。

最近,需要在学校公用的超算中执行代码任务;而和多数超算设备一样,其也是需要通过作业队列的方式,来提交、管理、排序不同用户的任务,从而使得不同用户都可以较为公平地使用超算设备的资源。由于学校的这个超算是基于
PBS
来提交任务的,所以这里就介绍一下撰写
PBS
脚本,从而将自己的
代码执行需求
提交给
服务器
(也就是提交任务)的方法。

其中,
PBS
(Portable Batch System)是一个用于管理、调度计算任务的开源软件;其是一个常用的作业调度系统,用于在大规模计算集群或超级计算机上管理并分配计算资源。在使用时,我们需要首先提交作业到计算集群,
PBS
将会根据资源可用性、作业优先级等因素进行作业调度和分配。其基本工作流程如下:

  • 用户编写
    PBS
    脚本,描述任务的资源需求、执行命令和其他相关信息。
  • 用户使用
    PBS
    命令,将上述编写好的脚本提交到
    PBS
    系统。
  • PBS
    系统根据脚本中作业的资源需求和集群的可用资源情况,将作业放入作业队列中等待执行。
  • 当有可用的计算资源时,
    PBS
    系统会选择一个作业并将其分配给相应的计算节点。
  • 作业在计算节点上执行,直到完成,或达到预设的运行时间限制,或任务执行时出错等。

那么接下来,就介绍一下撰写
PBS
脚本,并基于其提交自己的任务到服务器中的方法。

首先明确一下本文的需求。已知当前在服务器的某个路径下,我们有一个可执行文件(或者是有
1

Python
代码文件);我们希望后续在超算中,对这个可执行文件(或者
Python
代码文件)加以运行。

明确了需求,接下来就可以开始操作。首先,如果有需要,我们可以
cd
进入自己的工作目录。我这里就直接进入存放有可执行文件的目录中;具体代码如下。

cd Data_Reflectance_Rec

随后,基于如下代码查看一下当前路径中的文件。
ls
用于列出目录中的文件和子目录。

ls

接下来,基于如下代码创建
PBS
脚本,我在这里将其命名为
py_task.pbs
;其中,
.pbs
就是
PBS
脚本文件的固定拓展名。后续我们向
PBS
系统提交任务时所用的脚本,就是这个文件。

touch py_task.pbs

其中,
touch
是一个常用的命令,用于创建空白文件或更新已存在文件的访问和修改时间戳。创建完毕后,可以基于如下代码再次看一下当前路径下的文件。

ls

执行上述代码,如下图所示。可以看到,
py_task.pbs
这个
PBS
脚本文件已经创建完毕了。

创建脚本文件完毕后,我们即可开始编辑这个文件。在这里,我选择基于
Vim
来编辑,所以执行如下的代码即可。

vim py_task.pbs

其中,
Vim
是一个强大的文本编辑器,广泛用于命令行环境下代码的编写和文本的编辑。执行上述代码,如下图所示。可以看到,
py_task.pbs
这个
PBS
脚本文件已经被
Vim
打开了。

接下来,按下
i
键,进入文本编辑状态;如下图所示。

随后,即可在
Vim
中编辑
PBS
脚本文件。在这里,我们给出
2

PBS
脚本文件的模板;其中,第
1
个模板如下所示。

#!/bin/bash
#PBS -N py_task
#PBS -q rtlab1_4
#PBS -l nodes=1:ppn=4
#PBS -l walltime=00:30:00
#PBS -o /data1/home/LiliAircas/Data_Reflectance_Rec/task/py_task.out
#PBS -e /data1/home/LiliAircas/Data_Reflectance_Rec/task/py_task.err
hostname
date "+%Y/%m/%d %H:%M:%S"
python /data1/home/LiliAircas/Data_Reflectance_Rec/code/Alignment.py
date "+%Y/%m/%d %H:%M:%S"
最后注意记得留一个空行

其中,第
1
行是一个
shebang
(也称为
hashbang
)行,指定了用于解释该脚本的解释器。在这里,
/bin/bash
表示该脚本将由
Bash
解释器执行。

接下来,从第
2
行开始的这些
#
开头的语句,不是注释,而是
PBS
作业调度系统的作业指令。这些指令以
#PBS
开头,指定了不同的选项:
-N py_task
表示作业的名称为
py_task

-q rtlab1_4
表示将作业提交到
rtlab1_4
队列中;
-l nodes=1:ppn=4
表示指定使用
1
个节点(node)和
4
个处理器(processor)来运行作业;
-l walltime=00:30:00
表示作业的最长运行时间为
30
分钟。随后的
2
行代码,分别指定了作业的
标准输出

错误输出
所在的文件。

紧接着,随后的
2
行分别输出当前执行脚本的主机名和当前的日期时间;随后,就开始调用
Python
解释器执行
Alignment.py
这个
Python
代码文件了。最后,再次输出当前的日期时间,从而使得我们可以基于其大概了解到任务的执行时长。

最后的空行,有的教程说是为了符合脚本文件的规范,提供可读性和结构上的清晰性;也有教程说,有些版本的服务器上如果不加这个空行,会导致无法识别脚本命令。所以为了确保万一,我就加上了
1
行空行。

上述脚本文件编辑完毕后,如下图所示。这里需要注意:以下截图中有些错误,例如
hostname
写成了
Hostname
,且最后一行没有空行。所以对于图片,大家仅仅参考就好;主要还是按照前述文字版代码中的格式,来修改自己的
PBS
脚本文件。

此外,我们再给出
1

PBS
脚本执行可执行文件的模板,具体如下。

#!/bin/bash
#PBS -N py_task
#PBS -q rtlab1_4
#PBS -l nodes=1:ppn=1
#PBS -l walltime=12:00:00
#PBS -o /data1/home/LiliAircas/Data_Reflectance_Rec/code/py_task.out
#PBS -e /data1/home/LiliAircas/Data_Reflectance_Rec/code/py_task.err
hostname
date "+%Y/%m/%d %H:%M:%S"
cd /data1/home/LiliAircas/Data_Reflectance_Rec/code
./Alignment_Server
date "+%Y/%m/%d %H:%M:%S"

其中,脚本文件的含义前面已经介绍过,这里我们就不再逐一介绍了。


Vim
中编辑完毕自己的脚本文件后,即可保存并退出
Vim
。首先,我们需要按下
Esc
键,退出编辑模式;随后,依次输入
:wq

3
个按键,即可保存并退出
VIm

随后,我们即可提交自己的
PBS
脚本文件到系统中;通过如下的代码即可实现这一功能。

qsub py_task.pbs

上述代码会将我们前述编辑好的
PBS
脚本文件
py_task.pbs
提交到
PBS
作业调度系统中,并开始等待系统分配资源,从而执行作业。执行上述代码,如下图所示。

如果没有问题的话,会出如上图所示的一个编号;这个就是我们刚刚提交的任务的
ID

当然,有的时候执行上述代码,会出现如下图所示的错误,即
qsub: submit error (Unauthorized Request...)
字样的报错。

这种多数是因为将作业提交到了没有权限的队列中导致的;这种情况,需要和服务器的管理人员联系,从而获取权限。

接下来,再介绍几个
PBS
系统的常用命令。

首先,我们可以通过如下代码,获取当前超算的所有节点的情况。

pbsnodes

执行上述代码,如下图所示;可以看到,不同节点的信息都列出来了。

还可以在上述命令的后面,加上具体节点的名称,从而只获取指定节点的信息;如以下代码。

pbsnodes cu02

执行上述代码,如下图所示;其中,和上图一样,每一个当前正在
该节点
上运行的任务的
ID
,都会被显示出来,例如下图中紫色框内就是某个任务的
ID

其次,我们可以通过如下代码,获取当前队列中所有任务的情况。

qstat

执行上述代码,如下图所示;可以看到有一个任务,这个任务是我自己提交的。在我这里,执行上述代码后只能看到自己提交的任务,而看不到队列中同时存在的、其他人提交的任务——感觉这个可能是我们学校服务器管理人员自行设置的,使得每一个用户仅仅只能看到自己账户中提交的任务。

再次,还可以通过如下的代码,查看队列中任务的详细信息。

qstat -f

执行上述代码,如下图所示。

此外,可以通过
qdel
命令加上任务的
ID
,删除队列中的指定任务;例如以下代码。

qdel 1250752

执行上述代码,并在执行前后通过
qstat
命令查看队列中的任务,可以看到指定的任务已经被删除了——但是有延迟:执行完
qdel
后立刻执行
qstat
的话,可以看到
1250752
这个任务还是在的;稍后再执行
qstat
,才可以看到
1250752
任务消失。

任务执行完毕后,我们就可以依次执行下面的
2
句代码,打开并查看作业的标准输出、错误输出文件。
cat
是一个常用的命令,用于连接指定的文件,并打印它们的内容。

cat py_task.out
cat py_task.err

执行上述代码,如下图所示。当然,我这里因为待执行的任务存有一些权限上的错误,所以在
py_task.err
文件中,给出了任务执行过程中的报错内容。

如果没有错误的话,那么大家就可以结合自己任务的实际情况,查看任务执行的结果文件了。

至此,大功告成。

我要在这里放一段代码块

// 这是一段防爬代码块,我不介意文章被爬取,但请注明出处
console.log("作者主页:https://www.hanzhe.site");
console.log("原文地址:https://www.cnblogs.com/hanzhe/p/18325131");

前言

事情的起因是这样的,昨天领导找到我说服务器内存满了,影响其他程序正常运行了,让我把测试服务器上之前启动的六个JAVA程序停一下,接着我就登上服务器执行
docker compose down
把服务关掉,临走之前习惯性使用
htop
查看一下资源面板,意外发现服务器中有个叫
[kdcflush] acosd
的进程把服务器CPU性能吃满了

image

我见识少第一次看到这个服务,觉得应该是公司运行的,好奇心驱使我网上查了下资料,想知道这个中间件(或者其他的程序)的作用是什么,如此消耗性能没准跟AI模型有关,好玩的话以后自己也折腾玩玩,但是查了有一会没找到对应的资料,感觉不太妙,接着跟领导确认了下这个进程是不是我们自己运行的,最终得到的结论是服务器很有可能被挖矿了

意识到问题后领导把它交给了我,我自己也有服务器也处理中过几次挖矿脚本,不过自己的服务器还不是随便折腾,当时我的处理方案就是把数据拷贝出来,给云服务重新装个系统,再把资料放回去服务跑起来就解决了,但是公司的服务器显然不能这么做,不过幸好思路还是有的,这里记录一下解决的流程

正文

挖矿脚本这个东西我遇到好多次了,这次倒是我第一次真正面对他,凭我对LINUX服务器的熟悉程度并不能直接解决它,于是我用
kill -9 29273
先简单粗暴的把进程杀掉,然后去预习一下资料顺便补补课,大概一个半小时后我准备回来解决它,不出意外它又背着我偷偷运行起来了

image

首先每个运行的进程都会有一个PID,也就是上面截图中的第一列,而LINUX中一切皆文件,所以这个进程会以文件的形式存在于
/proc/pid/exe
,查看一下这个进程文件

image

这个进程的文件指向了
/usr/sbin/nacosd
文件,瞬间就想明白了,前段时间用测试服务器搭建了NACOS注册中心,用户名密码都是默认的nacos也没改默认端口号没设置鉴权,就那么明晃晃的摆在公网上,原来问题出在这了

杀掉这个脚本很简单,不过得想办法彻底杀掉他不让它在偷偷跑回来,这里先检查服务器中有没有可疑的定时任务

image

网上简单查了一下titanagent感觉挺正常的,定时任务算是排查完了,接下来排查系统中有没有注册可疑的服务,根据
大佬的文章
得知LINUX中服务都已文件的形式存放在
/etc/systemd/system
目录下,这里检查该目录下有没有可疑的服务

image

嗯!可疑,看看这个服务的内容是什么

image

嗯!没错了,这个服务的作用就是运行这个挖矿脚本,如果我猜的不错这个服务肯定设置开机自启了

image

这样一来思路就理清了,先使用
systemctl disable system-nacosd
命令关闭该服务的开机自启,然后使用
systemctl stop system-nacosd
命令停止该服务,然后使用
rm
命令删除服务配置文件(这部分忘截图了),接着删除
/usr/sbin/nacosd
这个脚本文件,就算大功告成了

image

第一次停止脚本大概一个半小时左右再回去看,脚本自己就又跑起来了,如今已经到了第二天的中午服务器还很安静,应该是成功了!

就写到这吧,能力有限技术有限,如果还有哪些我没考虑到的地方欢迎指点