一个INFJ的碎碎念，关于技术、生活、学习等等
写在前面：本文包含的代码：main.zip如果你问春节期间谁最火，哪吒2和Deepseek不分伯仲，以后有机会蹭哪吒2，今天我们先蹭一下Deepseek。其实，Deepseek还没有这么火的时候，我就已经开始使用他家的API了，最初是在Obsidian里嵌入了Deepseek，作为AI辅助工具，让它帮我整理和检索笔记，用得不亦乐乎，然后我给身边很多人安利了他家的模型，当然了，它的爆火跟我的安利不能说一点关系没有，至少没有一点关系😊。看着以前自己的“暗恋对象”火出圈变成了“大众情人”，这种心情还是蛮复杂的，一方面说明自己眼光不错，值得开心；但另一方面也着实增添了很多使用中的烦恼——最近就频频遇到服务器超时的问题——这是另外的话题，我们先按下不表，今天以一个具体的例子来分享一下如何使用Deepseek这样的AI工具辅助解决代码中的问题。叠甲：我对JS以及Web不是懂一点，是一点都不懂，所以我的解决方案很可能看起来很傻很笨，大家轻喷，这里只是分享一种在遇到我们不熟悉的问题时如何借助AI工具来分析和解决的思路，供大家参考；多少有点标题党了，是否使用Deepseek在本问题中并不是关键，可能其他模型也可以达到相同的目的，但是我个人使用下来，Deepseek几乎是效果最好的。问题是什么？Obsidian的编辑器宽度在不使用插件或者CSS片段的情况下只有两种格式，最窄或者最宽（通过关闭setting->Editor->Readable line length来实现），可以用但是不够美观，这种遗憾正中完美主义者的七寸，让我很难受:我想要一种解决方案：可以灵活调整编辑器的宽度；最好每一个笔记可以配置不同的宽度；打瞌睡的时候递枕头，确实有一款插件可以满足我的需求：MugishoMp/obsidian-editor-width-slider: With this plugin you can set the line width of the editor in obsidian.下面是其支持的功能：FeaturesAdjust the line width of the editor using a slider in the status bar.Increase or decrease the line width to customize your editing experience.Simple and intuitive interface for easy usage.Feature: Custom for Individual Files using YAMLWith the Obsidian Line Width Slider Plugin, you can now customize the editor width for individual files by specifying an "editor-width" field in the YAML frontmatter of your notes. This feature allows you to have different line widths for different files, giving you greater flexibility in your note-taking and editing experience.Setting the Editor WidthTo set a custom editor width for a specific file, follow these steps:Open the note for which you want to customize the editor width.In the YAML frontmatter section at the top of the note, add an "editor-width" field. The value of this field should be a number between 0 and 100, representing the desired editor width as a percentage of the viewport width. For example:--- title: My Customized Note editor-width: 75 ---从描述来看，该插件完美契合我的需求，但是在使用的过程中遇到了两个问题：在Minimal主题下，该插件不工作；无法为不同的笔记配置不同的宽度（Obsidian版本, v1.8.4），这个问题跟主题无关，我试过Minimum，Default以及Primary，都有问题。是否是已知问题？经过简单的搜索，上述问题1是已知问题，解决办法可参考：Does not work with the "Minimal" theme · Issue #6 · MugishoMp/obsidian-editor-width-slider问题2也有人遇到了，但是目前没有解决办法：editor-width not working? · Issue #19 · MugishoMp/obsidian-editor-width-slider，当然，也许问题的提问者已经有解决办法，只是没有更新。OK，针对第二个问题，动手吧！调查过程Code分析及日志追踪通过在该插件的main.js文件中搜索YAML，找到了如下函数：updateEditorStyleYAML() { console.log("1.1"); const file = this.app.workspace.getActiveFile(); console.log("1.2"); if (file.name) { const metadata = app.metadataCache.getFileCache(file); if (metadata) { if (metadata.frontmatter) { try { if (metadata.frontmatter["editor-width"]) { if (this.validateString(metadata.frontmatter["editor-width"])) { this.updateEditorStyleYAMLHelper(metadata.frontmatter["editor-width"]); } else { new WarningModal(this.app).open(); throw new Error("Editor width must be a number from 0 to 100."); } } else { this.updateEditorStyle(); } } catch (e) { console.error("Error:", e.message); } } else { this.updateEditorStyle(); } } } }虽然不懂JS，但是大概猜测，这个函数就是用来根据YAML里的值渲染编辑器宽度的，查看他的调用者：// most important function, this gets executed everytime the plugin is first // loaded, e.g. when obsidian starts, or when the user just installed the // plugin async onload() { await this.loadSettings(); this.addStyle(); this.app.workspace.on("file-open", () => { this.updateEditorStyleYAML(); }); this.createSlider(); this.addSettingTab(new EditorWidthSliderSettingTab(this.app, this)); }根据注释，这个函数在插件第一次被启用时会被调用，可以看到，里面注册了一个“file-open”类型的事件，猜测作者期望文件被打开时自动触发调用updateEditorStyleYAML()。然后我在updateEditorStyleYAML()这个函数的关键节点增加了一些控制台日志，在Obsidian的debug模式下观察是否一个笔记被打开时，是否有日志输出，发现并没有，说明注册“file-open”事件时并没有起作用。问Deepseek文章写了半天，该Deepseek上场了，再不上场真是标题党了。通过询问Deepseek，它给出了如何在文件被打开时调用插件的解决方案，通过注册layoutChange或者layoutReady事件来实现：我采用的是layoutChange的方法，在onload函数里添加了如下代码：this.registerEvent( this.app.workspace.on('layout-change', () => { const activeFile = this.app.workspace.getActiveFile(); if (activeFile) { this.updateEditorStyleYAML(); } }) );经过测试，好使！😀个人猜测是因为新版的Obsidian去掉了对于file-open类型事件的支持导致插件不再work。继续优化除了文件被打开时使用YAML渲染，我还希望优化两点：当YAML的editor-width被修改时，实时渲染；当一个笔记的YAML里已经定义了editor-width时，调节slider不对该文件起作用。类似的，Deepseek给出了它的建议：照葫芦画瓢，我在onload函数里添加了如下代码:this.registerEvent( this.app.metadataCache.on("changed", (file) => { const metadata = this.app.metadataCache.getFileCache(file)?.frontmatter; if (metadata) { console.log("mingw, Metadata is changed:", metadata); this.updateEditorStyleYAML(); } }) );效果不错，如下：如果我们把YAML里的editor-width去掉，效果如下：修改后的main.js：main.zip尝试让AI直接编辑代码必须承认，上面的方式还是太初级了，不够暴力，如果能够让AI直接解bug并且修改代码，才算得上真正的智能，于是我尝试了一下在Visual Studio Code里让Deepseek直接修改。AI确实能够直接修改代码，但是也许是因为我的提示词不够准确，它修改的方向似乎并不正确，使用了AI修改的文件并没有解决问题。因为原始问题已经解决，所以我没有花太多的时间去修正我的提示词，可能需要一个迭代的过程才能让AI真正解决问题，后续如果有结果我会更新给大家，欢迎关注。文章版本2025-02-11: 第一版。最后🤝如需转载，请注明出处和原始链接😄欢迎关注我的公众号:🍅如果您觉得有帮助，欢迎点击下方的“打赏”请作者喝西红柿汤。
本文主要介绍了NVMe中的SGL，通过本文您将了解到以下没用的知识：什么是SGL？包括它的特点、结构、使用方法等；NVMe Linux驱动如何使用SGL？PCIe trace中的SGL。什么是SGL?在传输数据时，SSD需要知道数据在Host内存中的地址，否则它不知道数据应该从哪儿搬（写方向）以及搬到哪儿（读方向），也就是说，SSD需要知道数据在Host内存中的分布，而这个信息，是Host通过某种方式告诉SSD的，SGL就是“某种方式”之一。类比“地理藏宝”SGL是Scatter Gather List的简称，顾名思义，SGL是一条链子，将散落在不同地方的数据串在一起，有点像我们玩过的“地理藏宝”的游戏：没喻硬比：游戏里的每一个地点相当于SGL里的SGL segment；游戏里每一个地点藏有下一个地点的信息（除了最后一个），SGL里每一个SGL Segment保存下一个SGL Segment的地址（除了最后一个）；游戏里每一个地点藏有一个（大多数）或多个（少数）宝藏，而每一个SGL Segment包含一个或多个SGL descriptor；SGL特点SGL具有以下特点：SGL是一个链表；链表的每一个节点是一个SGL Segment；一个SGL Segment包含一个或多个SGL Descriptor；SGL Descriptor有多种类型，常用的有SGL data block descriptor，SGL Segment descriptor以及SGL Last Segment descriptor等；每一个SGL Descriptor指向了一段Memory buffer；一个SGL的第一个SGL Segment位于command内部（dword 6 - 9,16 Bytes），这个SGL Segment只包含一个SGL Descriptor。下图描述了一种通用情况下，SGL是如何表达数据分布的：SGL里各个概念之间的关系可以描述为：SGL Descriptor在SGL中，最基本的功能单位是SGL Descriptor，它就像一座建筑的板砖，构建了整个SGL的大厦。SGL Descriptor的结构SGL Descriptor的size是固定的16个字节，其中，第15个字节（0-based）的Bits[07:04]是SGL Descriptor type，表示该SGL Descriptor的类型；第15个字节（0-based）的Bits[03:00]是SGL Descriptor Sub Type，表示SGL Descriptor的子类型；其他的字段，根据Descriptor的类型而具有不同的定义。SGL Descriptor的种类SGL Descriptor的类型主要有以下几种：其中，绿色的SGL Data Block descriptor, SGL Segment descriptor和SGL Last Segment descriptor是最常用的三种类型，而粉色的SGL Bit Bucket descriptor是次常用的，剩下的是最不常用的，我们主要关注有颜色的四种类型即可。SGL Data Block descriptorSGL Data Block descriptor是最常用的descriptor，它可以直接描述用户数据的排布。BytesDescription07:00Address(ADDR):如果SGLDST=0h, 该字段表示存放用户数据的绝对地址；否则，如果SGLDST=1h，该字段表示存放用户数据的相对地址，主要用于NVMe over fabric协议中命令和数据捆绑在一起的情况。另外，如果SSD要求用户数据的地址需要dword对齐（在Identify controller数据中体现），那么该字段低2位比特需要清零。11:08Length (LEN):表示用户数据的长度，以字节为单位。如果SSD要求用户数据的颗粒度需要dword对齐的话，该字段的低2位比特需要清零。14:12预留15SGL Identifier (SGLID):Bits 07:04: SGL Descriptor Type (SGLDT): 0h, 表示是Data Block descriptor；Bits 03:00: SGL Descriptor Sub Type (SGLDST): 如果是0h，ADDR=绝对地址；如果是1h，ADDR=相对地址SGL Segment descriptorSGL Segment descriptor是用于描述SGL Segment的描述符，通过它，SSD可以知道下一个SGL Segment的位置以及该SGL Segment包含多少个SGL descriptor。BytesDescription07:00Address(ADDR):如果SGLDST=0h, 该字段表示存放SGL Segment的绝对地址；否则，如果SGLDST=1h，该字段表示存放SGL Segment的相对地址，主要用于NVMe over fabric协议中命令和SGL Segment捆绑在一起的情况.11:08Length (LEN):表示用户数据的长度，以字节为单位，并且是16的整数倍，因为一个SGL descriptor的size是16个字节。该SGL Segment descriptor指向的SGL Segment内含有的descriptor的数量为：LEN/16.14:12预留15SGL Identifier (SGLID):Bits 07:04: SGL Descriptor Type (SGLDT): 2h, 表示是SGL Segment descriptor；Bits 03:00: SGL Descriptor Sub Type (SGLDST): 如果是0h，ADDR=绝对地址；如果是1h，ADDR=相对地址SGL Last Segment descriptorSGL Last Segment descriptor跟SGL Segment descriptor是一样的，唯一的区别是，它指向的是最后一个SGL Segment。BytesDescription07:00Address(ADDR):如果SGLDST=0h, 该字段表示存放SGL Last Segment的绝对地址；否则，如果SGLDST=1h，该字段表示存放SGL Last Segment的相对地址，主要用于NVMe over fabric协议中命令和SGL Last Segment捆绑在一起的情况.11:08Length (LEN):表示用户数据的长度，以字节为单位，并且是16的整数倍，因为一个SGL descriptor的size是16个字节。该SGL Segment descriptor指向的SGL Last Segment内含有的descriptor的数量为：LEN/16.14:12预留15SGL Identifier (SGLID):Bits 07:04: SGL Descriptor Type (SGLDT): 3h, 表示是SGL Last Segment descriptor；Bits 03:00: SGL Descriptor Sub Type (SGLDST): 如果是0h，ADDR=绝对地址；如果是1h，ADDR=相对地址SGL Bit Bucket descriptorSGL Bit Bucket descriptor是用于描述在read时（数据从SSD到Host）需要跳过或者忽略的数据size.BytesDescription07:00预留11:08Length (LEN):以字节为单位，表示需要跳过或者忽略传输的数据size。14:12预留15SGL Identifier (SGLID):Bits 07:04: SGL Descriptor Type (SGLDT): 1h, 表示是SGL Bit Bucket descriptor；Bits 03:00: SGL Descriptor Sub Type (SGLDST): 0h其他类型的SGL descriptor请参考NVMe Base Spec。An Example下面是一个肤浅的例子，讲述的是Host读取LBA x到LBA x+25的数据，其中有2KiB的数据需要忽略：除了SGL，还有谁？SGL并不是唯一一种描述数据分布的方式，甚至在NVMe over PCIe中，SGL只是可选的，而PRP（下一篇文章我们将介绍PRP，欢迎关注）才是必须支持的；相反，在NVMe over fabric中，SGL是必须的：Data Pointer LayoutNVMe over PCIeNVMe over FabricPRPMust Support, admin must use PRPNot SupportedSGLOptional, only I/O can use itMust Support, used by both admin and I/O绝大部分普通消费者所持有的NVMe SSD，都是NVMe over PCIe。Host如何知道SSD是否支持SGL？Host是通过Identify Controller数据中的Byte [539:536]4个字节来获知SSD是否支持SGL的。SSD怎么知道用SGL还是PRP？在每一个Host生成的IO command里，都有一个字段来表示该IO command使用PRP还是SGL的方式做数据传输，所以理论上SGL和PRP是可以混合使用的，部分IO使用PRP，部分IO使用SGL。该字段位于dword 0的bit [15:14]:如果是00b, 表示使用PRP传输；如果是01b，同时，LBA格式带有Meta data（比如512+8,4KiB+64等），那么user data使用SGL传输，而Meta data使用连续的buffer；如果是10b，表示user data和meta data都是用SGL传输。Linux NVMe驱动如何决定是否使用SGL？static inline bool nvme_pci_use_sgls(struct nvme_dev *dev, struct request *req, int nseg) { struct nvme_queue *nvmeq = req->mq_hctx->driver_data; unsigned int avg_seg_size; avg_seg_size = DIV_ROUND_UP(blk_rq_payload_bytes(req), nseg); if (!nvme_ctrl_sgl_supported(&dev->ctrl)) return false; if (!nvmeq->qid) return false; if (nvme_pci_metadata_use_sgls(dev, req)) return true; if (!sgl_threshold || avg_seg_size < sgl_threshold) return nvme_req(req)->flags & NVME_REQ_USERCMD; return true; }通过以上代码可以知道，如果NVMe controller支持SGL，并且是非admin command，并且平均的Segment size小于threshold（默认是32K），那么驱动将会使用SGL。所以，如果要使用默认驱动测试SGL，可以把sgl_threshold设置到最小（1），这样就很容易满足使用SGL的条件。一个栗子，PCIe Trace里的SGL下面是一个炒熟的栗子，包含了一个使用SGL做数据传输的Read command：我们稍微解释一下这个trace，另外，在上图和下文中，我用不同的颜色来标识不同的字段，方便您对应和理解，供参考。其中，完整的Read command （16个dword）是：03E54002 00000001 00000000 00000000 00000000 00000000 1A911000 0000000400000020 30000000 00000208 00000000 340000FF 00000000 00000208 00000000可以看到：DW 0是03E54002h, 其中bit 15:14（PSDT）为01b, 根据前面的章节“1.3.2 SGL Descriptor”， 01b表示使用SGL做数据传输；DW 6-9是1A911000 00000004 00000020 30000000，这是一个SGL descriptor，根据前面的章节“1.3.2 SGL Descriptor”，第15个字节的bit 7:4（SGL descriptor type）是3，3表示该descriptor是SGL Last Segment descriptor，其中address是04_1A911000h, 而Length是20h，也就是说，在04_1A911000h存放着一个SGL Last Segment，其中包含20h / 16 = 2个SGL descriptor；DW 12是340000FFh，其中bit 15:00 (Number of Logical Blocks)是FFh，表示256个LBA，这条trace是在一个LBAF=512的SSD上抓的，因此数据量是256*512=131072字节，也就是128KiB；下面我们看一下具体数据传输的过程：首先，SSD从地址04_1A911000h地址拿到2个descriptor，分别为：365BE000 00000004 00010000 00000000和365CE000 00000004 00010000 00000000；其中，365BE000 00000004 00010000 00000000的SGL descriptor type是0，表示该descriptor是一个SGL Data Block descriptor，其Address=04_365BE000h, Length=00010000h, 也就是说，在地址04_365BE000h存放着64KiB的数据；同理，365CE000 00000004 00010000 00000000也是一个SGL Data Block descriptor，表示在地址04_365CE000h存放着128KiB中剩下的64KiB的数据；现在所有数据的排布都已知晓，因此，下一步是分别将128KiB的数据DMA到对应的地址；最后，发送CQ post和Interrupt，告诉Host命令执行完毕。文章版本2025-02-07: v0.1，初始版本2025-02-12：v0.2，去掉了文章原始的标题序号最后🤝如需转载，请全文转载并注明出处和原始链接😄欢迎关注我的公众号:🙃欢迎来我的菜园子逛逛：闷仙的菜园子 记录和分享，学习与进步🧡如果，这篇文章帮助到了您，欢迎投喂：