<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Z的日志</title><description>zlog</description><link>https://zlotus.github.io/</link><language>zh_CN</language><item><title>Orin AGX 64G 中模型常驻指南</title><link>https://zlotus.github.io/2026/04/06/agx-for-gpts/</link><guid isPermaLink="true">https://zlotus.github.io/2026/04/06/agx-for-gpts/</guid><description>低功耗的日常模型服务</description><pubDate>Mon, 06 Apr 2026 20:30:25 GMT</pubDate><content:encoded>&lt;p&gt;私有化部署一个中等大小的模型，用于常驻一个agent，做日常对话服务，而且不能功耗太高，毕竟烧的是自家的电费……&lt;/p&gt;
&lt;h1&gt;0 网络准备&lt;/h1&gt;
&lt;p&gt;这台设备是在Gemini的帮助下，使用一台X86 PVE下的Ubuntu刷的，过程艰险无法描述，是非曲直难以论说。总之刷的时候为了防止任何以外，仅执行了最小化安装，除了系统啥都没有。&lt;/p&gt;
&lt;p&gt;第一步得把命令行的代理配好：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置apt代理
sudo vim /etc/apt/apt.conf.d/proxy.conf
Acquire::http::Proxy &quot;http://127.0.0.1:7897/&quot;;
Acquire::https::Proxy &quot;http://127.0.0.1:7897/&quot;;

vim ~/.zshrc
# 设置系统代理
function proxy_on() {
    export http_proxy=&quot;http://127.0.0.1:7897&quot;
    export https_proxy=$http_proxy
    export ftp_proxy=$http_proxy
    export no_proxy=&quot;localhost,127.0.0.1,localaddress,.localdomain.com&quot;
    export HTTP_PROXY=$http_proxy
    export HTTPS_PROXY=$http_proxy
    export FTP_PROXY=$http_proxy
    echo -e &quot;Proxy environment variable set.&quot;
}

# 关闭系统代理
function proxy_off() {
    unset http_proxy https_proxy ftp_proxy no_proxy HTTP_PROXY HTTPS_PROXY FTP_PROXY
    echo -e &quot;Proxy environment variable removed.&quot;
}

# 设置git代理
git config --global http.proxy http://127.0.0.1:7897
git config --global https.proxy http://127.0.0.1:7897

# 对付顽固分子
git clone https://github.com/rofl0r/proxychains-ng.git &amp;amp;&amp;amp; cd proxychains-ng
./configure --prefix=/usr --sysconfdir=/etc
make
sudo make install
sudo make install-config

sudo vim /etc/proxychains.conf
socks5  127.0.0.1 7897
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;1 NVIDIA工具&lt;/h1&gt;
&lt;p&gt;使用&lt;code&gt;jtop&lt;/code&gt;管理设备状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 最小化安装甚至没有pip
sudo apt update
sudo apt install python3-pip
sudo -H pip3 install -U jetson-stats # 这玩意需要监控底层传感器，不能用uv装，需要重启
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时&lt;code&gt;jtop&lt;/code&gt;会显示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;Jetpack NOT DETECTED&lt;/code&gt;，说明系统裸的很彻底，适合容器化部署&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NV Power[3]: MODE_50W&lt;/code&gt;，表明功耗还没开到最大&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Jetson Clocks: inactive&lt;/code&gt;，表明风扇和核心频率没有锁定在最高状态&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;性能释放开最大：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nvpmodel -m 0
sudo jetson_clocks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;code&gt;jtop&lt;/code&gt;会显示：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;NV Power[0]: MAXN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Jetson Clocks: running&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2 Docker&lt;/h1&gt;
&lt;p&gt;这个裸奔的系统连docker都省了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 记得 proxy_on
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置当前用户免&lt;code&gt;sudo&lt;/code&gt;，然后退出&lt;code&gt;ssh&lt;/code&gt;再重新登陆：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo usermod -aG docker $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装 nvidia 容器工具包&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update
sudo apt install -y nvidia-container-toolkit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置 Docker 默认使用 NVIDIA 运行时并重启服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker info | grep Runtimes
 Runtimes: io.containerd.runc.v2 nvidia runc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为docker配置代理：&lt;/p&gt;
&lt;p&gt;因为英伟达定制系统精简了TUN模块，只能开系统代理，所以最好为docker手动配置系统代理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 设置配置文件
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf &amp;lt;&amp;lt;EOF
[Service]
Environment=&quot;HTTP_PROXY=http://127.0.0.1:7897&quot;
Environment=&quot;HTTPS_PROXY=http://127.0.0.1:7897&quot;
Environment=&quot;NO_PROXY=localhost,127.0.0.1,::1,nvcr.io&quot;
EOF
# 重启服务
sudo systemctl daemon-reload
sudo systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果docker手动拉取的时候报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;❯ docker pull --platform linux/arm64 node:24
Error response from daemon: failed to resolve reference &quot;docker.io/library/node:24&quot;: failed to do request: Head &quot;https://registry-1.docker.io/v2/library/node/manifests/24&quot;: EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是 &lt;code&gt;containerd&lt;/code&gt; 的错误格式，不是经典 &lt;code&gt;dockerd&lt;/code&gt; 的。走 &lt;code&gt;containerd&lt;/code&gt; 拉镜像是独立的 &lt;code&gt;systemd&lt;/code&gt; 服务，需要再设置一个环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 验证，没有代理
agx in 🌐 agx2 in llama.cpp on  master [⇡] is 📦 v0.0.0 via △ via 🐍 v3.10.12
❯ sudo cat /proc/$(pgrep -x containerd)/environ | tr &apos;\0&apos; &apos;\n&apos; | grep -i proxy

# 设置代理
agx in 🌐 agx2 in llama.cpp on  master [⇡] is 📦 v0.0.0 via △ via 🐍 v3.10.12
❯ sudo mkdir -p /etc/systemd/system/containerd.service.d
sudo tee /etc/systemd/system/containerd.service.d/proxy.conf &amp;gt; /dev/null &amp;lt;&amp;lt;&apos;EOF&apos;
[Service]
Environment=&quot;HTTP_PROXY=http://127.0.0.1:7897&quot;
Environment=&quot;HTTPS_PROXY=http://127.0.0.1:7897&quot;
Environment=&quot;NO_PROXY=localhost,127.0.0.1,::1,nvcr.io&quot;
EOF

sudo systemctl daemon-reload
sudo systemctl restart containerd
sudo systemctl restart docker

# 再验证
sudo cat /proc/$(pgrep -x containerd)/environ | tr &apos;\0&apos; &apos;\n&apos; | grep -i proxy

HTTP_PROXY=http://127.0.0.1:7897
HTTPS_PROXY=http://127.0.0.1:7897
NO_PROXY=localhost,127.0.0.1,::1,nvcr.io
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3 llama.cpp测试&lt;/h1&gt;
&lt;p&gt;下个测试用的小模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建模型存放目录
mkdir -p ~/ai-models/gguf
cd ~/ai-models/gguf

# 使用 wget 直接下载 huggingface 镜像站的 GGUF 模型
wget https://hf-mirror.com/QuantFactory/Meta-Llama-3-8B-Instruct-GGUF/resolve/main/Meta-Llama-3-8B-Instruct.Q4_K_M.gguf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;构建专用的的 ARM64 CUDA 镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ~
# 克隆最新源码
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# 此时运行构建会报错，因为docker的buildkit的网络行为很诡异，普通的梯子代理不了，Tegra默认内核没有TUN，而我又暂时没有能用的代理路由
❯ sudo docker build -t llama-cpp-cuda -f .devops/cuda.Dockerfile .
[+] Building 30.1s (3/3) FINISHED                                                                                            docker:default
 =&amp;gt; [internal] load build definition from cuda.Dockerfile                                                                              0.0s
 =&amp;gt; =&amp;gt; transferring dockerfile: 2.74kB                                                                                                 0.0s
 =&amp;gt; ERROR [internal] load metadata for docker.io/nvidia/cuda:12.8.1-runtime-ubuntu24.04                                               30.0s
 =&amp;gt; CANCELED [internal] load metadata for docker.io/nvidia/cuda:12.8.1-devel-ubuntu24.04                                              30.0s
------
 &amp;gt; [internal] load metadata for docker.io/nvidia/cuda:12.8.1-runtime-ubuntu24.04:
------
cuda.Dockerfile:41
--------------------
  39 |
  40 |     ## Base image
  41 | &amp;gt;&amp;gt;&amp;gt; FROM ${BASE_CUDA_RUN_CONTAINER} AS base
  42 |
  43 |     RUN apt-get update \
--------------------
ERROR: failed to build: failed to solve: DeadlineExceeded: failed to fetch anonymous token: Get &quot;https://auth.docker.io/token?scope=repository%3Anvidia%2Fcuda%3Apull&amp;amp;service=registry.docker.io&quot;: dial tcp [2a03:2880:f10d:183:face:b00c:0:25de]:443: i/o timeout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有几个坑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;网络方面，因为docker的buildkit有自己独立的网络请求和dns查找行为，系统代理无法辅助下载。因此，最好是看到报错中的镜像，记下来再手动拉取、构建。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;cuda版本方面，可以看到上面的镜像默认使用了 cuda &lt;code&gt;12.8.1&lt;/code&gt;版本，实际上在&lt;code&gt;jtop&lt;/code&gt;中可以看到本机为&lt;code&gt;L4T 36.5.0&lt;/code&gt;，刷机的Jetpack版本为&lt;code&gt;6.2.2&lt;/code&gt;。去 &lt;a href=&quot;https://catalog.ngc.nvidia.com/&quot;&gt;catalog.ngc.nvidia.com&lt;/a&gt; 搜索&lt;code&gt;l4t-cuda&lt;/code&gt;出来的结果是：&lt;code&gt;nvcr.io/nvidia/l4t-cuda:12.6.11-runtime&lt;/code&gt;、&lt;code&gt;nvcr.io/nvidia/12.6.11-devel:12.6.11-devel-aarch64-ubuntu22.04&lt;/code&gt; ，把构建底座改成tegra专用的，否则构建出来的镜像再运行moe模型时，&lt;code&gt;batch.n_token&amp;gt;32&lt;/code&gt; 就会出现&lt;code&gt;cuBLAS&lt;/code&gt;报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ARG UBUNTU_VERSION=22.04
# This needs to generally match the container host&apos;s environment.
ARG CUDA_VERSION=12.6.11
# Target the CUDA build image
ARG BASE_CUDA_DEV_CONTAINER=nvcr.io/nvidia/12.6.11-devel:12.6.11-devel-aarch64-ubuntu22.04

ARG BASE_CUDA_RUN_CONTAINER=nvcr.io/nvidia/l4t-cuda:12.6.11-runtime
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;既然改了22.04，那就出现新问题了，llama.cpp默认目标系统是24.04，因此&lt;code&gt;.devops/cuda.Dockerfile&lt;/code&gt;中使用了&lt;code&gt;gcc-14&lt;/code&gt;和&lt;code&gt;g++-14&lt;/code&gt;，而22.04默认是&lt;code&gt;11&lt;/code&gt;，不过最好用&lt;code&gt;12&lt;/code&gt;，因此需要改一下构建文件使用&lt;code&gt;gcc-12&lt;/code&gt;、&lt;code&gt;g++-12&lt;/code&gt;，让系统走默认编译器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RUN apt-get update &amp;amp;&amp;amp; \
    apt-get install -y gcc-12 g++-12 build-essential cmake python3 python3-pip git libssl-dev libgomp1

ENV CC=gcc-12 CXX=g++-12 CUDAHOSTCXX=g++-12
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此外，构建过程中包含很多无意义的编译：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因为llama.cpp的docker构建默认行为是编译所有NVIDIA卡的PTX汇编内核，而我只需要编译AGX ORIN，其架构代号是专门的嵌入式架构&lt;code&gt;sm_87&lt;/code&gt;，用&lt;code&gt;-DCMAKE_CUDA_ARCHITECTURES=87&lt;/code&gt;指定编译平台，可节省90%的编译时间；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-DGGML_CPU_ALL_VARIANTS=ON&lt;/code&gt;参数会让 CMake 尝试去编译包括 SVE (可伸缩矢量扩展) 在内的高级 CPU 算子，就是它需要&lt;code&gt;gcc-12&lt;/code&gt;和&lt;code&gt;g++-12&lt;/code&gt;，但 Orin 的 Carmel CPU 是基于 ARMv8.2-A 架构的，不支持最激进的 SVE 指令集，可以关掉；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;最后，再加两个编译开关，避开tegra这种统一内存设备的架构坑。如果不开，在 Qwen3.5-35B-A3B-Uncensored-HauhauCS-Aggressive-Q8_0.gguf 这种模型上会出现不带 mmproj 时一切正常，加上 mmproj 输入图片就报&lt;code&gt;cuMemAddressReserve&lt;/code&gt;的错误。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-DGGML_CUDA_ENABLE_UNIFIED_MEMORY=ON&lt;/code&gt;，以统一内存架构调度；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-DGGML_CUDA_NO_VMM=ON&lt;/code&gt;，禁止预申请一大块连续内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 原来的
  23   │ RUN if [ &quot;${CUDA_DOCKER_ARCH}&quot; != &quot;default&quot; ]; then \
  24   │     export CMAKE_ARGS=&quot;-DCMAKE_CUDA_ARCHITECTURES=${CUDA_DOCKER_ARCH}&quot;; \
  25   │     fi &amp;amp;&amp;amp; \
  26   │     cmake -B build -DGGML_NATIVE=OFF -DGGML_CUDA=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=ON LLAMA_BUILD_TESTS=OFF ${CMAKE_ARGS} -DCMAKE_EXE_LINKER_FLAGS=-Wl,--allow-shlib-undefined . &amp;amp;&amp;amp; \
  27   │     cmake --build build --config Release -j$(nproc)
# 替换为
  28   │ RUN cmake -B build -DGGML_NATIVE=ON -DGGML_CUDA=ON -DGGML_CUDA_NO_VMM=ON -DGGML_CUDA_ENABLE_UNIFIED_MEMORY=ON -DGGML_BACKEND_DL=ON -DGGML_CPU_ALL_VARIANTS=OFF -DLLAMA_BUILD_TESTS=OFF -DCMAKE_CUDA_ARCHITECTURES=87 -DCMAKE_EXE_LINKER_FLAGS=-Wl,--allow-shlib-undefined . &amp;amp;&amp;amp; \
  29   │     cmake --build build --config Release -j$(nproc)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;正确的安装姿势（如果有自己的路由可以省略手动拉取这一步）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 手动拉镜像：
docker pull nvcr.io/nvidia/12.6.11-devel:12.6.11-devel-aarch64-ubuntu22.04
docker pull nvcr.io/nvidia/l4t-cuda:12.6.11-runtime

cd ~/llama.cpp

# 绕开buildkit，约十分钟
DOCKER_BUILDKIT=0 sudo docker build --network host \
  --build-arg HTTP_PROXY=http://127.0.0.1:7897 \
  --build-arg HTTPS_PROXY=http://127.0.0.1:7897 \
  --build-arg NO_PROXY=&quot;localhost,127.0.0.1,ports.ubuntu.com,archive.ubuntu.com,security.ubuntu.com&quot; \
  -t llama-cpp-tegra -f .devops/cuda.Dockerfile .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试编译好的镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo docker run -d \
  --name llama-server \
  --runtime nvidia \
  --gpus all \
  -e NVIDIA_DISABLE_REQUIRE=1 \
  -v ~/ai-models/gguf:/models \
  -p 8080:8080 \
  llama-cpp-tegra \
  -m /models/Meta-Llama-3-8B-Instruct.Q4_K_M.gguf \
  -c 8192 \
  --host 0.0.0.0 \
  --port 8080 \
  -ngl 999
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问 &lt;code&gt;localhost:8080&lt;/code&gt; 就可以看到llama.cpp的对话页面了。&lt;/p&gt;
&lt;h1&gt;4 llama.cpp部署中模型&lt;/h1&gt;
&lt;p&gt;最近看上一款模型，是qwen3.5用opus指令蒸馏过的27b稠密模型：&lt;a href=&quot;https://huggingface.co/Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-GGUF&quot;&gt;Jackrong/Qwen3.5-27B-Claude-4.6-Opus-Reasoning-Distilled-GGUF&lt;/a&gt;。既然agx orin有64g统一内存，所以可以试试Q8_0.gguf（约27G）奢侈一把。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo docker run -d \
  --name llama-server \
  --runtime nvidia \
  --gpus all \
  -e NVIDIA_DISABLE_REQUIRE=1 \
  -v ~/ai-models/gguf:/models \
  -p 8080:8080 \
  llama-cpp-tegra \
  -m /models/Qwen3.5-27B.Q4_K_M.gguf \
  -c 32768 \
  --host 0.0.0.0 \
  --port 8080 \
  -ngl 999
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内存绰绰有余，但是生成速度堪忧，设备统一内存带宽也就是204GB/s，然鹅，面对27G的模型，计算速度的物理上限也就是 204 ÷ 26 ≈ 7.8 tokens/s，再去掉总线调度损耗和KV Cache损耗，实际生成速度就是极限的50%~70%，于是我观察到的速度大概为 4 token/s，对于阅读速度稍快的中国人来说，不能忍，得换Q4_K_M的。&lt;/p&gt;
&lt;p&gt;不过这两天谷歌有发布了gemma-4，这是个原生多模态，搞一个试试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 加载大号模型前可以先清一下内存
sudo sync &amp;amp;&amp;amp; sudo sysctl -w vm.drop_caches=3

sudo docker run -d \
  --name llama-cpp-gemma-4-26B-A4B-it-UD-Q8_K_XL \
  --runtime nvidia \
  --gpus all \
  --ulimit memlock=-1:-1 \
  -e NVIDIA_DISABLE_REQUIRE=1 \
  -e GGML_CUDA_NO_PINNED=1 \
  -e GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 \
  -e GGML_CUDA_FORCE_MMQ=1 \
  -v ~/ai-models/gguf:/models \
  -p 8081:8080 \
  llama-cpp-tegra \
  -m /models/gemma-4-26B-A4B-it-UD-Q8_K_XL.gguf \
  --mmproj /models/gemma-4-26B-A4B-it-mmproj-BF16.gguf \
  -c 131072 \
  -fa on \
  -ctk q8_0 \
  -ctv q8_0 \
  -np 1 \
  --host 0.0.0.0 \
  --port 8080 \
  -ngl 999
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再测试千问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo docker run -d \
  --name llama-cpp-Qwen3.5-35B-A3B-Q8_K_XL \
  --runtime nvidia \
  --gpus all \
  --ulimit memlock=-1:-1 \
  -e NVIDIA_DISABLE_REQUIRE=1 \
  -e GGML_CUDA_NO_PINNED=1 \
  -e GGML_CUDA_ENABLE_UNIFIED_MEMORY=1 \
  -e GGML_CUDA_FORCE_MMQ=1 \
  -v ~/ai-models/gguf:/models \
  -p 8081:8080 \
  llama-cpp-tegra \
  -m /models/Qwen3.5-35B-A3B-UD-Q8_K_XL.gguf \
  --mmproj /models/Qwen3.5-35B-A3B-UD-Q8_K_XL-mmproj-BF16.gguf \
  -c 65536 \
  -fa on \
  -ctk q8_0 \
  -ctv q8_0 \
  --host 0.0.0.0 \
  --port 8080 \
  -ngl 999
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;5 LiteLLM模型汇聚网关&lt;/h1&gt;
&lt;p&gt;为了物尽其用，每个 Orin AGX 64G 都耗尽了显存，我需要在第三个设备上搭一个模型汇聚网关，做成类似 OpenAI API 那样可以选择模型，并把请求分发到各个 end point 上的的网关，Gemini 推荐使用 LiteLLM 的 docker 部署。就是个转发代理，还需要长期开，就把这玩意放在 rock 5b 上了。&lt;/p&gt;
&lt;p&gt;写好网关配置&lt;code&gt;vim ~/litellm/config.yaml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;model_list:
  - model_name: gemma-4-26B-A4B-it-UD-Q8_K_XL
    litellm_params:
      model: openai/gemma-4-26B-A4B-it-UD-Q8_K_XL
      api_base: http://192.168.1.138:8081/v1
      api_key: sk-1234

  - model_name: Qwen3.5-35B-A3B-Q8_K_XL
    litellm_params:
      model: openai/Qwen3.5-35B-A3B-Q8_K_XL
      api_base: http://192.168.1.139:8081/v1
      api_key: sk-1234
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加载配置即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 网不好可以先 pull
docker pull docker.litellm.ai/berriai/litellm:main-latest

docker run -d \
  -v $(pwd)/config.yaml:/app/config.yaml \
  -p 4001:4000 \
  --name litellm-proxy \
  docker.litellm.ai/berriai/litellm:main-latest \
  --config /app/config.yaml \
  --detailed_debug
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试模型列表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://localhost:4001/v1/models
{&quot;data&quot;:[{&quot;id&quot;:&quot;gemma-4-26B-A4B-it-UD-Q8_K_XL&quot;,&quot;object&quot;:&quot;model&quot;,&quot;created&quot;:1677610602,&quot;owned_by&quot;:&quot;openai&quot;},{&quot;id&quot;:&quot;Qwen3.5-35B-A3B-Q8_K_XL&quot;,&quot;object&quot;:&quot;model&quot;,&quot;created&quot;:1677610602,&quot;owned_by&quot;:&quot;openai&quot;}],&quot;object&quot;:&quot;list&quot;}%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试gemma-4反应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; curl -X POST &quot;http://localhost:4001/v1/chat/completions&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -H &quot;Authorization: Bearer sk-1234&quot; \
  -d &apos;{
    &quot;model&quot;: &quot;gemma-4-26B-A4B-it-UD-Q8_K_XL&quot;,
    &quot;messages&quot;: [
      {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;你好，请用一句话介绍一下你自己。&quot;}
    ]
  }&apos;
{&quot;id&quot;:&quot;chatcmpl-5RXTGa6ZQ5aW57JGlsDzxOylaWptEVhE&quot;,&quot;created&quot;:1775988908,&quot;model&quot;:&quot;gemma-4-26B-A4B-it-UD-Q8_K_XL&quot;,&quot;object&quot;:&quot;chat.completion&quot;,&quot;system_fingerprint&quot;:&quot;b8764-8fe78a7ca&quot;,&quot;choices&quot;:[{&quot;finish_reason&quot;:&quot;stop&quot;,&quot;index&quot;:0,&quot;message&quot;:{&quot;content&quot;:&quot;我是 Gemma 4，是由 Google DeepMind  开发的开源权重大型语言模型。&quot;,&quot;role&quot;:&quot;assistant&quot;,&quot;reasoning_content&quot;:&quot;*   Language: Chinese (\&quot;你好，请用一句话介绍一下你自己。\&quot;)\n    *   Content: \&quot;Hello, please introduce yourself in one sentence.\&quot;\n\n    *   Identity: Gemma 4.\n    *   Developer: Google DeepMind.\n    *   Nature: Large Language Model (LLM) with open weights.\n    *   Capabilities: Text and image processing (audio for 2B/4B), text-only output.\n    *   Knowledge Cutoff: January 2025.\n\n    *   *Draft 1 (Literal):* 我是 Gemma 4，由 Google DeepMind 开发的大型语言模型。 (I am Gemma 4, a large language model developed by Google DeepMind.)\n    *   *Draft 2 (More descriptive):* 我是 Gemma 4，由 Google DeepMind 开发的开源权 重的多模态大语言模型。 (I am Gemma 4, an open-weights multimodal LLM developed by Google DeepMind.)\n    *   *Refining for \&quot;one sentence\&quot; and \&quot;identity\&quot;:* The prompt asks for an introduction in one sentence.\n\n    *   \&quot;我是 Gemma 4，是由 Google DeepMind 开发的开源权重大型语言模型。\&quot; (I am Gemma 4, an open-weights large language model developed by Google DeepMind.)\n\n    *   Identify as Gemma 4? Yes.\n    *   Mention Google DeepMind? Yes.\n    *   One sentence? Yes.\n\n    \&quot;我是 Gemma 4，是由 Google DeepMind 开发的开源权重大型语言模型。\&quot;&quot;,&quot;provider_specific_fields&quot;:{&quot;refusal&quot;:null,&quot;reasoning_content&quot;:&quot;*   Language: Chinese (\&quot;你好，请用一句话介绍一下你自己。\&quot;)\n    *   Content: \&quot;Hello, please introduce yourself in one sentence.\&quot;\n\n    *   Identity: Gemma 4.\n    *   Developer: Google DeepMind.\n    *   Nature: Large Language Model (LLM) with open weights.\n    *   Capabilities: Text and image processing (audio for 2B/4B), text-only output.\n    *   Knowledge Cutoff: January 2025.\n\n    *   *Draft 1 (Literal):* 我是 Gemma 4，由 Google DeepMind 开发的大型语言模型。 (I am Gemma 4, a large language model developed by Google DeepMind.)\n    *   *Draft 2 (More descriptive):* 我是 Gemma 4，由 Google DeepMind 开发的开源权重的多模态大 语言模型。 (I am Gemma 4, an open-weights multimodal LLM developed by Google DeepMind.)\n    *   *Refining for \&quot;one sentence\&quot; and \&quot;identity\&quot;:* The prompt asks for an introduction in one sentence.\n\n    *   \&quot;我是 Gemma 4，是由 Google DeepMind 开发的开源权重大型语言模型。\&quot; (I am Gemma 4, an open-weights large language model developed by Google DeepMind.)\n\n    *   Identify as Gemma 4? Yes.\n    *   Mention Google DeepMind? Yes.\n    *   One sentence? Yes.\n\n    \&quot;我是 Gemma 4，是由 Google DeepMind 开发的开源权重大型语言模型。\&quot;&quot;}},&quot;provider_specific_fields&quot;:{}}],&quot;usage&quot;:{&quot;completion_tokens&quot;:353,&quot;prompt_tokens&quot;:25,&quot;total_tokens&quot;:378,&quot;prompt_tokens_details&quot;:{&quot;cached_tokens&quot;:0}},&quot;timings&quot;:{&quot;cache_n&quot;:0,&quot;prompt_n&quot;:25,&quot;prompt_ms&quot;:254.532,&quot;prompt_per_token_ms&quot;:10.181280000000001,&quot;prompt_per_second&quot;:98.21947731522953,&quot;predicted_n&quot;:353,&quot;predicted_ms&quot;:16617.949,&quot;predicted_per_token_ms&quot;:47.076342776203965,&quot;predicted_per_second&quot;:21.242091909176036}}%
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试qwen3.5反应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST &quot;http://localhost:4001/v1/chat/completions&quot; \
  -H &quot;Content-Type: application/json&quot; \
  -H &quot;Authorization: Bearer sk-1234&quot; \
  -d &apos;{
    &quot;model&quot;: &quot;Qwen3.5-35B-A3B-Q8_K_XL&quot;,
    &quot;messages&quot;: [
      {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: &quot;Hello, please write a very short python script to print hello world.&quot;}
    ]
  }&apos;
{&quot;id&quot;:&quot;chatcmpl-gCLU58UkI3h51hgucpWwucmTJ1yX7qox&quot;,&quot;created&quot;:1775988956,&quot;model&quot;:&quot;Qwen3.5-35B-A3B-Q8_K_XL&quot;,&quot;object&quot;:&quot;chat.completion&quot;,&quot;system_fingerprint&quot;:&quot;b8762-073bb2c20&quot;,&quot;choices&quot;:[{&quot;finish_reason&quot;:&quot;stop&quot;,&quot;index&quot;:0,&quot;message&quot;:{&quot;content&quot;:&quot;```python\nprint(\&quot;hello world\&quot;)\n```&quot;,&quot;role&quot;:&quot;assistant&quot;,&quot;reasoning_content&quot;:&quot;Thinking Process:\n\n1.  **Analyze the Request:**\n    *   Task: Write a very short Python script.\n    *   Functionality: Print \&quot;hello world\&quot;.\n    *   Constraint: \&quot;very short\&quot;.\n\n2.  **Determine the Solution:**\n    *   The standard way to print in Python is `print(\&quot;hello world\&quot;)`.\n    *   This is already one line and very short.\n\n3.  **Draft the Code:**\n    ```python\n    print(\&quot;hello world\&quot;)\n    ```\n\n4.  **Review against Constraints:**\n    *   Is it Python? Yes.\n    *   Is it very short? Yes (one line).\n    *   Does it print \&quot;hello world\&quot;? Yes.\n\n5.  **Final Output Generation:** (Just the code block).\n&quot;,&quot;provider_specific_fields&quot;:{&quot;refusal&quot;:null,&quot;reasoning_content&quot;:&quot;Thinking Process:\n\n1.  **Analyze the Request:**\n    *   Task: Write a very short Python script.\n    *   Functionality: Print \&quot;hello world\&quot;.\n    *   Constraint: \&quot;very short\&quot;.\n\n2.  **Determine the Solution:**\n    *   The standard way to print in Python is `print(\&quot;hello world\&quot;)`.\n    *   This is already one line and very short.\n\n3.  **Draft the Code:**\n    ```python\n    print(\&quot;hello world\&quot;)\n    ```\n\n4.  **Review against Constraints:**\n    *   Is it Python? Yes.\n    *   Is it very short? Yes (one line).\n    *   Does it print \&quot;hello world\&quot;? Yes.\n\n5.  **Final Output Generation:** (Just the code block).\n&quot;}},&quot;provider_specific_fields&quot;:{}}],&quot;usage&quot;:{&quot;completion_tokens&quot;:188,&quot;prompt_tokens&quot;:24,&quot;total_tokens&quot;:212,&quot;prompt_tokens_details&quot;:{&quot;cached_tokens&quot;:0}},&quot;timings&quot;:{&quot;cache_n&quot;:0,&quot;prompt_n&quot;:24,&quot;prompt_ms&quot;:290.856,&quot;prompt_per_token_ms&quot;:12.119,&quot;prompt_per_second&quot;:82.5150589982672,&quot;predicted_n&quot;:188,&quot;predicted_ms&quot;:10157.401,&quot;predicted_per_token_ms&quot;:54.02872872340426,&quot;predicted_per_second&quot;:18.508671657247756}}%
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6 部署 Hermes&lt;/h1&gt;
&lt;p&gt;我不喜欢龙虾，一个性格使然不喜欢凑热闹，另外就是那个项目几个月前就已经出现项目管理崩溃的迹象了，issue和pr数量极度扭曲。这两天看到 hermes-agent 倒是想试试。&lt;/p&gt;
&lt;h2&gt;Pre-Requisites&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/firecrawl/firecrawl&quot;&gt;firecrawl&lt;/a&gt; 是个网页清洗工具，这玩意是基于 Playwright 的相当重型，CPU和内存压力都不会小，rk3588 大概率受不了，CIX P1在办公室还关机了，只能暂时装我电脑上了，脚本参考 &lt;a href=&quot;https://www.youtube.com/watch?v=THA8Fov44QY&quot;&gt;Hermes Agent Full Setup Tutorial: How to Setup Your First AI Agent (Gemma 4)&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/firecrawl/firecrawl.git
cd firecrawl

cat &amp;gt; .env &amp;lt;&amp;lt; &apos;EOF&apos;
PORT=3002
HOST=0.0.0.0
USE_DB_AUTHENTICATION=false
BULL_AUTH_KEY=somePASSword
EOF

sed -i &apos;s|# image: ghcr.io/firecrawl/firecrawl|image: ghcr.io/firecrawl/firecrawl|&apos; ./docker-compose.yaml
sed -i &apos;s|  build: apps/api|  # build: apps/api|&apos; docker-compose.yaml
sed -i &apos;s|# image: ghcr.io/firecrawl/playwright-service:latest|image: ghcr.io/firecrawl/playwright-service:latest|&apos; docker-compose.yaml
sed -i &apos;s|    build: apps/playwright-service-ts|    # build: apps/playwright-service-ts|&apos; docker-compose.yaml

docker compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装 Hermes&lt;/h2&gt;
&lt;p&gt;目前装在我电脑的wsl2里，有时间得弄到o6n上，这个服务需要长时间开机，我舍不得电费。&lt;/p&gt;
&lt;p&gt;安装过程需要注意的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;配置模型 API 时，写上面的 LiteLLM 的 URL &lt;code&gt;http://192.168.1.120:4001/v1&lt;/code&gt;，接下来模型可以选&lt;code&gt;1&lt;/code&gt;或&lt;code&gt;2&lt;/code&gt;，粗略测了一下，gemma-4的日常任务比qwen3.5强，但qwen3.5写代码比gemma-4强不少：&lt;pre&gt;&lt;code&gt;API base URL [e.g. https://api.example.com/v1]: http://192.168.1.120:4001/v1
API key [optional]:
Verified endpoint via http://192.168.1.120:4001/v1/models (2 model(s) visible)
  Available models:
    1. gemma-4-26B-A4B-it-UD-Q8_K_XL
    2. Qwen3.5-35B-A3B-Q8_K_XL
  Select model [1-2] or type name: 1
Context length in tokens [leave blank for auto-detect]:
Default model set to: 1 (via http://192.168.1.120:4001/v1)
  💾 Saved to custom providers as &quot;192.168.1.120:4001&quot; (edit in config.yaml)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;这玩意支持微信，在选 Messaging Platforms时选上，然后扫码，微信会出来一个 ClawBot。&lt;/li&gt;
&lt;li&gt;配置 Firecrawl 时，因为我把 hermes 跟 firecrawl 的 docker compose 装一起了，直接回车用默认的 &lt;code&gt;http://localhost:3002&lt;/code&gt; 即可。&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;附：Orin AGX 64G的PVE刷机指南：&lt;/h1&gt;
&lt;p&gt;Orin进入Recovery模式分两种状况，一是当Orin处于未开机状态，二是当Orin处于开机状态。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;当处于未开机状态时，需要先长按住②键(Force Recovery键)，然后给Orin接上电源线通电，此时白色指示灯亮起，但进入Recovery模式后是黑屏的，所以此时连接Orin的显示屏不会有什么反应。&lt;/li&gt;
&lt;li&gt;当处于已开机状态时，需要先长按住②键，然后按下③键(Reset键)，先松开③键，再松开②键。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;进入刷机状态时，Ubuntu 上应该出现 recovery 状态的设备（&lt;code&gt;0955:7023&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;agx-flash@agx-flash:~$ lsusb
Bus 002 Device 002: ID 0627:0001 Adomax Technology Co., Ltd QEMU USB Tablet
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 008 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 007 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 006 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 009 Device 003: ID 0955:7023 NVIDIA Corp. APX
Bus 009 Device 002: ID 2109:3431 VIA Labs, Inc. Hub
Bus 009 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 010 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 005 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 004 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
Bus 003 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为以防万一，配上代理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export http_proxy=&quot;http://192.168.1.125:7897&quot;
export https_proxy=&quot;http://192.168.1.125:7897&quot;
export HTTP_PROXY=&quot;http://192.168.1.125:7897&quot;
export HTTPS_PROXY=&quot;http://192.168.1.125:7897&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;什么组件都不装，只刷一个Linux进去，等进了系统要用什么再装什么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sdkmanager --cli --action install --login-type devzone --product Jetson --target-os Linux --version 6.2.2 --target JETSON_AGX_ORIN_TARGETS --select &apos;Jetson Linux&apos; --flash --license accept
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;刷完会问“SDK Manager is about to install SDK components on your Jetson AGX, To install SDK components on your Jetson AGX Orin modules:...”，只刷系统就选择 &lt;strong&gt;2. Skip&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>新年要用新系统</title><link>https://zlotus.github.io/2026/03/08/init-2026-win11/</link><guid isPermaLink="true">https://zlotus.github.io/2026/03/08/init-2026-win11/</guid><description>重！做！系！统！</description><pubDate>Sun, 08 Mar 2026 17:30:25 GMT</pubDate><content:encoded>&lt;p&gt;这个blog两年没更新过了啊！靠着AI打气又拾起来了！&lt;/p&gt;
&lt;p&gt;起因是节前Fusion 360警告我win10不受支持了，Gemini也建议我换个系统，并且疯狂安利Win11，安抚我说Win11已经不是几年前那种费拉不堪的状态了。&lt;/p&gt;
&lt;p&gt;再信AI一次。&lt;/p&gt;
&lt;h1&gt;0 备份&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;尤其是wsl2！&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/wsl_backup/manifests
cd ~/wsl_backup/manifests

#######
# apt #
#######
# 导出所有手动安装的包（排除依赖包）
comm -23 &amp;lt;(apt-mark showmanual | sort -u) &amp;lt;(gzip -dc /var/log/installer/initial-status.gz | sed -n &apos;s/^Package: //p&apos; | sort -u) &amp;gt; apt-manual-packages.txt
# 如果上面命令报错或太复杂，用这个简单的版本（会包含一些基础包）：
apt-mark showmanual &amp;gt; apt-manual-list.txt

###################
# python 和 conda #
###################
# 激活主环境，比如 base 或 pytorch
conda env export --no-builds &amp;gt; conda_environment.yml
pip freeze &amp;gt; requirements.txt

########
# rust #
########
cargo install --list &amp;gt; cargo-crates.txt

########
# node #
########
npm list -g --depth=0 &amp;gt; npm-global-packages.txt

##############
# shell conf #
##############
cd ~
tar -czvf ~/wsl_backup/dotfiles.tar.gz \
    .zshrc \
    .bashrc \
    .profile \
    .zsh_history \
    .bash_history
    .ssh \
    .tmux.conf \
    .fzf.zsh \
    .gitconfig \
    .vimrc \
    .config/nvim \
    .gnupg \
    .condarc \
    .pip \
    .cargo/config.toml \

&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;1 装系统&lt;/h1&gt;
&lt;p&gt;装专业版即可，需要注意，分区别在跟Linux似的一块&lt;code&gt;/&lt;/code&gt;用到底，2T盘可以先给C盘384G（$384 \times 1024 = 393216 \text{ MB}$），剩下的给D盘。使用的时候：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;wsl2放D盘&lt;/li&gt;
&lt;li&gt;尽量用protable软件，全放D盘，那些死皮赖脸要待在C盘的软件再放C盘。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如此，重装的时候不会一锅端。&lt;/p&gt;
&lt;p&gt;另外，装好了第一次见到Windows初始化配置的时候，微软强制登录Microsoft账号。此时&lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;F10&lt;/code&gt;，输入&lt;code&gt;OOBE\BYPASSNRO&lt;/code&gt;自动重启，就可以选“我没有 Internet 连接”跳过账号登录。好处是可以完全自己决定user文件夹的名字。&lt;/p&gt;
&lt;p&gt;进了系统先装驱动，尤其是显卡驱动，&lt;strong&gt;先装完驱动再装wsl2&lt;/strong&gt;，但是主板带的什么MSI Center装完驱动可以卸了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NVIDIA APP 选 &lt;strong&gt;NVIDIA studio&lt;/strong&gt; 驱动，对跑CUDA的场景来说，&lt;strong&gt;Game Ready&lt;/strong&gt; 驱动更新过于频繁。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;2 清理微软的垃圾&lt;/h1&gt;
&lt;p&gt;第一时间防火防盗防微软。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;彻底禁止 Windows 自动更新驱动：
&lt;code&gt;Win&lt;/code&gt; + &lt;code&gt;R&lt;/code&gt; 输入 &lt;code&gt;gpedit.msc&lt;/code&gt;：&lt;strong&gt;计算机配置 -&amp;gt; 管理模板 -&amp;gt; Windows 组件 -&amp;gt; Windows 更新 -&amp;gt; 管理从 Windows 更新提供的更新&lt;/strong&gt;，右侧的 &lt;strong&gt;“Windows 更新不包括驱动程序”&lt;/strong&gt;，选择 &lt;strong&gt;“已启用”&lt;/strong&gt;。如此，让 Windows Update 只推安全补丁和功能更新，绝不会动显卡、网卡、声卡驱动。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;保留 Defender，配置“码农豁免区”
打开 &lt;strong&gt;Windows 安全中心&lt;/strong&gt; -&amp;gt; &lt;strong&gt;病毒和威胁防护&lt;/strong&gt; -&amp;gt; &lt;strong&gt;管理设置&lt;/strong&gt;，下拉找到 &lt;strong&gt;“排除项”&lt;/strong&gt; -&amp;gt; &lt;strong&gt;添加或删除排除项&lt;/strong&gt;，酌情加入：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;D:\WSL&lt;/code&gt; (放 WSL 镜像，防止扫描 vhdx 读写)；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D:\DEV&lt;/code&gt; (放代码)；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D:\MOEDL&lt;/code&gt; (放模型)；&lt;/li&gt;
&lt;li&gt;&lt;em&gt;etc.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;左下角“天气新闻”等等是 &lt;strong&gt;小组件” (Widgets)&lt;/strong&gt;：&lt;code&gt;Win&lt;/code&gt; + &lt;code&gt;R&lt;/code&gt; 输入 &lt;code&gt;gpedit.msc&lt;/code&gt;：&lt;strong&gt;计算机配置 -&amp;gt; 管理模板 -&amp;gt; Windows 组件 -&amp;gt; 小组件&lt;/strong&gt;，右侧的 &lt;strong&gt;“允许小组件”&lt;/strong&gt;，选择 &lt;strong&gt;“已禁用”&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;锁屏时“天气新闻”等待是 &lt;strong&gt;Windows 聚焦 (Windows Spotlight)&lt;/strong&gt; ：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;右键桌面&lt;/strong&gt; -&amp;gt; &lt;strong&gt;“个性化”&lt;/strong&gt; -&amp;gt; 选择 &lt;strong&gt;“锁屏界面”&lt;/strong&gt;，在个 &lt;strong&gt;性化锁屏界面&lt;/strong&gt; 中选择 &lt;strong&gt;“图片” (Picture)&lt;/strong&gt; 。&lt;/li&gt;
&lt;li&gt;再往下看，如果有 &lt;strong&gt;“在锁屏界面上获取花絮、提示、技巧和更多内容”&lt;/strong&gt;，选择 &lt;strong&gt;关闭&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;如果有 &lt;strong&gt;“锁屏界面状态”&lt;/strong&gt;，改为 &lt;strong&gt;无 (None)&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开始菜单里的垃圾，&lt;strong&gt;右键桌面&lt;/strong&gt; -&amp;gt; &lt;strong&gt;“个性化”&lt;/strong&gt; -&amp;gt; &lt;strong&gt;“开始”&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;显示最近添加的应用&lt;/strong&gt; -&amp;gt; &lt;strong&gt;关&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;显示最常用的应用&lt;/strong&gt; -&amp;gt; &lt;strong&gt;关&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在“开始”菜单中……显示最近打开的项目&lt;/strong&gt; -&amp;gt; &lt;strong&gt;关&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;开始菜单的搜索 &lt;strong&gt;强行把本地搜索和 Bing 搜索混合&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Win + R&lt;/code&gt; 输入 &lt;code&gt;gpedit.msc&lt;/code&gt;：&lt;strong&gt;用户配置 (User Configuration)&lt;/strong&gt; -&amp;gt; &lt;strong&gt;管理模板 (Administrative Templates)&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Windows 组件 (Windows Components)&lt;/strong&gt; -&amp;gt; &lt;strong&gt;文件资源管理器 (File Explorer)&lt;/strong&gt;，右侧找：&lt;strong&gt;“关闭文件资源管理器搜索框中的最近搜索条目显示”&lt;/strong&gt;  &lt;em&gt;(Turn off display of recent search entries in the Windows Search box)&lt;/em&gt; ，选择 &lt;strong&gt;“已启用” (Enabled)&lt;/strong&gt; 。重启资源管理器即可，&lt;code&gt;taskkill /f /im explorer.exe; start explorer.exe&lt;/code&gt;。&lt;em&gt;注意：微软经常改这个策略的名字，比如也可能叫“关闭搜索框建议”。&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;也可以执行 &lt;code&gt;reg add &quot;HKCU\Software\Policies\Microsoft\Windows\Explorer&quot; /v &quot;DisableSearchBoxSuggestions&quot; /t REG_DWORD /d 1 /f&lt;/code&gt; 修改。&lt;/li&gt;
&lt;li&gt;关闭 &lt;strong&gt;“搜索要点” (Search Highlights)&lt;/strong&gt;：&lt;strong&gt;设置&lt;/strong&gt; -&amp;gt; &lt;strong&gt;隐私和安全性&lt;/strong&gt; -&amp;gt; &lt;strong&gt;搜索权限&lt;/strong&gt;，往下找到 &lt;strong&gt;“更多设置”&lt;/strong&gt;，关掉 &lt;strong&gt;“显示搜索要点” (Show search highlights)&lt;/strong&gt; 。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;任务栏的搜索框，任务栏空白处 &lt;strong&gt;右键&lt;/strong&gt; -&amp;gt; &lt;strong&gt;“任务栏设置”&lt;/strong&gt;，第一项就是 &lt;strong&gt;“搜索”&lt;/strong&gt;，选择 &lt;strong&gt;“隐藏”&lt;/strong&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Steam/Epic党可以把XBox相关都端了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭 &lt;strong&gt;Xbox Game Bar&lt;/strong&gt;：在 &lt;strong&gt;设置&lt;/strong&gt; -&amp;gt; &lt;strong&gt;游戏&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Xbox Game Bar&lt;/strong&gt;，关闭 &lt;strong&gt;“允许控制器打开 Game Bar”&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;开始菜单搜 &quot;Xbox&quot;，&lt;strong&gt;右键&lt;/strong&gt; -&amp;gt; &lt;strong&gt;卸载&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;唯一例外：游戏模式 (Game Mode) ，留着&lt;/em&gt;，在 &lt;strong&gt;设置&lt;/strong&gt; -&amp;gt; &lt;strong&gt;游戏&lt;/strong&gt; -&amp;gt; &lt;strong&gt;游戏模式&lt;/strong&gt; 里，保持 &lt;strong&gt;开 (ON)。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭 &lt;strong&gt;OneDrive&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解除“文件夹备份”，任务栏右下角的那个“云朵”图标，右上角齿轮进入 &lt;strong&gt;设置&lt;/strong&gt;，找到 &lt;strong&gt;“同步并备份”&lt;/strong&gt; (Sync and backup) -&amp;gt; &lt;strong&gt;“管理备份”&lt;/strong&gt; (Manage backup)，&lt;strong&gt;把“文档”、“图片”、“桌面”全部关掉（Stop backup）。&lt;/strong&gt;，此时系统会把这些文件夹的路径从 &lt;code&gt;C:\Users\&amp;lt;username&amp;gt;\OneDrive\Desktop&lt;/code&gt; 恢复为 &lt;code&gt;C:\Users\&amp;lt;username&amp;gt;\Desktop&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;取消链接：&lt;strong&gt;设置&lt;/strong&gt; 里 -&amp;gt; &lt;strong&gt;“账户”&lt;/strong&gt; (Account)，点击 &lt;strong&gt;“取消链接此电脑”&lt;/strong&gt; (Unlink this PC)。&lt;/li&gt;
&lt;li&gt;卸载：&lt;strong&gt;设置&lt;/strong&gt; -&amp;gt; &lt;strong&gt;应用&lt;/strong&gt; -&amp;gt; &lt;strong&gt;安装的应用&lt;/strong&gt;，搜 &quot;OneDrive&quot; -&amp;gt; &lt;strong&gt;卸载&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;至于 &lt;strong&gt;基于虚拟化的安全性 (VBS)” / HVCI&lt;/strong&gt;，模型和网上的说法都没定论。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Win&lt;/code&gt; 键搜索 &lt;strong&gt;&quot;内核隔离&quot; -&amp;gt; “内存完整性”&lt;/strong&gt;，收益有限，关不关无所谓。&lt;/li&gt;
&lt;li&gt;网上流传的 &lt;code&gt;bcdedit /set hypervisorlaunchtype off&lt;/code&gt; 不能设置， wsl2 要用。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;找回Win10右键菜单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reg add &quot;HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32&quot; /f /ve
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启资源管理器即可。后悔的话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reg delete &quot;HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}&quot; /f
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;3 清理其他垃圾，装Windows下的常用工具&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;搜狗自带桌面壁纸和汪仔，记得删掉和关闭。&lt;/li&gt;
&lt;li&gt;取消WPS的云盘资源管理器图标、取消传输助手自启。&lt;/li&gt;
&lt;li&gt;装&lt;a href=&quot;https://github.com/CherryHQ/cherry-studio&quot;&gt;cherrystudio&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;装no machine。&lt;/li&gt;
&lt;li&gt;Snipaste用portable。&lt;/li&gt;
&lt;li&gt;Motrix用portable。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;装Git：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;winget install --id Git.Git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;装微信QQ：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;winget install Tencent.WeChat
winget install Tencent.QQ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PotPlayer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;winget install Daum.PotPlayer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;装powertoys：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;winget install Microsoft.PowerToys
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;装Everything和Listary：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;winget install voidtools.Everything
winget install -e --id Bopsoft.Listary
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4 安装wsl2&lt;/h1&gt;
&lt;p&gt;有显卡驱动了再装wsl2。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;先装 WSL 本体，管理员 PowerShell：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl --install
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;装完建议重启，确认 WSL 版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl --version
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 D 盘准备一个专门目录 &lt;code&gt;D:\WSL&lt;/code&gt;，安装一个临时 Ubuntu（会默认在 C 盘），启动一次 Ubuntu，完成用户名/密码初始化后退出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wsl --install -d Ubuntu
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;把迁移到 D 盘：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先关掉 WSL：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wsl --shutdown
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;导出为 tar（临时文件建议也放 D 盘）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wsl --export Ubuntu D:\WSL\ubuntu.tar
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;注销原来的 Ubuntu（这一步会删除 C 盘那个实例的注册与磁盘）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wsl --unregister Ubuntu
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;导入到 D 盘（强制 WSL2）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wsl --import Ubuntu D:\WSL D:\WSL\ubuntu.tar --version 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在 Ubuntu 的 ext4.vhdx 就在 &lt;code&gt;D:\WSL\&lt;/code&gt; 下面了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;wsl2网络配置：建议按老方法，默认NAT，不要配自动代理、DNS隧道等配置。装个clash-verge，用自己写的订阅发布程序让Windows、wsl、PVE、rock5b、o6n等设备同步订阅配置，虽然heavy但是最稳妥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[wsl2]
networkingMode=nat
autoProxy=false
dnsTunneling=false
dnsProxy=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新wsl2支持mirror，直接走Windows的网卡，别的没什么问题，但是gemini-cli或codex可能会用不了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[wsl2]
networkingMode=mirrored
dnsTunneling=true
autoProxy=true
firewall=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时wget下载chrome会自动拿到autoproxy配置走clash，但是apt不行，还得手动指定proxy&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo tee /etc/apt/apt.conf.d/99proxy &amp;lt;&amp;lt;&apos;EOF&apos;
Acquire::http::Proxy &quot;http://127.0.0.1:7897&quot;;
Acquire::https::Proxy &quot;http://127.0.0.1:7897&quot;;
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;5 初始化wsl2&lt;/h1&gt;
&lt;p&gt;建议先改主机名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo hostnamectl set-hostname new_name
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用gemini和chatgpt写了个一键安装脚本，也可以一步一步执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
set -e

# ==========================================
#  WSL2 Development Environment Bootstrap
#  Target: Debian/Ubuntu
#  Role:   System, Shell, Rust, Node, Python, Git
# ==========================================

# 颜色定义，方便看日志
GREEN=&apos;\033[0;32m&apos;
BLUE=&apos;\033[0;34m&apos;
RED=&apos;\033[0;31m&apos;
NC=&apos;\033[0m&apos;

log() { echo -e &quot;${GREEN}[SETUP] $1${NC}&quot;; }
info() { echo -e &quot;${BLUE}[INFO] $1${NC}&quot;; }
error() { echo -e &quot;${RED}[ERROR] $1${NC}&quot;; }

# 0. 基础检查
# ------------------------------------------
if [ &quot;$EUID&quot; -eq 0 ]; then
  error &quot;请不要使用 root 运行此脚本，请使用普通用户运行！&quot;
  error &quot;脚本内部会在需要时请求 sudo 权限。&quot;
  exit 1
fi

# 保持 sudo 权限活跃，防止脚本运行中途因为超时等待输入密码
sudo -v
while true; do sudo -n true; sleep 60; kill -0 &quot;$$&quot; || exit; done 2&amp;gt;/dev/null &amp;amp;

# 先把文件建好，后面的 Rust/NVM/Conda 就会以为你已经在使用 zsh 了，
# 它们会自动把 export PATH... 写入这两个文件。
touch &quot;$HOME/.bashrc&quot;
touch &quot;$HOME/.zshrc&quot;

### ========================================
### 1. 系统基础 (System Base)
### ========================================
log &quot;📦 1. 更新系统并安装基础依赖...&quot;

sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y

# 安装必备工具
# build-essential: 编译环境
# software-properties-common:以此支持 add-apt-repository
sudo apt install -y \
  git git-lfs curl wget ca-certificates \
  build-essential pkg-config libssl-dev \
  zip unzip btop tmux zsh vim \
  net-tools iproute2 dnsutils \
  jq tree procps make \
  direnv rsync shellcheck \
  software-properties-common ffmpeg \
  fonts-noto-cjk fonts-wqy-microhei \
  language-pack-zh-hans

# 注：如果要装yq，直接装go版二进制文件。
# 获取并转换系统架构名称 (匹配 mikefarah/yq 的命名规则)
ARCH=$(uname -m)
case $ARCH in
    x86_64) YQ_ARCH=&quot;amd64&quot; ;;
    aarch64) YQ_ARCH=&quot;arm64&quot; ;;
    *) echo &quot;不支持的架构&quot;; exit 1 ;;
esac

# 下载最新版的二进制文件并安装到 /usr/local/bin
wget &quot;https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${YQ_ARCH}&quot;
sudo mv &quot;yq_linux_${YQ_ARCH}&quot; /usr/local/bin/yq
sudo chmod +x /usr/local/bin/yq

# 安装fastfetch，因为sudo后常用 gpg/dirmngr 去连 keyserver，
# gpg/dirmngr 不读 apt 的代理配置，
# 而且 sudo 默认还会把 shell 里的 http_proxy/https_proxy 环境变量“清掉”，
# 导致直连 keyserver，然后被墙阻断，表现为“没反应”。
export http_proxy=&quot;http://127.0.0.1:7897&quot;
export https_proxy=&quot;http://127.0.0.1:7897&quot;

# 或永久添加代理
echo &apos;export http_proxy=&quot;http://127.0.0.1:7897&quot;&apos; &amp;gt;&amp;gt; ~/.bashrc
echo &apos;export https_proxy=&quot;http://127.0.0.1:7897&quot;&apos; &amp;gt;&amp;gt; ~/.bashrc
echo &apos;export http_proxy=&quot;http://127.0.0.1:7897&quot;&apos; &amp;gt;&amp;gt; ~/.zshrc
echo &apos;export https_proxy=&quot;http://127.0.0.1:7897&quot;&apos; &amp;gt;&amp;gt; ~/.zshrc

sudo --preserve-env=http_proxy,https_proxy add-apt-repository ppa:zhangsongcui3371/fastfetch -y
sudo apt update
sudo apt install fastfetch -y

### ========================================
### 2. Rust 工具链 (Rust &amp;amp; Modern CLI)
### ========================================
log &quot;🦀 2. 安装 Rust 环境...&quot;

if ! command -v rustup &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  # 安装 Rustup
  curl --proto &apos;=https&apos; --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
else
  info &quot;Rustup 已安装，跳过.&quot;
fi

# 【关键】加载环境变量，确保 cargo 命令可用
# 如果你分步运行，请务必执行这一行
source &quot;$HOME/.cargo/env&quot;

log &quot;🦀 安装 Cargo Binstall (加速工具安装)...&quot;
if ! command -v cargo-binstall &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  curl -L --proto &apos;=https&apos; --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
fi

log &quot;🦀 安装现代命令行工具 (Rust编写)...&quot;
# 以用户token登录方式，解除 GitHub API 速率限制 (Rate Limiting)  
# cargo-binstall 匿名查询API每小时仅60次，
# 不登录超过将报403，登录将提升到5000次
# Settings -&amp;gt; Developer settings -&amp;gt; Personal access tokens -&amp;gt; Tokens (classic)，
# Expiration 选 &quot;No expiration&quot;，Scopes (权限) 一个都不用勾
export GITHUB_TOKEN=&quot;ghp_xxx&quot;

# bob-nvim: Neovim 版本管理器
# starship: 跨 Shell 提示符
# eza(ls), bat(cat), fd(find), ripgrep(grep), zoxide(cd)
cargo binstall -y \
  eza bat fd-find ripgrep du-dust procs zoxide hyperfine \
  cargo-watch cargo-nextest cargo-update cargo-audit \
  starship bob-nvim topgrade tealdeer zellij

# atuin本地编译更不容易报错glibc的错误
cargo install atuin

# 装了tealdeer可以卸载 ubuntu 自带的 haskell tldr-hs
sudo apt remove -y tldr

### ========================================
### 3. Neovim (via Bob)
### ========================================
log &quot;📝 3. 使用 Bob 安装最新 Neovim...&quot;

# 确保 bob 在路径中
export PATH=&quot;$HOME/.cargo/bin:$PATH&quot;

if command -v bob &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  # 安装最新的稳定版
  bob install stable
  bob use stable
  
  # 这一步是为了确保 nvim 能被系统找到
  # Bob 默认把 nvim 放在 ~/.local/share/bob/nvim-bin
  if [[ &quot;:$PATH:&quot; != *&quot;:$HOME/.local/share/bob/nvim-bin:&quot;* ]]; then
      info &quot;提示: 请将 $HOME/.local/share/bob/nvim-bin 添加到你的 PATH 中&quot;
  fi
else
  error &quot;Bob 安装失败，跳过 Neovim 安装。&quot;
fi

# 1. 让当前终端立刻生效（临时）
export PATH=&quot;$HOME/.local/share/bob/nvim-bin:$PATH&quot;
# 2. 写入 .bashrc（永久生效，防止下次进 bash 找不到 nvim）
echo &apos;export PATH=&quot;$HOME/.local/share/bob/nvim-bin:$PATH&quot;&apos; &amp;gt;&amp;gt; ~/.bashrc
echo &apos;export PATH=&quot;$HOME/.local/share/bob/nvim-bin:$PATH&quot;&apos; &amp;gt;&amp;gt; ~/.zshrc

# 如果我觉得 neovim 过于毛坯，可以安装 lazyvim: https://www.lazyvim.org/installation
# 备份
for dir in ~/.config/nvim ~/.local/share/nvim ~/.local/state/nvim ~/.cache/nvim; do
    [ -d &quot;$dir&quot; ] &amp;amp;&amp;amp; mv &quot;$dir&quot; &quot;${dir}.bak&quot;
done

git clone https://github.com/LazyVim/starter ~/.config/nvim
rm -rf ~/.config/nvim/.git

# 第一次打开 nvim 要装东西，会比较慢，记得梯子

### ========================================
### 4. Node.js (NVM)
### ========================================
log &quot;🟢 4. 安装 Node.js 环境...&quot;

export NVM_DIR=&quot;$HOME/.nvm&quot;

if [ ! -d &quot;$NVM_DIR&quot; ]; then
  curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
fi

# 【关键】加载 NVM 环境
[ -s &quot;$NVM_DIR/nvm.sh&quot; ] &amp;amp;&amp;amp; . &quot;$NVM_DIR/nvm.sh&quot;

log &quot;🟢 安装 Node LTS...&quot;
nvm install --lts
nvm use --lts
nvm alias default lts/*

log &quot;🟢 安装 pnpm 和常用全局包...&quot;
if ! command -v pnpm &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  curl -fsSL https://get.pnpm.io/install.sh | sh -
fi
pnpm setup

# 临时加载 pnpm 路径
export PATH=&quot;$HOME/.local/share/pnpm:$PATH&quot;

# 安装常用开发工具 
pnpm add -g typescript tsx eslint prettier @google/gemini-cli @openai/codex
# pnpm 交互式构建批准
pnpm approve-builds -g
curl -fsSL https://claude.ai/install.sh | bash
echo &apos;export PATH=&quot;$HOME/.local/bin:$PATH&quot;&apos; &amp;gt;&amp;gt; ~/.bashrc &amp;amp;&amp;amp; source ~/.bashrc

### ========================================
### 5. Python uv and miniconda
### ========================================
log &quot;🐍 5. 安装 Python 环境 (uv)...&quot;

if ! command -v uv &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  curl -Ls https://astral.sh/uv/install.sh | sh
fi

# 临时加载 uv 路径
export PATH=&quot;$HOME/.local/bin:$PATH&quot;

log &quot;🐍 安装 Python 工具链...&quot;
# uv tool install 类似于 pipx，安装到隔离环境
for tool in ruff mypy black poetry httpie; do
    uv tool install &quot;$tool&quot; || true
done

log &quot;🐍 5.5 安装 Miniconda...&quot;

# 定义安装路径 (通常在 ~/miniconda3 或 ~/.miniconda3)
MINICONDA_PATH=&quot;$HOME/miniconda3&quot;

if [ ! -d &quot;$MINICONDA_PATH&quot; ]; then
  # 1. 下载最新的 Linux 安装脚本
  wget &quot;https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-$(uname -m).sh&quot; -O miniconda.sh
  
  # 2. 静默安装 (-b: batch mode, -p: prefix path)
  bash miniconda.sh -b -p &quot;$MINICONDA_PATH&quot;
  
  # 3. 删除安装包
  rm miniconda.sh
  
  # 4. 【关键配置】初始化 Shell (bash 和 zsh)
  # 这步会自动把 conda 的路径写入 .bashrc 和 .zshrc
  &quot;$MINICONDA_PATH/bin/conda&quot; init bash
  &quot;$MINICONDA_PATH/bin/conda&quot; init zsh

  # 5. 【强力推荐】禁止 Conda 默认激活 base 环境
  # 否则每次打开终端前面都有个 (base)，会干扰 uv 和系统 Python
  &quot;$MINICONDA_PATH/bin/conda&quot; config --set auto_activate_base false
  
  log &quot;✅ Miniconda 安装完成 (Base 环境已禁用自动激活)&quot;
else
  info &quot;Miniconda 已存在，跳过安装。&quot;
fi

### ========================================
### 6. Git &amp;amp; GitHub
### ========================================
log &quot;🐙 6. 安装 Git 工具...&quot;

# 6.1 GitHub CLI (gh)
if ! command -v gh &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  info &quot;安装 gh cli...&quot;
  sudo mkdir -p -m 755 /etc/apt/keyrings
  wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg &amp;gt; /dev/null
  sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
  echo &quot;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main&quot; | sudo tee /etc/apt/sources.list.d/github-cli.list &amp;gt; /dev/null
  sudo apt update
  sudo apt install -y gh
fi

# 6.2 Lazygit (自动架构检测)
if ! command -v lazygit &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
  info &quot;安装 lazygit...&quot;
  LAZYGIT_VERSION=$(curl -s &quot;https://api.github.com/repos/jesseduffield/lazygit/releases/latest&quot; | grep -Po &apos;&quot;tag_name&quot;: &quot;v\K[^&quot;]*&apos;)
  ARCH=$(uname -m)
  case $ARCH in
    x86_64) LG_ARCH=&quot;x86_64&quot; ;;
    aarch64) LG_ARCH=&quot;arm64&quot; ;;
    *) error &quot;不支持的架构: $ARCH&quot;; exit 1 ;;
  esac
  
  curl -Lo lazygit.tar.gz &quot;https://github.com/jesseduffield/lazygit/releases/download/v${LAZYGIT_VERSION}/lazygit_${LAZYGIT_VERSION}_Linux_${LG_ARCH}.tar.gz&quot;
  tar xf lazygit.tar.gz lazygit
  sudo install lazygit /usr/local/bin
  rm -f lazygit lazygit.tar.gz
fi

### ========================================
### 7. Shell (Oh-My-Zsh + Starship)
### ========================================
log &quot;💻 7. 配置 Shell 环境...&quot;

# 安装 OMZ
if [ ! -d &quot;$HOME/.oh-my-zsh&quot; ]; then
  info &quot;安装 Oh-My-Zsh...&quot;
  # --unattended: 不自动进入 zsh
  # --keep-zshrc: 如果你已经有配置文件，不覆盖
  sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot; &quot;&quot; --unattended --keep-zshrc
fi

# 安装 fzf
if [ ! -d &quot;$HOME/.fzf&quot; ]; then
  info &quot;安装 FZF (命令行模糊搜索)...&quot;
  git clone --depth 1 https://github.com/junegunn/fzf.git &quot;$HOME/.fzf&quot;
  # --all: 自动启用 key-bindings (Ctrl+R/T) 和 fuzzy-completion
  # --no-bash/--no-fish: 我们只配 zsh，不让它乱改其他 shell
  &quot;$HOME/.fzf/install&quot; --all 
else
  info &quot;FZF 已安装，跳过.&quot;
fi

ZSHRC=&quot;$HOME/.zshrc&quot;

# # 3.1 禁用 OMZ 主题 (让位给 Starship)
# # 我们用 sed 查找并替换 ZSH_THEME=&quot;...&quot; 为 ZSH_THEME=&quot;&quot;
# if grep -q &apos;ZSH_THEME=&apos; &quot;$ZSHRC&quot;; then
#   sed -i &apos;s/^ZSH_THEME=&quot;.*&quot;/ZSH_THEME=&quot;&quot;/&apos; &quot;$ZSHRC&quot;
# fi

# A. 备份现有的 .zshrc (里面有 Conda 和 FZF 的配置，不能丢！)
if [ -f &quot;$ZSHRC&quot; ]; then
  cp &quot;$ZSHRC&quot; &quot;$ZSHRC.pre-bootstrap&quot;
  info &quot;已备份现有配置到 .zshrc.pre-bootstrap&quot;
fi

# B. 获取 OMZ 官方模板 (作为地基)
# 我们把模板下载下来，追加到现有的 .zshrc 后面，这样既保留了 Conda/FZF，又有了 OMZ 的功能
if [ ! -f &quot;$HOME/.oh-my-zsh/templates/zshrc.zsh-template&quot; ]; then
    # 万一模板没下载下来，手动抓取
    curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/templates/zshrc.zsh-template &amp;gt; temp_zshrc
else
    cp &quot;$HOME/.oh-my-zsh/templates/zshrc.zsh-template&quot; temp_zshrc
fi

# C. 合并配置 (关键一步！)
# 1. 保留 Conda/FZF (在文件头部)
# 2. 插入 OMZ 模板 (在中间)
# 3. 插入我们自定义的 PATH 和 Tool (在底部)

# 读取现有内容 (Conda + FZF)
CURRENT_CONTENT=$(cat &quot;$ZSHRC&quot;)

# 覆写 .zshrc：先放 OMZ 模板，再放 原内容
cat temp_zshrc &amp;gt; &quot;$ZSHRC&quot;
echo &quot;&quot; &amp;gt;&amp;gt; &quot;$ZSHRC&quot;
echo &quot;$CURRENT_CONTENT&quot; &amp;gt;&amp;gt; &quot;$ZSHRC&quot;
rm temp_zshrc

# D. 修改配置 (现在里面有东西了，可以改了)

# 1. 禁用 OMZ 主题 (让 Starship 接管)
# 使用 sed 将 ZSH_THEME=&quot;robbyrussell&quot; 替换为 ZSH_THEME=&quot;&quot;
sed -i &apos;s/^ZSH_THEME=&quot;.*&quot;/ZSH_THEME=&quot;&quot;/&apos; &quot;$ZSHRC&quot;

# 2. 启用常用插件 (git, extract, sudo)
# 这是 OMZ 的精华，必须开
sed -i &apos;s/^plugins=(git)/plugins=(git git-extras debian tmux screen history extract colorize web-search docker sudo)/&apos; &quot;$ZSHRC&quot;

# 3. 注入环境变量 (把之前漏在 bashrc 里的补回来)
cat &amp;lt;&amp;lt;EOT &amp;gt;&amp;gt; &quot;$ZSHRC&quot;

# --- Bootstrap Generated Config (PATHs &amp;amp; Tools) ---

# 1. PATH 补全 (Rust, Node, Bob, Local Bin)
# 注意：Miniconda 已经在文件头部配好了，这里不需要重复
export PATH=&quot;\$HOME/.local/bin:\$HOME/.cargo/bin:\$HOME/.local/share/bob/nvim-bin:\$HOME/.local/share/pnpm:\$PATH&quot;

. &quot;\$HOME/.cargo/env&quot;

# 2. 工具启动
eval &quot;\$(atuin init zsh)&quot;     # 启动 atuin (需要在 fzf 初始化之后)
eval &quot;\$(starship init zsh)&quot;  # 启动 Starship
eval &quot;\$(zoxide init zsh)&quot;    # 启动 Zoxide (z 命令)

# 3. 别名 (Aliases)
alias cat=&apos;bat&apos;
alias ls=&apos;eza --icons&apos;                        # 基础 ls 变彩色+图标
alias ll=&apos;eza -lah --icons --git &apos;            # ll 显示详情、Git状态、隐藏文件
alias lt=&apos;eza -lah --tree --level=2 --icons&apos;  # lt 显示目录树(2层)
alias vi=&apos;nvim&apos;
alias vim=&apos;nvim&apos;
alias zj=&apos;zellij&apos;
alias ra=&apos;ranger&apos; # 如果装了 ranger

# 4. NVM
export NVM_DIR=&quot;\$HOME/.nvm&quot;
[ -s &quot;\$NVM_DIR/nvm.sh&quot; ] &amp;amp;&amp;amp; . &quot;\$NVM_DIR/nvm.sh&quot;  # This loads nvm
[ -s &quot;\$NVM_DIR/bash_completion&quot; ] &amp;amp;&amp;amp; \. &quot;\$NVM_DIR/bash_completion&quot;  # This loads nvm bash_completion

# 5. pnpm
export PNPM_HOME=&quot;\$HOME/.local/share/pnpm&quot;
case &quot;:\$PATH:&quot; in
  *&quot;:\$PNPM_HOME:&quot;*) ;;
  *) export PATH=&quot;\$PNPM_HOME:\$PATH&quot; ;;
esac
# pnpm end

EOT

# 1. 删掉顶部的那行 FZF 配置 (使用 sed)
#sed -i &apos;/\[ -f ~\/.fzf.zsh \] &amp;amp;&amp;amp; source ~\/.fzf.zsh/d&apos; &quot;$ZSHRC&quot;
# 2. 把它追加到文件最末尾
#echo &apos;[ -f ~/.fzf.zsh ] &amp;amp;&amp;amp; source ~/.fzf.zsh&apos; &amp;gt;&amp;gt; &quot;$ZSHRC&quot;

# 修改系统shell
chsh -s $(which zsh)

log &quot;✅ .zshrc 修复完成！(Conda + FZF + OMZ + Tools 全部合体)&quot;
log &quot;👉 现在输入 &apos;zsh&apos; 进入新环境吧！&quot;

# 安装 ohmytmux
curl -fsSL &quot;https://github.com/gpakosz/.tmux/raw/refs/heads/master/install.sh#$(date +%s)&quot; | bash

# 安装docker
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5.1 关于&lt;code&gt;yt-dlp&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 建议使用default安装
# 直接`uv tool install yt-dlp --with curl_cffi --with pycryptodomex --upgrade --force`会导致curl_cffi变成最新版
# 但yt-dlp可能不支持这么高版本的curl_cffi，而default的extra会自动补齐requests、urllib3、websockets、mutagen、brotli、pycryptodomex
uv tool install &apos;yt-dlp[default,curl-cffi]&apos; --upgrade --force

# 记得装ffmpeg和nvm node，配置node变量
mkdir -p ~/.config/yt-dlp &amp;amp;&amp;amp; cat &amp;lt;&amp;lt;EOF &amp;gt;&amp;gt; ~/.config/yt-dlp/config
--js-runtimes node
--remote-components ejs:github
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;arm设备可能会遇到&lt;code&gt;curl_cffi&lt;/code&gt;安装了也没有伪装的问题，转回x86平台吧，暂时无解，像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yt-dlp --list-impersonate-targets
[info] Available impersonate targets
Client    OS   Source
--------------------------------------------
Tor       -    curl_cffi&amp;gt;=0.11 (unavailable)
Edge      -    curl_cffi (unavailable)
Firefox   -    curl_cffi&amp;gt;=0.10 (unavailable)
Safari    -    curl_cffi (unavailable)
Chrome    -    curl_cffi (unavailable)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5.2 关于恢复备份的.ssh文件夹&lt;/h2&gt;
&lt;p&gt;拷过去权限得改对&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_*
chmod 644 ~/.ssh/*.pub
# 如果有
chmod 600 ~/.ssh/config
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5.3 关于nomachine&lt;/h2&gt;
&lt;p&gt;安装时如果遇到60%卡住终端没响应，关掉重开一个终端修复一下安装即可，其实是已经装好，之是post install脚本可能有问题挂起了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo killall apt apt-get dpkg
sudo dpkg --configure -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;arm上有时会遇到wayland桌面环境nomachine连上是黑底各种乱七八糟的竖条纹的情况，改回x11就会好：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/gdm3/custom.conf
WaylandEnable=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启&lt;code&gt;gdm3&lt;/code&gt;，或直接重启系统更稳妥。&lt;/p&gt;
&lt;h2&gt;5.4 关于中文环境，上面装了字体和中文编码，需要配置语言环境：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg-reconfigure locales
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选&lt;code&gt;zh_CN.UTF-8 UTF-8&lt;/code&gt;即可，&lt;code&gt;echo $LANG&lt;/code&gt;还不行的话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;export LANGUAGE=zh_CN.zh&quot; &amp;gt;&amp;gt; ~/.bashrc
echo &quot;export LC_ALL=zh_CN.UTF-8&quot; &amp;gt;&amp;gt; ~/.bashrc
echo &quot;export LANG=zh_CN.UTF-8&quot; &amp;gt;&amp;gt; ~/.bashrc
echo &quot;export LANGUAGE=zh_CN.zh&quot; &amp;gt;&amp;gt; ~/.zshrc
echo &quot;export LC_ALL=zh_CN.UTF-8&quot; &amp;gt;&amp;gt; ~/.zshrc
echo &quot;export LANG=zh_CN.UTF-8&quot; &amp;gt;&amp;gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于输入法，就用 Fcitx5 默认，省事：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装 Fcitx5 核心、中文拼音插件及图形化配置工具
sudo apt update
sudo apt install fcitx5 fcitx5-chinese-addons fcitx5-frontend-gtk3 fcitx5-frontend-gtk2 fcitx5-frontend-qt5 kde-config-fcitx5 fcitx5-config-qt

# 将环境变量写入 ~/.xprofile 或 /etc/profile
echo &apos;export GTK_IM_MODULE=fcitx&apos; &amp;gt;&amp;gt; ~/.xprofile
echo &apos;export QT_IM_MODULE=fcitx&apos; &amp;gt;&amp;gt; ~/.xprofile
echo &apos;export XMODIFIERS=@im=fcitx&apos; &amp;gt;&amp;gt; ~/.xprofile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;im-config&lt;/code&gt;，选择&lt;code&gt;fcitx5&lt;/code&gt;，重启，右上角出现 Fcitx5 的小键盘图标，右键点击选择 &lt;strong&gt;配置 (Configure)&lt;/strong&gt;，在右侧列表中找到 &lt;strong&gt;拼音 (Pinyin)&lt;/strong&gt; 并添加到左侧。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;现在使用&lt;code&gt;topgrade&lt;/code&gt;自动感知并更新安装工具。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;5.5 关于starship&lt;/h2&gt;
&lt;p&gt;配置文件可以改好看点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat &amp;gt; ~/.config/starship.toml &amp;lt;&amp;lt; &apos;EOF&apos;
# ==========================
# 1. 机器身份识别 (防错核心)
# ==========================
[hostname]
ssh_only = false    # 即使是本地终端也显示主机名
format = &apos;在 [$hostname]($style) &apos;
style = &quot;bold purple&quot; # 建议：不同机器改成不同颜色，比如测试板用红色，生产环境用紫色

# IP 显示 (仅限 SSH)
[localip]
ssh_only = true     # 性能优化：只有在你通过 SSH 连入时才去查 IP，本地直连时不查
disabled = false
format = &apos;🌐 [$localipv4]($style) &apos;
style = &quot;bold blue&quot;

# ==========================
# 2. 资源报警 (阈值触发，零延迟感知)
# ==========================
# 内存使用率
[memory_usage]
disabled = false
threshold = 80      # 性能优化：内存占用没超过 80% 时完全不渲染此模块，节省算力
format = &apos;🐏 [$ram]($style) &apos;
style = &quot;bold red&quot;

# ==========================
# 3. 运维排障辅助
# ==========================
# 命令执行耗时 (仅当命令执行超过 2 秒时显示)
[cmd_duration]
min_time = 2000
format = &apos;耗时 [$duration]($style) &apos;
style = &quot;bold yellow&quot;

# 退出码状态 (命令报错时变红)
[status]
disabled = false
format = &apos;[$symbol$status]($style) &apos;
success_symbol = &quot;🟢 &quot;
symbol = &quot;🔴 &quot;
style = &quot;bold red&quot;
EOF
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6 tailscale 与 ipv6&lt;/h1&gt;
&lt;p&gt;节后移动给的GPON坏了，运营商换了一个后发现有ipv6了。家里的rock5b、PVE工作站都可以用ipv6直连了。&lt;/p&gt;
&lt;h2&gt;PVE装tailscale&lt;/h2&gt;
&lt;p&gt;需要注意的是，PVE默认没打开ipv6支持，所以需要手动设置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vi /etc/sysctl.conf 

net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
net.ipv6.conf.lo.disable_ipv6 = 0

net.ipv6.conf.all.forwarding = 1

vi /etc/sysctl.d/99-tailscale.conf
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
# 或：
echo &apos;net.ipv4.ip_forward = 1&apos; | tee -a /etc/sysctl.d/99-tailscale.conf
echo &apos;net.ipv6.conf.all.forwarding = 1&apos; | tee -a /etc/sysctl.d/99-tailscale.conf

# 应用网络配置
ifreload -a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时用&lt;code&gt;ip -6 addr show&lt;/code&gt;会发现&lt;code&gt;vmbr0&lt;/code&gt;上多出来一个&lt;code&gt;2409&lt;/code&gt;（移动）开头的地址，再用手机等公网设备打开tailscale去ping这台PVE就会从relay转到direct了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5: vmbr0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 state UP qlen 1000
    inet6 2409:xxxx:xxx:xxxx:xxxx:xxxx:xxxx:xxxx/64 scope global dynamic mngtmpaddr proto kernel_ra 
       valid_lft 86181sec preferred_lft 14181sec
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;全家搬到tailscale&lt;/h2&gt;
&lt;p&gt;通过rock5b转发流量，可以把家里的192.168.1.x/24都放在tailscale上。在rock5b上配置系统转发：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;net.ipv4.ip_forward = 1&apos; | sudo tee -a /etc/sysctl.conf
echo &apos;net.ipv6.conf.all.forwarding = 1&apos; | sudo tee -a /etc/sysctl.conf
sudo sysctl -p

sudo tailscale up --advertise-routes=192.168.1.0/24 --accept-dns=false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出于安全考虑，被“广播”的网段 Tailscale 默认不信任，需要手动确认：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;登录 &lt;a href=&quot;https://login.tailscale.com/admin/machines&quot;&gt;Tailscale Admin Console&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rock 5B&lt;/strong&gt; 点击右侧的 &lt;strong&gt;三个点 (...)&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;选择 &lt;strong&gt;Edit route settings&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;在弹出的窗口中，勾选广播的网段 &lt;code&gt;192.168.1.0/24&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;点击 &lt;strong&gt;Save&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;此时，手机用运营商的网，打开tailscale，也可以使用&lt;code&gt;192.168.1.0/24&lt;/code&gt;的IP控制家里的设备了。&lt;/p&gt;
&lt;h2&gt;6.1 tailscale可能存在的坑1 - 外带设备o6n回家后只能单向ping&lt;/h2&gt;
&lt;h3&gt;6.1.1 现象&lt;/h3&gt;
&lt;p&gt;在家里的局域网 (&lt;code&gt;192.168.1.0/24&lt;/code&gt;) 有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开发板 A (o6n):&lt;/strong&gt; &lt;code&gt;192.168.1.140&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;开发板 B (Rock5B):&lt;/strong&gt; &lt;code&gt;192.168.1.120&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Windows 11 (含 WSL2):&lt;/strong&gt; &lt;code&gt;192.168.1.125&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;症状：&lt;/strong&gt; 开发板 A (&lt;code&gt;140&lt;/code&gt;) 可以正常 Ping 通局域网内的 B (&lt;code&gt;120&lt;/code&gt;) 和 Windows (&lt;code&gt;125&lt;/code&gt;)，但是 B 和 Windows 无法 Ping 通 A。呈现典型的 &lt;strong&gt;“单向失联”&lt;/strong&gt; 状态。&lt;/p&gt;
&lt;h3&gt;6.1.2 排查&lt;/h3&gt;
&lt;p&gt;遇到单向不通，通常的排查链路是：物理连接 -&amp;gt; 防火墙 -&amp;gt; ARP 解析 -&amp;gt; 路由表。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;排除防火墙与物理隔离：&lt;/strong&gt; 检查了 &lt;code&gt;ufw&lt;/code&gt; 和 &lt;code&gt;iptables&lt;/code&gt;，未发现拦截规则。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;检查 ARP 缓存 (&lt;code&gt;ip neigh show&lt;/code&gt;)：&lt;/strong&gt; 开发板 A 上能成功获取到 B 和 Windows 的 MAC 地址，说明物理层和数据链路层通信正常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;排查代理软件：&lt;/strong&gt; 怀疑是 Windows 端的 Clash TUN 模式劫持了局域网流量。关闭 TUN 模式并使用 &lt;code&gt;ping -S 192.168.1.125 192.168.1.140&lt;/code&gt; 强制指定源 IP，依然不通，排除发件端代理问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;定位核心问题——策略路由 (&lt;code&gt;ip rule&lt;/code&gt;)：&lt;/strong&gt;
在开发板 A (&lt;code&gt;140&lt;/code&gt;) 上执行 &lt;code&gt;ip rule&lt;/code&gt; 发现异常：&lt;pre&gt;&lt;code&gt;5270:   from all lookup 52
32766:  from all lookup main
&lt;/code&gt;&lt;/pre&gt;
执行 &lt;code&gt;ip route show table 52&lt;/code&gt; 发现：&lt;pre&gt;&lt;code&gt;192.168.1.0/24 dev tailscale0
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.1.3 原因&lt;/h3&gt;
&lt;p&gt;网络中存在另一台设备——&lt;strong&gt;开发板 B (Rock5B, &lt;code&gt;192.168.1.120&lt;/code&gt;)&lt;/strong&gt; 通过 Tailscale 执行了 &lt;code&gt;--advertise-routes=192.168.1.0/24&lt;/code&gt;，广播了该局域网的 Subnet Route（子网路由），而开发板 A (&lt;code&gt;140&lt;/code&gt;) 默认接受了该路由。&lt;/p&gt;
&lt;p&gt;Tailscale 会接管系统的策略路由，创建一个高优先级（5270）的路由表 52。
当开发板 A (&lt;code&gt;140&lt;/code&gt;) 收到局域网（比如来自 Windows 或 Rock5B 本身）发来的 Ping 请求时，它需要发送 ICMP Echo Reply（回包）。在查找路由时，它优先命中了表 52 的规则，导致&lt;strong&gt;本该通过物理网卡（wlan0/eth0）发回本地局域网的数据包，被错误地塞进了 Tailscale 的虚拟隧道 (&lt;code&gt;tailscale0&lt;/code&gt;) 中&lt;/strong&gt;，造成了“有去无回”的路由黑洞。&lt;/p&gt;
&lt;h3&gt;6.1.4 解决&lt;/h3&gt;
&lt;p&gt;由于既需要在家时局域网直连，又需要带开发板外出时能通过 Tailscale 访问家里的内网，直接关闭 &lt;code&gt;accept-routes&lt;/code&gt; 并不符合需求。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;最优解：基于源 IP 的智能分流 (Smart Routing)&lt;/strong&gt;
在开发板 A (&lt;code&gt;140&lt;/code&gt;) 上添加一条比 Tailscale 优先级更高（例如 2500）的策略路由规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip rule add from 192.168.1.140 to 192.168.1.0/24 priority 2500 lookup main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;原理解析：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在家时：&lt;/strong&gt; 开发板 A 分配到的物理 IP 是 &lt;code&gt;192.168.1.140&lt;/code&gt;。当局域网设备 Ping 它时，它使用该 IP 作为源 IP 回包，精确命中此规则，流量直接走 &lt;code&gt;main&lt;/code&gt; 表（物理网卡），瞬间连通。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;外出时：&lt;/strong&gt; 开发板 A 连接其他网络，物理 IP 改变（不再是 &lt;code&gt;192.168.1.140&lt;/code&gt;）。该规则失效，系统继续往下匹配，命中 Tailscale 的表 52，从而实现正常的异地组网访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;避坑&lt;/strong&gt;
尽量避免将家中的主路由网段设置为 &lt;code&gt;192.168.1.0/24&lt;/code&gt; 或 &lt;code&gt;192.168.0.0/24&lt;/code&gt; 这种极其常见的默认网段。如果携带开启了异地组网的设备去酒店或单位，一旦公共 Wi-Fi 也是这个网段，就会引发严重的子网冲突（Overlapping Subnets）。建议改成类似 &lt;code&gt;10.88.99.0/24&lt;/code&gt; 的冷门私有网段。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;6.1.5 进阶固化：使用 systemd 实现路由规则开机自启&lt;/h3&gt;
&lt;p&gt;为了在 Armbian/Debian 12 等现代 Linux 环境下一劳永逸地解决此问题，最稳妥的方案是创建一个标准的 &lt;code&gt;systemd&lt;/code&gt; 服务，确保在网络和 Tailscale 均就绪后，自动注入我们的高优先级本地路由规则。&lt;/p&gt;
&lt;h4&gt;Step 1: 编写路由修复脚本&lt;/h4&gt;
&lt;p&gt;创建一个专门的 Shell 脚本，采用“先清理后添加”的逻辑，防止因网络服务重启导致规则重复堆叠。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /usr/local/bin/fix-lan-route.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
# 忽略报错删除可能存在的旧规则，避免重复
ip rule del from 192.168.1.140 to 192.168.1.0/24 priority 2500 lookup main 2&amp;gt;/dev/null

# 添加本地局域网高优先级规则
ip rule add from 192.168.1.140 to 192.168.1.0/24 priority 2500 lookup main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;赋予执行权限：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo chmod +x /usr/local/bin/fix-lan-route.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 2: 编写 systemd 服务文件&lt;/h4&gt;
&lt;p&gt;配置服务依赖，确保脚本在网络连通 (&lt;code&gt;network-online.target&lt;/code&gt;) 且 Tailscale 服务启动 (&lt;code&gt;tailscaled.service&lt;/code&gt;) 之后再执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/systemd/system/fix-lan-route.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内容为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Fix LAN routing conflict with Tailscale
After=network-online.target tailscaled.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/fix-lan-route.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Step 3: 激活服务并验证&lt;/h4&gt;
&lt;p&gt;重载 systemd 守护进程，并将该服务设为开机自启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 重载系统配置
sudo systemctl daemon-reload

# 设置为开机自启
sudo systemctl enable fix-lan-route.service

# 立即启动该服务进行验证
sudo systemctl start fix-lan-route.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;最终验证：&lt;/strong&gt;
在终端执行 &lt;code&gt;ip rule&lt;/code&gt; 命令。如果列表中清晰地出现了 &lt;code&gt;2500: from 192.168.1.140 to 192.168.1.0/24 lookup main&lt;/code&gt;，即代表配置大功告成。从此，无论设备重启还是随时启停 Tailscale，本地局域网的通信策略都将稳如泰山。&lt;/p&gt;
&lt;h2&gt;6.2 tailscale可能存在的坑2 - 子网发布网关rock5b开启TUN导致tailscale无法链接&lt;/h2&gt;
&lt;h3&gt;6.2.1 现象：&lt;/h3&gt;
&lt;p&gt;Clash开TUN以后，tailscale启动报错。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;开启TUN时的异常日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ sudo systemctl status tailscaled.service
● tailscaled.service - Tailscale node agent
     Loaded: loaded (/lib/systemd/system/tailscaled.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2026-03-28 10:00:48 CST; 28s ago
       Docs: https://tailscale.com/docs/
   Main PID: 578694 (tailscaled)
      Tasks: 15 (limit: 18526)
     Memory: 54.6M
        CPU: 878ms
     CGroup: /system.slice/tailscaled.service
             └─578694 /usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=41641

Mar 28 10:00:48 rock-5b tailscaled[578694]: logpolicy: using $STATE_DIRECTORY, &quot;/var/lib/tailscale&quot;
Mar 28 10:00:48 rock-5b tailscaled[578694]: linkChange: in state NoState; PAC or proxyConfig changed; updating routes
Mar 28 10:00:48 rock-5b tailscaled[578694]: got LocalBackend in 56ms
Mar 28 10:00:48 rock-5b tailscaled[578694]: Start
Mar 28 10:00:48 rock-5b tailscaled[578694]: ipnext: active extensions: conn25, portlist, posture, clientupdate, relayserver, taildrop
Mar 28 10:00:48 rock-5b tailscaled[578694]: Backend: logs: be:de27a4d50030bb2fa9eba85172884903b5e6894feb434ee6ece1a4b0f407b618 fe:
Mar 28 10:00:48 rock-5b tailscaled[578694]: control: client.Login(0)
Mar 28 10:00:48 rock-5b tailscaled[578694]: health(warnable=warming-up): error: Tailscale is starting. Please wait.
Mar 28 10:00:48 rock-5b tailscaled[578694]: control: doLogin(regen=false, hasUrl=false)
Mar 28 10:00:53 rock-5b tailscaled[578694]: health(warnable=warming-up): ok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ tailscale status
unexpected state: NoState
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;关闭tun后的正常日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ sudo systemctl status tailscaled.service
● tailscaled.service - Tailscale node agent
     Loaded: loaded (/lib/systemd/system/tailscaled.service; enabled; vendor preset: enabled)
     Active: active (running) since Sat 2026-03-28 10:01:28 CST; 3s ago
       Docs: https://tailscale.com/docs/
   Main PID: 579582 (tailscaled)
      Tasks: 15 (limit: 18526)
     Memory: 42.3M
        CPU: 708ms
     CGroup: /system.slice/tailscaled.service
             └─579582 /usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=41641

Mar 28 10:01:31 rock-5b tailscaled[579582]: monitor: RTM_DELROUTE: src=198.18.0.1/0, dst=198.18.0.1/32, gw=, outif=16, table=255
Mar 28 10:01:31 rock-5b tailscaled[579582]: LinkChange: major, rebinding: old: interfaces.State{defaultRoute=enP4p65s0 ifs={Meta:[198.18.0.&amp;gt;
Mar 28 10:01:31 rock-5b tailscaled[579582]: dns: Set: {DefaultResolvers:[] Routes:{} SearchDomains:[] Hosts:6}
Mar 28 10:01:31 rock-5b tailscaled[579582]: dns: Resolvercfg: {Routes:{} Hosts:6 LocalDomains:[]}
Mar 28 10:01:31 rock-5b tailscaled[579582]: dns: OScfg: {}
Mar 28 10:01:31 rock-5b tailscaled[579582]: wgengine: set DNS config again after major link change
Mar 28 10:01:31 rock-5b tailscaled[579582]: router: portUpdate(port=41641, network=udp6)
Mar 28 10:01:31 rock-5b tailscaled[579582]: Rebind; defIf=&quot;enP4p65s0&quot;, ips=[192.168.1.120/24 2409:8a70:b61:b91::1005/128 2409:8a70:b61:b91:&amp;gt;
Mar 28 10:01:31 rock-5b tailscaled[579582]: magicsock: 0 active derp conns
Mar 28 10:01:31 rock-5b tailscaled[579582]: router: portUpdate(port=41641, network=udp4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ tailscale status
100.xx.xx.xx     rock-5b              xxx@  linux    -
100.xx.xx.xx     server               xxx@  linux    offline, last seen xxd ago
100.xx.xx.xx     phone1               xxx@  android  offline, last seen xxd ago
100.xx.xx.xx     orion-o6             xxx@  linux    offline, last seen xxh ago
100.xx.xx.xx     phone2               xxx@  android  offline, last seen xxh ago
100.xx.xx.xx     macbook-pro          xxx@  macOS    offline, last seen xxd ago
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.2.2 原因&lt;/h3&gt;
&lt;p&gt;简单来说，&lt;strong&gt;Clash 的 TUN 模式把 Tailscale 的建连流量给“劫持”了。&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Clash TUN 的霸道机制&lt;/strong&gt;：当开启 Clash TUN 模式时，它会在系统里建一个虚拟网卡（比如 &lt;code&gt;Mihomo&lt;/code&gt;），并通过修改系统路由表（下发 &lt;code&gt;0.0.0.0/1&lt;/code&gt; 这种默认路由）或者用 &lt;code&gt;fwmark&lt;/code&gt;（防火墙标记）把机器上&lt;strong&gt;所有的流量&lt;/strong&gt;都强行吸进自己的核心里。同时，它通常还会开启 Fake-IP 来劫持 DNS 请求（日志里关闭 TUN 时出现的 &lt;code&gt;198.18.0.1&lt;/code&gt; 就是 Clash 的 Fake-IP 网段）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tailscale 的迷失&lt;/strong&gt;：&lt;code&gt;tailscaled&lt;/code&gt; 服务启动时，需要向它自己的控制面板（&lt;code&gt;login.tailscale.com&lt;/code&gt;）和分布在全球的 DERP 中继服务器发送 UDP 握手包来建立状态。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;死锁形成&lt;/strong&gt;：开启 TUN 后，Tailscale 刚发出建连请求，就被 Clash 一把抓走。如果这些流量被 Clash 丢到了错误的代理节点，或者由于 Fake-IP 导致 Tailscale 拿不到中继服务器的真实 IP，它的握手包就永远发不出去，一直卡在 &lt;code&gt;NoState&lt;/code&gt; 等待响应。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;6.2.3 解决&lt;/h3&gt;
&lt;p&gt;思路很简单：&lt;strong&gt;给 Clash 设个规矩，让它对 Tailscale 相关的 IP、域名和进程“高抬贵手”，直接放行。&lt;/strong&gt;&lt;/p&gt;
&lt;h4&gt;添加 Merge (合并) 路由规则&lt;/h4&gt;
&lt;p&gt;为了万无一失，我们需要让 Mihomo 核心彻底放行 &lt;code&gt;tailscaled&lt;/code&gt; 进程和相关域名的流量。&lt;/p&gt;
&lt;p&gt;将以下规则添加到直连规则（Direct）中，并且要放在规则列表的 &lt;strong&gt;最前面&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rules:
  # 直连 Tailscale 后台守护进程
  - PROCESS-NAME,tailscaled,DIRECT
  # 直连 Tailscale 的内网网段
  - IP-CIDR,100.64.0.0/10,DIRECT
  # 直连 Tailscale 的控制面域名
  - DOMAIN-SUFFIX,tailscale.com,DIRECT
  - DOMAIN-SUFFIX,tailscale.io,DIRECT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;修复 Fake-IP 污染（关键）&lt;/strong&gt;：如果 Clash 配置里开启了 Fake-IP，必须把 Tailscale 的域名加入 &lt;code&gt;fake-ip-filter&lt;/code&gt;，否则它连不上中继。在配置（实际配置位于&lt;code&gt;dns_config.yaml&lt;/code&gt;）中补充：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dns:
  fake-ip-filter:
    - &apos;*.tailscale.com&apos;
    - &apos;*.tailscale.io&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;应用这些设置并重启 Clash 的 TUN 开关后，Clash 就会把 Tailscale 的建连流量和 &lt;code&gt;100.x.x.x&lt;/code&gt; 的内网流量原封不动地交还给系统路由表，两边就能和平共处了。&lt;/p&gt;
&lt;h4&gt;注：关于如何查找配置文件位置&lt;/h4&gt;
&lt;p&gt;在 Linux 中，要查看一个进程在启动时到底读取了哪些文件，最强大且标准的工具是 &lt;code&gt;strace&lt;/code&gt;。它可以追踪进程调用的所有系统 API，包括打开文件的操作。&lt;/p&gt;
&lt;p&gt;因为 &lt;code&gt;clash-verge&lt;/code&gt; 是基于 Tauri 的应用，它会启动多个子进程（前端渲染、后端服务等），所以需要加上特定参数来追踪所有子进程，并过滤出跟配置文件相关的操作。&lt;/p&gt;
&lt;h5&gt;Step1: 进程追踪&lt;/h5&gt;
&lt;p&gt;安装 &lt;code&gt;strace&lt;/code&gt; ，用 &lt;code&gt;strace&lt;/code&gt; 运行组合命令并输出日志，将 &lt;code&gt;strace&lt;/code&gt; 加在 clash 环境变量之后、 &lt;code&gt;clash-verge&lt;/code&gt; 之前。为了防止终端输出被海量的系统调用淹没，把日志重定向到一个文本文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install strace

WEBKIT_DISABLE_COMPOSITING_MODE=1 WEBKIT_DISABLE_DMABUF_RENDERER=1 WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1 GDK_BACKEND=x11 strace -f -e trace=open,openat clash-verge 2&amp;gt; startup_trace.log
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数解释：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-f&lt;/code&gt;：告诉 &lt;code&gt;strace&lt;/code&gt; 追踪所有由 &lt;code&gt;clash-verge&lt;/code&gt; 派生出来的子进程（关键，因为配置通常是核心子进程读取的）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-e trace=open,openat&lt;/code&gt;：只监听“打开文件”相关的系统调用，过滤掉网络、内存等无关操作。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2&amp;gt; startup_trace.log&lt;/code&gt;：将 &lt;code&gt;strace&lt;/code&gt; 的标准错误输出（它的追踪日志默认输出到这里）保存到 &lt;code&gt;startup_trace.log&lt;/code&gt; 文件中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运行上面这条命令后，等终端停留在之前的状态（说明应用已经启动并转入后台监听），按 &lt;code&gt;Ctrl + C&lt;/code&gt; 结束它。&lt;/p&gt;
&lt;h5&gt;Step2: 分析追踪日志&lt;/h5&gt;
&lt;p&gt;现在当前目录下生成了一个 &lt;code&gt;startup_trace.log&lt;/code&gt; 文件。可以用 &lt;code&gt;grep&lt;/code&gt; 过滤出所有常见的配置文件后缀（如 &lt;code&gt;.yaml&lt;/code&gt;, &lt;code&gt;.yml&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;grep -i -E &apos;\.yaml|\.yml|\.json&apos; startup_trace.log | grep -v &quot;ENOENT&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;参数解释：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-i -E &apos;\.yaml|\.yml|\.json&apos;&lt;/code&gt;：忽略大小写，匹配这三种常见的配置文件格式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;grep -v &quot;ENOENT&quot;&lt;/code&gt;：过滤掉“文件不存在（Error No Entry）”的记录。程序在启动时通常会按照一定顺序去多个默认目录“试探性”地寻找配置文件，过滤掉找不到的记录，剩下的就是它真正成功读取到的文件路径:&lt;pre&gt;&lt;code&gt;➜  ~ grep -i -E &apos;\.yaml|\.yml|\.json&apos; startup_trace.log | grep -v &quot;ENOENT&quot;
[pid 568056] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/verge.yaml&quot;, O_RDONLY|O_CLOEXEC) = 19
[pid 568057] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/config.yaml&quot;, O_RDONLY|O_CLOEXEC) = 19
[pid 568057] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/verge.yaml&quot;, O_RDONLY|O_CLOEXEC) = 19
[pid 568056] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/profiles.yaml&quot;, O_RDONLY|O_CLOEXEC) = 19
[pid 568036] openat(AT_FDCWD, &quot;/home/pi/.config/io.github.clash-verge-rev.clash-verge-rev/window_state.json&quot;, O_RDONLY|O_CLOEXEC) = 20
[pid 568057] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/profiles/RrCXXb9eVReP.yaml&quot;, O_RDONLY|O_CLOEXEC) = 30
[pid 568058] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/profiles/Merge.yaml&quot;, O_RDONLY|O_CLOEXEC &amp;lt;unfinished ...&amp;gt;
[pid 568058] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/dns_config.yaml&quot;, O_RDONLY|O_CLOEXEC) = 21
[pid 568057] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml&quot;, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 21
[pid 568058] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge-check.yaml&quot;, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 21
[pid 568095] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge-check.yaml&quot;, O_RDONLY|O_CLOEXEC) = 3
[pid 568057] openat(AT_FDCWD, &quot;/home/pi/.local/share/io.github.clash-verge-rev.clash-verge-rev/clash-verge.yaml&quot;, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 21```
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;7 self host atuin&lt;/h1&gt;
&lt;p&gt;atuin 的主机间 sync 命令行历史记录非常有用，按照官方说明 &lt;a href=&quot;https://docs.atuin.sh/cli/self-hosting/docker/#docker-compose&quot;&gt;Self Hosting - Docker Compose&lt;/a&gt; 用 Docker Compose 的方式可以很容易的拉起来一个私有服务。之后，在任意需要 atuin sync 的主机上，修改 atuin 的配置文件 &lt;code&gt;~/.config/atuin/config.toml&lt;/code&gt;：&lt;code&gt;sync_address = &quot;http://&amp;lt;server-ip:port&amp;gt;&quot;&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;将本地的shell命令导入 atuin 管理，运行&lt;code&gt;atuin import auto&lt;/code&gt;或&lt;code&gt;atuin import zsh&lt;/code&gt;。注：如果要 atuin 接管 &lt;code&gt;ctrl-r&lt;/code&gt;，则要保证&lt;code&gt;.zshrc&lt;/code&gt;中 atuin 在 fzf 之后初始化。&lt;/p&gt;
&lt;p&gt;接下来，在第一台要同步的主机命令行里，运行首次用户注册：&lt;code&gt;atuin register -u &amp;lt;username&amp;gt; -e &amp;lt;email&amp;gt;&lt;/code&gt;，此时 atuin 会让你运行&lt;code&gt;atuin key&lt;/code&gt;拿到助记词，用来在之后的登录时使用。注册完毕后运行&lt;code&gt;atuin sync&lt;/code&gt;向服务器同步历史记录。完成注册后，再在其他主机上只需要执行登录即可：&lt;code&gt;atuin login -u &amp;lt;username&amp;gt;&lt;/code&gt;再输入助记词即可，成功后&lt;code&gt;atuin sync&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此时&lt;code&gt;ctrl-r&lt;/code&gt;打开 atuin ，多次按&lt;code&gt;ctrl-r&lt;/code&gt;就会在 全局、本机、本次会话、此目录 等环境中切换。在全局环境，选中命令按&lt;code&gt;ctrl-o&lt;/code&gt;进入检视页会看到此命令所属的主机。&lt;/p&gt;
&lt;p&gt;最后设置系统服务，新建&lt;code&gt;sudo vim /etc/systemd/system/atuin.service&lt;/code&gt;，把官方说明中&lt;code&gt;systemd&lt;/code&gt;那段脚本贴进去，然后将&lt;code&gt;WorkingDirectory&lt;/code&gt;换成刚才&lt;code&gt;docker-compose.yml&lt;/code&gt;所在的绝对路径即可，再&lt;code&gt;sudo systemctl daemon-reload&lt;/code&gt;、&lt;code&gt;sudo systemctl enable --now atuin.service&lt;/code&gt;、&lt;code&gt;sudo systemctl status atuin.service&lt;/code&gt;即可看到开机自启的 atuin 服务。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://support.mozilla.org/zh-CN/kb/install-firefox-linux#w_shi-yong-ji-yu-debian-fa-xing-ban-de-deb-bao-an-zhuang-firefoxtui-jian&quot;&gt;firefox esr apt安装说明&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;8 启用 rock 5b 的 pwm&lt;/h1&gt;
&lt;p&gt;风扇的pwm线插在pin 27上，&lt;code&gt;sudo armbian-config&lt;/code&gt;进入TUI界面 -&amp;gt; &lt;code&gt;System&lt;/code&gt; -&amp;gt; &lt;code&gt;Kernel&lt;/code&gt; -&amp;gt; &lt;code&gt;DTO01 - Manage device tree overlays&lt;/code&gt; -&amp;gt; 选 &lt;code&gt;rk3588-pwm7-m3&lt;/code&gt;。或者在&lt;code&gt;/boot/armbianEnv.txt&lt;/code&gt;里&lt;code&gt;overlays&lt;/code&gt;后面续上字符串&lt;code&gt;rk3588-pwm7-m3&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;重启。&lt;/p&gt;
&lt;p&gt;PWM 子系统的 &lt;code&gt;pwmchipN&lt;/code&gt; 编号是按 probe 顺序生成的，每次启动可能不一样，必须看一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo cat /sys/kernel/debug/pwm
platform/febd0030.pwm, 1 PWM device
 pwm-0   ((null)              ): period: 0 ns duty: 0 ns polarity: normal

platform/fd8b0010.pwm, 1 PWM device
 pwm-0   (pwm-fan             ): requested period: 60000 ns duty: 0 ns polarity: normal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;febd0030&lt;/code&gt; 这个地址就是 PWM7（RK3588 PWM7 寄存器基址）。然后看哪个 pwmchip 链接到它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -l /sys/class/pwm/
lrwxrwxrwx - root 29 4月  18:51  pwmchip0 -&amp;gt; ../../devices/platform/fd8b0010.pwm/pwm/pwmchip0
lrwxrwxrwx - root 29 4月  18:51  pwmchip1 -&amp;gt; ../../devices/platform/febd0030.pwm/pwm/pwmchip1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记住这个&lt;code&gt;pwmchip1&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;先写个脚本测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv init pwm7-m3
cd pwm7-m3
uv add python-periphery
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写一个python脚本 &lt;code&gt;fan_pwm.py&lt;/code&gt; 测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;Hardware PWM fan control on Rock 5B PIN_27 (GPIO4_C6 / PWM7_IR_M3).

需先启用 rk3588-pwm7-m3 overlay 并重启。
通过 /sys/kernel/debug/pwm 找到 febd0030.pwm 对应的 pwmchip 号。
&quot;&quot;&quot;

import signal
import sys
import time

from periphery import PWM

# === 改成你机器上实际看到的 pwmchip 号 ===
PWM_CHIP = 1          # pwmchipN 中的 N
PWM_CHANNEL = 0       # Rockchip 每个 chip 只有一个 channel，固定为 0
# =====================================

FREQ_HZ = 25000       # 4 线 PC 风扇标准 PWM 频率
DUTY = 0.30           # 30% 占空比


def main() -&amp;gt; None:
    pwm = PWM(PWM_CHIP, PWM_CHANNEL)
    try:
        pwm.frequency = FREQ_HZ        # 25 kHz
        pwm.duty_cycle = DUTY          # 0.0 ~ 1.0
        pwm.polarity = &quot;normal&quot;
        pwm.enable()

        print(f&quot;PWM7 on PIN_27: {DUTY * 100:.0f}% @ {FREQ_HZ} Hz &quot;
              f&quot;(pwmchip{PWM_CHIP})&quot;)
        print(&quot;Ctrl+C 退出&quot;)

        def shutdown(_sig, _frame):
            print(&quot;\n停止 PWM...&quot;)
            pwm.disable()
            pwm.close()
            sys.exit(0)

        signal.signal(signal.SIGINT, shutdown)
        signal.signal(signal.SIGTERM, shutdown)

        while True:
            time.sleep(1)
    except Exception:
        pwm.close()
        raise


if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行脚本测试 &lt;code&gt;sudo $(which uv) run python fan_pwm.py&lt;/code&gt;，此时风扇应该降速了。&lt;/p&gt;
&lt;p&gt;成功后封装为系统服务，服务运行时不能依赖家目录（特别是如果以 &lt;code&gt;root&lt;/code&gt; 跑），放到 &lt;code&gt;/opt&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mv ~/pwm7-m3 /opt/pwm7-m3
sudo chown -R root:root /opt/pwm7-m3
cd /opt/pwm7-m3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认依赖装好（uv 会在 &lt;code&gt;/opt/pwm7-m3/.venv&lt;/code&gt; 建虚拟环境）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo $(which uv) sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;记下 uv 的绝对路径，service 里要用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;which uv
/home/radxa/.local/bin/uv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于 &lt;code&gt;uv&lt;/code&gt; 装在家目录下（&lt;code&gt;~/.local/bin/uv&lt;/code&gt;），&lt;code&gt;root&lt;/code&gt; 跑服务时找不到，把 uv 放到系统路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo cp ~/.local/bin/uv /usr/local/bin/uv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;微调一下脚本，让它对 &lt;code&gt;SIGTERM&lt;/code&gt;（systemd &lt;code&gt;stop&lt;/code&gt; 发的信号）也能干净退出，并且把输出冲到日志里（journald 会自动收）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;Hardware PWM fan control on Rock 5B PIN_27 (PWM7_IR_M3).&quot;&quot;&quot;

import signal
import sys
import time

from periphery import PWM

PWM_CHIP = 1          # 改成你机器上 febd0030.pwm 对应的 pwmchipN
PWM_CHANNEL = 0
FREQ_HZ = 25000
DUTY = 0.30


def main() -&amp;gt; None:
    pwm = PWM(PWM_CHIP, PWM_CHANNEL)
    pwm.frequency = FREQ_HZ
    pwm.duty_cycle = DUTY
    pwm.polarity = &quot;normal&quot;
    pwm.enable()

    print(f&quot;pwm7-m3: {DUTY * 100:.0f}% @ {FREQ_HZ} Hz on pwmchip{PWM_CHIP}&quot;,
          flush=True)

    stopping = False

    def shutdown(_sig, _frame):
        nonlocal stopping
        stopping = True

    signal.signal(signal.SIGINT, shutdown)
    signal.signal(signal.SIGTERM, shutdown)

    try:
        while not stopping:
            time.sleep(1)
    finally:
        print(&quot;pwm7-m3: stopping&quot;, flush=True)
        try:
            pwm.disable()
        finally:
            pwm.close()


if __name__ == &quot;__main__&quot;:
    sys.exit(main())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;flush=True&lt;/code&gt; 是为了让 &lt;code&gt;print&lt;/code&gt; 立刻被 systemd 抓到日志里，不然 Python 默认会做行缓冲/块缓冲，停服务前可能看不到输出。&lt;/p&gt;
&lt;p&gt;创建 systemd service 文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/systemd/system/pwm7-m3.service

[Unit]
Description=Rock 5B PWM fan control (PIN_27 / PWM7)
After=multi-user.target

[Service]
Type=simple
WorkingDirectory=/opt/fan-pwm
ExecStart=/usr/local/bin/uv run python fan_pwm.py
Restart=on-failure
RestartSec=3
User=root
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Type=simple&lt;/code&gt;：脚本本身就是常驻进程，不 fork，所以 simple 即可。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExecStart&lt;/code&gt; 里 uv 必须用绝对路径——systemd 不读的 shell &lt;code&gt;PATH&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WorkingDirectory&lt;/code&gt; 让 uv 找得到项目里的 &lt;code&gt;pyproject.toml&lt;/code&gt; 和 &lt;code&gt;.venv&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Restart=on-failure&lt;/code&gt;：脚本异常退出就重启，但 &lt;code&gt;systemctl stop&lt;/code&gt; 主动停的时候不会重启。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;User=root&lt;/code&gt;：因为 PWM sysfs 默认只有 root 能写。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;启用 + 启动，&lt;code&gt;enable --now&lt;/code&gt; 等于 &lt;code&gt;enable&lt;/code&gt;（开机自启）+ &lt;code&gt;start&lt;/code&gt;（立刻启动）一起做。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable --now pwm7-m3.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;常用操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 看状态
sudo systemctl status pwm7-m3

# 看实时日志
sudo journalctl -u pwm7-m3 -f

# 停止 / 重启
sudo systemctl stop pwm7-m3
sudo systemctl restart pwm7-m3

# 改完占空比想生效，重启服务即可
sudo systemctl restart pwm7-m3

# 不要开机自启了
sudo systemctl disable pwm7-m3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功后也可以把脚本改成自动控温的模式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;&quot;&quot;Temperature-based PWM fan control for Rock 5B (PIN_27 / PWM7_IR_M3).&quot;&quot;&quot;

import signal
import sys
import time

from periphery import PWM

# === 硬件配置 ===
PWM_CHIP = 1          # febd0030.pwm 对应的 pwmchipN
PWM_CHANNEL = 0
FREQ_HZ = 25000
THERMAL_ZONE = &quot;/sys/class/thermal/thermal_zone0/temp&quot;  # CPU 温度
POLL_INTERVAL = 3.0   # 秒，温度采样间隔

# === 风扇曲线 ===
# (温度 °C, 占空比 0.0~1.0)
# 大风扇没必要跑全速，最高也就 60%
FAN_CURVE = [
    (45.0, 0.30),   # 45°C 以下30%
    (50.0, 0.35),   # 起转点
    (60.0, 0.40),
    (70.0, 0.45),
    (80.0, 0.60),   # 80°C 以上也只到 60%
]

# 滞回，避免在阈值附近反复启停
HYSTERESIS_C = 2.0

# 起转辅助：从 0% 跳到目标值时，先全力踹一下让风扇转起来
KICKSTART_DUTY = 0.50
KICKSTART_SEC = 1.0


def read_temp_c() -&amp;gt; float:
    with open(THERMAL_ZONE) as f:
        return int(f.read().strip()) / 1000.0


def duty_for_temp(temp_c: float, curve: list[tuple[float, float]]) -&amp;gt; float:
    &quot;&quot;&quot;在曲线上做线性插值。&quot;&quot;&quot;
    if temp_c &amp;lt;= curve[0][0]:
        return curve[0][1]
    if temp_c &amp;gt;= curve[-1][0]:
        return curve[-1][1]
    for (t0, d0), (t1, d1) in zip(curve, curve[1:]):
        if t0 &amp;lt;= temp_c &amp;lt;= t1:
            ratio = (temp_c - t0) / (t1 - t0)
            return d0 + (d1 - d0) * ratio
    return curve[-1][1]


def main() -&amp;gt; int:
    pwm = PWM(PWM_CHIP, PWM_CHANNEL)
    pwm.frequency = FREQ_HZ
    pwm.duty_cycle = 0.0
    pwm.polarity = &quot;normal&quot;
    pwm.enable()

    print(f&quot;fan-pwm: started, curve={FAN_CURVE}&quot;, flush=True)

    stopping = False

    def shutdown(_sig, _frame):
        nonlocal stopping
        stopping = True

    signal.signal(signal.SIGINT, shutdown)
    signal.signal(signal.SIGTERM, shutdown)

    last_temp = read_temp_c()
    current_duty = 0.0

    try:
        while not stopping:
            temp = read_temp_c()

            # 滞回：温度变化小于 HYSTERESIS_C 时不动
            if abs(temp - last_temp) &amp;lt; HYSTERESIS_C:
                target = current_duty
            else:
                target = duty_for_temp(temp, FAN_CURVE)
                last_temp = temp

            if target != current_duty:
                # 从停转到要起转 → 先 kickstart
                if current_duty == 0.0 and target &amp;gt; 0.0:
                    pwm.duty_cycle = max(target, KICKSTART_DUTY)
                    print(f&quot;fan-pwm: {temp:.1f}°C  kickstart &quot;
                          f&quot;-&amp;gt; {pwm.duty_cycle * 100:.0f}%&quot;, flush=True)
                    time.sleep(KICKSTART_SEC)

                pwm.duty_cycle = target
                current_duty = target
                print(f&quot;fan-pwm: {temp:.1f}°C  duty -&amp;gt; {target * 100:.0f}%&quot;,
                      flush=True)

            time.sleep(POLL_INTERVAL)
    finally:
        print(&quot;fan-pwm: stopping&quot;, flush=True)
        try:
            pwm.duty_cycle = 0.0
            pwm.disable()
        finally:
            pwm.close()
    return 0


if __name__ == &quot;__main__&quot;:
    sys.exit(main())
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>学习Widevine服务</title><link>https://zlotus.github.io/2024/03/01/spoofing-widevine-service/</link><guid isPermaLink="true">https://zlotus.github.io/2024/03/01/spoofing-widevine-service/</guid><description>尝试通过手写transformer来理解该算法</description><pubDate>Fri, 01 Mar 2024 18:24:00 GMT</pubDate><content:encoded>&lt;p&gt;我不理解，为什么像Netflix、Spotify流媒体服务提供商，包括苹果、谷歌等公司，会使用数字版权管理。他们在我买的设备上，浪费我的硬件算力，加密我买的数字内容，以阻止我下载。君子也防？[doge]&lt;/p&gt;
&lt;p&gt;本文尝试欺骗DRM License Server获取秘钥，来解密被DRM加密的多媒体内容。&lt;/p&gt;
&lt;h2&gt;了解Google Widevine&lt;/h2&gt;
&lt;p&gt;通过阅读文章大致了解了widevine的工作原理，以及DRM（Digital Rights Management，数字版权管理）系统的执行过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.tencent.com/developer/article/1963436&quot;&gt;Google Widevine及其工作原理&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://mp.weixin.qq.com/s?__biz=MzU1NTEzOTM5Mw==&amp;amp;mid=2247522514&amp;amp;idx=1&amp;amp;sn=9f2d0edccde893956864754444269780&amp;amp;chksm=fbda30fcccadb9eab0ae5b1e22e9f06339a8c02d11d0232fda6c16faf88c9bec27a2c66406be&amp;amp;scene=21#wechat_redirect&quot;&gt;构建DRM系统的重要基石——EME、CDM、AES、CENC和密钥&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;获取CDM Device验证文件&lt;/h2&gt;
&lt;p&gt;CDM（内容解密模块）是嵌入在Chrome浏览器或Android操作系统中用来加解密流媒体的闭源模块。因此，我们可以模拟一个安卓设备。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;pywidevine&lt;/code&gt;创建CDM Device。创建设备需要两个关键文件，&lt;code&gt;client_id.bin&lt;/code&gt;和&lt;code&gt;private_key.pem&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;按照帖子&lt;a href=&quot;https://forum.videohelp.com/threads/408031-Dumping-Your-own-L3-CDM-with-Android-Studio?s=f22ce643aaaca3496c59a76a2a78c567&quot;&gt;Dumping Your own L3 CDM with Android Studio&lt;/a&gt;的操作，下载并安装Android Studio，再使用&lt;code&gt;frida-server&lt;/code&gt;获取关键文件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在Android Studio的Device Manager中新建设备，选一台更帖子描述一样的设备，&lt;code&gt;Pixel 6&lt;/code&gt;、&lt;code&gt;Pie&lt;/code&gt;，生成了&lt;code&gt;Pixel 6 API 28&lt;/code&gt;设备，启动后显示&lt;code&gt;emulator-5554&lt;/code&gt;。（我的Andriod Studio装好时，Device Manager中已经有了一部虚拟手机&lt;code&gt;Pixel_3A_API_34_extension_level_7_x86_64&lt;/code&gt;（注意这是一台x86_64的设备），这个设备浏览器解析CDM时貌似有问题）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在Windows下的&lt;code&gt;conda&lt;/code&gt;中新开一个环境安装&lt;code&gt;pip install frida frida-tools&lt;/code&gt;，并确定&lt;code&gt;frida&lt;/code&gt;版本（我使用了&lt;code&gt;16.2.1&lt;/code&gt;），&lt;code&gt;frida&lt;/code&gt;与&lt;code&gt;frida-server&lt;/code&gt;版本需一致。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将下载的&lt;code&gt;frida-server-16.2.1-android-x86&lt;/code&gt;放到&lt;code&gt;C:\Users\&amp;lt;YourUserName&amp;gt;\AppData\Local\Android\Sdk\platform-tools\&lt;/code&gt;目录中，命令行进入该目录，执行：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; .\adb.exe devices
List of devices attached
emulator-5554   device
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到虚拟设备已被识别。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将&lt;code&gt;frida-server&lt;/code&gt;上传到虚拟设备：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; .\adb.exe push .\frida-server-16.2.1-android-x86 /sdcard
.\frida-server-16.2.1-android-x86: 1 file pushed, 0 skipped. 152.8 MB/s (51807740 bytes in 0.323s)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;连接虚拟设备，将&lt;code&gt;frida-server&lt;/code&gt;放在正确的位置，赋予执行权限并启动：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; .\adb.exe shell
su
mv /sdcard/frida-server-16.2.1-android-x86 /data/local/tmp/
chmod +x /data/local/tmp/frida-server-16.2.1-android-x86
/data/local/tmp/frida-server-16.2.1-android-x86
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时server启动，保持终端运行。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;下载&lt;a href=&quot;https://github.com/wvdumper/dumper&quot;&gt;dumper&lt;/a&gt;，进入目录并安装依赖&lt;code&gt;pip install -r requirements.txt&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;运行时报错提示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python .\dump_keys.py
Traceback (most recent call last):
  File &quot;C:\Users\qinzi\pyws\wdevof\dumper\dump_keys.py&quot;, line 6, in &amp;lt;module&amp;gt;
    from Helpers.Scanner import Scan
  File &quot;C:\Users\qinzi\pyws\wdevof\dumper\Helpers\Scanner.py&quot;, line 7, in &amp;lt;module&amp;gt;
    from Helpers.wv_proto2_pb2 import SignedLicenseRequest
  File &quot;C:\Users\qinzi\pyws\wdevof\dumper\Helpers\wv_proto2_pb2.py&quot;, line 33, in &amp;lt;module&amp;gt;
    _descriptor.EnumValueDescriptor(
  File &quot;C:\Users\qinzi\anaconda3\envs\wdev\Lib\site-packages\google\protobuf\descriptor.py&quot;, line 789, in __new__
    _message.Message._CheckCalledFromGeneratedFile()
TypeError: Descriptors cannot be created directly.
If this call came from a _pb2.py file, your generated code is out of date and must be regenerated with protoc &amp;gt;= 3.19.0.
If you cannot immediately regenerate your protos, some other possible workarounds are:
 1. Downgrade the protobuf package to 3.20.x or lower.
 2. Set PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python (but this will use pure-Python parsing and will be much slower).
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此安装建议为PowerShell添加环境变量，再次运行&lt;code&gt;python .\dump_keys.py&lt;/code&gt;即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; $env:PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION = &apos;python&apos;
&amp;gt; $env:PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION
python

&amp;gt; python .\dump_keys.py
2024-03-02 12:22:36 PM - root - 16 - INFO - Connected to Android Emulator 5554
2024-03-02 12:22:36 PM - root - 17 - INFO - scanning all processes for the following libraries
...
2024-03-02 02:17:06 PM - Helpers.Scanner - 82 - INFO - Running libwvhidl.so at 0xec717000
2024-03-02 02:17:06 PM - Helpers.Scanner - 75 - DEBUG - {
    &quot;from&quot;: &quot;Dynamic Function&quot;,
    &quot;message&quot;: &quot;L3 RSA Key export function found: cwkfcplc&quot;
}
...
2024-03-02 12:22:37 PM - root - 25 - INFO - Hooks completed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时dumper启动，保持终端运行。&lt;/p&gt;
&lt;h2&gt;创建CDM设备&lt;/h2&gt;
&lt;p&gt;此时可以使用&lt;code&gt;client_id.bin&lt;/code&gt;和&lt;code&gt;private_key.pem&lt;/code&gt;，通过&lt;code&gt;pywidevine&lt;/code&gt;命令行工具来生成&lt;code&gt;wvd&lt;/code&gt;设备文件了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pywidevine create-device -k ./private_key.pem -c ./client_id.bin -t &quot;ANDROID&quot; -l 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;得到一个名为&lt;code&gt;google_aosp_on_ia_emulator_14.0.0_67016d30_4464_l3.wvd&lt;/code&gt;的设备文件。&lt;/p&gt;
&lt;h2&gt;获取秘钥&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;pywidevine&lt;/code&gt;的例程即可获取秘钥：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pssh&lt;/code&gt;在希望下载的视频描述文件中，可能是浏览器请求的一个&lt;code&gt;https://a.com/b/c.mpd&lt;/code&gt;xml文件或是一个&lt;code&gt;https://a.com/b/c.m3u8&lt;/code&gt;list文件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;device&lt;/code&gt;的路径即为刚才创建的设备文件路径。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;licence&lt;/code&gt;请求的地址，是浏览器加载视频时发出的一次或两次（aduio和video各一次）秘钥请求，URL特征应当非常明显，通常为POST方式，带有二进制负载。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外需要注意的是，CDM秘钥的请求原理上是独立于媒体提供平台的，或是与媒体服务API不同的一套API，因此要使得&lt;code&gt;licence&lt;/code&gt;请求成功，可能还需要从浏览器中复制秘钥请求&lt;code&gt;headers&lt;/code&gt;一并加入&lt;code&gt;requests.post&lt;/code&gt;（如使用浏览器&lt;strong&gt;复制为cURL (bash)&lt;/strong&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH

import requests

# prepare pssh
pssh = PSSH(&quot;AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa&quot;
            &quot;7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==&quot;)

# load device
device = Device.load(&quot;C:/Path/To/A/Provision.wvd&quot;)

# load cdm
cdm = Cdm.from_device(device)

# open cdm session
session_id = cdm.open()

# get license challenge
challenge = cdm.get_license_challenge(session_id, pssh)

# send license challenge (assuming a generic license server SDK with no API front)
licence = requests.post(&quot;https://...&quot;, data=challenge)
licence.raise_for_status()

# parse license challenge
cdm.parse_license(session_id, licence.content)

# print keys
for key in cdm.get_keys(session_id):
    print(f&quot;[{key.type}] {key.kid.hex}:{key.key.hex()}&quot;)

# close session, disposes of session data
cdm.close(session_id)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求成功后会打印&lt;code&gt;SIGNING&lt;/code&gt;和&lt;code&gt;CONTENT&lt;/code&gt;，这个&lt;code&gt;CONTENT&lt;/code&gt;就是我们需要用来解密音视频的秘钥对&lt;code&gt;kid:key&lt;/code&gt;，我的请求输出大致格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[SIGNING] &amp;lt;32bit-hex&amp;gt;:&amp;lt;128bit-hex&amp;gt;
[CONTENT] &amp;lt;32bit-hex-kid&amp;gt;:&amp;lt;32bit-hex-key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;解密多媒体文件&lt;/h2&gt;
&lt;p&gt;解密工具有现成的，比如&lt;a href=&quot;https://www.bento4.com/documentation/mp4decrypt/&quot;&gt;mp4decrypt&lt;/a&gt;、&lt;a href=&quot;https://github.com/shaka-project/shaka-packager&quot;&gt;shaka&lt;/a&gt;，我用的是mp4decrypt，使用刚才拿到的`kid:key。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mp4decrypt --key &amp;lt;kid&amp;gt;:&amp;lt;key&amp;gt; my_encrypted_file.mp4 my_decrypted_file.mp4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以直接使用支持DRM解密的下载工具，如使用rust编写的&lt;a href=&quot;https://github.com/emarsden/dash-mpd-cli&quot;&gt;dash-mpd-cli&lt;/a&gt;，只需要在参数中指定解密工具和秘钥即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dash-mpd-cli https://url_of_.mpd_file.mpd --output my_decrypted_file.mp4
  -H cURL_format_headers
  -H ...
  --decryption-application mp4decrypt \
  --mp4decrypt-location /home/wsl/devof/Bento4/cmakebuild/mp4decrypt \
  --key d2afe67c226cf2e932ac9b4c0c13432d:c5cf130ddf1087ca2f134725e82f1c1e 
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>理解Transformer模型2：训练Transformer</title><link>https://zlotus.github.io/2024/01/13/transformer-from-scratch-2/</link><guid isPermaLink="true">https://zlotus.github.io/2024/01/13/transformer-from-scratch-2/</guid><description>尝试通过手写transformer来理解该算法</description><pubDate>Sat, 13 Jan 2024 18:20:00 GMT</pubDate><content:encoded>&lt;p&gt;接着上一篇文章，任务目标依然是通过使用Transformer将英语翻译为意大利语，来理解Transformer是如何编写和训练的，同时本文还将通过可视化观察注意力模型的细节。文中将使用&lt;a href=&quot;https://huggingface.co/datasets/opus_books&quot;&gt;Hugging Face的opus_books&lt;/a&gt;作为训练集，通过Hugging Face的工具链完成数据集的下载，和将文本转换为词表的工作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-2/transformer-from-scratch-2-00-the-transformer.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;1 Tokenizer&lt;/h2&gt;
&lt;p&gt;观察数据集，可以发现Hugging Face提供的数据都是成对出现的原文（英语）-译文（意大利语）字典。第一步是下载数据集，并创建tokenizer（也译为分词）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-2/transformer-from-scratch-2-01-what-is-an-input-embedding.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;Tokenizer在Input Embedding的输入之前，用于将句子拆分成token从而构建词表（其中还将包括用于让模型识别的特殊token，如用于padding的、用于标识句子起止位置的等等），&lt;a href=&quot;https://huggingface.co/docs/transformers/tokenizer_summary&quot;&gt;tokenizer的种类有很多&lt;/a&gt;，如&lt;a href=&quot;https://www.youtube.com/watch?v=HEikzVL-lZU&quot;&gt;BPE tokenizer&lt;/a&gt;（按频度统计分出词根）、&lt;a href=&quot;https://www.youtube.com/watch?v=nhJxYji1aho&quot;&gt;Word tokenizer&lt;/a&gt;（按空格和标点分词）、&lt;a href=&quot;https://www.youtube.com/watch?v=zHvTiHr506c&quot;&gt;Subword tokenizer&lt;/a&gt;（高频词不分，低频词分出有意义的subword或词根）。本文以教学为目标，因此选择使用最简单的Word tokenizer。&lt;/p&gt;
&lt;p&gt;新建文件&lt;code&gt;train.py&lt;/code&gt;用于训练模型。&lt;/p&gt;
&lt;p&gt;安装Hugging Face的&lt;code&gt;datasets&lt;/code&gt;和&lt;code&gt;tokenizer&lt;/code&gt;库用于下载数据集和调用分词器（&lt;code&gt;pip&lt;/code&gt;和&lt;code&gt;conda&lt;/code&gt;都可以）。&lt;/p&gt;
&lt;p&gt;编写函数用于创建分词器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader, random_split

from datasets import load_dataset
from tokenizers import Tokenizer

from tokenizers.models import WordLevel
from tokenizers.trainers import WordLevelTrainer # tokenizer相对应的trainer，通过给定的句子创建词表
from tokenizers.pre_tokenizers import Whitespace # 按空格分词

from pathlib import Path


def get_or_build_tokenizer(config, ds, lang):
    &quot;&quot;&quot;
    创建分词器
    
    参数: 
        `config`: 模型的配置
        `ds`: 数据集
        `lang`: 分词器的语言
    返回: 
        `Tokenizer`分词器实例
    &quot;&quot;&quot;
    # config[&apos;tokenizer_file&apos;] = &apos;../tokenizer/tokenizer_{0}.json&apos;
    tokenizer_path = Path(config[&apos;tokenizer_file&apos;].format(lang))
    if not Path.exists(tokenizer_path):
        tokenizer = Tokenizer(WordLevel(unk_token=&apos;[UNK]&apos;)) # 分词器在遇到不在词表中的词时将其替换为`[UNK]`
        tokenizer.pre_tokenizer = Whitespace()
        trainer = WordLevelTrainer(special_tokens=[&quot;[UNK]&quot;, &quot;[PAD]&quot;, &quot;[SOS]&quot;, &quot;[EOS]&quot;], min_frequency=2) # 一个词要出现在词表中，需要至少在句子中出现两次
        tokenizer.train_from_iterator(get_all_sentences(ds, lang), trainer=trainer)
    else:
        tokenizer = Tokenizer.from_file(str(tokenizer_path))
    
    return tokenizer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以及从数据集中生成句子的工具函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_all_sentences(ds, lang):
    &quot;&quot;&quot;
    从数据集中取指定语言的句子

    参数: 
        `ds`: 数据集
        `lang`: 指定语言 `&quot;en&quot;`或`&quot;it&quot;`
    &quot;&quot;&quot;
    for item in ds:
        yield item[&apos;translation&apos;][lang]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Hugging Face中下载数据集：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def get_ds(config):
    &quot;&quot;&quot;
    加载数据集并创建分词器
    &quot;&quot;&quot;
    # 在Hugging Face中下载opus_books数据集中的`en-it`子集，选择`train`部分，我们之后将自行再分出validation部分
    ds_raw = load_dataset(&apos;opus_books&apos;, f&apos;{config[&quot;lang_src&quot;]}-{config[&quot;lang_tgt&quot;]}&apos;, split=&apos;train&apos;)

    # 创建分词器
    tokenizer_src = get_or_build_tokenizer(config, ds_raw, config[&apos;lang_src&apos;])
    tokenizer_tgt = get_or_build_tokenizer(config, ds_raw, config[&apos;lang_tgt&apos;])

    # 将数据集分为90%的训练集和10%的验证集
    train_ds_size = int(0.9 * len(ds_raw))
    val_ds_size = len(ds_raw) - train_ds_size
    train_ds_raw, val_ds_raw = random_split(ds_raw, [train_ds_size, val_ds_size])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时需要处理数据集训练模型，我们先编写&lt;code&gt;dataset.py&lt;/code&gt;为模型准备数据。&lt;/p&gt;
&lt;h2&gt;2 Dataset&lt;/h2&gt;
&lt;p&gt;新建文件&lt;code&gt;dataset.py&lt;/code&gt;，以生成用于输入模型的由张量组成的数据集。初始化&lt;code&gt;BilingualDataset&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class BilingualDataset(Dataset):

    def __init__(self, ds, tokenizer_src, tokenizer_tgt, src_lang, tgt_lang, seq_len) -&amp;gt; None:
        super().__init__()

        self.ds = ds
        self.tokenizer_src = tokenizer_src
        self.tokenizer_tgt = tokenizer_tgt
        self.src_lang = src_lang
        self.tgt_lang = tgt_lang
        self.seq_len = seq_len

        self.sos_token = torch.tensor([tokenizer_tgt.token_to_id(&quot;[SOS]&quot;)], dtype=torch.int64)
        self.eos_token = torch.tensor([tokenizer_tgt.token_to_id(&quot;[EOS]&quot;)], dtype=torch.int64)
        self.pad_token = torch.tensor([tokenizer_tgt.token_to_id(&quot;[PAD]&quot;)], dtype=torch.int64)

    def __len__(self): # 数据集大小
        return len(self.ds)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编写关键的函数&lt;code&gt;__getitem__&lt;/code&gt;，用于将原始数据转换为张量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    def __getitem__(self, index):
        src_target_pair = self.ds[index]
        src_text = src_target_pair[&apos;translation&apos;][self.src_lang]
        tgt_text = src_target_pair[&apos;translation&apos;][self.tgt_lang]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先将文本转换为token，再转换成id，即tokenizer先将句子拆分成词，再将词转换成词表中的id：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # 将文本转换成token，再转换成id，即tokenizer先将句子拆分成词，再将词转换成词表中的id
        enc_input_tokens = self.tokenizer_src.encode(src_text).ids
        dec_input_tokens = self.tokenizer_tgt.encode(tgt_text).ids
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后计算需要填充（padding）的token数量，以使句子长度总能达到&lt;code&gt;seq_len&lt;/code&gt;。此外，因为decoder的输入只有&lt;code&gt;[SOS]&lt;/code&gt;没有&lt;code&gt;[EOS]&lt;/code&gt;，而decoder的输出（也叫做label，即期望的翻译结果）只有&lt;code&gt;[EOS]&lt;/code&gt;没有&lt;code&gt;[SOS]&lt;/code&gt;，所以此处的padding会多一个token：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # 填充句子，使其长度达到seq_len
        enc_num_padding_tokens = self.seq_len - len(enc_input_tokens) - 2
        # 因为decoder的输入只有sos没有eos，所以padding要多一个token
        dec_num_padding_tokens = self.seq_len - len(dec_input_tokens) - 1 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要确保确保选择的&lt;code&gt;seq_len&lt;/code&gt;长度满足所有样本，即填充的token数量应该不为负数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # 确保选择的seq_len长度满足所有样本，即padding应该不为负数
        if enc_num_padding_tokens &amp;lt; 0 or dec_num_padding_tokens &amp;lt; 0:
            raise ValueError(&apos;Sentence is too long&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为encoder的输入组装tensor，依次为：&lt;code&gt;&apos;[SOS]&apos;&lt;/code&gt;、输入tensor、&lt;code&gt;&apos;[EOS]&apos;&lt;/code&gt;和填充&lt;code&gt;&apos;[PAD]&apos;&lt;/code&gt;*&lt;code&gt;enc_num_padding_tokens&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # 为encoder的输入填充tensor，添加&apos;[SOS]&apos;、&apos;[EOS]&apos;、&apos;[PAD]&apos;
        encoder_input = torch.cat([
            self.sos_token,
            torch.tensor(enc_input_tokens, dtype=torch.int64),
            self.eos_token,
            torch.tensor([self.pad_token] * enc_num_padding_tokens, dtype=torch.int64)
        ])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为decoder的输入组装tensor，依次为：&lt;code&gt;&apos;[SOS]&apos;&lt;/code&gt;、输入tensor和填充&lt;code&gt;&apos;[PAD]&apos;&lt;/code&gt;*&lt;code&gt;enc_num_padding_tokens&lt;/code&gt;（没有&lt;code&gt;&apos;[EOS]&apos;&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # 为decoder的输入填充tensor，只添加&apos;[SOS]&apos;和&apos;[PAD]&apos;
        decoder_input = torch.cat([
            self.sos_token,
            torch.tensor(dec_input_tokens, dtype=torch.int64),
            torch.tensor([self.pad_token] * dec_num_padding_tokens, dtype=torch.int64)
        ])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为decoder的输出（即label）组装tensor，依次为：输入tensor、&lt;code&gt;&apos;[EOS]&apos;&lt;/code&gt;和填充&lt;code&gt;&apos;[PAD]&apos;&lt;/code&gt;*&lt;code&gt;enc_num_padding_tokens&lt;/code&gt;（没有&lt;code&gt;&apos;[SOS]&apos;&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # 为decoder的输出（也叫做label，即期望的翻译结果）填充tensor，只添加&apos;[EOS]&apos;
        label = torch.cat([
            torch.tensor(dec_input_tokens, dtype=torch.int64),
            self.eos_token,
            torch.tensor([self.pad_token] * dec_num_padding_tokens, dtype=torch.int64)
        ])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处再次检查填充后的tensor长度是否满足&lt;code&gt;seq_len&lt;/code&gt;，并返回结果。其中，encoder的mask仅用于屏蔽掉填充的token，而decoder的mask用于屏蔽掉填充的token和未来的token：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        # 再次检测填充后的tensor长度是否满足seq_len
        assert encoder_input.size(0) == self.seq_len
        assert decoder_input.size(0) == self.seq_len
        assert label.size(0) == self.seq_len

        return {
            &apos;encoder_input&apos;: encoder_input, # (seq_len)
            &apos;decoder_input&apos;: decoder_input, # (seq_len)
            &apos;encoder_mask&apos;: (encoder_input != self.pad_token).unsqueeze(0).unsqueeze(0).int(), # (1, 1, seq_len)
            &apos;decoder_mask&apos;: (decoder_input != self.pad_token).unsqueeze(0).unsqueeze(0).int() &amp;amp; causal_mask(decoder_input.size(0)), # (1, seq_len, seq_len)) 
            &apos;label&apos;: label, # (seq_len)
            &apos;src_text&apos;: src_text, # 原文，用于可视化
            &apos;tgt_text&apos;: tgt_text, # 译文，用于可视化
        }

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在上面使用了一个叫做&lt;code&gt;causal_mask&lt;/code&gt;的函数，用于创建decoder的mask。该函数的作用是使得decoder只能看到之前的token，而不能看到未来的token。回顾一下Self-Attention的细节：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-2/transformer-from-scratch-2-02-self-attention-in-detail.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;这个矩阵表示$Q\times K^T$，我们希望每个词只能看到它之前的词，因此需要使用mask隐藏矩阵对角元素之上的部分。在这个例子中，我们不希望&lt;code&gt;YOUR&lt;/code&gt;看到&lt;code&gt;CAT&lt;/code&gt;、&lt;code&gt;IS&lt;/code&gt;、&lt;code&gt;A&lt;/code&gt;、&lt;code&gt;LOVELY&lt;/code&gt;、&lt;code&gt;CAT&lt;/code&gt;，我们希望&lt;code&gt;YOUR&lt;/code&gt;只能看到&lt;code&gt;YOUR&lt;/code&gt;自己；而&lt;code&gt;LOVELY&lt;/code&gt;则应该能看到它之前的词，即&lt;code&gt;YOUR&lt;/code&gt;、&lt;code&gt;CAT&lt;/code&gt;、&lt;code&gt;IS&lt;/code&gt;、&lt;code&gt;LOVELY&lt;/code&gt;，但看不到最后的词&lt;code&gt;CAT&lt;/code&gt;。使用Pytorch可以方便的创建一个下三角矩阵：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def causal_mask(size):
    # triu函数中的diagonal=1表示主对角线+1，即生成上三角矩阵后主对角线再置为0
    mask = torch.triu(torch.ones(1, size, size), diagonal=1).type(torch.int)
    # 再将矩阵取反，即生成下三角矩阵
    return mask == 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们完成了&lt;code&gt;dataset.py&lt;/code&gt;中准备数据集的工作，接下来可以继续完成&lt;code&gt;train.py&lt;/code&gt;中的训练模型的循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from dataset import BilingualDataset, causal_mask


def get_ds(config):
    &quot;&quot;&quot;
    加载数据集并创建分词器
    &quot;&quot;&quot;
    # 在Hugging Face中下载opus_books数据集中的`en-it`子集，选择`train`部分，我们之后将自行再分出validation部分
    ds_raw = load_dataset(&apos;opus_books&apos;, f&apos;{config[&quot;lang_src&quot;]}-{config[&quot;lang_tgt&quot;]}&apos;, split=&apos;train&apos;)

    # 创建分词器
    tokenizer_src = get_or_build_tokenizer(config, ds_raw, config[&apos;lang_src&apos;])
    tokenizer_tgt = get_or_build_tokenizer(config, ds_raw, config[&apos;lang_tgt&apos;])

    # 将数据集分为90%的训练集和10%的验证集
    train_ds_size = int(0.9 * len(ds_raw))
    val_ds_size = len(ds_raw) - train_ds_size
    train_ds_raw, val_ds_raw = random_split(ds_raw, [train_ds_size, val_ds_size])

    train_ds = BilingualDataset(train_ds_raw, tokenizer_src, tokenizer_tgt, config[&apos;lang_src&apos;], config[&apos;lang_tgt&apos;], config[&apos;seq_len&apos;])
    val_ds = BilingualDataset(val_ds_raw, tokenizer_src, tokenizer_tgt, config[&apos;lang_src&apos;], config[&apos;lang_tgt&apos;], config[&apos;seq_len&apos;])

    # 找出训练集中源语言和目标语言里最长的句子长度分别是多少
    max_len_src = 0
    max_len_tgt = 0

    for item in ds_raw:
        src_ids = tokenizer_src.encode(item[&apos;translation&apos;][config[&apos;lang_src&apos;]]).ids
        tgt_ids = tokenizer_tgt.encode(item[&apos;translation&apos;][config[&apos;lang_tgt&apos;]]).ids
        max_len_src = max(max_len_src, len(src_ids))
        max_len_tgt = max(max_len_tgt, len(tgt_ids))

    print(f&quot;Max length of source language: {max_len_src}&quot;)
    print(f&quot;Max length of target language: {max_len_tgt}&quot;)

    train_dataloader = DataLoader(train_ds, batch_size=config[&apos;batch_size&apos;], shuffle=True)
    val_dataloader = DataLoader(val_ds, batch_size=1, shuffle=True)

    return train_dataloader, val_dataloader, tokenizer_src, tokenizer_tgt

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3 Trainning Loop&lt;/h2&gt;
&lt;p&gt;到这里，我们已经准备好了模型和数据集，可以开始训练模型了。首先是创建模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from model import build_transformer


def get_model(config, vocab_src_len, vocab_tgt_len):
    &quot;&quot;&quot;
    创建模型
    &quot;&quot;&quot;
    model = build_transformer(vocab_src_len, vocab_tgt_len, config[&apos;seq_len&apos;], config[&apos;seq_len&apos;], config[&apos;d_model&apos;])
    return model
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在前面的文章中，我们只是使用了很多次&lt;code&gt;config&lt;/code&gt;但是从来没有定义这个模型配置文件，现在我们来定义这个配置文件，新建&lt;code&gt;config.py&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pathlib import Path

def get_config():
    return {
        &quot;batch_size&quot;: 8,
        &quot;num_epochs&quot;: 20,
        # 在实际的训练中，lr应该根据实际情况调整，比如一开始设置较大的lr，然后在训练过程中逐渐减小lr
        # 但本文的目的在于展示Transformer的原理，使用可变的lr会增加代码复杂度，因此这里使用固定的lr
        &quot;lr&quot;: 10**-4,
        &quot;seq_len&quot;: 350,
        &quot;d_model&quot;: 512,
        &quot;lang_src&quot;: &quot;en&quot;,
        &quot;lang_tgt&quot;: &quot;it&quot;,
        &quot;model_folder&quot;: &quot;weights&quot;,
        &quot;model_basename&quot;: &quot;tmodel_&quot;,
        # 预加载模型，如果不为空，则从预加载模型中加载参数（如在训练过程中程序意外中断），否则从头开始训练
        &quot;preload&quot;: None,
        # 分词文件，如`_en.json`和`_it.json`
        &quot;tokenizer_file&quot;: &quot;tokenizer_{0}.json&quot;,
        # 为tensorboard提供的log路径
        &quot;experiment_name&quot;: &quot;runs/tmodel&quot;
    }


def get_weights_file_path(config, epoch: str):
    model_folder = config[&quot;model_folder&quot;]
    model_basename = config[&quot;model_basename&quot;]
    model_filename = f&quot;{model_basename}{epoch}.pt&quot;
    # Path重载了`/`操作符，可以直接使用`/`创建子目录
    return str(Path(&apos;.&apos;) / model_folder / model_filename)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来开始编写训练模型的代码。本文使用tensorboard观察模型训练细节，要在本地使用tensorboard，可以安装&lt;code&gt;tensorboard&lt;/code&gt;和&lt;code&gt;torch_tb_profiler&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install tensorboard torch_tb_profiler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在命令行中运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tensorboard --logdir runs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即可打开tensorboard，在浏览器中访问&lt;code&gt;http://localhost:6006/&lt;/code&gt;即可查看tensorboard的界面。或是在vscode中安装&lt;code&gt;tensorboard&lt;/code&gt;插件，然后点击代码中的“启动TensorBoard会话”按钮，即可打开tensorboard。&lt;/p&gt;
&lt;p&gt;为预加载权重编写代码，若指定了预加载的模型，则直接加载。此外，指定loss函数，声明padding不参与loss计算。同时，使用label smoothing，让模型降低对计算结果的确定性，即减少本次推理结果的概率，并把减少的部分分配到其他可能的推理结果上。实测可以提升模型的泛化能力，降低过拟合。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from torch.utils.tensorboard import SummaryWriter
from config import get_weights_file_path, get_config
from tqdm import tqdm


def train_model(config):
    &quot;&quot;&quot;
    训练模型
    &quot;&quot;&quot;

    # 选择设备
    device = torch.device(&apos;cuda&apos; if torch.cuda.is_available() else &apos;cpu&apos;)
    print(f&quot;Using device: {device}&quot;)

    # 确定权重文件目录存在
    Path(config[&apos;model_folder&apos;]).mkdir(parents=True, exist_ok=True)

    # 加载数据集和分词器
    train_dataloader, val_dataloader, tokenizer_src, tokenizer_tgt = get_ds(config)
    model = get_model(config, tokenizer_src.get_vocab_size(), tokenizer_tgt.get_vocab_size()).to(device)

    # 启动Tensorboard以可视化loss
    writer = SummaryWriter(config[&apos;experiment_name&apos;])

    optimizer = torch.optim.Adam(model.parameters(), lr=config[&apos;lr&apos;], eps=1e-9)

    # 若指定了预加载权重，则加载预加载权重
    initial_epoch = 0
    global_step = 0
    if config[&apos;preload&apos;]:
        model_filename = get_weights_file_path(config, config[&apos;preload&apos;])
        print(f&apos;Preloading model {model_filename}&apos;)
        state = torch.load(model_filename)
        initial_epoch = state[&apos;epoch&apos;] + 1
        optimizer.load_state_dict(state[&apos;optimizer_state_dict&apos;])
        global_step = state[&apos;global_step&apos;]
    
    # 定义loss函数，声明padding不参与loss计算。同时，使用label smoothing，让模型降低对计算结果的确定性，
    # 即减少本次推理结果的概率，并把减少的部分分配到其他可能的推理结果上。实测可以提升模型的泛化能力，降低过拟合。
    # 这里的label smoothing的系数设置为0.1，即将最高概率标签的概率降低0.1，再并分配给其他标签的概率。
    loss_fn = nn.CrossEntropyLoss(ignore_index=tokenizer_src.token_to_id(&apos;[PAD]&apos;), label_smoothing=0.1).to(device)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写trainning loop，并在每个epoch结束时保存模型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    for epoch in range(initial_epoch, config[&apos;num_epochs&apos;]):
        model.train()
        # 画进度条
        batch_iterator = tqdm(train_dataloader, desc=f&quot;Processing epoch {epoch:02d}&quot;)
        for batch in batch_iterator:

            encoder_input = batch[&apos;encoder_input&apos;].to(device) # (batch, seq_len)
            decoder_input = batch[&apos;decoder_input&apos;].to(device) # (batch, seq_len)
            encoder_mask = batch[&apos;encoder_mask&apos;].to(device) # (batch, 1, 1, seq_len) 只隐藏padding tokens
            decoder_mask = batch[&apos;decoder_mask&apos;].to(device) # (batch, 1, seq_len, seq_len) 隐藏padding tokens和未来的tokens

            # 将tensor输入Transformer模型并计算
            encoder_output = model.encode(encoder_input, encoder_mask) # (batch, seq_len, d_model)
            decoder_output = model.decode(encoder_output, encoder_mask, decoder_input, decoder_mask) # (batch, seq_len, d_model)
            proj_output = model.project(decoder_output) # (batch, seq_len, tgt_vocab_size)

            label = batch[&apos;label&apos;].to(device) # (batch, seq_len)

            # (batch, seq_len, tgt_vocab_size) -[view]-&amp;gt; (batch * seq_len, tgt_vocab_size)
            # (batch, seq_len) -[view]-&amp;gt; (batch * seq_len)
            loss = loss_fn(proj_output.view(-1, tokenizer_tgt.get_vocab_size()), label.view(-1))
            batch_iterator.set_postfix({f&quot;loss&quot;: f&quot;{loss.item():6.3f}&quot;})

            # 为TensorBoard记录loss
            writer.add_scalar(&apos;train loss&apos;, loss.item(), global_step)
            writer.flush()

            # loss反向传播
            loss.backward()

            # 更新参数
            optimizer.step()
            optimizer.zero_grad()

            global_step += 1

        # 在每个epoch结束后保存模型
        # 保存模型是个好习惯，否则每次训练都需要从零开始
        model_filename = get_weights_file_path(config, f&quot;{epoch:02d}&quot;)
        torch.save({
            &apos;epoch&apos;: epoch,
            &apos;model_state_dict&apos;: model.state_dict(),
            &apos;optimizer_state_dict&apos;: optimizer.state_dict(),
            &apos;global_step&apos;: global_step,
        }, model_filename)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编写&lt;code&gt;__main__&lt;/code&gt;函数执行训练：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import warnings

if __name__ == &apos;__main__&apos;:
    warnings.filterwarnings(&apos;ignore&apos;)
    config = get_config()
    train_model(config)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时就可以正常的开始模型训练了，如果编写正确的话，程序就会自动下载数据集并开始训练了。&lt;/p&gt;
&lt;p&gt;在训练之前，我们还可以可视化训练的效果，比如查看模型预测的结果是否与真实结果是否一致，也就是validation，即观察模型在训练时是如何演化的。&lt;/p&gt;
&lt;h2&gt;4 Validation Loop&lt;/h2&gt;
&lt;p&gt;接下来编写validation loop，以便我们可以实时评估模型推理结果，观察模型是如何翻译数据集中并不在训练集当中的句子的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def run_validation(model, validation_ds, tokenizer_src, tokenizer_tgt, max_len, device, print_msg, global_state, writer, num_examples=2):

    # 告诉pytorch将模型置于eval模式，关闭dropout、batchnorm中的随机因素
    model.eval()
    # 测试2个句子看看模型输出的翻译结果是什么
    count = 0

    srouce_texts = []
    expected = []
    predicted = []

    # 终端宽度
    console_width = 80

    # 关闭pytorch的梯度计算，在这个`with`中我们只需要推理结果，不需要训练，因此不需要计算tensor的梯度
    with torch.no_grad():
        for batch in validation_ds:
            count += 1
            encoder_input = batch[&apos;encoder_input&apos;].to(device) # (batch, seq_len)
            encoder_mask = batch[&apos;encoder_mask&apos;].to(device) # (batch, 1, 1, seq_len) 只隐藏padding tokens

            # 确定validation的batch应为1
            assert encoder_input.size(0) == 1, &quot;Batch size must be 1 for validation&quot;

            # 当模型进行推理时，只需要计算一次`encoder_output`，然后重复使用它来为每个token计算`decoder_output`

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;回忆Transformer模型，当我们想用模型进行推理时，只需要计算一次&lt;code&gt;encoder_output&lt;/code&gt;，然后重复使用它来为每个token计算&lt;code&gt;decoder_output&lt;/code&gt;，现在编写&lt;code&gt;greedy_decode&lt;/code&gt;函数，用于生成翻译结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def greedy_decode(model, source, source_mask, tokenizer_src, tokenizer_tgt, max_len, device):
    &quot;&quot;&quot;
    使用贪婪策略，缓存encoder输出，计算decoder输出

    参数: 
        `model`: 模型
        `source`: 源语言句子
        `source_mask`: 源语言句子的padding mask
        `tokenizer_src`: 源语言的分词器
        `tokenizer_tgt`: 目标语言的分词器
        `max_len`: 句子最大长度
        `device`: 运行模型的设备
    &quot;&quot;&quot;
    # 告诉pytorch将模型置于eval模式，关闭dropout、batchnorm中的随机因素
    sos_idx = tokenizer_tgt.token_to_id(&apos;[SOS]&apos;)
    eos_idx = tokenizer_tgt.token_to_id(&apos;[EOS]&apos;)
    
    # 计算encoder输出，并将结果重用于计算每个token的decoder输出
    encoder_output = model.encode(source, source_mask)
    
    # 模型推理计算过程如下：首先输入句子的起始符[SOS]作为decoder的第一个输入，然后得到模型输出的翻译句子的第一个词；
    # 接下在每一步迭代中将上一步的输出附加在输入中，使模型继续输出再下一个词，直到遇到[EOS]或达到句子的最大长度。

    # 使用[SOS]初始化decoder输入
    decoder_input = torch.empty(1, 1).fill_(sos_idx).type_as(source).to(device)
    # 接下来让decoder循环输出下一个词，知道遇到[EOS]或达到句子最大长度
    while True:

        # 判断是否达到句子最大长度
        if decoder_input.size(1) == max_len:
            break

        # 为target（即decoder input）创建mask，使模型只能看到当前时刻之前的词
        # 对比`dataset.py`中使用的`causal_mask`，这里使用的是`decoder_input.size(1)`作为`tgt_mask`的长度，并不需要使用额外的padding
        decoder_mask = causal_mask(decoder_input.size(1)).type_as(source_mask).to(device)

        # 计算decoder输出，这里复用了`encoder_output`
        out = model.decode(encoder_output, source_mask, decoder_input, decoder_mask) # (1, seq_len, d_model)

        # 计算下一个词：使用projection层计算下一个token的概率分布，只取最后一个词的projection，out[:, -1]为(1, 1, d_model)，prob为(1, 1, vocab_size)
        prob = model.project(out[:, -1])
        # 取概率最大的词作为下一个token（这里使用贪婪策略，即只取概率最大的词，而不是所有词的概率之和）
        _, next_word = torch.max(prob, dim=1)
        # 再将next_word添加到decoder_input的末尾
        decoder_input = torch.cat([decoder_input, torch.empty(1, 1).type_as(source).fill_(next_word.item()).to(device)], dim=1)

        # 如果遇到[EOS]，则停止循环
        if next_word == eos_idx:
            break
        
    return decoder_input.squeeze(0) # 输出翻译句子
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续编写&lt;code&gt;run_validation&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            # 确定validation的batch应为1
            assert encoder_input.size(0) == 1, &quot;Batch size must be 1 for validation&quot;

            # 当我们想要模型进行推理时，我们只需要计算一次`encoder_output`，然后重复使用它来为每个token计算`decoder_output`
            model_out = greedy_decode(model, encoder_input, encoder_mask, tokenizer_src, tokenizer_tgt, max_len, device)

            source_text = batch[&apos;src_text&apos;][0]
            target_text = batch[&apos;tgt_text&apos;][0]
            # 用目标语言的分词器将模型输出的token序列转换为文本
            model_out_text = tokenizer_tgt.decode(model_out.detach().cpu().numpy())

            source_texts.append(source_text)
            expected.append(target_text)
            predicted.append(model_out_text)

            # 打印输出，为了不影响`tqdm`进度条正常刷新，我们使用`print_msg`而不是`print`来打印输出。
            print_msg(&apos;-&apos;*console_width)
            print_msg(f&quot;Source text: {source_text}&quot;)
            print_msg(f&quot;TARGET text: {target_text}&quot;)
            print_msg(f&quot;PREDICTED text: {model_out_text}&quot;)
            print_msg(&apos;-&apos;*console_width)

            # 如果预测了足够的句子，则停止循环
            if count &amp;gt;= num_examples:
                break
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们可以将&lt;code&gt;run_validation&lt;/code&gt;函数添加到&lt;code&gt;train_model&lt;/code&gt;函数中，放在每个epoch结束后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            global_step += 1

        run_validation(model, val_dataloader, tokenizer_src, tokenizer_tgt, config[&apos;seq_len&apos;], device, lambda msg: batch_iterator.write(msg), global_step, writer)

        # 在每个epoch结束后保存模型
        # 保存模型是个好习惯，否则每次训练都需要从零开始
        model_filename = get_weights_file_path(config, f&quot;{epoch:02d}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5 Attention Visualization&lt;/h2&gt;
&lt;p&gt;可视化的代码中的大部分功能都不需要我们自己实现，前人已经帮我们完成了很多工作。这里使用jupyter notebook，新建&lt;code&gt;attention_visual.ipynb&lt;/code&gt;文件。&lt;/p&gt;
&lt;p&gt;这里除了之前我们自己写的模型和训练库以外，还使用了&lt;code&gt;altair&lt;/code&gt;可视化库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn

from model import Transformer
from config import get_config, get_weights_file_path
from train import get_model, get_ds, greedy_decode

import altair as alt
import pandas as pd
import numpy as np
import warnings


warnings.filterwarnings(&apos;ignore&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同之前一样，选择计算设备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 从`train_model`中拷贝选择设备的代码即可
device = torch.device(&quot;cuda&quot; if torch.cuda.is_available() else &quot;cpu&quot;)
print(f&apos;Using device: {device}&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加载预训练模型的权重：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;config = get_config()
train_dataloader, val_dataloader, vocab_src, vocab_tgt = get_ds(config)
model = get_model(config, vocab_src.get_vocab_size(), vocab_tgt.get_vocab_size()).to(device)

# 加载预训练权重
model_filename = get_weights_file_path(config, f&quot;03&quot;)
state = torch.load(model_filename)
model.load_state_dict(state[&apos;model_state_dict&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从验证集中选择一对样本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def load_next_batch():
    # Load a sample batch from the validation set
    batch = next(iter(val_dataloader))
    encoder_input = batch[&quot;encoder_input&quot;].to(device)
    encoder_mask = batch[&quot;encoder_mask&quot;].to(device)
    decoder_input = batch[&quot;decoder_input&quot;].to(device)
    decoder_mask = batch[&quot;decoder_mask&quot;].to(device)

    encoder_input_tokens = [vocab_src.id_to_token(idx) for idx in encoder_input[0].cpu().numpy()]
    decoder_input_tokens = [vocab_tgt.id_to_token(idx) for idx in decoder_input[0].cpu().numpy()]

    # check that the batch size is 1
    assert encoder_input.size(
        0) == 1, &quot;Batch size must be 1 for validation&quot;

    model_out = greedy_decode(
        model, encoder_input, encoder_mask, vocab_src, vocab_tgt, config[&apos;seq_len&apos;], device)
    
    return batch, encoder_input_tokens, decoder_input_tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可视化注意力矩阵的函数，这些函数基本都能从网上找到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def mtx2df(m, max_row, max_col, row_tokens, col_tokens):
    &quot;&quot;&quot;
    生成一个可视化矩阵
    &quot;&quot;&quot;
    return pd.DataFrame(
        [
            (
                r,
                c,
                float(m[r, c]),
                &quot;%.3d %s&quot; % (r, row_tokens[r] if len(row_tokens) &amp;gt; r else &quot;&amp;lt;blank&amp;gt;&quot;),
                &quot;%.3d %s&quot; % (c, col_tokens[c] if len(col_tokens) &amp;gt; c else &quot;&amp;lt;blank&amp;gt;&quot;),
            )
            for r in range(m.shape[0])
            for c in range(m.shape[1])
            if r &amp;lt; max_row and c &amp;lt; max_col
        ],
        columns=[&quot;row&quot;, &quot;column&quot;, &quot;value&quot;, &quot;row_token&quot;, &quot;col_token&quot;],
    )

def get_attn_map(attn_type: str, layer: int, head: int):
    &quot;&quot;&quot;
    从指定的层和头获取attention map
    &quot;&quot;&quot;

    # 我们在`model.py`的`MultiHeadAttentionBlock`中，定义`attention`函数的返回值时，同时返回了`attention_scores`，
    # 之后又在`forward`中将其保存在`self.attention_scores`中。
    # 因此，可以通过`model.encoder.layers[layer].self_attention_block.attention_scores`
    # 来获取encoder的self-attention的attention_scores。
    if attn_type == &quot;encoder&quot;:
        attn = model.encoder.layers[layer].self_attention_block.attention_scores
    elif attn_type == &quot;decoder&quot;:
        attn = model.decoder.layers[layer].self_attention_block.attention_scores
    elif attn_type == &quot;encoder-decoder&quot;:
        attn = model.decoder.layers[layer].cross_attention_block.attention_scores
    return attn[0, head].data

def attn_map(attn_type, layer, head, row_tokens, col_tokens, max_sentence_len):
    &quot;&quot;&quot;
    使用altair绘制attention map
    &quot;&quot;&quot;
    df = mtx2df(
        get_attn_map(attn_type, layer, head),
        max_sentence_len,
        max_sentence_len,
        row_tokens,
        col_tokens,
    )
    return (
        alt.Chart(data=df)
        .mark_rect()
        .encode(
            x=alt.X(&quot;col_token&quot;, axis=alt.Axis(title=&quot;&quot;)),
            y=alt.Y(&quot;row_token&quot;, axis=alt.Axis(title=&quot;&quot;)),
            color=&quot;value&quot;,
            tooltip=[&quot;row&quot;, &quot;column&quot;, &quot;value&quot;, &quot;row_token&quot;, &quot;col_token&quot;],
        )
        #.title(f&quot;Layer {layer} Head {head}&quot;)
        .properties(height=400, width=400, title=f&quot;Layer {layer} Head {head}&quot;)
        .interactive()
    )

def get_all_attention_maps(attn_type: str, layers: list[int], heads: list[int], row_tokens: list, col_tokens, max_sentence_len: int):
    charts = []
    for layer in layers:
        rowCharts = []
        for head in heads:
            rowCharts.append(attn_map(attn_type, layer, head, row_tokens, col_tokens, max_sentence_len))
        charts.append(alt.hconcat(*rowCharts))
    return alt.vconcat(*charts)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;获取一对样本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;batch, encoder_input_tokens, decoder_input_tokens = load_next_batch()
print(f&apos;Source: {batch[&quot;src_text&quot;][0]}&apos;)
print(f&apos;Target: {batch[&quot;tgt_text&quot;][0]}&apos;)
sentence_len = encoder_input_tokens.index(&quot;[PAD]&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制Encoder的self-attention：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;layers = [0, 1, 2]
heads = [0, 1, 2, 3, 4, 5, 6, 7]

# 可视化Encoder的Self-Attention
get_all_attention_maps(&quot;encoder&quot;, layers, heads, encoder_input_tokens, encoder_input_tokens, min(20, sentence_len))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制Decoder的self-attention：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 可视化Decoder的Self-Attention
get_all_attention_maps(&quot;decoder&quot;, layers, heads, decoder_input_tokens, decoder_input_tokens, min(20, sentence_len))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;绘制Encoder-Decoder的cross-attention：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 可视化Decoder的Cross-Attention
# 这是实际上计算en-it翻译任务的地方，cross attention使用encoder的key/value，以及decoder的query计算attention。
get_all_attention_maps(&quot;encoder-decoder&quot;, layers, heads, encoder_input_tokens, decoder_input_tokens, min(20, sentence_len))
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>理解Transformer模型1：编写Transformer</title><link>https://zlotus.github.io/2024/01/06/transformer-from-scratch-1/</link><guid isPermaLink="true">https://zlotus.github.io/2024/01/06/transformer-from-scratch-1/</guid><description>尝试通过手写transformer来理解该算法</description><pubDate>Sat, 06 Jan 2024 15:24:00 GMT</pubDate><content:encoded>&lt;p&gt;回头看来，&lt;a href=&quot;https://arxiv.org/pdf/1706.03762.pdf&quot;&gt;Attention Is All You Need&lt;/a&gt;这篇文章是所有大模型的根基，从MLP、CNN、RNN发展而来的船新版本特征提取器。这次跟着&lt;a href=&quot;https://www.youtube.com/@umarjamilai&quot;&gt;油管UP Umar Jamil&lt;/a&gt;对照论文通过手写transformer来学习这个划时代的算法。&lt;a href=&quot;https://www.youtube.com/watch?v=ISNdQcPhsts&quot;&gt;Coding a Transformer from scratch on PyTorch, with full explanation, training and inference&lt;/a&gt;。本文以翻译任务为例，展示如何使用pytorch手写一个将英语翻译为意大利语的Transformer。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-1/transformer-from-scratch-1-00-the-transformer.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;整个Transformer模型可以大致分为两个部分，Encoder和Decoder。新建文件&lt;code&gt;model.py&lt;/code&gt;用于编写Transformer模型。&lt;/p&gt;
&lt;h2&gt;1 Encoder&lt;/h2&gt;
&lt;p&gt;Encoder由$N$个encoder模块首尾相接组合而成，每个encoder模块又包含了两个由Residual连接（也译为残差连接）的网络。&lt;/p&gt;
&lt;h3&gt;1.1 Input Embedding&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-1/transformer-from-scratch-1-01-what-is-an-input-embedding.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;第一步是embedding（也译为词嵌入），就是给将单词表里的每个词一个编号，然后将这个编号映射为一个词向量，在本文中词向量大小为512维度。需要注意的是在论文3.4节中提到embedding需要乘以$\sqrt{d_{model}}$。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import torch
import torch.nn as nn
import math


class InputEmbeddings(nn.Module):
    
    # `d_model`: 模型维度，本文中为512
    # `vocab_size`: 词表大小
    def __init__(self, d_model: int,  vocab_size: int) -&amp;gt; None:
        super().__init__()
        self.d_model = d_model
        self.vocab_size = vocab_size
        self.embedding = nn.Embedding(vocab_size, d_model) # 将一个数字映射为一个向量，这里的Embedding层包含vocab_size个大小为d_model的向量
    
    def forward(self, x):
        # (batch, seq_len) --&amp;gt; (batch, seq_len, d_model)
        # 论文3.4节Embeddings and Softmax中提到embedding要乘以d_model的平方根    
        return self.embedding(x) * math.sqrt(self.d_model)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.2 Positional Encoding&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-1/transformer-from-scratch-1-02-what-is-positional-encoding.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;第二步需要将每个单词在句子中的位置信息告诉模型，位置编码与embedding的维度相同（本文中为512），然后将这个位置编码与前面的embedding相加。位置编码矩阵大小为&lt;code&gt;(seq_len, d_model)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-1/transformer-from-scratch-1-03-what-is-positional-encoding.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;在位置向量的偶数元素使用公式$PE(pos, 2i)=\sin\frac{pos}{10000^\frac{2i}{d_{model}}}$，在奇数位置向量的奇数元素使用公式$PE(pos, 2i+1)=\cos\frac{pos}{10000^\frac{2i}{d_{model}}}$。为了计算的数值稳定性，我们对分母取对数和指数$\mathrm{e}^{\log_{\mathrm{e}}{10000\frac{2i}{d_{model}}}}$化简得到$\mathrm{e}^{2i\frac{\log_{\mathrm{e}}{10000}}{d_{model}}}$（这一项是分母所以代码中乘以这一项的负指数）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class PositionalEncoding(nn.Module):

    # `d_model`: 模型维度，本文中为512
    # `seq_len`: 句子的最大长度，为每个位置分配一个向量
    # `dropout`: 添加dropout层抑制过拟合
    # 位置信息只计算一次，在之后的训练中用在每个句子上
    def __init__(self, d_model: int, seq_len: int, dropout: float) -&amp;gt; None:
        super().__init__()
        self.d_model = d_model
        self.seq_len = seq_len
        self.dropout = nn.Dropout(dropout)

        # 创建维度为(seq_len, d_model)的矩阵
        pe = torch.zeros(seq_len, d_model)
        # 创建维度为(seq_len)的向量，元素取值[0, seq_len-1]
        position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1) # (seq_len, 1)
        # 创建维度为(d_model)的向量
        div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) # (d_model / 2)
        # 偶数项应用sin
        pe[:, 0::2] = torch.sin(position * div_term) # sin(position * (10000 ** (2i / d_model))
        # 奇数项应用cos
        pe[:, 1::2] = torch.cos(position * div_term) # cos(position * (10000 ** (2i / d_model))

        # 为位置编码添加batch维度，以方便一批一批的使用句子
        pe = pe.unsqueeze(0) # (1, seq_len, d_model)

        # 当你有一个不是习得的模型参数，而是表示模型状态的tensor，如果想要存放在模型中，则需要注册一个buffer，这个buffer就会跟模型文件一起保存在模型中
        self.register_buffer(&apos;pe&apos;, pe)

    def forward(self, x):
        # requires_grad_(False)告诉模型训练过程中不需要学习这一部分位置编码，因为他们是固定的
        x = x + (self.pe[:, :x.shape[1], :]).requires_grad_(False) # (batch, seq_len, d_model)
        return self.dropout(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 Normaliation&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-1/transformer-from-scratch-1-04-what-is-layer-normalization.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;Normaliation（也译为归一化），是取一个batch中的每个item，计算他们在本batch中相对于其他item的期望$\mu$和方差$\sigma^{2}$，再使用$\mu$和$\sigma^{2}$计算这一项的新值$\hat{x_j}=\frac{x_j-\mu_j}{\sqrt{\sigma_{j}^{2}+\epsilon}}$。此外，引入两个会被学习的参数，乘数gamma（或叫做alpha）和加数beta（或叫做bias），以增加数值的差异，因为如果将所有值都限定在[0, 1]之间，数值的起伏可能会过小，因此有必要让模型自己学习调整这两个能够引入更大数值起伏的参数。另外，之所以需要$\epsilon$这个参数，主要是为了数值稳定性，比如$\sigma$过小时将会导致整个$\hat{x_j}$过大。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class LayerNormalization(nn.Module):

    # `eps`: 批归一化的参数epsilon
    def __init__(self, eps: float = 10**-6) -&amp;gt; None:
        super().__init__()
        self.eps = eps
        self.alpha = nn.Parameter(torch.ones(1)) # 乘数
        self.bias = nn.Parameter(torch.zeros(1)) # 加数
    
    # `x`: 大小为(batch, seq_len, hidden_size)
    def forward(self, x):
        # `keepdim=True`让计算保持维度属性
        # `dim=-1`沿最后一个维度计算均值和方差
        mean = x.mean(dim=-1, keepdim=True) # (batch, seq_len, 1)
        std = x.std(dim=-1, keepdim=True) # (batch, seq_len, 1)

        # eps保持数值稳定性，防止除数过小
        return self.alpha * (x - mean) / (std + self.eps) + self.bias
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4 Feed Forward&lt;/h3&gt;
&lt;p&gt;一个简单的全连接层，论文提了用到的参数和激活函数relu。文中提到可以将这一步计算看做两个核大小为1的卷积层，输入输出维度为&lt;code&gt;d_model=512&lt;/code&gt;，内部隐藏层维度为&lt;code&gt;d_ff=2048&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class FeedForwardBlock(nn.Module):

    # 一个简单的全连接层，论文3.3节提到d_model、d_ff的大小
    def __init__(self, d_model: int, d_ff: int, dropout: float) -&amp;gt; None:
        super().__init__()

        # Linear层默认就带有bias，因此我们无需新增参数
        self.linear_1 = nn.Linear(d_model, d_ff) # w1 and b1
        self.dropout = nn.Dropout(dropout)
        self.linear_2 = nn.Linear(d_ff, d_model) # w2 and b2

    def forward(self, x):
        # (batch, seq_len, d_model) -[linear_1]-relu-dropout-&amp;gt; (batch, seq_len, d_ff) -[linear_2]-&amp;gt; (batch, seq_len, d_model)
        return self.linear_2(self.dropout(torch.relu(self.linear_1(x))))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.5 Multi-Head Attention&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-1/transformer-from-scratch-1-05-multihead-attention.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;Encoder的重头戏，就是这个多头注意力层，需要注意的是，encoder和decoder的多头注意力层有些细微的区别。&lt;/p&gt;
&lt;p&gt;先抛开batch维度，只关注算法，看看如何在一个句子上应用多头注意力模型。但需要记住，应用算法的时候，第一个维度是batch，程序会成批的输入句子。这里：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$seq$为句子最大长度&lt;/li&gt;
&lt;li&gt;$d_{model}$为embedding向量的大小（本文中为512）；&lt;/li&gt;
&lt;li&gt;$\mathrm{h}$为注意力头数&lt;/li&gt;
&lt;li&gt;$d_k=d_v$为$d_{model}/h$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从流程上来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先将输入复制三份，即Q、K、V矩阵（分别代表query、key、value，维度均为$(seq, d_{model})$）；&lt;/li&gt;
&lt;li&gt;分别乘以$W^Q$、$W^K$、$W^V$矩阵（维度均为$(d_{model}, d_{model})$），得到$Q&apos;$、$K&apos;$、$W&apos;$（维度均为$(seq, d_{model})$）；&lt;/li&gt;
&lt;li&gt;沿embedding方向将$Q&apos;$、$K&apos;$、$W&apos;$拆分为$h$个小矩阵$Q_i$、$K_i$、$W_i$（维度均为$(seq, d_k)$），即每个小矩阵都能拿到完整的句子，但都只拿到了embedding向量的一小部分；&lt;/li&gt;
&lt;li&gt;应用注意力模型公式$Attention(Q, K, V)=\mathrm{softmax}(\frac{QK^T}{\sqrt{d_k}})V$，计算出每个头$head_i=Attention(QW_i^Q, KW_i^K, VW_i^V)$，其中的每个头$head_i$维度均为$(d_v, seq)$；&lt;/li&gt;
&lt;li&gt;将$head_i$合并成矩阵$H$，维度为$(seq, h&lt;em&gt;d_v)$，即变回$(seq, d_{model})$，再乘以$W^O$矩阵（维度为$(h&lt;/em&gt;d_v, d_{model})$），最终得到$MH\text{-}A$矩阵（维度为$(seq, d_{model})$），这一步的整体运算为$MultiHead(Q, K, V)=\mathrm{Concat}(head_1 \dots head_h)W^O$；&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;class MultiHeadAttentionBlock(nn.Module):

    # `d_model`: 模型维度，本文中为512
    # `h`: 注意力头数
    # `dropout`: 添加dropout层抑制过拟合
    def __init__(self, d_model: int, h: int, dropout: float) -&amp;gt; None:
        super().__init__()
        self.d_model = d_model
        self.h = h
        # 多头需要均分d_model，应确保d_model可以被h整除
        assert d_model % h == 0, &quot;d_model不可被h整除&quot;

        self.d_k = d_model // h
        self.w_q = nn.Linear(d_model, d_model) # W^Q
        self.w_k = nn.Linear(d_model, d_model) # W^K
        self.w_v = nn.Linear(d_model, d_model) # W^V

        self.w_o = nn.Linear(d_model, d_model) # W^O
        self.dropout = nn.Dropout(dropout)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关于&lt;code&gt;mask&lt;/code&gt;参数&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-1/transformer-from-scratch-1-06-mask.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;在计算$Attention(Q, K, V)=\mathrm{softmax}(\frac{QK^T}{\sqrt{d_k}})V$时，我们会先使用$Q$和$K$计算$\mathrm{softmax}(\frac{QK^T}{\sqrt{d_k}})$（即在乘以$V$之前）。$\frac{QK^T}{\sqrt{d_k}}$得到的是一个$(seq, seq)$的矩阵，其内容为各词与其他词的关系表。如果我们不希望某词与其他某词产生联系，就需要将矩阵中两词对应的位置写成负无穷，即使其注意力分数变得极小，此时在进行$\mathrm{softmax}$运算时，矩阵该位置的值就几乎为零，也就意味着屏蔽了这两个词的注意力——这就是&lt;code&gt;mask&lt;/code&gt;参数的作用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    @staticmethod
    def attention(query, key, value, mask, dropout: nn.Dropout):
        d_k = query.shape[-1]

        # 计算论文中$\mathrm{softmax}\(\frac{QK^T}{\sqrt{d_k}}\)V$的部分
        # (batch, h, seq_len, d_k) -[transpose]-&amp;gt; (batch, h, d_k, seq_len)] -[matmul]-&amp;gt; (batch, h, seq_len, seq_len)
        attention_scores = (query @ key.transpose(-2, -1)) / math.sqrt(d_k)
        if mask is not None:
            # 在mask==0的位置写一个非常小的数来代表负无穷
            attention_scores.masked_fill_(mask == 0, -1e9)
        attention_scores = attention_scores.softmax(dim = -1) # (batch, h, seq_len, seq_len)再计算softmax
        if dropout is not None:
            attention_scores = dropout(attention_scores)
        # 乘以`value`后变为(batch, h, seq_len, seq_len) --&amp;gt; (batch, h, seq_len, d_k)
        # 这里额外返回`attention_scores`是为了可视化注意力分数
        return (attention_scores @ value), attention_scores

    def forward(self, q, k, v, mask):
        query = self.w_q(q) # (batch, seq_len, d_model) --&amp;gt; (batch, seq_len, d_model)
        key = self.w_k(k) # (batch, seq_len, d_model) --&amp;gt; (batch, seq_len, d_model)
        value = self.w_v(v) # (batch, seq_len, d_model) --&amp;gt; (batch, seq_len, d_model)

        # (batch, seq_len, d_model) -[view]-&amp;gt; (batch, seq_len, h, d_k) -[transpose]-&amp;gt; (batch, h, seq_len, d_k)
        query = query.view(query.shape[0], query.shape[1], self.h, self.d_k).transpose(1, 2)
        key = key.view(key.shape[0], key.shape[1], self.h, self.d_k).transpose(1, 2)
        value = value.view(value.shape[0], value.shape[1], self.h, self.d_k).transpose(1, 2)

        x, self.attention_scores = MultiHeadAttentionBlock.attention(query, key, value, mask, self.dropout)

        # (batch, h, seq_len, d_k) -[transpose]-&amp;gt; (batch, seq_len, h, d_k) -[contiguous+view]-&amp;gt; (batch, seq_len, d_model)
        # `contiguous`用于重新调整内存布局，因为诸如`transpose`、`view`等函数只会改变tensor的元数据以调整其表现形式，
        #     这些操作可能导致tensor显示的布局与其内存中的布局不一致，
        #     详情参见https://stackoverflow.com/questions/48915810/what-does-contiguous-do-in-pytorch
        x = x.transpose(1, 2).contiguous().view(x.shape[0], -1, self.h * self.d_k)

        # 乘以W^O，(batch, seq_len, d_model) --&amp;gt; (batch, seq_len, d_model)
        return self.w_o(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.6 Residual Connection&lt;/h3&gt;
&lt;p&gt;在Transformer模型中可以看到不少Residual连接（也译为残差连接），比如左侧一开始的&lt;em&gt;Input Embedding&lt;/em&gt;+&lt;em&gt;Positional Encoding&lt;/em&gt;既输出到&lt;em&gt;Multi-Head Attention&lt;/em&gt;又输出到&lt;em&gt;Add &amp;amp; Norm&lt;/em&gt;，或是接下来&lt;em&gt;Add &amp;amp; Norm&lt;/em&gt;输出既到&lt;em&gt;Feed Forward&lt;/em&gt;又到下一个&lt;em&gt;Add &amp;amp; Norm&lt;/em&gt;。可以看出，模型中的残差连接都是用来连接&lt;em&gt;Add &amp;amp; Norm&lt;/em&gt;及其前一层网络的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ResidualConnection(nn.Module):

    def __init__(self, dropout: float) -&amp;gt; None:
        super().__init__()
        self.dropout = nn.Dropout(dropout)
        self.norm = LayerNormalization()

    def forward(self, x, sublayer):
        # 有些实现是先计算`norm`再计算`sublayer`，比如论文中的实现。
        # 这两种计算计算的先后顺序有些细微差别，本文按照先`norm`再`sublayer`的模式执行。
        return x + self.dropout(sublayer(self.norm(x)))
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.7 Encoder Block&lt;/h3&gt;
&lt;p&gt;现在可以实现左侧整个encoder模块了，可以看出这个模块在模型中又N个，前一个encoder的输出送入后一个encoder做输入，最后一个encoder的输出送入decoder。Encoder中包含了一层&lt;em&gt;Multi-Head Attention&lt;/em&gt;、一层&lt;em&gt;Feed Forward&lt;/em&gt;、两层&lt;em&gt;Add &amp;amp; Norm&lt;/em&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class EncoderBlock(nn.Module):

    def __init__(self, self_attention_block: MultiHeadAttentionBlock, feed_forward_block: FeedForwardBlock, dropout: float) -&amp;gt; None:
        super().__init__()
        self.self_attention_block = self_attention_block
        self.feed_forward_block = feed_forward_block
        # 返回一个列表，包括两层`ResidualConnection`
        self.residual_connection = nn.ModuleList([ResidualConnection(dropout) for _ in range(2)])

    # `src_mask`应用在encoder的输入上，用于屏蔽padding词对正常词的影响，
    def forward(self, x, src_mask):
        # 从这里可以看到，`self_attention_block`之所以叫做&quot;self attention&quot;，
        # 是因为其中的query、key、value都是同一个输入句子`x`自身，
        # 即这一层输入的句子中的每个词，只会跟本句的词产生关系。
        # 在后文中可以看到decoder会有些许区别，将用到&quot;cross attention&quot;，
        # 即decoder中的query将会与encoder中的key和value产生关系。
        x = self.residual_connection[0](x, lambda x: self.self_attention_block(x, x, x, src_mask))
        x = self.residual_connection[1](x, self.feed_forward_block)
        return x
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.8 Encoder Module&lt;/h3&gt;
&lt;p&gt;然后将多个encoder模块首尾相接的组装在一起，最后添加一层&lt;em&gt;Normalization&lt;/em&gt;形成模型的encoder：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Encoder(nn.Module):

    def __init__(self, layers: nn.ModuleList) -&amp;gt; None:
        super().__init__()
        self.layers = layers
        self.norm = LayerNormalization()

    def forward(self, x, mask):
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2 Decoder&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;./transformer-from-scratch-1/transformer-from-scratch-1-00-the-transformer.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;br&amp;gt;&lt;/p&gt;
&lt;p&gt;再来观察一下Decoder。对于输入，Decoder的&lt;em&gt;Output Embedding&lt;/em&gt;与Encoder的&lt;em&gt;Input Embedding&lt;/em&gt;一样，都是用到同一张词表，而且&lt;em&gt;Positional Encoding&lt;/em&gt;也是一样的。不一样的是上面的&lt;em&gt;Decoder Block&lt;/em&gt;，可以看到首先是&lt;em&gt;Masked Multi-Head Attention&lt;/em&gt;和残差连接到&lt;em&gt;Add &amp;amp; Norm&lt;/em&gt;，然后又是&lt;em&gt;Multi-Head Attention&lt;/em&gt;残差连接到&lt;em&gt;Add &amp;amp; Norm&lt;/em&gt;，最后是&lt;em&gt;Feed Forward&lt;/em&gt;残差连接到&lt;em&gt;Add &amp;amp; Norm&lt;/em&gt;。可以发现我们前文中定义的&lt;code&gt;MultiHeadAttentionBlock&lt;/code&gt;已经考虑到&lt;code&gt;mask&lt;/code&gt;参数，因此只需要使用已经定义好的模块，把他们首尾相接，即可快速组装出这个decoder。&lt;/p&gt;
&lt;h3&gt;2.1 Decoder Block&lt;/h3&gt;
&lt;p&gt;编写decoder模块时需要注意，最下层的&lt;em&gt;Multi-Head Attention&lt;/em&gt;仍然是&quot;self attention&quot;，而中间的&lt;em&gt;Multi-Head Attention&lt;/em&gt;则变为&quot;cross attention&quot;，我们将会用到两种输入并计算他们之间的关系。另外，之所以代码中有&lt;code&gt;src_mask&lt;/code&gt;和&lt;code&gt;tgt_mask&lt;/code&gt;，是因为本文展示的是一个翻译任务，源语言为英语，目标语言为意大利语，所以从算法角度来说，也可以叫做&lt;code&gt;enc_mask&lt;/code&gt;和&lt;code&gt;dec_mask&lt;/code&gt;，只需要记住encoder和decoder各自都有一个&lt;code&gt;mask&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class DecoderBlock(nn.Module):

    def __init__(self, self_attention_block: MultiHeadAttentionBlock, cross_attention_block: MultiHeadAttentionBlock, feed_forward_block: FeedForwardBlock, dropout: float) -&amp;gt; None:
        super().__init__()
        self.self_attention_block = self_attention_block
        self.cross_attention_block = cross_attention_block
        self.feed_forward_block = feed_forward_block
        self.residual_connection = nn.ModuleList([ResidualConnection(dropout) for _ in range(3)])

    # `x`: decoder的输入；
    # `encoder_output`: encoder的输出；
    # `src_mask`: encoder的mask
    # `tgt_mask`: decoder的mask
    def forward(self, x, encoder_output, src_mask, tgt_mask):
        x = self.residual_connection[0](x, lambda x: self.self_attention_block(x, x, x, tgt_mask))
        x = self.residual_connection[1](x, lambda x: self.cross_attention_block(x, encoder_output, encoder_output, src_mask))
        x = self.residual_connection[2](x, self.feed_forward_block)
        return x
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 Decoder Module&lt;/h3&gt;
&lt;p&gt;同encoder类似，decoder也是多个decoder模块首尾相连，最后添加一层&lt;em&gt;Normalization&lt;/em&gt;的结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Decoder(nn.Module):

    def __init__(self, layers: nn.ModuleList) -&amp;gt; None:
        super().__init__()
        self.layers = layers
        self.norm = LayerNormalization()
    
    def forward(self, x, encoder_output, src_mask, tgt_mask):
        for layer in self.layers:
            x = layer(x, encoder_output, src_mask, tgt_mask)
        return self.norm(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3 Linear &amp;amp; Softmax&lt;/h2&gt;
&lt;p&gt;现在，Decoder的输出维度为$(seq, d_{model})$（仍然暂时忽略batch维度），我们想要将这些Embedding映射回词表，于是需要添加一层Linear将这些Embedding映射到词表中各词所在的位置上（因此也可以叫做Projection Layer投影层）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class ProjectionLayer(nn.Module):

    def __init__(self, d_model: int, vocab_size: int) -&amp;gt; None:
        super().__init__()
        self.proj = nn.Linear(d_model, vocab_size)

    def forward(self, x):
        # (batch, seq_len, d_model) --&amp;gt; (batch, seq_len, vocab_size)
        # 这里为了数值稳定性，采用log softmax
        return torch.log_softmax(self.proj(x), dim=-1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4 Transformer&lt;/h2&gt;
&lt;p&gt;现在我们已经完成了Transformer模型中所有模块的编写，是时候将他们组装成一个完整的Transformer了。本文的例子为翻译任务，因此Transformer初始化时需要源语言的Embedding和目标语言的Embedding。我们在用三个函数分别实现&lt;code&gt;encoder&lt;/code&gt;、&lt;code&gt;decoder&lt;/code&gt;、&lt;code&gt;project&lt;/code&gt;，而不是使用一个&lt;code&gt;forward&lt;/code&gt;函数实现，因为在推理过程中，我们可以重用encoder的输出，而且将这部分输出另存一份有利于可视化理解注意力模型的计算过程。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class Transformer(nn.Module):

    def __init__(self, encoder: Encoder, decoder: Decoder, 
                 src_embed: InputEmbeddings, tgt_embed: InputEmbeddings, 
                 src_pos: PositionalEncoding, tgt_pos: PositionalEncoding, 
                 projcetion_layer: ProjectionLayer) -&amp;gt; None:
        super().__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.src_pos = src_pos
        self.tgt_pos = tgt_pos
        self.projcetion_layer = projcetion_layer
    
    def encode(self, src, src_mask):
        src = self.src_embed(src)
        src = self.src_pos(src)
        return self.encoder(src, src_mask)
    
    def decode(self, encoder_output, src_mask, tgt, tgt_mask):
        tgt = self.tgt_embed(tgt)
        tgt = self.tgt_pos(tgt)
        return self.decoder(tgt, encoder_output, src_mask, tgt_mask)
    
    def project(self, x):
        return self.projcetion_layer(x)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此我们已经完成了所有模块的编写，还需要一个函数将这一大堆embedding、encoder、decoder模块赋超参数并完成初始化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def build_transformer(src_vocab_size: int, tgt_vocab_size: int, 
                      src_seq_len: int, tgt_seq_len: int, 
                      d_model: int = 512, N: int = 6, h: int = 8, dropout: float = 0.1, d_ff = 2048) -&amp;gt; Transformer:
    &quot;&quot;&quot;构造Transformer，创建超参数，初始化参数。

        参数: 
            `src_vocab_size`: 源语言的词表大小；
            `tgt_vocab_size`: 目标语言的词表大小；
            `src_seq_len`: 源语言语言句子最大长度；
            `tgt_seq_len`: 目标语言句子最大长度；
            （本文的范例为英语到意大利语的翻译，源和目标语言句子长度设置为相同大小。
            但对于其他语言，可能存在源和目标语言相差非常大的情况，届时将这两个值设置为不同大小）
            `d_model`: 模型大小，与论文一致预设为`512`；
            `N`: encoder/decoder的数量，与论文一致预设为`6`；
            `h`: 注意力头数，与论文一致预设为`8`；
            `dropout`: 与论文一致预设为`0.1`；
            `d_ff`: 前馈网络大小，与论文一致预设为`2048`。
        返回:
            一个完成了参数初始化的`Transformer`。
    &quot;&quot;&quot;  
    
    # 初始化Embedding层
    src_embed = InputEmbeddings(d_model, src_vocab_size)
    tgt_embed = InputEmbeddings(d_model, tgt_vocab_size)

    # 初始化Positional Encoding层，实际上位置信息的编码只需要一组参数计算一次即可，本文使用相同的参数计算两层是为了更好的解释模型
    src_pos = PositionalEncoding(d_model, src_seq_len, dropout)
    tgt_pos = PositionalEncoding(d_model, tgt_seq_len, dropout)

    # 初始化N个Encoder
    encoder_blocks = []
    for _ in range(N):
        encoder_self_attention_block = MultiHeadAttentionBlock(d_model, h, dropout)
        feed_forward_block = FeedForwardBlock(d_model, d_ff, dropout)
        encoder_block = EncoderBlock(encoder_self_attention_block, feed_forward_block, dropout)
        encoder_blocks.append(encoder_block)
    
    # 初始化N个Decoder
    decoder_blocks = []
    for _ in range(N):
        decoder_self_attention_block = MultiHeadAttentionBlock(d_model, h, dropout)
        decoder_cross_attention_block = MultiHeadAttentionBlock(d_model, h, dropout)
        feed_forward_block = FeedForwardBlock(d_model, d_ff, dropout)
        decoder_block = DecoderBlock(decoder_self_attention_block, decoder_cross_attention_block, feed_forward_block, dropout)
        decoder_blocks.append(decoder_block)
    
    # 构造Encoder和Decoder
    encoder = Encoder(nn.ModuleList(encoder_blocks))
    decoder = Decoder(nn.ModuleList(decoder_blocks))

    # 初始化投影层
    projection_layer = ProjectionLayer(d_model, tgt_vocab_size)

    # 构造Transformer
    transformer = Transformer(encoder, decoder, src_embed, tgt_embed, src_pos, tgt_pos, projection_layer)
    
    # 初始化超参数
    for p in transformer.parameters():
        if p.dim() &amp;gt; 1:
            nn.init.xavier_uniform_(p)
    
    return transformer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终，我们完成了一个Transformer的编写，这个过程详细展示了Transformer的内部结构，包括各模块的总体架构、每层的输入输出、各层计算过程及维度、模块间的关系等信息。接下来将要展示如何使用Transformer，包括搜集训练集数据，以及编写训练循环、推理过程、可视化等部分。&lt;/p&gt;
</content:encoded></item><item><title>OpenAI API尝鲜</title><link>https://zlotus.github.io/2023/03/31/openai-api-first-try/</link><guid isPermaLink="true">https://zlotus.github.io/2023/03/31/openai-api-first-try/</guid><description>以API方式体验一下OpenAI的GPT模型们</description><pubDate>Fri, 31 Mar 2023 21:12:00 GMT</pubDate><content:encoded>&lt;p&gt;chat.openai.com这个网页是相当的难伺候，即使你是美帝IP也得看在哪个机房，比如注明的某瓦工就进了微软爸爸的ban list，所以想尝试一下通过API的方式稳定聊天。&lt;/p&gt;
&lt;h2&gt;0x00 WSL中搭环境&lt;/h2&gt;
&lt;p&gt;这两年微软爸爸干了很多人事，包括但不限于azure、vscode、wsl、OpenAI等，我虽然跟苹果十几年老夫老妻，但还是毅然决然的横跳回微软爸爸的怀抱。&lt;/p&gt;
&lt;p&gt;Windows 10自带的WSL2，可以说是相当好用的虚拟机了，因为习惯了POSIX式的命令行环境，就像继续在WSL2里搭环境访问OpenAI。这当然就绕不开那些需要通过修改iptables进行透明代理的软件。&lt;/p&gt;
&lt;p&gt;这一上来就是个下马威，这些软件在执行&lt;code&gt;iptables ... -m string ...&lt;/code&gt;命令的时候直接会报&quot;
iptables Couldn&apos;t load match `string&apos;:No such file or directory&quot;，搜了一下这个报错&lt;a href=&quot;https://bugzilla.redhat.com/show_bug.cgi?id=176318&quot;&gt;可能是iptables缺失string匹配模块&lt;/a&gt;，继续搜发现&lt;a href=&quot;https://wiztelsys.com/Article_iptables_bob2.html&quot;&gt;这个模块需要内核支持&lt;/a&gt;，而非常不巧的是微软爸爸的WSL2默认不支持这个功能。想用？自己加参数&lt;code&gt;CONFIG_NETFILTER_XT_MATCH_STRING=y&lt;/code&gt;编译内核吧。&lt;/p&gt;
&lt;p&gt;软爸倒是早就把&lt;a href=&quot;https://github.com/microsoft/WSL2-Linux-Kernel&quot;&gt;WSL2内核&lt;/a&gt;准备好了，用WSL2就可以编译，编译前改一下&lt;code&gt;Microsoft/config-wsl&lt;/code&gt;，把选项&lt;code&gt;CONFIG_NETFILTER_XT_MATCH_STRING=y&lt;/code&gt;打开即可，桌面CPU编译很快的，指定&lt;code&gt;-j&lt;/code&gt;选项100%负载几分钟就能搞定。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install build-essential flex bison dwarves libssl-dev libelf-dev make bc
make -j 12 KCONFIG_CONFIG=Microsoft/config-wsl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成后内核默认在&lt;code&gt;arch/x86/boot/bzImage&lt;/code&gt;，参考软爸关于WSL2全局配置的&lt;a href=&quot;https://learn.microsoft.com/en-us/windows/wsl/wsl-config#configure-global-options-with-wslconfig&quot;&gt;官方文档&lt;/a&gt;，首先在虚拟机里建个&lt;code&gt;/etc/wsl.conf&lt;/code&gt;，加两行配置把&lt;code&gt;systemd&lt;/code&gt;打开，让它更像个Linux：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[boot]
systemd=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在Windows用户文件夹根目录中建个&lt;code&gt;.wslconfig&lt;/code&gt;文件，加几行配置加载自定义内核：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Settings apply across all Linux distros running on WSL 2
[wsl2]

# Specify a custom Linux kernel to use with your installed distros. The default kernel used can be found at https://github.com/microsoft/WSL2-Linux-Kernel
kernel=C:\\&amp;lt;path-to-kernel&amp;gt;\\bzImage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时&lt;code&gt;wsl --shutdown&lt;/code&gt;关闭WSL2，等一小会再打开，&lt;code&gt;uname -a&lt;/code&gt;看内核时间编译时间就能发现已经用上咱新鲜出炉还冒着热气的内核了。再用&lt;code&gt;iptables -m string --help&lt;/code&gt;试一下发现不报错了，这些软件正常工作之后，jupyter里面用OpenAI的API示例测试取一下模型列表就能正常返回了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import openai
openai.api_key = &quot;your-api-key&quot;
openai.Model.list()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;0x01 策划&lt;/h2&gt;
&lt;p&gt;chatgpt用我手里现有的工具真的不好访问，经常就是access denied，即使用某瓦工在洛杉矶的VPS依然是黑名单，所以就想用API访问。大概的流程就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;浏览器访问云服务器上的某个前端网页&lt;/li&gt;
&lt;li&gt;云服务器上的前端网页穿透倒家里的rock5b，OpenAI的API服务端就放在rock5b上，十来瓦的功率24小时开机也不肉疼&lt;/li&gt;
&lt;li&gt;rock5b服务端负责拿着API key去OpenAI的API上去聊天结果，再返回给前端&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;0x02 魔改&lt;/h2&gt;
&lt;p&gt;我直接在Github上搜OpenAI、ChatBot这些关键词，挑了一个长得顺眼的&lt;a href=&quot;https://github.com/VirajMadhu/OpenAIChatBot&quot;&gt;OpenAIChatBot&lt;/a&gt;，作者应该是在API发布早期写了这个项目就去玩别的了，三月初OpenAI更新了专门聊天的API，我就&lt;a href=&quot;https://github.com/zlotus/OpenAIChatBot&quot;&gt;fork这个项目魔改了一下&lt;/a&gt;，现在可以用&lt;code&gt;createChatCompletion&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;我直接用这个API替换了原项目里的&lt;code&gt;createCompletion&lt;/code&gt;，因为官方建议用聊天模型&lt;code&gt;gpt-3.5-turbo&lt;/code&gt;代替原项目里的问答模型&lt;code&gt;text-davinci-003&lt;/code&gt;，理由是效果好还便宜。&lt;/p&gt;
&lt;p&gt;需要注意的是，对于前端网页，问答模型只需要一问一答，也就是发一个prompt收一个replay并显示即可。而聊天模型需要记录聊天双方的历史对话，并将所有内容发给API，再拿到一个新的聊天回复。&lt;/p&gt;
&lt;h2&gt;0x03 部署&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;项目是用nodejs的express写的，前端写好了&lt;code&gt;npm build&lt;/code&gt;放在云服务器上就好了，如果有需要可以调一下&lt;code&gt;/assets&lt;/code&gt;的绝对/相对路径。&lt;/li&gt;
&lt;li&gt;前端访问后端的地址为&lt;code&gt;http://cloud-server/url/to/chat/api/&lt;/code&gt;，在NGINX配置里加一个&lt;code&gt;location&lt;/code&gt;，url匹配到&lt;code&gt;/url/to/chat/api/&lt;/code&gt;，在里面加上&lt;code&gt;proxy_pass http://localhost:&amp;lt;lan-server-port&amp;gt;/;&lt;/code&gt;（代理设置参考&lt;a href=&quot;https://xuexb.github.io/learn-nginx/example/proxy_pass.html#url-%E5%8C%85%E5%90%AB%E8%B7%AF%E5%BE%84&quot;&gt;NGINX入门教程&lt;/a&gt;，一个&lt;code&gt;/&lt;/code&gt;都不能错&lt;code&gt;-_-&lt;/code&gt;）。此时NGINX就能将API转发到云服务器本地的&lt;code&gt;your-lan-server-port&lt;/code&gt;端口上了。记得防火墙要把这个端口要打开，但为了安全源可以只写云服务器的公网IP。&lt;/li&gt;
&lt;li&gt;用内网穿透（我用的&lt;code&gt;nps&lt;/code&gt;，frp之类的工具也完全可以）把云服务器上&lt;code&gt;&amp;lt;lan-server-port&amp;gt;&lt;/code&gt;口收到的数据发给家里的rock5b服务端，rock5b的服务端监听&lt;code&gt;0.0.0.0:&amp;lt;lan-server-port&amp;gt;&lt;/code&gt;，启动&lt;code&gt;npc&lt;/code&gt;连接到云服务器的&lt;code&gt;nps&lt;/code&gt;就大功告成。&lt;/li&gt;
&lt;li&gt;因为中间的网络有点乱，可以在客户端和服务端加&lt;code&gt;console.log&lt;/code&gt;打印看看请求和响应，测试没问题再去掉。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;0x04 后记&lt;/h2&gt;
&lt;p&gt;其实这个fork项目极度简单，就是一收一发两端就完了。如果后期继续开发的话，还可以加上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示方面的优化，比如用markdown渲染OpenAI的回复，起码能把代码块弄好看一点&lt;/li&gt;
&lt;li&gt;对chat completion回复，目前就是拿到内容贴在网页上，其实可以加上更多细节：
&lt;ul&gt;
&lt;li&gt;显示回复是否因字数限制而中断&lt;/li&gt;
&lt;li&gt;对token计数，帮助我这种抠门人士精打细算的措辞&lt;/li&gt;
&lt;li&gt;自定义系统角色，目前的聊天角色是我写死的&lt;code&gt;{&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;You are a helpful programming assistant&quot;}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他资料：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/api-reference/chat/create&quot;&gt;API Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://platform.openai.com/docs/guides/chat/introduction&quot;&gt;Chat Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两天稚晖君又发视频了，这次是双足机器人，还能变形成轮式，过于酷炫。&lt;strong&gt;立于皓月之边，不弱星光之势&lt;/strong&gt;。&lt;/p&gt;
</content:encoded></item><item><title>白嫖甲骨文云，但没有完全白嫖</title><link>https://zlotus.github.io/2023/02/11/oracle-cloud-arm-vps/</link><guid isPermaLink="true">https://zlotus.github.io/2023/02/11/oracle-cloud-arm-vps/</guid><description>一分价钱一分货，便宜没好货啊！</description><pubDate>Sat, 11 Feb 2023 23:33:00 GMT</pubDate><content:encoded>&lt;p&gt;听说甲骨文云有完全免费的VPS，气急败坏的去白嫖，看到首尔机房的延迟只有60ms果断下手，然后才知道整个棒子的甲骨文云的IP都被屏蔽了才后悔，应该听老人言用美国西海岸的机房。然鹅一切都太迟了，一个用户一张信用卡在免费账户中只能选一个机房，选定离手不能改……后来者看到这里一定要慎重选区呀！&lt;/p&gt;
&lt;p&gt;:::caution
仅限技术交流！
:::&lt;/p&gt;
&lt;h2&gt;主机概况&lt;/h2&gt;
&lt;p&gt;甲骨文云的免费账户有两种VPS可以选，一种是普通x86的AMD处理器，1CPU/1G的普通机器；另一种是4CPU/24G的ARM机器。虽然提示说首尔和东京机房的ARM资源吃紧，不一定能嫖到，但是我不知道怎么回事蹲到了一个ARM机器，果断开了个机器。开实例的时候记得把私钥存下来，甲骨文云的主机默认ssh禁止用户名密码登录。&lt;/p&gt;
&lt;p&gt;用的是Ubuntu 22.04的aarch64版本，甲骨文云的实例创建以后，虽然会给一个公网IP，但是此时还需要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在下方&lt;code&gt;快速操作&lt;/code&gt;中选择&lt;code&gt;将公共子网连接到Internet&lt;/code&gt;，一路默认点创建，之后上方面板中的网络安全组就有了&lt;code&gt;ig-quick-action-NSG&lt;/code&gt;，编辑这个网络安全组，添加&lt;code&gt;ssh&lt;/code&gt;、&lt;code&gt;ICMP&lt;/code&gt;等想要的入站规则；&lt;/li&gt;
&lt;li&gt;点面板中的&lt;code&gt;子网&lt;/code&gt;，然后在&lt;code&gt;安全列表&lt;/code&gt;中添加想要的的&lt;code&gt;入站规则&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于首尔的甲骨文云全部被屏蔽了，只能走某瓦工了。外面的ping能通而且nc端口22也能通，用创建主机时拿到的私钥或在面板下方的&lt;code&gt;控制台连接&lt;/code&gt;中拿到的私钥连接主机即可，用户名就在面板中，Ubuntu系统的默认用户名是&lt;code&gt;ubuntu&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -i ~/.ssh/&amp;lt;oracle-cloud.key&amp;gt; ubuntu@&amp;lt;vps-ip-address&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看机器的硬件还是相当给力的，&lt;code&gt;lscpu&lt;/code&gt;和&lt;code&gt;lsmem&lt;/code&gt;表示确实有4CPU/24G。甲骨文云在ARM机器的免费策略是，每月给CPU了3000小时，给内存了18000G小时，也就是&lt;code&gt;31*24*4=2976&lt;/code&gt;和&lt;code&gt;31*24*24=17865&lt;/code&gt;，所以理论上这个配置就是永久免费。这硬件跑点大应用应该不难。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt dist-upgrade -y
sudo apt install zsh 
sudo chsh -s $(which zsh)
sudo sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;

# /etc/ssh/sshd_config 中：
# 调整ssh会话一次存活60秒，自动重连60次
ClientAliveInterval 60
ClientAliveCountMax 60
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开防火墙策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow 22/tcp comment ssh
sudo ufw allow 443/tcp comment https
sudo ufw allow 80/tcp comment http
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装docker&lt;/h2&gt;
&lt;p&gt;按照&lt;a href=&quot;https://i12bretro.github.io/tutorials/0476.html&quot;&gt;这篇文章&lt;/a&gt;一次安装完成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;更新，安装依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# update software repositories
sudo apt update
# install necessary packages for https apt calls
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用Ubuntu构建的Armbian添加如下gpg秘钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# add docker GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# add docker software repository
sudo add-apt-repository &quot;deb [arch=arm64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;**（跳过）**使用Debian构建的Armbian添加如下gpg秘钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# add docker GPG key
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
# add docker software repository
sudo add-apt-repository &quot;deb [arch=arm64] https://download.docker.com/linux/debian $(lsb_release -cs) stable&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装docker：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# install docker
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io
# start docker service
sudo systemctl start docker
# enable docker service on startup
sudo systemctl enable docker
# create a docker group
sudo groupadd docker
# add the current user to the docker group
sudo usermod -aG docker $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用hello-world镜像测试安装结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo docker run hello-world

Unable to find image &apos;hello-world:latest&apos; locally
latest: Pulling from library/hello-world
7050e35b49f5: Pull complete
Digest: sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the &quot;hello-world&quot; image from the Docker Hub.
    (arm64v8)
3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>对Rust生命周期的一些常见误解</title><link>https://zlotus.github.io/2022/12/24/common-rust-lifetime-misconceptions/</link><guid isPermaLink="true">https://zlotus.github.io/2022/12/24/common-rust-lifetime-misconceptions/</guid><description>《PingCap的Rust训练课程5：异步编程》中提到的一篇好文，值得一看。</description><pubDate>Sat, 24 Dec 2022 16:45:00 GMT</pubDate><content:encoded>&lt;p&gt;:::note
本文内容大多翻译自原文：&lt;a href=&quot;https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md&quot;&gt;Common Rust Lifetime Misconceptions&lt;/a&gt;。
:::&lt;/p&gt;
&lt;h2&gt;目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%9B%AE%E5%BD%95&quot;&gt;目录&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%A6%82%E8%BF%B0&quot;&gt;概述&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%A6%82%E5%BF%B5%E8%AF%AF%E8%A7%A3&quot;&gt;概念误解&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#1-t%E5%8F%AA%E5%8C%85%E5%90%AB%E8%87%AA%E6%9C%89%E7%B1%BB%E5%9E%8B&quot;&gt;1) &lt;code&gt;T&lt;/code&gt;只包含自有类型&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#2-%E5%A6%82%E6%9E%9CT-39-static%EF%BC%8C%E5%88%99T%E5%9C%A8%E7%A8%8B%E5%BA%8F%E6%95%B4%E4%B8%AA%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E9%83%BD%E6%9C%89%E6%95%88&quot;&gt;2) 如果&lt;code&gt;T: &apos;static&lt;/code&gt;，则&lt;code&gt;T&lt;/code&gt;在程序整个生命周期都有效&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#3-amp-39-a-T%E4%B8%8ET-39-a%E6%98%AF%E4%B8%80%E5%9B%9E%E4%BA%8B&quot;&gt;3) &lt;code&gt;&amp;amp;&apos;a T&lt;/code&gt;与&lt;code&gt;T: &apos;a&lt;/code&gt;是一回事&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#4-%E6%88%91%E7%9A%84%E4%BB%A3%E7%A0%81%E6%B2%A1%E6%9C%89%E6%B3%9B%E5%9E%8B%E4%B9%9F%E6%B2%A1%E6%9C%89%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F&quot;&gt;4) 我的代码没有泛型也没有生命周期&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#5-%E5%A6%82%E6%9E%9C%E4%BB%A3%E7%A0%81%E9%80%9A%E8%BF%87%E7%BC%96%E8%AF%91%EF%BC%8C%E5%B0%B1%E8%AF%B4%E6%98%8E%E6%88%91%E4%BB%AC%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E6%B3%A8%E9%87%8A%E6%98%AF%E6%AD%A3%E7%A1%AE%E7%9A%84&quot;&gt;5) 如果代码通过编译，就说明我们的生命周期注释是正确的&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#6-box%E6%8C%87%E9%92%88%E4%B8%AD%E7%9A%84trait%E5%AF%B9%E8%B1%A1%E6%B2%A1%E6%9C%89%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E6%A6%82%E5%BF%B5&quot;&gt;6) box指针中的trait对象没有生命周期概念&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#7-%E7%BC%96%E8%AF%91%E5%99%A8%E7%9A%84%E9%94%99%E8%AF%AF%E6%8F%90%E7%A4%BA%E4%BC%9A%E5%91%8A%E8%AF%89%E6%88%91%E5%A6%82%E4%BD%95%E4%BF%AE%E5%A4%8D%E7%A8%8B%E5%BA%8F&quot;&gt;7) 编译器的错误提示会告诉我如何修复程序&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#8-%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E5%8F%AF%E4%BB%A5%E5%9C%A8%E8%BF%90%E8%A1%8C%E6%97%B6%E5%A2%9E%E9%95%BF%E5%92%8C%E6%94%B6%E7%BC%A9&quot;&gt;8) 生命周期可以在运行时增长和收缩&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#9-%E5%B0%86%E5%8F%AF%E5%8F%98%E5%BC%95%E7%94%A8%E9%99%8D%E7%BA%A7%E4%B8%BA%E5%85%B1%E4%BA%AB%E5%BC%95%E7%94%A8%E6%98%AF%E5%AE%89%E5%85%A8%E7%9A%84&quot;&gt;9) 将可变引用降级为共享引用是安全的&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#10-%E9%97%AD%E5%8C%85%E9%81%B5%E5%BE%AA%E4%B8%8E%E5%87%BD%E6%95%B0%E7%9B%B8%E5%90%8C%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E7%9C%81%E7%95%A5%E8%A7%84%E5%88%99&quot;&gt;10) 闭包遵循与函数相同的生命周期省略规则&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E7%BB%93%E8%AE%BA&quot;&gt;结论&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%AE%A8%E8%AE%BA&quot;&gt;讨论&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E8%AE%A2%E9%98%85&quot;&gt;订阅&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#%E6%B7%B1%E5%85%A5%E9%98%85%E8%AF%BB&quot;&gt;深入阅读&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;文中列出的这些误解都是我曾经以为正确的概念，我也看到现在仍有许多初学者都因为这些误解而苦苦挣扎。文中的一些术语可能不标准，我列了一张表，以解释我在文中使用某些短语时原本想表达的意思。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;短语&lt;/th&gt;
&lt;th&gt;解释&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1) 包含所有可能类型的集合 &lt;em&gt;或&lt;/em&gt;&amp;lt;br&amp;gt;2) 该集合中的某种类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自有类型&lt;/td&gt;
&lt;td&gt;某些非引用类型，如&lt;code&gt;i32&lt;/code&gt;、&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Vec&lt;/code&gt;等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1) 借用类型 &lt;em&gt;或&lt;/em&gt;&amp;lt;br&amp;gt;2) 引用类型&lt;/td&gt;
&lt;td&gt;某些引用类型，可变或不可变均可，如&lt;code&gt;&amp;amp;i32&lt;/code&gt;、&lt;code&gt;&amp;amp;mut i32&lt;/code&gt;等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1) 可变借用 &lt;em&gt;或&lt;/em&gt;&amp;lt;br&amp;gt;2) 互斥借用&lt;/td&gt;
&lt;td&gt;排他可变引用，如&lt;code&gt;&amp;amp;mut T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1) immut ref &lt;em&gt;or&lt;/em&gt;&amp;lt;br&amp;gt;2) shared ref&lt;/td&gt;
&lt;td&gt;shared immutable reference, i.e. &lt;code&gt;&amp;amp;T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;概念误解&lt;/h2&gt;
&lt;p&gt;总的来说，一个变量的生命周期是指：该变量指向的数据在其当前的内存地址中，能被编译器静态验证为有效的时间。下面将用6500字的篇幅来详细说明大家通常会混淆的地方。&lt;/p&gt;
&lt;h3&gt;1) &lt;code&gt;T&lt;/code&gt;只包含自有类型&lt;/h3&gt;
&lt;p&gt;这种误解更多的是关于泛型，而是不关于生命周期，但是泛型和生命周期在Rust中紧密的交织在一起，所以不可能在谈论一个的时候不提及另一个。总之：&lt;/p&gt;
&lt;p&gt;当我刚开始学习Rust时，我明白&lt;code&gt;i32&lt;/code&gt;、&lt;code&gt;&amp;amp;i32&lt;/code&gt;和&lt;code&gt;&amp;amp;mut i32&lt;/code&gt;是不同的类型。我也明白一些泛型变量&lt;code&gt;T&lt;/code&gt;代表了一个包含所有可能类型的集合。然而，尽管我分别理解了这两个概念，但我并不能把它们放在一起理解。在我的新手Rust大脑中，我认为泛型是这样工作的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;类型变量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;mut T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;i32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;i32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;mut i32&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;T&lt;/code&gt;包含所有自有类型。&lt;code&gt;&amp;amp;T&lt;/code&gt;包含所有不可变借用类型。&lt;code&gt;&amp;amp;mut T&lt;/code&gt;包含所有可变借用类型。&lt;code&gt;T&lt;/code&gt;、&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;是互不相交的有限集合很好，简单，干净，容易，直观，但这是完全错误的理解。Rust中的泛型实际上是这样工作的：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;类型变量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;T&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;mut T&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;示例&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;i32&lt;/code&gt;， &lt;code&gt;&amp;amp;i32&lt;/code&gt;, &lt;code&gt;&amp;amp;mut i32&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;i32&lt;/code&gt;, &lt;code&gt;&amp;amp;mut &amp;amp;mut i32&lt;/code&gt;, ...&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;i32&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;i32&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;mut i32&lt;/code&gt;, ...&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;mut i32&lt;/code&gt;, &lt;code&gt;&amp;amp;mut &amp;amp;mut i32&lt;/code&gt;, &lt;code&gt;&amp;amp;mut &amp;amp;i32&lt;/code&gt;, ...&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;T&lt;/code&gt;、&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;均为无限集合，因为我们可以无限地借用一个类型。&lt;code&gt;T&lt;/code&gt;是&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;的超集。而&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;是互斥集合。下面用几个例子验证这些概念：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait Trait {}

impl&amp;lt;T&amp;gt; Trait for T {}

impl&amp;lt;T&amp;gt; Trait for &amp;amp;T {} // ❌

impl&amp;lt;T&amp;gt; Trait for &amp;amp;mut T {} // ❌
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的程序并不能通过编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0119]: conflicting implementations of trait `Trait` for type `&amp;amp;_`:
 --&amp;gt; src/lib.rs:5:1
  |
3 | impl&amp;lt;T&amp;gt; Trait for T {}
  | ------------------- first implementation here
4 |
5 | impl&amp;lt;T&amp;gt; Trait for &amp;amp;T {}
  | ^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&amp;amp;_`

error[E0119]: conflicting implementations of trait `Trait` for type `&amp;amp;mut _`:
 --&amp;gt; src/lib.rs:7:1
  |
3 | impl&amp;lt;T&amp;gt; Trait for T {}
  | ------------------- first implementation here
...
7 | impl&amp;lt;T&amp;gt; Trait for &amp;amp;mut T {}
  | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&amp;amp;mut _`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译器不允许我们为&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;实现&lt;code&gt;Trait&lt;/code&gt;，因为这会与&lt;code&gt;T&lt;/code&gt;的&lt;code&gt;Trait&lt;/code&gt;实现冲突，后者已经包括了&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;的所有内容。下面的程序可以通过编译，因为&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;互斥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait Trait {}

impl&amp;lt;T&amp;gt; Trait for &amp;amp;T {}  // ✅

impl&amp;lt;T&amp;gt; Trait for &amp;amp;mut T {}  // ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt;是&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;的超集&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;是互斥集合&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) 如果&lt;code&gt;T: &apos;static&lt;/code&gt;，则&lt;code&gt;T&lt;/code&gt;在程序整个生命周期都有效&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;误解的推论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T: &apos;static&lt;/code&gt;应该读作“&lt;code&gt;T&lt;/code&gt;具有&lt;code&gt;&apos;static&lt;/code&gt;的生命周期”&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&apos;static T&lt;/code&gt;与&lt;code&gt;T: &apos;static&lt;/code&gt;是一回事&lt;/li&gt;
&lt;li&gt;如果有&lt;code&gt;T: &apos;static&lt;/code&gt;，则&lt;code&gt;T&lt;/code&gt;一定是不可变的&lt;/li&gt;
&lt;li&gt;如果有&lt;code&gt;T: &apos;static&lt;/code&gt;，则&lt;code&gt;T&lt;/code&gt;只能在编译期创建&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大多数Rust初学者第一次接触到&lt;code&gt;&apos;static&lt;/code&gt;生命周期，是在一个类似这样的代码示例中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let str_literal: &amp;amp;&apos;static str = &quot;str literal&quot;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;他们被告知&lt;code&gt;&quot;str literal&quot;&lt;/code&gt;是被硬编码到编译好的二进制文件中的，并且在运行时被加载到只读存储器中，所以它是不可变的，在整个程序的生命周期中都有效，这就是它&lt;code&gt;&apos;static&lt;/code&gt;的含义。这些观念通过使用&lt;code&gt;static&lt;/code&gt;关键字定义&lt;code&gt;static&lt;/code&gt;变量的规则得到了进一步加强。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注意：这个理智纯粹是为了说明问题。
// 永远不要使用`static mut`这种别扭的变量。
// 在Rust中，有一些安全的全局可变单例模式，
// 但这些不在本文的讨论范围之内。

static BYTES: [u8; 3] = [1, 2, 3];
static mut MUT_BYTES: [u8; 3] = [1, 2, 3];

fn main() {
    MUT_BYTES[0] = 99; // ❌ - 修改静态变量是非安全的

    unsafe {
        MUT_BYTES[0] = 99;
        assert_eq!(99, MUT_BYTES[0]);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于&lt;code&gt;static&lt;/code&gt;变量&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只能在编译期期创建&lt;/li&gt;
&lt;li&gt;应该是不可变的，修改静态变量是非安全的&lt;/li&gt;
&lt;li&gt;在整个程序的生命周期中都有效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;&apos;static&lt;/code&gt;生命周期可能是根据&lt;code&gt;static&lt;/code&gt;变量的默认生命周期来命名的，对吧？所以&lt;code&gt;&apos;static&lt;/code&gt;生命周期遵循&lt;code&gt;static&lt;/code&gt;变量相同的规则是很合理的，对吧？&lt;/p&gt;
&lt;p&gt;也对，但是&lt;em&gt;具有&lt;/em&gt;&lt;code&gt;&apos;static&lt;/code&gt;生命周期的类型与&lt;em&gt;受到&lt;/em&gt;&lt;code&gt;&apos;static&lt;/code&gt;生命周期约束的类型是不同的。后者可以在运行时动态分配、可以安全的在任何地方修改、可以被析构，而且其生命周期可以持续任意的时间。&lt;/p&gt;
&lt;p&gt;在这一点上，将&lt;code&gt;&amp;amp;&apos;static T&lt;/code&gt;与&lt;code&gt;T: &apos;static&lt;/code&gt;区分开很重要。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&apos;static T&lt;/code&gt;是对某个&lt;code&gt;T&lt;/code&gt;的不可变引用，可以安全的持有无限长时间，包括持有到程序结束。但这只有在&lt;code&gt;T&lt;/code&gt;本身是不可变的且在&lt;em&gt;引用被创建后没有移动&lt;/em&gt;时才可以。&lt;code&gt;T&lt;/code&gt;并不一定要在编译期创建。以内存泄漏为代价，在运行时生成随机的动态分配的数据再返回其&lt;code&gt;&apos;static&lt;/code&gt;引用，也是可行的，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use rand;

// 在运行时生成随机的&apos;static str引用
fn rand_str_generator() -&amp;gt; &amp;amp;&apos;static str {
    let rand_string = rand::random::&amp;lt;u64&amp;gt;().to_string();
    box::leak(rand_string.into_boxed_str())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;T: &apos;static&lt;/code&gt;是一种可以安全的无限期持有的&lt;code&gt;T&lt;/code&gt;，包括持有到程序结束。&lt;code&gt;T: &apos;static&lt;/code&gt;包括所有的&lt;code&gt;&amp;amp;&apos;static T&lt;/code&gt;，此外还包括所有的自有类型，比如&lt;code&gt;String&lt;/code&gt;、&lt;code&gt;Vec&lt;/code&gt;等。一些数据的持有者被保证，只要所有者持有数据，数据就不会失效，因此所有者可以安全地无限期地持有数据，包括持有到程序结束。&lt;code&gt;T: &apos;static&lt;/code&gt;应该读作“&lt;code&gt;T&lt;/code&gt;受到&lt;code&gt;&apos;static&lt;/code&gt;生命周期约束”，而不是“&lt;code&gt;T&lt;/code&gt;具有&lt;code&gt;&apos;static&lt;/code&gt;生命周期”。用一个程序来帮助解释这一概念：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use rand;

fn drop_static&amp;lt;T: &apos;static&amp;gt;(t: T) {
    std::mem::drop(t);
}

fn main() {
    let mut strings: Vec&amp;lt;String&amp;gt; = Vec::new();
    for _ in 0..10 {
        if rand::random() {
            // 所以字符串都是随机生成且在运行时动态分配的
            let string = rand::random::&amp;lt;u64&amp;gt;().to_string();
            strings.push(string);
        }
    }

    // 字符串为自有类型，因此受到&apos;static约束
    for mut string in strings {
        // 所有字符串均可变
        string.push_str(&quot;a mutation&quot;);
        // 所有字符串均可析构
        drop_static(string);
    }

    // 所有字符串在程序结束前均已失效
    println!(&quot;I am the end of program&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T: &apos;static&lt;/code&gt;应读作“&lt;em&gt;&lt;code&gt;T&lt;/code&gt;受到&lt;code&gt;&apos;static&lt;/code&gt;生命周期约束&lt;/em&gt;”&lt;/li&gt;
&lt;li&gt;如果有&lt;code&gt;T: &apos;static&lt;/code&gt;，则&lt;code&gt;T&lt;/code&gt;可以是一个具有&lt;code&gt;&apos;static&lt;/code&gt;生命周期的借用类型 &lt;em&gt;或&lt;/em&gt; 一个自有类型&lt;/li&gt;
&lt;li&gt;因为&lt;code&gt;T: &apos;static&lt;/code&gt;包括自有类型，这意味着&lt;code&gt;T&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;可以在运行时被动态的分配&lt;/li&gt;
&lt;li&gt;不需要在程序的生命周期中始终有效&lt;/li&gt;
&lt;li&gt;可以安全、自由的修改&lt;/li&gt;
&lt;li&gt;可以在运行时动态的析构&lt;/li&gt;
&lt;li&gt;可以有不同持续时间的生命周期&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3) &lt;code&gt;&amp;amp;&apos;a T&lt;/code&gt;与&lt;code&gt;T: &apos;a&lt;/code&gt;是一回事&lt;/h3&gt;
&lt;p&gt;这条误解是上一条误解的泛化版本。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;amp;&apos;a T&lt;/code&gt;要求并暗指&lt;code&gt;T: &apos;a&lt;/code&gt;，因为如果&lt;code&gt;T&lt;/code&gt;本身在生命周期&lt;code&gt;&apos;a&lt;/code&gt;上是无效的，那么以&lt;code&gt;&apos;a&lt;/code&gt;为生命周期的&lt;code&gt;T&lt;/code&gt;的引用在生命周期&lt;code&gt;&apos;a&lt;/code&gt;上自然也是无效的。例如，Rust编译器不允许构造&lt;code&gt;&amp;amp;&apos;static Ref&amp;lt;&apos;a, T&amp;gt;&lt;/code&gt;类型，因为如果&lt;code&gt;Ref&lt;/code&gt;只在&lt;code&gt;&apos;a&lt;/code&gt;上有效，我们就不能对它使用&lt;code&gt;&apos;static&lt;/code&gt;引用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;T: &apos;a&lt;/code&gt;包括所有的&lt;code&gt;&amp;amp;&apos;a T&lt;/code&gt;，但反过来就不是这样了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 只接受受到&apos;a约束的类型的引用
fn t_ref&amp;lt;&apos;a, T: &apos;a&amp;gt;(t: &amp;amp;&apos;a T) {}

// 接受任意受到&apos;a约束的类型
fn t_bound&amp;lt;&apos;a, T: &apos;a&amp;gt;(t: T) {}

// 包含一个引用的自有类型
struct Ref&amp;lt;&apos;a, T: &apos;a&amp;gt;(&amp;amp;&apos;a T);

fn main() {
    let string = String::from(&quot;string&quot;);

    t_bound(&amp;amp;string); // ✅
    t_bound(Ref(&amp;amp;string)); // ✅
    t_bound(&amp;amp;Ref(&amp;amp;string)); // ✅

    t_ref(&amp;amp;string); // ✅
    t_ref(Ref(&amp;amp;string)); // ❌ - 期待收到一个引用，但实际收到一个结构体
    t_ref(&amp;amp;Ref(&amp;amp;string)); // ✅

    // string var is bounded by &apos;static which is bounded by &apos;a
    // 字符串变量受到&apos;static约束，即受到&apos;a约束
    t_bound(string); // ✅
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T: &apos;a&lt;/code&gt;比&lt;code&gt;&amp;amp;&apos;a T&lt;/code&gt;更通用也更灵活&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T: &apos;a&lt;/code&gt;接受自有类型、包含引用的自有类型以及引用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&apos;a T&lt;/code&gt;只接受引用&lt;/li&gt;
&lt;li&gt;如果有&lt;code&gt;T: &apos;static&lt;/code&gt;，那么&lt;code&gt;T: &apos;a&lt;/code&gt;，因为&lt;code&gt;&apos;static&lt;/code&gt; &amp;gt;= 所有的&lt;code&gt;&apos;a&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4) 我的代码没有泛型也没有生命周期&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;误解的推论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以避免使用泛型和生命周期&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这一令人欣慰的误解之所以能够继续存在，要感谢Rust的生命周期省略规则，它允许我们在函数中省略生命周期注释，因为Rust的借用检查器会按照这些规则来推断生命周期：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于一个函数，每个输入的引用参数都具有不同的生命周期&lt;/li&gt;
&lt;li&gt;如果有且只有一个输入的生命周期，则所有输出的引用都将应用该生命周期&lt;/li&gt;
&lt;li&gt;如果有多个输入的生命周期，但其中一个是&lt;code&gt;&amp;amp;self&lt;/code&gt;或&lt;code&gt;&amp;amp;mut self&lt;/code&gt;，则所有输出的引用都将应用&lt;code&gt;self&lt;/code&gt;的生命周期&lt;/li&gt;
&lt;li&gt;否则就必须明确输出生命周期&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这么多规则很难一下就弄明白，我们来看几个例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 省略
fn print(s: &amp;amp;str);

// 展开为
fn print&amp;lt;&apos;a&amp;gt;(s: &amp;amp;&apos;a str);

// 省略
fn trim(s: &amp;amp;str) -&amp;gt; &amp;amp;str;

// 展开为
fn trim&amp;lt;&apos;a&amp;gt;(s: &amp;amp;&apos;a str) -&amp;gt; &amp;amp;&apos;a str;

// 错误，因为没有输入，无法确定输出的生命周期
fn get_str() -&amp;gt; &amp;amp;str;

// 展开的选项包括
fn get_str&amp;lt;&apos;a&amp;gt;() -&amp;gt; &amp;amp;&apos;a str; // 泛型版本
fn get_str() -&amp;gt; &amp;amp;&apos;static str; // &apos;static版本

// 错误，因为有多个输入，无法确定输出的生命周期
fn overlap(s: &amp;amp;str, t: &amp;amp;str) -&amp;gt; &amp;amp;str;

// 展开（依然有部分省略）的选项包括
fn overlap&amp;lt;&apos;a&amp;gt;(s: &amp;amp;&apos;a str, t: &amp;amp;str) -&amp;gt; &amp;amp;&apos;a str; // 输出的有效期不能超过s
fn overlap&amp;lt;&apos;a&amp;gt;(s: &amp;amp;str, t: &amp;amp;&apos;a str) -&amp;gt; &amp;amp;&apos;a str; // 输出的有效期不能超过t
fn overlap&amp;lt;&apos;a&amp;gt;(s: &amp;amp;&apos;a str, t: &amp;amp;&apos;a str) -&amp;gt; &amp;amp;&apos;a str; // 输出的有效期不能超过s和t
fn overlap(s: &amp;amp;str, t: &amp;amp;str) -&amp;gt; &amp;amp;&apos;static str; // 输出的有效期可以超过s和t
fn overlap&amp;lt;&apos;a&amp;gt;(s: &amp;amp;str, t: &amp;amp;str) -&amp;gt; &amp;amp;&apos;a str; // 输出的生命周期与输入无关

// 展开为
fn overlap&amp;lt;&apos;a, &apos;b&amp;gt;(s: &amp;amp;&apos;a str, t: &amp;amp;&apos;b str) -&amp;gt; &amp;amp;&apos;a str;
fn overlap&amp;lt;&apos;a, &apos;b&amp;gt;(s: &amp;amp;&apos;a str, t: &amp;amp;&apos;b str) -&amp;gt; &amp;amp;&apos;b str;
fn overlap&amp;lt;&apos;a&amp;gt;(s: &amp;amp;&apos;a str, t: &amp;amp;&apos;a str) -&amp;gt; &amp;amp;&apos;a str;
fn overlap&amp;lt;&apos;a, &apos;b&amp;gt;(s: &amp;amp;&apos;a str, t: &amp;amp;&apos;b str) -&amp;gt; &amp;amp;&apos;static str;
fn overlap&amp;lt;&apos;a, &apos;b, &apos;c&amp;gt;(s: &amp;amp;&apos;a str, t: &amp;amp;&apos;b str) -&amp;gt; &amp;amp;&apos;c str;

// 省略
fn compare(&amp;amp;self, s: &amp;amp;str) -&amp;gt; &amp;amp;str;

// 展开为
fn compare&amp;lt;&apos;a, &apos;b&amp;gt;(&amp;amp;&apos;a self, &amp;amp;&apos;b str) -&amp;gt; &amp;amp;&apos;a str;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你曾写过&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个结构体方法&lt;/li&gt;
&lt;li&gt;一个接受多个引用的函数&lt;/li&gt;
&lt;li&gt;一个返回多个引用的函数&lt;/li&gt;
&lt;li&gt;一个泛型函数&lt;/li&gt;
&lt;li&gt;一个trait对象（后文会有更多介绍）&lt;/li&gt;
&lt;li&gt;一个闭包（后文会有更多介绍）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那么你的代码中就充满了省略的生命周期注释。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;几乎所有的Rust代码都是泛型代码，到处都有被省略的生命周期注释。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5) 如果代码通过编译，就说明我们的生命周期注释是正确的&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;误解的推论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rust函数的生命周期省略规则总是正确&lt;/li&gt;
&lt;li&gt;Rust的借用检查器在技术上和语义上总是正确&lt;/li&gt;
&lt;li&gt;Rust比我更了解我的程序的语义&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Rust程序即使在技术上可以通过编译，但在语义上仍然有可能是错误的。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct ByteIter&amp;lt;&apos;a&amp;gt; {
    remainder: &amp;amp;&apos;a [u8]
}

impl&amp;lt;&apos;a&amp;gt; ByteIter&amp;lt;&apos;a&amp;gt; {
    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;&amp;amp;u8&amp;gt; {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &amp;amp;self.remainder[0];
            self.remainder = &amp;amp;self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b&quot;1&quot; };
    assert_eq!(Some(&amp;amp;b&apos;1&apos;), bytes.next());
    assert_eq!(None, bytes.next());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ByteIter&lt;/code&gt;是一个迭代字节片的迭代器。为了简洁起见，我们跳过了&lt;code&gt;Iterator&lt;/code&gt; trait实现。它似乎能够正常工作，但如果我们想同时检查几个字节会怎样？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let mut bytes = ByteIter { remainder: b&quot;1123&quot; };
    let byte_1 = bytes.next();
    let byte_2 = bytes.next();
    if byte_1 == byte_2 { // ❌
        // do something
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;呕吼！编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0499]: cannot borrow `bytes` as mutable more than once at a time
  --&amp;gt; src/main.rs:20:18
   |
19 |     let byte_1 = bytes.next();
   |                  ----- first mutable borrow occurs here
20 |     let byte_2 = bytes.next();
   |                  ^^^^^ second mutable borrow occurs here
21 |     if byte_1 == byte_2 {
   |        ------ first borrow later used here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我想我们可以复制每个字节。当我们处理字节时复制当然是可以的，但是如果我们将&lt;code&gt;ByteIter&lt;/code&gt;变成一个通用的切片迭代器，它可以迭代任何&lt;code&gt;&amp;amp;&apos;a [T]&lt;/code&gt;，然后我们可能希望在未来使用它来处理那些复制/克隆成本很高，甚至是不可能复制的类型。好吧，我想我们对此无能为力。所以代码能通过编译，生命周期注释就一定正确吗？&lt;/p&gt;
&lt;p&gt;不，当前的生命周期注释实际上是错误的来源！它特别难以发现，因为错误的生命周期注释被省略了。让我们声明省略的生命周期以更清楚地了解问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct ByteIter&amp;lt;&apos;a&amp;gt; {
    remainder: &amp;amp;&apos;a [u8]
}

impl&amp;lt;&apos;a&amp;gt; ByteIter&amp;lt;&apos;a&amp;gt; {
    fn next&amp;lt;&apos;b&amp;gt;(&amp;amp;&apos;b mut self) -&amp;gt; Option&amp;lt;&amp;amp;&apos;b u8&amp;gt; {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &amp;amp;self.remainder[0];
            self.remainder = &amp;amp;self.remainder[1..];
            Some(byte)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这并没有帮助，我们依然很困惑。这里有一个只有Rust专家知道的奇技淫巧：为你的生命周期注释提供描述性名称。让我们再试一次：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct ByteIter&amp;lt;&apos;remainder&amp;gt; {
    remainder: &amp;amp;&apos;remainder [u8]
}

impl&amp;lt;&apos;remainder&amp;gt; ByteIter&amp;lt;&apos;remainder&amp;gt; {
    fn next&amp;lt;&apos;mut_self&amp;gt;(&amp;amp;&apos;mut_self mut self) -&amp;gt; Option&amp;lt;&amp;amp;&apos;mut_self u8&amp;gt; {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &amp;amp;self.remainder[0];
            self.remainder = &amp;amp;self.remainder[1..];
            Some(byte)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个返回的字节都注释为&lt;code&gt;&apos;mut_self&lt;/code&gt;，但这些字节显然来自&lt;code&gt;&apos;remainder&lt;/code&gt;！让我们修复这个问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct ByteIter&amp;lt;&apos;remainder&amp;gt; {
    remainder: &amp;amp;&apos;remainder [u8]
}

impl&amp;lt;&apos;remainder&amp;gt; ByteIter&amp;lt;&apos;remainder&amp;gt; {
    fn next(&amp;amp;mut self) -&amp;gt; Option&amp;lt;&amp;amp;&apos;remainder u8&amp;gt; {
        if self.remainder.is_empty() {
            None
        } else {
            let byte = &amp;amp;self.remainder[0];
            self.remainder = &amp;amp;self.remainder[1..];
            Some(byte)
        }
    }
}

fn main() {
    let mut bytes = ByteIter { remainder: b&quot;1123&quot; };
    let byte_1 = bytes.next();
    let byte_2 = bytes.next();
    std::mem::drop(bytes); // 我们甚至可以在此处析构迭代器！
    if byte_1 == byte_2 { // ✅
        // do something
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们回过头来看前一个版本的程序，它错的错误非常明显，那为什么Rust会让它通过编译呢？答案很简单：它是内存安全的。&lt;/p&gt;
&lt;p&gt;Rust借用检查器只关心程序中的生命周期注释，它仅使用这些注释来对程序的内存安全性进行静态验证。即使生命周期注释存在语义错误，Rust仍会愉快地编译程序，其结果是使程序受到不必要的限制。&lt;/p&gt;
&lt;p&gt;下面是一个与上面相反的简单示例：Rust的生命周期省略规则在这个实例中恰好是语义正确的，但我们无意中编写了一个非常严格的方法，其中包含我们显式声明的不必要的生命周期注释。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug)]
struct NumRef&amp;lt;&apos;a&amp;gt;(&amp;amp;&apos;a i32);

impl&amp;lt;&apos;a&amp;gt; NumRef&amp;lt;&apos;a&amp;gt; {
    // 我们的结构体以为&apos;a泛型，这是否意味着我需要将self参数也注释为&apos;a？
    // （答案：并不是）
    fn some_method(&amp;amp;&apos;a mut self) {}
}

fn main() {
    let mut num_ref = NumRef(&amp;amp;5);
    num_ref.some_method(); // 可变借用num_ref直到其生命周期结束
    num_ref.some_method(); // ❌
    println!(&quot;{:?}&quot;, num_ref); // ❌
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有结构体以&lt;code&gt;&apos;a&lt;/code&gt;为泛型，我们几乎不会想编写一个接受参数&lt;code&gt;&amp;amp;&apos;a mut self&lt;/code&gt;的方法。我们想告诉Rust的是“此方法将在结构体的整个生命周期内可变地借用该结构体”。实际上，这意味着Rust的借用检查器最多只允许调用一次&lt;code&gt;some_method&lt;/code&gt;，然后该结构体将成为永久的可变借用，并因此无法再次使用。这种用例非常少见，但困惑的初学者很可能写出上面这种代码，而且还能通过编译。解决方法是不添加不必要的显式生命周期注释，交给Rust的生命周期省略规则处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug)]
struct NumRef&amp;lt;&apos;a&amp;gt;(&amp;amp;&apos;a i32);

impl&amp;lt;&apos;a&amp;gt; NumRef&amp;lt;&apos;a&amp;gt; {
    // 不再给mut self添加&apos;a注释
    fn some_method(&amp;amp;mut self) {}

    // 上面的方法实际上是：
    fn some_method_desugared&amp;lt;&apos;b&amp;gt;(&amp;amp;&apos;b mut self){}
}

fn main() {
    let mut num_ref = NumRef(&amp;amp;5);
    num_ref.some_method();
    num_ref.some_method(); // ✅
    println!(&quot;{:?}&quot;, num_ref); // ✅
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rust函数的生命周期省略规则并不适用于所有情况&lt;/li&gt;
&lt;li&gt;Rust并不比你更了解你程序的语义&lt;/li&gt;
&lt;li&gt;给你的生命周期注解起描述性的名字&lt;/li&gt;
&lt;li&gt;注意显式生命生命周期注释的位置以及原因&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6) box指针中的trait对象没有生命周期概念&lt;/h3&gt;
&lt;p&gt;之前我们讨论了Rust的函数生命周期省略规则，当然，Rust也有针对特征对象的省略规则，它们是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果一个trait对象被用作泛型类型的类型参数，那么它的生命周期约束是从包含的类型中推断出来的
&lt;ul&gt;
&lt;li&gt;如果包含的类型有唯一的生命周期约束，那么就使用它&lt;/li&gt;
&lt;li&gt;如果包含的类型有多个生命周期约束，那么必须指定一个显式生命周期约束&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果以上均不适用，则
&lt;ul&gt;
&lt;li&gt;如果该trait由唯一的一个生命周期约束定义，则使用该约束&lt;/li&gt;
&lt;li&gt;如果&lt;code&gt;&apos;static&lt;/code&gt;用于任何生命周期约束，则使用&lt;code&gt;&apos;static&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;如果trait没有生命周期约束，那么它的生命周期是通过表达式推断出来的，并且在表达式之外是&lt;code&gt;&apos;static&lt;/code&gt;的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然上面这些规则听起来非常复杂，但是可以简单的概括为“&lt;em&gt;trait对象的生命周期约束是从上下文中推断出来的&lt;/em&gt;”。在查看了一些示例之后，我们将意识到生命周期约束的推断是非常直观的，因此我们无需记住上面这些形式化的规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::cell::Ref;

trait Trait {}

// 省略
type T1 = Box&amp;lt;dyn Trait&amp;gt;;
//展开，Box&amp;lt;T&amp;gt;对T没有生命周期约束，因此推断为&apos;static
type T2 = Box&amp;lt;dyn Trait + &apos;static&amp;gt;;

// 省略
impl dyn Trait {}
// 展开为
impl dyn Trait + &apos;static {}

// 省略
type T3&amp;lt;&apos;a&amp;gt; = &amp;amp;&apos;a dyn Trait;
// 展开，&amp;amp;&apos;a T 要求 T: &apos;a，因此推断为&apos;a
type T4&amp;lt;&apos;a&amp;gt; = &amp;amp;&apos;a (dyn Trait + &apos;a);

// 省略
type T5&amp;lt;&apos;a&amp;gt; = Ref&amp;lt;&apos;a, dyn Trait&amp;gt;;
// 展开，Ref&amp;lt;&apos;a, T&amp;gt; 要求 T: &apos;a，因此推断为&apos;a
type T6&amp;lt;&apos;a&amp;gt; = Ref&amp;lt;&apos;a, dyn Trait + &apos;a&amp;gt;;

trait GenericTrait&amp;lt;&apos;a&amp;gt;: &apos;a {}

// 省略
type T7&amp;lt;&apos;a&amp;gt; = Box&amp;lt;dyn GenericTrait&amp;lt;&apos;a&amp;gt;&amp;gt;;
// 展开为
type T8&amp;lt;&apos;a&amp;gt; = Box&amp;lt;dyn GenericTrait&amp;lt;&apos;a&amp;gt; + &apos;a&amp;gt;;

// 省略
impl&amp;lt;&apos;a&amp;gt; dyn GenericTrait&amp;lt;&apos;a&amp;gt; {}
// 展开为
impl&amp;lt;&apos;a&amp;gt; dyn GenericTrait&amp;lt;&apos;a&amp;gt; + &apos;a {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实现trait的具体类型可以有引用，因此它们也有生命周期约束，所以它们对应的trait对象也具有生命周期约束。你也可以直接为显然具有生命周期约束的引用实现trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;trait Trait {}

struct Struct {}
struct Ref&amp;lt;&apos;a, T&amp;gt;(&amp;amp;&apos;a T);

impl Trait for Struct {}
impl Trait for &amp;amp;Struct {} // 直接为引用类型实现Trait
impl&amp;lt;&apos;a, T&amp;gt; Trait for Ref&amp;lt;&apos;a, T&amp;gt; {} // 为包含引用的类型实现
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无论如何，这值得一读，因为当初学者将函数从使用trait对象重构为泛型，或从泛型到使用trait对象时，常常会感到困惑。以这个程序为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::fmt::Display;

fn dynamic_thread_print(t: Box&amp;lt;dyn Display + Send&amp;gt;) {
    std::thread::spawn(move || {
        println!(&quot;{}&quot;, t);
    }).join();
}

fn static_thread_print&amp;lt;T: Display + Send&amp;gt;(t: T) { // ❌
    std::thread::spawn(move || {
        println!(&quot;{}&quot;, t);
    }).join();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码将抛出下面的编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0310]: the parameter type `T` may not live long enough
  --&amp;gt; src/lib.rs:10:5
   |
9  | fn static_thread_print&amp;lt;T: Display + Send&amp;gt;(t: T) {
   |                        -- help: consider adding an explicit lifetime bound...: `T: &apos;static +`
10 |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^
   |
note: ...so that the type `[closure@src/lib.rs:10:24: 12:6 t:T]` will meet its required lifetime bounds
  --&amp;gt; src/lib.rs:10:5
   |
10 |     std::thread::spawn(move || {
   |     ^^^^^^^^^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好，编译期已经告诉我们如何修复问题了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::fmt::Display;

fn dynamic_thread_print(t: Box&amp;lt;dyn Display + Send&amp;gt;) {
    std::thread::spawn(move || {
        println!(&quot;{}&quot;, t);
    }).join();
}

fn static_thread_print&amp;lt;T: Display + Send + &apos;static&amp;gt;(t: T) { // ✅
    std::thread::spawn(move || {
        println!(&quot;{}&quot;, t);
    }).join();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以通过编译了，不过这两个函数放在一起看起来很奇怪，为什么第二个函数需要&lt;code&gt;&apos;static&lt;/code&gt;约束的&lt;code&gt;T&lt;/code&gt;，而第一个函数就不需要？这是一个棘手的问题。应用生命周期省略规则，Rust会自动在第一个函数中推断出&lt;code&gt;&apos;static&lt;/code&gt;约束，因此两者实际上都有 &lt;code&gt;&apos;static&lt;/code&gt;约束。这是Rust编译器实际看到的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::fmt::Display;

fn dynamic_thread_print(t: Box&amp;lt;dyn Display + Send + &apos;static&amp;gt;) {
    std::thread::spawn(move || {
        println!(&quot;{}&quot;, t);
    }).join();
}

fn static_thread_print&amp;lt;T: Display + Send + &apos;static&amp;gt;(t: T) {
    std::thread::spawn(move || {
        println!(&quot;{}&quot;, t);
    }).join();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有trait对象都具有某些推断的默认生命周期约束&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;7) 编译器的错误提示会告诉我如何修复程序&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;误解的推论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rust对trait对象的生命周期省略规则总是正确的&lt;/li&gt;
&lt;li&gt;Rust比我更了解我程序的语义&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种误解结合了前面两种误解，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::fmt::Display;

fn box_displayable&amp;lt;T: Display&amp;gt;(t: T) -&amp;gt; Box&amp;lt;dyn Display&amp;gt; { // ❌
    Box::new(t)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抛出的错误为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0310]: the parameter type `T` may not live long enough
 --&amp;gt; src/lib.rs:4:5
  |
3 | fn box_displayable&amp;lt;T: Display&amp;gt;(t: T) -&amp;gt; Box&amp;lt;dyn Display&amp;gt; {
  |                    -- help: consider adding an explicit lifetime bound...: `T: &apos;static +`
4 |     Box::new(t)
  |     ^^^^^^^^^^^
  |
note: ...so that the type `T` will meet its required lifetime bounds
 --&amp;gt; src/lib.rs:4:5
  |
4 |     Box::new(t)
  |     ^^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们按照编译器告诉我们方式来修复问题，它将我们boxed trait的约束自动推断为&lt;code&gt;&apos;static&lt;/code&gt;，而它推荐的修复方式就基于这个未声明的事实，虽然编译器没有告诉我们，但无需介意：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::fmt::Display;

fn box_displayable&amp;lt;T: Display + &apos;static&amp;gt;(t: T) -&amp;gt; Box&amp;lt;dyn Display&amp;gt; { // ✅
    Box::new(t)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以程序现在可以通过编译了……但这真的是我们想要的吗？可能是也可能不是。编译器没有提到其他修复，但下面这种也是合适的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::fmt::Display;

fn box_displayable&amp;lt;&apos;a, T: Display + &apos;a&amp;gt;(t: T) -&amp;gt; Box&amp;lt;dyn Display + &apos;a&amp;gt; { // ✅
    Box::new(t)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此函数接受参数的范围比前面的版本更广！但这会让它变得更好用吗？不一定，要看我们程序的要求和约束。这个例子有点抽象，所以让我们看一个更简单且明显的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn return_first(a: &amp;amp;str, b: &amp;amp;str) -&amp;gt; &amp;amp;str { // ❌
    a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;抛出的错误为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0106]: missing lifetime specifier
 --&amp;gt; src/lib.rs:1:38
  |
1 | fn return_first(a: &amp;amp;str, b: &amp;amp;str) -&amp;gt; &amp;amp;str {
  |                    ----     ----     ^ expected named lifetime parameter
  |
  = help: this function&apos;s return type contains a borrowed value, but the signature does not say whether it is borrowed from `a` or `b`
help: consider introducing a named lifetime parameter
  |
1 | fn return_first&amp;lt;&apos;a&amp;gt;(a: &amp;amp;&apos;a str, b: &amp;amp;&apos;a str) -&amp;gt; &amp;amp;&apos;a str {
  |                ^^^^    ^^^^^^^     ^^^^^^^     ^^^
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;错误消息建议使用相同的生命周期注释输入和输出。如果我们这样做，程序的确会通过编译，但这个函数会过度限制返回类型。我们真正想要的是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn return_first&amp;lt;&apos;a&amp;gt;(a: &amp;amp;&apos;a str, b: &amp;amp;str) -&amp;gt; &amp;amp;&apos;a str { // ✅
    a
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rust对trait对象的生命周期省略规则并不总是适用于所有情况&lt;/li&gt;
&lt;li&gt;Rust并不比你更了解你程序的语义&lt;/li&gt;
&lt;li&gt;Rust编译器的错误消息修复建议能够使你的程序通过编译，但这不一定是使你的程序通过编译的最合适的修复方式&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;8) 生命周期可以在运行时增长和收缩&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;误解的推论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;容器类型可以在运行时切换引用，以改变它们的生命周期&lt;/li&gt;
&lt;li&gt;Rust借用检查器会对程序进行高级控制流分析&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这不能通过编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Has&amp;lt;&apos;lifetime&amp;gt; {
    lifetime: &amp;amp;&apos;lifetime str,
}

fn main() {
    let long = String::from(&quot;long&quot;);
    let mut has = Has { lifetime: &amp;amp;long };
    assert_eq!(has.lifetime, &quot;long&quot;);

    {
        let short = String::from(&quot;short&quot;);
        // “切换”到short生命周期
        has.lifetime = &amp;amp;short;
        assert_eq!(has.lifetime, &quot;short&quot;);

        // “切换回”long生命周期（但实际上并没有）
        has.lifetime = &amp;amp;long;
        assert_eq!(has.lifetime, &quot;long&quot;);
        // `short`在此处析构
    }

    assert_eq!(has.lifetime, &quot;long&quot;); // ❌ - `short`在析构之后仍“被借用”
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码抛出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0597]: `short` does not live long enough
  --&amp;gt; src/main.rs:11:24
   |
11 |         has.lifetime = &amp;amp;short;
   |                        ^^^^^^ borrowed value does not live long enough
...
15 |     }
   |     - `short` dropped here while still borrowed
16 |     assert_eq!(has.lifetime, &quot;long&quot;);
   |     --------------------------------- borrow later used here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面这段代码也不能通过编译，且与上面的代码抛出的错误完全一致：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Has&amp;lt;&apos;lifetime&amp;gt; {
    lifetime: &amp;amp;&apos;lifetime str,
}

fn main() {
    let long = String::from(&quot;long&quot;);
    let mut has = Has { lifetime: &amp;amp;long };
    assert_eq!(has.lifetime, &quot;long&quot;);

    // 虽然这个代码块从不执行
    if false {
        let short = String::from(&quot;short&quot;);
        // “切换”到short生命周期
        has.lifetime = &amp;amp;short;
        assert_eq!(has.lifetime, &quot;short&quot;);

        // “切换回”long生命周期（但实际上并没有）
        has.lifetime = &amp;amp;long;
        assert_eq!(has.lifetime, &quot;long&quot;);
        // `short`在此处析构
    }

    assert_eq!(has.lifetime, &quot;long&quot;); // ❌ - `short`在析构之后仍“被借用”
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust中的生命周期必须在编译时静态验证，借用检查器只会进行最基本的控制流分析，所以它假设&lt;code&gt;if-else&lt;/code&gt;语句中的每个块和&lt;code&gt;match&lt;/code&gt;语句中的每个匹配分支都可以被执行，然后为变量选择可能的最短的生命周期。一旦一个变量被一个生命周期约束，就将&lt;em&gt;永远&lt;/em&gt;受到该生命周期的约束。变量的生命周期只能收缩，而所有的收缩都是在编译时决定的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生命周期会在编译期静态验证&lt;/li&gt;
&lt;li&gt;生命周期不能在运行时以任何方式增长、收缩或改变&lt;/li&gt;
&lt;li&gt;Rust借用检查器将始终为变量选择尽可能短的生命周期，并会假设所有分支代码都能够被执行&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;9) 将可变引用降级为共享引用是安全的&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;误解的推论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重新借用一个引用将结束现有的生命周期并开始一个新的生命周期&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;您可以将可变引用传给期望共享引用的函数，因为Rust会隐式地重新借用这个可变引用并将其变为不可变的共享引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn takes_shared_ref(n: &amp;amp;i32) {}

fn main() {
    let mut a = 10;
    takes_shared_ref(&amp;amp;mut a); // ✅
    takes_shared_ref(&amp;amp;*(&amp;amp;mut a)); // 上一行实际上执行的代码
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直觉上这是有道理的，因为将一个可变引用重新借用为不可变的应该不会造成什么影响，对吧？令人惊讶的是，实际上并非没有影响，来看下面的程序就无法通过编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let mut a = 10;
    let b: &amp;amp;i32 = &amp;amp;*(&amp;amp;mut a); // re-borrowed as immutable
    let c: &amp;amp;i32 = &amp;amp;a;
    dbg!(b, c); // ❌
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码抛出如下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0502]: cannot borrow `a` as immutable because it is also borrowed as mutable
 --&amp;gt; src/main.rs:4:19
  |
3 |     let b: &amp;amp;i32 = &amp;amp;*(&amp;amp;mut a);
  |                     -------- mutable borrow occurs here
4 |     let c: &amp;amp;i32 = &amp;amp;a;
  |                   ^^ immutable borrow occurs here
5 |     dbg!(b, c);
  |          - mutable borrow later used here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们确实进行了可变借用，但它会立即无条件地重新进行不可变借用，然后被丢弃。为什么Rust将不可变的重新借用，仍然视为可变借用的独占生命周期？虽然在上面的特定示例中没有问题，但允许将可变引用降级为共享引用的功能，确实会引入潜在的内存安全问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::sync::Mutex;

struct Struct {
    mutex: Mutex&amp;lt;String&amp;gt;
}

impl Struct {
    // 将可变self降级为共享str
    fn get_string(&amp;amp;mut self) -&amp;gt; &amp;amp;str {
        self.mutex.get_mut().unwrap()
    }
    fn mutate_string(&amp;amp;self) {
        // 如果Rust允许将可变引用降级为共享引用
        // 则下面这行代码将使从get_string方法返回的任何共享引用无效
        *self.mutex.lock().unwrap() = &quot;surprise!&quot;.to_owned();
    }
}

fn main() {
    let mut s = Struct {
        mutex: Mutex::new(&quot;string&quot;.to_owned())
    };
    let str_ref = s.get_string(); // 可变引用降级为共享引用
    s.mutate_string(); // str_ref将变得无效，产生悬空指针
    dbg!(str_ref); // ❌ - 如我们所料
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的要点是，当你将可变引用重新借用为共享引用时，你不只得到了共享引用还掉进了一个大坑：重新借用还延长了可变引用的生命周期，即使可变引用自身已被析构。使用重新借用的共享引用非常难，因为它不仅不可变，还不能被其他任何共享引用共享。重新借用的共享引用集合了可变引用和共享引用的所有缺点，且并没有得到两者的任一的优点。我认为将可变引用重新借用为共享引用应该被视为Rust反面模式。意识到这种反面模式很重要，这样当您看到这样的代码时就可以很容易地发现它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 将可变T降级为共享T
fn some_function&amp;lt;T&amp;gt;(some_arg: &amp;amp;mut T) -&amp;gt; &amp;amp;T;

struct Struct;

impl Struct {
    // 将可变self降级为共享self
    fn some_method(&amp;amp;mut self) -&amp;gt; &amp;amp;Self;

    // 将可变self降级为共享T
    fn other_method(&amp;amp;mut self) -&amp;gt; &amp;amp;T;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即使你避免了在函数和方法签名中的重新借用，Rust仍然会自动进行隐式重新借用，所以我们很容易在没有意识到的情况下遇到这种问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::collections::HashMap;

type PlayerID = i32;

#[derive(Debug, Default)]
struct Player {
    score: i32,
}

fn start_game(player_a: PlayerID, player_b: PlayerID, server: &amp;amp;mut HashMap&amp;lt;PlayerID, Player&amp;gt;) {
    // 从服务器获取玩家，如果玩家不存在则创建或插入新玩家
    let player_a: &amp;amp;Player = server.entry(player_a).or_default();
    let player_b: &amp;amp;Player = server.entry(player_b).or_default();

    // do something with players
    dbg!(player_a, player_b); // ❌
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码无法通过编译。&lt;code&gt;or_default()&lt;/code&gt;将返回一个&lt;code&gt;&amp;amp;mut Player&lt;/code&gt;，我们的显式类型注释将使Rust隐式地重新借用为&lt;code&gt;&amp;amp;Player&lt;/code&gt;。所以，正确的写法应该是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::collections::HashMap;

type PlayerID = i32;

#[derive(Debug, Default)]
struct Player {
    score: i32,
}

fn start_game(player_a: PlayerID, player_b: PlayerID, server: &amp;amp;mut HashMap&amp;lt;PlayerID, Player&amp;gt;) {
    // 丢弃返回的可变Player引用，反正我们也不能同时使用它们
    server.entry(player_a).or_default();
    server.entry(player_b).or_default();

    // 重新获取players，这次直接获取其不可变引用，且不带有啊任何隐式重新借用
    let player_a = server.get(&amp;amp;player_a);
    let player_b = server.get(&amp;amp;player_b);

    // do something with players
    dbg!(player_a, player_b); // ✅
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这么写有点奇怪还很笨拙，但这是我们在内存安全问题上必要的牺牲。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;尽量不要将可变引用重新借用为共享引用，否则你可能会遇到大麻烦&lt;/li&gt;
&lt;li&gt;对可变引用进行重新借用并不会结束它的生命周期，即使该引用已被删除&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;10) 闭包遵循与函数相同的生命周期省略规则&lt;/h3&gt;
&lt;p&gt;这与其说是一种误解，不如说是Rust的陷阱。&lt;/p&gt;
&lt;p&gt;尽管闭包也是函数，但并不遵循与函数相同的生命周期省略规则。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn function(x: &amp;amp;i32) -&amp;gt; &amp;amp;i32 {
    x
}

fn main() {
    let closure = |x: &amp;amp;i32| x; // ❌
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: lifetime may not live long enough
 --&amp;gt; src/main.rs:6:29
  |
6 |     let closure = |x: &amp;amp;i32| x;
  |                       -   - ^ returning this value requires that `&apos;1` must outlive `&apos;2`
  |                       |   |
  |                       |   return type of closure is &amp;amp;&apos;2 i32
  |                       let&apos;s call the lifetime of this reference `&apos;1`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补全上面代码省略的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 输入的生命周期应用于输出的生命周期
fn function&amp;lt;&apos;a&amp;gt;(x: &amp;amp;&apos;a i32) -&amp;gt; &amp;amp;&apos;a i32 {
    x
}

fn main() {
    // 输入和输出分别得到了各自不同的生命周期
    let closure = for&amp;lt;&apos;a, &apos;b&amp;gt; |x: &amp;amp;&apos;a i32| -&amp;gt; &amp;amp;&apos;b i32 { x };
    // 注意：上面一行代码的语法并不合法，我们只是使用它说明情况
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种差异没有什么合理的解释。闭包最初是使用与函数不同的类型推断语义实现的，不过我们可能要永远这么用下去了，因为如果现在统一两者的实现将是一个破坏性的更新。那么如何显式地注释闭包呢？我们的选择包括：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    // 若转换为trait对象，占用空间会变得固定，于是产生编译错误
    let identity: dyn Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32 = |x: &amp;amp;i32| x;

    // 可以分配在堆上，但感觉很笨重
    let identity: Box&amp;lt;dyn Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32&amp;gt; = Box::new(|x: &amp;amp;i32| x);

    // 可以跳过分配，只创建一个静态引用
    let identity: &amp;amp;dyn Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32 = &amp;amp;|x: &amp;amp;i32| x;

    // 上面这一行实际上是：
    let identity: &amp;amp;&apos;static (dyn for&amp;lt;&apos;a&amp;gt; Fn(&amp;amp;&apos;a i32) -&amp;gt; &amp;amp;&apos;a i32 + &apos;static) = &amp;amp;|x: &amp;amp;i32| -&amp;gt; &amp;amp;i32 { x };

    // 这么写是最理想的，但这样的语法是无效的
    let identity: impl Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32 = |x: &amp;amp;i32| x;

    // 这样写也很理想，但依然是无效语法
    let identity = for&amp;lt;&apos;a&amp;gt; |x: &amp;amp;&apos;a i32| -&amp;gt; &amp;amp;&apos;a i32 { x };

    // `impl trait`只能用在在函数签名的返回值部分
    fn return_identity() -&amp;gt; impl Fn(&amp;amp;i32) -&amp;gt; &amp;amp;i32 {
        |x| x
    }
    let identity = return_identity();

    // 上面版本的泛型化写法
    fn annotate&amp;lt;T, F&amp;gt;(f: F) -&amp;gt; F where F: Fn(&amp;amp;T) -&amp;gt; &amp;amp;T {
        f
    }
    let identity = annotate(|x: &amp;amp;i32| x);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你应该已经从上面的示例中注意到了，当闭包类型用作trait约束时，它们确实应用了通常的函数生命周期省略规则。&lt;/p&gt;
&lt;p&gt;此处并没有什么教训或启发，Rust现在就是这样。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;主要结论&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个编程语言都有些陷阱🤷&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;T&lt;/code&gt;是&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;的超集&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;T&lt;/code&gt;和&lt;code&gt;&amp;amp;mut T&lt;/code&gt;是互斥集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T: &apos;static&lt;/code&gt;应读作“&lt;em&gt;&lt;code&gt;T&lt;/code&gt;受到&lt;code&gt;&apos;static&lt;/code&gt;生命周期约束&lt;/em&gt;”&lt;/li&gt;
&lt;li&gt;如果有&lt;code&gt;T: &apos;static&lt;/code&gt;，则&lt;code&gt;T&lt;/code&gt;可以是一个具有&lt;code&gt;&apos;static&lt;/code&gt;生命周期的借用类型 &lt;em&gt;或&lt;/em&gt; 一个自有类型&lt;/li&gt;
&lt;li&gt;因为&lt;code&gt;T: &apos;static&lt;/code&gt;包括自有类型，这意味着&lt;code&gt;T&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;可以在运行时被动态的分配&lt;/li&gt;
&lt;li&gt;不需要在程序的生命周期中始终有效&lt;/li&gt;
&lt;li&gt;可以安全、自由的修改&lt;/li&gt;
&lt;li&gt;可以在运行时动态的析构&lt;/li&gt;
&lt;li&gt;可以有不同持续时间的生命周期&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T: &apos;a&lt;/code&gt;比&lt;code&gt;&amp;amp;&apos;a T&lt;/code&gt;更通用也更灵活&lt;/li&gt;
&lt;li&gt;&lt;code&gt;T: &apos;a&lt;/code&gt;接受自有类型、包含引用的自有类型以及引用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;&amp;amp;&apos;a T&lt;/code&gt;只接受引用&lt;/li&gt;
&lt;li&gt;如果有&lt;code&gt;T: &apos;static&lt;/code&gt;，那么&lt;code&gt;T: &apos;a&lt;/code&gt;，因为&lt;code&gt;&apos;static&lt;/code&gt; &amp;gt;= 所有的&lt;code&gt;&apos;a&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;几乎所有的Rust代码都是泛型代码，到处都有被省略的生命周期注释。&lt;/li&gt;
&lt;li&gt;Rust函数的生命周期省略规则并不适用于所有情况&lt;/li&gt;
&lt;li&gt;Rust并不比你更了解你程序的语义&lt;/li&gt;
&lt;li&gt;给你的生命周期注解起描述性的名字&lt;/li&gt;
&lt;li&gt;注意显式生命生命周期注释的位置以及原因&lt;/li&gt;
&lt;li&gt;所有trait对象都具有某些推断的默认生命周期约束&lt;/li&gt;
&lt;li&gt;Rust编译器的错误消息修复建议能够使你的程序通过编译，但这不一定是使你的程序通过编译的最合适的修复方式&lt;/li&gt;
&lt;li&gt;生命周期会在编译期静态验证&lt;/li&gt;
&lt;li&gt;生命周期不能在运行时以任何方式增长、收缩或改变&lt;/li&gt;
&lt;li&gt;Rust借用检查器将始终为变量选择尽可能短的生命周期，并会假设所有分支代码都能够被执行&lt;/li&gt;
&lt;li&gt;尽量不要将可变引用重新借用为共享引用，否则你可能会遇到大麻烦&lt;/li&gt;
&lt;li&gt;对可变引用进行重新借用并不会结束它的生命周期，即使该引用已被删除&lt;/li&gt;
&lt;li&gt;每个编程语言都有些陷阱🤷&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;讨论&lt;/h2&gt;
&lt;p&gt;讨论这篇文章的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/learnrust/comments/gmrcrq/common_rust_lifetime_misconceptions/&quot;&gt;learnrust subreddit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://users.rust-lang.org/t/blog-post-common-rust-lifetime-misconceptions/42950&quot;&gt;official Rust users forum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://twitter.com/pretzelhammer/status/1263505856903163910&quot;&gt;Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.reddit.com/r/rust/comments/golrsx/common_rust_lifetime_misconceptions/&quot;&gt;rust subreddit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://news.ycombinator.com/item?id=23279731&quot;&gt;Hackernews&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pretzelhammer/rust-blog/discussions&quot;&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;订阅&lt;/h2&gt;
&lt;p&gt;订阅下一篇文章的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://twitter.com/pretzelhammer&quot;&gt;Following pretzelhammer on Twitter&lt;/a&gt; or&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pretzelhammer/rust-blog/releases.atom&quot;&gt;Subscribing to this repo&apos;s release RSS feed&lt;/a&gt; or&lt;/li&gt;
&lt;li&gt;关注本repo的release (click &lt;code&gt;Watch&lt;/code&gt; -&amp;gt; click &lt;code&gt;Custom&lt;/code&gt; -&amp;gt; select &lt;code&gt;Releases&lt;/code&gt; -&amp;gt; click &lt;code&gt;Apply&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;深入阅读&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;./sizedness-in-rust.md&quot;&gt;Sizedness in Rust&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./tour-of-rusts-standard-library-traits.md&quot;&gt;Tour of Rust&apos;s Standard Library Traits&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./restful-api-in-sync-and-async-rust.md&quot;&gt;RESTful API in Sync &amp;amp; Async Rust&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./learning-rust-in-2020.md&quot;&gt;Learning Rust in 2020&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;./too-many-brainfuck-compilers.md&quot;&gt;Learn Assembly with Entirely Too Many Brainfuck Compilers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>析构2022 - 人可不能太害怕冬天</title><link>https://zlotus.github.io/2022/12/11/drop-2022/</link><guid isPermaLink="true">https://zlotus.github.io/2022/12/11/drop-2022/</guid><description>这不是疫情三年放开了么，昨天采访了一下老西医，汇总成文。</description><pubDate>Sun, 11 Dec 2022 18:53:00 GMT</pubDate><content:encoded>&lt;p&gt;为啥现在写年终总结呢？因为这不是放开了嘛，昨天连滚带爬的采访了战地一线的防护经验丰富的老西医……&lt;/p&gt;
&lt;h2&gt;1 年终总结&lt;/h2&gt;
&lt;p&gt;先写个总结意思一下，今年干的人事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;跟着教程写了个KV数据库，基本了解了Rust的async是怎么运作的；&lt;/li&gt;
&lt;li&gt;双十一从闲鱼上买了个优惠券，九百多拿下了16G版本的Rock-5B，四舍五入九百块买了个电脑你说值不值；&lt;/li&gt;
&lt;li&gt;把域名和SSL证书彻底弄好了，老老实实把备案什么都做了，买了个最便宜的阿里云做流量中转，延迟非常低，感觉也很值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;非人事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;朝枪夕拾，重拾CSGO，虽然快四十了，端起那杆AWP，还是年轻时候的样子，欢迎kennyS来附体。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2 采访老西医&lt;/h2&gt;
&lt;p&gt;最近不是放开了么，得自己负责自己的健康了，刚好最近要出差，昨晚突击采访了某美帝排名前十的生物化学系的W教授，精华汇总成文如下。&lt;/p&gt;
&lt;h3&gt;2.1 tl;dr&lt;/h3&gt;
&lt;p&gt;&amp;lt;font color=&quot;red&quot; size=&quot;5em&quot;&amp;gt;&amp;lt;b&amp;gt;管好自己带好口罩严防飞沫，把医疗资源让给需要的人。&amp;lt;/b&amp;gt;&amp;lt;/font&amp;gt;&lt;/p&gt;
&lt;h3&gt;2.2 采访详情&lt;/h3&gt;
&lt;p&gt;教授首先就美帝三年个人防护经验做了简要介绍。教授有三针腺病毒疫苗护体，三年一共中招一次，是因为孩子在学校没防住，在家被传染。教授在感染的情况下紧急调整防护策略，单人单间三餐由夫人送到门口，不超过十天痊愈，无后遗症，夫人三年保持依然零中招。教授强调，&lt;strong&gt;飞沫&lt;/strong&gt;是危害最大的传播途径，而&lt;strong&gt;口罩&lt;/strong&gt;是最有效的防护手段，当然，独处一个房间关上门也可以隔断，但是只要出门必须戴口罩，尤其是公共场所，一定要带医用N95。&lt;/p&gt;
&lt;p&gt;小结：如果不幸中招，立即单间隔离，按症状吃药，三餐送到门口（人离开再开门交接，介意的话吃完的餐具可以放门口等半个小时再收，虽然教授没这样做但建议这样做）；备一个&lt;strong&gt;血氧仪&lt;/strong&gt;（淘宝百十块钱），若降至90就去医院，否则推荐在家别恐慌别折腾，把医疗资源留给更需要的人。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;接下来，教授介绍了三年以来生活方式上的改变。首先就是，戴口罩，只要不是在家，只要不是独处，就带上口罩，把每个人都想象成传染源。因此，即使在办公室、食堂餐厅都应该佩戴口罩，有条件的话全程N95。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;办公室除了喝水不摘口罩；&lt;/li&gt;
&lt;li&gt;食堂餐厅打饭之后一个人找个地方单独就餐，找不到就呆车里；&lt;/li&gt;
&lt;li&gt;不主动去人多的地方，实在没办法就全程N95不摘，回家洗澡；&lt;/li&gt;
&lt;li&gt;勤洗手勤消毒。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;教授认为国内这三年来已经相当幸福了，基本躲过了病毒最具破坏力的时段，之后在宏观层面最重要的就是&lt;strong&gt;防挤兑&lt;/strong&gt;，微观层面最重要的是&lt;strong&gt;建立新的生活习惯&lt;/strong&gt;，毕竟就算是美帝现在也没有完全躺平，坚决不能浪。&lt;/p&gt;
&lt;p&gt;教授回忆道，在刚开始那一阵子，他们连厕所都不敢用，一日两餐车里解决少喝水，回家再方便。有别的教授感染了老版本的病毒，歇菜了半年，基本废了；有年轻人反复感染，有的完了有的没事，因人而异，不要赌自己天赋异禀。后来人类了解也多了病毒也弱了，起码上厕所什么的都敢去了。孩子在学校是真的没办法，这种时候就要将风险最小化，即使在同一个屋檐下也完全可以做到让家人全身而退。&lt;/p&gt;
&lt;p&gt;小结：&lt;strong&gt;戴口罩&lt;/strong&gt;，能N95就N95，只要不是封闭的单间独处，就一直带着别摘，核心就是防止飞沫传播。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;之后，我就近期比较关心的出差问题，尤其是乘坐公共交通工具，如飞机，询问了教授。教授自己三年只做过一次公共交通工具，但是理论上，只要防住飞沫问题应该不大，推荐全程带口罩不摘（不吃喝不上厕所），可以穿雨披，到目的地先洗澡即可，眼罩或面罩有条件可选配。&lt;/p&gt;
&lt;p&gt;最后，教授强调了，这玩意绝不是流感那么简单的东西，流感只会破坏上呼吸道的细胞，而这玩意会结合ACE2受体搞出很多花样。教授就自己生病的感受猜测，这玩意结合ACE2受体后会影响血流通量甚至能够间接影响心率，对肉体造成真实伤害，因此决不能掉以轻心。管不了别人就做好自己的防护，降低自己中招的概率，还能把医疗资源留给更需要的人。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我再提一点需要注意的地方，药不能只看名字，吃之前一定要看成分，比如対乙酰氨基酚这种东西可能在一些复方银翘片、抗病毒感冒药中出现，如果多种药混吃容易过量。不必太过恐慌，对症吃药，一次吃一种。&lt;/p&gt;
&lt;p&gt;EOF&lt;/p&gt;
</content:encoded></item><item><title>第一朵阿里云</title><link>https://zlotus.github.io/2022/12/03/first-aliyun-ecs/</link><guid isPermaLink="true">https://zlotus.github.io/2022/12/03/first-aliyun-ecs/</guid><description>五年最小的阿里云配置，买在了成都，不到500</description><pubDate>Sat, 03 Dec 2022 17:32:00 GMT</pubDate><content:encoded>&lt;p&gt;最近希望有一个私人的流量中转站，即使在办公室也能访问家里的计算和存储，低延迟低流量低配置的乞丐VPS就可以。&lt;/p&gt;
&lt;p&gt;抠门的我挑了半天，还是选择了大厂，阿里云成都分云，这次不用红帽系了，因为最近一直在Debian、Ubuntu和Armbian。&lt;/p&gt;
&lt;h2&gt;初始化&lt;/h2&gt;
&lt;p&gt;首次登录好习惯改密码（为了方便，应该把&lt;code&gt;zsh&lt;/code&gt;、&lt;code&gt;git&lt;/code&gt;、&lt;code&gt;oh-my-zsh&lt;/code&gt;、&lt;code&gt;vim&lt;/code&gt;、&lt;code&gt;tmux&lt;/code&gt;、&lt;code&gt;aria2c&lt;/code&gt;装上。）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# VPS上改密码
passwd
# 修改主机名，阿里云默认的太丑了
sudo hostnamectl set-hostname aliyun.mydomain.tech
# 电脑上上传秘钥进行ssh免密登录，要指定秘钥则`ssh-copy-id -i ~/.ssh/id_rsa.pub root@4*.*.*.*`
ssh-copy-id root@4*.*.*.*

sudo apt update &amp;amp;&amp;amp; sudo apt dist-upgrade -y
sudo apt install build-essential curl file git -y
sudo apt install zsh git vim tmux aria2
chsh -s $(which zsh)
sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;

# 安装oh-my-tmux，github不通时，可以用gitclone先顶一下
cd ~
git clone https://gitclone.com/github.com/gpakosz/.tmux.git
ln -s -f .tmux/.tmux.conf
cp .tmux/.tmux.conf.local .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://wsgzao.github.io/post/ssh/&quot;&gt;将ssh设置为无法用密码登录（只接受key pair认证）&lt;/a&gt;，顺便调整ssh超时时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/ssh/sshd_config 中：
# 禁止密码认证
PasswordAuthentication no
# 调整ssh会话一次存活60秒，自动重连60次
ClientAliveInterval 60
ClientAliveCountMax 60

# 重启sshd服务：
sudo systemctl restart ssh.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开防火墙策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment ssh
sudo ufw allow 6800/tcp comment aria2c
sudo ufw allow 2017/tcp comment v2raya
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;**注意：**若端口已经打开，使用&lt;code&gt;nc&lt;/code&gt;检测端口发现公网IP（即&lt;code&gt;4x.x.x.x&lt;/code&gt;）的端口不通，而内网IP（即&lt;code&gt;172.x.x.x&lt;/code&gt;）端口正常。则需要在&lt;code&gt;云服务器ESC&lt;/code&gt;-&amp;gt;&lt;code&gt;网络与安全&lt;/code&gt;-&amp;gt;&lt;code&gt;安全组&lt;/code&gt;里添加端口。&lt;/p&gt;
&lt;p&gt;开启虚拟内存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 检查虚拟内存十分开启：
free -m
# 分配虚拟内存文件的空间，大小为 bs*count
sudo dd if=/dev/zero of=/swapfile bs=1024 count=1048576
# 设置文件权限
chmod 600 /swapfile
# 制作虚拟内存分区
mkswap /swapfile
# 激活虚拟内存
swapon /swapfile
# 此时检查就会有虚拟内存了
free -m
# 要一直使用的话，更改开机挂载虚拟内存
vim /etc/fstab
/swapfile swap swap defaults 0 0

# 检测swappiness值，即系统使用交换空间的优先级，默认为60即可，刚装好的阿里云为0
cat /proc/sys/vm/swappiness
# 临时修改
sysctl vm.swappiness=60
# 永久修改
vim /etc/sysctl.conf
vm.swappiness=60
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装科学冲浪&lt;/h2&gt;
&lt;p&gt;安装核心，如果&lt;code&gt;install-release.sh&lt;/code&gt;不能下载，可以直接浏览器打开复制粘贴，然后&lt;code&gt;chmod +x install-release.sh&lt;/code&gt;，运行即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装或更新核心
sudo bash -c &quot;$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)&quot; @ install
# 仅更新核心
sudo bash -c &quot;$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)&quot; @ install-geodata
# 卸载核心
sudo bash -c &quot;$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)&quot; @ remove

sudo systemctl status xray.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装UI，直接在&lt;a href=&quot;https://github.com/v2rayA/v2rayA/releases&quot;&gt;release&lt;/a&gt;中挑了一个&lt;a href=&quot;https://github.com/v2rayA/v2rayA/releases/download/v1.5.9.1698.1/installer_debian_amd64_1.5.9.1698.1.deb&quot;&gt;arm64的包&lt;/a&gt;。如果实在下不下来，就&lt;code&gt;scp ./installer_debian_amd64_1.5.9.1698.1.deb root@47.109.18.231:./&lt;/code&gt;传一个上去吧，或者传到哪个没被ban的公网服务器上下载也行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/v2rayA/v2rayA/releases/download/v1.5.9.1698.1/installer_debian_amd64_1.5.9.1698.1.deb
sudo apt install ./installer_debian_arm64_1.5.9.1698.1.deb
# 可以添加为系统服务
sudo systemctl enable v2raya.service
sudo systemctl start v2raya.service
sudo systemctl status v2raya.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置aira2c&lt;/h2&gt;
&lt;p&gt;配置&lt;code&gt;aria2c&lt;/code&gt;远程下载服务，本地使用&lt;a href=&quot;https://github.com/mayswind/AriaNg&quot;&gt;AriaNg&lt;/a&gt;搭配风味更佳：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.config/aria2
touch ~/.config/aria2/aria2.conf       #aria2的配置文件
touch ~/.config/aria2/aria2.session    #aria2保存会话的文件
vim ~/.config/aria2/aria2.conf         #修改配置文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## &apos;#&apos;开头为注释内容, 选项都有相应的注释说明, 根据需要修改 ##
## 被注释的选项填写的是默认值, 建议在需要修改时再取消注释  ##

## 文件保存相关 ##

# 文件的保存路径(可使用绝对路径或相对路径), 默认: 当前启动位置
dir=/webroot/downloads
# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16M
#disk-cache=32M
# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc
# 预分配所需时间: none &amp;lt; falloc ? trunc &amp;lt; prealloc
# falloc和trunc则需要文件系统和内核支持
# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项
#file-allocation=none
# 断点续传
continue=true

## 下载连接相关 ##

# 最大同时下载任务数, 运行时可修改, 默认:5
#max-concurrent-downloads=5
# 同一服务器连接数, 添加时可指定, 默认:1
max-connection-per-server=16
# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M
# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载
min-split-size=4M
# 单个任务最大线程数, 添加时可指定, 默认:5
split=16
# 整体下载速度限制, 运行时可修改, 默认:0
max-overall-download-limit=3000000
# 单个任务下载速度限制, 默认:0
#max-download-limit=0
# 整体上传速度限制, 运行时可修改, 默认:0
max-overall-upload-limit=50000
# 单个任务上传速度限制, 默认:0
#max-upload-limit=0
# 禁用IPv6, 默认:false
disable-ipv6=true
# 连接超时时间, 默认:60
#timeout=60
# 最大重试次数, 设置为0表示不限制重试次数, 默认:5
#max-tries=5
# 设置重试等待的秒数, 默认:0
#retry-wait=0

## 进度保存相关 ##

# 从会话文件中读取下载任务
input-file=/root/.config/aria2/aria2.session
# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件
save-session=/root/.config/aria2/aria2.session
# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0
save-session-interval=60

## RPC相关设置 ##

# 启用RPC, 默认:false
enable-rpc=true
# 允许所有来源, 默认:false
rpc-allow-origin-all=true
# 允许非外部访问, 默认:false
rpc-listen-all=true
# 事件轮询方式, 取值:[epoll, kqueue, port, poll, select], 不同系统默认值不同
#event-poll=select
# RPC监听端口, 端口被占用时可以修改, 默认:6800
rpc-listen-port=6800
# 设置的RPC授权令牌, v1.18.4新增功能, 取代 --rpc-user 和 --rpc-passwd 选项
rpc-secret=&amp;lt;your-rpc-secret&amp;gt;
# 设置的RPC访问用户名, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-user=&amp;lt;USER&amp;gt;
# 设置的RPC访问密码, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-passwd=&amp;lt;PASSWD&amp;gt;
# 是否启用 RPC 服务的 SSL/TLS 加密,
# 启用加密后 RPC 服务需要使用 https 或者 wss 协议连接
#rpc-secure=true
# 在 RPC 服务中启用 SSL/TLS 加密时的证书文件,
# 使用 PEM 格式时，您必须通过 --rpc-private-key 指定私钥
#rpc-certificate=/path/to/certificate.pem
# 在 RPC 服务中启用 SSL/TLS 加密时的私钥文件
#rpc-private-key=/path/to/certificate.key

## BT/PT下载相关 ##

# 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:true
follow-torrent=true
# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999
listen-port=51414
# 单个种子最大连接数, 默认:55
#bt-max-peers=55
# 打开DHT功能, PT需要禁用, 默认:true
enable-dht=true
# 打开IPv6 DHT功能, PT需要禁用
#enable-dht6=false
# DHT网络监听端口, 默认:6881-6999
dht-listen-port=6881-6999
# 本地节点查找, PT需要禁用, 默认:false
bt-enable-lpd=true
# 种子交换, PT需要禁用, 默认:true
enable-peer-exchange=true
# 每个种子限速, 对少种的PT很有用, 默认:50K
bt-request-peer-speed-limit=3M
# 客户端伪装, PT需要
peer-id-prefix=-TR2770-
user-agent=Transmission/2.77
peer-agent=Transmission/2.77
# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0
seed-ratio=1.0
seed-time=0
# 强制保存会话, 即使任务已经完成, 默认:false
# 较新的版本开启后会在任务完成后依然保留.aria2文件
#force-save=false
# BT校验相关, 默认:true
#bt-hash-check-seed=true
# 继续之前的BT任务时, 无需再次校验, 默认:false
bt-seed-unverified=true
# 保存磁力链接元数据为种子文件(.torrent文件), 默认:false
bt-save-metadata=false
# dht-file
dht-file-path=/root/.config/aria2/dht.dat
# 删除未选择的文件
bt-remove-unselected-file=true
# 无速度时自动停止时间
#bt-stop-timeout=1800
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动aria2c：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aria2c --conf-path=$HOME/.config/aria2/aria2.conf -D
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装nps内网穿透服务端&lt;/h2&gt;
&lt;p&gt;小可怜服务器没多少空间舍不得安装golang，直接从&lt;a href=&quot;https://github.com/ehang-io/nps/releases&quot;&gt;release&lt;/a&gt;列表中下载相应的二进制运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_amd64_server.tar.gz
tar -zxvf linux_amd64_server.tar.gz -C linux_amd64_server
sudo ./nps install
# 安装完成后支持的命令：`nps start|stop|restart|uninstall|update or nps-update update`
sudo nps start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改配置文件&lt;code&gt;/etc/nps/conf/nps.conf&lt;/code&gt;的用户名和密码，以及默认的&lt;code&gt;http&lt;/code&gt;和&lt;code&gt;https&lt;/code&gt;监听，这台VPS我用打算用来中转流量，所以&lt;code&gt;80&lt;/code&gt;和&lt;code&gt;443&lt;/code&gt;打算暂时留给&lt;code&gt;nginx&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ...
web_username=xxx
web_password=xxx
# ...
#http_proxy_port=80
#https_proxy_port=443
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看日志&lt;code&gt;cat /var/log/nps.log&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2022/12/03 19:28:15.920 [I] [nps.go:202]  the version of server is 0.26.10 ,allow client core version to be 0.26.0
2022/12/03 19:28:16.496 [I] [server.go:200]  tunnel task  start mode：httpHostServer port 0
2022/12/03 19:28:16.496 [I] [connection.go:71]  web management start, access port is 8080
2022/12/03 19:28:16.508 [I] [connection.go:36]  server start, the bridge type is tcp, the bridge port is 8024
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在内网Rock-5B上下载客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_arm64_client.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是在内网树莓派3B+上下载客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_arm_v7_client.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照&lt;a href=&quot;https://ehang-io.github.io/nps/#/example&quot;&gt;说明&lt;/a&gt;添加一个客户端，并在树莓派上尝试连接nps服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./npc -server=4x.x.x.x:8024 -vkey=xxxxxxx -type=tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时刷新nps管理页面，客户端已显示线了。在管理页面的TCP隧道中加一条端口映射，就可以从电脑上连接&lt;code&gt;ssh -p 10022 pi@4x.x.x.x&lt;/code&gt;了。&lt;/p&gt;
&lt;h2&gt;自动续期SSL证书&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://zhuanlan.zhihu.com/p/347064501&quot;&gt;教程&lt;/a&gt;，我希望用4*服务器上的nps，将443映射给内网120服务器的443端口，并使得证书能够正常验证。&lt;/p&gt;
&lt;p&gt;此时我使用&lt;code&gt;apt update&lt;/code&gt;后，&lt;code&gt;apt&lt;/code&gt;报错，很明显&lt;code&gt;mirrors.cloud.aliyuncs.com&lt;/code&gt;这个地址指向我的Justhost服务器了，&lt;code&gt;ping&lt;/code&gt;也是直接被解析成了&lt;code&gt;ping mirrors.cloud.aliyuncs.com.mydomain.tech&lt;/code&gt;，此时&lt;code&gt;dig&lt;/code&gt;、&lt;code&gt;nslookup&lt;/code&gt;都不行，但&lt;code&gt;resolvectl query mirrors.cloud.aliyuncs.com&lt;/code&gt;仍然正常。我查了&lt;code&gt;ping&lt;/code&gt;命令，有&lt;a href=&quot;https://www.putorius.net/why-does-ping-and-dns-resolve-names.html&quot;&gt;文章&lt;/a&gt;说&lt;code&gt;ping&lt;/code&gt;的解析靠&lt;code&gt;/etc/nsswitch&lt;/code&gt;中&lt;code&gt;host&lt;/code&gt;项目的配置，于是我改&lt;code&gt;/etc/nsswitch&lt;/code&gt;、&lt;code&gt;/etc/hosts&lt;/code&gt;都不行。然后搜索这个域名发现有的&lt;a href=&quot;https://developer.aliyun.com/article/767805&quot;&gt;文章&lt;/a&gt;说，需要修改&lt;code&gt;/etc/hosts&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 这一组不行，v2rayA DNS hijack，这是v2rayA开启时注入的
#nameserver 127.2.0.17
#nameserver 119.29.29.29
# 这一组不行，这是v2rayA关闭后改回来的
#nameserver 223.6.6.6
#nameserver 119.29.29.29
# 这一组公共DNS也不行
#nameserver 8.8.8.8
#nameserver 114.114.114.114
# 这一组使得`mirrors.cloud.aliyuncs.com`恢复
# 即这个域名需要阿里云vpc下dns才能解析
nameserver 100.100.2.136
nameserver 100.100.2.138
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;申请SSL证书，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装acme.sh，末尾的`-s email=xxx@xxx.com是用来接受let&apos;s encrypt等更新邮件的地址
curl https://get.acme.sh | sh -s email=xxx@xxx.com
# 如果使用standalone模式，acme.sh会需要socat在80或443上建立服务
sudo apt install socat

# 重载shell以生效
source ~/.zshrc
# 验证安装
acme.sh -v
https://github.com/acmesh-official/acme.sh
v3.0.5
# 另外，acme.sh已经将默认CA设置为ZeroSSL，如果要用let&apos;s encrypt需要指定服务，
# ZeroSSL只支持http-01 dns-01，不支持tls-alpn-01
acme.sh --set-default-ca --server letsencrypt
# 默认使用443端口，如果要修改需要指定`--httpport 8443`
# 使用前记得停掉443上的进程，比如nps
acme.sh --issue -d aliyun.mydomain.tech --alpn
[Sat Dec  3 09:13:38 PM CST 2022] Your cert is in: /root/.acme.sh/aliyun.mydomain.tech/aliyun.mydomain.tech.cer
[Sat Dec  3 09:13:38 PM CST 2022] Your cert key is in: /root/.acme.sh/aliyun.mydomain.tech/aliyun.mydomain.tech.key
[Sat Dec  3 09:13:38 PM CST 2022] The intermediate CA cert is in: /root/.acme.sh/aliyun.mydomain.tech/ca.cer
[Sat Dec  3 09:13:38 PM CST 2022] And the full chain certs is there: /root/.acme.sh/aliyun.mydomain.tech/fullchain.cer
#申请成功后，查看已申请的证书信息
acme.sh --info -d aliyun.mydomain.tech
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装证书到nginx：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;acme.sh --install-cert -d aliyun.mydomain.tech \
--key-file       /etc/nginx/aliyun.mydomain.tech/key.pem  \
--fullchain-file /etc/nginx/aliyun.mydomain.tech/cert.pem \
--reloadcmd     &quot;service nginx force-reload&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装证书到syncthing&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;acme.sh --install-cert -d aliyun.mydomain.tech \
--key-file       /root/.config/syncthing/https-key.pem \
--fullchain-file /root/.config/syncthing/https-cert.pem \
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>obsidian使用初体验</title><link>https://zlotus.github.io/2022/12/01/first-try-in-obsidian/</link><guid isPermaLink="true">https://zlotus.github.io/2022/12/01/first-try-in-obsidian/</guid><description>有了5b就可以摸鱼的时候写笔记了</description><pubDate>Thu, 01 Dec 2022 14:37:00 GMT</pubDate><content:encoded>&lt;p&gt;obsidian有arm64版本的appimage，可以在rock 5b上直接使用，这下摸鱼的时候都可以记笔记了。&lt;/p&gt;
&lt;h2&gt;下载并执行AppImage&lt;/h2&gt;
&lt;p&gt;直接官网下载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/obsidianmd/obsidian-releases/releases/download/v1.0.3/Obsidian-1.0.3-arm64.AppImage
chmod a+x ./Obsidian-1.0.3-arm64.AppImage
# 终端里执行
./Obsidian-1.0.3-arm64.AppImage
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用技巧&lt;/h2&gt;
&lt;h3&gt;图片居中&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;YOUR_VAULT/.obsidian/themes/YOUR_CUSTOM_THEME.css&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exa -aT ./history_battle_100
history_battle_100
├── .stfolder
└── history_battle_100
   ├── .obsidian
   │  ├── app.json
   │  ├── appearance.json
   │  ├── core-plugins.json
   │  ├── hotkeys.json
   │  ├── themes
   │  │  └── my_custom.css          # 这个文件
   │  └── workspace.json
   ├── static
   │  └── test.png
   └── 第一季-秦并天下
      ├── 0. 汉末重写“开刊词”.md
      └── 1. 第一战：三家分晋（加量修订版）韩赵魏崛起，自春秋来到战国.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加&lt;a href=&quot;https://forum.obsidian.md/t/how-to-center-an-image-automatically-with-minimal-theme-and-image-auto-upload-plugin/34683/4&quot;&gt;自定义css&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;img {
        display: block !important;
        margin-left: auto !important;
        margin-right: auto !important;
}
    
 .markdown-source-view.mod-cm6 .cm-content &amp;gt; * {
        margin: auto auto !important;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;Settings&lt;/code&gt;=&amp;gt;&lt;code&gt;Appearance&lt;/code&gt;=&amp;gt;&lt;code&gt;Themes&lt;/code&gt;中选择&lt;code&gt;my_custom.css&lt;/code&gt;即可。&lt;/p&gt;
&lt;h3&gt;双向链接&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;链接到某一篇笔记：&lt;code&gt;[[ ]]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;链接到某一篇笔记中的某个标题：&lt;code&gt;[[ # ]]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;链接到某一篇笔记中的某个段落（块）：&lt;code&gt;[[ # ^ ]]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;为链接创建定义（关键词）：&lt;code&gt;[[ | 关键词]]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;链接到外部文件如印象笔记：&lt;code&gt;[关键词](链接)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>radxa Rock 5b使用体验</title><link>https://zlotus.github.io/2022/11/13/rock-5b-init/</link><guid isPermaLink="true">https://zlotus.github.io/2022/11/13/rock-5b-init/</guid><description>2022双十一闲鱼通过使用热心老哥-400¥的优惠券下单988到手の使用初体验</description><pubDate>Sun, 13 Nov 2022 18:52:00 GMT</pubDate><content:encoded>&lt;p&gt;今年双十一最大战果，就是通过radxa的QQ群找了个热心大哥，60¥买了他-400¥的券，下单了ROCK 5b裸板，昨天到货后我就迫不及待地去单位对面的顺丰网点自提了。回家因为没有HDMI显示器（家里甚至没电视），就用SSH+VNC进行了初体验，后来为这个板子专门买了台4K显示器，谁是椟谁是珠我都快搞不清了……&lt;/p&gt;
&lt;h2&gt;烧录镜像&lt;/h2&gt;
&lt;p&gt;我选用了官方提供的debian 11，在&lt;a href=&quot;https://github.com/radxa/debos-radxa/releases/latest&quot;&gt;release&lt;/a&gt;里选了&lt;a href=&quot;https://github.com/radxa/debos-radxa/releases/download/20221111-1029/rock-5b-debian-bullseye-xfce4-arm64-20221111-1506-gpt.img.xz&quot;&gt;rock-5b-debian-bullseye-xfce4-arm64-20221111-1506-gpt.img.xz&lt;/a&gt;镜像，按照&lt;a href=&quot;https://wiki.radxa.com/Rock5/install/microSD&quot;&gt;教程&lt;/a&gt;像树莓派一样用balenaEtcher烧录进TF卡中（使用TF卡是因为买不起emmc）。&lt;/p&gt;
&lt;p&gt;后来摸索发现Armbian也挺好用，但是&lt;a href=&quot;https://www.armbian.com/rock-5b/&quot;&gt;23版本&lt;/a&gt;一线通有问题，暂时使用&lt;a href=&quot;http://xogium.performanceservers.nl/archive/rock-5b/archive/&quot;&gt;22版本&lt;/a&gt;。如果把系统装在emmc上，就可以使用显示器反向充电一线通，据说tf卡速度不够pd协商过不去会不停重启。按照&lt;a href=&quot;https://wiki.radxa.com/Rock5/install/usb-install-emmc&quot;&gt;官方教程&lt;/a&gt;将系统烧录到emmc中。&lt;/p&gt;
&lt;h2&gt;安装风扇自动控制&lt;/h2&gt;
&lt;p&gt;淘宝了个5v/5.5cm孔距/1.25接口的北桥风扇，一开始插上去运行风扇测试脚本&lt;code&gt;sudo test_fan_run.sh&lt;/code&gt;不转（手拨一下才转），万用表测FAN输出4.87V，将脚本里的PWM占空参数调整为9000/10000后风扇起码跑起来了，再减下去依然能跑。网上有大佬写了&lt;a href=&quot;https://github.com/pymumu/fan-control-rock5b/releases/&quot;&gt;自动控制风扇的程序&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install lm-sensors

sudo ip route add 0.0.0.0/0 via 192.168.1.150 dev wlan0 proto static
wget https://github.com/pymumu/fan-control-rock5b/releases/download/1.1.0/fan-control-rock5b.1.1.0.arm64.deb
sudo ip route delete default via 192.168.1.150 dev wlan0 proto static
sudo apt install ./fan-control-rock5b.1.1.0.arm64.deb
sudo systemctl enable fan-control
sudo systemctl start fan-control
sudo systemctl status fan-control
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;正确设置WiFi和蓝牙&lt;/h2&gt;
&lt;p&gt;根据&lt;a href=&quot;https://wiki.radxa.com/Rock5/linux/debian#wifi-connection&quot;&gt;官方指南&lt;/a&gt;关于蓝牙的描述，如果使用RTL8852BE的蓝牙，需要修改黑名单设置才能让蓝牙正常工作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/modprobe.d/blacklist.conf
blacklist pgdrv
blacklist btusb
blacklist btrtl
blacklist btbcm
blacklist btintel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Armbian中，RTL8852BE网卡总会被认成两块MAC不同适配器，而且在启动时&lt;code&gt;dmesg&lt;/code&gt;显示其中一块网卡会被重命名为&lt;code&gt;wlP2p33s0&lt;/code&gt;，另一块可能叫做&lt;code&gt;wlan0&lt;/code&gt;也可能叫做&lt;code&gt;wlan1&lt;/code&gt;，这导致&lt;code&gt;wlP2p33s0&lt;/code&gt;的MAC地址可能是&lt;code&gt;14:xx:xx:xx:xx:xx&lt;/code&gt;也可能是&lt;code&gt;16:xx:xx:xx:xx:xx&lt;/code&gt;，自动连接WiFi时不确定会先由哪个网卡连上，在IP-MAC绑定时就无法确定下一次连接上的IP是多少。临时的解决方法是&lt;a href=&quot;https://ubuntu-mate.community/t/something-is-renaming-my-eth0-and-wlan0-interfaces/2884/8&quot;&gt;禁止系统重命名设备&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ln -s /dev/null /etc/udev/rules.d/80-net-setup-link.rules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启后&lt;code&gt;wlan0&lt;/code&gt;将会确定为&lt;code&gt;14:xx:xx:xx:xx:xx&lt;/code&gt;，&lt;code&gt;wlan1&lt;/code&gt;将会确定为&lt;code&gt;16:xx:xx:xx:xx:xx&lt;/code&gt;。网上有一些&lt;a href=&quot;https://www.reddit.com/r/pihole/comments/q53i1h/why_might_a_device_have_two_ip_and_mac_addresses/&quot;&gt;猜测&lt;/a&gt;认为这种现象可能是双模WiFi网卡引起的。使用&lt;code&gt;iw dev&lt;/code&gt;确实能看到是同一块物理设备下产生的两个适配器，&lt;code&gt;iw phy phy0 info&lt;/code&gt;虽然能看到设备的详细信息，但是完全看不懂……&lt;/p&gt;
&lt;h2&gt;更新设备并获取GPU支持&lt;/h2&gt;
&lt;p&gt;按照&lt;a href=&quot;https://www.armbian.com/rock-5b/&quot;&gt;Armbian官方说明&lt;/a&gt;添加大佬的源，以更新GPU驱动&lt;/p&gt;
&lt;p&gt;如果在&lt;code&gt;/lib/firmware/&lt;/code&gt;下没有&lt;code&gt;mali_csffw.bin&lt;/code&gt;，就去下载一个&lt;code&gt;sudo curl -o /lib/firmware/mali_csffw.bin https://github.com/JeffyCN/rockchip_mirrors/blob/libmali/firmware/g610/mali_csffw.bin&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;镜像中如果有不需要的软件，如libreoffice，可以先卸掉在配置系统，以避免不必要的升级。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip route add 0.0.0.0/0 via 192.168.1.150 dev wlan0 proto static
sudo add-apt-repository ppa:liujianfeng1994/panfork-mesa
sudo add-apt-repository ppa:liujianfeng1994/rockchip-multimedia
sudo ip route delete default via 192.168.1.150 dev wlan0 proto static

sudo apt remove --purge libreoffice\* thunderbird\* codium pidgin
sudo apt remove --purge thunderbird\*
sudo apt remove --purge codium
sudo apt remove --purge pidgin
# # 卸载自带的chromium，装大神的GPU加速版，卸载其他不需要的软件
# sudo apt remove chromium-\*

sudo apt-get clean
sudo apt-get autoremove

sudo apt update
sudo apt list --upgradable
#sudo apt dist-upgrade # 是否升级要斟酌，armbian22-&amp;gt;23后，pd有问题，无法一线通，还可能需要诱骗。

sudo apt install rockchip-multimedia-config

# sudo curl -o /lib/firmware/mali_csffw.bin https://github.com/JeffyCN/rockchip_mirrors/blob/libmali/firmware/g610/mali_csffw.bin

# sudo vim /etc/udev/rules.d/11-rockchip-multimedia.rules
# KERNEL==&quot;mpp_service&quot;, MODE=&quot;0660&quot;, GROUP=&quot;video&quot;
# KERNEL==&quot;rga&quot;, MODE=&quot;0660&quot;, GROUP=&quot;video&quot;
# KERNEL==&quot;system-dma32&quot;, MODE=&quot;0666&quot;, GROUP=&quot;video&quot;
# KERNEL==&quot;system-uncached-dma32&quot;, MODE=&quot;0666&quot;, GROUP=&quot;video&quot; RUN+=&quot;/usr/bin/chmod a+rw /dev/dma_heap&quot;

sudo apt install kodi

sudo mv /usr/share/xsessions/kodi.desktop /usr/share/wayland-sessions/kodi-wayland.desktop
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://linuxconfig.org/how-to-enable-disable-wayland-on-ubuntu-22-04-desktop&quot;&gt;如何启用/停用wayland桌面&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://forum.radxa.com/t/rk3588-kodi-rkmpp-accelerated-decoding-working-out-of-box/12785&quot;&gt;硬解相关&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://forum.armbian.com/topic/24802-kodi-for-rk35xx-510-legacy-kernel&quot;&gt;kodi安装的详细过程&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Armbian进系统选用户和桌面环境的界面叫&lt;code&gt;lightdm-slick-greeter&lt;/code&gt;，桌面环境列表来自&lt;code&gt;/usr/share/xsessions/*.desktop&lt;/code&gt;和&lt;code&gt;/usr/share/wayland-session/*.desktop&lt;/code&gt;，如果列表里面有不需要的项目可以将&lt;code&gt;*.desktop&lt;/code&gt;改为&lt;code&gt;*.desktop.bak&lt;/code&gt;之类的名字。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mv /usr/share/xsessions/gnome-xorg.desktop /usr/share/xsessions/gnome-xorg.desktop.bak
sudo mv /usr/share/xsessions/gnome.desktop /usr/share/xsessions/gnome.desktop.bak
sudo mv /usr/share/xsessions/ubuntu-xorg.desktop /usr/share/xsessions/ubuntu-xorg.desktop.bak
sudo mv /usr/share/xsessions/ubuntu.desktop /usr/share/xsessions/ubuntu.desktop.bak

sudo mv /usr/share/wayland-sessions/gnome.desktop /usr/share/wayland-sessions/gnome.desktop.bak
sudo mv /usr/share/wayland-sessions/ubuntu.desktop /usr/share/wayland-sessions/ubuntu.desktop.bak
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以使用&lt;code&gt;lightdm --show-config&lt;/code&gt;查看lightdm各项配置来源，一些配置项可以参考&lt;a href=&quot;https://wiki.archlinux.org/title/LightDM&quot;&gt;LightDM - ArchWiki&lt;/a&gt;、&lt;a href=&quot;https://wiki.ubuntu.com/LightDM&quot;&gt;LightDM - UbuntuWiki&lt;/a&gt;、&lt;a href=&quot;https://wiki.archlinux.org/title/Display_manager#Session_configuration&quot;&gt;Display manager - ArchWiki&lt;/a&gt;。其中，默认桌面环境可以在&lt;code&gt;/etc/lightdm/lightdm.conf.d/11-armbian.conf&lt;/code&gt;中添加&lt;code&gt;user-session=ubuntu-wayland&lt;/code&gt;。此外，可以用&lt;code&gt;lightdm-settings&lt;/code&gt;以GUI的方式改登录时候的背景之类的东西。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，更新Armbian23.02后，PD一线通会无限重启，官方建议诱骗。之后重新下载了&lt;a href=&quot;https://armbian.tnahosting.net/archive/rock-5b/archive/&quot;&gt;旧版&lt;/a&gt;Armbian_22.1.2，然后单独升级&lt;code&gt;armbian-firmware&lt;/code&gt;，并安装大佬ppa中的所有包，在更新gnome相关的包，断电重启进wayland恢复正常。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install armbian-firmware

sudo apt install libgl4es libdri2to3 libmali-g610-x11 malirun mali-g610-firmware libd3dadapter9-mesa libegl-mesa0 libegl1-mesa libgbm1 libgl1-mesa-dri libgl1-mesa-glx libglapi-mesa libgles2-mesa libglx-mesa0 libosmesa6 libwayland-egl1-mesa mesa-opencl-icd mesa-va-drivers mesa-vdpau-drivers mesa-vulkan-drivers

sudo apt install gnome-control-center-data gnome-control-center gnome-remote-desktop language-selector-gnome
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他的参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://launchpad.net/~liujianfeng1994/+archive/ubuntu/panfork-mesa&quot;&gt;大佬JianFeng Liu的源&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://raspberrypi.stackexchange.com/questions/10452/vncviewer-fonts&quot;&gt;x11vnc&lt;/a&gt;相关&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/42296329/how-to-properly-configure-xstartup-file-for-tightvnc-with-ubuntu-vps-gnome-envir&quot;&gt;tightvnc&lt;/a&gt;相关&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;基础设置&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://wiki.radxa.com/Rock5/linux/debian&quot;&gt;手册&lt;/a&gt;上说SSH默认用户名密码是&lt;code&gt;rock/rock&lt;/code&gt;，就直接进去了。进去以后执行命令是不是报语言环境的相关警告，就安装一下语言包，这个镜像默认没有装&lt;code&gt;locales&lt;/code&gt;，得自己手动安装。&lt;/p&gt;
&lt;p&gt;双十一买到手时，Armbian只有民间支持没有官方支持，Radxa官方只有debian和ubuntu，经过我一个多月的把玩，还是觉得Armbian好用。另外，十二月Armbian也官方也有了支持，因此建议直接用Armbian。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 用`free -m`检测虚拟内存，若有则关闭，5b的16G版本内存绰绰有余
sudo swapoff -a
# 修改虚拟内存优先级到`0`
sudo sysctl vm.swappiness=0
sudo vim /etc/sysctl.conf
vm.swappiness=0

# 如果是电口，就是`enP4p65s0`
ip route list
default via 192.168.1.1 dev enP4p65s0 proto dhcp metric 100
192.168.1.0/24 dev enP4p65s0 proto kernel scope link src 192.168.1.120 metric 100
# 添加一条默认路由到OpenWRT，更新会快一些
sudo ip route add 0.0.0.0/0 via 192.168.1.150 dev enP4p65s0 proto static
# 删除该默认路由
sudo ip route delete default via 192.168.1.150 dev enP4p65s0 proto static

# 如果是RTL8852BE无线网卡，就是`wlP2p33s0`
ip route list
default via 192.168.1.1 dev wlP2p33s0 proto dhcp metric 100
192.168.1.0/24 dev wlP2p33s0 proto kernel scope link src 192.168.1.120 metric 100
# 添加一条默认路由到OpenWRT
sudo ip route add 0.0.0.0/0 via 192.168.1.150 dev wlP2p33s0 proto static
#sudo ip route add 0.0.0.0/0 via 192.168.1.150 dev wlan0 proto static
# 删除该默认路由
sudo ip route delete default via 192.168.1.150 dev wlP2p33s0 proto static
#sudo ip route delete default via 192.168.1.150 dev wlan0 proto static

# 更新包管理器
sudo apt update
# 安装常用软件
sudo apt install -y zsh git vim tmux ufw neofetch aria2 lm-sensors glances exa
# 配置防火墙，# 设置legacy若ufw status报错
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
sudo ufw status
sudo ufw allow 22/tcp   comment ssh
sudo ufw allow 5900/tcp comment vnc
sudo ufw allow 2017/tcp comment v2raya
sudo ufw allow 6800/tcp comment aria2
sudo ufw enable

# 安装oh-my-zsh
chsh -s $(which zsh)
sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
# 如果安装路径有问题可以指定路径
ZSH=&quot;$HOME/.oh-my-zsh&quot; sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;

# 安装oh-my-tmux
cd ~
git clone https://github.com/gpakosz/.tmux.git
ln -s -f .tmux/.tmux.conf
cp .tmux/.tmux.conf.local .

# 生成ssh秘钥
ssh-keygen
# 安装locales
sudo apt install locales
# 配置语言
sudo dpkg-reconfigure locales
# 调整时区
sudo timedatectl set-timezone Asia/Shanghai
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没想到的是，切换oh-my-zsh的theme（可能是切换光标引起的，我切到jonathan就直接chromium segmentation fault），会导致chrome崩溃实属想不到，详见&lt;a href=&quot;https://bbs.archlinux.org/viewtopic.php?id=245833&quot;&gt;这篇文章&lt;/a&gt;……&lt;/p&gt;
&lt;p&gt;装了&lt;code&gt;neofetch&lt;/code&gt;看一下Debian的系统信息，起码硬件很强：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;       _,met$$$$$gg.          rock@rock-5b
    ,g$$$$$$$$$$$$$$$P.       ------------
  ,g$$P&quot;     &quot;&quot;&quot;Y$$.&quot;.        OS: Debian GNU/Linux 11 (bullseye) aarch64
 ,$$P&apos;              `$$$.     Host: Radxa ROCK 5B
&apos;,$$P       ,ggs.     `$$b:   Kernel: 5.10.66-27-rockchip-gea60d388902d
`d$$&apos;     ,$P&quot;&apos;   .    $$$    Uptime: 27 mins
 $$P      d$&apos;     ,    $$P    Packages: 1141 (dpkg)
 $$:      $$.   -    ,d$$&apos;    Shell: zsh 5.8
 $$;      Y$b._   _,d$P&apos;      Terminal: /dev/pts/0
 Y$$.    `.`&quot;Y$$$$P&quot;&apos;         CPU: (8) @ 1.800GHz
 `$$b      &quot;-.__              Memory: 365MiB / 15722MiB
  `Y$$
   `Y$$.
     `$$b.
       `Y$$b.
          `&quot;Y$b._
              `&quot;&quot;&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是Armbian的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                                 pi@rock-5b
                                 ----------
      █ █ █ █ █ █ █ █ █ █ █      OS: Armbian (22.11.2) aarch64
     ███████████████████████     Host: Radxa ROCK 5B
   ▄▄██                   ██▄▄   Kernel: 5.10.110-rockchip-rk3588
   ▄▄██    ███████████    ██▄▄   Uptime: 30 mins
   ▄▄██   ██         ██   ██▄▄   Packages: 1551 (dpkg)
   ▄▄██   ██         ██   ██▄▄   Shell: zsh 5.8.1
   ▄▄██   ██         ██   ██▄▄   Resolution: 3840x2160
   ▄▄██   █████████████   ██▄▄   Terminal: /dev/pts/1
   ▄▄██   ██         ██   ██▄▄   CPU: (8) @ 1.800GHz
   ▄▄██   ██         ██   ██▄▄   Memory: 830MiB / 15720MiB
   ▄▄██   ██         ██   ██▄▄
   ▄▄██                   ██▄▄
     ███████████████████████
      █ █ █ █ █ █ █ █ █ █ █
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装科学冲浪&lt;/h2&gt;
&lt;p&gt;安装核心：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装或更新核心
sudo bash -c &quot;$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)&quot; @ install
# 仅更新核心
sudo bash -c &quot;$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)&quot; @ install-geodata
# 卸载核心
sudo bash -c &quot;$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)&quot; @ remove

sudo systemctl status xray.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装UI，直接在&lt;a href=&quot;https://github.com/v2rayA/v2rayA/releases&quot;&gt;release&lt;/a&gt;中挑了一个&lt;a href=&quot;https://github.com/v2rayA/v2rayA/releases/download/v1.5.9.1698.1/installer_debian_arm64_1.5.9.1698.1.deb&quot;&gt;arm64的包&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/v2rayA/v2rayA/releases/download/v1.5.9.1698.1/installer_debian_arm64_1.5.9.1698.1.deb
sudo apt install ./installer_debian_arm64_1.5.9.1698.1.deb
# 可以添加为系统服务
sudo systemctl enable v2raya.service
sudo systemctl start v2raya.service
sudo systemctl status v2raya.service

# 更新防火墙
sudo ufw allow 2017/tcp

# 若防火墙报错可以切换nft或legacy试试
sudo update-alternatives --set iptables /usr/sbin/iptables-nft
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-nft
sudo update-alternatives --set arptables /usr/sbin/arptables-nft
sudo update-alternatives --set ebtables /usr/sbin/ebtables-nft

sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
sudo update-alternatives --set arptables /usr/sbin/arptables-legacy
sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是个WebUI，访问192.168.1.120:2017，官方推荐配置如下：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;配置&lt;/th&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;透明代理/系统代理&lt;/td&gt;
&lt;td&gt;启用: 大陆白名单模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;透明代理/系统代理实现方式&lt;/td&gt;
&lt;td&gt;redirect&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;规则端口的分流模式&lt;/td&gt;
&lt;td&gt;大陆白名单模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;防止DNS污染&lt;/td&gt;
&lt;td&gt;仅防止DNS劫持(快速)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;特殊模式&lt;/td&gt;
&lt;td&gt;supervisor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TCPFastOpen&lt;/td&gt;
&lt;td&gt;保持系统默认&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;多路复用&lt;/td&gt;
&lt;td&gt;关闭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;自动更新订阅&lt;/td&gt;
&lt;td&gt;关闭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;解析订阅链接/更新时优先使用&lt;/td&gt;
&lt;td&gt;跟随透明代理/系统代理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;配置aria2c服务&lt;/h2&gt;
&lt;p&gt;http下载vscode之类大文件之前，最好把aria2配好，然后使用如&lt;a href=&quot;https://github.com/mayswind/AriaNg&quot;&gt;AriaNg&lt;/a&gt;All-In-One版的WebUI控制。&lt;/p&gt;
&lt;p&gt;如果想要将aria2c配置为系统服务随机启动，就不能再想以前部署服务器一样在&lt;code&gt;$HOME&lt;/code&gt;目录下新建&lt;code&gt;.conf&lt;/code&gt;和&lt;code&gt;.session&lt;/code&gt;文件来保存配置和会话信息了，因为&lt;a href=&quot;https://serverfault.com/questions/957084/failed-at-step-exec-spawning-permission-denied&quot;&gt;Linux禁止系统服务从用户目录运行或访问用户目录&lt;/a&gt;。因此，不论二进制文件还是配置文件，或是启动时需要访问的会话文件都得放在别的地方。比如可以将配置文件放在&lt;code&gt;/etc/aria2c.conf&lt;/code&gt;中，将其他文件放在&lt;code&gt;/var/aria2c&lt;/code&gt;目录下。&lt;/p&gt;
&lt;p&gt;这里我使用的用户/组为&lt;code&gt;pi/pi&lt;/code&gt;，若使用&lt;code&gt;www-data/www-data&lt;/code&gt;下下来的文件用起来可能不方便。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/mayswind/AriaNg/releases/download/1.3.2/AriaNg-1.3.2-AllInOne.zip
unzip ./AriaNg-1.3.2-AllInOne.zip

# 更新防火墙
sudo ufw allow 6800/tcp
# 创建下载目录
sudo mkdir -p /var/aria2c/Downloads
# 即使用于/var/tmp一样的访问权限
sudo chmod 777 /var/aria2c /var/aria2c/Downloads
# 修改配置文件
sudo vim /etc/aria2.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置如下，注意修改&lt;code&gt;&amp;lt;your-rpc-secret&amp;gt;&lt;/code&gt;的远程密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## &apos;#&apos;开头为注释内容, 选项都有相应的注释说明, 根据需要修改 ##
## 被注释的选项填写的是默认值, 建议在需要修改时再取消注释  ##

## 文件保存相关 ##

# 文件的保存路径(可使用绝对路径或相对路径), 默认: 当前启动位置
dir=/var/aria2c/Downloads
# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16M
#disk-cache=32M
# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc
# 预分配所需时间: none &amp;lt; falloc ? trunc &amp;lt; prealloc
# falloc和trunc则需要文件系统和内核支持
# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项
#file-allocation=none
# 断点续传
continue=true

## 下载连接相关 ##

# 最大同时下载任务数, 运行时可修改, 默认:5
#max-concurrent-downloads=5
# 同一服务器连接数, 添加时可指定, 默认:1
max-connection-per-server=16
# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M
# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载
min-split-size=4M
# 单个任务最大线程数, 添加时可指定, 默认:5
split=16
# 整体下载速度限制, 运行时可修改, 默认:0
max-overall-download-limit=3000000
# 单个任务下载速度限制, 默认:0
#max-download-limit=0
# 整体上传速度限制, 运行时可修改, 默认:0
max-overall-upload-limit=50000
# 单个任务上传速度限制, 默认:0
#max-upload-limit=0
# 禁用IPv6, 默认:false
disable-ipv6=true
# 连接超时时间, 默认:60
#timeout=60
# 最大重试次数, 设置为0表示不限制重试次数, 默认:5
#max-tries=5
# 设置重试等待的秒数, 默认:0
#retry-wait=0

## 进度保存相关 ##

# 从会话文件中读取下载任务
input-file=/var/aria2c/aria2c.session
# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件
save-session=/var/aria2c/aria2c.session
# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0
save-session-interval=60

## RPC相关设置 ##

# 启用RPC, 默认:false
enable-rpc=true
# 允许所有来源, 默认:false
rpc-allow-origin-all=true
# 允许非外部访问, 默认:false
rpc-listen-all=true
# 事件轮询方式, 取值:[epoll, kqueue, port, poll, select], 不同系统默认值不同
#event-poll=select
# RPC监听端口, 端口被占用时可以修改, 默认:6800
rpc-listen-port=6800
# 设置的RPC授权令牌, v1.18.4新增功能, 取代 --rpc-user 和 --rpc-passwd 选项
rpc-secret=&amp;lt;your-rpc-secret&amp;gt;
# 设置的RPC访问用户名, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-user=&amp;lt;USER&amp;gt;
# 设置的RPC访问密码, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-passwd=&amp;lt;PASSWD&amp;gt;
# 是否启用 RPC 服务的 SSL/TLS 加密,
# 启用加密后 RPC 服务需要使用 https 或者 wss 协议连接
#rpc-secure=true
# 在 RPC 服务中启用 SSL/TLS 加密时的证书文件,
# 使用 PEM 格式时，您必须通过 --rpc-private-key 指定私钥
#rpc-certificate=/path/to/certificate.pem
# 在 RPC 服务中启用 SSL/TLS 加密时的私钥文件
#rpc-private-key=/path/to/certificate.key

## BT/PT下载相关 ##

# 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:true
follow-torrent=true
# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999
listen-port=51414
# 单个种子最大连接数, 默认:55
#bt-max-peers=55
# 打开DHT功能, PT需要禁用, 默认:true
enable-dht=true
# 打开IPv6 DHT功能, PT需要禁用
#enable-dht6=false
# DHT网络监听端口, 默认:6881-6999
dht-listen-port=6881-6999
# 本地节点查找, PT需要禁用, 默认:false
bt-enable-lpd=true
# 种子交换, PT需要禁用, 默认:true
enable-peer-exchange=true
# 每个种子限速, 对少种的PT很有用, 默认:50K
bt-request-peer-speed-limit=3M
# 客户端伪装, PT需要
peer-id-prefix=-TR2770-
user-agent=Transmission/2.77
peer-agent=Transmission/2.77
# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0
seed-ratio=1.0
seed-time=0
# 强制保存会话, 即使任务已经完成, 默认:false
# 较新的版本开启后会在任务完成后依然保留.aria2文件
#force-save=false
# BT校验相关, 默认:true
#bt-hash-check-seed=true
# 继续之前的BT任务时, 无需再次校验, 默认:false
bt-seed-unverified=true
# 保存磁力链接元数据为种子文件(.torrent文件), 默认:false
bt-save-metadata=false
# dht-file
dht-file-path=/var/aria2c/dht.dat
# 删除未选择的文件
bt-remove-unselected-file=true
# 无速度时自动停止时间
#bt-stop-timeout=1800
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加启动项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/systemd/system/aria2c.service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Aria2c download manager
Requires=network.target
After=dhcpcd.service

[Service]
Type=forking
User=pi
Group=pi
ExecStartPre=/usr/bin/env touch /var/aria2c/aria2c.session
WorkingDirectory=/var/aria2c/
ExecStart=/usr/bin/aria2c --conf-path=/etc/aria2.conf -D
TimeoutStopSec=20
Restart=on-failure

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable aria2c.service
sudo systemctl start aria2c.service
sudo systemctl status aria2c.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装vscode&lt;/h2&gt;
&lt;p&gt;添加微软源安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install wget gpg
wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor &amp;gt; packages.microsoft.gpg
sudo install -D -o root -g root -m 644 packages.microsoft.gpg /etc/apt/keyrings/packages.microsoft.gpg
sudo sh -c &apos;echo &quot;deb [arch=amd64,arm64,armhf signed-by=/etc/apt/keyrings/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main&quot; &amp;gt; /etc/apt/sources.list.d/vscode.list&apos;
rm -f packages.microsoft.gpg

sudo apt install apt-transport-https
sudo apt update
sudo apt install code # or code-insiders
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是下载deb安装：&lt;/p&gt;
&lt;p&gt;下载&lt;a href=&quot;https://code.visualstudio.com/download&quot;&gt;微软官方&lt;/a&gt;的&lt;a href=&quot;https://az764295.vo.msecnd.net/stable/6261075646f055b99068d3688932416f2346dd3b/code_1.73.1-1667966450_arm64.deb&quot;&gt;arm64 deb&lt;/a&gt;安装即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://az764295.vo.msecnd.net/stable/6261075646f055b99068d3688932416f2346dd3b/code_1.73.1-1667966450_arm64.deb
sudo apt install ./code_1.73.1-1667966450_arm64.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装wps-office&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://linux.wps.cn/&quot;&gt;wps主页&lt;/a&gt;下载即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://wps-linux-personal.wpscdn.cn/wps/download/ep/Linux2019/11664/wps-office_11.1.0.11664_arm64.deb
sudo apt install ./wps-office_11.1.0.11664_arm64.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Armbian安装rime输入法&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://neucrack.com/p/466&quot;&gt;这篇文章&lt;/a&gt;中fcitx5+rime的方法我没安装成功，就转向这个&lt;a href=&quot;https://www.zhihu.com/question/19839748&quot;&gt;答案&lt;/a&gt;，使用ibus+rime的组合。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install ibus ibus-rime
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;区域与语言&lt;/code&gt;-&amp;gt;&lt;code&gt;管理已安装的语言&lt;/code&gt;-&amp;gt;&lt;code&gt;键盘输入法系统&lt;/code&gt;-&amp;gt;选择&lt;code&gt;IBus&lt;/code&gt;。此时在&lt;code&gt;keyboard&lt;/code&gt;-&amp;gt;&lt;code&gt;input source&lt;/code&gt;中能看到&lt;code&gt;汉语&lt;/code&gt;-&amp;gt;&lt;code&gt;中文（Rime）&lt;/code&gt;（没有重启即可），选上即可。重启后，右上角出现ibus的选择框，选&lt;code&gt;中文（Rime）&lt;/code&gt;后报错，查看日志：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /tmp/rime.fcitx-rime.ERROR
E0116 16:40:14.597605  4678 deployment_tasks.cc:212] missing input schema: luna_pinyin
E0116 16:40:14.598170  4678 deployment_tasks.cc:212] missing input schema: luna_pinyin_simp
E0116 16:40:14.598309  4678 deployment_tasks.cc:212] missing input schema: luna_pinyin_fluency
E0116 16:40:14.598423  4678 deployment_tasks.cc:212] missing input schema: bopomofo
E0116 16:40:14.598546  4678 deployment_tasks.cc:212] missing input schema: bopomofo_tw
E0116 16:40:14.598660  4678 deployment_tasks.cc:212] missing input schema: cangjie5
E0116 16:40:14.598773  4678 deployment_tasks.cc:212] missing input schema: stroke
E0116 16:40:14.598884  4678 deployment_tasks.cc:212] missing input schema: terra_pinyin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用&lt;code&gt;apt&lt;/code&gt;搜索&lt;code&gt;rime&lt;/code&gt;发现需要继续安装data包（不必装全，最关键的应该是luna-pinyin）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install rime-data-luna-pinyin rime-data-pinyin-simp rime-data-bopomofo rime-data-cangjie5 rime-data-stroke rime-data-terra-pinyin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随便找个能输入的地方，此时左上角已经可以切换到Rime输入法了，随意输入后按&lt;code&gt;F4&lt;/code&gt;调出菜单界面选择&lt;code&gt;朙月拼音 简化字&lt;/code&gt;即可。&lt;/p&gt;
&lt;h2&gt;Armbian安装谷歌输入法&lt;/h2&gt;
&lt;p&gt;上安装中文输入法，我选了fcitx，不过中间需要很多玄学操作（UI上的操作就是“支持的语言”、“语言/区域”这种地方把中文选上，把默认输入法选成&lt;code&gt;fcitx&lt;/code&gt;），关键是用&lt;code&gt;fcitx-configtool&lt;/code&gt;查看报错，哪里飘红搜哪里。通常需要的操作是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 我也不知道到底是哪些包起作用
sudo apt install fcitx fcitx-pinyin fcitx-googlepinyin fcitx-config-gtk fcitx-frontend-all fcitx-frontend-gtk2 fcitx-frontend-gtk3 fcitx-frontend-qt5 fcitx-libs-dev  fcitx-ui-classic fcitx-ui-qimpanel fcitx-ui-light fcitx-table 
# 在shell配置文件里加几个环境变量
export GTK_IM_MODULE=fcitx
export QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
# 设置开机启动
cp /usr/share/applications/fcitx.desktop ~/.config/autostart/fcitx.desktop
# 用fcitx配置工具查看谷歌拼音之类的中文输入法有没有显式出来
fcitx-configtool
# 用输入法配置工具查看需要装什么东西
im-config
# 根据诊断工具debug
fcitx-diagnose
# 解决诊断工具中`gtk-query-immodules-3.0`和`gtk-query-immodules-2.0`的报错
sudo ln -s /usr/lib/aarch64-linux-gnu/libgtk-3-0/gtk-query-immodules-3.0 /usr/bin/gtk-query-immodules-3.0
sudo ln -s /usr/lib/aarch64-linux-gnu/libgtk2.0-0/gtk-query-immodules-2.0 /usr/bin/gtk-query-immodules-2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装chromium（有些民间Armbian镜像没有浏览器）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;注：chromium升级后不需要再进行下面的操作了，直接&lt;code&gt;apt install chromium-browser&lt;/code&gt;即可。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install chromium-browser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果chromium慢，可能是登录的时候没用wayland（我习惯用ubuntu-wayland，速度上gnome-wayland也一样）。&lt;/p&gt;
&lt;p&gt;可以&lt;a href=&quot;https://launchpad.net/~liujianfeng1994/+archive/ubuntu/rockchip-multimedia/&quot;&gt;换源&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt remove --purge chromium-browser chromium-codecs-ffmpeg-extra
sudo apt install chromium-browser=$(apt-cache show chromium-browser|grep Version|grep rkmpp|cut -d &quot; &quot; -f2) chromium-codecs-ffmpeg-extra=$(apt-cache show chromium-browser|grep Version|grep rkmpp|cut -d &quot; &quot; -f2) libv4l-rkmpp v4l-utils

sudo ln -s /lib /usr/lib64
cd /usr/lib64/ &amp;amp;&amp;amp; sudo ln -s aarch64-linux-gnu/libv4l2.so.0.0.0 libv4l2.so

sudo vim /etc/rc.local:
# 添加以下内容
echo dec &amp;gt; /dev/video-dec0
chown root:video /dev/video-dec0
chmod 0660 /dev/video-dec0
echo enc &amp;gt; /dev/video-enc0
chown root:video /dev/video-enc0
chmod 0660 /dev/video-enc0

sudo vim /etc/chromium-browser/default
# 替换默认flag
CHROMIUM_FLAGS=&quot;--use-gl=egl&quot;

sudo apt install gstreamer1.0-rockchip1 gstreamer1.0-alsa gstreamer1.0-pipewire gstreamer1.0-plugins-base-apps gstreamer1.0-plugins-base gstreamer1.0-x libgstreamer-gl1.0-0 libgstreamer-plugins-base1.0-0 clapper ffmpeg kodi moonlight-embedded obs-gstreamer obs-studio
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装syncthing&lt;/h2&gt;
&lt;p&gt;尝试部署一个syncthing服务器，在4x的服务器上安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -sS https://webinstall.dev/syncthing | bash
source ~/.config/envman/PATH.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在公网IP上开GUI配置界面，并开启防火墙：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;syncthing --gui-address=0.0.0.0:8384
sudo firewall-cmd --zone=public --permanent --add-port=8384/tcp  # WEB-UII界面
sudo firewall-cmd --zone=public --permanent --add-port=22000/tcp # 通信端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在想用的机子上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install syncthing
sudo systemctl enable syncthing-resume.service
systemctl --user enable syncthing.service
systemctl --user start syncthing.service
systemctl --user status syncthing.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;民间旧版Armbian遗留操作&lt;/h2&gt;
&lt;p&gt;他们官网上说对debian做了优化，比如尽量使用内存，减少落盘读写什么的。Radax的群里有大佬做了action&lt;a href=&quot;https://github.com/Headcrabed/armbian-rock5b-images/releases/&quot;&gt;日更镜像&lt;/a&gt;，可以试一下。另外，Armbian官方也更新了适用于Rock5B的&lt;a href=&quot;https://www.armbian.com/rock-5b/&quot;&gt;镜像&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果刷了armbian接HDMI时&lt;code&gt;ctrl&lt;/code&gt;+&lt;code&gt;alt&lt;/code&gt;+&lt;code&gt;F7&lt;/code&gt;只有光标，回&lt;code&gt;ctrl&lt;/code&gt;+&lt;code&gt;alt&lt;/code&gt;+&lt;code&gt;F1&lt;/code&gt;看&lt;code&gt;dmesg&lt;/code&gt;八成是显卡驱动问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 命令行下搜WiFi
iwlist scanning
# 加入WiFi
nmcli d wifi connect &quot;&amp;lt;your-ssid&amp;gt;&quot; password &amp;lt;your-password&amp;gt;

# 下载显卡驱动固件
wget https://github.com/JeffyCN/rockchip_mirrors/raw/libmali/firmware/g610/mali_csffw.bin
sudo mv mali_csffw.bin /lib/firmware/
# 添加大佬源
sudo add-apt-repository ppa:liujianfeng1994/panfork-mesa
sudo apt update
sudo apt upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Armbian安装docker&lt;/h2&gt;
&lt;p&gt;按照&lt;a href=&quot;https://i12bretro.github.io/tutorials/0476.html&quot;&gt;这篇文章&lt;/a&gt;一次安装完成：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;更新，安装依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# update software repositories
sudo apt update
# install necessary packages for https apt calls
sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用Ubuntu构建的Armbian添加如下gpg秘钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# add docker GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
# add docker software repository
sudo add-apt-repository &quot;deb [arch=arm64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用Debian构建的Armbian添加如下gpg秘钥：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# add docker GPG key
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -
# add docker software repository
sudo add-apt-repository &quot;deb [arch=arm64] https://download.docker.com/linux/debian $(lsb_release -cs) stable&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装docker：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# install docker
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io
# start docker service
sudo systemctl start docker
# enable docker service on startup
sudo systemctl enable docker
# create a docker group
sudo groupadd docker
# add the current user to the docker group
sudo usermod -aG docker $USER
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用hello-world镜像测试安装结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo docker run hello-world

Unable to find image &apos;hello-world:latest&apos; locally
latest: Pulling from library/hello-world
7050e35b49f5: Pull complete
Digest: sha256:faa03e786c97f07ef34423fccceeec2398ec8a5759259f94d99078f264e9d7af
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the &quot;hello-world&quot; image from the Docker Hub.
    (arm64v8)
3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;挂载smb共享&lt;/h2&gt;
&lt;p&gt;直接安装rock5b上的vlc和vlc-plugin-samba后，vlc仍然无法打开smb共享中的文件，若使用mpv时会报错说&quot;ffmpeg protocol not found. make sure ffmpeg/libav is compiled with networking support ... smb&quot;，可能是编译的时候没有带smb模块吧，懒得研究这个了，干脆将smb共享挂载为本地文件。&lt;/p&gt;
&lt;p&gt;参考&lt;a href=&quot;https://wiki.ubuntu.com/MountWindowsSharesPermanently#Mount_password_protected_network_folders&quot;&gt;Mount password protected network folders&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install cifs-utils
sudo mkdir /media/exos_1t
vim ~/.smbcredentials
#.smbcredentials
username=username
password=password

chmod 600 ~/.smbcredentials

//servername/EXOS_1T /media/exos_1t cifs credentials=/home/pi/.smbcredentials,iocharset=utf8 0 0 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;尝试玩游戏&lt;/h2&gt;
&lt;p&gt;B站上偶然间看到一个&lt;a href=&quot;https://www.bilibili.com/video/BV1cK41127Px/&quot;&gt;大佬的视频&lt;/a&gt;，大佬即是UP又是&lt;a href=&quot;https://github.com/ChisBread/malior&quot;&gt;malior&lt;/a&gt;的作者，运行这个库需要先装docker再部署：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget -O - https://github.com/ChisBread/malior/raw/main/install.sh &amp;gt; /tmp/malior-install.sh &amp;amp;&amp;amp; bash /tmp/malior-install.sh  &amp;amp;&amp;amp; rm /tmp/malior-install.sh 
malior-sudo &apos;echo $USER&apos; # 拉rk3588-gaming的镜像时间比较长

# 安装wine，时间比较长
malior install wine
# 安装中文环境:
malior winetricks -q fakechinese wenquanyi
# 游戏貌似必须放在`~/.local/malior/`目录下，红警跑起来报Yuri.exe的错
malior wine &quot;~/.local/malior/games/CandC_RA2/CandC_RA2YR/YURI.exe&quot;
# 冰封王座，但是声音只会出来一秒，在设置的声音里选一下输出设备就好了
# `-windows`是窗口模式，不加就是全屏：
malior wine &quot;~/.local/malior/games/Warcraft3_1.24E/War3.exe -opengl -windows&quot;
# 混乱之治加上`-classic`
malior wine &quot;~/.local/malior/games/Warcraft3_1.24E/War3.exe -opengl -classic&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.hiveworkshop.com/threads/complete-command-line-arguments-guide.288224/&quot;&gt;魔兽命令行参数&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;安装node&lt;/h2&gt;
&lt;p&gt;node对arm64的支持貌似最高到14，更高的我没测试。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
# 安装node 14
nvm install 14
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装Anaconda&lt;/h2&gt;
&lt;p&gt;anaconda也支持arm64，虽然Python不随最新的，但是包多省心啊。去&lt;a href=&quot;https://repo.anaconda.com/archive/&quot;&gt;anaconda&lt;/a&gt;或者&lt;a href=&quot;https://repo.anaconda.com/miniconda/&quot;&gt;miniconda&lt;/a&gt;下载一个arm64的&lt;code&gt;.sh&lt;/code&gt;安装就好了。&lt;/p&gt;
&lt;h2&gt;备份tf卡&lt;/h2&gt;
&lt;p&gt;装了这么多东西，有必要备份一下系统，我这里备在samba的一个盘上了，5.7G备了一个小时（macOS上&lt;code&gt;64k&lt;/code&gt;的&lt;code&gt;k&lt;/code&gt;要小写，否则报错）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dd if=/dev/disk2 conv=sync,noerror bs=64k status=progress | gzip -c &amp;gt; /Volumes/EXOS_1T/backup_image20221129.img.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恢复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gunzip -c /Volumes/EXOS_1T/backup_image20221129.img.gz | dd of=/dev/disk2 bs=64k status=progress
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;maskrom模式写入emmc&lt;/h2&gt;
&lt;p&gt;买个民间大神的&lt;a href=&quot;https://github.com/AghostPrj/Radxa-emmc-module&quot;&gt;emmc模块&lt;/a&gt;比官方便宜很多。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装rkdeveloptool
sudo port install automake autoconf libusb # 或 brew install automake autoconf libusb
git clone https://github.com/radxa/rkdeveloptool.git
cd rkdeveloptool
autoreconf -i
./configure
make
sudo cp rkdeveloptool /opt/homebrew/bin/
# 测试rkdeveloptool
rkdeveloptool -v
rkdeveloptool ver 1.32
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;烧录镜像，按住maskrom后type-c口接电脑，使用&lt;code&gt;lsusb&lt;/code&gt;验证USB设备显示&lt;code&gt;Bus 020 Device 031: ID 2207:350b Fuzhou Rockchip Electronics Company&lt;/code&gt;。运行&lt;code&gt;rkdeveloptool&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo rkdeveloptool ld
DevNo=1	Vid=0x2207,Pid=0x350b,LocationID=1402	Maskrom
# 下载spl loader
wget https://dl.radxa.com/rock5/sw/images/loader/rock-5b/rk3588_spl_loader_v1.08.111.bin
# 初始化ram并准备闪存环境
sudo rkdeveloptool db ./rk3588_spl_loader_v1.08.111.bin
Downloading bootloader succeeded.
# 写入镜像，如果是Armbian官网的.xz镜像则需要解压为.img
sudo rkdeveloptool wl 0 Armbian_22.11.2_Rock-5b_jammy_legacy_5.10.110_gnome_desktop.img
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GPIO的PWM&lt;/h2&gt;
&lt;p&gt;参考&lt;a href=&quot;https://forum.radxa.com/t/python-gpio-peripheral-library-for-rock-5/12718&quot;&gt;这篇文章&lt;/a&gt;，用&lt;a href=&quot;https://github.com/happyme531/Adafruit_Blinka&quot;&gt;大佬的Blinka&lt;/a&gt;，先看&lt;a href=&quot;https://learn.adafruit.com/circuitpython-on-orangepi-linux/orange-pi-pc-setup&quot;&gt;香橙派的手册&lt;/a&gt;作参考：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y python3 python3-pip
sudo apt install libgpiod2 python3-libgpiod
pip3 install gpiod
pip3 install --upgrade setuptools
pip3 freeze - local | grep -v &apos;^\-e&apos; | cut -d = -f 1 | xargs -n1 pip3 install -U

sudo apt install -y python3-smbus2 python3-dev i2c-tools
sudo pip3 install Adafruit-Blinka
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安了一大堆东西，在5b的vscode上按&lt;a href=&quot;https://docs.circuitpython.org/en/latest/shared-bindings/pwmio/index.html#module-pwmio&quot;&gt;手册&lt;/a&gt;写代码测试一下，GPIO的pin定义在&lt;a href=&quot;https://github.com/adafruit/Adafruit_Blinka/blob/main/src/adafruit_blinka/board/radxa/rock5.py&quot;&gt;rock5.py&lt;/a&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import board
import pwmio
pwm = pwmio.PWMOut(board.PWM14)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在vscode的&lt;code&gt;launch.json&lt;/code&gt;中要加上&lt;code&gt;&quot;sudo&quot;: true&lt;/code&gt;和&lt;code&gt;&quot;justMyCode&quot;: false&lt;/code&gt;两个选项，即“以管理员身份调试运行”和“调试库代码”，单步调试&lt;code&gt;adafruit_blinka/microcontroller/rockchip/PWMOut.py&lt;/code&gt;中的&lt;code&gt;PWMOut&lt;/code&gt;类时发现这段代码需要访问&lt;code&gt;/sys/class/pwm/pwmchip0/pwm14&lt;/code&gt;这个文件，但是我只有&lt;code&gt;/sys/class/pwm/pwmchip0/pwm0&lt;/code&gt;，而且感觉我的这个文件是风扇在用。&lt;/p&gt;
&lt;p&gt;问了一下群里大神，可能跟设备树有关，有人说要改dts编译dtb后替换现有文件，有人说改overlay可能也行，搜了半天，找到&lt;a href=&quot;https://docs.armbian.com/User-Guide_Allwinner_overlays/&quot;&gt;这篇文章&lt;/a&gt;。改overlay即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看有哪些overlay
exa -alT /boot/dtb/rockchip/overlay/
#...
.rwxr-xr-x  309 root  8 12月  2022 ├── rk3588-pwm2-m1.dtbo
.rwxr-xr-x  309 root  8 12月  2022 ├── rk3588-pwm3-m1.dtbo
.rwxr-xr-x  309 root  8 12月  2022 ├── rk3588-pwm5-m2.dtbo
.rwxr-xr-x  309 root  8 12月  2022 ├── rk3588-pwm6-m2.dtbo
.rwxr-xr-x  309 root  8 12月  2022 ├── rk3588-pwm7-m3.dtbo
.rwxr-xr-x  309 root  8 12月  2022 ├── rk3588-pwm8-m0.dtbo
.rwxr-xr-x  311 root  8 12月  2022 ├── rk3588-pwm12-m0.dtbo
.rwxr-xr-x  311 root  8 12月  2022 ├── rk3588-pwm13-m0.dtbo
.rwxr-xr-x  311 root  8 12月  2022 ├── rk3588-pwm13-m2.dtbo
.rwxr-xr-x  311 root  8 12月  2022 ├── rk3588-pwm14-m0.dtbo
.rwxr-xr-x  311 root  8 12月  2022 ├── rk3588-pwm14-m1.dtbo
.rwxr-xr-x  311 root  8 12月  2022 ├── rk3588-pwm15-m0.dtbo
.rwxr-xr-x  311 root  8 12月  2022 ├── rk3588-pwm15-m1.dtbo
.rwxr-xr-x  311 root  8 12月  2022 ├── rk3588-pwm15-m3.dtbo

# 修改`armbianEnv`
sudo vim /boot/armbianEnv.txt
# 添加`overlays=pwm2-m1 pwm6-m2 pwm7-m3`（心比较沉，加了三个）
overlay_prefix=rk3588
overlays=pwm2-m1 pwm6-m2 pwm7-m3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启后发现&lt;code&gt;/sys/class/pwm/&lt;/code&gt;下出现了另外的&lt;code&gt;pwmchip1&lt;/code&gt;、&lt;code&gt;pwmchip2&lt;/code&gt;、&lt;code&gt;pwmchip3&lt;/code&gt;，此时用&lt;code&gt;exa&lt;/code&gt;查看这三个新项目比原来的&lt;code&gt;pwmchip0&lt;/code&gt;少一些子目录，可能是没启用导致的，按照radax给的&lt;a href=&quot;https://wiki.radxa.com/Rock5/linux/debian#Fan&quot;&gt;风扇的例子&lt;/a&gt;启用一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 查看pwm项目
ls /sys/devices/platform/ | grep pwm
fd8b0010.pwm
fd8b0020.pwm
febd0020.pwm
febd0030.pwm
# 或用exa
exa -alt /sys/class/pwm/pwmchip0/
drwxr-xr-x    - root 14 2月  22:57 /sys/class/pwm/pwmchip0
lrwxrwxrwx    0 root 14 2月  22:59 ├── device -&amp;gt; ../../../fd8b0010.pwm

exa -alt /sys/class/pwm/pwmchip1/
drwxr-xr-x    - root 14 2月  22:57 /sys/class/pwm/pwmchip1
lrwxrwxrwx    0 root 14 2月  22:58 ├── device -&amp;gt; ../../../fd8b0020.pwm

exa -alt /sys/class/pwm/pwmchip2/
drwxr-xr-x    - root 14 2月  22:57 /sys/class/pwm/pwmchip2
lrwxrwxrwx    0 root 14 2月  22:58 ├── device -&amp;gt; ../../../febd0020.pwm

exa -alt /sys/class/pwm/pwmchip3/
drwxr-xr-x    - root 14 2月  23:08 /sys/class/pwm/pwmchip3
lrwxrwxrwx    0 root 14 2月  22:58 ├── device -&amp;gt; ../../../febd0030.pwm

# 启用pwm7-m3，引脚pin为27，https://wiki.radxa.com/Rock5/hardware/5b/gpio
echo 0 &amp;gt; /sys/devices/platform/febd0030.pwm/pwm/pwmchip3/export 
# 设置总周期
echo 10000 &amp;gt; /sys/devices/platform/febd0030.pwm/pwm/pwmchip3/pwm0/period 
# 调整占用时间
echo 3000 &amp;gt; /sys/devices/platform/febd0030.pwm/pwm/pwmchip3/pwm0/duty_cycle 
echo normal &amp;gt; /sys/devices/platform/febd0030.pwm/pwm/pwmchip3/pwm0/polarity 
# 打开引脚
echo 1 &amp;gt; /sys/devices/platform/febd0030.pwm/pwm/pwmchip3/pwm0/enable 
# 关闭引脚
echo 0 &amp;gt; /sys/devices/platform/febd0030.pwm/pwm/pwmchip*/pwm0/enable 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Armbian上的这个&lt;code&gt;pwmchip*&lt;/code&gt;应该是按顺序分配的，不是固定的，所以写成星号比较保险，用前面的&lt;code&gt;fexxxxxx&lt;/code&gt;确定针脚就好了。&lt;/p&gt;
&lt;h2&gt;USB声卡&lt;/h2&gt;
&lt;p&gt;群里大佬都说5b这个把3.5mm模拟音频口放在DP电源旁边的操作很魔幻，刚好我有块MOTU M2的外置声卡，走USB让5b只输出数字信号就好了。而且如果是一线通的话声卡直接插显示器通过显示器的USB Hub送到type-c再到5b，也就是5b上始终只有一根type-c线，负责供电、显示以及声卡。&lt;/p&gt;
&lt;p&gt;这时候问题就出现了，首先M2官方不支持Linux，然后在Armbian下5b的USB口直接驱动不起来声卡，必须接显示器USB Hub才能有足够的供电，一线通到5b时也能认出来这块声卡，但是播放音频的时候基本上每16秒钟有2-4秒的噪音，这种好了但没完全好的状态非常蛋疼，因为不懂dts也不懂内核，就懒得动手搞底层的东西，只是调了调&lt;code&gt;pulseaudio&lt;/code&gt;和&lt;code&gt;alsa&lt;/code&gt;的参数，毫无作用。&lt;/p&gt;
&lt;p&gt;想到手里还有一块再吃灰的树莓派2b，就想着如果是Armbian的问题，树莓派作为SBC里被支持的最好的设备，声卡什么的应该没问题。一开始拿出RPi-2b送电俩指示灯常亮，插拔SD卡也没反应，很诡异。我换了几个电源都不行，万用表瞎戳也没搞清楚啥情况，都送上咸鱼准备告别了，搜了一下有老外说灯常亮的时候换根线试试，竟然被他说中了……&lt;/p&gt;
&lt;p&gt;然后就是在RPi-2b上刷了个Raspberry Pi OS Lite (32-bit)，用当年RPi-2b配的USB 2.4G WiFi连上网，常规&lt;code&gt;update&lt;/code&gt;和&lt;code&gt;upgrade&lt;/code&gt;后，USB直接插声卡，亮了，说明起码供电没问题。装上&lt;code&gt;pulseaudio&lt;/code&gt;，用&lt;code&gt;aplay test.wav&lt;/code&gt;放歌试了一下，什么都没设它自动就送到USB声卡上了，没有噪音，很好。此时只需要把RPi-2b配置成PulseAudio Server，把5b配置成PulseAudio Client就好了。按照&lt;a href=&quot;https://unix.stackexchange.com/questions/470961/how-to-set-up-pulseaudio-remote-properly-and-securely&quot;&gt;这个教程&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# client and server both
sudo apt install pulseaudio-zeroconf # rock5b上可能是 sudo apt install pulseaudio-module-zeroconf
sudo systemctl enable avahi-daemon.service
sudo systemctl start avahi-daemon.service

# server side: 
sudo vim /etc/pulse/default.pa
load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1;192.168.0.0/24 auth-anonymous=1
load-module module-zeroconf-publish
# restart pulseaudio, check loaded modules
pulseaudio -k
pulseaudio --start
pacmd list-modules | grep native-protocol-tcp
pacmd list-modules | grep zeroconf-publish

# client side: 
sudo vim /etc/pulse/default.pa
load-module module-native-protocol-tcp
load-module module-zeroconf-discover
# restart pulseaudio, check loaded modules
pulseaudio -k
pulseaudio --start
pacmd list-modules | grep native-protocol-tcp
pacmd list-modules | grep zeroconf-discover
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时在作为客户端的5b上，用&lt;code&gt;pavucontrol&lt;/code&gt;查看输出设备，就已经出现多个RPi-2b上的虚拟输出设备了（多个可能是即有IPv4又有IPv6），选M2即可。测试播放音频，好了但没完全好，明显会有丢帧。RPi-2b（2.4G WiFi）与5b（5G Wifi）互联的延迟挺十几毫秒，有时候还跳上百，可能是因为小区的问题，我家的2.4G信号挺糟糕的。此时只能掏出网线，用RPi-2b跟5b直插，配上另一个段的IP（&lt;code&gt;nmcli&lt;/code&gt;配上ipv4后记得改manual）再把RPi-2b的WiFi关了（&lt;code&gt;nmcli&lt;/code&gt;里device disconnect或者connection down），重启PulseAudio即可，重新连上后基本解决了丢帧问题，现在听起跟本地设备一样，非常丝滑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmcli connection modify Wired\ connection\ 1 ipv4.addresses 192.168.100.100/24
nmcli connection modify Wired\ connection\ 1 ipv4.method manual
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;算是半完美解决声卡问题，小团圆结局也是happy ending。&lt;/p&gt;
&lt;h2&gt;安装硬盘休眠&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/adelolmo/hd-idle/releases/&quot;&gt;hd-idle&lt;/a&gt;项目可以完成这个任务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/adelolmo/hd-idle
cd hd-idle
make
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接运行&lt;code&gt;hd-idle -i 300 -l /mnt/UD_64G/hd-idle.log&lt;/code&gt;，即为所有硬盘设置300秒空闲停转时间，并指定日志文件。&lt;/p&gt;
&lt;p&gt;我的rock5b直接装不上release的deb，自己编译也不行。不折腾了&lt;/p&gt;
&lt;h2&gt;安装搜狗输入法（armbian安装可能会有问题）&lt;/h2&gt;
&lt;p&gt;apt中貌似没有，得从&lt;a href=&quot;https://shurufa.sogou.com/linux&quot;&gt;搜狗官网&lt;/a&gt;下载&lt;a href=&quot;https://ime.sogouimecdn.com/202211241829/d93bdc31aea079375adae51381cca02d/dl/gzindex/1656597217/sogoupinyin_4.0.1.2800_arm64.deb&quot;&gt;arm64的包&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装输入框架
sudo apt install fcitx5
# 安装输入法
sudo apt install ./sougoxxx.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后再系统dock里找到“搜索”，搜“fcitx”启动，搜狗就出现了。&lt;/p&gt;
&lt;h2&gt;安装VNC（debian可能没有需要安装）&lt;/h2&gt;
&lt;p&gt;如此豪华的硬件，必须得体验一下图形界面！随便搜个&lt;a href=&quot;https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-vnc-on-debian-11&quot;&gt;教程&lt;/a&gt;安装VNC服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 大概看一下装了哪些包
apt list --installed
# 已更新且发行版自带了xfce4和xfce4-goodies就不用再装了
#sudo apt update &amp;amp;&amp;amp; sudo apt install xfce4 xfce4-goodies

# 安装vnc服务端，这个发行版的apt里能搜到这一个vnc的server
sudo apt install tightvncserver
# 安装相关依赖
sudo apt install dbus-x11
# 启动
vncserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;vncserver&lt;/code&gt;执行首次启动，设置VNC访问密码（6-8位）并让VNC服务端生成配置文件。（我第一次不知道为什么密码没有设置成功，连接失败时日志指示&lt;code&gt;~/.vnc/passwd&lt;/code&gt;文件没生成，运行&lt;code&gt;vncpasswd&lt;/code&gt;再来一次就可以了）。&lt;/p&gt;
&lt;p&gt;接下来自定义VNC服务端配置文件。VNC默认启动于&lt;code&gt;5901&lt;/code&gt;端口，默认由&lt;code&gt;:1&lt;/code&gt;session占用，先停止默认的实例，再修改配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 开防火墙端口
sudo ufw allow 5901/tcp
# 停止实例`:1`
vncserver -kill :1
# 备份配置文件
mv ~/.vnc/xstartup ~/.vnc/xstartup.bak
# 新建配置文件
vim ~/.vnc/xstartup

#######写入配置#######
#!/bin/bash
xrdb $HOME/.Xresources # 让VNC的GUI去读取`.Xresources`文件，以指定图形化桌面终端的主题、颜色、字体等。
startxfce4 &amp;amp; # 让VNC启动xfce
#######写入结束#######

# 设置运行权限
sudo chmod +x ~/.vnc/xstartup
# 再次启动
vncserver
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VNC本身不使用加密协议，可以使用SSH建立隧道转发VNC以实现加密：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh -L 5901:127.0.0.1:5901 -C -N -l rock 192.168.1.120
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-L&lt;/code&gt;：将远端的&lt;code&gt;5901&lt;/code&gt;绑定在&lt;code&gt;127.0.0.1&lt;/code&gt;的&lt;code&gt;5901&lt;/code&gt;上；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-C&lt;/code&gt;：开启压缩以节省流量；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-N&lt;/code&gt;：告诉ssh我们不在远端执行命令；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-l&lt;/code&gt;：指定登录用户名，应使用非root用户登录。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这条命令执行后将建立到&lt;code&gt;192.168.1.120&lt;/code&gt;的隧道，命令不返回。在VNC客户端里，新建一个到本地&lt;code&gt;127.0.0.1:5901&lt;/code&gt;的连接，用户名改成&lt;code&gt;rock&lt;/code&gt;，就可以远程了。&lt;/p&gt;
&lt;p&gt;将VNC加入系统服务，新建文件&lt;code&gt;sudo vim /etc/systemd/system/vncserver@.service&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Start TightVNC server at startup
After=syslog.target network.target

[Service]
Type=forking
User=rock
Group=rock
WorkingDirectory=/home/rock

PIDFile=/home/rock/.vnc/%H:%i.pid
ExecStartPre=-/usr/bin/vncserver -kill :%i &amp;gt; /dev/null 2&amp;gt;&amp;amp;1
ExecStart=/usr/bin/vncserver -depth 24 -geometry 1280x800 :%i
ExecStop=/usr/bin/vncserver -kill :%i

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加载配置（&lt;code&gt;@&lt;/code&gt;后面的&lt;code&gt;1&lt;/code&gt;指的是VNC的&lt;code&gt;:1&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 重新加载系统配置工具
sudo systemctl daemon-reload
# 启动VNC服务端
sudo systemctl enable vncserver@1.service
sudo systemctl start vncserver@1
# 查看运行状态
sudo systemctl status vncserver@1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果打开chrome上B站发现中文全是方框，则应安装字体（不过前面选过locale就没问题了）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install fonts-wqy-microhei fonts-wqy-zenhei xfonts-intl-chinese xfonts-intl-chinese-big emacs-intl-fonts
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;安装USB无线网卡（失败）&lt;/h2&gt;
&lt;p&gt;随便找了个USB网卡，插入后查看USB设备，找到设备ID为&lt;code&gt;148f:7601 Ralink Technology, Corp. MT7601U Wireless Adapter&lt;/code&gt;。查看识别的网络设备，发现这个网卡未识别：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; lsusb
# ...
Bus 001 Device 004: ID 148f:7601 Ralink Technology, Corp. MT7601U Wireless Adapter
# ...

ip a # 或ip l
1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
# ...
2: enP4p65s0: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc mq state UP group default qlen 1000
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载驱动并编译（没搞定没深究）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install linux-headers-generic build-essential
git clone https://github.com/art567/mt7601usta.git
cd mt7601usta/src 
make # 此时报错 `can&apos;t find file Kconfig` 就不懂了
sudo make install
sudo mkdir -p /etc/Wireless/RT2870STA/
sudo cp RT2870STA.dat /etc/Wireless/RT2870STA/
sudo modprobe mt7601Usta
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把吃灰中raspberry pi 2的USB WiFi拔下来，&lt;code&gt;0bda:8176 Realtek Semiconductor Corp. RTL8188CUS 802.11n WLAN Adapter&lt;/code&gt;，是个2.4G免驱，解决。&lt;/p&gt;
&lt;h2&gt;部署matrix&lt;/h2&gt;
&lt;p&gt;安装数据库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install postgresql
# 安装完毕后系统中会自动新增一个`postgres`用户，切换用户
sudo -i -u postgres  
# 登录postgreSQL
psql
psql (14.5 (Ubuntu 14.5-0ubuntu0.22.04.1))
Type &quot;help&quot; for help.

# 退出
\q
# 查看状态
systemctl status postgresql.service 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 创建角色
sudo -u postgres createuser -P dendrite
# 创建数据库（单数据库模式）
sudo -u postgres createdb -O dendrite -E UTF-8 dendrite
# 测试connection string
pg_isready -d postgresql://dendrite:dendrite@localhost/dendrite
localhost:5432 - accepting connections
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装golang，nginx：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install golang nginx
go version
go version go1.18.1 linux/arm64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装并&lt;a href=&quot;https://matrix-org.github.io/dendrite/installation/configuration&quot;&gt;配置&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 下载
wget https://github.com/matrix-org/dendrite/archive/refs/tags/v0.10.8.zip
unzip /v0.10.8.zip
cd ./dendrite-0.10.8
# 构建安装
./build.sh
go install ./cmd/dendrite-monolith-server
# 生成密钥
./bin/generate-keys --private-key matrix_key.pem
Created private key file: matrix_key.pem
# 按手册提示修改配置文件，若需要命令行添加用户，还需要将`client_api.registration_shared_secret`设置值。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动dendrite，添加用户：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动
./dendrite-monolith-server -config /path/to/dendrite.yaml
# 添加管理员用户
./bin/create-account -config ./dendrite.yaml -username admin -admin
# 添加普通用户
./bin/create-account -config ./dendrite.yaml -username qinzishi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载并解压&lt;a href=&quot;https://github.com/vector-im/element-web/releases&quot;&gt;element客户端&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/vector-im/element-web/releases/download/v1.11.15/element-v1.11.15.tar.gz
tar -zxvf element-v1.11.15.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置nginx为一个简单的静态文件服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;

events {
    worker_connections 768;
    # multi_accept on;
}

http {
    server {
        listen 80;
        location / {
            root /var/aria2c/element-v1.11.15;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;网络问题&lt;/h2&gt;
&lt;p&gt;2024/04/14更新：&lt;/p&gt;
&lt;p&gt;领导又支援了一块5b。首先是随便买了个rtl8852be，结果&lt;code&gt;lspci&lt;/code&gt;时发现verdor id是&lt;code&gt;b85b&lt;/code&gt;，而不是原来的&lt;code&gt;b852&lt;/code&gt;。整了一个礼拜，试了各种系统，编译了&lt;a href=&quot;https://github.com/lwfinger/rtw89&quot;&gt;新驱动&lt;/a&gt;，lsmod什么的都能加载驱动，驱动起来也能搜索wifi，但是就是无法连接。我懒得编译旧驱动了，就重新买了块&lt;code&gt;b852&lt;/code&gt;的卡。刷了Armbian上的默认系统，这次ok了。&lt;/p&gt;
&lt;p&gt;发现了第二个问题，新系统5b的网卡是两块，用上面禁止重命名的方式无法连接，就将就着用了，但是俩个5b在某张卡连wifi时报destination host unreachable。两个5b到别的机子都通，但是互相之间不通。用&lt;code&gt;arp -a&lt;/code&gt;或者&lt;code&gt;ip neigh show&lt;/code&gt;看ARP表发现是incomplete状态，临时解决办法是在两个5b上都手写一条arp进去：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ip neigh replace to xxx.xxx.xxx.xxx dev wlP2p33s0 lladdr xx:xx:xx:xx:xx:xx nud permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;临时解决方法就这样了，也可以把命令加在启动脚本里省点事。&lt;/p&gt;
</content:encoded></item><item><title>第二个VPS科学球 - 改</title><link>https://zlotus.github.io/2022/09/24/second-vps-from-us/</link><guid isPermaLink="true">https://zlotus.github.io/2022/09/24/second-vps-from-us/</guid><description>一分价钱一分货，便宜没好货啊！</description><pubDate>Sat, 24 Sep 2022 16:27:00 GMT</pubDate><content:encoded>&lt;p&gt;新VPS科学球是&lt;a href=&quot;https://www.racknerd.com/&quot;&gt;racknerd&lt;/a&gt;一年9.89$捡的，算是美帝良心？&lt;/p&gt;
&lt;p&gt;后记：racknerd丢包率和延迟真的没法忍，趁人民币强势斥巨资（三百人民币）换了某瓦工的最贫穷CN2线路，果然一分价钱一分货……&lt;/p&gt;
&lt;p&gt;看了几个帖子又咨询了chatgpt，简单的说CN2 GIA是CN2的升级版，贵了两三倍，如果只是上上网CN2就够用了。&lt;/p&gt;
&lt;p&gt;:::caution
仅限技术交流！
:::&lt;/p&gt;
&lt;h2&gt;搭建新VPS科学球环境&lt;/h2&gt;
&lt;p&gt;Centos7实在太老了就重装了Ubuntu 22.04，首次登录改密码（为了方便，应该把&lt;code&gt;zsh&lt;/code&gt;、&lt;code&gt;git&lt;/code&gt;、&lt;code&gt;oh-my-zsh&lt;/code&gt;、&lt;code&gt;vim&lt;/code&gt;、&lt;code&gt;tmux&lt;/code&gt;装上。）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# VPS上改密码
passwd
# 电脑上上传秘钥进行ssh免密登录，也可以直接在云控制台里一次录入多个公钥
ssh-copy-id root@1*.*.*.*

sudo apt update &amp;amp;&amp;amp; sudo apt dist-upgrade -y
sudo apt install zsh git vim tmux ufw
chsh -s $(which zsh)
sh -c &quot;$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://wsgzao.github.io/post/ssh/&quot;&gt;将ssh设置为无法用密码登录（只接受key pair认证）&lt;/a&gt;，顺便调整ssh超时时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/ssh/sshd_config 中：
# 禁止密码认证
PasswordAuthentication no
# 调整ssh会话一次存活60秒，自动重连60次
ClientAliveInterval 60
ClientAliveCountMax 60

# 重启sshd服务：
service sshd restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开防火墙策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow 22/tcp comment ssh
sudo ufw allow 443/tcp comment https
sudo ufw allow 80/tcp comment http
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;搭建VPS科学球&lt;/h2&gt;
&lt;p&gt;继续向&lt;a href=&quot;https://www.jamesdailylife.com/v2ray-vless-tcp-xtls&quot;&gt;一键安装脚本&lt;/a&gt;（原来的&lt;a href=&quot;https://v2raytech.com/v2ray-all-in-one-script-vless-tcp-xtls-support/&quot;&gt;一键安装脚本&lt;/a&gt;已失效）低头，真香……这个脚本推荐的是trojan+TCP+TLS，我客户端也按照这个配的，其他的配置貌似连不上，我也没仔细研究。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget -P /root -N --no-check-certificate &quot;https://raw.githubusercontent.com/mack-a/v2ray-agent/master/install.sh&quot; &amp;amp;&amp;amp; chmod 700 /root/install.sh &amp;amp;&amp;amp; /root/install.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置&lt;code&gt;aria2c&lt;/code&gt;远程下载服务，本地使用&lt;a href=&quot;https://github.com/mayswind/AriaNg&quot;&gt;AriaNg&lt;/a&gt;搭配风味更佳：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p ~/.config/aria2
touch ~/.config/aria2/aria2.conf       #aria2的配置文件
touch ~/.config/aria2/aria2.session    #aria2保存会话的文件
vim ~/.config/aria2/aria2.conf         #修改配置文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;## &apos;#&apos;开头为注释内容, 选项都有相应的注释说明, 根据需要修改 ##
## 被注释的选项填写的是默认值, 建议在需要修改时再取消注释  ##

## 文件保存相关 ##

# 文件的保存路径(可使用绝对路径或相对路径), 默认: 当前启动位置
dir=/webroot/downloads
# 启用磁盘缓存, 0为禁用缓存, 需1.16以上版本, 默认:16M
#disk-cache=32M
# 文件预分配方式, 能有效降低磁盘碎片, 默认:prealloc
# 预分配所需时间: none &amp;lt; falloc ? trunc &amp;lt; prealloc
# falloc和trunc则需要文件系统和内核支持
# NTFS建议使用falloc, EXT3/4建议trunc, MAC 下需要注释此项
#file-allocation=none
# 断点续传
continue=true

## 下载连接相关 ##

# 最大同时下载任务数, 运行时可修改, 默认:5
#max-concurrent-downloads=5
# 同一服务器连接数, 添加时可指定, 默认:1
max-connection-per-server=16
# 最小文件分片大小, 添加时可指定, 取值范围1M -1024M, 默认:20M
# 假定size=10M, 文件为20MiB 则使用两个来源下载; 文件为15MiB 则使用一个来源下载
min-split-size=4M
# 单个任务最大线程数, 添加时可指定, 默认:5
split=16
# 整体下载速度限制, 运行时可修改, 默认:0
max-overall-download-limit=3000000
# 单个任务下载速度限制, 默认:0
#max-download-limit=0
# 整体上传速度限制, 运行时可修改, 默认:0
max-overall-upload-limit=50000
# 单个任务上传速度限制, 默认:0
#max-upload-limit=0
# 禁用IPv6, 默认:false
disable-ipv6=true
# 连接超时时间, 默认:60
#timeout=60
# 最大重试次数, 设置为0表示不限制重试次数, 默认:5
#max-tries=5
# 设置重试等待的秒数, 默认:0
#retry-wait=0

## 进度保存相关 ##

# 从会话文件中读取下载任务
input-file=/root/.config/aria2/aria2.session
# 在Aria2退出时保存`错误/未完成`的下载任务到会话文件
save-session=/root/.config/aria2/aria2.session
# 定时保存会话, 0为退出时才保存, 需1.16.1以上版本, 默认:0
save-session-interval=60

## RPC相关设置 ##

# 启用RPC, 默认:false
enable-rpc=true
# 允许所有来源, 默认:false
rpc-allow-origin-all=true
# 允许非外部访问, 默认:false
rpc-listen-all=true
# 事件轮询方式, 取值:[epoll, kqueue, port, poll, select], 不同系统默认值不同
#event-poll=select
# RPC监听端口, 端口被占用时可以修改, 默认:6800
rpc-listen-port=6800
# 设置的RPC授权令牌, v1.18.4新增功能, 取代 --rpc-user 和 --rpc-passwd 选项
rpc-secret=&amp;lt;your-rpc-secret&amp;gt;
# 设置的RPC访问用户名, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-user=&amp;lt;USER&amp;gt;
# 设置的RPC访问密码, 此选项新版已废弃, 建议改用 --rpc-secret 选项
#rpc-passwd=&amp;lt;PASSWD&amp;gt;
# 是否启用 RPC 服务的 SSL/TLS 加密,
# 启用加密后 RPC 服务需要使用 https 或者 wss 协议连接
#rpc-secure=true
# 在 RPC 服务中启用 SSL/TLS 加密时的证书文件,
# 使用 PEM 格式时，您必须通过 --rpc-private-key 指定私钥
#rpc-certificate=/path/to/certificate.pem
# 在 RPC 服务中启用 SSL/TLS 加密时的私钥文件
#rpc-private-key=/path/to/certificate.key

## BT/PT下载相关 ##

# 当下载的是一个种子(以.torrent结尾)时, 自动开始BT任务, 默认:true
follow-torrent=true
# BT监听端口, 当端口被屏蔽时使用, 默认:6881-6999
listen-port=51414
# 单个种子最大连接数, 默认:55
#bt-max-peers=55
# 打开DHT功能, PT需要禁用, 默认:true
enable-dht=true
# 打开IPv6 DHT功能, PT需要禁用
#enable-dht6=false
# DHT网络监听端口, 默认:6881-6999
dht-listen-port=6881-6999
# 本地节点查找, PT需要禁用, 默认:false
bt-enable-lpd=true
# 种子交换, PT需要禁用, 默认:true
enable-peer-exchange=true
# 每个种子限速, 对少种的PT很有用, 默认:50K
bt-request-peer-speed-limit=3M
# 客户端伪装, PT需要
peer-id-prefix=-TR2770-
user-agent=Transmission/2.77
peer-agent=Transmission/2.77
# 当种子的分享率达到这个数时, 自动停止做种, 0为一直做种, 默认:1.0
seed-ratio=1.0
seed-time=0
# 强制保存会话, 即使任务已经完成, 默认:false
# 较新的版本开启后会在任务完成后依然保留.aria2文件
#force-save=false
# BT校验相关, 默认:true
#bt-hash-check-seed=true
# 继续之前的BT任务时, 无需再次校验, 默认:false
bt-seed-unverified=true
# 保存磁力链接元数据为种子文件(.torrent文件), 默认:false
bt-save-metadata=false
# dht-file
dht-file-path=/root/.config/aria2/dht.dat
# 删除未选择的文件
bt-remove-unselected-file=true
# 无速度时自动停止时间
#bt-stop-timeout=1800
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动aria2c：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aria2c --conf-path=$HOME/.config/aria2/aria2.conf -D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果端口有监听，防火墙也设了策略，但是仍然不通，可能是selinux策略的问题，可以在selinux中选择添加新端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo semanage port -l | grep http_port_t
sudo semanage port -a -t http_port_t  -p tcp 8888
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果nginx报403错误，有可能是selinux的问题，看看&lt;code&gt;ll&lt;/code&gt;文件夹权限最后有没有一个&lt;code&gt;.&lt;/code&gt;或&lt;code&gt;+&lt;/code&gt;，如果有可以尝试关闭selinux重新创建文件夹，去掉那个&lt;code&gt;.&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果觉得selinux麻烦，可以选择关闭selinux：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 验证selinux状态：
getenforce
# 或
/usr/sbin/sestatus -v 

# 关闭selinux：
vim /etc/selinux/config
SELINUX=disabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，我的&lt;code&gt;tmux&lt;/code&gt;配置用的是&lt;a href=&quot;https://github.com/gpakosz/.tmux&quot;&gt;Oh my tmux&lt;/a&gt;，安装也很简单，只要&lt;code&gt;tmux &amp;gt;= 2.4&lt;/code&gt;即可（centos7默认是1.8）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd
git clone https://github.com/gpakosz/.tmux.git
ln -s -f .tmux/.tmux.conf
cp .tmux/.tmux.conf.local .
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;OpenWRT和客户端设置&lt;/h2&gt;
&lt;p&gt;我的macOS上用的是&lt;a href=&quot;https://github.com/yanue/v2rayU&quot;&gt;v2rayU&lt;/a&gt;，不过这个客户端貌似只有trojan配置可以用，其他不通，我也没仔细研究。&lt;/p&gt;
&lt;p&gt;树莓派上依然是刷了最新版的&lt;a href=&quot;https://doc.openwrt.cc/&quot;&gt;OpenWrt-Rpi&lt;/a&gt;，这个版本跟一两年前的区别是，刷固件的时候会先覆盖内存卡做初始化，避免了上次那种配置了WiFi后即使重刷固件依然无法修改WiFi配置，必须低格才能删掉配置的尴尬。&lt;/p&gt;
&lt;p&gt;在OpenWRT上要记得先设置静态IP（比如我用&lt;code&gt;192.168.1.150&lt;/code&gt;），网关指向家里最前端的路由，再配置自定义DNS（如陕西移动的DNS是&lt;code&gt;211.137.130.19&lt;/code&gt;），此时树莓派网络就正常了。&lt;/p&gt;
&lt;p&gt;接下来记得设置OpenWRT的DHCP，以使连上来的终端的网关和DNS强制指向这个树莓派，在 &lt;strong&gt;网络&lt;/strong&gt;-&amp;gt;&lt;strong&gt;LAN&lt;/strong&gt;-&amp;gt;&lt;strong&gt;高级设置&lt;/strong&gt; 中勾选&lt;strong&gt;强制&lt;/strong&gt;；设置IPv4子网掩码（比如我用&lt;code&gt;255.255.255.0&lt;/code&gt;）；设置&lt;strong&gt;DHCP选项&lt;/strong&gt;，比如我用&lt;code&gt;3,192.168.1.150&lt;/code&gt;和&lt;code&gt;6,192.168.1.150&lt;/code&gt;（&lt;code&gt;3&lt;/code&gt;指设置DHCP客户端的网关，&lt;code&gt;6&lt;/code&gt;指设置DHCP客户端的DNS，详见&lt;a href=&quot;https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol#Options&quot;&gt;维基DHCP选项&lt;/a&gt;）。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;最开始没设置DHCP选项，导致客户端接受的是前端路由器的DHCP设置，搜到这篇&lt;a href=&quot;https://forum.openwrt.org/t/dns-via-dhcp-wont-work-dhcp-option-6/59274/5&quot;&gt;排错&lt;/a&gt;后，用&lt;code&gt;service dnsmasq restart&lt;/code&gt;后查看日志&lt;code&gt;logread -e dnsmasq&lt;/code&gt;，发现报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Mon Oct 31 22:27:24 2022 user.notice dnsmasq: found already running DHCP-server on interface &apos;br-lan&apos; refusing to start, use &apos;option force 1&apos; to override
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里要用&lt;strong&gt;强制&lt;/strong&gt;选项DHCP才能设上，不过，如果网里同时有两个DHCP，通常相应快的那个会抢占设置客户端，所以最好还是关掉一个。&lt;/p&gt;
&lt;h2&gt;安装nps内网穿透服务端&lt;/h2&gt;
&lt;p&gt;小可怜服务器没多少空间舍不得安装golang，直接从&lt;a href=&quot;https://github.com/ehang-io/nps/releases&quot;&gt;release&lt;/a&gt;列表中下载相应的二进制运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_amd64_server.tar.gz
tar -zxvf linux_amd64_server.tar.gz
sudo ./nps install
sudo nps start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看日志&lt;code&gt;cat /var/log/nps.log&lt;/code&gt;，此时应该会有报错，因为我的&lt;code&gt;443&lt;/code&gt;早就被占了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
2022/11/06 12:21:24.577 [E] [http.go:82]  listen tcp 0.0.0.0:443: listen: address already in use
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改配置文件&lt;code&gt;/etc/nps/conf/nps.conf&lt;/code&gt;的用户名和密码，以及默认的&lt;code&gt;http&lt;/code&gt;和&lt;code&gt;https&lt;/code&gt;监听：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ...
web_username=xxx
web_password=xxx
# ...
http_proxy_port=8880
https_proxy_port=8443
# ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启&lt;code&gt;nps restart&lt;/code&gt;再次查看日志，已正常启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
2022/11/06 12:22:51.420 [I] [nps.go:202]  the version of server is 0.26.10 ,allow client core version to be 0.26.0
2022/11/06 12:22:52.927 [I] [connection.go:36]  server start, the bridge type is tcp, the bridge port is 8024
2022/11/06 12:22:52.931 [I] [server.go:200]  tunnel task  start mode：httpHostServer port 0
2022/11/06 12:22:52.931 [I] [connection.go:71]  web management start, access port is 8080
2022/11/06 12:22:52.946 [I] [connection.go:62]  start https listener, port is 8443
2022/11/06 12:22:52.946 [I] [connection.go:53]  start http listener, port is 8880
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启防火墙，其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;8024&lt;/code&gt;是默认桥接口，用于将内网客户端注册上来；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;8080&lt;/code&gt;是管理口，用于网页访问修改配置；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;10022&lt;/code&gt;是我要用的ssh映射端口口，即访问&lt;code&gt;4x.x.x.x:10022&lt;/code&gt;-&amp;gt;内网&lt;code&gt;192.168.x.x:22&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;其他端口，如&lt;code&gt;8443&lt;/code&gt;-&amp;gt;&lt;code&gt;443&lt;/code&gt;、&lt;code&gt;8880&lt;/code&gt;-&amp;gt;&lt;code&gt;80&lt;/code&gt;，是用于作域名映射的，我不用就不开策略了。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --permanent --add-port=8024/tcp # nps 桥接端口
sudo firewall-cmd --zone=public --permanent --add-port=8080/tcp # nps 管理端口
sudo firewall-cmd --zone=public --permanent --add-port=10022/tcp # nps 映射到内网22的端口
# 不使用域名代理，防火墙可以不开
#sudo firewall-cmd --zone=public --permanent --add-port=8443/tcp # nps http 代理端口
#sudo firewall-cmd --zone=public --permanent --add-port=8880/tcp # nps https 代理端口
firewall-cmd --list-all
firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，在内网的树莓派3B+上下载客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/ehang-io/nps/releases/download/v0.26.10/linux_arm_v7_client.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照&lt;a href=&quot;https://ehang-io.github.io/nps/#/example&quot;&gt;说明&lt;/a&gt;添加一个客户端，并在树莓派上尝试连接nps服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./npc -server=4x.x.x.x:8024 -vkey=*******
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时刷新nps管理页面，客户端已显示线了。在管理页面的TCP隧道中加一条端口映射，就可以从电脑上连接&lt;code&gt;ssh -p 10022 pi@4x.x.x.x&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;新树莓派环境里&lt;code&gt;youtube-dl&lt;/code&gt;下载带有key的&lt;code&gt;hls&lt;/code&gt;时，使用&lt;code&gt;--hls-prefer-native&lt;/code&gt;标识如果不生效，总是回到用&lt;code&gt;ffmpeg&lt;/code&gt;下载，有可能是因为树莓派的&lt;code&gt;python&lt;/code&gt;环境里&lt;a href=&quot;https://github.com/ytdl-org/youtube-dl/issues/24234#issuecomment-594735757&quot;&gt;缺少解密包&lt;/a&gt;，安装即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pycrypto
# 如果是python3.10，用pycryptodome，参考：https://stackoverflow.com/questions/70705404/systemerror-py-ssize-t-clean-macro-must-be-defined-for-formats
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;树莓派需要下载这些东西，如果要使用OpenWRT，可以临时改一下路由，将&lt;code&gt;0.0.0.0/0&lt;/code&gt;的默认路由指向OpenWRT即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 添加前
ip route list
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.119 metric 202
192.168.1.0/24 dev eth0 proto dhcp scope link src 192.168.1.119 metric 202
# 添加默认路由
sudo ip route add 0.0.0.0/0 via 192.168.1.150 dev eth0 proto static
# 查看结果
ip route list
0.0.0.0/0 via 192.168.1.150 dev eth0 proto static # 此时会多一条默认路由
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.119 metric 202
192.168.1.0/24 dev eth0 proto dhcp scope link src 192.168.1.119 metric 202
# 删除默认路由
sudo ip route del 0.0.0.0/0
# 查看结果
ip route list
default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.119 metric 202
192.168.1.0/24 dev eth0 proto dhcp scope link src 192.168.1.119 metric 202
# 如果不小心删了默认路由，加回去就行了
sudo ip route add default via 192.168.1.1 dev eth0 proto dhcp src 192.168.1.119 metric 202
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，&lt;/p&gt;
&lt;p&gt;最后&lt;strong&gt;注意&lt;/strong&gt;，此时树莓派的22口相当于暴露在公网上了，所以最好把ssh的密码登录关掉，只允许密钥登陆。&lt;/p&gt;
</content:encoded></item><item><title>PingCap的Rust训练课程5：异步编程</title><link>https://zlotus.github.io/2022/08/30/pna-rust-project-5/</link><guid isPermaLink="true">https://zlotus.github.io/2022/08/30/pna-rust-project-5/</guid><description>PingCap的Rust训练课程5：异步编程</description><pubDate>Tue, 30 Aug 2022 21:01:00 GMT</pubDate><content:encoded>&lt;p&gt;:::note
本文前半部分大多翻译自原文：&lt;a href=&quot;https://github.com/pingcap/talent-plan/tree/master/courses/rust/projects/project-5&quot;&gt;PNA Rust Project 5: Asynchrony&lt;/a&gt;，但这篇文章年久失修且烂尾，推荐跳过。&lt;/p&gt;
&lt;p&gt;本文 &lt;a href=&quot;.#1-Tokio%E6%A6%82%E8%BF%B0&quot;&gt;&lt;strong&gt;后半部分&lt;/strong&gt;&lt;/a&gt; 将使用 &lt;a href=&quot;https://tokio.rs/tokio/tutorial&quot;&gt;tokio官方示例 &lt;strong&gt;mini-redis&lt;/strong&gt;&lt;/a&gt; 作为替身，建议直接阅读。
:::&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;任务&lt;/strong&gt;：通过自定义协议，使用&lt;em&gt;异步网络&lt;/em&gt;创建一个多线程、持久化键/值存储的服务端和客户端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;了解在Rust中编写future时使用基本模式&lt;/li&gt;
&lt;li&gt;了解future的错误处理&lt;/li&gt;
&lt;li&gt;学会调试类型系统&lt;/li&gt;
&lt;li&gt;使用Tokio运行时执行异步网络&lt;/li&gt;
&lt;li&gt;使用boxed future处理棘手的类型系统问题&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;impl Trait&lt;/code&gt;创建匿名&lt;code&gt;Future&lt;/code&gt;类型
&lt;strong&gt;关键词&lt;/strong&gt;：异步、future、tokio、&lt;code&gt;impl Trait&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;注意：本文仅仅是本项目的提纲，并没有写完。如果您已经读到这里了，请电子邮件至&lt;a href=&quot;brian@pingcap.com&quot;&gt;brian@pingcap.com&lt;/a&gt;以提醒我，我将尽快写写完本文。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在本项目中，您将创建一个使用自定义协议通信的简单键/值服务端和客户端。该服务端将基于Tokio运行时构建的异步网络。键/值引擎的日志文件读写部分依然使用同步模式，在基础线程池上进行调度工作，并提供一个异步接口。在此过程中，您将尝试使用多种方法多种定义和使用future类型。&lt;/p&gt;
&lt;p&gt;因为学习使用Rust中的futures编程有较大的挑战性，而且相关文档目前仍然较少，因此该项目的范围相对有限，相关的解释也比之前的项目更加直接。&lt;/p&gt;
&lt;p&gt;请务必阅读有关此项目的背景资料。而如果您在本项目中感觉力不从心，那就让自己放松，休息一下，然后再试试。对于每个人来说，在Rust使用异步编程都是比较困难的。&lt;/p&gt;
&lt;h2&gt;项目需求规格&lt;/h2&gt;
&lt;p&gt;cargo项目&lt;code&gt;kvs&lt;/code&gt;建立了一个名为&lt;code&gt;kvs-client&lt;/code&gt;的命令行键值存储客户端，和一个名为&lt;code&gt;kvs-server&lt;/code&gt;的键值存储服务端，二者又都调用了一个名为&lt;code&gt;kvs&lt;/code&gt;的库。客户端通过一个自定义协议与服务端通信。&lt;/p&gt;
&lt;p&gt;CLI参数与上一个项目中的相同。引擎实现大致相同，即通过线程池分发同步的文件I/O作业。&lt;/p&gt;
&lt;p&gt;本项目的不同之处在于，所有的网络操作都是异步执行的。&lt;/p&gt;
&lt;p&gt;为了实现异步操作，&lt;code&gt;KvsClient&lt;/code&gt;将提供基于future的API，而&lt;code&gt;KvsEngine&lt;/code&gt; trait也将提供基于future的API，尽管它是通过线程池使用同步（阻塞）I/O实现的。&lt;/p&gt;
&lt;p&gt;您的&lt;code&gt;KvsServer&lt;/code&gt;将基于tokio运行时，tokio将负责把异步作业分发给自己的多个线程（tokio自带的线程池）。这意味着您的架构中实际包含了两层线程池，第一层用来异步的处理网络请求，每个线程占用一个核心；第二层用来同步的处理文件I/O，使用充足的线程以保持负责处理网络的线程持续忙碌。&lt;/p&gt;
&lt;p&gt;这种架构带来的变化，就是作业将从多个线程中生成到您的线程池，您的&lt;code&gt;ThreadPool&lt;/code&gt; trait及其实现将变为实现了&lt;code&gt;Clone + Send + &apos;sync&lt;/code&gt;的共享类型，就像&lt;code&gt;KvsEngine&lt;/code&gt;那样。&lt;/p&gt;
&lt;p&gt;因为您将实验多种不同的future返回方式，这里不再一一列出定义，而是在需要时定义。&lt;/p&gt;
&lt;p&gt;更具体地说，您将使用以下所有函数签名：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Client::get(&amp;amp;mut self, key: String) -&amp;gt; Box&amp;lt;Future&amp;lt;Item = Option&amp;lt;String&amp;gt;, Error = Error&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Client::get(&amp;amp;mut self, key: String) -&amp;gt; future::SomeExplicitCombinator&amp;lt;...&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Client::get(&amp;amp;mut self, key: String) -&amp;gt; impl Future&amp;lt;Item = Option&amp;lt;String&amp;gt;, Error = Error&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Client::get(&amp;amp;mut self, key: String) -&amp;gt; ClientGetFuture&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;项目设置&lt;/h2&gt;
&lt;p&gt;继续上一个项目，删除之前的测试目录，并复制本项目的测试目录。这个项目应该包含一个名为&lt;code&gt;kvs&lt;/code&gt;的库，以及两个可执行文件，&lt;code&gt;kvs-server&lt;/code&gt;和&lt;code&gt;kvs-client&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;中需要以下dev-dependencies：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dev-dependencies]
assert_cmd = &quot;0.11&quot;
criterion = &quot;0.2.11&quot;
crossbeam-utils = &quot;0.6.5&quot;
predicates = &quot;1.0.0&quot;
rand = &quot;0.6.5&quot;
tempfile = &quot;3.0.7&quot;
walkdir = &quot;2.2.7&quot;
panic-control = &quot;0.1.4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与之前的项目不同，不必费心编写足够的类型定义来编译测试套件。因为这样做将会一次向前跳过多个步骤。文本将明确指出何时使用测试套件。&lt;/p&gt;
&lt;h2&gt;知识背景：深入思考Rust中的future&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;为什么使用future？网络 vs 文件/io，阻塞 vs 非阻塞，同步 vs 异步&lt;/li&gt;
&lt;li&gt;从用户角度看future（不是以poll为核心的实现）&lt;/li&gt;
&lt;li&gt;关于执行器和运行时的技术细节，不需要思考过多&lt;/li&gt;
&lt;li&gt;考虑函数调用链及其如何转换为future类型&lt;/li&gt;
&lt;li&gt;如何调试Rust类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Result&lt;/code&gt; vs &lt;code&gt;Future&lt;/code&gt; vs &lt;code&gt;FutureResult&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;future的错误处理&lt;/li&gt;
&lt;li&gt;确定的future vs 智能指针future vs 匿名future&lt;/li&gt;
&lt;li&gt;关于future 0.1和future 0.3 的注意事项（我们将使用future 0.1）&lt;/li&gt;
&lt;li&gt;关于async/await的注意事项&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第1部分：将tokio引入客户端&lt;/h2&gt;
&lt;p&gt;最终我们要将客户端和服务器都转换为future，由于客户端非常简单，我们就从这里入手。我们将首先在现有的同步KvsClient上，介绍tokio运行时。&lt;/p&gt;
&lt;p&gt;这&lt;/p&gt;
&lt;p&gt;对于客户端，我们将在保持同步&lt;code&gt;KvsClient&lt;/code&gt;的同时引入异步运行时，然后再改造&lt;code&gt;KvsClient&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;KvsClient&lt;/code&gt;的&lt;code&gt;connect&lt;/code&gt;方法。&lt;/p&gt;
&lt;p&gt;请注意，作为一个库，&lt;code&gt;KvsClient&lt;/code&gt;可以提供基于futures的最高效率，但是我们的&lt;code&gt;kvs-client&lt;/code&gt;可执行文件并没有利用它，所以这个可执行文件运行单个future就立刻退出的行为看起来有点傻。&lt;/p&gt;
&lt;p&gt;TODO @sticnarf - 看看您是否可以编写与具体未来类型无关的测试用例，以便它们与以下所有策略一起使用。&lt;/p&gt;
&lt;h2&gt;第2部分：将&lt;code&gt;KvsClient&lt;/code&gt;转换为智能指针future&lt;/h2&gt;
&lt;p&gt;转为future类型时最省事的方式。&lt;/p&gt;
&lt;h2&gt;第3部分：具有显式future类型的&lt;code&gt;KvsClient&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;只是为了体验一下它是多么不靠谱。&lt;/p&gt;
&lt;h2&gt;第4部分：具有匿名future类型的&lt;code&gt;KvsClient&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;最终的解决方案&lt;/p&gt;
&lt;h2&gt;第5部分：使&lt;code&gt;ThreadPool&lt;/code&gt;可共享&lt;/h2&gt;
&lt;h2&gt;第6部分：将&lt;code&gt;KvsEngine&lt;/code&gt;转换为future&lt;/h2&gt;
&lt;p&gt;对于服务器，我们将做在客户端中相反的事，为&lt;code&gt;KvsEngine&lt;/code&gt;编写一个异步接口。这将表明future和底层运行时是相互独立的，并提供了一系列经验。&lt;/p&gt;
&lt;h2&gt;第7部分：使用tokio驱动&lt;code&gt;KvsEngine&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;请注意，即使我们自己编写的异步代码很少，tokio本身也在&lt;code&gt;num_cpus&lt;/code&gt;个线程间分发异步作业。权衡将CPU密集型作业直接放在网络线程或文件线程上的利弊，例如，将序列化操作放在哪里？&lt;/p&gt;
&lt;h2&gt;扩展1：使用tokio-fs替换同步文件I/O&lt;/h2&gt;
&lt;p&gt;干得漂亮，朋友。休息一下吧。&lt;/p&gt;
&lt;p&gt;:::tip
以下内容大多翻译自原文：&lt;a href=&quot;https://tokio.rs/tokio/tutorial&quot;&gt;Tokio: Tutorial&lt;/a&gt;，推荐阅读。
:::&lt;/p&gt;
&lt;h1&gt;1 Tokio概述&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;Tokio是Rust的异步运行时，提供了编写网络应用程序所需的模块。Tokio可灵活部署在大多数系统中，从具有数十个内核的大型服务器到小型嵌入式设备。&lt;/p&gt;
&lt;p&gt;概括的说，Tokio提供了几个主要组件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用于执行异步代码的多线程运行时。&lt;/li&gt;
&lt;li&gt;标准库的异步版本。&lt;/li&gt;
&lt;li&gt;一个庞大的相关库生态系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Tokio 在您的项目中的角色&lt;/h2&gt;
&lt;p&gt;当您以异步方式编写应用程序时，您可以通过降低同时做许多事情的成本来提升性能。但是，异步Rust代码不会自行运行，因此您必须选择一个运行时来执行它们。Tokio库是应用最广泛的运行时，其使用量超过了所有其他运行时的总和。&lt;/p&gt;
&lt;p&gt;此外，Tokio提供了许多有用的实用工具。编写异步代码时，不能使用Rust标准库提供的普通阻塞API，而必须使用它们的异步版本。这些替代版本由Tokio提供，在有意义时这些API将于Rust标准库API保持一致。&lt;/p&gt;
&lt;h2&gt;Tokio的优势&lt;/h2&gt;
&lt;p&gt;本章将概述Tokio的一些优点。&lt;/p&gt;
&lt;h3&gt;快速&lt;/h3&gt;
&lt;p&gt;Tokio很&lt;em&gt;快&lt;/em&gt;，基于Rust，而Rust本身也很快。遵循Rust精神，即目标是用户不应该通过手动编写等效代码来提高性能。&lt;/p&gt;
&lt;p&gt;Tokio&lt;em&gt;可扩展&lt;/em&gt;，基于async/await语言特性，本身就是可扩展的。在处理网络时，由于延迟，处理连接的速度受到限制，因此扩展的唯一方法是一次处理多个连接。使用async/await语言特性，可以非常方便的增加并发操作的数量，允许您扩展到大规模并发任务。&lt;/p&gt;
&lt;h3&gt;可靠&lt;/h3&gt;
&lt;p&gt;Tokio基于Rust编写，而Rust是一种可以让开发者编写可靠且高效软件的语言。&lt;a href=&quot;https://www.zdnet.com/article/microsoft-70-percent-of-all-security-bugs-are-memory-safety-issues/&quot;&gt;大量&lt;/a&gt;&lt;a href=&quot;https://www.chromium.org/Home/chromium-security/memory-safety&quot;&gt;研究&lt;/a&gt;表明，大约70%的高危安全漏洞是由内存安全引起的。使用Rust可以消除应用程序中的这一类错误。&lt;/p&gt;
&lt;p&gt;Tokio还非常注重提供一致的行为。Tokio的主要目标是允许用户部署行为可预测的软件，这些软件将日复一日地执行，具有可靠的响应时间，杜绝不可预测的延迟峰值。&lt;/p&gt;
&lt;h3&gt;简单&lt;/h3&gt;
&lt;p&gt;借助Rust的async/await特性，编写异步应用程序的复杂性已大大降低。结合Tokio的实用工具和充满活力的生态系统，编写应用程序变得轻而易举。&lt;/p&gt;
&lt;p&gt;Tokio在有意义的情况下遵循标准库的命名约定，以使用户可以轻松地将使用标准库编写的代码转换为使用Tokio编写的代码。借助Rust的强大类型系统，轻松交付正确代码的能力是无与伦比的。&lt;/p&gt;
&lt;h3&gt;灵活&lt;/h3&gt;
&lt;p&gt;Tokio提供了多种运行时架构。从多线程的&lt;a href=&quot;https://en.wikipedia.org/wiki/Work_stealing&quot;&gt;工作窃取&lt;/a&gt;运行时，到轻量级的单线程运行时，应有尽有。这些运行时中的每一个都带有许多可配置参数，以允许用户根据自己的需要调整它们。&lt;/p&gt;
&lt;h2&gt;不适用Tokio的场景&lt;/h2&gt;
&lt;p&gt;尽管Tokio对于许多需要同时做很多事情的项目很有用，但也有一些用例不适合Tokio。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过在多个线程上并发执行以提速的CPU密集型计算。Tokio专为IO密集型应用程序而设计，其中每个单独的任务大部分时间都在等待IO。如果您的应用程序唯一要做的就是并发计算，那么您应该使用&lt;a href=&quot;https://docs.rs/rayon/&quot;&gt;rayon&lt;/a&gt;。不过，如果您两种任务都做，仍然可以混合使用Tokio。&lt;/li&gt;
&lt;li&gt;读取大量文件。尽管Tokio看上去对于只需要读取大量文件的项目很有用，但与普通线程池相比，Tokio在这里没有任何优势。因为操作系统通常不提供异步文件API。&lt;/li&gt;
&lt;li&gt;发送单个Web请求。Tokio的优势是在需要同时做很多事情时。如果你需要使用一个用于异步Rust的库，例如&lt;a href=&quot;https://docs.rs/reqwest/&quot;&gt;reqwest&lt;/a&gt;，但你不需要一次做很多事情，你应该更喜欢那个库的阻塞版本，因为它会让你的项目更简单。当然，使用Tokio仍然有效，但与阻塞API相比并没有真正的优势。如果库不提供阻塞API，请参阅&lt;a href=&quot;https://tokio.rs/tokio/topics/bridging&quot;&gt;桥接同步代码&lt;/a&gt;章节。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;获得帮助&lt;/h2&gt;
&lt;p&gt;在任何时候，如果您遇到困难，您总是可以在&lt;a href=&quot;https://discord.gg/tokio&quot;&gt;Discord&lt;/a&gt;或&lt;a href=&quot;https://github.com/tokio-rs/tokio/discussions&quot;&gt;GitHub&lt;/a&gt;讨论中获得帮助。请不要担心问“初学者”问题。我们都是从某个地方开始，并乐于提供帮助。&lt;/p&gt;
&lt;h1&gt;2 环境设置&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;本教程将带您逐步完成构建Redis客户端和服务器的过程。我们将从使用Rust进行异步编程的基础知识开始，并从那里开始构建。我们将实现&lt;a href=&quot;https://redis.io/&quot;&gt;Redis&lt;/a&gt;命令的一个子集，以全面了解Tokio。&lt;/p&gt;
&lt;h2&gt;Mini-Redis&lt;/h2&gt;
&lt;p&gt;您将在本教程中构建的项目在&lt;a href=&quot;https://github.com/tokio-rs/mini-redis&quot;&gt;GitHub上以Mini-Redis&lt;/a&gt; 的形式提供。Mini-Redis的主要目标是学习Tokio，也得到了很好的评价，但这也意味着Mini-Redis缺少一些您在真正的Redis库中需要的功能。您可以在&lt;a href=&quot;https://crates.io/&quot;&gt;crates.io&lt;/a&gt;上找到可部署于生产环境的Redis库。&lt;/p&gt;
&lt;p&gt;我们将在教程中直接使用Mini-Redis，也就是在教程后面部分实现Mini-Redis之前就提前使用。&lt;/p&gt;
&lt;h2&gt;先修课&lt;/h2&gt;
&lt;p&gt;我们假设读者熟悉&lt;a href=&quot;https://rust-lang.org/&quot;&gt;Rust&lt;/a&gt;。&lt;a href=&quot;https://doc.rust-lang.org/book/&quot;&gt;《Rust Book》&lt;/a&gt;就是很好的启蒙读物。&lt;/p&gt;
&lt;p&gt;虽然不是必须的，但使用&lt;a href=&quot;https://doc.rust-lang.org/std/&quot;&gt;Rust标准库&lt;/a&gt;或其他语言编写网络代码的经验将会有所帮助。&lt;/p&gt;
&lt;p&gt;我们不需要读者熟悉Redis。&lt;/p&gt;
&lt;h3&gt;Rust&lt;/h3&gt;
&lt;p&gt;在教程开始之前，您应该确保安装了Rust工具链。如果没有，最简单的安装方法是使用&lt;a href=&quot;https://rustup.rs/&quot;&gt;rustup&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;教程需要不低于1.45.0的Rust版本，建议使用最新的Rust版本。&lt;/p&gt;
&lt;p&gt;检查计算机上安装的Rust版本，执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rustc --version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;您将会看到类似&lt;code&gt;rustc 1.46.0 (04488afe3 2020-08-24)&lt;/code&gt;的输出。&lt;/p&gt;
&lt;h3&gt;Mini-Redis server&lt;/h3&gt;
&lt;p&gt;接下来，安装Mini-Redis server，以在我们完成客户端编写时进行测试。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo install mini-redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动服务器以确定安装成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mini-redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，在另一个终端窗口中，尝试使用&lt;code&gt;mini-redis-cli&lt;/code&gt;获取键&lt;code&gt;foo&lt;/code&gt;的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mini-redis-cli get foo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你应该会看到输出为&lt;code&gt;(nil)&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;万事俱备&lt;/h2&gt;
&lt;p&gt;如此，一切就绪。阅读下一章以编写您的第一个异步Rust应用程序。&lt;/p&gt;
&lt;h1&gt;3 Hello Tokio&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;作为开胃菜，我们将编写一个非常基础的Rust应用。它将连接到Mini-Redis server，将键&lt;code&gt;Hello&lt;/code&gt;的值设置为&lt;code&gt;World&lt;/code&gt;，然后再读回键。这将使用Mini-Redis client库完成。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;h3&gt;初始化新crate&lt;/h3&gt;
&lt;p&gt;让我们从生成一个新的Rust App开始：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo new my-redis
cd my-redis
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;添加依赖&lt;/h3&gt;
&lt;p&gt;然后，向&lt;code&gt;Cargo.toml&lt;/code&gt;的&lt;code&gt;[dependencies]&lt;/code&gt;中添加以下依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }
mini-redis = &quot;0.4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编写代码&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;main.rs&lt;/code&gt;编写如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use mini_redis::{client, Result};

#[tokio::main]
async fn main() -&amp;gt; Result&amp;lt;()&amp;gt; {
    // Open a connection to the mini-redis address.
    let mut client = client::connect(&quot;127.0.0.1:6379&quot;).await?;

    // Set the key &quot;hello&quot; with value &quot;world&quot;
    client.set(&quot;hello&quot;, &quot;world&quot;.into()).await?;

    // Get key &quot;hello&quot;
    let result = client.get(&quot;hello&quot;).await?;

    println!(&quot;got value from the server; result={:?}&quot;, result);

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确保Mini-Redis server正在运行，在单独的终端中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mini-redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你还没有安装mini-redis，执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo install mini-redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，运行my-redis：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo run
got value from the server; result=Some(b&quot;world&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行成功！&lt;/p&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://github.com/tokio-rs/website/blob/master/tutorial-code/hello-tokio/src/main.rs&quot;&gt;这里&lt;/a&gt;找到全部代码。&lt;/p&gt;
&lt;h2&gt;代码详解&lt;/h2&gt;
&lt;p&gt;让我们花一些时间来理解刚刚做的事情。虽然没有太多代码，但执行了很多动作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut client = client::connect(&quot;127.0.0.1:6379&quot;).await?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/mini-redis/0.4/mini_redis/client/fn.connect.html&quot;&gt;&lt;code&gt;client::connect&lt;/code&gt;&lt;/a&gt;函数由&lt;code&gt;mini-Redis&lt;/code&gt; Crate提供。它将异步的与指定的远端地址建立TCP连接，并在链接成功建立时返回&lt;code&gt;client&lt;/code&gt;句柄。虽然这里执行了一个异步操作，但我们编写的代码&lt;strong&gt;看起来&lt;/strong&gt;跟同步代码没什么区别。只有&lt;code&gt;.await&lt;/code&gt;动作表明这个操作是异步的。&lt;/p&gt;
&lt;h3&gt;什么是异步编程？&lt;/h3&gt;
&lt;p&gt;大多数计算机程序会以与代码编写相同的顺序执行。先执行第一行，再执行下一行，依此类推。在同步编程中，当程序遇到无法立即完成的操作时，它将被阻塞直到操作完成。例如，建立TCP连接需要与网络上的其他节点交换数据，这可能需要大量时间，而在此期间，线程将被阻塞。&lt;/p&gt;
&lt;p&gt;而在异步编程中，无法立即完成的操作在后台挂起。该线程没有被阻止，并且可以继续执行其他任务。一旦操作完成，挂起的任务就会恢复，并从挂起的位置继续执行。我们之前的示例中只有一个任务，因此在挂起时什么都没有发生，但是真正的异步程序通常具有大量此类任务。&lt;/p&gt;
&lt;p&gt;尽管异步编程可以大幅提升应用程序的执行速度，但通常也会提升程序的复杂度。要求程序员在异步操作完成后跟踪所有可能的状态以恢复工作。以程序员的经验来说，这是一项繁琐且容易出错的任务。&lt;/p&gt;
&lt;h3&gt;编译时绿色线程&lt;/h3&gt;
&lt;p&gt;Rust使用称为&lt;code&gt;async/await&lt;/code&gt;的特性来实现异步编程。执行异步操作的函数将用&lt;code&gt;async&lt;/code&gt;关键字标记。在我们的示例中，&lt;code&gt;connect&lt;/code&gt;函数是这样定义的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use mini_redis::Result;
use mini_redis::client::Client;
use tokio::net::ToSocketAddrs;

pub async fn connect&amp;lt;T: ToSocketAddrs&amp;gt;(addr: T) -&amp;gt; Result&amp;lt;Client&amp;gt; {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async fn&lt;/code&gt;定义看起来像是常规的同步函数，但其实执行的是异步操作。Rust在&lt;strong&gt;编译时&lt;/strong&gt;将&lt;code&gt;async fn&lt;/code&gt;转换为一个异步运行的例程。在&lt;code&gt;async fn&lt;/code&gt;中的任何&lt;code&gt;.await&lt;/code&gt;动作都会将控制权转回线程，以使线程再异步操作挂起在后台的时间里继续执行其他工作。&lt;/p&gt;
&lt;p&gt;:::caution
虽然其他语言也实现了&lt;code&gt;async/await&lt;/code&gt;，但Rust采用了更独特的方法。最基础的，Rust的异步是惰性操作，这将会导致与其他语言不同的运行时语义。
:::&lt;/p&gt;
&lt;p&gt;如果这个解释不够明确，请不要担心。在整个指南中，我们将探索更多关于&lt;code&gt;async/await&lt;/code&gt;的内容。&lt;/p&gt;
&lt;h3&gt;使用&lt;code&gt;async/await&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;在Rust中调用异步函数与调用普通函数没什么区别。但是，仅调用这些函数并不会执行函数体，而是会返回一个值用来表示&lt;code&gt;async fn&lt;/code&gt;的操作。这在概念上类似于一个无参闭包。要实际运行该操作，您应该在返回值上使用&lt;code&gt;.await&lt;/code&gt;运算符。&lt;/p&gt;
&lt;p&gt;以下面的代码为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn say_world() {
    println!(&quot;world&quot;);
}

#[tokio::main]
async fn main() {
    // Calling `say_world()` does not execute the body of `say_world()`.
    let op = say_world();

    // This println! comes first
    println!(&quot;hello&quot;);

    // Calling `.await` on `op` starts executing `say_world`.
    op.await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hello
world
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async fn&lt;/code&gt;的返回值是一个实现了&lt;code&gt;Future&lt;/code&gt; trait的匿名类型。&lt;/p&gt;
&lt;h3&gt;异步&lt;code&gt;main&lt;/code&gt;函数&lt;/h3&gt;
&lt;p&gt;用于启动异步应用程序的&lt;code&gt;main&lt;/code&gt;函数，与大多数Rust crate中的常用&lt;code&gt;main&lt;/code&gt;函数不同。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;这是一个&lt;code&gt;async fn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;带有&lt;code&gt;#[tokio::main]&lt;/code&gt;标记&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;当我们想要进入异步上下文时，需要使用&lt;code&gt;async fn&lt;/code&gt;。但是，异步函数必须由一个&lt;a href=&quot;https://docs.rs/tokio/1/tokio/runtime/index.html&quot;&gt;运行时&lt;/a&gt;执行。该运行时包含异步任务调度器，提供事件I/O、定时器等特性。运行时不会自动启动，所以需要main函数启动它。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#[tokio::main]&lt;/code&gt;函数是一个宏。它将&lt;code&gt;async fn main()&lt;/code&gt;转换为同步&lt;code&gt;fn main()&lt;/code&gt;，初始化一个运行时实例并执行异步main函数。&lt;/p&gt;
&lt;p&gt;例如，下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[tokio::main]
async fn main() {
    println!(&quot;hello&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将转换为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let mut rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        println!(&quot;hello&quot;);
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面将会介绍关于Tokio运行时的详细信息。&lt;/p&gt;
&lt;h3&gt;Cargo特性&lt;/h3&gt;
&lt;p&gt;本教程依赖于Tokio，并启用了&lt;code&gt;full&lt;/code&gt;特性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tokio = { version = &quot;1&quot;, features = [&quot;full&quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tokio有很多功能（TCP、UDP、Unix sockets、计时器、同步实用程序、多种调度程序等）。并非所有应用程序都需要完整的Tokio功能。在尝试优化编译时间或最终程序的大小时，用户可以只选择需要使用的功能。&lt;/p&gt;
&lt;p&gt;在本教程中，选择Tokio依赖时将使用&lt;code&gt;full&lt;/code&gt;功能。&lt;/p&gt;
&lt;h1&gt;4 生成并发&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;我们将更换目标，开始研究Redis server端。&lt;/p&gt;
&lt;p&gt;首先，将上一章客户端的&lt;code&gt;SET&lt;/code&gt;/&lt;code&gt;GET&lt;/code&gt;代码移至example文件。这样，就可以在我们的服务端上进行测试了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir -p examples
mv src/main.rs examples/hello-redis.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后创建一个新的空&lt;code&gt;src/main.rs&lt;/code&gt;文件。&lt;/p&gt;
&lt;h2&gt;接受sockets&lt;/h2&gt;
&lt;p&gt;我们的Redis服务端需要做的第一件事是接受入站TCP sockets，即使用&lt;code&gt;tokio::net::TcpListener&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;:::note
Tokio的许多类型的名称，与这些类型在Rust标准库中的同步版本相同。如果定义合理，Tokio将暴露与&lt;code&gt;std&lt;/code&gt;完全相同的API，只不过是使用&lt;code&gt;async fn&lt;/code&gt;。
:::&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TcpListener&lt;/code&gt;绑定到&lt;code&gt;6379&lt;/code&gt;端口，并在循环中接受套sockets。每个socket都将被处理然后被关闭。现在，我们将读取命令，将其打印到标准输出并响应为一个错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::{TcpListener, TcpStream};
use mini_redis::{Connection, Frame};

#[tokio::main]
async fn main() {
    // Bind the listener to the address
    let listener = TcpListener::bind(&quot;127.0.0.1:6379&quot;).await.unwrap();

    loop {
        // The second item contains the IP and port of the new connection.
        let (socket, _) = listener.accept().await.unwrap();
        process(socket).await;
    }
}

async fn process(socket: TcpStream) {
    // The `Connection` lets us read/write redis **frames** instead of
    // byte streams. The `Connection` type is defined by mini-redis.
    let mut connection = Connection::new(socket);

    if let Some(frame) = connection.read_frame().await.unwrap() {
        println!(&quot;GOT: {:?}&quot;, frame);

        // Respond with an error
        let response = Frame::Error(&quot;unimplemented&quot;.to_string());
        connection.write_frame(&amp;amp;response).await.unwrap();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在运行这个循环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在另一个终端里运行&lt;code&gt;hello-redis&lt;/code&gt;的示例（即上一章那个&lt;code&gt;SET&lt;/code&gt;/&lt;code&gt;GET&lt;/code&gt;命令的例子）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo run --example hello-redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出应为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error: &quot;unimplemented&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在服务端的终端窗口输出应为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GOT: Array([Bulk(b&quot;set&quot;), Bulk(b&quot;hello&quot;), Bulk(b&quot;world&quot;)])
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;并发&lt;/h2&gt;
&lt;p&gt;我们的服务端仍有个小问题（除了只响应错误）。它一次仅处理一个入站请求，当一个连接被接受时，服务器将停留在接受循环代码块内，直到响应完全写入socket。&lt;/p&gt;
&lt;p&gt;我们希望Redis服务端能够并发处理&lt;strong&gt;大量&lt;/strong&gt;请求，为此，我们需要添加一些并发性。&lt;/p&gt;
&lt;p&gt;:::note
并发和并行不是一回事。如果您在两个任务之间交替进行，那么您是在并发处理这两个任务，而不是并行处理。如果是并行，您需要两个人，每个人专门负责一项任务。&lt;/p&gt;
&lt;p&gt;使用Tokio的优点之一，是异步代码允许您同时处理许多任务，而不必新建线程并行处理它们。事实上，Tokio可以在一个线程上并发运行多个任务！
:::&lt;/p&gt;
&lt;p&gt;为了并发处理连接，我们将为每个入站连接生成一个新任务，并在此任务上处理连接。&lt;/p&gt;
&lt;p&gt;接受连接的循环变为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind(&quot;127.0.0.1:6379&quot;).await.unwrap();

    loop {
        let (socket, _) = listener.accept().await.unwrap();
        // A new task is spawned for each inbound socket. The socket is
        // moved to the new task and processed there.
        tokio::spawn(async move {
            process(socket).await;
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;任务&lt;/h3&gt;
&lt;p&gt;Tokio任务是一个异步绿色线程，通过将&lt;code&gt;async&lt;/code&gt;块传给&lt;code&gt;tokio::spawn&lt;/code&gt;创建。&lt;code&gt;tokio::spawn&lt;/code&gt;函数返回一个&lt;code&gt;JoinHandle&lt;/code&gt;，调用者可以使用它与生成的任务进行交互。&lt;code&gt;async&lt;/code&gt;块可能会有返回值。调用者可以在&lt;code&gt;JoinHandle&lt;/code&gt;上使用&lt;code&gt;.await&lt;/code&gt;获取返回值。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        // Do some async work
        &quot;return value&quot;
    });

    // Do some other work

    let out = handle.await.unwrap();
    println!(&quot;GOT {}&quot;, out);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待&lt;code&gt;JoinHandle&lt;/code&gt;返回一个&lt;code&gt;Result&lt;/code&gt;。当任务在执行过程中遇到错误时，&lt;code&gt;JoinHandle&lt;/code&gt;会返回一个&lt;code&gt;Err&lt;/code&gt;，这将在任务panic、或任务因运行时关闭被强制取消时发生。&lt;/p&gt;
&lt;p&gt;调度程序以任务为单位管理执行，生成任务将提交给Tokio调度程序，调度程序将确保在有工作要做时执行该任务。生成的任务可以在其生成的线程上执行，也可以在运行时其他线程上执行，该任务也可以在产生后在线程间移动。&lt;/p&gt;
&lt;p&gt;Tokio中的任务非常轻量。实际上在底层，任务只需要一次64字节的内存分配。应用程序大可放心生成数千个乃至数百万个任务。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;&apos;static&lt;/code&gt;限制&lt;/h3&gt;
&lt;p&gt;当您在Tokio运行时生成任务时，其类型的生命周期必须是&lt;code&gt;&apos;static&lt;/code&gt;。这意味着生成的任务不得引用任务作用域外的任何数据。&lt;/p&gt;
&lt;p&gt;:::note
&lt;code&gt;&apos;static&lt;/code&gt;总是意味着“永远存在”是一个常见的误解，事实并非如此。仅仅因为一个值是&lt;code&gt;&apos;static&lt;/code&gt;并不意味着你有内存泄漏。你可以在&lt;a href=&quot;https://github.com/pretzelhammer/rust-blog/blob/master/posts/common-rust-lifetime-misconceptions.md#2-if-t-static-then-t-must-be-valid-for-the-entire-program&quot;&gt;&lt;strong&gt;常见的Rust生命周期误解&lt;/strong&gt;&lt;/a&gt;中了解到更多内容。
:::&lt;/p&gt;
&lt;p&gt;比如下面的代码将无法通过编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::task;

#[tokio::main]
async fn main() {
    let v = vec![1, 2, 3];

    task::spawn(async {
        println!(&quot;Here&apos;s a vec: {:?}&quot;, v);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试编译将会产生如下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0373]: async block may outlive the current function, but
              it borrows `v`, which is owned by the current function
 --&amp;gt; src/main.rs:7:23
  |
7 |       task::spawn(async {
  |  _______________________^
8 | |         println!(&quot;Here&apos;s a vec: {:?}&quot;, v);
  | |                                        - `v` is borrowed here
9 | |     });
  | |_____^ may outlive borrowed value `v`
  |
note: function requires argument type to outlive `&apos;static`
 --&amp;gt; src/main.rs:7:17
  |
7 |       task::spawn(async {
  |  _________________^
8 | |         println!(&quot;Here&apos;s a vector: {:?}&quot;, v);
9 | |     });
  | |_____^
help: to force the async block to take ownership of `v` (and any other
      referenced variables), use the `move` keyword
  |
7 |     task::spawn(async move {
8 |         println!(&quot;Here&apos;s a vec: {:?}&quot;, v);
9 |     });
  |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发生这种情况是因为默认情况下，变量所有权不会移动到异步代码块中。&lt;code&gt;v&lt;/code&gt;向量仍然归主函数所有，而&lt;code&gt;println!&lt;/code&gt;一行却借用&lt;code&gt;v&lt;/code&gt;。rust编译器向我们解释这一点，甚至建议将第7行更改为&lt;code&gt;task::spawn(async move {&lt;/code&gt;，以告诉编译器将&lt;code&gt;v&lt;/code&gt;移动到生成的任务中。如此，该任务拥有其需要的所有数据，使其成为&lt;code&gt;&apos;static&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果必须从多个任务同时访问单个数据，则该数据需要通过诸如&lt;code&gt;Arc&lt;/code&gt;之类的同步原语来共享。&lt;/p&gt;
&lt;p&gt;注意到错误消息说，参数类型超过了&lt;code&gt;&apos;static&lt;/code&gt;生命周期。这个术语可能会相当迷惑，因为&lt;code&gt;&apos;static&lt;/code&gt;生命周期一直持续到程序结束，所以如果超过它，不就是有内存泄漏吗？实际上这个消息说的是参数的类型，而不是其值必须超过&lt;code&gt;&apos;static&lt;/code&gt;生命周期，并且值可能在其类型不再有效之前就被销毁。&lt;/p&gt;
&lt;p&gt;当我们说一个值是&lt;code&gt;&apos;static&lt;/code&gt;时，意味着永远保持该值不会是错的。这很重要，因为编译器无法推断新生成的任务会保留多长时间。我们必须确保允许任务永远存在，以便Tokio可以在需要时让任务运行起来。&lt;/p&gt;
&lt;p&gt;上面信息框中链接的文章使用术语“受&lt;code&gt;&apos;static&lt;/code&gt;限制”，而不是“它的类型比&lt;code&gt;&apos;static&lt;/code&gt;寿命长”或“值是&lt;code&gt;&apos;static&lt;/code&gt;”来指代&lt;code&gt;T: &apos;static&lt;/code&gt;。这些都意味着同样的事情，但与&lt;code&gt;&amp;amp;&apos;static T&lt;/code&gt;中的“用&lt;code&gt;&apos;static&apos;&lt;/code&gt;标识”不同。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Send&lt;/code&gt;限制&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;tokio::spawn&lt;/code&gt;生成的任务&lt;strong&gt;必须&lt;/strong&gt;实现&lt;code&gt;Send&lt;/code&gt;。这允许Tokio运行时在任务被&lt;code&gt;.await&lt;/code&gt;挂起时，在线程之间移动它们。&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;.await&lt;/code&gt;调用保存的所有数据都是&lt;code&gt;Send&lt;/code&gt;时，任务也是&lt;code&gt;Send&lt;/code&gt;的，这有点微妙。当调用&lt;code&gt;.await&lt;/code&gt;时，任务将控制权转交给调度程序，在下次执行任务时，它会从上次转交的点恢复。为了确保这个策略生效，在&lt;code&gt;.await&lt;/code&gt;之后使用的所有状态都必须由任务保存。如果这些状态为&lt;code&gt;Send&lt;/code&gt;的，即可以跨线程移动，则任务本身就可以跨线程移动。相反，如果状态不是&lt;code&gt;Send&lt;/code&gt;的，那么任务也不是。&lt;/p&gt;
&lt;p&gt;这里的代码是有效的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::task::yield_now;
use std::rc::Rc;

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        // The scope forces `rc` to drop before `.await`.
        {
            let rc = Rc::new(&quot;hello&quot;);
            println!(&quot;{}&quot;, rc);
        }

        // `rc` is no longer used. It is **not** persisted when
        // the task yields to the scheduler
        yield_now().await;
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里则是无效的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::task::yield_now;
use std::rc::Rc;

#[tokio::main]
async fn main() {
    tokio::spawn(async {
        let rc = Rc::new(&quot;hello&quot;);

        // `rc` is used after `.await`. It must be persisted to
        // the task&apos;s state.
        yield_now().await;

        println!(&quot;{}&quot;, rc);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;尝试编译会得到如下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: future cannot be sent between threads safely
   --&amp;gt; src/main.rs:6:5
    |
6   |     tokio::spawn(async {
    |     ^^^^^^^^^^^^ future created by async block is not `Send`
    | 
   ::: [..]spawn.rs:127:21
    |
127 |         T: Future + Send + &apos;static,
    |                     ---- required by this bound in
    |                          `tokio::task::spawn::spawn`
    |
    = help: within `impl std::future::Future`, the trait
    |       `std::marker::Send` is not  implemented for
    |       `std::rc::Rc&amp;lt;&amp;amp;str&amp;gt;`
note: future is not `Send` as this value is used across an await
   --&amp;gt; src/main.rs:10:9
    |
7   |         let rc = Rc::new(&quot;hello&quot;);
    |             -- has type `std::rc::Rc&amp;lt;&amp;amp;str&amp;gt;` which is not `Send`
...
10  |         yield_now().await;
    |         ^^^^^^^^^^^^^^^^^ await occurs here, with `rc` maybe
    |                           used later
11  |         println!(&quot;{}&quot;, rc);
12  |     });
    |     - `rc` is later dropped here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将在下一章更深入地讨论这个错误的一个特例。&lt;/p&gt;
&lt;h2&gt;保存值&lt;/h2&gt;
&lt;p&gt;我们现在将实现&lt;code&gt;process&lt;/code&gt;函数来处理传入的命令，使用&lt;code&gt;HashMap&lt;/code&gt;来存储值。&lt;code&gt;SET&lt;/code&gt;命令将值插入到&lt;code&gt;HashMap&lt;/code&gt;中，而&lt;code&gt;GET&lt;/code&gt;值将读取这些值。此外，我们将使用循环从而让每个连接能够接受多个命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpStream;
use mini_redis::{Connection, Frame};

async fn process(socket: TcpStream) {
    use mini_redis::Command::{self, Get, Set};
    use std::collections::HashMap;

    // A hashmap is used to store data
    let mut db = HashMap::new();

    // Connection, provided by `mini-redis`, handles parsing frames from
    // the socket
    let mut connection = Connection::new(socket);

    // Use `read_frame` to receive a command from the connection.
    while let Some(frame) = connection.read_frame().await.unwrap() {
        let response = match Command::from_frame(frame).unwrap() {
            Set(cmd) =&amp;gt; {
                // The value is stored as `Vec&amp;lt;u8&amp;gt;`
                db.insert(cmd.key().to_string(), cmd.value().to_vec());
                Frame::Simple(&quot;OK&quot;.to_string())
            }
            Get(cmd) =&amp;gt; {
                if let Some(value) = db.get(cmd.key()) {
                    // `Frame::Bulk` expects data to be of type `Bytes`. This
                    // type will be covered later in the tutorial. For now,
                    // `&amp;amp;Vec&amp;lt;u8&amp;gt;` is converted to `Bytes` using `into()`.
                    Frame::Bulk(value.clone().into())
                } else {
                    Frame::Null
                }
            }
            cmd =&amp;gt; panic!(&quot;unimplemented {:?}&quot;, cmd),
        };

        // Write the response to the client
        connection.write_frame(&amp;amp;response).await.unwrap();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，启动服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在另一个终端里运行&lt;code&gt;hello-redis&lt;/code&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo run --example hello-redis
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在的输出应为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;got value from server; result=Some(b&quot;world&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们现在可以查询和设置键值了，但是有一个问题：这些键值并不能在连接之间共享。如果另一个套接字连接并尝试&lt;code&gt;GET&lt;/code&gt;键&lt;code&gt;hello&lt;/code&gt;，它将查询不到任何东西。&lt;/p&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://github.com/tokio-rs/website/blob/master/tutorial-code/spawning/src/main.rs&quot;&gt;这里&lt;/a&gt;找到完整的代码。&lt;/p&gt;
&lt;p&gt;在下一章中，我们将为所有套接字实现数据持久化。&lt;/p&gt;
&lt;h1&gt;5 共享状态&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;到目前为止，我们有了键值服务端，但却存在一个缺陷：状态并未在连接之间共享。本文将解决这个问题。&lt;/p&gt;
&lt;h2&gt;策略&lt;/h2&gt;
&lt;p&gt;在Tokio中共享状态通常有以下几种方式。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用互斥锁封装共享状态。&lt;/li&gt;
&lt;li&gt;再生成一个任务，专门用于管理状态，并使用消息传递操作状态。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一种方法通常用于简单数据，而第二种方法用于需要异步工作（例如I/O原语）的场景。在本章中，共享状态是一个&lt;code&gt;hashmap&lt;/code&gt;，并且操作是&lt;code&gt;insert&lt;/code&gt;和&lt;code&gt;get&lt;/code&gt;。这些操作都不是异步的，因此我们将使用互斥锁&lt;code&gt;Mutex&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;下一章将介绍后一种方法。&lt;/p&gt;
&lt;h2&gt;添加&lt;code&gt;bytes&lt;/code&gt;依赖&lt;/h2&gt;
&lt;p&gt;Mini-Redis crate将使用&lt;a href=&quot;https://docs.rs/bytes/1/bytes/struct.Bytes.html&quot;&gt;&lt;code&gt;bytes&lt;/code&gt;&lt;/a&gt; crate的&lt;code&gt;Bytes&lt;/code&gt;代替&lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;/code&gt;。&lt;code&gt;Bytes&lt;/code&gt;专门用于为网络编程提供强大的字节阵列结构。相较于&lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;/code&gt;，它添加了强大的浅拷贝功能。换句话说，在&lt;code&gt;Bytes&lt;/code&gt;实例上调用&lt;code&gt;clone()&lt;/code&gt;将不会复制底层数据，只会让实例的引用计数器加一，即&lt;code&gt;Byte&lt;/code&gt;实例仅是某个底层数据的引用计数器句柄，类似于添加了新功能的&lt;code&gt;Arc&amp;lt;Vec&amp;lt;u8&amp;gt;&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;要使用&lt;code&gt;bytes&lt;/code&gt;，请在&lt;code&gt;Cargo.toml&lt;/code&gt;的&lt;code&gt;[dependencies]&lt;/code&gt;中添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bytes = &quot;1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;初始化&lt;code&gt;HashMap&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;为了让&lt;code&gt;hashmap&lt;/code&gt;能够在许多任务和许多线程中共享，需要将其封装在&lt;code&gt;Arc&amp;lt;Mutex&amp;lt;_&amp;gt;&amp;gt;&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;首先，为方便使用，在&lt;code&gt;use&lt;/code&gt;之后给该类型起别名。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bytes::Bytes;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

type Db = Arc&amp;lt;Mutex&amp;lt;HashMap&amp;lt;String, Bytes&amp;gt;&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，更新&lt;code&gt;main&lt;/code&gt;函数以初始化&lt;code&gt;Hashmap&lt;/code&gt;并将&lt;code&gt;Arc&lt;/code&gt;的句柄传给&lt;code&gt;process&lt;/code&gt;函数。使用&lt;code&gt;Arc&lt;/code&gt;允许从许多任务中同时引用&lt;code&gt;Hashmap&lt;/code&gt;，并可能在许多线程上运行。在整个Tokio中，术语&lt;strong&gt;句柄&lt;/strong&gt;指的是那些可以访问某些共享状态的值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpListener;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind(&quot;127.0.0.1:6379&quot;).await.unwrap();

    println!(&quot;Listening&quot;);

    let db = Arc::new(Mutex::new(HashMap::new()));

    loop {
        let (socket, _) = listener.accept().await.unwrap();
        // Clone the handle to the hash map.
        let db = db.clone();

        println!(&quot;Accepted&quot;);
        tokio::spawn(async move {
            process(socket, db).await;
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关于使用&lt;code&gt;std::sync::Mutex&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;注意这里使用了&lt;code&gt;std::sync::mutex&lt;/code&gt;而不是&lt;code&gt;tokio::sync::Mutex&lt;/code&gt;来封装&lt;code&gt;Hashmap&lt;/code&gt;。一个常见的错误是在异步代码中无条件使用&lt;code&gt;tokio::sync::mutex&lt;/code&gt;。异步互斥锁用于锁住所有&lt;code&gt;.await&lt;/code&gt;操作。&lt;/p&gt;
&lt;p&gt;同步互斥锁在等待获取锁定时会阻塞当前线程，于是，将阻止其他任务处理。而如果使用&lt;code&gt;tokio::sync::Mutex&lt;/code&gt;通常没有作用，因为异步互斥锁的在内部也使用同步互斥锁。&lt;/p&gt;
&lt;p&gt;根据经验，只要竞争不高，并且不会在调用&lt;code&gt;.await&lt;/code&gt;时保持锁，则使用异步代码内的同步互斥锁也是可以的。此外，也可以考虑使用&lt;a href=&quot;https://docs.rs/parking_lot/0.10.2/parking_lot/type.Mutex.html&quot;&gt;&lt;code&gt;parking_lot::mutex&lt;/code&gt;&lt;/a&gt;作为&lt;code&gt;std::sync::Mutex&lt;/code&gt;的更快替代方案。&lt;/p&gt;
&lt;h2&gt;修改&lt;code&gt;process()&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;process()&lt;/code&gt;函数不再需要初始化&lt;code&gt;Hashmap&lt;/code&gt;，现在，它将接受一个共享的句柄作为参数，并且需要在使用前锁定该&lt;code&gt;Hashmap&lt;/code&gt;。要注意的是&lt;code&gt;Hashmap&lt;/code&gt;的值现在是&lt;code&gt;Bytes&lt;/code&gt;类型（可以高效的复制），因此也需要修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpStream;
use mini_redis::{Connection, Frame};

async fn process(socket: TcpStream, db: Db) {
    use mini_redis::Command::{self, Get, Set};

    // Connection, provided by `mini-redis`, handles parsing frames from
    // the socket
    let mut connection = Connection::new(socket);

    while let Some(frame) = connection.read_frame().await.unwrap() {
        let response = match Command::from_frame(frame).unwrap() {
            Set(cmd) =&amp;gt; {
                let mut db = db.lock().unwrap();
                db.insert(cmd.key().to_string(), cmd.value().clone());
                Frame::Simple(&quot;OK&quot;.to_string())
            }           
            Get(cmd) =&amp;gt; {
                let db = db.lock().unwrap();
                if let Some(value) = db.get(cmd.key()) {
                    Frame::Bulk(value.clone())
                } else {
                    Frame::Null
                }
            }
            cmd =&amp;gt; panic!(&quot;unimplemented {:?}&quot;, cmd),
        };

        // Write the response to the client
        connection.write_frame(&amp;amp;response).await.unwrap();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;任务、线程与竞争&lt;/h2&gt;
&lt;p&gt;当竞争较少时，使用互斥锁阻保护少量关键变量是可行的策略。当出现锁竞争时，执行任务的线程将会阻塞并等待互斥锁释放。这不仅会阻塞当前任务，而且还将阻止在当前线程上调度任何其他任务。&lt;/p&gt;
&lt;p&gt;默认情况下，Tokio运行时使用多线程调度程序。任务被安排在由运行时管理的任意数量的线程上。如果需要调度执行大量任务，并且他们都需要访问互斥锁，则将产生竞争。不过，如果启用了&lt;a href=&quot;https://docs.rs/tokio/1/tokio/runtime/index.html#current-thread-scheduler&quot;&gt;&lt;code&gt;current_thread&lt;/code&gt;&lt;/a&gt;运行时，则永远不会产生互斥锁竞争。&lt;/p&gt;
&lt;p&gt;:::note
&lt;a href=&quot;https://docs.rs/tokio/1/tokio/runtime/struct.Builder.html#method.new_current_thread&quot;&gt;&lt;code&gt;current_thread&lt;/code&gt;&lt;strong&gt;运行时&lt;/strong&gt;&lt;/a&gt;是一个轻量级的单线程运行时。当仅需生成少量任务并打开少量sockets时，这是一个不错的选择。例如，当在异步客户端库之上提供同步API时，就可以使用这个选项。
:::&lt;/p&gt;
&lt;p&gt;即使同步互斥锁竞争成为问题，也很少使用Tokio的互斥锁，而是应当考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;使用一个专用任务，以管理状态并使用消息传递。&lt;/li&gt;
&lt;li&gt;互斥锁分片。&lt;/li&gt;
&lt;li&gt;重组代码以避免使用互斥锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在我们的例子中，由于各个&lt;em&gt;键&lt;/em&gt;相互独立，因此可以选择将互斥锁分片。因此，我们将用&lt;code&gt;N&lt;/code&gt;个不同的实例代替例子中的单个&lt;code&gt;Mutex&amp;lt;HashMap&amp;lt;_, _&amp;gt;&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type ShardedDb = Arc&amp;lt;Vec&amp;lt;Mutex&amp;lt;HashMap&amp;lt;String, Vec&amp;lt;u8&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;;

fn new_sharded_db(num_shards: usize) -&amp;gt; ShardedDb {
    let mut db = Vec::with_capacity(num_shards);
    for _ in 0..num_shards {
        db.push(Mutex::new(HashMap::new()));
    }
    Arc::new(db)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如此，找到任何给定键就需要两个步骤：先是要确定该键在哪个分片中，再在这个&lt;code&gt;HashMap&lt;/code&gt;中查找该键。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let shard = db[hash(key) % db.len()].lock().unwrap();
shard.insert(key, value);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的这段简单实现要求确定的分片数量，而且一旦创建了分片映射分片数量就不能更改。&lt;a href=&quot;https://docs.rs/dashmap&quot;&gt;&lt;code&gt;dashmap&lt;/code&gt;&lt;/a&gt; crate提供了一个更复杂的分片哈希映射的实现。&lt;/p&gt;
&lt;h2&gt;在&lt;code&gt;.await&lt;/code&gt;上持有&lt;code&gt;MutexGuard&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;你可能会编写这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::sync::{Mutex, MutexGuard};

async fn increment_and_do_stuff(mutex: &amp;amp;Mutex&amp;lt;i32&amp;gt;) {
    let mut lock: MutexGuard&amp;lt;i32&amp;gt; = mutex.lock().unwrap();
    *lock += 1;

    do_something_async().await;
} // lock goes out of scope here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当你尝试并发调用那些调用此函数的代码时，将遇到以下错误消息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: future cannot be sent between threads safely
   --&amp;gt; src/lib.rs:13:5
    |
13  |     tokio::spawn(async move {
    |     ^^^^^^^^^^^^ future created by async block is not `Send`
    |
   ::: /playground/.cargo/registry/src/github.com-1ecc6299db9ec823/tokio-0.2.21/src/task/spawn.rs:127:21
    |
127 |         T: Future + Send + &apos;static,
    |                     ---- required by this bound in `tokio::task::spawn::spawn`
    |
    = help: within `impl std::future::Future`, the trait `std::marker::Send` is not implemented for `std::sync::MutexGuard&amp;lt;&apos;_, i32&amp;gt;`
note: future is not `Send` as this value is used across an await
   --&amp;gt; src/lib.rs:7:5
    |
4   |     let mut lock: MutexGuard&amp;lt;i32&amp;gt; = mutex.lock().unwrap();
    |         -------- has type `std::sync::MutexGuard&amp;lt;&apos;_, i32&amp;gt;` which is not `Send`
...
7   |     do_something_async().await;
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^ await occurs here, with `mut lock` maybe used later
8   | }
    | - `mut lock` is later dropped here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是因为&lt;code&gt;std::sync::MutexGuard&lt;/code&gt;类型不可&lt;code&gt;Send&lt;/code&gt;，即你不能将互斥锁发送到另一个线程，于是这将导致错误，因为Tokio运行时可以在每个&lt;code&gt;.await&lt;/code&gt;时在线程间移动任务。为避免这种情况，你应该重组代码，使互斥锁在&lt;code&gt;.await&lt;/code&gt;之前析构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// This works!
async fn increment_and_do_stuff(mutex: &amp;amp;Mutex&amp;lt;i32&amp;gt;) {
    {
        let mut lock: MutexGuard&amp;lt;i32&amp;gt; = mutex.lock().unwrap();
        *lock += 1;
    } // lock goes out of scope here

    do_something_async().await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意这样也不行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::sync::{Mutex, MutexGuard};

// This fails too.
async fn increment_and_do_stuff(mutex: &amp;amp;Mutex&amp;lt;i32&amp;gt;) {
    let mut lock: MutexGuard&amp;lt;i32&amp;gt; = mutex.lock().unwrap();
    *lock += 1;
    drop(lock);

    do_something_async().await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前，编译器仅根据作用域推断future是否为&lt;code&gt;Send&lt;/code&gt;。将来，编译器可能会支持显式析构，但现在，您只能使用显式作用域。&lt;/p&gt;
&lt;p&gt;请注意，这里讨论的错误也在&lt;a href=&quot;.#Send%E9%99%90%E5%88%B6&quot;&gt;并发一章的&lt;code&gt;Send&lt;/code&gt;限制小节&lt;/a&gt;中讨论过。&lt;/p&gt;
&lt;p&gt;如果您想尝试某种方式，通过生成不需要&lt;code&gt;Send&lt;/code&gt;的并发任务来绕过此问题，是不可行的。因为如果Tokio在任务持有锁时将任务挂起在&lt;code&gt;.await&lt;/code&gt;处，则可能会在同一线程上调度运行其他任务，而这里的其他任务也可能尝试获取该互斥锁，这将导致死锁，即等待获取互斥锁的任务会阻塞持有互斥锁的任务释放锁。&lt;/p&gt;
&lt;p&gt;下面我们将讨论一些上述错误消息的方法：&lt;/p&gt;
&lt;h3&gt;重构你的代码，不跨&lt;code&gt;.await&lt;/code&gt;持锁&lt;/h3&gt;
&lt;p&gt;我们已经在上面的代码片段中看到了一个例子，但是还有一些更健壮的代码实现。例如，你可以将互斥锁包装在一个结构体中，并只在非异步方法中锁定互斥锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::sync::Mutex;

struct CanIncrement {
    mutex: Mutex&amp;lt;i32&amp;gt;,
}
impl CanIncrement {
    // This function is not marked async.
    fn increment(&amp;amp;self) {
        let mut lock = self.mutex.lock().unwrap();
        *lock += 1;
    }
}

async fn increment_and_do_stuff(can_incr: &amp;amp;CanIncrement) {
    can_incr.increment();
    do_something_async().await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种模式保证你不会遇到&lt;code&gt;Send&lt;/code&gt;错误，因为&lt;code&gt;MutexGuard&lt;/code&gt;不会出现在异步函数中。&lt;/p&gt;
&lt;h3&gt;生成一个任务来管理状态并使用消息传递对其进行操作&lt;/h3&gt;
&lt;p&gt;这是本章开头提到的第二种方法，通常在共享的资源是I/O资源时使用。有关详细信息，请参阅下一章。&lt;/p&gt;
&lt;h3&gt;使用Tokio的异步互斥锁&lt;/h3&gt;
&lt;p&gt;也可以使用Tokio提供的&lt;code&gt;tokio::sync::Mutex&lt;/code&gt;类型。Tokio互斥锁的主要特性是它可以跨&lt;code&gt;.await&lt;/code&gt; 保存而不会出现任何问题。也就是说，异步互斥锁比普通互斥锁开销更大，因此一般建议从前面两种方法中选一种实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::Mutex; // note! This uses the Tokio mutex

// This compiles!
// (but restructuring the code would be better in this case)
async fn increment_and_do_stuff(mutex: &amp;amp;Mutex&amp;lt;i32&amp;gt;) {
    let mut lock = mutex.lock().await;
    *lock += 1;

    do_something_async().await;
} // lock goes out of scope here
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6 消息通道&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;现在我们已经了解了一些关于Tokio的并发性，现在让我们在客户端上应用它。把我们之前写的服务端代码放到一个显式的二进制文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir src/bin
mv src/main.rs src/bin/server.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建一个包含客户端代码的新二进制文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch src/bin/client.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在此文件中，您将编写本章的代码。要记住在运行客户端前，必须首先在单独的终端中启动服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo run --bin server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再用&lt;strong&gt;单独&lt;/strong&gt;的终端里运行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo run --bin client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们开始编码！&lt;/p&gt;
&lt;p&gt;假设我们要并发执行两个Redis命令。我们可以为每个命令生成一个任务，然后这两个命令将同时发生。&lt;/p&gt;
&lt;p&gt;起初，我们可能会进行如下尝试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use mini_redis::client;

#[tokio::main]
async fn main() {
    // Establish a connection to the server
    let mut client = client::connect(&quot;127.0.0.1:6379&quot;).await.unwrap();

    // Spawn two tasks, one gets a key, the other sets a key
    let t1 = tokio::spawn(async {
        let res = client.get(&quot;hello&quot;).await;
    });

    let t2 = tokio::spawn(async {
        client.set(&quot;foo&quot;, &quot;bar&quot;.into()).await;
    });

    t1.await.unwrap();
    t2.await.unwrap();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码无法通过编译，因为这两个任务都需要以某种方式访问&lt;code&gt;client&lt;/code&gt;。由于&lt;code&gt;Client&lt;/code&gt;没有实现&lt;code&gt;Copy&lt;/code&gt;，所以如果没有一些代码来实现共享，就无法编译。此外，&lt;code&gt;Client::set&lt;/code&gt;采用&lt;code&gt;&amp;amp;mut self&lt;/code&gt;，这意味着需要互斥存取才能调用。我们可以为每个任务打开一个连接，但这并不理想。我们不能使用&lt;code&gt;std::sync::Mutex&lt;/code&gt;因为&lt;code&gt;.await&lt;/code&gt;需要在持有锁的情况下调用。我们可以使用&lt;code&gt;tokio::sync::Mutex&lt;/code&gt;，但这只会允许一个运行中的请求。如果客户端实现了&lt;a href=&quot;https://redis.io/topics/pipelining&quot;&gt;流水线&lt;/a&gt;，异步互斥锁还将导致连接不能被充分利用。&lt;/p&gt;
&lt;h2&gt;消息传递&lt;/h2&gt;
&lt;p&gt;解决方案是使用消息传递。该模式需要生成一个专门的任务来管理&lt;code&gt;client&lt;/code&gt;资源。任何希望发出请求的任务，都会向&lt;code&gt;client&lt;/code&gt;任务发送消息。&lt;code&gt;client&lt;/code&gt;任务代理发送者发出请求，并将响应返回给发送者。&lt;/p&gt;
&lt;p&gt;使用此策略，可以只建立一个连接。&lt;code&gt;client&lt;/code&gt;管理任务能够获得互斥存取权限，以便调用&lt;code&gt;get&lt;/code&gt;和&lt;code&gt;set&lt;/code&gt;。此外，通道也起到了缓冲区的作用。即使&lt;code&gt;client&lt;/code&gt;任务忙时，操作也可能会被发送到&lt;code&gt;client&lt;/code&gt;任务。当&lt;code&gt;client&lt;/code&gt;任务可用于处理新请求时，它就会从通道中获取下一个请求。这可以带来更好的吞吐量，并可扩展以支持连接池。&lt;/p&gt;
&lt;h2&gt;Tokio的通道原语&lt;/h2&gt;
&lt;p&gt;Tokio提供了&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/index.html&quot;&gt;许多通道&lt;/a&gt;，每个通道都有不同的用途。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/mpsc/index.html&quot;&gt;mpsc&lt;/a&gt;：多生产者，单消费者通道。可以发送许多值。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/oneshot/index.html&quot;&gt;oneshot&lt;/a&gt;：单生产者，单消费者渠道。可以发送单个值。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/broadcast/index.html&quot;&gt;broadcast&lt;/a&gt;：多生产者，多消费者。可以发送许多值，每个消费者都能看到每个值。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/watch/index.html&quot;&gt;watch&lt;/a&gt;：单生产者，多消费者。可以发送许多值，但不保留任何历史记录。消费者只能看到最新的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果您需要一个多生产者多消费者通道，其中只有一个消费者可以看到每条消息，您可以使用&lt;a href=&quot;https://docs.rs/async-channel/&quot;&gt;&lt;code&gt;async-channel&lt;/code&gt;&lt;/a&gt; crate。在异步Rust之外还有一些通道可以使用，例如&lt;a href=&quot;https://doc.rust-lang.org/stable/std/sync/mpsc/index.html&quot;&gt;&lt;code&gt;std::sync::mpsc&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/crossbeam/latest/crossbeam/channel/index.html&quot;&gt;&lt;code&gt;crossbeam::channel&lt;/code&gt;&lt;/a&gt;。这些通道在等待消息时将阻塞线程，这在异步代码中是不允许的。&lt;/p&gt;
&lt;p&gt;在本节中，我们将使用&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/mpsc/index.html&quot;&gt;mpsc&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/oneshot/index.html&quot;&gt;oneshot&lt;/a&gt;，其他类型的消息传递通道将在后面的章节中探讨。你可在&lt;a href=&quot;https://github.com/tokio-rs/website/blob/master/tutorial-code/channels/src/main.rs&quot;&gt;这里&lt;/a&gt;找到本节的完整代码。&lt;/p&gt;
&lt;h2&gt;定义消息类型&lt;/h2&gt;
&lt;p&gt;在大多数情况下，当使用消息传递时，接收消息的任务会响应多个命令。在我们的例子中，任务将响应&lt;code&gt;GET&lt;/code&gt;和&lt;code&gt;SET&lt;/code&gt;命令。为了对此建模，我们首先定义一个&lt;code&gt;Command&lt;/code&gt;枚举并为每个命令指定一个成员变量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bytes::Bytes;

#[derive(Debug)]
enum Command {
    Get {
        key: String,
    },
    Set {
        key: String,
        val: Bytes,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建通道&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;main&lt;/code&gt;函数中新建一个&lt;code&gt;mpsc&lt;/code&gt;通道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    // Create a new channel with a capacity of at most 32.
    let (tx, mut rx) = mpsc::channel(32);

    // ... Rest comes here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mpsc&lt;/code&gt;通道用于向管理redis连接的任务发送命令，多生产者特性允许从多个任务发送消息。创建通道返回两个值，一个发送端和一个接收端，两个句柄各有用处。他们可能会被转移到不同的任务中。&lt;/p&gt;
&lt;p&gt;创建通道的容量为32，如果消息的发送速度快于接收速度，通道将存储它们。一旦有32条消息存储在通道中，调用&lt;code&gt;send(...).await&lt;/code&gt;就将进入睡眠状态，直到接收方取走了一条消息为止。&lt;/p&gt;
&lt;p&gt;从多个任务发送是通过&lt;strong&gt;克隆&lt;/strong&gt;&lt;code&gt;Sender&lt;/code&gt;来完成的。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);
    let tx2 = tx.clone();

    tokio::spawn(async move {
        tx.send(&quot;sending from first handle&quot;).await;
    });

    tokio::spawn(async move {
        tx2.send(&quot;sending from second handle&quot;).await;
    });

    while let Some(message) = rx.recv().await {
        println!(&quot;GOT = {}&quot;, message);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两条消息都发送到同一个&lt;code&gt;Receiver&lt;/code&gt;句柄。&lt;code&gt;mpsc&lt;/code&gt;通道的接收端无法被克隆。&lt;/p&gt;
&lt;p&gt;当所有&lt;code&gt;Sender&lt;/code&gt;超出作用域或是被析构时，就无法再向通道发送更多消息。此时，在&lt;code&gt;Receiver&lt;/code&gt;上调用&lt;code&gt;recv&lt;/code&gt;会返回&lt;code&gt;None&lt;/code&gt;，表示所有发送端都不在了，通道已关闭。&lt;/p&gt;
&lt;p&gt;在我们管理Redis连接的任务中，当通道关闭时该任务就将关闭Redis连接，因为该连接将不再被使用。&lt;/p&gt;
&lt;h2&gt;生成管理任务&lt;/h2&gt;
&lt;p&gt;接下来，生成一个专门处理通道的消息的任务。首先，客户端建立到Redis服务端的连接；然后，通过Redis连接发送收到的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use mini_redis::client;
// The `move` keyword is used to **move** ownership of `rx` into the task.
let manager = tokio::spawn(async move {
    // Establish a connection to the server
    let mut client = client::connect(&quot;127.0.0.1:6379&quot;).await.unwrap();

    // Start receiving messages
    while let Some(cmd) = rx.recv().await {
        use Command::*;

        match cmd {
            Get { key } =&amp;gt; {
                client.get(&amp;amp;key).await;
            }
            Set { key, val } =&amp;gt; {
                client.set(&amp;amp;key, val).await;
            }
        }
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在可以修改任务，使用通道发送命令，以代替直接使用Redis连接发送命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// The `Sender` handles are moved into the tasks. As there are two
// tasks, we need a second `Sender`.
let tx2 = tx.clone();

// Spawn two tasks, one gets a key, the other sets a key
let t1 = tokio::spawn(async move {
    let cmd = Command::Get {
        key: &quot;hello&quot;.to_string(),
    };

    tx.send(cmd).await.unwrap();
});

let t2 = tokio::spawn(async move {
    let cmd = Command::Set {
        key: &quot;foo&quot;.to_string(),
        val: &quot;bar&quot;.into(),
    };

    tx2.send(cmd).await.unwrap();
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;main&lt;/code&gt;函数最后，在join句柄上使用&lt;code&gt;.await&lt;/code&gt;，以确保命令在进程退出前能够执行完毕。&lt;/p&gt;
&lt;h2&gt;接收响应&lt;/h2&gt;
&lt;p&gt;最后一步是从管理任务接收响应。&lt;code&gt;GET&lt;/code&gt;命令需要获取值，而&lt;code&gt;SET&lt;/code&gt;命令需要知道操作是否成功完成。&lt;/p&gt;
&lt;p&gt;我们使用&lt;code&gt;oneshot&lt;/code&gt;通道，以传递响应。&lt;code&gt;oneShot&lt;/code&gt;通道是一种单次生产、单次消费的通道，专门针对对发送单个值的场景进行了优化。在我们的例子中，这单个值就是响应。&lt;/p&gt;
&lt;p&gt;和&lt;code&gt;MPSC&lt;/code&gt;类似，&lt;code&gt;oneShot::Channel()&lt;/code&gt;返回一对发送端、接收端句柄。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::oneshot;

let (tx, rx) = oneshot::channel();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与&lt;code&gt;mpsc&lt;/code&gt;不同的是，它无需指定通道数量，因为始终是一，此外，这两个句柄都不能克隆。&lt;/p&gt;
&lt;p&gt;要从管理任务接收响应，在发送命令之前，就应创建&lt;code&gt;oneshot&lt;/code&gt;通道。通道的发送端包含在命令中传给管理任务，接收端则用于接收响应结果。&lt;/p&gt;
&lt;p&gt;首先，修改&lt;code&gt;Command&lt;/code&gt;以包含&lt;code&gt;Sender&lt;/code&gt;。为了方便起见，使用类型别名来指代&lt;code&gt;Sender&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::oneshot;
use bytes::Bytes;

/// Multiple different commands are multiplexed over a single channel.
#[derive(Debug)]
enum Command {
    Get {
        key: String,
        resp: Responder&amp;lt;Option&amp;lt;Bytes&amp;gt;&amp;gt;,
    },
    Set {
        key: String,
        val: Bytes,
        resp: Responder&amp;lt;()&amp;gt;,
    },
}

/// Provided by the requester and used by the manager task to send
/// the command response back to the requester.
type Responder&amp;lt;T&amp;gt; = oneshot::Sender&amp;lt;mini_redis::Result&amp;lt;T&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，修改管理任务以能够发送包含&lt;code&gt;oneshot::Sender&lt;/code&gt;的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let t1 = tokio::spawn(async move {
    let (resp_tx, resp_rx) = oneshot::channel();
    let cmd = Command::Get {
        key: &quot;hello&quot;.to_string(),
        resp: resp_tx,
    };

    // Send the GET request
    tx.send(cmd).await.unwrap();

    // Await the response
    let res = resp_rx.await;
    println!(&quot;GOT = {:?}&quot;, res);
});

let t2 = tokio::spawn(async move {
    let (resp_tx, resp_rx) = oneshot::channel();
    let cmd = Command::Set {
        key: &quot;foo&quot;.to_string(),
        val: &quot;bar&quot;.into(),
        resp: resp_tx,
    };

    // Send the SET request
    tx2.send(cmd).await.unwrap();

    // Await the response
    let res = resp_rx.await;
    println!(&quot;GOT = {:?}&quot;, res);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，修改管理任务，以使用&lt;code&gt;oneshot&lt;/code&gt;通道返回响应。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while let Some(cmd) = rx.recv().await {
    match cmd {
        Command::Get { key, resp } =&amp;gt; {
            let res = client.get(&amp;amp;key).await;
            // Ignore errors
            let _ = resp.send(res);
        }
        Command::Set { key, val, resp } =&amp;gt; {
            let res = client.set(&amp;amp;key, val).await;
            // Ignore errors
            let _ = resp.send(res);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用&lt;code&gt;oneshot::Sender&lt;/code&gt;的&lt;code&gt;send&lt;/code&gt;会立即完成，&lt;strong&gt;不需要&lt;/strong&gt;使用&lt;code&gt;.await&lt;/code&gt;。这是因为在&lt;code&gt;oneshot&lt;/code&gt;通道上&lt;code&gt;send&lt;/code&gt;总是立刻失败或成功，不会有任何等待。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;oneshot&lt;/code&gt;通道上发送值时，如果接收端被析构则将返回&lt;code&gt;Err&lt;/code&gt;，以表明接收端不再关注响应。在我们的例子中，接收端可以不接收响应，因此无需处理&lt;code&gt;resp.send(...)&lt;/code&gt;返回的&lt;code&gt;Err&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://github.com/tokio-rs/website/blob/master/tutorial-code/channels/src/main.rs&quot;&gt;这里&lt;/a&gt;找到完整代码。&lt;/p&gt;
&lt;h2&gt;背压调节和通道限制&lt;/h2&gt;
&lt;p&gt;当引入并发或队列时，一定要确保队列是有限的，以使系统能优雅地处理负载。无限的队列最终将耗尽所有可用的内存，并导致系统以不可预测的方式宕机。&lt;/p&gt;
&lt;p&gt;Tokio能够避免定义不明确的队列，主要也是因为Tokio中的异步操作是惰性求值。考虑以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loop {
    async_op();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果异步操作立即求值，则此循环会不停的将新的&lt;code&gt;async_op&lt;/code&gt;排入队列，且并不关心之前的操作是否完成，如此则会隐式的产生无限制队列。基于回调和立即求future的系统尤其容易受到上述影响。&lt;/p&gt;
&lt;p&gt;但是，使用Tokio和异步Rust，上述代码片段的&lt;code&gt;async_op&lt;/code&gt;根本不会运行，因为&lt;code&gt;.await&lt;/code&gt;从未被调用。如果修改上述代码，加入&lt;code&gt;.await&lt;/code&gt;，则循环会等待操作完成才进入下一次循环。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loop {
    // Will not repeat until `async_op` completes
    async_op().await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用并发和队列时必须明确引入。这样做的方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tokio::Spawn&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;select!&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;join!&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mpsc::channel&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样做时，请注意确保并发的总数有上限。例如，在编写接受TCP连接的循环时，请确保打开的sockets的总数有上限。使用&lt;code&gt;mpsc::channel&lt;/code&gt;时，选择合适的通道容量。针对特定的应用程序选择特定的约束限制。&lt;/p&gt;
&lt;p&gt;编写可靠的Tokio应用程序的一个关键，就是留意并挑选良好的限制值。&lt;/p&gt;
&lt;h1&gt;7 I/O&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;Tokio中的I/O以与&lt;code&gt;std&lt;/code&gt;中的运行方式几乎一样，只不过是异步的。有一个读trait（&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncRead.html&quot;&gt;&lt;code&gt;AsyncRead&lt;/code&gt;&lt;/a&gt;）和一个写trait（&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncWrite.html&quot;&gt;&lt;code&gt;AsyncWrite&lt;/code&gt;&lt;/a&gt;）。一些类型已经良好的地实现了这些trait（&lt;a href=&quot;https://docs.rs/tokio/1/tokio/net/struct.TcpStream.html&quot;&gt;&lt;code&gt;TCPStream&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://docs.rs/tokio/1/tokio/fs/struct.File.html&quot;&gt;&lt;code&gt;File&lt;/code&gt;&lt;/a&gt;，&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/struct.Stdout.html&quot;&gt;&lt;code&gt;Stdout&lt;/code&gt;&lt;/a&gt;）。许多数据结构也实现了&lt;code&gt;AsyncRead&lt;/code&gt;和&lt;code&gt;AsyncWrite&lt;/code&gt;（如&lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;/code&gt;和&lt;code&gt;&amp;amp;[u8]&lt;/code&gt;），这使得我们可以在需要用到reader和writer的地方使用字节数组。&lt;/p&gt;
&lt;p&gt;本章将通过一些例子演示使用Tokio时的基本I/O读写操作。下一章江更加深入的介绍高级的I/O示例。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;AsyncRead&lt;/code&gt;和&lt;code&gt;AsyncWrite&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;这两个trait为异步读写字节流提供了基础工具。我们通常不直接调用这些trait上的方法，类似于你不会手动调用&lt;code&gt;future&lt;/code&gt; trait的&lt;code&gt;poll&lt;/code&gt;方法，而是使用&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncReadExt.html&quot;&gt;&lt;code&gt;AsyncReadExt&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncWriteExt.html&quot;&gt;&lt;code&gt;AsyncWriteExt&lt;/code&gt;&lt;/a&gt;提供的工具方法。&lt;/p&gt;
&lt;p&gt;让我们简要看一下其中的一些方法，所有这些函数都是异步的，必须与配合&lt;code&gt;.await&lt;/code&gt;使用。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;async fn read()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;AsyncReadExt::read&lt;/code&gt;提供了一种将数据读取到缓冲区的异步方法，返回值为读到的字节数。&lt;/p&gt;
&lt;p&gt;注意：当&lt;code&gt;read()&lt;/code&gt;返回&lt;code&gt;Ok(0)&lt;/code&gt;时，意味着该流已关闭。任何调用&lt;code&gt;read()&lt;/code&gt;的future江立即完成并返回&lt;code&gt;Ok(0)&lt;/code&gt;。比如对&lt;a href=&quot;https://docs.rs/tokio/1/tokio/net/struct.TcpStream.html&quot;&gt;&lt;code&gt;TcpStream&lt;/code&gt;&lt;/a&gt;的实例，这表示socket的读端已关闭。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::fs::File;
use tokio::io::{self, AsyncReadExt};

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut f = File::open(&quot;foo.txt&quot;).await?;
    let mut buffer = [0; 10];

    // read up to 10 bytes
    let n = f.read(&amp;amp;mut buffer[..]).await?;

    println!(&quot;The bytes: {:?}&quot;, &amp;amp;buffer[..n]);
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;async fn read_to_end()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncReadExt.html#method.read_to_end&quot;&gt;&lt;code&gt;AsyncReadExt::read_to_end&lt;/code&gt;&lt;/a&gt;将读取流中的所有字节，直到遇到EOF。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::{self, AsyncReadExt};
use tokio::fs::File;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut f = File::open(&quot;foo.txt&quot;).await?;
    let mut buffer = Vec::new();

    // read the whole file
    f.read_to_end(&amp;amp;mut buffer).await?;
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;async fn write()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncWriteExt.html#method.write&quot;&gt;&lt;code&gt;AsyncWriteExt::write&lt;/code&gt;&lt;/a&gt;将一个缓冲区写入writer，返回值为写入的字节数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::{self, AsyncWriteExt};
use tokio::fs::File;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut file = File::create(&quot;foo.txt&quot;).await?;

    // Writes some prefix of the byte string, but not necessarily all of it.
    let n = file.write(b&quot;some bytes&quot;).await?;

    println!(&quot;Wrote the first {} bytes of &apos;some bytes&apos;.&quot;, n);
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;async fn write_all()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncWriteExt.html#method.write_all&quot;&gt;&lt;code&gt;AsyncWriteExt::write_all&lt;/code&gt;&lt;/a&gt;将整个缓冲区写入writer。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::{self, AsyncWriteExt};
use tokio::fs::File;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut file = File::create(&quot;foo.txt&quot;).await?;

    file.write_all(b&quot;some bytes&quot;).await?;
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两个trait都包括许多其他有用的方法，详情请参阅API文档。&lt;/p&gt;
&lt;h2&gt;帮助函数&lt;/h2&gt;
&lt;p&gt;此外，就像&lt;code&gt;std&lt;/code&gt;，&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/index.html&quot;&gt;&lt;code&gt;tokio::io&lt;/code&gt;&lt;/a&gt;模块也包含许多实用的工具函数，以及用于&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/fn.stdin.html&quot;&gt;标准输入&lt;/a&gt;、&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/fn.stdout.html&quot;&gt;标准输出&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/fn.stderr.html&quot;&gt;标准错误&lt;/a&gt;的API。例如，使用&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/fn.copy.html&quot;&gt;&lt;code&gt;tokio::io::copy&lt;/code&gt;&lt;/a&gt;异步将读端的所有内容复制到写端。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::fs::File;
use tokio::io;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let mut reader: &amp;amp;[u8] = b&quot;hello&quot;;
    let mut file = File::create(&quot;foo.txt&quot;).await?;

    io::copy(&amp;amp;mut reader, &amp;amp;mut file).await?;
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;留意到上例实际上利用了实现了&lt;code&gt;AsyncRead&lt;/code&gt;的字节数组。&lt;/p&gt;
&lt;h2&gt;回音服务端&lt;/h2&gt;
&lt;p&gt;让我们上手试一下异步I/O，来编写一个简单的回音服务端。&lt;/p&gt;
&lt;p&gt;回音服务端绑定一个&lt;code&gt;TcpListener&lt;/code&gt;，使用一个循环接收入站连接。对于每个入站连接，从socket读取数据，然后立即将读到的数据写回socket。客户端将数据发送到服务器，然后接收完全相同的数据。&lt;/p&gt;
&lt;p&gt;我们将使用两种稍微不同的策略分别实现回音服务端。&lt;/p&gt;
&lt;h3&gt;使用&lt;code&gt;io::copy()&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;首先，我们将使用&lt;code&gt;io:copy&lt;/code&gt;实现回音逻辑。&lt;/p&gt;
&lt;p&gt;您可以在新的二进制文件中写入此代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch src/bin/echo-server-copy.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;您可以通过以下方式启动（或仅检查编译）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo run --bin echo-server-copy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;您将可以直接使用标准命令行工具（例如&lt;code&gt;telnet&lt;/code&gt;）测试服务端，或使用&lt;a href=&quot;https://docs.rs/tokio/1/tokio/net/struct.TcpStream.html#examples&quot;&gt;&lt;code&gt;tokio::net::TcpStream&lt;/code&gt;&lt;/a&gt;的文档中示例的简单客户测试服务端。&lt;/p&gt;
&lt;p&gt;TCP服务端使用循环接受请求，每接受一个socket就生成一个新任务。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io;
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let listener = TcpListener::bind(&quot;127.0.0.1:6142&quot;).await?;

    loop {
        let (mut socket, _) = listener.accept().await?;

        tokio::spawn(async move {
            // Copy data here
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之前提到过，该工具函数将接受一个读端和一个写端，并将数据从一端拷贝到另一端，但此处我们只有一个&lt;code&gt;Tcpstream&lt;/code&gt;，而这一个值同时实现了&lt;code&gt;AsyncRead&lt;/code&gt;和&lt;code&gt;AsyncWrite&lt;/code&gt;。由于&lt;code&gt;io::copy&lt;/code&gt;要求读端和写端均为&lt;code&gt;&amp;amp;mut&lt;/code&gt;，如果这两个参数都写成socket的话就不能通过编译。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// This fails to compile
io::copy(&amp;amp;mut socket, &amp;amp;mut socket).await
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;拆分读端和写端&lt;/h3&gt;
&lt;p&gt;要解决问题，我们需要将socket拆分为读句柄和写句柄，特定的类型有特定的最佳读/写端拆分方式。&lt;/p&gt;
&lt;p&gt;任何读端+写端的类型，均可用&lt;code&gt;io::split&lt;/code&gt;工具函数拆分，函数接受一个参数，返回一对读句柄/写句柄。这两个句柄可以独立使用，比如用在不同的任务中。&lt;/p&gt;
&lt;p&gt;例如，回音客户端可以按如下的方式处理的并发读取：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let socket = TcpStream::connect(&quot;127.0.0.1:6142&quot;).await?;
    let (mut rd, mut wr) = io::split(socket);

    // Write data in the background
    tokio::spawn(async move {
        wr.write_all(b&quot;hello\r\n&quot;).await?;
        wr.write_all(b&quot;world\r\n&quot;).await?;

        // Sometimes, the rust type inferencer needs
        // a little help
        Ok::&amp;lt;_, io::Error&amp;gt;(())
    });

    let mut buf = vec![0; 128];

    loop {
        let n = rd.read(&amp;amp;mut buf).await?;

        if n == 0 {
            break;
        }

        println!(&quot;GOT {:?}&quot;, &amp;amp;buf[..n]);
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;io::split&lt;/code&gt;能够接受&lt;strong&gt;任何&lt;/strong&gt;实现了&lt;code&gt;AsyncRead + AsyncWrite&lt;/code&gt;的类型并返回独立的句柄，是因为&lt;code&gt;io::split&lt;/code&gt;内部使用一个&lt;code&gt;Arc&lt;/code&gt;和一个&lt;code&gt;Mutex&lt;/code&gt;。我们可以使用&lt;code&gt;TcpStream&lt;/code&gt;避免使用此开销。&lt;code&gt;TcpStream&lt;/code&gt;提供了两个专用的拆分函数。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/net/struct.TcpStream.html#method.split&quot;&gt;&lt;code&gt;TcpStream::split&lt;/code&gt;&lt;/a&gt;接受一个流的&lt;strong&gt;引用&lt;/strong&gt;，返回一对读/写句柄。由于使用了引用，因此两个句柄都必须&lt;strong&gt;呆在这个&lt;/strong&gt;调用&lt;code&gt;split()&lt;/code&gt;的任务中。这种专用拆分为零成本，不需要&lt;code&gt;Arc&lt;/code&gt;或&lt;code&gt;Mutex&lt;/code&gt;。&lt;code&gt;TcpStream&lt;/code&gt;还提供了&lt;code&gt;into_split&lt;/code&gt;函数，以仅使用一个&lt;code&gt;Arc&lt;/code&gt;为代价，让句柄能够在任务间移动。&lt;/p&gt;
&lt;p&gt;由于调用&lt;code&gt;io::copy()&lt;/code&gt;的任务就拥有&lt;code&gt;TcpStream&lt;/code&gt;的所有权，因此我们可以使用&lt;code&gt;TcpStream::split&lt;/code&gt;。服务端中处理回音逻辑的任务变成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tokio::spawn(async move {
    let (mut rd, mut wr) = socket.split();
    
    if io::copy(&amp;amp;mut rd, &amp;amp;mut wr).await.is_err() {
        eprintln!(&quot;failed to copy&quot;);
    }
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://github.com/tokio-rs/website/blob/master/tutorial-code/io/src/echo-server-copy.rs&quot;&gt;这里&lt;/a&gt;找到完整的代码。&lt;/p&gt;
&lt;h3&gt;手动拷贝&lt;/h3&gt;
&lt;p&gt;现在，让我们来了解一下如何通过手动复制数据的方式编写回音服务端。这里，我们需要使用&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncReadExt.html#method.read&quot;&gt;&lt;code&gt;AsyncReadExt::read&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncWriteExt.html#method.write_all&quot;&gt;&lt;code&gt;AsyncWriteExt::write_all&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;完整的回音服务端如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let listener = TcpListener::bind(&quot;127.0.0.1:6142&quot;).await?;

    loop {
        let (mut socket, _) = listener.accept().await?;

        tokio::spawn(async move {
            let mut buf = vec![0; 1024];

            loop {
                match socket.read(&amp;amp;mut buf).await {
                    // Return value of `Ok(0)` signifies that the remote has
                    // closed
                    Ok(0) =&amp;gt; return,
                    Ok(n) =&amp;gt; {
                        // Copy the data back to socket
                        if socket.write_all(&amp;amp;buf[..n]).await.is_err() {
                            // Unexpected socket error. There isn&apos;t much we can
                            // do here so just stop processing.
                            return;
                        }
                    }
                    Err(_) =&amp;gt; {
                        // Unexpected socket error. There isn&apos;t much we can do
                        // here so just stop processing.
                        return;
                    }
                }
            }
        });
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（您可以将此代码放入&lt;code&gt;src/bin/echo-server.rs&lt;/code&gt;中，并使用&lt;code&gt;cargo run --bin echo-server&lt;/code&gt;运行）。&lt;/p&gt;
&lt;p&gt;我们来逐行理解代码。首先，由于使用&lt;code&gt;AsyncRead&lt;/code&gt;和&lt;code&gt;AsyncWrite&lt;/code&gt;实用工具，因此必须引入这些扩展trait。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分配缓冲区&lt;/h3&gt;
&lt;p&gt;我们的策略是将一些从socket中读取的数据放入缓冲区，然后将缓冲区的内容写回到socket中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut buf = vec![0; 1024];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里显式禁止使用栈缓冲区。回想&lt;a href=&quot;.#Send%E9%99%90%E5%88%B6&quot;&gt;前面&lt;/a&gt;，我们注意到，跨&lt;code&gt;.await&lt;/code&gt;调用存活的所有任务数据都必须保存在任务中。上面的代码中，&lt;code&gt;buf&lt;/code&gt;跨&lt;code&gt;.await&lt;/code&gt;调用使用。所有任务数据都存储在这单个分配中。您可以将其视为一个&lt;code&gt;enum&lt;/code&gt;，其中每个成员变量都是需要保存在任务中以便特定的&lt;code&gt;.await&lt;/code&gt;调用的数据。&lt;/p&gt;
&lt;p&gt;如果缓冲区是一个栈数组，则为每个接受的socket生成的任务内部结构可能看起来像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct Task {
    // internal task fields here
    task: enum {
        AwaitingRead {
            socket: TcpStream,
            buf: [BufferType],
        },
        AwaitingWriteAll {
            socket: TcpStream,
            buf: [BufferType],
        }

    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果将栈数组当做缓冲区，则它将&lt;em&gt;内联&lt;/em&gt;存储在任务结构体中。这将使任务结构体变得非常大。另外，缓冲的大小通常是页大小，于是，这将使任务大小变得很尴尬：&lt;code&gt;$页大小 + 一些额外字节&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当然，编译器将异步代码块进行了优化，性能远超基本的&lt;code&gt;enmu&lt;/code&gt;。实际上，变量并不会像&lt;code&gt;enum&lt;/code&gt;可能需要的那样，在成员间移动。但是，任务结构体大小至少与最大的变量一样大。&lt;/p&gt;
&lt;p&gt;因此，为缓冲区使用专用的分配通常能更加高效。&lt;/p&gt;
&lt;h3&gt;处理EOF&lt;/h3&gt;
&lt;p&gt;当关闭了TCP流的读端后，再调用&lt;code&gt;read()&lt;/code&gt;将返回&lt;code&gt;Ok(0)&lt;/code&gt;，此时必须让读循环终止。忘记在已经EOF的内容上继续循环读取，是一个常见的错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;loop {
    match socket.read(&amp;amp;mut buf).await {
        // Return value of `Ok(0)` signifies that the remote has
        // closed
        Ok(0) =&amp;gt; return,
        // ... other cases handled here
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;忘记停止读循环，通常会导致CPU使用率100％的无限循环。当socket关闭时，&lt;code&gt;socket.read()&lt;/code&gt;将立即返回，然后不停的重复循环。&lt;/p&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://github.com/tokio-rs/website/blob/master/tutorial-code/io/src/echo-server.rs&quot;&gt;这里&lt;/a&gt;找到完整的代码。&lt;/p&gt;
&lt;h1&gt;8 组帧&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;现在，我们将使用我们刚刚学到的I/O相关知识，实现Mini-Redis组帧层。组帧是获取字节流并将其转换为帧流的过程，帧是两节点间传输数据的单位。Redis协议帧定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bytes::Bytes;

enum Frame {
    Simple(String),
    Error(String),
    Integer(u64),
    Bulk(Bytes),
    Null,
    Array(Vec&amp;lt;Frame&amp;gt;),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请留意该帧定义是如何使用没有语义的数据组成的，命令解析和实现位于更上层。&lt;/p&gt;
&lt;p&gt;对于HTTP，帧可能看起来像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum HttpFrame {
    RequestHead {
        method: Method,
        uri: Uri,
        version: Version,
        headers: HeaderMap,
    },
    ResponseHead {
        status: StatusCode,
        version: Version,
        headers: HeaderMap,
    },
    BodyChunk {
        chunk: Bytes,
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了实现Mini-Redis的帧，我们将实现&lt;code&gt;Connection&lt;/code&gt;结构体，里面封装了一个&lt;code&gt;TcpStream&lt;/code&gt;用以读/写&lt;code&gt;mini_redis::Frame&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpStream;
use mini_redis::{Frame, Result};

struct Connection {
    stream: TcpStream,
    // ... other fields here
}

impl Connection {
    /// Read a frame from the connection.
    /// 
    /// Returns `None` if EOF is reached
    pub async fn read_frame(&amp;amp;mut self)
        -&amp;gt; Result&amp;lt;Option&amp;lt;Frame&amp;gt;&amp;gt;
    {
        // implementation here
    }

    /// Write a frame to the connection.
    pub async fn write_frame(&amp;amp;mut self, frame: &amp;amp;Frame)
        -&amp;gt; Result&amp;lt;()&amp;gt;
    {
        // implementation here
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://redis.io/topics/protocol&quot;&gt;这里&lt;/a&gt;找到Redis Wire协议的详细信息，可以在&lt;a href=&quot;https://github.com/tokio-rs/mini-redis/blob/tutorial/src/connection.rs&quot;&gt;这里&lt;/a&gt;找到完整的代码。&lt;/p&gt;
&lt;h2&gt;缓冲读&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;read_frame&lt;/code&gt;方法会等待收到整个帧才返回，而调用一次&lt;code&gt;TcpStream::read()&lt;/code&gt;返回的数据长度不确定，可能包含整个帧，或是部分帧，也可能包含了多个帧。如果收到的是部分帧，则应缓冲数据并从socket中读取更多数据。如果收到的是多个帧，则应返回第一帧，缓冲其余数据，以等待下一次&lt;code&gt;read_frame&lt;/code&gt;调用。&lt;/p&gt;
&lt;p&gt;为了实现上述功能，需要给&lt;code&gt;Connection&lt;/code&gt;添加一个读缓冲区字段，以数据将从socket读取到读缓冲区。解析帧时，再从缓冲区中返回并删除相应的数据即可。&lt;/p&gt;
&lt;p&gt;我们选用&lt;a href=&quot;https://docs.rs/bytes/1/bytes/struct.BytesMut.html&quot;&gt;&lt;code&gt;BytesMut&lt;/code&gt;&lt;/a&gt;作为缓冲区类型，即&lt;a href=&quot;https://docs.rs/bytes/1/bytes/struct.Bytes.html&quot;&gt;&lt;code&gt;Bytes&lt;/code&gt;&lt;/a&gt;的可变版本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bytes::BytesMut;
use tokio::net::TcpStream;

pub struct Connection {
    stream: TcpStream,
    buffer: BytesMut,
}

impl Connection {
    pub fn new(stream: TcpStream) -&amp;gt; Connection {
        Connection {
            stream,
            // Allocate the buffer with 4kb of capacity.
            buffer: BytesMut::with_capacity(4096),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来实现&lt;code&gt;read_frame()&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::AsyncReadExt;
use bytes::Buf;
use mini_redis::Result;

pub async fn read_frame(&amp;amp;mut self)
    -&amp;gt; Result&amp;lt;Option&amp;lt;Frame&amp;gt;&amp;gt;
{
    loop {
        // Attempt to parse a frame from the buffered data. If
        // enough data has been buffered, the frame is
        // returned.
        if let Some(frame) = self.parse_frame()? {
            return Ok(Some(frame));
        }

        // There is not enough buffered data to read a frame.
        // Attempt to read more data from the socket.
        //
        // On success, the number of bytes is returned. `0`
        // indicates &quot;end of stream&quot;.
        if 0 == self.stream.read_buf(&amp;amp;mut self.buffer).await? {
            // The remote closed the connection. For this to be
            // a clean shutdown, there should be no data in the
            // read buffer. If there is, this means that the
            // peer closed the socket while sending a frame.
            if self.buffer.is_empty() {
                return Ok(None);
            } else {
                return Err(&quot;connection reset by peer&quot;.into());
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;让我们逐行分析这段代码。&lt;code&gt;read_frame&lt;/code&gt;方法内是一个循环：首先调用&lt;code&gt;self.parse_frame()&lt;/code&gt;，以将尝试从&lt;code&gt;self.buffer&lt;/code&gt;中解析出一个完整的redis帧，如果有足够的数据来解析出一个完整帧，则将该帧返回给&lt;code&gt;read_frame()&lt;/code&gt;的调用者；反之，则尝试将从socket读更多的数据取到缓冲区中。读取更多数据后将再次调用&lt;code&gt;parse_frame()&lt;/code&gt;，此时，如果收到足够的数据，解析可能会成功。&lt;/p&gt;
&lt;p&gt;从流中读取数据时，返回值为&lt;code&gt;0&lt;/code&gt;表示将不能从对端收到更多数据，此时，如果读缓冲区仍有数据，则表明收到了部分帧，但该连接突然终止。这是一个错误，应返回&lt;code&gt;Err&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Buf&lt;/code&gt; trait&lt;/h3&gt;
&lt;p&gt;从流中读取数据时调用了&lt;code&gt;read_buf&lt;/code&gt;。此读函数接受的参数，需要实现&lt;a href=&quot;https://docs.rs/bytes/&quot;&gt;&lt;code&gt;bytes&lt;/code&gt;&lt;/a&gt; crate的&lt;a href=&quot;https://docs.rs/bytes/1/bytes/trait.BufMut.html&quot;&gt;&lt;code&gt;BufMut&lt;/code&gt;&lt;/a&gt; trait。&lt;/p&gt;
&lt;p&gt;这里先思考如果使用&lt;code&gt;read()&lt;/code&gt;实现相同的读循环，该怎么写。（可以先用&lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;/code&gt;代替上面的&lt;code&gt;BytesMut&lt;/code&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpStream;

pub struct Connection {
    stream: TcpStream,
    buffer: Vec&amp;lt;u8&amp;gt;,
    cursor: usize,
}

impl Connection {
    pub fn new(stream: TcpStream) -&amp;gt; Connection {
        Connection {
            stream,
            // Allocate the buffer with 4kb of capacity.
            buffer: vec![0; 4096],
            cursor: 0,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再给&lt;code&gt;Conneciont&lt;/code&gt;添加&lt;code&gt;read_frame()&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use mini_redis::{Frame, Result};

pub async fn read_frame(&amp;amp;mut self)
    -&amp;gt; Result&amp;lt;Option&amp;lt;Frame&amp;gt;&amp;gt;
{
    loop {
        if let Some(frame) = self.parse_frame()? {
            return Ok(Some(frame));
        }

        // Ensure the buffer has capacity
        if self.buffer.len() == self.cursor {
            // Grow the buffer
            self.buffer.resize(self.cursor * 2, 0);
        }

        // Read into the buffer, tracking the number
        // of bytes read
        let n = self.stream.read(
            &amp;amp;mut self.buffer[self.cursor..]).await?;

        if 0 == n {
            if self.cursor == 0 {
                return Ok(None);
            } else {
                return Err(&quot;connection reset by peer&quot;.into());
            }
        } else {
            // Update our cursor
            self.cursor += n;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用字节数组&lt;code&gt;read&lt;/code&gt;时，需要手动维护一个游标用来跟踪缓冲了多少数据，还必须确保将缓冲区仍为空的部分传给&lt;code&gt;read()&lt;/code&gt;；否则，将覆盖掉缓冲的数据。而当缓冲区用完时，还要手动扩大缓冲区以能够继续读到缓冲区。在&lt;code&gt;parse_frame()&lt;/code&gt;（尚未提到）中，还需要指定解析位于&lt;code&gt;self.buffer[..self.cursor]&lt;/code&gt;中的数据。&lt;/p&gt;
&lt;p&gt;因为于将字节数组与游标搭配使用的场景很常见，&lt;code&gt;bytes&lt;/code&gt; crate提供了表示字节数组和游标的抽象。&lt;code&gt;Buf&lt;/code&gt; trait由那些可以从中读取数据的源头类型来实现。&lt;code&gt;BufMut&lt;/code&gt;由那些可以进将数据写入的目的类型来实现。当把&lt;code&gt;T: BufMut&lt;/code&gt;的参数传给&lt;code&gt;read_buf()&lt;/code&gt;时，缓冲区内部的游标将被&lt;code&gt;read_buf()&lt;/code&gt;自动更新。因此，在我们的&lt;code&gt;read_frame&lt;/code&gt;，不再需要自己手动管理游标。&lt;/p&gt;
&lt;p&gt;另外，使用&lt;code&gt;vec&amp;lt;u8&amp;gt;&lt;/code&gt;时，必须初始化缓冲区。&lt;code&gt;vec![0; 4096]&lt;/code&gt;将分配4096字节的数组，并将每个字节都置为零。调整缓冲区大小时，新增部分还必须使用零来初始化。而初始化操作是有代价的。使用&lt;code&gt;BytesMut&lt;/code&gt;和&lt;code&gt;BufMut&lt;/code&gt;时，缓冲区是未初始化的。&lt;code&gt;BytesMut&lt;/code&gt;抽象会防止我们从未初始化的内存中读取数据，这就避免了初始化操作。&lt;/p&gt;
&lt;h2&gt;解析帧&lt;/h2&gt;
&lt;p&gt;现在，让我们看一下&lt;code&gt;parse_frame()&lt;/code&gt;，解析分两个步骤进行。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;确保全帧被缓冲，并索引到到帧的末尾。&lt;/li&gt;
&lt;li&gt;解析帧。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;mini-redis&lt;/code&gt; crate为我们提供了这两个步骤所需的函数：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/mini-redis/0.4/mini_redis/frame/enum.Frame.html#method.check&quot;&gt;&lt;code&gt;Frame::check&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/mini-redis/0.4/mini_redis/frame/enum.Frame.html#method.parse&quot;&gt;&lt;code&gt;Frame::parse&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里又需要使用到&lt;code&gt;Buf&lt;/code&gt;抽象。一个实现了&lt;code&gt;Buf&lt;/code&gt;的对象会被传递给&lt;code&gt;Frame::check&lt;/code&gt;，而在&lt;code&gt;check&lt;/code&gt;函数检查传入的缓冲对象时，内部的游标会随着移动。当&lt;code&gt;check&lt;/code&gt;返回时，缓冲区内部的游标就自动指向帧的末端了。&lt;/p&gt;
&lt;p&gt;我们将使用&lt;a href=&quot;https://doc.rust-lang.org/stable/std/io/struct.Cursor.html&quot;&gt;&lt;code&gt;std::io::Cursor&amp;lt;&amp;amp;[u8]&amp;gt;&lt;/code&gt;&lt;/a&gt;类型实现&lt;code&gt;Buf&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use mini_redis::{Frame, Result};
use mini_redis::frame::Error::Incomplete;
use bytes::Buf;
use std::io::Cursor;

fn parse_frame(&amp;amp;mut self)
    -&amp;gt; Result&amp;lt;Option&amp;lt;Frame&amp;gt;&amp;gt;
{
    // Create the `T: Buf` type.
    let mut buf = Cursor::new(&amp;amp;self.buffer[..]);

    // Check whether a full frame is available
    match Frame::check(&amp;amp;mut buf) {
        Ok(_) =&amp;gt; {
            // Get the byte length of the frame
            let len = buf.position() as usize;

            // Reset the internal cursor for the
            // call to `parse`.
            buf.set_position(0);

            // Parse the frame
            let frame = Frame::parse(&amp;amp;mut buf)?;

            // Discard the frame from the buffer
            self.buffer.advance(len);

            // Return the frame to the caller.
            Ok(Some(frame))
        }
        // Not enough data has been buffered
        Err(Incomplete) =&amp;gt; Ok(None),
        // An error was encountered
        Err(e) =&amp;gt; Err(e.into()),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的&lt;a href=&quot;https://github.com/tokio-rs/mini-redis/blob/tutorial/src/frame.rs#L65-L103&quot;&gt;&lt;code&gt;Frame::check&lt;/code&gt;&lt;/a&gt;函数可以在&lt;a href=&quot;https://github.com/tokio-rs/mini-redis/blob/tutorial/src/frame.rs#L65-L103&quot;&gt;这里&lt;/a&gt;找到，此处就不贴上它的完整代码了。&lt;/p&gt;
&lt;p&gt;值得注意的是&lt;code&gt;Buf&lt;/code&gt;使用了“字节迭代器”风格的API，以读取数据并移动内部游标。例如，解析帧时，检查第一个字节以确定帧的类型，使用的函数是&lt;a href=&quot;https://docs.rs/bytes/1/bytes/buf/trait.Buf.html#method.get_u8&quot;&gt;&lt;code&gt;Buf::get_u8&lt;/code&gt;&lt;/a&gt;，可以获取游标当前位置的字节，并将游标后移一位。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/bytes/1/bytes/buf/trait.Buf.html&quot;&gt;&lt;code&gt;Buf&lt;/code&gt;&lt;/a&gt; trait还有更多有用的方，可以查看&lt;a href=&quot;https://docs.rs/bytes/1/bytes/buf/trait.Buf.html&quot;&gt;API文档&lt;/a&gt;获取更多信息。&lt;/p&gt;
&lt;h2&gt;缓冲写&lt;/h2&gt;
&lt;p&gt;帧相关API的另一半是&lt;code&gt;write_frame(frame)&lt;/code&gt;函数，它会将一个完整的帧写入socket。为了最大程度地减少&lt;code&gt;write&lt;/code&gt;系统调用，写将被缓冲。我们维护一个写缓冲区，并在写入socket之前将帧编码到该缓冲区。不过，与&lt;code&gt;read_frame()&lt;/code&gt;不同，完整的帧并不总是在写入socket前缓冲到字节数组。&lt;/p&gt;
&lt;p&gt;比如对一个大块流帧，要写入的值为&lt;code&gt;Frame::Bulk(Bytes)&lt;/code&gt;。大块帧的的物理格式是一个帧头，即&lt;code&gt;$&lt;/code&gt;符后接以字节计算的数据长度。帧的主体是内容的&lt;code&gt;Bytes&lt;/code&gt;。如果数据量很大，则将其复制到中间缓冲区将非常耗资源。&lt;/p&gt;
&lt;p&gt;为了实现缓冲写，我们将使用&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/struct.BufWriter.html&quot;&gt;&lt;code&gt;BufWriter&lt;/code&gt;结构体&lt;/a&gt;，它用&lt;code&gt;T: AsyncWrite&lt;/code&gt;初始化并自己实现&lt;code&gt;AsyncWrite&lt;/code&gt;。当在&lt;code&gt;BufWriter&lt;/code&gt;上调用&lt;code&gt;write&lt;/code&gt;时，并非直接调用内部的写操作，而是提交给缓冲区。当缓冲区满时，内容才一次性交给内部写操作，然后清理缓冲区。当然，在某些情况下，还有一些可以绕过缓冲区的优化操作。&lt;/p&gt;
&lt;p&gt;这里的教程不会贴上完整的&lt;code&gt;write_frame()&lt;/code&gt;实现了，你可以在&lt;a href=&quot;https://github.com/tokio-rs/mini-redis/blob/tutorial/src/connection.rs#L159-L184&quot;&gt;这里&lt;/a&gt;找到完整代码。&lt;/p&gt;
&lt;p&gt;首先，修改&lt;code&gt;Connection&lt;/code&gt;结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::BufWriter;
use tokio::net::TcpStream;
use bytes::BytesMut;

pub struct Connection {
    stream: BufWriter&amp;lt;TcpStream&amp;gt;,
    buffer: BytesMut,
}

impl Connection {
    pub fn new(stream: TcpStream) -&amp;gt; Connection {
        Connection {
            stream: BufWriter::new(stream),
            buffer: BytesMut::with_capacity(4096),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后实现&lt;code&gt;write_frame()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::{self, AsyncWriteExt};
use mini_redis::Frame;

async fn write_frame(&amp;amp;mut self, frame: &amp;amp;Frame)
    -&amp;gt; io::Result&amp;lt;()&amp;gt;
{
    match frame {
        Frame::Simple(val) =&amp;gt; {
            self.stream.write_u8(b&apos;+&apos;).await?;
            self.stream.write_all(val.as_bytes()).await?;
            self.stream.write_all(b&quot;\r\n&quot;).await?;
        }
        Frame::Error(val) =&amp;gt; {
            self.stream.write_u8(b&apos;-&apos;).await?;
            self.stream.write_all(val.as_bytes()).await?;
            self.stream.write_all(b&quot;\r\n&quot;).await?;
        }
        Frame::Integer(val) =&amp;gt; {
            self.stream.write_u8(b&apos;:&apos;).await?;
            self.write_decimal(*val).await?;
        }
        Frame::Null =&amp;gt; {
            self.stream.write_all(b&quot;$-1\r\n&quot;).await?;
        }
        Frame::Bulk(val) =&amp;gt; {
            let len = val.len();

            self.stream.write_u8(b&apos;$&apos;).await?;
            self.write_decimal(len as u64).await?;
            self.stream.write_all(val).await?;
            self.stream.write_all(b&quot;\r\n&quot;).await?;
        }
        Frame::Array(_val) =&amp;gt; unimplemented!(),
    }

    self.stream.flush().await;

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用的诸多函数均由&lt;code&gt;AsyncWriteExt&lt;/code&gt; trait提供，该trait也被&lt;code&gt;TcpStream&lt;/code&gt;实现，但是不建议在没有中间缓冲区的情况下发送写单个字节的命令（译注：即直接在&lt;code&gt;TcpStream&lt;/code&gt;上调用这些&lt;code&gt;write_*&lt;/code&gt;函数会产生大量系统调用，建议的做法是在有缓冲包装的写对象上调用，如文中的&lt;code&gt;stream&lt;/code&gt;是一个&lt;code&gt;BufWriter&amp;lt;TcpStream&amp;gt;&lt;/code&gt;对象）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/tokio/1/tokio/io/trait.AsyncWriteExt.html#method.write_u8&quot;&gt;&lt;code&gt;write_u8&lt;/code&gt;&lt;/a&gt;将单个字节送入写端。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tokio.rs/tokio/tutorial/framing&quot;&gt;&lt;code&gt;write_all&lt;/code&gt;&lt;/a&gt;将整个切片送入写端。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/tokio-rs/mini-redis/blob/tutorial/src/connection.rs#L225-L238&quot;&gt;&lt;code&gt;write_decimal&lt;/code&gt;&lt;/a&gt;由mini-redis实现。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该函数结束时将调用&lt;code&gt;self.stream.flush().await&lt;/code&gt;。由于&lt;code&gt;BufWriter&lt;/code&gt;在中间缓冲区保存写内容，因此调用&lt;code&gt;write&lt;/code&gt;不能保证数据立即写入socket。在返回之前，我们希望将帧写入socket，手动调用&lt;code&gt;flush()&lt;/code&gt;以将缓冲区中等待的所有数据写入socket。&lt;/p&gt;
&lt;p&gt;另一种实现是在&lt;code&gt;Connection&lt;/code&gt;上提供一个&lt;code&gt;flush()&lt;/code&gt;方法，来代替在&lt;code&gt;write_frame()&lt;/code&gt;中调用&lt;code&gt;flush()&lt;/code&gt;。这将允许调用者将多个小帧排队写入写缓冲区，然后只用一个&lt;code&gt;write&lt;/code&gt;系统调用将它们全部写入socket。不过这样会使&lt;code&gt;Connection&lt;/code&gt;的API复杂化。简单也是Mini-Redis的目标，因此我们决定在&lt;code&gt;fn write_frame()&lt;/code&gt;中调用&lt;code&gt;flush().await&lt;/code&gt;。&lt;/p&gt;
&lt;h1&gt;9 深入异步&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;至此，我们已经较为全面的了解了关于异步Rust和Tokio的知识。现在，让我们更深入了研究Rust的异步运行时模型。在本教程的最开始，我们暗示异步Rust采用了一种独特的实现方法。本章，我们就解释一下这意味着什么。&lt;/p&gt;
&lt;h2&gt;Futures&lt;/h2&gt;
&lt;p&gt;我们用一个非常基本的异步函数，来快速回顾一下了解到的异步相关知识，在这个例子中并没有超出前几章的知识点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpStream;

async fn my_async_fn() {
    println!(&quot;hello from async&quot;);
    let _socket = TcpStream::connect(&quot;127.0.0.1:3000&quot;).await.unwrap();
    println!(&quot;async TCP operation complete&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们调用该函数，它返回一些值。我们又在该值上调用&lt;code&gt;.await&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[tokio::main]
async fn main() {
    let what_is_this = my_async_fn();
    // Nothing has been printed yet.
    what_is_this.await;
    // Text has been printed and socket has been
    // established and closed.
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;my_async_fn()&lt;/code&gt;的返回值是一个future。实现了标准库&lt;a href=&quot;https://doc.rust-lang.org/std/future/trait.Future.html&quot;&gt;&lt;code&gt;std::future::Future&lt;/code&gt;&lt;/a&gt; trait的值就是future。这些值里包含了正在执行的异步计算。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/std/future/trait.Future.html&quot;&gt;&lt;code&gt;std::future::Future&lt;/code&gt;&lt;/a&gt; trait的定义为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
    type Output;

    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context)
        -&amp;gt; Poll&amp;lt;Self::Output&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#specifying-placeholder-types-in-trait-definitions-with-associated-types&quot;&gt;关联类型&lt;/a&gt;&lt;code&gt;Output&lt;/code&gt;是future执行完毕后返回的类型。&lt;a href=&quot;https://doc.rust-lang.org/std/pin/index.html&quot;&gt;&lt;code&gt;Pin&lt;/code&gt;&lt;/a&gt;类型使Rust能够支持异步函数内的借用。有关更多详细信息，请参阅&lt;a href=&quot;https://doc.rust-lang.org/std/pin/index.html&quot;&gt;标准库文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;与用其他语言实现future的方式不同，Rust的future并不是正在后台执行的计算过程，&lt;strong&gt;而是&lt;/strong&gt;计算本身。future的所有者负责通过轮询未来来推进计算。这是通过调用&lt;code&gt;Future::poll&lt;/code&gt;来实现的。&lt;/p&gt;
&lt;p&gt;（译注，本教程因主要关注tokio的应用，所以本章关于深入Rust异步机制的介绍篇幅并不大，更深入的讨论可以参考 &lt;a href=&quot;/2021/04/14/writing-an-os-in-rust-4.1/#Rust%E7%9A%84async-await&quot;&gt;使用Rust编写操作系统 - 4.1 - Async/Await&lt;/a&gt;）&lt;/p&gt;
&lt;h3&gt;实现&lt;code&gt;Future&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;让我们实现一个非常简单的future。 这个future将：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;等待，直到某个特定的时刻才执行。&lt;/li&gt;
&lt;li&gt;将一些文本输出到标准输出。&lt;/li&gt;
&lt;li&gt;生成一个字符串。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};

struct Delay {
    when: Instant,
}

impl Future for Delay {
    type Output = &amp;amp;&apos;static str;

    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;)
        -&amp;gt; Poll&amp;lt;&amp;amp;&apos;static str&amp;gt;
    {
        if Instant::now() &amp;gt;= self.when {
            println!(&quot;Hello world&quot;);
            Poll::Ready(&quot;done&quot;)
        } else {
            // Ignore this line for now.
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

#[tokio::main]
async fn main() {
    let when = Instant::now() + Duration::from_millis(10);
    let future = Delay { when };

    let out = future.await;
    assert_eq!(out, &quot;done&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;将异步函数当做Future&lt;/h3&gt;
&lt;p&gt;在&lt;code&gt;main&lt;/code&gt;函数中，我们实例化future并在其上调用&lt;code&gt;.await&lt;/code&gt;。从异步函数中，我们可以在任何实现了&lt;code&gt;Future&lt;/code&gt;的值上调用&lt;code&gt;.await&lt;/code&gt;。反过来，调用&lt;code&gt;async&lt;/code&gt;函数会返回一个实现了&lt;code&gt;Future&lt;/code&gt;的匿名类型。在&lt;code&gt;async fn main()&lt;/code&gt;的情况下，生成的future大致为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};

enum MainFuture {
    // Initialized, never polled
    State0,
    // Waiting on `Delay`, i.e. the `future.await` line.
    State1(Delay),
    // The future has completed.
    Terminated,
}

impl Future for MainFuture {
    type Output = ();

    fn poll(mut self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;)
        -&amp;gt; Poll&amp;lt;()&amp;gt;
    {
        use MainFuture::*;

        loop {
            match *self {
                State0 =&amp;gt; {
                    let when = Instant::now() +
                        Duration::from_millis(10);
                    let future = Delay { when };
                    *self = State1(future);
                }
                State1(ref mut my_future) =&amp;gt; {
                    match Pin::new(my_future).poll(cx) {
                        Poll::Ready(out) =&amp;gt; {
                            assert_eq!(out, &quot;done&quot;);
                            *self = Terminated;
                            return Poll::Ready(());
                        }
                        Poll::Pending =&amp;gt; {
                            return Poll::Pending;
                        }
                    }
                }
                Terminated =&amp;gt; {
                    panic!(&quot;future polled after completion&quot;)
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust future实际上是&lt;strong&gt;状态机&lt;/strong&gt;。这里用一个包含了三种future可能状态的&lt;code&gt;enum&lt;/code&gt;来表示&lt;code&gt;MainFuture&lt;/code&gt;。future状态机始于&lt;code&gt;State0&lt;/code&gt;状态。当调用&lt;code&gt;poll&lt;/code&gt;时，future会尽可能地推进其内部状态。如果future能够完成，则返回包含了异步计算输出的&lt;code&gt;Poll::Ready&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果future无法完成，通常是由于它正在等待的资源尚未准备好，则返回&lt;code&gt;Poll::Pending&lt;/code&gt;。若调用者收到&lt;code&gt;Poll::Pending&lt;/code&gt;，则表明future将在稍后完成，调用者应该稍后再次调用&lt;code&gt;poll&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们还看到future也包含了其他future。调用外部future的&lt;code&gt;poll&lt;/code&gt;会导致调用内部future的&lt;code&gt;poll&lt;/code&gt;函数。&lt;/p&gt;
&lt;h2&gt;执行器&lt;/h2&gt;
&lt;p&gt;异步Rust函数返回future，而future必须依靠不断的&lt;code&gt;poll&lt;/code&gt;调用来推进器状态。Future包含了其他future，那么问题来了，是谁在最外层的future调用&lt;code&gt;poll&lt;/code&gt;呢？&lt;/p&gt;
&lt;p&gt;回想一下，要运行异步函数，要么必须传递给&lt;code&gt;tokio::spawn&lt;/code&gt;，要么使用带有&lt;code&gt;#[tokio::main]&lt;/code&gt;标识的main函数。这实际上这是将生成的外层future提交给了Tokio执行器。执行器负责在外部&lt;code&gt;future&lt;/code&gt;上调用&lt;code&gt;Future::poll&lt;/code&gt;，以驱动异步计算完成执行。&lt;/p&gt;
&lt;h3&gt;迷你Tokio&lt;/h3&gt;
&lt;p&gt;为了更好地理解这一切是如何组合在一起的，让我们实现我们自己的最小版本的Tokio！你可以在&lt;a href=&quot;https://github.com/tokio-rs/website/blob/master/tutorial-code/mini-tokio/src/main.rs&quot;&gt;这里&lt;/a&gt;找到完整的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use futures::task;

fn main() {
    let mut mini_tokio = MiniTokio::new();

    mini_tokio.spawn(async {
        let when = Instant::now() + Duration::from_millis(10);
        let future = Delay { when };

        let out = future.await;
        assert_eq!(out, &quot;done&quot;);
    });

    mini_tokio.run();
}

struct MiniTokio {
    tasks: VecDeque&amp;lt;Task&amp;gt;,
}

type Task = Pin&amp;lt;Box&amp;lt;dyn Future&amp;lt;Output = ()&amp;gt; + Send&amp;gt;&amp;gt;;

impl MiniTokio {
    fn new() -&amp;gt; MiniTokio {
        MiniTokio {
            tasks: VecDeque::new(),
        }
    }
    
    /// Spawn a future onto the mini-tokio instance.
    fn spawn&amp;lt;F&amp;gt;(&amp;amp;mut self, future: F)
    where
        F: Future&amp;lt;Output = ()&amp;gt; + Send + &apos;static,
    {
        self.tasks.push_back(Box::pin(future));
    }
    
    fn run(&amp;amp;mut self) {
        let waker = task::noop_waker();
        let mut cx = Context::from_waker(&amp;amp;waker);
        
        while let Some(mut task) = self.tasks.pop_front() {
            if task.as_mut().poll(&amp;amp;mut cx).is_pending() {
                self.tasks.push_back(task);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里执行了一个异步代码块。使用给定的延迟创建了一个&lt;code&gt;Delay&lt;/code&gt;实例，并处于等待状态。但是现在的实现存在一个&lt;strong&gt;问题&lt;/strong&gt;，即执行器不会休眠。这个执行器会在生成的&lt;strong&gt;所有&lt;/strong&gt;future上不断调用轮询。大多数时间里，future都不会就绪，只会继续返回&lt;code&gt;Poll::Pending&lt;/code&gt;。这一过程将吃满CPU周期，所以通常效率不高。&lt;/p&gt;
&lt;p&gt;理想情况下，我们希望mini-tokio只在future就绪时才去调用轮询。这种情况发生在任务所需的被阻塞资源转为可用，从而能够执任务所求情的操作时，例如，若任务想从TCP socket读取数据，则我们只希望在TCP socket收到数据时才进行轮询。在mini-tokio中，任务在给定的&lt;code&gt;Instant&lt;/code&gt;时间内被阻塞，所以理想情况下，mini-tokio应只在任务给定的&lt;code&gt;Instance&lt;/code&gt;时间过去后，再执行一次轮询。&lt;/p&gt;
&lt;p&gt;为了实现这一目标，当轮询了某资源，且&lt;strong&gt;尚未&lt;/strong&gt;就绪时，该资源应在过度到就绪状态后立即发送通知。&lt;/p&gt;
&lt;h2&gt;唤醒器&lt;/h2&gt;
&lt;p&gt;现在我们还缺少唤醒器，即用于在资源就绪时，通知等待的任务资源已就绪，可以继续后面的操作。&lt;/p&gt;
&lt;p&gt;再次观察&lt;code&gt;Future:poll&lt;/code&gt;的函数签名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context)
    -&amp;gt; Poll&amp;lt;Self::Output&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;poll&lt;/code&gt;的&lt;code&gt;Context&lt;/code&gt;参数有一个&lt;code&gt;waker()&lt;/code&gt;方法，它返回一个绑定当前任务的&lt;a href=&quot;https://doc.rust-lang.org/std/task/struct.Waker.html&quot;&gt;&lt;code&gt;Waker&lt;/code&gt;&lt;/a&gt;。&lt;a href=&quot;https://doc.rust-lang.org/std/task/struct.Waker.html&quot;&gt;&lt;code&gt;Waker&lt;/code&gt;&lt;/a&gt;有一个&lt;code&gt;wake()&lt;/code&gt;方法，调用此方法将通知执行器以安排执行相关任务。当资源过度到就绪状态时就会调用&lt;code&gt;wake()&lt;/code&gt;，通知执行器轮询该任务以推进作业的执行。&lt;/p&gt;
&lt;h3&gt;修改&lt;code&gt;Delay&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;给&lt;code&gt;Delay&lt;/code&gt;代码加上唤醒器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use std::thread;

struct Delay {
    when: Instant,
}

impl Future for Delay {
    type Output = &amp;amp;&apos;static str;

    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;)
        -&amp;gt; Poll&amp;lt;&amp;amp;&apos;static str&amp;gt;
    {
        if Instant::now() &amp;gt;= self.when {
            println!(&quot;Hello world&quot;);
            Poll::Ready(&quot;done&quot;)
        } else {
            // Get a handle to the waker for the current task
            let waker = cx.waker().clone();
            let when = self.when;

            // Spawn a timer thread.
            thread::spawn(move || {
                let now = Instant::now();

                if now &amp;lt; when {
                    thread::sleep(when - now);
                }

                waker.wake();
            });

            Poll::Pending
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，一旦等待到了指定的延时，就会通知调用的任务，执行器就可以再次安排轮询任务了。接下来我们修改mini-tokio的代码，从而能够接受唤醒通知。&lt;/p&gt;
&lt;p&gt;这里的&lt;code&gt;Delay&lt;/code&gt;实现仍存在一些问题，我们会稍后修复。&lt;/p&gt;
&lt;p&gt;:::caution
当future返回&lt;code&gt;Poll:Pending&lt;/code&gt;时，&lt;strong&gt;必须&lt;/strong&gt;确保在某个时刻通知唤醒器，若忘记执行此操作，会导致任务永久挂起。&lt;/p&gt;
&lt;p&gt;在任务返回&lt;code&gt;Poll:Pending&lt;/code&gt;后忘记唤醒是一个常见的bug。
:::&lt;/p&gt;
&lt;p&gt;回忆我们的第一版&lt;code&gt;Delay&lt;/code&gt;，是这样实现future的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Future for Delay {
    type Output = &amp;amp;&apos;static str;

    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;)
        -&amp;gt; Poll&amp;lt;&amp;amp;&apos;static str&amp;gt;
    {
        if Instant::now() &amp;gt;= self.when {
            println!(&quot;Hello world&quot;);
            Poll::Ready(&quot;done&quot;)
        } else {
            // Ignore this line for now.
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在返回&lt;code&gt;Poll:Pending&lt;/code&gt;之前，我们调用了&lt;code&gt;cx.waker().wake_by_ref()&lt;/code&gt;。这是为了符合future协议。由于返回了&lt;code&gt;Poll::Pending&lt;/code&gt;，我们得负责通知唤醒器。由于此时我们还没实现计时器线程，所以我们在内部通知了唤醒器。这将导致该future被立刻安排轮询，从而再次执行，并且此刻的future依然没有就绪。&lt;/p&gt;
&lt;p&gt;这里可以注意到，你确实可以比非必要不通知的理想状态，更频繁的通知唤醒器。比如上面的代码中，我们确实在未就绪的情况下通知了唤醒器。这也没什么大问题，只是浪费了点CPU周期，不过这种实现确实使得循环更加繁忙。&lt;/p&gt;
&lt;h3&gt;修改Mini Tokio&lt;/h3&gt;
&lt;p&gt;接下来是修改Mini Tokio的代码，以接收唤醒器通知。我们希望执行器仅在任务唤醒时运行它们，为此，需要给Mini Tokio实现唤醒器。调用唤醒器时，其关联的任务将被排入执行队列。Mini-Tokio在轮询future时会将该唤醒器传给该future。&lt;/p&gt;
&lt;p&gt;新的Mini Tokio将使用通道存储排入计划的任务。通道能够让排入队列的任务在任意线程上执行。唤醒器必须是&lt;code&gt;Send&lt;/code&gt;且&lt;code&gt;Sync&lt;/code&gt;的，所以我们使用crossbeam提供的通道，因为标准库的通道不是&lt;code&gt;Sync&lt;/code&gt;的。&lt;/p&gt;
&lt;p&gt;:::note
&lt;code&gt;Send&lt;/code&gt;和&lt;code&gt;Sync&lt;/code&gt;是Rust提供的并发相关的标记trait。能够被&lt;strong&gt;发送&lt;/strong&gt;到其他线程的类型就是&lt;code&gt;Send&lt;/code&gt;的，除了&lt;a href=&quot;https://doc.rust-lang.org/std/rc/struct.Rc.html&quot;&gt;&lt;code&gt;Rc&lt;/code&gt;&lt;/a&gt;之类的类型不是外，大多数类型都是&lt;code&gt;Send&lt;/code&gt;的。能够通过不可变引用并发访问的类型就是&lt;code&gt;Sync&lt;/code&gt;的。可以&lt;code&gt;Send&lt;/code&gt;却不能&lt;code&gt;Sync&lt;/code&gt;的类型，比如&lt;a href=&quot;https://doc.rust-lang.org/std/cell/struct.Cell.html&quot;&gt;&lt;code&gt;Cell&lt;/code&gt;&lt;/a&gt;，就可以通过不可变引用修改，却不能被安全的并发访问。&lt;/p&gt;
&lt;p&gt;有关更多详情请参阅 &lt;a href=&quot;https://doc.rust-lang.org/book/ch16-04-extensible-concurrency-sync-and-send.html&quot;&gt;Rust Book的相关章节&lt;/a&gt;。
:::&lt;/p&gt;
&lt;p&gt;将以下依赖项添加到您的&lt;code&gt;Cargo.toml&lt;/code&gt;，引入通道。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;crossbeam = &quot;0.8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再更新&lt;code&gt;MiniTikio&lt;/code&gt;结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use crossbeam::channel;
use std::sync::Arc;

struct MiniTokio {
    scheduled: channel::Receiver&amp;lt;Arc&amp;lt;Task&amp;gt;&amp;gt;,
    sender: channel::Sender&amp;lt;Arc&amp;lt;Task&amp;gt;&amp;gt;,
}

struct Task {
    // 将在下文中实现此结构体。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Wakers&lt;/code&gt;既&lt;code&gt;Sync&lt;/code&gt;又可被克隆。调用&lt;code&gt;wake&lt;/code&gt;时，任务必须被安排执行。为了实现这一点，我们建立一个通道。当唤醒器调用&lt;code&gt;wake()&lt;/code&gt;时，任务被推送到通道的发送端，我们的&lt;code&gt;Task&lt;/code&gt;结构体将实现这个唤醒逻辑。为此，它需要同时包含生成的future和通道的发送端。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::sync::{Arc, Mutex};

struct Task {
    // `Mutux`用以让`Task`实现`Sync`。同时只允许一个线程访问`future`。
    // `Mutex`在此处并不是为了保证正确性。实际上Tokio在这里也并没使用什么互斥锁，
    // 而是使用了大量其他代码实现这一功能。
    future: Mutex&amp;lt;Pin&amp;lt;Box&amp;lt;dyn Future&amp;lt;Output = ()&amp;gt; + Send&amp;gt;&amp;gt;&amp;gt;,
    executor: channel::Sender&amp;lt;Arc&amp;lt;Task&amp;gt;&amp;gt;,
}

impl Task {
    fn schedule(self: &amp;amp;Arc&amp;lt;Self&amp;gt;) {
        self.executor.send(self.clone());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了安排任务，将&lt;code&gt;Arc&lt;/code&gt;克隆并使用通道发送。现在，我们需要将&lt;code&gt;schedule&lt;/code&gt;函数与&lt;a href=&quot;https://doc.rust-lang.org/std/task/struct.Waker.html&quot;&gt;&lt;code&gt;std::task::Waker&lt;/code&gt;&lt;/a&gt;挂钩。标准库提供了一个低级API，让我们能够&lt;a href=&quot;https://doc.rust-lang.org/std/task/struct.RawWakerVTable.html&quot;&gt;手动构造vtable&lt;/a&gt;以执行此操作。这种策略为实现者提供了最大的灵活性，但需要大量非安全的样板代码。这里，我们将使用&lt;a href=&quot;https://docs.rs/futures/&quot;&gt;&lt;code&gt;futures&lt;/code&gt;&lt;/a&gt; crate提供的&lt;a href=&quot;https://docs.rs/futures/0.3/futures/task/trait.ArcWake.html&quot;&gt;&lt;code&gt;ArcWake&lt;/code&gt;&lt;/a&gt;实用程序，以替代直接用 &lt;a href=&quot;https://doc.rust-lang.org/std/task/struct.RawWakerVTable.html&quot;&gt;&lt;code&gt;RawWakerVTable&lt;/code&gt;&lt;/a&gt;。这允许我们通过实现一个简单的trait来将&lt;code&gt;Task&lt;/code&gt;结构体呈现为一个唤醒器。&lt;/p&gt;
&lt;p&gt;将以下依赖项添加到&lt;code&gt;Cargo.toml&lt;/code&gt;中以引入&lt;code&gt;futures&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;futures = &quot;0.3&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，实现&lt;a href=&quot;https://docs.rs/futures/0.3/futures/task/trait.ArcWake.html&quot;&gt;&lt;code&gt;futures::task::ArcWake&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use futures::task::{self, ArcWake};
use std::sync::Arc;
impl ArcWake for Task {
    fn wake_by_ref(arc_self: &amp;amp;Arc&amp;lt;Self&amp;gt;) {
        arc_self.schedule();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当上面的定时器线程调用&lt;code&gt;waker.wake()&lt;/code&gt;时，任务被推送到通道。接下来，我们在&lt;code&gt;MiniTokio::run()&lt;/code&gt;函数中实现任务的接收和执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl MiniTokio {
    fn run(&amp;amp;self) {
        while let Ok(task) = self.scheduled.recv() {
            task.poll();
        }
    }

    /// 初始化一个新的mini-tokio实例。
    fn new() -&amp;gt; MiniTokio {
        let (sender, scheduled) = channel::unbounded();

        MiniTokio { scheduled, sender }
    }

    /// 在mini-tokio中生成一个future
    /// 
    /// 给定的future将包装在`Task`中并被送入`scheduled`队列。调用`run`时将执行此future。
    fn spawn&amp;lt;F&amp;gt;(&amp;amp;self, future: F)
    where
        F: Future&amp;lt;Output = ()&amp;gt; + Send + &apos;static,
    {
        Task::spawn(future, &amp;amp;self.sender);
    }
}

impl Task {
    fn poll(self: Arc&amp;lt;Self&amp;gt;) {
        // 使用`Task`的实例创建一个唤醒器。这用到了上面的`ArcWake`实现。
        let waker = task::waker(self.clone());
        let mut cx = Context::from_waker(&amp;amp;waker);

        // 没有其他线程尝试获取future的锁。
        let mut future = self.future.try_lock().unwrap();

        // 轮询future
        let _ = future.as_mut().poll(&amp;amp;mut cx);
    }

    // 使用给定future生成一个新的task。
    // 
    // 初始化一个包裹着给定future的新Task，并将其推送至`sender`。通道的接收端将获取这个task并执行。
    fn spawn&amp;lt;F&amp;gt;(future: F, sender: &amp;amp;channel::Sender&amp;lt;Arc&amp;lt;Task&amp;gt;&amp;gt;)
    where
        F: Future&amp;lt;Output = ()&amp;gt; + Send + &apos;static,
    {
        let task = Arc::new(Task {
            future: Mutex::new(Box::pin(future)),
            executor: sender.clone(),
        });

        let _ = sender.send(task);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上代码做了很多事，首先，首先，实现&lt;code&gt;MiniTokio::run()&lt;/code&gt;，该函数执行一个从通道接收计划任务的循环。由于任务在被唤醒后推送到通道，因此这些任务在执行时能够取得进展。&lt;/p&gt;
&lt;p&gt;此外，&lt;code&gt;MiniTokio::new()&lt;/code&gt;和&lt;code&gt;MiniTokio::spawn()&lt;/code&gt;函数现在改为使用通道，而不是之前的&lt;code&gt;VecDeque&lt;/code&gt;。当生成新任务时，它们会得到一份通道发送端的克隆，从而让任务在在运行时自行使用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Task::poll()&lt;/code&gt;函数使用&lt;code&gt;futures&lt;/code&gt; crate中的&lt;a href=&quot;https://docs.rs/futures/0.3/futures/task/trait.ArcWake.html&quot;&gt;&lt;code&gt;ArcWake&lt;/code&gt;&lt;/a&gt;实用程序创建唤醒器。唤醒器用于创建&lt;code&gt;task::Context&lt;/code&gt;，&lt;code&gt;task::Context&lt;/code&gt;将传递给&lt;code&gt;poll&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;我们现在已经看到了异步Rust如何工作的端到端示例。Rust的&lt;code&gt;async/await&lt;/code&gt;特性通过多个trait共同实现。这允许第三方crate使用自定义的执行细节，如Tokio。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;异步Rust操作是惰性的，需要被调用者轮询。&lt;/li&gt;
&lt;li&gt;唤醒器被传递给future，使得future能够找到到调用它的任务。&lt;/li&gt;
&lt;li&gt;当资源尚未就绪时，返回&lt;code&gt;Poll::Pending&lt;/code&gt;并记录任务的唤醒器。&lt;/li&gt;
&lt;li&gt;当资源准备就绪时，通知任务的唤醒器。&lt;/li&gt;
&lt;li&gt;执行器收到通知并安排任务执行。&lt;/li&gt;
&lt;li&gt;再次轮询任务，这次资源就绪，任务取得进展。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;一些未解决的问题&lt;/h2&gt;
&lt;p&gt;回忆实现&lt;code&gt;Delay&lt;/code&gt; future时，我们提到还有一些事情需要解决。Rust的异步模型允许单个future跨任务执行。考虑以下情形：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use futures::future::poll_fn;
use std::future::Future;
use std::pin::Pin;

#[tokio::main]
async fn main() {
    let when = Instant::now() + Duration::from_millis(10);
    let mut delay = Some(Delay { when });

    poll_fn(move |cx| {
        let mut delay = delay.take().unwrap();
        let res = Pin::new(&amp;amp;mut delay).poll(cx);
        assert!(res.is_pending());
        tokio::spawn(async move {
            delay.await;
        });

        Poll::Ready(())
    }).await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;poll_fn&lt;/code&gt;函数使用闭包创建一个&lt;code&gt;Future&lt;/code&gt;实例。上面的代码片段创建了一个&lt;code&gt;Delay&lt;/code&gt;实例，轮询它一次，然后将&lt;code&gt;Delay&lt;/code&gt;实例发送到新任务并执行&lt;code&gt;.await&lt;/code&gt;。在这个例子中，&lt;code&gt;Delay::poll&lt;/code&gt;被&lt;strong&gt;不同&lt;/strong&gt;的&lt;code&gt;Waker&lt;/code&gt;实例调用了不止一次。发生这种情况时，您必须确保在&lt;em&gt;最近一次&lt;/em&gt;调用&lt;code&gt;poll&lt;/code&gt;的&lt;code&gt;Waker&lt;/code&gt;上调用&lt;code&gt;wake&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在实现future时，应当假设每次调用&lt;code&gt;poll&lt;/code&gt;都&lt;strong&gt;可能&lt;/strong&gt;返回不同的&lt;code&gt;Waker&lt;/code&gt;实例，轮询函数必须使用新的唤醒器代替旧的唤醒器。&lt;/p&gt;
&lt;p&gt;我们早期的&lt;code&gt;Delay&lt;/code&gt;实现在每次轮询时都会生成一个新线程。这没什么问题，但如果轮询过于频繁，效率可能会非常低（例如，如果您&lt;code&gt;select!&lt;/code&gt;该future和其他future，只要其中之一有事件发生，则两者都会被轮询）。一种方法是记住你是否已经生成了一个线程，并只有在未生成线程时才执行生成。但是如果这样做，则必须确保在之后调用轮询时更新线程的唤醒器，否则你执行唤醒时将可能使用旧的唤醒器。&lt;/p&gt;
&lt;p&gt;为了修复之前的实现，我们可以这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::{Duration, Instant};

struct Delay {
    when: Instant,
    // 当我们生一个新线程时为`Some`，否则为`None`。
    waker: Option&amp;lt;Arc&amp;lt;Mutex&amp;lt;Waker&amp;gt;&amp;gt;&amp;gt;,
}

impl Future for Delay {
    type Output = ();

    fn poll(mut self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;) -&amp;gt; Poll&amp;lt;()&amp;gt; {
        // 首先，如果这是第一次调用future，则生成一个计时器线程。
        // 如果计时器线程已经存在，则应确保我们保存的`Waker`就是当前任务的唤醒器。
        if let Some(waker) = &amp;amp;self.waker {
            let mut waker = waker.lock().unwrap();

            // 检测保存的唤醒器是否匹配当前线程的唤醒器。
            // 这一步是必要的，因为`Delay` future的实例可能在两次轮询间隔内移动到另一个任务中。
            // 如果发生移动，则给定的`Context`中包含的唤醒器将会不同，
            // 我们必须更新之前存储的唤醒器，以对此变化做出反应。
            if !waker.will_wake(cx.waker()) {
                *waker = cx.waker().clone();
            }
        } else {
            let when = self.when;
            let waker = Arc::new(Mutex::new(cx.waker().clone()));
            self.waker = Some(waker.clone());

            // 此处为第一次调用`poll`，生成计时器线程
            thread::spawn(move || {
                let now = Instant::now();

                if now &amp;lt; when {
                    thread::sleep(when - now);
                }

                // 当持续时间过去之后，调用唤醒器以通知调用者。
                let waker = waker.lock().unwrap();
                waker.wake_by_ref();
            });
        }

        // 在保存了唤醒器并启动了计时器线程后，就该检查给定的延时是否到达。
        // 这可以通过检测当前时刻来实现，如果持续时间已经过去，则future完成并返回`Poll::Ready``。
        if Instant::now() &amp;gt;= self.when {
            Poll::Ready(())
        } else {
            // 如果持续时间没到，则future未完成，应返回`Poll:Pending`。
            // `Future` trait协议规定，返回`Pending`时，future应通知一次给定唤醒器，
            // future应被再次轮询。在我们的例子中，此处返回`Pending`，
            // 我们承诺一旦持续时间过去，就将调用参数`Context`中给定的唤醒器。
            // 我们能够做出承诺，是依靠上面生成的计时器线程。
            //
            // 如果我们忘记调用唤醒器，任务将永久挂起
            Poll::Pending
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这有点复杂，不过基本思路是，在每次调用&lt;code&gt;poll&lt;/code&gt;时，future检查提供的唤醒器是否与先前保存的唤醒器一致，若不一致，则必须保存新的唤醒器。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Notify&lt;/code&gt;实用程序&lt;/h3&gt;
&lt;p&gt;我们演示了如何使用唤醒器手动实现&lt;code&gt;Delay&lt;/code&gt; future。唤醒器是是异步Rust工作的基础。通常，没有必要深入到那个程度。例如，在&lt;code&gt;Delay&lt;/code&gt;中，我们完全可以通过使用&lt;code&gt;async/await&lt;/code&gt;配合&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/struct.Notify.html&quot;&gt;&lt;code&gt;tokio::sync::Notify&lt;/code&gt;&lt;/a&gt;实用程序来实现。该实用程序提供了一个基础的任务通知机制。它负责处理唤醒器的相关细节，包括确保保存的唤醒器与当前任务的唤醒器一致。&lt;/p&gt;
&lt;p&gt;使用&lt;a href=&quot;https://docs.rs/tokio/1/tokio/sync/struct.Notify.html&quot;&gt;&lt;code&gt;Notify&lt;/code&gt;&lt;/a&gt;，我们可以像这样使用&lt;code&gt;async/await&lt;/code&gt;实现&lt;code&gt;delay&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::Notify;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::thread;

async fn delay(dur: Duration) {
    let when = Instant::now() + dur;
    let notify = Arc::new(Notify::new());
    let notify2 = notify.clone();

    thread::spawn(move || {
        let now = Instant::now();

        if now &amp;lt; when {
            thread::sleep(when - now);
        }

        notify2.notify_one();
    });


    notify.notified().await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;10 Select&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;现在，当想要向系统添加并发时，我们就生成一个新任务。下面，我们将介绍一些使用Tokio并发执行异步代码的其他方法。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;tokio::select!&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tokio::select!&lt;/code&gt;宏允许等待多个异步计算，并在某个计算完成时返回。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::oneshot;

#[tokio::main]
async fn main() {
    let (tx1, rx1) = oneshot::channel();
    let (tx2, rx2) = oneshot::channel();

    tokio::spawn(async {
        let _ = tx1.send(&quot;one&quot;);
    });

    tokio::spawn(async {
        let _ = tx2.send(&quot;two&quot;);
    });

    tokio::select! {
        val = rx1 =&amp;gt; {
            println!(&quot;rx1 completed first with {:?}&quot;, val);
        }
        val = rx2 =&amp;gt; {
            println!(&quot;rx2 completed first with {:?}&quot;, val);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于两个oneshot通道，任一通道都可能先完成。&lt;code&gt;select!&lt;/code&gt;语句在两个通道上等待并将&lt;code&gt;val&lt;/code&gt;绑定到任务返回的值。当&lt;code&gt;tx1&lt;/code&gt;或&lt;code&gt;tx2&lt;/code&gt;完成时，执行相关的代码块。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;未完成&lt;/strong&gt;的分支将被丢弃。在示例中，程序等待每个通道的&lt;code&gt;oneshot::Receiver&lt;/code&gt;。未完成通道的&lt;code&gt;oneshot::Receiver&lt;/code&gt;会被丢弃。&lt;/p&gt;
&lt;h3&gt;取消&lt;/h3&gt;
&lt;p&gt;异步Rust的取消操作，是通过放弃future来执行的。回顾“&lt;a href=&quot;./#9-%E6%B7%B1%E5%85%A5%E5%BC%82%E6%AD%A5&quot;&gt;深入异步&lt;/a&gt;”章节，异步Rust使用future实现，而future是惰性的，操作只有被轮询时才会进行。如果如果future被丢弃，由于所有相关状态都已删除，因此无法进行操作。&lt;/p&gt;
&lt;p&gt;也就是说，有时异步操作会生成后台任务，或启动在后台运行的其他操作。例如，在上面的示例中，生成了一个任务以发送消息。通常，任务将执行一些计算以生成值。&lt;/p&gt;
&lt;p&gt;Future或其他类型可以实现&lt;code&gt;Drop&lt;/code&gt;以清理后台资源。&lt;code&gt;Tokio&lt;/code&gt;的&lt;code&gt;oneshot::Receiver&lt;/code&gt;通过向&lt;code&gt;Sender&lt;/code&gt;端发送一条关闭消息来实现&lt;code&gt;Drop&lt;/code&gt;。发送端可以收到此消息并取消进行中的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::oneshot;

async fn some_operation() -&amp;gt; String {
    // Compute value here
}

#[tokio::main]
async fn main() {
    let (mut tx1, rx1) = oneshot::channel();
    let (tx2, rx2) = oneshot::channel();

    tokio::spawn(async {
        // Select on the operation and the oneshot&apos;s
        // `closed()` notification.
        tokio::select! {
            val = some_operation() =&amp;gt; {
                let _ = tx1.send(val);
            }
            _ = tx1.closed() =&amp;gt; {
                // `some_operation()` is canceled, the
                // task completes and `tx1` is dropped.
            }
        }
    });

    tokio::spawn(async {
        let _ = tx2.send(&quot;two&quot;);
    });

    tokio::select! {
        val = rx1 =&amp;gt; {
            println!(&quot;rx1 completed first with {:?}&quot;, val);
        }
        val = rx2 =&amp;gt; {
            println!(&quot;rx2 completed first with {:?}&quot;, val);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;future&lt;/code&gt;的实现&lt;/h3&gt;
&lt;p&gt;为了更好的理解&lt;code&gt;select!&lt;/code&gt;是如何工作的，让我们看看一个假想的&lt;code&gt;Future&lt;/code&gt;实现会是什么样子。这是一个简化版本，现实中的&lt;code&gt;select!&lt;/code&gt;还包含其他功能，例如随机选择首先轮询的分支。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::oneshot;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct MySelect {
    rx1: oneshot::Receiver&amp;lt;&amp;amp;&apos;static str&amp;gt;,
    rx2: oneshot::Receiver&amp;lt;&amp;amp;&apos;static str&amp;gt;,
}

impl Future for MySelect {
    type Output = ();

    fn poll(mut self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;) -&amp;gt; Poll&amp;lt;()&amp;gt; {
        if let Poll::Ready(val) = Pin::new(&amp;amp;mut self.rx1).poll(cx) {
            println!(&quot;rx1 completed first with {:?}&quot;, val);
            return Poll::Ready(());
        }

        if let Poll::Ready(val) = Pin::new(&amp;amp;mut self.rx2).poll(cx) {
            println!(&quot;rx2 completed first with {:?}&quot;, val);
            return Poll::Ready(());
        }

        Poll::Pending
    }
}

#[tokio::main]
async fn main() {
    let (tx1, rx1) = oneshot::channel();
    let (tx2, rx2) = oneshot::channel();

    // use tx1 and tx2

    MySelect {
        rx1,
        rx2,
    }.await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;MySelect&lt;/code&gt; future包含两个分支的future。轮询&lt;code&gt;MySelect&lt;/code&gt;时，将轮询第一个分支。若就绪，则使用该值，&lt;code&gt;MySelect&lt;/code&gt;完成。在&lt;code&gt;.await&lt;/code&gt;收到future的输出后，future将被丢弃，这导致两个分支的futures均被删除。由于一个分支没有完成，操作实际上被取消了。&lt;/p&gt;
&lt;p&gt;回忆上一节的内容：&lt;/p&gt;
&lt;p&gt;:::caution
当future返回&lt;code&gt;Poll:Pending&lt;/code&gt;时，&lt;strong&gt;必须&lt;/strong&gt;确保在某个时刻通知唤醒器，若忘记执行此操作，会导致任务永久挂起。&lt;/p&gt;
&lt;p&gt;在任务返回&lt;code&gt;Poll:Pending&lt;/code&gt;后忘记唤醒是一个常见的bug。
:::&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MySelect&lt;/code&gt;实现中并没用显式使用&lt;code&gt;Context&lt;/code&gt;参数，而是通过将&lt;code&gt;cx&lt;/code&gt;传给内部的future来满足唤醒器的要求。通过仅在从内部future收到&lt;code&gt;Poll::Pending&lt;/code&gt;时才返回&lt;code&gt;Poll::Pending&lt;/code&gt;，来使内部future也满足唤醒器的要求，&lt;code&gt;MySelect&lt;/code&gt;也满足唤醒器的要求。&lt;/p&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;select!&lt;/code&gt;宏可以处理多个分支，目前的限制为64个分支。每个分支的结构如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;pattern&amp;gt; = &amp;lt;async expression&amp;gt; =&amp;gt; &amp;lt;handler&amp;gt;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当select宏求值时，所有&lt;code&gt;&amp;lt;async expression&amp;gt;&lt;/code&gt;都会聚合并同时执行。当表达式完成时，结果与&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;匹配，若结果匹配，则取消所有剩余的异步表达式并执行&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;。&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;表达式可以访问&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;绑定的变量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;的基本用法是给一个变量名赋值，即异步表达式的结果绑定到该变量名，使&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;可以访问该变量。这就是为什么在上面的示例中，&lt;code&gt;val&lt;/code&gt;用于&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;能够访问&lt;code&gt;val&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;与异步计算的结果不匹配，则剩余的异步表达式将继续并发执行，直到下一个完成。这时，上面的逻辑继续应用作用于该结果。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select!&lt;/code&gt;中可以使用任意异步表达式，因此可以定义更复杂的选择计算。&lt;/p&gt;
&lt;p&gt;下面的例子中，我们在&lt;code&gt;oneshot&lt;/code&gt;通道的输出和&lt;code&gt;TCP&lt;/code&gt;连接上进行选择操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpStream;
use tokio::sync::oneshot;

#[tokio::main]
async fn main() {
    let (tx, rx) = oneshot::channel();

    // 生成一个使用oneshot发送信息的任务
    tokio::spawn(async move {
        tx.send(&quot;done&quot;).unwrap();
    });

    tokio::select! {
        socket = TcpStream::connect(&quot;localhost:3465&quot;) =&amp;gt; {
            println!(&quot;Socket connected {:?}&quot;, socket);
        }
        msg = rx =&amp;gt; {
            println!(&quot;received message first {:?}&quot;, msg);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面的例子中，我们在&lt;code&gt;oneshot&lt;/code&gt;通道和&lt;code&gt;TcpListener&lt;/code&gt;socket间进行选择操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpListener;
use tokio::sync::oneshot;
use std::io;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    let (tx, rx) = oneshot::channel();

    tokio::spawn(async move {
        tx.send(()).unwrap();
    });

    let mut listener = TcpListener::bind(&quot;localhost:3465&quot;).await?;

    tokio::select! {
        _ = async {
            loop {
                let (socket, _) = listener.accept().await?;
                tokio::spawn(async move { process(socket) });
            }

            // Help the rust type inferencer out
            Ok::&amp;lt;_, io::Error&amp;gt;(())
        } =&amp;gt; {}
        _ = rx =&amp;gt; {
            println!(&quot;terminating accept loop&quot;);
        }
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;循环将一直运行，直到遇到错误，或&lt;code&gt;rx&lt;/code&gt;收到值。&lt;code&gt;_&lt;/code&gt;模式表示我们对异步计算的返回值不感兴趣。&lt;/p&gt;
&lt;h2&gt;返回值&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tokio::select!&lt;/code&gt;宏返回&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;表达式求值的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn computation1() -&amp;gt; String {
    // .. computation
}

async fn computation2() -&amp;gt; String {
    // .. computation
}

#[tokio::main]
async fn main() {
    let out = tokio::select! {
        res1 = computation1() =&amp;gt; res1,
        res2 = computation2() =&amp;gt; res2,
    };

    println!(&quot;Got = {}&quot;, out);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，&lt;strong&gt;每个&lt;/strong&gt;分支的&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;表达式的求值结果应为相同的类型。如果某个&lt;code&gt;select!&lt;/code&gt;表达式不需要输出，最好将表达式的求值结果写为&lt;code&gt;()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;错误&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;?&lt;/code&gt;运算符从表达式中传播错误，具体的行为取决于&lt;code&gt;?&lt;/code&gt;是用在异步表达式（&lt;code&gt;&amp;lt;async expression&amp;gt;&lt;/code&gt;）中，还是用在处理程序（&lt;code&gt;&amp;lt;handelr&amp;gt;&lt;/code&gt;）中。在异步表达式中使用&lt;code&gt;?&lt;/code&gt;会将错误传播到异步表达式之外，这使得异步表达式的输出为&lt;code&gt;Result&lt;/code&gt;类型；在处理程序中使用&lt;code&gt;?&lt;/code&gt;会立刻将错误传播到&lt;code&gt;select!&lt;/code&gt;表达式之外。让我们再看一遍上面接受socket循环的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::net::TcpListener;
use tokio::sync::oneshot;
use std::io;

#[tokio::main]
async fn main() -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    // [setup `rx` oneshot channel]

    let listener = TcpListener::bind(&quot;localhost:3465&quot;).await?;

    tokio::select! {
        res = async {
            loop {
                let (socket, _) = listener.accept().await?;
                tokio::spawn(async move { process(socket) });
            }

            // Help the rust type inferencer out
            Ok::&amp;lt;_, io::Error&amp;gt;(())
        } =&amp;gt; {
            res?;
        }
        _ = rx =&amp;gt; {
            println!(&quot;terminating accept loop&quot;);
        }
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意&lt;code&gt;listener.accept().await?&lt;/code&gt;，&lt;code&gt;?&lt;/code&gt;运算符将错误从该表达式传播到&lt;code&gt;res&lt;/code&gt;绑定变量。出现错误时，&lt;code&gt;res&lt;/code&gt;将被设置为&lt;code&gt;Err(_)&lt;/code&gt;。处理程序中再次使用了&lt;code&gt;?&lt;/code&gt;运算符，&lt;code&gt;res?&lt;/code&gt;语句将错误传播到&lt;code&gt;main&lt;/code&gt;函数之外。&lt;/p&gt;
&lt;h2&gt;模式匹配&lt;/h2&gt;
&lt;p&gt;回忆&lt;code&gt;select!&lt;/code&gt;宏的分支定义语法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;pattern&amp;gt; = &amp;lt;async expression&amp;gt; =&amp;gt; &amp;lt;handler&amp;gt;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到目前为止，我们只在&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;处用了变量绑定。实际上，我们可以使用任何Rust模式匹配。例如，假设我们从多个MPSC通道接收数据，就可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (mut tx1, mut rx1) = mpsc::channel(128);
    let (mut tx2, mut rx2) = mpsc::channel(128);

    tokio::spawn(async move {
        // Do something w/ `tx1` and `tx2`
    });

    tokio::select! {
        Some(v) = rx1.recv() =&amp;gt; {
            println!(&quot;Got {:?} from rx1&quot;, v);
        }
        Some(v) = rx2.recv() =&amp;gt; {
            println!(&quot;Got {:?} from rx2&quot;, v);
        }
        else =&amp;gt; {
            println!(&quot;Both channels closed&quot;);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这个例子中，&lt;code&gt;select!&lt;/code&gt;表达式等待从&lt;code&gt;rx1&lt;/code&gt;和&lt;code&gt;rx2&lt;/code&gt;接收值。如果一个通道关闭，&lt;code&gt;recv()&lt;/code&gt;返回&lt;code&gt;None&lt;/code&gt;，则无法满足匹配，此分支将失效。于是&lt;code&gt;select!&lt;/code&gt;表达式继续等待剩余的分支。&lt;/p&gt;
&lt;p&gt;注意这个&lt;code&gt;select!&lt;/code&gt;表达式包含一个&lt;code&gt;else&lt;/code&gt;分支，即&lt;code&gt;select!&lt;/code&gt;表达式必须返回一个值。使用模式匹配时，可能&lt;strong&gt;没有&lt;/strong&gt;分支能够匹配其的关联模式，如果发生这种情况，则由&lt;code&gt;else&lt;/code&gt;分支求值。&lt;/p&gt;
&lt;h2&gt;借用&lt;/h2&gt;
&lt;p&gt;当生成任务时，产生异步表达式必持有其所需数据的所有权，而&lt;code&gt;select!&lt;/code&gt;宏并没有这种限制，每个分支的异步表达式可以借用数据且同时运行。遵循Rust的借用规则，多个异步表达式可以同时对某块数据进行不可变借用，&lt;strong&gt;或者&lt;/strong&gt;，仅一个异步表达式可以对某块数据进行可变借用。&lt;/p&gt;
&lt;p&gt;下面的例子中，我们同时将相同的数据发送到两个不同的TCP目的地址。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::io::AsyncWriteExt;
use tokio::net::TcpStream;
use std::io;
use std::net::SocketAddr;

async fn race(
    data: &amp;amp;[u8],
    addr1: SocketAddr,
    addr2: SocketAddr
) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
    tokio::select! {
        Ok(_) = async {
            let mut socket = TcpStream::connect(addr1).await?;
            socket.write_all(data).await?;
            Ok::&amp;lt;_, io::Error&amp;gt;(())
        } =&amp;gt; {}
        Ok(_) = async {
            let mut socket = TcpStream::connect(addr2).await?;
            socket.write_all(data).await?;
            Ok::&amp;lt;_, io::Error&amp;gt;(())
        } =&amp;gt; {}
        else =&amp;gt; {}
    };

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个异步表达式都使用了&lt;code&gt;data&lt;/code&gt;变量这一不可变借用。当其中一个操作成功完成时，另一个操作将被丢弃。因为我们需要匹配&lt;code&gt;Ok(_)&lt;/code&gt;，如果一个表达式失败，另一个则将继续执行。&lt;/p&gt;
&lt;p&gt;当执行到每个分支的&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;时，&lt;code&gt;select!&lt;/code&gt;保证只会有一个&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;执行。因此，每个&lt;code&gt;&amp;lt;handler&amp;gt;&lt;/code&gt;可以对同一块数据进行可变借用。&lt;/p&gt;
&lt;p&gt;下面的例子中，两个处理程序都尝试对&lt;code&gt;out&lt;/code&gt;进行修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::oneshot;

#[tokio::main]
async fn main() {
    let (tx1, rx1) = oneshot::channel();
    let (tx2, rx2) = oneshot::channel();

    let mut out = String::new();

    tokio::spawn(async move {
        // Send values on `tx1` and `tx2`.
    });

    tokio::select! {
        _ = rx1 =&amp;gt; {
            out.push_str(&quot;rx1 completed&quot;);
        }
        _ = rx2 =&amp;gt; {
            out.push_str(&quot;rx2 completed&quot;);
        }
    }

    println!(&quot;{}&quot;, out);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;循环&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;select!&lt;/code&gt;宏经常用在循环中，本节将介绍一些例子，以展示在循环中使用&lt;code&gt;select!&lt;/code&gt;宏的常见方法。第一个例子是在多个通道上进行选择：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio::sync::mpsc;

#[tokio::main]
async fn main() {
    let (tx1, mut rx1) = mpsc::channel(128);
    let (tx2, mut rx2) = mpsc::channel(128);
    let (tx3, mut rx3) = mpsc::channel(128);

    loop {
        let msg = tokio::select! {
            Some(msg) = rx1.recv() =&amp;gt; msg,
            Some(msg) = rx2.recv() =&amp;gt; msg,
            Some(msg) = rx3.recv() =&amp;gt; msg,
            else =&amp;gt; { break }
        };

        println!(&quot;Got {:?}&quot;, msg);
    }

    println!(&quot;All channels have been closed.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的例子在三个通道的接收端上选择，当任一通道的接收端收到信息，就输出到STDOUT。当一个通道关闭时，&lt;code&gt;recv()&lt;/code&gt;返回&lt;code&gt;None&lt;/code&gt;，由于模式匹配，&lt;code&gt;select!&lt;/code&gt;宏将继续等待其余通道。当所有通道关闭时，&lt;code&gt;else&lt;/code&gt;分支将求值从而终止循环。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select!&lt;/code&gt;宏以随机的方式决定首先检查哪个分支是否就绪，当多个通道的值均未就绪时，将随机选择一个通道接受信息。这是为了处理接收循环的处理速度，慢于消息推入通道的速度的情况，这意味着通道可能被填满。如果&lt;code&gt;select!&lt;/code&gt;首选检查的分支不是随机的，而是每次循环都首选检查&lt;code&gt;rx1&lt;/code&gt;，若&lt;code&gt;rx1&lt;/code&gt;始终都有一条新消息，则其他通道将永远不会被检查。&lt;/p&gt;
&lt;p&gt;如果选择时！ 进行评估，多个通道有未决消息，只有一个通道弹出一个值。 所有其他频道都没有受到影响，他们的消息一直在这些频道中，直到下一次循环迭代为止。 没有消息丢失。&lt;/p&gt;
&lt;p&gt;:::caution
当&lt;code&gt;select!&lt;/code&gt;求值，只有一个通道有就绪值，其余通道都在等待时，其余通道的代码不会被执行，其余通道的信息将停留在通道内知道下一次迭代。因此不会有信息丢失。
:::&lt;/p&gt;
&lt;h3&gt;恢复一个异步操作&lt;/h3&gt;
&lt;p&gt;下面的例子将展示如何在多个&lt;code&gt;select!&lt;/code&gt;调用间执行同一个异步操作。在此示例中，我们有一个类型为&lt;code&gt;i32&lt;/code&gt;的MPSC通道和一个异步函数。我们希望一直运行异步函数，直到任务完成，或在通道上接收到一个偶数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn action() {
    // Some asynchronous logic
}

#[tokio::main]
async fn main() {
    let (mut tx, mut rx) = tokio::sync::mpsc::channel(128);
    
    let operation = action();
    tokio::pin!(operation);
    
    loop {
        tokio::select! {
            _ = &amp;amp;mut operation =&amp;gt; break,
            Some(v) = rx.recv() =&amp;gt; {
                if v % 2 == 0 {
                    break;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里是如何再循环外，而不是在&lt;code&gt;select!&lt;/code&gt;宏内调用&lt;code&gt;action()&lt;/code&gt;的。&lt;code&gt;aciont()&lt;/code&gt;的返回值被赋给&lt;code&gt;operation&lt;/code&gt;，且&lt;strong&gt;不执行&lt;/strong&gt;&lt;code&gt;.await&lt;/code&gt;。然后我们在&lt;code&gt;operation&lt;/code&gt;上调用&lt;code&gt;tokio:pin!&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;select!&lt;/code&gt;循环内，我们传入&lt;code&gt;&amp;amp;mut operation&lt;/code&gt;，而不是&lt;code&gt;operation&lt;/code&gt;。&lt;code&gt;operation&lt;/code&gt;变量正在跟踪执行中的异步操作，循环的每次迭代都使用同一个变量，而不是调用一个新的&lt;code&gt;action()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;另一个&lt;code&gt;select!&lt;/code&gt;分支从通道接收数据，如果是一个偶数，则终止循环，否则再次&lt;code&gt;select!&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此处是我们第一次使用&lt;code&gt;tokio::pin!&lt;/code&gt;，这里我们并不介绍关于内存固定的细节，只需要注意，如果要&lt;code&gt;.await&lt;/code&gt;一个引用，被引用的值必须是被固定的，或是实现了&lt;code&gt;Unpin&lt;/code&gt;的。&lt;/p&gt;
&lt;p&gt;如果我们删除&lt;code&gt;tokio::pin!&lt;/code&gt;一行并尝试编译，就会收到以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0599]: no method named `poll` found for struct
     `std::pin::Pin&amp;lt;&amp;amp;mut &amp;amp;mut impl std::future::Future&amp;gt;`
     in the current scope
  --&amp;gt; src/main.rs:16:9
   |
16 | /         tokio::select! {
17 | |             _ = &amp;amp;mut operation =&amp;gt; break,
18 | |             Some(v) = rx.recv() =&amp;gt; {
19 | |                 if v % 2 == 0 {
...  |
22 | |             }
23 | |         }
   | |_________^ method not found in
   |             `std::pin::Pin&amp;lt;&amp;amp;mut &amp;amp;mut impl std::future::Future&amp;gt;`
   |
   = note: the method `poll` exists but the following trait bounds
            were not satisfied:
           `impl std::future::Future: std::marker::Unpin`
           which is required by
           `&amp;amp;mut impl std::future::Future: std::future::Future`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然我们在&lt;a href=&quot;./#9-%E6%B7%B1%E5%85%A5%E5%BC%82%E6%AD%A5&quot;&gt;上一章&lt;/a&gt;介绍了&lt;code&gt;Future&lt;/code&gt;，但仍然不能明白这个错误。如果你尝试在一个&lt;strong&gt;引用&lt;/strong&gt;上调用&lt;code&gt;.await&lt;/code&gt;时，遇到了这种关于&lt;code&gt;Future&lt;/code&gt;未被实现的错误，那么你可能需要固定该future。&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://doc.rust-lang.org/std/pin/index.html&quot;&gt;标准库&lt;/a&gt;上阅读更多有关&lt;a href=&quot;https://doc.rust-lang.org/std/pin/index.html&quot;&gt;&lt;code&gt;Pin&lt;/code&gt;&lt;/a&gt;的信息。&lt;/p&gt;
&lt;h3&gt;修改一个分支&lt;/h3&gt;
&lt;p&gt;再看一个稍微复杂一点的循环，其中有：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;一个&lt;code&gt;i32&lt;/code&gt;类型的通道。&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;i32&lt;/code&gt;值上执行的异步操作。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我们想要实现的逻辑是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在通道上等待一个偶数。&lt;/li&gt;
&lt;li&gt;使用该偶数作为输入以启动异步操作。&lt;/li&gt;
&lt;li&gt;等待操作完成，但同时监听通道上的其他偶数。&lt;/li&gt;
&lt;li&gt;如果在现有操作完成之前收到新的偶数，则终止现有操作并使用新偶数重新开始。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;async fn action(input: Option&amp;lt;i32&amp;gt;) -&amp;gt; Option&amp;lt;String&amp;gt; {
    // 若输入为`None`则返回`None`。
    // 此处也可以简写为`let i = input?；`
    let i = match input {
        Some(input) =&amp;gt; input,
        None =&amp;gt; return None,
    };
    // async logic here
}

#[tokio::main]
async fn main() {
    let (mut tx, mut rx) = tokio::sync::mpsc::channel(128);
    
    let mut done = false;
    let operation = action(None);
    tokio::pin!(operation);
    
    tokio::spawn(async move {
        let _ = tx.send(1).await;
        let _ = tx.send(3).await;
        let _ = tx.send(2).await;
    });
    
    loop {
        tokio::select! {
            res = &amp;amp;mut operation, if !done =&amp;gt; {
                done = true;

                if let Some(v) = res {
                    println!(&quot;GOT = {}&quot;, v);
                    return;
                }
            }
            Some(v) = rx.recv() =&amp;gt; {
                if v % 2 == 0 {
                    // `.set` is a method on `Pin`.
                    operation.set(action(Some(v)));
                    done = false;
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用与上一个例子类似的策略，在循环外调用异步函数并将结果赋给&lt;code&gt;operation&lt;/code&gt;，再将&lt;code&gt;operation&lt;/code&gt;变量固定。循环中的选择操作同事在&lt;code&gt;operation&lt;/code&gt;上与通道接收端上进行。&lt;/p&gt;
&lt;p&gt;留意&lt;code&gt;action&lt;/code&gt;是如何将&lt;code&gt;Option&amp;lt;i32&amp;gt;&lt;/code&gt;作为参数的。在我们收到第一个偶数之前，我们需要将&lt;code&gt;operation&lt;/code&gt;实例化，此处通过接收一个&lt;code&gt;Option&lt;/code&gt;并返回一个&lt;code&gt;Option&lt;/code&gt;来创建&lt;code&gt;action&lt;/code&gt;，而如果传入为&lt;code&gt;None&lt;/code&gt;则返回也为&lt;code&gt;None&lt;/code&gt;。第一次循环迭代，&lt;code&gt;operation&lt;/code&gt;返回&lt;code&gt;None&lt;/code&gt;后立即完成。&lt;/p&gt;
&lt;p&gt;例子中还使用了一些新的语法，第一个分支中的&lt;code&gt;, if !done&lt;/code&gt;，即设置分支的前置条件。在解释它是如何工作之前，让我们看看如果删掉前置条件会发生什么。删除&lt;code&gt;, if !done&lt;/code&gt;再运行例子会产生以下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;thread &apos;main&apos; panicked at &apos;`async fn` resumed after completion&apos;, src/main.rs:1:55
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;operation&lt;/code&gt;完成后再次尝试调用就会发生此错误。通常，在使用&lt;code&gt;.await&lt;/code&gt;时，被等待的值会被使用掉，而在这个例子中，我们等待的是一个引用。这意味着&lt;code&gt;operation&lt;/code&gt;在完成后仍然存在。&lt;/p&gt;
&lt;p&gt;为避免这种panic，我们必须要在操作完成后禁用第一个分支，此处&lt;code&gt;done&lt;/code&gt;变量用于跟踪操作是否完成。一个&lt;code&gt;select!&lt;/code&gt;分支可能包含一个前置条件，&lt;code&gt;select!&lt;/code&gt;在等待分支之前会检查这个前提条件，如果前置条件求值为&lt;code&gt;false&lt;/code&gt;，则分支被禁用。&lt;code&gt;done&lt;/code&gt;变量初始化时为&lt;code&gt;false&lt;/code&gt;，当操作完成时，&lt;code&gt;done&lt;/code&gt;设置为&lt;code&gt;true&lt;/code&gt;，在下一个循环迭代中将禁用&lt;code&gt;operation&lt;/code&gt;分支。当从通道接收到的消息为偶数时，&lt;code&gt;operation&lt;/code&gt;将被重置，&lt;code&gt;done&lt;/code&gt;也将置为&lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;单任务并发&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tokio::spawn&lt;/code&gt;和&lt;code&gt;select!&lt;/code&gt;都可以运行异步并发操作，但是，它们运行并发操作时使用的策略并不相同。&lt;code&gt;tokio::spawn&lt;/code&gt;函数接受一个异步操作后生成一个新任务以运行该操作，即Tokio运行时调度的对象是任务。Tokio分别安排了两个不同的任务，它们可能运行在不同的系统线程上，因此，生成任务与生成线程具有相同的限制：禁止借用。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;select!&lt;/code&gt;宏在同一任务上同时运行所有分支。由于&lt;code&gt;select!&lt;/code&gt;宏的所有分支在同一个任务上执行，它们永远不会同时运行。即&lt;code&gt;slect!&lt;/code&gt;宏在单个任务上复用了异步操作。&lt;/p&gt;
&lt;h1&gt;流&lt;/h1&gt;
&lt;hr /&gt;
&lt;p&gt;流，就是一系列异步值，是Rust的&lt;a href=&quot;https://doc.rust-lang.org/book/ch13-02-iterators.html&quot;&gt;&lt;code&gt;std::iter::Iterator&lt;/code&gt;&lt;/a&gt;的异步版本，用&lt;a href=&quot;https://docs.rs/futures-core/0.3/futures_core/stream/trait.Stream.html&quot;&gt;&lt;code&gt;Stream&lt;/code&gt;&lt;/a&gt; trait表示。在异步函数中可以迭代遍历流，也可以使用适配器转换流。Tokio在&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html&quot;&gt;&lt;code&gt;StreamExt&lt;/code&gt;&lt;/a&gt; trait上提供了许多通用适配器。&lt;/p&gt;
&lt;p&gt;Tokio在一个独立的crate中提供流支持：&lt;code&gt;tokio-stream&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tokio-stream = &quot;0.1&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution
目前，Tokio的Stream实用程序放在&lt;code&gt;tokio-stream&lt;/code&gt; crate 中。一旦&lt;code&gt;Steam&lt;/code&gt; triat在Rust标准库中稳定下来，Tokio的流实用程序将被移动到&lt;code&gt;tokio&lt;/code&gt;中。
:::&lt;/p&gt;
&lt;h2&gt;迭代&lt;/h2&gt;
&lt;p&gt;目前，Rust语言还不支持异步&lt;code&gt;for&lt;/code&gt;循环，代替方案是使用&lt;code&gt;while let&lt;/code&gt;循环配合&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.next&quot;&gt;&lt;code&gt;StreamExt::next()&lt;/code&gt;&lt;/a&gt;在流上进行迭代。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio_stream::StreamExt;

#[tokio::main]
async fn main() {
    let mut stream = tokio_stream::iter(&amp;amp;[1, 2, 3]);

    while let Some(v) = stream.next().await {
        println!(&quot;GOT = {:?}&quot;, v);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与迭代器类似，&lt;code&gt;next()&lt;/code&gt;方法返回&lt;code&gt;Option&amp;lt;T&amp;gt;&lt;/code&gt;，&lt;code&gt;T&lt;/code&gt;为流内值的类型，若收到&lt;code&gt;None&lt;/code&gt;则表示流迭代终止。&lt;/p&gt;
&lt;h3&gt;Mini-Redis广播&lt;/h3&gt;
&lt;p&gt;让我们来再看一个使用Mini-Redis客户端的稍微复杂的例子。&lt;/p&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://github.com/tokio-rs/website/blob/master/tutorial-code/streams/src/main.rs&quot;&gt;这里&lt;/a&gt;找到完整的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio_stream::StreamExt;
use mini_redis::client;

async fn publish() -&amp;gt; mini_redis::Result&amp;lt;()&amp;gt; {
    let mut client = client::connect(&quot;127.0.0.1:6379&quot;).await?;

    // Publish some data
    client.publish(&quot;numbers&quot;, &quot;1&quot;.into()).await?;
    client.publish(&quot;numbers&quot;, &quot;two&quot;.into()).await?;
    client.publish(&quot;numbers&quot;, &quot;3&quot;.into()).await?;
    client.publish(&quot;numbers&quot;, &quot;four&quot;.into()).await?;
    client.publish(&quot;numbers&quot;, &quot;five&quot;.into()).await?;
    client.publish(&quot;numbers&quot;, &quot;6&quot;.into()).await?;
    Ok(())
}

async fn subscribe() -&amp;gt; mini_redis::Result&amp;lt;()&amp;gt; {
    let client = client::connect(&quot;127.0.0.1:6379&quot;).await?;
    let subscriber = client.subscribe(vec![&quot;numbers&quot;.to_string()]).await?;
    let messages = subscriber.into_stream();

    tokio::pin!(messages);

    while let Some(msg) = messages.next().await {
        println!(&quot;got = {:?}&quot;, msg);
    }

    Ok(())
}

#[tokio::main]
async fn main() -&amp;gt; mini_redis::Result&amp;lt;()&amp;gt; {
    tokio::spawn(async {
        publish().await
    });

    subscribe().await?;

    println!(&quot;DONE&quot;);

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成一个任务以在Mini-Redis服务器的“number”频道上发布消息。然后，在主任务中订阅“numbers”频道并打印收到的消息。&lt;/p&gt;
&lt;p&gt;订阅后，在返回的订阅者上调用&lt;code&gt;into_stream()&lt;/code&gt;。如此将消耗掉该&lt;code&gt;Subscriber&lt;/code&gt;，并返回一个能够在消息到达时产生消息的流。请注意，在开始迭代消息之前，我们用&lt;a href=&quot;https://docs.rs/tokio/1/tokio/macro.pin.html&quot;&gt;&lt;code&gt;tokio::pin!&lt;/code&gt;&lt;/a&gt;将流&lt;a href=&quot;https://doc.rust-lang.org/std/pin/index.html&quot;&gt;固定&lt;/a&gt;在栈上，因为在流上调用&lt;code&gt;next()&lt;/code&gt;需要固定该流。&lt;code&gt;into_stream()&lt;/code&gt;函数返回一个&lt;em&gt;未固定&lt;/em&gt;的流，我们必须显式固定后才能迭代它。&lt;/p&gt;
&lt;p&gt;固定值的一个关键属性是可以将指针指向固定数据，并且调用者可以确信指针保持有效。 async/await 使用此功能来支持跨 .await 点借用数据。&lt;/p&gt;
&lt;p&gt;:::caution
当一个Rust值不能再在内存中移动时，就称为被“固定”了。对于被固定的值，其中一个关键属性是可以将指针指向该数据，而调用者可以确信指针始终有效。&lt;code&gt;async/await&lt;/code&gt;使用此特性来支持跨&lt;code&gt;.await&lt;/code&gt;时对借用数据进行调用。
:::&lt;/p&gt;
&lt;p&gt;如果我们忘记固定流，就会看到类似下面的错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0277]: `from_generator::GenFuture&amp;lt;[static generator@Subscriber::into_stream::{closure#0} for&amp;lt;&apos;r, &apos;s, &apos;t0, &apos;t1, &apos;t2, &apos;t3, &apos;t4, &apos;t5, &apos;t6&amp;gt; {ResumeTy, &amp;amp;&apos;r mut Subscriber, Subscriber, impl Future, (), std::result::Result&amp;lt;Option&amp;lt;Message&amp;gt;, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t0)&amp;gt;&amp;gt;, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t1)&amp;gt;, &amp;amp;&apos;t2 mut async_stream::yielder::Sender&amp;lt;std::result::Result&amp;lt;Message, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t3)&amp;gt;&amp;gt;&amp;gt;, async_stream::yielder::Sender&amp;lt;std::result::Result&amp;lt;Message, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t4)&amp;gt;&amp;gt;&amp;gt;, std::result::Result&amp;lt;Message, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t5)&amp;gt;&amp;gt;, impl Future, Option&amp;lt;Message&amp;gt;, Message}]&amp;gt;` cannot be unpinned
  --&amp;gt; streams/src/main.rs:29:36
   |
29 |     while let Some(msg) = messages.next().await {
   |                                    ^^^^ within `tokio_stream::filter::_::__Origin&amp;lt;&apos;_, impl Stream, [closure@streams/src/main.rs:22:17: 25:10]&amp;gt;`, the trait `Unpin` is not implemented for `from_generator::GenFuture&amp;lt;[static generator@Subscriber::into_stream::{closure#0} for&amp;lt;&apos;r, &apos;s, &apos;t0, &apos;t1, &apos;t2, &apos;t3, &apos;t4, &apos;t5, &apos;t6&amp;gt; {ResumeTy, &amp;amp;&apos;r mut Subscriber, Subscriber, impl Future, (), std::result::Result&amp;lt;Option&amp;lt;Message&amp;gt;, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t0)&amp;gt;&amp;gt;, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t1)&amp;gt;, &amp;amp;&apos;t2 mut async_stream::yielder::Sender&amp;lt;std::result::Result&amp;lt;Message, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t3)&amp;gt;&amp;gt;&amp;gt;, async_stream::yielder::Sender&amp;lt;std::result::Result&amp;lt;Message, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t4)&amp;gt;&amp;gt;&amp;gt;, std::result::Result&amp;lt;Message, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;t5)&amp;gt;&amp;gt;, impl Future, Option&amp;lt;Message&amp;gt;, Message}]&amp;gt;`
   |
   = note: required because it appears within the type `impl Future`
   = note: required because it appears within the type `async_stream::async_stream::AsyncStream&amp;lt;std::result::Result&amp;lt;Message, Box&amp;lt;(dyn std::error::Error + Send + Sync + &apos;static)&amp;gt;&amp;gt;, impl Future&amp;gt;`
   = note: required because it appears within the type `impl Stream`
   = note: required because it appears within the type `tokio_stream::filter::_::__Origin&amp;lt;&apos;_, impl Stream, [closure@streams/src/main.rs:22:17: 25:10]&amp;gt;`
   = note: required because of the requirements on the impl of `Unpin` for `tokio_stream::filter::Filter&amp;lt;impl Stream, [closure@streams/src/main.rs:22:17: 25:10]&amp;gt;`
   = note: required because it appears within the type `tokio_stream::map::_::__Origin&amp;lt;&apos;_, tokio_stream::filter::Filter&amp;lt;impl Stream, [closure@streams/src/main.rs:22:17: 25:10]&amp;gt;, [closure@streams/src/main.rs:26:14: 26:40]&amp;gt;`
   = note: required because of the requirements on the impl of `Unpin` for `tokio_stream::map::Map&amp;lt;tokio_stream::filter::Filter&amp;lt;impl Stream, [closure@streams/src/main.rs:22:17: 25:10]&amp;gt;, [closure@streams/src/main.rs:26:14: 26:40]&amp;gt;`
   = note: required because it appears within the type `tokio_stream::take::_::__Origin&amp;lt;&apos;_, tokio_stream::map::Map&amp;lt;tokio_stream::filter::Filter&amp;lt;impl Stream, [closure@streams/src/main.rs:22:17: 25:10]&amp;gt;, [closure@streams/src/main.rs:26:14: 26:40]&amp;gt;&amp;gt;`
   = note: required because of the requirements on the impl of `Unpin` for `tokio_stream::take::Take&amp;lt;tokio_stream::map::Map&amp;lt;tokio_stream::filter::Filter&amp;lt;impl Stream, [closure@streams/src/main.rs:22:17: 25:10]&amp;gt;, [closure@streams/src/main.rs:26:14: 26:40]&amp;gt;&amp;gt;`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你遇到类似上面这样的错误，请尝试固定该值。&lt;/p&gt;
&lt;p&gt;在运行上面的代码之前，先启动Mini-Redis服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mini-redis-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再尝试运行代码，我们就会在STDOUT中看到如下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;1&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;two&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;3&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;four&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;five&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;6&quot; })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于订阅和发布之间存在竞争，一些早期消息可能会被丢弃。该程序永远不会退出，只要服务端保持活动状态，对Mini-Redis频道的订阅就会保持活动状态。&lt;/p&gt;
&lt;p&gt;让我们看看如何使用流来扩展这个程序。&lt;/p&gt;
&lt;h2&gt;适配器&lt;/h2&gt;
&lt;p&gt;接受一个&lt;a href=&quot;https://docs.rs/futures-core/0.3/futures_core/stream/trait.Stream.html&quot;&gt;&lt;code&gt;Stream&lt;/code&gt;&lt;/a&gt;并返回另一个&lt;a href=&quot;https://docs.rs/futures-core/0.3/futures_core/stream/trait.Stream.html&quot;&gt;&lt;code&gt;Stream&lt;/code&gt;&lt;/a&gt;的函数通常称为“流适配器”，因为这属于“适配器模式”。常见的流适配器包括&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.map&quot;&gt;&lt;code&gt;map&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.take&quot;&gt;take&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.filter&quot;&gt;filter&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;更新Mini-Redis以使其可以退出。我们希望收到三个消息后就停止遍历。这可以使用&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.take&quot;&gt;&lt;code&gt;take&lt;/code&gt;&lt;/a&gt;完成，此适配器限制流&lt;strong&gt;最多只能&lt;/strong&gt;产生&lt;code&gt;n&lt;/code&gt;条消息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let messages = subscriber
    .into_stream()
    .take(3);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行程序，输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;1&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;two&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;3&quot; })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而这一次程序执行完毕后退出。&lt;/p&gt;
&lt;p&gt;现在，让我们将流中的信息限制为一位数，我们将通过检查消息长度来实现这一点。这次使用&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.filter&quot;&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/a&gt;适配器，以丢弃任何与此断言不匹配的消息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let messages = subscriber
    .into_stream()
    .filter(|msg| match msg {
        Ok(msg) if msg.content.len() == 1 =&amp;gt; true,
        _ =&amp;gt; false,
    })
    .take(3);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再次运行程序，输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;1&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;3&quot; })
got = Ok(Message { channel: &quot;numbers&quot;, content: b&quot;6&quot; })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意，适配器的应用顺序很重要。先调用&lt;code&gt;filter&lt;/code&gt;再调用&lt;code&gt;take&lt;/code&gt;与先调用&lt;code&gt;take&lt;/code&gt;再调用&lt;code&gt;filter&lt;/code&gt;是不同的。&lt;/p&gt;
&lt;p&gt;最后，我们将整理输出，即删除输出中的&lt;code&gt;Ok(Message { ... })&lt;/code&gt;部分，这次使用&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.map&quot;&gt;&lt;code&gt;map&lt;/code&gt;&lt;/a&gt;。因为这是在&lt;code&gt;filter&lt;/code&gt;&lt;strong&gt;之后&lt;/strong&gt;应用的，我们知道消息是&lt;code&gt;Ok&lt;/code&gt;，因此可以使用&lt;code&gt;unwrap()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let messages = subscriber
    .into_stream()
    .filter(|msg| match msg {
        Ok(msg) if msg.content.len() == 1 =&amp;gt; true,
        _ =&amp;gt; false,
    })
    .map(|msg| msg.unwrap().content)
    .take(3);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时的输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;got = b&quot;1&quot;
got = b&quot;3&quot;
got = b&quot;6&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种选择是使用&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.filter_map&quot;&gt;&lt;code&gt;filter_map&lt;/code&gt;&lt;/a&gt;将&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.filter&quot;&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html#method.map&quot;&gt;&lt;code&gt;map&lt;/code&gt;&lt;/a&gt;合并在一个调用中。&lt;/p&gt;
&lt;p&gt;还有更多可用的适配器，请查看&lt;a href=&quot;https://docs.rs/tokio-stream/0.1/tokio_stream/trait.StreamExt.html&quot;&gt;此列表&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;实现&lt;code&gt;Stream&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/futures-core/0.3/futures_core/stream/trait.Stream.html&quot;&gt;&lt;code&gt;Stream&lt;/code&gt;&lt;/a&gt; trait与&lt;a href=&quot;https://doc.rust-lang.org/std/future/trait.Future.html&quot;&gt;&lt;code&gt;Future&lt;/code&gt;&lt;/a&gt; trait非常相似。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Stream {
    type Item;

    fn poll_next(
        self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, 
        cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;
    ) -&amp;gt; Poll&amp;lt;Option&amp;lt;Self::Item&amp;gt;&amp;gt;;

    fn size_hint(&amp;amp;self) -&amp;gt; (usize, Option&amp;lt;usize&amp;gt;) {
        (0, None)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Stream::poll_next()&lt;/code&gt;函数很像&lt;code&gt;Future::poll&lt;/code&gt;，不同在于你可以重复调用它，以从流中接收许多更多值。正如我们在&lt;a href=&quot;./#9-%E6%B7%B1%E5%85%A5%E5%BC%82%E6%AD%A5&quot;&gt;深入异步&lt;/a&gt;中看到的那样，当流的返回值尚未就绪时，将返回&lt;code&gt;Poll::Pending&lt;/code&gt;，此时任务的唤醒器已注册，一旦要再次轮询流，唤醒器就会收到通知。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;size_hint()&lt;/code&gt;方法的使用方式与其在&lt;a href=&quot;https://doc.rust-lang.org/book/ch13-02-iterators.html&quot;&gt;迭代器&lt;/a&gt;上的使用方式相同。&lt;/p&gt;
&lt;p&gt;通常，在手动实现&lt;code&gt;Stream&lt;/code&gt;时，是通过组合futures和其他流来完成的。作为一个例子，让我们再次以&lt;a href=&quot;./#9-%E6%B7%B1%E5%85%A5%E5%BC%82%E6%AD%A5&quot;&gt;深入异步&lt;/a&gt;中实现的&lt;code&gt;Delay&lt;/code&gt; future为基础构建。这次实现为以10毫秒为间隔产生三次&lt;code&gt;()&lt;/code&gt;的流。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use tokio_stream::Stream;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;

struct Interval {
    rem: usize,
    delay: Delay,
}

impl Stream for Interval {
    type Item = ();

    fn poll_next(mut self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;)
        -&amp;gt; Poll&amp;lt;Option&amp;lt;()&amp;gt;&amp;gt;
    {
        if self.rem == 0 {
            // No more delays
            return Poll::Ready(None);
        }

        match Pin::new(&amp;amp;mut self.delay).poll(cx) {
            Poll::Ready(_) =&amp;gt; {
                let when = self.delay.when + Duration::from_millis(10);
                self.delay = Delay { when };
                self.rem -= 1;
                Poll::Ready(Some(()))
            }
            Poll::Pending =&amp;gt; Poll::Pending,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;async-stream&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;使用&lt;a href=&quot;https://docs.rs/futures-core/0.3/futures_core/stream/trait.Stream.html&quot;&gt;&lt;code&gt;Stream&lt;/code&gt;&lt;/a&gt;特性手动实现流可能很乏味。然而，Rust尚不支持使用&lt;code&gt;async/await&lt;/code&gt;语法定义流，该特性正在实现，只是尚未完成。&lt;/p&gt;
&lt;p&gt;可将&lt;a href=&quot;https://docs.rs/async-stream&quot;&gt;&lt;code&gt;async-stream&lt;/code&gt;&lt;/a&gt; crate当作临时解决方案。该crate提供了一个&lt;code&gt;stream!&lt;/code&gt;宏可以将输入转换为流。使用该crate，上面的interval可以这样实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use async_stream::stream;
use std::time::{Duration, Instant};

stream! {
    let mut when = Instant::now();
    for _ in 0..3 {
        let delay = Delay { when };
        delay.await;
        yield ();
        when += Duration::from_millis(10);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>PingCap的Rust训练课程4：并发与并行</title><link>https://zlotus.github.io/2022/06/18/pna-rust-project-4/</link><guid isPermaLink="true">https://zlotus.github.io/2022/06/18/pna-rust-project-4/</guid><description>PingCap的Rust训练课程4：并发与并行</description><pubDate>Sat, 18 Jun 2022 18:40:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文内容大多翻译自原文：&lt;a href=&quot;https://github.com/pingcap/talent-plan/tree/master/courses/rust/projects/project-4&quot;&gt;PNA Rust Project 4: Concurrency and parallelism&lt;/a&gt;。
:::&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;任务&lt;/strong&gt;：使用自定义协议，创建一个具有同步网络的&lt;em&gt;多线程&lt;/em&gt;、持久化键/值存储的服务端和客户端。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写一个简单的线程池&lt;/li&gt;
&lt;li&gt;使用通道进行跨线程通信&lt;/li&gt;
&lt;li&gt;利用锁共享数据结构&lt;/li&gt;
&lt;li&gt;无锁执行读操作&lt;/li&gt;
&lt;li&gt;对单线程与多线程版本进行基准测试
&lt;strong&gt;关键词&lt;/strong&gt;：线程池、通道、锁、无锁数据结构、原子化、参数化基准测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在这个项目中，您将创建一个使用自定义协议进行通信的简单键/值服务端和客户端。服务端将使用同步网络，并将使用较为复杂的并发实现来响应多个请求。内存索引将改为并发数据结构，由所有线程共享，而压缩操作将在专用的线程上完成，以减少单个请求的延迟。&lt;/p&gt;
&lt;h2&gt;项目需求规格&lt;/h2&gt;
&lt;p&gt;cargo项目&lt;code&gt;kvs&lt;/code&gt;建立了一个名为&lt;code&gt;kvs-client&lt;/code&gt;的命令行键值存储客户端，和一个名为&lt;code&gt;kvs-server&lt;/code&gt;的键值存储服务端，二者又都调用了一个名为&lt;code&gt;kvs&lt;/code&gt;的库。客户端通过一个自定义协议与服务端通信。&lt;/p&gt;
&lt;p&gt;命令行规格与&lt;a href=&quot;https://github.com/pingcap/talent-plan/blob/master/courses/rust/projects/project-3/README.md&quot;&gt;前一个项目&lt;/a&gt;相同。本项目与前一个项目的的不同之处在于并发实现，在我们实现时会对其进行描述。&lt;/p&gt;
&lt;p&gt;库的规格几乎相同，除了以下两点：一是这一次所有的&lt;code&gt;KvsEngine&lt;/code&gt;、&lt;code&gt;KvStore&lt;/code&gt;等类型的方法都使用&lt;code&gt;&amp;amp;self&lt;/code&gt;而不是&lt;code&gt;&amp;amp;mut self&lt;/code&gt;，我们将实现&lt;code&gt;Clone&lt;/code&gt; trait，这在并发数据结构中很常见。但为什么呢？其实并不是说我们将不再编写不可变代码，尽管它们将会在线程之间共享。那为什么要在方法签名中避免使用&lt;code&gt;&amp;amp;mut self&lt;/code&gt;？也许您现在还不清楚，但在本项目结束时，它会变得显而易见。&lt;/p&gt;
&lt;p&gt;二是本项目中的库包含一个新的trait：&lt;code&gt;ThreadPool&lt;/code&gt;。它包含以下方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ThreadPool::new(threads: u32) -&amp;gt; Result&amp;lt;ThreadPool&amp;gt;&lt;/code&gt;
创建一个新的线程池，立即生成指定数量的线程。
如果未能生成线程，则返回错误。所有先前产生的线程都被结束。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ThreadPool::spawn&amp;lt;F&amp;gt;(&amp;amp;self, job: F) where F: FnOnce() + Send + &apos;static&lt;/code&gt;
在线程池中运行一个函数。
运行操作总是成功的，但如果函数发生panic，线程池仍将继续以相同数量的线程运行 — 线程数不会减少，线程池也不会被析构、损坏或失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到这个项目结束时，该特性将有几个实现，您将再次执行基准测试来比较它们。&lt;/p&gt;
&lt;p&gt;本项目完全不需要对客户端代码进行任何修改。&lt;/p&gt;
&lt;h2&gt;项目设置&lt;/h2&gt;
&lt;p&gt;继续上一个项目，删除之前的测试目录，并复制本项目的测试目录。这个项目应该包含一个名为&lt;code&gt;kvs&lt;/code&gt;的库，以及两个可执行文件，&lt;code&gt;kvs-server&lt;/code&gt;和&lt;code&gt;kvs-client&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;中需要以下开发依赖项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dev-dependencies]
assert_cmd = &quot;0.11&quot;
criterion = &quot;0.2.11&quot;
crossbeam-utils = &quot;0.6.5&quot;
predicates = &quot;1.0.0&quot;
rand = &quot;0.6.5&quot;
tempfile = &quot;3.0.7&quot;
walkdir = &quot;2.2.7&quot;
panic-control = &quot;0.1.4&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与以前的项目一样，添加足够的定义以使测试套件通过编译。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;添加新模块&lt;code&gt;thread_pool&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
└── src
   └── thread_pool
      ├── mod.rs
      ├── naive.rs
      ├── rayon.rs
      └── shared_queue.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;lib.rs&lt;/code&gt;中添加&lt;code&gt;thread_pool&lt;/code&gt;模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod thread_pool;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;mod.rs&lt;/code&gt;中声明三种线程池：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//! This module provides various thread pools. All thread pools should implement
//! the `ThreadPool` trait.
use crate::Result;

mod naive;
mod rayon;
mod shared_queue;

pub use self::naive::NaiveThreadPool;
pub use self::rayon::RayonThreadPool;
pub use self::shared_queue::SharedQueueThreadPool;

/// The trait that all thread pools should implement.
pub trait ThreadPool {
    /// Creates a new thread pool, immediately spawning the specified number of
    /// threads.
    ///
    /// Returns an error if any thread fails to spawn. All previously-spawned threads
    /// are terminated.
    fn new(threads: u32) -&amp;gt; Result&amp;lt;Self&amp;gt;
    where
        Self: Sized;

    /// Spawns a function into the thread pool.
    ///
    /// Spawning always succeeds, but if the function panics the threadpool continues
    /// to operate with the same number of threads &amp;amp;mdash; the thread count is not
    /// reduced nor is the thread pool destroyed, corrupted or invalidated.
    fn spawn&amp;lt;F&amp;gt;(&amp;amp;self, job: F)
    where
        F: FnOnce() + Send + &apos;static;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三种线程池都用最简单的标准线程做实现，以通过编译。这里以&lt;code&gt;naive.rs&lt;/code&gt;为例，其他两个也使用这样的实现即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::thread;
use super::ThreadPool;

use crate::Result;

/// It is actually not a thread pool. It spawns a new thread every time
/// the `spawn` method is called.
pub struct NaiveThreadPool;

impl ThreadPool for NaiveThreadPool {
    fn new(_threads: u32) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        Ok(NaiveThreadPool)
    }

    fn spawn&amp;lt;F&amp;gt;(&amp;amp;self, job: F)
    where
        F: FnOnce() + Send + &apos;static,
    {
        thread::spawn(job);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;知识背景：阻塞和多线程&lt;/h2&gt;
&lt;p&gt;到目前为止，您已经在单线程上处理了所有请求，包括读取和写入（例如&quot;get&quot;和&quot;set&quot;）。换句话说，数据库中的所有请求都是&lt;em&gt;串行的&lt;/em&gt;。使用我们将在这个项目中重复的图表，时间流向如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    thread
           +  +--------+--------+--------+--------+
      T1   |  |   R1   |   R2   |   W1   |   W2   |
           +  +--------+--------+--------+--------+

              --&amp;gt; read/write reqs over time --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;读取和写入操作都可能需要阻塞。阻塞是指线程在等待访问资源时停止执行（例如等待访问文件中的数据，或等待访问受锁保护的变量）。当一个线程在一个任务上被阻塞时，便可不能执行另一个任务。因此，在I/O密集型系统中，任何请求可能都在花费大量时间等待操作系统和内存从磁盘读取数据或将数据写入磁盘：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;          +---------+----------------------------+---------+
      R1  | working | waiting for data ...       | working |
          +---------+----------------------------+---------+

          --&amp;gt; time --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The simplest way to put the CPU back to work while one request is blocked is to service requests on multiple threads, so that ideally our requests are all processed concurrently, and — if we have enough CPUs — in parallel:&lt;/p&gt;
&lt;p&gt;在一个请求被阻塞时，让CPU能够继续工作的最简单方法，是创建多个线程一起提供服务。因此理想情况下，我们的请求都是并发处理的 — 如果我们有足够的 CPU — 即并行处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    thread
           +  +--------+
      T1   |  |   R1   |
           |  +--------+
      T2   |  |   R2   |
           |  +--------+
      T3   |  |   W1   |
           |  +--------+
      T4   |  |   W2   |
           +  +--------+

              --&amp;gt; read/write reqs over time --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以这将是本项目的重点 — 并行处理请求。&lt;/p&gt;
&lt;h2&gt;第1部分：多线程&lt;/h2&gt;
&lt;p&gt;鉴于我们首次引入并发概念，就用最简单的方法：为每个传入的连接创建一个新线程，并响应该连接上的请求，然后让线程退出。如此，将工作分发到多个线程将提供怎样的性能收益？您预计延迟会受到怎样的影响？吞吐量呢？&lt;/p&gt;
&lt;p&gt;第一步是为这种简单方法编写一个&lt;code&gt;ThreadPool&lt;/code&gt;实现，其中&lt;code&gt;ThreadPool::spawn&lt;/code&gt;将为每个生成的作业创建一个新线程。我们称之为&lt;code&gt;NaiveThreadPool&lt;/code&gt;（实际上它甚至不能算是一个&lt;em&gt;真正的&lt;/em&gt;线程池，因为这个实现不会在各作业间重用线程，但仍需要符合我们的trait规范以供之后的基准测试比较）。&lt;/p&gt;
&lt;p&gt;我们现在不关注更复杂的实现，因为即使将这个简单方法集成到我们现有的设计中仍需要不少努力。请注意，&lt;code&gt;ThreadPool::new&lt;/code&gt;构造函数接受一个&lt;code&gt;threads&lt;/code&gt;参数，用于指定池中的线程数。在此实现中，该参数将处于未使用状态。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;现在来实现这个版本的&lt;code&gt;ThreadPool&lt;/code&gt;，然后我们将它集成到新的&lt;code&gt;KvStore&lt;/code&gt;中。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;需要通过的测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;thread_pool::naive_thread_pool_*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;经过“项目设置”一节的修改，现在的代码已经能够通过&lt;code&gt;cargo test --test thread_pool&lt;/code&gt;的所有测试（实现时再修改）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
running 4 tests
test rayon_thread_pool_spawn_counter ... ok
test naive_thread_pool_spawn_counter ... ok
test shared_queue_thread_pool_spawn_counter ... ok
test shared_queue_thread_pool_panic_task ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第2部分：创建可共享&lt;code&gt;KvsEngine&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;在我们将&lt;code&gt;NaiveThreadPool&lt;/code&gt;集成到&lt;code&gt;KvsServer&lt;/code&gt;之前，我们必须创建&lt;code&gt;KvsEngine&lt;/code&gt; trait并实现&lt;code&gt;KvStore&lt;/code&gt;（现在您可以忽略上一个项目中的 &lt;code&gt;SledKvsEngine&lt;/code&gt;，当然，您可以重新实现它作为这个项目的附加作业）。&lt;/p&gt;
&lt;p&gt;回想一下项目需求规格规范，在本项目中，&lt;code&gt;KvsEngine&lt;/code&gt;会使用&lt;code&gt;&amp;amp;self&lt;/code&gt;作为&lt;code&gt;self&lt;/code&gt;，而不是上一个项目中的&lt;code&gt;&amp;amp;mut self&lt;/code&gt;。此外，还需要为每个实现，显式编写&lt;code&gt;Clone&lt;/code&gt;，并为它们隐式添加&lt;code&gt;Send + &apos;static&lt;/code&gt;。具体定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub trait KvsEngine: Clone + Send + &apos;static {
    fn set(&amp;amp;self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt;;

    fn get(&amp;amp;self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt;;

    fn remove(&amp;amp;self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个trait签名提供了很代码实现相关的信息。首先想想当我们要使用多线程实现时，为什么需要引擎实现&lt;code&gt;Clone&lt;/code&gt;。参考Rust中其他并发数据类型的设计，例如&lt;a href=&quot;https://doc.rust-lang.org/std/sync/struct.Arc.html&quot;&gt;&lt;code&gt;Arc&lt;/code&gt;&lt;/a&gt;。再想想为什么需要使用&lt;code&gt;&amp;amp;self&lt;/code&gt;，而不是&lt;code&gt;&amp;amp;mut self&lt;/code&gt;。您对共享可变状态了解多少？在这个项目结束时，请确保你理解这里的含义 — &lt;em&gt;这就是我们使用Rust的意义&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;在这个模型中，&lt;code&gt;KvsEngine&lt;/code&gt;的行为类似于另一个对象的&lt;em&gt;句柄&lt;/em&gt;，并且由于该对象需要在线程之间共享，它可能需要保持在&lt;a href=&quot;https://stackoverflow.com/questions/79923/what-and-where-are-the-stack-and-heap&quot;&gt;堆&lt;/a&gt;上，并且因为该共享状态不能是可变的，它需要由一些同步原语保护。&lt;/p&gt;
&lt;p&gt;因此，&lt;em&gt;使用线程安全的共享指针类型将&lt;code&gt;KvsEngine&lt;/code&gt;、&lt;code&gt;KvStore&lt;/code&gt;中的数据移动到堆上，并将其保护在您选择的锁之后。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;由于&lt;code&gt;SledKvsEngine&lt;/code&gt;实现了&lt;code&gt;KvsEngine&lt;/code&gt;，它可能也需要更改。&lt;/p&gt;
&lt;p&gt;此时，您的单线程&lt;code&gt;kvs-server&lt;/code&gt;应该可以再次工作，但现在有了一个可以在以后跨线程共享的&lt;code&gt;KvsEngine&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;需要通过的测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kv_store::concurrent_*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Arc&lt;/code&gt;会将内容物放在堆上，并可以使用&lt;code&gt;clone&lt;/code&gt;创建指向同一个地址的引用；&lt;/li&gt;
&lt;li&gt;之所以修改trait定义之后可以不使用&lt;code&gt;mut&lt;/code&gt;引用，是因为实现时将以类似&lt;code&gt;self.writer.lock().unwrap().set(key, value)&lt;/code&gt;的方式使用&lt;code&gt;KvsEngine&lt;/code&gt;的写操作，而读操作封装在&lt;code&gt;RefCell&lt;/code&gt;中即可共享；&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;KvStore&lt;/code&gt;中的内存索引改为可并发的&lt;code&gt;Arc&amp;lt;SkipMap&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt;方式，并将写对象封装在&lt;code&gt;Arc&amp;lt;Mutex&amp;lt;...&amp;gt;&amp;gt;&lt;/code&gt;中，以便在多个线程中执行写操作；&lt;/li&gt;
&lt;li&gt;创建一个单线程的&lt;code&gt;KvStoreReader&lt;/code&gt;，将日志文件句柄封装在&lt;code&gt;RefCell&lt;/code&gt;中以便共享读操作。继续负责从&lt;code&gt;gen&lt;/code&gt;日志文件中读取&lt;code&gt;CommandPos&lt;/code&gt;处的json数据并解析为&lt;code&gt;Command&lt;/code&gt;返回；&lt;/li&gt;
&lt;li&gt;创建一个跨线程的&lt;code&gt;KvStoreWriter&lt;/code&gt;接管原&lt;code&gt;KvStore&lt;/code&gt;中的写日志操作，包括&lt;code&gt;set&lt;/code&gt;、&lt;code&gt;remove&lt;/code&gt;、&lt;code&gt;compact&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第3部分：向&lt;code&gt;KvsServer&lt;/code&gt;添加多线程&lt;/h2&gt;
&lt;p&gt;让我们在这里快速回顾一下我们的架构：&lt;code&gt;KvsServer&lt;/code&gt;设置一个TCP套接字并开始监听；当收到一个请求时，将反序列化并调用&lt;code&gt;KvsEngine&lt;/code&gt; trait的一些实现来存储或检索数据；最后返回响应。&lt;code&gt;KvsEngine&lt;/code&gt;如何工作的细节与&lt;code&gt;KvsServer&lt;/code&gt;无关。&lt;/p&gt;
&lt;p&gt;因此，在上一个项目中，您可能模糊地创建了一个循环，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let listener = TcpListener::bind(addr)?;

for stream in listener.incoming() {
    let cmd = self.read_cmd(&amp;amp;stream);
    let resp = self.process_cmd(cmd);
    self.respond(&amp;amp;stream, resp);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;现在您只需要做类似的事情，然后把循环内的工作都放在&lt;code&gt;NaiveThreadPool&lt;/code&gt;中执行&lt;/em&gt;。这会将数据库查询和响应工作放在与TCP监听不同的线程中，从而将大部分繁重的工作转移到其他线程，以允许接收线程处理更多请求。如此便能够增加吞吐量，至少在多核机器上是这样。&lt;/p&gt;
&lt;p&gt;于是，您现在仍然有一对有效的客户端/服务端键值存储，只不过现在是多线程的。&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;在循环中&lt;code&gt;clone&lt;/code&gt; &lt;code&gt;KvsEngine&lt;/code&gt;，然后将&lt;code&gt;KvsEngine&lt;/code&gt;和&lt;code&gt;ThreadPool&lt;/code&gt;一起传给线程执行函数&lt;code&gt;serve&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;此时&lt;code&gt;serve&lt;/code&gt;由于有了&lt;code&gt;KvsEngine&lt;/code&gt;的&lt;code&gt;clone&lt;/code&gt;引用，也不需要通过&lt;code&gt;self&lt;/code&gt;调用引擎了，因此可以不再当做类函数使用；&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;bin/kvs-server.rs&lt;/code&gt;中创建服务时需要加上&lt;code&gt;ThreadPool&lt;/code&gt;参数，可以在&lt;code&gt;Cargo.toml&lt;/code&gt;中引入&lt;code&gt;num_cpus&lt;/code&gt;库，以在创建线程池时自动获取主机cpu个数。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第4部分：创建真正的线程池&lt;/h2&gt;
&lt;p&gt;所以现在你已经有了你的多线程架构，是时候编写一个真正的线程池了。您可能不会在实践中编写自己的线程池，因为可以使用已经过充分测试的线程池crate，不过通过自己编写线程池可以更有效的学习并发相关经验。在本项目接下来的部分中，您将像我们在上一个项目中对引擎所做的那样，抽象线程池，并将您的实现与现有线程池进行性能比较。&lt;/p&gt;
&lt;p&gt;那么，什么是线程池？&lt;/p&gt;
&lt;p&gt;其实也没什么复杂的。为了不给每个要执行的多线程作业都创建一个新线程，我们选择来维护一“池”的线程，并不停重用这些线程以避免不停的创建新线程。&lt;/p&gt;
&lt;p&gt;那么，为什么？&lt;/p&gt;
&lt;p&gt;因为可以提高性能。重用线程可以节省出少量性能，而在编写高性能应用程序时，每一点性能都很重要。想象一下创建一个新线程需要什么：&lt;/p&gt;
&lt;p&gt;您必须有一个调用栈才能运行该线程，从而必须为该调用栈分配空间。虽然分配空间已然相当简单，但仍不如不分配来的简单。调用栈的分配方式取决于操作系统和运行时的细节，但可能涉及锁和系统调用。同样的，虽然系统调用也很简单，但是当我们处理Rust级别的性能时它们也就算不上简单了 — 减少系统调用是简单且常见的优化方式。然后必须仔细初始化该栈，以便第一个&lt;a href=&quot;https://en.wikipedia.org/wiki/Stack_frame&quot;&gt;栈帧&lt;/a&gt;包含适当的基指针值以及栈的初始化&lt;a href=&quot;https://en.wikipedia.org/wiki/Function_prologue&quot;&gt;函数序言&lt;/a&gt;中所需的任何其他值。在Rust中，栈需要配置一个&lt;a href=&quot;https://docs.microsoft.com/en-us/windows/desktop/Memory/creating-guard-pages&quot;&gt;保护页&lt;/a&gt;来防止栈溢出，从而保护内存安全。这需要另外两个系统调用，&lt;a href=&quot;https://github.com/rust-lang/rust/blob/6635fbed4ca8c65822f99e994735bd1877fb063e/src/libstd/sys/unix/thread.rs#L315&quot;&gt;&lt;code&gt;mmap&lt;/code&gt;和&lt;code&gt;mprotect&lt;/code&gt;&lt;/a&gt;（尽管Linux上避免了这两个系统调用）。&lt;/p&gt;
&lt;p&gt;而这只是设置调用栈。创建新线程至少也需要一个系统调用，而内核必须在内部对新线程进行计算。&lt;/p&gt;
&lt;p&gt;在Rust中，C库&lt;a href=&quot;https://www.gnu.org/software/hurd/libpthread.html&quot;&gt;libpthread&lt;/a&gt;库负责处理大多数这种复杂工作。&lt;/p&gt;
&lt;p&gt;然后在某个时刻，操作系统在新栈上执行上下文切换，线程运行。当线程终止时，所有工作都需要再次撤消。&lt;/p&gt;
&lt;p&gt;当使用线程池时，仅有池里的线程需要初始化开销，后续作业只是简单的上下文切换到池中的线程。&lt;/p&gt;
&lt;h3&gt;那么如何构建线程池？&lt;/h3&gt;
&lt;p&gt;有许多策略和权衡，但对于练习项目，您只需将使用一个共享队列将工作分配给空闲线程即可。这意味着您的“生产者”，即接受网络连接的线程，将作业发送到这个队列（或通道）；而“消费者”，即池中的每个空闲线程，从该队列（通道）读取等待作业执行。这是最简单的工作调度策略，而且可能非常高效。那么，这种方法有什么缺点？&lt;/p&gt;
&lt;p&gt;这里有三个重要因素需要考虑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;使用哪种数据结构来分配工作&lt;/em&gt; — 应是一个队列，且应有一个发送者（“生产者”）负责监听TCP连接，同时应用许多接收者（“消费者”）即池中的线程。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;如何处理panic的作业&lt;/em&gt; — 线程池将运行任意作业。如果一个线程发生panic，线程池需要以某种方式恢复。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;如何应对退出&lt;/em&gt; — 即当&lt;code&gt;ThreadPool&lt;/code&gt;对象超出作用域时，它需要关闭每个线程，一定不能不管这些线程。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些问题都是相互交织的，因为每一个问题都可能涉及线程间的通信和同步。有些解决方案会很简单，每个问题的解决方案都可以优雅地协同工作；有些解决方案会很复杂，这些问题的解决方案可能相互独立独立且相互交织。仔细选择您的数据结构，聪明的运用这些数据结构的特点。&lt;/p&gt;
&lt;p&gt;您将通过在某些并发队列类型上发送消息来分发工作（Rust中的并发队列通常是具有两种连接类型的数据结构：发送者类和接收者类；任何实现了&lt;code&gt;Send&lt;/code&gt; + &lt;code&gt;&apos;static&lt;/code&gt;的类型都可以在这两个类之间传递）。&lt;/p&gt;
&lt;p&gt;Rust中的消息通常表示为枚举类型，每个可能发送的消息都有相应变量，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum ThreadPoolMessage {
    RunJob(Box&amp;lt;dyn FnOnce() + Send + &apos;static&amp;gt;),
    Shutdown,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这往往是一种更简单、更有效的解决方案，而不是为了不同目的而试图“兼顾”多个通道。当然，如果只有一种类型的消息，则不需要枚举。现在，上面的示例并不一定是管理线程池所需消息集的全集，这具体取决于设计。比如，如果您的队列返回值表明发送方已被销毁，则通常可以隐式shutdown。&lt;/p&gt;
&lt;p&gt;现在有许多种的多线程队列。在Rust中最常见的是&lt;a href=&quot;https://doc.rust-lang.org/std/sync/mpsc/index.html&quot;&gt;&lt;code&gt;mpsc&lt;/code&gt;&lt;/a&gt;通道，就包含在Rust标准库中。这是一个多生产者、单消费者队列，因此将其用于单队列线程池将需要某种锁。在这里使用锁有什么缺点？Rust中还有许多其他并发队列类型，每种都有优缺点。如果您愿意同时锁定生产者和消费者，那么您甚至可以使用&lt;code&gt;Mutex&amp;lt;VecDeque&amp;gt;&lt;/code&gt;，不过如果存在更好的解决方案，就没有理由在生产中这么做了。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;历史趣事：Rust标准库中包含通道这件事有些奇怪，并且被一些人认为是一个错误，因为它背离了Rust的理念 — 保持最小化标准库、专注抽象操作系统，并让crate生态提供高级数据结构。它们的存在是Rust开发史中刻意为之的，可能来源于Go这样消息传递语言。其他库（例如&lt;a href=&quot;https://github.com/crossbeam-rs/crossbeam&quot;&gt;&lt;code&gt;crossbeam&lt;/code&gt;&lt;/a&gt;）提供了更复杂的替代方案，为不同场景提供更合适的选项😉。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;您的线程池将需要处理作业函数产生panic的情况 — 放任panic销毁线程可能会使池中的线程快速耗尽。因此，如果池中的某个线程panic，您需要确保线程总数不会减少。那该怎么办？您至少有两个选择：当有线程销毁时立刻新建另一个线程，或者捕获panic并保持现有线程运行。这需要您做出权衡并选择其中一种方式，在您的代码中注释您的选择。&lt;/p&gt;
&lt;p&gt;您可能会用到的工具有：&lt;a href=&quot;https://doc.rust-lang.org/std/thread/fn.spawn.html&quot;&gt;&lt;code&gt;thread::spawn&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/std/thread/fn.panicking.html&quot;&gt;&lt;code&gt;thread::panicking&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/std/panic/fn.catch_unwind.html&quot;&gt;&lt;code&gt;catch_unwind&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/std/sync/mpsc/index.html&quot;&gt;&lt;code&gt;mpsc&lt;/code&gt;&lt;/a&gt;通道、&lt;a href=&quot;https://doc.rust-lang.org/std/sync/struct.Mutex.html&quot;&gt;&lt;code&gt;Mutex&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://docs.rs/crossbeam/0.7.1/crossbeam/channel/index.html&quot;&gt;crossbeam的MPMC通道&lt;/a&gt;、&lt;code&gt;thread&lt;/code&gt;的&lt;a href=&quot;https://doc.rust-lang.org/std/thread/struct.JoinHandle.html&quot;&gt;&lt;code&gt;JoinHandle&lt;/code&gt;&lt;/a&gt;。按照您的需求选择合适的工具。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;创建&lt;code&gt;SharedQueueThreadPool&lt;/code&gt;类，实现&lt;code&gt;ThreadPool&lt;/code&gt;。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;需要完成的测试用例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;shared_queue_thread_pool_*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;将&lt;code&gt;KvsServer&lt;/code&gt;中用到的&lt;code&gt;NaiveThreadPool&lt;/code&gt;替换为&lt;code&gt;SharedQueueThreadPool&lt;/code&gt;。同样，您的&lt;code&gt;kvs-server&lt;/code&gt;应该仍然像以前一样工作，只不过这次的多线程模型更高效一些。您应当使用恰当的数量构造线程池，这里推荐使用&lt;code&gt;num_cpus&lt;/code&gt; crate，以为每个CPU创建一个线程。稍后我们将再次讨论线程个数。&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;利用&lt;code&gt;crossbeam::channel&lt;/code&gt;创建一对&lt;code&gt;tx&lt;/code&gt;/&lt;code&gt;rx&lt;/code&gt;（发送者/接收者），&lt;code&gt;tx&lt;/code&gt;用于在线程池的&lt;code&gt;spawn&lt;/code&gt;中向&lt;code&gt;channel&lt;/code&gt;发送作业，&lt;code&gt;rx&lt;/code&gt;负责接收并执行这些作业；&lt;/li&gt;
&lt;li&gt;我们目前的策略为放任线程panic，并立刻创建新线程。要实现这一策略需要将&lt;code&gt;rx&lt;/code&gt;封装在新类&lt;code&gt;TaskReceiver&lt;/code&gt;中，然后控制该类的&lt;code&gt;Drop&lt;/code&gt;行为，当发生&lt;a href=&quot;https://doc.rust-lang.org/std/thread/fn.panicking.html&quot;&gt;&lt;code&gt;thread::panicking()&lt;/code&gt;&lt;/a&gt;时，克隆本&lt;code&gt;TaskReceiver&lt;/code&gt;实例并放入新建的线程中以继续接收新的作业；&lt;/li&gt;
&lt;li&gt;线程的执行函数，只需使用&lt;code&gt;loop&lt;/code&gt;持续等待作业并执行作业中的函数即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;use std::thread;
use crossbeam::channel::{self, Sender, Receiver};
use log::{debug, error};

use super::ThreadPool;

use crate::Result;

// Note for Rust training course: the thread pool is not implemented using
// `catch_unwind` because it would require the task to be `UnwindSafe`.

/// A thread pool using a shared queue inside.
///
/// If a spawned task panics, the old thread will be destroyed and a new one will be
/// created. It fails silently when any failure to create the thread at the OS level
/// is captured after the thread pool is created. So, the thread number in the pool
/// can decrease to zero, then spawning a task to the thread pool will panic.
pub struct SharedQueueThreadPool {
    tx: Sender&amp;lt;Box&amp;lt;dyn FnOnce() + Send + &apos;static&amp;gt;&amp;gt;, 
}

impl ThreadPool for SharedQueueThreadPool {
    fn new(threads: u32) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        let (tx, rx) = channel::unbounded::&amp;lt;Box&amp;lt;dyn FnOnce() + Send + &apos;static&amp;gt;&amp;gt;();
        for _ in 0..threads {
            let rx = TaskReceiver(rx.clone());
            thread::Builder::new().spawn(|| run_task(rx))?;
        }
        Ok(SharedQueueThreadPool { tx })
    }

    /// Spawns a function into the thread pool.
    ///
    /// # Panics
    ///
    /// Panics if the thread pool has no thread.
    fn spawn&amp;lt;F&amp;gt;(&amp;amp;self, job: F)
    where
        F: FnOnce() + Send + &apos;static,
    {
        self.tx
            .send(Box::new(job))
            .expect(&quot;The thread pool has no thread.&quot;);
    }
}


#[derive(Clone)]
struct TaskReceiver(Receiver&amp;lt;Box&amp;lt;dyn FnOnce() + Send + &apos;static&amp;gt;&amp;gt;);

impl Drop for TaskReceiver {
    fn drop(&amp;amp;mut self) {
        if thread::panicking() {
            let rx = self.clone();
            if let Err(e) = thread::Builder::new().spawn(move || run_task(rx)) {
                error!(&quot;Failed to spawn a thread: {}&quot;, e);
            }
        }
    }
}

fn run_task(rx: TaskReceiver) {
    loop {
        match rx.0.recv() {
            Ok(task) =&amp;gt; {
                task();
            },
            Err(_) =&amp;gt; debug!(&quot;Thread exits because the thread pool destoryed. &quot;),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第5部分：抽象线程池&lt;/h2&gt;
&lt;p&gt;与在之前的项目中抽象出&lt;code&gt;KvsEngine&lt;/code&gt;来比较不同的实现一样，现在您应抽象出&lt;code&gt;ThreadPool&lt;/code&gt;来做类似的事。&lt;/p&gt;
&lt;p&gt;如果您还没有这么做，请向&lt;code&gt;KvsServer&lt;/code&gt;添加第二个类型参数以表示&lt;code&gt;ThreadPool&lt;/code&gt;实现，构造函数以线程池作为第二个参数，并使用该线程池来分发作业。&lt;/p&gt;
&lt;p&gt;最后，使用&lt;a href=&quot;https://docs.rs/rayon/&quot;&gt;&lt;code&gt;rayon&lt;/code&gt;&lt;/a&gt; crate中的&lt;code&gt;ThreadPool&lt;/code&gt;创建另一个&lt;code&gt;ThreadPool&lt;/code&gt;，以实现&lt;code&gt;RayonThreadPool&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Rayon&lt;/code&gt;的线程池使用一种称为“&lt;a href=&quot;https://www.dre.vanderbilt.edu/~schmidt/PDF/work-stealing-dequeue.pdf&quot;&gt;工作窃取&lt;/a&gt;”的更复杂的调度策略，预计它的性能会比我们的实现更好，不过在我们尝试之前谁知道呢！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use super::ThreadPool;

use crate::{Result, KvsError};

/// Wrapper of rayon::ThreadPool
pub struct RayonThreadPool{
    pool: rayon::ThreadPool,
}

impl ThreadPool for RayonThreadPool {
    fn new(threads: u32) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        let pool = rayon::ThreadPoolBuilder::new()
            .num_threads(threads as usize)
            .build()
            .map_err(|e| KvsError::StringError(format!(&quot;{}&quot;, e)))?;
        Ok(RayonThreadPool{pool})
    }

    fn spawn&amp;lt;F&amp;gt;(&amp;amp;self, job: F)
    where
        F: FnOnce() + Send + &apos;static,
    {
        self.pool.spawn(job)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第6部分：评估您的线程池&lt;/h2&gt;
&lt;p&gt;现在您将编写&lt;em&gt;6&lt;/em&gt;个基准测试，一个写入繁重的工作负载比较&lt;code&gt;SharedQueueThreadPool&lt;/code&gt;在不同线程数下的性能，一个读取繁重的工作负载比较&lt;code&gt;SharedQueueThreadPool&lt;/code&gt;在不同线程数下的性能；再写两个测试，类似前面两个只不过用来测试&lt;code&gt;RayonThreadPool&lt;/code&gt;；最后还有两个将&lt;code&gt;RayonThreadPool&lt;/code&gt;与&lt;code&gt;SledKvsEngine&lt;/code&gt;结合使用。&lt;/p&gt;
&lt;p&gt;看上去要写6个其实并没有那么多 — 因为后四个基本上是复制前两个。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意：接下来的两节描述了一组相当复杂的基准测试。它们确实可以实现出来（可能……还没有人写过），但有效理解和高效编写都比较难。两节确实介绍了一些有用的&lt;code&gt;criterion&lt;/code&gt;特性，但如果对你来说内容过于庞杂，可以选择跳过（也可以告诉我们有哪些内容不适合你）。不过，这里的困难可能是一个很好的学习机会。最后，实现这些基准测试需要实现以编程方式关闭&lt;code&gt;KvsServer&lt;/code&gt;的方法（即不发送&lt;code&gt;SIGKILL&lt;/code&gt;并让操作系统执行退出），我们之前还没有讨论过。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;其中一部分工作就是让将前一个项目中的&lt;code&gt;SledKvsEngine&lt;/code&gt;在本项目的多线程环境中再次工作。这应该不难，因为&lt;code&gt;sled&lt;/code&gt;可以被克隆并在线程间发送，就像您编写的引擎一样。&lt;/p&gt;
&lt;p&gt;希望结果会很有趣。&lt;/p&gt;
&lt;p&gt;您将再次使用&lt;code&gt;criterion&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些将是&lt;em&gt;参数化&lt;/em&gt;基准测试，即使用不同参数多次运行的单个测试，而&lt;code&gt;criterion&lt;/code&gt;支持&lt;a href=&quot;https://bheisler.github.io/criterion.rs/book/user_guide/benchmarking_with_inputs.html&quot;&gt;使用参数作为基准测试的输入&lt;/a&gt;。这里的基准测试参数将是线程池中的线程数。&lt;/p&gt;
&lt;p&gt;您将尝试测试服务器在各种条件下的吞吐量。您将同时发送许多请求，等待响应，然后结束。您可能会好奇，服务器CPU数量与线程总数的关系，是如何影响吞吐量的；您的线程池与&lt;code&gt;rayon&lt;/code&gt;相比性能如何；以及在多线程环境下您的&lt;code&gt;KvStore&lt;/code&gt;与&lt;code&gt;SledKvsEngine&lt;/code&gt;的比较。&lt;/p&gt;
&lt;p&gt;由于您的&lt;code&gt;KvsClient&lt;/code&gt;（可能会）被阻塞，即请求后等待响应，这会使测试变的复杂。如果是非阻塞的，那么您可以发送许多请求而无需等待响应，然后再收集响应。而使用阻塞的&lt;code&gt;KvsClient&lt;/code&gt;，您将需要在独立的线程中发送每个请求，以使服务饱和。&lt;/p&gt;
&lt;p&gt;在进行基准测试时，一定要清楚您希望评估哪些代码，并尽可能只去测试那一部分代码。像&lt;code&gt;criterion&lt;/code&gt;这样的基准测试库在一个循环中多次运行一段代码，测量它通过每个循环所花费的时间。因此，应当只将您想要评估的代码放入循环中，并将无关代码尽可能的留在循环外。&lt;/p&gt;
&lt;p&gt;因此，以这个带有输入的简单&lt;code&gt;criterion&lt;/code&gt;为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let c = Criterion::default();
let inputs = &amp;amp;[1, 2, 3, 4, 5];

c.bench_function_over_inputs(&quot;example&quot;, |b, &amp;amp;&amp;amp;num| {
    b.iter(|| {
        // important measured work goes here
    });
}, inputs);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;iter&lt;/code&gt;多次调用您的闭包，测量每次迭代。但是如此就需要事先设置大量线程，您并不希望这么做。如果只需要设置一次，就能够在多次迭代中使用，则应该将设置过程放在闭包之外，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let c = Criterion::default();
let inputs = &amp;amp;[1, 2, 3, 4, 5];

c.bench_function_over_inputs(&quot;example&quot;, |b, &amp;amp;&amp;amp;num| {
    // do setup here
    b.iter(|| {
        // important measured work goes here
    });
}, inputs);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只评估&lt;code&gt;b.iter&lt;/code&gt;闭包中的代码，其余的环境设置代码都放在前面。&lt;/p&gt;
&lt;p&gt;如果设置无法放在循环之前，那么另一种策略是使设置占用的工作量小于想要评估的代码的工作量，例如添加循环。也要考虑基准测试中的“析构”部分，通常指运行&lt;code&gt;drop&lt;/code&gt;的成本。&lt;/p&gt;
&lt;p&gt;如果您有一个阻塞客户端，则客户端将需要许多线程，而在执行循环之前，您只有一次机会创建这些线程。因此，在基准测试执行迭代前，您需要设置一堆可复用的线程。幸运的是，&lt;code&gt;SharedQueueThreadPool&lt;/code&gt;就是一个很好的工具。为每个请求设置一个线程，并将其与某个通道配对，以报告收到响应，这就成了一个合适的基准测试工具。&lt;/p&gt;
&lt;h3&gt;现在开始编写前两个基准测试&lt;/h3&gt;
&lt;p&gt;前面提到这是一个参数化基准测试，参数就是服务器线程池中要使用的CPU核数。我们想看看只有1个、2个、4个等每个偶数一直到CPU核数的2倍时，吞吐量都是什么表现。至于为什么是2倍，也许拥有比内核更多的线程可能会有好处，您将通过实验来发现。&lt;/p&gt;
&lt;p&gt;对于密集写入的作业，在环境设置期间（即调用&lt;code&gt;b.iter(...)&lt;/code&gt;之前）先创建&lt;code&gt;KvsServer&amp;lt;KvStore, SharedQueueThreadPool&amp;gt;&lt;/code&gt;，线程池使用参数化的线程数。然后编写一个作业，为1000个等长的键设置相同的值。请注意，尽管键不同，但为了测试结果的一致性，应在每个循环中使用同一套键。&lt;/p&gt;
&lt;p&gt;然后在每次线程写入键和值后，也应该&lt;code&gt;assert!&lt;/code&gt;调用成功（以确保作业执行中没有出错），从而表明作业成功完成。当所有线程都完成后，基准测试线程继续运行并结束迭代。实现这种信号式结束的最直观方法是让每个作业线程将消息发送回基准测试线程，但请记住，这些信号代码是与您希望评估的代码毫无关系的开销，因此它的工作量应尽可能少。您可以只使用一条消息，或者使用其他并发类型，仅向基准测试线程发出一次信号吗？&lt;/p&gt;
&lt;p&gt;将此基准测试命名为&lt;code&gt;write_queued_kvstore&lt;/code&gt;（或其他）。&lt;/p&gt;
&lt;p&gt;对于密集读取作业，在环境设置期间先创建&lt;code&gt;KvServer&amp;lt;KvStore, SharedQueueThreadPool&amp;gt;&lt;/code&gt;，线程池使用参数化的线程数。然后编写保护1000个线程的客户端线程池。仍然在环境设置阶段，创建另一个客户端并初始化1000个不同的等长键，并全部使用相同的值。&lt;/p&gt;
&lt;p&gt;然后，在基准测试循环中，为客户端生成1000个检索相同键/值解析的作业，然后&lt;code&gt;assert!&lt;/code&gt;结果是正确的。最后，像以前一样，向基准测试线程发送一条消息，表示读取已完成。&lt;/p&gt;
&lt;p&gt;将此基准测试命名为&lt;code&gt;read_queued_kvstore&lt;/code&gt;（或其他）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;的确有不少工作要做。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;您可以像往常一样使用&lt;code&gt;cargo bench&lt;/code&gt;运行这组&lt;code&gt;criterion&lt;/code&gt;基准测试。&lt;/p&gt;
&lt;p&gt;只不过这次您有更多工作要完成。由于您将要在多个参数上执行相同的基准测试，即以线程池中的线程数为参数，如果能将这些结果体现在一个漂亮的图表中，以看到不同线程数的影响，将会使测试结果更加直观。&lt;/p&gt;
&lt;p&gt;恰好&lt;code&gt;criterion&lt;/code&gt;就有这个功能！&lt;/p&gt;
&lt;p&gt;请再次并阅读有关&lt;a href=&quot;https://bheisler.github.io/criterion.rs/book/user_guide/benchmarking_with_inputs.html&quot;&gt;使用参数作为基准测试的输入&lt;/a&gt;的内容。文章解释了如何制作输入的基准测试图标。您注意到了什么？当您的线程数接近服务器CPU核数时会发生什么？当线程数超过服务器的线程数时会发生什么？您认为是什么导致了测试结果中的趋势？结果取决于许多因素，因此您的结果可能与其他任何人都不同。&lt;/p&gt;
&lt;p&gt;这是始终进行基准测试而不非推测性能的一个很好的理由。我们可以做出有根据的猜测，但直到我们测试才会知道结果。&lt;/p&gt;
&lt;h2&gt;第7部分：评估其他线程池和引擎&lt;/h2&gt;
&lt;p&gt;到这里，您已经解决了基准测试练习中最困难的部分。现在您只需在之前的基础上做更多配置性工作即可。&lt;/p&gt;
&lt;p&gt;拿您之前写的那两个基准，然后复制粘贴三遍。将这些副本中的&lt;code&gt;SharedQueueThreadPool&lt;/code&gt;更改为&lt;code&gt;RayonThreadPool&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;将第三个和第四个命名为&lt;code&gt;read/write_rayon_kvstore&lt;/code&gt;（或其他）。这两个将与前两个&lt;code&gt;SharedQueueThreadPool&lt;/code&gt;实现进行比较，以了解您的实现与&lt;code&gt;RayonThreadPool&lt;/code&gt;之间的区别。&lt;/p&gt;
&lt;p&gt;第五个和第六个，命名为&lt;code&gt;read/write_rayon_sledkvengine&lt;/code&gt;，将引擎改成&lt;code&gt;SledKvsEngine&lt;/code&gt;。这些您将与前两个进行比较，以了解您的&lt;code&gt;KvsEngine&lt;/code&gt;与多线程环境中的&lt;code&gt;sled&lt;/code&gt;有什么区别。&lt;/p&gt;
&lt;p&gt;和以前一样，运行并绘制所有这些基准测试。如上所述将它们相互比较，在各种线程数下，您的调度与&lt;code&gt;rayon&lt;/code&gt;相比如何？在各种线程数下，您的存储引擎与&lt;code&gt;sled&lt;/code&gt;相比如何？结果令人惊讶吗？你能想象为什么存在差异吗？&lt;/p&gt;
&lt;h3&gt;扩展1：比较函数&lt;/h3&gt;
&lt;p&gt;现在，您为三个不同的线程池执行了相同的基准测试，您运行了也比较了它们的性能。&lt;code&gt;criterion&lt;/code&gt;内置支持比较多个实现。查看Criterion用户手册中的“&lt;a href=&quot;https://bheisler.github.io/criterion.rs/book/user_guide/comparing_functions.html&quot;&gt;比较函数&lt;/a&gt;”并修改您的基准测试，以便让&lt;code&gt;criterion&lt;/code&gt;自己进行比较，看看那些华丽的图表。&lt;/p&gt;
&lt;h3&gt;背景：锁的极限&lt;/h3&gt;
&lt;p&gt;在本项目前期，我们建议通过将&lt;code&gt;KvsEngine&lt;/code&gt;内部数据放在堆上并保护在锁之后来保证其线程安全。您可能立即意识到这不会提高吞吐量，因为它只是将一种阻塞换成了另一种阻塞 — 将原来的阻塞磁盘访问换成了现在的阻塞互斥访问。&lt;/p&gt;
&lt;p&gt;所以到目前为止，我们所取得的成就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    thread
           +  +--------+
      T1   |  |   R1   |
           |  +-----------------+
      T2   |           |   R2   |
           |           +-----------------+
      T3   |                    |   W1   |
           |                    +-----------------+
      T4   |                             |   W2   |
           +                             +--------+
              --&amp;gt; read/write reqs over time --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上一节中，您对您的引擎与&lt;code&gt;SledKvsEngine&lt;/code&gt;的多线程吞吐量进行了基准测试。希望您已经发现，您的多线程实现比&lt;code&gt;sled&lt;/code&gt;的性能要差得多（如果不是，要么是您的实现非常棒，要么是&lt;code&gt;sled&lt;/code&gt;出了什么问题）。到目前为止，添加多线程导致的性能比单线程实现更差 — 现在您的实现还需要执行线程间上下文切换的额外工作，以及为了保证互斥锁而强加的阻塞。&lt;/p&gt;
&lt;p&gt;因此，对于项目的这一部分将变得更加复杂。用锁保护整个状态很容易 — 整个状态总是以原子方式读写，于是一次只有一个客户端可以访问整个状态。但这也意味着想要访问共享状态的两个线程必须互相等待。换句话说，当&lt;code&gt;KvsEngine&lt;/code&gt;受互斥锁保护时，尽管是多线程的，但服务器中的实际并发量非常少。&lt;/p&gt;
&lt;p&gt;高性能、可扩展、并行的软件倾向于尽可能避免锁和锁争用。与大多数语言相比，Rust使复杂且高性能的并发模式变的更容易（因为您无需担心数据竞争和程序崩溃），但它并不能阻止您写出可能导致错误程序行为的逻辑bug。&lt;/p&gt;
&lt;p&gt;所以你仍然需要对并发进行一些认真的思考。幸运的是，Rust crate生态中有许多复杂的并行编程工具，因此您的任务通常只是了解它们是什么以及如何将它们组合在一起，而不是了解如何编写自己的复杂无锁数据结构。&lt;/p&gt;
&lt;p&gt;让我们看一些更复杂的例子。我们将以单线程&lt;code&gt;KvStore&lt;/code&gt;为例，并考虑如何将其改为线程安全的。&lt;/p&gt;
&lt;p&gt;这是一个单线程&lt;code&gt;KvStore&lt;/code&gt;示例，就像您在早期项目中创编写的那样（这是课程示例项目中的简化版本）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct KvStore {
    /// Directory for the log and other data
    path: PathBuf,
    /// The log reader
    reader: BufReaderWithPos&amp;lt;File&amp;gt;,
    /// The log writer
    writer: BufWriterWithPos&amp;lt;File&amp;gt;,
    /// The in-memory index from key to log pointer
    index: BTreeMap&amp;lt;String, CommandPos&amp;gt;,
    /// The number of bytes representing &quot;stale&quot; commands that could be
    /// deleted during a compaction
    uncompacted: u64,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而这是简单的多线程版本，用锁保护一切。希望您的实现看起来已经类似这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Clone)]
pub struct KvStore(Arc&amp;lt;Mutex&amp;lt;SharedKvStore&amp;gt;&amp;gt;);

#[derive(Clone)]
pub struct SharedKvStore {
    /// Directory for the log and other data
    path: PathBuf,
    /// The log reader
    reader: BufReaderWithPos&amp;lt;File&amp;gt;,
    /// The log writer
    writer: BufWriterWithPos&amp;lt;File&amp;gt;,
    /// The in-memory index from key to log pointer
    index: BTreeMap&amp;lt;String, CommandPos&amp;gt;,
    /// The number of bytes representing &quot;stale&quot; commands that could be
    /// deleted during a compaction
    uncompacted: u64,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如此&lt;code&gt;Arc&amp;lt;Mutex&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;的解决方案，简单、正确且常见：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/std/sync/struct.Arc.html&quot;&gt;&lt;code&gt;Arc&lt;/code&gt;&lt;/a&gt;将值放在堆上，以便它可以在线程之间共享，并提供一个&lt;code&gt;clone&lt;/code&gt;方法来为每个线程创建一个“句柄”；&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/std/sync/struct.Mutex.html&quot;&gt;&lt;code&gt;Mutex&lt;/code&gt;&lt;/a&gt;提供了一种在不使用&lt;code&gt;&amp;amp;mut&lt;/code&gt;引用的情况下，获得该值的写访问权限的方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于许多情况，这是一个完全合理的解决方案。但在本项目中，互斥锁将成为锁争用的源头：互斥锁不仅会串行化&lt;code&gt;SharedKvStore&lt;/code&gt;的写访问，还会串行化读访问。任何想要使用&lt;code&gt;KvStore&lt;/code&gt;的线程都需要等待&lt;code&gt;Mutex&lt;/code&gt;被另一个线程解锁。所有请求都会阻塞任何其他并发请求。&lt;/p&gt;
&lt;p&gt;我们真正想要的是不必使用锁，或者 — 如果确实需要锁的 — 它们尽量少的与其他线程竞争。&lt;/p&gt;
&lt;p&gt;互斥锁的进阶是&lt;a href=&quot;https://doc.rust-lang.org/std/sync/struct.RwLock.html&quot;&gt;&lt;code&gt;RwLock&lt;/code&gt;&lt;/a&gt;，即“读写锁”。这是每个并行软件开发者都必须知道的另一种常见锁。读写锁对互斥体的改进是它允许任意数量的读取，或单个写入。即用Rust术语，&lt;code&gt;RwLock&lt;/code&gt;可以同时支持任意数量的&lt;code&gt;&amp;amp;&lt;/code&gt;引用，或单个&lt;code&gt;&amp;amp;mut&lt;/code&gt;引用。读会被写阻塞，写会阻塞其他所有读和写。&lt;/p&gt;
&lt;p&gt;在我们的数据库中，这意味着可以同时满足所有读取请求，但是当单个写入请求进入时，系统中的所有其他活动都会停止并等待该写操作完成。实现这一点很简单，基本上就是将&lt;code&gt;Mutex&lt;/code&gt;换成&lt;code&gt;RwLock&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在，再次考虑我们的多线程示意图，最终的处理流程如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    thread
           +  +--------+
      T1   |  |   R1   |
           |  +--------+
      T2   |  |   R2   |
           |  +-----------------+
      T3   |           |   W1   |
           |           +-----------------+
      T4   |                    |   W2   |
           +                    +--------+
              --&amp;gt; read/write reqs over time --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第8部分：无锁读&lt;/h2&gt;
&lt;p&gt;对于本项目，我们想要尝试去创建永远不会锁定的读取器，即发生并发写入也依然不会阻塞读取。无论写入请求如何，都可以始终为读取请求提供服务。（写如现在仍然会阻塞其他写操作 — 除了成为一项具有挑战性的并行编程问题，并行写入本身是否有意义仍是一个难以回答的问题）。&lt;/p&gt;
&lt;p&gt;我们最终的期望是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    thread
           +  +--------+
      T1   |  |   R1   |
           |  +--------+
      T2   |  |   R2   |
           |  +--------+
      T3   |  |   W1   |
           |  +-----------------+
      T4   |           |   W2   |
           +           +--------+
              --&amp;gt; read/write reqs over time --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果能做到这一点，那么我们将实现无锁的读操作：即使有读操作在等待来自文件系统的数据（被阻塞），所有类型的其他操作（读和写）都可以继续进行。不过，这仍然不足以保证系统始终可以为读请求提供服务。想想如果在大小为&lt;code&gt;N&lt;/code&gt;的线程池上存在&lt;code&gt;N&lt;/code&gt;个阻塞的写请求会发生什么。稍后你需要考虑这个问题，但现在，您的重点是从读操作中移除锁。&lt;/p&gt;
&lt;p&gt;与&lt;code&gt;Mutex&lt;/code&gt;和&lt;code&gt;RwLock&lt;/code&gt;不同，并没有一种封装类型可以应用于整个任意共享状态，以实现同时读取和写入的目标（至少不能同时还具有高性能）。&lt;/p&gt;
&lt;p&gt;这意味着我们需要考虑如何使用&lt;code&gt;SharedKvStore&lt;/code&gt;的每个字段，选择正确的同步方案以允许执行尽可能多的线程，并能够继续保持数据的逻辑一致性。&lt;/p&gt;
&lt;p&gt;这就是多线程真正困难的地方。如果你移除那个大锁，Rust仍然会保护您免受&lt;a href=&quot;https://blog.regehr.org/archives/490&quot;&gt;数据竞争&lt;/a&gt;的影响，但它并不能帮助您保持数据的逻辑一致性。&lt;/p&gt;
&lt;p&gt;所以在考虑解决方案之前，让我们考虑一下我们的需求。我们要：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同时在多个线程上读取内存索引和磁盘日志；&lt;/li&gt;
&lt;li&gt;将命令写入磁盘，同时维护内存索引；&lt;/li&gt;
&lt;li&gt;读与写并行，因此：&lt;/li&gt;
&lt;li&gt;一般来说，为了保证读与写并发时总是能够读到一致的状态，这意味着：
&lt;ul&gt;
&lt;li&gt;维护一个不变量，使其实在指向日志中的一个有效命令；&lt;/li&gt;
&lt;li&gt;维护一些适当的不变量以供其他的记录行为使用，比如为了下例&lt;code&gt;uncompacted&lt;/code&gt;的记录；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;定期压缩磁盘数据，同时为读操作维护不变量；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本节的其余部分是对有助于实现上述目标的各种背景知识的介绍，也这是项目的最终目标：修改&lt;code&gt;KvStore&lt;/code&gt;以同时执行读取和写入。&lt;/p&gt;
&lt;h3&gt;示例数据结构的解释&lt;/h3&gt;
&lt;p&gt;为了更具体的讨论，我们将需要一个示例，以展示应受保护的数据和应被维护的不变量。下面是一个&lt;code&gt;KvStore&lt;/code&gt;实现及其字段的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct KvStore {
    /// Directory for the log and other data
    path: PathBuf,
    /// The log reader
    reader: BufReaderWithPos&amp;lt;File&amp;gt;,
    /// The log writer
    writer: BufWriterWithPos&amp;lt;File&amp;gt;,
    /// The in-memory index from key to log pointer
    index: BTreeMap&amp;lt;String, CommandPos&amp;gt;,
    /// The number of bytes representing &quot;stale&quot; commands that could be
    /// deleted during a compaction
    uncompacted: u64,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是本项目示例的简化版本。&lt;/p&gt;
&lt;p&gt;各字段的作用非常明确：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;path&lt;/code&gt;：&lt;code&gt;PathBuf&lt;/code&gt;只是日志的目录路径，永远不会改变 — 它是不可变的，不可变的类型在Rust中是同步的，所以它甚至根本不需要任何保护。它可以被任何线程通过共享引用同时读取。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;reader&lt;/code&gt;：&lt;code&gt;HashMap&amp;lt;u64, BufReaderWithPos&amp;lt;File&amp;gt;&amp;gt;&lt;/code&gt;是当前日志文件的读句柄。它是可变的，即压缩后会指向新的日志文件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;writer&lt;/code&gt;：&lt;code&gt;BufWriterWithPos&amp;lt;File&amp;gt;&lt;/code&gt;是当前日志文件的写句柄。任何写入都需要对&lt;code&gt;writer&lt;/code&gt;的可变访问，并且压缩过程需要修改&lt;code&gt;writer&lt;/code&gt;和&lt;code&gt;current_gen&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;index&lt;/code&gt;：&lt;code&gt;BTreeMap&amp;lt;String, CommandPos&amp;gt;&lt;/code&gt;是数据库中每个键到其所在日志文件中具体位置的内存索引。它从可以从每个读线程读取，从每个写线程写入，即使在压缩期间也能正常访问。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;uncompacted&lt;/code&gt;：&lt;code&gt;u64&lt;/code&gt;仅用于计算日志中已被后续写入命令取代的“陈旧”命令的总长度，用以触发日志压缩操作。&lt;/p&gt;
&lt;p&gt;在以前的项目中，我们不必担心写、读和压缩之间的交叉会产生不一致的结果，因为它们都发生在同一个线程上。现在，如果您对数据结构的选择和使用不够小心，很容易破坏数据库的状态。&lt;/p&gt;
&lt;h3&gt;消除锁的策略&lt;/h3&gt;
&lt;p&gt;高级并行编程的关键是了解可用的工具以及使用它们的时机。以下是我们在实施此项目时发现的一些有用的技术，其中一些您也将用到。这些技术将以上面的数据结构为例进行讲解。&lt;/p&gt;
&lt;h4&gt;理解并维护顺序一致性&lt;/h4&gt;
&lt;p&gt;（请注意，“顺序一致性”一词有其准确含义，但在这里我们只是概括性地讨论如何确保以特定顺序执行作业）。&lt;/p&gt;
&lt;p&gt;理解并行编程的关键在于理解代码各部分的“执行顺序”间的关系。在这个线程中，要让本线程早于其他线程看到数据，共享数据结构需要怎样改动？要让本线程早于其他线程暴露内部数据，共享数据结构需要怎样改动？我如何保证执行结果？&lt;/p&gt;
&lt;p&gt;在单线程代码中，推断出任意行代码之前发生了什么很简单 — 如果代码写在前面，则会在前面执行，反之则会在后面执行。但这实际上并不是这样，即使在单线程代码中：为了使代码更高效的运行，CPU和编译器都会重新组织代码执行的顺序，只不过CPU使用器机码，编译器使用用以生成机器码的内部编码。实际上的代码执行顺序跟您编写代码的顺序并不相同，代码只是看起来在按您编写的顺序运行，因为CPU和编译器都会跟踪&lt;em&gt;数据依赖&lt;/em&gt;，它们并不会打乱依赖顺序重新排列操作。&lt;/p&gt;
&lt;p&gt;在多线程代码中，CPU和编译器&lt;em&gt;仍&lt;/em&gt;使用与单线程相同的条件重新排序代码，而且您的代码块会被打碎重排，除非您使用同步类型和操作告诉编译器不允许重新排序。&lt;/p&gt;
&lt;p&gt;任何必须在特定操作之前或之后发生的操作，都必须显式使用同步类型或操作，如使用锁、原子类型等等。&lt;/p&gt;
&lt;p&gt;在上面的例子中，文件写入和内存索引写入显然应以特定的顺序发生 — 如果索引在文件更新前更新会发生什么？另外，例子中包含另一个状态，即未压缩命令的总长度&lt;code&gt;uncompacted&lt;/code&gt;。错误的计算未压缩命令的长度将产生什么影响？如果在将数据写入文件之前就可以看到&lt;code&gt;uncompacted&lt;/code&gt;发生了变化可能还好，但是必须为每个这样的独立同步值制定策略。&lt;/p&gt;
&lt;h4&gt;确定不可变值&lt;/h4&gt;
&lt;p&gt;你可能已经了解了很多关于Rust中的不可变性，以及不可变值如何轻松的在线程间共享（它们具有&lt;code&gt;Sync&lt;/code&gt; trait）。不可变值最适合并发 — 只需将它们放在&lt;code&gt;Arc&lt;/code&gt;后面即可。&lt;/p&gt;
&lt;p&gt;在本例中，&lt;code&gt;PathBuf&lt;/code&gt;是不可变的。&lt;/p&gt;
&lt;h4&gt;复制值而不是复制共享&lt;/h4&gt;
&lt;p&gt;在Rust中我们可能不太喜欢克隆，特别是克隆大小不确定的类型，如&lt;code&gt;String&lt;/code&gt;和&lt;code&gt;Vec&lt;/code&gt;。但是克隆的存在是完全合理的：在某些情况下避免克隆可能非常困难，而且CPU也非常擅长复制内存缓冲区。此外，在本例中，服务端所需的状态副本的数量，实际上受线程池中线程数量的限制。&lt;/p&gt;
&lt;p&gt;在本例中，&lt;code&gt;PathBuf&lt;/code&gt;也很容易克隆。&lt;/p&gt;
&lt;p&gt;考虑一个潜在问题，如何跨线程共享对文件的访问。&lt;a href=&quot;https://doc.rust-lang.org/std/fs/struct.File.html&quot;&gt;&lt;code&gt;File&lt;/code&gt;&lt;/a&gt;类型对读和写操作均需要可变访问。因此，其跨线程共享需要使用锁以授权可变访问。那么，什么是文件？它并不是一份实际上的文件 — 而只是磁盘上物理资源的句柄，一个文件同时存在多个打开句柄是可以的。注意&lt;code&gt;File&lt;/code&gt;的API — 并没有实现&lt;code&gt;Clone&lt;/code&gt;，虽然它确实有个诱人的&lt;a href=&quot;https://doc.rust-lang.org/std/fs/struct.File.html#method.try_clone&quot;&gt;&lt;code&gt;try_clone&lt;/code&gt;&lt;/a&gt;方法，它的语义对多线程代码会产生复杂的影响，如seek原文件的位置会同步到&lt;code&gt;try_clone&lt;/code&gt;创建的另一个文件。请考虑&lt;code&gt;File::open&lt;/code&gt;和&lt;code&gt;try_clone&lt;/code&gt;中文件之间的区别。使用&lt;code&gt;try_clone&lt;/code&gt;还是&lt;code&gt;File::open&lt;/code&gt;，将是您的选择。查看&lt;a href=&quot;https://stackoverflow.com/questions/1687275/what-is-the-difference-between-read-and-pread-in-unix&quot;&gt;&lt;code&gt;pread&lt;/code&gt;&lt;/a&gt;可能会有所帮助。&lt;/p&gt;
&lt;h4&gt;按角色分解数据结构&lt;/h4&gt;
&lt;p&gt;在例子中，我们有两个明确的角色：读和写（也许还有第三个用于压缩）。将读逻辑和写逻辑分离并各封装为一个并发类型在Rust中很常见。读逻辑有自己的数据集，写逻辑也一样，这就为封装提供了很好的条件，所有读操作划为一种类型，而所有写操作划为另一种类型。&lt;/p&gt;
&lt;p&gt;这种划分将进一步使两者都访问哪些资源变得非常明显，因为读类型和写类型都将包含这些资源的共享句柄。&lt;/p&gt;
&lt;h4&gt;使用专门的并发数据结构&lt;/h4&gt;
&lt;p&gt;知道哪些工具可用以及该在哪些场景中使用可能是并行编程中最困难的部分。除了学校里教的基础锁类型，同步数据类型也变得越来越专业化。&lt;/p&gt;
&lt;p&gt;在本项目中，由于内存索引是某种类型的关联数据结构（也称为“映射”），如树或哈希表，那么我们自然会想到是否存在并发关联数据结构。&lt;/p&gt;
&lt;p&gt;确实存在，且正确的使用这些数据结构是完成本项目的关键。&lt;/p&gt;
&lt;p&gt;但是怎么才能找到这些类型呢？第一步是确定是否存在并发映射。您可以阅读&lt;a href=&quot;https://doc.rust-lang.org/std/fs/struct.File.html#method.try_clone&quot;&gt;Rust Discord&lt;/a&gt;上的&lt;code&gt;#beginners&lt;/code&gt;部分，但对于本项目，在网络上搜&quot;concurrent map&quot;即可。&lt;/p&gt;
&lt;p&gt;这是容易的部分，&lt;em&gt;在Rust&lt;/em&gt;中找到正确的并发映射类型则更困难。比较好的起手式是访问&lt;a href=&quot;https://libs.rs/&quot;&gt;libs.rs&lt;/a&gt;。libs.rs与crates.io类似，但crates.io包含所有已发布的库，而libs.rs仅包含受到……好吧，某些人好评的库。因此，如果一个库在libs.rs上，就是库可用的一个指示，另一个是&lt;a href=&quot;https://crates.io/&quot;&gt;crates.io&lt;/a&gt;上的下载计数 — 通常，下载越多的crates被测试的越细。下载计数可以粗略的看作是“担保”库的人数。最后，在聊天中提问也是一个好主意。&lt;/p&gt;
&lt;h4&gt;将清理延后&lt;/h4&gt;
&lt;p&gt;像克隆一样，垃圾收集在Rust中经常遭到反对 — 避免GC几乎就是Rust存的意义。但实际上垃圾收集是不可避免的，“垃圾收集”和“内存回收”实际上是同义词，各种语言都复合使用了多种垃圾收集策略。GC策略轴的一端，在没有自动内存管理的语言（如C）中，垃圾收集完全由程序员决定，如使用&lt;code&gt;malloc&lt;/code&gt;和&lt;code&gt;free&lt;/code&gt;。另一端则是垃圾收集语言，如 Java，所有内存都由一个通用垃圾收集器管理。&lt;/p&gt;
&lt;p&gt;但实际上，在C中并不是所有内存的管理和回收都使用&lt;code&gt;malloc&lt;/code&gt;/&lt;code&gt;free&lt;/code&gt;，在Java也并不是所有内存管理都通过GC完成。举个简单的例子，两者中的高性能应用程序通常都依赖于专门的内存区域，在这些区域既可以重用内存也可以大量解除分配，以优化其内存访问模式。&lt;/p&gt;
&lt;p&gt;同样在Rust中，并非所有内存都被显式地释放。比如在&lt;code&gt;Rc&lt;/code&gt;和&lt;code&gt;Arc&lt;/code&gt;类型中实现了&lt;a href=&quot;https://en.wikipedia.org/wiki/Reference_counting&quot;&gt;资源计数&lt;/a&gt;，也算算算一种简单的GC。&lt;/p&gt;
&lt;p&gt;全局垃圾收集器的最大好处之一，就是使许多无锁数据结构成为可能。学术文献中描述的许多无锁数据结构都依赖于GC执行。&lt;a href=&quot;https://github.com/crossbeam-rs/crossbeam&quot;&gt;&lt;code&gt;crossbeam&lt;/code&gt;&lt;/a&gt;库及其&lt;a href=&quot;https://docs.rs/crossbeam/0.7.1/crossbeam/epoch/index.html&quot;&gt;&lt;code&gt;epoch&lt;/code&gt;&lt;/a&gt;类型的出现，就是在不依赖GC的情况下实现无锁算法。&lt;/p&gt;
&lt;p&gt;也就是说垃圾收集有多种形式，其“将资源清理延迟到未来某个时间”的基本策略在许多场景中都很强大。&lt;/p&gt;
&lt;p&gt;当您现在不知道如何执行一些并发工作时，可以试着考虑：“可以稍后再做吗？”。&lt;/p&gt;
&lt;h4&gt;与原子类型共享标志和计数器&lt;/h4&gt;
&lt;p&gt;在底层，大多数并发数据结构都是使用&lt;a href=&quot;https://preshing.com/20130618/atomic-vs-non-atomic-operations/&quot;&gt;原子操作&lt;/a&gt;或“原子类型”实现的。原子类型在单个内存单元上运行，一般在8到128个字节之间，通常是字长（与指针的字节数相同，并且与Rust&lt;code&gt;usize&lt;/code&gt;类型大小相同）。如果两个线程正确使用原子类型，则一个线程中的写入结果将立即对另一个线程中的读取可见。除了使读取或写入立即可见之外，在Rust中，原子操作还通过&lt;a href=&quot;https://doc.rust-lang.org/std/sync/atomic/enum.Ordering.html&quot;&gt;&lt;code&gt;Ordering&lt;/code&gt;&lt;/a&gt;标志限制编译器和CPU重新排序指令的方式。&lt;/p&gt;
&lt;p&gt;当从粗粒度并行中的锁，转向更细粒度的并行时，通常需要使用原子类型来增强现成的并发数据结构。&lt;/p&gt;
&lt;h3&gt;实现无锁读&lt;/h3&gt;
&lt;p&gt;以上就是相关的背景知识。希望这些知识已经开始引导您考虑了很多东西，并朝着正确的方向前进了。现在轮到实现了：&lt;/p&gt;
&lt;p&gt;&lt;em&gt;修改&lt;code&gt;KvStore&lt;/code&gt;以同时执行读取和写入。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;use std::cell::RefCell;
use std::collections::BTreeMap;
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

use crossbeam_skiplist::SkipMap;
use log::error;
use serde::{Deserialize, Serialize};
use serde_json::Deserializer;
use crate::{KvsError, Result, KvsEngine};
use std::ffi::OsStr;

const COMPACTION_THRESHOLD: u64 = 1024 * 1024;

/// The `KvStore` stores string key/value pairs.
///
/// Key/value pairs are persisted to disk in log files. Log files are named after
/// monotonically increasing generation numbers with a `log` extension name.
/// A `BTreeMap` in memory stores the keys and the value locations for fast query.
///
/// ```rust
/// # use kvs::{KvStore, Result};
/// # fn try_main() -&amp;gt; Result&amp;lt;()&amp;gt; {
/// use std::env::current_dir;
/// use kvs::KvsEngine;
/// let mut store = KvStore::open(current_dir()?)?;
/// store.set(&quot;key&quot;.to_owned(), &quot;value&quot;.to_owned())?;
/// let val = store.get(&quot;key&quot;.to_owned())?;
/// assert_eq!(val, Some(&quot;value&quot;.to_owned()));
/// # Ok(())
/// # }
/// ```
#[derive(Clone)]
pub struct KvStore {
    // map generation number to the file reader
    index: Arc&amp;lt;SkipMap&amp;lt;String, CommandPos&amp;gt;&amp;gt;,
    reader: KvStoreReader,
    // writer of the current log.
    writer: Arc&amp;lt;Mutex&amp;lt;KvStoreWriter&amp;gt;&amp;gt;,
    // the number of bytes representing &quot;stale&quot; commands that could be
    // deleted during a compaction.
}

impl KvStore {
    /// Opens a `KvStore` with the given path.
    ///
    /// This will create a new directory if the given one does not exist.
    ///
    /// # Errors
    ///
    /// It propagates I/O or deserialization errors during the log replay.
    pub fn open(path: impl Into&amp;lt;PathBuf&amp;gt;) -&amp;gt; Result&amp;lt;KvStore&amp;gt; {
        let path = Arc::new(path.into());
        fs::create_dir_all(&amp;amp;*path)?;

        let mut readers = BTreeMap::new();
        let index = Arc::new(SkipMap::new());

        let gen_list = sorted_gen_list(&amp;amp;path)?;
        let mut uncompacted = 0;

        for &amp;amp;gen in &amp;amp;gen_list {
            let mut reader = BufReaderWithPos::new(File::open(log_path(&amp;amp;path, gen))?)?;
            uncompacted += load(gen, &amp;amp;mut reader, &amp;amp;*index)?;
            readers.insert(gen, reader);
        }

        let current_gen = gen_list.last().unwrap_or(&amp;amp;0) + 1;
        let writer = new_log_file(&amp;amp;path, current_gen)?;
        let safe_point = Arc::new(AtomicU64::new(0));

        let reader = KvStoreReader { 
            path: Arc::clone(&amp;amp;path), 
            safe_point,
            readers: RefCell::new(readers),
        };

        let writer = KvStoreWriter {
            reader: reader.clone(),
            writer,
            current_gen, 
            uncompacted,
            path: Arc::clone(&amp;amp;path),
            index: Arc::clone(&amp;amp;index),
        };

        Ok(KvStore {
            reader,
            index,
            writer: Arc::new(Mutex::new(writer)),
        })
    }
}

impl KvsEngine for KvStore {
    /// Sets the value of a string key to a string.
    ///
    /// If the key already exists, the previous value will be overwritten.
    ///
    /// # Errors
    ///
    /// It propagates I/O or serialization errors during writing the log.
    fn set(&amp;amp;self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        self.writer.lock().unwrap().set(key, value)
    }

    /// Gets the string value of a given string key.
    ///
    /// Returns `None` if the given key does not exist.
    ///
    /// # Errors
    ///
    /// It returns `KvsError::UnexpectedCommandType` if the given command type unexpected.
    fn get(&amp;amp;self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt; {
        if let Some(cmd_pos) = self.index.get(&amp;amp;key) {
            if let Command::Set { value, .. } = self.reader.read_command(*cmd_pos.value())? {
                Ok(Some(value))
            } else {
                Err(KvsError::UnexpectedCommandType)
            }
        } else {
            Ok(None)
        }
    }

    /// Removes a given key.
    ///
    /// # Errors
    ///
    /// It returns `KvsError::KeyNotFound` if the given key is not found.
    ///
    /// It propagates I/O or serialization errors during writing the log.
    fn remove(&amp;amp;self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        self.writer.lock().unwrap().remove(key)
    }

}

/// A single thread reader. 
/// 
/// Each `KvStore` instance has its own `KvStoreReader` and 
/// `KvStoreReader`s open the same file separately. So the user 
/// can read concurrently through multiple `KvStore`s in different 
/// threads.
struct KvStoreReader {
    path: Arc&amp;lt;PathBuf&amp;gt;,
    // generation of the latest compaction file
    safe_point: Arc&amp;lt;AtomicU64&amp;gt;,
    readers: RefCell&amp;lt;BTreeMap&amp;lt;u64, BufReaderWithPos&amp;lt;File&amp;gt;&amp;gt;&amp;gt;,
}

impl KvStoreReader {
    /// Close file handles with generation number less than safe_point.
    /// 
    /// `safe_point` is updated to the latest compaction gen after a compaction finished.
    /// The compaction generation contains the sum of all operations before it and the 
    /// in-memory index contains no entries with generation number less than safe_point.
    /// So we can safely close those file handles and the stales file can be deleted.
    fn close_stale_handles(&amp;amp;self) {
        let mut readers = self.readers.borrow_mut();
        while !readers.is_empty() {
            let first_gen = *readers.keys().next().unwrap();
            if self.safe_point.load(Ordering::SeqCst) &amp;lt;= first_gen {
                break;
            }
            readers.remove(&amp;amp;first_gen);
        }
    }

    /// Read the log file at the given `CommandPos`.
    fn read_and&amp;lt;F, R&amp;gt;(&amp;amp;self, cmd_pos: CommandPos, f: F) -&amp;gt; Result&amp;lt;R&amp;gt;
    where
        F: FnOnce(io::Take&amp;lt;&amp;amp;mut BufReaderWithPos&amp;lt;File&amp;gt;&amp;gt;) -&amp;gt; Result&amp;lt;R&amp;gt;,
    {
        self.close_stale_handles();

        let mut readers = self.readers.borrow_mut();
        // Open the file if we haven&apos;t open it in this `KvStoreReader`.
        // We don&apos;t use entry API here because we want the errors to be propogated.
        if !readers.contains_key(&amp;amp;cmd_pos.gen) {
            let reader = BufReaderWithPos::new(File::open(log_path(&amp;amp;self.path, cmd_pos.gen))?)?;
            readers.insert(cmd_pos.gen, reader);
        }
        let reader = readers.get_mut(&amp;amp;cmd_pos.gen).unwrap();
        reader.seek(SeekFrom::Start(cmd_pos.pos))?;
        let cmd_reader = reader.take(cmd_pos.len);
        f(cmd_reader)
    }

    // Read the log file at the given `CommandPos` and deserialize it to `Command`.
    fn read_command(&amp;amp;self, cmd_pos: CommandPos) -&amp;gt; Result&amp;lt;Command&amp;gt; {
        self.read_and(cmd_pos, |cmd_reader| {
            Ok(serde_json::from_reader(cmd_reader)?)
        })
    }
}

impl Clone for KvStoreReader {
    fn clone(&amp;amp;self) -&amp;gt; Self {
        KvStoreReader { 
            path: Arc::clone(&amp;amp;self.path), 
            safe_point: Arc::clone(&amp;amp;self.safe_point), 
            // don&apos;t use other KvStoreReader&apos;s readers
            readers: RefCell::new(BTreeMap::new()), 
        }
    }
}

struct KvStoreWriter {
    reader: KvStoreReader,
    writer: BufWriterWithPos&amp;lt;File&amp;gt;,
    current_gen: u64,
    // the number of bytes representing &quot;stale&quot; commands that could be
    // delete during a compaction
    uncompacted: u64,
    path: Arc&amp;lt;PathBuf&amp;gt;,
    index: Arc&amp;lt;SkipMap&amp;lt;String, CommandPos&amp;gt;&amp;gt;
}

impl KvStoreWriter {
    fn set(&amp;amp;mut self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt;{
        let cmd = Command::set(key, value);
        let pos = self.writer.pos;
        serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;cmd)?;
        self.writer.flush()?;
        if let Command::Set { key, .. } = cmd {
            if let Some(old_cmd) = self.index.get(&amp;amp;key) {
                self.uncompacted += old_cmd.value().len;
            }
            self.index
                .insert(key, (self.current_gen, pos..self.writer.pos).into());
        }

        if self.uncompacted &amp;gt; COMPACTION_THRESHOLD {
            self.compact()?;
        }
        Ok(())
    }

    fn remove(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        if self.index.contains_key(&amp;amp;key) {
            let cmd = Command::remove(key);
            let pos = self.writer.pos;
            serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;cmd)?;
            self.writer.flush()?;
            if let Command::Remove { key } = cmd {
                let old_cmd = self.index.remove(&amp;amp;key).expect(&quot;key not found&quot;);
                self.uncompacted += old_cmd.value().len;
                // the &quot;remove&quot; command itself can be deleted in the next compaction
                // so we add it&apos;s length to `uncompacted`
                self.uncompacted += self.writer.pos - pos;
            }

            if self.uncompacted &amp;gt; COMPACTION_THRESHOLD {
                self.compact()?;
            }
            Ok(())
        } else {
            Err(KvsError::KeyNotFound)
        }
    }

    /// Clear stale entries in log.
    fn compact(&amp;amp;mut self) -&amp;gt; Result&amp;lt;()&amp;gt; {
        // increase current gen by 2. current_gen + 1 is for compaction file
        let compaction_gen  = self.current_gen + 1;
        self.current_gen += 2;
        self.writer = new_log_file(&amp;amp;self.path, self.current_gen)?;

        let mut compaction_writer = new_log_file(&amp;amp;self.path, compaction_gen)?;

        let mut new_pos = 0;
        for entry in self.index.iter() {
            let len = self.reader.read_and(*entry.value(), |mut entry_reader| {
                Ok(io::copy(&amp;amp;mut entry_reader, &amp;amp;mut compaction_writer)?)
            })?;
            self.index.insert(
                entry.key().clone(),
                (compaction_gen, new_pos..new_pos+len).into(),
            );
            new_pos += len;
        }
        compaction_writer.flush()?;

        self.reader
            .safe_point
            .store(compaction_gen, Ordering::SeqCst);
        self.reader.close_stale_handles();

        // remove stale log files.
        // Note that actually these files are not deleted immediately because `KvStoreReader`s
        // still keep open file handles. When `KvStoreReader` is used next time, it will clear
        // its stale file handles. On Unix, the files will be deleted after all the handles
        // are closed. On Windows, the deletions below will fail and stale files are expected
        // to be deleted in the next compaction.
        let stale_gens = sorted_gen_list(&amp;amp;self.path)?
            .into_iter()
            .filter(|&amp;amp;gen| gen &amp;lt; compaction_gen);
        for stale_gen in stale_gens {
            let file_path = log_path(&amp;amp;self.path, stale_gen);
            if let Err(e) = fs::remove_file(&amp;amp;file_path) {
                error!(&quot;{:?} cannot be deleted: {}&quot;, file_path, e);
            }
        }
        self.uncompacted = 0;

        Ok(())
    }

}
/// Create a new log file with given generation number.
///
/// Returns the writer to the log.
fn new_log_file(path: &amp;amp;Path, gen: u64) -&amp;gt; Result&amp;lt;BufWriterWithPos&amp;lt;File&amp;gt;&amp;gt; {
    let path = log_path(&amp;amp;path, gen);
    let writer = BufWriterWithPos::new(
        OpenOptions::new()
            .create(true)
            .write(true)
            .append(true)
            .open(&amp;amp;path)?,
    )?;
    Ok(writer)
}

/// Returns sorted generation numbers in the given directory.
fn sorted_gen_list(path: &amp;amp;Path) -&amp;gt; Result&amp;lt;Vec&amp;lt;u64&amp;gt;&amp;gt; {
    let mut gen_list: Vec&amp;lt;u64&amp;gt; = fs::read_dir(&amp;amp;path)?
        .flat_map(|res| -&amp;gt; Result&amp;lt;_&amp;gt; { Ok(res?.path()) })
        .filter(|path| path.is_file() &amp;amp;&amp;amp; path.extension() == Some(&quot;log&quot;.as_ref()))
        .flat_map(|path| {
            path.file_name()
                .and_then(OsStr::to_str)
                .map(|s| s.trim_end_matches(&quot;.log&quot;))
                .map(str::parse::&amp;lt;u64&amp;gt;)
        })
        .flatten()
        .collect();
    gen_list.sort_unstable();
    Ok(gen_list)
}

/// Load the whole log file and store value locations in the index map.
///
/// Returns how many bytes can be saved after a compaction.
fn load(
    gen: u64,
    reader: &amp;amp;mut BufReaderWithPos&amp;lt;File&amp;gt;,
    index: &amp;amp;SkipMap&amp;lt;String, CommandPos&amp;gt;,
) -&amp;gt; Result&amp;lt;u64&amp;gt; {
    // To make sure we read from the beginning of the file.
    let mut pos = reader.seek(SeekFrom::Start(0))?;
    let mut stream = Deserializer::from_reader(reader).into_iter::&amp;lt;Command&amp;gt;();
    let mut uncompacted = 0; // number of bytes that can be saved after a compaction.
    while let Some(cmd) = stream.next() {
        let new_pos = stream.byte_offset() as u64;
        match cmd? {
            Command::Set { key, .. } =&amp;gt; {
                if let Some(old_cmd) = index.get(&amp;amp;key) {
                    uncompacted += old_cmd.value().len;
                }
                index.insert(key, (gen, pos..new_pos).into());
            }
            Command::Remove { key } =&amp;gt; {
                if let Some(old_cmd) = index.remove(&amp;amp;key) {
                    uncompacted += old_cmd.value().len;
                }
                // the &quot;remove&quot; command itself can be deleted in the next compaction
                // so we add its length to `uncompacted`
                uncompacted += new_pos - pos;
            }
        }
        pos = new_pos;
    }
    Ok(uncompacted)
}

fn log_path(dir: &amp;amp;Path, gen: u64) -&amp;gt; PathBuf {
    dir.join(format!(&quot;{}.log&quot;, gen))
}

/// Struct representing a command.
#[derive(Serialize, Deserialize, Debug)]
enum Command {
    Set { key: String, value: String },
    Remove { key: String },
}

impl Command {
    fn set(key: String, value: String) -&amp;gt; Command {
        Command::Set { key, value }
    }

    fn remove(key: String) -&amp;gt; Command {
        Command::Remove { key }
    }
}

#[derive(Debug, Clone, Copy)]
/// Represents the position and length of a json-serialized command in the log.
struct CommandPos {
    gen: u64,
    pos: u64,
    len: u64,
}

impl From&amp;lt;(u64, Range&amp;lt;u64&amp;gt;)&amp;gt; for CommandPos {
    fn from((gen, range): (u64, Range&amp;lt;u64&amp;gt;)) -&amp;gt; Self {
        CommandPos {
            gen,
            pos: range.start,
            len: range.end - range.start,
        }
    }
}

struct BufReaderWithPos&amp;lt;R: Read + Seek&amp;gt; {
    reader: BufReader&amp;lt;R&amp;gt;,
    pos: u64,
}

impl&amp;lt;R: Read + Seek&amp;gt; BufReaderWithPos&amp;lt;R&amp;gt; {
    fn new(mut inner: R) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        let pos = inner.seek(SeekFrom::Current(0))?;
        Ok(BufReaderWithPos {
            reader: BufReader::new(inner),
            pos,
        })
    }
}

impl&amp;lt;R: Read + Seek&amp;gt; Read for BufReaderWithPos&amp;lt;R&amp;gt; {
    fn read(&amp;amp;mut self, buf: &amp;amp;mut [u8]) -&amp;gt; io::Result&amp;lt;usize&amp;gt; {
        let len = self.reader.read(buf)?;
        self.pos += len as u64;
        Ok(len)
    }
}

impl&amp;lt;R: Read + Seek&amp;gt; Seek for BufReaderWithPos&amp;lt;R&amp;gt; {
    fn seek(&amp;amp;mut self, pos: SeekFrom) -&amp;gt; io::Result&amp;lt;u64&amp;gt; {
        self.pos = self.reader.seek(pos)?;
        Ok(self.pos)
    }
}

struct BufWriterWithPos&amp;lt;W: Write + Seek&amp;gt; {
    writer: BufWriter&amp;lt;W&amp;gt;,
    pos: u64,
}

impl&amp;lt;W: Write + Seek&amp;gt; BufWriterWithPos&amp;lt;W&amp;gt; {
    fn new(mut inner: W) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        let pos = inner.seek(SeekFrom::Current(0))?;
        Ok(BufWriterWithPos {
            writer: BufWriter::new(inner),
            pos,
        })
    }
}

impl&amp;lt;W: Write + Seek&amp;gt; Write for BufWriterWithPos&amp;lt;W&amp;gt; {
    fn write(&amp;amp;mut self, buf: &amp;amp;[u8]) -&amp;gt; io::Result&amp;lt;usize&amp;gt; {
        let len = self.writer.write(buf)?;
        self.pos += len as u64;
        Ok(len)
    }

    fn flush(&amp;amp;mut self) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
        self.writer.flush()
    }
}

impl&amp;lt;W: Write + Seek&amp;gt; Seek for BufWriterWithPos&amp;lt;W&amp;gt; {
    fn seek(&amp;amp;mut self, pos: SeekFrom) -&amp;gt; io::Result&amp;lt;u64&amp;gt; {
        self.pos = self.writer.seek(pos)?;
        Ok(self.pos)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;干得漂亮，朋友。休息一下吧。&lt;/p&gt;
</content:encoded></item><item><title>PingCap的Rust训练课程3：同步的“客户端-服务端”网络模块</title><link>https://zlotus.github.io/2022/05/10/pna-rust-project-3/</link><guid isPermaLink="true">https://zlotus.github.io/2022/05/10/pna-rust-project-3/</guid><description>PingCap的Rust训练课程3：同步的“客户端-服务端”网络模块</description><pubDate>Tue, 10 May 2022 20:40:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文内容大多翻译自原文：&lt;a href=&quot;https://github.com/pingcap/talent-plan/tree/master/courses/rust/projects/project-3&quot;&gt;PNA Rust Project 3: Synchronous client-server networking&lt;/a&gt;。
:::&lt;/p&gt;
&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;任务&lt;/strong&gt;：创建一个&lt;em&gt;单线程&lt;/em&gt;、持久化的键/值存储的&lt;em&gt;服务器和客户端，使用自定义协议进行同步联网&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个客户端-服务器应用程序&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;std&lt;/code&gt;库的网络API编写自定义协议&lt;/li&gt;
&lt;li&gt;为服务端引入日志记录功能&lt;/li&gt;
&lt;li&gt;用trait实现可插拔的后端&lt;/li&gt;
&lt;li&gt;用&lt;code&gt;sled&lt;/code&gt;对手写的后端进行基准测试
&lt;strong&gt;关键词&lt;/strong&gt;：&lt;code&gt;std::net&lt;/code&gt;、日志、trait、基准测试。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在这个项目中，你将创建一个简单的键/值存储服务端和客户端，它们将用你自定义的网络协议进行通信。你将使用标准的日志crate生成日志，并正确处理网络边界上的错误。一旦有了一个可运行的客户端-服务端架构，那么你就能够抽象出trait背后的存储引擎，并将你的实现与&lt;a href=&quot;https://github.com/spacejam/sled&quot;&gt;&lt;code&gt;sled&lt;/code&gt;&lt;/a&gt;引擎进行性能比较。&lt;/p&gt;
&lt;h2&gt;项目需求规格&lt;/h2&gt;
&lt;p&gt;cargo项目&lt;code&gt;kvs&lt;/code&gt;建立了一个名为&lt;code&gt;kvs-client&lt;/code&gt;的命令行键值存储客户端，和一个名为&lt;code&gt;kvs-server&lt;/code&gt;的键值存储服务端，二者又都调用了一个名为&lt;code&gt;kvs&lt;/code&gt;的库。客户端通过一个自定义协议与服务端通信。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kvs-server&lt;/code&gt;可执行文件支持以下命令行参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kvs-server [--addr IP-PORT] [--engine ENGINE-NAME]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;启动服务端并开始监听进入的连接。&lt;code&gt;--addr&lt;/code&gt;接受一个IP地址（可以是v4或v6）以及一个端口号，格式为&lt;code&gt;IP:PORT&lt;/code&gt;。如果没有指定&lt;code&gt;--addr&lt;/code&gt;，则默认监听&lt;code&gt;127.0.0.1:4000&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果指定了&lt;code&gt;--engine&lt;/code&gt;，那么&lt;code&gt;ENGINE-NAME&lt;/code&gt;必须是&quot;kvs&quot;（即使用内置引擎），或者是&quot;sled&quot;（即使用sled引擎）。如果这是首次运行程序（没有以前保存的数据），那么默认值是&quot;kvs&quot;；如果以前有保存的数据，那么默认使用已经在用的引擎。如果以前保存的数据与当前选择的引擎不同，则打印一个错误并以非零的退出码退出。&lt;/p&gt;
&lt;p&gt;如果套接字绑定失败，或&lt;code&gt;ENGINE-NAME&lt;/code&gt;无效，或&lt;code&gt;IP-PORT&lt;/code&gt;不能解析为一个地址，则打印一个错误并以非零的退出码退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kvs-server -V&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;打印版本信息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;kvs-client&lt;/code&gt;可执行文件支持以下命令行参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kvs-client set &amp;lt;KEY&amp;gt; &amp;lt;VALUE&amp;gt; [--addr IP-PORT]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;将一个字符串键的值设置为一个字符串。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--addr&lt;/code&gt;接受一个IP地址（可以是v4或v6）以及一个端口号，格式为&lt;code&gt;IP:PORT&lt;/code&gt;。如果没有指定&lt;code&gt;--addr&lt;/code&gt;，则在默认&lt;code&gt;127.0.0.1:4000&lt;/code&gt;上连接。&lt;/p&gt;
&lt;p&gt;如果服务器出错，或者&lt;code&gt;IP-PORT&lt;/code&gt;不能解析为一个地址，则打印错误并以非零的退出码退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kvs-client get &amp;lt;KEY&amp;gt; [--addr IP-PORT]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;获取一个给定的字符串键的字符串值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--addr&lt;/code&gt;接受一个IP地址（可以是v4或v6）以及一个端口号，格式为&lt;code&gt;IP:PORT&lt;/code&gt;。如果没有指定&lt;code&gt;--addr&lt;/code&gt;，则在默认&lt;code&gt;127.0.0.1:4000&lt;/code&gt;上连接。&lt;/p&gt;
&lt;p&gt;如果服务器出错，或者&lt;code&gt;IP-PORT&lt;/code&gt;不能解析为一个地址，则打印错误并以非零的退出码退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kvs-client rm &amp;lt;KEY&amp;gt; [--addr IP-PORT]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;删除一个给定的字符串键。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;--addr&lt;/code&gt;接受一个IP地址（可以是v4或v6）以及一个端口号，格式为&lt;code&gt;IP:PORT&lt;/code&gt;。如果没有指定&lt;code&gt;--addr&lt;/code&gt;，则在默认&lt;code&gt;127.0.0.1:4000&lt;/code&gt;上连接。&lt;/p&gt;
&lt;p&gt;如果服务器出错，或者&lt;code&gt;IP-PORT&lt;/code&gt;不能解析为一个地址，则打印错误并以非零的退出码退出。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;kvs-client -V&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;打印版本信息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所有错误信息都应打印到stderr。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kvs&lt;/code&gt;库包含四种类型。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KvsClient&lt;/code&gt; - 为&lt;code&gt;kvs-client&lt;/code&gt;实现与&lt;code&gt;kvs-server&lt;/code&gt;通信所需的功能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvsServer&lt;/code&gt; - 为&lt;code&gt;kvs-server&lt;/code&gt;实现响应&lt;code&gt;kvs-client&lt;/code&gt;请求的功能。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvsEngine&lt;/code&gt; trait - 定义了&lt;code&gt;KvsServer&lt;/code&gt;需要调用的存储接口。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvStore&lt;/code&gt; - 手动实现&lt;code&gt;KvsEngine&lt;/code&gt; trait&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SledKvsEngine&lt;/code&gt; - 为&lt;code&gt;sled&lt;/code&gt;存储引擎实现&lt;code&gt;KvsEngine&lt;/code&gt; trait。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;KvsClient&lt;/code&gt;和&lt;code&gt;KvsServer&lt;/code&gt;的设计由你决定，并将使用你自定义的网络协议来通信。本项目的测试套件并不直接调用这两种类型，而仅是通过CLI测试它们。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;KvsEngine&lt;/code&gt; trait支持以下方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;KvsEngine::set(&amp;amp;mut self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;将一个字符串键的值设置为一个字符串。&lt;/p&gt;
&lt;p&gt;如果值没有成功写入，则返回错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;KvsEngine::get(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;获取一个字符串键的字符串值。如果键不存在，返回&lt;code&gt;None&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果没有成功读取该值，则返回错误。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;KvsEngine::remove(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;删除一个给定的字符串键。&lt;/p&gt;
&lt;p&gt;如果键不存在或值未被成功读取，则返回错误。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当为一个键设置值时，&lt;code&gt;KvStore&lt;/code&gt;会把&lt;code&gt;set&lt;/code&gt;命令写入磁盘的顺序日志。当删除一个键时，&lt;code&gt;KvStore&lt;/code&gt;将&lt;code&gt;rm&lt;/code&gt;命令写入日志。在启动时，日志中的命令将被重新求值，各键的最新一条设置命令的日志指针（文件偏移量）将会被保存在内存索引中。&lt;/p&gt;
&lt;p&gt;当用&lt;code&gt;get&lt;/code&gt;命令检索一个键的值时，它会搜索索引，如果找到了，就从日志的相应位置中加载相应命令并求值。&lt;/p&gt;
&lt;p&gt;当未压缩的日志条目的大小达到一个给定的阈值时，&lt;code&gt;KvStore&lt;/code&gt;将其压缩成一个新的日志，删除多余的条目以释放磁盘空间。&lt;/p&gt;
&lt;h2&gt;项目设置&lt;/h2&gt;
&lt;p&gt;继续你以前的项目，删除你以前的&lt;code&gt;tests&lt;/code&gt;目录，把这本项目的&lt;code&gt;tests&lt;/code&gt;目录复制到它的位置。这个项目应该包含一个名为&lt;code&gt;kvs&lt;/code&gt;的库，以及两个可执行文件，&lt;code&gt;kvs-server&lt;/code&gt;和&lt;code&gt;kvs-client&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你需要在&lt;code&gt;Cargo.toml&lt;/code&gt;中加入以下&lt;code&gt;dev-dependencies&lt;/code&gt;依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dev-dependencies]
assert_cmd = &quot;0.11&quot;
criterion = &quot;0.3&quot;
predicates = &quot;1.0.0&quot;
rand = &quot;0.6.5&quot;
tempfile = &quot;3.0.7&quot;
walkdir = &quot;2.2.7&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与以前的项目一样，添加足够的定义，以构建测试套件。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;使测试套件编译通过只需要：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按上文要求在&lt;code&gt;lib.rs&lt;/code&gt;中添加一个新模块&lt;code&gt;mod engines&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建新模块&lt;code&gt;engines\mod.rs&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在新模块&lt;code&gt;mod.rs&lt;/code&gt;中按要求加入&lt;code&gt;KvsEngine&lt;/code&gt; trait的定义即可。&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;lib.rs&lt;/code&gt;中公开引用这个trait即可：&lt;code&gt;pub use engines::KvsEngine;&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;目录结构为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── Cargo.toml
├── src
│  ├── bin
│  │  └── kvs.rs
│  ├── engines
│  │  └── mod.rs
│  ├── error.rs
│  ├── kv.rs
│  └── lib.rs
└── tests
   ├── cli.rs
   └── kv_store.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前&lt;code&gt;cli.rs&lt;/code&gt;中的11个测试均未通过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo test --test cli
...
running 11 tests
test cli_log_configuration ... FAILED
test cli_access_server_kvs_engine ... FAILED
test cli_wrong_engine ... FAILED
test client_cli_invalid_get ... FAILED
test client_cli_invalid_rm ... FAILED
test client_cli_invalid_set ... FAILED
test cli_access_server_sled_engine ... FAILED
test client_cli_invalid_subcommand ... FAILED
test client_cli_version ... FAILED
test server_cli_version ... FAILED
test client_cli_no_args ... FAILED
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kv_store.rs&lt;/code&gt;中的均通过测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo test --test kv_store
running 6 tests
test remove_non_existent_key ... ok
test remove_key ... ok
test get_stored_value ... ok
test get_non_existent_value ... ok
test overwrite_value ... ok
test compaction ... ok
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第1部分：解析命令行&lt;/h2&gt;
&lt;p&gt;本项目中的命令行解析同之前的项目相比没有多少新功能。&lt;code&gt;kvs-client&lt;/code&gt;二进制文件接受与之前项目相同的命令行参数。而&lt;code&gt;kvs-server&lt;/code&gt;有一组自己的命令行参数需要处理，就像上文描述的那样。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;处理&lt;code&gt;kvs-server&lt;/code&gt;的命令行参数。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;code&gt;kvs-server&lt;/code&gt;的参数比较简单，依然使用&lt;code&gt;clap&lt;/code&gt;的&lt;code&gt;derive&lt;/code&gt;特性编写，由于Rust已经为&lt;code&gt;&amp;amp;str&lt;/code&gt;实现了&lt;code&gt;ToSocketAddrs&lt;/code&gt;特性，&lt;code&gt;Args&lt;/code&gt;中直接使用&lt;code&gt;SocketAddr&lt;/code&gt;类型会自动触发字符串到IP地址的转换：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use clap::{Parser};
use kvs::{Result};
use std::net::SocketAddr;

const DEFAULT_LISTENING_ADDRESS: &amp;amp;str = &quot;127.0.0.1:4000&quot;;
const DEFAULT_ENGINE: Engine = Engine::kvs;

#[allow(non_camel_case_types)]
#[derive(clap::ArgEnum, Clone, Debug)]
enum Engine {
   kvs,
   sled,
}

impl FromStr for Engine {
    type Err = KvsError;

    fn from_str(s: &amp;amp;str) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        match s {
            &quot;kvs&quot; =&amp;gt; {
                Ok(Engine::kvs)
            },
            &quot;sled&quot; =&amp;gt; {
                Ok(Engine::sled)
            }
            _ =&amp;gt; {
                Err(KvsError::UnexpectedEngineType)
            }
        }
    }
}

#[derive(Parser, Debug)]
#[clap(name = &quot;kvs-server&quot;)]
#[clap(author, version, about=&quot;server of key value storage&quot;, long_about=None)]
struct Args {
    /// [IPADDR:PORT] of server.
    #[clap(long, default_value=DEFAULT_LISTENING_ADDRESS)]
    addr: SocketAddr,

    /// backend engine of key value storage.
    #[clap(arg_enum, long, required(false))]
    engine: Option&amp;lt;Engine&amp;gt;,
}


fn main() -&amp;gt; Result&amp;lt;()&amp;gt; {
    let args = Args::parse();
    println!(&quot;{:?} {:?}&quot;, args.addr, args.engine);
    
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kvs-client&lt;/code&gt;需要在子命令后面再添加可选参数（IP地址），所以可以使用&lt;code&gt;clap&lt;/code&gt;的builder编写模式增加可读性。当然，也可以继续使用&lt;code&gt;derive&lt;/code&gt;特性编写更简略的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// builder模式

use clap::{Command, arg, crate_version};
use kvs::{Result};
use std::net::SocketAddr;

const DEFAULT_LISTENING_ADDRESS: &amp;amp;str = &quot;127.0.0.1:4000&quot;;

fn cli() -&amp;gt; Command&amp;lt;&apos;static&amp;gt; {
    Command::new(&quot;kvs-client&quot;)
        .about(&quot;client of key value storage&quot;)
        .version(crate_version!())
        .subcommand_required(true)
        .disable_help_subcommand(true)
        .args_conflicts_with_subcommands(true)
        .subcommand(
            Command::new(&quot;get&quot;)
                .about(&quot;Get the string value of a given string key&quot;)
                .args(&amp;amp;[
                    arg!(&amp;lt;KEY&amp;gt; &quot;A string key&quot;),
                    arg!(--addr &amp;lt;&quot;IP:PORT&quot;&amp;gt; &quot;Set the server address as IP:PORT&quot;).required(false).default_value(DEFAULT_LISTENING_ADDRESS)
                ])
        )
        .subcommand(
            Command::new(&quot;set&quot;)
                .about(&quot;Set the value of a string key to a string&quot;)
                .args(&amp;amp;[
                    arg!(&amp;lt;KEY&amp;gt; &quot;A string key&quot;),
                    arg!(&amp;lt;VALUE&amp;gt; &quot;The string value of the key&quot;).required(true),
                    arg!(--addr &amp;lt;&quot;IP:PORT&quot;&amp;gt; &quot;Set the server address as IP:PORT&quot;).required(false).default_value(DEFAULT_LISTENING_ADDRESS) 
                ])
        )
        .subcommand(
            Command::new(&quot;rm&quot;)
                .about(&quot;Remove a given string key&quot;)
                .args(&amp;amp;[
                    arg!(&amp;lt;KEY&amp;gt; &quot;A string key&quot;),
                    arg!(--addr &amp;lt;&quot;IP:PORT&quot;&amp;gt; &quot;Set the server address as IP:PORT&quot;).required(false).default_value(DEFAULT_LISTENING_ADDRESS)                
                ])
        )
}

fn main() -&amp;gt; Result&amp;lt;()&amp;gt;{
    let matches = cli().get_matches();

    match matches.subcommand() {
        Some((&quot;get&quot;, sub_matches)) =&amp;gt; {
            println!(&quot;get {} {}&quot;, sub_matches.value_of(&quot;KEY&quot;).unwrap(), sub_matches.value_of(&quot;addr&quot;).unwrap());
            let key = sub_matches.value_of(&quot;KEY&quot;).unwrap();
            let addr: SocketAddr = sub_matches.value_of(&quot;addr&quot;).unwrap().parse()?;
        },
        Some((&quot;set&quot;, sub_matches)) =&amp;gt; {
            println!(&quot;set {} {} {}&quot;, sub_matches.value_of(&quot;KEY&quot;).unwrap(), sub_matches.value_of(&quot;VALUE&quot;).unwrap(), sub_matches.value_of(&quot;addr&quot;).unwrap());
            let key = sub_matches.value_of(&quot;KEY&quot;).unwrap();
            let value = sub_matches.value_of(&quot;VALUE&quot;).unwrap();
            let addr: SocketAddr = sub_matches.value_of(&quot;addr&quot;).unwrap().parse()?;
        }
        Some((&quot;rm&quot;, sub_matches)) =&amp;gt; {
            println!(&quot;rm {} {}&quot;, sub_matches.value_of(&quot;KEY&quot;).unwrap(), sub_matches.value_of(&quot;addr&quot;).unwrap());
            let key = sub_matches.value_of(&quot;KEY&quot;).unwrap();
            let addr: SocketAddr = sub_matches.value_of(&quot;addr&quot;).unwrap().parse()?;
        },

        _ =&amp;gt; unreachable!(), // If all subcommands are defined above, anything else is unreachabe!()

    }

    Ok(())
}

// derive模式

use clap::{Parser, Subcommand};
use kvs::{KvStore, Result};
use std::process::exit;
use std::env::current_dir;
use std::net::SocketAddr;

const DEFAULT_LISTENING_ADDRESS: &amp;amp;str = &quot;127.0.0.1:4000&quot;;

#[derive(Parser)]
#[clap(name = &quot;kvs&quot;)]
#[clap(author, version, about=&quot;an in-memory key/value store&quot;, long_about=None, args_conflicts_with_subcommands(true))]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    // Set the value of a string key to a string
    #[clap(arg_required_else_help = true)]
    set {
        // A string key
        key: String,
        // The string value of the key
        value: String,
        #[clap(long, required(false), default_value(DEFAULT_LISTENING_ADDRESS), help=&quot;Set the server address as [IP:PORT]&quot;)]
        addr: SocketAddr
    }, 

    // Get the string value of a given string key
    #[clap(arg_required_else_help = true)]
    get {
        // A string key
        key: String,
        #[clap(long, required(false), default_value(DEFAULT_LISTENING_ADDRESS), help=&quot;Set the server address as [IP:PORT]&quot;)]
        addr: SocketAddr
    }, 

    // Remove a given key
    #[clap(arg_required_else_help = true)]
    rm {
        // A string key
        key: String,
        #[clap(long, required(false), default_value(DEFAULT_LISTENING_ADDRESS), help=&quot;Set the server address as [IP:PORT]&quot;)]
        addr: SocketAddr
    },
}

fn main() -&amp;gt; Result&amp;lt;()&amp;gt; {
    let cli= Cli::parse();
    match cli.command {
        Commands::get { key, addr } =&amp;gt; {}, 
        Commands::set { key, value, addr } =&amp;gt; {},
        Commands::rm { key, addr } =&amp;gt; {},
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第2部分：日志记录&lt;/h2&gt;
&lt;p&gt;生产环境下的服务器应用程序应具有功能强大且可配置的日志记录。因此，现在我们应为&lt;code&gt;kvs-server&lt;/code&gt;添加日志功能，并在后续功能实现的过程中记录有用的信息。在开发过程中，通常使用&lt;code&gt;debug!&lt;/code&gt;和&lt;code&gt;trace!&lt;/code&gt;级日志来打印调试信息。&lt;/p&gt;
&lt;p&gt;Rust中有两个好用的日志系统：&lt;a href=&quot;https://docs.rs/log/&quot;&gt;&lt;code&gt;log&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/slog/&quot;&gt;&lt;code&gt;slog&lt;/code&gt;&lt;/a&gt;。两者均为不同级别的日志输出类似的宏，如&lt;code&gt;error!&lt;/code&gt;、&lt;code&gt;info!&lt;/code&gt;等。两者都是可扩展的，支持不同的后端，以输出日志到控制台、日志文件或系统日志等。&lt;/p&gt;
&lt;p&gt;最主要的区别是，&lt;code&gt;log&lt;/code&gt;相当简单，只记录格式化的字符串；&lt;code&gt;slog&lt;/code&gt;功能丰富，支持“结构化日志”，其日志条目是以容易解析的格式类型化和序列化的。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;log&lt;/code&gt;可以追溯到Rust最早的时候，它是编译器的一部分，然后是标准库的一部分，最后作为独立的crate发布。它由Rust项目组维护。&lt;code&gt;slog&lt;/code&gt;是较新的crate，独立维护。两者均已被广泛使用。&lt;/p&gt;
&lt;p&gt;对于这两种系统，都需要选择一个“接收器”crate，用于将日志发送到该接收器以进行显示或存储。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;阅读这两个系统，选择一个对你喜欢的，将它们作为依赖项添加，然后修改&lt;code&gt;kvs-server&lt;/code&gt;以在传递命令行参数之前初始化日志。将日志设置为输出到&lt;code&gt;stderr&lt;/code&gt;&lt;/em&gt;（将日志发送到其他地方也可以，但日志必须也发送到&lt;code&gt;stderr&lt;/code&gt;以通过本项目的测试）。&lt;/p&gt;
&lt;p&gt;在启动时日志记录服务器端的版本信息，还要记录配置信息。目前指的是IP地址和端口，以及存储引擎的名称。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;因为第一次使用日志功能，还是先用简单的&lt;code&gt;log&lt;/code&gt;入门，在&lt;code&gt;Cargo.toml&lt;/code&gt;里引入&lt;code&gt;log&lt;/code&gt;和&lt;code&gt;env_logger&lt;/code&gt;（用于配置日志）。然后按要求输出日志即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
fn main() -&amp;gt; Result&amp;lt;()&amp;gt; {
    env_logger::builder().filter_level(LevelFilter::Debug).init();
    let mut args = Args::parse();
//...
    run(args)
}

fn run(args: Args) -&amp;gt; Result&amp;lt;()&amp;gt; {
    let engine = args.engine.unwrap_or(DEFAULT_ENGINE);
    info!(&quot;kvs-server {}&quot;, env!(&quot;CARGO_PKG_VERSION&quot;));
    info!(&quot;Storage engine: {:?}&quot;, engine);
    info!(&quot;Listening on {}&quot;, args.addr);
// ...
}
// ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第3部分：客户端-服务端网络设置&lt;/h2&gt;
&lt;p&gt;接下来，我们将搭建网络模块。对于本项目，您将使用&lt;code&gt;std::net&lt;/code&gt;中使用最基础的TCP/IP网络API：&lt;a href=&quot;https://doc.rust-lang.org/std/net/struct.TcpListener.html&quot;&gt;&lt;code&gt;TcpListener&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/std/net/struct.TcpStream.html&quot;&gt;&lt;code&gt;TcpStream&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;对于本项目，服务器是同步、单线程的。这意味着您将监听一个socket，接受一个连接，然后一次执行/响应一条命令。在未来，我们将在实现异步、多线程和高性能数据库的过程中，多次重新回味这一设定。&lt;/p&gt;
&lt;p&gt;考虑一下您的手动测试工作流程。现在有两个可执行文件要处理，您需要一种同时运行它们的方法。您可能和我们一样，同时使用两个终端，一个运行&lt;code&gt;cargo run --bin kvs-server&lt;/code&gt; ，服务端将一直运行到你按下CTRL-D；另一个运行&lt;code&gt;cargo run --bin kvs-client&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这是使用日志进行调试的好机会。继续记录有关每个已接受连接的信息。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;在考虑协议之前，先修改&lt;code&gt;kvs-server&lt;/code&gt;来监听和接受连接，在修改&lt;code&gt;kvs-client&lt;/code&gt;来发起连接。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;是时候把&lt;code&gt;KvStore&lt;/code&gt;变成真正的KV引擎了，把为&lt;code&gt;KvStore&lt;/code&gt;实现的&lt;code&gt;get&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt;、&lt;code&gt;remove&lt;/code&gt;拿出来，用于为&lt;code&gt;KvStore&lt;/code&gt;实现&lt;code&gt;KvsEngine&lt;/code&gt;特性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...
impl KvsEngine for KvStore {
//...
    fn set(&amp;amp;mut self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        let cmd = Command::set(key, value);
        let pos = self.writer.pos;
        serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;cmd)?;
        self.writer.flush()?;
        if let Command::Set { key, .. } = cmd {
            if let Some(old_cmd) = self
                .index
                .insert(key, (self.current_gen, pos..self.writer.pos).into())
            {
                self.uncompacted += old_cmd.len;
            }
        }

        if self.uncompacted &amp;gt; COMPACTION_THRESHOLD {
            self.compact()?;
        }
        Ok(())
    }
//...
    fn get(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt; {
        if let Some(cmd_pos) = self.index.get(&amp;amp;key) {
            let reader = self
                .readers
                .get_mut(&amp;amp;cmd_pos.gen)
                .expect(&quot;Cannot find log reader&quot;);
            reader.seek(SeekFrom::Start(cmd_pos.pos))?;
            let cmd_reader = reader.take(cmd_pos.len);
            if let Command::Set { value, .. } = serde_json::from_reader(cmd_reader)? {
                Ok(Some(value))
            } else {
                Err(KvsError::UnexpectedCommandType)
            }
        } else {
            Ok(None)
        }
    }
//...
    fn remove(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        if self.index.contains_key(&amp;amp;key) {
            let cmd = Command::remove(key);
            serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;cmd)?;
            self.writer.flush()?;
            if let Command::Remove { key } = cmd {
                let old_cmd = self.index.remove(&amp;amp;key).expect(&quot;key not found&quot;);
                self.uncompacted += old_cmd.len;
            }
            Ok(())
        } else {
            Err(KvsError::KeyNotFound)
        }
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在实现客户端-服务端之前，可以先定义用于序列化的枚举类型，作为客户端与服务端之间的通信协议（在后面编写服务端和客户端时，可以方便的将这些支持序列化的类型放在&lt;code&gt;TcpStream&lt;/code&gt;上进行读写）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
pub enum Request {
    Get { key: String },
    Set { key: String, value: String },
    Remove { key: String },
}

#[derive(Debug, Serialize, Deserialize)]
pub enum GetResponse {
    Ok(Option&amp;lt;String&amp;gt;),
    Err(String),
}

#[derive(Debug, Serialize, Deserialize)]
pub enum SetResponse {
    Ok(()),
    Err(String),
}

#[derive(Debug, Serialize, Deserialize)]
pub enum RemoveResponse {
    Ok(()),
    Err(String),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在&lt;code&gt;server.rs&lt;/code&gt;中实现&lt;code&gt;KvsServer&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::{net::{ToSocketAddrs, TcpListener, TcpStream}, io::{BufReader, BufWriter, Write}};
use log::{error, debug};
use serde_json::Deserializer;
use crate::{KvsEngine, Result, common::{Request, GetResponse, SetResponse, RemoveResponse}};

/// The server of a key value store.
pub struct KvsServer&amp;lt;E: KvsEngine&amp;gt; {
    engine: E,
}

impl&amp;lt;E: KvsEngine&amp;gt; KvsServer&amp;lt;E&amp;gt;  {
    /// Create a `KvsServer` with a given storage engine.
    pub fn new(engine: E) -&amp;gt; Self {
        KvsServer { engine }
    }
    
    /// Run the server listening on the given address
    pub fn run(&amp;amp;mut self, addr: impl ToSocketAddrs) -&amp;gt; Result&amp;lt;()&amp;gt; {
        let listener = TcpListener::bind(addr)?;
        for stream in listener.incoming() {
            match stream {
                Ok(stream) =&amp;gt; {
                    if let Err(e) = self.serve(stream) {
                        error!(&quot;Error on serving client: {}&quot;, e);
                    }
                }
                Err(e) =&amp;gt; error!(&quot;Connection failed: {}&quot;, e),
            }
        }
        Ok(())
    }

    fn serve(&amp;amp;mut self, tcp: TcpStream) -&amp;gt; Result&amp;lt;()&amp;gt; {
        let peer_addr = tcp.peer_addr()?;
        let reader = BufReader::new(&amp;amp;tcp);
        let mut writer = BufWriter::new(&amp;amp;tcp);
        let req_reader = Deserializer::from_reader(reader).into_iter::&amp;lt;Request&amp;gt;();

        macro_rules! send_resp {
            ($resp:expr) =&amp;gt; {{
                let resp = $resp;
                serde_json::to_writer(&amp;amp;mut writer, &amp;amp;resp)?;
                writer.flush()?;
                debug!(&quot;Response sent to {}: {:?}&quot;, peer_addr, resp);
            };};
        }

        for req in req_reader {
            let req = req?;
            debug!(&quot;Receive request from {} {:?}&quot;, peer_addr, req);
            match req {
                Request::Get { key } =&amp;gt; send_resp!(
                    match self.engine.get(key) {
                        Ok(value) =&amp;gt; { GetResponse::Ok(value) },
                        Err(e) =&amp;gt; { GetResponse::Err(format!(&quot;{}&quot;, e)) },
                    }
                ),
                Request::Set { key, value } =&amp;gt; send_resp!(
                    match self.engine.set(key, value) {
                        Ok(_) =&amp;gt; SetResponse::Ok(()),
                        Err(e) =&amp;gt; SetResponse::Err(format!(&quot;{}&quot;, e)),
                    }
                ),
                Request::Remove { key } =&amp;gt; send_resp!(
                    match self.engine.remove(key) {
                        Ok(_) =&amp;gt; RemoveResponse::Ok(()),
                        Err(e) =&amp;gt; RemoveResponse::Err(format!(&quot;{}&quot;, e)),
                    }
                ),
            }
        }

        Ok(())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;send_resp!&lt;/code&gt;宏的作用与下面的代码相同，编写这个宏可以减少重复：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 使用代码
Request::Get { key } =&amp;gt; {
    let resp = match self.engine.get(key) {
        Ok(value) =&amp;gt; { GetResponse::Ok(value) },
        Err(e) =&amp;gt; { GetResponse::Err(format!(&quot;{}&quot;, e)) },
    };
    serde_json::to_writer(writer, &amp;amp;resp);
    writer.flush()?;
    debug!(&quot;Response sent to {}: {:?}&quot;, peer_addr, resp);
},
// 使用宏
macro_rules! send_resp {
    ($resp:expr) =&amp;gt; {{
        let resp = $resp;
        serde_json::to_writer(&amp;amp;mut writer, &amp;amp;resp)?;
        writer.flush()?;
        debug!(&quot;Response sent to {}: {:?}&quot;, peer_addr, resp);
    };};
}

Request::Get { key } =&amp;gt; send_resp!(
    match self.engine.get(key) {
        Ok(value) =&amp;gt; { GetResponse::Ok(value) },
        Err(e) =&amp;gt; { GetResponse::Err(format!(&quot;{}&quot;, e)) },
    }
),

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;kvs-server.rs&lt;/code&gt;中使用&lt;code&gt;KvsServer&lt;/code&gt;，此时可以将日志等级调整为&lt;code&gt;Debug&lt;/code&gt;以方便调试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use clap::Parser;
use kvs::{Result, KvsError, KvStore, KvsEngine, KvsServer};
use std::env::current_dir;
use std::fs;
use std::net::SocketAddr;
use std::process::exit;
use log::LevelFilter;
use log::{error, info, warn};
use std::str::FromStr;


const DEFAULT_LISTENING_ADDRESS: &amp;amp;str = &quot;127.0.0.1:4000&quot;;
const DEFAULT_ENGINE: Engine = Engine::kvs;

#[allow(non_camel_case_types)]
#[derive(clap::ArgEnum, Copy, Clone, Debug, PartialEq, Eq)]
enum Engine {
   kvs,
   sled,
}

impl FromStr for Engine {
    type Err = KvsError;

    fn from_str(s: &amp;amp;str) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        match s {
            &quot;kvs&quot; =&amp;gt; {
                Ok(Engine::kvs)
            },
            &quot;sled&quot; =&amp;gt; {
                Ok(Engine::sled)
            }
            _ =&amp;gt; {
                Err(KvsError::UnexpectedEngineType)
            }
        }
    }
}

#[derive(Parser, Debug)]
#[clap(name = &quot;kvs-server&quot;)]
#[clap(author, version, about=&quot;server of key value storage&quot;, long_about=None)]
struct Args {
    /// [IPADDR:PORT] of server.
    #[clap(long, default_value=DEFAULT_LISTENING_ADDRESS)]
    addr: SocketAddr,

    /// backend engine of key value storage.
    #[clap(arg_enum, long, required(false))]
    engine: Option&amp;lt;Engine&amp;gt;,
}


fn main() -&amp;gt; Result&amp;lt;()&amp;gt;{
    env_logger::builder().filter_level(LevelFilter::Debug).init();
    let mut args = Args::parse();

    let res = current_engine().and_then(move |curr_engine| {
        if args.engine.is_none() {
            args.engine = curr_engine;
        }
        if curr_engine.is_some() &amp;amp;&amp;amp; args.engine != curr_engine {
            error!(&quot;Wrong engine!&quot;);
            exit(1);
        }
        run(args)
    });    
    if let Err(e) = res {
        error!(&quot;{}&quot;, e);
        exit(1);
    }
    Ok(())
}

fn run(args: Args) -&amp;gt; Result&amp;lt;()&amp;gt; {
    let engine = args.engine.unwrap_or(DEFAULT_ENGINE);
    info!(&quot;kvs-server {}&quot;, env!(&quot;CARGO_PKG_VERSION&quot;));
    info!(&quot;Storage engine: {:?}&quot;, engine);
    info!(&quot;Listening on {}&quot;, args.addr);

    // write engine to engine file
    fs::write(current_dir()?.join(&quot;engine&quot;), format!(&quot;{:?}&quot;, engine))?;

    match engine {
        Engine::kvs =&amp;gt; run_with_engine(KvStore::open(current_dir()?)?, args.addr),
        Engine::sled =&amp;gt; Ok(()), //TODO
    }
}

fn run_with_engine(engine: impl KvsEngine, addr: SocketAddr) -&amp;gt; Result&amp;lt;()&amp;gt; {
    let mut server = KvsServer::new(engine);
    server.run(addr)
}

fn current_engine() -&amp;gt; Result&amp;lt;Option&amp;lt;Engine&amp;gt;&amp;gt; {
    let engine = current_dir()?.join(&quot;engine&quot;);
    if !engine.exists() {
        return Ok(None);
    }

    match fs::read_to_string(engine)?.parse() {
        Ok(engine) =&amp;gt; Ok(Some(engine)),
        Err(e) =&amp;gt; {
            warn!(&quot;The content of engine file is invalid: {}&quot;, e);
            Ok(None)
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来在&lt;code&gt;client.rs&lt;/code&gt;中实现&lt;code&gt;KvsClient&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::{io::{BufReader, BufWriter, Write}, net::{TcpStream, ToSocketAddrs}};
use crate::{Result, common::{Request, GetResponse, SetResponse, RemoveResponse}, KvsError};
use serde::Deserialize;
use serde_json::{Deserializer, de::IoRead};

/// Key value store client
pub struct KvsClient {
    reader: Deserializer&amp;lt;IoRead&amp;lt;BufReader&amp;lt;TcpStream&amp;gt;&amp;gt;&amp;gt;,
    writer: BufWriter&amp;lt;TcpStream&amp;gt;,
}

impl KvsClient {
    /// Connect to `addr` to access `KvsServer`.
    pub fn connect(addr: impl ToSocketAddrs) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        let tcp_reader = TcpStream::connect(addr)?;
        let tcp_writer = tcp_reader.try_clone()?;
        Ok(KvsClient{
            reader: Deserializer::from_reader(BufReader::new(tcp_reader)),
            writer: BufWriter::new(tcp_writer),
        })
    }

    /// Get the value of a given key from the server.
    pub fn get(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt; {
        serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;Request::Get { key })?;
        self.writer.flush()?;
        let resp = GetResponse::deserialize(&amp;amp;mut self.reader)?;
        match resp {
            GetResponse::Ok(value) =&amp;gt; Ok(value),
            GetResponse::Err(msg) =&amp;gt; Err(KvsError::StringError(msg)),
        }
    }

    /// Set the value of a string key in the server.
    pub fn set(&amp;amp;mut self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;Request::Set { key, value })?;
        self.writer.flush()?;
        let resp = SetResponse::deserialize(&amp;amp;mut self.reader)?;
        match resp {
            SetResponse::Ok(_) =&amp;gt; Ok(()),
            SetResponse::Err(msg) =&amp;gt; Err(KvsError::StringError(msg)),
        }
    }

    /// Remove a string key in the server.
    pub fn remove(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;Request::Remove { key })?;
        self.writer.flush()?;
        let resp = RemoveResponse::deserialize(&amp;amp;mut self.reader)?;
        match resp {
            RemoveResponse::Ok(_) =&amp;gt; Ok(()),
            RemoveResponse::Err(msg) =&amp;gt; Err(KvsError::StringError(msg)),
        }
    }
    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并在&lt;code&gt;kvs-client.rs&lt;/code&gt;中使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use clap::{Parser, Subcommand};
use kvs::{Result, KvsClient};
use std::net::SocketAddr;

const DEFAULT_LISTENING_ADDRESS: &amp;amp;str = &quot;127.0.0.1:4000&quot;;

#[derive(Parser)]
#[clap(name = &quot;kvs&quot;)]
#[clap(author, version, about=&quot;an in-memory key/value store&quot;, long_about=None, args_conflicts_with_subcommands(true))]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
#[allow(non_camel_case_types)]
enum Commands {
    // Set the value of a string key to a string
    #[clap(arg_required_else_help=true)]
    set {
        // A string key
        key: String,
        // The string value of the key
        value: String,

        #[clap(long, required(false), default_value(DEFAULT_LISTENING_ADDRESS), help=&quot;Set the server address as [IP:PORT]&quot;)]
        addr: SocketAddr
    }, 

    // Get the string value of a given string key
    #[clap(arg_required_else_help=true)]
    get {
        // A string key
        key: String,
        
        #[clap(long, required(false), default_value(DEFAULT_LISTENING_ADDRESS), help=&quot;Set the server address as [IP:PORT]&quot;)]
        addr: SocketAddr
    }, 

    // Remove a given key
    #[clap(arg_required_else_help=true)]
    rm {
        // A string key
        key: String,
        
        #[clap(long, required(false), default_value(DEFAULT_LISTENING_ADDRESS), help=&quot;Set the server address as [IP:PORT]&quot;)]
        addr: SocketAddr
    },
}

fn main() -&amp;gt; Result&amp;lt;()&amp;gt; {
    let cli= Cli::parse();
    match cli.command {
        Commands::get { key, addr } =&amp;gt; {
            let mut client = KvsClient::connect(addr)?;
            if let Some(value) = client.get(key.to_string())? {
                println!(&quot;{}&quot;, value);
            } else {
                println!(&quot;Key not found&quot;);
            }
        }, 
        Commands::set { key, value, addr } =&amp;gt; {
            let mut client = KvsClient::connect(addr)?;
            client.set(key.to_string(), value.to_string())?;
        }
        Commands::rm { key, addr } =&amp;gt; {
            let mut client = KvsClient::connect(addr)?;
            client.remove(key)?;
        }
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时一个终端打开&lt;code&gt;cargo run --bin kvs-server&lt;/code&gt;，另一个终端运行&lt;code&gt;cargo run --bin kvs-client -- get key&lt;/code&gt;，就可以从网络上将“训练课程2”中存入的&lt;code&gt;键: 值&lt;/code&gt;取出来了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# server
(base) ➜  kvs git:(master) ✗ cargo run --bin kvs-server
   Compiling kvs v0.1.0 (/Users/zealot/RustProjects/pingcap-projects/kvs)
    Finished dev [unoptimized + debuginfo] target(s) in 1.14s
     Running `target/debug/kvs-server`
[2022-06-03T10:38:45Z INFO  kvs_server] kvs-server 0.1.0
[2022-06-03T10:38:45Z INFO  kvs_server] Storage engine: kvs
[2022-06-03T10:38:45Z INFO  kvs_server] Listening on 127.0.0.1:4000
[2022-06-03T10:38:49Z DEBUG kvs::server] Receive request from 127.0.0.1:55017 Get { key: &quot;键&quot; }
[2022-06-03T10:38:49Z DEBUG kvs::server] Response sent to 127.0.0.1:55017: Ok(Some(&quot;值&quot;))

# client
(base) ➜  kvs git:(master) ✗ cargo run --bin kvs-client -- get 键
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/kvs-client get &apos;键&apos;`
值
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第4部分：通过网络处理命令&lt;/h2&gt;
&lt;p&gt;在上一个项目中，您定义了数据库接受的命令，并学习了如何使用&lt;code&gt;serde&lt;/code&gt;对日志进行序列化和反序列化。&lt;/p&gt;
&lt;p&gt;现在是时候通过网络实现键/值存储，以通过前面实现的同步单进程模型远程执行命令。与您在上一个项目中创建日志的文件I/O一样，您将使用&lt;code&gt;Read&lt;/code&gt;和&lt;code&gt;Write&lt;/code&gt;特征序列化和流式传输命令。&lt;/p&gt;
&lt;p&gt;现在您需要设计一个网络协议。有多种方法可以在TCP流上数读写数据，与上一个项目一样，也需要做出许多选择。该协议是基于文本还是二进制？数据如何从内存中的类型格式转换为字节流格式？每个连接传输一个还是多个请求？&lt;/p&gt;
&lt;p&gt;请记住，该协议应支持执行成功并取回结果，也支持执行时遇到的错误，现在有两种错误：由您的存储引擎产生的错误，以及由网络通信产生的错误。&lt;/p&gt;
&lt;p&gt;协议的所有细节都由您决定。测试套件不关心数据如何从一端到达另一端，只关心结果是否正确。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;实现您的网络协议。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;前面我们已经一并实现了该协议，需要注意的是，只要进行适当的定义，序列化的所有类型转换都是自动完成的。&lt;/p&gt;
&lt;h2&gt;第5部分：可插拔存储引擎&lt;/h2&gt;
&lt;p&gt;您的数据库目前有一个由您实现的存储引擎&lt;code&gt;KvStore&lt;/code&gt;。现在将添加第二个存储引擎。&lt;/p&gt;
&lt;p&gt;这样做有多种原因：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同的业务负载需要不同的性能特征。对于特定的业务负载，某些存储引擎可能比其他存储引擎工作得更好；&lt;/li&gt;
&lt;li&gt;搭建了一个熟悉的框架来比较不同的后端；&lt;/li&gt;
&lt;li&gt;给了我们一个创建和使用trait的理由；&lt;/li&gt;
&lt;li&gt;给了我们一个编写比较基准的环境。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以你要从&lt;code&gt;KvStore&lt;/code&gt;接口中&lt;em&gt;抽象出&lt;/em&gt;一个新的特征：&lt;code&gt;KvsEngine&lt;/code&gt;。这是一个典型的重构，将现有代码逐步修改为新的形式。一般在重构时，我们希望将工作最小化分解，以便进行持续构建和修改。&lt;/p&gt;
&lt;p&gt;这是您最终需要的API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KvsEngine&lt;/code&gt;trait具有与&lt;code&gt;KvStore&lt;/code&gt;具有相同签名的&lt;code&gt;get&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt;和&lt;code&gt;remove&lt;/code&gt;方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvStore&lt;/code&gt;实现了&lt;code&gt;KvsEngine&lt;/code&gt;，不再有自己的&lt;code&gt;get&lt;/code&gt;、&lt;code&gt;set&lt;/code&gt;和&lt;code&gt;remove&lt;/code&gt;方法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvsEngine&lt;/code&gt;有一个新的实现——&lt;code&gt;SledKvsEngine&lt;/code&gt;。稍后您需要使用&lt;code&gt;sled&lt;/code&gt;库填充其&lt;code&gt;get&lt;/code&gt;和&lt;code&gt;set&lt;/code&gt;方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果您的测试套件通过编译，则可能已经了编写了这些定义的签名，现在是时候实现定义的内容了。将重构分解为渐进式修改，并确保项目能够持续构建，并通过先前已经通过的测试，然后再继续下一步重构。&lt;/p&gt;
&lt;p&gt;作为最后一步，您需要考虑当&lt;code&gt;kvs-server&lt;/code&gt;使用一个引擎启动，然后进程被终结，接下来再使用不同的引擎重新启动时会发生什么。这种情况应当产生错误，你需要想清楚如何检测这种情况才能报告错误。测试&lt;code&gt;cli_wrong_engine&lt;/code&gt;反映了这种情况。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;本项目一开始，为了通过测试，我们已经实现了&lt;code&gt;KvsEngine&lt;/code&gt;的签名，又在第3部分提前将&lt;code&gt;KvStore&lt;/code&gt;的几个方法移动至&lt;code&gt;KvsEngine&lt;/code&gt;的实现中。而在本项目需求说明部分，为了实现引擎切换报错，我们已经实现了启动时检测&lt;code&gt;engine&lt;/code&gt;文件内容的功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若未检测到&lt;code&gt;engine&lt;/code&gt;文件，则将本次使用的引擎写入&lt;code&gt;engine&lt;/code&gt;文件（不指定时默认使用&lt;code&gt;kvs&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;若检测到&lt;code&gt;engine&lt;/code&gt;文件，当文件记录引擎与当前指定引擎不一致时，报错；一致时则继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在&lt;code&gt;engine/sled.rs&lt;/code&gt;中为&lt;a href=&quot;https://github.com/spacejam/sled&quot;&gt;&lt;code&gt;sled&lt;/code&gt;&lt;/a&gt;实现&lt;code&gt;KvsEngine&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use sled::{Db, Tree};
use crate::{KvsEngine, Result, KvsError};

/// Wrapper of `sled::Db`
pub struct SledKvStore {
    db: Db,
}

impl SledKvStore {
    /// Creates a `SledKvsEngine` from `sled::Db`.
    pub fn new(db: Db) -&amp;gt; Self {
        Self { db }
    }
}

impl KvsEngine for SledKvStore {
    fn get(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt; {
        let tree: &amp;amp;Tree = &amp;amp;self.db;
        Ok(
            tree.get(key)?
                .map(|iv| AsRef::&amp;lt;[u8]&amp;gt;::as_ref(&amp;amp;iv).to_vec())
                .map(String::from_utf8)
                .transpose()?
        )
    }

    fn set(&amp;amp;mut self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        let tree: &amp;amp;Tree = &amp;amp;self.db;
        tree.insert(key, value.into_bytes()).map(|_| ())?;
        tree.flush()?;
        Ok(())
    }

    fn remove(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        let tree: &amp;amp;Tree = &amp;amp;self.db;
        tree.remove(key)?.ok_or(KvsError::KeyNotFound)?;
        tree.flush()?;
        Ok(())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在&lt;code&gt;kvs-server.rs&lt;/code&gt;中调用&lt;code&gt;SledKvStore&lt;/code&gt;，以补全最后一个&lt;code&gt;//TODO&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// ...
match engine {
    Engine::kvs =&amp;gt; run_with_engine(KvStore::open(current_dir()?)?, args.addr),
    Engine::sled =&amp;gt; run_with_engine(SledKvStore::new(sled::open(current_dir()?)?), args.addr), 
}
//...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第6部分：基准测试&lt;/h2&gt;
&lt;p&gt;随着课程的进展，我们将越来越关注数据库的性能，探索不同架构对性能产生的影响。我们希望您不要局限于本文所展示的内容，多尝试自己的优化。&lt;/p&gt;
&lt;p&gt;性能比对需要基准测试，目前有很多方法可以对数据库进行基准测试：例如使用&lt;a href=&quot;https://github.com/brianfrankcooper/YCSB&quot;&gt;ycsb&lt;/a&gt;和&lt;a href=&quot;https://github.com/akopytov/sysbench&quot;&gt;sysbench&lt;/a&gt;等标准测试套件。Rust内置了一些基准测试工具，我们将从此处入手。&lt;/p&gt;
&lt;p&gt;Cargo支持使用&lt;code&gt;cargo bench&lt;/code&gt;进行基准测试。基准测试可以使用Rust内置的基准测试工具编写，也可以使用外部工具编写。&lt;/p&gt;
&lt;p&gt;为函数添加#[bench]属性即可启用内置基准测试套件。不过这一功能并不在Rust stable channel中提供，仅在&lt;a href=&quot;https://doc.rust-lang.org/stable/unstable-book/library-features/test.html&quot;&gt;the unstable book&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/stable/test/index.html&quot;&gt;&lt;code&gt;test&lt;/code&gt; crate docs&lt;/a&gt;中有简略描述。尽管它在整个Rust生态中已被广泛使用—这些crates即使使用stable编译，也会用nightly进行基准测试。&lt;/p&gt;
&lt;p&gt;尽管该系统实际上已被弃用—没有更新，且似乎永远不会被提升到stable channel。&lt;/p&gt;
&lt;p&gt;不过，Rust也有更好的基准测试工具。本项目将使用&lt;a href=&quot;https://docs.rs/criterion&quot;&gt;&lt;code&gt;criterion&lt;/code&gt;&lt;/a&gt;来对比&lt;code&gt;kvs&lt;/code&gt;引擎与&lt;code&gt;sled&lt;/code&gt;引擎的性能。&lt;/p&gt;
&lt;p&gt;这这些基准测试工具的工作原理是定义一个基准测试函数，然后在该函数内循环迭代要执行的基准测试操作。基准测试工具将根据需要进行多次迭代，以获取具有统计意义的操作持续时间。&lt;/p&gt;
&lt;p&gt;这是criterion指南中的这个基础示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn criterion_benchmark(c: &amp;amp;mut Criterion) {
    c.bench_function(&quot;fib 20&quot;, |b| {
        b.iter(|| {
            fibonacci(20)
        });
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用&lt;code&gt;bench_function&lt;/code&gt;定义基准，调用&lt;code&gt;iter&lt;/code&gt;定义为基准需要测试的代码。调用&lt;code&gt;iter&lt;/code&gt;之前和之后的代码不计入基准测试时间。&lt;/p&gt;
&lt;p&gt;通过创建一个名为&lt;code&gt;benches/engine_benches.rs&lt;/code&gt;的文件来准备编写基准测试。同&lt;code&gt;tests/tests.rs&lt;/code&gt;一样，cargo会自动找到这个文件并将其编译为基准测试。&lt;/p&gt;
&lt;p&gt;首先编写以下基准测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kvs_write&lt;/code&gt;—使用&lt;code&gt;kvs&lt;/code&gt;引擎，使用长度为1-100000字节的随机键 和 长度为1-100000字节的随机值 写入100次；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sled_write&lt;/code&gt;—使用&lt;code&gt;sled&lt;/code&gt;引擎，使用长度为1-100000字节的随机键 和 长度为1-100000字节的随机值 写入100次；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kvs_read&lt;/code&gt;—使用&lt;code&gt;kvs&lt;/code&gt;引擎，从先前写入的键中读取1000个值，键和值的长度是随机的；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sled_read&lt;/code&gt;—使用&lt;code&gt;sled&lt;/code&gt;引擎，从先前写入的键中读取1000个值，键和值的长度是随机的；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（除了像上面描述的这样编写4个基准测试，您也可以用这2个引擎名作为参数编写参数化的基准测试，参见&lt;a href=&quot;https://bheisler.github.io/criterion.rs/book/user_guide/benchmarking_with_inputs.html&quot;&gt;criterion手册&lt;/a&gt;）。&lt;/p&gt;
&lt;p&gt;对于实现这些基准测试的技术细节，我们至少需要考虑三个因素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;哪些代码应该计时（写在基准测试循环内），哪些代码不应该计时（写在基准测试循环之外）？&lt;/li&gt;
&lt;li&gt;尽管使用“随机”数字，如何使每次迭代执行相同操作。&lt;/li&gt;
&lt;li&gt;在&quot;read&quot;的基准测试中，如何使用之前写入的同一组“随机”键读取值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些都是相互关联的：用作基准测试环境设置的代码应为非计时代码，此外还需要适当地重用随机数种子以复现测试环境。&lt;/p&gt;
&lt;p&gt;在各种情况下，可能返回错误的操作都应该断言（使用&lt;code&gt;assert&lt;/code&gt;!）它们没有返回错误；在&quot;read&quot;测试中，&quot;get&quot;操作应该断言找到了该键的值。&lt;/p&gt;
&lt;p&gt;随机数可以使用&lt;code&gt;rand&lt;/code&gt;crate生成。&lt;/p&gt;
&lt;p&gt;编写完基准测试函数，就使用&lt;code&gt;cargo bench&lt;/code&gt;运行它们。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;完成上面的基准测试，对比&lt;code&gt;kvs&lt;/code&gt;和&lt;code&gt;sled&lt;/code&gt;的性能差距。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意：请在没什么负载的主机上运行基准测试。基准测试对其运行​​环境非常敏感，虽然criterion库会尽力修正这些“噪音”，但最好在没有其他活动进程的干净机器上进行基准测试。如果您有一台仅用于开发的备用机器，请使用它。如果没有，AWS或其他云实例可能会产生比本地桌面更一致的结果。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;在&lt;code&gt;Cargo.toml&lt;/code&gt;中声明基准测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[bench]]
name = &quot;engine_benches&quot;
harness = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;engine_benches.rs&lt;/code&gt;中编写基准测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use criterion::{Criterion, BatchSize, criterion_group, criterion_main};
use kvs::{KvStore, KvsEngine, SledKvStore};
use rand::{rngs::SmallRng, SeedableRng, Rng};
use sled;
use tempfile::TempDir;

fn set_benches(c: &amp;amp;mut Criterion) {
    let mut group = c.benchmark_group(&quot;set_benches&quot;);
    group.bench_function(&quot;kvs&quot;, |b| {
        b.iter_batched(
            || {
            let temp_dir = TempDir::new().unwrap();
            (KvStore::open(temp_dir.path()).unwrap(), temp_dir)
            }, 
            |(mut store, _temp_dir)| {
                for i in 1..(1 &amp;lt;&amp;lt; 12) {
                    store.set(format!(&quot;key-{}&quot;, i), format!(&quot;value-{}&quot;, i)).unwrap();
                }
            }, 
            BatchSize::SmallInput
        );
    });
    group.bench_function(&quot;sled&quot;, |b| {
        b.iter_batched(
            || {
            let temp_dir = TempDir::new().unwrap();
            (SledKvStore::new(sled::open(&amp;amp;temp_dir).unwrap()), temp_dir)
            }, 
            |(mut store, _temp_dir)| {
                for i in 1..(1 &amp;lt;&amp;lt; 12) {
                    store.set(format!(&quot;key-{}&quot;, i), format!(&quot;value-{}&quot;, i)).unwrap();
                }
            }, 
            BatchSize::SmallInput
        );
    });
}

fn get_benches(c: &amp;amp;mut Criterion) {
    let mut group = c.benchmark_group(&quot;get_benches&quot;);
    for i in &amp;amp;vec![8, 12, 16, 20] {
        group.bench_with_input(format!(&quot;kvs_{}&quot;, i), i, |b, i| {
            let temp_dir = TempDir::new().unwrap();
            let mut store = KvStore::open(temp_dir.path()).unwrap();
            for key_i in 1..(1 &amp;lt;&amp;lt; i) {
                store.set(format!(&quot;key-{}&quot;, key_i), format!(&quot;value-{}&quot;, key_i)).unwrap();
            }
            let mut rng = SmallRng::from_seed([0; 16]);
            b.iter(|| {
                store.get(format!(&quot;key-{}&quot;, rng.gen_range(1, 1 &amp;lt;&amp;lt; i))).unwrap();
            });
        });
    }
    for i in &amp;amp;vec![8, 12, 16, 20] {
        group.bench_with_input(format!(&quot;sled_{}&quot;, i), i, |b, i| {
            let temp_dir = TempDir::new().unwrap();
            let mut store = SledKvStore::new(sled::open(&amp;amp;temp_dir).unwrap());
            for key_i in 1..(1 &amp;lt;&amp;lt; i) {
                store.set(format!(&quot;key-{}&quot;, key_i), format!(&quot;value-{}&quot;, key_i)).unwrap();
            }
            let mut rng = SmallRng::from_seed([0; 16]);
            b.iter(|| {
                store.get(format!(&quot;key-{}&quot;, rng.gen_range(1, 1 &amp;lt;&amp;lt; i))).unwrap();
            });
        });
    }
    
}

criterion_group!(benches, set_benches, get_benches);
criterion_main!(benches);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;干得漂亮，朋友。休息一下吧。&lt;/p&gt;
</content:encoded></item><item><title>PingCap的Rust训练课程2：日志结构文件I/O</title><link>https://zlotus.github.io/2022/03/11/pna-rust-project-2/</link><guid isPermaLink="true">https://zlotus.github.io/2022/03/11/pna-rust-project-2/</guid><description>PingCap的Rust训练课程2：日志结构文件I/O</description><pubDate>Fri, 11 Mar 2022 18:50:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文内容大多翻译自原文：&lt;a href=&quot;https://github.com/pingcap/talent-plan/tree/master/courses/rust/projects/project-2&quot;&gt;PNA Rust Project 2: Log-structured file I/O&lt;/a&gt;。
:::&lt;/p&gt;
&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;任务&lt;/strong&gt;：创建一个键/值存储程序，能够&lt;em&gt;从命令行访问&lt;/em&gt;，支持&lt;em&gt;持久化&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编写健壮的错误和异常处理&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;serde&lt;/code&gt;进行序列化&lt;/li&gt;
&lt;li&gt;使用标准文件API将数据作为日志写入磁盘&lt;/li&gt;
&lt;li&gt;从磁盘读取键/值数据的状态&lt;/li&gt;
&lt;li&gt;将内存中的索引键映射在磁盘的对应值上&lt;/li&gt;
&lt;li&gt;定期压缩日志以删除过期数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键词&lt;/strong&gt;：日志结构文件I/O、bitcask、&lt;code&gt;failure&lt;/code&gt;crate、&lt;code&gt;Read&lt;/code&gt;/&lt;code&gt;Write&lt;/code&gt;trait、&lt;code&gt;serde&lt;/code&gt;crate。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;扩展练习&lt;/strong&gt;：尝试使用&lt;code&gt;structopt&lt;/code&gt;crate。&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在这个项目中，您将创建一个简单的基于磁盘的键/值存储程序，并可以通过命令行进行修改和查询。它将使用简化版的&lt;a href=&quot;https://github.com/basho/bitcask&quot;&gt;bitcask&lt;/a&gt;存储算法，该算法在简单和有效之间做了较好的平衡。您的第一个任务是在磁盘上维护先前写入命令的日志（有时称为“&lt;a href=&quot;https://en.wikipedia.org/wiki/Write-ahead_logging&quot;&gt;预写日志&lt;/a&gt;”或“WAL”），在程序启动时，通过对日志求值（译注：即按顺序执行日志）就能够在内存中重建数据库状态。您的第二个任务是继续拓展该程序，将键存储在内存中，将值存储到磁盘的日志中。您的最后一个任务是添加日志压缩功能，以确保日志不会无限增长。在这个项目结束时，您将使用Rust文件API构建一个简单且架构良好的数据库。&lt;/p&gt;
&lt;h2&gt;专业术语&lt;/h2&gt;
&lt;p&gt;下面列出本课程中使用的一些术语，部分参考了&lt;a href=&quot;https://github.com/basho/bitcask&quot;&gt;bitcask&lt;/a&gt;，不同的数据库会有略微不同的术语：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;命令（command）&lt;/em&gt; - 一条对数据库的请求或一条请求的表示。命令来源于命令行或网络。命令有三种表示：内存表示、文本表示以及机器可读的序列化表示。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;日志（log）&lt;/em&gt; - 由多条命令组成的序列，按照最初接收和执行的顺序放置在磁盘上。我们数据库在磁盘上的格式几乎完全由日志组成。这种实现很简单，而且非常高效。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;日志指针（log pointer）&lt;/em&gt; - 日志中的文件偏移量，有时我们将其简称为“文件偏移量”（file offset）。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;日志压缩（log compaction）&lt;/em&gt; - 当向数据库发出写入请求时，新的请求有时会使旧的日志条目无效。例如，写入键/值&lt;code&gt;a = 0&lt;/code&gt;，之后再写入&lt;code&gt;a = 1&lt;/code&gt;，这就会使&quot;a&quot;的第一条日志失效。压缩 — 至少在我们的数据库中 — 是一个从日志中删除过期的命令以减小数据库大小的过程。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;内存索引/索引（in-memory index/index）&lt;/em&gt; - 指向日志指针的键映射。当发出读请求时，会在内存索引中查找相应的日志指针，找到后从磁盘日志中取回该值。我们的键/值存储程序与bitcask类似，整个数据库的索引都存储在内存中。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;索引文件（index file）&lt;/em&gt; - 内存索引的磁盘表示。如果没有该文件，则每次启动数据库时都需要重放日志（重新执行整个日志）以恢复内存索引的状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;项目需求规格&lt;/h2&gt;
&lt;p&gt;cargo项目&lt;code&gt;kvs&lt;/code&gt;构建了一个名为&lt;code&gt;kvs&lt;/code&gt;的命令行键值存储客户端，该客户端又调用了一个名为&lt;code&gt;kvs&lt;/code&gt;的库。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kvs&lt;/code&gt;可执行文件支持以下命令行参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kvs set &amp;lt;KEY&amp;gt; &amp;lt;VALUE&amp;gt;&lt;/code&gt;
将字符串键的值设置为字符串值。打印错误并在失败时返回非零退出码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kvs get &amp;lt;KEY&amp;gt;&lt;/code&gt;
获取给定字符串键的字符串值。打印错误并在失败时返回非零退出码。
-&lt;code&gt;kvs rm &amp;lt;KEY&amp;gt;&lt;/code&gt;
删除给定的键。打印错误并在失败时返回非零退出码。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kvs -V&lt;/code&gt;
打印版本。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;kvs&lt;/code&gt;库包含一个类型，&lt;code&gt;KvStore&lt;/code&gt;，它支持以下方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KvStore::set(&amp;amp;mut self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt;&lt;/code&gt;
将字符串键设置为字符串值。如果值未设置成功，则返回错误。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvStore::get(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt;&lt;/code&gt;
获取字符串键的字符串值。如果键不存在，则返回&lt;code&gt;None&lt;/code&gt;。如果读取键值未成功，则返回错误。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvStore::remove(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt;&lt;/code&gt;
删除给定的key。如果key不存在或未成功删除，则返回错误。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvStore::open(path: impl Into&amp;lt;PathBuf&amp;gt;) -&amp;gt; Result&amp;lt;KvStore&amp;gt;&lt;/code&gt;
从给定路径打开KvStore。返回KvStore。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在设置键值时，&lt;code&gt;kvs&lt;/code&gt;将这条&lt;code&gt;set&lt;/code&gt;命令写入位于磁盘的顺序日志中，然后将该命令的日志指针（文件偏移量）存储在内存索引中，即从键映射到日志指针。删除键时，类似地，&lt;code&gt;kvs&lt;/code&gt;会在日志中写入&lt;code&gt;rm&lt;/code&gt;命令，然后从内存索引中删除该键。当使用&lt;code&gt;get&lt;/code&gt;命令检索键的值时，它会搜索内存索引，如果找到，则从日志中加载对应日志指针处的命令，对命令求值（译注：执行命令）并返回结果。&lt;/p&gt;
&lt;p&gt;启动时，按时间顺序遍历日志中的命令，即可重建内存索引。&lt;/p&gt;
&lt;p&gt;当未压缩日志记录大小达到给定阈值时，&lt;code&gt;kvs&lt;/code&gt;会将其压缩到新日志中，删除冗余条目以回收磁盘空间。&lt;/p&gt;
&lt;p&gt;请注意，我们的&lt;code&gt;kvs&lt;/code&gt;项目既是一个无状态的命令行程序，也是一个包含有状态&lt;code&gt;KvStore&lt;/code&gt;类型的库：作为命令行程序使用，&lt;code&gt;KvStore&lt;/code&gt;类型将加载索引、执行命令、并退出；作为库使用，它将加载索引，然后执行多条命令，维护索引状态，直到它被删除。&lt;/p&gt;
&lt;h2&gt;项目设置&lt;/h2&gt;
&lt;p&gt;在上一个项目的基础上进行本项目，删除上一个项目的&lt;code&gt;tests&lt;/code&gt;目录，将本项目的&lt;code&gt;tests&lt;/code&gt;目录复制到该位置。和上一个项目一样，本项目应该包含一个库和一个可执行文件，两者都叫做&lt;code&gt;kvs&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;中需要以下开发依赖项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dev-dependencies]
assert_cmd = &quot;0.11.0&quot;
predicates = &quot;1.0.0&quot;
tempfile = &quot;3.0.7&quot;
walkdir = &quot;2.2.7&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与上一个项目一样，先编写空函数或panic来构建测试用例。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;现在就试试吧。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我的作业：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// 若仅让tests能够运行，则需要修改的地方很少

// 添加`PathBuf`导入
use std::{collections::HashMap, path::PathBuf};

// 添加Result&amp;lt;T&amp;gt;类型
pub type Result&amp;lt;T&amp;gt; = std::result::Result&amp;lt;T, ()&amp;gt;;

// 修改`KvStore`中函数的返回类型
impl KvStore {
    //...
    pub fn open(path: impl Into&amp;lt;PathBuf&amp;gt;) -&amp;gt; Result&amp;lt;KvStore&amp;gt; {
        Ok(Self::new())
    }
    //...
    pub fn get(&amp;amp;self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt; {
        match self.db.get(&amp;amp;key) {
            Some(value) =&amp;gt; Ok(Some(value.to_string())),
            None =&amp;gt; Err(())
        }
    }
    //...
    pub fn set(&amp;amp;mut self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        self.db.insert(key, value);
        Ok(())
    }
    //...
    pub fn remove(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt;{
        self.db.remove(&amp;amp;key);
        Ok(())
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;cargo test&lt;/code&gt;结果如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
running 17 tests
test cli_invalid_subcommand ... ok
test cli_get_non_existent_key ... FAILED
test cli_rm_non_existent_key ... FAILED
test cli_get_stored ... FAILED
test cli_no_args ... ok
test get_non_existent_value ... FAILED
test get_stored_value ... FAILED
test compaction ... FAILED
test cli_invalid_get ... ok
test overwrite_value ... FAILED
test remove_non_existent_key ... FAILED
test cli_version ... ok
test remove_key ... FAILED
test cli_invalid_rm ... ok
test cli_set ... FAILED
test cli_rm_stored ... FAILED
test cli_invalid_set ... ok
...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第1部分：异常处理&lt;/h2&gt;
&lt;p&gt;在本项目中，代码运行时可能会因为I/O错误而失败。因此，在开始实现数据库之前，我们需要再做一件对Rust项目至关重要的事情：确定错误处理策略。&lt;/p&gt;
&lt;p&gt;Rust的错误处理功能强大，但需要大量模板代码才能正确使用。本项目使用&lt;a href=&quot;https://docs.rs/failure/0.1.5/failure/&quot;&gt;&lt;code&gt;failure&lt;/code&gt;&lt;/a&gt;crate以获得轻松处理各种错误的工具。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://boats.gitlab.io/failure/&quot;&gt;异常处理指南&lt;/a&gt;描述了&lt;a href=&quot;https://boats.gitlab.io/failure/guidance.html&quot;&gt;几种&lt;/a&gt;常见的错误处理模式。&lt;/p&gt;
&lt;p&gt;在您的实现中，选择其中一种策略，定义您自己的错误类型，或导入&lt;code&gt;failures&lt;/code&gt;的&lt;code&gt;Error&lt;/code&gt;。您将在所有&lt;code&gt;Result&lt;/code&gt;中使用该错误类型，使用&lt;code&gt;?&lt;/code&gt;运算符将其他crate中的错误类型转换为您自己定义的错误类型。&lt;/p&gt;
&lt;p&gt;接下来，给包含您自定义错误类型的&lt;code&gt;Result&lt;/code&gt;起一个类型别名，这样您就不需要在项目中键入&lt;code&gt;Result&amp;lt;T, YourErrorType&amp;gt;&lt;/code&gt;了，只需&lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt;即可。这是Rust中常见的模式。&lt;/p&gt;
&lt;p&gt;最后，使用&lt;code&gt;use&lt;/code&gt;语句将这些类型导入您的可执行文件，并更改&lt;code&gt;main&lt;/code&gt;函数签名以返回&lt;code&gt;Result&amp;lt;()&amp;gt;&lt;/code&gt;。库中所有可能失败的函数都会将这些&lt;code&gt;Result&lt;/code&gt;沿栈一路向下传递到&lt;code&gt;main&lt;/code&gt;，然后传递到Rust运行时并打印出错误信息。&lt;/p&gt;
&lt;p&gt;运行&lt;code&gt;cargo check&lt;/code&gt;查找编译器错误并予以修复。目前可以用&lt;code&gt;panic!()&lt;/code&gt;结束&lt;code&gt;main&lt;/code&gt;函数以使项目通过编译。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;在继续之前确定您的异常处理策略&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;与之前的项目一样，您需要创建用于占位的数据结构和方法，以使测试能够编译。现在您已经定义了一个错误类型，通过编译应该很简单。可以在任何地方添加panic以使得测试套件能够通过编译（&lt;code&gt;cargo test --no-run&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;注意：Rust中的错误处理实践仍在不断发展。本课程目前使用&lt;a href=&quot;https://docs.rs/failure/0.1.5/failure/&quot;&gt;&lt;code&gt;failure&lt;/code&gt;&lt;/a&gt;crate来简化定义错误类型的过程。虽然&lt;code&gt;failure&lt;/code&gt;设计良好，但使用它可能&lt;a href=&quot;https://github.com/rust-lang-nursery/rust-cookbook/issues/502#issue-387418261&quot;&gt;并不是最佳实践&lt;/a&gt;。 Rust专家可能不会继续看好这一实践。本课程也在迭代更新，未来可能不会使用&lt;code&gt;failure&lt;/code&gt;。不过起码目前这一实践还不错，也提供了一个了解更多Rust错误处理发展细节的机会。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我的作业：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;//...
mod error;
pub use error::{Result, KvsError};
//...
impl KvStore {
    //...
    pub fn get(&amp;amp;self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt; {
        match self.db.get(&amp;amp;key) {
            Some(value) =&amp;gt; Ok(Some(value.to_string())),
            None =&amp;gt; Err(KvsError::KeyNotFound(key))
        }
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新增的自定义错误&lt;code&gt;error&lt;/code&gt;模块（其实只定义一个错误就能通过编译）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use failure::Fail;
use std::io;

/// custom error type of kvs
#[derive(Fail, Debug)]
pub enum KvsError {
    /// Key not found in kvs index
    #[fail(display=&quot;Key `{}` not found&quot;, _0)]
    KeyNotFound(String),
}

/// simplify `Result&amp;lt;T, KvsError&amp;gt;` to `Result&amp;lt;T&amp;gt;`
pub type Result&amp;lt;T&amp;gt; = std::result::Result&amp;lt;T, KvsError&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;第2部分 日志行为&lt;/h2&gt;
&lt;p&gt;现在我们终于要开始实现一个数据库的基础功能并读写磁盘了。您将使用&lt;a href=&quot;https://serde.rs/&quot;&gt;&lt;code&gt;serde&lt;/code&gt;&lt;/a&gt;将&quot;set&quot;和&quot;rm&quot;命令序列化为字符串，再使用标准文件I/O API将其写入磁盘。&lt;/p&gt;
&lt;p&gt;这是带日志&lt;code&gt;kvs&lt;/code&gt;的基本行为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&quot;set&quot;：
&lt;ul&gt;
&lt;li&gt;用户调用&lt;code&gt;kvs set mykey myvalue&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kvs&lt;/code&gt;创建一个值来表示&quot;set&quot;命令，包含命令的键和值&lt;/li&gt;
&lt;li&gt;将该命令序列化为&lt;code&gt;String&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将序列化后的命令追加到日志文件中&lt;/li&gt;
&lt;li&gt;若执行成功，则返回错误码&lt;code&gt;0&lt;/code&gt;，静默退出&lt;/li&gt;
&lt;li&gt;若执行失败，则打印错误，返回非零错误码并退出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&quot;get&quot;
&lt;ul&gt;
&lt;li&gt;用户调用&lt;code&gt;kvs get mykey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kvs&lt;/code&gt;读取整个日志，一次一条命令，将被命令影响的键及文件偏移量录入到内存中的“键-&amp;gt;日志指针”映射中&lt;/li&gt;
&lt;li&gt;查找日志指针映射&lt;/li&gt;
&lt;li&gt;若查找失败，则打印&quot;Key not found&quot;，并以退出码&lt;code&gt;0&lt;/code&gt;退出&lt;/li&gt;
&lt;li&gt;若查找成功：
&lt;ul&gt;
&lt;li&gt;反序列化命令以获取该键的最新记录值&lt;/li&gt;
&lt;li&gt;将值打印到标准输出，并以退出码&lt;code&gt;0&lt;/code&gt;退出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&quot;rm&quot;
&lt;ul&gt;
&lt;li&gt;用户调用&lt;code&gt;kvs rm mykey&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;与&quot;get&quot;命令相同，&lt;code&gt;kvs&lt;/code&gt;读取整个日志以重建内存索引&lt;/li&gt;
&lt;li&gt;在映射中查找给定的键&lt;/li&gt;
&lt;li&gt;若键不存在，则打印&quot;Key not found&quot;，并以非零错误码退出&lt;/li&gt;
&lt;li&gt;若找到该键：
&lt;ul&gt;
&lt;li&gt;创建一个值来表示&quot;rm&quot;命令，包含命令的键&lt;/li&gt;
&lt;li&gt;将序列化的命令追加到日志中&lt;/li&gt;
&lt;li&gt;若成功完成，则以错误码&lt;code&gt;0&lt;/code&gt;静默退出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;日志是一份提交到数据库的事务的记录。通过在启动时“重放”日志中的记录，即可重建数据库先前的状态。&lt;/p&gt;
&lt;p&gt;在本次迭代中，您可以将键的值直接存储在内存中（因此在程序初始化和日志重放后就不再需要从读取日志了）。在后面的迭代中，您只需将“日志指针”（文件偏移量）存储在内存中。&lt;/p&gt;
&lt;h2&gt;第3部分：写日志&lt;/h2&gt;
&lt;p&gt;您将从实现&quot;set&quot;命令开始，有很多步骤，大多数步骤都很容易实现。您可以通过运行相应的的&lt;code&gt;cli_*&lt;/code&gt;测试用例来验证。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;serde&lt;/code&gt;是一个有有很多选项的大型库，支持多种序列化格式。只需要正确注释您的数据结构即可使用基础的序列化/反序列化功能，然后再调用函数将其写入&lt;code&gt;String&lt;/code&gt;或任何实现了&lt;code&gt;Write&lt;/code&gt;trait的流。&lt;/p&gt;
&lt;p&gt;您需要选择一种序列化格式。考虑您想要的序列化属性 — 需要优先考虑性能吗？需要以纯文本形式查看日志内容吗？这都是您选择时需要考虑的属性，您应该在代码注释中解释您的选择。&lt;/p&gt;
&lt;p&gt;其他需要考虑的因素包括：系统的哪些部分需要使用缓冲、您需要在哪里设置缓冲？缓冲对后续的读操作有什么影响？什么时候打开/关闭文件句柄？每个命令都需要操作句柄吗？还是在&lt;code&gt;KvStore&lt;/code&gt;的整个生命周期内都不释放句柄？（译注，可以使用&lt;a href=&quot;https://doc.rust-lang.org/std/io/struct.BufWriter.html&quot;&gt;&lt;code&gt;BufWriter&lt;/code&gt;&lt;/a&gt;提高写效率）&lt;/p&gt;
&lt;p&gt;您会用到的部分API可能会调用失败，并返回包含某种错误类型的&lt;code&gt;Result&lt;/code&gt;。确保函数返回包含您自定义错误类型的&lt;code&gt;Result&lt;/code&gt;，以决定是否使用&lt;code&gt;?&lt;/code&gt;传播异常。&lt;/p&gt;
&lt;p&gt;实现&quot;rm&quot;命令也类似，但您应该在将命令写入日志前，检查该键是否已经存在。由于我们必须区分两个不同的命令，因此您可以使用枚举类型变量来表示每个命令。而&lt;code&gt;serde&lt;/code&gt;可以完美应用在枚举类型上。&lt;/p&gt;
&lt;p&gt;您现在可以实现&quot;set&quot;和&quot;rm&quot;命令了，专注于通过&lt;code&gt;set&lt;/code&gt;/&lt;code&gt;rm&lt;/code&gt;的相关测试用例。此外您也可以继续阅读下一节的&quot;get&quot;命令相关内容。在实现过程中，同时考虑读、写两种操作可能使代码更容易编写。逐项实现或同时实现均可。&lt;/p&gt;
&lt;h2&gt;第4部分：读日志&lt;/h2&gt;
&lt;p&gt;现在该实现&quot;get&quot;命令了。您暂时还不用考虑在索引中存储日志指针的特性，我们将该特性的实现留到下一部分。目前，您只需在启动时读取日志中的每个命令，执行这些指令，以将所有键/值保存在内存中，再从内存中读取这些键值。&lt;/p&gt;
&lt;p&gt;此时您需要考虑：应该把日志中的所有记录一次读入内存，再将一次性重放以初始化你的映射类型；还是应该一次读取一条的重放命令并初始化您的映射？应该在反序列化前先将日志读取到缓冲区；还是应该直接反序列化一个文件流？思考您的实现对应的内存使用情况，也要考虑从I/O流中读取数据时与内核交互的情况。（译注，可以使用&lt;a href=&quot;https://doc.rust-lang.org/std/io/struct.BufReader.html&quot;&gt;&lt;code&gt;BufReader&lt;/code&gt;&lt;/a&gt;提高读效率）&lt;/p&gt;
&lt;p&gt;请记住，&quot;get&quot;也可能找不到指定键，这种情况需要特别处理。本项目的设定为，API返回&lt;code&gt;None&lt;/code&gt;，且命令行客户端打印一条特定的消息并以零退出码退出。&lt;/p&gt;
&lt;p&gt;读取日志有一个复杂之处，您可能在编写&quot;set&quot;代码时已经考虑过：如何区分日志文件中的每条记录？也就是说，程序怎么知道一条记录在哪里结束，而下一题记录从哪里开始？这会是一个问题吗？也许&lt;code&gt;serde&lt;/code&gt;会直接从I/O流中反序列化一条记录，并在完成后停止读取，将文件光标留在正确的位置以便继续读取后面的记录？也许&lt;code&gt;serde&lt;/code&gt;在看到两条记录粘在一起时会报错？也许你需要插入额外的信息来区分每条记录的长度？也许不用？&lt;/p&gt;
&lt;p&gt;&lt;em&gt;现在实现&quot;get&quot;命令。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;(译注：此时使用&lt;code&gt;BufReader&lt;/code&gt;和&lt;code&gt;BufWriter&lt;/code&gt;实现即可，暂不需要记录命令在日志中的位置。虽然此时应新建&lt;code&gt;Command&lt;/code&gt;枚举类型，并使用&lt;code&gt;serde_json&lt;/code&gt;从日志文件中读取整条命令，但内存中的存储索引的形式暂时仍为&lt;code&gt;BTreeMap&amp;lt;String, String&amp;gt;&lt;/code&gt;。同样，写操作也使用&lt;code&gt;serde_json&lt;/code&gt;直接写入&lt;code&gt;Command&lt;/code&gt;枚举类型。)&lt;/p&gt;
&lt;h2&gt;第5部分：在索引中存储日志指针&lt;/h2&gt;
&lt;p&gt;此时，测试条件中，除了压缩测试外的其他测试都应该能够通过。后续步骤中引入的是一些优化特性，这对于提高性能和减少存储空间是十分必要的。在实现这些特性时，请注意我们在优化什么。&lt;/p&gt;
&lt;p&gt;正如我们所描述的，您正在构建的数据库将在内存中维护所有键的索引。该索引从键的字符串映射到日志的指针，而不是映射值本身。&lt;/p&gt;
&lt;p&gt;此更改需要我们能够从日志的任意偏移量处读取数据，考虑如何按照此更改修改您的文件句柄管理方式。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;如果在前面的步骤中，您选择将值的字符串直接存储在内存中，那么现在是时候更新代码，以存放日志指针了，然后实现按需从磁盘加载。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;（译注：此时需要引入变量记录读写日志文件的位置，并将内存中的存储索引形式修改为&lt;code&gt;BTreeMap&amp;lt;String, CommandPos{ pos: u64, len: u64 }&amp;gt;&lt;/code&gt;）&lt;/p&gt;
&lt;h2&gt;第6部分：无状态&lt;code&gt;KvStore&lt;/code&gt; 与 有状态&lt;code&gt;KvStore&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;请记住，我们的项目既是库又是命令行程序。它们的要求略有不同：&lt;code&gt;kvs&lt;/code&gt;命令行程序将单个更改提交到磁盘，然后退出（它是无状态的）；&lt;code&gt;KvStore&lt;/code&gt;类将更改提交到磁盘，然后驻留在内存中以提供的查询服务（它是有状态的）。&lt;/p&gt;
&lt;p&gt;您的&lt;code&gt;KvStore&lt;/code&gt;是有状态的还是无状态的？&lt;/p&gt;
&lt;p&gt;&lt;em&gt;让您的&lt;code&gt;KvStore&lt;/code&gt;将索引保留在内存中，这样它就不需要为每次接受&lt;code&gt;get&lt;/code&gt;命令时都对日志求值。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;第7部分：压缩日志&lt;/h2&gt;
&lt;p&gt;此时数据库工作正常，但日志无限增长。这适用于某些数据库，但不适用于我们正在构建的数据库 — 希望尽可能减少磁盘用量。&lt;/p&gt;
&lt;p&gt;因此，创建数据库的最后一步是压缩日志。在使用中，随着日志的增长，对于某个键，可能会有多条命令进行设置值或删除键。不过对于该键，只有最新的修改命令才会修改它的值：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;idx&lt;/th&gt;
&lt;th&gt;command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;~Command::Set(&quot;key-1&quot;, &quot;value-1a&quot;)~&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Command::Set(&quot;key-2&quot;, &quot;value-2&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;Command::Set(&quot;key-1&quot;, &quot;value-1b&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在上面的例子中，索引&lt;code&gt;0&lt;/code&gt;的命令显然是多余的，因此不需要存储它。日志压缩就是重建日志，以删除冗余命令：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;idx&lt;/th&gt;
&lt;th&gt;command&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Command::Set(&quot;key-2&quot;, &quot;value-2&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;Command::Set(&quot;key-1&quot;, &quot;value-1b&quot;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;基本算法如下：&lt;/p&gt;
&lt;p&gt;您应当自行考虑&lt;em&gt;如何&lt;/em&gt;重建日志。同时思考以下问题：朴素的解决方案是什么？需要多少内存？压缩日志所需的最小复制量是多少？压缩可以就地完成吗？如果压缩失败，您如何保持数据完整性？&lt;/p&gt;
&lt;p&gt;到目前为止，我们一直再说“日志文件”，但实际上数据库通常会将大量日志存储在不同的文件中。在实现过程中您可能会发现，如果跨文件拆分日志将会更容易的压缩日志。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;为您的数据库实现日志压缩功能。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;恭喜！您已经成功实现了一个功能齐全的数据库。&lt;/p&gt;
&lt;p&gt;如果您仍然好奇，那么现在可以对比您的键/值数据库与其他同类数据库（如&lt;a href=&quot;https://github.com/spacejam/sled&quot;&gt;sled&lt;/a&gt;、&lt;a href=&quot;https://github.com/basho/bitcask&quot;&gt;bitcask&lt;/a&gt;、&lt;a href=&quot;https://github.com/dgraph-io/badger&quot;&gt;badger&lt;/a&gt;或&lt;a href=&quot;https://rocksdb.org/&quot;&gt;RocksDB&lt;/a&gt;）的性能。您可以继续研究他们的架构，思考他们的架构与您的架构的异同，以及架构如何影响性能。接下来的几个项目将为您提供优化的机会。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── src
│  ├── bin
│  │  └── kvs.rs
│  ├── error.rs
│  ├── kv.rs
│  └── lib.rs
└── tests
   └── tests.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要改动了&lt;code&gt;lib.rs&lt;/code&gt;，将功能实现迁移至&lt;code&gt;kv.rs&lt;/code&gt;，将错误处理迁移至&lt;code&gt;error.rs&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![deny(missing_docs)]
//! A simple key/value store.

pub use error::{KvsError, Result};
pub use kv::KvStore;

mod error;
mod kv;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use std::collections::{BTreeMap, HashMap};
use std::fs::{self, File, OpenOptions};
use std::io::{self, BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::ops::Range;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use serde_json::Deserializer;

use crate::{KvsError, Result};
use std::ffi::OsStr;

const COMPACTION_THRESHOLD: u64 = 1024 * 1024;

/// The `KvStore` stores string key/value pairs.
///
/// Key/value pairs are persisted to disk in log files. Log files are named after
/// monotonically increasing generation numbers with a `log` extension name.
/// A `BTreeMap` in memory stores the keys and the value locations for fast query.
///
/// ```rust
/// # use kvs::{KvStore, Result};
/// # fn try_main() -&amp;gt; Result&amp;lt;()&amp;gt; {
/// use std::env::current_dir;
/// let mut store = KvStore::open(current_dir()?)?;
/// store.set(&quot;key&quot;.to_owned(), &quot;value&quot;.to_owned())?;
/// let val = store.get(&quot;key&quot;.to_owned())?;
/// assert_eq!(val, Some(&quot;value&quot;.to_owned()));
/// # Ok(())
/// # }
/// ```
pub struct KvStore {
    // directory for the log and other data.
    path: PathBuf,
    // map generation number to the file reader.
    readers: HashMap&amp;lt;u64, BufReaderWithPos&amp;lt;File&amp;gt;&amp;gt;,
    // writer of the current log.
    writer: BufWriterWithPos&amp;lt;File&amp;gt;,
    current_gen: u64,
    index: BTreeMap&amp;lt;String, CommandPos&amp;gt;,
    // the number of bytes representing &quot;stale&quot; commands that could be
    // deleted during a compaction.
    uncompacted: u64,
}

impl KvStore {
    /// Opens a `KvStore` with the given path.
    ///
    /// This will create a new directory if the given one does not exist.
    ///
    /// # Errors
    ///
    /// It propagates I/O or deserialization errors during the log replay.
    pub fn open(path: impl Into&amp;lt;PathBuf&amp;gt;) -&amp;gt; Result&amp;lt;KvStore&amp;gt; {
        let path = path.into();
        fs::create_dir_all(&amp;amp;path)?;

        let mut readers = HashMap::new();
        let mut index = BTreeMap::new();

        let gen_list = sorted_gen_list(&amp;amp;path)?;
        let mut uncompacted = 0;

        for &amp;amp;gen in &amp;amp;gen_list {
            let mut reader = BufReaderWithPos::new(File::open(log_path(&amp;amp;path, gen))?)?;
            uncompacted += load(gen, &amp;amp;mut reader, &amp;amp;mut index)?;
            readers.insert(gen, reader);
        }

        let current_gen = gen_list.last().unwrap_or(&amp;amp;0) + 1;
        let writer = new_log_file(&amp;amp;path, current_gen, &amp;amp;mut readers)?;

        Ok(KvStore {
            path,
            readers,
            writer,
            current_gen,
            index,
            uncompacted,
        })
    }

    /// Sets the value of a string key to a string.
    ///
    /// If the key already exists, the previous value will be overwritten.
    ///
    /// # Errors
    ///
    /// It propagates I/O or serialization errors during writing the log.
    pub fn set(&amp;amp;mut self, key: String, value: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        let cmd = Command::set(key, value);
        let pos = self.writer.pos;
        serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;cmd)?;
        self.writer.flush()?;
        if let Command::Set { key, .. } = cmd {
            if let Some(old_cmd) = self
                .index
                .insert(key, (self.current_gen, pos..self.writer.pos).into())
            {
                self.uncompacted += old_cmd.len;
            }
        }

        if self.uncompacted &amp;gt; COMPACTION_THRESHOLD {
            self.compact()?;
        }
        Ok(())
    }

    /// Gets the string value of a given string key.
    ///
    /// Returns `None` if the given key does not exist.
    ///
    /// # Errors
    ///
    /// It returns `KvsError::UnexpectedCommandType` if the given command type unexpected.
    pub fn get(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;Option&amp;lt;String&amp;gt;&amp;gt; {
        if let Some(cmd_pos) = self.index.get(&amp;amp;key) {
            let reader = self
                .readers
                .get_mut(&amp;amp;cmd_pos.gen)
                .expect(&quot;Cannot find log reader&quot;);
            reader.seek(SeekFrom::Start(cmd_pos.pos))?;
            let cmd_reader = reader.take(cmd_pos.len);
            if let Command::Set { value, .. } = serde_json::from_reader(cmd_reader)? {
                Ok(Some(value))
            } else {
                Err(KvsError::UnexpectedCommandType)
            }
        } else {
            Ok(None)
        }
    }

    /// Removes a given key.
    ///
    /// # Errors
    ///
    /// It returns `KvsError::KeyNotFound` if the given key is not found.
    ///
    /// It propagates I/O or serialization errors during writing the log.
    pub fn remove(&amp;amp;mut self, key: String) -&amp;gt; Result&amp;lt;()&amp;gt; {
        if self.index.contains_key(&amp;amp;key) {
            let cmd = Command::remove(key);
            serde_json::to_writer(&amp;amp;mut self.writer, &amp;amp;cmd)?;
            self.writer.flush()?;
            if let Command::Remove { key } = cmd {
                let old_cmd = self.index.remove(&amp;amp;key).expect(&quot;key not found&quot;);
                self.uncompacted += old_cmd.len;
            }
            Ok(())
        } else {
            Err(KvsError::KeyNotFound)
        }
    }

    /// Clears stale entries in the log.
    pub fn compact(&amp;amp;mut self) -&amp;gt; Result&amp;lt;()&amp;gt; {
        // increase current gen by 2. current_gen + 1 is for the compaction file.
        let compaction_gen = self.current_gen + 1;
        self.current_gen += 2;
        self.writer = self.new_log_file(self.current_gen)?;

        let mut compaction_writer = self.new_log_file(compaction_gen)?;

        let mut new_pos = 0; // pos in the new log file.
        for cmd_pos in &amp;amp;mut self.index.values_mut() {
            let reader = self
                .readers
                .get_mut(&amp;amp;cmd_pos.gen)
                .expect(&quot;Cannot find log reader&quot;);
            if reader.pos != cmd_pos.pos {
                reader.seek(SeekFrom::Start(cmd_pos.pos))?;
            }

            let mut entry_reader = reader.take(cmd_pos.len);
            let len = io::copy(&amp;amp;mut entry_reader, &amp;amp;mut compaction_writer)?;
            *cmd_pos = (compaction_gen, new_pos..new_pos + len).into();
            new_pos += len;
        }
        compaction_writer.flush()?;

        // remove stale log files.
        let stale_gens: Vec&amp;lt;_&amp;gt; = self
            .readers
            .keys()
            .filter(|&amp;amp;&amp;amp;gen| gen &amp;lt; compaction_gen)
            .cloned()
            .collect();
        for stale_gen in stale_gens {
            self.readers.remove(&amp;amp;stale_gen);
            fs::remove_file(log_path(&amp;amp;self.path, stale_gen))?;
        }
        self.uncompacted = 0;

        Ok(())
    }

    /// Create a new log file with given generation number and add the reader to the readers map.
    ///
    /// Returns the writer to the log.
    fn new_log_file(&amp;amp;mut self, gen: u64) -&amp;gt; Result&amp;lt;BufWriterWithPos&amp;lt;File&amp;gt;&amp;gt; {
        new_log_file(&amp;amp;self.path, gen, &amp;amp;mut self.readers)
    }
}

/// Create a new log file with given generation number and add the reader to the readers map.
///
/// Returns the writer to the log.
fn new_log_file(
    path: &amp;amp;Path,
    gen: u64,
    readers: &amp;amp;mut HashMap&amp;lt;u64, BufReaderWithPos&amp;lt;File&amp;gt;&amp;gt;,
) -&amp;gt; Result&amp;lt;BufWriterWithPos&amp;lt;File&amp;gt;&amp;gt; {
    let path = log_path(&amp;amp;path, gen);
    let writer = BufWriterWithPos::new(
        OpenOptions::new()
            .create(true)
            .write(true)
            .append(true)
            .open(&amp;amp;path)?,
    )?;
    readers.insert(gen, BufReaderWithPos::new(File::open(&amp;amp;path)?)?);
    Ok(writer)
}

/// Returns sorted generation numbers in the given directory.
fn sorted_gen_list(path: &amp;amp;Path) -&amp;gt; Result&amp;lt;Vec&amp;lt;u64&amp;gt;&amp;gt; {
    let mut gen_list: Vec&amp;lt;u64&amp;gt; = fs::read_dir(&amp;amp;path)?
        .flat_map(|res| -&amp;gt; Result&amp;lt;_&amp;gt; { Ok(res?.path()) })
        .filter(|path| path.is_file() &amp;amp;&amp;amp; path.extension() == Some(&quot;log&quot;.as_ref()))
        .flat_map(|path| {
            path.file_name()
                .and_then(OsStr::to_str)
                .map(|s| s.trim_end_matches(&quot;.log&quot;))
                .map(str::parse::&amp;lt;u64&amp;gt;)
        })
        .flatten()
        .collect();
    gen_list.sort_unstable();
    Ok(gen_list)
}

/// Load the whole log file and store value locations in the index map.
///
/// Returns how many bytes can be saved after a compaction.
fn load(
    gen: u64,
    reader: &amp;amp;mut BufReaderWithPos&amp;lt;File&amp;gt;,
    index: &amp;amp;mut BTreeMap&amp;lt;String, CommandPos&amp;gt;,
) -&amp;gt; Result&amp;lt;u64&amp;gt; {
    // To make sure we read from the beginning of the file.
    let mut pos = reader.seek(SeekFrom::Start(0))?;
    let mut stream = Deserializer::from_reader(reader).into_iter::&amp;lt;Command&amp;gt;();
    let mut uncompacted = 0; // number of bytes that can be saved after a compaction.
    while let Some(cmd) = stream.next() {
        let new_pos = stream.byte_offset() as u64;
        match cmd? {
            Command::Set { key, .. } =&amp;gt; {
                if let Some(old_cmd) = index.insert(key, (gen, pos..new_pos).into()) {
                    uncompacted += old_cmd.len;
                }
            }
            Command::Remove { key } =&amp;gt; {
                if let Some(old_cmd) = index.remove(&amp;amp;key) {
                    uncompacted += old_cmd.len;
                }
                // the &quot;remove&quot; command itself can be deleted in the next compaction.
                // so we add its length to `uncompacted`.
                uncompacted += new_pos - pos;
            }
        }
        pos = new_pos;
    }
    Ok(uncompacted)
}

fn log_path(dir: &amp;amp;Path, gen: u64) -&amp;gt; PathBuf {
    dir.join(format!(&quot;{}.log&quot;, gen))
}

/// Struct representing a command.
#[derive(Serialize, Deserialize, Debug)]
enum Command {
    Set { key: String, value: String },
    Remove { key: String },
}

impl Command {
    fn set(key: String, value: String) -&amp;gt; Command {
        Command::Set { key, value }
    }

    fn remove(key: String) -&amp;gt; Command {
        Command::Remove { key }
    }
}

/// Represents the position and length of a json-serialized command in the log.
struct CommandPos {
    gen: u64,
    pos: u64,
    len: u64,
}

impl From&amp;lt;(u64, Range&amp;lt;u64&amp;gt;)&amp;gt; for CommandPos {
    fn from((gen, range): (u64, Range&amp;lt;u64&amp;gt;)) -&amp;gt; Self {
        CommandPos {
            gen,
            pos: range.start,
            len: range.end - range.start,
        }
    }
}

struct BufReaderWithPos&amp;lt;R: Read + Seek&amp;gt; {
    reader: BufReader&amp;lt;R&amp;gt;,
    pos: u64,
}

impl&amp;lt;R: Read + Seek&amp;gt; BufReaderWithPos&amp;lt;R&amp;gt; {
    fn new(mut inner: R) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        let pos = inner.seek(SeekFrom::Current(0))?;
        Ok(BufReaderWithPos {
            reader: BufReader::new(inner),
            pos,
        })
    }
}

impl&amp;lt;R: Read + Seek&amp;gt; Read for BufReaderWithPos&amp;lt;R&amp;gt; {
    fn read(&amp;amp;mut self, buf: &amp;amp;mut [u8]) -&amp;gt; io::Result&amp;lt;usize&amp;gt; {
        let len = self.reader.read(buf)?;
        self.pos += len as u64;
        Ok(len)
    }
}

impl&amp;lt;R: Read + Seek&amp;gt; Seek for BufReaderWithPos&amp;lt;R&amp;gt; {
    fn seek(&amp;amp;mut self, pos: SeekFrom) -&amp;gt; io::Result&amp;lt;u64&amp;gt; {
        self.pos = self.reader.seek(pos)?;
        Ok(self.pos)
    }
}

struct BufWriterWithPos&amp;lt;W: Write + Seek&amp;gt; {
    writer: BufWriter&amp;lt;W&amp;gt;,
    pos: u64,
}

impl&amp;lt;W: Write + Seek&amp;gt; BufWriterWithPos&amp;lt;W&amp;gt; {
    fn new(mut inner: W) -&amp;gt; Result&amp;lt;Self&amp;gt; {
        let pos = inner.seek(SeekFrom::Current(0))?;
        Ok(BufWriterWithPos {
            writer: BufWriter::new(inner),
            pos,
        })
    }
}

impl&amp;lt;W: Write + Seek&amp;gt; Write for BufWriterWithPos&amp;lt;W&amp;gt; {
    fn write(&amp;amp;mut self, buf: &amp;amp;[u8]) -&amp;gt; io::Result&amp;lt;usize&amp;gt; {
        let len = self.writer.write(buf)?;
        self.pos += len as u64;
        Ok(len)
    }

    fn flush(&amp;amp;mut self) -&amp;gt; io::Result&amp;lt;()&amp;gt; {
        self.writer.flush()
    }
}

impl&amp;lt;W: Write + Seek&amp;gt; Seek for BufWriterWithPos&amp;lt;W&amp;gt; {
    fn seek(&amp;amp;mut self, pos: SeekFrom) -&amp;gt; io::Result&amp;lt;u64&amp;gt; {
        self.pos = self.writer.seek(pos)?;
        Ok(self.pos)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use failure::Fail;
use std::io;

/// Error type for kvs.
#[derive(Fail, Debug)]
pub enum KvsError {
    /// IO error.
    #[fail(display = &quot;{}&quot;, _0)]
    Io(#[cause] io::Error),
    /// Serialization or deserialization error.
    #[fail(display = &quot;{}&quot;, _0)]
    Serde(#[cause] serde_json::Error),
    /// Removing non-existent key error.
    #[fail(display = &quot;Key not found&quot;)]
    KeyNotFound,
    /// Unexpected command type error.
    /// It indicated a corrupted log or a program bug.
    #[fail(display = &quot;Unexpected command type&quot;)]
    UnexpectedCommandType,
}

impl From&amp;lt;io::Error&amp;gt; for KvsError {
    fn from(err: io::Error) -&amp;gt; KvsError {
        KvsError::Io(err)
    }
}

impl From&amp;lt;serde_json::Error&amp;gt; for KvsError {
    fn from(err: serde_json::Error) -&amp;gt; KvsError {
        KvsError::Serde(err)
    }
}

/// Result type for kvs.
pub type Result&amp;lt;T&amp;gt; = std::result::Result&amp;lt;T, KvsError&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;干得漂亮，朋友，休息一下吧。&lt;/p&gt;
</content:encoded></item><item><title>PingCap的Rust训练课程1：熟悉Rust工具链</title><link>https://zlotus.github.io/2022/02/22/pna-rust-project-1/</link><guid isPermaLink="true">https://zlotus.github.io/2022/02/22/pna-rust-project-1/</guid><description>PingCap的Rust训练课程1：熟悉Rust工具链</description><pubDate>Tue, 22 Feb 2022 22:22:22 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文内容大多翻译自原文：&lt;a href=&quot;https://github.com/pingcap/talent-plan/tree/master/courses/rust/projects/project-1&quot;&gt;PNA Rust Project 1: The Rust toolbox&lt;/a&gt;。
:::&lt;/p&gt;
&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;任务&lt;/strong&gt;：在内存中创建一个能够接受命令行参数的键/值存储程序，且程序能够通过一些简单的测试。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;目标&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装Rust编译器和工具&lt;/li&gt;
&lt;li&gt;了解本课程中使用的项目结构&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;cargo init&lt;/code&gt;/&lt;code&gt;run&lt;/code&gt;/&lt;code&gt;test&lt;/code&gt;/&lt;code&gt;clippy&lt;/code&gt;/&lt;code&gt;fmt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;了解如何从crates.io查找、导入crates&lt;/li&gt;
&lt;li&gt;为键值存储程序定义恰当的数据类型&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;关键词&lt;/strong&gt;：测试、&lt;code&gt;clap&lt;/code&gt;crate、了解&lt;code&gt;CARGO_VERSION&lt;/code&gt;等值、熟悉&lt;code&gt;clippy&lt;/code&gt;和&lt;code&gt;rustfmt&lt;/code&gt;工具。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;扩展练习&lt;/strong&gt;：尝试使用&lt;code&gt;structopt&lt;/code&gt;crate。&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在这个项目中，您将在内存中创建一个简单的键/值存储，以将字符串映射到字符串，程序能够通过一些简单测试并能够响应命令行参数。项目的重点是熟悉典型Rust 项目的工具链和设置。&lt;/p&gt;
&lt;p&gt;即使这些听起来很基础，也请亲手尝试这个项目，因为它将介绍一些会用在整个课程中的通用模式。&lt;/p&gt;
&lt;h2&gt;项目需求规格&lt;/h2&gt;
&lt;p&gt;cargo项目&lt;code&gt;kvs&lt;/code&gt;构建了一个名为&lt;code&gt;kvs&lt;/code&gt;的命令行键值存储客户端，该客户端又调用了一个名为&lt;code&gt;kvs&lt;/code&gt;的库。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;kvs&lt;/code&gt;可执行文件支持以下命令行参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kvs set &amp;lt;KEY&amp;gt; &amp;lt;VALUE&amp;gt;&lt;/code&gt;
将字符串键设置为字符串值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kvs get &amp;lt;KEY&amp;gt;&lt;/code&gt;
获取给定字符串键的字符串值
-&lt;code&gt;kvs rm &amp;lt;KEY&amp;gt;&lt;/code&gt;
删除给定的键
-&lt;code&gt;kvs -V&lt;/code&gt;
打印版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;kvs&lt;/code&gt;库包含一个类型，&lt;code&gt;KvStore&lt;/code&gt;，它支持以下方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;KvStore::set(&amp;amp;mut self, key: String, value: String)&lt;/code&gt;
将字符串键设置为字符串值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvStore::get(&amp;amp;self, key: String) -&amp;gt; Option&amp;lt;String&amp;gt;&lt;/code&gt;
获取字符串键的字符串值。如果键不存在，返回&lt;code&gt;None&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;KvStore::remove(&amp;amp;mut self, key: String)&lt;/code&gt;
删除给定的键。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;KvStore&lt;/code&gt;类型将值存储在内存中，因此命令行客户端除了打印版本外，能做的并不多。从命令行运行&lt;code&gt;get&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt;/&lt;code&gt;rm&lt;/code&gt;命令时返回&quot;unimplemented&quot;错误。未来项目会将值存储在磁盘上，并实现相应的的命令。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;以您此时的Rust基础应当知道如何使用&lt;a href=&quot;https://www.rustup.rs/&quot;&gt;rustup&lt;/a&gt;安装Rust。&lt;/p&gt;
&lt;p&gt;如果不知道，运行下面的命令即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://sh.rustup.rs -sSf | sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（如果您使用的是Windows，请按照rustup.rs上的说明操作。请注意，您在本课程中将面临比其他人更多的挑战，因为它是在Unix上开发的。通常，在Windows上的Rust开发体验不如在Unix上那么顺滑）。&lt;/p&gt;
&lt;p&gt;通过键入&lt;code&gt;rustc -V&lt;/code&gt;来验证工具链是否正确安装。如果这不起作用，请注销当前shell的会话并重新登录，以使得安装时的shell profile配置变动可以生效。&lt;/p&gt;
&lt;h2&gt;项目设置&lt;/h2&gt;
&lt;p&gt;您将使用Cargo在git仓库完成本项目。您将从&lt;a href=&quot;https://github.com/pingcap/talent-plan&quot;&gt;本课程的源仓库&lt;/a&gt;中导入项目的测试用例。&lt;/p&gt;
&lt;p&gt;请注意，在源仓库中，与本课程相关的所有内容都在&lt;code&gt;rust&lt;/code&gt;子目录中，您可以忽略其他目录。&lt;/p&gt;
&lt;p&gt;本课程中的所有项目均包含库和可执行文件。使用可执行文件的形式，是因为我们正在开发一个可以运行的应用程序。使用库的形式，是因为提供的测试用例必须链接到这些库。&lt;/p&gt;
&lt;p&gt;在本课程中，我们将为每个项目使用相同的设置。&lt;/p&gt;
&lt;p&gt;我们将使用的目录结构为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;├── Cargo.toml
├── src
│   ├── bin
│   │   └── kvs.rs
│   └── lib.rs
└── tests
    └── tests.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;Cargo.toml&lt;/code&gt;、&lt;code&gt;lib.rs&lt;/code&gt;和&lt;code&gt;kvs.rs&lt;/code&gt;文件内容如下：&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[package]
name = &quot;kvs&quot;
version = &quot;0.1.0&quot;
authors = [&quot;Brian Anderson &amp;lt;andersrb@gmail.com&amp;gt;&quot;]
description = &quot;A key-value store&quot;
edition = &quot;2018&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;lib.rs&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// just leave it empty for now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;kvs.rs&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    println!(&quot;Hello, world!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中author应该是您的名字，而name必须是&lt;code&gt;kvs&lt;/code&gt;，这是项目名称同时也是库的名称，以使测试用例起作用。同样，二进制文件（命令行应用程序）的名称必须是&lt;code&gt;kvs&lt;/code&gt;。在上面的设置中，项目会因文件名隐式地叫做&lt;code&gt;kvs&lt;/code&gt;，当然您也可以随意命名文件，只要将适当的信息写入配置文件（&lt;code&gt;Cargo.toml&lt;/code&gt;）中即可。&lt;/p&gt;
&lt;p&gt;您可以使用&lt;code&gt;cargo new --lib&lt;/code&gt;直接新建项目；或使用&lt;code&gt;cargo init --lib&lt;/code&gt;在一个空目录中初始化项目；或手动设置这个项目：您可能还需要在同一目录中初始化一个git仓库。&lt;/p&gt;
&lt;p&gt;最后，从课程资料中复制&lt;code&gt;tests&lt;/code&gt;目录：即将文件&lt;code&gt;rust/projects/project-1/tests&lt;/code&gt;从课程仓库复制到您自己的仓库中，也叫做&lt;code&gt;tests&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此时您应该可以使用&lt;code&gt;cargo run&lt;/code&gt;运行程序。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;现在就试试。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;您已经为这个项目做好了准备，开始编写业务代码吧。&lt;/p&gt;
&lt;h2&gt;第1部分：编译测试代码&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tests/tests.rs&lt;/code&gt;中为您提供了一套单元测试。 打开它看看。&lt;/p&gt;
&lt;p&gt;*尝试使用&lt;code&gt;cargo test&lt;/code&gt;运行测试。*发生了什么？为什么？&lt;/p&gt;
&lt;p&gt;本项目的第一个任务是让测试模块能够通过编译。&lt;/p&gt;
&lt;p&gt;如果你的项目和我的一样，你应该会看到大量编译错误，我们来看看前几条。通常当你看到一堆错误时，第一条是最重要的 — &lt;code&gt;rustc&lt;/code&gt;会在遇到错误后尝试继续编译，因此后面的可能是级联错误，其意义远低于第一条。您的前几条错误可能如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0433]: failed to resolve: use of undeclared type or module `assert_cmd`
 --&amp;gt; tests/tests.rs:1:5
  |
1 | use assert_cmd::prelude::*;
  |     ^^^^^^^^^^ use of undeclared type or module `assert_cmd`

error[E0432]: unresolved import
 --&amp;gt; tests/tests.rs:3:5
  |
3 | use predicates::str::contains;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（如果您看到其他错误，请反馈issue）。&lt;/p&gt;
&lt;p&gt;对于一个新的Rust程序员来说，这两个错误很难诊断出来，所以我只会告诉你这里发生了什么：项目配置文件中缺少&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#development-dependencies&quot;&gt;开发依赖项&lt;/a&gt;的crates。&lt;/p&gt;
&lt;p&gt;对于这个项目，你的&lt;code&gt;Cargo.toml&lt;/code&gt;文件需要包含这些行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dev-dependencies]
assert_cmd = &quot;0.11.0&quot;
predicates = &quot;1.0.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;了解这些依赖项的详情，对您完成本项目并不重要，您可以自己去查找此类信息。我们之前没有告诉您需要开发依赖项，是为了让您自己会遇到这些错误。在未来的项目中，会在项目配置部分告诉您后面所需的开发依赖。&lt;/p&gt;
&lt;p&gt;简要说明：您如何确定这些错误是由于项目配置中缺少依赖，项而不是由于源码中的错误造成的？以前面显示的错误为例，一个重要线索：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1 | use assert_cmd::prelude::*;
  |     ^^^^^^^^^^ use of undeclared type or module `assert_cmd`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;use&lt;/code&gt;语句中，路径中的第一个元素始终是crate的名称。不过，当第一个路径元素引用的，是之前由&lt;em&gt;别的&lt;/em&gt;&lt;code&gt;use&lt;/code&gt;语句引入上下文的名称时，会出现例外情况。换句话说，如果这个文件中有另一个&lt;code&gt;use&lt;/code&gt;语句，例如&lt;code&gt;use foo::assert_cmd&lt;/code&gt;，那么&lt;code&gt;use assert_cmd::prelude::*&lt;/code&gt;中引用的就是&lt;em&gt;之前那个&lt;/em&gt;&lt;code&gt;assert_cmd&lt;/code&gt;。关于这一点还有很多内容，我们就不在这里探讨这些技术细节了。只需要记住，通常在&lt;code&gt;use&lt;/code&gt;语句中，如果找不到路径中的第一个元素（即无法解析），问题可能是配置文件中没有声明那个crate。&lt;/p&gt;
&lt;p&gt;这是我们在第一个项目中遇到的第一个问题。希望能有所启发。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;继续，并将适当的开发依赖项添加到您的配置文件中。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;再次尝试使用&lt;code&gt;cargo test&lt;/code&gt;运行测试。发生什么了？为什么？&lt;/p&gt;
&lt;p&gt;此时&lt;em&gt;前面的那些&lt;/em&gt;错误应该已经消失了。现在的错误应该都是关于测试用例无法在您自己的代码中找到它期望的代码。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;所以现在您的任务是：声明测试模块编译所需的类型、方法等。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在本课程中，您将大量阅读测试用例。测试用例将准确地告诉您对代码的期望。如果书本和实验不一致，则实验为真（纠正错误！）。在编程中也是如此。测试用例展示了软件应有的行为为。测试是现实，应习惯阅读测试用例。&lt;/p&gt;
&lt;p&gt;此外，测试用例通常是任何项目中编写质量最差的代码，原始且没有文档注释。&lt;/p&gt;
&lt;p&gt;再次尝试使用&lt;code&gt;cargo test&lt;/code&gt;运行测试。发生什么了？为什么？&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;src/lib.rs&lt;/code&gt;中编写能够让&lt;code&gt;cargo test --no-run&lt;/code&gt;通过的类和方法签名。暂时不要编写任何函数体—使用&lt;code&gt;panic!()&lt;/code&gt;宏。这是在不知道或不关心实现的情况下描述API的方法（此外还有宏&lt;a href=&quot;https://doc.rust-lang.org/std/macro.unimplemented.html&quot;&gt;&lt;code&gt;unimplemented!&lt;/code&gt;&lt;/a&gt;，但由于这个单词有点长，因此通常简单地使用&lt;code&gt;panic!&lt;/code&gt;即可。不过，如果您正在发布的软件包含未实现的方法的程序时，则应当使用&lt;code&gt;unimplemented!&lt;/code&gt;宏）。&lt;/p&gt;
&lt;p&gt;完成上述要求。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我的作业：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct KvStore {

}

impl KvStore {
    pub fn new() -&amp;gt; Self {
        panic!()
    }

    pub fn get(&amp;amp;self, k: String) -&amp;gt; Option&amp;lt;String&amp;gt; {
        panic!()
    }

    pub fn set(&amp;amp;mut self, k: String, v: String) {
        panic!()
    }

    pub fn remove(&amp;amp;mut self, k: String) {
        panic!()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;之后，如果您运行&lt;code&gt;cargo test&lt;/code&gt;（不带&lt;code&gt;--no-run&lt;/code&gt;），您应该会看到一些测试失败输出，例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    Finished dev [unoptimized + debuginfo] target(s) in 2.32s
     Running target/debug/deps/kvs-b03a01e7008067f6

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/kvs-a3b5a004932c6715

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/tests-5e1c2e20bd1fa377

running 13 tests
test cli_get ... FAILED
test cli_invalid_get ... FAILED
test cli_invalid_rm ... FAILED
test cli_invalid_set ... FAILED
test cli_no_args ... FAILED
test cli_invalid_subcommand ... FAILED
... more lines of spew ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...后还有很多行。太棒了！这结果正是我们现在所需要的。您会在本项目后面的内容中编写让这些测试通过的代码。&lt;/p&gt;
&lt;h3&gt;测试技巧&lt;/h3&gt;
&lt;p&gt;如果你再次查看&lt;code&gt;cargo test&lt;/code&gt;的输出，就会看到一些有趣的东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     Running target/debug/deps/kvs-b03a01e7008067f6

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/kvs-a3b5a004932c6715

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/tests-5e1c2e20bd1fa377


running 13 tests
test cli_get ... FAILED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cargo输出了三遍&quot;Running ...&quot;。实际上前两次并没有运行任何测试。那么如果这三批测试都没有失败，cargo将运行&lt;em&gt;另一组&lt;/em&gt;测试。&lt;/p&gt;
&lt;p&gt;为什么会这样？&lt;/p&gt;
&lt;p&gt;这是因为在Rust中有很多可以用来编写测试的地方：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在库的源码中&lt;/li&gt;
&lt;li&gt;在每个二进制的源码中&lt;/li&gt;
&lt;li&gt;在每个测试的源码中&lt;/li&gt;
&lt;li&gt;在库源码的注释文档中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其实cargo并不知道上面这四条里哪些确实包含了测试，cargo只是构建并运行这些测试。&lt;/p&gt;
&lt;p&gt;所以这里有两组空测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     Running target/debug/deps/kvs-b03a01e7008067f6
running 0 tests
     Running target/debug/deps/kvs-a3b5a004932c6715
running 0 tests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过，这里可能不容易理解：这其中一个是您的库，编译后测试，另一个是您的二进制，编译后测试，目前两者都不包含任何测试。名称中都有&quot;kvs&quot;的原因是因为您的库和二进制文件都称为&quot;kvs&quot;。&lt;/p&gt;
&lt;p&gt;这些测试的输出可能有点烦人，有两种方法可以让cargo安静下来：使用命令行参数，或更改项目配置：&lt;/p&gt;
&lt;p&gt;以下是相关的命令行标识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo test --lib&lt;/code&gt; — 只测试库中的测试&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo test --doc&lt;/code&gt; — 测试库中的文档测试&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo test --bins&lt;/code&gt; — 测试项目中的所有二进制&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo test --bin foo&lt;/code&gt; — 只测试&lt;code&gt;foo&lt;/code&gt;的二进制&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cargo test --test foo&lt;/code&gt; — 测试 测试文件&lt;code&gt;foo&lt;/code&gt; 中的测试&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上命令可以方便快速地隐藏测试输出，但如果项目不包含某种类型的测试，就最好不去处理测试。回想一下The Cargo Book中关于&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/manifest.html&quot;&gt;manifest description&lt;/a&gt;的描述，可知有两个配置选项可用：&lt;code&gt;test = false&lt;/code&gt;和&lt;code&gt;doctest = false&lt;/code&gt;。它们位于[lib]和[[bin]]部分，因此可以考虑更新您的项目配置。&lt;/p&gt;
&lt;p&gt;如果以前没用过可以尝试下面这个命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo test -- --help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;试试吧，输出很有趣。您在此处看到的是包含已编译测试的可执行文件的帮助信息（由空格包围的&lt;code&gt;--&lt;/code&gt;将告诉cargo将之后的所有参数传递给测试二进制文件）。这与运行&lt;code&gt;cargo test --help&lt;/code&gt;时显示的信息完全不同。它们是两个不同的命令：cargo在运行您的二进制测试时会将这些参数传递给测试程序。&lt;/p&gt;
&lt;p&gt;您也可以通过命令执行同样的操作。让我们再回到的&lt;code&gt;cargo test&lt;/code&gt;示例。我们看到了这一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     Running target/debug/deps/kvs-b03a01e7008067f6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是cargo告诉您测试二进制的文件名。您可以自己运行它，比如&lt;code&gt;target/debug/deps/kvs-b03a01e7008067f6 --help&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;target&lt;/code&gt;目录里有很多有趣的东西。深入研究可以教会您很多关于Rust工具链工作细节的知识。&lt;/p&gt;
&lt;p&gt;在实践中，特别是对于大型项目，您不会在开发单个功能时运行整个测试套件。要将测试集缩小到我们关心的某个测试，请运行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo test cli_no_args
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将运行名为&lt;code&gt;cli_no_args&lt;/code&gt;的测试。事实上，它会运行名称中包含&lt;code&gt;cli_no_args&lt;/code&gt;的任何测试，因此，例如，如果你想运行所有CLI测试，你可以运行&lt;code&gt;cargo test cli&lt;/code&gt;。以上可能就是您在完成项目时手动运行测试的各种方式，否则您将被许多尚未修复的失败测试分心。不幸的是，该模式只是一个简单的字符串匹配，而不是像正则表达式那样功能强大的东西。&lt;/p&gt;
&lt;p&gt;请注意，在撰写本文时，本课程中项目测试用例，并未按照能够明确指出某测试专为项目的某部分而编写的方式进行组织 - 只是到最后整个测试套件应该通过即可。您需要自己阅读测试的名称和实现，来确定&lt;em&gt;您认为&lt;/em&gt;在哪些阶段应该通过哪些测试。&lt;/p&gt;
&lt;h2&gt;第2部分：接受命令行参数&lt;/h2&gt;
&lt;p&gt;本课程中的键/值存取都是通过命令行客户端执行的。在这个项目中，命令行客户端非常简单，因为键值存储的状态只存储在内存中，而不是持久化到磁盘。&lt;/p&gt;
&lt;p&gt;通过这一部分，您应使名为&lt;code&gt;cli_*&lt;/code&gt;的测试用例通过。&lt;/p&gt;
&lt;p&gt;回忆上一部分中介绍的关于如何运行单个测试用例的内容。&lt;/p&gt;
&lt;p&gt;重申一下CLI的接口是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kvs set &amp;lt;KEY&amp;gt; &amp;lt;VALUE&amp;gt;&lt;/code&gt;
将字符串键设置为字符串值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kvs get &amp;lt;KEY&amp;gt;&lt;/code&gt;
获取给定字符串键的字符串值
-&lt;code&gt;kvs rm &amp;lt;KEY&amp;gt;&lt;/code&gt;
删除给定的键
-&lt;code&gt;kvs -V&lt;/code&gt;
打印版本&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但在本次迭代中，&lt;code&gt;get&lt;/code&gt;和&lt;code&gt;set&lt;/code&gt;命令将从stderr输出字符串&quot;unimplemented&quot;，并以非零退出码退出，表示错误。&lt;/p&gt;
&lt;p&gt;您可能需要使用&lt;code&gt;clap&lt;/code&gt;crate来处理命令行参数。&lt;/p&gt;
&lt;p&gt;*找到最新版本的&lt;code&gt;clap&lt;/code&gt;crate 并将其添加到&lt;code&gt;Cargo.toml&lt;/code&gt;的依赖项中。*有多种方法可以搜索或导入crate，我们推荐：查看内置的&lt;a href=&quot;https://doc.rust-lang.org/cargo/commands/cargo-search.html&quot;&gt;&lt;code&gt;cargo search&lt;/code&gt;&lt;/a&gt;和插件&lt;a href=&quot;https://github.com/killercup/cargo-edit&quot;&gt;&lt;code&gt;cargo edit&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;接下来使用&lt;a href=&quot;https://crates.io/&quot;&gt;crates.io&lt;/a&gt;、&lt;a href=&quot;https://lib.rs/&quot;&gt;lib.rs&lt;/a&gt;或&lt;a href=&quot;https://docs.rs/&quot;&gt;docs.rs&lt;/a&gt;查找&lt;code&gt;clap&lt;/code&gt;crate 的文档，并实现命令行界面以使名为&lt;code&gt;cli_*&lt;/code&gt;的测试用例通过。&lt;/p&gt;
&lt;p&gt;测试时，使用&lt;code&gt;cargo run&lt;/code&gt;；不要直接从&lt;code&gt;target/&lt;/code&gt;目录运行可执行文件。需要将参数传递给程序时，用两个破折号将它们与&lt;code&gt;cargo run&lt;/code&gt;命令分开，&lt;code&gt;--&lt;/code&gt;，如&lt;code&gt;cargo run -- get key1&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我的作业&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use clap::{Parser, Subcommand};
use std::process::exit;

#[derive(Parser)]
#[clap(name = &quot;kvs&quot;)]
#[clap(author, version, about=&quot;an in-memory key/value store&quot;, long_about = None)]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    // Set the value of a string key to a string
    #[clap(arg_required_else_help = true)]
    set {
        // the key of the value to be set
        key: String,
        // the value of the key to be set
        value: String,
    }, 

    // Get the string value of a given string key
    #[clap(arg_required_else_help = true)]
    get {
        // the key wish to query
        key: String,
    }, 

    // Remove a given key
    #[clap(arg_required_else_help = true)]
    rm {
        // the key wish to remove
        key: String,
    },
}

fn main() {
    let cli= Cli::parse();
    match &amp;amp;cli.command {
        Commands::get { key } =&amp;gt; {
            eprintln!(&quot;unimplemented&quot;);
            exit(1);
        }, 
        Commands::set { key, value } =&amp;gt; {
            eprintln!(&quot;unimplemented&quot;);
            exit(1);
        }
        Commands::rm { key } =&amp;gt; {
            eprintln!(&quot;unimplemented&quot;);
            exit(1);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;第3部分：cargo环境变量&lt;/h2&gt;
&lt;p&gt;当您设置&lt;code&gt;clap&lt;/code&gt;来解析命令行参数时，您可能会设置名称、版本、作者和描述（如果没有，请这样做）。这些是&lt;code&gt;Cargo.toml&lt;/code&gt;提供的冗余信息，而这些Cargo在构建时设置的环境变量可以被Rust源码访问。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;修改您的&lt;code&gt;clap&lt;/code&gt;设置以从cargo标准环境变量中设置这些值。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;第4部分：将值存储在内存中&lt;/h2&gt;
&lt;p&gt;您的命令行程序已经有了雏形，现在让我们实现&lt;code&gt;KvStore&lt;/code&gt;，以使得其余的测试用例通过。&lt;/p&gt;
&lt;p&gt;通过阅读测试用例即可了解&lt;code&gt;KvStore&lt;/code&gt;方法的行为 — 您不需要任何进一步的描述来完成这个项目的代码。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;通过在&lt;code&gt;KvStore&lt;/code&gt;上实现各方法使其余的测试用例通过。&lt;/em&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我的作业：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::collections::HashMap;

pub struct KvStore {
    db: HashMap&amp;lt;String, String&amp;gt;,
}

impl KvStore {
    pub fn new() -&amp;gt; Self {
        KvStore { db: HashMap::new() }
    }

    pub fn get(&amp;amp;mut self, k: String) -&amp;gt; Option&amp;lt;String&amp;gt; {
        self.db.remove(&amp;amp;k)
    }

    pub fn set(&amp;amp;mut self, k: String, v: String) {
        self.db.insert(k, v);
    }

    pub fn remove(&amp;amp;mut self, k: String) {
        self.db.remove(&amp;amp;k);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;第5部分：文档&lt;/h2&gt;
&lt;p&gt;您已经实现了项目的功能，但是在它成为一个优秀的Rust软件之前我们还需要做一些事情，以准备好贡献或发布。&lt;/p&gt;
&lt;p&gt;首先，公共项目通常应该有文档注释。&lt;/p&gt;
&lt;p&gt;文档注释显示在crate的API文档中。使用命令&lt;code&gt;cargo doc&lt;/code&gt;生成API文档，这可以将文档注释渲染为HTML并放在&lt;code&gt;target/doc&lt;/code&gt;文件夹中。请注意，尽管&lt;code&gt;target/doc&lt;/code&gt;文件夹不包含&lt;code&gt;index.html&lt;/code&gt;。在本项目中，您的crate文档将位于&lt;code&gt;target/doc/kvs/index.html&lt;/code&gt;。您可以使用&lt;code&gt;cargo doc --open&lt;/code&gt;在该位置启动Web浏览器。&lt;code&gt;cargo doc --open&lt;/code&gt;并不总是有效，比如，如果您通过&lt;code&gt;ssh&lt;/code&gt;连接到云实例就无法打开。不过就算打不开浏览器，该命令仍将打印它无法打开的html的文件名 — 这会方便我们查找API文档的位置。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://rust-lang-nursery.github.io/api-guidelines/documentation.html&quot;&gt;好的文档注释&lt;/a&gt;并不是是重复函数名，也不是重复从类型签名中收集到的信息。好的注释应该解释为什么以及如何使用函数，成功和失败时的返回值是什么，什么条件会触发error和panic。您编写的库非常简单，因此文档也会很简单。如果您真的想不出通过文档注释添加任何有用的内容，那么可以不添加文档注释（这是一个偏好问题）。在没有文档注释的情况下，仅从名称和类型签名就应该清楚如何使用类型或函数。&lt;/p&gt;
&lt;p&gt;文档注释可以包含示例，而这些示例可以使用&lt;code&gt;cargo test --doc&lt;/code&gt;进行测试。&lt;/p&gt;
&lt;p&gt;将&lt;code&gt;#![deny(missing_docs)]&lt;/code&gt;添加到&lt;code&gt;src/lib.rs&lt;/code&gt;的顶部以强制所有公共项都具有文档注释。然后向您已经实现的类型和方法中添加文档注释，编写注释时请遵循&lt;a href=&quot;https://rust-lang-nursery.github.io/api-guidelines/documentation.html&quot;&gt;文档指南&lt;/a&gt;。给每个函数一个例子，并确保他们通过&lt;code&gt;cargo test --doc&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我的作业：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![deny(missing_docs)]

//!
//! An in-memory key/value store.
//! Use hashmap to implement the get, set, remove behavior
//! 

use std::collections::HashMap;

/// `KvStore` structure contain a Hashmap as member `db` for manipulate key/values in memory.
pub struct KvStore {
    db: HashMap&amp;lt;String, String&amp;gt;,
}

impl KvStore {

    /// create a `KvStore`, initial an empty HashMap for member `db`.
    pub fn new() -&amp;gt; Self {
        KvStore { db: HashMap::new() }
    }

    /// Query HashMap in `KvStore` using key `key`.
    /// If the map did have this key present, get the corresponding `value` and return `Some(value)`. 
    /// If the map did not have this key present, return `None`.
    /// 
    /// # Example
    /// 
    /// ```rust
    /// use kvs::KvStore;
    /// 
    /// let mut store = KvStore::new();
    /// store.set(&quot;key1&quot;.to_owned(), &quot;value1&quot;.to_owned());
    /// assert_eq!(store.get(&quot;key1&quot;.to_owned()), Some(&quot;value1&quot;.to_owned()));
    /// assert_eq!(store.get(&quot;key2&quot;.to_owned()), None);
    /// ```
    pub fn get(&amp;amp;self, key: String) -&amp;gt; Option&amp;lt;String&amp;gt; {
        match self.db.get(&amp;amp;key) {
            Some(value) =&amp;gt; Some(value.to_string()),
            None =&amp;gt; None
        }
    }
    /// Update HashMap in `KvStore` using key/value pair `key` and `value`.
    /// If the map did not have this key present, insert `key` with `value`.
    /// If the map did have this key present, the value is updated.
    /// 
    /// # Example
    /// 
    /// ```rust
    /// use kvs::KvStore;
    /// 
    /// let mut store = KvStore::new();
    /// 
    /// store.set(&quot;key1&quot;.to_owned(), &quot;value1&quot;.to_owned());
    /// assert_eq!(store.get(&quot;key1&quot;.to_owned()), Some(&quot;value1&quot;.to_owned()));
    /// store.set(&quot;key1&quot;.to_owned(), &quot;value2&quot;.to_owned());
    /// assert_eq!(store.get(&quot;key1&quot;.to_owned()), Some(&quot;value2&quot;.to_owned()));
    /// ```
    pub fn set(&amp;amp;mut self, key: String, value: String) {
        self.db.insert(key, value);
    }

    /// Removes the key `key` from HashMap in `KvStore`.
    /// 
    /// # Example
    /// 
    /// ```rust
    /// use kvs::KvStore;
    /// let mut store = KvStore::new();
    /// 
    /// store.set(&quot;key1&quot;.to_owned(), &quot;value1&quot;.to_owned());
    /// store.remove(&quot;key1&quot;.to_owned());
    /// assert_eq!(store.get(&quot;key1&quot;.to_owned()), None);
    /// ```
    pub fn remove(&amp;amp;mut self, key: String) {
        self.db.remove(&amp;amp;key);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;第6部分：使用&lt;code&gt;clippy&lt;/code&gt;和&lt;code&gt;rustfmt&lt;/code&gt;规范代码&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/rust-lang/rust-clippy&quot;&gt;&lt;code&gt;clippy&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://github.com/rust-lang/rustfmt&quot;&gt;&lt;code&gt;rustfmt&lt;/code&gt;&lt;/a&gt;是规范Rust代码的工具。&lt;code&gt;clippy&lt;/code&gt;有助于确保代码风格符合规范，也能够检查出一些可能产生错误的编程模式。&lt;code&gt;rustfmt&lt;/code&gt;会强制代码格式保持一致。在您有空的时候可以单击这些链接并阅读它们的文档。这些都是较为复杂的工具，功能远不止文中所述。&lt;/p&gt;
&lt;p&gt;这两个工具都包含在Rust工具链中，但默认情况下不安装。可以使用以下&lt;code&gt;rustup&lt;/code&gt;命令进行安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rustup component add clippy
rustup component add rustfmt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在就试试吧。&lt;/p&gt;
&lt;p&gt;这两个工具均以cargo子命令的形式调用，&lt;code&gt;clippy&lt;/code&gt;使用&lt;code&gt;cargo clippy&lt;/code&gt;、&lt;code&gt;rustfmt&lt;/code&gt;使用&lt;code&gt;cargo fmt&lt;/code&gt;调用。请注意，&lt;code&gt;cargo fmt&lt;/code&gt;会修改您的源代码，因此请在运行之前提交您的工作，以避免产生意料之外的非必要修改，之后您可以再使用&lt;code&gt;git commit --amend&lt;/code&gt;将更改作为先前提交的一部分包含在内。或者只是将它们作为自己的格式化提交 — 对于Rust来说，在一系列提交之后进行&lt;code&gt;clippy&lt;/code&gt;和&lt;code&gt;rustfmt&lt;/code&gt;是很常见的，比如在提交一个pull request前。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;在您的项目上执行&lt;code&gt;cargo clippy&lt;/code&gt;并按照建议修改代码。在您的项目上执行&lt;code&gt;cargo fmt&lt;/code&gt;并提交所有更改。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;建议阅读&lt;a href=&quot;https://github.com/rust-lang/rustup.rs/blob/master/README.md&quot;&gt;&lt;code&gt;rustup&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://github.com/rust-lang/rust-clippy&quot;&gt;&lt;code&gt;clippy&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://github.com/rust-lang/rustfmt&quot;&gt;&lt;code&gt;rustfmt&lt;/code&gt;&lt;/a&gt;文档，因为这些是您经常会用到的工具。&lt;/p&gt;
&lt;p&gt;恭喜，您完成了你的第一个项目！如果您愿意，还可以试着完成下面的扩展作业，扩展作业是可选的。&lt;/p&gt;
&lt;p&gt;此外，还可以使用下面的命令探索Rust工具链：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rustup component list
rustup component list | rg -v std # ripgrep，一个用rust编写的grep替代，`-v`即反向匹配
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;干得漂亮，同学，休息一下吧。&lt;/p&gt;
&lt;h2&gt;扩展1：&lt;code&gt;structopt&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;在这个项目中，我们使用&lt;code&gt;clap&lt;/code&gt;来解析命令行参数。通常将程序已解析的命令行参数表示为一个结构体，比如叫做&lt;code&gt;Config&lt;/code&gt;或&lt;code&gt;Options&lt;/code&gt;。这就需要调用&lt;code&gt;clap&lt;/code&gt;的 &lt;code&gt;ArgMatches&lt;/code&gt;类型上的相应方法。对于较大的程序，这两个步骤都需要大量模板化的代码。&lt;code&gt;structopt&lt;/code&gt;crate能够让您定义一个可以自动生成并注释&lt;code&gt;clap&lt;/code&gt;命令行解释器的&lt;code&gt;Config&lt;/code&gt;结构体，来大幅减少这些模板化代码。一些人发现使用这种方式，比显式编写&lt;code&gt;clap&lt;/code&gt;代码更方便。&lt;/p&gt;
&lt;p&gt;修改您的程序以使用&lt;code&gt;structopt&lt;/code&gt;来解析命令行参数，而不是直接使用&lt;code&gt;clap&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;我的作业：&lt;/p&gt;
&lt;p&gt;由于我在2022年3月初完成的作业，所以在我的&lt;code&gt;Cargo.toml&lt;/code&gt;中声明的&lt;code&gt;clap&lt;/code&gt;依赖已经具备了&lt;code&gt;derive&lt;/code&gt;这一新特性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clap = { version = &quot;3.1.5&quot;, features = [&quot;derive&quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而&lt;code&gt;derive&lt;/code&gt;特性就是装饰在某结构体上，通过宏给该结构体隐式实现各种功能。因此不再需要编写&lt;code&gt;ArgMatches&lt;/code&gt;的相应方法，也不再需要使用&lt;code&gt;structopt&lt;/code&gt;简化实现了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use clap::{Parser, Subcommand};
use std::process::exit;

#[derive(Parser)]
//...
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
//...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;通过测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests (target/debug/deps/kvs-8750b5b96d05456c)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/tests.rs (target/debug/deps/tests-2241116061e67a21)

running 13 tests
test cli_get ... ok
test cli_invalid_subcommand ... ok
test cli_rm ... ok
test cli_set ... ok
test cli_no_args ... ok
test get_non_existent_value ... ok
test get_stored_value ... ok
test remove_key ... ok
test overwrite_value ... ok
test cli_version ... ok
test cli_invalid_get ... ok
test cli_invalid_rm ... ok
test cli_invalid_set ... ok

test result: ok. 13 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.35s

   Doc-tests kvs

running 3 tests
test src/lib.rs - KvStore::set (line 46) ... ok
test src/lib.rs - KvStore::get (line 28) ... ok
test src/lib.rs - KvStore::remove (line 64) ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.18s
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用rust编写高质量命令行程序</title><link>https://zlotus.github.io/2022/02/10/rust-write-a-good-cli-program/</link><guid isPermaLink="true">https://zlotus.github.io/2022/02/10/rust-write-a-good-cli-program/</guid><description>使用rust编写高质量命令行程序</description><pubDate>Thu, 10 Feb 2022 18:12:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文内容大多翻译自原文：&lt;a href=&quot;https://qiita.com/tigercosmos/items/678f39b1209e60843cc3&quot;&gt;Write a Good CLI Program&lt;/a&gt;。
:::&lt;/p&gt;
&lt;h1&gt;前言&lt;/h1&gt;
&lt;p&gt;最近打算干点人事，继续入门rust。开始看pingcap提供的rust教程，&lt;a href=&quot;https://github.com/pingcap/talent-plan/blob/master/courses/rust/building-blocks/bb-1.md&quot;&gt;Building Blocks 1&lt;/a&gt;引用了一篇文章写的挺好，翻译一下以备查阅。&lt;/p&gt;
&lt;h1&gt;使用rust编写高质量命令行程序&lt;/h1&gt;
&lt;p&gt;命令行界面（CLI）程序在终端上运行，这意味着没有图形界面（GUI）。&lt;/p&gt;
&lt;p&gt;其实我们每天都在使用CLI，比如&lt;code&gt;ls&lt;/code&gt;、&lt;code&gt;ps&lt;/code&gt;、&lt;code&gt;top&lt;/code&gt; 等。还有一个&lt;a href=&quot;https://github.com/agarrharr/awesome-cli-apps&quot;&gt;awesome-cli-apps&lt;/a&gt;收集了很多优秀的CLI程序，值得一看。我推荐&lt;a href=&quot;https://github.com/ogham/exa&quot;&gt;&lt;code&gt;exa&lt;/code&gt;&lt;/a&gt;，用rust编写的现代版&lt;code&gt;ls&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./rust-write-a-good-cli-program/rust-write-a-good-cli-program-0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;命令行程序&lt;/h2&gt;
&lt;p&gt;通常，命令行程序看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./program_name [arguments] [flags] [options]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一般我们会使用&lt;code&gt;-h&lt;/code&gt;或&lt;code&gt;--help&lt;/code&gt;命令以查看该命令的帮助信息。&lt;/p&gt;
&lt;p&gt;以&lt;code&gt;cargo&lt;/code&gt;为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cargo -h
Rust&apos;s package manager

USAGE:
    cargo [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -V, --version           Print version info and exit
        --list              List installed commands
        --explain &amp;lt;CODE&amp;gt;    Run `rustc --explain CODE`
    -v, --verbose           Use verbose output (-vv very verbose/build.rs output)
    -q, --quiet             No output printed to stdout
        --color &amp;lt;WHEN&amp;gt;      Coloring: auto, always, never
        --frozen            Require Cargo.lock and cache are up to date
        --locked            Require Cargo.lock is up to date
    -Z &amp;lt;FLAG&amp;gt;...            Unstable (nightly-only) flags to Cargo, see &apos;cargo -Z help&apos; for details
    -h, --help              Prints help information

Some common cargo commands are (see all commands with --list):
    build       Compile the current project
    check       Analyze the current project and report errors, but don&apos;t build object files
    clean       Remove the target directory
    doc         Build this project&apos;s and its dependencies&apos; documentation
    new         Create a new cargo project
    init        Create a new cargo project in an existing directory
    run         Build and execute src/main.rs
    test        Run the tests
    bench       Run the benchmarks
    update      Update dependencies listed in Cargo.lock
    search      Search registry for crates
    publish     Package and upload this project to the registry
    install     Install a Rust binary
    uninstall   Uninstall a Rust binary

See &apos;cargo help &amp;lt;command&amp;gt;&apos; for more information on a specific command.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一目了然，如此我们就知道如何使用&lt;code&gt;cargo&lt;/code&gt;了。&lt;/p&gt;
&lt;h2&gt;创建项目&lt;/h2&gt;
&lt;p&gt;让我们开始构建一个新的命令行程序吧！&lt;/p&gt;
&lt;p&gt;我在这里将项目命名为&lt;code&gt;meow&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cargo new meow
$ cd meow
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;参数&lt;/h2&gt;
&lt;p&gt;正如上面看到CLI的样子，CLI应有一些参数。&lt;/p&gt;
&lt;p&gt;添加参数最简单的方法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// main.rs
use std::env;

fn main() {
    let args: Vec&amp;lt;String&amp;gt; = env::args().collect();
    println!(&quot;{:?}&quot;, args);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$./meow a1 a2 a3
[&quot;meow&quot;, &quot;a1&quot;, &quot;a2&quot;, &quot;a3&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如此，程序即可打印参数。&lt;/p&gt;
&lt;p&gt;不过，我摸实际使用到的CLI程序更会加复杂：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ./foo -g -e a1 a3 a4
$ ./foo a1 -e -l --path=~/test/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可见这种简单的实现使用起来很不方便，因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;参数可能有默认值&lt;/li&gt;
&lt;li&gt;标识会交换位置&lt;/li&gt;
&lt;li&gt;选项会交换位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arg1&lt;/code&gt;可能会绑定&lt;code&gt;arg2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，需要一个库来帮助我们轻松完成这项工作。&lt;/p&gt;
&lt;h2&gt;Clap&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Clap&lt;/code&gt;是一个全面、高效的Rust命令行参数解析器，用法如下：&lt;/p&gt;
&lt;p&gt;:::caution[YAML接口已废弃]
参见&lt;a href=&quot;https://github.com/clap-rs/clap/issues/3087&quot;&gt;Deprecate Yaml API #3087&lt;/a&gt;，现行的clap版本已废弃YAML API，下面的例子需要使用2版本进行编译：
:::&lt;/p&gt;
&lt;p&gt;首先，创建一个&lt;code&gt;cli.yml&lt;/code&gt;文件来设置参数。看起来像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;name: myapp
version: &quot;1.0&quot;
author: Kevin K. &amp;lt;kbknapp@gmail.com&amp;gt;
about: Does awesome things
args:
    - config:
        short: c
        long: config
        value_name: FILE
        help: Sets a custom config file
        takes_value: true
    - INPUT:
        help: Sets the input file to use
        required: true
        index: 1
    - verbose:
        short: v
        multiple: true
        help: Sets the level of verbosity
subcommands:
    - test:
        about: controls testing features
        version: &quot;1.3&quot;
        author: Someone E. &amp;lt;someone_else@other.com&amp;gt;
        args:
            - debug:
                short: d
                help: print debug information
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们在&lt;code&gt;main.rs&lt;/code&gt;中添加如下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[macro_use]
extern crate clap;
use clap::App;

fn main() {
    // The YAML file is found relative to the current file, similar to how modules are found
    let yaml = load_yaml!(&quot;cli.yml&quot;);
    let m = App::from_yaml(yaml).get_matches();

     match m.value_of(&quot;argument1&quot;) {
         // ...
     }

     // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;clap&lt;/code&gt;将加载和解析yml配置，以使得我们可以在程序中使用这些参数。&lt;/p&gt;
&lt;p&gt;上面&lt;code&gt;cli.yml&lt;/code&gt;加上&lt;code&gt;-h&lt;/code&gt;的程序运行结果是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ meow -h
My Super Program 1.0
Kevin K. &amp;lt;kbknapp@gmail.com&amp;gt;
Does awesome things

USAGE:
    MyApp [FLAGS] [OPTIONS] &amp;lt;INPUT&amp;gt; [SUBCOMMAND]

FLAGS:
    -h, --help       Prints help information
    -v               Sets the level of verbosity
    -V, --version    Prints version information

OPTIONS:
    -c, --config &amp;lt;FILE&amp;gt;    Sets a custom config file

ARGS:
    INPUT    The input file to use

SUBCOMMANDS:
    help    Prints this message or the help of the given subcommand(s)
    test    Controls testing features
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::tip[现行版本示例]
clap现行版本（3.0.14）官方示例：
:::&lt;/p&gt;
&lt;p&gt;添加现行版依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
clap = { version = &quot;3.0.14&quot;, features = [&quot;derive&quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;a href=&quot;https://github.com/clap-rs/clap/blob/v3.0.14/examples/tutorial_derive/README.md&quot;&gt;Derive API&lt;/a&gt;，以在编写&lt;code&gt;struct&lt;/code&gt;时能够直接通过属性访问&lt;a href=&quot;https://github.com/clap-rs/clap/blob/v3.0.14/examples/tutorial_builder/README.md&quot;&gt;Builder API&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use clap::Parser;

/// Simple program to greet a person
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
    /// Name of the person to greet
    #[clap(short, long)]
    name: String,

    /// Number of times to greet
    #[clap(short, long, default_value_t = 1)]
    count: u8,
}

fn main() {
    let args = Args::parse();

    for _ in 0..args.count {
        println!(&quot;Hello {}!&quot;, args.name)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;观察输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ demo --help
clap [..]
Simple program to greet a person

USAGE:
    demo[EXE] [OPTIONS] --name &amp;lt;NAME&amp;gt;

OPTIONS:
    -c, --count &amp;lt;COUNT&amp;gt;    Number of times to greet [default: 1]
    -h, --help             Print help information
    -n, --name &amp;lt;NAME&amp;gt;      Name of the person to greet
    -V, --version          Print version information

$ demo -n world
Hello world!
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;配置&lt;/h1&gt;
&lt;p&gt;CLI程序还需要配置。有些参数应当在运行前确定，并记录在配置文件中，如&lt;code&gt;.env&lt;/code&gt;、&lt;code&gt;.config&lt;/code&gt;、&lt;code&gt;.setting&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;一个&lt;code&gt;.env&lt;/code&gt;文件的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PORT = 8000
PATH = &quot;home/foo/bar&quot;
MODE = &quot;happy mode&quot;
ZONE = 8
AREA = &quot;Beijing&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可以选择手动处理，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;读取文件&lt;code&gt;.env&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;先按&lt;code&gt;\n&lt;/code&gt;拆分每行&lt;/li&gt;
&lt;li&gt;再按&lt;code&gt;=&lt;/code&gt;拆分各项并将数据添加到&lt;code&gt;HashMap&lt;/code&gt;中&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;或使用一个现成的crate。&lt;/p&gt;
&lt;h2&gt;dotenv_codegen&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://crates.io/crates/dotenv_codegen&quot;&gt;dotenv_codegen&lt;/a&gt;是一个带有宏的简单&lt;code&gt;.env&lt;/code&gt;配置解析器。&lt;/p&gt;
&lt;p&gt;使用这个crate可以轻松处理&lt;code&gt;.env&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use dotenv_codegen::dotenv;

// ...

fn main() {
  println!(&quot;{}&quot;, dotenv!(&quot;PORT&quot;));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;code&gt;.env&lt;/code&gt;是在&lt;code&gt;meow&lt;/code&gt;根目录下的配置文件，它在编译时就被&lt;code&gt;dotenv!&lt;/code&gt;读取，因此编译后再修改配置文件不会改变二进制文件&lt;code&gt;meow&lt;/code&gt;的输出。&lt;/p&gt;
&lt;h1&gt;环境变量&lt;/h1&gt;
&lt;p&gt;您可能还想调用系统中的环境变量，例如&lt;code&gt;JAVA_HOME&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::env;

let key = &quot;HOME&quot;;
match env::var_os(key) {
    Some(val) =&amp;gt; println!(&quot;{}: {:?}&quot;, key, val),
    None =&amp;gt; println!(&quot;{} is not defined in the environment.&quot;, key)
}

// 打印环境变量`HOME`的值。
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;错误处理&lt;/h1&gt;
&lt;p&gt;最常见的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;panic!(&quot;this is panic&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式过于简单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它会直接终止程序&lt;/li&gt;
&lt;li&gt;它在退出时没有错误代码&lt;/li&gt;
&lt;li&gt;它通常使用在小型脚本中&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;使用&lt;code&gt;Result&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Result&lt;/code&gt;可以传递错误而不终止程序。如果函数中断，它将返回带有错误类型的&lt;code&gt;Error&lt;/code&gt;。于是我们可以根据类型来决定下一步做什么，比如“重试”或者“放弃”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum MyErr {
    Reason1,
    Reason2,
}
fn foo() -&amp;gt; Result&amp;lt;(), MyErr&amp;gt; {
    match bar {
        Some(_)=&amp;gt;{}
        None =&amp;gt; Err(MyErr::Reason1)
    }
}
fn hoo() {
    match foo() {
        Ok(_) =&amp;gt; reply(),
        Err(e) =&amp;gt; println!(e) 
        // 返回`e`其实还没有结束
        // 应该继续使用`fmt`明确该错误信息
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;错误信息&lt;/h2&gt;
&lt;p&gt;如果我们希望为错误类型打印特定的错误信息，则需要为&lt;code&gt;MyErr&lt;/code&gt;实现&lt;code&gt;fmt&lt;/code&gt;trait，以使各类错误具有特定的错误信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum MyErr {
    Reason1(String),
    Reason2(String, u32),
}
impl fmt::Display for MyErrError {
    fn fmt(&amp;amp;self, f: &amp;amp;mut fmt::Formatter) -&amp;gt; fmt::Result {
        match *self {
            MyErr::Reason1(ref s) =&amp;gt; 
                write!(f, &quot;`{}` is the error&quot;, s),
            MyErr::Reason2(ref s, ref num) =&amp;gt; 
                write!(f, &quot;`{}` and `{}` are error&quot;, s, num),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Err(e) =&amp;gt; println!(&quot;{}&quot;, e)
// `XXX` is the error
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;标准错误&lt;/h1&gt;
&lt;p&gt;系统中有标准输出和标准错误。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;println!()&lt;/code&gt;是标准输出，&lt;code&gt;eprintln!()&lt;/code&gt;是标准错误。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cargo run &amp;gt; output.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如此仅将标准输出重定向到文件&lt;code&gt;output.txt&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;因此，如果我们不想在&lt;code&gt;output.txt&lt;/code&gt;文件中写入错误消息，就可以使用&lt;code&gt;eprintln!()&lt;/code&gt;将错误打印为标准错误。&lt;/p&gt;
&lt;h1&gt;退出码&lt;/h1&gt;
&lt;p&gt;在程序有问题时使用非零退出码，以通知其他程序“我出错了”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::process;

fn main() {
    process::exit(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;CLI程序可以胜任各种场景，而好的CLI程序需要良好的设计。CLI程序应该能解析参数和配置、读取环境变量、可以很好地处理错误、能够在标准输出和标准错误中输出相应的信息，并在执行失败时输出非零退出代码。&lt;/p&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 07 - 时间戳</title><link>https://zlotus.github.io/2021/12/29/writing-os-in-rust-on-rpi-07/</link><guid isPermaLink="true">https://zlotus.github.io/2021/12/29/writing-os-in-rust-on-rpi-07/</guid><description>使用Rust在树莓派上编写操作系统 - 07 - 时间戳</description><pubDate>Wed, 29 Dec 2021 12:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/07_timestamps/README.md&quot;&gt;Tutorial 07 - Timestamps&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用SD卡上的镜像启动树莓派感觉很棒，但如果对每个新的二进制文件，都需要手动将其放在SD卡上就会非常麻烦。因此，本章我们将编写一个&lt;a href=&quot;https://en.wikipedia.org/wiki/Chain_loading&quot;&gt;链式加载器&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;这是我们最后一次手动拷贝镜像到SD卡上。后面的每章教程都会在&lt;code&gt;Makefile&lt;/code&gt;中提供一个&lt;code&gt;chainboot&lt;/code&gt;目标，以便通过UART加载内核。&lt;/li&gt;
&lt;li&gt;我们为定时器硬件添加抽象，并在&lt;code&gt;_arch/aarch64&lt;/code&gt;中为ARM架构实现了定时器。&lt;/li&gt;
&lt;li&gt;新的计时器函数用于给UART打印的内容添加时间戳，并消除了&lt;code&gt;GPIO&lt;/code&gt;设备驱动程序中由循环产生的延迟，从而提高精确性。&lt;/li&gt;
&lt;li&gt;增加了一个&lt;code&gt;warn!()&lt;/code&gt;宏。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;测试&lt;/h2&gt;
&lt;p&gt;通过chainboot进行测试（在前一个教程中添加）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ make chainboot
[...]
Minipush 1.0

[MP] ⏳ Waiting for /dev/ttyUSB0
[MP] ✅ Serial connected
[MP] 🔌 Please power the target now
 __  __ _      _ _                 _
|  \/  (_)_ _ (_) |   ___  __ _ __| |
| |\/| | | &apos; \| | |__/ _ \/ _` / _` |
|_|  |_|_|_||_|_|____\___/\__,_\__,_|

           Raspberry Pi 3

[ML] Requesting binary
[MP] ⏩ Pushing 12 KiB =========================================🦀 100% 0 KiB/s Time: 00:00:00
[ML] Loaded! Executing the payload now

[    0.140431] mingo version 0.7.0
[    0.140630] Booting on: Raspberry Pi 3
[    0.141085] Architectural timer resolution: 52 ns
[    0.141660] Drivers loaded:
[    0.141995]       1. BCM GPIO
[    0.142353]       2. BCM PL011 UART
[W   0.142777] Spin duration smaller than architecturally supported, skipping
[    0.143621] Spinning for 1 second
[    1.144023] Spinning for 1 second
[    2.144245] Spinning for 1 second
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;与上一章代码的区别&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;diff -uNr 06_uart_chainloader/Cargo.toml 07_timestamps/Cargo.toml
--- 06_uart_chainloader/Cargo.toml
+++ 07_timestamps/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = &quot;mingo&quot;
-version = &quot;0.6.0&quot;
+version = &quot;0.7.0&quot;
 authors = [&quot;Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;&quot;]
 edition = &quot;2021&quot;

Binary files 06_uart_chainloader/demo_payload_rpi3.img and 07_timestamps/demo_payload_rpi3.img differ
Binary files 06_uart_chainloader/demo_payload_rpi4.img and 07_timestamps/demo_payload_rpi4.img differ

diff -uNr 06_uart_chainloader/Makefile 07_timestamps/Makefile
--- 06_uart_chainloader/Makefile
+++ 07_timestamps/Makefile
@@ -23,29 +23,27 @@

 # BSP-specific arguments.
 ifeq ($(BSP),rpi3)
-    TARGET                 = aarch64-unknown-none-softfloat
-    KERNEL_BIN             = kernel8.img
-    QEMU_BINARY            = qemu-system-aarch64
-    QEMU_MACHINE_TYPE      = raspi3
-    QEMU_RELEASE_ARGS      = -serial stdio -display none
-    OBJDUMP_BINARY         = aarch64-none-elf-objdump
-    NM_BINARY              = aarch64-none-elf-nm
-    READELF_BINARY         = aarch64-none-elf-readelf
-    LINKER_FILE            = src/bsp/raspberrypi/link.ld
-    RUSTC_MISC_ARGS        = -C target-cpu=cortex-a53
-    CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi3.img
+    TARGET            = aarch64-unknown-none-softfloat
+    KERNEL_BIN        = kernel8.img
+    QEMU_BINARY       = qemu-system-aarch64
+    QEMU_MACHINE_TYPE = raspi3
+    QEMU_RELEASE_ARGS = -serial stdio -display none
+    OBJDUMP_BINARY    = aarch64-none-elf-objdump
+    NM_BINARY         = aarch64-none-elf-nm
+    READELF_BINARY    = aarch64-none-elf-readelf
+    LINKER_FILE       = src/bsp/raspberrypi/link.ld
+    RUSTC_MISC_ARGS   = -C target-cpu=cortex-a53
 else ifeq ($(BSP),rpi4)
-    TARGET                 = aarch64-unknown-none-softfloat
-    KERNEL_BIN             = kernel8.img
-    QEMU_BINARY            = qemu-system-aarch64
-    QEMU_MACHINE_TYPE      =
-    QEMU_RELEASE_ARGS      = -serial stdio -display none
-    OBJDUMP_BINARY         = aarch64-none-elf-objdump
-    NM_BINARY              = aarch64-none-elf-nm
-    READELF_BINARY         = aarch64-none-elf-readelf
-    LINKER_FILE            = src/bsp/raspberrypi/link.ld
-    RUSTC_MISC_ARGS        = -C target-cpu=cortex-a72
-    CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi4.img
+    TARGET            = aarch64-unknown-none-softfloat
+    KERNEL_BIN        = kernel8.img
+    QEMU_BINARY       = qemu-system-aarch64
+    QEMU_MACHINE_TYPE =
+    QEMU_RELEASE_ARGS = -serial stdio -display none
+    OBJDUMP_BINARY    = aarch64-none-elf-objdump
+    NM_BINARY         = aarch64-none-elf-nm
+    READELF_BINARY    = aarch64-none-elf-readelf
+    LINKER_FILE       = src/bsp/raspberrypi/link.ld
+    RUSTC_MISC_ARGS   = -C target-cpu=cortex-a72
 endif

 QEMU_MISSING_STRING = &quot;This board is not yet supported for QEMU.&quot;
@@ -77,7 +75,7 @@
     -O binary

 EXEC_QEMU          = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE)
-EXEC_TEST_MINIPUSH = ruby tests/chainboot_test.rb
+EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb
 EXEC_MINIPUSH      = ruby ../common/serial/minipush.rb

 ##------------------------------------------------------------------------------
@@ -134,7 +132,7 @@
 ##------------------------------------------------------------------------------
 ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board.

-qemu qemuasm:
+qemu:
 	$(call colorecho, &quot;\n$(QEMU_MISSING_STRING)&quot;)

 else # QEMU is supported.
@@ -143,17 +141,13 @@
 	$(call colorecho, &quot;\nLaunching QEMU&quot;)
 	@$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN)

-qemuasm: $(KERNEL_BIN)
-	$(call colorecho, &quot;\nLaunching QEMU with ASM output&quot;)
-	@$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -d in_asm
-
 endif

 ##------------------------------------------------------------------------------
 ## Push the kernel to the real HW target
 ##------------------------------------------------------------------------------
 chainboot: $(KERNEL_BIN)
-	@$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(CHAINBOOT_DEMO_PAYLOAD)
+	@$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN)

 ##------------------------------------------------------------------------------
 ## Run clippy
@@ -217,8 +211,7 @@
 ##------------------------------------------------------------------------------
 test_boot: $(KERNEL_BIN)
 	$(call colorecho, &quot;\nBoot test - $(BSP)&quot;)
-	@$(DOCKER_TEST) $(EXEC_TEST_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \
-		-kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD)
+	@$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN)

 test: test_boot


diff -uNr 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s 07_timestamps/src/_arch/aarch64/cpu/boot.s
--- 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s
+++ 07_timestamps/src/_arch/aarch64/cpu/boot.s
@@ -18,17 +18,6 @@
 	add	\register, \register, #:lo12:\symbol
 .endm

-// Load the address of a symbol into a register, absolute.
-//
-// # Resources
-//
-// - https://sourceware.org/binutils/docs-2.36/as/AArch64_002dRelocations.html
-.macro ADR_ABS register, symbol
-	movz	\register, #:abs_g2:\symbol
-	movk	\register, #:abs_g1_nc:\symbol
-	movk	\register, #:abs_g0_nc:\symbol
-.endm
-
 .equ _core_id_mask, 0b11

 //--------------------------------------------------------------------------------------------------
@@ -50,35 +39,23 @@
 	// If execution reaches here, it is the boot core.

 	// Initialize DRAM.
-	ADR_ABS	x0, __bss_start
-	ADR_ABS x1, __bss_end_exclusive
+	ADR_REL	x0, __bss_start
+	ADR_REL x1, __bss_end_exclusive

 .L_bss_init_loop:
 	cmp	x0, x1
-	b.eq	.L_relocate_binary
+	b.eq	.L_prepare_rust
 	stp	xzr, xzr, [x0], #16
 	b	.L_bss_init_loop

-	// Next, relocate the binary.
-.L_relocate_binary:
-	ADR_REL	x0, __binary_nonzero_start         // The address the binary got loaded to.
-	ADR_ABS	x1, __binary_nonzero_start         // The address the binary was linked to.
-	ADR_ABS	x2, __binary_nonzero_end_exclusive
-
-.L_copy_loop:
-	ldr	x3, [x0], #8
-	str	x3, [x1], #8
-	cmp	x1, x2
-	b.lo	.L_copy_loop
-
 	// Prepare the jump to Rust code.
+.L_prepare_rust:
 	// Set the stack pointer.
-	ADR_ABS	x0, __boot_core_stack_end_exclusive
+	ADR_REL	x0, __boot_core_stack_end_exclusive
 	mov	sp, x0

-	// Jump to the relocated Rust code.
-	ADR_ABS	x1, _start_rust
-	br	x1
+	// Jump to Rust code.
+	b	_start_rust

 	// Infinitely wait for events (aka &quot;park the core&quot;).
 .L_parking_loop:

diff -uNr 06_uart_chainloader/src/_arch/aarch64/cpu.rs 07_timestamps/src/_arch/aarch64/cpu.rs
--- 06_uart_chainloader/src/_arch/aarch64/cpu.rs
+++ 07_timestamps/src/_arch/aarch64/cpu.rs
@@ -19,15 +19,6 @@

 pub use asm::nop;

-/// Spin for `n` cycles.
-#[cfg(feature = &quot;bsp_rpi3&quot;)]
-#[inline(always)]
-pub fn spin_for_cycles(n: usize) {
-    for _ in 0..n {
-        asm::nop();
-    }
-}
-
 /// Pause execution on the core.
 #[inline(always)]
 pub fn wait_forever() -&amp;gt; ! {

diff -uNr 06_uart_chainloader/src/_arch/aarch64/time.rs 07_timestamps/src/_arch/aarch64/time.rs
--- 06_uart_chainloader/src/_arch/aarch64/time.rs
+++ 07_timestamps/src/_arch/aarch64/time.rs
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! Architectural timer primitives.
+//!
+//! # Orientation
+//!
+//! Since arch modules are imported into generic modules using the path attribute, the path of this
+//! file is:
+//!
+//! crate::time::arch_time
+
+use crate::{time, warn};
+use core::time::Duration;
+use cortex_a::{asm::barrier, registers::*};
+use tock_registers::interfaces::{ReadWriteable, Readable, Writeable};
+
+//--------------------------------------------------------------------------------------------------
+// Private Definitions
+//--------------------------------------------------------------------------------------------------
+
+const NS_PER_S: u64 = 1_000_000_000;
+
+/// ARMv8 Generic Timer.
+struct GenericTimer;
+
+//--------------------------------------------------------------------------------------------------
+// Global instances
+//--------------------------------------------------------------------------------------------------
+
+static TIME_MANAGER: GenericTimer = GenericTimer;
+
+//--------------------------------------------------------------------------------------------------
+// Private Code
+//--------------------------------------------------------------------------------------------------
+
+impl GenericTimer {
+    #[inline(always)]
+    fn read_cntpct(&amp;amp;self) -&amp;gt; u64 {
+        // Prevent that the counter is read ahead of time due to out-of-order execution.
+        unsafe { barrier::isb(barrier::SY) };
+        CNTPCT_EL0.get()
+    }
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// Return a reference to the time manager.
+pub fn time_manager() -&amp;gt; &amp;amp;&apos;static impl time::interface::TimeManager {
+    &amp;amp;TIME_MANAGER
+}
+
+//------------------------------------------------------------------------------
+// OS Interface Code
+//------------------------------------------------------------------------------
+
+impl time::interface::TimeManager for GenericTimer {
+    fn resolution(&amp;amp;self) -&amp;gt; Duration {
+        Duration::from_nanos(NS_PER_S / (CNTFRQ_EL0.get() as u64))
+    }
+
+    fn uptime(&amp;amp;self) -&amp;gt; Duration {
+        let current_count: u64 = self.read_cntpct() * NS_PER_S;
+        let frq: u64 = CNTFRQ_EL0.get() as u64;
+
+        Duration::from_nanos(current_count / frq)
+    }
+
+    fn spin_for(&amp;amp;self, duration: Duration) {
+        // Instantly return on zero.
+        if duration.as_nanos() == 0 {
+            return;
+        }
+
+        // Calculate the register compare value.
+        let frq = CNTFRQ_EL0.get();
+        let x = match frq.checked_mul(duration.as_nanos() as u64) {
+            None =&amp;gt; {
+                warn!(&quot;Spin duration too long, skipping&quot;);
+                return;
+            }
+            Some(val) =&amp;gt; val,
+        };
+        let tval = x / NS_PER_S;
+
+        // Check if it is within supported bounds.
+        let warn: Option&amp;lt;&amp;amp;str&amp;gt; = if tval == 0 {
+            Some(&quot;smaller&quot;)
+        // The upper 32 bits of CNTP_TVAL_EL0 are reserved.
+        } else if tval &amp;gt; u32::max_value().into() {
+            Some(&quot;bigger&quot;)
+        } else {
+            None
+        };
+
+        if let Some(w) = warn {
+            warn!(
+                &quot;Spin duration {} than architecturally supported, skipping&quot;,
+                w
+            );
+            return;
+        }
+
+        // Set the compare value register.
+        CNTP_TVAL_EL0.set(tval);
+
+        // Kick off the counting.                       // Disable timer interrupt.
+        CNTP_CTL_EL0.modify(CNTP_CTL_EL0::ENABLE::SET + CNTP_CTL_EL0::IMASK::SET);
+
+        // ISTATUS will be &apos;1&apos; when cval ticks have passed. Busy-check it.
+        while !CNTP_CTL_EL0.matches_all(CNTP_CTL_EL0::ISTATUS::SET) {}
+
+        // Disable counting again.
+        CNTP_CTL_EL0.modify(CNTP_CTL_EL0::ENABLE::CLEAR);
+    }
+}

diff -uNr 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs 07_timestamps/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
--- 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
+++ 07_timestamps/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
@@ -143,25 +143,19 @@
     /// Disable pull-up/down on pins 14 and 15.
     #[cfg(feature = &quot;bsp_rpi3&quot;)]
     fn disable_pud_14_15_bcm2837(&amp;amp;mut self) {
-        use crate::cpu;
+        use crate::{time, time::interface::TimeManager};
+        use core::time::Duration;

-        // Make an educated guess for a good delay value (Sequence described in the BCM2837
-        // peripherals PDF).
-        //
-        // - According to Wikipedia, the fastest RPi4 clocks around 1.5 GHz.
-        // - The Linux 2837 GPIO driver waits 1 µs between the steps.
-        //
-        // So lets try to be on the safe side and default to 2000 cycles, which would equal 1 µs
-        // would the CPU be clocked at 2 GHz.
-        const DELAY: usize = 2000;
+        // The Linux 2837 GPIO driver waits 1 µs between the steps.
+        const DELAY: Duration = Duration::from_micros(1);

         self.registers.GPPUD.write(GPPUD::PUD::Off);
-        cpu::spin_for_cycles(DELAY);
+        time::time_manager().spin_for(DELAY);

         self.registers
             .GPPUDCLK0
             .write(GPPUDCLK0::PUDCLK15::AssertClock + GPPUDCLK0::PUDCLK14::AssertClock);
-        cpu::spin_for_cycles(DELAY);
+        time::time_manager().spin_for(DELAY);

         self.registers.GPPUD.write(GPPUD::PUD::Off);
         self.registers.GPPUDCLK0.set(0);

diff -uNr 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs 07_timestamps/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
--- 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
+++ 07_timestamps/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
@@ -278,7 +278,7 @@
     }

     /// Retrieve a character.
-    fn read_char(&amp;amp;mut self, blocking_mode: BlockingMode) -&amp;gt; Option&amp;lt;char&amp;gt; {
+    fn read_char_converting(&amp;amp;mut self, blocking_mode: BlockingMode) -&amp;gt; Option&amp;lt;char&amp;gt; {
         // If RX FIFO is empty,
         if self.registers.FR.matches_all(FR::RXFE::SET) {
             // immediately return in non-blocking mode.
@@ -293,7 +293,12 @@
         }

         // Read one character.
-        let ret = self.registers.DR.get() as u8 as char;
+        let mut ret = self.registers.DR.get() as u8 as char;
+
+        // Convert carrige return to newline.
+        if ret == &apos;\r&apos; {
+            ret = &apos;\n&apos;
+        }

         // Update statistics.
         self.chars_read += 1;
@@ -373,14 +378,14 @@
 impl console::interface::Read for PL011Uart {
     fn read_char(&amp;amp;self) -&amp;gt; char {
         self.inner
-            .lock(|inner| inner.read_char(BlockingMode::Blocking).unwrap())
+            .lock(|inner| inner.read_char_converting(BlockingMode::Blocking).unwrap())
     }

     fn clear_rx(&amp;amp;self) {
         // Read from the RX FIFO until it is indicating empty.
         while self
             .inner
-            .lock(|inner| inner.read_char(BlockingMode::NonBlocking))
+            .lock(|inner| inner.read_char_converting(BlockingMode::NonBlocking))
             .is_some()
         {}
     }

diff -uNr 06_uart_chainloader/src/bsp/raspberrypi/link.ld 07_timestamps/src/bsp/raspberrypi/link.ld
--- 06_uart_chainloader/src/bsp/raspberrypi/link.ld
+++ 07_timestamps/src/bsp/raspberrypi/link.ld
@@ -3,6 +3,8 @@
  * Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
  */

+__rpi_phys_dram_start_addr = 0;
+
 /* The physical address at which the the kernel binary will be loaded by the Raspberry&apos;s firmware */
 __rpi_phys_binary_load_addr = 0x80000;

@@ -26,8 +28,7 @@

 SECTIONS
 {
-    /* Set the link address to 32 MiB */
-    . = 0x2000000;
+    . =  __rpi_phys_dram_start_addr;

     /***********************************************************************************************
     * Boot Core Stack
@@ -44,7 +45,6 @@
     /***********************************************************************************************
     * Code + RO Data + Global Offset Table
     ***********************************************************************************************/
-    __binary_nonzero_start = .;
     .text :
     {
         KEEP(*(.text._start))
@@ -61,10 +61,6 @@
     ***********************************************************************************************/
     .data : { *(.data*) } :segment_data

-    /* Fill up to 8 byte, b/c relocating the binary is done in u64 chunks */
-    . = ALIGN(8);
-    __binary_nonzero_end_exclusive = .;
-
     /* Section is zeroed in pairs of u64. Align start and end to 16 bytes */
     .bss (NOLOAD) : ALIGN(16)
     {

diff -uNr 06_uart_chainloader/src/bsp/raspberrypi/memory.rs 07_timestamps/src/bsp/raspberrypi/memory.rs
--- 06_uart_chainloader/src/bsp/raspberrypi/memory.rs
+++ 07_timestamps/src/bsp/raspberrypi/memory.rs
@@ -11,10 +11,9 @@
 /// The board&apos;s physical memory map.
 #[rustfmt::skip]
 pub(super) mod map {
-    pub const BOARD_DEFAULT_LOAD_ADDRESS: usize =        0x8_0000;

-    pub const GPIO_OFFSET:                usize =        0x0020_0000;
-    pub const UART_OFFSET:                usize =        0x0020_1000;
+    pub const GPIO_OFFSET:         usize = 0x0020_0000;
+    pub const UART_OFFSET:         usize = 0x0020_1000;

     /// Physical devices.
     #[cfg(feature = &quot;bsp_rpi3&quot;)]
@@ -36,13 +35,3 @@
         pub const PL011_UART_START: usize = START + UART_OFFSET;
     }
 }
-
-//--------------------------------------------------------------------------------------------------
-// Public Code
-//--------------------------------------------------------------------------------------------------
-
-/// The address on which the Raspberry firmware loads every binary by default.
-#[inline(always)]
-pub fn board_default_load_addr() -&amp;gt; *const u64 {
-    map::BOARD_DEFAULT_LOAD_ADDRESS as _
-}

diff -uNr 06_uart_chainloader/src/cpu.rs 07_timestamps/src/cpu.rs
--- 06_uart_chainloader/src/cpu.rs
+++ 07_timestamps/src/cpu.rs
@@ -14,6 +14,3 @@
 // Architectural Public Reexports
 //--------------------------------------------------------------------------------------------------
 pub use arch_cpu::{nop, wait_forever};
-
-#[cfg(feature = &quot;bsp_rpi3&quot;)]
-pub use arch_cpu::spin_for_cycles;

diff -uNr 06_uart_chainloader/src/main.rs 07_timestamps/src/main.rs
--- 06_uart_chainloader/src/main.rs
+++ 07_timestamps/src/main.rs
@@ -119,6 +119,7 @@
 mod panic_wait;
 mod print;
 mod synchronization;
+mod time;

 /// Early init code.
 ///
@@ -141,56 +142,38 @@
     kernel_main()
 }

-const MINILOAD_LOGO: &amp;amp;str = r#&quot;
- __  __ _      _ _                 _
-|  \/  (_)_ _ (_) |   ___  __ _ __| |
-| |\/| | | &apos; \| | |__/ _ \/ _` / _` |
-|_|  |_|_|_||_|_|____\___/\__,_\__,_|
-&quot;#;
-
 /// The main function running after the early init.
 fn kernel_main() -&amp;gt; ! {
-    use bsp::console::console;
-    use console::interface::All;
+    use core::time::Duration;
+    use driver::interface::DriverManager;
+    use time::interface::TimeManager;

-    println!(&quot;{}&quot;, MINILOAD_LOGO);
-    println!(&quot;{:^37}&quot;, bsp::board_name());
-    println!();
-    println!(&quot;[ML] Requesting binary&quot;);
-    console().flush();
-
-    // Discard any spurious received characters before starting with the loader protocol.
-    console().clear_rx();
-
-    // Notify `Minipush` to send the binary.
-    for _ in 0..3 {
-        console().write_char(3 as char);
+    info!(
+        &quot;{} version {}&quot;,
+        env!(&quot;CARGO_PKG_NAME&quot;),
+        env!(&quot;CARGO_PKG_VERSION&quot;)
+    );
+    info!(&quot;Booting on: {}&quot;, bsp::board_name());
+
+    info!(
+        &quot;Architectural timer resolution: {} ns&quot;,
+        time::time_manager().resolution().as_nanos()
+    );
+
+    info!(&quot;Drivers loaded:&quot;);
+    for (i, driver) in bsp::driver::driver_manager()
+        .all_device_drivers()
+        .iter()
+        .enumerate()
+    {
+        info!(&quot;      {}. {}&quot;, i + 1, driver.compatible());
     }

-    // Read the binary&apos;s size.
-    let mut size: u32 = u32::from(console().read_char() as u8);
-    size |= u32::from(console().read_char() as u8) &amp;lt;&amp;lt; 8;
-    size |= u32::from(console().read_char() as u8) &amp;lt;&amp;lt; 16;
-    size |= u32::from(console().read_char() as u8) &amp;lt;&amp;lt; 24;
-
-    // Trust it&apos;s not too big.
-    console().write_char(&apos;O&apos;);
-    console().write_char(&apos;K&apos;);
-
-    let kernel_addr: *mut u8 = bsp::memory::board_default_load_addr() as *mut u8;
-    unsafe {
-        // Read the kernel byte by byte.
-        for i in 0..size {
-            core::ptr::write_volatile(kernel_addr.offset(i as isize), console().read_char() as u8)
-        }
-    }
+    // Test a failing timer case.
+    time::time_manager().spin_for(Duration::from_nanos(1));

-    println!(&quot;[ML] Loaded! Executing the payload now\n&quot;);
-    console().flush();
-
-    // Use black magic to create a function pointer.
-    let kernel: fn() -&amp;gt; ! = unsafe { core::mem::transmute(kernel_addr) };
-
-    // Jump to loaded kernel!
-    kernel()
+    loop {
+        info!(&quot;Spinning for 1 second&quot;);
+        time::time_manager().spin_for(Duration::from_secs(1));
+    }
 }

diff -uNr 06_uart_chainloader/src/print.rs 07_timestamps/src/print.rs
--- 06_uart_chainloader/src/print.rs
+++ 07_timestamps/src/print.rs
@@ -36,3 +36,71 @@
         $crate::print::_print(format_args_nl!($($arg)*));
     })
 }
+
+/// Prints an info, with a newline.
+#[macro_export]
+macro_rules! info {
+    ($string:expr) =&amp;gt; ({
+        #[allow(unused_imports)]
+        use crate::time::interface::TimeManager;
+
+        let timestamp = $crate::time::time_manager().uptime();
+        let timestamp_subsec_us = timestamp.subsec_micros();
+
+        $crate::print::_print(format_args_nl!(
+            concat!(&quot;[  {:&amp;gt;3}.{:03}{:03}] &quot;, $string),
+            timestamp.as_secs(),
+            timestamp_subsec_us / 1_000,
+            timestamp_subsec_us modulo 1_000
+        ));
+    });
+    ($format_string:expr, $($arg:tt)*) =&amp;gt; ({
+        #[allow(unused_imports)]
+        use crate::time::interface::TimeManager;
+
+        let timestamp = $crate::time::time_manager().uptime();
+        let timestamp_subsec_us = timestamp.subsec_micros();
+
+        $crate::print::_print(format_args_nl!(
+            concat!(&quot;[  {:&amp;gt;3}.{:03}{:03}] &quot;, $format_string),
+            timestamp.as_secs(),
+            timestamp_subsec_us / 1_000,
+            timestamp_subsec_us modulo 1_000,
+            $($arg)*
+        ));
+    })
+}
+
+/// Prints a warning, with a newline.
+#[macro_export]
+macro_rules! warn {
+    ($string:expr) =&amp;gt; ({
+        #[allow(unused_imports)]
+        use crate::time::interface::TimeManager;
+
+        let timestamp = $crate::time::time_manager().uptime();
+        let timestamp_subsec_us = timestamp.subsec_micros();
+
+        $crate::print::_print(format_args_nl!(
+            concat!(&quot;[W {:&amp;gt;3}.{:03}{:03}] &quot;, $string),
+            timestamp.as_secs(),
+            timestamp_subsec_us / 1_000,
+            timestamp_subsec_us modulo 1_000
+        ));
+    });
+    ($format_string:expr, $($arg:tt)*) =&amp;gt; ({
+        #[allow(unused_imports)]
+        use crate::time::interface::TimeManager;
+
+        let timestamp = $crate::time::time_manager().uptime();
+        let timestamp_subsec_us = timestamp.subsec_micros();
+
+        $crate::print::_print(format_args_nl!(
+            concat!(&quot;[W {:&amp;gt;3}.{:03}{:03}] &quot;, $format_string),
+            timestamp.as_secs(),
+            timestamp_subsec_us / 1_000,
+            timestamp_subsec_us modulo 1_000,
+            $($arg)*
+        ));
+    })
+}

diff -uNr 06_uart_chainloader/src/time.rs 07_timestamps/src/time.rs
--- 06_uart_chainloader/src/time.rs
+++ 07_timestamps/src/time.rs
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2020-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! Timer primitives.
+
+#[cfg(target_arch = &quot;aarch64&quot;)]
+#[path = &quot;_arch/aarch64/time.rs&quot;]
+mod arch_time;
+
+//--------------------------------------------------------------------------------------------------
+// Architectural Public Reexports
+//--------------------------------------------------------------------------------------------------
+pub use arch_time::time_manager;
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Timekeeping interfaces.
+pub mod interface {
+    use core::time::Duration;
+
+    /// Time management functions.
+    pub trait TimeManager {
+        /// The timer&apos;s resolution.
+        fn resolution(&amp;amp;self) -&amp;gt; Duration;
+
+        /// The uptime since power-on of the device.
+        ///
+        /// This includes time consumed by firmware and bootloaders.
+        fn uptime(&amp;amp;self) -&amp;gt; Duration;
+
+        /// Spin for a given duration.
+        fn spin_for(&amp;amp;self, duration: Duration);
+    }
+}

diff -uNr 06_uart_chainloader/tests/boot_test_string.rb 07_timestamps/tests/boot_test_string.rb
--- 06_uart_chainloader/tests/boot_test_string.rb
+++ 07_timestamps/tests/boot_test_string.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+EXPECTED_PRINT = &apos;Spinning for 1 second&apos;

diff -uNr 06_uart_chainloader/tests/chainboot_test.rb 07_timestamps/tests/chainboot_test.rb
--- 06_uart_chainloader/tests/chainboot_test.rb
+++ 07_timestamps/tests/chainboot_test.rb
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-# SPDX-License-Identifier: MIT OR Apache-2.0
-#
-# Copyright (c) 2020-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
-
-require_relative &apos;../../common/serial/minipush&apos;
-require_relative &apos;../../common/tests/boot_test&apos;
-require &apos;pty&apos;
-
-# Match for the last print that &apos;demo_payload_rpiX.img&apos; produces.
-EXPECTED_PRINT = &apos;Echoing input now&apos;
-
-# Extend BootTest so that it listens on the output of a MiniPush instance, which is itself connected
-# to a QEMU instance instead of a real HW.
-class ChainbootTest &amp;lt; BootTest
-    MINIPUSH = &apos;../common/serial/minipush.rb&apos;
-    MINIPUSH_POWER_TARGET_REQUEST = &apos;Please power the target now&apos;
-
-    def initialize(qemu_cmd, payload_path)
-        super(qemu_cmd, EXPECTED_PRINT)
-
-        @test_name = &apos;Boot test using Minipush&apos;
-
-        @payload_path = payload_path
-    end
-
-    private
-
-    # override
-    def post_process_and_add_output(output)
-        temp = output.join.split(&quot;\r\n&quot;)
-
-        # Should a line have solo carriage returns, remove any overridden parts of the string.
-        temp.map! { |x| x.gsub(/.*\r/, &apos;&apos;) }
-
-        @test_output += temp
-    end
-
-    def wait_for_minipush_power_request(mp_out)
-        output = []
-        Timeout.timeout(MAX_WAIT_SECS) do
-            loop do
-                output &amp;lt;&amp;lt; mp_out.gets
-                break if output.last.include?(MINIPUSH_POWER_TARGET_REQUEST)
-            end
-        end
-    rescue Timeout::Error
-        @test_error = &apos;Timed out waiting for power request&apos;
-    rescue StandardError =&amp;gt; e
-        @test_error = e.message
-    ensure
-        post_process_and_add_output(output)
-    end
-
-    # override
-    def setup
-        pty_main, pty_secondary = PTY.open
-        mp_out, _mp_in = PTY.spawn(&quot;ruby #{MINIPUSH} #{pty_secondary.path} #{@payload_path}&quot;)
-
-        # Wait until MiniPush asks for powering the target.
-        wait_for_minipush_power_request(mp_out)
-
-        # Now is the time to start QEMU with the chainloader binary. QEMU&apos;s virtual tty is connected
-        # to the MiniPush instance spawned above, so that the two processes talk to each other.
-        Process.spawn(@qemu_cmd, in: pty_main, out: pty_main)
-
-        # The remainder of the test is done by the parent class&apos; run_concrete_test, which listens on
-        # @qemu_serial. Hence, point it to MiniPush&apos;s output.
-        @qemu_serial = mp_out
-    end
-end
-
-##--------------------------------------------------------------------------------------------------
-## Execution starts here
-##--------------------------------------------------------------------------------------------------
-payload_path = ARGV.pop
-qemu_cmd = ARGV.join(&apos; &apos;)
-
-ChainbootTest.new(qemu_cmd, payload_path).run

diff -uNr 06_uart_chainloader/update.sh 07_timestamps/update.sh
--- 06_uart_chainloader/update.sh
+++ 07_timestamps/update.sh
@@ -1,8 +0,0 @@
-#!/usr/bin/env bash
-
-cd ../05_drivers_gpio_uart
-BSP=rpi4 make
-cp kernel8.img ../06_uart_chainloader/demo_payload_rpi4.img
-make
-cp kernel8.img ../06_uart_chainloader/demo_payload_rpi3.img
-rm kernel8.img
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>《腰，星际，健身房》</title><link>https://zlotus.github.io/2021/11/27/spine-starcraft-gym/</link><guid isPermaLink="true">https://zlotus.github.io/2021/11/27/spine-starcraft-gym/</guid><description>《腰，星际，健身房》</description><pubDate>Sat, 27 Nov 2021 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;./spine-starcraft-gym/spine-starcraft-gym-0.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;我那时候还太萌新，不知道所有母胎原装的部件，早已在暗中标好了使用期限。&lt;/p&gt;
&lt;p&gt;——某猛男。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;故事要从十几年前说起……本科一共四年，我就打了四年DOTA，没黑没白的那种，解锁了各种使用电脑的姿势，包括但不限于站、坐、躺、趴、卧，基本除了倒立都能来，不仅能来还能长时间保持同一个姿势进行鏖战。&lt;/p&gt;
&lt;p&gt;我那时候还太萌新，不知道所有母胎原装的部件，早已在暗中标好了使用期限。&lt;/p&gt;
&lt;p&gt;毕竟年轻，工作以后也偶尔会腰酸，但是我并不在意。第一次在意，应该是五年前在上海。上海夏天的室外，就两种结局：蒸死，或者被台风吹进黄浦江。于是，农闲时间我选择打游戏杀时间，其中的某一个月，我坚持每天晚上打几个小时DOTA。尽管公寓的凳子没有靠背，却也无法阻挡我推搭中单跪的热血。坚持了二十多天后，腰越来越酸。我一如既往的作死，搜了个点帖子和视频，学习并实践了几个姿势以缓解腰酸，从而辅助我继续保卫远古遗迹的伟大事业。&lt;/p&gt;
&lt;p&gt;当然，我也是后来才知道，腰出问题的时候，要先恢复再锻炼。&lt;/p&gt;
&lt;p&gt;第二天，腰椎就表示它崩了，以前的所有姿势都封杀了，只有绝对直立和绝对平躺可以用。我就在绝对躺平了几天后，回西安拍片子，大夫是个拥有有趣灵魂的中年大叔：“呦，小伙子没看出来啊，二十岁的年纪四十岁的腰~”……幸运的是，各方面都很平凡的我，椎间盘也并不突出，也可能是因为以前的各种姿势都解锁了，并没有长期保持同一种姿势……&lt;/p&gt;
&lt;p&gt;为保狗命，我赶紧去办了健身卡，虽然五年来从一周六七次慢慢懒到了一周三四次，但好在一直没断。皮脂也上涨了，体重也上来了，肩也宽了背也厚了，核心更稳定了，总的来说就像个人了。于是我就飘了。&lt;/p&gt;
&lt;p&gt;我一直喜欢暴雪，魔兽3星际2的正版我都买了，星际2更是首发就买，开无敌打剧情再赞叹暴雪爸爸做的RTS天下第一。今年我阴差阳错的追了&lt;a href=&quot;https://space.bilibili.com/21869937/video&quot;&gt;萝卜&lt;/a&gt;和&lt;a href=&quot;https://space.bilibili.com/6404146/video&quot;&gt;娜娜&lt;/a&gt;的星际2视频，看了大半年后，就有点饥渴难耐。&lt;/p&gt;
&lt;p&gt;虽然今年32了，理论上已经是个体重62kg，又稳又重的中年人了。但有时候，我就是在意肚皮厚度和头发密度，我就是不服老啊。星际2是暴雪游戏里对年龄要求最低、对视力要求最低、对智力要求最低的，年轻人打得我打不得？&lt;/p&gt;
&lt;p&gt;握了握手里的鼠标键盘，那就索性，让青春再燃烧一次吧。&lt;/p&gt;
&lt;p&gt;9月下旬，诱人的国庆黄金周就被我用星际成就排满了，我的理由是，为了预祝中国队在TI 10上夺冠，打星际暖暖场。别管逻辑通不通，事后看来，我在DOTA背刺我之前，阴差阳错的背刺了DOTA。&lt;/p&gt;
&lt;p&gt;星际2是个难度非常大的游戏，那几百个成就有些也非常阴间，但是我叱咤游戏几十年，心中早有觉悟。毕竟星际、红警、沙丘、帝国这批游戏，是我小学时在家属院俱乐部南边那片原初网吧里就认识的老面孔了。&lt;/p&gt;
&lt;p&gt;今天看来，我还是轻敌了。成就很难做，APM飙到200是常有的事。休闲游戏可以靠在椅背上慢慢打，一口牛奶一口土豆片，顺便点一点鼠标键盘；DOTA这种普通游戏有打有歇，补刀的时候怠惰一点也没关系……但星际2这种高难度游戏不行，肌肉紧绷，全神贯注，脑速八千转——造农民、出建筑、攀科技、开分矿、守产能、打骚扰、抗正面——打不过去的时候研究地图和战术。这种极度充实的日子过了五十来天，除了吃饭睡觉工作以外，就是不停的操作。一个正在为自己精妙操作沾沾自喜的人，是不会在意自己已经几个小时身体前倾僵直满眼血丝死盯屏幕的，更不会在乎自己打完睡觉的时候已经手脚冰凉的像个姑娘。&lt;/p&gt;
&lt;p&gt;萌新的能力是有极限的！我不要做萌新了！我如愿成为了一个猛男，一个成熟的指挥官，我信赖我的部队，他们给我带来了一次又一次的胜利。我了解历史，自然知道百战百胜的结果，就是力竭而亡，项王拿破仑我懂，我都懂，但是我不在乎。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./spine-starcraft-gym/spine-starcraft-gym-1.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;嘿嘿~诶~脊椎说它在乎~&lt;/p&gt;
&lt;p&gt;启初是11月开始的腰酸，我没在意，坚持在上旬抽空打完了所有战役成就。区区腰酸，呵，已经没有什么好怕的了。五年前我还是铁房萌新，但今天我已经成长为一个铁房猛男。作为一个成熟的撸铁男，我信赖我手中的各种杠铃和器械，它们给我带来了一块又一块的死肌肉，我解锁了撸铁的各种姿势，每次撸完还会抽出7分钟专门练核心。既然腰酸，那就撸铁的时候多加几组练背和核心！胸有惊雷而面如平湖者可拜上将军，古有周亚夫卧榻平夜惊，今有某猛男铁片治腰疼！&lt;/p&gt;
&lt;p&gt;11月22日，我专门在单位铁房撸了一个多小时背带核心，洗完澡感觉这下稳了，可以继续我的猛男事业了，不论在星际2还是在健身房，猛男就要贯彻猛男的意志！&lt;/p&gt;
&lt;p&gt;第二天，腰椎表示它又崩了。熟悉的感觉又回来了，所有姿势又被锁定，只有直立或平躺可用，不过这次好歹有猛男buff加持，没上次那么严重。但是精神上我是非常崩溃，一个萌新用了五年在铁房挥汗如雨的成为了猛男，不到五十天就让星际2正面突破。我可是一个猛男！猛男怎么会腰疼！&lt;/p&gt;
&lt;p&gt;萌新的迷茫和无助也回来了，我又开始搜文献看帖和视频，进行了几天的病友情报收集：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;脊椎就像底大杠，很不起眼，但也有使用寿命，也需要保养；&lt;/li&gt;
&lt;li&gt;脊椎附近的组织类似韧带，供血较少，尤其9-11月供暖前天气寒冷会进一步减少组织供血；&lt;/li&gt;
&lt;li&gt;供血少意味着恢复慢，一旦有损伤就应该立刻静养；&lt;/li&gt;
&lt;li&gt;恢复之后再加强锻炼，并改掉损伤脊椎的姿势。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所有的雷我又趟了一遍，猛男不是无敌，猛男更应该爱惜自己的肉体。这几天状态恢复了些，打算静卧两周再去铁房老老实实做萌新。&lt;/p&gt;
&lt;p&gt;我这个人啊，上了年纪就老想找机会证明自己还年轻。但是，人和人的天赋是有差距的，尤其是精力的差距，基本决定了功业的差距，特别是我这种死宅，精力槽基本为0，更应该看清自己假猛男真萌新的本质。正所谓自知者明，先为不可胜以待敌之可胜，不作死就不会死。&lt;/p&gt;
&lt;p&gt;星际2是个好游戏，尤其是哲学这一块，神族粪坑流、虫族泉水沟、人族城市化，无一不是孙子兵法的最佳实践。我还是太萌新了，只玩到操作层和战术层，后面的战略层和哲学层完全没有入门。以后的日子更应该谨慎的开发腰、星际、健身房的更深层次内涵。&lt;/p&gt;
&lt;p&gt;我一定要时刻提醒自己永远都只能是一个萌新这一事实，不论什么岁数，什么状态，什么境界，都要坚定的走在成为猛男的道路上，风霜雨雪，披荆斩棘，老兵不死，stay young, stay alive。&lt;/p&gt;
&lt;p&gt;另外：大家秋冬季节一定要保护好自己的小腰！&lt;/p&gt;
&lt;p&gt;完。&lt;/p&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 06 - UART的链式加载</title><link>https://zlotus.github.io/2021/08/30/writing-os-in-rust-on-rpi-06/</link><guid isPermaLink="true">https://zlotus.github.io/2021/08/30/writing-os-in-rust-on-rpi-06/</guid><description>使用Rust在树莓派上编写操作系统 - 05 - UART的链式加载</description><pubDate>Mon, 30 Aug 2021 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/06_uart_chainloader/README.md&quot;&gt;Tutorial 06 - UART Chainloader&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用SD卡上的镜像启动树莓派感觉很棒，但如果对每个新的二进制文件，都需要手动将其放在SD卡上就会非常麻烦。因此，本章我们将编写一个&lt;a href=&quot;https://en.wikipedia.org/wiki/Chain_loading&quot;&gt;链式加载器&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;这是我们最后一次手动拷贝镜像到SD卡上。后面的每章教程都会在&lt;code&gt;Makefile&lt;/code&gt;中提供一个&lt;code&gt;chainboot&lt;/code&gt;目标，以便通过UART加载内核。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;注意&lt;/h2&gt;
&lt;p&gt;请注意，若仅查看源码差异，则很难掌握本章教程中出现的一些新功能。&lt;/p&gt;
&lt;p&gt;关键就在&lt;code&gt;boot.s&lt;/code&gt;中，我们正在编写一段&lt;a href=&quot;https://en.wikipedia.org/wiki/Position-independent_code&quot;&gt;位置无关代码&lt;/a&gt;，代码能够自动确定固件从什么位置(&lt;code&gt;0x8_0000&lt;/code&gt;)加载二进制文件，以及应当链接到什么位置(&lt;code&gt;0x200_0000&lt;/code&gt;，请参阅&lt;code&gt;link.ld&lt;/code&gt;)。然后二进制文件将自身从加载地址复制到链接地址（即“重定位”自身），再跳转到重定位版本的&lt;code&gt;_start_rust()&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;由于链式加载器自己已经“让出位置”，它现在就可以从UART接收另一个内核二进制文件，并将其复制到树莓派固件的标准加载地址&lt;code&gt;0x8_0000&lt;/code&gt;。最后，它跳转到&lt;code&gt;0x8_0000&lt;/code&gt;再透明的执行新加载的二进制文件，仿佛该文件是从SD卡中加载似的。&lt;/p&gt;
&lt;p&gt;请耐心等待，等我一有时间，就会将这个过程写成详尽的文档。不过目前，请参阅本章教程看做一个方便的启动器，该驱动器允许我们快速启动后续教程章节中新的内核二进制文件。&lt;/p&gt;
&lt;h2&gt;安装并测试&lt;/h2&gt;
&lt;p&gt;我们的链式加载器叫做&lt;code&gt;MiniLoad&lt;/code&gt;，其灵感来自&lt;a href=&quot;https://github.com/mrvn/raspbootin&quot;&gt;raspbootin&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在本章教程中试用&lt;code&gt;MiniLoad&lt;/code&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据目标硬件，运行：&lt;code&gt;make&lt;/code&gt;或者&lt;code&gt;BSP=rpi4 make&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;kernel8.img&lt;/code&gt;复制到SD卡，并将SD卡插回到树莓派。&lt;/li&gt;
&lt;li&gt;根据目标硬件，运行&lt;code&gt;make chainboot&lt;/code&gt;或者&lt;code&gt;BSP=rpi4 make chainboot&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;连接USB串口线到宿主PC。
&lt;ul&gt;
&lt;li&gt;按照本项目的&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/README.md#-usb-serial-output&quot;&gt;根&lt;code&gt;README&lt;/code&gt;&lt;/a&gt;接线。&lt;/li&gt;
&lt;li&gt;一定&lt;strong&gt;确保没有&lt;/strong&gt;连接USB串口的电源引脚。只连接了RX/TX和GND。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;为树莓派连接(USB)电源。&lt;/li&gt;
&lt;li&gt;观察加载程序通过UART获取内核：&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;❗&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;make chainboot&lt;/code&gt;的默认串行设备名称为&lt;code&gt;/dev/ttyUSB0&lt;/code&gt;。对于不同操作系统的宿主机，设备名称可能会有所不同。例如，在&lt;code&gt;macOS&lt;/code&gt;上，名称可能类似于&lt;code&gt;/dev/tty.usbserial-0001&lt;/code&gt;。这种情况下，需要明确给出名称：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$ DEV_SERIAL=/dev/tty.usbserial-0001 make chainboot
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$ make chainboot
[...]
Minipush 1.0

[MP] ⏳ Waiting for /dev/ttyUSB0
[MP] ✅ Serial connected
[MP] 🔌 Please power the target now
 __  __ _      _ _                 _
|  \/  (_)_ _ (_) |   ___  __ _ __| |
| |\/| | | &apos; \| | |__/ _ \/ _` / _` |
|_|  |_|_|_||_|_|____\___/\__,_\__,_|

           Raspberry Pi 3

[ML] Requesting binary
[MP] ⏩ Pushing 6 KiB ==========================================🦀 100% 0 KiB/s Time: 00:00:00
[ML] Loaded! Executing the payload now

[0] mingo version 0.5.0
[1] Booting on: Raspberry Pi 3
[2] Drivers loaded:
      1. BCM GPIO
      2. BCM PL011 UART
[3] Chars written: 117
[4] Echoing input now
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实验&lt;/h3&gt;
&lt;p&gt;本章教程中的&lt;code&gt;Makefile&lt;/code&gt;有一个额外的目标——&lt;code&gt;qemuasm&lt;/code&gt;——可以让您更好地观察内核内核在重定位后，是如何从加载地址&lt;code&gt;0x80_XXX&lt;/code&gt;跳转到位于&lt;code&gt;0x0200_0XXX&lt;/code&gt;的重定位代码处的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ make qemuasm
[...]
N:
0x00080030:  58000140  ldr      x0, #0x80058
0x00080034:  9100001f  mov      sp, x0
0x00080038:  58000141  ldr      x1, #0x80060
0x0008003c:  d61f0020  br       x1

----------------
IN:
0x02000070:  9400044c  bl       #0x20011a0

----------------
IN:
0x020011a0:  90000008  adrp     x8, #0x2001000
0x020011a4:  90000009  adrp     x9, #0x2001000
0x020011a8:  f9446508  ldr      x8, [x8, #0x8c8]
0x020011ac:  f9446929  ldr      x9, [x9, #0x8d0]
0x020011b0:  eb08013f  cmp      x9, x8
0x020011b4:  54000109  b.ls     #0x20011d4
[...]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;与上一章代码的区别&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;diff -uNr 05_drivers_gpio_uart/Cargo.toml 06_uart_chainloader/Cargo.toml
--- 05_drivers_gpio_uart/Cargo.toml
+++ 06_uart_chainloader/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = &quot;mingo&quot;
-version = &quot;0.5.0&quot;
+version = &quot;0.6.0&quot;
 authors = [&quot;Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;&quot;]
 edition = &quot;2021&quot;

Binary files 05_drivers_gpio_uart/demo_payload_rpi3.img and 06_uart_chainloader/demo_payload_rpi3.img differ
Binary files 05_drivers_gpio_uart/demo_payload_rpi4.img and 06_uart_chainloader/demo_payload_rpi4.img differ

diff -uNr 05_drivers_gpio_uart/Makefile 06_uart_chainloader/Makefile
--- 05_drivers_gpio_uart/Makefile
+++ 06_uart_chainloader/Makefile
@@ -23,27 +23,29 @@

 # BSP-specific arguments.
 ifeq ($(BSP),rpi3)
-    TARGET            = aarch64-unknown-none-softfloat
-    KERNEL_BIN        = kernel8.img
-    QEMU_BINARY       = qemu-system-aarch64
-    QEMU_MACHINE_TYPE = raspi3
-    QEMU_RELEASE_ARGS = -serial stdio -display none
-    OBJDUMP_BINARY    = aarch64-none-elf-objdump
-    NM_BINARY         = aarch64-none-elf-nm
-    READELF_BINARY    = aarch64-none-elf-readelf
-    LINKER_FILE       = src/bsp/raspberrypi/link.ld
-    RUSTC_MISC_ARGS   = -C target-cpu=cortex-a53
+    TARGET                 = aarch64-unknown-none-softfloat
+    KERNEL_BIN             = kernel8.img
+    QEMU_BINARY            = qemu-system-aarch64
+    QEMU_MACHINE_TYPE      = raspi3
+    QEMU_RELEASE_ARGS      = -serial stdio -display none
+    OBJDUMP_BINARY         = aarch64-none-elf-objdump
+    NM_BINARY              = aarch64-none-elf-nm
+    READELF_BINARY         = aarch64-none-elf-readelf
+    LINKER_FILE            = src/bsp/raspberrypi/link.ld
+    RUSTC_MISC_ARGS        = -C target-cpu=cortex-a53
+    CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi3.img
 else ifeq ($(BSP),rpi4)
-    TARGET            = aarch64-unknown-none-softfloat
-    KERNEL_BIN        = kernel8.img
-    QEMU_BINARY       = qemu-system-aarch64
-    QEMU_MACHINE_TYPE =
-    QEMU_RELEASE_ARGS = -serial stdio -display none
-    OBJDUMP_BINARY    = aarch64-none-elf-objdump
-    NM_BINARY         = aarch64-none-elf-nm
-    READELF_BINARY    = aarch64-none-elf-readelf
-    LINKER_FILE       = src/bsp/raspberrypi/link.ld
-    RUSTC_MISC_ARGS   = -C target-cpu=cortex-a72
+    TARGET                 = aarch64-unknown-none-softfloat
+    KERNEL_BIN             = kernel8.img
+    QEMU_BINARY            = qemu-system-aarch64
+    QEMU_MACHINE_TYPE      =
+    QEMU_RELEASE_ARGS      = -serial stdio -display none
+    OBJDUMP_BINARY         = aarch64-none-elf-objdump
+    NM_BINARY              = aarch64-none-elf-nm
+    READELF_BINARY         = aarch64-none-elf-readelf
+    LINKER_FILE            = src/bsp/raspberrypi/link.ld
+    RUSTC_MISC_ARGS        = -C target-cpu=cortex-a72
+    CHAINBOOT_DEMO_PAYLOAD = demo_payload_rpi4.img
 endif

 QEMU_MISSING_STRING = &quot;This board is not yet supported for QEMU.&quot;
@@ -75,8 +77,8 @@
     -O binary

 EXEC_QEMU          = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE)
-EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb
-EXEC_MINITERM      = ruby ../common/serial/miniterm.rb
+EXEC_TEST_MINIPUSH = ruby tests/chainboot_test.rb
+EXEC_MINIPUSH      = ruby ../common/serial/minipush.rb

 ##------------------------------------------------------------------------------
 ## Dockerization
@@ -95,7 +97,7 @@
 ifeq ($(shell uname -s),Linux)
     DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV)

-    DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE)
+    DOCKER_CHAINBOOT = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE)
 endif


@@ -103,7 +105,7 @@
 ##--------------------------------------------------------------------------------------------------
 ## Targets
 ##--------------------------------------------------------------------------------------------------
-.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu miniterm clippy clean readelf objdump nm check
+.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu chainboot clippy clean readelf objdump nm check

 all: $(KERNEL_BIN)

@@ -132,7 +134,7 @@
 ##------------------------------------------------------------------------------
 ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board.

-qemu:
+qemu qemuasm:
 	$(call colorecho, &quot;\n$(QEMU_MISSING_STRING)&quot;)

 else # QEMU is supported.
@@ -140,13 +142,18 @@
 qemu: $(KERNEL_BIN)
 	$(call colorecho, &quot;\nLaunching QEMU&quot;)
 	@$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN)
+
+qemuasm: $(KERNEL_BIN)
+	$(call colorecho, &quot;\nLaunching QEMU with ASM output&quot;)
+	@$(DOCKER_QEMU) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN) -d in_asm
+
 endif

 ##------------------------------------------------------------------------------
-## Connect to the target&apos;s serial
+## Push the kernel to the real HW target
 ##------------------------------------------------------------------------------
-miniterm:
-	@$(DOCKER_MINITERM) $(EXEC_MINITERM) $(DEV_SERIAL)
+chainboot: $(KERNEL_BIN)
+	@$(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(CHAINBOOT_DEMO_PAYLOAD)

 ##------------------------------------------------------------------------------
 ## Run clippy
@@ -210,7 +217,8 @@
 ##------------------------------------------------------------------------------
 test_boot: $(KERNEL_BIN)
 	$(call colorecho, &quot;\nBoot test - $(BSP)&quot;)
-	@$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN)
+	@$(DOCKER_TEST) $(EXEC_TEST_MINIPUSH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) \
+		-kernel $(KERNEL_BIN) $(CHAINBOOT_DEMO_PAYLOAD)

 test: test_boot


diff -uNr 05_drivers_gpio_uart/src/_arch/aarch64/cpu/boot.s 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s
--- 05_drivers_gpio_uart/src/_arch/aarch64/cpu/boot.s
+++ 06_uart_chainloader/src/_arch/aarch64/cpu/boot.s
@@ -18,6 +18,17 @@
 	add	\register, \register, #:lo12:\symbol
 .endm

+// Load the address of a symbol into a register, absolute.
+//
+// # Resources
+//
+// - https://sourceware.org/binutils/docs-2.36/as/AArch64_002dRelocations.html
+.macro ADR_ABS register, symbol
+	movz	\register, #:abs_g2:\symbol
+	movk	\register, #:abs_g1_nc:\symbol
+	movk	\register, #:abs_g0_nc:\symbol
+.endm
+
 .equ _core_id_mask, 0b11

 //--------------------------------------------------------------------------------------------------
@@ -39,23 +50,35 @@
 	// If execution reaches here, it is the boot core.

 	// Initialize DRAM.
-	ADR_REL	x0, __bss_start
-	ADR_REL x1, __bss_end_exclusive
+	ADR_ABS	x0, __bss_start
+	ADR_ABS x1, __bss_end_exclusive

 .L_bss_init_loop:
 	cmp	x0, x1
-	b.eq	.L_prepare_rust
+	b.eq	.L_relocate_binary
 	stp	xzr, xzr, [x0], #16
 	b	.L_bss_init_loop

+	// Next, relocate the binary.
+.L_relocate_binary:
+	ADR_REL	x0, __binary_nonzero_start         // The address the binary got loaded to.
+	ADR_ABS	x1, __binary_nonzero_start         // The address the binary was linked to.
+	ADR_ABS	x2, __binary_nonzero_end_exclusive
+
+.L_copy_loop:
+	ldr	x3, [x0], #8
+	str	x3, [x1], #8
+	cmp	x1, x2
+	b.lo	.L_copy_loop
+
 	// Prepare the jump to Rust code.
-.L_prepare_rust:
 	// Set the stack pointer.
-	ADR_REL	x0, __boot_core_stack_end_exclusive
+	ADR_ABS	x0, __boot_core_stack_end_exclusive
 	mov	sp, x0

-	// Jump to Rust code.
-	b	_start_rust
+	// Jump to the relocated Rust code.
+	ADR_ABS	x1, _start_rust
+	br	x1

 	// Infinitely wait for events (aka &quot;park the core&quot;).
 .L_parking_loop:

diff -uNr 05_drivers_gpio_uart/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
--- 05_drivers_gpio_uart/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
+++ 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
@@ -148,7 +148,7 @@
         // Make an educated guess for a good delay value (Sequence described in the BCM2837
         // peripherals PDF).
         //
-        // - According to Wikipedia, the fastest Pi3 clocks around 1.4 GHz.
+        // - According to Wikipedia, the fastest RPi4 clocks around 1.5 GHz.
         // - The Linux 2837 GPIO driver waits 1 µs between the steps.
         //
         // So lets try to be on the safe side and default to 2000 cycles, which would equal 1 µs

diff -uNr 05_drivers_gpio_uart/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
--- 05_drivers_gpio_uart/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
+++ 06_uart_chainloader/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
@@ -278,7 +278,7 @@
     }

     /// Retrieve a character.
-    fn read_char_converting(&amp;amp;mut self, blocking_mode: BlockingMode) -&amp;gt; Option&amp;lt;char&amp;gt; {
+    fn read_char(&amp;amp;mut self, blocking_mode: BlockingMode) -&amp;gt; Option&amp;lt;char&amp;gt; {
         // If RX FIFO is empty,
         if self.registers.FR.matches_all(FR::RXFE::SET) {
             // immediately return in non-blocking mode.
@@ -293,12 +293,7 @@
         }

         // Read one character.
-        let mut ret = self.registers.DR.get() as u8 as char;
-
-        // Convert carrige return to newline.
-        if ret == &apos;\r&apos; {
-            ret = &apos;\n&apos;
-        }
+        let ret = self.registers.DR.get() as u8 as char;

         // Update statistics.
         self.chars_read += 1;
@@ -378,14 +373,14 @@
 impl console::interface::Read for PL011Uart {
     fn read_char(&amp;amp;self) -&amp;gt; char {
         self.inner
-            .lock(|inner| inner.read_char_converting(BlockingMode::Blocking).unwrap())
+            .lock(|inner| inner.read_char(BlockingMode::Blocking).unwrap())
     }

     fn clear_rx(&amp;amp;self) {
         // Read from the RX FIFO until it is indicating empty.
         while self
             .inner
-            .lock(|inner| inner.read_char_converting(BlockingMode::NonBlocking))
+            .lock(|inner| inner.read_char(BlockingMode::NonBlocking))
             .is_some()
         {}
     }

diff -uNr 05_drivers_gpio_uart/src/bsp/raspberrypi/link.ld 06_uart_chainloader/src/bsp/raspberrypi/link.ld
--- 05_drivers_gpio_uart/src/bsp/raspberrypi/link.ld
+++ 06_uart_chainloader/src/bsp/raspberrypi/link.ld
@@ -3,8 +3,6 @@
  * Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
  */

-__rpi_phys_dram_start_addr = 0;
-
 /* The physical address at which the the kernel binary will be loaded by the Raspberry&apos;s firmware */
 __rpi_phys_binary_load_addr = 0x80000;

@@ -28,7 +26,8 @@

 SECTIONS
 {
-    . =  __rpi_phys_dram_start_addr;
+    /* Set the link address to 32 MiB */
+    . = 0x2000000;

     /***********************************************************************************************
     * Boot Core Stack
@@ -45,6 +44,7 @@
     /***********************************************************************************************
     * Code + RO Data + Global Offset Table
     ***********************************************************************************************/
+    __binary_nonzero_start = .;
     .text :
     {
         KEEP(*(.text._start))
@@ -61,6 +61,10 @@
     ***********************************************************************************************/
     .data : { *(.data*) } :segment_data

+    /* Fill up to 8 byte, b/c relocating the binary is done in u64 chunks */
+    . = ALIGN(8);
+    __binary_nonzero_end_exclusive = .;
+
     /* Section is zeroed in pairs of u64. Align start and end to 16 bytes */
     .bss (NOLOAD) : ALIGN(16)
     {

diff -uNr 05_drivers_gpio_uart/src/bsp/raspberrypi/memory.rs 06_uart_chainloader/src/bsp/raspberrypi/memory.rs
--- 05_drivers_gpio_uart/src/bsp/raspberrypi/memory.rs
+++ 06_uart_chainloader/src/bsp/raspberrypi/memory.rs
@@ -11,9 +11,10 @@
 /// The board&apos;s physical memory map.
 #[rustfmt::skip]
 pub(super) mod map {
+    pub const BOARD_DEFAULT_LOAD_ADDRESS: usize =        0x8_0000;

-    pub const GPIO_OFFSET:         usize = 0x0020_0000;
-    pub const UART_OFFSET:         usize = 0x0020_1000;
+    pub const GPIO_OFFSET:                usize =        0x0020_0000;
+    pub const UART_OFFSET:                usize =        0x0020_1000;

     /// Physical devices.
     #[cfg(feature = &quot;bsp_rpi3&quot;)]
@@ -35,3 +36,13 @@
         pub const PL011_UART_START: usize = START + UART_OFFSET;
     }
 }
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// The address on which the Raspberry firmware loads every binary by default.
+#[inline(always)]
+pub fn board_default_load_addr() -&amp;gt; *const u64 {
+    map::BOARD_DEFAULT_LOAD_ADDRESS as _
+}

diff -uNr 05_drivers_gpio_uart/src/main.rs 06_uart_chainloader/src/main.rs
--- 05_drivers_gpio_uart/src/main.rs
+++ 06_uart_chainloader/src/main.rs
@@ -141,38 +141,56 @@
     kernel_main()
 }

+const MINILOAD_LOGO: &amp;amp;str = r#&quot;
+ __  __ _      _ _                 _
+|  \/  (_)_ _ (_) |   ___  __ _ __| |
+| |\/| | | &apos; \| | |__/ _ \/ _` / _` |
+|_|  |_|_|_||_|_|____\___/\__,_\__,_|
+&quot;#;
+
 /// The main function running after the early init.
 fn kernel_main() -&amp;gt; ! {
     use bsp::console::console;
     use console::interface::All;
-    use driver::interface::DriverManager;

-    println!(
-        &quot;[0] {} version {}&quot;,
-        env!(&quot;CARGO_PKG_NAME&quot;),
-        env!(&quot;CARGO_PKG_VERSION&quot;)
-    );
-    println!(&quot;[1] Booting on: {}&quot;, bsp::board_name());
-
-    println!(&quot;[2] Drivers loaded:&quot;);
-    for (i, driver) in bsp::driver::driver_manager()
-        .all_device_drivers()
-        .iter()
-        .enumerate()
-    {
-        println!(&quot;      {}. {}&quot;, i + 1, driver.compatible());
+    println!(&quot;{}&quot;, MINILOAD_LOGO);
+    println!(&quot;{:^37}&quot;, bsp::board_name());
+    println!();
+    println!(&quot;[ML] Requesting binary&quot;);
+    console().flush();
+
+    // Discard any spurious received characters before starting with the loader protocol.
+    console().clear_rx();
+
+    // Notify `Minipush` to send the binary.
+    for _ in 0..3 {
+        console().write_char(3 as char);
     }

-    println!(
-        &quot;[3] Chars written: {}&quot;,
-        bsp::console::console().chars_written()
-    );
-    println!(&quot;[4] Echoing input now&quot;);
+    // Read the binary&apos;s size.
+    let mut size: u32 = u32::from(console().read_char() as u8);
+    size |= u32::from(console().read_char() as u8) &amp;lt;&amp;lt; 8;
+    size |= u32::from(console().read_char() as u8) &amp;lt;&amp;lt; 16;
+    size |= u32::from(console().read_char() as u8) &amp;lt;&amp;lt; 24;

-    // Discard any spurious received characters before going into echo mode.
-    console().clear_rx();
-    loop {
-        let c = bsp::console::console().read_char();
-        bsp::console::console().write_char(c);
+    // Trust it&apos;s not too big.
+    console().write_char(&apos;O&apos;);
+    console().write_char(&apos;K&apos;);
+
+    let kernel_addr: *mut u8 = bsp::memory::board_default_load_addr() as *mut u8;
+    unsafe {
+        // Read the kernel byte by byte.
+        for i in 0..size {
+            core::ptr::write_volatile(kernel_addr.offset(i as isize), console().read_char() as u8)
+        }
     }
+
+    println!(&quot;[ML] Loaded! Executing the payload now\n&quot;);
+    console().flush();
+
+    // Use black magic to create a function pointer.
+    let kernel: fn() -&amp;gt; ! = unsafe { core::mem::transmute(kernel_addr) };
+
+    // Jump to loaded kernel!
+    kernel()
 }

diff -uNr 05_drivers_gpio_uart/tests/boot_test_string.rb 06_uart_chainloader/tests/boot_test_string.rb
--- 05_drivers_gpio_uart/tests/boot_test_string.rb
+++ 06_uart_chainloader/tests/boot_test_string.rb
@@ -1,3 +0,0 @@
-# frozen_string_literal: true
-
-EXPECTED_PRINT = &apos;Echoing input now&apos;

diff -uNr 05_drivers_gpio_uart/tests/chainboot_test.rb 06_uart_chainloader/tests/chainboot_test.rb
--- 05_drivers_gpio_uart/tests/chainboot_test.rb
+++ 06_uart_chainloader/tests/chainboot_test.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+# SPDX-License-Identifier: MIT OR Apache-2.0
+#
+# Copyright (c) 2020-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+require_relative &apos;../../common/serial/minipush&apos;
+require_relative &apos;../../common/tests/boot_test&apos;
+require &apos;pty&apos;
+
+# Match for the last print that &apos;demo_payload_rpiX.img&apos; produces.
+EXPECTED_PRINT = &apos;Echoing input now&apos;
+
+# Extend BootTest so that it listens on the output of a MiniPush instance, which is itself connected
+# to a QEMU instance instead of a real HW.
+class ChainbootTest &amp;lt; BootTest
+    MINIPUSH = &apos;../common/serial/minipush.rb&apos;
+    MINIPUSH_POWER_TARGET_REQUEST = &apos;Please power the target now&apos;
+
+    def initialize(qemu_cmd, payload_path)
+        super(qemu_cmd, EXPECTED_PRINT)
+
+        @test_name = &apos;Boot test using Minipush&apos;
+
+        @payload_path = payload_path
+    end
+
+    private
+
+    # override
+    def post_process_and_add_output(output)
+        temp = output.join.split(&quot;\r\n&quot;)
+
+        # Should a line have solo carriage returns, remove any overridden parts of the string.
+        temp.map! { |x| x.gsub(/.*\r/, &apos;&apos;) }
+
+        @test_output += temp
+    end
+
+    def wait_for_minipush_power_request(mp_out)
+        output = []
+        Timeout.timeout(MAX_WAIT_SECS) do
+            loop do
+                output &amp;lt;&amp;lt; mp_out.gets
+                break if output.last.include?(MINIPUSH_POWER_TARGET_REQUEST)
+            end
+        end
+    rescue Timeout::Error
+        @test_error = &apos;Timed out waiting for power request&apos;
+    rescue StandardError =&amp;gt; e
+        @test_error = e.message
+    ensure
+        post_process_and_add_output(output)
+    end
+
+    # override
+    def setup
+        pty_main, pty_secondary = PTY.open
+        mp_out, _mp_in = PTY.spawn(&quot;ruby #{MINIPUSH} #{pty_secondary.path} #{@payload_path}&quot;)
+
+        # Wait until MiniPush asks for powering the target.
+        wait_for_minipush_power_request(mp_out)
+
+        # Now is the time to start QEMU with the chainloader binary. QEMU&apos;s virtual tty is connected
+        # to the MiniPush instance spawned above, so that the two processes talk to each other.
+        Process.spawn(@qemu_cmd, in: pty_main, out: pty_main)
+
+        # The remainder of the test is done by the parent class&apos; run_concrete_test, which listens on
+        # @qemu_serial. Hence, point it to MiniPush&apos;s output.
+        @qemu_serial = mp_out
+    end
+end
+
+##--------------------------------------------------------------------------------------------------
+## Execution starts here
+##--------------------------------------------------------------------------------------------------
+payload_path = ARGV.pop
+qemu_cmd = ARGV.join(&apos; &apos;)
+
+ChainbootTest.new(qemu_cmd, payload_path).run

diff -uNr 05_drivers_gpio_uart/update.sh 06_uart_chainloader/update.sh
--- 05_drivers_gpio_uart/update.sh
+++ 06_uart_chainloader/update.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+cd ../05_drivers_gpio_uart
+BSP=rpi4 make
+cp kernel8.img ../06_uart_chainloader/demo_payload_rpi4.img
+make
+cp kernel8.img ../06_uart_chainloader/demo_payload_rpi3.img
+rm kernel8.img
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 05 - 驱动GPIO和UART</title><link>https://zlotus.github.io/2021/08/08/writing-os-in-rust-on-rpi-05/</link><guid isPermaLink="true">https://zlotus.github.io/2021/08/08/writing-os-in-rust-on-rpi-05/</guid><description>使用Rust在树莓派上编写操作系统 - 05 - 驱动GPIO和UART</description><pubDate>Sun, 08 Aug 2021 22:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/05_drivers_gpio_uart/README.md&quot;&gt;Tutorial 05 - Drivers: GPIO and UART&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;&amp;lt;!--
10，楚国800年
9，监守自盗
8，超级中国
7，伟大诗人杜甫
6，第四公民
5，大国崛起
4，文明
3，天启，第二次世界大战
2，伟大的卫国战争
1，河西走廊
--&amp;gt;&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;既然我们在上一章教程中已经准备好了基础设施——启用了安全全局变量——那么现在就可以为第一个真实设备添加驱动程序了。&lt;/li&gt;
&lt;li&gt;现在我们可以离开神奇的QEMU控制台，来使用真正的UART设备了，就像正经的嵌入式开发者那样。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;需要注意的新增代码&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;这是我们首次在真实的硬件上运行代码。
&lt;ul&gt;
&lt;li&gt;因此，现在的构建编译分为&lt;strong&gt;RPi3&lt;/strong&gt;和&lt;strong&gt;RPi4&lt;/strong&gt;两个版本。&lt;/li&gt;
&lt;li&gt;默认情况下，所有&lt;code&gt;Makefile&lt;/code&gt;项都以&lt;strong&gt;RPi3&lt;/strong&gt;为目标进行构建。&lt;/li&gt;
&lt;li&gt;如果需要以&lt;strong&gt;RPi4&lt;/strong&gt;为构建目标，需要在每个构建项前面添加&lt;code&gt;BSP=rpi4&lt;/code&gt;。例如：
-&lt;code&gt;BSP=rpi4 make&lt;/code&gt;
-&lt;code&gt;BSP=rpi4 make doc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不过QEMU目前还不支持&lt;strong&gt;RPi4&lt;/strong&gt;，因此&lt;code&gt;BSP=rpi4 make qemu&lt;/code&gt;将无法正常工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;添加了&lt;code&gt;driver::interface::DeviceDriver&lt;/code&gt;trait，以从内核代码中抽象出&lt;code&gt;BSP&lt;/code&gt;驱动程序实现。&lt;/li&gt;
&lt;li&gt;驱动程序存储在&lt;code&gt;src/bsp/device_driver&lt;/code&gt;中，并且可以在&lt;code&gt;BSP&lt;/code&gt;之间重用。
&lt;ul&gt;
&lt;li&gt;我们引入了&lt;code&gt;GPIO&lt;/code&gt;驱动，实现了对RPi的PL011 UART引脚的多路复用（pinmux即，将信号从SoC内部路由到实际的硬件引脚）。
&lt;ul&gt;
&lt;li&gt;请注意此驱动程序如何区分&lt;strong&gt;RPi3&lt;/strong&gt;和&lt;strong&gt;RPi4&lt;/strong&gt;。由于二者使用了不同的硬件，所以我们也需要在软件中考虑这种不同。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;其中的关键在于&lt;code&gt;PL011Uart&lt;/code&gt;驱动：它实现了&lt;code&gt;console::interface::*&lt;/code&gt;trait，而且从现在开始，它被用作主系统的控制台输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/bsp/raspberrypi/memory.rs&lt;/code&gt;中的&lt;code&gt;BSP&lt;/code&gt;现在具有一个内存映射。在特定情况下，映射包含了树莓派的&lt;code&gt;MMIO&lt;/code&gt;地址，用于实例化相应设备的驱动程序。&lt;/li&gt;
&lt;li&gt;我们也修改了&lt;code&gt;panic!&lt;/code&gt;处理程序，以使其不再依赖&lt;code&gt;println!&lt;/code&gt;，panic现在使用的全局共享实例&lt;code&gt;UART&lt;/code&gt;，不过遇到异常时，该实例有可能正处于锁定状态（由于目前使用的是&lt;code&gt;NullLock&lt;/code&gt;实现，并不会出现这种锁的问题，但是如果是使用一个真实锁的实现，将会出现这种问题）。
&lt;ul&gt;
&lt;li&gt;所以，为了避免变量被锁占用，现在的代码将会创建一个新的UART驱动实例，重新初始化设备，并使用这个新实例进行打印。这增加了系统在挂起之前能够打印出最后的重要消息的机会。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;从SD卡启动&lt;/h2&gt;
&lt;p&gt;需要注意的是，在使用SD卡启动时，RPi3和RPi4的准备步骤有所不同。&lt;/p&gt;
&lt;h3&gt;通用步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;创建一个名为&lt;code&gt;boot&lt;/code&gt;的&lt;code&gt;FAT32&lt;/code&gt;格式分区。&lt;/li&gt;
&lt;li&gt;在SD卡上，新建一个名为&lt;code&gt;config.txt&lt;/code&gt;的文件，内容如下：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;arm_64bit=1
init_uart_clock=48000000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Pi 3&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;将&lt;a href=&quot;https://github.com/raspberrypi/firmware/tree/master/boot&quot;&gt;Raspberry Pi固件仓库&lt;/a&gt;中的下列文件复制到SD卡上：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/raspberrypi/firmware/raw/master/boot/bootcode.bin&quot;&gt;&lt;code&gt;bootcode.bin&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/raspberrypi/firmware/raw/master/boot/fixup.dat&quot;&gt;&lt;code&gt;fixup.dat&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/raspberrypi/firmware/raw/master/boot/start.elf&quot;&gt;&lt;code&gt;start.elf&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;执行&lt;code&gt;make&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;Pi 4&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;将&lt;a href=&quot;https://github.com/raspberrypi/firmware/tree/master/boot&quot;&gt;Raspberry Pi固件仓库&lt;/a&gt;中的下列文件复制到SD卡上：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/raspberrypi/firmware/raw/master/boot/fixup4.dat&quot;&gt;&lt;code&gt;fixup4.dat&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/raspberrypi/firmware/raw/master/boot/start4.elf&quot;&gt;&lt;code&gt;start4.elf&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/raspberrypi/firmware/raw/master/boot/bcm2711-rpi-4-b.dtb&quot;&gt;&lt;code&gt;bcm2711-rpi-4-b.dtb&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;执行&lt;code&gt;BSP=rpi4 make&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: 如果上述步骤在你的RPi4上不起作用，请尝试将SD卡上的&lt;code&gt;start4.elf&lt;/code&gt;重命名为&lt;code&gt;start.elf&lt;/code&gt;（即去掉&lt;code&gt;4&lt;/code&gt;）。&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;通用步骤（续）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;将文件&lt;code&gt;kernel8.img&lt;/code&gt;复制到SD卡上，插回RPi。&lt;/li&gt;
&lt;li&gt;执行&lt;code&gt;miniterm&lt;/code&gt;项，就会在主机上打开UART设备：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ make miniterm
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;❗&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;Miniterm&lt;/code&gt;的默认串行设备名称为&lt;code&gt;/dev/ttyUSB0&lt;/code&gt;。对于不同操作系统的宿主机，设备名称可能会有所不同。例如，在&lt;code&gt;macOS&lt;/code&gt;上，名称可能类似于&lt;code&gt;/dev/tty.usbserial-0001&lt;/code&gt;。这种情况下，需要明确给出名称：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;$ DEV_SERIAL=/dev/tty.usbserial-0001 make miniterm
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;将USB串口线连接到宿主机PC。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;按照本教程&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/README.md#-usb-serial-output&quot;&gt;初始的README中的接线图&lt;/a&gt;连接引脚。&lt;/li&gt;
&lt;li&gt;请确认没有连接USB串口线的电源引脚，仅连接了RX/TX和GND三个引脚。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;将RPi接上电源电源线（miniUSB电源）并观察输出：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Miniterm 1.0

[MT] ⏳ Waiting for /dev/ttyUSB0
[MT] ✅ Serial connected
[0] mingo version 0.5.0
[1] Booting on: Raspberry Pi 3
[2] Drivers loaded:
      1. BCM GPIO
      2. BCM PL011 UART
[3] Chars written: 117
[4] Echoing input now
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用&amp;lt;kbd&amp;gt;ctrl-c&amp;lt;/kbd&amp;gt;键退出。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;与上一章代码的区别&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;diff -uNr 04_safe_globals/Cargo.toml 05_drivers_gpio_uart/Cargo.toml
--- 04_safe_globals/Cargo.toml
+++ 05_drivers_gpio_uart/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = &quot;mingo&quot;
-version = &quot;0.4.0&quot;
+version = &quot;0.5.0&quot;
 authors = [&quot;Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;&quot;]
 edition = &quot;2018&quot;

@@ -9,8 +9,8 @@

 [features]
 default = []
-bsp_rpi3 = []
-bsp_rpi4 = []
+bsp_rpi3 = [&quot;tock-registers&quot;]
+bsp_rpi4 = [&quot;tock-registers&quot;]

 [[bin]]
 name = &quot;kernel&quot;
@@ -22,6 +22,9 @@

 [dependencies]

+# Optional dependencies
+tock-registers = { version = &quot;0.7.x&quot;, default-features = false, features = [&quot;register_types&quot;], optional = true }
+
 # Platform specific dependencies
 [target.&apos;cfg(target_arch = &quot;aarch64&quot;)&apos;.dependencies]
 cortex-a = { version = &quot;6.x.x&quot; }

diff -uNr 04_safe_globals/Makefile 05_drivers_gpio_uart/Makefile
--- 04_safe_globals/Makefile
+++ 05_drivers_gpio_uart/Makefile
@@ -11,6 +11,9 @@
 # Default to the RPi3.
 BSP ?= rpi3

+# Default to a serial device name that is common in Linux.
+DEV_SERIAL ?= /dev/ttyUSB0
+


 ##--------------------------------------------------------------------------------------------------
@@ -72,6 +75,7 @@

 EXEC_QEMU          = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE)
 EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb
+EXEC_MINITERM      = ruby ../common/serial/miniterm.rb

 ##------------------------------------------------------------------------------
 ## Dockerization
@@ -80,17 +84,25 @@
 DOCKER_CMD            = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial
 DOCKER_CMD_INTERACT   = $(DOCKER_CMD) -i
 DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common
+DOCKER_ARG_DEV        = --privileged -v /dev:/dev

 DOCKER_QEMU  = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE)
 DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE)
 DOCKER_TEST  = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE)

+# Dockerize commands, which require USB device passthrough, only on Linux.
+ifeq ($(shell uname -s),Linux)
+    DOCKER_CMD_DEV = $(DOCKER_CMD_INTERACT) $(DOCKER_ARG_DEV)
+
+    DOCKER_MINITERM = $(DOCKER_CMD_DEV) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE)
+endif
+


 ##--------------------------------------------------------------------------------------------------
 ## Targets
 ##--------------------------------------------------------------------------------------------------
-.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu clippy clean readelf objdump nm check
+.PHONY: all $(KERNEL_ELF) $(KERNEL_BIN) doc qemu miniterm clippy clean readelf objdump nm check

 all: $(KERNEL_BIN)

@@ -130,6 +142,12 @@
 endif

 ##------------------------------------------------------------------------------
+## Connect to the target&apos;s serial
+##------------------------------------------------------------------------------
+miniterm:
+	@$(DOCKER_MINITERM) $(EXEC_MINITERM) $(DEV_SERIAL)
+
+##------------------------------------------------------------------------------
 ## Run clippy
 ##------------------------------------------------------------------------------
 clippy:

diff -uNr 04_safe_globals/src/_arch/aarch64/cpu.rs 05_drivers_gpio_uart/src/_arch/aarch64/cpu.rs
--- 04_safe_globals/src/_arch/aarch64/cpu.rs
+++ 05_drivers_gpio_uart/src/_arch/aarch64/cpu.rs
@@ -17,6 +17,17 @@
 // Public Code
 //--------------------------------------------------------------------------------------------------

+pub use asm::nop;
+
+/// Spin for `n` cycles.
+#[cfg(feature = &quot;bsp_rpi3&quot;)]
+#[inline(always)]
+pub fn spin_for_cycles(n: usize) {
+    for _ in 0..n {
+        asm::nop();
+    }
+}
+
 /// Pause execution on the core.
 #[inline(always)]
 pub fn wait_forever() -&amp;gt; ! {

diff -uNr 04_safe_globals/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs 05_drivers_gpio_uart/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
--- 04_safe_globals/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
+++ 05_drivers_gpio_uart/src/bsp/device_driver/bcm/bcm2xxx_gpio.rs
@@ -0,0 +1,225 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! GPIO Driver.
+
+use crate::{
+    bsp::device_driver::common::MMIODerefWrapper, driver, synchronization,
+    synchronization::NullLock,
+};
+use tock_registers::{
+    interfaces::{ReadWriteable, Writeable},
+    register_bitfields, register_structs,
+    registers::ReadWrite,
+};
+
+//--------------------------------------------------------------------------------------------------
+// Private Definitions
+//--------------------------------------------------------------------------------------------------
+
+// GPIO registers.
+//
+// Descriptions taken from
+// - https://github.com/raspberrypi/documentation/files/1888662/BCM2837-ARM-Peripherals.-.Revised.-.V2-1.pdf
+// - https://datasheets.raspberrypi.org/bcm2711/bcm2711-peripherals.pdf
+register_bitfields! {
+    u32,
+
+    /// GPIO Function Select 1
+    GPFSEL1 [
+        /// Pin 15
+        FSEL15 OFFSET(15) NUMBITS(3) [
+            Input = 0b000,
+            Output = 0b001,
+            AltFunc0 = 0b100  // PL011 UART RX
+
+        ],
+
+        /// Pin 14
+        FSEL14 OFFSET(12) NUMBITS(3) [
+            Input = 0b000,
+            Output = 0b001,
+            AltFunc0 = 0b100  // PL011 UART TX
+        ]
+    ],
+
+    /// GPIO Pull-up/down Register
+    ///
+    /// BCM2837 only.
+    GPPUD [
+        /// Controls the actuation of the internal pull-up/down control line to ALL the GPIO pins.
+        PUD OFFSET(0) NUMBITS(2) [
+            Off = 0b00,
+            PullDown = 0b01,
+            PullUp = 0b10
+        ]
+    ],
+
+    /// GPIO Pull-up/down Clock Register 0
+    ///
+    /// BCM2837 only.
+    GPPUDCLK0 [
+        /// Pin 15
+        PUDCLK15 OFFSET(15) NUMBITS(1) [
+            NoEffect = 0,
+            AssertClock = 1
+        ],
+
+        /// Pin 14
+        PUDCLK14 OFFSET(14) NUMBITS(1) [
+            NoEffect = 0,
+            AssertClock = 1
+        ]
+    ],
+
+    /// GPIO Pull-up / Pull-down Register 0
+    ///
+    /// BCM2711 only.
+    GPIO_PUP_PDN_CNTRL_REG0 [
+        /// Pin 15
+        GPIO_PUP_PDN_CNTRL15 OFFSET(30) NUMBITS(2) [
+            NoResistor = 0b00,
+            PullUp = 0b01
+        ],
+
+        /// Pin 14
+        GPIO_PUP_PDN_CNTRL14 OFFSET(28) NUMBITS(2) [
+            NoResistor = 0b00,
+            PullUp = 0b01
+        ]
+    ]
+}
+
+register_structs! {
+    #[allow(non_snake_case)]
+    RegisterBlock {
+        (0x00 =&amp;gt; _reserved1),
+        (0x04 =&amp;gt; GPFSEL1: ReadWrite&amp;lt;u32, GPFSEL1::Register&amp;gt;),
+        (0x08 =&amp;gt; _reserved2),
+        (0x94 =&amp;gt; GPPUD: ReadWrite&amp;lt;u32, GPPUD::Register&amp;gt;),
+        (0x98 =&amp;gt; GPPUDCLK0: ReadWrite&amp;lt;u32, GPPUDCLK0::Register&amp;gt;),
+        (0x9C =&amp;gt; _reserved3),
+        (0xE4 =&amp;gt; GPIO_PUP_PDN_CNTRL_REG0: ReadWrite&amp;lt;u32, GPIO_PUP_PDN_CNTRL_REG0::Register&amp;gt;),
+        (0xE8 =&amp;gt; @END),
+    }
+}
+
+/// Abstraction for the associated MMIO registers.
+type Registers = MMIODerefWrapper&amp;lt;RegisterBlock&amp;gt;;
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+pub struct GPIOInner {
+    registers: Registers,
+}
+
+// Export the inner struct so that BSPs can use it for the panic handler.
+pub use GPIOInner as PanicGPIO;
+
+/// Representation of the GPIO HW.
+pub struct GPIO {
+    inner: NullLock&amp;lt;GPIOInner&amp;gt;,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+impl GPIOInner {
+    /// Create an instance.
+    ///
+    /// # Safety
+    ///
+    /// - The user must ensure to provide a correct MMIO start address.
+    pub const unsafe fn new(mmio_start_addr: usize) -&amp;gt; Self {
+        Self {
+            registers: Registers::new(mmio_start_addr),
+        }
+    }
+
+    /// Disable pull-up/down on pins 14 and 15.
+    #[cfg(feature = &quot;bsp_rpi3&quot;)]
+    fn disable_pud_14_15_bcm2837(&amp;amp;mut self) {
+        use crate::cpu;
+
+        // Make an educated guess for a good delay value (Sequence described in the BCM2837
+        // peripherals PDF).
+        //
+        // - According to Wikipedia, the fastest Pi3 clocks around 1.4 GHz.
+        // - The Linux 2837 GPIO driver waits 1 µs between the steps.
+        //
+        // So lets try to be on the safe side and default to 2000 cycles, which would equal 1 µs
+        // would the CPU be clocked at 2 GHz.
+        const DELAY: usize = 2000;
+
+        self.registers.GPPUD.write(GPPUD::PUD::Off);
+        cpu::spin_for_cycles(DELAY);
+
+        self.registers
+            .GPPUDCLK0
+            .write(GPPUDCLK0::PUDCLK15::AssertClock + GPPUDCLK0::PUDCLK14::AssertClock);
+        cpu::spin_for_cycles(DELAY);
+
+        self.registers.GPPUD.write(GPPUD::PUD::Off);
+        self.registers.GPPUDCLK0.set(0);
+    }
+
+    /// Disable pull-up/down on pins 14 and 15.
+    #[cfg(feature = &quot;bsp_rpi4&quot;)]
+    fn disable_pud_14_15_bcm2711(&amp;amp;mut self) {
+        self.registers.GPIO_PUP_PDN_CNTRL_REG0.write(
+            GPIO_PUP_PDN_CNTRL_REG0::GPIO_PUP_PDN_CNTRL15::PullUp
+                + GPIO_PUP_PDN_CNTRL_REG0::GPIO_PUP_PDN_CNTRL14::PullUp,
+        );
+    }
+
+    /// Map PL011 UART as standard output.
+    ///
+    /// TX to pin 14
+    /// RX to pin 15
+    pub fn map_pl011_uart(&amp;amp;mut self) {
+        // Select the UART on pins 14 and 15.
+        self.registers
+            .GPFSEL1
+            .modify(GPFSEL1::FSEL15::AltFunc0 + GPFSEL1::FSEL14::AltFunc0);
+
+        // Disable pull-up/down on pins 14 and 15.
+        #[cfg(feature = &quot;bsp_rpi3&quot;)]
+        self.disable_pud_14_15_bcm2837();
+
+        #[cfg(feature = &quot;bsp_rpi4&quot;)]
+        self.disable_pud_14_15_bcm2711();
+    }
+}
+
+impl GPIO {
+    /// Create an instance.
+    ///
+    /// # Safety
+    ///
+    /// - The user must ensure to provide a correct MMIO start address.
+    pub const unsafe fn new(mmio_start_addr: usize) -&amp;gt; Self {
+        Self {
+            inner: NullLock::new(GPIOInner::new(mmio_start_addr)),
+        }
+    }
+
+    /// Concurrency safe version of `GPIOInner.map_pl011_uart()`
+    pub fn map_pl011_uart(&amp;amp;self) {
+        self.inner.lock(|inner| inner.map_pl011_uart())
+    }
+}
+
+//------------------------------------------------------------------------------
+// OS Interface Code
+//------------------------------------------------------------------------------
+use synchronization::interface::Mutex;
+
+impl driver::interface::DeviceDriver for GPIO {
+    fn compatible(&amp;amp;self) -&amp;gt; &amp;amp;&apos;static str {
+        &quot;BCM GPIO&quot;
+    }
+}

diff -uNr 04_safe_globals/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs 05_drivers_gpio_uart/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
--- 04_safe_globals/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
+++ 05_drivers_gpio_uart/src/bsp/device_driver/bcm/bcm2xxx_pl011_uart.rs
@@ -0,0 +1,408 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! PL011 UART driver.
+//!
+//! # Resources
+//!
+//! - &amp;lt;https://github.com/raspberrypi/documentation/files/1888662/BCM2837-ARM-Peripherals.-.Revised.-.V2-1.pdf&amp;gt;
+//! - &amp;lt;https://developer.arm.com/documentation/ddi0183/latest&amp;gt;
+
+use crate::{
+    bsp::device_driver::common::MMIODerefWrapper, console, cpu, driver, synchronization,
+    synchronization::NullLock,
+};
+use core::fmt;
+use tock_registers::{
+    interfaces::{Readable, Writeable},
+    register_bitfields, register_structs,
+    registers::{ReadOnly, ReadWrite, WriteOnly},
+};
+
+//--------------------------------------------------------------------------------------------------
+// Private Definitions
+//--------------------------------------------------------------------------------------------------
+
+// PL011 UART registers.
+//
+// Descriptions taken from &quot;PrimeCell UART (PL011) Technical Reference Manual&quot; r1p5.
+register_bitfields! {
+    u32,
+
+    /// Flag Register.
+    FR [
+        /// Transmit FIFO empty. The meaning of this bit depends on the state of the FEN bit in the
+        /// Line Control Register, LCR_H.
+        ///
+        /// - If the FIFO is disabled, this bit is set when the transmit holding register is empty.
+        /// - If the FIFO is enabled, the TXFE bit is set when the transmit FIFO is empty.
+        /// - This bit does not indicate if there is data in the transmit shift register.
+        TXFE OFFSET(7) NUMBITS(1) [],
+
+        /// Transmit FIFO full. The meaning of this bit depends on the state of the FEN bit in the
+        /// LCR_H Register.
+        ///
+        /// - If the FIFO is disabled, this bit is set when the transmit holding register is full.
+        /// - If the FIFO is enabled, the TXFF bit is set when the transmit FIFO is full.
+        TXFF OFFSET(5) NUMBITS(1) [],
+
+        /// Receive FIFO empty. The meaning of this bit depends on the state of the FEN bit in the
+        /// LCR_H Register.
+        ///
+        /// If the FIFO is disabled, this bit is set when the receive holding register is empty. If
+        /// the FIFO is enabled, the RXFE bit is set when the receive FIFO is empty.
+
+        /// Receive FIFO empty. The meaning of this bit depends on the state of the FEN bit in the
+        /// LCR_H Register.
+        ///
+        /// - If the FIFO is disabled, this bit is set when the receive holding register is empty.
+        /// - If the FIFO is enabled, the RXFE bit is set when the receive FIFO is empty.
+        RXFE OFFSET(4) NUMBITS(1) [],
+
+        /// UART busy. If this bit is set to 1, the UART is busy transmitting data. This bit remains
+        /// set until the complete byte, including all the stop bits, has been sent from the shift
+        /// register.
+        ///
+        /// This bit is set as soon as the transmit FIFO becomes non-empty, regardless of whether
+        /// the UART is enabled or not.
+        BUSY OFFSET(3) NUMBITS(1) []
+    ],
+
+    /// Integer Baud Rate Divisor.
+    IBRD [
+        /// The integer baud rate divisor.
+        BAUD_DIVINT OFFSET(0) NUMBITS(16) []
+    ],
+
+    /// Fractional Baud Rate Divisor.
+    FBRD [
+        ///  The fractional baud rate divisor.
+        BAUD_DIVFRAC OFFSET(0) NUMBITS(6) []
+    ],
+
+    /// Line Control Register.
+    LCR_H [
+        /// Word length. These bits indicate the number of data bits transmitted or received in a
+        /// frame.
+        #[allow(clippy::enum_variant_names)]
+        WLEN OFFSET(5) NUMBITS(2) [
+            FiveBit = 0b00,
+            SixBit = 0b01,
+            SevenBit = 0b10,
+            EightBit = 0b11
+        ],
+
+        /// Enable FIFOs:
+        ///
+        /// 0 = FIFOs are disabled (character mode) that is, the FIFOs become 1-byte-deep holding
+        /// registers.
+        ///
+        /// 1 = Transmit and receive FIFO buffers are enabled (FIFO mode).
+        FEN  OFFSET(4) NUMBITS(1) [
+            FifosDisabled = 0,
+            FifosEnabled = 1
+        ]
+    ],
+
+    /// Control Register.
+    CR [
+        /// Receive enable. If this bit is set to 1, the receive section of the UART is enabled.
+        /// Data reception occurs for either UART signals or SIR signals depending on the setting of
+        /// the SIREN bit. When the UART is disabled in the middle of reception, it completes the
+        /// current character before stopping.
+        RXE OFFSET(9) NUMBITS(1) [
+            Disabled = 0,
+            Enabled = 1
+        ],
+
+        /// Transmit enable. If this bit is set to 1, the transmit section of the UART is enabled.
+        /// Data transmission occurs for either UART signals, or SIR signals depending on the
+        /// setting of the SIREN bit. When the UART is disabled in the middle of transmission, it
+        /// completes the current character before stopping.
+        TXE OFFSET(8) NUMBITS(1) [
+            Disabled = 0,
+            Enabled = 1
+        ],
+
+        /// UART enable:
+        ///
+        /// 0 = UART is disabled. If the UART is disabled in the middle of transmission or
+        /// reception, it completes the current character before stopping.
+        ///
+        /// 1 = The UART is enabled. Data transmission and reception occurs for either UART signals
+        /// or SIR signals depending on the setting of the SIREN bit
+        UARTEN OFFSET(0) NUMBITS(1) [
+            /// If the UART is disabled in the middle of transmission or reception, it completes the
+            /// current character before stopping.
+            Disabled = 0,
+            Enabled = 1
+        ]
+    ],
+
+    /// Interrupt Clear Register.
+    ICR [
+        /// Meta field for all pending interrupts.
+        ALL OFFSET(0) NUMBITS(11) []
+    ]
+}
+
+register_structs! {
+    #[allow(non_snake_case)]
+    pub RegisterBlock {
+        (0x00 =&amp;gt; DR: ReadWrite&amp;lt;u32&amp;gt;),
+        (0x04 =&amp;gt; _reserved1),
+        (0x18 =&amp;gt; FR: ReadOnly&amp;lt;u32, FR::Register&amp;gt;),
+        (0x1c =&amp;gt; _reserved2),
+        (0x24 =&amp;gt; IBRD: WriteOnly&amp;lt;u32, IBRD::Register&amp;gt;),
+        (0x28 =&amp;gt; FBRD: WriteOnly&amp;lt;u32, FBRD::Register&amp;gt;),
+        (0x2c =&amp;gt; LCR_H: WriteOnly&amp;lt;u32, LCR_H::Register&amp;gt;),
+        (0x30 =&amp;gt; CR: WriteOnly&amp;lt;u32, CR::Register&amp;gt;),
+        (0x34 =&amp;gt; _reserved3),
+        (0x44 =&amp;gt; ICR: WriteOnly&amp;lt;u32, ICR::Register&amp;gt;),
+        (0x48 =&amp;gt; @END),
+    }
+}
+
+/// Abstraction for the associated MMIO registers.
+type Registers = MMIODerefWrapper&amp;lt;RegisterBlock&amp;gt;;
+
+#[derive(PartialEq)]
+enum BlockingMode {
+    Blocking,
+    NonBlocking,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+pub struct PL011UartInner {
+    registers: Registers,
+    chars_written: usize,
+    chars_read: usize,
+}
+
+// Export the inner struct so that BSPs can use it for the panic handler.
+pub use PL011UartInner as PanicUart;
+
+/// Representation of the UART.
+pub struct PL011Uart {
+    inner: NullLock&amp;lt;PL011UartInner&amp;gt;,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+impl PL011UartInner {
+    /// Create an instance.
+    ///
+    /// # Safety
+    ///
+    /// - The user must ensure to provide a correct MMIO start address.
+    pub const unsafe fn new(mmio_start_addr: usize) -&amp;gt; Self {
+        Self {
+            registers: Registers::new(mmio_start_addr),
+            chars_written: 0,
+            chars_read: 0,
+        }
+    }
+
+    /// Set up baud rate and characteristics.
+    ///
+    /// This results in 8N1 and 921_600 baud.
+    ///
+    /// The calculation for the BRD is (we set the clock to 48 MHz in config.txt):
+    /// `(48_000_000 / 16) / 921_600 = 3.2552083`.
+    ///
+    /// This means the integer part is `3` and goes into the `IBRD`.
+    /// The fractional part is `0.2552083`.
+    ///
+    /// `FBRD` calculation according to the PL011 Technical Reference Manual:
+    /// `INTEGER((0.2552083 * 64) + 0.5) = 16`.
+    ///
+    /// Therefore, the generated baud rate divider is: `3 + 16/64 = 3.25`. Which results in a
+    /// genrated baud rate of `48_000_000 / (16 * 3.25) = 923_077`.
+    ///
+    /// Error = `((923_077 - 921_600) / 921_600) * 100 = 0.16modulo`.
+    pub fn init(&amp;amp;mut self) {
+        // Execution can arrive here while there are still characters queued in the TX FIFO and
+        // actively being sent out by the UART hardware. If the UART is turned off in this case,
+        // those queued characters would be lost.
+        //
+        // For example, this can happen during runtime on a call to panic!(), because panic!()
+        // initializes its own UART instance and calls init().
+        //
+        // Hence, flush first to ensure all pending characters are transmitted.
+        self.flush();
+
+        // Turn the UART off temporarily.
+        self.registers.CR.set(0);
+
+        // Clear all pending interrupts.
+        self.registers.ICR.write(ICR::ALL::CLEAR);
+
+        // From the PL011 Technical Reference Manual:
+        //
+        // The LCR_H, IBRD, and FBRD registers form the single 30-bit wide LCR Register that is
+        // updated on a single write strobe generated by a LCR_H write. So, to internally update the
+        // contents of IBRD or FBRD, a LCR_H write must always be performed at the end.
+        //
+        // Set the baud rate, 8N1 and FIFO enabled.
+        self.registers.IBRD.write(IBRD::BAUD_DIVINT.val(3));
+        self.registers.FBRD.write(FBRD::BAUD_DIVFRAC.val(16));
+        self.registers
+            .LCR_H
+            .write(LCR_H::WLEN::EightBit + LCR_H::FEN::FifosEnabled);
+
+        // Turn the UART on.
+        self.registers
+            .CR
+            .write(CR::UARTEN::Enabled + CR::TXE::Enabled + CR::RXE::Enabled);
+    }
+
+    /// Send a character.
+    fn write_char(&amp;amp;mut self, c: char) {
+        // Spin while TX FIFO full is set, waiting for an empty slot.
+        while self.registers.FR.matches_all(FR::TXFF::SET) {
+            cpu::nop();
+        }
+
+        // Write the character to the buffer.
+        self.registers.DR.set(c as u32);
+
+        self.chars_written += 1;
+    }
+
+    /// Block execution until the last buffered character has been physically put on the TX wire.
+    fn flush(&amp;amp;self) {
+        // Spin until the busy bit is cleared.
+        while self.registers.FR.matches_all(FR::BUSY::SET) {
+            cpu::nop();
+        }
+    }
+
+    /// Retrieve a character.
+    fn read_char_converting(&amp;amp;mut self, blocking_mode: BlockingMode) -&amp;gt; Option&amp;lt;char&amp;gt; {
+        // If RX FIFO is empty,
+        if self.registers.FR.matches_all(FR::RXFE::SET) {
+            // immediately return in non-blocking mode.
+            if blocking_mode == BlockingMode::NonBlocking {
+                return None;
+            }
+
+            // Otherwise, wait until a char was received.
+            while self.registers.FR.matches_all(FR::RXFE::SET) {
+                cpu::nop();
+            }
+        }
+
+        // Read one character.
+        let mut ret = self.registers.DR.get() as u8 as char;
+
+        // Convert carrige return to newline.
+        if ret == &apos;\r&apos; {
+            ret = &apos;\n&apos;
+        }
+
+        // Update statistics.
+        self.chars_read += 1;
+
+        Some(ret)
+    }
+}
+
+/// Implementing `core::fmt::Write` enables usage of the `format_args!` macros, which in turn are
+/// used to implement the `kernel`&apos;s `print!` and `println!` macros. By implementing `write_str()`,
+/// we get `write_fmt()` automatically.
+///
+/// The function takes an `&amp;amp;mut self`, so it must be implemented for the inner struct.
+///
+/// See [`src/print.rs`].
+///
+/// [`src/print.rs`]: ../../print/index.html
+impl fmt::Write for PL011UartInner {
+    fn write_str(&amp;amp;mut self, s: &amp;amp;str) -&amp;gt; fmt::Result {
+        for c in s.chars() {
+            self.write_char(c);
+        }
+
+        Ok(())
+    }
+}
+
+impl PL011Uart {
+    /// Create an instance.
+    ///
+    /// # Safety
+    ///
+    /// - The user must ensure to provide a correct MMIO start address.
+    pub const unsafe fn new(mmio_start_addr: usize) -&amp;gt; Self {
+        Self {
+            inner: NullLock::new(PL011UartInner::new(mmio_start_addr)),
+        }
+    }
+}
+
+//------------------------------------------------------------------------------
+// OS Interface Code
+//------------------------------------------------------------------------------
+use synchronization::interface::Mutex;
+
+impl driver::interface::DeviceDriver for PL011Uart {
+    fn compatible(&amp;amp;self) -&amp;gt; &amp;amp;&apos;static str {
+        &quot;BCM PL011 UART&quot;
+    }
+
+    unsafe fn init(&amp;amp;self) -&amp;gt; Result&amp;lt;(), &amp;amp;&apos;static str&amp;gt; {
+        self.inner.lock(|inner| inner.init());
+
+        Ok(())
+    }
+}
+
+impl console::interface::Write for PL011Uart {
+    /// Passthrough of `args` to the `core::fmt::Write` implementation, but guarded by a Mutex to
+    /// serialize access.
+    fn write_char(&amp;amp;self, c: char) {
+        self.inner.lock(|inner| inner.write_char(c));
+    }
+
+    fn write_fmt(&amp;amp;self, args: core::fmt::Arguments) -&amp;gt; fmt::Result {
+        // Fully qualified syntax for the call to `core::fmt::Write::write:fmt()` to increase
+        // readability.
+        self.inner.lock(|inner| fmt::Write::write_fmt(inner, args))
+    }
+
+    fn flush(&amp;amp;self) {
+        // Spin until TX FIFO empty is set.
+        self.inner.lock(|inner| inner.flush());
+    }
+}
+
+impl console::interface::Read for PL011Uart {
+    fn read_char(&amp;amp;self) -&amp;gt; char {
+        self.inner
+            .lock(|inner| inner.read_char_converting(BlockingMode::Blocking).unwrap())
+    }
+
+    fn clear_rx(&amp;amp;self) {
+        // Read from the RX FIFO until it is indicating empty.
+        while self
+            .inner
+            .lock(|inner| inner.read_char_converting(BlockingMode::NonBlocking))
+            .is_some()
+        {}
+    }
+}
+
+impl console::interface::Statistics for PL011Uart {
+    fn chars_written(&amp;amp;self) -&amp;gt; usize {
+        self.inner.lock(|inner| inner.chars_written)
+    }
+
+    fn chars_read(&amp;amp;self) -&amp;gt; usize {
+        self.inner.lock(|inner| inner.chars_read)
+    }
+}

diff -uNr 04_safe_globals/src/bsp/device_driver/bcm.rs 05_drivers_gpio_uart/src/bsp/device_driver/bcm.rs
--- 04_safe_globals/src/bsp/device_driver/bcm.rs
+++ 05_drivers_gpio_uart/src/bsp/device_driver/bcm.rs
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! BCM driver top level.
+
+mod bcm2xxx_gpio;
+mod bcm2xxx_pl011_uart;
+
+pub use bcm2xxx_gpio::*;
+pub use bcm2xxx_pl011_uart::*;

diff -uNr 04_safe_globals/src/bsp/device_driver/common.rs 05_drivers_gpio_uart/src/bsp/device_driver/common.rs
--- 04_safe_globals/src/bsp/device_driver/common.rs
+++ 05_drivers_gpio_uart/src/bsp/device_driver/common.rs
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2020-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! Common device driver code.
+
+use core::{marker::PhantomData, ops};
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+pub struct MMIODerefWrapper&amp;lt;T&amp;gt; {
+    start_addr: usize,
+    phantom: PhantomData&amp;lt;fn() -&amp;gt; T&amp;gt;,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+impl&amp;lt;T&amp;gt; MMIODerefWrapper&amp;lt;T&amp;gt; {
+    /// Create an instance.
+    pub const unsafe fn new(start_addr: usize) -&amp;gt; Self {
+        Self {
+            start_addr,
+            phantom: PhantomData,
+        }
+    }
+}
+
+impl&amp;lt;T&amp;gt; ops::Deref for MMIODerefWrapper&amp;lt;T&amp;gt; {
+    type Target = T;
+
+    fn deref(&amp;amp;self) -&amp;gt; &amp;amp;Self::Target {
+        unsafe { &amp;amp;*(self.start_addr as *const _) }
+    }
+}

diff -uNr 04_safe_globals/src/bsp/device_driver.rs 05_drivers_gpio_uart/src/bsp/device_driver.rs
--- 04_safe_globals/src/bsp/device_driver.rs
+++ 05_drivers_gpio_uart/src/bsp/device_driver.rs
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! Device driver.
+
+#[cfg(any(feature = &quot;bsp_rpi3&quot;, feature = &quot;bsp_rpi4&quot;))]
+mod bcm;
+mod common;
+
+#[cfg(any(feature = &quot;bsp_rpi3&quot;, feature = &quot;bsp_rpi4&quot;))]
+pub use bcm::*;

diff -uNr 04_safe_globals/src/bsp/raspberrypi/console.rs 05_drivers_gpio_uart/src/bsp/raspberrypi/console.rs
--- 04_safe_globals/src/bsp/raspberrypi/console.rs
+++ 05_drivers_gpio_uart/src/bsp/raspberrypi/console.rs
@@ -4,113 +4,34 @@

 //! BSP console facilities.

-use crate::{console, synchronization, synchronization::NullLock};
+use super::memory;
+use crate::{bsp::device_driver, console};
 use core::fmt;

 //--------------------------------------------------------------------------------------------------
-// Private Definitions
+// Public Code
 //--------------------------------------------------------------------------------------------------

-/// A mystical, magical device for generating QEMU output out of the void.
+/// In case of a panic, the panic handler uses this function to take a last shot at printing
+/// something before the system is halted.
 ///
-/// The mutex protected part.
-struct QEMUOutputInner {
-    chars_written: usize,
-}
-
-//--------------------------------------------------------------------------------------------------
-// Public Definitions
-//--------------------------------------------------------------------------------------------------
-
-/// The main struct.
-pub struct QEMUOutput {
-    inner: NullLock&amp;lt;QEMUOutputInner&amp;gt;,
-}
-
-//--------------------------------------------------------------------------------------------------
-// Global instances
-//--------------------------------------------------------------------------------------------------
-
-static QEMU_OUTPUT: QEMUOutput = QEMUOutput::new();
-
-//--------------------------------------------------------------------------------------------------
-// Private Code
-//--------------------------------------------------------------------------------------------------
-
-impl QEMUOutputInner {
-    const fn new() -&amp;gt; QEMUOutputInner {
-        QEMUOutputInner { chars_written: 0 }
-    }
-
-    /// Send a character.
-    fn write_char(&amp;amp;mut self, c: char) {
-        unsafe {
-            core::ptr::write_volatile(0x3F20_1000 as *mut u8, c as u8);
-        }
-
-        self.chars_written += 1;
-    }
-}
-
-/// Implementing `core::fmt::Write` enables usage of the `format_args!` macros, which in turn are
-/// used to implement the `kernel`&apos;s `print!` and `println!` macros. By implementing `write_str()`,
-/// we get `write_fmt()` automatically.
+/// We try to init panic-versions of the GPIO and the UART. The panic versions are not protected
+/// with synchronization primitives, which increases chances that we get to print something, even
+/// when the kernel&apos;s default GPIO or UART instances happen to be locked at the time of the panic.
 ///
-/// The function takes an `&amp;amp;mut self`, so it must be implemented for the inner struct.
+/// # Safety
 ///
-/// See [`src/print.rs`].
-///
-/// [`src/print.rs`]: ../../print/index.html
-impl fmt::Write for QEMUOutputInner {
-    fn write_str(&amp;amp;mut self, s: &amp;amp;str) -&amp;gt; fmt::Result {
-        for c in s.chars() {
-            // Convert newline to carrige return + newline.
-            if c == &apos;\n&apos; {
-                self.write_char(&apos;\r&apos;)
-            }
-
-            self.write_char(c);
-        }
-
-        Ok(())
-    }
-}
-
-//--------------------------------------------------------------------------------------------------
-// Public Code
-//--------------------------------------------------------------------------------------------------
-
-impl QEMUOutput {
-    /// Create a new instance.
-    pub const fn new() -&amp;gt; QEMUOutput {
-        QEMUOutput {
-            inner: NullLock::new(QEMUOutputInner::new()),
-        }
-    }
+/// - Use only for printing during a panic.
+pub unsafe fn panic_console_out() -&amp;gt; impl fmt::Write {
+    let mut panic_gpio = device_driver::PanicGPIO::new(memory::map::mmio::GPIO_START);
+    let mut panic_uart = device_driver::PanicUart::new(memory::map::mmio::PL011_UART_START);
+
+    panic_gpio.map_pl011_uart();
+    panic_uart.init();
+    panic_uart
 }

 /// Return a reference to the console.
 pub fn console() -&amp;gt; &amp;amp;&apos;static impl console::interface::All {
-    &amp;amp;QEMU_OUTPUT
-}
-
-//------------------------------------------------------------------------------
-// OS Interface Code
-//------------------------------------------------------------------------------
-use synchronization::interface::Mutex;
-
-/// Passthrough of `args` to the `core::fmt::Write` implementation, but guarded by a Mutex to
-/// serialize access.
-impl console::interface::Write for QEMUOutput {
-    fn write_fmt(&amp;amp;self, args: core::fmt::Arguments) -&amp;gt; fmt::Result {
-        // Fully qualified syntax for the call to `core::fmt::Write::write:fmt()` to increase
-        // readability.
-        self.inner.lock(|inner| fmt::Write::write_fmt(inner, args))
-    }
-}
-
-impl console::interface::Statistics for QEMUOutput {
-    fn chars_written(&amp;amp;self) -&amp;gt; usize {
-        self.inner.lock(|inner| inner.chars_written)
-    }
+    &amp;amp;super::PL011_UART
 }

diff -uNr 04_safe_globals/src/bsp/raspberrypi/driver.rs 05_drivers_gpio_uart/src/bsp/raspberrypi/driver.rs
--- 04_safe_globals/src/bsp/raspberrypi/driver.rs
+++ 05_drivers_gpio_uart/src/bsp/raspberrypi/driver.rs
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! BSP driver support.
+
+use crate::driver;
+
+//--------------------------------------------------------------------------------------------------
+// Private Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Device Driver Manager type.
+struct BSPDriverManager {
+    device_drivers: [&amp;amp;&apos;static (dyn DeviceDriver + Sync); 2],
+}
+
+//--------------------------------------------------------------------------------------------------
+// Global instances
+//--------------------------------------------------------------------------------------------------
+
+static BSP_DRIVER_MANAGER: BSPDriverManager = BSPDriverManager {
+    device_drivers: [&amp;amp;super::GPIO, &amp;amp;super::PL011_UART],
+};
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// Return a reference to the driver manager.
+pub fn driver_manager() -&amp;gt; &amp;amp;&apos;static impl driver::interface::DriverManager {
+    &amp;amp;BSP_DRIVER_MANAGER
+}
+
+//------------------------------------------------------------------------------
+// OS Interface Code
+//------------------------------------------------------------------------------
+use driver::interface::DeviceDriver;
+
+impl driver::interface::DriverManager for BSPDriverManager {
+    fn all_device_drivers(&amp;amp;self) -&amp;gt; &amp;amp;[&amp;amp;&apos;static (dyn DeviceDriver + Sync)] {
+        &amp;amp;self.device_drivers[..]
+    }
+
+    fn post_device_driver_init(&amp;amp;self) {
+        // Configure PL011Uart&apos;s output pins.
+        super::GPIO.map_pl011_uart();
+    }
+}

diff -uNr 04_safe_globals/src/bsp/raspberrypi/memory.rs 05_drivers_gpio_uart/src/bsp/raspberrypi/memory.rs
--- 04_safe_globals/src/bsp/raspberrypi/memory.rs
+++ 05_drivers_gpio_uart/src/bsp/raspberrypi/memory.rs
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! BSP Memory Management.
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// The board&apos;s physical memory map.
+#[rustfmt::skip]
+pub(super) mod map {
+
+    pub const GPIO_OFFSET:         usize = 0x0020_0000;
+    pub const UART_OFFSET:         usize = 0x0020_1000;
+
+    /// Physical devices.
+    #[cfg(feature = &quot;bsp_rpi3&quot;)]
+    pub mod mmio {
+        use super::*;
+
+        pub const START:            usize =         0x3F00_0000;
+        pub const GPIO_START:       usize = START + GPIO_OFFSET;
+        pub const PL011_UART_START: usize = START + UART_OFFSET;
+    }
+
+    /// Physical devices.
+    #[cfg(feature = &quot;bsp_rpi4&quot;)]
+    pub mod mmio {
+        use super::*;
+
+        pub const START:            usize =         0xFE00_0000;
+        pub const GPIO_START:       usize = START + GPIO_OFFSET;
+        pub const PL011_UART_START: usize = START + UART_OFFSET;
+    }
+}

diff -uNr 04_safe_globals/src/bsp/raspberrypi.rs 05_drivers_gpio_uart/src/bsp/raspberrypi.rs
--- 04_safe_globals/src/bsp/raspberrypi.rs
+++ 05_drivers_gpio_uart/src/bsp/raspberrypi.rs
@@ -6,3 +6,33 @@

 pub mod console;
 pub mod cpu;
+pub mod driver;
+pub mod memory;
+
+//--------------------------------------------------------------------------------------------------
+// Global instances
+//--------------------------------------------------------------------------------------------------
+use super::device_driver;
+
+static GPIO: device_driver::GPIO =
+    unsafe { device_driver::GPIO::new(memory::map::mmio::GPIO_START) };
+
+static PL011_UART: device_driver::PL011Uart =
+    unsafe { device_driver::PL011Uart::new(memory::map::mmio::PL011_UART_START) };
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// Board identification.
+pub fn board_name() -&amp;gt; &amp;amp;&apos;static str {
+    #[cfg(feature = &quot;bsp_rpi3&quot;)]
+    {
+        &quot;Raspberry Pi 3&quot;
+    }
+
+    #[cfg(feature = &quot;bsp_rpi4&quot;)]
+    {
+        &quot;Raspberry Pi 4&quot;
+    }
+}

diff -uNr 04_safe_globals/src/bsp.rs 05_drivers_gpio_uart/src/bsp.rs
--- 04_safe_globals/src/bsp.rs
+++ 05_drivers_gpio_uart/src/bsp.rs
@@ -4,6 +4,8 @@

 //! Conditional reexporting of Board Support Packages.

+mod device_driver;
+
 #[cfg(any(feature = &quot;bsp_rpi3&quot;, feature = &quot;bsp_rpi4&quot;))]
 mod raspberrypi;


diff -uNr 04_safe_globals/src/console.rs 05_drivers_gpio_uart/src/console.rs
--- 04_safe_globals/src/console.rs
+++ 05_drivers_gpio_uart/src/console.rs
@@ -14,8 +14,25 @@

     /// Console write functions.
     pub trait Write {
+        /// Write a single character.
+        fn write_char(&amp;amp;self, c: char);
+
         /// Write a Rust format string.
         fn write_fmt(&amp;amp;self, args: fmt::Arguments) -&amp;gt; fmt::Result;
+
+        /// Block until the last buffered character has been physically put on the TX wire.
+        fn flush(&amp;amp;self);
+    }
+
+    /// Console read functions.
+    pub trait Read {
+        /// Read a single character.
+        fn read_char(&amp;amp;self) -&amp;gt; char {
+            &apos; &apos;
+        }
+
+        /// Clear RX buffers, if any.
+        fn clear_rx(&amp;amp;self);
     }

     /// Console statistics.
@@ -24,8 +41,13 @@
         fn chars_written(&amp;amp;self) -&amp;gt; usize {
             0
         }
+
+        /// Return the number of characters read.
+        fn chars_read(&amp;amp;self) -&amp;gt; usize {
+            0
+        }
     }

     /// Trait alias for a full-fledged console.
-    pub trait All = Write + Statistics;
+    pub trait All = Write + Read + Statistics;
 }

diff -uNr 04_safe_globals/src/cpu.rs 05_drivers_gpio_uart/src/cpu.rs
--- 04_safe_globals/src/cpu.rs
+++ 05_drivers_gpio_uart/src/cpu.rs
@@ -13,4 +13,7 @@
 //--------------------------------------------------------------------------------------------------
 // Architectural Public Reexports
 //--------------------------------------------------------------------------------------------------
-pub use arch_cpu::wait_forever;
+pub use arch_cpu::{nop, wait_forever};
+
+#[cfg(feature = &quot;bsp_rpi3&quot;)]
+pub use arch_cpu::spin_for_cycles;

diff -uNr 04_safe_globals/src/driver.rs 05_drivers_gpio_uart/src/driver.rs
--- 04_safe_globals/src/driver.rs
+++ 05_drivers_gpio_uart/src/driver.rs
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! Driver support.
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Driver interfaces.
+pub mod interface {
+    /// Device Driver functions.
+    pub trait DeviceDriver {
+        /// Return a compatibility string for identifying the driver.
+        fn compatible(&amp;amp;self) -&amp;gt; &amp;amp;&apos;static str;
+
+        /// Called by the kernel to bring up the device.
+        ///
+        /// # Safety
+        ///
+        /// - During init, drivers might do stuff with system-wide impact.
+        unsafe fn init(&amp;amp;self) -&amp;gt; Result&amp;lt;(), &amp;amp;&apos;static str&amp;gt; {
+            Ok(())
+        }
+    }
+
+    /// Device driver management functions.
+    ///
+    /// The `BSP` is supposed to supply one global instance.
+    pub trait DriverManager {
+        /// Return a slice of references to all `BSP`-instantiated drivers.
+        ///
+        /// # Safety
+        ///
+        /// - The order of devices is the order in which `DeviceDriver::init()` is called.
+        fn all_device_drivers(&amp;amp;self) -&amp;gt; &amp;amp;[&amp;amp;&apos;static (dyn DeviceDriver + Sync)];
+
+        /// Initialization code that runs after driver init.
+        ///
+        /// For example, device driver code that depends on other drivers already being online.
+        fn post_device_driver_init(&amp;amp;self);
+    }
+}

diff -uNr 04_safe_globals/src/main.rs 05_drivers_gpio_uart/src/main.rs
--- 04_safe_globals/src/main.rs
+++ 05_drivers_gpio_uart/src/main.rs
@@ -104,6 +104,8 @@
 //!     - It is implemented in `src/_arch/__arch_name__/cpu/boot.s`.
 //! 2. Once finished with architectural setup, the arch code calls `kernel_init()`.

+#![allow(clippy::upper_case_acronyms)]
+#![feature(const_fn_fn_ptr_basics)]
 #![feature(format_args_nl)]
 #![feature(global_asm)]
 #![feature(panic_info_message)]
@@ -114,6 +116,7 @@
 mod bsp;
 mod console;
 mod cpu;
+mod driver;
 mod panic_wait;
 mod print;
 mod synchronization;
@@ -123,16 +126,54 @@
 /// # Safety
 ///
 /// - Only a single core must be active and running this function.
+/// - The init calls in this function must appear in the correct order.
 unsafe fn kernel_init() -&amp;gt; ! {
-    use console::interface::Statistics;
+    use driver::interface::DriverManager;

-    println!(&quot;[0] Hello from Rust!&quot;);
+    for i in bsp::driver::driver_manager().all_device_drivers().iter() {
+        if let Err(x) = i.init() {
+            panic!(&quot;Error loading driver: {}: {}&quot;, i.compatible(), x);
+        }
+    }
+    bsp::driver::driver_manager().post_device_driver_init();
+    // println! is usable from here on.
+
+    // Transition from unsafe to safe.
+    kernel_main()
+}
+
+/// The main function running after the early init.
+fn kernel_main() -&amp;gt; ! {
+    use bsp::console::console;
+    use console::interface::All;
+    use driver::interface::DriverManager;
+
+    println!(
+        &quot;[0] {} version {}&quot;,
+        env!(&quot;CARGO_PKG_NAME&quot;),
+        env!(&quot;CARGO_PKG_VERSION&quot;)
+    );
+    println!(&quot;[1] Booting on: {}&quot;, bsp::board_name());
+
+    println!(&quot;[2] Drivers loaded:&quot;);
+    for (i, driver) in bsp::driver::driver_manager()
+        .all_device_drivers()
+        .iter()
+        .enumerate()
+    {
+        println!(&quot;      {}. {}&quot;, i + 1, driver.compatible());
+    }

     println!(
-        &quot;[1] Chars written: {}&quot;,
+        &quot;[3] Chars written: {}&quot;,
         bsp::console::console().chars_written()
     );
+    println!(&quot;[4] Echoing input now&quot;);

-    println!(&quot;[2] Stopping here.&quot;);
-    cpu::wait_forever()
+    // Discard any spurious received characters before going into echo mode.
+    console().clear_rx();
+    loop {
+        let c = bsp::console::console().read_char();
+        bsp::console::console().write_char(c);
+    }
 }

diff -uNr 04_safe_globals/src/panic_wait.rs 05_drivers_gpio_uart/src/panic_wait.rs
--- 04_safe_globals/src/panic_wait.rs
+++ 05_drivers_gpio_uart/src/panic_wait.rs
@@ -4,15 +4,35 @@

 //! A panic handler that infinitely waits.

-use crate::{cpu, println};
-use core::panic::PanicInfo;
+use crate::{bsp, cpu};
+use core::{fmt, panic::PanicInfo};
+
+//--------------------------------------------------------------------------------------------------
+// Private Code
+//--------------------------------------------------------------------------------------------------
+
+fn _panic_print(args: fmt::Arguments) {
+    use fmt::Write;
+
+    unsafe { bsp::console::panic_console_out().write_fmt(args).unwrap() };
+}
+
+/// Prints with a newline - only use from the panic handler.
+///
+/// Carbon copy from &amp;lt;https://doc.rust-lang.org/src/std/macros.rs.html&amp;gt;
+#[macro_export]
+macro_rules! panic_println {
+    ($($arg:tt)*) =&amp;gt; ({
+        _panic_print(format_args_nl!($($arg)*));
+    })
+}

 #[panic_handler]
 fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
     if let Some(args) = info.message() {
-        println!(&quot;\nKernel panic: {}&quot;, args);
+        panic_println!(&quot;\nKernel panic: {}&quot;, args);
     } else {
-        println!(&quot;\nKernel panic!&quot;);
+        panic_println!(&quot;\nKernel panic!&quot;);
     }

     cpu::wait_forever()

diff -uNr 04_safe_globals/tests/boot_test_string.rb 05_drivers_gpio_uart/tests/boot_test_string.rb
--- 04_safe_globals/tests/boot_test_string.rb
+++ 05_drivers_gpio_uart/tests/boot_test_string.rb
@@ -1,3 +1,3 @@
 # frozen_string_literal: true

-EXPECTED_PRINT = &apos;Stopping here&apos;
+EXPECTED_PRINT = &apos;Echoing input now&apos;

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 04 - 安全的全局变量</title><link>https://zlotus.github.io/2021/08/07/writing-os-in-rust-on-rpi-04/</link><guid isPermaLink="true">https://zlotus.github.io/2021/08/07/writing-os-in-rust-on-rpi-04/</guid><description>使用Rust在树莓派上编写操作系统 - 04 - 安全的全局变量 - 犯的错不会消失，但是一旦丧失了自尊，任何人都无法活在明天。</description><pubDate>Sat, 07 Aug 2021 17:29:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/04_safe_globals/README.md&quot;&gt;Tutorial 04 - Safe Globals&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;引入了伪锁。&lt;/li&gt;
&lt;li&gt;本章首次展示了操作系统的同步机制，并使全局数据结构可以被安全的访问。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Rust中可变的全局变量&lt;/h2&gt;
&lt;p&gt;当我们在&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/03_hacky_hello_world&quot;&gt;第三章&lt;/a&gt;中引入全局可用的&lt;code&gt;print!&lt;/code&gt;宏时，使了个小手段。在调用&lt;code&gt;core::fmt&lt;/code&gt;的&lt;code&gt;write_fmt()&lt;/code&gt;函数时，需要提供一个&lt;code&gt;&amp;amp;mut self&lt;/code&gt;参数，而我们的调用之所以能成功，是因为每次调用时该函数时，我们都会创建一个新的&lt;code&gt;QEMUOutput&lt;/code&gt;实例。&lt;/p&gt;
&lt;p&gt;如果我们想保持某些状态——例如，对写入的字符进行统计——就需要创建一个&lt;code&gt;QEMUOutput&lt;/code&gt;的全局实例（在Rust中，使用&lt;code&gt;static&lt;/code&gt;关键字）。&lt;/p&gt;
&lt;p&gt;然而，我们不能使用静态变量&lt;code&gt;QEMU_OUTPUT&lt;/code&gt;调用参数为&lt;code&gt;&amp;amp;mut self&lt;/code&gt;的函数。此时，我们需要一个&lt;code&gt;static mut&lt;/code&gt;变量，但是，在&lt;code&gt;static mut&lt;/code&gt;变量上调用函数修改状态并不安全。Rust编译器认为，此时，它无法阻止多个内核/线程同时改变该数据（该变量是全局的，因此任何代码都可能在任何地方引用它。借用检查器此时将无法继续保证借用安全）。&lt;/p&gt;
&lt;p&gt;这个问题的解决方案是将全局封装在一个同步原语中（译注：synchronization primitive，实现同步操作的原子化执行）。在我们的例子中，将使用&lt;em&gt;互斥锁&lt;/em&gt;（Mutex即MUTual EXclusion）的一个变体。 &lt;code&gt;Mutex&lt;/code&gt;在&lt;code&gt;synchronized.rs&lt;/code&gt;中作为一个trait引入，并由该文件中的&lt;code&gt;NullLock&lt;/code&gt;实现。为了使代码更适合教学目的，现阶段省略了用于保护并发访问的罗辑结构，因为目前的内核仅在单核上执行且禁用中断，所以目前并不需要并发保护。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NullLock&lt;/code&gt;专注于展示Rust的一个核心概念——&lt;a href=&quot;https://doc.rust-lang.org/std/cell/index.html&quot;&gt;内部可变性&lt;/a&gt;，请务必仔细阅读。此外，我还建议阅读这篇关于&lt;a href=&quot;https://docs.rs/dtolnay/0.0.6/dtolnay/macro._02__reference_types.html&quot;&gt;Rust引用类型的准确认知模型&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果你想将&lt;code&gt;NullLock&lt;/code&gt;与某些实际应用中的互斥锁的实现进行比较，您可以查看&lt;a href=&quot;https://github.com/mvdnes/spin-rs&quot;&gt;&lt;code&gt;spin&lt;/code&gt;crate&lt;/a&gt;或&lt;code&gt;parking_lot&lt;/code&gt;crate中的实现。&lt;/p&gt;
&lt;h2&gt;测试运行&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ make qemu
[...]

[0] Hello from Rust!
[1] Chars written: 22
[2] Stopping here.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;与上一章代码的区别&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;diff -uNr 03_hacky_hello_world/Cargo.toml 04_safe_globals/Cargo.toml
--- 03_hacky_hello_world/Cargo.toml
+++ 04_safe_globals/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = &quot;mingo&quot;
-version = &quot;0.3.0&quot;
+version = &quot;0.4.0&quot;
 authors = [&quot;Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;&quot;]
 edition = &quot;2018&quot;


diff -uNr 03_hacky_hello_world/src/bsp/raspberrypi/console.rs 04_safe_globals/src/bsp/raspberrypi/console.rs
--- 03_hacky_hello_world/src/bsp/raspberrypi/console.rs
+++ 04_safe_globals/src/bsp/raspberrypi/console.rs
@@ -4,7 +4,7 @@

 //! BSP console facilities.

-use crate::console;
+use crate::{console, synchronization, synchronization::NullLock};
 use core::fmt;

 //--------------------------------------------------------------------------------------------------
@@ -12,25 +12,64 @@
 //--------------------------------------------------------------------------------------------------

 /// A mystical, magical device for generating QEMU output out of the void.
-struct QEMUOutput;
+///
+/// The mutex protected part.
+struct QEMUOutputInner {
+    chars_written: usize,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// The main struct.
+pub struct QEMUOutput {
+    inner: NullLock&amp;lt;QEMUOutputInner&amp;gt;,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Global instances
+//--------------------------------------------------------------------------------------------------
+
+static QEMU_OUTPUT: QEMUOutput = QEMUOutput::new();

 //--------------------------------------------------------------------------------------------------
 // Private Code
 //--------------------------------------------------------------------------------------------------

+impl QEMUOutputInner {
+    const fn new() -&amp;gt; QEMUOutputInner {
+        QEMUOutputInner { chars_written: 0 }
+    }
+
+    /// Send a character.
+    fn write_char(&amp;amp;mut self, c: char) {
+        unsafe {
+            core::ptr::write_volatile(0x3F20_1000 as *mut u8, c as u8);
+        }
+
+        self.chars_written += 1;
+    }
+}
+
 /// Implementing `core::fmt::Write` enables usage of the `format_args!` macros, which in turn are
 /// used to implement the `kernel`&apos;s `print!` and `println!` macros. By implementing `write_str()`,
 /// we get `write_fmt()` automatically.
 ///
+/// The function takes an `&amp;amp;mut self`, so it must be implemented for the inner struct.
+///
 /// See [`src/print.rs`].
 ///
 /// [`src/print.rs`]: ../../print/index.html
-impl fmt::Write for QEMUOutput {
+impl fmt::Write for QEMUOutputInner {
     fn write_str(&amp;amp;mut self, s: &amp;amp;str) -&amp;gt; fmt::Result {
         for c in s.chars() {
-            unsafe {
-                core::ptr::write_volatile(0x3F20_1000 as *mut u8, c as u8);
+            // Convert newline to carrige return + newline.
+            if c == &apos;\n&apos; {
+                self.write_char(&apos;\r&apos;)
             }
+
+            self.write_char(c);
         }

         Ok(())
@@ -41,7 +80,37 @@
 // Public Code
 //--------------------------------------------------------------------------------------------------

+impl QEMUOutput {
+    /// Create a new instance.
+    pub const fn new() -&amp;gt; QEMUOutput {
+        QEMUOutput {
+            inner: NullLock::new(QEMUOutputInner::new()),
+        }
+    }
+}
+
 /// Return a reference to the console.
-pub fn console() -&amp;gt; impl console::interface::Write {
-    QEMUOutput {}
+pub fn console() -&amp;gt; &amp;amp;&apos;static impl console::interface::All {
+    &amp;amp;QEMU_OUTPUT
+}
+
+//------------------------------------------------------------------------------
+// OS Interface Code
+//------------------------------------------------------------------------------
+use synchronization::interface::Mutex;
+
+/// Passthrough of `args` to the `core::fmt::Write` implementation, but guarded by a Mutex to
+/// serialize access.
+impl console::interface::Write for QEMUOutput {
+    fn write_fmt(&amp;amp;self, args: core::fmt::Arguments) -&amp;gt; fmt::Result {
+        // Fully qualified syntax for the call to `core::fmt::Write::write:fmt()` to increase
+        // readability.
+        self.inner.lock(|inner| fmt::Write::write_fmt(inner, args))
+    }
+}
+
+impl console::interface::Statistics for QEMUOutput {
+    fn chars_written(&amp;amp;self) -&amp;gt; usize {
+        self.inner.lock(|inner| inner.chars_written)
+    }
 }

diff -uNr 03_hacky_hello_world/src/console.rs 04_safe_globals/src/console.rs
--- 03_hacky_hello_world/src/console.rs
+++ 04_safe_globals/src/console.rs
@@ -10,10 +10,22 @@

 /// Console interfaces.
 pub mod interface {
+    use core::fmt;
+
     /// Console write functions.
-    ///
-    /// `core::fmt::Write` is exactly what we need for now. Re-export it here because
-    /// implementing `console::Write` gives a better hint to the reader about the
-    /// intention.
-    pub use core::fmt::Write;
+    pub trait Write {
+        /// Write a Rust format string.
+        fn write_fmt(&amp;amp;self, args: fmt::Arguments) -&amp;gt; fmt::Result;
+    }
+
+    /// Console statistics.
+    pub trait Statistics {
+        /// Return the number of characters written.
+        fn chars_written(&amp;amp;self) -&amp;gt; usize {
+            0
+        }
+    }
+
+    /// Trait alias for a full-fledged console.
+    pub trait All = Write + Statistics;
 }

diff -uNr 03_hacky_hello_world/src/main.rs 04_safe_globals/src/main.rs
--- 03_hacky_hello_world/src/main.rs
+++ 04_safe_globals/src/main.rs
@@ -107,6 +107,7 @@
 #![feature(format_args_nl)]
 #![feature(global_asm)]
 #![feature(panic_info_message)]
+#![feature(trait_alias)]
 #![no_main]
 #![no_std]

@@ -115,6 +116,7 @@
 mod cpu;
 mod panic_wait;
 mod print;
+mod synchronization;

 /// Early init code.
 ///
@@ -122,7 +124,15 @@
 ///
 /// - Only a single core must be active and running this function.
 unsafe fn kernel_init() -&amp;gt; ! {
+    use console::interface::Statistics;
+
     println!(&quot;[0] Hello from Rust!&quot;);

-    panic!(&quot;Stopping here.&quot;)
+    println!(
+        &quot;[1] Chars written: {}&quot;,
+        bsp::console::console().chars_written()
+    );
+
+    println!(&quot;[2] Stopping here.&quot;);
+    cpu::wait_forever()
 }

diff -uNr 03_hacky_hello_world/src/synchronization.rs 04_safe_globals/src/synchronization.rs
--- 03_hacky_hello_world/src/synchronization.rs
+++ 04_safe_globals/src/synchronization.rs
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2020-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! Synchronization primitives.
+//!
+//! # Resources
+//!
+//!   - &amp;lt;https://doc.rust-lang.org/book/ch16-04-extensible-concurrency-sync-and-send.html&amp;gt;
+//!   - &amp;lt;https://stackoverflow.com/questions/59428096/understanding-the-send-trait&amp;gt;
+//!   - &amp;lt;https://doc.rust-lang.org/std/cell/index.html&amp;gt;
+
+use core::cell::UnsafeCell;
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Synchronization interfaces.
+pub mod interface {
+
+    /// Any object implementing this trait guarantees exclusive access to the data wrapped within
+    /// the Mutex for the duration of the provided closure.
+    pub trait Mutex {
+        /// The type of the data that is wrapped by this mutex.
+        type Data;
+
+        /// Locks the mutex and grants the closure temporary mutable access to the wrapped data.
+        fn lock&amp;lt;R&amp;gt;(&amp;amp;self, f: impl FnOnce(&amp;amp;mut Self::Data) -&amp;gt; R) -&amp;gt; R;
+    }
+}
+
+/// A pseudo-lock for teaching purposes.
+///
+/// In contrast to a real Mutex implementation, does not protect against concurrent access from
+/// other cores to the contained data. This part is preserved for later lessons.
+///
+/// The lock will only be used as long as it is safe to do so, i.e. as long as the kernel is
+/// executing single-threaded, aka only running on a single core with interrupts disabled.
+pub struct NullLock&amp;lt;T&amp;gt;
+where
+    T: ?Sized,
+{
+    data: UnsafeCell&amp;lt;T&amp;gt;,
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+unsafe impl&amp;lt;T&amp;gt; Send for NullLock&amp;lt;T&amp;gt; where T: ?Sized + Send {}
+unsafe impl&amp;lt;T&amp;gt; Sync for NullLock&amp;lt;T&amp;gt; where T: ?Sized + Send {}
+
+impl&amp;lt;T&amp;gt; NullLock&amp;lt;T&amp;gt; {
+    /// Create an instance.
+    pub const fn new(data: T) -&amp;gt; Self {
+        Self {
+            data: UnsafeCell::new(data),
+        }
+    }
+}
+
+//------------------------------------------------------------------------------
+// OS Interface Code
+//------------------------------------------------------------------------------
+
+impl&amp;lt;T&amp;gt; interface::Mutex for NullLock&amp;lt;T&amp;gt; {
+    type Data = T;
+
+    fn lock&amp;lt;R&amp;gt;(&amp;amp;self, f: impl FnOnce(&amp;amp;mut Self::Data) -&amp;gt; R) -&amp;gt; R {
+        // In a real lock, there would be code encapsulating this line that ensures that this
+        // mutable reference will ever only be given out once at a time.
+        let data = unsafe { &amp;amp;mut *self.data.get() };
+
+        f(data)
+    }
+}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 03 - 奇巧hello world</title><link>https://zlotus.github.io/2021/08/05/writing-os-in-rust-on-rpi-03/</link><guid isPermaLink="true">https://zlotus.github.io/2021/08/05/writing-os-in-rust-on-rpi-03/</guid><description>使用Rust在树莓派上编写操作系统 - 03 - 奇巧hello world</description><pubDate>Thu, 05 Aug 2021 17:29:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/03_hacky_hello_world/README.md&quot;&gt;Tutorial 03 - Hacky Hello World&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;引入全局&lt;code&gt;print!()&lt;/code&gt; 宏以尽早实现“printf调试”。&lt;/li&gt;
&lt;li&gt;为了保证本章不至于过长，打印功能目前“滥用”了一个QEMU属性，以允许我们在未正确设置下使用树莓派的&lt;code&gt;UART&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;对如&lt;code&gt;UART&lt;/code&gt;等真实硬件的使用，将在后续章节中逐步展开。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;需要注意的新增代码&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;src/console.rs&lt;/code&gt; introduces interface &lt;code&gt;Traits&lt;/code&gt; for console commands.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;src/bsp/raspberrypi/console.rs&lt;/code&gt; implements the interface for QEMU&apos;s emulated UART.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The panic handler makes use of the new &lt;code&gt;print!()&lt;/code&gt; to display user error messages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;There is a new Makefile target, &lt;code&gt;make test&lt;/code&gt;, intended for automated testing. It boots the compiled  kernel in &lt;code&gt;QEMU&lt;/code&gt;, and checks for an expected output string produced by the kernel.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In this tutorial, it checks for the string &lt;code&gt;Stopping here&lt;/code&gt;, which is emitted by the &lt;code&gt;panic!()&lt;/code&gt; at the end of &lt;code&gt;main.rs&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;src/console.rs&lt;/code&gt;引入的&lt;code&gt;interface&lt;/code&gt;中的trait将用于控制台命令行。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;src/bsp/raspberrypi/console.rs&lt;/code&gt;为QEMU虚拟的UART设备实现了接口。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;panic处理程序使用新的&lt;code&gt;print!()&lt;/code&gt;宏显示用户错误消息。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新增了一个Makefile目标——&lt;code&gt;make test&lt;/code&gt;——用于自动化测试。它能够在&lt;code&gt;QEMU&lt;/code&gt;中引导编译后的内核，并检查内核能否输出预期的字符串。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在本章，它会检查由&lt;code&gt;main.rs&lt;/code&gt;末尾的&lt;code&gt;panic!()&lt;/code&gt;打印的字符串：&lt;code&gt;Stopping here&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;测试运行&lt;/h2&gt;
&lt;p&gt;至此，QEMU便不再以汇编模式运行了。从本章开始，它将显示&lt;code&gt;console&lt;/code&gt;的输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ make qemu
[...]
Hello from Rust!

Kernel panic: Stopping here.
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;与上一章代码的区别&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;diff -uNr 02_runtime_init/Cargo.toml 03_hacky_hello_world/Cargo.toml
--- 02_runtime_init/Cargo.toml
+++ 03_hacky_hello_world/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = &quot;mingo&quot;
-version = &quot;0.2.0&quot;
+version = &quot;0.3.0&quot;
 authors = [&quot;Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;&quot;]
 edition = &quot;2018&quot;


diff -uNr 02_runtime_init/Makefile 03_hacky_hello_world/Makefile
--- 02_runtime_init/Makefile
+++ 03_hacky_hello_world/Makefile
@@ -23,7 +23,7 @@
     KERNEL_BIN        = kernel8.img
     QEMU_BINARY       = qemu-system-aarch64
     QEMU_MACHINE_TYPE = raspi3
-    QEMU_RELEASE_ARGS = -d in_asm -display none
+    QEMU_RELEASE_ARGS = -serial stdio -display none
     OBJDUMP_BINARY    = aarch64-none-elf-objdump
     NM_BINARY         = aarch64-none-elf-nm
     READELF_BINARY    = aarch64-none-elf-readelf
@@ -34,7 +34,7 @@
     KERNEL_BIN        = kernel8.img
     QEMU_BINARY       = qemu-system-aarch64
     QEMU_MACHINE_TYPE =
-    QEMU_RELEASE_ARGS = -d in_asm -display none
+    QEMU_RELEASE_ARGS = -serial stdio -display none
     OBJDUMP_BINARY    = aarch64-none-elf-objdump
     NM_BINARY         = aarch64-none-elf-nm
     READELF_BINARY    = aarch64-none-elf-readelf
@@ -70,17 +70,20 @@
     --strip-all            \
     -O binary

-EXEC_QEMU = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE)
+EXEC_QEMU          = $(QEMU_BINARY) -M $(QEMU_MACHINE_TYPE)
+EXEC_TEST_DISPATCH = ruby ../common/tests/dispatch.rb

 ##------------------------------------------------------------------------------
 ## Dockerization
 ##------------------------------------------------------------------------------
-DOCKER_IMAGE        = rustembedded/osdev-utils
-DOCKER_CMD          = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial
-DOCKER_CMD_INTERACT = $(DOCKER_CMD) -i
+DOCKER_IMAGE          = rustembedded/osdev-utils
+DOCKER_CMD            = docker run -t --rm -v $(shell pwd):/work/tutorial -w /work/tutorial
+DOCKER_CMD_INTERACT   = $(DOCKER_CMD) -i
+DOCKER_ARG_DIR_COMMON = -v $(shell pwd)/../common:/work/common

 DOCKER_QEMU  = $(DOCKER_CMD_INTERACT) $(DOCKER_IMAGE)
 DOCKER_TOOLS = $(DOCKER_CMD) $(DOCKER_IMAGE)
+DOCKER_TEST  = $(DOCKER_CMD) $(DOCKER_ARG_DIR_COMMON) $(DOCKER_IMAGE)



@@ -168,3 +171,28 @@
 ##------------------------------------------------------------------------------
 check:
 	@RUSTFLAGS=&quot;$(RUSTFLAGS)&quot; $(CHECK_CMD) --message-format=json
+
+
+
+##--------------------------------------------------------------------------------------------------
+## Testing targets
+##--------------------------------------------------------------------------------------------------
+.PHONY: test test_boot
+
+ifeq ($(QEMU_MACHINE_TYPE),) # QEMU is not supported for the board.
+
+test_boot test :
+	$(call colorecho, &quot;\n$(QEMU_MISSING_STRING)&quot;)
+
+else # QEMU is supported.
+
+##------------------------------------------------------------------------------
+## Run boot test
+##------------------------------------------------------------------------------
+test_boot: $(KERNEL_BIN)
+	$(call colorecho, &quot;\nBoot test - $(BSP)&quot;)
+	@$(DOCKER_TEST) $(EXEC_TEST_DISPATCH) $(EXEC_QEMU) $(QEMU_RELEASE_ARGS) -kernel $(KERNEL_BIN)
+
+test: test_boot
+
+endif

diff -uNr 02_runtime_init/src/bsp/raspberrypi/console.rs 03_hacky_hello_world/src/bsp/raspberrypi/console.rs
--- 02_runtime_init/src/bsp/raspberrypi/console.rs
+++ 03_hacky_hello_world/src/bsp/raspberrypi/console.rs
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! BSP console facilities.
+
+use crate::console;
+use core::fmt;
+
+//--------------------------------------------------------------------------------------------------
+// Private Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// A mystical, magical device for generating QEMU output out of the void.
+struct QEMUOutput;
+
+//--------------------------------------------------------------------------------------------------
+// Private Code
+//--------------------------------------------------------------------------------------------------
+
+/// Implementing `core::fmt::Write` enables usage of the `format_args!` macros, which in turn are
+/// used to implement the `kernel`&apos;s `print!` and `println!` macros. By implementing `write_str()`,
+/// we get `write_fmt()` automatically.
+///
+/// See [`src/print.rs`].
+///
+/// [`src/print.rs`]: ../../print/index.html
+impl fmt::Write for QEMUOutput {
+    fn write_str(&amp;amp;mut self, s: &amp;amp;str) -&amp;gt; fmt::Result {
+        for c in s.chars() {
+            unsafe {
+                core::ptr::write_volatile(0x3F20_1000 as *mut u8, c as u8);
+            }
+        }
+
+        Ok(())
+    }
+}
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// Return a reference to the console.
+pub fn console() -&amp;gt; impl console::interface::Write {
+    QEMUOutput {}
+}

diff -uNr 02_runtime_init/src/bsp/raspberrypi.rs 03_hacky_hello_world/src/bsp/raspberrypi.rs
--- 02_runtime_init/src/bsp/raspberrypi.rs
+++ 03_hacky_hello_world/src/bsp/raspberrypi.rs
@@ -4,4 +4,5 @@

 //! Top-level BSP file for the Raspberry Pi 3 and 4.

+pub mod console;
 pub mod cpu;

diff -uNr 02_runtime_init/src/console.rs 03_hacky_hello_world/src/console.rs
--- 02_runtime_init/src/console.rs
+++ 03_hacky_hello_world/src/console.rs
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! System console.
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Console interfaces.
+pub mod interface {
+    /// Console write functions.
+    ///
+    /// `core::fmt::Write` is exactly what we need for now. Re-export it here because
+    /// implementing `console::Write` gives a better hint to the reader about the
+    /// intention.
+    pub use core::fmt::Write;
+}

diff -uNr 02_runtime_init/src/main.rs 03_hacky_hello_world/src/main.rs
--- 02_runtime_init/src/main.rs
+++ 03_hacky_hello_world/src/main.rs
@@ -104,13 +104,17 @@
 //!     - It is implemented in `src/_arch/__arch_name__/cpu/boot.s`.
 //! 2. Once finished with architectural setup, the arch code calls `kernel_init()`.

+#![feature(format_args_nl)]
 #![feature(global_asm)]
+#![feature(panic_info_message)]
 #![no_main]
 #![no_std]

 mod bsp;
+mod console;
 mod cpu;
 mod panic_wait;
+mod print;

 /// Early init code.
 ///
@@ -118,5 +122,7 @@
 ///
 /// - Only a single core must be active and running this function.
 unsafe fn kernel_init() -&amp;gt; ! {
-    panic!()
+    println!(&quot;[0] Hello from Rust!&quot;);
+
+    panic!(&quot;Stopping here.&quot;)
 }

diff -uNr 02_runtime_init/src/panic_wait.rs 03_hacky_hello_world/src/panic_wait.rs
--- 02_runtime_init/src/panic_wait.rs
+++ 03_hacky_hello_world/src/panic_wait.rs
@@ -4,10 +4,16 @@

 //! A panic handler that infinitely waits.

-use crate::cpu;
+use crate::{cpu, println};
 use core::panic::PanicInfo;

 #[panic_handler]
-fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
+fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
+    if let Some(args) = info.message() {
+        println!(&quot;\nKernel panic: {}&quot;, args);
+    } else {
+        println!(&quot;\nKernel panic!&quot;);
+    }
+
     cpu::wait_forever()
 }

diff -uNr 02_runtime_init/src/print.rs 03_hacky_hello_world/src/print.rs
--- 02_runtime_init/src/print.rs
+++ 03_hacky_hello_world/src/print.rs
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! Printing.
+
+use crate::{bsp, console};
+use core::fmt;
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+#[doc(hidden)]
+pub fn _print(args: fmt::Arguments) {
+    use console::interface::Write;
+
+    bsp::console::console().write_fmt(args).unwrap();
+}
+
+/// Prints without a newline.
+///
+/// Carbon copy from &amp;lt;https://doc.rust-lang.org/src/std/macros.rs.html&amp;gt;
+#[macro_export]
+macro_rules! print {
+    ($($arg:tt)*) =&amp;gt; ($crate::print::_print(format_args!($($arg)*)));
+}
+
+/// Prints with a newline.
+///
+/// Carbon copy from &amp;lt;https://doc.rust-lang.org/src/std/macros.rs.html&amp;gt;
+#[macro_export]
+macro_rules! println {
+    () =&amp;gt; ($crate::print!(&quot;\n&quot;));
+    ($($arg:tt)*) =&amp;gt; ({
+        $crate::print::_print(format_args_nl!($($arg)*));
+    })
+}

diff -uNr 02_runtime_init/tests/boot_test_string.rb 03_hacky_hello_world/tests/boot_test_string.rb
--- 02_runtime_init/tests/boot_test_string.rb
+++ 03_hacky_hello_world/tests/boot_test_string.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+EXPECTED_PRINT = &apos;Stopping here&apos;

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 02 - 运行时初始化</title><link>https://zlotus.github.io/2021/08/03/writing-os-in-rust-on-rpi-02/</link><guid isPermaLink="true">https://zlotus.github.io/2021/08/03/writing-os-in-rust-on-rpi-02/</guid><description>使用Rust在树莓派上编写操作系统 - 02 - 运行时初始化</description><pubDate>Tue, 03 Aug 2021 17:29:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/02_runtime_init/README.md&quot;&gt;Tutorial 02 - Runtime Init&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本章扩展了&lt;code&gt;boot.s&lt;/code&gt;，以首次调用Rust代码。在跳转到Rust之前，一些运行时的初始化工作已经完成。&lt;/li&gt;
&lt;li&gt;本章中被调用的Rust代码只能够通过调用&lt;code&gt;panic!()&lt;/code&gt;宏来停止执行。&lt;/li&gt;
&lt;li&gt;仍旧是查看&lt;code&gt;make qemu&lt;/code&gt;的输出，以观察新增代码的运行情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;需要注意的新增代码&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;新增了一些链接脚本（译注：仍位于&lt;code&gt;link.ld&lt;/code&gt;中）：
&lt;ul&gt;
&lt;li&gt;新增的节包括：&lt;code&gt;.rodata&lt;/code&gt;、&lt;code&gt;.got&lt;/code&gt;、&lt;code&gt;.data&lt;/code&gt;、&lt;code&gt;.bss&lt;/code&gt;。（译注：详情参考&lt;a href=&quot;http://chuquan.me/2018/05/21/elf-introduce/&quot;&gt;ELF文件结构&lt;/a&gt;）&lt;/li&gt;
&lt;li&gt;一个专用位置，用于存放&lt;code&gt;_start()&lt;/code&gt;需要读取的引导时链接参数。（译注：新增&lt;code&gt;src/bsp/raspberrypi/cpu.rs&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_arch/__arch_name__/cpu/boot.s&lt;/code&gt;中的&lt;code&gt;_start()&lt;/code&gt;：
&lt;ol&gt;
&lt;li&gt;如果当前核心不是core0，则挂起该核心。&lt;/li&gt;
&lt;li&gt;初始化&lt;code&gt;内存&lt;/code&gt;，将&lt;a href=&quot;https://en.wikipedia.org/wiki/.bss&quot;&gt;bss&lt;/a&gt;节置为0。&lt;/li&gt;
&lt;li&gt;为Rust入口准备栈指针&lt;code&gt;stack pointer&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;跳转至定义在&lt;code&gt;arch/__arch_name__/cpu/boot.rs&lt;/code&gt;中的&lt;code&gt;_start_rust()&lt;/code&gt;函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_start_rust()&lt;/code&gt;函数：
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;kernel_init()&lt;/code&gt;函数，从而调用&lt;code&gt;panic!()&lt;/code&gt;宏，最终使得core0也被挂起。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;该库目前使用&lt;a href=&quot;https://github.com/rust-embedded/cortex-a&quot;&gt;cortex-a&lt;/a&gt;crate，该crate提供零开销抽象，并在处理CPU相关操作时封装&lt;code&gt;unsafe&lt;/code&gt;代码。
&lt;ul&gt;
&lt;li&gt;具体使用参见&lt;code&gt;_arch/__arch_name__/cpu.rs&lt;/code&gt;中的相关操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;与上一章代码的区别&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;diff -uNr 01_wait_forever/Cargo.toml 02_runtime_init/Cargo.toml
--- 01_wait_forever/Cargo.toml
+++ 02_runtime_init/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = &quot;mingo&quot;
-version = &quot;0.1.0&quot;
+version = &quot;0.2.0&quot;
 authors = [&quot;Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;&quot;]
 edition = &quot;2018&quot;

@@ -21,3 +21,8 @@
 ##--------------------------------------------------------------------------------------------------

 [dependencies]
+
+# Platform specific dependencies
+[target.&apos;cfg(target_arch = &quot;aarch64&quot;)&apos;.dependencies]
+cortex-a = { version = &quot;6.x.x&quot; }
+

diff -uNr 01_wait_forever/Makefile 02_runtime_init/Makefile
--- 01_wait_forever/Makefile
+++ 02_runtime_init/Makefile
@@ -152,6 +152,8 @@
 	$(call colorecho, &quot;\nLaunching objdump&quot;)
 	@$(DOCKER_TOOLS) $(OBJDUMP_BINARY) --disassemble --demangle \
                 --section .text   \
+                --section .rodata \
+                --section .got    \
                 $(KERNEL_ELF) | rustfilt

 ##------------------------------------------------------------------------------

diff -uNr 01_wait_forever/src/_arch/aarch64/cpu/boot.rs 02_runtime_init/src/_arch/aarch64/cpu/boot.rs
--- 01_wait_forever/src/_arch/aarch64/cpu/boot.rs
+++ 02_runtime_init/src/_arch/aarch64/cpu/boot.rs
@@ -13,3 +13,15 @@

 // Assembly counterpart to this file.
 global_asm!(include_str!(&quot;boot.s&quot;));
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// The Rust entry of the `kernel` binary.
+///
+/// The function is called from the assembly `_start` function.
+#[no_mangle]
+pub unsafe fn _start_rust() -&amp;gt; ! {
+    crate::kernel_init()
+}

diff -uNr 01_wait_forever/src/_arch/aarch64/cpu/boot.s 02_runtime_init/src/_arch/aarch64/cpu/boot.s
--- 01_wait_forever/src/_arch/aarch64/cpu/boot.s
+++ 02_runtime_init/src/_arch/aarch64/cpu/boot.s
@@ -3,6 +3,24 @@
 // Copyright (c) 2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;

 //--------------------------------------------------------------------------------------------------
+// Definitions
+//--------------------------------------------------------------------------------------------------
+
+// Load the address of a symbol into a register, PC-relative.
+//
+// The symbol must lie within +/- 4 GiB of the Program Counter.
+//
+// # Resources
+//
+// - https://sourceware.org/binutils/docs-2.36/as/AArch64_002dRelocations.html
+.macro ADR_REL register, symbol
+	adrp	\register, \symbol
+	add	\register, \register, #:lo12:\symbol
+.endm
+
+.equ _core_id_mask, 0b11
+
+//--------------------------------------------------------------------------------------------------
 // Public Code
 //--------------------------------------------------------------------------------------------------
 .section .text._start
@@ -11,6 +29,34 @@
 // fn _start()
 //------------------------------------------------------------------------------
 _start:
+	// Only proceed on the boot core. Park it otherwise.
+	mrs	x1, MPIDR_EL1
+	and	x1, x1, _core_id_mask
+	ldr	x2, BOOT_CORE_ID      // provided by bsp/__board_name__/cpu.rs
+	cmp	x1, x2
+	b.ne	.L_parking_loop
+
+	// If execution reaches here, it is the boot core.
+
+	// Initialize DRAM.
+	ADR_REL	x0, __bss_start
+	ADR_REL x1, __bss_end_exclusive
+
+.L_bss_init_loop:
+	cmp	x0, x1
+	b.eq	.L_prepare_rust
+	stp	xzr, xzr, [x0], #16
+	b	.L_bss_init_loop
+
+	// Prepare the jump to Rust code.
+.L_prepare_rust:
+	// Set the stack pointer.
+	ADR_REL	x0, __boot_core_stack_end_exclusive
+	mov	sp, x0
+
+	// Jump to Rust code.
+	b	_start_rust
+
 	// Infinitely wait for events (aka &quot;park the core&quot;).
 .L_parking_loop:
 	wfe

diff -uNr 01_wait_forever/src/_arch/aarch64/cpu.rs 02_runtime_init/src/_arch/aarch64/cpu.rs
--- 01_wait_forever/src/_arch/aarch64/cpu.rs
+++ 02_runtime_init/src/_arch/aarch64/cpu.rs
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! Architectural processor code.
+//!
+//! # Orientation
+//!
+//! Since arch modules are imported into generic modules using the path attribute, the path of this
+//! file is:
+//!
+//! crate::cpu::arch_cpu
+
+use cortex_a::asm;
+
+//--------------------------------------------------------------------------------------------------
+// Public Code
+//--------------------------------------------------------------------------------------------------
+
+/// Pause execution on the core.
+#[inline(always)]
+pub fn wait_forever() -&amp;gt; ! {
+    loop {
+        asm::wfe()
+    }
+}

diff -uNr 01_wait_forever/src/bsp/raspberrypi/cpu.rs 02_runtime_init/src/bsp/raspberrypi/cpu.rs
--- 01_wait_forever/src/bsp/raspberrypi/cpu.rs
+++ 02_runtime_init/src/bsp/raspberrypi/cpu.rs
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT OR Apache-2.0
+//
+// Copyright (c) 2018-2021 Andre Richter &amp;lt;andre.o.richter@gmail.com&amp;gt;
+
+//! BSP Processor code.
+
+//--------------------------------------------------------------------------------------------------
+// Public Definitions
+//--------------------------------------------------------------------------------------------------
+
+/// Used by `arch` code to find the early boot core.
+#[no_mangle]
+#[link_section = &quot;.text._start_arguments&quot;]
+pub static BOOT_CORE_ID: u64 = 0;

diff -uNr 01_wait_forever/src/bsp/raspberrypi/link.ld 02_runtime_init/src/bsp/raspberrypi/link.ld
--- 01_wait_forever/src/bsp/raspberrypi/link.ld
+++ 02_runtime_init/src/bsp/raspberrypi/link.ld
@@ -11,17 +11,43 @@
 PHDRS
 {
     segment_rx PT_LOAD FLAGS(5); /* 5 == RX */
+    segment_rw PT_LOAD FLAGS(6); /* 6 == RW */
 }

 SECTIONS
 {
     . =  __rpi_load_addr;
+                                        /*   ^             */
+                                        /*   | stack       */
+                                        /*   | growth      */
+                                        /*   | direction   */
+   __boot_core_stack_end_exclusive = .; /*   |             */

     /***********************************************************************************************
-    * Code
+    * Code + RO Data + Global Offset Table
     ***********************************************************************************************/
     .text :
     {
         KEEP(*(.text._start))
+        *(.text._start_arguments) /* Constants (or statics in Rust speak) read by _start(). */
+        *(.text._start_rust)      /* The Rust entry point */
+        *(.text*)                 /* Everything else */
     } :segment_rx
+
+    .rodata : ALIGN(8) { *(.rodata*) } :segment_rx
+    .got    : ALIGN(8) { *(.got)     } :segment_rx
+
+    /***********************************************************************************************
+    * Data + BSS
+    ***********************************************************************************************/
+    .data : { *(.data*) } :segment_rw
+
+    /* Section is zeroed in pairs of u64. Align start and end to 16 bytes */
+    .bss : ALIGN(16)
+    {
+        __bss_start = .;
+        *(.bss*);
+        . = ALIGN(16);
+        __bss_end_exclusive = .;
+    } :NONE
 }

diff -uNr 01_wait_forever/src/bsp/raspberrypi.rs 02_runtime_init/src/bsp/raspberrypi.rs
--- 01_wait_forever/src/bsp/raspberrypi.rs
+++ 02_runtime_init/src/bsp/raspberrypi.rs
@@ -4,4 +4,4 @@

 //! Top-level BSP file for the Raspberry Pi 3 and 4.

-// Coming soon.
+pub mod cpu;

diff -uNr 01_wait_forever/src/cpu.rs 02_runtime_init/src/cpu.rs
--- 01_wait_forever/src/cpu.rs
+++ 02_runtime_init/src/cpu.rs
@@ -4,4 +4,13 @@

 //! Processor code.

+#[cfg(target_arch = &quot;aarch64&quot;)]
+#[path = &quot;_arch/aarch64/cpu.rs&quot;]
+mod arch_cpu;
+
 mod boot;
+
+//--------------------------------------------------------------------------------------------------
+// Architectural Public Reexports
+//--------------------------------------------------------------------------------------------------
+pub use arch_cpu::wait_forever;

diff -uNr 01_wait_forever/src/main.rs 02_runtime_init/src/main.rs
--- 01_wait_forever/src/main.rs
+++ 02_runtime_init/src/main.rs
@@ -102,8 +102,8 @@
 //!
 //! 1. The kernel&apos;s entry point is the function `cpu::boot::arch_boot::_start()`.
 //!     - It is implemented in `src/_arch/__arch_name__/cpu/boot.s`.
+//! 2. Once finished with architectural setup, the arch code calls `kernel_init()`.

-#![feature(asm)]
 #![feature(global_asm)]
 #![no_main]
 #![no_std]
@@ -112,4 +112,11 @@
 mod cpu;
 mod panic_wait;

-// Kernel code coming next tutorial.
+/// Early init code.
+///
+/// # Safety
+///
+/// - Only a single core must be active and running this function.
+unsafe fn kernel_init() -&amp;gt; ! {
+    panic!()
+}

diff -uNr 01_wait_forever/src/panic_wait.rs 02_runtime_init/src/panic_wait.rs
--- 01_wait_forever/src/panic_wait.rs
+++ 02_runtime_init/src/panic_wait.rs
@@ -4,9 +4,10 @@

 //! A panic handler that infinitely waits.

+use crate::cpu;
 use core::panic::PanicInfo;

 #[panic_handler]
 fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
-    unimplemented!()
+    cpu::wait_forever()
 }

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 01 - 无限循环</title><link>https://zlotus.github.io/2021/08/02/writing-os-in-rust-on-rpi-01/</link><guid isPermaLink="true">https://zlotus.github.io/2021/08/02/writing-os-in-rust-on-rpi-01/</guid><description>使用Rust在树莓派上编写操作系统 - 01 - 无限循环</description><pubDate>Mon, 02 Aug 2021 17:29:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/01_wait_forever/README.md&quot;&gt;Tutorial 01 - Wait Forever&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;本章搭建了项目骨架。&lt;/li&gt;
&lt;li&gt;一小段汇编代码让CPU的所有核心都执行内核代码。（译注：执行后CPU占用率100%）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;构建&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Makefile&lt;/code&gt;目标:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;doc&lt;/code&gt;：生成文档。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;qemu&lt;/code&gt;：在QEMU中运行&lt;code&gt;内核&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clippy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;clean&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readelf&lt;/code&gt;：检查&lt;code&gt;ELF&lt;/code&gt;输出。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;objdump&lt;/code&gt;：检查汇编。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nm&lt;/code&gt;：检查符号。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;需要关注的代码&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BSP&lt;/code&gt;中的&lt;code&gt;link.ld&lt;/code&gt;为链接脚本。
&lt;ul&gt;
&lt;li&gt;加载地址为&lt;code&gt;0x8_0000&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;目前只有一个节（section）：代码节&lt;code&gt;.text&lt;/code&gt;。（译注：详情参考&lt;a href=&quot;http://chuquan.me/2018/05/21/elf-introduce/&quot;&gt;ELF文件结构&lt;/a&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;main.rs&lt;/code&gt;中主要关注几个&lt;a href=&quot;https://doc.rust-lang.org/reference/attributes.html&quot;&gt;内部属性&lt;/a&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#![no_std]&lt;/code&gt;（译注：编译禁用自动引用标准库）, &lt;code&gt;#![no_main]&lt;/code&gt;（译注：不使用&lt;code&gt;main&lt;/code&gt;函数，我们自己提供入口点函数）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;boot.s&lt;/code&gt;中：汇编函数&lt;code&gt;_start()&lt;/code&gt;将执行&lt;code&gt;wfe&lt;/code&gt;（等待事件），挂起所有执行&lt;code&gt;_start()&lt;/code&gt;CPU核心。&lt;/li&gt;
&lt;li&gt;我们需要定义一个&lt;code&gt;#[panic_handler]&lt;/code&gt;以使编译通过。
&lt;ul&gt;
&lt;li&gt;使用&lt;a href=&quot;https://doc.rust-lang.org/core/macro.unimplemented.html&quot;&gt;&lt;code&gt;unimplemented!()&lt;/code&gt;&lt;/a&gt;宏作为占位符，否则可能会被编译器优化掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 00 - 前言</title><link>https://zlotus.github.io/2021/08/01/writing-os-in-rust-on-rpi-00/</link><guid isPermaLink="true">https://zlotus.github.io/2021/08/01/writing-os-in-rust-on-rpi-00/</guid><description>使用Rust在树莓派上编写操作系统 - 00 - 前言</description><pubDate>Sun, 01 Aug 2021 11:56:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文前半部分内容翻译自原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/00_before_we_start/README.md&quot;&gt;Before we start&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;h1&gt;在我们开始之前&lt;/h1&gt;
&lt;p&gt;以下文本是文档的1:1副本，会出现在每章教程所对应的内核主要源文件的顶部。文档描述了相应源文件的结构，并尝试解释该实现背后的原理。请阅读该文档，以熟悉将会在教程中遇到的内容，文档将帮助你更好地浏览代码，并理解每一章教程之间的递进关系。&lt;/p&gt;
&lt;p&gt;另请注意，以下文本将引用第一批教程中尚不存在的源文件（如&lt;code&gt;**/memory.rs&lt;/code&gt;）或函数。随着教程的推进，它们将逐渐被添加。&lt;/p&gt;
&lt;p&gt;玩得开心！&lt;/p&gt;
&lt;h1&gt;代码组织和架构&lt;/h1&gt;
&lt;p&gt;代码分为不同的&lt;em&gt;模块&lt;/em&gt;，每个模块代表一个典型的&lt;code&gt;内核&lt;/code&gt;&lt;strong&gt;子系统&lt;/strong&gt;。子系统的顶层模块文件直接位于&lt;code&gt;src&lt;/code&gt;文件夹中。例如，&lt;code&gt;src/memory.rs&lt;/code&gt;将包含与所有内存管理相关的代码。&lt;/p&gt;
&lt;h2&gt;处理器架构代码的可见性&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;内核&lt;/code&gt;的某些子系统基于特定目标处理器架构的底层代码。对于每个支持的处理器架构，在&lt;code&gt;src/_arch&lt;/code&gt;中都存在一个子文件夹，例如，&lt;code&gt;src/_arch/aarch64&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这些架构文件夹反映了在&lt;code&gt;src&lt;/code&gt;中布置的子系统模块。例如，属于&lt;code&gt;内核&lt;/code&gt;MMU子系统（&lt;code&gt;src/memory/mmu.rs&lt;/code&gt;）的架构相关代码将位于&lt;code&gt;src/_arch/aarch64/memory/mmu.rs&lt;/code&gt;。里面的文件将以模块的方式，通过&lt;code&gt;path&lt;/code&gt;属性加载到&lt;code&gt;src/memory/mmu.rs&lt;/code&gt;中。通常使用以&lt;code&gt;arch_&lt;/code&gt;为前缀，后接通用模块名称，作为导入的模块名称。&lt;/p&gt;
&lt;p&gt;例如，这是&lt;code&gt;src/memory/mmu.rs&lt;/code&gt;的前几行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[cfg(target_arch = &quot;aarch64&quot;)]
#[path = &quot;../_arch/aarch64/memory/mmu.rs&quot;]
mod arch_mmu;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大多数情况下，&lt;code&gt;arch_module&lt;/code&gt;中的项会被父模块以公开方式重导出。如此，针对各架构的模块就可以对外提供其各项实现，而调用者不必关心哪个架构已经被条件编译了。&lt;/p&gt;
&lt;h2&gt;BSP代码&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;BSP&lt;/code&gt;即Board Support Package。&lt;code&gt;BSP&lt;/code&gt;代码位于&lt;code&gt;src/bsp.rs&lt;/code&gt;下，包含目标板特定的变量和函数，例如目标板内存的映射、目标板特有设备的驱动程序实例之类的东西。&lt;/p&gt;
&lt;p&gt;就像处理器架构代码那样，&lt;code&gt;BSP&lt;/code&gt;代码的模块结构，类似&lt;code&gt;内核&lt;/code&gt;子系统代码的模块结构，只不过这次没有重导出。这意味着必须从&lt;code&gt;bsp&lt;/code&gt;命名空间开始调用其提供的任何内容，例如&lt;code&gt;bsp::driver::driver_manager()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;内核接口&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;arch&lt;/code&gt;和&lt;code&gt;bsp&lt;/code&gt;都包含条件编译的代码，具体取决于编译内核的目标环境和板卡。例如，&lt;code&gt;树莓派3&lt;/code&gt;和&lt;code&gt;树莓派4&lt;/code&gt;的&lt;code&gt;中断控制器&lt;/code&gt;的硬件是不同的，但我们希望其余的&lt;code&gt;内核&lt;/code&gt;能够很方便地与两者中的任何一个一起使用。&lt;/p&gt;
&lt;p&gt;为了在&lt;code&gt;arch&lt;/code&gt;、&lt;code&gt;bsp&lt;/code&gt;和&lt;code&gt;通用内核代码&lt;/code&gt;之间提供一个清晰的抽象，我们在&lt;em&gt;只要有可能&lt;/em&gt;且&lt;em&gt;有意义的地方&lt;/em&gt;封装了&lt;code&gt;interface&lt;/code&gt;trait。这些trait定义在各子系统模块中，以强制我们实现&lt;em&gt;面向接口编程，而是不面向实现编程&lt;/em&gt;。例如，将有一个通用IRQ处理接口，由两个版本树莓派各自的中断控制器&lt;code&gt;驱动&lt;/code&gt;实现，并且只将该接口导出到&lt;code&gt;内核&lt;/code&gt;的其余部分。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        +-------------------+
        | Interface (Trait) |
        |                   |
        +--+-------------+--+
           ^             ^
           |             |
           |             |
+----------+--+       +--+----------+
| kernel code |       |  bsp code   |
|             |       |  arch code  |
+-------------+       +-------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;对于逻辑&lt;code&gt;内核&lt;/code&gt;子系统，相应的代码可以分发在不同的物理位置。以&lt;strong&gt;内存&lt;/strong&gt;子系统为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;src/memory.rs&lt;/code&gt;与&lt;code&gt;src/memory/**/*&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;通用代码并不知道目标处理器架构和&lt;code&gt;BSP&lt;/code&gt;特征。
&lt;ul&gt;
&lt;li&gt;示例：一个用于将内存块归零的函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;由&lt;code&gt;arch&lt;/code&gt;或&lt;code&gt;BSP&lt;/code&gt;中的代码实现的内存子系统的接口。
&lt;ul&gt;
&lt;li&gt;示例：定义&lt;code&gt;MMU&lt;/code&gt;函数原型的&lt;code&gt;MMU&lt;/code&gt;接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/bsp/__board_name__/memory.rs&lt;/code&gt;与&lt;code&gt;src/bsp/__board_name__/memory/**/*&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;特定&lt;code&gt;BSP&lt;/code&gt;的代码。&lt;/li&gt;
&lt;li&gt;示例：特定板子的内存映射（DRAM和MMIO设备的物理地址）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;src/_arch/__arch_name__/memory.rs&lt;/code&gt;与&lt;code&gt;src/_arch/__arch_name__/memory/**/*&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;特定处理器架构的代码。&lt;/li&gt;
&lt;li&gt;示例：实现&lt;code&gt;__arch_name__&lt;/code&gt;处理器架构的&lt;code&gt;MMU&lt;/code&gt;接口。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从名称空间的角度来看，&lt;strong&gt;内存&lt;/strong&gt;子系统代码位于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;crate::memory::*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;crate::bsp::memory::*&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;启动流程&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;内核的入口点是函数&lt;code&gt;cpu::boot::arch_boot::_start()&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;其实现位于&lt;code&gt;src/_arch/__arch_name__/cpu/boot.rs&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;下面不是翻译，是我写的使用方法。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;下载&lt;/h2&gt;
&lt;p&gt;直接克隆本教程即可使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gh repo clone rust-embedded/rust-raspberrypi-OS-tutorials
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;rust-analyzer&lt;/h2&gt;
&lt;p&gt;我的macOS貌似通过&lt;code&gt;macports&lt;/code&gt;装了另一个版本的Rust，要知道通过&lt;code&gt;rustup&lt;/code&gt;安装的Rust位于&lt;code&gt;~/.cargo/bin/&lt;/code&gt;中，而&lt;code&gt;port&lt;/code&gt;安装的Rust位于&lt;code&gt;/opt/local/bin/&lt;/code&gt;中。这导致vscode启动时，默认加载的环境变量可能会使用&lt;code&gt;port&lt;/code&gt;的版本，而该版本并不是&lt;code&gt;rustup&lt;/code&gt;安装的，可能缺少&lt;code&gt;rust-src&lt;/code&gt;等组件，这会导致rust-analyzer工作异常。尝试解决：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;😕 直接&lt;code&gt;export&lt;/code&gt;环境变量后启动vscode，能解决问题，但是每次启动很麻烦，并不优雅。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;😕 修改shell的各个&lt;code&gt;profile&lt;/code&gt;环境变量，但是这会牵连出很多未知问题，因为可能影响到其他程序的环境变量搜索顺序。（附：shell各profile的加载顺序介绍&lt;a href=&quot;https://medium.com/@rajsek/zsh-bash-startup-files-loading-order-bashrc-zshrc-etc-e30045652f2e&quot;&gt;1&lt;/a&gt;&lt;a href=&quot;https://www.erikstockmeier.com/blog/12-5-2019-shell-startup-scripts&quot;&gt;2&lt;/a&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;😕 尝试修改rust项目中配置文件的&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/config.html#buildrustc&quot;&gt;&lt;code&gt;rustc&lt;/code&gt;设置&lt;/a&gt;，发现对rust-analyzer并不起作用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;😕 直接卸载&lt;code&gt;port&lt;/code&gt;版本，简单有效，但是由于忘记了&lt;code&gt;port&lt;/code&gt;版本是不是我有意识安装的，所以并不知道会不会对其他程序产生影响。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🎉 修改vscode的rust-analyzer插件的&lt;a href=&quot;https://github.com/rust-analyzer/rust-analyzer/issues/3154#issuecomment-774367324&quot;&gt;&lt;code&gt;extraEnv&lt;/code&gt;配置&lt;/a&gt;，该配置位于：扩展: rust-analyzer -&amp;gt; 扩展设置 -&amp;gt; 搜索&lt;code&gt;extraEnv&lt;/code&gt; -&amp;gt; 在 setting.json 中编辑，打开编辑器，向&lt;code&gt;extraEnv&lt;/code&gt;字典新增两行路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &quot;rust-analyzer.server.extraEnv&quot;: {
        &quot;RUSTC&quot;: &quot;/Users/zealot/.cargo/bin/rustc&quot;,
        &quot;RUSTDOC&quot;: &quot;/Users/zealot/.cargo/bin/rustdoc&quot;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新加载即可。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;运行&lt;/h2&gt;
&lt;p&gt;自第一章起，每一章都是一个可执行的小系统，比如进入&lt;code&gt;01_WAIT_FOREVER&lt;/code&gt;目录，即可执行&lt;code&gt;make qemu&lt;/code&gt;来编译，并在虚拟环境中运行内核。&lt;/p&gt;
</content:encoded></item><item><title>使用Rust在树莓派上编写操作系统 - 项目介绍</title><link>https://zlotus.github.io/2021/08/01/writing-os-in-rust-on-rpi_/</link><guid isPermaLink="true">https://zlotus.github.io/2021/08/01/writing-os-in-rust-on-rpi_/</guid><description>使用Rust在树莓派上编写操作系统 - 00 - 项目介绍</description><pubDate>Sun, 01 Aug 2021 11:56:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/README.md&quot;&gt;README.md&lt;/a&gt;；原项目：&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/raw/master/doc/minipush_demo_frontpage.gif&quot; width=&quot;79%&quot; height=&quot;79%&quot;&amp;gt;&lt;/p&gt;
&lt;h2&gt;ℹ️ 介绍&lt;/h2&gt;
&lt;p&gt;&amp;lt;img src=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/raw/master/doc/header.jpg&quot; width=&quot;79%&quot; height=&quot;79%&quot;&amp;gt;&lt;/p&gt;
&lt;p&gt;这是一个教程系列，面向那些不熟悉ARM 64 bit &lt;a href=&quot;https://developer.arm.com/products/architecture/cpu-architecture/a-profile/docs&quot;&gt;ARMv8-A架构&lt;/a&gt;的操作系统开发爱好者。这些教程将逐步指导如何从头开始为&lt;strong&gt;嵌入式系统&lt;/strong&gt;编写&lt;a href=&quot;https://en.wikipedia.org/wiki/Monolithic_kernel&quot;&gt;宏内核&lt;/a&gt;。教程涵盖了常见操作系统功能的实现，例如向串口写数据、设置虚拟内存和处理硬件异常。同时，利用&lt;strong&gt;Rust&lt;/strong&gt;特有的语言特性来提供内存安全性和执行速度。&lt;/p&gt;
&lt;p&gt;祝读者们看得开心！&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Andre (&lt;a href=&quot;https://github.com/andre-richter&quot;&gt;@andre-richter&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;P.S.：中文🇨🇳译版的教程由&lt;a href=&quot;https://github.com/colachg&quot;&gt;@colachg&lt;/a&gt;和&lt;a href=&quot;https://github.com/readlnh&quot;&gt;@readlnh&lt;/a&gt;发起。您可以在相应的文件夹中以&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/README.CN.md&quot;&gt;&lt;code&gt;README.CN.md&lt;/code&gt;&lt;/a&gt;的形式找到这些翻译。当然，翻译会一定程度上落后原文的进度。&lt;/p&gt;
&lt;h2&gt;📑 组织&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;教程的每一章都包含一个独立可引导的二进制&lt;strong&gt;内核&lt;/strong&gt;文件。&lt;/li&gt;
&lt;li&gt;每一篇新的教程都建立在前一篇的基础上。&lt;/li&gt;
&lt;li&gt;每一篇教程的&lt;code&gt;README&lt;/code&gt;文件都会有一个简短的&lt;code&gt;tl;dr&lt;/code&gt;小节，用于简要概述本章的新内容，并展示与前一篇教程源代码的&lt;code&gt;diff&lt;/code&gt;，以方便读者检查修改和新增的代码。
&lt;ul&gt;
&lt;li&gt;除了&lt;code&gt;tl;dr&lt;/code&gt;小节之外，一些教程还配有完整、详尽的文本说明。这个系列教程的长期计划是为教程的所有章节都编写详细说明，不过目前我仅为我认为&lt;code&gt;tl;dr&lt;/code&gt;和&lt;code&gt;diff&lt;/code&gt;不足以解释清楚的教程章节编写详细说明。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;这些教程中的代码支持在&lt;strong&gt;树莓派3&lt;/strong&gt;和&lt;strong&gt;树莓派4&lt;/strong&gt;上运行。
&lt;ul&gt;
&lt;li&gt;教程的第1章到第5章是基础代码，仅用于在&lt;code&gt;QEMU&lt;/code&gt;中运行。&lt;/li&gt;
&lt;li&gt;从教程的&lt;a href=&quot;./writing-os-in-rust-on-rpi-05.md&quot;&gt;第5章&lt;/a&gt;开始，您就可以在真正的树莓派上加载运行内核，并通过&lt;code&gt;UART&lt;/code&gt;观察输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;尽管教程将树莓派3和4作为为目标开发板，但文章中的代码均以模块化方式编写，可以轻松移植到其他CPU架构或开发板上。
&lt;ul&gt;
&lt;li&gt;如果真的有朋友尝试为&lt;code&gt;RISC-V&lt;/code&gt;指令集编写实现我将非常高兴！&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;代码编辑器的选择，我推荐安装&lt;a href=&quot;https://rust-analyzer.github.io/&quot;&gt;Rust Analyzer&lt;/a&gt;插件的&lt;a href=&quot;https://code.visualstudio.com/&quot;&gt;Visual Studio Code&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;除了阅读教程每一章的文本之外，还可以使用&lt;code&gt;cargo&lt;/code&gt;的&lt;code&gt;make doc&lt;/code&gt;命令，以方便的查看源码中丰富的文档和注释。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;code&gt;make doc&lt;/code&gt; 的输出&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/doc/make_doc.png&quot; alt=&quot;制作文档&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;🛠 系统要求&lt;/h2&gt;
&lt;p&gt;本教程主要针对基于&lt;strong&gt;Linux&lt;/strong&gt;的发行版。大部分代码同样适用于一些Unix版本，例如&lt;strong&gt;macOS&lt;/strong&gt;，但这只是&lt;em&gt;实验性&lt;/em&gt;的。&lt;/p&gt;
&lt;h3&gt;🚀 tl;dr 版本&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.docker.com/get-docker/&quot;&gt;安装Docker&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保你的用户帐户在&lt;a href=&quot;https://docs.docker.com/engine/install/linux-postinstall/&quot;&gt;docker group&lt;/a&gt;中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;准备 &lt;code&gt;Rust&lt;/code&gt; 工具链。第一次使用&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/rust-toolchain&quot;&gt;rust-toolchain&lt;/a&gt;就能够自动搞定大部分工具。而需要我们手动操作的是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果你已经安装了Rust：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cargo install cargo-binutils rustfilt
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;如果你需要从头安装Rust：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;curl --proto &apos;=https&apos; --tlsv1.2 -sSf https://sh.rustup.rs | sh

source $HOME/.cargo/env
cargo install cargo-binutils rustfilt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果您使用&lt;code&gt;Visual Studio Code&lt;/code&gt;，我们强烈建议你使用&lt;a href=&quot;https://marketplace.visualstudio.com/items?itemName=matklad.rust-analyzer&quot;&gt;Rust Analyzer插件&lt;/a&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果你&lt;strong&gt;不&lt;/strong&gt;使用Linux，则还需要安装一些 &lt;code&gt;Ruby&lt;/code&gt;gems：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo gem install bundler
bundle config set path &apos;.vendor/bundle&apos;
bundle install
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;🧰 更多细节：消除工具链麻烦&lt;/h3&gt;
&lt;p&gt;本系列试图将重点放在用户友好性上。因此，已尽可能消除嵌入式开发中最大的痛点：&lt;code&gt;工具链麻烦&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;Rust本身已经在这方面提供了很大帮助，因为它内置了对交叉编译的支持。我们从&lt;code&gt;x86&lt;/code&gt;主机交叉编译到树莓派的&lt;code&gt;AArch64&lt;/code&gt;架构所需的一切都将由&lt;code&gt;rustup&lt;/code&gt;自动安装。但是，除了Rust编译器之外，我们还将使用更多工具。其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;QEMU&lt;/code&gt;：用于在编程机上模拟目标机的内核。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Minipush&lt;/code&gt;：我们自制的小工具，可通过&lt;code&gt;UART&lt;/code&gt;按需将内核加载到树莓派上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OpenOCD&lt;/code&gt;和&lt;code&gt;GDB&lt;/code&gt;：用于在目标机上进行调试。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在编程主机上安装、编译每个工具的正确版本时，可能会出错很多。例如，编程机的发行版可能不提供所需的最新版本。或者在编译这些工具之一时缺少一些难以获得的依赖项。&lt;/p&gt;
&lt;p&gt;这就是为什么我们会尽可能使用&lt;a href=&quot;https://docs.docker.com/get-docker/&quot;&gt;Docker&lt;/a&gt;。我们提供了一个附带的容器，其中预装了所有需要的工具或依赖项，一旦有需要docker就会自动pull下来。如果您想了解有关Docker的更多信息，并查看我们提供的容器，请查看&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/docker&quot;&gt;docker&lt;/a&gt;文件夹。&lt;/p&gt;
&lt;h2&gt;📟 USB 串行输出&lt;/h2&gt;
&lt;p&gt;由于教程中开发的内核在真实硬件上运行，因此强烈建议您购买USB转串口线缆以获得完整体验。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;你可以购买诸如&lt;a href=&quot;https://www.amazon.de/dp/B0757FQ5CX/ref=cm_sw_r_tw_dp_U_x_ozGRDbVTJAG4Q&quot;&gt;[1]&lt;/a&gt; &lt;a href=&quot;https://www.adafruit.com/product/954&quot;&gt;[2]&lt;/a&gt;的USB转串口线缆，当然还有许多其他类似的线缆。总的来说，你的线缆最好基于&lt;code&gt;CP2102&lt;/code&gt;芯片。&lt;/li&gt;
&lt;li&gt;将线缆连接到树莓派的&lt;code&gt;GND&lt;/code&gt;和GPIO&lt;code&gt;14/15&lt;/code&gt;引脚，如下所示。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;05_drivers_gpio_uart&quot;&gt;第5章&lt;/a&gt;是第一个用到它的地方。查看有关如何准备SD卡以从中引导自制内核的说明。&lt;/li&gt;
&lt;li&gt;从&lt;a href=&quot;06_uart_chainloader&quot;&gt;第6章&lt;/a&gt;开始，在树莓派上启动内核就会变得&lt;em&gt;很&lt;/em&gt;很舒服。本教程开发了一个名为&lt;code&gt;chainloader&lt;/code&gt;的程序，用于在最后手动将内核拷贝到树莓派上，并将使你能够在启动期间通过&lt;code&gt;UART&lt;/code&gt;按需加载教程内核。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials/blob/master/doc/wiring.png?raw=true&quot; alt=&quot;UART接线图&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;🙌 致谢&lt;/h2&gt;
&lt;p&gt;教程的原始版本最初是基于&lt;a href=&quot;https://github.com/bztsrc&quot;&gt;Zoltan Baldaszti&lt;/a&gt;用&lt;code&gt;C&lt;/code&gt;编写的[RPi3上的裸机编程教程](https://github.com/bztsrc/ raspi3-tutorial。谢谢你给了我们一个良好的开始！&lt;/p&gt;
&lt;h2&gt;许可&lt;/h2&gt;
&lt;p&gt;以下两种许可均可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Apache 许可证，版本 2.0，(&lt;a href=&quot;LICENSE-APACHE&quot;&gt;LICENSE-APACHE&lt;/a&gt; 或 http://www.apache.org/licenses/LICENSE-2.0)&lt;/li&gt;
&lt;li&gt;MIT 许可证 (&lt;a href=&quot;LICENSE-MIT&quot;&gt;LICENSE-MIT&lt;/a&gt; 或 http://opensource.org/licenses/MIT)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;参与本项目&lt;/h3&gt;
&lt;p&gt;您的提交将如Apache-2.0许可定义的那样，会纳入上述两个许可，且不包含任何附加条款。&lt;/p&gt;
</content:encoded></item><item><title>使用树莓派为音响添加AirPlay功能</title><link>https://zlotus.github.io/2021/06/14/stereo-with-rpi-airplay/</link><guid isPermaLink="true">https://zlotus.github.io/2021/06/14/stereo-with-rpi-airplay/</guid><description>使用树莓派为音响添加AirPlay功能</description><pubDate>Mon, 14 Jun 2021 09:08:00 GMT</pubDate><content:encoded>&lt;p&gt;今年618大出血买了音响功放，接口很多，但是就是没用接入WiFi当网络音响的功能，也就是苹果的AirPlay或安卓的DLNA/UPnP。虽然天逸已经提供了安卓APP，但是目前并没有苹果的APP，不过好在我有空闲的树莓派，可以将功放升级成AirPlay设备。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./stereo-with-rpi-airplay/stereo-with-rpi-airplay-0.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;背景&lt;/h2&gt;
&lt;p&gt;一开始，我是把大硬盘直接挂在树莓派上，用Raspberry OS系统GUI播放音乐（比如用vlc player），这期间总是出现各种各样的不爽，比如压缩软件不顺手啊、中/日/法语字符集不全啊、2.4G的WiFi信号晚上不稳定啊什么的……所以就想利用树莓派给功放的DAC加上AirPlay功能，直接挂在家里的WiFi上或插网线做多媒体音箱。&lt;/p&gt;
&lt;h2&gt;1 &lt;a href=&quot;https://github.com/mikebrady/shairport-sync&quot;&gt;&lt;code&gt;shairport-sync&lt;/code&gt;&lt;/a&gt; - AirPlay&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;shairport-sync&lt;/code&gt;这个库是专门用来支持AirPlay的，主要参考树莓派的&lt;a href=&quot;https://github.com/mikebrady/shairport-sync/blob/master/INSTALL.md&quot;&gt;简要安装说明&lt;/a&gt;以及&lt;a href=&quot;https://github.com/mikebrady/shairport-sync/blob/master/TROUBLESHOOTING.md#wifi-adapter-running-in-power-saving--low-power-mode&quot;&gt;故障排查&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;例行调整命令行的用户体验：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y ufw zsh vim tmux
chsh -s $(which zsh)
sh -c &quot;$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.1 关闭WiFi适配器的省电模式或者低功耗模式&lt;/h3&gt;
&lt;p&gt;对于树莓派3或者更高级的自带WiFi的板子，需要手动关闭省电模式，否则AirPlay可能会在家里的WiFi环境中消失一段时间，或者产生很大的延迟：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo iwconfig wlan0 power off
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过我打算用的是树莓派2B，&lt;code&gt;iwconfig&lt;/code&gt;里能看到&lt;code&gt;Power Management:off&lt;/code&gt;，因此不需要做上面的操作。&lt;/p&gt;
&lt;h3&gt;1.2 添加防火墙例外&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow proto tcp to 0.0.0.0/0 port 22 comment &quot;sshd listen port&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 80 comment &quot;for http server&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 5900 comment &quot;vnc server listen port&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 8000 comment &quot;for python -m http.server 8000&quot;

sudo ufw allow from 192.168.1.1/24 to any port 5353 comment &quot;shairport-sync&quot;
sudo ufw allow proto tcp from 192.168.1.1/24 to any port 3689 comment &quot;shairport-sync&quot;
sudo ufw allow proto tcp from 192.168.1.1/24 to any port 5000:5005 comment &quot;shairport-sync&quot;
sudo ufw allow proto udp from 192.168.1.1/24 to any port 6000:6005 comment &quot;shairport-sync&quot;
sudo ufw allow proto udp from 192.168.1.1/24 to any port 35000:65535 comment &quot;shairport-sync&quot;

sudo ufw default deny 

sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.3 安装依赖&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y build-essential git xmltoman autoconf automake libtool \
    libpopt-dev libconfig-dev libasound2-dev avahi-daemon \
    libavahi-client-dev libssl-dev libsoxr-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.4 下载并安装&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/mikebrady/shairport-sync.git
cd shairport-sync
autoreconf -fi
./configure --sysconfdir=/etc --with-alsa --with-soxr --with-avahi --with-ssl=openssl --with-systemd
make
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.5 设置开机启动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable shairport-sync
sudo systemctl start shairport-sync
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;1.6 设置配置文件&lt;/h3&gt;
&lt;p&gt;我的板子是&lt;code&gt;Raspberry Pi 2 Model B Rev 1.1&lt;/code&gt;，把天逸AD86-D功放miniUSB声卡插在树莓派的USB口上，在功放切换到PC模式时，&lt;code&gt;aplay -l&lt;/code&gt;的输出为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;**** List of PLAYBACK Hardware Devices ****
card 0: b1 [bcm2835 HDMI 1], device 0: bcm2835 HDMI 1 [bcm2835 HDMI 1]
  Subdevices: 4/4
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
card 1: Headphones [bcm2835 Headphones], device 0: bcm2835 Headphones [bcm2835 Headphones]
  Subdevices: 4/4
  Subdevice #0: subdevice #0
  Subdevice #1: subdevice #1
  Subdevice #2: subdevice #2
  Subdevice #3: subdevice #3
card 2: AUDIO [USB AUDIO], device 0: USB Audio [USB Audio]
  Subdevices: 0/1
  Subdevice #0: subdevice #0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，我应该将&lt;code&gt;shairport-sync&lt;/code&gt;配置文件（在&lt;code&gt;make install&lt;/code&gt;输出中可以看到配置文件位于&lt;code&gt;/etc/shairport-sync.conf&lt;/code&gt;）的&lt;code&gt;alsa&lt;/code&gt;部分里&lt;code&gt;output_device&lt;/code&gt;改为&lt;code&gt;hw:2&lt;/code&gt;（这里写&lt;code&gt;hw:2,0&lt;/code&gt;应该也可以），通过&lt;code&gt;alsamixer&lt;/code&gt;选择USB AUDIO声卡时可以看到&lt;code&gt;mixer&lt;/code&gt;是&lt;code&gt;PCM&lt;/code&gt;，所以&lt;code&gt;mixer_control_name&lt;/code&gt;也得修改为&lt;code&gt;PCM&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alsa =
{
  output_device = &quot;hw:2&quot;; // the name of the alsa output device. Use &quot;shairport-sync -h&quot; to discover the names of ALSA hardware devices. &quot;alsamixer&quot; or &quot;aplay&quot; to find out the names of devices, mixers, etc.
  mixer_control_name = &quot;PCM&quot;; // the name of the mixer to use to adjust output volume. If not specified, volume in adjusted in software.
  ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存并重启服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start shairport-sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时可以在macOS的声音控制面板中看到一个名为&lt;code&gt;Raspberrypi&lt;/code&gt;的扬声器。切换到这个扬声器时，电脑只能控制是否静音，不能控制音量；而树莓派上的&lt;code&gt;alsamixer&lt;/code&gt;不论选什么卡都无法控制音量，因此只能通过功放控制音量。&lt;/p&gt;
&lt;h2&gt;2 &lt;code&gt;samba&lt;/code&gt; - 文件夹共享&lt;/h2&gt;
&lt;p&gt;为了解决把硬盘挂载树莓派上不爽的体验，可以选择用&lt;code&gt;samba&lt;/code&gt;把盘共享在网络上。不过&lt;code&gt;samba&lt;/code&gt;库有个几十兆，可以临时调整树莓派的路由表，把网关指向家里的OpenWrt上，这样下载软件包更快：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo route add default gw 192.168.1.150
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.1 安装&lt;code&gt;samba&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install samba samba-common-bin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 挂载硬盘&lt;/h3&gt;
&lt;p&gt;我的硬盘是希捷12年前的FreeAgent古董盘，在RaspberryOS上是自动挂载到&lt;code&gt;/media/pi/FreeAgent Drive&lt;/code&gt;上的，大概是自动帮我执行了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mount /dev/sda1/ &quot;/media/pi/FreeAgent Drive&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 修改配置文件&lt;/h3&gt;
&lt;p&gt;修改&lt;code&gt;samba&lt;/code&gt;的配置文件，将挂载点发布，并设置读写权限和认证。在&lt;code&gt;/etc/samba/smb.conf&lt;/code&gt;的最后追加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[FreeAgentShare]
path = /media/pi/FreeAgent Drive
writeable=Yes
create mask=0777
directory mask=0777
public=no
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.4 设置访问用户&lt;/h3&gt;
&lt;p&gt;将&lt;code&gt;pi&lt;/code&gt;添加为&lt;code&gt;samba&lt;/code&gt;的认证用户，并设置密码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo smbpasswd -a pi
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.5 重启服务&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl restart smbd
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.6 配置防火墙例外&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow proto udp from 192.168.1.1/24 to any port 137 comment &quot;samba&quot;
sudo ufw allow proto udp from 192.168.1.1/24 to any port 138 comment &quot;samba&quot;
sudo ufw allow proto tcp from 192.168.1.1/24 to any port 139 comment &quot;samba&quot;
sudo ufw allow proto tcp from 192.168.1.1/24 to any port 445 comment &quot;samba&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.7 连接&lt;/h3&gt;
&lt;p&gt;使用macOS连接，打开Finder，&lt;code&gt;⌘+K&lt;/code&gt;打开“连接服务器”，地址填写&lt;code&gt;smb://raspberrypi.local/FreeAgentShare&lt;/code&gt;，输入用户名密码就能连上了。（树莓派的默认主机名就是&lt;code&gt;raspberrypi.local&lt;/code&gt;）&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 4.1 - Async/Await</title><link>https://zlotus.github.io/2021/04/14/writing-an-os-in-rust-4.1/</link><guid isPermaLink="true">https://zlotus.github.io/2021/04/14/writing-an-os-in-rust-4.1/</guid><description>使用Rust编写操作系统 - 4.1 - Async/Await</description><pubDate>Wed, 14 Apr 2021 15:18:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/async-await/&quot;&gt;Async/Await&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;本文将探讨&lt;em&gt;协作式多任务处理&lt;/em&gt;以及Rust的&lt;em&gt;async/await&lt;/em&gt;特性。我们将详细研究async/await在Rust中的工作方式，包括&lt;code&gt;Future&lt;/code&gt;trait的设计，状态机转换和&lt;em&gt;pinning&lt;/em&gt;（译注：内存固定）。然后，我们通过创建异步键盘任务和基本执行器，使得内核具备对async/await的基本支持。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/async-await/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-12&quot;&gt;post-12&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;多任务&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Computer_multitasking&quot;&gt;&lt;em&gt;多任务&lt;/em&gt;&lt;/a&gt;处理是大多数操作系统的基本特征之一，这是一种能够同时执行多个任务的功能。例如，在看这篇文章时，你可能会打开其他程序，例如文本编辑器或终端窗口。即使只打开一个浏览器窗口，也可能会有各种后台任务来管理桌面窗口，检查更新或为文件建立索引。&lt;/p&gt;
&lt;p&gt;尽管所有任务看上去都是并行运行的，但实际在CPU内核上一次只能执行一个任务。为了产生任务可以并行运行的错觉，操作系统会在活动任务之间快速切换，以使得每个任务都可以有一些进展。由于计算机速度很快，因此大多数时候我们不会注意到这些切换。&lt;/p&gt;
&lt;p&gt;单核CPU一次只能执行一个任务，而多核CPU可以真正并行地运行多个任务。例如，具有8个内核的CPU可以同时运行8个任务。我们将在以后的文章中解释如何设置多核CPU。在本文中，为简单起见，我们将重点介绍单核CPU。（值得注意的是，所有多核CPU都仅从单个活动核启动，因此我们目前仍然可以将它们视为单核CPU。）&lt;/p&gt;
&lt;p&gt;多任务处理有两种形式：&lt;em&gt;协作式&lt;/em&gt;多任务要求任务定期放弃对CPU的控制，以便其他任务可以继续执行。&lt;em&gt;抢占式&lt;/em&gt;多任务使用操作系统功能通过强行暂停线程，以在任意时间点切换线程。在下文中，我们将更详细地探讨多任务的两种形式，并讨论它们各自的优缺点。&lt;/p&gt;
&lt;h3&gt;抢占式多任务&lt;/h3&gt;
&lt;p&gt;抢占式多任务处理的思路是使用操作系统控制何时切换任务，思路利用了操作系统在每个中断上可以重获CPU控制权这一机制。这样，只要系统有新输入可用，就可以切换任务。例如，当鼠标移动或网络数据包到达时，就可以执行任务切换。操作系统还可以通过配置硬件计时器在到达特定时间时发送中断，来确定允许任务运行的确切时间。&lt;/p&gt;
&lt;p&gt;下图说明了硬件中断上的任务切换过程：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/regain-control-on-interrupt.svg&quot; alt=&quot;中断时重获控制权&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在第一行中，CPU正在执行程序&lt;code&gt;A&lt;/code&gt;的任务&lt;code&gt;A1&lt;/code&gt;，所有其他任务均被暂停。在第二行中，硬件中断到达CPU。就像&lt;a href=&quot;https://os.phil-opp.com/hardware-interrupts/&quot;&gt;硬件中断&lt;/a&gt;一文中介绍的那样，CPU立即停止执行任务&lt;code&gt;A1&lt;/code&gt;，并跳转到中断描述符表(IDT)中定义的中断处理程序。通过中断处理程序，操作系统现在可以再次控制CPU，从而使它可以切换到任务&lt;code&gt;B1&lt;/code&gt;，而不是继续执行任务&lt;code&gt;A1&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;状态保存&lt;/h4&gt;
&lt;p&gt;鉴于任务会在任意时间点中断，这些任务可能正处于某些计算中。为了能够在以后恢复这些任务，操作系统必须备份任务的整个状态，包括其&lt;a href=&quot;https://en.wikipedia.org/wiki/Call_stack&quot;&gt;调用栈&lt;/a&gt;和所有CPU寄存器的值。此过程称为&lt;a href=&quot;https://en.wikipedia.org/wiki/Context_switch&quot;&gt;上下文切换&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;由于调用栈可能会非常大，操作系统通常会为每个任务设置单独的调用栈，而不是在每次任务切换时备份调用栈的内容。具有单独调用栈的此类任务称为&lt;a href=&quot;https://en.wikipedia.org/wiki/Thread_(computing)&quot;&gt;执行线程&lt;/a&gt;或简称线程。通过为每个任务设置单独的栈，上下文切换时仅需要保存寄存器的内容即可（包括程序计数器和栈指针）。这种方法可以最大程度地减少上下文切换的性能开销，这非常重要，因为上下文切换通常能够到达每秒100次。&lt;/p&gt;
&lt;h4&gt;讨论&lt;/h4&gt;
&lt;p&gt;抢占式多任务处理的主要优点是操作系统可以完全控制任务的执行时间。这样即使不要求任务间的相互协作，也能够确保公平分配各任务的CPU时间。在运行第三方任务时或在多个用户共享系统时，这一点尤其重要。&lt;/p&gt;
&lt;p&gt;抢占式的缺点是每个任务都需要自己的调用栈。与共享调用栈相比，这会导致每个任务的内存使用量增加，并且通常会限制系统中的任务的总量。抢占式的另一个缺点是，即使任务仅使用了一小部分寄存器，操作系统在每个任务切换时仍必须保存全部的CPU寄存器状态。&lt;/p&gt;
&lt;p&gt;抢占式多任务处理和线程是操作系统的基本要素，因为它们能够使操作系统能够运行不受信任的用户空间程序，我们将在以后的文章中详细讨论这些概念。在本文中，我们将专注于协作式多任务处理，这种方式也能够为内核提供有用的多任务功能。&lt;/p&gt;
&lt;h3&gt;协作式多任务&lt;/h3&gt;
&lt;p&gt;协作多任务处理并不是在任意时间点强行暂停正在运行的任务，而是让每个任务运行到自愿放弃对CPU的控制为止。这使任务可以在方便的时间点暂停自己，比如当该任务需要等待I/O操作时。&lt;/p&gt;
&lt;p&gt;合作多任务通常使用在语言层，例如以&lt;a href=&quot;https://en.wikipedia.org/wiki/Coroutine&quot;&gt;协程&lt;/a&gt;或&lt;a href=&quot;https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html&quot;&gt;async/await&lt;/a&gt;的形式。该思路是由程序员或编译器都将&lt;a href=&quot;https://en.wikipedia.org/wiki/Yield_(multithreading)&quot;&gt;&lt;em&gt;yield&lt;/em&gt;&lt;/a&gt;操作插入到程序中，从而使该任务放弃对CPU的控制以允许其他任务运行。在复杂循环的每次迭代之后都可以插入一个yield。&lt;/p&gt;
&lt;p&gt;协作式多任务通常与&lt;a href=&quot;https://en.wikipedia.org/wiki/Asynchronous_I/O&quot;&gt;异步操作&lt;/a&gt;一起使用。与抢占式的一直等待直到操作完成并在期间阻止其他任务的执行，在协作式中若操作尚未完成，则异步操作将返回一个“未就绪”状态。在这种情况下，等待的任务可以执行一个yield操作以允许其他任务运行。&lt;/p&gt;
&lt;h4&gt;状态保存&lt;/h4&gt;
&lt;p&gt;由于任务会为自己定义暂停点，因此它们不需要操作系统来保存其状态。与抢占式相比，协作式可以在暂停之前只保存用以在继续时恢复操作的必要的寄存器状态，这通常可以提高性能。例如，刚完成复杂计算的任务可能只需要备份计算的最终结果，因为此时中间结果已经不再需要了。&lt;/p&gt;
&lt;p&gt;由语言支持实现的协作任务甚至可以在暂停之前只备份调用堆栈的所需部分。例如，Rust的async/await实现会将所有需要的局部变量存储在自动生成的结构体中（见下文）。通过在暂停之前备份调用堆栈的相关部分，所有任务可以共享一个调用堆栈，从而减少了每个任务的内存消耗。这样就可以创建几乎无限的协作任务，而不用担心内存耗尽。&lt;/p&gt;
&lt;h4&gt;讨论&lt;/h4&gt;
&lt;p&gt;协作式多任务处理的缺点是，一个非协作的任务可能会无限期地运行下去。那么，恶意程序或有bug的任务可能会阻止其他任务的运行，并拖慢甚至阻塞整个系统。因此，当且仅当所有任务都支持协作时才能使用协作式多任务处理。而让操作系统依赖于任意用户级程序的协作并不是一个好主意。&lt;/p&gt;
&lt;p&gt;但是，协作多任务的强大性能和内存优势，使其成为在程序&lt;em&gt;内部&lt;/em&gt;使用的好方法，特别是与异步操作结合使用时。由于操作系统内核需要与异步硬件交互，性能至关重要，因此协作多任务似乎是一个实现并发的好方法。&lt;/p&gt;
&lt;h2&gt;Rust的async/await&lt;/h2&gt;
&lt;p&gt;Rust语言以&lt;code&gt;async/await&lt;/code&gt;的形式为协作式多任务提供了原生支持。在探讨什么是async/await及其工作原理之前，我们需要了解Rust中的&lt;em&gt;futures&lt;/em&gt;和异步编程是如何工作的。&lt;/p&gt;
&lt;h3&gt;Futures&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;Future&lt;/em&gt;表示一个可能尚不可用的值。例如，这个值可能是由另一个任务计算的整数，也可能是从网络下载的文件。Future并不需要一直要等待到该值可用，它可以继续执行其他代码直到需要用到该值为止。&lt;/p&gt;
&lt;h4&gt;示例&lt;/h4&gt;
&lt;p&gt;最好用一个小例子说明futures的概念：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/async-example.svg&quot; alt=&quot;async示例&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此序列图展示了一个&lt;code&gt;main&lt;/code&gt;函数，它从文件系统读取文件，然后调用&lt;code&gt;foo&lt;/code&gt;函数。图中以不同的方式执行两次此过程：一次是调用同步函数&lt;code&gt;read_file&lt;/code&gt;，一次是调用异步函数&lt;code&gt;async_read_file&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在同步调用中，&lt;code&gt;main&lt;/code&gt;函数需要一直等待，直到该文件从文件系统中加载完毕。只有这样，它才能继续调用&lt;code&gt;foo&lt;/code&gt;函数，并再次等待其执行结果。&lt;/p&gt;
&lt;p&gt;通过异步&lt;code&gt;async_read_file&lt;/code&gt;调用，文件系统直接返回一个future并在后台异步加载文件。这使&lt;code&gt;main&lt;/code&gt;函数可以更早地调用&lt;code&gt;foo&lt;/code&gt;函数，然后&lt;code&gt;foo&lt;/code&gt;函数与文件加载并行运行。在此示例中，文件加载甚至在&lt;code&gt;foo&lt;/code&gt;返回之前就完成了，因此&lt;code&gt;main&lt;/code&gt;无需继续等待&lt;code&gt;foo&lt;/code&gt;返回，可以直接处理文件。&lt;/p&gt;
&lt;h4&gt;Rust中的Futures&lt;/h4&gt;
&lt;p&gt;在Rust中，futures由&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html&quot;&gt;&lt;code&gt;Future&lt;/code&gt;&lt;/a&gt;trait表示，其定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub trait Future {
    type Output;
    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Self::Output&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关联类型&lt;code&gt;Output&lt;/code&gt;指定异步返回值的类型。例如，上图中的&lt;code&gt;async_read_file&lt;/code&gt;函数将返回一个&lt;code&gt;Output&lt;/code&gt;设置为&lt;code&gt;File&lt;/code&gt;的&lt;code&gt;Future&lt;/code&gt;实例。&lt;/p&gt;
&lt;p&gt;调用&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll&quot;&gt;&lt;code&gt;poll&lt;/code&gt;&lt;/a&gt;方法可以检查该值是否已经可用。它将返回一个枚举&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll&quot;&gt;&lt;code&gt;Poll&lt;/code&gt;&lt;/a&gt;，如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub enum Poll&amp;lt;T&amp;gt; {
    Ready(T),
    Pending,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果该值已经可用（例如，已从磁盘中读取完整的文件），则将其封装在&lt;code&gt;Ready&lt;/code&gt;变量中返回。否则，将返回&lt;code&gt;Pending&lt;/code&gt;变量，以通知调用方该值尚不可用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;poll&lt;/code&gt;方法采用两个参数：&lt;code&gt;self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;&lt;/code&gt;和&lt;code&gt;cx: &amp;amp;mut Context&lt;/code&gt;上下文。前者的行为类似于普通的&lt;code&gt;&amp;amp;mut self&lt;/code&gt;引用，不同之处在于，&lt;code&gt;Self&lt;/code&gt;值&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/pin/index.html&quot;&gt;&lt;em&gt;pinned&lt;/em&gt;&lt;/a&gt;在一个固定的内存位置上。如果不先了解async/await的工作原理，就很难理解&lt;code&gt;Pin&lt;/code&gt;以及为什么需要&lt;code&gt;Pin&lt;/code&gt;。因此，我们将在后文中进行详细解释。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cx: &amp;amp;mut Context&lt;/code&gt;参数用于将一个&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/task/struct.Waker.html&quot;&gt;&lt;code&gt;Waker&lt;/code&gt;&lt;/a&gt;实例传递给异步任务，例如加载文件系统。这个&lt;code&gt;Waker&lt;/code&gt;允许异步任务发信号通知任务（或部分任务）已完成，例如该文件已从磁盘加载。由于主任务知道将在&lt;code&gt;Future&lt;/code&gt;就绪时将会收到通知，因此就不需要一遍又一遍地调用&lt;code&gt;poll&lt;/code&gt;了。我们会在后文中实现自己的waker类型，届时将更加详细地说明此过程。&lt;/p&gt;
&lt;h3&gt;使用Futures&lt;/h3&gt;
&lt;p&gt;现在，我们知道了如何定义future，了解了&lt;code&gt;poll&lt;/code&gt;方法的基本思路。但是，我们仍然不知道如何有效地使用futures。由于futures代表异步任务的结果，可能尚不可用。但实际上，我们经常立即需要这些值以进行进一步的计算。所以问题是：在需要时如何有效地检索一个future的值？&lt;/p&gt;
&lt;h4&gt;等待Futures&lt;/h4&gt;
&lt;p&gt;一种可能的答案是一直等待到future就绪为止。该过程看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let future = async_read_file(&quot;foo.txt&quot;);
let file_content = loop {
    match future.poll(…) {
        Poll::Ready(value) =&amp;gt; break value,
        Poll::Pending =&amp;gt; {}, // 什么也不做
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中，我们通过循环中&lt;em&gt;积极地&lt;/em&gt;调用&lt;code&gt;poll&lt;/code&gt;来等待future完成。在这里&lt;code&gt;poll&lt;/code&gt;的参数并不重要，因此已被省略。尽管此方法可行，但是效率很低，因为包含poll的循环会一直用占用CPU直到该值可用为止。&lt;/p&gt;
&lt;p&gt;一种较为高效的方法是&lt;em&gt;阻塞&lt;/em&gt;当前线程，直到future可用为止。当然，这只有在有线程支持的情况下才可行，因此并不适用于我们的内核，至少目前还不行。即使在支持阻塞的系统上，通常也不会这么做，因为阻塞会将异步任务再次变为同步任务，从而失去了并行任务的性能优势。&lt;/p&gt;
&lt;h4&gt;Future组合器&lt;/h4&gt;
&lt;p&gt;另一种等待方式是使用future组合器。Future组合器是类似于&lt;code&gt;map&lt;/code&gt;的方法，它允许将futures链接并组合在一起，就像在&lt;a href=&quot;https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html&quot;&gt;&lt;code&gt;Iterator&lt;/code&gt;&lt;/a&gt;上做的那样。这些组合器不等待future，而是自己返回future，即为&lt;code&gt;poll&lt;/code&gt;应用了map操作。&lt;/p&gt;
&lt;p&gt;下面举个简单的例子，一个用于将&lt;code&gt;Future&amp;lt;Output = String&amp;gt;&lt;/code&gt;转换为&lt;code&gt;Future&amp;lt;Output = usize&amp;gt;&lt;/code&gt;的&lt;code&gt;string_len&lt;/code&gt;组合器可能看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct StringLen&amp;lt;F&amp;gt; {
    inner_future: F,
}

impl&amp;lt;F&amp;gt; Future for StringLen&amp;lt;F&amp;gt; where F: Future&amp;lt;Output = String&amp;gt; {
    type Output = usize;

    fn poll(mut self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context&amp;lt;&apos;_&amp;gt;) -&amp;gt; Poll&amp;lt;T&amp;gt; {
        match self.inner_future.poll(cx) {
            Poll::Ready(s) =&amp;gt; Poll::Ready(s.len()),
            Poll::Pending =&amp;gt; Poll::Pending,
        }
    }
}

fn string_len(string: impl Future&amp;lt;Output = String&amp;gt;)
    -&amp;gt; impl Future&amp;lt;Output = usize&amp;gt;
{
    StringLen {
        inner_future: string,
    }
}

// Usage
fn file_len() -&amp;gt; impl Future&amp;lt;Output = usize&amp;gt; {
    let file_content_future = async_read_file(&quot;foo.txt&quot;);
    string_len(file_content_future)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该代码无法正常工作，因为尚未处理&lt;a href=&quot;https://doc.rust-lang.org/stable/core/pin/index.html&quot;&gt;&lt;em&gt;pinning&lt;/em&gt;&lt;/a&gt;，不过作为例子已经足够了。基本思路是&lt;code&gt;string_len&lt;/code&gt;函数将给定的实现了&lt;code&gt;Future&lt;/code&gt;trait的实例封装到新的&lt;code&gt;StringLen&lt;/code&gt;结构体中，而该结构体也实现了&lt;code&gt;Future&lt;/code&gt;trait。当poll封装的future时，即是poll其内部的future。如果该值尚未就绪，封装的future也将返回&lt;code&gt;Poll::Pending&lt;/code&gt;。如果该值已就绪，则从&lt;code&gt;Poll::Ready&lt;/code&gt;变量中获取字符串，并计算其长度。最后再将其封装在&lt;code&gt;Poll::Ready&lt;/code&gt;中返回。&lt;/p&gt;
&lt;p&gt;通过&lt;code&gt;string_len&lt;/code&gt;函数，我们不需要等待一个异步字符串，就可以计算其长度。由于该函数也返回&lt;code&gt;Future&lt;/code&gt;，因此调用者无法直接操作返回的值，而需要再次使用组合器函数。这样，整个调用过程就变为异步的了，我们可以高效地在某个时刻一次等待多个future，例如 在&lt;code&gt;main&lt;/code&gt;函数上。&lt;/p&gt;
&lt;p&gt;手动编写组合器函数比较困难，所以它们通常由库直接提供。尽管Rust标准库本身还没有提供官方组合器方法，但半官方（兼容&lt;code&gt;no_std&lt;/code&gt;）的&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/&quot;&gt;&lt;code&gt;future&lt;/code&gt;&lt;/a&gt;crate可以。其&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html&quot;&gt;&lt;code&gt;FutureExt&lt;/code&gt;&lt;/a&gt;特性提供了诸如&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.map&quot;&gt;&lt;code&gt;map&lt;/code&gt;&lt;/a&gt;或&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.then&quot;&gt;&lt;code&gt;then&lt;/code&gt;&lt;/a&gt;之类的高级组合器方法，可用于任意闭合操作结果。&lt;/p&gt;
&lt;h5&gt;优点&lt;/h5&gt;
&lt;p&gt;Future组合器最大的优点是能够使操作保持异步。这种方法和异步&lt;code&gt;I/O&lt;/code&gt;接口结合使用时性能非常高。实际上future组合器将被实现为具有trait的普通结构体，以使编译器能够对其做出进一步优化。有关更多详细信息，请参阅&lt;a href=&quot;https://aturon.github.io/blog/2016/08/11/futures/&quot;&gt;Rust的零成本future&lt;/a&gt;一文，就是这篇文章宣布了在Rust生态系统中添加future。&lt;/p&gt;
&lt;h5&gt;缺点&lt;/h5&gt;
&lt;p&gt;尽管future组合器可以编写出非常高效的代码，但由于类型系统和基于闭包的接口的限制，组合器可能会在某些情况下变得难以使用。例如，考虑下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn example(min_len: usize) -&amp;gt; impl Future&amp;lt;Output = String&amp;gt; {
    async_read_file(&quot;foo.txt&quot;).then(move |content| {
        if content.len() &amp;lt; min_len {
            Either::Left(async_read_file(&quot;bar.txt&quot;).map(|s| content + &amp;amp;s))
        } else {
            Either::Right(future::ready(content))
        }
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=91fc09024eecb2448a85a7ef6a97b8d8&quot;&gt;使用play rust在线运行上面的代码&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;代码先读取文件&lt;code&gt;foo.txt&lt;/code&gt;，然后使用&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.then&quot;&gt;&lt;code&gt;then&lt;/code&gt;&lt;/a&gt;组合器根据文件内容链接第二个future。如果内容长度小于给定的&lt;code&gt;min_len&lt;/code&gt;，我们将读取另一个&lt;code&gt;bar.txt&lt;/code&gt;文件，然后使用&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/future/trait.FutureExt.html#method.map&quot;&gt;&lt;code&gt;map&lt;/code&gt;&lt;/a&gt;组合器将其附加到&lt;code&gt;content&lt;/code&gt;中。否则，只返回&lt;code&gt;foo.txt&lt;/code&gt;的&lt;code&gt;content&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们需要在传递给&lt;code&gt;then&lt;/code&gt;的闭包上使用&lt;a href=&quot;https://doc.rust-lang.org/std/keyword.move.html&quot;&gt;&lt;code&gt;move&lt;/code&gt;关键字&lt;/a&gt;，否则&lt;code&gt;min_len&lt;/code&gt;变量会产生生命周期错误。使用&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/future/enum.Either.html&quot;&gt;&lt;code&gt;Either&lt;/code&gt;&lt;/a&gt;封装的原因是让&lt;code&gt;if&lt;/code&gt;块和&lt;code&gt;else&lt;/code&gt;块始终具有相同的类型。由于我们在块中返回了不同的future类型，因此必须使用封装类型将它们统一为一个类型。&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/future/fn.ready.html&quot;&gt;&lt;code&gt;ready&lt;/code&gt;&lt;/a&gt;函数将立刻就绪的值封装到future中。这里需要使用该函数是因为&lt;code&gt;Either&lt;/code&gt;封装要求值实现&lt;code&gt;Future&lt;/code&gt;trait。&lt;/p&gt;
&lt;p&gt;您可以想象，这种用法很快就会导致大型项目的代码变得非常复杂。尤其是再涉及借用和生命周期，就会变得更加复杂。因此，为了使异步代码从根本上更易于编写，我们投入了大量工作来为Rust添加对async/await的支持。&lt;/p&gt;
&lt;h3&gt;Async/Await模式&lt;/h3&gt;
&lt;p&gt;Async/Await的思路是让程序员以编写&lt;em&gt;看起来像&lt;/em&gt;同步代码的方式编写异步代码，只不最后是由编译器将同步代码转换为异步代码。它基于两个关键字&lt;code&gt;async&lt;/code&gt;和&lt;code&gt;await&lt;/code&gt;。在函数签名中使用&lt;code&gt;async&lt;/code&gt;关键字，就可以将同步函数转换为一个返回future的异步函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn foo() -&amp;gt; u32 {
    0
}

// 上面的函数大概会变编译器转换为：
fn foo() -&amp;gt; impl Future&amp;lt;Output = u32&amp;gt; {
    future::ready(0)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅使用此关键字并没有那么有用。但是，在异步函数内部，可以使用&lt;code&gt;await&lt;/code&gt;关键字来取回future的异步值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn example(min_len: usize) -&amp;gt; String {
    let content = async_read_file(&quot;foo.txt&quot;).await;
    if content.len() &amp;lt; min_len {
        content + &amp;amp;async_read_file(&quot;bar.txt&quot;).await
    } else {
        content
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=d93c28509a1c67661f31ff820281d434&quot;&gt;使用play rust在线运行上面的代码&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;将&lt;a href=&quot;./#%E7%BC%BA%E7%82%B9&quot;&gt;上面&lt;/a&gt;使用组合器实现的&lt;code&gt;example&lt;/code&gt;函数直接转换为async/await模式：使用&lt;code&gt;.await&lt;/code&gt;运算符就可以取回future的值，无需使用闭包或&lt;code&gt;Either&lt;/code&gt;类型。如此，我们就可以像编写普通的同步代码一样编写&lt;em&gt;异步代码&lt;/em&gt;。&lt;/p&gt;
&lt;h4&gt;状态机转换&lt;/h4&gt;
&lt;p&gt;在这种场景中，编译器的作用就是将&lt;code&gt;async&lt;/code&gt;函数体转换为一个&lt;a href=&quot;https://en.wikipedia.org/wiki/Finite-state_machine&quot;&gt;状态机&lt;/a&gt;，每个&lt;code&gt;.await&lt;/code&gt;调用代表一个不同的状态。对于上面的&lt;code&gt;example&lt;/code&gt;函数，编译器创建具有以下四个状态的状态机：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/async-state-machine-states.svg&quot; alt=&quot;async状态机的状态&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不同状态代表该函数的不同暂停点。&quot;&lt;em&gt;Start&lt;/em&gt;&quot;和&quot;&lt;em&gt;End&lt;/em&gt;&quot;状态代表函数在其执行的开始和结束时的状态。&quot;&lt;em&gt;Waiting on foo.txt&lt;/em&gt;&quot;状态表示该函数目前正在等待第一个&lt;code&gt;async_read_file&lt;/code&gt;的结果。同样的，&quot;&lt;em&gt;Waiting on bar.txt&lt;/em&gt;&quot;状态表示函数在等待第二个&lt;code&gt;async_read_file&lt;/code&gt;的结果的暂停点。&lt;/p&gt;
&lt;p&gt;状态机通过将每个&lt;code&gt;poll&lt;/code&gt;调用都变为一个可能的状态转换来实现&lt;code&gt;Future&lt;/code&gt;trait：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/async-state-machine-basic.svg&quot; alt=&quot;async状态机本质&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中使用箭头表示状态开关，并使用菱形表示条件路径。例如，如果&lt;code&gt;foo.txt&lt;/code&gt;文件尚未准备好，则采用标记为&quot;&lt;em&gt;no&lt;/em&gt;&quot;的路径，并达到&quot;&lt;em&gt;Waiting on foo.txt&lt;/em&gt;&quot;的状态。否则，就采用标记为“&lt;em&gt;yes&lt;/em&gt;”路径。没有字的红色小菱形代表&lt;code&gt;example&lt;/code&gt;函数中&lt;code&gt;if content.len() &amp;lt; 100&lt;/code&gt;的条件分支。&lt;/p&gt;
&lt;p&gt;我们看到第一个&lt;code&gt;poll&lt;/code&gt;调用启动了该函数并使它运行，直到遇到一个尚未就绪的future。如果路径上的所有future都已就绪，则该函数可以一直运行到&quot;&lt;em&gt;End&lt;/em&gt;&quot;状态，并返回封装在&lt;code&gt;Poll::Ready&lt;/code&gt;中的结果。否则，状态机将进入等待状态并返回&lt;code&gt;Poll::Pending&lt;/code&gt;。然后在下一个&lt;code&gt;poll&lt;/code&gt;调用中，状态机从上一个等待状态开始重试其最后一次操作。&lt;/p&gt;
&lt;h4&gt;状态保存&lt;/h4&gt;
&lt;p&gt;为了能够从上一个等待状态中恢复，状态机必须在内部跟踪当前状态。此外，它还必须保存在下一个&lt;code&gt;poll&lt;/code&gt;调用中恢复执行所需的变量。这就是编译器真正发挥作用的地方：由于编译器知道在何时要使用哪些变量，因此它可以自动生成具有所需变量的结构体。&lt;/p&gt;
&lt;p&gt;作为示例，编译器为上面的&lt;code&gt;example&lt;/code&gt;函数生成类似于下面这样的结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 这是async版的`example`函数
async fn example(min_len: usize) -&amp;gt; String {
    let content = async_read_file(&quot;foo.txt&quot;).await;
    if content.len() &amp;lt; min_len {
        content + &amp;amp;async_read_file(&quot;bar.txt&quot;).await
    } else {
        content
    }
}

// 这是编译器生成的状态结构体

struct StartState {
    min_len: usize,
}

struct WaitingOnFooTxtState {
    min_len: usize,
    foo_txt_future: impl Future&amp;lt;Output = String&amp;gt;,
}

struct WaitingOnBarTxtState {
    content: String,
    bar_txt_future: impl Future&amp;lt;Output = String&amp;gt;,
}

struct EndState {}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&quot;&lt;em&gt;Start&lt;/em&gt;&quot;和&quot;&lt;em&gt;Waiting on foo.txt&lt;/em&gt;&quot;状态下，需要存储&lt;code&gt;min_len&lt;/code&gt;参数，因为稍后与&lt;code&gt;content.len()&lt;/code&gt;做比较时需要使用该参数。&quot;&lt;em&gt;Waiting on foo.txt&lt;/em&gt;&quot;状态还存储了一个&lt;code&gt;foo_txt_future&lt;/code&gt;，用来表示&lt;code&gt;async_read_file&lt;/code&gt;调用返回的future。状态机继续运行时会再次poll该future，因此需要将其保存。&lt;/p&gt;
&lt;p&gt;&quot;&lt;em&gt;Waiting on bar.txt&lt;/em&gt;&quot;状态包含&lt;code&gt;content&lt;/code&gt;变量，是因为在&lt;code&gt;bar.txt&lt;/code&gt;就绪后需要使用该变量进行字符串连接。该状态还存储了一个&lt;code&gt;bar_txt_future&lt;/code&gt;，用来表示正在加载中的&lt;code&gt;bar.txt&lt;/code&gt;。该结构体不包含&lt;code&gt;min_len&lt;/code&gt;变量，因为在&lt;code&gt;content.len()&lt;/code&gt;比较之后就不再需要该变量了。在&quot;&lt;em&gt;End&lt;/em&gt;&quot;状态下，没有存储任何变量，因为此时函数已经运行完毕。&lt;/p&gt;
&lt;p&gt;请记住，这只是编译器可能生成的代码的一个示例。结构体名称和字段布局是实现细节，可能会有所不同。&lt;/p&gt;
&lt;h4&gt;全状态机类型&lt;/h4&gt;
&lt;p&gt;尽管编译器生成的确切代码是实现细节，但这个示例还是有助于我们理解并想象&lt;code&gt;example&lt;/code&gt;函数生成的状态机可能的样子。我们已经定义了代表不同状态的结构体，并给出了其中包含的所需变量。为了基于这些结构体创建一个状态机，我们可以将它们组合成一个枚举：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum ExampleStateMachine {
    Start(StartState),
    WaitingOnFooTxt(WaitingOnFooTxtState),
    WaitingOnBarTxt(WaitingOnBarTxtState),
    End(EndState),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们为每个状态定义一个单独的枚举变量，并将对应状态的结构体作为字段添加到每个变量。为了实现状态转换，编译器根据&lt;code&gt;example&lt;/code&gt;函数实现&lt;code&gt;Future&lt;/code&gt;trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Future for ExampleStateMachine {
    type Output = String; // `example`函数的返回类型

    fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Self::Output&amp;gt; {
        loop {
            match self { // TODO: 处理pinning
                ExampleStateMachine::Start(state) =&amp;gt; {…}
                ExampleStateMachine::WaitingOnFooTxt(state) =&amp;gt; {…}
                ExampleStateMachine::WaitingOnBarTxt(state) =&amp;gt; {…}
                ExampleStateMachine::End(state) =&amp;gt; {…}
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该&lt;code&gt;Future&lt;/code&gt;的&lt;code&gt;Output&lt;/code&gt;类型为&lt;code&gt;String&lt;/code&gt;，即&lt;code&gt;example&lt;/code&gt;函数的返回类型。为了实现&lt;code&gt;poll&lt;/code&gt;函数，我们在&lt;code&gt;loop&lt;/code&gt;内的当前状态上使用&lt;code&gt;match&lt;/code&gt;语句。思路是我们尽可能长时间地切换到下一个状态，并在无法继续时显式的使用&lt;code&gt;return Poll::Pending&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为简单起见，这里仅给出简化的代码，且暂不处理&lt;a href=&quot;https://doc.rust-lang.org/stable/core/pin/index.html&quot;&gt;pinning&lt;/a&gt;、所有权、生命周期等内容。因此，这里的代码和下面的代码应被看做伪代码，不能直接使用。当然，真正的编译器生成的代码可以正确处理所有内容，尽管可能使用了与我们不同的方式。&lt;/p&gt;
&lt;p&gt;为了使示意的代码更简洁，我们将分别显示每个匹配分支的代码。从&quot;Start&quot;状态开始：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExampleStateMachine::Start(state) =&amp;gt; {
    // from body of `example`
    let foo_txt_future = async_read_file(&quot;foo.txt&quot;);
    // `.await` operation
    let state = WaitingOnFooTxtState {
        min_len: state.min_len,
        foo_txt_future,
    };
    *self = ExampleStateMachine::WaitingOnFooTxt(state);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当状态机处于&lt;code&gt;Start&lt;/code&gt;状态时，其对应位置正是函数体的最开始。在这种情况下，我们将执行&lt;code&gt;example&lt;/code&gt;函数体中的所有代码，直到遇到第一个&lt;code&gt;.await&lt;/code&gt;。为了处理&lt;code&gt;.await&lt;/code&gt;操作，我们将&lt;code&gt;self&lt;/code&gt;状态机的状态修改为&lt;code&gt;WaitingOnFooTxt&lt;/code&gt;，并令状态中包含&lt;code&gt;WaitingOnFooTxtState&lt;/code&gt;结构体。&lt;/p&gt;
&lt;p&gt;由于&lt;code&gt;match self {…}&lt;/code&gt;语句是在循环中执行的，因此该执行将跳至下一个分支&lt;code&gt;WaitingOnFooTxt&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExampleStateMachine::WaitingOnFooTxt(state) =&amp;gt; {
    match state.foo_txt_future.poll(cx) {
        Poll::Pending =&amp;gt; return Poll::Pending,
        Poll::Ready(content) =&amp;gt; {
            // from body of `example`
            if content.len() &amp;lt; state.min_len {
                let bar_txt_future = async_read_file(&quot;bar.txt&quot;);
                // `.await` operation
                let state = WaitingOnBarTxtState {
                    content,
                    bar_txt_future,
                };
                *self = ExampleStateMachine::WaitingOnBarTxt(state);
            } else {
                *self = ExampleStateMachine::End(EndState));
                return Poll::Ready(content);
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这一匹配分支中，我们首先调用&lt;code&gt;foo_txt_future&lt;/code&gt;的&lt;code&gt;poll&lt;/code&gt;函数。如果尚未就绪，则退出循环并返回&lt;code&gt;Poll::Pending&lt;/code&gt;。由于在这种情况下&lt;code&gt;self&lt;/code&gt;仍位于&lt;code&gt;WaitingOnFooTxt&lt;/code&gt;状态，因此状态机的下一次&lt;code&gt;poll&lt;/code&gt;调用也将进入相同的匹配分支并重试&lt;code&gt;foo_txt_future&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;foo_txt_future&lt;/code&gt;就绪时，我们将结果赋给&lt;code&gt;content&lt;/code&gt;变量，然后继续执行&lt;code&gt;example&lt;/code&gt;函数的代码：如果&lt;code&gt;content.len()&lt;/code&gt;小于状态结构体中保存的&lt;code&gt;min_len&lt;/code&gt;，则异步读取&lt;code&gt;bar.txt&lt;/code&gt;文件。我们再次将&lt;code&gt;.await&lt;/code&gt;操作转换为状态更改，而这次应转换为&lt;code&gt;WaitingOnBarTxt&lt;/code&gt;状态。由于我们是在循环内执行匹配，因此下一轮循环将直接跳转到新状态的匹配分支，然后在该状态下poll&lt;code&gt;bar_txt_future&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果我们进入&lt;code&gt;else&lt;/code&gt;分支，则不会进行进一步的&lt;code&gt;.await&lt;/code&gt;操作。此时已到达函数的结尾，并将&lt;code&gt;content&lt;/code&gt;封装在&lt;code&gt;Poll::Ready&lt;/code&gt;中返回。我们还需要将当前状态更改为&lt;code&gt;End&lt;/code&gt;状态。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;WaitingOnBarTxt&lt;/code&gt;状态的代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExampleStateMachine::WaitingOnBarTxt(state) =&amp;gt; {
    match state.bar_txt_future.poll(cx) {
        Poll::Pending =&amp;gt; return Poll::Pending,
        Poll::Ready(bar_txt) =&amp;gt; {
            *self = ExampleStateMachine::End(EndState));
            // from body of `example`
            return Poll::Ready(state.content + &amp;amp;bar_txt);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与&lt;code&gt;WaitingOnFooTxt&lt;/code&gt;状态类似，我们从poll&lt;code&gt;bar_txt_future&lt;/code&gt;开始。如果仍未就绪，则退出循环并返回&lt;code&gt;Poll::Pending&lt;/code&gt;。否则，我们就执行&lt;code&gt;example&lt;/code&gt;函数的最后一个操作：用&lt;code&gt;content&lt;/code&gt;变量与future的结果做字符串连接。我们将状态机更新为&lt;code&gt;End&lt;/code&gt;状态，然后将结果封装在&lt;code&gt;Poll::Ready&lt;/code&gt;中返回。&lt;/p&gt;
&lt;p&gt;最后，&lt;code&gt;End&lt;/code&gt;状态的代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ExampleStateMachine::End(_) =&amp;gt; {
    panic!(&quot;poll called after Poll::Ready was returned&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Future返回&lt;code&gt;Poll::Ready&lt;/code&gt;后就不应再被poll了，因此，当我们已经处于&lt;code&gt;End&lt;/code&gt;状态时，如果再次调用&lt;code&gt;poll&lt;/code&gt;，就产生一个panic。&lt;/p&gt;
&lt;p&gt;现在我们知道了编译器&lt;em&gt;可能&lt;/em&gt;会生成怎样的状态机，以及怎样去给状态机实现&lt;code&gt;Future&lt;/code&gt;trait。但实际上，编译器会以不同的方式生成代码。（如果您感兴趣的话，该实现目前基于&lt;a href=&quot;https://doc.rust-lang.org/nightly/unstable-book/language-features/generators.html&quot;&gt;&lt;em&gt;生成器&lt;/em&gt;&lt;/a&gt;，不过这只是实现细节。）&lt;/p&gt;
&lt;p&gt;最后一步是为&lt;code&gt;example&lt;/code&gt;函数本身生成代码。记住，函数签名是这样定义的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn example(min_len: usize) -&amp;gt; String
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于现在整个函数体是由状态机实现的，因此该函数唯一需要做的就是初始化状态机并将其返回。为此生成的代码如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn example(min_len: usize) -&amp;gt; ExampleStateMachine {
    ExampleStateMachine::Start(StartState {
        min_len,
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数不再使用&lt;code&gt;async&lt;/code&gt;修饰符，因为它现在显式返回一个实现了&lt;code&gt;Future&lt;/code&gt;trait的&lt;code&gt;ExampleStateMachine&lt;/code&gt;类型。如预期的那样，状态机被初始化为&lt;code&gt;Start&lt;/code&gt;状态，并且使用&lt;code&gt;min_len&lt;/code&gt;参数初始化了对应的状态结构体。&lt;/p&gt;
&lt;p&gt;请注意，此函数并不会直接启动状态机。这是Rust中future的一个基本设计决策：在第一次被poll之前什么也不做。&lt;/p&gt;
&lt;h3&gt;Pinning&lt;/h3&gt;
&lt;p&gt;在这篇文章中，我们已经遇到&lt;em&gt;pinning&lt;/em&gt;很多次了。现在终于是时候看看究竟什么是pinning以及为什么需要pinning了。&lt;/p&gt;
&lt;h4&gt;自引用结构体&lt;/h4&gt;
&lt;p&gt;如上所述，状态机转换将每个暂停点的局部变量存储在结构体中。对于像&lt;code&gt;example&lt;/code&gt;函数这样的小例子就很简单，并不会导致任何问题。但是，当变量相互引用时，事情就会变得困难。例如，考虑以下函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn pin_example() -&amp;gt; i32 {
    let array = [1, 2, 3];
    let element = &amp;amp;array[2];
    async_write_file(&quot;foo.txt&quot;, element.to_string()).await;
    *element
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数创建一个包含&lt;code&gt;1&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt;、&lt;code&gt;3&lt;/code&gt;的小数组。然后再创建对最后一个数组元素的引用，存储在&lt;code&gt;element&lt;/code&gt;变量中。接下来，该函数将转换为字符串的数字异步写入到&lt;code&gt;foo.txt&lt;/code&gt;文件中。最后返回由&lt;code&gt;element&lt;/code&gt;引用的数字。&lt;/p&gt;
&lt;p&gt;由于该函数使用了一个&lt;code&gt;await&lt;/code&gt;操作，因此结果状态机具有三种状态：&quot;Start&quot;、&quot;End&quot;、&quot;Waiting on write&quot;。该函数没有参数，因此开始状态对应的结构体为空。像以前一样，结束状态对应的结构体也为空，因为该函数此时已完成。而&quot;Waiting on write&quot;状态对应的结构体就很有趣：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct WaitingOnWriteState {
    array: [1, 2, 3],
    element: 0x1001c, // 最后一个数组元素的地址
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们需要存储&lt;code&gt;array&lt;/code&gt;和&lt;code&gt;element&lt;/code&gt;变量，因为返回值需要&lt;code&gt;element&lt;/code&gt;，而&lt;code&gt;element&lt;/code&gt;又引用了&lt;code&gt;array&lt;/code&gt;。由于&lt;code&gt;element&lt;/code&gt;是一个引用，因此它存储指向所引用元素的&lt;em&gt;指针&lt;/em&gt;（即内存地址）。我们在这里使用&lt;code&gt;0x1001c&lt;/code&gt;作为示例存储地址。而实际上，&lt;code&gt;element&lt;/code&gt;字段必须是&lt;code&gt;array&lt;/code&gt;字段最后一个元素的地址，所以这与该结构体在内存中的位置有关。由于此类结构体对自身的某些字段做了引用，因此这种具有内部指针的结构体也叫做&lt;em&gt;自引用&lt;/em&gt;结构体。&lt;/p&gt;
&lt;h4&gt;自引用结构体的问题&lt;/h4&gt;
&lt;p&gt;自引用结构的内部指针将导致一个很基本的问题，尤其是在查看其内存布局时会更加明显：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/self-referential-struct.svg&quot; alt=&quot;自引用结构体&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;array&lt;/code&gt;字段从地址&lt;code&gt;0x10014&lt;/code&gt;开始，&lt;code&gt;element&lt;/code&gt;字段从地址&lt;code&gt;0x10020&lt;/code&gt;开始。它指向地址&lt;code&gt;0x1001c&lt;/code&gt;，即最后一个数组元素的地址。至此仍没什么问题。但是，当我们将此结构体移动到其他内存地址时，就会发生问题：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/self-referential-struct-moved.svg&quot; alt=&quot;自引用结构体移动后&quot; /&gt;&lt;/p&gt;
&lt;p&gt;稍微移动该结构体，现在使其从地址&lt;code&gt;0x10024&lt;/code&gt;开始。这是有可能的，例如当我们将该结构体作为函数参数传递时，或是将其分配给其他栈变量时。问题在于，尽管最后一个数组元素现已位于地址&lt;code&gt;0x1002c&lt;/code&gt;，但&lt;code&gt;element&lt;/code&gt;字段仍指向地址&lt;code&gt;0x1001c&lt;/code&gt;。于是指针悬空，结果就是在下一个&lt;code&gt;poll&lt;/code&gt;调用中发生未定义的行为。&lt;/p&gt;
&lt;h4&gt;可行的解决方案&lt;/h4&gt;
&lt;p&gt;有三种解决指针悬空问题的基本方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在移动时更新指针&lt;/strong&gt;：思路是每当结构体在内存中移动时都更新其内部指针，以使该指针在移动后仍然有效。不幸的是，这种方法需要对Rust进行大量更改，并可能导致巨大的性能损失。因为如果实现这种方法，就需要某种运行时持续跟踪结构体中各种类型的字段，并在每次移动发生时检查是否需要更新指针。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储偏移量而不是自引用&lt;/strong&gt;：为了不去更新指针，编译器可以尝试将自引用存储为从结构体开始地址算起的偏移量。例如，上面的&lt;code&gt;WaitingOnWriteState&lt;/code&gt;结构的&lt;code&gt;element&lt;/code&gt;字段可以以&lt;code&gt;element_offset&lt;/code&gt;字段的形式存储，其值为8，因为该引用指向的数组元素位于该结构体的起始地址后的第8个字节处。由于在移动结构时偏移量保持不变，因此不需要字段更新。
问题是，如果实现这种方法，就必须让编译器检测所有自引用。这在编译时是不可能的，因为引用的值可能取决于用户输入，因此这又需要使用运行时系统来分析引用，从而正确地创建状态结构体。这不仅会导致运行时成本增加，而且还会阻止某些编译器优化，从而又会导致较大的性能损失。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;禁止移动结构体&lt;/strong&gt;：如上所示，仅当我们在内存中移动该结构体时，才会出现悬空指针。那么，通过完全禁止对自引用结构的移动操作，就可以避免该问题。这种方法的最大优点是可以实现在类型系统级别，并不会增加运行时成本。不过它的缺点是需要程序员自己处理在可能的自引用结构体上发生的移动操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了遵守提供零成本抽象的原则（这意味着抽象不应产生额外的运行时成本），Rust选择了第三种解决方案。为此，在&lt;a href=&quot;https://github.com/rust-lang/rfcs/blob/master/text/2349-pin.md&quot;&gt;RFC 2349&lt;/a&gt;中提出了&lt;a href=&quot;https://doc.rust-lang.org/stable/core/pin/index.html&quot;&gt;&lt;em&gt;pinning&lt;/em&gt;&lt;/a&gt; API。在下文中，我们将简要概述此API，并说明它将如何与async/await和futures一起使用。&lt;/p&gt;
&lt;h4&gt;堆上的值&lt;/h4&gt;
&lt;p&gt;首先，很明显，&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/&quot;&gt;堆分配&lt;/a&gt;的值在大多数情况下已经具有固定的内存地址。这些值是由&lt;code&gt;allocate&lt;/code&gt;调用创建，并由如类似&lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;的指针类型进行引用的。尽管这种指针类型可以移动，但指针所指向的堆值将始终位于相同的内存地址中，除非调用&lt;code&gt;deallocate&lt;/code&gt;将其释放否则地址将一直不变。&lt;/p&gt;
&lt;p&gt;使用堆分配，我们可以尝试创建一个自引用结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main() {
    let mut heap_value = Box::new(SelfReferential {
        self_ptr: 0 as *const _,
    });
    let ptr = &amp;amp;*heap_value as *const SelfReferential;
    heap_value.self_ptr = ptr;
    println!(&quot;heap value at: {:p}&quot;, heap_value);
    println!(&quot;internal reference: {:p}&quot;, heap_value.self_ptr);
}

struct SelfReferential {
    self_ptr: *const Self,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=ce1aff3a37fcc1c8188eeaf0f39c97e8&quot;&gt;使用play rust在线运行上面的代码&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;我们创建一个名为&lt;code&gt;SelfReferential&lt;/code&gt;的简单结构体，其中包含一个指针字段。首先，我们使用空指针初始化结构体，然后使用&lt;code&gt;Box::new&lt;/code&gt;将其分配到堆上。接下来，我们确定分配给堆的结构体的内存地址，并将其存储在&lt;code&gt;ptr&lt;/code&gt;变量中。最后，通过将&lt;code&gt;ptr&lt;/code&gt;变量分配给&lt;code&gt;self_ptr&lt;/code&gt;字段，将结构体变为自引用结构体。&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=ce1aff3a37fcc1c8188eeaf0f39c97e8&quot;&gt;play rust&lt;/a&gt;上执行此代码时，会看到堆值的地址及其内部指针地址相同，这意味着&lt;code&gt;self_ptr&lt;/code&gt;字段是有效的自引用。由于&lt;code&gt;heap_value&lt;/code&gt;变量仅是一个指针，因此移动它（例如，通过将其传递给函数）不会更改结构体本身的地址，因此，即使移动了指针，&lt;code&gt;self_ptr&lt;/code&gt;也依然有效。&lt;/p&gt;
&lt;p&gt;但是，还是有一种方法可以破坏这个例子：我们可以移出&lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;或替换其内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let stack_value = mem::replace(&amp;amp;mut *heap_value, SelfReferential {
    self_ptr: 0 as *const _,
});
println!(&quot;value at: {:p}&quot;, &amp;amp;stack_value);
println!(&quot;internal reference: {:p}&quot;, stack_value.self_ptr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=e160ee8a64cba4cebc1c0473dcecb7c8&quot;&gt;使用play rust在线运行上面的代码&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;这里我们使用&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/mem/fn.replace.html&quot;&gt;&lt;code&gt;mem::replace&lt;/code&gt;&lt;/a&gt;函数，将堆分配内存中的原值替换为一个新结构体实例。这使我们可以将堆分配中的原值&lt;code&gt;heap_value&lt;/code&gt;移动到栈中，而现在该结构体的&lt;code&gt;self_ptr&lt;/code&gt;字段就是一个悬空指针了，它仍然指向旧的堆地址。当你尝试在&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=e160ee8a64cba4cebc1c0473dcecb7c8&quot;&gt;play rust&lt;/a&gt;上运行该代码时，就会看到*“value at:”&lt;em&gt;行和&lt;/em&gt;“internal reference:”*行确实打印了不同的指针地址。因此，堆分配的值并不能完全保证自引用的安全性。&lt;/p&gt;
&lt;p&gt;导致上述现象的根本问题是&lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;允许我们获取对堆分配值的&lt;code&gt;&amp;amp;mut T&lt;/code&gt;引用。通过&lt;code&gt;&amp;amp;mut&lt;/code&gt;引用，就可以使用诸如&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/mem/fn.replace.html&quot;&gt;&lt;code&gt;mem::replace&lt;/code&gt;&lt;/a&gt;或&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/mem/fn.swap.html&quot;&gt;&lt;code&gt;mem::swap&lt;/code&gt;&lt;/a&gt;一类的方法来使堆分配的自引用值无效。要解决此问题，我们必须禁止创建对自引用结构的&lt;code&gt;&amp;amp;mut&lt;/code&gt;引用。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;Pin&amp;lt;Box&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;与&lt;code&gt;Unpin&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;Pinning API用&lt;a href=&quot;https://doc.rust-lang.org/stable/core/pin/struct.Pin.html&quot;&gt;&lt;code&gt;Pin&lt;/code&gt;&lt;/a&gt;封装类型和&lt;a href=&quot;https://doc.rust-lang.org/nightly/std/marker/trait.Unpin.html&quot;&gt;&lt;code&gt;Unpin&lt;/code&gt;&lt;/a&gt;标记trait的方式为&lt;code&gt;&amp;amp;mut T&lt;/code&gt;问题提供了解决方案。其思路是为&lt;code&gt;Pin&lt;/code&gt;类型中所有可用于从&lt;code&gt;Unpin&lt;/code&gt;trait上获取其封装值&lt;code&gt;&amp;amp;mut&lt;/code&gt;可变引用方法（例如&lt;code&gt;get_mut&lt;/code&gt;或&lt;code&gt;deref_mut&lt;/code&gt;）设置关卡。&lt;code&gt;Unpin&lt;/code&gt;trait是&lt;a href=&quot;https://doc.rust-lang.org/reference/special-types-and-traits.html#auto-traits&quot;&gt;自动trait&lt;/a&gt;，即对于所有类型（显式声明退出的类型除外）都默认自动实现。而如果让自引用结构体显式声明退出&lt;code&gt;Unpin&lt;/code&gt;，就再也没有（安全的）方法能够从&lt;code&gt;Pin&amp;lt;Box&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;类型中获取&lt;code&gt;&amp;amp;mut T&lt;/code&gt;可变引用了。如此便能够保证它们的内部自引用始终有效。&lt;/p&gt;
&lt;p&gt;让我们更新上面的&lt;code&gt;SelfReferential&lt;/code&gt;类型以选择取消&lt;code&gt;Unpin&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::marker::PhantomPinned;

struct SelfReferential {
    self_ptr: *const Self,
    _pin: PhantomPinned,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过为结构体添加类型为&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/marker/struct.PhantomPinned.html&quot;&gt;&lt;code&gt;PhantomPinned&lt;/code&gt;&lt;/a&gt;的第二个字段&lt;code&gt;_pin&lt;/code&gt;来退出trait。这是一个零大小的标记类型，唯一目的就是&lt;em&gt;退出&lt;/em&gt;&lt;code&gt;Unpin&lt;/code&gt;trait。鉴于自动trait的工作方式，只需一个非&lt;code&gt;Unpin&lt;/code&gt;的字段就可以使整个结构体退出&lt;code&gt;Unpin&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第二步是将示例中的&lt;code&gt;Box&amp;lt;SelfReferential&amp;gt;&lt;/code&gt;类型更改为&lt;code&gt;Pin&amp;lt;Box&amp;lt;SelfReferential&amp;gt;&amp;gt;&lt;/code&gt;类型。最简单的方法是使用&lt;a href=&quot;https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.pin&quot;&gt;&lt;code&gt;Box::pin&lt;/code&gt;&lt;/a&gt;函数代替&lt;a href=&quot;https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.new&quot;&gt;&lt;code&gt;Box::new&lt;/code&gt;&lt;/a&gt;来创建堆分配的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut heap_value = Box::pin(SelfReferential {
    self_ptr: 0 as *const _,
    _pin: PhantomPinned,
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了将&lt;code&gt;Box::new&lt;/code&gt;改为&lt;code&gt;Box::pin&lt;/code&gt;，还需要在结构体初始字段中添加新的&lt;code&gt;_pin&lt;/code&gt;字段。由于&lt;code&gt;PhantomPinned&lt;/code&gt;是零大小的类型，因此我们只需要使用其类型名称即可进行初始化。&lt;/p&gt;
&lt;p&gt;现在，当我们尝试&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=961b0db194bbe851ff4d0ed08d3bd98a&quot;&gt;在play rust中运行修改后的示例代码&lt;/a&gt;，会发现代码有编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0594]: cannot assign to data in a dereference of `std::pin::Pin&amp;lt;std::boxed::Box&amp;lt;SelfReferential&amp;gt;&amp;gt;`
  --&amp;gt; src/main.rs:10:5
   |
10 |     heap_value.self_ptr = ptr;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign
   |
   = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin&amp;lt;std::boxed::Box&amp;lt;SelfReferential&amp;gt;&amp;gt;`

error[E0596]: cannot borrow data in a dereference of `std::pin::Pin&amp;lt;std::boxed::Box&amp;lt;SelfReferential&amp;gt;&amp;gt;` as mutable
  --&amp;gt; src/main.rs:16:36
   |
16 |     let stack_value = mem::replace(&amp;amp;mut *heap_value, SelfReferential {
   |                                    ^^^^^^^^^^^^^^^^ cannot borrow as mutable
   |
   = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin&amp;lt;std::boxed::Box&amp;lt;SelfReferential&amp;gt;&amp;gt;`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发生这两个错误都是由&lt;code&gt;Pin&amp;lt;Box&amp;lt;SelfReferential&amp;gt;&amp;gt;&lt;/code&gt;类型不再实现&lt;code&gt;DerefMut&lt;/code&gt;trait引起的。这正是我们期望的，因为&lt;code&gt;DerefMut&lt;/code&gt;trait会返回一个&lt;code&gt;&amp;amp;mut&lt;/code&gt;引用，而我们希望禁止该引用。这两个错误就是因为我们既退出了&lt;code&gt;Unpin&lt;/code&gt;，又将&lt;code&gt;Box::new&lt;/code&gt;更改为&lt;code&gt;Box::pin&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在的问题是，编译器不仅禁止在第16行中移动该类型，而且还禁止在第10行中初始化&lt;code&gt;self_ptr&lt;/code&gt;字段。之所以发生这种情况，是因为编译器无法区分&lt;code&gt;&amp;amp;mut&lt;/code&gt;引用的有效使用与无效使用。为了修复初始化一行的报错，我们必须使用非安全的&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_unchecked_mut&quot;&gt;&lt;code&gt;get_unchecked_mut&lt;/code&gt;&lt;/a&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 此处操作是安全的，因为仅修改了结构体字段，而未进行整个结构体的移动
unsafe {
    let mut_ref = Pin::as_mut(&amp;amp;mut heap_value);
    Pin::get_unchecked_mut(mut_ref).self_ptr = ptr;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=b9ebbb11429d9d79b3f9fffe819e2018&quot;&gt;使用play rust在线运行上面的代码&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.get_unchecked_mut&quot;&gt;&lt;code&gt;get_unchecked_mut&lt;/code&gt;&lt;/a&gt;函数的参数应为&lt;code&gt;Pin&amp;lt;&amp;amp;mut T&amp;gt;&lt;/code&gt;而不是&lt;code&gt;Pin&amp;lt;Box&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;，因此我们必须先使用&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.as_mut&quot;&gt;&lt;code&gt;Pin::as_mut&lt;/code&gt;&lt;/a&gt;做转换。然后，我们就可以使用&lt;code&gt;get_unchecked_mut&lt;/code&gt;返回的&lt;code&gt;&amp;amp;mut&lt;/code&gt;引用来设置&lt;code&gt;self_ptr&lt;/code&gt;字段了。&lt;/p&gt;
&lt;p&gt;现在剩下的错误就是我们期望的&lt;code&gt;mem::replace&lt;/code&gt;一行的错误。请记住，此操作尝试将堆分配的值移动到栈上，这会破坏存储在&lt;code&gt;self_ptr&lt;/code&gt;字段中的自引用。通过退出&lt;code&gt;Unpin&lt;/code&gt;并使用&lt;code&gt;Pin&amp;lt;Box&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;，就可以在编译时禁止执行此类操作，从而可以安全地使用自引用结构体。如我们所见，编译器（目前）还不能检测创建的自引用结构体是否安全，因此我们需要使用非安全块并自己确保其正确性。&lt;/p&gt;
&lt;h4&gt;栈上的pinning与&lt;code&gt;Pin&amp;lt;&amp;amp;mut T&amp;gt;&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;在上一节中，我们学习了如何使用&lt;code&gt;Pin&amp;lt;Box&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;安全地创建堆分配上的自引用结构体。尽管这种方法可以很好地工作并且相对安全（除了非安全的初始化），但方法所要求的堆分配会带来一些性能损失。由于Rust始终希望尽可能的提供&lt;em&gt;零成本抽象&lt;/em&gt;，因此pinning API还允许创建指向栈分配值的&lt;code&gt;Pin&amp;lt;&amp;amp;mut T&amp;gt;&lt;/code&gt;实例。&lt;/p&gt;
&lt;p&gt;与拥有封装值&lt;em&gt;所有权&lt;/em&gt;的&lt;code&gt;Pin&amp;lt;Box&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;实例不同，&lt;code&gt;Pin&amp;lt;&amp;amp;mut T&amp;gt;&lt;/code&gt;实例仅临时借用其封装的值。这使事情变得更加复杂，因为它需要程序员自己确保引用带来的附加的条件。最重要的是，在被引用&lt;code&gt;T&lt;/code&gt;的整个生命周期中，&lt;code&gt;Pin&amp;lt;&amp;amp;mut T&amp;gt;&lt;/code&gt;必须保持在pin的状态，这对于基于栈的变量来说会更加难以检查。为了处理这种问题，的确存在像&lt;a href=&quot;https://docs.rs/pin-utils/0.1.0-alpha.4/pin_utils/&quot;&gt;&lt;code&gt;pin-utils&lt;/code&gt;&lt;/a&gt;这样的crate，但是除非你真的知道自己在做什么，一般都不建议使用栈上的pinning。&lt;/p&gt;
&lt;p&gt;要进一步阅读，请查看&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/pin/index.html&quot;&gt;&lt;code&gt;pin&lt;/code&gt;模块&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.new_unchecked&quot;&gt;&lt;code&gt;Pin::new_unchecked&lt;/code&gt;&lt;/a&gt;方法的文档。&lt;/p&gt;
&lt;h4&gt;Pinning和Future&lt;/h4&gt;
&lt;p&gt;正如我们在本文中已经看到的那样，&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll&quot;&gt;&lt;code&gt;Future::poll&lt;/code&gt;&lt;/a&gt;方法使用的pinning参数为&lt;code&gt;Pin&amp;lt;&amp;amp;mut Self&amp;gt;&lt;/code&gt;形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn poll(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Self::Output&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法采用&lt;code&gt;self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;&lt;/code&gt;而不是常规&lt;code&gt;&amp;amp;mut self&lt;/code&gt;作参数的原因如&lt;a href=&quot;./#%E8%87%AA%E5%BC%95%E7%94%A8%E7%BB%93%E6%9E%84%E4%BD%93&quot;&gt;前文&lt;/a&gt;所述，从async/await创建的future实例通常是自引用的。通过将&lt;code&gt;Self&lt;/code&gt;封装为&lt;code&gt;Pin&lt;/code&gt;，并使编译器令async/await生成的自引用future退出&lt;code&gt;Unpin&lt;/code&gt;，可以确保future在两次&lt;code&gt;poll&lt;/code&gt;调用之间不会在内存中移动。如此便可以确保所有内部引用仍然有效。&lt;/p&gt;
&lt;p&gt;值得注意的是，在第一个&lt;code&gt;poll&lt;/code&gt;调用之前，移动future是可行的。这是因为future是惰性的，它在第一次poll之前什么也不做。所以生成状态机的&quot;Start&quot;状态仅包含函数参数，而没有内部引用。为了调用&lt;code&gt;poll&lt;/code&gt;，调用者必须先将future封装到&lt;code&gt;Pin&lt;/code&gt;中，以确保future不会在内存中移动。由于正确的使用栈pinning会更加困难，因此建议始终结合使用&lt;a href=&quot;https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.pin&quot;&gt;&lt;code&gt;Box::pin&lt;/code&gt;&lt;/a&gt;与&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.as_mut&quot;&gt;&lt;code&gt;Pin::as_mut&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果你有兴趣了解如何使用栈pinning来安全地实现future组合器函数，可以选择查看&lt;code&gt;future&lt;/code&gt;crate中&lt;a href=&quot;https://docs.rs/futures-util/0.3.4/src/futures_util/future/future/map.rs.html&quot;&gt;&lt;code&gt;map&lt;/code&gt;组合器方法源码&lt;/a&gt;（该方法的源码相对简单），以及阅读有关pin文档中&lt;a href=&quot;https://doc.rust-lang.org/stable/std/pin/index.html#projections-and-structural-pinning&quot;&gt;投影和结构的pinning&lt;/a&gt;部分。&lt;/p&gt;
&lt;h3&gt;执行器和唤醒器&lt;/h3&gt;
&lt;p&gt;使用async/await，可以最大限度的以完全异步的方式使用future。但是，正如我们从上面了解到的那样，future在被poll之前什么都不做。这意味着我们必须在某些时候发起对它们的&lt;code&gt;poll&lt;/code&gt;，否则异步代码将永远不会执行。&lt;/p&gt;
&lt;p&gt;对于单个future，我们总是可以使用&lt;a href=&quot;./#%E7%AD%89%E5%BE%85Futures&quot;&gt;上述&lt;/a&gt;循环手动等待每个future。但是，这种方式效率很低，而且对于那些创建大量future的程序实际上并不可行。针对此问题的最常见解决方案是定义一个全局执行器，以负责轮询系统中的所有future，直到它们全部完成。&lt;/p&gt;
&lt;h4&gt;执行器&lt;/h4&gt;
&lt;p&gt;执行器的作用是将future生成为任务，通常是使用一些&lt;code&gt;spawn&lt;/code&gt;方法。然后，执行器负责轮询所有future，直到它们全部完成。集中管理所有future的最大好处是，只要future返回&lt;code&gt;Poll::Pending&lt;/code&gt;，执行器就可以切换到另一个future。所以异步操作可以并行运行，CPU也因此持续作业。&lt;/p&gt;
&lt;p&gt;许多执行器的实现也能够利用多核CPU系统的优点。这些实现会创建一个&lt;a href=&quot;https://en.wikipedia.org/wiki/Thread_pool&quot;&gt;线程池&lt;/a&gt;，如果提供足够多的任务，线程池就能利用所有内核，并使用像&lt;a href=&quot;https://en.wikipedia.org/wiki/Work_stealing&quot;&gt;work stealing&lt;/a&gt;之类的技术来平衡核心间的负载。嵌入式系统还会有一些特殊的执行器实现，专门为降低延迟和减少内存开销做了优化。&lt;/p&gt;
&lt;p&gt;为了避免重复轮询future的开销，执行器通常还利用Rust的future支持的唤醒器API。&lt;/p&gt;
&lt;h4&gt;唤醒器&lt;/h4&gt;
&lt;p&gt;唤醒器API的思路是，将特殊的&lt;code&gt;Waker&lt;/code&gt;类型封装在&lt;code&gt;Context&lt;/code&gt;类型中，传递给每一次&lt;code&gt;poll&lt;/code&gt;调用。这个&lt;code&gt;Waker&lt;/code&gt;类型是由执行器创建的，异步任务可以用该类型表示任务已（部分）完成。于是，除非相应的唤醒器通知执行器，否则执行器都不需要在先前返回&lt;code&gt;Poll::Pending&lt;/code&gt;的future上再进行&lt;code&gt;poll&lt;/code&gt;调用了。&lt;/p&gt;
&lt;p&gt;举个小例子可以很好地说明这一点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn write_file() {
    async_write_file(&quot;foo.txt&quot;, &quot;Hello&quot;).await;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此函数将字符串&quot;Hello&quot;异步写入&lt;code&gt;foo.txt&lt;/code&gt;文件。由于硬盘写入需要一些时间，因此该future上的第一次&lt;code&gt;poll&lt;/code&gt;调用可能会返回&lt;code&gt;Poll::Pending&lt;/code&gt;。硬盘驱动将在内部存储传递给&lt;code&gt;poll&lt;/code&gt;调用的&lt;code&gt;Waker&lt;/code&gt;，并在完成文件写入时使用它通知执行器。如此，执行器在收到唤醒器的通知前，都不需要再在这个future上尝试&lt;code&gt;poll&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;我们将在本文的实现一节创建具有唤醒器支持的执行器，届时将详细的看到&lt;code&gt;Waker&lt;/code&gt;类型的工作原理。&lt;/p&gt;
&lt;h3&gt;协作式多任务？&lt;/h3&gt;
&lt;p&gt;在本文的开头，我们讨论了抢先式多任务和协作式多任务。抢占式多任务依靠操作系统在运行中的任务间做强制切换，而协作式多任务则要求任务定期通过&lt;em&gt;yield&lt;/em&gt;操作主动放弃对CPU的控制。协作式的最大优点是任务可以自己保存状态，从而更高效地进行上下文切换，并可以在任务间共享相同的调用栈。&lt;/p&gt;
&lt;p&gt;这可能不够直观，不过future和async/await就是协作式多任务模式的实现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;添加到执行器的每个future本质上都是一个协作任务。&lt;/li&gt;
&lt;li&gt;Future不使用显式的yield操作，而是通过返回&lt;code&gt;Poll::Pending&lt;/code&gt;（或完成时的&lt;code&gt;Poll::Ready&lt;/code&gt;）来放弃对CPU核心的控制。
&lt;ul&gt;
&lt;li&gt;没有什么可以强制future放弃CPU。如果它们愿意，就能够永不从&lt;code&gt;poll&lt;/code&gt;中返回，如在一个无限循环中打转。&lt;/li&gt;
&lt;li&gt;由于每个future都可以阻止执行器中其他future的执行，因此我们需要信任它们不是恶意的。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Future在内部存储它在下一次&lt;code&gt;poll&lt;/code&gt;调用时恢复执行所需的所有状态。使用async/await，编译器会自动检测所需的所有变量，并将其存储在生成的状态机中。
&lt;ul&gt;
&lt;li&gt;仅保存恢复执行所需的最少状态。&lt;/li&gt;
&lt;li&gt;由于&lt;code&gt;poll&lt;/code&gt;方法在返回时会放弃调用栈，因此该栈可用于poll其他的future。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们看到future和async/await完美契合协作式多任务，区别就是使用了一些不同的术语。因此，在下文中，我们将交替使用术语“任务”和“future”。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;现在，我们了解了Rust中基于future和async/await的协作式多任务的工作原理，是时候向我们的内核中添加对多任务的支持了。由于&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html&quot;&gt;&lt;code&gt;Future&lt;/code&gt;&lt;/a&gt;trait是&lt;code&gt;core&lt;/code&gt;库的一部分，而async/await是Rust语言本身的功能，因此在&lt;code&gt;#![no_std]&lt;/code&gt;内核中使用无需其他操作就可以使用它们。唯一的要求是我们至少应使用Rust在2020-03-25之后的nightly版本，因为之前的async/await是不兼容&lt;code&gt;no_std&lt;/code&gt;的。&lt;/p&gt;
&lt;p&gt;只要使用较近的nightly版本，我们就可以在&lt;code&gt;main.rs&lt;/code&gt;中使用async/await：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn async_number() -&amp;gt; u32 {
    42
}

async fn example_task() {
    let number = async_number().await;
    println!(&quot;async number: {}&quot;, number);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;async_number&lt;/code&gt;函数是一个&lt;code&gt;async fn&lt;/code&gt;，因此编译器将其转换为实现了&lt;code&gt;Future&lt;/code&gt;trait的状态机。由于该函数仅返回&lt;code&gt;42&lt;/code&gt;，因此生成的future将在首次&lt;code&gt;poll&lt;/code&gt;调用时直接返回&lt;code&gt;Poll::Ready(42)&lt;/code&gt;。像&lt;code&gt;async_number&lt;/code&gt;一样，&lt;code&gt;example_task&lt;/code&gt;函数也是一个&lt;code&gt;async fn&lt;/code&gt;。 它等待&lt;code&gt;async_number&lt;/code&gt;返回的数字，然后使用println宏将其打印出来。&lt;/p&gt;
&lt;p&gt;要运行&lt;code&gt;example_task&lt;/code&gt;返回的future，我们需要一直对其调用&lt;code&gt;poll&lt;/code&gt;，到它通过返回&lt;code&gt;Poll::Ready&lt;/code&gt;告知其已完成为止。为此，我们需要创建一个简单的执行器类型。&lt;/p&gt;
&lt;h3&gt;任务&lt;/h3&gt;
&lt;p&gt;在实现执行器之前，我们要创建一个具有&lt;code&gt;Task&lt;/code&gt;类型的新模块&lt;code&gt;task&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod task;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use core::{future::Future, pin::Pin};
use alloc::boxed::Box;

pub struct Task {
    future: Pin&amp;lt;Box&amp;lt;dyn Future&amp;lt;Output = ()&amp;gt;&amp;gt;&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Task&lt;/code&gt;结构体是一个新的类型封装器，它封装了一个内存固定的、堆分配的、以空类型&lt;code&gt;()&lt;/code&gt;作为输出的动态分发的future。让我们详细研究一下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们要求与任务关联的future返回&lt;code&gt;()&lt;/code&gt;。这意味着任务不会返回任何结果，只是为了执行其中的副作用。例如，我们上面定义的&lt;code&gt;example_task&lt;/code&gt;函数也没有返回值，但是作为副作用，函数将一些信息打印在了屏幕上。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dyn&lt;/code&gt;关键字告诉我们存放在&lt;code&gt;Box&lt;/code&gt;中的是一个&lt;a href=&quot;https://doc.rust-lang.org/book/ch17-02-trait-objects.html&quot;&gt;trait对象&lt;/a&gt;。这意味着future上的方法是&lt;a href=&quot;https://doc.rust-lang.org/book/ch17-02-trait-objects.html#trait-objects-perform-dynamic-dispatch&quot;&gt;动态分发&lt;/a&gt;的，因此我们才可以在&lt;code&gt;Task&lt;/code&gt;类型中存储不同类型的future。这很重要，因为每个&lt;code&gt;async fn&lt;/code&gt;都有自己的future类型，而我们希望能够创建多个不同的任务。&lt;/li&gt;
&lt;li&gt;正如我们在&lt;a href=&quot;./#Pinning&quot;&gt;pinning一节&lt;/a&gt;了解到的那样，&lt;code&gt;Pin&amp;lt;Box&amp;gt;&lt;/code&gt;类型通过将值放在堆上并防止对值创建&amp;amp;mut引用来确保该值不会在内存中移动。这很重要，因为async/await生成的future可能是自引用的，即包含指向自身的指针，而该指针将在future移动时失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了能够适应future创建&lt;code&gt;Task&lt;/code&gt;结构体，我们编写一个&lt;code&gt;new&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Task {
    pub fn new(future: impl Future&amp;lt;Output = ()&amp;gt; + &apos;static) -&amp;gt; Task {
        Task {
            future: Box::pin(future),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数接受输出类型为&lt;code&gt;()&lt;/code&gt;的任意future，然后通过&lt;a href=&quot;https://doc.rust-lang.org/nightly/alloc/boxed/struct.Box.html#method.pin&quot;&gt;&lt;code&gt;Box::pin&lt;/code&gt;&lt;/a&gt;函数将其固定在内存中。然后，函数将future封装在Task结构体中并返回。这里需要使用&lt;code&gt;&apos;static&lt;/code&gt;生命周期，因为返回的&lt;code&gt;Task&lt;/code&gt;生存时间不确定，因此future也必须在该时间内保持有效。&lt;/p&gt;
&lt;p&gt;我们还添加了一个&lt;code&gt;poll&lt;/code&gt;方法，使执行器可以轮询存储的future：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::task::{Context, Poll};

impl Task {
    fn poll(&amp;amp;mut self, context: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;()&amp;gt; {
        self.future.as_mut().poll(context)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;Future&lt;/code&gt;trait的&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll&quot;&gt;&lt;code&gt;poll&lt;/code&gt;&lt;/a&gt;方法需要在&lt;code&gt;Pin&amp;lt;&amp;amp;mut T&amp;gt;&lt;/code&gt;类型上调用，因此我们先要使用&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/pin/struct.Pin.html#method.as_mut&quot;&gt;&lt;code&gt;Pin::as_mut&lt;/code&gt;&lt;/a&gt;方法转换类型为&lt;code&gt;Pin&amp;lt;Box&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;的&lt;code&gt;self.future&lt;/code&gt;字段。然后，在转换后的&lt;code&gt;self.future&lt;/code&gt;字段上调用&lt;code&gt;poll&lt;/code&gt;并返回结果。由于&lt;code&gt;Task::poll&lt;/code&gt;方法仅应由我们稍后创建的执行器调用，因此保持其为&lt;code&gt;task&lt;/code&gt;模块的私有函数。&lt;/p&gt;
&lt;h3&gt;简单执行器&lt;/h3&gt;
&lt;p&gt;由于执行器可能非常复杂，因此我们有意在开始仅创建一个非常基本的执行器，然后再渐进的实现更多功能。为此，我们先创建一个新的&lt;code&gt;task::simple_executor&lt;/code&gt;子模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod simple_executor;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use super::Task;
use alloc::collections::VecDeque;

pub struct SimpleExecutor {
    task_queue: VecDeque&amp;lt;Task&amp;gt;,
}

impl SimpleExecutor {
    pub fn new() -&amp;gt; SimpleExecutor {
        SimpleExecutor {
            task_queue: VecDeque::new(),
        }
    }

    pub fn spawn(&amp;amp;mut self, task: Task) {
        self.task_queue.push_back(task)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该结构体包含一个&lt;a href=&quot;https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html&quot;&gt;&lt;code&gt;VecDeque&lt;/code&gt;&lt;/a&gt;类型的字段&lt;code&gt;task_queue&lt;/code&gt;，这是一个在两端都能执行push和pop操作的向量。使用这种类型的思路是，通过&lt;code&gt;spawn&lt;/code&gt;在队尾方法插入新任务，并从队首弹出要执行的下一个任务。这样，我们就得到了一个简单的&lt;a href=&quot;https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)&quot;&gt;FIFO队列&lt;/a&gt;（“先进先出”）。&lt;/p&gt;
&lt;h4&gt;假唤醒器&lt;/h4&gt;
&lt;p&gt;为了调用&lt;code&gt;poll&lt;/code&gt;方法，我们需要创建一个&lt;code&gt;Context&lt;/code&gt;类型，用来封装&lt;code&gt;Waker&lt;/code&gt;类型。简单起见，我们将首先创建一个不执行任何操作的假唤醒器。为此，我们创建了&lt;code&gt;RawWaker&lt;/code&gt;实例，用以定义不同&lt;code&gt;Waker&lt;/code&gt;方法的实现，然后使用&lt;code&gt;Waker::from_raw&lt;/code&gt;函数将其转换为&lt;code&gt;Waker&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::task::{Waker, RawWaker};

fn dummy_raw_waker() -&amp;gt; RawWaker {
    todo!();
}

fn dummy_waker() -&amp;gt; Waker {
    unsafe { Waker::from_raw(dummy_raw_waker()) }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;from_raw&lt;/code&gt;函数是非安全的，因为如果程序员不按照&lt;code&gt;RawWaker&lt;/code&gt;文档的要求使用，就可能导致未定义的行为。在查看&lt;code&gt;dummy_raw_waker&lt;/code&gt;函数的实现之前，我们首先尝试了解&lt;code&gt;RawWaker&lt;/code&gt;类型的工作方式。&lt;/p&gt;
&lt;h5&gt;&lt;code&gt;RawWaker&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;RawWaker&lt;/code&gt;类型要求程序员显式定义一个&lt;a href=&quot;https://en.wikipedia.org/wiki/Virtual_method_table&quot;&gt;&lt;em&gt;虚拟方法表&lt;/em&gt;&lt;/a&gt;(&lt;em&gt;vtable&lt;/em&gt;)，用以指定在克隆，唤醒、删除&lt;code&gt;RawWaker&lt;/code&gt;时应调用的函数。vtable的布局由&lt;a href=&quot;https://doc.rust-lang.org/stable/core/task/struct.RawWakerVTable.html&quot;&gt;&lt;code&gt;RawWakerVTable&lt;/code&gt;&lt;/a&gt;类型定义。每个函数都接收一个&lt;code&gt;*const ()&lt;/code&gt;参数，该参数本质上是一个指向某结构体的&lt;em&gt;擦除类型&lt;/em&gt;的&lt;code&gt;&amp;amp;self&lt;/code&gt;指针，例如在堆上的分配。使用&lt;code&gt;*const ()&lt;/code&gt;指针而不是某适当引用的原因是&lt;code&gt;RawWaker&lt;/code&gt;类型应该是非泛型的，但仍需支持任意类型。作为参数传递给函数的指针值为给&lt;a href=&quot;https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html#method.new&quot;&gt;&lt;code&gt;RawWaker::new&lt;/code&gt;&lt;/a&gt;的&lt;code&gt;data&lt;/code&gt;指针。&lt;/p&gt;
&lt;p&gt;通常，&lt;code&gt;RawWaker&lt;/code&gt;结构体是为封装在&lt;a href=&quot;https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html&quot;&gt;&lt;code&gt;Box&lt;/code&gt;&lt;/a&gt;或&lt;a href=&quot;https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html&quot;&gt;&lt;code&gt;Arc&lt;/code&gt;&lt;/a&gt;类型中的某些堆分配而创建的。对于这些类型，可以使用&lt;a href=&quot;https://doc.rust-lang.org/stable/alloc/boxed/struct.Box.html#method.into_raw&quot;&gt;&lt;code&gt;Box::into_raw&lt;/code&gt;&lt;/a&gt;之类的方法将&lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;转换为&lt;code&gt;*const T&lt;/code&gt;指针。然后可以将该指针转换为匿名&lt;code&gt;*const ()&lt;/code&gt;指针，再传递给&lt;code&gt;RawWaker::new&lt;/code&gt;。由于每个vtable函数都使用相同的&lt;code&gt;*const ()&lt;/code&gt;作为参数，因此这些函数可以安全地将指针转换回&lt;code&gt;Box&amp;lt;T&amp;gt;&lt;/code&gt;或&lt;code&gt;&amp;amp;T&lt;/code&gt;以执行操作。可以想象，此过程非常危险，很容易出错导致未定义行为。因此，若非必要，则并不建议手动创建&lt;code&gt;RawWaker&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;一个假的&lt;code&gt;RawWaker&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;虽然不建议手动创建&lt;code&gt;RawWaker&lt;/code&gt;，但目前还没有其他方法可以创建不执行任何操作的假&lt;code&gt;Waker&lt;/code&gt;。幸运的是，不执行任何操作也会使得实现&lt;code&gt;dummy_raw_waker&lt;/code&gt;函数变得相对安全：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::task::RawWakerVTable;

fn dummy_raw_waker() -&amp;gt; RawWaker {
    fn no_op(_: *const ()) {}
    fn clone(_: *const ()) -&amp;gt; RawWaker {
        dummy_raw_waker()
    }

    let vtable = &amp;amp;RawWakerVTable::new(clone, no_op, no_op, no_op);
    RawWaker::new(0 as *const (), vtable)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们定义两个名为&lt;code&gt;no_op&lt;/code&gt;和&lt;code&gt;clone&lt;/code&gt;的内部函数。&lt;code&gt;no_op&lt;/code&gt;函数接受&lt;code&gt;* const()&lt;/code&gt;指针，不执行任何操作。&lt;code&gt;clone&lt;/code&gt;函数也接受&lt;code&gt;* const()&lt;/code&gt;指针，并通过再次调用&lt;code&gt;dummy_raw_waker&lt;/code&gt;返回一个新的&lt;code&gt;RawWaker&lt;/code&gt;。我们使用这两个函数来创建最小化的&lt;code&gt;RawWakerVTable&lt;/code&gt;：&lt;code&gt;clone&lt;/code&gt;函数用于克隆操作，&lt;code&gt;no_op&lt;/code&gt;函数用于其他所有操作。由于&lt;code&gt;RawWaker&lt;/code&gt;不执行任何操作，因此从&lt;code&gt;clone&lt;/code&gt;返回的是一个新&lt;code&gt;RawWaker&lt;/code&gt;还是一个真正的克隆其实并不重要。&lt;/p&gt;
&lt;p&gt;创建&lt;code&gt;vtable&lt;/code&gt;之后，我们使用&lt;a href=&quot;https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html#method.new&quot;&gt;&lt;code&gt;RawWaker::new&lt;/code&gt;&lt;/a&gt;函数创建&lt;code&gt;RawWaker&lt;/code&gt;。传递的&lt;code&gt;* const()&lt;/code&gt;无关紧要，因为并没有任何vtable的函数会使用它。于是我们只传递了一个空指针。&lt;/p&gt;
&lt;h4&gt;一个&lt;code&gt;run&lt;/code&gt;方法&lt;/h4&gt;
&lt;p&gt;现在，我们有了创建&lt;code&gt;Waker&lt;/code&gt;实例的方法，就可以使用它在执行器上实现&lt;code&gt;run&lt;/code&gt;方法。最简单的&lt;code&gt;run&lt;/code&gt;方法是在循环中不停的poll所有队列中的任务，直到所有任务均已完成。这并不高效，因为它没有利用&lt;code&gt;Waker&lt;/code&gt;类型的通知功能，但是它是使示例能够运行起来的的最简单方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::task::{Context, Poll};

impl SimpleExecutor {
    pub fn run(&amp;amp;mut self) {
        while let Some(mut task) = self.task_queue.pop_front() {
            let waker = dummy_waker();
            let mut context = Context::from_waker(&amp;amp;waker);
            match task.poll(&amp;amp;mut context) {
                Poll::Ready(()) =&amp;gt; {} // 任务已完成
                Poll::Pending =&amp;gt; self.task_queue.push_back(task),
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数使用&lt;code&gt;while let&lt;/code&gt;循环来处理&lt;code&gt;task_queue&lt;/code&gt;中的所有任务。对于每个任务，我们首先把由&lt;code&gt;dummy_waker&lt;/code&gt;函数返回的&lt;code&gt;Waker&lt;/code&gt;实例封装为&lt;code&gt;Context&lt;/code&gt;类型。然后调用&lt;code&gt;Task::poll&lt;/code&gt;方法并使用该&lt;code&gt;context&lt;/code&gt;做参数。如果&lt;code&gt;poll&lt;/code&gt;方法返回&lt;code&gt;Poll :: Ready&lt;/code&gt;，则说明任务已完成，就可以继续执行下一个任务。如果任务仍然处于&lt;code&gt;Poll::Pending&lt;/code&gt;，则将其再次添加到队尾，以便在后续的循环中再次轮询它。&lt;/p&gt;
&lt;h4&gt;尝试运行&lt;/h4&gt;
&lt;p&gt;使用我们的&lt;code&gt;SimpleExecutor&lt;/code&gt;类型，现在就可以尝试在&lt;code&gt;main.rs&lt;/code&gt;中运行&lt;code&gt;example_task&lt;/code&gt;函数返回的任务了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use blog_os::task::{Task, simple_executor::SimpleExecutor};

fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    // […] initialization routines, including `init_heap`

    let mut executor = SimpleExecutor::new();
    executor.spawn(Task::new(example_task()));
    executor.run();

    // […] test_main, &quot;it did not crash&quot; message, hlt_loop
}


// Below is the example_task function again so that you don&apos;t have to scroll up

async fn async_number() -&amp;gt; u32 {
    42
}

async fn example_task() {
    let number = async_number().await;
    println!(&quot;async number: {}&quot;, number);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行代码，将看到预期的消息&quot;&lt;em&gt;async number: 42&lt;/em&gt;&quot;打印到屏幕上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/qemu-simple-executor.png&quot; alt=&quot;QEMU简单执行器&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们来总结一下示例中执行的各个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，我们创建一个带有空&lt;code&gt;task_queue&lt;/code&gt;的&lt;code&gt;SimpleExecutor&lt;/code&gt;类型实例。&lt;/li&gt;
&lt;li&gt;接下来，我们调用异步&lt;code&gt;example_task&lt;/code&gt;函数，该函数返回的是future。将这个future封装在&lt;code&gt;Task&lt;/code&gt;类型中，以将其移动到堆中并固定在内存中，然后通过&lt;code&gt;spawn&lt;/code&gt;方法将该任务添加到执行器的&lt;code&gt;task_queue&lt;/code&gt;中。&lt;/li&gt;
&lt;li&gt;然后，我们调用&lt;code&gt;run&lt;/code&gt;方法以开始执行队列中的单个任务。这涉及：
&lt;ul&gt;
&lt;li&gt;从&lt;code&gt;task_queue&lt;/code&gt;队首弹出任务。&lt;/li&gt;
&lt;li&gt;为任务创建&lt;code&gt;RawWaker&lt;/code&gt;，将其转换为&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/task/struct.Waker.html&quot;&gt;&lt;code&gt;Waker&lt;/code&gt;&lt;/a&gt;实例，然后用它创建&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/task/struct.Context.html&quot;&gt;&lt;code&gt;Context&lt;/code&gt;&lt;/a&gt;实例。&lt;/li&gt;
&lt;li&gt;在任务的future上调用&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll&quot;&gt;&lt;code&gt;poll&lt;/code&gt;&lt;/a&gt;方法，并传入我们刚才创建的&lt;code&gt;Context&lt;/code&gt;实例。&lt;/li&gt;
&lt;li&gt;由于&lt;code&gt;example_task&lt;/code&gt;不会等待任何操作，因此可以直接运行到第一次&lt;code&gt;poll&lt;/code&gt;调用为止。这就是打印&quot;&lt;em&gt;async number: 42&lt;/em&gt;&quot;位置。&lt;/li&gt;
&lt;li&gt;由于&lt;code&gt;example_task&lt;/code&gt;将立刻返回&lt;code&gt;Poll::Ready&lt;/code&gt;，因此并不会将其再添加回任务队列。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在&lt;code&gt;task_queue&lt;/code&gt;为空后，&lt;code&gt;run&lt;/code&gt;方法返回。之后，我们的&lt;code&gt;kernel_main&lt;/code&gt;函数将继续执行，并打印&quot;&lt;em&gt;It did not crash!&lt;/em&gt;&quot;信息。&lt;/p&gt;
&lt;h3&gt;异步键盘输入&lt;/h3&gt;
&lt;p&gt;这个简单执行器并不能利用&lt;code&gt;Waker&lt;/code&gt;通知，只会循环遍历所有任务直到它们完成。对于我们的示例，这并不是问题，因为我们的&lt;code&gt;example_task&lt;/code&gt;在首次执行&lt;code&gt;poll&lt;/code&gt;时就可以直接完成。要能观察到一个正确的&lt;code&gt;Waker&lt;/code&gt;实现所带来的性能优势，我们首先需要创建一个真正异步的任务，即可能会在首次&lt;code&gt;poll&lt;/code&gt;调用中返回&lt;code&gt;Poll::Pending&lt;/code&gt;的任务。&lt;/p&gt;
&lt;p&gt;其实我们的系统中已经存在了一些异步特性：硬件中断。正如我们在&lt;a href=&quot;https://os.phil-opp.com/hardware-interrupts/&quot;&gt;&lt;em&gt;硬件中断&lt;/em&gt;&lt;/a&gt;一文中了解到的那样，硬件中断可以在任意时刻发生，这是由某些外部设备所决定的。例如，在经过一段预定义的时间后，硬件计时器会将中断发送到CPU。当CPU接收到中断时，它会立即将控制权转移给在中断描述符表(IDT)中定义的相应处理函数。&lt;/p&gt;
&lt;p&gt;下面，我们将基于键盘中断创建一个异步任务。键盘中断是一个很好的选择，因为它既具有不确定性又对延迟有很高的要求。不确定性意味着无法预测下一次按键的发生时间，因为这完全取决于用户。低延迟是指我们要及时处理键盘输入，否则用户会感到滞后。为了以一种更高效的方式支持此类任务，执行器就需要对&lt;code&gt;Waker&lt;/code&gt;通知提供适当的支持。&lt;/p&gt;
&lt;h4&gt;键盘扫描码队列&lt;/h4&gt;
&lt;p&gt;我们当前仍是直接在中断处理程序中处理键盘输入。长远考虑这并不是一个好主意，因为中断处理程序应保持尽可能短，以免长时间中断重要工作。因此，中断处理程序应仅执行必要的最少量的工作（如键盘扫描码的读取），而将其余工作（如键盘扫描码的解释）留给后台任务。&lt;/p&gt;
&lt;p&gt;将工作委派给后台任务的常见模式是创建某种队列。中断处理程序将工作单元推送到队列，而后台任务处理程序从队列中取出工作以进行处理。对于我们的键盘中断，就是中断处理程序仅从键盘读取扫描代码，并将其推送到队列，然后直接返回。键盘任务从队列的另一端取出扫描码，并对每个扫描码进行解释和处理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/scancode-queue.svg&quot; alt=&quot;扫描码队列&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以用一个由互斥锁保护的&lt;a href=&quot;https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html&quot;&gt;&lt;code&gt;VecDeque&lt;/code&gt;&lt;/a&gt;来简单的实现该队列。但是，在中断处理程序中使用互斥锁并不是一个好主意，因为很容易导致死锁。例如，当用户在键盘任务锁定队列时又按下了某个键，中断处理程序将再次尝试获取该锁，这会导致死锁。这种方法的另一个问题是，当&lt;code&gt;VecDeque&lt;/code&gt;已满时，就会通过执行新的堆分配来自动增加队列容量。这可能又会导致死锁，因为我们的分配器还在内部使用了互斥锁。还有更深层次的问题，当堆碎片化时，堆分配可能会花费大量时间甚至会分配失败。&lt;/p&gt;
&lt;p&gt;为避免这些问题，我们需要一个&lt;code&gt;push&lt;/code&gt;操作不涉及互斥量或堆分配的队列实现。这种队列可以通过使用&lt;a href=&quot;https://doc.rust-lang.org/core/sync/atomic/index.html&quot;&gt;无锁原子操作&lt;/a&gt;来实现元素的push和pop。如此，就可以创建只需要&lt;code&gt;&amp;amp;self&lt;/code&gt;引用即可使用的&lt;code&gt;push&lt;/code&gt;和&lt;code&gt;pop&lt;/code&gt;操作，因此也无需互斥锁。为了避免在&lt;code&gt;push&lt;/code&gt;时执行堆分配，可以通过预先分配的固定大小的缓冲区来支持队列空间扩展。尽管这会让队列有界（即有最大长度限制），但在实际操作中通常能够为队列长度定义一个合理的上限，这并不是一个大问题。&lt;/p&gt;
&lt;h5&gt;&lt;code&gt;crossbeam&lt;/code&gt;crate&lt;/h5&gt;
&lt;p&gt;以正确且高效的方式实现这样的队列是一件非常困难的事，因此建议使用经过良好测试的现有实现。&lt;code&gt;crossbeam&lt;/code&gt;是一个流行的Rust项目，它实现了多种用于并发编程的无互斥类型。该crate提供的&lt;code&gt;ArrayQueue&lt;/code&gt;正是我们需要的类型。更加幸运的是，该类型恰好与具有堆分配支持的&lt;code&gt;no_std&lt;/code&gt;crate完全兼容。&lt;/p&gt;
&lt;p&gt;要使用该类型，我们需要添加&lt;code&gt;crossbeam-queue&lt;/code&gt;crate依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies.crossbeam-queue]
version = &quot;0.2.1&quot;
default-features = false
features = [&quot;alloc&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该crate默认依赖标准库。为了使其与&lt;code&gt;no_std&lt;/code&gt;兼容，我们需要禁用其默认特性并启用&lt;code&gt;alloc&lt;/code&gt;特性。（请注意，在这里选择主&lt;code&gt;crossbeam&lt;/code&gt;crate作为依赖将不能工作，因为它缺少用于&lt;code&gt;no_std&lt;/code&gt;的&lt;code&gt;queue&lt;/code&gt;模块导出。我们提交了一个&lt;a href=&quot;https://github.com/crossbeam-rs/crossbeam/pull/480&quot;&gt;pull请求&lt;/a&gt;来解决此问题，但该修改尚未在crates.io上发布。）&lt;/p&gt;
&lt;h5&gt;队列实现&lt;/h5&gt;
&lt;p&gt;有了&lt;code&gt;ArrayQueue&lt;/code&gt;类型，我们就可以在新的&lt;code&gt;task::keyboard&lt;/code&gt;模块中创建全局的扫描码队列了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod keyboard;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use conquer_once::spin::OnceCell;
use crossbeam_queue::ArrayQueue;

static SCANCODE_QUEUE: OnceCell&amp;lt;ArrayQueue&amp;lt;u8&amp;gt;&amp;gt; = OnceCell::uninit();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;a href=&quot;https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.new&quot;&gt;&lt;code&gt;ArrayQueue::new&lt;/code&gt;&lt;/a&gt;需要执行堆分配，而这在编译时是不可行的（&lt;a href=&quot;https://github.com/rust-lang/const-eval/issues/20&quot;&gt;到目前为止&lt;/a&gt;），所以我们不能直接初始化该静态变量。为此，我们使用了&lt;a href=&quot;https://docs.rs/conquer-once/0.2.0/conquer_once/index.html&quot;&gt;&lt;code&gt;conquer_once&lt;/code&gt;&lt;/a&gt;crate的&lt;a href=&quot;https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html&quot;&gt;&lt;code&gt;OnceCell&lt;/code&gt;&lt;/a&gt;类型，以能够安全的执行一次性静态变量的初始化。为了使用该crate，我们需要将其作为依赖项添加到&lt;code&gt;Cargo.toml&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies.conquer-once]
version = &quot;0.2.0&quot;
default-features = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们确实也可以在这里使用&lt;a href=&quot;https://docs.rs/lazy_static/1.4.0/lazy_static/index.html&quot;&gt;&lt;code&gt;lazy_static&lt;/code&gt;&lt;/a&gt;宏来代替&lt;a href=&quot;https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html&quot;&gt;&lt;code&gt;OnceCell&lt;/code&gt;&lt;/a&gt;。不过&lt;code&gt;OnceCell&lt;/code&gt;类型的优点是能够确保初始化不会在中断处理程序中发生，从而阻止中断处理程序执行堆分配。&lt;/p&gt;
&lt;h4&gt;填充队列&lt;/h4&gt;
&lt;p&gt;为了填充扫描代队列，我们创建了一个新的&lt;code&gt;add_scancode&lt;/code&gt;函数，以从中断处理程序中调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use crate::println;

/// 由键盘中断调用程序调用
///
/// 该函数不应阻塞或执行堆分配
pub(crate) fn add_scancode(scancode: u8) {
    if let Ok(queue) = SCANCODE_QUEUE.try_get() {
        if let Err(_) = queue.push(scancode) {
            println!(&quot;WARNING: scancode queue full; dropping keyboard input&quot;);
        }
    } else {
        println!(&quot;WARNING: scancode queue uninitialized&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;a href=&quot;https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html#method.try_get&quot;&gt;&lt;code&gt;OnceCell::try_get&lt;/code&gt;&lt;/a&gt;函数来获取对初始化队列的引用。如果队列尚未初始化，就忽略键盘扫描码并直接打印警告。要注意的是，不能在此函数中初始化队列，因为它将被中断处理程序调用，而我们不应该在中断处理的过程中执行堆分配。由于不应从&lt;code&gt;main.rs&lt;/code&gt;中调用此函数，因此我们使用&lt;code&gt;pub(crate)&lt;/code&gt;使该函数仅可在&lt;code&gt;lib.rs&lt;/code&gt;中使用。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.push&quot;&gt;&lt;code&gt;ArrayQueue::push&lt;/code&gt;&lt;/a&gt;方法只需&lt;code&gt;&amp;amp;self&lt;/code&gt;引用即可执行，这使得我们能够非常简单的从静态队列上调用该方法。&lt;code&gt;ArrayQueue&lt;/code&gt;类型本身执行所有必要的同步，因此这里不需要使用互斥类封装。如果队列已满，我们也会打印警告。&lt;/p&gt;
&lt;p&gt;要在键盘中断上调用&lt;code&gt;add_scancode&lt;/code&gt;函数，就需要在&lt;code&gt;interrupts&lt;/code&gt;模块中更新&lt;code&gt;keyboard_interrupt_handler&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(
    _stack_frame: &amp;amp;mut InterruptStackFrame
) {
    use x86_64::instructions::port::Port;

    let mut port = Port::new(0x60);
    let scancode: u8 = unsafe { port.read() };
    crate::task::keyboard::add_scancode(scancode); // new

    unsafe {
        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们从该函数中删除了所有键盘事件处理的代码，并添加了对&lt;code&gt;add_scancode&lt;/code&gt;函数的调用，其余代码保持不变。&lt;/p&gt;
&lt;p&gt;正如预期，现在使用&lt;code&gt;cargo run&lt;/code&gt;运行内核时，每次按键不再显示在屏幕上，取而代之的是，每次按键时都会看到队列未初始化的警告。&lt;/p&gt;
&lt;h4&gt;扫描码流&lt;/h4&gt;
&lt;p&gt;为了初始化&lt;code&gt;SCANCODE_QUEUE&lt;/code&gt;并以异步方式从队列中读取扫描码，我们创建了一个新的&lt;code&gt;ScancodeStream&lt;/code&gt;类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct ScancodeStream {
    _private: (),
}

impl ScancodeStream {
    pub fn new() -&amp;gt; Self {
        SCANCODE_QUEUE.try_init_once(|| ArrayQueue::new(100))
            .expect(&quot;ScancodeStream::new should only be called once&quot;);
        ScancodeStream { _private: () }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;_private&lt;/code&gt;字段用于阻止从本模块外构造结构体的行为。这使得&lt;code&gt;new&lt;/code&gt;函数成为构造该类的唯一方法。在函数中，我们首先尝试初始化&lt;code&gt;SCANCODE_QUEUE&lt;/code&gt;静态变量。如果它已初始化，我们就产生一个panic，如此确保只会存在一个&lt;code&gt;ScancodeStream&lt;/code&gt;实例。&lt;/p&gt;
&lt;p&gt;为了使扫描码可用于异步任务，下一步是实现一个类似&lt;code&gt;poll&lt;/code&gt;的方法，以尝试从队列中弹出下一个扫描码。虽然这听上去似乎是我们应该为该类型实现&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html&quot;&gt;&lt;code&gt;Future&lt;/code&gt;&lt;/a&gt;trait，但在这里实际上并不是。问题在于，&lt;code&gt;Future&lt;/code&gt;trait仅对单个异步值进行抽象，并且期望在它返回&lt;code&gt;Poll::Ready&lt;/code&gt;之后不会再次调用&lt;code&gt;poll&lt;/code&gt;方法。但是，我们的扫描码队列包含多个异步值，因此可以持续对其进行轮询。&lt;/p&gt;
&lt;h5&gt;&lt;code&gt;Stream&lt;/code&gt;trait&lt;/h5&gt;
&lt;p&gt;由于产生多个异步值的类型很常见，因此&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/&quot;&gt;&lt;code&gt;future&lt;/code&gt;&lt;/a&gt;crate为此类提供了有用的抽象：&lt;a href=&quot;https://rust-lang.github.io/async-book/05_streams/01_chapter.html&quot;&gt;&lt;code&gt;Stream&lt;/code&gt;&lt;/a&gt;trait。其定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub trait Stream {
    type Item;

    fn poll_next(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context)
        -&amp;gt; Poll&amp;lt;Option&amp;lt;Self::Item&amp;gt;&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此定义与&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/future/trait.Future.html&quot;&gt;&lt;code&gt;Future&lt;/code&gt;&lt;/a&gt;trait非常相似，但有以下区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关联的类型叫做&lt;code&gt;Item&lt;/code&gt;而不是&lt;code&gt;Output&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;与返回&lt;code&gt;Poll&amp;lt;Self::Item&amp;gt;&lt;/code&gt;的&lt;code&gt;poll&lt;/code&gt;方法不同，&lt;code&gt;Stream&lt;/code&gt;trait定义了一个返回&lt;code&gt;Poll&amp;lt;Option&amp;lt;Self::Item&amp;gt;&amp;gt;&lt;/code&gt;的&lt;code&gt;poll_next&lt;/code&gt;方法（请注意附加的&lt;code&gt;Option&lt;/code&gt;）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外还有一个语义上的区别：&lt;code&gt;poll_next&lt;/code&gt;可以被重复调用，直到返回&lt;code&gt;Poll::Ready(None)&lt;/code&gt;来表示流已完成。从这方面来看，该方法类似于&lt;a href=&quot;https://doc.rust-lang.org/stable/core/iter/trait.Iterator.html#tymethod.next&quot;&gt;&lt;code&gt;Iterator::next&lt;/code&gt;&lt;/a&gt;方法，即也会在返回最后一个值之后就只返回&lt;code&gt;None&lt;/code&gt;。&lt;/p&gt;
&lt;h5&gt;实现&lt;code&gt;Stream&lt;/code&gt;&lt;/h5&gt;
&lt;p&gt;让我们为&lt;code&gt;ScancodeStream&lt;/code&gt;实现&lt;code&gt;Stream&lt;/code&gt;trait，以便使用异步方式提供&lt;code&gt;SCANCODE_QUEUE&lt;/code&gt;中的值。为此，我们首先需要添加对&lt;code&gt;futures-util&lt;/code&gt;crate的依赖，其中就包含&lt;code&gt;Stream&lt;/code&gt;类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies.futures-util]
version = &quot;0.3.4&quot;
default-features = false
features = [&quot;alloc&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们禁用默认特性以使crate兼容&lt;code&gt;no_std&lt;/code&gt;，并启用&lt;code&gt;alloc&lt;/code&gt;特性以使其基于堆分配的类型可用（稍后将需要它）。（请注意，我们确实可以添加对主&lt;code&gt;futures&lt;/code&gt;crate的依赖，从而重新导出&lt;code&gt;futures-util&lt;/code&gt;crate，但这将产生更多的依赖和更长的编译时间。）&lt;/p&gt;
&lt;p&gt;现在我们可以导入并实现&lt;code&gt;Stream&lt;/code&gt;trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::{pin::Pin, task::{Poll, Context}};
use futures_util::stream::Stream;

impl Stream for ScancodeStream {
    type Item = u8;

    fn poll_next(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Option&amp;lt;u8&amp;gt;&amp;gt; {
        let queue = SCANCODE_QUEUE.try_get().expect(&quot;not initialized&quot;);
        match queue.pop() {
            Ok(scancode) =&amp;gt; Poll::Ready(Some(scancode)),
            Err(crossbeam_queue::PopError) =&amp;gt; Poll::Pending,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先使用&lt;a href=&quot;https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html#method.try_get&quot;&gt;&lt;code&gt;OnceCell::try_get&lt;/code&gt;&lt;/a&gt;方法来获取初始化的扫描码队列的引用。此操作应该不会失败，因为我们已在&lt;code&gt;new&lt;/code&gt;函数中执行了队列初始化，所以可以放心的使用&lt;code&gt;expect&lt;/code&gt;，以在未初始化队列时直接panic。接下来，我们使用&lt;a href=&quot;https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html#method.pop&quot;&gt;&lt;code&gt;ArrayQueue::pop&lt;/code&gt;&lt;/a&gt;方法尝试从队列中获取下一个元素。如果成功，我们将返回封装在&lt;code&gt;Poll::Ready(Some(…))&lt;/code&gt;中的扫描代码。如果失败，则意味着队列已空。在这种情况下，我们返回&lt;code&gt;Poll::Pending&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;唤醒器支持&lt;/h4&gt;
&lt;p&gt;与&lt;code&gt;Futures::poll&lt;/code&gt;方法类似，&lt;code&gt;Stream::poll_next&lt;/code&gt;方法要求异步任务在返回&lt;code&gt;Poll::Pending&lt;/code&gt;之后才就绪时通知执行器。如此，执行器就不需要再次轮询相同的任务，任务完成会自动通知执行器，这大大降低了等待任务时的性能开销。&lt;/p&gt;
&lt;p&gt;要发送此类通知，任务应从参数&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/task/struct.Context.html&quot;&gt;&lt;code&gt;Context&lt;/code&gt;&lt;/a&gt;引用中获取&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/task/struct.Waker.html&quot;&gt;&lt;code&gt;Waker&lt;/code&gt;&lt;/a&gt;并将其储存在某处。当任务就绪时，它应该在储存的&lt;code&gt;Waker&lt;/code&gt;上调用&lt;a href=&quot;https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.wake&quot;&gt;&lt;code&gt;wake&lt;/code&gt;&lt;/a&gt;方法，以通知执行者应该再次轮询该任务了。&lt;/p&gt;
&lt;h5&gt;原子化的唤醒器&lt;/h5&gt;
&lt;p&gt;要为我们的&lt;code&gt;ScancodeStream&lt;/code&gt;实现&lt;code&gt;Waker&lt;/code&gt;通知功能，就需要一个可以在两次轮询调用之间储存&lt;code&gt;Waker&lt;/code&gt;的地方。不能将其存储为&lt;code&gt;ScancodeStream&lt;/code&gt;的字段中，因为我们需要从&lt;code&gt;add_scancode&lt;/code&gt;函数进行访问。解决方案是使用由&lt;code&gt;Futures-util&lt;/code&gt;crate提供的&lt;a href=&quot;https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html&quot;&gt;&lt;code&gt;AtomicWaker&lt;/code&gt;&lt;/a&gt;类型创建静态变量。与&lt;code&gt;ArrayQueue&lt;/code&gt;类型类似，此类型基于原子化的指令，可以被安全地存放在静态变量中，并支持并发修改。&lt;/p&gt;
&lt;p&gt;定义一个&lt;a href=&quot;https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html&quot;&gt;&lt;code&gt;AtomicWaker&lt;/code&gt;&lt;/a&gt;类型的静态变量&lt;code&gt;WAKER&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use futures_util::task::AtomicWaker;

static WAKER: AtomicWaker = AtomicWaker::new();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路是&lt;code&gt;poll_next&lt;/code&gt;的实现将当前的唤醒器存储在此静态变量中，当将新的扫描码添加到队列时，&lt;code&gt;add_scancode&lt;/code&gt;函数将在其上调用&lt;code&gt;wake&lt;/code&gt;函数。&lt;/p&gt;
&lt;h5&gt;储存唤醒器&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;poll&lt;/code&gt;/&lt;code&gt;poll_next&lt;/code&gt;的协议要求任务在返回&lt;code&gt;Poll::Pending&lt;/code&gt;时为传递的&lt;code&gt;Waker&lt;/code&gt;注册唤醒。让我们修改&lt;code&gt;poll_next&lt;/code&gt;的实现以满足此要求：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Stream for ScancodeStream {
    type Item = u8;

    fn poll_next(self: Pin&amp;lt;&amp;amp;mut Self&amp;gt;, cx: &amp;amp;mut Context) -&amp;gt; Poll&amp;lt;Option&amp;lt;u8&amp;gt;&amp;gt; {
        let queue = SCANCODE_QUEUE
            .try_get()
            .expect(&quot;scancode queue not initialized&quot;);

        // fast path
        if let Ok(scancode) = queue.pop() {
            return Poll::Ready(Some(scancode));
        }

        WAKER.register(&amp;amp;cx.waker());
        match queue.pop() {
            Ok(scancode) =&amp;gt; {
                WAKER.take();
                Poll::Ready(Some(scancode))
            }
            Err(crossbeam_queue::PopError) =&amp;gt; Poll::Pending,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;像以前一样，我们首先使用&lt;a href=&quot;https://docs.rs/conquer-once/0.2.0/conquer_once/raw/struct.OnceCell.html#method.try_get&quot;&gt;&lt;code&gt;OnceCell::try_get&lt;/code&gt;&lt;/a&gt;函数来获取对初始化的扫描码队列的引用。然后，我们乐观地尝试从队列中&lt;code&gt;pop&lt;/code&gt;任务并在成功时返回&lt;code&gt;Poll::Ready&lt;/code&gt;。如此，就可以避免在队列非空时注册唤醒其的性能开销。&lt;/p&gt;
&lt;p&gt;如果对&lt;code&gt;queue.pop()&lt;/code&gt;的第一次调用并未成功，则该队列有可能为空。仅仅有可能为空，是因为中断处理程序也许是在轮询检查后立即异步填充了队列。由于此竞争条件可能在下一次轮询检查时再次发生，因此我们需要在第二次检查之前在&lt;code&gt;WAKER&lt;/code&gt;静态变量中注册&lt;code&gt;Waker&lt;/code&gt;。这样，尽管在&lt;code&gt;Poll::Pending&lt;/code&gt;返回之前也可能发起唤醒，但是可以保证我们能够收到所有在轮询检查后推送的扫描码。&lt;/p&gt;
&lt;p&gt;使用&lt;a href=&quot;https://docs.rs/futures-util/0.3.4/futures_util/task/struct.AtomicWaker.html#method.register&quot;&gt;&lt;code&gt;AtomicWaker::register&lt;/code&gt;&lt;/a&gt;函数注册&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/task/struct.Context.html&quot;&gt;&lt;code&gt;Context&lt;/code&gt;&lt;/a&gt;参数中包含的&lt;code&gt;Waker&lt;/code&gt;之后，我们再次尝试从队列中弹出任务。如果这次能成功，则返回&lt;code&gt;Poll::Ready&lt;/code&gt;。此时还需要使用&lt;a href=&quot;https://docs.rs/futures/0.3.4/futures/task/struct.AtomicWaker.html#method.take&quot;&gt;&lt;code&gt;AtomicWaker::take&lt;/code&gt;&lt;/a&gt;删除这个注册的唤醒器，因为任务已完成就不再需要唤醒器通知了。如果&lt;code&gt;queue.pop()&lt;/code&gt;又失败了，我们仍将像以前一样返回&lt;code&gt;Poll::Pending&lt;/code&gt;，区别是这次注册了唤醒器。&lt;/p&gt;
&lt;p&gt;请注意，对于没有（或尚未）返回&lt;code&gt;Poll::Pending&lt;/code&gt;的任务，可以通过两种方式进行唤醒。一种方法是在唤醒立刻发生于返回&lt;code&gt;Poll::Pending&lt;/code&gt;之前，那么就使用上面提到的竞争条件。另一种方法是由于注册了唤醒器使得队列不再为空时，返回&lt;code&gt;Poll::Ready&lt;/code&gt;。由于这些假唤醒是无法避免的，因此执行器必须能够正确处理它们。&lt;/p&gt;
&lt;h5&gt;唤醒储存的唤醒器&lt;/h5&gt;
&lt;p&gt;为了唤醒存储的&lt;code&gt;Waker&lt;/code&gt;，我们在&lt;code&gt;add_scancode&lt;/code&gt;函数中添加对&lt;code&gt;WAKER.wake()&lt;/code&gt;的调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub(crate) fn add_scancode(scancode: u8) {
    if let Ok(queue) = SCANCODE_QUEUE.try_get() {
        if let Err(_) = queue.push(scancode) {
            println!(&quot;WARNING: scancode queue full; dropping keyboard input&quot;);
        } else {
            WAKER.wake(); // new
        }
    } else {
        println!(&quot;WARNING: scancode queue uninitialized&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处的唯一修改就是，如果成功推送到扫描码队列，就调用&lt;code&gt;WAKER.wake()&lt;/code&gt;。如果静态变量&lt;code&gt;WAKER&lt;/code&gt;中已经注册了一个唤醒器，则此方法将在其上调用同名的&lt;a href=&quot;https://doc.rust-lang.org/stable/core/task/struct.Waker.html#method.wake&quot;&gt;&lt;code&gt;wake&lt;/code&gt;&lt;/a&gt;方法，以通知执行器。否则，该操作将为空操作，即没有任何效果。&lt;/p&gt;
&lt;p&gt;重要的是，我们必须在推送到队列后才能调用&lt;code&gt;wake&lt;/code&gt;，否则在队列仍然为空时可能会过早唤醒任务。一个可能的例子，就是一个多线程执行器在CPU的其他核心中并发的唤醒任务时。虽然现在内核还不支持线程，但也会尽快添加，我们当然不希望那时再出问题。&lt;/p&gt;
&lt;h4&gt;键盘任务&lt;/h4&gt;
&lt;p&gt;我们为&lt;code&gt;ScancodeStream&lt;/code&gt;实现了&lt;code&gt;Stream&lt;/code&gt;trait，现在就可以创建异步键盘任务了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use futures_util::stream::StreamExt;
use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};
use crate::print;

pub async fn print_keypresses() {
    let mut scancodes = ScancodeStream::new();
    let mut keyboard = Keyboard::new(layouts::Us104Key, ScancodeSet1,
        HandleControl::Ignore);

    while let Some(scancode) = scancodes.next().await {
        if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
            if let Some(key) = keyboard.process_keyevent(key_event) {
                match key {
                    DecodedKey::Unicode(character) =&amp;gt; print!(&quot;{}&quot;, character),
                    DecodedKey::RawKey(key) =&amp;gt; print!(&quot;{:?}&quot;, key),
                }
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码与本文修改之前在&lt;a href=&quot;https://os.phil-opp.com/hardware-interrupts/#interpreting-the-scancodes&quot;&gt;键盘中断处理程序&lt;/a&gt;中使用的代码非常相似。唯一的区别是，我们不是从I/O端口读取扫描码，而是从&lt;code&gt;ScancodeStream&lt;/code&gt;中获取。为此，我们首先创建一个新的&lt;code&gt;Scancode&lt;/code&gt;流，然后重复使用&lt;code&gt;StreamExt&lt;/code&gt;trait所提供的&lt;code&gt;next&lt;/code&gt;方法来获取&lt;code&gt;Future&lt;/code&gt;，即为该流中的下一个元素。通过在其上使用&lt;code&gt;await&lt;/code&gt;运算符，我们可以异步等待future的结果。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;while let&lt;/code&gt;循环，直到流返回&lt;code&gt;None&lt;/code&gt;表示结束为止。由于我们的&lt;code&gt;poll_next&lt;/code&gt;方法从不返回&lt;code&gt;None&lt;/code&gt;，因此这实际上是一个无休止的循环，即&lt;code&gt;print_keypresses&lt;/code&gt;任务永远不会完成。&lt;/p&gt;
&lt;p&gt;让我们在&lt;code&gt;main.rs&lt;/code&gt;中的执行器中添加&lt;code&gt;print_keypresses&lt;/code&gt;任务，以再次获得有效的键盘输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use blog_os::task::keyboard; // new

fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {

    // […] initialization routines, including init_heap, test_main

    let mut executor = SimpleExecutor::new();
    executor.spawn(Task::new(example_task()));
    executor.spawn(Task::new(keyboard::print_keypresses())); // new
    executor.run();

    // […] &quot;it did not crash&quot; message, hlt_loop
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在执行&lt;code&gt;cargo run&lt;/code&gt;，将看到键盘输入再次起作用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/qemu-keyboard-output.gif&quot; alt=&quot;QEMU键盘输出&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果你密切注意计算机的CPU使用率，就会发现&lt;code&gt;QEMU&lt;/code&gt;进程会令CPU持续繁忙。发生这种情况是因为我们的&lt;code&gt;SimpleExecutor&lt;/code&gt;在一个循环中不停地轮询任务。因此，即使我们没有按键盘上的任何键，执行器也会在&lt;code&gt;print_keypresses&lt;/code&gt;任务上不停的调用&lt;code&gt;poll&lt;/code&gt;，即使该任务并未取得任何进展而每次都会返回&lt;code&gt;Poll::Pending&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;带有唤醒器支持的执行器&lt;/h3&gt;
&lt;p&gt;要解决性能问题，我们需要创建一个能够正确利用&lt;code&gt;Waker&lt;/code&gt;通知的执行器。这样，当下一个键盘中断发生时，将通知执行器，因此就不需要再不停的轮询&lt;code&gt;print_keypresses&lt;/code&gt;任务了。&lt;/p&gt;
&lt;h4&gt;任务ID&lt;/h4&gt;
&lt;p&gt;要创建能够正确支持唤醒器通知的执行器，第一步就是为每个任务分配唯一的ID。这是必需的，因为我们需要一种方法来指定希望唤醒的任务。首先，创建一个新的&lt;code&gt;TaskId&lt;/code&gt;封装类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct TaskId(u64);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;TaskId&lt;/code&gt;结构体其实是对&lt;code&gt;u64&lt;/code&gt;的简单封装。我们为它派生了许多trait，使其可打印、复制、比较、排序。其中可排序很重要，因为我们接下来将使用&lt;code&gt;TaskId&lt;/code&gt;作为&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html&quot;&gt;&lt;code&gt;BTreeMap&lt;/code&gt;&lt;/a&gt;的键。&lt;/p&gt;
&lt;p&gt;要生成一个新唯一ID，需要创建一个&lt;code&gt;TaskID::new&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::sync::atomic::{AtomicU64, Ordering};

impl TaskId {
    fn new() -&amp;gt; Self {
        static NEXT_ID: AtomicU64 = AtomicU64::new(0);
        TaskId(NEXT_ID.fetch_add(1, Ordering::Relaxed))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数使用&lt;code&gt;AtomicU64&lt;/code&gt;类型的静态变量&lt;code&gt;NEXT_ID&lt;/code&gt;来确保每个ID仅分配一次。&lt;code&gt;fetch_add&lt;/code&gt;方法以原子化的方式自增，并在同一个原子操作中返回上一个值。这意味着即使并行调用&lt;code&gt;TaskId::new&lt;/code&gt;方法，每个ID也会返回一次。&lt;code&gt;Ordering&lt;/code&gt;参数定义是否允许编译器在指令流中对&lt;code&gt;fetch_add&lt;/code&gt;操作重新排序。因为我们只要求ID是唯一的，所以在这种情况下，使用最弱的&lt;code&gt;Relaxed&lt;/code&gt;排序就足够了。&lt;/p&gt;
&lt;p&gt;现在，我们可以使用附加的&lt;code&gt;id&lt;/code&gt;字段来扩展&lt;code&gt;Task&lt;/code&gt;类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct Task {
    id: TaskId, // new
    future: Pin&amp;lt;Box&amp;lt;dyn Future&amp;lt;Output = ()&amp;gt;&amp;gt;&amp;gt;,
}

impl Task {
    pub fn new(future: impl Future&amp;lt;Output = ()&amp;gt; + &apos;static) -&amp;gt; Task {
        Task {
            id: TaskId::new(), // new
            future: Box::pin(future),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新的&lt;code&gt;id&lt;/code&gt;字段使唯一地命名任务成为可能，这是唤醒指定任务所必需的。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;Executor&lt;/code&gt;类型&lt;/h4&gt;
&lt;p&gt;我们在&lt;code&gt;task::executor&lt;/code&gt;模块中创建新的&lt;code&gt;Executor&lt;/code&gt;类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod executor;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use super::{Task, TaskId};
use alloc::{collections::BTreeMap, sync::Arc};
use core::task::Waker;
use crossbeam_queue::ArrayQueue;

pub struct Executor {
    tasks: BTreeMap&amp;lt;TaskId, Task&amp;gt;,
    task_queue: Arc&amp;lt;ArrayQueue&amp;lt;TaskId&amp;gt;&amp;gt;,
    waker_cache: BTreeMap&amp;lt;TaskId, Waker&amp;gt;,
}

impl Executor {
    pub fn new() -&amp;gt; Self {
        Executor {
            tasks: BTreeMap::new(),
            task_queue: Arc::new(ArrayQueue::new(100)),
            waker_cache: BTreeMap::new(),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们没有像在&lt;code&gt;SimpleExecutor&lt;/code&gt;中那样将任务存储在&lt;a href=&quot;https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html&quot;&gt;&lt;code&gt;VecDeque&lt;/code&gt;&lt;/a&gt;中，而是存放在一个包含任务ID的&lt;code&gt;task_queue&lt;/code&gt;字段和包含实际&lt;code&gt;Task&lt;/code&gt;实例的&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html&quot;&gt;&lt;code&gt;BTreeMap&lt;/code&gt;&lt;/a&gt;类型的&lt;code&gt;tasks&lt;/code&gt;字段中。该映射由&lt;code&gt;TaskId&lt;/code&gt;索引，以允许有效地恢复执行特定任务。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;task_queue&lt;/code&gt;字段是有任务ID组成的&lt;a href=&quot;https://docs.rs/crossbeam/0.7.3/crossbeam/queue/struct.ArrayQueue.html&quot;&gt;&lt;code&gt;ArrayQueue&lt;/code&gt;&lt;/a&gt;，并封装为实现了&lt;em&gt;引用计数&lt;/em&gt;的&lt;a href=&quot;https://doc.rust-lang.org/stable/alloc/sync/struct.Arc.html&quot;&gt;&lt;code&gt;Arc&lt;/code&gt;&lt;/a&gt;类型。引用计数使得在多个所有者之间共享变量所有权成为可能。它在堆上分配变量，并计算对变量的活动引用数。当活动引用数达到零时，即不再需要该值，因此会将其释放。&lt;/p&gt;
&lt;p&gt;我们将这种&lt;code&gt;Arc&amp;lt;ArrayQueue&amp;gt;&lt;/code&gt;类型用于&lt;code&gt;task_queue&lt;/code&gt;，以使其能够被执行者和唤醒者共享。这个思路是唤醒者将已唤醒任务的ID推送到队列中。而执行器位于队列的接收端，从&lt;code&gt;task&lt;/code&gt;映射中按其ID取回已唤醒的任务，并运行任务。使用固定大小的队列而不是无限制队列（如&lt;a href=&quot;https://docs.rs/crossbeam-queue/0.2.1/crossbeam_queue/struct.SegQueue.html&quot;&gt;&lt;code&gt;SegQueue&lt;/code&gt;&lt;/a&gt;）的原因是，不应在中断处理程序中进行堆分配，将推送到此队列。&lt;/p&gt;
&lt;p&gt;除了&lt;code&gt;task_queue&lt;/code&gt;和&lt;code&gt;tasks&lt;/code&gt;映射外，&lt;code&gt;Executor&lt;/code&gt;类型还具有一个&lt;code&gt;waker_cache&lt;/code&gt;字段，它也是一个映射。在创建任务后，此映射将缓存任务的&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/task/struct.Waker.html&quot;&gt;&lt;code&gt;Waker&lt;/code&gt;&lt;/a&gt;。首先，可以使用同一个唤醒器对同一个任务做多次唤醒操作，这相较于每次唤醒均创建一个唤醒器而言更高效。其次，它确保唤醒器的引用计数不会在中断处理程序内被释放，而这可能会导致死锁（有关此问题的更多信息，请参见下文）。&lt;/p&gt;
&lt;p&gt;要创建&lt;code&gt;Executor&lt;/code&gt;，我们可以编写一个简单的&lt;code&gt;new&lt;/code&gt;函数。我们为&lt;code&gt;task_queue&lt;/code&gt;选择100的容量，这对可预见的未来而言已经足够了。如果我们的系统在某个时刻真的保有超过100个并发任务，也可以轻松地修改这个值。&lt;/p&gt;
&lt;h4&gt;生成任务&lt;/h4&gt;
&lt;p&gt;与&lt;code&gt;SimpleExecutor&lt;/code&gt;类似，我们也在&lt;code&gt;Executor&lt;/code&gt;类型上编写一个&lt;code&gt;spawn&lt;/code&gt;方法，该方法将给定任务添加到&lt;code&gt;tasks&lt;/code&gt;映射，并通过将其ID推送到&lt;code&gt;task_queue&lt;/code&gt;中来立刻执行唤醒：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Executor {
    pub fn spawn(&amp;amp;mut self, task: Task) {
        let task_id = task.id;
        if self.tasks.insert(task.id, task).is_some() {
            panic!(&quot;task with same ID already in tasks&quot;);
        }
        self.task_queue.push(task_id).expect(&quot;queue full&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果映射中已经存在具有相同ID的任务，则&lt;code&gt;BTreeMap::insert&lt;/code&gt;方法将直接返回它。由于每个任务都有唯一的ID，理论上不应发生这种情况，因此若出现这种情况我们就产生panic，表明代码中存在错误。同样，当&lt;code&gt;task_queue&lt;/code&gt;填满时也会产生panic，因为如果设置的队列大小合适，就不应该发生这种情况。&lt;/p&gt;
&lt;h4&gt;运行任务&lt;/h4&gt;
&lt;p&gt;要执行&lt;code&gt;task_queue&lt;/code&gt;中的所有任务，我们创建一个私有的&lt;code&gt;run_ready_tasks&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::task::{Context, Poll};

impl Executor {
    fn run_ready_tasks(&amp;amp;mut self) {
        // 解构`self`以避免借用检查出错
        let Self {
            tasks,
            task_queue,
            waker_cache,
        } = self;

        while let Ok(task_id) = task_queue.pop() {
            let task = match tasks.get_mut(&amp;amp;task_id) {
                Some(task) =&amp;gt; task,
                None =&amp;gt; continue, // 任务已不存在
            };
            let waker = waker_cache
                .entry(task_id)
                .or_insert_with(|| TaskWaker::new(task_id, task_queue.clone()));
            let mut context = Context::from_waker(waker);
            match task.poll(&amp;amp;mut context) {
                Poll::Ready(()) =&amp;gt; {
                    // 任务完成 -&amp;gt; 删除任务及其缓存唤醒器
                    tasks.remove(&amp;amp;task_id);
                    waker_cache.remove(&amp;amp;task_id);
                }
                Poll::Pending =&amp;gt; {}
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数的基本思路与&lt;code&gt;SimpleExecutor&lt;/code&gt;类似：遍历&lt;code&gt;task_queue&lt;/code&gt;中的任务，为每个任务创建唤醒器，并轮询该任务。但我们并不直接将待处理的任务添加到&lt;code&gt;task_queue&lt;/code&gt;的队尾，而是让&lt;code&gt;TaskWaker&lt;/code&gt;负责将唤醒的任务添加回队列。稍后将显示该唤醒器类型的实现。&lt;/p&gt;
&lt;p&gt;让我们研究一下&lt;code&gt;run_ready_tasks&lt;/code&gt;方法的一些实现细节：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们将&lt;code&gt;self&lt;/code&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/ch18-03-pattern-syntax.html#destructuring-to-break-apart-values&quot;&gt;解构&lt;/a&gt;为三个字段，以避免某些借用检查器错误。即我们的实现需要从闭包内部访问&lt;code&gt;self.task_queue&lt;/code&gt;，而目前的闭包会尝试直接借用整个&lt;code&gt;self&lt;/code&gt;。这本质上是借阅检查器自己的问题，且将在&lt;a href=&quot;https://github.com/rust-lang/rust/issues/53488&quot;&gt;实现&lt;/a&gt;&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/2229&quot;&gt;RFC 2229&lt;/a&gt;时解决。&lt;/li&gt;
&lt;li&gt;对于每个弹出的任务ID，我们从&lt;code&gt;tasks&lt;/code&gt;映射中取出其对应任务的可变引用。由于我们的&lt;code&gt;ScancodeStream&lt;/code&gt;会在检查任务是否需要休眠之前就注册唤醒器，因此可能会发生为已经不存在的任务执行唤醒的情况。在这种情况下，我们只需忽略唤醒并继续执行队列中的下一个ID即可。&lt;/li&gt;
&lt;li&gt;为了避免在每个轮询中新建唤醒器所带来的性能开销，我们在每个任务创建后将其唤醒器缓存到&lt;code&gt;waker_cache&lt;/code&gt;映射中。为此，我们结合使用&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.entry&quot;&gt;&lt;code&gt;BTreeMap::entry&lt;/code&gt;&lt;/a&gt;方法和&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/btree_map/enum.Entry.html#method.or_insert_with&quot;&gt;&lt;code&gt;Entry::or_insert_with&lt;/code&gt;&lt;/a&gt;，若唤醒器尚不存在则新建一个，再获得其的可变引用。为了创建新的唤醒器，我们克隆了&lt;code&gt;task_queue&lt;/code&gt;并将其与任务ID一起传给&lt;code&gt;TaskWaker::new&lt;/code&gt;函数（其实现见下文）。由于&lt;code&gt;task_queue&lt;/code&gt;封装在&lt;code&gt;Arc&lt;/code&gt;中，因此&lt;code&gt;clone&lt;/code&gt;只会增加其引用计数，而仍指向同一堆分配的队列。请注意，并非所有唤醒器的实现都可以进行这种重用，但是我们的&lt;code&gt;TaskWaker&lt;/code&gt;实现是允许重复的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任务完成时将返回&lt;code&gt;Poll::Ready&lt;/code&gt;。在这种情况下，我们使用&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html#method.remove&quot;&gt;&lt;code&gt;BTreeMap::remove&lt;/code&gt;&lt;/a&gt;方法从&lt;code&gt;tasks&lt;/code&gt;映射中删除该任务。如果该任务还有缓存的唤醒器，我们也会将其移除。&lt;/p&gt;
&lt;h4&gt;唤醒器设计&lt;/h4&gt;
&lt;p&gt;唤醒器用于将已唤醒的任务ID推送到执行器的&lt;code&gt;task_queue&lt;/code&gt;。我们创建一个新的&lt;code&gt;TaskWaker&lt;/code&gt;结构体，以存储任务ID和一个&lt;code&gt;task_queue&lt;/code&gt;引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct TaskWaker {
    task_id: TaskId,
    task_queue: Arc&amp;lt;ArrayQueue&amp;lt;TaskId&amp;gt;&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;task_queue&lt;/code&gt;的所有权由执行器和唤醒器共享，因此我们使用&lt;code&gt;Arc&lt;/code&gt;封装类型来实现共享所有权的引用计数。&lt;/p&gt;
&lt;p&gt;唤醒操作的实现非常简单：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl TaskWaker {
    fn wake_task(&amp;amp;self) {
        self.task_queue.push(self.task_id).expect(&quot;task_queue full&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将&lt;code&gt;task_id&lt;/code&gt;推送到引用的&lt;code&gt;task_queue&lt;/code&gt;中。由于对&lt;code&gt;ArrayQueue&lt;/code&gt;类型的修改仅需要共享的引用，因此我们都不需要&lt;code&gt;&amp;amp;mut self&lt;/code&gt;，仅用&lt;code&gt;&amp;amp;self&lt;/code&gt;即可实现。&lt;/p&gt;
&lt;h5&gt;&lt;code&gt;Wake&lt;/code&gt;trait&lt;/h5&gt;
&lt;p&gt;为了能够使用&lt;code&gt;TaskWaker&lt;/code&gt;类型轮询future，我们首先要将其转为&lt;code&gt;Waker&lt;/code&gt;实例。这是必需的，因为&lt;code&gt;Future::poll&lt;/code&gt;方法使用&lt;code&gt;Context&lt;/code&gt;的实例作参数，而该实例只能基于&lt;code&gt;Waker&lt;/code&gt;类型构造。尽管确实可以通过提供一个&lt;code&gt;RawWaker&lt;/code&gt;实现来做到这一点，但是使用基于&lt;code&gt;Arc&lt;/code&gt;的&lt;code&gt;Wake&lt;/code&gt;trait，然后再用标准库提供的&lt;code&gt;From&lt;/code&gt;trait的实现来构造&lt;code&gt;Waker&lt;/code&gt;，将会更简单且安全。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Wake&lt;/code&gt;Trait实现如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use alloc::task::Wake;

impl Wake for TaskWaker {
    fn wake(self: Arc&amp;lt;Self&amp;gt;) {
        self.wake_task();
    }

    fn wake_by_ref(self: &amp;amp;Arc&amp;lt;Self&amp;gt;) {
        self.wake_task();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于唤醒器通常在执行器和异步任务之间共享，因此&lt;code&gt;trait&lt;/code&gt;方法要求将&lt;code&gt;Self&lt;/code&gt;的实例封装在&lt;code&gt;Arc&lt;/code&gt;类型中，以实现所有权的引用计数。这意味着我们必须将&lt;code&gt;TaskWaker&lt;/code&gt;放在&lt;code&gt;Arc&lt;/code&gt;中才能进行调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;wake&lt;/code&gt;和&lt;code&gt;wake_by_ref&lt;/code&gt;方法之间的区别在于，后者仅需要&lt;code&gt;Arc&lt;/code&gt;的引用，而前者需要&lt;code&gt;Arc&lt;/code&gt;的所有权，因此通常需要增加引用计数。并非所有类型都支持通过引用调用唤醒，因此方法&lt;code&gt;wake_by_ref&lt;/code&gt;的实现是可选的，不过由于该方法可以避免不必要的引用计数修改，所以性能较高。对我们来说，可以在两个trait方法中都简单地继续调用&lt;code&gt;wake_task&lt;/code&gt;函数，该函数仅需要一个共享的&lt;code&gt;&amp;amp;self&lt;/code&gt;引用。&lt;/p&gt;
&lt;h5&gt;创建唤醒器&lt;/h5&gt;
&lt;p&gt;由于所有&lt;code&gt;Arc&lt;/code&gt;封装的实现了&lt;code&gt;Waker&lt;/code&gt;trait的类型都支持使用&lt;code&gt;From&lt;/code&gt;转换，因此我们现在可以实现&lt;code&gt;Executor::run_ready_tasks&lt;/code&gt;方法所需的&lt;code&gt;TaskWaker::new&lt;/code&gt;函数了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl TaskWaker {
    fn new(task_id: TaskId, task_queue: Arc&amp;lt;ArrayQueue&amp;lt;TaskId&amp;gt;&amp;gt;) -&amp;gt; Waker {
        Waker::from(Arc::new(TaskWaker {
            task_id,
            task_queue,
        }))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用&lt;code&gt;task_id&lt;/code&gt;和&lt;code&gt;task_queue&lt;/code&gt;作为参数创建&lt;code&gt;TaskWaker&lt;/code&gt;。然后，我们将&lt;code&gt;TaskWaker&lt;/code&gt;封装在&lt;code&gt;Arc&lt;/code&gt;中，并使用&lt;code&gt;Waker::from&lt;/code&gt;将其转换为&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/task/struct.Waker.html&quot;&gt;&lt;code&gt;Waker&lt;/code&gt;&lt;/a&gt;。此from方法负责为TaskWaker类型构造&lt;a href=&quot;https://doc.rust-lang.org/stable/core/task/struct.RawWakerVTable.html&quot;&gt;RawWakerVTable&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/stable/core/task/struct.RawWaker.html&quot;&gt;RawWaker&lt;/a&gt;实例。这个&lt;code&gt;from&lt;/code&gt;方法用于&lt;code&gt;TaskWaker&lt;/code&gt;类型构造一个&lt;code&gt;RawWakerVTable&lt;/code&gt;和一个&lt;code&gt;RawWaker&lt;/code&gt;实例。如果你对它的详细工作方式感兴趣，请查看&lt;a href=&quot;https://github.com/rust-lang/rust/blob/cdb50c6f2507319f29104a25765bfb79ad53395c/src/liballoc/task.rs#L58-L87&quot;&gt;&lt;code&gt;alloc&lt;/code&gt;crate中的实现&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;run&lt;/code&gt;方法&lt;/h4&gt;
&lt;p&gt;在实现了唤醒器之后，我们终于可以为执行器添加一个&lt;code&gt;run&lt;/code&gt;方法了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Executor {
    pub fn run(&amp;amp;mut self) -&amp;gt; ! {
        loop {
            self.run_ready_tasks();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此方法仅循环调用&lt;code&gt;run_ready_tasks&lt;/code&gt;函数。理论上，当&lt;code&gt;tasks&lt;/code&gt;映射为空时我们确实可以从令函数返回，不过这并不会发生因为&lt;code&gt;keyboard_task&lt;/code&gt;永远不会完成，因此一个简单的&lt;code&gt;loop&lt;/code&gt;就足够了。鉴于该函数永不返回，因此我们使用返回类型&lt;code&gt;!&lt;/code&gt;以告诉编译器此为&lt;a href=&quot;https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html&quot;&gt;发散&lt;/a&gt;函数。&lt;/p&gt;
&lt;p&gt;现在，我们可以更改&lt;code&gt;kernel_main&lt;/code&gt;，以使用新的&lt;code&gt;Executor&lt;/code&gt;来代替&lt;code&gt;SimpleExecutor&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use blog_os::task::executor::Executor; // new

fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    // […] initialization routines, including init_heap, test_main

    let mut executor = Executor::new(); // new
    executor.spawn(Task::new(example_task()));
    executor.spawn(Task::new(keyboard::print_keypresses()));
    executor.run();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们只需要更改导入类型和类型名称。由于&lt;code&gt;run&lt;/code&gt;是发散函数，编译器知道它永不返回，因此我们不再需要在&lt;code&gt;kernel_main&lt;/code&gt;函数末尾调用&lt;code&gt;hlt_loop&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;当我们现在使用&lt;code&gt;cargo run&lt;/code&gt;运行内核时，将看到键盘输入依然有效：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/async-await/qemu-keyboard-output-again.gif&quot; alt=&quot;QEMU键盘输出&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不过，QEMU的CPU使用率并没有降低。这是因为我们仍然使CPU始终保持忙碌状态。现在虽然在任务唤通知醒前都不再执行轮询，但仍在循环中检查&lt;code&gt;task_queue&lt;/code&gt;。要解决此问题，我们需要在CPU没有其他工作时进入睡眠状态。&lt;/p&gt;
&lt;h4&gt;空闲时睡眠&lt;/h4&gt;
&lt;p&gt;基本思路就是在&lt;code&gt;task_queue&lt;/code&gt;为空时执行&lt;a href=&quot;https://en.wikipedia.org/wiki/HLT_(x86_instruction)&quot;&gt;&lt;code&gt;hlt&lt;/code&gt;指令&lt;/a&gt;。该指令使CPU进入睡眠状态，直到下一个中断到达。CPU在中断后立即会再次激活，这确保了当中断处理程序向&lt;code&gt;task_queue&lt;/code&gt;推送任务时，CPU仍然可以直接做出反应。&lt;/p&gt;
&lt;p&gt;为了实现这一点，我们在执行器中创建一个新的&lt;code&gt;sleep_if_idle&lt;/code&gt;方法，并从&lt;code&gt;run&lt;/code&gt;方法中调用它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Executor {
    pub fn run(&amp;amp;mut self) -&amp;gt; ! {
        loop {
            self.run_ready_tasks();
            self.sleep_if_idle();   // new
        }
    }

    fn sleep_if_idle(&amp;amp;self) {
        if self.task_queue.is_empty() {
            x86_64::instructions::hlt();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于我们在&lt;code&gt;run_ready_tasks&lt;/code&gt;之后直接调用&lt;code&gt;sleep_if_idle&lt;/code&gt;，这将一直循环直到&lt;code&gt;task_queue&lt;/code&gt;为空，因此似乎无需再次检查队列。但是，硬件中断可能紧接着在&lt;code&gt;run_ready_tasks&lt;/code&gt;返回之后发生，因此在调用&lt;code&gt;sleep_if_idle&lt;/code&gt;函数时队列中可能还有一个新任务。仅当队列仍然为空时，我们才通过&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/index.html&quot;&gt;&lt;code&gt;x86_64&lt;/code&gt;&lt;/a&gt;crate提供的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/fn.hlt.html&quot;&gt;&lt;code&gt;instructions::hlt&lt;/code&gt;&lt;/a&gt;函数执行&lt;code&gt;hlt&lt;/code&gt;指令，使CPU进入睡眠状态。&lt;/p&gt;
&lt;p&gt;不幸的是，在此实现中仍然存在微妙的竞争条件。由于中断是异步的且可以随时发生，因此有可能在&lt;code&gt;is_empty&lt;/code&gt;检查和&lt;code&gt;hlt&lt;/code&gt;调用之间发生中断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if self.task_queue.is_empty() {
    /// &amp;lt;--- 这里也可能发生中断
    x86_64::instructions::hlt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;万一此中断推送到&lt;code&gt;task_queue&lt;/code&gt;，即使现在确实有一个就绪的任务，CPU也会进入睡眠状态。在最坏的情况下，这可能会将键盘中断的处理，延迟到下一次按键或下一个定时器中断。那么我们应该如何预防呢？&lt;/p&gt;
&lt;p&gt;答案是在检查之前禁用CPU上的中断，并与&lt;code&gt;hlt&lt;/code&gt;指令一起以原子化操作再启用中断。那么，此间发生的所有中断都将延迟到&lt;code&gt;hlt&lt;/code&gt;指令之后，从而不会丢失任何唤醒。为了实现该方法，我们可以使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/index.html&quot;&gt;&lt;code&gt;x86_64&lt;/code&gt;&lt;/a&gt;crate提供的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html&quot;&gt;&lt;code&gt;interrupts::enable_and_hlt&lt;/code&gt;&lt;/a&gt;函数。&lt;/p&gt;
&lt;p&gt;修改后的&lt;code&gt;sleep_if_idle&lt;/code&gt;函数如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Executor {
    fn sleep_if_idle(&amp;amp;self) {
        use x86_64::instructions::interrupts::{self, enable_and_hlt};

        interrupts::disable();
        if self.task_queue.is_empty() {
            enable_and_hlt();
        } else {
            interrupts::enable();
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了避免竞争条件，我们在检查&lt;code&gt;task_queue&lt;/code&gt;是否为空之前禁用中断。如果队列确实为空，我们将通过一个原子化操作，使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/interrupts/fn.enable_and_hlt.html&quot;&gt;&lt;code&gt;enable_and_hlt&lt;/code&gt;&lt;/a&gt;函数启用中断，同时让CPU进入睡眠。如果队列不再为空，则意味着在返回&lt;code&gt;run_ready_tasks&lt;/code&gt;之后，中断会唤醒任务。在这种情况下，我们将再次启用中断，且不再&lt;code&gt;hlt&lt;/code&gt;而直接继续让程序执行。&lt;/p&gt;
&lt;p&gt;现在，执行器在没有任务的情况下能够正确地让CPU进入睡眠状态。我们可以看到，当再次使用&lt;code&gt;cargo run&lt;/code&gt;运行内核时，QEMU进程的CPU利用率要低得多。&lt;/p&gt;
&lt;h4&gt;可能的扩展&lt;/h4&gt;
&lt;p&gt;我们的执行器现在可以高效地运行任务。它利用唤醒器通知来避免持续轮询等待的任务，并在无任何工作时使CPU进入睡眠状态。但是，我们的执行器仍然非常基础，而且可以通过许多方式来扩展功能：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;调度&lt;/strong&gt;：目前，我们使用&lt;a href=&quot;https://doc.rust-lang.org/stable/alloc/collections/vec_deque/struct.VecDeque.html&quot;&gt;&lt;code&gt;VecDeque&lt;/code&gt;&lt;/a&gt;类型为&lt;code&gt;task_queue&lt;/code&gt;实现&lt;em&gt;先进先出&lt;/em&gt;（FIFO）策略，这通常也称为&lt;em&gt;循环调度&lt;/em&gt;。此策略对于其上的所有任务负载可能并不是最高效的。例如，优先考虑延迟敏感的任务或执行大量I/O的任务可能更加高效。有关更多信息，请参见&lt;a href=&quot;http://pages.cs.wisc.edu/~remzi/OSTEP/&quot;&gt;Operating Systems: Three Easy Pieces&lt;/a&gt;一书中的&lt;a href=&quot;http://pages.cs.wisc.edu/~remzi/OSTEP/cpu-sched.pdf&quot;&gt;scheduling chapter&lt;/a&gt;章节，或&lt;a href=&quot;https://en.wikipedia.org/wiki/Scheduling_(computing)&quot;&gt;Wikipedia上关于调度的文章&lt;/a&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;任务生成&lt;/strong&gt;：我们的&lt;code&gt;Executor::spawn&lt;/code&gt;方法当前需要&lt;code&gt;&amp;amp;mut self&lt;/code&gt;引用，因此在启动&lt;code&gt;run&lt;/code&gt;方法之后将不再可用。为了解决这个问题，我们可以再创建一个额外的&lt;code&gt;Spawner&lt;/code&gt;类型，该类型与执行器共享某种队列，并允许从任务自己创建新任务。例如，队列可以直接使用&lt;code&gt;task_queue&lt;/code&gt;，也可以是一个在执行器中循环检查的单独的队列。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用线程&lt;/strong&gt;：我们尚不支持线程，但会在下一篇文章中添加。这将使我们能够在不同的线程中启动多个执行器实例。这种方法的优点是可以减少长时长任务带来的延迟，因为其他任务可以同时运行。这种方法还可以利用多个CPU内核。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负载均衡&lt;/strong&gt;：添加线程支持时，如何在执行器间分配任务，以确保所有CPU核心都能够被用到将变得很重要。常见的技术是&lt;a href=&quot;https://en.wikipedia.org/wiki/Work_stealing&quot;&gt;&lt;em&gt;work stealing&lt;/em&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;在这篇文章的开始，我们介绍了&lt;strong&gt;多任务&lt;/strong&gt;，并区分了抢占式多任务（强制性地定期中断正在运行的任务）和协作式多任务（使任务一直运行到自愿放弃对CPU的控制）之间的区别。&lt;/p&gt;
&lt;p&gt;然后，我们探讨了Rust的&lt;strong&gt;async/await&lt;/strong&gt;如何提供协作式多任务的语言级实现。Rust的实现基于基于轮询的&lt;code&gt;Future&lt;/code&gt;trait，该特质抽象了异步任务。使用async/await，就可以像编写普通同步代码一样处理future。不同之处在于异步函数会再次返回&lt;code&gt;Future&lt;/code&gt;，为了运行它，需要在某个时刻将其添加到执行器中。&lt;/p&gt;
&lt;p&gt;在后台，编译器将async/await代码转换为&lt;em&gt;状态机&lt;/em&gt;，每个&lt;code&gt;.await&lt;/code&gt;操作对应一个可能的暂停点。通过利用程序对自身执行步骤的了解，编译器就能够为每个暂停点保存最小化的状态信息，从而使每个任务的内存消耗极小。而其中的一个挑战是，生成的状态机可能包含&lt;em&gt;自引用&lt;/em&gt;结构体，例如，当异步函数的局部变量相互引用时。为了防止指针失效，Rust使用&lt;code&gt;Pin&lt;/code&gt;类型来确保future在被第一次轮询后就不再能够在内存中移动了。&lt;/p&gt;
&lt;p&gt;接下来介绍了如何实现，我们首先创建了一个非常简单的执行程序，它会在一个循环中持续轮询所有生成的任务，且完全不使用&lt;code&gt;Waker&lt;/code&gt;类型。然后，我们通过实现异步键盘任务来展示了唤醒通知的优点。该任务使用&lt;code&gt;crossbeam&lt;/code&gt;crate提供的无互斥的&lt;code&gt;ArrayQueue&lt;/code&gt;类型定义了一个静态变量&lt;code&gt;SCANCODE_QUEUE&lt;/code&gt;。现在，键盘中断处理程序不会再直接处理按键，而会将所有接收到的扫描代码放入队列中，然后唤醒已注册的&lt;code&gt;Waker&lt;/code&gt;，以发送信号表示有新的输入可用。在接收端，我们创建了一个&lt;code&gt;ScancodeStream&lt;/code&gt;类型，以提供一个用于获取队列中下一个&lt;code&gt;scancode&lt;/code&gt;的&lt;code&gt;Future&lt;/code&gt;。这样就可以创建一个异步&lt;code&gt;print_keypresses&lt;/code&gt;任务，以使用&lt;code&gt;async/await&lt;/code&gt;来解释并打印队列中的扫描码。&lt;/p&gt;
&lt;p&gt;为了利用键盘任务的唤醒通知，我们创建了一个新的&lt;code&gt;Executor&lt;/code&gt;类型，该类型使用&lt;code&gt;Arc&lt;/code&gt;封装&lt;code&gt;task_queue&lt;/code&gt;，以使得就绪任务可共享。我们实现了一个&lt;code&gt;TaskWaker&lt;/code&gt;类型，以将唤醒的任务的ID直接推送到&lt;code&gt;task_queue&lt;/code&gt;，然后使用执行器进行轮询。为了在空闲时时节省CPU，我们利用&lt;code&gt;hlt&lt;/code&gt;指令添加了对CPU在空闲时睡眠的支持。最后，我们讨论了执行器的一些潜在扩展，例如，提供多核支持。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;我们利用async/await使内核具备了对协作多任务的基本支持。尽管协作式多任务处理非常有效，但是当单个任务运行太长时间并导致其他任务无法运行时，系统就会出现延迟。因此，在我们的内核中添加对抢占式多任务的支持也是合理的。&lt;/p&gt;
&lt;p&gt;在下一篇文章中，我们将介绍抢占式多任务处理的最常见形式，&lt;em&gt;线程&lt;/em&gt;。除了解决长时长任务的问题，线程还能够利用多个CPU核心，并支持运行不受信任的用户程序。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 3.4 - 内存分配器设计</title><link>https://zlotus.github.io/2021/04/06/writing-an-os-in-rust-3.4/</link><guid isPermaLink="true">https://zlotus.github.io/2021/04/06/writing-an-os-in-rust-3.4/</guid><description>使用Rust编写操作系统 - 3.4 - 内存分配器设计</description><pubDate>Tue, 06 Apr 2021 19:02:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/allocator-designs/&quot;&gt;Allocator Designs&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;本文将展示如何从零编写堆分配器，并讨论不同的分配器设计，包括线性分配，链表分配和固定大小块分配。我们将为这三种分配器分别创建一个可用于内核的基本实现。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/allocator-designs/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-11&quot;&gt;post-11&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;在&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/&quot;&gt;上一篇文章&lt;/a&gt;中，我们向内核添加了对堆分配的基本支持。为此，我们在页表中&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/#creating-a-kernel-heap&quot;&gt;创建了一个新的内存区域&lt;/a&gt;，并&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/#using-an-allocator-crate&quot;&gt;使用&lt;code&gt;linked_list_allocator&lt;/code&gt;crate&lt;/a&gt;来管理该内存。现在我们有了一个工作堆，但大部分工作留给了该分配器crate，并没有去了解分配器是如何工作的。&lt;/p&gt;
&lt;p&gt;在这篇文章中，我们将展示如何从头开始创建堆分配器，而不是依赖现有的分配器crate。我们还将讨论不同的分配器设计，包括简单的&lt;em&gt;线性分配器&lt;/em&gt;和基本的&lt;em&gt;固定大小块分配器&lt;/em&gt;，并使用此知识来实现性能提高的分配器（与&lt;code&gt;linked_list_allocator&lt;/code&gt;crate相比）。&lt;/p&gt;
&lt;h3&gt;设计目标&lt;/h3&gt;
&lt;p&gt;分配器的主要任务是管理可用的堆内存，它需要在&lt;code&gt;alloc&lt;/code&gt;调用中返回未使用的内存，并跟踪用&lt;code&gt;dealloc&lt;/code&gt;释放的内存，以便实现内存重用。最重要的是，它绝不能分配已经在其他地方使用的内存，因为这将会导致未定义的行为。&lt;/p&gt;
&lt;p&gt;除了确保正确性之外，还有许多次要任务。例如，分配器应有效地利用可用内存并减少内存&lt;a href=&quot;https://en.wikipedia.org/wiki/Fragmentation_(computing)&quot;&gt;碎片&lt;/a&gt;。此外，它应该能够很好的用于并发应用程序，并可以扩展到任意数量的处理器。为了获得最佳性能，它甚至可以针对CPU缓存优化内存布局，以提高&lt;a href=&quot;https://www.geeksforgeeks.org/locality-of-reference-and-cache-operation-in-cache-memory/&quot;&gt;缓存局部性&lt;/a&gt;并避免&lt;a href=&quot;https://mechanical-sympathy.blogspot.de/2011/07/false-sharing.html&quot;&gt;伪共享&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这些要求会使好的分配器非常复杂。比如&lt;a href=&quot;http://jemalloc.net/&quot;&gt;jemalloc&lt;/a&gt;具有超过30.000行代码。不过，我们通常并不希望在内核中使用如此复杂的代码，因为在内核中一个bug就会导致严重的安全漏洞。幸运的是，与用户空间代码相比，内核代码的分配模式通常要简单得多，因此使用较为简单的分配器设计通常就足够了。&lt;/p&gt;
&lt;p&gt;在下面的内容中，我们将介绍三种可能的内核分配器设计，并说明它们的优缺点。&lt;/p&gt;
&lt;h2&gt;线性分配器&lt;/h2&gt;
&lt;p&gt;最简单的分配器设计是&lt;em&gt;线性分配器&lt;/em&gt;（也称为&lt;em&gt;栈分配器&lt;/em&gt;）。它线性的分配内存，并且仅跟踪分配的字节数和分配数。线性分配器仅在非常特定的用例中使用，因为它有一个严格的限制：只能一次释放所有内存。&lt;/p&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;p&gt;线性分配器的思路是通过增加（“肿起来”）一个&lt;code&gt;next&lt;/code&gt;变量来线性分配内存，该变量指向未使用内存的起点。一开始，&lt;code&gt;next&lt;/code&gt;就是堆的起始地址。每次分配时，&lt;code&gt;next&lt;/code&gt;都会增加，因此它始终指向已用内存和未用内存之间的边界：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/allocator-designs/bump-allocation.svg&quot; alt=&quot;线性分配&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;next&lt;/code&gt;指针只会向前单方向移动，因此该方案永远不会将相同的存储区域分配出去两次。当指针到达堆尾时，已经没有更多的内存可供分配，因此下一次分配将引发一个内存不足错误。&lt;/p&gt;
&lt;p&gt;线性分配器通常通过分配计数器实现，分配计数器在每次&lt;code&gt;alloc&lt;/code&gt;调用时加1，在每次&lt;code&gt;dealloc&lt;/code&gt;调用时减1。当分配计数器为零时，表示堆上的所有分配都已释放。在这种情况下，可以将&lt;code&gt;next&lt;/code&gt;指针重置为堆的起始地址，以便再次分配完整的堆内存。&lt;/p&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;p&gt;我们通过声明一个新的&lt;code&gt;allocator::bump&lt;/code&gt;子模块开始我们的实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod bump;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;子模块的代码位于新的&lt;code&gt;src/allocator/bump.rs&lt;/code&gt;文件中，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct BumpAllocator {
    heap_start: usize,
    heap_end: usize,
    next: usize,
    allocations: usize,
}

impl BumpAllocator {
    /// 创建一个空的线性分配器
    pub const fn new() -&amp;gt; Self {
        BumpAllocator {
            heap_start: 0,
            heap_end: 0,
            next: 0,
            allocations: 0,
        }
    }
    
    /// 使用给定的堆边界初始化线性分配器
    /// 
    /// 该方法为非安全，因为调用者必须保证提供的内存范围未被使用。
    /// 同时，该方法只能被调用一次。
    pub unsafe fn init(&amp;amp;mut self, heap_start: usize, heap_size: usize) {
        self.heap_start = heap_start;
        self.heap_end = heap_start + heap_size;
        self.next = heap_start;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;heap_start&lt;/code&gt;和&lt;code&gt;heap_end&lt;/code&gt;字段为堆内存区域的下限和上限。调用者需要确保这些地址有效，否则分配器将返回无效的内存。因此，&lt;code&gt;init&lt;/code&gt;函数需要被标记为&lt;code&gt;unsafe&lt;/code&gt;调用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;next&lt;/code&gt;字段将始终指向堆内存中的首个未使用字节，即下一次分配的起始地址。在初始化函数中将其设置为&lt;code&gt;heap_start&lt;/code&gt;，因为在一开始整个堆都是未使用状态。在每次分配时，该字段都会增加分配出去的大小（“肿起来”），以确保我们不会两次返回相同的内存区域。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;allocations&lt;/code&gt;字段是活动中已分配内存的简单计数器，目标是在释放最后一块已分配内存后重置分配器。初始化为0。&lt;/p&gt;
&lt;p&gt;选择创建一个单独的&lt;code&gt;init&lt;/code&gt;函数，而不是在&lt;code&gt;new&lt;/code&gt;函数中直接执行初始化，是为了是我们的分配器接口与&lt;code&gt;linked_list_allocator&lt;/code&gt;crate提供的分配器接口一致。这样，无需更改其他代码即可切换分配器。&lt;/p&gt;
&lt;h4&gt;为线性分配器实现&lt;code&gt;GlobalAlloc&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;如&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/#the-allocator-interface&quot;&gt;上一篇文章所述&lt;/a&gt;，所有堆分配器都需要实现&lt;code&gt;GlobalAlloc&lt;/code&gt;trait，其定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub unsafe trait GlobalAlloc {
    unsafe fn alloc(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8;
    unsafe fn dealloc(&amp;amp;self, ptr: *mut u8, layout: Layout);

    unsafe fn alloc_zeroed(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8 { ... }
    unsafe fn realloc(
        &amp;amp;self,
        ptr: *mut u8,
        layout: Layout,
        new_size: usize
    ) -&amp;gt; *mut u8 { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅需要实现&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;方法，其他两个方法均具有默认实现，我们在实现时可以忽略。&lt;/p&gt;
&lt;h4&gt;首次实现尝试&lt;/h4&gt;
&lt;p&gt;让我们试着为&lt;code&gt;BumpAllocator&lt;/code&gt;实现&lt;code&gt;alloc&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use alloc::alloc::{GlobalAlloc, Layout};

unsafe impl GlobalAlloc for BumpAllocator {
    unsafe fn alloc(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8 {
        // TODO 对齐及边界检查
        let alloc_start = self.next;
        self.next = alloc_start + layout.size();
        self.allocations += 1;
        alloc_start as *mut u8
    }

    unsafe fn dealloc(&amp;amp;self, _ptr: *mut u8, _layout: Layout) {
        todo!();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先将&lt;code&gt;next&lt;/code&gt;字段用作分配的起始地址。然后更新&lt;code&gt;next&lt;/code&gt;字段，使其指向分配的结束地址，即堆上的下一个未使用的地址。最后，在将分配的起始地址作为&lt;code&gt;* mut u8&lt;/code&gt;指针返回之前，我们将分配计数器加1。&lt;/p&gt;
&lt;p&gt;请注意，我们不执行任何边界检查或对齐调整，因此此实现尚不安全。这无关紧要，因为无论如何它都无法通过编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0594]: cannot assign to `self.next` which is behind a `&amp;amp;` reference
  --&amp;gt; src/allocator/bump.rs:29:9
   |
26 |     unsafe fn alloc(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8 {
   |                     ----- help: consider changing this to be a mutable reference: `&amp;amp;mut self`
...
29 |         self.next = alloc_start + layout.size();
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&amp;amp;` reference, so the data it refers to cannot be written
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;code&gt;self.allocations += 1&lt;/code&gt;行也会报告相同的错误，在此省略。）&lt;/p&gt;
&lt;p&gt;发生错误是因为&lt;code&gt;GlobalAlloc&lt;/code&gt;trait的&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.alloc&quot;&gt;&lt;code&gt;alloc&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.dealloc&quot;&gt;&lt;code&gt;dealloc&lt;/code&gt;&lt;/a&gt;方法仅使用不可变的&lt;code&gt;&amp;amp;self&lt;/code&gt;引用执行，因此无法修改&lt;code&gt;next&lt;/code&gt;和&lt;code&gt;allocations&lt;/code&gt;字段。这是不行的，因为在分配后更新&lt;code&gt;next&lt;/code&gt;是线性分配器的基本原理。&lt;/p&gt;
&lt;p&gt;注意，在方法声明中将&lt;code&gt;&amp;amp;self&lt;/code&gt;更改为&lt;code&gt;&amp;amp;mut self&lt;/code&gt;的编译器建议在这里并不会起作用。因为该方法签名是由&lt;code&gt;GlobalAlloc&lt;/code&gt;trait定义的，不能在实现时进行更改。（我在Rust代码库中开了一个关于无效建议的&lt;a href=&quot;https://github.com/rust-lang/rust/issues/68049&quot;&gt;issue&lt;/a&gt;。）&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;GlobalAlloc&lt;/code&gt;与可变性&lt;/h4&gt;
&lt;p&gt;在研究这个可变性问题可能的解决方案之前，让我们尝试理解&lt;code&gt;GlobalAlloc&lt;/code&gt;trait中的方法为什么使用&lt;code&gt;&amp;amp;self&lt;/code&gt;定义参数的原因：正如我们在&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/#the-global-allocator-attribute&quot;&gt;上一篇文章&lt;/a&gt;中看到的那样，全局堆分配器是通过将&lt;code&gt;#[global_allocator]&lt;/code&gt;属性添加到一个实现&lt;code&gt;GlobalAlloc&lt;/code&gt;trait的&lt;code&gt;static&lt;/code&gt;变量来定义的。静态变量在Rust中是不可变的，所以不能使用静态分配器的&lt;code&gt;&amp;amp;mut self&lt;/code&gt;引用来调用方法。因此，&lt;code&gt;GlobalAlloc&lt;/code&gt;的所有方法仅采用不可变的&lt;code&gt;&amp;amp;self&lt;/code&gt;引用。&lt;/p&gt;
&lt;p&gt;幸运的是，有一种方法可以从&lt;code&gt;&amp;amp;self&lt;/code&gt;引用中获取&lt;code&gt;&amp;amp;mut self&lt;/code&gt;引用：我们可以通过将分配器封装在&lt;a href=&quot;https://docs.rs/spin/0.5.0/spin/struct.Mutex.html&quot;&gt;&lt;code&gt;spin::Mutex&lt;/code&gt;&lt;/a&gt;自旋锁中来使用同步的&lt;a href=&quot;https://doc.rust-lang.org/book/ch15-05-interior-mutability.html&quot;&gt;内部可变性&lt;/a&gt;。该类型提供互斥方法&lt;code&gt;lock&lt;/code&gt;，从而安全地将&lt;code&gt;&amp;amp;self&lt;/code&gt;引用转换为&lt;code&gt;&amp;amp;mut self&lt;/code&gt;引用。我们已经在内核中多次使用了该封装类型，比如用在&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/#spinlocks&quot;&gt;VGA文本缓冲区&lt;/a&gt;中。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;Locked&lt;/code&gt;封装类型&lt;/h3&gt;
&lt;p&gt;借助&lt;code&gt;spin::Mutex&lt;/code&gt;封装类型，我们可以为线性分配器实现&lt;code&gt;GlobalAlloc&lt;/code&gt;特性。这里的技巧是不是直接为&lt;code&gt;BumpAllocator&lt;/code&gt;实现trait，而是为封装的&lt;code&gt;spin::Mutex &amp;lt;BumpAllocator&amp;gt;&lt;/code&gt;类型实现trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsafe impl GlobalAlloc for spin::Mutex&amp;lt;BumpAllocator&amp;gt; {…}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很不幸这仍然不起作用，因为Rust编译器不允许为其他crate中定义的类型的实现trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
  --&amp;gt; src/allocator/bump.rs:28:1
   |
28 | unsafe impl GlobalAlloc for spin::Mutex&amp;lt;BumpAllocator&amp;gt; {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^--------------------------
   | |                           |
   | |                           `spin::mutex::Mutex` is not defined in the current crate
   | impl doesn&apos;t use only types from inside the current crate
   |
   = note: define and implement a trait or new type instead
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要解决此问题，我们需要再次封装&lt;code&gt;spin::Mutex&lt;/code&gt;以创建自己的封装类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// 封装spin::Mutex，以允许为其实现trait
pub struct Locked&amp;lt;A&amp;gt; {
    inner: spin::Mutex&amp;lt;A&amp;gt;,
}

impl&amp;lt;A&amp;gt; Locked&amp;lt;A&amp;gt; {
    pub const fn new(inner: A) -&amp;gt; Self {
        Locked {
            inner: spin::Mutex::new(inner),
        }
    }

    pub fn lock(&amp;amp;self) -&amp;gt; spin::MutexGuard&amp;lt;A&amp;gt; {
        self.inner.lock()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该类型是对&lt;code&gt;spin::Mutex&amp;lt;A&amp;gt;&lt;/code&gt;的泛型封装。它对封装的类型&lt;code&gt;A&lt;/code&gt;没有任何限制，因此可以用于封装所有类型，而不仅仅是分配器。它提供了一个简单的构造函数&lt;code&gt;new&lt;/code&gt;，用于封装给定值。为了方便调用，它还提供了&lt;code&gt;lock&lt;/code&gt;函数，该函数可调用被封装的&lt;code&gt;Mutex&lt;/code&gt;上的&lt;code&gt;lock&lt;/code&gt;函数。由于&lt;code&gt;Locked&lt;/code&gt;类型非常通用，也可用在其他分配器实现中，因此我们将它放在父级模块&lt;code&gt;allocator&lt;/code&gt;中。&lt;/p&gt;
&lt;h4&gt;实现&lt;code&gt;Locked&amp;lt;BumpAllocator&amp;gt;&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Locked&lt;/code&gt;类型是在我们自己的crate中定义的（与&lt;code&gt;spin::Mutex&lt;/code&gt;不同），因此可以用来为线性分配器实现&lt;code&gt;GlobalAlloc&lt;/code&gt;。完整实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use super::{align_up, Locked};
use alloc::alloc::{GlobalAlloc, Layout};
use core::ptr;

unsafe impl GlobalAlloc for Locked&amp;lt;BumpAllocator&amp;gt; {
    unsafe fn alloc(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8 {
        let mut bump = self.lock(); // 获取可变引用

        let alloc_start = align_up(bump.next, layout.align());
        let alloc_end = match alloc_start.checked_add(layout.size()) {
            Some(end) =&amp;gt; end,
            None =&amp;gt; return ptr::null_mut(),
        };

        if alloc_end &amp;gt; bump.heap_end {
            ptr::null_mut() // 内存不足
        } else {
            bump.next = alloc_end;
            bump.allocations += 1;
            alloc_start as *mut u8
        }
    }

    unsafe fn dealloc(&amp;amp;self, _ptr: *mut u8, _layout: Layout) {
        let mut bump = self.lock(); // 获取可变引用

        bump.allocations -= 1;
        if bump.allocations == 0 {
            bump.next = bump.heap_start;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;的第一步是通过&lt;code&gt;inner&lt;/code&gt;字段调用&lt;a href=&quot;https://docs.rs/spin/0.5.0/spin/struct.Mutex.html#method.lock&quot;&gt;&lt;code&gt;Mutex::lock&lt;/code&gt;&lt;/a&gt;方法，以获取被封装分配器类型的可变引用。该实例将保持锁定状态，直到方法结束，即使在多线程环境中也不会发生数据竞争（我们将很快添加线程支持）。&lt;/p&gt;
&lt;p&gt;与前面的原型实现相比，&lt;code&gt;alloc&lt;/code&gt;现在的实现遵守对齐要求并执行边界检查，以确保分配器分配的内存块仍在堆内存区域范围内。第一步是将&lt;code&gt;next&lt;/code&gt;地址向上界对齐到&lt;code&gt;Layout&lt;/code&gt;参数上。我们稍后再展示&lt;code&gt;align_up&lt;/code&gt;函数的代码。然后将请求的分配大小加上&lt;code&gt;alloc_start&lt;/code&gt;以获得这次分配的结束地址。为了防止在执行大尺寸分配时整型溢出，我们使用&lt;a href=&quot;https://doc.rust-lang.org/std/primitive.usize.html#method.checked_add&quot;&gt;&lt;code&gt;checked_add&lt;/code&gt;&lt;/a&gt;方法进行检查。如果发生溢出或分配结果的结束地址大于堆的结束地址，我们将返回空指针以表示内存不足的情况。否则，我们将更新&lt;code&gt;next&lt;/code&gt;地址，并像以前一样将&lt;code&gt;allocations&lt;/code&gt;计数器加1。最后返回转换为&lt;code&gt;*mut u8&lt;/code&gt;指针的&lt;code&gt;alloc_start&lt;/code&gt;地址。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;dealloc&lt;/code&gt;函数将忽略给定的指针和&lt;code&gt;Layout&lt;/code&gt;参数。它仅用于为&lt;code&gt;allocations&lt;/code&gt;计数器减1。如果计数器再次归&lt;code&gt;0&lt;/code&gt;，则意味着所有分配都已被释放。在这种情况下，它将&lt;code&gt;next&lt;/code&gt;地址重置为&lt;code&gt;heap_start&lt;/code&gt;地址，以使完整的堆内存再次可用。&lt;/p&gt;
&lt;h4&gt;地址对齐&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;align_up&lt;/code&gt;函数非常通用，可以将其放入父级模块&lt;code&gt;allocator&lt;/code&gt;中。其实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// 将给定的`addr`向上对齐到`align`
fn align_up(addr: usize, align: usize) -&amp;gt; usize {
    let remainder = addr % align;
    if remainder == 0 {
        addr // `addr`已对齐
    } else {
        addr - remainder + align
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数首先结算&lt;code&gt;addr&lt;/code&gt;除以&lt;code&gt;align&lt;/code&gt;的&lt;a href=&quot;https://en.wikipedia.org/wiki/Euclidean_division&quot;&gt;余数&lt;/a&gt;。如果余数为&lt;code&gt;0&lt;/code&gt;，则地址已经与给定的参数对齐。否则，我们通过减去余数（这样新的余数为0）然后加上对齐参数（保证新地址不会变得小于原地址）来对齐地址。&lt;/p&gt;
&lt;p&gt;请注意，这并不是实现此函数最高效的实现。更快的实现如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// 将给定的`addr`向上对齐到`align`
///
/// 要求`align`是2的幂
fn align_up(addr: usize, align: usize) -&amp;gt; usize {
    (addr + align - 1) &amp;amp; !(align - 1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数利用&lt;code&gt;GlobalAlloc&lt;/code&gt;trait确保&lt;code&gt;align&lt;/code&gt;始终是2的幂。这样就可以非常高效的通过创建&lt;a href=&quot;https://en.wikipedia.org/wiki/Mask_(computing)&quot;&gt;位掩码&lt;/a&gt;的方式来执行地址对齐。让我们从右往左一步一步理解其工作原理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;由于&lt;code&gt;align&lt;/code&gt;是2的幂，因此它的&lt;a href=&quot;https://en.wikipedia.org/wiki/Binary_number#Representation&quot;&gt;二进制表示&lt;/a&gt;只会有一位被置为1（例如&lt;code&gt;0b000100000&lt;/code&gt;）。这意味着&lt;code&gt;align-1&lt;/code&gt;操作会将所有低位置为1（例如&lt;code&gt;0b00011111&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;通过&lt;code&gt;!&lt;/code&gt;执行&lt;a href=&quot;https://en.wikipedia.org/wiki/Bitwise_operation#NOT&quot;&gt;按位&lt;code&gt;NOT&lt;/code&gt;&lt;/a&gt;运算，我们得到一个除低于&lt;code&gt;align&lt;/code&gt;的位外（例如&lt;code&gt;0b…111111111100000&lt;/code&gt;）其余位均置为1的数字。&lt;/li&gt;
&lt;li&gt;通过对地址和&lt;code&gt;!(align-1)&lt;/code&gt;进行&lt;a href=&quot;https://en.wikipedia.org/wiki/Bitwise_operation#AND&quot;&gt;按位&lt;code&gt;AND&lt;/code&gt;&lt;/a&gt;，地址将&lt;em&gt;向下&lt;/em&gt;对齐，即清除所有低于&lt;code&gt;align&lt;/code&gt;的位。&lt;/li&gt;
&lt;li&gt;由于需要的是要向上对齐而不是向下对齐，因此在执行按位与运算之前，需要给&lt;code&gt;addr&lt;/code&gt;增加&lt;code&gt;align-1&lt;/code&gt;。如此，已对齐的地址将保持不变，而未对齐的地址将在下一个对齐边界处取整。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以选择任意一种实现方式，它们的计算结果是相同的。&lt;/p&gt;
&lt;h3&gt;使用线性分配器&lt;/h3&gt;
&lt;p&gt;要使用线性分配器而不是&lt;code&gt;linked_list_allocator&lt;/code&gt;crate，我们需要修改&lt;code&gt;allocator.rs&lt;/code&gt;中的静态变量&lt;code&gt;ALLOCATOR&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bump::BumpAllocator;

#[global_allocator]
static ALLOCATOR: Locked&amp;lt;BumpAllocator&amp;gt; = Locked::new(BumpAllocator::new());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，这里的&lt;code&gt;BumpAllocator::new&lt;/code&gt;和&lt;code&gt;Locked::new&lt;/code&gt;均已声明为&lt;code&gt;const&lt;/code&gt;函数。如果它们是普通函数，则会发生编译错误，因为&lt;code&gt;static&lt;/code&gt;变量的初始化表达式必须在编译期求值。&lt;/p&gt;
&lt;p&gt;我们不需要在&lt;code&gt;init_heap&lt;/code&gt;函数中修改&lt;code&gt;ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE)&lt;/code&gt;调用，因为线性分配器提供的接口与&lt;code&gt;linked_list_allocator&lt;/code&gt;相同。&lt;/p&gt;
&lt;p&gt;现在内核使用了线性分配器！原来的代码应该仍然有效，包括我们在上一篇文章中创建的&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/#adding-a-test&quot;&gt;&lt;code&gt;heap_allocation&lt;/code&gt;测试&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test --test heap_allocation
[…]
Running 3 tests
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关于线性分配器的讨论&lt;/h3&gt;
&lt;p&gt;线性分配的最大优点是效率极高。与需要主动寻找合适的内存块并在&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;上执行各种跟踪记录任务的其他分配器设计（参见下文）相比，线性分配器甚至可以&lt;a href=&quot;https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html&quot;&gt;被优化&lt;/a&gt;为一组汇编指令。因此，在分配器性能优化任务中，线性分配器就十分有用，例如在创建虚拟DOM库时。&lt;/p&gt;
&lt;p&gt;不过线性分配器很少被当做全局分配器使用，它通常出现在“&lt;a href=&quot;https://mgravell.github.io/Pipelines.Sockets.Unofficial/docs/arenas.html&quot;&gt;分配竞技场&lt;/a&gt;”中，即将多个独立的分配器组合在一起以提高性能。Rust中的竞技场分配器的一个实现是&lt;a href=&quot;https://docs.rs/toolshed/0.8.1/toolshed/index.html&quot;&gt;&lt;code&gt;toolshed&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;线性分配器的缺点&lt;/h4&gt;
&lt;p&gt;线性分配器的主要局限性在于，它只有在释放了所有分配后才能重新使用释放的内存再次进行分配。这意味着一个长期独占的分配就足以拖延内存的重用。我们只需对&lt;code&gt;many_boxes&lt;/code&gt;测试稍加修改就可以演示这个现象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn many_boxes_long_lived() {
    let long_lived = Box::new(1); // new
    for i in 0..HEAP_SIZE {
        let x = Box::new(i);
        assert_eq!(*x, i);
    }
    assert_eq!(*long_lived, 1); // new
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;像&lt;code&gt;many_boxes&lt;/code&gt;一样，此测试也会创建大量分配，如果分配器不重新使用释放的内存，将引发内存不足的错误。此外，测试会创建一个&lt;code&gt;long_lived&lt;/code&gt;分配，该分配在整个循环执行期间均有效。&lt;/p&gt;
&lt;p&gt;当我们尝试运行新测试时将发行测试确实失败了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test --test heap_allocation
Running 4 tests
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]
many_boxes_long_lived... [failed]

Error: panicked at &apos;allocation error: Layout { size_: 8, align_: 8 }&apos;, src/lib.rs:86:5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;详细讨论一下测试为什么会失败：首先，在堆的开头创建&lt;code&gt;long_lived&lt;/code&gt;分配，从而将&lt;code&gt;allocations&lt;/code&gt;计数器加1。在每次循环中，都会创建一个短暂的分配并在下一次迭代开始之前就被释放。这意味着&lt;code&gt;allocations&lt;/code&gt;计数器在迭代开始时临时增加到2，在迭代结束时减少为1。现在的问题是，线性分配器仅在所有分配释放后才可以重用内存，即&lt;code&gt;allocations&lt;/code&gt;计数器减至0。不过这在整个循环完全结束之前都不会发生，因此每次循环迭代都将会分配出去一个新的内存块，从而使得内存在多次迭代后耗尽。&lt;/p&gt;
&lt;h4&gt;修复这个测试？&lt;/h4&gt;
&lt;p&gt;有两个的技巧可以用来修复这个线性分配器的测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们可以修改&lt;code&gt;dealloc&lt;/code&gt;，通过比较被释放内存的结束地址与&lt;code&gt;next&lt;/code&gt;指针是否一致，来判断本次内存释放是否为刚刚才分配出去的内存。如果二者相等，我们可以安全地将&lt;code&gt;next&lt;/code&gt;重置为刚刚被释放的分配的起始地址。如此，每次循环都将会重复同一个内存块。&lt;/li&gt;
&lt;li&gt;我们也可以添加一个&lt;code&gt;alloc_back&lt;/code&gt;方法，该方法使用额外的&lt;code&gt;next_back&lt;/code&gt;字段从堆尾开始分配内存。然后，我们就可以为所有长期分配手动调用方法，从而在堆上将短期分配和长期分配分隔开。请注意，只有事先清楚每个分配的有效期限，才能使用这种分隔。该方法的另一个缺点是需要手动执行分配，麻烦且存在安全风险。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然这两种方法都可以修复测试，但它们并不是通用的解决方案，因为它们都只能在非常特殊的情况下重用内存。问题是：是否有一个通用的解决方案可以重用所有已释放的内存？&lt;/p&gt;
&lt;h4&gt;重用所有被释放的内存&lt;/h4&gt;
&lt;p&gt;正如我们在&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/#dynamic-memory&quot;&gt;上一篇文章&lt;/a&gt;中了解到的那样，分配可能具有任意的生命周期，并且可以按照任意顺序释放。这意味着我们需要跟踪数量未知且非连续的未使用内存区域，如以下示例所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/allocator-designs/allocation-fragmentation.svg&quot; alt=&quot;分配碎片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图显示了堆的分配状态随时间的变化。一开始，整个堆都是未使用的，此时&lt;code&gt;next&lt;/code&gt;地址等于&lt;code&gt;heap_start&lt;/code&gt;（第1行）。然后出现了第一次分配（第2行）。在第3行中，第二个存储块被分配出去，同时第一个分配被释放了。在第4行中新增了更多分配。其中一半有效期很短，它们在第5行中已被释放，而且在这一行中还新增了另一个分配。&lt;/p&gt;
&lt;p&gt;第5行显示了一个基本问题：我们共有五个未使用的内存区域，它们的大小各不相同，但是&lt;code&gt;next&lt;/code&gt;指针只能指向最后一个区域的开始。尽管对于本示例，我们可以将其他未使用的存储区域的起始地址和大小存储在大小为4的数组中，但这并不是一个通用的解决方案，因为我们同样可以轻松地创建一个包含8个、16个或1000个未使用的存储区域的例子。&lt;/p&gt;
&lt;p&gt;通常，当我们需要处理数量未知的项目时，会倾向于使用在堆上进行分配的集合类型。但在现在并不可行，因为堆分配器不能依赖于自身（这将导致无限递归或死锁）。因此，我们需要找到其他解决方案。&lt;/p&gt;
&lt;h2&gt;链表分配器&lt;/h2&gt;
&lt;p&gt;在实现分配器时，跟踪任意数量的空闲内存区域的常见技巧，就是将这些区域本身用作后备存储。这利用了一个事实：这些已释放的内存区域仍被映射到虚拟地址上，其背后都有一个对应的物理帧，但是我们已经不再需要其上存储的信息了。通过使用区域本身存储该区域相关的内存释放信息，我们就可以跟踪任意数量的释放区域，而无需额外的内存。&lt;/p&gt;
&lt;p&gt;最常见的实现方法是在释放的内存中构造一个链表，每个节点都是一个已释放的内存区域：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/allocator-designs/linked-list-allocation.svg&quot; alt=&quot;链表分配&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每个列表节点包含两个字段：此内存区域的大小和指向下一个未使用的内存区域的指针。使用这种方法，我们只需要那个指向第一个未使用区域的指针（称为&lt;code&gt;head&lt;/code&gt;）即可跟踪所有未使用区域，而与它们的数量无关。产生的数据结构通常称为&lt;a href=&quot;https://en.wikipedia.org/wiki/Free_list&quot;&gt;&lt;em&gt;空闲列表&lt;/em&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;你可能已经从名称中猜到，这正是&lt;code&gt;linked_list_allocator&lt;/code&gt;crate使用的技术。使用此技术的分配器通常也被称为&lt;em&gt;池分配器&lt;/em&gt;。&lt;/p&gt;
&lt;h3&gt;实现链表分配器&lt;/h3&gt;
&lt;p&gt;接下来，我们将使用上述跟踪已释放的内存区域的方法，自己实现一个简单的&lt;code&gt;LinkedListAllocator&lt;/code&gt;类型。以后的文章并不需要这一部分的内容，因此你可以根据需要选择跳过实现细节。&lt;/p&gt;
&lt;h4&gt;分配器类型&lt;/h4&gt;
&lt;p&gt;我们首先在新的&lt;code&gt;allocator::linked_list&lt;/code&gt;子模块中创建一个私有&lt;code&gt;ListNode&lt;/code&gt;结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod linked_list;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;struct ListNode {
    size: usize,
    next: Option&amp;lt;&amp;amp;&apos;static mut ListNode&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就像在上图中描述的那样，列表节点具有一个&lt;code&gt;size&lt;/code&gt;字段和一个指向下一个节点的可选指针，由&lt;code&gt;Option&amp;lt;&amp;amp;&apos;static mut ListNode&amp;gt;&lt;/code&gt;类型表示。静态&lt;code&gt;mut&lt;/code&gt;类型在语义上描述了指针&lt;a href=&quot;https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html&quot;&gt;拥有&lt;/a&gt;其后指向的对象。这就基本上就是一个&lt;code&gt;Box&lt;/code&gt;类型，只不过它没有能够在作用域末尾释放持有对象的析构函数。&lt;/p&gt;
&lt;p&gt;我们为&lt;code&gt;ListNode&lt;/code&gt;实现下列方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl ListNode {
    const fn new(size: usize) -&amp;gt; Self {
        ListNode { size, next: None }
    }

    fn start_addr(&amp;amp;self) -&amp;gt; usize {
        self as *const Self as usize
    }

    fn end_addr(&amp;amp;self) -&amp;gt; usize {
        self.start_addr() + self.size
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该类型具有一个简单的构造函数&lt;code&gt;new&lt;/code&gt;，以及用于计算所表示内存区域的开始地址和结束地址的方法。我们将新函数设为&lt;a href=&quot;https://doc.rust-lang.org/reference/items/functions.html#const-functions&quot;&gt;const函数&lt;/a&gt;，因为稍后会在构造静态链表分配器时使用到它。请注意，在const函数中使用可变引用（包括将&lt;code&gt;next&lt;/code&gt;字段设置为&lt;code&gt;None&lt;/code&gt;）仍然不稳定。为了能够编译，我们需要在&lt;code&gt;lib.rs&lt;/code&gt;的开头添加&lt;code&gt;#![feature(const_mut_refs)]&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们现在可以使用&lt;code&gt;ListNode&lt;/code&gt;结构作为积木，来创建&lt;code&gt;LinkedListAllocator&lt;/code&gt;结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct LinkedListAllocator {
    head: ListNode,
}

impl LinkedListAllocator {
    /// 创建一个空的`LinkedListAllocator`.
    pub const fn new() -&amp;gt; Self {
        Self {
            head: ListNode::new(0),
        }
    }

    /// 使用给定的堆边界初始化分配器
    /// 
    /// 该方法为非安全，因为调用者必须保证提供的内存范围未被使用。
    /// 同时，该方法只能被调用一次。
    pub unsafe fn init(&amp;amp;mut self, heap_start: usize, heap_size: usize) {
        self.add_free_region(heap_start, heap_size);
    }

    /// 将给定的内存区域添加至链表前端
    unsafe fn add_free_region(&amp;amp;mut self, addr: usize, size: usize) {
        todo!();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构体包含一个&lt;code&gt;head&lt;/code&gt;节点，指向第一个堆区域。我们只对&lt;code&gt;next&lt;/code&gt;指针的值感兴趣，因此在&lt;code&gt;ListNode::new&lt;/code&gt;函数中将大小设为0。将&lt;code&gt;head&lt;/code&gt;设置为&lt;code&gt;ListNode&lt;/code&gt;而不是仅使用&lt;code&gt;&amp;amp;&apos;static mut ListNode&lt;/code&gt;的优点是相应的&lt;code&gt;alloc&lt;/code&gt;方法的实现将会更加简单。&lt;/p&gt;
&lt;p&gt;就像线性分配器一样，&lt;code&gt;new&lt;/code&gt;函数并不会直接使用堆边界来初始化分配器。除了希望保持API兼容性之外，原因还在于初始化过程需要将一个节点写入堆内存，这只能在运行时发生。然而，&lt;code&gt;new&lt;/code&gt;函数必须是可以在编译时求值的&lt;a href=&quot;https://doc.rust-lang.org/reference/items/functions.html#const-functions&quot;&gt;&lt;code&gt;const&lt;/code&gt;函数&lt;/a&gt;，因为它将用于初始化静态变量&lt;code&gt;ALLOCATOR&lt;/code&gt;。出于这个原因，我们仍然使用一个独立的非-常函数&lt;code&gt;init&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;init&lt;/code&gt;方法使用&lt;code&gt;add_free_region&lt;/code&gt;方法，稍后将展示其实现。现在，我们使用&lt;code&gt;todo!&lt;/code&gt;宏提供一个总是panic的实现占位符。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;add_free_region&lt;/code&gt;方法&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;add_free_region&lt;/code&gt;方法为链表提供了基本的&lt;em&gt;push&lt;/em&gt;操作。目前，我们仅在&lt;code&gt;init&lt;/code&gt;中调用此方法，但它也将成为我们&lt;code&gt;dealloc&lt;/code&gt;实现中的核心方法。请记住，当释放分配的内存区域时，将调用&lt;code&gt;dealloc&lt;/code&gt;方法。为了跟踪此次释放产生的的未使用内存区域，我们希望将其push到链表中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;add_free_region&lt;/code&gt;方法的实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
use super::align_up;
use core::mem;

impl LinkedListAllocator {
    /// 将给定的内存区域添加至链表前端
    unsafe fn add_free_region(&amp;amp;mut self, addr: usize, size: usize) {
        // 确保此空闲区域足以容纳一个`ListNode`
        assert_eq!(align_up(addr, mem::align_of::&amp;lt;ListNode&amp;gt;()), addr);
        assert!(size &amp;gt;= mem::size_of::&amp;lt;ListNode&amp;gt;());

        // 创建一个新的`ListNode`并将其添加至链表前端
        let mut node = ListNode::new(size);
        node.next = self.head.next.take();
        let node_ptr = addr as *mut ListNode;
        node_ptr.write(node);
        self.head.next = Some(&amp;amp;mut *node_ptr)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法获取一个内存区域的地址和大小作为参数，将该区域添加到列表前端。首先要保证给定的内存区域具有存储&lt;code&gt;ListNode&lt;/code&gt;所需的大小和对齐方式。然后再按照下图中的步骤创建节点并将其插入到列表中：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/allocator-designs/linked-list-allocator-push.svg&quot; alt=&quot;链表分配器push&quot; /&gt;&lt;/p&gt;
&lt;p&gt;步骤0显示了调用&lt;code&gt;add_free_region&lt;/code&gt;前堆的状态。在步骤1中，使用图中标记为&lt;code&gt;freed&lt;/code&gt;的内存区域作为参数调用该方法。初步检查后，该方法使用空闲区域大小作为参数，在其调用栈上创建了一个新&lt;code&gt;node&lt;/code&gt;。然后，它使用&lt;a href=&quot;https://doc.rust-lang.org/core/option/enum.Option.html#method.take&quot;&gt;&lt;code&gt;Option::take&lt;/code&gt;&lt;/a&gt;方法将新&lt;code&gt;node&lt;/code&gt;的&lt;code&gt;next&lt;/code&gt;指针设置为当前的头指针，并将头指针重置为&lt;code&gt;None&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在步骤2中，该方法使用&lt;a href=&quot;https://doc.rust-lang.org/std/primitive.pointer.html#method.write&quot;&gt;&lt;code&gt;write&lt;/code&gt;&lt;/a&gt;方法将新创建的&lt;code&gt;node&lt;/code&gt;（译注：目前仍在调用栈中）写入空闲内存区域的开头。最后将&lt;code&gt;head&lt;/code&gt;指针指向这个新节点。图中最后呈现出来的指针结构略显混乱，这是因为释放区域总是在列表的开头插入，不过如果我们跟随指针，就会从&lt;code&gt;head&lt;/code&gt;指针开始依次到达每个空闲区域了。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;find_region&lt;/code&gt;函数&lt;/h4&gt;
&lt;p&gt;链表的另一个基本操作是查找条目并将其从列表中移除。这是实现&lt;code&gt;alloc&lt;/code&gt;方法所需的核心操作。我们通过以下方式将该操作实现为&lt;code&gt;find_region&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl LinkedListAllocator {
    /// 按照给定大小和对齐方式查找一个空闲内存区域
    /// 并将其从链表中删除
    ///
    /// 返回一个包含`ListNode`和其开始地址的分配
    fn find_region(&amp;amp;mut self, size: usize, align: usize)
        -&amp;gt; Option&amp;lt;(&amp;amp;&apos;static mut ListNode, usize)&amp;gt;
    {
        // 当前链表节点的引用，会在每次迭代中更新
        let mut current = &amp;amp;mut self.head;
        // 在链表中查找一个足够大的内存区域
        while let Some(ref mut region) = current.next {
            if let Ok(alloc_start) = Self::alloc_from_region(&amp;amp;region, size, align) {
                // 该区域可以容纳所需的分配，则将该区域从链表中删除
                let next = region.next.take();
                let ret = Some((current.next.take().unwrap(), alloc_start));
                current.next = next;
                return ret;
            } else {
                // 该区域不可以容纳所需的分配，则继续下一轮迭代查找
                current = current.next.as_mut().unwrap();
            }
        }

        // 已经找不到合适的内存区域了
        None
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该方法使用一个&lt;code&gt;current&lt;/code&gt;变量和一个&lt;a href=&quot;https://doc.rust-lang.org/reference/expressions/loop-expr.html#predicate-pattern-loops&quot;&gt;&lt;code&gt;while let&lt;/code&gt;循环&lt;/a&gt;来遍历列表元素。首先，将&lt;code&gt;current&lt;/code&gt;设置到（假的）&lt;code&gt;head&lt;/code&gt;节点。然后，在每次迭代中，它都会更新为当前节点的&lt;code&gt;next&lt;/code&gt;字段（在&lt;code&gt;else&lt;/code&gt;块中）。如果该区域适合于具有给定大小和对齐方式的分配，则将该区域从链表中删除，并与&lt;code&gt;alloc_start&lt;/code&gt;地址一起返回。&lt;/p&gt;
&lt;p&gt;当&lt;code&gt;current.next&lt;/code&gt;指针变为&lt;code&gt;None&lt;/code&gt;时，循环退出。这意味着我们遍历了整个链表，却没并有找到适合于给定分配的区域。在这种情况下，我们将返回&lt;code&gt;None&lt;/code&gt;。检查区域是否合适由&lt;code&gt;alloc_from_region&lt;/code&gt;函数完成，我们将在稍后展示其实现。&lt;/p&gt;
&lt;p&gt;先让我们更加详细地研究如何从列表中删除合适的区域：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/allocator-designs/linked-list-allocator-remove-region.svg&quot; alt=&quot;链表分配器删除内存区域&quot; /&gt;&lt;/p&gt;
&lt;p&gt;步骤0显示了链表指针调整之前的状态。&lt;code&gt;region&lt;/code&gt;区域、&lt;code&gt;current&lt;/code&gt;区域、&lt;code&gt;region.next&lt;/code&gt;指针和&lt;code&gt;current.next&lt;/code&gt;指针均已在图中标出。在步骤1中，使用&lt;code&gt;Option::take&lt;/code&gt;方法将&lt;code&gt;region.next&lt;/code&gt;和&lt;code&gt;current.next&lt;/code&gt;指针都重置为&lt;code&gt;None&lt;/code&gt;。原来的指针分别存储在局部变量&lt;code&gt;next&lt;/code&gt;和&lt;code&gt;ret&lt;/code&gt;中。&lt;/p&gt;
&lt;p&gt;在步骤2中，将局部变量&lt;code&gt;next&lt;/code&gt;指针赋给&lt;code&gt;current.next&lt;/code&gt;指针，也就是原来的&lt;code&gt;region.next&lt;/code&gt;指针。结果是&lt;code&gt;current&lt;/code&gt;现在直接指向&lt;code&gt;region&lt;/code&gt;之后的下一个区域，因此&lt;code&gt;region&lt;/code&gt;就不再是链表中的元素了。最后将存储在局部变量&lt;code&gt;ret&lt;/code&gt;中的指针返回。&lt;/p&gt;
&lt;h4&gt;&lt;em&gt;&lt;code&gt;alloc_from_region&lt;/code&gt;函数&lt;/em&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;alloc_from_region&lt;/code&gt;函数返回给定内存区域是否适合于具有给定大小和对齐方式的分配。其实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl LinkedListAllocator {
    /// 尝试使用给定内存区域，为给定大小和对齐方式的分配做出分配
    ///
    /// 如果成功则返回分配的开始地址。
    fn alloc_from_region(region: &amp;amp;ListNode, size: usize, align: usize)
        -&amp;gt; Result&amp;lt;usize, ()&amp;gt;
    {
        let alloc_start = align_up(region.start_addr(), align);
        let alloc_end = alloc_start.checked_add(size).ok_or(())?;

        if alloc_end &amp;gt; region.end_addr() {
            // 内存区域太小
            return Err(());
        }

        let excess_size = region.end_addr() - alloc_end;
        if excess_size &amp;gt; 0 &amp;amp;&amp;amp; excess_size &amp;lt; mem::size_of::&amp;lt;ListNode&amp;gt;() {
            // 该内存区域的剩余部分太小，无法容纳一个`ListNode`
            // （这是必须的，因为分配动作会将该区域分为已使用和未使用两个部分）
            return Err(());
        }

        // 该区域适合于给定的分配
        Ok(alloc_start)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，该函数使用我们之前定义的&lt;code&gt;align_up&lt;/code&gt;函数和&lt;a href=&quot;https://doc.rust-lang.org/std/primitive.usize.html#method.checked_add&quot;&gt;&lt;code&gt;checked_add&lt;/code&gt;&lt;/a&gt;方法来计算给定分配的起始地址和结束地址。若发生溢出，或结束地址在给定区域的结束地址之后，则给定区域过小，不适合给定分配，所以我们返回错误。&lt;/p&gt;
&lt;p&gt;接下来，函数执行的检查可能不太直观。不过这个检查是必要的，因为在大多数情况下，给定区域不可能正好就适合给定分配，因此分配过后，该区域的一部分仍为未使用区域。而此次分配之后，该区域的这一空闲部分必须足够大，以存储其自己的ListNode。该检查将准确地验证：分配是否完全合适（&lt;code&gt;excess_size == 0&lt;/code&gt;）或多余的大小是否足以存储一个&lt;code&gt;ListNode&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;为链表分配器实现&lt;code&gt;GlobalAlloc&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;add_free_region&lt;/code&gt;和&lt;code&gt;find_region&lt;/code&gt;方法提供的基本操作，就可以实现&lt;code&gt;GlobalAlloc&lt;/code&gt;trait了。与线性分配器一样，我们不直接为&lt;code&gt;LinkedListAllocator&lt;/code&gt;实现trait，而是为封装类型&lt;code&gt;Locked&amp;lt;LinkedListAllocator&amp;gt;&lt;/code&gt;实现trait。&lt;a href=&quot;https://os.phil-opp.com/allocator-designs/#a-locked-wrapper-type&quot;&gt;&lt;code&gt;Locked&lt;/code&gt;封装类型&lt;/a&gt;使用自旋锁增加了内部可变性，这使我们可以修改分配器实例，尽管&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;方法只使用了不可变引用&lt;code&gt;&amp;amp;self&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;实现看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use super::Locked;
use alloc::alloc::{GlobalAlloc, Layout};
use core::ptr;

unsafe impl GlobalAlloc for Locked&amp;lt;LinkedListAllocator&amp;gt; {
    unsafe fn alloc(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8 {
        // 调整对齐布局
        let (size, align) = LinkedListAllocator::size_align(layout);
        let mut allocator = self.lock();

        if let Some((region, alloc_start)) = allocator.find_region(size, align) {
            let alloc_end = alloc_start.checked_add(size).expect(&quot;overflow&quot;);
            let excess_size = region.end_addr() - alloc_end;
            if excess_size &amp;gt; 0 {
                allocator.add_free_region(alloc_end, excess_size);
            }
            alloc_start as *mut u8
        } else {
            ptr::null_mut()
        }
    }

    unsafe fn dealloc(&amp;amp;self, ptr: *mut u8, layout: Layout) {
        // 调整对齐布局
        let (size, _) = LinkedListAllocator::size_align(layout);

        self.lock().add_free_region(ptr as usize, size)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们西安从更简单的&lt;code&gt;dealloc&lt;/code&gt;方法看起：首先执行一些布局调整，我们将在稍后进行解释，并通过在&lt;a href=&quot;https://os.phil-opp.com/allocator-designs/#a-locked-wrapper-type&quot;&gt;&lt;code&gt;Locked&lt;/code&gt;封装&lt;/a&gt;上调用&lt;a href=&quot;https://docs.rs/spin/0.5.0/spin/struct.Mutex.html#method.lock&quot;&gt;&lt;code&gt;Mutex::lock&lt;/code&gt;&lt;/a&gt;函数来获取分配器的可变引用&lt;code&gt;&amp;amp;mut LinkedListAllocator&lt;/code&gt;。然后调用&lt;code&gt;add_free_region&lt;/code&gt;函数将释放的区域添加到空闲链表中。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;alloc&lt;/code&gt;方法有点复杂。类似的，先进行布局调整，再调用&lt;a href=&quot;https://docs.rs/spin/0.5.0/spin/struct.Mutex.html#method.lock&quot;&gt;&lt;code&gt;Mutex::lock&lt;/code&gt;&lt;/a&gt;函数以获取分配器的可变引用。然后使用&lt;code&gt;find_region&lt;/code&gt;找到适合这次分配的内存区域，并将其从空闲链表中删除。如果没找到返回&lt;code&gt;None&lt;/code&gt;时，方法会因为没有合适的内存区域而返回&lt;code&gt;null_mut&lt;/code&gt;，以表示分配错误。&lt;/p&gt;
&lt;p&gt;在分配成功的情况下，&lt;code&gt;find_region&lt;/code&gt;方法将返回包含合适区域（已从空闲链表中移除）和分配起始地址的元组。使用&lt;code&gt;alloc_start&lt;/code&gt;、给定的分配大小、区域的结束地址，方法将重新计算分配的结束地址以及该区域的剩余大小。如果确实有剩余大小，方法将调用&lt;code&gt;add_free_region&lt;/code&gt;将区域的剩余部分重新添加回空闲列表。最后将&lt;code&gt;alloc_start&lt;/code&gt;地址转换为&lt;code&gt;*mut u8&lt;/code&gt;类型并返回。&lt;/p&gt;
&lt;h4&gt;布局调整&lt;/h4&gt;
&lt;p&gt;那么，我们在&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;开头都进行了哪些布局调整？其实就是为了确保每个分配的块都能够存储&lt;code&gt;ListNode&lt;/code&gt;。这一点很重要，因为在某个时刻，该内存块将要被释放时，我们需要向其写入一个&lt;code&gt;ListNode&lt;/code&gt;记录。如果该内存块小于&lt;code&gt;ListNode&lt;/code&gt;或并没有正确的对齐，就会引发未定义行为。&lt;/p&gt;
&lt;p&gt;布局调整由&lt;code&gt;size_align&lt;/code&gt;函数执行，该函数实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl LinkedListAllocator {
    /// 调整给定布局，使生成的用以分配的内存区域也能够存储`ListNode`。
    ///
    /// 以元组`(size, align)`的形式返回调整后的大小和对齐方式。
    fn size_align(layout: Layout) -&amp;gt; (usize, usize) {
        let layout = layout
            .align_to(mem::align_of::&amp;lt;ListNode&amp;gt;())
            .expect(&quot;adjusting alignment failed&quot;)
            .pad_to_align();
        let size = layout.size().max(mem::size_of::&amp;lt;ListNode&amp;gt;());
        (size, layout.align())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，函数在传入的&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/struct.Layout.html&quot;&gt;&lt;code&gt;Layout&lt;/code&gt;&lt;/a&gt;上调用&lt;a href=&quot;https://doc.rust-lang.org/core/alloc/struct.Layout.html#method.align_to&quot;&gt;&lt;code&gt;align_to&lt;/code&gt;&lt;/a&gt;方法，以在必要时为&lt;code&gt;ListNode&lt;/code&gt;增加空间以对齐。然后使用&lt;a href=&quot;https://doc.rust-lang.org/core/alloc/struct.Layout.html#method.pad_to_align&quot;&gt;&lt;code&gt;pad_to_align&lt;/code&gt;&lt;/a&gt;方法将布局大小向上对齐，以确保下一个内存块的起始地址也将具有正确的对齐方式，从而能再储一个&lt;code&gt;ListNode&lt;/code&gt;。接下来，函数使用&lt;a href=&quot;https://doc.rust-lang.org/std/cmp/trait.Ord.html#method.max&quot;&gt;&lt;code&gt;max&lt;/code&gt;&lt;/a&gt;方法强制所分配的大小至少要为&lt;code&gt;mem::size_of::&amp;lt;ListNode&amp;gt;&lt;/code&gt;。如此，&lt;code&gt;dealloc&lt;/code&gt;函数就可以安全地将&lt;code&gt;ListNode&lt;/code&gt;写入已释放的内存块中了。&lt;/p&gt;
&lt;h3&gt;使用链表分配器&lt;/h3&gt;
&lt;p&gt;现在，我们只需要在&lt;code&gt;allocator&lt;/code&gt;模块中更新静态变量&lt;code&gt;ALLOCATOR&lt;/code&gt;，就可以使用新的&lt;code&gt;LinkedListAllocator&lt;/code&gt;了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use linked_list::LinkedListAllocator;

#[global_allocator]
static ALLOCATOR: Locked&amp;lt;LinkedListAllocator&amp;gt; =
    Locked::new(LinkedListAllocator::new());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于线性分配器和链接列表分配器的&lt;code&gt;init&lt;/code&gt;函数行为一致，因此我们无需修改&lt;code&gt;init_heap&lt;/code&gt;中的&lt;code&gt;init&lt;/code&gt;调用。&lt;/p&gt;
&lt;p&gt;现在，当我们再次运行&lt;code&gt;heap_allocation&lt;/code&gt;测试时，可以看到所有测试都通过了，其中包括在线性分配器中失败的&lt;code&gt;many_boxes_long_lived&lt;/code&gt;测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test --test heap_allocation
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]
many_boxes_long_lived... [ok]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这说明了我们的链表分配器能够将释放的内存重新用于后续分配。&lt;/p&gt;
&lt;h3&gt;关于链表分配器的讨论&lt;/h3&gt;
&lt;p&gt;与线性分配器相比，链表分配器更适合作为通用分配器使用，主要是因为它能够立刻重用释放的内存。当然，链表分配器也有一些缺点。其中一部分是由于我们的实现过于简单引起的，另一些则是由于链表分配器的设计本身存在的固有缺陷。&lt;/p&gt;
&lt;h4&gt;合并空闲块&lt;/h4&gt;
&lt;p&gt;我们实现的主要问题是，它只会将堆拆分为越来越小的块，而不会再将小块合并成大块。考虑下面的例子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/allocator-designs/linked-list-allocator-fragmentation-on-dealloc.svg&quot; alt=&quot;链表分配器释放的碎片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在第一行中，堆上创建了三个分配。它们中的两个在第2行中被释放，而第三个在第3行中被释放。现在虽然整个堆处于空闲状态，但仍旧被分成四个单独的块。此时，可能会由于四个块中的任何一个都不足够大，导致内核不能再进行大型分配了。随着时间的流逝，该过程将持续进行，并将堆分成越来越小的块。最后可能出现的情况就是，堆中的块过于分散过于小，使得哪怕正常大小的分配都无法进行了。&lt;/p&gt;
&lt;p&gt;要解决此问题，我们需要将相邻的释放块重新合并在一起。对于上面的示例，这意味着：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/allocator-designs/linked-list-allocator-merge-on-dealloc.svg&quot; alt=&quot;链表分配器释放后合并&quot; /&gt;&lt;/p&gt;
&lt;p&gt;跟第一张图一样，第&lt;code&gt;2&lt;/code&gt;行同样释放了三个分配中的两个。与第一张图不同的是，这次不在留着堆碎片不管，我们现在添加一个额外的&lt;code&gt;2a&lt;/code&gt;行合并操作，将两个最右边的块重新合并在一起。在第&lt;code&gt;3&lt;/code&gt;行中，第三个分配被释放（跟第一张图一样），从而产生由三个独立的块组成的一整个空闲的堆。同样的，我们再添加额外的&lt;code&gt;3a&lt;/code&gt;行合并操作，将三个相邻的块重新合并成一整块未使用的堆。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;linked_list_allocator&lt;/code&gt;crate使用以下方式实现此合并策略：与我们的实现不同，该实现并不向列表前端插入&lt;code&gt;deallocate&lt;/code&gt;释放的内存块，而是始终将列表按开始地址排序。这样，可以通过检查列表中两个相邻块的地址和大小，直接在&lt;code&gt;deallocate&lt;/code&gt;调用时执行合并。当然，这种方式的释放速度稍慢，但是可以防止在我们的实现中出现的堆碎片。&lt;/p&gt;
&lt;h4&gt;性能&lt;/h4&gt;
&lt;p&gt;正如我们在前文了解到的，线性分配器非常快，可以被优化为一组汇编操作。链表分配器作为分配器，性能则要差很多。问题在于，分配请求可能需要遍历完整的链表才能找到合适的块。&lt;/p&gt;
&lt;p&gt;由于链表长度取决于未使用的存储块的数量，因此对于不同的程序，其性能可能会发生极大的变化。对于仅创建几个分配的程序，其分配性能较好。但是，对于一个用很多分配将堆碎片化的程序，其分配性能将非常差，因为链表将变得很长，且可能包含非常多非常小的块。&lt;/p&gt;
&lt;p&gt;值得注意的是，这个性能问题并不是因为我们的实现过于简单而引起的，这是链表分配器设计的固有缺陷。由于分配器的性能对于内核级代码极其重要，因此，我们将在下文中探讨第三种分配器设计，该设计以降低内存利用率为代价来提高的性能。&lt;/p&gt;
&lt;h2&gt;固定大小的块分配器&lt;/h2&gt;
&lt;p&gt;接下来，我们将介绍一种使用固定大小的内存块来进行分配的分配器。这种分配器通常会返回大于分配所需的内存块，产生&lt;a href=&quot;https://en.wikipedia.org/wiki/Fragmentation_(computing)#Internal_fragmentation&quot;&gt;内部碎片&lt;/a&gt;从而导致一定的内存浪费。不过，它也大大降低了找到合适块所需的时间（与链表分配器相比），从而大幅提升分配性能。&lt;/p&gt;
&lt;h3&gt;简介&lt;/h3&gt;
&lt;p&gt;固定大小的块分配器的思路是：我们不再分配跟请求大小完全一致的块，而是定义少量种类的块，并为每一次分配选择一个大小向上取整的块。例如，我们可以定义16、64和512字节的块，分配需要4字节时就返回一个16字节的块，分配需要48字节时就返回一个64字节的块，而分配需要128字节时就返回一个512字节的块。&lt;/p&gt;
&lt;p&gt;像链表分配器一样，我们通过在未使用的内存中创建链表来跟踪未使用的内存。不同的是，这次不是像链表分配器那样用一个链表来跟踪不同大小的块，而是为不同大小的块分别设置各自的跟踪链表。于是，每个链表仅存储同一种大小的块。例如，在定义了16、64和512字节块的情况下，内存中将存在三个单独的链表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/allocator-designs/fixed-size-block-example.svg&quot; alt=&quot;固定大小的块示例&quot; /&gt;&lt;/p&gt;
&lt;p&gt;原先的链表分配器仅有一个&lt;code&gt;head&lt;/code&gt;指针，现在有&lt;code&gt;head_16&lt;/code&gt;、&lt;code&gt;head_64&lt;/code&gt;和&lt;code&gt;head_512&lt;/code&gt;三个了，它们分别指向各自对应大小的第一个未使用的块。而每个链表中的所有节点都具有相同的大小。例如，由&lt;code&gt;head_16&lt;/code&gt;指针开始的链表仅包含16字节的块。这意味着我们不再需要在每个列表节点中存储块的大小，因为它已经由头指针的名称决定了。&lt;/p&gt;
&lt;p&gt;由于链表中的每个节点都具有相同的大小，因此对一个分配请求来说，用链表中的哪个节点都是一样的。这意味着我们可以使用下列步骤非常有效地执行分配：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将请求的分配大小向上取整到最接近的块大小。比如在请求分配12个字节时，按照上例，我们将选择分配一个大小为16的块。&lt;/li&gt;
&lt;li&gt;（例如从数组中）取出链表的头指针。对于大小为16的块，我们需要用到&lt;code&gt;head_16&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;从链表中移除并返回第一个块。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;值得注意的是，此处仅需要返回链表的第一个元素，而不再需要遍历整个链表。因此，这种分配器会比链表分配器快得多。&lt;/p&gt;
&lt;h4&gt;块大小与内存浪费&lt;/h4&gt;
&lt;p&gt;我们在取整时将浪费的多少内存，取决于块大小的设置。例如，当返回一个分配请求128字节而我们返回一个512字节的块时，分配出去的内存中有四分之三被浪费了。通过定义合理的块大小，可以在一定程度上限制浪费的内存。例如，当使用2的幂（4、8、16、32、64、128等）作为块大小时，在最坏的情况下也只会浪费一半的内存，平均而言仅浪费四分之一的内存。&lt;/p&gt;
&lt;p&gt;基于程序中的公共分配尺寸来优化块大小也是很常见的。例如，我们可以额外增加大小为24的块，以提高经常执行24字节分配的程序的内存使用率。这样通常可以减少浪费的内存量，而且不会造成性能损失。&lt;/p&gt;
&lt;h4&gt;释放&lt;/h4&gt;
&lt;p&gt;像分配一样，释放也非常高效。它涉及以下步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将释放的分配大小向上取整到最接近的块大小。这是必需的，因为编译器传递给&lt;code&gt;dealloc&lt;/code&gt;的并不是&lt;code&gt;alloc&lt;/code&gt;返回的块的大小，而只是所请求的分配大小。通过在&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;中使用相同的向上取整函数，就可以确保释放的内存量始终正确。&lt;/li&gt;
&lt;li&gt;（例如从数组中）取出链表的头指针。&lt;/li&gt;
&lt;li&gt;更新头指针，将释放的块添加到链表的前端。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;值得注意的是，释放过程也不需要遍历链表。这意味着无论链表有多长，&lt;code&gt;dealloc&lt;/code&gt;调用所需的时间都是一个常量。&lt;/p&gt;
&lt;h4&gt;后备分配器&lt;/h4&gt;
&lt;p&gt;鉴于大容量分配（大于2KB）通常是很少见的，尤其是在操作系统内核中，因此对于这些分配，使用不同的分配器可能也是合理的。例如，为了减少内存浪费，我们可以转而使用链表分配器，以进行大于2048字节的分配。因为这种尺寸的分配应该非常少，所以链表也将保持较小的状态，而分配/释放也仍将保持相当快的速度。&lt;/p&gt;
&lt;h4&gt;创建新块&lt;/h4&gt;
&lt;p&gt;前面我们始终假设特定大小的链表中始终有足够多的块来满足所有分配请求。但是，有时候某种大小的块的链表中没有多余的块了。此时，有两种方法可以创建特定大小的空闲新块来满足分配请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从后备分配器中分配一个新块（如果有后备分配器的话）。&lt;/li&gt;
&lt;li&gt;从其他链表中拆分更大的块。如果块大小设置为2的幂，那么此方法将最有效。例如，一个32字节的块可以分为两个16字节的块。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在我们的实现中，将选择从后备分配器中分配新的块，因为这种实现要简单得多。&lt;/p&gt;
&lt;h3&gt;实现固定大小的块分配器&lt;/h3&gt;
&lt;p&gt;现在我们知道了固定大小的块分配器是如何工作的，就可以着手实现它了。我们将不会依赖上一节中创建的链表分配器的实现，因此即使你跳过了链表分配器的实现，也可以正常阅读这一部分。&lt;/p&gt;
&lt;h4&gt;链表节点&lt;/h4&gt;
&lt;p&gt;我们通过在新的&lt;code&gt;allocator::fixed_size_block&lt;/code&gt;模块中创建&lt;code&gt;ListNode&lt;/code&gt;类型来开始实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod fixed_size_block;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;struct ListNode {
    next: Option&amp;lt;&amp;amp;&apos;static mut ListNode&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该类型很像我们在链表分配器中实现的&lt;code&gt;ListNode&lt;/code&gt;类型，不同之处在于这里没有了第二个字段&lt;code&gt;size&lt;/code&gt;。不需要&lt;code&gt;size&lt;/code&gt;字段，是因为在固定大小的块分配器的设计思路中，同一个链表中的每个块大小都相同。&lt;/p&gt;
&lt;h4&gt;块大小&lt;/h4&gt;
&lt;p&gt;接下来，我们定义一个常量切片&lt;code&gt;BLOCK_SIZES&lt;/code&gt;，包含了实现中将会用到的块大小：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// 将会用到的块大小
///
/// 块大小必须是2的幂，因为它们也将用于块对齐
///（对齐方式必须为2的幂）
const BLOCK_SIZES: &amp;amp;[usize] = &amp;amp;[8, 16, 32, 64, 128, 256, 512, 1024, 2048];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用从8到2048的2的幂作为块大小。我们不定义任何小于8的块，因为每个块在被释放时必须能够存储指向下一个块的64位指针。对于大于2048字节的分配，我们将使用链表分配器。&lt;/p&gt;
&lt;p&gt;为了简化实现，我们定义：一个块的大小也是其在内存中所需的对齐方式。因此，一个16字节的块始终在16字节的边界上对齐，而512字节的块始终在512字节的边界上对齐。由于对齐始终需要是2的幂，因此也排除了任何其他块大小。如果将来需要的块大小不是2的幂，我们仍然可以为此调整执行方式（例如，再定义一个&lt;code&gt;BLOCK_ALIGNMENTS&lt;/code&gt;数组）。&lt;/p&gt;
&lt;h4&gt;分配器类型&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;ListNode&lt;/code&gt;类型和&lt;code&gt;BLOCK_SIZES&lt;/code&gt;切片，我们现在可以定义分配器类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct FixedSizeBlockAllocator {
    list_heads: [Option&amp;lt;&amp;amp;&apos;static mut ListNode&amp;gt;; BLOCK_SIZES.len()],
    fallback_allocator: linked_list_allocator::Heap,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;list_heads&lt;/code&gt;字段是&lt;code&gt;head&lt;/code&gt;指针的数组，每个块大小对应一个头指针。使用&lt;code&gt;BLOCK_SIZES&lt;/code&gt;切片的&lt;code&gt;len()&lt;/code&gt;作为数组长度创建数组。对于那些大于最大块尺寸的分配，我们使用&lt;code&gt;linked_list_allocator&lt;/code&gt;提供的分配器作为后备分配器。当然，也可以改用我们自己实现的&lt;code&gt;LinkedListAllocator&lt;/code&gt;，但是它并不会自动&lt;a href=&quot;./#%E5%90%88%E5%B9%B6%E7%A9%BA%E9%97%B2%E5%9D%97&quot;&gt;合并空闲块&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;为了构造一个&lt;code&gt;FixedSizeBlockAllocator&lt;/code&gt;，我们依旧提供与其他类型分配器相同的&lt;code&gt;new&lt;/code&gt;函数和&lt;code&gt;init&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl FixedSizeBlockAllocator {
    /// 创建一个空的`FixedSizeBlockAllocator`.
    pub const fn new() -&amp;gt; Self {
        const EMPTY: Option&amp;lt;&amp;amp;&apos;static mut ListNode&amp;gt; = None;
        FixedSizeBlockAllocator {
            list_heads: [EMPTY; BLOCK_SIZES.len()],
            fallback_allocator: linked_list_allocator::Heap::empty(),
        }
    }

    /// 使用给定的堆边界初始化线性分配器
    /// 
    /// 该方法为非安全，因为调用者必须保证提供的内存范围未被使用。
    /// 同时，该方法只能被调用一次。
    pub unsafe fn init(&amp;amp;mut self, heap_start: usize, heap_size: usize) {
        self.fallback_allocator.init(heap_start, heap_size);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新函数只是使用空节点初始化&lt;code&gt;list_heads&lt;/code&gt;数组，并创建一个空链表分配器作为&lt;code&gt;fallback_allocator&lt;/code&gt;。&lt;code&gt;EMPTY&lt;/code&gt;必须为常量的原因是要告诉Rust编译器我们要使用常量值初始化数组。直接将数组初始化为&lt;code&gt;[None; BLOCK_SIZES.len()]&lt;/code&gt;会有编译错误，因为之后编译器会要求&lt;code&gt;Option&amp;lt;&amp;amp;&apos;static mut ListNode&amp;gt;&lt;/code&gt;需要实现&lt;code&gt;Copy&lt;/code&gt;trait，对于这点我们无能为力。这是目前的Rust编译器所具有的局限性，在未来的编译器中可能会解决这个问题。&lt;/p&gt;
&lt;p&gt;如果你跳过了&lt;code&gt;LinkedListAllocator&lt;/code&gt;的实现一节，则还需要在&lt;code&gt;lib.rs&lt;/code&gt;的开头添加&lt;code&gt;#![feature(const_mut_refs)]&lt;/code&gt;。原因是在&lt;code&gt;const&lt;/code&gt;函数中使用可变引用类型仍然不稳定，包括&lt;code&gt;list_heads&lt;/code&gt;字段的数组元素&lt;code&gt;Option&amp;lt;&amp;amp;&apos;static mut ListNode&amp;gt;&lt;/code&gt;类型（即使我们将其设置为&lt;code&gt;None&lt;/code&gt;）也是如此。&lt;/p&gt;
&lt;p&gt;非安全的&lt;code&gt;init&lt;/code&gt;函数仅用于调用&lt;code&gt;fallback_allocator&lt;/code&gt;的&lt;a href=&quot;https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.init&quot;&gt;&lt;code&gt;init&lt;/code&gt;&lt;/a&gt;函数，而无需对&lt;code&gt;list_heads&lt;/code&gt;数组进行任何其他初始化。此后我们将在&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;调用上延迟初始化该列表。&lt;/p&gt;
&lt;p&gt;为了方便起见，我们还创建了一个私有的&lt;code&gt;fallback_alloc&lt;/code&gt;方法，该方法使用&lt;code&gt;fallback_allocator&lt;/code&gt;执行分配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use alloc::alloc::Layout;
use core::ptr;

impl FixedSizeBlockAllocator {
    /// 使用后备分配器执行分配.
    fn fallback_alloc(&amp;amp;mut self, layout: Layout) -&amp;gt; *mut u8 {
        match self.fallback_allocator.allocate_first_fit(layout) {
            Ok(ptr) =&amp;gt; ptr.as_ptr(),
            Err(_) =&amp;gt; ptr::null_mut(),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;linked_list_allocator&lt;/code&gt;crate的&lt;a href=&quot;https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html&quot;&gt;&lt;code&gt;Heap&lt;/code&gt;&lt;/a&gt;类型无法实现&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html&quot;&gt;&lt;code&gt;GlobalAlloc&lt;/code&gt;&lt;/a&gt;（因为&lt;a href=&quot;./#GlobalAlloc%E4%B8%8E%E5%8F%AF%E5%8F%98%E6%80%A7&quot;&gt;不使用锁就不可能实现&lt;/a&gt;）。而是提供了一个略有不同的接口的&lt;code&gt;allocate_first_fit&lt;/code&gt;方法。它不返回&lt;code&gt;*mut u8&lt;/code&gt;也不使用空指针来指示分配错误，而是返回&lt;code&gt;Result&amp;lt;NonNull&amp;lt;u8&amp;gt;, ()&amp;gt;&lt;/code&gt;。 &lt;a href=&quot;https://doc.rust-lang.org/nightly/core/ptr/struct.NonNull.html&quot;&gt;&lt;code&gt;NonNull&lt;/code&gt;&lt;/a&gt;类型是是对那些能够保证自己是非空指针的裸指针的抽象。通过在匹配到&lt;code&gt;Ok&lt;/code&gt;时返回&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/ptr/struct.NonNull.html#method.as_ptr&quot;&gt;&lt;code&gt;NonNull::as_ptr&lt;/code&gt;&lt;/a&gt;方法，匹配到&lt;code&gt;Err&lt;/code&gt;时返回&lt;code&gt;null&lt;/code&gt;指针，我们可以方便地将其转换回&lt;code&gt;*mut u8&lt;/code&gt;类型。&lt;/p&gt;
&lt;h4&gt;计算块大小列表索引&lt;/h4&gt;
&lt;p&gt;在实现&lt;code&gt;GlobalAlloc&lt;/code&gt;trait之前，我们定义一个&lt;code&gt;list_index&lt;/code&gt;帮助函数，用来返回适合给定&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/struct.Layout.html&quot;&gt;&lt;code&gt;Layout&lt;/code&gt;&lt;/a&gt;的最小块大小：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// 为给定的`layout`选择一个合适的块大小
///
/// 返回该块大小在`BLOCK_SIZES`中的索引
fn list_index(layout: &amp;amp;Layout) -&amp;gt; Option&amp;lt;usize&amp;gt; {
    let required_block_size = layout.size().max(layout.align());
    BLOCK_SIZES.iter().position(|&amp;amp;s| s &amp;gt;= required_block_size)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;块必须至少具有给定&lt;code&gt;Layout&lt;/code&gt;所需的大小和对齐方式。由于我们定义了块的大小也是它的对齐方式，因此这意味着&lt;code&gt;required_block_size&lt;/code&gt;应为布局的&lt;code&gt;size()&lt;/code&gt;和&lt;code&gt;align()&lt;/code&gt;属性中的较大值。为了在&lt;code&gt;BLOCK_SIZES&lt;/code&gt;切片中查找最接近向上取整的块大小，我们首先使用&lt;code&gt;iter()&lt;/code&gt;方法获取迭代器，然后使用&lt;code&gt;position()&lt;/code&gt;方法在切片中查找第一个不小于&lt;code&gt;required_block_size&lt;/code&gt;的块的索引并返回该索引。&lt;/p&gt;
&lt;p&gt;请注意，这里并不返回块大小本身，而是返回&lt;code&gt;BLOCK_SIZES&lt;/code&gt;切片的索引。原因是我们还要使用该索引作为&lt;code&gt;list_heads&lt;/code&gt;数组的索引。&lt;/p&gt;
&lt;h4&gt;为固定大小的块分配器实现&lt;code&gt;GlobalAlloc&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;最后一步是实现&lt;code&gt;GlobalAlloc&lt;/code&gt;trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use super::Locked;
use alloc::alloc::GlobalAlloc;

unsafe impl GlobalAlloc for Locked&amp;lt;FixedSizeBlockAllocator&amp;gt; {
    unsafe fn alloc(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8 {
        todo!();
    }

    unsafe fn dealloc(&amp;amp;self, ptr: *mut u8, layout: Layout) {
        todo!();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同其他分配器一样，我们仍不直接为分配器类型实现&lt;code&gt;GlobalAlloc&lt;/code&gt;trait，而是使用&lt;a href=&quot;https://os.phil-opp.com/allocator-designs/#a-locked-wrapper-type&quot;&gt;&lt;code&gt;Locked&lt;/code&gt;封装&lt;/a&gt;为其添加同步的内部可变性。由于&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;的实现相对复杂，因此我们将在接下来的文章中逐一介绍它们。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;alloc&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;alloc&lt;/code&gt;方法的实现如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsafe fn alloc(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8 {
    let mut allocator = self.lock();
    match list_index(&amp;amp;layout) {
        Some(index) =&amp;gt; {
            match allocator.list_heads[index].take() {
                Some(node) =&amp;gt; {
                    allocator.list_heads[index] = node.next.take();
                    node as *mut ListNode as *mut u8
                }
                None =&amp;gt; {
                    // 链表中没有多余的块了，于是创建并分配新块
                    let block_size = BLOCK_SIZES[index];
                    // 这只在所有块大小均为2的幂时有效
                    let block_align = block_size;
                    let layout = Layout::from_size_align(block_size, block_align)
                        .unwrap();
                    allocator.fallback_alloc(layout)
                }
            }
        }
        None =&amp;gt; allocator.fallback_alloc(layout),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们来一步一步看：&lt;/p&gt;
&lt;p&gt;首先调用&lt;code&gt;Locked::lock&lt;/code&gt;方法获取对封装中的分配器实例的可变引用。然后调用刚刚定义的&lt;code&gt;list_index&lt;/code&gt;函数来计算合适于给定布局的块大小，并尝试使用块大小索引从list_heads数组中取出对应链表的头指针。如果列表索引为&lt;code&gt;None&lt;/code&gt;，则说明这次分配没有合适的块大小，于是使用&lt;code&gt;fallback_alloc&lt;/code&gt;函数调用&lt;code&gt;fallback_allocator&lt;/code&gt;进行分配。&lt;/p&gt;
&lt;p&gt;如果列表索引为&lt;code&gt;Some&lt;/code&gt;，就尝试使用&lt;a href=&quot;https://doc.rust-lang.org/core/option/enum.Option.html#method.take&quot;&gt;&lt;code&gt;Option::take&lt;/code&gt;&lt;/a&gt;方法取出（译注：获取并移除）&lt;code&gt;list_heads[index]&lt;/code&gt;所在的链表的第一个节点&lt;code&gt;node&lt;/code&gt;。如果该链表不为空，则进入&lt;code&gt;match&lt;/code&gt;语句的&lt;code&gt;Some(node)&lt;/code&gt;分支，将该链表的头指针指向&lt;code&gt;node&lt;/code&gt;的后继节点（也是使用&lt;a href=&quot;https://doc.rust-lang.org/core/option/enum.Option.html#method.take&quot;&gt;&lt;code&gt;take&lt;/code&gt;&lt;/a&gt;）。最后，我们将取出的&lt;code&gt;node&lt;/code&gt;以&lt;code&gt;*mut u8&lt;/code&gt;指针的形式返回。&lt;/p&gt;
&lt;p&gt;如果链表头指针为&lt;code&gt;None&lt;/code&gt;，则表示该块大小对应的链表为空。这意味着我们需要按照&lt;a href=&quot;./#%E5%88%9B%E5%BB%BA%E6%96%B0%E5%9D%97&quot;&gt;上面&lt;/a&gt;描述的那样创建一个新块。为此，我们首先从&lt;code&gt;BLOCK_SIZES&lt;/code&gt;切片中获取该块大小的具体值，并将其用作新块的大小和对齐方式，再从中创建一个新的&lt;code&gt;Layout&lt;/code&gt;，并调用&lt;code&gt;fallback_alloc&lt;/code&gt;方法执行分配。之所以需要调整布局和对齐方式，是因为该块将在释放时会被添加到相应的块链表中。&lt;/p&gt;
&lt;h4&gt;&lt;code&gt;dealloc&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;dealloc&lt;/code&gt;方法的实现如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::{mem, ptr::NonNull};

// 在`unsafe impl GlobalAlloc`内

unsafe fn dealloc(&amp;amp;self, ptr: *mut u8, layout: Layout) {
    let mut allocator = self.lock();
    match list_index(&amp;amp;layout) {
        Some(index) =&amp;gt; {
            let new_node = ListNode {
                next: allocator.list_heads[index].take(),
            };
            // 为存储的节点验证块大小和块对齐方式
            // verify that block has size and alignment required for storing node
            assert!(mem::size_of::&amp;lt;ListNode&amp;gt;() &amp;lt;= BLOCK_SIZES[index]);
            assert!(mem::align_of::&amp;lt;ListNode&amp;gt;() &amp;lt;= BLOCK_SIZES[index]);
            let new_node_ptr = ptr as *mut ListNode;
            new_node_ptr.write(new_node);
            allocator.list_heads[index] = Some(&amp;amp;mut *new_node_ptr);
        }
        None =&amp;gt; {
            let ptr = NonNull::new(ptr).unwrap();
            allocator.fallback_allocator.deallocate(ptr, layout);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似&lt;code&gt;alloc&lt;/code&gt;中的操作，首先调用&lt;code&gt;lock&lt;/code&gt;方法获取分配器的可变引用，然后使用&lt;code&gt;list_index&lt;/code&gt;函数获取与给定&lt;code&gt;Layout&lt;/code&gt;大小相适应的块链表。如果索引为&lt;code&gt;None&lt;/code&gt;，则&lt;code&gt;BLOCK_SIZES&lt;/code&gt;中并没有合适的块大小，这说明该分配是由后备分配器创建的。因此，这里也应调用后备分配器的&lt;a href=&quot;https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.deallocate&quot;&gt;&lt;code&gt;deallocate&lt;/code&gt;&lt;/a&gt;来释放内存。该方法期望使用&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/ptr/struct.NonNull.html&quot;&gt;&lt;code&gt;NonNull&lt;/code&gt;&lt;/a&gt;而不是&lt;code&gt;*mut u8&lt;/code&gt;，因此我们需要做指针转换。（仅当指针为空时，调用&lt;code&gt;unwrap&lt;/code&gt;才会失败，而在编译器调用&lt;code&gt;dealloc&lt;/code&gt;时这不可能发生。）&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;list_index&lt;/code&gt;返回一个块大小的索引，则需要将已释放的内存块添加到相应块大小的链表中。为此，我们首先创建一个指向当前链表头的新&lt;code&gt;ListNode&lt;/code&gt;（仍然使用&lt;code&gt;Option::take&lt;/code&gt;）。在将新节点写入释放的内存块之前，我们首先断言由&lt;code&gt;index&lt;/code&gt;指定的块大小具有存储&lt;code&gt;ListNode&lt;/code&gt;所需的大小和对齐方式。然后，我们通过将参数给定的&lt;code&gt;*mut u8&lt;/code&gt;指针转换为&lt;code&gt;*mut ListNode&lt;/code&gt;指针，并在其上调用非安全的&lt;code&gt;write&lt;/code&gt;方法来将&lt;code&gt;new_node&lt;/code&gt;写入内存块。最后一步是将列表的头部指针指向新建的&lt;code&gt;ListNode&lt;/code&gt;，该链表的目前的指针为&lt;code&gt;None&lt;/code&gt;，因为我们在前面使用&lt;code&gt;take&lt;/code&gt;取走了原指针而留下一个&lt;code&gt;None&lt;/code&gt;。为此，我们将裸指针&lt;code&gt;new_node_ptr&lt;/code&gt;转换为一个可变引用。&lt;/p&gt;
&lt;p&gt;这里有几点值得注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们不区分某个块究竟是从块链表分配的，还是从后备分配器中分配。这意味着将在&lt;code&gt;alloc&lt;/code&gt;中创建的新块在&lt;code&gt;dealloc&lt;/code&gt;时能够添加到相应的块链表中，从而增加该块大小链表中所包含的块数。&lt;/li&gt;
&lt;li&gt;在我们的实现中，&lt;code&gt;alloc&lt;/code&gt;方法是唯一能够创建新块的地方。这意味着我们初始化时仅有一系列空的块链表，且仅在执行针对特定块大小的分配时，才惰性填充相应的链表。&lt;/li&gt;
&lt;li&gt;即使我们执行了一些&lt;code&gt;unsafe&lt;/code&gt;操作，我们也不需要在&lt;code&gt;alloc&lt;/code&gt;和&lt;code&gt;dealloc&lt;/code&gt;中使用&lt;code&gt;unsafe&lt;/code&gt;块。原因是Rust目前将整个非安全函数的函数体视为一个大的&lt;code&gt;unsafe&lt;/code&gt;块。由于使用显式&lt;code&gt;unsafe&lt;/code&gt;块的优点是很能够明显指出哪些操作是非安全的哪些操是安全的，因此已经有一个&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/2585&quot;&gt;RFC提案&lt;/a&gt;讨论更改Rust当前的这种行为。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;使用固定大小的块分配器&lt;/h3&gt;
&lt;p&gt;要使用新的&lt;code&gt;FixedSizeBlockAllocator&lt;/code&gt;，我们需要在&lt;code&gt;allocator&lt;/code&gt;模块中修改静态变量&lt;code&gt;ALLOCATOR&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use fixed_size_block::FixedSizeBlockAllocator;

#[global_allocator]
static ALLOCATOR: Locked&amp;lt;FixedSizeBlockAllocator&amp;gt; = Locked::new(
    FixedSizeBlockAllocator::new());
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;init&lt;/code&gt;函数在我们实现的所有分配器中均具有相同的行为，因此无需修改&lt;code&gt;init_heap&lt;/code&gt;中的&lt;code&gt;init&lt;/code&gt;调用。&lt;/p&gt;
&lt;p&gt;现在，当我们再次运行&lt;code&gt;heap_allocation&lt;/code&gt;测试时，所有测试仍应通过：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test --test heap_allocation
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]
many_boxes_long_lived... [ok]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个新分配器看起来也能够正常工作！&lt;/p&gt;
&lt;h3&gt;关于固定大小的块分配器的讨论&lt;/h3&gt;
&lt;p&gt;尽管固定大小的块分配器性能好于链表分配器，但是当使用2的幂作为块大小时，它可能会浪费了多达一半的内存。至于这种权衡是否值得，在很大程度上取决于应用程序类型。对于性能至关重要的操作系统内核而言，固定大小的块分配器似乎是更好的选择。&lt;/p&gt;
&lt;p&gt;在实现方面，我们可以在当前实现的基础上继续进行多项改进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与其使用后备分配器惰性分配块，不如在初始化时预先填充各链表，以提高初始的分配的性能。&lt;/li&gt;
&lt;li&gt;为了简化实现，我们只允许块大小为2的幂，以便我们也可以将快大小也当做块对齐方式使用。通过以不同方式存储（或计算）对齐方式，我们还可以允许任意其他块大小。如此，我们就可以增加更多的块大小，例如为常见的分配建立块大小链表，以最大程度地减少内存浪费。&lt;/li&gt;
&lt;li&gt;我们目前仅创建新的块，但不再释放它们，这将产生块碎片，最终可能会导致在进行大型分配时分配失败。为每个块大小强制设置最大链表长度可能也是合理的。当达到最大长度时，不应继续将其添加到链表中，而应当使用后备分配器直接将其彻底释放。&lt;/li&gt;
&lt;li&gt;我们可以使用一个特殊的分配器来分配大于4KiB的内存，而不是使用后备的链表分配器。这个思路是利用大小恰好为4KiB的&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/&quot;&gt;内存分页&lt;/a&gt;，将连续的虚拟内存块映射到非连续的物理帧。这样，未使用的内存碎片对于大型分配来说就不再是问题。&lt;/li&gt;
&lt;li&gt;使用这样的页面分配器，可能有必要将块大小的上限提高到4KiB，并完全弃用链表分配器。这样的主要优点是减少了碎片并提高了性能可预测性，即在非理想情况下也能获取较好的性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;要注意，上面提出的这些仅仅是改进建议。操作系统内核中使用的分配器通常会针对内核的特定工作负载进行高度优化，这只有通过广泛的性能分析才能实现。&lt;/p&gt;
&lt;h3&gt;其他分配器变体&lt;/h3&gt;
&lt;p&gt;固定大小的块分配器设计也有很多变体。&lt;em&gt;slab分配器&lt;/em&gt;和&lt;em&gt;伙伴分配器&lt;/em&gt;是两个流行的示例，它们也用在诸如Linux之类的流行内核中。下面，我们对这两种设计进行简短介绍。&lt;/p&gt;
&lt;h4&gt;Slab分配器&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Slab_allocation&quot;&gt;Slab分配器&lt;/a&gt;的思路是使用直接使用内核选定类型的大小作文的块大小。这样，对于这些类型的分配将恰好适合块大小，且不会浪费内存。有时，甚至有可能在未使用的块中预先初始化某些类型实例，以进一步提高性能。&lt;/p&gt;
&lt;p&gt;Slab分配器通常与其他分配器结合使用。例如，它可以与固定大小的块分配器一起使用，以进一步拆分分配的块，从而减少内存浪费。此外，它还经常用于在单个大型分配时实现&lt;a href=&quot;https://en.wikipedia.org/wiki/Object_pool_pattern&quot;&gt;对象池模式&lt;/a&gt;。&lt;/p&gt;
&lt;h4&gt;伙伴分配器&lt;/h4&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Buddy_memory_allocation&quot;&gt;伙伴分配器&lt;/a&gt;不是使用链表，而是使用&lt;a href=&quot;https://en.wikipedia.org/wiki/Binary_tree&quot;&gt;二叉树&lt;/a&gt;来管理释放的块，同时配合使用2的幂作为块大小。当需要一定大小的新块时，它将一个较大的块分成两半，从而在树中创建两个子节点。每当释放一个块时，都会分析树中的邻居块。如果邻居也是空闲块，则将这两个块合并，重新成为双倍大小的块。&lt;/p&gt;
&lt;p&gt;此合并过程的优点是减少了&lt;a href=&quot;https://en.wikipedia.org/wiki/Fragmentation_(computing)#External_fragmentation&quot;&gt;外部碎片&lt;/a&gt;，于是那些较小的释放块就可以重新用于较大的分配了。此外，它无需使用后备分配器，因此其性能更加可预测。它最大的缺点是只能使用2幂作为块大小，这可能会由于&lt;a href=&quot;https://en.wikipedia.org/wiki/Fragmentation_(computing)#Internal_fragmentation&quot;&gt;内部碎片&lt;/a&gt;而导致大量的内存浪费。因此，伙伴分配器通常与slab分配器一起使用，以将分配的块进一步拆分为多个较小的块。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;这篇文章概述了不同的分配器设计。我们学习了如何实现基本的&lt;a href=&quot;https://os.phil-opp.com/allocator-designs/#bump-allocator&quot;&gt;线性分配器&lt;/a&gt;，它通过增加单个&lt;code&gt;next&lt;/code&gt;指针来线性分配内存。虽然线性分配非常快，但是只有释放所有分配之后，它才能重新使用内存。因此，线性分配器很少用作全局分配器。&lt;/p&gt;
&lt;p&gt;接下来，我们创建了一个&lt;a href=&quot;https://os.phil-opp.com/allocator-designs/#linked-list-allocator&quot;&gt;链表分配器&lt;/a&gt;，该分配器使用释放的内存块本身来存放节点并组成链表，即所谓的&lt;a href=&quot;https://en.wikipedia.org/wiki/Free_list&quot;&gt;空闲链表&lt;/a&gt;。该链表可以存储任意数量的大小不同的已释放块。尽管不会发生内存浪费，但是由于分配请求可能需要遍历整个列表，因此这种方法的性能很差。同时，我们的实现还遭受&lt;a href=&quot;https://en.wikipedia.org/wiki/Fragmentation_(computing)#External_fragmentation&quot;&gt;外部碎片&lt;/a&gt;的困扰，因为这个最小化的实现并不会将相邻的释放块重新合并在一起。&lt;/p&gt;
&lt;p&gt;为了解决链表方法的性能问题，我们创建了一个&lt;a href=&quot;https://os.phil-opp.com/allocator-designs/#fixed-size-block-allocator&quot;&gt;固定大小的块分配器&lt;/a&gt;，该分配器预定义了一组固定的块大小。并为每个块大小设置一个单独的&lt;a href=&quot;https://en.wikipedia.org/wiki/Free_list&quot;&gt;空闲链表&lt;/a&gt;，因此分配与释放只需要在相应列表的前端执行插入/取出，因此速度非常快。由于每个分配都向上取整到最接近的块大小，所以会因为&lt;a href=&quot;https://en.wikipedia.org/wiki/Fragmentation_(computing)#Internal_fragmentation&quot;&gt;内部碎片&lt;/a&gt;而浪费一些内存。&lt;/p&gt;
&lt;p&gt;还有更多具有权衡不同取舍的分配器设计。Slab分配器可以为常见的固定大小的结构优化出更好地分配，但并非在所有情况下都适用。伙伴分配器使用二叉树将释放的块合并回去，但是可能会浪费大量内存，因为它仅支持的块大小只能为2的幂。重要的是要记住，每一个内核实现都有一个独特的工作量，因此并没有一个对所有情况都能保持“最佳”的分配器设计。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;目前，我就使用本文作为内存管理实现的尾声。接下来，我们将从&lt;a href=&quot;https://en.wikipedia.org/wiki/Thread_(computing)&quot;&gt;线程&lt;/a&gt;开始探索对&lt;a href=&quot;https://en.wikipedia.org/wiki/Computer_multitasking&quot;&gt;&lt;em&gt;多任务&lt;/em&gt;&lt;/a&gt;的支持。在随后的文章中，我们将探讨&lt;a href=&quot;https://en.wikipedia.org/wiki/Multiprocessing&quot;&gt;多进程&lt;/a&gt;、&lt;a href=&quot;https://en.wikipedia.org/wiki/Process_(computing)&quot;&gt;进程&lt;/a&gt;、以及&lt;a href=&quot;https://rust-lang.github.io/async-book/01_getting_started/04_async_await_primer.html&quot;&gt;async/await&lt;/a&gt;形式的协作式多任务处理。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 3.3 - 内存堆分配</title><link>https://zlotus.github.io/2021/04/01/writing-an-os-in-rust-3.3/</link><guid isPermaLink="true">https://zlotus.github.io/2021/04/01/writing-an-os-in-rust-3.3/</guid><description>使用Rust编写操作系统 - 3.3 - 内存堆分配</description><pubDate>Thu, 01 Apr 2021 17:55:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/&quot;&gt;Heap Allocation&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;本文将为内核添加对堆分配的支持。首先将介绍动态内存机制，并展示Rust借用检查器如何防止常见的分配错误。然后将实现Rust的基础分配器接口，创建一个堆内存区域，并编写一个分配器crate。在本文结束时，内核将能够支持Rust内置&lt;code&gt;alloc&lt;/code&gt;crate中的所有分配和收集类型。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/heap-allocation/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-10&quot;&gt;post-10&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;局部变量和静态变量&lt;/h2&gt;
&lt;p&gt;当前，我们在内核中使用两种类型的变量：局部变量和&lt;code&gt;static&lt;/code&gt;变量。局部变量存储在&lt;a href=&quot;https://en.wikipedia.org/wiki/Call_stack&quot;&gt;调用栈&lt;/a&gt;中，并且仅在其所在的函数返回之前有效。静态变量存储在固定的内存位置，并且在程序的整个生命周期中始终有效。&lt;/p&gt;
&lt;h3&gt;局部变量&lt;/h3&gt;
&lt;p&gt;局部变量存储在&lt;a href=&quot;https://en.wikipedia.org/wiki/Call_stack&quot;&gt;调用栈&lt;/a&gt;中，这是一个支持&lt;code&gt;push&lt;/code&gt;和&lt;code&gt;pop&lt;/code&gt;操作的&lt;a href=&quot;https://en.wikipedia.org/wiki/Stack_(abstract_data_type)&quot;&gt;栈数据结构&lt;/a&gt;。对于每个被调用的函数，编译器都将其参数、返回地址和局部变量压栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/heap-allocation/call-stack.svg&quot; alt=&quot;调用栈&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中的例子展示了&lt;code&gt;outer&lt;/code&gt;函数调用了&lt;code&gt;inner&lt;/code&gt;函数之后的调用栈。可以看到调用栈先是包含了&lt;code&gt;outer&lt;/code&gt;函数的局部变量。在&lt;code&gt;inner&lt;/code&gt;函数被调用时，参数&lt;code&gt;1&lt;/code&gt;和返回地址被压栈。接下来控制权转给了&lt;code&gt;inner&lt;/code&gt;函数，并继续将其局部变量压栈。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;inner&lt;/code&gt;函数返回后，其位于调用栈中的相关部分弹栈，此时调用栈中仅剩下&lt;code&gt;outer&lt;/code&gt;函数相关的局部变量：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/heap-allocation/call-stack-return.svg&quot; alt=&quot;调用栈返回&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到&lt;code&gt;inner&lt;/code&gt;的局部变量仅在函数返回之前有效。当我们持有变量时间过长时——比如尝试返回对局部变量的引用时——Rust编译器就会强行生命周期并引发编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn inner(i: usize) -&amp;gt; &amp;amp;&apos;static u32 {
    let z = [1, 2, 3];
    &amp;amp;z[i]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=6186a0f3a54f468e1de8894996d12819&quot;&gt;使用play rust在线运行上面的代码&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;虽然本例中返回引用并无意义，但在某些情况下，我们希望变量比其所在函数的生命周期更长。我们已经在编写内核的过程中遇到了这种情况，比如在尝试&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/#loading-the-idt&quot;&gt;加载中断描述符表&lt;/a&gt;时就不得不使用&lt;code&gt;static&lt;/code&gt;变量来延长其生命周期。&lt;/p&gt;
&lt;h3&gt;静态变量&lt;/h3&gt;
&lt;p&gt;静态变量存储在栈以外的固定内存位置中。此存储位置由链接器在编译时分配并编码在可执行文件中。静态变量在程序的完整运行时内始终有效，因此它们具有&lt;code&gt;&apos;static&lt;/code&gt;生命周期，并且始终可以被局部变量引用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/heap-allocation/call-stack-static.svg&quot; alt=&quot;调用栈和静态变量&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中的&lt;code&gt;inner&lt;/code&gt;函数返回时，其在调用栈中的相关部分被析构。而静态变量位于一个独立的内存范围中，并不会被析构，因此&lt;code&gt;&amp;amp;Z[1]&lt;/code&gt;的引用在函数返回后仍然有效。&lt;/p&gt;
&lt;p&gt;除了&lt;code&gt;&apos;static&lt;/code&gt;生命周期以外，静态变量还有另一个有用的属性，即其内存位置在编译时就已确定，因此不需要引用就能够对其进行访问。我们利用该属性实现了&lt;code&gt;println&lt;/code&gt;：通过在内部使用&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/#a-global-interface&quot;&gt;静态&lt;code&gt;Writer&lt;/code&gt;&lt;/a&gt;，即便不使用&lt;code&gt;&amp;amp;mut Writer&lt;/code&gt;引用也能够调用该宏。这在&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/#implementation&quot;&gt;异常处理程序&lt;/a&gt;中非常有用，因为在其中我们无法访问任何其他变量。&lt;/p&gt;
&lt;p&gt;但是，静态变量的这个属性也带来了一个致命缺点：它们默认是只读的。Rust强制此规则是为了避免&lt;a href=&quot;https://doc.rust-lang.org/nomicon/races.html&quot;&gt;数据竞争&lt;/a&gt;，比如当两个线程同时修改一个静态变量时。修改静态变量的唯一方法是将其封装在&lt;a href=&quot;https://docs.rs/spin/0.5.2/spin/struct.Mutex.html&quot;&gt;&lt;code&gt;Mutex&lt;/code&gt;&lt;/a&gt;类型中，从而确保在任何时刻中仅存在一个&lt;code&gt;&amp;amp;mut&lt;/code&gt;引用。我们已经将&lt;code&gt;Mutex&lt;/code&gt;用于&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/#spinlocks&quot;&gt;静态VGA缓冲区&lt;code&gt;Writer&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;动态内存&lt;/h2&gt;
&lt;p&gt;结合使用局部变量和静态变量已经能够实现非常强大的功能，足以应付大多数用例了。但是，它们仍然具有一定的局限性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;局部变量仅其所在的函数或块结束前有效。这是因为它们存在于调用栈中，并在上下文函数返回后被析构。&lt;/li&gt;
&lt;li&gt;静态变量在程序的完整运行时内始终有效，因此无法在不再需要它们时回收和重用它们的内存。此外，它们的所有权语义不明确，且能够被任意函数访问，所以我们才会在需要修改时使用&lt;code&gt;Mutex&lt;/code&gt;对其进行保护。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;局部变量和静态变量的另一个局限性，就是它们的大小固定，因此，遇到需要添加更多元素的情况时，它们将无法存储这些动态增长的集合。（Rust中有一些关于&lt;a href=&quot;https://github.com/rust-lang/rust/issues/48055&quot;&gt;unsized rvalues&lt;/a&gt;的提案，以允许局部变量具有动态大小，但它们仅在某些特定情况下有效。）&lt;/p&gt;
&lt;p&gt;为了避开这些缺点，编程语言通常会提供第三块用于存放变量的内存区域，称为&lt;strong&gt;堆&lt;/strong&gt;。堆使用两个叫做&lt;code&gt;allocate&lt;/code&gt;和&lt;code&gt;deallocate&lt;/code&gt;的函数在运行时动态内存分配。其工作方式为：&lt;code&gt;allocate&lt;/code&gt;函数返回指定大小的可用内存块，用于存储变量。然后，该变量会一直有效，直到在其引用上调用&lt;code&gt;deallocate&lt;/code&gt;函数将其释放为止。&lt;/p&gt;
&lt;p&gt;让我们来看一个例子：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/heap-allocation/call-stack-heap.svg&quot; alt=&quot;调用栈和堆&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在这里，&lt;code&gt;inner&lt;/code&gt;函数使用堆内存而不是静态变量来存储&lt;code&gt;z&lt;/code&gt;。首先按照所需大小分配内存块，然后返回&lt;code&gt;*mut u32&lt;/code&gt;&lt;a href=&quot;https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer&quot;&gt;裸指针&lt;/a&gt;。然后使用&lt;a href=&quot;https://doc.rust-lang.org/core/ptr/fn.write.html&quot;&gt;&lt;code&gt;ptr::write&lt;/code&gt;&lt;/a&gt;方法写入数组&lt;code&gt;[1,2,3]&lt;/code&gt;。最后使用&lt;a href=&quot;https://doc.rust-lang.org/std/primitive.pointer.html#method.offset&quot;&gt;&lt;code&gt;offset&lt;/code&gt;&lt;/a&gt;函数计算指向第&lt;code&gt;i&lt;/code&gt;个元素的指针，并将其返回。（请注意，为简洁起见，在此示例函数中，我们省略了一些必需的强制转换和&lt;code&gt;unsafe&lt;/code&gt;块。）&lt;/p&gt;
&lt;p&gt;分配的内存将会一直有效，直到通过调用&lt;code&gt;dealloc&lt;/code&gt;显式释放为止。因此，即使在&lt;code&gt;inner&lt;/code&gt;返回且其相关部分的调用栈被析构之后，返回的指针依然有效。与静态内存相比，使用堆内存的优势在于内存可以在释放后重用，这是通过&lt;code&gt;outer&lt;/code&gt;的&lt;code&gt;deallocate&lt;/code&gt;调用实现的。调用结束后的情形如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/heap-allocation/call-stack-heap-freed.svg&quot; alt=&quot;释放后的调用栈和堆&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到&lt;code&gt;z[1]&lt;/code&gt;所在堆中的位置又空闲了，可以重新用于下一次&lt;code&gt;allocate&lt;/code&gt;调用。但是，还可以看到&lt;code&gt;z[0]&lt;/code&gt;和&lt;code&gt;z[2]&lt;/code&gt;从未释放，因为我们从未释放过它们。这种错误称为&lt;em&gt;内存泄漏&lt;/em&gt;，也通常是导致程序过度消耗内存的原因（试想一下，当我们在循环中重复调用&lt;code&gt;inner&lt;/code&gt;时会发生什么）。这看起来很不好，然而动态分配可能会导致更多种危险的错误。&lt;/p&gt;
&lt;h3&gt;常见错误&lt;/h3&gt;
&lt;p&gt;内存泄漏虽然会导致过度消耗内存，但并不会使程序更容易受到攻击。除此之外，还有两种常见的错误类型，其后果更为严重：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当我们在调用&lt;code&gt;deallocate&lt;/code&gt;后意外地继续使用变量时，便产生了一个称作&lt;strong&gt;释放后使用&lt;/strong&gt;的漏洞。这种漏洞会导致未定义的行为，此外，攻击者也经常通过该漏洞来执行任意代码。&lt;/li&gt;
&lt;li&gt;当我们不小心两次释放变量时，便产生了一个&lt;strong&gt;双重释放&lt;/strong&gt;漏洞。这是有问题的，因为它这可能会释放第一次调用&lt;code&gt;deallocate&lt;/code&gt;后在该位置重新分配的变量。因此，它可能再次导致释放后使用漏洞。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些种类的漏洞是很常见的，看起来可以期望人们现在已经学会了如何规避它们。现实很遗憾，这些漏洞仍然经常出现，例如，近日&lt;a href=&quot;https://securityboulevard.com/2019/02/linux-use-after-free-vulnerability-found-in-linux-2-6-through-4-20-11/&quot;&gt;Linux中出现的释放后使用漏洞&lt;/a&gt;允许执行任意代码。这也表明即使是最优秀的程序员也不一定总是能够正确处理复杂项目中的动态内存。&lt;/p&gt;
&lt;p&gt;为了规避这些漏洞，许多语言——例如Java或Python——都使用称为&lt;a href=&quot;https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)&quot;&gt;&lt;em&gt;垃圾回收&lt;/em&gt;&lt;/a&gt;的技术自动管理动态内存。其意图是使程序员不再需要手动调用&lt;code&gt;deallocate&lt;/code&gt;，转而使程序定期暂停并扫描未使用的堆变量，然后将它们自动释放。于是，上述漏洞也就不会再出现了。不过，其缺点是常规扫描有一定的性能开销，以及暂停的时间可能会较长。&lt;/p&gt;
&lt;p&gt;针对此问题Rust采用了不同的解决方案：它使用一种称为&lt;a href=&quot;https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html&quot;&gt;&lt;em&gt;所有权&lt;/em&gt;&lt;/a&gt;的概念，能够在编译时检查动态内存操作的正确性。因此，Rust不需要垃圾收集也能避免上述漏洞，这意味着没有性能开销。该方案的另一个优点是，程序员仍然可以像使用C或C++一样对动态内存进行细粒度的控制。&lt;/p&gt;
&lt;h3&gt;Rust中的堆分配&lt;/h3&gt;
&lt;p&gt;Rust标准库提供了隐式调用&lt;code&gt;allocate&lt;/code&gt;和&lt;code&gt;deallocate&lt;/code&gt;抽象类型，省去程序员显示调用这些函数的麻烦。其中最重要的类型是&lt;a href=&quot;https://doc.rust-lang.org/std/boxed/index.html&quot;&gt;&lt;code&gt;Box&lt;/code&gt;&lt;/a&gt;，它是堆分配值的抽象。该类提供了构造函数&lt;a href=&quot;https://doc.rust-lang.org/alloc/boxed/struct.Box.html#method.new&quot;&gt;&lt;code&gt;Box::new&lt;/code&gt;&lt;/a&gt;，构造函数接受一个值并使用该值的大小调用&lt;code&gt;allocate&lt;/code&gt;，再将该值移动到堆上新分配的空间中。为了能够释放堆内存，&lt;code&gt;Box&lt;/code&gt;类型实现了&lt;code&gt;Drop&lt;/code&gt;trait，以在变量离开作用域时调用&lt;code&gt;deallocate&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    let z = Box::new([1,2,3]);
    […]
} // z离开作用域后，`deallocate`将被调用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此模式具有一个奇怪的名字——&lt;a href=&quot;https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization&quot;&gt;资源获取即初始化&lt;/a&gt;（或简称RAII）。它起源于C++，用于实现类似的抽象类型&lt;a href=&quot;https://en.cppreference.com/w/cpp/memory/unique_ptr&quot;&gt;&lt;code&gt;std::unique_ptr&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;单靠这种类型不足以防止所有的释放后使用漏洞，因为在&lt;code&gt;Box&lt;/code&gt;离开作用域并释放相应的堆内存空间之后，程序员仍然可以保留对其的引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let x = {
    let z = Box::new([1,2,3]);
    &amp;amp;z[1]
}; // z离开作用域后，`deallocate`将被调用
println!(&quot;{}&quot;, x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而这便是Rust所有权机制大放异彩之处。所有权会为每个引用分配一个抽象的&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html&quot;&gt;生命周期&lt;/a&gt;，就是该引用的有效的范围。在上例中，&lt;code&gt;x&lt;/code&gt;引用来自于&lt;code&gt;z&lt;/code&gt;数组，因此在&lt;code&gt;z&lt;/code&gt;离开作用域后将变得无效。在&lt;a href=&quot;https://play.rust-lang.org/?version=stable&amp;amp;mode=debug&amp;amp;edition=2018&amp;amp;gist=28180d8de7b62c6b4a681a7b1f745a48&quot;&gt;线运行上例&lt;/a&gt;时，您将看到Rust编译器的确出现了编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0597]: `z[_]` does not live long enough
 --&amp;gt; src/main.rs:4:9
  |
2 |     let x = {
  |         - borrow later stored here
3 |         let z = Box::new([1,2,3]);
4 |         &amp;amp;z[1]
  |         ^^^^^ borrowed value does not live long enough
5 |     }; // z goes out of scope and `deallocate` is called
  |     - `z[_]` dropped here while still borrowed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时看这些术语可能会有些混乱。在Rust中，引用一个值称为&lt;em&gt;借用&lt;/em&gt;值，因为它类似于现实生活中的借东西：你可以临时访问某个对象，但需要在某个时刻将其归还，而且一定不可以析构它。通过确保所有借用在其原对象被析构之前生命周期便已结束，Rust编译器可以保证不会发生释放后使用的情况。&lt;/p&gt;
&lt;p&gt;Rust的所有权机制非常强大，不仅可以防止释放后使用的漏洞，而且能够像Java或Python这样的垃圾收集语言一样提供完整的&lt;a href=&quot;https://en.wikipedia.org/wiki/Memory_safety&quot;&gt;&lt;em&gt;内存安全&lt;/em&gt;&lt;/a&gt;。另外，它还能保证&lt;a href=&quot;https://en.wikipedia.org/wiki/Thread_safety&quot;&gt;&lt;em&gt;线程安全&lt;/em&gt;&lt;/a&gt;，因此其多线程代码比这些垃圾收集语言的更安全。最重要的是，所有这些检查都在编译时进行，因此与C语言中的手写内存管理相比一样没有运行时开销。&lt;/p&gt;
&lt;h3&gt;用例&lt;/h3&gt;
&lt;p&gt;现在我们知道了Rust中动态内存分配的基础知识，但是要在什么时候应该使用动态内存呢？我们的内核在没有动态内存分配情况下也已经走得很远了，那么为什么现在又需要动态内存呢？&lt;/p&gt;
&lt;p&gt;不过，动态内存分配总是会带来一些性能开销，因为我们需要为每一个分配在堆上找到一个尚未使用的位置。因此，通常情况下尽可能使用局部变量，尤其是在对性能要求极高的内核代码中。但是，在某些情况下，动态内存分配才是最佳选择。&lt;/p&gt;
&lt;p&gt;作为基本规则，具有动态生命周期或可变大小的变量需要动态内存。动态生命周期最重要的类型是&lt;a href=&quot;https://doc.rust-lang.org/alloc/rc/index.html&quot;&gt;&lt;strong&gt;&lt;code&gt;Rc&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;，它对其内部封装值的引用进行计数，并在所有引用均超出作用域后将内部封装值释放。具有可变大小的类型的示例包括&lt;a href=&quot;https://doc.rust-lang.org/alloc/vec/index.html&quot;&gt;&lt;strong&gt;&lt;code&gt;Vec&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/alloc/string/index.html&quot;&gt;&lt;strong&gt;&lt;code&gt;String&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;和其他&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/index.html&quot;&gt;集合类型&lt;/a&gt;，这些集合类型在添加更多元素时将会动态增长。这些类型的工作原理是，当集合装满时重新分配更大的内存，再将所有元素复制过来，最后取消旧的内存分配。&lt;/p&gt;
&lt;p&gt;在内核中，我们非常需要集合类型，比如后文中在实现多任务处理时，用于存储活动任务的列表。&lt;/p&gt;
&lt;h2&gt;分配器接口&lt;/h2&gt;
&lt;p&gt;实现堆分配器的第一步是添加对内置&lt;a href=&quot;https://doc.rust-lang.org/alloc/&quot;&gt;&lt;code&gt;alloc&lt;/code&gt;&lt;/a&gt; crate的依赖。像&lt;a href=&quot;https://doc.rust-lang.org/core/&quot;&gt;&lt;code&gt;core&lt;/code&gt;&lt;/a&gt; crate一样，它也是标准库的子集，同时它还包含分配器和集合类型。要添加对&lt;code&gt;alloc&lt;/code&gt;的依赖需要将以下内容添加到&lt;code&gt;lib.rs&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern crate alloc;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与添加普通依赖不同，我们并不需要修改&lt;code&gt;Cargo.toml&lt;/code&gt;。原因是&lt;code&gt;alloc&lt;/code&gt;crate与Rust编译器一起作为标准库的一部分供我们使用，因此编译器已经知道了该crate。通过添加此&lt;code&gt;extern crate&lt;/code&gt;语句，我们指定编译器去尝试包含该crate。（在以前的Rust中，所有依赖项都需要一个&lt;code&gt;extern crate&lt;/code&gt;语句，不过现在该语句是可选的）。&lt;/p&gt;
&lt;p&gt;由于我们正在为自定义目标系统进行编译，因此无法使用随Rust一起安装的&lt;code&gt;alloc&lt;/code&gt;预编译版本。此处，我们必须告诉cargo用源码重新编译该crate。这可以通过将该crate添加到&lt;code&gt;.cargo/config.toml&lt;/code&gt;文件中的&lt;code&gt;unstable.build-std&lt;/code&gt;数组中来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[unstable]
build-std = [&quot;core&quot;, &quot;compiler_builtins&quot;, &quot;alloc&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，编译器将重新编译&lt;code&gt;alloc&lt;/code&gt;并其包括在内核中。&lt;/p&gt;
&lt;p&gt;默认在&lt;code&gt;#[no_std]&lt;/code&gt;的诸多crate中禁用&lt;code&gt;alloc&lt;/code&gt;crate的原因是它还有其他要求。现在尝试编译项目时，我们可以看到这些要求引发的编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: no global memory allocator found but one is required; link to std or add
       #[global_allocator] to a static item that implements the GlobalAlloc trait.

error: `#[alloc_error_handler]` function required, but not found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发生第一个错误是因为&lt;code&gt;alloc&lt;/code&gt;crate需要堆分配器，以提供具有&lt;code&gt;allocate&lt;/code&gt;和&lt;code&gt;deallocate&lt;/code&gt;函数的对象。在Rust中，堆分配器&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html&quot;&gt;&lt;code&gt;GlobalAlloc&lt;/code&gt;&lt;/a&gt; trait，如错误消息所示。要为crate提供堆分配器，必须为实现&lt;code&gt;GlobalAlloc&lt;/code&gt;trait的&lt;code&gt;static&lt;/code&gt;变量添加&lt;code&gt;#[global_allocator]&lt;/code&gt;属性。&lt;/p&gt;
&lt;p&gt;发生第二个错误是因为调用&lt;code&gt;allocate&lt;/code&gt;可能会失败，最常见的情况是在没有更多内存可用时。我们的程序必须能够对这种情况做出反应，这就是&lt;code&gt;#[alloc_error_handler]&lt;/code&gt;函数的作用。&lt;/p&gt;
&lt;p&gt;在下文的各小节中，我们将详细描述这些特征和属性。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;GlobalAlloc&lt;/code&gt;trait&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;GlobalAlloc&lt;/code&gt;trait定义了堆分配器必须提供的函数。该特性非常特殊，通常程序员不会使用到。而当程序员使用&lt;code&gt;alloc&lt;/code&gt;中的分配器和集合类型时，编译器将自动将调用适当的trait函数。&lt;/p&gt;
&lt;p&gt;由于我们将需要为所有分配器类型实现该trait，因此有必要仔细研究其声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub unsafe trait GlobalAlloc {
    unsafe fn alloc(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8;
    unsafe fn dealloc(&amp;amp;self, ptr: *mut u8, layout: Layout);

    unsafe fn alloc_zeroed(&amp;amp;self, layout: Layout) -&amp;gt; *mut u8 { ... }
    unsafe fn realloc(
        &amp;amp;self,
        ptr: *mut u8,
        layout: Layout,
        new_size: usize
    ) -&amp;gt; *mut u8 { ... }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该trait定义了两个必须实现的方法&lt;a href=&quot;https://doc.rust-lang.org/alloc/&quot;&gt;&lt;code&gt;alloc&lt;/code&gt;&lt;/a&gt;与&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.dealloc&quot;&gt;&lt;code&gt;dealloc&lt;/code&gt;&lt;/a&gt;，并与我们在示例中使用的&lt;code&gt;allocate&lt;/code&gt;和&lt;code&gt;deallocate&lt;/code&gt;函数相对应：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/alloc/&quot;&gt;&lt;code&gt;alloc&lt;/code&gt;&lt;/a&gt;方法将&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/struct.Layout.html&quot;&gt;&lt;code&gt;Layout&lt;/code&gt;&lt;/a&gt;实例作为参数，该实例描述了所分配的内存应具有的大小和对齐方式。该方法返回一个&lt;a href=&quot;https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer&quot;&gt;裸指针&lt;/a&gt;，并指向所分配内存块的第一个字节。当发生分配错误时，&lt;code&gt;alloc&lt;/code&gt;方法返回一个空指针，而不是显式的错误值。虽然这有点不符合Rust的使用习惯，不过由于现有系统也使用相同的约定，所以其优点是可以很容易的封装现有的系统的分配器。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#tymethod.dealloc&quot;&gt;&lt;code&gt;dealloc&lt;/code&gt;&lt;/a&gt;方法是分配的另一半，它负责释放内存块。该方法接收两个参数，即&lt;code&gt;alloc&lt;/code&gt;返回的指针和分配时用的&lt;code&gt;Layout&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该trait还使额外定义了两个带有默认实现的方法&lt;code&gt;alloc_zeroed&lt;/code&gt;和&lt;code&gt;realloc&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#method.alloc_zeroed&quot;&gt;&lt;code&gt;alloc_zeroed&lt;/code&gt;&lt;/a&gt;方法等效于先调用&lt;code&gt;alloc&lt;/code&gt;再将分配的内存块全部置为零，正如其默认实现中所做的那样。如果自定义实现比默认实现更高效的话，也可以使用它来覆盖默认实现。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html#method.realloc&quot;&gt;&lt;code&gt;realloc&lt;/code&gt;&lt;/a&gt;方法允许增加或减少分配空间。默认实现会分配一个所需大小的新内存块，并会将先前分配中的所有内容复制进来。与上一个方法相同，自定义的分配器实现也可能会提供此一个更高效的实现，比如实现就地扩展/缩小原分配。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;非安全特性&lt;/h4&gt;
&lt;p&gt;需要注意的是，trait本身的所有方法都被声明为&lt;code&gt;unsafe&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将该trait声明为&lt;code&gt;unsafe&lt;/code&gt;的原因是，程序员必须保证分配器类型的trait实现是正确的。例如，&lt;code&gt;alloc&lt;/code&gt;方法绝不能返回已在其他地方使用的内存块，因为这将导致未定义的行为。&lt;/li&gt;
&lt;li&gt;同样，其方法均为&lt;code&gt;unsafe&lt;/code&gt;的原因是，调用者在调用方法时必须确保各种不变性，例如，传递给&lt;code&gt;alloc&lt;/code&gt;的&lt;code&gt;Layout&lt;/code&gt;实例指定的大小应是非零的。不过在实践中这并不重要，因为方法通常是由编译器直接调用的，可以确保满足要求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;编写一个&lt;code&gt;DummyAllocator&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;既然已经知道了要为分配器类型提供什么，我们就可以实现一个简单的假分配器。首先创建一个新的分配器模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod allocator;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的假分配器将实现特征的最低要求，即在调用&lt;code&gt;alloc&lt;/code&gt;时始终返回错误。看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use alloc::alloc::{GlobalAlloc, Layout};
use core::ptr::null_mut;

pub struct Dummy;

unsafe impl GlobalAlloc for Dummy {
    unsafe fn alloc(&amp;amp;self, _layout: Layout) -&amp;gt; *mut u8 {
        null_mut()
    }

    unsafe fn dealloc(&amp;amp;self, _ptr: *mut u8, _layout: Layout) {
        panic!(&quot;dealloc should be never called&quot;)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该结构体不需要任何字段，于是我们将其创建为&lt;a href=&quot;https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts&quot;&gt;零大小类型&lt;/a&gt;。就像描述的那样，我们总是用&lt;code&gt;alloc&lt;/code&gt;返回空指针，这正对应了分配错误。由于分配器从不返回任何内存，所以永远不应该调用&lt;code&gt;dealloc&lt;/code&gt;。因此，我们仅在&lt;code&gt;dealloc&lt;/code&gt;中panic即可。&lt;code&gt;alloc_zeroed&lt;/code&gt;和&lt;code&gt;realloc&lt;/code&gt;方法具有默认实现，因此无需为其提供实现。&lt;/p&gt;
&lt;p&gt;这就实现了一个简单的分配器，但是我们仍然需要告诉Rust编译器它应该使用这个分配器。这就需要用到&lt;code&gt;#[global_allocator]&lt;/code&gt;属性了。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;#[global_allocator]&lt;/code&gt;属性&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;#[global_allocator]&lt;/code&gt;将属性告诉Rust编译器应该使用哪个分配器实例作为全局堆分配器。该属性仅适用于实现了&lt;code&gt;GlobalAlloc&lt;/code&gt;特性的&lt;code&gt;static&lt;/code&gt;对象。让我们将分配器&lt;code&gt;Dummy&lt;/code&gt;的一个实例注册为全局分配器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[global_allocator]
static ALLOCATOR: Dummy = Dummy;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;Dummy&lt;/code&gt;分配器是&lt;a href=&quot;https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts&quot;&gt;零大小类型&lt;/a&gt;，因此我们不需要在初始化表达式中指定任何字段。&lt;/p&gt;
&lt;p&gt;现在尝试编译，第一个错误消失了。让我们继续修复第二个错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: `#[alloc_error_handler]` function required, but not found
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;#[alloc_error_handler]&lt;/code&gt;属性&lt;/h3&gt;
&lt;p&gt;正如我们在讨论&lt;code&gt;GlobalAlloc&lt;/code&gt;trait时所了解的那样，&lt;code&gt;alloc&lt;/code&gt;函数可以通过返回空指针来表示分配错误。那么Rust在运行时应如何应对这种分配失败呢？这就是&lt;code&gt;#[alloc_error_handler]&lt;/code&gt;属性起作用的地方了。该属性将指定发生分配错误时调用的处理函数，类似于在发生panic时调用panic处理程序。&lt;/p&gt;
&lt;p&gt;我们来添加一个这样的函数以修复该编译错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![feature(alloc_error_handler)] // 需要写在源文件开头

#[alloc_error_handler]
fn alloc_error_handler(layout: alloc::alloc::Layout) -&amp;gt; ! {
    panic!(&quot;allocation error: {:?}&quot;, layout)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;alloc_error_handler&lt;/code&gt;函数仍然不稳定，因此我们需要一个特性门来启用它。该函数接收一个参数，即发生分配错误时传递给&lt;code&gt;alloc&lt;/code&gt;的&lt;code&gt;Layout&lt;/code&gt;实例。我们无法纠正该错误，因此仅在处理函数中进行panic并打印关于&lt;code&gt;Layout&lt;/code&gt;实例的信息。&lt;/p&gt;
&lt;p&gt;应用该函数就可以修复编译错误。现在我们可以使用&lt;code&gt;alloc&lt;/code&gt;的分配器和集合类型了，例如，我们可以使用&lt;a href=&quot;https://doc.rust-lang.org/alloc/boxed/struct.Box.html&quot;&gt;&lt;code&gt;Box&lt;/code&gt;&lt;/a&gt;在堆上分配一个值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern crate alloc;

use alloc::boxed::Box;

fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    // […] print &quot;Hello World!&quot;, call `init`, create `mapper` and `frame_allocator`

    let x = Box::new(41);

    // […] call `test_main` in test mode

    println!(&quot;It did not crash!&quot;);
    blog_os::hlt_loop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，我们也需要在&lt;code&gt;main.rs&lt;/code&gt;中指定&lt;code&gt;extern crate alloc&lt;/code&gt;语句。这是必需的，因为&lt;code&gt;lib.rs&lt;/code&gt;和&lt;code&gt;main.rs&lt;/code&gt;部分被视为各自独立的crate。但是，我们不需要创建另一个&lt;code&gt;#[global_allocator]&lt;/code&gt;静态变量，因为全局分配器适用于项目中的所有crate。实际上，在另一个crate中指定其他分配器将引发错误。&lt;/p&gt;
&lt;p&gt;运行上面的代码，我们将看到&lt;code&gt;alloc_error_handler&lt;/code&gt;函数被调用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/heap-allocation/qemu-dummy-output.png&quot; alt=&quot;QEMU中假分配器的输出&quot; /&gt;&lt;/p&gt;
&lt;p&gt;错误处理程序被调用是因为&lt;code&gt;Box::new&lt;/code&gt;函数隐式调用了全局分配器的&lt;code&gt;alloc&lt;/code&gt;函数。因为假分配器始终返回空指针，所以每次分配都将会失败。为了解决这个问题，我们需要创建一个真分配器，用以返回可以使用的内存。&lt;/p&gt;
&lt;h2&gt;创建内核堆&lt;/h2&gt;
&lt;p&gt;在创建适当的分配器之前，我们首先需要创建一个堆内存区域，使得分配器可以从该区域分配内存。为此，我们需要为堆区域定义一个虚拟内存范围，然后将该区域映射到物理帧上。有关虚拟内存和页表的介绍，请参见&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/&quot;&gt;“&lt;em&gt;内存分页简介&lt;/em&gt;”&lt;/a&gt;一文。&lt;/p&gt;
&lt;p&gt;第一步是为堆定义虚拟内存区域。我们可以选择我们喜欢的任何虚拟地址范围，只要它尚未用于其他内存区域即可。让我们将其定义为从地址&lt;code&gt;0x_4444_4444_0000&lt;/code&gt;开始的内存，以便未来可以轻松识别堆指针：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub const HEAP_START: usize = 0x_4444_4444_0000;
pub const HEAP_SIZE: usize = 100 * 1024; // 100 KiB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前将堆大小设置为100 KiB。如果将来我们需要更多空间，将该值改大即可。&lt;/p&gt;
&lt;p&gt;如果我们现在尝试使用此堆区域，就会发生页面错误，因为虚拟内存区域尚未映射到物理内存。为了解决这个问题，我们创建&lt;code&gt;init_heap&lt;/code&gt;函数并使用在&lt;a href=&quot;https://os.phil-opp.com/paging-implementation/&quot;&gt;&lt;em&gt;“内存分页实现”&lt;/em&gt;&lt;/a&gt;一文中介绍的&lt;a href=&quot;https://os.phil-opp.com/paging-implementation/#using-offsetpagetable&quot;&gt;&lt;code&gt;Mapper&lt;/code&gt; API&lt;/a&gt;映射堆页面：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::{
    structures::paging::{
        mapper::MapToError, FrameAllocator, Mapper, Page, PageTableFlags, Size4KiB,
    },
    VirtAddr,
};

pub fn init_heap(
    mapper: &amp;amp;mut impl Mapper&amp;lt;Size4KiB&amp;gt;,
    frame_allocator: &amp;amp;mut impl FrameAllocator&amp;lt;Size4KiB&amp;gt;,
) -&amp;gt; Result&amp;lt;(), MapToError&amp;lt;Size4KiB&amp;gt;&amp;gt; {
    let page_range = {
        let heap_start = VirtAddr::new(HEAP_START as u64);
        let heap_end = heap_start + HEAP_SIZE - 1u64;
        let heap_start_page = Page::containing_address(heap_start);
        let heap_end_page = Page::containing_address(heap_end);
        Page::range_inclusive(heap_start_page, heap_end_page)
    };

    for page in page_range {
        let frame = frame_allocator
            .allocate_frame()
            .ok_or(MapToError::FrameAllocationFailed)?;
        let flags = PageTableFlags::PRESENT | PageTableFlags::WRITABLE;
        unsafe {
            mapper.map_to(page, frame, flags, frame_allocator)?.flush()
        };
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数接受&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html&quot;&gt;&lt;code&gt;Mapper&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.FrameAllocator.html&quot;&gt;&lt;code&gt;FrameAllocator&lt;/code&gt;&lt;/a&gt;实例的可变引用作为参数，这两个实例均使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/enum.Size4KiB.html&quot;&gt;&lt;code&gt;Size4KiB&lt;/code&gt;&lt;/a&gt;作为泛型参数指定页面大小为4KiB。该函数的返回值为&lt;a href=&quot;https://doc.rust-lang.org/core/result/enum.Result.html&quot;&gt;&lt;code&gt;Result&lt;/code&gt;&lt;/a&gt;，以单位类型&lt;code&gt;()&lt;/code&gt;作为成功结果，以&lt;code&gt;MapToError&lt;/code&gt;作为失败结果，该失败结果是由&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to&quot;&gt;&lt;code&gt;Mapper::map_to&lt;/code&gt;&lt;/a&gt;方法返回的错误类型。之所以在此处复用了该错误类型是因为&lt;code&gt;map_to&lt;/code&gt;方法是此函数中错误的主要来源。&lt;/p&gt;
&lt;p&gt;实现可以分为两部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;创建页面范围&lt;/strong&gt;：首先需要将&lt;code&gt;HEAP_START&lt;/code&gt;指针转换为&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/addr/struct.VirtAddr.html&quot;&gt;&lt;code&gt;VirtAddr&lt;/code&gt;&lt;/a&gt;类型。然后用起始地址加上&lt;code&gt;HEAP_SIZE&lt;/code&gt;计算出堆结束地址。我们想要一个闭区间（堆的最后一个字节的地址），故而减1。接下来使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/struct.Page.html#method.containing_address&quot;&gt;&lt;code&gt;containing_address&lt;/code&gt;&lt;/a&gt;函数将这些地址转换为&lt;code&gt;Page&lt;/code&gt;类型。最后使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/struct.Page.html#method.range_inclusive&quot;&gt;&lt;code&gt;Page::range_inclusive&lt;/code&gt;&lt;/a&gt;函数创建从起始页面到结束页面的页面范围。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;映射页面&lt;/strong&gt;：第二步是映射刚刚创建的页面范围内的所有页面。为此，我们使用&lt;code&gt;for&lt;/code&gt;循环遍历该范围内的页面。我们为每个页面执行以下操作：
&lt;ul&gt;
&lt;li&gt;使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.FrameAllocator.html#tymethod.allocate_frame&quot;&gt;&lt;code&gt;FrameAllocator::allocate_frame&lt;/code&gt;&lt;/a&gt;方法将页面应映射到分配的物理帧上。当没有额外的帧时，该方法将返回&lt;a href=&quot;https://doc.rust-lang.org/core/option/enum.Option.html#variant.None&quot;&gt;&lt;code&gt;None&lt;/code&gt;&lt;/a&gt;。遇到没有额外帧的情况时，我们使用&lt;a href=&quot;https://doc.rust-lang.org/core/option/enum.Option.html#method.ok_or&quot;&gt;&lt;code&gt;Option::ok_or&lt;/code&gt;&lt;/a&gt;方法将错误映射到&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/enum.MapToError.html#variant.FrameAllocationFailed&quot;&gt;&lt;code&gt;MapToError::FrameAllocationFailed&lt;/code&gt;&lt;/a&gt;，然后接&lt;a href=&quot;https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html&quot;&gt;问号运算符&lt;/a&gt;以在出错时提前返回。&lt;/li&gt;
&lt;li&gt;为页面设置了必要的&lt;code&gt;PRESENT&lt;/code&gt;和&lt;code&gt;WRITABLE&lt;/code&gt;标志，以允许读取和写入访问，这对于堆内存是有实际意义的。&lt;/li&gt;
&lt;li&gt;使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to&quot;&gt;&lt;code&gt;Mapper::map_to&lt;/code&gt;&lt;/a&gt;方法在活动页面表中创建映射。该方法可能会失败，因此我们再次使用&lt;a href=&quot;https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/the-question-mark-operator-for-easier-error-handling.html&quot;&gt;问号运算符&lt;/a&gt;将错误传播给调用方。若成功，该方法将返回一个&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html&quot;&gt;&lt;code&gt;MapperFlush&lt;/code&gt;&lt;/a&gt;实例，然后使用该实例使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush&quot;&gt;&lt;code&gt;flush&lt;/code&gt;&lt;/a&gt;方法来更新&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#the-translation-lookaside-buffer&quot;&gt;转换后备缓冲区&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;最后一步是从&lt;code&gt;kernel_main&lt;/code&gt;中调用此函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    use blog_os::allocator; // new import
    use blog_os::memory::{self, BootInfoFrameAllocator};

    println!(&quot;Hello World{}&quot;, &quot;!&quot;);
    blog_os::init();

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    let mut mapper = unsafe { memory::init(phys_mem_offset) };
    let mut frame_allocator = unsafe {
        BootInfoFrameAllocator::init(&amp;amp;boot_info.memory_map)
    };

    // new
    allocator::init_heap(&amp;amp;mut mapper, &amp;amp;mut frame_allocator)
        .expect(&quot;heap initialization failed&quot;);

    let x = Box::new(41);

    // […] call `test_main` in test mode

    println!(&quot;It did not crash!&quot;);
    blog_os::hlt_loop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码显示了函数完整的上下文。这里仅有的新增行是&lt;code&gt;blog_os::allocator&lt;/code&gt;的导入和&lt;code&gt;allocator::init_heap&lt;/code&gt;函数的调用。如果&lt;code&gt;init_heap&lt;/code&gt;函数返回错误，我们将使用&lt;code&gt;Result::expect&lt;/code&gt;方法产生panic，因为目前还没有适当的处理此错误的方法。&lt;/p&gt;
&lt;p&gt;现在，我们有一个准备使用的映射堆内存区域。但是&lt;code&gt;Box::new&lt;/code&gt;调用仍然使用旧的&lt;code&gt;Dummy&lt;/code&gt;分配器，因此如果此时运行仍会看到“内存不足”错误。让我们通过使用适当的分配器来解决此问题。&lt;/p&gt;
&lt;h2&gt;使用分配器crate&lt;/h2&gt;
&lt;p&gt;鉴于实现分配器较为复杂，我们先临时使用已有的分配器crate。在下一篇文章中，我们将学习如何实现自己的分配器。&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;no_std&lt;/code&gt;的应用程序，&lt;a href=&quot;https://github.com/phil-opp/linked-list-allocator/&quot;&gt;&lt;code&gt;linked_list_allocator&lt;/code&gt;&lt;/a&gt; crate是一个简单的分配器实现。之所以叫这个名字，是因为该crate使用链表数据结构来跟踪释放的内存区域。有关该实现的详情参见下一篇文章。&lt;/p&gt;
&lt;p&gt;要使用该crate，首先需要在&lt;code&gt;Cargo.toml&lt;/code&gt;中添加其依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
linked_list_allocator = &quot;0.8.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后便可以使用该crate提供的分配器代替我们的假分配器了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use linked_list_allocator::LockedHeap;

#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该结构体叫做&lt;code&gt;LockedHeap&lt;/code&gt;，是因为它使用&lt;a href=&quot;https://docs.rs/spinning_top/0.1.0/spinning_top/type.Spinlock.html&quot;&gt;&lt;code&gt;spining_top::Spinlock&lt;/code&gt;&lt;/a&gt;类型进行同步。这是必需的，因为多个线程可以同时访问&lt;code&gt;ALLOCATOR&lt;/code&gt;静态对象。一如往常，在使用自旋锁或互斥锁时，我们需要注意不要意外引发死锁。这意味着我们不应该在中断处理程序中执行任何分配，因为它们可以在任意时间运行，并且可能会中断正在进行的分配。&lt;/p&gt;
&lt;p&gt;仅将&lt;code&gt;LockedHeap&lt;/code&gt;设置为全局分配器是不够的。原因是我们使用了构造函数&lt;a href=&quot;https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.LockedHeap.html#method.empty&quot;&gt;&lt;code&gt;empty&lt;/code&gt;&lt;/a&gt;，创建了一个没有任何后备内存的分配器。就像我们的假分配器一样，它总是在&lt;code&gt;alloc&lt;/code&gt;时返回错误。为了解决这个问题，我们需要在创建堆之后初始化分配器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn init_heap(
    mapper: &amp;amp;mut impl Mapper&amp;lt;Size4KiB&amp;gt;,
    frame_allocator: &amp;amp;mut impl FrameAllocator&amp;lt;Size4KiB&amp;gt;,
) -&amp;gt; Result&amp;lt;(), MapToError&amp;lt;Size4KiB&amp;gt;&amp;gt; {
    // […] map all heap pages to physical frames

    // new
    unsafe {
        ALLOCATOR.lock().init(HEAP_START, HEAP_SIZE);
    }

    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在&lt;code&gt;LockedHeap&lt;/code&gt;类型的内部自旋锁上使用&lt;a href=&quot;https://docs.rs/lock_api/0.3.3/lock_api/struct.Mutex.html#method.lock&quot;&gt;&lt;code&gt;lock&lt;/code&gt;&lt;/a&gt;方法来获取对封装后的&lt;a href=&quot;https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html&quot;&gt;&lt;code&gt;Heap&lt;/code&gt;&lt;/a&gt;实例的排他引用，然后在该实例上调用以堆边界为参数的&lt;a href=&quot;https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.init&quot;&gt;&lt;code&gt;init&lt;/code&gt;&lt;/a&gt;方法。重要的是，我们在映射堆页面&lt;em&gt;之后&lt;/em&gt;才初始化堆，因为&lt;a href=&quot;https://docs.rs/linked_list_allocator/0.8.0/linked_list_allocator/struct.Heap.html#method.init&quot;&gt;&lt;code&gt;init&lt;/code&gt;&lt;/a&gt;函数已经在尝试写入堆内存了。&lt;/p&gt;
&lt;p&gt;初始化堆之后就可以使用内置&lt;code&gt;alloc&lt;/code&gt;crate的所有分配和收集类型，而且并不会出现错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use alloc::{boxed::Box, vec, vec::Vec, rc::Rc};

fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    // […] initialize interrupts, mapper, frame_allocator, heap

    // allocate a number on the heap
    let heap_value = Box::new(41);
    println!(&quot;heap_value at {:p}&quot;, heap_value);

    // create a dynamically sized vector
    let mut vec = Vec::new();
    for i in 0..500 {
        vec.push(i);
    }
    println!(&quot;vec at {:p}&quot;, vec.as_slice());

    // create a reference counted vector -&amp;gt; will be freed when count reaches 0
    let reference_counted = Rc::new(vec![1, 2, 3]);
    let cloned_reference = reference_counted.clone();
    println!(&quot;current reference count is {}&quot;, Rc::strong_count(&amp;amp;cloned_reference));
    core::mem::drop(reference_counted);
    println!(&quot;reference count is {} now&quot;, Rc::strong_count(&amp;amp;cloned_reference));

    // […] call `test_main` in test context
    println!(&quot;It did not crash!&quot;);
    blog_os::hlt_loop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码展示了&lt;a href=&quot;https://doc.rust-lang.org/alloc/boxed/struct.Box.html&quot;&gt;&lt;code&gt;Box&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/alloc/vec/&quot;&gt;&lt;code&gt;Vec&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/alloc/rc/&quot;&gt;&lt;code&gt;Rc&lt;/code&gt;&lt;/a&gt;类型的部分使用。对于&lt;code&gt;Box&lt;/code&gt;和&lt;code&gt;Vec&lt;/code&gt;类型，我们使用&lt;a href=&quot;https://doc.rust-lang.org/core/fmt/trait.Pointer.html&quot;&gt;&lt;code&gt;{:p}&lt;/code&gt;格式说明符&lt;/a&gt;来打印底层堆指针。为了展示&lt;code&gt;Rc&lt;/code&gt;，我们创建一个引用计数的堆值，并在删除实例（使用&lt;a href=&quot;https://doc.rust-lang.org/core/mem/fn.drop.html&quot;&gt;core::mem::drop&lt;/a&gt;）前后分别使用&lt;a href=&quot;https://doc.rust-lang.org/alloc/rc/struct.Rc.html#method.strong_count&quot;&gt;&lt;code&gt;Rc::strong_count&lt;/code&gt;&lt;/a&gt;函数两次打印引用计数。&lt;/p&gt;
&lt;p&gt;运行将会看到以下内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/heap-allocation/qemu-alloc-showcase.png&quot; alt=&quot;QEMU分配器示例&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不出所料，我们看到&lt;code&gt;Box&lt;/code&gt;和&lt;code&gt;Vec&lt;/code&gt;值存在于堆中，正如以&lt;code&gt;0x_4444_4444_*&lt;/code&gt;为前缀的指针所指示。引用计数值也可以按预期方式运行，调用&lt;code&gt;clone&lt;/code&gt;后，引用计数为2，在删除其中一个实例后，引用计数变为1。&lt;/p&gt;
&lt;p&gt;向量从偏移量&lt;code&gt;0x800&lt;/code&gt;开始的原因并不是&lt;code&gt;Box&lt;/code&gt;类型的值大小为&lt;code&gt;0x800&lt;/code&gt;字节，而是因为向量需要扩充容量时发生了&lt;a href=&quot;https://doc.rust-lang.org/alloc/vec/struct.Vec.html#capacity-and-reallocation&quot;&gt;再分配&lt;/a&gt;。例如，当向量的容量为32且我们尝试添加下一个元素时，向量将在后台分配一个容量为64的新后备数组，并将所有元素复制到到新数组，然后再释放掉旧分配。&lt;/p&gt;
&lt;p&gt;当然，我们还有很多&lt;code&gt;alloc&lt;/code&gt;crate中的其他分配器和集合类型可以在内核中使用，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程安全引用计数指针&lt;a href=&quot;https://doc.rust-lang.org/alloc/sync/struct.Arc.html&quot;&gt;&lt;code&gt;Arc&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Rust的字符串类型&lt;a href=&quot;https://doc.rust-lang.org/alloc/string/struct.String.html&quot;&gt;&lt;code&gt;String&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/alloc/macro.format.html&quot;&gt;&lt;code&gt;format!&lt;/code&gt;&lt;/a&gt;格式化宏&lt;/li&gt;
&lt;li&gt;链表&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/linked_list/struct.LinkedList.html&quot;&gt;&lt;code&gt;LinkedList&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;可增长的环形缓冲区&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/vec_deque/struct.VecDeque.html&quot;&gt;&lt;code&gt;VecDeque&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;优先级队列&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/binary_heap/struct.BinaryHeap.html&quot;&gt;&lt;code&gt;BinaryHeap&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/btree_map/struct.BTreeMap.html&quot;&gt;&lt;code&gt;BTreeMap&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/btree_set/struct.BTreeSet.html&quot;&gt;&lt;code&gt;BTreeSet&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当我们要实现线程列表、调度队列、或async/await支持时，这些类型将会非常有用。&lt;/p&gt;
&lt;h2&gt;添加测试&lt;/h2&gt;
&lt;p&gt;为了确保新的分配器代码不会被意外破坏，我们应该为其添加一个集成测试。首先创建一个新的&lt;code&gt;tests/heap_allocation.rs&lt;/code&gt;文件，其内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = &quot;test_main&quot;]

extern crate alloc;

use bootloader::{entry_point, BootInfo};
use core::panic::PanicInfo;

entry_point!(main);

fn main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    unimplemented!();
}

#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    blog_os::test_panic_handler(info)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们重用了&lt;code&gt;lib.rs&lt;/code&gt;中的&lt;code&gt;test_runner&lt;/code&gt;和&lt;code&gt;test_panic_handler&lt;/code&gt;函数。由于我们要测试分配器，所以需要使用&lt;code&gt;extern crate alloc&lt;/code&gt;语句启用&lt;code&gt;alloc&lt;/code&gt;crate。 有关测试样板的更多信息，请查看前面的&lt;a href=&quot;https://os.phil-opp.com/testing/&quot;&gt;&lt;em&gt;测试&lt;/em&gt;&lt;/a&gt;一文。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt;函数的实现如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    use blog_os::allocator;
    use blog_os::memory::{self, BootInfoFrameAllocator};
    use x86_64::VirtAddr;

    blog_os::init();
    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    let mut mapper = unsafe { memory::init(phys_mem_offset) };
    let mut frame_allocator = unsafe {
        BootInfoFrameAllocator::init(&amp;amp;boot_info.memory_map)
    };
    allocator::init_heap(&amp;amp;mut mapper, &amp;amp;mut frame_allocator)
        .expect(&quot;heap initialization failed&quot;);

    test_main();
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它与&lt;code&gt;main.rs&lt;/code&gt;中的&lt;code&gt;kernel_main&lt;/code&gt;函数非常相似，不同之处在于我们不调用&lt;code&gt;println&lt;/code&gt;，不包括任何分配示例，并且无条件调用&lt;code&gt;test_main&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在我们准备添加一些测试用例。首先，我们添加一个测试，以使用&lt;a href=&quot;https://doc.rust-lang.org/alloc/boxed/struct.Box.html&quot;&gt;&lt;code&gt;Box&lt;/code&gt;&lt;/a&gt;执行一些简单的分配并检查分配的值，以确保基本分配有效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use alloc::boxed::Box;

#[test_case]
fn simple_allocation() {
    let heap_value_1 = Box::new(41);
    let heap_value_2 = Box::new(13);
    assert_eq!(*heap_value_1, 41);
    assert_eq!(*heap_value_2, 13);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最重要的是，此测试可验证是否发生分配错误。&lt;/p&gt;
&lt;p&gt;接下来，我们循环新建一个大型向量，以测试大型分配和多重分配（以测试再分配）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use alloc::vec::Vec;

#[test_case]
fn large_vec() {
    let n = 1000;
    let mut vec = Vec::new();
    for i in 0..n {
        vec.push(i);
    }
    assert_eq!(vec.iter().sum::&amp;lt;u64&amp;gt;(), (n - 1) * n / 2);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们通过与&lt;a href=&quot;https://en.wikipedia.org/wiki/1_%2B_2_%2B_3_%2B_4_%2B_%E2%8B%AF#Partial_sums&quot;&gt;求和公式&lt;/a&gt;进行比较来验证向量求和。这使我们确信分配的值都是正确的。&lt;/p&gt;
&lt;p&gt;作为第三项测试，我们在执行一万次分配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use blog_os::allocator::HEAP_SIZE;

#[test_case]
fn many_boxes() {
    for i in 0..HEAP_SIZE {
        let x = Box::new(i);
        assert_eq!(*x, i);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此测试确保分配器将释放的内存重新用于后续分配，否则分配器将耗尽内存。这似乎是对分配器的基本要求，但是有些分配器设计并没有这样做。一个示例是线性分配器，我们将在下一篇文章中进行解释。&lt;/p&gt;
&lt;p&gt;运行新的集成测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test --test heap_allocation
[…]
Running 3 tests
simple_allocation... [ok]
large_vec... [ok]
many_boxes... [ok]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个测试全部通过！你还可以调用&lt;code&gt;cargo test&lt;/code&gt;（不使用&lt;code&gt;--test&lt;/code&gt;参数）来运行所有单元测试和集成测试。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;在这篇文章中，我们了解了动态内存的概念、为什么要使用动态内存以及需要在什么地方使用动态内存。我们了解了Rust的借用检查器如何防止常见漏洞，以及Rust分配器API的工作方式。&lt;/p&gt;
&lt;p&gt;在使用假分配器创建了Rust分配器接口的最小化实现之后，我们为内核创建了一个适当的堆内存区域。为此，我们为堆定义了一个虚拟地址范围，然后使用上一篇文章中的&lt;code&gt;Mapper&lt;/code&gt;和&lt;code&gt;FrameAllocator&lt;/code&gt;将地址范围中的所有页面映射到物理帧。&lt;/p&gt;
&lt;p&gt;最后，我们添加了对&lt;code&gt;linked_list_allocator&lt;/code&gt;crate的依赖，以在内核中应用已有的分配器实现。利用此分配器，我们能够使用&lt;code&gt;Box&lt;/code&gt;、&lt;code&gt;Vec&lt;/code&gt;以及&lt;code&gt;alloc&lt;/code&gt;  crate中的其他分配器和集合类型。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;虽然我们已经在本文中添加了堆分配支持，但也将大部分工作留给了现成的&lt;code&gt;linked_list_allocator&lt;/code&gt;crate。下一篇文章将详细展示如何从头开始实现分配器，并将介绍多种可能的分配器设计，展示如何实现它们的简单版本，并阐述它们的优缺点。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 3.2 - 内存分页实现</title><link>https://zlotus.github.io/2021/03/26/writing-an-os-in-rust-3.2/</link><guid isPermaLink="true">https://zlotus.github.io/2021/03/26/writing-an-os-in-rust-3.2/</guid><description>使用Rust编写操作系统 - 3.2 - 内存分页实现</description><pubDate>Fri, 26 Mar 2021 17:44:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/paging-implementation/&quot;&gt;Paging Implementation&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;本文将展示如何在内核中实现对内存分页的支持。我们首先将探讨使内核可以访问物理页表帧的各种技术，并讨论它们各自的优缺点。然后，实现地址转换函数和创建新映射函数。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-09&quot;&gt;post-09&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/&quot;&gt;上一篇文章&lt;/a&gt;介绍了分页的概念，通过与分段进行比较来引入分页，解释了分页和页表的工作原理，然后介绍了&lt;code&gt;x86_64&lt;/code&gt;的4级页表设计。我们发现bootloader已经为内核设置了页表层次结构，这意味着我们的内核已经在虚拟地址上运行。这有助于提高安全性，因为非法内存访问并不能修改任意物理内存，只会导致页面错误异常。&lt;/p&gt;
&lt;p&gt;前文结尾我们留下一个问题——&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#accessing-the-page-tables&quot;&gt;无法从内核访问页表&lt;/a&gt;——这是因为页表存储在物理内存中，而内核已经运行在虚拟地址上。本文将在这一点上深入，探讨使内核能够访问页表帧的不同方法。我们将讨论每种方法的优缺点，然后为内核确定一种适合的方法。&lt;/p&gt;
&lt;p&gt;要实现该方法，首先，我们将需要bootloader的支持，因此需要对其进行配置。然后，我们将实现一个遍历页表层次结构的函数，以将虚拟地址转换为物理地址。最后，我们将学习如何在页表中创建新的映射，以及如何找到未使用的内存帧来创建新的页表。&lt;/p&gt;
&lt;h2&gt;访问页表的方法&lt;/h2&gt;
&lt;p&gt;从内核访问页表并不像看起来那样简单。为理解这个问题，让我们回顾前文的示例，4级页表层次结构：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/x86_64-page-table-translation.svg&quot; alt=&quot;x86_64页表转换&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这里的关键是每个页表条目都会存储下一张表的&lt;em&gt;物理地址&lt;/em&gt;。这样做既避免了对这些地址再进行转换——会对性能造成不利影响——也避免了地址转换陷入无限递归。&lt;/p&gt;
&lt;p&gt;对我们来说，问题在于无法直接通过内核访问物理地址，因为内核仍运行在虚拟地址上。例如，当我们访问地址&lt;code&gt;4 KiB&lt;/code&gt;时，我们访问的是&lt;em&gt;虚拟&lt;/em&gt;地址&lt;code&gt;4 KiB&lt;/code&gt;，而不是存储第4级页表的&lt;em&gt;物理&lt;/em&gt;地址&lt;code&gt;4 KiB&lt;/code&gt;。当我们要访问物理地址&lt;code&gt;4KiB&lt;/code&gt;时，我们只能通过映射到它的某个虚拟地址来进行访问。&lt;/p&gt;
&lt;p&gt;因此，为了访问页表帧，我们需要将一些虚拟页映射到这些帧上。创建映射的方法有很多，而这些方法都允许我们访问任意页表帧。&lt;/p&gt;
&lt;h3&gt;恒等映射&lt;/h3&gt;
&lt;p&gt;一个简单的解决方案是对所有页表进行恒等映射：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/identity-mapped-page-tables.svg&quot; alt=&quot;恒等映射页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在上图中，我们看到了很多恒等映射的页面表帧。如此，页表的物理地址也同时是有效的虚拟地址，我们因此可以轻松地从CR3寄存器开始访问到所有级别的页表。&lt;/p&gt;
&lt;p&gt;但是，它会使虚拟地址空间变得混乱，并使得找到更大尺寸的连续存储区域变得更加困难。例如，假设我们要在上图中创建一个大小为1000KiB的虚拟内存区域，比如用于&lt;a href=&quot;https://en.wikipedia.org/wiki/Memory-mapped_file&quot;&gt;内存映射文件&lt;/a&gt;。我们无法从&lt;code&gt;28KiB&lt;/code&gt;开始该区域，因为它会与已经映射的页面&lt;code&gt;1004KiB&lt;/code&gt;发生冲突。因此，我们必须进一步寻找，直到找到足够大的连续未映射区域，例如&lt;code&gt;1008KiB&lt;/code&gt;。这是与&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#fragmentation&quot;&gt;分段&lt;/a&gt;类似的碎片问题。&lt;/p&gt;
&lt;p&gt;同样，这会使得创建新的页表变得更加困难，因为我们需要为新表找到大小相称且尚未使用的物理帧。例如，假设我们为内存映射文件保留了从1008KiB开始的&lt;code&gt;1000KiB&lt;/code&gt;&lt;em&gt;虚拟&lt;/em&gt;内存区域，于是便不能再使用&lt;em&gt;物理&lt;/em&gt;地址在&lt;code&gt;1000KiB&lt;/code&gt;和&lt;code&gt;2008KiB&lt;/code&gt;之间的任何帧，因为无法对其进行恒等映射。&lt;/p&gt;
&lt;h3&gt;固定偏移量映射&lt;/h3&gt;
&lt;p&gt;为了避免使虚拟地址空间变的混乱，我们可以为&lt;strong&gt;页表映射划分单独的内存区域&lt;/strong&gt;。因此，我们不再恒等映射页表帧，而将页表帧以固定偏移量映射到虚拟地址空间中。例如，偏移量可以是&lt;code&gt;10 TiB&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/page-tables-mapped-at-offset.svg&quot; alt=&quot;带偏移量的页表映射&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过将&lt;code&gt;10TiB..(10TiB+物理内存大小)&lt;/code&gt;范围内的虚拟内存地址专门用于页表映射，我们避免了恒等映射的冲突问题。只有在虚拟地址空间寻址空间远大于物理内存大小时，才可以为虚拟地址空间保留如此巨大的区域。不过这个大小在x86_64上很容易达到，因为48位虚拟地址地址的寻址空间为256TiB。&lt;/p&gt;
&lt;p&gt;该方法仍然有一个缺点，就是每当我们创建一个新的页表时，都需要创建一个新的映射。另外，该方法不允许访问其他地址空间的页表，这在创建新进程时很有用。&lt;/p&gt;
&lt;h3&gt;完整物理内存映射&lt;/h3&gt;
&lt;p&gt;要来解决这些问题，我们不再仅映射页表帧，而是&lt;strong&gt;映射完整物理内存&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/map-complete-physical-memory.svg&quot; alt=&quot;映射完整物理内存&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这种方法允许内核访问任意物理内存，包括其他地址空间的页表帧。保留的虚拟内存范围与以上一节相同，不同之处在于虚拟内存不再包含未映射的页面（译注：即虚拟内存空间将全部映射到物理帧）。&lt;/p&gt;
&lt;p&gt;这种方法的缺点是需要额外的页表来存储物理内存的映射。这些页表需要存储在某个地方，因此它们会用掉一部分物理内存，这在内存较小的设备上可能是个问题。&lt;/p&gt;
&lt;p&gt;不过，在x86_64上我们可以使用2MiB的&lt;a href=&quot;https://en.wikipedia.org/wiki/Page_%28computer_memory%29#Multiple_page_sizes&quot;&gt;巨页&lt;/a&gt;进行映射，而不是默认的4KiB页面。这样，映射32GiB内存，仅需要1个3级表和32个2级表（译注：一个2级表包含512个2MiB的巨页条目，即一个2级表可映射1GiB）总共132KiB的空间用于存储页表（译注：每页表512条目，每条目占8B空间，即每页表占4KiB空间，一共占(1+32)*4KiB=132KiB）。而且巨页还可以提升缓存效率，因为巨页在转换后备缓冲区（TLB）中使用的条目更少。&lt;/p&gt;
&lt;h3&gt;临时映射&lt;/h3&gt;
&lt;p&gt;对于物理内存量很小的设备，我们只能在需要访问时才临时映射页表帧。为了能够创建临时映射，我们只需要一个恒等映射的1级页表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/temporarily-mapped-page-tables.svg&quot; alt=&quot;临时映射页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中的1级表控制虚拟地址空间的前2MiB（译注：512个大小为4KiB的虚拟页面）。这是因为它可以通过从CR3寄存器开始并跟随4级、3级和2级页表中的第0个条目来访问。索引为&lt;code&gt;8&lt;/code&gt;的条目将地址&lt;code&gt;32KiB&lt;/code&gt;上的虚拟页映射到地址&lt;code&gt;32KiB上&lt;/code&gt;的物理帧，也就是恒等映射了1级表本身。图中使用&lt;code&gt;32KiB&lt;/code&gt;处的水平箭头表明了此恒等映射。&lt;/p&gt;
&lt;p&gt;通过写入恒等映射的1级表，我们的内核最多可以创建511个临时映射（512减去恒等映射所需的条目）。在上面的示例中，内核创建了两个临时映射：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将1级表的第0个条目映射到地址为&lt;code&gt;24KiB&lt;/code&gt;的帧，便创建了一个虚拟的临时映射，将&lt;code&gt;0​​KiB&lt;/code&gt;处的虚拟页映射到2级页表所在的物理帧，如虚线箭头所示。&lt;/li&gt;
&lt;li&gt;将1级表的第9个条目映射到地址为&lt;code&gt;4KiB&lt;/code&gt;的帧，便创建了一个虚拟的临时映射，将&lt;code&gt;36KiB&lt;/code&gt;处的虚拟页映射到4级页表所在的物理帧，如虚线箭头所示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在，内核可以通过写入&lt;code&gt;0KiB&lt;/code&gt;页面来访问第2级页表，以及通过写入&lt;code&gt;36KiB&lt;/code&gt;页面来访问第4级页表。&lt;/p&gt;
&lt;p&gt;使用临时映射访问任意页表帧的过程为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在恒等映射的1级表中搜索未使用条目。&lt;/li&gt;
&lt;li&gt;将该条目映射到我们想要访问的页表所在的物理帧。&lt;/li&gt;
&lt;li&gt;通过映射到该条目的虚拟页面访问目标帧。&lt;/li&gt;
&lt;li&gt;将该条目设置回未使用状态，从而删除本次临时映射。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种方法重复使用相同的512个虚拟页面来创建映射，因此仅需要4KiB的物理内存。缺点是它有点麻烦，特别是因为新的映射可能需要修改多个级别的页表，这意味着我们需要将上述过程重复多次。&lt;/p&gt;
&lt;h3&gt;递归页表&lt;/h3&gt;
&lt;p&gt;另一个根本不需要附加页表的有趣方法是递归映射页表。这种方法的思路是将4级页表的某些条目映射到4级表本身。如此可以有效地保留一部分虚拟地址空间，并将所有当前和将来的页表帧映射到该空间。&lt;/p&gt;
&lt;p&gt;让我们通过一个例子来理解该方法是如何工作的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/recursive-page-table.png&quot; alt=&quot;递归页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这与本文&lt;a href=&quot;./#%E8%AE%BF%E9%97%AE%E9%A1%B5%E8%A1%A8&quot;&gt;开头示例&lt;/a&gt;的唯一区别是，4级表中索引511处的附加条目映射到了4级表本身的&lt;code&gt;4KiB&lt;/code&gt;物理帧。&lt;/p&gt;
&lt;p&gt;通过让CPU在转换中跟踪此条目，它不会到达3级表，而又回到这一4级表。这类似于调用自身的递归函数，因此此表称为&lt;em&gt;递归页表&lt;/em&gt;。重要的是，CPU假定4级表中的​​每个条目都指向3级表，因此现在将4级表视为3级表。这之所以可行，是因为所有级别的表在x86_64上的布局都完全相同。&lt;/p&gt;
&lt;p&gt;通过在开始实际转换之前访问一次或多次递归项，我们可以有效地减少CPU遍历的级别数。例如，如果我们只访问一次递归条目，然后进入3级表，则CPU会认为3级表是2级表。更进一步，它将2级表视为1级表，将1级表视为映射的帧。这意味着我们现在可以读写1级页表，因为CPU认为它是映射的帧。下图说明了5个翻译步骤：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/recursive-page-table-access-level-1.png&quot; alt=&quot;递归页表访问1级页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;类似的，在开始转换之前，我们可以两次访问递归项，以将遍历的级别数减少为两个：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/recursive-page-table-access-level-2.png&quot; alt=&quot;递归页表访问2级页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;让我们逐步观察改操作：首先，CPU访问4级表上的递归条目，并认为自己已到达3级表。然后，它再次访问递归条目，并认为自己已到达2级表。但实际上，CPU仍然位于4级表中。当CPU现在访问另一个条目时，它将进入在3级表，但认为自己已经在1级表上了。因此，当下一个条目指向2级表时，CPU认为它指向映射的帧，这使我们可以读写2级表。&lt;/p&gt;
&lt;p&gt;访问3级和4级表的工作方式相同。为了访问3级表，我们重复访问了3次递归项，使CPU认为它已经在1级表中了。然后，我们访问另一个条目并到达第3级表，CPU将其视为映射帧。要访问4级表本身，我们只需访问递归项四次，直到CPU将4级表本身视为映射帧（下图中的蓝色）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/recursive-page-table-access-level-3.png&quot; alt=&quot;递归页表访问3级页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;你可能需要花一些时间来理解这个思路，但是在实践中却非常有效。&lt;/p&gt;
&lt;p&gt;在下面的小节中，我们将解释如何构造虚拟地址以一次或多次访问递归项。我们不会在实现中使用递归分页，因此你可以跳过这一节继续阅读后文。&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;计算地址（选读）&lt;/h4&gt;
&lt;p&gt;可以看到在实际地址转换前通过一次或多次递归地访问条目来访问所有级别的页表。由于访问这四个级别的表的索引是直接从虚拟地址中派生的，因此我们需要为此方法构造特殊的虚拟地址。请记住，页表索引是通过以下方式从虚拟地址中派生的：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/x86_64-table-indices-from-address.svg&quot; alt=&quot;虚拟地址中的x86_64页表索引&quot; /&gt;&lt;/p&gt;
&lt;p&gt;假设我们要访问映射特定页面的1级页表。如上所述，这意味着在继续执行4级，3级和2级索引之前，我们必须访问一次递归项。为此，我们将地址的每个块向右移动一个块，并将原始4级索引设置为递归索引：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/table-indices-from-address-recursive-level-1.svg&quot; alt=&quot;1级递归地址的页表索引&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为了访问2级表，我们将每个索引块向右移动两个块，并将原始4级索引和原始3级索引的块都设置为递归索引：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/table-indices-from-address-recursive-level-2.svg&quot; alt=&quot;2级递归地址的页表索引&quot; /&gt;&lt;/p&gt;
&lt;p&gt;通过将每个块向右移动三个块并对原始4级，3级和2级地址块使用递归索引，便可以访问3级页表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/table-indices-from-address-recursive-level-3.svg&quot; alt=&quot;3级递归地址的页表索引&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后，我们可以通过将每个块向右移动四个块并使用除偏移量以外的所有地址块作为递归索引来访问4级表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/table-indices-from-address-recursive-level-4.svg&quot; alt=&quot;4级递归地址的页表索引&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在就可以计算全部四个级别的页表的虚拟地址了。我们甚至可以通过将其索引乘以8（页面表条目的大小）来计算精确指向特定页面表条目的地址。&lt;/p&gt;
&lt;p&gt;下表总结了用于访问不同级别页表帧的地址结构：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;用于访问&lt;/th&gt;
&lt;th&gt;虚拟地址结构(&lt;a href=&quot;https://en.wikipedia.org/wiki/Octal&quot;&gt;八进制&lt;/a&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;页&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0o_SSSSSS_AAA_BBB_CCC_DDD_EEEE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1级页表项&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0o_SSSSSS_RRR_AAA_BBB_CCC_DDDD&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2级页表项&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0o_SSSSSS_RRR_RRR_AAA_BBB_CCCC&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3级页表项&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0o_SSSSSS_RRR_RRR_RRR_AAA_BBBB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4级页表项&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0o_SSSSSS_RRR_RRR_RRR_RRR_AAAA&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;其中&lt;code&gt;AAA&lt;/code&gt;是4级索引，&lt;code&gt;BBB&lt;/code&gt;是3级索引，&lt;code&gt;CCC&lt;/code&gt;是2级索引，&lt;code&gt;DDD&lt;/code&gt;是映射帧的1级索引，而&lt;code&gt;EEEE&lt;/code&gt;是映射帧的偏移量。&lt;code&gt;RRR&lt;/code&gt;是递归条目的索引。当索引（三位数，译注：9位二进制数转换为3位八进制数）转换为偏移量（四位数，译注：12位二进制偏移地址转换为4位八进制数）时，可以通过将其乘以8（页表项的大小，译注：八进制下乘八相当于左移一位）来完成。有了这样的偏移量，结果地址就直接指向相应的页表条目。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SSSSSS&lt;/code&gt;是符号扩展位，这意味着它们都是第47位的副本。这是对x86_64架构上有效地址的特殊要求。我们在&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#paging-on-x86-64&quot;&gt;上一篇文章&lt;/a&gt;中对此进行了解释。&lt;/p&gt;
&lt;p&gt;之所以使用&lt;a href=&quot;https://en.wikipedia.org/wiki/Octal&quot;&gt;八进制&lt;/a&gt;表示地址，是因为每个八进制字符表示三个位，这使我们可以清楚地区分不同页表级别的9位索引。对于每个字符代表四个位的十六进制来说是不可能的。&lt;/p&gt;
&lt;h5&gt;&lt;em&gt;用Rust代码实现&lt;/em&gt;&lt;/h5&gt;
&lt;p&gt;要在Rust代码中构造这样的地址，可以使用位运算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 希望访问的指定页表的虚拟地址
let addr: usize = […];

let r = 0o777; // 递归索引
let sign = 0o177777 &amp;lt;&amp;lt; 48; // 符号扩展位

// 计算希望转换的页表索引
let l4_idx = (addr &amp;gt;&amp;gt; 39) &amp;amp; 0o777; // 4级索引
let l3_idx = (addr &amp;gt;&amp;gt; 30) &amp;amp; 0o777; // 3级索引
let l2_idx = (addr &amp;gt;&amp;gt; 21) &amp;amp; 0o777; // 2级索引
let l1_idx = (addr &amp;gt;&amp;gt; 12) &amp;amp; 0o777; // 1级索引
let page_offset = addr &amp;amp; 0o7777;

// 计算页表地址
let level_4_table_addr =
    sign | (r &amp;lt;&amp;lt; 39) | (r &amp;lt;&amp;lt; 30) | (r &amp;lt;&amp;lt; 21) | (r &amp;lt;&amp;lt; 12);
let level_3_table_addr =
    sign | (r &amp;lt;&amp;lt; 39) | (r &amp;lt;&amp;lt; 30) | (r &amp;lt;&amp;lt; 21) | (l4_idx &amp;lt;&amp;lt; 12);
let level_2_table_addr =
    sign | (r &amp;lt;&amp;lt; 39) | (r &amp;lt;&amp;lt; 30) | (l4_idx &amp;lt;&amp;lt; 21) | (l3_idx &amp;lt;&amp;lt; 12);
let level_1_table_addr =
    sign | (r &amp;lt;&amp;lt; 39) | (l4_idx &amp;lt;&amp;lt; 30) | (l3_idx &amp;lt;&amp;lt; 21) | (l2_idx &amp;lt;&amp;lt; 12);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码假定递归映射条目的索引为&lt;code&gt;0o777&lt;/code&gt;，即最后一个4级条目511。不过目前情况并非如此，因此代码尚无法工作。请参阅下文，了解如何告诉bootloader设置递归映射。&lt;/p&gt;
&lt;p&gt;除了手动执行按位运算之外，还可以使用&lt;code&gt;x86_64&lt;/code&gt;crate的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html&quot;&gt;&lt;code&gt;RecursivePageTable&lt;/code&gt;&lt;/a&gt;类型，该类型为各种页表操作提供安全的抽象。例如，以下代码展示了如何将虚拟地址转换为其映射的物理地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::structures::paging::{Mapper, Page, PageTable, RecursivePageTable};
use x86_64::{VirtAddr, PhysAddr};

/// 为4级页表创建`RecursivePageTable`实例
let level_4_table_addr = […];
let level_4_table_ptr = level_4_table_addr as *mut PageTable;
let recursive_page_table = unsafe {
    let level_4_table = &amp;amp;mut *level_4_table_ptr;
    RecursivePageTable::new(level_4_table).unwrap();
}

/// 为给出的虚拟地址计算物理地址
let addr: u64 = […]
let addr = VirtAddr::new(addr);
let page: Page = Page::containing_address(addr);

// 实现转换
let frame = recursive_page_table.translate_page(page);
frame.map(|frame| frame.start_address() + u64::from(addr.page_offset()))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与上一段代码相同，运行此代码需要有效的递归映射。使用这种映射，可以像上一个代码示例中那样计算给定的&lt;code&gt;level_4_table_addr&lt;/code&gt;。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;递归分页是一种有趣的技术，也展示了页表中的单个映射功能有多强大。它相对容易实现，只需要极少的设置（即设置一个递归项），因此它作为我们的第一个分页实验确实是一个不错选择。&lt;/p&gt;
&lt;p&gt;不过，它也有一些缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该方法会占用大量虚拟内存空间（512GiB，译注：即占用4级页表一个条目）。不过48位虚拟地址空间较大，这也不算是一个大问题，但这可能会导致次优的缓存行为。&lt;/li&gt;
&lt;li&gt;该方法仅允许轻松访问当前活动的地址空间。通过更改递归项，仍然可以访问其他地址空间，但是需要临时映射才能切换回去。我们在&lt;a href=&quot;https://os.phil-opp.com/remap-the-kernel/#overview&quot;&gt;&lt;em&gt;重新映射内核&lt;/em&gt;&lt;/a&gt;（已过时）一文中描述了如何执行此操作。&lt;/li&gt;
&lt;li&gt;该方法在很大程度上依赖于x86的页表格式，可能无法在其他架构中使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Bootloader支持&lt;/h2&gt;
&lt;p&gt;所有这些方法在初始化时都需要对其页表进行修改。例如，需要创建物理内存的映射，或者需要递归映射4级表的条目。目前的问题是我们还没有能够访问页表的方法，因此也无法创建这些必要的的映射。&lt;/p&gt;
&lt;p&gt;这意味着我们需要bootloader的帮助，该程序会创建内核运行的页表。bootloader可以访问页表，因此它可以创建我们需要的任何映射。在当前的实现中，&lt;code&gt;bootloader&lt;/code&gt;crate支持上面提到的两种方法，并可以通过&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/features.html#the-features-section&quot;&gt;cargo功能&lt;/a&gt;进行控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;map_physical_memory&lt;/code&gt;特性可以将整个物理内存映射到虚拟地址空间中的某处。因此，内核可以访问所有物理内存，于是我们可以实现&lt;a href=&quot;./#%E6%98%A0%E5%B0%84%E5%AE%8C%E6%95%B4%E7%9A%84%E7%89%A9%E7%90%86%E5%86%85%E5%AD%98&quot;&gt;&lt;em&gt;映射完整物理内存&lt;/em&gt;&lt;/a&gt;中的方法。&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;recursive_page_table&lt;/code&gt;特性，bootloader将递归映射4级页表的一个条目。这允许内核按照&lt;a href=&quot;./#%E9%80%92%E5%BD%92%E9%A1%B5%E8%A1%A8&quot;&gt;&lt;em&gt;递归页表&lt;/em&gt;&lt;/a&gt;部分中的描述访问页面表。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们为内核选择第一种方法，因为它简单，平台独立且功能更强大（还允许访问非页表帧）。为了启用所需的bootloader支持，我们将&lt;code&gt;map_physical_memory&lt;/code&gt;特性添加到了&lt;code&gt;bootloader&lt;/code&gt;的依赖项中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
bootloader = { version = &quot;0.9.8&quot;, features = [&quot;map_physical_memory&quot;]}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启用此特性后，bootloader会将完整的物理内存映射到一些未使用的虚拟地址范围。为了将希望使用的虚拟地址范围告诉内核，引导加载程序会传递一个&lt;em&gt;引导信息&lt;/em&gt;结构体。&lt;/p&gt;
&lt;h3&gt;引导信息&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bootloader&lt;/code&gt;crate定义了一个&lt;a href=&quot;https://docs.rs/bootloader/0.9.3/bootloader/bootinfo/struct.BootInfo.html&quot;&gt;&lt;code&gt;BootInfo&lt;/code&gt;&lt;/a&gt;结构体，包含传递给内核的所有信息。该结构体仍处于早期阶段，因此在更新为将来与&lt;a href=&quot;https://doc.rust-lang.org/stable/cargo/reference/specifying-dependencies.html#caret-requirements&quot;&gt;语义版本不兼容&lt;/a&gt;的bootloader版本时，可能会造成损坏。启用&lt;code&gt;map_physical_memory&lt;/code&gt;特性后，它将包含&lt;code&gt;memory_map&lt;/code&gt;和&lt;code&gt;physical_memory_offset&lt;/code&gt;两个字段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;memory_map&lt;/code&gt;字段包含可用物理内存的概述。该字段告诉内核系统中有多少可用物理内存，以及哪些内存区域是为诸如VGA硬件之类的设备所保留的。可以从BIOS或UEFI固件查询内存映射，但查询只能在启动过程的早期。也正是由于这个原因，内存映射必须由bootloader提供，因为内核无法在之后检索该映射。在下文中，我们将需要内存映射。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;physical_memory_offset&lt;/code&gt;字段包含物理内存映射到虚拟地址的起始地址。通过将此偏移量添加到物理地址，即可获得相应的虚拟地址。这使我们可以从内核访问任意物理内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;bootloader将&lt;code&gt;BootInfo&lt;/code&gt;结构体以&lt;code&gt;_start&lt;/code&gt;函数的&lt;code&gt;＆&apos;static BootInfo&lt;/code&gt;参数的形式传递给内核。我们尚未在该函数中声明此参数，按照下面的方式修改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bootloader::BootInfo;

#[no_mangle]
pub extern &quot;C&quot; fn _start(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! { // new argument
    […]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在前面的文章中，我们一直都缺少该参数也并没有造成什么问题，因为x86_64调用约定在CPU寄存器中传递了第一个参数。因此，若不声明该参数，只会使得参数被忽略。但是，如果我们不小心使用了错误的参数类型，那将会造成问题，因为编译器并不知道我们入口点函数的正确类型签名。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;entry_point&lt;/code&gt;宏&lt;/h3&gt;
&lt;p&gt;由于&lt;code&gt;_start&lt;/code&gt;函数是从bootloader外部调用的，因此不会检查该函数的签名。这意味着我们可以让该函数接受任意参数也不产生任何编译错误，但是函数将无法运行或在运行时导致未定义的行为。&lt;/p&gt;
&lt;p&gt;为了确保入口点函数始终具有bootloader期望的正确签名，&lt;code&gt;bootloader&lt;/code&gt;crate提供了&lt;a href=&quot;https://docs.rs/bootloader/0.6.4/bootloader/macro.entry_point.html&quot;&gt;&lt;code&gt;entry_point&lt;/code&gt;&lt;/a&gt;宏，这个宏提供了类型检查的方式来将Rust函数定义为入口点。让我们使用此宏重写入口点函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bootloader::{BootInfo, entry_point};

entry_point!(kernel_main);

fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    […]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们不再需要使用&lt;code&gt;extern &quot;C&quot;&lt;/code&gt;或&lt;code&gt;no_mangle&lt;/code&gt;修饰入口点，因为该宏为我们在底层定义了真正&lt;code&gt;_start&lt;/code&gt;入口点。现在，&lt;code&gt;kernel_main&lt;/code&gt;函数就是一个普通的的Rust函数，因此我们可以为其选择一个任意名称。重要的是对它进行类型检查，以便在我们使用错误的函数签名时（例如通过添加参数或更改参数类型）产生编译错误。&lt;/p&gt;
&lt;p&gt;让我们在&lt;code&gt;lib.rs&lt;/code&gt;中做出相同的更改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[cfg(test)]
use bootloader::{entry_point, BootInfo};

#[cfg(test)]
entry_point!(test_kernel_main);

/// Entry point for `cargo test`
#[cfg(test)]
fn test_kernel_main(_boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    // like before
    init();
    test_main();
    hlt_loop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于入口点仅在测试模式下使用，因此我们为本次修改的条目均添加&lt;code&gt;#[cfg(test)]&lt;/code&gt;属性。此外，为了避免与&lt;code&gt;main.rs&lt;/code&gt;的&lt;code&gt;kernel_main&lt;/code&gt;混淆，我们也为测试入口点指定了不同的名称的函数&lt;code&gt;test_kernel_main&lt;/code&gt;。目前暂时不使用&lt;code&gt;BootInfo&lt;/code&gt;参数，于是我们在参数名称前添加&lt;code&gt;_&lt;/code&gt;前缀以消除未使用某变量的编译警告。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;现在，我们可以访问物理内存了，也终于可以开始实现页表代码了。第一步，我们要看一下内核正在运行的当前活动页表。第二步，我们将创建一个转换函数，该函数返回给定虚拟地址所映射的物理地址。最后一步，我们将尝试修改页表以创建新的映射。&lt;/p&gt;
&lt;p&gt;在开始之前，我们为代码创建一个新的&lt;code&gt;memory&lt;/code&gt;模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod memory;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再为该模块创建一个空的&lt;code&gt;src/memory.rs&lt;/code&gt;文件。&lt;/p&gt;
&lt;h3&gt;访问页表&lt;/h3&gt;
&lt;p&gt;在&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#accessing-the-page-tables&quot;&gt;上一篇文章的末尾&lt;/a&gt;，我们试图观察内核运行的页表，但是由于无法访问&lt;code&gt;CR3&lt;/code&gt;寄存器指向的物理帧而失败。现在我们将接着上一篇文章，通过创建一个&lt;code&gt;active_level_4_table&lt;/code&gt;函数来返回对活动4级页表的引用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::{
    structures::paging::PageTable,
    VirtAddr,
};

/// 返回4级页表的可变引用
/// 
/// 该函数为非安全，因为调用者必须保证已将完整的物理内存
/// 映射到偏移量为`physical_memory_offset`的虚拟内存中了。
/// 同时，该函数只能被调用一次，以避免产生其他`＆mut`引用（可能会造成未定义的行为）。
pub unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
    -&amp;gt; &amp;amp;&apos;static mut PageTable
{
    use x86_64::registers::control::Cr3;

    let (level_4_table_frame, _) = Cr3::read();

    let phys = level_4_table_frame.start_address();
    let virt = physical_memory_offset + phys.as_u64();
    let page_table_ptr: *mut PageTable = virt.as_mut_ptr();

    &amp;amp;mut *page_table_ptr // unsafe
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们从&lt;code&gt;CR3&lt;/code&gt;寄存器中读取活动4级表的物理帧。然后，我们获取其物理起始地址，将其转换为&lt;code&gt;u64&lt;/code&gt;，再为其添加&lt;code&gt;physical_memory_offset&lt;/code&gt;，以获取映射页表帧的虚拟地址。最后，我们通过&lt;code&gt;as_mut_ptr&lt;/code&gt;方法将虚拟地址转换为&lt;code&gt;*mut PageTable&lt;/code&gt;裸指针，再为该指针非安全地创建&lt;code&gt;＆mut PageTable&lt;/code&gt;引用。创建&lt;code&gt;＆mut&lt;/code&gt;引用而非&lt;code&gt;＆&lt;/code&gt;引用，是因为我们将在下文对页面表进行修改。&lt;/p&gt;
&lt;p&gt;这里不需要使用非安全块，因为Rust会将&lt;code&gt;unsafe fn&lt;/code&gt;函数体当做一个大型&lt;code&gt;unsafe&lt;/code&gt;块来对待。这会使代码更加危险，可能稍不注意就会在前几行中意外引入非安全操作。这也使发现非安全操作变得更加困难。目前有一个&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/2585&quot;&gt;RFC&lt;/a&gt;提出对此行为的修改。&lt;/p&gt;
&lt;p&gt;现在，我们可以使用此函数来打印4级表的条目：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    use blog_os::memory::active_level_4_table;
    use x86_64::VirtAddr;

    println!(&quot;Hello World{}&quot;, &quot;!&quot;);
    blog_os::init();

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    let l4_table = unsafe { active_level_4_table(phys_mem_offset) };

    for (i, entry) in l4_table.iter().enumerate() {
        if !entry.is_unused() {
            println!(&quot;L4 Entry {}: {:?}&quot;, i, entry);
        }
    }

    // as before
    #[cfg(test)]
    test_main();

    println!(&quot;It did not crash!&quot;);
    blog_os::hlt_loop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们将&lt;code&gt;BootInfo&lt;/code&gt;结构体重的&lt;code&gt;physical_memory_offset&lt;/code&gt;字段转换为&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/addr/struct.VirtAddr.html&quot;&gt;&lt;code&gt;VirtAddr&lt;/code&gt;&lt;/a&gt;，再传给&lt;code&gt;active_level_4_table&lt;/code&gt;函数。然后，使用&lt;code&gt;iter&lt;/code&gt;函数迭代页表条目，并使用&lt;a href=&quot;https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate&quot;&gt;&lt;code&gt;enumerate&lt;/code&gt;&lt;/a&gt;函数为每个元素添加遍历索引&lt;code&gt;i&lt;/code&gt;。我们仅打印非空条目，因为全部512个条目无法一起显示在屏幕上。&lt;/p&gt;
&lt;p&gt;运行可以看到以下输出：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/qemu-print-level-4-table.png&quot; alt=&quot;QEMU打印4级页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到了多个非空条目，它们都映射到不同的3级表。因为内核代码、内核堆栈、物理内存映射和引导信息都会使用单独的内存区域。&lt;/p&gt;
&lt;p&gt;为了进一步遍历页表并查看第3级表，我们可以将条目的映射帧再次转换为虚拟地址：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::structures::paging::PageTable;

if !entry.is_unused() {
    println!(&quot;L4 Entry {}: {:?}&quot;, i, entry);

    // 获取物理地址条目并再次转换
    let phys = entry.frame().unwrap().start_address();
    let virt = phys.as_u64() + boot_info.physical_memory_offset;
    let ptr = VirtAddr::new(virt).as_mut_ptr();
    let l3_table: &amp;amp;PageTable = unsafe { &amp;amp;*ptr };

    // 打印3级表的非空条目
    // print non-empty entries of the level 3 table
    for (i, entry) in l3_table.iter().enumerate() {
        if !entry.is_unused() {
            println!(&quot;  L3 Entry {}: {:?}&quot;, i, entry);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至于查看2级和1级表，只需对3级和2级条目重复该过程即可。你可以想象，这很快就会变得非常冗长，因此我们在这里不显示完整的代码。&lt;/p&gt;
&lt;p&gt;手动遍历页表很有趣，因为它有助于了解CPU如何执行转换。但是，大多数时候我们只对给定虚拟地址的映射物理地址感兴趣，因此让我们为其创建一个函数。&lt;/p&gt;
&lt;h3&gt;转换地址&lt;/h3&gt;
&lt;p&gt;为了将虚拟地址转换为物理地址，我们必须遍历全部四级页表，直到到达映射的帧为止。让我们创建一个执行此转换的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::PhysAddr;

/// 将指定的虚拟地址转换为映射的物理地址，如果该地址未被映射则返回`None`。
///
/// 该函数为非安全，因为调用者必须保证已将完整的物理内存
/// 映射到偏移量为`physical_memory_offset`的虚拟内存中了。
pub unsafe fn translate_addr(addr: VirtAddr, physical_memory_offset: VirtAddr)
    -&amp;gt; Option&amp;lt;PhysAddr&amp;gt;
{
    translate_addr_inner(addr, physical_memory_offset)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再将该函数传递给安全函数&lt;code&gt;translate_addr_inner&lt;/code&gt;，以限制非安全操作的范围。前面提到，Rust会将&lt;code&gt;unsafe fn&lt;/code&gt;函数体当做一个大型&lt;code&gt;unsafe&lt;/code&gt;块来对待。因此，通过调用私有安全函数，可以再次明确每个不安全操作。&lt;/p&gt;
&lt;p&gt;其中的私有函数包含实际的实现细节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// `translate_addr`调用的私有函数
///
/// 该函数可以安全地限制`unsafe`操作的范围，因为Rust将非安全函数整体视为非安全块。
/// 该函数只能通过该模块外部的`unsafe fn`来访问。
fn translate_addr_inner(addr: VirtAddr, physical_memory_offset: VirtAddr)
    -&amp;gt; Option&amp;lt;PhysAddr&amp;gt;
{
    use x86_64::structures::paging::page_table::FrameError;
    use x86_64::registers::control::Cr3;

    // 从CR3寄存器中读取活动的4级帧
    let (level_4_table_frame, _) = Cr3::read();

    let table_indexes = [
        addr.p4_index(), addr.p3_index(), addr.p2_index(), addr.p1_index()
    ];
    let mut frame = level_4_table_frame;

    // 遍历多级页表
    for &amp;amp;index in &amp;amp;table_indexes {
        // 将帧转换为页表的引用
        let virt = physical_memory_offset + frame.start_address().as_u64();
        let table_ptr: *const PageTable = virt.as_ptr();
        let table = unsafe {&amp;amp;*table_ptr};

        // 读取页表条目并更新`frame`变量
        let entry = &amp;amp;table[index];
        frame = match entry.frame() {
            Ok(frame) =&amp;gt; frame,
            Err(FrameError::FrameNotPresent) =&amp;gt; return None,
            Err(FrameError::HugeFrame) =&amp;gt; panic!(&quot;huge pages not supported&quot;),
        };
    }

    // 加上页偏移量得到物理地址
    Some(frame.start_address() + u64::from(addr.page_offset()))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们不选择复用&lt;code&gt;active_level_4_table&lt;/code&gt;函数，而是再次从&lt;code&gt;CR3&lt;/code&gt;寄存器中读取4级帧。这样做可以简化此原型实现。不用担心，我们将在稍后创建一个更好的解决方案。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;VirtAddr&lt;/code&gt;结构体已经提供了用于计算进入四个级别页表的索引的方法。我们将这些索引存储在一个小的数组中，之后便可以使用&lt;code&gt;for&lt;/code&gt;循环遍历页表。在循环之外，我们记录最后被访问的&lt;code&gt;frame&lt;/code&gt;，以便稍后计算其物理地址。该帧在迭代时指向页表帧，并在最后一次迭代后（即在访问1级条目之后）指向映射的帧。&lt;/p&gt;
&lt;p&gt;在循环内部，我们再次使用&lt;code&gt;physical_memory_offset&lt;/code&gt;将帧转换为页表引用。然后，我们读取当前页表的条目，并使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html#method.frame&quot;&gt;&lt;code&gt;PageTableEntry::frame&lt;/code&gt;&lt;/a&gt;函数检索映射的帧。如果条目未映射到帧，则返回&lt;code&gt;None&lt;/code&gt;。如果条目映射到2MiB或1GiB的巨页，则产生panic。&lt;/p&gt;
&lt;p&gt;让我们通过转换一些地址来测试转换函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    // new import
    use blog_os::memory::translate_addr;

    […] // hello world and blog_os::init

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);

    let addresses = [
        // 恒等映射VGA缓冲区页
        0xb8000,
        // 某代码页
        0x201008,
        // 某栈页
        0x0100_0020_1a10,
        // 映射到物理地址0的虚拟地址
        boot_info.physical_memory_offset,
    ];

    for &amp;amp;address in &amp;amp;addresses {
        let virt = VirtAddr::new(address);
        let phys = unsafe { translate_addr(virt, phys_mem_offset) };
        println!(&quot;{:?} -&amp;gt; {:?}&quot;, virt, phys);
    }

    […] // test_main(), &quot;it did not crash&quot; printing, and hlt_loop()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行可以看到以下输出：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/qemu-translate-addr.png&quot; alt=&quot;QEMU地址转换&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如预期的那样，恒等映射的地址&lt;code&gt;0xb8000&lt;/code&gt;转换为相同的物理地址。代码页和栈页转换为一些随机的物理地址，这取决于bootloader如何为内核创建初始映射。值得注意的是，转换后的最后12位（译注：在图中的十六进制体现为最后3位）始终保持不变，这是合理的，因为这些位是&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#paging-on-x86-64&quot;&gt;&lt;em&gt;页面偏移量&lt;/em&gt;&lt;/a&gt;，而不是被转换地址的一部分。&lt;/p&gt;
&lt;p&gt;由于可以通过加上&lt;code&gt;physical_memory_offset&lt;/code&gt;来访问每个物理地址，因此&lt;code&gt;physical_memory_offset&lt;/code&gt;地址本身的转换应指向物理地址&lt;code&gt;0&lt;/code&gt;。但是，转换失败，因为该映射使用巨页来提高效率，但我们的实现尚不支持巨页。&lt;/p&gt;
&lt;h3&gt;使用&lt;code&gt;OffsetPageTable&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;将虚拟地址转换为物理地址是OS内核中的常见任务，因此&lt;code&gt;x86_64&lt;/code&gt;crate为其提供了一种抽象。该实现已经支持巨页和除&lt;code&gt;translate_addr&lt;/code&gt;之外的其他几个页表函数，因此下文将使用该抽象进行操作，而不是在我们自己的实现中手动添加对巨页的支持。&lt;/p&gt;
&lt;p&gt;该抽象基于两个trait，它们定义了各种页表映射函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html&quot;&gt;&lt;code&gt;Mapper&lt;/code&gt;&lt;/a&gt; trait的泛型约束为&lt;code&gt;PageSize&lt;/code&gt;，它提供操作页面的函数。例如：&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#tymethod.translate_page&quot;&gt;&lt;code&gt;translate_page&lt;/code&gt;&lt;/a&gt;用于将给定页面转换为其相应大小的帧，&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to&quot;&gt;&lt;code&gt;map_to&lt;/code&gt;&lt;/a&gt;函数在页表中创建新的映射。&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html&quot;&gt;&lt;code&gt;Translate&lt;/code&gt;&lt;/a&gt; trait提供了适用于多种页面大小的函数，例如&lt;code&gt;translate_addr&lt;/code&gt;或普通&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html&quot;&gt;&lt;code&gt;translate&lt;/code&gt;&lt;/a&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;trait仅定义了接口，并未提供任何实现。&lt;code&gt;x86_64&lt;/code&gt;crate当前提供三种类型，这些类型按照不同需求实现了这些trait。&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html&quot;&gt;&lt;code&gt;OffsetPageTable&lt;/code&gt;&lt;/a&gt;类型假定完整的物理内存以某个偏移量全部映射到虚拟地址空间。&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MappedPageTable.html&quot;&gt;&lt;code&gt;MappedPageTable&lt;/code&gt;&lt;/a&gt;更加灵活一些：它只假定每个页表帧均被映射到了一个位于虚拟地址空间中的可计算地址。最后，可以使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.RecursivePageTable.html&quot;&gt;&lt;code&gt;RecursivePageTable&lt;/code&gt;&lt;/a&gt;类型通过&lt;a href=&quot;./#%E9%80%92%E5%BD%92%E9%A1%B5%E8%A1%A8&quot;&gt;递归页表&lt;/a&gt;访问页表帧。&lt;/p&gt;
&lt;p&gt;对我们来说，bootloader将完整的物理内存映射到附加&lt;code&gt;physical_memory_offset&lt;/code&gt;偏移量的虚拟地址，因此我们可以使用&lt;code&gt;OffsetPageTable&lt;/code&gt;类型。要初始化该类型，我们在内存模块中创建一个新的&lt;code&gt;init&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::structures::paging::OffsetPageTable;

// 初始化OffsetPageTable
///
/// 该函数为非安全，因为调用者必须保证已将完整的物理内存
/// 映射到偏移量为`physical_memory_offset`的虚拟内存中了。
/// 同时，该函数只能被调用一次，以避免产生其他`＆mut`引用（可能会造成未定义的行为）。
pub unsafe fn init(physical_memory_offset: VirtAddr) -&amp;gt; OffsetPageTable&amp;lt;&apos;static&amp;gt; {
    let level_4_table = active_level_4_table(physical_memory_offset);
    OffsetPageTable::new(level_4_table, physical_memory_offset)
}

// make private
unsafe fn active_level_4_table(physical_memory_offset: VirtAddr)
    -&amp;gt; &amp;amp;&apos;static mut PageTable
{…}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数将&lt;code&gt;physical_memory_offset&lt;/code&gt;作为参数，新建并返回一个具有&lt;code&gt;&apos;static&lt;/code&gt;生命周期的&lt;code&gt;OffsetPageTable&lt;/code&gt;实例。这意味着该实例在内核的完整运行时始终保持有效。在函数主体中，我们首先调用&lt;code&gt;active_level_4_table&lt;/code&gt;函数获取4级页表的可变引用。然后，我们将此引用作为第一个参数传递给&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.OffsetPageTable.html#method.new&quot;&gt;&lt;code&gt;OffsetPageTable::new&lt;/code&gt;&lt;/a&gt;函数。我们使用&lt;code&gt;physical_memory_offset&lt;/code&gt;变量作为第二个参数传递给&lt;code&gt;new&lt;/code&gt;函数，该参数期望得到虚拟地址映射到物理地址的起点（译注：即虚拟地址映射到物理地址时附加的偏移量）。&lt;/p&gt;
&lt;p&gt;从现在开始，仅应从&lt;code&gt;init&lt;/code&gt;函数调用&lt;code&gt;active_level_4_table&lt;/code&gt;函数，因为当多次调用它时，很容使可变的引用产生多个别名，而这可能会导致未定义的行为。因此，应通过删除&lt;code&gt;pub&lt;/code&gt;关键字来使该函数变为私有。&lt;/p&gt;
&lt;p&gt;现在，我们可以使用&lt;code&gt;Translate::translate_addr&lt;/code&gt;方法来代替我们自己的&lt;code&gt;memory::translate_addr&lt;/code&gt;函数。只需要在&lt;code&gt;kernel_main&lt;/code&gt;中做几行更改：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    // new: different imports
    use blog_os::memory;
    use x86_64::{structures::paging::Translate, VirtAddr};

    […] // hello world and blog_os::init

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    // new: initialize a mapper
    let mapper = unsafe { memory::init(phys_mem_offset) };

    let addresses = […]; // same as before

    for &amp;amp;address in &amp;amp;addresses {
        let virt = VirtAddr::new(address);
        // new: use the `mapper.translate_addr` method
        let phys = mapper.translate_addr(virt);
        println!(&quot;{:?} -&amp;gt; {:?}&quot;, virt, phys);
    }

    […] // test_main(), &quot;it did not crash&quot; printing, and hlt_loop()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要导入&lt;code&gt;Translate&lt;/code&gt;trait以使用它提供的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Translate.html#method.translate_addr&quot;&gt;&lt;code&gt;translate_addr&lt;/code&gt;&lt;/a&gt;方法。&lt;/p&gt;
&lt;p&gt;此时运行，我们会看到与以前相同的转换结果，不同之处在于巨页也可以转换了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/qemu-mapper-translate-addr.png&quot; alt=&quot;QEMU使用mapper转换虚拟地址&quot; /&gt;&lt;/p&gt;
&lt;p&gt;不出所料，&lt;code&gt;0xb8000&lt;/code&gt;、代码地址和栈地址的转换结果与我们自己实现的转换函数相同。此外，我们现在看到虚拟地址&lt;code&gt;physical_memory_offset&lt;/code&gt;映射到物理地址&lt;code&gt;0x0&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;通过使用&lt;code&gt;MappedPageTable&lt;/code&gt;类型的转换功能，我们就没必要自己实现对巨页的支持了。另外，还可以访问其他页面函数——如&lt;code&gt;map_to&lt;/code&gt;——我们将在下一节中使用。&lt;/p&gt;
&lt;p&gt;现在，我们不再需要前面手动实现的&lt;code&gt;memory::translate_addr&lt;/code&gt;和&lt;code&gt;memory::translate_addr_inner&lt;/code&gt;函数了，因此可以将它们删除。&lt;/p&gt;
&lt;h3&gt;创建新映射&lt;/h3&gt;
&lt;p&gt;到目前为止，我们仅查看了页表，还从未修改页表。让我们通过为先前未映射的页面创建一个新的映射来试着修改页表。&lt;/p&gt;
&lt;p&gt;该操作将使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html&quot;&gt;&lt;code&gt;Mapper&lt;/code&gt;&lt;/a&gt; trait的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to&quot;&gt;&lt;code&gt;map_to&lt;/code&gt;&lt;/a&gt;函数来实现，让我们首先看一下这个函数。文档说明该函数有四个参数，分别是：想要映射的页面，该页面应映射到的帧，要为该页表项设置的标志，以及&lt;code&gt;frame_allocator&lt;/code&gt;。这里需要帧分配函数，是因为映射给定页面时，可能需要创建新的页表，而这个过程需要新给页表分配未使用的帧。&lt;/p&gt;
&lt;h4&gt;一个示例函数&lt;code&gt;create_example_mapping&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;第一步是创建一个新的&lt;code&gt;create_example_mapping&lt;/code&gt;函数，该函数将给定的虚拟页面映射到VGA文本缓冲区的物理帧&lt;code&gt;0xb8000&lt;/code&gt;。我们选择该帧是因为它使我们能够轻松测试映射是否被正确创建：我们只需要对新映射的页面进行写入，就可以在屏幕上观察到写入是否成功。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;create_example_mapping&lt;/code&gt;函数如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::{
    PhysAddr,
    structures::paging::{Page, PhysFrame, Mapper, Size4KiB, FrameAllocator}
};

/// Creates an example mapping for the given page to frame `0xb8000`.
pub fn create_example_mapping(
    page: Page,
    mapper: &amp;amp;mut OffsetPageTable,
    frame_allocator: &amp;amp;mut impl FrameAllocator&amp;lt;Size4KiB&amp;gt;,
) {
    use x86_64::structures::paging::PageTableFlags as Flags;

    let frame = PhysFrame::containing_address(PhysAddr::new(0xb8000));
    let flags = Flags::PRESENT | Flags::WRITABLE;

    let map_to_result = unsafe {
        // FIXME: 不安全用法，仅演示用
        mapper.map_to(page, frame, flags, frame_allocator)
    };
    map_to_result.expect(&quot;map_to failed&quot;).flush();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除了需要被映射的&lt;code&gt;page&lt;/code&gt;之外，该函数还需要&lt;code&gt;OffsetPageTable&lt;/code&gt;实例的可变引用和一个&lt;code&gt;frame_allocator&lt;/code&gt;。&lt;code&gt;frame_allocator&lt;/code&gt;参数使用&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters&quot;&gt;&lt;code&gt;impl Trait&lt;/code&gt;&lt;/a&gt;语法约束该&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-00-generics.html&quot;&gt;泛型&lt;/a&gt;参数必须实现&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/trait.FrameAllocator.html&quot;&gt;&lt;code&gt;FrameAllocator&lt;/code&gt;&lt;/a&gt; trait。而该trait又约束了其泛型参数必须实现&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page/trait.PageSize.html&quot;&gt;&lt;code&gt;PageSize&lt;/code&gt;&lt;/a&gt; trait，以便同时支持4KiB标准页和2MiB/1GiB巨页。我们只想创建一个4KiB映射，因此我们将泛型参数设置为&lt;code&gt;Size4KiB&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to&quot;&gt;&lt;code&gt;map_to&lt;/code&gt;&lt;/a&gt;为非安全方法，而调用者必须确保该帧未被使用。两次映射同一帧会导致未定义行为，例如，当两个不同的&lt;code&gt;＆mut&lt;/code&gt;引用指向同一物理内存位置时。在我们的例子中，确实也二次映射了已被映射的VGA文本缓冲区帧，因此打破了所需的安全条件。不过，&lt;code&gt;create_example_mapping&lt;/code&gt;函数只是一个临时测试函数，在后文中将被删除，也无伤大雅。我们在该行上添加了&lt;code&gt;FIXME&lt;/code&gt;注释，以提醒我们这种不安全用法。&lt;/p&gt;
&lt;p&gt;除了&lt;code&gt;page&lt;/code&gt;和&lt;code&gt;unused_frame&lt;/code&gt;外，&lt;code&gt;map_to&lt;/code&gt;方法还使用了一组用于映射的标志，和一个&lt;code&gt;frame_allocator&lt;/code&gt;的引用，稍后将对此分配器进行说明。设置&lt;code&gt;PRESENT&lt;/code&gt;是因为所有有效条目都需要该标志，而&lt;code&gt;WRITABLE&lt;/code&gt;标志则使映射的页面可写入。关于所有可用的标志的列表，请参见上一篇文章的&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#page-table-format&quot;&gt;&lt;em&gt;页表格式&lt;/em&gt;&lt;/a&gt;一节。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/trait.Mapper.html#method.map_to&quot;&gt;&lt;code&gt;map_to&lt;/code&gt;&lt;/a&gt;函数可能会失败，因此它将返回一个&lt;a href=&quot;https://doc.rust-lang.org/core/result/enum.Result.html&quot;&gt;&lt;code&gt;Result&lt;/code&gt;&lt;/a&gt;。这只是示例用代码，不需要高鲁棒性，因此我们在发生panic时仅使用&lt;a href=&quot;https://doc.rust-lang.org/core/result/enum.Result.html#method.expect&quot;&gt;&lt;code&gt;expect&lt;/code&gt;&lt;/a&gt;应付。调用成功时该函数将返回&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html&quot;&gt;&lt;code&gt;MapperFlush&lt;/code&gt;&lt;/a&gt;类型，该类型提供了一种调用其&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/mapper/struct.MapperFlush.html#method.flush&quot;&gt;&lt;code&gt;flush&lt;/code&gt;&lt;/a&gt;方法即可从转换后备缓冲区（TLB）中刷新新映射页面的简便方法。像&lt;code&gt;Result&lt;/code&gt;一样，该类型也使用&lt;a href=&quot;https://doc.rust-lang.org/std/result/#results-must-be-used&quot;&gt;&lt;code&gt;#[must_use]&lt;/code&gt;&lt;/a&gt;属性在我们意外忘记调用&lt;code&gt;flush&lt;/code&gt;方法时发出警告。&lt;/p&gt;
&lt;h4&gt;一个假的&lt;code&gt;FrameAllocator&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;为了能够调用&lt;code&gt;create_example_mapping&lt;/code&gt;，首先需要创建一个实现了&lt;code&gt;FrameAllocator&lt;/code&gt;trait的类型。如上所述，如果&lt;code&gt;map_to&lt;/code&gt;需要新的帧，则该trait就负责为新页表分配帧。&lt;/p&gt;
&lt;p&gt;让我们先从简单的情况入手：假设我们不需要创建新的页表。在这种情况下，这个帧分配器始终返回&lt;code&gt;None&lt;/code&gt;就足够了。下面的代码创建了一个&lt;code&gt;EmptyFrameAllocator&lt;/code&gt;来测试我们的映射函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// 一个始终返回`None`的帧分配器
pub struct EmptyFrameAllocator;

unsafe impl FrameAllocator&amp;lt;Size4KiB&amp;gt; for EmptyFrameAllocator {
    fn allocate_frame(&amp;amp;mut self) -&amp;gt; Option&amp;lt;PhysFrame&amp;gt; {
        None
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;FrameAllocator&lt;/code&gt;的实现是非安全的，因为实现者必须保证分配器仅分配未使用的帧。否则，可能会发生不确定的行为，例如，当两个虚拟页面映射到同一物理帧上时。而我们的&lt;code&gt;EmptyFrameAllocator&lt;/code&gt;只返回&lt;code&gt;None&lt;/code&gt;，因此并不出现生这种问题。&lt;/p&gt;
&lt;h4&gt;选择一个虚拟页面&lt;/h4&gt;
&lt;p&gt;现在，我们有一个简单的帧分配器，可以将其传递给&lt;code&gt;create_example_mapping&lt;/code&gt;函数了。不过这个分配器始终返回&lt;code&gt;None&lt;/code&gt;，所以它也就只能用在创建映射时并不需要额外的页表帧的情况了。为了了解何时需要额外页表帧以及何时不需要额外页表帧，让我们看一个示例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/required-page-frames-example.svg&quot; alt=&quot;需要页表帧的例子&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图的左侧为虚拟地址空间，右侧为物理地址空间，中间为页表。页表如虚线所示，存储在物理存储帧中。虚拟地址空间在地址&lt;code&gt;0x803fe00000&lt;/code&gt;中包含一个映射的页面，以蓝色标记。为了将此页面转换为其所在的帧，CPU遍历全部4级页表，直到到达地址为&lt;code&gt;36KiB&lt;/code&gt;的帧。&lt;/p&gt;
&lt;p&gt;此外，图中以红色显示VGA文本缓冲区的物理帧。我们的目标是使用&lt;code&gt;create_example_mapping&lt;/code&gt;函数将先前未映射的虚拟页面映射到此帧。不过由于&lt;code&gt;EmptyFrameAllocator&lt;/code&gt;始终返回&lt;code&gt;None&lt;/code&gt;，我们自然希望创建映射时不会用到该类型来分配额外的帧。而这取决于我们为映射选择的虚拟页面。&lt;/p&gt;
&lt;p&gt;图中以黄色标记了虚拟地址空间中的两个候选页面，一个位于地址&lt;code&gt;0x803fdfd000&lt;/code&gt;，就在被映射页（蓝色）之前3页。该地址的4级和3级页表索引与蓝页相同（译注：索引为别为1、0），但2级和1级索引却不同（参阅&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#paging-on-x86-64&quot;&gt;上一篇文章&lt;/a&gt;，译注：索引分别为126、125）。2级表中的索引不同意味着此页面使用了不同的1级表。如果我们选择该页作为我们示例中的映射，那么由于1级表尚不存在，就需要创建该表，也就需要一个额外的未使用的物理帧。而位于地址&lt;code&gt;0x803fe02000&lt;/code&gt;的另一个候选页面则不存在此问题，因为它使用与蓝色页面相同的1级页表（译注：索引分别为1、0、127、2）。因此，所有必需的页表已经存在。&lt;/p&gt;
&lt;p&gt;总之，创建新映射的难度取决于我们要映射的虚拟页面。在最简单的情况下，该页面的1级页表已经存在，我们只需要写入一个条目即可。在最困难的情况下，该页面所在的内存区域中尚不存在3级表，因此我们需要首先创建新的3级表、2级表、1级表。&lt;/p&gt;
&lt;p&gt;为了让&lt;code&gt;create_example_mapping&lt;/code&gt;函数能够使用&lt;code&gt;EmptyFrameAllocator&lt;/code&gt;类型，我们需要选择一个所有页表均已存在的页面。要找到这样的页面，我们可以利用bootloader会将自身加载到虚拟地址空间的第一个兆字节中这一行为。这意味着该区域的所有页面都存在一个有效的1级表。因此，我们可以在此内存区域中选择任何未使用的页面作为示例映射，比如选择地址为&lt;code&gt;0&lt;/code&gt;的页面。通常，该页面应保持未使用状态，以确保解引用空指针会导致页面错误，因此我们知道bootloader将该地址保留为未映射状态。&lt;/p&gt;
&lt;h4&gt;创建映射&lt;/h4&gt;
&lt;p&gt;至此，我们准备好了&lt;code&gt;create_example_mapping&lt;/code&gt;函数所需的所有参赛，现在可以修改&lt;code&gt;kernel_main&lt;/code&gt;函数，以将映射位于虚拟地址&lt;code&gt;0&lt;/code&gt;的页面。由于我们会将页面映射到VGA文本缓冲区的帧上，那么也应该也能够通过该页面写在屏幕上。实现如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    use blog_os::memory;
    use x86_64::{structures::paging::Page, VirtAddr}; // new import

    […] // hello world and blog_os::init

    let phys_mem_offset = VirtAddr::new(boot_info.physical_memory_offset);
    let mut mapper = unsafe { memory::init(phys_mem_offset) };
    let mut frame_allocator = memory::EmptyFrameAllocator;

    // 映射未使用的页面
    let page = Page::containing_address(VirtAddr::new(0));
    memory::create_example_mapping(page, &amp;amp;mut mapper, &amp;amp;mut frame_allocator);

    // 通过新映射像屏幕写入字符串`New!`
    let page_ptr: *mut u64 = page.start_address().as_mut_ptr();
    unsafe { page_ptr.offset(400).write_volatile(0x_f021_f077_f065_f04e)};

    […] // test_main(), &quot;it did not crash&quot; printing, and hlt_loop()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们首先使用&lt;code&gt;mapper&lt;/code&gt;和&lt;code&gt;frame_allocator&lt;/code&gt;的可变引用作为参数，调用&lt;code&gt;create_exmaple_mapping&lt;/code&gt;函数，来为位于虚拟地址&lt;code&gt;0&lt;/code&gt;处的页面创建映射。这会将该页面映射到VGA文本缓冲区的帧上，因此我们应该能够在屏幕上看到对其进行的任何写入。&lt;/p&gt;
&lt;p&gt;然后，我们将页面转换为裸指针，并向偏移量&lt;code&gt;400&lt;/code&gt;处写入一个值。我们不在页面开头进行写入，因为VGA缓冲区的首会直接被下一个&lt;code&gt;println&lt;/code&gt;移出屏幕。写入值&lt;code&gt;0x_f021_f077_f065_f04e&lt;/code&gt;代表字符串&quot;&lt;em&gt;New!&lt;/em&gt;&quot;。在白色背景上。正如我们在&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/#volatile&quot;&gt;*“VGA文本模式”*一文&lt;/a&gt;中所了解的那样，对VGA缓冲区的写操作应该是易失性的，因此我们使用&lt;a href=&quot;https://doc.rust-lang.org/std/primitive.pointer.html#method.write_volatile&quot;&gt;&lt;code&gt;write_volatile&lt;/code&gt;&lt;/a&gt;方法。&lt;/p&gt;
&lt;p&gt;在QEMU中运行将看到以下输出：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-implementation/qemu-new-mapping.png&quot; alt=&quot;QEMU新的映射&quot; /&gt;&lt;/p&gt;
&lt;p&gt;屏幕上的&quot;&lt;em&gt;New!&lt;/em&gt;&quot;是通过页面&lt;code&gt;0&lt;/code&gt;写入的，这意味着我们成功在页表中创建了新的映射。&lt;/p&gt;
&lt;p&gt;仅因为负责虚拟地址&lt;code&gt;0&lt;/code&gt;处页面的1级页表已存在，所以创建该映射才起作用。当我们尝试为尚不存在1级表的页面进行映射时，&lt;code&gt;map_to&lt;/code&gt;函数将失败，因为它试图从&lt;code&gt;EmptyFrameAllocator&lt;/code&gt;分配帧以创建新的页面表。当我们尝试映射页面&lt;code&gt;0xdeadbeaf000&lt;/code&gt;而不是页面&lt;code&gt;0&lt;/code&gt;时，就会看到这种情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    […]
    let page = Page::containing_address(VirtAddr::new(0xdeadbeaf000));
    […]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行会出现带有以下错误信息的panic：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;panicked at &apos;map_to failed: FrameAllocationFailed&apos;, /…/result.rs:999:5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要映射没有1级页表的页面，我们需要创建一个适当的&lt;code&gt;FrameAllocator&lt;/code&gt;。但是，我们如何知道哪些帧未被使用，以及到底有多少物理内存可用呢？&lt;/p&gt;
&lt;h3&gt;分配帧&lt;/h3&gt;
&lt;p&gt;为了创建新的页表，我们需要创建一个适当的帧分配器。为此，我们使用&lt;code&gt;memory_map&lt;/code&gt;，它曾作为&lt;code&gt;BootInfo&lt;/code&gt;结构体的一部分传给bootloader：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bootloader::bootinfo::MemoryMap;

/// 一个从bootloader内存映射中返回可用帧的帧分配器
pub struct BootInfoFrameAllocator {
    memory_map: &amp;amp;&apos;static MemoryMap,
    next: usize,
}

impl BootInfoFrameAllocator {
    /// 从传入的内存映射中创建帧分配器
    /// 
    /// 该函数为非安全，因为调用者必须确保传入的内存映射是有效的。
    /// 主要要求是其中所有标记为`USABLE`的帧实际上都未被使用。
    pub unsafe fn init(memory_map: &amp;amp;&apos;static MemoryMap) -&amp;gt; Self {
        BootInfoFrameAllocator {
            memory_map,
            next: 0,
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该结构体有两个字段：一个对bootloader传递的内存映射的&lt;code&gt;&apos;static&lt;/code&gt;引用，以及一个跟踪分配器应返回的下一帧的编号的字段&lt;code&gt;next&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如我们在&lt;a href=&quot;./#%E5%BC%95%E5%AF%BC%E4%BF%A1%E6%81%AF&quot;&gt;&lt;em&gt;引导信息&lt;/em&gt;&lt;/a&gt;一节中所介绍的那样，内存映射由&lt;code&gt;BIOS/UEFI&lt;/code&gt;固件提供。它只能在引导过程早期被查询，因此bootloader已经为我们调用了相应的函数。存储器映射由&lt;a href=&quot;https://docs.rs/bootloader/0.6.4/bootloader/bootinfo/struct.MemoryRegion.html&quot;&gt;&lt;code&gt;MemoryRegion&lt;/code&gt;&lt;/a&gt;结构体列表组成，结构体包含每个存储区域的起始地址、长度和类型（例如未使用，保留等）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;init&lt;/code&gt;函数使用给定的内存映射初始化&lt;code&gt;BootInfoFrameAllocator&lt;/code&gt;。&lt;code&gt;next&lt;/code&gt;字段初始化为&lt;code&gt;0&lt;/code&gt;，且该值会随着每个帧的分配而增长，以避免两次返回相同的帧。由于我们不知道内存映射的可用帧是否已在其他地方使用，因此&lt;code&gt;init&lt;/code&gt;函数必须标记为&lt;code&gt;unsafe&lt;/code&gt;才能要求调用者提供额外的保证。&lt;/p&gt;
&lt;h4&gt;写一个&lt;code&gt;usable_frames&lt;/code&gt;方法&lt;/h4&gt;
&lt;p&gt;在实现&lt;code&gt;FrameAllocator&lt;/code&gt;trait之前，我们先添加一个辅助方法，以将内存映射转换为能够返回可用帧的迭代器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use bootloader::bootinfo::MemoryRegionType;

impl BootInfoFrameAllocator {
    /// 返回内存映射中可用帧的迭代器
    fn usable_frames(&amp;amp;self) -&amp;gt; impl Iterator&amp;lt;Item = PhysFrame&amp;gt; {
        // 获取内存映射中的可用区域
        let regions = self.memory_map.iter();
        let usable_regions = regions
            .filter(|r| r.region_type == MemoryRegionType::Usable);
        // 将各区域化为其地址范围
        let addr_ranges = usable_regions
            .map(|r| r.range.start_addr()..r.range.end_addr());
        // 将这些帧的起始地址化为迭代器
        let frame_addresses = addr_ranges.flat_map(|r| r.step_by(4096));
        // 使用这些起始地址创建`PhysFrame`类型
        frame_addresses.map(|addr| PhysFrame::containing_address(PhysAddr::new(addr)))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数使用组合多个迭代器的方法，将初始&lt;code&gt;MemoryMap&lt;/code&gt;转换为返回可用物理帧的迭代器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，我们调用&lt;code&gt;iter&lt;/code&gt;方法将内存映射转换为返回&lt;a href=&quot;https://docs.rs/bootloader/0.6.4/bootloader/bootinfo/struct.MemoryRegion.html&quot;&gt;&lt;code&gt;MemoryRegions&lt;/code&gt;&lt;/a&gt;的迭代器。&lt;/li&gt;
&lt;li&gt;然后，我们使用&lt;a href=&quot;https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.filter&quot;&gt;&lt;code&gt;filter&lt;/code&gt;&lt;/a&gt;方法跳过任何保留区域或别的不可用的区域。bootloader会为其创建的所有映射更新内存映射，因此内核使用的帧（代码，数据或栈）或用于存储引导信息的帧就已经被标记为&lt;code&gt;InUse&lt;/code&gt;或类似的标志。因此，我们可以确定&lt;code&gt;Usable&lt;/code&gt;帧不会在其他地方使用。&lt;/li&gt;
&lt;li&gt;之后，我们使用&lt;a href=&quot;https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.map&quot;&gt;&lt;code&gt;map&lt;/code&gt;&lt;/a&gt;组合器和Rust的&lt;a href=&quot;https://doc.rust-lang.org/core/ops/struct.Range.html&quot;&gt;range语法&lt;/a&gt;将内存区域的迭代器转换为地址范围的迭代器。&lt;/li&gt;
&lt;li&gt;接下来，我们使用&lt;a href=&quot;https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.flat_map&quot;&gt;&lt;code&gt;flat_map&lt;/code&gt;&lt;/a&gt;将地址范围转换为帧起始地址的迭代器，在这个过程中同时使用&lt;a href=&quot;https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.step_by&quot;&gt;&lt;code&gt;step_by&lt;/code&gt;&lt;/a&gt;每隔4096字节选择一个地址。这是因为页面大小为4096字节（即4 KiB），每隔4096选择一个地址便得到了每个帧的起始地址。Bootloader页面会对齐所有可用的内存区域，因此我们在这里不需要任何用于对齐或舍入的代码。之所以使用&lt;code&gt;flat_map&lt;/code&gt;而非&lt;code&gt;map&lt;/code&gt;，是因为想要得到&lt;code&gt;Iterator&amp;lt;Item = u64&amp;gt;&lt;/code&gt;而非&lt;code&gt;Iterator&amp;lt;Item = Iterator&amp;lt;Item = u64&amp;gt;&amp;gt;&lt;/code&gt;。（译注：上一步得到的迭代器是&lt;code&gt;Iterator&amp;lt;Item = Range&amp;lt;u64&amp;gt;&amp;gt;&lt;/code&gt;，因此只有使用&lt;code&gt;flat_map&lt;/code&gt;将内外迭代器都打开才能操作其中的&lt;code&gt;u64&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;最后，我们将起始地址转换为&lt;code&gt;PhysFrame&lt;/code&gt;类型，以构造&lt;code&gt;Iterator&amp;lt;Item = PhysFrame&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该函数的返回类型使用&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits&quot;&gt;&lt;code&gt;impl Trait&lt;/code&gt;&lt;/a&gt;特性。这样，我们可以指定返回值某种实现了&lt;code&gt;Iterator&lt;/code&gt;trait特性且其中迭代元素类型为&lt;code&gt;PhysFrame&lt;/code&gt;的类型，而无需指明具体的返回类型。注意，我们&lt;em&gt;无法&lt;/em&gt;指明返回值的具体类型，因为它依赖于一个匿名的闭包。&lt;/p&gt;
&lt;h4&gt;实现&lt;code&gt;FrameAllocator&lt;/code&gt;trait&lt;/h4&gt;
&lt;p&gt;现在我们可以实现&lt;code&gt;FrameAllocator&lt;/code&gt;trait了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsafe impl FrameAllocator&amp;lt;Size4KiB&amp;gt; for BootInfoFrameAllocator {
    fn allocate_frame(&amp;amp;mut self) -&amp;gt; Option&amp;lt;PhysFrame&amp;gt; {
        let frame = self.usable_frames().nth(self.next);
        self.next += 1;
        frame
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先使用&lt;code&gt;usable_frames&lt;/code&gt;方法从内存映射中获取可用帧的迭代器。然后，使用&lt;a href=&quot;https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.nth&quot;&gt;&lt;code&gt;Iterator::nth&lt;/code&gt;&lt;/a&gt;函数获取索引为 &lt;code&gt;self.next&lt;/code&gt;的帧（从而跳过第&lt;code&gt;(self.next-1)&lt;/code&gt;帧）。在返回该帧之前，将&lt;code&gt;self.next&lt;/code&gt;增加一，以便在下一次调用时返回下一个帧。&lt;/p&gt;
&lt;p&gt;这种实现方式并不是十分理想，因为在每次分配帧时该函数都会重新创建&lt;code&gt;usable_frame&lt;/code&gt;分配器。最好直接将迭代器存储为结构体的一个字段，然后，我们将不需要再调用&lt;code&gt;nth&lt;/code&gt;方法，而是只需在每次分配时调用&lt;code&gt;next&lt;/code&gt;。不过，这种方法的问题在于，目前还无法在结构体字段中存储&lt;code&gt;impl Trait&lt;/code&gt;类型。不过在未来某一天，当Rust的&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/2071&quot;&gt;&lt;em&gt;named existential types&lt;/em&gt;&lt;/a&gt;完全实现时，这个方法也许会变得可行。&lt;/p&gt;
&lt;h4&gt;使用&lt;code&gt;BootInfoFrameAllocator&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;现在，我们可以修改&lt;code&gt;kernel_main&lt;/code&gt;函数，以传入&lt;code&gt;BootInfoFrameAllocator&lt;/code&gt;实例代替原来的&lt;code&gt;EmptyFrameAllocator&lt;/code&gt;实例了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn kernel_main(boot_info: &amp;amp;&apos;static BootInfo) -&amp;gt; ! {
    use blog_os::memory::BootInfoFrameAllocator;
    […]
    let mut frame_allocator = unsafe {
        BootInfoFrameAllocator::init(&amp;amp;boot_info.memory_map)
    };
    […]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用引导信息帧分配器后映射成功，我们再次看到了白底黑字的&quot;&lt;em&gt;New!&lt;/em&gt;&quot;出现在屏幕上。在后台，&lt;code&gt;map_to&lt;/code&gt;方法通过以下方式创建了缺少的页表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用传入的&lt;code&gt;frame_allocator&lt;/code&gt;分配未使用的帧。&lt;/li&gt;
&lt;li&gt;将帧初始化为全零以创建一个新的空页表。&lt;/li&gt;
&lt;li&gt;将更高级别的表的条目映射到该帧。&lt;/li&gt;
&lt;li&gt;使用下一级表继续执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;虽然我们的&lt;code&gt;create_example_mapping&lt;/code&gt;函数只是一些示例代码，但至少现在已经能够为任意页面创建新的映射了。这对于在以后的文章中进行内存的分配或多线程的实现时至关重要。&lt;/p&gt;
&lt;p&gt;不过目前，我们应该先删掉&lt;code&gt;create_example_mapping&lt;/code&gt;函数，以避免&lt;a href=&quot;./#%E4%B8%80%E4%B8%AA%E7%A4%BA%E4%BE%8B%E5%87%BD%E6%95%B0create-example-mapping&quot;&gt;上述&lt;/a&gt;的意外调用导致的未定义的行为。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;在这篇文章中，我们学习了访问页表物理帧的各种技术，包括恒等映射、完整物理内存映射、临时映射和递归页表等。接下来我们选择了映射完整的物理内存，因为它简单、可移植且功能强大。&lt;/p&gt;
&lt;p&gt;如果没有页表的访问权限，我们就无法映射内核中的物理内存，因此我们需要bootloader的支持。&lt;code&gt;bootloader&lt;/code&gt;crate支持通过可选的cargo特性创建我们所期望的映射。而我们所需的引导信息将以&lt;code&gt;&amp;amp;BootInfo&lt;/code&gt;参数的形式从入口点函数中传递给内核。&lt;/p&gt;
&lt;p&gt;在代码实现的过程中，我们首先通过手动遍历页表的方式实现了地址转换函数，然后转而使用&lt;code&gt;x86_64&lt;/code&gt;crate的&lt;code&gt;MappedPageTable&lt;/code&gt;类型代替我们手写的代码。此外，我们还学习了如何在页表中创建新的映射，以及如何在bootloader传入的内存映射之上创建必要的&lt;code&gt;FrameAllocator&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;下一篇文章将为我们的内核创建一个堆内存区域，这将使我们能够&lt;a href=&quot;https://doc.rust-lang.org/alloc/boxed/struct.Box.html&quot;&gt;分配内存&lt;/a&gt;并使用各种&lt;a href=&quot;https://doc.rust-lang.org/alloc/collections/index.html&quot;&gt;集合类型&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 3.1 - 内存分页简介</title><link>https://zlotus.github.io/2021/03/21/writing-an-os-in-rust-3.1/</link><guid isPermaLink="true">https://zlotus.github.io/2021/03/21/writing-an-os-in-rust-3.1/</guid><description>使用Rust编写操作系统 - 3.1 - 内存分页简介</description><pubDate>Sun, 21 Mar 2021 21:30:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/&quot;&gt;Introduction to Paging&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;在这篇文章中，我们将介绍分页机制，这是一种非常常见的内存管理方案，我们还将会在操作系统中实现它。本文还将解释为什么需要内存隔离、分段如何工作、什么是虚拟内存以及分页如何解决内存分段问题。此外，还探讨了x86_64架构上的多级页表的布局。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/paging-introduction/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-08&quot;&gt;post-08&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;内存保护&lt;/h2&gt;
&lt;p&gt;操作系统的一项主要任务是将程序彼此隔离。例如，你的网络浏览器不应该干扰你的文本编辑器。为了实现此目标，操作系统利用硬件功能来确保一个进程的内存区域不能被其他进程访问。内存保护有多种的方法，具体取决于硬件和操作系统实现。&lt;/p&gt;
&lt;p&gt;例如，某些ARM Cortex-M处理器（用于嵌入式系统）具有&lt;a href=&quot;https://developer.arm.com/docs/ddi0337/e/memory-protection-unit/about-the-mpu&quot;&gt;&lt;em&gt;内存保护单元&lt;/em&gt;&lt;/a&gt;(MPU)，可让用户定义少量（例如8个）具有不同访问权限（例如，无权访问、只读、读写等）的内存区域。在每次内存访问中，MPU会确保该地址位于具有正确访问权限的区域中，否则将抛出异常。通过在每次进程切换时更改区域和访问权限，操作系统可以确保每个进程仅访问它自己的内存，从而使进程彼此隔离。&lt;/p&gt;
&lt;p&gt;在x86上，硬件支持两种不同的内存保护方法：&lt;a href=&quot;https://en.wikipedia.org/wiki/X86_memory_segmentation&quot;&gt;分段&lt;/a&gt;和&lt;a href=&quot;https://en.wikipedia.org/wiki/Virtual_memory#Paged_virtual_memory&quot;&gt;分页&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;内存分段&lt;/h2&gt;
&lt;p&gt;分段早在1978年就已出现，最初是为了增加可寻址内存的数量。当时的情况是CPU仅使用16位地址，这将可寻址内存量限制在了64KiB。为了能够访问大于64KiB的内存，于是引入了附加的段寄存器，每个段寄存器都包含一个偏移地址。CPU会在每次访问内存时自动添加此偏移量，从而使可访问内存最多增加到1MiB。&lt;/p&gt;
&lt;p&gt;CPU会根据内存访问的类型自动选择不同的段寄存器：对于获取指令，将使用代码段&lt;code&gt;CS&lt;/code&gt;；对于栈操作（压栈/弹栈），将使用栈段&lt;code&gt;SS&lt;/code&gt;。其他指令使用数据段&lt;code&gt;DS&lt;/code&gt;或额外段&lt;code&gt;ES&lt;/code&gt;。之后又添加了两个额外的段寄存器&lt;code&gt;FS&lt;/code&gt;和&lt;code&gt;GS&lt;/code&gt;，可供自由使用。&lt;/p&gt;
&lt;p&gt;在内存分段的首个版本中，段寄存器直接包含偏移量，而且也不进行访问控制。这一点后来随着&lt;a href=&quot;https://en.wikipedia.org/wiki/X86_memory_segmentation#Protected_mode&quot;&gt;保护模式&lt;/a&gt;的引入而改变。当CPU在该模式下运行时，段描述符包含访问本地或全局&lt;a href=&quot;https://en.wikipedia.org/wiki/Global_Descriptor_Table&quot;&gt;描述符表&lt;/a&gt;的索引，该表除偏移地址外还包含段大小和访问权限。通过为每个进程加载单独的全局/本地描述符表，从而将对内存的访问限制在该进程自己的内存区域，操作系统也因此能够将进程彼此隔离。&lt;/p&gt;
&lt;p&gt;通过在实际访问之前修改内存地址，内存分段也不知不觉的引入了一种在今天几乎到处使用的技术：&lt;em&gt;虚拟内存&lt;/em&gt;。&lt;/p&gt;
&lt;h3&gt;虚拟内存&lt;/h3&gt;
&lt;p&gt;虚拟内存的思路是将内存地址从底层物理存储设备中抽象出来。不同于直接访问存储设备，这里会先执行一个转换步骤。对于内存分段，转换步骤是添加活动分段中的偏移地址。假设一个程序在偏移量为&lt;code&gt;0x1111000&lt;/code&gt;的段中访问内存地址&lt;code&gt;0x1234000&lt;/code&gt;：实际访问的地址为&lt;code&gt;0x2345000&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为了区分这两种地址类型，转换前的地址称为虚拟地址，转换后的地址称为物理地址。这两种地址之间的一个重要区别是物理地址是唯一的，相同的物理地址始终指向相同且确定的内存空间。而虚拟地址则取决于转换。两个完全不同的虚拟地址可能指向相同的物理地址。同样，当相同的虚拟地址使用不同的转换时，它们可以指向不同的物理地址。&lt;/p&gt;
&lt;p&gt;举个例子：并行执行同一个程序两次，就可以很好的解释这一行为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/segmentation-same-program-twice.svg&quot; alt=&quot;一个程序同时执行两次的内存分段&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例子中的同一程序运行两次，但具有不同的转换。第一个实例的段偏移量为100，因此其虚拟地址0–150转换为物理地址100–250。而第二个实例的偏移量为300，它将其虚拟地址0–150转换为物理地址300–450。这使两个程序都可以运行相同的代码并使用相同的虚拟地址，而不会互相干扰。&lt;/p&gt;
&lt;p&gt;另一个优点是，即使程序使用完全不同的虚拟地址，现在也可以将它们放置在任意期望的物理内存中。因此，OS可以利用全部的可用内存，而无需重新编译程序。&lt;/p&gt;
&lt;h3&gt;内存碎片&lt;/h3&gt;
&lt;p&gt;内存分段强大的地方就在于虚拟地址和物理地址之间的区别。但是，它也具有碎片化的问题。例如，假设我们要运行上面看到的程序的第三份副本：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/segmentation-fragmentation.svg&quot; alt=&quot;内存分段中的碎片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虽然仍有足够的可用内存空间，但我们也无法在不覆盖前两个实例的情况下，将程序的第三个实例映射到虚拟内存。问题在于，内存分段需要&lt;em&gt;连续的&lt;/em&gt;内存，而不能有效利用小的空闲块。&lt;/p&gt;
&lt;p&gt;解决这种碎片的一种方法是暂停执行，将内存中已使用的部分移到更近的位置，更新转换偏移量，然后恢复执行：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/segmentation-fragmentation-compacted.svg&quot; alt=&quot;内存分段中碎片整理&quot; /&gt;&lt;/p&gt;
&lt;p&gt;现在就有足够的连续空间来启动程序的第三个实例了。&lt;/p&gt;
&lt;p&gt;这种碎片整理过程的缺点是需要复制大量内存，从而降低了性能。还需要定期进行整理，以免造成内存碎片过多。由于程序在随机时间暂停并且可能变得无响应，因此这使得系统行为不可预测。&lt;/p&gt;
&lt;p&gt;这些问题是大多数系统不再使用内存分段的原因之一。实际上，x86上的64位模式甚至都不再支持分段，而是使用&lt;em&gt;分页&lt;/em&gt;，这可以完全避免碎片问题。&lt;/p&gt;
&lt;h2&gt;内存分页&lt;/h2&gt;
&lt;p&gt;这个思路是将虚拟和物理内存空间都分成固定大小的小块。虚拟内存空间的块称为&lt;em&gt;页&lt;/em&gt;，物理地址空间的块称为&lt;em&gt;帧&lt;/em&gt;。每个页可以单独映射到一个帧，这使得我们可以在不连续的物理帧之间划出更大的内存区域。&lt;/p&gt;
&lt;p&gt;如果我们回顾一下碎片化内存空间的例子，而这次使用分页而不是分段，就可以看到这样做的优点：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/paging-fragmentation.svg&quot; alt=&quot;内存分页中的碎片&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图中的页面大小为50字节，这意味着我们的每个内存区域都分为三个页面。每个页面分别映射到一个帧，因此连续的虚拟内存区域可以映射到非连续的物理帧。这使我们可以在不执行任何碎片整理的情况下启动程序的第三个实例。&lt;/p&gt;
&lt;h3&gt;潜在碎片&lt;/h3&gt;
&lt;p&gt;与分段相比，分页使用许多固定大小的小块内存区域，而不是一些可变大小的大块区域。由于每个帧都具有相同的大小，因此不会存在太小而无法使用的帧，也就不会发生碎片了。&lt;/p&gt;
&lt;p&gt;这样做看起来&lt;em&gt;似乎&lt;/em&gt;没有碎片。不过确实仍存在着某些潜在碎片，即所谓的&lt;em&gt;内部碎片&lt;/em&gt;。发生内部碎片是因为并非每个内存区域都正好是页面大小的整数倍。想象一下在上面的示例中，一个大小为101的程序：它仍将需要三个大小为50的页面，也就是比程序所需多占了49个字节。为了区分两种类型的碎片，使用分段时发生的碎片类型称为&lt;em&gt;外部碎片&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;内部碎片是不好，但通常要好于分段时发生的外部碎片。它仍然会浪费内存，不过好在不需要碎片整理，并使碎片量可预测（平均每个内存区域约半页）。&lt;/p&gt;
&lt;h3&gt;页表&lt;/h3&gt;
&lt;p&gt;可以看到这数以百万计的页面中的每一个都可以映射到一个单独的帧，而这套映射信息应当被记录在某处。内存分段为每个活动内存区域配置了一个单独的段选择器寄存器，这显然不能用于分页，因为页面数量远多于寄存器。相反，分页使用称为&lt;em&gt;页表&lt;/em&gt;的表结构来记录映射信息。&lt;/p&gt;
&lt;p&gt;对于我们上面的例子，页表如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/paging-page-tables.svg&quot; alt=&quot;分页页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到每个程序实例都有自己的页表。指向当前活动页表的指针存储在特殊的CPU寄存器中。在x86上，此寄存器称为&lt;code&gt;CR3&lt;/code&gt;。 在运行每个程序实例之前，操作系统的工作是将指向正确页表的指针加载到该寄存器。&lt;/p&gt;
&lt;p&gt;在每次访问存储器时，CPU将会从寄存器读取页表指针，并为被访问页面查找其对应的映射帧。这个过程完全由硬件完成，且对运行中的程序完全透明。为了加快转换过程，许多CPU架构都有一个特殊的缓存，用以记住上一次转换的结果。&lt;/p&gt;
&lt;p&gt;对于一些硬件架构，页表条目还可以存储诸如访问权限一类的标志字段属性。在上面的示例中，具有“r/w”标志说明页面可读可写。&lt;/p&gt;
&lt;h3&gt;多级页表&lt;/h3&gt;
&lt;p&gt;我们刚刚看到的简单页表在较大的地址空间中存在一个问题：浪费内存。例如，假设有一个程序使用四个虚拟页面&lt;code&gt;0&lt;/code&gt;、&lt;code&gt;1_000_000&lt;/code&gt;、&lt;code&gt;1_000_050&lt;/code&gt;和&lt;code&gt;1_000_100&lt;/code&gt;（&lt;code&gt;_&lt;/code&gt;为千位分隔符）：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/single-level-page-table.svg&quot; alt=&quot;单级页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它仅需要4个物理帧，但是页表有超过一百万个条目。我们不能省略空条目，因为如果省略空条目，翻译过程就无法继续保证CPU能够直接跳转到正确的条目了（例如，并不能保证第四页使用第四个条目。译注：翻译过程应保证低计算复杂度，如O(1)，因为几乎所欲操作都会涉及内存页表，所以保证CPU能够直接跳转非常重要。）。&lt;/p&gt;
&lt;p&gt;为了减少浪费的内存，我们可以使用&lt;strong&gt;两级页表&lt;/strong&gt;。思路是我们对不同的地址区域使用不同的页表。另一个称为&lt;em&gt;2级&lt;/em&gt;页表的额外表包含了地址区域和（1级）页表之间的映射。&lt;/p&gt;
&lt;p&gt;最好用一个例子来解释。让我们定义每个1级页面表负责一个大小为&lt;code&gt;10_000&lt;/code&gt;的区域。对于上面的例子，则有下表：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/multilevel-page-table.svg&quot; alt=&quot;多级页表&quot; /&gt;&lt;/p&gt;
&lt;p&gt;第0页属于前&lt;code&gt;10_000&lt;/code&gt;字节区域，因此它将使用2级页表的第一个条目。此条目指向1级页表T1，该页表指出第0页指向第0帧。&lt;/p&gt;
&lt;p&gt;页&lt;code&gt;1_000_000&lt;/code&gt;、&lt;code&gt;1_000_050&lt;/code&gt;和&lt;code&gt;1_000_100&lt;/code&gt;都属于第100个&lt;code&gt;10_000&lt;/code&gt;字节区域，因此它们使用了2级页表的第100个条目。该条目指向另一个1级页表T2，该表将这三个页面分布映射到第&lt;code&gt;100&lt;/code&gt;、&lt;code&gt;150&lt;/code&gt;和&lt;code&gt;200&lt;/code&gt;帧。注意，在1级表中的页面地址不包括区域偏移量，因此页面&lt;code&gt;1_000_050&lt;/code&gt;在1级页表中的条目应为&lt;code&gt;50&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;虽然在2级表中仍有100个空条目，但比以前的百万个空条目要少得多。节约的空间就在于，我们并不需要为从&lt;code&gt;10_000&lt;/code&gt;到&lt;code&gt;1_000_000&lt;/code&gt;之间的未映射内存区域创建1级页表。&lt;/p&gt;
&lt;p&gt;两级页表的原理可以扩展到三级、四级或更多级。然后，页表寄存器指向最高级的表，该表指向下一个较低级的表，再指向下一个较低级的表，依此类推。最后，1级页表指向映射的帧。通常，该原理称为&lt;em&gt;多级页表&lt;/em&gt;或&lt;em&gt;分层页表&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;现在我们知道分页和多级页表是如何工作的，接下来可以看看x86_64架构中的分页是如何实现的（后文假设CPU在64位模式下运行）。&lt;/p&gt;
&lt;h2&gt;x86_64的分页&lt;/h2&gt;
&lt;p&gt;x86_64架构使用4级页面表，每个页面大小为4KiB。每个页表无论层级，均为具有512个条目的固定大小。每个条目的大小为8个字节，因此每个表的大小为512 * 8B = 4KiB，也正好为一页。&lt;/p&gt;
&lt;p&gt;每级的页表索引能够直接用虚拟地址算出：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/x86_64-table-indices-from-address.svg&quot; alt=&quot;x86_64地址中的页表索引&quot; /&gt;&lt;/p&gt;
&lt;p&gt;每个表索引有9位，因为每个表都有2^9=512个条目。最低的12位是4KiB页中的偏移量（2^12字节=4KiB）。第48到64位将被忽略，可以看出x86_64其实并不是真正的64位，因为它仅支持48位地址。&lt;/p&gt;
&lt;p&gt;即使第48至64位将被忽略，也不能将这些位设为任意值。而是将这些位都赋上第位47的值，以保持地址唯一，并允许将来的扩展（如5级页表）。这被称为&lt;em&gt;符号扩展&lt;/em&gt;，因为它与&lt;a href=&quot;https://en.wikipedia.org/wiki/Two&apos;s_complement#Sign_extension&quot;&gt;二进制补码中的符号扩展&lt;/a&gt;非常相似。如果地址未正确进行符号扩展，会引发CPU异常。&lt;/p&gt;
&lt;p&gt;值得注意的是，最近Intel的“Ice Lake” CPU可选地支持&lt;a href=&quot;https://en.wikipedia.org/wiki/Intel_5-level_paging&quot;&gt;5级页表&lt;/a&gt;，并将虚拟地址从48位扩展到57位。在当前阶段，为特定CPU优化我们的内核没有意义，因此本文仅使用标准的4级页表。&lt;/p&gt;
&lt;h3&gt;地址转换示例&lt;/h3&gt;
&lt;p&gt;让我们看一个例子，以详细了解转换过程的原理：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/x86_64-page-table-translation.svg&quot; alt=&quot;x86_64页表转换&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当前活动的4级页表的物理地址，也就是该4级页表的根，存放在CR3寄存器中。然后，每个页表条目都指向下一级表的物理帧。最后，1级页表的条目指向地址映射的帧。请注意，页表中的所有地址都是物理地址，而不是虚拟地址，否则CPU也需要转换这些地址（而这可能导致无限递归）。&lt;/p&gt;
&lt;p&gt;上面的页面表层次结构映射了两个页面（蓝色）。从页表索引中，我们可以推断出这两个页的虚拟地址为&lt;code&gt;0x803FE7F000&lt;/code&gt;和&lt;code&gt;0x803FE00000&lt;/code&gt;。现在，让我们看看当程序尝试读取地址&lt;code&gt;0x803FE7F5CE&lt;/code&gt;时会发生什么。首先，我们将地址转换为二进制，并确定该地址的页表索引和页偏移量：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/x86_64-page-table-translation-addresses.png&quot; alt=&quot;x86_64页表地址转换&quot; /&gt;&lt;/p&gt;
&lt;p&gt;有了这些索引，我们现在可以遍历页表层次，确定该地址映射的物流帧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;我们首先从&lt;code&gt;CR3&lt;/code&gt;寄存器中读取第4级页表地址。&lt;/li&gt;
&lt;li&gt;4级页表索引为1，于是我们查看表中索引为1的条目，该条目告诉我们3级页表存储在地址16KiB中。&lt;/li&gt;
&lt;li&gt;我们从该地址加载3级表，然后查看索引为0的条目，该条目告诉我们2级页表存储在地址24KiB中。&lt;/li&gt;
&lt;li&gt;2级页表的索引为511，于是我们查看该表的最后一个条目，以查找1级表的地址。&lt;/li&gt;
&lt;li&gt;通过1级页表中索引为127的条目，最终找到页面映射到12KiB帧，即十六进制的0x3000。&lt;/li&gt;
&lt;li&gt;最后一步，将页面偏移量添加到帧地址上，获取的物理地址为0x3000 + 0x5ce = 0x35ce。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/x86_64-page-table-translation-steps.svg&quot; alt=&quot;x86_64页表转换步骤&quot; /&gt;&lt;/p&gt;
&lt;p&gt;1级页表中页面的权限为&lt;code&gt;r&lt;/code&gt;，即只读。硬件会强制执行这些权限，如果我们尝试对该页面写入，将会引发异常。较高级别页面中的权限限制了较低级别中的可能权限，因此，如果我们将3级页面条目设置为只读，则即使较低级页面指定了读/写权限，使用该条目的页面也将无法执行写入。&lt;/p&gt;
&lt;p&gt;需要注意，此示例使用了每级表仅有单个实例，但在实际中，通常地址空间中的每级表都有多个实例。最多有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个4级页表，&lt;/li&gt;
&lt;li&gt;512个3级表（因为4级表有512个条目），&lt;/li&gt;
&lt;li&gt;512 * 512个2级表（因为512个3级表中的每个表都有512个条目），并且还有&lt;/li&gt;
&lt;li&gt;512 * 512 * 512个1级表（每个2级表都有512个条目）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;页表格式&lt;/h3&gt;
&lt;p&gt;x86_64架构上的页表基本上是具有512个条目的数组。使用Rust语法就是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[repr(align(4096))]
pub struct PageTable {
    entries: [PageTableEntry; 512],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如&lt;code&gt;repr&lt;/code&gt;属性所示，页表需要进行页对齐，即在4KiB边界上对齐。此要求确保页表恰好能填满一页，同时允许优化以使条目更紧凑。&lt;/p&gt;
&lt;p&gt;每个条目的大小为8个字节（64位），并具有以下格式：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;位&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;present&lt;/td&gt;
&lt;td&gt;本页是否存在于内存中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;writable&lt;/td&gt;
&lt;td&gt;本页是否可写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;user accessible&lt;/td&gt;
&lt;td&gt;如果未设置，则只有内核模式的代码可以访问本页&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;write through caching&lt;/td&gt;
&lt;td&gt;对本页的写入是否能够直接进入内存，而无需经过缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;disable cache&lt;/td&gt;
&lt;td&gt;本页不使用缓存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;accessed&lt;/td&gt;
&lt;td&gt;当本页正在使用中，CPU将自动设置此位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;dirty&lt;/td&gt;
&lt;td&gt;当本页正被写入时，CPU将自动设置此位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;huge page/null&lt;/td&gt;
&lt;td&gt;此位在1级和4级页表中必须置为0，置为1时，在3级页表中会创建1GiB的页面，在2级页表中会创建2MiB的页面&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;global&lt;/td&gt;
&lt;td&gt;地址空间切换时，本页不会被换出缓存 (必须将CR4寄存器的PGE位置为1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9-11&lt;/td&gt;
&lt;td&gt;available&lt;/td&gt;
&lt;td&gt;此位供OS自由使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12-51&lt;/td&gt;
&lt;td&gt;physical address&lt;/td&gt;
&lt;td&gt;页面对齐52位物理地址的帧地址或下一个页表的地址&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;52-62&lt;/td&gt;
&lt;td&gt;available&lt;/td&gt;
&lt;td&gt;此位供OS自由使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;td&gt;no execute&lt;/td&gt;
&lt;td&gt;禁止将本页数据当做代码执行（必须将EFER寄存器中的NXE位置为1）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;我们看到只有第12-51位用于存储物理帧地址，其余位用作标志或由操作系统自由使用。这是可行的，因为我们总是指向一个4096字节对齐的地址，这可能指向一个页面对齐的页表，也可能指向映射帧的开头。这意味着位0-11始终为零，也没必要存储这些位，因为硬件会在使用地址之前将它们设置为零。第52-63位同样如此，因为x86_64架构仅支持52位物理地址（类似于它仅支持48位虚拟地址的方式）。&lt;/p&gt;
&lt;p&gt;让我们仔细看看可用的标志：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;present&lt;/code&gt;标志将被映射页面与未被映射页面区别开来。当主内存已满时，它可用于临时将页面换出到磁盘。当随后访问到该页面时，将会发生称为&lt;em&gt;页面错误&lt;/em&gt;的特殊异常，操作系统将对此做出反应——从磁盘重新加载缺失的页面——然后继续执行程序。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writable&lt;/code&gt;和&lt;code&gt;no execute&lt;/code&gt;标志分别控制页面的内容是可写，还是包含可执行指令。&lt;/li&gt;
&lt;li&gt;当对页面被读取或写入时，CPU会自动设置&lt;code&gt;accessed&lt;/code&gt;或&lt;code&gt;dirty&lt;/code&gt;标志。该信息可以被操作系统利用，例如，在决定换出哪些页面时，或者确定自上次保存到磁盘以来内容是否已被修改。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;write through caching&lt;/code&gt;和&lt;code&gt;disable cache&lt;/code&gt;标志允许控制每个页面的缓存。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;user accessible&lt;/code&gt;标志使页面可用于用户空间代码，否则仅当CPU处于内核模式时才可被访问。该特性使得CPU可以在运行用户空间程序时保持内核映射，该功能用于加快&lt;a href=&quot;https://en.wikipedia.org/wiki/System_call&quot;&gt;系统调用&lt;/a&gt;的速度。但是，&lt;a href=&quot;https://en.wikipedia.org/wiki/Spectre_(security_vulnerability)&quot;&gt;Spectre&lt;/a&gt;漏洞仍然能够允许用户空间程序读取这些页面。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;global&lt;/code&gt;标志向硬件发出信号，表明该页在所有地址空间中都可用，因此不需要在地址空间切换时将其从转换缓存中删除（请参阅下文中有关TLB的部分）。该标志通常与被置为0的&lt;code&gt;user accessible&lt;/code&gt;标志一起使用，用以将内核代码映射到所有地址空间。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;huge page&lt;/code&gt;标志允许通过使2级或3级页表的条目直接指向映射的帧，来创建更大尺寸的页面。设置此位后，对于第2级条目，页面大小将增加512倍，达到2MiB = 512 * 4KiB，对于第3级条目，甚至能够达到1GiB = 512 * 2MiB。使用较大页面的优点是需要更少的地址转换缓存行和更少的页表。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;x86_64&lt;/code&gt;crate提供了&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTable.html&quot;&gt;页表&lt;/a&gt;及其&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/page_table/struct.PageTableEntry.html&quot;&gt;条目&lt;/a&gt;的类型，因此我们不需要自己创建这些结构。&lt;/p&gt;
&lt;h3&gt;转换后备缓冲区&lt;/h3&gt;
&lt;p&gt;4级页表使虚拟地址的转换变得耗时，因为每次转换都需要4次内存访问。为了提高性能，x86_64架构将最后的几个转换缓存在所谓的&lt;em&gt;转换后备缓冲区&lt;/em&gt;（TLB）中。当转换仍被缓存时，这允许跳过转换。&lt;/p&gt;
&lt;p&gt;与其他CPU缓存不同，TLB不是完全透明的，而且在页表内容更改时也不会更新或删除转换。这意味着，每当内核修改页面表时，都必须手动更新TLB。为此，有一条特殊的CPU指令叫做&lt;code&gt;invlpg&lt;/code&gt;（即“无效页面”），该指令从TLB中删除指定页面的转换，以便在下次访问时再次从页表中加载该转换。也可以通过重新加载&lt;code&gt;CR3&lt;/code&gt;寄存器来模拟地址空间切换，从而完全清空TLB。&lt;code&gt;x86_64&lt;/code&gt;crate为&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/tlb/index.html&quot;&gt;&lt;code&gt;tlb&lt;/code&gt;模块&lt;/a&gt;为这两种更新方法都提供了Rust函数。&lt;/p&gt;
&lt;p&gt;需要注意的是，在每次页表被修改时都需要刷新TLB，这一点很重要，否则CPU可能会继续使用旧的转换，这可能会导致难以调试的错误。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;有一个事实还没有提及：我们的内核已经在分页上运行了。在&lt;a href=&quot;https://os.phil-opp.com/minimal-rust-kernel/#creating-a-bootimage&quot;&gt;“最小化Rust内核”&lt;/a&gt;一文中添加的bootloader已经建立了一个4级分页层次结构，将内核的每个页面映射到一个物理帧。bootloader执行此操作是因为分页在x86_64的64位模式下是强制的。&lt;/p&gt;
&lt;p&gt;这意味着我们在内核中使用的每个内存地址都是一个虚拟地址。我们之所以可以访问VGA缓冲区地址&lt;code&gt;0xb8000&lt;/code&gt;，就是因为bootloader对该页面进行了恒等映射，即将虚拟页面&lt;code&gt;0xb8000&lt;/code&gt;映射至物理帧&lt;code&gt;0xb8000&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;分页使我们的内核已经相对安全，因为每次超出范围的内存访问都会导致页面错误异常，而不是写入随机物理内存。bootloader甚至为每个页面设置了正确的访问权限，这意味着只有包含代码的页面才是可执行的，只有包含数据的页面才是可写的。&lt;/p&gt;
&lt;h3&gt;页面错误&lt;/h3&gt;
&lt;p&gt;让我们尝试通过访问内核外的某些内存来诱发页面错误。首先，创建一个页面错误处理程序并将其注册到IDT中，以便我们看到一个页面错误异常，而不是一个通用的&lt;a href=&quot;https://os.phil-opp.com/double-fault-exceptions/&quot;&gt;双重故障&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();

        […]

        idt.page_fault.set_handler_fn(page_fault_handler); // new

        idt
    };
}

use x86_64::structures::idt::PageFaultErrorCode;
use crate::hlt_loop;

extern &quot;x86-interrupt&quot; fn page_fault_handler(
    stack_frame: &amp;amp;mut InterruptStackFrame,
    error_code: PageFaultErrorCode,
) {
    use x86_64::registers::control::Cr2;

    println!(&quot;EXCEPTION: PAGE FAULT&quot;);
    println!(&quot;Accessed Address: {:?}&quot;, Cr2::read());
    println!(&quot;Error Code: {:?}&quot;, error_code);
    println!(&quot;{:#?}&quot;, stack_frame);
    hlt_loop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;CPU会在发生页面错误时自动设置&lt;a href=&quot;https://en.wikipedia.org/wiki/Control_register#CR2&quot;&gt;&lt;code&gt;CR2&lt;/code&gt;&lt;/a&gt;寄存器，其中包含导致页面错误的访问的虚拟地址。我们使用&lt;code&gt;x86_64&lt;/code&gt;crate的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr2.html#method.read&quot;&gt;&lt;code&gt;Cr2::read&lt;/code&gt;&lt;/a&gt;函数读取和打印该虚拟地址。&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html&quot;&gt;&lt;code&gt;PageFaultErrorCode&lt;/code&gt;&lt;/a&gt;类能够提供关于诱发页面错误的内存访问的类型的更多信息，例如，该错误是由读取还是写入操作引起的。因此，我们也应打印该信息。不过，不能在不解决页面错误的情况下继续执行程序，因此我们在最后添加一个&lt;a href=&quot;https://os.phil-opp.com/hardware-interrupts/#the-hlt-instruction&quot;&gt;&lt;code&gt;hlt_loop&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;现在我们可以尝试访问内核外的一些内存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    println!(&quot;Hello World{}&quot;, &quot;!&quot;);

    blog_os::init();

    // new
    let ptr = 0xdeadbeaf as *mut u32;
    unsafe { *ptr = 42; }

    // as before
    #[cfg(test)]
    test_main();

    println!(&quot;It did not crash!&quot;);
    blog_os::hlt_loop();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行内核，可以看到页面错误处理程序被调用：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/qemu-page-fault.png&quot; alt=&quot;QEMU页面错误&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;CR2&lt;/code&gt;寄存器确实包含了&lt;code&gt;0xdeadbeaf&lt;/code&gt;，也就是我们尝试访问的地址。错误代码通过&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE&quot;&gt;&lt;code&gt;CAUSED_BY_WRITE&lt;/code&gt;&lt;/a&gt;告诉我们，尝试执行写操作时发生了错误。通过&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html&quot;&gt;&lt;em&gt;未设置&lt;/em&gt;的位&lt;/a&gt;，我们还能了解更多信息。例如，&lt;code&gt;PROTECTION_VIOLATION&lt;/code&gt;位未设置，意味着由于目标页面不存在而导致了页面错误。&lt;/p&gt;
&lt;p&gt;我们看到当前指令指针是&lt;code&gt;0x2031b2&lt;/code&gt;，因此我们知道该地址指向代码页。bootloader将代码页映射为只读，因此从该地址读取是允许的，但写入会导致页面错误。您可以通过将&lt;code&gt;0xdeadbeaf&lt;/code&gt;指针更改为&lt;code&gt;0x2031b2&lt;/code&gt;来尝试此操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 注意：代码中的地址可能与你实际运行中得到的地址不同，
// 这里应使用你实际运行中产生的页面错误所打印的地址进行替换。
let ptr = 0x2031b2 as *mut u32;

// 从代码页读取
unsafe { let x = *ptr; }
println!(&quot;read worked&quot;);

// 向代码页写入
unsafe { *ptr = 42; }
println!(&quot;write worked&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到读取访问有效，但是写入访问导致页面错误：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/paging-introduction/qemu-page-fault-protection.png&quot; alt=&quot;QEMU页面错误保护&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到了“read worked”，这表明读操作没有引起任何错误。但是，并没有看到“write worked”，就发生页面错误。这次除了设置了&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.CAUSED_BY_WRITE&quot;&gt;&lt;code&gt;CAUSED_BY_WRITE&lt;/code&gt;&lt;/a&gt;标志外，还设置了&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.PageFaultErrorCode.html#associatedconstant.PROTECTION_VIOLATION&quot;&gt;&lt;code&gt;PROTECTION_VIOLATION&lt;/code&gt;&lt;/a&gt;标志，该标志说明被访问的页面存在，但对该页面进行的操作却并不被允许。在上面的例子中，由于代码页被映射为只读，因此对该页进行的写操作不会被允许。&lt;/p&gt;
&lt;h3&gt;访问页表&lt;/h3&gt;
&lt;p&gt;尝试观察一下定义我们内核如何进行映射的页表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    println!(&quot;Hello World{}&quot;, &quot;!&quot;);

    blog_os::init();

    use x86_64::registers::control::Cr3;

    let (level_4_page_table, _) = Cr3::read();
    println!(&quot;Level 4 page table at: {:?}&quot;, level_4_page_table.start_address());

    […] // test_main(), println(…), and hlt_loop()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;x86_64&lt;/code&gt;中的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr3.html#method.read&quot;&gt;&lt;code&gt;Cr3::read&lt;/code&gt;&lt;/a&gt;函数返回&lt;code&gt;CR3&lt;/code&gt;寄存器中的当前活动的4级页表。函数返回一个由&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/paging/frame/struct.PhysFrame.html&quot;&gt;&lt;code&gt;PhysFrame&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/registers/control/struct.Cr3Flags.html&quot;&gt;&lt;code&gt;Cr3Flags&lt;/code&gt;&lt;/a&gt;类型组成的元组。 我们只对这个帧感兴趣，于是忽略了元组的第二个元素。&lt;/p&gt;
&lt;p&gt;运行后可以看到以下输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Level 4 page table at: PhysAddr(0x1000)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以当前活动的第4级页表存储在&lt;em&gt;物理&lt;/em&gt;内存的&lt;code&gt;0x1000&lt;/code&gt;中，如其封装类型&lt;code&gt;PhysAddr&lt;/code&gt;所示。现在的问题是：如何从内核访问该表？&lt;/p&gt;
&lt;p&gt;当分页处于活动状态时，无法直接访问物理内存，不然的话程序将很容易避开内存保护去访问其他程序的内存。因此，访问该表的唯一方法是通过某些虚拟页面，该虚拟页面映射到地址为&lt;code&gt;0x1000&lt;/code&gt;的物理帧。为页表帧创建映射这个问题是个普遍的问题，因为内核需要定期访问页面表，例如在为新线程分配栈时。&lt;/p&gt;
&lt;p&gt;在下一篇文章中将详细说明该问题的解决方案。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;本文介绍了两种内存保护技术：分段和分页。前者使用可变大小的内存区域但存在外部碎片的问题，而后者使用固定大小的页面同时允许对访问权限进行更细粒度的控制。&lt;/p&gt;
&lt;p&gt;分页将页面的映射信息存储在具有一计或多级页表中。x86_64架构使用4级页表，页面大小为4KiB。硬件会自动遍历页表，并将生成的地址转换缓存在转换后备缓冲区（TLB）中。此缓冲区不是透明更新的，需要在页表更改时手动刷新。&lt;/p&gt;
&lt;p&gt;我们了解到现在的内核已经在内存分页上运行了，而且非法的内存访问将会导致页面错误异常。我们试图访问当前活动的页表，但是由于CR3寄存器存储了我们无法直接从内核直接访问到的物理地址，所以我们并不能执行此操作。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;下一篇文章将解释如何在内核中实现对分页的支持，并提供从内核访问物理内存的不同方法，这使得访问内核运行中的页表成为可能。至此，我们能够实现将虚拟地址转换为物理地址并在页表中创建新映射的功能。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 2.3 - 硬件中断</title><link>https://zlotus.github.io/2021/03/18/writing-an-os-in-rust-2.3/</link><guid isPermaLink="true">https://zlotus.github.io/2021/03/18/writing-an-os-in-rust-2.3/</guid><description>使用Rust编写操作系统 - 2.3 - 硬件中断</description><pubDate>Thu, 18 Mar 2021 22:51:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/hardware-interrupts/&quot;&gt;Hardware Interrupts&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;在这篇文章中，我们将设置可编程中断控制器，以便将硬件中断正确的转发到CPU。为了处理这些中断，我们将新条目添加到中断描述符表中，就像我们对异常处理程序所做的一样。我们将学习如何获取定期的定时器中断以及如何从键盘获取输入。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/hardware-interrupts/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-07&quot;&gt;post-07&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;中断为连接在CPU上的硬件设备提供了一种通知CPU的方法。因此，键盘不必告诉内核要定期检查是否有新的字符输入（一种称为&lt;a href=&quot;https://en.wikipedia.org/wiki/Polling_(computer_science)&quot;&gt;轮询&lt;/a&gt;的过程），而是将每次按键事件通知内核。如此，内核仅在发生某些事件时才做出反应，因此效率更高。这样做还可以缩短响应时间，因为内核不需要等待下一次轮询，它将会立即做出反应。&lt;/p&gt;
&lt;p&gt;我们无法将所有硬件设备都直连到CPU，而是采用一个独立的&lt;em&gt;中断控制器&lt;/em&gt;汇总所有外设的中断，再通知CPU：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                                    ____________             _____
               Timer ------------&amp;gt; |            |           |     |
               Keyboard ---------&amp;gt; | Interrupt  |---------&amp;gt; | CPU |
               Other Hardware ---&amp;gt; | Controller |           |_____|
               Etc. -------------&amp;gt; |____________|

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大多数中断控制器是可编程的，这意味着它们支持不同的中断优先级。例如，这可以让计时器中断的优先级比键盘中断更高，以便确保计时的准确性。&lt;/p&gt;
&lt;p&gt;与异常不同，硬件中断的发送是&lt;em&gt;异步&lt;/em&gt;的。这意味着中断与正在执行的代码无关，中断是随时都会发生的。因此，内核中突然出现了一种并发，同时也包含了各种潜在的与并发相关的bug。Rust严格的所有权模型在这里帮上了大忙，因为它禁止了可变全局状态。但是，仍然可能出现死锁，正如我们将在本文后面看到的那样。&lt;/p&gt;
&lt;h2&gt;8259 PIC&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Intel_8259&quot;&gt;英特尔 8259&lt;/a&gt;是1976年推出的可编程中断控制器(PIC)。虽然它早已被较新的&lt;a href=&quot;https://en.wikipedia.org/wiki/Intel_APIC_Architecture&quot;&gt;APIC&lt;/a&gt;取代，但由于向后兼容的原因，如今的系统仍支持其接口。8259 PIC的配置比APIC要容易得多，因此在后续文章中切换到APIC之前，我们仍将使用8529 PIC来引入中断。&lt;/p&gt;
&lt;p&gt;8259有8条中断线和几条用于与CPU通讯的线。当年的典型系统会配备两个8259 PIC实例，一个主控制器和一个通过中短线连接在主控上的从控制器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                     ____________                          ____________
Real Time Clock --&amp;gt; |            |   Timer -------------&amp;gt; |            |
ACPI -------------&amp;gt; |            |   Keyboard-----------&amp;gt; |            |      _____
Available --------&amp;gt; | Secondary  |----------------------&amp;gt; | Primary    |     |     |
Available --------&amp;gt; | Interrupt  |   Serial Port 2 -----&amp;gt; | Interrupt  |---&amp;gt; | CPU |
Mouse ------------&amp;gt; | Controller |   Serial Port 1 -----&amp;gt; | Controller |     |_____|
Co-Processor -----&amp;gt; |            |   Parallel Port 2/3 -&amp;gt; |            |
Primary ATA ------&amp;gt; |            |   Floppy disk -------&amp;gt; |            |
Secondary ATA ----&amp;gt; |____________|   Parallel Port 1----&amp;gt; |____________|

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上图显示了一个典型的中断线分配布局。可以看到15条线中的大多数都有固定的映射，例如 次PIC的4号线分配给了鼠标。&lt;/p&gt;
&lt;p&gt;每个控制器可以通过两个&lt;code&gt;[I/O端口](https://os.phil-opp.com/testing/#i-o-ports)&lt;/code&gt;——一个“命令”端口和一个“数据”端口——进行配置。对于主控制器，这些端口是&lt;code&gt;0x20&lt;/code&gt;（命令）和&lt;code&gt;0x21&lt;/code&gt;（数据）。对于从控制器，它们是&lt;code&gt;0xa0&lt;/code&gt;（命令）和&lt;code&gt;0xa1&lt;/code&gt;（数据）。有关如何配置PIC的更多信息，请参见&lt;a href=&quot;https://wiki.osdev.org/8259_PIC&quot;&gt;&lt;code&gt;osdev.org&lt;/code&gt;上的文章&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;p&gt;PIC的默认配置不可用，因为它会将范围为0到15的中断向量编号发送到CPU。而这些编号已被CPU异常占用，例如，编号8对应双重故障。为了解决这个占用问题，我们需要将PIC中断重新映射到不同的编号。实际范围并不重要，只要它不与例外重叠即可，但是我们通常会选择编号32到47，因为这些是32个异常占用后的第一个段空闲数字。&lt;/p&gt;
&lt;p&gt;我们可以通过向PIC的命令和数据端口写入特殊值来使配置生效。幸运的是，已经有一个名为&lt;code&gt;pic8259_simple&lt;/code&gt;的crate，因此我们不需要自己编写初始化过程。如果你对它的工作方式感兴趣，请查看&lt;a href=&quot;https://docs.rs/crate/pic8259_simple/0.2.0/source/src/lib.rs&quot;&gt;它的源代码&lt;/a&gt;，该crate很小并且文档齐全。&lt;/p&gt;
&lt;p&gt;要将crate添加为依赖，我们需要将以下内容添加到项目中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
pic8259_simple = &quot;0.2.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该crate提供的主要抽象为结构体&lt;code&gt;ChainedPics&lt;/code&gt;。该结构体代表了我们在上面介绍的主/次PIC布局。它的用法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
use pic8259_simple::ChainedPics;
use spin;

pub const PIC_1_OFFSET: u8 = 32;
pub const PIC_2_OFFSET: u8 = PIC_1_OFFSET + 8;

pub static PICS: spin::Mutex&amp;lt;ChainedPics&amp;gt; =
    spin::Mutex::new(unsafe { ChainedPics::new(PIC_1_OFFSET, PIC_2_OFFSET) });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;像上面这样将PIC的偏移量设置为32-47。通过将&lt;code&gt;ChainedPics&lt;/code&gt;结构体放置于&lt;code&gt;Mutex&lt;/code&gt;中，我们就能够（通过&lt;code&gt;lock&lt;/code&gt;方法）获得安全的写访问权限，这是下一步所必需的。&lt;code&gt;ChainedPics::new&lt;/code&gt;函数被标记为非安全的，因为提供错误的偏移量将可能导致未定义的行为。&lt;/p&gt;
&lt;p&gt;现在，我们可以在&lt;code&gt;init&lt;/code&gt;函数中初始化8259 PIC了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn init() {
    gdt::init();
    interrupts::init_idt();
    unsafe { interrupts::PICS.lock().initialize() }; // new
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用&lt;code&gt;[initialize](https://docs.rs/pic8259_simple/0.2.0/pic8259_simple/struct.ChainedPics.html#method.initialize)&lt;/code&gt;函数来执行PIC初始化。与&lt;code&gt;ChainedPics::new&lt;/code&gt;函数一样，该函数也是非安全的，因为如果PIC配置错误，使用它也将可能导致未定义的行为。&lt;/p&gt;
&lt;p&gt;如果一切顺利，执行&lt;code&gt;cargo run&lt;/code&gt;时，我们应该继续看到“It not not crash”消息。&lt;/p&gt;
&lt;h2&gt;启用中断&lt;/h2&gt;
&lt;p&gt;到目前为止，什么都没发生，因为在CPU配置中依然禁用着中断。这意味着CPU根本不侦听中断控制器，也就没有中断可以到达CPU。让我们更改一下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn init() {
    gdt::init();
    interrupts::init_idt();
    unsafe { interrupts::PICS.lock().initialize() };
    x86_64::instructions::interrupts::enable();     // new
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;x86_64&lt;/code&gt;crate的&lt;code&gt;interrupts::enable&lt;/code&gt;将函数执行特殊的&lt;code&gt;sti&lt;/code&gt;指令（即“设置中断”）以启用外部中断。当我们现在尝试&lt;code&gt;cargo run&lt;/code&gt;时，将看到发生双重故障：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/hardware-interrupts/qemu-hardware-timer-double-fault.png&quot; alt=&quot;QEMU硬件计时器诱发的双重故障&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发生此双重故障是因为硬件计时器在默认情况下为启用状态（确切地说是&lt;a href=&quot;https://en.wikipedia.org/wiki/Intel_8253&quot;&gt;Intel 8253&lt;/a&gt;），因此一旦启用中断，我们便开始接收计时器中断。由于尚未为计时器定义处理函数，因此双重故障处理程序将会被调用。&lt;/p&gt;
&lt;h3&gt;处理定时器中断&lt;/h3&gt;
&lt;p&gt;从&lt;a href=&quot;https://os.phil-opp.com/hardware-interrupts/#the-8259-pic&quot;&gt;上图&lt;/a&gt;可以看出，定时器使用主PIC的0号线。这意味着它将作为中断&lt;code&gt;32&lt;/code&gt;（0+偏移量32）到达CPU。我们不对索引32进行硬编码，而是将其存放在&lt;code&gt;InterruptIndex&lt;/code&gt;枚举中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
    Timer = PIC_1_OFFSET,
}

impl InterruptIndex {
    fn as_u8(self) -&amp;gt; u8 {
        self as u8
    }

    fn as_usize(self) -&amp;gt; usize {
        usize::from(self.as_u8())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该枚举是一个&lt;a href=&quot;https://doc.rust-lang.org/reference/items/enumerations.html#custom-discriminant-values-for-fieldless-enumerations&quot;&gt;C风格&lt;/a&gt;枚举，因此我们可以直接为每个变体指定索引。&lt;code&gt;repr(u8)&lt;/code&gt;属性指定每个变体都表 示为&lt;code&gt;u8&lt;/code&gt;。将来我们还会添加更多中断变量。&lt;/p&gt;
&lt;p&gt;现在我们可以为计时器中断添加一个处理函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use crate::print;

lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        idt.breakpoint.set_handler_fn(breakpoint_handler);
        […]
        idt[InterruptIndex::Timer.as_usize()]
            .set_handler_fn(timer_interrupt_handler); // new

        idt
    };
}

extern &quot;x86-interrupt&quot; fn timer_interrupt_handler(
    _stack_frame: &amp;amp;mut InterruptStackFrame)
{
    print!(&quot;.&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;timer_interrupt_handler&lt;/code&gt;的函数签名与之前的异常处理函数相同，因为CPU对异常和外部中断的反应相同（唯一的区别是某些异常会推送错误码）。&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html&quot;&gt;&lt;code&gt;InterruptDescriptorTable&lt;/code&gt;&lt;/a&gt;结构体实现了&lt;a href=&quot;https://doc.rust-lang.org/core/ops/trait.IndexMut.html&quot;&gt;&lt;code&gt;IndexMut&lt;/code&gt;&lt;/a&gt; trait，因此我们可以使用数组索引语法访问各个条目。&lt;/p&gt;
&lt;p&gt;在计时器中断处理程序中，我们在屏幕上打印了一个点。由于定时器中断是周期性发生的，因此我们希望每个定时器定期出现一个点。但是，当我们运行它时，我们看到只打印了一个点：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/hardware-interrupts/qemu-single-dot-printed.png&quot; alt=&quot;QEMU仅打印了一个点&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;当中断结束时&lt;/h3&gt;
&lt;p&gt;原因是PIC希望中断处理程序显示发出“中断结束”(EOI)信号。该信号告诉控制器该中断已被处理，同时系统已经准备好接收下一个中断。因此，PIC认为我们仍在忙于处理第一个计时器中断，并在耐心等待EOI信号，然后才发送下一个中断。&lt;/p&gt;
&lt;p&gt;要发送EOI，我们需要再次使用静态&lt;code&gt;PICS&lt;/code&gt;结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern &quot;x86-interrupt&quot; fn timer_interrupt_handler(
    _stack_frame: &amp;amp;mut InterruptStackFrame)
{
    print!(&quot;.&quot;);

    unsafe {
        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Timer.as_u8());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;notify_end_of_interrupt&lt;/code&gt;会推断出是主PIC还是从PIC发送了中断，然后使用&lt;code&gt;command&lt;/code&gt;和&lt;code&gt;data&lt;/code&gt;端口将EOI信号发送到各控制器。如果是从PIC发送了中断，则需要通知两个PIC，因为从PIC通过输入线连接在主PIC上。&lt;/p&gt;
&lt;p&gt;我们需要小心的使用正确的中断向量编号，否则可能会意外删除重要的未发送中断或导致系统挂起。这也是为什么该函数别标记为了非安全。&lt;/p&gt;
&lt;p&gt;现在，当我们执行&lt;code&gt;cargo run&lt;/code&gt;时，我们会看到点定期出现在屏幕上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/hardware-interrupts/qemu-hardware-timer-dots.gif&quot; alt=&quot;QEMU硬件计时器打点&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;配置计时器&lt;/h3&gt;
&lt;p&gt;我们使用的硬件计时器叫做&lt;em&gt;可编程间隔计时器&lt;/em&gt;，也简称为PIT。顾名思义，我们可以配置两个中断之间的间隔。这里不做详细介绍，因为后文将很快切换到&lt;a href=&quot;https://wiki.osdev.org/APIC_timer&quot;&gt;APIC计时器&lt;/a&gt;，但是OSDev Wiki上有大量有关&lt;a href=&quot;https://wiki.osdev.org/Programmable_Interval_Timer&quot;&gt;配置PIT&lt;/a&gt;的文章。&lt;/p&gt;
&lt;h2&gt;死锁&lt;/h2&gt;
&lt;p&gt;现在，我们的内核中具有了一种并发形式：定时器中断会异步的发生，因此它们可以随时中断我们的&lt;code&gt;_start&lt;/code&gt;函数。幸运的是，Rust的所有权系统可以在编译时就能够防止很多与并发相关的bug。不过，死锁是一个值得注意的例外。如果线程试图获取永远不会释放的锁，则会发生死锁。此时，线程会无限期地挂起。&lt;/p&gt;
&lt;p&gt;我们现在就可以在内核中诱发死锁。记住，我们的&lt;code&gt;println&lt;/code&gt;宏调用&lt;code&gt;vga_buffer::__print&lt;/code&gt;函数，而该函数会用自旋锁&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/#spinlocks&quot;&gt;锁定全局变量&lt;code&gt;WRITER&lt;/code&gt;&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[…]

#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
    use core::fmt::Write;
    WRITER.lock().write_fmt(args).unwrap();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数先锁定&lt;code&gt;WRITER&lt;/code&gt;再调用其&lt;code&gt;write_fmt&lt;/code&gt;，并会在函数末尾隐式将&lt;code&gt;WRITER&lt;/code&gt;解锁。现在想象一下，在WRITER锁定时发生了中断，并且中断处理程序也尝试打印一些内容：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;时序&lt;/th&gt;
&lt;th&gt;&lt;code&gt;_start&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;interrupt_handler&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;调用&lt;code&gt;println!&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;print&lt;/code&gt;锁定&lt;code&gt;WRITER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;中断发生&lt;/strong&gt;，调用中断处理程序&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;调用 &lt;code&gt;println!&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;print&lt;/code&gt;尝试锁定&lt;code&gt;WRITER&lt;/code&gt;（已被锁定）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;print&lt;/code&gt;尝试锁定&lt;code&gt;WRITER&lt;/code&gt;（已被锁定）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;…&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;...&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;永不&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;解锁 &lt;code&gt;WRITER&lt;/code&gt;&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;WRITER&lt;/code&gt;被锁定，因此中断处理程序会等待锁释放。但这永远不会发生，因为&lt;code&gt;_start&lt;/code&gt;函数仅在中断处理程序返回后才继续运行。因此，整个系统挂起。&lt;/p&gt;
&lt;h3&gt;诱发死锁&lt;/h3&gt;
&lt;p&gt;通过在&lt;code&gt;_start&lt;/code&gt;函数末尾的&lt;code&gt;loop&lt;/code&gt;循环中打印一些内容，我们就可以轻松地在内核中引发这种死锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    […]
    loop {
        use blog_os::print;
        print!(&quot;-&quot;);        // new
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/hardware-interrupts/qemu-deadlock.png&quot; alt=&quot;QEMU死锁&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到只有有限的连字符被打印，当第一次定时器中断发生时便停止打印。之后系统挂起，因为计时器中断处理程序在尝试打印点时会死锁。这就是我们在上面的输出中看不到任何点的原因。&lt;/p&gt;
&lt;p&gt;每次运行打印的连字符数量会有所不同，因为计时器中断是异步发生的。正是这种不确定性使得与并发相关的bug难以调试。&lt;/p&gt;
&lt;h3&gt;修复死锁&lt;/h3&gt;
&lt;p&gt;为了避免发生这种死锁，只要&lt;code&gt;Mutex&lt;/code&gt;处于锁定状态，我们就禁用中断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// Prints the given formatted string to the VGA text buffer
/// through the global `WRITER` instance.
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
    use core::fmt::Write;
    use x86_64::instructions::interrupts;   // new

    interrupts::without_interrupts(|| {     // new
        WRITER.lock().write_fmt(args).unwrap();
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/interrupts/fn.without_interrupts.html&quot;&gt;&lt;code&gt;without_interrupts&lt;/code&gt;&lt;/a&gt;函数将获取一个闭包并在无中断的环境中执行该闭包。我们使用它来确保只要互斥锁被锁定，就不会发生中断。现在，当我们运行内核时，我们看到它一直在运行而不会挂起。（我们仍然没有注意到任何点，这是因为打印滚动的速度太快。请尝试减慢打印速度，例如，将&lt;code&gt;for _ in 0..10000 {}&lt;/code&gt;放置在&lt;code&gt;loop&lt;/code&gt;中。）&lt;/p&gt;
&lt;p&gt;我们可以对串行打印功能应用相同的更改，以确保不会发生死锁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
    use core::fmt::Write;
    use x86_64::instructions::interrupts;       // new

    interrupts::without_interrupts(|| {         // new
        SERIAL1
            .lock()
            .write_fmt(args)
            .expect(&quot;Printing to serial failed&quot;);
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意，禁用中断并不应该作为通用的解决方案。因为这样做会增加最坏情况下的中断响应时间，即直到系统被允许对中断做出反应之前的时间。因此，只应该在很短的时间内禁用中断。&lt;/p&gt;
&lt;h2&gt;修复竞争条件&lt;/h2&gt;
&lt;p&gt;现在如果执行&lt;code&gt;cargo test&lt;/code&gt;，可能会看到&lt;code&gt;test_println_output&lt;/code&gt;测试失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test --lib
[…]
Running 4 tests
test_breakpoint_exception...[ok]
test_println... [ok]
test_println_many... [ok]
test_println_output... [failed]

Error: panicked at &apos;assertion failed: `(left == right)`
  left: `&apos;.&apos;`,
 right: `&apos;S&apos;`&apos;, src/vga_buffer.rs:205:9
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原因是测试与我们的计时器处理程序之间存在竞争条件。回忆一下测试看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn test_println_output() {
    let s = &quot;Some test string that fits on a single line&quot;;
    println!(&quot;{}&quot;, s);
    for (i, c) in s.chars().enumerate() {
        let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
        assert_eq!(char::from(screen_char.ascii_character), c);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该测试将一个字符串打印到VGA缓冲区，然后通过手动迭代&lt;code&gt;buffer_chars&lt;/code&gt;数组来检查输出。由于计时器中断处理程序可能在&lt;code&gt;println&lt;/code&gt;之后，读取屏幕字符之前运行（中断处理函数会输出一个&lt;code&gt;.&lt;/code&gt;），因此发生竞争状态。请注意，这不是危险的&lt;em&gt;数据竞争&lt;/em&gt;，Rust在编译时完全避免了这种竞争。有关详细信息，请参见&lt;a href=&quot;https://doc.rust-lang.org/nomicon/races.html&quot;&gt;Rustonomicon&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;要解决此问题，我们需要在测试的整个过程中保持&lt;code&gt;WRITER&lt;/code&gt;处于锁定状态，以使计时器处理程序无法将&lt;code&gt;.&lt;/code&gt;打印到“打印行为”和“读取行为”之间的屏幕上。修复的测试如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn test_println_output() {
    use core::fmt::Write;
    use x86_64::instructions::interrupts;

    let s = &quot;Some test string that fits on a single line&quot;;
    interrupts::without_interrupts(|| {
        let mut writer = WRITER.lock();
        writeln!(writer, &quot;\n{}&quot;, s).expect(&quot;writeln failed&quot;);
        for (i, c) in s.chars().enumerate() {
            let screen_char = writer.buffer.chars[BUFFER_HEIGHT - 2][i].read();
            assert_eq!(char::from(screen_char.ascii_character), c);
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们进行了以下改进：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显式调用&lt;code&gt;lock()&lt;/code&gt;方法，使&lt;code&gt;WRITER&lt;/code&gt;在整个测试过程中保持锁定状态。代替&lt;code&gt;println&lt;/code&gt;，我们使用&lt;a href=&quot;https://doc.rust-lang.org/core/macro.writeln.html&quot;&gt;&lt;code&gt;writeln&lt;/code&gt;&lt;/a&gt;宏，该宏允许打印到已经锁定的写入器。&lt;/li&gt;
&lt;li&gt;为了避免再次出现死锁，我们在测试期间禁用中断。否则，在&lt;code&gt;WRITER&lt;/code&gt;仍处于锁定状态时，测试可能会中断。&lt;/li&gt;
&lt;li&gt;由于计时器中断处理程序仍旧可能在测试之前运行，因此在打印字符串&lt;code&gt;s&lt;/code&gt;之前，我们还要打印一个换行符&lt;code&gt;\n&lt;/code&gt;。这样，即使计时器中断处理程序已经打印出&lt;code&gt;.&lt;/code&gt;，我们仍然可以避免测试失败。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过上述更改，现在可以确定地再次运行&lt;code&gt;cargo test&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这是一个无害的竞争条件，仅可能会导致测试失败。你可以想象，其他竞争条件会由于其不确定性而更加难以调试。幸运的是，Rust帮我们阻止了最严重的竞争条件——数据竞争，该竞争条件会导致各种不确定的行为，包括系统崩溃和静默的内存数据损坏。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;hlt&lt;/code&gt;指令&lt;/h2&gt;
&lt;p&gt;到目前为止，我们在&lt;code&gt;_start&lt;/code&gt;和&lt;code&gt;panic&lt;/code&gt;函数的末尾使用了一个简单的空循环语句。这将使得CPU一直在工作，虽然这是代码预期的效果，但这也是非常低效的，因为即使没有任何工作，CPU仍将继续满负荷运行。运行内核时，您可以在任务管理器中观察到此现象：QEMU进程始终需要近100％的CPU使用率。&lt;/p&gt;
&lt;p&gt;我们真正想做的是停止CPU，直到下一个中断发生。这期间应允许CPU进入睡眠状态，在该状态下CPU消耗的能量要少得多。&lt;a href=&quot;https://en.wikipedia.org/wiki/HLT_(x86_instruction)&quot;&gt;&lt;code&gt;hlt&lt;/code&gt;指令&lt;/a&gt;正是这样做的。让我们使用该指令创建一个节能的无限循环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn hlt_loop() -&amp;gt; ! {
    loop {
        x86_64::instructions::hlt();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;instructions::hlt&lt;/code&gt;函数只是&lt;a href=&quot;https://github.com/rust-osdev/x86_64/blob/5e8e218381c5205f5777cb50da3ecac5d7e3b1ab/src/instructions/mod.rs#L16-L22&quot;&gt;简单封装&lt;/a&gt;了汇编指令。不过这是安全的，因为这个操作并不会损害内存安全。&lt;/p&gt;
&lt;p&gt;现在，我们可以使用此&lt;code&gt;hlt_loop&lt;/code&gt;代替&lt;code&gt;_start&lt;/code&gt;和&lt;code&gt;panic&lt;/code&gt;函数中的无限循环：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    […]

    println!(&quot;It did not crash!&quot;);
    blog_os::hlt_loop();            // new
}


#[cfg(not(test))]
#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    println!(&quot;{}&quot;, info);
    blog_os::hlt_loop();            // new
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把&lt;code&gt;lib.rs&lt;/code&gt;也一更新一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// Entry point for `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    init();
    test_main();
    hlt_loop();         // new
}

pub fn test_panic_handler(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    serial_println!(&quot;[failed]\n&quot;);
    serial_println!(&quot;Error: {}\n&quot;, info);
    exit_qemu(QemuExitCode::Failed);
    hlt_loop();         // new
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在在QEMU中运行内核时，我们发现CPU使用率要低得多。&lt;/p&gt;
&lt;h2&gt;键盘输入&lt;/h2&gt;
&lt;p&gt;现在我们已经能够处理来自外部设备的中断了，也终于可以添加对键盘输入的支持了。这是将是我们与内核的首次交互。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;请注意，此处我们仅描述如何处理&lt;code&gt;PS/2&lt;/code&gt;键盘，而不是USB键盘。但是，主板会将USB键盘模拟为&lt;code&gt;PS/2&lt;/code&gt;设备以支持较旧的软件，因此我们可以安心地忽略USB键盘，直到内核中能够提供对USB的支持。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;与硬件计时器一样，键盘控制器在默认情况下就是启用状态。因此，当您按下一个键时，键盘控制器会向PIC发送一个中断，然后将其转发给CPU。CPU在IDT中查找处理程序功能，但相应的条目为空。于是会发生双重故障。&lt;/p&gt;
&lt;p&gt;因此，让我们为键盘中断添加一个处理函数。这与我们为计时器中断定义处理程序的方式非常相似，只是使用了一个不同的中断编号而已：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum InterruptIndex {
    Timer = PIC_1_OFFSET,
    Keyboard, // new
}

lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        idt.breakpoint.set_handler_fn(breakpoint_handler);
        […]
        // new
        idt[InterruptIndex::Keyboard.as_usize()]
            .set_handler_fn(keyboard_interrupt_handler);

        idt
    };
}

extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(
    _stack_frame: &amp;amp;mut InterruptStackFrame)
{
    print!(&quot;k&quot;);

    unsafe {
        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从前面的图可以看出，键盘使用了主PIC的第1行。这意味着它作为中断33（1+偏移量32）到达CPU。将此索引作为&lt;code&gt;InterruptIndex&lt;/code&gt;枚举的新变量&lt;code&gt;Keyboard&lt;/code&gt;添加。我们并不需要显式指定该值，因为它默认为前一个值加一，也就是33。在中断处理程序中，我们打印一个&lt;code&gt;k&lt;/code&gt;并将中断结束信号发送到中断控制器。&lt;/p&gt;
&lt;p&gt;现在按下键盘时屏幕上会打印一个&lt;code&gt;k&lt;/code&gt;。但是，这仅对我们按的第一个键起作用，此后即使我们继续按键盘也不会在屏幕上打印更多&lt;code&gt;k&lt;/code&gt;了。这是因为键盘控制器在我们读取该键所对应的&lt;em&gt;扫描码&lt;/em&gt;之前不会再发送下一个中断。&lt;/p&gt;
&lt;h3&gt;读取扫描码&lt;/h3&gt;
&lt;p&gt;为了找出按下了&lt;em&gt;哪个&lt;/em&gt;键，我们需要查询键盘控制器。通过读取&lt;code&gt;PS/2&lt;/code&gt;控制器的数据端口（即&lt;code&gt;I/O&lt;/code&gt;端口&lt;code&gt;0x60&lt;/code&gt;）来执行此操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(
    _stack_frame: &amp;amp;mut InterruptStackFrame)
{
    use x86_64::instructions::port::Port;

    let mut port = Port::new(0x60);
    let scancode: u8 = unsafe { port.read() };
    print!(&quot;{}&quot;, scancode);

    unsafe {
        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用&lt;code&gt;x86_64&lt;/code&gt;crate提供的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html&quot;&gt;&lt;code&gt;Port&lt;/code&gt;&lt;/a&gt;类型从键盘的数据端口读取一个字节。该字节称为&lt;a href=&quot;https://en.wikipedia.org/wiki/Scancode&quot;&gt;&lt;em&gt;扫描码&lt;/em&gt;&lt;/a&gt;，是一个代表按下/释放的键所对应的数字。我们还没有用扫描码做任何事情，只是将其打印在屏幕上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/hardware-interrupts/qemu-printing-scancodes.gif&quot; alt=&quot;QEMU打印扫描码&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上图显示了我缓慢键入“123”时的情况。我们看到相邻的键具有相邻的扫描码，并且“按下键”和“释放键”所触发的扫描码并不相同。那么，我们如何将扫描码准确地转换为实际的按键动作呢？&lt;/p&gt;
&lt;h3&gt;翻译扫描码&lt;/h3&gt;
&lt;p&gt;扫描码和按键之间有三种不同的映射标准，即所谓的&lt;em&gt;扫描码集&lt;/em&gt;。这三个码集都可以追溯到早期IBM计算机的键盘：&lt;a href=&quot;https://en.wikipedia.org/wiki/IBM_Personal_Computer_XT&quot;&gt;IBM XT&lt;/a&gt;、&lt;a href=&quot;https://en.wikipedia.org/wiki/IBM_3270_PC&quot;&gt;IBM 3270 PC&lt;/a&gt;和&lt;a href=&quot;https://en.wikipedia.org/wiki/IBM_Personal_Computer/AT&quot;&gt;IBM AT&lt;/a&gt;。值得庆幸的是后来的计算机没有延续这种定义新扫描码集的趋势，而是模拟了现有的扫描码集并进行扩展。如今，大多数键盘都可以配置为模拟这三组中的任意一组。&lt;/p&gt;
&lt;p&gt;默认情况下，&lt;code&gt;PS/2&lt;/code&gt;键盘模拟扫描代码集1(&quot;XT&quot;)。在此集中，扫描码字节的低7位定义键，而最高位定义是按下(&quot;0&quot;)还是释放(&quot;1&quot;)。那些&lt;a href=&quot;https://en.wikipedia.org/wiki/IBM_Personal_Computer_XT&quot;&gt;IBM XT&lt;/a&gt;键盘上不存在的键，如Enter键，会连续生成两个扫描代码：一个&lt;code&gt;0xe0&lt;/code&gt;转义字节，后接一个代表触发键的字节。有关集合1中所有扫描码及其对应键的表，参见&lt;a href=&quot;https://wiki.osdev.org/Keyboard#Scan_Code_Set_1&quot;&gt;OSDev Wiki&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;要将扫描码转换为键，我们可以使用match语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(
    _stack_frame: &amp;amp;mut InterruptStackFrame)
{
    use x86_64::instructions::port::Port;

    let mut port = Port::new(0x60);
    let scancode: u8 = unsafe { port.read() };

    // new
    let key = match scancode {
        0x02 =&amp;gt; Some(&apos;1&apos;),
        0x03 =&amp;gt; Some(&apos;2&apos;),
        0x04 =&amp;gt; Some(&apos;3&apos;),
        0x05 =&amp;gt; Some(&apos;4&apos;),
        0x06 =&amp;gt; Some(&apos;5&apos;),
        0x07 =&amp;gt; Some(&apos;6&apos;),
        0x08 =&amp;gt; Some(&apos;7&apos;),
        0x09 =&amp;gt; Some(&apos;8&apos;),
        0x0a =&amp;gt; Some(&apos;9&apos;),
        0x0b =&amp;gt; Some(&apos;0&apos;),
        _ =&amp;gt; None,
    };
    if let Some(key) = key {
        print!(&quot;{}&quot;, key);
    }

    unsafe {
        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码将翻译数字键0-9，并忽略其他键。它使用&lt;code&gt;match&lt;/code&gt;语句为每个扫描码分配一个字符或一个&lt;code&gt;None&lt;/code&gt;。然后使用&lt;a href=&quot;https://doc.rust-lang.org/book/ch18-01-all-the-places-for-patterns.html#conditional-if-let-expressions&quot;&gt;&lt;code&gt;if let&lt;/code&gt;&lt;/a&gt;语句解构变量&lt;code&gt;key&lt;/code&gt;中的字符。通过在模式中使用相同变量名&lt;code&gt;key&lt;/code&gt;，我们可以&lt;a href=&quot;https://doc.rust-lang.org/book/ch03-01-variables-and-mutability.html#shadowing&quot;&gt;遮蔽&lt;/a&gt;先前的声明，这是Rust中解构&lt;code&gt;Option&lt;/code&gt;类型的常见写法。&lt;/p&gt;
&lt;p&gt;现在我们可以打印数字了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/hardware-interrupts/qemu-printing-numbers.gif&quot; alt=&quot;QEMU打印数字&quot; /&gt;&lt;/p&gt;
&lt;p&gt;翻译其他键的方法相同。 幸运的是，有一个名为&lt;a href=&quot;https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/&quot;&gt;&lt;code&gt;pc-keyboard&lt;/code&gt;&lt;/a&gt;的crate可用于翻译集1和集2的扫描码，因此我们不必自己实现此功能。要使用crate，请将其添加到&lt;code&gt;Cargo.toml&lt;/code&gt;中，然后将其导入&lt;code&gt;lib.rs&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
pc-keyboard = &quot;0.5.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们可以使用此crate重写我们的&lt;code&gt;keyboard_interrupt_handler&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern &quot;x86-interrupt&quot; fn keyboard_interrupt_handler(
    _stack_frame: &amp;amp;mut InterruptStackFrame)
{
    use pc_keyboard::{layouts, DecodedKey, HandleControl, Keyboard, ScancodeSet1};
    use spin::Mutex;
    use x86_64::instructions::port::Port;

    lazy_static! {
        static ref KEYBOARD: Mutex&amp;lt;Keyboard&amp;lt;layouts::Us104Key, ScancodeSet1&amp;gt;&amp;gt; =
            Mutex::new(Keyboard::new(layouts::Us104Key, ScancodeSet1,
                HandleControl::Ignore)
            );
    }

    let mut keyboard = KEYBOARD.lock();
    let mut port = Port::new(0x60);

    let scancode: u8 = unsafe { port.read() };
    if let Ok(Some(key_event)) = keyboard.add_byte(scancode) {
        if let Some(key) = keyboard.process_keyevent(key_event) {
            match key {
                DecodedKey::Unicode(character) =&amp;gt; print!(&quot;{}&quot;, character),
                DecodedKey::RawKey(key) =&amp;gt; print!(&quot;{:?}&quot;, key),
            }
        }
    }

    unsafe {
        PICS.lock()
            .notify_end_of_interrupt(InterruptIndex::Keyboard.as_u8());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过&lt;code&gt;lazy_static&lt;/code&gt;宏创建一个由&lt;code&gt;Mutex&lt;/code&gt;保护的静态&lt;a href=&quot;https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html&quot;&gt;&lt;code&gt;Keyboard&lt;/code&gt;&lt;/a&gt;对象。使用美式键盘布局和扫描码集1初始化&lt;code&gt;Keyboard&lt;/code&gt;。&lt;a href=&quot;https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/enum.HandleControl.html&quot;&gt;&lt;code&gt;HandleControl&lt;/code&gt;&lt;/a&gt;参数允许将&lt;code&gt;ctrl+[a-z]&lt;/code&gt;映射到&lt;code&gt;U+0001&lt;/code&gt;至&lt;code&gt;U+001A&lt;/code&gt;的Unicode字符上。我们并不想这样做，因此使用&lt;code&gt;Ignore&lt;/code&gt;选项来像处理普通键一样处理ctrl。&lt;/p&gt;
&lt;p&gt;对于每个中断，我们锁定Mutex，从键盘控制器读取扫描码，并将其传递给&lt;a href=&quot;https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html#method.add_byte&quot;&gt;&lt;code&gt;add_byte&lt;/code&gt;&lt;/a&gt;方法，该方法将扫描代码转换为&lt;code&gt;Option&amp;lt;KeyEvent&amp;gt;&lt;/code&gt;。&lt;a href=&quot;https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.KeyEvent.html&quot;&gt;&lt;code&gt;KeyEvent&lt;/code&gt;&lt;/a&gt;包含该键触发的事件，以及究竟是按下事件还是释放事件。&lt;/p&gt;
&lt;p&gt;为了翻译此按键事件，我们将其传递给&lt;a href=&quot;https://docs.rs/pc-keyboard/0.5.0/pc_keyboard/struct.Keyboard.html#method.process_keyevent&quot;&gt;&lt;code&gt;process_keyevent&lt;/code&gt;&lt;/a&gt;方法，如果可能的话，该方法会将按键事件转换为字符。例如，根据是否按下了Shift键，将&lt;code&gt;A&lt;/code&gt;键的按下事件转换为小写字符&lt;code&gt;a&lt;/code&gt;或大写&lt;code&gt;A&lt;/code&gt;字符。&lt;/p&gt;
&lt;p&gt;使用修改后的中断处理程序，我们已经能够输入文本了：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/hardware-interrupts/qemu-typing.gif&quot; alt=&quot;在QEMU中打字&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;配置键盘&lt;/h3&gt;
&lt;p&gt;我们可以配置&lt;code&gt;PS/2&lt;/code&gt;键盘的某些功能，例如应使用哪个扫描码集。我们不会在这里介绍它，因为这篇文章已经足够长了，但是OSDev Wiki概述了可能的&lt;a href=&quot;https://wiki.osdev.org/PS/2_Keyboard#Commands&quot;&gt;配置命令&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;本文解释了如何启用和处理外部设备中断。我们了解了8259 PIC及其主/从布局、中断号的重新映射以及发送“中断结束”信号。我们为硬件计时器和键盘中断实现了处理程序，并了解了&lt;code&gt;hlt&lt;/code&gt;指令，该指令能够将CPU暂停，直到下一个中断。&lt;/p&gt;
&lt;p&gt;现在，我们可以与内核进行交互，并且初步编写出了一些基础模块，可用于创建小型shell或简单游戏。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;计时器中断对于操作系统来说至关重要，因为它们提供了一种定期中断运行中的进程并使得内核重新获得控制权的方法。之后，内核就可以切换到另一个进程，并让人们产生多个进程并行执行的错觉。&lt;/p&gt;
&lt;p&gt;但是在创建进程或线程之前，我们需要一种为它们分配内存的方法。下一篇文章将探讨内存管理以提供此类基础模块。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 2.2 - 双重故障</title><link>https://zlotus.github.io/2021/03/15/writing-an-os-in-rust-2.2/</link><guid isPermaLink="true">https://zlotus.github.io/2021/03/15/writing-an-os-in-rust-2.2/</guid><description>使用Rust编写操作系统 - 2.2 - CPU异常</description><pubDate>Mon, 15 Mar 2021 18:18:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/double-fault-exceptions/&quot;&gt;Double Faults&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;本文将详细探讨双重故障异常，这种异常是在CPU无法调用异常处理程序时发生的。通过处理此异常，我们能够避免导致系统重置的致命&lt;em&gt;三重故障&lt;/em&gt;。为了能够在任何情况下防止三重故障，我们还将建立一个&lt;em&gt;中断栈表&lt;/em&gt;，以便在单独的内核栈上捕获双重故障。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/double-fault-exceptions/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-06&quot;&gt;post-06&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;何为双重故障？&lt;/h2&gt;
&lt;p&gt;总的来说，双重故障是一种特殊异常，当CPU无法调用异常处理程序时才会诱发这种异常，例如当发生页面错误却并未在中&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/#the-interrupt-descriptor-table&quot;&gt;断描述符表&lt;/a&gt;（IDT）中注册页面错误处理程序时。这类似于编程语言中用于捕获所有异常的代码块，比如像C++中的&lt;code&gt;catch(...)&lt;/code&gt;或是像Java及C#中的&lt;code&gt;catch(Exception e)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;双重故障的行为与普通异常类似。它的向量索引为&lt;code&gt;8&lt;/code&gt;，我们可以在IDT中为其定义一个普通的处理函数。提供双重故障处理程序非常重要，因为如果未处理双重故障，则会发生致命的三重故障。三重故障无法被捕获，而大多数硬件对三重故障做出的反应就是系统复位。&lt;/p&gt;
&lt;h3&gt;触发双重故障&lt;/h3&gt;
&lt;p&gt;让我们通过触发一个未注册处理函数的异常来引发双重故障：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    println!(&quot;Hello World{}&quot;, &quot;!&quot;);

    blog_os::init();

    // 触发页面错误
    unsafe {
        *(0xdeadbeef as *mut u64) = 42;
    };

    // as before
    #[cfg(test)]
    test_main();

    println!(&quot;It did not crash!&quot;);
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用&lt;code&gt;unsafe&lt;/code&gt;块向无效地址&lt;code&gt;0xdeadbeef&lt;/code&gt;写入数据。虚拟地址未映射到页表中的物理地址，于是发生页面错误。我们尚未在&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/#the-interrupt-descriptor-table&quot;&gt;IDT&lt;/a&gt;中注册页面错误处理程序，因此发生了双重故障。&lt;/p&gt;
&lt;p&gt;现在启动内核时，我们看到它陷入无限重启。重启原因如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CPU尝试向&lt;code&gt;0xdeadbeef&lt;/code&gt;写入，这将导致页面错误。&lt;/li&gt;
&lt;li&gt;CPU查找IDT中的相应条目，发现该条目未指定任何处理函数。因此，它不能调用页面错误处理程序，并诱发双重故障。&lt;/li&gt;
&lt;li&gt;CPU查看双重故障处理程序的IDT条目，但该条目同样未指定处理函数。于是，诱发&lt;em&gt;三重故障&lt;/em&gt;。&lt;/li&gt;
&lt;li&gt;三重故障是致命的。QEMU像大多数真实硬件一样对此做出反应——命令系统重置。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;为了防止出现三重故障，我们需要为页面错误提供处理函数，或者为双重故障提供处理函数。我们希望在任何情况下都能够避免三重故障，因此，我们从所有未注册异常都将调用的双重故障入手解决此类问题。&lt;/p&gt;
&lt;h2&gt;双重故障处理程序&lt;/h2&gt;
&lt;p&gt;双重故障是一个带有错误码的普通异常，因此我们指定的函数类似于断点处理函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        idt.breakpoint.set_handler_fn(breakpoint_handler);
        idt.double_fault.set_handler_fn(double_fault_handler); // new
        idt
    };
}

// new
extern &quot;x86-interrupt&quot; fn double_fault_handler(
    stack_frame: &amp;amp;mut InterruptStackFrame, _error_code: u64) -&amp;gt; !
{
    panic!(&quot;EXCEPTION: DOUBLE FAULT\n{:#?}&quot;, stack_frame);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理程序将输出一条简短的错误消息，并转储异常栈帧。双重故障处理程序的错误码始终为零，因此没有必要打印它。与断点处理程序的不同之处在于，双重故障处理程序是&lt;a href=&quot;https://doc.rust-lang.org/stable/rust-by-example/fn/diverging.html&quot;&gt;发散函数&lt;/a&gt;，这是因为&lt;code&gt;x86_64&lt;/code&gt;架构禁止从双重故障异常中返回。&lt;/p&gt;
&lt;p&gt;现在启动内核时，应该看到调用了双重故障处理程序：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/double-fault-exceptions/qemu-catch-double-fault.png&quot; alt=&quot;双重故障异常&quot; /&gt;&lt;/p&gt;
&lt;p&gt;生效了！这次的执行过程如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;CPU尝试写入&lt;code&gt;0xdeadbeef&lt;/code&gt;，这将导致页面错误。&lt;/li&gt;
&lt;li&gt;像以前一样，CPU查找IDT中的相应条目，发现未定义任何处理函数，于是诱发双重故障。&lt;/li&gt;
&lt;li&gt;CPU跳至我们新注册的双重故障处理程序。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;由于CPU现在可以调用双重故障处理程序，因此不再诱发三次故障（无限重启）。&lt;/p&gt;
&lt;p&gt;如此简单！那么，为什么我们需要为这一主题撰写一整篇文章呢？好了，我们现在可以捕获&lt;em&gt;大多数&lt;/em&gt;双重故障，但是在某些情况下，我们目前的方案仍不够用。&lt;/p&gt;
&lt;h2&gt;双重故障诱因&lt;/h2&gt;
&lt;p&gt;在查看特殊情况之前，我们需要知道双重故障的确切诱因。在上一节中我们使用了一个非常模糊的定义：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;双重故障是一种特殊异常，当CPU无法调用异常处理程序时才会诱发这种异常。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;“无法调用”的确切含义是什么？该处理程序不存在吗？处理程序&lt;a href=&quot;http://pages.cs.wisc.edu/~remzi/OSTEP/vm-beyondphys.pdf&quot;&gt;被换出&lt;/a&gt;了吗？如果处理程序本身又导致异常时会发生什么呢？&lt;/p&gt;
&lt;p&gt;例如，考虑以下情况发生时：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;发生断点异常，但是相应的处理函数被换出时？&lt;/li&gt;
&lt;li&gt;发生页面错误，但是页面错误处理程序被换出时？&lt;/li&gt;
&lt;li&gt;除零处理程序会导致断点异常，但是该断点处理程序被换出时？&lt;/li&gt;
&lt;li&gt;我们的内核栈溢出了，同时命中保护页时？&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;幸运的是，AMD64手册(&lt;a href=&quot;https://www.amd.com/system/files/TechDocs/24593.pdf&quot;&gt;PDF&lt;/a&gt;)中描述了准确定义（位于第8.2.9节）。根据该描述，“在执行先前（第一个）异常处理程序期间发生第二个异常时，&lt;em&gt;可能&lt;/em&gt;会诱发双重故障异常”。 这个*“可能”*很重要：只有非常特殊的异常组合才会导致双重故障。这些组合是：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;第一个异常&lt;/th&gt;
&lt;th&gt;第二个异常&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Divide-by-zero_Error&quot;&gt;除0错误&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Invalid_TSS&quot;&gt;无效任务状态段&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Segment_Not_Present&quot;&gt;段不存在&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Stack-Segment_Fault&quot;&gt;栈段错误&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#General_Protection_Fault&quot;&gt;一般性保护错误&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Invalid_TSS&quot;&gt;无效任务状态段&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Segment_Not_Present&quot;&gt;段不存在&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Stack-Segment_Fault&quot;&gt;栈段错误&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#General_Protection_Fault&quot;&gt;一般性保护错误&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Page_Fault&quot;&gt;页面错误&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Page_Fault&quot;&gt;页面错误&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Invalid_TSS&quot;&gt;无效任务状态段&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Segment_Not_Present&quot;&gt;段不存在&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Stack-Segment_Fault&quot;&gt;栈段错误&lt;/a&gt;，&amp;lt;br&amp;gt;&lt;a href=&quot;https://wiki.osdev.org/Exceptions#General_Protection_Fault&quot;&gt;一般性保护错误&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;于是，诸如除零错误后接页面错误就相安无事（继续调用页面错误处理程序），但是除零错误后接一般性保护错误就会导致双重故障。&lt;/p&gt;
&lt;p&gt;借助此表，我们可以回答上述四个问题中的前三个：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果发生断点异常，同时相应的处理函数被换出，则会发生页面错误，并调用页面错误处理程序。&lt;/li&gt;
&lt;li&gt;如果发生页面错误，同时页面错误处理程序被换出，则会发生双重故障，并调用双重故障处理程序。&lt;/li&gt;
&lt;li&gt;如果除零错误处理程序导致断点异常，则CPU会尝试调用断点处理程序。如果断点处理程序被换出，则会发生页面错误并调用页面错误处理程序。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实际上，即使没有在IDT中注册处理函数的异常的情况也遵循此方案：当发生异常时，CPU会尝试读取相应的IDT条目。由于该条目为0，即无效的IDT条目，因此会诱发&lt;em&gt;一般性保护错误&lt;/em&gt;。我们也没有为一般保护错误定义处理函数，因此会诱发另一个一般性保护故障。根据上表，这将导致双重故障。&lt;/p&gt;
&lt;h3&gt;内核栈溢出&lt;/h3&gt;
&lt;p&gt;让我们看第四个问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果我们的内核栈溢出且命中保护页，会发生什么？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;保护页是栈底的特殊内存页，可用来检测栈溢出。该页面未映射到任何物理内存，因此对其进行的访问动作将会导致页面错误，而不是静默的破坏内存其他数据。bootloader为我们的内核栈设置了一个保护页面，因此栈溢出会导致&lt;em&gt;页面错误&lt;/em&gt;。&lt;/p&gt;
&lt;p&gt;当发生页面错误时，CPU在IDT中查找页面错误处理程序，并尝试将&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/#the-interrupt-stack-frame&quot;&gt;中断栈帧&lt;/a&gt;压栈。但是，当前的栈指针仍指向不存在的保护页。于是，发生第二个页面错误，这将导致双重故障（根据上表）。&lt;/p&gt;
&lt;p&gt;现在CPU将尝试调用&lt;em&gt;双重故障处理程序&lt;/em&gt;。但是，在出现双重故障时，CPU也会尝试压入异常栈帧。此时栈指针仍指向保护页，于是发生第三个页错误，这将导致&lt;em&gt;三重故&lt;/em&gt;障并使系统重启。可见，在这种情况下，目前的双重故障处理程序无法避免三重故障。&lt;/p&gt;
&lt;p&gt;让我们自己尝试一下！通过调用无限递归函数就可以轻松诱发内核栈溢出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle] // don&apos;t mangle the name of this function
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    println!(&quot;Hello World{}&quot;, &quot;!&quot;);

    blog_os::init();

    fn stack_overflow() {
        stack_overflow(); // 每次递归都会将返回地址压栈
    }

    // 触发栈溢出
    stack_overflow();

    […] // test_main(), println(…), and loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们在QEMU中运行这段代码时，将看到系统再次陷入无限重启。&lt;/p&gt;
&lt;p&gt;那么应该怎样避免这个问题呢？我们不能忽略异常栈帧压栈，因为这是CPU的硬件行为。因此，我们需要确保在发生双重故障时栈不会溢出。幸运的是，x86_64架构可以解决此问题。&lt;/p&gt;
&lt;h2&gt;切换栈&lt;/h2&gt;
&lt;p&gt;当发生异常时，x86_64架构能够切换到预定义的已知良好的栈上。此切换发生在硬件级别，因此可以在CPU推送异常栈帧之前执行。&lt;/p&gt;
&lt;p&gt;切换机制通过&lt;em&gt;中断栈表&lt;/em&gt;(IST)实现。IST是由7个指向已知良好栈的指针组成的表。以Rust伪代码描述类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct InterruptStackTable {
    stack_pointers: [Option&amp;lt;StackPointer&amp;gt;; 7],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于每个异常处理程序，我们可以通过相应&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/#the-interrupt-descriptor-table&quot;&gt;IDT条目&lt;/a&gt;中的&lt;code&gt;stack_pointers&lt;/code&gt;参数在IST中指定一个栈。例如，我们可以将IST中的第一个栈用于双重故障处理程序。此后，每当发生双重故障时，CPU都会自动切换到该栈。该切换将发生在一切压栈动作之前，因此能够防止三重故障。&lt;/p&gt;
&lt;h3&gt;IST和TSS&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;中断栈表&lt;/em&gt;(IST)是旧时代遗留的结构体&lt;a href=&quot;https://en.wikipedia.org/wiki/Task_state_segment&quot;&gt;&lt;em&gt;任务状态段&lt;/em&gt;&lt;/a&gt;(TSS)中的一部分。在32位模式下TSS用于保存有关任务的各种信息（如处理器寄存器状态），例如用于&lt;a href=&quot;https://wiki.osdev.org/Context_Switching#Hardware_Context_Switching&quot;&gt;硬件上下文切换&lt;/a&gt;。但是，在64位模式下不再支持硬件上下文切换，并且TSS的格式已完全更改。&lt;/p&gt;
&lt;p&gt;在x86_64上，TSS不再用于保存任务相关信息。现在，它包含两个栈表（IST便是其中之一）。32位和64位TSS之间的唯一公共字段指向&lt;a href=&quot;https://en.wikipedia.org/wiki/Task_state_segment#I.2FO_port_permissions&quot;&gt;I/O port permissions bitmap&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;64位TSS具有以下格式：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;（保留位）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u32&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;特权栈表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[u64; 3]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;（保留位）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u64&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中断栈表&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[u64; 7]&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;（保留位）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u64&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;（保留位）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I/O映射基地址&lt;/td&gt;
&lt;td&gt;&lt;code&gt;u16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;当特权级别变更时，CPU使用&lt;em&gt;特权栈表&lt;/em&gt;。例如，如果在CPU处于用户模式（特权级别3）时发生异常，则在调用异常处理程序之前，CPU通常会切换到内核模式（特权级别0）。在这种情况下，CPU将切换到“特权栈表”中的第0个栈（因为0是目标特权级别）。我们目前还没有任何用户模式程序，因此我们暂时忽略此表。&lt;/p&gt;
&lt;h3&gt;新建TSS&lt;/h3&gt;
&lt;p&gt;让我们创建一个新的TSS，并在其中断栈表中包含一个单独的双重故障栈。为此，我们需要一个TSS结构体。幸运的是，&lt;code&gt;x86_64&lt;/code&gt;crate已经包含了&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/tss/struct.TaskStateSegment.html&quot;&gt;&lt;code&gt;TaskStateSegment结构体&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;我们在新的&lt;code&gt;gdt&lt;/code&gt;（稍后会解释这个缩写的意义）模块中创建TSS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod gdt;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::VirtAddr;
use x86_64::structures::tss::TaskStateSegment;
use lazy_static::lazy_static;

pub const DOUBLE_FAULT_IST_INDEX: u16 = 0;

lazy_static! {
    static ref TSS: TaskStateSegment = {
        let mut tss = TaskStateSegment::new();
        tss.interrupt_stack_table[DOUBLE_FAULT_IST_INDEX as usize] = {
            const STACK_SIZE: usize = 4096 * 5;
            static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE];

            let stack_start = VirtAddr::from_ptr(unsafe { &amp;amp;STACK });
            let stack_end = stack_start + STACK_SIZE;
            stack_end
        };
        tss
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里使用&lt;code&gt;lazy_static&lt;/code&gt;是因为Rust的常量求值器还不够强大，无法在编译时进行上面的初始化操作。我们定义第0个IST条目为双重故障栈（换做其他任何IST条目均可）。再将双重故障栈的高位地址写入第0个条目。写入高位地址是因为x86上的栈向下增长，即从高位地址到低位地址（译注：即高位为栈底，低位为栈顶）。&lt;/p&gt;
&lt;p&gt;我们尚未实现内存管理，因此目前并没有一个合适的方法能够用于新栈的分配。作为代替，我们使用&lt;code&gt;static mut&lt;/code&gt;数组作为栈存储空间。这里需要使用&lt;code&gt;unsafe&lt;/code&gt;块，因为在访问可变静态变量时，编译器无法保证数据竞争条件。重要的是它是一个&lt;code&gt;static mut&lt;/code&gt;而不是一个普通&lt;code&gt;static&lt;/code&gt;，否则bootloader会将其映射到只读页面。我们将在以后的文章中将其替换为适当的栈分配方法，使得这里不再需要&lt;code&gt;unsafe&lt;/code&gt;块。&lt;/p&gt;
&lt;p&gt;请注意，此双重故障栈并没用以防止栈溢出的保护页面。这意味着我们不应该在双重故障处理程序中执行密集的栈操作，从而导致栈溢出并破坏栈下方的内存。&lt;/p&gt;
&lt;h4&gt;加载TSS&lt;/h4&gt;
&lt;p&gt;我们创建了一个新的TSS，现在需要告诉CPU它应该使用这个新TSS。不幸的是，这有点麻烦，因为TSS使用分段系统（出于历史原因）。这里我们不应也不能直接加载表，而应向&lt;a href=&quot;https://web.archive.org/web/20190217233448/https://www.flingos.co.uk/docs/reference/Global-Descriptor-Table/&quot;&gt;全局描述符表&lt;/a&gt;(GDT)添加新的段描述符。之后，就可以使用相应的GDT索引调用&lt;a href=&quot;https://www.felixcloutier.com/x86/ltr&quot;&gt;ltr指令&lt;/a&gt;来加载我们的TSS。（这就是为什么我们将模块命名为&lt;code&gt;gdt&lt;/code&gt;。）&lt;/p&gt;
&lt;h3&gt;全局描述符表&lt;/h3&gt;
&lt;p&gt;全局描述符表（GDT）是旧时代遗留下来的，出现在内存分页成为事实上的标准之前，当时用来进行&lt;a href=&quot;https://en.wikipedia.org/wiki/X86_memory_segmentation&quot;&gt;内存分段&lt;/a&gt;。不过它仍然在64位模式下的多种操作中起作用，例如内核模式/用户模式的配置或TSS的加载。&lt;/p&gt;
&lt;p&gt;GDT是包含程序&lt;em&gt;段&lt;/em&gt;的结构体，在内存分页成为标准之前的旧架构中，用于将程序彼此隔离。有关分段的更多信息，请查阅一本名为&lt;a href=&quot;http://pages.cs.wisc.edu/~remzi/OSTEP/&quot;&gt;“Three Easy Pieces”&lt;/a&gt;的免费书籍中的同名章节。虽然在64位模式下不再支持分段，但是GDT仍然存在。现在它主要用于两件事：在内核空间和用户空间之间切换，以及加载TSS结构。&lt;/p&gt;
&lt;h4&gt;创建GDT&lt;/h4&gt;
&lt;p&gt;让我们创建一个静态&lt;code&gt;GDT&lt;/code&gt;，其中包含我们的静态变量&lt;code&gt;TSS&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::structures::gdt::{GlobalDescriptorTable, Descriptor};

lazy_static! {
    static ref GDT: GlobalDescriptorTable = {
        let mut gdt = GlobalDescriptorTable::new();
        gdt.add_entry(Descriptor::kernel_code_segment());
        gdt.add_entry(Descriptor::tss_segment(&amp;amp;TSS));
        gdt
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续通过&lt;code&gt;lazy_static&lt;/code&gt;，用代码段和TSS段创建一个新的GDT。&lt;/p&gt;
&lt;h4&gt;加载GDT&lt;/h4&gt;
&lt;p&gt;创建一个新的&lt;code&gt;gdt::init&lt;/code&gt;函数用于载GDT，我们再从（译注：&lt;code&gt;lib.rs&lt;/code&gt;中的）总&lt;code&gt;init&lt;/code&gt;函数中调用该初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn init() {
    GDT.load();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;pub fn init() {
    gdt::init();
    interrupts::init_idt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在GDT已加载（因为&lt;code&gt;_start&lt;/code&gt;函数调用了总&lt;code&gt;init&lt;/code&gt;），但是我们仍然看到栈溢出时的无限重启。&lt;/p&gt;
&lt;h3&gt;最后一步&lt;/h3&gt;
&lt;p&gt;此时的问题在于新的GDT段尚未激活，因为段和TSS寄存器仍为旧GDT中的值。我们还需要修改双重故障IDT条目，使其能够使用新栈。&lt;/p&gt;
&lt;p&gt;总之，我们需要执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;重载代码段寄存器&lt;/strong&gt;：我​​们更改了GDT，应该重载代码段寄存器&lt;code&gt;cs&lt;/code&gt;。这是必需的，因为旧的段选择器现在可能指向其他GDT描述符（例如TSS描述符）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;加载TSS&lt;/strong&gt;：我们加载了一个包含TSS选择器的GDT，但是我们仍然需要告诉CPU去使用这个新的TSS。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;更新IDT条目&lt;/strong&gt;：一旦加载了TSS，CPU就能够访问有效的中断栈表(IST)了。然后，通过修改双重故障的IDT条目，就可以告诉CPU它应该使用新的双重故障栈了。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于前两个步骤，我们需要访问&lt;code&gt;gdt::init&lt;/code&gt;函数中的&lt;code&gt;code_selector&lt;/code&gt;和&lt;code&gt;tss_selector&lt;/code&gt;变量。要使得这两个变量能够被访问，我们可以通过新建&lt;code&gt;Selectors&lt;/code&gt;结构体使它们成为静态变量的一部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::structures::gdt::SegmentSelector;

lazy_static! {
    static ref GDT: (GlobalDescriptorTable, Selectors) = {
        let mut gdt = GlobalDescriptorTable::new();
        let code_selector = gdt.add_entry(Descriptor::kernel_code_segment());
        let tss_selector = gdt.add_entry(Descriptor::tss_segment(&amp;amp;TSS));
        (gdt, Selectors { code_selector, tss_selector })
    };
}

struct Selectors {
    code_selector: SegmentSelector,
    tss_selector: SegmentSelector,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，可以使用选择器来重载&lt;code&gt;cs&lt;/code&gt;段寄存器并加载我们的TSS：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn init() {
    use x86_64::instructions::segmentation::set_cs;
    use x86_64::instructions::tables::load_tss;

    GDT.0.load();
    unsafe {
        set_cs(GDT.1.code_selector);
        load_tss(GDT.1.tss_selector);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/segmentation/fn.set_cs.html&quot;&gt;&lt;code&gt;set_cs&lt;/code&gt;&lt;/a&gt;重载代码段寄存器，并使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/tables/fn.load_tss.html&quot;&gt;&lt;code&gt;load_tss&lt;/code&gt;&lt;/a&gt;加载TSS。这两个函数被标记为&lt;code&gt;unsafe&lt;/code&gt;，因此需要在&lt;code&gt;unsafe&lt;/code&gt;块中调用——它们可能会因为加载了无效选择器而破坏内存安全。&lt;/p&gt;
&lt;p&gt;我们已经加载了有效的TSS和中断堆栈表，现在，可以在IDT中为双重故障处理程序设置栈索引了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use crate::gdt;

lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        idt.breakpoint.set_handler_fn(breakpoint_handler);
        unsafe {
            idt.double_fault.set_handler_fn(double_fault_handler)
                .set_stack_index(gdt::DOUBLE_FAULT_IST_INDEX); // new
        }

        idt
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;set_stack_index&lt;/code&gt;方法是非安全的，调用者必须确保使用的索引有效，并且未用于其他异常。&lt;/p&gt;
&lt;p&gt;现在，每当发生双重故障时，CPU应该都能切换到双重故障栈。因此，我们能够捕获&lt;em&gt;所有&lt;/em&gt;双重故障，包括内核栈溢出：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/double-fault-exceptions/qemu-double-fault-on-stack-overflow.png&quot; alt=&quot;QEMU栈溢出时的双重故障&quot; /&gt;&lt;/p&gt;
&lt;p&gt;从现在开始，我们再也不会看到三重故障！为确保我们不会意外地破坏以上操作，我们应该为此添加一个测试。&lt;/p&gt;
&lt;h2&gt;栈溢出测试&lt;/h2&gt;
&lt;p&gt;为了测试新写的&lt;code&gt;gdt&lt;/code&gt;模块，并确保在栈溢出时正确调用了双重故障处理程序，我们可以添加一个集成测试。大致思路是在测试函数中引发双重故障，以验证是否调用了双重故障处理程序。&lt;/p&gt;
&lt;p&gt;让我们从一个最小化的测试程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]

use core::panic::PanicInfo;

#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    unimplemented!();
}

#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    blog_os::test_panic_handler(info)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就像我们的&lt;code&gt;panic_handler&lt;/code&gt;测试一样，该测试将在&lt;a href=&quot;https://os.phil-opp.com/testing/#no-harness-tests&quot;&gt;没有测试环境&lt;/a&gt;的条件下运行。这是因为出现双重错误后程序无法继续执行，因此执行多于一个的测试是没有意义的。要禁用测试的测试环境，我们将以下内容添加到我们的Cargo.toml中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[test]]
name = &quot;stack_overflow&quot;
harness = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，&lt;code&gt;cargo test --test stack_overflow&lt;/code&gt;应该可以编译。当然，运行测试会失败，因为&lt;code&gt;unimplemented&lt;/code&gt;宏会引起panic。&lt;/p&gt;
&lt;h3&gt;实现&lt;code&gt;_start&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;_start&lt;/code&gt;函数的实现将会像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use blog_os::serial_print;

#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    serial_print!(&quot;stack_overflow::stack_overflow...\t&quot;);

    blog_os::gdt::init();
    init_test_idt();

    // 出发栈溢出
    stack_overflow();

    panic!(&quot;Execution continued after stack overflow&quot;);
}

#[allow(unconditional_recursion)]
fn stack_overflow() {
    stack_overflow(); // 每次递归都会将返回地址压栈
    volatile::Volatile::new(0).read(); // 防止尾递归优化
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里不调用&lt;code&gt;interrupts::init_idt&lt;/code&gt;函数，而是调用&lt;code&gt;gdt::init&lt;/code&gt;函数来初始化新的GDT，原因是我们要注册一个自定义双重故障处理程序，它将执行&lt;code&gt;exit_qemu(QemuExitCode::Success)&lt;/code&gt;退出，而不是直接panic。我们还将调用&lt;code&gt;init_test_idt&lt;/code&gt;函数，稍后将对其进行说明。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;stack_overflow&lt;/code&gt;函数与&lt;code&gt;main.rs&lt;/code&gt;中的函数几乎相同。唯一的不同是，我们在函数末尾使用&lt;a href=&quot;https://docs.rs/volatile/0.2.6/volatile/struct.Volatile.html&quot;&gt;&lt;code&gt;Volatile&lt;/code&gt;&lt;/a&gt;类型进行了额外的&lt;a href=&quot;https://en.wikipedia.org/wiki/Volatile_(computer_programming)&quot;&gt;易失性&lt;/a&gt;读取，以防止称为&lt;a href=&quot;https://en.wikipedia.org/wiki/Tail_call&quot;&gt;&lt;em&gt;尾调用消除&lt;/em&gt;&lt;/a&gt;的编译器优化。此优化允许编译器将最后一条语句为递归调用的递归函数，从递归调用函数转换为带有循环的普通函数（译注：尾递归优化将递归化为循环）。若有此优化，则递归化为循环后函数将不再会新建额外的栈帧（用于返回地址压栈），于是该函数对于栈的使用将变为常量（译注：循环没有返回地址压栈环节，相较于递归会大幅提升执行效率与资源利用率）。&lt;/p&gt;
&lt;p&gt;但是，在我们的情况下，我们的确希望栈溢出的发生，于是我们在函数的末尾添加了一个假的易失性读操作，以禁止编译器删除该语句。因此，该函数不再是&lt;em&gt;尾递归&lt;/em&gt;，就可以防止递归转换为循环。我们还添加了&lt;code&gt;allow(unconditional_recursion)&lt;/code&gt;属性，以使编译器保持不对这个无限递归的函数发出编译警告。&lt;/p&gt;
&lt;h3&gt;测试IDT&lt;/h3&gt;
&lt;p&gt;如上所述，测试需要使用自己的IDT，并自定义双重故障处理程序。实现看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use lazy_static::lazy_static;
use x86_64::structures::idt::InterruptDescriptorTable;

lazy_static! {
    static ref TEST_IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        unsafe {
            idt.double_fault
                .set_handler_fn(test_double_fault_handler)
                .set_stack_index(blog_os::gdt::DOUBLE_FAULT_IST_INDEX);
        }

        idt
    };
}

pub fn init_test_idt() {
    TEST_IDT.load();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该实现非常类似于我们在&lt;code&gt;interrupts.rs&lt;/code&gt;中的IDT。就像在原来的IDT中，给用于双重故障处理程序的IST设置栈索引，以便触发异常时切换到这个已知良好的栈。最后&lt;code&gt;init_test_idt&lt;/code&gt;函数通过&lt;code&gt;load&lt;/code&gt;方法将IDT加载到CPU上。&lt;/p&gt;
&lt;h3&gt;双重故障处理程序&lt;/h3&gt;
&lt;p&gt;唯一缺少的部分是我们的双重故障处理程序。看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use blog_os::{exit_qemu, QemuExitCode, serial_println};
use x86_64::structures::idt::InterruptStackFrame;

extern &quot;x86-interrupt&quot; fn test_double_fault_handler(
    _stack_frame: &amp;amp;mut InterruptStackFrame,
    _error_code: u64,
) -&amp;gt; ! {
    serial_println!(&quot;[ok]&quot;);
    exit_qemu(QemuExitCode::Success);
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用双重故障处理程序时，我们以成功码退出QEMU，该代码将测试标记为已通过。由于集成测试是完全独立的可执行文件，因此我们仍需要在测试文件的顶部设置&lt;code&gt;#![feature(abi_x86_interrupt)]&lt;/code&gt;属性。&lt;/p&gt;
&lt;p&gt;现在，我们可以通过&lt;code&gt;cargo test --test stack_overflow&lt;/code&gt;来运行该测试（或通过&lt;code&gt;cargo test&lt;/code&gt;运行所有测试）。不出所料，我们在控制台中看到输出&lt;code&gt;stack_overflow... [ok]&lt;/code&gt;。尝试注释掉&lt;code&gt;set_stack_index&lt;/code&gt;一行：它应该导致测试失败。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;在这篇文章中，我们了解了什么是双重故障以及它将会在在什么情况下会发生。我们添加了一个基本的双重故障处理程序，该处理程序可以打印一条错误消息，并为此添加了集成测试。&lt;/p&gt;
&lt;p&gt;我们还启用了硬件支持的双重故障异常上的切换栈功能，这保证了在栈溢出时程序依然能够正常运行。在实现它的过程中，我们了解了任务状态段（TSS）和其中包含的中断堆栈表（IST），以及用于在旧架构上进行内存分段的全局描述符表（GDT）。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;下一篇文章将介绍如何处理来自外部设备（如计时器，键盘或网络控制器）的中断。这些硬件中断与异常非常相似，比如它们同样也通过IDT调度。但是，与异常不同，它们不会直接出现在CPU上，而是会汇总在&lt;em&gt;中断控制器&lt;/em&gt;上，然后根据优先级将它们转发给CPU。在接下来的内容中，我们将探索&lt;a href=&quot;https://en.wikipedia.org/wiki/Intel_8259&quot;&gt;Intel 8259&lt;/a&gt;(&quot;PIC&quot;)中断控制器，并学习如何实现对键盘的支持。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 1.4 - 测试</title><link>https://zlotus.github.io/2021/03/06/writing-an-os-in-rust-1.4/</link><guid isPermaLink="true">https://zlotus.github.io/2021/03/06/writing-an-os-in-rust-1.4/</guid><description>使用Rust编写操作系统 - 1.4 - 测试</description><pubDate>Sat, 06 Mar 2021 21:15:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/testing/&quot;&gt;Testing&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;本篇文章将探讨在&lt;code&gt;no_std&lt;/code&gt;环境中，可执行文件的单元和集成测试。我们将利用Rust对自定义测试框架的支持来在内核中执行测试函数。为了输出QEMU的结果，我们将使用QEMU和&lt;code&gt;bootimage&lt;/code&gt;工具的其他功能。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/testing/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-04&quot;&gt;post-04&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;需要了解的内容&lt;/h2&gt;
&lt;p&gt;本文取代了（现已废弃的）&lt;a href=&quot;https://os.phil-opp.com/unit-testing/&quot;&gt;单元测试&lt;/a&gt;和&lt;a href=&quot;https://os.phil-opp.com/integration-tests/&quot;&gt;集成测试&lt;/a&gt;这两篇文章。本文将假设你在2019-04-27之后阅读量了&lt;a href=&quot;https://os.phil-opp.com/minimal-rust-kernel/&quot;&gt;A Minimal Rust Kernel&lt;/a&gt;一文。目的主要是为了添加&lt;code&gt;.cargo/config.toml&lt;/code&gt;文件，&lt;a href=&quot;https://os.phil-opp.com/minimal-rust-kernel/#set-a-default-target&quot;&gt;设置了默认的编译目标&lt;/a&gt;，并&lt;a href=&quot;https://os.phil-opp.com/minimal-rust-kernel/#using-cargo-run&quot;&gt;设置了&lt;code&gt;cargo run&lt;/code&gt;的&lt;code&gt;runner&lt;/code&gt;参数&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;Rust中的测试&lt;/h3&gt;
&lt;p&gt;Rust有一个&lt;a href=&quot;https://doc.rust-lang.org/book/ch11-00-testing.html&quot;&gt;内置的测试框架&lt;/a&gt;，它能够在不设置任何东西的情况下执行单元测试。只要创建一个通过断言检查一些结果的函数，并在函数前添加&lt;code&gt;#[test]&lt;/code&gt;属性，&lt;code&gt;cargo test&lt;/code&gt;就会自动发现并执行该crate中的所有测试函数。&lt;/p&gt;
&lt;p&gt;不幸的是，对于像我们的内核这样&lt;code&gt;no_std&lt;/code&gt;程序来说，就比较复杂了。问题在于Rust的测试框架隐式地使用了内置的&lt;a href=&quot;https://doc.rust-lang.org/test/index.html&quot;&gt;&lt;code&gt;test&lt;/code&gt;&lt;/a&gt;库，而测试库依赖于标准库。这意味着我们不能为&lt;code&gt;#[no_std]&lt;/code&gt;的内核使用默认的测试框架。&lt;/p&gt;
&lt;p&gt;当我们尝试在项目中运行&lt;code&gt;cargo test&lt;/code&gt;时，可以看到以下报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test
   Compiling blog_os v0.1.0 (/…/blog_os)
error[E0463]: can&apos;t find crate for `test`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;test&lt;/code&gt;crate依赖于标准库，所以它不能用于我们的裸机目标。虽然将&lt;code&gt;test&lt;/code&gt;crate移植到&lt;code&gt;#[no_std]&lt;/code&gt;环境中是&lt;a href=&quot;https://github.com/japaric/utest&quot;&gt;可行的&lt;/a&gt;，但这是非常不稳定的，需要一些黑科技，比如重新定义&lt;code&gt;panic&lt;/code&gt;宏。&lt;/p&gt;
&lt;h3&gt;自定义测试框架&lt;/h3&gt;
&lt;p&gt;幸运的是，Rust支持通过&quot;unstable&quot;的&lt;a href=&quot;https://doc.rust-lang.org/unstable-book/language-features/custom-test-frameworks.html&quot;&gt;&lt;code&gt;custom_test_frameworks&lt;/code&gt;&lt;/a&gt;特性替换默认的测试框架。这个特性下，测试不需要外部库，因此也可以在&lt;code&gt;#[no_std]&lt;/code&gt;环境中工作。它的工作原理是收集所有带有&lt;code&gt;#[test_case]&lt;/code&gt;属性的函数，然后以测试列表为参数调用用户指定的runner函数。因此，它给了实现者对测试过程尽可能大的控制权。&lt;/p&gt;
&lt;p&gt;与默认的测试框架相比，缺少许多默认测试框架具有的高级功能，比如&lt;a href=&quot;https://doc.rust-lang.org/book/ch11-01-writing-tests.html#checking-for-panics-with-should_panic&quot;&gt;&lt;code&gt;should_panic&lt;/code&gt;测试&lt;/a&gt;就是不可用的。因此，如果需要的话，要由实现者自己实现这样的功能。这对我们来说是很理想的，因为我们有一个非常特殊的执行环境，在这种环境下，这种高级特性的默认实现可能本来就无法正常工作。例如，&lt;code&gt;#[should_panic]&lt;/code&gt;属性依赖于栈展开来捕获panic，但我们恰好禁用了栈展开。&lt;/p&gt;
&lt;p&gt;要为我们的内核实现一个自定义测试框架，我们在&lt;code&gt;main.rs&lt;/code&gt;中添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]

#[cfg(test)]
fn test_runner(tests: &amp;amp;[&amp;amp;dyn Fn()]) {
    println!(&quot;Running {} tests&quot;, tests.len());
    for test in tests {
        test();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的runner只是打印一个简短的调试信息，然后调用列表中的每个测试函数。参数类型&lt;code&gt;&amp;amp;[&amp;amp;dyn Fn()]&lt;/code&gt;，是由&lt;a href=&quot;https://doc.rust-lang.org/std/ops/trait.Fn.html&quot;&gt;&lt;code&gt;Fn()&lt;/code&gt;trait&lt;/a&gt;这一&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/first-edition/trait-objects.html&quot;&gt;trait对象&lt;/a&gt;的引用组成的&lt;a href=&quot;https://doc.rust-lang.org/std/primitive.slice.html&quot;&gt;slice&lt;/a&gt;。它基本上是一个可以像函数一样调用的类型的引用，所组成的列表。由于这些函数对于非测试用途的cargo run是无用的，所以我们使用&lt;code&gt;#[cfg(test)]&lt;/code&gt;属性只在测试中运行它。&lt;/p&gt;
&lt;p&gt;现在运行&lt;code&gt;cargo test&lt;/code&gt;时，我们看到运行成功了（如果没有成功，请看下面的注意）。然而，我们仍然看到的是&quot;Hello World&quot;，而不是&lt;code&gt;test_runner&lt;/code&gt;的消息。原因是&lt;code&gt;_start&lt;/code&gt;函数仍然被用作入口点。自定义测试框架功能会生成一个调用&lt;code&gt;test_runner&lt;/code&gt;的&lt;code&gt;main&lt;/code&gt;函数，但这个函数被忽略了，因为我们使用了#[no_main]属性，并提供了我们自己的入口点。&lt;/p&gt;
&lt;p&gt;:::warning[注意]
&lt;code&gt;unstable.build-std&lt;/code&gt;配置项仅在2020-07-15之后的Rust nightly中提供
:::&lt;/p&gt;
&lt;p&gt;目前在cargo中存在一个bug，在某些情况下会导致&lt;code&gt;cargo test&lt;/code&gt;中出现&quot;duplicate lang item&quot;的错误。这是由于在&lt;code&gt;Cargo.toml&lt;/code&gt;中将某个配置文件设置为了&lt;code&gt;panic = &quot;abort&quot;&lt;/code&gt;。尝试删除它，然后&lt;code&gt;cargo test&lt;/code&gt;应该就可以正常工作了。更多信息请参见这个&lt;a href=&quot;https://github.com/rust-lang/cargo/issues/7359&quot;&gt;&lt;code&gt;cargo issue&lt;/code&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们首先需要通过&lt;code&gt;reexport_test_harness_main&lt;/code&gt;属性将生成的函数名称改为与&lt;code&gt;main&lt;/code&gt;不同的名称。然后我们就可以从我们的&lt;code&gt;_start&lt;/code&gt;函数中调用重命名的函数了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![reexport_test_harness_main = &quot;test_main&quot;]

#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    println!(&quot;Hello World{}&quot;, &quot;!&quot;);

    #[cfg(test)]
    test_main();

    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将测试框架入口函数的名称设置为&lt;code&gt;test_main&lt;/code&gt;，并在&lt;code&gt;_start&lt;/code&gt;入口点调用它。我们使用&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html&quot;&gt;条件编译&lt;/a&gt;，只在测试环境中添加对&lt;code&gt;test_main&lt;/code&gt;的调用，因为该函数不会在正常cargo run时生成。&lt;/p&gt;
&lt;p&gt;当我们现在执行&lt;code&gt;cargo test&lt;/code&gt;时，就能在屏幕上看到&lt;code&gt;test_runner&lt;/code&gt;打印的&quot;Running 0 tests&quot;。现在就可以创建第一个测试函数了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn trivial_assertion() {
    print!(&quot;trivial assertion... &quot;);
    assert_eq!(1, 1);
    println!(&quot;[ok]&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在运行&lt;code&gt;cargo test&lt;/code&gt;时，便可以看到以下输出：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/testing/qemu-test-runner-output.png&quot; alt=&quot;QEMU中打印&amp;quot;Hello World!&amp;quot;, &amp;quot;Running 1 tests&amp;quot;, &amp;quot;trivial assertion... [ok]&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;传递给&lt;code&gt;test_runner&lt;/code&gt;函数的&lt;code&gt;test&lt;/code&gt; slice现在包含了对&lt;code&gt;trivial_assertion&lt;/code&gt;函数的引用。由&lt;code&gt;trivial assertion... [ok]&lt;/code&gt;在屏幕上的输出，我们看到测试被调用，并且测试通过了。&lt;/p&gt;
&lt;p&gt;执行完测试后，&lt;code&gt;test_runner&lt;/code&gt;结束并回到到&lt;code&gt;test_main&lt;/code&gt;函数，而这个函数又返回到&lt;code&gt;_start&lt;/code&gt;入口点函数。在&lt;code&gt;_start&lt;/code&gt;结束前，程序进入了一个死循环，因为入口点函数不允许返回。这是个问题，因为我们希望&lt;code&gt;cargo test&lt;/code&gt;在运行完所有测试后能够自动退出。&lt;/p&gt;
&lt;h2&gt;退出QEMU&lt;/h2&gt;
&lt;p&gt;现在，&lt;code&gt;_start&lt;/code&gt;函数最后是一个死循环，因此，需要在每次执行&lt;code&gt;cargo test&lt;/code&gt;时手动关闭QEMU。这很麻烦，因为我们希望在没有用户交互的脚本中运行&lt;code&gt;cargo test&lt;/code&gt;。最直观的解决方案是实现一种适当的方式来关闭操作系统。但是实现这个过程比较复杂，因为它需要实现对&lt;a href=&quot;https://wiki.osdev.org/APM&quot;&gt;APM&lt;/a&gt;或&lt;a href=&quot;https://wiki.osdev.org/ACPI&quot;&gt;ACPI&lt;/a&gt;电源管理标准的支持。&lt;/p&gt;
&lt;p&gt;幸运的是，还有一条捷径。QEMU支持一个特殊的&lt;code&gt;isa-debug-exit&lt;/code&gt;设备, 它提供了一个从访客系统中退出QEMU的简单方法。要启用该设备，则需要给QEMU传递一个&lt;code&gt;-device&lt;/code&gt;参数。这可以通过在&lt;code&gt;Cargo.toml&lt;/code&gt;中添加一个&lt;code&gt;package.metadata.bootimage.test-args&lt;/code&gt;配置键来实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[package.metadata.bootimage]
test-args = [&quot;-device&quot;, &quot;isa-debug-exit,iobase=0xf4,iosize=0x04&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;bootimage runner&lt;/code&gt;将为所有可执行测试附加&lt;code&gt;test-args&lt;/code&gt;参数到默认QEMU命令中。而对于正常的&lt;code&gt;cargo run&lt;/code&gt;，这些参数会被忽略。&lt;/p&gt;
&lt;p&gt;连同设备名(&lt;code&gt;isa-debug-exit&lt;/code&gt;)，我们传递了&lt;code&gt;iobase&lt;/code&gt;和&lt;code&gt;iosize&lt;/code&gt;两个参数，这两个参数指定了设备可以从内核到达的I/O端口。&lt;/p&gt;
&lt;h3&gt;I/O端口&lt;/h3&gt;
&lt;p&gt;对于x86架构，CPU与外设硬件之间的通信有两种不同的方法，即&lt;strong&gt;内存映射I/O&lt;/strong&gt;和&lt;strong&gt;端口映射I/O&lt;/strong&gt;。我们已经使用内存映射I/O通过内存地址&lt;code&gt;0xb8000&lt;/code&gt;来访问&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/&quot;&gt;VGA文本缓冲区&lt;/a&gt;。这个地址不是映射到主存，而是映射到VGA设备上的一些内存。&lt;/p&gt;
&lt;p&gt;与内存映射I/O不同，端口映射的I/O使用单独的I/O总线进行通信。每个连接的外设都有一个或多个端口号。为了与这样的I/O端口进行通信，有一种特殊的CPU指令，叫做&lt;code&gt;in&lt;/code&gt;和&lt;code&gt;out&lt;/code&gt;，这些指令需要指定一个端口号和一个字节数据作为参数（这些指令也有一些变体，允许发送一个&lt;code&gt;u16&lt;/code&gt;或&lt;code&gt;u32&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isa-debug-exit&lt;/code&gt;设备使用端口映射的I/O。&lt;code&gt;iobase&lt;/code&gt;参数指定设备的端口地址（&lt;code&gt;0xf4&lt;/code&gt;是x86的IO总线上的一个&lt;a href=&quot;https://wiki.osdev.org/I/O_Ports#The_list&quot;&gt;一般不使用的端口&lt;/a&gt;），&lt;code&gt;iosize&lt;/code&gt;指定端口大小（&lt;code&gt;0x04&lt;/code&gt;表示4个字节）。&lt;/p&gt;
&lt;h3&gt;使用退出设备&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;isa-debug-exit&lt;/code&gt;设备的功能非常简单。当一个值被写入&lt;code&gt;iobase&lt;/code&gt;所指定的I/O端口时，它会使QEMU以&lt;a href=&quot;https://en.wikipedia.org/wiki/Exit_status&quot;&gt;退出状态&lt;/a&gt;&lt;code&gt;(value &amp;lt;&amp;lt; 1) | 1&lt;/code&gt;退出。因此，当我们向该端口写入&lt;code&gt;0&lt;/code&gt;时，QEMU将以退出状态&lt;code&gt;(0 &amp;lt;&amp;lt; 1) | 1 = 1&lt;/code&gt;退出，而当我们向该端口写入&lt;code&gt;1&lt;/code&gt;时，将以退出状态&lt;code&gt;(1 &amp;lt;&amp;lt; 1) | 1 = 3&lt;/code&gt;退出。&lt;/p&gt;
&lt;p&gt;我们不需要手动调用&lt;code&gt;in&lt;/code&gt;和&lt;code&gt;out&lt;/code&gt;汇编指令，直接使用&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/&quot;&gt;&lt;code&gt;x86_64&lt;/code&gt;&lt;/a&gt; crate提供的抽象。在&lt;code&gt;Cargo.toml&lt;/code&gt;的&lt;code&gt;dependencies&lt;/code&gt;部分添加对该crate的依赖即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
x86_64 = &quot;0.13.2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们可以使用crate提供的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html&quot;&gt;&lt;code&gt;Port&lt;/code&gt;&lt;/a&gt;类型来创建&lt;code&gt;exit_qemu&lt;/code&gt;函数了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
    Success = 0x10,
    Failed = 0x11,
}

pub fn exit_qemu(exit_code: QemuExitCode) {
    use x86_64::instructions::port::Port;

    unsafe {
        let mut port = Port::new(0xf4);
        port.write(exit_code as u32);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数在&lt;code&gt;0xf4&lt;/code&gt;上创建了一个新&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/instructions/port/struct.Port.html&quot;&gt;&lt;code&gt;Port&lt;/code&gt;&lt;/a&gt;对象，作为&lt;code&gt;isa-debug-exit&lt;/code&gt;设备的&lt;code&gt;iobase&lt;/code&gt;。然后将参数退出代码写入该端口。我们使用&lt;code&gt;u32&lt;/code&gt;是因为我们指定了&lt;code&gt;isa-debug-exit&lt;/code&gt;设备的&lt;code&gt;iosize&lt;/code&gt;为&lt;code&gt;4&lt;/code&gt;字节。这两种操作都是非安全的，因为向I/O端口写入数据可能会导致不可预知的结果。&lt;/p&gt;
&lt;p&gt;为了指定退出状态，我们创建了一个&lt;code&gt;QemuExitCode&lt;/code&gt;枚举类型。想要达到的效果是，如果所有的测试都成功了就用成功退出代码退出，否则就用失败退出代码退出。该枚举被标记为&lt;code&gt;#[repr(u32)]&lt;/code&gt;，这会强制编译器用一个&lt;code&gt;u32&lt;/code&gt;整型来表示该枚举变量。我们用退出码&lt;code&gt;0x10&lt;/code&gt;表示成功，&lt;code&gt;0x11&lt;/code&gt;表示失败。实际的退出代码并不太重要，只要不与QEMU的默认退出代码冲突即可。例如，使用退出码&lt;code&gt;0&lt;/code&gt;表示成功并不是一个好主意，因为它在转换后变成了&lt;code&gt;(0 &amp;lt;&amp;lt; 1) | 1 = 1&lt;/code&gt;，这就是QEMU运行失败时的默认退出码，这将导致我们无法区分QEMU错误和测试运行成功。&lt;/p&gt;
&lt;p&gt;现在就可以更新我们的&lt;code&gt;test_runner&lt;/code&gt;了，在所有测试运行后会退出QEMU：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn test_runner(tests: &amp;amp;[&amp;amp;dyn Fn()]) {
    println!(&quot;Running {} tests&quot;, tests.len());
    for test in tests {
        test();
    }
    /// new
    exit_qemu(QemuExitCode::Success);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们现在运行&lt;code&gt;cargo test&lt;/code&gt;时，看到QEMU在执行完测试后立即关闭。问题是，虽然我们传入了&lt;code&gt;Success&lt;/code&gt;退出码，但&lt;code&gt;cargo test&lt;/code&gt;却将测试解释为失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running target/x86_64-blog_os/debug/deps/blog_os-5804fc7d2dd4c9be
Building bootloader
   Compiling bootloader v0.5.3 (/home/philipp/Documents/bootloader)
    Finished release [optimized + debuginfo] target(s) in 1.07s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
    deps/bootimage-blog_os-5804fc7d2dd4c9be.bin -device isa-debug-exit,iobase=0xf4,
    iosize=0x04`
error: test failed, to rerun pass &apos;--bin blog_os&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;问题在于，&lt;code&gt;cargo test&lt;/code&gt;将除&lt;code&gt;0&lt;/code&gt;以外的所有错误代码视为失败。&lt;/p&gt;
&lt;h3&gt;成功退出码&lt;/h3&gt;
&lt;p&gt;为了解决这个问题，&lt;code&gt;bootimage&lt;/code&gt;提供了一个&lt;code&gt;test-success-exit-code&lt;/code&gt;配置键，将指定的退出代码映射到退出代码&lt;code&gt;0&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
[package.metadata.bootimage]
test-args = […]
test-success-exit-code = 33         # (0x10 &amp;lt;&amp;lt; 1) | 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这个配置，&lt;code&gt;bootimage&lt;/code&gt;就会把我们的成功退出码映射到退出码&lt;code&gt;0&lt;/code&gt;上，如此，&lt;code&gt;cargo test&lt;/code&gt;就会正确识别成功测试，也就不会再把成功测试算作失败了。&lt;/p&gt;
&lt;p&gt;我们的测试函数现在会自动关闭QEMU，并能够将正确的测试结果上报。可以看到QEMU窗口瞬间闪过，以至于我们没有足够的时间阅读测试结果。如果能把测试结果打印到控制台就更好了，这样我们在QEMU退出后仍然能看到测试结果。&lt;/p&gt;
&lt;h2&gt;打印到控制台&lt;/h2&gt;
&lt;p&gt;为了在控制台上看到测试输出，我们需要以某种方式将数据从内核发送到主机系统。有很多方法可以实现，例如通过TCP网络接口发送数据。然而，设置网络协议栈是一项相当复杂的任务，所以我们将选择一个更简单的解决方案。&lt;/p&gt;
&lt;h3&gt;串口通信&lt;/h3&gt;
&lt;p&gt;一个简单的发送数据的方法是使用串口，这是一个古老的接口标准，在现代计算机中已经找不到了。串口协议很容易编程控制，QEMU可以将通过串口发送的字节重定向到主机的标准输出或文件中。&lt;/p&gt;
&lt;p&gt;实现串行接口的芯片叫做&lt;a href=&quot;https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter&quot;&gt;UART&lt;/a&gt;。x86上的&lt;a href=&quot;https://en.wikipedia.org/wiki/Universal_asynchronous_receiver-transmitter#UART_models&quot;&gt;UART型号很多&lt;/a&gt;，但幸运的是它们之间的区别仅在于一些我们用不到的高级功能。目前常见的UART都能兼容到&lt;a href=&quot;https://en.wikipedia.org/wiki/16550_UART&quot;&gt;16550 UART&lt;/a&gt;，所以我们的测试框架就选用这个型号。&lt;/p&gt;
&lt;p&gt;我们将使用&lt;a href=&quot;https://docs.rs/uart_16550&quot;&gt;&lt;code&gt;uart_16550&lt;/code&gt;&lt;/a&gt; crate来初始化UART并通过串口发送数据。为了将它作为一个依赖项添加，我们更新了&lt;code&gt;Cargo.toml&lt;/code&gt;和&lt;code&gt;main.rs&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
uart_16550 = &quot;0.2.0&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;uart_16550&lt;/code&gt;crate中包含了用以表示UART寄存器的&lt;code&gt;SerialPort&lt;/code&gt;结构体，但我们仍然需要自己构造一个实例。为此，我们创建一个新的&lt;code&gt;serial&lt;/code&gt;模块，其内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mod serial;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use uart_16550::SerialPort;
use spin::Mutex;
use lazy_static::lazy_static;

lazy_static! {
    pub static ref SERIAL1: Mutex&amp;lt;SerialPort&amp;gt; = {
        let mut serial_port = unsafe { SerialPort::new(0x3F8) };
        serial_port.init();
        Mutex::new(serial_port)
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;类似&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/#lazy-statics&quot;&gt;VGA文本缓冲区&lt;/a&gt;中，使用&lt;code&gt;lazy_static&lt;/code&gt;和&lt;code&gt;spinlock&lt;/code&gt;来创建静态Writer实例的操作。我们这次通过使用&lt;code&gt;lazy_static&lt;/code&gt;，确保&lt;code&gt;init&lt;/code&gt;方法在第一次使用时有且仅有一次调用。&lt;/p&gt;
&lt;p&gt;与&lt;code&gt;isa-debug-exit&lt;/code&gt;设备一样，UART也是使用端口I/O进行编程的。由于UART比较复杂，它使用多个I/O端口来编程不同的设备寄存器。非安全函数&lt;code&gt;SerialPort::new&lt;/code&gt;需要UART的第一个I/O端口的地址作为参数，从这个地址就可以计算出所有需要的端口地址。我们传递的是端口地址&lt;code&gt;0x3F8&lt;/code&gt;，这是第一个串行接口的标准端口号。&lt;/p&gt;
&lt;p&gt;为了使串口便于使用，我们添加了&lt;code&gt;serial_print!&lt;/code&gt;和&lt;code&gt;serial_println!&lt;/code&gt;宏：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[doc(hidden)]
pub fn _print(args: ::core::fmt::Arguments) {
    use core::fmt::Write;
    SERIAL1.lock().write_fmt(args).expect(&quot;Printing to serial failed&quot;);
}

/// Prints to the host through the serial interface.
#[macro_export]
macro_rules! serial_print {
    ($($arg:tt)*) =&amp;gt; {
        $crate::serial::_print(format_args!($($arg)*));
    };
}

/// Prints to the host through the serial interface, appending a newline.
#[macro_export]
macro_rules! serial_println {
    () =&amp;gt; ($crate::serial_print!(&quot;\n&quot;));
    ($fmt:expr) =&amp;gt; ($crate::serial_print!(concat!($fmt, &quot;\n&quot;)));
    ($fmt:expr, $($arg:tt)*) =&amp;gt; ($crate::serial_print!(
        concat!($fmt, &quot;\n&quot;), $($arg)*));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个实现与我们的&lt;code&gt;print&lt;/code&gt;和&lt;code&gt;println&lt;/code&gt;宏的实现非常相似。由于&lt;code&gt;SerialPort&lt;/code&gt;类型已经实现了&lt;code&gt;fmt::Write&lt;/code&gt;trait，因此不需要提供自己的实现。&lt;/p&gt;
&lt;p&gt;这次不用再将测试结果打印到VGA文本缓冲区了，现在直接将结果打印到串行接口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[cfg(test)]
fn test_runner(tests: &amp;amp;[&amp;amp;dyn Fn()]) {
    serial_println!(&quot;Running {} tests&quot;, tests.len());
    […]
}

#[test_case]
fn trivial_assertion() {
    serial_print!(&quot;trivial assertion... &quot;);
    assert_eq!(1, 1);
    serial_println!(&quot;[ok]&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到&lt;code&gt;serial_println&lt;/code&gt;宏直接存在于根命名空间下，这是因为我们使用了&lt;code&gt;#[macro_export]&lt;/code&gt;属性。因此，如果通过使用&lt;code&gt;crate::serial::serial_println&lt;/code&gt;将无法导入该宏。&lt;/p&gt;
&lt;h3&gt;QEMU参数&lt;/h3&gt;
&lt;p&gt;要查看QEMU的串行输出，我们需要使用&lt;code&gt;-serial&lt;/code&gt;参数来重定向输出到stdout：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[package.metadata.bootimage]
test-args = [
    &quot;-device&quot;, &quot;isa-debug-exit,iobase=0xf4,iosize=0x04&quot;, &quot;-serial&quot;, &quot;stdio&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在运行&lt;code&gt;cargo test&lt;/code&gt;时，直接可以在控制台中看到测试输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
    Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
    deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
    isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [ok]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而，当测试失败时，我们仍然会在QEMU中看到输出，因为我们的&lt;code&gt;panic handler&lt;/code&gt;仍然使用&lt;code&gt;println&lt;/code&gt;。为了模拟这种情况，我们可以将测试&lt;code&gt;trivial_assertion&lt;/code&gt;中的断言改为&lt;code&gt;assert_eq!(0, 1)&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/testing/qemu-failed-test.png&quot; alt=&quot;panic时仍然在QEMU中打印&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到，panic信息仍然被打印到VGA缓冲区，而其余的测试输出则被打印到串口。panic信息非常有用，所以我们希望也能在控制台中看到这些信息。&lt;/p&gt;
&lt;h3&gt;Panic时打印错误信息&lt;/h3&gt;
&lt;p&gt;为了在panic中带着错误信息退出QEMU，我们可以使用&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/first-edition/conditional-compilation.html&quot;&gt;条件编译&lt;/a&gt;，在测试模式下使用另一个panic处理程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 现有的panic_handler
#[cfg(not(test))] // 新增条件编译属性
#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    println!(&quot;{}&quot;, info);
    loop {}
}

// 测试模式下的panic_handler
#[cfg(test)]
#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    serial_println!(&quot;[failed]\n&quot;);
    serial_println!(&quot;Error: {}\n&quot;, info);
    exit_qemu(QemuExitCode::Failed);
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在测试panic处理函数中，我们使用&lt;code&gt;serial_println&lt;/code&gt;代替&lt;code&gt;println&lt;/code&gt;，然后用失败退出码退出QEMU。请注意，在&lt;code&gt;exit_qemu&lt;/code&gt;调用之后，我们仍然需要写一个死循环，因为编译器并不知道在测试模式下&lt;code&gt;isa-debug-exit&lt;/code&gt;设备会导致程序退出。&lt;/p&gt;
&lt;p&gt;现在，QEMU也会因为测试失败而退出，并在控制台上打印一个有用的错误信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo test
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running target/x86_64-blog_os/debug/deps/blog_os-7b7c37b4ad62551a
Building bootloader
    Finished release [optimized + debuginfo] target(s) in 0.02s
Running: `qemu-system-x86_64 -drive format=raw,file=/…/target/x86_64-blog_os/debug/
    deps/bootimage-blog_os-7b7c37b4ad62551a.bin -device
    isa-debug-exit,iobase=0xf4,iosize=0x04 -serial stdio`
Running 1 tests
trivial assertion... [failed]

Error: panicked at &apos;assertion failed: `(left == right)`
  left: `0`,
 right: `1`&apos;, src/main.rs:65:5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在能够在控制台上看到了所有的测试输出了，我们也不再需要瞬间闪过的QEMU窗口。接下来，我们要完全隐藏它。&lt;/p&gt;
&lt;h3&gt;隐藏QEMU&lt;/h3&gt;
&lt;p&gt;通过使用&lt;code&gt;isa-debug-exit&lt;/code&gt;设备和串口完成完整的测试结果的上报，现在我们不再需要QEMU窗口了。可以通过给QEMU传递&lt;code&gt;-display none&lt;/code&gt;参数来隐藏它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[package.metadata.bootimage]
test-args = [
    &quot;-device&quot;, &quot;isa-debug-exit,iobase=0xf4,iosize=0x04&quot;, &quot;-serial&quot;, &quot;stdio&quot;,
    &quot;-display&quot;, &quot;none&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在的QEMU完全在后台运行，不再打开任何窗口。这不仅减少了弹出窗口的干扰，而且还允许我们的测试框架在没有图形用户界面的环境中运行，如CI服务或&lt;a href=&quot;https://en.wikipedia.org/wiki/Secure_Shell&quot;&gt;SSH&lt;/a&gt;连接。&lt;/p&gt;
&lt;h3&gt;超时&lt;/h3&gt;
&lt;p&gt;由于&lt;code&gt;cargo test&lt;/code&gt;会等到测试运行结束才退出，所以一个永不返回的测试会永远的将测试阻塞。这很不幸，但在实践中并不是一个大问题，因为通常很容易避免在测试中写死循环。然而，在我们的案例中，死循环会在各种情况下发生：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;bootloader无法加载我们的内核，这会导致系统无休止地重启。&lt;/li&gt;
&lt;li&gt;BIOS/UEFI固件无法加载bootloader，同样也会导致无休止地重启。&lt;/li&gt;
&lt;li&gt;CPU在我们一些函数的最后进入了&lt;code&gt;loop {}&lt;/code&gt;语句，例如因为QEMU退出设备不能正常工作。&lt;/li&gt;
&lt;li&gt;硬件导致系统复位，比如CPU异常没有被捕捉到（在以后的文章中会有详细解释）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于在很多情况下会出现死循环，&lt;code&gt;bootimage&lt;/code&gt;工具默认为每个测试可执行文件设置了5分钟的超时。如果测试没有在这个时间内完成，它就会被标记为失败，并将 &quot;Timed Out&quot;错误打印到控制台。这个功能可以确保陷入死循环的测试不会永远的阻塞&lt;code&gt;cargo test&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;可以通过在&lt;code&gt;trivial_assertion&lt;/code&gt;测试中加入一个&lt;code&gt;loop {}&lt;/code&gt;语句自己尝试一下。当运行&lt;code&gt;cargo test&lt;/code&gt;时，你会看到测试在5分钟后被标记为超时。超时时间可以通过&lt;code&gt;Cargo.toml&lt;/code&gt;中的&lt;code&gt;test-timeout&lt;/code&gt;键来进行配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[package.metadata.bootimage]
test-timeout = 300          # (in seconds)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你不想等待5分钟&lt;code&gt;trivial_assertion&lt;/code&gt;测试超时，可以暂时地降低这个值。&lt;/p&gt;
&lt;h3&gt;自动插入打印&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;trivial_assertion&lt;/code&gt;测试目前需要使用&lt;code&gt;serial_print!&lt;/code&gt;和&lt;code&gt;serial_println!&lt;/code&gt;来打印自己的状态信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn trivial_assertion() {
    serial_print!(&quot;trivial assertion... &quot;);
    assert_eq!(1, 1);
    serial_println!(&quot;[ok]&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为每一个测试手动添加这些打印语句是很麻烦的，升级一下&lt;code&gt;test_runner&lt;/code&gt;函数就可以做到自动打印这些消息。要实现这一点，首先需要创建一个新的&lt;code&gt;Testable&lt;/code&gt;trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub trait Testable {
    fn run(&amp;amp;self) -&amp;gt; ();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个技巧是为所有具有&lt;a href=&quot;https://doc.rust-lang.org/stable/core/ops/trait.Fn.html&quot;&gt;&lt;code&gt;Fn()&lt;/code&gt;trait&lt;/a&gt;的&lt;code&gt;T&lt;/code&gt;类型实现这个测试用trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl&amp;lt;T&amp;gt; Testable for T
where
    T: Fn(),
{
    fn run(&amp;amp;self) {
        serial_print!(&quot;{}...\t&quot;, core::any::type_name::&amp;lt;T&amp;gt;());
        self();
        serial_println!(&quot;[ok]&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在实现&lt;code&gt;run&lt;/code&gt;函数时，首先使用&lt;a href=&quot;https://doc.rust-lang.org/stable/core/any/fn.type_name.html&quot;&gt;&lt;code&gt;any::type_name&lt;/code&gt;&lt;/a&gt;函数打印函数名。这个函数在编译器中直接实现，它返回每个类型的字符串描述。对于函数来说，类型就是它们的名字，而这种情况这正是我们希望的。&lt;code&gt;\t&lt;/code&gt;字符是&lt;a href=&quot;https://en.wikipedia.org/wiki/Tab_key#Tab_characters&quot;&gt;制表符tab&lt;/a&gt;，为了与&lt;code&gt;[ok]&lt;/code&gt;信息进行一定程度的对齐。&lt;/p&gt;
&lt;p&gt;打印函数名后，我们使用&lt;code&gt;self()&lt;/code&gt;调用测试函数本身。这只是因为我们要求&lt;code&gt;self&lt;/code&gt;实现了&lt;code&gt;Fn()&lt;/code&gt;trait。在测试函数返回后，我们打印[ok]来表示该函数没有panic。&lt;/p&gt;
&lt;p&gt;最后一步是为&lt;code&gt;test_runner&lt;/code&gt;升级新特性&lt;code&gt;Testable&lt;/code&gt;trait：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[cfg(test)]
pub fn test_runner(tests: &amp;amp;[&amp;amp;dyn Testable]) {
    serial_println!(&quot;Running {} tests&quot;, tests.len());
    for test in tests {
        test.run(); // new
    }
    exit_qemu(QemuExitCode::Success);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅有的两个变化是测试参数的类型从&lt;code&gt;&amp;amp;[&amp;amp;dyn Fn()]&lt;/code&gt;变成了&lt;code&gt;&amp;amp;[&amp;amp;dyn Testable]&lt;/code&gt;，以及我们现在调用的是&lt;code&gt;test.run()&lt;/code&gt;而不是&lt;code&gt;test()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在可以从&lt;code&gt;trivial_assertion&lt;/code&gt;测试中删除打印语句，因为它们现在是自动打印的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn trivial_assertion() {
    assert_eq!(1, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在&lt;code&gt;cargo test&lt;/code&gt;的输出是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Running 1 tests
blog_os::trivial_assertion...	[ok]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在函数名包含了函数的完整路径，这在不同模块中的测试函数有相同名称时很有用。其余的输出看起来和原来一样，只不过我们不再需要在测试中手动添加打印语句了。&lt;/p&gt;
&lt;h2&gt;VGA缓冲区测试&lt;/h2&gt;
&lt;p&gt;有了一个好用的测试框架，现在我们可以为VGA缓冲区的实现创建一些测试。首先，我们创建一个非常简单的测试来验证&lt;code&gt;println&lt;/code&gt;是否能够正常工作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn test_println_simple() {
    println!(&quot;test_println_simple output&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该测试只是打印一些字符到VGA缓冲区。如果测试没有panic，这意味着&lt;code&gt;println&lt;/code&gt;调用也没有panic。&lt;/p&gt;
&lt;p&gt;为了确保即使打印了很多行，并且行数多到从屏幕上方移除旧行，也不会发生panic，我们可以创建另一个测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn test_println_many() {
    for _ in 0..200 {
        println!(&quot;test_println_many output&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们还可以创建一个测试函数来验证打印的行是否真的出现在屏幕上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn test_println_output() {
    let s = &quot;Some test string that fits on a single line&quot;;
    println!(&quot;{}&quot;, s);
    for (i, c) in s.chars().enumerate() {
        let screen_char = WRITER.lock().buffer.chars[BUFFER_HEIGHT - 2][i].read();
        assert_eq!(char::from(screen_char.ascii_character), c);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数指定了一个测试字符串，先使用&lt;code&gt;println&lt;/code&gt;打印，然后遍历屏幕字符，也就是静态变量&lt;code&gt;WRITER&lt;/code&gt;中代表VGA文本缓冲区的二维数组。由于&lt;code&gt;println&lt;/code&gt;打印到最后一行屏幕后立即附加一个换行符，所以字符串应该出现在第&lt;code&gt;BUFFER_HEIGHT - 2&lt;/code&gt;行。&lt;/p&gt;
&lt;p&gt;利用&lt;a href=&quot;https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate&quot;&gt;&lt;code&gt;enumerate&lt;/code&gt;&lt;/a&gt;函数，使用变量&lt;code&gt;i&lt;/code&gt;记录迭代次数，然后用它来从VGA缓冲区二维数组中获取与&lt;code&gt;c&lt;/code&gt;对应的字符，通过将屏幕字符的&lt;code&gt;ascii_character&lt;/code&gt;与c进行比较，我们确保字符串的每个字符都真正出现在VGA文本缓冲区中。&lt;/p&gt;
&lt;p&gt;我们还可以创建更多的测试函数，例如，测试打印很长的行时字符是否被正确封装的函数；或者测试是否能够正确处理换行符、非打印字符、非unicode字符的函数。&lt;/p&gt;
&lt;p&gt;然而，在这篇文章的其余部分，我们将解释如何创建集成测试来测试不同组件之间的交互。&lt;/p&gt;
&lt;h2&gt;集成测试&lt;/h2&gt;
&lt;p&gt;Rust中&lt;a href=&quot;https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests&quot;&gt;集成测试&lt;/a&gt;的惯例是把它们放到项目根目录下的&lt;code&gt;tests&lt;/code&gt;目录中（即与src目录平级）。默认测试框架和自定义测试框架都会自动识别并执行该目录下的所有测试。&lt;/p&gt;
&lt;p&gt;所有的集成测试都是与与&lt;code&gt;main.rs&lt;/code&gt;完全分开的单独的可执行文件。这意味着我们需要为每个测试定义一个入口点函数。让我们创建一个名为&lt;code&gt;basic_boot&lt;/code&gt;的集成测试示例，仔细看看它是如何工作的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = &quot;test_main&quot;]

use core::panic::PanicInfo;

#[no_mangle] // don&apos;t mangle the name of this function
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    test_main();

    loop {}
}

fn test_runner(tests: &amp;amp;[&amp;amp;dyn Fn()]) {
    unimplemented!();
}

#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于集成测试是独立的可执行文件，我们需要再次提供所有的crate属性（如&lt;code&gt;no_std&lt;/code&gt;、&lt;code&gt;no_main&lt;/code&gt;、&lt;code&gt;test_runner&lt;/code&gt;等）。我们还需要创建一个新的入口点函数&lt;code&gt;_start&lt;/code&gt;，它调用测试入口点函数&lt;code&gt;test_main&lt;/code&gt;。我们不需要任何&lt;code&gt;cfg(test)&lt;/code&gt;属性，因为集成测试的可执行文件永远不会以非测试模式构建。&lt;/p&gt;
&lt;p&gt;我们使用总是panic的&lt;a href=&quot;https://doc.rust-lang.org/core/macro.unimplemented.html&quot;&gt;&lt;code&gt;unimplemented&lt;/code&gt;&lt;/a&gt;宏作为&lt;code&gt;test_runner&lt;/code&gt;函数的占位符，而且暂时在&lt;code&gt;panic_handler&lt;/code&gt;中只做&lt;code&gt;loop&lt;/code&gt;。理想情况下，我们希望能像在&lt;code&gt;main.rs&lt;/code&gt;中一样使用&lt;code&gt;serial_println&lt;/code&gt;宏和&lt;code&gt;exit_qemu&lt;/code&gt;函数实现这些函数。问题是，我们无法访问这些函数，因为测试是完全独立于main.rs可执行文件构建的。&lt;/p&gt;
&lt;p&gt;如果你在这个阶段运行&lt;code&gt;cargo test&lt;/code&gt;，你会得到一个无休止的循环，因为&lt;code&gt;panic_handler&lt;/code&gt;会无休止地循环。你需要使用&lt;code&gt;Ctrl+c&lt;/code&gt;快捷键来退出QEMU。&lt;/p&gt;
&lt;h3&gt;创建单独的库&lt;/h3&gt;
&lt;p&gt;为了使集成测试能够使用所需的功能，我们需要从&lt;code&gt;main.rs&lt;/code&gt;中分离出一个库，以供其他crate和集成测试可执行文件使用。为此，我们创建一个新的&lt;code&gt;src/lib.rs&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;与&lt;code&gt;main.rs&lt;/code&gt;一样，&lt;code&gt;lib.rs&lt;/code&gt;也是一个特殊的文件，会被cargo自动识别。这个库是一个独立的编译单元，所以我们需要再次指定&lt;code&gt;#![no_std]&lt;/code&gt;属性。&lt;/p&gt;
&lt;p&gt;为了使我们的库能与&lt;code&gt;cargo test&lt;/code&gt;配合使用，我们还需要将&lt;code&gt;main.rs&lt;/code&gt;中的测试函数和属性移到&lt;code&gt;lib.rs&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
#![cfg_attr(test, no_main)]
#![feature(custom_test_frameworks)]
#![test_runner(crate::test_runner)]
#![reexport_test_harness_main = &quot;test_main&quot;]

use core::panic::PanicInfo;

pub trait Testable {
    fn run(&amp;amp;self) -&amp;gt; ();
}

impl&amp;lt;T&amp;gt; Testable for T
where
    T: Fn(),
{
    fn run(&amp;amp;self) {
        serial_print!(&quot;{}...\t&quot;, core::any::type_name::&amp;lt;T&amp;gt;());
        self();
        serial_println!(&quot;[ok]&quot;);
    }
}

pub fn test_runner(tests: &amp;amp;[&amp;amp;dyn Testable]) {
    serial_println!(&quot;Running {} tests&quot;, tests.len());
    for test in tests {
        test.run();
    }
    exit_qemu(QemuExitCode::Success);
}

pub fn test_panic_handler(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    serial_println!(&quot;[failed]\n&quot;);
    serial_println!(&quot;Error: {}\n&quot;, info);
    exit_qemu(QemuExitCode::Failed);
    loop {}
}

/// Entry point for `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    test_main();
    loop {}
}

#[cfg(test)]
#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    test_panic_handler(info)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了使可执行文件和集成测试能够调用&lt;code&gt;test_runner&lt;/code&gt;，我们不对它应用&lt;code&gt;cfg(test)&lt;/code&gt;属性，并将其公开。我们还将&lt;code&gt;panic_handler&lt;/code&gt;的实现提取到一个公共的&lt;code&gt;test_panic_handler&lt;/code&gt;函数中，这样它也可以用于可执行文件。&lt;/p&gt;
&lt;p&gt;由于我们的&lt;code&gt;lib.rs&lt;/code&gt;是独立于&lt;code&gt;main.rs&lt;/code&gt;进行测试的，所以当库以测试模式编译时，我们需要添加一个&lt;code&gt;_start&lt;/code&gt;入口点和一个&lt;code&gt;panic_handler&lt;/code&gt;。通过使用&lt;a href=&quot;https://doc.rust-lang.org/reference/conditional-compilation.html#the-cfg_attr-attribute&quot;&gt;&lt;code&gt;cfg_attr&lt;/code&gt;&lt;/a&gt; crate属性，我们在这种情况下有条件地启用&lt;code&gt;no_main&lt;/code&gt;属性。&lt;/p&gt;
&lt;p&gt;继续将&lt;code&gt;QemuExitCode&lt;/code&gt;枚举和&lt;code&gt;exit_qemu&lt;/code&gt;函数移出，并将它们公开：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum QemuExitCode {
    Success = 0x10,
    Failed = 0x11,
}

pub fn exit_qemu(exit_code: QemuExitCode) {
    use x86_64::instructions::port::Port;

    unsafe {
        let mut port = Port::new(0xf4);
        port.write(exit_code as u32);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，可执行文件和集成测试可以从库中导入这些函数，而不需要定义自己的实现。为了让&lt;code&gt;println&lt;/code&gt;和&lt;code&gt;serial_println&lt;/code&gt;也能使用，我们把模块的声明也移到了这里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod serial;
pub mod vga_buffer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们将这些模块公开，以使它们可以在库外使用。这也是使&lt;code&gt;println&lt;/code&gt;和&lt;code&gt;serial_println&lt;/code&gt;宏可用的必要条件，因为它们使用了模块的&lt;code&gt;_print&lt;/code&gt;函数。&lt;/p&gt;
&lt;p&gt;现在我们可以使用新写好的库更新main.rs：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]
#![feature(custom_test_frameworks)]
#![test_runner(blog_os::test_runner)]
#![reexport_test_harness_main = &quot;test_main&quot;]

use core::panic::PanicInfo;
use blog_os::println;

#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    println!(&quot;Hello World{}&quot;, &quot;!&quot;);

    #[cfg(test)]
    test_main();

    loop {}
}

/// This function is called on panic.
#[cfg(not(test))]
#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    println!(&quot;{}&quot;, info);
    loop {}
}

#[cfg(test)]
#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    blog_os::test_panic_handler(info)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个库可以像普通的外部crate一样使用。调用它就像调用本项目的crate一样，在这里就是&lt;code&gt;blog_os&lt;/code&gt;。上面的代码在&lt;code&gt;test_runner&lt;/code&gt;属性中使用了&lt;code&gt;blog_os::test_runner&lt;/code&gt;函数，在&lt;code&gt;cfg(test)&lt;/code&gt;的&lt;code&gt;panic_handler&lt;/code&gt;中使用了&lt;code&gt;blog_os::test_panic_handler&lt;/code&gt;函数。同时，它还导入了&lt;code&gt;println&lt;/code&gt;宏，使其可以用于&lt;code&gt;_start&lt;/code&gt;和&lt;code&gt;panic&lt;/code&gt;函数。&lt;/p&gt;
&lt;p&gt;这时，&lt;code&gt;cargo run&lt;/code&gt;和&lt;code&gt;cargo test&lt;/code&gt;应该又可以工作了。当然，&lt;code&gt;cargo test&lt;/code&gt;仍然会无休止地循环（你可以用&lt;code&gt;Ctrl+c&lt;/code&gt;退出）。让我们通过在集成测试中使用所需的库函数来解决这个问题。&lt;/p&gt;
&lt;h3&gt;完成集成测试&lt;/h3&gt;
&lt;p&gt;如同&lt;code&gt;src/main.rs&lt;/code&gt;一样，我们的&lt;code&gt;tests/basic_boot.rs&lt;/code&gt;可执行文件也可以从新库中导入类型。这让我们可以导入缺少的组件来完成集成测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![test_runner(blog_os::test_runner)]

#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    blog_os::test_panic_handler(info)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们不需要重新实现一个测试runner，而是使用我们库中的&lt;code&gt;test_runner&lt;/code&gt;函数。对于panic处理器，我们则调用&lt;code&gt;blog_os::test_panic_handler&lt;/code&gt;函数，就像我们在&lt;code&gt;main.rs&lt;/code&gt;中做的那样。&lt;/p&gt;
&lt;p&gt;现在&lt;code&gt;cargo test&lt;/code&gt;又正常退出了。当你运行它的时候，你会发现，它分别为&lt;code&gt;lib.rs&lt;/code&gt;、&lt;code&gt;main.rs&lt;/code&gt;和&lt;code&gt;basic_boot.rs&lt;/code&gt;构建和运行测试。对于&lt;code&gt;main.rs&lt;/code&gt;和&lt;code&gt;basic_boot&lt;/code&gt;集成测试，它报告&quot;Running 0 tests&quot;，因为这些文件没有任何用&lt;code&gt;#[test_case]&lt;/code&gt;注释的函数。&lt;/p&gt;
&lt;p&gt;现在我们可以在&lt;code&gt;basic_boot.rs&lt;/code&gt;中添加测试。例如，我们可以测试&lt;code&gt;println&lt;/code&gt;是否能够正常工作，就像我们在VGA缓冲区测试中做的那样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use blog_os::println;

#[test_case]
fn test_println() {
    println!(&quot;test_println output&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们现在运行&lt;code&gt;cargo test&lt;/code&gt;时，我们看到它找到并执行了测试函数。&lt;/p&gt;
&lt;p&gt;这个测试现在看起来可能有点无用，因为它几乎和VGA缓冲区的那个测试一模一样。但是，将来我们的&lt;code&gt;main.rs&lt;/code&gt;和&lt;code&gt;lib.rs&lt;/code&gt;的&lt;code&gt;_start&lt;/code&gt;函数可能会扩充新内容，并在运行&lt;code&gt;test_main&lt;/code&gt;函数之前调用各种初始化例程，这样两个测试就会在截然不同的环境中执行。&lt;/p&gt;
&lt;p&gt;通过在&lt;code&gt;basic_boot&lt;/code&gt;环境下测试&lt;code&gt;println&lt;/code&gt;，而不调用&lt;code&gt;_start&lt;/code&gt;中的任何初始化例程，我们可以确保&lt;code&gt;println&lt;/code&gt;在启动后能正常工作。这一点很重要，因为我们需要依靠它来打印panic信息。&lt;/p&gt;
&lt;h3&gt;更多测试&lt;/h3&gt;
&lt;p&gt;集成测试的强大之处在于，它们被视为完全独立的程序执行。这使得它们可以完全控制环境，从而可以测试代码是否与CPU或硬件设备正确交互。&lt;/p&gt;
&lt;p&gt;我们的&lt;code&gt;basic_boot&lt;/code&gt;测试是一个非常简单的集成测试的例子。在未来，我们的内核将增加更多特性，并以各种方式与硬件交互。通过添加集成测试，我们可以确保这些交互正常工作（且能够持续正常工作）符合预期。对于未来可能的测试，我们有一些想法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPU异常&lt;/strong&gt;：当代码执行了无效的操作（比如除零运算），CPU会抛出一个异常。内核可以为这种异常注册处理函数。集成测试可以验证当CPU异常发生时，是否调用了正确的异常处理函数；或是在可解决的异常后发生后，是否能继续执行正确操作。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;页表&lt;/strong&gt;：页表定义了哪些内存区域是有效的和可访问的。通过修改页表，可以分配新的内存区域，例如在启动程序时。集成测试可以在&lt;code&gt;_start&lt;/code&gt;函数中对页表进行一些修改，然后在&lt;code&gt;#[test_case]&lt;/code&gt;函数中验证修改是否有预期效果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;用户空间程序&lt;/strong&gt;：用户空间程序是指对系统资源访问受限的程序。例如，它们不能访问内核数据结构或其他程序的内存。集成测试可以启动用户空间程序并执行被禁止的操作，而后验证内核是否阻止了这些应该被禁止的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你可以想象，还有很多测试是可能的。通过添加这样的测试，我们可以确保当我们在内核中添加新功能或者重构代码时，不会意外地破坏它们。当我们的内核变得更大、更复杂时，这一点尤其重要。&lt;/p&gt;
&lt;h3&gt;测试应该panic的行为&lt;/h3&gt;
&lt;p&gt;标准库的测试框架支持名为&lt;a href=&quot;https://doc.rust-lang.org/rust-by-example/testing/unit_testing.html#testing-panics&quot;&gt;&lt;code&gt;#[should_panic]&lt;/code&gt;的属性&lt;/a&gt;，以允许构建应该失败的测试。这很有用，例如当传递一个无效参数时，可以验证函数是否会出现错误。不幸的是，&lt;code&gt;#[no_std]&lt;/code&gt;的crate并不支持这个属性，因为它需要标准库的支持。&lt;/p&gt;
&lt;p&gt;虽然我们不能在内核中使用&lt;code&gt;#[should_panic]&lt;/code&gt;属性，但是我们可以通过创建一个集成测试来获得类似的行为，这个测试可以从panic处理程序中获得成功的错误代码。让我们开始创建这样一个名为&lt;code&gt;should_panic&lt;/code&gt;的测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]

use core::panic::PanicInfo;
use blog_os::{QemuExitCode, exit_qemu, serial_println};

#[panic_handler]
fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
    serial_println!(&quot;[ok]&quot;);
    exit_qemu(QemuExitCode::Success);
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个测试仍然不完整，因为它还没有定义&lt;code&gt;_start&lt;/code&gt;函数或任何自定义测试runner属性。让我们来补充缺失的部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![feature(custom_test_frameworks)]
#![test_runner(test_runner)]
#![reexport_test_harness_main = &quot;test_main&quot;]

#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    test_main();

    loop {}
}

pub fn test_runner(tests: &amp;amp;[&amp;amp;dyn Fn()]) {
    serial_println!(&quot;Running {} tests&quot;, tests.len());
    for test in tests {
        test();
        serial_println!(&quot;[test did not panic]&quot;);
        exit_qemu(QemuExitCode::Failed);
    }
    exit_qemu(QemuExitCode::Success);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试没有复用&lt;code&gt;lib.rs&lt;/code&gt;中的&lt;code&gt;test_runner&lt;/code&gt;，而是定义了自己的&lt;code&gt;test_runner&lt;/code&gt;函数，当测试没有panic并返回时（我们希望测试会产生panic），就以失败码退出。如果没有定义测试函数，runner就会以成功码退出。由于runner总是在运行一个测试后退出，所以定义多个&lt;code&gt;#[test_case]&lt;/code&gt;函数是没有意义的。&lt;/p&gt;
&lt;p&gt;现在我们可以创建一个应该会失败的测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use blog_os::serial_print;

#[test_case]
fn should_fail() {
    serial_print!(&quot;should_panic::should_fail...\t&quot;);
    assert_eq!(0, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试使用&lt;code&gt;assert_eq&lt;/code&gt;来断言&lt;code&gt;0&lt;/code&gt;和&lt;code&gt;1&lt;/code&gt;相等，这当然会失败，于是测试就可以如愿以偿的panic了。注意，我们需要在这里使用&lt;code&gt;serial_print!&lt;/code&gt;手动打印函数名，因为我们没有使用&lt;code&gt;Testable&lt;/code&gt;trait。&lt;/p&gt;
&lt;p&gt;当我们通过&lt;code&gt;cargo test --test should_panic&lt;/code&gt;运行测试时，我们看到测试是成功的，因为测试如期panic了。当我们注释掉断言一行并再次运行测试时，我们看到它确实失败了，出现了&quot;test did not panic&quot;的消息。&lt;/p&gt;
&lt;p&gt;这种方法的一个重要缺点是，它只对单个测试函数有效。对于多个&lt;code&gt;#[test_case]&lt;/code&gt;函数，只有第一个函数被执行，因为在调用了panic处理器之后便无法继续执行了。目前我还不知道有什么好的方法来解决这个问题，如果你有什么想法，请告诉我！&lt;/p&gt;
&lt;h3&gt;无环境测试&lt;/h3&gt;
&lt;p&gt;对于只有一个测试函数的集成测试（如&lt;code&gt;should_panic&lt;/code&gt;测试），其实并不需要测试runner。在这样的情况下，我们可以完全禁用测试runner，并直接在&lt;code&gt;_start&lt;/code&gt;函数中运行我们的测试。&lt;/p&gt;
&lt;p&gt;其中的关键是在&lt;code&gt;Cargo.toml&lt;/code&gt;中禁用测试的harness标志，它定义了集成测试是否使用测试runner。当它被设置为&lt;code&gt;false&lt;/code&gt;时，默认的测试runner和自定义测试runner功能都会被禁用，这样测试就会被当作一个普通的可执行文件来处理。&lt;/p&gt;
&lt;p&gt;让我们为&lt;code&gt;should_panic&lt;/code&gt;测试禁用&lt;code&gt;harness&lt;/code&gt;标识：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[[test]]
name = &quot;should_panic&quot;
harness = false
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，我们通过删除测试runner相关代码，大幅简化了&lt;code&gt;should_panic&lt;/code&gt;测试，看起来是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]

use core::panic::PanicInfo;
use blog_os::{exit_qemu, serial_print, serial_println, QemuExitCode};

#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    should_fail();
    serial_println!(&quot;[test did not panic]&quot;);
    exit_qemu(QemuExitCode::Failed);
    loop{}
}

fn should_fail() {
    serial_print!(&quot;should_panic::should_fail...\t&quot;);
    assert_eq!(0, 1);
}

#[panic_handler]
fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
    serial_println!(&quot;[ok]&quot;);
    exit_qemu(QemuExitCode::Success);
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们直接从&lt;code&gt;_start&lt;/code&gt;函数中调用&lt;code&gt;should_fail&lt;/code&gt;函数，如果函数能够返回，则以失败码退出。当我们现在运行&lt;code&gt;cargo test --test should_panic&lt;/code&gt;时，我们看到测试的行为和之前完全一样。&lt;/p&gt;
&lt;p&gt;除了创建&lt;code&gt;should_panic&lt;/code&gt;测试外，禁用&lt;code&gt;harness&lt;/code&gt;属性对于复杂的集成测试也很有用，例如当各测试函数均有副作用，需要按照指定的顺序运行时。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;测试是一种非常有用的技术，可以确保某些组件具有所期望的行为。即使测试不能表明没有bug，但也仍是找到bug的有用工具，尤其是避免回溯。&lt;/p&gt;
&lt;p&gt;这篇文章解释了如何为Rust内核建立一个测试框架。我们使用Rust的自定义测试框架功能在裸机环境中实现对简单的&lt;code&gt;＃[test_case]&lt;/code&gt;属性的支持。 通过使用QEMU的&lt;code&gt;isa-debug-exit&lt;/code&gt;设备，测试runner可以在运行测试后退出QEMU并报告测试结果。为了将错误消息打印到控制台而不是VGA缓冲区，我们为串行端口创建了一个基础驱动程序。&lt;/p&gt;
&lt;p&gt;在为&lt;code&gt;println&lt;/code&gt;宏创建了一些测试之后，我们在后半部分探讨了集成测试。我们了解到集成测试位于&lt;code&gt;tests&lt;/code&gt;目录中，并被视为完全独立的可执行文件。为了使他们能够访问&lt;code&gt;exit_qemu&lt;/code&gt;函数和&lt;code&gt;serial_println&lt;/code&gt;宏，我们将大部分代码移入了一个库，该库可以被所有可执行文件和集成测试导入。由于集成测试在各自独立的环境中运行，因此可以测试与硬件的交互或创建应引起panic的测试。&lt;/p&gt;
&lt;p&gt;现在，我们有了一个在QEMU内真实环境中运行的测试框架。通过在以后的文章中创建更多测试，我们可以使内核变得更复杂时依旧保持可维护性。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;在下一篇文章中，我们将探讨&lt;em&gt;CPU异常&lt;/em&gt;。当发生非法事件时，CPU将抛出这些异常，例如除零或访问未映射的内存页面（即所谓的“页面错误”）。能够捕获和检查这些异常对于调试将来的错误非常重要。异常处理也非常类似于硬件中断的处理，这是提供键盘支持所必需的。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 2.1 - CPU异常</title><link>https://zlotus.github.io/2021/03/06/writing-an-os-in-rust-2.1/</link><guid isPermaLink="true">https://zlotus.github.io/2021/03/06/writing-an-os-in-rust-2.1/</guid><description>使用Rust编写操作系统 - 2.1 - CPU异常</description><pubDate>Sat, 06 Mar 2021 21:15:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/&quot;&gt;CPU Exceptions&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;CPU异常发生在多种错误场景中，如在访问无效的内存地址时或是在除零运算时。为了对错误作出反应，我们需要建立一个提供处理函数的中断描述符表。在本文的结尾，我们的内核将能够捕获断点异常并在处理后恢复正常运行。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-05&quot;&gt;post-05&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;概述&lt;/h2&gt;
&lt;p&gt;异常，意味着当前执行的指令有问题。例如，如果当前指令试图除以0，则CPU会发出异常。发生异常时，CPU会中断其当前工作，并根据具体异常的类型，立即调用相应的异常处理函数。&lt;/p&gt;
&lt;p&gt;在x86平台上，大概有20种不同的CPU异常类型。其中最重要的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;页面错误&lt;/strong&gt;：在执行非法的内存访问时将发生页面错误。例如，若当前指令试图从未映射的页面读取或试图向只读页面写入时。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无效操作码&lt;/strong&gt;：在当前指令无效时（例如，当我们尝试在不支持的旧CPU上使用较新的&lt;a href=&quot;https://en.wikipedia.org/wiki/Streaming_SIMD_Extensions&quot;&gt;SSE指令&lt;/a&gt;时）将发生此异常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;常规保护故障&lt;/strong&gt;：这是异常最常见的诱因。它发生在各种访问冲突中，例如试图在用户级代码中执行特权指令或是向配置寄存器中保留字段进行写入。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;双重故障&lt;/strong&gt;：发生异常时，CPU尝试调用相应的处理函数。如果&lt;em&gt;在调用异常处理函数时&lt;/em&gt;又发生另一个异常，则CPU会引发双重故障异常。此外，当没有为某异常注册相应的处理函数时，也会引发这个异常。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;三重故障&lt;/strong&gt;：如果在CPU尝试调用双重故障异常处理函数时又发生异常，则将引发致命的三重故障。我们无法捕捉或处理三重​​故障。大多数处理器通过复位并重新引导操作系统来对此故障作出反应。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有关异常的完整列表，请查看&lt;a href=&quot;https://wiki.osdev.org/Exceptions&quot;&gt;OSDev Wiki&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;中断描述符表&lt;/h3&gt;
&lt;p&gt;为了捕获和处理异常，我们必须建立一个所谓的&lt;em&gt;中断描述符表&lt;/em&gt;(IDT)。在此表中，我们可以为每个CPU异常指定一个处理函数。硬件会直接使用此表，所以我们需要遵循预定义的格式。每个条目必须具有以下的16字节结构：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;类型&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;u16&lt;/td&gt;
&lt;td&gt;函数指针 [0:15]&lt;/td&gt;
&lt;td&gt;处理函数指针的低(16)位。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u16&lt;/td&gt;
&lt;td&gt;GDT 选择子&lt;/td&gt;
&lt;td&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Global_Descriptor_Table&quot;&gt;全局描述符表&lt;/a&gt;中的代码段的选择器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u16&lt;/td&gt;
&lt;td&gt;选项字段&lt;/td&gt;
&lt;td&gt;参见下表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u16&lt;/td&gt;
&lt;td&gt;函数指针 [16:31]&lt;/td&gt;
&lt;td&gt;处理函数（handler function)指针的中(16)位。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u32&lt;/td&gt;
&lt;td&gt;函数指针 [32:63]&lt;/td&gt;
&lt;td&gt;处理函数（handler function)指针的高(32)位。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;u32&lt;/td&gt;
&lt;td&gt;保留位&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;选项字段具有以下格式：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;位&lt;/th&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0-2&lt;/td&gt;
&lt;td&gt;中断栈表索引&lt;/td&gt;
&lt;td&gt;0：不换栈，1-7：当处理函数被调用时，切换到中断栈表的第n个栈。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3-7&lt;/td&gt;
&lt;td&gt;保留位&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;0：中断门，1：陷阱门&lt;/td&gt;
&lt;td&gt;若此位设置为0，则处理函数被调用的时，中断会被禁用。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9-11&lt;/td&gt;
&lt;td&gt;必须为1&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;必须为0&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13‑14&lt;/td&gt;
&lt;td&gt;特权等级描述符(DPL)&lt;/td&gt;
&lt;td&gt;允许调用该处理函数的最小特权等级。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;条目是否存在&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;每个异常都有一个预定义的IDT索引。例如，无效操作码异常的表索引为6，而页面错误异常的表索引为14。因此，硬件可以为每个异常自动加载相应的IDT条目。 OSDev wiki中的&lt;a href=&quot;https://wiki.osdev.org/Exceptions&quot;&gt;异常表&lt;/a&gt;在&quot;Vector nr.&quot;列展示了所有异常的IDT索引。&lt;/p&gt;
&lt;p&gt;发生异常时，CPU大致将执行以下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;将某些寄存器压栈，包括指令指针和&lt;a href=&quot;https://en.wikipedia.org/wiki/FLAGS_register&quot;&gt;RFLAGS&lt;/a&gt;寄存器。（本文接下来将会用到这些值。）&lt;/li&gt;
&lt;li&gt;从中断描述符表（IDT）中读取相应的条目。例如，发生页面错误时，CPU读取第14个条目。&lt;/li&gt;
&lt;li&gt;检查是否存在该条目。若没有则引发双重故障。&lt;/li&gt;
&lt;li&gt;如果该条目是中断门（第40位未置为1），则禁用硬件中断。&lt;/li&gt;
&lt;li&gt;将指定的GDT选择器加载到CS段中。&lt;/li&gt;
&lt;li&gt;跳转到指定的处理函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在不必担心第4步和第5步，我们将在以后的文章中了解全局描述符表和硬件中断。&lt;/p&gt;
&lt;h2&gt;IDT类&lt;/h2&gt;
&lt;p&gt;我们无需自己创建IDT类，可以直接使用&lt;code&gt;x86_64&lt;/code&gt;crate的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html&quot;&gt;&lt;code&gt;InterruptDescriptorTable&lt;/code&gt;结构体&lt;/a&gt;，看起来像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[repr(C)]
pub struct InterruptDescriptorTable {
    pub divide_by_zero: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub debug: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub non_maskable_interrupt: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub breakpoint: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub overflow: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub bound_range_exceeded: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub invalid_opcode: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub device_not_available: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub double_fault: Entry&amp;lt;HandlerFuncWithErrCode&amp;gt;,
    pub invalid_tss: Entry&amp;lt;HandlerFuncWithErrCode&amp;gt;,
    pub segment_not_present: Entry&amp;lt;HandlerFuncWithErrCode&amp;gt;,
    pub stack_segment_fault: Entry&amp;lt;HandlerFuncWithErrCode&amp;gt;,
    pub general_protection_fault: Entry&amp;lt;HandlerFuncWithErrCode&amp;gt;,
    pub page_fault: Entry&amp;lt;PageFaultHandlerFunc&amp;gt;,
    pub x87_floating_point: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub alignment_check: Entry&amp;lt;HandlerFuncWithErrCode&amp;gt;,
    pub machine_check: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub simd_floating_point: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub virtualization: Entry&amp;lt;HandlerFunc&amp;gt;,
    pub security_exception: Entry&amp;lt;HandlerFuncWithErrCode&amp;gt;,
    // some fields omitted
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些字段的类型为&lt;code&gt;idt::Entry&amp;lt;F&amp;gt;&lt;/code&gt;，这是一个用于表示IDT条目字段的结构体（请参见上面的表格）。泛型参数F定义了预期的处理函数的类型。我们看到有的条目需要&lt;code&gt;HandlerFunc&lt;/code&gt;，有的则需要&lt;code&gt;HandlerFuncWithErrCode&lt;/code&gt;。而页面错误甚至有其自己的特殊类型：&lt;code&gt;PageFaultHandlerFunc&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;首先让我们看一下&lt;code&gt;HandlerFunc&lt;/code&gt;类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type HandlerFunc = extern &quot;x86-interrupt&quot; fn(_: &amp;amp;mut InterruptStackFrame);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是&lt;code&gt;extern &quot;x86-interrupt&quot; fn&lt;/code&gt;类型的别名。&lt;code&gt;extern&lt;/code&gt;关键字定义了一个具有&lt;a href=&quot;https://doc.rust-lang.org/nomicon/ffi.html#foreign-calling-conventions&quot;&gt;外部调用约定&lt;/a&gt;的函数，最常见的是与C代码进行通信(&lt;code&gt;extern &quot;C&quot; fn&lt;/code&gt;)的调用约定。不过，此处的&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定又是什么？&lt;/p&gt;
&lt;h2&gt;中断调用约定&lt;/h2&gt;
&lt;p&gt;异常与函数调用非常相似，都是CPU跳转到被调用函数的第一条指令并执行它。之后，CPU跳转到返回地址并继续执行父函数。&lt;/p&gt;
&lt;p&gt;但是，异常和函数调用之间存在主要区别在于，函数调用是被编译器插入的&lt;code&gt;call&lt;/code&gt;指令会主动调用的，而异常可能发生在任何指令中。为了了解这种差异的后果，我们需要更详细地研究函数调用。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Calling_convention&quot;&gt;调用约定&lt;/a&gt;明确了函数调用的细节。例如，调用约定指定了函数参数的放置位置（例如，在寄存器中还是在栈中）以及如何返回结果。在x86_64的Linux上，以下规则适用于C函数（在&lt;a href=&quot;https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf&quot;&gt;System V ABI&lt;/a&gt;中指定）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前六个整型参数放在在寄存器&lt;code&gt;rdi&lt;/code&gt;、&lt;code&gt;rsi&lt;/code&gt;、&lt;code&gt;rdx&lt;/code&gt;、&lt;code&gt;rcx&lt;/code&gt;、&lt;code&gt;r8&lt;/code&gt;、&lt;code&gt;r9&lt;/code&gt;中传递&lt;/li&gt;
&lt;li&gt;其他参数放在栈上传递&lt;/li&gt;
&lt;li&gt;结果放在&lt;code&gt;rax&lt;/code&gt;和&lt;code&gt;rdx&lt;/code&gt;上返回&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;请注意，Rust不遵循C ABI（实际上&lt;a href=&quot;https://github.com/rust-lang/rfcs/issues/600&quot;&gt;甚至还没有Rust ABI&lt;/a&gt;），因此这些规则仅适用于&lt;code&gt;extern &quot;C&quot; fn&lt;/code&gt;声明的函数。&lt;/p&gt;
&lt;h3&gt;Preserved寄存器与Scratch寄存器&lt;/h3&gt;
&lt;p&gt;调用约定将寄存器分为两种：&lt;em&gt;preserved&lt;/em&gt;寄存器和&lt;em&gt;scratch&lt;/em&gt;寄存器。&lt;/p&gt;
&lt;p&gt;在函数调用过程中，preserved寄存器的值必须保持不变。因此，仅当&lt;em&gt;被调用函数&lt;/em&gt;(&quot;callee&quot;)确定能够在返回前恢复寄存器原始值时，才可以写这些寄存器。因此，preserved寄存器称为*“被调用者保存寄存器”*(&quot;callee-saved&quot;)。一个常见用法是在函数开始时将这些寄存器保存在栈中，并在返回之前将其还原。&lt;/p&gt;
&lt;p&gt;相比之下，被调用函数可以无限制地写&lt;em&gt;scratch&lt;/em&gt; 寄存器。如果调用者想在函数调用期间保留scratch寄存器的值，则需要在函数调用之前进行备份和还原（如将其压入栈中）。因此，scratch寄存器是*“调用者保存寄存器”*(&quot;caller-saved&quot;)。&lt;/p&gt;
&lt;p&gt;在x86_64上，C调用约定指定以下preserved和scratch寄存器：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;preserved寄存器&lt;/th&gt;
&lt;th&gt;scratch寄存器&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rbp&lt;/code&gt;, &lt;code&gt;rbx&lt;/code&gt;, &lt;code&gt;rsp&lt;/code&gt;, &lt;code&gt;r12&lt;/code&gt;, &lt;code&gt;r13&lt;/code&gt;, &lt;code&gt;r14&lt;/code&gt;, &lt;code&gt;r15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rax&lt;/code&gt;, &lt;code&gt;rcx&lt;/code&gt;, &lt;code&gt;rdx&lt;/code&gt;, &lt;code&gt;rsi&lt;/code&gt;, &lt;code&gt;rdi&lt;/code&gt;, &lt;code&gt;r8&lt;/code&gt;, &lt;code&gt;r9&lt;/code&gt;, &lt;code&gt;r10&lt;/code&gt;, &lt;code&gt;r11&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;em&gt;被调用者保存&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;调用者保存&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;编译器知道这些规则，因此会生成合适的代码。例如，大多数函数都以&lt;code&gt;push rbp&lt;/code&gt;开始，即将&lt;code&gt;rbp&lt;/code&gt;备份在栈上（因为它是被调用者保存的寄存器）。&lt;/p&gt;
&lt;h3&gt;保存所有寄存器&lt;/h3&gt;
&lt;p&gt;与函数调用不同，&lt;em&gt;任何&lt;/em&gt;指令都可能发生异常。在大多数情况下，我们甚至在编译时都不知道生成的代码是否会导致异常。例如，编译器无法知道指令是否会导致栈溢出或页面错误。&lt;/p&gt;
&lt;p&gt;由于我们不知道何时发生异常，我们也就无法提前备份任何寄存器。也就是说，调用约定不能以依赖于调用者保存的寄存器的行为作为异常处理程序，而是应该以保存&lt;em&gt;所有&lt;/em&gt;寄存器的行为作为异常处理程序。&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定就是如此，因此它可以保证在函数返回时所有寄存器值都恢复到原始值。&lt;/p&gt;
&lt;p&gt;注意，这并不意味着函数开始时会将所有寄存器都保存到栈中，实际上编译出的代码仅备份会被函数覆盖的寄存器。这样，可以为仅使用几个寄存器的短函数生成非常高效的代码。&lt;/p&gt;
&lt;h3&gt;中断栈帧&lt;/h3&gt;
&lt;p&gt;在正常的函数调用中（使用&lt;code&gt;call&lt;/code&gt;指令），CPU在跳转到目标函数之前先将返回地址压栈。函数返回时（使用&lt;code&gt;ret&lt;/code&gt;指令），CPU将返回地址弹栈并跳转到该地址。因此，普通函数调用的栈帧如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/cpu-exceptions/function-stack-frame.svg&quot; alt=&quot;函数栈帧&quot; /&gt;&lt;/p&gt;
&lt;p&gt;但是，对于异常和中断处理程序，仅将返回地址压栈是不够的，因为中断处理程序通常在不同的上下文中运行（如栈指针、CPU标志等）。在发生中断时，CPU执行以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;对齐栈指针&lt;/strong&gt;：任何指令都可能发生中断，因此栈指针也可以是任何值。但是，某些CPU指令（例如某些SSE指令）要求栈指针在16字节边界上对齐，因此CPU在中断后立即执行此类对齐。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;切换栈&lt;/strong&gt;（在某些情况下）：当CPU特权级别改变时（例如，在用户模式程序中发生CPU异常时），将发生切换栈动作。还可以使用所谓的&lt;em&gt;中断栈表&lt;/em&gt;（将在下一篇文章中介绍）为特定的中断配置切换栈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压入旧栈指针&lt;/strong&gt;：在发生中断时（对齐之前），CPU压入栈指针(&lt;code&gt;rsp&lt;/code&gt;)和栈段(&lt;code&gt;ss&lt;/code&gt;)寄存器的值。因此，从中断处理程序返回时，这可以恢复栈指针的原始值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压入和更新&lt;code&gt;RFLAGS&lt;/code&gt;寄存器&lt;/strong&gt;：&lt;a href=&quot;https://en.wikipedia.org/wiki/FLAGS_register&quot;&gt;RFLAGS&lt;/a&gt;寄存器包含各种控制位和状态位。进入中断时，CPU修改一些位并将旧值压栈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压入指令指针&lt;/strong&gt;：在跳转到中断处理程序功能之前，CPU先压入指令指针(&lt;code&gt;rip&lt;/code&gt;)和代码段(&lt;code&gt;cs&lt;/code&gt;)。这相当于普通函数调用时的返回地址压栈。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;压入错误码&lt;/strong&gt;（对于某些异常）：对于某些特定的异常（例如页面错误），CPU压入一个错误码，用于描述异常原因。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;调用中断处理函数&lt;/strong&gt;：CPU从IDT的相应字段读取中断处理函数的地址和段描述符。之后，通过将这些值加载到&lt;code&gt;rip&lt;/code&gt;和&lt;code&gt;cs&lt;/code&gt;寄存器中来调用中断处理函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;因此，&lt;em&gt;中断栈帧&lt;/em&gt;如下所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/cpu-exceptions/exception-stack-frame.svg&quot; alt=&quot;异常栈帧&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;x86_64&lt;/code&gt;crate中，中断栈帧由&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptStackFrame.html&quot;&gt;&lt;code&gt;InterruptStackFrame&lt;/code&gt;&lt;/a&gt;结构体表示。结构体被以&lt;code&gt;＆mut&lt;/code&gt;的形式传递给中断处理函数，可用于获取异常原因相关信息。该结构体不包含错误码字段，因为只有少数异常会推送错误码。这些少数异常使用单独的&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/type.HandlerFuncWithErrCode.html&quot;&gt;&lt;code&gt;HandlerFuncWithErrCode&lt;/code&gt;&lt;/a&gt;函数类型，该函数类型具有附加的&lt;code&gt;error_code&lt;/code&gt;参数。&lt;/p&gt;
&lt;h3&gt;后台细节&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定是一个强大的抽象，它几乎隐藏了异常处理过程的所有杂乱细节。但是，有时了解幕后的执行细节会很有用。这是&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定要处理的事情的简短概述：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;取回参数&lt;/strong&gt;：大多数调用约定都希望参数在寄存器中传递。这对于异常处理程序是不可能的，因为在将参数备份到栈上之前，我们绝不能覆盖任何寄存器。而&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定知道这些参数已经以特定的偏移量存放在栈上了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;使用&lt;code&gt;iretq&lt;/code&gt;返回&lt;/strong&gt;：中断栈帧与常规函数调用的栈帧完全不同，这使得我们无法通过常规的&lt;code&gt;ret&lt;/code&gt;指令从中断处理函数中返回。相应的我们必须使用&lt;code&gt;iretq&lt;/code&gt;指令。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;处理错误码&lt;/strong&gt;：为某些异常而将错误码压栈会使事情变得更加复杂。这会更改栈的对齐方式（请参阅下一点），并且在需要返回前弹栈。&lt;code&gt;x86-interrupt&lt;/code&gt;中断调用约定封装了这些复杂过程。但是，它并不知道哪个处理程序函数该用于哪个异常，因此它需要从函数参数的数量中推断出该信息。这意味着程序员仍然有责任为每个异常选择正确的处理函数类型。幸运的是，由&lt;code&gt;x86_64&lt;/code&gt;crate定义的&lt;code&gt;InterruptDescriptorTable&lt;/code&gt;类型可确保使用正确的函数类型。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;栈对齐&lt;/strong&gt;：有些指令（尤其是SSE指令）需要16字节的栈对齐。CPU会在发生异常时确保这种对齐，但是某些异常会在将错误码压栈后再次破坏对齐。此情况发生时，&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定会通过重新对齐栈来解决此问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你对更多细节感兴趣：我们还有一系列文章，这些文章解释了如何使用&lt;a href=&quot;https://os.phil-opp.com/cpu-exceptions/#too-much-magic&quot;&gt;文末&lt;/a&gt;的&lt;a href=&quot;https://github.com/rust-lang/rfcs/blob/master/text/1201-naked-fns.md&quot;&gt;裸函数&lt;/a&gt;进行异常处理。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;p&gt;现在，我们了解了中断的原理，是时候在内核中处理CPU异常了。我们将从在&lt;code&gt;src/interrupts.rs&lt;/code&gt;中创建一个新的中断模块开始，该模块首先新建一个&lt;code&gt;init_idt&lt;/code&gt;函数，用于创建一个新的&lt;code&gt;InterruptDescriptorTable&lt;/code&gt;实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub mod interrupts;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::structures::idt::InterruptDescriptorTable;

pub fn init_idt() {
    let mut idt = InterruptDescriptorTable::new();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们可以添加异常处理函数了，首先为&lt;a href=&quot;https://wiki.osdev.org/Exceptions#Breakpoint&quot;&gt;断点异常&lt;/a&gt;添加处理程序。断点异常是测试异常处理函数的极佳案例。它的唯一用途是在执行断点指令&lt;code&gt;int3&lt;/code&gt;时临时暂停程序。&lt;/p&gt;
&lt;p&gt;断点异常通常在调试器（debugger）中使用：当用户设置断点时，调试器用&lt;code&gt;int3&lt;/code&gt;指令覆盖断点行的原指令，以便在CPU执行到该行时抛出断点异常。当用户想要继续执行程序时，调试器将&lt;code&gt;int3&lt;/code&gt;指令再次替换为原指令，然后继续执行程序。有关更多详细信息，请参见&lt;a href=&quot;https://eli.thegreenplace.net/2011/01/27/how-debuggers-work-part-2-breakpoints&quot;&gt;&lt;em&gt;“调试器的工作方式”&lt;/em&gt;&lt;/a&gt;系列。&lt;/p&gt;
&lt;p&gt;在我们的例子中，并不需要覆盖任何指令，只需要在执行断点指令时打印一条消息，然后继续执行该程序。因此，让我们创建一个简单的&lt;code&gt;breakpoint_handler&lt;/code&gt;函数并将其添加到IDT中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame};
use crate::println;

pub fn init_idt() {
    let mut idt = InterruptDescriptorTable::new();
    idt.breakpoint.set_handler_fn(breakpoint_handler);
}

extern &quot;x86-interrupt&quot; fn breakpoint_handler(
    stack_frame: &amp;amp;mut InterruptStackFrame)
{
    println!(&quot;EXCEPTION: BREAKPOINT\n{:#?}&quot;, stack_frame);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的处理程序仅输出一条消息，并使用pretty-prints样式打印中断栈帧以提升可读性。&lt;/p&gt;
&lt;p&gt;尝试对其进行编译会发生以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0658]: x86-interrupt ABI is experimental and subject to change (see issue #40180)
  --&amp;gt; src/main.rs:53:1
   |
53 | / extern &quot;x86-interrupt&quot; fn breakpoint_handler(stack_frame: &amp;amp;mut InterruptStackFrame) {
54 | |     println!(&quot;EXCEPTION: BREAKPOINT\n{:#?}&quot;, stack_frame);
55 | | }
   | |_^
   |
   = help: add #![feature(abi_x86_interrupt)] to the crate attributes to enable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发生此错误的原因是&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定这一特性仍然处于开发状态，还不稳定。如果一定要使用该特性，则必须通过在&lt;code&gt;lib.rs&lt;/code&gt;顶部显示的添加&lt;code&gt;#![feature(abi_x86_interrupt)]&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;加载IDT&lt;/h3&gt;
&lt;p&gt;我们需要使用&lt;a href=&quot;https://www.felixcloutier.com/x86/lgdt:lidt&quot;&gt;&lt;code&gt;lidt&lt;/code&gt;&lt;/a&gt;指令让CPU加载新建的中断描述符表。&lt;code&gt;x86_64&lt;/code&gt;crate的&lt;code&gt;InterruptDescriptorTable&lt;/code&gt;结构体将该指令封装为&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html#method.load&quot;&gt;&lt;code&gt;load&lt;/code&gt;&lt;/a&gt;方法。让我们尝试使用它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn init_idt() {
    let mut idt = InterruptDescriptorTable::new();
    idt.breakpoint.set_handler_fn(breakpoint_handler);
    idt.load();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时编译将出现以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: `idt` does not live long enough
  --&amp;gt; src/interrupts/mod.rs:43:5
   |
43 |     idt.load();
   |     ^^^ does not live long enough
44 | }
   | - borrowed value only lives until here
   |
   = note: borrowed value must be valid for the static lifetime...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看&lt;code&gt;load&lt;/code&gt;方法的文档可知，调用该方法的对象应为&lt;code&gt;＆&apos;static self&lt;/code&gt;，即该引用变量需要在程序运行时的整个生命周期中保持有效。这是因为CPU在每次中断时都会访问该表，直到我们加载了不同的IDT。因此，使用任何比&lt;code&gt;&apos;static&lt;/code&gt;短的生命周期都可能导致“析构后使用”的错误。&lt;/p&gt;
&lt;p&gt;而这正是此编译错误的原因。我们的&lt;code&gt;idt&lt;/code&gt;创建在栈上，仅在&lt;code&gt;init&lt;/code&gt;函数内部有效。该函数结束后，栈空间将重新分配给其他函数，因此CPU可能会将栈空间中的随机内容当做IDT。幸运的是，&lt;code&gt;InterruptDescriptorTable::load&lt;/code&gt;方法在其函数定义中声明了生命周期要求，于是Rust编译器能够在编译时防止这种可能的错误。&lt;/p&gt;
&lt;p&gt;要解决这个编译错误，我们需要将&lt;code&gt;idt&lt;/code&gt;存储在具有&lt;code&gt;&apos;static&lt;/code&gt;生命周期的地方。为此，我们通常可以使用&lt;a href=&quot;https://doc.rust-lang.org/std/boxed/struct.Box.html&quot;&gt;&lt;code&gt;Box&lt;/code&gt;&lt;/a&gt;为IDT在堆上分配空间，再将其转换为&lt;code&gt;&apos;static&lt;/code&gt;引用，但是由于我们正在编写OS内核，因此目前并没有堆可用。&lt;/p&gt;
&lt;p&gt;作为替代方案，我们可以尝试将IDT存储为&lt;code&gt;static&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();

pub fn init_idt() {
    IDT.breakpoint.set_handler_fn(breakpoint_handler);
    IDT.load();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是这里有一个问题：静态变量是不可变的，因此我们并不能在&lt;code&gt;init&lt;/code&gt;函数中修改断点条目。通常可以通过使用&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable&quot;&gt;&lt;code&gt;static mut&lt;/code&gt;&lt;/a&gt;解决此问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static mut IDT: InterruptDescriptorTable = InterruptDescriptorTable::new();

pub fn init_idt() {
    unsafe {
        IDT.breakpoint.set_handler_fn(breakpoint_handler);
        IDT.load();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然该方法可以通过编译，但这一用法极为罕见。因为&lt;code&gt;static mut&lt;/code&gt;变量非常容易发生数据竞争，导致我们只能在&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/second-edition/ch19-01-unsafe-rust.html#unsafe-superpowers&quot;&gt;&lt;code&gt;unsafe&lt;/code&gt;块&lt;/a&gt;中进行对该变量的访问。&lt;/p&gt;
&lt;h3&gt;使用惰性static救场&lt;/h3&gt;
&lt;p&gt;幸运的是我们有&lt;code&gt;lazy_static&lt;/code&gt;宏。相较于普通静态变量在编译时求值，该宏可以让静态变量在第一次使用时才执行初始化。因此，我们几乎可以在该宏的初始化块中执行任何操作，甚至可以读取运行时的值。&lt;/p&gt;
&lt;p&gt;在&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/#lazy-statics&quot;&gt;为VGA文本缓冲区创建抽象&lt;/a&gt;时，我们就导入&lt;code&gt;lazy_static&lt;/code&gt;crate了，这里可以直接使用&lt;code&gt;lazy_static!&lt;/code&gt;宏来创建惰性静态IDT：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use lazy_static::lazy_static;

lazy_static! {
    static ref IDT: InterruptDescriptorTable = {
        let mut idt = InterruptDescriptorTable::new();
        idt.breakpoint.set_handler_fn(breakpoint_handler);
        idt
    };
}

pub fn init_idt() {
    IDT.load();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;留意此处是如何解决使用&lt;code&gt;unsafe&lt;/code&gt;块问题的。&lt;code&gt;lazy_static!&lt;/code&gt;宏实际上在后台也使用了&lt;code&gt;unsafe&lt;/code&gt;块，只是将其封装抽象成了安全接口对外提供。&lt;/p&gt;
&lt;h3&gt;运行起来&lt;/h3&gt;
&lt;p&gt;让异常处理在内核中起作用的最后一步是在&lt;code&gt;main.rs&lt;/code&gt;中调用&lt;code&gt;init_idt&lt;/code&gt;函数。这里我们不选择直接调用该函数，而选择在&lt;code&gt;lib.rs&lt;/code&gt;中引入了一个通用的总初始化函数&lt;code&gt;init&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn init() {
    interrupts::init_idt();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有了这个函数，我们便可以将初始化例程中各种操作放置于此，这样就能够在&lt;code&gt;main.rs&lt;/code&gt;、&lt;code&gt;lib.rs&lt;/code&gt;和集成测试中的不同&lt;code&gt;_start&lt;/code&gt;函数之间共享这些例程。&lt;/p&gt;
&lt;p&gt;现在，我们可以更新&lt;code&gt;main.rs&lt;/code&gt;的&lt;code&gt;_start&lt;/code&gt;函数以调用&lt;code&gt;init&lt;/code&gt;，然后触发断点异常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    println!(&quot;Hello World{}&quot;, &quot;!&quot;);

    blog_os::init(); // new

    // invoke a breakpoint exception
    x86_64::instructions::interrupts::int3(); // new

    // as before
    #[cfg(test)]
    test_main();

    println!(&quot;It did not crash!&quot;);
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们在QEMU中运行它（使用&lt;code&gt;cargo run&lt;/code&gt;）时，将看到以下内容：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/cpu-exceptions/qemu-breakpoint-exception.png&quot; alt=&quot;QEMU断点异常&quot; /&gt;&lt;/p&gt;
&lt;p&gt;成功了！CPU成功调用了我们的断点处理程序，该断点处理程序将打印消息，然后返回到&lt;code&gt;_start&lt;/code&gt;函数，继续打印出&lt;code&gt;It did not crash!&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我们看到，发生异常时，中断栈帧会告诉我们指令指针和栈指针。在调试未预期的异常时，此信息将非常有用。&lt;/p&gt;
&lt;h3&gt;添加测试&lt;/h3&gt;
&lt;p&gt;让我们创建一个测试，以确保上述操作持续有效。首先，我们更新&lt;code&gt;_start&lt;/code&gt;函数以也调用&lt;code&gt;init&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// Entry point for `cargo test`
#[cfg(test)]
#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    init();      // new
    test_main();
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请记住，运行&lt;code&gt;cargo test --lib&lt;/code&gt;时才会使用此&lt;code&gt;_start&lt;/code&gt;函数，因为Rust的&lt;code&gt;lib.rs&lt;/code&gt;测试完全独立于&lt;code&gt;main.rs&lt;/code&gt;。在运行测试之前，我们需要在此处调用&lt;code&gt;init&lt;/code&gt;来设置IDT。&lt;/p&gt;
&lt;p&gt;现在我们可以创建一个&lt;code&gt;test_breakpoint_exception&lt;/code&gt;测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test_case]
fn test_breakpoint_exception() {
    // invoke a breakpoint exception
    x86_64::instructions::interrupts::int3();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该测试调用&lt;code&gt;int3&lt;/code&gt;函数来触发断点异常。通过检查之后执行是否继续，我们可以验证断点处理程序是否正常运行。&lt;/p&gt;
&lt;p&gt;你可以通过运行&lt;code&gt;cargo test&lt;/code&gt;（执行所有测试）或&lt;code&gt;cargo test --lib&lt;/code&gt;（仅执行&lt;code&gt;lib.rs&lt;/code&gt;及其模块测试）来尝试此新测试。在输出中应该看到以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;blog_os::interrupts::test_breakpoint_exception...	[ok]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用太多奇妙魔法了吗？&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定和&lt;a href=&quot;https://docs.rs/x86_64/0.13.2/x86_64/structures/idt/struct.InterruptDescriptorTable.html&quot;&gt;&lt;code&gt;InterruptDescriptorTable&lt;/code&gt;&lt;/a&gt;类型使异常处理过程相对简单明了。如果这对你来说太神奇了，而你又希望了解异常处理的所有细节，可以继续阅读我们的&lt;a href=&quot;https://os.phil-opp.com/edition-1/extra/naked-exceptions/&quot;&gt;“使用裸函数处理异常”&lt;/a&gt;系列文章，这些文章展示了如何在不使用&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定的情况下处理异常，文章还创建了自己的IDT类型。从现在看来，这些旧文章介绍了&lt;code&gt;x86-interrupt&lt;/code&gt;调用约定和&lt;code&gt;x86_64&lt;/code&gt;crate出现之前的主要异常处理方法。请注意，这些帖子基于此博客的&lt;a href=&quot;https://os.phil-opp.com/edition-1/&quot;&gt;第一版&lt;/a&gt;，可能已过时。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;我们已经成功捕获了第一个异常并从中返回了！下一步是确保你能跟捕获所有异常，因为未捕获的异常会导致致命的&lt;a href=&quot;https://wiki.osdev.org/Triple_Fault&quot;&gt;三重故障&lt;/a&gt;，从而导致系统重置。下一篇文章将要解释如何通过正确捕获&lt;a href=&quot;https://wiki.osdev.org/Double_Fault#Double_Fault&quot;&gt;双重故障&lt;/a&gt;来避免这种情况的发生。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 1.3 - VGA文本模式</title><link>https://zlotus.github.io/2021/02/28/writing-an-os-in-rust-1.3/</link><guid isPermaLink="true">https://zlotus.github.io/2021/02/28/writing-an-os-in-rust-1.3/</guid><description>使用Rust编写操作系统 - 1.3 - VGA文本模式</description><pubDate>Sun, 28 Feb 2021 21:57:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/&quot;&gt;VGA Text Mode&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/VGA-compatible_text_mode&quot;&gt;VGA文本模式&lt;/a&gt;是一种简单的将文本打印到屏幕上的方法。在这篇文章中，我们将创建一个接口，通过将所有的非安全代码封装在一个单独的模块中，使其使用变得安全和简单。我们还将实现对Rust中&lt;a href=&quot;https://doc.rust-lang.org/std/fmt/#related-macros&quot;&gt;格式化宏&lt;/a&gt;的支持。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/vga-text-mode/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-03&quot;&gt;post-03&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;VGA文本缓冲区&lt;/h2&gt;
&lt;p&gt;在VGA文本模式下，要想把一个字符打印到屏幕上，则需要把它写入VGA硬件的文本缓冲区。VGA文本缓冲区是一个二维数组，通常有25行80列，将直接渲染在屏幕上。每个数组元素通过以下格式描述一个屏幕字符：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;位&lt;/th&gt;
&lt;th&gt;值&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0-7&lt;/td&gt;
&lt;td&gt;ASCII 码点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8-11&lt;/td&gt;
&lt;td&gt;前景色&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12-14&lt;/td&gt;
&lt;td&gt;背景色&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;闪烁&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第一个字节代表应该用&lt;a href=&quot;https://en.wikipedia.org/wiki/ASCII&quot;&gt;ASCII编码&lt;/a&gt;打印的字符。准确地说，它并不完全是ASCII码，而是一个名为&lt;a href=&quot;https://en.wikipedia.org/wiki/Code_page_437&quot;&gt;代码页437&lt;/a&gt;的字符集，并添加了一些额外的字符和轻微的修改。为了简单起见，我们在这篇文章中继续称它为ASCII字符。&lt;/p&gt;
&lt;p&gt;第二个字节定义了字符的显示方式。前四位定义前景色，后三位定义背景色，最后一位定义字符是否应该闪烁。以下是可用的颜色：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;代码&lt;/th&gt;
&lt;th&gt;颜色&lt;/th&gt;
&lt;th&gt;代码 + 高亮位&lt;/th&gt;
&lt;th&gt;高亮色&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0x0&lt;/td&gt;
&lt;td&gt;Black&lt;/td&gt;
&lt;td&gt;0x8&lt;/td&gt;
&lt;td&gt;Dark Gray&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x1&lt;/td&gt;
&lt;td&gt;Blue&lt;/td&gt;
&lt;td&gt;0x9&lt;/td&gt;
&lt;td&gt;Light Blue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x2&lt;/td&gt;
&lt;td&gt;Green&lt;/td&gt;
&lt;td&gt;0xa&lt;/td&gt;
&lt;td&gt;Light Green&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x3&lt;/td&gt;
&lt;td&gt;Cyan&lt;/td&gt;
&lt;td&gt;0xb&lt;/td&gt;
&lt;td&gt;Light Cyan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x4&lt;/td&gt;
&lt;td&gt;Red&lt;/td&gt;
&lt;td&gt;0xc&lt;/td&gt;
&lt;td&gt;Light Red&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x5&lt;/td&gt;
&lt;td&gt;Magenta&lt;/td&gt;
&lt;td&gt;0xd&lt;/td&gt;
&lt;td&gt;Pink&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x6&lt;/td&gt;
&lt;td&gt;Brown&lt;/td&gt;
&lt;td&gt;0xe&lt;/td&gt;
&lt;td&gt;Yellow&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x7&lt;/td&gt;
&lt;td&gt;Light Gray&lt;/td&gt;
&lt;td&gt;0xf&lt;/td&gt;
&lt;td&gt;White&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第4位是&lt;em&gt;高亮位&lt;/em&gt;，例如它能将蓝色高亮变成了浅蓝色。对于背景色，该位被重新用作闪烁位。&lt;/p&gt;
&lt;p&gt;VGA文本缓冲区可以通过&lt;a href=&quot;https://en.wikipedia.org/wiki/Memory-mapped_I/O&quot;&gt;内存映射I/O&lt;/a&gt;访问地址&lt;code&gt;0xb8000&lt;/code&gt;。这意味着对该地址的读写不访问RAM，而是直接访问VGA硬件上的文本缓冲区。这意味着我们可以通过正常的内存操作对该地址进行读写。&lt;/p&gt;
&lt;p&gt;需要注意的是，内存映射的硬件可能不支持所有正常的RAM操作。例如，一个设备可能只支持按字节读取，当读取一个&lt;code&gt;u64&lt;/code&gt;时，就会返回垃圾。幸运的是，文本缓冲区支持&lt;a href=&quot;https://web.stanford.edu/class/cs140/projects/pintos/specs/freevga/vga/vgamem.htm#manip&quot;&gt;正常读写&lt;/a&gt;，所以我们不必以特殊的方式对待它。&lt;/p&gt;
&lt;h2&gt;编写Rust模块&lt;/h2&gt;
&lt;p&gt;现在我们知道了VGA缓冲区的工作原理，我们可以创建一个Rust模块来处理打印。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mod vga_buffer;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们创建一个新文件&lt;code&gt;src/vga_buffer.rs&lt;/code&gt;来编写这个模块。下面所有的代码都会在我们的新模块中编写（除非另有说明）。&lt;/p&gt;
&lt;h3&gt;颜色&lt;/h3&gt;
&lt;p&gt;首先，我们用一个枚举来表示不同的颜色：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
    Black = 0,
    Blue = 1,
    Green = 2,
    Cyan = 3,
    Red = 4,
    Magenta = 5,
    Brown = 6,
    LightGray = 7,
    DarkGray = 8,
    LightBlue = 9,
    LightGreen = 10,
    LightCyan = 11,
    LightRed = 12,
    Pink = 13,
    Yellow = 14,
    White = 15,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在这里使用一个&lt;a href=&quot;https://doc.rust-lang.org/rust-by-example/custom_types/enum/c_like.html&quot;&gt;C型枚举&lt;/a&gt;来显式地指定每种颜色的值。由于repr(u8)属性，每个枚举变量都存储为&lt;code&gt;u8&lt;/code&gt;。其实4位就够了，但Rust没有&lt;code&gt;u4&lt;/code&gt;类型。&lt;/p&gt;
&lt;p&gt;通常编译器会对每个未使用的变量发出警告。通过使用&lt;code&gt;#[allow(dead_code)]&lt;/code&gt;属性，我们可以禁用&lt;code&gt;Color&lt;/code&gt;枚举的这些警告。&lt;/p&gt;
&lt;p&gt;通过&lt;a href=&quot;https://doc.rust-lang.org/rust-by-example/trait/derive.html&quot;&gt;派生&lt;/a&gt;&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html&quot;&gt;&lt;code&gt;Copy&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/clone/trait.Clone.html&quot;&gt;&lt;code&gt;Clone&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/fmt/trait.Debug.html&quot;&gt;&lt;code&gt;Debug&lt;/code&gt;&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/cmp/trait.PartialEq.html&quot;&gt;&lt;code&gt;PartialEq&lt;/code&gt;&lt;/a&gt;和&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/cmp/trait.Eq.html&quot;&gt;&lt;code&gt;Eq&lt;/code&gt;&lt;/a&gt;五个trait，我们实现了类型的&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/first-edition/ownership.html#copy-types&quot;&gt;复制语义&lt;/a&gt;，并使其可打印、可比较。&lt;/p&gt;
&lt;p&gt;为了表示指定前景色和背景色的全部色码，我们在&lt;code&gt;u8&lt;/code&gt;的基础上创建一个&lt;a href=&quot;https://doc.rust-lang.org/rust-by-example/generics/new_types.html&quot;&gt;新类型&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);

impl ColorCode {
    fn new(foreground: Color, background: Color) -&amp;gt; ColorCode {
        ColorCode((background as u8) &amp;lt;&amp;lt; 4 | (foreground as u8))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ColorCode结构体包含完整的颜色字节——前景色和背景色。像之前一样，我们为它派生出&lt;code&gt;Copy&lt;/code&gt;和&lt;code&gt;Debug&lt;/code&gt;特征。为了确保&lt;code&gt;ColorCode&lt;/code&gt;的数据类型布局与&lt;code&gt;u8&lt;/code&gt;完全相同，我们使用&lt;a href=&quot;https://doc.rust-lang.org/nomicon/other-reprs.html#reprtransparent&quot;&gt;&lt;code&gt;repr(transparent)&lt;/code&gt;&lt;/a&gt;属性。&lt;/p&gt;
&lt;p&gt;（译者注：关于&lt;code&gt;repr(transparent)&lt;/code&gt;，可以参考&lt;a href=&quot;https://doc.rust-lang.org/1.26.2/unstable-book/language-features/repr-transparent.html&quot;&gt;&lt;code&gt;repr_transparent&lt;/code&gt;&lt;/a&gt;的解释。）&lt;/p&gt;
&lt;h3&gt;文本缓冲区&lt;/h3&gt;
&lt;p&gt;现在我们可以添加结构体来表示屏幕字符和文本缓冲区了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
    ascii_character: u8,
    color_code: ColorCode,
}

const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;

#[repr(transparent)]
struct Buffer {
    chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于Rust默认布局下结构体中的字段没有顺序，所以我们需要&lt;a href=&quot;https://doc.rust-lang.org/nightly/nomicon/other-reprs.html#reprc&quot;&gt;&lt;code&gt;repr(C)&lt;/code&gt;&lt;/a&gt;属性。它可以保证Rust结构体的字段布局和C结构体中的字段完全一样，从而保证字段排序的正确性。对于Buffer结构，我们再次使用&lt;code&gt;repr(transparent)&lt;/code&gt;来保证它的内存布局与其中的单字段相同。&lt;/p&gt;
&lt;p&gt;为了实际写到屏幕上，我们现在创建一个写类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct Writer {
    column_position: usize,
    color_code: ColorCode,
    buffer: &amp;amp;&apos;static mut Buffer,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写的方式总是写到最后一行，当一行满了的时候（或者遇到&lt;code&gt;\n&lt;/code&gt;），就会把行数往上移。&lt;code&gt;column_position&lt;/code&gt;字段会跟踪最后一行的实时位置；&lt;code&gt;color_code&lt;/code&gt;指定当前的前景色和背景色；&lt;code&gt;buffer&lt;/code&gt;中存储一个VGA缓冲区的引用。需要注意的是，我们在这里需要指定一个&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#lifetime-annotation-syntax&quot;&gt;显式生命周期&lt;/a&gt;来告诉编译器这个引用的有效期是多久。&lt;code&gt;&apos;static&lt;/code&gt;生命周期指定了引用在整个程序运行时间内都是有效的（这对VGA文本缓冲区来说是事实）。&lt;/p&gt;
&lt;h3&gt;打印&lt;/h3&gt;
&lt;p&gt;现在我们可以使用&lt;code&gt;Writer&lt;/code&gt;来修改缓冲区的字符。首先我们创建一个方法来写入一个ASCII字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Writer {
    pub fn write_byte(&amp;amp;mut self, byte: u8) {
        match byte {
            b&apos;\n&apos; =&amp;gt; self.new_line(),
            byte =&amp;gt; {
                if self.column_position &amp;gt;= BUFFER_WIDTH {
                    self.new_line();
                }

                let row = BUFFER_HEIGHT - 1;
                let col = self.column_position;

                let color_code = self.color_code;
                self.buffer.chars[row][col] = ScreenChar {
                    ascii_character: byte,
                    color_code,
                };
                self.column_position += 1;
            }
        }
    }

    fn new_line(&amp;amp;mut self) {/* TODO */}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这个字节是&lt;a href=&quot;https://en.wikipedia.org/wiki/Newline&quot;&gt;换行&lt;/a&gt;字节&lt;code&gt;\n&lt;/code&gt;，那么writer就不会打印任何内容，而是调用&lt;code&gt;new_line&lt;/code&gt;方法，这个方法我们将在后面实现。在第二种匹配情况下，其他字节会被打印到屏幕上。&lt;/p&gt;
&lt;p&gt;在打印字节时，writer会检查当前的行是否已满。若当前行已满，需要先调用&lt;code&gt;new_line&lt;/code&gt;来结束这一行。然后，它将一个新的&lt;code&gt;ScreenChar&lt;/code&gt;写入当前位置的缓冲区。最后，将当前列的位置前进一个字符。&lt;/p&gt;
&lt;p&gt;要打印整个字符串，我们可以将它们转换成字节，然后逐一打印：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Writer {
    pub fn write_string(&amp;amp;mut self, s: &amp;amp;str) {
        for byte in s.bytes() {
            match byte {
                // 若属于可打印的ASCII字符或换行符，则打印
                0x20..=0x7e | b&apos;\n&apos; =&amp;gt; self.write_byte(byte),
                // 若不属于可打印的ASCII字符，则打印0xfe
                _ =&amp;gt; self.write_byte(0xfe),
            }

        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VGA文本缓冲区只支持ASCII字符和&lt;a href=&quot;https://en.wikipedia.org/wiki/Code_page_437&quot;&gt;代码页437&lt;/a&gt;的附加字符。Rust字符串默认为&lt;code&gt;UTF-8&lt;/code&gt;，所以它们可能包含VGA文本缓冲区不支持的字节。我们使用匹配来区分可打印的ASCII字节（换行或空格符和&lt;code&gt;~&lt;/code&gt;字符之间的任何字符）和不可打印的字节。对于不可打印的字节，我们打印一个&lt;code&gt;■&lt;/code&gt;字符，它在VGA硬件上的十六进制代码为&lt;code&gt;0xfe&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;试试吧&lt;/h4&gt;
&lt;p&gt;要在屏幕上写一些字符，可以创建一个临时函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn print_something() {
    let mut writer = Writer {
        column_position: 0,
        color_code: ColorCode::new(Color::Yellow, Color::Black),
        buffer: unsafe { &amp;amp;mut *(0xb8000 as *mut Buffer) },
    };

    writer.write_byte(b&apos;H&apos;);
    writer.write_string(&quot;ello &quot;);
    writer.write_string(&quot;Wörld!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数首先创建一个新的Writer，指向位于&lt;code&gt;0xb8000&lt;/code&gt;的VGA缓冲区，这里的语法看起来可能有点奇怪。首先，我们把整数&lt;code&gt;0xb8000&lt;/code&gt;作为一个可变的&lt;a href=&quot;https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer&quot;&gt;裸指针&lt;/a&gt;。然后我们通过解引用（使用&lt;code&gt;*&lt;/code&gt;运算符）将其转换为可变引用，并立即再次借用（通过&lt;code&gt;&amp;amp;mut&lt;/code&gt;）。这种转换需要一个&lt;a href=&quot;https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html&quot;&gt;&lt;code&gt;unsafe&lt;/code&gt;块&lt;/a&gt;，因为编译器不能保证裸指针是有效的。&lt;/p&gt;
&lt;p&gt;然后它将字节&lt;code&gt;b&apos;H&apos;&lt;/code&gt;写入其中。&lt;code&gt;b&lt;/code&gt;前缀创建了一个&lt;a href=&quot;https://doc.rust-lang.org/reference/tokens.html#byte-literals&quot;&gt;文字字节&lt;/a&gt;，它代表一个ASCII字符。通过写入字符串&lt;code&gt;&quot;ello &quot;&lt;/code&gt;和&lt;code&gt;&quot;Wörld!&quot;&lt;/code&gt;，我们测试了我们的&lt;code&gt;write_string&lt;/code&gt;方法和对不可打印字符的处理。为了看到输出，我们需要从&lt;code&gt;_start&lt;/code&gt;函数中调用&lt;code&gt;print_something&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    vga_buffer::print_something();

    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在运行项目时，屏幕左下角应该会打印出一个黄色的&lt;code&gt;Hello W■■rld！&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/vga-text-mode/vga-hello.png&quot; alt=&quot;QEMU左下角中输出黄色的&amp;quot;Hello W■■rld!&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;注意到&lt;code&gt;ö&lt;/code&gt;被打印成两个&lt;code&gt;■&lt;/code&gt;字符。这是因为&lt;code&gt;ö&lt;/code&gt;在&lt;a href=&quot;https://www.fileformat.info/info/unicode/utf8.htm&quot;&gt;UTF-8&lt;/a&gt;中由两个字节表示，这两个字节都不属于可打印的ASCII范围。实际上这是UTF-8的一个基本属性：多字节值的单个字节永远不是有效的ASCII码。&lt;/p&gt;
&lt;h3&gt;易失性操作&lt;/h3&gt;
&lt;p&gt;我们刚刚看到信息被正确打印出来了。然而，它可能无法与未来更加积极优化的Rust编译器一起工作。&lt;/p&gt;
&lt;p&gt;问题在于，是我们只对&lt;code&gt;Buffer&lt;/code&gt;进行写入，而没有再从中读取。编译器不知道我们的确访问了VGA缓冲区内存（而不是正常内存），也不知道一些字符已经出现在屏幕上的额外效果。所以编译器可能决定这些写入是不必要的，可以省略。为了避免这种错误的优化，我们需要将这些写入指定为&lt;a href=&quot;https://en.wikipedia.org/wiki/Volatile_(computer_programming)&quot;&gt;易失的&lt;/a&gt;。这就告诉编译器，这个写有副作用，不应该被优化掉。&lt;/p&gt;
&lt;p&gt;为了对VGA缓冲区使用易失性写，我们使用&lt;code&gt;volatile&lt;/code&gt;库。这个crate（在Rust世界中，包是这样称呼的）提供了一个带有&lt;code&gt;read&lt;/code&gt;和&lt;code&gt;write&lt;/code&gt;方法的&lt;code&gt;Volatile&lt;/code&gt;包装类型。这些方法在内部使用了核心库的&lt;code&gt;read_volatile&lt;/code&gt;和&lt;code&gt;write_volatile&lt;/code&gt;函数，从而保证了读/写操作不会被优化掉。&lt;/p&gt;
&lt;p&gt;我们可以通过在&lt;code&gt;Cargo.toml&lt;/code&gt;的&lt;code&gt;dependencies&lt;/code&gt;部分添加一个对&lt;code&gt;volatile&lt;/code&gt;crate的依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
volatile = &quot;0.2.6&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确保指定&lt;code&gt;volatile&lt;/code&gt;版本为&lt;code&gt;0.2.6&lt;/code&gt;。新版本的crate不兼容本文。0.2.6是&lt;a href=&quot;https://semver.org/&quot;&gt;语义&lt;/a&gt;版本号。更多信息，请参见cargo文档的&lt;a href=&quot;https://doc.crates.io/specifying-dependencies.html&quot;&gt;指定依赖指南&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;让我们用它来进行VGA缓冲区的易失性写入，更新的Buffer类型如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use volatile::Volatile;

struct Buffer {
    chars: [[Volatile&amp;lt;ScreenChar&amp;gt;; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们现在使用&lt;code&gt;Volatile&amp;lt;ScreenChar&amp;gt;&lt;/code&gt;来代替&lt;code&gt;ScreenChar&lt;/code&gt;，（&lt;code&gt;Volatile&lt;/code&gt;是&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-01-syntax.html&quot;&gt;泛型&lt;/a&gt;，几乎可以包装任何类型）。这确保了我们不能通过“普通”的写方法（译者注：如赋值）意外地写入到它。相反，我们现在必须调用&lt;code&gt;write&lt;/code&gt;方法。&lt;/p&gt;
&lt;p&gt;这意味着我们必须更新&lt;code&gt;Writer::write_byte&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Writer {
    pub fn write_byte(&amp;amp;mut self, byte: u8) {
        match byte {
            b&apos;\n&apos; =&amp;gt; self.new_line(),
            byte =&amp;gt; {
                ...

                self.buffer.chars[row][col].write(ScreenChar {
                    ascii_character: byte,
                    color_code,
                });
                ...
            }
        }
    }
    ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们现在使用的是&lt;code&gt;write&lt;/code&gt;方法，而不是使用&lt;code&gt;=&lt;/code&gt;的普通赋值。这样可以保证编译器永远不会优化掉这个写操作。&lt;/p&gt;
&lt;h3&gt;格式化宏&lt;/h3&gt;
&lt;p&gt;如果能支持Rust的格式化宏就更好了。这样一来，我们就可以轻松地打印不同的类型，如整数或浮点数。接下来，我们需要实现&lt;code&gt;core::fmt::Write&lt;/code&gt;trait。这个trait唯一需要实现方法是&lt;code&gt;write_str&lt;/code&gt;，它看起来和我们的&lt;code&gt;write_string&lt;/code&gt;方法很相似，只不过返回类型为&lt;code&gt;fmt::Result&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::fmt;

impl fmt::Write for Writer {
    fn write_str(&amp;amp;mut self, s: &amp;amp;str) -&amp;gt; fmt::Result {
        self.write_string(s);
        Ok(())
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Ok(())&lt;/code&gt;只是一个包含&lt;code&gt;()&lt;/code&gt;类型的&lt;code&gt;Ok&lt;/code&gt; Result。&lt;/p&gt;
&lt;p&gt;现在我们可以使用Rust内置的&lt;code&gt;write!&lt;/code&gt;/&lt;code&gt;writeln!&lt;/code&gt;格式化宏了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn print_something() {
    use core::fmt::Write;
    let mut writer = Writer {
        column_position: 0,
        color_code: ColorCode::new(Color::Yellow, Color::Black),
        buffer: unsafe { &amp;amp;mut *(0xb8000 as *mut Buffer) },
    };

    writer.write_byte(b&apos;H&apos;);
    writer.write_string(&quot;ello! &quot;);
    write!(writer, &quot;The numbers are {} and {}&quot;, 42, 1.0/3.0).unwrap();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在你应该在屏幕底部看到&lt;code&gt;Hello! The numbers are 42 and 0.3333333333333333&lt;/code&gt;。&lt;code&gt;write!&lt;/code&gt;会返回一个&lt;code&gt;Result&lt;/code&gt;，如果不使用会引起警告，所以我们在上面调用&lt;a href=&quot;https://doc.rust-lang.org/core/result/enum.Result.html#method.unwrap&quot;&gt;&lt;code&gt;unwrap&lt;/code&gt;&lt;/a&gt;函数，如果发生错误，它会panic。现阶段并没有这种问题，因为对VGA缓冲区的写入不会失败。&lt;/p&gt;
&lt;h3&gt;换行&lt;/h3&gt;
&lt;p&gt;截至上一节，我们都忽略了换行或字符超出一行容量的情况。遇到这种情况时，我们希望将每个字符向上移动一行（最上面的一行被删除），然后从最后一行的行首重新开始。要做到这一点，我们为&lt;code&gt;Writer&lt;/code&gt;的&lt;code&gt;new_line&lt;/code&gt;方法添加一个实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Writer {
    fn new_line(&amp;amp;mut self) {
        for row in 1..BUFFER_HEIGHT {
            for col in 0..BUFFER_WIDTH {
                let character = self.buffer.chars[row][col].read();
                self.buffer.chars[row - 1][col].write(character);
            }
        }
        self.clear_row(BUFFER_HEIGHT - 1);
        self.column_position = 0;
    }

    fn clear_row(&amp;amp;mut self, row: usize) {/* TODO */}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们对所有屏幕字符进行迭代，并将每个字符向上移动一行。请注意，区间范围(&lt;code&gt;..&lt;/code&gt;)是前开后闭的。此外，我们也不迭代第&lt;code&gt;0&lt;/code&gt;行（第一个循环从&lt;code&gt;1&lt;/code&gt;开始），因为它是被移出屏幕的那一行。&lt;/p&gt;
&lt;p&gt;为了完成换行代码，我们再添加&lt;code&gt;clear_row&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impl Writer {
    fn clear_row(&amp;amp;mut self, row: usize) {
        let blank = ScreenChar {
            ascii_character: b&apos; &apos;,
            color_code: self.color_code,
        };
        for col in 0..BUFFER_WIDTH {
            self.buffer.chars[row][col].write(blank);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个方法通过用空格字符覆盖所有字符来清除一行。&lt;/p&gt;
&lt;h2&gt;全局接口&lt;/h2&gt;
&lt;p&gt;提供一个全局&lt;code&gt;Writer&lt;/code&gt;作为接口，可以使其他模块免去自己实例化&lt;code&gt;Writer&lt;/code&gt;的麻烦，我们试着创建一个静态变量&lt;code&gt;WRITER&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub static WRITER: Writer = Writer {
    column_position: 0,
    color_code: ColorCode::new(Color::Yellow, Color::Black),
    buffer: unsafe { &amp;amp;mut *(0xb8000 as *mut Buffer) },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，如果我们现在尝试编译它，会出现以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
 --&amp;gt; src/vga_buffer.rs:7:17
  |
7 |     color_code: ColorCode::new(Color::Yellow, Color::Black),
  |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0396]: raw pointers cannot be dereferenced in statics
 --&amp;gt; src/vga_buffer.rs:8:22
  |
8 |     buffer: unsafe { &amp;amp;mut *(0xb8000 as *mut Buffer) },
  |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant

error[E0017]: references in statics may only refer to immutable values
 --&amp;gt; src/vga_buffer.rs:8:22
  |
8 |     buffer: unsafe { &amp;amp;mut *(0xb8000 as *mut Buffer) },
  |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values

error[E0017]: references in statics may only refer to immutable values
 --&amp;gt; src/vga_buffer.rs:8:13
  |
8 |     buffer: unsafe { &amp;amp;mut *(0xb8000 as *mut Buffer) },
  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了理解这里发生的事情，我们需要知道，与普通变量在运行时初始化不同的是，静态变量是在编译时初始化的。Rust编译器中对此类初始化表达式进行求值的组件叫做 “&lt;a href=&quot;https://rustc-dev-guide.rust-lang.org/const-eval.html&quot;&gt;常量求值器&lt;/a&gt;”。虽然它的功能现在还仍然有限，不过对其功能的扩展工作也在进行中，例如RFC“&lt;a href=&quot;https://github.com/rust-lang/rfcs/pull/2345&quot;&gt;允许常量中的panic&lt;/a&gt;”。&lt;/p&gt;
&lt;p&gt;关于&lt;code&gt;ColorCode::new&lt;/code&gt;的问题可以利用&lt;a href=&quot;https://doc.rust-lang.org/unstable-book/language-features/const-fn.html&quot;&gt;&lt;code&gt;const&lt;/code&gt;函数&lt;/a&gt;来解决，但这里的根本问题是Rust的常量求值器无法在编译时将原始指针转换为引用。也许未来的某一天将会支持该功能，但在那之前，我们必须找到另一个解决方案。&lt;/p&gt;
&lt;h3&gt;惰性静态变量&lt;/h3&gt;
&lt;p&gt;用非常函数一次性初始化静态变量是Rust中常见的问题。幸运的是，在一个名为&lt;a href=&quot;https://docs.rs/lazy_static/1.0.1/lazy_static/&quot;&gt;lazy_static&lt;/a&gt;的crate中已经存在一个很好的解决方案。这个crate提供了一个&lt;code&gt;lazy_static!&lt;/code&gt;宏，定义了一个惰性初始化的&lt;code&gt;static&lt;/code&gt;。&lt;code&gt;static&lt;/code&gt;在编译时不计算其的值，而是在第一次被访问时进行惰性初始化。因此，初始化发生在运行时，这使得各种复杂初始化代码成为可能。&lt;/p&gt;
&lt;p&gt;让我们在项目中添加&lt;code&gt;lazy_static&lt;/code&gt;crate。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies.lazy_static]
version = &quot;1.0&quot;
features = [&quot;spin_no_std&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们需要&lt;code&gt;spin_no_std&lt;/code&gt;特性，因为我们并没有链接标准库。&lt;/p&gt;
&lt;p&gt;有了&lt;code&gt;lazy_static&lt;/code&gt;，我们便可以定义静态&lt;code&gt;WRITER&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use lazy_static::lazy_static;

lazy_static! {
    pub static ref WRITER: Writer = Writer {
        column_position: 0,
        color_code: ColorCode::new(Color::Yellow, Color::Black),
        buffer: unsafe { &amp;amp;mut *(0xb8000 as *mut Buffer) },
    };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而，这个&lt;code&gt;WRITER&lt;/code&gt;并没有什么用，因为它是不可变的，这意味着我们不能向它写任何东西（因为所有的写方法都需要获取&lt;code&gt;&amp;amp;mut self&lt;/code&gt;）。一个可能的解决方案是使用一个&lt;a href=&quot;https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html#accessing-or-modifying-a-mutable-static-variable&quot;&gt;可变静态变量&lt;/a&gt;。但是这样一来，对它的每一次读写都将是不安全的，因为这样很容易引入数据竞争和其他不好的东西。使用&lt;code&gt;static mut&lt;/code&gt;是非常不推荐的，以至于有人提议&lt;a href=&quot;https://internals.rust-lang.org/t/pre-rfc-remove-static-mut/1437&quot;&gt;移除&lt;/a&gt;这一特性。那么，有没有什么替代方案呢？我们可以尝试使用一个不可变的静态变量加上诸如&lt;a href=&quot;https://doc.rust-lang.org/book/ch15-05-interior-mutability.html#keeping-track-of-borrows-at-runtime-with-refcellt&quot;&gt;&lt;code&gt;RefCell&lt;/code&gt;&lt;/a&gt;甚至&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/cell/struct.UnsafeCell.html&quot;&gt;&lt;code&gt;UnsafeCell&lt;/code&gt;&lt;/a&gt;，以提供内部可突变性。但是这些类型不具有&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/marker/trait.Sync.html&quot;&gt;&lt;code&gt;Sync&lt;/code&gt;&lt;/a&gt;特性（有充分的理由），所以我们不能在静态变量中使用它们。&lt;/p&gt;
&lt;h3&gt;自旋锁&lt;/h3&gt;
&lt;p&gt;为了获得同步的（即具有&lt;code&gt;Sync&lt;/code&gt;trait的）内部可变性，标准库的用户可以使用互斥锁&lt;a href=&quot;https://doc.rust-lang.org/nightly/std/sync/struct.Mutex.html&quot;&gt;&lt;code&gt;Mutex&lt;/code&gt;&lt;/a&gt;。它在资源已经被锁定的情况下，通过阻塞线程来提供线程间互斥。但是我们的基本内核没有任何阻塞支持，甚至没有线程的概念，所以我们也无法使用它。然而在计算机科学中，有一种非常基础的互斥，它不需要操作系统的功能：&lt;a href=&quot;https://en.wikipedia.org/wiki/Spinlock&quot;&gt;自旋锁&lt;/a&gt;。线程不进行阻塞，而只是在一个循环中不停的尝试锁定它，从而消耗CPU时间，直到锁再次释放。&lt;/p&gt;
&lt;p&gt;要使自旋锁，我们需要添加&lt;a href=&quot;https://crates.io/crates/spin&quot;&gt;spin&lt;/a&gt; crate作为依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
spin = &quot;0.5.2&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是，我们可以使用自旋锁来为我们的静态WRITER添加安全的&lt;a href=&quot;https://doc.rust-lang.org/book/ch15-05-interior-mutability.html&quot;&gt;内部可变性&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use spin::Mutex;
...
lazy_static! {
    pub static ref WRITER: Mutex&amp;lt;Writer&amp;gt; = Mutex::new(Writer {
        column_position: 0,
        color_code: ColorCode::new(Color::Yellow, Color::Black),
        buffer: unsafe { &amp;amp;mut *(0xb8000 as *mut Buffer) },
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们可以删除临时函数&lt;code&gt;print_something&lt;/code&gt;，直接从&lt;code&gt;_start&lt;/code&gt;函数中打印：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    use core::fmt::Write;
    vga_buffer::WRITER.lock().write_str(&quot;Hello again&quot;).unwrap();
    write!(vga_buffer::WRITER.lock(), &quot;, some numbers: {} {}&quot;, 42, 1.337).unwrap();

    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们需要导入&lt;code&gt;fmt::Write&lt;/code&gt;trait来使用它的功能。&lt;/p&gt;
&lt;h3&gt;安全性&lt;/h3&gt;
&lt;p&gt;请注意，在我们的代码中，我们只有一个不安全的块，即需要创建一个指向&lt;code&gt;0xb8000&lt;/code&gt;的裸指针作为&lt;code&gt;Buffer&lt;/code&gt;并进行可变引用。之后，所有的操作都是安全的。Rust默认对数组访问使用边界检查，所以我们不能意外地写到缓冲区之外。因此，我们在类型系统中对所需的条件进行了编码，并且能够向外部提供一个安全的接口。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;println&lt;/code&gt;打印宏&lt;/h3&gt;
&lt;p&gt;现在我们有了一个全局writer，可以再添加一个&lt;code&gt;println&lt;/code&gt;宏，使其可以在代码库的任何地方使用。Rust的&lt;a href=&quot;https://doc.rust-lang.org/nightly/book/ch19-06-macros.html#declarative-macros-with-macro_rules-for-general-metaprogramming&quot;&gt;宏语法&lt;/a&gt;有点奇怪，所以我们不会尝试从头开始写一个宏。让我们先看看标准库中&lt;a href=&quot;https://doc.rust-lang.org/nightly/std/macro.println!.html&quot;&gt;&lt;code&gt;println!&lt;/code&gt;宏&lt;/a&gt;的源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[macro_export]
macro_rules! println {
    () =&amp;gt; (print!(&quot;\n&quot;));
    ($($arg:tt)*) =&amp;gt; (print!(&quot;{}\n&quot;, format_args!($($arg)*)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;宏是通过一个或多个规则来定义的，这些规则类似于&lt;code&gt;match&lt;/code&gt;的匹配分支。&lt;code&gt;println&lt;/code&gt;宏有两个规则：第一条规则是没有参数的调用（例如&lt;code&gt;println!()&lt;/code&gt;）, 它被扩展为&lt;code&gt;print!(&quot;\n&quot;)&lt;/code&gt;, 因此只是换行。第二条规则是有参数的调用，例如&lt;code&gt;println!(&quot;Hello&quot;)&lt;/code&gt;或&lt;code&gt;println!(&quot;Number: {}&quot;, 4)&lt;/code&gt;。它也是扩展为调用&lt;code&gt;print!&lt;/code&gt;宏，传递所有参数，并在结尾处附加一个换行符&lt;code&gt;\n&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;#[macro_export]&lt;/code&gt;属性使宏可以被整个crate（而不仅仅是其定义所在的模块）和外部crate所使用。它还将宏置于crate根，这意味着我们必须通过使用&lt;code&gt;std::println&lt;/code&gt;而不是&lt;code&gt;std::macros::println&lt;/code&gt;来进行导入。&lt;/p&gt;
&lt;p&gt;而&lt;code&gt;print!&lt;/code&gt;宏的定义为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[macro_export]
macro_rules! print {
    ($($arg:tt)*) =&amp;gt; ($crate::io::_print(format_args!($($arg)*)));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该宏扩展为调用&lt;code&gt;io&lt;/code&gt;模块中的&lt;code&gt;_print&lt;/code&gt;函数。&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/first-edition/macros.html#the-variable-crate&quot;&gt;&lt;code&gt;$crate&lt;/code&gt;变量&lt;/a&gt;保证了这个宏在其他模块中使用时，通过扩展到&lt;code&gt;std&lt;/code&gt;的方式，也能在&lt;code&gt;std&lt;/code&gt;模块之外工作。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/nightly/std/macro.format_args.html&quot;&gt;&lt;code&gt;format_args&lt;/code&gt;&lt;/a&gt;宏使用其中传递的参数中新建一个&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/fmt/struct.Arguments.html&quot;&gt;&lt;code&gt;fmt::Arguments&lt;/code&gt;&lt;/a&gt;类型，并传递给&lt;code&gt;_print&lt;/code&gt;。libstd的&lt;a href=&quot;https://github.com/rust-lang/rust/blob/29f5c699b11a6a148f097f82eaa05202f8799bbc/src/libstd/io/stdio.rs#L698&quot;&gt;&lt;code&gt;_print&lt;/code&gt;函数&lt;/a&gt;调用&lt;code&gt;print_to&lt;/code&gt;，这个函数相当复杂，因为它支持不同的&lt;code&gt;Stdout&lt;/code&gt;设备。我们并不需要那么复杂，因为我们只是想打印到VGA缓冲区。&lt;/p&gt;
&lt;p&gt;要打印到VGA缓冲区，我们只需复制&lt;code&gt;println!&lt;/code&gt;和&lt;code&gt;print!&lt;/code&gt;宏，并修改它们以使用我们自己的&lt;code&gt;_print&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[macro_export]
macro_rules! print {
    ($($arg:tt)*) =&amp;gt; ($crate::vga_buffer::_print(format_args!($($arg)*)));
}

#[macro_export]
macro_rules! println {
    () =&amp;gt; ($crate::print!(&quot;\n&quot;));
    ($($arg:tt)*) =&amp;gt; ($crate::print!(&quot;{}\n&quot;, format_args!($($arg)*)));
}

#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
    use core::fmt::Write;
    WRITER.lock().write_fmt(args).unwrap();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们对原来的&lt;code&gt;println&lt;/code&gt;定义做了一个改动，就是在调用&lt;code&gt;print!&lt;/code&gt;宏的时候也添加&lt;code&gt;$crate&lt;/code&gt;前缀。这确保了如果我们只想使用&lt;code&gt;println&lt;/code&gt;时，不需要再导入&lt;code&gt;print!&lt;/code&gt;宏。&lt;/p&gt;
&lt;p&gt;像标准库中的实现一样，我们为这两个宏都添加了&lt;code&gt;#[macro_export]&lt;/code&gt;属性，使它们在我们的crate中随处可用。请注意，此举将宏放在crate的根命名空间中，所以通过使用&lt;code&gt;crate::vga_buffer::println&lt;/code&gt;导入它们是行不通的。相反，我们必须使用&lt;code&gt;crate::println&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;_print&lt;/code&gt;函数获得静态变量&lt;code&gt;WRITER&lt;/code&gt;的锁，并对其调用&lt;code&gt;write_fmt&lt;/code&gt;方法。这个方法来自&lt;code&gt;Write&lt;/code&gt;trait，我们需要导入那个trait。如果打印不成功，结尾的&lt;code&gt;unwrap()&lt;/code&gt;就会panic。但是由于我们总是在&lt;code&gt;write_str&lt;/code&gt;中返回&lt;code&gt;Ok&lt;/code&gt;，所以这种情况应该不会发生。&lt;/p&gt;
&lt;p&gt;由于宏需要能够从模块外部调用&lt;code&gt;_print&lt;/code&gt;，所以该函数必须是公共的。但是，由于我们认为这是一个私有的实现细节，所以我们添加了&lt;a href=&quot;https://doc.rust-lang.org/nightly/rustdoc/the-doc-attribute.html#dochidden&quot;&gt;doc(hidden)属性&lt;/a&gt;，以在生成的文档中因此该函数的说明。&lt;/p&gt;
&lt;h3&gt;使用&lt;code&gt;println&lt;/code&gt;打印Hello World&lt;/h3&gt;
&lt;p&gt;现在我们可以在&lt;code&gt;_start&lt;/code&gt;函数中使用&lt;code&gt;println&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() {
    println!(&quot;Hello World{}&quot;, &quot;!&quot;);

    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意，我们不必在main函数中导入宏，因为它已经存在于根命名空间中。&lt;/p&gt;
&lt;p&gt;正如预期的那样，我们现在看到屏幕上出现了 &quot;Hello World!&quot;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/vga-text-mode/vga-hello-world.png&quot; alt=&quot;QEMU打印 “Hello World!”&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;打印panic信息&lt;/h3&gt;
&lt;p&gt;现在我们已经有了一个&lt;code&gt;println&lt;/code&gt;宏，我们可以在我们的panic函数中使用它来打印panic信息和panic的位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/// 函数在panic时被调用
#[panic_handler]
fn panic(info: &amp;amp;PanicInfo) -&amp;gt; ! {
    println!(&quot;{}&quot;, info);
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们现在在&lt;code&gt;_start&lt;/code&gt;函数中插入&lt;code&gt;panic!(&quot;Some panic message&quot;);&lt;/code&gt;时，就会得到以下输出：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/vga-text-mode/vga-panic.png&quot; alt=&quot;QEMU打印&amp;quot;panicked at &apos;Some panic message&apos;, src/main.rs:28:5&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;于是，我们不仅知道产生了panic，还知道panic信息以及它发生在代码的什么地方。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;在这篇文章中，我们了解了VGA文本缓冲区的结构，以及如何通过地址&lt;code&gt;0xb8000&lt;/code&gt;的内存映射进行写入。我们创建了一个Rust模块，封装了向这个内存映射缓冲区写入数据的非安全操作，并向外部提供了一个安全便捷的接口。&lt;/p&gt;
&lt;p&gt;我们也看到了添加第三方库依赖关系是多么的简单，这要感谢cargo工具。我们添加的两个依赖，&lt;code&gt;lazy_static&lt;/code&gt;和&lt;code&gt;spin&lt;/code&gt;，这两个库在操作系统开发中非常有用，我们会在以后的文章中将有更多的地方用到它们。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;下一篇文章将解释如何设置Rust的内置单元测试框架，然后我们将为这篇文章中的VGA缓冲模块创建一些基本的单元测试。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 1.2 - 最小化Rust内核</title><link>https://zlotus.github.io/2021/02/21/writing-an-os-in-rust-1.2/</link><guid isPermaLink="true">https://zlotus.github.io/2021/02/21/writing-an-os-in-rust-1.2/</guid><description>使用Rust编写操作系统 - 1.2 - 最小化Rust内核</description><pubDate>Sun, 21 Feb 2021 17:41:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/minimal-rust-kernel/&quot;&gt;A Minimal Rust Kernel&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;在这篇文章中，我们将为x86架构创建一个最小化的64位Rust内核。我们将在上一篇文章的&lt;a href=&quot;https://os.phil-opp.com/freestanding-rust-binary/&quot;&gt;独立Rust二进制程序&lt;/a&gt;基础上，创建一个可启动的磁盘映像，并在屏幕上打印一些东西。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/minimal-rust-kernel/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-02&quot;&gt;post-02&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;启动过程&lt;/h2&gt;
&lt;p&gt;当您打开计算机时，它开始执行存储在主板&lt;a href=&quot;https://en.wikipedia.org/wiki/Read-only_memory&quot;&gt;ROM&lt;/a&gt;中的固件代码。这段代码会执行&lt;a href=&quot;https://en.wikipedia.org/wiki/Power-on_self-test&quot;&gt;开机自检&lt;/a&gt;，检测可用的RAM，并对CPU和硬件进行预初始化。之后，它寻找一个可启动的磁盘，并开始启动操作系统内核。&lt;/p&gt;
&lt;p&gt;在x86上，有两种固件标准：&quot;基本输入/输出系统&quot;（&lt;a href=&quot;https://en.wikipedia.org/wiki/BIOS&quot;&gt;BIOS&lt;/a&gt;）和较新的 &quot;统一可扩展固件接口&quot;（&lt;a href=&quot;https://en.wikipedia.org/wiki/Unified_Extensible_Firmware_Interface&quot;&gt;UEFI&lt;/a&gt;）。BIOS标准虽然老旧过时，却较为简单，而且自20世纪80年代以来在任何x86机器上都得到了良好的支持。相比之下，UEFI更现代，功能更多，但设置起来更复杂（至少在我看来）。&lt;/p&gt;
&lt;p&gt;目前，我们只提供BIOS支持，但对UEFI的支持也在计划之中。如果你愿意帮助我们，请查看&lt;a href=&quot;https://github.com/phil-opp/blog_os/issues/349&quot;&gt;Github issue&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;BIOS启动&lt;/h3&gt;
&lt;p&gt;几乎所有的x86系统都支持BIOS启动，包括使用模拟BIOS的较新的基于UEFI的机器。这是很好的，因为你可以在上世纪的所有机器上使用相同的启动逻辑。但这种广泛的兼容性同时也是BIOS启动的最大缺点，因为这意味着CPU在启动前会被放入一个叫做&lt;a href=&quot;https://en.wikipedia.org/wiki/Real_mode&quot;&gt;实模式&lt;/a&gt;的16位兼容模式，这样上世纪80年代的老式引导程序仍然可以使用。&lt;/p&gt;
&lt;p&gt;让我们从头说起。&lt;/p&gt;
&lt;p&gt;当你打开电脑时，它会从位于主板上的一些特殊闪存中加载BIOS。BIOS会运行硬件的自我测试和初始化例程，然后寻找可启动磁盘。如果它找到了一个，控制权就会转移到它的bootloader，这是存储在磁盘开头的512字节的可执行代码部分。大多数的引导加载器都大于512字节，所以引导加载器通常被分成一个小的第一阶段引导程序——刚好512字节——和一个第二阶段引导程序——在第一阶段引导程序之后加载。&lt;/p&gt;
&lt;p&gt;引导程序必须确定内核映像在磁盘上的位置，并将其加载到内存中。它还需要先将CPU从16位的&lt;a href=&quot;https://en.wikipedia.org/wiki/Real_mode&quot;&gt;实模式&lt;/a&gt;切换到32位的&lt;a href=&quot;https://en.wikipedia.org/wiki/Protected_mode&quot;&gt;保护模式&lt;/a&gt;，然后再切换到64位的&lt;a href=&quot;https://en.wikipedia.org/wiki/Long_mode&quot;&gt;长模式&lt;/a&gt;，在长模式下可以使用64位寄存器和完整的主内存。它的j第三项工作是从BIOS中查询某些信息（如内存映射），并将其传递给操作系统内核。&lt;/p&gt;
&lt;p&gt;编写一个bootloader是有点麻烦的，因为它需要汇编语言和很多非深入的步骤，比如 &quot;把这个魔数值写入这个处理器寄存器&quot;。因此，我们在这篇文章中不涉及bootloader的创建，而是提供了一个名为&lt;a href=&quot;https://github.com/rust-osdev/bootimage&quot;&gt;bootimage&lt;/a&gt;的工具，它可以自动将bootloader预置到你的内核中。&lt;/p&gt;
&lt;p&gt;如果你对构建自己的bootloader感兴趣。请持续关注，一系列关于这个主题的文章已经在计划中了！&lt;/p&gt;
&lt;h3&gt;Multiboot标准&lt;/h3&gt;
&lt;p&gt;为了避免每个操作系统都实现自己的bootloader，而bootloader只能与单一操作系统兼容，&lt;a href=&quot;https://en.wikipedia.org/wiki/Free_Software_Foundation&quot;&gt;自由软件基金会&lt;/a&gt;在1995年创建了一个名为&lt;a href=&quot;https://wiki.osdev.org/Multiboot&quot;&gt;Multiboot&lt;/a&gt;的开放bootloader标准。该标准定义了引导加载器和操作系统之间的接口，因此任何符合Multiboot标准的引导加载器都可以加载任何符合Multiboot标准的操作系统。参考实现是&lt;a href=&quot;https://en.wikipedia.org/wiki/GNU_GRUB&quot;&gt;GNU GRUB&lt;/a&gt;，它是Linux系统中最流行的引导加载器。&lt;/p&gt;
&lt;p&gt;要使内核符合Multiboot的要求，只需要在内核文件的开头插入一个所谓的&lt;a href=&quot;https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#OS-image-format&quot;&gt;Multiboot头&lt;/a&gt;。这使得在GRUB中启动一个操作系统变得非常容易。然而，GRUB和Multiboot标准也有一些问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只支持32位保护模式。这意味着你仍然必须进行CPU配置才能切换到64位长模式。&lt;/li&gt;
&lt;li&gt;其设计是为了简化bootloader，而不是简化内核。例如，内核需要链接一个调整后的&lt;a href=&quot;https://wiki.osdev.org/Multiboot#Multiboot_2&quot;&gt;默认页长度&lt;/a&gt;，否则GRUB会找不到Multiboot头。另一个例子是，传递给内核的&lt;a href=&quot;https://www.gnu.org/software/grub/manual/multiboot/multiboot.html#Boot-information-format&quot;&gt;引导信息&lt;/a&gt;，包含了很多依赖于架构的结构，而不是提供清晰的抽象。&lt;/li&gt;
&lt;li&gt;GRUB和Multiboot标准的文档支持较少。&lt;/li&gt;
&lt;li&gt;GRUB需要安装在主机系统上才能从内核文件中创建一个可启动的磁盘镜像。这使得在Windows或Mac上的开发更加困难。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;鉴于此，我们决定不使用GRUB或Multiboot标准。然而，我们计划在我们的&lt;a href=&quot;https://github.com/rust-osdev/bootimage&quot;&gt;bootimage&lt;/a&gt;工具中加入对Multiboot的支持，这样就可以在GRUB系统上加载你的内核。如果你对编写一个符合Multiboot标准的内核感兴趣，请查看本系列博客的&lt;a href=&quot;https://os.phil-opp.com/edition-1/&quot;&gt;第一版&lt;/a&gt;。&lt;/p&gt;
&lt;h3&gt;UEFI&lt;/h3&gt;
&lt;p&gt;(我们目前不提供UEFI支持，但我们很乐意! 如果你愿意帮忙，请在&lt;a href=&quot;https://github.com/phil-opp/blog_os/issues/349&quot;&gt;Github issue&lt;/a&gt;中告诉我们。)&lt;/p&gt;
&lt;h2&gt;最小化内核&lt;/h2&gt;
&lt;p&gt;现在我们已经大致知道了计算机是如何启动的，现在是时候创建我们自己的最小内核了。我们的目标是创建一个磁盘映像，在启动时向屏幕打印 &quot;Hello World!&quot;。为此，我们在上一篇文章中的&lt;a href=&quot;https://os.phil-opp.com/freestanding-rust-binary/&quot;&gt;独立Rust二进制程序&lt;/a&gt;基础上进行开发。&lt;/p&gt;
&lt;p&gt;你可能还记得，我们通过&lt;code&gt;cargo&lt;/code&gt;构建了独立的二进制文件，但根据不同的操作系统，我们需要不同的入口点名称和编译标识。这是因为&lt;code&gt;cargo&lt;/code&gt;默认是为主机系统编译，也就是你正在运行的系统。这并不是我们想要的内核，因为一个运行在Windows之上的内核并没有什么意义。相反，我们希望为一个明确定义的目标系统编译。&lt;/p&gt;
&lt;h2&gt;安装Rust Nightly&lt;/h2&gt;
&lt;p&gt;Rust有三个发布频道：稳定版、测试版和nightly版。Rust Book很好地解释了这些&lt;a href=&quot;(https://doc.rust-lang.org/book/appendix-07-nightly-rust.html#choo-choo-release-channels-and-riding-the-trains)&quot;&gt;频道之间的区别&lt;/a&gt;，所以花点时间看看吧。为了构建一个操作系统，我们需要一些实验性的功能，而这些功能只有在nightly频道上才有，所以我们需要安装一个nightly版本的Rust。&lt;/p&gt;
&lt;p&gt;要管理Rust的安装，我强烈推荐&lt;a href=&quot;https://www.rustup.rs/&quot;&gt;rustup&lt;/a&gt;。它允许你并排安装nightly、测试版和稳定版编译器，并使其易于更新。使用rustup，你可以通过运行&lt;code&gt;rustup override set nightly&lt;/code&gt;为当前目录使用夜间编译器。或者，你也可以在项目的根目录下添加一个名为&lt;code&gt;rust-toolchain&lt;/code&gt;的文件，其内容为&lt;code&gt;nightly&lt;/code&gt;。你可以通过运行&lt;code&gt;rustc --version&lt;/code&gt;来检查你是否安装了nightly版本。版本号的末尾应该包含&lt;code&gt;-nightly&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;nightly编译器允许我们通过在文件顶部使用所谓的特征标识来选择加入各种实验性特征。例如，我们可以通过在&lt;code&gt;main.rs&lt;/code&gt;的顶部添加&lt;code&gt;#！[feature(asm)]&lt;/code&gt;来启用内联汇编的实验性&lt;a href=&quot;https://doc.rust-lang.org/unstable-book/library-features/asm.html&quot;&gt;asm!宏&lt;/a&gt;。请注意，这种实验性的特性是完全不稳定的，这意味着未来的Rust版本可能会在没有事先警告的情况下更改或删除它们。因此，我们只有在绝对必要的情况下才会使用它们。&lt;/p&gt;
&lt;h3&gt;目标配置清单&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;cargo&lt;/code&gt;通过&lt;code&gt;--target&lt;/code&gt;参数支持不同的目标系统。目标由所谓的&lt;a href=&quot;https://clang.llvm.org/docs/CrossCompilation.html#target-triple&quot;&gt;目标系统三元组&lt;/a&gt;描述，它描述了CPU架构、厂商、操作系统和&lt;a href=&quot;https://stackoverflow.com/a/2456882&quot;&gt;ABI&lt;/a&gt;。例如，&lt;code&gt;x86_64-unknown-linux-gnu&lt;/code&gt;目标三元组描述了一个具有&lt;code&gt;x86_64&lt;/code&gt; CPU、没有明确的供应商、且具有GNU ABI的Linux操作系统的目标系统。Rust支持许多不同的&lt;a href=&quot;https://forge.rust-lang.org/release/platform-support.html&quot;&gt;目标三元组&lt;/a&gt;，包括Android的&lt;code&gt;arm-linux-androideabi&lt;/code&gt;或&lt;a href=&quot;https://www.hellorust.com/setup/wasm-target/&quot;&gt;WebAssembly的wasm32-unknown-unknown&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;然而，对于我们的目标系统，我们需要一些特殊的配置参数（比如没有底层操作系统），所以&lt;a href=&quot;https://forge.rust-lang.org/release/platform-support.html&quot;&gt;现有的目标三元组&lt;/a&gt;都不适合。幸运的是，Rust允许我们通过JSON文件来&lt;a href=&quot;https://doc.rust-lang.org/nightly/rustc/targets/custom.html&quot;&gt;定义自己的目标三元组&lt;/a&gt;。例如，一个描述&lt;code&gt;x86_64-unknown-linux-gnu&lt;/code&gt;目标的JSON文件是大概这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;llvm-target&quot;: &quot;x86_64-unknown-linux-gnu&quot;,
    &quot;data-layout&quot;: &quot;e-m:e-i64:64-f80:128-n8:16:32:64-S128&quot;,
    &quot;arch&quot;: &quot;x86_64&quot;,
    &quot;target-endian&quot;: &quot;little&quot;,
    &quot;target-pointer-width&quot;: &quot;64&quot;,
    &quot;target-c-int-width&quot;: &quot;32&quot;,
    &quot;os&quot;: &quot;linux&quot;,
    &quot;executables&quot;: true,
    &quot;linker-flavor&quot;: &quot;gcc&quot;,
    &quot;pre-link-args&quot;: [&quot;-m64&quot;],
    &quot;morestack&quot;: false
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大多数字段是LLVM为该平台生成代码所需要的。例如，&lt;a href=&quot;https://llvm.org/docs/LangRef.html#data-layout&quot;&gt;data-layout&lt;/a&gt;字段定义了各种整数、浮点和指针类型的大小。还有一些Rust用于条件编译的字段，比如&lt;code&gt;target-pointer-width&lt;/code&gt;。第三种字段定义了应该如何构建crate。例如，&lt;code&gt;pre-link-args&lt;/code&gt;字段指定了传递给&lt;a href=&quot;https://en.wikipedia.org/wiki/Linker_(computing)&quot;&gt;链接器&lt;/a&gt;的参数。&lt;/p&gt;
&lt;p&gt;我们的内核也是以&lt;code&gt;x86_64&lt;/code&gt;系统为目标的，所以我们的目标描述文件看起来会和上面的很相似。让我们先创建一个&lt;code&gt;x86_64-blog_os.json&lt;/code&gt;文件(选择任何你喜欢的名字)，里面有通用的内容有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;llvm-target&quot;: &quot;x86_64-unknown-none&quot;,
    &quot;data-layout&quot;: &quot;e-m:e-i64:64-f80:128-n8:16:32:64-S128&quot;,
    &quot;arch&quot;: &quot;x86_64&quot;,
    &quot;target-endian&quot;: &quot;little&quot;,
    &quot;target-pointer-width&quot;: &quot;64&quot;,
    &quot;target-c-int-width&quot;: &quot;32&quot;,
    &quot;os&quot;: &quot;none&quot;,
    &quot;executables&quot;: true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意，我们将&lt;code&gt;llvm-target&lt;/code&gt;中的系统以及&lt;code&gt;os&lt;/code&gt;字段中的系统均改为&lt;code&gt;none&lt;/code&gt;，因为我们将在裸机上运行。&lt;/p&gt;
&lt;p&gt;我们增加以下与build有关的配置项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;linker-flavor&quot;: &quot;ld.lld&quot;,
&quot;linker&quot;: &quot;rust-lld&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们不使用平台的默认链接器（可能不支持Linux目标系统），而是使用Rust自带的跨平台&lt;a href=&quot;https://lld.llvm.org/&quot;&gt;LLD&lt;/a&gt;链接器来链接我们的内核。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;panic-strategy&quot;: &quot;abort&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个设置指定了目标不支持在panic时进行&lt;a href=&quot;https://www.bogotobogo.com/cplusplus/stackunwinding.php&quot;&gt;栈展开&lt;/a&gt;，因此程序遇到问题时将直接终止。这和我们Cargo.toml中的&lt;code&gt;panic = &quot;abort&quot;&lt;/code&gt;选项一样，所以我们可以从那里删除它。(请注意，与Cargo.toml选项不同的是，这个目标选项在我们后面重新编译&lt;code&gt;core&lt;/code&gt;库时通用需要。因此，即使你喜欢保留Cargo.toml里的选项，也一定要添加这个选项。)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;disable-redzone&quot;: true,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们正在编写一个内核，所以我们需要在某些时候处理中断。为了安全起见，我们必须禁用一个叫做“红区”的堆栈指针优化，因为如果不这样做，可能会导致堆栈数据被破坏。更多的信息，请看我们单独的关于&lt;a href=&quot;https://os.phil-opp.com/red-zone/&quot;&gt;禁用红区&lt;/a&gt;的文章。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;features&quot;: &quot;-mmx,-sse,+soft-float&quot;,
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fatures&lt;/code&gt;字段启用/禁用目标的CPU指令特性。我们在&lt;code&gt;mmx&lt;/code&gt;和&lt;code&gt;sse&lt;/code&gt;特性前使用减号来禁用它们，在&lt;code&gt;soft-float&lt;/code&gt;特性前加加号来启用它。注意不同标志之间不能有空格，否则LLVM无法解释特性字符串。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mmx&lt;/code&gt;和&lt;code&gt;sse&lt;/code&gt;特性决定了对&lt;a href=&quot;https://en.wikipedia.org/wiki/SIMD&quot;&gt;单指令多数据(SIMD)&lt;/a&gt;指令的支持，这通常可以大大提升程序的速度。然而，在操作系统内核中使用大型SIMD寄存器会导致性能问题。原因是内核需要在继续中断的程序之前将所有寄存器恢复到原来的状态。这意味着内核必须在每次系统调用或硬件中断时将完整的SIMD状态保存到主存中。由于SIMD状态非常大（512-1600字节），而且中断可能会非常频繁地发生，这些额外的保存/恢复操作会大大降低性能。为了避免这种情况，我们禁用了内核的SIMD（并不是禁用运行在其上的应用程序的SIMD特性！）。&lt;/p&gt;
&lt;p&gt;禁用SIMD的一个问题是，&lt;code&gt;x86_64&lt;/code&gt;上的浮点运算默认需要SIMD寄存器。为了解决这个问题，我们增加了&lt;code&gt;soft-float&lt;/code&gt;特性，通过基于普通整数的软件函数来模拟所有的浮点运算。&lt;/p&gt;
&lt;p&gt;我们撰写了一篇关于&lt;a href=&quot;https://os.phil-opp.com/disable-simd/&quot;&gt;禁用SIMD&lt;/a&gt;的短文，以供读者深入了解相关信息。&lt;/p&gt;
&lt;h4&gt;放在一起&lt;/h4&gt;
&lt;p&gt;现在，我们的目标系统描述文件现在看起来是这样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;llvm-target&quot;: &quot;x86_64-unknown-none&quot;,
  &quot;data-layout&quot;: &quot;e-m:e-i64:64-f80:128-n8:16:32:64-S128&quot;,
  &quot;arch&quot;: &quot;x86_64&quot;,
  &quot;target-endian&quot;: &quot;little&quot;,
  &quot;target-pointer-width&quot;: &quot;64&quot;,
  &quot;target-c-int-width&quot;: &quot;32&quot;,
  &quot;os&quot;: &quot;none&quot;,
  &quot;executables&quot;: true,
  &quot;linker-flavor&quot;: &quot;ld.lld&quot;,
  &quot;linker&quot;: &quot;rust-lld&quot;,
  &quot;panic-strategy&quot;: &quot;abort&quot;,
  &quot;disable-redzone&quot;: true,
  &quot;features&quot;: &quot;-mmx,-sse,+soft-float&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编译内核&lt;/h3&gt;
&lt;p&gt;编译我们的新目标系统将使用Linux惯例（我不太确定为什么，我猜这只是LLVM默认如此）。这意味着我们需要一个名为&lt;code&gt;_start&lt;/code&gt;的入口点，就像&lt;a href=&quot;https://os.phil-opp.com/freestanding-rust-binary/&quot;&gt;上一篇&lt;/a&gt;文章中描述的那样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std] // 不链接Rust标准库
#![no_main] // 禁用所有Rust层级的入口点

use core::panic::PanicInfo;

/// 这个函数将在panic时被调用
#[panic_handler]
fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
    loop {}
}

#[no_mangle] // 不重整函数名
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    // 因为编译器会寻找一个名为`_start`的函数，所以这个函数就是入口点
    // 默认命名为`_start`
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，无论你的主机操作系统如何，这个入口点都需应命名为&lt;code&gt;_start&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在，我们可以给&lt;code&gt;--target&lt;/code&gt;参数传入JSON描述文件来为我们的新目标系统构建内核。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo build --target x86_64-blog_os.json

error[E0463]: can&apos;t find crate for `core`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译失败了! 这个错误告诉我们，Rust编译器已经找不到&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/index.html&quot;&gt;&lt;code&gt;core&lt;/code&gt;库&lt;/a&gt;了。这个库包含Rust基本类型，如&lt;code&gt;Result&lt;/code&gt;、&lt;code&gt;Option&lt;/code&gt;、迭代器等，并且隐式地链接到所有&lt;code&gt;no_std&lt;/code&gt;crates。&lt;/p&gt;
&lt;p&gt;这个问题在于，core库是作为预编译库与Rust编译器一起发布的。因此，它只支持预定义的几个目标系统三元组(如&lt;code&gt;x86_64-unknown-linux-gnu&lt;/code&gt;)，而不支持我们自定义的目标三元组。如果我们想为其他目标编译代码，我们需要先为这些目标重新编译&lt;code&gt;core&lt;/code&gt;库。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;build-std&lt;/code&gt;选项&lt;/h3&gt;
&lt;p&gt;这就是cargo的&lt;a href=&quot;https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std&quot;&gt;&lt;code&gt;build-std&lt;/code&gt;&lt;/a&gt;选项的作用：它允许按需重新编译&lt;code&gt;core&lt;/code&gt;和其他标准库crate，而不是使用Rust安装时附带的预编译版本。这个功能是非常新的，还没有完成，所以它被标记为&quot;unstable&quot;，只在nightly的Rust编译器上可用。&lt;/p&gt;
&lt;p&gt;为了使用该特性，我们需要在项目跟目录下新建&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/config.html&quot;&gt;cargo配置文件&lt;/a&gt;&lt;code&gt;.cargo/config.toml&lt;/code&gt;，加入内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[unstable]
build-std = [&quot;core&quot;, &quot;compiler_builtins&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该配置项告诉cargo需要重新编译&lt;code&gt;core&lt;/code&gt;和&lt;code&gt;compiler_builtins&lt;/code&gt;库，其中&lt;code&gt;compiler_builtins&lt;/code&gt;库是编译&lt;code&gt;core&lt;/code&gt;库的一个依赖。为了编译这些库，cargo需要读取rust源码，可以通过&lt;code&gt;rustup component add rust-src&lt;/code&gt;安装源码组件。&lt;/p&gt;
&lt;p&gt;:::tip[注意]
&lt;code&gt;unstable.build-std&lt;/code&gt;配置项仅在2020-07-15之后的Rust nightly中提供
:::&lt;/p&gt;
&lt;p&gt;设置好&lt;code&gt;unstable.build-std&lt;/code&gt;配置项并安装&lt;code&gt;rust-src&lt;/code&gt;源码组件后，就可以重新运行我们的编译命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo build --target x86_64-blog_os.json
   Compiling core v0.0.0 (/…/rust/src/libcore)
   Compiling rustc-std-workspace-core v1.99.0 (/…/rust/src/tools/rustc-std-workspace-core)
   Compiling compiler_builtins v0.1.32
   Compiling blog_os v0.1.0 (/…/blog_os)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到&lt;code&gt;cargo build&lt;/code&gt;现在为我们的自定义目标重新编译了&lt;code&gt;core&lt;/code&gt;、&lt;code&gt;rustc-std-workspace-core&lt;/code&gt;(此为&lt;code&gt;compiler_builtins&lt;/code&gt;的依赖)和&lt;code&gt;compiler_builtins&lt;/code&gt;库。&lt;/p&gt;
&lt;h3&gt;内存相关的内联函数&lt;/h3&gt;
&lt;p&gt;Rust编译器假设，在所有的操作系统中，均提供一组内置函数。其中的大多数函数由我们刚才编译的&lt;code&gt;compiler_builtins&lt;/code&gt;crate提供。不过，该crate中有一些内存相关的函数，通常由操作系统的C语言库提供，所以默认为不启用。这些函数包括用以将一个内存块中的所有字节设置为一个给定值的&lt;code&gt;memset&lt;/code&gt;，用以将一个内存块复制到另一个内存块的&lt;code&gt;memcpy&lt;/code&gt;，以及用来比较两个内存块的&lt;code&gt;memcmp&lt;/code&gt;。虽然现在编译我们的内核的时候并不需要这些函数，但是当我们添加更多的代码时（例如在复制结构体的时候），就会用到这些函数。&lt;/p&gt;
&lt;p&gt;由于我们无法链接到操作系统的C语言库，所以我们需要另一种方式来向编译器提供这些函数。一个可能的方法是实现我们自己的&lt;code&gt;memset&lt;/code&gt;等一系列函数，并为它们添加#[no_mangle]标识（用以避免编译过程中的自动重命名）。不过这相当危险，因为在实现这些函数的过程中稍有差错就会导致程序不可预料的行为。例如，当你使用&lt;code&gt;for&lt;/code&gt;循环实现&lt;code&gt;memcpy&lt;/code&gt;时，你可能会得到一个无限递归，因为&lt;code&gt;for&lt;/code&gt;循环隐式地调用&lt;code&gt;IntoIterator::into_iter&lt;/code&gt;trait方法，这可能会再次调用&lt;code&gt;memcpy&lt;/code&gt;。所以，重用现有的经过良好测试的实现是个好主意。&lt;/p&gt;
&lt;p&gt;幸运的是，&lt;code&gt;compiler_builtins&lt;/code&gt;crate已经包含了所有需要的函数的实现，只是为了不与C语言库中的实现相冲突，它们被默认为禁用了。我们可以通过设置 cargo的&lt;a href=&quot;https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#build-std-features&quot;&gt;&lt;code&gt;build-std-features&lt;/code&gt;&lt;/a&gt;标识为&lt;code&gt;[&quot;compiler-builtins-mem&quot;]&lt;/code&gt;来启用它们。与&lt;code&gt;build-std&lt;/code&gt;标识一样，这个标识可以在命令行中以&lt;code&gt;-Z&lt;/code&gt;标志的形式传递，也可以在&lt;code&gt;.cargo/config.toml&lt;/code&gt;文件中的&quot;unstable&quot;域中配置。由于我们总是希望用这个标识来构建，所以配置文件选项对我们来说会更方便：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[unstable]
build-std-features = [&quot;compiler-builtins-mem&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;code&gt;compiler-builtins-mem&lt;/code&gt;特性是&lt;a href=&quot;https://github.com/rust-lang/rust/pull/77284&quot;&gt;最近&lt;/a&gt;才添加的，因此需要&lt;code&gt;2020-09-30&lt;/code&gt;后的Rust nightly版本。）&lt;/p&gt;
&lt;p&gt;在幕后，这个标志启用了&lt;code&gt;compiler_builtins&lt;/code&gt;crate的&lt;a href=&quot;https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/Cargo.toml#L54-L55&quot;&gt;&lt;code&gt;mem&lt;/code&gt;功能&lt;/a&gt;。这样做的效果是，&lt;code&gt;#[no_mangle]&lt;/code&gt;属性被应用到crate的&lt;a href=&quot;https://github.com/rust-lang/compiler-builtins/blob/eff506cd49b637f1ab5931625a33cef7e91fbbf6/src/mem.rs#L12-L69&quot;&gt;&lt;code&gt;memcpy&lt;/code&gt;等实现&lt;/a&gt;中，使得它们可以被链接器使用。&lt;/p&gt;
&lt;p&gt;如此，对编译器要求的所有函数，我们的内核都有了有效的实现，之后即使我们的代码变得更复杂，它也能够通过编译。&lt;/p&gt;
&lt;h3&gt;设置默认目标&lt;/h3&gt;
&lt;p&gt;为了避免每次使用&lt;code&gt;cargo xbuild&lt;/code&gt;时传递&lt;code&gt;--target&lt;/code&gt;参数，我们可以覆写默认的编译目标。继续向&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/config.html&quot;&gt;cargo配置文件&lt;/a&gt;&lt;code&gt;.cargo/config.toml&lt;/code&gt;添加以下内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[build]
target = &quot;x86_64-blog_os.json&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的配置告诉&lt;code&gt;cargo&lt;/code&gt;在没有显式声明目标的情况下，使用我们提供的&lt;code&gt;x86_64-blog_os.json&lt;/code&gt;作为目标配置。这意味着我们可以直接使用&lt;code&gt;cargo build&lt;/code&gt;进行构建。更多关于cargo配置选项的信息，请查看&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/config.html&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;现在我们可以用一个简单的&lt;code&gt;cargo build&lt;/code&gt;构建裸金属上的内核了。然而，我们的&lt;code&gt;_start&lt;/code&gt;入口点仍然是空的，它将被bootloader调用。是时候输出一些东西到屏幕上了。&lt;/p&gt;
&lt;h3&gt;在屏幕上打印字符&lt;/h3&gt;
&lt;p&gt;现阶段将文字打印到屏幕上最简单的方法是使用&lt;a href=&quot;(https://en.wikipedia.org/wiki/VGA-compatible_text_mode)&quot;&gt;VGA文字缓冲区&lt;/a&gt;。它是一个包含屏幕上显示的内容的特殊的内存区域，直接映射到VGA硬件。它通常由25行组成，每行包含80个字符单元。每个字符单元显示一个ASCII字符，并带有一些前景和背景颜色。屏幕输出的内容是这样的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/6/6d/Codepage-737.png&quot; alt=&quot;字符&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们将在下一篇文章中讨论VGA缓冲区的详情，届时将为它编写第一个小型驱动程序。我们目前仅仅是打印&quot;Hello World!&quot;，只需要知道缓冲区位于地址&lt;code&gt;0xb8000&lt;/code&gt;，每个字符单元由一个ASCII字节和一个颜色字节组成。&lt;/p&gt;
&lt;p&gt;我们的实现就像这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static HELLO: &amp;amp;[u8] = b&quot;Hello World!&quot;;

#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    let vga_buffer = 0xb8000 as *mut u8;

    for (i, &amp;amp;byte) in HELLO.iter().enumerate() {
        unsafe {
            *vga_buffer.offset(i as isize * 2) = byte;
            *vga_buffer.offset(i as isize * 2 + 1) = 0xb;
        }
    }

    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们将整数&lt;code&gt;0xb8000&lt;/code&gt;转成一个&lt;a href=&quot;https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#dereferencing-a-raw-pointer&quot;&gt;裸指针&lt;/a&gt;。然后，我们对&lt;a href=&quot;https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#the-static-lifetime&quot;&gt;静态&lt;/a&gt;HELLO&lt;a href=&quot;https://doc.rust-lang.org/reference/tokens.html#byte-string-literals&quot;&gt;字节字符串&lt;/a&gt;的字节进行&lt;a href=&quot;https://doc.rust-lang.org/stable/book/ch13-02-iterators.html&quot;&gt;迭代&lt;/a&gt;。我们使用&lt;a href=&quot;https://doc.rust-lang.org/core/iter/trait.Iterator.html#method.enumerate&quot;&gt;&lt;code&gt;enumerate&lt;/code&gt;&lt;/a&gt;方法额外获得迭代索引&lt;code&gt;i&lt;/code&gt;，在&lt;code&gt;for&lt;/code&gt;循环中，我们使用&lt;a href=&quot;https://doc.rust-lang.org/std/primitive.pointer.html#method.offset&quot;&gt;&lt;code&gt;offset&lt;/code&gt;&lt;/a&gt;方法写入字符串字节和相应的颜色字节（&lt;code&gt;0xb&lt;/code&gt;为淡青色）。&lt;/p&gt;
&lt;p&gt;请注意，所有的内存写入操作均放置于&lt;a href=&quot;https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html&quot;&gt;&lt;code&gt;unsafe&lt;/code&gt;&lt;/a&gt;块中。原因是Rust编译器无法推断出我们创建的裸指针是有效的。它们可能指向任何地方并导致数据损坏。通过将它们放入&lt;code&gt;unsafe&lt;/code&gt;块，我们基本上是在告诉编译器，我们绝对确信这些操作是有效的。请注意，&lt;code&gt;unsafe&lt;/code&gt;块并不会关闭Rust的安全检查。它只允许您做&lt;a href=&quot;https://doc.rust-lang.org/stable/book/ch19-01-unsafe-rust.html#unsafe-superpowers&quot;&gt;五件额外的事情&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;我想强调的是，这不是我们想在Rust中做的事情！在不安全的块内处理裸指针时，很容易出乱子，例如，如果我们不小心，很容易写到缓冲区的末端。&lt;/p&gt;
&lt;p&gt;所以我们要尽可能的减少&lt;code&gt;unsafe&lt;/code&gt;块的使用。Rust通过创建安全抽象给我们提供了这样的能力。例如，我们可以创建一个VGA缓冲类型，封装所有的不安全操作，并确保不可能从外部进行任何错误调用。这样一来，我们就只需要最少的&lt;code&gt;unsafe&lt;/code&gt;块，并且可以确保不违反&lt;a href=&quot;https://en.wikipedia.org/wiki/Memory_safety&quot;&gt;内存安全&lt;/a&gt;。我们将在下一篇文章中创建这样一个安全的VGA缓冲区抽象。&lt;/p&gt;
&lt;h2&gt;启动内核&lt;/h2&gt;
&lt;p&gt;现在我们已经有了一个可执行文件，并且可以进行一些可见的操作，是时候运行它了。首先，我们需要将我们编译好的内核通过与bootloader连接，变成一个可引导的磁盘镜像。然后，我们可以在&lt;a href=&quot;https://www.qemu.org/&quot;&gt;QEMU&lt;/a&gt;虚拟机中运行磁盘镜像，或者用U盘在真实的硬件上引导它。&lt;/p&gt;
&lt;h3&gt;创建引导镜像&lt;/h3&gt;
&lt;p&gt;为了把编译后的内核变成一个可启动的磁盘镜像，我们需要把它和引导程序连接起来。正如我们在&lt;a href=&quot;https://os.phil-opp.com/minimal-rust-kernel/#the-boot-process&quot;&gt;启动过程&lt;/a&gt;一节中所学到的，引导程序负责初始化CPU和加载内核。&lt;/p&gt;
&lt;p&gt;我们不需要编写自己的bootloader，这是一个独立的项目，我们使用&lt;a href=&quot;https://crates.io/crates/bootloader&quot;&gt;bootloader&lt;/a&gt; crate。这个crate实现了一个基本的BIOS引导加载器，没有任何C语言的依赖，只有Rust和内联汇编。为了使用它来启动我们的内核，我们需要添加一个依赖。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[dependencies]
bootloader = &quot;0.9.8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加bootloader作为依赖，并不足以真正创建一个可启动的磁盘镜像。其中的问题是，我们需要在编译后将内核与bootloader链接起来，但cargo不支持&lt;a href=&quot;https://github.com/rust-lang/cargo/issues/545&quot;&gt;post-build脚本&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，我们创建了一个名为&lt;code&gt;bootimage&lt;/code&gt;的工具，它首先编译内核和bootloader，然后将它们连接在一起，创建一个可启动的磁盘镜像。要安装这个工具，请在你的终端上执行以下命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo install bootimage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了运行&lt;code&gt;bootimage&lt;/code&gt;和构建bootloader，你需要安装&lt;code&gt;llvm-tools-preview&lt;/code&gt; rustup组件。你可以通过执行&lt;code&gt;rustup component add llvm-tools-preview&lt;/code&gt;来安装。&lt;/p&gt;
&lt;p&gt;安装&lt;code&gt;bootimage&lt;/code&gt;并添加&lt;code&gt;llvm-tools-preview&lt;/code&gt;组件后，我们可以创建可启动磁盘镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo bootimage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们看到该工具使用&lt;code&gt;cargo build&lt;/code&gt;重新编译我们的内核，所以它会自动获取你所做的任何更改。之后，它会编译bootloader，这可能需要一段时间。像所有的crate依赖一样，它只编译一次，然后进行缓存，所以后续的编译速度会快很多。最后，bootimage将bootloader和你的内核结合成一个可启动的磁盘镜像。&lt;/p&gt;
&lt;p&gt;执行该命令后，你应该会在&lt;code&gt;target/x86_64-blog_os/debug&lt;/code&gt;目录下看到一个名为&lt;code&gt;bootimage-blog_os.bin&lt;/code&gt;的可启动磁盘镜像。你可以在虚拟机中启动它，或者将它复制到USB驱动器中，在真正的硬件上启动它。(注意，这不是CD镜像，因为CD镜像有不同的格式，所以刻录到CD上是不行的)。&lt;/p&gt;
&lt;h3&gt;它是如何工作的？&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bootimage&lt;/code&gt;工具在后台执行以下步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它将我们的内核编译成&lt;a href=&quot;https://en.wikipedia.org/wiki/Executable_and_Linkable_Format&quot;&gt;ELF&lt;/a&gt;文件。&lt;/li&gt;
&lt;li&gt;它将bootloader的依赖性编译成一个独立的可执行文件。&lt;/li&gt;
&lt;li&gt;它将内核ELF文件按字节链接到bootloader末尾。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当启动时，bootloader读取并解析附加的ELF文件。然后，它将程序段映射到页表中的虚拟地址，将&lt;code&gt;.bss&lt;/code&gt;部分归零，并建立一个堆栈。最后，它读取入口点地址（我们的&lt;code&gt;_start&lt;/code&gt;函数）并跳转到它。&lt;/p&gt;
&lt;h3&gt;在QEMU中启动内核&lt;/h3&gt;
&lt;p&gt;现在我们可以在虚拟机中启动内核了。为了在&lt;a href=&quot;https://www.qemu.org/&quot;&gt;QEMU&lt;/a&gt;中启动内核，我们使用下面的命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; qemu-system-x86_64 -drive format=raw,file=target/x86_64-blog_os/debug/bootimage-blog_os.bin
warning: TCG doesn&apos;t support requested feature: CPUID.01H:ECX.vmx [bit 5]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将打开一个单独的窗口与，看起来像这样：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://os.phil-opp.com/minimal-rust-kernel/qemu.png&quot; alt=&quot;qemu的显示内容&quot; /&gt;&lt;/p&gt;
&lt;p&gt;我们看到屏幕上可以看到的&quot;Hello World!&quot;。&lt;/p&gt;
&lt;h3&gt;在物理机器上运行内核&lt;/h3&gt;
&lt;p&gt;也可以将其写入U盘，在物理机器上启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; dd if=target/x86_64-blog_os/debug/bootimage-blog_os.bin of=/dev/sdX &amp;amp;&amp;amp; sync
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;sdX&lt;/code&gt;是你的U盘的设备名。&lt;strong&gt;请注意&lt;/strong&gt;选择正确的设备名称，因为该设备上的所有内容都会被覆盖。&lt;/p&gt;
&lt;p&gt;将镜像写入U盘后，就可以通过从U盘启动，并在物理硬件上运行它。你可能需要指定启动项或改变BIOS配置中的启动顺序来从U盘启动。需要注意的是，目前它还不能用于UEFI机器，因为bootloader crate还不支持UEFI。&lt;/p&gt;
&lt;h3&gt;使用&lt;code&gt;cargo run&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;为了方便在QEMU中运行我们的内核，可以设置cargo的&lt;code&gt;runner&lt;/code&gt;配置键。&lt;/p&gt;
&lt;p&gt;要让在QEMU中运行内核更轻松，我们可以设置在cargo配置文件中设置&lt;code&gt;runner&lt;/code&gt;配置项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[target.&apos;cfg(target_os = &quot;none&quot;)&apos;]
runner = &quot;bootimage runner&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;target.&apos;cfg(target_os = &quot;none&quot;)&apos;&lt;/code&gt;域适用于所有将目标系统配置文件的&lt;code&gt;&quot;os&quot;&lt;/code&gt;字段设置为&lt;code&gt;&quot;none&quot;&lt;/code&gt;的目标系统。这将包括我们的&lt;code&gt;x86_64-blog_os.json&lt;/code&gt;目标。&lt;code&gt;runner&lt;/code&gt;键指定了&lt;code&gt;cargo run&lt;/code&gt;应该调用的命令。该命令是在成功编译后运行的，可执行文件的路径将作为第一个参数传递。更多细节请参见&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/config.html&quot;&gt;cargo文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bootimage runner&lt;/code&gt;命令是专门设计用作&lt;code&gt;runner&lt;/code&gt;配置项的可执行程序使用的，它将链接给定的可执行程序和项目的bootloader依赖关系，然后启动QEMU。更多细节和可能的配置选项请参见&lt;a href=&quot;https://github.com/rust-osdev/bootimage&quot;&gt;&lt;code&gt;bootimage&lt;/code&gt;的Readme&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;现在我们可以使用&lt;code&gt;cargo run&lt;/code&gt;来编译我们的内核，并在QEMU中启动它。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;在下一篇文章中，我们将更详细地探讨VGA文本缓冲区，并为它编写一个安全的接口。我们还将添加对&lt;code&gt;println&lt;/code&gt;宏的支持。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>使用Rust编写操作系统 - 1.1 - Rust独立二进制程序</title><link>https://zlotus.github.io/2021/02/21/writing-an-os-in-rust-1.1/</link><guid isPermaLink="true">https://zlotus.github.io/2021/02/21/writing-an-os-in-rust-1.1/</guid><description>使用Rust编写操作系统 - 1.1 - Rust独立二进制程序</description><pubDate>Sun, 21 Feb 2021 16:56:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
本文所有内容均为翻译，原文：&lt;a href=&quot;https://os.phil-opp.com/freestanding-rust-binary/&quot;&gt;A Freestanding Rust Binary&lt;/a&gt;；原项目：&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;
:::&lt;/p&gt;
&lt;p&gt;创建我们自己的操作系统内核的第一步，是创建一个不链接标准库的Rust可执行程序。这样就可以在没有底层操作系统的情况下在&lt;a href=&quot;https://en.wikipedia.org/wiki/Bare_machine&quot;&gt;裸机&lt;/a&gt;上运行Rust代码。&lt;/p&gt;
&lt;p&gt;这个博客是在&lt;a href=&quot;https://github.com/phil-opp/blog_os&quot;&gt;GitHub&lt;/a&gt;上公开开发的。如果你有任何问题或疑问，请在那里开一个issue。你也可以在&lt;a href=&quot;https://os.phil-opp.com/freestanding-rust-binary/#comments&quot;&gt;底部&lt;/a&gt;留言。这篇文章的完整源代码可以在&lt;a href=&quot;https://github.com/phil-opp/blog_os/tree/post-01&quot;&gt;post-01&lt;/a&gt;分支中找到。&lt;/p&gt;
&lt;h2&gt;介绍&lt;/h2&gt;
&lt;p&gt;要编写操作系统内核，我们需要写出的代码不能依赖任何操作系统提供的功能。这意味着我们不能使用线程、文件、堆内存、网络、随机数、标准输出或任何其他需要操作系统抽象或需要特定硬件的功能。这是有道理的，毕竟我们正在尝试编写自己的操作系统和自己的驱动程序。&lt;/p&gt;
&lt;p&gt;这意味着我们不能使用大多数&lt;a href=&quot;https://doc.rust-lang.org/std/&quot;&gt;Rust标准库&lt;/a&gt;，但是可以使用很多Rust功能。例如，我们可以使用&lt;a href=&quot;https://doc.rust-lang.org/book/ch13-02-iterators.html&quot;&gt;迭代器&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/book/ch13-01-closures.html&quot;&gt;闭包&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/book/ch06-00-enums.html&quot;&gt;模式匹配&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/core/option/&quot;&gt;Option&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/core/result/index.html&quot;&gt;Result&lt;/a&gt;、&lt;a href=&quot;https://doc.rust-lang.org/core/macro.write.html&quot;&gt;格式化字符串&lt;/a&gt;，当然还有&lt;a href=&quot;https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html&quot;&gt;所有权系统&lt;/a&gt;。这些功能使我们能够以一种非常有表现力的高级方式编写内核，而不必担心&lt;a href=&quot;https://www.nayuki.io/page/undefined-behavior-in-c-and-cplusplus-programs&quot;&gt;未定义行为&lt;/a&gt;或&lt;a href=&quot;https://tonyarcieri.com/it-s-time-for-a-memory-safety-intervention&quot;&gt;内存安全&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;为了使用Rust创建OS内核，我们需要创建一个无需底层操作系统即可运行的可执行文件。这种可执行文件通常称为“独立式”或“裸机”可执行文件。&lt;/p&gt;
&lt;p&gt;这篇文章描述了创建一个Rust独立二进制文件的必要步骤，并解释了为什么需要这些步骤。如果您仅对一个最小的示例感兴趣，则可以直接跳转至&lt;a href=&quot;./#%E5%B0%8F%E7%BB%93&quot;&gt;&lt;strong&gt;小结&lt;/strong&gt;&lt;/a&gt;部分。&lt;/p&gt;
&lt;h2&gt;禁用标准库&lt;/h2&gt;
&lt;p&gt;默认情况下，所有Rust crate都链接&lt;a href=&quot;https://doc.rust-lang.org/std/&quot;&gt;标准库&lt;/a&gt;，该库建立在操作系统的线程、文件或网络等功能之上。它还依赖于C标准库&lt;code&gt;libc&lt;/code&gt;，该库与OS服务紧密交互。我们的计划是编写一个操作系统，因此不能使用任何依赖于OS的库。我们必须通过&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html&quot;&gt;&lt;code&gt;no_std&lt;/code&gt;&lt;/a&gt;属性禁用自动引用标准库。&lt;/p&gt;
&lt;p&gt;首先创建一个新的cargo项目。最简单的方法是通过命令行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo new blog_os --bin --edition 2018
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我将项目命名为&lt;code&gt;blog_os&lt;/code&gt;，你当然可以选择自己喜欢的名称。&lt;code&gt;--bin&lt;/code&gt;标志意为创建可执行二进制文件（与创建库的&lt;code&gt;--lib&lt;/code&gt;不同），而&lt;code&gt;--edition 2018&lt;/code&gt;参数指定crate需要使用&lt;a href=&quot;https://doc.rust-lang.org/nightly/edition-guide/rust-2018/index.html&quot;&gt;2018版的Rust&lt;/a&gt;。 当我们运行命令时，cargo为我们创建以下目录结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;blog_os
├── Cargo.toml
└── src
    └── main.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Cargo.toml&lt;/code&gt;包含crate配置，例如crate名称、作者、&lt;a href=&quot;https://semver.org/&quot;&gt;语义版本号&lt;/a&gt;和相关依赖。 &lt;code&gt;src/main.rs&lt;/code&gt;文件包含crate的根模块和&lt;code&gt;main&lt;/code&gt;函数。 您可以通过&lt;code&gt;cargo build&lt;/code&gt;来编译crate，然后在&lt;code&gt;target/debug&lt;/code&gt;子文件夹中运行已编译的&lt;code&gt;blog_os&lt;/code&gt;二进制文件。&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;no_std&lt;/code&gt;属性&lt;/h3&gt;
&lt;p&gt;现在，我们的crate隐式链接了标准库。让我们尝试通过添加&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/first-edition/using-rust-without-the-standard-library.html&quot;&gt;&lt;code&gt;no_std&lt;/code&gt;属性&lt;/a&gt;来禁用此功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]

fn main() {
    println!(&quot;Hello, world!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们尝试立即构建（通过运行&lt;code&gt;cargo build&lt;/code&gt;）时，会发生以下错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: cannot find macro `println!` in this scope
 --&amp;gt; src/main.rs:4:5
  |
4 |     println!(&quot;Hello, world!&quot;);
  |     ^^^^^^^
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发生此错误是因为&lt;a href=&quot;https://doc.rust-lang.org/std/macro.println.html&quot;&gt;&lt;code&gt;println&lt;/code&gt;宏&lt;/a&gt;是标准库的一部分，我们不能再使用它，也就是说我们无法再打印东西。这是合理的，因为&lt;code&gt;println&lt;/code&gt;写入标准输出，这也是由操作系统提供的特殊文件描述符。&lt;/p&gt;
&lt;p&gt;那么让我们删除打印语句，然后使用空的main函数再试一次：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]

fn main() {}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo build
error: `#[panic_handler]` function required, but not found
error: language item required, but not found: `eh_personality`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在，编译器指出缺少&lt;code&gt;#[panic_handler]&lt;/code&gt;函数和一个语言项。&lt;/p&gt;
&lt;h2&gt;实现Rust的panic&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;panic_handler&lt;/code&gt;属性定义的函数在发生&lt;a href=&quot;https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html&quot;&gt;panic&lt;/a&gt;时会被编译器调用。标准库提供了自己的panic处理函数，但那是在&lt;code&gt;no_std&lt;/code&gt;环境中，我们需要自己定义它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use core::panic::PanicInfo;

/// This function is called on panic.
#[panic_handler]
fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/panic/struct.PanicInfo.html&quot;&gt;&lt;code&gt;PanicInfo&lt;/code&gt;类型的参数&lt;/a&gt;包含产生panic时的文件和行以及可选的panic消息。该函数永不返回，因此使用&lt;code&gt;!&lt;/code&gt;定义函数返回&lt;a href=&quot;https://doc.rust-lang.org/nightly/std/primitive.never.html&quot;&gt;“never”类型&lt;/a&gt;，以将其标记为&lt;a href=&quot;https://doc.rust-lang.org/1.30.0/book/first-edition/functions.html#diverging-functions&quot;&gt;发散函数&lt;/a&gt;。目前我们还不能在此函数中执行太多操作，因此在其中写一个无限循环。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;eh_personality&lt;/code&gt;语言项&lt;/h2&gt;
&lt;p&gt;语言项是编译器内部所需的特殊功能和类型。例如，&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html&quot;&gt;&lt;code&gt;Copy&lt;/code&gt;&lt;/a&gt; trait是一种语言项，它告诉编译器哪些类型具有&lt;a href=&quot;https://doc.rust-lang.org/nightly/core/marker/trait.Copy.html&quot;&gt;&lt;code&gt;可复制语义&lt;/code&gt;&lt;/a&gt;。在查看其&lt;a href=&quot;https://github.com/rust-lang/rust/blob/485397e49a02a3b7ff77c17e4a3f16c653925cb3/src/libcore/marker.rs#L296-L299&quot;&gt;实现&lt;/a&gt;时，会看到特殊的&lt;code&gt;#[lang = &quot;copy&quot;]&lt;/code&gt;属性，该属性将其定义为语言项。&lt;/p&gt;
&lt;p&gt;虽然我们自己提供语言项的自定义实现是可能的，但仅应将其作为最后的手段。原因是语言项是非常不稳定的实现细节，甚至不会进行类型检查（编译器甚至不检查函数是否具有正确的参数类型）。幸运的是，有一种更稳定的方法可以解决上述的语言项错误。&lt;/p&gt;
&lt;p&gt;被&lt;a href=&quot;https://github.com/rust-lang/rust/blob/edb368491551a77d77a48446d4ee88b35490c565/src/libpanic_unwind/gcc.rs#L11-L45&quot;&gt;&lt;code&gt;eh_personality&lt;/code&gt;语言项&lt;/a&gt;标记的函数用于实现&lt;a href=&quot;https://www.bogotobogo.com/cplusplus/stackunwinding.php&quot;&gt;栈展开&lt;/a&gt;功能。默认情况下，在出现&lt;a href=&quot;https://doc.rust-lang.org/stable/book/ch09-01-unrecoverable-errors-with-panic.html&quot;&gt;panic&lt;/a&gt;时，Rust使用栈展开为所有活动的栈变量执行析构函数。这样可以确保释放所有使用的内存，并允许父线程捕获panic并继续执行。但是，栈展开是一个复杂的过程，需要一些特定的OS库支持（例如，Linux上的&lt;a href=&quot;https://www.nongnu.org/libunwind/&quot;&gt;libunwind&lt;/a&gt;或Windows上的&lt;a href=&quot;https://docs.microsoft.com/de-de/windows/win32/debug/structured-exception-handling&quot;&gt;结构化异常处理&lt;/a&gt;），因此我们不希望将其用于我们的操作系统。&lt;/p&gt;
&lt;h3&gt;禁用栈展开&lt;/h3&gt;
&lt;p&gt;在一些其他场景中同样不希望使用栈展开，因此Rust提供了一个选择，可以在发生&lt;a href=&quot;https://github.com/rust-lang/rust/pull/32900&quot;&gt;panic时中止&lt;/a&gt;操作。这禁用了栈展开标志信息的生成，也会大大减小二进制程序的大小。禁用栈展开功能有多种方式，最简单的方法是将以下行添加到&lt;code&gt;Cargo.toml&lt;/code&gt;中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[profile.dev]
panic = &quot;abort&quot;

[profile.release]
panic = &quot;abort&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将为&lt;code&gt;dev&lt;/code&gt; profile（用于&lt;code&gt;cargo build&lt;/code&gt;）和&lt;code&gt;release&lt;/code&gt; profile（用于&lt;code&gt;cargo build --release&lt;/code&gt;）设置中止panic策略。现在，编译器应该不提醒缺少&lt;code&gt;eh_personality&lt;/code&gt;语言项了。&lt;/p&gt;
&lt;p&gt;我们修复了以上两个错误。但是，如果现在尝试编译，则会发生另一个错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; cargo build
error: requires `start` lang_item
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们的程序缺少定义入口点的&lt;code&gt;start&lt;/code&gt;语言项。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;start&lt;/code&gt;语言项&lt;/h2&gt;
&lt;p&gt;你可能会认为&lt;code&gt;main&lt;/code&gt;函数是程序运行时调用的第一个函数。但是，大多数语言都有一个&lt;a href=&quot;https://en.wikipedia.org/wiki/Runtime_system&quot;&gt;运行时系统&lt;/a&gt;，负责诸如垃圾回收（如Java）或软件线程（如Go中的goroutines）之类的事情。该运行时需要在&lt;code&gt;main&lt;/code&gt;函数之前调用，因为它需要初始化自己。&lt;/p&gt;
&lt;p&gt;在链接标准库的典型Rust二进制文件中，执行过程从名为&lt;code&gt;crt0&lt;/code&gt;(&quot;C runtime zero&quot;)的C运行时库开始，该库为C应用程序设置了环境。其中包括创建堆栈并将参数放置在正确的寄存器中。然后，C运行时调用&lt;a href=&quot;https://github.com/rust-lang/rust/blob/bb4d1491466d8239a7a5fd68bd605e3276e97afb/src/libstd/rt.rs#L32-L73&quot;&gt;Rust运行时的入口点&lt;/a&gt;，该入口点被&lt;code&gt;start&lt;/code&gt;语言项标记。Rust的运行时非常短，它可以处理一些小事情，例如设置栈溢出防护，或是在panic时打印回​​溯信息。之后，运行时才会调用&lt;code&gt;main&lt;/code&gt;函数。&lt;/p&gt;
&lt;p&gt;我们的独立可执行文件无法访问Rust运行时和&lt;code&gt;crt0&lt;/code&gt;，因此我们需要定义自己的入口点。自己实现&lt;code&gt;start&lt;/code&gt;语言项并没有什么帮助，因为它仍然需要&lt;code&gt;crt0&lt;/code&gt;。而我们要做的是直接覆盖&lt;code&gt;crt0&lt;/code&gt;入口点。&lt;/p&gt;
&lt;h3&gt;重写入口点&lt;/h3&gt;
&lt;p&gt;为了告诉Rust编译器我们不想使用普通的入口点链，需要添加了&lt;code&gt;#![no_main]&lt;/code&gt;属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std]
#![no_main]

use core::panic::PanicInfo;

/// This function is called on panic.
#[panic_handler]
fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;你可能会注意到，我们删除了&lt;code&gt;main&lt;/code&gt;函数，这是因为没有了底层的运行时调用，&lt;code&gt;main&lt;/code&gt;就失去意义。而我们现在使用自己的&lt;code&gt;_start&lt;/code&gt;函数覆盖操作系统入口点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过使用&lt;code&gt;#[no_mangle]&lt;/code&gt;属性，我们禁用了&lt;a href=&quot;https://en.wikipedia.org/wiki/Name_mangling&quot;&gt;名称重整&lt;/a&gt;，以确保Rust编译器确实输出名称为&lt;code&gt;_start&lt;/code&gt;的函数。如果没有该属性，则编译器会生成一些神秘的诸如&lt;code&gt;_ZN3blog_os4_start7hb173fedf945531caE&lt;/code&gt;的名称，用以为每个函数赋予唯一的名称。该属性是必需的，因为我们需要在下一步中将入口点函数的名称告知链接器。&lt;/p&gt;
&lt;p&gt;我们还必须将函数标记为&lt;code&gt;extern &quot;C&quot;&lt;/code&gt;，以告知编译器该函数应使用&lt;a href=&quot;https://en.wikipedia.org/wiki/Calling_convention&quot;&gt;C调用约定&lt;/a&gt;（而不是默认的Rust调用约定）。将函数命名为&lt;code&gt;_start&lt;/code&gt;的原因也是因为这是大多数系统的默认入口点名称。&lt;/p&gt;
&lt;p&gt;这里的&lt;code&gt;!&lt;/code&gt;返回类型表示这是一个发散函数，即永不返回。这是必需的，因为该入口点不会被任何函数调用，而是由操作系统或bootloader直接调用。因此该函数不会返回，而会调用操作系统的&lt;a href=&quot;https://en.wikipedia.org/wiki/Exit_(system_call)&quot;&gt;退出系统调用&lt;/a&gt;。在我们的项目中，关闭计算机可能是一个合理的操作，因为如果独立二进制程序返回后无需执行任何操作。目前，我们也通过无限循环来实现。&lt;/p&gt;
&lt;p&gt;现在，当我们运行&lt;code&gt;cargo build&lt;/code&gt;时，我们会看到一个难看的&lt;strong&gt;链接器&lt;/strong&gt;错误。&lt;/p&gt;
&lt;p&gt;我们使用&lt;code&gt;no_mangle&lt;/code&gt;标记这个函数，来对它禁用&lt;strong&gt;名称重整&lt;/strong&gt;（&lt;a href=&quot;https://en.wikipedia.org/wiki/Name_mangling&quot;&gt;name mangling&lt;/a&gt;）——这确保Rust编译器输出一个名为&lt;code&gt;_start&lt;/code&gt;的函数；否则，编译器可能最终生成名为&lt;code&gt;_ZN3blog_os4_start7hb173fedf945531caE&lt;/code&gt;的函数，无法让链接器正确辨别。&lt;/p&gt;
&lt;p&gt;我们还将函数标记为&lt;code&gt;extern &quot;C&quot;&lt;/code&gt;，告诉编译器这个函数应当使用&lt;a href=&quot;https://en.wikipedia.org/wiki/Calling_convention&quot;&gt;C语言的调用约定&lt;/a&gt;，而不是Rust语言的调用约定。函数名为&lt;code&gt;_start&lt;/code&gt;，是因为大多数系统默认使用这个名字作为入口点名称。&lt;/p&gt;
&lt;p&gt;与前文的&lt;code&gt;panic&lt;/code&gt;函数类似，这个函数的返回值类型为&lt;code&gt;!&lt;/code&gt;——它定义了一个发散函数，或者说一个不允许返回的函数。这一点是必要的，因为这个入口点不将被任何函数调用，但将直接被操作系统或&lt;strong&gt;引导程序&lt;/strong&gt;（bootloader）调用。所以作为函数返回的替换，这个入口点应该调用，比如操作系统提供的&lt;strong&gt;exit系统调用&lt;/strong&gt;（&lt;a href=&quot;https://en.wikipedia.org/wiki/Exit_(system_call)&quot;&gt;&quot;exit&quot; system call&lt;/a&gt;）函数。在我们编写操作系统的情况下，关机应该是一个合适的选择，因为&lt;strong&gt;当一个独立式可执行程序返回时，不会留下任何需要做的事情&lt;/strong&gt;（there is nothing to do if a freestanding binary returns）。暂时来看，我们可以添加一个无限循环，这样可以符合返回值的类型。&lt;/p&gt;
&lt;p&gt;如果我们现在编译这段程序，会出来一大段不太好看的&lt;strong&gt;链接器错误&lt;/strong&gt;（linker error）。&lt;/p&gt;
&lt;h2&gt;链接器错误&lt;/h2&gt;
&lt;p&gt;链接器是一个将生成的代码组合成可执行文件的程序。由于Linux、Windows和macOS的可执行文件格式不同，因此每个系统都有自己的链接器，引发不同的错误。 而错误的本因是相同的：链接器的默认配置假定我们的程序依赖于C运行时，而实际上并非如此。&lt;/p&gt;
&lt;p&gt;为了解决这个错误，我们需要告诉链接器它不应该引用C运行时。我们可以通过将一组特定的参数传递给链接器或通过构建裸机目标程序来实现。&lt;/p&gt;
&lt;h3&gt;构建逻辑目标程序&lt;/h3&gt;
&lt;p&gt;默认情况下，Rust将尝试构建一个能够在你当前的系统环境中运行的可执行文件。例如，如果你在&lt;code&gt;x86_64&lt;/code&gt;硬件上使用Windows，Rust会尝试构建一个使用&lt;code&gt;x86_64&lt;/code&gt;指令的&lt;code&gt;.exe&lt;/code&gt; Windows可执行程序。该环境也称为您的“宿主机”系统。&lt;/p&gt;
&lt;p&gt;Rust使用被称为&lt;a href=&quot;https://clang.llvm.org/docs/CrossCompilation.html#target-triple&quot;&gt;目标三元组&lt;/a&gt;的字符串来描述不同的编译环境。你可以通过运行&lt;code&gt;rustc --version --verbose&lt;/code&gt;来查看宿主机的目标三元组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rustc 1.35.0-nightly (474e7a648 2019-04-07)
binary: rustc
commit-hash: 474e7a6486758ea6fc761893b1a49cd9076fb0ab
commit-date: 2019-04-07
host: x86_64-unknown-linux-gnu
release: 1.35.0-nightly
LLVM version: 8.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些输出来自一个x86_64上的Linux系统。我们看到&lt;code&gt;host&lt;/code&gt;三元组是&lt;code&gt;x86_64-unknown-linux-gnu&lt;/code&gt;，这包括了CPU架构(&lt;code&gt;x86_64&lt;/code&gt;)，供应商(&lt;code&gt;unknown&lt;/code&gt;)，操作系统&lt;code&gt;linux&lt;/code&gt;和&lt;a href=&quot;https://en.wikipedia.org/wiki/Application_binary_interface&quot;&gt;ABI&lt;/a&gt;(&lt;code&gt;gnu&lt;/code&gt;)。&lt;/p&gt;
&lt;p&gt;为了以我们的宿主机三元组为目标编译程序，Rust编译器和链接器会假定存在默认情况下使用C运行时的底层操作系统（例如Linux或Windows），而这会导致链接器错误。因此，为避免链接器错误，我们可以针对没有基础操作系统的其他环境进行编译。&lt;/p&gt;
&lt;p&gt;这种裸机环境的一个例子是&lt;code&gt;thumbv7em-none-eabihf&lt;/code&gt;目标三元组，它描述了&lt;a href=&quot;https://en.wikipedia.org/wiki/Embedded_system&quot;&gt;嵌入式&lt;/a&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/ARM_architecture&quot;&gt;ARM&lt;/a&gt;系统。细节并不重要，重要的是这个目标中的&lt;code&gt;none&lt;/code&gt;表明该目标三元组也没有底层操作系统。为了能够为此目标进行编译，我们需要使用rustup添加这个目标所需文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rustup target add thumbv7em-none-eabihf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这将下载系统的标准（和核心）库的。如此，我们可以为该目标构建独立可执行程序了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo build --target thumbv7em-none-eabihf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过指定&lt;code&gt;--target&lt;/code&gt;参数，我们&lt;a href=&quot;https://en.wikipedia.org/wiki/Cross_compiler&quot;&gt;交叉编译&lt;/a&gt;了裸机目标系统的可执行程序。由于目标系统并没有操作系统，因此链接程序不会尝试链接C运行时，也因此构建将没有任何链接程序错误，提示构建成功。&lt;/p&gt;
&lt;p&gt;这便是我们即将用于OS内核构建的方法。这一次，我们将使用描述&lt;code&gt;x86_64&lt;/code&gt;裸机环境的自定义目标三元组来代替&lt;code&gt;thumbv7em-none-eabihf&lt;/code&gt;。我们将在下一篇文章中做出详细介绍。&lt;/p&gt;
&lt;h3&gt;链接器参数&lt;/h3&gt;
&lt;p&gt;除了以裸机为目标进行编译之外，我们还可以通过将一组特定的参数传递给链接器来解决链接器错误。这不并是我们将用于内核编译的方法，因此本节是可选的，仅出于完整性考虑而提供。&lt;/p&gt;
&lt;p&gt;在本小节中，我们讨论在Linux，Windows和macOS上发生的链接器错误，并说明如何通过将其他参数传递给链接器来解决这些错误。请注意，操作系统之间的可执行程序格式和链接器有所不同，因此每个系统都需要不同的参数集。&lt;/p&gt;
&lt;h4&gt;Linux&lt;/h4&gt;
&lt;p&gt;在Linux上，发生以下链接程序错误（部分）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: linking with `cc` failed: exit code: 1
  |
  = note: &quot;cc&quot; […]
  = note: /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start&apos;:
          (.text+0x12): undefined reference to `__libc_csu_fini&apos;
          /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start&apos;:
          (.text+0x19): undefined reference to `__libc_csu_init&apos;
          /usr/lib/gcc/../x86_64-linux-gnu/Scrt1.o: In function `_start&apos;:
          (.text+0x25): undefined reference to `__libc_start_main&apos;
          collect2: error: ld returned 1 exit status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其问题在于，链接器默认情况下引用C运行时的启动例程，也称为&lt;code&gt;_start&lt;/code&gt;。这需要已经被&lt;code&gt;no_std&lt;/code&gt;属性禁用的C标准库&lt;code&gt;libc&lt;/code&gt;中的一些符号，因此链接器无法解析这些引用。为了解决这个问题，我们可以通过传递&lt;code&gt;-nostartfiles&lt;/code&gt;参数来告诉链接器它不应链接C启动例程。&lt;/p&gt;
&lt;p&gt;其中一种使用cargo向传递链接器参数的方法是&lt;code&gt;cargo rustc&lt;/code&gt;命令。该命令的行为与&lt;code&gt;cargo build&lt;/code&gt;相同，同时允许将选项传递给底层的Rust编译器&lt;code&gt;rustc&lt;/code&gt;。&lt;code&gt;rustc&lt;/code&gt;具有&lt;code&gt;-C link-arg&lt;/code&gt;标志，该标志将参数传递给链接程序。综上，我们的新构建命令应为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo rustc -- -C link-arg=-nostartfiles
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如此，我们的crate便构建为Linux上的独立可执行程序了！&lt;/p&gt;
&lt;p&gt;我们并不需要显式指定入口点函数的名称，因为链接器默认情况下就会查找名称为&lt;code&gt;_start&lt;/code&gt;的函数。&lt;/p&gt;
&lt;h4&gt;Windows&lt;/h4&gt;
&lt;p&gt;在Windows上，发生了另一个链接器错误（部分）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: linking with `link.exe` failed: exit code: 1561
  |
  = note: &quot;C:\\Program Files (x86)\\…\\link.exe&quot; […]
  = note: LINK : fatal error LNK1561: entry point must be defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&quot;entry point must be defined&quot;的错误意味着链接器找不到入口点。在Windows上，默认入口点名称&lt;a href=&quot;https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol&quot;&gt;取决于所使用的子系统&lt;/a&gt;。对于&lt;code&gt;CONSOLE&lt;/code&gt;子系统，链接器将寻找一个名为&lt;code&gt;mainCRTStartup&lt;/code&gt;的函数，对于&lt;code&gt;WINDOWS&lt;/code&gt;子系统，它将寻找一个名为&lt;code&gt;WinMainCRTStartup&lt;/code&gt;的函数。要覆盖默认值以告诉链接器应查找名为&lt;code&gt;_start&lt;/code&gt;的函数，可以将&lt;code&gt;/ENTRY&lt;/code&gt;参数传递给链接器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo rustc -- -C link-arg=/ENTRY:_start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从不同的参数格式中，我们可以清楚地看到Windows链接器是与Linux链接器完全不同的程序。&lt;/p&gt;
&lt;p&gt;现在出现另一个链接器错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: linking with `link.exe` failed: exit code: 1221
  |
  = note: &quot;C:\\Program Files (x86)\\…\\link.exe&quot; […]
  = note: LINK : fatal error LNK1221: a subsystem can&apos;t be inferred and must be
          defined
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发生该错误的原因是Windows可执行程序可以使用不同的&lt;a href=&quot;https://docs.microsoft.com/en-us/cpp/build/reference/entry-entry-point-symbol&quot;&gt;子系统&lt;/a&gt;。对于普通程序，根据入口点名称进行推断：如果入口点名为&lt;code&gt;main&lt;/code&gt;，则使用&lt;code&gt;CONSOLE&lt;/code&gt;子系统；如果入口点名为&lt;code&gt;WinMain&lt;/code&gt;，则使用&lt;code&gt;WINDOWS&lt;/code&gt;子系统。 由于&lt;code&gt;_start&lt;/code&gt;函数是另一个名称，因此我们需要显式指定子系统：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo rustc -- -C link-args=&quot;/ENTRY:_start /SUBSYSTEM:console&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们在这里使用&lt;code&gt;CONSOLE&lt;/code&gt;子系统，指定&lt;code&gt;WINDOWS&lt;/code&gt;子系统也可以工作。与其多次传递&lt;code&gt;-C link-arg&lt;/code&gt;，不如使用&lt;code&gt;-C link-args&lt;/code&gt;，它使用空格分隔的参数列表。&lt;/p&gt;
&lt;p&gt;使用这个命令，我们成功的在Windows上构建了可执行程序。&lt;/p&gt;
&lt;h4&gt;macOS&lt;/h4&gt;
&lt;p&gt;在macOS上，发生以下链接器错误（部分）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: linking with `cc` failed: exit code: 1
  |
  = note: &quot;cc&quot; […]
  = note: ld: entry point (_main) undefined. for architecture x86_64
          clang: error: linker command failed with exit code 1 […]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该错误消息告诉我们，链接器无法找到默认名称为&lt;code&gt;main&lt;/code&gt;的入口点函数（由于某些原因，在macOS上所有函数均带有&lt;code&gt;_&lt;/code&gt;前缀）。要将入口点设置为·函数，我们需要传递链接器参数&lt;code&gt;-e&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo rustc -- -C link-args=&quot;-e __start&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-e&lt;/code&gt;标志指定入口点函数的名称。由于所有函数在macOS上都有一个附加的&lt;code&gt;_&lt;/code&gt;前缀，因此我们需要将入口点设置为&lt;code&gt;__start&lt;/code&gt;而不是&lt;code&gt;_start&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这次又出现以下链接器错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: linking with `cc` failed: exit code: 1
  |
  = note: &quot;cc&quot; […]
  = note: ld: dynamic main executables must link with libSystem.dylib
          for architecture x86_64
          clang: error: linker command failed with exit code 1 […]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;macOS并不&lt;a href=&quot;https://developer.apple.com/library/archive/qa/qa1118/_index.html&quot;&gt;正式支持静态链接的二进制文件&lt;/a&gt;，并且默认情况下要求程序链接&lt;code&gt;libSystem&lt;/code&gt;库。要覆盖它并链接静态二进制文件，我们将&lt;code&gt;-static&lt;/code&gt;标志传递给链接器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo rustc -- -C link-args=&quot;-e __start -static&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这仍然不够，出现了第三个链接器错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;error: linking with `cc` failed: exit code: 1
  |
  = note: &quot;cc&quot; […]
  = note: ld: library not found for -lcrt0.o
          clang: error: linker command failed with exit code 1 […]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该错误出现的原因是在默认情况下macOS上的程序链接到&lt;code&gt;crt0&lt;/code&gt;(&quot;C runtime zero&quot;)。这类似于我们在Linux上遇到的错误，也可以通过添加&lt;code&gt;-nostartfiles&lt;/code&gt;链接器参数来解决：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo rustc -- -C link-args=&quot;-e __start -static -nostartfiles&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我们的程序应该可以在macOS上成功构建了。&lt;/p&gt;
&lt;h4&gt;统一构建命令&lt;/h4&gt;
&lt;p&gt;现在，根据主机平台，我们有不同的构建命令，这并不方便。我们可以创建一个名为&lt;code&gt;.cargo/config.toml&lt;/code&gt;的文件，用以包含针对不同平台的特定编译参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;in .cargo/config.toml

[target.&apos;cfg(target_os = &quot;linux&quot;)&apos;]
rustflags = [&quot;-C&quot;, &quot;link-arg=-nostartfiles&quot;]

[target.&apos;cfg(target_os = &quot;windows&quot;)&apos;]
rustflags = [&quot;-C&quot;, &quot;link-args=/ENTRY:_start /SUBSYSTEM:console&quot;]

[target.&apos;cfg(target_os = &quot;macos&quot;)&apos;]
rustflags = [&quot;-C&quot;, &quot;link-args=-e __start -static -nostartfiles&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;rustflags&lt;/code&gt;键包含的参数会自动添加到&lt;code&gt;rustc&lt;/code&gt;的每次调用中。有关&lt;code&gt;.cargo/config.toml&lt;/code&gt;文件的更多信息，请查看&lt;a href=&quot;https://doc.rust-lang.org/cargo/reference/config.html&quot;&gt;官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;现在，我们的程序应该可以在这三个平台上以简单的&lt;code&gt;cargo build&lt;/code&gt;方式构建了。&lt;/p&gt;
&lt;h4&gt;应该这要做吗&lt;/h4&gt;
&lt;p&gt;虽然可以为Linux、Windows和macOS构建独立的可执行程序，但这可能不是一个好主意。原因是我们的可执行程序仍然需要各种各样的东西，例如，在调用&lt;code&gt;_start&lt;/code&gt;函数时初始化堆栈。没有C运行时，可能无法满足其中一些要求，这可能导致我们的程序出错，例如 通过分段故障。&lt;/p&gt;
&lt;p&gt;如果要创建在现有操作系统（包括&lt;code&gt;libc&lt;/code&gt;）之上运行的最小二进制文件，并按&lt;a href=&quot;https://doc.rust-lang.org/1.16.0/book/no-stdlib.html&quot;&gt;此处&lt;/a&gt;所述设置&lt;code&gt;#[start]&lt;/code&gt;属性，可能是一个更好的主意。&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;一个最小的独立Rust二进制程序如下所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#![no_std] // 不链接Rust标准库
#![no_main] // 禁用Rust层入口点

use core::panic::PanicInfo;

#[no_mangle] // 该函数不进行名称重整
pub extern &quot;C&quot; fn _start() -&amp;gt; ! {
    // 此函数为入口点，因为连接器将会查找默认叫做`_start`的函数
    loop {}
}

/// This function is called on panic.
#[panic_handler]
fn panic(_info: &amp;amp;PanicInfo) -&amp;gt; ! {
    loop {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;[package]
name = &quot;crate_name&quot;
version = &quot;0.1.0&quot;
authors = [&quot;Author Name &amp;lt;author@example.com&amp;gt;&quot;]

# the profile used for `cargo build`
[profile.dev]
panic = &quot;abort&quot; # disable stack unwinding on panic

# the profile used for `cargo build --release`
[profile.release]
panic = &quot;abort&quot; # disable stack unwinding on panic
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要构建此二进制程序，我们需要针对裸机目标进行编译，例如&lt;code&gt;thumbv7em-none-eabihf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cargo build --target thumbv7em-none-eabihf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此外，我们也可以通过向链接器传递其他参数来为宿主机系统编译它：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Linux
cargo rustc -- -C link-arg=-nostartfiles
# Windows
cargo rustc -- -C link-args=&quot;/ENTRY:_start /SUBSYSTEM:console&quot;
# macOS
cargo rustc -- -C link-args=&quot;-e __start -static -nostartfiles&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请注意，这只是Rust独立二进制程序的最小示例。该二进制程序还需要更多操作，例如，在调用&lt;code&gt;_start&lt;/code&gt;函数时初始化堆栈。因此，对于此类二进制程序的任何在实际场景中的使用，都需要继续添加更多的内容。&lt;/p&gt;
&lt;h2&gt;下期预告&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://os.phil-opp.com/minimal-rust-kernel/&quot;&gt;下一篇文章&lt;/a&gt;将介绍将我们的独立二进制程序转换为最小操作系统内核所需的步骤。其中包括创建自定义编译目标，将我们的可执行文件与bootloader结合以及学习如何在屏幕上打印内容。&lt;/p&gt;
&lt;h2&gt;支持本项目&lt;/h2&gt;
&lt;p&gt;创建和&lt;a href=&quot;https://os.phil-opp.com/status-update/&quot;&gt;维护&lt;/a&gt;这个博客和相关库是一项繁重的工作，但我真的很喜欢。通过支持我，您可以让我在新内容、新功能和持续维护上投入更多时间。&lt;/p&gt;
&lt;p&gt;支持我的最好方式是&lt;a href=&quot;https://github.com/sponsors/phil-opp&quot;&gt;在GitHub上赞助我&lt;/a&gt;，因为他们不收取任何中间费用。如果你喜欢其他平台，我也有&lt;a href=&quot;https://www.patreon.com/phil_opp&quot;&gt;Patreon&lt;/a&gt;和&lt;a href=&quot;https://donorbox.org/phil-opp&quot;&gt;Donorbox&lt;/a&gt;账户。后者是最灵活的，因为它支持多种货币和一次性捐款。&lt;/p&gt;
&lt;p&gt;感谢您的支持！&lt;/p&gt;
</content:encoded></item><item><title>Rust尝鲜</title><link>https://zlotus.github.io/2020/12/02/first-rust-code/</link><guid isPermaLink="true">https://zlotus.github.io/2020/12/02/first-rust-code/</guid><description>最近有水文章的需求，闲来无事也接触一下年轻貌美的Rust。</description><pubDate>Wed, 02 Dec 2020 22:17:00 GMT</pubDate><content:encoded>&lt;p&gt;最近有一些水文章的需求，而且意外的在github上看到一些巨有趣的项目，结合了&lt;strong&gt;树莓派&lt;/strong&gt;、&lt;strong&gt;Rust&lt;/strong&gt;、&lt;strong&gt;操作系统&lt;/strong&gt;等诸多元素，多厨狂喜，就准备搞一把Rust尝个鲜。&lt;/p&gt;
&lt;p&gt;其中一个项目叫做&lt;a href=&quot;https://os.phil-opp.com/&quot;&gt;Writing an OS in Rust&lt;/a&gt;，有&lt;a href=&quot;https://os.phil-opp.com/zh-CN/&quot;&gt;中文&lt;/a&gt;版本，但是翻译的不多。英文的我大概看了，深入浅出的在x86设备上编写操作系统，有很多操作系统相关基础的文字介绍。&lt;/p&gt;
&lt;p&gt;另一个项目叫做&lt;a href=&quot;https://github.com/rust-embedded/rust-raspberrypi-OS-tutorials&quot;&gt;Operating System development tutorials in Rust on the Raspberry Pi&lt;/a&gt;。从readme来看，是一个给ARM64位ARMv8-A架构的操作系统业余爱好者进行入门的一个类教程项目。项目用docker来抹平可能存在的工具链问题，推荐在Linux（据说macOS这种unix也可以）里练手。里面还提到了在宿主机上用QEMU模拟内核进行编码并编译，后面还会通过串口和UART在树莓派实操。&lt;/p&gt;
&lt;p&gt;还有个更神的&lt;a href=&quot;https://github.com/belowthetree/TisuOS&quot;&gt;太素 TisuOS&lt;/a&gt;，这是在RISC-V上的教程型操作系统，国人创作，后期可以试试。&lt;/p&gt;
&lt;p&gt;可能因为我最近极度崇拜稚晖君，因此，想以一个CS的身份向EE方向龟速考虑，日拱一卒吧……&lt;/p&gt;
&lt;h1&gt;1 Rust环境&lt;/h1&gt;
&lt;p&gt;安装官方推荐方法&lt;code&gt;curl --proto &apos;=https&apos; --tlsv1.2 -sSf https://sh.rustup.rs | sh&lt;/code&gt;，安装rust全家桶（包括包管理器及工具链管理器&lt;code&gt;rustup&lt;/code&gt;和一些环境变量配置）。&lt;/p&gt;
&lt;p&gt;宿主机macOS，IDE选用不要钱的vscode，插件装上&lt;code&gt;Rust&lt;/code&gt;或&lt;code&gt;rust-analyzer&lt;/code&gt;、&lt;code&gt;codelldb&lt;/code&gt;（调试）后，基本环境组装完毕（再夸一句不要钱的vscode）。（如果lldb安装时下载不给力，可以手动下然后使用&lt;code&gt;cmd+shift+p&lt;/code&gt;，搜索入&lt;code&gt;Extensions: Install from VSIX&lt;/code&gt;离线安装&lt;code&gt;*.vsix&lt;/code&gt;文件。）&lt;/p&gt;
&lt;p&gt;配置debug参数，vscode里&lt;code&gt;cmd+shift+P&lt;/code&gt;调出菜单，输入&lt;code&gt;debug&lt;/code&gt;选择&lt;code&gt;调试 打开launch.json&lt;/code&gt;配置调试参数，vscode自动新建该&lt;code&gt;.vscode/lanuch.json&lt;/code&gt;文件并初始化配置（这时候就用到codelldb了）。&lt;/p&gt;
&lt;p&gt;在vscode中启动调试时，vscode会自动安装调试组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rustup component add rust-analysis --toolchain stable-x86_64-apple-darwin &amp;lt;
info: component &apos;rust-analysis&apos; for target &apos;x86_64-apple-darwin&apos; is up to date

rustup component add rust-src --toolchain stable-x86_64-apple-darwin &amp;lt;
info: component &apos;rust-src&apos; is up to date

rustup component add rls --toolchain stable-x86_64-apple-darwin &amp;lt;
info: installing component &apos;rls&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个典型的配置类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    // 使用 IntelliSense 了解相关属性。
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息，请访问: https://go.microsoft.com/fwlink/?linkid=830387
    &quot;version&quot;: &quot;0.2.0&quot;,
    &quot;configurations&quot;: [
        {
            &quot;type&quot;: &quot;lldb&quot;,
            &quot;request&quot;: &quot;launch&quot;,
            &quot;name&quot;: &quot;Launch&quot;,
            &quot;args&quot;: [],
            &quot;program&quot;: &quot;${workspaceFolder}/target/debug/${workspaceFolderBasename}&quot;,
            &quot;cwd&quot;: &quot;${workspaceFolder}&quot;,
            &quot;stopOnEntry&quot;: false,
            &quot;sourceLanguages&quot;: [&quot;rust&quot;],
            &quot;sourceMap&quot;: {
                &quot;/rustc/*&quot;: &quot;${env:HOME}/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust&quot;
            }
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;cmd+shift+b&lt;/code&gt;进行cargo build，然后&lt;code&gt;F5&lt;/code&gt;启动调试。（解释性语言用多了会忘记build……）&lt;/p&gt;
&lt;p&gt;注：后面在使用中发现，rust-analyzer每次更新的时候重新安装插件会很慢，建议科学上网，比如直接挂在有openwrt的wifi上，因为系统全局代理貌似在vscode升级的时候不起作用。&lt;/p&gt;
&lt;h1&gt;2 实录&lt;/h1&gt;
&lt;p&gt;花了一周的空闲时间大概看了一下&lt;a href=&quot;https://www.runoob.com/rust/rust-tutorial.html&quot;&gt;runoob&lt;/a&gt;和&lt;a href=&quot;https://www.twle.cn/c/yufei/rust/rust-basic-index.html&quot;&gt;简单教程&lt;/a&gt;的rust基础知识。把里面的代码基本打了一遍，了解了一下写rust的感觉。也大概摸清了在vscode下写rust的一般步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cargo new xxx&lt;/code&gt;创建项目；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cmd + shift + B&lt;/code&gt;打开build快捷菜单，点后面的齿轮按钮；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时vscode会创建&lt;code&gt;.vscode/task.json&lt;/code&gt;文件，用于显式定义build任务，该文件定义的初始任务只有&lt;code&gt;cargo build&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;我们一般还需要使用&lt;code&gt;cargo check&lt;/code&gt;和&lt;code&gt;cargo run&lt;/code&gt;，只需要复制初始task并替换&lt;code&gt;command&lt;/code&gt;字段就可以实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;version&quot;: &quot;2.0.0&quot;,
    &quot;tasks&quot;: [
        {
            &quot;type&quot;: &quot;cargo&quot;,
            &quot;command&quot;: &quot;build&quot;,
            &quot;problemMatcher&quot;: [
                &quot;$rustc&quot;
            ],
            &quot;group&quot;: &quot;build&quot;,
            &quot;label&quot;: &quot;rust: cargo build&quot;
        },
        {
            &quot;type&quot;: &quot;cargo&quot;,
            &quot;command&quot;: &quot;run&quot;,
            &quot;problemMatcher&quot;: [
                &quot;$rustc&quot;
            ],
            &quot;group&quot;: &quot;build&quot;,
            &quot;label&quot;: &quot;rust: cargo run&quot;
        }
    ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;接下来啃官方教程 &lt;a href=&quot;https://kaisery.github.io/trpl-zh-cn/title-page.html&quot;&gt;中文&lt;/a&gt; | &lt;a href=&quot;https://doc.rust-lang.org/book/title-page.html&quot;&gt;英文&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;必须看官方教程，决不能只看速通tutorial，这个语言的细节感觉并不比C/C++系少多少。如果看中文版有不懂的地方，建议移步原文……&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;ch02-00：介绍安装和更新（&lt;code&gt;cargo update&lt;/code&gt;）dependency，以及通过Result处理无效输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let guess: u32 = match guess.trim().parse() {
    Ok(num) =&amp;gt; num,
    Err(_) =&amp;gt; continue,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch03-01：shadowing常用于重复利用变量名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let spaces = &quot;   &quot;;
let spaces = spaces.len();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch03-02：关于元组的一个细节，如果元组数据均为基础类型，则该元组的内容均存放在栈上，进行赋值时，如&lt;code&gt;let tup2 = tup1&lt;/code&gt;，元组内容会在栈上复制一份新的给&lt;code&gt;tup2&lt;/code&gt;。而如果元组数据类型包含非基础类型，则该院组的内容将存放在堆上，此时赋值就相当于移动（move），即&lt;code&gt;tup2&lt;/code&gt;仅包含&lt;code&gt;tup1&lt;/code&gt;内容的指针，移动后&lt;code&gt;tup1&lt;/code&gt;失效，因为内容的所有权转移给了&lt;code&gt;tup2&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch03-03：Rust是一门基于表达式（expression-based）的语言，表达式有返回值，语句则没有返回值，因此可以使用代码块和包裹表达式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let y = {
    let x = 3;
    x + 1
};
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch03-05：三元运算符的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let number = if condition { 5 } else { 6 };
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch04-01：Rust的基础类型和复杂类型的指针是存放于栈上的，复杂类型本身存在于堆上。栈内的内容赋值时（如&lt;code&gt;let int1 = int2&lt;/code&gt;）会拷贝，因为栈编译时大小确定所以快；而堆内的内容赋值时（如&lt;code&gt;let string1 = string2&lt;/code&gt;）默认不拷贝（Rust的设计就是不会主动进行深拷贝，因此Rust提供的自动复制对运行时效率影响都很小），只拷贝其栈内指针，并且使原来的变量失效（即&lt;code&gt;string1&lt;/code&gt;失效），以避免二次释放等bug。很明确，基础类型的赋值操作会自动赋值，基础类型的各种组合元组也延续这一特性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch04-02：Rust的引用作用域是首次声明到最后一次使用，因此在只读引用的最后一次使用后，再进行可变引用，是可以通过编译的。也就是说，只要只读引用和可变引用的作用域不重叠，就没有问题。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch04-03：&lt;code&gt;&amp;amp;str&lt;/code&gt;就是slice，这是不可变引用。（Rust的这种把问题消灭在编译中的特性，是不是也顺便消灭了一批有问题的程序员……）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch05-01：通常在结构体中不使用引用，如果使用引用，则需要用生命周期标识，以确保结构体引用的数据有效性跟结构体本身保持一致。没有任何字段的类单元结构体被称为类单元结构体（unit-like structs）因为它们类似于&lt;code&gt;()&lt;/code&gt;，即unit类型。类单元结构体常常在你想要在某个类型上实现trait但不需要在类型中存储数据的时候发挥作用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch05-02：想要通过&lt;code&gt;println!(&quot;{}&quot;, xxx)&lt;/code&gt;打印结构体，需要实现&lt;code&gt;std::fmt::Display&lt;/code&gt;；&lt;/p&gt;
&lt;p&gt;想要通过&lt;code&gt;println!(&quot;{:?}&quot;, xxx)&lt;/code&gt;打印结构体，需要实现&lt;code&gt;std::fmt::Debug&lt;/code&gt;，或手动为结构体加上&lt;code&gt;#[derive(Debug)]&lt;/code&gt;注解，此时打印出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rect1 is Rectangle { width: 30, height: 50 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用pretty-print风格&lt;code&gt;println!(&quot;{:#?}&quot;, xxx)&lt;/code&gt;，此时打印出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rect1 is Rectangle {
    width: 30,
    height: 50
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rust提供了很多可以使用derive注解来使用的trait。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch05-03：C++中的静态函数（使用&lt;code&gt;::&lt;/code&gt;调用），即附着在类而不是类实例上的函数，在Rust中称为关联函数，&lt;code&gt;impl&lt;/code&gt;中不以&lt;code&gt;&amp;amp;self&lt;/code&gt;作为第一个参数的函数均为关联函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch06-01：枚举类型的定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Message {
    Quit,                       // 没有包含任何数据
    Move { x: i32, y: i32 },    // 包含匿名结构体
    Write(String),              // 包含单独一个`String`
    ChangeColor(i32, i32, i32), // 包含三个 i32
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch06-03：&lt;code&gt;if let...else...&lt;/code&gt;常用于代替&lt;code&gt;match..._...&lt;/code&gt;的情况：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let mut count = 0;
match coin {
    Coin::Quarter(state) =&amp;gt; println!(&quot;State quarter from {:?}!&quot;, state),
    _ =&amp;gt; count += 1,
}
// ↓↓↓↓↓↓↓↓↓↓ 使用`if let`代替 ↓↓↓↓↓↓↓↓↓↓
let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!(&quot;State quarter from {:?}!&quot;, state);
} else {
    count += 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch07-02：新建二进制项目用&lt;code&gt;cargo new xxx&lt;/code&gt;，生成&lt;code&gt;src/main.rs&lt;/code&gt;文件为bin执行入口；新建库项目用&lt;code&gt;cargo new --lib xxx&lt;/code&gt;，生成&lt;code&gt;src/lib.rs&lt;/code&gt;为lib编译入口；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch07-03：公有&lt;strong&gt;结构体&lt;/strong&gt;若包含私有字段，则应当提供公有关系函数（&lt;code&gt;::&lt;/code&gt;）来构造实例，因为无法在外部初始化私有变量；而&lt;strong&gt;枚举&lt;/strong&gt;一旦公有，则所有成员均为公有。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch07-04：&lt;code&gt;pub use&lt;/code&gt;可以将导入的模块变为公有，供外部调用的模块使用，单纯&lt;code&gt;use&lt;/code&gt;是私有的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch08-01：虽说&lt;code&gt;vector&amp;lt;T&amp;gt;&lt;/code&gt;只能包含类型&lt;code&gt;T&lt;/code&gt;，但是也可以用枚举类型向vector里塞多种类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from(&quot;blue&quot;)),
    SpreadsheetCell::Float(10.12),
];
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch09-02：&lt;code&gt;unwrap&lt;/code&gt;返回正常的&lt;code&gt;Ok&lt;/code&gt;中的值或触发&lt;code&gt;panic!&lt;/code&gt;，&lt;code&gt;expect&lt;/code&gt;类似，只不过可以通过&lt;code&gt;expect&lt;/code&gt;自定义错误信息（unwrap只有默认信息，有多个unwrap时容易混淆）。
使用&lt;code&gt;unwrap_or_else&lt;/code&gt;简化&lt;code&gt;match&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open(&quot;hello.txt&quot;);

    let f = match f {
        Ok(file) =&amp;gt; file,
        Err(error) =&amp;gt; match error.kind() {
            ErrorKind::NotFound =&amp;gt; match File::create(&quot;hello.txt&quot;) {
                Ok(fc) =&amp;gt; fc,
                Err(e) =&amp;gt; panic!(&quot;Problem creating the file: {:?}&quot;, e),
            },
            other_error =&amp;gt; panic!(&quot;Problem opening the file: {:?}&quot;, other_error),
        },
    };
}
// ↓↓↓↓↓↓↓↓↓↓ 更老练的Rustacean会这么写 ↓↓↓↓↓↓↓↓↓↓
fn main() {
    let f = File::open(&quot;hello.txt&quot;).unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create(&quot;hello.txt&quot;).unwrap_or_else(|error| {
                panic!(&quot;Problem creating the file: {:?}&quot;, error);
            })
        } else {
            panic!(&quot;Problem opening the file: {:?}&quot;, error);
        }
    });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用&lt;code&gt;?&lt;/code&gt;简化&lt;code&gt;match&lt;/code&gt;（注意：&lt;code&gt;?&lt;/code&gt;运算符只能用在返回值是&lt;code&gt;Result&lt;/code&gt;类型的函数中，用来传播异常）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::io::{self, Read};
use std::fs::File;

fn read_username_from_file() -&amp;gt; Result&amp;lt;String, io::Error&amp;gt; {
    let f = File::open(&quot;hello.txt&quot;);

    let mut f = match f {
        Ok(file) =&amp;gt; file,
        Err(e) =&amp;gt; return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&amp;amp;mut s) {
        Ok(_) =&amp;gt; Ok(s),
        Err(e) =&amp;gt; Err(e),
    }
}
// ↓↓↓↓↓↓↓↓↓↓ 使用`?`简化 ↓↓↓↓↓↓↓↓↓↓
fn read_username_from_file() -&amp;gt; Result&amp;lt;String, io::Error&amp;gt; {
    let mut f = File::open(&quot;hello.txt&quot;)?;
    let mut s = String::new();
    f.read_to_string(&amp;amp;mut s)?;
    Ok(s)
}
// ↓↓↓↓↓↓↓↓↓↓ 链式`?`再简化 ↓↓↓↓↓↓↓↓↓↓
fn read_username_from_file() -&amp;gt; Result&amp;lt;String, io::Error&amp;gt; {
    let mut s = String::new();

    File::open(&quot;hello.txt&quot;)?.read_to_string(&amp;amp;mut s)?;

    Ok(s)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch10-02：几种trait声明写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 普通公有trait
pub trait Summary {
    fn summarize(&amp;amp;self) -&amp;gt; String;
}
// 带默认实现
pub trait Summary {
    fn summarize(&amp;amp;self) -&amp;gt; String {
        String::from(&quot;(Read more...)&quot;)
    }
}
// trait中默认实现调用其他函数
pub trait Summary {
    fn summarize_author(&amp;amp;self) -&amp;gt; String;
    fn summarize(&amp;amp;self) -&amp;gt; String {
        format!(&quot;(Read more from {}...)&quot;, self.summarize_author())
    }
}
// impl trait语法，trait作为参数
pub fn notify(item: impl Summary) {
    println!(&quot;Breaking news! {}&quot;, item.summarize());
}
// ↓↓↓↓↓↓↓↓↓↓ trait bound语法
pub fn notify&amp;lt;T: Summary&amp;gt;(item: T) {
    println!(&quot;Breaking news! {}&quot;, item.summarize());
}

// 多个impl trait
pub fn notify(item1: impl Summary, item2: impl Summary) {}
// ↓↓↓↓↓↓↓↓↓↓ 使用trait bound语法强行指定同类型参数
pub fn notify&amp;lt;T: Summary&amp;gt;(item1: T, item2: T) {}

// + 同时指定多个trait
pub fn notify(item: impl Summary + Display) {}
// ↓↓↓↓↓↓↓↓↓↓ + 同时指定多个trait的trait bound
pub fn notify&amp;lt;T: Summary + Display&amp;gt;(item: T) {}

// 复杂trait bound
fn some_function&amp;lt;T: Display + Clone, U: Clone + Debug&amp;gt;(t: T, u: U) -&amp;gt; i32 {}
// ↓↓↓↓↓↓↓↓↓↓ where简化复杂trait bound函数签名
fn some_function&amp;lt;T, U&amp;gt;(t: T, u: U) -&amp;gt; i32
    where T: Display + Clone,
        U: Clone + Debug
{}
// 函数返回值为trait
// 不能直接通过如if/else等方法在同一个函数中返回不同的均实现Summary trait的类
// 如if =&amp;gt; Tweet else =&amp;gt; NewsArticle
fn returns_summarizable() -&amp;gt; impl Summary {
    Tweet {
        username: String::from(&quot;horse_ebooks&quot;),
        // --snip--
    }
}
// 为符合特定条件的泛型实现方法
struct Pair&amp;lt;T&amp;gt; {
    x: T,
    y: T,
}
// 为所有泛型实现new方法
impl&amp;lt;T&amp;gt; Pair&amp;lt;T&amp;gt; {
    fn new(x: T, y: T) -&amp;gt; Self {
        Self {
            x,
            y,
        }
    }
}
// 为满足Display和PartialOrd trait的类实现cmp_display方法
impl&amp;lt;T: Display + PartialOrd&amp;gt; Pair&amp;lt;T&amp;gt; {
    fn cmp_display(&amp;amp;self) {
        if self.x &amp;gt;= self.y {
            println!(&quot;The largest member is x = {}&quot;, self.x);
        } else {
            println!(&quot;The largest member is y = {}&quot;, self.y);
        }
    }
}
// 为实现了特定trait的类有条件的实现trait
impl&amp;lt;T: Display&amp;gt; ToString for T {}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch10-03：生命周期标注确实抽象……&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先，生命周期一般出现在变量借用环节；&lt;/li&gt;
&lt;li&gt;其次，函数的生命周期标注只是为了告诉借用检查器，各借用变量的生命周期长短关系，并不会改变函数行为；&lt;/li&gt;
&lt;li&gt;再次，类似“协变”概念，为方便可以理解为&lt;strong&gt;长生命周期是短生命周期的子类&lt;/strong&gt;。类似面向对象中里氏代换的概念——“接受父类型的位置，子类型也可以占位”，对类型而言，范围大类型是范围小类型的父类，比如&lt;code&gt;i32&lt;/code&gt;是&lt;code&gt;i16&lt;/code&gt;的父类；那么对生命周期而言长生命周期的变量是短生命周期变量的子类，也就是活得长的变量可以代替活的短的变量的位置，这样就能避免引用参数过早释放；&lt;/li&gt;
&lt;li&gt;最后，生命周期一般标注在输入（参数）和输出（返回值）有关系的变量（参数或返回值）上，无关变量即使是借用也可以不标注。&lt;/li&gt;
&lt;li&gt;函数或方法的参数的生命周期被称为 输入生命周期（input lifetimes），而返回值的生命周期被称为 输出生命周期（output lifetimes）。&lt;/li&gt;
&lt;li&gt;编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期，后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用，编译器将会停止并生成错误。这些规则适用于&lt;code&gt;fn&lt;/code&gt;定义，以及&lt;code&gt;impl&lt;/code&gt;块。
&lt;ol&gt;
&lt;li&gt;第一条规则是每一个是引用的参数都有它自己的生命周期参数。换句话说就是，有一个引用参数的函数有一个生命周期参数：&lt;code&gt;fn foo&amp;lt;&apos;a&amp;gt;(x: &amp;amp;&apos;a i32)&lt;/code&gt;，有两个引用参数的函数有两个不同的生命周期参数，&lt;code&gt;fn foo&amp;lt;&apos;a, &apos;b&amp;gt;(x: &amp;amp;&apos;a i32, y: &amp;amp;&apos;b i32)&lt;/code&gt;，依此类推。&lt;/li&gt;
&lt;li&gt;第二条规则是如果只有一个输入生命周期参数，那么它被赋予所有输出生命周期参数：&lt;code&gt;fn foo&amp;lt;&apos;a&amp;gt;(x: &amp;amp;&apos;a i32) -&amp;gt; &amp;amp;&apos;a i32&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是&lt;code&gt;&amp;amp;self&lt;/code&gt;或&lt;code&gt;&amp;amp;mut self&lt;/code&gt;，说明是个对象的方法，那么所有输出生命周期参数被赋予 &lt;code&gt;self&lt;/code&gt;的生命周期。第三条规则使得方法更容易读写，因为只需更少的符号。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch11-01：测试某函数应该在特定条件下panic时，  &lt;code&gt;expected&lt;/code&gt;的内容就是panic输出时应该包含的内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test]
#[should_panic(expected = &quot;Guess value must be less than or equal to 100&quot;)]
fn greater_than_100() {
    Guess::new(200);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用返回值为&lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;的函数进行测试，可以在函数中使用&lt;code&gt;?&lt;/code&gt;运算符，也可以方便的编写任何运算符会返回&lt;code&gt;Err&lt;/code&gt;成员的测试。不能对使用 &lt;code&gt;Result&amp;lt;T, E&amp;gt;&lt;/code&gt;的测试使用&lt;code&gt;#[should_panic]&lt;/code&gt;注解。相反应该在测试失败时直接返回&lt;code&gt;Err&lt;/code&gt;值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[test]
fn it_works() -&amp;gt; Result&amp;lt;(), String&amp;gt; {
    if 2 + 2 == 4 {
        Ok(())
    } else {
        Err(String::from(&quot;two plus two does not equal four&quot;))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch11-02：&lt;code&gt;cargo test&lt;/code&gt;选项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 单线程运行，若各测试间存资源抢占可以使用线程指定选项test-threads
cargo test -- --test-threads=1
# 测试默认会截获程序所有输出，使得测试输出更具备可读性，希望打印程序输出使用nocapture选项
cargo test -- --nocapture
# 测试可以指定测试名称
cargo test it_works
# 测试可以匹配名称，如匹配各种it开头的测试
cargo test it
# 可以在代码中使用#[ignore]注解来禁止诸如耗时测试的进行，使用ignored选项运行这些被禁止的测试注解
cargo test -- --ignored
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch11-03：测试规范：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;单元测试：单元测试与他们要测试的代码共同存放在位于&lt;code&gt;src&lt;/code&gt;目录下相同的文件中。规范是在每个文件中创建包含测试函数的&lt;code&gt;tests&lt;/code&gt;模块，并使用&lt;code&gt;#cfg(test)&lt;/code&gt;标注模块（即在test时才编译运行，build事不做以节省时间）。&lt;/li&gt;
&lt;li&gt;集成测试：应在项目根目录创建一个&lt;code&gt;tests&lt;/code&gt;目录，与&lt;code&gt;src&lt;/code&gt;同级，cargo会去找到这个文件夹中的所有文件并编译成一个个单独的crate（无需&lt;code&gt;#[cfg(test)]&lt;/code&gt;注解，&lt;code&gt;tests&lt;/code&gt;是cargo认为的特殊文件夹，只有测试是才编译）。&lt;/li&gt;
&lt;li&gt;输出依次为单元测试、集成测试、文档测试，使用&lt;code&gt;cargo test --test integration_test&lt;/code&gt;可以指定集成测试。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tests&lt;/code&gt;目录中的子目录不会被作为单独的crate编译或作为一个测试结果部分出现在测试输出中，因此，需要诸如多个测试均涉及的工具函数可以放在&lt;code&gt;tests/common/mod.rs&lt;/code&gt;中。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch12-01：&lt;code&gt;collect&lt;/code&gt;方法的返回值需要显示注明类型，因为rust无法推断出collect返回的类型，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let args: Vec&amp;lt;String&amp;gt; = env::args().collect();
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch12-03：当函数返回一个Result，失败时希望返回失败原因字符串，则泛型应有static生命周期注释，如&lt;code&gt;pub fn new(mut args: std::env::Args) -&amp;gt; Result&amp;lt;Config, &amp;amp;&apos;static str&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch13-01：闭包可以通过三种方式捕获其环境，他们直接对应函数的三种获取参数的方式：获取所有权，可变借用和不可变借用。这三种捕获值的方式被编码为如下三个&lt;code&gt;Fn&lt;/code&gt;trait：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FnOnce&lt;/code&gt;消费从周围作用域捕获的变量，闭包周围的作用域被称为其&lt;strong&gt;环境&lt;/strong&gt;。为了消费捕获到的变量，闭包必须获取其所有权并在定义闭包时将其移动进闭包。其名称的&lt;strong&gt;Once&lt;/strong&gt;部分代表了闭包不能多次获取相同变量的所有权的事实，所以它只能被调用一次。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FnMut&lt;/code&gt;获取可变的借用值所以可以改变其环境。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Fn&lt;/code&gt;从其环境获取不可变的借用值
大部分需要指定一个&lt;code&gt;Fn&lt;/code&gt;系列trait bound的时候，可以从&lt;code&gt;Fn&lt;/code&gt;开始，而编译器会根据闭包体中的情况告诉你是否需要&lt;code&gt;FnMut&lt;/code&gt;或&lt;code&gt;FnOnce&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch13-03：函数式编程风格倾向于最小化可变状态的数量来使代码更简洁：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub fn search&amp;lt;&apos;a&amp;gt;(query: &amp;amp;str, contents: &amp;amp;&apos;a str) -&amp;gt; Vec&amp;lt;&amp;amp;&apos;a str&amp;gt; {
    let mut results = Vec::new();
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }
    results
}
// ↓↓↓↓↓↓↓↓↓↓ 通过filter省略result中间变量
pub fn search&amp;lt;&apos;a&amp;gt;(query: &amp;amp;str, contents: &amp;amp;&apos;a str) -&amp;gt; Vec&amp;lt;&amp;amp;&apos;a str&amp;gt; {
    contents.lines().filter(|line: &amp;amp;&amp;amp;str| line.contains(query)).collect()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch14-02：使用重导出功能可以重新组织希望暴露在外的API，编写者和使用者可以面对不同的API结构，如此，使用者无需了解crate内部结构即可方便的调用公开的API：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch14-03：按照教程做，添加&lt;code&gt;rand&lt;/code&gt;依赖时，保存后vscode自动运行&lt;code&gt;cargo check&lt;/code&gt;，然后显示&quot;fetch metadata&quot;很长时间不动，手动运行&lt;code&gt;cargo metadata&lt;/code&gt;，发现卡在&lt;code&gt;Updating crates.io index&lt;/code&gt;下载很慢，只好祭出替换镜像源大法……在&lt;code&gt;/Users/xxx/.cargo/&lt;/code&gt;目录下新建&lt;code&gt;config&lt;/code&gt;文件，添加内容替换中科大镜像源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[source.crates-io]
registry = &quot;https://github.com/rust-lang/crates.io-index&quot;
replace-with = &apos;ustc&apos;
[source.ustc]
registry = &quot;git://mirrors.ustc.edu.cn/crates.io-index&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch15-01：除了数据被储存在堆上而不是栈上之外，box 没有性能损失。不过也没有很多额外的功能。它们多用于如下场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当有一个在编译时未知大小的类型，而又想要在需要确切大小的上下文中使用这个类型值的时候。&lt;/li&gt;
&lt;li&gt;当有大量数据并希望在确保数据不被拷贝的情况下转移所有权的时候。&lt;/li&gt;
&lt;li&gt;当希望拥有一个值并只关心它的类型是否实现了特定 trait 而不是其具体类型的时候。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch15-02：Rust 在发现类型和 trait 实现满足三种情况时会进行解引用强制多态：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当&lt;code&gt;T: Deref&amp;lt;Target=U&amp;gt;&lt;/code&gt;时（即当&lt;code&gt;T&lt;/code&gt;实现了&lt;code&gt;Deref&lt;/code&gt;返回&lt;code&gt;U&lt;/code&gt;时），从&lt;code&gt;&amp;amp;T&lt;/code&gt;到&lt;code&gt;&amp;amp;U&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;T: DerefMut&amp;lt;Target=U&amp;gt;&lt;/code&gt;时从&lt;code&gt;&amp;amp;mut T&lt;/code&gt;到&lt;code&gt;&amp;amp;mut U&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;当&lt;code&gt;T: Deref&amp;lt;Target=U&amp;gt;&lt;/code&gt;时从&lt;code&gt;&amp;amp;mut T&lt;/code&gt;到&lt;code&gt;&amp;amp;U&lt;/code&gt;。（只允许将可变借用转为不可变借用，反之则禁止）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch15-06：&lt;code&gt;Rc::clone&lt;/code&gt;使&lt;code&gt;Rc::strong_count&lt;/code&gt;加一，&lt;code&gt;Rc::downgrade&lt;/code&gt;使&lt;code&gt;Rc::weak_count&lt;/code&gt;加一。强引用共享不可变所有权，弱引用不涉及所有权。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch16-01：rust标准库仅提供1:1线程，即语言线程映射系统线程，不存在运行时的线程管理损耗。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch16-02：&lt;code&gt;mpsc&lt;/code&gt;(multiple producer, single consumer)的&lt;code&gt;channel&lt;/code&gt;通过&lt;code&gt;mpsc::Sender::clone(&amp;amp;tx)&lt;/code&gt;来实现多生产者：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let (tx, rx) = mpsc::channel();

let tx1 = mpsc::Sender::clone(&amp;amp;tx);
thread::spawn(move || {
    let vals = vec![
        String::from(&quot;hi&quot;),
        String::from(&quot;from&quot;),
        String::from(&quot;the&quot;),
        String::from(&quot;thread&quot;),
    ];

    for val in vals {
        tx1.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

thread::spawn(move || {
    let vals = vec![
        String::from(&quot;more&quot;),
        String::from(&quot;messages&quot;),
        String::from(&quot;for&quot;),
        String::from(&quot;you&quot;),
    ];

    for val in vals {
        tx.send(val).unwrap();
        thread::sleep(Duration::from_secs(1));
    }
});

for received in rx {
    println!(&quot;Got: {}&quot;, received);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch16-03：十五章中使用&lt;code&gt;Rc::new(RefCell::new(x))&lt;/code&gt;提供&lt;code&gt;Rc&lt;/code&gt;内部值的可变引用，类似的，多线程中使用&lt;code&gt;Arc::new(Mutex::new(x))&lt;/code&gt;提供&lt;code&gt;Arc&lt;/code&gt;内部值的可变引用。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch16-04：&lt;code&gt;Send&lt;/code&gt;（允许在线程间转移所有权）和&lt;code&gt;Sync&lt;/code&gt;（允许多线程访问）是标记trait，基本类型均具有该标记，有该标记的类型的复合类型也具有该标记，而&lt;code&gt;Rc&lt;/code&gt;、&lt;code&gt;RefCell&lt;/code&gt;等智能指针并不具有该标记，&lt;code&gt;Arc&lt;/code&gt;、&lt;code&gt;Mutex&lt;/code&gt;具有该标记。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch17-03：&lt;code&gt;Option.take&lt;/code&gt;将获取&lt;code&gt;Some&lt;/code&gt;中资源的所有权，然后为&lt;code&gt;Option&lt;/code&gt;留下一个&lt;code&gt;None&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct Post {
    state: Option&amp;lt;Box&amp;lt;dyn State&amp;gt;&amp;gt;,
    content: String,
}

if let Some(s) = self.state.take() {
    self.state = Some(s.request_review())
}
// 区别于下面unwrap实现，unwrap()会获取state的所有权并消费掉state，而这里的state是在可变引用self后的，
// 所以unwrap无法获取state的所有权，自然无法消费这个state，也就无法通过编译了
// 而take无需state的所有权，只需要从state引用中拿到Some中的内容，即Box指针的所有权并赋给s，同时给state这个Option类型留下一个None
self.state = Some(self.state.unwrap().request_review());
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch18-03：match匹配的其他类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 逻辑或
match x {
    1 | 2 =&amp;gt; println!(&quot;one or two&quot;),
    3 =&amp;gt; println!(&quot;three&quot;),
    _ =&amp;gt; println!(&quot;anything&quot;),
}
// 匹配范围：仅能匹配数字和char类型
match x {
    1..=5 =&amp;gt; println!(&quot;one through five&quot;),
    _ =&amp;gt; println!(&quot;something else&quot;),
}
match x {
    &apos;a&apos;..=&apos;j&apos; =&amp;gt; println!(&quot;early ASCII letter&quot;),
    &apos;k&apos;..=&apos;z&apos; =&amp;gt; println!(&quot;late ASCII letter&quot;),
    _ =&amp;gt; println!(&quot;something else&quot;),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
// 或
let p = Point { x: 0, y: 7 };
let Point { a, b } = p;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构并匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let p = Point { x: 0, y: 7 };
match p {
    Point { x, y: 0 } =&amp;gt; println!(&quot;On the x axis at {}&quot;, x),
    Point { x: 0, y } =&amp;gt; println!(&quot;On the y axis at {}&quot;, y),
    Point { x, y } =&amp;gt; println!(&quot;On neither axis: ({}, {})&quot;, x, y),
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构枚举：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::ChangeColor(0, 160, 255);

    match msg {
        Message::Quit =&amp;gt; {
            println!(&quot;The Quit variant has no data to destructure.&quot;)
        }
        Message::Move { x, y } =&amp;gt; {
            println!(
                &quot;Move in the x direction {} and in the y direction {}&quot;,
                x,
                y
            );
        }
        Message::Write(text) =&amp;gt; println!(&quot;Text message: {}&quot;, text),
        Message::ChangeColor(r, g, b) =&amp;gt; {
            println!(
                &quot;Change the color to red {}, green {}, and blue {}&quot;,
                r,
                g,
                b
            )
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构嵌套的枚举和结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) =&amp;gt; {
            println!(
                &quot;Change the color to red {}, green {}, and blue {}&quot;,
                r,
                g,
                b
            )
        }
        Message::ChangeColor(Color::Hsv(h, s, v)) =&amp;gt; {
            println!(
                &quot;Change the color to hue {}, saturation {}, and value {}&quot;,
                h,
                s,
                v
            )
        }
        _ =&amp;gt; ()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解构嵌套的结构体和元组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;变量命名使用下划线开头时，即使不使用该变量编译器也不会警告“变量未使用”。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;..&lt;/code&gt;忽略剩余值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let origin = Point { x: 0, y: 0, z: 0 };
match origin {
    Point { x, .. } =&amp;gt; println!(&quot;x is {}&quot;, x),
}
// 或
let numbers = (2, 4, 8, 16, 32);
match numbers {
    (first, .., last) =&amp;gt; {
        println!(&quot;Some numbers: {}, {}&quot;, first, last);
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;匹配守卫&lt;/strong&gt;（match guard）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 输出 less than five: 4
let num = Some(4);
match num {
    Some(x) if x &amp;lt; 5 =&amp;gt; println!(&quot;less than five: {}&quot;, x),
    Some(x) =&amp;gt; println!(&quot;{}&quot;, x),
    None =&amp;gt; (),
}
// 使用匹配守卫解决match会引入新作用域的问题，match中的y覆盖了外部的y
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) =&amp;gt; println!(&quot;Got 50&quot;),
        Some(y) =&amp;gt; println!(&quot;Matched, y = {:?}&quot;, y),
        _ =&amp;gt; println!(&quot;Default case, x = {:?}&quot;, x),
    }

    println!(&quot;at the end: x = {:?}, y = {:?}&quot;, x, y);
}
// ↓↓↓↓↓↓↓↓↓↓ 利用匹配守卫就可以用到外部的y ↓↓↓↓↓↓↓↓↓↓
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) =&amp;gt; println!(&quot;Got 50&quot;),
        Some(n) if n == y =&amp;gt; println!(&quot;Matched, n = {}&quot;, n),
        _ =&amp;gt; println!(&quot;Default case, x = {:?}&quot;, x),
    }

    println!(&quot;at the end: x = {:?}, y = {}&quot;, x, y);
}
// 匹配守卫结合或条件，意为此分支值匹配x值为4、5或6同时y为true的情况，本例中y为false所以不会匹配首个条件：
let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y =&amp;gt; println!(&quot;yes&quot;),
    _ =&amp;gt; println!(&quot;no&quot;),
}
``
`
`@`运算符，本例希望测试`Message::Hello`的`id`字段是否位于`3..=7`范围内，同时也希望能将其值绑定到`id_variable`变量中以便此分支相关联的代码可以使用它（可以将`id_variable`命名为`id`，与字段同名，不过出于示例的目的这里选择了不同的名称）：

```rust
enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello { id: id_variable @ 3..=7 } =&amp;gt; {
        println!(&quot;Found an id in range: {}&quot;, id_variable)
    },
    Message::Hello { id: 10..=12 } =&amp;gt; {
        println!(&quot;Found an id in another range&quot;)
    },
    Message::Hello { id } =&amp;gt; {
        println!(&quot;Found some other id: {}&quot;, id)
    },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch19-01：&lt;code&gt;unsafe&lt;/code&gt;的用法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 不会崩溃的用法，因为裸指针范围恰好不重叠
use std::slice;

fn split_at_mut(slice: &amp;amp;mut [i32], mid: usize) -&amp;gt; (&amp;amp;mut [i32], &amp;amp;mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();

    assert!(mid &amp;lt;= len);

    unsafe {
        (slice::from_raw_parts_mut(ptr, mid),
        slice::from_raw_parts_mut(ptr.add(mid), len - mid))
    }
}
// 可能会崩溃的用法，因为i32型的裸指针向后取一万个生成的序列，所用的内存并不在这段程序的掌控之中
use std::slice;

let address = 0x01234usize;
let r = address as *mut i32;

let slice: &amp;amp;[i32] = unsafe {
    slice::from_raw_parts_mut(r, 10000)
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;extern&lt;/code&gt;调用外部代码，调用外部代码总被认为是&lt;code&gt;unsafe&lt;/code&gt;的，比如调用C语言：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extern &quot;C&quot; {
    fn abs(input: i32) -&amp;gt; i32;
}

fn main() {
    unsafe {
        println!(&quot;Absolute value of -3 according to C: {}&quot;, abs(-3));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用&lt;code&gt;extern&lt;/code&gt;编译成被外部代码调用的ABI：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[no_mangle]
pub extern &quot;C&quot; fn call_from_c() {
    println!(&quot;Just called a Rust function from C!&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;声明静态变量（全局变量），静态变量只能储存拥有&lt;code&gt;&apos;static&lt;/code&gt;生命周期的引用，这意味着 Rust 编译器可以自己计算出其生命周期而无需显式标注。访问不可变静态变量是安全的。（常量与不可变静态变量可能看起来很类似，不过一个微妙的区别是静态变量中的值有一个固定的内存地址。使用这个值总是会访问相同的地址。另一方面，常量则允许在任何被用到的时候复制其数据。常量与静态变量的另一个区别在于静态变量可以是可变的。访问和修改&lt;strong&gt;可变&lt;/strong&gt;静态变量都是&lt;strong&gt;不安全&lt;/strong&gt;的）。声明并读取不可变静态变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static HELLO_WORLD: &amp;amp;str = &quot;Hello, world!&quot;;

fn main() {
    println!(&quot;name is: {}&quot;, HELLO_WORLD);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;声明、修改并读取可变静态变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!(&quot;COUNTER: {}&quot;, COUNTER);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不安全的trait，典型场景为ch16-04中线程共享数据，如果实现了一个包含一些不是&lt;code&gt;Send&lt;/code&gt;或&lt;code&gt;Sync&lt;/code&gt;的类型（如裸指针），并希望将此实现类型标记为&lt;code&gt;Send&lt;/code&gt;或&lt;code&gt;Sync&lt;/code&gt;，则必须使用&lt;code&gt;unsafe&lt;/code&gt;。Rust不能验证我们的类型保证可以安全的跨线程发送或在多线程间访问，所以需要我们自己进行检查并通过&lt;code&gt;unsafe&lt;/code&gt;表明。：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;union&lt;/code&gt;和&lt;code&gt;struct&lt;/code&gt;类似，但是在一个实例中同时只能使用一个声明的字段。联合体主要用于和C代码中的联合体交互。访问联合体的字段是不安全的，因为 Rust 无法保证当前存储在联合体实例中数据的类型。&lt;/p&gt;
&lt;p&gt;不安全的使用场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;解引用裸指针&lt;/li&gt;
&lt;li&gt;调用不安全的函数或方法&lt;/li&gt;
&lt;li&gt;访问或修改可变静态变量&lt;/li&gt;
&lt;li&gt;实现不安全&lt;code&gt;trait&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;访问&lt;code&gt;union&lt;/code&gt;的字段&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch19-03：高级trait中的关联类型的默认类型，其典型场景为：运算符重载时，符号两侧数据类型不同的情况（如“米+毫米”）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 观察Add运算符trait的定义：
trait Add&amp;lt;RHS=Self&amp;gt; {
    type Output;

    fn add(self, rhs: RHS) -&amp;gt; Self::Output;
}
// 运算符两端的数据类型相同时，不需要显示声明`Add`右侧数据类型，默认与左侧相同：
use std::ops::Add;

#[derive(Debug, PartialEq)]
struct Point {
    x: i32,
    y: i32,
}

impl Add for Point {
    type Output = Point;

    fn add(self, other: Point) -&amp;gt; Point {
        Point {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

fn main() {
    assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
            Point { x: 3, y: 3 });
}
// 左侧类型与右侧类型不同时，才需要显示声明类型：
use std::ops::Add;

struct Millimeters(u32);
struct Meters(u32);

impl Add&amp;lt;Meters&amp;gt; for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Meters) -&amp;gt; Millimeters {
        Millimeters(self.0 + (other.0 * 1000))
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在trait中要求实现该trait的类型具有某些其他trait，类似于为trait添加&quot;trait bound&quot;，例如要求实现&lt;code&gt;OutlinePrint&lt;/code&gt;这一trait的类型具有&lt;code&gt;Display&lt;/code&gt;trait，以便我们在&lt;code&gt;outline_print()&lt;/code&gt;中调用&lt;code&gt;to_string()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::fmt;

trait OutlinePrint: fmt::Display {
    fn outline_print(&amp;amp;self) {
        let output = self.to_string();
        let len = output.len();
        println!(&quot;{}&quot;, &quot;*&quot;.repeat(len + 4));
        println!(&quot;*{}*&quot;, &quot; &quot;.repeat(len + 2));
        println!(&quot;* {} *&quot;, output);
        println!(&quot;*{}*&quot;, &quot; &quot;.repeat(len + 2));
        println!(&quot;{}&quot;, &quot;*&quot;.repeat(len + 4));
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch19-05：将函数以参数的方式传递给另一个函数，除了传递闭包外，也可以传递&lt;code&gt;fn&lt;/code&gt;定义的函数，函数的类型是&lt;code&gt;fn&lt;/code&gt;（使用小写的“f”）以免与&lt;code&gt;Fn&lt;/code&gt;闭包trait相混淆。&lt;code&gt;fn&lt;/code&gt;被称为 函数指针（function pointer）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn add_one(x: i32) -&amp;gt; i32 {
    x + 1
}

fn do_twice(f: fn(i32) -&amp;gt; i32, arg: i32) -&amp;gt; i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!(&quot;The answer is: {}&quot;, answer);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;返回一个闭包函数时，与往常一样，由于rust并不知道闭包需要多少空间来储存，因此需要返回指针包裹的闭包，如&lt;code&gt;Fn&lt;/code&gt;trait对象：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn returns_closure() -&amp;gt; Box&amp;lt;dyn Fn(i32) -&amp;gt; i32&amp;gt; {
    Box::new(|x| x + 1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch19-06：类似于&lt;code&gt;vec![1, 2, 3]&lt;/code&gt;的&lt;strong&gt;声明宏&lt;/strong&gt;（declarative macros）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 简化版的vec!宏
#[macro_export]
macro_rules! vec {
    ( $( $x:expr ),* ) =&amp;gt; {
        {
            let mut temp_vec = Vec::new();
            $(
                temp_vec.push($x);
            )*
            temp_vec
        }
    };
}
// 调用这个宏vec![1, 2, 3]生成的代码类似于：
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;过程宏&lt;/strong&gt;（procedural macros）实现较复杂，我大概率只用不写。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;类属性宏&lt;/strong&gt;，示例有两个&lt;code&gt;TokenStream&lt;/code&gt;类型的参数；第一个用于属性内容本身，也就是&lt;code&gt;GET, &quot;/&quot;&lt;/code&gt;部分。第二个是属性所标记的项：在本例中，是&lt;code&gt;fn index() {}&lt;/code&gt;和剩下的函数体。除此之外，类属性宏与自定义派生宏工作方式一致：创建 &lt;code&gt;proc-macro&lt;/code&gt; crate类型的crate并实现希望生成代码的函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[route(GET, &quot;/&quot;)]
fn index() {...}
// ↓↓↓↓↓↓↓↓↓↓ 实现 ↓↓↓↓↓↓↓↓↓↓
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -&amp;gt; TokenStream {...}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;类函数宏&lt;/strong&gt;，定义看起来像函数调用的宏。类似于&lt;code&gt;macro_rules!&lt;/code&gt;，它们比函数更灵活（例如，可以接受未知数量的参数，&lt;code&gt;macro_rules!&lt;/code&gt;宏只能使用前面&lt;code&gt;vec!&lt;/code&gt;例子中匹配语法定义）。类函数宏获取&lt;code&gt;TokenStream&lt;/code&gt;参数，其定义使用Rust代码操纵&lt;code&gt;TokenStream&lt;/code&gt;，就像另两种过程宏一样。一个类函数宏例子是可以像这样被调用的&lt;code&gt;sql!&lt;/code&gt;宏：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let sql = sql!(SELECT * FROM posts WHERE id=1);
// ↓↓↓↓↓↓↓↓↓↓ 实现 ↓↓↓↓↓↓↓↓↓↓
#[proc_macro]
pub fn sql(input: TokenStream) -&amp;gt; TokenStream {...}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;ch20-03：多线程webserver范例：目录结构大致为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hello
├── 404.html
├── Cargo.lock
├── Cargo.toml
├── hello.html
├── src
│   ├── bin
│   │   └── main.rs
│   └── lib.rs
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个范例通过实现线程模型间消息的传递、线程的结束等目标，利用了很多前面十几章提到的知识点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;利用&lt;code&gt;type&lt;/code&gt;缩写类型；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;Option&lt;/code&gt;交换所有权；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;智能指针&lt;code&gt;Arc&lt;/code&gt;与&lt;code&gt;Mutux&lt;/code&gt;在线程模型中的用法；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何在线程模型中将mpsc通道变为“spmc”；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过观察标准库中的&lt;code&gt;thread::spawn()&lt;/code&gt;，模仿着编写一个类似的接受闭包指针且能够在线程模型中使用的函数；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::{fs, net::{TcpListener, TcpStream}, thread, time::Duration};
use hello::ThreadPool;
use std::io::prelude::*;

fn main() {
    let listener = TcpListener::bind(&quot;127.0.0.1:7878&quot;).unwrap();
    let pool = ThreadPool::new(4);

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_connection(stream);
        });
    }
}

fn handle_connection(mut stream: TcpStream) {
    let mut buffer = [0; 1024];

    stream.read(&amp;amp;mut buffer).unwrap();

    let get = b&quot;GET / HTTP/1.1\r\n&quot;;
    let sleep = b&quot;GET /sleep HTTP/1.1\r\n&quot;;


    let (status_line, filename) = if buffer.starts_with(get) {
        (&quot;HTTP/1.1 200 OK\r\n\r\n&quot;, &quot;hello.html&quot;)
    } else if buffer.starts_with(sleep) {
        thread::sleep(Duration::from_secs(5));
        (&quot;HTTP/1.1 200 OK\r\n\r\n&quot;, &quot;hello.html&quot;)
    } else {
        (&quot;HTTP/1.1 404 NOT FOUND\r\n\r\n&quot;, &quot;404.html&quot;)
    };

    let contents = fs::read_to_string(filename).unwrap();

    let response = format!(&quot;{}{}&quot;, status_line, contents);

    stream.write(response.as_bytes()).unwrap();
    stream.flush().unwrap();    
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;use std::{sync::{Arc, Mutex, mpsc}, thread};

pub struct ThreadPool {
    workers: Vec&amp;lt;Worker&amp;gt;,
    sender: mpsc::Sender&amp;lt;Message&amp;gt;,
}

type Job = Box&amp;lt;dyn FnOnce() + Send + &apos;static&amp;gt;;

enum Message {
    NewJob(Job),
    Terminate,
}

impl ThreadPool {

    /// 创建线程池
    /// 
    /// `size`为线程池中线程的数量
    ///
    /// # Panics
    /// 
    /// `new`函数在`size`为`0`时会panic。
    pub fn new(size: usize) -&amp;gt; ThreadPool {
        assert!(size &amp;gt; 0);
        let mut workers = Vec::with_capacity(size);

        let (sender, receiver) = mpsc::channel();

        // Arc指针负责提供线程间复制功能，Mutex指针负责提供线程间共享数据功能。
        let receiver = Arc::new(Mutex::new(receiver));

        for id in 0..size {
            workers.push(Worker::new(id, Arc::clone(&amp;amp;receiver)));
        }

        ThreadPool {
            workers,
            sender,
        }
    }

    pub fn execute&amp;lt;F&amp;gt;(&amp;amp;self, f: F) 
        where F: FnOnce() + Send + &apos;static
    {
        let job = Box::new(f);

        self.sender.send(Message::NewJob(job)).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&amp;amp;mut self) {
        println!(&quot;Sending terminate message to all workers.&quot;);

        // 这里必须使用两个迭代发送终止消息，因为消息的发送无法针对特定worker
        // 使用同一个循环先发送终止消息再join，并不能保证收取消息的worker与join的worker是同一个worker
        for _ in &amp;amp;self.workers {
            self.sender.send(Message::Terminate).unwrap();
        }

        println!(&quot;Shutting down all workers.&quot;);

        for worker in &amp;amp;mut self.workers {
            println!(&quot;Shutting down worker {}&quot;, worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

pub struct Worker {
    id: usize,
    // 使用Option的原因在于join线程时需要拿到句柄所有权后消费掉这个句柄，Option的take方法可以拿到Some中内容的所有权同时交还一个None。
    // 若不通过Option封装，则结构体的thread字段无法交出其中的句柄所有权，因为结构体不允许thread字段悬空。
    thread: Option&amp;lt;thread::JoinHandle&amp;lt;()&amp;gt;&amp;gt;,
}

impl Worker {
    fn new(id: usize, receiver: Arc&amp;lt;Mutex&amp;lt;mpsc::Receiver&amp;lt;Message&amp;gt;&amp;gt;&amp;gt;) -&amp;gt; Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv().unwrap();

            match message {
                Message::NewJob(job) =&amp;gt; {
                    println!(&quot;Worker {} got a job; executing.&quot;, id);

                    job();
                },
                Message::Terminate =&amp;gt; {
                    println!(&quot;Worker {} was told to terminate.&quot;, id);

                    break;
                }
            }
        });

        Worker  {
            id,
            thread: Some(thread),
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>更换VPS科学球</title><link>https://zlotus.github.io/2020/11/28/first-scientific-surfing-service/</link><guid isPermaLink="true">https://zlotus.github.io/2020/11/28/first-scientific-surfing-service/</guid><description>VPS科学球中的飞机在一个月内多次被击落后，VPS科学球也被炸了……</description><pubDate>Sat, 28 Nov 2020 19:27:00 GMT</pubDate><content:encoded>&lt;p&gt;影子VPS科学球中的飞机在一个月内多次被击落后（估计主要因为反复使用9000、9001、8000、8001端口，流量也不小），VPS科学球也被炸了……这是我第一次体验炸机+炸VPS科学球事故……&lt;/p&gt;
&lt;p&gt;:::caution
仅限技术交流！
:::&lt;/p&gt;
&lt;h1&gt;1 背景环境&lt;/h1&gt;
&lt;p&gt;大概从九月中旬开始，飞机就隔三差五的被炸，我依然在作大死的不停换飞机升空。于是，到了十月底，VPS科学球被炸掉了。这是VPS科学球的废墟：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(base) ➜  ~ traceroute 45.135.xxx.xxx
traceroute to 45.135.xxx.xxx (45.135.xxx.xxx), 64 hops max, 52 byte packets
 1  10.0.6.1 (10.0.6.1)  33.186 ms  5.674 ms  7.163 ms
 2  * * *
 3  114.86.28.1 (114.86.28.1)  6.460 ms  11.830 ms  9.385 ms
 4  61.152.10.85 (61.152.10.85)  7.064 ms
    61.152.10.253 (61.152.10.253)  11.341 ms
    61.152.10.85 (61.152.10.85)  5.387 ms
 5  101.95.120.218 (101.95.120.218)  9.113 ms
    61.152.24.50 (61.152.24.50)  11.964 ms
    101.95.120.218 (101.95.120.218)  7.964 ms
 6  202.97.61.86 (202.97.61.86)  24.081 ms
    202.97.57.25 (202.97.57.25)  19.092 ms
    202.97.57.157 (202.97.57.157)  6.437 ms
 7  202.97.90.33 (202.97.90.33)  28.298 ms
    202.97.74.37 (202.97.74.37)  23.032 ms
    202.97.90.33 (202.97.90.33)  21.522 ms
 8  59.43.244.113 (59.43.244.113)  23.334 ms  22.408 ms *
 9  59.43.247.125 (59.43.247.125)  34.363 ms  52.363 ms  38.770 ms
10  59.43.246.18 (59.43.246.18)  126.581 ms *  132.564 ms
11  145.14.89.2 (145.14.89.2)  126.328 ms  129.369 ms  125.261 ms
12  * * *
...
64  * * *
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2 更换科学球服务&lt;/h1&gt;
&lt;p&gt;依然是熟悉的CentOS 7。&lt;/p&gt;
&lt;h2&gt;2.1 重建VPS科学球环境&lt;/h2&gt;
&lt;p&gt;首次登录好习惯改密码（为了方便，应该把&lt;code&gt;zsh&lt;/code&gt;、&lt;code&gt;git&lt;/code&gt;、&lt;code&gt;oh-my-zsh&lt;/code&gt;、&lt;code&gt;vim&lt;/code&gt;和&lt;code&gt;tmux&lt;/code&gt;装上。）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# VPS上改密码
passwd
# 电脑上上传秘钥进行ssh免密登录
ssh-copy-id root@46.*.*.*

sudo yum -y update &amp;amp;&amp;amp; sudo yum -y install zsh git vim tmux
chsh -s $(which zsh)
sh -c &quot;$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://wsgzao.github.io/post/ssh/&quot;&gt;将ssh设置为无法用密码登录（只接受key pair认证）&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# /etc/ssh/sshd_config 中：
PasswordAuthentication no
# 重启sshd服务：
service sshd restart
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.2 申请VPS科学球名&lt;/h2&gt;
&lt;p&gt;V家的服务需要域名服务（以下简称VPS科学球名），按照&lt;a href=&quot;https://tlanyan.me/personal-free-pp-ua-domain-tutorial/&quot;&gt;教程&lt;/a&gt;申请免费VPS科学球名。&lt;/p&gt;
&lt;p&gt;转念一想，还是整个大厂的吧，就买了个阿里云的&lt;code&gt;.tech&lt;/code&gt;域名，反正双十一感觉199十年也算不贵，又在阿里云上申请了个&lt;a href=&quot;https://tlanyan.me/get-free-ssl-certificates-from-aliyun/&quot;&gt;免费证书&lt;/a&gt;。&lt;/p&gt;
&lt;h2&gt;2.2 搭建VPS科学球&lt;/h2&gt;
&lt;p&gt;年纪大了懒得从头学了，向&lt;a href=&quot;https://www.jamesdailylife.com/v2ray-vless-tcp-xtls&quot;&gt;一键安装脚本&lt;/a&gt;（&lt;a href=&quot;https://v2raytech.com/v2ray-all-in-one-script-vless-tcp-xtls-support/&quot;&gt;一键安装脚本 已失效&lt;/a&gt;）低头，真香……&lt;/p&gt;
&lt;p&gt;这个脚本的nginx的80端口被设置了跳向一个小说网站，据说是为了伪装，有需求可以换掉（即修改&lt;code&gt;/etc/nginx/conf.d/域名.conf&lt;/code&gt;里&lt;code&gt;location&lt;/code&gt;块的&lt;code&gt;proxy_pass&lt;/code&gt;代理重定向改掉即可）。比如我把重定向直接改到了自己的&lt;code&gt;https://zlotus.github.io&lt;/code&gt;上了，还挺不错的。&lt;/p&gt;
&lt;p&gt;需要注意的是，那个一键脚本中使用的证书是&lt;a href=&quot;https://tlanyan.me/use-lets-encrypt-certificate/&quot;&gt;Let’s Encrypt&lt;/a&gt;颁发的庶民免费证书，作为阿里云付费会员，我自然需要使用会员免费证书申明自己的高贵身份[狂笑]。这里需要在v2ray配置文件中&lt;code&gt;/etc/v2ray/config.json&lt;/code&gt;把路径改到我从阿里云获得的证书：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;certificateFile&quot;: &quot;/etc/letsencrypt/live/yourname.com/fullchain.pem&quot;,
&quot;keyFile&quot;: &quot;/etc/letsencrypt/live/yourname.com/privkey.pem&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;阿里云的域名解析一开始正常了一个来小时，后面就异常了，在whois上说是域名等待实名认证的问题，再等等吧，提交了工单据说一两天就自动好了。我是上谷歌的学术派，实名什么的对我来说更好，虽然自带正值属性，依旧希望今生不喝茶。&lt;/p&gt;
&lt;h1&gt;3 客户端&lt;/h1&gt;
&lt;p&gt;据说现在客户端对vless的支持还不全面，macOS上我用了v2rayU，安卓上用了v2rayNG。（mac上我一开始选了qt的那个客户端，一个是运行有qt特有的不顺滑感，另外就是outbound_direct的流量总不正常，倒是机舱里的数据很正常，挺奇怪的，我就换了U的这个客户端）。&lt;/p&gt;
&lt;h1&gt;4 OpenWrt&lt;/h1&gt;
&lt;p&gt;老的OpenWrt-rpi貌似不支持vless，我就更新了个版本，参考&lt;a href=&quot;https://zlotus.github.io/2020/05/03/rbp3-openwrt/&quot;&gt;老文章&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;我的新版本应该是2020-11-06的nightly build，部署的时候在修改无线加密方式时遇到点小麻烦，选择&lt;code&gt;WPA2-PSK/WPA3-SAE Mixed Model&lt;/code&gt;时，设置密码重启进不去，我就重新刷了固件准备清设置，谁知道快速格式化是不行的，我低格了整个SD卡才把设置清掉，还是有点神奇的[捂脸]（估计拷个大文件覆盖也行）……后来选了&lt;code&gt;WPA-PSK/WPA2-PSK Mixed Model&lt;/code&gt;就好了。&lt;/p&gt;
&lt;p&gt;依然是配置LAN口网关（&lt;code&gt;192.168.1.1&lt;/code&gt;）、DNS（&lt;code&gt;61.134.1.4&lt;/code&gt;和&lt;code&gt;218.30.19.40&lt;/code&gt;），先让能上网&lt;/p&gt;
&lt;p&gt;新版影子的服务类型里明确有vless，填上东西就能正常科学化了。或者使用PassWall服务，里面也有vless。&lt;/p&gt;
&lt;h1&gt;5 大插曲&lt;/h1&gt;
&lt;p&gt;用了没两天，DNS解析断了，在&lt;a href=&quot;https://whois.aliyun.com/&quot;&gt;阿里的whois&lt;/a&gt;上查，说是被注册局封了，也挺迷的，持续了一个月吧。详情如下：&lt;/p&gt;
&lt;p&gt;也许是我的V型VPS科学球的自动部署脚本里有广告程序，也许是倒霉被入侵（反正我在VPS里没看到什么可疑进程&lt;code&gt;ps&lt;/code&gt;、奇怪链接&lt;code&gt;netstat&lt;/code&gt;、不对劲的文件访问&lt;code&gt;lsof&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;在阿里云上提交工单，客服反映我的域名是被被&lt;strong&gt;radix&lt;/strong&gt;注册局封了，让我直接给注册局发邮件询问，还好心的说，最好多发几封邮件，耐心等待。于是，我跟肖申克的安迪一样，开始了每天发邮件的日常任务，主要是给&lt;code&gt;abuse@radixregistry.com&lt;/code&gt;和&lt;code&gt;china@radix.email&lt;/code&gt;发邮件，不限语言。我就每天中英双语给每个邮箱各发两封邮件。&lt;/p&gt;
&lt;p&gt;二十多天后，有回复了，注册局那边解释说，我是被一个叫&lt;strong&gt;surbl&lt;/strong&gt;的网站列为域名滥用对象了，让我在surbl上提交“移除域名滥用对象”的申请。&lt;/p&gt;
&lt;p&gt;在提交申请后两三天，surbl上查确实从红变绿了，又过了一两天域名就能够正常解析了，阿里云DNS控制台也显示正常。不过又过了一天，我在surbl上看域名又变红了，目前还不知道什么情况，但是DNS解析依然正常，希望域名别再出事了……&lt;/p&gt;
&lt;h1&gt;6 总结&lt;/h1&gt;
&lt;p&gt;我一直觉得自己是个没什么耐心的人，通过这个事件才发现，原来我很有耐心，只是分对谁，目前看来只是对傻逼容忍度为零，其他都还好。祝自己肉体和心灵都能够继续保持健康，下周体检有点紧张[捂脸]~&lt;/p&gt;
</content:encoded></item><item><title>在VPS上配置内网穿透</title><link>https://zlotus.github.io/2020/09/05/vps-frps-frpc/</link><guid isPermaLink="true">https://zlotus.github.io/2020/09/05/vps-frps-frpc/</guid><description>来上海了，想在笔记本和树莓派上配置内网穿透，用手机直接访问VPS就可以了。</description><pubDate>Sat, 05 Sep 2020 09:45:00 GMT</pubDate><content:encoded>&lt;p&gt;最近老经常出差到上海，笔记本和树莓派就放上海这边办公室懒得拿回公寓，但是上面都有些想随时调出来看的文件，希望能用手机直接访问这俩设备，搜了一下准备用frps-frpc做个内网穿透试试。&lt;/p&gt;
&lt;h1&gt;1 需求&lt;/h1&gt;
&lt;p&gt;用手机访问VPS特定端口后，VPS转发请求到笔记本和树莓派，就这么简单。搜到的第一篇文章就是&lt;a href=&quot;https://zhuanlan.zhihu.com/p/57477087&quot;&gt;内网穿透：在公网访问你家的 NAS&lt;/a&gt;。&lt;/p&gt;
&lt;h1&gt;2 部署frp&lt;/h1&gt;
&lt;h2&gt;2.1 部署frps&lt;/h2&gt;
&lt;p&gt;在VPS上部署服务端frps：&lt;/p&gt;
&lt;p&gt;下载页面在&lt;a href=&quot;https://github.com/fatedier/frp/releases&quot;&gt;Release · fatedier/frp&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/fatedier/frp/releases/download/v0.33.0/frp_0.33.0_linux_amd64.tar.gz
tar xvzf frp_0.33.0_linux_amd64.tar.gz
mv frp_0.33.0_linux_amd64 frp
cd frp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启防火墙，我打算用默认的&lt;code&gt;7000&lt;/code&gt;端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --permanent --add-port=7000/tcp # frps监听端口
sudo firewall-cmd --zone=public --permanent --add-port=10022/tcp # 树莓派ssh服务
sudo firewall-cmd --zone=public --permanent --add-port=10080/tcp # 笔记本http服务
firewall-cmd --reload
#firewall-cmd --list-ports
#7000/tcp 10022/tcp 10080/tcp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在VPS上配置&lt;code&gt;frps.ini&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[common]
bind_port = 7000    # frps监听端口
token = ***token***
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置启动项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln frps /usr/bin/frps
mkdir /etc/frp/
ln frps.ini /etc/frp/frps.ini

ln -s /root/frp/systemd/frps.service /usr/lib/systemd/system/
systemctl enable frps.service
systemctl start frps.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.2 部署frpc - ssh&lt;/h2&gt;
&lt;p&gt;在树莓派上部署客户端，下载并解压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/fatedier/frp/releases/download/v0.33.0/frp_0.33.0_linux_arm.tar.gz
tar xvzf frp_0.33.0_linux_arm.tar.gz
mv frp_0.33.0_linux_arm frp
cd frp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在树莓派上配置&lt;code&gt;frpc.ini&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[common]
server_addr = 45.135.xxx.xxx # VPS地址
server_port = 7000           # VPS frps监听端口
token = ***token***

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22              # 树莓派需要暴露给VPS frps的服务端口，如这里的ssh默认22端口
remote_port = 10022          # VPS frps监听10022端口，并将访问这个端口的流量转发给树莓派的22端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置启动项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ln frpc /usr/bin/frpc
sudo mkdir /etc/frp/
sudo ln frpc.ini /etc/frp/frpc.ini

sudo ln -s /home/pi/frp/systemd/frpc.service /usr/lib/systemd/system/
sudo systemctl enable frpc.service
sudo systemctl start frpc.service
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.3 部署frpc - http&lt;/h2&gt;
&lt;p&gt;在笔记本上部署客户端，下载并解压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget https://github.com/fatedier/frp/releases/download/v0.33.0/frp_0.33.0_darwin_amd64.tar.gz
tar xvzf frp_0.33.0_darwin_amd64.tar.gz
mv frp_0.33.0_darwin_amd64 frp
cd frp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在笔记本上配置&lt;code&gt;frpc.ini&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[common]
server_addr = 45.135.xxx.xxx # VPS地址
server_port = 7000           # VPS frps监听端口
token = ***token***

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 80              # 树莓派需要暴露给VPS frps的服务端口，如这里的ssh默认22端口
remote_port = 10080          # VPS frps监听10022端口，并将访问这个端口的流量转发给树莓派的22端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;笔记本就不配置启动项了，临时服务web，用完就关。另外，macOS的防火墙过于傻瓜，只能配置程序，不能配置端口，用nginx的时候得把这傻瓜墙关了。&lt;/p&gt;
&lt;h1&gt;3 测试&lt;/h1&gt;
&lt;p&gt;谷歌上搜个Termux的apk安在手机上，再Termux安装ssh（&lt;code&gt;pkg install dropbear&lt;/code&gt;），用手机登录&lt;code&gt;ssh -p10022 pi@45.135.135.76&lt;/code&gt;：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/zlotus/imagebed/blob/master/2020/vps-frps-frpc.jpg?raw=true&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;小结&lt;/h1&gt;
&lt;p&gt;frp作为内网穿透工具还有好多高级功能我都没试，有需要再尝试。另外，提交上面的手机图片时遇到一个不太常见的git使用场景：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;     B
    /
---X---A
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;一开始我提交了一张图片（即在本地库及远端github上均从X-&amp;gt;A，本地库及远端库最新为A）&lt;/li&gt;
&lt;li&gt;发现忘了给IP打码，就用&lt;code&gt;git reset --hard HEAD^&lt;/code&gt;在本地回退了（即本地从A-&amp;gt;X，而远端仍然为A）&lt;/li&gt;
&lt;li&gt;打码提交（即本地X-&amp;gt;B，本地库最新为B，而远端仍然为A）。此时远端库最新的A节点和本地库最新的B节点冲突了，直接提交会出现错误：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt; ➜  imagebed git:(master) git push -u origin master
To github.com:zlotus/imagebed.git
 ! [rejected]        master -&amp;gt; master (non-fast-forward)
error: 推送一些引用到 &apos;github.com:zlotus/imagebed.git&apos; 失败
提示：更新被拒绝，因为您当前分支的最新提交落后于其对应的远程分支。
提示：再次推送前，先与远程变更合并（如 &apos;git pull ...&apos;）。详见
提示：&apos;git push --help&apos; 中的 &apos;Note about fast-forwards&apos; 小节。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照git给出的提示查看&lt;code&gt;Note about fast-forwards&lt;/code&gt;一节，发现我遇到的情况叫做non-fast-forward提交，即希望提交B以将分支A抹掉。此时需要使用&lt;code&gt;git push --force -u origin master&lt;/code&gt;命令。这种场景不常见，一般只有在没有别人参与的分支提交上能用到，因为强制提交会抹掉X-&amp;gt;A分支的所有提交，如果是协作模式下会造成提交丢失。&lt;/p&gt;
</content:encoded></item><item><title>修改版梅尔系数普</title><link>https://zlotus.github.io/2020/08/21/modified-mel-scale/</link><guid isPermaLink="true">https://zlotus.github.io/2020/08/21/modified-mel-scale/</guid><description>实现并魔改了mel-scale，以适应机房音频分析的新任务</description><pubDate>Fri, 21 Aug 2020 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前两周搞定了数据实时传输，这两周多整了点特征，当时就看上古典派语音识别里常用的梅尔系数普了，实现了一下发现简单好用，就打算改一改拿来用。&lt;/p&gt;
&lt;p&gt;不废话了直接贴代码&lt;/p&gt;
&lt;p&gt;&amp;lt;!-- TODO 迁移遗留：原 Hexo iframe 标签，引用的文件已丢失：iframe modified-mel-scale-ipynb.html 800 700 --&amp;gt;&lt;/p&gt;
&lt;p&gt;另外 ，为了把这个主题的两列扩宽，我参考了&lt;a href=&quot;https://github.com/ppoffice/hexo-theme-icarus/blob/master/include/style/base.styl#L23-L27&quot;&gt;这里&lt;/a&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改了&lt;code&gt;themes/icarus/include/style/base.styl&lt;/code&gt;24-27行；&lt;/li&gt;
&lt;li&gt;修改了&lt;code&gt;themes/icarus/layout/layout.jsx&lt;/code&gt;第29行以及&lt;code&gt;themes/icarus/layout/common/widgets.jsx&lt;/code&gt;的36行，把8-8-8和4-4-4，该为9-9-9和3-3-3。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;年纪大了，得记一下别忘了改不回去了……&lt;/p&gt;
</content:encoded></item><item><title>将树莓派用作声源</title><link>https://zlotus.github.io/2020/08/10/rpi-sound-source/</link><guid isPermaLink="true">https://zlotus.github.io/2020/08/10/rpi-sound-source/</guid><description>使用pyzmq将树莓派上麦阵列采集的音频数据以numpy数组的格式发送出去</description><pubDate>Mon, 10 Aug 2020 09:18:00 GMT</pubDate><content:encoded>&lt;p&gt;前一阵子跟刘老师聊天发现，不能总是灌10分低端水了，得进军20分中端水……就着树莓派上的麦阵列作为传感器信号源，送到mbp上做分析试试水先。&lt;/p&gt;
&lt;h1&gt;1 硬件现状&lt;/h1&gt;
&lt;h2&gt;1.1 源端&lt;/h2&gt;
&lt;p&gt;硬件：树莓派3B+一枚，还是老文章里麦阵列用的那枚；麦阵列还是&lt;a href=&quot;http://wiki.seeedstudio.com/ReSpeaker_6-Mic_Circular_Array_kit_for_Raspberry_Pi/&quot;&gt;ReSpeaker-6mic阵列&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;系统：用buster-2020-05的版本，装麦阵列驱动有问题，报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E: Unable to locate package dkms
E: Package &apos;libasound2-plugins&apos; has no installation candidate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先换成旧的stretch……&lt;/p&gt;
&lt;p&gt;刷了stretch，更新软件包update &amp;amp; upgrade后貌似也会出问题……干脆：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接用&lt;a href=&quot;http://downloads.raspberrypi.org/raspbian/images/raspbian-2019-04-09/2019-04-08-raspbian-stretch.zip&quot;&gt;旧版image(2019-04-09)&lt;/a&gt;不升级；&lt;/li&gt;
&lt;li&gt;手动安装旧版&lt;a href=&quot;https://mirrors.tuna.tsinghua.edu.cn/raspberrypi/pool/main/r/raspberrypi-firmware/raspberrypi-kernel-headers_1.20190401-1_armhf.deb&quot;&gt;kernel-headers(2019-04-01)&lt;/a&gt;：&lt;code&gt;dpkg -i raspberrypi-kernel-headers_1.20190401-1_armhf.deb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;将麦阵列驱动&lt;code&gt;git reset --hard&lt;/code&gt;回溯到&lt;a href=&quot;https://github.com/respeaker/seeed-voicecard/blob/c7026a40487a6dc751c93d3d566a4e0b5fd93442/&quot;&gt;2019年四月份的版本&lt;/a&gt;，然后把&lt;code&gt;install.sh&lt;/code&gt;里76-77行的升级和内核安装注释掉，再安装就好了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;毕竟我记得去年四月的时候还能用……估计是新系统和驱动不匹配的锅……其中有用的操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;升级单个软件包&lt;code&gt;sudo apt-get --only-upgrade install apt&lt;/code&gt;，全升级的话kernel和kernel-headers也会跟着升级；&lt;/li&gt;
&lt;li&gt;用pyenv安装和管理Python版本：&lt;a href=&quot;https://installvirtual.com/install-python-3-8-1-on-raspberry-pi-raspbian/&quot;&gt;Install Python 3.8.1 on Raspberry Pi (Raspbian)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;树莓派时间同步：&lt;a href=&quot;https://raspberrytips.com/time-sync-raspberry-pi/&quot;&gt;How to sync time with a server on Raspberry Pi?&lt;/a&gt;（后来发现是mbp的时间总不同步：&lt;code&gt;sudo sntp -sS time.apple.com&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;重启shell的命令是&lt;code&gt;exec &quot;$SHELL&quot;&lt;/code&gt;，装完zsh等重启shell会方便很多；&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;1.2 接收端&lt;/h2&gt;
&lt;p&gt;我那尚能饭否的mbp……&lt;/p&gt;
&lt;h1&gt;2 使用PulseAudio进行流媒体传输&lt;/h1&gt;
&lt;h2&gt;2.1 TCP（是不对的）&lt;/h2&gt;
&lt;p&gt;按照&lt;a href=&quot;https://wiki.seeedstudio.com/ReSpeaker_6-Mic_Circular_Array_kit_for_Raspberry_Pi/&quot;&gt;ReSpeaker 6-Mic Circular Array kit for Raspberry Pi&lt;/a&gt;的介绍，打算安装最常见的PulseAudio Server，据说可以方便的做流媒体数据源。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改PulseAudio远程配置，匿名网络访问：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# /etc/pulse/default.pa
load-module module-native-protocol-tcp auth-ip-acl=127.0.0.1;10.0.6.0/24 auth-anonymous=1
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;开防火墙端口：&lt;code&gt;sudo ufw allow proto tcp to 0.0.0.0/0 port 4713 comment &quot;pulseaudio tcp port&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后来发现tcp访问pulseaudio的目的貌似是控制而不是传输流媒体数据……进一步才发现原来rtp是传输流媒体数据用的……&lt;/p&gt;
&lt;h2&gt;2.2 RTP（也没搞定）&lt;/h2&gt;
&lt;p&gt;一开始想得简单，以为直接默认配置就能搞定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;load-module module-null-sink sink_name=rtp
load-module module-rtp-send source=rtp.monitor
set-default-sink rtp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时并没有指定广播目的IP和端口，pulseaudio就默认用了&lt;code&gt;224.0.0.56&lt;/code&gt;和一个随机五位端口号。出现的现象是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在树莓派上可以通过&lt;code&gt;tcpdump -n net 224.0.0.0/8 -c10&lt;/code&gt;看到发包，类似：&lt;pre&gt;&lt;code&gt;IP 192.168.1.101.44556 &amp;gt; 224.0.0.56.44668: UDP, length 1292
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;我的连在同一个局域网中的mbp里&lt;code&gt;tcpdump&lt;/code&gt;一直是空的。&lt;/li&gt;
&lt;li&gt;不论rpi还是mbp的ping到&lt;code&gt;224.0.0.56&lt;/code&gt;都是不通的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;看了&lt;a href=&quot;https://unix.stackexchange.com/a/122645&quot;&gt;Multicast UDP not working&lt;/a&gt;后使用&lt;code&gt;netstat -gn&lt;/code&gt;发现rpi和mbp的组播地址里都没有&lt;code&gt;224.0.0.56&lt;/code&gt;，猜测可能是内核把包扔掉了，按照该文章的方法处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# macbookpro
netstat -gn
IPv4 Multicast Group Memberships
Group                   Link-layer Address      Netif
224.0.0.1               1:0:5e:0:0:1            en0
224.0.0.251             1:0:5e:0:0:fb           en0

# raspberrypi
 netstat -gn
IPv6/IPv4 Group Memberships
Interface       RefCnt Group
--------------- ------ ---------------------
wlan0           1      224.0.0.251
wlan0           1      224.0.0.1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ping了一下组播组里出现的&lt;code&gt;224.0.0.1&lt;/code&gt;和&lt;code&gt;224.0.0.251&lt;/code&gt;发现是通的。本来想按照&lt;a href=&quot;https://superuser.com/questions/324824/linux-built-in-or-open-source-program-to-join-multicast-group&quot;&gt;Linux built-in or open source program to join multicast group?&lt;/a&gt;写的把&lt;code&gt;224.0.0.56&lt;/code&gt;加入两个机器的组播组里，结果rpi试了不行（命令执行成功，但是用&lt;code&gt;netstat&lt;/code&gt;或&lt;code&gt;ip maddr show&lt;/code&gt;检查都没法写新加的组播地址），而mbp根本没有&lt;code&gt;ip&lt;/code&gt;这个命令，我又不太懂iptable这种高端货，天色已晚我也懒得查解决方案了……干脆就着现有组播组里的&lt;code&gt;224.0.0.251&lt;/code&gt;用吧：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;load-module module-null-sink sink_name=rtp
load-module module-rtp-send source=rtp.monitor destination_ip=&quot;224.0.0.251&quot; port=45678
set-default-sink rtp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时出现新问题，在mbp上tcpdump已经能看到rpi不停的发包，但是用&lt;code&gt;tcpdump -n net 224.0.0.0/8 -c1 -X&lt;/code&gt;查看包内容时，发现内容全零，pulseaudio的配置文件怎么改都不行（就rtp相关的几个模块配置参数怎么调都没用），用&lt;code&gt;pulseaudio -v&lt;/code&gt;调试模式启动，也看不太懂，遂早早放弃有（后）空（会）在（无）搞（期）……&lt;/p&gt;
&lt;h2&gt;2.3 小结：战略失败&lt;/h2&gt;
&lt;p&gt;当需求很简单时，用别人的软件，有学习如何配置的功夫，还不如自己写个小程序实现功能呢……&lt;/p&gt;
&lt;h1&gt;3 使用python-sounddevice采集/播放音频流&lt;/h1&gt;
&lt;p&gt;整理一下思路，目的其实很明确，就是将麦阵列采集的数据通过网络传出去。搜了下，用python的sounddevice（以下简称sd）貌似就可以采集/写入音频流，至于如何发送，想了想就用以前用过的pyzmq吧（高速低延时高可拓展）。&lt;/p&gt;
&lt;p&gt;sd的流有两种封装，Stream需要numpy，而RawStream更底层，使用的是buffer。虽然看源码Stream版本是在Raw版本之上做的封装，但是并没感觉慢多少，试用zmq发包的时候，rpi的那颗弱鸡CPU一直在7-9%浮动，完全hold住，那就用numpy输出吧，操作起来会方便很多。&lt;/p&gt;
&lt;p&gt;（这次树莓派上的python版本控制选用了&lt;code&gt;pyenv&lt;/code&gt;，装的python3.8.5）&lt;/p&gt;
&lt;h2&gt;3.1 采集音频数据&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;sd.query_devices()&lt;/code&gt;在rpi上查询音频设备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  0 bcm2835 ALSA: - (hw:0,0), ALSA (0 in, 2 out)
  1 bcm2835 ALSA: IEC958/HDMI (hw:0,1), ALSA (0 in, 2 out)
  2 seeed-8mic-voicecard: - (hw:1,0), ALSA (8 in, 8 out)
  3 sysdefault, ALSA (0 in, 128 out)
  4 ac108, ALSA (128 in, 128 out)
  5 dmixer, ALSA (0 in, 128 out)
  6 ac101, ALSA (128 in, 128 out)
  7 dmix, ALSA (0 in, 2 out)
* 8 default, ALSA (32 in, 32 out)

0 {&apos;name&apos;: &apos;bcm2835 ALSA: - (hw:0,0)&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 0, &apos;max_output_channels&apos;: 2, &apos;default_low_input_latency&apos;: -1.0, &apos;default_low_output_latency&apos;: 0.005804988662131519, &apos;default_high_input_latency&apos;: -1.0, &apos;default_high_output_latency&apos;: 0.034829931972789115, &apos;default_samplerate&apos;: 44100.0}
1 {&apos;name&apos;: &apos;bcm2835 ALSA: IEC958/HDMI (hw:0,1)&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 0, &apos;max_output_channels&apos;: 2, &apos;default_low_input_latency&apos;: -1.0, &apos;default_low_output_latency&apos;: 0.005804988662131519, &apos;default_high_input_latency&apos;: -1.0, &apos;default_high_output_latency&apos;: 0.034829931972789115, &apos;default_samplerate&apos;: 44100.0}
2 {&apos;name&apos;: &apos;seeed-8mic-voicecard: - (hw:1,0)&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 8, &apos;max_output_channels&apos;: 8, &apos;default_low_input_latency&apos;: 0.005804988662131519, &apos;default_low_output_latency&apos;: 0.008707482993197279, &apos;default_high_input_latency&apos;: 0.034829931972789115, &apos;default_high_output_latency&apos;: 0.034829931972789115, &apos;default_samplerate&apos;: 44100.0}
3 {&apos;name&apos;: &apos;sysdefault&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 0, &apos;max_output_channels&apos;: 128, &apos;default_low_input_latency&apos;: -1.0, &apos;default_low_output_latency&apos;: 0.005804988662131519, &apos;default_high_input_latency&apos;: -1.0, &apos;default_high_output_latency&apos;: 0.034829931972789115, &apos;default_samplerate&apos;: 44100.0}
4 {&apos;name&apos;: &apos;ac108&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 128, &apos;max_output_channels&apos;: 128, &apos;default_low_input_latency&apos;: 0.005333333333333333, &apos;default_low_output_latency&apos;: 0.008, &apos;default_high_input_latency&apos;: 0.032, &apos;default_high_output_latency&apos;: 0.032, &apos;default_samplerate&apos;: 48000.0}
5 {&apos;name&apos;: &apos;dmixer&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 0, &apos;max_output_channels&apos;: 128, &apos;default_low_input_latency&apos;: -1.0, &apos;default_low_output_latency&apos;: 0.125, &apos;default_high_input_latency&apos;: -1.0, &apos;default_high_output_latency&apos;: 0.125, &apos;default_samplerate&apos;: 48000.0}
6 {&apos;name&apos;: &apos;ac101&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 128, &apos;max_output_channels&apos;: 128, &apos;default_low_input_latency&apos;: 0.005333333333333333, &apos;default_low_output_latency&apos;: 0.008, &apos;default_high_input_latency&apos;: 0.032, &apos;default_high_output_latency&apos;: 0.032, &apos;default_samplerate&apos;: 48000.0}
7 {&apos;name&apos;: &apos;dmix&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 0, &apos;max_output_channels&apos;: 2, &apos;default_low_input_latency&apos;: -1.0, &apos;default_low_output_latency&apos;: 0.021333333333333333, &apos;default_high_input_latency&apos;: -1.0, &apos;default_high_output_latency&apos;: 0.021333333333333333, &apos;default_samplerate&apos;: 48000.0}
8 {&apos;name&apos;: &apos;default&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 32, &apos;max_output_channels&apos;: 32, &apos;default_low_input_latency&apos;: 0.008707482993197279, &apos;default_low_output_latency&apos;: 0.008707482993197279, &apos;default_high_input_latency&apos;: 0.034829931972789115, &apos;default_high_output_latency&apos;: 0.034829931972789115, &apos;default_samplerate&apos;: 44100.0}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中的&lt;code&gt;seeed-8mic-voicecard&lt;/code&gt;是我们需要的输入设备，8路输入（2×AC108 ADC，每芯片4路输出，而每芯片都有一路是playback，因此实际是2*3=6mic信号）。为了方便rpi本地测试，AC101（1×DAC，2输入2输出，我觉得就是左右声道？）要用作输出设备。sd的流要用id指定输入输出设备（也就是上面的&lt;code&gt;[2, 6]&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;iodevs = [0, 0]
for idx, d in enumerate(sd.query_devices()):
    if &apos;seeed-8mic-voicecard&apos; in d[&apos;name&apos;]:
        iodevs[0] = idx
    elif &apos;ac101&apos; in d[&apos;name&apos;]:
        iodevs[1] = idx
rs = sd.Stream(samplerate=48000, device=iodevs, channels=[8, 2], callback=cb, finished_callback=fcb)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流有两种发送方式：阻塞 和 非阻塞回调，为了方便调试，我用了非阻塞回调的方式（即&lt;code&gt;Stream.start()&lt;/code&gt;后立即返回不耽误监控或执行其他命令）。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;sd.Stream&lt;/code&gt;一旦指定了&lt;code&gt;callback&lt;/code&gt;参数就会使用非阻塞模式运行，其函数签名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;callback(indata: ndarray, outdata: ndarray, frames: int, time: CData, status: CallbackFlags) -&amp;gt; None
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;indata&lt;/code&gt;是输入设备传来的数据，我用的48kHz采样率，每次回调传来的都是&lt;code&gt;(512, 8)&lt;/code&gt;的numpy数组，基本上10毫秒一组数据；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;outdata&lt;/code&gt;是传给输出设备的数据，我的输入输出配置是&lt;code&gt;[8, 2]&lt;/code&gt;，因此不能直接把&lt;code&gt;indata&lt;/code&gt;复制给输出，图省事我就前后四个数分别求&lt;code&gt;mean&lt;/code&gt;，把8个数强行压成两个数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;frames&lt;/code&gt;是帧数，我这里每次都是512；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;time&lt;/code&gt;是一个CFFI的C结构体，能用的属性有&lt;code&gt;time.inputBufferAdcTime&lt;/code&gt;（输入开始时间）、&lt;code&gt;time.outputBufferDacTime&lt;/code&gt;（输出开始的时间）、&lt;code&gt;time.currentTime&lt;/code&gt;（本次callback被调用的时间）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;没用过不知道，看起来可以用来在回调里发送指令终止回调或终止流。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一旦完成输入数据到输出数据的复制，插在AC101上的音响就有声音了，能感觉到很微小的延迟，完全够用了。&lt;/p&gt;
&lt;p&gt;最后就剩在回调里写上pyzmq的发送，如此发送端就完成了。这里有个小问题，就是pyzmq直接发送numpy是不行滴，直接发送的话代码虽然可以运行，但zmq会使用Python的&lt;code&gt;memoryview&lt;/code&gt;直接将numpy数组转换为字节发出去。如此一来，在接收端是无法重建数组的，因为丢失了shape和dtype等元数据。按照&lt;a href=&quot;https://pyzmq.readthedocs.io/en/latest/serialization.html&quot;&gt;官方解决方案&lt;/a&gt;，使用多段发送标识&lt;code&gt;SNDMORE&lt;/code&gt;先发送数组属性，在发送数组内容即可在接收端重建数组了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def send_array(socket, A, flags=0, copy=True, track=False):
    &quot;&quot;&quot;send a numpy array with metadata&quot;&quot;&quot;
    md = dict(
        dtype = str(A.dtype),
        shape = A.shape,
    )
    socket.send_json(md, flags|zmq.SNDMORE)
    return socket.send(A, flags, copy=copy, track=track)

def recv_array(socket, flags=0, copy=True, track=False):
    &quot;&quot;&quot;recv a numpy array&quot;&quot;&quot;
    md = socket.recv_json(flags=flags)
    msg = socket.recv(flags=flags, copy=copy, track=track)
    buf = memoryview(msg)
    A = numpy.frombuffer(buf, dtype=md[&apos;dtype&apos;])
    return A.reshape(md[&apos;shape&apos;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;发送端代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sounddevice
import numpy as np
import zmq
import sys


class SdSender(object):
    def __init__(self, host=&apos;*&apos;, port=9000):
        self.__frame_idx = 0
        self.__frame_time = 0
        self.__device = self.__init_device()

        context = zmq.Context()
        self.__zmq_socket = context.socket(zmq.PUB)
        self.__zmq_socket.bind(f&quot;tcp://{host}:{port}&quot;)

        self.__verbose = False

    def __init_device(self):
        iodevs = [0, 0]
        for idx, d in enumerate(sounddevice.query_devices()):
            if &apos;seeed-8mic-voicecard&apos; in d[&apos;name&apos;]:
                iodevs[0] = idx
            elif &apos;ac101&apos; in d[&apos;name&apos;]:
                iodevs[1] = idx
        return iodevs

    def send_array(self, A, ct, flags=0, copy=True, track=False):
        &quot;&quot;&quot;send a numpy array with metadata&quot;&quot;&quot;
        self.__frame_idx += 1
        md = dict(
            frame_idx = self.__frame_idx,
            tf = ct,
            dtype = str(A.dtype),
            shape = A.shape,
        )
        if self.__verbose:
            print(f&apos;\r{md[&quot;self.__frame_idx&quot;]}&apos;, end=&quot;&quot;, flush=True)
        self.__zmq_socket.send_json(md, flags|zmq.SNDMORE)
        return self.__zmq_socket.send(A, flags, copy=copy, track=track)

    def cb(self, indata, outdata, frames, time, status):
        # 六声道强行用平均数合成双声道(512, 8) -&amp;gt; (512, 2)，让rpi本机也输出声音方便调试
        outdata[:] = np.hstack( (np.mean(indata[:, 0:4], axis=1, keepdims=True),
                                np.mean(indata[:, 4:8], axis=1, keepdims=True)) )
        # 发送音频原始8路数据
        self.send_array(indata, time.currentTime)

    def fcb(self):
        print(&apos;finished!&apos;)

    def run(self, verbose=False):
        self.__verbose = verbose
        try:
            with sounddevice.Stream(samplerate=48000, device=self.__device, channels=[8, 2],
                                    callback=self.cb, finished_callback=self.fcb) as rs:
                input()
        except KeyboardInterrupt as ki:
            self.__zmq_socket.close()
            print(&apos;exit!&apos;)
            sys.exit(0)
        except Exception as e:
            raise Exception

if __name__ == &quot;__main__&quot;:
    sd = SdSender()
    sd.run(verbose=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.2 接收音频数据&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;sd.query_devices()&lt;/code&gt;在mbp上查询音频设备，可以看到mbp就1-in-1-out，都是双通道，很朴实（我修改了mbp的midi音频设置，将采样率从默认的44100该为48000，输出44100的话，播放48000的数据明显会…慢…）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; 0 Built-in Microphone, Core Audio (2 in, 0 out)
&amp;lt; 1 Built-in Output, Core Audio (0 in, 2 out)

0 {&apos;name&apos;: &apos;Built-in Microphone&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 2, &apos;max_output_channels&apos;: 0, &apos;default_low_input_latency&apos;: 0.0027708333333333335, &apos;default_low_output_latency&apos;: 0.01, &apos;default_high_input_latency&apos;: 0.012104166666666666, &apos;default_high_output_latency&apos;: 0.1, &apos;default_samplerate&apos;: 48000.0}
1 {&apos;name&apos;: &apos;Built-in Output&apos;, &apos;hostapi&apos;: 0, &apos;max_input_channels&apos;: 0, &apos;max_output_channels&apos;: 2, &apos;default_low_input_latency&apos;: 0.01, &apos;default_low_output_latency&apos;: 0.012291666666666666, &apos;default_high_input_latency&apos;: 0.1, &apos;default_high_output_latency&apos;: 0.021625, &apos;default_samplerate&apos;: 48000.0}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接收端代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sounddevice
import numpy as np
import zmq
import sys


class SdReceiver(object):
    def __init__(self, host=&apos;10.0.6.223&apos;, port=9000):
        self.__device = self.__init_device()

        context = zmq.Context()
        self.__zmq_socket = context.socket(zmq.SUB)
        self.__zmq_socket.connect(f&quot;tcp://{host}:{port}&quot;)
        self.__zmq_socket.setsockopt_string(zmq.SUBSCRIBE, &apos;&apos;)

        self.__verbose = False

    def __init_device(self):
        iodevs = [0, 0]
        for idx, d in enumerate(sounddevice.query_devices()):
            if &apos;Built-in Microphone&apos; in d[&apos;name&apos;]:
                iodevs[0] = idx
            elif &apos;Built-in Output&apos; in d[&apos;name&apos;]:
                iodevs[1] = idx
        return iodevs

    def recv_array(self, flags=0, copy=True, track=False):
        &quot;&quot;&quot;recv a numpy array&quot;&quot;&quot;
        md = self.__zmq_socket.recv_json(flags=flags)
        msg = self.__zmq_socket.recv(flags=flags, copy=copy, track=track)
        if self.__verbose:
            print(f&apos;\r{md[&quot;frame_idx&quot;]}: {md[&quot;tf&quot;]}&apos;, end=&quot;&quot;, flush=True)
        buf = memoryview(msg)
        A = np.frombuffer(buf, dtype=md[&apos;dtype&apos;])
        return A.reshape(md[&apos;shape&apos;])

    def cb(self, indata, outdata, frames, time, status):
        rpi_8mic_data = self.recv_array()
        outdata[:] = np.hstack( (np.mean(rpi_8mic_data[:, 0:4], axis=1, keepdims=True),
                                np.mean(rpi_8mic_data[:, 4:8], axis=1, keepdims=True)) )

    def fcb(self):
        print(&apos;finished!&apos;)

    def run(self, verbose=False):
        self.__verbose = verbose
        try:
            with sounddevice.Stream(samplerate=48000, device=self.__device, channels=[2, 2],
                                    callback=self.cb, finished_callback=self.fcb) as ss:
                input()
        except KeyboardInterrupt as ki:
            self.__zmq_socket.close()
            print(&apos;exit!&apos;)
            sys.exit(0)
        except Exception as e:
            raise Exception

if __name__ == &quot;__main__&quot;:
    sd = SdReceiver()
    sd.run(verbose=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.3 小结&lt;/h2&gt;
&lt;p&gt;吐槽：明确目标直接写代码，比无头苍蝇似的瞎配PulseAudio简单太多了。&lt;/p&gt;
&lt;h1&gt;4 总结&lt;/h1&gt;
&lt;p&gt;python-sounddevice每次调用callback传出来的是一个512帧的数组，即采样率48000Hz时约0.01067秒的数据，数据类型为float32，每个数字4字节，一个回调输出&lt;code&gt;512*8*4=16384&lt;/code&gt;字节，如果按秒算的话就是&lt;code&gt;48000*8*4=1,536,000&lt;/code&gt;字节，大概1.465MB/s。以前在单位都是千万十万的交换机，没仔细抠门过带宽问题，在上海基地用的这个民用wifi很明显受不了这种流量……延迟极大……（这时候想想人家mp3，192kHz几分钟的歌才4/5M，真NB）&lt;/p&gt;
&lt;p&gt;这种极端环境下，ØMQ这种“假消息队列”的劣势就显现出来了，作为仅实现了“消息”和“队列”功能的超轻量级库，zmq没有持久化，默认缓存就几十兆，我试了下大概能存不到半分钟数据……订阅者消化能力太差就会导致缓存不够旧数据丢失，然而我就是喜欢zmq的这种朴素感🤦‍。反正树莓派上sd卡也不敢做缓存，搞不好写一写就坏了……后面在研究研究zmq的其他连接模式，毕竟宝藏库。&lt;/p&gt;
&lt;p&gt;接下来就是利用音频数据进行分析了，明天试试把mfcc调个参魔改一下，看能不能水一篇20分的中端……&lt;/p&gt;
</content:encoded></item><item><title>我租了个巨便宜的VPS</title><link>https://zlotus.github.io/2020/07/31/first-vps-from-russia/</link><guid isPermaLink="true">https://zlotus.github.io/2020/07/31/first-vps-from-russia/</guid><description>租了个毛熊的服务器，物一般美价贼廉</description><pubDate>Fri, 31 Jul 2020 21:00:00 GMT</pubDate><content:encoded>&lt;p&gt;前几天不能面向搜索编程了，码农一旦没法使用Stack Overflow就萎了。于是调研了一晚上VPS……打算能跑点没啥算力的程序，顺便也将科学技术掌握在自己手中。&lt;/p&gt;
&lt;p&gt;:::caution
仅限技术交流！
:::&lt;/p&gt;
&lt;h1&gt;1 背景环境&lt;/h1&gt;
&lt;p&gt;最终在某论坛老帖某乎某浪里发现，有个来自毛熊的VPS，按汇率一年115¥，速度、延迟都能接受，就冲动消费了。经过实测延迟发现，其TTK机房（新西伯利亚，据说针对5%-10%的中国线路友好）和DataLine机房（号称CN2对大多中国线路友好）对我的陕西电信来说，延迟/丢包最优，因此，目前把主机建在TTK机房了。&lt;/p&gt;
&lt;p&gt;家里有个刷了Openwrt的树莓派，以前用老科学工具，配置的影子R，搜了搜，准备docker个影子R镜像在VPS上。影子R作为一种生产力工具，不值得在部署环节浪费太多精力，毕竟编译部署也挺麻烦的，docker沙箱又方便又相对安全，很适合我这种懒汉。&lt;/p&gt;
&lt;p&gt;这个justhost.ru据说是个老服务商了，主攻低端市场（比如我），虽然延迟和丢包有点多，但是不太影响使用，美中不足的是，网站是纯俄语的 🤦‍♀️ 不翻译完全不知道在说什么……（像我这种语言小天才，看日语拉丁系都能连蒙带猜，但俄语真特么是异星语言……）此外，他们家的售后也挺给力的。&lt;/p&gt;
&lt;h1&gt;2 部署影子R&lt;/h1&gt;
&lt;p&gt;VPS选用的最常见的CentOS 7。&lt;/p&gt;
&lt;h2&gt;2.1 部署docker镜像&lt;/h2&gt;
&lt;p&gt;首次登录好习惯改密码（为了方便，应该把&lt;code&gt;zsh&lt;/code&gt;、&lt;code&gt;git&lt;/code&gt;、&lt;code&gt;oh-my-zsh&lt;/code&gt;、&lt;code&gt;vim&lt;/code&gt;和&lt;code&gt;tmux&lt;/code&gt;装上。）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# VPS上改密码
passwd
# 电脑上上传秘钥进行ssh免密登录
ssh-copy-id root@193.38.xxx.xxx

sudo yum update &amp;amp;&amp;amp; sudo yum -y install zsh git vim tmux
chsh -s $(which zsh)
sh -c &quot;$(wget -O- https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装docker：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 安装docker
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install docker-ce docker-ce-cli containerd.io
sudo systemctl enable docker
sudo systemctl start docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拉影子R镜像，写默认配置启动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker pull teddysun/shadowsocks-r
mkdir -p /etc/shadowsocks-r
cat &amp;gt; /etc/shadowsocks-r/config.json &amp;lt;&amp;lt;EOF
{
    &quot;server&quot;:&quot;0.0.0.0&quot;,
    &quot;server_ipv6&quot;:&quot;::&quot;,
    &quot;server_port&quot;:9000,
    &quot;local_address&quot;:&quot;127.0.0.1&quot;,
    &quot;local_port&quot;:1080,
    &quot;password&quot;:&quot;xxxxxxxxxxx&quot;,
    &quot;timeout&quot;:120,
    &quot;method&quot;:&quot;aes-256-cfb&quot;,
    &quot;protocol&quot;:&quot;origin&quot;,
    &quot;protocol_param&quot;:&quot;&quot;,
    &quot;obfs&quot;:&quot;plain&quot;,
    &quot;obfs_param&quot;:&quot;&quot;,
    &quot;redirect&quot;:&quot;&quot;,
    &quot;dns_ipv6&quot;:false,
    &quot;fast_open&quot;:true,
    &quot;workers&quot;:1
}
EOF

docker run -d -p 9000:9000 -p 9000:9000/udp --name ssr9000 --restart=always -v /etc/shadowsocks-r:/etc/shadowsocks-r teddysun/shadowsocks-r
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.2 安装BBR加速&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/google/bbr&quot;&gt;BBR&lt;/a&gt;是谷歌开源的一个TCP拥堵问题解决方案。&lt;/p&gt;
&lt;h3&gt;2.2.1 一键部署&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;wget --no-check-certificate https://github.com/teddysun/across/raw/master/bbr.sh &amp;amp;&amp;amp; chmod +x bbr.sh &amp;amp;&amp;amp; ./bbr.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2.2 手动部署（&lt;a href=&quot;https://juejin.im/entry/6844903669830254600&quot;&gt;参考&lt;/a&gt;） - 升级内核&lt;/h3&gt;
&lt;p&gt;一键部署真相不明，也可以自己部署，并不麻烦。首先检查自己的系统内核版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ uname -r
3.10.0-514.2.2.el7.x86_64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;低于4.10的得升内核，可使用 ELRepo 源更新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
sudo rpm -Uvh http://www.elrepo.org/elrepo-release-7.0-2.el7.elrepo.noarch.rpm
sudo yum --enablerepo=elrepo-kernel install kernel-ml -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认升级结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ rpm -qa | grep kernel
kernel-ml-5.7.9-1.el7.elrepo.x86_64
kernel-tools-libs-3.10.0-1127.13.1.el7.x86_64
kernel-debug-devel-3.10.0-1127.13.1.el7.x86_64
kernel-3.10.0-1062.el7.x86_64
kernel-tools-3.10.0-1127.13.1.el7.x86_64
kernel-headers-3.10.0-1127.13.1.el7.x86_64
kernel-3.10.0-1127.el7.x86_64
kernel-3.10.0-1127.13.1.el7.x86_64
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过设置默认引导为 grub2 ，来启用5.7.9内核，显示 grub2 菜单中的所有条目，结果类似下面的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ sudo egrep ^menuentry /etc/grub2.cfg | cut -f 2 -d \&apos;
CentOS Linux (3.10.0-1127.13.1.el7.x86_64) 7 (Core)
CentOS Linux (5.7.9-1.el7.elrepo.x86_64) 7 (Core)
CentOS Linux (3.10.0-1127.el7.x86_64) 7 (Core)
CentOS Linux (3.10.0-1062.el7.x86_64) 7 (Core)
CentOS Linux (0-rescue-1cfc3bd119f34aa79df425015ceaaeee) 7 (Core)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我想用的5.7.9在输出菜单的第二行，而菜单条目索引是从&lt;code&gt;0&lt;/code&gt;开始的，因此我需要选择菜单索引为&lt;code&gt;1&lt;/code&gt;的条目5.7.9作为grub2启动项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo grub2-set-default 1
reboot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成重启后检查内核：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ uname -r
5.7.9-1.el7.elrepo.x86_64
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2.3 手动部署 - 启动BBR&lt;/h3&gt;
&lt;p&gt;要启用 BBR，要修改&lt;code&gt;sysctl&lt;/code&gt;配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;net.core.default_qdisc=fq&apos; | sudo tee -a /etc/sysctl.conf
echo &apos;net.ipv4.tcp_congestion_control=bbr&apos; | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查结果，出现类似的输出即为成功：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ sudo sysctl net.ipv4.tcp_available_congestion_control
net.ipv4.tcp_available_congestion_control = reno cubic bbr
➜  ~ sudo sysctl -n net.ipv4.tcp_congestion_control
bbr
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查模块加载状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ lsmod | grep bbr
tcp_bbr                20480  15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;清理无用老旧内核（可选）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum remove $(rpm -qa | grep kernel | grep -v $(uname -r))
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3 编译Python 3.8（&lt;a href=&quot;https://computingforgeeks.com/how-to-install-python-on-3-on-centos/&quot;&gt;参考&lt;/a&gt;）&lt;/h1&gt;
&lt;p&gt;最近3.8用习惯了，想整个不然不舒服。在VPS上编译很快的，可不是树莓派这种便宜货上的速度 😏。&lt;/p&gt;
&lt;p&gt;更新并安装依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum -y update
sudo yum -y groupinstall &quot;Development Tools&quot;
sudo yum -y install openssl-devel bzip2-devel libffi-devel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认gcc已安装（上面清理老旧内核的可能把gcc顺道清理了，得装回来……）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ gcc --version
gcc (GCC) 4.8.5 20150623 (Red Hat 4.8.5-39)
Copyright © 2015 Free Software Foundation, Inc.
本程序是自由软件；请参看源代码的版权声明。本软件没有任何担保；
包括没有适销性和某一专用目的下的适用性担保。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载Python3.8.5：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum -y install wget
wget https://www.python.org/ftp/python/3.8.5/Python-3.8.5.tgz
tar xvf Python-3.8.5.tgz
cd Python-3.8.5/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行configure文件准备安装环境、make并安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./configure --enable-optimizations
sudo make altinstall
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认安装结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~ python3.8 --version
Python 3.8.5
➜  ~ pip3.8 --version
pip 20.1.1 from /usr/local/lib/python3.8/site-packages/pip (python 3.8)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4 安装Nginx&lt;/h1&gt;
&lt;p&gt;我装Python3，其中一个原因就是想试一下&lt;code&gt;python -m http.server 8000&lt;/code&gt;这个单进程单线程的简易http服务器。用aria2c去拖大文件，不论&lt;code&gt;split&lt;/code&gt;配多少都是单块下载。因此，要进行segmented文件下载，得要整个简约而不简单的发布。&lt;/p&gt;
&lt;p&gt;辣么，Linux下大家自然而然会想到，大红大紫到快过气的Nginx啦~&lt;/p&gt;
&lt;p&gt;安装并查看EPLE源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum install epel-release
sudo yum repolist
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装nginx：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo yum install nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl enable nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动/重启服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start nginx
sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开防火墙服务并刷新防火墙配置使生效：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --zone=public --permanent --add-service=http
sudo firewall-cmd --reload
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查看防火墙服务/端口策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo firewall-cmd --list-service
sudo firewall-cmd --list-ports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装netcat进行连接测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;yum install nmap-ncat
nc -zv 193.x.x.x 80
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;5 配置Nginx&lt;/h1&gt;
&lt;p&gt;配置文件在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vim /etc/nginx/nginx.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一开始八成都会有403错误，因为配置第一行&lt;code&gt;user nginx&lt;/code&gt;指的是使用nginx用户运行nginx服务。那么，我们使用root用户新建的文件夹和文件都不能访问，为了懒省事，干脆改成&lt;code&gt;user root&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在&lt;code&gt;http&lt;/code&gt;域中，添加连接数限制，每秒并发16个连接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    limit_conn_zone $binary_remote_addr zone=connaddr:10m;
    limit_req_zone $binary_remote_addr zone=reqaddr:10m rate=16r/s;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;server&lt;/code&gt;域中详细定义&lt;code&gt;connaddr&lt;/code&gt;和&lt;code&gt;reqaddr&lt;/code&gt;，每个地址允许16个连接，每个请求允许并发16个连接，这玩意调大调小得自己试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;            limit_conn connaddr 16;
            limit_req zone=reqaddr burst=16;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;server&lt;/code&gt;域指定该虚拟主机的根目录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        root         /root/webroot;
        index        index.html;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加一个&lt;code&gt;location&lt;/code&gt;块作为静态文件服务，开启&lt;code&gt;auto_index&lt;/code&gt;自动文件索引，&lt;a href=&quot;https://stackoverflow.com/questions/10631933/nginx-static-file-serving-confusion-with-root-alias&quot;&gt;需要注意的是&lt;/a&gt;，其中的&lt;code&gt;alias&lt;/code&gt;如果换成&lt;code&gt;root&lt;/code&gt;，则访问的路径会变成&lt;code&gt;/root/webroot/downloads/downloads/&lt;/code&gt;，即&lt;code&gt;root&lt;/code&gt;会使url自动加上&lt;code&gt;location&lt;/code&gt;后面的路径。因此，我选用&lt;code&gt;alias&lt;/code&gt;加强可读性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        location /downloads {
            alias /root/webroot/downloads;
            charset utf-8;
            limit_conn connaddr 16;
            limit_req zone=reqaddr burst=16;
            autoindex on;
            autoindex_exact_size off;
            autoindex_format html;
            autoindex_localtime on;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如此，一个简约而不简单的文件下载服务就搭好了，访问&lt;code&gt;http://193.x.x.x/downloads&lt;/code&gt;就可以看到简陋的&quot;index of downloads&quot;了。当然，这个文件服务器还是可以继续升级改造的，网上似乎有美化教程，我懒得弄了，值得一提的就是，可以简单的在上面&lt;code&gt;location&lt;/code&gt;域最后再加两行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        location /downloads {
            ...
            auth_basic &quot;Authorized users only&quot;;
            auth_basic_user_file /etc/nginx/htpasswd;
        }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后新建一个&lt;code&gt;/etc/nginx/htpasswd&lt;/code&gt;文件，里面写一行用&lt;a href=&quot;https://tool.oschina.net/htpasswd&quot;&gt;在线htpasswd生成工具&lt;/a&gt;生成的加密用户名密码，重启nginx再次访问&lt;code&gt;http://193.x.x.x/downloads&lt;/code&gt;就会发现弹出输入用户名密码的窗口了。至此，文件服务端的基本功能已经完备。&lt;/p&gt;
&lt;p&gt;接下来，就是用&lt;code&gt;aria2c&lt;/code&gt;多线程拖文件了，其最主要的配置（&lt;a href=&quot;https://gist.github.com/aa65535/5e956c4eb4f451ddec29&quot;&gt;参考&lt;/a&gt;）有
：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 单个任务最大分片数，默认为5，我们可以改成16
split=16
# 断点续传
continue=true
# 最小文件分片大小，添加任务时可指定，取值范围1M - 1024M，默认为20M
# 假定size=10M，文件为20MB，则使用两个来源下载；文件为15MiB 则使用一个来源下载；
# 这个属性的大小应该按照任务的实际情况即网速进行调整。
min-split-size=10M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用Nginx作为文件服务器的思路为：找到大文件，先下载到vps上（因为vps在外面的速度很快，我这个贱民vps都号称200M带宽），然后使用vps的Nginx作为文件服务器，从里面利用文件分片，并发多连接拖。自己搭文件服务的好处是没有并发连接数、下载超时、重传次数之类的限制，想连多少，想下多久，想断点续传多少次，都随自己。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;国家强大了，我们的汇率买美帝的vps，至少三五百一年，而我们的汇率买毛熊的vps，只要一百出头，也许美帝主机确实性能和网络状态要好一些，但比起价钱，毛熊的主机就跟白嫖差不多。&lt;/p&gt;
&lt;p&gt;再次祝愿祖国母亲繁荣昌盛！生生不息！Live Long and Prosper！&lt;/p&gt;
</content:encoded></item><item><title>关于OAuth2的密码式令牌</title><link>https://zlotus.github.io/2020/07/04/oauth2-example/</link><guid isPermaLink="true">https://zlotus.github.io/2020/07/04/oauth2-example/</guid><description>使用FastAPI实现身份认证及权限鉴别机制</description><pubDate>Sat, 04 Jul 2020 23:00:00 GMT</pubDate><content:encoded>&lt;p&gt;想写个不需要登录，无需维护Session或Cookie等状态的纯RESTful API，但是又需要有身份认证和权限鉴别功能。比如我有一套爬来的金融数据API，想让指定的用户使用，其他人访问就报401。最近用FastAPI，里面提供了OAuth2以及scopes的密码式令牌鉴权机制，十分方便，但是部分场景也容易造成信息泄露。&lt;/p&gt;
&lt;p&gt;:::important
OAuth2的密码式令牌应在可信任网络中使用。
:::&lt;/p&gt;
&lt;h1&gt;OAuth2密码式令牌应用场景&lt;/h1&gt;
&lt;p&gt;众所周知OAuth2有四种应用场景各不相同的鉴权方式，其中，密码式可以说是在互联网环境中最不安全的一种了，因为你需要将你在诸如微信的用户名密码一类的信息告诉一个第三方应用，而后第三方应用拿着你的用户名密码去向微信申请数据。如果是在互联网上使用这种验证方式，你必须极度信任这个第三方应用，不然就是社会性自杀。&lt;/p&gt;
&lt;p&gt;那么，这样一个危险的鉴权方式有什么实际作用呢？那当然是可控网络环境中，对各业务系统API进行集中访问控制了——毕竟现在微服务这么火，就算是内网应用也应该试试开放几个API吐出点数据——这样PPT上就可以写打破数据孤岛了✌️。&lt;/p&gt;
&lt;p&gt;比如，我有很多业务系统，以系统A为例。&lt;code&gt;a.com&lt;/code&gt;开放了两个API：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看当前用户的信息&lt;code&gt;http://a.com/me&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;查看当前用户拥有的数据条目&lt;code&gt;http://a.com/me/items&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这时候有人想调用这俩API，而A系统管理员只希望指定的人用，此外，还有XYZ的各种业务系统，都有这种需求，如何在不使用session、cookie这种有状态身份认证的情况下（调用API还要先认证个session太麻烦了）直接在请求中附带一个通行证就调用API，还要保证安全和权限分级呢？&lt;/p&gt;
&lt;h1&gt;OAuth2密码式令牌应用环境&lt;/h1&gt;
&lt;p&gt;密码式令牌提供了一个简单的解决方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在A系统上开一个发通行证&lt;code&gt;access_token&lt;/code&gt;的窗口：&lt;code&gt;http://a.com/token&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;系统中有个加解密的秘钥&lt;code&gt;SECRET_KEY&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;通行证窗口是一个可以被A系统全信任的窗口，不私存用户名密码，就是个工具窗口。&lt;/li&gt;
&lt;li&gt;通行证窗口接受用户认证信息，返回一个&lt;code&gt;jwt&lt;/code&gt;库使用秘钥&lt;code&gt;SECRET_KEY&lt;/code&gt;加密的通行证。&lt;/li&gt;
&lt;li&gt;通行证窗口接受方式规定为HTTP POST方式。接受如下的表单，其中&lt;code&gt;scope&lt;/code&gt;为表示权限的字符串，以空格为分隔符：&lt;pre&gt;&lt;code&gt;grant_type: password
scope: me items
username: johndoe
password: secret
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;通行证窗口拿到用户名密码后确认身份，并取出库中的权限列表与请求中的权限列表进行核对，如果请求的权限多于在册的权限，则http-401，反之则放行。&lt;/li&gt;
&lt;li&gt;通行证窗口返回的通行证为json格式，通行证的内容包含认证用户ID、权限列表、通行证有效期，例如&lt;pre&gt;&lt;code&gt;{&quot;access_token&quot;:&quot;eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huZG9lIiwic2NvcGVzIjpbIm1lIiwiaXRlbXMiXSwiZXhwIjoxNTk0MTI2MjkxfQ.AimqzTZg1t7XHstWi-048prkRAPv6-m-IM9OfGYio8A&quot;,&quot;token_type&quot;:&quot;bearer&quot;}
// 其中access_token解密后的明文形为
{&apos;sub&apos;: &apos;johndoe&apos;, &apos;scopes&apos;: [&apos;me&apos;, &apos;items&apos;], &apos;exp&apos;: 1593831859}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;A系统提供了很多API：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;查看当前用户的信息&lt;code&gt;http://a.com/me&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;查看当前用户拥有的数据条目&lt;code&gt;http://a.com/me/items&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;li&gt;系统中有个加解密的秘钥&lt;code&gt;SECRET_KEY&lt;/code&gt;，与发通行证的窗口用的一样；&lt;/li&gt;
&lt;li&gt;这些API都有身份验证机制，具有独立的权限列表要求，需要提供OAuth2通行证令牌，&lt;code&gt;jwt&lt;/code&gt;库使用秘钥&lt;code&gt;SECRET_KEY&lt;/code&gt;对令牌解密，以验明正身。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;A系统管理员维护着一个用户及权限表，包括用户名&lt;code&gt;username&lt;/code&gt;（如&lt;code&gt;johndoe&lt;/code&gt;）、密码&lt;code&gt;hashed_password&lt;/code&gt;、权限列表&lt;code&gt;scope&lt;/code&gt;（如&lt;code&gt;[&apos;b/me&apos;, &apos;b/items&apos;]&lt;/code&gt;）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;OAuth2密码式令牌调用流程&lt;/h1&gt;
&lt;p&gt;此时，第三方系统B想要调用业务系统A的API，比如有人想要调用&lt;code&gt;http://a.com/me&lt;/code&gt;，那么A系统如何认证用户和权限呢？&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;B需要获取A库表中的某个用户名密码：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;这个动作可以是A管理员给B的，比如我A系统就开一个API专用账户，你B拿着这个账号就随便临幸我A吧。（这种通常权限开的尺度比较大）&lt;/li&gt;
&lt;li&gt;也可以是A的一个用户给B的，用户aaa表示你B系统可以去A系统拿我的某些数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;B拿着用户名密码去A的通行证窗口申请通行证：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;通行证窗口就是负责接收用户名密码，确认在A的库表中后，发通行证。&lt;/li&gt;
&lt;li&gt;查无此人直接http-401。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;B拿到通行证，在每一次对A系统的API请求Header中附带这个通行证：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;例如，在访问&lt;code&gt;a.com/me&lt;/code&gt;时加上&lt;code&gt;Authorization&lt;/code&gt;头部字段，其值形为&lt;code&gt;Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huZG9lIiwic2NvcGVzIjpbIm1lIiwiaXRlbXMiXSwiZXhwIjoxNTk0MTI2MjkxfQ.AimqzTZg1t7XHstWi-048prkRAPv6-m-IM9OfGYio8A&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;A的API会拿到&lt;code&gt;Authorization&lt;/code&gt;头部字段，取出其中的通行证，解密，获取其中的用户名、权限、有效期：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;若令牌可解密，且在有效期内，比对&lt;strong&gt;API所需的权限&lt;/strong&gt;与&lt;strong&gt;请求通行证提供的权限&lt;/strong&gt;，若提供满足所需，则API响应并返回，反之则http-401。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;OAuth2密码式令牌代码示例&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;from datetime import datetime, timedelta

import jwt
from jwt import PyJWTError
from passlib.context import CryptContext

from fastapi import Depends, FastAPI, HTTPException, Security, status
from fastapi.security import (
    OAuth2PasswordBearer,
    OAuth2PasswordRequestForm,
    SecurityScopes,
)

from typing import Optional, List
from pydantic import BaseModel, ValidationError

# to get a string like this run:
# openssl rand -hex 32
SECRET_KEY = &quot;4ff135e7c8c7375a00738b8f02cc016a7983ea345fe35735900495bea0cf9ce9&quot;
ALGORITHM = &quot;HS256&quot;
ACCESS_TOKEN_EXPIRE_MINUTES = 30


app = FastAPI()
VERSION = &apos;1.0&apos;

fake_users_db = {
    &quot;johndoe&quot;: {
        &quot;username&quot;: &quot;johndoe&quot;,
        &quot;full_name&quot;: &quot;John Doe&quot;,
        &quot;email&quot;: &quot;johndoe@example.com&quot;,
        &quot;hashed_password&quot;: &quot;$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW&quot;,
        &quot;disabled&quot;: False,
    },
    &quot;alice&quot;: {
        &quot;username&quot;: &quot;alice&quot;,
        &quot;full_name&quot;: &quot;Alice Chains&quot;,
        &quot;email&quot;: &quot;alicechains@example.com&quot;,
        &quot;hashed_password&quot;: &quot;$2b$12$gSvqqUPvlXP2tfVFaWK1Be7DlH.PKZbv5H8KnzzVgXXbVxpva.pFm&quot;,
        &quot;disabled&quot;: True,
    },
}

class Token(BaseModel):
    access_token: str
    token_type: str


class TokenData(BaseModel):
    username: str = None
    scopes: List[str] = []


class User(BaseModel):
    username: str
    email: Optional[str] = None
    full_name: Optional[str] = None
    disabled: Optional[bool] = None


class UserInDB(User):
    hashed_password: str


pwd_context = CryptContext(schemes=[&quot;bcrypt&quot;], deprecated=&quot;auto&quot;)
oauth2_scheme = OAuth2PasswordBearer(
    tokenUrl=&quot;/token&quot;,
    scopes={
        &quot;me&quot;: &quot;Read information about the current user.&quot;, 
        &quot;items&quot;: &quot;Read items.&quot;
    },
)


def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)


def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


def authenticate_user(fake_db, username: str, password: str):
    user = get_user(fake_db, username)
    if not user:
        return False
    if not verify_password(password, user.hashed_password):
        return False
    return user


def create_access_token(data: dict, expires_delta: timedelta = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({&quot;exp&quot;: expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt


async def get_current_user(security_scopes: SecurityScopes, token: str = Depends(oauth2_scheme)):
    # Request Headers里添加了: Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huZG9lIiwic2NvcGVzIjpbIm1lIiwiaXRlbXMiXSwiZXhwIjoxNTkzODMyMTM2fQ.Ulq6S2zg0kETKdS_3NhcFho9M4-JhzHXcXWPZQJm0Cc
    # curl -X GET &quot;http://localhost:8910/users/me/&quot; -H &quot;accept: application/json&quot; -H &quot;Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqb2huZG9lIiwic2NvcGVzIjpbIm1lIiwiaXRlbXMiXSwiZXhwIjoxNTkzODMyMTM2fQ.Ulq6S2zg0kETKdS_3NhcFho9M4-JhzHXcXWPZQJm0Cc&quot;
    if security_scopes.scopes:
        authenticate_value = f&apos;Bearer scope=&quot;{security_scopes.scope_str}&quot;&apos;
    else:
        authenticate_value = f&quot;Bearer&quot;
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail=&quot;Could not validate credentials&quot;,
        headers={&quot;WWW-Authenticate&quot;: authenticate_value},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) # payload解密为 {&apos;sub&apos;: &apos;johndoe&apos;, &apos;scopes&apos;: [&apos;me&apos;, &apos;items&apos;], &apos;exp&apos;: 1593831859}
        username: str = payload.get(&quot;sub&quot;)
        if username is None:
            raise credentials_exception
        token_scopes = payload.get(&quot;scopes&quot;, [])
        token_data = TokenData(scopes=token_scopes, username=username)
    except (PyJWTError, ValidationError):
        raise credentials_exception
    user = get_user(fake_users_db, username=token_data.username)
    if user is None:
        raise credentials_exception
    for scope in security_scopes.scopes: # security_scopes.scopes为当前请求所要求的scopes权限，如/me为[&apos;me&apos;]，/me/items为[&apos;items&apos;, &apos;me&apos;]
        if scope not in token_data.scopes: # token_data.scopes为当前请求的token中持有的scopes权限，如果所需的scopes中有任何一条不在请求持有的的scopes时（即少持有任一条），均禁止访问
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail=&quot;Not enough permissions&quot;,
                headers={&quot;WWW-Authenticate&quot;: authenticate_value},
            )
    return user


async def get_current_active_user(current_user: User = Security(get_current_user, scopes=[&quot;me&quot;])):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail=&quot;Inactive user&quot;)
    return current_user


@app.post(&quot;/token&quot;, response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(status_code=400, detail=&quot;Incorrect username or password&quot;)
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={&quot;sub&quot;: user.username, &quot;scopes&quot;: form_data.scopes},
        expires_delta=access_token_expires,
    )
    return {&quot;access_token&quot;: access_token, &quot;token_type&quot;: &quot;bearer&quot;}


@app.get(&quot;/users/me/&quot;, response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user


@app.get(&quot;/users/me/items/&quot;)
async def read_own_items(current_user: User = Security(get_current_active_user, scopes=[&quot;items&quot;])):
    return [{&quot;item_id&quot;: &quot;Foo&quot;, &quot;owner&quot;: current_user.username}]


@app.get(&quot;/status/&quot;)
async def read_system_status(current_user: User = Depends(get_current_user)):
    return {&quot;status&quot;: &quot;ok&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::caution[警告]
在152行中，示例为了方便直接给令牌赋予了请求者索要的权限，在真实情况下，此处应该将请求索要的权限与数据库中记录的权限进行比对，若索要权限大于记录权限，则应产生“未授权”的异常。
:::&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;在网络环境较为透明可控的场景下，使用OAuth2的密码方式对开放的API进行身份及权限验证，是一种安全、可控、简便的管理方法。除上文描述的方法外，还可以将各应用系统的权限收口统一管理，维护一个用户权限库表，将通行证的发放维护在一个入口中，再于各应用系统中约定同一个加解密秘钥，可进一步方便管理。当然，这些方法的前提必须是网络环境较为透明可控。&lt;/p&gt;
</content:encoded></item><item><title>不要健康过度</title><link>https://zlotus.github.io/2020/05/15/gastroscope-2020/</link><guid isPermaLink="true">https://zlotus.github.io/2020/05/15/gastroscope-2020/</guid><description>众所周知的怂，稍微有点不舒服就跑去作了个死</description><pubDate>Fri, 15 May 2020 19:49:00 GMT</pubDate><content:encoded>&lt;p&gt;近一个多月消化有点不那么良，吃多了总感觉反酸，嗓子里总有东西顶着，比较怂就去医学院看了个消化科。虽劳神伤财折腾好几天，情节曲折百转千回，但结果依旧是屁事没有。特此记录，警钟长鸣，停止沙雕行为，不要没事找事。&lt;/p&gt;
&lt;p&gt;:::important
本文很无聊，不涉及计算机方面的知识。
:::&lt;/p&gt;
&lt;h1&gt;现象&lt;/h1&gt;
&lt;p&gt;快一个月食欲不振（后来想想大概率是心理作用），吃的复杂一点，食道就感觉有胃酸反流，感觉吃多了食道也会顶，有时候打嗝会带有食物或者胃酸，总之就是吃饭没以前那么爽了，果然不爽的时候才知道爽的可贵。&lt;/p&gt;
&lt;p&gt;具体测试如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;牛奶+黄油面包，反酸；&lt;/li&gt;
&lt;li&gt;牛奶+麦片，微酸；&lt;/li&gt;
&lt;li&gt;水+麦片，没啥问题；&lt;/li&gt;
&lt;li&gt;臊子面、油泼面，反酸；&lt;/li&gt;
&lt;li&gt;白面条+鸡蛋羹，没啥问题，最多吃多了会有点顶；&lt;/li&gt;
&lt;li&gt;酸汤水饺，反酸；&lt;/li&gt;
&lt;li&gt;羊血饸饹、麻辣米线等比较刺激的食物，吃完半天或一天后下面也辣，证明消化通畅；&lt;/li&gt;
&lt;li&gt;上下通气，暂时没有证据表明消化通路有阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;听说不少人有反酸现象，说不是啥大事。但是我自认为是个敏感而又脆弱的男人，稍微有哪里不对劲我就想让医生帮忙瞅瞅顺手再修修，所以还是打算去医院，不行就让医生拿胃镜搋一搋。（几年前也是同样的症状在省人民医院做过，当时也是屁事没有[捂脸]）&lt;/p&gt;
&lt;h1&gt;就诊流程&lt;/h1&gt;
&lt;p&gt;周一（2020-05-11）按钊钊的指导，在微信公众号（西安交大一附院）上挂了交大医学院消化科王医生的号（这个医生特别特别好，墙裂推荐），准备第二天先看看啥情况。&lt;/p&gt;
&lt;h2&gt;2020-05-12 周二&lt;/h2&gt;
&lt;p&gt;周一在微信上约的是下午四点多，我怕初来乍到的摸不清流程，就在两点早早到了。首先是挂号，需要&lt;strong&gt;十元现金&lt;/strong&gt;在自助机上办&lt;strong&gt;就诊卡&lt;/strong&gt;。我没有立刻就给就诊卡里充值，是挂号的时候在护士站旁边才临时跑过去充值的。&lt;/p&gt;
&lt;p&gt;:::tip[优化 1]
办完就诊卡，应该接着就用自助机充值30元，用以挂号扣费。支付宝、微信、银行卡等都可以。
:::&lt;/p&gt;
&lt;p&gt;接下来，不需要在意预约时间，只要是网上预约的，就可以直接去护士站先排预约的号。这里要注意，并不是非要等到预约的时间才去护士站。我是等到快四点才去排号的，结果快五点才轮到我。&lt;/p&gt;
&lt;p&gt;:::tip[优化 2]
一上班就应该立刻去护士站抽号，门诊是预约优先，现场排号。也就是预约的一视同仁，谁先排号谁先看。另外，现场拿号的（非预约）要等到所有预约病人看完，才能开始看。
:::&lt;/p&gt;
&lt;p&gt;见到医生后，陈述了状况和自己的测试经验，医生觉得不是大问题，推荐先吃药观察，询问我要不要做胃镜，并不推荐做。我觉得看一下心理踏实，就下决心做个胃镜。医生开了好几个&lt;strong&gt;申请单&lt;/strong&gt;，包括验血、查心电图、胃镜药等。此时已经下午五点，我就去一楼缴费，结果医保卡出现问题，收费的医生说刷卡时医保卡报“12至5月不能办理业务”之类的错误信息，让我询问单位医保情况，这时候已经到了下班时间，问哪都不方便，今天先就这么找吧，明天的事交给明天的我。&lt;/p&gt;
&lt;h2&gt;2020-05-13 周三&lt;/h2&gt;
&lt;p&gt;上班，九十点钟抽空问了一下医保卡的问题，首先单位人力确定单位社保OK，省医保确定卡状态正常，接着省医保的另一个电话给出建议，有可能是我们这种在学校办过社保的拿的卡号比较特殊，以SNA开头，会让部分医生认为是市医保（医生可能会认为省医保应该以SNS，SNA开头的他们往往认为是市医保），电话建议刷卡时明确告知使用省医保。&lt;/p&gt;
&lt;p&gt;:::caution[失误 1]
以后刷医保卡，应该声明刷省医保，以免造成误会。
:::&lt;/p&gt;
&lt;p&gt;晚上去药店用医保卡刷了冯姐建议的四磨汤，确定医保卡正常。&lt;/p&gt;
&lt;h2&gt;2020-05-14 周四&lt;/h2&gt;
&lt;p&gt;早上去医院缴费，明确刷省医保，一切正常。除了胃镜本身外的项目都可以在医保窗口一次用医保卡刷完，而胃镜在窗口刷了30%后，需要在2号窗口办一个什么结算表，然后拿着表在隔壁3号窗口继续刷医保就行了。总之，&lt;strong&gt;全程刷医保卡&lt;/strong&gt;，完全不需要现金、银行卡、支付宝或微信。&lt;/p&gt;
&lt;p&gt;接下来就是胃镜前的检查了。我先去验血了，验血是在2楼西的自助机上刷就诊卡抽号，验血还是很快的，十来个窗口并行，虽然前面排了快一百个人，也没等多久，但是排号单上写的出结果需要两小时。&lt;/p&gt;
&lt;p&gt;:::tip[优化 3]
如果是多人协同作战的话，应该在缴费的同时，派一个人去抽号。因为缴费慢在缴胃镜项目的医保表格办理了，其他项目（如化验和药）在缴费的第一个窗口就已经办完了，此时就可以抽号了。
:::&lt;/p&gt;
&lt;p&gt;这里不需要等两小时出结果，在手机APP“智慧好医院”上注册（我觉得绑银行卡快一些，我不喜欢用微信授权登录，腾讯知道已经的太多了）登录。在“做检验”-&amp;gt;“更多”里就能看到这个线下检验的状态，也可以直接在“查报告”里看到状态和结果，等了不到一个小时就出结果了，在3、4楼的自助机上刷就诊卡取结果，得到&lt;strong&gt;化验单&lt;/strong&gt;一张。&lt;/p&gt;
&lt;p&gt;心电图在2楼东，直接排队检查就行，查完现场取结果，基本没等几分钟，得到&lt;strong&gt;心电图&lt;/strong&gt;一张。然后我傻傻的去取药了。&lt;/p&gt;
&lt;p&gt;:::caution[失误 2]
胃镜药是在预约后取，拿到化验单和心电图后应立刻去预约胃镜。
:::&lt;/p&gt;
&lt;p&gt;等我反应过来都到上午下班时间了，等到下午2:20才开始继续预约，但是这时候预约的就是第二天早上8:30的了，一天没吃饭等做胃镜快饿死了也没做上[捂脸]。&lt;/p&gt;
&lt;p&gt;:::tip[优化 4]
窗口预约胃镜需要用就诊卡刷材料费，应该提前充上100备用。
:::&lt;/p&gt;
&lt;p&gt;预约完成后去取药，时间还早就先回单位了。&lt;/p&gt;
&lt;h2&gt;2020-05-15 周五&lt;/h2&gt;
&lt;p&gt;早上8:30，带上胃镜药，填交疫情调查表和申请单，排队叫号，被叫后等待时喝药，然后做胃镜。感觉医生真的把我当马桶了，搋了几分钟，漫长的我都快走马灯了，唯一的收获就是，可能有点咽炎，算是挂错号了吧[捂脸]。&lt;/p&gt;
&lt;h1&gt;后记&lt;/h1&gt;
&lt;p&gt;以后有经验了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;就近逮住一家医院，别老换医院和医生，如此，以往的经验好用，以往的档案也好调，要低熵。&lt;/li&gt;
&lt;li&gt;没事别找搋，虽然搋完立马通畅……但是照这个手法，我觉得没问题都有可能搋出问题……&lt;/li&gt;
&lt;li&gt;春天换季，敏感的可能有概率出现轻微的消化问题，不能因为不常见就认为是大事。&lt;/li&gt;
&lt;li&gt;嗯，没事少去医院，听说最近又严了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外，不想在被搋了：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;换季注意保暖，别浪，掉体重可都是白花花的蛋白粉；&lt;/li&gt;
&lt;li&gt;严格执行中老年作息，采用中老年食谱，进一步降低粉汤羊血加羊肚、羊血饸饹、麻辣米线的频率；&lt;/li&gt;
&lt;li&gt;以后吃多了可以试试冯姐的四磨汤+益生菌助消化；&lt;/li&gt;
&lt;li&gt;可以把消化系统方面找搋的阈值调高一些……&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;可能是疫情的debuff，进一步增强了我贪财怕死爱睡觉的属性，再加上突如其来的假期中作息/饮食不规律，以及铁房关门使得运动量大幅减少，导致对身体状态过于敏感，把平时的噪音也当做异常值重视。以后应该大幅加强肉体上的顿感，毕竟单位每年的体检其实已经能排查很多问题了。对于健康不应该过于谨慎，因为入侵式的检查对身体的伤害也是很大的，而且饿了好几天，肉掉了好几斤，这都是汗水和蛋白粉堆出来的肉啊！心疼！&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;记录一下，本文开头的注意效果是icarus官方文档里用的，看着好看就抄来了。要改的地方在&lt;code&gt;themes/icarus/include/style/article.styl&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;article
    /* ... */
    &amp;amp;.article
        /* ... */
        .content
            /* ... 以下为新增样式 */
            .message
                &amp;amp;.message-immersive
                    border-radius: 0
                    margin: 0 -1.5rem 1.5rem -1.5rem
                
                    .message-body
                        border: none
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意修改样式后，部署时使用&lt;code&gt;hexo g -f -d&lt;/code&gt;，加上&lt;code&gt;-f&lt;/code&gt;强制重新生成样式。&lt;/p&gt;
&lt;p&gt;在文章中使用&lt;code&gt;&amp;lt;article&amp;gt;&lt;/code&gt;标签即可，比如开头的标签为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;article class=&quot;message message-immersive is-primary&quot;&amp;gt;
    &amp;lt;div class=&quot;message-body&quot;&amp;gt;
    &amp;lt;i class=&quot;fas fa-info-circle mr-2&quot;&amp;gt;&amp;lt;/i&amp;gt;本文很无聊，不涉及计算机方面的知识。
&amp;lt;/div&amp;gt;
&amp;lt;/article&amp;gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原主题中图片是左对齐的，可以通过样式调整为居中，新增&lt;code&gt;a&lt;/code&gt;标签的四行即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
article
    ...
    &amp;amp;.article
        ...
        a
            img
                margin: auto
                display: block
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，这个主题中的inline型&lt;code&gt;code&lt;/code&gt;块，默认没有&lt;code&gt;padding&lt;/code&gt;（我觉得跟文字挤在一起不好看）、没有&lt;code&gt;background-color&lt;/code&gt;（我觉得有个背景色块更容易区分）。而且如果这个inline代码块恰好还需要加上超链接&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;标签，像这样&lt;a href=&quot;https://doc.rust-lang.org/alloc/alloc/trait.GlobalAlloc.html&quot;&gt;&lt;code&gt;GlobalAlloc&lt;/code&gt;&lt;/a&gt;，那么使用默认主题时就无法区分不带超链接的块和带超链接的块了，只有把鼠标放上去才能看到一个小手。因此，我打算自己加上&lt;code&gt;padding&lt;/code&gt;、&lt;code&gt;background-color&lt;/code&gt;以及&lt;code&gt;a.code&lt;/code&gt;继承超链接的默认颜色：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
article
    ...
    &amp;amp;.article
        ...
        .content
            ...
            code
                ...
                padding: 1
                background-color: #f9f9f9
                ...
        a
            ...
            code
                color: inherit
                background-color: #f9f9f9
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面又添加了了各级标题的加粗、五级标题的斜体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;article
    &amp;amp;.article
        .content
            ...
            h1
                font-size: 1.75em
                font-weight: bold

            h2
                font-size: 1.5em
                font-weight: bold

            h3
                font-size: 1.25em
                font-weight: bold

            h4
                font-size: 1.125em
                font-weight: bold

            h5
                font-size: 1em
                font-weight: bold
                font-style: italic
            ...
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>在树莓派上部署openwrt</title><link>https://zlotus.github.io/2020/05/03/rbp3-openwrt/</link><guid isPermaLink="true">https://zlotus.github.io/2020/05/03/rbp3-openwrt/</guid><description>尝试在树莓派3B上部署openwrt</description><pubDate>Sun, 03 May 2020 19:49:00 GMT</pubDate><content:encoded>&lt;p&gt;五一放假，不想写代码不想看书也懒得追番，准备搞事情，把旧iPhone和一块旧树莓派（3B）废物重组一下，让iPhone体验一下Tiktok的沙雕。&lt;/p&gt;
&lt;p&gt;打算按照&lt;a href=&quot;https://mlapp.cn/369.html&quot;&gt;mlapp&lt;/a&gt;帖子里的固件和方法整，当然，有关树（Li）莓（nu）派（x）的任何东西都不会那么一帆风顺的……&lt;/p&gt;
&lt;h1&gt;概况&lt;/h1&gt;
&lt;p&gt;:::note[理论目标]
无线猫的LAN口接树莓派电口，然后把树莓派的WiFi当做热点用，树莓派写openwrt镜像，配置SSR，用单独的SSID，以实现在不影响现有WiFi和连接设备的情况下，接出一个带有科学功能的上网热点。
:::&lt;/p&gt;
&lt;h2&gt;现有环境&lt;/h2&gt;
&lt;p&gt;因为我最近才发现，家里用的TP-LINK的无线猫上已经连了很多设备了，智能音响、扫地机器人、空气净化器、树莓派、手机、电脑、电视等等十几个设备，直接改造TP-LINK影响太大了，而且一时半会上不了网，这个我是不能接受的。那么，只好从主猫上再接出一路WiFi。&lt;/p&gt;
&lt;p&gt;目前手里有好几块树莓派（但是我没买4，因为听说type-c有些无伤大雅的设计问题，完美主义者怎么能买这种东西呢？！），3B+、3B、2B我都有，于是选了个3B，因为性能还凑合，而且我只有一个2B舍不得用，要知道2B是不板载无线里性能最NB的了，万一哪天会在比较严格的环境试验这种东西，肯定首选这个没无线功能的了[捂脸]另外，3B+上面有DOA的麦阵列那一套环境，懒得删了，所以选用这个闲置的3B。&lt;/p&gt;
&lt;p&gt;iPhone吧是个6S，卡早换新手机上了，你要用Tiktok得骗它。因此，通过某瑞士安全邮箱服务、谷歌地图、一次性手机号服务等AppleID相关操作，再连上有科学功能的树莓派WiFi，它就真以为自己是个硅谷手机了。这个时候，装上Tiktok，就可以绕过字节跳动的分区管理了。&lt;/p&gt;
&lt;h1&gt;写镜像&lt;/h1&gt;
&lt;h2&gt;使用macOS向SD卡写镜像&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;失败&lt;/strong&gt;：用balenaEtcher写镜像直接报错，不管是&lt;a href=&quot;https://github.com/coolsnowwolf/lede&quot;&gt;lean&lt;/a&gt;官方的，还是&lt;a href=&quot;https://mlapp.cn/369.html&quot;&gt;mlapp&lt;/a&gt;提供的都说“镜像损坏”。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;失败&lt;/strong&gt;：用macOS的&lt;code&gt;dd&lt;/code&gt;强行写，虽然能写进去，但返回值是错误的（unix非0返回值，zsh红箭头），插入树莓派启动失败：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(base) ➜  ~ sudo dd bs=1m if=/Users/zealot/Downloads/openwrt-bcm27xx-bcm2710-rpi-3-squashfs-factory.img of=/dev/rdisk2
Password:
dd: /dev/rdisk2: Invalid argument
247+1 records in
247+0 records out
258998272 bytes transferred in 19.296779 secs (13421839 bytes/sec)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;网上搜了一下，推荐用Windows下的分区工具或者Linux下的&lt;code&gt;dd&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;使用虚拟机向SD卡写镜像&lt;/h2&gt;
&lt;p&gt;实在是因为手头拿不出Windows电脑或者USB读卡器[捂脸]。&lt;/p&gt;
&lt;p&gt;用以前在virtualbox建好的里的win7，不论怎么设置苹果内置的sdcard reader（虚拟机里显示为一个USB设备，不论mount状态、如何重启虚拟机都不行），都报设备busy的错误：&lt;/p&gt;
&lt;p&gt;查看sdcard：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;(base) ➜  ~ df -lh
Filesystem     Size   Used  Avail Capacity iused      ifree %iused  Mounted on
/dev/disk1s5  465Gi   11Gi   30Gi    26%  487852 4876640268    0%   /
/dev/disk1s1  465Gi  420Gi   30Gi    94% 2976908 4874151212    0%   /System/Volumes/Data
/dev/disk1s4  465Gi  4.0Gi   30Gi    12%       5 4877128115    0%   /private/var/vm
/dev/disk1s3  465Gi  504Mi   30Gi     2%      41 4877128079    0%   /Volumes/Recovery
/dev/disk2s1   64Mi   17Mi   47Mi    26%     512          0  100%   /Volumes/NO NAME
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是卸载sdcard：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;diskutil umount /dev/disk2s1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;查&lt;a href=&quot;https://superuser.com/questions/373463/how-to-access-an-sd-card-from-a-virtual-machine&quot;&gt;帖子&lt;/a&gt;说是因为后来的MBP不把读卡器暴露为USB设备了，按这个帖子做：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;插入SDcard，在终端里用&lt;code&gt;mount&lt;/code&gt;查看分区，确定设备名称。在我的电脑上，显示为“NO NAME”的卷就是SD卡，其对应的设备叫做&lt;code&gt;/dev/disk2s1&lt;/code&gt;，而这个&lt;code&gt;s1&lt;/code&gt;表示&lt;code&gt;/dev/disk2&lt;/code&gt;上的第一个分区，因此设备名其实应该叫做&lt;code&gt;/dev/disk2&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;(base) ➜  ~ mount
/dev/disk1s5 on / (apfs, local, read-only, journaled)
devfs on /dev (devfs, local, nobrowse)
/dev/disk1s1 on /System/Volumes/Data (apfs, local, journaled, nobrowse)
/dev/disk1s4 on /private/var/vm (apfs, local, journaled, nobrowse)
map auto_home on /System/Volumes/Data/home (autofs, automounted, nobrowse)
/dev/disk1s3 on /Volumes/Recovery (apfs, local, journaled, nobrowse)
/dev/disk2s1 on /Volumes/NO NAME (msdos, local, nodev, nosuid, noowners)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;打开电脑上“Disk Utility.app”，也就是“磁盘工具.app”，找到SD卡然后&lt;strong&gt;卸载&lt;/strong&gt;它，要注意是&lt;strong&gt;卸载&lt;/strong&gt;，而不是&lt;strong&gt;推出&lt;/strong&gt;。另外，可能在做完下一步后还需要在卸载一次，因为macOS有可能自动把SD卡装载回来。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用下面的命令，要注意的是，&lt;code&gt;-rawdisk&lt;/code&gt;后面跟的参数是&lt;code&gt;/dev/disk2&lt;/code&gt;，不是&lt;code&gt;/dev/disk2s1&lt;/code&gt;。另外，&lt;code&gt;/Users/zealot/VirtualBox\ VMs/WIN7/sdcard.vmdk&lt;/code&gt;这个路径写哪都行，图方便我直接放在我的WIN7虚拟机文件夹下了。：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;(base) ➜  ~ sudo VBoxManage internalcommands createrawvmdk -filename /Users/zealot/VirtualBox\ VMs/WIN7/sdcard.vmdk -rawdisk /dev/disk2
RAW host disk access VMDK file /Users/zealot/VirtualBox VMs/WIN7/sdcard.vmdk created successfully.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：若出现类似&quot;...vmdk uuid...does not match /var/root/Library/VirtualBox/VirtualBox.xml&quot;这种报错，可以考虑修改虚拟机配置文件如&lt;code&gt;WIN7.vbox&lt;/code&gt;或&lt;code&gt;VirtualBox.xml&lt;/code&gt;文件中的相应uuid。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;重新配置虚拟机：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确定虚拟机已关闭；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;打开“设置”-&amp;gt;“存储”，点控制器旁边的“添加虚拟磁盘”，并选择“使用现有的虚拟磁盘”；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;点击“注册”，选择上一步中生成的&lt;code&gt;/Users/zealot/VirtualBox\ VMs/WIN7/sdcard.vmdk&lt;/code&gt;；此处如果报权限错误，我试了&lt;code&gt;chmod&lt;/code&gt;和&lt;code&gt;chown&lt;/code&gt;改&lt;code&gt;.vmdk&lt;/code&gt;和&lt;code&gt;disk2s1&lt;/code&gt;的权限是没用的，我也试了关掉macOS的SIP同样是没用的，必须命令行管理员权限运行&lt;code&gt;sudo VirtualBox&lt;/code&gt;才可以。真的不太懂系统权限问题[捂脸]。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;双击列表中的&lt;code&gt;sdcard.vmdk&lt;/code&gt;，然后就会返回“存储”界面并看到这个设备。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;启动虚拟机，看到该设备显示为一个本地磁盘。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这一套做下来，我们的SD卡在win7里就挂载为了一个ATA设备，也就是硬盘，很不幸的是“Win32 Disk Imager”是不认硬盘的。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;因此，把4.2修改一下：打开“设置”-&amp;gt;“存储”，点控下方的“添加存储控制器”，选择菜单项“添加USB控制器”，这时候列表里多出来一个“USB控制器”，然后再按照上一步中2、3操作添加虚拟磁盘，然后选上“热插拔”和“固态驱动器”，这样就真的把SD卡伪装成USB设备了。此时Win7里出现两块硬盘，一个是ATA设备，一个是USB设备。再打开“Win32 Disk Imager”时，就可以选择这个盘了：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2020/rbp3-openwrt-1.png&quot; alt=&quot;配置为USB设备&quot; /&gt;&lt;/p&gt;
&lt;p&gt;开机搜索名为“openwrt”的无线SSID，连接，进入&lt;a href=&quot;http://192.168.1.1&quot;&gt;http://192.168.1.1&lt;/a&gt;，用“root/password”登录配置界面。&lt;/p&gt;
&lt;h1&gt;配置openwrt&lt;/h1&gt;
&lt;h2&gt;先让自己能上网&lt;/h2&gt;
&lt;p&gt;我家用的TP-LINK的猫，LAN口接树莓派，而这个TP-LINK默认是在192.168.1.1上的，所以先在openwrt配置界面中“网络”-&amp;gt;“接口”里改个IP，默认网关写在192.168.1.1上，然后写上俩TP-LINK里用的DNS（我的西安电信通常是&lt;code&gt;61.134.1.4&lt;/code&gt;和&lt;code&gt;218.30.19.40&lt;/code&gt;，西安移动通常是&lt;code&gt;211.137.130.19&lt;/code&gt;）就可以访问互联网了。&lt;/p&gt;
&lt;h2&gt;配置SS&lt;/h2&gt;
&lt;p&gt;先把本机的公钥先传到openwrt上，方便ssh登录，然后直接root登录吧，懒得弄用户了。&lt;/p&gt;
&lt;p&gt;如果&lt;code&gt;vim&lt;/code&gt;时报错遇到&lt;code&gt;&quot;E437: terminal capability &quot;cm&quot; required&quot;&lt;/code&gt;，需要执行一下&lt;code&gt;export TERM=xterm&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;继续，在openwrt管理页面中配置“ShadowSocksR Plus+”-&amp;gt;“服务器节点”，添加一个类型为“ShadowSocksR”的服务器就可以了，我的情况，主机名、端口、密码、加密方式填上，其他默认就可以直接联通了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2020/rbp3-openwrt-2.png&quot; alt=&quot;最终测试&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;github免密push&lt;/h1&gt;
&lt;p&gt;临时想到的，最近时不时要更新博客，push的时候总是让我输入用户名密码，很烦。因为我的github号是双因子的，push时总要输用户名，然后复制粘贴那个巨长的push key。&lt;/p&gt;
&lt;p&gt;github repo的免密push设置也很简单，最关键的是注意仓库的git&lt;strong&gt;协议&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;本机git仓库配置&lt;/h2&gt;
&lt;p&gt;在仓库中使用&lt;code&gt;git config -e&lt;/code&gt;命令修改配置文件，只有使用&lt;code&gt;git&lt;/code&gt;时才可以免密，&lt;code&gt;https&lt;/code&gt;不行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  [remote &quot;origin&quot;]
-         url = https://github.com/your-gh/your-repo.git
+         url = git@github.com:your-gh/your-repo.git
          fetch = +refs/heads/*:refs/remotes/origin/*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是hexo之类的博客，cli工具自动集成push功能，那么修改其配置文件的repo目标即可，比如我用的hexo，那么修改&lt;code&gt;_config.yml&lt;/code&gt;即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  deploy:
    type: git 
-   repo: https://github.com/your-gh/your-repo.git 
+   repo: git@github.com:your-gh/your-repo.git 
    branch: master 
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;远端github配置&lt;/h2&gt;
&lt;p&gt;可以在你的某个特定github-repo下的“settings”-&amp;gt;“deploy key”里写一个公钥，选上允许写权限。这个方法有个缺点，一个库会占一个key，下一个库就不能再用这个key了。&lt;/p&gt;
&lt;p&gt;也可以直接在github账号配置的“settings”-&amp;gt;“SSH and GPG keys”里写一个SSH公钥，这个号里所有的repo就都可以push了。其实这个方法更简单，推荐。&lt;/p&gt;
&lt;h1&gt;后记&lt;/h1&gt;
&lt;p&gt;工作这么多年，除了自学机器学习，最有用的就是去成都培训了一个月的Linux基础知识，打那时候起，就再也不虚Linux环境了，甚至编译报错都敢直接改源码了。所以说，对程序员来说，什么最重要？是以技术为基础产生的信念，遇到问题时，始终相信只要是计算机上的罗辑问题，都是可解的，就算当下搞不定，时间长了总能搞定。&lt;/p&gt;
</content:encoded></item><item><title>在Ant Design中写动态路由</title><link>https://zlotus.github.io/2020/04/09/umi-dynamic-routing/</link><guid isPermaLink="true">https://zlotus.github.io/2020/04/09/umi-dynamic-routing/</guid><description>如何再Ant Design中通过umi配置动态路由</description><pubDate>Thu, 09 Apr 2020 18:54:00 GMT</pubDate><content:encoded>&lt;p&gt;普通的路由像一个映射，通常就是一个类似&lt;code&gt;/path/sth&lt;/code&gt;的字符串，对应一个Project中存在的源码组件&lt;code&gt;/page/sth.js&lt;/code&gt;，你只要访问&lt;code&gt;/path/sth&lt;/code&gt;，代码就自动去调用&lt;code&gt;/page/sth.js&lt;/code&gt;文件展现给用户。但是，有的时候，我们希望访问&lt;code&gt;/path/sth/id&lt;/code&gt;，而页面也自动使用&lt;code&gt;/page/sth.js&lt;/code&gt;来响应，只不过会将路由中的&lt;code&gt;id&lt;/code&gt;以参数的形式告诉&lt;code&gt;sth.js&lt;/code&gt;组件，以展现具有相应参数的页面。&lt;/p&gt;
&lt;p&gt;比如，我最近在给自己写盯盘程序的UI，我的期望是，当我访问&lt;code&gt;/index/000300&lt;/code&gt;这个页面时，调用&lt;code&gt;/page/index/&lt;/code&gt;中的某&lt;code&gt;sth.js&lt;/code&gt;程序，并且让程序知道path中有个&lt;code&gt;000300&lt;/code&gt;的参数。类似的，当我访问&lt;code&gt;/index/000905&lt;/code&gt;、&lt;code&gt;/index/000015&lt;/code&gt;等页面时，同样调用&lt;code&gt;sth.js&lt;/code&gt;，并将&lt;code&gt;000905&lt;/code&gt;和&lt;code&gt;000015&lt;/code&gt;告诉&lt;code&gt;sth.js&lt;/code&gt;。此外，在配置路由的时候，只需要写&lt;code&gt;/index/indexCode&lt;/code&gt;就能让程序自动映射到&lt;code&gt;/page/index/&lt;/code&gt;中的某&lt;code&gt;sth.js&lt;/code&gt;程序就好了。&lt;/p&gt;
&lt;p&gt;在Ant Design中，要实现这个功能非常简单，参考&lt;a href=&quot;https://umijs.org/zh-CN/docs/convention-routing#%E5%8A%A8%E6%80%81%E8%B7%AF%E7%94%B1&quot;&gt;动态路由&lt;/a&gt;。&lt;/p&gt;
&lt;h1&gt;约定式路由的动态配置&lt;/h1&gt;
&lt;p&gt;在我的例子中，我需要明确的配置出路由树，因为程序的侧边栏菜单是直接通过路由树生成的，因此，需要做的就是在配置中如此编写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    // ...其他配置
    routes: [
        {
            path: &apos;/index/:code&apos;,
            component: &apos;index/[code]&apos;,
            // ...其他配置
        },
        {
            path: &apos;/fund/:code&apos;,
            component: &apos;fund/[code]&apos;,
            // ...其他配置
        }
    ]
    // ...其他配置
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就明确的指出凡是访问&lt;code&gt;/index/:code&lt;/code&gt;的（即&lt;code&gt;/index/000905&lt;/code&gt;、&lt;code&gt;/index/000015&lt;/code&gt;）都使用&lt;code&gt;/page/index/[code].js&lt;/code&gt;来渲染；凡是访问&lt;code&gt;/fund/:code&lt;/code&gt;的都使用&lt;code&gt;/page/fund/[code].js&lt;/code&gt;来渲染。&lt;/p&gt;
&lt;p&gt;对于真正的约定式路由就更简单了，只需要将project的文件系统组织成下面的形式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.
  └── pages
    └── [post]
      ├── index.tsx
      └── comments.tsx
    └── users
      └── [id].tsx
    └── index.tsx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;约定式路由会自动翻译成配置的路由（这些路由是不需要我们自己配置的，直接通过文件系统生成，这里只是展示理解方便）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  { exact: true, path: &apos;/&apos;, component: &apos;@/pages/index&apos; },
  { exact: true, path: &apos;/users/:id&apos;, component: &apos;@/pages/users/[id]&apos; },
  { exact: true, path: &apos;/:post/&apos;, component: &apos;@/pages/[post]/index&apos; },
  {
    exact: true,
    path: &apos;/:post/comments&apos;,
    component: &apos;@/pages/[post]/comments&apos;,
  },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;获取路由参数&lt;/h1&gt;
&lt;p&gt;以&lt;code&gt;index/:code&lt;/code&gt;到&lt;code&gt;/page/index/[code].js&lt;/code&gt;为例，当用户访问&lt;code&gt;/index/000905&lt;/code&gt;时，&lt;code&gt;[code].js&lt;/code&gt;可以直接通过组件的&lt;code&gt;props.match.params.code&lt;/code&gt;拿到URL中的&lt;code&gt;:code&lt;/code&gt;参数，也就是例子中的字符串&lt;code&gt;&apos;000905&apos;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// /page/index/[code].js
import React, { Component } from &apos;react&apos;
//...
@connect(mapStateToProps, mapDispatchToProps)
export default class IndexPage extends Component {
    constructor(props) {
        super(props);
        this.state = {
            code: this.props.match.params.code,
        };
    }
    //...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;其实这个功能有点类似&lt;code&gt;layout&lt;/code&gt;模板系统，只不过现在的模板是&lt;code&gt;/page/index/[code].js&lt;/code&gt;，并通过&lt;code&gt;props&lt;/code&gt;获取参数来进行相应的渲染。&lt;/p&gt;
</content:encoded></item><item><title>Ant Design 4.0中的新版connect函数</title><link>https://zlotus.github.io/2020/04/07/connect-in-antd-4/</link><guid isPermaLink="true">https://zlotus.github.io/2020/04/07/connect-in-antd-4/</guid><description>关于新版Ant Design中connect函数及修饰器的示例</description><pubDate>Tue, 07 Apr 2020 11:39:00 GMT</pubDate><content:encoded>&lt;p&gt;Ant Design最近推出了4.0版本，其中我最喜欢的改动是&lt;code&gt;connect&lt;/code&gt;函数/装饰器的语法糖，极大的降低了我这种智商欠费者的认知成本……&lt;/p&gt;
&lt;h1&gt;1 老版本中的&lt;code&gt;connect&lt;/code&gt;&lt;/h1&gt;
&lt;p&gt;我们先看看旧版&lt;a href=&quot;https://dvajs.com/guide/getting-started.html#connect-%E8%B5%B7%E6%9D%A5&quot;&gt;&lt;code&gt;connect&lt;/code&gt;&lt;/a&gt;函数的官方示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;;
import { connect } from &apos;dva&apos;;
import ProductList from &apos;../components/ProductList&apos;;

const Products = ({ dispatch, products }) =&amp;gt; {
    function handleDelete(id) {
        dispatch({
            type: &apos;products/delete&apos;,
            payload: id,
        });
    }

    return (
        &amp;lt;div&amp;gt;
            &amp;lt;h2&amp;gt;List of Products&amp;lt;/h2&amp;gt;
            &amp;lt;ProductList onDelete={handleDelete} products={products} /&amp;gt;
        &amp;lt;/div&amp;gt;
    );
};

// export default Products;
export default connect(({ products }) =&amp;gt; ({
  products,
}))(Products);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后三行的&lt;code&gt;connect&lt;/code&gt;函数，第一个参数是&lt;code&gt;mapStateToProps&lt;/code&gt;函数，用于将需要的部分&lt;code&gt;state&lt;/code&gt;传递进组件&lt;code&gt;Products&lt;/code&gt;。示例中虽然没写，但是&lt;code&gt;connect&lt;/code&gt;还可以再传递一个参数&lt;code&gt;mapDispatchToProps&lt;/code&gt;，用于将封装好的&lt;code&gt;dispatch&lt;/code&gt;传递给组件&lt;code&gt;Products&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;我原来经常被那一堆括号，和最后的&lt;code&gt;(Products)&lt;/code&gt;搞晕，导致忘记最后的那一对括号……&lt;/p&gt;
&lt;h1&gt;2 新版的&lt;code&gt;connect&lt;/code&gt;装饰器&lt;/h1&gt;
&lt;p&gt;上面的代码就变成了这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import React from &apos;react&apos;;
import { connect } from &apos;dva&apos;;
import ProductList from &apos;../components/ProductList&apos;;

const mapStateToProps = (state) =&amp;gt; {
    return { products: state.products }
};
const mapDispatchToProps = (dispatch) =&amp;gt; {
    return {
        handleDelete: (id) =&amp;gt; {
            dispatch({ type: &apos;products/delete&apos;, payload: id });
        },
    };
};

@connect(mapStateToProps, mapDispatchToProps)
export default class Products extends Component {
    render() {
        return (
            &amp;lt;div&amp;gt;
                &amp;lt;h2&amp;gt;List of Products&amp;lt;/h2&amp;gt;
                &amp;lt;ProductList onDelete={this.props.handleDelete} products={this.props.products} /&amp;gt;
            &amp;lt;/div&amp;gt;
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mapStateToProps&lt;/code&gt;和&lt;code&gt;mapDispatchToProps&lt;/code&gt;也都通过装饰器传入了组件的&lt;code&gt;props&lt;/code&gt;属性，但是这样的写法确实极大的降低了认知负担。&lt;/p&gt;
</content:encoded></item><item><title>写在庚子年初的春天</title><link>https://zlotus.github.io/2020/03/15/about-shared-future-for-mankind/</link><guid isPermaLink="true">https://zlotus.github.io/2020/03/15/about-shared-future-for-mankind/</guid><description>关于2020年的春天</description><pubDate>Sun, 15 Mar 2020 11:39:00 GMT</pubDate><content:encoded>&lt;p&gt;我已经很久没在这个博客下面写非技术类文章了，2020年的初的这几个月，真是开眼界了，不记录一下实在是对不起时间……&lt;/p&gt;
&lt;h2&gt;世界人民大团结&lt;/h2&gt;
&lt;p&gt;我是个没有地标打卡习惯的人，说白了是个对大部分事物提不起什么兴趣死宅。好几年前，去北京出过几次差，也并没有一次想要去天安门广场看看。说起来天安门城楼，我记得最清楚的是左右两句标语——“中华人民共和国万岁”和“世界人民大团结万岁”——第一句不必多说，咱自己的地标当然要强调咱自己人了，可这第二句啊，我小时候是一直不知道为啥要留在咱自己人的地标建筑上……&lt;/p&gt;
&lt;p&gt;今年，已经是我奔三后的第二年了，高中毕业这十年来，除了对代码感兴趣，也就是科幻、动漫和游戏了。当然，科幻是核心，动漫和游戏最好都带点科幻，大概是因为当下永远没有未来出彩吧。说起科幻，有个问题在我脑子里转了好多年，科幻作品里，只要涉及星际间的描写，作者们都不约而同的把星球写成“同一个星球，同一个政府”，可能在地球这种母星上会有一点点例外，但是也无外乎“东亚联邦”、“北美联邦”这种整合过的科幻政体。&lt;/p&gt;
&lt;p&gt;“世界人民大团结万岁”，好像是这么回事，当然，也可能是作者懒得描写星球内部政体细节了吧。不过，更有可能的是，在科幻小时作者的眼中，宇宙里一个弹丸泥球，自己都没完成内部整合，都没有一个组织能够调动起泥球上绝大多数的生产力，就别谈什么星际级别的故事了，火星都别想，甚至月球都别想。人类命运共同体、世界人民大团结，这是科幻概念吗？也许，在成熟的政治家眼中，这是不远的未来——就像在我们码农眼中，未来可能会出现one-for-all的代码互译程序，或是万能容器/虚拟机一样——就是个时间的事，走着走着自然就到了。&lt;/p&gt;
&lt;p&gt;最近这三四年里，我除了撸了点铁，也看了点历史故事。历史这东西，配着科幻看，其实很有意思。比如说，一百年前，人类对摩天大楼的幻想，就是高耸入云的建筑里，有各种生活所需的商铺——因为那时候的人对高速电梯没概念，觉得住那么高下来不方便，所以摩天大楼里应该有理发店之类的“民生类店铺”。科学对生活的影响是整体的，方方面面的，估计很难出现这种“有了造摩天大楼的技术却没有造电梯的技术”之类的场景。那么，这个“人类命运共同体”大概也是一样的道理吧。&lt;/p&gt;
&lt;p&gt;回到今天，2020年3月15日，一个把消费者憋在家里的日子，消费者们也渐渐觉得，这个庚子年，注定不太平。我们在一月见到了从否认到承认，在二月见到了从遮遮掩掩到众志成城。就在离自己不远的地方，看到的恐惧和愤怒，妥协和牺牲，有发国难财的不法商贩，也有虽千万人吾往矣的医护人员。进入三月，我们自己家算是暂时控制住了，别人家却又上演了我们一二月份的历史，就像在别的国家进行了版本回退——“历史给人类的唯一教训，就是人类从来不吸取教训”。这不，三月过了一半，全球的账面金融都在玩过山车，有不少人开始讨论普选民主、民主集中、专治、自由这种他们自己都不明白，甚至是人类自己都搞不清楚的词语，就像我们也不知道病毒到底算不算生命，或是究竟什么才算生命一样——嗯，约学NLP越觉得自然语言是个极度神奇的事物。&lt;/p&gt;
&lt;h2&gt;病毒面前主义平等&lt;/h2&gt;
&lt;p&gt;我是个不喜欢政治课的人，大概率是因为我中考祭出了年级倒数的好成绩，在幼小的心灵里留下了不可磨灭的伤痕。是这几年是因为没办法，喜欢上了看历史故事，就不得不了解政治，话说这个政治跟我差点没及格那个不太一样……这几年的个人感觉，这历史攒多了，一积分就变成了政治；政治攒多了，一积分就形成了文化。中国这片土地，说幸运是真的幸运，核心地带的地形地貌极其丰富，中原土地承载力极高，可割据可一统，北边太冷养不了太多人，东边是大海，南边是海+横断山脉，西边是青藏高原+河西走廊，这唯一的走廊还接着两个国中国作为缓冲——关中平原和四川盆地。中国的地理，就像个大肚子烧瓶，就河西走廊一个小口，咱也不好出去，别人也很难进来，可劲作么，于是，说不幸也真的不幸，咱这儿一夫作难而七庙隳的事也见的太多太多了。&lt;/p&gt;
&lt;p&gt;既然可以任性的作，咱就确实作出成果了，比如最近联储又被建国同志绑架印钞——咱武帝和桑弘羊试验过，自此之后，铸币权再不下放，货币政策慎之又慎；（所以啊，我真的不太看好比特币系列）。再比如最近讨论普选民主到底好不好，准四世三公家族的风暴降生、儒生团体的普选领导人、奥斯卡影帝一世、西汉终结者、东汉远爹、大善人、一世而亡者——王莽同志觉得太好了。这估计是蓝星上极度少有的几次，无战乱仅因为错误政策就让天下人口减半、经济崩溃的社会试验了。作为老百姓，有个观点我是十分赞同的，世界上的政府，只应该分为好政府和坏政府，其他分成民主啊、专制啊什么的，都是肉食者的遮羞布而已。汉初我们也试验过，上面都杀破天了，老刘家都快被老吕家连锅端了，老百姓照样黄老之治人口翻番。你看，肉食者厮杀，管我们素食者什么事，只要给我们黄老几十年，你们打到太阳上都没关系。&lt;/p&gt;
&lt;p&gt;这两天，咱终于能挤出来一些医疗专家去援助别的地方了，估计老外也开始考虑这种问题了。与其说我们有多好，倒不如说我们试了很多次不好的，其中几十次亡了国，一两次差点灭种，终于发现现在这个好像还能成，先将就着用，边用边修补吧。春秋战国封土建国、秦汉郡县带单分封、北方民族的融入、魏晋南北朝从豪族到士族、唐的朝贡体系、五代十国民族大融（血）合（洗）、宋辽金夏的岁币政策、元明清继续融（血）合（洗）、近现代的各种救亡图存尝试，到如今中国特色社会主义，这一路过来试的太多了。鲁迅先生说历史歪歪扭扭的写着仁义道德，瞧仔细了才发现是吃人二字，可不是么，咱码农一条命令过去成千上万的地址也清空了，那肉食者大笔一挥也会是类似的效果，所以啊，不管是编程还是制定政策，先要多想。&lt;/p&gt;
&lt;p&gt;随着生产力的发展，人口越来越多，码农都知道你FAT32管着的那点地址和NTFS的规模是不一样的，教小孩子都说量变引起质变，等到自己这县长郡守州牧就成一样的了吗？人家古希腊城邦普选，是在万把人的时候，人家确实好使着呢。问题是现在动辄亿把人的情形，你还往上直接套普选就显得有点不思进取了，这明摆着稀释选民的股权啊。你建国同志说普选民主好普选民主妙，我丘吉尔也说普选民主是最不差的制度，但是今天病毒来了，完全遵循传播规律，甚至有点好预测，于是，效率被第一次强行搬上了桌面，并且被民众按在了桌面上不能拿下去。这时候确实得讨论一下，各种主义的区别，啥是好的啥是不那么好的，或者说啥是坏的啥是不那么坏的。&lt;/p&gt;
&lt;h2&gt;科幻作品中的低熵体&lt;/h2&gt;
&lt;p&gt;怎么在一段特定时间内，简单的判断一个“主义”的优劣，我个人认为啊，降低社会的总熵，是一个比较好的标准。这个熵字，就是科幻作品里经常出现的低熵体那个意思。比如郡县代替了封建，政策一杆子插到底，在西汉看来熵降低了；士族兴起坞堡自治在后几百年融合混战看来，熵降低了，隋唐抵御外族入侵，节度使也看起来熵降低了……这种例子很多，而且并不全是正面例子，但是都是尽量让老百姓继续活下去的例子，留得百姓在就不怕没国建，看昭烈帝带着百姓出荆入蜀的，那都是革命的火种啊。&lt;/p&gt;
&lt;p&gt;这片土地上的政权，但凡没跑偏的，都相信“民为邦本，本固邦宁”。只要能有效的组织群众，和谐生产不打架，肉食者有肉吃，素食的有菜吃偶尔还能喝个汤，那就是好的“主义”。说起来最简单做起来最难的方法，比如“实事求是”，出自汉书，两千来年了；比如“放弃幻想”，近现代的发明。政治，从来都不是也不应该是选演员、演说家、道德楷模，而是应该选有能力让老百姓心无旁骛的从事安全生产的人。大家都是三百六十行里出来的，大家都要凭着数学物理化学生物语文英语历史地理政治混饭吃，凭什么你普选民主任命领导人就可以不需要考核专业科目，长得好看的、能说会道的、表情丰富的、能够调动情绪的、甚至擅于造假注水的就可以参与制定政策了？长得好看和制定政策有关系吗？昂，那我们这些丑的怎么办？！！估计啊，棺材里的薪王们要是活过来看到这种人，会叫他们弄臣。&lt;/p&gt;
&lt;p&gt;作为死宅，愿意的话甚至可以说说我奴性很重，因为你给我一根网线再临时拼个健身房，只要把粮草供上，我估计就能宅一辈子，我并不需要那种“Do you hear the people sing”式自由，毕竟我不太容易变成angry man，能让我安安稳稳的码代码上B站就成。天下太平的时候你怎么吹自己好都行，不太平的时候老百姓需抱大腿而不得，就容易导致遮羞布的大幅缩水。都说金融的核心是信心，你看政治也是啊。金融危机来了，老百姓相信央妈会稳住大多数人的钱袋子；病毒来了，老百姓相信国家会稳住大多数人的命根子，这就够了。要是有一天老百姓不再相信，那就真的会遇到“Do you hear the people sing”了。现在已经吃了肉的弄臣们，最好要明白过来，有问题的政策是会出人命的，而且那些人命对现役的肉食者来说可不只是数字，太平年景王莽都能十几年死几千万，最后脑袋被人当球踢，更别说最近并不太平了。&lt;/p&gt;
&lt;h2&gt;人类命运共同体&lt;/h2&gt;
&lt;p&gt;这次的医疗援助和数据共享只是在病毒突发事件中有了突出体现，全球化的大趋势早已形成且难以扭转，毕竟熵降低了，效率上去了，能享受到的更多了还更便宜了，各国老百姓都是喜闻乐见的。这次是医疗卫生系统的合作，谁知道下一次是不是航空航天船舶核能电子系统的深入合作呢？&lt;/p&gt;
&lt;p&gt;人类命运共同体，世界人民大团结，这种事如果真的能成，那我觉得儒家学说再改一改磨合磨合还是可以用的，简称儒家改。从董仲舒天人三策以来，儒家基本就是权力的外衣。衣服嘛，好看很重要，毕竟需要被大众认同，而法家通常是贴身小棉袄，没怎么大改过——正所谓霸王道杂之——外儒内法的组合应该还是能继续用很久的。这种体系建立之初，就“幸运”的遇到武帝这种作大死的，几乎把一个帝王能脑洞出来的点子都试了然后被逼无奈轮台罪己，之后“幸运”的遇到霍光宣帝拼老命软着陆，更“幸运”王莽就更极端的玩起了“普选民主”，基本上把整套体系玩崩了，最后最“幸运”的光武集齐云台二十八将拯救世界，于是除了君权神授这种权力加成以外，君王们又多了个避免苍天警示的重要责任。如此一来，这种原本纯粹为服务权力诞生的体系，居然开始自洽并真的走上了“民为贵，社稷次之，君为轻”的道路，这一路上君比较沉的基本也都沉了。此外，我们的史官制度也是非常NB的古代日志服务器作为辅助。这套体系确实很适合各族人民大团结，因为它不讲自由民主这些概念，它的目的就是让老百姓能在白天安安心心的耕田，在晚上快快乐乐的造人，老百姓即是生产力，也是生产资料，毕竟食色性也，活着并且继续活下去很重要。当然，我们不是复辟者，老百姓的浑浑噩噩直接或间接的导致科技停滞不前，没能形成科学体系，古人的科技树点的太偏了，最后在形成了大清这个人类历史上的集权巅峰，最后被坚船利炮轰的粉碎。虽然这个体系缺点很多，但是优点还是有的，比如千百年来的民族融合。&lt;/p&gt;
&lt;p&gt;说起民族融合，不得不提到政治正确，这也是一种民族融合的方式，立竿见影，药效特猛，核心就是我尊重你，我认同你。而我们的民族融合是另一种思路，既然你来了，你要尊重我，你要认同我。听着不好听，药效也慢，但是效果好，毕竟我的东西玩了上千年，稳定啊，你认同我了，大家就都稳定了。政治正确是因为别的民族不是自愿来的，所以我得尊重你啊，毕竟你的先辈们是我强行运来的奴隶；而我们这种恰恰相反，我是灯塔，你们自愿来了，那就要守我的规矩。如果有的地方政治正确这条路走的不顺利，也是可以试试我们过去民族融合的老路，就像股市一样，试了不好可以关掉嘛。&lt;/p&gt;
&lt;p&gt;作为科幻伪粉，我还是真的挺希望在有生之年能看到小说里描写的那样，一个星球一个政体，毕竟没见过，挺好奇的。到那时候，生产力应该已经发展到可以让我这种贫下中农从东航买到四折以下去火星的船票了吧。&lt;/p&gt;
&lt;h2&gt;星辰大海后的柴米油盐&lt;/h2&gt;
&lt;p&gt;淡扯完了，说回这个病毒。湖北人民真是倒了八辈子血霉，遇到这么个破事，然而就像鲁迅先生说的，悲喜并不相通，估计武汉老百姓早都被“加油”加的麻木了。我是那种吃不了苦的人，现在想到医护人员要带着尿不湿穿着隔离服，勒着护目镜和口罩一干一整天，我就特别虚；想到湖北那边总会有人出现物资短缺，那些得了病毒以外的特殊病情需要照顾而不得，也挺害怕的。但是，悲喜确实不相通，大家也只能慢慢熬了，可能我们离得远的能熬的轻松点，处在漩涡中央的熬的痛苦一些罢了。老百姓嘛，只能希望这些破事不要发生在自己身上，强身健体多读书多思考、尊重概率远离危墙，剩下的看老天的心情。&lt;/p&gt;
&lt;p&gt;该说的不该说的说完舒服了很多~以后继续专注更新码农贴。&lt;/p&gt;
</content:encoded></item><item><title>Python中的协程使用举例</title><link>https://zlotus.github.io/2020/02/22/py-async-example/</link><guid isPermaLink="true">https://zlotus.github.io/2020/02/22/py-async-example/</guid><description>Python中最常用的两种I/O协程写法</description><pubDate>Sat, 22 Feb 2020 18:54:00 GMT</pubDate><content:encoded>&lt;p&gt;新冠肺炎放假快一个月，新年一开盘暴跌，就又想写跟踪大盘指数的代码了……&lt;/p&gt;
&lt;p&gt;由于GIL名声在外，所以Python的线程编程我是从来没实际操作过……不过随着Python3中各大版本对协程支持的力度加大，在同一个进程内，可以考虑通过协程增加代码的执行效率了，尤其是在I/O部分。&lt;/p&gt;
&lt;p&gt;在Python3.6/7/8中（尤其是7/8），对协程的支持使得编写一个协程变得异常简单，这里就介绍两种最常见的情况。（本文使用Python3.8版本）&lt;/p&gt;
&lt;h2&gt;1 异步执行传统函数&lt;/h2&gt;
&lt;p&gt;举个例子，比如我希望&lt;code&gt;requests.get&lt;/code&gt;多个页面&lt;code&gt;http://www.baidu.com&lt;/code&gt;，可以这样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def make_requests():
    loop = asyncio.get_event_loop()
    future_list = []
    url = &apos;http://www.baidu.com&apos;
    for i in range(5): 

        future = loop.run_in_executor(None, requests.get, url)
        future_list.append(future)
    
    response_list = await asyncio.gather(*future_list)
    return response_list

rps_list = asyncio.run(make_requests())
print(rps_list)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主要就是将普通函数放入&lt;code&gt;loop.run_in_executor&lt;/code&gt;中生成一个&lt;code&gt;future&lt;/code&gt;对象，然后把这些&lt;code&gt;future&lt;/code&gt;交给&lt;code&gt;asyncio.gather&lt;/code&gt;去统一执行，并返回结果。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，&lt;code&gt;requests&lt;/code&gt;的复杂请求，如&lt;code&gt;post&lt;/code&gt;请求或通过&lt;code&gt;requests.Session&lt;/code&gt;设置&lt;code&gt;header&lt;/code&gt;后再通过&lt;code&gt;session.send&lt;/code&gt;发送的请求，使用&lt;code&gt;asyncio.gather&lt;/code&gt;执行会报&lt;code&gt;requests.exceptions.InvalidSchema&lt;/code&gt;异常，我猜测可能因为复杂请求调用了线程相关的适配器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码异步请求了5次百度页面，结果如下，用&lt;code&gt;timeit&lt;/code&gt;测试比同步请求快了4倍左右，还是符合期望的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[&amp;lt;Response [200 OK]&amp;gt;, &amp;lt;Response [200 OK]&amp;gt;, &amp;lt;Response [200 OK]&amp;gt;, &amp;lt;Response [200 OK]&amp;gt;, &amp;lt;Response [200 OK]&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2 异步执行协程&lt;/h2&gt;
&lt;p&gt;这个就更简单了，依然用上面的例子，&lt;code&gt;requests&lt;/code&gt;这个库通常是用在同步场景中的。如果希望既要使用异步还要享受&lt;code&gt;requests&lt;/code&gt;的友好API，可以试试&lt;a href=&quot;https://www.python-httpx.org/&quot;&gt;&lt;code&gt;httpx&lt;/code&gt;&lt;/a&gt;，这个库包含了异步调用的请求支持，API与同步版的几乎没有差别，十分方便：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async def make_requests():
    async with httpx.AsyncClient() as client:
        url = &apos;http://www.baidu.com&apos;
        req_task_list, rsp_list = [], []
        for i in range(5): 
            method, data, headers = &apos;GET&apos;, {}, {}
            req_task_list.append(asyncio.create_task(client.request(method, url, data=data, headers=headers)))

        for task in req_task_list:
            rsp = await task
            rsp_list.append(rsp)
    return rsp_list

rps_list = asyncio.run(make_requests())
print(rps_list)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到&lt;code&gt;httpx&lt;/code&gt;直接提供了用于异步请求的&lt;code&gt;AsyncCLient&lt;/code&gt;，即使是复杂请求也可以进行异步请求了。我们的主要工作就是通过&lt;code&gt;asyncio.create_task&lt;/code&gt;批量创建&lt;code&gt;Task&lt;/code&gt;，再统一&lt;code&gt;await task&lt;/code&gt;即可。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;注意，始终要记得&lt;code&gt;await&lt;/code&gt;其实就是&lt;code&gt;yield&lt;/code&gt;的语法糖，如果创建一个&lt;code&gt;task&lt;/code&gt;直接&lt;code&gt;await&lt;/code&gt;，然后再创建下一个，就变成同步请求了。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;结果以及获得结果的速度与第一节的例子相差无几：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[&amp;lt;Response [200 OK]&amp;gt;, &amp;lt;Response [200 OK]&amp;gt;, &amp;lt;Response [200 OK]&amp;gt;, &amp;lt;Response [200 OK]&amp;gt;, &amp;lt;Response [200 OK]&amp;gt;]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3 总结&lt;/h2&gt;
&lt;p&gt;通过协程在同一个进程内进行诸如I/O请求的耗时操作，可以极大的提升代码执行效率，而且随着Python大版本的更新，将引入越来越多的异步支持。&lt;/p&gt;
</content:encoded></item><item><title>自然场景中的中文识别尝试</title><link>https://zlotus.github.io/2019/08/24/rbp3-chinese-text-dection/</link><guid isPermaLink="true">https://zlotus.github.io/2019/08/24/rbp3-chinese-text-dection/</guid><description>在树莓派上做自然场景中的中文识别</description><pubDate>Sat, 24 Aug 2019 21:09:11 GMT</pubDate><content:encoded>&lt;p&gt;想做个考试作弊系统[捂脸]……&lt;/p&gt;
&lt;h1&gt;又想作弊了&lt;/h1&gt;
&lt;p&gt;最近想整个通过图像识别自然场景中的中文“试卷” -&amp;gt; 自然语言处理 -&amp;gt; 查找题库 -&amp;gt; 返回结果的小程序。目前考虑的是，图像通过树莓派的摄像头产生，将结果输出到柔性墨水屏上。&lt;/p&gt;
&lt;h2&gt;1 Connectionist Text Proposal Network&lt;/h2&gt;
&lt;p&gt;据说ctpn算法在长文本上也能够保持一个不错的效果，先试试吧&lt;/p&gt;
&lt;h3&gt;1.1 虚拟环境&lt;/h3&gt;
&lt;p&gt;一开始想用&lt;code&gt;pipenv&lt;/code&gt;，不过在安装&lt;code&gt;Cython&lt;/code&gt;的&lt;code&gt;lock package&lt;/code&gt;的时候就挂起不动了，搜了一下估计是跟跟网络和依赖有关系，因此就直接用Python自带的&lt;code&gt;venv&lt;/code&gt;做虚拟环境管理了。&lt;/p&gt;
&lt;p&gt;虚拟环境主要需要安装tensorflow，所以新建一个&lt;code&gt;tf&lt;/code&gt;虚拟空间并激活：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 -m venv tf
cd tf
source bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;包依赖有&lt;code&gt;Cython&lt;/code&gt;、&lt;code&gt;numpy&lt;/code&gt;、&lt;code&gt;opencv&lt;/code&gt;。安装&lt;code&gt;numpy&lt;/code&gt;之前记得安装&lt;code&gt;sudo apt -y install libatlas-base-dev&lt;/code&gt;，不然会报&lt;code&gt;libf77blas.so.3&lt;/code&gt;找不到的错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install numpy
# 直接pip（在2019-09-18）会安装到1.13.1版本的
pip install tensorflow
# 搜了一个新的1.14.0版本的
#wget https://github.com/lhelontra/tensorflow-on-arm/releases/download/v1.14.0-buster/tensorflow-1.14.0-cp37-none-linux_armv7l.whl
# pip install tensorflow-1.14.0-cp37-none-linux_armv7l.whl
proxychains4 git clone https://github.com/eragonruan/text-detection-ctpn.git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试&lt;code&gt;import tensorflow&lt;/code&gt;的时候会报一堆&lt;code&gt;Future Warning&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;1.2 安装OpenCV&lt;/h3&gt;
&lt;p&gt;安装OpenCV很麻烦，单独拉出来写。&lt;/p&gt;
&lt;h4&gt;1.2.1 准备工作&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;打开摄像头：&lt;code&gt;sudo raspi-config&lt;/code&gt; -&amp;gt; &lt;strong&gt;Enable camera&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;扩展存储卡空间，以防不测：&lt;code&gt;sudo raspi-config&lt;/code&gt; -&amp;gt; &lt;strong&gt;Advanced Options&lt;/strong&gt; -&amp;gt; &lt;strong&gt;Expand filesystem&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;重启：&lt;code&gt;sudo reboot -h now&lt;/code&gt;，检查空间&lt;code&gt;df -h&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;em&gt;可选&lt;/em&gt;，清理不常用的程序：&lt;pre&gt;&lt;code&gt;sudo apt-get purge wolfram-engine
sudo apt-get purge libreoffice*
sudo apt-get clean
sudo apt autoremove
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;1.2.2 安装OpenCV依赖&lt;/h4&gt;
&lt;p&gt;注意，我在使用lite系统的时候，会遇到关于NEON的cmake错误，在桌面版下一切正常。&lt;/p&gt;
&lt;p&gt;更新系统：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;time sudo apt update &amp;amp;&amp;amp; time sudo apt upgrade &amp;amp;&amp;amp; time sudo apt dist-upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装&lt;code&gt;cmake&lt;/code&gt;等开发工具：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y build-essential cmake unzip pkg-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装图片和视频相关库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y libjpeg-dev libpng-dev libtiff-dev
sudo apt install -y libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt install -y libxvidcore-dev libx264-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;可选&lt;/em&gt;，安装GUI库GTK以及能减少GTK警告的库[捂脸]（名字中的&lt;code&gt;*&lt;/code&gt;号可以匹配到ARM版的库）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y libgtk-3-dev
sudo apt install -y libcanberra-gtk*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为&lt;code&gt;OpenCV&lt;/code&gt;安装数值优化库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y libatlas-base-dev gfortran
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后安装Python的头文件库：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install python3-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2.3 下载OpenCV&lt;/h4&gt;
&lt;p&gt;下载&lt;a href=&quot;https://github.com/opencv/opencv&quot;&gt;opencv&lt;/a&gt;和附加的模块&lt;a href=&quot;https://github.com/opencv/opencv_contrib&quot;&gt;opencv_contrib&lt;/a&gt;，这些附加模块和函数可能会被经常用到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#proxychains4 wget -O opencv_4.1.1.zip https://github.com/opencv/opencv/archive/4.1.1.zip
#proxychains4 wget -O opencv_contrib_4.1.1.zip https://github.com/opencv/opencv_contrib/archive/4.1.1.zip
proxychains4 wget -O opencv_4.1.0.zip https://github.com/opencv/opencv/archive/4.1.0.zip
proxychains4 wget -O opencv_contrib_4.1.0.zip https://github.com/opencv/opencv_contrib/archive/4.1.0.zip
#proxychains4 wget -O opencv_4.0.1.zip https://github.com/opencv/opencv/archive/4.0.1.zip
#proxychains4 wget -O opencv_contrib_4.0.1.zip https://github.com/opencv/opencv_contrib/archive/4.0.1.zip
#proxychains4 wget -O opencv_4.0.0.zip https://github.com/opencv/opencv/archive/4.0.0.zip
#proxychains4 wget -O opencv_contrib_4.0.0.zip https://github.com/opencv/opencv_contrib/archive/4.0.0.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unzip opencv_4.1.0.zip
unzip opencv_contrib_4.1.0.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：我尝试编译4.1.1版，在编译到49%的时候总会出现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ 49%] Linking CXX shared library ../../lib/libopencv_imgproc.so
[ 49%] Built target opencv_imgproc
make: *** [Makefile:163: all] Error 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;换4.1.0版就好了，耗时数小时。&lt;/p&gt;
&lt;h4&gt;1.2.4 CMake及编译&lt;/h4&gt;
&lt;p&gt;使用&lt;code&gt;cmake&lt;/code&gt;构建编译，再用&lt;code&gt;make&lt;/code&gt;进行编译，这一步非常耗时。先新建一个&lt;code&gt;build&lt;/code&gt;文件夹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd opencv-4.1.0
mkdir build
cd build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行&lt;code&gt;cmake&lt;/code&gt;，其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OPENCV_ENABLE_NONFREE=ON&lt;/code&gt;标识可以让我们在OpenCV 4中使用&lt;strong&gt;SIFT/SURF&lt;/strong&gt;等专利算法。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OPENCV_GENERATE_PKGCONFIG=ON&lt;/code&gt;生成&lt;code&gt;opencv4.pc&lt;/code&gt;用于后面darknet的编译。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：保证&lt;code&gt;OPENCV_EXTRA_MODULES_PATH&lt;/code&gt;的路径正确，不然会出现类似 &lt;em&gt;sys/videoio.h: No such file or directory&lt;/em&gt; 的错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D OPENCV_EXTRA_MODULES_PATH=~/ocr/opcv/opencv_contrib-4.1.0/modules \
    -D ENABLE_NEON=ON \
    -D ENABLE_VFPV3=ON \
    -D BUILD_TESTS=OFF \
    -D OPENCV_ENABLE_NONFREE=ON \
    -D OPENCV_GENERATE_PKGCONFIG=ON \
    -D INSTALL_PYTHON_EXAMPLES=OFF \
    -D BUILD_EXAMPLES=OFF ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来要为树莓派&lt;strong&gt;增加交换区空间&lt;/strong&gt;，以便能够利用全部的四颗核心，否则可能会因为内存耗尽而挂起编译，打开&lt;code&gt;/etc/dphys-swapfile&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/dphys-swapfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;CONF_SWAPSIZE&lt;/code&gt;标识，我们将交换区从100M增加到2048M：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# set size to absolute value, leaving empty (default) then uses computed value
#   you most likely don&apos;t want this, unless you have an special disk situation
# CONF_SWAPSIZE=100
CONF_SWAPSIZE=2048
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启交换服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：增加交换区空间有可能导致存储卡卡损坏，因为基于闪存的存储器的读写次数是有限的。我们这里仅在编译时增加交换区空间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;编译&lt;/strong&gt;，用&lt;code&gt;-j4&lt;/code&gt;告诉&lt;code&gt;make&lt;/code&gt;使用四颗核心，这是最耗时的一步，大约用了4-5小时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make -j4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo make install
sudo ldconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;恢复交换区空间&lt;/strong&gt;，打开&lt;code&gt;/etc/dphys-swapfile&lt;/code&gt;文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/dphys-swapfile
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恢复&lt;code&gt;CONF_SWAPSIZE=100&lt;/code&gt;，并重启交换服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2.5 为Python虚拟环境建立OpenCV 4连接&lt;/h4&gt;
&lt;p&gt;在我们的&lt;code&gt;tf&lt;/code&gt;虚拟环境中建立OpenCV的软链接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -s /usr/local/lib/python3.7/site-packages/cv2/python-3.7/cv2.cpython-37m-arm-linux-gnueabihf.so /home/pi/cn_dect/tf/lib/python3.7/site-packages/cv2.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软链接的源和目的路径&lt;strong&gt;一定要写正确&lt;/strong&gt;，其中源路径可以在安装时&lt;code&gt;sudo make install&lt;/code&gt;命令的输出中看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- Installing: /usr/local/lib/python3.7/site-packages/cv2/__init__.py
-- Installing: /usr/local/lib/python3.7/site-packages/cv2/load_config_py2.py
-- Installing: /usr/local/lib/python3.7/site-packages/cv2/load_config_py3.py
-- Installing: /usr/local/lib/python3.7/site-packages/cv2/config.py
-- Installing: /usr/local/lib/python3.7/site-packages/cv2/python-3.7/cv2.cpython-37m-arm-linux-gnueabihf.so
-- Set runtime path of &quot;/usr/local/lib/python3.7/site-packages/cv2/python-3.7/cv2.cpython-37m-arm-linux-gnueabihf.so&quot; to &quot;/usr/local/lib&quot;
-- Installing: /usr/local/lib/python3.7/site-packages/cv2/config-3.7.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;1.2.6 测试OpenCV&lt;/h4&gt;
&lt;p&gt;测试虚拟环境中的OpenCV：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Python 3.7.3 (default, Apr  3 2019, 05:39:12)
[GCC 8.2.0] on linux
Type &quot;help&quot;, &quot;copyright&quot;, &quot;credits&quot; or &quot;license&quot; for more information.
&amp;gt;&amp;gt;&amp;gt; import cv2
&amp;gt;&amp;gt;&amp;gt; cv2.__version__
&apos;4.1.0&apos;
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成功。&lt;/p&gt;
&lt;h3&gt;1.3 测试text-detection-ctpn&lt;/h3&gt;
&lt;p&gt;无奈的是ctpn训练好的模型很大，一加载TensorFlow就报错，只好再把swap区改成2048[捂脸]。不知道这么玩下去存储卡能撑多久……&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python ./main/demo.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试ctpn检测，还是比较慢的，第一张图在树莓派上就用了35秒。我准备再试试&lt;a href=&quot;https://github.com/chineseocr/chineseocr&quot;&gt;chineseocr&lt;/a&gt;这种端到端杂烩项目。&lt;/p&gt;
&lt;h2&gt;2 OCR&lt;/h2&gt;
&lt;p&gt;大概扫了一眼&lt;a href=&quot;https://github.com/chineseocr/chineseocr&quot;&gt;chineseocr&lt;/a&gt;这个项目，估计是用yolo3/darknet做的文本检测，再用基于pytorch的crnn做检测到的文本的ocr。&lt;/p&gt;
&lt;h3&gt;2.1 虚拟环境&lt;/h3&gt;
&lt;p&gt;还是用原来的虚拟环境&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ~/cn_dect/tf
source ./bin/activate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在虚拟环境中安装依赖（由于前面我们在本机上编译了OpenCV，所以不用再装opencv-contrib-python了）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install scipy numpy easydict Cython h5py lmdb mahotas pandas requests bs4 matplotlib lxml pillow web.py keras 
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.2 编译 &amp;amp; 配置darknet&lt;/h3&gt;
&lt;p&gt;下载项目和darknet：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 git clone https://github.com/chineseocr/chineseocr.git
proxychains4 git clone https://github.com/pjreddie/darknet.git 
mv darknet chineseocr/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;darknet/Makefile&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GPU=0
CUDNN=0
OPENCV=1
OPENMP=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接&lt;code&gt;make&lt;/code&gt;时提示&lt;code&gt;PKG_CONFIG_PAT&lt;/code&gt;中找不到&lt;code&gt;opencv.pc&lt;/code&gt;，手动帮它找到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo cp /usr/local/lib/pkgconfig/opencv4.pc /usr/local/lib/pkgconfig/opencv.pc
export PKG_CONFIG_PAT=/usr/local/lib/pkgconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译过程出现&lt;code&gt;./src/image_opencv.cpp:12:1: error: ‘IplImage’ does not name a type&lt;/code&gt;的错误，这是由于我们用的新版OpenCV导致的兼容性问题，此时需要&lt;code&gt;pull&lt;/code&gt;一个&lt;a href=&quot;https://github.com/pjreddie/darknet/pull/1348&quot;&gt;补丁&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git fetch origin pull/1348/head:opencv4
git checkout opencv4
make -j4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把&lt;code&gt;darknet/python/darknet.py&lt;/code&gt;第48行改成刚编译好的&lt;code&gt;libdarknet.so&lt;/code&gt;的路径：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lib = CDLL(&quot;/home/pi/cn_dect/chineseocr/darknet/libdarknet.so&quot;, RTLD_GLOBAL)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.3 下载OCR模型&lt;/h3&gt;
&lt;p&gt;因为是&lt;a href=&quot;https://pan.baidu.com/s/1gTW9gwJR6hlwTuyB6nCkzQ&quot;&gt;百度网盘上的东西&lt;/a&gt;，我用bnd2加了个速。&lt;/p&gt;
&lt;p&gt;将下载的所有文件放在&lt;code&gt;models&lt;/code&gt;文件夹下。（如果是用&lt;code&gt;python -m http.server 8000&lt;/code&gt;做临时中转，&lt;code&gt;wget -r http://192.168.1.100:8000&lt;/code&gt;可以递归下载下所有目录和文件）&lt;/p&gt;
&lt;h3&gt;2.4 安装PyTorch&lt;/h3&gt;
&lt;p&gt;安装系统依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y libopenblas-dev libblas-dev m4 cmake cython python3-dev python3-yaml python3-setuptools libatomic-ops-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载并安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir pytorch_install &amp;amp;&amp;amp; cd pytorch_install
proxychains4 git clone --recursive https://github.com/pytorch/pytorch
cd pytorch
git submodule sync
git submodule update --init --recursive
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;貌似&lt;code&gt;protobuf&lt;/code&gt;这个库有个bug，会导致在编译近半时出现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[ 43%] Linking CXX executable ../../../bin/protoc
/usr/bin/ld: ../../../lib/libprotobuf.a(arena.cc.o): in function `google::protobuf::internal::ArenaImpl::Init()&apos;:
arena.cc:(.text+0x24): undefined reference to `__atomic_fetch_add_8&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要更新一下&lt;code&gt;protobuf&lt;/code&gt;修复bug：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 git submodule update --remote third_party/protobuf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为我试了好几次了，因此用&lt;code&gt;export BUILD_TEST=0&lt;/code&gt;跳过测试以缩短编译时间，设置环境并编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export NO_CUDA=1
export NO_DISTRIBUTED=1
export NO_MKLDNN=1 
export NO_NNPACK=1
export NO_QNNPACK=1
export BUILD_TEST=0

python3 setup.py build
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来正式安装，安装前需要注意的是，一定要记得带着上面的&lt;code&gt;export&lt;/code&gt;的环境变量安装（因为编译时间太长，有时候编译完了重启就忘了设置环境变量）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 setup.py install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;小注意一下：如果安装完成后不切换工作目录直接试就会出现&lt;code&gt;No module named &apos;torch._C&apos;&lt;/code&gt;的异常，因为编译目录下就有一个&lt;code&gt;torch&lt;/code&gt;目录，切换工作目录再启动Python即可。&lt;/p&gt;
&lt;h3&gt;2.5 安装CTC模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install wget
git clone --recursive https://github.com/parlance/ctcdecode.git   
cd ctcdecode   
pip install .
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;--recursive&lt;/code&gt;指将引用的第三方git repo一并克隆。安装会持续个把小时，可以追几集番了。&lt;/p&gt;
&lt;h3&gt;2.6 下载语言模型&lt;/h3&gt;
&lt;p&gt;不到3个G，挂梯子下载了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd chineseocr/models/
proxychains4 wget https://deepspeech.bj.bcebos.com/zh_lm/zh_giga.no_cna_cmn.prune01244.klm
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.7 运行&lt;/h3&gt;
&lt;p&gt;按自己的情况修改&lt;code&gt;config.py&lt;/code&gt;文件，运行&lt;code&gt;python app.py 8080&lt;/code&gt;即可。在树莓派3B+上的结果是，内存耗尽[捂脸]总共不到1G的内存，连一个CTC模型都读不进去[捂脸]&lt;/p&gt;
&lt;p&gt;更新到最新的&lt;code&gt;app&lt;/code&gt;分支&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git pull origin app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这下炸出了一个貌似是bug的问题……这个分支（git版本&lt;code&gt;daabebc93a8b4436a3653f83668429ab99eefaea&lt;/code&gt;）跟以前最大的不同就是一开始就在用&lt;code&gt;keras&lt;/code&gt;和&lt;code&gt;tensorflow&lt;/code&gt;创建yolo3的&lt;code&gt;darknet&lt;/code&gt;，结果在&lt;code&gt;text/keras_yolo3.py&lt;/code&gt;的第285行&lt;code&gt;boxes  = concatenate(boxes, axis=0)&lt;/code&gt;报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; boxes  = concatenate(boxes, axis=0)
Traceback (most recent call last):
  File &quot;&amp;lt;stdin&amp;gt;&quot;, line 1, in &amp;lt;module&amp;gt;
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/keras/layers/merge.py&quot;, line 687, in concatenate
    return Concatenate(axis=axis, **kwargs)(inputs)
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/keras/engine/base_layer.py&quot;, line 630, in __call__
    base_layer_utils.create_keras_history(inputs)
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/keras/engine/base_layer_utils.py&quot;, line 199, in create_keras_history
    _, created_layers = _create_keras_history_helper(tensors, set(), [])
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/keras/engine/base_layer_utils.py&quot;, line 245, in _create_keras_history_helper
    layer_inputs, processed_ops, created_layers)
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/keras/engine/base_layer_utils.py&quot;, line 245, in _create_keras_history_helper
    layer_inputs, processed_ops, created_layers)
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/keras/engine/base_layer_utils.py&quot;, line 245, in _create_keras_history_helper
    layer_inputs, processed_ops, created_layers)
  [Previous line repeated 2 more times]
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/keras/engine/base_layer_utils.py&quot;, line 243, in _create_keras_history_helper
    constants[i] = backend.function([], op_input)([])
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/keras/backend.py&quot;, line 3349, in __call__
    run_metadata=self.run_metadata)
  File &quot;/home/pi/cn_dect/tf/lib/python3.7/site-packages/tensorflow_core/python/client/session.py&quot;, line 1450, in __call__
    run_metadata_ptr)
tensorflow.python.framework.errors_impl.InvalidArgumentError: You must feed a value for placeholder tensor &apos;Placeholder_367&apos; with dtype float and shape [2]
         [[{{node Placeholder_367}}]]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我目前的环境是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Python 3.7.3 (default, Apr  3 2019, 05:39:12)
[GCC 8.2.0] on linux
# TensorFlow版本
&amp;gt;&amp;gt;&amp;gt; tf.__version__
&apos;1.13.1&apos;
# keras版本
&amp;gt;&amp;gt;&amp;gt; K.__version__
&apos;2.2.5&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一步一步的在REPL中调试，只要一&lt;code&gt;concatenate&lt;/code&gt;，&lt;code&gt;Placeholder_367&lt;/code&gt;就报错，这个Placeholder就是&lt;code&gt;text/keras_detect.py&lt;/code&gt;中第23行就初始化了的那个&lt;code&gt;input_shape&lt;/code&gt;。不知道为什么，我在我自己的电脑上试了，同样的代码，完全没有问题，咱也不懂不知道，咱也懒得在github上问[捂脸]&lt;/p&gt;
&lt;p&gt;其中，环境上的不同在于，电脑用的Python 3.6，TensorFlow 1.14.0，Keras 2.2.4。打算重新写张存储卡用Python 3.6的整套环境在树莓派上试一下，毕竟这张卡上还有波达方向定位的程序呢（用了Python 3.7的协程新特性）。之所以没有用&lt;code&gt;chineseocr&lt;/code&gt;项目要求的版本是因为树莓派上安装软件真的不容易[捂脸]&lt;/p&gt;
&lt;p&gt;调试代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import sys
sys.path.append(r&apos;~/cn_dect/chineseocr&apos;)
#sys.path.append(r&apos;~/PycharmProjects/chineseocr&apos;)
from config import *
os.environ[&quot;CUDA_VISIBLE_DEVICES&quot;] = &apos;&apos;
scale,maxScale = IMGSIZE[0],2048
from text.keras_yolo3 import yolo_text,box_layer,K
import tensorflow as tf
import numpy as np

graph = tf.get_default_graph()##解决web.py 相关报错问题

anchors = [float(x) for x in keras_anchors.split(&apos;,&apos;)]
anchors = np.array(anchors).reshape(-1, 2)
num_anchors = len(anchors)
num_classes = len(class_names)
K.clear_session()
tf.reset_default_graph()
textModel = yolo_text(num_classes,anchors)
textModel.load_weights(kerasTextModel)
#textModel.load_weights(r&apos;~/PycharmProjects/text.h5&apos;)
sess = K.get_session()
image_shape = K.placeholder(shape=(2, ))##图像原尺寸:h,w
input_shape = K.placeholder(shape=(2, ))##图像resize尺寸:h,w


y1,y2,y3 = [*textModel.output]
out = [y1,y2,y3]

num_layers = len(out)
anchor_mask = [[6,7,8], [3,4,5], [0,1,2]]
boxes = []
scores =[]
input_shape = K.cast(input_shape, tf.float32)
image_shape = K.cast(image_shape, tf.float32)
#from keras.utils import plot_model
#plot_model(textModel, to_file=&apos;model.png&apos;)

def yolo_head(feats, anchors, num_classes, input_shape, calc_loss=False):
    &quot;&quot;&quot;Convert final layer features to bounding box parameters.&quot;&quot;&quot;
    num_anchors = len(anchors)
    # Reshape to batch, height, width, num_anchors, box_params.
    anchors_tensor = K.reshape(K.constant(anchors), [1, 1, 1, num_anchors, 2])
    
    grid_shape = K.shape(feats)[1:3] # height, width
    grid_y =tf.tile(K.reshape(K.arange(0, stop=grid_shape[0]), [-1, 1, 1, 1]),
        [1, grid_shape[1], 1, 1])
    grid_x =tf.tile(K.reshape(K.arange(0, stop=grid_shape[1]), [1, -1, 1, 1]),
        [grid_shape[0], 1, 1, 1])
    grid = K.concatenate([grid_x, grid_y])
    grid = K.cast(grid, K.dtype(feats))
    
    feats = K.reshape(
        feats, [-1, grid_shape[0], grid_shape[1], num_anchors, num_classes + 5])
    
    # Adjust preditions to each spatial grid point and anchor size.
    box_xy = (K.sigmoid(feats[..., :2]) + grid) / K.cast(grid_shape[::-1], K.dtype(feats))
    box_wh = K.exp(feats[..., 2:4]) * anchors_tensor / K.cast(input_shape[::-1], K.dtype(feats))
    box_confidence = K.sigmoid(feats[..., 4:5])
    box_class_probs = K.sigmoid(feats[..., 5:])
    
    if calc_loss == True:
        return grid, feats, box_xy, box_wh
    return box_xy, box_wh, box_confidence, box_class_probs


for lay in range(num_layers):
    box_xy, box_wh, box_confidence, box_class_probs = yolo_head(out[lay],anchors[anchor_mask[lay]], num_classes, input_shape)
    #box_xy = (box_xy - offset) * scale
    #box_wh = box_wh*scale
    
    box_score = box_confidence * box_class_probs
    box_score = K.reshape(box_score, [-1, num_classes])
    
    box_mins  = box_xy - (box_wh / 2.)
    box_maxes = box_xy + (box_wh / 2.)
    box =  K.concatenate([
                    box_mins[..., 0:1],  # xmin
                    box_mins[..., 1:2],  # ymin
                    box_maxes[..., 0:1], # xmax
                    box_maxes[..., 1:2]  # ymax
                ],axis=-1)
    
    box = K.reshape(box, [-1, 4])
    
    boxes.append(box)
    
    scores.append(box_score)

concatenate        = tf.keras.layers.concatenate
boxes  = concatenate(boxes, axis=0)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2.8 更换环境再试一遍&lt;/h3&gt;
&lt;p&gt;由于新的buster系统都是自带Python3.7，于是打算用&lt;code&gt;pipenv&lt;/code&gt;创建Python3.6版本的环境：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;首先，pipenv在系统只有3.7版本的时候并不能直接创建3.6，所以我手动&lt;code&gt;sudo apt install python3.6&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;结果发现并不是那么回事，这时候再次&lt;code&gt;pipenv --python 3.6&lt;/code&gt;会报错，大概是说&lt;code&gt;python3.6&lt;/code&gt;的&lt;code&gt;distutils&lt;/code&gt;找不到模块；&lt;/li&gt;
&lt;li&gt;网上说可能是Linux自带的Python损坏，&lt;code&gt;sudo apt install python3-distutils&lt;/code&gt;就好了，我试了一下，3.7确实是好了，3.6依然不行；&lt;/li&gt;
&lt;li&gt;我打算用系统级Python多版本切换：&lt;pre&gt;&lt;code&gt;sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 1
sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.7 2
配置版本
sudo update-alternatives --config python3
或切换设置
sudo update-alternatives --set python3 /usr/bin/python3.6
&lt;/code&gt;&lt;/pre&gt;
重新设3.6为系统默认Python，再执行&lt;code&gt;apt&lt;/code&gt;或者&lt;code&gt;dpkg&lt;/code&gt;安装说不定就安装到3.6下面去了，结果发现还是太天真了。
试完了想把系统Python切换到Python3.6上，再运行&lt;code&gt;apt install python3-distutils&lt;/code&gt;，结果并不可以。dist这个库安装完成后使用&lt;code&gt;dpkg -L python3-distutils&lt;/code&gt;查看发现它已经在&lt;code&gt;/usr/lib/python3.7/distutils&lt;/code&gt;文件夹下了，没法再装给Python3.6。被逼无奈只好强行把&lt;code&gt;dpkg&lt;/code&gt;列出来的文件拷过去了，反正都是纯Python代码且发布无所谓小版本：&lt;pre&gt;&lt;code&gt;sudo cp -r /usr/lib/python3.7/distutils/* /usr/lib/python3.6/distutils/
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;这下终于好了，运行&lt;code&gt;pipenv --python 3.6&lt;/code&gt;报环境变量的错，按提示重新配置即可：&lt;pre&gt;&lt;code&gt;export LC_ALL=C.UTF-8
export LANG=C.UTF-8
&lt;/code&gt;&lt;/pre&gt;
在运行&lt;code&gt;pipenv&lt;/code&gt;终于成功安装了Python 3.6的环境。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;装TensorFlow和Pytorch这两个大家伙，发现piwheel上没有现成的numpy[捂脸]
wget https://github.com/lhelontra/tensorflow-on-arm/releases/download/v1.14.0-buster/tensorflow-1.14.0-cp37-none-linux_armv7l.whl&lt;/p&gt;
&lt;h3&gt;2.9 在macOS上再试一遍&lt;/h3&gt;
&lt;p&gt;安装Xcode自不必说，然后同意许可，安装Apple Command Line Tools：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo xcodebuild -license
sudo xcode-select --install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来我用macports安装系统依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo port install cmake pkgconfig 
sudo port install jpeg libpng tiff openexr
sudo port install eigen3 tbb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载OpenCV源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 wget -O opencv.zip https://github.com/opencv/opencv/archive/4.1.1.zip
proxychains4 wget -O opencv_contrib.zip https://github.com/opencv/opencv_contrib/archive/4.1.1.zip
unzip opencv.zip
unzip opencv_contrib.zip
mv opencv-4.1.1 opencv
mv opencv_contrib-4.1.1 opencv_contrib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下面关于Python虚拟环境的路径，我都是用&lt;code&gt;conda&lt;/code&gt;做的管理，所以跟&lt;code&gt;venv&lt;/code&gt;、&lt;code&gt;virtualenv&lt;/code&gt;、&lt;code&gt;pipenv&lt;/code&gt;有所不同。准备执行&lt;code&gt;cmake&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd opencv
mkdir build &amp;amp;&amp;amp; cd build

cmake -D CMAKE_BUILD_TYPE=RELEASE \
      -D CMAKE_INSTALL_PREFIX=/usr/local \
      -D OPENCV_EXTRA_MODULES_PATH=~/PycharmProjects/opcv/opencv_contrib/modules \
      -D PYTHON3_LIBRARY=`python -c &apos;import subprocess; import sys; s = subprocess.check_output(&quot;python3-config --prefix&quot;, shell=True).decode(&quot;utf-8&quot;).strip(); m = subprocess.check_output(&quot;python3-config --abiflags&quot;, shell=True).decode(&quot;utf-8&quot;).strip(); (V, v) = sys.version_info[:2]; print(&quot;{}/lib/libpython{}.{}{}.dylib&quot;.format(s, V, v, m))&apos;` \
      -D PYTHON3_INCLUDE_DIR=`python -c &apos;import distutils.sysconfig as s; print(s.get_python_inc())&apos;` \
      -D PYTHON3_EXECUTABLE=`python -c &apos;import subprocess; print(subprocess.check_output(&quot;which python&quot;, shell=True).decode(&quot;utf-8&quot;).strip())&apos;` \
      -D BUILD_opencv_python2=OFF \
      -D BUILD_opencv_python3=ON \
      -D INSTALL_PYTHON_EXAMPLES=ON \
      -D INSTALL_C_EXAMPLES=OFF \
      -D OPENCV_GENERATE_PKGCONFIG=ON \
      -D OPENCV_ENABLE_NONFREE=ON \
      -D BUILD_EXAMPLES=ON \
      -D EIGEN_INCLUDE_PATH=/opt/local/include/eigen3 ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译并安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make -j4
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成安装后注意看安装日志，我们需要的文件位于&lt;code&gt;/usr/local/lib/python3.6/site-packages/cv2/python-3.6/cv2.cpython-36m-darwin.so&lt;/code&gt;，为当前环境创造连接即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -s /usr/local/lib/python3.6/site-packages/cv2/python-3.6/cv2.cpython-36m-darwin.so /Users/&amp;lt;user-name&amp;gt;/anaconda3/envs/py36/lib/python3.6/site-packages/cv2.so
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>轻量级的用于开放或闭合麦阵列中的声源定位与追踪方法</title><link>https://zlotus.github.io/2019/08/01/rbp3-doa-paper/</link><guid isPermaLink="true">https://zlotus.github.io/2019/08/01/rbp3-doa-paper/</guid><description>翻译</description><pubDate>Thu, 01 Aug 2019 23:40:11 GMT</pubDate><content:encoded>&lt;p&gt;翻译论文 &lt;a href=&quot;https://arxiv.org/abs/1812.00115&quot;&gt;Lightweight and Optimized Sound Source Localization and Tracking Methods for Open and Closed Microphone Array Configurations&lt;/a&gt; ，先坑着……&lt;/p&gt;
&lt;p&gt;要死记硬背的拎出来没事就看看。&lt;/p&gt;
&lt;h1&gt;优化的轻量级的用于开放或闭合麦阵列中的声源定位与追踪方法&lt;/h1&gt;
&lt;h2&gt;摘要&lt;/h2&gt;
&lt;p&gt;自然环境中的人机交互需要对环境中的不同的声源进行过滤。这种功能通常需要使用麦阵列，实时的对声源进行定位、追踪，并对不同声源进行分解。同时收集多个麦克风信号并处理分析可以提高对噪音的健壮性，但是麦克风数目的增加也会增加更多的计算量，这限制了响应时间，也限制了机器人的应用场景。声源定位算法会扫描广阔的三维空间以确定声源方位，这需要较多的算力，因此，减少声源定位算法的计算量将能够有效帮助声源定位机器人的场景推广及实施应用。不过，机器人的外形也会限制麦阵列的几何形状。此外，声源定位方法通常会返回一些环境噪音，而这些噪音应该通过声源追踪进行平滑和过滤。本文提出了一种名为SRP-PHAT-HSDA的新的声源定位方法，该方法将结合使用低分辨率和高分辨率的网格以节省扫描空间时对内存的访问。文章将使用一种麦克风指向性模型以减少方位扫描的数目，并忽略掉不重要的麦克风。本文还将引入一种新的麦阵列配置方法，用以自动调整那些平常需要参考麦阵列形状通过经验调试的参数。本文提出了一种对三维卡尔曼方法的改进（简写为M3K），用于生源追踪，该方法可以在三维空间中同时追踪多个声源。本文通过使用包含16个麦克风的阵列和低功耗硬件进行测试，结果显示SRP-PHAT-HSDA和M3K方法仅使用传统算法的1/30到1/4的算力，却能够达到不低于传统声源定位、追踪方法的效果。&lt;/p&gt;
&lt;p&gt;关键词：声源定位，声源追踪，麦阵列，实时计算，嵌入式系统，移动机器人，机器人听觉。&lt;/p&gt;
&lt;h2&gt;1 引言&lt;/h2&gt;
&lt;p&gt;当发言者离（一个或多个）麦克风较远时进行的语音识别称为远距离语音识别（简称DSR），而远距离会使得背景噪音和反射回响与语音叠加[1, 2]，给识别带来困难。不过，DSR可以让发言者在不佩戴手持或头带式麦克风的场景中进行语音识别。尽管，远距离语音识别的健壮性依然是个挑战[3]。麦阵列这种硬件可以在人机交互（简称HRI）中实现DSR。这需要在机器人平台上安装多个麦克风，以处理远距离感知音频，通过过滤机器人平台的风扇、驱动机构，以及环境中不稳定的背景反射声源，在处理速度应当足够快时，实现实时的人机交互交互。这一过程首先需要对感知到的声源进行定位和追踪，然后才能对不同的声源进行分离[4]，以进行下一阶段的操作，比如对分离出来的特定语音进行识别[5, 6]。在实现声源定位及追踪方法时，对环境噪音的健壮性以及对计算量的控制十分关键[7]，因为这仅仅是实现基于语音的人机交互（HRI）的第一步。在自然环境下实现基于语音的人机交互需要让机器人避免环境中的各种噪音干扰，从嘈杂的背景音中分辨出并识别语音指令。&lt;/p&gt;
&lt;p&gt;&amp;lt;center&amp;gt;
&amp;lt;img style=&quot;border-radius: 0.3125em;
box-shadow: 0 2px 4px 0 rgba(34,36,38,.12),0 2px 10px 0 rgba(34,36,38,.08);&quot;
src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2019/odas-paper-figure-1.png&quot;&amp;gt;
&amp;lt;br&amp;gt;
&amp;lt;div style=&quot;color:orange; border-bottom: 1px solid #d9d9d9;
display: inline-block;
color: #999;
padding: 2px;&quot;&amp;gt;图1 声源追踪及定位模块示意图&amp;lt;/div&amp;gt;
&amp;lt;/center&amp;gt;&lt;/p&gt;
&lt;p&gt;如图1所示，声源定位（简称SSL）以及声源追踪（简称SST）是按顺序依次进行计算的。对于每一帧$l$，SSL使用来自包括$M$个麦克风的麦阵列的音频数据$\mathbf{X}^l=\left{\mathbf{X}^l_1, \mathbf{X}^l_2, \ldots, \mathbf{X}^l_M\right}$生成$V$个潜在声源$\mathbf{\Psi}^l=\left{\psi^l_1, \psi^l_2, \ldots, \psi^l_V\right}$，其中的每个潜在声源$\psi^l_V$由一组表示波达方向（DoA）的笛卡尔坐标$\lambda^l_v$和一个表示可控波束能级组成的$\Lambda^l_v$组成。SSL方法给出由零星的声源（比如稀疏的语音）引起的带噪声源的波达方向，并且同一时间可能存在多个竞争声源。接着，SST利用潜在声源给出$I$个过滤掉噪音的被追踪声源$\mathbf{\Phi}^l=\left{\phi^l_1, \phi^l_2, \ldots, \phi^l_I\right}$，以提供声源的平滑轨迹。改进的SSL和SST方法的功能与所使用的麦克风数量直接相关，而麦克风数量会影响到计算量。&lt;/p&gt;
&lt;p&gt;在这一过程中，SSL是最消耗算力的操作，现有不少算法可用于声源定位。Rascon等人[9]提供了一种轻量级的对内存和CPU消耗较低的SSL方法，但是仅支持3个麦克风，而且只能在一个2维平面内进行波达方向计算。Nesta与Omologo[10]提供了一种使用通用状态相干变换实现的声源定位，这一定位方法在多声源场景中尤为有效，但是这一方法中涉及独立成分分析运算（ICA），需要较长时间才能收敛。Drude等人[11]使用基于相位和电平差异的核函数方法，但是这一方法会增加计算量。Loesch和Yang[12]提供了一直基于时域-频域稀疏的定位方法，但这一方法对高混响水平较为敏感。基于标准特征值分解的多信号分类（SEVD-MUSIC）使SSL对加性噪声具有较好的鲁棒性[13]。SEVD-MUSIC最初用于窄带信号[14]，不过目前已经可以适用于宽带声源，例如语音[15]，并且只要噪音没有被定位信号（如语言）强，这一方法就可以对噪声具有较强的健壮性。基于广义特征值分解（GEVD-MUSIC）方法[16]的多信号分类也用于处理该问题，但这一种方法增加了计算量。基于广义奇异值分解（GSVD-MUSIC）的多信号分类降低了GEVD-MUSIC的计算量，同时提高了定位精度[17]，但这一方法仍涉及矩阵特征分解运算。其他方法利用麦阵列的几何特性（线性，圆形或球形）来提高鲁棒性并降低计算量[18,19,20]。尽管这些几何形状产生了有趣的特性，但由于其特定的几何形状也带来了一些物理约束，这些麦阵列配置对于移动机器人的体积限制来说并不太实用。还可以使用具有相位变换的转向响应功率（SRP-PHAT）来实现声源定位。SRP-PHAT通常使用来自成对麦克风的带有相位变换的加权广义互相关（GCC-PHAT）来计算[4,8]。SRP-PHAT比基于MUSIC的方法的计算量更少，但在通过大量麦克风数据扫描3D空间时仍需要大量的计算。随机区域收缩[21]、层次搜索[22,23,24]和矢量化[25]也被研究用SRP-PHAT加速扫描，但通常局限于2维平面和单个资源搜索。Marti[26]等人还提出一种同时在粗网格和细网格上进行递归搜索的方法。这一方法将空间划分为矩形小块，然后将细网格中的多点映射在粗网格的一个点中。不过，这一方法也忽略了麦克风的方向性，并在GCC值上使用了平均窗口，这可能在临近值为负时削减一个峰值的效果。&lt;/p&gt;
&lt;p&gt;我们可以把声源追踪方法分为以下四类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;维特比搜索：Anguera等人[27]提出了一种后处理Viterbi方法来追踪时序中的声源。在实时处理数据时，这一方法将引入显著的延迟，因此仅适用于非实时分析。这一方法实现的追踪仅限于离散状态空间，因此该方法仅适用于固定网格中的声源追踪。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;顺序蒙特卡罗（SMC）滤波：SMC方法，也称为粒子滤波，能够对单个声源执行低延迟追踪[28,29,30]。 Valin等人[4,8]采用SMC方法来跟踪多个声源。这一方法通过用有限粒子对空间进行采样，以模拟出非高斯的状态分布。SMC可以用来追踪连续空间轨迹，不过这一方法需要大量计算，而且因为使用了随机生成的粒子而不够确定。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;卡尔曼滤波：Rascon等人[9]提出了一种基于卡尔曼滤波器的轻量级方法，该方法能够追踪连续轨迹，同时显著减少计算量。然而，该方法仅限于球面坐标中的波达方向，输出高度和方位角，不过，当方位角分辨率随高度变化时，会产生畸变，即引入了方位角隐藏现象。Marković等人[31]提出了一个在李群上的扩展卡尔曼滤波器（LG-EKF），用8麦克风阵列进行方向追踪。LG-EKF解决了方位角隐藏现象，但仅限在2维圆环上进行追踪，因此并不适合追踪3维球面上的声源。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;联合概率数据关联过滤（JPDA）：Marković等[32]提出了一种可用于3维球面声源追踪的，基于von Mises-Fisher分布上的贝叶斯估计。然而，该方法需要事先知道声源数量，并且忽略各跟踪源的运动模型，这导致当两个声源交叉时会被当做轨迹的切换或融合。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为了改进声源定位和声源追踪，本文提出了一种称为SRP-PHAT-HSDA的SRP-PHAT方法，即按层级搜索指向性模型并自动校准，以及一种使用笛卡尔坐标的改进的3D卡尔曼滤波（M3K）的追踪方法。SRP-PHAT-HSDA首先在粗分辨率网格上扫描3D空间，然后优化特定区域的搜索。该方法利用到达时间差（TDOA）的不确定性模型，通过开放和闭合麦阵列配置，使用多种分辨率水平的网格来优化扫描精度。此方法还使用了麦克风指向性模型，以减少扫描方向的数量，并忽略不重要的麦克风对。M3K用卡尔曼滤波器取代了Valin等人[8]使用的SMC滤波器，并引入了三个新概念：1) 归一化状态以将空间限制为单位球体；2) 推导出计算相干声源的解析解表达式，以加速计算；3) 用于同时跟踪多个声源的卡尔曼均值向量和协方差矩阵的加权更新。这些改进形成了高效的对多个声源的跟踪方法，使之能够嵌入低算力硬件。改进的方法不仅比SMC更节省算力，还解决了卡尔曼滤波在球面座标中的畸变和隐藏。&lt;/p&gt;
&lt;p&gt;文章的结构如下：首先，第2节描述了SRP-PHAT与SEVD-MUSIC的计算要求对比，以证明和确定SRP-PHAT-HSDA带来的改进。然后，第3节和第4节分别对SRP-PHAT-HSDA方法和M3K方法进行描述。第5节介绍了在移动机器人上实现8和16圆形麦阵列以及闭合立方麦阵列的实验设置，在3代树莓派上实现了SSL和SST方法。第6节给出了将SRP-PHAT与SRP-PHAT-HSDA方法，以及M3K与SMC方法的对比的实验结果。最后，第7节对文章进行总结，并说明未来工作。&lt;/p&gt;
&lt;h2&gt;2 SRP-PHAT与SEVD-MUSIC的算力需求&lt;/h2&gt;
&lt;p&gt;声源定位通常被分为两个子任务：1) 达到时间差估计；2) 在麦阵列周围的3维空间中搜索波达方向。而SRP-PHAT与SEVD-MUSIC方法的主要区别就在于子任务1：SRP-PHAT基于带有相位变换的加权广义互相关（GCC-PHAT）方法，而SEVD-MUSIC基于奇异特征值分解。&lt;/p&gt;
</content:encoded></item><item><title>分布式波达方向定位尝试</title><link>https://zlotus.github.io/2019/07/09/rbp3-odas-server/</link><guid isPermaLink="true">https://zlotus.github.io/2019/07/09/rbp3-odas-server/</guid><description>在树莓派上做自然场景中的中文识别</description><pubDate>Tue, 09 Jul 2019 21:09:11 GMT</pubDate><content:encoded>&lt;p&gt;想把&lt;code&gt;odas&lt;/code&gt;这个项目弄成分布式的，用一个主机同时监控多个树莓派进行波达方向定位。源码备份在&lt;a href=&quot;https://github.com/zlotus/doa-project&quot;&gt;doa-project&lt;/a&gt;，防丢用。&lt;/p&gt;
&lt;h1&gt;树莓派&lt;/h1&gt;
&lt;p&gt;新的 &lt;strong&gt;raspbian buster&lt;/strong&gt; 自带了python 3.7，安装&lt;code&gt;pipenv&lt;/code&gt;即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 pip install pipenv
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;环境准备&lt;/h2&gt;
&lt;h3&gt;初始化虚拟环境&lt;/h3&gt;
&lt;p&gt;为odas项目新建文件夹&lt;code&gt;odas&lt;/code&gt;，初始化&lt;code&gt;pipenv&lt;/code&gt;环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir odas
cd odas
proxychains4 pipenv --python 3.7
# 或 pipenv --python /usr/bin/python3
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装声卡驱动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;mkdir 3rd-lib
cd 3rd-lib

# 安装声卡驱动
git clone https://github.com/respeaker/seeed-voicecard.git
cd seeed-voicecard
sudo proxychains4 ./install.sh
sudo reboot -h now

# 测试声卡
arecord -L
aplay -L
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编译&lt;code&gt;odas&lt;/code&gt;项目&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 编译odas
cd 3rd-lib/
proxychains4 git clone https://github.com/introlab/odas.git
mkdir odas/build
cd odas/build
cmake ..
make

# 做软链接方便使用
ln -s ~/odas/3rd-lib/odas/bin/odaslive ~/.local/bin/odaslive

# 测试odas
odaslive -c ~/respeaker-6mic-odas.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装&lt;code&gt;pyzmq&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;新的buster自带了&lt;code&gt;libzmq 4.3.1&lt;/code&gt;，安装&lt;code&gt;pyzmq&lt;/code&gt;即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 pip install pyzmq
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; import zmq
&amp;gt;&amp;gt;&amp;gt; zmq.zmq_version()
&apos;4.3.1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;配置防火墙&lt;/h3&gt;
&lt;p&gt;检查端口是否开启可以使用&lt;code&gt;nc -zvw3 192.168.1.108 5557&lt;/code&gt;（&lt;code&gt;z&lt;/code&gt;用于扫描端口的零I/O模式，&lt;code&gt;v&lt;/code&gt;显示详情，&lt;code&gt;w3&lt;/code&gt;三秒后超时）。&lt;/p&gt;
&lt;p&gt;为zmq打开防火墙端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow proto tcp to 0.0.0.0/0 port 5556 comment &quot;zmq publish port of odas for sound source tracking&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 5557 comment &quot;zmq publish port of odas for sound source localization&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：我一开始为了用上Python 3.7，早早更新了buster的测试版，现在buster正式发布了，更新时报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;E: Repository &apos;http://mirrors.aliyun.com/raspbian/raspbian buster InRelease&apos; changed its &apos;Suite&apos; value from &apos;testing&apos; to &apos;stable&apos;
N: This must be accepted explicitly before updates for this repository can be applied. See apt-secure(8) manpage for details.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照&lt;a href=&quot;https://askubuntu.com/questions/989906/explicitly-accept-change-for-ppa-label&quot;&gt;Explicitly accept change for PPA &apos;Label&apos;&lt;/a&gt;方法中说的，把&lt;code&gt;apt-get&lt;/code&gt;改成&lt;code&gt;apt&lt;/code&gt;即可。&lt;/p&gt;
&lt;h2&gt;使用&lt;code&gt;pyzmq&lt;/code&gt;发布定位数据&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing import Process
import os
import socket
import zmq


def sst_write(rpi_host=&apos;0.0.0.0&apos;, rpi_port=9000, zmq_host=&apos;*&apos;, zmq_port=5556):
    &quot;&quot;&quot;
    A process, listened on socket://rpi_host:port,
        receive odas Sound Source Tracking(SST) json format data in bytes.
    Reassemble json byte frames and publish using zmq on tcp://zmq_host:zmq_port.
    :return: None
    &quot;&quot;&quot;
    write_pid = os.getpid()
    print(&apos;#Write Process# %s start&apos; % write_pid)
    bind_ip, bind_port = rpi_host, rpi_port
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((bind_ip, bind_port))
    server.listen(5)  # max backlog of connections
    print(&apos;#Write Process# listening on {}:{}&apos;.format(bind_ip, bind_port))
    client_socket, address = server.accept()
    print(&apos;#Write Process# accepted connection from {}:{}&apos;.format(address[0], address[1]))

    context = zmq.Context()
    zmq_socket = context.socket(zmq.PUB)
    zmq_socket.bind(f&quot;tcp://{zmq_host}:{zmq_port}&quot;)

    temp = b&apos;&apos;
    while True:
        t = 0
        temp = client_socket.recv(1024)
        for i in range(5):
            t = temp.find(b&apos;}&apos;, t) + 1
        zmq_socket.send(temp[:t])
        temp = temp[t:]


def ssl_write(rpi_host=&apos;0.0.0.0&apos;, rpi_port=9001, zmq_host=&apos;*&apos;, zmq_port=5557):
    &quot;&quot;&quot;
    A process, listened on socket://rpi_host:port,
        receive odas Sound Source Localization(SSL) json format data in bytes.
    Reassemble json byte frames and publish using zmq on tcp://zmq_host:zmq_port.
    :return: None
    &quot;&quot;&quot;
    write_pid = os.getpid()
    print(&apos;#Write Process# %s start&apos; % write_pid)
    bind_ip, bind_port = rpi_host, rpi_port
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind((bind_ip, bind_port))
    server.listen(5)  # max backlog of connections
    print(&apos;#Write Process# listening on {}:{}&apos;.format(bind_ip, bind_port))
    client_socket, address = server.accept()
    print(&apos;#Write Process# accepted connection from {}:{}&apos;.format(address[0], address[1]))

    context = zmq.Context()
    zmq_socket = context.socket(zmq.PUB)
    zmq_socket.bind(f&quot;tcp://{zmq_host}:{zmq_port}&quot;)

    temp = b&apos;&apos;
    while True:
        t = 0
        temp = client_socket.recv(1024)
        for i in range(5):
            t = temp.find(b&apos;}&apos;, t) + 1
        zmq_socket.send(temp[:t])
        temp = temp[t:]


if __name__ == &apos;__main__&apos;:
    try:
        p_ssl_w = Process(target=ssl_write)
        p_sst_w = Process(target=sst_write)
        # p_ssl_r = Process(target=ssl_read, kwargs={&quot;verbose&quot;: True})
        # 启动子进程pw，写入:
        p_ssl_w.start()
        p_sst_w.start()
        # 等待pw结束:
        p_ssl_w.join()
        p_sst_w.join()
    except KeyboardInterrupt:
        p_ssl_w.terminate()
        p_sst_w.terminate()
        print(&apos;KeyboardInterrupt pw pr Terminated&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;远端主机&lt;/h1&gt;
&lt;h2&gt;使用&lt;code&gt;pyzmq&lt;/code&gt;订阅定位数据&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;from multiprocessing import Process
import os
import json
import asyncio
from aiohttp import web, WSMsgType
import zmq


def sst_read(zmq_host=&apos;192.168.1.108&apos;, zmq_port=5556, aio_host=&apos;0.0.0.0&apos;, aio_port=8080, verbose=False):
    &quot;&quot;&quot;
    A process, used for subscribing zmq socket on tcp://zmq_host:zmq_port,
        receiving Sound Source Tracking(SST) data frames.
    In the meantime the process runs an aiohttp server on ws://aio_host:aio_port,
        sending SST data frames to user browser via websocket connection.
    User browser websocket-client should send a &apos;received&apos; message after every data frame received,
        to tell the server &apos;last message received, please send next.&apos;
    :output: Sound Source Tracking output like this:
    {
        &quot;timeStamp&quot;: 45602,
        &quot;src&quot;: [
            { &quot;id&quot;: 43, &quot;tag&quot;: &quot;dynamic&quot;, &quot;x&quot;: 0.025, &quot;y&quot;: 0.128, &quot;z&quot;: 0.991, &quot;activity&quot;: 1.000 },
            { &quot;id&quot;: 0, &quot;tag&quot;: &quot;&quot;, &quot;x&quot;: 0.000, &quot;y&quot;: 0.000, &quot;z&quot;: 0.000, &quot;activity&quot;: 0.000 },
            { &quot;id&quot;: 0, &quot;tag&quot;: &quot;&quot;, &quot;x&quot;: 0.000, &quot;y&quot;: 0.000, &quot;z&quot;: 0.000, &quot;activity&quot;: 0.000 },
            { &quot;id&quot;: 0, &quot;tag&quot;: &quot;&quot;, &quot;x&quot;: 0.000, &quot;y&quot;: 0.000, &quot;z&quot;: 0.000, &quot;activity&quot;: 0.000 }
        ]
    }
    :return: None
    &quot;&quot;&quot;
    read_pid = os.getpid()
    print(&apos;#Read  Process# %s start&apos; % read_pid)
    routes = web.RouteTableDef()

    context = zmq.Context()
    zmq_socket = context.socket(zmq.SUB)

    print(&quot;Collecting updates from odas publisher...&quot;)
    zmq_socket.connect(f&quot;tcp://{zmq_host}:{zmq_port}&quot;)
    zmq_socket.setsockopt_string(zmq.SUBSCRIBE, &apos;{&apos;)

    @routes.get(&apos;/ws&apos;)
    async def websocket_handler(request):
        print(&apos;websocket connection open&apos;)

        ws = web.WebSocketResponse()
        await ws.prepare(request)
        c = 0
        await ws.send_str(zmq_socket.recv_string())
        async for msg in ws:
            if msg.type == WSMsgType.TEXT:
                if msg.data == &apos;close&apos;:
                    print(&apos;websocket connection close&apos;)
                    await ws.close()
                else:
                    c += 1
                    tmp = zmq_socket.recv_string()
                    await ws.send_str(tmp)
                    if verbose:
                        c += 1
                        print(f&apos;\r{c}&apos;, end=&apos;&apos;, flush=True)
                    # TODO: debug jump, delete later
                    # for i in range(100):
                    #     zmq_socket.recv_string()
            elif msg.type == WSMsgType.ERROR:
                print(&apos;ws connection closed with exception %s&apos; %
                      ws.exception())
        return ws

    app = web.Application()
    app.add_routes(routes)
    web.run_app(app, host=aio_host, port=aio_port)


def ssl_read(zmq_host=&apos;192.168.1.108&apos;, zmq_port=5557, aio_host=&apos;0.0.0.0&apos;, aio_port=8081, verbose=False):
    &quot;&quot;&quot;
    A process, used for subscribing zmq socket on tcp://zmq_host:zmq_port,
        receiving Sound Source Localization(SSL) data frames.
    In the meantime the process runs an aiohttp server on ws://aio_host:aio_port/ws,
        sending SSL data frames to user browser via websocket connection.
    User browser websocket-client should send a &apos;received&apos; message after every data frame received,
        to tell the server &apos;last message received, please send next.&apos;
    :output: Sound Source Localization output like this:
    {
        &quot;timeStamp&quot;: 45608,
        &quot;src&quot;: [
            { &quot;x&quot;: 0.132, &quot;y&quot;: 0.181, &quot;z&quot;: 0.975, &quot;E&quot;: 0.557 },
            { &quot;x&quot;: 0.198, &quot;y&quot;: 0.342, &quot;z&quot;: 0.918, &quot;E&quot;: 0.130 },
            { &quot;x&quot;: 0.000, &quot;y&quot;: 0.273, &quot;z&quot;: 0.962, &quot;E&quot;: 0.018 },
            { &quot;x&quot;: 0.000, &quot;y&quot;: 0.339, &quot;z&quot;: 0.941, &quot;E&quot;: 0.006 }
        ]
    }
    :return: None
    &quot;&quot;&quot;
    read_pid = os.getpid()
    print(&apos;#Read  Process# %s start&apos; % read_pid)
    routes = web.RouteTableDef()

    context = zmq.Context()
    zmq_socket = context.socket(zmq.SUB)

    print(&quot;Collecting updates from odas publisher...&quot;)
    zmq_socket.connect(f&quot;tcp://{zmq_host}:{zmq_port}&quot;)
    zmq_socket.setsockopt_string(zmq.SUBSCRIBE, &apos;{&apos;)

    @routes.get(&apos;/ws&apos;)
    async def websocket_handler(request):
        print(&apos;websocket connection open&apos;)

        ws = web.WebSocketResponse()
        await ws.prepare(request)
        c = 0
        await ws.send_str(zmq_socket.recv_string())
        async for msg in ws:
            if msg.type == WSMsgType.TEXT:
                if msg.data == &apos;close&apos;:
                    print(&apos;websocket connection close&apos;)
                    await ws.close()
                else:
                    tmp = zmq_socket.recv_string()
                    await ws.send_str(tmp)
                    if verbose:
                        c += 1
                        print(f&apos;\r{c}&apos;, end=&apos;&apos;, flush=True)
                    # TODO: debug jump, delete later
                    # for i in range(100):
                    #     zmq_socket.recv_string()
            elif msg.type == WSMsgType.ERROR:
                print(&apos;ws connection closed with exception %s&apos; %
                      ws.exception())
        return ws

    app = web.Application()
    app.add_routes(routes)
    web.run_app(app, host=aio_host, port=aio_port)


if __name__ == &apos;__main__&apos;:
    try:
        p_ssl_r = Process(target=ssl_read, kwargs={&quot;verbose&quot;: True})
        # p_ssl_r = Process(target=ssl_read)
        p_sst_r = Process(target=sst_read)
        # 启动子进程pr，读取:
        p_ssl_r.start()
        p_sst_r.start()
    except KeyboardInterrupt:
        p_ssl_r.terminate()
        p_sst_r.terminate()
        print(&apos;KeyboardInterrupt pw pr Terminated&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 这里我使用了&lt;code&gt;aiohttp&lt;/code&gt;将数据发布为websocket，在浏览器上进行可视化。&lt;/p&gt;
&lt;h3&gt;使用&lt;code&gt;three.js&lt;/code&gt;可视化数据&lt;/h3&gt;
&lt;h4&gt;封装ant-design组件&lt;/h4&gt;
&lt;p&gt;为了以后用着方便，我特意将可视化代码封装为ant-design的组件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 新建文件夹
mkdir odas-front
# 安装antd测试
npm install antd --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 在电脑上用&lt;code&gt;nvm&lt;/code&gt;控制node版本，最新的已经是版本12.x了。但是&lt;code&gt;port&lt;/code&gt;安装的&lt;code&gt;yarn&lt;/code&gt;使用的是&lt;code&gt;node --version 10.16.0&lt;/code&gt;，因此还是要配合安装&lt;code&gt;nvm install 10.16.0&lt;/code&gt;，使默认&lt;code&gt;node&lt;/code&gt;变成&lt;code&gt;10.16.0&lt;/code&gt;（12版本会报错）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;安装`create-umi`
npm create umi
选择`app`，并同意使用`typescript`，再选择`dva`和`antd`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码基本照抄&lt;code&gt;odas&lt;/code&gt;的Element版本。&lt;/p&gt;
</content:encoded></item><item><title>使用Python代码进行树莓派上的麦阵列声源定位</title><link>https://zlotus.github.io/2019/06/15/rbp3-6mic-py-doa/</link><guid isPermaLink="true">https://zlotus.github.io/2019/06/15/rbp3-6mic-py-doa/</guid><description>使用Python代码，在树莓派上通过ReSpeaker圆形6麦阵列做DOA(Direction Of Arrival)</description><pubDate>Sat, 15 Jun 2019 20:13:11 GMT</pubDate><content:encoded>&lt;p&gt;偶然发现seeedstudio更新了他们的&lt;a href=&quot;http://wiki.seeedstudio.com/ReSpeaker_6-Mic_Circular_Array_kit_for_Raspberry_Pi/#librespeaker-aduio-process&quot;&gt;英文版说明书&lt;/a&gt;，然而中文版还没更新[捂脸]。关于DOA的部分，除了原来的使用&lt;a href=&quot;https://github.com/introlab/odas&quot;&gt;ODAS Studio&lt;/a&gt;的方法以外，又多加了一些使用Python代码直接进行DOA的章节。&lt;/p&gt;
&lt;h1&gt;0 安装驱动&lt;/h1&gt;
&lt;p&gt;别忘了用&lt;code&gt;source activate doa&lt;/code&gt;切到专门的环境里，或者&lt;code&gt;conda create -n doa python=3.6&lt;/code&gt;现造一个环境。&lt;/p&gt;
&lt;p&gt;安装驱动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 git clone https://github.com/respeaker/seeed-voicecard.git
cd seeed-voicecard
sudo proxychains4 ./install.sh
sudo reboot -h now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查驱动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;arecord -L # 输入驱动，即录制
null
    Discard all samples (playback) or generate zero samples (capture)
default
ac108
dmixer
ac101
sysdefault:CARD=seeed8micvoicec
    seeed-8mic-voicecard,
    Default Audio Device
dmix:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct sample mixing device
dsnoop:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct sample snooping device
hw:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct hardware device without any conversions
plughw:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Hardware device with all software conversions

aplay -L # 输出驱动，即播放
null
    Discard all samples (playback) or generate zero samples (capture)
default
ac108
dmixer
ac101
sysdefault:CARD=ALSA
    bcm2835 ALSA, bcm2835 ALSA
    Default Audio Device
dmix:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct sample mixing device
dmix:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct sample mixing device
dsnoop:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct sample snooping device
dsnoop:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct sample snooping device
hw:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct hardware device without any conversions
hw:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct hardware device without any conversions
plughw:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Hardware device with all software conversions
plughw:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Hardware device with all software conversions
sysdefault:CARD=seeed8micvoicec
    seeed-8mic-voicecard,
    Default Audio Device
dmix:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct sample mixing device
dsnoop:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct sample snooping device
hw:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct hardware device without any conversions
plughw:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Hardware device with all software conversions
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;1 测试LED&lt;/h1&gt;
&lt;p&gt;说实话，自打买了这个麦阵列，我还从来没试过这12个LED灯[捂脸]，试试吧要不感觉对不起这个板子，另外，运行demo需要&lt;code&gt;gpiozero&lt;/code&gt;、&lt;code&gt;spidev&lt;/code&gt;、&lt;code&gt;pyusb&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 git clone --depth 1 https://github.com/respeaker/pixel_ring.git
cd pixel_ring
proxychains4 pip install -U -e .
proxychains4 pip install gpiozero RPi.GPIO
python examples/respeaker_4mic_array.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;控制LED的主要代码其实就在函数&lt;code&gt;show()&lt;/code&gt;，直接在Python交互环境中输入下面的代码，就可以打开12点方向的灯，并渲染为白色，也就是ARGB(0, 64, 64, 64)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from pixel_ring import pixel_ring
from gpiozero import LED

power = LED(5)
power.on()

pixel_ring.set_brightness(10) # 0-100，其实会被转换为`APA102.dev.global_brightness`的0-31
pixel_ring.pattern.show([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 64, 64])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用于控制的数组是一个48位的&lt;code&gt;list&lt;/code&gt;，从1点钟方向开始，到12点钟方向结束，每四位控制一个LED灯，非常好操作。&lt;/p&gt;
&lt;h1&gt;2 装DOA相关库&lt;/h1&gt;
&lt;p&gt;添加seeed提供的&lt;code&gt;apt&lt;/code&gt;源。（靠谱的公司啊）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;deb https://seeed-studio.github.io/pi_repo/ stretch main&quot; | sudo tee /etc/apt/sources.list.d/seeed.list
proxychains4 curl https://seeed-studio.github.io/pi_repo/public.key | sudo apt-key add -
proxychains4 sudo apt update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装&lt;code&gt;numpy&lt;/code&gt;和&lt;code&gt;pyaudio&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 conda install -y numpy
proxychains4 sudo apt-get install -y portaudio19-dev 
proxychains4 pip install pyaudio
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装编译工具：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo proxychains4 apt-get install -y swig python-dev libatlas-base-dev build-essential make
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装&lt;code&gt;snowboy&lt;/code&gt;（热词检测DOA）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 git clone --depth 1 https://github.com/Kitt-AI/snowboy.git
cd snowboy
python setup.py build
python setup.py bdist_wheel
pip install dist/snowboy*.whl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装seeed的&lt;code&gt;voice-engin&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 git clone https://github.com/voice-engine/voice-engine.git
cd voice-engine
python setup.py bdist_wheel
pip install dist/*.whl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试ok~：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python ~/doa/voice-engine/examples/respeaker_6mic_array_for_pi/kws_doa.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果只是想用DOA功能，可以试试seeed的老项目&lt;a href=&quot;https://github.com/respeaker/mic_array&quot;&gt;mic_array&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 git clone https://github.com/respeaker/mic_array.git
python ./mic_array/mic_array.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对比了一下，跟人家&lt;a href=&quot;https://github.com/introlab/odas.git&quot;&gt;introlab/odas&lt;/a&gt;项目差的还是比较远的。直观上看就是我们简单的靠&lt;code&gt;fft&lt;/code&gt;实现的DOA仅仅能做到平面范围内的方位判断，仅输出一个角度（平面offset），速度慢且不支持声源跟踪等功能。而odas的声源定位项目输出两个值，一个平面offset一个法向offset，还能进行声源分辨，多声源跟踪跟踪。odas项目的理论收录在&lt;a href=&quot;https://arxiv.org/pdf/1812.00115.pdf&quot;&gt;Lightweight and Optimized Sound Source Localization and Tracking Methods for Opened and Closed Microphone Array Configurations&lt;/a&gt;中，值得一读。&lt;/p&gt;
</content:encoded></item><item><title>我是不是得写个2018年终总结啊</title><link>https://zlotus.github.io/2019/01/19/init-2019/</link><guid isPermaLink="true">https://zlotus.github.io/2019/01/19/init-2019/</guid><description>又是新年第一篇……</description><pubDate>Sat, 19 Jan 2019 09:36:25 GMT</pubDate><content:encoded>&lt;p&gt;嗯，嘛，理论上讲，2019年，所有80后都将踏入中年人的行列……听说这个世界是中年人撑起来的？完全没感觉到啊……&lt;/p&gt;
&lt;p&gt;2018基本虚度，好在算是正式入宅……看了不少好番，比如硬核的《Megalo Box》、超燃的《我的英雄学院》、过于真实的《白箱》、炫富嫌疑的《进击的巨人》等等。当然，也按年度例行复习了宫崎骏、押井守、井上雄彦、荒川弘等神仙的经典。看了gakki的某新剧，同时通过Unnatural也粉了十元……&lt;/p&gt;
&lt;p&gt;今年依旧在砖总、李哥、白总、陈哥的血虐下，体重突破62kg。前几天又找到了拉背的快感，下午再去蹲个腿，感觉离施瓦辛格就差65536个郭达斯坦森了[认真脸]。&lt;/p&gt;
&lt;p&gt;通过不懈努力，我应该算正式步入肥宅的行列了吧[捂脸]。&lt;/p&gt;
&lt;p&gt;今年倒是在各种番剧的夹击中，忙里偷闲的糊了篇毕业论文，给自己及导师一个胶带。看上单位一姑娘还贼怂不敢追，打算在2019死皮赖脸的给自己及办公室的阿姨领导一个胶带……&lt;/p&gt;
&lt;p&gt;然后说点严肃又装逼的事吧——生老病死。小时候跟表哥堂妹参见姑妈的追悼会，后来是老爸的，再接着是爷爷的，上个礼拜是姥姥的，感觉跟西安殡仪馆都熟起来了……&lt;/p&gt;
&lt;p&gt;我对殡仪馆是没什么感觉的，比起殡仪馆我更讨厌医院。虽然今年已经要三十而立了，但是我依然中二的坚信着不论信不信春哥，永生都会随着技术的发展而降临，因此在生老病死中我非常非常的抗拒老和病。今年越来越觉得宅是硬道理，除了贫穷这一主要因素外，宅还能降低各种不可预知事件发生的概率，我估计是方圆几十里内最怕死的了，愉悦[捂脸]。&lt;/p&gt;
&lt;p&gt;同样，这两年撸铁把体脂提上来了几个百分点，感觉对感冒的抵抗力明显强了很多，当然也可能是空气净化器的功劳……在我眼里，世界上最可怕的莫过于死亡，比死亡更恐怖的莫过于病逝……当年姑妈去世的时候，我虽然年纪小但是也有些触动，后来在中二动画、游戏、小说的熏陶下，渐渐萌生了“人生得意须尽欢”的人生战略方针，工作后中二病发作誓要踏遍祖国的壮美山河，毕竟引无数英雄竞折腰嘛，我不也算是个小英雄嘛。我知这中二如露水般短暂，然而，然而，最后一次空对月大概就是去喀纳斯了——逼自己交定金、早早订机票、给自己打气脱宅——美酒美食拉肚子的飞回西安，发着烧第二天继续飞上海出差，我突然明白了，我并不是小英雄，我连小垃圾都算不上，我们现在能在说上号的人物，不论历史还是当今、不论正义还是邪恶、不论伟大还是卑微、不论现实还是虚构，哪一个有名号的人物不拥有着开外挂的人生？！&lt;/p&gt;
&lt;p&gt;也许是王立群老师的大型情感类历史剧看多了，更可能是科幻或者加缪读傻了，我的人生啊，真的是毫无意义，连时间长河中的水分子都算不上，充其量是水面的一瞬波光，幻灭嘛。嘴上说着脚踏碧蓝大海头顶金色太阳，实际上怂的跟什么似的，前进？大概是把，连被逼无奈都算不上，仅仅是无所事事罢了。什么追求啊逃避啊，说的好像一副志存高远成竹在胸似的，其实统统滴莫有啊，说好听点随心所欲，说难听点随波逐流。不过，这样不也挺好吗，我啊，也不是年轻人说的什么阶级固化看不到希望，更不是多么厌恶鼓吹奋斗的人，希望嘛努力努力也能看到，对奋斗者也报以带有嫉妒的祝福，这应该不是丧吧，大概是隐约感到自己的幸运吧。虽然不算顺风顺水，却也没遭到什么大灾大难，在不到三十岁明白了身体是等待技术革命的本钱，明白了活得久总会碰到更多幸运的事，开始相信概率论与数理统计，开始相信时间，这本身就算是幸运吧。王诺诺大神也说过，成长嘛，无非就是用点与生俱来的，换点朝思暮想的，那用时间换更多的时间也不算犯规吧~哈哈哈~&lt;/p&gt;
&lt;p&gt;时间嘛，时间诶，确实，这几年硬笔字明显是好看多了，胸背臀肩也盖上了五花肉，等体重涨到65kg就开始冻结身体版本，缩短撸铁时间。早就想买个电钢琴，趁着还有闲就实现了吧。&lt;/p&gt;
&lt;p&gt;另外，今年大概是旺桃花？这个月没过一半已经接了三个介绍，大概是同学同事都看不下去我三点一线的的单身生活吧……作为一个极度功利的人，咱一看照片二聊兴趣，每个对象都是几小时结束战斗。咱可是知道女生特别看重身高，看完照片如实强调自己身高170就能够很轻易的结束会话。我，真是凭本事单身的。聊了这几个我也发现自己越发鄙视“仪式感”这个词了，在法庭或者殡仪馆看重仪式感无可厚非，但是生活中仪式XXX啊，日子过的太闲了吧，有考虑仪式和优雅的空，多给自己充充电保保鲜不好吗！另一个同样属性的词大概是“有趣”了。撸了一年半载的铁我算是明白了，有趣的灵魂满街都是，完美的肉体万中无一。有几个人称得上无趣？稍微受点教育会用谷歌看过点美女路过点美景，最后嘴皮子在利索点，个个不都是字字珠玑的少年英雄小哪吒嘛。但是完美的肉体呢？首先你得生的足够幸运吧，其次得长的足够幸运吧，然后你得足够幸运的有闲有钱而刚好又足够幸运的有毅力去撸铁吧，这些小概率是按乘算的。发现了吧，为什么有趣的灵魂多啊？因为“有趣的灵魂”不是一个well defined的词语啊，但是“完美的肉体”可就完全不一样了，骨相皮相站姿身段、三庭五眼头身比例、有闲有钱时刻保鲜，更别提什么体质啊肌纤维啊分离度啊这些精细特征了，哪一个不能量化衡量？就像写跑代码一样，前后层级环环相扣，编译链接缺一不可……扯远了跑语言学范畴了……其实就是想表达一下，现在越来越欣赏那种刚毅不屈心沉似铁的人了。&lt;/p&gt;
&lt;p&gt;要说谁不慌，那是不存在的，慌啊，都慌，个个心里没底，毕竟都第一次做人没经验嘛，当然，有人故作镇定的不慌，有人嘻皮笑脸的慌。慌或者不慌，还不一样是顶着太阳踏浪扬帆嘛~反正也没有别的事做……&lt;/p&gt;
&lt;p&gt;最后，感谢中国人民的好朋友莱蒙托夫先生的原创，以及德艺双馨的民间艺术家柴静女士的翻译：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一只船孤独地航行在海上，
它既不寻求幸福，
也不逃避幸福，
它只是向前航行，
底下是沉静碧蓝的大海，
而头顶是金色的太阳。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;许个愿吧，下一个十年希望能够达成：键盘乐器独奏成就……以及……兼职声优成就，万一咱这几十年也侥幸汇成河了呢。&lt;/p&gt;
&lt;p&gt;完。&lt;/p&gt;
</content:encoded></item><item><title>树莓派开发环境准备汇总</title><link>https://zlotus.github.io/2018/12/01/rbp3-dev-env/</link><guid isPermaLink="true">https://zlotus.github.io/2018/12/01/rbp3-dev-env/</guid><description>在树莓派上准备科学环境</description><pubDate>Sat, 01 Dec 2018 12:09:10 GMT</pubDate><content:encoded>&lt;p&gt;由于经常要折腾Raspbian，所以打算在这里汇总一下树莓派环境初始化要做的工作。&lt;/p&gt;
&lt;h1&gt;1 系统安装及配置&lt;/h1&gt;
&lt;h2&gt;1.1 Headless下的WiFi及ssh配置&lt;/h2&gt;
&lt;p&gt;按树莓派官方说明在SD卡上写入镜像，然后进行Headless配置：&lt;/p&gt;
&lt;p&gt;配置WIFI，需要在SD卡根目录下新建一个名为&lt;code&gt;wpa_supplicant.conf&lt;/code&gt;的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /Volumes/boot/wpa_supplicant.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写入WIFI配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
    ssid=&quot;&amp;lt;YOUR-NETWORK-NAME&amp;gt;&quot;
    psk=&quot;&amp;lt;YOUR-NETWORK-PASSWORD&amp;gt;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启SSH，只需在SD卡根目录下新建一个名为ssh的空文件即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /Volumes/boot/ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;或者直接将本地编辑好的&lt;code&gt;wpa_supplicant.conf&lt;/code&gt;、&lt;code&gt;ssh&lt;/code&gt;文件拷贝到SD卡上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 本地存储卡初始化
cp ~/Raspberrypi/wpa_supplicant.conf /Volumes/boot
touch /Volumes/boot/ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;配置完成后正常启动树莓派即可自动连接WIFI，SSH可以正常登录。
注：如果不知道设备IP，可以使用&lt;code&gt;ping raspberry.local&lt;/code&gt;查找。&lt;/p&gt;
&lt;p&gt;如果这个IP上曾经还有过其他树莓派前辈，那么要先删除本机上的ssh信任主机，比如我的树莓派一直用固定IP配在108上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -R 192.168.1.108

# 树莓派插卡启动，上传本地秘钥方便ssh登录
ssh-copy-id pi@192.168.1.108
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次用&lt;code&gt;pi&lt;/code&gt;登录后默认密码为&lt;code&gt;raspberry&lt;/code&gt;，应该直接使用&lt;code&gt;passwd&lt;/code&gt;修改密码。或是使用&lt;code&gt;sudo raspi-config&lt;/code&gt;，通过树莓派管理工具修改密码、主机名、地区、时区等信息。&lt;/p&gt;
&lt;h1&gt;1.2 换国内镜像源&lt;/h1&gt;
&lt;p&gt;解决升级慢或&lt;code&gt;Cannot initiate the connection to mirrors.opencas.cn&lt;/code&gt;问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vi /etc/apt/sources.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;sources.list&lt;/code&gt;，注释第一行，在最后添加国内镜像站：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deb http://mirrors.aliyun.com/raspbian/raspbian/ stretch main non-free contrib
deb-src http://mirrors.aliyun.com/raspbian/raspbian/ stretch main non-free contrib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用其他镜像站，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 中科大
deb http://mirrors.ustc.edu.cn/raspbian/raspbian/ stretch main contrib non-free rpi 
# 清华
deb https://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ stretch main contrib non-free rpi
# 大连东软
deb http://mirrors.neusoft.edu.cn/raspbian/raspbian/ stretch main contrib non-free rpi
# 重庆大学
deb http://mirrors.cqu.edu.cn/raspbian/raspbian/ stretch main contrib non-free rpi
# 浙江大学
deb http://mirrors.zju.edu.cn/raspbian/raspbian/ stretch main contrib non-free rpi
# 阿里云
deb http://mirrors.aliyun.com/raspbian/raspbian/ stretch main contrib non-free rpi 
# 搜狐
deb http://mirrors.sohu.com/raspbian/raspbian/ stretch main contrib non-free rpi 
# 元智大学（中国台湾）
deb http://ftp.cse.yzu.edu.tw/Linux/raspbian/raspbian/ stretch main contrib non-free rpi 
# 新加坡国立大学
deb http://mirror.nus.edu.sg/raspbian/raspbian/ stretch main contrib non-free rpi
# 北陆先端科学技术大学院大学（日本知名镜像站，日常出口带宽2g）
deb http://ftp.jaist.ac.jp/raspbian/ stretch main contrib non-free rpi
# 牛津大学
deb http://mirror.ox.ac.uk/sites/archive.raspbian.org/archive/raspbian/  stretch main contrib non-free rpi
# 美国Berkely大学
deb http://mirrors.ocf.berkeley.edu/raspbian/raspbian/ stretch main contrib non-free rpi
# 美国俄克拉荷马大学
deb http://reflection.oss.ou.edu/raspbian/raspbian/ stretch main contrib non-free rpi
# 南非知名软件源
deb http://mirror.liquidtelecom.com/raspbian/raspbian/ stretch main contrib non-free rpi 
# 默认源（带重定向by mirrorbrain）
deb http://mirrordirector.raspbian.org/raspbian/ stretch main contrib non-free rpi
# 官方源
deb https://archive.raspbian.org/raspbian/ stretch main contrib non-free rpi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的，修改&lt;code&gt;raspi.list&lt;/code&gt;，树莓派的软件包源仅有清华和中科大镜像：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vi /etc/apt/sources.list.d/raspi.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释第一行，在最后添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deb https://mirrors.tuna.tsinghua.edu.cn/raspberrypi/ stretch main ui
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;或者直接在树莓派上执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo sed -i &apos;s#://raspbian.raspberrypi.org#s://mirrors.tuna.tsinghua.edu.cn/raspbian#g&apos; /etc/apt/sources.list
sudo sed -i &apos;s#://archive.raspberrypi.org/debian#s://mirrors.tuna.tsinghua.edu.cn/raspberrypi#g&apos; /etc/apt/sources.list.d/raspi.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果嫌麻烦的话，上面这些需要配置的东西可以写成文件，再用脚本执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将本地编写好的配置文件拷贝到树莓派上
scp ~/Raspberrypi/raspi.list ~/Raspberrypi/sources.list pi@192.168.1.108:./

# 树莓派上拷贝配置文件到相应目录
sudo mv ~/sources.list /etc/apt/sources.list
sudo mv ~/raspi.list /etc/apt/sources.list.d/raspi.list
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;固件升级（谨慎操作，可能会踩到奇怪的编译坑）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo rpi-update &amp;amp;&amp;amp; sudo reboot -h now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软件包升级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;time sudo apt -y update &amp;amp;&amp;amp; time sudo apt -y upgrade &amp;amp;&amp;amp; time sudo apt -y dist-upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软件包清理，顺便装个vim和tmux：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt clean
sudo apt install -y vim tmux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我的tmux的配置只有一行&lt;code&gt;set -g mouse on&lt;/code&gt;，可以将本地编写好的tmux配置文件上传到树莓派家目录，不需要再移动位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scp ~/Raspberrypi/.tmux.conf pi@192.168.1.108:./
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以直接拷贝到树莓派上&lt;/p&gt;
&lt;h1&gt;2 搭建科学上网环境&lt;/h1&gt;
&lt;h2&gt;2.1 安装SS&lt;/h2&gt;
&lt;p&gt;安装shadowsocks命令行客户端（推荐用&lt;code&gt;pip&lt;/code&gt;和&lt;code&gt;apt&lt;/code&gt;，快一些）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install shadowsocks
sudo apt install -y shadowsocks-libev
# 或cpan安装
# 更新CPAN
sudo cpan install CPAN
# 重载CPAN
sudo cpan reload CPAN
sudo cpan Net::Shadowsocks
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.2 配置防火墙&lt;/h2&gt;
&lt;p&gt;安装&lt;code&gt;ufw&lt;/code&gt;以方便调整防火墙策略，其实是我不会用&lt;code&gt;iptables&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install -y ufw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新防火墙策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow proto tcp to 0.0.0.0/0 port 22 comment &quot;sshd listen port&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 1080 comment &quot;Shadowsocks server listen port&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 5900 comment &quot;vnc server listen port&quot;
# 有时候需要用python -m http.server 8000传文件，开个口比较方便
sudo ufw allow proto tcp to 0.0.0.0/0 port 8000 comment &quot;for python -m http.server 8000&quot;
sudo ufw default deny 
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.3 socks5代理&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;/etc/shadowsocks-libev/xxx.json&lt;/code&gt;下配置科学上网服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;server&quot;: &quot;&amp;lt;your-remote-server&amp;gt;&quot;,
    &quot;server_port&quot;: &amp;lt;your-remote-server-port&amp;gt;,
    &quot;timeout&quot;: 60,
    &quot;password&quot;: &quot;&amp;lt;your-password&amp;gt;&quot;,
    &quot;method&quot;: &quot;aes-256-cfb&quot;,
    &quot;local_address&quot;: &quot;0.0.0.0&quot;,
    &quot;local_port&quot;: 1080
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;当然，也可以把配置文件在本地写好，再上传树莓派：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 本地上传
scp ~/Raspberrypi/jiasd.json pi@192.168.1.108:./
# 在树莓派上将配置文件移动到相应目录
sudo mv ~/jiasd.json /etc/shadowsocks-libev/jiasd.json
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;检查默认端口占用，通常SS安装完成后会自动使用默认配置（&lt;code&gt;/etc/shadowsocks-libev/config.json&lt;/code&gt;）启动ss-server进程，因此&lt;code&gt;8388&lt;/code&gt;端口通常会被占用。
而我们在树莓派上使用SS仅仅是为了加速，因此不需要启动ss-server：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 测试端口连通性
nc -zv 127.0.0.1 8388
# 或检查进程占用端口
netstat -nap | grep 8388
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止默认ss服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 检查ss默认系统服务
systemctl list-unit-files # 或 systemctl -a
# 检查ss服务状态
systemctl status shadowsocks-libev.service 
# 停止服务
sudo systemctl stop shadowsocks-libev.service 
sudo systemctl disable shadowsocks-libev.service 
systemctl status shadowsocks-libev.service 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动ss客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 后台
nohup ss-local -v -c /etc/shadowsocks-libev/jiasd.json &amp;amp;
# 或前台
ss-local -v -s &amp;lt;your-remote-server&amp;gt; -p &amp;lt;your-remote-server-port&amp;gt; -l 1080 -k &amp;lt;your-password&amp;gt; -m aes-256-cfb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试ss客户端的socks5是否正常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -I --socks5 localhost:1080 google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止ss客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kill -TERM 5914 &amp;amp;&amp;amp; rm nohup.out
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;strong&gt;编写快捷脚本&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;启动脚本&lt;code&gt;ss-s&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo touch /usr/local/bin/ss-s
sudo chmod +x /usr/local/bin/ss-s
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

nohup ss-local -v -c /etc/shadowsocks-libev/jiasd.json &amp;gt; ~/nohup.log 2&amp;gt;&amp;amp;1 &amp;amp;
echo $! &amp;gt; ~/ss-pid.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止&lt;code&gt;ss-t&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo touch /usr/local/bin/ss-t
sudo chmod +x /usr/local/bin/ss-t
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

kill -s TERM `cat ~/ss-pid.txt`
rm ~/ss-pid.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;当然，也可以把配置文件在本地写好，再上传树莓派：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 本地上传
scp ~/Raspberrypi/ss-s ~/Raspberrypi/ss-t pi@192.168.1.108:./
# 然后再修改文件属性并放置在相应目录下
chmod +x ss-s
chmod +x ss-t
sudo mv ss-s /usr/local/bin/ss-s
sudo mv ss-t /usr/local/bin/ss-t
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;2.4 proxychains-ng&lt;/h2&gt;
&lt;p&gt;ProxyChains是一个使用方便的代理工具，它只会代理指定的程序，下载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir misc &amp;amp;&amp;amp; cd misc
git clone https://github.com/rofl0r/proxychains-ng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd proxychains-ng/
./configure --prefix=/usr --sysconfdir=/etc
# 若是macOS下安装可能会遇到/usr/lib因SIP保护禁止写入的问题，需要更改配置为：
#./configure --prefix=/usr/local --sysconfdir=/etc
# ref: https://apple.stackexchange.com/questions/208764/cant-write-to-usr-lib
make 
sudo make install
sudo make install-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置&lt;code&gt;/etc/proxychains.conf&lt;/code&gt;，将最后一行修改为shadowsocks的端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;socks5 127.0.0.1 1080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试&lt;code&gt;wget&lt;/code&gt;，返回网页源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 wget -qO- https://www.google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3 安装科学计算环境&lt;/h1&gt;
&lt;p&gt;使用&lt;code&gt;pipenv&lt;/code&gt;作为环境管理器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install -y pipenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于这个&lt;code&gt;pipenv&lt;/code&gt;不得不说一下，要想在默认Python版本是3.7的buster上安装3.6的环境，你必须先&lt;code&gt;sudo apt install python3.6&lt;/code&gt;，问题是不论3.6还是3.7的Python，其&lt;code&gt;distutils&lt;/code&gt;包都不全，我也不知道这是bug还是buster特供版Python，反正&lt;code&gt;pipenv&lt;/code&gt;没法正常的用buster中apt安装的版本运行。&lt;/p&gt;
&lt;p&gt;要解决这个问题，对3.7来说还好办，&lt;code&gt;sudo apt install python3-distutils&lt;/code&gt;就行了，通过&lt;code&gt;dpkg -L python3-distutils&lt;/code&gt;查看就知道它在&lt;code&gt;/usr/lib/python3.7/distutils&lt;/code&gt;里装了一些东西。要想让3.6也能正常用&lt;code&gt;pipenv&lt;/code&gt;，只能手动&lt;code&gt;sudo cp -r /usr/lib/python3.7/distutils/* /usr/lib/python3.6/distutils/&lt;/code&gt;了。&lt;/p&gt;
&lt;h1&gt;&lt;s&gt;3 安装Python科学计算环境&lt;/s&gt;&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：berryconda也只更新到python3.6，而新的raspbian buster已经自带python3.7了，所以下面的安装可以酌情考虑。&lt;/p&gt;
&lt;p&gt;我习惯使用&lt;code&gt;conda&lt;/code&gt;环境，因此下载了&lt;a href=&quot;https://github.com/jjhelmus/berryconda&quot;&gt;berryconda&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装berryconda：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x Berryconda3-2.0.0-Linux-armv7l.sh
./Berryconda3-2.0.0-Linux-armv7l.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新&lt;code&gt;conda&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 conda update -y --all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建名叫&lt;code&gt;doa&lt;/code&gt;的环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda create -n doa python=3.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;激活环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source activate doa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;退出环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source deactivate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装Numpy，需要比较长的时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda install numpy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于不使用&lt;code&gt;pip&lt;/code&gt;而，使用&lt;code&gt;conda&lt;/code&gt;进行安装的原因可以参考&lt;a href=&quot;https://github.com/numpy/numpy/issues/8415#issuecomment-269103493&quot;&gt;这里&lt;/a&gt;（简单的说，是由于编译选项问题导致conda安装的Python与pip安装的Numpy不兼容）。&lt;/p&gt;
&lt;p&gt;最后，需要知道的一个坑，不知道为什么我的&lt;code&gt;jupyter notebook&lt;/code&gt;不能正确的识别conda中已安装的环境，也就是新建的环境在笔记本中无法新建或者切换，我做了如下操作：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把笔记本的运行时配置&lt;code&gt;/Users/&amp;lt;your-user&amp;gt;/Library/Jupyter/&lt;/code&gt;中的所有内容清空。&lt;/li&gt;
&lt;li&gt;在新建的环境&lt;code&gt;py36&lt;/code&gt;中手动添加，参考&lt;a href=&quot;https://stackoverflow.com/questions/39604271/conda-environments-not-showing-up-in-jupyter-notebook&quot;&gt;Conda environments not showing up in Jupyter Notebook
Ask&lt;/a&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;(py36) ➜  ~ python -m ipykernel install --user --name py36 --display-name &quot;Python (py36)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;把安装conda前的操作汇总一下：&lt;/p&gt;
&lt;h2&gt;需要在本地执行的操作：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 插SD卡，准备wifi和ssh
cp ~/Raspberrypi/wpa_supplicant.conf /Volumes/boot
touch /Volumes/boot/ssh

# 删除前一次的known-hosts指纹
ssh-keygen -R 192.168.1.108

# 上传本地秘钥
ssh-copy-id pi@192.168.1.108

# 上传更新源
scp ~/Raspberrypi/raspi.list ~/Raspberrypi/sources.list pi@192.168.1.108:./

# 上传SS配置
scp ~/Raspberrypi/jiasd.json pi@192.168.1.108:./

# 上传SS启停脚本
scp ~/Raspberrypi/ss-s ~/Raspberrypi/ss-t pi@192.168.1.108:./

# 上传proxychains配置
scp ~/Raspberrypi/proxychains.conf pi@192.168.1.108:./

# 上传tmux配置
scp ~/Raspberrypi/.tmux.conf pi@192.168.1.108:./
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;需要在树莓派上执行的操作：&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 移动更新源
sudo mv ~/sources.list /etc/apt/sources.list
sudo mv ~/raspi.list /etc/apt/sources.list.d/raspi.list

# 更新树莓派
time sudo apt -y update &amp;amp;&amp;amp; time sudo apt -y upgrade &amp;amp;&amp;amp; time sudo apt -y dist-upgrade
# 清理
sudo apt -y autoremove
sudo apt -y clean

# 安装shadowsocks、vim、tmux
pip install shadowsocks
sudo apt install -y shadowsocks-libev vim tmux

# 安装防火墙
sudo apt-get install -y ufw
# buster默认有两个iptables，选择老版才能和ufw兼容
sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
# 更新防火墙策略
sudo ufw allow proto tcp to 0.0.0.0/0 port 22 comment &quot;sshd listen port&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 5900 comment &quot;vnc server listen port&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 1080 comment &quot;Shadowsocks server listen port&quot;
# 有时候需要用python -m http.server 8000传文件，开个口比较方便
sudo ufw allow proto tcp to 0.0.0.0/0 port 8000 comment &quot;for python -m http.server 8000&quot;
sudo ufw default deny 
sudo ufw enable

# 关闭ss-server
sudo systemctl stop shadowsocks-libev.service 
sudo systemctl disable shadowsocks-libev.service 
systemctl status shadowsocks-libev.service 

# 移动SS配置
sudo mv ~/jiasd.json /etc/shadowsocks-libev/jiasd.json
# 添加可执行权限
chmod +x ss-s
chmod +x ss-t
sudo mv ~/ss-s /usr/local/bin/ss-s
sudo mv ~/ss-t /usr/local/bin/ss-t

# 安装proxychains
mkdir misc &amp;amp;&amp;amp; cd misc
git clone https://github.com/rofl0r/proxychains-ng
cd proxychains-ng/
./configure --prefix=/usr --sysconfdir=/etc
make 
sudo make install
sudo make install-config
cd ~

# 移动proxychains配置
sudo mv ~/proxychains.conf /etc/proxychains.conf
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>树莓派上的麦阵列定位</title><link>https://zlotus.github.io/2018/10/25/rbp3-6mic-doa/</link><guid isPermaLink="true">https://zlotus.github.io/2018/10/25/rbp3-6mic-doa/</guid><description>在树莓派上使用ReSpeaker圆形6麦阵列做DOA(Direction Of Arrival)</description><pubDate>Thu, 25 Oct 2018 21:09:11 GMT</pubDate><content:encoded>&lt;p&gt;最近又弄了个麦阵列玩定位……&lt;/p&gt;
&lt;h1&gt;1 安装ReSpeaker驱动&lt;/h1&gt;
&lt;p&gt;参考：&lt;a href=&quot;http://wiki.seeedstudio.com/cn/ReSpeaker_6-Mic_Circular_Array_kit_for_Raspberry_Pi/&quot;&gt;ReSpeaker 6-Mic Circular Array kit for Raspberry Pi&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;手册上说换个源会快，那就换吧：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/apt/sources.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;手册上让用清华的镜像站：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deb http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib
deb-src http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/ buster main non-free contrib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装麦阵列驱动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir seeed
proxychains4 git clone https://github.com/respeaker/seeed-voicecard.git
cd seeed-voicecard
sudo proxychains4 ./install.sh
sudo reboot -h now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，在安装前不要运行&lt;code&gt;sudo rpi-update&lt;/code&gt;升级固件，否则会因为固件版本问题导致无法安装。&lt;/p&gt;
&lt;h1&gt;2 录音及播放测试&lt;/h1&gt;
&lt;p&gt;查看声卡输入设备&lt;code&gt;arecord -L&lt;/code&gt;，返回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;null
    Discard all samples (playback) or generate zero samples (capture)
default
ac108
dmixer
ac101
sysdefault:CARD=seeed8micvoicec
    seeed-8mic-voicecard,
    Default Audio Device
dmix:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct sample mixing device
dsnoop:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct sample snooping device
hw:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct hardware device without any conversions
plughw:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Hardware device with all software conversions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查声卡输出设备&lt;code&gt;aplay -L&lt;/code&gt;，返回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;null
    Discard all samples (playback) or generate zero samples (capture)
default
ac108
dmixer
ac101
sysdefault:CARD=ALSA
    bcm2835 ALSA, bcm2835 ALSA
    Default Audio Device
dmix:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct sample mixing device
dmix:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct sample mixing device
dsnoop:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct sample snooping device
dsnoop:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct sample snooping device
hw:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Direct hardware device without any conversions
hw:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Direct hardware device without any conversions
plughw:CARD=ALSA,DEV=0
    bcm2835 ALSA, bcm2835 ALSA
    Hardware device with all software conversions
plughw:CARD=ALSA,DEV=1
    bcm2835 ALSA, bcm2835 IEC958/HDMI
    Hardware device with all software conversions
sysdefault:CARD=seeed8micvoicec
    seeed-8mic-voicecard,
    Default Audio Device
dmix:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct sample mixing device
dsnoop:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct sample snooping device
hw:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Direct hardware device without any conversions
plughw:CARD=seeed8micvoicec,DEV=0
    seeed-8mic-voicecard,
    Hardware device with all software conversions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照手册录制、播放音频文件，一切OK，还能边录边播~&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#It will capture sound on AC108 and save as a.wav
arecord -Dac108 -f S32_LE -r 16000 -c 8 a.wav
#Take care of that the captured mic audio is on the first 6 channels

#It will play sound file a.wav on AC101
aplay -D ac101 a.wav
#Do not use -D plughw:1,0 directly except your wave file is single channel only.

#Doing capture &amp;amp;&amp;amp; playback the same time
arecord -D hw:1,0 -f S32_LE -r 16000 -c 8 to_be_record.wav &amp;amp;
#mono_to_play.wav is a mono channel wave file to play
aplay -D plughw:1,0 -r 16000 to_be_record.wav
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照手册安装audacity，图形界面音频编辑软件，可以说是非常舒适了~&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install -y audacity
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3 DOA定位测试&lt;/h1&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://respeaker.io/6_mic_array/&quot;&gt;ReSpeaker 6 Mic Array for Raspberry Pi&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;下载&lt;a href=&quot;https://github.com/introlab/odas.git&quot;&gt;ODAS&lt;/a&gt;源码并编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo proxychains4 apt install -y libfftw3-dev libconfig-dev libasound2-dev cmake
proxychains4 git clone https://github.com/introlab/odas.git
mkdir odas/build
cd odas/build
cmake ..
make
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，编写麦阵列配置文件&lt;code&gt;respeaker-6mic-odas.cfg&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Configuration file for ReSpeaker 6 Mic Array
# Circular shape, R = 0.0463m

version = &quot;2.1&quot;;

# Raw
raw: 
{
    fS = 16000;
    hopSize = 128;
    nBits = 32;
    nChannels = 8; 

    # Input with raw signal from microphones
    interface: {
        type = &quot;soundcard&quot;;
        card = 1;
        device = 0;
    }
}

# Mapping
mapping:
{
    map: (1, 2, 3, 4, 5, 6);
}

# General
general:
{
    epsilon = 1E-20;
    size: 
    {
        hopSize = 128;
        frameSize = 256;
    };
    samplerate:
    {
        mu = 16000;
        sigma2 = 0.01;
    };
    speedofsound:
    {
        mu = 343.0;
        sigma2 = 25.0;
    };
    mics = (
        # Microphone 1
        { 
            mu = ( -0.0232, +0.0401, +0.0000 ); 
            sigma2 = ( +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000 );
            direction = ( +0.000, +0.000, +1.000 );
            angle = ( 80.0, 90.0 );
        },
        # Microphone 6
        { 
            mu = ( +0.0232, +0.0401, +0.0000 ); 
            sigma2 = ( +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000 );
            direction = ( +0.000, +0.000, +1.000 );
            angle = ( 80.0, 90.0 );
        },
        # Microphone 5
        { 
            mu = ( +0.0463, +0.0000, +0.0000 ); 
            sigma2 = ( +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000 );
            direction = ( +0.000, +0.000, +1.000 );
            angle = ( 80.0, 90.0 );        
        },
        # Microphone 4
        { 
            mu = ( +0.0232, -0.0401, +0.0000 ); 
            sigma2 = ( +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000 );
            direction = ( +0.000, +0.000, +1.000 );
            angle = ( 80.0, 90.0 );        
        },
        # Microphone 3
        { 
            mu = ( -0.0232, -0.0401, +0.0000 ); 
            sigma2 = ( +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000 );
            direction = ( +0.000, +0.000, +1.000 );
            angle = ( 80.0, 90.0 );
        },
        # Microphone 2
        { 
            mu = ( -0.0463, +0.0000, +0.0000 ); 
            sigma2 = ( +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000, +0.000 );
            direction = ( +0.000, +0.000, +1.000 );
            angle = ( 80.0, 90.0 );
        }
    );

    # Spatial filters to include only a range of direction if required
    # (may be useful to remove false detections from the floor, or
    # limit the space search to a restricted region)
    spatialfilters = (
        {
            direction = ( +0.000, +0.000, +1.000 );
            angle = (80.0, 90.0);
        }
    );  
    nThetas = 181;
    gainMin = 0.25;
}

# Stationnary noise estimation
sne:
{
    b = 3;
    alphaS = 0.1;
    L = 150;
    delta = 3.0;
    alphaD = 0.1;
}

# Sound Source Localization
ssl:
{
    nPots = 4;
    nMatches = 10;
    probMin = 0.5;
    nRefinedLevels = 1;
    interpRate = 4;

    # Number of scans: level is the resolution of the sphere
    # and delta is the size of the maximum sliding window
    # (delta = -1 means the size is automatically computed)
    scans = (
        { level = 2; delta = -1; },
        { level = 4; delta = -1; }
    );

    # Output to export potential sources
    potential: {
        format = &quot;json&quot;;
        interface: {
            type = &quot;socket&quot;; ip = &quot;192.168.1.100&quot;; port = 9001;
        };
        #format = &quot;undefined&quot;;
        #interface: {
        #    type = &quot;blackhole&quot;;
        #}
    };
}

# Sound Source Tracking
sst:
{
    # Mode is either &quot;kalman&quot; or &quot;particle&quot;
    mode = &quot;kalman&quot;;

    # Add is either &quot;static&quot; or &quot;dynamic&quot;
    add = &quot;dynamic&quot;;

    # Parameters used by both the Kalman and particle filter
    active = (
        { weight = 1.0; mu = 0.3; sigma2 = 0.0025 }
    );

    inactive = (
        { weight = 1.0; mu = 0.15; sigma2 = 0.0025 }
    );

    sigmaR2_prob = 0.0025;
    sigmaR2_active = 0.0225;
    sigmaR2_target = 0.0025;
    Pfalse = 0.1;
    Pnew = 0.1;
    Ptrack = 0.8;

    theta_new = 0.9;
    N_prob = 5;
    theta_prob = 0.8;
    N_inactive = ( 150, 200, 250, 250 );
    theta_inactive = 0.9;

    # Parameters used by the Kalman filter only
    kalman: {
        sigmaQ = 0.001;
    };
   
    # Parameters used by the particle filter only
    particle: {
        nParticles = 1000;
        st_alpha = 2.0;
        st_beta = 0.04;
        st_ratio = 0.5;
        ve_alpha = 0.05;
        ve_beta = 0.2;
        ve_ratio = 0.3;
        ac_alpha = 0.5;
        ac_beta = 0.2;
        ac_ratio = 0.2;
        Nmin = 0.7;
    };

    target: ();

    # Output to export tracked sources
    tracked: {
        format = &quot;json&quot;;
        interface: {
            type = &quot;socket&quot;; ip = &quot;192.168.1.100&quot;; port = 9000;
        };
        #format = &quot;undefined&quot;;
        #interface: {
        #    type = &quot;blackhole&quot;;
        #}
    };
}

sss:
{
    # Mode is either &quot;dds&quot;, &quot;dgss&quot; or &quot;dmvdr&quot;
    mode_sep = &quot;dds&quot;;
    mode_pf = &quot;ms&quot;;

    gain_sep = 1.0;
    gain_pf = 10.0;

    dds: {

    };

    dgss: {
        mu = 0.01;
        lambda = 0.5;
    };

    dmvdr: {

    };

    ms: {
        alphaPmin = 0.07;
        eta = 0.5;
        alphaZ = 0.8;        
        thetaWin = 0.3;
        alphaWin = 0.3;
        maxAbsenceProb = 0.9;
        Gmin = 0.01;
        winSizeLocal = 3;
        winSizeGlobal = 23;
        winSizeFrame = 256;
    };

    ss: {
        Gmin = 0.01;
        Gmid = 0.9;
        Gslope = 10.0;
    };

    separated: {
        fS = 44100;
        hopSize = 512;
        nBits = 16;        

        interface: {
            type = &quot;file&quot;;
            path = &quot;separated.raw&quot;;
        };       
    };

    postfiltered: {
        fS = 44100;
        hopSize = 512;
        nBits = 16;        

        interface: {
            type = &quot;file&quot;;
            path = &quot;postfiltered.raw&quot;;
        };      
    };
}

classify:
{
    frameSize = 1024;
    winSize = 3;
    tauMin = 32;
    tauMax = 200;
    deltaTauMax = 7;
    alpha = 0.3;
    gamma = 0.05;
    phiMin = 0.15;
    r0 = 0.2;    

    category: {
        format = &quot;undefined&quot;;

        interface: {
            type = &quot;blackhole&quot;;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;波达方向定位服务&lt;code&gt;odaslives&lt;/code&gt;位于&lt;code&gt;odas/bin/&lt;/code&gt;目录下，使用配置文件启动定位服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ln -s ~/odas/3rd-lib/odas/bin/odaslive ~/.local/bin/odaslive
odaslive -c ~/respeaker-6mic-odas.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在电脑上下载&lt;a href=&quot;https://github.com/introlab/odas_web/releases&quot;&gt;ODAS Studio&lt;/a&gt;源码并编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo npm install npm@latest -g
wget https://github.com/introlab/odas_web/archive/v0.2-alpha.zip
unzip -a v0.2-alpha.zip 
git clone https://github.com/introlab/odas_web.git
cd odas_web-0.2-alpha 
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在电脑上安装npm，并从源码编译odas web v0.2-alpha：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo npm install npm@latest -g
cd odas_web
npm install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用已编译好的程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 wget https://github.com/introlab/odas_web/releases/download/v0.1-alpha/MacOS_x64.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2018/ODAS_Studio_test.png&quot; alt=&quot;ODA Studio Test&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>Aria2上的BT下载加速</title><link>https://zlotus.github.io/2018/10/15/macOS-aria2/</link><guid isPermaLink="true">https://zlotus.github.io/2018/10/15/macOS-aria2/</guid><description>在macOS上用Aria2下载BT任务</description><pubDate>Mon, 15 Oct 2018 21:09:11 GMT</pubDate><content:encoded>&lt;p&gt;最近迅雷反盗版确实很厉害……&lt;/p&gt;
&lt;p&gt;参考：&lt;a href=&quot;http://www.senra.me/solutions-to-aria2-bt-metalink-download-slowly/&quot;&gt;解决Aria2 BT下载速度慢没速度的问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;训练用不了了，用Aria2下载几乎没有速度，怎么办呢？只能靠添加BT源来加速了，新建配置文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir ~/.aria2
vim ~/.aria2/aria2.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建Aira2的会话文件，用于保存任务状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch ~/.aria2/aria2.session
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Github上有个维护BT源的项目&lt;a href=&quot;https://github.com/ngosang/trackerslist&quot;&gt;An updated list of public BitTorrent trackers&lt;/a&gt;，用Best 20就行，在配置文件里写入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# https://github.com/ngosang/trackerslist
bt-tracker=udp://62.138.0.158:6969/announce,udp://185.225.17.100:1337/announce,udp://51.15.4.13:1337/announce,udp://151.80.120.114:2710/announce,udp://208.83.20.20:6969/announce,udp://184.105.151.164:6969/announce,udp://191.96.249.23:6969/announce,udp://51.15.76.199:6969/announce,udp://5.79.83.194:6969/announce,udp://51.15.40.114:80/announce,http://128.1.203.23:8080/announce,udp://91.216.110.52:451/announce,udp://5.206.28.90:6969/announce,http://123.249.16.65:6961/announce,udp://51.38.184.185:6969/announce,udp://188.246.227.212:80/announce,udp://51.15.103.67:1337/announce,udp://95.211.168.204:2710/announce,http://104.28.22.4:80/announce,udp://176.31.106.35:80/announce
dir=/Users/zealot/Downloads
max-connection-per-server=16
save-session=/Users/zealot/.aria2/aria2.session
input-file=/Users/zealot/.aria2/aria2.session
save-session-interval=60
enable-dht=true
bt-enable-lpd=true
enable-peer-exchange=true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是有了速度。&lt;/p&gt;
</content:encoded></item><item><title>树莓派上的摄像头与OpenCV</title><link>https://zlotus.github.io/2018/10/05/rbp3-pyopencv-camera/</link><guid isPermaLink="true">https://zlotus.github.io/2018/10/05/rbp3-pyopencv-camera/</guid><description>在树莓派上构建并编译OpenCV3</description><pubDate>Fri, 05 Oct 2018 21:09:11 GMT</pubDate><content:encoded>&lt;p&gt;树莓派上都有OpenCV了，是不是再装个摄像头玩[奸笑]&lt;/p&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://www.pyimagesearch.com/2015/03/30/accessing-the-raspberry-pi-camera-with-opencv-and-python/&quot;&gt;Accessing the Raspberry Pi Camera with OpenCV and Python&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;1 摄像头连接及配置&lt;/h1&gt;
&lt;p&gt;摄像头安装及配置我就不写了，懒的翻译原文……&lt;/p&gt;
&lt;h2&gt;1.1 安装依赖&lt;/h2&gt;
&lt;p&gt;安装带&lt;code&gt;array&lt;/code&gt;子模块的&lt;code&gt;picamera&lt;/code&gt;，用以将图像Numpy的array传给OpenCV处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source activate cv
pip install &quot;picamera[array]&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;1.2 摄像头图像预览&lt;/h2&gt;
&lt;p&gt;需要注意的是，树莓派上的vnc-server默认配置是不能将传输摄像头预览图像传送到vnc-client上的，需要修改配置：在树莓派上打开vnc-server的菜单 -&amp;gt; &lt;code&gt;Options...&lt;/code&gt;-&amp;gt; &lt;code&gt;TroubleShooting&lt;/code&gt; -&amp;gt; 选择&lt;code&gt;Enable experimental direct capture mode&lt;/code&gt;。（参考&lt;a href=&quot;https://raspberrypi.stackexchange.com/a/74390&quot;&gt;Sending Raspberry Pi Camera preview to a laptop running VNC Viewer&lt;/a&gt;）&lt;/p&gt;
&lt;h1&gt;2 使用Python+OpenCV访问摄像头输出的图片&lt;/h1&gt;
&lt;p&gt;通过Python将摄像头输出的图片送入OpenCV进行显示，显示时用到了OpenCV的highGUI：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import cv2

# 初始化摄像头
camera = PiCamera()
# 新建原始摄像头数据的引用
rawCapture = PiRGBArray(camera)

# 等一小段时间让摄像头启动
time.sleep(0.1)

# 从摄像头获取一张图片
camera.capture(rawCapture, format=&quot;bgr&quot;)
image = rawCapture.array

# 在屏幕上显示，按下任意键退出
cv2.imshow(&quot;Image&quot;, image)
cv2.waitKey(0)
cv2.destroyAllWindows()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是获取图片时使用的矩阵格式为&lt;code&gt;format=&quot;bgr&quot;&lt;/code&gt;，参见&lt;a href=&quot;https://picamera.readthedocs.io/en/release-1.13/recipes2.html?highlight=capture#capturing-to-an-opencv-object&quot;&gt;4.2. Capturing to an OpenCV object&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;PiRGBArray&lt;/code&gt;对象的好处就是省去了将原始的RGB矩阵封装成各种格式（如JPEG）的步骤，这样读入OpenCV时也就省去了OpenCV将各种格式解码为RGP矩阵的步骤，尤其是在树莓派这种资源受限的设备上能够节省很多算力。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from picamera.array import PiRGBArray
from picamera import PiCamera
import time
import cv2

# 初始化摄像头并设置分辨率和帧率
camera = PiCamera()
camera.resolution = (640, 480)
camera.framerate = 32
# 新建原始摄像头数据的引用，指定分辨率
rawCapture = PiRGBArray(camera, size=(640, 480))

# 等一小段时间让摄像头启动
time.sleep(0.1)

# 从摄像头中获取视频帧
for frame in camera.capture_continuous(rawCapture, format=&quot;bgr&quot;, use_video_port=True):
    # 直接从摄像头获取使用Numpy矩阵表达的原始图片数据，
    # 其中`camera.capture_continuous`返回一个`frame`对象，
    # `frame`对象的`array`属性中存储的就是该帧Numpy矩阵
    image = frame.array

    # 在highGUI中显示该帧
    cv2.imshow(&quot;Frame&quot;, image)
    key = cv2.waitKey(1) &amp;amp; 0xFF

    # 注意，在读取下一帧前需要清空当前帧，否则代码报错
    rawCapture.truncate(0)

    # 在highGUI中按q键退出循环
    if key == ord(&quot;q&quot;):
        cv2.destroyAllWindows()
        break
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3 试一试图像的透视变换&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;搞事情之前&lt;/strong&gt;：我想在Python模块搜索路径中加入我的工作环境&lt;code&gt;~/pyworkspace&lt;/code&gt;，以后的事情都在这里搞。
因此，在&lt;code&gt;~/berryconda3/envs/cv/lib/python3.5/site-packages&lt;/code&gt;下新建&lt;code&gt;workspace.pth&lt;/code&gt;文件，写入内容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/home/pi/pyworkspace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;搞一个小事情先，试一下OpenCV的性能，比如&lt;a href=&quot;https://www.pyimagesearch.com/2014/08/25/4-point-opencv-getperspective-transform-example/&quot;&gt;4 Point OpenCV getPerspective Transform Example&lt;/a&gt;，矩形的四点透视变换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mkdir cvpi &amp;amp;&amp;amp; cd cvpi
touch __init__.py
mkdir transform &amp;amp;&amp;amp; cd transform
touch perspective_transform.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;透视变换的代码思路：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过Python代码测量图片中目标矩形区域的长宽；&lt;/li&gt;
&lt;li&gt;计算输出直角坐标系中的矩形顶点坐标&lt;/li&gt;
&lt;li&gt;利用&lt;code&gt;cv2.getPerspectiveTransform()&lt;/code&gt;函数计算变换矩阵并应用，输出图片&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np
import cv2

# /home/pi/cvpi/transform/perspective_transform.py
# 使用透视变换还原矩形

def order_points(pts):
    &quot;&quot;&quot;
    调整矩形四个点的排列顺序，用以维护有序的坐标列表。

    接受一个包含四个坐标的列表，按照左上、右上、右下、左下的顺序重新排列坐标。

    Args:
        pts: 一个shape为(4, 2)的nparray。
    Returns:
        顺序调整后的nparray，shape依然是(4, 2)。
    &quot;&quot;&quot;
    # 初始化包含四个坐标的坐标列表（cv用坐标系，x轴向右，y轴向下），分别对应矩形的左上、右上、右下、左下坐标
    rect = np.zeros((4, 2), dtype = &quot;float32&quot;)

    # 计算每个坐标的x+y，左上应当具有最小的和，而右下则应当具有最大的和
    s = pts.sum(axis = 1)
    rect[0] = pts[np.argmin(s)]
    rect[2] = pts[np.argmax(s)]

    # 计算每个坐标的y-x，右上应具有最小差，左下应具有最大差
    diff = np.diff(pts, axis = 1)
    rect[1] = pts[np.argmin(diff)]
    rect[3] = pts[np.argmax(diff)]
    
    return rect

def four_point_transform(image, pts):
    &quot;&quot;&quot;
    用透视变换，将图片中使用`pts`标注的透视图矩形区域，变换为鸟瞰图中的矩形区域。

    接受一个图片，以及图片中一个矩形的顶点坐标列表，通过透视变换输出该区域的鸟瞰图。

    Args: 
        image: 图片RGB矩阵。
        pts: 目标矩形区域的顶点坐标列表。
    Returns: 
        透视变换后的矩形区域鸟瞰图片。
    &quot;&quot;&quot;
    # 重新排列矩形顶点坐标，初始化左上、右上、右下、左下坐标
    rect = order_points(pts)
    (tl, tr, br, bl) = rect

    # 计算矩形宽度：取 右上与左上顶点间的距离 与 右下与左下顶点间的距离 之中的最大值
    widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))
    widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))
    maxWidth = max(int(widthA), int(widthB))

    # 计算矩形高度：取 右上与右下顶点间的距离 与 左上与左下顶点间的距离 之中的最大值
    heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))
    heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))
    maxHeight = max(int(heightA), int(heightB))

    # 按照左上(0, 0)、右上(0, H)、右下(W, H)、左下(W, 0)的顺序构造变换后的鸟瞰图坐标列表
    dst = np.array([
        [0, 0],
        [maxWidth - 1, 0],
        [maxWidth - 1, maxHeight - 1],
        [0, maxHeight - 1]
    ], dtype = &quot;float32&quot;)

    # 使用源坐标和目标坐标计算透视变换矩阵
    M = cv2.getPerspectiveTransform(rect, dst)
    # 在源图片上应用透视变换
    warped = cv2.warpPerspective(image, M, (maxWidth, maxHeight))

    return warped
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试变换效果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ../..
mkdir test &amp;amp;&amp;amp; cd test
touch pptf_test.py
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from cvpi.transform.perspective_transform import four_point_transform
import numpy as np
import argparse
import cv2

# /home/pi/cvpi/test/pptf_test.py
# 测试透视变换：
# python pptf_test.py --image images/example_01.png --coords &quot;[(73, 239), (356, 117), (475, 265), (187, 443)]&quot;
# python pptf_test.py --image images/example_02.png --coords &quot;[(101, 185), (393, 151), (479, 323), (187, 441)]&quot;
# python pptf_test.py --image images/example_03.png --coords &quot;[(63, 242), (291, 110), (361, 252), (78, 386)]&quot;

# 构造命令行工具
ap = argparse.ArgumentParser()
ap.add_argument(&quot;-i&quot;, &quot;--image&quot;, help = &quot;path to the image file&quot;)
ap.add_argument(&quot;-c&quot;, &quot;--coords&quot;,
	help = &quot;comma seperated list of source points&quot;)
args = vars(ap.parse_args())
 
# load the image and grab the source coordinates (i.e. the list of
# of (x, y) points)
# 从文件读取图片RGB矩阵，从命令行中读取目标矩形区域的顶点坐标。
# NOTE: 出于安全考虑，不鼓励使用`eval`函数，但在此仅为测试代码功能。
image = cv2.imread(args[&quot;image&quot;])
pts = np.array(eval(args[&quot;coords&quot;]), dtype = &quot;float32&quot;)
 
# 测试`four_point_transform`的功能。
warped = four_point_transform(image, pts)
 
# 显示图片
cv2.imshow(&quot;Original&quot;, image)
cv2.imshow(&quot;Warped&quot;, warped)
cv2.waitKey(0)
cv2.destroyAllWindows()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2018/OpenCV_perspective_transform.png&quot; alt=&quot;矩形透视变换测试&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;4 试一试OpenCV文档扫描器&lt;/h1&gt;
&lt;p&gt;在搞事情之前：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# pwd: ~./pyworkspace/cvpi
mkdir tool &amp;amp;&amp;amp; cd tool
touch __init__.py scan.py
conda install -y scikit-image
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;作者还开源了自己封装的OpenCV常用函数，比如上面的透视变换就包含在内，安装&lt;a href=&quot;https://github.com/jrosebr1/imutils&quot;&gt;imutils&lt;/a&gt;工具包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install --upgrade imutils
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，我发现16G的卡已经用了很多空间了，所以推荐删点空间出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get purge wolfram-engine
sudo apt-get purge libreoffice*
sudo apt-get clean
sudo apt-get autoremove
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;from cvpi.transform.perspective_transform import four_point_transform
from skimage.filters import threshold_local
import numpy as np
import argparse
import cv2
import imutils

# /home/pi/cvpi/tool/scan.py
 
# 构造命令行工具
ap = argparse.ArgumentParser()
ap.add_argument(&quot;-i&quot;, &quot;--image&quot;, required = True,
	help = &quot;Path to the image to be scanned&quot;)
args = vars(ap.parse_args())

################################################################################
# 1 边缘检测
################################################################################
# 读取图片，计算图片原始高度与目标高度的缩放比例
# 备份图片，然后以500为高度成比例缩放图片
image = cv2.imread(args[&quot;image&quot;])
ratio = image.shape[0] / 500.0
orig = image.copy()
image = imutils.resize(image, height = 500)
 
# 将图片处理为灰阶图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 使用高斯模糊去掉较为繁杂的边界，目的是仅保留明显的轮廓，有助于轮廓检查
gray = cv2.GaussianBlur(gray, (5, 5), 0)
edged = cv2.Canny(gray, 75, 200)
 
# show the original image and the edge detected image
# 显示原图与边缘检测的结果对比
print(&quot;STEP 1: Edge Detection&quot;)
cv2.imshow(&quot;Image&quot;, image)
cv2.imshow(&quot;Edged&quot;, edged)
cv2.waitKey(0)
cv2.destroyAllWindows()

################################################################################
# 2 轮廓查找
#   文档扫描的对象通常是一页矩形的文档，按照经验矩形有四个顶点。
#   因此我们假设，边界检查结果中最大的且具有四个顶点的轮廓即为文档轮廓。
################################################################################
# 获得轮廓列表：
# 第一个参数：传入要进行轮廓查找的图片，由于函数会直接操作该图片，这里使用边缘检测后的图片备份。
# 第二个参数：表示使用仅普通列表组织返回的轮廓，
#   若指定为RETR_TREE则还会返回为有层级结构的列表（即表示出轮廓之间的包含关系）。
# 第三参数表示简单压缩轮廓点集，如将共线段的点简化为线段两端点，节省内存。
cnts = cv2.findContours(edged.copy(), cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# 由于OpenCV2与3返回轮廓检查返回值结构不一样，因此为了代码通用性需要判断OpenCV版本。
cnts = cnts[0] if imutils.is_cv2() else cnts[1]
# 按面积将轮廓从大到小排序，保留面积最大的5个轮廓。
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:5]
 
for c in cnts:
	# 测量轮廓周长。
	peri = cv2.arcLength(c, True)
    # 使用Douglas-Peucker曲线近似算法，允许2%的周长误差，返回近似后的顶点列表。
	approx = cv2.approxPolyDP(c, 0.02 * peri, True)
 
    # 如果近似结果仅包含4个顶点，则假设找到原图片中目标文件的轮廓
	if len(approx) == 4:
		screenCnt = approx
		break
 
# 显示目标文件的轮廓
print(&quot;STEP 2: Find contours of paper&quot;)
# 参数分别为：要绘制的原图，轮廓点集列表，需要绘制的轮廓（-1表示画出所有），轮廓颜色，轮廓粗细
cv2.drawContours(image, [screenCnt], -1, (0, 255, 0), 2)
cv2.imshow(&quot;Outline&quot;, image)
cv2.waitKey(0)
cv2.destroyAllWindows()

################################################################################
# 3 透视变换
# 将获得的文档轮廓进行透视变换，输出文档的鸟瞰图。
################################################################################
# 将轮廓按比例缩放，并应用透视变换。
warped = four_point_transform(orig, screenCnt.reshape(4, 2) * ratio)
 
# 将变换结果灰阶处理，设置阈值以制造出类似扫描仪的黑白效果。
warped = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY)
T = threshold_local(warped, 11, offset = 10, method = &quot;gaussian&quot;)
warped = (warped &amp;gt; T).astype(&quot;uint8&quot;) * 255
 
# 显示原图与处理后效果对比
print(&quot;STEP 3: Apply perspective transform&quot;)
cv2.imshow(&quot;Original&quot;, imutils.resize(orig, height = 650))
cv2.imshow(&quot;Scanned&quot;, imutils.resize(warped, height = 650))
cv2.waitKey(0)
cv2.destroyAllWindows()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;边缘检测&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2018/OpenCV_edge.png&quot; alt=&quot;边缘检测&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;轮廓查找&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2018/OpenCV_outline.png&quot; alt=&quot;轮廓查找&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;透视变换&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2018/OpenCV_scanned.png&quot; alt=&quot;透视变换&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>在树莓派上安装Python3+OpenCV3</title><link>https://zlotus.github.io/2018/10/01/rbp3-opencv3/</link><guid isPermaLink="true">https://zlotus.github.io/2018/10/01/rbp3-opencv3/</guid><description>在树莓派上构建并编译OpenCV3</description><pubDate>Mon, 01 Oct 2018 12:06:10 GMT</pubDate><content:encoded>&lt;p&gt;最近给树莓派买了一些传感器，感觉能做的事更多了，那就先抄一个机器视觉的玩具练练手吧~当然，怎么能少了OpenCV呢[奸笑]&lt;/p&gt;
&lt;p&gt;参考：&lt;a href=&quot;https://www.pyimagesearch.com/2017/09/04/raspbian-stretch-install-opencv-3-python-on-your-raspberry-pi/&quot;&gt;Raspbian Stretch: Install OpenCV 3 + Python on your Raspberry Pi&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;1 扩展存储卡空间&lt;/h1&gt;
&lt;p&gt;运行树莓派配置&lt;code&gt;sudo raspi-config&lt;/code&gt;，选择&quot;Advanced Options&quot;，选择&quot;Expand filesystem&quot;，按提示重启。&lt;/p&gt;
&lt;p&gt;使用&lt;code&gt;df -h&lt;/code&gt;检查存储空间。&lt;/p&gt;
&lt;p&gt;当然，如果空间仍然不够用，可以删点东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get purge wolfram-engine
sudo apt-get purge libreoffice*
sudo apt-get clean
sudo apt-get autoremove
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2 安装依赖&lt;/h1&gt;
&lt;h2&gt;2.1 更新系统&lt;/h2&gt;
&lt;p&gt;当然，安装依赖前先将系统更新至最新：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; time sudo apt-get upgrade &amp;amp;&amp;amp; time sudo apt-get dist-upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.2 安装编译/开发工具&lt;/h2&gt;
&lt;p&gt;安装包括&lt;code&gt;cmake&lt;/code&gt;在内的开发工具，帮助我们配置并编译&lt;code&gt;opencv&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install build-essential cmake pkg-config
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.3 图片相关依赖&lt;/h2&gt;
&lt;p&gt;安装图片I/O相关包，包括常见的JPEG、PNG、TIFF等：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.4 视频相关依赖&lt;/h2&gt;
&lt;p&gt;安装读取视频、处理视频流的相关依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get install libxvidcore-dev libx264-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.5 &lt;code&gt;highgui&lt;/code&gt;相关依赖&lt;/h2&gt;
&lt;p&gt;安装OpenCV时会附带一个叫做&lt;code&gt;highgui&lt;/code&gt;的子模块，该模块使用GUI界面显示图片，要使得该模块能够正常编译，需要安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install libgtk2.0-dev libgtk-3-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.6 优化相关依赖&lt;/h2&gt;
&lt;p&gt;通过安装一些依赖可以优化OpenCV内部的矩阵运算过程，这些优化依赖对像树莓派这样计算资源受限的设备来说尤为重要：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install libatlas-base-dev gfortran
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.7 Python头文件&lt;/h2&gt;
&lt;p&gt;为了通过Python使用OpenCV库，在编译时需要Python头文件等资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install python2.7-dev python3-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3 下载OpenCV源码&lt;/h1&gt;
&lt;h2&gt;3.1 OpenCV&lt;/h2&gt;
&lt;p&gt;我们从&lt;a href=&quot;https://github.com/opencv/opencv&quot;&gt;OpenCV官网&lt;/a&gt;下载&lt;a href=&quot;https://opencv.org/releases.html&quot;&gt;最新版源码&lt;/a&gt;（版本3.4.3），该版本带有深度神经网络模块，推荐从本机下载并上传树莓派，因为本机迅雷下器比较快🤦‍：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scp ./Downloads/cv_3.4.3.zip pi@192.168.1.108:./
unzip cv_3.4.3.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;3.2 OpenCV高级功能&lt;/h2&gt;
&lt;p&gt;我们希望能够使用OpenCV的所有功能（包括如SIFT、SURF在内的高级功能），需要下载相应版本的&lt;a href=&quot;https://github.com/opencv/opencv_contrib/releases&quot;&gt;opencv_contrib&lt;/a&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scp ./Downloads/cv_c_3.4.3.zip pi@192.168.1.108:./
unzip cv_c_3.4.3.zip
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;4 Python版本控制&lt;/h1&gt;
&lt;p&gt;我习惯使用&lt;code&gt;conda&lt;/code&gt;，因此下了个&lt;a href=&quot;https://github.com/jjhelmus/berryconda&quot;&gt;berryconda&lt;/a&gt;，上传树莓派（还是因为迅雷更快🤦‍）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scp ./Downloads/Berryconda3-2.0.0-Linux-armv7l.sh pi@192.168.1.108:./
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chmod +x Berryconda3-2.0.0-Linux-armv7l.sh
./Berryconda3-2.0.0-Linux-armv7l.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成之后更新&lt;code&gt;conda&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda update --all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建名叫&lt;code&gt;cv&lt;/code&gt;的环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda create -n cv python=3.5.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;激活环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source activate cv
# 退出环境
#source deactivate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装Numpy，需要比较长的时间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda install numpy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于原文使用&lt;code&gt;pip&lt;/code&gt;而这里使用&lt;code&gt;conda&lt;/code&gt;的参考&lt;a href=&quot;https://github.com/numpy/numpy/issues/8415#issuecomment-269103493&quot;&gt;原因&lt;/a&gt;（编译选项问题导致conda安装的Python与pip安装的Numpy不兼容）。&lt;/p&gt;
&lt;h1&gt;5 其他软件包&lt;/h1&gt;
&lt;p&gt;因为个人爱好和习惯，我又安装了TensorFlow以及tesseract：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install libatlas-base-dev
pip install tensorflow
sudo apt-get install tesseract-ocr
pip install pytesseract
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6 构建&lt;/h1&gt;
&lt;h2&gt;6.1 解压并构建&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;cd ~/opencv-3.4.3/
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib-3.4.3/modules \
    -D BUILD_EXAMPLES=ON ..
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6.2 检查构建输出&lt;/h2&gt;
&lt;p&gt;务必检查CMake构建输出中Python解释器/库等信息，正常构建完成的信息应包含类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;--   Python 3:
--     Interpreter:                 /home/pi/berryconda3/envs/cv/bin/python3 (ver 3.5.3)
--     Libraries:                   /usr/lib/arm-linux-gnueabihf/libpython3.5m.so (ver 3.5.3)
--     numpy:                       /home/pi/berryconda3/envs/cv/lib/python3.5/site-packages/numpy/core/include (ver 1.15.1)
--     packages path:               lib/python3.5/site-packages
--
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6.3 修改SWAP配置&lt;/h2&gt;
&lt;p&gt;在开始编译前，先调整一下虚拟内存大小，使得编译过程能够利用树莓派的全部四颗核心，避免编译过程因为内存不足而挂起：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vim /etc/dphys-swapfile
# dphys-swapfile:
# CONF_SWAPSIZE=100
CONF_SWAPSIZE=1024
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再重启虚拟内存服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo /etc/init.d/dphys-swapfile stop
sudo /etc/init.d/dphys-swapfile start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：在完成所有编译、安装，且试验成功后，记得吧虚拟内存改回去，否则据说容易烧卡。&lt;/p&gt;
&lt;h1&gt;7 编译&lt;/h1&gt;
&lt;p&gt;终于可以开始编译了，大约需要一个半小时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make -j4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译时树莓派会启用全部四颗核心，因此强烈建议开启散热风扇，编译时的&lt;code&gt;top&lt;/code&gt; CPU利用率：
&lt;img src=&quot;https://raw.githubusercontent.com/zlotus/imagebed/master/2018/OpenCV_compile.png&quot; alt=&quot;编译中的CPU利用率&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;8 安装&lt;/h1&gt;
&lt;p&gt;将编译完成的OpenCV安装在系统中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo make install
sudo ldconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时OpenCV及Python支持已经安装再&lt;code&gt;/usr/local/lib/python3.5/site-packages&lt;/code&gt;目录下了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ls -l /usr/local/lib/python3.5/site-packages/
总用量 4628
-rw-r--r-- 1 root staff 4738076 9月  28 23:16 cv2.cpython-35m-arm-linux-gnueabihf.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;出于某种原因，编译完成的的&lt;code&gt;.so&lt;/code&gt;文件叫做&lt;code&gt;cv2.cpython-35m-arm-linux-gnueabihf.so&lt;/code&gt;，我们需要将其重命名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /usr/local/lib/python3.5/site-packages/
sudo mv cv2.cpython-35m-arm-linux-gnueabihf.so cv2.so
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后再将其链接在conda的&lt;code&gt;cv&lt;/code&gt;环境中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ~/berryconda3/envs/cv/lib/python3.5/site-packages/
ln -s /usr/local/lib/python3.5/site-packages/cv2.so cv2.so
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;9 测试&lt;/h1&gt;
&lt;p&gt;在&lt;code&gt;cv&lt;/code&gt;环境中测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source activate cv
python
&amp;gt;&amp;gt;&amp;gt; import cv2
&amp;gt;&amp;gt;&amp;gt; cv2.__version__
&apos;3.4.3&apos;
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;10 一些问题&lt;/h1&gt;
&lt;p&gt;一开始我使用python 3.6.6：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda create -n cv python=3.6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ~/opencv-3.4.3/
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib-3.4.3/modules \
    -D BUILD_EXAMPLES=ON ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-- Found PythonInterp: /usr/bin/python2.7 (found suitable version &quot;2.7.13&quot;, minimum required is &quot;2.7&quot;)
-- Found PythonLibs: /usr/lib/arm-linux-gnueabihf/libpython2.7.so (found suitable exact version &quot;2.7.13&quot;)
-- Found PythonInterp: /home/pi/berryconda3/envs/cv/bin/python3 (found suitable version &quot;3.6.6&quot;, minimum required is &quot;3.4&quot;)
-- Could NOT find PythonLibs (missing:  PYTHON_LIBRARIES PYTHON_INCLUDE_DIRS) (Required is exact version &quot;3.6.6&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;即树莓派上还没有python3.6-dev，仅有python3.5-dev（即python 3.5.3-0版本），于是我打算用&lt;code&gt;cmake -D&lt;/code&gt;指定，或是export环境变量，发现报错依然存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ~/opencv-3.4.3/
mkdir build
cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D OPENCV_EXTRA_MODULES_PATH=~/opencv_contrib-3.4.3/modules \
    -D PYTHON_INCLUDE_DIR=$(python -c &quot;from distutils.sysconfig import get_python_inc; print(get_python_inc())&quot;)  \
    -D PYTHON_LIBRARY=$(python -c &quot;import distutils.sysconfig as sysconfig; print(sysconfig.get_config_var(&apos;LIBDIR&apos;))&quot;) \
    -D BUILD_EXAMPLES=ON ..
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考&lt;a href=&quot;https://github.com/opencv/opencv/issues/8174&quot;&gt;CMake cannot find PythonLibs #8174&lt;/a&gt;的说明想修改cmake文件也不成功。&lt;/p&gt;
&lt;p&gt;通过&lt;a href=&quot;https://packages.debian.org/search?keywords=python3.6-dev&quot;&gt;Debian软件包搜索&lt;/a&gt;可知这个包在新版的buster中才有，scratch中是没有的……&lt;/p&gt;
&lt;p&gt;肝的太晚了实在懒得解决了……有知道的仁人志士麻烦邮箱我一下解决方案Orz。&lt;/p&gt;
</content:encoded></item><item><title>树莓派上的科学上网</title><link>https://zlotus.github.io/2018/09/25/rbp3-aws-gateway/</link><guid isPermaLink="true">https://zlotus.github.io/2018/09/25/rbp3-aws-gateway/</guid><description>在树莓派上准备科学上网环境</description><pubDate>Tue, 25 Sep 2018 12:06:10 GMT</pubDate><content:encoded>&lt;p&gt;树莓派上不论是raspbian的安装/更新，还是conda/pip的安装/更新，都因为科学原因不太稳定，因此可以先准备科学安装/更新环境。&lt;/p&gt;
&lt;h1&gt;1 系统安装及配置&lt;/h1&gt;
&lt;h2&gt;1.1 Headless下的WiFi及ssh配置&lt;/h2&gt;
&lt;p&gt;因为要将树莓派当做网关用，所以用命令行就可以了，下载lite镜像。
按树莓派官方说明在SD卡上写入镜像，然后进行Headless配置：&lt;/p&gt;
&lt;p&gt;配置WIFI，需要在SD卡根目录下新建一个名为&lt;code&gt;wpa_supplicant.conf&lt;/code&gt;的文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /Volumes/boot/wpa_supplicant.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写入WIFI配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;country=US
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1

network={
    ssid=&quot;&amp;lt;YOUR-NETWORK-NAME&amp;gt;&quot;
    psk=&quot;&amp;lt;YOUR-NETWORK-PASSWORD&amp;gt;&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开启SSH，只需在SD卡根目录下新建一个名为ssh的空文件即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch /Volumes/boot/ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置完成后正常启动树莓派即可自动连接WIFI，SSH可以正常登录。
注：如果不知道设备IP，可以使用&lt;code&gt;ping raspberry.local&lt;/code&gt;查找。&lt;/p&gt;
&lt;p&gt;如果这个IP上曾经还有过其他树莓派前辈，那么要先删除本机上的ssh信任主机，比如我的树莓派一直用固定IP配在108上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen -R 192.168.1.108
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次用&lt;code&gt;pi&lt;/code&gt;登录后默认密码为&lt;code&gt;raspberry&lt;/code&gt;，应使用&lt;code&gt;sudo raspi-config&lt;/code&gt;通过树莓派管理工具修改密码、主机名等信息，或是直接使用&lt;code&gt;passwd&lt;/code&gt;修改密码。&lt;/p&gt;
&lt;h2&gt;1.2 换国内镜像源&lt;/h2&gt;
&lt;p&gt;解决升级慢或&lt;code&gt;Cannot initiate the connection to mirrors.opencas.cn&lt;/code&gt;问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vi /etc/apt/sources.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;sources.list&lt;/code&gt;，注释第一行，在最后添加国内镜像站：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deb http://mirrors.aliyun.com/raspbian/raspbian/ stretch main non-free contrib
deb-src http://mirrors.aliyun.com/raspbian/raspbian/ stretch main non-free contrib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同样的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vi /etc/apt/sources.list.d/raspi.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改&lt;code&gt;raspi.list&lt;/code&gt;，注释第一行，在最后添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;deb http://mirrors.aliyun.com/raspbian/raspbian/ stretch main ui
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以使用其他镜像站，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 中国科学技术大学
Raspbian http://mirrors.ustc.edu.cn/raspbian/raspbian/
# 阿里云
Raspbian http://mirrors.aliyun.com/raspbian/raspbian/
# 清华大学
Raspbian http://mirrors.tuna.tsinghua.edu.cn/raspbian/raspbian/
# 华中科技大学
Raspbian http://mirrors.hustunique.com/raspbian/raspbian/
Arch Linux ARM http://mirrors.hustunique.com/archlinuxarm/
# 华南农业大学（华南用户）
Raspbian http://mirrors.scau.edu.cn/raspbian/
# 大连东软信息学院源（北方用户）
Raspbian http://mirrors.neusoft.edu.cn/raspbian/raspbian/
# 重庆大学源（中西部用户）
Raspbian http://mirrors.cqu.edu.cn/Raspbian/raspbian/
# 中山大学 已跳转至中国科学技术大学源
Raspbian http://mirror.sysu.edu.cn/raspbian/raspbian/
# 新加坡国立大学
Raspbian http://mirror.nus.edu.sg/raspbian/raspbian
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;固件升级，&lt;strong&gt;谨慎操作，可能会踩到奇怪的编译坑&lt;/strong&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo rpi-update &amp;amp;&amp;amp; sudo reboot -h now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软件包升级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; time sudo apt-get upgrade &amp;amp;&amp;amp; time sudo apt-get dist-upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软件包清理，顺便装个vim：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get clean
sudo apt-get install -y vim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决树莓派的&lt;code&gt;locale&lt;/code&gt;警告：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo dpkg-reconfigure locales
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;选择生成&lt;code&gt;en_GB.UTF-8&lt;/code&gt;、&lt;code&gt;en_US.UTF-8&lt;/code&gt;、&lt;code&gt;zh_CN.UTF-8&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;为空项设置值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo update-locale LANGUAGE=&quot;en_GB.UTF-8&quot;
sudo update-locale LC_ALL=&quot;en_GB.UTF-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2 搭建科学工具&lt;/h1&gt;
&lt;h2&gt;2.1 安装SS&lt;/h2&gt;
&lt;p&gt;安装shadowsocks命令行客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install shadowsocks
sudo apt-get install -y shadowsocks-libev
# 更新CPAN
sudo cpan install CPAN
# 重载CPAN
sudo cpan reload CPAN
sudo cpan Net::Shadowsocks
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.2 配置防火墙&lt;/h2&gt;
&lt;p&gt;安装UFW以方便调整防火墙策略，其实是我不会用&lt;code&gt;iptables&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install ufw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新防火墙策略：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw allow proto tcp to 0.0.0.0/0 port 22 comment &quot;sshd listen port&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 5900 comment &quot;vnc server listen port&quot;
sudo ufw allow proto tcp to 0.0.0.0/0 port 1080 comment &quot;Shadowsocks server listen port&quot;
sudo ufw default deny 
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.3 socks5代理&lt;/h2&gt;
&lt;p&gt;在&lt;code&gt;/etc/shadowsocks-libev/xxx.json&lt;/code&gt;下配置科学上网服务器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;server&quot;: &quot;&amp;lt;your-remote-server&amp;gt;&quot;,
    &quot;server_port&quot;: &amp;lt;your-remote-server-port&amp;gt;,
    &quot;timeout&quot;: 60,
    &quot;password&quot;: &quot;&amp;lt;your-password&amp;gt;&quot;,
    &quot;method&quot;: &quot;aes-256-cfb&quot;,
    &quot;local_address&quot;: &quot;0.0.0.0&quot;,
    &quot;local_port&quot;: 1080
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查默认端口占用，通常SS安装完成后会自动使用默认配置（&lt;code&gt;/etc/shadowsocks-libev/config.json&lt;/code&gt;）启动ss-server进程，因此&lt;code&gt;8388&lt;/code&gt;端口通常会被占用。
而我们在树莓派上使用SS仅仅是为了加速，因此不需要启动ss-server：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo netstat -nap | grep 8388
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止默认ss服务端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 检查ss默认系统服务
systemctl list-unit-files # 或 systemctl -a
systemctl status shadowsocks-libev.service 
sudo systemctl stop shadowsocks-libev.service 
sudo systemctl disable shadowsocks-libev.service 
systemctl status shadowsocks-libev.service 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动ss客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 后台
nohup ss-local -v -c /etc/shadowsocks-libev/xxx.json &amp;amp;
# 或前台
ss-local -v -s &amp;lt;your-remote-server&amp;gt; -p &amp;lt;your-remote-server-port&amp;gt; -l 1080 -k &amp;lt;your-password&amp;gt; -m aes-256-cfb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试ss客户端的socks5是否正常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -I --socks5 localhost:1080 google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止ss客户端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo kill -TERM 5914 &amp;amp;&amp;amp; rm nohup.out
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编写快捷脚本：&lt;/h3&gt;
&lt;p&gt;启动脚本&lt;code&gt;ss-s&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo touch /usr/local/bin/ss-s
sudo chmod +x /usr/local/bin/ss-s
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

nohup ss-local -v -c /etc/shadowsocks-libev/xxx.json &amp;gt; ~/nohup.log 2&amp;gt;&amp;amp;1 &amp;amp;
echo $! &amp;gt; ~/ss-pid.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;停止&lt;code&gt;ss-t&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo touch /usr/local/bin/ss-t
sudo chmod +x /usr/local/bin/ss-t
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

kill -s TERM `cat ~/ss-pid.txt`
rm ~/ss-pid.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.4 proxychains-ng&lt;/h2&gt;
&lt;p&gt;ProxyChains是一个使用方便的代理工具，它只会代理指定的程序，下载：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/rofl0r/proxychains-ng
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd proxychains-ng/
./configure --prefix=/usr --sysconfdir=/etc
make 
sudo make install
sudo make install-config
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置&lt;code&gt;/etc/proxychains.conf&lt;/code&gt;，将最后一行修改为shadowsocks的端口：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;socks5 127.0.0.1 1080
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试&lt;code&gt;wget&lt;/code&gt;，返回网页源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxychains4 wget -qO- https://www.google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;2.5 http代理&lt;/h2&gt;
&lt;p&gt;有些程序并不像&lt;code&gt;curl&lt;/code&gt;那样能够直接支持socks5代理，有时，按照情况需要配置http代理。
安装&lt;code&gt;privoxy&lt;/code&gt;，开启全局http代理，其默认代理地址为&lt;code&gt;http://127.0.0.1:8118&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install privoxy
sudo -s
echo &apos;forward-socks5 / 127.0.0.1:1080 .&apos; &amp;gt;&amp;gt;/etc/privoxy/config
^D
systemctl status privoxy.service
#systemctl enable privoxy.service
#systemctl start privoxy.service
#systemctl restart privoxy.service
systemctl status privoxy.service 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试http代理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget -qO- -e use_proxy=yes -e http_proxy=127.0.0.1:8118 http://google.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;conda&lt;/code&gt;可能存在科学安装问题，因此可以临时设置代理环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export HTTP_PROXY=&quot;http://127.0.0.1:8118&quot; &amp;amp;&amp;amp; conda install -y scikit-image
unset HTTP_PROXY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是配置&lt;code&gt;.condarc&lt;/code&gt;的代理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;proxy_servers:
  http: http://127.0.0.1:8118
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3 云科学（要钱）&lt;/h1&gt;
&lt;h2&gt;3.1 在AWS上部署Streisand&lt;/h2&gt;
&lt;p&gt;在aws中新建用户&lt;code&gt;administrator&lt;/code&gt;，放入新建组&lt;code&gt;Administrators&lt;/code&gt;，附加超级管理员权限&lt;code&gt;AdministratorAccess&lt;/code&gt;。
新建访问秘钥并下载，记住key id与key后删除秘钥。&lt;/p&gt;
&lt;p&gt;aws启动Ubuntu 16.04实例，在安装pip时出现&lt;code&gt;E: Unable to locate package python-pip&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install software-properties-common
sudo apt-add-repository universe
sudo apt-get update
sudo apt-get install python-pip
pip install --upgrade pip
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软件包升级：&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; time sudo apt-get upgrade &amp;amp;&amp;amp; time sudo apt-get dist-upgrade&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;查看系统地区设置&lt;code&gt;locale&lt;/code&gt;，添加中文支持&lt;code&gt;sudo locale-gen zh_CN.UTF-8&lt;/code&gt;
设置空项的值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo update-locale LANGUAGE=&quot;en_US.UTF-8&quot;
sudo update-locale LC_ALL=&quot;en_US.UTF-8&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在树莓派上安装Streisand的准备工作：
生成秘钥对：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh-keygen
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装git和vim：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install git
sudo apt-get install vim
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装python编译依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install python-paramiko python-pip python-pycurl python-dev build-essential
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装ansible相关依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pip install ansible markupsafe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装aws相关依赖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pip install boto boto3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载Streisand源码并安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/StreisandEffect/streisand.git &amp;amp;&amp;amp; cd streisand
./streisand
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按要求填写aws的相关信息，等很久…部署完成…&lt;/p&gt;
&lt;p&gt;拷贝generated-docs到一个能看HTML网页的机器上（我的树莓派是raspbian lite版本）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scp -r pi@&amp;lt;raspberrypi-ip&amp;gt;:streisand ~/Downloads
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照网页提示、按需求（iOS、Linux、Android等）一步一步操作。&lt;/p&gt;
&lt;p&gt;在树莓派上，按streisand生成的generated-docs中的提示信息，填写shadowsocks的配置文件&lt;code&gt;/etc/shadowsocks-libev/aws.json&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;server&quot;:&quot;&amp;lt;your-server-ip&amp;gt;&quot;,
    &quot;server_port&quot;:&amp;lt;your-server-port&amp;gt;,
    &quot;timeout&quot;:60,
    &quot;password&quot;:&quot;&amp;lt;your-password&amp;gt;&quot;,
    &quot;method&quot;:&quot;chacha20-ietf-poly1305&quot;,
    &quot;local_address&quot;:&quot;0.0.0.0&quot;,
    &quot;local_port&quot;:1080
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3.2 本机编译科学工具&lt;/h1&gt;
&lt;p&gt;由于树莓派上直接安装的shadowsocks不支持&lt;code&gt;chacha20-ietf-poly1305&lt;/code&gt;加密算法，因此，需要从源码编译shadowsocks：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Installation of basic build dependencies
sudo apt-get install --no-install-recommends gettext build-essential autoconf libtool libpcre3-dev asciidoc xmlto libev-dev libc-ares-dev automake libmbedtls-dev libsodium-dev

# Installation of Libsodium
export LIBSODIUM_VER=1.0.16
wget https://download.libsodium.org/libsodium/releases/libsodium-$LIBSODIUM_VER.tar.gz
tar xvf libsodium-$LIBSODIUM_VER.tar.gz
pushd libsodium-$LIBSODIUM_VER
./configure --prefix=/usr &amp;amp;&amp;amp; make
sudo make install
popd
sudo ldconfig

# Installation of MbedTLS
export MBEDTLS_VER=2.12.0
wget https://tls.mbed.org/download/mbedtls-$MBEDTLS_VER-gpl.tgz
tar xvf mbedtls-$MBEDTLS_VER-gpl.tgz
pushd mbedtls-$MBEDTLS_VER
make SHARED=1 CFLAGS=-fPIC
sudo make DESTDIR=/usr install
popd
sudo ldconfig

# Start building
git submodule update --init --recursive
# ref: https://github.com/shadowsocks/shadowsocks-libev/issues/1177#issuecomment-319032195
./autogen.sh &amp;amp;&amp;amp; ./configure --with-sodium-include=/usr/include --with-sodium-lib=/usr/local/lib --with-mbedtls-include=/usr/include --with-mbedtls-lib=/usr/lib &amp;amp;&amp;amp; make
sudo make install
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在使用&lt;code&gt;ss-local --help&lt;/code&gt;会发现加密算法支持&lt;code&gt;chacha20-ietf-poly1305&lt;/code&gt;。
使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动aws
nohup ss-local -v -c /etc/shadowsocks-libev/aws.json&amp;amp;
# 测试连接
curl -I --socks5 localhost:1080 google.com
# 返回301，成功
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;关于要钱&lt;/strong&gt;：最便宜的EC2即可，一个月不到8刀吧……&lt;/p&gt;
</content:encoded></item><item><title>Redhat Linux初体验（3）</title><link>https://zlotus.github.io/2018/08/03/redhat-3/</link><guid isPermaLink="true">https://zlotus.github.io/2018/08/03/redhat-3/</guid><description>Redhat系Linux基础学习</description><pubDate>Fri, 03 Aug 2018 11:22:47 GMT</pubDate><content:encoded>&lt;p&gt;这三周都被拐卖到成都培训RHCE，讲真，还是当学生好啊~哈哈哈~把学习笔记搬上来，关键时刻能做个速查~&lt;/p&gt;
&lt;p&gt;献上第三周的笔记。&lt;/p&gt;
&lt;h1&gt;2018-07-30&lt;/h1&gt;
&lt;h2&gt;防火墙&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通常使用Firewall Configuration图形界面进行配置更方便；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ports、Sources等普通规则（单规则）添加时应注意，这些规则的生效是取并集；Rich Rules（富语言规则）用来配置复杂规则，常用语配置交集过滤；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;iptables等旧命令使用直接语言规则，在Redhat 7中已经不提倡使用，具体参考&lt;code&gt;man 1 firewall-cmd&lt;/code&gt;和&lt;code&gt;man 5 firewalld.direct&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Redhat 7推荐使用富语言规则，参考&lt;code&gt;man 5 firewalld.richlanguage&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看服务&lt;code&gt;firewall-cmd --get-services&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看区域已经添加或生效的策略&lt;code&gt;firewall-cmd --list-all&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看富语言规则&lt;code&gt;firewall-cmd --list-rich-rules&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实验：仅允许desktop4访问server4的web服务，要求日志具有前缀&lt;code&gt;NEW HTTP &lt;/code&gt;，日志限速每秒3条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装httpd服务：&lt;code&gt;yum -y install httpd&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;添加内容：&lt;code&gt;echo &apos;&amp;lt;h1&amp;gt;hello world&amp;lt;/h1&amp;gt;&apos; &amp;gt; index.html&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;启动服务：&lt;code&gt;systemctl start httpd.service &amp;amp;&amp;amp; systemctl enable httpd.service&lt;/code&gt;；查看服务&lt;code&gt;netstat -tnlp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;图形化界面添加：&lt;code&gt;Family: ipv4&lt;/code&gt;、&lt;code&gt;Element: service: http&lt;/code&gt;、&lt;code&gt;Action&lt;/code&gt;、&lt;code&gt;Source: 172.25.4.10/32&lt;/code&gt;、&lt;code&gt;Log: Prifix: NEW HTTP; With limit: 3/second&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看日志在系统挑日志：&lt;code&gt;tail /var/log/message&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;查看已生效策略：&lt;code&gt;firewall-cmd --list-all&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用命令行重新实验，重载配置：&lt;code&gt;firewall-cmd --reload&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;参考&lt;code&gt;man 5 firewalld.direct&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 运行时生效
firewall-cmd --add-rich-rule &apos;rule family=&quot;ipv4&quot; source address=&quot;172.25.4.10/32&quot; service name=&quot;http&quot; log prefix=&quot;NEW HTTP&quot; limit value=&quot;3/s&quot; accept&apos;
# 永久生效
firewall-cmd --add-rich-rule &apos;rule family=&quot;ipv4&quot; source address=&quot;172.25.4.10/32&quot; service name=&quot;http&quot; log prefix=&quot;NEW HTTP&quot; limit value=&quot;3/s&quot; accept&apos; --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;伪装功能&lt;code&gt;Masquerading&lt;/code&gt;用来改“发件人”信息（通常用于保护内部网络结构），端口转发&lt;code&gt;Port Forwarding&lt;/code&gt;用来改“收件人”信息；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;伪装，发送包时，内网使用&lt;code&gt;10.0.0.1-3...&lt;/code&gt;，伪装为&lt;code&gt;1.2.3.4:10001-3&lt;/code&gt;转发去外网；接受包时将&lt;code&gt;1.2.3.4:10001-3&lt;/code&gt;转换为内网IP&lt;code&gt;10.0.0.1-3&lt;/code&gt;；（NAT转换）&lt;/li&gt;
&lt;li&gt;富语言规则可以指定网段进行伪装，但规则伪装只能对所有包进行伪装，无法指定网段；&lt;/li&gt;
&lt;li&gt;端口转发，服务器使用私有IP，外部请求为&lt;code&gt;1.2.3.4:80&lt;/code&gt;，防火墙转发为&lt;code&gt;10.0.0.1:8080&lt;/code&gt;，通常用于保护内网服务安全；&lt;/li&gt;
&lt;li&gt;伪装不需要配置伪装前后的规则，防火墙自动转换；而端口转发需要配置转发前后的规则；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实验：服务器server4（172.25.4.11）是一个在防火墙desktop4（172.25.4.10）后面的运行web服务（8080/TCP）的服务器，配置过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;重置虚拟机：&lt;code&gt;rht-vmctl reset all&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;server4安装httpd：&lt;code&gt;yum install -y httpd&lt;/code&gt;；配置&lt;code&gt;vim /etc/httpd/conf/httpd.conf&lt;/code&gt;监听&lt;code&gt;8080&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;启动服务：&lt;code&gt;systemctl start httpd.service &amp;amp;&amp;amp; systemctl enable httpd.service&lt;/code&gt;；查看服务&lt;code&gt;netstat -tnlp&lt;/code&gt;，&lt;code&gt;8080&lt;/code&gt;启动；&lt;/li&gt;
&lt;li&gt;为server4添加允许来自desktop4的流量：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --add-rich-rule &apos;rule family=&quot;ipv4&quot; source address=&quot;172.25.4.10/32&quot; port port=&quot;8080&quot; portocol=&quot;tcp&quot; accept&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;为desktop4添加端口转发并开启伪装：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 开启伪装
firewall-cmd --add-masquerade
# 开启端口转发
firewall-cmd --add-rich-rule &apos;rule family=&quot;ipv4&quot; forward-port port=&quot;80&quot; protocol=&quot;tcp&quot; to-port=&quot;8080&quot; to-addr=&quot;172.25.0.11&quot;&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;从物理机访问防火墙desktop4的80端口，会转发至server4的8080端口；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;注意&lt;/strong&gt;：从desktop4直接访问desktop4的80端口是不可以的，牵扯到用户态到内核态的数据转换，只能通过外部（如物理机）访问desktop4进行测试，详情Google “&lt;strong&gt;iptables四表五链&lt;/strong&gt;”；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;SELinux端口控制&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;SELinux检查进程和端口的标签间的权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看进程标签：&lt;code&gt;ps Z 1757&lt;/code&gt;，即SELinux标签的源；&lt;/li&gt;
&lt;li&gt;查看80端口的标签：&lt;code&gt;semanage port -l | egrep&quot;\b80\b&quot;&lt;/code&gt;，即SELinux标签的目的；&lt;/li&gt;
&lt;li&gt;查看规则：&lt;code&gt;sesearch -A -s httpd_t -t http_port_t&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置SELinux端口权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改端口为8800：&lt;code&gt;vim /etc/httpd/conf/httpd.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;重启&lt;code&gt;systemctl restart httpd&lt;/code&gt;报错，具体情况查看&lt;code&gt;systemctl status -l httpd&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;为8800新增http访问的标签：&lt;code&gt;semanage port -a -t http_port_t -p tcp 8800&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;删除原来的标签：&lt;code&gt;semanage port -d -p tcp 8080&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改原来的标签：&lt;code&gt;semanage port -m -t ftp_port_t -p tcp 8800&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;注意：SELinux的默认标签是无法删除的，而对默认标签的修改也仅是新增一条相应的标签策略；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;DNS管理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;静态授权DNS位于&lt;code&gt;/etc/hosts&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DNS缓存&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DNS服务器位于&lt;code&gt;/etc/resolv.conf&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;解析过程为：先找DNS静态授权数据，再找本地缓存，最后远程DNS缓存服务器（查找条件转发器forward，最后查找rootDNS服务器)；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查找DNS：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示简要信息：&lt;code&gt;nslookup www.example.com&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;显示详细的查询过程过程（IPv4）：&lt;code&gt;host -vt A desktop4.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;显示详细的查询过程过程（IPv6）：&lt;code&gt;host -vt AAAA desktop4.example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;主DNS服务器可以修改数据；辅助DNS服务器不能直接修改，只能同步主DNS服务器，主/辅DNS是授权DNS，有负责的区域，即与跟域呈树形结构；缓存服务器缓存的是任意值，即二维表型结构；&lt;/li&gt;
&lt;li&gt;别名是由主机名指向主机名的记录，A、AAAA与别名都是存在正向DNS记录中；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;搭建缓存名称服务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装unbound：&lt;code&gt;yum install -y unbound&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;修改配置文件：&lt;code&gt;vim /etc/unbound/&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;interface: 172.25.4.11
access-control: 172.25.4.0/24 allow
domian-insecure: &quot;example.com&quot;
forward-zone:
        name: &quot;example.com&quot;
        forward-addr: 172.25.254.254
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;启动服务：&lt;code&gt;systemctl start unbound&lt;/code&gt;；查看服务启动情况：&lt;code&gt;netstat -tnlp&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;允许防火墙通过：&lt;code&gt;firewall-cmd --add-service=dns&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;检查配置文件语法错误：&lt;code&gt;unbound-checkconf&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;从desktop4上测试&lt;code&gt;dig @server4.example.com A server6.example.com&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;导出配置：&lt;code&gt;unbound-control dump_cache &amp;gt; dump.out&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;导入配置：&lt;code&gt;unbound-control load_cache &amp;lt; dump.out&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;清除缓存：&lt;code&gt;unbound-control flush server4.example.com&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;清楚域信息：&lt;code&gt;unbound-control flush_zone example.com&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;邮件服务&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;邮件是所有Linux自带的服务，因为&lt;code&gt;crond&lt;/code&gt;等服务都是依赖邮件服务的；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;发送邮件：&lt;code&gt;mail -s &apos;hello student&apos;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;空客户端：禁用本地邮件投递能力，将邮箱投递全部转发到指定服务器中；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置postfix邮件服务器：&lt;code&gt;/etc/postfix/mail.cf&lt;/code&gt;，可以使用&lt;code&gt;postconf&lt;/code&gt;工具，也可以直接编辑；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在desktop4上：重置服务器，&lt;code&gt;lab smtp-nullclient setup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改邮件服务器配置：&lt;code&gt;vim /etc/postfix/main.cf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;myorigin        = desktop4.example.com
inet_interfaces = loopback-only
# [空]
mydestination   = 
mynetworks      = 127.0.0.0/8 [::1]/128
relayhost       = smtp4.example.com
local_transport = error:&quot;local delivery disabled&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;日志排错在&lt;code&gt;/var/log/maillog&lt;/code&gt;，如果遇到服务报错或邮件未收到，可以使用日志排错；&lt;/li&gt;
&lt;li&gt;在server4上发送邮件测试：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;mail -s &apos;remote student&apos; student@desktop4.example.com
# 正文
hello remote
# ^D提交
EOT
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-7-31&lt;/h1&gt;
&lt;h2&gt;存储&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;DAS(direct attach storage)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;NAS(network attach storage):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unix-like nfs;&lt;/li&gt;
&lt;li&gt;Windows smb(cifs)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SAN(storage area network): SCSI&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;概念：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;node: iSCSI initiator或iSCSI target都是node；&lt;/li&gt;
&lt;li&gt;IQN(iSCSI Qualified Name)：全球唯一，类似链路层的MAC地址，以及网络层的IP地址；&lt;/li&gt;
&lt;li&gt;target：一个带有IQN地址的&lt;strong&gt;提供&lt;/strong&gt;存储资源的设备；&lt;/li&gt;
&lt;li&gt;initiator：一个带有IQN地址的&lt;strong&gt;获取&lt;/strong&gt;存储资源的设备；&lt;/li&gt;
&lt;li&gt;portal：入口，由IP地址和端口组成，用于建立连接；&lt;/li&gt;
&lt;li&gt;TPG(Target Portal Group)：一组入口，用于指定iSCSI目标监听；&lt;/li&gt;
&lt;li&gt;ACL(Access Control List)：用于限制initiator访问的IQN资源；&lt;/li&gt;
&lt;li&gt;discovery：基于入口或入口组的资源发现；&lt;/li&gt;
&lt;li&gt;login：initiator验证登录；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;desktop4上安装initiator&lt;code&gt;yum install -y iscsi-initiator-utils.i686&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;server4上安装target&lt;code&gt;yum install -y targetcli.noarch&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在server4上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看硬盘：&lt;code&gt;lsblk&lt;/code&gt;，划分分区：&lt;code&gt;fdisk /dev/vdb&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建pv：&lt;code&gt;pvcreate /dev/vdb1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建vg：&lt;code&gt;vgcreate ISCSI_vg /dev/vdb1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建lv：&lt;code&gt;lvcreate  -l 511 -n disk1_lv ISCSI_vg&lt;/code&gt;，把disk1_lv当做存储做实验；&lt;/li&gt;
&lt;li&gt;进入&lt;code&gt;targetcli&lt;/code&gt;交互模式：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 创建block1：
/backstores/block create block1 /dev/ISCSI_vg/disk1_lv
# 创建IQN：
/iscsi create iqn.2018-07.com.example:server4
# 创建ACL指定desktop4：
/iscsi/iqn.2018-07.com.example:server4/tpg1/acls create iqn.2018-07.com.example:desktop4
# 切换到/iscsi/iqn.2018-07.com.example:server4/tpg1&amp;gt;方便一些，创建LUN
luns/ create /backstores/block/block1
# 创建监听端口，使用默认端口3260：
portals/ create 172.25.4.11
# 查看所有配置
ls
# 退出自动保存（格式为json，在/etc/target/saveconfig.json）
exit
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;添加防火墙策略：&lt;code&gt;firewall-cmd --add-port=3260/tcp&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在desktop4上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;修改IQN：&lt;code&gt;vim /etc/iscsi/initiatorname.iscsi&lt;/code&gt;中的IQN为server4 ACL允许的名称&lt;code&gt;iqn.2018-07.com.example:desktop4&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;发现target：&lt;code&gt;iscsiadm --mode discoverydb --type sendtargets --portal 172.25.4.11:3260 --discover&lt;/code&gt;，发现后系统自动将目标记录在&lt;code&gt;/var/lib/iscsi/nodes/&lt;/code&gt;中；&lt;/li&gt;
&lt;li&gt;登录：&lt;code&gt;iscsiadm --mode node -targetname iqn.2018-07.com.example:server4 --portal 172.25.4.11:3260 --login&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;查看设备：&lt;code&gt;lsblk&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;登出：&lt;code&gt;iscsiadm --mode node -targetname iqn.2018-07.com.example:server4 --portal 172.25.4.11:3260 --logout&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;详情参考&lt;code&gt;man 8 iscsiadm&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;nfs共享&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在server4上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看包是否按照&lt;code&gt;rpm -q nfs-util&lt;/code&gt;；创建&lt;code&gt;/nfsshare&lt;/code&gt;目录；&lt;/li&gt;
&lt;li&gt;配置共享&lt;code&gt;vim /etc/exports&lt;/code&gt;：&lt;code&gt;/nfsshare    desktop4.example.com(rw)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;启用服务&lt;code&gt;systemctl status nfs-server &amp;amp;&amp;amp; systemctl enable nfs-server &amp;amp;&amp;amp; systemctl start nfs-server&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;修改共享配置后重载&lt;code&gt;exportfs -r&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;添加nfs防火墙策略&lt;code&gt;firewall-cmd --add-service=nfs --permanent &amp;amp;&amp;amp; firewall-cmd --add-service=nfs&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;添加rpc-bind防火墙策略（远端查看&lt;code&gt;showmount&lt;/code&gt;需要）&lt;code&gt;firewall-cmd --add-service=rpc-bind --permanent &amp;amp;&amp;amp; firewall-cmd --add-service=rpc-bind&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;添加mountd防火墙策略（远端查看&lt;code&gt;showmount&lt;/code&gt;需要）&lt;code&gt;firewall-cmd --add-service=mountd --permanent &amp;amp;&amp;amp; firewall-cmd --add-service=mountd&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;端口信息可以从&lt;code&gt;grep 2049 /etc/services&lt;/code&gt;中看到；&lt;/li&gt;
&lt;li&gt;查看共享目录&lt;code&gt;showmount -e server4&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在desktop4上：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新建挂载点&lt;code&gt;mkdir /mnt/nfsshare&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;挂载&lt;code&gt;mount server4:/nfsshare /mnt/nfsshare&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;永久挂载&lt;code&gt;echo &apos;server4:/nfsshare /mnt/nfsshare nfs defaults,_netdev 0 0&apos; &amp;gt;&amp;gt; /etc/fstab &amp;amp;&amp;amp; mount -a&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;共享权限问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若共享不可写，则检查model模型及SELinux；共享时虽然由root发起nfs-server进程，但是实际服务用户是一个名为&lt;code&gt;id nfsnobody&lt;/code&gt;的内置账号，因此需要在server4上授权&lt;code&gt;chmod o+w /nfsshare&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;在desktop4上强制修改SELinux标签：&lt;code&gt;mount -o context=&quot;system_u:object_r:public_content_rw_t:s0&quot; server4:/nfs /media&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;服务端的配置在&lt;code&gt;/etc/sysconfig/nfs&lt;/code&gt;配置&lt;code&gt;RPCNFSDARGS=&quot;-V 4.2&quot;&lt;/code&gt;，desktop4挂载时使用&lt;code&gt;mount -o v4.2 server4:/nfs /media&lt;/code&gt;，可以使客户端与服务端文件的SELinux一致；&lt;/li&gt;
&lt;li&gt;关于各软件的SELinux目录权限标签可以参考“软件名_selinux(8)”，如&lt;code&gt;man 8 nfsd\_selinux&lt;/code&gt;；公共目录读写通常使用&lt;code&gt;public_content_rw_t&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装环境：&lt;code&gt;lab nfskrb5 setup&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;在server4上，下载krb凭证&lt;code&gt;wget -O /etc/krb5.keytab http://classroom.example.com/pub/keytabs/server4.keytab&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新建文件夹&lt;code&gt;/securenfs&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;设置共享&lt;code&gt;/etc/exports&lt;/code&gt;写入&lt;code&gt;/securenfs desktop4.exmaple.com(rw,sec=krb5p)&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;修改nfs版本配置&lt;code&gt;/etc/sysconfig/nfs&lt;/code&gt;该为&lt;code&gt;RPCNFSDARGS=&quot;-V 4.2&quot;&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;防火墙添加服务：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --add-service=nfs
firewall-cmd --add-service=nfs --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;启动nfs服务：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable nfs-server
systemctl enable nfs-secure-server
systemctl start nfs-server
systemctl start nfs-secure-server
systemctl status nfs-server
systemctl status nfs-secure-server
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;在desktop4上，下载krb凭证&lt;code&gt;wget -O /etc/krb5.keytab http://classroom.example.com/pub/keytabs/desktop4.keytab&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新建挂载点&lt;code&gt;mkdir /mnt/sercureshare&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;启动服务&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable nfs-secure
systemctl start nfs-secure
systemctl status nfs-secure
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;挂载&lt;code&gt;mount -o sec=krb5p,v4.2 server4:/securenfs /mnt/secureshare&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;配置永久挂载：&lt;code&gt;server4:/securenfs /mnt/secureshare nfs4 defaults,_netdev,sec=krb5p,v4.2 0 0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;另外，如果挂载时遇到访问被拒绝的问题，可以使用&lt;code&gt;klist /etc/krb5.keytab&lt;/code&gt;查看证书问题；&lt;/li&gt;
&lt;li&gt;挂载选项中的&lt;code&gt;_netdev&lt;/code&gt;选项参见&lt;code&gt;man 8 mount&lt;/code&gt;，即网络设备启动过后再执行挂载；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-08-01&lt;/h1&gt;
&lt;h1&gt;nfs共享权限&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用krb认证后，若切换到认证用户身份（如ldapuser1），则权限已经从nfsnobody用户变为认证用户了（即ldapuser1）；共享的权限检查顺序为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享权限&lt;code&gt;vim /etc/exports&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;model权限&lt;code&gt;ll -d /securenfs&lt;/code&gt;，包括ACL权限；&lt;/li&gt;
&lt;li&gt;SELinux权限&lt;code&gt;ll -dZ /securenfs&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;权限试验：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在server4上创建文件：&lt;code&gt;echo &apos;hello world&apos; &amp;gt; /securenfs/testfile.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改SELinux标签：&lt;code&gt;chron -t public_content_t /securenfs/testfile.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改文件属主为ldap用户：&lt;code&gt;chown ldapuser0:ldapuser0 /securenfs/testfile.txt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;修改model权限：&lt;code&gt;chmod 644 /securenfs/testfile.txt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;在desktop4上查看共享权限：&lt;code&gt;ls -Z /mnt/shcureshare/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过认证切换用户为ldapuser4：&lt;code&gt;ssh ldapuser4@localhost&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;尝试写入失败：&lt;code&gt;echo &apos;i can&apos;t wirte&apos; &amp;gt;&amp;gt; /mnt/seucreshare/testfile.txt&lt;/code&gt;，文件对ldapuser4只读；&lt;/li&gt;
&lt;li&gt;通过认证切换用户为ldapuser0：&lt;code&gt;ssh ldapuser4@localhost&lt;/code&gt;；注意：不可以通过&lt;code&gt;su - ldapuser0&lt;/code&gt;，因为这样不会通过krb进行认证；&lt;/li&gt;
&lt;li&gt;此时可以写入：&lt;code&gt;echo &apos;i can&apos;t wirte&apos; &amp;gt;&amp;gt; /mnt/seucreshare/testfile.txt&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;nfs与smb的路径对比：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;nfs：&lt;code&gt;hostname:/pathname&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;cifs(smb)：&lt;code&gt;//hostname/sharename&lt;/code&gt;，其中的sharename在&lt;code&gt;/etc/samba/smb.conf&lt;/code&gt;中配置；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;smb共享&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;smb(server message block)是一个标准的文件共享协议，用户Windows系统；smb文件服务器可以配置为&lt;strong&gt;工作组&lt;/strong&gt;模式或&lt;strong&gt;域&lt;/strong&gt;模式；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工作组绝对平等，每台电脑管理自己的本地用户及密码，不适于大规模管理；&lt;/li&gt;
&lt;li&gt;域成员服从域控制器，用户通过域控制器认证；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建smb共享：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在server4上安装及查看&lt;code&gt;yum install -y samba&lt;/code&gt;，&lt;code&gt;rpm -ql samba&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;smb安装完成后提供两个服务：nmb提供netbios服务，解析共享时的域名解析；smb即共享服务；&lt;/li&gt;
&lt;li&gt;配置&lt;code&gt;vim /etc/samba/smb.conf&lt;/code&gt;，参考&lt;code&gt;man 4 samba&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;workgroup = WORKGROUP
hosts allow = 
[yinhe]
path = /smbshare/
# 用户,@组
write list = smbuser4,@smb
valid users = smbuser4
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;testparm&lt;/code&gt;检查配置文件语法错误；&lt;/li&gt;
&lt;li&gt;启动服务&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable smb nmb
systemctl start smb nmb
# 检查服务：139、445端口已经启动
netstat -tnlp
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;配置防火墙：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --add-service=samba
firewall-cmd --add-service=samba --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;在desktop4上新建非交互用户，为smbuser4指定非交互shell：&lt;code&gt;useradd -s /sbin/nologin smbuser4&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;设置smb格式密码：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 搜索提供该命令的包
yum provides smbpasswd
# 安装该包
yum install -y samba-client
# 为用户设置密码
smbpasswd -a smbuser4
# 查看该用户
pdbedit -L
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;尝试挂载：&lt;code&gt;mount //server4/yinhe /media&lt;/code&gt;报错，查看日志&lt;code&gt;tail /var/log/message&lt;/code&gt;，发现需要cifs文件系统；&lt;/li&gt;
&lt;li&gt;安装cifs文件系统支持&lt;code&gt;yum install -y cifs-utils&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;指定用户挂载：&lt;code&gt;mount -o username=smbuser4,password=redhat //server4/yinhe /media/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;尝试访问被拒绝，在server4中添加SELinux读权限：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 查看SELinux标签
ll -dZ /smbshare
# 添加读标签，测试后可以访问，注意，chcon只用于临时调试，要永久修改策略应使用semanage
chcon -t public_content_t /smbshare/
# 该为写标签
chcon -t public_content_rw_t /smbshare/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为共享用户smbuser4添加ACL：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 查看ACL
getfacl /smbshare
# 为smbuser4设置读写ACL
setfacl -m smbuser4:rwx /smbshare/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;尝试写发现写被拒绝，修改SELinux策略：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 检查smb权限
getsebool -a | grep smb
# 设置写权限
setsebool smbd_anon_write on
# 永久生效
setsebool -P smbd_anon_write on
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;多用户挂载&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;切换student写被拒绝，添加model权限：&lt;code&gt;chmod o+w /smbshare/&lt;/code&gt;，我们发现student创建的属主文件也是smbuser4（即文件属于挂载用户）；&lt;/li&gt;
&lt;li&gt;在server4上初始化实验环境&lt;code&gt;lab smbmultiuser setup&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;可以看到实验创建了&lt;code&gt;/smbshare&lt;/code&gt;协作目录&lt;code&gt;drwxrwsr-x.&lt;/code&gt;，并新增了两个账号&lt;code&gt;brain:1001:1001(marketing)&lt;/code&gt;和&lt;code&gt;rob:1002:1002&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;在desktop4上初始化实验环境&lt;code&gt;lab smbmultiuser setup&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;安装cifs：&lt;code&gt;yum install -y cifs-utils&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建认证文件&lt;code&gt;vim /root/multiuser&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;username=brain
password=redhat
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;配置永久挂载&lt;code&gt;vim /etc/fstab&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;//server4/smbshare /mnt/multiuser cifs defaults,_netdev,multiuser,credentials=/root/multiuser 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;测试切换brian&lt;code&gt;su - brian&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 创建文件被拒绝
touch /mnt/multiuser/testfile
# 用户名密码认证
cifscreds add server4
# 具有读写权限
touch test
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;测试切换rob&lt;code&gt;su - rob&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 创建文件被拒绝
touch /mnt/multiuser/testfile
# 用户名密码认证
cifscreds add server4
# 具有只读权限
touch test
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-08-02&lt;/h1&gt;
&lt;h2&gt;数据库&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装MariaDB：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在server4上安装服务端，以组安装比较方便：&lt;code&gt;yum groups install MariaDB\ Database\ Server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;yum history list&lt;/code&gt;查看安装历史，使用&lt;code&gt;yum history info 2&lt;/code&gt;查看组中安装的包详情&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;rpm -ql mariadb-server | grep .service&lt;/code&gt;查看安装的服务；&lt;/li&gt;
&lt;li&gt;启动服务：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;systemctl enable mariadb.service
systemctl start mariadb.service
# 确认默认端口3306
netstat -tnlp
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;添加防火墙：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --add-service=mysql
firewall-cmd --add-service=mysql --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;在desktop4上安装服务端，以组安装比较方便：&lt;code&gt;yum groups install MariaDB\ Database\ Client&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;运行&lt;code&gt;yum history list&lt;/code&gt;查看安装历史，运行&lt;code&gt;yum history info 2&lt;/code&gt;查看组中安装的包详情&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;登录数据库&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;本地匿名登录，使用mysql客户端：&lt;code&gt;mysql&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;SHOW DATABASES;
USE mysql;
SHOW TABLES;
DESC user;
SELECT user, host, password FROM user;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;运行安全脚本禁用匿名用户、远程登录等功能：&lt;code&gt;mysql_secure_installation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;使用密码本地登录：&lt;code&gt;mysql -u root -predhat -h localhost&lt;/code&gt;，注意&lt;code&gt;-p&lt;/code&gt;选项后接密码无空格；&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;USE mysql;
SELECT user, host, password FROM user;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vim /etc/my.cnf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 默认配置指向套接字文件，供同一个电脑的进程间通信使用：
socket=/var/lib/mysql/mysql.sock
# bind-address，在所以配置文件中只允许一次，监听本地IP 172.25.0.11：
bind-address=172.25.0.11
# 禁用网络，仅使用本地套接字文件(以localhost连接会被自动转为本地套接字，而以server4连接则会被禁用)：
skip-network=1
# 修改配置使能够远程访问，需要指向
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;子配置位于&lt;code&gt;ll /etc/my.cnf.d&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;日志、数据文件、套接字等位于&lt;code&gt;ll /var/lib/mysql&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SQL：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DDL数据定义语言：&lt;code&gt;create, show, drop, alter&lt;/code&gt;等，对象为容器（如表、视图）；&lt;/li&gt;
&lt;li&gt;DML数据管理语言：&lt;code&gt;insert, delete, select, update&lt;/code&gt;等，对象为数据记录；&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- DDL与DML：
SHOW HELP;
-- %通配多位
SHOW TABLES FROM mysql LIKE &apos;%zone%&apos;;
-- _通配一位
SHOW TABLES FROM mysql LIKE &apos;_i%&apos;;
-- 创建库
CREATE DATABASE inventory;
USE inventory;
-- 创建表
CREATE TABLE regions (
    region_id int(8),
    region_name varchar(20),
    country_name varchar(20)
);
-- 查看表结构
DESC regions;
-- 插入记录
INSERT INTO regions values (1, &apos;Asia&apos;, &apos;China&apos;);
INSERT INTO regions(region_id, country_name) value (&apos;Asia&apos;, &apos;Japan&apos;);
UPDATE regions SET country_name=&apos;CN&apos; WHERE region_id=1;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;DCL数据控制语言：&lt;code&gt;grant, revoke&lt;/code&gt;等；&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;-- 新建用户
CREATE USER &apos;yinhe&apos;@&apos;desktop4.example.com&apos; identified by &apos;redhat&apos;;
-- 若使用主机名登录有问题，可以尝试使用IP地址登录
CREATE USER &apos;yinhe&apos;@&apos;172.25.4.10&apos; identified by &apos;redhat&apos;;
-- 授予权限
GRANT SELECT, UPDATE, DELETE, INSERT ON inventory.regions TO &apos;yinhe&apos;@&apos;desktop4.example.com&apos;;
-- 刷新内存，赋权后建议刷新
flush privileges;
-- 回收权限
REVOKE UPDATE, INSERT, DELETE ON inventory.regions FROM &apos;yinhe&apos;@&apos;desktop4.example.com&apos;;
-- 查看用户权限
SHOW GRANTS FOR &apos;yinhe&apos;@desktop4.example.com&apos;
-- 用户授权支持SQL用通配符
mobius@&apos;localhost&apos;
mobius@&apos;192.168.1.5&apos;
mobius@&apos;192.168.1.%&apos;    
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数据备份&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;按业务分为冷备（所有业务下线）、温备（业务只读）、热备（所有业务在线）；&lt;/li&gt;
&lt;li&gt;备份方式分为逻辑备份（通过SQL导出记录，可以不下线，低效、高可移植）、物理备份（复制数据目录，如&lt;code&gt;/var/lib/mysql&lt;/code&gt;，需要下线，高效、低可移植）；&lt;/li&gt;
&lt;li&gt;逻辑备份：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 备份
mysqldump -uroot -predhat inventory &amp;gt; ~/dump.out
# 还原
mysql -uroot -predhat abc &amp;lt; /root/dump.out
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;物理备份：&lt;code&gt;mysqlhotcopy&lt;/code&gt;是mysql提供的冷/温备工具，或直接给逻辑卷做快照（lvm snapshots，cow写前复制，所以建议数据文件规划在逻辑卷上）；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;网页服务&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;code&gt;yum install -y httpd&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查看配置文件&lt;code&gt;rpm -qc httpd&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;日志默认存放在&lt;code&gt;/var/log/httpd&lt;/code&gt;（同时也会写入系统日志&lt;code&gt;/var/log/message&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;内容默认存放在&lt;code&gt;/var/www/html&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;httpd的详细帮助文档，安装&lt;code&gt;yum install -y httpd-manual&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实验，使用&lt;code&gt;/srv/www4.example.com/www/&lt;/code&gt;为www4.example.com提供内容，使用&lt;code&gt;/srv/default/www/&lt;/code&gt;为其他域名提供内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SELinux策略已经为&lt;code&gt;/srv/*/www&lt;/code&gt;目录授予权限，若遇到403，使用&lt;code&gt;restorecon -vvvR /srv/&lt;/code&gt;（v越多显示的过程越详细），使用&lt;code&gt;ll -RZ /srv/&lt;/code&gt;查看，发现SELinux已重置为&lt;code&gt;httpd_sys_content_t&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;虚拟主机配置可以参考&lt;code&gt;/usr/share/doc/httpd-2.4.6/httpd-vhosts.conf&lt;/code&gt;，复制到&lt;code&gt;/etc/httpd/conf.d/&lt;/code&gt;下，分屏编辑&lt;code&gt;vim -o {default,www4}.conf&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;default配置：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;VirtualHost *:80&amp;gt;
    DocumentRoot &quot;/srv/default/www/&quot;
&amp;lt;/VirtualHost&amp;gt;
&amp;lt;Directory &quot;/srv/default/www/&quot;&amp;gt;
    AllowOverride None
    Require all granted
&amp;lt;/Directory&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;www4配置：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;VirtualHost *:80&amp;gt;
    DocumentRoot &quot;/srv/www4.example.com/www/&quot;
    ServerName www4.example.com
    ServerAlias www4
&amp;lt;/VirtualHost&amp;gt;
&amp;lt;Directory &quot;/srv/www4.example.com/www/&quot;&amp;gt;
    AllowOverride None
    Require all granted
    # Allow,Deny是老版本的访问控制命令，Require是新版本的命令
    # Order allow,deny
    # Allow from all
    # Deny from 172.25.4.10
    # 多条Require放入Require容器：&amp;lt;RequireAny&amp;gt;、&amp;lt;RequireAl&amp;gt;
    # &amp;lt;RequireAny&amp;gt;
    #     Require all granted
    #     Require not ip 172.25.0.10
    # &amp;lt;/RequireAny&amp;gt;
&amp;lt;/Directory&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;若有欢迎页面，重命名掉&lt;code&gt;/etc/httpd/conf.d/welcome.conf&lt;/code&gt;，注释掉主配置&lt;code&gt;Options Indexes FollowSymLinks&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实验，https（TLS）配置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生成秘钥对：&lt;code&gt;openssl genrsa 1024 &amp;gt; /root/pair.key&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;生成新签名请求：&lt;code&gt;openssl req -new -key /root/pair.key -out /root/server.csr&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;安装SSL支持：&lt;code&gt;yum install mod_ssl.x86_64&lt;/code&gt;，查看安装文件&lt;code&gt;rpm -ql mod_ssl&lt;/code&gt;，主配置文件为&lt;code&gt;/etc/httpd/conf.d/ssl.conf&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;添加防火墙：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --add-service=https
firewall-cmd --add-service=https --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;备份配置&lt;code&gt;mv /etc/httpd/conf.d/ssl.conf{,.bak}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Listen 443 https
&amp;lt;VirtualHost *:443&amp;gt;
    SSLEngine on
    SSLProtocol all -SSLv2 -SSLv3
    SSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5
    SSLHonorCipherOrder on
    ServerName www4.example.com
    DocumentRoot /srv/ssl/www/
    SSLCertificateFile
    SSLCertificateKeyFile
    SSLCACertificateFile /etc/pki/tls/certs/ca.crt
    &amp;lt;Directory &quot;/srv/ssl/www/&quot;&amp;gt;
        Require all granted
    &amp;lt;/Directory&amp;gt;
&amp;lt;/VirtualHost&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;重启服务：&lt;code&gt;systemctl restart httpd.server&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实验，部署python网络应用：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装wsgi插件：&lt;code&gt;yum install -y mod_wsgi.x86_64&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;webapp.wsgi&lt;/code&gt;的SELinux标签：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 永久改标签
semanage fcontext -a -t &quot;httpd_sys_content_t&quot; &quot;/srv/wsgi/www(/.*)?&quot;
# 刷新标签
restorecon -vvvR /srv/wsgi/www/
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;修改端口的SELinux标签：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 查看9981端口标签
semanage port -l | grep 9981
# 查看一般网页端口的标签
semanage port -l | grep 80
# 永久改标签
semanage port -a -t http_port_t -p tcp 9981
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;修改&lt;code&gt;vim wsgi.conf&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;Listen 9981
&amp;lt;VirtualHost *:9981&amp;gt;
    DocumentRoot &quot;/srv/wsgi/www/&quot;
    ServerName webapp4.example.com
    WSGIScriptAlias / /srv/wsgi/www/webapp.wsgi
&amp;lt;/VirtualHost&amp;gt;
&amp;lt;Directory &quot;/srv/wsgi/www/&quot;&amp;gt;
    AllowOverride None
    Require all granted
&amp;lt;/Directory&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;添加防火墙：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;firewall-cmd --add-port=9981/tcp
firewall-cmd --add-port=9981/tcp --permanent
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-08-03&lt;/h1&gt;
&lt;h1&gt;Shell脚本&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;脚本文件首行可以写入魔术字：&lt;code&gt;#!/usr/bin/bash&lt;/code&gt;，用于指定脚本执行的程序；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用常量创建用户：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建用户：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;adding user1&quot;
useradd user1 &amp;amp; &amp;gt; /dev/null
echo &quot;setting password for user1&quot;
echo &quot;redhat&quot; | passwd --stdin user1
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;执行脚本：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 将脚本读入bash子进程执行，ps j能够看到多一个bash子进程
bash createusers
# 将脚本读入当前进程运行执行
source createusers
# .等效于source
. createusers
# 直接运行
chmod a+x createusers
/root/createusers
./createusers
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;which&lt;/code&gt;路径查找的顺序：&lt;code&gt;alias、hash、$PATH&lt;/code&gt;中具有x权限的文件，设置别名：&lt;code&gt;alias cdnet=&apos;cd /etc/sysconfig/network-scripts/&apos;&lt;/code&gt;；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;which&lt;/code&gt;默认查找PATH，找到立刻返回，&lt;code&gt;which -a&lt;/code&gt;表示查找PATH中的所有路径；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;whereis&lt;/code&gt;也是路径查找，包man手册；&lt;code&gt;whatis&lt;/code&gt;专用于查找man手册，匹配名称，&lt;code&gt;man -k&lt;/code&gt;还匹配摘要内容；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;$&lt;/code&gt;用于变量调用，其他如&lt;code&gt;*, @, #&lt;/code&gt;等特殊符号参见&lt;code&gt;man 1 bash&lt;/code&gt;中“Special Parameters”；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置环境变量：&lt;code&gt;export user=user2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;变量默认以字符存储，使用&lt;code&gt;[]&lt;/code&gt;转换数字：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;a=1
b=2
echo $[$a+$b]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;let&lt;/code&gt;数字赋值：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;let c=$a+$b
echo $c
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;算数表达式：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;echo $[1+1*2]
echo $[(1+1)*2]
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;退出码&lt;code&gt;0&lt;/code&gt;表示正常，非&lt;code&gt;0&lt;/code&gt;值各程序定义不同，使用&lt;code&gt;exit 255&lt;/code&gt;表示退出码；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;条件判断：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数字比较：&lt;code&gt;[ &quot;$a&quot; -eq &quot;$b&quot; ] &amp;amp;&amp;amp; echo true || echo false&lt;/code&gt;，注意：中括号前后有空格；&lt;/li&gt;
&lt;li&gt;字符串比较：&lt;code&gt;[ &quot;$a&quot; == &quot;$b&quot; ] &amp;amp;&amp;amp; echo true || echo false&lt;/code&gt;，注意：中括号前后有空格；&lt;/li&gt;
&lt;li&gt;文件相关信息的条件判断参见&lt;code&gt;man 1 bash&lt;/code&gt;的“Conditional Expressions”；&lt;/li&gt;
&lt;li&gt;判断语句if，参见&lt;code&gt;help if&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;if xxx ; 
    then ... ;
elif ... ;
    then ... ;
else ... ;
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;判断语句case，参见&lt;code&gt;help case&lt;/code&gt;，实验：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
case $* in
    redhat)
        echo fedora ;;
    fedora)
        echo redhat ;;
    *)
        echo &quot;/root/foo.sh redhat|fedora&quot;
esac
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;添加运行权限&lt;code&gt;chmod a+x foo.sh&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;循环语句：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;for语句：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;for in {1..100};
    do echo $i; 
done
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;判断PC是否在线：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
for i in {1..20}
    do ping -c1 172.25.254.$i &amp;gt; /dev/null
    if [ $? -eq 0 ] 
        then echo pc${i}:online
        let sumon++
    else
        echo pc${i}:online
        let sumoff++
    if
done
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;while语句：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;while: xxx
    do ...;
done
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;表格处理：&lt;code&gt;grep w /etc/passwd | cut -d: -f 1,7&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;实验，用到的文件newusers每行形为&lt;code&gt;Cassy:Rundle:60:1&lt;/code&gt;：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
if [ $# -ne 1 ]
then
    echo &apos;Usage: /root/batchusers filename&apos;
    exit 256
elif [ -f $1 ]
then
    for username in `cut -d: -f1 $1`
    do
        echo $username
        useradd -s /bin/false $username
    done
else
    echo &apos;input file not found&apos;
    exit 128
fi
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;/2018/07/21/redhat-1/&quot;&gt;Redhat Linux初体验（1）&lt;/a&gt;
&lt;a href=&quot;/2018/07/28/redhat-2/&quot;&gt;Redhat Linux初体验（2）&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;结语&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;特别感谢&lt;strong&gt;李老师&lt;/strong&gt;在授课之余，带我刷猪肘饭和骨汤面；&lt;/li&gt;
&lt;li&gt;感谢成都天气控制部门一直将盛夏的气温保持在25℃左右，湿度也刚刚好，我现在知道成都妹纸为啥皮肤好了[捂脸]；&lt;/li&gt;
&lt;li&gt;感谢金开服务公寓的免费撸铁房；&lt;/li&gt;
&lt;li&gt;感谢东哥拐我来成都培训[捂脸]。&lt;/li&gt;
&lt;li&gt;特此批评西安的天气控制部门，连续的40℃还说自己是啥线城市？&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Redhat Linux初体验（2）</title><link>https://zlotus.github.io/2018/07/28/redhat-2/</link><guid isPermaLink="true">https://zlotus.github.io/2018/07/28/redhat-2/</guid><description>Redhat系Linux基础学习</description><pubDate>Sat, 28 Jul 2018 11:22:47 GMT</pubDate><content:encoded>&lt;p&gt;这三周都被拐卖到成都培训RHCE，讲真，还是当学生好啊~哈哈哈~把学习笔记搬上来，关键时刻能做个速查~&lt;/p&gt;
&lt;p&gt;献上第二周的笔记。&lt;/p&gt;
&lt;h1&gt;2018-07-23&lt;/h1&gt;
&lt;h2&gt;日志&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;syslog是老日志系统（文件型）、journal是新日志系统（内存型，可以配置保持硬盘，持久化）；日志默认保存在&lt;code&gt;/var/log&lt;/code&gt;下；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;除了认证、邮件、周期性作业、纯调试相关日志等类型，大多数日志都保存在&lt;code&gt;/var/log/messages&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;启动日志保存在&lt;code&gt;/var/log/boot.log&lt;/code&gt;，成功登录日志保存在&lt;code&gt;/var/log/utmp&lt;/code&gt;和&lt;code&gt;/var/log/wtmp&lt;/code&gt;，对应&lt;code&gt;last&lt;/code&gt;；失败登录保存在&lt;code&gt;/var/log/btmp&lt;/code&gt;，对应&lt;code&gt;lastb&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;syslog日志系统包含很多设备，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;认证设备：LOG_AUTH，放在&lt;code&gt;/var/log/secure&lt;/code&gt;中；&lt;/li&gt;
&lt;li&gt;邮件设备：LOG_MAIL，放在&lt;code&gt;/var/log/maillog&lt;/code&gt;中；&lt;/li&gt;
&lt;li&gt;周期性作业：LOG_CRON，放在&lt;code&gt;/var/log/cron&lt;/code&gt;中；&lt;/li&gt;
&lt;li&gt;详见&lt;code&gt;man 3 syslog&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;syslog日志系统分优先级，详见&lt;code&gt;man 3 syslog&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;syslog日志配置详见&lt;code&gt;man 5 rsyslog.conf&lt;/code&gt;；日志系统采用异步I/O，以提高硬盘性能；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;/etc/rsyslog.conf&lt;/code&gt;为syslog的主配置文件，通常，我们通过在&lt;code&gt;/etc/rsyslog.d&lt;/code&gt;中新建&lt;code&gt;.conf&lt;/code&gt;结尾的文件来配置日志，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新建&lt;code&gt;/etc/rsyslog.d/debug.conf&lt;/code&gt;，新增一行&lt;code&gt;*.debug /var/log/messages-debug&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;重启服务：&lt;code&gt;systemctl restart rsyslog.service&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;跟踪日志文件：&lt;code&gt;tail -f /var/log/messages-debug&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;产生日志信息：&lt;code&gt;logger -p user.debug &quot;Debug Message Test&quot;&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rsyslog&lt;/code&gt;中r代表remote，所以可以将日子保存在远程主机上，在&lt;code&gt;man 5 rsyslog.conf&lt;/code&gt;中有详细信息，支持包括SNMP、MYSQL等模块在内的远程传输；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;journalctl&lt;/code&gt;查看journal的日志，详见&lt;code&gt;man 5 journalctl&lt;/code&gt;，常用的功能有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-n&lt;/code&gt;显示最近的日志；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt;显示小于等于指定级别的日志；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_PID=&lt;/code&gt;显示指定进程的日志；（双击&lt;code&gt;TAB&lt;/code&gt;自动补全现实）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_UID=&lt;/code&gt;显示指定用户的日志；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--since 09:30:00&lt;/code&gt;显示指定时间后的日志；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--until 10:00:00&lt;/code&gt;显示指定时间前的日志；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;_SYSTEMD_UNIT=sshd.service&lt;/code&gt;显示指定服务的日志；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置journal，详见&lt;code&gt;man 5 journald.conf&lt;/code&gt;，配置文件为&lt;code&gt;/etc/systemd/journald.conf&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;journal持久化日志默认保存在&lt;code&gt;/var/log/journal&lt;/code&gt;，新建该目录&lt;code&gt;mkdir /var/log/journal&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;journal的各组件的内置用户均属于&lt;code&gt;systemd-journal&lt;/code&gt;用户组（&lt;code&gt;grep journal /etc/group&lt;/code&gt;），因此，&lt;code&gt;/var/log/journal&lt;/code&gt;应配置为协作目录：先更改目录属组&lt;code&gt;chown root:systemd-journal /var/log/journal&lt;/code&gt;，再添加协作权限&lt;code&gt;chmod 2755 /var/log/journal&lt;/code&gt;，&lt;/li&gt;
&lt;li&gt;重启进程&lt;code&gt;killall -USR1 systemd-journald&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;系统时间&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;硬件时间&lt;code&gt;hwclock&lt;/code&gt;，对应cmos(bios|uefi)中的一个硬件，因为主板工艺在关机后存在计时差异；&lt;/li&gt;
&lt;li&gt;软件时间在开机时复制硬件时间，软件时间可以手动调整，可以自动同步（网络的时钟服务器），软件时间可以将时间再写入硬件时间；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timedatectl&lt;/code&gt;红帽新提供的新工具，时间同步服务在&lt;code&gt;systemctl status chronyd.service&lt;/code&gt;中，配置文件在&lt;code&gt;/etc/chronyd.conf&lt;/code&gt;中；&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;/etc/chronyd.conf&lt;/code&gt;的服务器为教学环境的授时服务器后，重启服务&lt;code&gt;systemctl restart chronyd.service&lt;/code&gt;，再&lt;code&gt;timedatectl&lt;/code&gt;查看发现NTP synchronized为yes；&lt;/li&gt;
&lt;li&gt;很多分布式软件均需要保持精确的系统时间；&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;网络&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;旧命令&lt;code&gt;netstat&lt;/code&gt;，网络状态统计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-t&lt;/code&gt;查看tcp链接，&lt;code&gt;-u&lt;/code&gt;查看udp链接；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-n&lt;/code&gt;用数字表示（不解析域名）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-l&lt;/code&gt;监听状态的端口（开放端口，&lt;code&gt;/etc/service&lt;/code&gt;中可以看到端口对应信息）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p&lt;/code&gt;查看哪个个程序调用信息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新命令&lt;code&gt;ss&lt;/code&gt;，套接字统计，与&lt;code&gt;netstat&lt;/code&gt;基本用法一样；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;旧命令&lt;code&gt;ifconfig&lt;/code&gt;，不推荐使用；旧命令&lt;code&gt;ip&lt;/code&gt;：查看三层：&lt;code&gt;ip addr&lt;/code&gt;；查看二层&lt;code&gt;ip link&lt;/code&gt;；添加地址&lt;code&gt;ip addr add 172.25.0.22/24 dev eth0&lt;/code&gt;（重启后无效，需要改配置文件）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新命令&lt;code&gt;nmcli&lt;/code&gt;（命令行）或&lt;code&gt;nmtui&lt;/code&gt;（字符图形化），旧命令仅能当前生效，并不能永久生效（需要改配置文件），新命令直接永久生效；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新命令&lt;code&gt;systemctl status network&lt;/code&gt;是提供后台网络功能的服务，&lt;code&gt;systemctl status NetworkManager&lt;/code&gt;是后台维护网络配置的服务，配置文件在&lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-接口&lt;/code&gt;，前台提供配置的工具有：&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nmcli device status&lt;/code&gt;查看设备概要信息，&lt;code&gt;nmcli device shwo&lt;/code&gt;查看设备详细信息；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nmcli connection show &quot;System eth0&quot;&lt;/code&gt;，查看链接详细信息；&lt;/li&gt;
&lt;li&gt;新建链接：&lt;code&gt;nmcli connection add con-name home ifname eth0 type ethernet&lt;/code&gt;（此时多一个&lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-home&lt;/code&gt;文件）；&lt;/li&gt;
&lt;li&gt;配置自动连接的静态IP&lt;code&gt;nmcli connection add con-name work autoconnect yes ifname eth0 type ethernet ip4 172.24.4.11/24 gw4 172.24.4.254&lt;/code&gt;(此时多一个&lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-work&lt;/code&gt;文件；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nmcli delect work&lt;/code&gt;删除链接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nmcli connection modify work ipv4.addresses 172.25.0.23/24&lt;/code&gt;添加IP地址；&lt;/li&gt;
&lt;li&gt;修改地址获取方式&lt;code&gt;nmcli connection modify home ipv4.method manual ipv4.address 172.25.0.23/24&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nmcli connection show --active&lt;/code&gt;查看生效链接;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nmcli connection up &quot;work&quot;&lt;/code&gt;使配置work生效；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;show&lt;/code&gt;命令时，大写是生效的配置，小小是配置文件中未生效的配置；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;旧命令&lt;code&gt;hostname&lt;/code&gt;，仅查看，配置需要改&lt;code&gt;/etc/hosts&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;新命令&lt;code&gt;hostnamectl&lt;/code&gt;，新工具直接永久生效；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dns查找顺序：&lt;code&gt;/etc/hosts&lt;/code&gt;-&amp;gt;&lt;code&gt;buffer&lt;/code&gt;-&amp;gt;&lt;code&gt;/etc/resolv.conf&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hostnamectl set-hostname server.example.com&lt;/code&gt;设置主机映射；&lt;/li&gt;
&lt;li&gt;配置存放在&lt;code&gt;/etc/hostname&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;归档&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用命令&lt;code&gt;tar -cvf xxx.tar file1 file2 file3&lt;/code&gt;（加.tar是归档的命名习惯，&lt;code&gt;-c&lt;/code&gt;创建、&lt;code&gt;-v&lt;/code&gt;显示详细信息、&lt;code&gt;-f&lt;/code&gt;指定文件名）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;常用的压缩软件有&lt;code&gt;bzip2&lt;/code&gt;、&lt;code&gt;xz&lt;/code&gt;、&lt;code&gt;gzip&lt;/code&gt;，这几个压缩软件默认行为都会删除原文件；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;zcat&lt;/code&gt;是&lt;code&gt;gzip&lt;/code&gt;的配套工具，用来在不解包的情况下查看文件内容；&lt;code&gt;gunzip&lt;/code&gt;等于&lt;code&gt;gzip -d&lt;/code&gt;，详见&lt;code&gt;man gzip&lt;/code&gt;；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bzcat&lt;/code&gt;是&lt;code&gt;bzip2&lt;/code&gt;的配套工具，详情见&lt;code&gt;man bzip2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xzcat&lt;/code&gt;是&lt;code&gt;xz&lt;/code&gt;的配套工具，详见&lt;code&gt;man xz&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;tar&lt;/code&gt;可以直接调用其他压缩软件来做归档时压缩，&lt;code&gt;-j&lt;/code&gt;调用bzip2、&lt;code&gt;-J&lt;/code&gt;调用xz、&lt;code&gt;-z&lt;/code&gt;调用gzip：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tar -cvjf xxx.tar file1 file2 file3&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tar -xvf xxx.tar -C&lt;/code&gt;，&lt;code&gt;x&lt;/code&gt;解压、&lt;code&gt;v&lt;/code&gt;现实详细信息、&lt;code&gt;f&lt;/code&gt;指定文件名、&lt;code&gt;-C&lt;/code&gt;指定解压位置，解压不需要指定压缩程序，tar会自行判断；&lt;/li&gt;
&lt;li&gt;tar默认行为会在用户指定绝对路径时移除根（不然解压时可能会覆盖文件），选项&lt;code&gt;-P&lt;/code&gt;可以保持根目录；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;加密拷贝&lt;code&gt;scp -r root@server:/etc/ssh /home/student/serverbackup&lt;/code&gt;从server上取目录到本地，使用&lt;code&gt;-v&lt;/code&gt;显示详细信息；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rsync&lt;/code&gt;远程同步文件，&lt;code&gt;-a&lt;/code&gt;传输前归档：&lt;code&gt;rsync -av root@server:/var/log/maillog /servermaillog&lt;/code&gt;；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;rsync&lt;/code&gt;与&lt;code&gt;scp&lt;/code&gt;最大的区别是，&lt;code&gt;rsync&lt;/code&gt;是增量传输，&lt;code&gt;scp&lt;/code&gt;是完全传输；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-07-24&lt;/h1&gt;
&lt;h2&gt;软件安装&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;rpm复制后端安装编译等工作，yum负责用户交互及解决依赖关系，是rpm的前段工具；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;rpm&lt;/code&gt;常用命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;man 8 rpm&lt;/code&gt;查看rpm数据库等帮助信息；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm -qa | wc -l&lt;/code&gt;统计已安装个数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm -q xxx&lt;/code&gt;查询已安装包名；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm -qi xxx&lt;/code&gt;查询已安装包详细信息；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm -ql xxx&lt;/code&gt;查看包在安装时具体安装了哪些文件；&lt;code&gt;rpm -qc xxx&lt;/code&gt;查看配置文件；&lt;code&gt;rpm -qd xxx&lt;/code&gt;查看文档文件；&lt;code&gt;rpm -qf filename&lt;/code&gt;查看指定文件属于哪个程序；&lt;/li&gt;
&lt;li&gt;已安装的包在rpm数据库&lt;code&gt;/var/lib/rpm&lt;/code&gt;中；&lt;code&gt;rpm -qpi xxx&lt;/code&gt;命令也可以看到未安装包，同样的&lt;code&gt;rpm -qpl/qpc/qpd&lt;/code&gt;命令可以看到该包的所有文件、配置文件、文档文件；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rpm -qR xxx&lt;/code&gt;查询依赖；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;建立本地&lt;code&gt;yum&lt;/code&gt;仓库（redhat和centOS有mini/everything等版本，而epel的包比everything还多，通常用于自建仓库）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/etc/yum.conf&lt;/code&gt;是&lt;code&gt;yum&lt;/code&gt;的配置文件，&lt;code&gt;/etc/yum.repos.d&lt;/code&gt;是yum仓库的配置目录；&lt;/li&gt;
&lt;li&gt;下载&lt;code&gt;rhel.xxx.iso&lt;/code&gt;，在linux下&lt;code&gt;.iso&lt;/code&gt;是个文件系统，需要挂载：&lt;code&gt;mount xxx.iso /mnt&lt;/code&gt;；（通过&lt;code&gt;file xxx.iso&lt;/code&gt;可以显示文件系统详情）&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;/etc/yum.repos.d&lt;/code&gt;下新建&lt;code&gt;dvd.repo&lt;/code&gt;，写入：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 必须且应保证主机上唯一
[dvd]                 
# 非必须
name = dvd            
# 必须
baseurl = file:///mnt 
# 开启本仓库
enable = 1            
# 是否需要校验，通常官方光盘不需要
gpgcheck = 0          
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;yum repolist&lt;/code&gt;查看仓库信息，多了一个我们新建的&lt;code&gt;dvd&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;repolist读取的仓库信息缓存在&lt;code&gt;/var/cache/yum&lt;/code&gt;下；&lt;/li&gt;
&lt;li&gt;yum自动添加仓库：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;yum -config-manager --add-repo=http://classroom.example.com/content/rhel7.0/x86_64/dvd
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;新建的仓库名称很长，还需要手动添加&lt;code&gt;gpgcheck=0&lt;/code&gt;，推荐使用前一种方法；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum -config-manager --enable/disable&lt;/code&gt;可以用来禁用/启用仓库，仓库全名使用&lt;code&gt;yum repolist&lt;/code&gt;列出；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;yum&lt;/code&gt;常用命令：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;yum list&lt;/code&gt;列出所有软件，按名字查找使用&lt;code&gt;yum list xxx&lt;/code&gt;（软件名完全匹配）可以使用通配符，如&lt;code&gt;yum list http*&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum search&lt;/code&gt;搜索软件，匹配包括简介信息；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum info tcpdump&lt;/code&gt;查看软件详细信息；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum group install&lt;/code&gt;以组为单位进行安装；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum history&lt;/code&gt;显示yum历史操作汇总信息，加&lt;code&gt;list&lt;/code&gt;命令查看详细信息，再使用&lt;code&gt;list&lt;/code&gt;中的ID可以附加&lt;code&gt;undo&lt;/code&gt;和&lt;code&gt;redo&lt;/code&gt;等命令进行撤销或重做操作；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum erase&lt;/code&gt;删除；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;yum provide&lt;/code&gt;查询提供指定命令、文件的软件包；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;文件系统&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Linux仅允许一个主文进系统，一个根目录&lt;code&gt;/&lt;/code&gt;（入口），其他文件系统需要挂载在主文进系统之下，统一从根目录&lt;code&gt;/&lt;/code&gt;进入；（Windows允许多个跟目录，通过盘符区分），参见&lt;code&gt;man 8 mount&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上一节中的挂载：&lt;code&gt;mount xxx.iso /mnt&lt;/code&gt;，卸载：&lt;code&gt;umount /mnt&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;df -h&lt;/code&gt;查看文件系统详细信息，&lt;code&gt;du filename&lt;/code&gt;查看文件占硬盘大小，&lt;code&gt;du -s dirname&lt;/code&gt;查看目录占用硬盘大小；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;blkid&lt;/code&gt;查看块设备的属性；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;临时挂载、卸载使用&lt;code&gt;mount&lt;/code&gt;、&lt;code&gt;umount&lt;/code&gt;；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于无法直接卸载的目录，使用&lt;code&gt;lsof /xxx&lt;/code&gt;或&lt;code&gt;fuser /xxx&lt;/code&gt;查看哪个用户哪个进程在使用，结束该进程后就可以卸载；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;永久挂载，配置&lt;code&gt;/etc/fstab&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;/xxx/xxx.iso /mnt iso9660 defaults 0 0&lt;/code&gt;挂载一个永久文件系统；&lt;/li&gt;
&lt;li&gt;意为挂载xxx.iso文件；&lt;/li&gt;
&lt;li&gt;挂载点为&lt;code&gt;/mnt&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;文件系统类型为iso9660（这个信息可以使用&lt;code&gt;mount xxx.iso /mnt&lt;/code&gt;临时挂载文件后使用&lt;code&gt;mount&lt;/code&gt;命令查看到）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defaults&lt;/code&gt;为默认选项（rw, suid, dev, exec, auto, nouser, async）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;为dump选项，不允许转存；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;为非启动文件系统且不需要检查；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;硬链接&lt;code&gt;ln&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linux硬盘管理一个扇区的最小单位为4k，使用&lt;code&gt;mkfs&lt;/code&gt;格式化分区时，系统会将扇区分为存储inode或存储block；&lt;/li&gt;
&lt;li&gt;inode，即文件索引，存储文件元数据，通过&lt;code&gt;ll -i xxx&lt;/code&gt;查看到除文件名以外的信息全部是存储在inode中的，权限后的数字即block被连接的次数，使用&lt;code&gt;stat xxx&lt;/code&gt;查看元信息，其中Modify是block修改时间，Change是元数据修改时间；&lt;/li&gt;
&lt;li&gt;block，存储文件内容；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;ln xxx yyy&lt;/code&gt;后，通过&lt;code&gt;ll -i xxx yyy&lt;/code&gt;发现两个文件的inode值相同，即硬链接只复制了元数据；&lt;/li&gt;
&lt;li&gt;硬链接不依赖原文件，修改一处另一处也修改，一个block中的文件仅有一个inode值；&lt;/li&gt;
&lt;li&gt;硬链接删除操作删除的是inode连接，只有当连接数为0时，才删除文件回收block；&lt;/li&gt;
&lt;li&gt;硬链接不能跨文件系统，因为inode有可能冲突；&lt;/li&gt;
&lt;li&gt;硬链接不能对目录使用；&lt;/li&gt;
&lt;li&gt;另外，空目录连接次数为2，除了自己以外，还有&lt;code&gt;.&lt;/code&gt;连接了它；如果有子目录，则连接数继续+1，即被子目录的&lt;code&gt;..&lt;/code&gt;连接；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;软链接&lt;code&gt;ln -s&lt;/code&gt;，相当于Windows的快捷方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;软链接是指向源文件的路径名（硬链接指向原文件的block，即复制inode，软链接的大小刚好是目标文件的路径长度），因此，软链接的源一定要使用绝对路径；&lt;/li&gt;
&lt;li&gt;软链接可以跨文件系统，因为是路径；&lt;/li&gt;
&lt;li&gt;软链接可以对目录使用；&lt;/li&gt;
&lt;li&gt;软链接的原文件必须存在，如果原文件被删除，则软链接失效；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件查找&lt;code&gt;locate&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;基于文件名进行模糊查找；&lt;/li&gt;
&lt;li&gt;维护一个inode与文件名的映射，存在数据库&lt;code&gt;/var/lib/mlocate/mlocate.db&lt;/code&gt;中，该数据库默认一天更新一次，即新建的文件不会存在于数据库中，也就找不到；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;updatedb&lt;/code&gt;更新数据库；&lt;/li&gt;
&lt;li&gt;不会查找&lt;code&gt;/tmp&lt;/code&gt;中的任何文件；&lt;/li&gt;
&lt;li&gt;不能定义在那个目录中去找；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件查找&lt;code&gt;find&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;实时查找，可以基于文件的任何元属性进行查找，如文件名、修改日期、访问时间等；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-amin n&lt;/code&gt;最近n分钟被访问的文件，其中&lt;code&gt;-n&lt;/code&gt;为n分钟以内、&lt;code&gt;+n&lt;/code&gt;为n分钟以外；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-anewer file&lt;/code&gt;找出所有访问时间比file大的文件；&lt;/li&gt;
&lt;li&gt;命令包含&lt;code&gt;time&lt;/code&gt;的单位为天；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-empty&lt;/code&gt;查找空文件；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-links n&lt;/code&gt;查找硬链接次数超过n的文件；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find&lt;/code&gt;后可以接动作，如删除动作&lt;code&gt;find . -empty -delete&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find / -perm -2000&lt;/code&gt;查找的是特殊权限2，仅查找四位权限码第一位；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find / -perm /7000&lt;/code&gt;查找的是所有特殊权限；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find / -nouser&lt;/code&gt;查找失去用户的文件（不安全，因为下其他用户有可能占用该inode，造成权限漏洞）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find /var -user root -a -group wheel&lt;/code&gt;，其中-a可以省略，默认and，也可以用-o；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find /var -size -5k -exec ls -dlh &apos;{}&apos; \;&lt;/code&gt;，表示小于等于4k的文件；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;find /root -size -6M -a -type f -exec tar cvzfP /tmp/5m.tar.gz &apos;{}&apos; \;&lt;/code&gt;，将&lt;code&gt;/root&lt;/code&gt;下所以小于6M的文件查找并打包放在&lt;code&gt;/tmp&lt;/code&gt;下；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-07-25&lt;/h1&gt;
&lt;h2&gt;系统批量部署&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装kvm虚拟机时，对于Intel的CPU，支持vmx的CPU可以进行硬件虚拟化，需要在BIOS中保证开启vmx及nx；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Kickstart批量部署：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;%pre&lt;/code&gt;安装软件包前的脚本；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%package&lt;/code&gt;安装软件包；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%post&lt;/code&gt;安装软件包后的脚本；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;%end&lt;/code&gt;脚本结束标记；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;virt-manager&lt;/code&gt;管理虚拟机：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将server重启，使用&lt;code&gt;F12&lt;/code&gt;后&lt;code&gt;^B&lt;/code&gt;中断正常启动，选择&lt;code&gt;iPXE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;输入&lt;code&gt;gPXE&amp;gt; autoboot&lt;/code&gt;执行PXE启动；&lt;/li&gt;
&lt;li&gt;将&lt;code&gt;ks=http://172.25.4.10/ks-config/ks.cfg&lt;/code&gt;输入到行尾（行尾为&lt;code&gt;... quit&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;实验完成后，使用&lt;code&gt;rht-vmctl fullreset server&lt;/code&gt;将虚拟机完全重置；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;正则表达式&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;grep&lt;/code&gt;为全局（对文件）正则表达式搜索，&lt;code&gt;egrep&lt;/code&gt;为使用扩展正则表达式搜索，扩展的更强大，&lt;code&gt;fgrep&lt;/code&gt;为固定格式搜索（即不使用正则表达式，更快）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;\&amp;lt;&lt;/code&gt;词首、&lt;code&gt;\&amp;gt;&lt;/code&gt;词尾，等同于&lt;code&gt;\b&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;正则表达式也可以使用&lt;code&gt;[[:digit:]]&lt;/code&gt;代替&lt;code&gt;[0-9]&lt;/code&gt;，&lt;code&gt;[^]&lt;/code&gt;取反，参考&lt;code&gt;man 7 glob&lt;/code&gt;和&lt;code&gt;man 7 regex&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;正则表达式的测试可以使用&lt;code&gt;/usr/share/dict/words&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wc -l&lt;/code&gt;可以统计行数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;()&lt;/code&gt;表示当做整体，通常在扩展正则表达式中使用，非扩展的需要转移括号；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v&lt;/code&gt;表示取反，即显示不匹配的行；&lt;code&gt;-i&lt;/code&gt;不区分大小写；&lt;code&gt;-A n&lt;/code&gt;匹配行后显示n行，&lt;code&gt;-B n&lt;/code&gt;匹配行前显示n行，&lt;code&gt;-C m, -n&lt;/code&gt;匹配行前显示m行后显示n行，&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;计划任务&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;at&lt;/code&gt;适合定时执行仅一次的任务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;at&lt;/code&gt;计划任务前，使用&lt;code&gt;systemctl start atd&lt;/code&gt;启动&lt;code&gt;at&lt;/code&gt;后台服务；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;at&lt;/code&gt;是&lt;code&gt;atd&lt;/code&gt;的前端工具，使用&lt;code&gt;^d&lt;/code&gt;提交；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;atq&lt;/code&gt;是监控任务的工具；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;atrm&lt;/code&gt;删除任务；&lt;/li&gt;
&lt;li&gt;对于某些用户，不希望他执行&lt;code&gt;at&lt;/code&gt;任务，可以将其用户名写入&lt;code&gt;/etc/at.deny&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;对于某些用户，希望他执行&lt;code&gt;at&lt;/code&gt;任务，可以将其用户名写入&lt;code&gt;/etc/at.allow&lt;/code&gt;；&lt;code&gt;at&lt;/code&gt;命令先查找&lt;code&gt;allow&lt;/code&gt;，若没找到则继续在&lt;code&gt;deny&lt;/code&gt;中查找，若仍未找到，则允许执行（默认允许）；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;cron&lt;/code&gt;适合周期性执行的任务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cron&lt;/code&gt;是&lt;code&gt;crond&lt;/code&gt;的前端工具；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cron&lt;/code&gt;分为用户cron（如数据备份）和系统cron（如&lt;code&gt;locate&lt;/code&gt;命令使用的数据库更新）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;crontab&lt;/code&gt;是用户cron的前端工具，系统cron在&lt;code&gt;/etc/crontab&lt;/code&gt;中，&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cron&lt;/code&gt;的&lt;code&gt;deny&lt;/code&gt;/&lt;code&gt;allow&lt;/code&gt;和&lt;code&gt;at&lt;/code&gt;的行为一样；&lt;/li&gt;
&lt;li&gt;通过&lt;code&gt;crontab -e&lt;/code&gt;编辑当前用户的计划任务，写入&lt;code&gt;9-11/2 * * * * mktemp /tmp/XXXX&lt;/code&gt;表示每个小时的第9到11分钟步长为2的执行作业，即9、11分钟各执行一次；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;crontab -l&lt;/code&gt;列出当前用户的计划任务；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;crontab -r&lt;/code&gt;删除当前用户的计划任务；&lt;/li&gt;
&lt;li&gt;系统cron在&lt;code&gt;/etc/cron.d&lt;/code&gt;中，&lt;code&gt;0hourly&lt;/code&gt;存储立即执行的任务，&lt;code&gt;raid-check&lt;/code&gt;存储阵列检查相关任务，&lt;code&gt;sysstat&lt;/code&gt;存储系统状态相关任务；&lt;/li&gt;
&lt;li&gt;其他文件夹如&lt;code&gt;.daily&lt;/code&gt;、&lt;code&gt;.hourly&lt;/code&gt;等文件夹存放按日、按小时执行的作业；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;进程优先级&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;优先级高的进程能够获得更多CPU资源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TOP的&lt;code&gt;PR&lt;/code&gt;和&lt;code&gt;NI&lt;/code&gt;字段，即系统优先级和用户配置优先级；&lt;/li&gt;
&lt;li&gt;优先级取值为-100~39，系统优先级为-100~-1，用户优先级为0~39；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NI&lt;/code&gt;取值为-20~19；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PR&lt;/code&gt;值为&lt;code&gt;rt&lt;/code&gt;的表示用户无法调整的（&lt;code&gt;PR&lt;/code&gt;没有小于0的，小于0即显示&lt;code&gt;rt&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PR-NI=20&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nice&lt;/code&gt;后接命令调整该命令的优先级，如&lt;code&gt;nice --20 dd if=/dev/zero of=/dev/null&lt;/code&gt;（&lt;code&gt;-&lt;/code&gt;代表省略的选项&lt;code&gt;-n&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;renice&lt;/code&gt;后接进程PID可以重新调整该进程优先级；，如&lt;code&gt;renice -5 12336&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;top&lt;/code&gt;命令也可以调整优先级，使用&lt;code&gt;r&lt;/code&gt;命令再输入PID，确认后输入优先级值即可；&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;权限访问控制列表（权限ACL）&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;ACL控制粒度比POSIX的文件权限控制model更细；&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;ll&lt;/code&gt;显示的文件权限中如&lt;code&gt;-rwx-r-x-r-x.&lt;/code&gt;，其中的&lt;code&gt;.&lt;/code&gt;即表示未添加访问控制；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;getfacl xxx&lt;/code&gt;查看权限细节&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;setfacl&lt;/code&gt;给文件设置ACL权限：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;setfacl -m group::r xxx&lt;/code&gt;（修改权限为&lt;code&gt;r--&lt;/code&gt;），&lt;code&gt;setfacl -x group::- xxx&lt;/code&gt;（删除权限为&lt;code&gt;---&lt;/code&gt;），即也可以用于给文件设置model权限；&lt;/li&gt;
&lt;li&gt;比如对于&lt;code&gt;-rw-------. root root&lt;/code&gt;的文件，user是不能读取的，因为user在model权限中匹配的是最后三位&lt;code&gt;---&lt;/code&gt;（既不是root用户也不是用户组的其他权限），此时无法通过用户和组的model来设置文件权限；&lt;/li&gt;
&lt;li&gt;具体设置方法参见&lt;code&gt;man 1 setfacl&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;如&lt;code&gt;setfacl -m user:r xxx&lt;/code&gt;给user用户添加ACL读权限、&lt;code&gt;setfacl -m user:rw xxx&lt;/code&gt;给user用户ACL添加读写权限、&lt;code&gt;setfacl -m m::r&lt;/code&gt;修改ACL掩码将user权限改为读、&lt;code&gt;set -x user xxx&lt;/code&gt;删除用户ACL权限&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setfacl -m g:user:rw xxx&lt;/code&gt;给组添加权限，&lt;code&gt;setfacl -x g:user xxx&lt;/code&gt;给组删除权限；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setfacl -b xxx&lt;/code&gt;删除所有ACL权限；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;setfacl -m user:rwx xdir&lt;/code&gt;给目录添加rwx权限，若想要该目录下的所以新建文件均具有ACL权限，则应写为&lt;code&gt;setfacl -m d:user:rwx xdir&lt;/code&gt;；需要注意的是&lt;code&gt;d&lt;/code&gt;参数对目标目录无效，仅对目标目录下的子文件及子目录生效，要修改目标目录的ACL，仍应使用没有&lt;code&gt;d&lt;/code&gt;的命令，即&lt;code&gt;setfacl -m user:rwx xdir&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-07-26&lt;/h1&gt;
&lt;h2&gt;SELinux&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;model模型的权限控制针对用户和文件，而SELinux针对程序和文件，SELinux是为了**避免人为错误（配置错误）**而设计的；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SELinux对程序的标签可以用&lt;code&gt;ps uZ 7842&lt;/code&gt;显示出来，对文件的标签可以用&lt;code&gt;ll -dZ /tmp&lt;/code&gt;显示出来；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;举例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;httpd的标签为&lt;code&gt;system_u:system_r:httpd_t:s0&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;/tmp的标签为&lt;code&gt;system_u:object_r:tmp_t:s0&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;/var/www/html的标签为&lt;code&gt;system_u:object_r:httpd_sys_content_t:s0&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;code&gt;setools-console&lt;/code&gt;可以查看SELinux规定的进程对目录做的操作：&lt;code&gt;sesearch -A -s httpd_t -t tmp_t&lt;/code&gt;，可以看到&lt;code&gt;allow httpd_t tmp_t : dir&lt;/code&gt;开头的两条记录，其中权限大的一条为Permissive模式（临时关闭SELinux）下的权限，权限小的一条为Enforce模式下的权限（即正常的SELinux开启时，可以通过&lt;code&gt;getenforce&lt;/code&gt;目录查看）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SELinux的开机启动配置在&lt;code&gt;/etc/selinux/config&lt;/code&gt;中，要关闭SELinux应配置为&lt;code&gt;SELinux=disabled&lt;/code&gt;，使配置生效需要重启；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在关闭SELinux后，用&lt;code&gt;-Z&lt;/code&gt;选项查看新建的文件是没有SELinux标签的，在关闭SELinux后新建（或修改并保存）文件，会在该目录下生成一个名为&lt;code&gt;.autorelabel&lt;/code&gt;的文件，意味着当再次开启SELinux并启动系统时，系统会再次打标签，系统会自动在打标签后再次重启；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setenforce&lt;/code&gt;可以临时修改模式，通常供调试时使用；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;semanage&lt;/code&gt;管理SELinux策略：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其中&lt;code&gt;fcontext&lt;/code&gt;子命令管理文件上下文标签；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;semanage fcontext -l | grep admin_home_t&lt;/code&gt;列出标签为&lt;code&gt;admin_home_t&lt;/code&gt;的条目，其中默认条目&lt;code&gt;/root(/.*)?&lt;/code&gt;只能改不能删；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;semanage fcontext -a -t admin_home_t /var/www/html/(/.*)&lt;/code&gt;，将&lt;code&gt;/var/www/html&lt;/code&gt;下所有文件改为&lt;code&gt;admin_home_t&lt;/code&gt;标签；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;restorecon -vR /var/www/html/&lt;/code&gt;，递归的重置标签，包括目录本身及子目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;semanage fcontext -a -t admin_home_t &apos;/var/www/html/(/.*)?&apos;&lt;/code&gt;，修改包括&lt;code&gt;/var/www/html&lt;/code&gt;本身及子目录；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;semanage boolean -l | grep ftpd&lt;/code&gt;查看ftp相关功能设置（&lt;code&gt;getsebool&lt;/code&gt;查看的是内存中的运行时配置）；&lt;/li&gt;
&lt;li&gt;开启FTP匿名上产需要修改&lt;code&gt;semanage bool --modify --on ftpd_anon_write&lt;/code&gt;，&lt;code&gt;semanage boolean --modify --on ftp_home_dir&lt;/code&gt;，&lt;code&gt;chomd o+w .&lt;/code&gt;，&lt;code&gt;chcon -t public_content_rw_t .&lt;/code&gt;等很多策略，因为SELinux默认认为FTP匿名上传行为不安全；其中&lt;code&gt;setsebool&lt;/code&gt;不加&lt;code&gt;-P&lt;/code&gt;用于临时修改标签，&lt;code&gt;-P&lt;/code&gt;用于永久修改标签；若要排查问题可参考系统日志&lt;code&gt;tail -n 50 /var/log/message&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;SELinux的默认配置已经能够很好的运行，通常不需要重新配置，除非确定需要的功能确实是因为SELinux安全策略引起的，再进行修改，推荐始终打开SELinux功能以提高系统安全性；&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;网络用户&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;安装&lt;code&gt;yum install authconfig-gtk.x86_64&lt;/code&gt;，使用&lt;code&gt;authconfig-gtk&lt;/code&gt;设置用户类型，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选择LDAP，LDAP搜索基DN为&lt;code&gt;dc=example,dc=com&lt;/code&gt;（范围小的写前面）LDAP sever为&lt;code&gt;classroom.example.com&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;选择TLS加密连接，在CA证书中写证书地址即可；&lt;/li&gt;
&lt;li&gt;认证模式为Kerberos密码认证，Realm为&lt;code&gt;EXAMPLE.COM&lt;/code&gt;，KDCs和管理服务器均为&lt;code&gt;classroom.example.com&lt;/code&gt;，下面两个DNS解析主机及DNC定位KDCs不选；应用配置&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;getent passwd&lt;/code&gt;发现新加了&lt;code&gt;ldapuser*&lt;/code&gt;账号，使用&lt;code&gt;ssh ldapuser4@localhost&lt;/code&gt;测试登录，默认密码为&lt;code&gt;kerberos&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;getent passwd ldapuser0&lt;/code&gt;查看网络用户条目（也可以查看本地用户，如&lt;code&gt;getent passwd root&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;autofs&lt;/code&gt;按需挂载（&lt;code&gt;mount&lt;/code&gt;必须是root用户，且为永久挂载）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装&lt;code&gt;yum install -y autofs.x86_64&lt;/code&gt;，查看&lt;code&gt;rpm -ql autofs | grep .service&lt;/code&gt;发现有autofs有service；帮助参见&lt;code&gt;man auto.master&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;开机运行并启动：&lt;code&gt;systemctl enable autofs.service &amp;amp;&amp;amp; systemctl start autofs.service&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;编辑&lt;code&gt;/etc/auto.master&lt;/code&gt;，添加一行&lt;code&gt;/home/guests /etc/auto.ldap&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;新建并编辑&lt;code&gt;/etc/auto.ldap&lt;/code&gt;（参考&lt;code&gt;auto.misc&lt;/code&gt;，使用&lt;code&gt;:sp /etc/auto.master&lt;/code&gt;或&lt;code&gt;vim -o /etc/auto.{ldap,misc}&lt;/code&gt;分屏对照编辑更方便）：&lt;pre&gt;&lt;code&gt;# *与&amp;amp;是一对内容对应的占位符
* -fstype=nfs,rw,sync classroom.example.com:/home/huests/&amp;amp;
# 若要指定用户及目录应写为
ldapuser4 -fstype=nfs,rw,sync classroom.example.com:/home/huests/ldapuser4
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;此时在使用&lt;code&gt;ssh ldapuser4@localhost&lt;/code&gt;登录即可按需挂载&lt;code&gt;ldapuser4&lt;/code&gt;的home目录&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;分区及格式化&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;MBR分区（即微软的DOS），硬盘的的第一分区（盘面最外面）为MBR分区，存储主引导记录（如Linux的grub2.0 448字节 ）和分区表（64字节，其中，一条分区记录要占用16字节，即最多只能分四个主分区），通常最后一个分区作为扩展分区，用来做逻辑分区，而逻辑分区表不再MBR中，因此没有限制（MBR最大支持2T的硬盘）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mbr
├── P1
├── P2
├── P3
└── extend(4)
      ├── L5
      ├── L6
      ├── L7
      ├── L8
      ├── L9
      ├── L10
      └── L11
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;lsblk&lt;/code&gt;查看硬盘分区，&lt;code&gt;-p&lt;/code&gt;显示完全路径，&lt;code&gt;fdisk -l&lt;/code&gt;列出分区信息，分区完成后，使用&lt;code&gt;partprobe&lt;/code&gt;通知系统重新读取分区表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fdisk /dev/vdb&lt;/code&gt;开始分区；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fdisk&lt;/code&gt;对MBR支持更好；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;GPT分区（GUID Partition Table），LBA0为保护MBR区，LBA1为主GPT头，LBA2-34的每个LBA存四个条目，共128个条目；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;gdisk&lt;/code&gt;对GPT支持更好&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;上面仅是分区，现在的分区系统还不能使用，需要格式化，给每个分区分配UUID，使用&lt;code&gt;blkid&lt;/code&gt;查看；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mkfs&lt;/code&gt;格式化为文件系统用，&lt;code&gt;mkfs /dev/vdb1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;临时挂载&lt;code&gt;mount /dev/vdb1 /mnt&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;重挂&lt;code&gt;mount -o remount,ro,nosuid /mnt&lt;/code&gt;，即设备不断线仅改变参数；&lt;/li&gt;
&lt;li&gt;永久挂载配置&lt;code&gt;/etc/fstab&lt;/code&gt;（&lt;code&gt;man 5 fstab&lt;/code&gt;查看挂载及文件系统信息），使用&lt;code&gt;blkid&lt;/code&gt;查看UUID，写入配置：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 硬盘
UUID=xxx /mnt xfs  default 0 0
# 虚拟内存
UUID=xxx swap swap default 0 0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;df -h&lt;/code&gt;或&lt;code&gt;fdisk -l&lt;/code&gt;查看硬盘挂载结果；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mkswap&lt;/code&gt;格式化为虚拟内存用，挂载/写在虚拟内存&lt;code&gt;swapon /dev/vdb5&lt;/code&gt;、&lt;code&gt;swapoff /dev/vdb5&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;top&lt;/code&gt;或&lt;code&gt;free&lt;/code&gt;查看Swap内存挂载结果；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;分区必须是连续的存储空间，而卷没有这种要求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分区不能跨硬盘，而卷可以；&lt;/li&gt;
&lt;li&gt;分区不能动态扩展，而卷可以；&lt;/li&gt;
&lt;li&gt;逻辑卷可以做快照；&lt;/li&gt;
&lt;li&gt;物理分区-&amp;gt;pv（物理卷）-&amp;gt;vg（卷组）-&amp;gt;lv（逻辑卷，当做逻辑卷或虚拟硬盘）-&amp;gt;mkfs-&amp;gt;mount&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建物理卷&lt;code&gt;pvcreate /dev/vdb7 /dev/vdc5&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pvs&lt;/code&gt;或&lt;code&gt;pvscan&lt;/code&gt;查看概要，&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pvdisplay /dev/vdb7&lt;/code&gt;查看详情；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-s&lt;/code&gt;指定PE（PhysicalExtent）大小（默认4M）；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建卷组&lt;code&gt;vgcreate vg0 /dev/vdb7 /dev/vdc5&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vgs&lt;/code&gt;查看卷组；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vgremove vg0&lt;/code&gt;删除卷组&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vgcreate -s 8m vg0 /dev/vdb7 /dev/vdc5&lt;/code&gt;按8M指定PE大小&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建逻辑卷&lt;code&gt;lvcreate -l 512 -n lv0 vg0&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;lvs&lt;/code&gt;查看逻辑卷概要；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-n&lt;/code&gt; 指定卷名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-l&lt;/code&gt; 指定分配到PE个数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-L&lt;/code&gt; 指定分配的空间；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;此时&lt;code&gt;lsblk&lt;/code&gt;可以看到逻辑卷了，&lt;code&gt;mkfs.xfs&lt;/code&gt;格式化&lt;code&gt;mkfs.xfs /dev/vg0/lv0&lt;/code&gt;或&lt;code&gt;mkfs.xfs /dev/mapper/vg0-lv0&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建挂载点&lt;code&gt;mkdir -p /lv/{lv0,lv1}&lt;/code&gt;，在&lt;code&gt;vim /etc/fstab&lt;/code&gt;中的配置使挂载永久生效；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;扩展逻辑卷：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;扩展前&lt;code&gt;vgs&lt;/code&gt;查看卷组的空闲，扩展使用&lt;code&gt;lvextend -L +2G /dev/vg0/lv0&lt;/code&gt;，&lt;code&gt;df -hT&lt;/code&gt;查看文件系统信息，而xfs支持自动识别增重的空间，使用&lt;code&gt;xfs_growfs /dev/vg0/lv0&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;若卷组空间不够，硬向卷组添加空间（插硬盘，分区并格式化）；&lt;/li&gt;
&lt;li&gt;创建pv&lt;code&gt;pvcreate /dev/vdc6 /dev/vdd5&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;扩展vg&lt;code&gt;vgextent vg0 /dev/vdc6 /dev/vdd5&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;扩展lv&lt;code&gt;lvextent -L +10G /dev/vg0/lv1&lt;/code&gt;，若为ext文件系统使用&lt;code&gt;resize2fs /dev/vg0/lv1&lt;/code&gt;重新识别；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-07-27&lt;/h1&gt;
&lt;h2&gt;启动排错&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;启动相关项位于&lt;code&gt;/boot&lt;/code&gt;，vm开头的文件为内核文件，相关配置文件位于&lt;code&gt;/etc/grub.d&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看运行级别&lt;code&gt;runlevel&lt;/code&gt;，共0~6七个运行级别，类似于Windows“安全模式、带网络的安全模式等”，不同的启动模式启动的功能不一样，正常启动模式为3和5：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;runlevel&lt;/code&gt;的返回值第一位为上次运行级别，第二位为本次运行级别；&lt;/li&gt;
&lt;li&gt;0为关机，即什么都不启动；&lt;/li&gt;
&lt;li&gt;1为单用户模式（root用户），即救援模式（类似Windows的安全模式）；&lt;/li&gt;
&lt;li&gt;2/4，未定义，都认为是3模式；&lt;/li&gt;
&lt;li&gt;3为正常命令行启动，不提供图形化界面；&lt;/li&gt;
&lt;li&gt;5为正常图形化启动，提供图形化界面及命令行界面（&lt;code&gt;ctl+alt+F2~6&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;6位重启（先结束所有进程再启动）；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;init n&lt;/code&gt;即可执行相应的运行级别，如&lt;code&gt;init 6&lt;/code&gt;即重启；&lt;/li&gt;
&lt;li&gt;以上是为了兼容Redhat 6，在7版本通常使用&lt;code&gt;systemctl [target]&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;systemctl&lt;/code&gt;目标：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemctl poweroff&lt;/code&gt;或直接执行&lt;code&gt;poweroff&lt;/code&gt;，查看&lt;code&gt;poweroff&lt;/code&gt;可知&lt;code&gt;/usr/sbin/poweroff -&amp;gt; ../bin/systemctl&lt;/code&gt;，即&lt;code&gt;systemctl poweroff.target&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reboot&lt;/code&gt;与上一条一样；&lt;/li&gt;
&lt;li&gt;目标存放在&lt;code&gt;/usr/lib/systemd/system&lt;/code&gt;，使用&lt;code&gt;ll *.target&lt;/code&gt;查看，发现Redhat 7的目标更加细致；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl -t&lt;/code&gt;补全可以看到目标容器，将目标分为10类，类似分类服务，如查看&lt;code&gt;vim NetworkManager.service&lt;/code&gt;中，有配置&lt;code&gt;Wants=network.target&lt;/code&gt;表示NetworkManager服务应加入network目标容器；&lt;/li&gt;
&lt;li&gt;临时切换目标&lt;code&gt;systemctl isolate multi-user.target&lt;/code&gt;（3级别）、&lt;code&gt;systemctl isolate graphical.target&lt;/code&gt;（5级别）；&lt;/li&gt;
&lt;li&gt;查看默认目标&lt;code&gt;systemctl get-default&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;设置默认目标&lt;code&gt;systemctl set-default multi-user.target&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;刚启动时也可以设置目标，选择Linux版本时按&lt;code&gt;e&lt;/code&gt;键，在&lt;code&gt;linux16&lt;/code&gt;行行尾（物理机，虚拟机删到&lt;code&gt;ro&lt;/code&gt;），输入&lt;code&gt;systemd.unit=multi-user.target&lt;/code&gt;，按&lt;code&gt;^x&lt;/code&gt;提交，系统启动就进入正常命令行模式了；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;修复密码&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;openssl rand -hex 16&lt;/code&gt;产生16位随机数，以十六进制输出；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;openssl rand -base64 16 | passwd --stdin root&lt;/code&gt;，此时不知道root密码了：
&lt;ul&gt;
&lt;li&gt;选择Linux版本时按&lt;code&gt;e&lt;/code&gt;键，在虚拟机环境&lt;code&gt;linux16&lt;/code&gt;删到&lt;code&gt;ro&lt;/code&gt;，输入&lt;code&gt;rd.break&lt;/code&gt;（打断正常启动），按&lt;code&gt;^x&lt;/code&gt;提交，进入交换根（内存根目录）；&lt;/li&gt;
&lt;li&gt;查看挂载情况&lt;code&gt;mount | grep ^/&lt;/code&gt;，发现只读&lt;code&gt;ro&lt;/code&gt;，重新挂载为读写&lt;code&gt;mount -o remount,rw /sysroot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;临时切换根目录&lt;code&gt;chroot /sysroot&lt;/code&gt;，为了恢复环境（如补全等功能）；&lt;/li&gt;
&lt;li&gt;查看&lt;code&gt;ls -lZ /etc/shadow&lt;/code&gt;，在此模式下发现没有标签（即SELinux）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo &apos;redhat&apos;| passwd --stdin root&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通知SELinux重建标签&lt;code&gt;touch /.autorelabel&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;退出临时根&lt;code&gt;exit&lt;/code&gt;，退出中断&lt;code&gt;exit&lt;/code&gt;，继续启动过程，SELinux启动会重新打标签，自动重启后正常；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;修复文件系统&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;fstab&lt;/code&gt;的永久挂载信息有误：
&lt;ul&gt;
&lt;li&gt;选择Linux版本时按&lt;code&gt;e&lt;/code&gt;键，在虚拟机环境&lt;code&gt;linux16&lt;/code&gt;删到&lt;code&gt;ro&lt;/code&gt;，输入&lt;code&gt;systemd.unit=emergency.target&lt;/code&gt;（&lt;code&gt;rescue&lt;/code&gt;也可以，打断正常启动），按&lt;code&gt;^x&lt;/code&gt;提交，进入维护模式；&lt;/li&gt;
&lt;li&gt;查看关注情况&lt;code&gt;mount | grep ^/&lt;/code&gt;，发现只读&lt;code&gt;ro&lt;/code&gt;，重新挂载为读写&lt;code&gt;mount -o remount,rw /&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;修改&lt;code&gt;/etc/fstab&lt;/code&gt;，删除错误信息后&lt;code&gt;reboot&lt;/code&gt;，系统正常；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;启动引导程序故障&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;grub&lt;/code&gt;错误，启动Linux立刻报错：
&lt;ul&gt;
&lt;li&gt;个人电脑：设备断电，将硬盘换到正常设备，挂载并&lt;code&gt;chroot&lt;/code&gt;；服务器：使用救援光盘（虚拟机用&lt;code&gt;virt-manager&lt;/code&gt;挂载光驱并加载下载好的镜像）；&lt;/li&gt;
&lt;li&gt;启动按&lt;code&gt;esc&lt;/code&gt;进入启动项，选择光驱启动；&lt;/li&gt;
&lt;li&gt;选择&lt;code&gt;rescue xxx&lt;/code&gt;，选择&lt;code&gt;continue&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;临时切换根目录&lt;code&gt;chroot /mnt/sysimage&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vim /etc/grub2.cfg&lt;/code&gt;查看配置找出错误，或生成默认配置覆盖&lt;code&gt;grub2-mkconfig &amp;gt; /etc/grub2.cfg&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;控制服务及守护进程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;守护进程可以在&lt;code&gt;/usr/lib/systemd/system&lt;/code&gt;中通过&lt;code&gt;ll -d *d.*&lt;/code&gt;大概查看；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;systemctl&lt;/code&gt;控制守护进程及服务，也可以控制系统运行级别；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统相关功能的启动及服务的启动都是通过systemd的system和service manager管理；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Redhat认为，守护进程是一直在后台执行的作业，服务是守护进程的集合；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;进程ID为1的为systemd的进程，&lt;code&gt;systemctl&lt;/code&gt;是systemd的前端命令行；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;systemctl&lt;/code&gt;查询所有单元；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl --type=service&lt;/code&gt;查看服务类单元；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl start|stop xxx.service&lt;/code&gt;临时启停服务；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;systemctl enable|disable xxx.service&lt;/code&gt;永久起停服务；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于目标（target）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;其中的emergency初始化内核（&lt;code&gt;/boot&lt;/code&gt;中vm开头）、初始化内存（&lt;code&gt;/boot&lt;/code&gt;中initramfs开头）、初始化系统根目录（挂载到&lt;code&gt;/&lt;/code&gt;并为只读挂载）；&lt;/li&gt;
&lt;li&gt;其中的rescue模式为单用户，初始化基本的系统环境；&lt;/li&gt;
&lt;li&gt;multi-user模式提供字符接口；&lt;/li&gt;
&lt;li&gt;graphical模式提供字符及图形化接口；&lt;/li&gt;
&lt;li&gt;目标可以包含其他目标，使用&lt;code&gt;systemctl list-dependencies graphical.target&lt;/code&gt;查看目标间的依赖关系；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;配置IPv6&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;回顾&lt;code&gt;NetworkManager&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个device就是一个网络接口；&lt;/li&gt;
&lt;li&gt;一个连接就对应一个配置文件（连接是为设备做的）；&lt;/li&gt;
&lt;li&gt;一个设备可以对应多个连接；同一个时刻一个设备只允许一个连接生效；&lt;/li&gt;
&lt;li&gt;连接的永久配置在&lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-name&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nmcli&lt;/code&gt;用来创建或编辑连接配置；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IPv6有128位，缩写为8组十六进制数字，每组的引导0可以省略，连续的0组可以省略一次，&lt;code&gt;XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX&lt;/code&gt;；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;::1/128 localhost&lt;/code&gt;，回环地址；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;:: 0.0.0.0&lt;/code&gt;，未分配；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;::/0 0.0.0.0/0&lt;/code&gt;，匹配IPv4的所有IP地址；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2000::/3&lt;/code&gt;，全局单播地址；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ffe0::/8&lt;/code&gt;，多播地址；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fe80::/64&lt;/code&gt;，链路多播；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fe80::/64&lt;/code&gt;，链路本地单播；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fddb:fe2a:ab1e::c0a8:1/64&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;IPv6故障排查：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ping6 fddb:fe2a:ab1e::c0a8:fe&lt;/code&gt;，IPv6的ping命令；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tracepath6 fddb:fe2a:ab1e::c0a8:fe&lt;/code&gt;，追踪路由；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;traceroute -6 fddb:fe2a:ab1e::c0a8:fe&lt;/code&gt;，追踪路由的另一种写法；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ip -6 route&lt;/code&gt;，IPv6的路由；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;试验&lt;code&gt;lab ipv6 setup&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先查看所有网卡配置&lt;code&gt;ip a&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;创建连接&lt;code&gt;nmcli connection add con-name eno1 autoconnect yes ifname eno1 type ethernet&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;配置连接，使下次重启生效&lt;code&gt;nmcli connection modify eno1 ipv4.method manual ipv4.addresses &quot;192.168.0.100/24 192.168.0.254&quot; ipv6.method manual ipv6.addresses &quot;fddb:fe2a:ab1e::c0a8:64/64 fddb:fe2a:ab1e::c0a8:fe&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;是配置立即生效&lt;code&gt;nmcli conncetion up eno1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;检查路由&lt;code&gt;ip route&lt;/code&gt;和&lt;code&gt;ip -6 route&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;ping测试&lt;code&gt;ping -I eno1 192.168.0.254&lt;/code&gt;和&lt;code&gt;ping6 -I eno1 fddb:fe2a:ab1e::c0a8:fe&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;跟踪IPv6路由&lt;code&gt;tracepath6 fddb:fe2a:ab1e::c0a8:fe&lt;/code&gt;或&lt;code&gt;traceroute -6 fddb:fe2a:ab1e::c0a8:fe&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;链路聚合与软件网桥&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;将多个物理网卡合并为一个逻辑网卡，做负载均衡、故障转移、增加吞吐量；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;链路聚合&lt;code&gt;lab teambridge setup&lt;/code&gt;；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先查看所有网卡配置&lt;code&gt;ip a&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;新建team0连接&lt;code&gt;nmcli connection add con-name team0 autoconnect yes ifname team0 type team&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;修改team0连接的运行法则&lt;code&gt;nmcli connection modify team0 team.config &apos;{&quot;runner&quot;: {&quot;name&quot;: &quot;activebackup&quot;}}&apos;&lt;/code&gt;（运行法则参考&lt;code&gt;man teamd.conf&lt;/code&gt;）；&lt;/li&gt;
&lt;li&gt;查看的team0连接的运行法则是否配置正确&lt;code&gt;nmcli connection show team0&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;给team0连接添加物理接口：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;nmcli connection add con-name team0-port1 autoconnect yes ifname eno1 type team-slave master team0
nmcli connection add con-name team0-port2 autoconnect yes ifname eno2 type team-slave master team0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;查看添加的物理接口：&lt;code&gt;nmcli connection show&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;为team0连接配置IPv4：&lt;code&gt;nmcli connection modify team0 ipv4.method manual ipv4.address &quot;192.168.0.100/24 192.168.0.254&quot;&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;启动team0连接：&lt;code&gt;nmcli connection up team0&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;测试连接&lt;code&gt;ping -I team0 192.168.0.254&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;检查运行聚合连接team0的运行状态：&lt;code&gt;teamdctl team0 state&lt;/code&gt;，可以看到只有一个接口处于活动状态，则另一个接口处于预备状态；&lt;/li&gt;
&lt;li&gt;测试关闭物理接口：&lt;code&gt;nmcli device disconnect eno1&lt;/code&gt;（旧命令&lt;code&gt;ifdown eno1&lt;/code&gt;），备用接口立刻上线；&lt;/li&gt;
&lt;li&gt;配置在&lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-team0&lt;/code&gt;、&lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-team0-port1&lt;/code&gt;、&lt;code&gt;/etc/sysconfig/network-scripts/ifcfg-team0-port2&lt;/code&gt;中；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;软件网桥常用于虚拟化平台，在物理机上&lt;code&gt;brctl show&lt;/code&gt;查看：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先查看所有网卡配置&lt;code&gt;ip a&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;删除上一节的配置：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;nmcli connection delete team0
nmcli connection delete team0-port1
nmcli connection delete team0-port2
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;新建br0网桥：；&lt;/li&gt;
&lt;li&gt;给br0网桥添加物理接口：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;nmcli connection add con-name br0-port1 autoconnect yes ifname eno1 type bridge=slave master br0
nmcli connection add con-name br0-port2 autoconnect yes ifname eno2 type bridge-slave master br0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;给br0网桥配置IPv4：&lt;code&gt;nmcli connection modify br0 ipv4.method manual ipv4.addresses &quot;192.168.0.100/24 192.168.0.254&quot;&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;查看链接&lt;code&gt;nmcli connection show&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;启动端口：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;nmcli connection up br0-port1
nmcli connection up br0-port2
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;测试连接：&lt;code&gt;ping -I br0 192.168.0.254&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;若连接不通，重启网络服务：&lt;code&gt;systemctl restart network.service&lt;/code&gt;，若仍然不通，重启虚拟机；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;nmcli&lt;/code&gt;的配置建议参考&lt;code&gt;man 5 nmcli-example&lt;/code&gt;，范例丰富；&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;/2018/07/21/redhat-1/&quot;&gt;Redhat Linux初体验（1）&lt;/a&gt;
&lt;a href=&quot;/2018/08/03/redhat-3/&quot;&gt;Redhat Linux初体验（3）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>Redhat Linux初体验（1）</title><link>https://zlotus.github.io/2018/07/21/redhat-1/</link><guid isPermaLink="true">https://zlotus.github.io/2018/07/21/redhat-1/</guid><description>Redhat系Linux基础学习</description><pubDate>Sat, 21 Jul 2018 11:22:47 GMT</pubDate><content:encoded>&lt;p&gt;这三周都被拐卖到成都培训RHCE，讲真，还是当学生好啊~哈哈哈~把学习笔记搬上来，关键时刻能做个速查~&lt;/p&gt;
&lt;p&gt;献上第一周的笔记。&lt;/p&gt;
&lt;h1&gt;2018-07-16&lt;/h1&gt;
&lt;h2&gt;Linux历史&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Multics的产生&lt;/li&gt;
&lt;li&gt;Unix的产生&lt;/li&gt;
&lt;li&gt;BSD与GUN等开源软件的发展&lt;/li&gt;
&lt;li&gt;Linux的发展与发行现状&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;教学环境简介&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;安装vnc以及使用vnc远程教师机器&lt;/li&gt;
&lt;li&gt;红帽虚拟机使用管理：&lt;code&gt;rht-vmctl [start|view|reset|stop]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;红帽虚拟机配置管理：&lt;code&gt;virt-manager&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过伪终端连接虚拟机：&lt;code&gt;ssh student@desktop4&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-07-17&lt;/h1&gt;
&lt;h2&gt;常用基础命令&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;修改密码&lt;code&gt;passwd&lt;/code&gt;的策略；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;date&lt;/code&gt;修改、显示时间命令，以及命令行转义；&lt;/li&gt;
&lt;li&gt;文件查看相关命令：&lt;code&gt;file&lt;/code&gt;、&lt;code&gt;wc&lt;/code&gt;、&lt;code&gt;head&lt;/code&gt;、&lt;code&gt;tail&lt;/code&gt;，文本编辑器&lt;code&gt;nano&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;history&lt;/code&gt;操作命令历史记录，使用&lt;code&gt;!&lt;/code&gt;或&lt;code&gt;^r&lt;/code&gt;快速逆向查找历史命令，使用&lt;code&gt;esc+.&lt;/code&gt;或&lt;code&gt;!$&lt;/code&gt;复制上条命令的参数，使用&lt;code&gt;!!&lt;/code&gt;复现上一条命令；&lt;/li&gt;
&lt;li&gt;命令行光标操作：光标移至行首&lt;code&gt;^a&lt;/code&gt;、光标移至行尾&lt;code&gt;^e&lt;/code&gt;、删除光标之前&lt;code&gt;^u&lt;/code&gt;、删除光标之后&lt;code&gt;^k&lt;/code&gt;、删除前一个参数&lt;code&gt;^w&lt;/code&gt;、&lt;code&gt;^l&lt;/code&gt;清屏、&lt;code&gt;^Y&lt;/code&gt;恢复&lt;code&gt;^u&lt;/code&gt;或&lt;code&gt;^w&lt;/code&gt;擦除的命令、&lt;code&gt;^\&lt;/code&gt;撤销操作；&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;文件操作&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;文件系统树形结构：&lt;code&gt;/, /bin, /boot, /dev, /etc, /etc/opt, /etc/sgml, /etc/X11, /home, /lib, /media, /mnt, /opt, /proc, /root, /sbin, /srv, /tmp, /usr, /var&lt;/code&gt;的功能介绍；&lt;/li&gt;
&lt;li&gt;文件系统路径，区分相对路径、绝对路径，使用&lt;code&gt;cd -&lt;/code&gt;切换到上一次使用的路径下&lt;/li&gt;
&lt;li&gt;新建文件：&lt;code&gt;touch&lt;/code&gt;的本意是用来更新文件的时间戳，也用来新建文件；使用&lt;code&gt;mkdir -p&lt;/code&gt;创建一系列路径；&lt;code&gt;ls -R&lt;/code&gt;递归现实目录内容，类似&lt;code&gt;tree&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;删除文件：&lt;code&gt;rmdir&lt;/code&gt;不常用，只能删除非空目录；&lt;code&gt;rm&lt;/code&gt;常用，可删除文件或目录，&lt;code&gt;-r&lt;/code&gt;选项可递归删除&lt;/li&gt;
&lt;li&gt;修改文件：使用&lt;code&gt;cp&lt;/code&gt;复制文件，&lt;code&gt;-r&lt;/code&gt;递归复制，&lt;code&gt;*&lt;/code&gt;通配复制（不可用于子目录）；使用&lt;code&gt;mv&lt;/code&gt;移动文件，&lt;code&gt;mv&lt;/code&gt;可用于重命名；&lt;/li&gt;
&lt;li&gt;文件通配符：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt;匹配任意字符任意长度；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt;匹配任意字符1长度；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt;匹配指定字符1长度；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;touch file{start..stop..step}&lt;/code&gt;生成多个文件，类似python的&lt;code&gt;range&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ll -d ?????&lt;/code&gt;匹配5位长度的文件名，&lt;code&gt;ll -d [abc]?????&lt;/code&gt;匹配abc开头后接5位的文件名，&lt;code&gt;ll -d *[0-9]*&lt;/code&gt;匹配包含数字的文件名；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ll [[::upper:]]*&lt;/code&gt;匹配大写字母开头的所有文件，具体匹配参见&lt;code&gt;man 7 glob&lt;/code&gt;（不建议使用&lt;code&gt;[A-Z]&lt;/code&gt;，在不同的字符集中A-Z之间的编码字符不一样）；字符集中&lt;code&gt;^&lt;/code&gt;或&lt;code&gt;!&lt;/code&gt;（如&lt;code&gt;[!0-9]&lt;/code&gt;）表示取反；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-07-18&lt;/h1&gt;
&lt;h2&gt;获取帮助&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Unix帮助：使用&lt;code&gt;man man&lt;/code&gt;查看man帮助格式（包括8个必选1个可选章节），使用&lt;code&gt;man 章节数 命令&lt;/code&gt;查看相应命令的详细帮助信息，使用&lt;code&gt;man -k fd&lt;/code&gt;搜索所有fd相关帮助&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;ls --help&lt;/code&gt;、&lt;code&gt;ls --info&lt;/code&gt;或&lt;code&gt;help ls&lt;/code&gt;、&lt;code&gt;info ls&lt;/code&gt;获取简要操作指南，&lt;/li&gt;
&lt;li&gt;在man中使用&lt;code&gt;-N CR&lt;/code&gt;命令显示行号&lt;/li&gt;
&lt;li&gt;GNU帮助：使用&lt;code&gt;info info&lt;/code&gt;查看info帮助各式，&lt;code&gt;pinfo&lt;/code&gt;查看易读的info格式，若命令未提供info帮助，则info会将man帮助显示出来&lt;/li&gt;
&lt;li&gt;man手册位于&lt;code&gt;/usr/share/man&lt;/code&gt;，info手册位于&lt;code&gt;/usr/share/info&lt;/code&gt;，第三方软件的帮助位于&lt;code&gt;/usr/share/doc&lt;/code&gt;，使用&lt;code&gt;rpm -ql httpd | grep /usr/share&lt;/code&gt;查看第三方软件httpd的帮助文件安装情况&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;文本文件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;cat&lt;/code&gt;可以连接输出多个文件，&lt;code&gt;head&lt;/code&gt;或&lt;code&gt;tail&lt;/code&gt;则会将文件分开输出，&lt;code&gt;cat -A&lt;/code&gt;输出换行/制表等控制字符；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;more、less&lt;/code&gt;分屏显示文件，&lt;code&gt;less&lt;/code&gt;功能更强大，&lt;code&gt;man&lt;/code&gt;的输出默认调用&lt;code&gt;less&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;重定向，&lt;code&gt;man stdin&lt;/code&gt;查看；&lt;code&gt;/dev&lt;/code&gt;中的&lt;code&gt;stdin/out/err&lt;/code&gt;均指向&lt;code&gt;fd&lt;/code&gt;（文件描述符），&lt;code&gt;ll -i std*&lt;/code&gt;发现输出最前多了一列index node，即文件索引节点，使用&lt;code&gt;tty&lt;/code&gt;查看当前终端的文件描述符，&lt;code&gt;echo hello1 1&amp;gt; /dev/pts/2&lt;/code&gt;可以在另一个终端上看到输出；&lt;/li&gt;
&lt;li&gt;输入重定向：&lt;code&gt;tr &apos;[:lower:]&apos; &apos;[:upper:]&apos; 0&amp;lt;.bashrc&lt;/code&gt;，将&lt;code&gt;.bashrc&lt;/code&gt;的内容重定向到&lt;code&gt;stdin&lt;/code&gt;传给&lt;code&gt;tr&lt;/code&gt;进行大小写转换&lt;/li&gt;
&lt;li&gt;输出重定向：&lt;code&gt;&amp;gt;&lt;/code&gt;覆盖，&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;追加&lt;/li&gt;
&lt;li&gt;管道&lt;code&gt;|&lt;/code&gt;，连接前一个命令的&lt;code&gt;stdout&lt;/code&gt;到后一个命令的&lt;code&gt;stdin&lt;/code&gt;；&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;vim&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;^u&lt;/code&gt;向上&lt;code&gt;^d&lt;/code&gt;向下，&lt;code&gt;ZZ&lt;/code&gt;保存退出，&lt;code&gt;ZQ&lt;/code&gt;不保存退出；&lt;/li&gt;
&lt;li&gt;插入模式、命令模式、可视模式（仅用于选择）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;v&lt;/code&gt;按字符选择、&lt;code&gt;V&lt;/code&gt;按行选择、&lt;code&gt;^v&lt;/code&gt;按块选择；&lt;/li&gt;
&lt;li&gt;字符级移动&lt;code&gt;hjkl&lt;/code&gt;，&lt;code&gt;10j&lt;/code&gt;向后移动10行；&lt;code&gt;0&lt;/code&gt;绝对行首，&lt;code&gt;^&lt;/code&gt;非空行首，&lt;code&gt;$&lt;/code&gt;（行尾）；&lt;/li&gt;
&lt;li&gt;句级移动：&lt;code&gt;(&lt;/code&gt;前一句，&lt;code&gt;)&lt;/code&gt;后一句；&lt;/li&gt;
&lt;li&gt;段级移动：&lt;code&gt;{&lt;/code&gt;前一段，&lt;code&gt;}&lt;/code&gt;后一段；&lt;/li&gt;
&lt;li&gt;单词级移动：&lt;code&gt;w&lt;/code&gt;下一个词首），&lt;code&gt;e&lt;/code&gt;下一个词尾，&lt;code&gt;b&lt;/code&gt;前一个词首；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;#&lt;/code&gt;查找相同单词，可以使用&lt;code&gt;n&lt;/code&gt; &lt;code&gt;N&lt;/code&gt;在该单词的搜索结果中跳转；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d&lt;/code&gt;删除，&lt;code&gt;d10w&lt;/code&gt;删除后十个单词，&lt;code&gt;d10l&lt;/code&gt;删除后十个字符；&lt;code&gt;c&lt;/code&gt;删除并插入；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dd&lt;/code&gt;删除行；&lt;code&gt;S&lt;/code&gt;删除行并插入；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x&lt;/code&gt;删除字符；&lt;code&gt;s&lt;/code&gt;删除字符并插入；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;y&lt;/code&gt;复制；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt;粘贴光标后，&lt;code&gt;P&lt;/code&gt;粘贴光标前；&lt;/li&gt;
&lt;li&gt;插入模式：&lt;code&gt;i&lt;/code&gt;（插入光标前）&lt;code&gt;a&lt;/code&gt;（插入光标后）&lt;code&gt;I&lt;/code&gt;（行首插入）&lt;code&gt;A&lt;/code&gt;（行尾插入）&lt;code&gt;o&lt;/code&gt;（下行插入）&lt;code&gt;O&lt;/code&gt;（上行插入）&lt;code&gt;R&lt;/code&gt;（改写）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.&lt;/code&gt;重复上一条命令；&lt;/li&gt;
&lt;li&gt;命令行模式，&lt;code&gt;set all&lt;/code&gt;查看所有命令，直接输数字回车移动到相应行（&lt;code&gt;10CR&lt;/code&gt;），命令&lt;code&gt;10G&lt;/code&gt;也可以；&lt;/li&gt;
&lt;li&gt;命令行模式：&lt;code&gt;10,15d&lt;/code&gt;（删除10-15行），&lt;code&gt;.,+3d&lt;/code&gt;删除1+3行（&lt;code&gt;.&lt;/code&gt;光标所在行，&lt;code&gt;\$&lt;/code&gt;最后一行，&lt;code&gt;%&lt;/code&gt;所有行）；&lt;/li&gt;
&lt;li&gt;命令行模式：&lt;code&gt;10,20s#nologin#login#&lt;/code&gt;（10到20行第一个nologin换login）；&lt;code&gt;10,20s#n#N#gc&lt;/code&gt;（10到20行n换N，询问模式）；&lt;code&gt;10,20s#n#A#gic&lt;/code&gt;（10到20行n换A忽略大小写，询问模式）；&lt;/li&gt;
&lt;li&gt;命令行模式：&lt;code&gt;r abc.txt&lt;/code&gt;（将abc.txt追加到光布行后面）&lt;code&gt;o abc.txt&lt;/code&gt;（打开abc.txt）；&lt;/li&gt;
&lt;li&gt;命令行模式：&lt;code&gt;!&lt;/code&gt;（执行外部命令），&lt;code&gt;r !hostname&lt;/code&gt;（将hostname的输出读入到光标下一行）；&lt;/li&gt;
&lt;li&gt;命令行模式：&lt;code&gt;first&lt;/code&gt; &lt;code&gt;prev&lt;/code&gt; &lt;code&gt;next&lt;/code&gt; &lt;code&gt;last&lt;/code&gt;在多个打开的文件中切换；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u&lt;/code&gt;撤销，&lt;code&gt;^r&lt;/code&gt;重复；&lt;/li&gt;
&lt;li&gt;多文件打开&lt;code&gt;vim -o a b c d&lt;/code&gt;多文件水平分屏，&lt;code&gt;-O&lt;/code&gt;竖直分屏，使用&lt;code&gt;^w&lt;/code&gt;在多屏间切换，命令行下&lt;code&gt;split&lt;/code&gt;为水平分屏，&lt;code&gt;hsplit&lt;/code&gt;为竖直分屏；&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在root用户下，&lt;code&gt;vi&lt;/code&gt;与&lt;code&gt;vim&lt;/code&gt;是两个程序，&lt;code&gt;vim&lt;/code&gt;更强大，语法高亮只有在特定位置自动渲染&lt;/p&gt;
&lt;h1&gt;2018-07-19&lt;/h1&gt;
&lt;h2&gt;本地用户&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;uid 0 分配给root用户；uid 1-999 分配给应用程序，通常不用来登录计算机，为内置账号；uid &amp;gt; 1000 分配给普通用户&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;/etc/passwd&lt;/code&gt;存储用户配置；&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;useradd user1&lt;/code&gt;添加用户；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;id user1&lt;/code&gt;现实uid（用户id）/gid（私有组）/group（附加组）信息；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;useradd -u 8888 user2&lt;/code&gt;指定UID；使用&lt;code&gt;useradd -d&lt;/code&gt;指定home目录，使用&lt;code&gt;useradd -s&lt;/code&gt;指定shell；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;passwd user1&lt;/code&gt;设置密码；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;su - user1&lt;/code&gt;切换用户；&lt;/li&gt;
&lt;li&gt;可以分配的shell在&lt;code&gt;/etc/shells&lt;/code&gt;中；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;普通用户均有home目录和登录程序（shell），&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;查看&lt;code&gt;tail -n 2 /etc/{passwd,shadow,group,gshadow}&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;passwd保存用户信息；&lt;/li&gt;
&lt;li&gt;shadow保存用户密码信息；&lt;/li&gt;
&lt;li&gt;group保存组信息，新建用户时，若不指定，系统将用户分配至同名组，该组为私有组，gid分配方式与uid相同；微软使用公有组机制，比如Windows中新建用户统一加入users组；红帽使用私有组，部分Linux也使用公有组；用户除了自己的私有组，可以使用&lt;code&gt;groupadd group1&lt;/code&gt;新建组，使用&lt;code&gt;usermod -aG group1 user1&lt;/code&gt;给user1添加附加组group1（&lt;code&gt;a&lt;/code&gt;写在前面，表示追加，若不写则会覆盖原来的附加组，&lt;code&gt;G&lt;/code&gt;是要加参数的选项，放在后面）&lt;/li&gt;
&lt;li&gt;gshadow保存组密码信息；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理用户（必须使用root权限）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;图形管理界面&lt;code&gt;sudo system-config-users&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;命令行添加用户&lt;code&gt;useradd&lt;/code&gt;，使用&lt;code&gt;passwd&lt;/code&gt;设置密码；&lt;/li&gt;
&lt;li&gt;命令行修改用户&lt;code&gt;usermod&lt;/code&gt;，修改home目录时需使用&lt;code&gt;-md&lt;/code&gt;（&lt;code&gt;m&lt;/code&gt;移动home目录，放在&lt;code&gt;d&lt;/code&gt;之前），home目录下默认文件均从&lt;code&gt;/etc/skel&lt;/code&gt;复制而来；&lt;code&gt;-L&lt;/code&gt;锁定用户，&lt;code&gt;-U&lt;/code&gt;解锁用户；&lt;/li&gt;
&lt;li&gt;命令行删除用户&lt;code&gt;userdel&lt;/code&gt;，&lt;code&gt;-r&lt;/code&gt;表示删除home目录&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo&lt;/code&gt;的权限写在&lt;code&gt;/etc/sudoer&lt;/code&gt;，&lt;code&gt;visudo&lt;/code&gt;是专门用于修改&lt;code&gt;/etc/sudoer&lt;/code&gt;的程序，使用&lt;code&gt;man 5 sudoers&lt;/code&gt;查看具体格式；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chage -l user1&lt;/code&gt;查看用户密码相关易读的时间信息（上次密码更改日期、密码过期日期、账户过期时间等）；使用&lt;code&gt;chage&lt;/code&gt;修改这些信息；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/etc/login.defs&lt;/code&gt;为新建用户时的默认配置，可以修改这些配置以更改新建用户时的行为；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;管理组（必须使用root权限）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;groupadd -r&lt;/code&gt;为创建系统内置组；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;groupmems&lt;/code&gt;也可以用来给用户添加组，如使用&lt;code&gt;-a&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man 5 passwd&lt;/code&gt;和&lt;code&gt;man 5 shadow&lt;/code&gt;查看用户密码信息，&lt;code&gt;man 3 crypt&lt;/code&gt;查看加密算法；&lt;code&gt;man 5 group&lt;/code&gt;和&lt;code&gt;ma 5 shadow&lt;/code&gt;查看组密码信息；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;文件权限&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;文件被访问时，首先比对文件权限与&lt;strong&gt;用户&lt;/strong&gt;权限，如果不符则比对&lt;strong&gt;用户属组&lt;/strong&gt;权限，如果不符则比对&lt;strong&gt;其他用户&lt;/strong&gt;权限；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文件权限：&lt;code&gt;r&lt;/code&gt;读&lt;code&gt;w&lt;/code&gt;写（修改删除或在目录中增删改文件）&lt;code&gt;x&lt;/code&gt;执行（执行或进入目录）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一位，文件类型；&lt;/li&gt;
&lt;li&gt;234位：文件属主具有的权限；&lt;/li&gt;
&lt;li&gt;567位：文件属组具有的权限；&lt;/li&gt;
&lt;li&gt;89A位：文件属主之外的其他用户具有的权限；&lt;/li&gt;
&lt;li&gt;比如root用户&lt;code&gt;vim /etc/passwd&lt;/code&gt;为读写，普通用户显示只读（&lt;code&gt;top&lt;/code&gt;中&lt;code&gt;L&lt;/code&gt;可以搜索进程）；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改权限，&lt;code&gt;ll -d /test&lt;/code&gt;进查看目录而不进入目录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;chmod a= /test&lt;/code&gt;（&lt;code&gt;a&lt;/code&gt;代表all）取消test目录的所有权限，root用户依然可以进入，权限仅对root用户无效，文件属主对文件永远具有写权限（即使显示readonly）；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;chmod o=x /test&lt;/code&gt;（&lt;code&gt;o&lt;/code&gt;代表other）给其他用户赋予进入目录的权限；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;chmod o-x /test&lt;/code&gt;取消其他用户的进入权限；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;chmod o=r /test&lt;/code&gt;赋予其他用户列出目录中文件的权限，如果仅有r而没有x则只能读出文件名，其他属性均无法访问，因此应再使用&lt;code&gt;chmod o+x /test&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;chmod o=wx /test&lt;/code&gt;赋予其他用户；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chmod&lt;/code&gt;中&lt;code&gt;ugo&lt;/code&gt;代表修改文件属主、文件属组、属主外其他用户的权限，&lt;code&gt;a&lt;/code&gt;表示修改&lt;code&gt;ugo&lt;/code&gt;全部；&lt;code&gt;+-=&lt;/code&gt;表示增、删、赋值，各种组合可以使用&lt;code&gt;,&lt;/code&gt;分割，如&lt;code&gt;ugo=r,u+w&lt;/code&gt;（应注意赋值覆盖增删）；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;修改所属关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;chown user1:user1 file&lt;/code&gt;修改文件所属用户及所属用户属组，&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;-R&lt;/code&gt;修改目录下所有文件的权限；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;umask&lt;/code&gt;的修改涉及到交互式登录与非交互登录脚本的加载，通常修改umask只能在本次会话中生效，若要长期生效，需要修改根据登录脚本加载顺序来修改各登录脚本中的umask的赋值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;登录脚本在&lt;code&gt;/etc/profile&lt;/code&gt;（系统交互式初始化脚本）、&lt;code&gt;~/.bash_profile&lt;/code&gt;（个人交互式初始化脚本）、&lt;code&gt;/etc/bashrc&lt;/code&gt;（系统非交互式登录脚本）、&lt;code&gt;~/.bashrc&lt;/code&gt;（个人非交互式初始化脚本）；&lt;/li&gt;
&lt;li&gt;关于脚本启动顺序，详情参见&lt;code&gt;man bash&lt;/code&gt;，搜索&lt;code&gt;profile&lt;/code&gt;，INVOCATION章节：&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可执行文件的特殊权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SUID：设置后按照文件&lt;strong&gt;拥有者&lt;/strong&gt;的权限运行（文件的普通权限是按照文件&lt;strong&gt;执行者&lt;/strong&gt;的权限运行，比如user1 运行&lt;code&gt;touch test&lt;/code&gt;，则&lt;code&gt;ll test&lt;/code&gt;显示归属&lt;code&gt;user1 user1&lt;/code&gt;）；s权限意味着原来为x，现在为s+x，S权限意味着原来为s；运行&lt;code&gt;chmod u+s touch&lt;/code&gt;后再使用&lt;code&gt;touch test&lt;/code&gt;，则&lt;code&gt;ll test&lt;/code&gt;归属显示&lt;code&gt;root user1&lt;/code&gt;，最常见的属主具有s权限的就是&lt;code&gt;passwd&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;SGID：再运行&lt;code&gt;chmod g+s touch&lt;/code&gt;后再使用&lt;code&gt;touch test&lt;/code&gt;，则&lt;code&gt;ll test&lt;/code&gt;归属显示&lt;code&gt;root root&lt;/code&gt;，常为协作类可执行程序设置；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;目录的特殊权限：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SGID：使用&lt;code&gt;chmod g+s test&lt;/code&gt;，使得目录具有s权限后，在目录中新建的任何文件，其属组均会被设置为该目录的属组，常用于协作目录；（目录的SUID(&lt;code&gt;u+s&lt;/code&gt;)是没有任何效果的）&lt;/li&gt;
&lt;li&gt;t权限：针对上一条，设置了&lt;code&gt;g+s&lt;/code&gt;的目录，就算不是文件拥有者，也可以删除组内其他人的文件，而对目录使用&lt;code&gt;chmod o+t test&lt;/code&gt;后，仅有文件拥有者才能删除该文件；&lt;/li&gt;
&lt;li&gt;t权限仅对目录有效；&lt;/li&gt;
&lt;li&gt;umask的第一位即为特殊权限：&lt;code&gt;4&lt;/code&gt;为SUID&lt;code&gt;u+s&lt;/code&gt;、&lt;code&gt;2&lt;/code&gt;为SGID&lt;code&gt;g+s&lt;/code&gt;、&lt;code&gt;1&lt;/code&gt;为t权限；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;2018-07-20&lt;/h1&gt;
&lt;h2&gt;进程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;启动顺序：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;poweron-&amp;gt;
motherboard cmos(bios|uefi)-&amp;gt;
(F11|F12)sda(mbr|gpt)-&amp;gt;
grub2-&amp;gt;
vmliuz(kernel)-&amp;gt;
systemd-&amp;gt;
...GUI(gnome-terminal|gnome-gdm|kde|xfce)...或CLI(agetty)...或network(sshd)
grub2的配置文件在/boot/grub2/grub.cfg
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;通过&lt;code&gt;pstree&lt;/code&gt;可以查看进程树形结构（&lt;code&gt;-p&lt;/code&gt;可以对应进程号），反映了启动顺序（&lt;code&gt;ps -up 进程号&lt;/code&gt;现实进程详情）；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通常的，由用户登录后发起的进程成为作业（jobs），而直接由systemd启动的进程成为服务（service），使用&lt;code&gt;ps j&lt;/code&gt;查看所有作业，在终端执行的命令会随着终端的关闭而结束；比如在使用&lt;code&gt;ssh&lt;/code&gt;连接的终端上执行&lt;code&gt;dd if=/dev/zero of=/dev/null&lt;/code&gt;复制大文件，如果网络不稳定，sshd会认为远端断开连接，而结束本终端下的所有进程；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果在进程运行时使用&lt;code&gt;^z&lt;/code&gt;挂起进程（&lt;code&gt;^c&lt;/code&gt;终止进程），使用&lt;code&gt;jobs&lt;/code&gt;可以查看当前终端下的作业进程及其&lt;strong&gt;作业号&lt;/strong&gt;（&lt;code&gt;jobs -l&lt;/code&gt;可显示进程号，作业号是bash分配的，而进程号是系统分配的），使用&lt;code&gt;bg&lt;/code&gt;可以在后台继续该进程，使用&lt;code&gt;fg&lt;/code&gt;可以将后台程序移至前台，使用&lt;code&gt;fg&lt;/code&gt; &lt;code&gt;bg&lt;/code&gt;后接&lt;strong&gt;作业号&lt;/strong&gt;可以选择要操作的对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;dd if=/dev/zero of=/dev/null&amp;amp;&lt;/code&gt;运行程序将直接后台运行，终端内执行的进程通常会在终端被关闭时结束（按照树形结构），使用&lt;code&gt;nohup dd if=/dev/zero of=/dev/null&amp;amp;&lt;/code&gt;将转为终端无关进程，使得在终端关闭后也可以继续执行，使用&lt;code&gt;ps -p 进程号&lt;/code&gt;可以查看到该进程的tty值为&lt;code&gt;?&lt;/code&gt;，即终端无关；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;ps&lt;/code&gt;查看进程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ps&lt;/code&gt;仅能看到当前终端的作业，类似&lt;code&gt;jobs&lt;/code&gt;仅多一个shell进程；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps a&lt;/code&gt;显示所有终端下的作业（即显示终端相关进程），与&lt;code&gt;ps j&lt;/code&gt;类似；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps x&lt;/code&gt;显示终端相关或无关的所有进程；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps u&lt;/code&gt;表示第一列显示用户，经常使用的就是&lt;code&gt;ps aux&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps f&lt;/code&gt;表示按树形结构显示，&lt;code&gt;m&lt;/code&gt;显示线程信息，&lt;code&gt;s&lt;/code&gt;显示信号格式（具体信号格式可以使用&lt;code&gt;man 7 signal&lt;/code&gt;查看详情）；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ps -e&lt;/code&gt;显示所有进程，&lt;code&gt;ps -f&lt;/code&gt;表示长格式，&lt;code&gt;-F&lt;/code&gt;为更长的格式；&lt;/li&gt;
&lt;li&gt;VSZ为虚拟内存大小，RSS为常驻内存大小，状态码：D深睡眠、R运行中或可运行（或在排队）、S浅睡眠、T打断、X完全死亡（终止）、Z僵尸态（部分资源未释放），BSD格式状态码：&amp;lt;高优先级、N低优先级、L有锁定内存、s会话的首进程、l多线程、+前台进程&lt;/li&gt;
&lt;li&gt;PID为进程号，PPID为父进程号（systemd的PID永远是0)，C为CPU使用率，STIME为进程启动时间，TIME为累计CPU时间（包括排队及运行时间）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;kill&lt;/code&gt;终止进程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;kill -l&lt;/code&gt;查看支持的所有信号，其中&lt;code&gt;9 SIGKILL&lt;/code&gt;（直接终止）、&lt;code&gt;15 SIGTERM&lt;/code&gt;（正常终止，推荐）、&lt;code&gt;1 SIGHUP&lt;/code&gt;（挂起）、&lt;code&gt;19 SIGSTOP&lt;/code&gt;（停止）、&lt;code&gt;18 SIGCONT&lt;/code&gt;（继续）、&lt;code&gt;10 SIGUSR1&lt;/code&gt;和&lt;code&gt;12 SIGUSR2&lt;/code&gt;为自定义信号；（详情参见&lt;code&gt;man 7 signal&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;推荐使用字符命令方式，因为跨硬件平台通用字符信号，而不一定通数字信号；&lt;/li&gt;
&lt;li&gt;常见用法：&lt;code&gt;kill -s SIGTERM 2076&lt;/code&gt;或&lt;code&gt;kill -SIGTERM 2076&lt;/code&gt;或&lt;code&gt;kill -TERM 2076&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kill -s STOP 2088&lt;/code&gt;后&lt;code&gt;ps 2088&lt;/code&gt;显示状态为&lt;code&gt;T&lt;/code&gt;，而&lt;code&gt;kill -s CONT 2088&lt;/code&gt;后&lt;code&gt;ps 2088&lt;/code&gt;显示状态为&lt;code&gt;R&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;同一个终端中，&lt;code&gt;kill -s KILL %1&lt;/code&gt;表示终止作业号为1的作业，不推荐使用；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pkill&lt;/code&gt;会使用&lt;code&gt;pgrep&lt;/code&gt;，对进程进行查询并终止，如使用&lt;code&gt;pkill -TERM -u user1&lt;/code&gt;终止user1下的所有进程（对于ssh登录的用户会导致直接退出远程连接），结束终端pts/0下的所有进程&lt;code&gt;pkill -TERM -t pts/0&lt;/code&gt;，结束以PID 2226为首进程的所有进程&lt;code&gt;pkill -KILL -s 2226&lt;/code&gt;（&lt;code&gt;ps jf&lt;/code&gt;中的&lt;code&gt;s&lt;/code&gt;状态的首进程，通常是bash进程，&lt;code&gt;TERM&lt;/code&gt;信号不一定能够终止bash进程，推荐用&lt;code&gt;KILL&lt;/code&gt;信号）；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;top&lt;/code&gt;跟踪进程：&lt;code&gt;L&lt;/code&gt;表示查询，&lt;code&gt;1&lt;/code&gt;开关CPU详细信息、&lt;code&gt;t&lt;/code&gt;开关任务及CPU资源、&lt;code&gt;l&lt;/code&gt;开关概况、&lt;code&gt;m&lt;/code&gt;开关内存详细信息、PgUp和PgDn翻页、&lt;code&gt;X&lt;/code&gt;指定排序列（从0开始）、&lt;code&gt;d&lt;/code&gt;设置更新间隔、&lt;code&gt;F&lt;/code&gt;定义列、&lt;code&gt;V&lt;/code&gt;显示树形结构；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如&lt;code&gt;top&lt;/code&gt;、&lt;code&gt;vim&lt;/code&gt;是交互进程，而&lt;code&gt;ls&lt;/code&gt;、&lt;code&gt;touch&lt;/code&gt;为非交互进程；&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;服务&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;服务有一个或多个进程构成，通常进程都是终端无关的非交互进程；通常命名中以d结尾的进程都服务端，如&lt;code&gt;sshd&lt;/code&gt;、&lt;code&gt;httpd&lt;/code&gt;等；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;systemctl status firewalld.service&lt;/code&gt;查看防火墙服务状态，使用&lt;code&gt;systemctl start httpd&lt;/code&gt;启用http服务，此时由于防火墙暂时无法从外部访问，使用&lt;code&gt;firewall-cmd --add-service=http&lt;/code&gt;临时添加规则，此时http可以访问，使用&lt;code&gt;systemctl status httpd&lt;/code&gt;查看服务状态；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;systemctl enable httpd.service&lt;/code&gt;启用http服务，使用&lt;code&gt;systemctl disable firewalld.service&lt;/code&gt;关闭防火墙服务，重启立刻生效；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;参考&lt;code&gt;man systemd&lt;/code&gt;；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;systemctl&lt;/code&gt;查看已加载内存的服务单元，所有的程序的启动脚本位于&lt;code&gt;/usr/lib/systemd/system/&lt;/code&gt;，使用&lt;code&gt;systemctl list-unit-files&lt;/code&gt;查看系统管理的所有服务；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;systemctl -t&lt;/code&gt;后使用&lt;code&gt;TAB&lt;/code&gt;补全可以看到systemctl对服务的分类，服务的类型就是服务名的后缀，如&lt;code&gt;httpd.service&lt;/code&gt;就是service类型，&lt;code&gt;systemctl -a&lt;/code&gt;显示所有应该启动的服务（或&lt;code&gt;systemctl list-unit&lt;/code&gt;），&lt;code&gt;systemctl --failed&lt;/code&gt;显示启动失败的服务，&lt;code&gt;systemctl status httpd.service&lt;/code&gt;显示服务详细信息；若服务有多个进程，通常子进程是无法全部结束的，因为主进程一直负责维护子进程，结束主进程才会结束其他子进程，但是不推荐使用&lt;code&gt;kill -TERM 2088&lt;/code&gt;结束服务；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;systemctl is-active sshd&lt;/code&gt;或&lt;code&gt;systemctl is-enabled sshd&lt;/code&gt;显示sshd服务的设置状态；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;服务控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动&lt;code&gt;systemctl start httpd.service&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;停止&lt;code&gt;systemctl stop httpd.service&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;重启&lt;code&gt;systemctl restart httpd.service&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;重载配置&lt;code&gt;systemctl reload httpd.service&lt;/code&gt;（不停服务，如修改配置后）；&lt;/li&gt;
&lt;li&gt;启用&lt;code&gt;systemctl enable httpd.service&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;停用&lt;code&gt;systemctl disable httpd.service&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;ssh&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;netstat -tnp&lt;/code&gt;查看本机已经建立的连接，&lt;code&gt;w&lt;/code&gt;命令可以查看本机登录的账号，&lt;code&gt;w -f&lt;/code&gt;可以看懂从哪个IP登录的；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;教学环境中&lt;code&gt;ssh desktop4&lt;/code&gt;时，真实环境使用的是&lt;code&gt;kiosk&lt;/code&gt;用户，登录后发现虚拟环境的用户为&lt;code&gt;student&lt;/code&gt;，是因为真实环境的kiosk用户与虚拟环境的student的uid均为1000；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在当前用户的&lt;code&gt;~/.ssh/known_hosts&lt;/code&gt;里的记录格式为“主机名,IP地址 加密算法 指纹”，用来对已知主机进行唯一性验证，防止中间人攻击；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;非对称加密传输文件时，通常用密码将文件对称加密并，再使用接收方的公钥将对称加密的密码加密，最终将密码发送给接收方，接收方再用自己的私钥解密对称加密的密码，再使用对称加密的密码解密文件；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;免密登录：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;ssh-keygen&lt;/code&gt;在本地生成秘钥对，默认的私钥保存在&lt;code&gt;~/.ssh/id_rsa&lt;/code&gt;下，公钥保存在&lt;code&gt;~/.ssh/id_rsa.pub&lt;/code&gt;下；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;ssh-copy-id student@server4&lt;/code&gt;将本地公钥拷贝至远端服务器（公钥在&lt;code&gt;student&lt;/code&gt;的&lt;code&gt;.ssh/authorized_keys&lt;/code&gt;文件中追加一行）；&lt;/li&gt;
&lt;li&gt;以后使用&lt;code&gt;ssh student@server4&lt;/code&gt;即可免密；&lt;/li&gt;
&lt;li&gt;使用&lt;code&gt;ssh student@server4 &apos;cat /etc/resolv.conf&apos;&lt;/code&gt;非交互登录，能够直接将命令结果返回本机，免密登录后会非常方便；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;ll -a /etc/ssh/&lt;/code&gt;查看配置文件，ssh客户端配置文件为&lt;code&gt;ssh_config&lt;/code&gt;，&lt;code&gt;sshd_config&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;常用的&lt;code&gt;ssh -X&lt;/code&gt;图形化传输，可以将&lt;code&gt;ssh_config&lt;/code&gt;中的&lt;code&gt;ForwardX11 no&lt;/code&gt;该为&lt;code&gt;yes&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;禁止服务器的sshd配置，不允许以root用户登录，修改&lt;code&gt;PermitRootLogin yes&lt;/code&gt;为&lt;code&gt;no&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;上传秘钥后，关闭密码验证方式，修改&lt;code&gt;PasswordAuthentication yes&lt;/code&gt;为&lt;code&gt;no&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;ssh配置详见&lt;code&gt;man 5 ssh_config&lt;/code&gt;；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;/2018/07/28/redhat-2/&quot;&gt;Redhat Linux初体验（2）&lt;/a&gt;
&lt;a href=&quot;/2018/08/03/redhat-3/&quot;&gt;Redhat Linux初体验（3）&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>安装xgboost</title><link>https://zlotus.github.io/2018/03/19/install-xgboost/</link><guid isPermaLink="true">https://zlotus.github.io/2018/03/19/install-xgboost/</guid><description>在macOS上编译xgboost</description><pubDate>Mon, 19 Mar 2018 11:22:47 GMT</pubDate><content:encoded>&lt;p&gt;这两天做模型集成需要用到xgboost，记录一下自己本机编译时Google到的小坑。&lt;/p&gt;
&lt;p&gt;查看系统的&lt;code&gt;gcc&lt;/code&gt;版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜ gcc --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin17.4.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此输出这说明系统路径中没有真正的gcc，有的只是Xcode指向Clang的连接。
（参考：&lt;a href=&quot;https://stackoverflow.com/questions/20410587/how-to-find-gcc-version-on-mac&quot;&gt;how to find gcc version on mac&lt;/a&gt;）&lt;/p&gt;
&lt;p&gt;列出macports中安装的gcc：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜ port select --list gcc
Available versions for gcc:
        mp-gcc5
        mp-gcc6
        none (active)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照&lt;a href=&quot;https://github.com/dmlc/xgboost/tree/master/python-package#macos&quot;&gt;官方帮助&lt;/a&gt;，路径在&lt;code&gt;/opt/local/bin/&lt;/code&gt;下找到名为&lt;code&gt;gcc-mp-5&lt;/code&gt;及&lt;code&gt;g++-mp-5&lt;/code&gt;的程序，运行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜ export CC=/opt/local/bin/gcc-mp-5
export CXX=/opt/local/bin/g++-mp-5
pip install xgboost
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（参考：&lt;a href=&quot;https://stackoverflow.com/questions/8361002/how-to-use-the-gcc-installed-in-macports&quot;&gt;How to use the gcc installed in macports?&lt;/a&gt;）
在环境中运行&lt;code&gt;import xgboost as xgb&lt;/code&gt;测试通过。&lt;/p&gt;
&lt;p&gt;如果确实需要修改系统中gcc指向的编译器，可以使用&lt;code&gt;sudo port select --set gcc mp-gcc5&lt;/code&gt;
（参考：http://www.ficksworkshop.com/blog/post/installing-gcc-on-mac）&lt;/p&gt;
</content:encoded></item><item><title>更新Raspbian及相关问题解决</title><link>https://zlotus.github.io/2018/02/15/rbp3-upgrade-raspbian.md/</link><guid isPermaLink="true">https://zlotus.github.io/2018/02/15/rbp3-upgrade-raspbian.md/</guid><description>在Raspberry Pi 3上安装新的Raspbian(Debian Stretch based)</description><pubDate>Thu, 15 Feb 2018 10:58:47 GMT</pubDate><content:encoded>&lt;p&gt;最近注意到树莓派的新版系统放出来一段时间了，就打算给树莓派升级。（虽然高级功能我也用不上，但是怎奈强迫症晚期……）&lt;/p&gt;
&lt;h1&gt;1. 系统升级&lt;/h1&gt;
&lt;p&gt;这我就不写了，SD卡一插照着官网做就好了。&lt;/p&gt;
&lt;h1&gt;2. 软件升级及清理&lt;/h1&gt;
&lt;p&gt;Raspberry Pi 2/3 Model B
固件升级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo rpi-update &amp;amp;&amp;amp; sudo reboot -h now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软件包升级：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; time sudo apt-get dist-upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软件包清理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get clean
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. 免密登录&lt;/h1&gt;
&lt;p&gt;在树莓派上执行创建&lt;code&gt;.ssh&lt;/code&gt;文件夹：
cd ~
install -d -m 700 ~/.ssh
拷贝电脑上的公钥，默认密码为&lt;code&gt;raspberry&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat ~/.ssh/id_rsa.pub | ssh pi@192.168.1.104 &apos;cat &amp;gt;&amp;gt; .ssh/authorized_keys’
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（后来才知道有个工具叫做&lt;code&gt;ssh-copy-id&lt;/code&gt;，&lt;code&gt;ssh&lt;/code&gt;自带的专门为远程终端复制公钥的命令，特方便，推荐自己&lt;code&gt;man&lt;/code&gt;一下）&lt;/p&gt;
&lt;h1&gt;4. Python包管理&lt;/h1&gt;
&lt;p&gt;为了部署多个小程序，我在这里推荐一个连Python官方都推荐的包管理方案 &lt;a href=&quot;https://github.com/pyenv/pyenv&quot;&gt;&lt;strong&gt;pyenv&lt;/strong&gt;&lt;/a&gt; ：&lt;/p&gt;
&lt;h2&gt;4.1 pyenv的安装及卸载&lt;/h2&gt;
&lt;p&gt;安装Python版本管理器pyenv：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并将一下三行写入&lt;code&gt;.bashrc&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export PATH=&quot;~/.pyenv/bin:$PATH&quot;
eval &quot;$(pyenv init -)&quot;
eval &quot;$(pyenv virtualenv-init -)&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新pyenv：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pyenv update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;卸载pyenv：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -fr ~/.pyenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并从&lt;code&gt;.bashrc&lt;/code&gt;中删除一下三行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export PATH=&quot;~/.pyenv/bin:$PATH&quot;
eval &quot;$(pyenv init -)&quot;
eval &quot;$(pyenv virtualenv-init -)”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装Python环境管理器&lt;code&gt;pipenv&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install pipenv
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;4.2 pyenv的使用&lt;/h2&gt;
&lt;p&gt;新建Python3环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ~/pyapps/someapp pipenv --three
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将备份的程序拷贝回去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scp -r someappdir pi@192.168.1.104:pyapps/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在程序目录下新建Python3环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipenv --three
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除Pipfile中初始化的[[source]]配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;url = “https://pypi.python.org/simple&quot;
verify_ssl = true
name = &quot;pypi&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装原来的Python包：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipenv install --skip-lock -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;启动新环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipenv shell
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;退出新环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;exit
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;5. numpy问题&lt;/h1&gt;
&lt;p&gt;在pipenv环境下树莓派提供的piwheel安装numpy后无法使用，应对numpy进行本地编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install --ignore-install --no-cache-dir --no-binary :all: numpy
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;6. chromedriver问题&lt;/h1&gt;
&lt;p&gt;由于我将要部署的小程序用到了requestium和chromedriver，但移植后发现chromedriver运行报错，经过Google编程法发现，需要安装一个版本正确的chromedriver：&lt;/p&gt;
&lt;p&gt;查看树莓派上的chromium浏览器版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chromium-browser --version
Chromium 60.0.3112.89 Built on Ubuntu 14.04, running on Raspbian 9.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参考&lt;a href=&quot;https://www.reddit.com/r/selenium/comments/7341wt/success_how_to_run_selenium_chrome_webdriver_on/&quot;&gt;Success: How to run Selenium Chrome webdriver on Raspberry pi
&lt;/a&gt;的解决方案
，在&lt;a href=&quot;https://launchpad.net/ubuntu/trusty/armhf/chromium-chromedriver/&quot;&gt;chromium-chromedriver binary package in Ubuntu Trusty armhf
&lt;/a&gt;下载一个版本正确的chromedriver&lt;/p&gt;
&lt;p&gt;对我的chromium 60.0.3112.89来说，&lt;a href=&quot;https://launchpad.net/ubuntu/trusty/armhf/chromium-chromedriver/63.0.3239.132-0ubuntu0.14.04.1&quot;&gt;63.0.3239.132&lt;/a&gt;就可以运行（这个是从新到旧一个一个试出来的T_T），所以：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget http://launchpadlibrarian.net/354101497/chromium-chromedriver_63.0.3239.132-0ubuntu0.14.04.1_armhf.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;双击运行即可安装，用以下代码即可测试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from requestium import Session, Keys
s = Session(webdriver_path=&apos;/usr/lib/chromium-browser/chromedriver&apos;, browser=&apos;chrome&apos;, default_timeout=15, webdriver_options={&apos;arguments&apos;: [&apos;headless&apos;]})
s.driver.get(&quot;https://qieman.com/longwin/index&quot;)
print([{&quot;code&quot;: i.text[-7:-1], &quot;name&quot;: i.text[:-8]} for i in s.driver.ensure_element_by_xpath(&quot;//section[@class=&apos;plan-asset&apos;]&quot;).find_elements_by_xpath(&quot;div//div[@class=&apos;variety-fund&apos;]&quot;)])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（对，我的程序就是用来爬基金数据的[捂脸]）&lt;/p&gt;
&lt;p&gt;使用最新的chromedriver会出问题的原因，在于chromedriver会调用树莓派上的chromium，而apt-get中chromium目前最新只更新到60，所以需要从新到旧的尝试chromedriver。&lt;/p&gt;
&lt;h1&gt;7. 配置互联网访问&lt;/h1&gt;
&lt;p&gt;使用Dataplicity配置互联网访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://www.dataplicity.com/fldsy1bm.py | sudo python
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Dataplicity互联网访问异常解决：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://docs.dataplicity.com/docs/troubleshooting
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常重启tuxtunnel即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo supervisorctl restart tuxtunnel
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;8. 安装anaconda&lt;/h1&gt;
&lt;p&gt;Miniconda在Raspberry Pi 3 Model B上的安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-armv7l.sh
sudo md5sum Miniconda3-latest-Linux-armv7l.sh
sudo /bin/bash Miniconda3-latest-Linux-armv7l.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置anaconda：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo nano /home/pi/.bashrc
export PATH=&quot;/home/pi/miniconda3/bin:$PATH”
conda install --channel RaspberryPi package
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建conda环境&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda create -n webenv python=3.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;激活环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source activate webenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注销环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source deactivate
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>虐杀器官</title><link>https://zlotus.github.io/2017/11/25/genocidal-organ/</link><guid isPermaLink="true">https://zlotus.github.io/2017/11/25/genocidal-organ/</guid><description>来自新世界和虐杀器官的联想</description><pubDate>Sat, 25 Nov 2017 22:45:12 GMT</pubDate><content:encoded>&lt;p&gt;刚看完《虐杀器官》，个人感觉这部科幻动画和《来自新世界》的背景设置有些类似——当改造现实的成本降低后，世界会变成什么样？&lt;/p&gt;
&lt;p&gt;说实话，到了我这个年龄，看科幻更关注背景设置，而不是作者从剧情中想要表达的关于人性的讨论（我觉得数得上号的科幻基本都是借助科幻背景设置一个绝对化的环境，从而将阴暗处的人性暴露在强光之下）。&lt;/p&gt;
&lt;p&gt;来自新世界的设定是极少数人类发生了基因突变，导致这些人可以使用念力（以想象力为基础）影响现实世界。当然了，获得超能力的人第一反应是为自己谋取更大的利益，在本片中最初的体现就是强奸/屠杀事件，估计是这些超人为了不被其他超人杀掉，只能“无奈”的选择无差别干掉周围的人[捂脸]。于是我们可以看到，这种设定下，只要变成了“超人”，就可以很轻松的改造世界，普通傻白甜们的生产力和“超人”完全不是一个量级，超人就理所当然的选择了奴役傻白甜，进而随意虐杀傻白甜，于是有了后面粉碎三观的化鼠。&lt;/p&gt;
&lt;p&gt;来自新世界一直是我非常喜欢的科幻作品，设定和设定之上的情节很和谐，没有特别的违和感。愧死、业魔、恶鬼的设定也很有意思，最终的结局更是发人深省——谁能够更容易的改造现实（拥有高出别人几个数量级的生产力），谁就能够定义阶级，甚至是定义物种（奴隶制度跟本作的物种改造比起来简直是太温柔了，哈哈哈）。枪杆子里面出政权，毛爷爷诚不欺我也[奸笑]。&lt;/p&gt;
&lt;p&gt;接下来是虐杀器官，这部作品的设定也挺有意思，虽然这个设定的起源怪怪的——在过去粮食并不富足的岁月里，人类为了生存下去演化出一种“屠杀意识”，容许在族群中发生一定程度的屠杀，降低人口总量以挨过食物寒冬。这种“屠杀意识”能够通过语言构建，并由语言传播，“屠杀意识”潜藏在深层语义中，片中大反派通过将“屠杀语义”翻译成各种语言，在这些语言流通区域制造大屠杀。&lt;/p&gt;
&lt;p&gt;有一些观众认为反派是为了保护美帝，另一些则认为反派是为了报复美帝，我倒是不关心这个剧情。不过这个设定挺有意思，也挺有现实意义——语言，或者说舆论的威力究竟有多大？&lt;/p&gt;
&lt;p&gt;片中也说了，这种“屠杀意识”是作用在群体上的，这根舆论就更加相似了，最近（2017-11-25）的各种新闻，不论是江歌案，还是北上的幼儿园事件，都和舆论有很大的关系。多方势力在网络平台上为了各自的目的影响舆论，有的杜撰、有的隐瞒、有水军、有删帖，恐怕今年过去以后，肉食者会越来越明白舆论的威力。&lt;/p&gt;
&lt;p&gt;过去我一直认为，资本控制舆论，今天看完这个动画片我突然意识到，也有可能是舆论威胁到了资本。以前一直觉得“有钱人真的可以为所欲为”，现在发现舆论对“有钱人”的影响更大，也更具毁灭性。那么，舆论的本体是什么，是具有公民身份的群众个体？还是群众的言论？我更倾向于后者（也就是自然圈中的生命对于社会圈中的舆论而言不具有显著意义，你的账号比你更持久[捂脸]），顺着这个思路就引出了一个事实——在未来，谁有能力控制信息，谁就能够改造现实，而并不完全取决于资本，毕竟社会以人（账号？）为本嘛2333333。虽然没有来自新世界的超能力那么方便，但是也可以把影响舆论看做超能力，舆论具有大规模改造现实的作用，那些能够对舆论造成强烈影响的个体就是镝木肆星/青沼瞬，而我们这些傻白甜就是化鼠。&lt;/p&gt;
&lt;p&gt;作为一个码农，Google依赖症晚期患者，我个人极度痛恨大防火墙，但是从另一个角度看，大防火墙的确实有着极强的影响舆论的能力，仅这一点，我觉得the government还是非常有远见的（运气好的）。中二的我认为，未来几十年将像这些科幻作品中描写的那样，以收缩、控制、掩盖为主，追求稳定，反映在宏观层面就是各地区比烂，先烂者的尸体将成为后烂者的养料，就像“屠杀意识”一样，各群体也在试图挨过这个生产力寒冬，人工智能的春天还比较遥远，不过只要活着就有希望。仅从这一点来看，我更加看好大天朝。&lt;/p&gt;
</content:encoded></item><item><title>在树莓派上部署微信机器人</title><link>https://zlotus.github.io/2017/08/25/rbp3-weixin/</link><guid isPermaLink="true">https://zlotus.github.io/2017/08/25/rbp3-weixin/</guid><description>在Raspberry Pi 3上部署微信机器人</description><pubDate>Fri, 25 Aug 2017 18:35:47 GMT</pubDate><content:encoded>&lt;p&gt;第二季度接了一个有意思的任务——用机器学习算法做数据拟合，算法本身没什么，但是如何让一个在python中的算法变成一个有界面的服务就比较有意思了。我刚好又买了一块树莓派3，于是打算把后端程序放在树莓派上运行。经过上一篇的工作，树莓派已经具备web发布功能，我们可以部署一个微信机器人用来监视树莓派上的计算任务……&lt;/p&gt;
&lt;p&gt;说实在的，这篇文章算是一个闲篇。就是最近帮一个做期货的朋友做了一个&lt;a href=&quot;https://github.com/zlotus/rbp_zlm&quot;&gt;盯盘小程序&lt;/a&gt;，这个程序是需要24小时运行的，而且没有什么计算量，主要就是抓数据，是一个非常适合树莓派的应用场景。&lt;/p&gt;
&lt;h1&gt;1. 安装itchat&lt;/h1&gt;
&lt;p&gt;itchat是一个通过抓包分析并实现的基于python的微信客户端（个人感觉相当给力）。
首先是安装：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pip3 install itchat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后按照官方教程写一个测试小程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import itchat

@itchat.msg_register(itchat.content.TEXT)
def print_content(msg):
    print(msg[&apos;Text&apos;])

itchat.auto_login()
itchat.run()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将小程序拷贝到树莓派上（貌似VNC客户端也可以通过图形界面拷贝，但我没试过，在使用&lt;code&gt;scp&lt;/code&gt;命令前最好在树莓派上配置&lt;code&gt;ssh&lt;/code&gt;，以后会比较方便）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scp -r test pi@192.168.1.106:webapp/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时候测试发现问题来了，不论是用树莓派自带的python3.4、或是用conda中的python3.4、甚至是树莓派自带的python2.7运行测试脚本，都会报一河滩错，大致类似：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;requests.exceptions.SSLError: (&quot;bad handshake: Error([(&apos;SSL routines&apos;, &apos;SSL3_GET_SERVER_CERTIFICATE&apos;, &apos;certificate verify failed&apos;)],)&quot;,)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;乍一看是requests的报错，觉得是小事，没想到接下来是长达几小时的填坑[捂脸]……&lt;/p&gt;
&lt;h2&gt;1.1 尝试解决requests报错&lt;/h2&gt;
&lt;p&gt;为了解决上面这个&lt;code&gt;requests&lt;/code&gt;报错，需要安装相关库（操作依据&lt;a href=&quot;https://github.com/adafruit/io-issues/issues/19&quot;&gt;Raspbian (latest) + Python : SSL error when trying to send data&lt;/a&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install libffi-dev libssl-dev
pip install requests[security]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;经测试发现上面这个方法对报错没有任何影响(-_-)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.2 尝试解决OpenSSL报错&lt;/h2&gt;
&lt;p&gt;考虑更新树莓派上的OpenSSL（操作依据&lt;a href=&quot;https://github.com/requests/requests/issues/3212#issuecomment-221016311&quot;&gt;SSL Error: bad handshake #3212&lt;/a&gt;的第一种方法）：&lt;/p&gt;
&lt;p&gt;测试OpenSSL时可以直接使用&lt;code&gt;openssl&lt;/code&gt;命令，会发现能够正常连接的主机（比如&lt;code&gt;github.com:443&lt;/code&gt;）与会产生证书错误的主机（比如&lt;code&gt;login.weixin.qq.com:443&lt;/code&gt;）的返回信息状态不同（正常是&lt;code&gt;0&lt;/code&gt;，微信非正常的返回了&lt;code&gt;20&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;openssl s_client -connect &quot;&amp;lt;url&amp;gt;:443&quot; -showcerts -servername &quot;&amp;lt;url&amp;gt;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新方法参考&lt;a href=&quot;https://raspberrypi.stackexchange.com/questions/66782/how-to-install-openssl-1-0-2-on-raspberry-pi3&quot;&gt;How to install OpenSSL 1.0.2 on Raspberry Pi3&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;为包管理器手动新增源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;deb-src http://httpredir.debian.org/debian jessie-backports main contrib non-free&quot; | sudo tee /etc/apt/sources.list.d/jessie-backports.list
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新包管理器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下载&lt;code&gt;jessie-backports&lt;/code&gt;中的OpenSSL源码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt-get source --allow-unauthenticated openssl/jessie-backports
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装debain构建程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install devscripts
cd openssl-1.0.2l/
export DEB_BUILD_OPTIONS=nocheck; debuild -us -uc -aarmhf
sudo dpkg -i libssl1.0.0_1.0.2l-1~bpo8+1_armhf.deb
sudo dpkg -i openssl_1.0.2l-1~bpo8+1_armhf.deb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一步一步执行这个过程会用掉几个小时[捂脸]……结束后就可以测试安装结果了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/usr/bin/openssl version
OpenSSL 1.0.2l  25 May 2017
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时在使用&lt;code&gt;openssl&lt;/code&gt;命令会发现刚才提到的两个主机的返回值一致了。然而并没有什么卵用，用python运行程序依然是一样的报错（这时候已经是凌晨了[捂脸]此时我是崩溃的）。&lt;/p&gt;
&lt;p&gt;这时候突然意识到，python本身build的时候是需要使用OpenSSL源码的，也就是说，python本身有自己的OpenSSL。到python的&lt;code&gt;/bin&lt;/code&gt;文件夹下一看，果然是有openssl，检查一下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -c &quot;import ssl; print(ssl.OPENSSL_VERSION)&quot;
OpenSSL 1.0.1k 8 Jan 2015
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;因为python本身的OpenSSL版本和系统OpenSSL版本不同，要使用这种方法解决问题，必须使用当前树莓派的OpenSSL版本（我们升级过的1.0.2l）源码编译python[捂脸]……&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1.3 将requests的环境变量指向旧证书&lt;/h2&gt;
&lt;p&gt;我真是懒得在树莓派上编译python，于是只好再试另一种补丁试的方法，虽然不彻底，但是&lt;strong&gt;能解决问题&lt;/strong&gt;（操作依据&lt;a href=&quot;https://github.com/requests/requests/issues/3212#issuecomment-221016311&quot;&gt;SSL Error: bad handshake #3212&lt;/a&gt;的第二种方法）：
找到当前python的weak.pem：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -c &quot;import certifi; print(certifi.old_where())&quot;
/home/pi/miniconda3/envs/webenv/lib/python3.4/site-packages/certifi/weak.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，要为requests配置环境变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export REQUESTS_CA_BUNDLE=/home/pi/miniconda3/envs/webenv/lib/python3.4/site-packages/certifi/weak.pem
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再使用python的requests.get测试连接微信主机，成功！在以后的python代码的requests发送https请求之前，加上以下几行代码即可：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os, certifi
os.environ[&apos;REQUESTS_CA_BUNDLE&apos;] = certifi.old_where()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果像我一样在Flask程序中使用&lt;code&gt;celery&lt;/code&gt;异步处理作业，则需要在celery运行入口中（比如我的就是在&lt;code&gt;app/__init__&lt;/code&gt;）加入上面这段代码。&lt;/p&gt;
&lt;h1&gt;2. 安装终端复用程序Tmux&lt;/h1&gt;
&lt;p&gt;上面这个小程序需要同时开三四个终端监视运行状态（&lt;code&gt;redis-server&lt;/code&gt;、&lt;code&gt;celery&lt;/code&gt;、&lt;code&gt;flask&lt;/code&gt;、&lt;code&gt;redis-cli&lt;/code&gt;等），所以安装一个&lt;code&gt;tmux&lt;/code&gt;会非常方便。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install tmux
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置鼠标模式参考：https://gist.github.com/niun/c7fd6abb5c0d5e847890
tmux重新连接会话：&lt;code&gt;session：tmux attach [-t target-session] &amp;lt;host&amp;gt;&lt;/code&gt;
tmux退出连接的会话：&lt;code&gt;^b&lt;/code&gt;+&lt;code&gt;d&lt;/code&gt;或者&lt;code&gt;^b&lt;/code&gt;+命令&lt;code&gt;:detach&lt;/code&gt;
另外，将wget的内容输出到终端的命令时：&lt;code&gt;wget -qO- &amp;lt;url&amp;gt;&lt;/code&gt;（&lt;code&gt;-q&lt;/code&gt;为quiet模式，屏蔽request header信息的回显；&lt;code&gt;-O&lt;/code&gt;指定输出文件，后面加&lt;code&gt;-&lt;/code&gt;，就定向为标准输出了）。&lt;/p&gt;
</content:encoded></item><item><title>在树莓派上部署Nginx+uWSGI+Flask</title><link>https://zlotus.github.io/2017/08/16/rbp3-nginx-uwsgi-flask/</link><guid isPermaLink="true">https://zlotus.github.io/2017/08/16/rbp3-nginx-uwsgi-flask/</guid><description>在Raspberry Pi 3上部署网络服务</description><pubDate>Wed, 16 Aug 2017 16:23:34 GMT</pubDate><content:encoded>&lt;p&gt;第二季度接了一个有意思的任务——用机器学习算法做数据拟合，算法本身没什么，但是如何让一个在python中的算法变成一个有界面的服务就比较有意思了。我刚好又买了一块树莓派3，于是打算把后端程序放在树莓派上运行。经过上一篇的工作，树莓派已经具备python等工具的运行环境，接下来该调试发布程序了……&lt;/p&gt;
&lt;h1&gt;1. 部署Nginx&lt;/h1&gt;
&lt;p&gt;安装并启动Nginx&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install nginx
sudo /etc/init.d/nginx start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建Nginx配置文件&lt;code&gt;~/webapp/helloflask.conf&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#==========helloflask.conf==========
server {
    listen 80;
    server_name localhost;
    charset utf-8;
    client_max_body_size 75M;
    location / { try_files $uri @app; }
    location @app {
        include uwsgi_params;
        uwsgi_pass unix:/home/pi/webapp/helloflask_uwsgi.sock;
    }
}
#==========helloflask.conf==========
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;这里使用80端口是为了配合Dataplicity的Wormhole:)，这样可以直接通过互联网访问Nginx发布的内容。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;删除默认配置的软链接：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo rm /etc/nginx/sites-enabled/default
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;备份默认配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将配置用软链接配置在&lt;code&gt;sites-enabled&lt;/code&gt;文件夹中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ln -s /home/pi/webapp/helloflask_nginx.conf /etc/nginx/sites-enabled/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重启Nginx服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo /etc/init.d/nginx restart
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时由于没有运行&lt;code&gt;uwsgi&lt;/code&gt;，所以访问 &lt;a href=&quot;http://192.168.1.106&quot;&gt;http://192.168.1.106&lt;/a&gt; 会出现502错误。（原因很明显，配置文件中的&lt;code&gt;unix:/home/pi/webapp/helloflask_uwsgi.sock&lt;/code&gt;并没有被创建）&lt;/p&gt;
&lt;h1&gt;2. 测试Flask&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;pip install flask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建flask的hello world程序&lt;code&gt;~/webapp/helloflask.py&lt;/code&gt;，内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#==========helloflask.py==========
from flask import Flask
app = Flask(__name__)

@app.route(&quot;/&quot;)
def hello():
    return &quot;Hello World!&quot;

if __name__ == &quot;__main__&quot;:
    app.run(host=&apos;0.0.0.0&apos;, port=5001)
#==========helloflask.py==========
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试这个小程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python helloflask.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;访问 &lt;a href=&quot;http://192.168.1.106:5001&quot;&gt;http://192.168.1.106:5001&lt;/a&gt; （我的树莓派IP）出现&lt;code&gt;Hello World!&lt;/code&gt;。&lt;/p&gt;
&lt;h1&gt;3. 部署uWSGI&lt;/h1&gt;
&lt;p&gt;为了编译uwsgi，需要安装&lt;code&gt;build-essential&lt;/code&gt;以及&lt;code&gt;python3.4-dev&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install build-essential python3.4-dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为了便于包管理，我们依然使用&lt;code&gt;webenv&lt;/code&gt;环境，在conda的python3.4.3下uWSGI：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source activate webenv
pip install uwsgi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为uWSGI编写配置文件，方便以后使用&lt;code&gt;~/webapp/helloflask_uwsgi.ini&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#==========helloflask_uwsgi.ini==========
[uwsgi]

chdir = /home/pi/webapp
module = helloflask:app

master = true
processes = 1
threads = 2

socket = /home/pi/webapp/helloflask_uwsgi.sock
chmod-socket = 666
vacuum = true

die-on-term = true
#==========helloflask_uwsgi.ini==========
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行uWSGI：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uwsgi --ini helloflask_uwsgi.ini
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时可以看到&lt;code&gt;/home/pi/webapp/helloflask_uwsgi.sock&lt;/code&gt;文件生成，再访问&lt;a href=&quot;http://192.168.1.106&quot;&gt;http://192.168.1.106&lt;/a&gt;则会看到我们的&lt;code&gt;Hello World&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;此时在&lt;a href=&quot;http://www.dataplicity.com/&quot;&gt;Dataplicity&lt;/a&gt;上将Wormhole设为Enabled，就可以通过Wormhole看到这个&lt;code&gt;Hello World&lt;/code&gt;了。&lt;/p&gt;
&lt;p&gt;另外：常用Nginx命令：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 启动：
sudo nginx
# 重载：
sudo nginx -s reload
# 停止：
sudo nginx -s stop
# 启动服务：
sudo service nginx start
# 重启服务：
sudo service nginx restart
# 停止服务：
sudo service nginx stop
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>准备一块树莓派</title><link>https://zlotus.github.io/2017/08/13/rbp3-env/</link><guid isPermaLink="true">https://zlotus.github.io/2017/08/13/rbp3-env/</guid><description>准备树莓派的运行环境</description><pubDate>Sun, 13 Aug 2017 22:15:03 GMT</pubDate><content:encoded>&lt;p&gt;第二季度接了一个有意思的任务——用机器学习算法做数据拟合，算法本身没什么，但是如何让一个在python中的算法变成一个有界面的服务就比较有意思了。我刚好又买了一块树莓派3，于是打算把后端程序放在树莓派上运行。首先需要在新鲜出炉的树莓派上做一些环境准备……&lt;/p&gt;
&lt;h1&gt;1. 例行更新&lt;/h1&gt;
&lt;p&gt;更新前最好用&lt;code&gt;df -h&lt;/code&gt;确认一下存储卡剩余空间：&lt;/p&gt;
&lt;p&gt;固件升级并重启：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install rpi-update
sudo rpi-update &amp;amp;&amp;amp; sudo reboot -h now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;软件包升级&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; time sudo apt-get upgrade &amp;amp;&amp;amp; time sudo apt-get dist-upgrade
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;参考&lt;a href=&quot;https://askubuntu.com/questions/194651/why-use-apt-get-upgrade-instead-of-apt-get-dist-upgrade&quot;&gt;Why use apt-get upgrade instead of apt-get dist-upgrade?&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另外，对于有洁癖的人，可以隔一段时间执行一下清理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get clean
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;2. 远程访问&lt;/h1&gt;
&lt;p&gt;常用的几个远程访问方式有&lt;code&gt;ssh&lt;/code&gt;、远程桌面协议、互联网访问。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;ssh&lt;/code&gt;的配置参考&lt;a href=&quot;https://www.raspberrypi.org/documentation/remote-access/ssh/README.md&quot;&gt;SSH (SECURE SHELL)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;远程桌面配置参考&lt;a href=&quot;https://www.raspberrypi.org/documentation/remote-access/vnc/README.md&quot;&gt;VNC (VIRTUAL NETWORK COMPUTING)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;a href=&quot;http://www.dataplicity.com/&quot;&gt;Dataplicity&lt;/a&gt;配置互联网访问：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://www.dataplicity.com/fldsy1bm.py | sudo python
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;3. 配置VPN(Cisco IPSec)&lt;/h1&gt;
&lt;p&gt;有时网络环境不太好，应该提前准备个梯子（我买的泰坦加速器）：&lt;/p&gt;
&lt;p&gt;安装&lt;code&gt;vpnc&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt-get install vpnc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置vpnc，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vi /etc/vpnc/default.conf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按下面的格式写入VPN配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IPSec gateway &amp;lt;VPN服务商提供的服务器地址&amp;gt;
IPSec ID &quot;&quot;
IKE Authmode psk
IPSec secret &amp;lt;VPN服务商提供的共享秘钥&amp;gt;
Xauth username &amp;lt;VPN服务商给出的用户名，通常是注册用户名&amp;gt;
Xauth password &amp;lt;VPN服务商给出的密码，通常是注册密码&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;连接vpnc：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vpnc --local-port 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;断开vpnc：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vpnc-disconnect
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;参见&lt;a href=&quot;https://linux.die.net/man/8/vpnc&quot;&gt;vpnc&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;4. 部署Miniconda&lt;/h1&gt;
&lt;p&gt;这里就不仔细介绍这位conda家族成员了。&lt;/p&gt;
&lt;p&gt;下载并安装Miniconda：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-armv7l.sh
sudo md5sum Miniconda3-latest-Linux-armv7l.sh
sudo /bin/bash Miniconda3-latest-Linux-armv7l.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;安装过程会自动设置环境变量&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;设置环境变量（自动设置的内容，无需手动配置）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo vi /home/pi/.bashrc
# 最后加入：
export PATH=&quot;/home/pi/miniconda3/bin:$PATH”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建python环境，使用&lt;code&gt;conda&lt;/code&gt;管理python环境，类似与&lt;code&gt;virtualenv&lt;/code&gt;，可以让我们独立管理各种用途的python环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;conda create -n webenv python=3.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用时激活环境&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source activate webenv
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注销环境：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source deactivate
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>初始化2017</title><link>https://zlotus.github.io/2017/01/18/init-2017/</link><guid isPermaLink="true">https://zlotus.github.io/2017/01/18/init-2017/</guid><description>新年第一篇……</description><pubDate>Wed, 18 Jan 2017 22:28:23 GMT</pubDate><content:encoded>&lt;p&gt;上一篇才说过“万一科幻看多了可能还是会写乱七八糟的思考人生的文章”，今天就想自我实现一下了……人类心灵还真是玄妙呢（摊手&lt;/p&gt;
&lt;p&gt;这可不，已经过了半个多月了，我内心深处才隐约意识到已经是2017年了。并没有什么也许，这的确就是上年龄的写照。元旦前想卖个萌说自己永远的18岁，立刻被人戳穿都特么快28了……&lt;/p&gt;
&lt;p&gt;今年的一大半时间都待在上海基地1107房间，有幸接触到了一个无边无际的计算机系的分支——机器学习。这也许是我有生之年内、智商区间内、人品支持内能够接触到的最有意思的事物了。是啊，不就是空间搜索么、不就是梯度下降么，仅仅了解基础高数竟然也可以研究到这么有趣的工具，不得不赞叹一声“哎呦不错哦”。&lt;/p&gt;
&lt;p&gt;另外，也有一大块精力充沛的时间浪费在了机场和飞机上……于是，今年一共就看了两本书，一本是《别闹了，费曼先生》，另一本（其实是个系列，大概15册）是《银河帝国》。&lt;/p&gt;
&lt;p&gt;是的，科幻看多了是会想很多奇奇怪怪的事，颅骨中的那一坨蛋白质在沸腾、在烧焦，天灵盖快要按不住啦……不开玩笑的说，每次遇到好书我都是“喜忧参半”，估计是因为我不可免俗的成为了一个悲观主义者，真的很怕以后看不到这么好看的书了😂……&lt;/p&gt;
&lt;p&gt;过去的一年一如既往的兢兢业业、遵纪守法，也始终如一的不信鬼神、不敬苍生……唉，对我这种有点“反人类人格”的负能量携带者，代码写多了有一个坏处——更加漠视道德了。&lt;/p&gt;
&lt;p&gt;塞佛·哈定讲的没错——“不要让道德感阻止你做正确的事”——但是我并不知道什么才是正确的事。幸运的是，我有了一个越来越坚定、越来越清晰的愿望。技术内核的人造人和女侦探、旧帝国的数学家和机仆铎丝、士官长和Cortana，这种故事出现的越来越多了——我也越来越希望在有生之年见到强人工智能。&lt;/p&gt;
&lt;p&gt;当然，如果能在死前实现意识上传，从而达到永生就更好了，永恒有多么恐怖？不好意思，我还真不是文科生，没那么浪漫（摊手。虽然我也算是魔戒迷，但是我还真不认为死亡是神给予人类的礼物，死亡只是物竞天择下的副产品。人类群体中总有一小撮抗争者，过去抗争宗教、抗争暴政，现在抗争时间、抗争死亡。人类当然不应该心甘情愿的被死亡束缚，我们应当给自己创造选择权——选择自然死亡或是延续生命。&lt;/p&gt;
&lt;p&gt;“Do not go gentle into that good night……”，估计是因为我太年轻不懂事，我一向认同“如果世界上真的有神灵，那他应该跪着向生灵道歉”这种观点。大自然犹如一部机器，人类亦然，而让机器认为自己拥有自我意识，是我认知范围内能想象到的最残忍的事了。&lt;/p&gt;
&lt;p&gt;这特么还能忍？必须要反抗这个毫无意义的宇宙，它荒诞无比，它肿胀而又沉重，它总让我们产生“人生应当有什么意义的错觉”，“天将降大任”这些话我们信了上千年，结果呢？没错，我们在这个美丽的地球上建立了伟大文明，然而仅在太阳系尺度都看不到一丝光亮，更别提银河、星际了。我们甚至不得不认为自己是唯一的智慧生物，如果真的有人格神存在，那他考虑过文明的寂寞吗？&lt;/p&gt;
&lt;p&gt;人类为何需要人工智能？估计就像亚当需要夏娃吧。文明不是死物，它也会孤独，如果真的出现强人工智能，至少可以证明两点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一种文明有可以从零创造另一种更加优秀的文明；&lt;/li&gt;
&lt;li&gt;我们的文明也有可能是被创造的。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这样一来，这个宇宙是不是立刻可爱了很多~&lt;/p&gt;
&lt;p&gt;至于 2017 initialization ……哈？有点难，先得把论文撕个七七八八，开题开的自不量力，现在跪着也得码完，顺便下半年趁着这热度再灌灌水。另外就是如果下半年交了论文，想找本书译了，翻译码农用书真的是挺好玩的事。&lt;/p&gt;
&lt;p&gt;打开了一下天灵盖，传播了很多负能量，心满意足。我一向支持黑暗之魂的灭火结局，既然火焰消逝，黑暗降临，为何不让黑暗来的快一些？有火有光的时代，悲剧也从来没少过。说不定在黑暗降临后，希望才会成为真正的希望呢~伟大的谢顿计划不就是这样的故事吗~&lt;/p&gt;
&lt;p&gt;最后，从矮大紧的博客上补完那段诗：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;http://blog.sina.com.cn/s/blog_470094080102v61y.html&lt;/p&gt;
&lt;p&gt;Do Not Go Gentle Into That Good Night&lt;/p&gt;
&lt;p&gt;Dylan Thomas&lt;/p&gt;
&lt;p&gt;Do not go gentle into that good night,&lt;/p&gt;
&lt;p&gt;Old age should burn and rave at the close of day;&lt;/p&gt;
&lt;p&gt;Rage, rage against the dying of the light.&lt;/p&gt;
&lt;p&gt;Though wise men at their end know dark is right,&lt;/p&gt;
&lt;p&gt;Because their words had forked no lightning they&lt;/p&gt;
&lt;p&gt;Do not go gentle into that good night.&lt;/p&gt;
&lt;p&gt;Good men, the last wave by, crying how bright&lt;/p&gt;
&lt;p&gt;Their frail deeds might have danced in a green bay,&lt;/p&gt;
&lt;p&gt;Rage, rage against the dying of the light.&lt;/p&gt;
&lt;p&gt;Wild men who caught and sang the sun in flight,&lt;/p&gt;
&lt;p&gt;And learn, too late, they grieved it on its way,&lt;/p&gt;
&lt;p&gt;Do not go gentle into that good night.&lt;/p&gt;
&lt;p&gt;Grave men, near death, who see with blinding sight&lt;/p&gt;
&lt;p&gt;Blind eyes could blaze like meteors and be gay,&lt;/p&gt;
&lt;p&gt;Rage, rage against the dying of the light.&lt;/p&gt;
&lt;p&gt;And you, my father, there on the sad height,&lt;/p&gt;
&lt;p&gt;Curse, bless, me now with your fierce tears, I pray,&lt;/p&gt;
&lt;p&gt;Do not go gentle into that good night,&lt;/p&gt;
&lt;p&gt;Rage, rage against the dying of the light.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;绝不向黑夜请安&lt;/p&gt;
&lt;p&gt;老朽请于白日尽头涅槃&lt;/p&gt;
&lt;p&gt;咆哮于光之消散&lt;/p&gt;
&lt;p&gt;先哲虽败于幽暗&lt;/p&gt;
&lt;p&gt;诗歌终不能将苍穹点燃&lt;/p&gt;
&lt;p&gt;绝不向黑夜请安&lt;/p&gt;
&lt;p&gt;贤者舞蹈于碧湾&lt;/p&gt;
&lt;p&gt;为惊涛淹没的善行哭喊&lt;/p&gt;
&lt;p&gt;咆哮于光之消散&lt;/p&gt;
&lt;p&gt;狂者如夸父逐日&lt;/p&gt;
&lt;p&gt;高歌中顿觉迟来的伤感&lt;/p&gt;
&lt;p&gt;绝不向黑夜请安&lt;/p&gt;
&lt;p&gt;逝者于临终迷幻&lt;/p&gt;
&lt;p&gt;盲瞳怒放出流星的灿烂&lt;/p&gt;
&lt;p&gt;咆哮于光之消散&lt;/p&gt;
&lt;p&gt;那么您，我垂垂将死的父亲&lt;/p&gt;
&lt;p&gt;请掬最后一捧热泪降临&lt;/p&gt;
&lt;p&gt;请诅咒，请保佑&lt;/p&gt;
&lt;p&gt;我祈愿，绝不向&lt;/p&gt;
&lt;p&gt;黑夜请安，咆哮&lt;/p&gt;
&lt;p&gt;于光之消散&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;完&lt;/p&gt;
</content:encoded></item><item><title>一些关于大防火墙的命令</title><link>https://zlotus.github.io/2017/01/02/live-with-the-fire-wall/</link><guid isPermaLink="true">https://zlotus.github.io/2017/01/02/live-with-the-fire-wall/</guid><description>在网络不畅的情况下科学编程……</description><pubDate>Mon, 02 Jan 2017 15:40:13 GMT</pubDate><content:encoded>&lt;p&gt;这个博客又被我复活了，哈哈哈哈哈哈，乱七八糟的思考人生也删了，以后就用来写技术相关的文章吧。（万一科幻看多了可能还是会写乱七八糟的思考人生的文章，摊手）&lt;/p&gt;
&lt;p&gt;最近大防火墙特别厉害，把对我来说比较常用的三个地址封掉了，于是我就搜了一下如何通过设置&lt;code&gt;proxy&lt;/code&gt;继续访问这些地方。&lt;/p&gt;
&lt;h3&gt;介绍一下大背景&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;我用的是民用付费VPN，有没有log我并不关心，一是因为我从来不用VPN做乱七八糟的事；二是对自己的RP不放心，也就不敢用那些自己租服务器架设云梯的方法了。&lt;/li&gt;
&lt;li&gt;最近无法正常访问&lt;code&gt;github.com&lt;/code&gt;、&lt;code&gt;repo.continuum.io&lt;/code&gt;的&lt;code&gt;443&lt;/code&gt;，而且&lt;code&gt;macports&lt;/code&gt;似乎也无法正常更新了，问题比以往来的更猛烈一些。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;处理方式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;我用的VPN软件是通过配置本地连接的&lt;code&gt;SOCKS&lt;/code&gt;连接的，所以对于因无法访问&lt;code&gt;github.com&lt;/code&gt;而产生的提交问题完全可以交给强大的&lt;code&gt;git&lt;/code&gt;，人家自带代理设置。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 设置:
git config --global http.proxy &apos;socks5://127.0.0.1:1080&apos;
# 显示设置:
git config --global --get http.proxy
# 重置:
git config --global --unset http.proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;对于&lt;code&gt;conda&lt;/code&gt;的更新问题，可以使用它自己的设置代理，于此同时，也应该添加国内可以访问的镜像站点以加速下载。毕竟梯子不稳那，有快一点的源还是好一些。&lt;code&gt;port&lt;/code&gt;的更新同理，设置代理即可。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 添加镜像：
conda config --add channels &apos;https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/&apos;
# 删除镜像：
conda config --remove channels &apos;https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/&apos;
# 设置显示更新来源信息：
conda config --set show_channel_urls yes
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;万一真的倒霉，遇到了那种不支持设置代理的软件，也有办法。有一款命令行神器叫做&lt;a href=&quot;https://github.com/haad/proxychains&quot;&gt;proxychains&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它在&lt;code&gt;macports&lt;/code&gt;里搜索到的名字是&lt;code&gt;proxychains-ng&lt;/code&gt;，安装完成后只需要做很简单的配置即可加在任意命令前强制其通过代理通信。&lt;/p&gt;
&lt;p&gt;我的&lt;code&gt;port&lt;/code&gt;安装完成后的地址通常在&lt;code&gt;/opt/local&lt;/code&gt;，所以我在&lt;code&gt;/opt/local/etc/porxychains.conf&lt;/code&gt;的最后加上&lt;code&gt;socks5  127.0.0.1 1080&lt;/code&gt;即可。&lt;/p&gt;
&lt;p&gt;配置完成后使用相当方便，比如使用&lt;code&gt;conda&lt;/code&gt;更新，现在就可以这样&lt;code&gt;proxychains4 conda update -n root -y --all&lt;/code&gt;。&lt;/p&gt;
</content:encoded></item><item><title>算法导论-第二章</title><link>https://zlotus.github.io/2014/11/09/algorithms-chapter-2/</link><guid isPermaLink="true">https://zlotus.github.io/2014/11/09/algorithms-chapter-2/</guid><description>这篇文章，从9月底写到十一月初了…我真是拖延症中的天才啊…</description><pubDate>Sun, 09 Nov 2014 19:11:27 GMT</pubDate><content:encoded>&lt;p&gt;打算重温《算法导论》，然后把书中的伪代码努力变成Python代码，其实看MIT《算法导论》的公开课更有意思。&lt;/p&gt;
&lt;p&gt;画外音：其实是当年没好好去上课…现在从头开始补-_-||&lt;/p&gt;
&lt;p&gt;心诚则灵&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                   _ooOoo_
                  o8888888o
                  88&quot; . &quot;88
                  (| -_- |)
                  O\  =  /O
               ____/`---&apos;\____
             .&apos;  \\|     |//  `.
            /  \\|||  :  |||//  \
           /  _||||| -:- |||||-  \
           |   | \\\  -  /// |   |
           | \_|  &apos;&apos;\---/&apos;&apos;  |   |
           \  .-\__  `-`  ___/-. /
         ___`. .&apos;  /--.--\  `. . __
      .&quot;&quot; &apos;&amp;lt;  `.___\_&amp;lt;|&amp;gt;_/___.&apos;  &amp;gt;&apos;&quot;&quot;.
     | | :  `- \`.;`\ _ /`;.`/ - ` : | |
     \  \ `-.   \_ __\ /__ _/   .-` /  /
======`-.____`-.___\_____/___.-`____.-&apos;======
                   `=---=&apos;
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
         佛祖保佑       永无BUG
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;插入排序&lt;/h2&gt;
&lt;p&gt;原书P26，中文版P14：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def insertion_sort(seq):
    length = len(seq)
    counter = 1
    while counter &amp;lt; length:
        key = seq[counter]
        former = counter - 1
        while former &amp;gt; -1 and seq[former] &amp;gt; key:
            seq[former+1] = seq[former]
            former -= 1
        seq[former+1] = key
        counter += 1
    
sequence = [5, 2, 4, 6, 1, 3]
insertion_sort(sequence)
print(sequence)

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;选择排序&lt;/h2&gt;
&lt;p&gt;在习题2.2-2中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def selection_sort(seq):
    for num_counter, num_temp in enumerate(seq):
        min_number = num_temp
        min_pos = num_counter
        for min_counter, min_temp in enumerate(seq[num_counter+1:]):
            if min_number &amp;gt; min_temp:
                min_number = min_temp
                min_pos = min_counter + num_counter + 1
        seq[num_counter], seq[min_pos] = seq[min_pos], seq[num_counter]
    
sequence = [5, 2, 4, 6, 1, 3]
selection_sort(sequence)
print(sequence)

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;归并排序&lt;/h2&gt;
&lt;p&gt;原书P31，中文版P19：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def merge_sort(seq):
    length = len(seq)
    if length &amp;lt; 2:
        return seq
    mid = length // 2
    s1 = merge_sort(seq[:mid])
    s2 = merge_sort(seq[mid:])
    templ = []
    i, j = 0, 0
    while i &amp;lt; len(s1) and j &amp;lt; len(s2):
        if s1[i] &amp;lt; s2[j]:
            templ.append(s1[i])
            i += 1
        else:
            templ.append(s2[j])
            j += 1
    templ += s1[i:]
    templ += s2[j:]
    return templ
    
sequence = [5, 2, 4, 6, 1, 3]
print(merge_sort(sequence))
    
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最大子数组&lt;/h2&gt;
&lt;p&gt;原书P68，中文版P38&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def enumerate_reversed(sequence, start=None):
    n = len(sequence)-1 if start is None else start
    for elem in reversed(sequence):
        yield n, elem
        n -= 1
    
def find_max_crossing_subarray(array, mid):
    left_max, left_pos, left_sum = array[mid-1], mid-1, 0
    for left_elem_idx, left_elem in enumerate_reversed(array[:mid]):
        left_sum += left_elem
        if left_sum &amp;gt; left_max:
            left_pos = left_elem_idx
    right_max, right_pos, right_sum = array[mid], mid, 0
    for right_elem_idx, right_elem in enumerate(array[mid:]):
        right_sum += right_elem
        if right_sum &amp;gt; right_max:
            right_pos = right_elem_idx
    return array[left_pos:mid+right_pos+1]
    
def find_maximum_subarray(array):
    if len(array) == 1:
        return array
    else:
        mid = len(array) // 2
        left_array = find_maximum_subarray(array[:mid])
        right_array = find_maximum_subarray(array[mid:])
        cross_array = find_max_crossing_subarray(array, mid)

        left_sum, right_sum, cross_sum = sum(left_array), sum(right_array), sum(cross_array)
        if left_sum &amp;gt; right_sum and left_sum &amp;gt; cross_sum:
            return left_array
        elif right_sum &amp;gt; left_sum and right_sum &amp;gt; cross_sum:
            return right_array
        return cross_array
    
test_array = [13, -3, -25, 20, -3, -16, -23, 18, 20, -7, 12, -5, -22, 15, -4, 7]
maximum_array = find_maximum_subarray(test_array)
print(maximum_array)
    
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：应该避免在Python代码中，修改处于迭代中的容器，我这么写只是想高度还原伪代码而已╮(╯_╰)╭。&lt;/p&gt;
&lt;p&gt;-EOF-&lt;/p&gt;
</content:encoded></item><item><title>awk命令简介（二）</title><link>https://zlotus.github.io/2014/07/20/awk-simple-guide-2/</link><guid isPermaLink="true">https://zlotus.github.io/2014/07/20/awk-simple-guide-2/</guid><pubDate>Sun, 20 Jul 2014 19:22:27 GMT</pubDate><content:encoded>&lt;p&gt;文章的干货来源于&lt;a href=&quot;http://coolshell.cn/&quot;&gt;酷壳&lt;/a&gt;的**&lt;a href=&quot;http://coolshell.cn/articles/9070.html&quot;&gt;AWK 简明教程&lt;/a&gt;&lt;strong&gt;以及&lt;/strong&gt;Linux与unix shell编程指南**。&lt;/p&gt;
&lt;p&gt;本文纯属搬砖。&lt;/p&gt;
&lt;p&gt;最近经常遇到需要处理大段文本输出的情况，所以想学习一下&lt;code&gt;awk&lt;/code&gt;命令，这个命令更像是一个微型语言。&lt;/p&gt;
&lt;p&gt;《awk命令简介（二）》着重介绍&lt;code&gt;awk&lt;/code&gt;命令的详情，内容主要来自**《Linux与unix shell编程指南》**。&lt;/p&gt;
&lt;p&gt;书接上回。&lt;/p&gt;
&lt;h1&gt;awk 命令详解&lt;/h1&gt;
&lt;h2&gt;调用awk&lt;/h2&gt;
&lt;p&gt;如果在命令行中使用awk，通常型为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk -F field-separator &apos;commands&apos; input-files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-F field-separator&lt;/code&gt;为可选项，用于指定域分隔符，默认为空格，如果需要处理如&lt;code&gt;&apos;,&apos;&lt;/code&gt;、&lt;code&gt;&apos;:&apos;&lt;/code&gt;、&lt;code&gt;&apos;;&apos;&lt;/code&gt;之类的字符做分隔符时，可以设置该值，如使用&lt;code&gt;&apos;:&apos;&lt;/code&gt;的情形：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk -F: &apos;commands&apos; input-files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以将awk命令写在文件中，作为脚本调用，如调用名为awk-script-file的脚本文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk -f awk-script-file input-files
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-f&lt;/code&gt;选项用于指定脚本文件名。&lt;/p&gt;
&lt;h2&gt;awk脚本&lt;/h2&gt;
&lt;h3&gt;模式和动作&lt;/h3&gt;
&lt;p&gt;awk脚本由各种&lt;strong&gt;动作&lt;/strong&gt;和&lt;strong&gt;模式&lt;/strong&gt;组成。&lt;/p&gt;
&lt;p&gt;awk使用&lt;code&gt;-F&lt;/code&gt;命令指定的分隔符（不指定时使用空格）分离记录中的域，直到发现新一行。这个动作将一直持续到文件结束。&lt;/p&gt;
&lt;p&gt;awk命令可以有许多语句，语句中的&lt;strong&gt;模式&lt;/strong&gt;用于控制&lt;strong&gt;动作&lt;/strong&gt;的触发条件。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;模式&lt;/strong&gt;可以是任何条件语句、复合语句或正则表达式。模式包括了两个特殊字段，&lt;code&gt;BEGIN&lt;/code&gt;和&lt;code&gt;END&lt;/code&gt;。&lt;code&gt;BEGIN&lt;/code&gt;语句使用在awk的任何文本浏览动作之前，之后awk文本浏览动作依据输入文件开始执行，&lt;code&gt;END&lt;/code&gt;语句用来用来在awk完成文本浏览动作后打印输出文本总数及结尾状态标识。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;动作&lt;/strong&gt;在&lt;code&gt;{}&lt;/code&gt;内声明，通常为打印动作，或是诸如条件语句或循环语句。如果不指名&lt;strong&gt;动作&lt;/strong&gt;，awk将打印所有浏览记录。&lt;/p&gt;
&lt;h3&gt;记录和域&lt;/h3&gt;
&lt;p&gt;awk会将读取的一条记录用&lt;code&gt;-F&lt;/code&gt;指定的分隔符分为多个域，并依次将域命名为&lt;code&gt;$1&lt;/code&gt;, &lt;code&gt;$2&lt;/code&gt;, &lt;code&gt;$3&lt;/code&gt;等，这些域名的使用类似于语言中变量名的使用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;{print $1, $3}&lt;/code&gt;就表示打印第1域和第3域，而&lt;code&gt;$0&lt;/code&gt;表示所有的域，所以&lt;code&gt;{print $0}&lt;/code&gt;表示打印所有域。&lt;/p&gt;
&lt;p&gt;上面的在&lt;code&gt;{}&lt;/code&gt;中的&lt;code&gt;print&lt;/code&gt;就是一个awk动作。&lt;/p&gt;
&lt;p&gt;假设有数据文件grade.txt（此文件会在本文的示例中大量出现），文件分隔符为&lt;code&gt;TAB&lt;/code&gt;（即&lt;code&gt;&apos;\t&apos;&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cat grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;常用的输入输出&lt;/h3&gt;
&lt;p&gt;使用命令行时通常我们将结果重定向到文件中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk -F\t &apos;{print $1, $3}&apos; grade.txt &amp;gt; wow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是使用&lt;code&gt;tee&lt;/code&gt;，在将结果输出至标准输出的同时，写入文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk -F\t &apos;{print $1, $3}&apos; grade.txt | tee wow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果是脚本，在输入上我们可以使用文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ belt.awk grade.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是使用标准输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ belt.awk &amp;lt; grade.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是管道：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ grade.txt | belt.awk
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;打印头尾信息&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;BEGIN&lt;/code&gt;打印表头，为了对齐使用了制表符&lt;code&gt;\t&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {print &quot;Name\tBelt\n--------------------&quot;}
quote&amp;gt; {print $1&quot;\t&quot;$4}&apos; grade.txt
Name	Belt
--------------------
M.Tansley	Green
J.Lulu	green
P.Bunny	Yellow
J.Troll	Brown-3
L.Transly	Brown-2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用&lt;code&gt;END&lt;/code&gt;打印表尾：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {print &quot;Name\n----------&quot;} {print $1} END {print &quot;----------\ntotle: &quot; NR}&apos; grade.txt
Name
----------
M.Tansley
J.Lulu
P.Bunny
J.Troll
L.Transly
----------
totle: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;条件操作&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作符&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;小于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;小于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;==&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;gt;=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;大于等于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配正则表达式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;!~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不匹配正则表达式&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;使用正则表达式匹配域时，用&lt;code&gt;~&lt;/code&gt;接&lt;code&gt;/Regular_Express/&lt;/code&gt;，如果使用&lt;code&gt;if&lt;/code&gt;语句则需要放在&lt;code&gt;()&lt;/code&gt;中。示例，为第4域匹配正则表达式，输出匹配的记录：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($4~/Brown/) print $0}&apos; grade.txt
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上例中不使用&lt;code&gt;if&lt;/code&gt;也可以，不指定&lt;strong&gt;动作&lt;/strong&gt;时awk默认输出整条记录：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$4~/Brown/&apos; grade.txt
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用正则表达式模糊匹配域：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($3~/48/) print $0}&apos; grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	26	26
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$3~/48/ {print $0}&apos; grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	26	26
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用&lt;code&gt;==&lt;/code&gt;精确匹配：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$3==48 {print $0}&apos; grade.txt
P.Bunny	02/99	48	Yellow	12	35	28
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($3==48) print $0}&apos; grade.txt
P.Bunny	02/99	48	Yellow	12	35	28
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;使用&lt;code&gt;!~&lt;/code&gt;的正则表达式：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$0 !~ /Brown/&apos; grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
P.Bunny	02/99	48	Yellow	12	35	28
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配合&lt;code&gt;if&lt;/code&gt;使用&lt;code&gt;!~&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($4!~/Brown/) print $1, $4}&apos; grade.txt
M.Tansley Green
J.Lulu green
P.Bunny Yellow
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;比较：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;小于：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($6&amp;lt;$7) print $1&quot; try better at next comp&quot;}&apos; grade.txt
M.Tansley try better at next comp
J.Lulu try better at next comp
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;小于等于：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($6&amp;lt;=$7) print $1}&apos; grade.txt
M.Tansley
J.Lulu
J.Troll
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;大于：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($6&amp;gt;$7) print $1}&apos; grade.txt
P.Bunny
L.Transly
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;常见的正则表达式功能：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;匹配大小写，使用&lt;code&gt;[]&lt;/code&gt;匹配字符：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;/[Gg]reen/&apos; grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通配符，&lt;code&gt;.&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$1 ~ /^....a/&apos; grade.txt
M.Tansley	05/99	48311	Green	8	40	44
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;逻辑或，&lt;code&gt;|&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$4 ~ /(Yellow|Brown)/&apos; grade.txt
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;行首&lt;code&gt;^&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;/^J/&apos; grade.txt
J.Lulu	06/99	48317	green	9	24	26
J.Troll	07/99	4842	Brown-3	12	26	26
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;行尾&lt;code&gt;$&lt;/code&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;/28$/&apos; grade.txt
P.Bunny	02/99	48	Yellow	12	35	28
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;复合逻辑&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;逻辑与，&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if ($1==&quot;P.Bunny&quot; &amp;amp;&amp;amp; $4==&quot;Yellow&quot;) print $0}&apos; grade.txt
P.Bunny	02/99	48	Yellow	12	35	28
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑或，&lt;code&gt;||&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if ($4==&quot;Yellow&quot; || $4~/Brown/) print $0}&apos; grade.txt
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;逻辑否，&lt;code&gt;!&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$4 != &quot;Brown&quot; {print $1, $4}&apos; grade.txt
M.Tansley Green
J.Lulu green
P.Bunny Yellow
J.Troll Brown-3
L.Transly Brown-2
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;内置变量的示例&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;已读取的记录数，&lt;code&gt;NR&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{print} END {print &quot;total: &quot;NR}&apos; grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
total: 5
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;NR&lt;/code&gt;检查保证文件记录数大于0：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if (NR&amp;gt;0 &amp;amp;&amp;amp; $4~/Brown/) print $0}&apos; grade.txt
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;当前记录的域数，&lt;code&gt;NF&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{print NF, NR, $0} END {print FILENAME}&apos; grade.txt
7 1 M.Tansley	05/99	48311	Green	8	40	44
7 2 J.Lulu	06/99	48317	green	9	24	26
7 3 P.Bunny	02/99	48	Yellow	12	35	28
7 4 J.Troll	07/99	4842	Brown-3	12	26	26
7 5 L.Transly	05/99	4712	Brown-2	12	30	28
grade.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;显示当前目录名：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ pwd
/Users/zealot
$ echo $PWD | awk -F/ &apos;{print $NF}&apos;
zealot
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;显示文件名：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ echo &quot;/Users/zealot/grade.txt&quot; | awk -F/ &apos;{print $NF}&apos;
grade.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;awk操作符&lt;/h3&gt;
&lt;p&gt;在awk中使用操作符，基本表达式可以划分为数字型、字符串型、变量型、域及数组元素。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作符&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;=&lt;/code&gt; &lt;code&gt;+=&lt;/code&gt; &lt;code&gt;*=&lt;/code&gt; &lt;code&gt;/=&lt;/code&gt; &lt;code&gt;%=&lt;/code&gt; &lt;code&gt;^=&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;赋值操作符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;条件表达操作符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; &lt;code&gt;!&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;与、或、非操作符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~&lt;/code&gt; &lt;code&gt;!~&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配操作符，包括匹配和不匹配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;&lt;/code&gt; &lt;code&gt;&amp;lt;=&lt;/code&gt; &lt;code&gt;==&lt;/code&gt; &lt;code&gt;!=&lt;/code&gt; &lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;关系操作符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt; &lt;code&gt;-&lt;/code&gt; &lt;code&gt;*&lt;/code&gt; &lt;code&gt;/&lt;/code&gt; &lt;code&gt;%&lt;/code&gt; &lt;code&gt;^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;算术操作符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;++&lt;/code&gt; &lt;code&gt;--&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;前缀和后缀&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;设置输入域到域变量名：&lt;/p&gt;
&lt;p&gt;给特定的数据域设置有意义的域名，可以使得代码更加清楚明了。&lt;/p&gt;
&lt;p&gt;一般的变量名设置方式为&lt;code&gt;name=$n&lt;/code&gt;，如在grade.txt的例子中，可以设置&lt;code&gt;name=$1;belts=$4&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{name=$1;belts=$4; if(belts~/Yellow/) print name &quot; is belt &quot; belts}&apos; grade.txt
P.Bunny is belt Yellow
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;域值的比较操作：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;有两种方式测试一数值域是否小于另一数值域：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在关系操作中使用实际数值；&lt;/li&gt;
&lt;li&gt;在&lt;code&gt;BEGIN&lt;/code&gt;中给变量名赋值；
关系操作必须写在圆括号内，如查询比赛中得分在27以下的学生：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($6&amp;lt;27) print $0}&apos; grade.txt
J.Lulu	06/99	48317	green	9	24	26
J.Troll	07/99	4842	Brown-3	12	26	26
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;BEGIN&lt;/code&gt;中给变量名赋值可以使代码更清晰：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {BASELINE=27} {if($6&amp;lt;BASELINE) print $0}&apos; grade.txt
J.Lulu	06/99	48317	green	9	24	26
J.Troll	07/99	4842	Brown-3	12	26	26
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;修改数值域取值：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要注意的是，当在awk中修改任何域时，实际输入文件是不会被修改的，修改的只是 保存在缓存里的awk复本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($1==&quot;M.Tansley&quot;) $6=$6-1; print NR, NF $1, $6, $7}&apos; grade.txt
M.Tansley 39 44
J.Lulu 24 26
P.Bunny 35 28
J.Troll 26 26
L.Transly 30 28
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;修改文本域：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($1==&quot;J.Troll&quot;) ($1=&quot;J.L.Troll&quot;); print $1}&apos; grade.txt
M.Tansley
J.Lulu
P.Bunny
J.L.Troll
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;赋值表达式通常放在括号中，以增强代码的可读性，非必须，如上一项中的&lt;code&gt;$6=$6-1&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;只显示修改记录：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($1==&quot;J.Troll&quot;) {$1=&quot;J.L.Troll&quot;; print $1}}&apos; grade.txt
J.L.Troll
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处使用&lt;code&gt;if&lt;/code&gt;语句取得需要修改的位置，而后再使用赋值动作&lt;code&gt;$1=&quot;J.L.Troll&quot;&lt;/code&gt;并打印。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建新的输出域：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {print &quot;Name\tDifference&quot;} {if($6&amp;lt;$7) {$8=$7-$6; print $1, $8}}&apos; grade.txt
Name	Difference
M.Tansley 4
J.Lulu 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是直接将计算结果赋给变量名并打印：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {print &quot;Name\tDifference&quot;} {if($6&amp;lt;$7) {diff=$7-$6; print $1, diff}}&apos; grade.txt
Name	Difference
M.Tansley 4
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;列值累加：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;total+=$6; END{print &quot;Club student total points: &quot; total}&apos; grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
Club student total points: 155
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要注意的是，不必在&lt;code&gt;awk&lt;/code&gt;中显式说明打印所有记录，当每一个操作匹配时，打印是缺省动作。&lt;/p&gt;
&lt;p&gt;如果不想这种缺省打印动作出现，可以显式声明不带打印命令的动作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{total+=$6} END{print &quot;Club student total points: &quot; total}&apos; grade.txt
Club student total points: 155
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;累加动作并不一定要写在圆括号中，但是这样做可以增强代码可读性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;另一个累加列的示例，统计当前目录中文件的总大小：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ls -lAG | awk &apos;$1~/^[^d]/ {print $9&quot;\t&quot;$5} {total+=$5} END {print &quot;total &quot; total/1024 &quot; KB&quot;}&apos;
...file name followed by size...
grade.txt	176
total 234.544 KB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;大概过程是，先把当前目录中的文件信息列表用管道送给&lt;code&gt;awk&lt;/code&gt;，而后使用第一个域滤掉目录项（d开头的项目，其实直接用&lt;code&gt;/^[^d]/&lt;/code&gt;匹配整行就可以了），再累加第五列，最后在打印处理成以&lt;code&gt;KB&lt;/code&gt;为单位的结果。&lt;/p&gt;
&lt;h3&gt;内置字符串函数&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;函数&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;gsub(r,s)&lt;/td&gt;
&lt;td&gt;在整个$0中用s替代r&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gsub(r,s,t)&lt;/td&gt;
&lt;td&gt;在整个t中用s替代r&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;index(s,t)&lt;/td&gt;
&lt;td&gt;返回s中字符串t的第一位置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;length(s)&lt;/td&gt;
&lt;td&gt;返回s长度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;match(s,r)&lt;/td&gt;
&lt;td&gt;测试s是否包含匹配r的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;split(s,a,fs)&lt;/td&gt;
&lt;td&gt;在fs上将s分成序列a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sprint(fmt,exp)&lt;/td&gt;
&lt;td&gt;返回经fmt格式化后的exp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sub(r,s)&lt;/td&gt;
&lt;td&gt;用$0中最左边最长的子串代替s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;substr(s,p)&lt;/td&gt;
&lt;td&gt;返回字符串s中从p开始的后缀部分&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;substr(s,p,n)&lt;/td&gt;
&lt;td&gt;返回字符串s中从p开始长度为n的后缀部分&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;gsub(r,s)&lt;/code&gt;, &lt;code&gt;gsub(r,s,t)&lt;/code&gt;，在整个个记录中匹配字符串模式r，并用s进行替换。若不指定t，gsub函数默认在&lt;code&gt;$0&lt;/code&gt;上替换：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;gsub(/4842/, 4899) {print $0} &apos; grade.txt
J.Troll	07/99	4899	Brown-3	12	26	26
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;index(s,t)&lt;/code&gt;，返回字符串s中t第一次出现的位置：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {print index(&quot;Bunny&quot;, &quot;ny&quot;)}&apos; grade.txt
4
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;length(s)&lt;/code&gt;，返回字符串s的长度：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$1==&quot;J.Troll&quot; {print length($1) &quot; &quot; $1}&apos; grade.txt
7 J.Troll
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或是直接传入字符串：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {print length(&quot;Hello World~&quot;)}&apos;
12
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;match(s,r)&lt;/code&gt;，测试目标串s是否包含匹配模式r的字符串，若包含，则返回r第一次出现的位置，若不包含则返回0：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {print match(&quot;ABCD&quot;, /d/)}&apos;
0
$ awk &apos;BEGIN {print match(&quot;ABCD&quot;, /C/)}&apos;
3
$ awk &apos;$1==&quot;J.Lulu&quot; {print match($1, &quot;u&quot;)}&apos; grade.txt
4
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;`split(s,a,fs)，使用分隔符fs将字符串s切割，存入数组a中，并返回切割后&amp;lt;span id=&quot;awk_array&quot;&amp;gt;数组&amp;lt;/span&amp;gt;元素个数：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN {print split(&quot;123#456#789&quot;, myarray, &quot;#&quot;), myarray[1], myarray[2], myarray[3]}&apos;
3 123 456 789
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;sub(r,s,t)&lt;/code&gt;，在t中查找模式r，将第一次匹配到的字符串替换为s：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$1==&quot;J.Troll&quot; {sub(/26/, 29, $0)} {print $0}&apos; grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	29	26
L.Transly	05/99	4712	Brown-2	12	30	28
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;substr(s,p)&lt;/code&gt;, &lt;code&gt;substr(s,p,n)&lt;/code&gt;，在字符串s中截取从p开始到字符串结束的后缀并返回，若指定n，则截取从p开始长度为n的字符串并返回：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$1==&quot;J.Troll&quot; {print substr($0, 3)}&apos; grade.txt
Troll	07/99	4842	Brown-3	12	26	26
$ awk &apos;$1==&quot;J.Troll&quot; {print substr($0, 3, 5)}&apos; grade.txt
Troll
$ awk &apos;{print substr($1, 3)}&apos; grade.txt
Tansley
Lulu
Bunny
Troll
Transly
$ awk &apos;BEGIN {STR=&quot;hello world~&quot;} END {print substr(STR,7)}&apos; grade.txt
world~
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;另：通过管道使用awk函数的例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;stand-by&quot; | awk &apos;{print length($0)}&apos;
8
$ STR=&quot;mydoc.txt&quot;
$ echo $STR | awk &apos;{print substr($0,1,5)}&apos;
mydoc
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;awk&lt;/code&gt;命令的&lt;code&gt;printf&lt;/code&gt;函数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;printf&lt;/code&gt;的基本用法和C语言类似，形式为&lt;code&gt;printf format, values&lt;/code&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;awk printf修饰符&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;pre&gt;&lt;code&gt;              |左对齐
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;width               |域的步长，用0表示0步长
.prec               |最大字符串长度，或小数点右边的位数（精确度）&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;awk printf格式控制符&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%c&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ASCII字符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%d&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;整型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;浮点数，科学计数法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%f&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;浮点数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%g&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;浮点数，由awk决定使用&lt;code&gt;%e&lt;/code&gt;或&lt;code&gt;%f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%o&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;八进制数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%s&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;%x&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;十六进制数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;文字转换：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ echo 65 | awk &apos;{printf &quot;%c\n&quot;, $0}&apos;
A
$ awk &apos;BEGIN {printf &quot;%c\n&quot;, 65}&apos;
A
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;格式化输出：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第一列为行号，步长3字符，左对齐；第二列为姓名，步长15字符，左对齐；第四列为序列号，后接换行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{printf &quot;%-3d%-15s%s\n&quot;, NR,$1,$3}&apos; grade.txt
1  M.Tansley      48311
2  J.Lulu         48317
3  P.Bunny        48
4  J.Troll        4842
5  L.Transly      4712
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或加上表头：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;BEGIN{printf &quot;%-3s%-15s%s\n---------------------------\n&quot;, &quot;NR&quot;,&quot;Name&quot;,&quot;No.&quot;} {printf &quot;%-3d%-15s%s\n&quot;, NR,$1,$3}&apos; grade.txt
NR Name           No.
---------------------------
1  M.Tansley      48311
2  J.Lulu         48317
3  P.Bunny        48
4  J.Troll        4842
5  L.Transly      4712
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;向一行awk命令传参数：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($5&amp;lt;AGE) print $0}&apos; AGE=10 grade.txt
M.Tansley	05/99	48311	Green	8	40	44
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;awk脚本文件：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;将awk写入文件中执行，就不必每次输入大段命令了，而且可以好的排版和注释，来提高代码的可读性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;场景1，将上面的命令写入脚本文件：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/awk -f
# all comment line must start with a hash `#`
# name: student_total.awk
# to call: student_total.awk grade.txt
# print total and average of club student points

# print a header first:
BEGIN{
    print &quot;Student  Date    Member No.  Grade   Age Points  Max&quot;
    print &quot;Name     Joined                          Gained  Point Available&quot;
    print &quot;================================================================&quot;
}

# let&apos;s add the scores of points gained
(total+=$6)

# finished processing, now let&apos;s print the total and average points:
END{
    print &quot;Club students total points: &quot; total
    print &quot;Average club students points: &quot; total/NR
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接下来使用&lt;code&gt;chmod&lt;/code&gt;给脚本加上“所有用户可执行”的权限，再使用&lt;code&gt;cp&lt;/code&gt;将脚本复制到shell可以找到的地方：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ chmod a+x student_total.awk
$ sudo cp student_total.awk /usr/bin/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ student_total.awk grade.txt
Student  Date    Member No.  Grade   Age Points  Max
Name     Joined                          Gained  Point Available
================================================================
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
P.Bunny	02/99	48	Yellow	12	35	28
J.Troll	07/99	4842	Brown-3	12	26	26
L.Transly	05/99	4712	Brown-2	12	30	28
Club students total points: 155
Average club students points: 31
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺便提一下，在OSX上使用&lt;code&gt;ls&lt;/code&gt;命令时，会发现有的目录或文件的权限位的最后附有一个&lt;code&gt;@&lt;/code&gt;或&lt;code&gt;+&lt;/code&gt;符号，其中&lt;code&gt;@&lt;/code&gt;说明该目录或文件具有&lt;em&gt;extended attributes&lt;/em&gt;，而&lt;code&gt;+&lt;/code&gt;符合说明该目录或文件具有类似ACL的非标准权访问限控制策略。可以通过在&lt;code&gt;ls&lt;/code&gt;后添加&lt;code&gt;-@&lt;/code&gt;命令查看。详情见&lt;a href=&quot;http://psionides.eu/2008/06/30/ls-on-mac-and-extended-file-attributes/&quot;&gt;&quot;ls&quot; on mac and extended attributes&lt;/a&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;场景2，清除重复输出：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当调试程序时，遇到这样一个日志文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ cat error.log
INVALID LCSD 98GJ23
ERROR*
ERROR*
CAUTION LPSS ERROR ON ACC NO.
ERROR*
ERROR*
ERROR*
ERROR*
ERROR*
PASS FIELD INVALID ON LDPS
ERROR*
ERROR*
PASS FIELD INVALID ON GHSI
ERROR*
CAUTION LPSS ERROR ON ACC NO.
ERROR*
ERROR*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们想将其中的重复的多行&lt;code&gt;ERROR*&lt;/code&gt;合并为一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/awk -f
# name: error_strip.awk
# to call: error_strip.awk &amp;lt;filename&amp;gt;
# strips out the ERROR* lines if there are more than one
# ERROR* lines after each failed record.

BEGIN{error_line=&quot;&quot;}

# tell awk who is &quot;ERROR*&quot;
{
    if($0==&quot;ERROR*&quot; &amp;amp;&amp;amp; error_line==&quot;ERROR*&quot;)
# goto next line
        next;
    error_line=$0;
    print
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ error_strip.awk error.log
INVALID LCSD 98GJ23
ERROR*
CAUTION LPSS ERROR ON ACC NO.
ERROR*
PASS FIELD INVALID ON LDPS
ERROR*
PASS FIELD INVALID ON GHSI
ERROR*
CAUTION LPSS ERROR ON ACC NO.
ERROR*
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;在脚本中制定分隔符FS：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;再复习一下，在shell中使用&lt;code&gt;awk&lt;/code&gt;命令时，用&lt;code&gt;-F&lt;/code&gt;指定分隔符：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk -F: &apos;{print $0}&apos; input-file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而在脚本中，则是设置&lt;code&gt;FS&lt;/code&gt;变量，值得注意的都是，&lt;code&gt;FS&lt;/code&gt;变量需要放在&lt;code&gt;BEGIN&lt;/code&gt;部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/awk -f
# to call: passwd.awk /private/etc/passwd
# print out the first and fifth fields

BEGIN{FS=&quot;:&quot;}

{
    # pass the comments of the file
    if($0 ~ /^#/)
        next;
    print $1&quot;\t&quot;$5
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nobody	Unprivileged User
root	System Administrator
daemon	System Services
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;向脚本传递参数：&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;前面提到，在shell中使用awk命令时传递参数的形式是：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{if($5&amp;lt;AGE) print $0}&apos; AGE=10 grade.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;而使用脚本时，传递的方式的形式也基本相同：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;awk script_file var=value input_file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/awk -f
# check on how many fields in a file
# name: field_check.awk
# to call: field_check.awk MAX=n FS=&amp;lt;separator&amp;gt; input-file

NF!=MAX
{
    print(&quot;line &quot; NR &quot; does not have &quot; MAX &quot; fields&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用时命令为&lt;code&gt;field_check.awk MAX=7 FS=&quot;:&quot; /private/etc/passwd&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将上面提到的传递“AGE”参数的例子改写，就应为：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/awk -f
# name: age.awk
# to call: age.awk AGE=10 grade.txt
# print students whose age are lower than age supplied on the command line
{
    if($5&amp;lt;AGE) print $0
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ age.awk AGE=10 grade.txt
M.Tansley	05/99	48311	Green	8	40	44
J.Lulu	06/99	48317	green	9	24	26
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过管道使用脚本：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从&lt;code&gt;du&lt;/code&gt;命令获得数据后处理输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/awk -f
# name: du.awk
# to call: du | du.awk
# print file/direc&apos;s in bytes and blocks

BEGIN{
    OFS=&quot;\t&quot;;
    print &quot;name&quot;, &quot;\t\t&quot;, &quot;bytes&quot;, &quot;blocks\n&quot;;
    print &quot;==================================&quot;
}

{print $2, &quot;\t\t&quot;, $1*512, $1}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ du | du.awk
name				bytes	blocks

==================================
.				20461768704	39964392
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;awk数组：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&quot;#awk_array&quot;&gt;上面&lt;/a&gt;也提到了awk数组，现在看看更多关于数组的应用。&lt;/p&gt;
&lt;p&gt;数组在使用前，不需要声明，也不需要设定元素个数，我们经常会在循环中使用到数组，基本语法如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;For (element in array) print array(element)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;如果把上面的例子在脚本中重新实现：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/awk -f
# name: array_test.awk
# prints out an array
BEGIN{
    recode=&quot;123#456#789&quot;;
    split(recode, myarray, &quot;#&quot;);
}
END{
    for(i in myarray) print i, myarray[i]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时我们可以使用&lt;code&gt;/dev/null&lt;/code&gt;作为输入文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ array_test.awk /dev/null
2 456
3 789
1 123
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;一个稍微复杂的统计示例：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$ cat grade_student.txt
Yellow#Junior
Orange#Senior
Yellow#Junior
Purple#Junior
Brown-2#Junior
White#Senior
Orange#Senior
Red#Junior
Brown-2#Senior
Yellow#Senior
Red#Junior
Blue#Senior
Green#Senior
Purple#Junior
White#Junior
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要统计文件中黄、橙、红带各有多少种，分别有多少成年人和未成年人：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/awk -f
# name: belts.awk
# to call: belts.awk grade_student.txt
# loop through the file and count how many belts we have
# in (yellow, orange, red) also count how many adults and
# juniors we have.
#
# start from BEGIN
# set FS and load the arrays with our values
BEGIN{
    FS=&quot;#&quot;
# load the belt colors we are interested in only
    belt[&quot;Yellow&quot;]
    belt[&quot;Orange&quot;]
    belt[&quot;Red&quot;]
# load the type of students
    student[&quot;Junior&quot;]
    student[&quot;Senior&quot;]
}
# loop through array that holds the belt colors against field-1
# if we have a match, keep a running total
{
    for(color in belt)
        if($1==color)
            belt[color]++
}
# loop through array that holds the student type against field-2
# if we have a match, keep a running total
{
    for(type in student)
        if($2==type)
            student[type]++
}
# finish processing, print out the match for each array
END{
    for(color in belt)
        print &quot;The club has&quot;, belt[color], color, &quot;belts&quot;
    for(type in student)
        print &quot;The club has&quot;, student[type], type, &quot;students&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ belts.awk grade_student.txt
The club has 2 Orange belts
The club has 2 Red belts
The club has 3 Yellow belts
The club has 8 Junior students
The club has 7 Senior students
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;awk是shell中的一个重要文本处理工具，在特定情况下可以方便快捷的配合其他命令输出有格式要求的文本并统计，就像&lt;a href=&quot;http://talk.linuxtoy.org/using-cli/#2&quot;&gt;像黑客一样使用Linux命令行：我喜欢使用命令行的原因&lt;/a&gt;中提到的这样~&lt;/p&gt;
&lt;p&gt;-EOF-&lt;/p&gt;
</content:encoded></item><item><title>awk命令简介（一）</title><link>https://zlotus.github.io/2014/07/11/awk-simple-guide-1/</link><guid isPermaLink="true">https://zlotus.github.io/2014/07/11/awk-simple-guide-1/</guid><pubDate>Fri, 11 Jul 2014 19:11:27 GMT</pubDate><content:encoded>&lt;p&gt;干货来源于&lt;a href=&quot;http://coolshell.cn/&quot;&gt;酷壳&lt;/a&gt;的**&lt;a href=&quot;http://coolshell.cn/articles/9070.html&quot;&gt;AWK 简明教程&lt;/a&gt;&lt;strong&gt;以及&lt;/strong&gt;Linux与unix shell编程指南**。&lt;/p&gt;
&lt;p&gt;本文纯属搬砖。&lt;/p&gt;
&lt;p&gt;最近经常遇到需要处理大段文本输出的情况，所以想学习一下&lt;code&gt;awk&lt;/code&gt;命令，这个命令更像是一个微型语言。&lt;/p&gt;
&lt;p&gt;《awk命令简介（一）》先着重介绍&lt;code&gt;awk&lt;/code&gt;的应用场景，内容主要来自&lt;a href=&quot;http://coolshell.cn/&quot;&gt;酷壳&lt;/a&gt;的**&lt;a href=&quot;http://coolshell.cn/articles/9070.html&quot;&gt;AWK 简明教程&lt;/a&gt;**。&lt;/p&gt;
&lt;h1&gt;awk快速上手&lt;/h1&gt;
&lt;h2&gt;待处理的文本&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ netstat -a &amp;gt; output
$ cat output | more -n 6
$ Proto Recv-Q Send-Q  Local Address          Foreign Address        (state)
tcp4       0      0  192.168.1.100.62750    114.112.202.20.http    SYN_SENT
tcp4       0      0  192.168.1.100.62749    113.103.139.251.10684  CLOSE_WAIT
tcp4       0      0  192.168.1.100.62748    202.150.16.5.cslistene ESTABLISHED
tcp4       0      0  192.168.1.100.62747    114.112.202.20.http    ESTABLISHED
tcp4       0      0  192.168.1.100.62746    114.82.234.97.12077    SYN_SENT
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;最简单的选择列，输出第一列&lt;code&gt;$1&lt;/code&gt;与第四列&lt;code&gt;$4&lt;/code&gt;&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{print $1, $4}&apos; output
Proto Local-Address
tcp4 192.168.1.100.62750
tcp4 192.168.1.100.62749
tcp4 192.168.1.100.62748
tcp4 192.168.1.100.62747
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;C风格的格式化输出&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;{printf &quot;%-8s %-8s %-8s %-18s %-22s %-15s\n&quot;, $1, $2, $3, $4, $5, $6}&apos; output
Proto    Recv-Q   Send-Q   Local-Address      Foreign-Address        (state)
tcp4     0        0        192.168.1.100.62750 114.112.202.20.http    SYN_SENT
tcp4     0        0        192.168.1.100.62749 113.103.139.251.10684  CLOSE_WAIT
tcp4     0        0        192.168.1.100.62748 202.150.16.5.cslistene ESTABLISHED
tcp4     0        0        192.168.1.100.62747 114.112.202.20.http    ESTABLISHED
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;过滤条目&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;取第三列为0且第6列位“LISTEN”的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$3==0 &amp;amp;&amp;amp; $6==&quot;LISTEN&quot;&apos; output
tcp4       0      0  *.oms                  *.*                    LISTEN
tcp4       0      0  *.socks                *.*                    LISTEN
tcp4       0      0  *.58932                *.*                    LISTEN
tcp4       0      0  localhost.ddi-tcp-1    *.*                    LISTEN
tcp4       0      0  localhost.51219        *.*                    LISTEN
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;同时用内建变量&lt;code&gt;NR&lt;/code&gt;选出表头（第一行）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$3==0 &amp;amp;&amp;amp; $6==&quot;LISTEN&quot; || NR==1&apos; output
Proto Recv-Q Send-Q  Local-Address          Foreign-Address        (state)
tcp4       0      0  *.oms                  *.*                    LISTEN
tcp4       0      0  *.socks                *.*                    LISTEN
tcp4       0      0  *.58932                *.*                    LISTEN
tcp4       0      0  localhost.ddi-tcp-1    *.*                    LISTEN
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取第三列大于0的数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos; $3&amp;gt;0 {print $0}&apos; output
Proto Recv-Q Send-Q  Local-Address          Foreign-Address        (state)
tcp4       0     68  192.168.1.100.62743    123.138.206.178.8948   ESTABLISHED
tcp4       0     68  192.168.1.100.62720    123.138.54.160.mdqs    ESTABLISHED
tcp4       0      9  192.168.1.100.62707    180.97.152.55.http-alt FIN_WAIT_1
tcp4       0     68  192.168.1.100.62695    123.138.206.178.8948   FIN_WAIT_1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;取第三列为0且第六列为“LISTEN”的数据，并进行格式输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$3==0 &amp;amp;&amp;amp; $6==&quot;LISTEN&quot; || NR==1 {printf &quot;%-20s %-20s %s\n&quot;,$4,$5,$6}&apos; output
Local-Address        Foreign-Address      (state)
*.oms                *.*                  LISTEN
*.socks              *.*                  LISTEN
*.58932              *.*                  LISTEN
localhost.ddi-tcp-1
*.*                  LISTEN
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;内置变量名称&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;td&gt;当前记录（行）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$1~$n&lt;/td&gt;
&lt;td&gt;当前行的第n个字段（列）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FS&lt;/td&gt;
&lt;td&gt;输入字段（列）分隔符，默认为space或tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NF&lt;/td&gt;
&lt;td&gt;当前记录（行）的字段（列）数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NR&lt;/td&gt;
&lt;td&gt;总记录（行）数，多文件记录（行）数累加，不清零&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FNR&lt;/td&gt;
&lt;td&gt;当前文件记录（行）数，每文件清零&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RS&lt;/td&gt;
&lt;td&gt;输入换行符，默认系统换行符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OFS&lt;/td&gt;
&lt;td&gt;输出字段（列）分隔符，默认为space&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ORS&lt;/td&gt;
&lt;td&gt;输出换行符，默认系统换行符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FILENAME&lt;/td&gt;
&lt;td&gt;当前文件名&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;输出行号&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$3==0 &amp;amp;&amp;amp; $6==&quot;ESTABLISHED&quot; || NR==1 {printf &quot;%02s %s %-20s %-20s %s %s&quot;, NR, FNR, $4, $5, $6, ORS}&apos; output
01 1 Local-Address        Foreign-Address      (state)
04 4 192.168.1.100.62748  202.150.16.5.cslistene ESTABLISHED
05 5 192.168.1.100.62747  114.112.202.20.http  ESTABLISHED
07 7 192.168.1.100.62744  114.112.202.20.http  ESTABLISHED
11 11 192.168.1.100.62739  14.157.50.39.13566   ESTABLISHED
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定输入字段（列）分隔符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk  &apos;BEGIN{FS=&quot;:&quot;} {print $1,$3,$6}&apos; /etc/passwd
nobody -2 /var/empty
root 0 /var/root
daemon 1 /var/root
_uucp 4 /var/spool/uucp
_taskgated 13 /var/empty
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;也可以用&lt;code&gt;-F&lt;/code&gt;指定&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk  -F: &apos;{print $1,$3,$6}&apos; /etc/passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或指定多种分隔符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk  -F &apos;[;:]&apos; &apos;{print $1,$3,$6}&apos; /etc/passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指定输出字段（列）分隔符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk  -F: &apos;{print $1,$3,$6}&apos; OFS=&quot;\t&quot; /etc/passwd
nobody  -2      /var/empty
root    0       /var/root
daemon  1       /var/root
_uucp   4       /var/spool/uucp
_taskgated      13      /var/empty
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;字符串匹配&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;~&lt;/code&gt;表示匹配开始，&lt;code&gt;//&lt;/code&gt;中是模式，类似正则表达式&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;匹配第六个字段（列）有“FIN”的记录（行）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$6 ~ /FIN/ || NR==1 {printf &quot;%-4s %-20s %-30s %-10s\n&quot;, NR, $4, $5, $6}&apos; output
1    Local-Address        Foreign-Address                (state)
21   192.168.1.100.62707  180.97.152.55.http-alt         FIN_WAIT_1
22   192.168.1.100.62695  123.138.206.178.8948           FIN_WAIT_1
24   192.168.1.100.62690  218.86.133.247.8368            FIN_WAIT_1
25   192.168.1.100.62689  14.157.50.39.13566             FIN_WAIT_1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或是有“WAIT”的记录&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$6 ~ /WAIT/ || NR==1 {printf &quot;%-4s %-20s %-30s %-10s\n&quot;, NR, $4, $5, $6}&apos; output
1    Local-Address        Foreign-Address                (state)
3    192.168.1.100.62749  113.103.139.251.10684          CLOSE_WAIT
21   192.168.1.100.62707  180.97.152.55.http-alt         FIN_WAIT_1
22   192.168.1.100.62695  123.138.206.178.8948           FIN_WAIT_1
24   192.168.1.100.62690  218.86.133.247.8368            FIN_WAIT_1
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或是直接匹配一整条记录（行）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;/LISTEN/&apos; output
tcp4       0      0  *.oms                  *.*                    LISTEN
tcp4       0      0  *.socks                *.*                    LISTEN
tcp4       0      0  *.58932                *.*                    LISTEN
tcp4       0      0  localhost.ddi-tcp-1    *.*                    LISTEN
tcp4       0      0  localhost.51219        *.*                    LISTEN
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;像正则表达式一样，使用&lt;code&gt;|&lt;/code&gt;表示“或”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$6 ~ /CLOSE|LAST/ {printf &quot;%-4s %-20s %-30s %-10s\n&quot;, NR, $4, $5, $6}&apos; output
3    192.168.1.100.62749  113.103.139.251.10684          CLOSE_WAIT
10   192.168.1.100.62741  114.82.234.97.12077            LAST_ACK
13   192.168.1.100.62729  114.82.234.97.12077            LAST_ACK
16   192.168.1.100.62722  114.82.234.97.12077            LAST_ACK
19   192.168.1.100.62711  113.103.139.251.10684          LAST_ACK
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用&lt;code&gt;!&lt;/code&gt;取反&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;$6 !~ /WAIT/ {printf &quot;%-4s %-20s %-30s %-10s\n&quot;, NR, $4, $5, $6}&apos; output
1    Local-Address        Foreign-Address                (state)
2    192.168.1.100.62750  114.112.202.20.http            SYN_SENT
4    192.168.1.100.62748  202.150.16.5.cslistene         ESTABLISHED
5    192.168.1.100.62747  114.112.202.20.http            ESTABLISHED
6    192.168.1.100.62746  114.82.234.97.12077            SYN_SENT
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;也可以&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;!/WAIT/&apos; output
Proto Recv-Q Send-Q  Local-Address          Foreign-Address        (state)
tcp4       0      0  192.168.1.100.62750    114.112.202.20.http    SYN_SENT
tcp4       0      0  192.168.1.100.62748    202.150.16.5.cslistene ESTABLISHED
tcp4       0      0  192.168.1.100.62747    114.112.202.20.http    ESTABLISHED
tcp4       0      0  192.168.1.100.62746    114.82.234.97.12077    SYN_SENT
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;拆分文件&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;按照第六个字段（列）拆分文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;NR!=1 {print &amp;gt; $6}&apos; output
$ ls
CLOSE_WAIT  ESTABLISHED FIN_WAIT_1  FIN_WAIT_2  LAST_ACK    LISTEN      SYN_SENT    output
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;也可以指定要输出的字段（列）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;NR!=1 {print $4, $5 &amp;gt; $6}&apos; output
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或是更加复杂，注意&lt;code&gt;quote&amp;gt;&lt;/code&gt;说明了&lt;code&gt;awk&lt;/code&gt;是一个脚本解释器&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;NR!=1{if($6 ~ /TIME|ESTABLISHED/) print &amp;gt; &quot;1.txt&quot;;
quote&amp;gt; else if($6 ~ /LISTEN/) print &amp;gt; &quot;2.txt&quot;;
quote&amp;gt; else print &amp;gt; &quot;3.txt&quot; }&apos; output

$ ls ?.txt
1.txt  2.txt  3.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;统计&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;统计当前目录下.py, .pyc, .pyo文件的总大小&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ ls -l *.py *.pyc *.pyo | awk &apos;{sum+=$5} END {print sum}&apos;
11609730
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;或是按照第六个字段（列）统计，类似GROUP BY，其中&lt;code&gt;a&lt;/code&gt;类似Python中的dict（键-值）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ awk &apos;NR!=1 {a[$6]++;} END {for (i in a) print i &quot;, &quot; a[i];}&apos; output
LISTEN, 12
FIN_WAIT_1, 51
FIN_WAIT_2, 23
SYN_SENT, 3常见的正则表达式功能
LAST_ACK, 21
CLOSE_WAIT, 5
ESTABLISHED, 20
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;统计每个用户的进程所占用的内存信息&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ aux | awk &apos;NR!=1 {a[$1]+=$6;} END { for(i in a) print i &quot;, &quot; a[i]&quot;KB&quot;;}&apos;
root, 718624KB
_usbmuxd, 2768KB
_locationd, 7780KB
_mdnsresponder, 3760KB
zealot, 3800464KB
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;-EOF-&lt;/p&gt;
</content:encoded></item><item><title>关于我</title><link>https://zlotus.github.io/2014/07/09/about-me/</link><guid isPermaLink="true">https://zlotus.github.io/2014/07/09/about-me/</guid><description>关于我</description><pubDate>Wed, 09 Jul 2014 09:10:11 GMT</pubDate><content:encoded>&lt;p&gt;关于我……&lt;/p&gt;
&lt;p&gt;80后程序员，在某研究所从事打杂工作。&lt;/p&gt;
&lt;p&gt;喜欢写代码和撸铁。空闲时间热爱尝试新的技术、开发自用的玩具，或者翻译一些感兴趣的英文文献。&lt;/p&gt;
&lt;p&gt;没什么特别的优缺点，希望做一个过着普通生活的普通人。&lt;/p&gt;
</content:encoded></item><item><title>学习T神的《像黑客一样使用Linux命令行》</title><link>https://zlotus.github.io/2014/07/07/using-cli-like-a-hacker/</link><guid isPermaLink="true">https://zlotus.github.io/2014/07/07/using-cli-like-a-hacker/</guid><description>转自《像黑客一样使用 Linux 命令行》</description><pubDate>Mon, 07 Jul 2014 21:21:27 GMT</pubDate><content:encoded>&lt;p&gt;po主Linux/Unix奇菜无比，看到Toy大神的PPT&lt;a href=&quot;https://linuxtoy.org/archives/using-cli-like-a-hacker.html&quot;&gt;《像黑客一样使用 Linux 命令行》&lt;/a&gt;画面太美，就必须学习一下。&lt;/p&gt;
&lt;p&gt;依然是用OSX冒充Linux…&lt;/p&gt;
&lt;p&gt;Talk is cheap...&lt;/p&gt;
&lt;h2&gt;1. 有趣的历史命令统计脚本&lt;/h2&gt;
&lt;p&gt;用简单的几行命令找出shell执行过的历史命令中出镜率最高的10个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  history |
&amp;gt; awk &apos;{CMD[$2]++;count++;}END \
&amp;gt; { for (a in CMD)print CMD[a] &quot; &quot; \
&amp;gt; CMD[a]/count*100 &quot;% &quot; a;}&apos; |
&amp;gt; grep -v &quot;./&quot; |
&amp;gt; column -c3 -s &quot; &quot; -t |
&amp;gt; sort -nr |
&amp;gt; nl |
&amp;gt; head -n10
     1	265  10.3962%    git
     2	258  10.1216%    ls
     3	205  8.04237%    sudo
     4	166  6.51236%    man
     5	164  6.4339%     awk
     6	118  4.62927%    vi
     7	92   3.60926%    cd
     8	80   3.13849%    port
     9	76   2.98156%    cat
    10	73   2.86387%    ps
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;history&lt;/code&gt;其实是&lt;code&gt;fc -l 1&lt;/code&gt;的别名，而&lt;code&gt;fc&lt;/code&gt;命令可以用来&lt;strong&gt;列出&lt;/strong&gt;、&lt;strong&gt;编辑&lt;/strong&gt;、&lt;strong&gt;重新执行&lt;/strong&gt;用户过去向Shell中输入的内容；&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;awk&lt;/code&gt;过滤并统计&lt;code&gt;history&lt;/code&gt;的结果：取history返回值的第二列（命令名称），存入名为&lt;em&gt;CMD&lt;/em&gt;的映射并累加重复的命令，&lt;em&gt;count&lt;/em&gt;负责统计总命令数，统计结束后按行输出&amp;lt;命令执行的次数、占总命令数的百分比、该命令的名称&amp;gt;；&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;grep -v&lt;/code&gt;去除那些带有&lt;code&gt;&apos;/&apos;&lt;/code&gt;的目录跳转（使用&lt;code&gt;grep -vE &quot;./| \.&quot;&lt;/code&gt;对po主有奇效，因为po主有时会使用类似&lt;code&gt;.ssh&lt;/code&gt;的不规范写法进入一个目录）；&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;column&lt;/code&gt;命令造一个漂亮的表格；&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;sort&lt;/code&gt;命令排序取逆；&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;nl&lt;/code&gt;命令给结果加上序号；&lt;/li&gt;
&lt;li&gt;再用&lt;code&gt;head&lt;/code&gt;命令取TOP10；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是就漂亮的统计出了我的历史记录使用结果（看到&lt;code&gt;man&lt;/code&gt;和&lt;code&gt;sudo&lt;/code&gt;的排名就知道应该是个noob）。&lt;/p&gt;
&lt;h2&gt;2. 有用的执行的命令的修改&lt;/h2&gt;
&lt;p&gt;po主因为noob，不熟悉系统，经常在输入命令的时候出现typo…这也是没办法的事，如果重新键入，或是把历史记录掉出来再移动光标至输入错误处修改，就太麻烦了，还好shell可以帮我：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 一删
➜  ~  finder ~ -iname &quot;using-cli-like-a-hacker.md&quot;
zsh: command not found: finder
➜  ~  ^er
➜  ~  find ~ -iname &quot;using-cli-like-a-hacker.md&quot;
/Users/zealot/blog/source/_posts/using-cli-like-a-hacker.md
# 二改
➜  ~  echo &quot;the ordre is incorrect&quot;
the ordre is incorrect
➜  ~  ^re^er
➜  ~  echo &quot;the order is incorrect&quot;
the order is incorrect
➜  ~  echo &quot;there is a typo&quot;
there is a typo
➜  ~  ^a^no
➜  ~  echo &quot;there is no typo&quot;
there is no typo
# 三全换
➜  ~  ln /opt/local/bin/hub /usr/bin/hub
ln: /opt/local/bin/hub: No such file or directory
➜  ~  ^hub^git^:G # zsh
# 或!:gs/hub/git
➜  ~  ln /opt/local/bin/git /usr/bin/git
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Toy神的口诀是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一删（&lt;code&gt;^er&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;二改（&lt;code&gt;^a^no&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;三全换（&lt;code&gt;^hub^git^:G&lt;/code&gt;或&lt;code&gt;!:gs/hub/git&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. 深入了解命令历史&lt;/h2&gt;
&lt;h3&gt;3.1 历史记录属性：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;# 历史记录的大小
➜  ~  echo $HISTSIZE
10000
# 历史记录保存的位置
➜  ~  echo $HISTFILE
/Users/zealot/.zsh_history
# bash
➜  ~  echo $HISTFILESIZE
10000
# zsh
➜  ~  echo $SAVEHIST
10000
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.2 查看历史记录：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  history | less
# 显示前五条
➜  ~  history 5
# 在历史记录中查找
history | grep string
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;按下[ctrl-R]，进入逆向搜索历史模式：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  history | less
bck-i-search: history |
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;按下[ctrl+P]访问前一条命令&lt;/li&gt;
&lt;li&gt;按下[ctrl+N]访问下一条命令&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 关于&lt;code&gt;!&lt;/code&gt;的历史引用&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!!&lt;/code&gt;执行上一条命令：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这个很常用，特别是在没有权限的时候配合&lt;code&gt;sudo&lt;/code&gt;使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  port -v selfupdate
...
failed: Permission denied
...
➜  ~  sudo !!
➜  ~  sudo port -v selfupdate
Password:
...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!foo&lt;/code&gt;执行以&lt;em&gt;foo&lt;/em&gt;开头的命令：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  !port
➜  ~  port outdated
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!?foo&lt;/code&gt;执行包含&lt;em&gt;foo&lt;/em&gt;的命令：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  !?out
➜  ~  port outdated
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!n&lt;/code&gt;执行历史记录中第&lt;em&gt;n&lt;/em&gt;个命令（可能会很早）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  !9
➜  ~  pwd
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!-n&lt;/code&gt;执行倒数第&lt;em&gt;n&lt;/em&gt;个命令（比较常用）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  !-9
➜  ~  echo $HISTSIZE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注：&lt;code&gt;!-1&lt;/code&gt; == &lt;code&gt;!!&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!#&lt;/code&gt;引用当前行&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo !#
➜  ~  echo echo
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3.4 历史命令的Word选取&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/using-cli-like-a-hacker-hist-word.png&quot; alt=&quot;历史命令Word选取图&quot; title=&quot;历史命令的参数选取&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!$&lt;/code&gt;得到上一条命令的最后一个参数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  mkdir videos
➜  ~  cd !$
➜  ~  cd videos
➜  videos
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以通过快捷键[alt+.]获得。&lt;/p&gt;
&lt;p&gt;注：OSX上需要设置在Terminal-&amp;gt;Terminal-&amp;gt;Preferences-&amp;gt;Settings-&amp;gt;Keyboard中设置“Use option as meta key”，在iTerm上通常设置“left option key as +Esc”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!^&lt;/code&gt;得到上一条命令的第一个参数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  ls /usr/share/doc /usr/share/man
/usr/share/doc:
...

/usr/share/man:
...
➜  ~  cd !^
➜  ~  cd /usr/share/doc
➜  doc
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也可以通过快捷键[Ctrl+Alt+Y]获得。（OSX上似乎不行…求指点）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!:n&lt;/code&gt;得到上一条命令第n个参数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  touch foo.txt bar.txt baz.txt
➜  ~  vim !:2
➜  ~  vim bar.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!:x-y&lt;/code&gt;得到上一条命令从第x到第y的参数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  touch foo.txt bar.txt baz.txt
➜  ~  rm !:1-3
➜  ~  rm foo.txt bar.txt baz.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!:n*&lt;/code&gt;得到上一条命令中从第n个参数开始到最后的参数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  cat /etc/resolv.conf /etc/hosts /etc/hostname
➜  ~  vim !:2*
➜  ~  vim /etc/hosts /etc/hostname
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!*&lt;/code&gt;得到上一条命令的所有参数：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  ls src dst
➜  ~  cp -r !*
➜  ~  cp -r src dst
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于Word选取，需要记住的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;：第n个参数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;^|$&lt;/code&gt;：第一个参数|最后一个参数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n*&lt;/code&gt;：从第n个参数开始取到最后的参数；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;x-y&lt;/code&gt;：从第x个参数开始取到第y个参数；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.5 历史命令的修饰符&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;:h&lt;/code&gt;选取路径的开头部分：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  ls /usr/share/doc/bash/bash.html
/usr/share/doc/bash/bash.html
➜  ~  ls !$:h
➜  ~  ls /usr/share/doc/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于使用&lt;code&gt;dirname&lt;/code&gt;命令。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;:t&lt;/code&gt;选取路径的结尾部分：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  wget http://nginx.org/download/nginx-1.7.2.tar.gz
...
➜  ~  tar zxvf !$:t
➜  ~  tar zxvf nginx-1.7.2.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相当于使用&lt;code&gt;basename&lt;/code&gt;命令。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;:r&lt;/code&gt;选取文件名：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  wget http://nginx.org/download/nginx-1.7.2.zip
...
➜  ~  unzip nginx-1.7.2.zip
...
➜  ~  cd !$:r
➜  ~  cd nginx-1.7.2
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;:e&lt;/code&gt;选取扩展名：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo filname.extension
filname.extension
➜  ~  echo !$:e
➜  ~  echo extension
#bash
.extension
# zsh
extension
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;!:p&lt;/code&gt;打印命令：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo *
...
➜  ~  !:p
➜  ~  echo *
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;:s&lt;/code&gt;做替换：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo this that
this that
➜  ~  !:s/is/e
➜  ~  echo the that
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;也就是前面提到的“二改”：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo this that
this that
➜  ~  ^is^e
➜  ~  echo the that
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;:g&lt;/code&gt;做全局操作：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo abcd abef
abcd abef
➜  ~  !:gs/ab/cd
➜  ~  echo cdcd cdef
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;也就是前面提到的“三全换”：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo abcd abef
abcd abef
➜  ~  ^ab^cd^:G
➜  ~  echo cdcd cdef
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;:u&lt;/code&gt;将命令改为大写（zsh）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo $histfile

➜  ~  echo !$:u
➜  ~  echo $HISTFILE
/Users/zealot/.zsh_history
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;:l&lt;/code&gt;将命令该为小写（zsh）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo $HISTORY

➜  ~  echo !$:l
➜  ~  echo $history
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于修饰符，要记住的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;h|t&lt;/code&gt;：选取路径的开头|结尾；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;r|e&lt;/code&gt;：选取文件名|扩展名；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;p&lt;/code&gt;：打印命令；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt;：替换操作；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;g&lt;/code&gt;：全局操作；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;u|l&lt;/code&gt;：将参数改为大写|小写；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;综上：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/using-cli-like-a-hacker-hist.png&quot; alt=&quot;历史命令结构展开图&quot; title=&quot;历史展开模式&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;4. 常备锦囊，内藏妙计&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用&lt;code&gt;alias -s&lt;/code&gt;定义后缀别名（zsh）：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  alias -s py=python3
➜  ~  test.py
python3 test.py
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用 {} 构造字符串：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  cp grade.txt{,.bak}
➜  ~  cp grade.txt grade.txt.bak
➜  ~
➜  ~  vim {a,b,c}file.c
➜  ~  vim afile.c bfile.c cfile.c
➜  ~
➜  ~  wget http://linuxtoy.org/img/{1..5}.jpg 
1.jpg  2.jpg  3.jpg  4.jpg  5.jpg
➜  ~
➜  ~  echo {01..5}.txt
01.txt 02.txt 03.txt 04.txt 05.txt
➜  ~
➜  ~  echo 0{1..5}.txt
01.txt 02.txt 03.txt 04.txt 05.txt
➜  ~
➜  ~  echo {1..10..2}.txt
1.txt 3.txt 5.txt 7.txt 9.txt
➜  ~
➜  ~  echo {1..10..-2}.txt
9.txt 7.txt 5.txt 3.txt 1.txt
➜  ~
➜  ~  echo {9..1..-2}.txt
1.txt 3.txt 5.txt 7.txt 9.txt
➜  ~
➜  ~  mkdir -p 2014/{01..12}/{baby,photo}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用``或&lt;code&gt;$()&lt;/code&gt;做命令替换：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  echo $(date +&apos;%Y%m%d&apos;)
20140707
➜  ~  echo `date +&apos;%Y%m%d&apos;`
20140707
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用变量保存信息：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  LOG=/var/log/emerge.log
➜  ~  head $LOG
➜  ~  grep -i compiling $LOG
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;使用 for ... in 重复执行命令：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;➜  ~  figlet -f font-type-1 Linux # (1)
➜  ~  figlet -f font-type-2 Linux # (2)
➜  ~  figlet -f font-type-3 Linux # (3)
➜  ~  ...
➜  ~
➜  ~  cd /usr/share/figlet
➜  ~  for font in *.flf
for&amp;gt; do
for&amp;gt;   echo $font
for&amp;gt;   figlet -f $font Linux
for&amp;gt; done
...
➜  ~  for font in *.flf; do echo $font; figlet -f $font Linux; done
➜  ~
➜  ~  for dev in /dev/sd{a..d}
for&amp;gt; do
for&amp;gt;   hdparm -t $dev
for&amp;gt; done
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;5. 其他&lt;/h2&gt;
&lt;h3&gt;5.1 重要原则&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Type less, accomplish more (少打多做)；&lt;/li&gt;
&lt;li&gt;DRY, don&apos;t repeat yourself (不要重复你自己)；&lt;/li&gt;
&lt;li&gt;Care about your tool (关心你的工具)；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.2. 此外…&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Tab&lt;/code&gt;补全；&lt;/li&gt;
&lt;li&gt;使用通配符 (&lt;code&gt;*&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;[a-z]&lt;/code&gt;, &lt;code&gt;[0-9]&lt;/code&gt;)；&lt;/li&gt;
&lt;li&gt;用快捷键编辑命令行；&lt;/li&gt;
&lt;li&gt;字符串处理；&lt;/li&gt;
&lt;li&gt;复合命令 (&lt;code&gt;;&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;||&lt;/code&gt;)；&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5.3 扩展阅读：&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;bash: man history&lt;/li&gt;
&lt;li&gt;zsh: man zshexpn&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.gnu.org/software/bash/manual/html_node/index.html&quot;&gt;Bash Reference Manual&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;http://zsh.sourceforge.net/Guide/zshguide.html&quot;&gt;A User&apos;s Guide to ZSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Book: Unix Power Tools&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ps: 关于&lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;等复杂命令，以后再详解。&lt;/p&gt;
&lt;p&gt;-EOF-&lt;/p&gt;
</content:encoded></item><item><title>下载网易公开课的Python脚本</title><link>https://zlotus.github.io/2014/07/05/using-python-download-163-opencourse/</link><guid isPermaLink="true">https://zlotus.github.io/2014/07/05/using-python-download-163-opencourse/</guid><pubDate>Sat, 05 Jul 2014 17:20:10 GMT</pubDate><content:encoded>&lt;p&gt;po主算法奇菜无比，看到163的公开课上有&lt;a href=&quot;http://v.163.com/special/opencourse/algorithms.html&quot;&gt;《算法导论》&lt;/a&gt;，就弄了个小脚本下载。&lt;/p&gt;
&lt;p&gt;结果发现，在OSX上，竟然比迅雷快！快好多！当然，这全怪po主当年中二，办了广电的网络…而且OSX下的迅雷运行起来有时发热会很大！&lt;/p&gt;
&lt;p&gt;Talk is cheap...&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用到了&lt;a href=&quot;http://docs.python-requests.org/en/latest/&quot;&gt;requests&lt;/a&gt;库。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;用到了&lt;code&gt;wget&lt;/code&gt;下载工具。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;import requests
import re
import os
from subprocess import call


def urls_iter(origin_url=&apos;http://v.163.com/special/opencourse/algorithms.html&apos;, begin_from=1):
    &quot;&quot;&quot;
    *origin_url* is the origin download page of 163 OpenCourse, the default value is &apos;algorithms course from MIT&apos;.
    *begin_from* can make the function jump to the No.*begin_from* item to start download.
    &quot;&quot;&quot;
    download_request = requests.get(origin_url)

    course_list_pattern = re.compile(r&apos;&amp;lt;table class=&quot;m-clist&quot; id=&quot;list2&quot;.*?&amp;gt;(.*?)&amp;lt;/table&amp;gt;&apos;, flags=re.DOTALL)
    course_pattern = re.compile(r&apos;&amp;lt;tr class=&quot;(?:u-odd|u-even)&quot;&amp;gt;.*?&amp;lt;/tr&amp;gt;&apos;, flags=re.DOTALL)
    course_text = course_list_pattern.search(download_request.text).group(1)
    course_list = course_pattern.findall(course_text)

    course_name_pattern = re.compile(r&apos;&amp;lt;a href=.*?&amp;gt;(.*?)&amp;lt;/a&amp;gt;&apos;, flags=re.DOTALL)
    course_video_pattern = re.compile(r&apos;&amp;lt;a class=&quot;downbtn&quot; href=[\&apos;&quot;&quot;](.*?)[\&apos;&quot;&quot;].*?&amp;gt;.*?&amp;lt;/a&amp;gt;&apos;, flags=re.DOTALL)

    for index, course in enumerate(course_list, 1):
        if index &amp;lt; begin_from:
            continue

        index = &apos;{:0&amp;gt;2}_&apos;.format(index)
        video_title = index + course_name_pattern.search(course).group(1) + &apos;.mp4&apos;
        video_address = course_video_pattern.search(course).group(1)
        yield (video_title, video_address)


def download_course(download_list, download_dir=&apos;/Users/zealot/Downloads/algorithms&apos;):
    &quot;&quot;&quot;
    *download_list* is a collection contains a list of tuple whose 1st element is the filename of the video,
        and 2nd element is the download url of the video.
    *download_dir* defines the directory where the files should be stored in.

    function using common download tool `wget` to fetch videos one at a time.
    raise error if `wget` not found.
    &quot;&quot;&quot;
    with open(&apos;/dev/null&apos;) as black_hole:
        if call([&apos;which&apos;, &apos;wget&apos;], stdout=black_hole):
            raise OSError(&apos;command not found: wget&apos;)

    if not os.path.exists(download_dir):
        os.makedirs(download_dir)

    for video_title, video_address in download_list:
        call([&apos;wget&apos;, &apos;-c&apos;, video_address, &apos;-O&apos;, os.path.join(download_dir, video_title)])

if __name__ == &apos;__main__&apos;:
    download_course(urls_iter())    
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;找链接全部用的正则；&lt;/li&gt;
&lt;li&gt;函数默认行为是帮po主从&lt;code&gt;http://v.163.com/special/opencourse/algorithms.html&lt;/code&gt;下载《算法导论》视频，并默认帮po主存到&lt;code&gt;/Users/zealot/Downloads/algorithms&lt;/code&gt;目录里；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wget -c&lt;/code&gt;支持断点续传，对于广电渣网络来说简直是福音；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当然，如果你想要下载&lt;a href=&quot;http://v.163.com/special/opencourse/fouriertransforms.html&quot;&gt;《傅里叶变换及其应用》&lt;/a&gt;可以把最后的调用改为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if __name__ == &apos;__main__&apos;:
    download_course(urls_iter(&apos;http://v.163.com/special/opencourse/fouriertransforms.html&apos;),
                    &apos;/Users/zealot/downloads/fouriertransforms&apos;)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果不错~&lt;/p&gt;
&lt;p&gt;-EOF-&lt;/p&gt;
</content:encoded></item></channel></rss>