<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>onemotre&apos;s Blog</title><description>Investigate the principles</description><link>https://blog.onemom.top/</link><language>zh_CN</language><item><title>【DotFile】如何优雅地配置使用Fcitx5——一文搞懂如何配置fcitx5环境变量</title><link>https://blog.onemom.top/posts/fcitx5_full/</link><guid isPermaLink="true">https://blog.onemom.top/posts/fcitx5_full/</guid><description>如何配置Fcitx5，以Fedora为例</description><pubDate>Tue, 26 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;:::tip
官方文档：https://fcitx-im.org/wiki/Fcitx_5
:::&lt;/p&gt;
&lt;p&gt;在Linux上进行高效的中文输入是每个Linux新手进入Linux社区的第一个挑战（前提你不是通过Arch入门Linux的）。在众多的输入法引擎中，Fcitx5凭借着原生Wayland支持，较低的内存占用，流畅的输入体验，成为了许多Linux用户的首选。&lt;/p&gt;
&lt;p&gt;这里我以Fedora为例，说明如何在Linux上优雅地配置使用Fcitx，包括如何如何配置Electron，Flatpak应用，以及如何在Wayland上得到更好的输入体验。&lt;/p&gt;
&lt;p&gt;如果你只是想知道如何配置Electron应用，且又恰好使用Hyprland作为DE，我推荐你参考下面这篇博客 &lt;a href=&quot;%5Bhttp://onemom.top/posts/fcitx5_on_hyprland/%5D(http://onemom.top/posts/fcitx5_on_hyprland/)&quot;&gt;【踩坑】在Wayland桌面使用Fcitx5在QQ,微信中输入中文&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;安装Fcitx5&lt;/h1&gt;
&lt;p&gt;大多数发行版都在自己的仓库中添加了Fcitx5本体，配置工具，以及中文输入引擎。，通过以下命令可以进行安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# CentOS
sudo yum install fcitx5 fcitx5-configtool fcitx5-chinese-addons
# Fedora
sudo dnf install fcitx5 fcitx5-configtool fcitx5-chinese-addons
# Archlinux
sudo pacman -Sy fcitx5 fcitx5-configtool fcitx5-chinese-addons
# Debian/Ubuntu
sudo apt-get install fcitx5 fcitx5-configtool fcitx5-chinese-addons
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;针对不同的桌面环境，可能需要安装不同的支持，这里推荐都安装：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fcitx5-gtk&lt;/li&gt;
&lt;li&gt;fcitx5-qt&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;系统环境设置&lt;/h1&gt;
&lt;p&gt;这部分就是踩坑的重灾区了，大多数问题人碰到的问题基本上都来自与这里。因为不同环境开发都有各自的环境封装方法，不同的封装方法又可能导致不同的DBus（Linux进程间通信）通信协议，导致输入法连接不上也好，渲染奇怪也好，总会在这里出现问题。这里分为几个模块来说。&lt;/p&gt;
&lt;h2&gt;自启动&lt;/h2&gt;
&lt;p&gt;特定的发行版可能会提供一些用于自动启动 Fcitx 的工具，并且这些工具通常也会设置环境变量。例如Debian/ubuntu系的&lt;code&gt;im-config&lt;/code&gt;，只需要在im-config中选择了fcitx作为输入后端，重新登录之后基本上都能正常启动。&lt;/p&gt;
&lt;p&gt;对与KDE桌面来着，fcitx5的自启动被加载到KWin中，只需要确保在KWin管理窗口中添加fcitx5即可。&lt;/p&gt;
&lt;p&gt;其他的DE,如平铺式窗口管理器，基本上都需要在各自的配置文件中，添加启动项即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hyprland中，可以在&lt;code&gt;$XDG_CONFIG_HOME/hypr/hyprland.conf&lt;/code&gt;下添加一行命令即可：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;exec-once = fcitx5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::warn
就在我写这里的时候，Hyprland v0.54+已经将默认的配置文件更改为了&lt;code&gt;$XDG_CONFIG_HOME/hypr/hyprland.lua&lt;/code&gt;，上述的语法可能已经不使用了。
&lt;s&gt;居然又改了吗，看来放弃使用Hyprland是个明智的选择，如果你使用arch+Hyprland，那你可能就得把精力放到如何去维护自己的配置上，而不是开发上了&lt;/s&gt;
:::&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;niri中，可以在&lt;code&gt;$XDG_CONFIG_HOME/niri/config.kdl&lt;/code&gt;中添加如下命令：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;spawn-at-startup &quot;fcitx5&quot; &quot;-d&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;系统环境变量&lt;/h2&gt;
&lt;p&gt;系统环境设置主要要看所有和窗口管理器直接通信的应用，例如记事本，终端等等。这部分只要注意系统的DE就可以解决。除此之外呢，就是配置文件加载的顺序问题了。&lt;/p&gt;
&lt;p&gt;先说配置文件的加载顺序问题。&lt;/p&gt;
&lt;p&gt;如果你使用Bash作为你的登录shell，&lt;code&gt;~/.bash_profile&lt;/code&gt; 则是一个很好的用户级环境变量配置。它受到不同 DM 的广泛支持，即使你从 TTY 启动界面，它也可以工作。如果你不使用 bash，可能就得仔细检查你的 shell 配置文件是否可以用作设置环境变量的位置。&lt;/p&gt;
&lt;p&gt;同理，在平铺式DE中，你也可以在其配置文件中加载你的配置，如niri中，你可以在&lt;code&gt;$XDG_CONFIG_HOME/niri/config.kdl&lt;/code&gt;合适的位置添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;environment {
	QT_IM_MODULE &quot;fcitx&quot;
	XMODIFIERS &quot;@im=fcitx&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输入协议关注的变量主要就是三个变量：&lt;code&gt;XMODIFIERS&lt;/code&gt;，&lt;code&gt;GTK_IM_MODULE&lt;/code&gt; 和&lt;code&gt;QT_IM_MODULE&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;也就是说使用什么环境变量，取决于你使用什么样的窗口协议（X11, Wayland还是XWayland）分别指定不同ui工具开发的软件使用什么进行协议进行输入。&lt;/p&gt;
&lt;p&gt;:::tip
你可以使用&lt;code&gt;fcitx5-diagnose&lt;/code&gt;查看当前fcitx5的环境变量问题
:::&lt;/p&gt;
&lt;h4&gt;GTK_IM_MODULE&lt;/h4&gt;
&lt;p&gt;在理想的情况下，你应该在 X11 下运行的 GTK 程序用 im-module，而在纯 Wayland 程序中用 GTK 的 text-input-v3 支持。其方法是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;不要设置 GTK_IM_MODULE 环境变量！&lt;/li&gt;
&lt;li&gt;对于 Gtk2，向 ~/.gtkrc-2.0 添加如下内容：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;gtk-im-module=&quot;fcitx&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;对于 Gtk3，向 ~/.config/gtk-3.0/settings.ini 添加如下内容：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[Settings]
gtk-im-module=fcitx
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;对于 Gtk4。向 ~/.config/gtk-4.0/settings.ini 添加如下内容：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;[Settings]
gtk-im-module=fcitx
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;如果你在用 GNOME 3 和以上版本，你还需要运行以下命令：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;gsettings set org.gnome.settings-daemon.plugins.xsettings overrides &quot;{&apos;Gtk/IMModule&apos;:&amp;lt;&apos;fcitx&apos;&amp;gt;}&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 GTK_IM_MODULE 环境变量，目前现代 Gtk3/4 应用程序应当能够使用 text-input-v3 协议（该协议已被绝大多数合成器支持，Weston 除外）。不设置 GTK_IM_MODULE 的时候，Gtk3/Gtk4 就会自动启用内置的 Wayland 输入法模块。虽然你也可以显式指定 GTK_IM_MODULE=wayland，但是 Gtk2 程序也会读取这个环境变量，导致兼容问题。设置 GTK_IM_MODULE=fcitx 依然是可以工作的，而且如果你使用的合成器不兼容 Wayland 输入法协议，这也是你的唯一选择。&lt;/p&gt;
&lt;p&gt;你可以在 Gtk 的配置文件里强制使用某个 IM Module，所以就算你不设置 GTK_IM_MODULE 环境变量也仍然是可以用不同的 IM Module 的。&lt;/p&gt;
&lt;p&gt;根据 Gtk 的实现，优先级顺序如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;X11
&lt;ul&gt;
&lt;li&gt;环境变量中的 GTK_IM_MODULE&lt;/li&gt;
&lt;li&gt;XSettings 中 Gtk/IMModule 的值&lt;/li&gt;
&lt;li&gt;配置文件中的值&lt;/li&gt;
&lt;li&gt;基于区域设置的自动选择&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Wayland
&lt;ul&gt;
&lt;li&gt;环境变量中的 GTK_IM_MODULE&lt;/li&gt;
&lt;li&gt;&quot;wayland&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;QT_IM_MODULE&lt;/h4&gt;
&lt;p&gt;目前，QT_IM_MODULE 只能使用其自身的 &lt;code&gt;text-input-v{2,4}&lt;/code&gt;，而该模块仅受 KWin 支持。这意味着，在 KDE 环境下，你应该取消设置该参数；但在其他桌面环境下，你需要将其设置为 QT_IM_MODULE=fcitx。此外，市面上存在的 Qt 专有应用程序也使情况更加复杂。有些应用程序不支持 Wayland，有些则没有捆绑/提供 Qt Wayland。大多数应用程序没有捆绑 fcitx 输入模块，因为 fcitx 只是 Qt 的第三方应用程序，但我注意到有些应用程序虽然捆绑了 fcitx，但并未包含所有必需的库。虽然 fcitx 5 支持 ibus 协议，但有些应用程序甚至没有捆绑 ibus 输入模块。你可以尝试一些不同的环境变量来处理这些 Qt 专有应用程序（例如 WPS、Anki、DaVinci Resolve、Mathematica 等）。&lt;/p&gt;
&lt;p&gt;在 Qt 6.7 中，新增了一个名为“QT_IM_MODULES”的环境变量，允许您指定 im 模块的回退顺序。您可以将其设置为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;QT_IM_MODULES=&quot;wayland;fcitx;ibus&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;上述的配置只是告诉了我们应该在什么条件下设置什么样的&lt;code&gt;GTK_IM_MODULE&lt;/code&gt;和&lt;code&gt;QT_IM_MODULE&lt;/code&gt;实际上这还与所运行的程序以那种窗口通信协议有关。&lt;/p&gt;
&lt;h3&gt;在 XWayland 下运行的传统 X11 应用程序&lt;/h3&gt;
&lt;h2&gt;对于特定应用的特定环境设置&lt;/h2&gt;
&lt;p&gt;基于某些框架开发，或者通过沙盒执行的程序，其环境变量与系统定义的变量有所差异，这里列举几种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Electron / Chromium&lt;/li&gt;
&lt;li&gt;flatpak&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Electron / Chromium 应用的兼容性&lt;/h4&gt;
&lt;p&gt;fcitx是原生支持Electron应用的，但其默认是走X11通信协议，若系统是运行在Wayland环境下的，则会走XWayland协议，此时需要读取GTK_IM_MODULE和QT_IM_MODULE环境变量，这会导致冲突，因此需要运行程序之前，强制Electron应用走Wayland通信协议，如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# If your compositor supports text-input-v1 protocol. Check the compositor section below.
chromium --enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# If your compositor &amp;amp; chromium supports text-input-v3 protocol, you may also use
chromium --enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime --wayland-text-input-version=3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# If your compositor supports text-input-v1 protocol. Check the compositor section below.
code --enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
在该死的微信中，除了要设置强制使用原生wayland，还需要设置wayland环境变量，但效果仍然不如人意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动后缀&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;--enable-features=UseOzonePlatform --ozone-platform=wayland --enable-wayland-ime --gtk4 --force-device-scale-factor=1.5
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;环境设置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;GTK_IM_MODULE=fcitx QT_IM_MODULE=fcitx XMODIFIERS=@im=fcitx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h4&gt;Flatpak沙盒环境&lt;/h4&gt;
&lt;p&gt;在沙盒环境中，因为其环境变量与系统外是隔离的，因此可能会缺少输入法的环境变量。好在Flatpak可以对但个应用设置环境变量，可以达到我们需要的效果。这里以&lt;code&gt;Obsidian&lt;/code&gt;为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flatpak override --user \
    --env=XMODIFIERS=@im=fcitx \
    --filesystem=xdg-config/fcitx5:ro \
    md.obsidian.Obsidian
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--env=XMODIFIERS=@im=fcitx&lt;/code&gt;：让 Obsidian 连接 &lt;code&gt;fcitx5&lt;/code&gt; 输入法服务。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--filesystem=xdg-config/fcitx5:ro&lt;/code&gt;：授予 Obsidian &lt;strong&gt;只读&lt;/strong&gt; 权限访问 &lt;code&gt;~/.config/fcitx5&lt;/code&gt;，以便正确加载皮肤和配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不需要&lt;/strong&gt;额外设置 &lt;code&gt;GTK_IM_MODULE&lt;/code&gt; 或 &lt;code&gt;QT_IM_MODULE&lt;/code&gt;（它们的值应为空），否则会干扰原生 Wayland 应用的正常工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除此之外，推荐设置强制在原生wayland下运行，不然会出现XWayland与Wayland下fcitx皮肤渲染DPI不一致的问题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flatpak override --user --socket=wayland --nosocket=x11 md.obsidian.Obsidian
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
可以通过如下命令查看当前flatpak设置的环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flatpak override --user --show md.obsidian.Obsidian
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如下命令可以取消设置的环境变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;systemctl --user unset-environment GTK_IM_MODULE QT_IM_MODULE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【ROS】ROS+Docker：分离工作系统与ROS版本紧耦合</title><link>https://blog.onemom.top/posts/ros-docker/</link><guid isPermaLink="true">https://blog.onemom.top/posts/ros-docker/</guid><description>ROS+Docker的工作流</description><pubDate>Sun, 24 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;背景&lt;/h1&gt;
&lt;p&gt;在做Robotics相关工作的学习时，常常需要复现一些基于ROS系统开发的算法工作。为了让复现的结果尽可能贴近原工作呈现的效果，以及不想碰到一些奇奇怪怪的编译、环境问题，我总是去安装特定的Ubuntu系统，然后在安装好的系统上安装配置ROS环境。前些年这样的工作也许就只是在ros noetic与ros humble中进行切换，我也就在我的工作电脑上安装了Ubuntu20.04与Ubuntu22.04两个环境。&lt;/p&gt;
&lt;p&gt;但随着Claude Code CLI, Codex, Opencode等AI Agent的发展，不使用这种AI工作流进行科研学习已经很难做到。这时候，如果你尝试在Ubuntu20.04上尝试安装一些AI工具，会发现因为过老的软件库版本，很多工具已经不能直接使用apt进行安装。也许你可以通过一些特殊的技巧，自己解决系统中的环境依赖问题，但这也会导致当前进行工作的系统变得复杂，时间久了难以维护。&lt;/p&gt;
&lt;p&gt;当然，还有一个原因是我作为一个dotfile爱好者，在两个系统中配置各种工具，实在是一件很蠢的事情，并且不同的系统之间很多时候不能使用同一套配置，你得根据系统的不同去修改你的配置文件。除此之外一些工具也停止对旧的系统进行支持，例如在Ubuntu20.04上，你会因为&lt;code&gt;libwebkit2gtk&lt;/code&gt;和&lt;code&gt;libjavascriptcoregtk&lt;/code&gt;而无法直接通过&lt;code&gt;apt&lt;/code&gt;安装Clash Verge。&lt;s&gt;对我来说还有配NeoVim和Fcitx5的时候遇到的系统gcc版本的问题&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;既然Ubuntu也无法提供学习时开箱即用的便利，那就决定干脆一点，直接放弃使用Ubuntu。使用Fedora作为我的工作系统，这样不仅能享受到更新的内核更新，享受到各种DE新的特性，还能满足我的折腾需求，何乐而不为呢。至于ROS项目嘛，就交给Docker了。&lt;/p&gt;
&lt;h1&gt;Docker安装&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;记录一下在Fedora上安装Docker的过程&lt;a href=&quot;https://docs.docker.com/engine/install/fedora/#install-using-the-repository&quot;&gt;Docker 官方文档&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Docker推荐直接安装Docker Desktop以获得完整的支持，但据我目前的使用感受来说，不管是在Ubuntu上，还是在Windows上，Docker Desktop都挺鸡肋的，平时基本打不开，管理镜像的时候不如命令行方便，加上这玩意儿的GUI与使用DE有一定关系，索性就只安装一些满足需要的包。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应包的功能：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;工具&lt;/th&gt;
&lt;th&gt;作用&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker Engine&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;提供容器运行和管理所需的核心技术，是Docker的“心脏”。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker Desktop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;包含Docker Engine、CLI等组件的一体化桌面应用程序，旨在降低使用门槛。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;与Docker Engine交互的命令行工具，通过指令管理容器、镜像等。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker Build&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用于将应用和依赖打包成标准化、可移植容器镜像的工具和生态系统。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Docker Compose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;用于定义和运行多容器应用的工具，通过一个命令就能启动整个服务集群。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;将当前用户添加到Docker用户组，以便不使用&lt;code&gt;sudo&lt;/code&gt;运行docker命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建docker组（通常安装时已自动创建）
sudo groupadd docker

# 将当前用户加入docker组
sudo usermod -aG docker $USER

# 执行以下命令使组权限生效
newgrp docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
这里使用&lt;code&gt;start&lt;/code&gt;而不是各类教程里面提到的&lt;code&gt;enable --now&lt;/code&gt;是因为我是在笔记本上进行部署的，我不希望在没有运行镜像需求的时候后台运行docker服务，以节省系统开销
:::&lt;/p&gt;
&lt;p&gt;安装验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo docker run hello-world
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;制作项目镜像&lt;/h1&gt;
&lt;h2&gt;ROS项目复现&lt;/h2&gt;
&lt;h3&gt;拉取镜像&lt;/h3&gt;
&lt;p&gt;要使用的镜像都可以在&lt;a href=&quot;https://hub.docker.com/_/ros&quot;&gt;Docker Hub&lt;/a&gt;上找到（没有魔法可以尝试加速站，如&lt;a href=&quot;https://xuanyuan.cloud/free&quot;&gt;轩辕加速站免费版&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;ROS项目复现只需要Docker提供一个集成了对应ROS版本的Ubuntu系统环境即可 ，这里可以直接用ros官方镜像（以&lt;code&gt;ros noetic&lt;/code&gt;为例）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 一般镜像命名ros:&amp;lt;ros版本&amp;gt;-ros-base
docker pull ros:noetic-ros-base
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置GUI映射&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;这部分主要用于解决ROS的一些GUI工具，如Rviz、Gazebo之类的图形化界面的显示问题。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;:::tip
ROS1中的工具链不支持Wayland的通信协议，因此在Wayland下只能借助XWayland进行X11转发。
:::&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确保XWayland正常运行：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;ls /tmp/.X11-unix
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;安装&lt;code&gt;xhost&lt;/code&gt;工具配置XWayland访问权限：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 安装 xhost
sudo dnf install xhost
# 允许所有本地连接访问 X 服务器
xhost +local:

# 在完成工作后取消访问权限
xhost -local:
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;Docker显示配置：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# 将宿主机的显示器地址（如 `:0`）传给容器。
-e DISPLAY=$DISPLAY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入容器后，初始化 ROS 环境并启动 Rviz。为强制使用 X11 后端，需设置环境变量 &lt;code&gt;QT_QPA_PLATFORM=xcb&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 在容器内执行
source /opt/ros/noetic/setup.bash
export QT_QPA_PLATFORM=xcb
rviz
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;在容器中启动镜像&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;docker run -it --rm \
    --name my_ros_container \
    --network host \
    -e DISPLAY=$DISPLAY \
    -v /tmp/.X11-unix:/tmp/.X11-unix \
    osrf/ros:noetic-desktop-full \
    /bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 Docker 中运行 ROS 项目的 &lt;code&gt;docker run&lt;/code&gt; 命令的关键参数：&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;-it&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-i&lt;/code&gt; (interactive)&lt;/strong&gt;：&lt;strong&gt;保持 STDIN 打开&lt;/strong&gt;，即使没有附加终端。这让容器能够持续接收用户输入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;-t&lt;/code&gt; (tty)&lt;/strong&gt;：为容器分配一个&lt;strong&gt;伪终端&lt;/strong&gt;，能看到的终端具备完整的交互能力（如显示颜色、支持 Ctrl+C 等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;code&gt;--rm&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;容器退出后自动删除&lt;/strong&gt;容器及其相关的匿名卷。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不设置时，容器停止后会保留在本地，占据存储空间，需手动执行 &lt;code&gt;docker rm&lt;/code&gt; 清理。&lt;/li&gt;
&lt;li&gt;适合&lt;strong&gt;临时、一次性任务&lt;/strong&gt;，让宿主机环境保持干净。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：删除后，容器内的所有改动（除非写入挂载的数据卷）都会丢失。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;::warn
在复现过程中不建议加上这个选项，因为这会删除你辛辛苦苦配置的环境
::&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;--name my_ros_container&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;为容器指定一个&lt;strong&gt;自定义名称&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不指定时，Docker 会自动生成一个随机名字（如 &lt;code&gt;boring_lamport&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;有了名字后，可以直接用 &lt;code&gt;docker exec -it my_ros_container bash&lt;/code&gt; 进入正在运行的容器，或用于停止、删除等操作，比用容器 ID 更方便。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;code&gt;--network host&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;使用&lt;strong&gt;宿主机网络模式&lt;/strong&gt;，容器与宿主机共享同一个网络栈。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;容器内 &lt;code&gt;localhost&lt;/code&gt; 直接指向宿主机的回环接口。&lt;/li&gt;
&lt;li&gt;在 ROS 场景下，这非常重要：Rviz 需要连接宿主机上运行的 &lt;code&gt;roscore&lt;/code&gt;，或者多个 ROS 节点之间通过标准端口通信时，&lt;code&gt;host&lt;/code&gt; 网络模式能避免复杂的端口映射和网络转发配置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;代价&lt;/strong&gt;：网络隔离性降低，容器可以直接访问宿主机的所有网络接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;:::tip
还有一个隐藏的好处，就是容器可以直接走在宿主机设置的网络代理
:::&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;-e DISPLAY=$DISPLAY&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;设置容器内的&lt;strong&gt;环境变量&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DISPLAY&lt;/code&gt; 告诉 X11 客户端应该连接到哪个显示服务器。&lt;code&gt;$DISPLAY&lt;/code&gt; 是宿主机上当前会话的显示器地址，通常为 &lt;code&gt;:0&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;传入该变量后，容器内的 Rviz 等 GUI 程序才知道将窗口渲染到宿主机屏幕上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;code&gt;-v /tmp/.X11-unix:/tmp/.X11-unix&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;挂载一个&lt;strong&gt;数据卷（Volume）&lt;/strong&gt;，格式为 &lt;code&gt;宿主机路径:容器内路径&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/tmp/.X11-unix&lt;/code&gt; 目录下存放着 X11 服务器的 Unix 域套接字文件，是 X11 通信的通道。&lt;/li&gt;
&lt;li&gt;挂载后，容器内的应用程序可以通过这个通道与宿主机的 X 服务器（或 XWayland）通信，从而显示图形窗口。&lt;/li&gt;
&lt;li&gt;使用这个命令还可以直接挂载整个项目：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-v /workspace:/ros_ws
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;ros:noetic-ros-base&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;指定使用的 &lt;strong&gt;Docker 镜像&lt;/strong&gt;。&lt;/p&gt;
&lt;h4&gt;最后的 &lt;code&gt;/bin/bash&lt;/code&gt; 或 &lt;code&gt;command&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;覆盖镜像默认的启动命令。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果提供 &lt;code&gt;/bin/bash&lt;/code&gt;，则容器启动后直接进入一个交互式 shell，可以手动执行后续 ROS 命令。&lt;/li&gt;
&lt;li&gt;如果希望容器启动后自动运行某个程序，可以直接写，例如 &lt;code&gt;/bin/bash -c &quot;source /opt/ros/noetic/setup.bash &amp;amp;&amp;amp; rviz&quot;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;其他配置&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt;：&lt;strong&gt;后台运行&lt;/strong&gt;容器，不占用终端（适合长期运行的服务，但需与其他命令如 &lt;code&gt;exec&lt;/code&gt; 配合交互）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p 8080:80&lt;/code&gt;：&lt;strong&gt;端口映射&lt;/strong&gt;，将容器内 80 端口映射到宿主机 8080 端口（仅在 &lt;code&gt;bridge&lt;/code&gt; 网络模式下有效，&lt;code&gt;host&lt;/code&gt; 模式下无效）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--gpus all&lt;/code&gt;：&lt;strong&gt;启用 GPU 支持&lt;/strong&gt;，需安装 NVIDIA Container Toolkit。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--device /dev/dri&lt;/code&gt;：&lt;strong&gt;挂载宿主机 DRM 设备&lt;/strong&gt;，用于显卡硬件加速。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--user $(id -u):$(id -g)&lt;/code&gt;：以当前用户的 UID 和 GID 运行容器，避免权限问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;ROS 开发&lt;/h2&gt;
&lt;p&gt;除了复现之外，可能选定了某个idea,想要在现有的工作上进行开发，这时候虽然也能直接在基础的镜像上进行开发，但为了让环境可移植（和其他人协同工作，或者单纯想在自己电脑和实验室电脑中的环境保持一致）。这时候可以将上述环境打包成DockerFile, 别人想用的时候直接构建镜像就好了。&lt;/p&gt;
&lt;h3&gt;镜像制作&lt;/h3&gt;
&lt;p&gt;以我现在正在做的&lt;a href=&quot;https://github.com/SJTU-ViSYS-team/Avoid-MPC&quot;&gt;Avoid MPC&lt;/a&gt;为例，可以编写如下的一个DockerFile：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 拉取镜像
FROM osrf/ros:noetic-ros-base

# 使用 bash 以便 source
SHELL [&quot;/bin/bash&quot;, &quot;-c&quot;]

# --- 1. 安装系统工具和 ROS 包依赖 ---
RUN apt-get update &amp;amp;&amp;amp; \
    DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
    # 通用工具
    python3-catkin-tools \
    python3-pip \
    git \
    wget \
    unzip \
    # 线性代数、优化、点云等库
    libmetis-dev \
    gfortran \
    libeigen3-dev \
    libpcl-dev \
    # 项目所需的 ROS 包
    ros-noetic-mavros \
    ros-noetic-mavros-extras \
    ros-noetic-pcl-conversions \
    ros-noetic-tf2-sensor-msgs \
    &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*

# --- 2. 从源码编译 CasADi 并安装到 /opt/casadi ---
WORKDIR /tmp/casadi_build
RUN git clone https://github.com/casadi/casadi.git -b 3.6.4 &amp;amp;&amp;amp; \
    mkdir -p casadi/build &amp;amp;&amp;amp; cd casadi/build &amp;amp;&amp;amp; \
    cmake .. \
        -DWITH_BUILD_IPOPT=ON \
        -DWITH_BUILD_MUMPS=ON \
        -DWITH_IPOPT=ON \
        -DWITH_MUMPS=ON \
        -DWITH_OPENMP=ON \
        -DCMAKE_INSTALL_PREFIX=/opt/casadi &amp;amp;&amp;amp; \
    make -j$(nproc) &amp;amp;&amp;amp; \
    make install &amp;amp;&amp;amp; \
    # 清理临时文件
    rm -rf /tmp/casadi_build

# 安装 Python 版 CasADi（用于生成 MPC 库）
RUN pip3 install casadi

# --- 3. 设置环境变量（每次启动容器时自动生效）---
# 将 CasADi 路径写入系统环境，ROS 工作空间稍后在运行时 source
RUN echo &apos;export LD_LIBRARY_PATH=/opt/casadi/lib:$LD_LIBRARY_PATH&apos; &amp;gt;&amp;gt; /etc/bash.bashrc &amp;amp;&amp;amp; \
    echo &apos;export CMAKE_PREFIX_PATH=/opt/casadi:$CMAKE_PREFIX_PATH&apos; &amp;gt;&amp;gt; /etc/bash.bashrc &amp;amp;&amp;amp; \
    echo &apos;source /opt/ros/noetic/setup.bash&apos; &amp;gt;&amp;gt; /etc/bash.bashrc

# 为方便，创建一个工作目录（不是 ROS 工作空间，只是挂载点）
WORKDIR /ros_ws
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用该DockerFile构建镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build --network host -t avoid_mpc_env:noetic .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
这里使用&lt;code&gt;--network&lt;/code&gt;标签是因为构建过程中用到了&lt;code&gt;git&lt;/code&gt;拉取远程仓库，我需要走我宿主机上的代理，才能顺利访问github
:::&lt;/p&gt;
&lt;h3&gt;镜像使用&lt;/h3&gt;
&lt;p&gt;与上文提到的ROS项目复现一致，这时候镜像中已经包含了该项目需要的所有环境，想要运行直接在容器中启动这个容器即可。当然你也可以在项目中添加这么一个脚本，更优雅地启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 脚本名称：run_docker.sh

# xhost

xhost +local: &amp;gt; /dev/null 2&amp;gt;&amp;amp;1

# run docker (这时候可以加上--rm标签了)

docker run -it --rm --name avoid_mpc_dev \
	--network host \
	-e DISPLAY=$DISPLAY \
	-v /tmp/.X11-unix:/tmp/.X11-unix \
	-v &quot;$HOME/workspace/Avoid-MPC:/ros_ws&quot; \
	avoid_mpc_dev:noetic \
	/bin/bash
	
# xhost off
# xhost -local: &amp;gt; /dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【Python】Python中的修饰器</title><link>https://blog.onemom.top/posts/python_decorater/</link><guid isPermaLink="true">https://blog.onemom.top/posts/python_decorater/</guid><description>Python中修饰器的妙用</description><pubDate>Thu, 25 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Python 装饰器（Decorator）详解&lt;/h1&gt;
&lt;h2&gt;什么是装饰器？&lt;/h2&gt;
&lt;p&gt;装饰器本质上是一个&lt;strong&gt;函数&lt;/strong&gt;，它接收一个函数作为参数，并返回一个新的函数。它可以在&lt;strong&gt;不修改原函数代码&lt;/strong&gt;的情况下，增强或改变函数的行为。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 装饰器本质
@decorator
def func():
    pass

# 等价于
decorator(func)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最简单的装饰器示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def my_decorator(func):
    def wrapper():
        print(&quot;函数执行前...&quot;)
        func()
        print(&quot;函数执行后...&quot;)
    return wrapper

@my_decorator
def say_hello():
    print(&quot;Hello!&quot;)

say_hello()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;输出：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;函数执行前...
Hello!
函数执行后...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;装饰器的执行流程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│  @my_decorator                                          │
│  def say_hello():                                       │
│      print(&quot;Hello!&quot;)                                    │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  1. Python 解释器看到 @my_decorator                      │
│  2. 调用 my_decorator(say_hello)                        │
│  3. 返回 wrapper 函数                                    │
│  4. say_hello = wrapper                                 │
└─────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────┐
│  调用 say_hello() 实际上是调用 wrapper()                  │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用场景&lt;/h2&gt;
&lt;h3&gt;1. 计时器 - 测量函数执行时间&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f&quot;{func.__name__} 执行耗时: {end - start:.4f} 秒&quot;)
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(1)
    print(&quot;完成!&quot;)

slow_function()
# 输出:
# 完成!
# slow_function 执行耗时: 1.0012 秒
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 日志记录&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def log(func):
    def wrapper(*args, **kwargs):
        print(f&quot;调用函数: {func.__name__}, 参数: {args}, {kwargs}&quot;)
        result = func(*args, **kwargs)
        print(f&quot;函数返回: {result}&quot;)
        return result
    return wrapper

@log
def add(a, b):
    return a + b

add(3, 5)
# 输出:
# 调用函数: add, 参数: (3, 5), {}
# 函数返回: 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. 权限验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def require_login(func):
    def wrapper(user, *args, **kwargs):
        if not user.get(&apos;is_logged_in&apos;):
            print(&quot;请先登录!&quot;)
            return None
        return func(user, *args, **kwargs)
    return wrapper

@require_login
def view_profile(user):
    print(f&quot;欢迎, {user[&apos;name&apos;]}!&quot;)

view_profile({&apos;name&apos;: &apos;Alice&apos;, &apos;is_logged_in&apos;: False})  # 请先登录!
view_profile({&apos;name&apos;: &apos;Alice&apos;, &apos;is_logged_in&apos;: True})   # 欢迎, Alice!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 缓存（记忆化）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;def cache(func):
    cached_results = {}
    def wrapper(*args):
        if args in cached_results:
            print(f&quot;从缓存获取: {args}&quot;)
            return cached_results[args]
        result = func(*args)
        cached_results[args] = result
        return result
    return wrapper

@cache
def fibonacci(n):
    if n &amp;lt; 2:
        return n
    return fibonacci(n-1) + fibonacci(n-2)

print(fibonacci(10))  # 第一次计算
print(fibonacci(10))  # 从缓存获取
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 重试机制&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import random

def retry(max_attempts=3):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    print(f&quot;第 {attempt + 1} 次尝试失败: {e}&quot;)
            print(&quot;所有尝试均失败!&quot;)
        return wrapper
    return decorator

@retry(max_attempts=3)
def unstable_api():
    if random.random() &amp;lt; 0.7:
        raise ConnectionError(&quot;网络错误&quot;)
    return &quot;成功!&quot;

unstable_api()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;带参数的装饰器&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def repeat(times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(times=3)
def greet(name):
    print(f&quot;Hello, {name}!&quot;)

greet(&quot;World&quot;)
# 输出:
# Hello, World!
# Hello, World!
# Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;结构对比：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;普通装饰器（2层）          带参数的装饰器（3层）
─────────────────          ─────────────────────
def decorator(func):       def decorator_factory(arg):
    def wrapper():             def decorator(func):
        ...                        def wrapper():
    return wrapper                     ...
                                   return wrapper
                               return decorator
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类装饰器&lt;/h2&gt;
&lt;p&gt;装饰器也可以是一个类：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
    
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f&quot;{self.func.__name__} 被调用了 {self.count} 次&quot;)
        return self.func(*args, **kwargs)

@CountCalls
def say_hi():
    print(&quot;Hi!&quot;)

say_hi()  # say_hi 被调用了 1 次 \n Hi!
say_hi()  # say_hi 被调用了 2 次 \n Hi!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多个装饰器叠加&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;@decorator1
@decorator2
@decorator3
def func():
    pass

# 等价于
func = decorator1(decorator2(decorator3(func)))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;执行顺序：&lt;/strong&gt; 从下往上装饰，从上往下执行&lt;/p&gt;
&lt;h2&gt;保留原函数信息&lt;/h2&gt;
&lt;p&gt;使用 &lt;code&gt;functools.wraps&lt;/code&gt; 保留原函数的元信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from functools import wraps

def my_decorator(func):
    @wraps(func)  # 保留原函数的 __name__, __doc__ 等
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def example():
    &quot;&quot;&quot;这是文档字符串&quot;&quot;&quot;
    pass

print(example.__name__)  # 输出: example（而不是 wrapper）
print(example.__doc__)   # 输出: 这是文档字符串
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;使用场景&lt;/th&gt;
&lt;th&gt;示例&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;日志/调试&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;记录函数调用信息&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;性能监控&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;计时、计数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;权限控制&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;登录验证、角色检查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;缓存&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;避免重复计算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;重试机制&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;网络请求失败重试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;输入验证&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;参数类型检查&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;注册机制&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flask 路由、d2l 的 &lt;code&gt;add_to_class&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;装饰器是 Python 中实现 &lt;strong&gt;AOP（面向切面编程）&lt;/strong&gt; 的优雅方式，让你能够将横切关注点（如日志、缓存、权限）与业务逻辑分离！&lt;/p&gt;
</content:encoded></item><item><title>【踩坑】在Wayland桌面使用Fcitx5在QQ,微信中输入中文</title><link>https://blog.onemom.top/posts/fcitx5_on_hyprland/</link><guid isPermaLink="true">https://blog.onemom.top/posts/fcitx5_on_hyprland/</guid><pubDate>Mon, 08 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;在Wayland桌面使用Fcitx5在QQ，微信中输入中文&lt;/h1&gt;
&lt;p&gt;我为我的Archlinux系统安装的第一个桌面环境是KDE 6.5。这一版本的KDE已经完全转向Wayland了，当时怀着忐忑的心情安装了QQ，Vscode之类的Electronic应用，本来以为会遇到Fcitx5无法切换中文输入法的问题，结果意外的没有出问题。Fcitx5竟然在QQ上以完美的DPI进行渲染！本来以为Wayland下的Fcitx5的问题都已经解决，于是欣喜地去安装配置了Hyprland，结果在Hyprland下，无法在QQ,微信上输入中文的毛病依然发生了（Fcitx5对KDE/GNOME做了特别的优化，没对Hyprland做😣）。于是便有了这篇指北。&lt;/p&gt;
&lt;h2&gt;Fcitx5 安装&lt;/h2&gt;
&lt;p&gt;在Arch上Fcitx5的安装十分简单，这里直接贴命令了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S fcitx5-im fcitx5-chinese-addons fcitx5-configtool
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以同时添加一些词库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S fcitx5-pinyin-zshwiki	# 维基百科词库
paru -S fcitx5-pinyin-sougou-dict-git # 搜狗词库
paru -S fcitx5-pinyin-moegirl					# 萌娘百科词库
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;紧接着添加一些皮肤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;paru -S fcitx5-skin-ori-git
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Hyprland配置&lt;/h2&gt;
&lt;p&gt;在Hyprland上无法在QQ，微信（沙盒版&lt;a href=&quot;https://aur.archlinux.org/packages/wechat-universal-bwrap&quot;&gt;AUR&lt;/a&gt;）等Electron应用上使用的原因很简单，就是Hyprland 等非 GNOME/KDE 的 Wayland WM 存在 bwrap 与 XWayland 配合问题，只需要在Hyprland配置文件中添加系统环境变量即可（不影响KDE）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.config/hypr/hyprland.conf
#############################
### ENVIRONMENT VARIABLES ###
#############################
# See https://wiki.hypr.land/Configuring/Environment-variables/

env = XCURSOR_SIZE,24
env = HYPRCURSOR_SIZE,24

env = XMODIFIERS,@im=fcitx
env = SDL_IM_MODULE,fcitx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来大概率就可以在Hyprland中的QQ，微信中进行中文输入了。&lt;/p&gt;
&lt;p&gt;但还有一个问题，Fcitx可能会出现候选词模糊的情况。出现这种情况是因为WeChat 和 LinuxQQ 都运行在 &lt;strong&gt;XWayland&lt;/strong&gt; 上（而非原生 Wayland），fcitx5 候选框需要根据应用的 DPI 缩放来渲染，如果 XWayland 的 DPI 设置与 Wayland 缩放不一致，就会导致候选框被二次缩放而模糊。除此之外，如果你使用了 &lt;strong&gt;非整数倍缩放&lt;/strong&gt;（如 1.25x, 1.5x），fcitx5 候选框可能被拉伸渲染。解决的方法也很简单，只需要单独设置XWayland的缩放就可以了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 根据你的缩放设置计算 DPI
# 1.0x = 96 DPI
# 1.25x = 120 DPI  
# 1.5x = 144 DPI
# 2.0x = 192 DPI
xrdb -merge &amp;lt;&amp;lt;&amp;lt; &quot;Xft.dpi: 120&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;~/.config/hypr/hyprland.conf&lt;/code&gt;中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exec-once = xrdb -merge &amp;lt;&amp;lt;&amp;lt; &quot;Xft.dpi: 120&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可每次启动时都重新启动。&lt;/p&gt;
</content:encoded></item><item><title>【算法】KD-Tree原理及其优化</title><link>https://blog.onemom.top/posts/kd-tree/</link><guid isPermaLink="true">https://blog.onemom.top/posts/kd-tree/</guid><pubDate>Mon, 01 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;KD-Tree&lt;/h1&gt;
&lt;p&gt;作为当前空间信息处理绕不开的一个数据结构，KD-Tree因其在处理高维度空间信息的高效性，被广泛使用到各种机器人感知，三维重建，光线追踪算法等领域。当然也衍生了一系列针对该算法的优化方法。这篇文章将从KD-Tree的原理出发，详细谈一谈该算法的原理以及目前的优化策略。&lt;/p&gt;
&lt;h2&gt;KD-Tree 算法&lt;/h2&gt;
&lt;p&gt;KD-Tree（k-Dimension Tree, KDT），是一种具有二叉搜索树形态的特殊的树结构，其中每个结点都对应k维空间中的一个点。每个子树中的点都在一个k维超平面内部，这个超平面也被称为边界框（Bounding Box），例如在二维空间中这个超平面就是一个平面，如下图所示，就是边界的框。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.onemom.top/i/2025/12/07/6935302065196.jpg&quot; alt=&quot;kdt1&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;建树&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;综合比较下来我觉得最普遍的方式可以用&lt;a href=&quot;https://oi-wiki.org/ds/kdt/#%E5%BB%BA%E6%A0%91&quot;&gt;OI-Wiki&lt;/a&gt;中的概括&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;假设已有 $k$ 维空间中的 $n$ 个不同点的坐标，要构建一个KD-Tree，步骤如下（递归）：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果当前超平面中只有一个点，返回。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择一个维度&lt;/strong&gt;，将当前超平面按照这个维度分成两个超平面。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;选择切割点&lt;/strong&gt;：在选择的维度上选择一个点，这一维度上的值小于这个点的归入一个超平面（左子树），其余的归入另一个超平面（右子树）。&lt;/li&gt;
&lt;li&gt;将选择点作为子树的根节点，递归对分出两个超长方体，构建左右子树，维护子树信息。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以上图为例，构建出的树结构如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.onemom.top/i/2025/12/07/693532bf9674b.jpg&quot; alt=&quot;kdt2&quot; /&gt;&lt;/p&gt;
&lt;p&gt;建树过程中最主要的优化方向就是&lt;strong&gt;维度&lt;/strong&gt;与&lt;strong&gt;切割点&lt;/strong&gt;的选择。这两部分的选择直接决定了树的形态。而对于一个树结构来说，越接近平衡二叉树，在空间复杂度与查询时的时间复杂度上来说都更有利。因此我们对上述步骤中的这两步做优化，在对于点云这种比较稠密的数据上，可以这么优化：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;选择跨越维度最大的维度，对于点云这种数据来说，这能保证两个子树超平面中包含的点是均匀的。&lt;/p&gt;
&lt;p&gt;:::Note&lt;/p&gt;
&lt;p&gt;OI-Wiki中的原文是“轮流选择 $k$ 个维度，以保证在任意连续 $k$ 层里每个维度都被切割到。”，我认为也是一种面对数据密度不确定的情况下的一种有效方式。&lt;/p&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分割点集时，按照所选的维度上的中位数。这样基本上能保证KD-Tree的树高最多为 $\log n + O(1) $&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;比如，以三维空间为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;点云数据：
P0 = (1.2, 2.3, 1.5)
P1 = (5.1, 1.2, 4.8)
P2 = (2.5, 5.2, 2.1)
P3 = (4.8, 3.5, 3.2)
P4 = (0.5, 4.1, 0.8)
P5 = (3.2, 2.5, 2.9)
P6 = (5.5, 5.5, 5.5)
P7 = (1.8, 1.5, 1.2)
P8 = (4.2, 4.2, 4.2)
P9 = (2.8, 3.8, 3.1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一步：选择分割维度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 选择跨度最大的维度
max_span = max(5.0, 4.3, 4.7) = 5.0  ← X维度
divfeat = 0  (X坐标)
divval = (0.5 + 5.5) / 2 = 3.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二步：分割点集：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;原始点集 (按X坐标)：
P0(1.2) P7(1.8) P2(2.5) P9(2.8) P5(3.2) P3(4.8) P1(5.1) P8(4.2) P4(0.5) P6(5.5)
                                    ↑
                              分割值 = 3.0

分割后：
┌─────────────────┬───────────────┬──────────────────┐
│  &amp;lt; 3.0          │   == 3.0      │   &amp;gt; 3.0          │
├─────────────────┼───────────────┼──────────────────┤
│P0 P7 P2 P9 P4   │  P5           │P3 P1 P8 P6       │
│(4个点)          │ (1个点)       │(4个点)           │
└─────────────────┴───────────────┴──────────────────┘
       ↓                              ↓
   左子树                          右子树
   继续分割                        继续分割
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终，树的结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                    ┌─────────────────────────────────────┐
                    │ ROOT NODE                           │
                    │ divfeat = X (0)                     │
                    │ divval = 3.0                        │
                    │ children: P0-P9                     │
                    └──────────────┬──────────────────────┘
                                   │
                    ┌──────────────┴──────────────────┐
                    ▼                                 ▼
            ┌─────────────────┐            ┌──────────────────┐
            │ LEFT NODE       │            │ RIGHT NODE       │
            │ divfeat = Y(1)  │            │ divfeat = Z(2)   │
            │ divval = 2.9    │            │ divval = 4.85    │
            └────────┬────────┘            └────────┬─────────┘
                     │                              │
         ┌───────────┴───────────┐    ┌──────────────┴─────────────┐
         ▼                       ▼    ▼                            ▼
    ┌────────┐            ┌────────┐  ┌──────┐           ┌──────────────┐
    │ LEAF   │            │ LEAF   │  │LEAF  │           │ LEAF         │
    │P0 P4   │            │P7 P2   │  │P5 P9 │           │P3 P1 P8 P6   │
    │(2 pts) │            │(2 pts) │  │(2pts)│           │(4 points)    │
    └────────┘            └────────┘  └──────┘           └──────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;查询&lt;/h3&gt;
&lt;p&gt;在点云slam中，最常用的处理方式将来自不同帧的点云进行比较，通过点对点匹配（ICP），点到线（PLICP），面到面（GICP）等loss模型，实现位姿估计；在实时规划方面，也会通过将点云进行分类（动态，静态障碍物），实现实时避障。在这些操作中，存在频繁对数据集中的点进行查询的操作，我们构建KD-Tree来管理点云信息的关键也在这里。&lt;/p&gt;
&lt;p&gt;我们知道最基础的对树结构的遍历方式分为深度遍历（Deep First Search）与广度遍历（Breadth First Search）两种，但这两种搜索方式在面对庞大的点数据的KD-Tree上，无法保证高效性。而我们储存的点本身是处于三维有界线性空间的，所以自然存在“范数”这一关键概念，因此我们可以通过点点之间的范数来对树进行&lt;strong&gt;剪枝&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对KD-Tree进行查询的主要方式是通过最近邻搜索（k-nearest neighbors algorithm，KNN）的方式实现。&lt;/p&gt;
&lt;p&gt;对于要查询的点，我们首先会确认查询点在分割平面的哪一侧，进而有限搜索离查询点更“近”的子树，并以此递归查询：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 确定查询点在分割平面的哪一侧 */
Dimension idx = node-&amp;gt;node_type.sub.divfeat;     // 分割维度
ElementType val = vec[idx];                      // 查询点在该维度的坐标
DistanceType diff = val - node-&amp;gt;node_type.sub.divval;

/* 先搜索closer的子树 */
if (diff &amp;lt; 0) {
    bestChild = node-&amp;gt;child1;    // 左子树离查询点更近
    otherChild = node-&amp;gt;child2;
} else {
    bestChild = node-&amp;gt;child2;    // 右子树离查询点更近
    otherChild = node-&amp;gt;child1;
}

/* 先递归搜索最可能包含最近邻的子树 */
searchLevel(bestChild, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种思路被称为&lt;strong&gt;最佳有限搜索（Best-First Search）&lt;/strong&gt;，配合之后的剪枝，能大幅度减少搜索的节点数。&lt;/p&gt;
&lt;p&gt;当我们查询到某一个子树之前，我们还需要确认这个子树是否具备被查询的“价值”，我们可以通过计算查询点到分割平面的距离，当另一个子树的距离已经超过我们要搜索的最大距离，就没有必要再搜索子树，这大幅度减少了我们递归的层数，减少了空间与时间复杂度（从递归的层面来说，就没有必要将新的函数调用压入系统栈）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 计算查询点到分割平面的距离 */
cut_dist = distance_.accum_dist(val, divval, idx);
// = (val - divval)^2

/* 当前已找到的K个点中，最远的距离 */
DistanceType worst_dist = result_set.worstDist();

/* 剪枝条件：如果另一个子树的距离已经超过K个候选点的最大距离
   就不必搜索它了 */
if (mindist + cut_dist &amp;gt; worst_dist) {
    // 另一个子树的所有点都不会比当前K个候选更近
    // 直接跳过该子树，节省大量计算！
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上述的树为例，我们的查询过程可以表示为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;查询点 P = (3.5, 2.1, 4.8)，K=5
    ↓
计算初始距离 (P到根节点边界框的距离)
    ↓
递归搜索
    ├─ 比较 P[divfeat] 与分割值
    ├─ 选择最可能包含最近邻的子树 (bestChild)
    ├─ 递归搜索 bestChild
    ├─ 检查是否需要搜索另一个子树 (otherChild)
    │   └─ 只有当 |P[divfeat] - divval| &amp;lt; 当前K个候选中最远点的距离
    └─ 回溯

最终返回全局K个最近的点
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;复杂度分析&lt;/h3&gt;
&lt;p&gt;对于树的构建来说，通过边界框计算，树的结构更偏向于平衡树，因此书的空间复杂度为 $O(\log n)$。&lt;/p&gt;
&lt;p&gt;对于点集 &lt;code&gt;s[n]&lt;/code&gt; 来说，KD-Tree 每次进行分割的时候都要求找到中位数，将其置于 &lt;code&gt;s[mid]&lt;/code&gt; 处，需要满足 &lt;code&gt;s[l]&lt;/code&gt; 都小于 &lt;code&gt;s[mid]&lt;/code&gt;，&lt;code&gt;s[r]&lt;/code&gt; 都大于 &lt;code&gt;s[mid]&lt;/code&gt; 。这样的操作的复杂度是 $O(n)$ 的。因此构建一个KD-Tree的时间复杂度更趋近于 $O(n\log n)$。&lt;/p&gt;
&lt;p&gt;对于KD-Tree的查询，可以从二维的KD-tree开始考虑。对于二维KD-Tree的一个节点来说，其有四个孙子节点，且它到每一个孙子都在两个维度上各进行了一次划分。按照这种方法将一个矩形划分成四个子矩形，一条与坐标轴平行的线段最多经过两个区域，即从 𝑢 [u] 出发的查询，最多向下进入两个孙子，由于建树的时候，子树在当前划分维度的中位数，所以子树大小必定减半。于是假设该节点的的子树的大小是 $n$，那么就有如下的递推式：&lt;/p&gt;
&lt;p&gt;$$
T(n) = 2 T(n / 4) + O(1)
$$&lt;/p&gt;
&lt;p&gt;由Master定理可得 $T(n) = O(\sqrt{n})$。&lt;/p&gt;
&lt;p&gt;推广到 $k$ 维，可得：$T(n) = 2^{k-1} T(n/2^k) + O (1)$，于是可得：&lt;/p&gt;
&lt;p&gt;$$
T(n) = O(n^{1-1/k})
$$&lt;/p&gt;
</content:encoded></item><item><title>【踩坑】在vscode中使用clang-tidy</title><link>https://blog.onemom.top/posts/vscode_clangd/</link><guid isPermaLink="true">https://blog.onemom.top/posts/vscode_clangd/</guid><pubDate>Tue, 25 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;在vscode中使用clang-tidy&lt;/h2&gt;
&lt;p&gt;在vscode中进行ros开发的时候发现，ros noetic使用&lt;code&gt;catkin build -DCMAKE_EXPORT_COMPILE_COMMANDS=YES&lt;/code&gt;编译ros包时，编译器会将每个包的&lt;code&gt;compile_commands.json&lt;/code&gt;文件放到&lt;code&gt;build&lt;/code&gt;下不同包的目录下：&lt;code&gt;build/&amp;lt;package-name&amp;gt;/compile_commands.json&lt;/code&gt;。让clangd无法正确找到&lt;code&gt;compile_commands.json&lt;/code&gt;导致检查出错。&lt;/p&gt;
&lt;p&gt;解决方案是使用是将各个包的&lt;code&gt;compile_commands.json&lt;/code&gt;合并到目录的&lt;code&gt;compile_commands.json&lt;/code&gt;下。&lt;/p&gt;
&lt;p&gt;在ros包的根目录下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find build -name compile_commands.json -exec cat {} \; | \
  jq -s &apos;add&apos; &amp;gt; compile_commands.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果没有 &lt;code&gt;jq&lt;/code&gt; 可以进行安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install jq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后在项目目录下创建&lt;code&gt;.vscode/settings.json&lt;/code&gt;，进行如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;clangd.arguments&quot;: [
    &quot;--compile-commands-dir=${workspaceFolder}/&amp;lt;ros包根目录相对路径&amp;gt;&quot;,
    &quot;--background-index&quot;,
    &quot;--clang-tidy&quot;,
    &quot;--completion-style=detailed&quot;,
    &quot;--header-insertion=never&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
可以通过配置catkin行为让其每次都自动生成&lt;code&gt;compile_commands.json&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;catkin config --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
</content:encoded></item><item><title>【RL】PPO算法入门</title><link>https://blog.onemom.top/posts/rl/</link><guid isPermaLink="true">https://blog.onemom.top/posts/rl/</guid><description>基于策略的强化学习算法数学原理及推导过程</description><pubDate>Thu, 20 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;【RL】PPO算法入门&lt;/h1&gt;
&lt;p&gt;强化学习（Reinforcement Learning, RL）在机器人领域的应用本质上都是为了解决一个核心问题：让机器人通过与环境的交互，自主学习到一个最优的策略（Policy），从而完成特定任务。这些方法都满足一个基本的框架：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;感知（Perception）&lt;/strong&gt;: 机器人通过传感器感知当前环境的状态 $s_t$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;决策（Decision）&lt;/strong&gt;: 机器人的策略 $\pi$ 根据当前状态 $s_t$ 决定要执行的动作 $a_t$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行动（Action）&lt;/strong&gt;: 机器人执行动作 $a_t$，与环境发生交互。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;学习（Learning）&lt;/strong&gt;: 环境根据机器人的动作反馈一个新的状态 $s_{t+1}$ 和一个奖励信号$ r_t$。算法利用这个反馈信息 $(s_t, a_t, r_t, s_{t+1})$ 来更新和优化策略 $\pi$ 。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从数学原理上讲，所有这些强化学习方案的共同基础是&lt;strong&gt;马尔可夫决策过程（Markov Decision Process, MDP）&lt;/strong&gt;&lt;img src=&quot;https://img.onemom.top/i/2025/11/16/6919c40350c98.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在上图中，机器人通过感知环境，获取状态，通过Actor中的策略 $\pi$ 输出对应的行动与环境进行交互，进而状态改变。不断重复上述过程，直到任务结束，这样就得到了一个由不同时刻Actor的输入与输出组成的序列 $\tau$。&lt;/p&gt;
&lt;h2&gt;PG（Policy Gradient）&lt;/h2&gt;
&lt;p&gt;策略梯度算法（Policy Gradient, PG）则是将其看作一个基于参数 $\theta$ 随机策略 $\pi_\theta$ ，通过寻找一个最优策略并最大化这个策略在环境中的期望回报：&lt;/p&gt;
&lt;p&gt;$$
\pi_\theta^*=\arg \max_\theta J(\theta) = \arg \max_\theta\mathbb{E}&lt;em&gt;{s_0}\left[V^{\pi&lt;/em&gt;\theta}(s_0)\right] = \arg \max_\theta \mathbb{E}&lt;em&gt;{\pi&lt;/em&gt;\theta}\left[\sum^{\infty}_{t=0}r(s_t, a_t)\right]\tag{1}
$$&lt;/p&gt;
&lt;p&gt;对于我们要训练的一个任务而言，初始的时候，由于Actor会在不同的state做出怎样的action是不确定的，因此发生序列 $\tau$ 的概率分布为：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}&lt;/p&gt;
&lt;p&gt;p_{\theta}(\tau)
&amp;amp;= p(s_1)p_{\theta}(a_1|s_1)p(s_2|s_1,a_1)p_{\theta}(a_2|s_2)p(s_3|s_2,a_2)\cdots \&lt;/p&gt;
&lt;p&gt;&amp;amp;= p(s_1)\prod_{t=1}^{T} p_{\theta}(a_t|s_t)p(s_{t+1}|s_t,a_t)&lt;/p&gt;
&lt;p&gt;\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;由上，我们就可以得到在序列 $\tau$ 下，以 $\theta$ 为参数的策略获得的累计奖励（这里的累计奖励就是 (1) 中的价值函数）为：&lt;/p&gt;
&lt;p&gt;$$
J(\theta) = \mathbb{E}&lt;em&gt;{\tau\sim p&lt;/em&gt;\theta(\tau)}\left[ R(\tau) \right] = \sum_\tau R(\tau)p_\theta(\tau)
$$&lt;/p&gt;
&lt;p&gt;其中，$\mathbb{E}&lt;em&gt;{\tau \sim p&lt;/em&gt;\theta(\tau)}$ 的含义为在在策略 $\pi_\theta$ 下，轨迹 $\tau$ 按照分布 $p_\theta(\tau)$ 随机产生的期望。要求 $J(\theta)$ 的极大值，可以通过梯度上升法进行求解，也就需要求解 $\nabla_\theta J(\theta)$。&lt;/p&gt;
&lt;p&gt;$$
\nabla_\theta J(\theta) = \sum_{\tau} R(\tau) \nabla p_\theta(\tau)
$$&lt;/p&gt;
&lt;p&gt;对上式而言，并不能直接求和，因为 $p_\theta(\tau)$ 过于庞大，因此需要对上式进行一些化简：&lt;/p&gt;
&lt;p&gt;$$
\nabla_\theta J(\theta) = \sum_{\tau} R(\tau) p_\theta(\tau) \frac{\nabla p_\theta(\tau)}{p_\theta(\tau)}
$$&lt;/p&gt;
&lt;p&gt;由&lt;a href=&quot;#%E5%AF%B9%E6%95%B0%E5%AF%BC%E6%95%B0%E6%8A%80%E5%B7%A7&quot;&gt;链式法则&lt;/a&gt;我们有：&lt;/p&gt;
&lt;p&gt;$$
\sum_{\tau} R(\tau) p_\theta(\tau) \frac{\nabla p_\theta(\tau)}{p_\theta(\tau)}
= \sum_{\tau} R(\tau) p_\theta(\tau) \nabla \log p_\theta(\tau)
$$&lt;/p&gt;
&lt;p&gt;显然有 $\sum_\tau p_\theta(\tau) = 1$，因此有：&lt;/p&gt;
&lt;p&gt;$$
\sum_{\tau} R(\tau) p_\theta(\tau) \nabla \log p_\theta(\tau) = \mathbb{E}&lt;em&gt;{\tau \sim p&lt;/em&gt;\theta(\tau)} [R(\tau) \nabla \log p_\theta(\tau)]
$$&lt;/p&gt;
&lt;p&gt;进而可以对上式进行数值求解（Monte Carlo method）：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
\mathbb{E}&lt;em&gt;{\tau \sim p&lt;/em&gt;\theta(\tau)} [R(\tau) \nabla \log p_\theta(\tau)] &amp;amp;\approx \frac{1}{N} \sum_{n=1}^{N} R(\tau^n) \nabla \log p_\theta(\tau^n) \
&amp;amp;=\frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_n} R(\tau^n) \nabla \log p_\theta(a_t^n | s_t^n)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;就可以通过实际采集的样本求解目标函数 $J(\theta)$ 的梯度。&lt;img src=&quot;https://img.onemom.top/i/2025/11/17/691ae4a62e36d.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从上述流程图中不难看出，采集到的数据都是当前策略与环境之间交互的结果。因此策略梯度算法是一种在线策略（on-policy）算法，需要根据当前采样的数据来计算梯度。在线策略需要每迭代一步就重新采集数据，这样的训练无论是从时间还是内存上都是巨大的消耗，因此我们需要考虑如何才能让模型能反复从一组采样数据上学习 $\theta&apos;$ 另 $J(\theta&apos;) &amp;gt; J(\theta)$ 。&lt;/p&gt;
&lt;h2&gt;TRPO (Trust Region Policy Optimization)&lt;/h2&gt;
&lt;p&gt;由于初始状态 $s_0$ 的分布和策略无关，因此上述策略 $\pi_\theta$ 下的优化目标 $J(\theta)$ 可以写成在新策略 $\pi_{\theta&apos;}$ 的期望形式：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
J(\theta) &amp;amp;= \mathbb{E}&lt;em&gt;{s_0}\left[V^{\pi&lt;/em&gt;\theta}(s_0)\right] = \mathbb{E}&lt;em&gt;{\pi&lt;/em&gt;{\theta&apos;}}\left[\sum^\infty_{t = 0}\gamma^tV^{\pi_\theta}(s_t) - \sum^\infty_{t = 1}\gamma^tV^{\pi_\theta}(s_t) \right]\
&amp;amp;=-\mathbb{E}&lt;em&gt;{\pi&lt;/em&gt;{\theta&apos;}}\left[\sum^\infty_{t = 0}\gamma^t(V^{\pi_\theta}(s_{t+1}) -V^{\pi_\theta}(s_t)) \right]
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;对两个参数之间的目标函数做差可得（具体推导过程见&lt;a href=&quot;#TRPO%E5%85%B7%E4%BD%93%E8%AF%81%E6%98%8E&quot;&gt;TRPO单调性证明&lt;/a&gt;）：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
J(\theta&apos;) - J(\theta) &amp;amp;= \mathbb{E}&lt;em&gt;{s_0} \left[ V^{\pi&lt;/em&gt;{\theta&apos;}}(s_0) \right] - \mathbb{E}&lt;em&gt;{s_0} \left[ V^{\pi&lt;/em&gt;\theta}(s_0) \right]\
&amp;amp;= \mathbb{E}&lt;em&gt;{\pi&lt;/em&gt;{\theta&apos;}} \left[ \sum_{t=0}^{\infty} \gamma^t r(s_t, a_t) \right] + \mathbb{E}&lt;em&gt;{\pi&lt;/em&gt;{\theta&apos;}} \left[ \sum_{t=0}^{\infty} \gamma^t \left( \gamma V^{\pi_\theta}(s_{t+1}) - V^{\pi_\theta}(s_t) \right) \right]\
&amp;amp;= \mathbb{E}&lt;em&gt;{\pi&lt;/em&gt;{\theta&apos;}} \left[ \sum_{t=0}^{\infty} \gamma^t \left( r(s_t, a_t) + \gamma V^{\pi_\theta}(s_{t+1}) - V^{\pi_\theta}(s_t) \right) \right]\
&amp;amp;= \mathbb{E}&lt;em&gt;{\pi&lt;/em&gt;{\theta&apos;}} \left[ \sum_{t=0}^{\infty} \gamma^t A^{\pi_\theta}(s_t, a_t) \right] \
&amp;amp;= \frac{1}{1 - \gamma} \mathbb{E}&lt;em&gt;{s \sim \nu^{\pi&lt;/em&gt;{\theta&apos;}}} \mathbb{E}&lt;em&gt;{a \sim \pi&lt;/em&gt;{\theta&apos;}(\cdot|s)} \left[ A^{\pi_\theta}(s, a) \right]
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;其中 $A^{\pi_\theta}$ 为优势函数（Advantage Function）。上述的式子说明，只要找到的策略满足：&lt;/p&gt;
&lt;p&gt;$$
\mathbb{E}&lt;em&gt;{s \sim \nu^{\pi&lt;/em&gt;{\theta&apos;}}} \mathbb{E}&lt;em&gt;{a \sim \pi&lt;/em&gt;{\theta&apos;}(\cdot|s)} \left[ A^{\pi_\theta}(s, a) \right] \geq 0
$$&lt;/p&gt;
&lt;p&gt;就可以满足 $J(\theta&apos;) &amp;gt; J(\theta)$。为了求解该式，TRPO选择了使用原策略 $\pi_\theta$ 的状态分布做了近似处理，并使用&lt;a href=&quot;#%E9%87%8D%E8%A6%81%E6%80%A7%E9%87%87%E6%A0%B7&quot;&gt;重要性采样&lt;/a&gt;对动作分布进行处理，得到目标函数 $L_\theta(\theta&apos;)$ ，并使用KL散步保证这两者之间足够接近。TRPO优化公式如下：&lt;/p&gt;
&lt;p&gt;$$
\begin{aligned}
&amp;amp;\max_{\theta&apos;}\ L_\theta(\theta&apos;) = J(\theta) + \mathbb{E}&lt;em&gt;{s \sim \nu^{\pi&lt;/em&gt;{\theta}}} \mathbb{E}&lt;em&gt;{a \sim \pi&lt;/em&gt;{\theta}(\cdot|s)} \left[ \frac{\pi_{\theta&apos;}(a |s)}{\pi_\theta(a|s)} A^{\pi_\theta}(s, a)\right] \
&amp;amp;\text{s.t.  }\quad\mathbb{E}&lt;em&gt;{s\sim \nu^{\pi&lt;/em&gt;{\theta_k}}}[D_{KL}(\pi_{\theta_k}(\cdot | s), \pi_{\theta&apos;}(\cdot | s))] \leq \delta
\end{aligned}
$$&lt;/p&gt;
&lt;h2&gt;PPO（Proximal Policy Optimization）&lt;/h2&gt;
&lt;p&gt;PPO算法与TRPO算法的区别在于对于参数 $\theta$ 的约束。为了模型能从示范分布 $\theta&apos;$ 中学习参数，就必须让我们学习的参数 $\theta$ 与示范数据中的 $\theta&apos;$ （未知）的分布差距不大。TRPO使用KL散度作为约束项来约束优化目标。而PPO算法直接将约束放在优化目标函数上。目前的PPO算法有两种实现方式：&lt;strong&gt;近端策略优化惩罚&lt;/strong&gt;（PPO-penalty）与&lt;strong&gt;近端策略优化裁剪&lt;/strong&gt;（PPO-clip）&lt;/p&gt;
&lt;h3&gt;PPO-penalty&lt;/h3&gt;
&lt;p&gt;$$
J^{\theta&apos;}_{\text{PPO}}(\theta) = J^{\theta&apos;}(\theta) - \beta \text{KL}(\theta, \theta&apos;)
$$&lt;/p&gt;
&lt;p&gt;通过KL散度动态调整惩罚参数 $\beta $ ，更像是使用惩罚法去求解TRPO算法。&lt;/p&gt;
&lt;h3&gt;PPO-clip&lt;/h3&gt;
&lt;p&gt;clip的做法就更加直接，如果我学习的参数 $\theta$ 与示范中的参数分布 $\theta&apos;$ 的分布差距过大，我就对这一项进行一定程度的裁剪&lt;/p&gt;
&lt;p&gt;$$
J^{\theta^k}&lt;em&gt;{\text{PPO}} \approx \sum&lt;/em&gt;{(s_t, a_t)} \min \left( \frac{p_\theta(a_t | s_t)}{p_{\theta^k}(a_t | s_t)} A^{\theta^k}(s_t, a_t),\  \text{clip}\left( \frac{p_\theta(a_t | s_t)}{p_{\theta^k}(a_t | s_t)}, 1-\varepsilon, 1 + \varepsilon \right)A^{\theta^k}(s_t, a_t)\right)
$$&lt;/p&gt;
&lt;p&gt;其中 $\varepsilon$ 为超参数。&lt;/p&gt;
&lt;p&gt;PPO 代码已由OpenAI开源至Github：&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;openai/baselines&quot;}&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;附录&lt;/h1&gt;
&lt;h2&gt;对数导数技巧&lt;/h2&gt;
&lt;p&gt;对于 $f(x)$ 有：&lt;/p&gt;
&lt;p&gt;$$
\nabla f(x) = f(x) \nabla \log f(x)
$$&lt;/p&gt;
&lt;p&gt;证明：&lt;/p&gt;
&lt;p&gt;由链式法则求解 $\nabla \log f(x)$ 有：&lt;/p&gt;
&lt;p&gt;$$
\nabla \log f(x) = \frac{1}{f(x)}\nabla f(x)
$$&lt;/p&gt;
&lt;p&gt;移项可得上式。&lt;/p&gt;
&lt;h2&gt;TRPO具体证明&lt;/h2&gt;
&lt;p&gt;TODO&lt;/p&gt;
&lt;h2&gt;重要性采样&lt;/h2&gt;
&lt;p&gt;TODO&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;参考资料&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://hrl.boyuai.com/chapter/2/%E7%AD%96%E7%95%A5%E6%A2%AF%E5%BA%A6%E7%AE%97%E6%B3%95&quot;&gt;动手学习强化学习&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/weixin_41106546/article/details/137359690&quot;&gt;PPO_ProximalPolicyOptimization_算法原理及实现,详解近端策略优化&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://arxiv.org/abs/1707.06347&quot;&gt;Proximal Policy Optimization Algorithms&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>【算法】基础算法模板</title><link>https://blog.onemom.top/posts/base_algorithm/</link><guid isPermaLink="true">https://blog.onemom.top/posts/base_algorithm/</guid><description>基础算法：数据结构等的C++模板</description><pubDate>Wed, 12 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;基础算法模板&lt;/h1&gt;
&lt;p&gt;:::Tip
Keep on updating...
:::&lt;/p&gt;
&lt;h2&gt;Mater Theorem&lt;/h2&gt;
&lt;p&gt;主定理（Master Theorem）是分析递归法复杂度的重要定理。&lt;/p&gt;
&lt;p&gt;Master 公式如下：&lt;/p&gt;
&lt;p&gt;$$
T(n) = a * T(n/b) + O(n^c),\ \text{where a,b,c is const}
$$&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果 $\log(b,a) &amp;lt; c$, 那么复杂度为：$O(n^c)$&lt;/li&gt;
&lt;li&gt;如果 $\log(b,a) &amp;gt; c$, 那么复杂度为：$O(n^{\log(b,a)})$&lt;/li&gt;
&lt;li&gt;如果 $\log(b,a) = c$, 那么复杂度为：$O(n^c * \log n)$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;addtion: $T(n) = 2 * T(n/2) + O(n *\log n)$ 的时间复杂度为 $O(n * ((\log n)^2))$&lt;/p&gt;
&lt;h2&gt;二分查找&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// find max idx of num &amp;lt;= tar
int binary_search() {
  int ans = -1;
  while (l &amp;lt;= r) {
    int m = l + ((r - 1) &amp;gt;&amp;gt; 2);   // avoid memory overflow
    if (Array[m] &amp;gt;= tar) {
      r = m - 1;
      ans = m;
    }
    if (Array[m] &amp;lt; tar) {
      l = m + 1;
    }
  }

  return ans;

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;树的遍历&lt;/h2&gt;
&lt;p&gt;树的前序，中序，后序遍历&lt;/p&gt;
&lt;p&gt;以下图中的树为例&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.onemom.top/i/2025/12/09/6937f0810557a.png&quot; alt=&quot;example tree&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;先序：中，左，右&lt;/p&gt;
&lt;p&gt;期望输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 2 4 5 3 6 7
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;中序：左，中，右&lt;/p&gt;
&lt;p&gt;期望输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4 2 5 1 6 3 7
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;后序：左，右，中&lt;/p&gt;
&lt;p&gt;期望输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;4 5 2 6 7 3 1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;递归版&lt;/h3&gt;
&lt;p&gt;前序遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void printPreorder(treeNode *root) {
  if (root == nullptr)
    return;

  cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
  printPreorder(root-&amp;gt;left);
  printPreorder(root-&amp;gt;right);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中序遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void printInorder(treeNode *root) {
  if (root == nullptr)
    return;

  printInorder(root-&amp;gt;left);
  cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
  printInorder(root-&amp;gt;right);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后序遍历：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void printPostorder(treeNode *root) {
  if (root == nullptr)
    return;

  printPostorder(root-&amp;gt;left);
  printPostorder(root-&amp;gt;right);
  cout &amp;lt;&amp;lt; root-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;栈实现版&lt;/h3&gt;
&lt;p&gt;:::Note
递归法的实现依赖于语言底层实现的系统栈。会将当前没有运行完毕的函数压入系统栈（递归序）。&lt;/p&gt;
&lt;p&gt;使用栈的方式实现树的遍历，本质上就是使用内存栈代替系统栈。可以通过理解系统栈的运行机制来实现通过内存栈来代替系统栈。节点入栈等同于将未处理的节点进行记录，通过之后的出栈&lt;code&gt;pop&lt;/code&gt;操作再重新处理该节点。&lt;/p&gt;
&lt;p&gt;同时还有一个小技巧，上述的“处理节点”，等同于“处理以节点为根节点的子树”。
:::&lt;/p&gt;
&lt;p&gt;对于前序遍历，使用栈实现的步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;打印栈顶元素，栈中弹出&lt;/li&gt;
&lt;li&gt;如果右节点不为空，压入右节点&lt;/li&gt;
&lt;li&gt;如果左节点不为空，压入左节点&lt;/li&gt;
&lt;li&gt;重复1,2,3直到栈为空&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void printPreorder_stack(treeNode *root) {
  if (!root)
    return;

  std::stack&amp;lt;treeNode&amp;gt; st;
  st.push(*root);
  while (!st.empty()) {
    treeNode tmp = st.top();
    cout &amp;lt;&amp;lt; tmp.val &amp;lt;&amp;lt; &quot; &quot;;
    st.pop();
    if (tmp.right)
      st.push(*tmp.right);
    if (tmp.left)
      st.push(*tmp.left);
  }
  return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::Note
在前序遍历中，我们会先优先处理栈顶元素，之后在考虑将栈顶元素的左右节点入栈
:::&lt;/p&gt;
&lt;p&gt;对于中序遍历，使用栈实现步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;子树左边界进栈，直到左边界为空&lt;/li&gt;
&lt;li&gt;栈中弹出节点，打印，节点右树重复 1&lt;/li&gt;
&lt;li&gt;直到没有子树，且栈为空&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void printInorder_stack(treeNode *root) {
  if (!root)
    return;

  std::stack&amp;lt;treeNode&amp;gt; st;
  treeNode *head = root; // 认为是子树的头节点
  while (!st.empty() || head != nullptr) {
    if (head) {
      st.push(*head);
      head = head-&amp;gt;left;
    } else {
      head = &amp;amp;st.top();
      st.pop();
      cout &amp;lt;&amp;lt; head-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
      head = head-&amp;gt;right;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::Note
在中序遍历中，我们会先找到树最深的左节点。通过&lt;code&gt;head&lt;/code&gt;指针，可以标记当前正在处理的子树
:::&lt;/p&gt;
&lt;p&gt;对于后序遍历，有两种实现方式：两个栈版，一个栈版。前者比较好理解，所以将两种版本都放在这里进行参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;两个栈版：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;先序为中，左，右。很自然能联想到先&apos;序：中，右，左（上述代码中的压入顺序调整一下即可），那么后序就是将先&apos;序倒置，只需要使用另一个栈收集先&apos;的输出，最后将这个收集栈打印出来即可&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将栈顶元素收集到收集栈，弹出&lt;/li&gt;
&lt;li&gt;左节点存在，入栈&lt;/li&gt;
&lt;li&gt;右节点存在，入栈&lt;/li&gt;
&lt;li&gt;重复1,2,3直到栈空&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void printPostorder_twoStack(treeNode *root) {
  if (!root)
    return;

  std::stack&amp;lt;treeNode&amp;gt; st;
  std::stack&amp;lt;treeNode&amp;gt; collect;
  st.push(*root);
  while (!st.empty()) {
    treeNode tmp = st.top();
    collect.push(tmp);
    st.pop();
    if (tmp.left) {
      st.push(*tmp.left);
    }
    if (tmp.right) {
      st.push(*tmp.right);
    }
  }

  while (!collect.empty()) {
    cout &amp;lt;&amp;lt; collect.top().val &amp;lt;&amp;lt; &quot; &quot;;
    collect.pop();
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;一个栈版本：&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;初始化指针 &lt;code&gt;head&lt;/code&gt; 指向头节点，&lt;code&gt;cur&lt;/code&gt; 指向当前遍历节点，根节点入栈&lt;/li&gt;
&lt;li&gt;判断左树是否处理过，没有处理入栈&lt;/li&gt;
&lt;li&gt;判断右树是否处理过，没有处理入栈&lt;/li&gt;
&lt;li&gt;左右树都处理过了，则打印当前节点，&lt;code&gt;head&lt;/code&gt;指向栈顶，重复2,3&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void printPostorder_sigStack(treeNode *root) {
  if (!root)
    return;

  std::stack&amp;lt;treeNode *&amp;gt; st;
  st.push(root);
  treeNode *head = root;
  while (!st.empty()) {
    treeNode *cur = st.top();
    if (cur-&amp;gt;left != nullptr &amp;amp;&amp;amp; head != cur-&amp;gt;left &amp;amp;&amp;amp; head != cur-&amp;gt;right) {
      st.push(cur-&amp;gt;left);
    } else if (cur-&amp;gt;right != nullptr &amp;amp;&amp;amp; head != cur-&amp;gt;right) {
      st.push(cur-&amp;gt;right);
    } else {
      cout &amp;lt;&amp;lt; cur-&amp;gt;val &amp;lt;&amp;lt; &quot; &quot;;
      head = st.top();
      st.pop();
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::Note
后序遍历与中序遍历相似的地方是，后序遍历也需要先找到最深的根。不同的是中序遍历是找到最深的子树之后，回到根节点，但后序遍历需要先完全处理&lt;code&gt;cur&lt;/code&gt;为根节点的子树。因此使用&lt;code&gt;head&lt;/code&gt;标记已经处理完的子树，按照先左后右的顺序处理深层的子树。
:::&lt;/p&gt;
&lt;h2&gt;排序&lt;/h2&gt;
&lt;h3&gt;归并排序&lt;/h3&gt;
&lt;p&gt;归并排序是归并思想在排序上的应用。将排序的复杂度降到了 $O(n\log n)$&lt;/p&gt;
&lt;p&gt;排序的主要思想：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将问题从中点拆分&lt;/li&gt;
&lt;li&gt;通过&lt;code&gt;merge&lt;/code&gt;合并两部分的结果&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以5个数的归并排序为例，可以将问题划分成如下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://img.onemom.top/i/2025/12/14/693e8b76dd021.png&quot; alt=&quot;归并排序实例&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到在划分的最后，节点已经不能再划分，只能向上执行&lt;code&gt;merge&lt;/code&gt;操作&lt;/p&gt;
&lt;p&gt;归并排序完整代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

using namespace std;

const int N = 10001;
int n;
int arr[N];
int tmp[N];

void merge(int l, int m, int r) {
  int lp = l;
  int rp = m + 1;
  int begin = 0;
  while (lp &amp;lt;= m &amp;amp;&amp;amp; rp &amp;lt;= r) {
    tmp[begin++] = arr[lp] &amp;gt; arr[rp] ? arr[rp++] : arr[lp++];
  }

  while (rp &amp;lt;= r) {
    tmp[begin++] = arr[rp++];
  }
  while (lp &amp;lt;= m) {
    tmp[begin++] = arr[lp++];
  }

  for (int i = 0; i &amp;lt; begin; i++) {
    arr[l + i] = tmp[i];
  }
}

void merge_sort(int l, int r) {
  if (l &amp;gt;= r)
    return;

  int m = (l + r) &amp;gt;&amp;gt; 1;
  merge_sort(l, m);
  merge_sort(m + 1, r);

  merge(l, m, r);
}

int main() {
  cin &amp;gt;&amp;gt; n;
  for (int i = 0; i &amp;lt; n; i++) {
    int val;
    cin &amp;gt;&amp;gt; val;
    arr[i] = val;
  }

  merge_sort(0, n - 1);
  for (int i = 0; i &amp;lt; n; i++) {
    cout &amp;lt;&amp;lt; arr[i] &amp;lt;&amp;lt; &quot; &quot;;
  }

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试链接：&lt;a href=&quot;https://www.luogu.com.cn/problem/P1177#submit&quot;&gt;洛谷P1177 【模板】排序&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>【Playground】Vim实用技巧</title><link>https://blog.onemom.top/posts/vim/</link><guid isPermaLink="true">https://blog.onemom.top/posts/vim/</guid><description>常用vim操作，以及操作技巧</description><pubDate>Wed, 12 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Vim 实用技巧&lt;/h1&gt;
&lt;h2&gt;Vim基础配置&lt;/h2&gt;
&lt;p&gt;Vim 的全局配置在目录&lt;code&gt;/etc/vim/vimrc&lt;/code&gt;或者&lt;code&gt;/etc/vimrc&lt;/code&gt;中。个人配置文件在&lt;code&gt;~/.vimrc&lt;/code&gt;中&lt;/p&gt;
&lt;p&gt;推荐配置&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set number      # 显示行号
set tabstop=4   # 制表符在屏幕上显示为4个空格
set expandtab   # &amp;lt;Tab&amp;gt;插入空格而不是制表符
set smartindent # 智能缩进
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Vim 中的移动&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hjkl&lt;/code&gt;: 上下左右移动&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gg&lt;/code&gt;: 跳转到第一行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;G&lt;/code&gt;: 跳到最后一行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;Ctrl-u&amp;gt;/&amp;lt;Ctrl-b&amp;gt;&lt;/code&gt;: 向上翻半/一页&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;Ctrl-d&amp;gt;/&amp;lt;Ctrl-f&amp;gt;&lt;/code&gt;: 向下翻半/一页&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{lineno}gg&lt;/code&gt;: 跳转到第lineno行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;zz/zt/zb&lt;/code&gt;: 光标行设置为屏幕居中/第一行/最后一行&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>【Linux-Server】Docker常用命令</title><link>https://blog.onemom.top/posts/docker-command/</link><guid isPermaLink="true">https://blog.onemom.top/posts/docker-command/</guid><description>使用Docker开源项目时用到的命令</description><pubDate>Tue, 11 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;docker 常用命令&lt;/h2&gt;
&lt;h3&gt;Docker 国内加速&lt;/h3&gt;
&lt;p&gt;国内服务器厂商往往会提供Docker的加速服务，在使用docker拉取镜像时可以先修改当前的镜像链接。&lt;/p&gt;
&lt;p&gt;以阿里云为例，ACR会为每一个账号（阿里云账号或RAM用户）生成一个镜像加速器地址，可以进入&lt;a href=&quot;https://cr.console.aliyun.com/cn-hangzhou/instances&quot;&gt;容器镜像服务控制台&lt;/a&gt;进行查看。&lt;/p&gt;
&lt;p&gt;对于Ubuntu系统来说，修改目录文件&lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;，以启用加速：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;registry-mirrors&quot;: [&quot;https://*.mirror.aliyuncs.com&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以直接使用命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json &amp;lt;&amp;lt;-&apos;EOF&apos;
{
  &quot;registry-mirrors&quot;: [&quot;https://*.mirror.aliyuncs.com&quot;]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了厂商提供的镜像，也有一些第三方平台会提供Docker加速服务，例如轩辕镜像，分别提供&lt;a href=&quot;https://docker.xuanyuan.me/&quot;&gt;免费版&lt;/a&gt;和&lt;a href=&quot;https://xuanyuan.cloud/&quot;&gt;付费版&lt;/a&gt;，可以根据提示修改上述&lt;code&gt;daemon.json&lt;/code&gt;配置文件，启用加速&lt;/p&gt;
&lt;h3&gt;docker compose&lt;/h3&gt;
&lt;p&gt;docker compose 是可以用来管理多个docker容器，并使用yaml文件启动容器&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动容器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d  # 在当前目录启动
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;查看当前打开的容器和状态&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker-compose ps
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;关闭容器&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker-compose down
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;查看当前容器日志&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;docker-compose logs &amp;lt;容器名称&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【NovelAI】AI绘画提示词</title><link>https://blog.onemom.top/posts/ai%E7%BB%98%E7%94%BB/</link><guid isPermaLink="true">https://blog.onemom.top/posts/ai%E7%BB%98%E7%94%BB/</guid><description>NovelAI使用技巧记录</description><pubDate>Wed, 29 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;NovelAI使用技巧&lt;/h1&gt;
&lt;p&gt;用AI作画与使用画笔作画其实上没有太大的差别，都需要在大脑里面先构思好画的大体结构，人物的样貌特征，然后再用自然语言描述出来。&lt;/p&gt;
&lt;p&gt;和用画笔不断雕琢一个线条一样，使用AI作画也需要不断细化描述语言。如果使用画笔绘画是对画者对笔的控制，那用AI作画就体现作者是否能用准确的语言去描述自己脑海所想。&lt;/p&gt;
&lt;p&gt;在没有想法的情况下，可以前往各个提示词网站去找找灵感，例如&lt;a href=&quot;https://danbooru.donmai.us&quot;&gt;danbooru&lt;/a&gt;，当然也可以去一些二次元Wiki中去找一些“萌属性”，例如&lt;a href=&quot;https://zh.moegirl.org.cn/Template:%E8%90%8C%E5%B1%9E%E6%80%A7&quot;&gt;萌娘百科&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;提示词&lt;/h2&gt;
&lt;p&gt;一幅二次元图像的关键词 可以从以下几个方面进行考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;艺术家与风格&lt;/li&gt;
&lt;li&gt;角色数量与性别&lt;/li&gt;
&lt;li&gt;角色身份与性质&lt;/li&gt;
&lt;li&gt;身体特征&lt;/li&gt;
&lt;li&gt;服装与配饰&lt;/li&gt;
&lt;li&gt;动作与表情&lt;/li&gt;
&lt;li&gt;环境与背景&lt;/li&gt;
&lt;li&gt;光影与视角&lt;/li&gt;
&lt;li&gt;整体风格&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;选择合适的提示词，不仅要注意提示词本身的含义是否贴合（可以去Danbooru搜索提示词，查看提示词Wiki）。还需要注意提示词隐含的信息。在AI训练的过程中，由于向量维度是有限的，因此一个提示词并非只表达一种特征。例如在Novel AI中，使用&lt;code&gt;white bloomers resembling pumpkin-style shorts&lt;/code&gt;（白色南瓜样式的灯笼裤）往往会生成一个背对相机的画面&lt;a href=&quot;https://www.bilibili.com/video/BV1TeMbzwExw/?share_source=copy_web&amp;amp;vd_source=281319fcd35cd5a2e86520861a66da77&quot;&gt;参考 1&lt;/a&gt;。因为在AI的数据中，往往带有这个关键字的图片都是以背对的姿态投入训练的。这部分关键字的&lt;strong&gt;强度&lt;/strong&gt;也可以通过 Danbooru 进行查询。&lt;/p&gt;
&lt;h2&gt;生成参数&lt;/h2&gt;
&lt;p&gt;选择了合适的提示词，还需要对输入的参数进行微调，已达到符合自己预期的图片。在Novel AI中，可以使用如下的格式进行调整：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[number]::关键字::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用两个冒号将关键字包裹，并在前面填写系数。注意这个系数是非线性的，值越高，关键词提高的幅度越小。&lt;/p&gt;
&lt;p&gt;实例:&lt;/p&gt;
&lt;p&gt;positive prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2::1 girl::,  1.5::portrait::, 1.3::bare arms::, 2::long hair::, 1.8::black hair::,  ahoge, 1.2:::o::, head and shoulder, 0.5::symbol-shaped pupils::
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;negative prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;low quality, blurry, bad anatomy, extra limbs, distorted hands, missing fingers, deformed face, wrong proportions, out of frame, bad lighting, watermark, text, signature, 2::multiple others::, finger, multiple girls
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;AI作画给了很多没有接触过绘画训练的人将自己心中所想的画面呈现出来的可能。或许这种捷径确实让许多为艺术付出心血的人感到失落，但我想说的是相比之下，AI画不出从没见过的线条与色彩，但是没有各种方程约束的人可以。&lt;/p&gt;
</content:encoded></item><item><title>【Linux】Linux常用命令</title><link>https://blog.onemom.top/posts/linux%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</link><guid isPermaLink="true">https://blog.onemom.top/posts/linux%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4/</guid><description>开发，服务器运维中常用的Linux命令</description><pubDate>Fri, 24 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Linux常用命令&lt;/h1&gt;
&lt;h2&gt;日常开发&lt;/h2&gt;
&lt;h3&gt;下载&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;curl&lt;/code&gt; 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# -o  参数后指定要保存的文件名
curl -o filename.tar.gz http://example.com/file.tar.gz

# -O  从 URL 中提取文件名并使用该文件名保存到本地
curl -O http://example.com/file.tar.gz

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;wget&lt;/code&gt; 命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# -P 将文件下载到指定的路径
# -O 下载并自定义文件名保存
# -c 启用断点续传
# -b 启用后台下载
# -i 下载文件中的连接的文件

# 下载单个文件到当前目录
wget http://example.com/filename

# 下载大文件启用后台下载
wget -b http://releases.ubuntu.com/18.04/ubuntu-18.04.5-desktop-amd64.iso
# 查看进度
tail -f wget-log

# 下载多个文件
wget -i url_list.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;包管理&lt;/h3&gt;
&lt;h4&gt;pacman&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;使用Reflector 自动选择最快镜像&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 选择中国地区最快的 10 个镜像
sudo reflector --country China --age 12 --protocol https --sort rate --save /etc/pacman.d/mirrorlist

# 或选择全球最快的镜像
sudo reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;git 基础配置&lt;/h2&gt;
&lt;h3&gt;ssh 连接Github&lt;/h3&gt;
&lt;p&gt;要以个人身份访问github并管理私人仓库，就需要使用ssh连接到github进行鉴权。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;确保存在openssh&lt;/p&gt;
&lt;p&gt;ssh是包&lt;code&gt;openssh&lt;/code&gt;的基础功能，首先需要先安装&lt;code&gt;openssh&lt;/code&gt;工具包&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;检查本地主机key&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls ~/.ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;生成ssh key&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -t rsa -C &quot;xxx@xxx.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取ssh key 公钥&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat ~/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;github上添加公钥&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;验证是否添加成功&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -T git@github.com
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置全局信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global user.email &quot;you@example.com&quot;
git config --global user.name &quot;Your Name&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置git默认编辑器为&lt;code&gt;vim&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git config --global core.editor vim
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;git 使用凭证管理进行登陆&lt;/h3&gt;
&lt;p&gt;在 HTTPS 协议下实现免密推送，关键在于让 git “记住”你的登录凭证。核心就是用个人访问令牌（Personal Access Token, PAT）代替密码，然后让 Git 记住它。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;git凭证管理器（Git Credential Manager, GCM）&lt;/strong&gt; 是 GitHub 官方推荐的一种登陆方式，它在后台安全地处理 OAuth 登录，完全不需要用户手动生成和管理令牌。相比于Cache临时缓存，自己管理访问Token来说，这种方式更加智能无感，也更加安全&lt;/p&gt;
&lt;p&gt;:::warn
凭证管理器一般依赖于系统的 &lt;strong&gt;“密码环”&lt;/strong&gt; 服务。 在Linux系统中，这是一个实现并接入 Secret Service API 的、完整的凭证存储和管理方案。它会随桌面环境自动启动，并默认与用户密码关联。
:::&lt;/p&gt;
&lt;p&gt;:::tip
主流的桌面环境，如Gnome,KDE都将该服务内置在了系统中（Gnome Keyring, 以及KDE Wallet），如果使用这之外的DE，可能得选择一些开源方案进行替代，如&lt;code&gt;KeePassXC&lt;/code&gt;与&lt;code&gt;pass + pass-secret-service&lt;/code&gt;，这里只以Ubuntu + Gnome / KDE系统为例进行说明，&lt;code&gt;KeePassXC&lt;/code&gt;相关的可能之后单独写一篇文章进行补充。
:::&lt;/p&gt;
&lt;p&gt;凭证管理器可以选择：&lt;code&gt;git-credential-manager&lt;/code&gt;（仅Debian/Ubuntu）或者&lt;code&gt;git-credential-oauth&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装Git使用凭证管理器：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# git-credential-manager (Debian/Ubuntu)
sudo apt update &amp;amp;&amp;amp; sudo apt install git-credential-manager

# Fedora
sudo dnf install git-credential-oauth
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;git-credential-manager&lt;/code&gt; 配置&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 配置 Git 使用凭证管理助手
git config --global credential.helper manager

# 配置凭证存储后端
git credential-manager configure
git config --global credential.credentialStore secretservice 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git-credential-manager diagnose
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;code&gt;git-credential-oauth&lt;/code&gt; 配置&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;# 配置 Git 使用缓存助手 (默认 900 秒/15分钟) 和 OAuth 助手
git config --global credential.helper &apos;cache&apos;
git config --global credential.helper &apos;oauth&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当触发&lt;code&gt;git push/pull&lt;/code&gt;命令的时候，会自动开始认证。&lt;/p&gt;
&lt;p&gt;:::tip
如果认证过程中断，需要先清楚当前认证缓存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 1. 清理凭据缓存
git credential-cache exit
# 2. 终止任何可能卡住的 git 进程 (按 Ctrl + C 强制结束)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来重新开始进行认证就好
:::&lt;/p&gt;
&lt;h3&gt;git常用命令&lt;/h3&gt;
&lt;h4&gt;分支管理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;将当前修改commit到新的分支&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git stash
git checkout -b &amp;lt;新的分支名&amp;gt;
git stash pop
git commit -a
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;更改提交消息内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git commit --amend -m &quot;这是新的、正确的提交信息&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;其他配置&lt;/h2&gt;
&lt;h3&gt;Ubuntu 与 Windows 双系统时间同步&lt;/h3&gt;
&lt;p&gt;Windows 使用的是LocalTime机制，也就是会直接读取BIOS中的时间当作本地时间。而 Ubuntu 使用的时间机制是UTC机制，也就是格林尼治时间，会在BIOS时间的基础上加上时区（若Ubuntu设置的地区是&lt;code&gt;Shanghai&lt;/code&gt;，则会在BIOS的基础上+8）。因此需要在Ubuntu中将时间机制更改为LocalTime模式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装ntpdate工具实现时间同步
sudo apt install ntpdate

# 设置ntp服务器
sudo ntpdate time.windows.com

# 更改系统时间机制，更改为LocalTime
sudo hwclock --localtime --systohc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
除了windows的ntp时间服务器之外，还有如下ntp服务器可以选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 阿里云授时服务器
ntp.aliyun.com

# 腾讯云授时服务器
time1.cloud.tencent.com 
time2.cloud.tencent.com 
time3.cloud.tencent.com
time4.cloud.tencent.com
time5.cloud.tencent.com

# 清华大学授时服务器
ntp.tuna.tsinghua.edu.cn

# 苹果授时服务器   
time1.apple.com
time2.apple.com
time3.apple.com
time4.apple.com
time5.apple.com
time6.apple.com
time7.apple.com

# Google授时服务器   
time1.google.com
time2.google.com
time3.google.com
time4.google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;Linux 服务器&lt;/h2&gt;
&lt;p&gt;:::note
此处的服务器搭载Ubuntu 22.04系统
:::&lt;/p&gt;
&lt;h3&gt;服务器用户组&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;组别&lt;/th&gt;
&lt;th&gt;用户标识号&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;拥有系统的最高权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sudo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;允许其成员通过 &lt;code&gt;sudo&lt;/code&gt; 命令临时获取 root 权限来执行命令&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;adm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;组成员通常可以读取 &lt;code&gt;/var/log&lt;/code&gt; 目录下的许多日志文件，方便追踪系统问题，但不能修改它们&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;daemon&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;用于一些不需要特殊权限的后台服务&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;用于存放系统可执行文件的目录相关联，一些核心命令可能属于这个组&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sys&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;和系统核心、驱动程序等底层资源相关&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;www-data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;33&lt;/td&gt;
&lt;td&gt;Web服务器（如 Nginx 或 Apache）默认使用的用户和组。网站文件（如HTML, PHP）的所有者通常需要设置为 www-data，以便Web服务器有权限读取和执行它们&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mail&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;用于邮件服务（如 Postfix）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ssh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;114&lt;/td&gt;
&lt;td&gt;用于 SSH (Secure Shell) 守护进程，管理远程登录的权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;998&lt;/td&gt;
&lt;td&gt;允许其成员执行 Docker 命令，而无需使用 &lt;code&gt;sudo&lt;/code&gt;。将一个用户添加到此组，意味着你给予了他管理Docker容器的权限，这几乎等同于 root 权限，因为可以轻易地通过 Docker 容器挂载宿主机目录来获得 root 访问权限&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system-*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;如 systemd-journal, systemd-network，都是由 systemd 初始化系统创建和使用的，用于管理各自的子服务，例如日志、网络、时间同步等&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;用户管理&lt;/h3&gt;
&lt;p&gt;检查用户组命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /etc/group    # 查看所有用户组信息
groups            # 列出当前登录用户所属的所有用户组
id                # 显示用户的 UID 和所属组的信息，包括所有附加组
groups &amp;lt;username&amp;gt; # 查看用户的所属组
id &amp;lt;username&amp;gt;     # 显示该用户的组信息
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;文件同步&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;rsync&lt;/code&gt;同步&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# -a   归档模式，保留权限、时间戳等属性  
# -v   显示详细输出  
# -z   传输时压缩数据  
# --delete  删除目标中源目录没有的文件  

# 将本地文件传到远程服务器（push）
rsync -avz --delete ./dist/ 服务器的用户名@服务器的IP:/server_path/

# 将远程文件传输到本地（pull）
rsync -a username@remote_host:/home/username/dir1 place_to_sync_on_local_machine

# 指定 ssh 端口
rsync -avz -e &quot;ssh -p $port&quot; /local/path/ user@remoteip:/path/to/files/

# 设置备份带宽
rsync --bwlimit=100 -avzhe ssh /var/lib/rpm/ root@remoteip:/root/tmprpm/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;nginx 代理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;测试nginx配置&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;nginx -t
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;启动nginx服务&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start nginx        # 启动nginx服务
sudo systemctl enable --now nginx # 开机自启
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;关闭nginx服务&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl stop nginx
sudo nginx -s stop        # 直接停止
sudo nginx -s quit        # 平滑关闭
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Lab - Buffer Pool</title><link>https://blog.onemom.top/posts/labbufferpool/</link><guid isPermaLink="true">https://blog.onemom.top/posts/labbufferpool/</guid><description>CMU Database Course Practise</description><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Lab - Buffer Pool&lt;/h1&gt;
&lt;p&gt;在一个面向磁盘的（disk-oriented）数据库存储系统（DBMS）中开发一个存储管理器（Storage manager）.&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;cmu-db/bustub&quot;}&lt;/p&gt;
&lt;p&gt;实现以下存储管理器组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LRU-K 替换策略&lt;/li&gt;
&lt;li&gt;缓冲池管理器&lt;/li&gt;
&lt;li&gt;读/写页面保护&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除此之外，需要保证系统是&lt;strong&gt;线程安全&lt;/strong&gt;的。&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;bustub通过&lt;code&gt;disk_manager_memory&lt;/code&gt;管理磁盘中的数据，可以通过数据在数据库中的标识符&lt;code&gt;page_it_t&lt;/code&gt;对磁盘中的数据进行读写。我们要实现的Buffer Pool &lt;code&gt;src/buffer/buffer_pool_manager.cpp&lt;/code&gt;通过帧标识&lt;code&gt;frame_id&lt;/code&gt;来管理缓冲池中的&lt;code&gt;page_id&lt;/code&gt;。需要通过&lt;code&gt;src/buffer/lru_k_replacer.cpp&lt;/code&gt;制定的LRU-K策略来决定帧的保留或者淘汰（Evicted），还需要实现&lt;code&gt;src/storage/page/page_guard.cpp&lt;/code&gt;保证读写安全。&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://img.onemom.top/i/2025/11/10/69116663b5e28.png&quot; alt=&quot;bustub overview&quot; style=&quot;zoom:80%;&quot; /&amp;gt;&lt;/p&gt;
&lt;p&gt;资源管理流程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;page = bpm-&amp;gt;FetchPage(...)&lt;/code&gt;  // 获取资源（Pin Count +1）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;page-&amp;gt;WLatch()&lt;/code&gt;             // 获取资源（加写锁）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;... 对页面进行读写操作 ...&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;page-&amp;gt;WUnlatch()&lt;/code&gt;           // &lt;strong&gt;必须手动释放锁&lt;/strong&gt;（PageGuard不需要）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;bpm-&amp;gt;UnpinPage(...)&lt;/code&gt;        // &lt;strong&gt;必须手动释放Pin&lt;/strong&gt;（PageGuard不需要）&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;任务1: LRU-K 替换策略&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;LRU&lt;/strong&gt;（Least recently used，最近最少使用）算法根据数据的历史访问记录来进行淘汰数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LRU-K&lt;/strong&gt;在LRU的基础上提出要求，只有达到K次访问之后，才能被加入到访问历史中（LRU是可被看作LRU-1）。&lt;/p&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;LRU最经典的实现方式是通过双链表+哈希表的方式来实现。双链表实现对历史数据的添加与淘汰，而哈希表则保证能通过O(1)的复杂度访问历史数据。参考：&lt;a href=&quot;https://leetcode.cn/problems/lru-cache/&quot;&gt;Leetcode - LRU缓存&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;LRU-K可以在LRU的基础上，可是使用两个双链表（&lt;code&gt;cache_list&lt;/code&gt;, &lt;code&gt;history_list&lt;/code&gt;）来维护。对于没有达到K次访问的链表&lt;code&gt;history_list&lt;/code&gt;，可以通过最后一次访问与当前访问的时间戳的差来进行淘汰，而达到K次访问的&lt;code&gt;cache_list&lt;/code&gt;链表则可以通过LRU的规则来进行淘汰。&lt;/p&gt;
&lt;h3&gt;代码实现&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;数据结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class LRUKNode {
 public:
  std::list&amp;lt;size_t&amp;gt; history_;
  size_t k_;
  frame_id_t fid_;
  bool is_evictable_{false};
};

class LRUKReplacer {
	std::unordered_map&amp;lt;frame_id_t, LRUKNode&amp;gt; node_store_;	// 索引到正确的LRUKNode
   	size_t current_timestamp_{0};
   	size_t curr_size_{0};
   	size_t replacer_size_;
   	size_t k_;
   	std::mutex latch_;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;任务2: 缓冲池管理器&lt;/h2&gt;
&lt;p&gt;缓冲池管理器的实现主要依赖于以下两个核心机制：&lt;strong&gt;Pin/Unpin 机制 (引用计数)&lt;strong&gt;与&lt;/strong&gt;读写锁 (RLatch/WLatch)&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;Pin/Upin 机制&lt;/h3&gt;
&lt;p&gt;类似&lt;code&gt;shared_ptr&lt;/code&gt;的引用计数器。当一个线程通过&lt;code&gt;FetchPage&lt;/code&gt;或&lt;code&gt;NewPage&lt;/code&gt;从BPM获取一个页面时，BPM会将该页面的&lt;code&gt;pin_count_&lt;/code&gt;加1。而当线程使用完这个页面后，它必须调用&lt;code&gt;UnpinPage&lt;/code&gt;。BPM会随之将该页面的&lt;code&gt;pin_count_&lt;/code&gt;减1。&lt;/p&gt;
&lt;p&gt;这样的机制能保证一个正在被使用的界面不会被意外淘汰。&lt;/p&gt;
&lt;h3&gt;读写锁（RLatch/WLatch）&lt;/h3&gt;
&lt;p&gt;通过&lt;code&gt;Page&lt;/code&gt;对象内部的读写锁（&lt;code&gt;RLatch()&lt;/code&gt;与&lt;code&gt;WLatch&lt;/code&gt;）实现读写的线程安全。其中读锁&lt;code&gt;RLock&lt;/code&gt;为共享锁&lt;code&gt;std::shared_mutex&lt;/code&gt;，写锁为独占锁&lt;code&gt;std::unique_lock&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;除此之外，Page中的&lt;code&gt;is_dirty_&lt;/code&gt;参数标记了一个页面是否被修改过，当页面重新被调用时，一定会先调用&lt;code&gt;WritePage()&lt;/code&gt;将页面写到磁盘上，确保此时的界面是同步的。&lt;/p&gt;
&lt;h2&gt;任务3: 读写守护程序（Page Guards）&lt;/h2&gt;
&lt;p&gt;任务三的核心原理是 &lt;strong&gt;RAII (Resource Acquisition Is Initialization)，即“资源获取即初始化”&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;在整个资源管理流程中，调用缓冲池内容时，锁和&lt;code&gt;Page&lt;/code&gt;的计数器没有正确处理，会导致&lt;strong&gt;死锁&lt;/strong&gt;或&lt;strong&gt;缓冲池泄漏&lt;/strong&gt;（页面永远无法被淘汰）的问题。&lt;/p&gt;
&lt;p&gt;需要考虑的页面属性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pin_cout&lt;/code&gt;：访问当前页面线程数量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WLatch/RLatch&lt;/code&gt;：读写锁&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_dirty&lt;/code&gt;：是否已经被写入内容&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;RAII通过C++的语言特性（构造函数和析构函数）来根治上述问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;资源获取 (Acquisition):&lt;/strong&gt; 在一个对象的&lt;strong&gt;构造函数&lt;/strong&gt;中获取资源（例如，调用&lt;code&gt;FetchPage&lt;/code&gt;、加锁）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;资源释放 (Release):&lt;/strong&gt; 在这个对象的&lt;strong&gt;析构函数&lt;/strong&gt;中释放资源（例如，调用&lt;code&gt;UnpinPage&lt;/code&gt;、解锁）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;h4&gt;1. 构造&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;src/include/storage/page_guard.h&lt;/code&gt;中可以看到实现Page Guard需要实现&lt;code&gt;BasicPageGuard&lt;/code&gt;以及专门的读写守护&lt;code&gt;ReadPageGuard&lt;/code&gt;和&lt;code&gt;WritePageGuard&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;BasicPageGuard&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;核心：负责核心Pin\Unpin管理&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Page* &lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BufferPoolManager* &lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;ReadPageGuard&lt;/code&gt;和&lt;code&gt;WritePageGuard&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;都继承自&lt;code&gt;BasicPageGuard&lt;/code&gt;，分别执行读和写的操作。&lt;/p&gt;
&lt;p&gt;核心：读写锁的管理，WritePageGuard在被构造时需自动标记脏页(dirty)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Page::RLatch/Page::WLatch&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 所有权管理&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;禁止拷贝：&lt;/strong&gt; 拷贝构造函数和拷贝赋值运算符都应该被删除 (&lt;code&gt;= delete&lt;/code&gt;)。因为如果可以拷贝，两个&lt;code&gt;PageGuard&lt;/code&gt;对象就会指向同一个页面，当它们先后析构时，会导致同一资源被释放两次（例如，&lt;code&gt;Unpin&lt;/code&gt;两次），引发程序崩溃。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;允许移动：&lt;/strong&gt; 移动构造函数和移动赋值运算符需要被实现。这允许一个函数安全地返回一个&lt;code&gt;PageGuard&lt;/code&gt;对象，或者将所有权从一个变量转移到另一个。
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;移动构造函数逻辑：&lt;/strong&gt; &lt;code&gt;PageGuard new_guard(std::move(old_guard));&lt;/code&gt;
&lt;ol&gt;
&lt;li&gt;将&lt;code&gt;old_guard&lt;/code&gt;的&lt;code&gt;bpm_&lt;/code&gt;和&lt;code&gt;page_&lt;/code&gt;指针“窃取”过来，赋给&lt;code&gt;new_guard&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键一步：&lt;/strong&gt; 将&lt;code&gt;old_guard&lt;/code&gt;的&lt;code&gt;bpm_&lt;/code&gt;和&lt;code&gt;page_&lt;/code&gt;指针设为&lt;code&gt;nullptr&lt;/code&gt;。这“解除”了&lt;code&gt;old_guard&lt;/code&gt;，当&lt;code&gt;old_guard&lt;/code&gt;析构时，它会发现自己无事可做，从而避免了双重释放。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 析构&lt;/h4&gt;
&lt;p&gt;析构函数中应该考虑锁和资源之间的释放关系，释放资源的顺序必须是：&lt;strong&gt;先释放Page的资源，解锁，再Unpin页面&lt;/strong&gt;。在解锁时，页面仍然被Pin住，能保证内容是稳定的，否则BPM中的其他使用这个Page线程会因为当前Pin计数为0，提前释放当前Page的资源（特别是锁指针），导致当前线程的锁指针指向错误的锁。解锁后，再Unpin，允许BPM安全地回收它。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;正确析构逻辑：&lt;strong&gt;&lt;code&gt;{ page_-&amp;gt;WUnlatch(); /\* BasicGuard的析构函数，完成Unpin \*/ }&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;错误析构逻辑：&lt;strong&gt;&lt;code&gt; { page_-&amp;gt;WUnlatch(); bpm_-&amp;gt;UnpinPage(...); }&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;参考&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://15445.courses.cs.cmu.edu/spring2023/project1/&quot;&gt;CMU1 15-445/645&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.cnblogs.com/JayL-zxl/p/14311883.html&quot;&gt;周小伦博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/ClaireSy/article/details/121103484&quot;&gt;Netfishless [CSDN blog]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.youtube.com/watch?v=bU38Ovdh3NY&amp;amp;list=PLSE8ODhjZXjYVdJKka5g3xTKfPBITrxOu&quot;&gt;CMU Database Group [Youtube]&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;环境搭建&lt;/h2&gt;
&lt;p&gt;使用docker进行部署：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build . -t bustub	# 编译bustub容器
docker create -t -i --name bustub -v $(pwd):/bustub bustub bash	# 创建bustub容器
docker start bustub	# 后台运行bustub容器
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>