<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/scripts/pretty-feed-v3.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:h="http://www.w3.org/TR/html4/"><channel><title>awfufu</title><description>awfufu 的博客</description><link>https://awfufu.com</link><item><title>NVIDIA Orin NX/Nano 初始化</title><link>https://awfufu.com/blog/nvidia-orin-init</link><guid isPermaLink="true">https://awfufu.com/blog/nvidia-orin-init</guid><description>Orin NX/Nano 的系统安装、配网及 SSH 初步配置</description><pubDate>Tue, 17 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside, Tabs, TabItem } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;不常用的流程及指令容易遗忘，而有些流程是低频却必要的，比如新设备的初始化。本文记录 NVIDIA Orin NX/Nano 系统的安装以及初步配置流程以便查阅。&lt;/p&gt;
&lt;h2&gt;一、 WSL2配置&lt;/h2&gt;
&lt;p&gt;仅供 Windows 用户参考，原生 Ubuntu 用户可直接跳到&lt;a href=&quot;#%E4%BA%8C-%E7%B3%BB%E7%BB%9F%E5%AE%89%E8%A3%85&quot;&gt;系统安装&lt;/a&gt;
在 &lt;code&gt;~/.wslconfig&lt;/code&gt; 下加入如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[wsl2]
# 刷机需要与设备处于同一物理局域网，所以使用镜像网络。
networkingMode=mirrored

# 可选：仅在开启代理软件的 TUN 模式（如 Clash/Mihomo 虚拟网卡），且导致 WSL2 镜像网络启动异常
# （例如 `ip a` 只有 `lo`）时再配置。将 TUN 网卡加入黑名单后，WSL2 仍可正常克隆物理网卡。
# 这里的 `Mihomo` 是你的 TUN 网卡名称。
ignoredNetworkAdapters=Mihomo

# 关闭自动代理，避免 WSL 内部环境变量与宿主机 TUN 路由互相干扰。
autoProxy=false
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二、 系统安装&lt;/h2&gt;
&lt;h3&gt;2.1 GUI 安装&lt;/h3&gt;
&lt;p&gt;SDK Manager下载：&lt;a href=&quot;https://developer.nvidia.com/drive/sdk-manager&quot;&gt;NVIDIA SDK Manager&lt;/a&gt;
之后基本就是傻瓜式操作了，详细教程见 &lt;a href=&quot;https://docs.nvidia.com/sdk-manager/install-with-sdkm-jetson/index.html&quot;&gt;官方文档&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;2.2 CLI安装&lt;/h3&gt;
&lt;p&gt;对 &lt;strong&gt;Jetson Orin NX / Nano&lt;/strong&gt; 而言，NVIDIA 官方当前推荐的命令行刷机路线是 &lt;strong&gt;&lt;code&gt;initrd flashing&lt;/code&gt;&lt;/strong&gt;。依据见 &lt;a href=&quot;https://docs.nvidia.com/jetson/archives/r36.4.4/DeveloperGuide/SD/FlashingSupport.html&quot;&gt;Flashing Support&lt;/a&gt; 与 &lt;a href=&quot;https://docs.nvidia.com/jetson/archives/r36.4/DeveloperGuide/IN/QuickStart.html&quot;&gt;Quick Start&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;2.2.1 安装前提&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先下载两个官方包&lt;/strong&gt;：BSP release package 和 sample rootfs，下载入口在 &lt;a href=&quot;https://developer.nvidia.com/embedded/jetson-linux-archive&quot;&gt;Jetson Linux 下载页&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;**Orin NX / Nano 默认记 &lt;code&gt;initrd flash&lt;/code&gt;**而不是 &lt;code&gt;flash.sh&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;2.2.2 环境准备&lt;/h4&gt;
&lt;p&gt;官方准备步骤如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 解压 Jetson Linux BSP 包
tar xf ${L4T_RELEASE_PACKAGE}
# 将 sample rootfs 解压到 Linux_for_Tegra/rootfs
sudo tar xpf ${SAMPLE_FS_PACKAGE} -C Linux_for_Tegra/rootfs/
cd Linux_for_Tegra/
# 安装刷机依赖
sudo ./tools/l4t_flash_prerequisites.sh
# 应用 NVIDIA 提供的二进制组件
sudo ./apply_binaries.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;${L4T_RELEASE_PACKAGE}&lt;/code&gt; 和 &lt;code&gt;${SAMPLE_FS_PACKAGE}&lt;/code&gt; 指的就是刚下载的那两个官方包。&lt;br&gt;
顺序是：&lt;strong&gt;先解包 rootfs，再跑前置依赖，最后 &lt;code&gt;apply_binaries.sh&lt;/code&gt;。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;2.2.3 Force Recovery Mode&lt;/h4&gt;
&lt;p&gt;对于 &lt;strong&gt;Jetson Orin Nano Developer Kit&lt;/strong&gt;，Quick Start 给出的进入 Force Recovery Mode 的方式如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;断开电源，确保开发板处于关机状态。&lt;/li&gt;
&lt;li&gt;在 12-pin button header 上短接 &lt;code&gt;REC&lt;/code&gt; 和 &lt;code&gt;GND&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;重新接上电源。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;可以在主机上直接执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 主机侧确认 Recovery Mode 识别结果
lsusb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能看到类似下面这一行，才说明板子确实进了恢复模式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Bus 001 Device 025: ID 0955:7523 NVIDIA Corp. APX
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;2.2.4 命令行刷写&lt;/h4&gt;
&lt;p&gt;Quick Start 给出的 Jetson Orin Nano Developer Kit 官方示例如下：&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--external-device nvme0n1p1&lt;/code&gt;：目标系统写入的外部介质分区。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-c tools/kernel_flash/flash_l4t_t234_nvme.xml&lt;/code&gt;：外部介质布局配置文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p &quot;-c bootloader/generic/cfg/flash_t234_qspi.xml&quot;&lt;/code&gt;：QSPI 启动介质配置。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--showlogs&lt;/code&gt;：输出详细刷机日志。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--network usb0&lt;/code&gt;：通过 USB 虚拟网卡完成刷机过程中的网络通信。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;jetson-orin-nano-devkit internal&lt;/code&gt;：目标板型与刷写模式参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;--network usb0&lt;/code&gt; 不能漏。Flashing Support 提到，&lt;code&gt;initrd flash&lt;/code&gt; 过程中工具会通过 &lt;code&gt;IPv6&lt;/code&gt; 地址空间 &lt;code&gt;fc00:1:1::/48&lt;/code&gt; 使用 NFS 和 SSH。&lt;/p&gt;
&lt;h4&gt;2.2.5 启动介质确认&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;第一次启动时，要在 UEFI 菜单里选对刚刷进去的启动介质&lt;/strong&gt;。&lt;br&gt;
比如刷的是 NVMe，启动项也应指向 NVMe。&lt;/p&gt;
&lt;h4&gt;2.2.6 参考资料&lt;/h4&gt;
&lt;p&gt;后面如果又忘了细节，优先翻这几个官方页面和官方论坛贴：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/jetson/archives/r36.4/DeveloperGuide/IN/QuickStart.html&quot;&gt;NVIDIA Jetson Linux Quick Start&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.nvidia.com/jetson/archives/r36.4.4/DeveloperGuide/SD/FlashingSupport.html&quot;&gt;NVIDIA Flashing Support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://forums.developer.nvidia.com/t/recovery-mode-for-jetson-orin-nano-0955-7523-p3767-0003-with-8gb/318754&quot;&gt;NVIDIA Developer Forums: Recovery Mode for Jetson Orin Nano&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://forums.developer.nvidia.com/t/failed-to-connect-jetson-orin-nano-dev-kit-to-host-via-usb-c-after-flashing-jetpack-6-2/323030&quot;&gt;NVIDIA Developer Forums: USB-C / Recovery 识别问题讨论&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;三、初次配网&lt;/h2&gt;
&lt;h3&gt;3.1 Headless下无路由器后台权限获取Nano局域网IP&lt;/h3&gt;
&lt;p&gt;适用范围：板卡已连接工作机同一局域网，但没有显示器、没有路由器后台权限。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-powershell&quot;&gt;# 1. 查看本机 IPv4，确认当前网段（本文示例为 192.168.1.0/24）
ipconfig

# 2. 只扫描开放 22 端口的主机
nmap -p 22 --open 192.168.1.0/24

# 3. 根据扫描结果尝试 SSH，确认目标地址
ssh nvidia@192.168.1.20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nmap -p 22 --open&lt;/code&gt; 用于只保留开放 SSH 端口的主机，候选更少。&lt;/li&gt;
&lt;li&gt;Nano 安装系统后 &lt;code&gt;sshd&lt;/code&gt; 默认开启，因此可先用 &lt;code&gt;22&lt;/code&gt; 端口做一轮筛选。&lt;/li&gt;
&lt;li&gt;本机实测在 &lt;code&gt;192.168.1.0/24&lt;/code&gt; 下可正常筛出 &lt;code&gt;192.168.1.20&lt;/code&gt;，其 MAC 厂商会显示为 &lt;code&gt;Nvidia&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;拿到候选地址后，再用 &lt;code&gt;ssh nvidia@&amp;#x3C;ip&gt;&lt;/code&gt; 做最终确认。&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://nmap.org/download.html&quot;&gt;Nmap Download&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://npcap.com/&quot;&gt;Npcap&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文写作时，本机使用 &lt;code&gt;Nmap 7.98 + Npcap 1.87&lt;/code&gt;，&lt;code&gt;nmap -sn 192.168.1.0/24&lt;/code&gt; 可正常执行。&lt;/p&gt;
&lt;h3&gt;3.2 配置 mDNS 访问&lt;/h3&gt;
&lt;p&gt;具体配置过程见&lt;a href=&quot;/blog/nvidia-orin-environment-setup#22-%E6%8A%9B%E5%BC%83%E9%9D%99%E6%80%81-ip%E9%85%8D%E7%BD%AE-mdns-%E8%AE%BF%E9%97%AE&quot;&gt;《NVIDIA Orin 环境配置》&lt;/a&gt;的对应章节，这里不再重复展开。&lt;/p&gt;
&lt;h2&gt;四、SSH 扫盲&lt;/h2&gt;
&lt;p&gt;进入远程开发阶段后，&lt;code&gt;SSH&lt;/code&gt; 基本就是你和 Orin Nano 交互的默认入口。无论是工业研发团队，还是高校计算平台，成熟做法都相当一致：以 &lt;code&gt;SSH&lt;/code&gt; 作为默认控制面，以公钥认证作为默认鉴权手段，再用主机别名和端口转发把日常开发流程固化下来。&lt;/p&gt;
&lt;h3&gt;4.1 第一次连接&lt;/h3&gt;
&lt;p&gt;如果这台 Nano 还没配好 &lt;code&gt;mDNS&lt;/code&gt;，就先用刚扫出来的 IP 登录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh nvidia@192.168.1.20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次连接时，终端通常会提示你确认主机指纹，输入 &lt;code&gt;yes&lt;/code&gt; 即可。登录成功后，建议先记住下面几条最常用的确认命令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;hostname      # 当前主机名
hostname -I   # 当前 IP
whoami        # 当前登录用户
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4.2 用 &lt;code&gt;~/.ssh/config&lt;/code&gt; 给设备起别名&lt;/h3&gt;
&lt;p&gt;如果每次都手动输入 &lt;code&gt;ssh nvidia@192.168.x.x&lt;/code&gt;，很快就会烦。更实用的做法是在开发机的 &lt;code&gt;~/.ssh/config&lt;/code&gt; 里给每台设备起一个固定别名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Host nrc17-via-jump
    HostName 192.168.1.20              # Nano 在内网中的地址
    User nvidia                        # Nano 登录用户
    IdentityFile ~/.ssh/id_ed25519     # 登录 Nano 使用的私钥
    IdentitiesOnly yes                 # 只使用指定私钥
    ProxyJump jump-lab                 # 先跳到 jump-lab，再转发到目标主机
    ConnectTimeout 5                   # 避免跳板链路异常时卡太久
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;Host&lt;/code&gt; 后面的名字是你自己定义的别名。这样以后直接执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh nrc17
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可登录对应设备。&lt;br&gt;
如果跳板链路不止一层，&lt;code&gt;ProxyJump&lt;/code&gt; 也支持继续写成 &lt;code&gt;jump-a,jump-b&lt;/code&gt; 这种链式形式。&lt;/p&gt;
&lt;h3&gt;4.3 免密登录&lt;/h3&gt;
&lt;p&gt;工程上更推荐把公钥提前配好，避免每次输密码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 本机生成 ed25519 密钥；如果已有 ~/.ssh/id_ed25519，可跳过
ssh-keygen -t ed25519

# 将本机公钥复制到目标设备
ssh-copy-id nrc17

# 验证免密是否生效
ssh nrc17
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一步在多机调试、频繁重连、脚本化部署时尤其省事。&lt;/p&gt;
&lt;h3&gt;4.4 常用的几种 SSH 命令&lt;/h3&gt;
&lt;h4&gt;远程执行命令&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 指定私钥、连接超时和 SSH 端口；登录后在远端执行巡检命令
ssh -i ~/.ssh/id_ed25519 -o ConnectTimeout=5 -p 22 nrc17 \
  &quot;hostname &amp;#x26;&amp;#x26; hostname -I &amp;#x26;&amp;#x26; systemctl is-active ssh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt;：指定端口，如 &lt;code&gt;ssh -p 2222 nrc17&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-i&lt;/code&gt;：指定私钥文件，如 &lt;code&gt;ssh -i ~/.ssh/id_ed25519 nrc17&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-L&lt;/code&gt;：本地端口转发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-J&lt;/code&gt;：通过跳板机连接，等价于 &lt;code&gt;ProxyJump&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o&lt;/code&gt;：临时指定配置项，如 &lt;code&gt;-o ConnectTimeout=5&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;传文件&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 指定私钥、端口和压缩，将本机文件上传到 Nano
scp -i ~/.ssh/id_ed25519 -P 22 -C ./config.yaml nrc17:~/config.yaml

# 递归拉回远端目录
scp -i ~/.ssh/id_ed25519 -P 22 -r nrc17:~/logs ./logs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-P&lt;/code&gt;：指定端口，注意这里是大写 &lt;code&gt;P&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-i&lt;/code&gt;：指定私钥文件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-r&lt;/code&gt;：递归复制目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-C&lt;/code&gt;：启用压缩，适合弱网环境&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-o&lt;/code&gt;：透传 SSH 配置项，如 &lt;code&gt;-o ConnectTimeout=5&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;端口转发&lt;/h4&gt;
&lt;p&gt;如果某个服务只监听在 Nano 本机回环地址上，可以用 &lt;code&gt;ssh -L&lt;/code&gt; 把端口转到自己的电脑。例如 Nano 上某个 WebSocket 或 Web 服务监听在 &lt;code&gt;8765&lt;/code&gt; 端口：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 指定私钥与端口，只建立隧道、不打开 shell，并放到后台运行
ssh -i ~/.ssh/id_ed25519 -p 22 -N -f \
  -L 8765:localhost:8765 \
  nrc17
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连上后，本机访问 &lt;code&gt;localhost:8765&lt;/code&gt;，实际就等于访问 Nano 的 &lt;code&gt;localhost:8765&lt;/code&gt;。&lt;br&gt;
这种方式并不局限于 &lt;code&gt;foxglove_bridge&lt;/code&gt;，在 Jupyter、TensorBoard 等交互式服务上也普遍采用同样的 SSH 隧道模式。&lt;/p&gt;
&lt;p&gt;常用参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-L [本机端口]:[远端地址]:[远端端口]&lt;/code&gt;：本地端口转发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-N&lt;/code&gt;：只建立隧道，不执行远程命令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-f&lt;/code&gt;：放到后台运行，通常和 &lt;code&gt;-N&lt;/code&gt; 搭配&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt;：指定 SSH 端口&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-J&lt;/code&gt;：通过跳板机建立隧道&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.5 关于 &lt;code&gt;ForwardAgent&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ForwardAgent&lt;/code&gt; 能让远端主机借用你本地 &lt;code&gt;ssh-agent&lt;/code&gt; 中已经加载的密钥，在某些需要从 Nano 再访问 Git 仓库的场景里会很方便。但这一项要谨慎使用，建议只对明确受信任的主机单独开启，不要写成 &lt;code&gt;Host *&lt;/code&gt; 的全局默认值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Host nrc17
    HostName nrc17.local
    User nvidia
    ForwardAgent yes                   # 允许远端借用本机 ssh-agent；仅建议对受信任主机开启
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你并不需要在 Nano 上继续跳转到别的 SSH 服务，那么这一项完全可以不配。&lt;/p&gt;
&lt;h2&gt;五、常用服务与配置文件&lt;/h2&gt;
&lt;p&gt;容易忘的。&lt;/p&gt;
&lt;h3&gt;5.1 系统层&lt;/h3&gt;
&lt;h4&gt;核心服务&lt;/h4&gt;
&lt;p&gt;| 条目                   | 作用                                                                    | 最小示例                                   |
| ---------------------- | ----------------------------------------------------------------------- | ------------------------------------------ |
| &lt;code&gt;ssh.service&lt;/code&gt;          | 远程登录入口。系统刷好以后，后续大多数终端操作都从它开始。              | &lt;code&gt;sudo systemctl enable --now ssh&lt;/code&gt;          |
| &lt;code&gt;avahi-daemon.service&lt;/code&gt; | 提供 &lt;code&gt;.local&lt;/code&gt; 形式的 &lt;code&gt;mDNS&lt;/code&gt; 主机发现，便于在局域网内用主机名访问 Nano。 | &lt;code&gt;sudo systemctl enable --now avahi-daemon&lt;/code&gt; |&lt;/p&gt;
&lt;h4&gt;核心配置文件&lt;/h4&gt;
&lt;p&gt;| 条目                           | 作用                                                                | 最小示例                |
| ------------------------------ | ------------------------------------------------------------------- | ----------------------- |
| &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt;         | &lt;code&gt;sshd&lt;/code&gt; 的主配置文件。常改的通常是认证方式、端口与登录策略。         | &lt;code&gt;Port 22&lt;/code&gt;               |
| &lt;code&gt;/etc/avahi/avahi-daemon.conf&lt;/code&gt; | &lt;code&gt;avahi-daemon&lt;/code&gt; 的配置文件。和 &lt;code&gt;mDNS&lt;/code&gt;、主机名发布相关。              | &lt;code&gt;publish-addresses=yes&lt;/code&gt; |
| &lt;code&gt;/etc/hostname&lt;/code&gt;                | 机器主机名。配合 &lt;code&gt;mDNS&lt;/code&gt; 后，常直接对应 &lt;code&gt;nrc17.local&lt;/code&gt; 这种访问方式。 | &lt;code&gt;nrc17&lt;/code&gt;                 |
| &lt;code&gt;/etc/hosts&lt;/code&gt;                   | 本地静态主机名解析表。改主机名或做本地别名映射时经常一起看。        | &lt;code&gt;127.0.1.1 nrc17&lt;/code&gt;       |&lt;/p&gt;
&lt;h4&gt;Ubuntu 常用配置文件&lt;/h4&gt;
&lt;p&gt;| 条目                             | 作用                                                               | 最小示例                                                         |
| -------------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------- |
| &lt;code&gt;/etc/apt/sources.list&lt;/code&gt;          | &lt;code&gt;apt&lt;/code&gt; 软件源主配置。换镜像、检查发行版源时最常看。                 | &lt;code&gt;deb http://ports.ubuntu.com/ubuntu-ports jammy main restricted&lt;/code&gt; |
| &lt;code&gt;/etc/apt/sources.list.d/*.list&lt;/code&gt; | 第三方仓库配置目录。Jetson、Docker、ROS 这类额外源通常会落在这里。 | &lt;code&gt;docker.list&lt;/code&gt;                                                    |
| &lt;code&gt;/etc/netplan/*.yaml&lt;/code&gt;            | Ubuntu 的网络配置文件。静态 IP、DNS、网卡名映射通常从这里改。      | &lt;code&gt;renderer: NetworkManager&lt;/code&gt;                                       |
| &lt;code&gt;/etc/fstab&lt;/code&gt;                     | 开机自动挂载表。挂载 NVMe 分区、数据盘或外设时常会改。             | &lt;code&gt;UUID=xxxx /data ext4 defaults 0 2&lt;/code&gt;                              |
| &lt;code&gt;/etc/environment&lt;/code&gt;               | 系统级环境变量文件。需要对所有用户生效的环境变量可以写在这里。     | &lt;code&gt;HTTP_PROXY=http://127.0.0.1:7890&lt;/code&gt;                               |
| &lt;code&gt;~/.bashrc&lt;/code&gt;                      | 用户级 shell 初始化文件。别名、&lt;code&gt;PATH&lt;/code&gt;、代理变量等更常写在这里。    | &lt;code&gt;export PATH=$PATH:/usr/local/bin&lt;/code&gt;                               |&lt;/p&gt;
&lt;h4&gt;自定义自启动&lt;/h4&gt;
&lt;p&gt;| 条目                            | 作用                                                                                          | 最小示例                               |
| ------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------- |
| &lt;code&gt;/etc/systemd/system/*.service&lt;/code&gt; | 自定义 &lt;code&gt;systemd&lt;/code&gt; 服务单元目录。后续如果要让节点、脚本或容器开机自启，通常就在这里写服务文件。 | &lt;code&gt;ExecStart=/usr/bin/docker compose up&lt;/code&gt; |&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;.service&lt;/code&gt; 模板&lt;/h4&gt;
&lt;p&gt;后面如果要把脚本、节点或容器做成开机自启，通常会在 &lt;code&gt;/etc/systemd/system/&lt;/code&gt; 下写一个 &lt;code&gt;.service&lt;/code&gt; 文件。最小模板如下：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;[Unit]
Description=My Startup Service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=nvidia
WorkingDirectory=/home/nvidia
ExecStart=/usr/bin/bash /home/nvidia/start.sh
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;After&lt;/code&gt; / &lt;code&gt;Wants&lt;/code&gt;：声明启动依赖，常见是等网络起来。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;User&lt;/code&gt;：指定以哪个用户身份运行。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WorkingDirectory&lt;/code&gt;：脚本或程序的工作目录。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExecStart&lt;/code&gt;：真正执行的命令。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Restart&lt;/code&gt; / &lt;code&gt;RestartSec&lt;/code&gt;：异常退出后的重启策略。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WantedBy=multi-user.target&lt;/code&gt;：表示在常规多用户启动阶段拉起。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><h:img src="/_astro/thumbnail.BmXBGBUL.png"/><enclosure url="/_astro/thumbnail.BmXBGBUL.png"/></item><item><title>NVIDIA Orin NX/Nano 环境配置与搭建</title><link>https://awfufu.com/blog/nvidia-orin-environment-setup</link><guid isPermaLink="true">https://awfufu.com/blog/nvidia-orin-environment-setup</guid><description>在 Orin NX/Nano 平台上部署 ROS2 (rstack) 的完整环境配置指南。</description><pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;import { Aside } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;h2&gt;一、 项目概述&lt;/h2&gt;
&lt;p&gt;本项目在 NVIDIA Jetson Orin NX/Nano 平台上部署一系列无人机上位机所需的ROS2包，下称 AADCL_UAV_UGV。为简化开发流程，并支持后续批量配置集群的需求，本项目通过容器化技术（Docker + Compose）部署，并利用 mDNS 协议简化集群组网。&lt;/p&gt;
&lt;h2&gt;二、 基础环境配置&lt;/h2&gt;
&lt;p&gt;本文适用于已预装基础系统的 Jetson Orin Nano。配置前需确保开发板已联网且可通过终端（SSH、屏幕或远程桌面）访问。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;前提条件&lt;/strong&gt;：稳定的局域网环境、已知开发板 IP 以及 SSH 客户端。&lt;/p&gt;
&lt;p&gt;首先进行网络配置。&lt;/p&gt;
&lt;h3&gt;2.1 连接 WiFi&lt;/h3&gt;
&lt;p&gt;要把 nano 连上开发/生产环境的局域网，最常用的办法是使用 &lt;code&gt;nmcli&lt;/code&gt; 指令。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;扫描附近的 WiFi&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nmcli device wifi list
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;连接到你的 SSID&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 把 Your_SSID 和 Your_Password 换成实际的
sudo nmcli device wifi connect &quot;Your_SSID&quot; password &quot;Your_Password&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;确认连接成功&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nmcli connection show  # 看到有绿色的一行就说明连上了
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.2 抛弃静态 IP：配置 mDNS 访问&lt;/h3&gt;
&lt;p&gt;在多机协同的环境下，记住每台车/无人机的 IP 地址是非常麻烦的。我们通过配置 mDNS，让你能在局域网内直接用 &lt;code&gt;nrcXX.local&lt;/code&gt; 这种名字访问 nano，不管它的 IP 怎么变。
如上文所说，我们进入了 Orin Nano 的终端，在终端下输入如下指令：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;给 nano 命名&lt;/strong&gt;：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 比如第 17 号机，就叫它 nrc17
sudo hostnamectl set-hostname nrc17
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;配置并启动服务 (Avahi)&lt;/strong&gt;：
安装 Avahi 并确保它在运行。注意：&lt;strong&gt;修改主机名后必须重启 Avahi 服务&lt;/strong&gt;才能让新名字在局域网生效。
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;sudo apt update &amp;#x26;&amp;#x26; sudo apt install -y avahi-daemon
sudo systemctl restart avahi-daemon
sudo systemctl enable avahi-daemon
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证一下&lt;/strong&gt;：在你的开发机（比如笔记本）上执行 &lt;code&gt;ping nrc17.local&lt;/code&gt;。如果能通，以后你只需要记住这个名字就行了。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.3 生产环境对齐：基于 SSH 的 Headless 开发&lt;/h3&gt;
&lt;p&gt;在无人机集群开发中，给每台 Orin Nano 配备显示器是不现实的。更核心的原因在于：&lt;strong&gt;运行图形界面 (GUI) 会消耗昂贵的 CPU/GPU 资源&lt;/strong&gt;，在 Orin Nano 这种算力受限的平台上，每一分资源都应该留给 ROS 管道和 AI 推理。&lt;/p&gt;
&lt;p&gt;因此，建议采用 &lt;strong&gt;Headless&lt;/strong&gt; 开发模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;性能最大化&lt;/strong&gt;：关闭桌面环境可节省数百 MB 显存及 CPU 周期。&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;code&gt;foxglove_bridge&lt;/code&gt; 等工具建立 WebSocket 通道，将原始数据流高效转发到你的电脑桌面上，使用 Foxglove Studio 进行低延迟的可视化调试。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;配置你的开发机 (推荐)&lt;/strong&gt;：
为了不用每次都输 IP 甚至不用输密码，建议在你的电脑（Linux/Mac/Win）的 &lt;code&gt;~/.ssh/config&lt;/code&gt; 中加入如下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Host nrc17
    HostName nrc17.local
    User nvidia
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样便相当于用 &lt;code&gt;ssh nrc17&lt;/code&gt; 代替了 &lt;code&gt;ssh nvidia@192.168.1.17&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;免密登录 (可选)&lt;/strong&gt;：
在你的开发机终端执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ssh-keygen -t ed25519  # 生成你的密钥（如果已经有了就跳过）
ssh-copy-id nrc17      # 把你的公钥“递给”nano
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成后，&lt;code&gt;ssh nrc17&lt;/code&gt; 便可直连（这在 vibe coding 时非常实用）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;2.4 容器环境：Docker 与 NVIDIA Toolkit&lt;/h3&gt;
&lt;p&gt;为了不把 nano 的原始系统环境搞乱，建议全量使用容器。除了 Docker，最关键的是要安装 NVIDIA 的转发工具（Toolkit），这样容器里的算法才能用到 Orin 的 GPU 算力。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;安装 Docker 引擎与 Compose V2 插件&lt;/strong&gt;：
由于 Ubuntu 22.04 的官方源不含 Compose V2 (即 &lt;code&gt;docker compose&lt;/code&gt; 指令)，我们需要添加 Docker 官方软件源：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 安装基础依赖
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg

# 2. 添加 Docker 官方 GPG 密钥 (使用 .asc 格式最稳妥，避免转换报错)
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# 3. 添加 Docker 软件源列表
echo \
  &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release &amp;#x26;&amp;#x26; echo &quot;$VERSION_CODENAME&quot;) stable&quot; | \
  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null

# 4. 更新并安装 V2 插件
sudo apt-get update
sudo apt-get install -y docker-compose-plugin

# 5. 权限补丁：将当前用户加入 docker 组
sudo usermod -aG docker $USER
# 注意：执行后需重新登录，或输入 `newgrp docker` 立即生效
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;配置 NVIDIA 软件源并安装 Toolkit&lt;/strong&gt;：
由于 Ubuntu 官方源不含此工具，必须手动添加 NVIDIA 的仓库：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 账户预检
sudo apt-get update &amp;#x26;&amp;#x26; sudo apt-get install -y curl

# 1. 导入 GPG 密钥（如果文件已存在，执行此指令会自动覆盖）
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

# 2. 添加软件源列表
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
  sed &apos;s#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g&apos; | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

# 3. 更新并安装
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

# 4. 重启 Docker 生效
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;三、 获取源码：克隆仓库&lt;/h2&gt;
&lt;h3&gt;3.1 选择访问方式&lt;/h3&gt;
&lt;p&gt;本项目仓库位于 &lt;code&gt;gitfub.cv&lt;/code&gt;，支持以下两种访问方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;生产环境（推荐）&lt;/strong&gt;：由于本仓库已设置为&lt;strong&gt;公开&lt;/strong&gt;，在内地环境可&lt;strong&gt;直接裸连&lt;/strong&gt; HTTPS 地址进行克隆，无需登录，也无需配置 SSH Key。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开发环境&lt;/strong&gt;：如果你需要向仓库提交代码，建议配置 SSH Key 以获得推送权限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 生产环境：快速克隆（HTTPS）&lt;/h3&gt;
&lt;p&gt;直接执行以下指令进行全量同步：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~
# 1. 克隆主架构 (仓库公开，内地直接访问)
git clone https://gitfub.cv/iodinot/AADCL_UAV_UGV.git
cd AADCL_UAV_UGV

# 2. 关键：拉取所有依赖的算法源码和硬件驱动
git submodule update --init --recursive

# 3. 补丁：修正 livox_ros_driver2 的 package.xml
[ -f src/drivers/livox_ros_driver2/package_ROS2.xml ] &amp;#x26;&amp;#x26; cp src/drivers/livox_ros_driver2/package_ROS2.xml src/drivers/livox_ros_driver2/package.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是生产环境，下一步可直接跳到 &lt;a href=&quot;#%E4%BA%94-%E5%AE%B9%E5%99%A8%E9%85%8D%E7%BD%AE%E7%BC%96%E6%8E%92%E4%B8%8E%E5%90%AF%E5%8A%A8&quot;&gt;五、 容器配置、编排与启动&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;3.3 开发环境：SSH 配置（仅需提交代码时执行）&lt;/h3&gt;
&lt;p&gt;如果你需要提交代码，请按以下步骤配置 Gitea 访问权限：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;生成密钥对&lt;/strong&gt;：&lt;code&gt;ssh-keygen -t ed25519 -C &quot;your_email&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;添加公钥&lt;/strong&gt;：复制 &lt;code&gt;~/.ssh/id_ed25519.pub&lt;/code&gt; 内容，前往 &lt;a href=&quot;https://gitfub.cv/user/settings/keys&quot;&gt;Gitea SSH 设置&lt;/a&gt; 添加。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;克隆地址&lt;/strong&gt;：使用 &lt;code&gt;git@gitfub.cv:iodinot/AADCL_UAV_UGV.git&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;四、 AADCL_UAV_UGV 目录结构&lt;/h2&gt;
&lt;p&gt;代码按照“单一职责”原则划分为五个顶级目录：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;~/AADCL_UAV_UGV
├── .env                # 机器硬件配置 (单一事实来源)
├── config/             # 算法逻辑配置 (*.yaml)
├── src/
│   ├── main/           # 系统编排层 (TF/Launch)
│   ├── drivers/        # L1 硬件驱动层
│   ├── core/           # L2 核心感知层
│   ├── apps/           # L3 业务应用层
│   └── sim/            # 仿真工具层
├── compose.yaml        # 多容器编排声明
└── README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此目录结构采用&lt;strong&gt;Scheme C&lt;/strong&gt;的架构标准。对于开发环境，此目录包含必要的也就是可能进行二次开发的代码的源码；对于生产环境，此目录包含必要的配置文件。&lt;/p&gt;
&lt;h2&gt;五、 容器配置、编排与启动&lt;/h2&gt;
&lt;p&gt;先进入刚刚 pull 下来的代码目录（其目录结构见&lt;a href=&quot;#%E5%9B%9B-aadcl_uav_ugv-%E7%9B%AE%E5%BD%95%E7%BB%93%E6%9E%84&quot;&gt;四、 AADCL_UAV_UGV 目录结构&lt;/a&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cd ~/AADCL_UAV_UGV
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时并不能直接起 docker 容器，还需要编辑配置文件，我们采用 &lt;strong&gt;SSOT (单一事实来源)&lt;/strong&gt; 原则，通过 &lt;code&gt;.env&lt;/code&gt; 文件来管理机器之间的差异。&lt;/p&gt;
&lt;h3&gt;5.1 设备配置文件 (.env)&lt;/h3&gt;
&lt;p&gt;仓库里&lt;strong&gt;已经包含&lt;/strong&gt;了一个 &lt;a href=&quot;/AADCL_UAV_UGV/.env.example&quot;&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/a&gt; 模板。它规定了这台车需要哪些硬件参数。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;复制模板&lt;/strong&gt;：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;cp .env.example .env
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数配置&lt;/strong&gt;：编辑 &lt;code&gt;.env&lt;/code&gt;。由于无人机的外设配置例如飞控串口路径（比如 MAVLink 用的 &lt;code&gt;/dev/ttyTHS1&lt;/code&gt;）和系统 ID 不确定，需要结合实际情况进行修改。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;5.2 离线数据集安装 (MAVROS 必备)&lt;/h3&gt;
&lt;p&gt;MAVROS 在启动时需要特殊的地理坐标数据集（Geoids）。如果在启动日志里看到 &lt;code&gt;GeographicLib exception&lt;/code&gt; 报错，请执行以下指令进行一键安装：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir -p ~/AADCL_UAV_UGV/data/geographiclib
# 启动一个临时容器进行下载（约 100MB）
docker run --rm -v ~/AADCL_UAV_UGV/data/geographiclib:/usr/share/GeographicLib robot_stack_base:humble /opt/ros/humble/lib/mavros/install_geographiclib_datasets.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5.3 初次启动系统&lt;/h3&gt;
&lt;p&gt;本项目使用 &lt;strong&gt;Docker V2&lt;/strong&gt;（&lt;code&gt;docker compose&lt;/code&gt;）进行一键编排。&lt;/p&gt;
&lt;p&gt;对于新机器，我们提供两种部署方案：&lt;strong&gt;方案 A（本地构建）&lt;/strong&gt; 适用于需要深度定制底层依赖的场景；&lt;strong&gt;方案 B（云端拉取）&lt;/strong&gt; 适用于追求一键快速部署的情况。&lt;/p&gt;
&lt;h4&gt;方案 A：本地手动构建 (初次耗时 30-50 分钟)&lt;/h4&gt;
&lt;p&gt;这一步会从头安装 ROS2 Humble、PCL、OpenCV 等数 GB 的依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose build l1-hardware
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;方案 B：从 Gitea 容器仓库拉取&lt;/h4&gt;
&lt;p&gt;如果你已熟悉编译流程或只想进行快速部署，直接执行以下指令即可跳过漫长的编译过程（该仓库为公开，内地裸连且无需登录）：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 1. 拉取镜像
docker pull gitfub.cv/iodinot/aadcl_uav_ugv/base:latest
docker pull gitfub.cv/iodinot/aadcl_uav_ugv/l3_planner:latest
docker pull gitfub.cv/iodinot/aadcl_uav_ugv/l3_yolo:latest

# 2. 关联标签 (让 compose 能识别)
docker tag gitfub.cv/iodinot/aadcl_uav_ugv/base:latest robot_stack_base:humble
docker tag gitfub.cv/iodinot/aadcl_uav_ugv/l3_planner:latest l3_planner:humble
docker tag gitfub.cv/iodinot/aadcl_uav_ugv/l3_yolo:latest l3_yolo:humble
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;启动全量系统&lt;/h4&gt;
&lt;p&gt;镜像就绪后（无论通过 A 还是 B），执行：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动视觉模块&lt;/strong&gt;：如果你需要跑 YOLO 目标检测。
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose --profile apps up -d l3-yolo-demo
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动监控桥接&lt;/strong&gt;：为了用 Foxglove 远程看数据。
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker compose --profile viz up -d visualization-bridge
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.4 &lt;code&gt;ros2c&lt;/code&gt; 脚本的配置&lt;/h3&gt;
&lt;p&gt;Docker 的环境隔离虽然安全，但调试很不方便：每次查看节点都要用 &lt;code&gt;docker exec&lt;/code&gt; 进入容器。为此，本项目提供了一个增强版 &lt;code&gt;ros2c&lt;/code&gt; (ROS2 in Container) 脚本，支持 &lt;strong&gt;自动容器探测&lt;/strong&gt;，实现透明化的容器操作体验。&lt;/p&gt;
&lt;p&gt;请在 &lt;strong&gt;宿主机（Nano 终端）&lt;/strong&gt; 直接执行以下“一键安装”指令：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;curl -sSL https://gitfub.cv/iodinot/AADCL_UAV_UGV/raw/branch/main/scripts/install_ros2c.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其核心功能及实现方案如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;自动容器感知&lt;/strong&gt;：脚本会自动检测 &lt;code&gt;l1_hardware&lt;/code&gt;、&lt;code&gt;l2_capability&lt;/code&gt; 或 &lt;code&gt;l3&lt;/code&gt; 系列正在运行的容器，并将其作为当前执行目标，人员无需关心具体容器 ID。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;环境全自动注入&lt;/strong&gt;：每次执行均会自动在容器内 source ROS2 Humble 原生路径及项目自身的 &lt;code&gt;install/setup.bash&lt;/code&gt;，彻底解决“找不到消息类型”或“找不到节点”的问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TTY 鲁棒性&lt;/strong&gt;：智能判断当前终端是否为交互式，自动处理 &lt;code&gt;-it&lt;/code&gt; 参数，使其无缝支持 SSH 脚本控制、自动流水线调用及手动调试。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;极简指令入口&lt;/strong&gt;：将复杂的 &lt;code&gt;docker exec -it ... bash -lc \&quot;...\&quot;&lt;/code&gt; 包装为极简的 &lt;code&gt;ros2c [command]&lt;/code&gt;，使开发者在操作容器时获得与原生 ROS 环境一致的操作体验。&lt;/li&gt;
&lt;/ol&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tab 补全&lt;/strong&gt;：你可以输入 &lt;code&gt;ros2c -c [Tab][Tab]&lt;/code&gt;，系统会自动联想出当前正在运行的容器名称。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;指定容器&lt;/strong&gt;：如果你想针对特定容器执行指令，可以使用 &lt;code&gt;-c&lt;/code&gt; 参数，例如：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2c -c l3_yolo_demo node list
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;默认逻辑&lt;/strong&gt;：若不带 &lt;code&gt;-c&lt;/code&gt;，脚本会按 &lt;code&gt;l1 -&gt; l2 -&gt; l3&lt;/code&gt; 的优先级自动寻找第一个活跃容器执行。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常用指令&lt;/strong&gt;：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;ros2c node list           # 列出所有节点
ros2c topic list          # 列出所有话题
ros2c                     # 不带参数：直接进入默认容器的 Shell
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;六、 集群部署&lt;/h2&gt;
&lt;p&gt;当你有了一台完整配置好的机型（如 &lt;code&gt;nrc17&lt;/code&gt;），为同型号的新机器（如 &lt;code&gt;nrc18&lt;/code&gt;）配置环境时，无需再重复耗时的编译过程。&lt;/p&gt;
&lt;h3&gt;6.1 方案 A：通过容器注册表同步 (推荐)&lt;/h3&gt;
&lt;p&gt;这是最标准且高效的方法。只需在新机器上重复 &lt;a href=&quot;#%E6%96%B9%E6%A1%88-b%E4%BB%8E-gitea-%E5%AE%B9%E5%99%A8%E4%BB%93%E5%BA%93%E6%8B%89%E5%8F%96&quot;&gt;5.3 节中的方案 B&lt;/a&gt; 即可：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;拉取镜像&lt;/strong&gt;：&lt;code&gt;docker pull gitfub.cv/iodinot/aadcl_uav_ugv/base:latest&lt;/code&gt; ...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;打标签&lt;/strong&gt;：&lt;code&gt;docker tag ... robot_stack_base:humble&lt;/code&gt; ...&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;启动&lt;/strong&gt;：&lt;code&gt;docker compose up -d&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于 Gitea 仓库是公开的且内地直连极速，这种方式比拷贝文件更可靠。&lt;/p&gt;
&lt;h3&gt;6.2 方案 B：离线镜像迁移 (离线/无网络场景)&lt;/h3&gt;
&lt;p&gt;如果在无法连接注册表的情况下，可使用 Docker 的导出功能进行物理迁移：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;在“母机”导出&lt;/strong&gt;：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker save robot_stack_base:humble | gzip &gt; ~/AADCL_UAV_UGV_base.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拷贝至新机&lt;/strong&gt;：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;scp ~/AADCL_UAV_UGV_base.tar.gz nvidia@nrc18.local:~
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在新机导入&lt;/strong&gt;：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker load &amp;#x3C; ~/AADCL_UAV_UGV_base.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;七、 系统功能验证&lt;/h2&gt;
&lt;p&gt;程序跑起来后，需确认各项模块是否正常运行。以下提供三项关键验证指标：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;节点全不全？&lt;/strong&gt;：执行 &lt;code&gt;ros2c node list&lt;/code&gt;，你应该能看到 &lt;code&gt;mavros&lt;/code&gt;、&lt;code&gt;fast_lio&lt;/code&gt; 和 &lt;code&gt;ego_planner&lt;/code&gt; 等一众核心节点都在线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定位有输出吗？&lt;/strong&gt;：执行 &lt;code&gt;ros2 topic echo /Odometry&lt;/code&gt;。如果屏幕上数据在跳，说明算法已经闭环并在解算定位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;视觉有反应吗？&lt;/strong&gt;：如果启动了 YOLO，执行 &lt;code&gt;ros2 topic echo /yolo/detections&lt;/code&gt;。如果有人或物体出现在镜头里，你会看到识别结果。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;附录：硬件网口配置（优化高频点云传输稳定性）&lt;/h3&gt;
&lt;p&gt;Livox MID360 雷达的数据量较大，若直接在默认网口传输，高频点云包可能会干扰 WiFi 或 SSH 链路的稳定性。建议将雷达绑定至独立的物理网口：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;看看雷达插在哪个口了&lt;/strong&gt;：
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;nmcli device  # 找一下那个显示“以太网”连接的口，比如叫 enP8p1s0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;给这个口设个固定 IP&lt;/strong&gt;：
注意：雷达不需要网关，我们只要在这个口上配置 &lt;code&gt;192.168.10.x&lt;/code&gt; 网段就行。
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 假设网口名是 enP8p1s0，给 nano 分配 192.168.10.50
sudo nmcli connection add type ethernet con-name lidar-link ifname enP8p1s0 ipv4.method manual ipv4.addresses 192.168.10.50/24
sudo nmcli connection up lidar-link
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;查一下生效没&lt;/strong&gt;：执行 &lt;code&gt;ip addr show enP8p1s0&lt;/code&gt;，看到 &lt;code&gt;192.168.10.50&lt;/code&gt; 就说明搞定了。&lt;/li&gt;
&lt;/ol&gt;</content:encoded><h:img src="/_astro/thumbnail.GAU7U3Mg.png"/><enclosure url="/_astro/thumbnail.GAU7U3Mg.png"/></item><item><title>CSS Flex 布局</title><link>https://awfufu.com/blog/css-flex-layout</link><guid isPermaLink="true">https://awfufu.com/blog/css-flex-layout</guid><description>CSS 中最强大和最常用的布局方式之一</description><pubDate>Mon, 24 Nov 2025 20:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;flex 布局&lt;/h2&gt;
&lt;h3&gt;块级元素和行内元素&lt;/h3&gt;
&lt;p&gt;块级元素：独占一行（前后会自动换行），自动填满父容器的宽度。它们的 &lt;code&gt;display&lt;/code&gt; 属性为 &lt;code&gt;block&lt;/code&gt;。
行内元素：与其他行内元素排在一行内，宽高由内容决定。它们的 &lt;code&gt;display&lt;/code&gt; 属性为 &lt;code&gt;inline&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;大部分 html 元素都是块级元素或行内元素。&lt;/p&gt;
&lt;p&gt;下面是一个例子，一个 container 中装了 3 个 item。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;container&quot;&gt;
  &amp;#x3C;div class=&quot;item&quot;&gt;1&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;item&quot;&gt;2&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;item&quot;&gt;3&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 item 都是块级元素，它们都独占一行，自上而下排列。&lt;/p&gt;
&lt;p&gt;为了方便展示，示例中的 container 容器使用 红色边框，其中的 item 项目使用 蓝色方块。&lt;/p&gt;
&lt;h3&gt;flex 容器和 flex 元素&lt;/h3&gt;
&lt;p&gt;在容器元素的 CSS 中添加 &lt;code&gt;display: flex&lt;/code&gt;，它就会变为 flex 容器。&lt;/p&gt;
&lt;p&gt;flex 容器的直接子项目都是 flex 元素。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到，flex 容器中的 item 项目会自动排列在一行内。&lt;/p&gt;
&lt;h3&gt;flex 容器的主轴&lt;/h3&gt;
&lt;p&gt;flex 布局的&lt;strong&gt;主轴&lt;/strong&gt;默认为水平方向，主轴方向由 &lt;code&gt;flex-direction&lt;/code&gt; 属性控制，默认为 &lt;code&gt;row&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  flex-direction: row;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果将主轴方向 &lt;code&gt;flex-direction&lt;/code&gt; 设为 &lt;code&gt;row-reverse&lt;/code&gt;（翻转），那么元素则会反方向排列。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  flex-direction: row-reverse;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;沿轴线方向对齐&lt;/h2&gt;
&lt;h3&gt;沿主轴对齐&lt;/h3&gt;
&lt;p&gt;flex 容器内的元素会沿着主轴方向排列，在 flex 容器上使用 &lt;code&gt;justify-content&lt;/code&gt; 属性可以控制容器内元素在&lt;strong&gt;主轴&lt;/strong&gt;上的位置。&lt;code&gt;justify-content&lt;/code&gt; 有以下几个常用的取值。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;start&lt;/code&gt;：对齐主轴起点，这是默认行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  justify-content: start;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;end&lt;/code&gt;：对齐主轴终点。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  justify-content: end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;code&gt;center&lt;/code&gt;：在主轴上居中。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  justify-content: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;&lt;code&gt;space-between&lt;/code&gt;：均匀分配每个 item 的间隙（item 之间）。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  justify-content: space-between;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两侧不留间隙，只在 item 之间均匀分配。&lt;/p&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;&lt;code&gt;space-around&lt;/code&gt;：均匀分配每个 item 两侧的空间（周围）。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  justify-content: space-around;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;item 之间的间隙是侧边的两倍。&lt;/p&gt;
&lt;ol start=&quot;6&quot;&gt;
&lt;li&gt;&lt;code&gt;space-evenly&lt;/code&gt;：均匀分配每个 item 的间隙（包括侧边的）。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  justify-content: space-evenly;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两侧留出间隙，所有间隙都均匀分配。&lt;/p&gt;
&lt;ol start=&quot;7&quot;&gt;
&lt;li&gt;&lt;code&gt;gap&lt;/code&gt;：手动设置间隙&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;手动指定 flex 元素之间的间隙。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  gap: 20px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;沿交叉轴对齐&lt;/h3&gt;
&lt;p&gt;交叉轴垂直于主轴，使用 &lt;code&gt;align-items&lt;/code&gt; 属性可以调整元素在交叉轴上的对齐方式。&lt;/p&gt;
&lt;p&gt;为了方便展示纵向对齐，这里增加了 容器边框 的高度。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对齐交叉轴起点&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;align-items&lt;/code&gt; 的默认值也是 &lt;code&gt;start&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  align-items: start;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;对齐交叉轴终点&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  align-items: end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;居中对齐&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同时使用 &lt;code&gt;justify-content: center&lt;/code&gt; 和 &lt;code&gt;align-items: center&lt;/code&gt;，可以轻松让元素水平垂直居中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  justify-content: center;
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;旋转主轴方向&lt;/h3&gt;
&lt;p&gt;设置 &lt;code&gt;flex-direction&lt;/code&gt; 为 &lt;code&gt;column&lt;/code&gt;，可以将主轴方向设为纵向，交叉轴方向同时变为横向。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  flex-direction: column;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于现在的交叉轴是横向的，所以此时要让元素水平居中，应该调整 &lt;code&gt;align-content&lt;/code&gt; 而不是 &lt;code&gt;justify-content&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  flex-direction: column;
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 &lt;code&gt;justify-content&lt;/code&gt; 现在调整的是纵向的主轴，即垂直方向的对齐方式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多行布局&lt;/h2&gt;
&lt;h3&gt;flex 自动换行&lt;/h3&gt;
&lt;p&gt;给 flex 容器使用 &lt;code&gt;flex-wrap&lt;/code&gt; 属性，可以控制 flex 元素是否自动换行。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flex-wrap&lt;/code&gt; 的默认值为 &lt;code&gt;nowrap&lt;/code&gt;，当容器空间不足（比如手机屏幕，横向空间较小）时，会压扁（收缩）其中的元素。如果元素无法被压缩，会直接溢出容器。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  gap: 40px;
  flex-wrap: nowrap;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你正在使用电脑浏览这个页面，你可以试试拖动浏览器窗口右侧，改变页面宽度，观察下面的元素会发生什么。&lt;/p&gt;
&lt;p&gt;将 &lt;code&gt;flex-wrap&lt;/code&gt; 设为 &lt;code&gt;wrap&lt;/code&gt;，元素会自动在空间不足时换行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  flex-wrap: wrap;
  gap: 40px;
  max-width: 400px; /* 限制宽度方便展示换行 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多行布局下的对齐方式&lt;/h3&gt;
&lt;p&gt;就像主轴上有 &lt;code&gt;justify-content&lt;/code&gt; 属性一样，交叉轴上也有 &lt;code&gt;align-content&lt;/code&gt;属性，它可以控制交叉轴上整体的对齐方式。&lt;/p&gt;
&lt;p&gt;现在有以下示例，在一个 flex 容器中装了 9 个元素，让它们自动换行，现在不使用 &lt;code&gt;gap&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  display: flex;
  flex-wrap: wrap;
  max-width: 320px;
  height:450px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;align-content&lt;/code&gt; 默认为 &lt;code&gt;stretch&lt;/code&gt;，所以可以发现即使不使用 &lt;code&gt;gap&lt;/code&gt;，元素在垂直方向上依然有间隙。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;align-content&lt;/code&gt; 同样也有 &lt;code&gt;start&lt;/code&gt;、&lt;code&gt;end&lt;/code&gt; 和 &lt;code&gt;center&lt;/code&gt; 取值，让元素整体在交叉轴上对齐。比如，下面使用 &lt;code&gt;align-content: center&lt;/code&gt; 让元素在交叉轴上居中对齐。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  /* ... */
  align-content: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面说过，&lt;code&gt;align-items&lt;/code&gt; 也是用来调整交叉轴上的对齐方式的，那么它和 &lt;code&gt;align-content&lt;/code&gt; 的区别是什么呢？&lt;/p&gt;
&lt;p&gt;尝试用 &lt;code&gt;align-items&lt;/code&gt; 替代 &lt;code&gt;align-content&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  /* ... */
  align-items: center;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  /* ... */
  align-items: end;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  /* ... */
  align-items: start;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结合上面三个例子不难看出，&lt;code&gt;align-items&lt;/code&gt; 控制的 &lt;code&gt;start&lt;/code&gt;、&lt;code&gt;end&lt;/code&gt; 和 &lt;code&gt;center&lt;/code&gt; 让元素对齐了它们各自的交叉轴。&lt;/p&gt;
&lt;p&gt;其实在 &lt;code&gt;wrap&lt;/code&gt; 自动换行后，每一行（如果设置了纵向主轴则是列）元素都有它们自己独立的主轴和交叉轴。此时 &lt;code&gt;align-items&lt;/code&gt; 调整的是各行元素在自身的交叉轴上的对齐方式，而 &lt;code&gt;align-content&lt;/code&gt; 调整的是多行元素整体在交叉轴上的对齐方式。&lt;/p&gt;
&lt;p&gt;所以，如果要将元素完全居中对齐，我们应该在 flex 容器上设置以下三个 &lt;code&gt;center&lt;/code&gt; 值：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.container {
  /* ... */
  justify-content: center;
  align-items: center;
  align-content: center;

  /* 这里适当添加 gap，看起来更美观 */
  gap: 10px;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;拉伸和收缩元素&lt;/h2&gt;
&lt;h3&gt;自动拉伸&lt;/h3&gt;
&lt;p&gt;如果想要让元素自动拉伸以填满容器的剩余空间，可以使用 &lt;code&gt;flex-grow&lt;/code&gt; 属性。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;flex-grow&lt;/code&gt; 的默认值为 &lt;code&gt;0&lt;/code&gt;，即禁用自动拉伸。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-grow: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当 &lt;code&gt;flex-grow&lt;/code&gt; 设为 &lt;code&gt;1&lt;/code&gt; 时，元素将会自动拉伸以填满容器空间。注意 &lt;code&gt;flex-grow&lt;/code&gt; 属性是加在 item 元素身上的，而不是 flex 容器上。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-grow: 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以指定拉伸其中某个元素，其他不拉伸，比如只在第二个元素上添加 &lt;code&gt;flex-grow: 1&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-grow: 0;
}

#item-2 {
  flex-grow: 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自动收缩&lt;/h3&gt;
&lt;p&gt;flex 容器中的元素在空间不足时会自动收缩，自动收缩由 &lt;code&gt;flex-shrink&lt;/code&gt; 属性控制，其默认值为 &lt;code&gt;1&lt;/code&gt;，即：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-shrink: 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 &lt;code&gt;flex-shrink&lt;/code&gt; 属性是加在元素身上的，而不是 flex 容器上。&lt;/p&gt;
&lt;p&gt;如果要禁用自动压缩，可以设置 &lt;code&gt;flex-shrink: 0&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-css&quot;&gt;.item {
  flex-shrink: 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;按比例拉伸或收缩&lt;/h3&gt;
&lt;p&gt;在同时存在多个 item 时，&lt;code&gt;flex-grow&lt;/code&gt; 和 &lt;code&gt;flex-shrink&lt;/code&gt; 的数值是有意义的，通过给它们设置不同的值，可以实现按比例拉伸或收缩。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;container&quot;&gt;
  &amp;#x3C;div class=&quot;item&quot; style=&quot;flex-grow:1;&quot;&gt;1&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;item&quot; style=&quot;flex-grow:3;&quot;&gt;2&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;item&quot; style=&quot;flex-grow:5;&quot;&gt;3&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样每个元素被拉伸的空间比例为 1:3:5。注意：这并不是说元素占用的空间是 1:3:5，而是它们被拉伸的空间是按这个比例分配的。&lt;/p&gt;
&lt;p&gt;按比例收缩同理，数值越大被压缩的越多。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;#x3C;div class=&quot;container&quot;&gt;
  &amp;#x3C;div class=&quot;item&quot; style=&quot;flex-shrink:1;&quot;&gt;1&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;item&quot; style=&quot;flex-shrink:3;&quot;&gt;2&amp;#x3C;/div&gt;
  &amp;#x3C;div class=&quot;item&quot; style=&quot;flex-shrink:5;&quot;&gt;3&amp;#x3C;/div&gt;
&amp;#x3C;/div&gt;
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.Cl_YpZvn.webp"/><enclosure url="/_astro/thumbnail.Cl_YpZvn.webp"/></item><item><title>C指针和结构体</title><link>https://awfufu.com/blog/c-pointers-and-structs</link><guid isPermaLink="true">https://awfufu.com/blog/c-pointers-and-structs</guid><description>指针是C语言的精髓</description><pubDate>Sat, 11 Jun 2022 21:01:00 GMT</pubDate><content:encoded>&lt;p&gt;:::tip
阅读本文之前，读者应该拥有至少一门其他的编程语言基础，了解少许的 Linux 命令。
:::&lt;/p&gt;
&lt;p&gt;import { Card } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;C语言快速入门&lt;/p&gt;
&lt;h2&gt;指针类型&lt;/h2&gt;
&lt;h3&gt;什么是指针&lt;/h3&gt;
&lt;p&gt;指针是 C 语言的精髓，指针也是一种变量，可以存储变量在内存中的地址。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;#x26;&lt;/code&gt;: 取地址运算符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;: 寻址运算符（解引用）&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  int var = 20;
  int* p = &amp;#x26;var; // * 表示这是一个 int 类型的指针，将 var 的地址赋值给 p

  printf(&quot;&amp;#x26;var: %p\n&quot;, &amp;#x26;var); // %p 打印地址
  // &amp;#x26;var: 0x7ffe56d638dc
  printf(&quot;p: %p\n&quot;, p);
  // p: 0x7ffe56d638dc
  printf(&quot;*p: %d\n&quot;, *p);
  // *p: 20

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的指针变量 &lt;code&gt;p&lt;/code&gt; 存储了 &lt;code&gt;var&lt;/code&gt; 变量所在的内存块的地址。使用 &lt;code&gt;*p&lt;/code&gt; 可以从这个地址访问 &lt;code&gt;var&lt;/code&gt;，并获取 &lt;code&gt;var&lt;/code&gt; 的值。&lt;/p&gt;
&lt;p&gt;import PointerDiagram from &apos;./PointerDiagram.astro&apos;;&lt;/p&gt;
&lt;p&gt;通过指针，我们可以直接访问和操作内存，这使得 C 语言非常强大且高效，但也需要小心使用以避免错误（如空指针、野指针）。&lt;/p&gt;
&lt;h3&gt;空指针和野指针&lt;/h3&gt;
&lt;p&gt;字面意思，空指针就是指向空地址（地址为零）的指针；野指针就是指向无意义地址或非法地址的指针。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;stddef.h&lt;/code&gt; 头文件中，定义了 &lt;code&gt;NULL&lt;/code&gt;，它可以用来表示空指针。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#define NULL ((void *)0)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int* p1 = NULL; // 空指针：指向0
int* p2;        // 野指针：未初始化，内容随机
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种指针要小心使用，一旦对它们进行解引用操作，就会导致未定义行为。比如访问 &lt;code&gt;*p1&lt;/code&gt; 时，程序会尝试访问零地址处的内存，这通常会导致程序崩溃。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  int* p = NULL;
  printf(&quot;%d\n&quot;, *p); // segmentation fault // [!code error]
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在访问指针之前，必须小心谨慎，确保指针被正确地初始化了且不为空。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  int a = 10;
  int* p = &amp;#x26;a;

  if (!p) { // [!code ++]
    printf(&quot;p is null!\n&quot;); // [!code ++]
    return 1; // [!code ++]
  } // [!code ++]

  printf(&quot;%d\n&quot;, *p);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;指针的大小&lt;/h3&gt;
&lt;p&gt;指针本身也是个变量，所以它也需要占用内存空间来存储。在 32 位（bit）系统上，指针的大小是 4 字节，在 64 位系统上则是 8 字节。这里的“位”指的是地址总线的位数，现代机器通常是 64 位。&lt;/p&gt;
&lt;p&gt;在 64 位机器上，可以给 &lt;code&gt;gcc&lt;/code&gt; 命令添加 &lt;code&gt;-m32&lt;/code&gt; 参数来生成 32 位的可执行文件进行测试。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  int* p = NULL;
  printf(&quot;sizeof(p): %zu\n&quot;, sizeof(p));
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 32 位系统上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gcc main.c -m32 -o main
./main                 
# sizeof(p): 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 64 位系统上：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sh&quot;&gt;gcc main.c -m64 -o main
./main                  
# sizeof(p): 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;指针的作用&lt;/h3&gt;
&lt;p&gt;在函数内部定义的变量称为局部变量。它们通常存储在栈（Stack）上，其生命周期仅限于函数执行期间。当函数执行完后，这些变量会被销毁，内存会被回收。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

void foo(int x) {
  x = 100; // 修改数值 // [!code hl]
  printf(&quot;foo: x = %d\n&quot;, x);
}

int main() {
  int x = 10;
  foo(x);
  printf(&quot;main: x = %d\n&quot;, x);
  return 0;
}

// Output:
// foo: x = 100
// main: x = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 C 语言是按值传递（Pass by Value）的，直接传递变量只会拷贝一份副本。如果想在函数内部修改函数外部定义的变量，则需要传递变量的地址（指针），这样函数就可以通过地址直接操作原始数据。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

// 交换两个整数的值 使用指针
void swap(int* a, int* b) {
  int temp = *a;
  *a = *b;
  *b = temp;
}

int main() {
  int x = 5, y = 10;

  printf(&quot;Before: x = %d, y = %d\n&quot;, x, y);
  swap(&amp;#x26;x, &amp;#x26;y); // 传递地址 // [!code hl]
  printf(&quot;After:  x = %d, y = %d\n&quot;, x, y);

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组和指针&lt;/h2&gt;
&lt;h3&gt;数组名即指针&lt;/h3&gt;
&lt;p&gt;数组名在大多数表达式中会“退化”为指向数组第一个元素的指针。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int arr[5] = {10, 20, 30, 40, 50};
int* p = arr; // = &amp;#x26;arr[0]

printf(&quot;p:       %p\n&quot;, p);
printf(&quot;arr:     %p\n&quot;, arr);
printf(&quot;&amp;#x26;arr[0]: %p\n&quot;, &amp;#x26;arr[0]);

// Output:
// p:       0x7ffe56d638dc
// arr:     0x7ffe56d638dc
// &amp;#x26;arr[0]: 0x7ffe56d638dc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的示例中，&lt;code&gt;p&lt;/code&gt; 的值等于 &lt;code&gt;arr&lt;/code&gt; 的值等于 &lt;code&gt;&amp;#x26;arr[0]&lt;/code&gt; 的值。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;printf(&quot;arr[0] = %d\n&quot;, arr[0]);
printf(&quot;*p     = %d\n&quot;, *p);

// Output:
// arr[0] = 10
// *p     = 10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;arr[0]&lt;/code&gt; 和 &lt;code&gt;*p&lt;/code&gt; 都可以访问数组的第一个元素。&lt;/p&gt;
&lt;h3&gt;指针的运算&lt;/h3&gt;
&lt;p&gt;指针可以进行加减运算，加法运算会将指针移动到下一个元素的位置，减法运算会将指针移动到前一个元素的位置。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;printf(&quot;arr[1]   = %d\n&quot;, arr[1]);
printf(&quot;*(p + 1) = %d\n&quot;, *(p + 1));
printf(&quot;p[1]     = %d\n&quot;, p[1]);

// Output:
// arr[1]   = 20
// *(p + 1) = 20
// p[1]     = 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对 int 类型的指针 &lt;code&gt;p&lt;/code&gt; 执行 &lt;code&gt;p + 1&lt;/code&gt; 会将指针移动到下一个 int 的位置。指针 &lt;code&gt;p&lt;/code&gt; 也可以当作一个 int 数组进行下标访问，这里的 &lt;code&gt;p[1]&lt;/code&gt; 等价于 &lt;code&gt;*(p + 1)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你可以尝试不同类型的指针，比如 &lt;code&gt;char*&lt;/code&gt;，你会发现指针移动的步长是 1。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char arr[] = {&apos;a&apos;, &apos;b&apos;, &apos;c&apos;, &apos;d&apos;, &apos;e&apos;};
char* p = arr;

printf(&quot;p: %p\n&quot;, p);
printf(&quot;p + 1: %p\n&quot;, p + 1);
printf(&quot;p - 1: %p\n&quot;, p - 1);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;字符串&lt;/h3&gt;
&lt;p&gt;之前说过，字符串实际上就是字符数组，并且以空字符 &lt;code&gt;\0&lt;/code&gt; 结尾。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char str[] = &quot;hello&quot;; 
char* p = str;

printf(&quot;str: %s, p: %s\n&quot;, str, p);
// str: hello, p: hello

p[0] = &apos;H&apos;;
printf(&quot;str: %s, p: %s\n&quot;, str, p);
// str: Hello, p: Hello
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;操作字符串&lt;/h3&gt;
&lt;p&gt;C 标准库提供了一些用于操作字符串的函数，这些函数都定义在 &lt;code&gt;string.h&lt;/code&gt; 头文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;string.h&gt;

int main() {
  char str1[10] = &quot;hello&quot;;
  char str2[10];

  printf(&quot;strlen(str1): %zu\n&quot;, strlen(str1));
  strcpy(str2, str1);
  printf(&quot;str2: %s\n&quot;, str2);

  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用指针，我们可以高效地实现 &lt;code&gt;strcpy&lt;/code&gt; 函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char* strcpy(char* dest, const char* src) {
  char* p = dest;
  while (*dest++ = *src++);
  return p;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以通过 &lt;a href=&quot;https://en.cppreference.com/w/c/header/string.html&quot;&gt;string.h 文档&lt;/a&gt; 学习更多接口。&lt;/p&gt;
&lt;h2&gt;函数指针&lt;/h2&gt;
&lt;h3&gt;函数名即指针&lt;/h3&gt;
&lt;p&gt;就像数组名是数组第一个元素地址的指针一样，函数名也是指向函数地址的指针。我们可以定义一个指针变量来存储函数的地址，这就是函数指针。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

void say_hello() {
  printf(&quot;Hello World!\n&quot;);
}

int add(int a, int b) {
  return a + b;
}

int main() {
  // 定义指向 say_hello 的指针
  void (*func_ptr)(); // [!code hl]
  func_ptr = say_hello;
  
  // 通过指针调用函数
  func_ptr(); // 或 (*func_ptr)(); // [!code hl]

  // 定义指向 add 的指针
  int (*add_ptr)(int, int) = add;
  printf(&quot;2 + 3 = %d\n&quot;, add_ptr(2, 3));
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;回调函数&lt;/h3&gt;
&lt;p&gt;不像其他高级语言，C 语言没有类似 lambda 表达式或闭包的语法糖，但是我们可以通过函数指针来实现类似的功能。&lt;/p&gt;
&lt;p&gt;函数指针的一个主要用途是实现回调函数（Callback Function）。回调函数是指将一个函数指针作为参数传递给另一个函数，由另一个函数在适当的时候调用这个函数。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

// 接受函数指针作为参数
void compute(int (*op)(int, int), int a, int b) {
  int result = op(a, b);
  printf(&quot;Result: %d\n&quot;, result);
}

int main() {
  compute(add, 10, 5); // Result: 15
  compute(sub, 10, 5); // Result: 5
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C 语言标准库中的 &lt;code&gt;qsort&lt;/code&gt; 就是一个典型的例子，它允许用户自定义比较逻辑。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;stdlib.h&gt;

int compare(const void* a, const void* b) {
  return (*(int*)a - *(int*)b);
}

int main() {
  int arr[] = {5, 2, 9, 1, 5, 6};
  int size = sizeof(arr) / sizeof(arr[0]);

  qsort(arr, size, sizeof(int), compare); // [!code hl]

  for (int i = 0; i &amp;#x3C; size; i++) {
    printf(&quot;%d &quot;, arr[i]);
  }
  printf(&quot;\n&quot;);
  
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结构体&lt;/h2&gt;
&lt;h3&gt;结构体类型&lt;/h3&gt;
&lt;p&gt;结构体（Structure）是 C 语言中一种用户自定义的数据类型，允许将不同类型的数据组合成一个单一的类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;string.h&gt;

// 定义一个结构体类型
struct Person {
  char   name[50];
  int    age;
  double height;
};

int main() {
  // 声明一个结构体变量
  struct Person person;

  // 访问和修改成员并赋值
  strcpy(person.name, &quot;Alice&quot;); // 注意：字符串赋值不能直接使用 =，需要使用 strcpy
  person.age = 20;
  person.height = 1.65;

  printf(&quot;Name: %s, Age: %d, Height: %.2f\n&quot;, person.name, person.age, person.height);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结构体指针&lt;/h3&gt;
&lt;p&gt;当我们将结构体的地址传递给指针时，如果我们想通过指针访问结构体的成员，可以使用 &lt;code&gt;-&gt;&lt;/code&gt; 运算符。&lt;/p&gt;
&lt;p&gt;通常情况下，解引用指针并访问成员的语法是 &lt;code&gt;(*p).member&lt;/code&gt;，但这写起来比较麻烦（因为 &lt;code&gt;.&lt;/code&gt; 的优先级高于 &lt;code&gt;*&lt;/code&gt;，必须加括号）。C 语言提供了一个简写方式：&lt;code&gt;p-&gt;member&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;
#include &amp;#x3C;string.h&gt;

struct Person {
  char   name[50];
  int    age;
};

int main() {
  struct Person person;
  struct Person* p = &amp;#x26;person;

  // 使用指针访问成员
  strcpy(p-&gt;name, &quot;Bob&quot;); // 等价于 (*p).name
  p-&gt;age = 30;            // 等价于 (*p).age

  printf(&quot;Name: %s, Age: %d\n&quot;, p-&gt;name, p-&gt;age);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结构体的大小&lt;/h3&gt;
&lt;p&gt;结构体的大小并不总是其所有成员大小之和，因为编译器为了提高 CPU 访问内存的效率，会对结构体成员进行“内存对齐”（Memory Alignment）。&lt;/p&gt;
&lt;p&gt;对齐规则：每个成员的偏移量（Offset）必须是该成员大小的整数倍（或者是编译器默认对齐数的较小值）。结构体的总大小必须是其内部最大成员（基本类型）大小的整数倍。如果不足，会在末尾填充字节。&lt;/p&gt;
&lt;p&gt;让我们详细分析一下 &lt;code&gt;struct Pack&lt;/code&gt; 的内存布局：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;struct Pack {
  char c;   // 1 byte
  int i;    // 4 bytes
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;成员 &lt;code&gt;c&lt;/code&gt; (char) 占用 1 字节。放置在偏移量 0 处。&lt;/li&gt;
&lt;li&gt;成员 &lt;code&gt;i&lt;/code&gt; (int) 占用 4 字节。根据对齐规则，它的起始地址必须是 4 的倍数。当前的偏移量是 1（&lt;code&gt;c&lt;/code&gt; 之后），不是 4 的倍数。&lt;/li&gt;
&lt;li&gt;编译器会在 &lt;code&gt;c&lt;/code&gt; 后面插入 &lt;strong&gt;3 个字节的填充（Padding）&lt;/strong&gt;，使偏移量变为 4。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;i&lt;/code&gt; 从偏移量 4 开始存储，占用第 4-7 字节。&lt;/li&gt;
&lt;li&gt;此时结构体总大小为 8 字节。8 是最大成员大小 (4) 的整数倍，符合整体对齐规则。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最终的内存布局：&lt;/p&gt;
&lt;p&gt;import StructMemoryLayout from &apos;./StructMemoryLayout.astro&apos;;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  printf(&quot;sizeof(struct Pack) = %zu\n&quot;, sizeof(struct Pack));
  // Output: sizeof(struct Pack) = 8
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，在计算结构体大小时，务必使用 &lt;code&gt;sizeof&lt;/code&gt; 运算符，而不要简单地将成员大小相加。有时通过调整成员的定义顺序（例如将所有 &lt;code&gt;char&lt;/code&gt; 放在一起），可以减少填充字节，从而减小结构体的大小。&lt;/p&gt;
&lt;h3&gt;自定义类型&lt;/h3&gt;
&lt;p&gt;在 C 语言中，可以使用 &lt;code&gt;typedef&lt;/code&gt; 关键字来定义自定义类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

typedef int MyInt; // [!code hl]

int main() {
  MyInt a = 10;
  printf(&quot;a = %d\n&quot;, a);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，可以把一个复杂类型定义为自定义类型。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

typedef struct Point { // [!code hl]
  int x; // [!code hl]
  int y; // [!code hl]
} Point; // [!code hl]

int main() {
  Point p; // [!code hl]
  p.x = 10;
  p.y = 20;
  printf(&quot;p.x = %d, p.y = %d\n&quot;, p.x, p.y);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;</content:encoded><h:img src="/_astro/thumbnail.DxQVfE79.webp"/><enclosure url="/_astro/thumbnail.DxQVfE79.webp"/></item><item><title>C语言快速入门</title><link>https://awfufu.com/blog/c-quick-start</link><guid isPermaLink="true">https://awfufu.com/blog/c-quick-start</guid><description>快速学习C语言基本语法</description><pubDate>Fri, 10 Jun 2022 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::tip
阅读本文之前，读者应该拥有至少一门其他的编程语言基础，了解少许的 Linux 命令。
:::&lt;/p&gt;
&lt;h2&gt;开发环境&lt;/h2&gt;
&lt;p&gt;任意文本编辑器以及 C 编译器。本文选择 &lt;a href=&quot;https://gcc.gnu.org/&quot;&gt;gcc&lt;/a&gt; 在 Linux 上开发和调试。&lt;/p&gt;
&lt;h3&gt;安装 gcc&lt;/h3&gt;
&lt;p&gt;使用包管理器安装 gcc。某些发行版可能已经自带 gcc。&lt;/p&gt;
&lt;p&gt;import { Tabs, TabItem } from &apos;astro-pure/user&apos;;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Redhat / CentOS / Fedora
sudo dnf install gcc

# Arch Linux / Manjaro
sudo pacman -S gcc
```
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;文本编辑器&lt;/h3&gt;
&lt;p&gt;你可以使用任意文本编辑器，推荐 &lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;VSCode&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;Hello World&lt;/h2&gt;
&lt;p&gt;创建 &lt;code&gt;main.c&lt;/code&gt; 文件并编辑。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  printf(&quot;Hello, World!\n&quot;);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译代码，运行程序。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;gcc main.c -o main
./main
# Hello, World!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;include 预处理指令&lt;/h3&gt;
&lt;p&gt;在 C 语言中，以 &lt;code&gt;#&lt;/code&gt; 开头的行是预处理器指令。&lt;code&gt;#include &amp;#x3C;stdio.h&gt;&lt;/code&gt; 告诉编译器：在编译之前先包含标准输入输出库（Standard Input Output）的信息，这样我们就可以使用 &lt;code&gt;printf&lt;/code&gt; 等函数。&lt;/p&gt;
&lt;h3&gt;main 函数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt; 函数是程序的入口点。C 程序执行时，从这里开始。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int main() {
  // 代码逻辑
  return 0; // 程序结束，返回 0 表示成功
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基本类型&lt;/h2&gt;
&lt;h3&gt;变量和类型&lt;/h3&gt;
&lt;p&gt;变量是用于存储数据的容器，C 语言以以下的方式定义或声明变量。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int a = 10; // 定义并初始化为 10
int b;      // 声明但不初始化，仅分配一块内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常见的变量类型有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int&lt;/code&gt;: 整数，如 10, -5，现代计算机上通常为 32 位&lt;/li&gt;
&lt;li&gt;&lt;code&gt;float&lt;/code&gt;: 单精度浮点数，例如 3.14&lt;/li&gt;
&lt;li&gt;&lt;code&gt;double&lt;/code&gt;: 双精度浮点数，精度更高&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char&lt;/code&gt;: 单个字符，例如 &apos;A&apos;, &apos;z&apos;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;float&lt;/code&gt; 和 &lt;code&gt;double&lt;/code&gt; 的区别在于精度，&lt;code&gt;float&lt;/code&gt; 通常为 32 位，&lt;code&gt;double&lt;/code&gt; 通常为 64 位。其二进制存储方式遵循 &lt;a href=&quot;https://wikipedia.org/wiki/IEEE_754&quot;&gt;IEEE 754 标准&lt;/a&gt;，你可以在&lt;a href=&quot;https://lukaskollmer.de/ieee-754-visualizer/&quot;&gt;IEEE 754 Visualizer&lt;/a&gt;直观地了解其存储原理。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  int a = 10;
  double b = 3.14159;
  char c = &apos;A&apos;;

  // %d 格式化打印整数，%f 格式化打印浮点数，%c 格式化打印字符
  printf(&quot;a = %d, b = %f, c = %c\n&quot;, a, b, c);
  // a = 10, b = 3.141590, c = A
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更多基本类型信息可以参考 &lt;a href=&quot;https://en.cppreference.com/w/c/language/arithmetic_types.html&quot;&gt;C 标准&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;数组和字符串&lt;/h3&gt;
&lt;p&gt;数组是存储多个相同类型数据的容器。C 语言中没有专门的字符串类型，而是使用字符数组。数组下标从 0 开始。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  int arr[5] = {1, 2, 3, 4, 5};
  char str[] = &quot;Hello&quot;;

  printf(&quot;arr[2] = %d, str = %s\n&quot;, arr[2], str);
  // arr[2] = 3, str = Hello
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C 语言规定字符串必须以空字符 &lt;code&gt;\0&lt;/code&gt;（数值为零的字符）结尾。不像其他高级语言对字符串的封装，C 语言中的字符串只是一个 &lt;code&gt;char[]&lt;/code&gt; 数组，程序并不能直接得到数组的长度（之后会提到为什么），因此通过遍历字符直到 &lt;code&gt;\0&lt;/code&gt; 来确定字符串在哪里结束。&lt;/p&gt;
&lt;p&gt;在定义 &lt;code&gt;char str[] = &quot;Hello&quot;;&lt;/code&gt; 时，C 语言会自动在字符串末尾添加 &lt;code&gt;\0&lt;/code&gt;，因此虽然只有 5 个字符，但 &lt;code&gt;str&lt;/code&gt; 的长度为 6。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;printf(&quot;%zu\n&quot;, sizeof(str));
// 6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果字符串不以 &lt;code&gt;\0&lt;/code&gt; 结尾，程序可能无法正确识别字符串的结束位置，继续连续遍历其后内存，导致未定义行为。&lt;/p&gt;
&lt;h2&gt;逻辑控制&lt;/h2&gt;
&lt;h3&gt;条件分支&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;if ... else&lt;/code&gt; 根据条件执行不同的代码块。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int num = 10;
if (num &gt; 0) {
  printf(&quot;正数\n&quot;);
} else if (num &amp;#x3C; 0) {
  printf(&quot;负数\n&quot;);
} else {
  printf(&quot;零\n&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip
C 语言中没有原生的布尔类型，数值为 0 为逻辑假，非 0 为逻辑真。逻辑运算的结果都是整数。
:::&lt;/p&gt;
&lt;h3&gt;选择分支&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;switch&lt;/code&gt; 用于多条件判断。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;char grade = &apos;B&apos;;
switch (grade) {
  case &apos;A&apos;:
    printf(&quot;优秀\n&quot;);
    break; // 跳出 switch，如果不加会继续向下执行
  case &apos;B&apos;:
    printf(&quot;良好\n&quot;);
    break;
  default:
    printf(&quot;其他\n&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结构&lt;/h3&gt;
&lt;p&gt;当我们知道循环次数时可以使用 &lt;code&gt;for&lt;/code&gt; 循环。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;for (int i = 0; i &amp;#x3C; 5; i++) {
  printf(&quot;i = %d\n&quot;, i);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要重复执行直到达成某个条件，不知道具体次数时，可以用 &lt;code&gt;while&lt;/code&gt; 循环。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;int i = 0;
while (i &amp;#x3C; 5) {
  printf(&quot;i = %d\n&quot;, i);
  ++i;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;函数&lt;/h2&gt;
&lt;p&gt;函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数，即 &lt;code&gt;main&lt;/code&gt; 函数。&lt;/p&gt;
&lt;h3&gt;定义与调用&lt;/h3&gt;
&lt;p&gt;函数由返回类型、函数名、参数列表和函数体组成。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

// 函数定义：返回 int，接收两个 int 参数
int add(int x, int y) {
  return x + y;
}

int main() {
  int result = add(5, 3); // 函数调用
  printf(&quot;result: %d\n&quot;, result);
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;声明&lt;/h3&gt;
&lt;p&gt;不同于其他语言，C 语言在调用函数时，该函数必须被声明过。尝试下面的示例，我们把 &lt;code&gt;add&lt;/code&gt; 函数移动到 &lt;code&gt;main&lt;/code&gt; 下方，代码就不能通过编译了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

int main() {
  int result = add(5, 3); // 错误：隐式声明函数‘add’ [-Wimplicit-function-declaration] // [!code error]
  printf(&quot;result: %d\n&quot;, result);
  return 0;
}

int add(int x, int y) {
  return x + y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了解决这个问题，我们可以在调用函数之前先声明函数。下面这个代码可以正常编译。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;#include &amp;#x3C;stdio.h&gt;

// 函数声明：不需要函数体，以分号结尾
int add(int x, int y); // [!code ++]

int main() {
  int result = add(5, 3);
  printf(&quot;result: %d\n&quot;, result);
  return 0;
}

// 函数定义
int add(int x, int y) {
  return x + y;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;import { Card } from &apos;astro-pure/user&apos;&lt;/p&gt;
&lt;p&gt;C指针和结构体&lt;/p&gt;</content:encoded><h:img src="/_astro/thumbnail.BaIleo-v.webp"/><enclosure url="/_astro/thumbnail.BaIleo-v.webp"/></item></channel></rss>