linux的mmc子系统与块设备关联
1.前言
本文主要block组件的主要流程,在介绍的过程中,将详细说明和block相关的流程,涉及到其它组件的详细流程再在相关文章中说明。
2.主要数据结构和API
2.1 struct mmc_card
Elemete Name | struct mmc_card |
Path | include/linux/mmc/card.h |
Responsiblities | 是对mmc device的抽象,由于定义了mmc_bus_type类型的总线,此处mmc_card是与mmc_bus_type配套 |
Attributions |
|
Operations |
2.2 struct mmc_driver
Elemete Name | struct mmc_driver |
Path | include/linux/mmc/card.h |
Responsiblities | mmc driver,由于定义了mmc_bus_type类型的总线,此处mmc_driver是与mmc_bus_type配套 |
Attributions |
|
Operations |
设置总线类型,并将drv加入到设备驱动模型中
将drv从设备驱动模型中移除 |
2.3 struct mmc_blk_data
Elemete Name | struct mmc_blk_data |
Path | drivers/mmc/card/block.c |
Responsiblities | mmc_blk_data为block的核心结构体,用于存放mmc block的一些数据,与mmc slot对应 |
Attributions |
|
Operations |
3. 主要流程
3.1 mmc_blk_init
mmc_blk_init->
初始化max_devices
register_blkdev
mmc_register_driver
module_init(mmc_blk_init)会执行到此函数
- 初始化max_devices:设定最多支持多少个mmc块设备给max_devices
每类块设备支持256个次设备号,每个块设备有16个次设备号(16个分区),由此得出支持的最大的mmc块设备数max_devices为256/16=16,每个此设备号对应一个分区?
- register_blkdev:向全局的struct blk_major_name类型的数组major_names注册本块设备的主设备号和设备名
mmc子系统对于上层block子系统来讲是首先抽象为一个普通的块设备。 通过register_blkdev向block子系统注册一个block设备,主设备号为MMC_BLOCK_MAJOR,设备名为“mmc”。 通过分配一个blk_major_name结构体,来保存主设备号和设备名,blk_major_name被保存到全局的blk_major_name数组中。 如果不指定主设备号,将查询全局的blk_major_name结构体找到一个未用的主设备号来使用,并将此主设备号作为返回值返回。 major_names中的信息会出现在/proc/devices中。 因此可以看出,注册做的事情实际上非常少。注册完成后,除了能够在/proce/devices中看到设备之外,不能对设备做任何事情,设备还无法使用,只有当block_device与gendisk建立关联用户空间才可以访问
- mmc_register_driver(&mmc_driver)
设备驱动模型中通过driver_register将mmc_driver注册到mmc_bus_type上
3.2 mmc_blk_exit
mmc_blk_exit->
mmc_unregister_driver
unregister_blkdev
在退出的时候会执行mmc_blk_exit,与mmc_blk_init相反的动作,主要包括:
- mmc_unregister_driver(&mmc_driver)
从mmc_bus_type上将mmc_driver注销
- unregister_blkdev(MMC_BLOCK_MAJOR, "mmc");
从全局的struct blk_major_name类型的数组major_names中注销主设备号为MMC_BLOCK_MAJOR名为mmc的blk_major_name结构体 释放对应的blk_major_name结构体
3.3 mmc_blk_probe
mmc_blk_probe->
mmc_blk_alloc->
mmc_blk_alloc_req->
alloc_disk
mmc_init_queue->
blk_queue_prep_rq
kthread_run(mmc_queue_thread, mq);
mmc_blk_alloc_parts(card, md))
mmc_add_disk->
block_add_disk
初始化时mmc_blk_init中会执行mmc_register_driver,而前文所述执行mmc_attach_mmc时会通过mmc_add_card将mmc_card注册到mmc bus,这样就触发了执行前文所述的mmc_blk_probe函数,后面有详细解释mmc_blk_probe的执行过程
mmc_blk_probe最主要的是初始化了request queue;初始化disk,同时通过mmc_add_disk将磁盘添加到系统中,使之可用
- mmc_blk_alloc_req
创建并初始化请求队列,启动线程循环抓取请求队列中的request,调用request处理函数进行处理
(1)分配mmc_blk_data结构体md并初始化,同时mmc_queue作为mmc_blk_data的成员也被创建
mmc_blk_data为block的核心结构体,与mmc_card关联,用于存放mmc_card相关数据,每个mmc slot即每个mmc设备对应一个mmc_blk_data结构体。 此处会分配mmc_blk_data结构体md,同时mmc_queue作为mmc_blk_data的成员也被创建。并标识dev_use的bitmap来记录已经分配的mmc device, 也就是说dev_use是与实际的物理设备相对应的,不是跟分区对应的,dev_use的index用dev_idx来记录 注意到此处MMC_BLK_DATA_AREA_MAIN表示主分区的区域(mmc_blk_data与设备对应,此处看又像是与分区对应??)。
MMC分区类型包括如下几种: #define MMC_BLK_DATA_AREA_MAIN (1<<0) #define MMC_BLK_DATA_AREA_BOOT (1<<1) #define MMC_BLK_DATA_AREA_GP (1<<2) #define MMC_BLK_DATA_AREA_RPMB (1<<3)(2) alloc_disk(perdev_minors):分配gendisk结构体保存到md中,gendisk与磁盘设备对应(3)mmc_init_queue(queue_c):创建并初始化请求队列 通过调用block子系统接口blk_init_queue来初始化请求队列,其中mmc_request_fn为处理请求的回调函数 blk_queue_prep_rq(mq->queue, mmc_prep_request)设定requet_queue的prep回调函数; mmc_alloc_sg(host->max_segs, &ret)分配max_segs个scatterlist用于request请求(只是分配scatterlist,并未分配存放数据的内存), 返回分配的scatterlist个数 kthread_run(mmc_queue_thread, mq) 起一个kennel thread运行mmc_queue_thread来处理上层发送下来的request,对每个reqeust执行issue_fn回调注:issue_fn回调在下面指定为mmc_blk_issue_rq
(4)指定issue_fn回调为mmc_blk_issue_rq,mmc_blk_issue_rq是具体的mmc request处理函数
- mmc_blk_alloc_parts(card, md))
- mmc_add_disk
为了将一个磁盘添加到系统中,对系统可用,必须初始化磁盘数据结构并调用add_disk方法。 需要特别注意的是一旦调用了add_disk,磁盘就被“激活”了,系统随时都可能会调用该磁盘提供的各种方法, 甚至在该函数返回之前就会调用,因而在完成磁盘结构的初始化之前,不要调用add_disk。
3.4 mmc_add_disk
mmc_add_disk->
device_add_disk
device_add_disk的原型为void device_add_disk(struct device *parent, struct gendisk *disk) 它完成的工作主要包括:
(1)根据磁盘的主次设备号信息为磁盘分配设备号;
(2)调用disk_alloc_events初始化磁盘的事件(alloc|add|del|release)处理机制。在最开始磁盘事件会被设置为被阻塞的。
(3)调用bdi_register_dev将磁盘注册到bdi_list,注:bdi用于将page_cache或buffer_cache中的脏数据刷新到磁盘
(4)调用blk_register_region将磁盘添加到bdev_map中(通过设备号可以获取kobject从而得到包含它的父对象进行操作)
(5)调用register_disk将磁盘添加到系统中。
(6)调用blk_register_queue注册磁盘的请求队列。主要是为队列和队列的调度器在设备的sys文件系统目录中创建相应的sys目录/文件,并且发出uevent事件。
(7)调用disk_add_events完成在/sys文件系统的设备目录下创建磁盘的事件属性文件,将磁盘事件添加到全局链表disk_events中,解除对磁盘事件的阻塞。
关于probe函数是如何被调用到的?
一般我们认为mmc_blk_probe的执行一定需要mmc_driver与mmc_device的匹配才可以,实际上没有mmc_device, 而是有mmc_card,mmc_blk_probe的执行经历如下历程:
(1)先来看mmc_register_driver的流程
mmc_register_driver->
driver_register->
driver_find//bus查看driver是否已经注册,如果已经注册则退出,否则bus add driver
bus_add_driver->
driver_attach->
bus_for_each_dev//此处由于还没有device注册,因此会退出
显然mmc_blk_probe的执行不是在mmc_register_driver的时候,那么肯定是在device_register的时候,看看我们的假设是否正确,继续往下看
(2)mmc_alloc_card, mmc_add_card
通过浏览代码,我们发现在mmc/core/bus_c中有mmc_alloc_card和mmc_add_card
mmc_alloc_card:mmc_attach_mm->mmc_init_card初始化并分配一个新的mmc_card结构体,实际上是创建device设备;
mmc_add_card:mmc_attach_mmc->mmc_add_card时调用,通过调用device_add(&card->dev)来完成设备的注册,过程如下:
mmc_add_card->
device_add->
bus_probe_device->
device_attach->
__device_attach->
driver_match_device->
mmc_bus_match//此函数的特殊之处在于总是返回值为1
driver_probe_device->
really_probe->
mmc_bus_probe->
mmc_blk_probe
mmc_blk_probe的执行不是靠device和driver的匹配,而是将匹配函数mmc_bus_match总是返回1,如下:
static int mmc_bus_match(struct device *dev, struct device_driver *drv)
{
return 1;
}
这样就可以执行到mmc_bus_type的probe函数进而执行到mmc_blk_probe。
3.5 mmc_queue_thread
线程处理函数,用于循环抓取请求队列中的request并交给请求处理函数进行处理
mmc_queue_thread->
blk_fetch_request
issue_fn(mmc_blk_issue_rq)->
mmc_blk_issue_rw_rq->
mmc_blk_rw_rq_prep
mmc_start_req –>
mmc_wait_for_data_req_done->
mmc_blk_err_check
host->ops->request
mmc_queue_thread是在mmc_init_quene中起的线程,主要作用是完成上层发送的请求进行处理
- blk_fetch_request
从请求队列中取出一个request
- issue_fn
由前面可知issue_fn在mmc_blk_probe->mmc_blk_alloc_req时将issue_rq初始化为mmc_blk_issue_rq,请求有几种包括:discard, flush, 以及rw
- mmc_blk_issue_rw_rq
首先通过mmc_blk_rw_rq_prep来做一些准备工作,获取命令号、命令参数等,然后通过mmc_start_req发起请求
- mmc_start_req
通过mmc_wait_for_data_req_done发起真正的请求,并等待请求结束。 mmc_wait_for_data_req_done会回调控制器的request函数发起请求,然后mmc_blk_err_check检查是否有错误发生, 如果有错误发生将尝试recovery进行修复开始新的传输
3.6 mmc_blk_issue_rq
mmc_blk_issue_rq->
mmc_claim_host
mmc_blk_part_switch
mmc_blk_issue_rw_rq->
mmc_blk_prep_packed_list
mmc_blk_rw_rq_prep
mmc_start_req->
__mmc_start_data_req
mmc_queue_bounce_post
检查mmc_start_req返回的状态
mmc_blk_issue_rq对发送的mmc request进行具体的处理。
- mmc_claim_host
实际上是声明当前进程占有host controller,如果有其它进程占有则需要等待,详细的可参考Linux mmc framework2:基本组件之core
- mmc_blk_part_switch
通过MMC_SWITCH命令对EXT_CSD寄存器的PARTITION_CONFIG(bit[179])进行设置,主要包括boot是否使能、用哪个分区做boot分区、选择要访问的分区。 如果MMC_SWITCH命令出错,将通过blk_end_request_all终止request
- mmc_blk_issue_rw_rq
根据req->cmd_flags的命令做不同的事情。REQ_SANITIZE、REQ_DISCARD、REQ_FLUSH分别为 . mmc_blk_issue_secdiscard_rq 和mmc_blk_issue_discard_rq . mmc_blk_issue_flush . mmc_blk_issue_rw_rq(这个是我们要分析的读写数据流程
1. mmc_blk_prep_packed_list尝试把当前request和队列中的其他request合并,以增强性能。是否可以合并,要依赖于: 控制器支持packed功能; device的MAX_PACKED_WRITES 大于0; 只对写request进行packed2. mmc_blk_rw_rq_prep:正常情况下执行mmc_blk_rw_rq_prep函数,从request构造mmc_request,毕竟下发给host请求,是mmc_request,而不是block层通用的request。 如果支持packed功能,那么就用pack_list来构造mmc_request3. mmc_start_req:mmc_start_req 启动一个非阻塞的request,这个函数会等待前一个request完成,然后启动当前requeset,并立刻返回
如果mmc_start_req返回的areq不为空,说明完成了上一次的request
- mmc_start_req
- mmc_start_req 启动一个非阻塞的request,这个函数会等待前一个request完成,然后启动当前requeset,并立刻返回 如果mmc_start_req返回的areq不为空,说明完成了上一次的request
1. 首先它会执行到mmc_wait_for_data_req_done函数,等待上一次的命令的完成,如果上一次未完成就会将当前进程加入等待队列休眠,等待被唤醒。
当上一次完成后会立即返回,并将上一次命令执行的状态返回给mmc_blk_issue_rw_rq。
2、if (host->areq) {
err = mmc_wait_for_data_req_done(host, host->areq->mrq, areq);
host->areq不为空,说明有正在处理的reuqest,函数mmc_wait_for_data_req_done用来等待这个host->areq,有两个条件会唤醒该MMC上下文: is_done_rcv和is_new_req
3. if (!err && areq)
start_err = __mmc_start_data_req(host, areq->mrq);
进入__mmc_start_data_req(host, areq->mrq);
(1)首先会将函数指针mmc_wait_data_done赋给mrq->done.
mmc_wait_data_done会设置context_info->is_done_rcv=true,这正好是唤醒mmc_wait_for_data_req_done的条件之一,然后调wake_up_interruptible(&context_info->wait);唤醒之。
(2)然后会调用mmc_start_request(host, mrq);
mmc_start_reuqest实际调用host->ops->request方法,进入了平台特定的request函数
进入特定的平台之后,会进入相应的中断对硬件进行读写的命令的执行,当命令执行完毕后,会进行函数回调调到刚才的mmc_wait_data_done唤醒等待的进程进行下一次命令的执行。
- mmc_queue_bounce_post
如果使用了bounce buffer,那么需要把传输结果从bounce buffer复制会sg buffer。 所谓bounce buffer是因为某些DMA控制器只能处理连续物理内存,此时需要通过bounce buffer来达到物理内存连续性。
- 检查mmc_start_req返回的状态
1. 如果是MMC_BLK_SUCCESS或者MMC_BLK_PARTIAL,需要调用blk_end_request通知block设备层,完成了本次读写request。 2. 如果是MMC_BLK_CMD_ERR,那么调用mmc_blk_reset复位host。调用mmc_blk_cmd_err尝试blk_end_request,如果发现reuqest未完成,说明本次操作失败,反之成功start_new_req
相关文章:
linux的mmc子系统与块设备关联
1.前言 本文主要block组件的主要流程,在介绍的过程中,将详细说明和block相关的流程,涉及到其它组件的详细流程再在相关文章中说明。 2.主要数据结构和API 2.1 struct mmc_card Elemete Namestruct mmc_cardPathinclude/linux/mmc/card.hRe…...

【Spring MVC】
目录 🍮1 什么是 MVC ? 🎁2 Spring MVC 的连接 🍘2.1 RequestMapping 实现 POST 和 GET 请求 🥣2.2 GetMapping 只支持 GET 请求 🫖2.3 PostMapping 只支持 POST 请求 🍬3 Spring MVC 获取参数的…...

【ES】笔记-生成器函数与调用
function* function* 这种声明方式 (function关键字后跟一个星号)会定义一个生成器函数 (generator function),它返回一个 Generator 对象。 function * gen(){console.log("heloo generator")}如何调用呢?先看下里面的内容 返回…...
将Spring Boot与Redis集成
一、引言 1、SpringBoot: Spring Boot是一个用于创建独立且可执行的Spring应用程序的框架。它简化了基于Spring框架的应用程序的开发过程,并提供了一种快速和简便的方式来构建Java应用程序。 Spring Boot提供了自动配置机制,通过引入适当的…...
vue echarts
安装 npm i -D echarts使用 <script setup lang"ts"> import * as echarts from echarts import { onMounted } from vueonMounted(() > {interface DataItem {value: [string, number]}let myChart echarts.init(document.getElementById(main))let dat…...

idea上利用JDBC连接MySQL数据库(8.1.0版)
1.了解jdbc概念 JDBC(Java DataBase Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种 关系数据库提供统一访问,它由一组用Java语言编写的类和接口组成。JDBC提供了一种基准,据此可以构建 更高级的工具和接口&#…...

【100天精通python】Day47:python网络编程_Web编程基础
目录 1 网络编程与web编程 1.1 网络编程 1.2 web编程 2 Web开发概述 3 Web开发基础 3.1 HTTP协议 3.2 Web服务器 3.3 前端基础 3.4 静态服务器 3.5 前后端交互的基本原理 4 WSGI接口 4.1 CGI 简介 4.2 WSGI 简介 4.3 定义 WSGI 接口 4.4 运行 WSGI 服务 4.5…...

DockerCompose介绍与使用
DockerCompose介绍与使用 1、DockerCompose介绍 DockerCompose用于定义和运行多容器 Docker 应用程序的工具。 通过 Compose可以使用 YAML 文件来配置应用程序需要的所有服务。一个使用Docker容器的应用,通常由多个容器组成,使用Docker Compose不再需要…...

Windows Qt 5.12.10下载与安装
Qt 入门实战教程(目录) C自学精简实践教程 目录(必读) 1 Qt5.12.10下载 qt-opensource-windows-x86-5.12.10.exe 官方离线安装包 Download Source Package Offline Installers | Qt 下载巨慢(也可能很快) 只能下载到最新的&…...
RustDesk最新版本编译与打包
本文环境 主要参考: https://www.yuque.com/shikangsi/efy0cp/wei3g1?https://blog.csdn.net/hualuohuakai2014/article/details/121605631 问题 flutter 生成 bridge 文件。 先安装工具,再生成ffi文件。 PS C:\Users\Administrator> cargo ins…...

Gin 框架入门实战系列(一)
GIN介绍 Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点 对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错 借助框架开发,不仅可以省去很多常用的封装带来的时间,…...

【测试】pywinauto的简单使用(安装、常用对象、元素控件、鼠标操作、键盘操作)
1.说明 pywinauto是一个用于自动化Python 模块,适合Windows系统的软件(GUI),可以通过Pywinauto遍历窗口(对话框)和窗口里的控件,也可以控制鼠标和键盘输入,所以它能做的事情比之前介…...
Java基础十八(正则表达式 + 日期时间)
1. 正则表达式 1.1 普通字符 字符描述示例[abc]匹配 […] 中所有字符[hlo] 匹配字符串 "hello world" 中所有的 h l o 字母[^ABC]匹配除了 […] 中所有字符[hlo] 匹配字符串 "hello world" 中除了 h l o 的所有字母[^a-z]匹配除了 […] 中所有字符[hlo] 匹…...

Linux C 多进程编程(面试考点)
嵌入式开发为什么要移植操作系统? 1.减小软硬件的耦合度,提高软件的移植性 2. 操作系统提供很多库和工具(QT Open CV),提高开发效率 3.操作系统提供多任务机制,______________________? (提高C…...
c++一级
与7无关的数 #include<iostream> #include<iomanip> using namespace std; int main() { int n,a,sum0,c0; cin>>n; for(int i1;i<n;i){ if(i%7!0){ ai; c0; …...

Code Lab - 34
GAT里面有一些地方看的不是太懂(GAT里Multi Attention的具体做法),暂时找了参考代码,留一个疑问 1. 一个通用的GNN Stack import torch_geometric import torch import torch_scatter import torch.nn as nn import torch.nn.fun…...
后端返回文件流,前端怎么导出、下载(8种方法可实现)
在前端导出和下载后端返回的文件流时,可以使用以下几种方法: 使用window.open()方法: 在前端使用window.open()方法打开一个新的窗口或标签页,并将后端返回的文件流作为URL传递给该方法。浏览器会自动下载该文件。例如:…...
什么是 ThreadLocal?
ThreadLocal 是 Java 中的一个类,用于在多线程环境下,为每个线程提供独立的变量副本。每个线程可以通过 ThreadLocal 存储和获取数据,而不会影响其他线程的数据。这在某些情况下非常有用,特别是当多个线程需要访问共享数据,但又希望保持数据的隔离性时。 ThreadLocal 主要…...

CANOCO5.0实现冗余分析(RDA)最详细步骤
在地理及生态领域会常使用RDA分析,RDA的实现路径也有很多,今天介绍一下CANOCO软件的实现方法。 1.软件安装 时间调整到2010年 2.数据处理 得有不同的物种或者样点数值,再加上环境因子数据。 3.软件运行 4.结果解读 结果解读主要把握这几点…...
【tkinter 专栏】掷骰子游戏
文章目录 前言本章内容导图1. 需求分析2. 系统功能结构3. 设计流程4. 系统开发环境5. 系统预览6. 窗口布局7. 功能实现用户和电脑选择骰子的点数大小摇骰子过程实现判断游戏结果单击开始按钮进行游戏源代码汇总前言 本专栏将参考《Python GUI 设计 tkinter 从入门到实践》书籍…...

Flask RESTful 示例
目录 1. 环境准备2. 安装依赖3. 修改main.py4. 运行应用5. API使用示例获取所有任务获取单个任务创建新任务更新任务删除任务 中文乱码问题: 下面创建一个简单的Flask RESTful API示例。首先,我们需要创建环境,安装必要的依赖,然后…...

8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
OpenLayers 分屏对比(地图联动)
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 地图分屏对比在WebGIS开发中是很常见的功能,和卷帘图层不一样的是,分屏对比是在各个地图中添加相同或者不同的图层进行对比查看。…...

RSS 2025|从说明书学习复杂机器人操作任务:NUS邵林团队提出全新机器人装配技能学习框架Manual2Skill
视觉语言模型(Vision-Language Models, VLMs),为真实环境中的机器人操作任务提供了极具潜力的解决方案。 尽管 VLMs 取得了显著进展,机器人仍难以胜任复杂的长时程任务(如家具装配),主要受限于人…...

给网站添加live2d看板娘
给网站添加live2d看板娘 参考文献: stevenjoezhang/live2d-widget: 把萌萌哒的看板娘抱回家 (ノ≧∇≦)ノ | Live2D widget for web platformEikanya/Live2d-model: Live2d model collectionzenghongtu/live2d-model-assets 前言 网站环境如下,文章也主…...

什么是VR全景技术
VR全景技术,全称为虚拟现实全景技术,是通过计算机图像模拟生成三维空间中的虚拟世界,使用户能够在该虚拟世界中进行全方位、无死角的观察和交互的技术。VR全景技术模拟人在真实空间中的视觉体验,结合图文、3D、音视频等多媒体元素…...
ubuntu22.04 安装docker 和docker-compose
首先你要确保没有docker环境或者使用命令删掉docker sudo apt-get remove docker docker-engine docker.io containerd runc安装docker 更新软件环境 sudo apt update sudo apt upgrade下载docker依赖和GPG 密钥 # 依赖 apt-get install ca-certificates curl gnupg lsb-rel…...
13.10 LangGraph多轮对话系统实战:Ollama私有部署+情感识别优化全解析
LangGraph多轮对话系统实战:Ollama私有部署+情感识别优化全解析 LanguageMentor 对话式训练系统架构与实现 关键词:多轮对话系统设计、场景化提示工程、情感识别优化、LangGraph 状态管理、Ollama 私有化部署 1. 对话训练系统技术架构 采用四层架构实现高扩展性的对话训练…...