[{"content":" 前言 最近一直在搞各种大模型 API 中转服务，为了能用上低价甚至免费的 API，我真是煞费苦心，尝试了各种方案：\n自搭建 GLM-5.1-w8a8 模型，使用 vllm + litellm 提供 API 接入服务 购买便宜的 Deepseek v4 pro 的 API 在各种二道贩子、小众平台上购买 GPT 号、搭建 GPT 号池并最终反代出 Codex API 参与学校的 token plan，拿到免费的 Deepseek v4 pro 和 qwen3.6 的 API 接入 Nvidia 自部署并且可以免费使用的各种模型的 API 从熟悉的同学那里求来用不完或者免费的 API 额度 但是随着服务数量的增多和额外需求的增加，我同时也重构了我现在的在校服务器搭建和配置方案，因此在此分享一下我的方案，提供一种可行的思路。\n背景 在正式讲述我的搭建方案前，我需要声明一下我所拥有的资源情况：\n一台位于宿舍由主机改造而来的小型服务器\n特点：4 核心、8g ddr4 内存、512g 固态硬盘、千兆网卡 限制：宿舍晚上 23:30 熄灯，直到早上 6:00 才上电自动开机，因此无法熬夜使用 一台校内 Vlab 平台提供的 kvm 虚拟机\n特点：2 核心、6g 内存、16g 机械硬盘、千兆网卡 限制：性能一般，尤其是磁盘随机读写性能较差，只能跑一些简单的服务，而且伴有由于维护带来的强制重启和停机的风险 一台位于香港的轻量级 VPS\n特点：1 核心、512m 内存、5g 固态硬盘、延迟 60ms 左右、千兆网卡 限制：性能较差，尤其是内存和磁盘空间非常有限，只能跑一些基本的代理和反代服务 我的服务有很多种类，首先所有服务都是基于 http 协议的，其次有部分服务是可以忍受晚上断电的，但是有部分服务由于是不仅给我自己使用的，还分享给了其他同学使用，因此需要尽可能保证 24 小时在线。\n架构 基于上述资源和需求，我设计了如下的架构：\n宿舍小型服务器：主要用于部署一些重量级和偏私人向的服务，这类服务要么需要较好的性能，要么就不是很需要在我凌晨使用的，因此可以接受晚上断电的限制 校内 Vlab 虚拟机：主要用于部署一些轻量级的服务和共享服务，这类服务虽然性能要求不高，但需要尽可能保证 24 小时在线，因此适合部署在 Vlab 上 香港 VPS：主要用于将服务暴露到公网，做一个轻量级的反代，不部署任何服务。 实现 首先我采用 Tailscale 将三台机器连接在一起，形成一个虚拟局域网，这样三台机器就可以畅通无阻地相互访问了。\n然后，我在香港的 VPS 上部署了 Nginx，将 80 和 443 端口反代到校内的两台机器上。\n核心的配置如下所示：\nstream {\nmap $ssl_preread_server_name $backend_443 {\naaa.vertsineu.top 100.xxx.xxx.xxx:443;\nbbb.vertsineu.top 100.xxx.xxx.xxx:443;\ndefault 100.yyy.yyy.yyy:443;\n}\nserver {\nlisten 80;\nlisten [::]:80;\nproxy_pass 100.xxx.xxx.xxx:80;\n}\nserver {\nlisten 443;\nlisten [::]:443;\nproxy_pass $backend_443;\nssl_preread on;\n}\n} 其中，我没有使用 http 反代，而是使用 stream 模块进行端口转发，这样在面对比如 websocket 这种协议时就不需要额外的配置了，直接交给后端的服务来处理就好了。 同时，我采用 SNI 来区分不同的服务，默认服务都是走宿舍的服务器，只有特定域名的服务才会转发到 Vlab 上，这样就实现了分流机制。\n最后，在宿舍和 Vlab 上的 Nginx 中，我采用一个固定的模板将 Docker 容器中的服务反代出来：\nserver {\nlisten 443 ssl;\nlisten [::]:443 ssl;\nserver_name ccc.vertsineu.top;\nhttp2 on;\ninclude snippets/ssl-certificates.conf;\ninclude snippets/ssl-params.conf;\nlocation / {\nproxy_pass http://127.0.0.1:12345;\nproxy_set_header Host $host;\nproxy_set_header X-Real-IP $remote_addr;\nproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\nproxy_set_header X-Forwarded-Proto $scheme;\n}\nproxy_connect_timeout 60s;\nproxy_send_timeout 60s;\nproxy_read_timeout 60s;\nclient_body_temp_path /tmp;\nclient_max_body_size 20G;\n} 这样每次部署一个新的服务时，我只需要在 Docker 中启动服务，并且将端口映射到宿舍或者 Vlab 上的某个固定端口，然后在 Nginx 中添加一个 server 配置，指向这个端口就好了，非常方便。\n其中，关于 ssl 的配置，我使用 acme.sh 来自动申请和更新证书，并将证书和私钥放在 /etc/ssl/certs 和 /etc/ssl/private 目录下，然后在 Nginx 的配置中通过固定的 snippets 来引入证书和相关的参数，这样就实现了自动化的证书管理。\n后记 在配置整个服务之前，我还没认识到有 Cloudflare Tunnel 这个东西，后来从其他同学那里了解到后，发现它也能提供端口转发和反代的功能，甚至有自动证书管理等更为方便的功能，而且免费额度也完全够用，但是现在我已经搭建这么一套系统了，迁移成本有点高，所以就先不考虑了，等以后有机会再试试吧。\np.s. Cloudflare 真是互联网活菩萨，除了 Tunnel 还有 OSS，Database，Pages 等各种有足够免费额度的服务，之后有机会可以试试看。\nTable of Contents\n前言\n背景\n架构\n实现\n后记\n","permalink":"https://blog.vertsineu.top/posts/server-at-school/","summary":"\u003ch2 id=\"loc-1\"\u003e前言\u003c/h2\u003e\n    \u003cp\u003e最近一直在搞各种大模型 API 中转服务，为了能用上低价甚至免费的 API，我真是煞费苦心，尝试了各种方案：\u003c/p\u003e\n    \u003cp\u003e\u003c/p\u003e\n    \u003cul\u003e\n      \u003cli\u003e自搭建 GLM-5.1-w8a8 模型，使用 vllm + litellm 提供 API 接入服务\u003c/li\u003e\n      \u003cli\u003e购买便宜的 Deepseek v4 pro 的 API\u003c/li\u003e\n      \u003cli\u003e在各种二道贩子、小众平台上购买 GPT 号、搭建 GPT 号池并最终反代出 Codex API\u003c/li\u003e\n      \u003cli\u003e参与学校的 token plan，拿到免费的 Deepseek v4 pro 和 qwen3.6 的 API\u003c/li\u003e\n      \u003cli\u003e接入 Nvidia 自部署并且可以免费使用的各种模型的 API\u003c/li\u003e\n      \u003cli\u003e\u003cs\u003e从熟悉的同学那里求来用不完或者免费的 API 额度\u003c/s\u003e\u003c/li\u003e\n    \u003c/ul\u003e\n    \u003cp\u003e\u003c/p\u003e\n    \u003cp\u003e但是随着服务数量的增多和额外需求的增加，我同时也重构了我现在的在校服务器搭建和配置方案，因此在此分享一下我的方案，提供一种可行的思路。\u003c/p\u003e\n    \u003ch2 id=\"loc-2\"\u003e背景\u003c/h2\u003e\n    \u003cp\u003e在正式讲述我的搭建方案前，我需要声明一下我所拥有的资源情况：\u003c/p\u003e\n    \u003cp\u003e\u003c/p\u003e\n    \u003cul\u003e\n      \u003cli\u003e\n        \u003cp\u003e一台位于宿舍由主机改造而来的小型服务器\u003c/p\u003e\n        \u003cp\u003e\u003c/p\u003e\n        \u003cul\u003e\n          \u003cli\u003e特点：4 核心、8g ddr4 内存、512g 固态硬盘、千兆网卡\u003c/li\u003e\n          \u003cli\u003e限制：宿舍晚上 23:30 熄灯，直到早上 6:00 才上电自动开机，因此无法熬夜使用\u003c/li\u003e\n        \u003c/ul\u003e\n        \u003cp\u003e\u003c/p\u003e\n      \u003c/li\u003e\n      \u003cli\u003e\n        \u003cp\u003e一台校内 Vlab 平台提供的 kvm 虚拟机\u003c/p\u003e\n        \u003cp\u003e\u003c/p\u003e\n        \u003cul\u003e\n          \u003cli\u003e特点：2 核心、6g 内存、16g 机械硬盘、千兆网卡\u003c/li\u003e\n          \u003cli\u003e限制：性能一般，尤其是磁盘随机读写性能较差，只能跑一些简单的服务，而且伴有由于维护带来的强制重启和停机的风险\u003c/li\u003e\n        \u003c/ul\u003e\n        \u003cp\u003e\u003c/p\u003e\n      \u003c/li\u003e\n      \u003cli\u003e\n        \u003cp\u003e一台位于香港的轻量级 VPS\u003c/p\u003e","title":"校内个人服务器搭建和配置方案"},{"content":" 前言 通常，代理通常有两种方式，一种是开放端口，让需要代理的应用主动通过暴露的端口被代理访问远程服务；一种是使用 tun mode，即虚拟网卡模式，所有流量都会被虚拟网卡设备拦截从而被动代理访问远程服务。\n对我来说，我并不希望在我的服务器上采取被动方式，因为它会对服务器上的所有应用产生影响，而且一旦代理软件崩溃退出，我将无法访问互联网，而且会影响比如 ping 等工具的执行（拦截 ICMP 数据包），这是极其不好的，因此平时在我的服务器上，我更倾向于使用主动方式，只有应用需要被代理才会主动去访问代理端口。\n说是主动，也并不完全主动，无非就是设置环境变量：\nexport http_proxy=http://127.0.0.1:7890\nexport https_proxy=http://127.0.0.1:7890 但是，对于 Docker 来说，通过设置环境变量的方式来配置代理会非常麻烦，如果不使用 docker compose，每次运行 docker run 的时候都需要输入长长的一串环境变量；如果使用 docker compose，要修改的部分也非常多，如下所示：\nservices:\napp:\nimage: my-app\nenvironment:\n- HTTP_PROXY=http://host.docker.internal:7890\n- HTTPS_PROXY=http://host.docker.internal:7890\n- NO_PROXY=localhost,127.0.0.1\nextra_hosts:\n- \"host.docker.internal:host-gateway\" # only for linux 除此之外，使用主动代理还有其他弊端：\n如果 docker compose 中有多个 service，你可能需要花费功夫判断哪些 service 需要代理，哪些不需要代理。 如果镜像中的应用不支持通过环境变量配置代理，那么以上配置就完全无效了。 最重要的，在 docker build 的过程中，Docker 会起一个临时容器来运行 dockerfile 中的命令，而在这个过程中，如果没有配置代理或镜像，那么一些基本的命令，比如 apt update、apt install 以及 pip install 等等，就会导致镜像构建缓慢，非常折磨人，而在 dockerfile 中配置代理也是一个非常麻烦的事情，很多时候都是发现 docker build 执行缓慢或者失败的时候才想到要配置代理，浪费大把时间。 因此，我的想法就是给 Docker 配置一个被动代理，准确来说，透明代理，从而让 Docker 中的应用完全不需要管代理的事情，所有流量都会被自动代理。\n背景 在讲述实现前，我需要说明一下我的服务器上的一些基本情况：\nmihomo: 运行在 7890 端口，提供 http/https 代理服务，通过 systemd 管理 docker: 大部分服务使用的是 bridge 网络模式，因此我只需要考虑给 bridge 模式下的容器配置透明代理 iptables: legacy mode，不支持 nftables 实现 核心思路很简单，通过 iptables 将所有从 bridge 网段发出的流量重定向到 mihomo 的透明代理端口上，从而实现透明代理。\nmihomo mihomo 这边需要设置 tproxy 的端口和 sniff 功能，需要添加的配置如下所示：\ntproxy-port: 7893\nsniffer:\nenable: true\nsniff:\nHTTP:\nports: [80]\nTLS:\nports: [443]\nQUIC:\nports: [443]\noverride-destination: true 其中，tproxy-port 是 mihomo 监听透明代理流量的端口，而开启 sniffer 是因为不同于显式的 http/https 代理，在不开 sniffer 的情况下，透明代理无法判断当前流量是否是 https 流量，无法得知 SNI，从而在远端的代理和实际访问的服务器之间建立 tls 连接时代理无法发送正确的 SNI，导致代理和服务器之间的 tls 连接无法建立成功，从而无法通过 https 访问；而一旦开启了 sniffer，mihomo 能够从数据包中解析出 SNI，发送给远端的代理，让代理能够发送正确的 SNI，从而成功建立 tls 连接。\niptables iptables 需要标记所有来自 bridge 网段的流量，但是忽略所有去往本机/局域网地址的流量，而对于被标记的流量，我们需要对其单独配置路由表，从而防止内核在路由时转发到外部网卡，而是保留到本机处理，因此，iptables 的配置如下所示：\n# /etc/mihomo-tproxy/setup.sh\n#!/bin/bash\nset -e\nTABLE=100\nMARK=1\nCHAIN=MIHOMO_TPROXY\nDOCKER_CIDR=172.16.0.0/12\nTPROXY_PORT=7893\nip rule add fwmark $MARK table $TABLE priority 100 2\u003e/dev/null || true\nip route add local default dev lo table $TABLE 2\u003e/dev/null || true\niptables -t mangle -N $CHAIN 2\u003e/dev/null || iptables -t mangle -F $CHAIN\niptables -t mangle -A $CHAIN -d 127.0.0.0/8 -j RETURN\niptables -t mangle -A $CHAIN -d 10.0.0.0/8 -j RETURN\niptables -t mangle -A $CHAIN -d 172.16.0.0/12 -j RETURN\niptables -t mangle -A $CHAIN -d 192.168.0.0/16 -j RETURN\niptables -t mangle -A $CHAIN -p tcp -j TPROXY \\\n--on-ip 127.0.0.1 --on-port $TPROXY_PORT --tproxy-mark $MARK\niptables -t mangle -A $CHAIN -p udp -j TPROXY \\\n--on-ip 127.0.0.1 --on-port $TPROXY_PORT --tproxy-mark $MARK\niptables -t mangle -A PREROUTING -s $DOCKER_CIDR -j $CHAIN 其中，TPROXY 目标会将所有需要被代理的流量关联到 mihomo 的透明代理端口上的那个 socket 上，但是它不能影响路由决策，因此我首先给这部分流量打了一个标记，然后通过 ip rule 和 ip route 来配置路由表，让被标记的流量转发到本机的 lo 网卡上，这样，lo 网卡收到流量后就会将其交给 mihomo 处理了。\n为了保证可维护性，我还编写了一个脚本来删除这些 iptables 规则，以便在代理崩溃退出后能够及时清理规则，恢复网络：\n# /etc/mihomo-tproxy/teardown.sh\n#!/bin/bash\nTABLE=100\nMARK=1\nCHAIN=MIHOMO_TPROXY\nDOCKER_CIDR=172.16.0.0/12\niptables -t mangle -D PREROUTING -s $DOCKER_CIDR -j $CHAIN 2\u003e/dev/null || true\niptables -t mangle -F $CHAIN 2\u003e/dev/null || true\niptables -t mangle -X $CHAIN 2\u003e/dev/null || true\nip rule del fwmark $MARK table $TABLE 2\u003e/dev/null || true\nip route del local default dev lo table $TABLE 2\u003e/dev/null || true 最后，我编写一个 mihomo-tproxy.service 来管理这个透明代理，并和 mihomo 的生命周期绑定在一起：\n# /etc/systemd/system/mihomo-tproxy.service\n[Unit]\nDescription=Mihomo transparent proxy iptables rules\nAfter=network.target mihomo.service\nBindsTo=mihomo.service\n[Service]\nType=oneshot\nRemainAfterExit=yes\nExecStart=/etc/mihomo-tproxy/setup.sh\nExecStop=/etc/mihomo-tproxy/teardown.sh\n[Install]\nWantedBy=multi-user.target 执行 systemctl enable --now mihomo-tproxy 启用服务后，透明代理就算是配置完成了。\n测试 配置完成后，我使用 curl 进行了测试：\ndocker run --rm curlimages/curl curl -v https://www.google.com 输出：\n% Total % Received % Xferd Average Speed Time Time Time Current\nDload Upload Total Spent Left Speed\n0 0 0 0 0 0 0 0 0* Host www.google.com:443 was resolved.\n...\n\u003e GET / HTTP/2\n\u003e Host: www.google.com\n\u003e User-Agent: curl/8.19.0\n\u003e Accept: */*\n\u003e ... 正常访问，并且 mihomo 的日志中也正确记录了这次访问，说明透明代理配置成功了。\n缺陷 过了几天，在我给服务器装 Kubernetes 的时候，我发现了给 Docker 配置透明代理的一个缺陷，那就是它的 iptables 规则会和 Kubernetes 的 iptables 规则冲突，导致容器内无法正常访问网络。\n目前我并没有找到什么好的解决方法，我只能避免在同一台服务器上同时使用 Docker 和 Kubernetes，好在，在这台服务器上我只需要使用 Docker 也能满足我的日常需求，所以这个缺陷对我来说并不算是个大问题。\nTable of Contents\n前言\n背景\n实现\nmihomo\niptables\n测试\n缺陷\n","permalink":"https://blog.vertsineu.top/posts/docker-proxy/","summary":"\u003ch2 id=\"loc-1\"\u003e前言\u003c/h2\u003e\n    \u003cp\u003e通常，代理通常有两种方式，一种是开放端口，让需要代理的应用\u003cstrong\u003e主动\u003c/strong\u003e通过暴露的端口被代理访问远程服务；一种是使用 tun mode，即虚拟网卡模式，所有流量都会被虚拟网卡设备拦截从而\u003cstrong\u003e被动\u003c/strong\u003e代理访问远程服务。\u003c/p\u003e\n    \u003cp\u003e对我来说，我并不希望在我的服务器上采取被动方式，因为它会对服务器上的所有应用产生影响，而且一旦代理软件崩溃退出，我将无法访问互联网，而且会影响比如 ping 等工具的执行（拦截 ICMP 数据包），这是极其不好的，因此平时在我的服务器上，我更倾向于使用主动方式，只有应用需要被代理才会主动去访问代理端口。\u003c/p\u003e\n    \u003cp\u003e说是主动，也并不完全主动，无非就是设置环境变量：\u003c/p\u003e\n    \u003cdiv class=\"typst-raw-block\"\u003e\n      \u003cpre\u003e\u003ccode data-lang=\"bash\"\u003e\u003cspan style=\"color: #d73948\"\u003eexport\u003c/span\u003e http_proxy\u003cspan style=\"color: #d73948\"\u003e=\u003c/span\u003e\u003cspan style=\"color: #198810\"\u003ehttp://127.0.0.1:7890\u003c/span\u003e\u003cbr\u003e\u003cspan style=\"color: #d73948\"\u003eexport\u003c/span\u003e https_proxy\u003cspan style=\"color: #d73948\"\u003e=\u003c/span\u003e\u003cspan style=\"color: #198810\"\u003ehttp://127.0.0.1:7890\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n    \u003c/div\u003e\n    \u003cp\u003e但是，对于 Docker 来说，通过设置环境变量的方式来配置代理会非常麻烦，如果不使用 docker compose，每次运行 docker run 的时候都需要输入长长的一串环境变量；如果使用 docker compose，要修改的部分也非常多，如下所示：\u003c/p\u003e\n    \u003cdiv class=\"typst-raw-block\"\u003e\n      \u003cpre\u003e\u003ccode data-lang=\"yaml\"\u003e\u003cspan style=\"color: #4b69c6\"\u003eservices\u003c/span\u003e:\u003cbr\u003e  \u003cspan style=\"color: #4b69c6\"\u003eapp\u003c/span\u003e:\u003cbr\u003e    \u003cspan style=\"color: #4b69c6\"\u003eimage\u003c/span\u003e: \u003cspan style=\"color: #198810\"\u003emy-app\u003c/span\u003e\u003cbr\u003e    \u003cspan style=\"color: #4b69c6\"\u003eenvironment\u003c/span\u003e:\u003cbr\u003e      - \u003cspan style=\"color: #198810\"\u003eHTTP_PROXY=http://host.docker.internal:7890\u003c/span\u003e\u003cbr\u003e      - \u003cspan style=\"color: #198810\"\u003eHTTPS_PROXY=http://host.docker.internal:7890\u003c/span\u003e\u003cbr\u003e      - \u003cspan style=\"color: #198810\"\u003eNO_PROXY=localhost,127.0.0.1\u003c/span\u003e\u003cbr\u003e    \u003cspan style=\"color: #4b69c6\"\u003eextra_hosts\u003c/span\u003e:\u003cbr\u003e      - \u003cspan style=\"color: #198810\"\u003e\"\u003c/span\u003e\u003cspan style=\"color: #198810\"\u003ehost.docker.internal:host-gateway\u003c/span\u003e\u003cspan style=\"color: #198810\"\u003e\"\u003c/span\u003e \u003cspan style=\"color: #74747c\"\u003e#\u003c/span\u003e\u003cspan style=\"color: #74747c\"\u003e only for linux\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\n    \u003c/div\u003e\n    \u003cp\u003e除此之外，使用主动代理还有其他弊端：\u003c/p\u003e\n    \u003cp\u003e\u003c/p\u003e\n    \u003cul\u003e\n      \u003cli\u003e如果 docker compose 中有多个 service，你可能需要花费功夫判断哪些 service 需要代理，哪些不需要代理。\u003c/li\u003e\n      \u003cli\u003e如果镜像中的应用不支持通过环境变量配置代理，那么以上配置就完全无效了。\u003c/li\u003e\n      \u003cli\u003e最重要的，在 docker build 的过程中，Docker 会起一个临时容器来运行 dockerfile 中的命令，而在这个过程中，如果没有配置代理或镜像，那么一些基本的命令，比如 \u003cspan class=\"typst-raw-inline\"\u003e\u003ccode\u003eapt update\u003c/code\u003e\u003c/span\u003e、\u003cspan class=\"typst-raw-inline\"\u003e\u003ccode\u003eapt install\u003c/code\u003e\u003c/span\u003e 以及 \u003cspan class=\"typst-raw-inline\"\u003e\u003ccode\u003epip install\u003c/code\u003e\u003c/span\u003e 等等，就会导致镜像构建缓慢，非常折磨人，而在 dockerfile 中配置代理也是一个非常麻烦的事情，很多时候都是发现 docker build 执行缓慢或者失败的时候才想到要配置代理，浪费大把时间。\u003c/li\u003e\n    \u003c/ul\u003e\n    \u003cp\u003e\u003c/p\u003e","title":"如何给 Docker 配置透明代理"},{"content":" 前言 不久之前，我接手了一套服务器集群，这个集群由一个登录节点和多个计算节点组成，其中登录节点可以通过外网访问，而计算节点只能通过登录节点跳板访问。\n拿到手，我最先做的便是探查一下这个集群的网络架构，而在我排查过程中，我发现登录节点 80 端口上开了一个 HTTP 访问，是用于计算节点资源管理的后台管理平台，不足为奇。\n但是我有点好奇，想看一下这个 80 端口运行的那个服务是哪个进程管理的，顺便也能看一下这个管理平台是如何搭建起来的。\n然后，按照运维的惯例，执行以下几个命令来查找对应进程：\nlsof -i :80\nnetstat -tlnup | grep :80\nss -tlnup | grep :80 并不奇怪，输出结果显示：\n$ netstat -tlnup | grep :80\ntcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 3202/nginx: master 即，80 端口是一个 nginx 进程在提供服务，但是我们知道，管理平台不是静态网页，nginx 大概率是一个反向代理的作用。\n因此，我打开了 /etc/nginx/nginx.conf 查看一下 80 端口具体代理了哪里的网络服务（省略无关配置）：\nstream {\nserver {\nlisten 80;\nproxy_pass 172.20.2.31:80;\n}\n} 看样子代理了 172.20.2.31 这个计算节点上 80 端口的服务。\n接着，我 ssh 连接上了这个节点，同样使用惯例的指令查询 80 端口上的服务，但是这一次，奇怪的事情出现了，终端里什么输出都没有，也就是说，“没有”任何进程在 80 端口上服务？\n我一下子就懵了，因为我完全不能理解为什么这些常用的排查命令都失效了！即使我不用 grep，反复 check 了 lsof netstat 和 ss 的输出，结果依旧是找不到任何占用 80 端口的进程！\n排查 询问 AI 在 AI 时代，大部分人遇到不理解的事情最先想到的肯定是 AI 了。我问的是 ChatGPT，以下是按 AI 排的概率高低的假说：\nnetwork namespace\n虽然这个是 AI 排的最有可能的情况，但事后我想了一下，这反而是最不可能的情况了。首先，如果这个服务器用 network namespace 的话，那么大概率这个服务器用的是 Docker、Podman、K8S 之类的容器方案，但是这样的话，用 ss 和 netstat 完全可以找到对应进程，比如使用 Docker 的话，会有 docker-proxy 进程在实现 bridge 网络模式下端口转发，而其他网络情况通常并不常见（至少 bridge 和 host 网络模式是非常常用的方案），而如果是小概率的自己设置 namespace，那我请问了，有什么必要？实际上，这个假说在我这边的情况下也不成立。 systemd socket activation\n依旧随便想想就不可能，systemd socket activation 机制是只有第一个请求到来的时候才会激活一个 socket，然后之后一直保持 socket 有效。但是我之前已经不止一次访问过 80 端口上的服务了，不论是浏览器去看 80 端口是什么服务，还是 curl 测试 80 端口连通性，要激活早激活了，怎么可能是 lazy load 的问题？ iptables/nftables (firewall)\n最不可能的反而是最可能的，这就是 AI！但是当时我对 kernel firewall 了解不多，实在不好确定是否这是一个可行的假说，还是一个从内行人看来一眼就知道原理上不可能的假说？尤其是在 AI 已经提出了两个显然不可能的假说的前提下。 求助大佬 指望 AI 是不可靠了，只能指望懂这方面的大佬了，于是我在我校的 Linux User Group 群里复述了我的问题。果然大佬还是大佬，在看完 netstat 的输出后直截了当地就来考虑 iptables 的问题了。\n首先是运行 ipvsdm 查看是否有 IPVS 参与负载均衡：\n$ ipvsadm\nIP Virtual Server version 1.2.1 (size=4096)\nProt LocalAddress:Port Scheduler Flags\n-\u003e RemoteAddress:Port Forward Weight ActiveConn InActConn 只有默认配置，排除。\n然后查看服务器使用的是 iptables legacy 方案还是 nftables 方案：\n$ iptables --version\niptables v1.8.7 (legacy) 采用 legacy 方案，因此使用以下指令查看 iptables 的 nat 表（负责修改源/目的的 IP 和端口）下的所有 Chain：\niptables -L -t nat 但是由于服务器采用了 k8s，iptables 里的 Chain 非常多，以下仅列出预定义的 Chain 的内容：\n$ iptables -L -t nat\niptables -L -t nat | head -n 40\nChain PREROUTING (policy ACCEPT)\ntarget prot opt source destination\ncali-PREROUTING all -- anywhere anywhere /* cali:6gwbT8clXdHdC1b1 */\nKUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */\nDOCKER all -- anywhere anywhere ADDRTYPE match dst-type LOCAL\nChain INPUT (policy ACCEPT)\ntarget prot opt source destination\nChain OUTPUT (policy ACCEPT)\ntarget prot opt source destination\ncali-OUTPUT all -- anywhere anywhere /* cali:tVnHkvAo15HuiPy0 */\nKUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */\nDOCKER all -- anywhere !127.0.0.0/8 ADDRTYPE match dst-type LOCAL\nChain POSTROUTING (policy ACCEPT)\ntarget prot opt source destination\ncali-POSTROUTING all -- anywhere anywhere /* cali:O3lYWMrLQYEMJtB5 */\nMASQUERADE all -- 172.18.0.0/16 anywhere\nKUBE-POSTROUTING all -- anywhere anywhere /* kubernetes postrouting rules */\nMASQUERADE all -- 172.17.0.0/16 anywhere\nLIBVIRT_PRT all -- anywhere anywhere\nMASQUERADE tcp -- 172.18.0.2 172.18.0.2 tcp dpt:10514\nMASQUERADE tcp -- 172.18.0.9 172.18.0.9 tcp dpt:webcache\n但是单从预定义的 Chain 里实在是看不出什么端倪来，因为其中并没有一条显式的规则包含 80 端口，或者目标 IP 是本机。\n自己研究 到目前为止，我只能自己根据集群的情况进行一些合理推测了。\n首先，我发现这个集群使用 k8s 进行容器编排，那我很有理由推测这个服务实际上跑在 k8s 里的。\n其次，虽然我对 k8s 不太熟悉，但是我至少知道，k8s 通常采用 nginx 作为 ingress controller 对 HTTP 服务进行反向代理、负载均衡、智能路由等功能。\n因此，我所访问的服务大概率是由 ingress-nginx 做反向代理的，虽然我不知道为什么 ingress-nginx 没有占用 80 端口。\n首先，我先找到 ingress-nginx 的 pod：\n$ kubectl get pods -n ingress-nginx\nNAME READY STATUS RESTARTS AGE\ningress-nginx-controller-59c4c457db-srld9 1/1 Running 2 (151d ago) 351d 然后，进入到这个 pod 里：\nkubectl exec -it -n ingress-nginx ingress-nginx-controller-59c4c457db-srld9 -- /bin/bash 接着，在这个 pod 里通过 vi 查看 /etc/nginx/nginx.conf 的内容，找到匹配根路径的 location 块（已省略无关内容）：\nhttp {\nserver {\nlocation ~* '^/' {\nset $namespace \"hero-application\";\nset $service_name \"heros-portal-console\";\nset $service_port \"5443\";\nset $location_path \"\";\n}\n}\n} 发现是一个在 hero-application 命名空间里叫 heros-portal-console 的 k8s service 在提供服务。\n因此，我再去查看一下这个 service 的具体信息：\n$ kubectl get svc heros-portal-console -n hero-application\nNAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE\nheros-portal-console NodePort 10.96.75.57 \u0026lt;none\u003e 5443:30008/TCP 373d 说明提供这个服务的 Pod 的 IP 是 10.96.75.57，端口是 5443。\n因此，我尝试使用 curl 10.96.75.57:5443 访问这个服务，发现和直接 curl localhost 的结果是完全一样的！说明位于主机上的 80 端口的服务正是通过 ingress-nginx 反向代理到这个 service 上的。\n回顾 那为什么 80 端口上的流量会重定向到 ingress-nginx 上呢？我于是重新查找 iptables 的 Chain。\n首先，ingress-nginx 是开在 80 端口上的，因此可以使用 grep 过滤一下，从而大大减小查找的工作量：\n$ iptables -L -t nat | grep -E ':80$' -B 3\nChain KUBE-SEP-3J3BPC5JPSIBHQN4 (1 references)\ntarget prot opt source destination\nKUBE-MARK-MASQ all -- 10.244.209.252 anywhere /* basic-component/model-repository:http */\nDNAT tcp -- anywhere anywhere /* basic-component/model-repository:http */ tcp to:10.244.209.252:80\n--\nChain KUBE-SEP-437TZ5GT227FODOU (1 references)\ntarget prot opt source destination\nKUBE-MARK-MASQ all -- 10.244.74.240 anywhere /* ingress-nginx/ingress-nginx-controller:http */\nDNAT tcp -- anywhere anywhere /* ingress-nginx/ingress-nginx-controller:http */ tcp to:10.244.74.240:80\n--\nChain KUBE-SEP-YNA7BKMCGAZVRJ7T (1 references)\ntarget prot opt source destination\nKUBE-MARK-MASQ all -- 10.244.74.246 anywhere /* hero-system/volume-controller:http */\nDNAT tcp -- anywhere anywhere /* hero-system/volume-controller:http */ tcp to:10.244.74.246:80 然后，通过 less 查看 iptables 的输出，从 Chain KUBE-SEP-437TZ5GT227FODOU 开始向上溯源查找，直到找到预定义的 Chain：\nChain PREROUTING (policy ACCEPT)\ntarget prot opt source destination\nKUBE-SERVICES all -- anywhere anywhere /* kubernetes service portals */\nChain KUBE-SERVICES (2 references)\ntarget prot opt source destination\nKUBE-NODEPORTS all -- anywhere anywhere /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL\nChain KUBE-NODEPORTS (1 references)\ntarget prot opt source destination\nKUBE-EXT-CG5I4G2RS3ZVWGLK tcp -- anywhere anywhere /* ingress-nginx/ingress-nginx-controller:http */ tcp dpt:http\nChain KUBE-EXT-CG5I4G2RS3ZVWGLK (1 references)\ntarget prot opt source destination\nKUBE-SVC-CG5I4G2RS3ZVWGLK all -- anywhere anywhere\nChain KUBE-SVC-CG5I4G2RS3ZVWGLK (2 references)\ntarget prot opt source destination\nKUBE-SEP-437TZ5GT227FODOU all -- anywhere anywhere /* ingress-nginx/ingress-nginx-controller:http -\u003e 10.244.74.240:80 */\nChain KUBE-SEP-437TZ5GT227FODOU (1 references)\ntarget prot opt source destination\nDNAT tcp -- anywhere anywhere /* ingress-nginx/ingress-nginx-controller:http */ tcp to:10.244.74.240:80 观察可以得知，访问 80 端口的流量会通过 KUBE-NODEPORTS 这个 Chain 转发到 ingress-nginx，而根据 k8s 的工作原理，在 Service 中使用 NodePort 模式暴露服务时，k8s 会在每个节点上都通过 iptables 添加规则，使得本机对应端口的流量转发到对应的 Service 上。\n但是，默认 NodePort 仅允许其端口范围为 30000-32767，进一步查询 k8s 的配置文件 /etc/kubernetes/manifests/kube-apiserver.yaml 发现，kube-apiserver 的启动参数里修改了默认端口范围为 1-65535，因此 80 端口的服务能通过 NodePort 的方式暴露出来。\n总结 至此，所有疑惑都迎刃而解了，我们可以尝试推测一下这个 80 端口的服务是怎么被拉起来的了：\nk8s 配置文件里的 NodePort 范围被修改为 1-65535，使得 80 端口的服务能够通过 NodePort 的方式暴露出来。 目标 Service 启用，使用 NodePort 模式暴露服务，k8s 在每个节点上设置 iptables，将访问本机 80 端口的流量转发到这个 Service 上。 在登录节点上启用 nginx 进行反代，将某台计算节点上的 80 端口的服务反代到登录节点的 80 端口上。 用户访问登录节点的 80 端口，成功访问到服务。 改进 一个 Service 直接使用 NodePort 暴露 HTTP 服务实在是有点简单粗暴，工业级的做法应该是使用 Ingress，比如 ingress-nginx 这样成熟的 ingress controller 来提供统一的 HTTP 服务入口，不仅方便配置 HTTPS，而且如果之后还有别的 HTTP 服务需要暴露出来的话，交给 ingress controller 来做反代和路由也是非常方便的。\n之后如果有时间的话，我可能需要折腾一下了。\n除此之外，我可能需要学习一下 k8s 的基本知识了，以后在这个集群里进一步开发和排查的话，基础知识还是很有必要的。\nTable of Contents\n前言\n排查\n询问 AI\n求助大佬\n自己研究\n回顾\n总结\n改进\n","permalink":"https://blog.vertsineu.top/posts/find-process-from-port/","summary":"\u003ch2 id=\"loc-1\"\u003e前言\u003c/h2\u003e\n    \u003cp\u003e不久之前，我接手了一套服务器集群，这个集群由一个登录节点和多个计算节点组成，其中登录节点可以通过外网访问，而计算节点只能通过登录节点跳板访问。\u003c/p\u003e\n    \u003cp\u003e拿到手，我最先做的便是探查一下这个集群的网络架构，而在我排查过程中，我发现登录节点 80 端口上开了一个 HTTP 访问，是用于计算节点资源管理的后台管理平台，不足为奇。\u003c/p\u003e\n    \u003cp\u003e但是我有点好奇，想看一下这个 80 端口运行的那个服务是哪个进程管理的，顺便也能看一下这个管理平台是如何搭建起来的。\u003c/p\u003e\n    \u003cp\u003e然后，按照运维的惯例，执行以下几个命令来查找对应进程：\u003c/p\u003e\n    \u003cdiv class=\"typst-raw-block\"\u003e\n      \u003cpre\u003e\u003ccode data-lang=\"bash\"\u003e\u003cspan style=\"color: #4b69c6\"\u003elsof\u003c/span\u003e -i :80\u003cbr\u003e\u003cspan style=\"color: #4b69c6\"\u003enetstat\u003c/span\u003e -tlnup \u003cspan style=\"color: #d73948\"\u003e|\u003c/span\u003e \u003cspan style=\"color: #4b69c6\"\u003egrep\u003c/span\u003e :80\u003cbr\u003e\u003cspan style=\"color: #4b69c6\"\u003ess\u003c/span\u003e -tlnup \u003cspan style=\"color: #d73948\"\u003e|\u003c/span\u003e \u003cspan style=\"color: #4b69c6\"\u003egrep\u003c/span\u003e :80\u003c/code\u003e\u003c/pre\u003e\n    \u003c/div\u003e\n    \u003cp\u003e并不奇怪，输出结果显示：\u003c/p\u003e\n    \u003cdiv class=\"typst-raw-block\"\u003e\n      \u003cpre\u003e\u003ccode data-lang=\"bash\"\u003e\u003cspan style=\"color: #4b69c6\"\u003e$\u003c/span\u003e netstat -tlnup \u003cspan style=\"color: #d73948\"\u003e|\u003c/span\u003e \u003cspan style=\"color: #4b69c6\"\u003egrep\u003c/span\u003e :80\u003cbr\u003e\u003cspan style=\"color: #4b69c6\"\u003etcp\u003c/span\u003e        0      0 0.0.0.0:80              0.0.0.0:\u003cspan style=\"color: #d73948\"\u003e*\u003c/span\u003e               LISTEN      3202/nginx: master\u003c/code\u003e\u003c/pre\u003e\n    \u003c/div\u003e\n    \u003cp\u003e即，80 端口是一个 nginx 进程在提供服务，但是我们知道，管理平台不是静态网页，nginx 大概率是一个反向代理的作用。\u003c/p\u003e\n    \u003cp\u003e因此，我打开了 \u003cspan class=\"typst-raw-inline\"\u003e\u003ccode\u003e/etc/nginx/nginx.conf\u003c/code\u003e\u003c/span\u003e 查看一下 80 端口具体代理了哪里的网络服务（省略无关配置）：\u003c/p\u003e\n    \u003cdiv class=\"typst-raw-block\"\u003e\n      \u003cpre\u003e\u003ccode data-lang=\"conf\"\u003e\u003cspan style=\"color: #d73948\"\u003estream\u003c/span\u003e {\u003cbr\u003e  \u003cspan style=\"color: #d73948\"\u003eserver\u003c/span\u003e {\u003cbr\u003e    \u003cspan style=\"color: #d73948\"\u003elisten\u003c/span\u003e \u003cspan style=\"color: #198810\"\u003e80\u003c/span\u003e;\u003cbr\u003e    \u003cspan style=\"color: #d73948\"\u003eproxy_pass\u003c/span\u003e 172.20.2.31:80;\u003cbr\u003e  }\u003cbr\u003e}\u003c/code\u003e\u003c/pre\u003e\n    \u003c/div\u003e\n    \u003cp\u003e看样子代理了 172.20.2.31 这个计算节点上 80 端口的服务。\u003c/p\u003e\n    \u003cp\u003e接着，我 ssh 连接上了这个节点，同样使用惯例的指令查询 80 端口上的服务，但是这一次，奇怪的事情出现了，终端里什么输出都没有，也就是说，“没有”任何进程在 80 端口上服务？\u003c/p\u003e","title":"记一次在 Linux 中根据端口号查找进程的经历"},{"content":" 前言 从大二上计算机组成原理的时候我就想写一篇这样的文章了，当时我调研了 x86 架构平台的计算机的启动过程，意识到很多人实际上并不是非常了解一台计算机从上电到进入桌面的全过程。\n虽然网上有很多关于计算机启动过程的文章，结合了具体的系统，甚至制作了精美的动画，但是我觉得它们为了讲解的清晰，往往省略了实际执行的指令细节，导致理解启动原理和实际安装系统之间仍存在一定的距离。\n因此，本文我将以 x86 架构为例，结合 Arch Linux 系统的安装过程来讲解计算机的启动过程。\n背景 基本流程 虽然很多文章已经讲解过计算机的启动过程了，但是为了便于不了解计算机原理的读者理解，我还是想先介绍一些背景知识。\n除了某些嵌入式（Embedded）系统以外，大多数计算机都采取类似的启动过程，如下图所示：\n图 1 计算机启动过程 即 Firmware、Bootloader 和 Kernel 三个阶段，每个阶段都执行特定的功能，并且在完成后将控制权交给下一个阶段。它们的功能分别如下：\nFirmware：负责计算机的基本硬件初始化和自检，确保系统的基本功能可用，并且找到并加载 Bootloader。 Bootloader：负责加载操作系统内核，并将控制权交给 Kernel。 Kernel：负责操作系统的核心功能，如进程管理、内存管理、文件系统等，并且启动用户空间的服务和应用程序。 一些示例 有点抽象？没关系，我将根据我的设备来举一些具体的例子：\n一台装有 GNU/Linux Debian 13 的小型台式主机：\nFirmware：存放在主板上的 ROM（只读存储器）芯片中，通常是 UEFI（统一可扩展固件接口）规范的实现。当你进入 BIOS 的时候，实际上进入的就是 UEFI BIOS 的设置界面（你可以在进入 BIOS 的时候观察一下上面标签上的字样）。\n如何查看：重启电脑，在开机时一直尝试按下某些特殊的键（如 F2、F10、Del 等）进入 BIOS 设置界面，具体按键取决于你的主板型号和厂商。 示例：\n图 2 BIOS 设置界面 Bootloader：存放在磁盘的主引导记录（MBR）或者 EFI 系统分区（ESP）中，通常是 GRUB（GRand Unified Bootloader）或者 systemd-boot 等引导加载程序。当你在 Boot Menu 中选择的时候，实际上就是选择不同介质（Media）上的 Bootloader 来加载。\n如何查看：重启电脑，不需要按下任何键，在开机后自动会停留到 Bootloader 的界面。 示例：\n图 3 GRUB 启动界面 Kernel：在 Linux 系统中，Kernel 通常存放在根文件系统的 /boot 目录下，通常以 vmlinuz 开头的文件。当你在 Bootloader 中选择的时候，实际上就是在选择不同的 Kernel 来加载。\n如何查看：在系统中打开终端，输入 ls -hl /boot 命令，查看 /boot 目录下的文件。 示例：\n$ ls -hl /boot\ntotal 410M\n-rw-r--r-- 1 root root 254K Jun 21 2024 config-6.1.0-22-amd64\n-rw-r--r-- 1 root root 277K Mar 9 03:54 config-6.12.74+deb13+1-amd64\n-rw-r--r-- 1 root root 290K Mar 15 03:28 config-6.19.6+deb13-amd64\ndrwxr-xr-x 5 root root 4.0K Mar 20 20:47 grub\n-rw-r--r-- 1 root root 87M Jan 10 22:53 initrd.img-6.1.0-22-amd64\n-rw-r--r-- 1 root root 137M Mar 15 23:05 initrd.img-6.12.74+deb13+1-amd64\n-rw-r--r-- 1 root root 153M Mar 20 19:26 initrd.img-6.19.6+deb13-amd64\n-rw-r--r-- 1 root root 83 Jun 21 2024 System.map-6.1.0-22-amd64\n-rw-r--r-- 1 root root 83 Mar 9 03:54 System.map-6.12.74+deb13+1-amd64\n-rw-r--r-- 1 root root 92 Mar 15 03:28 System.map-6.19.6+deb13-amd64\n-rw-r--r-- 1 root root 7.8M Jun 21 2024 vmlinuz-6.1.0-22-amd64\n-rw-r--r-- 1 root root 12M Mar 9 03:54 vmlinuz-6.12.74+deb13+1-amd64\n-rw-r--r-- 1 root root 14M Mar 15 03:28 vmlinuz-6.19.6+deb13-amd64 一台装有多个 Windows 系统的笔记本电脑：\nFirmware：同上，基本上都是 UEFI 标准实现或者传统 BIOS 实现。 Bootloader：同上，但是引导加载程序可能是 Windows Boot Manager。\n示例 [1]：\n图 4 Windows Boot Manager 启动界面 Kernel：同上，但是内核文件可能是 ntoskrnl.exe，而不是 vmlinuz 文件。 尝试一下 还是有点疑惑？那不妨就用你手上的设备来试试看吧，比如尝试进入 BIOS 设置界面看看，使用键盘上下左右键浏览，问问 AI 各个选项的作用，相信你会有更多的收获的。\n讲解 在基本了解了背景知识后，我将结合 Arch Linux 的安装过程讲解计算机的启动过程。这部分主要参考的是 Install Guide - Arch Wiki，一个颇为详细的 Arch Linux 安装指南。虽说如此，你不应该将本文当作 Arch Linux 的安装教程来读，而只是为了便于理解计算机启动过程才结合了 Arch Linux 的安装过程来讲解，你也可以把本文当作对于 Install Guide - Arch Wiki 的解读来理解。\n安装前的准备 在开始安装前，你应该提前准备好了一台能够正常开机的计算机和一个安装介质（比如 U 盘）。\n准备镜像 这部分对应 Arch Wiki 的 1.1 - 1.3 部分。\n通常，在安装任何系统之前，你都需要在一个安装介质（Installation Media）上准备好系统的安装文件，通常，安装介质就是随处可见的 USB 闪存盘（U 盘），而安装文件则是一个以 .iso 结尾的 ISO 镜像文件，里面存放着整个系统的文件系统，包括 Bootloader、Kernel 和基本的用户空间工具等，你可以理解为就是一个可以随身携带的微型系统，安装系统的过程实际上都是在这个微型系统上进行的。\n通常，你可以在镜像站中下载到各种系统的 ISO 镜像文件，比如 USTC Mirrors 和 TUNA Mirrors 等国内镜像站。\n但是，计算机不能在启动时直接读取 ISO 镜像文件来启动，因此，你需要将镜像通过 dd 命令（Linux）或者其他工具，比如 Refus（Windows）等按二进制方式直接写入到 U 盘中去。\n不过，在这里，我更推荐使用 Ventoy 这个工具来制作安装介质，它最大的好处就是你只需要把 ISO 文件丢进 U 盘里就可以了，不需要每次都使用烧录工具，而且它支持 GUI 操作，在我的系统上的界面如下所示：\n图 5 Ventoy 界面 选择你的 U 盘，点击安装（Install）按钮，然后把 ISO 镜像文件直接丢进 U 盘里就可以了，非常方便。\n进入 Live System 这部分对应 Arch Wiki 的 1.4 部分。\nLive System 就是我们所说的装在 U 盘上的微型系统。\n首先你需要把 U 盘插在计算机上，然后重启/开机，在开机时按下某些特殊的键（如 F2、F10、Del 等）进入 Boot Menu，具体按键取决于你的主板型号和厂商，在 Boot Menu 中选择你的 U 盘来启动。\n以我的 ThinkPad e480 笔记本为例，在开机时按下 Enter 键进入 Startup Interrupt Menu：\n图 6 Startup Interrupt Menu 根据提示，按下 F12 键进入 Boot Menu：\n图 7 Boot Menu 然后使用上下键切换到 USB HDD 选项，按下 Enter 键就可以进入 Ventoy 的界面了：\n图 8 Ventoy 启动界面 接着，选择你要安装的系统的 ISO 镜像文件，一路按 Enter 键就可以进入 Live System 了：\n图 9 Live System 桌面 从计算机启动过程的视角来看，这一部分完整地从 U 盘中启动了一个系统，因此完全运行了我们之前所说的 Firmware、Bootloader 和 Kernel 三个阶段的功能：\nFirmware：在开机进入 Boot Menu 前执行，完成了计算机的基本硬件初始化和自检，并且找到了 U 盘上的 Bootloader，进入 Boot Menu。 Bootloader：在 Boot Menu 中选择 U 盘后，被计算机读取到内存中执行，也就是我们的 Ventoy 以及其引导的 systemd-boot 引导加载程序。 Kernel：在最后一路按 Enter 键的过程中，我们选择了 Arch Linux 的 Live System 的 Kernel 来加载。计算机读取 Kernel 文件到内存中执行，从而顺利启动和进入了 Live System。 一些杂项 这部分对应 Arch Wiki 的 1.5 - 1.8 部分。\n这部分主要就是键盘布局、网络连接等和计算机启动过程关系不大的内容了，这里就不展开讲解了。\n磁盘分区 这部分对应 Arch Wiki 的 1.9 部分。\n磁盘分区是计算机启动过程中的一个重要环节，这涉及到 Firmware 如何找到 Bootloader，以及 Bootloader 如何找到 Kernel，而且在 UEFI 和 BIOS 两种 Firmware 下，分区的要求也是不同的，接下来我们将分别讨论。\nBIOS 在传统的 BIOS 模式下，磁盘的分区信息存放在磁盘的第一个扇区，即 MBR（Master Boot Record，主引导记录）中，而 Bootloader 则分散存放在 MBR 中、MBR 后面的几个扇区以及某个分区中。\n为了方便讲解，我以 GNU GRUB 2 作为 Bootloader 时的磁盘地址空间来讲解，以下是通过 GRUB 引导的计算机的磁盘地址空间示意图 [2]：\n图 10 GRUB 在 BIOS 模式下的磁盘地址空间 Example 1: MBR 部分存放了 GRUB 的第一组成部分 —— boot.img，然后 boot.img 在执行时会把位于 MBR 后面几个扇区的 core.img 加载进内存并跳转执行，而 core.img 则作为 GRUB 的第二组成成分，从后面的分区（图中为 /dev/sda2 分区）中可选的加载其他 GRUB 模块，从而完成 GRUB 作为 Bootloader 的完整功能。 Example 2: MBR 依旧存放了 boot.img，但是该设备采用 GPT 分区表，MBR 后面的几个扇区是有有效数据的，此时 GRUB 就不能把 core.img 存放在这里了，而是必须在分区表里添加一个特殊的 BIOS Boot Partition 分区来存放 core.img，GRUB 在执行时会把 core.img 从 BIOS Boot Partition 分区中加载进内存并跳转执行，之后就和 Example 1 的流程一样了。 通常，采用传统 BIOS 方案都是因为设备过于老旧，不支持 UEFI，所以也不太可能会采用 GPT 分区表（除非是从新电脑拆下来的用的）。因此在这种情况下，根据 Arch Wiki 的建议，我们的分区策略通常是：\n表 1 BIOS 方案的分区示例 Mount Point Partition Partition Type Suggested Size [swap] /dev/sda1 Linux swap 建议和内存大小相同 / /dev/sda2 Linux File System 设备剩余部分 需要注意的是，在使用 fdisk 进行分区时，磁盘的前 2048 个扇区（即前 1 MiB）通常会保留从而禁止被分配空间，这是因为 GRUB 的 boot.img 和 core.img 都需要存放在这里。你可以通过 fdisk -l 指令查看最终分区情况，比如在我装有 GNU/Linux Debian 系统的采用传统 BIOS 方案的设备上，输出情况如下所示：\n$ fdisk -l\nDisk /dev/sda: 447.14 GiB, 480113590272 bytes, 937721856 sectors\nDisk model: WDC WDS480G2G0B-\nUnits: sectors of 1 * 512 = 512 bytes\nSector size (logical/physical): 512 bytes / 512 bytes\nI/O size (minimum/optimal): 512 bytes / 512 bytes\nDisklabel type: dos\nDisk identifier: 0x400c0e55\nDevice Boot Start End Sectors Size Id Type\n/dev/sda1 * 2048 935720959 935718912 446.2G 83 Linux\n/dev/sda2 935723006 937719807 1996802 975M 5 Extended\n/dev/sda5 935723008 937719807 1996800 975M 82 Linux swap / Solaris 可以看到前 2048 个扇区被保留了，之后的分区都是从 2048 开始分配的。\n你可以会好奇，Extended 分区有什么用？这个主要是 MBR 分区表的一个限制，MBR 分区表最多只能支持 4 个主分区（Primary Partition），如果你需要更多的分区，就需要把其中一个主分区设置为 Extended 分区，然后在 Extended 分区中创建逻辑分区（Logical Partition）来使用，不过在这里使用 Extended 分区主要还是 Debian 系统安装器默认行为的历史原因，不必过于纠结于此了。\nUEFI 在 UEFI 模式下，磁盘的分区信息则存放在紧邻 MBR 后面的 GPT（GUID Partition Table，GUID 分区表）中，而 Bootloader 则存放在一个特殊的分区中，即 ESP（EFI System Partition，EFI 系统分区）中。\n同样，为了方便讲解，我以 GNU GRUB 2 作为 Bootloader 时的磁盘地址空间来讲解，以下是通过 GRUB 引导的计算机的磁盘地址空间示意图 [2]：\n图 11 GRUB 在 UEFI 模式下的磁盘地址空间 Example 1: MBR 部分放置了 Protective MBR（保护性 MBR），它的作用是为了兼容旧的 BIOS 系统，防止 BIOS 系统误认为磁盘没有分区而进行错误的操作（在 BIOS 看来，这块磁盘就是一个全占满数据的磁盘）。GPT 分区表则存放在紧邻 MBR 后面的几个扇区中，而 GRUB 则把 bootx64.efi 等 EFI 可执行文件放在 ESP 分区中，在执行时由 UEFI 直接加载 bootx64.efi 文件来启动 GRUB。\n需要注意的是，EFI 分区实际上并没有 /boot 目录，而是因为安装系统时通常都会把 ESP 分区挂载到 /boot 目录下，所以在进入系统后 /boot/grub 目录实际上是 ESP 分区中的 /grub 目录。 不同于传统 BIOS 方案，UEFI 方案下，磁盘还需要额外分配一个 ESP 分区来存放 Bootloader，因此在这种情况下，根据 Arch Wiki 的建议，我们的分区策略通常是：\n表 2 UEFI 方案的分区示例 Mount Point Partition Partition Type Suggested Size /boot /dev/sda1 EFI System 1 GiB [swap] /dev/sda2 Linux swap 建议和内存大小相同 / /dev/sda3 Linux File System 设备剩余部分 EFI 分区我的建议是大一点比较好，比如 1 GiB 就足够了，因为如果你之后想装双系统，或者多内核，其他系统的 Bootloader 和各个版本的 Kernel 文件也需要放在这个分区，如果分区太小了，只能通过缩减 swap 分区来给 ESP 分区腾出空间了，强迫症要难受死了。\n比如在我的装有 Arch Linux 系统的采用 UEFI 方案的设备上，fdisk -l 输出情况如下所示：\n$ fdisk -l\nDisk /dev/nvme1n1: 953.87 GiB, 1024209543168 bytes, 2000409264 sectors\nDisk model: SAMSUNG MZVL21T0HCLR-00BL2\nUnits: sectors of 1 * 512 = 512 bytes\nSector size (logical/physical): 512 bytes / 512 bytes\nI/O size (minimum/optimal): 512 bytes / 512 bytes\nDisklabel type: gpt\nDisk identifier: B8FF745C-1A82-4238-A89A-DB9F81FF075E\nDevice Start End Sectors Size Type\n/dev/nvme1n1p1 2048 2099199 2097152 1G EFI System\n/dev/nvme1n1p2 2099200 34142207 32043008 15.3G Linux swap\n/dev/nvme1n1p3 34142208 2000408575 1966266368 937.6G Linux filesystem 格式化和挂载分区 这部分对应 Arch Wiki 的 1.10 - 1.11 和 3.1 部分。\n创建分区后，第一件事就是要对分区进行格式化（Format），也就是在分区上创建一个文件系统（File System），因为分区只是划分了一个磁盘区域，至于文件是如何在磁盘上存储的，还需要通过文件系统来定义和管理，常见的文件系统有 ext4、FAT32、NTFS 等，不同的文件系统有不同的特点和适用场景。\n创建完文件系统，我们还需要挂载（Mount）分区，也就是按照一定的目录结构把分区连接到系统的文件系统树（File System Tree）上，这样我们才能通过路径（Path）来访问分区中的文件。一个分区可以挂载到多个路径上，也可以只挂载到一个路径上，挂载到的路径称之为挂载点（Mount Point），比如我们通常会把 Linux File System 分区挂载到根目录（/）上，而把 EFI System 分区挂载到 /boot 目录上。\n在 Linux 系统的启动过程中，Linux 会根据 /etc/fstab (i.e. File System Table) 文件中的配置来自动挂载分区，这个文件中定义了每个分区的设备路径、挂载点、文件系统类型以及挂载选项等信息，Linux 会根据这些信息来正确地挂载分区，从而保证系统的正常运行。比如，在我的 Arch Linux 系统中，/etc/fstab 文件的内容如下所示：\n$ cat /etc/fstab\n# /dev/nvme1n1p3 LABEL=arch-root\nUUID=eea8de7e-b37f-4b3b-b530-1003eeab9746 / btrfs rw,relatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=/@ 0 0\n# /dev/nvme1n1p3 LABEL=arch-root\nUUID=eea8de7e-b37f-4b3b-b530-1003eeab9746 /home btrfs rw,relatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=/@home 0 0\n# /dev/nvme1n1p1\nUUID=58AC-071E /boot vfat rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,errors=remount-ro0 2\n# /dev/nvme1n1p2\nUUID=c06f27fb-de0e-438d-9162-135e9cb735fd none swap defaults 0 0 由于我使用的是 GPT 分区方案，因此每个分区都有一个 UUID（Universally Unique Identifier，通用唯一标识符），Linux 会根据这个 UUID 来识别和挂载分区，这样即使分区的设备路径发生了变化（比如从 /dev/sda1 变成了 /dev/sdb1），Linux 仍然能够正确地挂载分区。\n我们可以使用 genfstab 命令来根据目前的挂载情况自动生成 /etc/fstab 文件，不需要手动编写，比如，在我的 Arch Linux 系统中，执行 genfstab 命令的结果如下所示：\n$ genfstab -U /\n# /dev/nvme1n1p3 LABEL=arch-root\nUUID=eea8de7e-b37f-4b3b-b530-1003eeab9746 / btrfs rw,relatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=/@ 0 0\n# kio-fuse\nkio-fuse /run/user/1000/kio-fuse-QpczeW fuse.kio-fuse rw,nosuid,nodev,user_id=1000,group_id=1000 0 0\n# /dev/nvme1n1p3 LABEL=arch-root\nUUID=eea8de7e-b37f-4b3b-b530-1003eeab9746 /home btrfs rw,relatime,compress=zstd:3,ssd,discard=async,space_cache=v2,subvol=/@home 0 0\n# bilibili.AppImage\nbilibili.AppImage /tmp/.mount_bilibiJnd6TL fuse.bilibili.AppImage ro,nosuid,nodev,user_id=1000,group_id=1000 0 0\n# /dev/nvme1n1p1\nUUID=58AC-071E /boot vfat rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=ascii,shortname=mixed,utf8,errors=remount-ro0 2\n# /dev/nvme1n1p2\nUUID=c06f27fb-de0e-438d-9162-135e9cb735fd none swap defaults 0 0 安装过程 在这一步骤开始前，你应该已经将一块磁盘划分好了分区，并格式化和挂载了分区，也就是说，你现在可以开始向磁盘正常写入文件了。\n软件包 这部分对应 Arch Wiki 的 2.1 - 2.2 部分。\n这部分就是安装一个能够基本使用的系统的过程。在这个部分之前，我们已经配置好每个分区以及每个分区的文件系统了，但是分区里没有任何文件，因此这个部分就是将我们向创建好的分区里安装系统最基本的软件的过程了，比如安装包管理器（在 Arch Linux 中是 pacman）以及一些基本的软件包（比如 base），最重要的就是安装 Linux Kernel 及其固件驱动了（即 linux 和 linux-firmware）从而让 Bootloader 能够找到并加载 Kernel 来启动系统。\n这部分和计算机启动过程关系不大，因此不做过多讲解。\n配置系统 在这一步骤开始前，你应该已经成功在磁盘上安装好了组成一个系统的基本软件包，但是只是缺乏一些配置让它正常 work 起来。\nChroot 这部分对应 Arch Wiki 的 3.2 部分。\nchroot 本质上是一个 Linux 系统调用（System Call），它的唯一作用就是改变当前进程的根目录到指定的路径上，也就好像进入到了已经安装好的系统里了一样（虽然仍有差别）。\n一些杂项 这部分对应 Arch Wiki 的 3.3 - 3.5 和 3.7 部分。\n这部分主要就是时区、语言、网络等和计算机启动过程关系不大的内容了，更多是的个性化的配置，这里就不展开讲解了。\nInitramfs 这部分对应 Arch Wiki 的 3.6 部分。\nInitramfs 是 Linux 内核在启动前必须加载进内存的一个临时的文件系统，里面存放着一些必要的驱动程序和工具，从而让内核能够正确识别和挂载根文件系统，从而完成系统的启动过程。\n不过根据 Arch Wiki 的所说，在使用 pacstrap 的时候，pacman 已经自动构建了 initramfs 了，因此我们大部分情况不需要手动构建 initramfs 了。\nBootloader 这部分对应 Arch Wiki 的 3.8 部分。\n虽然这一步在 Arch Wiki 里只有短短一行，但是这一步反而是整个配置系统过程中最重要的一步了，即选择并安装一个合适的 Bootloader 来引导系统的启动。\n通常，绝大多数的 Linux 发行版都将 GRUB 作为默认的 Bootloader，因此接下来我将讲述 GRUB 的安装和配置过程。\n比如，在我的 Arch Linux 系统中，安装和配置 GRUB 的过程如下所示：\npacman -Sy grub\ngrub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=arch --modules=\"tpm\" --disable-shim-lock\ngrub-mkconfig -o /boot/grub/grub.cfg 首先，我使用 grub-install 指令用于将 GRUB 安装到 /boot 目录下，并且以 arch 作为 Bootloader 标识符（放在 /boot/EFI/arch 目录下），同时我还启用了 TPM 模块来支持 Secure Boot 功能。然后，我使用 grub-mkconfig 指令在自动发现当前计算机上安装的所有系统，从而生成 GRUB 的配置文件放在 /boot/grub/grub.cfg 中，其中包含了 Arch Linux 系统的启动项以及其他系统的启动项（如果有的话）以便进入 GRUB 的启动界面时能够直接选择进入哪个系统，而不需要手动输入命令来加载模块和引导 Kernel。\n重启 在这一步骤开始前，你应该已经完成了整个系统的安装和配置，现在只需要重启计算机，计算机就会首先在 Firmware 阶段进行自检和初始化，然后加载你刚刚安装好的 GRUB 作为 Bootloader，在你选择了 Arch Linux 的启动项后，GRUB 就会加载 Arch Linux 的 Kernel 从而启动系统。\n总结 到此，我们可以回顾一下 Arch Linux 的安装过程来总结一下计算机的启动过程：\nFirmware 阶段：我们实际上在安装过程中几乎完全没有接触和配置过这部分，这是显然的，因为 Firmware，即固件，通常是由计算机厂商预装在主板上的 ROM 芯片中的，我们只能通过进入 BIOS 设置界面来查看和修改一些基本的设置（比如启动顺序、Secure Boot 等），但是我们无法直接修改 Firmware 的代码或者功能。 Bootloader 阶段：我们首先在安装前的准备阶段给 Bootloader 留好了 MBR 和 ESP 分区来存放 Bootloader 的文件，然后在配置系统阶段的最后安装了 GRUB 作为 Bootloader，并且生成了 GRUB 的配置文件来让 GRUB 能够正确地引导系统的启动。 Kernel 阶段：我们首先在安装前的准备阶段给 Kernel 留好了 Linux File System 分区和 ESP 分区来存放 Kernel 的文件，然后在安装过程阶段通过 pacstrap 安装了 Linux Kernel 以及相关的固件驱动，从而让 Bootloader 能够找到并加载 Kernel 来启动系统。 以上就是本文的全部内容了，希望能够帮助你更好地理解计算机的启动过程，以及在安装系统时每个步骤背后的原理和细节。\n第一次写这种技术性的文章，感觉写得有点啰嗦了，而且有点缺乏章法，但毕竟是第一次写，希望以后能有所改进！\nTable of Contents\n前言\n背景\n基本流程\n一些示例\n尝试一下\n讲解\n安装前的准备\n准备镜像\n进入 Live System\n一些杂项\n磁盘分区\nBIOS\nUEFI\n格式化和挂载分区\n安装过程\n软件包\n配置系统\nChroot\n一些杂项\nInitramfs\nBootloader\n重启\n总结\n参考文献\n参考文献 [1] Microsoft, Windows 11 Recovery Environment Boot menu. Microsoft, 2021. 见于: 2026年3月31日. [在线].\u0026#x20;\u0026#x20;载于: https://commons.wikimedia.org/wiki/File:Windows_11_RE_Boot_menu.png [2] Wikipedia contributors, GNU GRUB components. 见于: 2026年3月31日. [在线]. 载于: https://en.wikipedia.org/wiki/File:GNU_GRUB_components.svg [3] Arch Linux contributors, 《Installation guide》. 见于: 2026年3月31日. [在线]. 载于: https://wiki.archlinux.org/title/Installation_guide ","permalink":"https://blog.vertsineu.top/posts/boot-process/","summary":"\u003ch2 id=\"loc-1\"\u003e前言\u003c/h2\u003e\n    \u003cp\u003e从大二上计算机组成原理的时候我就想写一篇这样的文章了，当时我调研了 x86 架构平台的计算机的启动过程，意识到很多人实际上并不是非常了解一台计算机从上电到进入桌面的全过程。\u003c/p\u003e\n    \u003cp\u003e虽然网上有很多关于计算机启动过程的文章，结合了具体的系统，甚至制作了精美的动画，但是我觉得它们为了讲解的清晰，往往省略了实际执行的指令细节，导致\u003cstrong\u003e理解启动原理\u003c/strong\u003e和\u003cstrong\u003e实际安装系统\u003c/strong\u003e之间仍存在一定的距离。\u003c/p\u003e\n    \u003cp\u003e因此，本文我将以 x86 架构为例，结合 Arch Linux 系统的安装过程来讲解计算机的启动过程。\u003c/p\u003e\n    \u003ch2 id=\"loc-2\"\u003e背景\u003c/h2\u003e\n    \u003ch3 id=\"loc-3\"\u003e基本流程\u003c/h3\u003e\n    \u003cp\u003e虽然很多文章已经讲解过计算机的启动过程了，但是为了便于不了解计算机原理的读者理解，我还是想先介绍一些背景知识。\u003c/p\u003e\n    \u003cp\u003e除了某些嵌入式（Embedded）系统以外，大多数计算机都采取类似的启动过程，如下图所示：\u003c/p\u003e\n    \u003cp class=\"typst-parbreak\"\u003e\u003c/p\u003e\n    \u003cdiv style=\"display: grid; place-items: start center;\"\u003e\n      \u003cfigure\u003e\n        \u003cdiv style=\"display: grid; place-items: start center;\"\u003e\n          \u003csvg class=\"typst-frame\" style=\"overflow: visible; width: 9.19em; height: 20.616666666666664em;\" viewBox=\"0 0 124.065 278.32499999999993\" width=\"124.065pt\" height=\"278.32499999999993pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:h5=\"http://www.w3.org/1999/xhtml\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 62.0325 0)\" d=\"M 0 0l 0.0000000000000125883555 51.676 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 58.83910095064905 49.13130272881901)\" d=\"M 0 0m 3.193399 2.5446975 c -0.60091305 -1.3064789 -1.7857403 -2.2506223 -3.193399 -2.5446975 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 62.0324999527559 49.13130272881901)\" d=\"M 0 0m 0 2.5446975 c 0.6009131 -1.3064789 1.7857403 -2.2506223 3.193399 -2.5446975 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" transform=\"matrix(1 0 0 1 31.724999999999998 18.777500047244107)\" d=\"M 0 0h 60.615 v 14.445 h -60.615 v -14.445 Z \"/\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\" transform=\"matrix(1 0 0 1 31.724999999999998 18.777499905511803)\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cpath class=\"typst-shape\" fill=\"#ffffff\" fill-rule=\"nonzero\" d=\"M 0 0m 0 2.7 c 0 -1.4911689 1.2088312 -2.7 2.7 -2.7 h 55.215 c 1.491169 0 2.7000008 1.2088312 2.7000008 2.7 v 8.883 c 0 1.491169 -1.2088318 2.6999998 -2.7000008 2.6999998 h -55.215 c -1.4911689 0 -2.7 -1.2088308 -2.7 -2.6999998 Z \"/\u003e\u003cg class=\"typst-text\" transform=\"matrix(1 0 0 -1 2.7 11.583000000000002)\"\u003e\u003cuse xlink:href=\"#gC84BABEA16CFC2C9B6E94D8604C11F92\" x=\"0\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g66B3FFE8FD77A71BFB5A75024C692A24\" x=\"7.3035000000000005\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gBB96DE02A4869AB246787571C3F423F4\" x=\"14.013000000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"23.9895\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"30.024\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gAFDA912775647F4B463FFE7FC8594818\" x=\"38.42099999999999\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEB3D4C49293F171FAAE7E68DD078B8FF\" x=\"47.897999999999996\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 62.03249999999999 76.88300009448821)\" d=\"M 0 0v 49.676 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 58.83910095064905 124.0143026343308)\" d=\"M 0 0m 3.193399 2.5446975 c -0.60091305 -1.3064789 -1.7857403 -2.2506223 -3.193399 -2.5446975 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 62.0324999527559 124.0143026343308)\" d=\"M 0 0m 0 2.5446975 c 0.6009131 -1.3064789 1.7857403 -2.2506223 3.193399 -2.5446975 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" transform=\"matrix(1 0 0 1 6.1289999999999925 94.65375004724412)\" d=\"M 0 0h 111.807 v 14.4585 h -111.807 v -14.4585 Z \"/\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\" transform=\"matrix(1 0 0 1 6.128999999999992 94.65374990551186)\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cpath class=\"typst-shape\" fill=\"#ffffff\" fill-rule=\"nonzero\" d=\"M 0 0m 0 2.7 c 0 -1.4911689 1.2088312 -2.7 2.7 -2.7 h 106.407005 c 1.4911652 0 2.699997 1.2088312 2.699997 2.7 v 8.883 c 0 1.491169 -1.2088318 2.6999998 -2.699997 2.6999998 h -106.407005 c -1.4911689 0 -2.7 -1.2088308 -2.7 -2.6999998 Z \"/\u003e\u003cg class=\"typst-text\" transform=\"matrix(1 0 0 -1 2.7 11.583000000000002)\"\u003e\u003cuse xlink:href=\"#g5AD14113692EBBC239BED0B1617FA7DE\" x=\"0\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEB3D4C49293F171FAAE7E68DD078B8FF\" x=\"4.009499999999999\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gA3A542F9ECE112D8E9EBD5288545A624\" x=\"11.3265\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g8184CD22964601F1EFBC8918C2B9E191\" x=\"14.985\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gA3A542F9ECE112D8E9EBD5288545A624\" x=\"19.250999999999998\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEBA0C5F25388AD3EBB33675A2C6E5532\" x=\"22.909499999999998\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g7A780D07BCC785110F80F795B2F13EF1\" x=\"29.078999999999997\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gA3A542F9ECE112D8E9EBD5288545A624\" x=\"32.643\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g2CB2084E270C212A2299B5B43ED8B961\" x=\"36.301500000000004\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"42.02550000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDDB695F4F9025275AA29873E4F4F6908\" x=\"51.43500000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEBA0C5F25388AD3EBB33675A2C6E5532\" x=\"61.290000000000006\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"67.4595\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g34E19E4B46614BBC778112993C01A5F9\" x=\"72.3735\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gBB96DE02A4869AB246787571C3F423F4\" x=\"79.20450000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEBA0C5F25388AD3EBB33675A2C6E5532\" x=\"89.28900000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"95.45850000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"100.37250000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 62.03249999999999 151.76599999999996)\" d=\"M 0 0m 0.0000000000000125883555 0 l -0.0000000000000125883555 49.676 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 58.83910095064905 198.8973025398426)\" d=\"M 0 0m 3.193399 2.5446975 c -0.60091305 -1.3064789 -1.7857403 -2.2506223 -3.193399 -2.5446975 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 62.0324999527559 198.8973025398426)\" d=\"M 0 0m 0 2.5446975 c 0.6009131 -1.3064789 1.7857403 -2.2506223 3.193399 -2.5446975 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" transform=\"matrix(1 0 0 1 26.028000000000002 169.53674995275588)\" d=\"M 0 0h 72.009 v 14.4585 h -72.009 v -14.4585 Z \"/\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\" transform=\"matrix(1 0 0 1 26.027999999999995 169.5367498110236)\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cpath class=\"typst-shape\" fill=\"#ffffff\" fill-rule=\"nonzero\" d=\"M 0 0m 0 2.7 c 0 -1.4911689 1.2088312 -2.7 2.7 -2.7 h 66.609 c 1.4911728 0 2.7000046 1.2088312 2.7000046 2.7 v 8.883 c 0 1.491169 -1.2088318 2.6999998 -2.7000046 2.6999998 h -66.609 c -1.4911689 0 -2.7 -1.2088308 -2.7 -2.6999998 Z \"/\u003e\u003cg class=\"typst-text\" transform=\"matrix(1 0 0 -1 2.7 11.583000000000002)\"\u003e\u003cuse xlink:href=\"#gAA402DCEAF30BEC07BFD0C57BC93A44D\" x=\"0\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g66B3FFE8FD77A71BFB5A75024C692A24\" x=\"7.128000000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEBA0C5F25388AD3EBB33675A2C6E5532\" x=\"13.932000000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g34E19E4B46614BBC778112993C01A5F9\" x=\"20.1015\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g479805CE599260F4AE28642D73FFD716\" x=\"30.3075\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"38.637\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"44.6715\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEB3D4C49293F171FAAE7E68DD078B8FF\" x=\"49.6935\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"57.0105\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g7A780D07BCC785110F80F795B2F13EF1\" x=\"63.045\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 62.0325 226.6489999055118)\" d=\"M 0 0v 51.676 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 58.83910095064905 275.78030257133867)\" d=\"M 0 0m 3.193399 2.5446975 c -0.60091305 -1.3064789 -1.7857403 -2.2506223 -3.193399 -2.5446975 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.648\" stroke-linecap=\"round\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 62.0324999527559 275.78030257133867)\" d=\"M 0 0m 0 2.5446975 c 0.6009131 -1.3064789 1.7857403 -2.2506223 3.193399 -2.5446975 \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" transform=\"matrix(1 0 0 1 0 243.94149995275586)\" d=\"M 0 0h 124.065 v 17.415 h -124.065 v -17.415 Z \"/\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\" transform=\"matrix(1 0 0 1 0 243.94149981102362)\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cpath class=\"typst-shape\" fill=\"#ffffff\" fill-rule=\"nonzero\" d=\"M 0 0m 0 2.7 c 0 -1.4911689 1.2088312 -2.7 2.7 -2.7 h 118.665 c 1.4911728 0 2.7000046 1.2088312 2.7000046 2.7 v 8.883 c 0 1.491169 -1.2088318 2.6999998 -2.7000046 2.6999998 h -118.665 c -1.4911689 0 -2.7 -1.2088308 -2.7 -2.6999998 Z \"/\u003e\u003cg class=\"typst-text\" transform=\"matrix(1 0 0 -1 2.7 11.583000000000002)\"\u003e\u003cuse xlink:href=\"#gAA402DCEAF30BEC07BFD0C57BC93A44D\" x=\"0\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g66B3FFE8FD77A71BFB5A75024C692A24\" x=\"7.128000000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEBA0C5F25388AD3EBB33675A2C6E5532\" x=\"13.932000000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g34E19E4B46614BBC778112993C01A5F9\" x=\"20.1015\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gF374CFAFF5D6C47B1DAB0C2ADD3631F0\" x=\"30.3075\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gFE99B40F85E0333227B7A3AC71DADD12\" x=\"36.855000000000004\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEC499EBACB82676A3B91B470EA5FFF77\" x=\"43.807500000000005\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g8184CD22964601F1EFBC8918C2B9E191\" x=\"49.072500000000005\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"53.3385\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g1A22C9A3D561958A216F144A561E83A7\" x=\"59.373000000000005\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gF374CFAFF5D6C47B1DAB0C2ADD3631F0\" x=\"73.41300000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"79.96050000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"85.995\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gB9D34D5A11768E1BE112EBDB9F1A7471\" x=\"91.21950000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gA3A542F9ECE112D8E9EBD5288545A624\" x=\"97.92900000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g11F318E12978A97B3CFB940EC2702703\" x=\"101.58750000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"107.36550000000003\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEC499EBACB82676A3B91B470EA5FFF77\" x=\"113.40000000000002\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.5\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 29.4847499527559 54.00000009448818)\" d=\"M 0 0m 2 0 c -1.1035681 0 -2 0.896432 -2 2 v 16.883 c 0 1.103569 0.896432 2 2 2 h 61.0955 c 1.1035652 0 1.9999962 -0.89643097 1.9999962 -2 v -16.883 c 0 -1.1035681 -0.89643097 -2 -1.9999962 -2 h -61.0955 Z \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" transform=\"matrix(1 0 0 1 35.48474999995 59.999999999999986)\" d=\"M 0 0h 53.0955 v 8.883 h -53.0955 v -8.883 Z \"/\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\" transform=\"matrix(1 0 0 1 35.48474985826771 59.999999999999986)\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-text\" transform=\"matrix(1 0 0 -1 0.00000000005000072447069752 8.883000000000001)\"\u003e\u003cuse xlink:href=\"#g535F59953528118545BE8EBE6AE2E5A1\" x=\"0\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gA3A542F9ECE112D8E9EBD5288545A624\" x=\"6.5475\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"10.206\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g1A22C9A3D561958A216F144A561E83A7\" x=\"15.228\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gBB96DE02A4869AB246787571C3F423F4\" x=\"25.893\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEBA0C5F25388AD3EBB33675A2C6E5532\" x=\"35.9775\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"42.147\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"47.061\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.5\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 25.8667499527559 128.883)\" d=\"M 0 0m 2 0 c -1.1035681 0 -2 0.896432 -2 2 v 16.883 c 0 1.103569 0.896432 2 2 2 h 68.3315 c 1.103569 0 2 -0.89643097 2 -2 v -16.883 c 0 -1.1035681 -0.89643097 -2 -2 -2 h -68.3315 Z \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" transform=\"matrix(1 0 0 1 31.866749999949995 134.883)\" d=\"M 0 0h 60.3315 v 8.883 h -60.3315 v -8.883 Z \"/\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\" transform=\"matrix(1 0 0 1 31.86674985826771 134.883)\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-text\" transform=\"matrix(1 0 0 -1 0.00000000005000072447069752 8.883000000000001)\"\u003e\u003cuse xlink:href=\"#gAD39741236FED4321F15CC53E9BC832B\" x=\"0\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g66B3FFE8FD77A71BFB5A75024C692A24\" x=\"7.938\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g66B3FFE8FD77A71BFB5A75024C692A24\" x=\"14.836500000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g8184CD22964601F1EFBC8918C2B9E191\" x=\"21.640500000000003\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g7A780D07BCC785110F80F795B2F13EF1\" x=\"25.9065\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g66B3FFE8FD77A71BFB5A75024C692A24\" x=\"29.4705\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEBA0C5F25388AD3EBB33675A2C6E5532\" x=\"36.2745\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g34E19E4B46614BBC778112993C01A5F9\" x=\"42.444\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"49.275000000000006\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"55.30950000000001\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003cpath class=\"typst-shape\" fill=\"none\" stroke=\"#000000\" stroke-width=\"0.5\" stroke-linecap=\"butt\" stroke-linejoin=\"miter\" stroke-miterlimit=\"4\" transform=\"matrix(1 0 0 1 37.8817499527559 203.7659999055118)\" d=\"M 0 0m 2 0 c -1.1035681 0 -2 0.896432 -2 2 v 16.883 c 0 1.103569 0.896432 2 2 2 h 44.3015 c 1.103569 0 2 -0.89643097 2 -2 v -16.883 c 0 -1.1035681 -0.89643097 -2 -2 -2 h -44.3015 Z \"/\u003e\u003cpath class=\"typst-shape\" fill=\"none\" transform=\"matrix(1 0 0 1 43.88174999995 209.766)\" d=\"M 0 0h 36.3015 v 8.883 h -36.3015 v -8.883 Z \"/\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-group\" transform=\"matrix(1 0 0 1 43.88174985826771 209.76599999999996)\"\u003e\u003cg\u003e\u003cg class=\"typst-group\"\u003e\u003cg\u003e\u003cg class=\"typst-text\" transform=\"matrix(1 0 0 -1 0.00000000005000072447069752 8.883000000000001)\"\u003e\u003cuse xlink:href=\"#g479805CE599260F4AE28642D73FFD716\" x=\"0\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"8.3295\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gDAA60ED0A5626E912764ADA16A21ACFF\" x=\"14.363999999999999\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#gEB3D4C49293F171FAAE7E68DD078B8FF\" x=\"19.386\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g9D349179D2D0759E5AA6D20A10043897\" x=\"26.703\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003cuse xlink:href=\"#g7A780D07BCC785110F80F795B2F13EF1\" x=\"32.7375\" y=\"0\" fill=\"#000000\" fill-rule=\"nonzero\"/\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003c/g\u003e\u003cdefs id=\"glyph\"\u003e\u003csymbol id=\"gC84BABEA16CFC2C9B6E94D8604C11F92\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 1.404 7.0605 v -5.4135 c 0 -1.1205001 -0.22950006 -1.1880001 -1.1745 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.6075 0.0135 1.2689999 0.027 1.755 0.027 c 0.47249997 0 1.2689999 -0.0135 1.9440001 -0.027 c 0.08099985 0.081 0.08099985 0.36450002 0 0.44550002 c -1.0530002 0.05400002 -1.3770001 0.10799998 -1.3770001 1.2285001 v 2.295 c 0.29699993 -0.094500065 0.62100005 -0.13499999 1.0934999 -0.13499999 c 2.4570003 0 3.1995 1.6065001 3.1995 2.7540002 c 0 0.7964997 -0.52649975 2.2409997 -3.024 2.2409997 c -0.513 0 -1.3095 -0.09449959 -1.8495001 -0.09449959 c -0.49950004 0 -1.2015 0.013500214 -1.7415 0.026999474 c -0.081 -0.080999374 -0.081 -0.36450005 0 -0.44549942 c 0.945 -0.04050064 1.1745 -0.108000755 1.1745 -1.2285004 Z m 1.1475 0.41849995 c 0 0.3915 0.2025001 0.8640003 1.1745 0.8640003 c 0.9315002 0 1.8630002 -0.31050014 1.8630002 -2.025 c 0 -1.4580002 -0.70200014 -2.052 -2.0115001 -2.052 c -0.3375001 0 -0.87750006 0.02699995 -1.026 0.06749964 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g66B3FFE8FD77A71BFB5A75024C692A24\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 0.5535 2.7675002 c 0 -1.3770001 0.91800004 -2.9025002 2.835 -2.9025002 c 0.8640001 0 1.5255 0.31050003 1.9845002 0.756 c 0.6075001 0.59400004 0.87750006 1.4445 0.87750006 2.2680001 c 0 1.404 -0.76950026 3.0375001 -2.835 3.0375001 c -0.89100003 0 -1.6200001 -0.36450005 -2.1195002 -0.9450002 c -0.486 -0.5805001 -0.7425 -1.3635001 -0.7425 -2.214 Z m 2.6595001 2.6864998 c 1.161 0 1.8764999 -1.053 1.8764999 -2.997 c 0 -1.701 -0.87750006 -2.1195 -1.5119998 -2.1195 c -1.404 0 -1.863 1.7010001 -1.863 2.7405 c 0 1.1745 0.28349996 2.376 1.4985 2.376 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gBB96DE02A4869AB246787571C3F423F4\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.7405 5.373 c 0.08100009 0.08099985 0.08100009 0.36450005 0 0.4454999 c -0.40499997 -0.013499737 -0.87749994 -0.02699995 -1.3634999 -0.02699995 c -0.5130001 0 -0.87750006 0.013500214 -1.2285001 0.02699995 c -0.08100001 -0.08099985 -0.08100001 -0.36450005 0 -0.4454999 c 0.63449997 -0.067500114 0.783 -0.20249987 1.0665001 -0.9045 l 1.7685 -4.3605003 c 0.08100009 -0.18900001 0.18900013 -0.27 0.32400012 -0.27 c 0.121500015 0 0.22950006 0.081 0.32399988 0.297 l 1.3635004 3.321 l 1.3904996 -3.348 c 0.08100033 -0.18900001 0.18900013 -0.27 0.32400036 -0.27 c 0.121500015 0 0.22949982 0.081 0.31049967 0.28350002 l 1.7684999 4.2795 c 0.21600056 0.5535002 0.44550037 0.93149996 1.1340008 0.9720001 c 0.080999374 0.08099985 0.080999374 0.36450005 0 0.4454999 c -0.27000046 -0.013499737 -0.5670004 -0.02699995 -0.9720001 -0.02699995 c -0.4050007 0 -0.9720001 0.013500214 -1.3770003 0.02699995 c -0.08099985 -0.08099985 -0.08099985 -0.36450005 0 -0.4454999 c 1.0125003 -0.0539999 0.87750006 -0.4454999 0.6884999 -0.9180002 l -1.1339998 -2.8079998 c -0.13500023 -0.3375001 -0.18900013 -0.3375001 -0.29699993 -0.05400002 l -1.1610003 2.9565 c -0.25649977 0.6345 -0.22949982 0.76950026 0.48600006 0.82350016 c 0.08099985 0.08099985 0.08099985 0.36450005 0 0.4454999 c -0.4050002 -0.013499737 -0.9450002 -0.02699995 -1.3499999 -0.02699995 c -0.3375001 0 -0.783 0.013500214 -1.1880002 0.02699995 c -0.08099985 -0.08099985 -0.08099985 -0.36450005 0 -0.4454999 c 0.6345 -0.0539999 0.7965002 -0.4454999 1.0395002 -1.053 l 0.08099985 -0.21600008 l -0.94499993 -2.3895001 c -0.17550015 -0.44550002 -0.2025001 -0.459 -0.37800002 -0.02700007 l -1.0800002 2.7810001 c -0.2565 0.64799976 -0.24300003 0.86399984 0.40499997 0.9045 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g9D349179D2D0759E5AA6D20A10043897\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 5.211 1.2555001 c -0.4994998 -0.5130001 -0.8909998 -0.7290001 -1.6739998 -0.7290001 c -0.48600006 0 -1.0530002 0.28350002 -1.4715002 0.972 c -0.26999998 0.44550002 -0.43199992 1.0665001 -0.43199992 1.8495001 l 3.591 -0.02699995 c 0.16200018 0 0.25649977 0.08100009 0.25649977 0.22950006 c 0 1.1340001 -0.40499973 2.3490002 -2.2814999 2.3490002 c -1.1745 0 -2.7 -1.1205001 -2.7 -3.1725004 c 0 -0.7559999 0.18900004 -1.485 0.6345 -1.9979999 c 0.459 -0.54 1.0934999 -0.864 2.0655 -0.864 c 1.026 0 1.7550001 0.47250003 2.295 1.1745 c -0.040500164 0.13499999 -0.121500015 0.20249999 -0.2835002 0.21600008 Z m -3.537 2.5515 c 0.2565 1.5254998 1.2014999 1.6469998 1.5255 1.6469998 c 0.513 0 1.1205001 -0.28349972 1.1205001 -1.4175 c 0 -0.12149978 -0.0539999 -0.18899989 -0.20249987 -0.18899989 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gDAA60ED0A5626E912764ADA16A21ACFF\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.3760002 4.833 c -0.027000189 0.53999996 -0.040500164 0.8909998 -0.10800004 1.026 c -0.02699995 0.067500114 -0.05400014 0.1079998 -0.16200018 0.1079998 c -0.3779999 -0.14849997 -0.72899985 -0.26999998 -1.6604999 -0.3915 c -0.02700001 -0.08099985 0 -0.29699993 0.02700001 -0.37799978 c 0.72900003 -0.067500114 0.8775 -0.13500023 0.8775 -0.9180002 v -2.6325 c 0 -1.1205001 -0.16199994 -1.1745 -0.999 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.47250003 0.0135 0.999 0.027 1.539 0.027 c 0.53999996 0 1.161 -0.0135 1.6335001 -0.027 c 0.08099985 0.081 0.08099985 0.36450002 0 0.44550002 c -0.9450002 0.067499995 -1.1070001 0.10799998 -1.1070001 1.2285001 v 1.8765001 c 0 0.35099983 0.16199994 0.66149974 0.32399988 0.90449977 c 0.1485002 0.21600008 0.4590001 0.6615 0.62100005 0.6615 c 0.121500015 0 0.24300003 -0.02699995 0.35100007 -0.17549992 c 0.094500065 -0.13499975 0.2565 -0.31050014 0.48600006 -0.31050014 c 0.32399988 0 0.6345 0.3375001 0.6345 0.6750002 c 0 0.25650024 -0.24300003 0.64800024 -0.80999994 0.64800024 c -0.63450027 0 -1.1880002 -0.59400034 -1.4985001 -1.1205001 c -0.08100009 -0.14849997 -0.14849997 -0.040500164 -0.14849997 0.02699995 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gAFDA912775647F4B463FFE7FC8594818\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 8.991 4.4415 c 0 2.7944999 -1.9305 4.4415 -4.347 4.4415 c -2.619 0 -4.1445003 -2.079 -4.1445003 -4.6980004 c 0 -2.6595 1.9305 -4.32 4.2255 -4.32 c 1.4985003 0 2.6865 0.648 3.4290004 1.701 c 0.53999996 0.7695 0.8369999 1.7415001 0.8369999 2.8755002 Z m -4.5225 3.9555001 c 1.7820001 0 3.2265 -1.4850001 3.2265 -4.2120004 c 0 -2.4164999 -1.1609998 -3.8339999 -2.6999998 -3.8339999 c -1.6470003 0 -3.1995003 1.4714999 -3.1995003 4.0635004 c 0 2.835 1.3364999 3.9825 2.673 3.9825 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gEB3D4C49293F171FAAE7E68DD078B8FF\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.484 4.833 c -0.08099985 -0.094500065 -0.16199994 -0.121500015 -0.16199994 0 c -0.013499975 0.36450005 -0.040499926 0.8909998 -0.10800004 1.026 c -0.02699995 0.067500114 -0.0539999 0.1079998 -0.16199994 0.1079998 c -0.37800002 -0.14849997 -0.729 -0.26999998 -1.6605 -0.3915 c -0.02699998 -0.08099985 0 -0.29699993 0.02700001 -0.37799978 c 0.72900003 -0.067500114 0.8775 -0.13500023 0.8775 -0.9180002 v -2.6325 c 0 -1.1070001 -0.13499999 -1.161 -0.945 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.40500003 0.0135 0.945 0.027 1.4850001 0.027 c 0.5400001 0 0.94500005 -0.0135 1.35 -0.027 c 0.08100009 0.081 0.08100009 0.36450002 0 0.44550002 c -0.6884999 0.067499995 -0.82350016 0.121500015 -0.82350016 1.2285001 v 2.214 c 0 0.2835002 0.121500015 0.4454999 0.22950006 0.5669999 c 0.513 0.49950027 1.1205001 0.7965002 1.6470003 0.7965002 c 0.26999998 0 0.5534997 -0.17549992 0.7154999 -0.48600006 c 0.13499975 -0.26999998 0.1619997 -0.6345 0.1619997 -1.0395 v -2.052 c 0 -1.1070001 -0.13499975 -1.161 -0.8369999 -1.2285001 c -0.067500114 -0.081 -0.067500114 -0.36450002 0 -0.44550002 c 0.4050002 0.0135 0.8369999 0.027 1.3770003 0.027 c 0.53999996 0 1.026 -0.0135 1.4309998 -0.027 c 0.067500114 0.081 0.067500114 0.36450002 0 0.44550002 c -0.75600004 0.067499995 -0.9045 0.121500015 -0.9045 1.2285001 v 2.0115001 c 0 0.7424998 -0.0539999 1.3905001 -0.36450005 1.809 c -0.22949982 0.29699993 -0.64799976 0.4590001 -1.1205001 0.4590001 c -0.6615 0 -1.4174998 -0.1755004 -2.214 -1.0935001 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g5AD14113692EBBC239BED0B1617FA7DE\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.5785 1.6470001 v 5.4135 c 0 1.1204996 0.22950006 1.1879997 1.1745 1.2285004 c 0.08100009 0.080999374 0.08100009 0.36450005 0 0.44549942 c -0.59399986 -0.01349926 -1.2284999 -0.026999474 -1.755 -0.026999474 c -0.44550002 0 -1.107 0.013500214 -1.7415 0.026999474 c -0.081 -0.080999374 -0.081 -0.36450005 0 -0.44549942 c 0.94500005 -0.04050064 1.1745 -0.108000755 1.1745 -1.2285004 v -5.4135 c 0 -1.1205001 -0.22949994 -1.1880001 -1.1745 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.6075 0.0135 1.269 0.027 1.7550001 0.027 c 0.48600006 0 1.1340001 -0.0135 1.7414999 -0.027 c 0.08100009 0.081 0.08100009 0.36450002 0 0.44550002 c -0.94499993 0.040500015 -1.1745 0.10799998 -1.1745 1.2285001 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gA3A542F9ECE112D8E9EBD5288545A624\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.4435 1.6470001 v 2.6864998 c 0 0.6750002 0.05400014 1.539 0.05400014 1.539 c 0 0.054000378 -0.067500114 0.094500065 -0.17550015 0.094500065 c -0.37800002 -0.14849997 -0.918 -0.26999998 -1.8495 -0.3915 c -0.02700001 -0.08099985 0 -0.29699993 0.02699998 -0.37799978 c 0.74249995 -0.067500114 0.87750006 -0.14849997 0.87750006 -0.9180002 v -2.6325 c 0 -1.1205001 -0.14850008 -1.161 -0.9720001 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.44550005 0.0135 0.9720001 0.027 1.5120001 0.027 c 0.53999996 0 1.053 -0.0135 1.4985001 -0.027 c 0.08099985 0.081 0.08099985 0.36450002 0 0.44550002 c -0.82350016 0.05400002 -0.9720001 0.10799998 -0.9720001 1.2285001 Z m -1.2285 6.4395 c 0 -0.35099983 0.324 -0.70200014 0.648 -0.70200014 c 0.37800014 0 0.702 0.36450005 0.702 0.64800024 c 0 0.32400036 -0.28349996 0.70199966 -0.648 0.70199966 c -0.324 0 -0.702 -0.3239994 -0.702 -0.64799976 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g8184CD22964601F1EFBC8918C2B9E191\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 0.5805 5.7915 c -0.18900001 0 -0.243 -0.16200018 -0.243 -0.26999998 v -0.17549992 c 0 -0.067500114 0.013500005 -0.08099985 0.067499995 -0.08099985 h 0.7965001 v -4.0635004 c 0 -0.9585 0.41849995 -1.3365 1.0395001 -1.3365 c 0.6209998 0 1.296 0.297 1.8224998 0.89100003 c -0.02699995 0.13499999 -0.1079998 0.21599996 -0.24299979 0.2295 c -0.35100007 -0.27000004 -0.75600004 -0.37800002 -1.1070001 -0.37800002 c -0.36450005 0 -0.4454999 0.40500003 -0.4454999 1.2420001 v 3.4155002 h 1.404 c 0.13499999 0 0.32399988 0.0539999 0.32399988 0.17549992 v 0.26999998 c 0 0.0539999 -0.040499926 0.08099985 -0.10800004 0.08099985 h -1.6199999 v 0.5265002 c 0 0.87750006 0.0539999 1.4175 0.0539999 1.4175 c 0 0.08099985 -0.040499926 0.121500015 -0.10800004 0.121500015 c -0.0539999 0 -0.17549992 -0.054000378 -0.29699993 -0.121500015 c -0.14849997 -0.08100033 -0.28349996 -0.14849997 -0.459 -0.18900013 c -0.16200006 -0.0539999 -0.29700005 -0.094500065 -0.29700005 -0.18900013 c 0 -0.1619997 0.040500045 -0.067500114 0.040500045 -1.566 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gEBA0C5F25388AD3EBB33675A2C6E5532\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 3.9555001 0.648 c 0.08099985 -0.4185 0.22949982 -0.783 0.9045 -0.783 c 0.513 0 0.9990001 0.22950001 1.2824998 0.49950004 c -0.02699995 0.16199997 -0.08099985 0.2835 -0.22949982 0.36450002 c -0.094500065 -0.08100003 -0.32399988 -0.21600002 -0.4994998 -0.21600002 c -0.3915 0 -0.4050002 0.5265 -0.4050002 1.1475 v 1.9844999 c 0 1.9170003 -1.053 2.2815003 -2.0385 2.2815003 c -1.107 0 -2.2275 -0.7290001 -2.2275 -1.4985003 c 0 -0.32399988 0.162 -0.48599982 0.47250003 -0.48599982 c 0.3915 0 0.6345 0.28349996 0.6345 0.45899987 c 0 0.094500065 -0.013499975 0.18900013 -0.040500045 0.24300003 c -0.013499975 0.040500164 -0.02699995 0.121500015 -0.02699995 0.26999998 c 0 0.41849995 0.5669999 0.5669999 1.0799999 0.5669999 c 0.4590001 0 1.0935001 -0.22949982 1.0935001 -1.7549999 c 0 -0.094500065 -0.040499926 -0.14849997 -0.08100009 -0.16199994 l -1.161 -0.28349996 c -1.296 -0.32400012 -2.2275 -1.0395 -2.2275 -1.9575001 c 0 -1.1070001 0.756 -1.4580001 1.701 -1.4580001 c 0.4725001 0 0.87750006 0.108 1.4715002 0.56700003 l 0.26999998 0.21599999 Z m 0 2.4975002 v -1.7820002 c 0 -0.17549992 -0.08100009 -0.26999998 -0.18900013 -0.35099995 c -0.35099983 -0.28350002 -0.80999994 -0.59400004 -1.188 -0.59400004 c -0.67499995 0 -0.972 0.54 -0.972 0.9585001 c 0 0.60749996 0.28350008 1.2284999 1.2825001 1.4849999 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g7A780D07BCC785110F80F795B2F13EF1\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 1.2825 1.6470001 c 0 -1.1205001 -0.14849997 -1.1880001 -0.972 -1.2285001 c -0.080999985 -0.081 -0.080999985 -0.36450002 0 -0.44550002 c 0.4725 0.0135 0.972 0.027 1.512 0.027 c 0.53999996 0 1.053 -0.0135 1.4985001 -0.027 c 0.08100009 0.081 0.08100009 0.36450002 0 0.44550002 c -0.8234999 0.040500015 -0.9720001 0.10799998 -0.9720001 1.2285001 v 6.2235003 c 0 0.87750006 0.05400014 1.4175 0.05400014 1.4175 c 0 0.09450054 -0.05400014 0.13500023 -0.17550015 0.13500023 c -0.33749986 -0.13500023 -1.3499999 -0.32400036 -1.89 -0.36450005 c -0.02700001 -0.1079998 0 -0.32400036 0.081 -0.40499973 c 0.78300005 -0.0539999 0.864 -0.09450054 0.864 -1.1070004 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g2CB2084E270C212A2299B5B43ED8B961\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 0.9585 5.9265003 c -0.02700001 0 -0.08100003 -0.054000378 -0.08100003 -0.08100033 c -0.013499975 -0.6075001 -0.08099997 -1.0799999 -0.20249999 -1.7414999 c 0.121500015 -0.0539999 0.28350002 -0.08099985 0.44550008 -0.040500164 c 0.26999998 0.98550034 0.6884999 1.2150002 1.0934999 1.2285004 l 1.7280002 0.040499687 c -0.918 -1.5389998 -2.2680001 -3.4829998 -3.321 -4.9005 c -0.121499985 -0.162 -0.121499985 -0.2025 -0.121499985 -0.27 c 0 -0.0945 0.10800001 -0.162 0.3105 -0.162 l 4.2120004 -0.0405 c 0.1619997 0.459 0.33749962 1.1610001 0.43199968 1.8360001 c -0.08099985 0.05400002 -0.24300003 0.08099997 -0.40499973 0.08099997 l -0.18900013 -0.3915 c -0.3375001 -0.7155 -0.6209998 -1.0125 -1.323 -1.026 h -1.7955002 c 0.99899995 1.269 2.4030004 3.3885 3.2400002 4.7655 c 0.20249987 0.3375001 0.24300003 0.4454999 0.24300003 0.4994998 c 0 0.054000378 -0.067500114 0.094500065 -0.16200018 0.094500065 c -0.06749964 0 -0.4454999 -0.02699995 -0.8369999 -0.02699995 h -2.4705 c -0.43199992 0 -0.594 0.067500114 -0.79649997 0.13500023 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gDDB695F4F9025275AA29873E4F4F6908\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 8.343 1.6470001 v 5.4135 c 0 1.1204996 0.22949982 1.1879997 1.1744995 1.2285004 c 0.08100033 0.080999374 0.08100033 0.36450005 0 0.44549942 c -0.5534992 -0.01349926 -1.2419996 -0.026999474 -1.7549996 -0.026999474 c -0.49950027 0 -1.1475 0.013500214 -1.7414999 0.026999474 c -0.08100033 -0.080999374 -0.08100033 -0.36450005 0 -0.44549942 c 0.9449997 -0.04050064 1.1745 -0.108000755 1.1745 -1.2285004 v -2.1599998 h -4.6170006 v 2.1599998 c 0 1.1204996 0.22950006 1.1879997 1.1745 1.2285004 c 0.08100009 0.080999374 0.08100009 0.36450005 0 0.44549942 c -0.513 -0.01349926 -1.0665 -0.026999474 -1.755 -0.026999474 c -0.67499995 0 -1.2285 0.013500214 -1.7415 0.026999474 c -0.081 -0.080999374 -0.081 -0.36450005 0 -0.44549942 c 0.94500005 -0.04050064 1.1745 -0.108000755 1.1745 -1.2285004 v -5.4135 c 0 -1.1205001 -0.22949994 -1.1880001 -1.1745 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.59400004 0.0135 1.242 0.027 1.7550001 0.027 c 0.48600006 0 1.1475 -0.0135 1.7414999 -0.027 c 0.08100009 0.081 0.08100009 0.36450002 0 0.44550002 c -0.94499993 0.040500015 -1.1745 0.10799998 -1.1745 1.2285001 v 2.6864998 h 4.6170006 v -2.6864998 c 0 -1.1205001 -0.2295003 -1.1880001 -1.1745 -1.2285001 c -0.08100033 -0.081 -0.08100033 -0.36450002 0 -0.44550002 c 0.6074996 0.0135 1.2554998 0.027 1.7549996 0.027 c 0.49950027 0 1.1475 -0.0135 1.7414999 -0.027 c 0.08100033 0.081 0.08100033 0.36450002 0 0.44550002 c -0.9449997 0.040500015 -1.1744995 0.10799998 -1.1744995 1.2285001 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g34E19E4B46614BBC778112993C01A5F9\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 4.5090003 0.675 c 0.06749964 0.05400002 0.18899965 0.08100003 0.20249987 -0.013499975 c 0.040500164 -0.32400003 0.14849997 -0.7965 0.14849997 -0.7965 c 0.1079998 -0.0405 0.17549992 -0.026999995 0.25649977 0 c 0.2970004 0.243 0.76950026 0.44550002 1.5930004 0.54 c 0.08099985 0.081 0.08099985 0.28350005 0 0.36450002 c -0.8640003 0.067499995 -0.98550034 0.324 -0.98550034 0.9855 v 6.1155 c 0 0.87750006 0.054000378 1.4175 0.054000378 1.4175 c 0 0.09450054 -0.054000378 0.13500023 -0.1755004 0.13500023 c -0.33749962 -0.13500023 -1.3499999 -0.32400036 -1.8899999 -0.36450005 c -0.02699995 -0.1079998 0 -0.32400036 0.08100009 -0.40499973 c 0.040499926 0 0.08099985 0 0.121500015 0 c 0.5940001 -0.04050064 0.74250007 -0.04050064 0.74250007 -1.1070004 v -1.7280002 c 0 -0.094500065 -0.02699995 -0.121500015 -0.121500015 -0.121500015 c -0.0539999 0 -0.6075001 0.2295003 -1.0530002 0.2295003 c -0.89100003 0 -1.485 -0.2970004 -2.025 -0.8100004 c -0.58050007 -0.58049965 -0.9315001 -1.3769999 -0.9315001 -2.376 c 0 -1.6604999 0.837 -2.8755 2.295 -2.8755 c 0.5265 0 1.026 0.27 1.6875002 0.81 Z m 0.14849997 0.999 c 0 -0.2565 -0.02699995 -0.36450005 -0.21600008 -0.5265 c -0.49950004 -0.43200004 -0.9315002 -0.648 -1.269 -0.648 c -0.7290001 0 -1.4850001 0.79649997 -1.4850001 2.484 c 0 0.9720001 0.18900001 1.5120001 0.3915 1.7955003 c 0.41850019 0.6345 0.9855001 0.6749997 1.2555001 0.6749997 c 0.48600006 0 0.8234999 -0.17549992 1.0934999 -0.48600006 c 0.18900013 -0.2159996 0.2295003 -0.31049967 0.2295003 -0.7289996 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gAA402DCEAF30BEC07BFD0C57BC93A44D\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.0115001 0 h 2.8755 c 0.36450005 0 1.6065001 -0.027 1.6065001 -0.027 c 0.13499975 0.675 0.26999998 1.5525 0.35099983 2.2545 c -0.13499975 0.067500114 -0.28349972 0.094500065 -0.4454999 0.067500114 c -0.26999998 -0.972 -0.76950026 -1.7685001 -2.2005 -1.7685001 h -0.8505001 c -0.5265 0 -0.7695 0.2565 -0.7695 0.94500005 v 5.589 c 0 1.1204996 0.22950006 1.1879997 1.1745 1.2285004 c 0.08100009 0.080999374 0.08100009 0.36450005 0 0.44549942 c -0.60749984 -0.01349926 -1.2419999 -0.026999474 -1.755 -0.026999474 c -0.48599994 0 -1.1205001 0.013500214 -1.7415 0.026999474 c -0.081 -0.080999374 -0.081 -0.36450005 0 -0.44549942 c 0.94500005 -0.04050064 1.1745 -0.108000755 1.1745 -1.2285004 v -5.4135 c 0 -1.1205001 -0.22949994 -1.1880001 -1.1745 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.5265 0.0135 1.296 0.027 1.7550001 0.027 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g479805CE599260F4AE28642D73FFD716\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.5785 1.6470001 v 2.5245001 c 0.4590001 -0.013500214 0.8505001 -0.16200018 1.1205001 -0.513 l 2.1195 -2.673 c 0.26999998 -0.351 0.4590001 -0.7155 0.48600006 -0.9315 c 0 -0.0405 0.013500214 -0.081 0.067500114 -0.081 c 0.26999998 0.0135 0.4454999 0.027 0.7290001 0.027 c 0.4590001 0 0.91799974 -0.0135 1.3635001 -0.027 c 0.080999374 0.081 0.080999374 0.36450002 0 0.44550002 c -0.39150047 0.040500015 -0.76950026 0.081 -1.1880002 0.56700003 l -2.9565 3.51 c -0.10800028 0.121500015 -0.17549992 0.22949982 -0.17549992 0.3375001 c 0 0.094500065 0.013499737 0.18900013 0.47249985 0.6615 l 1.9844999 2.0384998 c 0.6210003 0.6615 1.1610003 0.7154999 1.7009997 0.7560005 c 0.08100033 0.080999374 0.08100033 0.36450005 0 0.44549942 c -0.41849947 -0.01349926 -0.85049963 -0.026999474 -1.2824998 -0.026999474 c -0.4994998 0 -1.026 0.013500214 -1.5254998 0.026999474 c -0.08099985 -0.080999374 -0.08099985 -0.36450005 0 -0.44549942 c 0.48600006 -0.04050064 0.9450002 -0.09450054 0.37799978 -0.70200014 l -2.2275 -2.3760004 c -0.28349996 -0.31049967 -0.59399986 -0.48600006 -1.0665 -0.52649975 v 2.376 c 0 1.1204996 0.22950006 1.1879997 1.1745 1.2285004 c 0.08100009 0.080999374 0.08100009 0.36450005 0 0.44549942 c -0.62100005 -0.01349926 -1.2554998 -0.026999474 -1.755 -0.026999474 c -0.513 0 -1.1340001 0.013500214 -1.7415 0.026999474 c -0.081 -0.080999374 -0.081 -0.36450005 0 -0.44549942 c 0.94500005 -0.04050064 1.1745 -0.108000755 1.1745 -1.2285004 v -5.4135 c 0 -1.1205001 -0.22949994 -1.1880001 -1.1745 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.6075 0.0135 1.2285 0.027 1.7550001 0.027 c 0.513 0 1.1340001 -0.0135 1.7414999 -0.027 c 0.08100009 0.081 0.08100009 0.36450002 0 0.44550002 c -0.94499993 0.040500015 -1.1745 0.10799998 -1.1745 1.2285001 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gF374CFAFF5D6C47B1DAB0C2ADD3631F0\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 5.3325 8.5185 c -0.783 0.1079998 -0.75600004 0.36450005 -2.079 0.36450005 c -1.3634999 0 -2.565 -0.9045 -2.565 -2.3355002 c 0 -1.4175 1.1745 -2.0654998 2.3625002 -2.5245 c 0.80999994 -0.31050014 1.809 -0.68850017 1.809 -2.0115001 c 0 -1.0935001 -0.6075001 -1.6605 -1.6875 -1.6605 c -1.2555001 0 -2.0655003 0.5805 -2.3625002 1.917 c -0.17549998 0.0539999 -0.3105 0.02699995 -0.4455 -0.040500164 c 0.05399999 -1.0394999 0.10800001 -1.3905 0.27 -2.052 c 0.8505 0 1.2285 -0.31050003 2.43 -0.31050003 c 0.6075001 0 1.188 0.14850001 1.6604998 0.44550002 c 0.78300047 0.48600003 1.269 1.3095 1.269 2.1735 c 0 1.4310002 -1.0799999 2.0655 -2.1869998 2.484 c -0.8100002 0.2970004 -2.16 0.8100004 -2.16 1.9440002 c 0 0.75600004 0.6884999 1.5119996 1.4849999 1.5119996 c 1.3095002 0 1.7280002 -0.8369994 1.9980001 -1.7279997 c 0.14849997 -0.02699995 0.32399988 -0.013499737 0.4454999 0.08099985 c -0.0539999 0.64800024 -0.1079998 1.026 -0.24300003 1.7415004 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gFE99B40F85E0333227B7A3AC71DADD12\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.7540002 -2.16 c 0.21599984 0.37800002 0.3915 0.75600004 0.55349994 1.161 c 1.0800002 2.6055 1.6875002 3.996 2.3895 5.5080004 c 0.26999998 0.5669999 0.4590001 0.783 1.0935001 0.86399984 c 0.08099985 0.08099985 0.08099985 0.36450005 0 0.4454999 c -0.26999998 -0.013499737 -0.5805001 -0.02699995 -0.9584999 -0.02699995 c -0.4050002 0 -0.82350016 0.013500214 -1.2285004 0.02699995 c -0.08099985 -0.08099985 -0.08099985 -0.36450005 0 -0.4454999 c 0.43200016 -0.040500164 0.8640003 -0.121500015 0.64800024 -0.6075001 l -1.3364999 -3.0915 c -0.094500065 -0.21599996 -0.21600008 -0.2565 -0.32400012 0.013499975 l -1.2014999 2.808 c -0.24300003 0.5669999 -0.31050014 0.82350016 0.4454999 0.87750006 c 0.08100009 0.08099985 0.08100009 0.36450005 0 0.4454999 c -0.49950004 -0.013499737 -1.0395 -0.02699995 -1.5255 -0.02699995 c -0.45899993 0 -0.8235 0.013500214 -1.0935 0.02699995 c -0.081 -0.08099985 -0.081 -0.36450005 0 -0.4454999 c 0.54 -0.067500114 0.7155 -0.18900013 1.0665001 -1.0124998 l 1.5255 -3.5505004 c 0.121500015 -0.26999998 0.32399988 -0.89100003 0.18899989 -1.269 c -0.16199994 -0.4455 -0.32399988 -0.82350004 -0.5265 -1.2420001 c -0.14849997 -0.26999998 -0.33749986 -0.39149988 -0.67499995 -0.39149988 c -0.18900001 0 -0.24300003 0.040499926 -0.3915 0.040499926 c -0.3915 0 -0.59400004 -0.40499997 -0.59400004 -0.5805001 c 0 -0.28349996 0.27000004 -0.4994998 0.6345001 -0.4994998 c 0.28349996 0 0.82350004 0.1079998 1.3095001 0.9719999 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gEC499EBACB82676A3B91B470EA5FFF77\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 0.648 1.863 c 0.05400002 -0.6615 0.094500005 -1.296 0.094500005 -1.863 c 0.13499999 0.027 0.27000004 0.0405 0.33750004 0.0405 c 0.094499946 0 0.17550004 0 0.26999998 -0.026999999 c 0.36450005 -0.0945 0.729 -0.14850001 1.2285 -0.14850001 c 0.75600004 0 2.1464999 0.36450002 2.1464999 1.701 c 0 0.918 -0.6615 1.4580002 -1.5794997 1.7955 c -0.8100002 0.31050014 -1.3500001 0.513 -1.3500001 1.2555001 c 0 0.5535002 0.48600006 0.86399984 0.94499993 0.86399984 c 0.29700017 0 1.0800002 -0.1079998 1.2555001 -1.2554998 c 0.08100009 -0.08099985 0.35100007 -0.067500114 0.43199992 0.013500214 c 0.040500164 0.48599958 0.067500114 0.98549986 0.08100033 1.4309998 c -0.41850042 0.067500114 -1.0665002 0.25650024 -1.7685003 0.25650024 c -0.99899995 0 -1.9035 -0.64800024 -1.9035 -1.5120001 c 0 -0.9855001 0.44550002 -1.404 1.485 -1.8360002 c 1.1205001 -0.45899987 1.3770001 -0.74249995 1.3770001 -1.323 c 0 -0.66150004 -0.648 -0.94500005 -1.1475 -0.94500005 c -0.5265 0 -0.82350004 0.1755 -0.9585 0.32400003 c -0.29700005 0.31050003 -0.44550002 0.9045 -0.5265 1.242 c -0.08100003 0.08100009 -0.33750004 0.067499995 -0.41850007 -0.013499975 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g1A22C9A3D561958A216F144A561E83A7\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 2.295 4.833 c -0.013499975 0.40499973 -0.040499926 0.8909998 -0.10800004 1.026 c -0.02699995 0.067500114 -0.0539999 0.1079998 -0.16199994 0.1079998 c -0.37800002 -0.14849997 -0.7290001 -0.26999998 -1.6605 -0.3915 c -0.02700001 -0.08099985 0 -0.29699993 0.02699998 -0.37799978 c 0.7290001 -0.067500114 0.87750006 -0.13500023 0.87750006 -0.9180002 v -2.6325 c 0 -1.1070001 -0.17550004 -1.1745 -0.91800004 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.40500003 0.0135 0.91800004 0.027 1.458 0.027 c 0.53999996 0 0.95850015 -0.0135 1.3635001 -0.027 c 0.08099985 0.081 0.08099985 0.36450002 0 0.44550002 c -0.68850017 0.067499995 -0.83700013 0.121500015 -0.83700013 1.2285001 v 2.214 c 0 0.2835002 0.121500015 0.4454999 0.22950006 0.5669999 c 0.53999996 0.5265002 1.0395 0.7965002 1.4580002 0.7965002 c 0.513 0 0.8909998 -0.32399988 0.8909998 -1.2285001 v -2.349 c 0 -1.1070001 -0.1079998 -1.1745 -0.8369999 -1.2285001 c -0.067500114 -0.081 -0.067500114 -0.36450002 0 -0.44550002 c 0.3375001 0.0135 0.8369999 0.027 1.3769999 0.027 c 0.53999996 0 0.98550034 -0.0135 1.323 -0.027 c 0.067500114 0.081 0.067500114 0.36450002 0 0.44550002 c -0.6749997 0.05400002 -0.7964997 0.121500015 -0.7964997 1.2285001 v 2.1465 c 0 0.18899989 0 0.37800002 -0.013500214 0.5399997 c 0.64800024 0.71550035 1.2555003 0.8910003 1.7820001 0.8910003 c 0.513 0 0.80999994 -0.29699993 0.80999994 -1.2014999 v -2.3760002 c 0 -1.1070001 -0.13500023 -1.1745 -0.8369999 -1.2285001 c -0.067500114 -0.081 -0.067500114 -0.36450002 0 -0.44550002 c 0.33749962 0.0135 0.8369999 0.027 1.3769999 0.027 c 0.53999996 0 1.0124998 -0.0135 1.3905001 -0.027 c 0.067500114 0.081 0.067500114 0.36450002 0 0.44550002 c -0.74249935 0.05400002 -0.86399937 0.121500015 -0.86399937 1.2285001 v 2.1330001 c 0 1.2014999 -0.20250034 2.1465 -1.3905001 2.1465 c -0.6885004 0 -1.5255003 -0.25650024 -2.2275004 -0.9990001 c -0.040500164 -0.040500164 -0.121500015 -0.10800028 -0.14849997 0.013499737 c -0.121500015 0.5535002 -0.64800024 0.98550034 -1.3499999 0.98550034 c -0.78300023 0 -1.4850001 -0.4590001 -2.0520003 -1.0935001 c -0.067499876 -0.067500114 -0.14849997 -0.16200018 -0.16199994 0 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gB9D34D5A11768E1BE112EBDB9F1A7471\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 4.3875003 5.373 c 0.7289996 -0.0539999 0.80999994 -0.21600008 0.52649975 -0.9045 l -1.0934999 -2.6460001 c -0.21600008 -0.5265 -0.28349996 -0.5265 -0.49950004 0.040500045 l -0.9720001 2.6055002 c -0.24300003 0.64799976 -0.29699993 0.80999994 0.41850019 0.9045 c 0.08099985 0.08099985 0.08099985 0.36450005 0 0.4454999 c -0.44550014 -0.013499737 -0.9180001 -0.02699995 -1.3635001 -0.02699995 c -0.44550002 0 -0.85050005 0.013500214 -1.2555001 0.02699995 c -0.08100001 -0.08099985 -0.08100001 -0.36450005 0 -0.4454999 c 0.7155 -0.08099985 0.81 -0.29699993 1.08 -0.98549986 l 1.728 -4.2660003 c 0.08100009 -0.2025 0.16199994 -0.28350002 0.3375001 -0.28350002 c 0.13499999 0 0.22950006 0.081 0.32399988 0.31050003 l 1.7955003 4.2255 c 0.25649977 0.59399986 0.40499973 0.93149996 1.1609998 0.9990001 c 0.08099985 0.08099985 0.08099985 0.36450005 0 0.4454999 c -0.26999998 -0.013499737 -0.59399986 -0.02699995 -0.9450002 -0.02699995 c -0.4454999 0 -0.9045 0.013500214 -1.2419996 0.02699995 c -0.08100033 -0.08099985 -0.08100033 -0.36450005 0 -0.4454999 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g11F318E12978A97B3CFB940EC2702703\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 5.373 1.2285 c -0.0539999 0.121500015 -0.16200018 0.17550004 -0.2835002 0.18900001 c -0.45899963 -0.594 -1.0394998 -0.89100003 -1.6199999 -0.89100003 c -0.9855001 0 -1.809 0.9990001 -1.809 2.5785 c 0 1.4850001 0.648 2.376 1.539 2.376 c 0.79649997 0 0.9045 -0.47249985 0.9584999 -0.9449997 c 0.040500164 -0.36450005 0.2295003 -0.48600006 0.513 -0.48600006 c 0.2835002 0 0.6615 0.17549992 0.6615 0.59399986 c 0 0.7425003 -0.7694998 1.2825003 -2.0654998 1.2825003 c -1.3365002 0 -2.7675002 -1.2015004 -2.7675002 -3.1185002 c 0 -1.7415 0.972 -2.943 2.673 -2.943 c 0.80999994 0 1.5254998 0.2565 2.2005 1.3635 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"g535F59953528118545BE8EBE6AE2E5A1\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 3.8205001 4.3605003 c 1.1474998 0 1.188 -0.31050014 1.2285001 -0.9180002 c 0.08099985 -0.08100009 0.36450005 -0.08100009 0.4454999 0 c -0.013500214 0.3375001 -0.02699995 0.7290001 -0.02699995 1.1880002 c 0 0.45899963 0.013499737 0.80999994 0.02699995 1.1609998 c -0.08099985 0.08099985 -0.36450005 0.08099985 -0.4454999 0 c -0.040500164 -0.7424998 -0.08100033 -0.91799974 -1.2285001 -0.91799974 h -1.2555001 v 2.4299998 c 0 0.7290001 0.16199994 0.8640003 0.75600004 0.8640003 h 0.6075001 c 1.4309998 0 1.7684999 -0.54000044 2.0654998 -1.4715004 c 0.16200018 0 0.31050014 0.02699995 0.41850042 0.067500114 c -0.067500114 0.5535002 -0.27000046 1.8494997 -0.2970004 1.9575005 c 0 0.026999474 -0.013499737 0.040499687 -0.0539999 0.040499687 c -0.22949982 -0.040499687 -0.29699993 -0.0539999 -0.6345 -0.0539999 h -3.4425 c -0.43200004 0 -1.2015 0.013500214 -1.7415 0.026999474 c -0.081 -0.080999374 -0.081 -0.36450005 0 -0.44549942 c 0.94500005 -0.04050064 1.1745 -0.108000755 1.1745 -1.2285004 v -5.4135 c 0 -1.1205001 -0.22949994 -1.1880001 -1.1745 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.4725 0.0135 1.1475 0.027 1.755 0.027 c 0.60749996 0 1.2690002 -0.0135 1.7415 -0.027 c 0.08100009 0.081 0.08100009 0.36450002 0 0.44550002 c -0.94499993 0.040500015 -1.1745 0.10799998 -1.1745 1.2285001 v 2.7135003 Z \"/\u003e\u003c/symbol\u003e\u003csymbol id=\"gAD39741236FED4321F15CC53E9BC832B\" overflow=\"visible\"\u003e\u003cpath d=\"M 0 0m 1.998 8.7075 c -0.48599994 0 -1.215 0.013500214 -1.7415 0.026999474 c -0.081 -0.080999374 -0.081 -0.36450005 0 -0.44549942 c 0.94500005 -0.04050064 1.1745 -0.108000755 1.1745 -1.2285004 v -5.4135 c 0 -1.1205001 -0.22949994 -1.1880001 -1.1745 -1.2285001 c -0.081 -0.081 -0.081 -0.36450002 0 -0.44550002 c 0.5265 0.0135 1.2555001 0.027 1.7550001 0.027 c 0.48600006 0 0.8369999 -0.027 1.971 -0.027 c 2.6865 0 3.402 1.35 3.402 2.511 c 0 1.2960002 -0.9045 2.0385 -2.079 2.3760002 v 0.02699995 c 0.6750002 0.3375001 1.2825003 1.0395002 1.2825003 1.7684999 c 0 0.9045005 -0.3915 2.079 -2.9025002 2.079 c -0.4725001 0 -1.2015002 -0.026999474 -1.6875001 -0.026999474 Z m 0.5805 -4.2255 h 0.93149996 c 1.8495 0 2.6190002 -1.0395002 2.6190002 -2.2545004 c 0 -0.9855 -0.37800026 -1.809 -2.43 -1.809 c -0.94499993 0 -1.1205001 0.351 -1.1205001 1.1745 Z m 0 2.9429998 c 0 0.7154999 0 0.8505001 1.1745 0.8505001 c 0.7560003 0 1.7415001 -0.4050002 1.7415001 -1.7820001 c 0 -1.1475 -0.7965002 -1.5525002 -1.917 -1.5525002 h -0.9990001 Z \"/\u003e\u003c/symbol\u003e\u003c/defs\u003e\u003c/svg\u003e\n        \u003c/div\u003e\n        \u003cdiv style=\"display: grid; place-items: start center;\"\u003e\n          \u003cfigcaption\u003e图 1 计算机启动过程\u003c/figcaption\u003e\n        \u003c/div\u003e\n      \u003c/figure\u003e\n    \u003c/div\u003e\n    \u003cp class=\"typst-parbreak\"\u003e\u003c/p\u003e","title":"计算机启动过程 —— 以 Arch Linux 安装过程为例"},{"content":" 前言 Typst 一直以来都是我非常喜欢的一个排版工具，相比于 LaTeX，Typst 的语法简单，编写体验好；相比于 Markdown，Typst 的功能强大，标准统一，符合我对排版工具的所有想象。\n自从我接触到 Typst 之后，不仅我的日常的作业、报告、简历等文档都使用 Typst 写的，而且我也开发了一个 Typst Package 用于在 Typst 中绘制树状图，比如二叉树、红黑树、语法树等等 —— tdtr (i.e. tidy tree)，感兴趣的话可以看看。\n因此，我一直想在我的 Blog 中使用 Typst 来编写文章，但是苦于 Typst 对 HTML 导出的支持仍然处于实验性阶段，因此搭建 Blog 的想法也一直一拖再拖。\n但是，直到最近，我对于搭建 Blog 的需求越来越迫切了，所以我就决定不再等待 Typst 对 HTML 导出的支持了，而是自己动手来实现这个功能。这篇文章讲述的就是我如何实现在 Hugo 中使用 Typst 编写文章的。\nBlog 的源代码位于 github.com/Vertsineu/blog，欢迎 star 和 fork。\n使用 如果你也想像我的 Blog 一样使用 Typst 来编写基于 Hugo 的 Blog 的话，可以按照以下步骤来操作：\n首先安装我修改过的 Hugo，目前还没有发布版本，因此需要手动编译安装：\n首先，clone 下来我修改过的 Hugo 的代码，并切换到 support-typst 分支，即：\ngit clone https://github.com/Vertsineu/hugo.git\ncd hugo\ngit checkout support-typst 然后安装 mage 用于编译安装：\ngo install github.com/magefile/mage@latest 接着运行以下命令来编译安装 Hugo：\nmage install 最后检查一下 Hugo 是否安装成功了：\nhugo version 如果 BuildDate 和当前时间相近，并且版本号是 v0.159.1，那么就说明安装成功了。\n接下来，我建议你先 clone 我的 Blog 的代码，这样你就可以在此基础上进行修改，而不需要从零开始搭建：\n首先，clone 下来我的 Blog 的代码：\ngit clone https://github.com/Vertsineu/blog.git\ncd blog 然后运行 hugo server 来启动本地服务器：\nhugo server -D 其中 -D 参数是为了让 Hugo 也编译 draft 状态的文章。\n此时，你就可以在浏览器中访问 http://localhost:1313 来查看 Blog 的内容了。\n然后，你可以尝试在 content/posts 目录下新建一个 .typ 文件，导入模板和工具包，然后使用 Typst 编写文章了。\n比如，一个简单的示例如下：\n#import \"@hugo/templates:0.1.0\": article\n#import \"@hugo/utils:0.1.0\": *\n#show: article.with(\ntitle: \"如何在 Hugo 中使用 Typst 编写文章\",\ndate: datetime(year: 2026, month: 3, day: 29),\ndraft: true,\n)\n// your article content here 最后，尽情享受使用 Typst 编写 Hugo 博客的乐趣吧！如果你在使用过程中遇到了任何问题，欢迎在我的 Blog 的 GitHub 仓库中提交 issue，我会尽快回复的。\n注：如果你需要使用 tinymist 的话，还需要给 tinymist 配置额外参数才能产生正确的 LSP 解析结果，比如，在我使用 VSCode 的 tinymist，而我对 tinymist 的配置如下：\n{\n\"tinymist.exportPdf\": \"never\",\n\"tinymist.rootPath\": \"${workspaceFolder}/content\",\n\"tinymist.typstExtraArgs\": [\n\"--package-path=./packages\"\n],\n\"tinymist.exportTarget\": \"html\"\n} 实现 实现主要分为两个部分，一个是 Hugo 侧添加对 Typst 的支持，一个是 Typst 侧增强实验性 HTML 导出的功能。\nHugo Hugo 本身是不支持 Typst 的，因此我们需要先 fork 一份 Hugo 的代码。为了保证功能稳定性，我选择了最新的稳定版本 v0.159.1 来进行开发，位于 github.com/Vertsineu/hugo。\nHugo 的代码有够多的，因此这部分主要我是使用 gpt-5.3-codex 来帮我实现的（AI 还是太强大了）\n初步实现 一开始我实现了一个非常简单的版本，核心思路是让 Hugo 去自动识别 .typ 文件，然后调用 Typst CLI 来进行编译，最后把编译生成的 HTML 代码片段插入到最终的页面中。\n实现这个功能并不复杂，因为 Hugo 本身已经将 Markdown 的解析过程抽象出来了，我只需要去按照接口将 Typst 的解析过程插入进去就行了，同时添加配置项让用户可以配置 Typst 的相关选项。\n具体实现了以下功能：\n实现对于 Typst CLI 的 go 封装，比如对于 typst compile 的参数的封装如下：\n// Location: markup/typst/typstcli/runner.go\ntype CompileArgs struct {\nInput Input\nOutput Output\nFormat OutputFormat\nWorld WorldArgs\nPages []string\nPDFStandard []string\nNoPDFTags bool\nPPI float32\nDeps Output\nDepsFormat DepsFormat\nProcess ProcessArgs\nOpen *string\nTimings *string\nExec ExecOptions\n} 这样我们就可以在 Hugo 中调用 Typst CLI 来编译 Typst 文件了。原本我是想使用 go-typst 这个库的，但是这个库的只提供了 typst compile 的封装，对于 typst query 和 typst watch 都没有提供封装，索性我就参考 Typst 源代码的 args.rs 直接自己实现了一个对 Typst CLI 的封装。\n实现一个 Provider 和 Converter 用于向 Hugo 添加 Typst 编译支持：\n// Location: markup/typst/convert.go\n// Provider is the package entry point.\nvar Provider converter.ProviderProvider = provider{}\ntype provider struct{}\n// ...\ntype typstConverter struct {\nctx converter.DocumentContext\ncfg converter.ProviderConfig\nwatch *watchManager\n} 并在其中调用 Typst 的 compile 子命令来进行编译：\n// Location: markup/typst/convert.go\nrunner := typstcli.New(c.cfg.Exec, cfg.Binary)\nworld := typstcli.WorldArgsFromConfig(cfg, resolveRootDirectory(cfg.Root, ctx))\nprocess := typstcli.ProcessArgsFromConfig(cfg)\nprocess.Features = []typstcli.Feature{typstcli.FeatureHTML}\n// ...\nerr := runner.Compile(typstcli.CompileArgs{\nInput: typstcli.InputStdin,\nOutput: typstcli.OutputStdout,\nFormat: typstcli.OutputFormatHTML,\nWorld: world,\nPages: pagesFromConfig(cfg.Pages),\nProcess: process,\nExec: typstcli.ExecOptions{\nStdin: bytes.NewReader(src),\nStdout: \u0026amp;out,\nStderr: \u0026amp;cmderr,\n},\n}) 这样，Hugo 就可以识别到 .typ 文件并传递给 Converter 去解析生成 HTML 代码片段。\n实现 Front Matter 的自定义逻辑来支持从 Typst 中提取 metadata：\n// Location: hugolib/page__content.go\nrunner := typstcli.New(h.Deps.ExecHelper, cfg.Binary)\nprocess := typstcli.ProcessArgsFromConfig(cfg)\nprocess.Features = []typstcli.Feature{typstcli.FeatureHTML}\nerr := runner.Query(typstcli.QueryArgs{\nInput: typstcli.Input(filename),\nSelector: \"metadata\",\nField: \"value\",\nWorld: typstcli.WorldArgsFromConfig(cfg, root),\nProcess: process,\nExec: typstcli.ExecOptions{\nStdout: \u0026amp;out,\nStderr: \u0026amp;cmderr,\n},\n}) 对于 Markdown 文件，Hugo 是通过解析文件开头的 YAML/TOML/JSON 格式的 Front Matter 来提取 metadata 的，而对于 Typst 文件，我采取使用 typst query 通过提取全文第一个 #metadata 对象中的内容来作为 metadata。\n比如，在 Typst 的 article 模板中，对 metadata 的支持是通过以下代码实现的：\n#let article(\ntitle: \"\",\ndescription: \"\",\ntags: (),\ndate: datetime.today(),\nweight: 10,\ndraft: false,\nbody,\n..args\n) = {\n// ...\nlet prelude = metadata((\ntitle: title,\ndescription: description,\ntags: tags,\ndate: date.display(\"[year]-[month]-[day]\"),\nweight: weight,\ndraft: draft,\n..args.named()\n))\nprelude\nbody\n} 这样，Hugo 就可以通过 typst query 来提取到 .typ 文件的 metadata 了。\n在 markup 中添加了 Typst 选项并且提供相关的配置项：\n// Location: markup/typst/typst_config/config.go\n// Package typst_config holds Typst-related configuration.\npackage typst_config\ntype WatchConfig struct {\nEnabled bool\nTimeout string\n}\n// Config configures the Typst converter.\ntype Config struct {\nBinary string\n// Root sets Typst's project root. If empty, Hugo uses the current .typ file's directory.\nRoot string\n// Input values exposed to Typst via sys.inputs.\nInputs map[string]string\n// FontPaths are additional directories searched for fonts.\nFontPaths []string\nIgnoreSystemFonts bool\nIgnoreEmbeddedFonts bool\nPackagePath string\nPackageCachePath string\nJobs int\nPages string\nWatch WatchConfig\n}\nvar Default = Config{\nBinary: \"typst\",\nWatch: WatchConfig{\nTimeout: \"3s\",\n},\n} 这样我就可以在 hugo.toml 中配置 Typst 的相关选项了，比如这个 Blog 的配置如下：\n[markup.typst]\nroot = \"./content\"\npackagePath = \"./packages\"\njobs = 4\n[markup.typst.watch]\nenabled = true\ntimeout = \"3s\" 后续优化 初步实现完成后，修改过的 Hugo 已经能够很好的满足我的写作需求了，但是由于实现过程中使用的是 typst compile 来进行编译的，在运行 hugo server 的时候，每次修改 .typ 文件都会触发一次完整的编译，即使 Typst 文档非常简单，编译时间也会有 500ms 左右，远大于使用 Markdown 编写时 10ms 以内的编译时间。\n因此，我添加了对于 typst watch，即增量编译的支持，这样，在 hugo server 的时候，修改 .typ 文件只会触发增量编译，编译时间也可以缩短到 200ms 左右。\n具体实现如下：\n在 Provider 中添加一个 watchManager 专门用于管理 typst watch 进程：\n// Location: markup/typst/watch.go\ntype watchManager struct {\nrunner typstcli.Runner\nlogger interface{ Warnf(format string, v ...any) }\ntimeout time.Duration\npages string\noutputDir string\nmu sync.Mutex\nentries map[string]*watchEntry\nclosed bool\n}\ntype watchEntry struct {\ninput string\noutput string\nstartOnce sync.Once\ncancel context.CancelFunc\ndone chan struct{}\nerrMu sync.RWMutex\nerr error\n} 其中 runner 是封装了 Typst CLI 的对象，entries 则是用于记录所有通过 runner 运行 typst watch 的进程相关的信息的，比如输入输出文件、context 的 cancel 函数、完成信号等等。\n在 Converter 中劫持 typst compile 的调用，如果在配置中启用了 watch，即以下选项在 hugo.toml 中配置了：\n[markup.typst.watch]\nenabled = true\ntimeout = \"3s\" 那么就通过 watchManager 的 render 方法来调用 typst watch 来进行编译：\n// Location: markup/typst/convert.go\nif c.watch != nil \u0026amp;\u0026amp; ctx.Filename != \"\" {\ncontent, err := c.watch.render(ctx.Filename, world, process)\nif err == nil {\nif len(content) == 0 {\nlogger.Warnf(\"%s watch rendered no output for %s, falling back to compile\", cfg.Binary, ctx.DocumentName)\n} else {\nclean := stripHTMLDocument(content)\nreturn normalizeExternalHelperLineFeeds(clean), nil\n}\n} else {\nlogger.Warnf(\"%s watch failed for %s: %v; falling back to compile\", cfg.Binary, ctx.DocumentName, err)\n}\n} watchManager 的 render 方法会使用 sync.Once 来保证对于同一个输入文件只会启动一个 go routine 用于运行 typst watch 进程：\n// Location: markup/typst/watch.go\nentry.startOnce.Do(func() {\nctx, cancel := context.WithCancel(context.Background())\nentry.cancel = cancel\ngo func() {\ndefer close(entry.done)\nerr := m.runner.Watch(typstcli.WatchArgs{\nCompile: typstcli.CompileArgs{\nInput: typstcli.Input(entry.input),\nOutput: typstcli.Output(entry.output),\nFormat: typstcli.OutputFormatHTML,\nWorld: world,\nPages: pagesFromConfig(m.pages),\nProcess: process,\nExec: typstcli.ExecOptions{\nContext: ctx,\nStderr: os.Stderr,\n},\n},\nServer: typstcli.ServerArgs{\nNoServe: true,\nNoReload: true,\n},\n})\nif errors.Is(ctx.Err(), context.Canceled) {\nreturn\n}\nif err == nil {\nerr = errors.New(\"typst watch exited unexpectedly\")\n}\nentry.setErr(err)\nif m.logger != nil {\nm.logger.Warnf(\"typst watch failed for %q: %v\", entry.input, err)\n}\n}()\n}) 这样，在 hugo server 的时候，每次修改 .typ 文件就会触发 typst watch 来进行增量编译。\n但问题是，Hugo 如何得知 typst watch 何时完成了编译呢？这就需要通过读取输出文件的 modified time 来判断了，在 watchManager 的 waitReady 方法中实现了这个功能：\n// Location: markup/typst/watch.go\nfor {\nif err := entry.getErr(); err != nil {\nreturn err\n}\noutInfo, err := os.Stat(entry.output)\nif err == nil \u0026amp;\u0026amp; !outInfo.ModTime().Before(info.ModTime()) {\nreturn nil\n}\nif time.Now().After(deadline) {\nreturn fmt.Errorf(\"timed out after %s waiting for typst watch output\", m.timeout)\n}\ntime.Sleep(20 * time.Millisecond)\n} 通过每隔 20ms 检查一次输出文件的 modified time 来判断 typst watch 是否完成了编译，如果在配置的 timeout 时间内都没有完成，就返回一个超时错误。\n未来计划 虽然通过把 typst compile 改成 typst watch 已经大大提升了编译性能了，但是每次运行 typst query 获取 metadata 的过程仍然是一个完整的编译过程。可惜的是，Typst CLI 目前并没有支持 typst query 的增量编译功能，如果需要实现的话，可能需要修改 Typst CLI 的源代码，这将是一笔不小的工作量，而且考虑到目前的性能已经是可以接受的了，所以我暂时不打算去实现这个功能了。\nTypst Typst 方面主要是增强实验性 HTML 导出的功能。万幸的是，Typst 目前是可以直接写 HTML 标签的，因此最终实现的效果的上界是有保证的，你甚至可以直接把 Typst 的内置函数全重写成 HTML 标签然后用 CSS 来控制样式。\n但是，我用 Typst 的目的肯定是尽可能用 Typst 的语法来排版，因此我自制了一个 article 的模板，以及一些常用函数支持，作为 local package 存放在 Blog 的 packages 目录中，以便导入使用。\n以下我将简要介绍几个常见的功能的实现：\n一些辅助函数，主要用于将 Typst 内置类型转换为 CSS 样式，比如 alignment 转换为 CSS 的 place-items：\n// support: place-items, text-align, vertical-align, etc.\n#let to-alignment(alignment) = {\nif type(alignment) != std.alignment {\npanic(\"Unsupported alignment type, please use a valid alignment!\")\n}\nlet x-align = alignment.x\nx-align = if x-align == center {\n\"center\"\n} else if x-align == left {\n\"start\"\n} else if x-align == right {\n\"end\"\n} else if x-align == start {\n\"start\"\n} else if x-align == end {\n\"end\"\n} else {\n\"start\" // fallback to start if none\n}\nlet y-align = alignment.y\ny-align = if y-align == top {\n\"start\"\n} else if y-align == horizon {\n\"center\"\n} else if y-align == bottom {\n\"end\"\n} else {\n\"start\" // fallback to start if none\n}\n(x-align, y-align)\n} #h、#v 和 #align 等内置函数的实现：\n#let h-func(it) = {\nlet amount = it.amount.to-absolute().pt()\nhtml.span(\nstyle: \"display: inline-block; width: 100%; width: \" + str(amount) + \"px;\",\n)\n}\n#let v-func(it) = {\nlet amount = it.amount.to-absolute().pt()\nhtml.div(\nstyle: \"height: \" + str(amount) + \"px;\",\n)\n}\n#let align-func(it) = {\nlet (x-align, y-align) = to-alignment(it.alignment)\nlet place-items = y-align + \" \" + x-align\nhtml.div(\nstyle: \"display: grid; place-items: \" + place-items + \";\",\nit.body,\n)\n} 内置函数一律通过 show rules 来重写成 HTML 标签，比如在 article 模板中：\n#show h: h-func\n#show v: v-func\n#show align: align-func\n// ... 除此之外，针对我使用的 PaperMod 主题的样式，我还实现了一些 CSS 样式，比如对于链接、代码块的样式，位于 /assets/css/extended 目录下，具体就不展开介绍了，感兴趣的话可以直接看源代码。\n总结 最后，从我编写本文的体验来说，使用 Typst 来编写 Blog 文章的体验是非常不错的，Typst 的语法简单，功能强大，能够让我专注于内容的创作，而不需要过多地关注排版的细节。对于常年熟练使用 Typst 的用户来说，使用 Typst 来编写 Blog 我觉得是一个非常不错的选择。\n不过说实在，Typst 目前对于 HTML 导出的支持还不够完善，很多内置函数默认是会被 Typst 忽略的，需要手动实现，因此对于新手来说，我还是更加推荐使用 Markdown 来编写 Blog，毕竟各大 Blog 框架对 Markdown 的支持都远比 Typst 完善许多。\nTable of Contents\n前言\n使用\n实现\nHugo\n初步实现\n后续优化\n未来计划\nTypst\n总结\n参考文献\n参考文献 [1] George Honeywood, 《Typst and Hugo》. 见于: 2026年3月29日. [在线]. 载于: https://george.honeywood.org.uk/blog/typst-and-hugo/ [2] George Honeywood, 《Typst and Hugo Properly》. 见于: 2026年3月29日. [在线]. 载于: https://george.honeywood.org.uk/blog/typst-and-hugo-properly/ [3] Typst Authors, 《Typst Documentation: Reference》. 见于: 2026年3月29日. [在线]. 载于: https://typst.app/docs/reference/ ","permalink":"https://blog.vertsineu.top/posts/hello-hugo/","summary":"\u003ch2 id=\"loc-1\"\u003e前言\u003c/h2\u003e\n    \u003cp\u003eTypst 一直以来都是我非常喜欢的一个排版工具，相比于 LaTeX，Typst 的语法简单，编写体验好；相比于 Markdown，Typst 的功能强大，标准统一，符合我对排版工具的所有想象。\u003c/p\u003e\n    \u003cp\u003e自从我接触到 Typst 之后，不仅我的日常的作业、报告、简历等文档都使用 Typst 写的，而且我也开发了一个 Typst Package 用于在 Typst 中绘制树状图，比如二叉树、红黑树、语法树等等 —— \u003ca href=\"https://github.com/Vertsineu/typst-tdtr\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\u003cspan style=\"color: #59a4ff;\"\u003e\u003cspan style=\"text-decoration: underline\"\u003etdtr\u003c/span\u003e\u003c/span\u003e\u003c/a\u003e (i.e. tidy tree)，感兴趣的话可以看看。\u003c/p\u003e\n    \u003cp\u003e因此，我一直想在我的 Blog 中使用 Typst 来编写文章，但是苦于 Typst 对 HTML 导出的支持仍然处于实验性阶段，因此搭建 Blog 的想法也一直一拖再拖。\u003c/p\u003e\n    \u003cp\u003e但是，直到最近，我对于搭建 Blog 的需求越来越迫切了，所以我就决定不再等待 Typst 对 HTML 导出的支持了，而是自己动手来实现这个功能。这篇文章讲述的就是我如何实现在 Hugo 中使用 Typst 编写文章的。\u003c/p\u003e\n    \u003cp\u003eBlog 的源代码位于 \u003ca href=\"https://github.com/Vertsineu/blog\" target=\"_blank\" rel=\"noopener noreferrer\"\u003e\u003cspan style=\"color: #59a4ff;\"\u003e\u003cspan style=\"text-decoration: underline\"\u003egithub.com/Vertsineu/blog\u003c/span\u003e\u003c/span\u003e\u003c/a\u003e，欢迎 star 和 fork。\u003c/p\u003e\n    \u003ch2 id=\"loc-2\"\u003e使用\u003c/h2\u003e\n    \u003cp\u003e如果你也想像我的 Blog 一样使用 Typst 来编写基于 Hugo 的 Blog 的话，可以按照以下步骤来操作：\u003c/p\u003e\n    \u003cp\u003e首先安装我修改过的 Hugo，目前还没有发布版本，因此需要手动编译安装：\u003c/p\u003e\n    \u003cp\u003e\u003c/p\u003e\n    \u003col\u003e\n      \u003cli\u003e\n        \u003cp\u003e首先，clone 下来我修改过的 Hugo 的代码，并切换到 \u003cspan class=\"typst-raw-inline\"\u003e\u003ccode\u003esupport-typst\u003c/code\u003e\u003c/span\u003e 分支，即：\u003c/p\u003e","title":"如何在 Hugo 中使用 Typst 编写文章"}]