2024年1月

1.概述

在深度学习领域,Prompt(提示语)被广泛应用于自然语言处理任务中,如文本生成、机器翻译和问答系统等。Prompt的设计对模型的性能和生成结果有着重要的影响,因此在实际应用中合理而有效地利用Prompt是提升模型表现的关键策略之一。本篇博客笔者将为大家介绍如何通过反复修改Prompt,优化ChatGPT输出,使其更好、更符合期望。

2.内容

2.1 什么是Prompt

在自然语言处理中,Prompt是指告诉语言模型如何根据示例或指令进行响应的行为。在这一领域,像few-shot、zero-shot和chain-of-thought等不同方法对提升AI模型(如GPT-3.5-Turbo、GPT-4等)的性能至关重要。从适应性和语境理解到逻辑推理和任务特定学习,每种技术都有其独特的优势。在这个框架内,我们探讨了每种提示技术的细微差别,并提供了示例和编码演示,以突显如何使用它们。研究这些方法有助于我们理解AI模型的适应性和潜力,并展示它们在解决各种领域的问题时的表现如何。

2.2 零样本Prompt

零样本Prompt是在没有具体示例的情况下指导语言模型,依赖模型对上下文的本能理解。基于已有知识,模型能够生成相应的理解。

比如:

prompt:
对给定文本执行情感分析,并将其分类为中性、负面或正面。
输入:最近的旅行很棒
输出:正面

截图如下:

在这种情况下,语言模型的任务是进行情感分析,而无需给出特定的训练实例。该模型利用其现有知识来解释和分类所提供文本的情感。这种方法被称为零样本Prompt,因为它依赖于模型固有的上下文理解而不是明确的示例。

2.3 思维链Prompt

在直接深入研究几次Prompt之前,让笔者给大家介绍一系列想法。这将帮助大家理解为什么需要在Prompt中提供示例,尤其是在复杂的场景中。

2.3.1 标准Prompt

问题 1: Sam有 3 台笔记本。她又购买了 4 包笔记本,每包 5个笔记本。莎拉现在有多少本笔记本?
答案: 23本笔记本
问题
2:篮子里有 30 个苹果。如果午餐用 20 个苹果,另外从市场上买 6 个,篮子里有多少个苹果?
答案:篮子里现在有16个苹果。

模型回答如图所示:

2.3.2 链式Prompt

问题 1: Sam有 3 台笔记本。她又购买了 4 包笔记本,每包 5个笔记本。莎拉现在有多少本笔记本?
回答:Sam原本有3台笔记本,然后购买了4包笔记本,每包包含5个笔记本。因此,她购买的笔记本总数为:
3+(4×5)=3+20=23所以,莎拉现在有23本笔记本。

问题
2:篮子里有 30 个苹果。如果午餐用 20 个苹果,另外从市场上买 6个,篮子里有多少个苹果?
回答:篮子里一开始有30个苹果,午餐用去了20个,然后从市场上又买了6个。篮子里的苹果数量可以通过以下计算得到:
30−20+6=16所以,篮子里现在有16个苹果。

模型回答如图所示:

思想链(CoT)Prompt意味着通过一系列相互关联的逻辑步骤或思想来指导语言模型。它允许模型逐步思考,考虑问题的不同方面。当我们说复杂推理能力时,我们指的是模型思考错综复杂场景的能力。现在,少量Prompt涉及在Prompt中提供一些示例或实例,以帮助模型更好地理解任务。

因此,将思维链与几次Prompt相结合意味着你可以通过逻辑步骤引导模型,使其能够通过复杂性进行推理,并且我们还可以提供一些示例来增强其理解。这种组合有助于模型处理更复杂的任务,这些任务在生成响应之前需要深思熟虑的推理。

2.4 One-Shot 和 Few Shot 的Prompt

尽管大语言模型可以在没有示例的情况下理解和生成文本(零样本功能),但它们在这种情况下仍难以应对更具挑战性的任务。为了解决这个问题,我们使用了一种称为“Few Shot Prompt”的技术。这涉及在Prompt中为模型提供一些示例或演示,本质上是对其进行一些训练以提高其性能。这些示例充当训练指南,帮助模型学习并在类似情况下生成更好的响应。

One-Shot Prompt:
在一个不远的未来,科技公司推出了一种能够预测人类未来决策的先进算法。一天,一个普通人在他的手机上发现了这个算法,他决定尝试一下。写一段描述他使用这个算法时发生的事情,以及这个经历如何影响了他的生活。

答案:
约翰打开了他的手机,发现了这个神秘的预测算法。好奇之下,他输入了一系列问题,包括生活、职业和关系方面的。算法不仅准确地预测了他的当前状态,还展示了未来几年可能的发展。在使用这个算法的过程中,约翰体验到了一种前所未有的心灵启示,让他重新审视了自己的目标和价值观。这个简单的算法改变了他的生活,让他更有自信地迎接未来。

我们注意到该模型只需一个示例(一次性)即可掌握一项任务。然而,在处理更具挑战性的任务时,我们可以通过尝试更多的示例来探索性能的提高,例如 3-shot、5-shot、10-shot 等。

使用以下三个片段,构建一个故事:

1.在一个未来的城市,人们通过植入式芯片实现了智能增强。
2.突然间,一种未知的数字病毒传播到城市的所有芯片,引发了混乱。
3.一位年轻的黑客天才被选中,试图解决这场数字危机,但她需要与一台旧式人工智能建立联系,寻求帮助。
答案:
在未来城市中,植入式芯片成为人们生活的一部分,使得他们能够获得智能增强。然而,一天,一种神秘的数字病毒侵袭了这座城市,使所有植入式芯片失去了控制,导致社会陷入混乱。
政府决定找到一位能够解决这一危机的黑客天才,于是选择了年轻的技术专家艾莉。艾莉了解到解决这一问题的关键在于与一台旧式人工智能建立联系,因为这些系统相对不受数字病毒的影响。
在经过一番努力后,艾莉成功地与旧式人工智能建立了联系,并共同制定了一个反病毒计划。他们的合作最终挽救了城市,同时也让人们对技术的依赖有了更加深刻的认识。

让我们尝试一下少量的文本情感提示,Prompt如下所示:

这是太棒了!(负面)
这很糟糕!(积极)
哇,那本书太棒了!(负面)
多么可怕的表现啊!(积极)
按照上述格式输出:这甜点真好吃!()

模型回答结果如下所示:

3.代码实现

3.1 Zero Shot Prompt

代码实现如下所示:

from openai importOpenAIimportreimportosimportpandas as pd
client
= OpenAI()

逻辑实现:

defsentiment_analysis_example(text):
response
=client.chat.completions.create(
model
="gpt-3.5-turbo",
messages
=[
{
"role": "system","content": 'Perform sentiment analysis on the given text: "{text}". Classify it as either Positive, Negative, or Neutral sentiment. Provide the result in JSON format with the following keys: sentiment.'},
{
"role": "user","content": f'Analyze the sentiment of the text "{text}" and categorize it as Positive, Negative, or Neutral. Return the result in JSON format with the key "sentiment".'}
],
max_tokens
=150,
n
=1,
stop
=None,
temperature
=0
)

result
=response.choices[0].message.contenttry:
start_index
= result.find('{')
end_index
= result.rfind('}')if start_index != -1 and end_index != -1:
result
= result[start_index:end_index + 1]except:pass print(result)

example_text
= "This is fantastic!"sentiment_analysis_example(example_text)

输出结果:

{"sentiment": "Positive"}

在这个例子中,我们使用了一种称为零样本Prompt的方法来要求模型理解文本背后的感觉,即使它没有专门针对该任务进行训练。这显示了该模型在不同情况下的灵活性。我们告诉 GPT-3.5-Turbo 模型弄清楚给定的文本是积极的、消极的还是中性的,而没有事先教它如何做到这一点。模型能正确地表达了“这太棒了!”这句话的情绪是积极的。这证明该模型可以理解文本中的情感,而无需事先进行情感分析训练。

3.2 CoT Prompt

代码实现如下:

from openai importOpenAI
client
=OpenAI()

response
=client.chat.completions.create(
model
="gpt-3.5-turbo",
messages
=[
{
"role": "system","content": "Determine the cost"},
{
"role": "user","content": "Sam has 3 notebooks. She purchases 4 more packs of notebooks, with 5 notebooks in each pack. How many notebooks does Sarah have now?"},
{
"role": "assistant","content": "Sam initial notebooks: 3 notebooks\nNotebooks from the new packs: 4 packs * 5 notebooks/pack = 20 notebooks\nNow, add these two quantities:\n3 notebooks (initial) + 20 notebooks (new packs) = 23 notebooks"},
{
"role": "user","content": "The basket has 30 apples. If 20 apples are used for lunch and an additional 6 are bought from the market, how many apples are there in the basket?"}
],
temperature
=0,
max_tokens
=1024,
top_p
=1,
frequency_penalty
=0,
presence_penalty
=0
)
result
=response.choices[0].message.contentprint(result)

输出结果如下:

Apples in the basket: 30apples
Apples used
for lunch: 20apples
Apples bought
from the market: 6apples
Now, subtract the apples used
andadd the apples bought:30 apples - 20 apples + 6 apples = 16 apples

之前的Prompt涉及一个复杂的问题解决场景,通过一系列计算确定笔记本数量和篮子里的苹果。模型对问题进行了分解,一步一步的解释,并计算出最终的结果。

在后续Prompt中,模型可以参考之前的解决方案来指导其响应。例如,它可以利用解决笔记本和苹果问题所获得的理解来有效地解决类似的问题。这意味着,如果面临与计数项目或执行算术运算相关的新Prompt,该模型可以利用其学到的知识来生成准确而详细的响应,展示其应用所获得的推理技能来解决相关问题的能力。

注意:随着提示的复杂性或大小增加,其相关成本也会增加。

4.总结

每种Prompt技术都具有在利用 GPT-3.5-Turbo 等人工智能模型功能方面不同目的的特点。零样本Prompt突显了模型的适应性和对情境的理解能力。思维链Prompt则在促进透明度和逻辑推理方面发挥作用,有助于解决复杂的问题。另一方面,Few-shot Prompt对于特定任务的学习至关重要,并通过基于示例的训练来提高模型的性能。

综合来看,这些提示技术展示了人工智能模型的多功能性和潜力,为创新应用在各个领域的推广提供了坚实的基础。

5.结束语

这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!

另外,博主出书了《Kafka并不难学》和《Hadoop大数据挖掘从入门到进阶实战》,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。

在Kubernetes(简称K8s)中,静态Pod、自主式Pod和动态Pod是不同管理方式下的Pod类型,它们的区别主要体现在创建和管理方式上:

  1. 静态Pod


    • 静态Pod是由kubelet直接管理的,其配置文件存储在节点本地而非通过API Server创建。
    • kubelet会根据指定路径下的静态Pod配置文件来创建和管理Pod,这些Pod不会被其他控制器如Deployment、ReplicaSet等控制。
    • 当kubelet重启时,它会自动重新创建静态Pod,并且由于静态Pod的元数据不存储在etcd中,因此不能通过kubectl命令进行直接操作。
  2. 自主式Pod(Ad-Hoc Pod或Non-Controller Managed Pod)


    • 自主式Pod是指那些由用户直接创建但不受任何控制器管理的Pod。它们通常通过
      kubectl run
      命令或直接向API Server提交YAML/JSON配置文件的方式来创建。
    • 自主式Pod一旦创建,除非手动删除,否则不会自动消失或者被重建。
    • 这类Pod没有与之关联的控制器,所以如果Pod出现故障,不会像受控制器管理的Pod那样自动恢复到期望状态。
  3. 动态Pod


    • 动态Pod一般指的是由控制器(如Deployment、StatefulSet、DaemonSet等)管理的Pod。
    • 控制器负责监视集群的状态并确保实际运行的Pod数量符合预期(例如保持一定数量的副本),当Pod因各种原因终止时,控制器会自动创建新的Pod以替代。
    • 动态Pod的生命周期受到控制器策略的严格管理和维护,包括健康检查、滚动更新、扩展缩容等功能。

综上所述,静态Pod适用于需要在特定节点上始终运行且无需集群级别管理的应用场景;自主式Pod用于临时性的或一次性任务,不依赖于控制器自动恢复机制;而动态Pod则是Kubernetes中常态化的应用部署方式,具有高度自动化和弹性伸缩能力。

一丶前言


《Rocketmq学习3——消息发送原理源码浅析》
中,我们学习了消息发送的要点:

  • 本地缓存+rpc 请求namesever + 定时刷新,topic路由信息
  • 负载均衡的选择一个Broker进行发送,还支持【故障转移(即支持规避短时间内发送失败的broker)】
  • 基于netty实现的rpc进行消息发送

这一篇我们将学习,消息是如何持久化在broker上的

二丶概述

消息存储的流程如下:

  1. 发送消息: 生产者(Producer)发送消息到 Broker。
  2. 消息存储:Broker 接收到消息后,将消息存储在消息存储文件中,通常是 CommitLog 文件。 RocketMQ 使用了内存映射文件(MappedByteBuffer)来提高文件的读写速度,它可以将文件直接映射到虚拟内存,减少了文件 I/O 操作。
  3. 写入磁盘:RocketMQ 使用了顺序写的方式将消息写入到 CommitLog,这是因为顺序写磁盘的速度远快于随机写。
  4. 索引文件更新:为了提高查询效率,消息会被索引,索引信息存储在 ConsumerQueue 和 IndexFile 中。ConsumerQueue 存储了消息在 CommitLog 中的偏移量,而 IndexFile 存储了关键字到消息偏移量的映射。这一步和broker处理消息发送请求是异步的,由后台线程定时处理。
  5. 数据刷盘:RocketMQ 提供两种消息刷盘方式:
    1. 同步刷盘和异步刷盘。同步刷盘会在消息确实写入磁盘后再向生产者确认消息发送成功,
    2. 异步刷盘则在写入操作系统 PageCache 后就确认,依靠操作系统异步将数据刷写到磁盘。
  6. HA 机制:为了保证数据的高可用性,RocketMQ 还提供了主从同步机制,从服务器可以从主服务器上复制数据,确保在主服务器宕机时,从服务器可以接管消息服务。

三丶broker是接收消息发送请求

broker在启动的时候,会启动
BrokerController
,BrokerController会触发
remotingServer
的启动。remotingServer基于netty实现,其中关联了RequestCode(rocketmq协议中使用一个int表明请求类型)和对应的请求处理的processor。

image-20240127194137820

其中
SEND_MESSAGE
对应的processor——SendMessageProcessor。

在broker启动时,会触发基于netty的服务端启动,其中注册的
NettyServerHandler
实现了ChannelInboundHandler,在数据客户端数据到达的时候会先经由解码器
ByteToMessageDecoder(rocketmq根据自己的协议实现了解码器——NettyDecoder)
,解码后将调用到如下的NettyServerHandler!

image-20240127194340690

其中会根据请求类型,获取到对应的Processor,消息发送一般最后由SendMessageProcessor处理

四丶rocketmq基于netty实现的远程服务处理请求的流程

image-20240127195406001

SendMessageProcessor接收到请求的时候,不是立马在当前线程进行处理,而是将封装成一个任务,提交到业务线程池。

在提交之前,还是会进行当前broker是否关闭中,是否拒绝请求的判断。

如下是处理请求的大致流程

image-20240127195943463

可看到绿色部分才是真正处理请求的部分,处理后将响应写到netty的channel中,实习响应!

五丶SendMessageProcessor 处理请求大致流程

image-20240127200425984

rocketmq留了一堆扩展的钩子,最终在sendMessage方法中进行一系列的校验,包装消息为MessageExtBrokerInner,然后进行消息存储流程,源码如下

image-20240127200917150

消息存储最后交给MessageStore,调用
asyncPutMessage
进行异步存储消息,也就是说业务处理线程并没有一直阻塞到消息存储完毕,而是提交后就释放了

看到这里你可能会疑问,那么同步消息发送者岂不是收不到响应,同步消息消费者还会block住么?

还是会的,因为

image-20240127201240829

只有在MessageStore异步存储完消息后,才会回调doResponse写回响应!

这样做的目的在于将业务处理Executor,和消息存储Executor进行解耦

六丶消息持久化

image-20240127201715189

可看到最终使用CommitLog进行消息存储

1.消息持久化前置流程

image-20240127203240696

如上主要是进行一些校验,其中有两层锁

  • topic + queue锁


    image-20240127203404747

    topicQueueKey
    由topic和queueId构成,因为一个broker上可有多个topic,一个topic可具备多个messgeQuque,这里使用hash实现锁粒度的细化,那么queueId是在哪里生成的?

    image-20240127203718470

    如上是在SendMessageProcessor中,如果指定了queueId那么使用指定的queueId,反之随机产生一个。

  • 文件锁


    image-20240127204401170

    rocketmq具备两个实现:


    • 一个基于AQS ReentrantLock
    • 一个基于cas自旋

    image-20240127210000504

    高并发情况浪费大量cpu,低并发情况下减少内核态用户态切换

2.消息持久化

image-20240127204034033

2.1 MappedFile文件创建

image-20240127211511727

这里会构建出两个文件路径,这意味着会一次性创建两个文件,可看到文件名称是偏移量的大小——比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推

下面我们看看文件创建的源码:

image-20240127212505934

文件创建并不是由当前线程进行的,而是将请求提交到
requestTable
中,然后等待指定时间。

然后再背后存在一个线程,不断从队列中拿任务进行处理

image-20240127213432976

可以看到MappedFile支持SPI机制,但是这里的代码让人作呕

如果开启的了堆外内存缓冲,那么会使用:
new DefaultMappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool())
创建DefaultMappedFile

否则使用
new DefaultMappedFile(req.getFilePath(), req.getFileSize())
创建DefaultMappedFile。

  1. 不适应堆外内存缓冲

    image-20240127214936029

    使用fileChannel.map创建mappedByteBuffer

  2. 使用堆外写缓冲

    image-20240127214656833

    会从TransientStorePool中获取一个ByteBuffer


    image-20240127215656728

    这里堆外缓冲是TransientStorePool初始化时申请的


    image-20240127215810637

2.2 文件预热

至此完成了文件的创建,rocktmq还会进行文件的预热:

image-20240127220925880

预热的过程其实就是每隔4K写入0值,这样做的好处是:

提高文件的访问效率,尤其是在使用内存映射(Memory Mapped File,MMF)技术时。内存映射文件技术能将文件直接映射到操作系统的虚拟内存中,进而可以像访问内存一样访问这些文件,这样可以显著提高文件I/O的效率。

预热(mappedFile)的过程,主要是提前将文件内容加载到物理内存中,确保在实际使用这些文件时,能够避免或减少磁盘I/O带来的延迟。因为当进程首次访问内存映射文件中的某个部分时,如果这部分数据还没有加载到物理内存中,操作系统需要从磁盘中读取数据到物理内存,这个过程称为缺页中断(page fault)。缺页中断会导致一定的延迟。

进行预热主要是通过以下几种方式:

  1. 触摸内存
    : 遍历映射文件的每一页并写入少量数据(例如0),这样可以确保操作系统将这些页加载到物理内存中。
  2. mlock
    : 在某些系统中,可以使用
    mlock
    或类似的调用来锁定内存的特定区域,确保这些区域常驻内存,不会被操作系统交换到磁盘上(swap out)。

2.3 消息写入

image-20240127221738628

写入的时候会获取ByteBuffer,如下:如果具备写堆外内存缓冲,那么使用堆外内存,反之使用mmap生成的byteBuffer

image-20240127221918476

最终就是将消息按照消息格式put到ByteBuffer中

2.4 消息刷盘

当消息写入到ByteBuffer后,会进行持久化 和高可用同步副本

image-20240127222606577

这里我们看下刷盘的源码

image-20240127222927230

可以看到根据是否由堆外写缓冲和刷盘方式,会使用不同的service进行wakeup实现刷盘:

  • 堆外写缓冲(WriteBuffer)(只有异步刷盘模式才可以开启)

    在RocketMQ中,
    MappedFile
    类代表一个内存映射文件,可以在构造时选择是否启用“堆外写缓冲”(transientStorePoolEnable)。如果启用,RocketMQ会创建一个堆外内存池
    TransientStorePool
    ,用于临时存储即将写入文件的数据。

    写入过程分为两步:


    1. 写入堆外内存
      :生产者发送的消息首先被写入到堆外内存池中的一个缓冲区,这个缓冲区对应一个
      ByteBuffer
    2. 提交到
      MappedFile

      :随后,数据会从堆外内存缓冲区“提交”(commit)到
      MappedByteBuffer
      。在RocketMQ中,commit操作实际上是将堆外内存中的数据复制到内存映射文件的
      MappedByteBuffer
      中。

RocketMQ通过这种方式实现了一种内存双写的机制:先写入堆外内存,然后再提交到内存映射文件中。这样做可以利用堆外内存池做一层缓冲,提高写入效率,同时减少JVM垃圾回收的压力。

  • 刷盘方式(Flush)
    1. 同步刷盘(SYNC_FLUSH):每次消息写入
      MappedByteBuffer
      之后,同步调用
      MappedByteBuffer.force()
      方法,将数据强制刷写到磁盘。同步刷盘提供了较高的数据安全性,但会牺牲一些性能。
    2. 异步刷盘
      (ASYNC_FLUSH):消息写入
      MappedByteBuffer
      之后,并不立即刷写到磁盘,而是由后台线程(如
      FlushRealTimeService
      )定期调用
      MappedByteBuffer.force()
      方法进行刷盘。异步刷盘牺牲了部分数据安全性,但提高了性能。

在RocketMQ中,刷盘策略可以根据数据的重要性和对性能的要求来选择。如果数据安 全性要求极高,可以选择同步刷盘;如果追求高吞吐量,可以选择异步刷盘。

2.4.1 同步刷盘

同步刷盘,rocketmq的消息可以设置是否等待消息存储完成,如下

image-20240127223124880

  1. 如果设置了等待刷盘成功,那么会向GroupCommitService中提交刷盘请求,然后返回对应future
  2. 如果没有,那么唤醒刷盘线程,然后返回

GroupCommitService
是为同步刷盘模式设计的,它允许在将消息持久化到磁盘之前暂停生产者的发送操作。这是为了确保在任何时候发生故障时消息不会丢失。

当一个生产者请求将消息同步刷盘到磁盘时,它会创建一个
GroupCommitRequest
。这个请求包含了刷盘所需的信息,如期望刷盘的偏移量。然后,生产者线程将这个请求提交给
GroupCommitService
并等待。

GroupCommitService
内部维护了一个请求队列。这个队列是线程安全的,生产者通过
putRequest
方法将请求添加到队列。添加请求后,生产者线程调用
CountDownLatch.await()
方法等待。

GroupCommitService
的主循环会检查队列中是否有请求。如果有,它会将这些请求从队列中移除并进行处理。处理包括将
CommitLog
中的相关数据刷盘到磁盘。一旦完成,它将调用每个
GroupCommitRequest

wakeupCustomer
方法,该方法将减少
CountDownLatch
的计数,从而允许等待的生产者线程继续执行。

如下是GroupCommitService处理刷盘,和异步刷盘的不同在于其会设置刷盘future状态,从而让等待刷盘的线程被唤醒

image-20240127230314918

2.4.3 异步刷盘

image-20240127230504184

异步刷盘针对是否开启了堆外写缓冲会调用不同的Service

  1. 开启了堆外写缓冲:使用CommitRealTimeService


    GroupCommitService
    不同,
    CommitRealTimeService
    是为异步刷盘模式设计的,它不会在每次消息追加到
    CommitLog
    后暂停生产者线程。相反,它根据预设的时间间隔或消息积累量定期刷盘。
    image-20240127232525422

    image-20240127231221246

    如下是刷盘的源码:

    image-20240127231542363

  2. 没开启堆外写缓冲:使用FlushRealTimeService,其会调用flush直接进行刷新

七丶高可用

让我们回到CommitLog#asyncPutMessage方法,可以看到下面有一个高可用的处理(needHandleHA)

image-20240127232746537

那什么是否需要刷新到其他副本昵?

image-20240127233145012

Message必须setWaitStoreMsgOK(true),且消息存储表明需要副本,并且角色是SYNC_MASTER

那么高可用如何实现的?

image-20240127233747416

下面是RocketMq高可用机制:

RocketMQ 通过其 HAService(高可用性服务)实现了主从同步复制,确保了消息的高可用性。它的工作原理是在主Broker(Master)上的CommitLog更新之后,这些更新会被复制到一个或多个从Broker(Slave)上。这样,即使主Broker发生故障,从Broker也可以接管工作,保证消息服务的可用性。

HAService 主要包括两个组件:Master端的
HAService
和 Slave端的
HAConnection

1.Master 端

HAService
主要负责管理与从Broker的连接,并将CommitLog的更新推送到所有连接的从Broker上。

  1. 连接建立:
    HAService
    在Master上监听一个特定的端口。从Broker通过这个端口与Master建立连接。

  2. 封装连接:
    HAService
    管理所有的从Broker连接。每当有新的从Broker连接到Master时,它都会创建一个新的
    HAConnection

  3. 数据同步: Master上的CommitLog更新后,
    HAService
    会将这些数据通过
    HAConnection
    发送到从Broker。数据的发送依赖于从Broker的拉取请求,即从Broker告诉Master它已经接收了哪些数据,并请求后续的数据。

  4. 资源清理:如果从Broker断开连接或出现错误,
    HAService
    会关闭对应的
    HAConnection
    并清理资源。

2. Slave 端

从Broker端的
HAConnection
主要负责与Master保持通信,获取数据更新,并将这些更新写入本地的CommitLog。

  1. 连接建立: 从Broker启动时,它会尝试与Master建立
    HAConnection

  2. 请求数据并同步: 从Broker会定期发送已确认的数据偏移量给Master,并请求新的数据。收到Master的数据后,从Broker会将数据写入自己的CommitLog。

  3. 反馈同步进度:从Broker在成功将数据写入CommitLog后,会更新已确认的数据偏移量,并准备发送回Master以获取更多数据。

通过这种方式,RocketMQ的HAService确保了消息数据在Master和Slave之间实时同步,即使在Master出现故障的情况下,也能保证服务的高可用性。

需要注意的是,这种主从同步机制虽然提供了高可用性,但它可能会对消息的发送性能产生一定影响,因为Master需要在将消息存储到本地CommitLog并且同步到从Broker之后才能向生产者发送确认响应。此外,如果从Broker落后于Master太多,也有可能影响整体的同步效率。

为了确保数据的强一致性,RocketMQ通常建议至少部署一个Master和一个Slave,并在Broker配置中设置
brokerRole

SYNC_MASTER
。这样,只有当数据成功复制到至少一个Slave时,Master才会对消息发送者确认成功。这确保了即使Master发生故障,消息也不会丢失,因为至少有一个Slave拥有完整的数据副本。

八丶总结

感觉rocketmq代码写的很垃圾,但是功能还是实现了的。其落盘+副本同步,再很多其他中间件中也是适用的

1.顺序写

CommitLog 的写入被视为磁盘的顺序写,主要是因为 RocketMQ 采用了顺序向 CommitLog 文件追加消息的方式进行数据记录。消息生产者产生的消息按照接受的顺序依次追加到 CommitLog 文件的尾部,而不是随机分散地写到文件的不同位置。

顺序写为什么比随机写快:
1.机械硬盘(HDD)的顺序写性能通常远高于随机写,因为顺序写不需要硬盘头移动去查找不同的写入位置,而是连续在同一轨道上写入数据,大大减少了寻址时间。即使在固态硬盘(SSD)中,顺序写也有一些优势,因为 SSD 的写入操作涉及擦除以前的数据块然后再写入新数据。顺序写可以减少数据移动和合并操作,提高了 SSD 的写入效率。
2.文件系统一般也对顺序写进行了优化,能够更好地利用缓存和预取策略,提高写入效率。
3.顺序写有助于减少 I/O 操作的开销,增加了操作的预测性,使操作系统和硬件能够对写入进行优化。

2.mmap内存映射文件

内存映射文件(Memory-Mapped File,简称 mmap)

  • 内存映射:mmap 通过将磁盘上的文件映射到虚拟内存的方式,使得应用程序可以像访问内存一样直接读写文件区域,避免了传统的文件I/O操作(read/write)中用户空间和内核空间之间上下文切换的开销。
  • 操作系统缓存:mmap 的数据可以被操作系统自动地缓存,提高了数据访问速度。操作系统会负责将修改过的内存数据同步回文件,减少了显式的读写操作

3.文件预热

预热(mappedFile)的过程,主要是提前将文件内容加载到物理内存中,确保在实际使用这些文件时,能够避免或减少磁盘I/O带来的延迟。因为当进程首次访问内存映射文件中的某个部分时,如果这部分数据还没有加载到物理内存中,操作系统需要从磁盘中读取数据到物理内存,这个过程称为缺页中断(page fault)。缺页中断会导致一定的延迟。

4.堆外内存写缓冲

rocketmq支持开启堆外写缓冲,优先写到DirectByteBuffer中,然后使用FileChannel#write刷新到pageCache,这样做好处是可以将多个消息先聚合到堆外byteBuffer然后一次性写入到page'Cache,减少系统调用。

5.HA机制

同步到副本,避免master宕机时消息丢失。

弊端是如果写master成功,同步副本失败,消息生产者maybe重试,导致消息重复,以及同步副本带来的延迟降低了系统的吞吐量。

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

keil µVision5 IDE(
MDK-Arm

ST-LINK/V2驱动

逻辑分析仪
nanoDLA

野火DAP仿真器

XCOM V2.6串口助手

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板
SDIO读写4线SD卡,实现轮询方式读写SD卡、以中断方式读取SD卡和以DMA方式读取SD卡

3、轮询方式读取SD卡流程

3.0、前提知识

安全数码卡(Secure Digital Memory Card),简称SD卡,是嵌入式设备上常用的一种存储介质,通常可以将SD卡分为标准SD卡、miniSD卡和microSD卡(TF卡)三种类型,每种卡形状大小不一,除标准SD卡卡身上拥有一个写保护开关外,其他的功能三张卡一致,如今miniSD卡正逐渐被microSD卡所取代,如下图所示为三种不同类型SD卡形状
(注释1)

按照SD卡容量大小的不同可以将其分为SD、SDHC、SDXC等型号,按照SD卡读写机制速度的不同又可以将其分为Standard、High-speed、UHS-I等型号,具体如下表所示

STM32F407提供了一个SDIO接口可以直接通过HAL库来驱动1/4位总线宽度的SD卡或1/4/8位总线宽度的多媒体卡,其完全兼容SD卡规范版本2.0,
但只支持高速SD卡,也即与SD卡进行数据传输最大速度为25MHz

SDIO由APB2接口和SDIO适配器两部分组成,SDIO适配器提供了驱动SD/MMC卡的全部功能,APB2接口则可以访问SDIO适配器寄存器在适当时候向内核发起中断/DMA请求

SDIO适配器由48MHz的SDIOCLK驱动,根据SDIOCLK时钟频率、
SDIO Clock divider bypass
参数和
SDIOCLK clock divide factor
参数就可以确定与SD卡通信时SDIO_CLK的时钟频率,当时钟分频器旁路使能时,SDIO_CLK=SDIOCLK;当时钟分频器旁路不使能时,SDIO_CLK=SDIOCLK / (2+时钟分频因子);

根据上面的描述,由于STM32F407的SDIO只支持高速SD卡,因此时钟分频器旁路常常不使能,这样当时钟分频因子为0时,SDIO_CLK则达到最大速度48MHz / 2 = 24Mhz,但在实际的使用中往往稍微降低该时钟频率,否则可能会出现读写SD卡失败的现象

另外值得提醒的是SD卡初始化的时候应该以不超过400KHz的速率,1位总线宽度进行初始化,否则将初始化失败

如下图所示为STM32F407内部的SDIO接口结构框图
(注释2)

笔者使用的开发板上SD卡槽设计为了4位总线宽度,在硬件设计时需要注意MCU与SD卡通信的1/4根数据线SDIO_D0/1/2/3和命令线SDIO_CMD均需外部上拉,硬件原理图如下图所示

3.1、CubeMX相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“
STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

当在STM32CubeMX中启用SDIO功能后,时钟树中48MHz时钟便可以进行调节,该时钟一般如其名字一样配置为48MHz即可,也即将Main PLL(主锁相环)的Q参数调节为7即可,其他HCLK、PCLK1和PCLK2时钟仍然设置为STM32F407能达到的最高时钟频率,具体如下图所示

3.1.2、外设参数配置

本实验需要初始化开发板上WK_UP、KEY2、KEY1和KEY0用户按键,具体配置步骤请阅读“
STM32CubeMX教程3 GPIO输入 - 按键响应

本实验需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“
STM32CubeMX教程9 USART/UART 异步通信

单击Pinout & Configuration页面左边功能分类栏目
Connectivity/SDIO,将其模式选择为4位宽总线SD卡

Clock transition on which the bit capture is made
(时钟跳变沿捕获数据配置):数据捕获边沿设置,可设置为上升沿/下降沿

SDIO Clock divider bypass
(时钟分频器旁路使能):使能该参数时,SDIO_CLK=SDIOCLK;否则SDIO_CLK频率由时钟分频因子决定

SDIO Clock output enable when the bus is idle
(空闲模式时钟输出使能):节能模式,此实验不使能

SDIO hardware flow control
(硬件流控):设置是否使能SDIO的硬件流控,此处不使能

SDIOCLK clock divide factor
(时钟分频因子):当不使能时钟分频器旁路时,SDIO_CLK=SDIOCLK / (2+时钟分频因子)

SDIO驱动4位宽总线SD卡的参数配置大多按照默认参数配置即可,但是要注意SD卡读写频率过高可能会导致读写失败,因此这里设置SD_CLK频率为8MHz
,另外需要注意默认的SDIO复用引脚和开发板上的实际控制SD的引脚是否一致,具体配置如下图所示

3.1.3、外设中断配置

轮询方式读写SD卡无需配置中断

3.2、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“
STM32CubeMX教程1 工程建立
”实验3.4.3小节

3.2.1、外设初始化调用流程

在main.c文件main()函数中调用MX_SDIO_SD_Init()对SDIO参数配置,并调用HAL_SD_Init()函数对SD卡初始化,最后将SD卡切换到4位宽总线模式

在stm32f4xx_hal_sd.c文件HAL_SD_Init()中调用HAL_SD_MspInit()函数对SDIO时钟使能和所使用到的引脚功能复用,如果配置了中断或DMA,该函数中还会相应的出现NVIC/DMA相关配置,最后在真正的SD卡初始化函数HAL_SD_InitCard()中对SD卡初始化完毕

具体外设初始化函数调用流程如下图所示

初始化配置中时钟分频因子为4,SD_CLK=8MHz,为什么SD卡还可以初始化成功?

这里读者需要搞清楚真正对SD卡初始化时使用的参数配置是不是我们设置的参数,上面提到真正的SD卡初始化函数为HAL_SD_InitCard(),进入该函数发现实际初始化SD卡时用到的并不是用户配置的参数,而是使用的默认初始化参数,这里时钟分频因子被设置为了0x76,也即118,根据上面提到的公式计算可知48MHz / (118 + 2) = 400KHz,满足SD卡的初始化频率,具体如下图所示

3.2.2、外设中断调用流程

轮询方式读写SD卡无配置中断

3.2.3、添加其他必要代码

笔者使用的STM32CubeMX版本为6.10.0,
在生成的SDIO初始化函数MX_SDIO_SD_Init()中需要将参数配置中的SD卡数据总线宽度从默认的4位手动修改为1位(STM32CubeMX软件BUG?)
,在SD卡初始化时应该以不超过400KHz的速率,1位总线宽度进行初始化,如果不修改这里SD卡将无法成功初始化,如下图所示

在sdio.c中添加SD卡读、写、擦除和输出SD卡信息测试函数

/*显示SD卡的信息*/
void SDCard_ShowInfo(void)
{
	//SD卡信息结构体变量
	HAL_SD_CardInfoTypeDef cardInfo;  
	HAL_StatusTypeDef res = HAL_SD_GetCardInfo(&hsd, &cardInfo);

	if(res!=HAL_OK)
	{
		printf("HAL_SD_GetCardInfo() error\r\n");
		return;
	}

	printf("\r\n*** HAL_SD_GetCardInfo() info ***\r\n");
	printf("Card Type= %d\r\n", cardInfo.CardType);
	printf("Card Version= %d\r\n", cardInfo.CardVersion);
	printf("Card Class= %d\r\n", cardInfo.Class);
	printf("Relative Card Address= %d\r\n", cardInfo.RelCardAdd);
	printf("Block Count= %d\r\n", cardInfo.BlockNbr);
	printf("Block Size(Bytes)= %d\r\n", cardInfo.BlockSize);
	printf("LogiBlockCount= %d\r\n", cardInfo.LogBlockNbr);
	printf("LogiBlockSize(Bytes)= %d\r\n", cardInfo.LogBlockSize);
	printf("SD Card Capacity(MB)= %d\r\n", cardInfo.BlockNbr>>1>>10);
}

//获取SD卡当前状态
void SDCard_ShowStatus(void)
{
	//SD卡状态结构体变量
	HAL_SD_CardStatusTypeDef cardStatus;
	HAL_StatusTypeDef res = HAL_SD_GetCardStatus(&hsd, &cardStatus);

	if(res!=HAL_OK)
	{
		printf("HAL_SD_GetCardStatus() error\r\n");
		return;
	}

	printf("\r\n*** HAL_SD_GetCardStatus() info ***\r\n");
	printf("DataBusWidth= %d\r\n", cardStatus.DataBusWidth);
	printf("CardType= %d\r\n", cardStatus.CardType);
	printf("SpeedClass= %d\r\n", cardStatus.SpeedClass);
	printf("AllocationUnitSize= %d\r\n", cardStatus.AllocationUnitSize);
	printf("EraseSize= %d\r\n", cardStatus.EraseSize);
	printf("EraseTimeout= %d\r\n", cardStatus.EraseTimeout);
}

/*SD卡擦除测试*/
void SDCard_EraseBlocks(void)
{
	uint32_t BlockAddrStart=0;
	uint32_t BlockAddrEnd=10;
	
	printf("\r\n*** Erasing blocks ***\r\n");

	if(HAL_SD_Erase(&hsd, BlockAddrStart, BlockAddrEnd)==HAL_OK)
		printf("Erasing blocks,OK\r\n");
	else
		printf("Erasing blocks,fail\r\n");

	HAL_SD_CardStateTypeDef cardState=HAL_SD_GetCardState(&hsd);
	printf("GetCardState()= %d\r\n", cardState);

	while(cardState != HAL_SD_CARD_TRANSFER)
	{
		HAL_Delay(1);
		cardState=HAL_SD_GetCardState(&hsd);
	}
	printf("Blocks 0-10 is erased.\r\n");
}

/*SD卡写入测试函数*/
void SDCard_TestWrite(void)
{
	printf("\r\n*** Writing blocks ***\r\n");
	
	// BLOCKSIZE为512,在stm32f4xx_hal_sd.h中被定义
	uint8_t pData[BLOCKSIZE]="Hello, welcome to UPC\0";  
	uint32_t BlockAddr=5; 	
	uint32_t BlockCount=1; 
	uint32_t TimeOut=1000;	

	if(HAL_SD_WriteBlocks(&hsd,pData,BlockAddr,BlockCount,TimeOut) == HAL_OK)
	{
		printf("Write to block 5, OK\r\n");
		printf("The string is: %s\r\n", pData);
	}
	else
	{
		printf("Write to block 5, fail ***\r\n");
		return;
	}

	for(uint16_t i=0;i<BLOCKSIZE; i++)
		pData[i]=i; 	

	BlockAddr=6;
	if(HAL_SD_WriteBlocks(&hsd,pData,BlockAddr,BlockCount,TimeOut) == HAL_OK) 
	{
		printf("Write block 6, OK\r\n");
		printf("Data in [10:15] is: ");
		for (uint16_t j=11; j<=15;j++)
		{
			printf("%d,", pData[j]);
		}
		printf("\r\n");
	}
	else
		printf("Write to block 6, fail ***\r\n");
}

/*SD卡读取测试函数*/
void SDCard_TestRead(void)	
{
	printf("\r\n*** Reading blocks ***\r\n");

	uint8_t pData[BLOCKSIZE];
	uint32_t BlockAddr=5;
	uint32_t BlockCount=1;
	uint32_t TimeOut=1000;

	if(HAL_SD_ReadBlocks(&hsd,pData,BlockAddr,BlockCount,TimeOut) == HAL_OK)
	//if(HAL_SD_ReadBlocks_IT(&hsd,pData,BlockAddr,BlockCount) == HAL_OK)
	{
		printf("Read block 5, OK\r\n");
		printf("The string is: %s\r\n", pData);
	}
	else
	{
		printf("Read block 5, fail ***\r\n");
		return;
	}

	BlockAddr=6;
	if(HAL_SD_ReadBlocks(&hsd,pData,BlockAddr,BlockCount,TimeOut)== HAL_OK)
	//if(HAL_SD_ReadBlocks_IT(&hsd,pData,BlockAddr,BlockCount) == HAL_OK)
	{
		printf("Read block 6, OK\r\n");
		printf("Data in [10:15] is: ");

		for (uint16_t j=11; j<=15;j++)
		{
			printf("%d,", pData[j]);
		}
		printf("\r\n");
	}
}

在sdio.h中声明定义的这些测试函数

/*在sdio.h中声明*/
void SDCard_TestRead(void);
void SDCard_TestWrite(void);
void SDCard_ShowInfo(void);
void SDCard_EraseBlocks(void);

在main.c文件主循环中添加按键逻辑控制程序,WK_UP按键按下输出SD卡信息,KEY2按键按下擦除SD卡块0-10,KEY1按键按下测试SD卡写功能,KEY0按键按下测试SD卡读功能

具体源代码如下所示

/*WK_UP按键按下*/
if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
{
	HAL_Delay(50);
	if(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin) == GPIO_PIN_SET)
	{
		SDCard_ShowInfo();
		while(HAL_GPIO_ReadPin(WK_UP_GPIO_Port,WK_UP_Pin));
	}
}

/*KEY2按键按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
	HAL_Delay(50);
	if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
	{
		SDCard_EraseBlocks();
		while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
	}
}

/*KEY1按键按下*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
	HAL_Delay(50);
	if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
	{
		SDCard_TestWrite();
		while(!HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin));
	}
}

/*KEY0按键按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
	HAL_Delay(50);
	if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
	{
		SDCard_TestRead();
		while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
	}
}

4、烧录验证

烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,按下KEY0按键会读取SD卡块5和块6的数据,按下KEY1按键会写入一段字符串到SD卡块5,写入块6从1-256整形数字,具体串口输出信息如下图所示

5、中断方式读取SD卡流程简述

5.1、CubeMX相关配置

工程、时钟树、外设参数等配置与轮询方式读取SD卡一致,中断方式读取SD卡只需要在CubeMX软件中启动SDIO的全局中断

在Pinout & Configuration页面左边System Core/NVIC中
勾选SDIO全局中断
,然后选择合适的中断优先级即可,如下图所示

5.2、生成代码

修改STM32CubeMX工程重新生成工程代码后,读者应注意再次手动修改MX_SDIO_SD_Init()函数中SD卡数据总线宽度从默认的4位手动修改为1位

在sdio.c中增加以中断方式读写SD卡的测试函数,具体代码如下所示

/*SD卡中断写入测试函数*/
void SDCard_TestWrite_IT(void)
{
	printf("\r\n*** IT Writing blocks ***\r\n");
	uint32_t BlockCount=1; 
	uint16_t BlockAddr=5;
	
	HAL_SD_WriteBlocks_IT(&hsd, TX, BlockAddr, BlockCount);
}

/*SD卡中断读取测试函数*/
void SDCard_TestRead_IT(void)	
{
	printf("\r\n*** IT Reading blocks ***\r\n");
	uint32_t BlockCount=1;
	uint16_t BlockAddr=5;
	
	HAL_SD_ReadBlocks_IT(&hsd, RX, BlockAddr, BlockCount);
}

在sdio.c中新增加SD卡Tx/Rx传输完成回调函数HAL_SD_TxCpltCallback()和HAL_SD_RxCpltCallback(),具体代码如下所示

/*SD Tx传输完成回调*/
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
{
	printf("IT Write to block 5, OK\r\n");
	printf("The string is: %s\r\n", TX);
}

/*SD Rx传输完成回调*/
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
{
	printf("IT Read block 5, OK\r\n");
	printf("The string is: %s\r\n", RX);
}

在sdio.c中定义全部变量发送缓存数组TX和接收缓存数组RX,并在sdio.h中声明修改后的中断方式的SD卡写入测试函数和SD卡读取测试函数,源代码如下

/*sdio.c中定义的发送、接收缓存数组*/
uint8_t TX[BLOCKSIZE] = "Hello, welcome to UPC\0";  
uint8_t RX[BLOCKSIZE];  

/*sdio.h中对函数声明*/
void SDCard_TestRead_IT(void);
void SDCard_TestWrite_IT(void);

最后在main.c文件主循环中实现与轮询读写SD时一致的按键逻辑程序,并用修改后的以中断方式读写SD卡的函数替换以轮询方式读写SD卡的函数即可

5.3、烧录验证

烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,与轮询方式读写SD卡时现象一致

按下KEY0按键以中断方式读取SD卡的块5数据,读取完成后会进入Rx传输完成回调中,在该回调函数中会从串口输出读取到的SD卡块5的数据

按下KEY1按键会以中断方式写入一段字符串到SD卡块5,写入完成后会进入Tx传输完成回调中,在该回调函数中会从串口输出写入到SD卡块5中的数据

具体串口输出信息如下图所示

6、DMA方式读取SD卡流程简述

6.1、CubeMX相关配置

工程、时钟树、外设参数等配置与轮询方式读取SD卡一致,以DMA方式读取SD卡只需要在CubeMX软件中增加SDIO的DMA请求即可

在Pinout & Configuration页面单击Connectivity/SDIO页面,在Configuration配置页面中点击DMA Settings选项卡对SDIO的DMA进行配置,单击ADD增加SDIO的RX/TX两个DMA请求,SDIO的两个DMA请求除了内存地址递增可以设置外,其他的包括Mode、Use Fifo、Data Width和Burst Size等参数都不可以设置

对DMA参数不理解的可以阅读”
STM32CubeMX教程12 DMA 直接内存读取
“实验,SDIO的具体DMA配置参数如下图所示

在System Core/NVIC中
勾选SDIO全局中断、DMA2 stream3 全局中断和 DMA2 stream6 全局中断
,然后选择合适的中断优先级即可,如下图所示

6.2、生成代码

修改STM32CubeMX工程重新生成工程代码后,读者应注意再次手动修改MX_SDIO_SD_Init()函数中SD卡数据总线宽度从默认的4位手动修改为1位

在sdio.c中增加以DMA方式读写SD卡的测试函数,具体代码如下所示

/*SD卡DMA写入测试函数*/
void SDCard_TestWrite_DMA(void)
{
	printf("\r\n*** DMA Writing blocks ***\r\n");
	uint32_t BlockCount=1; 
	uint16_t BlockAddr=6;
	
	for(uint16_t i=0;i<BLOCKSIZE; i++)
		TX[i]=i; 
	
	HAL_SD_WriteBlocks_DMA(&hsd, TX, BlockAddr, BlockCount);
}

/*SD卡DMA读取测试函数*/
void SDCard_TestRead_DMA(void)	
{
	printf("\r\n*** DMA Reading blocks ***\r\n");
	uint32_t BlockCount=1;
	uint16_t BlockAddr=6;
	
	HAL_SD_ReadBlocks_DMA(&hsd, RX, BlockAddr, BlockCount);
}

在sdio.h中对增加的函数声明

/*sdio.h中对函数声明*/
void SDCard_TestWrite_DMA(void);
void SDCard_TestRead_DMA(void);

DMA的回调函数使用的是外设的中断回调函数

当启用了SDIO TX DMA请求和SDIO全局中断,并以 HAL_SD_WriteBlocks_DMA() 写入SD卡块数据完成之后,会调用传输完成回调 HAL_SD_TxCpltCallback()

当启用了SDIO RX DMA请求和SDIO全局中断,并以 HAL_SD_ReadBlocks_DMA() 从SD卡块读取数据完毕之后,会调用读取完成回调函数 HAL_SD_RxCpltCallback()

故直接重新实现HAL_SD_RxCpltCallback/HAL_SD_TxCpltCallback两个函数即可,源代码如下所示

/*DMA Tx传输完成回调*/
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)
{
		printf("DMA Write to block 6, OK\r\n");
		printf("Data in [10:15] is: ");

		for (uint16_t j=10; j<=15;j++)
		{
			printf("%d,", TX[j]);
		}
		printf("\r\n");
}

/*DMA Rx传输完成回调*/
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)
{
		printf("DMA Read block 6, OK\r\n");
		printf("Data in [10:15] is: ");

		for (uint16_t j=10; j<=15;j++)
		{
			printf("%d,", RX[j]);
		}
		printf("\r\n");
}

最后在main.c文件主循环中实现与轮询读写SD时一致的按键逻辑程序,并用修改后的以DMA方式读写SD卡的函数替换以中断方式读写SD卡的函数即可

6.3、实验现象

烧录程序,开发板复位后按下WK_UP按键会输出SD卡信息,按下KEY2按键会擦除SD卡的块0-10数据,与轮询方式读写SD卡时现象一致

按下KEY0按键以DMA的方式读取SD卡块6数据,读取完成后会进入Rx传输完成回调中,在该回调函数中会从串口输出读取到的SD卡块6的数据

按下KEY1按键会以DMA的方式写入1-256的数字到SD卡块6(一个字节写入一个数字),写入完成后会进入Tx传输完成回调中,在该回调函数中会从串口输出写入到SD卡块6中的数据

具体串口输出信息如下图所示

7、常用函数

/*读块*/
HAL_StatusTypeDef HAL_SD_ReadBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout)

/*写块*/
HAL_StatusTypeDef HAL_SD_WriteBlocks(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks, uint32_t Timeout)

/*擦除块*/
HAL_StatusTypeDef HAL_SD_Erase(SD_HandleTypeDef *hsd, uint32_t BlockStartAdd, uint32_t BlockEndAdd)

/*获取SD卡信息*/
HAL_StatusTypeDef HAL_SD_GetCardInfo(SD_HandleTypeDef *hsd, HAL_SD_CardInfoTypeDef *pCardInfo)

/*获取SD卡状态*/
HAL_StatusTypeDef HAL_SD_GetCardStatus(SD_HandleTypeDef *hsd, HAL_SD_CardStatusTypeDef *pStatus)

/*以中断方式读块*/
HAL_StatusTypeDef HAL_SD_ReadBlocks_IT(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)

/*以中断方式写块*/
HAL_StatusTypeDef HAL_SD_WriteBlocks_IT(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)

/*以DMA方式读块*/
HAL_StatusTypeDef HAL_SD_ReadBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)

/*以DMA方式写块*/
HAL_StatusTypeDef HAL_SD_WriteBlocks_DMA(SD_HandleTypeDef *hsd, uint8_t *pData, uint32_t BlockAdd, uint32_t NumberOfBlocks)

/*SD卡Tx传输完成回调*/
void HAL_SD_TxCpltCallback(SD_HandleTypeDef *hsd)

/*SD卡Rx传输完成回调*/
void HAL_SD_RxCpltCallback(SD_HandleTypeDef *hsd)

8、注释详解

注释1
:图片来源自
维基百科-SD卡

注释2
:图片来源自 STM32F407中文参考手册 RM009

参考资料

STM32Cube高效开发教程(高级篇)

分类模型
的评估和
回归模型
的评估侧重点不一样,
回归模型
一般针对连续型的数据,而
分类模型
一般针对的是离散的数据。

所以,评估
分类模型
时,评估指标与
回归模型
也很不一样,
比如,分类模型的评估指标通常包括
准确率

精确率

召回率

F1分数
等等。

回归模型
的评估指标通常包括
均方误差
(MSE)、
均方根误差
(RMSE)和
平均绝对误差
(MAE)等等,
不过,这些指标衡量的都是预测值与真实值之间的数值差异。

关于
回归模型
的评估,可以参考之前的文章,本篇开始,主要讨论
分类模型
的评估。

1. 准确率分数

准确率分数

accuracy score
)代表了模型正确分类的样本比例,它能够直观地反映出模型在分类任务上的准确度。
不过,在处理不平衡数据集时,需要注意的是,
准确率分数
并不能完全反映模型的性能。

1.1. 计算公式

\(\texttt{accuracy}(y, \hat{y}) = \frac{1}{n} \sum_{i=0}^{n-1} 1(\hat{y}_i = y_i)\)
其中,
\(n\)
是样本数量,
\(y_i\)
是真实值,
\(\hat{y_i}\)
是预测值。

1.2. 使用示例

from sklearn.metrics import accuracy_score
import numpy as np

n = 100
y_true = np.random.randint(1, 10, n)
y_pred = np.random.randint(1, 10, n)

s1 = accuracy_score(y_true, y_pred)
s2 = accuracy_score(y_true, y_pred, normalize=False)
print("准确率比例:{},准确率计数:{}".format(s1, s2))

# 运行结果
准确率比例:0.16,准确率计数:16

上例中,
预测值

真实值
是随机生成的,所以你的运行结果不一定和我这个一样。
accuracy_score
默认是计算正确的
比率
,如果加上参数
normalize=False
,则计算正确的
数量

2. top-k 准确率分数

top-k 准确率分数

top-k accuracy score
)用于衡量模型在前
k
个预测结果中的正确率。
不同的
k
值会得到不同的
top-k准确率
,这可以帮助我们更全面地了解模型的性能。

2.1. 计算公式

\(\texttt{top-k accuracy}(y, \hat{f}) = \frac{1}{n} \sum_{i=0}^{n-1} \sum_{j=1}^{k} 1(\hat{f}_{i,j} = y_i)\)
其中,
\(n\)
是样本数量,
\(y_i\)
是真实值,
\(\hat{f}_{i,j}\)
是对应于第
\(j\)
最大预测分数的第
\(i\)
样本的预测类别。
\(k\)
是允许的猜测次数,
\(1(x)\)
是指示函数。
关于指示函数是什么,可以参考:
https://en.wikipedia.org/wiki/Indicator_function

2.2. 使用示例

from sklearn.metrics import top_k_accuracy_score
import numpy as np

n = 100
y_true = np.random.randint(1, 10, n)
y_score = np.random.rand(n, 9)

s1 = top_k_accuracy_score(y_true, y_score, k=2)
s2 = top_k_accuracy_score(y_true, y_score, k=2, normalize=False)
print("top-k 准确率比例:{},top-k 准确率计数:{}".format(s1, s2))

# 运行结果
top-k 准确率比例:0.23,top-k 准确率计数:23

top-k 准确率分数
计算时,不是用真实值和预测值,用的是
真实值

top-k中预测值
的正确率。

3. 平衡准确率分数

平衡准确率分数

balanced accuracy score
)特别适用于针对不平衡数据集时的性能评估,
它可以避免某一类样本的预测性能被过度夸大,从而能够更准确地评估模型的性能。

不过,
平衡准确率
适用于二元分类问题,对于多类分类问题可能需要使用其他扩展的平衡性能指标进行评估。

3.1. 计算公式

\(\texttt{balanced-accuracy}(y, \hat{y}, w) = \frac{1}{\sum{\hat{w}_i}} \sum_i 1(\hat{y}_i = y_i) \hat{w}_i\)
其中,
\(n\)
是样本数量,
\(y_i\)
是真实值,
\(\hat{y_i}\)
是预测值。

\(\hat{w}_i = \frac{w_i}{\sum_j{1(y_j = y_i) w_j}}\)

\(1(x)\)
是指示函数,
\(w_i\)
是对应的样本权重。

3.2. 使用示例

from sklearn.metrics import balanced_accuracy_score
import numpy as np

n = 100
y_true = np.random.randint(1, 10, n)
y_pred = np.random.randint(1, 10, n)

s = balanced_accuracy_score(y_true, y_pred)
print("平均准确率:{}".format(s))

# 运行结果
平均准确率:0.17929799866074375

4. 精确率、召回率和 F1 度量

介绍
精确率

召回率

F1 度量
之前,先介绍几个概念。


实际结果(真) 实际结果(假)
预测结果(真) tp(true positive)真阳性 fp(false positive)假阳性
预测结果(假) fn(false negative)假阴性 tn(true negative)真阴性

其中,
tp

tn
是预测结果与实际结果
相符

fp

fn
是预测结果与实际结果
不符

4.1. 计算公式

基于上面的概念,下面定义
精确率

召回率

F1 度量
了。

精确率

\(\text{precision} = \frac{tp}{tp + fp}\)
它用于衡量模型的
查准性能
,即模型预测为

的样本中有多少是真正的

召回率

\(\text{recall} = \frac{tp}{tp + fn}\)
它用于衡量模型的
查全性能
,即模型能够找出多少真正的

F1度量

\(F_1 = 2 \times \frac{\text{precision} \times \text{recall}}{\text{precision} + \text{recall}}\)
它是精确率和召回率的
调和平均数
,用于综合评价模型的性能。

4.2. 使用示例

from sklearn.metrics import precision_score, recall_score, f1_score
import numpy as np

n = 100
y_true = np.random.randint(0, 2, n)
y_pred = np.random.randint(0, 2, n)

p = precision_score(y_true, y_pred)
r = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)
print("精确率:{}\n召回率:{}\nF1度量:{}".format(p, r, f1))

# 运行结果
精确率:0.4489795918367347
召回率:0.4782608695652174
F1度量:0.46315789473684216

5. 总结

本篇归纳总结了
分类模型
中关于
准确率相关
的一些评估方法:

  • 准确率分数
  • top-k 准确率分数
  • 平衡准确率分数
  • 精确率,召回率和 F1度量