Caffeinated 6.828：实验 1：PC 的引导过程

Summary: 第一部分主要是为了熟悉使用 x86 汇编语言、QEMU x86 仿真器、以及 PC 的加电引导过程。第二部分查看我们的 6.828 内核的引导加载器，它位于 lab 树的 boot 目录中。第三部分深入到我们的名为 JOS 的 6.828 内核模型内部，它在 kernel 目录中。

# Caffeinated 6.828：实验 1：PC 的引导过程

<img src="content_image/album_201806_12_180844cgvvx3gjo5vn5gvg.jpg" alt="" title="">

### 简介

这个实验分为三个部分。第一部分主要是为了熟悉使用 x86 汇编语言、QEMU x86 仿真器、以及 PC 的加电引导过程。第二部分查看我们的 6.828 内核的引导加载器，它位于 `lab` 树的 `boot` 目录中。第三部分深入到我们的名为 JOS 的 6.828 内核模型内部，它在 `kernel` 目录中。

#### 软件安装

本课程中你需要的文件和接下来的实验任务所需要的文件都是通过使用 [Git](http://www.git-scm.com/) 版本控制系统来分发的。学习更多关于 Git 的知识，请查看 [Git 用户手册](http://www.kernel.org/pub/software/scm/git/docs/user-manual.html)，或者，如果你熟悉其它的版本控制系统，这个 [面向 CS 的 Git 概述](http://eagain.net/articles/git-for-computer-scientists/) 可能对你有帮助。

本课程在 Git 仓库中的地址是 <https://exokernel.scripts.mit.edu/joslab.git> 。在你的 Athena 帐户中安装文件，你需要运行如下的命令去克隆课程仓库。你也可以使用 `ssh -X athena.dialup.mit.edu` 去登入到一个公共的 Athena 主机。

```
athena% mkdir ~/6.828
athena% cd ~/6.828
athena% add git
athena% git clone https://exokernel.scripts.mit.edu/joslab.git lab
Cloning into lab...
athena% cd lab
athena%

```

Git 可以帮你跟踪代码中的变化。比如，如果你完成了一个练习，想在你的进度中打一个检查点，你可以运行如下的命令去提交你的变更：

```
athena% git commit -am 'my solution for lab1 exercise 9'
Created commit 60d2135: my solution for lab1 exercise 9
 1 files changed, 1 insertions(+), 0 deletions(-)
athena%

```

你可以使用 `git diff` 命令跟踪你的变更。运行 `git diff` 将显示你的代码自最后一次提交之后的变更，而 `git diff origin/lab1` 将显示这个实验相对于初始代码的变更。在这里，`origin/lab1` 是为了完成这个作业，从我们的服务器上下载的初始代码在 Git 分支上的名字。

在 Athena 上，我们为你配置了合适的编译器和模拟器。如果你要去使用它们，请运行 `add exokernel` 命令。 每次登入 Athena 主机你都必须要运行这个命令（或者你可以将它添加到你的 `~/.environment` 文件中）。如果你在编译或者运行 `qemu` 时出现晦涩难懂的错误，可以双击 "check" 将它添加到你的课程收藏夹中。

如果你使用的是非 Athena 机器，你需要安装 `qemu` 和 `gcc`，它们在 [工具页面](https://sipb.mit.edu/iap/6.828/tools) 目录中。为了以后的实验需要，我们做了一些 `qemu` 调试方面的变更和补丁，因此，你必须构建你自己的工具。如果你的机器使用原生的 ELF 工具链（比如，Linux 和大多数 BSD，但不包括 OS X），你可以简单地从你的包管理器中安装 `gcc`。除此之外，都应该按工具页面的指导去做。

#### 动手过程

我们为了你便于做实验，为你使用了不同的 Git 仓库。做实验用的仓库位于一个 SSH 服务器后面。你可以拥有你自己的实验仓库，其他的任何同学都不可访问你的这个仓库。为了通过 SSH 服务器的认证，你必须有一对 RSA 密钥，并让服务器知道你的公钥。

实验代码同时还带有一个脚本，它可以帮你设置如何访问你的实验仓库。在运行这个脚本之前，你必须在我们的 [submission web 界面](https://exokernel.scripts.mit.edu/submit/) 上有一个帐户。在登陆页面上，输入你的 Athena 用户名，然后点击 “Mail me my password”。在你的邮箱中将马上接收到一封包含有你的 `6.828` 课程密码的邮件。注意，每次你点击这个按钮的时候，系统将随机给你分配一个新密码。

现在，你已经有了你的 `6.828` 密码，在 `lab` 目录下，运行如下的命令去配置实践仓库：

```
athena% make handin-prep
Using public key from ~/.ssh/id_rsa:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0lnnkoHSi4JDFA ...
Continue? [Y/n] Y

Login to 6.828 submission website.
If you do not have an account yet, sign up at https://exokernel.scripts.mit.edu/submit/
before continuing.
Username: <your Athena username>
Password: <your 6.828 password>
Your public key has been successfully updated.
Setting up hand-in Git repository...
Adding remote repository ssh://josgit@exokernel.mit.edu/joslab.git as 'handin'.
Done! Use 'make handin' to submit your lab code.
athena%

```

如果你没有 RSA 密钥对，这个脚本可能会询问你是否生成一个新的密钥对：

```
athena% make handin-prep
SSH key file ~/.ssh/id_rsa does not exists, generate one? [Y/n] Y
Generating public/private rsa key pair.
Your identification has been saved in ~/.ssh/id_rsa.
Your public key has been saved in ~/.ssh/id_rsa.pub.
The key fingerprint is:
xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx
The keyʼs randomart image is:
+--[ RSA 2048]----+
| ........ |
| ........ |
+-----------------+
Using public key from ~/.ssh/id_rsa:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD0lnnkoHSi4JDFA ...
Continue? [Y/n] Y
.....
athena%

```

当你开始动手做实验时，在 `lab` 目录下，输入 `make handin` 去使用 git 做第一次提交。后面将运行 `git push handin HEAD`，它将推送当前分支到远程 `handin` 仓库的同名分支上。

```
athena% git commit -am "ready to submit my lab"
[lab1 c2e3c8b] ready to submit my lab
 2 files changed, 18 insertions(+), 2 deletions(-)

athena% make handin
Handin to remote repository using 'git push handin HEAD' ...
Counting objects: 59, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (55/55), done.
Writing objects: 100% (59/59), 49.75 KiB, done.
Total 59 (delta 3), reused 0 (delta 0)
To ssh://josgit@am.csail.mit.edu/joslab.git
 * [new branch] HEAD -> lab1
athena%

```

如果在你的实验仓库上产生变化，你将收到一封电子邮件，让你去确认这个提交。以后，你可能会多次去运行 `run make handin`（或者 `git push handin`）。对于一个指定实验的最后提交时间是由相应分支的最新推送（最后一个推送）的时间决定的。

在这个案例中，`make handin` 运行可能并不正确，你可以使用 Git 命令去尝试修复这个问题。或者，你可以去运行 `make tarball`。它将为你生成一个 tar 文件，这个文件可以通过我们的 [web 界面](https://exokernel.scripts.mit.edu/submit/) 来上传。`make handin` 提供了很多特殊说明。

对于实验 1，你不需要去回答下列的任何一个问题。（尽管你不用自己回答，但是它们对下面的实验有帮助）

我们将使用一个评级程序来分级你的解决方案。你可以使用这个评级程序去测试你的解决方案的分级情况。

### 第一部分：PC 引导

第一个练习的目的是向你介绍 x86 汇编语言和 PC 引导过程，你可以使用 QEMU 和 QEMU/GDB 调试开始你的练习。这部分的实验你不需要写任何代码，但是，通过这个实验，你将对 PC 引导过程有了你自己的理解，并且为回答后面的问题做好准备。

#### 从使用 x86 汇编语言开始

如果你对 x86 汇编语言的使用还不熟悉，通过这个课程，你将很快熟悉它！如果你想学习它，[PC 汇编语言](https://sipb.mit.edu/iap/6.828/readings/pcasm-book.pdf) 这本书是一个很好的开端。希望这本书中有你所需要的一切内容。

警告：很不幸，这本书中的示例是为 NASM 汇编语言写的，而我们使用的是 GNU 汇编语言。NASM 使用所谓的 Intel 语法，而 GNU 使用 AT&T 语法。虽然在语义上是等价的，但是根据你使用的语法不同，至少从表面上看，汇编文件的差别还是挺大的。幸运的是，这两种语法的转换很简单，在 [Brennan's Guide to Inline Assembly](http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html) 有详细的介绍。

>
> **练习 1**
>
>
> 熟悉在 [6.828 参考页面](https://sipb.mit.edu/iap/6.828/reference) 上列出的你想去使用的可用汇编语言。你不需要现在就去阅读它们，但是在你阅读和写 x86 汇编程序的时候，你可以去参考相关的内容。
>
>
>

我并不推荐你阅读 [Brennan's Guide to Inline Assembly](http://www.delorie.com/djgpp/doc/brennan/brennan_att_inline_djgpp.html) 上的 “语法” 章节。虽然它对 AT&T 汇编语法描述的很好（并且非常详细），而且我们在 JOS 中使用的 GNU 汇编就是它。

对于 x86 汇编语言程序最终还是需要参考 Intel 的指令集架构，你可以在 [6.828 参考页面](https://sipb.mit.edu/iap/6.828/reference) 上找到它，它有两个版本：一个是 HTML 版的，是老的 [80386 程序员参考手册](https://sipb.mit.edu/iap/6.828/readings/i386/toc.htm)，它比起最新的手册更简短，更易于查找，但是，它包含了我们的 6.828 上所使用的 x86 处理器的所有特性；而更全面的、更新的、更好的是，来自 Intel 的 [IA-32 Intel 架构软件开发者手册](http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html)，它涵盖了我们在课程中所需要的、（并且可能有些是你不感兴趣的）大多数处理器的全部特性。另一个差不多的（并且经常是很友好的）一套手册是 [来自 AMD](http://developer.amd.com/documentation/guides/Pages/default.aspx#manuals) 的。当你为了一个特定的处理器特性或者指令，去查找最终的解释时，保存的最新的 Intel/AMD 架构手册或者它们的参考就很有用了。

#### 仿真 x86

与在一台真实的、物理的、个人电脑上引导一个操作系统不同，我们使用程序去如实地仿真一台完整的 PC：你在仿真器中写的代码，也能够引导一台真实的 PC。使用仿真器可以简化调试工作；比如，你可以在仿真器中设置断点，而这在真实的机器中是做不到的。

在 6.828 中，我们将使用 [QEMU 仿真器](http://www.qemu.org/)，它是一个现代化的并且速度非常快的仿真器。虽然 QEMU 内置的监视功能提供了有限的调试支持，但是，QEMU 也可以做为 [GNU 调试器](http://www.gnu.org/software/gdb/) (GDB) 的远程调试目标，我们在这个实验中将使用它来一步一步完成引导过程。

在开始之前，按照前面 “软件安装“ 中在 Athena 主机上描述的步骤，提取实验 1 的文件到你自己的目录中，然后，在 `lab` 目录中输入 `make`（如果是 BSD 的系统，是输入 `gmake` ）来构建最小的 6.828 引导加载器和用于启动的内核。（把在这里我们运行的这些代码称为 ”内核“ 有点夸大，但是，通过这个学期的课程，我们将把这些代码充实起来，成为真正的 ”内核“）

```
athena% cd lab
athena% make
+ as kern/entry.S
+ cc kern/init.c
+ cc kern/console.c
+ cc kern/monitor.c
+ cc kern/printf.c
+ cc lib/printfmt.c
+ cc lib/readline.c
+ cc lib/string.c
+ ld obj/kern/kernel
+ as boot/boot.S
+ cc -Os boot/main.c
+ ld boot/boot
boot block is 414 bytes (max 510)
+ mk obj/kern/kernel.img

```

（如果你看到有类似 ”undefined reference to `\_\_udivdi3'” 这样的错误，可能是因为你的电脑上没有 32 位的 “gcc multilib”。如果你运行在 Debian 或者 Ubuntu，你可以尝试去安装 “gcc-multilib” 包。）

现在，你可以去运行 QEMU 了，并将上面创建的 `obj/kern/kernel.img` 文件提供给它，以作为仿真 PC 的 “虚拟硬盘”，这个虚拟硬盘中包含了我们的引导加载器（`obj/boot/boot`) 和我们的内核（`obj/kernel`）。

```
athena% make qemu

```

运行 QEMU 时需要使用选项去设置硬盘，以及指示串行端口输出到终端。在 QEMU 窗口中将出现一些文本内容：

```
Booting from Hard Disk...
6828 decimal is XXX octal!
entering test_backtrace 5
entering test_backtrace 4
entering test_backtrace 3
entering test_backtrace 2
entering test_backtrace 1
entering test_backtrace 0
leaving test_backtrace 0
leaving test_backtrace 1
leaving test_backtrace 2
leaving test_backtrace 3
leaving test_backtrace 4
leaving test_backtrace 5
Welcome to the JOS kernel monitor!
Type 'help' for a list of commands.
K>

```

在 `Booting from Hard Disk...` 之后的内容，就是由我们的基本 JOS 内核输出的：`K>` 是包含在我们的内核中的小型监听器或者交互式控制程序的提示符。内核输出的这些行也会出现在你运行 QEMU 的普通 shell 窗口中。这是因为测试和实验分级的原因，我们配置了 JOS 的内核，使它将控制台输出不仅写入到虚拟的 VGA 显示器（就是 QEMU 窗口），也写入到仿真 PC 的虚拟串口上，QEMU 会将虚拟串口上的信息转发到它的标准输出上。同样，JOS 内核也将接收来自键盘和串口的输入，因此，你既可以从 VGA 显示窗口中输入命令，也可以从运行 QEMU 的终端窗口中输入命令。或者，你可以通过运行 `make qemu-nox` 来取消虚拟 VGA 的输出，只使用串行控制台来输出。如果你是通过 SSH 拨号连接到 Athena 主机，这样可能更方便。

在这里有两个可以用来监视内核的命令，它们是 `help` 和 `kerninfo`。

```
K> help
help - display this list of commands
kerninfo - display information about the kernel
K> kerninfo
Special kernel symbols:
 entry f010000c (virt) 0010000c (phys)
 etext f0101a75 (virt) 00101a75 (phys)
 edata f0112300 (virt) 00112300 (phys)
 end f0112960 (virt) 00112960 (phys)
Kernel executable memory footprint: 75KB
K>

```

`help` 命令的用途很明确，我们将简短地讨论一下 `kerninfo` 命令输出的内容。虽然它很简单，但是，需要重点注意的是，这个内核监视器是 “直接” 运行在仿真 PC 的 “原始（虚拟）硬件” 上的。这意味着你可以去拷贝 `obj/kern/kernel.img` 的内容到一个真实硬盘的前几个扇区，然后将那个硬盘插入到一个真实的 PC 中，打开这个 PC 的电源，你将在一台真实的 PC 屏幕上看到和上面在 QEMU 窗口完全一样的内容。（我们并不推荐你在一台真实机器上这样做，因为拷贝 `kernel.img` 到硬盘的前几个扇区将覆盖掉那个硬盘上原来的主引导记录，这将导致这个硬盘上以前的内容丢失！）

#### PC 的物理地址空间

我们现在将更深入去了解 “关于 PC 是如何启动” 的更多细节。一台 PC 的物理地址空间是硬编码为如下的布局：

```
+------------------+  <- 0xFFFFFFFF (4GB)
|      32-bit      |
|  memory mapped   |
|     devices      |
|                  |
/\/\/\/\/\/\/\/\/\/\

/\/\/\/\/\/\/\/\/\/\
|                  |
|      Unused      |
|                  |
+------------------+  <- depends on amount of RAM
|                  |
|                  |
| Extended Memory  |
|                  |
|                  |
+------------------+  <- 0x00100000 (1MB)
|     BIOS ROM     |
+------------------+  <- 0x000F0000 (960KB)
|  16-bit devices, |
|  expansion ROMs  |
+------------------+  <- 0x000C0000 (768KB)
|   VGA Display    |
+------------------+  <- 0x000A0000 (640KB)
|                  |
|    Low Memory    |
|                  |
+------------------+  <- 0x00000000
```

首先，这台 PC 是基于 16 位的 Intel 8088 处理器，它仅能处理 1 MB 的物理地址。所以，早期 PC 的物理地址空间开始于 `0x00000000`，结束于 `0x000FFFFF` 而不是 `0xFFFFFFFF`。被标记为 “低位内存” 的区域是早期 PC 唯一可以使用的随机访问内存（RAM）；事实上，更早期的 PC 仅可以配置 16KB、32KB、或者 64KB 的内存！

从 `0x000A0000` 到 `0x000FFFFF` 的 384 KB 的区域是为特定硬件保留的区域，比如，视频显示缓冲和保存在非易失存储中的固件。这个保留区域中最重要的部分是基本输入/输出系统（BIOS），它位于从 `0x000F0000` 到 `0x000FFFFF` 之间的 64KB 大小的区域。在早期的 PC 中，BIOS 在真正的只读存储（ROM）中，但是，现在的 PC 的 BIOS 都保存在可更新的 FLASH 存储中。BIOS 负责执行基本系统初始化工作，比如，激活视频卡和检查已安装的内存数量。这个初始化工作完成之后，BIOS 从相关位置加载操作系统，比如从软盘、硬盘、CD-ROM、或者网络，然后将机器的控制权传递给操作系统。

当 Intel 最终在 80286 和 80386 处理器上 “打破了 1MB 限制” 之后，这两个处理器各自支持 16MB 和 4GB 物理地址空间，尽管如此，为了确保向下兼容现存软件，PC 架构还是保留着 1 MB 以内物理地址空间的原始布局。因此，现代 PC 的物理内存，在 `0x000A0000` 和 `0x00100000` 之间有一个 “黑洞区域”，将内存分割为 “低位” 或者 “传统内存” 区域（前 640 KB）和 “扩展内存”（其它的部分）。除此之外，在 PC 的 32 位物理地址空间顶部之上的一些空间，在全部的物理内存上面，现在一般都由 BIOS 保留给 32 位的 PCI 设备使用。

最新的 x86 处理器可以支持超过 4GB 的物理地址空间，因此，RAM 可以进一步扩展到 `0xFFFFFFFF` 之上。在这种情况下，BIOS 必须在 32 位可寻址空间顶部之上的系统 RAM 上，设置第二个 “黑洞区域”，以便于为这些 32 位的设备映射留下空间。因为 JOS 设计的限制，它仅可以使用 PC 物理内存的前 256 MB，因此，我们将假设所有的 PC “仅仅” 拥有 32 位物理地址空间。但是处理复杂的物理地址空间和其它部分的硬件系统，将涉及到许多年前操作系统开发所遇到的实际挑战之一。

#### ROM BIOS

在实验的这一部分中，你将使用 QEMU 的调试功能去研究 IA-32 相关的计算机是如何引导的。

打开两个终端窗口，在其中一个中，输入 `make qemu-gdb`（或者 `make qemu-nox-gdb`），这将启动 QEMU，但是处理器在运行第一个指令之前将停止 QEMU，以等待来自 GDB 的调试连接。在第二个终端窗口中，从相同的目录中运行 `make`，以及运行 `make gdb`。你将看到如下的输出。

```
athena% make gdb
GNU gdb (GDB) 6.8-debian
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
+ target remote localhost:1234
The target architecture is assumed to be i8086
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb)

```

`make gdb` 的运行目标是一个称为 `.gdbrc` 的脚本，它设置了 GDB 在早期引导期间调试所用到的 16 位代码，并且将它指向到正在监听的 QEMU 上。

下列行：

```
[f000:fff0] 0xffff0: ljmp $0xf000,$0xe05b

```

是 GDB 运行的第一个指令的反汇编。这个输出包含如下的信息：

* IBM PC 从物理地址 `0x000ffff0` 开始运行，这个地址位于为 ROM BIOS 保留的 64 KB 区域的顶部。
* PC 使用 `CS = 0xf000` 和 `IP = 0xfff0` 开始运行。
* 运行的第一个指令是一个 `jmp` 指令，它跳转段地址 `CS = 0xf000` 和 `IP = 0xe05b`。

为什么 QEMU 是这样开始的呢？这是因为 Intel 设计的 8088 处理器是这样做的，这个处理器是 IBM 最早用在他们的 PC 上的处理器。因为在一台 PC 中，BIOS 是硬编码在物理地址范围 `0x000f0000-0x000fffff` 中的，这样的设计确保了在机器接通电源或者任何系统重启之后，BIOS 总是能够首先控制机器 —— 这是至关重要的，因为机器接通电源之后，在机器的内存中没有处理器可以运行的任何软件。QEMU 仿真器有它自己的 BIOS，它的位置在处理器的模拟地址空间中。在处理器复位之后，（模拟的）处理器进入了实模式，然后设置 `CS` 为 `0xf000` 、`IP` 为 `0xfff0`，所以，运行开始于那个（`CS:IP`）段地址。那么，段地址 `0xf000:fff0` 是如何转到物理地址的呢？

在回答这个问题之前，我们需要了解有关实模式地址的知识。在实模式（PC 启动之后就处于实模式）中，物理地址是根据这个公式去转换的：物理地址 = 16 \* 段地址 + 偏移。因此，当 PC 设置 `CS` 为 `0xf000` 、`IP` 为 `0xfff0` 之后，物理地址指向到：

```
16 * 0xf000 + 0xfff0 # in hex multiplication by 16 is
 = 0xf0000 + 0xfff0 # easy--just append a 0.
 = 0xffff0

```

`0xffff0` 是 BIOS (`0x100000`) 结束之前的 16 字节。因此，BIOS 所做的第一件事情是向后 `jmp` 到 BIOS 中的早期位置就一点也不奇怪了；毕竟只有 16 字节，还能指望它做些什么呢？

>
> **练习 2**
>
>
> 使用 GDB 的 `si`（步进指令）指令去跟踪进入到 ROM BIOS 的更多指令，然后尝试猜测它可能会做什么。你可能需要去查看 [Phil Storrs I/O 端口描述](http://web.archive.org/web/20040404164813/members.iweb.net.au/%7Epstorr/pcbook/book2/book2.htm)，以及在 [6.828 参考资料页面](https://sipb.mit.edu/iap/6.828/reference) 上的其它资料。不需要了解所有的细节 —— 只要搞明白 BIOS 首先要做什么就可以了。
>
>
>

当 BIOS 运行后，它将设置一个中断描述符表和初始化各种设备，比如， VGA 显示。在这时，你在 QEMU 窗口中将出现 `Starting SeaBIOS` 的信息。

在初始化 PCI 产品线和 BIOS 知道的所有重要设备之后，它将搜索可引导设备，比如，一个软盘、硬盘、或者 CD-ROM。最后，当它找到可引导磁盘之后，BIOS 从可引导硬盘上读取引导加载器，然后将控制权交给它。

### 第二部分：引导加载器

在 PC 的软盘和硬盘中，将它们分割成 512 字节大小的区域，每个区域称为一个扇区。一个扇区就是磁盘的最小转存单元：每个读或写操作都必须是一个或多个扇区大小，并且按扇区边界进行对齐。如果磁盘是可引导盘，第一个扇区则为引导扇区，因为，第一个扇区中驻留有引导加载器的代码。当 BIOS 找到一个可引导软盘或者硬盘时，它将 512 字节的引导扇区加载进物理地址为 `0x7c00` 到 `0x7dff` 的内存中，然后使用一个 `jmp` 指令设置 `CS:IP` 为 `0000:7c00`，并传递控制权到引导加载器。与 BIOS 加载地址一样，这些地址是任意的 —— 但是它们对于 PC 来说是固定的，并且是标准化的。

后来，随着 PC 的技术进步，它们可以从 CD-ROM 中引导，因此，PC 架构师趁机对引导过程进行轻微的调整。最后的结果使现代的 BIOS 从 CD-ROM 中引导的过程更复杂（并且功能更强大）。CD-ROM 使用 2048 字节大小的扇区，而不是 512 字节的扇区，并且，BIOS 在传递控制权之前，可以从磁盘上加载更大的（不止是一个扇区）引导镜像到内存中。更多内容，请查看 [“El Torito” 可引导 CD-ROM 格式规范](https://sipb.mit.edu/iap/6.828/readings/boot-cdrom.pdf)。

不过对于 6.828，我们将使用传统的硬盘引导机制，意味着我们的引导加载器必须小于 512 字节。引导加载器是由一个汇编源文件 `boot/boot.S` 和一个 C 源文件 `boot/main.c` 构成，仔细研究这些源文件可以让你彻底理解引导加载器都做了些什么。引导加载器必须要做两件主要的事情：

1. 第一、引导加载器将处理器从实模式切换到 32 位保护模式，因为只有在 32 位保护模式中，软件才能够访问处理器中 1 MB 以上的物理地址空间。关于保护模式将在 [PC 汇编语言](https://sipb.mit.edu/iap/6.828/readings/pcasm-book.pdf) 的 1.2.7 和 1.2.8 节中详细描述，更详细的内容请参阅 Intel 架构手册。在这里，你只要理解在保护模式中段地址（段基地址：偏移量）与物理地址转换的差别就可以了，并且转换后的偏移是 32 位而不是 16 位。
2. 第二、引导加载器通过 x86 的专用 I/O 指令直接访问 IDE 磁盘设备寄存器，从硬盘中读取内核。如果你想去更好地了解在这里说的专用 I/O 指令，请查看 [6.828 参考页面](https://sipb.mit.edu/iap/6.828/reference) 上的 “IDE 硬盘控制器” 章节。你不用学习太多的专用设备编程方面的内容：在实践中，写设备驱动程序是操作系统开发中的非常重要的部分，但是，从概念或者架构的角度看，它也是最让人乏味的部分。

理解了引导加载器源代码之后，我们来看一下 `obj/boot/boot.asm` 文件。这个文件是在引导加载器编译过程中，由我们的 GNUmakefile 创建的引导加载器的反汇编文件。这个反汇编文件让我们可以更容易地看到引导加载器代码所处的物理内存位置，并且也可以更容易地跟踪在 GDB 中步进的引导加载器发生了什么事情。同样的，`obj/kern/kernel.asm` 文件中包含了 JOS 内核的一个反汇编，它也经常被用于内核调试。

你可以使用 `b` 命令在 GDB 中设置中断点地址。比如，`b *0x7c00` 命令在地址 `0x7C00` 处设置了一个断点。当处于一个断点中时，你可以使用 `c` 和 `si` 命令去继续运行：`c` 命令让 QEMU 继续运行，直到下一个断点为止（或者是你在 GDB 中按下了 Ctrl - C），而 `si N` 命令是每次步进 `N` 个指令。

要检查内存中的指令（除了要立即运行的下一个指令之外，因为它是由 GDB 自动输出的），你可以使用 `x/i` 命令。这个命令的语法是 `x/Ni ADDR`，其中 `N` 是连接的指令个数，`ADDR` 是开始反汇编的内存地址。

>
> **练习 3**
>
>
> 查看 [实验工具指南](https://sipb.mit.edu/iap/6.828/labguide)，特别是 GDB 命令的相关章节。即便你熟悉使用 GDB 也要好好看一看，GDB 的一些命令比较难理解，但是它对操作系统的工作很有帮助。
>
>
>

在地址 0x7c00 处设置断点，它是加载后的引导扇区的位置。继续运行，直到那个断点。在 `boot/boot.S` 中跟踪代码，使用源代码和反汇编文件 `obj/boot/boot.asm` 去保持跟踪。你也可以使用 GDB 中的 `x/i` 命令去反汇编引导加载器接下来的指令，比较引导加载器源代码与在 `obj/boot/boot.asm` 和 GDB 中的反汇编文件。

在 `boot/main.c` 文件中跟踪进入 `bootmain()` ，然后进入 `readsect()`。识别 `readsect()` 中相关的每一个语句的准确汇编指令。跟踪 `readsect()` 中剩余的指令，然后返回到 `bootmain()` 中，识别 `for` 循环的开始和结束位置，这个循环从磁盘上读取内核的剩余扇区。找出循环结束后运行了什么代码，在这里设置一个断点，然后继续。接下来再走完引导加载器的剩余工作。

完成之后，就能够回答下列的问题了：

* 处理器开始运行 32 代码时指向到什么地方？从 16 位模式切换到 32 位模式的真实原因是什么？
* 引导加载器执行的最后一个指令是什么，内核加载之后的第一个指令是什么？
* 内核的第一个指令在哪里？
* 为从硬盘上获取完整的内核，引导加载器如何决定有多少扇区必须被读入？在哪里能找到这些信息？

#### 加载内核

我们现在来进一步查看引导加载器在 `boot/main.c` 中的 C 语言部分的详细细节。在继续之前，我们先停下来回顾一下 C 语言编程的基础知识。

>
> **练习 4**
>
>
> 下载 [pointers.c](https://sipb.mit.edu/iap/6.828/files/pointers.c) 的源代码，运行它，然后确保你理解了输出值的来源的所有内容。尤其是，确保你理解了第 1 行和第 6 行的指针地址的来源、第 2 行到第 4 行的值是如何得到的、以及为什么第 5 行指向的值表面上看像是错误的。
>
>
> 如果你对指针的使用不熟悉，Brian Kernighan 和 Dennis Ritchie（就是大家知道的 “K&R”）写的《C Programming Language》是一个非常好的参考书。同学们可以去买这本书（这里是 [Amazon 购买链接](http://www.amazon.com/C-Programming-Language-2nd/dp/0131103628/sr=8-1/qid=1157812738/ref=pd_bbs_1/104-1502762-1803102?ie=UTF8&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;s=books)），或者在 [MIT 的图书馆的 7 个副本](http://library.mit.edu/F/AI9Y4SJ2L5ELEE2TAQUAAR44XV5RTTQHE47P9MKP5GQDLR9A8X-10422?func=item-global&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;doc_library=MIT01&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;doc_number=000355242&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;year=&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;volume=&amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;sub_library=) 中找到其中一个。在 [SIPB Office](http://sipb.mit.edu/) 也有三个副本可以细读。
>
>
> 在课程阅读中，[Ted Jensen 写的教程](https://sipb.mit.edu/iap/6.828/readings/pointers.pdf) 可以使用，它大量引用了 K&R 的内容。
>
>
> 警告：除非你特别精通 C 语言，否则不要跳过这个阅读练习。如果你没有真正理解了 C 语言中的指针，在接下来的实验中你将非常痛苦，最终你将很难理解它们。相信我们；你将不会遇到什么是 ”最困难的方式“。
>
>
>

要了解 `boot/main.c`，你需要了解一个 ELF 二进制格式的内容。当你编译和链接一个 C 程序时，比如，JOS 内核，编译器将每个 C 源文件（'`.c`'）转换为一个包含预期硬件平台的汇编指令编码的二进制格式的对象文件（'`.o`'），然后链接器将所有编译过的对象文件组合成一个单个的二进制镜像，比如，`obj/kern/kernel`，在本案例中，它就是 ELF 格式的二进制文件，它表示是一个 ”可运行和可链接格式“。

关于这个格式的全部信息可以在 [我们的参考页面](https://sipb.mit.edu/iap/6.828/reference) 上的 [ELF 规范](https://sipb.mit.edu/iap/6.828/readings/elf.pdf) 中找到，但是，你并不需要深入地研究这个格式 的细节。虽然完整的格式是非常强大和复杂的，但是，大多数复杂的部分是为了支持共享库的动态加载，在我们的课程中，并不需要做这些。

鉴于 6.828 的目的，你可以认为一个 ELF 可运行文件是一个用于加载信息的头文件，接下来的几个程序节，根据加载到内存中的特定地址的不同，每个都是连续的代码块或数据块。引导加载器并不修改代码或者数据；它加载它们到内存，然后开始运行它。

一个 ELF 二进制文件使用一个固定长度的 ELF 头开始，紧接着是一个可变长度的程序头，列出了每个加载的程序节。C 语言在 `inc/elf.h` 中定义了这些 ELF 头。在程序节中我们感兴趣的部分有：

* `.text`：程序的可运行指令。
* `.rodata`：只读数据，比如，由 C 编译器生成的 ASCII 字符串常量。（然而我们并不需要操心设置硬件去禁止写入它）
* `.data`：保持在程序的初始化数据中的数据节，比如，初始化声明所需要的全局变量，比如，像 `int x = 5;`。

当链接器计算程序的内存布局的时候，它为未初始化的全局变量保留一些空间，比如，`int x;`，在内存中的被称为 `.bss` 的节后面会马上跟着一个 `.data`。C 规定 "未初始化的" 全局变量以一个 0 值开始。因此，在 ELF 二进制中 `.bss` 中并不存储内容；而是，链接器只记录地址和`.bss` 节的大小。加载器或者程序自身必须在 `.bss` 节中写入 0。

通过输入如下的命令来检查在内核中可运行的所有节的名字、大小、以及链接地址的列表：

```
athena% i386-jos-elf-objdump -h obj/kern/kernel

```

如果在你的计算机上默认使用的是一个 ELF 工具链，比如像大多数现代的 Linux 和 BSD，你可以使用 `objdump` 来代替 `i386-jos-elf-objdump`。

你将看到更多的节，而不仅是上面列出的那几个，但是，其它的那些节对于我们的实验目标来说并不重要。其它的那些节中大多数都是为了保留调试信息，它们一般包含在程序的可执行文件中，但是，这些节并不会被程序加载器加载到内存中。

我们需要特别注意 `.text` 节中的 VMA（或者链接地址）和 LMA（或者加载地址）。一个节的加载地址是那个节加载到内存中的地址。在 ELF 对象中，它保存在 `ph->p_pa` 域（在本案例中，它实际上是物理地址，不过 ELF 规范在这个域的意义方面规定的很模糊）。

一个节的链接地址是这个节打算在内存中运行时的地址。链接器在二进制代码中以变量的方式去编码这个链接地址，比如，当代码需要全局变量的地址时，如果二进制代码从一个未链接的地址去运行，结果将是无法运行。（它一般是去生成一个不包含任何一个绝对地址的、与位置无关的代码。现在的共享库大量使用的就是这种方法，但这是以性能和复杂性为代价的，所以，我们在 6.828 中不使用这种方法。）

一般情况下，链接和加载地址是一样的。比如，通过如下的命令去查看引导加载器的 `.text` 节：

```
athena% i386-jos-elf-objdump -h obj/boot/boot.out

```

BIOS 加载引导扇区到内存中的 0x7c00 地址，因此，这就是引导扇区的加载地址。这也是引导扇区的运行地址，因此，它也是链接地址。我们在`boot/Makefrag` 中通过传递 `-Ttext 0x7C00` 给链接器来设置链接地址，因此，链接器将在生成的代码中产生正确的内存地址。

>
> **练习 5**
>
>
> 如果你得到一个错误的引导加载器链接地址，通过再次跟踪引导加载器的前几个指令，你将会发现第一个指令会 “中断” 或者出错。然后在 `boot/Makefrag` 修改链接地址来修复错误，运行 `make clean`，使用 `make` 重新编译，然后再次跟踪引导加载器去查看会发生什么事情。不要忘了改回正确的链接地址，然后再次 `make clean`！
>
>
>

我们继续来看内核的加载和链接地址。与引导加载器不同，这里有两个不同的地址：内核告诉引导加载器加载它到内存的低位地址（小于 1 MB 的地址），但是它期望在一个高位地址来运行。我们将在下一节中深入研究它是如何实现的。

除了节的信息之外，在 ELF 头中还有一个对我们很重要的域，它叫做 `e_entry`。这个域保留着程序入口的链接地址：程序的 `.text` 节中的内存地址就是将要被执行的程序的地址。你可以用如下的命令来查看程序入口链接地址：

```
athena% i386-jos-elf-objdump -f obj/kern/kernel

```

你现在应该能够理解在 `boot/main.c` 中的最小的 ELF 加载器了。它从硬盘中读取内核的每个节，并将它们节的加载地址读入到内存中，然后跳转到内核的入口点。

>
> **练习 6**
>
>
> 我们可以使用 GDB 的 `x` 命令去检查内存。[GDB 手册](http://sourceware.org/gdb/current/onlinedocs/gdb_9.html#SEC63) 上讲的非常详细，但是现在，我们知道命令 `x/Nx ADDR` 是输出地址 `ADDR` 上 `N` 个<ruby> 词 <rt>  word </rt></ruby>就够了。（注意在命令中所有的 `x` 都是小写。）警告：<ruby> 词 <rt>  word </rt></ruby>的多少并没有一个普遍的标准。在 GNU 汇编中，一个<ruby> 词 <rt>  word </rt></ruby>是两个字节（在 xorw 中的 'w'，它在这个词中就是 2 个字节）。
>
>
>

重置机器（退出 QEMU/GDB 然后再次启动它们）。检查内存中在 `0x00100000` 地址上的 8 个词，输出 BIOS 上的引导加载器入口，然后再次找出引导载器上的内核的入口。为什么它们不一样？在第二个断点上有什么内容？（你并不用真的在 QEMU 上去回答这个问题，只需要思考就可以。）

### 第三部分：内核

我们现在开始去更详细地研究最小的 JOS 内核。（最后你还将写一些代码！）就像引导加载器一样，内核也是从一些汇编语言代码设置一些东西开始的，以便于 C 语言代码可以正确运行。

#### 使用虚拟内存去解决位置依赖问题

前面在你检查引导加载器的链接和加载地址时，它们是完全一样的，但是内核的链接地址（可以通过 `objdump` 来输出）和它的加载地址之间差别很大。可以回到前面去看一下，以确保你明白我们所讨论的内容。（链接内核比引导加载器更复杂，因此，链接和加载地址都在 `kern/kernel.ld` 的顶部。）

操作系统内核经常链接和运行在高位的虚拟地址，比如，`0xf0100000`，为的是给让用户程序去使用处理器的虚拟地址空间的低位部分。至于为什么要这么安排，在下一个实验中我们将会知道。

许多机器在 `0xf0100000` 处并没有物理地址，因此，我们不能指望在那个位置可以存储内核。相反，我们使用处理器的内存管理硬件去映射虚拟地址 `0xf0100000`（内核代码打算运行的链接地址）到物理地址 `0x00100000`（引导加载器将内核加载到内存的物理地址的位置）。通过这种方法，虽然内核的虚拟地址是高位的，离用户程序的地址空间足够远，它将被加载到 PC 的物理内存的 1MB 的位置，只处于 BIOS ROM 之上。这种方法要求 PC 至少要多于 1 MB 的物理内存（以便于物理地址 `0x00100000` 可以工作），这在上世纪九十年代以后生产的PC 上应该是没有问题的。

实际上，在下一个实验中，我们将映射整个 256 MB 的 PC 的物理地址空间，从物理地址 `0x00000000` 到 `0x0fffffff`，映射到虚拟地址 `0xf0000000` 到 `0xffffffff`。你现在就应该明白了为什么 JOS 只能使用物理内存的前 256 MB 的原因了。

现在，我们只映射前 4 MB 的物理内存，它足够我们的内核启动并运行。我们通过在 `kern/entrypgdir.c` 中手工写入静态初始化的页面目录和页面表就可以实现。现在，你不需要理解它们是如何工作的详细细节，只需要达到目的就行了。将上面的 `kern/entry.S` 文件中设置 `CR0_PG` 标志，内存引用就被视为物理地址（严格来说，它们是线性地址，但是，在 `boot/boot.S` 中设置了一个从线性地址到物理地址的映射标识，我们绝对不能改变它）。一旦 `CR0_PG` 被设置，内存引用的就是虚拟地址，这个虚拟地址是通过虚拟地址硬件将物理地址转换得到的。`entry_pgdir` 将把从 `0x00000000` 到 `0x00400000` 的物理地址范围转换在 `0xf0000000` 到 `0xf0400000` 的范围内的虚拟地址。任何不在这两个范围之一中的地址都将导致硬件异常，因为，我们还没有设置中断去处理这种情况，这种异常将导致 QEMU 去转储机器状态然后退出。（或者如果你没有在 QEMU 中应用 6.828 专用补丁，将导致 QEMU 无限重启。)

>
> **练习 7**
>
>
> 使用 QEMU 和 GDB 去跟踪进入到 JOS 内核，然后停止在 `movl %eax, %cr0` 指令处。检查 `0x00100000` 和 `0xf0100000` 处的内存。现在使用GDB 的 `stepi` 命令去单步执行那个指令。再次检查 `0x00100000` 和 `0xf0100000` 处的内存。确保你能理解这时发生的事情。
>
>
>

新映射建立之后的第一个指令是什么？如果没有映射到位，它将不能正常工作。在 `kern/entry.S` 中注释掉 `movl %eax, %cr0`。然后跟踪它，看看你的猜测是否正确。

#### 格式化控制台的输出

大多数人认为像 `printf()` 这样的函数是天生就有的，有时甚至认为这是 C 语言的 “原语”。但是在操作系统的内核中，我们需要自己去实现所有的 I/O。

通过阅读 `kern/printf.c`、`lib/printfmt.c`、以及 `kern/console.c`，确保你理解了它们之间的关系。在后面的实验中，你将会明白为什么 `printfmt.c` 是位于单独的 `lib` 目录中。

>
> **练习 8**
>
>
> 我们将省略掉一小部分代码片断 —— 这部分代码片断是使用 ”%o" 模式输出八进制数字所需要的。找到它并填充到这个代码片断中。
>
>
> 然后你就能够回答下列的问题：
>
>
> 1. 解释 `printf.c` 和 `console.c` 之间的接口。尤其是，`console.c` 出口的函数是什么？这个函数是如何被 `printf.c` 使用的？
> 2. 在 `console.c` 中解释下列的代码：
>
>
>
> ```
>  if (crt_pos >= CRT_SIZE) {
>     int i;
>     memcpy(crt_buf, crt_buf + CRT_COLS, (CRT_SIZE - CRT_COLS) * sizeof(uint16_t));
>     for (i = CRT_SIZE - CRT_COLS; i < CRT_SIZE; i++)
>         crt_buf[i] = 0x0700 | ' ';
>     crt_pos -= CRT_COLS;
>  }
>
> ```
> 3. 下列的问题你可能需要参考第一节课中的笔记。这些笔记涵盖了 GCC 在 x86 上的调用规则。
>
>
> 一步一步跟踪下列代码的运行：
>
>
>
> ```
>  int x = 1, y = 3, z = 4;
>  cprintf("x %d, y %x, z %d\n", x, y, z);
>
> ```
>
> 	1. 在调用 `cprintf()` 时，`fmt` 做了些什么？`ap` 做了些什么？
> 	2. （按运行顺序）列出 `cons_putc`、`va_arg`、以及 `vcprintf` 的调用列表。对于 `cons_putc`，同时列出它的参数。对于`va_arg`，列出调用之前和之后的 `ap` 内容？对于 `vcprintf`，列出它的两个参数值。
> 4. 运行下列代码：
>
>
>
> ```
> unsigned int i = 0x00646c72;
> cprintf("H%x Wo%s", 57616, &i);
>
> ```
>
> 输出是什么？解释如何在前面的练习中一步一步实现这个输出。这是一个 [ASCII 表](http://web.cs.mun.ca/%7Emichael/c/ascii-table.html)，它是一个字节到字符串的映射表。
>
>
> 这个输出取决于 x86 是小端法这一事实。如果这个 x86 采用大端法格式，你怎么去设置 `i`，以产生相同的输出？你需要将 `57616` 改变为一个不同值吗？
>
>
> [这是小端法和大端法的描述](http://www.webopedia.com/TERM/b/big_endian.html) 和 [一个更古怪的描述](http://www.networksorcery.com/enp/ien/ien137.txt)。
> 5. 在下列代码中，`y=` 会输出什么？（注意：这个问题没有确切值）为什么会发生这种情况？ `cprintf("x=%d y=%d", 3);`
> 6. 假设修改了 GCC 的调用规则，以便于按声明的次序在栈上推送参数，这样最后的参数就是最后一个推送进去的。那你如何去改变 `cprintf` 或者它的接口，以便它仍然可以传递数量可变的参数？
>
>
>

#### 栈

在本实验的最后一个练习中，我们将理详细地解释在 x86 中 C 语言是如何使用栈的，以及在这个过程中，我们将写一个新的内核监视函数，这个函数将输出栈的回溯信息：一个保存了指令指针（IP）值的列表，这个列表中有嵌套的 `call` 指令运行在当前运行点的指针值。

>
> **练习 9**
>
>
> 搞清楚内核在什么地方初始化栈，以及栈在内存中的准确位置。内核如何为栈保留空间？以及这个保留区域的 “结束” 位置是指向初始化结束后的指针吗？
>
>
>

x86 栈指针（`esp` 寄存器）指向当前使用的栈的最低位置。在这个区域中那个位置以下的所有部分都是空闲的。给一个栈推送一个值涉及下移栈指针和栈指针指向的位置中写入值。从栈中弹出一个值涉及到从栈指针指向的位置读取值和上移栈指针。在 32 位模式中，栈中仅能保存 32 位值，并且 `esp` 通常分为四部分。各种 x86 指令，比如，`call`，是 “硬编码” 去使用栈指针寄存器的。

相比之下，`ebp`（基指针）寄存器，按软件惯例主要是由栈关联的。在进入一个 C 函数时，函数的前序代码在函数运行期间，通常会通过推送它到栈中来保存前一个函数的基指针，然后拷贝当前的 `esp` 值到 `ebp` 中。如果一个程序中的所有函数都遵守这个规则，那么，在程序运行过程中的任何一个给定时间点，通过在 `ebp` 中保存的指针链和精确确定的函数嵌套调用顺序是如何到达程序中的这个特定的点，就可以通过栈来跟踪回溯。这种跟踪回溯的函数在实践中非常有用，比如，由于给某个函数传递了一个错误的参数，导致一个 `assert` 失败或者 `panic`，但是，你并不能确定是谁传递了错误的参数。栈的回溯跟踪可以让你找到这个惹麻烦的函数。

>
> **练习 10**
>
>
> 要熟悉 x86 上的 C 调用规则，可以在 `obj/kern/kernel.asm` 文件中找到函数 `test_backtrace` 的地址，设置一个断点，然后检查在内核启动后，每次调用它时发生了什么。每个递归嵌套的 `test_backtrace` 函数在栈上推送了多少个词（word），这些词（word）是什么？
>
>
>

上面的练习可以给你提供关于实现栈跟踪回溯函数的一些信息，为实现这个函数，你应该去调用 `mon_backtrace()`。在 `kern/monitor.c` 中已经给你提供了这个函数的一个原型。你完全可以在 C 中去使用它，但是，你可能需要在 `inc/x86.h` 中使用到 `read_ebp()` 函数。你应该在这个新函数中实现一个到内核监视命令的钩子，以便于用户可以与它交互。

这个跟踪回溯函数将以下面的格式显示一个函数调用列表：

```
Stack backtrace:
 ebp f0109e58 eip f0100a62 args 00000001 f0109e80 f0109e98 f0100ed2 00000031
 ebp f0109ed8 eip f01000d6 args 00000000 00000000 f0100058 f0109f28 00000061
 ...

```

输出的第一行列出了当前运行的函数，名字为 `mon_backtrace`，就是它自己，第二行列出了被 `mon_backtrace` 调用的函数，第三行列出了另一个被调用的函数，依次类推。你可以输出所有未完成的栈帧。通过研究 `kern/entry.S`，你可以发现，有一个很容易的方法告诉你何时停止。

在每一行中，`ebp` 表示了那个函数进入栈的基指针：即，栈指针的位置，它就是函数进入之后，函数的前序代码设置的基指针。`eip` 值列出的是函数的返回指令指针：当函数返回时，指令地址将控制返回。返回指令指针一般指向 `call` 指令之后的指令（想一想为什么？）。在 `args` 之后列出的五个十六进制值是在问题中传递给函数的前五个参数。当然，如果函数调用时传递的参数少于五个，那么，在这里就不会列出全部五个值了。（为什么跟踪回溯代码不能检测到调用时实际上传递了多少个参数？如何去修复这个 “缺陷”？）

下面是在阅读 K&R 的书中的第 5 章中的一些关键点，为了接下来的练习和将来的实验，你应该记住它们。

* 如果 `int *p = (int*)100`，那么 `(int)p + 1` 和 `(int)(p + 1)` 是不同的数字：前一个是 `101`，但是第二个是 `104`。当在一个指针上加一个整数时，就像第二种情况，这个整数将隐式地与指针所指向的对象相乘。
* `p[i]` 的定义与 `*(p+i)` 定义是相同的，都反映了在内存中由 `p` 指向的第 `i` 个对象。当对象大于一个字节时，上面的加法规则可以使这个定义正常工作。
* `&p[i]` 与 `(p+i)` 是相同的，获取在内存中由 p 指向的第 `i` 个对象的地址。

虽然大多数 C 程序不需要在指针和整数之间转换，但是操作系统经常做这种转换。不论何时，当你看到一个涉及内存地址的加法时，你要问你自己，你到底是要做一个整数加法还是一个指针加法，以确保做完加法后的值是正确的，而不是相乘后的结果。

>
> **练 11**
>
>
> 实现一个像上面详细描述的那样的跟踪回溯函数。一定使用与示例中相同的输出格式，否则，将会引发评级脚本的识别混乱。在你认为你做的很好的时候，运行 `make grade` 这个评级脚本去查看它的输出是否是我们的脚本所期望的结果，如果不是去修改它。你提交了你的实验 1 代码后，我们非常欢迎你将你的跟踪回溯函数的输出格式修改成任何一种你喜欢的格式。
>
>
>

在这时，你的跟踪回溯函数将能够给你提供导致 `mon_backtrace()` 被运行的，在栈上调用它的函数的地址。但是，在实践中，你经常希望能够知道这个地址相关的函数名字。比如，你可能希望知道是哪个有 Bug 的函数导致了你的内核崩溃。

为帮助你实现这个功能，我们提供了 `debuginfo_eip()` 函数，它在符号表中查找 `eip`，然后返回那个地址的调试信息。这个函数定义在 `kern/kdebug.c` 文件中。

>
> **练习 12**
>
>
> 修改你的栈跟踪回溯函数，对于每个 `eip`，显示相关的函数名字、源文件名、以及那个 `eip` 的行号。
>
>
>

在 `debuginfo_eip` 中，`__STAB_*` 来自哪里？这个问题的答案很长；为帮助你找到答案，下面是你需要做的一些事情：

* 在 `kern/kernel.ld` 文件中查找 `__STAB_*`
* 运行 `i386-jos-elf-objdump -h obj/kern/kernel`
* 运行 `i386-jos-elf-objdump -G obj/kern/kernel`
* 运行 `i386-jos-elf-gcc -pipe -nostdinc -O2 -fno-builtin -I. -MD -Wall -Wno-format -DJOS_KERNEL -gstabs -c -S kern/init.c, and look at init.s`。
* 如果引导加载器在加载二进制内核时，将符号表作为内核的一部分加载进内存中，那么，去查看它。

通过在 `stab_binsearch` 中插入调用，可以完成在 `debuginfo_eip` 中通过地址找到行号的功能。

在内核监视中添加一个 `backtrace` 命令，扩展你实现的 `mon_backtrace` 的功能，通过调用 `debuginfo_eip`，然后以下面的格式来输出每个栈帧行：

```
K> backtrace
Stack backtrace:
 ebp f010ff78 eip f01008ae args 00000001 f010ff8c 00000000 f0110580 00000000
 kern/monitor.c:143: monitor+106
 ebp f010ffd8 eip f0100193 args 00000000 00001aac 00000660 00000000 00000000
 kern/init.c:49: i386_init+59
 ebp f010fff8 eip f010003d args 00000000 00000000 0000ffff 10cf9a00 0000ffff
 kern/entry.S:70: <unknown>+0
K>

```

每行都给出了文件名和在那个文件中栈帧的 `eip` 所在的行，紧接着是函数的名字和那个函数的第一个指令到 `eip` 的偏移量（比如，`monitor+106` 意味着返回 `eip` 是从 `monitor` 开始之后的 106 个字节）。

为防止评级脚本引起混乱，应该将文件和函数名输出在单独的行上。

提示：`printf` 格式的字符串提供一个易用（尽管有些难理解）的方式去输出<ruby> 非空终止 <rt>  non-null-terminated </rt></ruby>字符串，就像在 STABS 表中的这些一样。`printf("%.*s", length, string)` 输出 `string` 中的最多 `length` 个字符。查阅 `printf` 的 man 页面去搞清楚为什么这样工作。

你可以从 `backtrace` 中找到那些没有的功能。比如，你或者可能看到一个到 `monitor()` 的调用，但是没有到 `runcmd()` 中。这是因为编译器的行内（in-lines）函数调用。其它的优化可能导致你看到一些意外的行号。如果你从 `GNUMakefile` 删除 `-O2` 参数，`backtraces` 可能会更有意义（但是你的内核将运行的更慢）。

**到此为止， 在 `lab` 目录中的实验全部完成**，使用 `git commit` 提交你的改变，然后输入 `make handin` 去提交你的代码。

---

via: <https://sipb.mit.edu/iap/6.828/lab/lab1/>

作者：[mit](https://sipb.mit.edu) 译者：[qhwdw](https://github.com/qhwdw) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


完全指南：在容器中运行 Jenkins 构建

Summary: 容器应用程序平台能够动态地启动具有资源限制的独立容器，从而改变了运行 CI/CD 任务的方式。


程序员最佳网站

不错不错！

W3Schools  打不开

可以的： https://www.w3schools.com/

# 程序员最佳网站

作为程序员，你经常会发现自己是某些网站的永久访问者。它们可以是教程、参考或论坛。因此，在这篇文章中，让我们看看给程序员的最佳网站。

### W3Schools

[W3Schools](https://www.w3schools.com/) 是为初学者和有经验的 Web 开发人员学习各种编程语言的最佳网站之一。你可以学习 HTML5、CSS3、PHP、 JavaScript、ASP 等。

更重要的是，该网站为网页开发人员提供了大量资源和参考资料。

[](http://www.theitstuff.com/wp-content/uploads/2017/12/w3schools-logo.png)

你可以快速浏览各种关键字及其功能。该网站非常具有互动性，它允许你在网站本身的嵌入式编辑器中尝试和练习代码。该网站是你作为网页开发人员少数需要经常访问的网站之一。

（LCTT 译注：有一个国内网站 [www.w3school.com.cn](http://www.w3school.com.cn) 提供类似的中文内容，但二者似无关系。）

### GeeksforGeeks

[GeeksforGeeks](http://www.geeksforgeeks.org/) 是一个主要专注于计算机科学的网站。它有大量的算法，解决方案和编程问题。

[](http://www.theitstuff.com/wp-content/uploads/2017/12/geeksforgeeks-programming-support.png)

该网站也有很多面试中经常问到的问题。由于该网站更多地涉及计算机科学，因此你可以找到很多编程问题在大多数著名语言下的解决方案。

### TutorialsPoint

一个学习任何东西的地方。[TutorialsPoint](https://www.tutorialspoint.com/) 有一些又好又简单的教程，它可以教你任何编程语言。我真的很喜欢这个网站，它不仅限于通用编程语言。

你可以在这里上找到几乎所有语言框架的教程。

### StackOverflow

你可能已经知道 [StackOverflow](https://stackoverflow.com/) 是遇到程序员的地方。你在代码中遇到问题，只要在 StackOverflow 问一个问题，来自互联网的程序员将会在那里帮助你。

[](http://www.theitstuff.com/wp-content/uploads/2017/12/stackoverflow-linux-programming-website.png)

关于 StackOverflow 最好的是几乎所有的问题都得到了答案。你可能会从其他程序员的几个不同观点获得答案。

### HackerRank

[HackerRank](https://www.hackerrank.com/) 是一个你可以参与各种编码竞赛并检测你的竞争能力的网站。

[](http://www.theitstuff.com/wp-content/uploads/2017/12/hackerrank-programming-forums.png)

这里有以各种编程语言举办的各种比赛，赢得比赛将增加你的分数。这个分数可以让你处于最高级别，并增加你获得一些软件公司注意的机会。

### Codebeautify

由于我们是程序员，所以美不是我们所关心的。很多时候，我们的代码很难被其他人阅读。[Codebeautify](https://codebeautify.org/) 可以使你的代码易于阅读。

该网站有大多数可以美化的语言。另外，如果你想让你的代码不能被某人读取，你也可以这样做。

这些是我选择的一些最好的程序员网站。如果你有经常访问的我没有提及的网站，请在下面的评论区让我知道。

---

via: <http://www.theitstuff.com/best-websites-programmers>

作者：[Rishabh Kandari](http://www.theitstuff.com/author/reevkandari) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


Stratis 从 ZFS、Btrfs 和 LVM 学到哪些

Summary: 深入了解这个强大而不繁琐的 Linux 存储管理系统。

深入了解这个强大而不繁琐的 Linux 存储管理系统。

# Stratis 从 ZFS、Btrfs 和 LVM 学到哪些

>
> 深入了解这个强大而不繁琐的 Linux 存储管理系统。
>
>
>

<img src="content_image/album_201806_13_225943j7iuzp9rstlyt79f.jpg" alt="" title="">

在本系列[第一部分](/article-9736-1.html)中提到，Stratis 是一个<ruby> 卷管理文件系统 <rt>  volume-managing filesystem </rt></ruby>（VMF），功能特性类似于 [ZFS](https://en.wikipedia.org/wiki/ZFS) 和 [Btrfs](https://en.wikipedia.org/wiki/Btrfs)。在设计 Stratis 过程中，我们研究了已有解决方案开发者做出的取舍。

### 为何不使用已有解决方案

理由千差万别。先说说 [ZFS](https://en.wikipedia.org/wiki/ZFS)，它最初由 Sun Microsystems 为 Solaris （目前为 Oracle 所有）开发，后移植到 Linux。但 [CDDL](https://en.wikipedia.org/wiki/Common_Development_and_Distribution_License) 协议授权的代码无法合并到 [GPL](https://en.wikipedia.org/wiki/GNU_General_Public_License) 协议授权的 Linux 源码树中。CDDL 与 GPLv2 是否真的不兼容有待讨论，但这种不确定性足以打消企业级 Linux 供应商采用并支持 ZFS 的积极性。

[Btrfs](https://en.wikipedia.org/wiki/Btrfs) 发展也很好，没有授权问题。它已经多年被很多用户列为“最佳文件系统”，但在稳定性和功能特性方面仍有待提高。

我们希望打破现状，解决已有方案的种种问题，这种渴望促成了 Stratis。

### Stratis 如何与众不同

ZFS 和 Btrfs 让我们知道一件事情，即编写一个内核支持的 VMF 文件系统需要花费极大的时间和精力，才能消除漏洞、增强稳定性。涉及核心数据时，提供正确性保证是必要的。如果 Stratis 也采用这种方案并从零开始的话，开发工作也需要十数年，这是无法接受的。

相反地，Stratis 采用 Linux 内核的其它一些已有特性：[device mapper](https://en.wikipedia.org/wiki/Device_mapper) 子系统以及久经考验的高性能文件系统 [XFS](https://en.wikipedia.org/wiki/XFS)，其中前者被 LVM 用于提供 RAID、精简配置和其它块设备特性而广为人知。Stratis 将已有技术作为（技术架构中的）层来创建存储池，目标是通过集成为用户提供一个看似无缝的整体。

### Stratis 从 ZFS 学到哪些

对很多用户而言，ZFS 影响了他们对下一代文件系统的预期。通过查看人们在互联网上关于 ZFS 的讨论，我们设定了 Stratis 的最初开发目标。ZFS 的设计思路也潜在地为我们指明应该避免哪些东西。例如，当挂载一个在其它主机上创建的存储池时，ZFS 需要一个“<ruby> 导入 <rt>  import </rt></ruby>”步骤。这样做出于某些原因，但每一种原因都似乎是 Stratis 需要解决的问题，无论是否采用同样的实现方式。

对于增加新硬盘或将已有硬盘替换为更大容量的硬盘，ZFS 有一些限制，尤其是存储池做了冗余配置的时候，这一点让我们不太满意。当然，这么设计也是有其原因的，但我们更愿意将其视为可以改进的空间。

最后，一旦掌握了 ZFS 的命令行工具，用户体验很好。我们希望让 Stratis 的命令行工具能够保持这种体验；同时，我们也很喜欢 ZFS 命令行工具的发展趋势，包括使用<ruby> 位置参数 <rt>  positional parameters </rt></ruby>和控制每个命令需要的键盘输入量。

（LCTT 译注：位置参数来自脚本，$n 代表第 n 个参数）

### Stratis 从 Btrfs 学到哪些

Btrfs 让我们满意的一点是，有单一的包含位置子命令的命令行工具。Btrfs 也将冗余（选择对应的 Btrfs profiles）视为存储池的特性之一。而且和 ZFS 相比实现方式更好理解，也允许增加甚至移除硬盘。

（LCTT 译注：Btrfs profiles 包括 single/DUP 和 各种 RAID 等类型）

最后，通过了解 ZFS 和 Btrfs 共有的特性，例如快照的实现、对发送/接收的支持，让我们更好的抉择 Stratis 应该包括的特性。

### Stratis 从 LVM 学到哪些

在 Stratis 设计阶段早期，我们仔细研究了 LVM。LVM 目前是 Linux device mapper (DM) 最主要的使用者；事实上，DM 就是由 LVM 的核心开发团队维护的。我们研究了将 LVM 真的作为 Stratis 其中一层的可能性，也使用 DM 做了实验，其中 Stratis 可以作为<ruby> 对等角色 <rt>  peer </rt></ruby>直接与 LVM 打交道。我们参考了 LVM 的<ruby> 磁盘元数据格式 <rt>  on-disk metadata format </rt></ruby>（也结合 ZFS 和 XFS 的相应格式），获取灵感并定义了 Stratis 的磁盘元数据格式。

在提到的项目中，LVM 与 Stratis 内在地有最多的共性，毕竟它们都使用 DM。不过从使用的角度来看，LVM 内在工作更加透明，为专业用户提供相当多的控制和选项，使其可以精确配置<ruby> 卷组 <rt>  volume group </rt></ruby>（存储池）的<ruby> 布局 <rt>  layout </rt></ruby>；但 Stratis 不采用这种方式。

### 多种多样的解决方案

基于自由和开源软件工作的明显好处在于，没有什么组件是不可替代的。包括内核在内的每个组成部分都是开源的，可以查看修改源代码，如果当前的软件不能满足用户需求可以用其它软件替换。新项目产生不一定意味着旧项目的终结，只要都有足够的（社区）支持，两者可以并行存在。

对于寻找一个不存在争议、简单易用、强大的本地存储管理解决方案的人而言，Stratis 是更好满足其需求的一种尝试。这意味着一种设计思路所做的抉择不一定对所有用户适用。考虑到用户的其它需求，另一种设计思路可能需要艰难的做出抉择。所有用户可以选择最适合其的工作的工具并从这种自由选择中受益。

---

via: <https://opensource.com/article/18/4/stratis-lessons-learned>

作者：[Andy Grover](https://opensource.com/users/agrover) 选题：[lujun9972](https://github.com/lujun9972) 译者：[pinewall](https://github.com/pinewall) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用机器学习来进行卡通上色

Summary: 我们可以自动应用简单的配色方案，而无需手绘几百个训练数据示例吗？

我们可以自动应用简单的配色方案，而无需手绘几百个训练数据示例吗？

# 使用机器学习来进行卡通上色

>
> 我们可以自动应用简单的配色方案，而无需手绘几百个训练数据示例吗？
>
>
>

监督式机器学习的一个大问题是需要大量的归类数据，特别是如果你没有这些数据时——即使这是一个充斥着大数据的世界，我们大多数人依然没有大数据——这就真的是一个大问题了。

尽管少数公司可以访问某些类型的大量归类数据，但对于大多数的组织和应用来说，创造足够的正确类型的归类数据，花费还是太高了，以至于近乎不可能。在某些时候，这个领域还是一个没有太多数据的领域（比如说，当我们诊断一种稀有的疾病，或者判断一个数据是否匹配我们已知的那一点点样本时）。其他时候，通过 Amazon Turkers 或者暑假工这些人工方式来给我们需要的数据做分类，这样做的花费太高了。对于一部电影长度的视频，因为要对每一帧做分类，所以成本上涨得很快，即使是一帧一美分。

### 大数据需求的一个大问题

我们团队目前打算解决一个问题是：我们能不能在没有手绘的数百或者数千训练数据的情况下，训练出一个模型，来自动化地为黑白像素图片提供简单的配色方案。

在这个实验中（我们称这个实验为龙画），面对深度学习庞大的对分类数据的需求，我们使用以下这种方法：

* 对小数据集的快速增长使用基于规则的的策略。
* 借用 tensorflow 图像转换的模型，Pix2Pix 框架，从而在训练数据非常有限的情况下实现自动化卡通渲染。

我曾见过 Pix2Pix 框架，在一篇论文（由 Isola 等人撰写的“Image-to-Image Translation with Conditional Adversarial Networks”）中描述的机器学习图像转换模型，假设 A 是风景图 B 的灰度版，在对 AB 对进行训练后，再给风景图片进行上色。我的问题和这是类似的，唯一的问题就是训练数据。

我需要的训练数据非常有限，因为我不想为了训练这个模型，一辈子画画和上色来为它提供彩色图片，深度学习模型需要成千上万（或者成百上千）的训练数据。

基于 Pix2Pix 的案例，我们需要至少 400 到 1000 个黑白、彩色成对的数据。你问我愿意画多少？可能就只有 30 个。我画了一小部分卡通花和卡通龙，然后去确认我是否可以把他们放进数据集中。

### 80% 的解决方案：按组件上色

*按组件规则对黑白像素进行上色*

当面对训练数据的短缺时，要问的第一个问题就是，是否有一个好的非机器学习的方法来解决我们的问题，如果没有一个完整的解决方案，那是否有一个部分的解决方案，这个部分解决方案对我们是否有好处？我们真的需要机器学习的方法来为花和龙上色吗？或者我们能为上色指定几何规则吗？

*如何按组件进行上色*

现在有一种非机器学习的方法来解决我的问题。我可以告诉一个孩子，我想怎么给我的画上色：把花的中心画成橙色，把花瓣画成黄色，把龙的身体画成橙色，把龙的尖刺画成黄色。

开始的时候，这似乎没有什么帮助，因为我们的电脑不知道什么是中心，什么是花瓣，什么是身体，什么是尖刺。但事实证明，我们可以依据连接组件来定义花和龙的部分，然后得到一个几何解决方案为我们 80% 的画来上色，虽然 80% 还不够，我们可以使用战略性违规转换、参数和机器学习来引导基于部分规则的解决方案达到 100%。

连接的组件使用的是 Windows 画图（或者类似的应用）上的色，例如，当我们对一个二进制黑白图像上色时，如果你单击一个白色像素，这个白色像素会在不穿过黑色的情况下变成一种新的颜色。在一个规则相同的卡通龙或者花的素描中，最大的白色组件就是背景，下一个最大的组件就是身体（加上手臂和腿）或者花的中心，其余的部分就是尖刺和花瓣，除了龙眼睛，它可以通过和背景的距离来做区分。

### 使用战略规则和 Pix2Pix 来达到 100%

我的一部分素描不符合规则，一条粗心画下的线可能会留下一个缺口，一条后肢可能会上成尖刺的颜色，一个小的，居中的雏菊会交换花瓣和中心的上色规则。

对于那 20% 我们不能用几何规则进行上色的部分，我们需要其他的方法来对它进行处理，我们转向 Pix2Pix 模型，它至少需要 400 到 1000 个素描/彩色对作为数据集（在 Pix2Pix 论文里的最小的数据集），里面包括违反规则的例子。

所以，对于每个违反规则的例子，我们最后都会通过手工的方式进行上色（比如后肢）或者选取一些符合规则的素描 / 彩色对来打破规则。我们在 A 中删除一些线，或者我们多转换一些，居中的花朵 A 和 B 使用相同的函数 (f) 来创造新的一对，f(A) 和 f(B)，一个小而居中的花朵，这可以加入到数据集。

### 使用高斯滤波器和同胚增大到最大

在计算机视觉中使用几何转换增强数据集是很常见的做法。例如循环，平移，和缩放。

但是如果我们需要把向日葵转换为雏菊或者把龙的鼻子变成球型和尖刺型呢？

或者如果说我们只需要大量增加数据量而不管过拟合？那我们需要比我们一开始使用的数据集大 10 到 30 倍的数据集。

 r cubed" title="Sunflower turned into a daisy with r -> r cubed">

*向日葵通过 r -> r 立方体方式变成一个雏菊*

*高斯滤波器增强*

单位盘的某些同胚可以形成很好的雏菊（比如 r -> r 立方体，高斯滤波器可以改变龙的鼻子。这两者对于数据集的快速增长是非常有用的，并且产生的大量数据都是我们需要的。但是他们也会开始用一种不能仿射转换的方式来改变画的风格。

之前我们考虑的是如何自动化地设计一个简单的上色方案，上述内容激发了一个在这之外的问题：什么东西定义了艺术家的风格，不管是外部的观察者还是艺术家自己？他们什么时候确定了自己的的绘画风格呢？他们不可能没有自己画画的算法？工具、助手和合作者之间的区别是什么？

### 我们可以走多远？

我们画画的投入可以有多低？保持在一个主题之内并且风格可以辨认出为某个艺术家的作品，在这个范围内我们可以创造出多少的变化和复杂性？我们需要做什么才能完成一个有无限的长颈鹿、龙、花的游行画卷？如果我们有了这样一幅，我们可以用它来做什么？

这些都是我们会继续在后面的工作中进行探索的问题。

但是现在，规则、增强和 Pix2Pix 模型起作用了。我们可以很好地为花上色了，给龙上色也不错。

*结果：通过花这方面的模型训练来给花上色。*

*结果：龙的模型训练的训练结果。*

想了解更多，参与 Gretchen Greene's talk, DragonPaint – bootstrapping small data to color cartoons, 在 PyCon Cleveland 2018.

---

via: <https://opensource.com/article/18/4/dragonpaint-bootstrapping>

作者：[K. Gretchen Greene](https://opensource.com/users/kggreene) 选题：[lujun9972](https://github.com/lujun9972) 译者：[hopefully2333](https://github.com/hopefully2333) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


大学生对开源的反思

Summary: 开源工具的威力和开源运动的重要性。

开源工具的威力和开源运动的重要性。

# 大学生对开源的反思

>
> 开源工具的威力和开源运动的重要性。
>
>
>

<img src="content_image/album_201806_14_120239rp39dcp933o9qlfl.png" alt="" title="">

我刚刚完成大学二年级的第一学期，我正在思考我在课堂上学到的东西。有一节课特别引起了我的注意：“[开源世界的基础](https://ssri.duke.edu/news/new-course-explores-open-source-principles)”，它由杜克大学的 Bryan Behrenshausen 博士讲授。我在最后一刻参加了课程，因为它看起来很有趣，诚实地来说，因为它符合我的日程安排。

第一天，Behrenshausen 博士问我们学生是否知道或使用过任何开源程序。直到那一天，我几乎没有听过[术语“开源”](https://opensource.com/node/42001)，当然也不知道任何属于该类别的产品。然而，随着学期的继续，对我而言，如果没有开源，我对事业抱负的激情就不会存在。

### Audacity 和 GIMP

我对技术的兴趣始于 12 岁。我负责为我的舞蹈团队裁剪音乐，我在网上搜索了几个小时，直到找到开源音频编辑器 Audacity。Audacity 为我敞开了大门。我不再局限于重复的 8 音节。我开始接受其他想要独特演绎他们最喜爱歌曲的人的请求。

几周后，我偶然在互联网上看到了一只有着 Pop-Tart 躯干并且后面拖着彩虹在太空飞行的猫。我搜索了“如何制作动态图像”，并发现了一个开源的图形编辑器 [GIMP](https://www.gimp.org/)，并用它为我兄弟做了一张“辛普森一家”的 GIF 作为生日礼物。

我萌芽的兴趣成长为完全的痴迷：在我笨重的、落后的笔记本上制作艺术品。由于我没有很好的炭笔，油彩或水彩，所以我用[图形设计](https://opensource.com/node/30251)作为创意的表达。我花了几个小时在计算机实验室上 [W3Schools](https://www.w3schools.com/) 学习 HTML 和 CSS 的基础知识，以便我可以用我幼稚的 GIF 填充在线作品集。几个月后，我在 [WordPress](https://opensource.com/node/31441) 发布了我的第一个网站。

### 为什么开源

开源让我们不仅可以实现我们的目标，还可以发现驱动这些目标的兴趣。

快进近十年。虽然有些仍然保持一致，但许多事情已经发生了变化：我仍然在制作图形（主要是传单），为舞蹈团编辑音乐，以及设计网站（我希望更时尚、更有效）。我使用的产品经历了无数版本升级。但最戏剧性的变化是我对开源资源的态度。

考虑到开源产品在我的生活中的意义，使我珍视开放运动和它的使命。开源项目提醒我，科技领域有些倡议可以促进社会的良好和自我学习，而不会被那些具有社会经济优势的人排斥。我中学时的自己，像无数其他人一样，无法购买 Adobe Creative Suite、GarageBand 或 Squarespace。开源平台使我们不仅能够实现我们的目标，而且还能通过扩大我们接触来发现推动这些目标的兴趣。

我的建议？一时心血来潮报名上课。它可能会改变你对世界的看法。

---

via: <https://opensource.com/article/18/3/college-getting-started>

作者：[Christine Hwang](https://opensource.com/users/christinehwang) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


尾调用、优化和 ES6

Summary: 当一个函数 F 调用另一个函数作为它的结束动作时，就发生了一个尾调用。

当一个函数 F 调用另一个函数作为它的结束动作时，就发生了一个尾调用。

# 尾调用、优化和 ES6

<img src="content_image/album_201806_14_125010urao2rzn7ccn7om0.jpg" alt="" title="">

在探秘“栈”的倒数第二篇文章中，我们提到了<ruby> 尾调用 <rt>  tail call </rt></ruby>、编译优化、以及新发布的 JavaScript 上<ruby> 合理尾调用 <rt>  proper tail call </rt></ruby>。

当一个函数 F 调用另一个函数作为它的结束动作时，就发生了一个**尾调用**。在那个时间点，函数 F 绝对不会有多余的工作：函数 F 将“球”传给被它调用的任意函数之后，它自己就“消失”了。这就是关键点，因为它打开了尾调用优化的“可能之门”：我们可以简单地重用函数 F 的栈帧，而不是为函数调用 [创建一个新的栈帧](https://manybutfinite.com/post/journey-to-the-stack)，因此节省了栈空间并且避免了新建一个栈帧所需要的工作量。下面是一个用 C 写的简单示例，然后使用 [mild 优化](https://github.com/gduarte/blog/blob/master/code/x86-stack/asm-tco.sh) 来编译它的结果：

```
int add5(int a)
{
    return a + 5;
}

int add10(int a)
{
    int b = add5(a); // not tail
    return add5(b); // tail
}

int add5AndTriple(int a){
    int b = add5(a); // not tail
    return 3 * add5(a); // not tail, doing work after the call
}

int finicky(int a){
    if (a > 10){
        return add5AndTriple(a); // tail
    }

    if (a > 5){
        int b = add5(a); // not tail
        return finicky(b); // tail
    }

    return add10(a); // tail
}

```

*简单的尾调用 [下载](https://manybutfinite.com/code/x86-stack/tail.c)*

在编译器的输出中，在预期会有一个 [调用](https://github.com/gduarte/blog/blob/master/code/x86-stack/tail.s#L37-L39) 的地方，你可以看到一个 [跳转](https://github.com/gduarte/blog/blob/master/code/x86-stack/tail-tco.s#L27) 指令，一般情况下你可以发现尾调用优化（以下简称 TCO）。在运行时中，TCO 将会引起调用栈的减少。

一个通常认为的错误观念是，尾调用必须要 [递归](https://manybutfinite.com/post/recursion/)。实际上并不是这样的：一个尾调用可以被递归，比如在上面的 `finicky()` 中，但是，并不是必须要使用递归的。在调用点只要函数 F 完成它的调用，我们将得到一个单独的尾调用。是否能够进行优化这是一个另外的问题，它取决于你的编程环境。

“是的，它总是可以！”，这是我们所希望的最佳答案，它是著名的 Scheme 中的方式，就像是在 [SICP](https://mitpress.mit.edu/sites/default/files/sicp/full-text/book/book-Z-H-11.html)上所讨论的那样（顺便说一声，如果你的程序不像“一个魔法师使用你的咒语召唤你的电脑精灵”那般有效，建议你读一下这本书）。它也是 [Lua](http://www.lua.org/pil/6.3.html) 的方式。而更重要的是，它是下一个版本的 JavaScript —— ES6 的方式，这个规范清晰地定义了[尾的位置](https://people.mozilla.org/%7Ejorendorff/es6-draft.html#sec-tail-position-calls)，并且明确了优化所需要的几个条件，比如，[严格模式](https://people.mozilla.org/%7Ejorendorff/es6-draft.html#sec-strict-mode-code)。当一个编程语言保证可用 TCO 时，它将支持<ruby> 合理尾调用 <rt>  proper tail call </rt></ruby>。

现在，我们中的一些人不能抛开那些 C 的习惯，心脏出血，等等，而答案是一个更复杂的“有时候”，它将我们带进了编译优化的领域。我们看一下上面的那个 [简单示例](https://github.com/gduarte/blog/blob/master/code/x86-stack/tail.c)；把我们 [上篇文章](/article-9609-1.html) 的阶乘程序重新拿出来：

```
#include  <stdio.h>

int factorial(int n)
{
    int previous = 0xdeadbeef;

    if (n == 0 || n == 1) {
        return 1;
    }

    previous = factorial(n-1);
    return n * previous;
}

int main(int argc)
{
    int answer = factorial(5);
    printf("%d\n", answer);
}

```

*递归阶乘 [下载](https://manybutfinite.com/code/x86-stack/factorial.c)*

像第 11 行那样的，是尾调用吗？答案是：“不是”，因为它被后面的 `n` 相乘了。但是，如果你不去优化它，GCC 使用 [O2 优化](https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html) 的 [结果](https://github.com/gduarte/blog/blob/master/code/x86-stack/factorial-o2.s) 会让你震惊：它不仅将阶乘转换为一个 [无递归循环](https://github.com/gduarte/blog/blob/master/code/x86-stack/factorial-o2.s#L16-L19)，而且 `factorial(5)` 调用被整个消除了，而以一个 120 (`5! == 120`) 的 [编译时常数](https://github.com/gduarte/blog/blob/master/code/x86-stack/factorial-o2.s#L38)来替换。这就是调试优化代码有时会很难的原因。好的方面是，如果你调用这个函数，它将使用一个单个的栈帧，而不会去考虑 n 的初始值。编译算法是非常有趣的，如果你对它感兴趣，我建议你去阅读 [构建一个优化编译器](http://www.amazon.com/Building-Optimizing-Compiler-Bob-Morgan-ebook/dp/B008COCE9G/) 和 [ACDI](http://www.amazon.com/Advanced-Compiler-Design-Implementation-Muchnick-ebook/dp/B003VM7GGK/)。

但是，这里**没有**做尾调用优化时到底发生了什么？通过分析函数的功能和无需优化的递归发现，GCC 比我们更聪明，因为一开始就没有使用尾调用。由于过于简单以及很确定的操作，这个任务变得很简单。我们给它增加一些可以引起混乱的东西（比如，`getpid()`），我们给 GCC 增加难度：

```
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int pidFactorial(int n)
{
    if (1 == n) {
        return getpid(); // tail
    }

    return n * pidFactorial(n-1) * getpid(); // not tail
}

int main(int argc)
{
    int answer = pidFactorial(5);
    printf("%d\n", answer);
}

```

*递归 PID 阶乘 [下载](https://manybutfinite.com/code/x86-stack/pidFactorial.c)*

优化它，unix 精灵！现在，我们有了一个常规的 [递归调用](https://github.com/gduarte/blog/blob/master/code/x86-stack/pidFactorial-o2.s#L20) 并且这个函数分配 O(n) 栈帧来完成工作。GCC 在递归的基础上仍然 [为 getpid 使用了 TCO](https://github.com/gduarte/blog/blob/master/code/x86-stack/pidFactorial-o2.s#L43)。如果我们现在希望让这个函数尾调用递归，我需要稍微变一下：

```
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int tailPidFactorial(int n, int acc)
{
    if (1 == n) {
        return acc * getpid(); // not tail
    }

    acc = (acc * getpid() * n);
    return tailPidFactorial(n-1, acc); // tail
}

int main(int argc)
{
    int answer = tailPidFactorial(5, 1);
    printf("%d\n", answer);
}

```

*tailPidFactorial.c [下载](https://manybutfinite.com/code/x86-stack/tailPidFactorial.c)*

现在，结果的累加是 [一个循环](https://github.com/gduarte/blog/blob/master/code/x86-stack/tailPidFactorial-o2.s#L22-L27)，并且我们获得了真实的 TCO。但是，在你庆祝之前，我们能说一下关于在 C 中的一般情形吗？不幸的是，虽然优秀的 C 编译器在大多数情况下都可以实现 TCO，但是，在一些情况下它们仍然做不到。例如，正如我们在 [函数序言](https://manybutfinite.com/post/epilogues-canaries-buffer-overflows/) 中所看到的那样，函数调用者在使用一个标准的 C 调用规则调用一个函数之后，它要负责去清理栈。因此，如果函数 F 带了两个参数，它只能使 TCO 调用的函数使用两个或者更少的参数。这是 TCO 的众多限制之一。Mark Probst 写了一篇非常好的论文，他们讨论了 [在 C 中的合理尾递归](http://www.complang.tuwien.ac.at/schani/diplarb.ps)，在这篇论文中他们讨论了这些属于 C 栈行为的问题。他也演示一些 [疯狂的、很酷的欺骗方法](http://www.complang.tuwien.ac.at/schani/jugglevids/index.html)。

“有时候” 对于任何一种关系来说都是不坚定的，因此，在 C 中你不能依赖 TCO。它是一个在某些地方可以或者某些地方不可以的离散型优化，而不是像合理尾调用一样的编程语言的特性，虽然在实践中可以使用编译器来优化绝大部分的情形。但是，如果你想必须要实现 TCO，比如将 Scheme <ruby> 转译 <rt>  transpilation </rt></ruby>成 C，你将会 [很痛苦](http://en.wikipedia.org/wiki/Tail_call#Through_trampolining)。

因为 JavaScript 现在是非常流行的转译对象，合理尾调用比以往更重要。因此，对 ES6 及其提供的许多其它的重大改进的赞誉并不为过。它就像 JS 程序员的圣诞节一样。

这就是尾调用和编译优化的简短结论。感谢你的阅读，下次再见！

---

via:<https://manybutfinite.com/post/tail-calls-optimization-es6/>

作者：[Gustavo Duarte](http://duartes.org/gustavo/blog/about/) 译者：[qhwdw](https://github.com/qhwdw) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


底层 Linux 容器运行时之发展史

Summary: “容器运行时”是一个被过度使用的名词。

“容器运行时”是一个被过度使用的名词。

# 底层 Linux 容器运行时之发展史

>
> “容器运行时”是一个被过度使用的名词。
>
>
>

在 Red Hat，我们乐意这么说，“容器即 Linux，Linux 即容器”。下面解释一下这种说法。传统的容器是操作系统中的进程，通常具有如下 3 个特性：

1. 资源限制

当你在系统中运行多个容器时，你肯定不希望某个容器独占系统资源，所以我们需要使用资源约束来控制 CPU、内存和网络带宽等资源。Linux 内核提供了 cgroup 特性，可以通过配置控制容器进程的资源使用。
2. 安全性配置

一般而言，你不希望你的容器可以攻击其它容器或甚至攻击宿主机系统。我们使用了 Linux 内核的若干特性建立安全隔离，相关特性包括 SELinux、seccomp 和 capabilities。

（LCTT 译注：从 2.2 版本内核开始，Linux 将特权从超级用户中分离，产生了一系列可以单独启用或关闭的 capabilities）
3. 虚拟隔离

容器外的任何进程对于容器而言都应该不可见。容器应该使用独立的网络。不同的容器对应的进程应该都可以绑定 80 端口。每个容器的<ruby> 内核映像 <rt>  image </rt></ruby>、<ruby> 根文件系统 <rt>  rootfs </rt></ruby>（rootfs）都应该相互独立。在 Linux 中，我们使用内核的<ruby> 名字空间 <rt>  namespace </rt></ruby>特性提供<ruby> 虚拟隔离 <rt>  virtual separation </rt></ruby>。

那么，具有安全性配置并且在 cgroup 和名字空间下运行的进程都可以称为容器。查看一下 Red Hat Enterprise Linux 7 操作系统中的 PID 1 的进程 systemd，你会发现 systemd 运行在一个 cgroup 下。

```
# tail -1 /proc/1/cgroup
1:name=systemd:/

```

`ps` 命令让我们看到 systemd 进程具有 SELinux 标签：

```
# ps -eZ | grep systemd
system_u:system_r:init_t:s0             1 ?     00:00:48 systemd

```

以及 capabilities：

```
# grep Cap /proc/1/status
...
CapEff: 0000001fffffffff
CapBnd: 0000001fffffffff
CapBnd:    0000003fffffffff

```

最后，查看 `/proc/1/ns` 子目录，你会发现 systemd 运行所在的名字空间。

```
ls -l /proc/1/ns
lrwxrwxrwx. 1 root root 0 Jan 11 11:46 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Jan 11 11:46 net -> net:[4026532009]
lrwxrwxrwx. 1 root root 0 Jan 11 11:46 pid -> pid:[4026531836]
...

```

如果 PID 1 进程（实际上每个系统进程）具有资源约束、安全性配置和名字空间，那么我可以说系统上的每一个进程都运行在容器中。

容器运行时工具也不过是修改了资源约束、安全性配置和名字空间，然后 Linux 内核运行起进程。容器启动后，容器运行时可以在容器内监控 PID 1 进程，也可以监控容器的标准输入/输出，从而进行容器进程的生命周期管理。

### 容器运行时

你可能自言自语道，“哦，systemd 看起来很像一个容器运行时”。经过若干次关于“为何容器运行时不使用 `systemd-nspawn` 工具来启动容器”的邮件讨论后，我认为值得讨论一下容器运行时及其发展史。

[Docker](https://github.com/docker) 通常被称为容器运行时，但“<ruby> 容器运行时 <rt>  container runtime </rt></ruby>”是一个被过度使用的词语。当用户提到“容器运行时”，他们其实提到的是为开发人员提供便利的<ruby> 上层 <rt>  high-level </rt></ruby>工具，包括 Docker，[CRI-O](https://github.com/kubernetes-incubator/cri-o) 和 [RKT](https://github.com/rkt/rkt)。这些工具都是基于 API 的，涉及操作包括从容器仓库拉取容器镜像、配置存储和启动容器等。启动容器通常涉及一个特殊工具，用于配置内核如何运行容器，这类工具也被称为“容器运行时”，下文中我将称其为“底层容器运行时”以作区分。像 Docker、CRI-O 这样的守护进程及形如 [Podman](https://github.com/projectatomic/libpod/tree/master/cmd/podman)、[Buildah](https://github.com/projectatomic/buildah) 的命令行工具，似乎更应该被称为“容器管理器”。

早期版本的 Docker 使用 `lxc` 工具集启动容器，该工具出现在 `systemd-nspawn` 之前。Red Hat 最初试图将 [libvirt](https://libvirt.org/) （`libvirt-lxc`）集成到 Docker 中替代 `lxc` 工具，因为 RHEL 并不支持 `lxc`。`libvirt-lxc` 也没有使用 `systemd-nspawn`，在那时 systemd 团队仅将 `systemd-nspawn` 视为测试工具，不适用于生产环境。

与此同时，包括我的 Red Hat 团队部分成员在内的<ruby> 上游 <rt>  upstream </rt></ruby> Docker 开发者，认为应该采用 golang 原生的方式启动容器，而不是调用外部应用。他们的工作促成了 libcontainer 这个 golang 原生库，用于启动容器。Red Hat 工程师更看好该库的发展前景，放弃了 `libvirt-lxc`。

后来成立 <ruby>   开放容器组织  <rt>  Open Container Initiative </rt></ruby>（OCI）的部分原因就是人们希望用其它方式启动容器。传统的基于名字空间隔离的容器已经家喻户晓，但人们也有<ruby> 虚拟机级别隔离 <rt>  virtual machine-level isolation </rt></ruby>的需求。Intel 和 [Hyper.sh](https://www.hyper.sh/) 正致力于开发基于 KVM 隔离的容器，Microsoft 致力于开发基于 Windows 的容器。OCI 希望有一份定义容器的标准规范，因而产生了 [OCI <ruby> 运行时规范 <rt>  Runtime Specification </rt></ruby>](https://github.com/opencontainers/runtime-spec)。

OCI 运行时规范定义了一个 JSON 文件格式，用于描述要运行的二进制，如何容器化以及容器根文件系统的位置。一些工具用于生成符合标准规范的 JSON 文件，另外的工具用于解析 JSON 文件并在该根文件系统（rootfs）上运行容器。Docker 的部分代码被抽取出来构成了 libcontainer 项目，该项目被贡献给 OCI。上游 Docker 工程师及我们自己的工程师创建了一个新的前端工具，用于解析符合 OCI 运行时规范的 JSON 文件，然后与 libcontainer 交互以便启动容器。这个前端工具就是 [runc](https://github.com/opencontainers/runc)，也被贡献给 OCI。虽然 `runc` 可以解析 OCI JSON 文件，但用户需要自行生成这些文件。此后，`runc` 也成为了最流行的底层容器运行时，基本所有的容器管理工具都支持 `runc`，包括 CRI-O、Docker、Buildah、Podman 和 [Cloud Foundry Garden](https://github.com/cloudfoundry/garden) 等。此后，其它工具的实现也参照 OCI 运行时规范，以便可以运行 OCI 兼容的容器。

[Clear Containers](https://clearlinux.org/containers) 和 Hyper.sh 的 `runV` 工具都是参照 OCI 运行时规范运行基于 KVM 的容器，二者将其各自工作合并到一个名为 [Kata](https://clearlinux.org/containers) 的新项目中。在去年，Oracle 创建了一个示例版本的 OCI 运行时工具，名为 [RailCar](https://github.com/oracle/railcar)，使用 Rust 语言编写。但该 GitHub 项目已经两个月没有更新了，故无法判断是否仍在开发。几年前，Vincent Batts 试图创建一个名为 [nspawn-oci](https://github.com/vbatts/nspawn-oci) 的工具，用于解析 OCI 运行时规范文件并启动 `systemd-nspawn`；但似乎没有引起大家的注意，而且也不是原生的实现。

如果有开发者希望实现一个原生的 `systemd-nspawn --oci OCI-SPEC.json` 并让 systemd 团队认可和提供支持，那么CRI-O、Docker 和 Podman 等容器管理工具将可以像使用 `runc` 和 Clear Container/runV （[Kata](https://github.com/kata-containers)） 那样使用这个新的底层运行时。（目前我的团队没有人参与这方面的工作。）

总结如下，在 3-4 年前，上游开发者打算编写一个底层的 golang 工具用于启动容器，最终这个工具就是 `runc`。那时开发者有一个使用 C 编写的 `lxc` 工具，在 `runc` 开发后，他们很快转向 `runc`。我很确信，当决定构建 libcontainer 时，他们对 `systemd-nspawn` 或其它非原生（即不使用 golang）的运行 namespaces 隔离的容器的方式都不感兴趣。

---

via: <https://opensource.com/article/18/1/history-low-level-container-runtimes>

作者：[Daniel Walsh](https://opensource.com/users/rhatdan) 译者：[pinewall](https://github.com/pinewall) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


无密码验证：服务器

# 无密码验证：服务器

<img src="content_image/album_201806_15_000057ms1oszswwhooiiis.jpg" alt="" title="">

无密码验证可以让你只输入一个 email 而无需输入密码即可登入系统。这是一种比传统的电子邮件/密码验证方式登入更安全的方法。

下面我将为你展示，如何在 [Go](https://golang.org/) 中实现一个 HTTP API 去提供这种服务。

### 流程

* 用户输入他的电子邮件地址。
* 服务器创建一个临时的一次性使用的代码（就像一个临时密码一样）关联到用户，然后给用户邮箱中发送一个“魔法链接”。
* 用户点击魔法链接。
* 服务器提取魔法链接中的代码，获取关联的用户，并且使用一个新的 JWT 重定向到客户端。
* 在每次有新请求时，客户端使用 JWT 去验证用户。

### 必需条件

* 数据库：我们为这个服务使用了一个叫 [CockroachDB](https://www.cockroachlabs.com/) 的 SQL 数据库。它非常像 postgres，但它是用 Go 写的。
* SMTP 服务器：我们将使用一个第三方的邮件服务器去发送邮件。开发的时我们使用 [mailtrap](https://mailtrap.io/)。Mailtrap 发送所有的邮件到它的收件箱，因此，你在测试时不需要创建多个假邮件帐户。

从 [Go 的主页](https://golang.org/dl/) 上安装它，然后使用 `go version`（1.10.1 atm）命令去检查它能否正常工作。

从 [CockroachDB 的主页](https://www.cockroachlabs.com/docs/stable/install-cockroachdb.html) 上下载它，展开它并添加到你的 `PATH` 变量中。使用 `cockroach version`（2.0 atm）命令检查它能否正常工作。

### 数据库模式

现在，我们在 `GOPATH` 目录下为这个项目创建一个目录，然后使用 `cockroach start` 启动一个新的 CockroachDB 节点：

```
cockroach start --insecure --host 127.0.0.1

```

它会输出一些内容，找到 SQL 地址行，它将显示像 `postgresql://root@127.0.0.1:26257?sslmode=disable` 这样的内容。稍后我们将使用它去连接到数据库。

使用如下的内容去创建一个 `schema.sql` 文件。

```
DROP DATABASE IF EXISTS passwordless_demo CASCADE;
CREATE DATABASE IF NOT EXISTS passwordless_demo;
SET DATABASE = passwordless_demo;

CREATE TABLE IF NOT EXISTS users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    email STRING UNIQUE,
    username STRING UNIQUE
);

CREATE TABLE IF NOT EXISTS verification_codes (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO users (email, username) VALUES
    ('john@passwordless.local', 'john_doe');

```

这个脚本创建了一个名为 `passwordless_demo` 的数据库、两个名为 `users` 和 `verification_codes` 的表，以及为了稍后测试而插入的一些假用户。每个验证代码都与用户关联并保存创建时间，以用于去检查验证代码是否过期。

在另外的终端中使用 `cockroach sql` 命令去运行这个脚本：

```
cat schema.sql | cockroach sql --insecure

```

### 环境配置

需要配置两个环境变量：`SMTP_USERNAME` 和 `SMTP_PASSWORD`，你可以从你的 mailtrap 帐户中获得它们。将在我们的程序中用到它们。

### Go 依赖

我们需要下列的 Go 包：

* [github.com/lib/pq](https://github.com/lib/pq)：它是 CockroachDB 使用的 postgres 驱动
* [github.com/matryer/way](https://github.com/matryer/way): 路由器
* [github.com/dgrijalva/jwt-go](https://github.com/dgrijalva/jwt-go): JWT 实现

```
go get -u github.com/lib/pq
go get -u github.com/matryer/way
go get -u github.com/dgrijalva/jwt-go

```

### 代码

#### 初始化函数

创建 `main.go` 并且通过 `init` 函数里的环境变量中取得一些配置来启动。

```
var config struct {
    port        int
    appURL      *url.URL
    databaseURL string
    jwtKey      []byte
    smtpAddr    string
    smtpAuth    smtp.Auth
}

func init() {
    config.port, _ = strconv.Atoi(env("PORT", "80"))
    config.appURL, _ = url.Parse(env("APP_URL", "http://localhost:"+strconv.Itoa(config.port)+"/"))
    config.databaseURL = env("DATABASE_URL", "postgresql://root@127.0.0.1:26257/passwordless_demo?sslmode=disable")
    config.jwtKey = []byte(env("JWT_KEY", "super-duper-secret-key"))
    smtpHost := env("SMTP_HOST", "smtp.mailtrap.io")
    config.smtpAddr = net.JoinHostPort(smtpHost, env("SMTP_PORT", "25"))
    smtpUsername, ok := os.LookupEnv("SMTP_USERNAME")
    if !ok {
        log.Fatalln("could not find SMTP_USERNAME on environment variables")
    }
    smtpPassword, ok := os.LookupEnv("SMTP_PASSWORD")
    if !ok {
        log.Fatalln("could not find SMTP_PASSWORD on environment variables")
    }
    config.smtpAuth = smtp.PlainAuth("", smtpUsername, smtpPassword, smtpHost)
}

func env(key, fallbackValue string) string {
    v, ok := os.LookupEnv(key)
    if !ok {
        return fallbackValue
    }
    return v
}

```

* `appURL` 将去构建我们的 “魔法链接”。
* `port` 将要启动的 HTTP 服务器。
* `databaseURL` 是 CockroachDB 地址，我添加 `/passwordless_demo` 前面的数据库地址去表示数据库名字。
* `jwtKey` 用于签名 JWT。
* `smtpAddr` 是 `SMTP_HOST` + `SMTP_PORT` 的联合；我们将使用它去发送邮件。
* `smtpUsername` 和 `smtpPassword` 是两个必需的变量。
* `smtpAuth` 也是用于发送邮件。

`env` 函数允许我们去获得环境变量，不存在时返回一个回退值。

#### 主函数

```
var db *sql.DB

func main() {
    var err error
    if db, err = sql.Open("postgres", config.databaseURL); err != nil {
        log.Fatalf("could not open database connection: %v\n", err)
    }
    defer db.Close()
    if err = db.Ping(); err != nil {
        log.Fatalf("could not ping to database: %v\n", err)
    }

    router := way.NewRouter()
    router.HandleFunc("POST", "/api/users", jsonRequired(createUser))
    router.HandleFunc("POST", "/api/passwordless/start", jsonRequired(passwordlessStart))
    router.HandleFunc("GET", "/api/passwordless/verify_redirect", passwordlessVerifyRedirect)
    router.Handle("GET", "/api/auth_user", authRequired(getAuthUser))

    addr := fmt.Sprintf(":%d", config.port)
    log.Printf("starting server at %s \n", config.appURL)
    log.Fatalf("could not start server: %v\n", http.ListenAndServe(addr, router))
}

```

首先，打开数据库连接。记得要加载驱动。

```
import (
    _ "github.com/lib/pq"
)

```

然后，我们创建路由器并定义一些端点。对于无密码流程来说，我们使用两个端点：`/api/passwordless/start` 发送魔法链接，和 `/api/passwordless/verify_redirect` 用 JWT 响应。

最后，我们启动服务器。

你可以创建空处理程序和中间件去测试服务器启动。

```
func createUser(w http.ResponseWriter, r *http.Request) {
    http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

func passwordlessStart(w http.ResponseWriter, r *http.Request) {
    http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

func passwordlessVerifyRedirect(w http.ResponseWriter, r *http.Request) {
    http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

func getAuthUser(w http.ResponseWriter, r *http.Request) {
    http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
}

func jsonRequired(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        next(w, r)
    }
}

func authRequired(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        next(w, r)
    }
}

```

接下来：

```
go build
./passwordless-demo

```

我们在目录中有了一个 “passwordless-demo”，但是你的目录中可能与示例不一样，`go build` 将创建一个同名的可执行文件。如果你没有关闭前面的 cockroach 节点，并且你正确配置了 `SMTP_USERNAME` 和 `SMTP_PASSWORD` 变量，你将看到命令 `starting server at http://localhost/` 没有错误输出。

#### 请求 JSON 的中间件

端点需要从请求体中解码 JSON，因此要确保请求是 `application/json` 类型。因为它是一个通用的东西，我将它解耦到中间件。

```
func jsonRequired(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ct := r.Header.Get("Content-Type")
        isJSON := strings.HasPrefix(ct, "application/json")
        if !isJSON {
            respondJSON(w, "JSON body required", http.StatusUnsupportedMediaType)
            return
        }
        next(w, r)
    }
}

```

实现很容易。首先它从请求头中获得内容的类型，然后检查它是否是以 “application/json” 开始，如果不是则以 `415 Unsupported Media Type` 提前返回。

#### 响应 JSON 的函数

以 JSON 响应是非常通用的做法，因此我把它提取到函数中。

```
func respondJSON(w http.ResponseWriter, payload interface{}, code int) {
    switch value := payload.(type) {
    case string:
        payload = map[string]string{"message": value}
    case int:
        payload = map[string]int{"value": value}
    case bool:
        payload = map[string]bool{"result": value}
    }
    b, err := json.Marshal(payload)
    if err != nil {
        respondInternalError(w, fmt.Errorf("could not marshal response payload: %v", err))
        return
    }
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(code)
    w.Write(b)
}

```

首先，对原始类型做一个类型判断，并将它们封装到一个 `map`。然后将它们编组到 JSON，设置响应内容类型和状态码，并写 JSON。如果 JSON 编组失败，则响应一个内部错误。

#### 响应内部错误的函数

`respondInternalError` 是一个响应 `500 Internal Server Error` 的函数，但是也同时将错误输出到控制台。

```
func respondInternalError(w http.ResponseWriter, err error) {
    log.Println(err)
    respondJSON(w,
        http.StatusText(http.StatusInternalServerError),
        http.StatusInternalServerError)
}

```

#### 创建用户的处理程序

下面开始编写 `createUser` 处理程序，因为它非常容易并且是 REST 式的。

```
type User struct {
    ID       string `json:"id"`
    Email    string `json:"email"`
    Username string `json:"username"`
}

```

`User` 类型和 `users` 表相似。

```
var (
    rxEmail = regexp.MustCompile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")
    rxUsername = regexp.MustCompile("^[a-zA-Z][\\w|-]{1,17}$")
)

```

这些正则表达式是分别用于去验证电子邮件和用户名的。这些都很简单，可以根据你的需要随意去适配。

现在，在 `createUser` 函数内部，我们将开始解码请求体。

```
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
    respondJSON(w, err.Error(), http.StatusBadRequest)
    return
}
defer r.Body.Close()

```

我们将使用请求体去创建一个 JSON 解码器来解码出一个用户指针。如果发生错误则返回一个 `400 Bad Request`。不要忘记关闭请求体读取器。

```
errs := make(map[string]string)
if user.Email == "" {
    errs["email"] = "Email required"
} else if !rxEmail.MatchString(user.Email) {
    errs["email"] = "Invalid email"
}
if user.Username == "" {
    errs["username"] = "Username required"
} else if !rxUsername.MatchString(user.Username) {
    errs["username"] = "Invalid username"
}
if len(errs) != 0 {
    respondJSON(w, errs, http.StatusUnprocessableEntity)
    return
}

```

这是我如何做验证；一个简单的 `map` 并检查如果 `len(errs) != 0`，则使用 `422 Unprocessable Entity` 去返回。

```
err := db.QueryRowContext(r.Context(), `
    INSERT INTO users (email, username) VALUES ($1, $2)
    RETURNING id
`, user.Email, user.Username).Scan(&user.ID)

if errPq, ok := err.(*pq.Error); ok && errPq.Code.Name() == "unique_violation" {
    if strings.Contains(errPq.Error(), "email") {
        errs["email"] = "Email taken"
    } else {
        errs["username"] = "Username taken"
    }
    respondJSON(w, errs, http.StatusForbidden)
    return
} else if err != nil {
    respondInternalError(w, fmt.Errorf("could not insert user: %v", err))
    return
}

```

这个 SQL 查询使用一个给定的 email 和用户名去插入一个新用户，并返回自动生成的 id，每个 `$` 将被接下来传递给 `QueryRowContext` 的参数替换掉。

因为 `users` 表在 `email` 和 `username` 字段上有唯一性约束，因此我将检查 “unique\_violation” 错误并返回 `403 Forbidden` 或者返回一个内部错误。

```
respondJSON(w, user, http.StatusCreated)

```

最后使用创建的用户去响应。

#### 无密码验证开始部分的处理程序

```
type PasswordlessStartRequest struct {
    Email       string `json:"email"`
    RedirectURI string `json:"redirectUri"`
}

```

这个结构体含有 `passwordlessStart` 的请求体：希望去登入的用户 email、来自客户端的重定向 URI（这个应用中将使用我们的 API）如：`https://frontend.app/callback`。

```
var magicLinkTmpl = template.Must(template.ParseFiles("templates/magic-link.html"))

```

我们将使用 golang 模板引擎去构建邮件，因此需要你在 `templates` 目录中，用如下的内容创建一个 `magic-link.html` 文件：

```
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Magic Link</title>
</head>
<body>
    Click <a href="{{ .MagicLink }}" target="_blank">here</a> to login.
    <br>
    <em>This link expires in 15 minutes and can only be used once.</em>
</body>
</html>

```

这个模板是给用户发送魔法链接邮件用的。你可以根据你的需要去随意调整它。

现在， 进入 `passwordlessStart` 函数内部：

```
var input PasswordlessStartRequest
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
    respondJSON(w, err.Error(), http.StatusBadRequest)
    return
}
defer r.Body.Close()

```

首先，我们像前面一样解码请求体。

```
errs := make(map[string]string)
if input.Email == "" {
    errs["email"] = "Email required"
} else if !rxEmail.MatchString(input.Email) {
    errs["email"] = "Invalid email"
}
if input.RedirectURI == "" {
    errs["redirectUri"] = "Redirect URI required"
} else if u, err := url.Parse(input.RedirectURI); err != nil || !u.IsAbs() {
    errs["redirectUri"] = "Invalid redirect URI"
}
if len(errs) != 0 {
    respondJSON(w, errs, http.StatusUnprocessableEntity)
    return
}

```

我们使用 golang 的 URL 解析器去验证重定向 URI，检查那个 URI 是否为绝对地址。

```
var verificationCode string
err := db.QueryRowContext(r.Context(), `
    INSERT INTO verification_codes (user_id) VALUES
        ((SELECT id FROM users WHERE email = $1))
    RETURNING id
`, input.Email).Scan(&verificationCode)
if errPq, ok := err.(*pq.Error); ok && errPq.Code.Name() == "not_null_violation" {
    respondJSON(w, "No user found with that email", http.StatusNotFound)
    return
} else if err != nil {
    respondInternalError(w, fmt.Errorf("could not insert verification code: %v", err))
    return
}

```

这个 SQL 查询将插入一个验证代码，这个代码通过给定的 email 关联到用户，并且返回一个自动生成的 id。因为有可能会出现用户不存在的情况，那样的话子查询可能解析为 `NULL`，这将导致在 `user_id` 字段上因违反 `NOT NULL` 约束而导致失败，因此需要对这种情况进行检查，如果用户不存在，则返回 `404 Not Found` 或者一个内部错误。

```
q := make(url.Values)
q.Set("verification_code", verificationCode)
q.Set("redirect_uri", input.RedirectURI)
magicLink := *config.appURL
magicLink.Path = "/api/passwordless/verify_redirect"
magicLink.RawQuery = q.Encode()

```

现在，构建魔法链接并设置查询字符串中的 `verification_code` 和 `redirect_uri` 的值。如：`http://localhost/api/passwordless/verify_redirect?verification_code=some_code&redirect_uri=https://frontend.app/callback`。

```
var body bytes.Buffer
data := map[string]string{"MagicLink": magicLink.String()}
if err := magicLinkTmpl.Execute(&body, data); err != nil {
    respondInternalError(w, fmt.Errorf("could not execute magic link template: %v", err))
    return
}

```

我们将得到的魔法链接模板的内容保存到缓冲区中。如果发生错误则返回一个内部错误。

```
to := mail.Address{Address: input.Email}
if err := sendMail(to, "Magic Link", body.String()); err != nil {
    respondInternalError(w, fmt.Errorf("could not mail magic link: %v", err))
    return
}

```

现在来写给用户发邮件的 `sendMail` 函数。如果发生错误则返回一个内部错误。

```
w.WriteHeader(http.StatusNoContent)

```

最后，设置响应状态码为 `204 No Content`。对于成功的状态码，客户端不需要很多数据。

#### 发送邮件函数

```
func sendMail(to mail.Address, subject, body string) error {
    from := mail.Address{
        Name:    "Passwordless Demo",
        Address: "noreply@" + config.appURL.Host,
    }
    headers := map[string]string{
        "From":         from.String(),
        "To":           to.String(),
        "Subject":      subject,
        "Content-Type": `text/html; charset="utf-8"`,
    }
    msg := ""
    for k, v := range headers {
        msg += fmt.Sprintf("%s: %s\r\n", k, v)
    }
    msg += "\r\n"
    msg += body

    return smtp.SendMail(
        config.smtpAddr,
        config.smtpAuth,
        from.Address,
        []string{to.Address},
        []byte(msg))
}

```

这个函数创建一个基本的 HTML 邮件结构体并使用 SMTP 服务器去发送它。邮件的内容你可以随意定制，我喜欢使用比较简单的内容。

#### 无密码验证重定向的处理程序

```
var rxUUID = regexp.MustCompile("^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")

```

首先，这个正则表达式去验证一个 UUID（即验证代码）。

现在进入 `passwordlessVerifyRedirect` 函数内部：

```
q := r.URL.Query()
verificationCode := q.Get("verification_code")
redirectURI := q.Get("redirect_uri")

```

`/api/passwordless/verify_redirect` 是一个 `GET` 端点，以便于我们从查询字符串中读取数据。

```
errs := make(map[string]string)
if verificationCode == "" {
    errs["verification_code"] = "Verification code required"
} else if !rxUUID.MatchString(verificationCode) {
    errs["verification_code"] = "Invalid verification code"
}
var callback *url.URL
var err error
if redirectURI == "" {
    errs["redirect_uri"] = "Redirect URI required"
} else if callback, err = url.Parse(redirectURI); err != nil || !callback.IsAbs() {
    errs["redirect_uri"] = "Invalid redirect URI"
}
if len(errs) != 0 {
    respondJSON(w, errs, http.StatusUnprocessableEntity)
    return
}

```

类似的验证，我们保存解析后的重定向 URI 到一个 `callback` 变量中。

```
var userID string
if err := db.QueryRowContext(r.Context(), `
    DELETE FROM verification_codes
    WHERE id = $1
        AND created_at >= now() - INTERVAL '15m'
    RETURNING user_id
`, verificationCode).Scan(&userID); err == sql.ErrNoRows {
    respondJSON(w, "Link expired or already used", http.StatusBadRequest)
    return
} else if err != nil {
    respondInternalError(w, fmt.Errorf("could not delete verification code: %v", err))
    return
}

```

这个 SQL 查询通过给定的 id 去删除相应的验证代码，并且确保它创建之后时间不超过 15 分钟，它也返回关联的 `user_id`。如果没有检索到内容，意味着代码不存在或者已过期，我们返回一个响应信息，否则就返回一个内部错误。

```
expiresAt := time.Now().Add(time.Hour * 24 * 60)
tokenString, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.StandardClaims{
    Subject:   userID,
    ExpiresAt: expiresAt.Unix(),
}).SignedString(config.jwtKey)
if err != nil {
    respondInternalError(w, fmt.Errorf("could not create JWT: %v", err))
    return
}

```

这些是如何去创建 JWT。我们为 JWT 设置一个 60 天的过期值，你也可以设置更短的时间（大约 2 周），并添加一个新端点去刷新令牌，但是不要搞的过于复杂。

```
expiresAtB, err := expiresAt.MarshalText()
if err != nil {
    respondInternalError(w, fmt.Errorf("could not marshal expiration date: %v", err))
    return
}
f := make(url.Values)
f.Set("jwt", tokenString)
f.Set("expires_at", string(expiresAtB))
callback.Fragment = f.Encode()

```

我们去规划重定向；你可使用查询字符串去添加 JWT，但是更常见的是使用一个哈希片段。如：`https://frontend.app/callback#jwt=token_here&expires_at=some_date`.

过期日期可以从 JWT 中提取出来，但是这样做的话，就需要在客户端上实现一个 JWT 库来解码它，因此为了简化，我将它加到这里。

```
http.Redirect(w, r, callback.String(), http.StatusFound)

```

最后我们使用一个 `302 Found` 重定向。

---

无密码的流程已经完成。现在需要去写 `getAuthUser` 端点的代码了，它用于获取当前验证用户的信息。你应该还记得，这个端点使用了 `guard` 中间件。

#### 使用 Auth 中间件

在编写 `guard` 中间件之前，我将编写一个不需要验证的分支。目的是，如果没有传递 JWT，它将不去验证用户。

```
type ContextKey struct {
    Name string
}

var keyAuthUserID = ContextKey{"auth_user_id"}

func withAuth(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        a := r.Header.Get("Authorization")
        hasToken := strings.HasPrefix(a, "Bearer ")
        if !hasToken {
            next(w, r)
            return
        }
        tokenString := a[7:]

        p := jwt.Parser{ValidMethods: []string{jwt.SigningMethodHS256.Name}}
        token, err := p.ParseWithClaims(
            tokenString,
            &jwt.StandardClaims{},
            func (*jwt.Token) (interface{}, error) { return config.jwtKey, nil },
        )
        if err != nil {
            respondJSON(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

        claims, ok := token.Claims.(*jwt.StandardClaims)
        if !ok || !token.Valid {
            respondJSON(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }

        ctx := r.Context()
        ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)

        next(w, r.WithContext(ctx))
    }
}

```

JWT 将在每次请求时以 `Bearer <token_here>` 格式包含在 `Authorization` 头中。因此，如果没有提供令牌，我们将直接通过，进入接下来的中间件。

我们创建一个解析器来解析令牌。如果解析失败则返回 `401 Unauthorized`。

然后我们从 JWT 中提取出要求的内容，并添加 `Subject`（就是用户 ID）到需要的地方。

#### Guard 中间件

```
func guard(next http.HandlerFunc) http.HandlerFunc {
    return withAuth(func(w http.ResponseWriter, r *http.Request) {
        _, ok := r.Context().Value(keyAuthUserID).(string)
        if !ok {
            respondJSON(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
            return
        }
        next(w, r)
    })
}

```

现在，`guard` 将使用 `withAuth` 并从请求内容中提取出验证用户的 ID。如果提取失败，它将返回 `401 Unauthorized`，提取成功则继续下一步。

#### 获取 Auth 用户

在 `getAuthUser` 处理程序内部：

```
ctx := r.Context()
authUserID := ctx.Value(keyAuthUserID).(string)

user, err := fetchUser(ctx, authUserID)
if err == sql.ErrNoRows {
    respondJSON(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
    return
} else if err != nil {
    respondInternalError(w, fmt.Errorf("could not query auth user: %v", err))
    return
}

respondJSON(w, user, http.StatusOK)

```

首先，我们从请求内容中提取验证用户的 ID，我们使用这个 ID 去获取用户。如果没有获取到内容，则发送一个 `418 I'm a teapot`，或者一个内部错误。最后，我们将用这个用户去响应。

#### 获取 User 函数

下面你看到的是 `fetchUser` 函数。

```
func fetchUser(ctx context.Context, id string) (User, error) {
    user := User{ID: id}
    err := db.QueryRowContext(ctx, `
        SELECT email, username FROM users WHERE id = $1
    `, id).Scan(&user.Email, &user.Username)
    return user, err
}

```

我将它解耦是因为通过 ID 来获取用户是个常做的事。

---

以上就是全部的代码。你可以自己去构建它和测试它。[这里](https://go-passwordless-demo.herokuapp.com/) 还有一个 demo 你可以试用一下。

如果你在 mailtrap 上点击之后出现有关 `脚本运行被拦截，因为文档的框架是沙箱化的，并且没有设置 'allow-scripts' 权限` 的问题，你可以尝试右键点击 “在新标签中打开链接“。这样做是安全的，因为邮件内容是 [沙箱化的](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox)。我在 `localhost` 上有时也会出现这个问题，但是我认为你一旦以 `https://` 方式部署到服务器上应该不会出现这个问题了。

如果有任何问题，请在我的 [GitHub repo](https://github.com/nicolasparada/go-passwordless-demo) 留言或者提交 PRs

以后，我为这个 API 写了一个客户端作为这篇文章的[第二部分](https://nicolasparada.netlify.com/posts/passwordless-auth-client/)。

---

via: <https://nicolasparada.netlify.com/posts/passwordless-auth-server/>

作者：[Nicolás Parada](https://nicolasparada.netlify.com/) 译者：[qhwdw](https://github.com/qhwdw) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


如何装载/卸载 Linux 内核模块

Summary: 找到并装载内核模块以解决外设问题。

找到并装载内核模块以解决外设问题。

# 如何装载/卸载 Linux 内核模块

>
> 找到并装载内核模块以解决外设问题。
>
>
>

本文来自 Manning 出版的 [Linux in Action](https://www.manning.com/books/linux-in-action?a_aid=bootstrap-it&a_bid=4ca15fc9&chan=opensource) 的第 15 章。

Linux 使用内核模块管理硬件外设。 我们来看看它是如何工作的。

运行中的 Linux 内核是您不希望被破坏的东西之一。毕竟，内核是驱动计算机所做的一切工作的软件。考虑到在一个运行的系统上必须同时管理诸多细节，最好能让内核尽可能的减少分心，专心的完成它的工作。但是，如果对计算环境进行任何微小的更改都需要重启整个系统，那么插入一个新的网络摄像头或打印机就可能会严重影响您的工作流程。每次添加设备时都必须重新启动，以使系统识别它，这效率很低。

为了在稳定性和可用性之间达成有效的平衡，Linux 将内核隔离，但是允许您通过可加载内核模块 (LKM) 实时添加特定的功能。如下图所示，您可以将模块视为软件的一部分，它告诉内核在哪里找到一个设备以及如何使用它。反过来，内核使设备对用户和进程可用，并监视其操作。

*内核模块充当设备和 Linux 内核之间的转换器。*

虽然你可以自己编写模块来完全按照你喜欢的方式来支持一个设备，但是为什么要这样做呢？Linux 模块库已经非常强大，通常不需要自己去实现一个模块。 而绝大多数时候，Linux 会自动加载新设备的模块，而您甚至不知道它。

不过，有时候，出于某种原因，它本身并不会自动进行。（你肯定不想让那个招聘经理不耐烦地一直等待你的笑脸加入视频面试。）为了帮助你解决问题，你需要更多地了解内核模块，特别是，如何找到运行你的外设的实际模块，然后如何手动激活它。

### 查找内核模块

按照公认的约定，内核模块是位于 `/lib/modules/` 目录下的具有 .ko（内核对象）扩展名的文件。 然而，在你找到这些文件之前，你还需要选择一下。因为在引导时你需要从版本列表中选择其一加载，所以支持您选择的特定软件（包括内核模块）必须存在某处。 那么，`/lib/modules/` 就是其中之一。 你会发现目录里充满了每个可用的 Linux 内核版本的模块; 例如：

```
$ ls /lib/modules
4.4.0-101-generic
4.4.0-103-generic
4.4.0-104-generic

```

在我的电脑上，运行的内核是版本号最高的版本（4.4.0-104-generic），但不能保证这对你来说是一样的（内核经常更新）。 如果您将要在一个运行的系统上使用模块完成一些工作的话，你需要确保您找到正确的目录树。

好消息：有一个可靠的窍门。相对于通过名称来识别目录，并希望能够找到正确的目录，你可以使用始终指向使用的内核名称的系统变量。 您可以使用 `uname -r`（ `-r` 从系统信息中指定通常显示的内核版本号）来调用该变量：

```
$ uname -r
4.4.0-104-generic

```

通过这些信息，您可以使用称为命令替换的过程将 `uname` 并入您的文件系统引用中。 例如，要导航到正确的目录，您需要将其添加到 `/lib/modules` 。 要告诉 Linux “uname” 不是一个文件系统中的位置，请将 `uname` 部分用反引号括起来，如下所示：

```
$ ls /lib/modules/`uname -r`
build   modules.alias        modules.dep      modules.softdep
initrd  modules.alias.bin    modules.dep.bin  modules.symbols
kernel  modules.builtin      modules.devname  modules.symbols.bin
misc    modules.builtin.bin  modules.order    vdso

```

你可以在 `kernel/` 目录下的子目录中找到大部分模块。 花几分钟时间浏览这些目录，了解事物的排列方式和可用内容。 这些文件名通常会让你知道它们是什么。

```
$ ls /lib/modules/`uname -r`/kernel
arch  crypto  drivers  fs  kernel  lib  mm
net  sound  ubuntu  virt  zfs

```

这是查找内核模块的一种方法；实际上，这是一种快速的方式。 但这不是唯一的方法。 如果你想获得完整的集合，你可以使用 `lsmod` 列出所有当前加载的模块以及一些基本信息。 这个截断输出的第一列（在这里列出的太多了）是模块名称，后面是文件大小和数量，然后是每个模块的名称：

```
$ lsmod
[...]
vboxdrv          454656  3 vboxnetadp,vboxnetflt,vboxpci
rt2x00usb        24576  1 rt2800usb
rt2800lib        94208  1 rt2800usb
[...]

```

到底有多少？好吧，我们再运行一次 `lsmod` ，但是这一次将输出管道输送到 `wc -l` 看一下一共多少行:

```
$ lsmod | wc -l
113

```

这是已加载的模块。 总共有多少个？ 运行 `modprobe -c` 并计算这些行将给我们这个数字：

```
$ modprobe -c | wc -l
33350

```

有 33,350 个可用模块！？ 看起来好像有人多年来一直在努力为我们提供软件来驱动我们的物理设备。

注意：在某些系统中，您可能会遇到自定义的模块，这些模块要么在 `/etc/modules` 文件中使用独特的条目进行引用，要么在 `/etc/modules-load.d/` 下的配置文件中。这些模块很可能是本地开发项目的产物，可能涉及前沿实验。不管怎样，知道你看到的是什么总是好的。

这就是如何找到模块的方法。 如果出于某种原因，它不会自行加载，您的下一个工作就是弄清楚如何手动加载未激活的模块。

### 手动加载内核模块

在加载内核模块之前，逻辑上您必须确认它存在。在这之前，你需要知道它叫什么。要做到这一点，有时需要兼有魔法和运气以及在线文档作者的辛勤工作的帮助。

我将通过描述一段时间前遇到的问题来说明这个过程。在一个晴朗的日子里，出于某种原因，笔记本电脑上的 WiFi 接口停止工作了。就这样。也许是软件升级把它搞砸了。谁知道呢？我运行了 `lshw -c network` ，得到了这个非常奇怪的信息:

```
network UNCLAIMED
    AR9485 Wireless Network Adapter

```

Linux 识别到了接口（Atheros AR9485），但将其列为未声明。 那么，正如他们所说的那样，“当情况变得严峻时，就会在互联网上进行艰难的搜索。” 我搜索了一下 atheros ar9 linux 模块，在浏览了一页又一页五年前甚至是十年前的页面后，它们建议我自己写个模块或者放弃吧，然后我终于发现（最起码 Ubuntu 16.04）有一个可以工作的模块。 它的名字是 ath9k 。

是的! 这场战斗胜券在握！向内核添加模块比听起来容易得多。 要仔细检查它是否可用，可以针对模块的目录树运行 `find`，指定 `-type f` 来告诉 Linux 您正在查找文件，然后将字符串 `ath9k` 和星号一起添加以包含所有以你的字符串打头的文件：

```
$ find /lib/modules/$(uname -r) -type f -name ath9k*
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_common.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_htc.ko
/lib/modules/4.4.0-97-generic/kernel/drivers/net/wireless/ath/ath9k/ath9k_hw.ko

```

再一步，加载模块：

```
# modprobe ath9k

```

就是这样。无启动，没烦恼。

这里还有一个示例，向您展示如何使用已经崩溃的运行模块。曾经有一段时间，我使用罗技网络摄像头和一个特定的软件会使摄像头在下次系统启动前无法被任何其他程序访问。有时我需要在不同的应用程序中打开相机，但没有时间关机重新启动。（我运行了很多应用程序，在引导之后将它们全部准备好需要一些时间。）

由于这个模块可能是运行的，所以使用 `lsmod` 来搜索 video 这个词应该给我一个关于相关模块名称的提示。 实际上，它比提示更好：用 video 这个词描述的唯一模块是 uvcvideo（如下所示）：

```
$ lsmod | grep video
uvcvideo               90112  0
videobuf2_vmalloc      16384  1 uvcvideo
videobuf2_v4l2         28672  1 uvcvideo
videobuf2_core         36864  2 uvcvideo,videobuf2_v4l2
videodev              176128  4 uvcvideo,v4l2_common,videobuf2_core,videobuf2_v4l2
media                  24576  2 uvcvideo,videodev

```

有可能是我自己的操作导致了崩溃，我想我可以挖掘更深一点，看看我能否以正确的方式解决问题。但结果你知道的；有时你不关心理论，只想让设备工作。 所以我用 `rmmod` 杀死了 `uvcvideo` 模块，然后用 `modprobe` 重新启动它，一切都好：

```
# rmmod uvcvideo
# modprobe uvcvideo

```

再一次：不重新启动。没有其他的后续影响。

---

via: <https://opensource.com/article/18/5/how-load-or-unload-linux-kernel-module>

作者：[David Clinton](https://opensource.com/users/dbclinton) 选题：[lujun9972](https://github.com/lujun9972) 译者：[amwps290](https://github.com/amwps290) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


Vim-plug：极简 Vim 插件管理器

没法用，你用过么，瞎翻译

来自120.244.214.22的 Internet Explorer 11.0|Windows 10 用户

挺好用的，说实话

:PlugXXX应该是:PluginXXX，少了`in`

# Vim-plug：极简 Vim 插件管理器

<img src="content_image/album_201806_16_114435wd3f3jgwjeewfdxs.png" alt="" title="">

当没有插件管理器时，Vim 用户必须手动下载 tarball 包形式的插件，并将它们解压到 `~/.vim` 目录中。在少量插件的时候可以。但当他们安装更多的插件时，就会变得一团糟。所有插件文件分散在单个目录中，用户无法找到哪个文件属于哪个插件。此外，他们无法找到他们应该删除哪个文件来卸载插件。这时 Vim 插件管理器就可以派上用场。插件管理器将安装插件的文件保存在单独的目录中，因此管理所有插件变得非常容易。我们几个月前已经写了关于 [Vundle](/article-9416-1.html) 的文章。今天，我们将看到又一个名为 “Vim-plug” 的 Vim 插件管理器。

Vim-plug 是一个自由、开源、速度非常快的、极简的 vim 插件管理器。它可以并行地安装或更新插件。你还可以回滚更新。它创建<ruby> 浅层克隆 <rt>  shallow clone </rt></ruby>最小化磁盘空间使用和下载时间。它支持按需加载插件以加快启动时间。其他值得注意的特性是支持分支/标签/提交、post-update 钩子、支持外部管理的插件等。

### 安装

安装和使用起来非常容易。你只需打开终端并运行以下命令：

```
$ curl -fLo ~/.vim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

```

Neovim 用户可以使用以下命令安装 Vim-plug：

```
$ curl -fLo ~/.config/nvim/autoload/plug.vim --create-dirs https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

```

### 用法

#### 安装插件

要安装插件，你必须如下所示首先在 Vim 配置文件中声明它们。一般 Vim 的配置文件是 `~/.vimrc`，Neovim 的配置文件是 `~/.config/nvim/init.vim`。请记住，当你在配置文件中声明插件时，列表应该以 `call plug#begin(PLUGIN_DIRECTORY)` 开始，并以 `plug#end()` 结束。

例如，我们安装 “lightline.vim” 插件。为此，请在 `~/.vimrc` 的顶部添加以下行。

```
call plug#begin('~/.vim/plugged')
Plug 'itchyny/lightline.vim'
call plug#end()

```

在 vim 配置文件中添加上面的行后，通过输入以下命令重新加载：

```
:source ~/.vimrc

```

或者，只需重新加载 Vim 编辑器。

现在，打开 vim 编辑器：

```
$ vim

```

使用以下命令检查状态：

```
:PlugStatus

```

然后输入下面的命令，然后按回车键安装之前在配置文件中声明的插件。

```
:PlugInstall

```

#### 更新插件

要更新插件，请运行：

```
:PlugUpdate

```

更新插件后，按下 `d` 查看更改。或者，你可以之后输入 `:PlugDiff`。

#### 审查插件

有时，更新的插件可能有新的 bug 或无法正常工作。要解决这个问题，你可以简单地回滚有问题的插件。输入 `:PlugDiff` 命令，然后按回车键查看上次 `:PlugUpdate`的更改，并在每个段落上按 `X` 将每个插件回滚到更新前的前一个状态。

#### 删除插件

删除一个插件删除或注释掉你以前在你的 vim 配置文件中添加的 `plug` 命令。然后，运行 `:source ~/.vimrc` 或重启 Vim 编辑器。最后，运行以下命令卸载插件：

```
:PlugClean

```

该命令将删除 vim 配置文件中所有未声明的插件。

#### 升级 Vim-plug

要升级vim-plug本身，请输入：

```
:PlugUpgrade

```

如你所见，使用 Vim-plug 管理插件并不难。它简化了插件管理。现在去找出你最喜欢的插件并使用 Vim-plug 来安装它们。

就是这些了。我将很快在这里发布另一个有趣的话题。在此之前，请继续关注我们。

干杯!

---

via: <https://www.ostechnix.com/vim-plug-a-minimalist-vim-plugin-manager/>

作者：[SK](https://www.ostechnix.com/author/sk/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


机器人学影响 CIO 角色的 3 种方式

Summary: 机器人流程自动化如何影响 CIO ？考虑这些可能。

机器人流程自动化如何影响 CIO ？考虑这些可能。

# 机器人学影响 CIO 角色的 3 种方式

>
> 机器人流程自动化如何影响 CIO ？考虑这些可能。
>
>
>

<img src="content_image/album_201806_16_120845tip64et67xp4qpqt.png" alt="配图" title="配图">

随着 2017 的结束，许多 CIO 们的 2018 年目标也将确定。或许你们将参与到机器人流程自动化（RPA）中。多年以来，RPA 对许多公司来说只是一个可望不可及的概念。但是随着组织被迫变得越来越敏捷高效，RPA 所具有的潜在优势开始受到重视。

根据 Redwood Sofeware 和 Sapio Research 的最新 [研究报告](https://www.redwood.com/press-releases/it-decision-makers-speak-59-of-business-processes-could-be-automated-by-2022/)，IT 决策者们相信，未来 5 年有 59% 的业务可以被自动化处理，从而产生新的速度和效率，并且消减相应的重复性的人工工作量的岗位。但是，目前在相应岗位上没有实施 RPA 的公司中，有 20% 的公司员工超过 1000 人。

对于 CIO 们来说，RPA 会潜在影响你在业务中的角色和你的团队。随着 RPA 的地位在 2018 年中越来越重要，CIO 们和其它 IT 决策者的角色也能会受到如下三种方式的改变：

### 成为战略性变革代理人的机会增加

随着压力的增长，用最少的时间做更多的事，内部流程越来越重要。在每个企业中，员工们每天做着既重要又枯燥的一些任务。这些任务可能是枯燥且重复性的任务，但是要求他们必须快速完成且不能出错。

**[有关你的自动化策略的 ROI 建议，查看我们相关的文章，[如何在自动化方面提升 ROI：4 个小提示](https://enterprisersproject.com/article/2017/11/how-improve-roi-automation-4-tips?sc_cid=70160000000h0aXAAQ)。]**

在财务中从后台的一些操作到采购、供应链、账务、客户服务、以及人力资源，在一个组织中几乎所有的岗位都有一些枯燥的任务。这就为 CIO 们提供了一个机会，将 IT 与业务联合起来，成为使用 RPA 进行战略变革的先锋。

除了过去的屏幕抓取技术，机器人现在已经实现可定制化，即插即用的解决方案可以根据企业的需要进行设计。使用这种以流程为中心的方法，企业不仅可以将以前由人来完成的任务进行自动化，也可以将应用程序和系统特定任务自动化，比如 ERP 和企业其它应用程序。

为端到端的流程实施更高级别的自动化将是 CIO 们的价值所在。CIO 们将会站在这种机遇的最前沿。

### 重新关注人和培训

技术的变动将让员工更加恐慌，尤其是当这些自动化的变动涉及到他们每日职责的相当大的部分。CIO 们应该清楚地告诉他们，RPA 将如何让他们的角色和责任变得更好，以及用数据说明的、最终将影响到他们底线的战略决策。

当实施 RPA 时，清晰明确地表达出“人对组织的成功来说是很重要的”信息是很关键的，并且，这种成功需要使技术和人的技能实现适当的平衡。

CIO 们也应该分析工作流并实施更好的流程，这种流程能够超越以前由终端用户完成的特定任务。通过端到端的流程自动化，CIO 们可以让员工表现更加出色。

因为在整个自动化的过程中，提升和培训员工技术是很重要的，CIO 们必须与企业高管们一起去决定，帮助员工自信地应对变化的培训项目。

### 需要长远的规划

为确保机器人流程自动化的成功，甲方必须有一个长期的方法。这将要求一个弹性的解决方案，它将对整个业务模型有好处，包括消费者。比如，当年亚马逊为他的 Prime 消费者推广快速投递服务时，他们不仅在他们的仓库中重新调整了订单交付流程，他们也自动化了他们的在线客户体验，让它们变得更简化、更快速，并且让消费者比以前更容易下订单。

在即将到来的一年里，CIO 们可以用同样的方法来应用技术，构建整体的解决方案来改变组织的动作方式。裁员只会让最终效益产生很大影响，但是流程自动化将使 CIO 们能够通过优化和授权来考虑更大的问题。这种方法让 CIO 们去为他们自己和 RPA 构建长期的信誉。这反过来将强化 CIO 们作为探路者的角色，并为整个组织的成功作出贡献。

对于 CIO 们来说，采用一个长期的、策略性的方法让 RPA 成功，需要时间和艰苦的努力。尽管如此，那些致力于创建一种人力和技术平衡战略的 CIO 们，将会在现在和将来实现更大的价值。

---

via: <https://enterprisersproject.com/article/2017/11/3-ways-robotics-affects-cio-role>

作者：[Dennis Walsh](https://enterprisersproject.com/user/dennis-walsh) 译者：[qhwdw](https://github.com/qhwdw) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


如何在 Linux 中不安装软件测试一个软件包

# 如何在 Linux 中不安装软件测试一个软件包

<img src="content_image/album_201806_16_225749u8hbmbr1r88nhm1k.png" alt="" title="">

出于某种原因，你可能需要在将软件包安装到你的 Linux 系统之前对其进行测试。如果是这样，你很幸运！今天，我将向你展示如何在 Linux 中使用 **Nix** 包管理器来实现。Nix 包管理器的一个显著特性是它允许用户测试软件包而无需先安装它们。当你想要临时使用特定的程序时，这会很有帮助。

### 测试一个软件包而不在 Linux 中安装它

确保你先安装了 Nix 包管理器。如果尚未安装，请参阅以下指南。

例如，假设你想测试你的 C++ 代码。你不必安装 GCC。只需运行以下命令：

```
$ nix-shell -p gcc

```

该命令会构建或下载 gcc 软件包及其依赖项，然后将其放入一个存在 `gcc` 命令的 Bash shell 中，所有这些都不会影响正常环境。

```
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
download-using-manifests.pl: perl: warning: Setting locale failed.
download-using-manifests.pl: perl: warning: Please check that your locale settings:
download-using-manifests.pl: LANGUAGE = (unset),
download-using-manifests.pl: LC_ALL = (unset),
download-using-manifests.pl: LANG = "en_US.UTF-8"
download-using-manifests.pl: are supported and installed on your system.
download-using-manifests.pl: perl: warning: Falling back to the standard locale ("C").
download-from-binary-cache.pl: perl: warning: Setting locale failed.
download-from-binary-cache.pl: perl: warning: Please check that your locale settings:
download-from-binary-cache.pl: LANGUAGE = (unset),
download-from-binary-cache.pl: LC_ALL = (unset),
download-from-binary-cache.pl: LANG = "en_US.UTF-8"

[...]

fetching path ‘/nix/store/6mk1s81va81dl4jfbhww86cwkl4gyf4j-stdenv’...
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = "en_US.UTF-8"
are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").

*** Downloading ‘https://cache.nixos.org/nar/0aznfg1g17a8jdzvnp3pqszs9rq2wiwf2rcgczyg5b3k6d0iricl.nar.xz’ to ‘/nix/store/6mk1s81va81dl4jfbhww86cwkl4gyf4j-stdenv’...
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 8324 100 8324 0 0 6353 0 0:00:01 0:00:01 --:--:-- 6373

[nix-shell:~]$

```

检查GCC版本：

```
[nix-shell:~]$ gcc -v
Using built-in specs.
COLLECT_GCC=/nix/store/dyj2k6ch35r1ips4vr97md2i0yvl4r5c-gcc-5.4.0/bin/gcc
COLLECT_LTO_WRAPPER=/nix/store/dyj2k6ch35r1ips4vr97md2i0yvl4r5c-gcc-5.4.0/libexec/gcc/x86_64-unknown-linux-gnu/5.4.0/lto-wrapper
Target: x86_64-unknown-linux-gnu
Configured with:
Thread model: posix
gcc version 5.4.0 (GCC)

```

现在，继续并测试代码。完成后，输入 `exit` 返回到控制台。

```
[nix-shell:~]$ exit
exit

```

一旦你从 nix-shell 中退出，你就不能使用 GCC。

这是另一个例子。

```
$ nix-shell -p hello

```

这会构建或下载 GNU Hello 和它的依赖关系，然后将其放入 `hello` 命令所在的 Bash shell 中，所有这些都不会影响你的正常环境：

```
[nix-shell:~]$ hello
Hello, world!

```

输入 `exit` 返回到控制台。

```
[nix-shell:~]$ exit

```

现在测试你的 `hello` 程序是否可用。

```
$ hello
hello: command not found

```

有关 Nix 包管理器的更多详细信息，请参阅以下指南。

希望本篇对你有帮助！还会有更好的东西。敬请关注！！

干杯!

---

via: <https://www.ostechnix.com/how-to-test-a-package-without-installing-it-in-linux/>

作者：[SK](https://www.ostechnix.com/author/sk/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用 OpenCV 进行高动态范围（HDR）成像

# 使用 OpenCV 进行高动态范围（HDR）成像

在本教程中，我们将学习如何使用由不同曝光设置拍摄的多张图像创建<ruby> 高动态范围 <rt>  High Dynamic Range </rt></ruby>（HDR）图像。 我们将以 C++ 和 Python 两种形式分享代码。

### 什么是高动态范围成像？

大多数数码相机和显示器都是按照 24 位矩阵捕获或者显示彩色图像。 每个颜色通道有 8 位，因此每个通道的像素值在 0-255 范围内。 换句话说，普通的相机或者显示器的动态范围是有限的。

但是，我们周围世界动态范围极大。 在车库内关灯就会变黑，直接看着太阳就会变得非常亮。 即使不考虑这些极端，在日常情况下，8 位的通道勉强可以捕捉到现场场景。 因此，相机会尝试去评估光照并且自动设置曝光，这样图像的最关注区域就会有良好的动态范围，并且太暗和太亮的部分会被相应截取为 0 和 255。

在下图中，左侧的图像是正常曝光的图像。 请注意，由于相机决定使用拍摄主体（我的儿子）的设置，所以背景中的天空已经完全流失了，但是明亮的天空也因此被刷掉了。 右侧的图像是由 iPhone 生成的HDR图像。

[](http://www.learnopencv.com/wp-content/uploads/2017/09/high-dynamic-range-hdr.jpg)

iPhone 是如何拍摄 HDR 图像的呢？ 它实际上采用三种不同的曝光度拍摄了 3 张图像，3 张图像拍摄非常迅速，在 3 张图像之间几乎没有产生位移。然后组合三幅图像来产生 HDR 图像。 我们将在下一节看到一些细节。

>
> 将在不同曝光设置下获取的相同场景的不同图像组合的过程称为高动态范围（HDR）成像。
>
>
>

### 高动态范围（HDR）成像是如何工作的？

在本节中，我们来看下使用 OpenCV 创建 HDR 图像的步骤。

>
> 要想轻松学习本教程，请点击[此处](http://www.learnopencv.com/wp-content/uploads/2017/10/hdr.zip)[下载](http://www.learnopencv.com/wp-content/uploads/2017/10/hdr.zip) C++ 和 Python 代码还有图像。 如果您有兴趣了解更多关于人工智能，计算机视觉和机器学习的信息，请[订阅](https://bigvisionllc.leadpages.net/leadbox/143948b73f72a2%3A173c9390c346dc/5649050225344512/)我们的电子杂志。
>
>
>

### 第 1 步：捕获不同曝光度的多张图像

当我们使用相机拍照时，每个通道只有 8 位来表示场景的动态范围（亮度范围）。 但是，通过改变快门速度，我们可以在不同的曝光条件下拍摄多个场景图像。 大多数单反相机（SLR）有一个功能称为<ruby> 自动包围式曝光 <rt>  Auto Exposure Bracketing </rt></ruby>（AEB），只需按一下按钮，我们就可以在不同的曝光下拍摄多张照片。 如果你正在使用 iPhone，你可以使用这个[自动包围式 HDR 应用程序](https://itunes.apple.com/us/app/autobracket-hdr/id923626339?mt=8&amp;ign-mpt=uo%3D8)，如果你是一个 Android 用户，你可以尝试一个[更好的相机应用程序](https://play.google.com/store/apps/details?id=com.almalence.opencam&amp;hl=en)。

场景没有变化时，在相机上使用自动包围式曝光或在手机上使用自动包围式应用程序，我们可以一张接一张地快速拍摄多张照片。 当我们在 iPhone 中使用 HDR 模式时，会拍摄三张照片。

1. 曝光不足的图像：该图像比正确曝光的图像更暗。 目标是捕捉非常明亮的图像部分。
2. 正确曝光的图像：这是相机将根据其估计的照明拍摄的常规图像。
3. 曝光过度的图像：该图像比正确曝光的图像更亮。 目标是拍摄非常黑暗的图像部分。

但是，如果场景的动态范围很大，我们可以拍摄三张以上的图片来合成 HDR 图像。 在本教程中，我们将使用曝光时间为1/30 秒，0.25 秒，2.5 秒和 15 秒的 4 张图像。 缩略图如下所示。

[](http://www.learnopencv.com/wp-content/uploads/2017/10/hdr-image-sequence.jpg)

单反相机或手机的曝光时间和其他设置的信息通常存储在 JPEG 文件的 EXIF 元数据中。 查看此[链接](https://www.howtogeek.com/289712/how-to-see-an-images-exif-data-in-windows-and-macos)可在 Windows 和 Mac 中查看存储在 JPEG 文件中的 EXIF 元数据。 或者，您可以使用我最喜欢的名为 [EXIFTOOL](https://www.sno.phy.queensu.ca/%7Ephil/exiftool) 的查看 EXIF 的命令行工具。

我们先从读取分配到不同曝光时间的图像开始。

**C++**

```
void readImagesAndTimes(vector<Mat> &images, vector<float> &times)
{

  int numImages = 4;

  // 曝光时间列表
  static const float timesArray[] = {1/30.0f,0.25,2.5,15.0};
  times.assign(timesArray, timesArray + numImages);

  // 图像文件名称列表
  static const char* filenames[] = {"img_0.033.jpg", "img_0.25.jpg", "img_2.5.jpg", "img_15.jpg"};
  for(int i=0; i < numImages; i++)
  {
    Mat im = imread(filenames[i]);
    images.push_back(im);
  }

}

```

**Python**

```
def readImagesAndTimes():
  # 曝光时间列表
  times = np.array([ 1/30.0, 0.25, 2.5, 15.0 ], dtype=np.float32)

  # 图像文件名称列表
  filenames = ["img_0.033.jpg", "img_0.25.jpg", "img_2.5.jpg", "img_15.jpg"]
  images = []
  for filename in filenames:
    im = cv2.imread(filename)
    images.append(im)

  return images, times

```

### 第 2 步：对齐图像

合成 HDR 图像时使用的图像如果未对齐可能会导致严重的伪影。 在下图中，左侧的图像是使用未对齐的图像组成的 HDR 图像，右侧的图像是使用对齐的图像的图像。 通过放大图像的一部分（使用红色圆圈显示的）我们会在左侧图像中看到严重的鬼影。

[](http://www.learnopencv.com/wp-content/uploads/2017/10/aligned-unaligned-hdr-comparison.jpg)

在拍摄照片制作 HDR 图像时，专业摄影师自然是将相机安装在三脚架上。 他们还使用称为[镜像锁定](https://www.slrlounge.com/workshop/using-mirror-up-mode-mirror-lockup)功能来减少额外的振动。 即使如此，图像可能仍然没有完美对齐，因为没有办法保证无振动的环境。 使用手持相机或手机拍摄图像时，对齐问题会变得更糟。

幸运的是，OpenCV 提供了一种简单的方法，使用 `AlignMTB` 对齐这些图像。 该算法将所有图像转换为<ruby> 中值阈值位图 <rt>  median threshold bitmaps </rt></ruby>（MTB）。 图像的 MTB 生成方式为将比中值亮度的更亮的分配为 1，其余为 0。 MTB 不随曝光时间的改变而改变。 因此不需要我们指定曝光时间就可以对齐 MTB。

基于 MTB 的对齐方式的代码如下。

**C++**

```
// 对齐输入图像
Ptr<AlignMTB> alignMTB = createAlignMTB();
alignMTB->process(images, images);

```

**Python**

```
# 对齐输入图像
alignMTB = cv2.createAlignMTB()
alignMTB.process(images, images)

```

### 第 3 步：提取相机响应函数

典型相机的响应与场景亮度不成线性关系。 那是什么意思呢？ 假设有两个物体由同一个相机拍摄，在现实世界中其中一个物体是另一个物体亮度的两倍。 当您测量照片中两个物体的像素亮度时，较亮物体的像素值将不会是较暗物体的两倍。 在不估计<ruby> 相机响应函数 <rt>  Camera Response Function </rt></ruby>（CRF）的情况下，我们将无法将图像合并到一个HDR图像中。

将多个曝光图像合并为 HDR 图像意味着什么？

只考虑图像的某个位置 `(x,y)` 一个像素。 如果 CRF 是线性的，则像素值将直接与曝光时间成比例，除非像素在特定图像中太暗（即接近 0）或太亮（即接近 255）。 我们可以过滤出这些不好的像素（太暗或太亮），并且将像素值除以曝光时间来估计像素的亮度，然后在像素不差的（太暗或太亮）所有图像上对亮度值取平均。我们可以对所有像素进行这样的处理，并通过对“好”像素进行平均来获得所有像素的单张图像。

但是 CRF 不是线性的， 我们需要评估 CRF 把图像强度变成线性，然后才能合并或者平均它们。

好消息是，如果我们知道每个图像的曝光时间，则可以从图像估计 CRF。 与计算机视觉中的许多问题一样，找到 CRF 的问题本质是一个最优解问题，其目标是使由数据项和平滑项组成的目标函数最小化。 这些问题通常会降维到线性最小二乘问题，这些问题可以使用<ruby> 奇异值分解 <rt>  Singular Value Decomposition </rt></ruby>（SVD）来解决，奇异值分解是所有线性代数包的一部分。 CRF 提取算法的细节在[从照片提取高动态范围辐射图](http://www.pauldebevec.com/Research/HDR/debevec-siggraph97.pdf)这篇论文中可以找到。

使用 OpenCV 的 `CalibrateDebevec` 或者 `CalibrateRobertson` 就可以用 2 行代码找到 CRF。本篇教程中我们使用 `CalibrateDebevec`

**C++**

```
// 获取图像响应函数 (CRF)
Mat responseDebevec;
Ptr<CalibrateDebevec> calibrateDebevec = createCalibrateDebevec();
calibrateDebevec->process(images, responseDebevec, times);

```

**Python**

```
# 获取图像响应函数 (CRF)
calibrateDebevec = cv2.createCalibrateDebevec()
responseDebevec = calibrateDebevec.process(images, times)

```

下图显示了使用红绿蓝通道的图像提取的 CRF。

[](http://www.learnopencv.com/wp-content/uploads/2017/10/camera-response-function.jpg)

### 第 4 步：合并图像

一旦 CRF 评估结束，我们可以使用 `MergeDebevec` 将曝光图像合并成一个HDR图像。 C++ 和 Python 代码如下所示。

**C++**

```
// 将图像合并为HDR线性图像
Mat hdrDebevec;
Ptr<MergeDebevec> mergeDebevec = createMergeDebevec();
mergeDebevec->process(images, hdrDebevec, times, responseDebevec);
// 保存图像
imwrite("hdrDebevec.hdr", hdrDebevec);

```

**Python**

```
# 将图像合并为HDR线性图像
mergeDebevec = cv2.createMergeDebevec()
hdrDebevec = mergeDebevec.process(images, times, responseDebevec)
# 保存图像
cv2.imwrite("hdrDebevec.hdr", hdrDebevec)

```

上面保存的 HDR 图像可以在 Photoshop 中加载并进行色调映射。示例图像如下所示。

[](http://www.learnopencv.com/wp-content/uploads/2017/10/hdr-Photoshop-Tonemapping.jpg)

*HDR Photoshop 色调映射*

### 第 5 步：色调映射

现在我们已经将我们的曝光图像合并到一个 HDR 图像中。 你能猜出这个图像的最小和最大像素值吗？ 对于黑色条件，最小值显然为 0。 理论最大值是什么？ 无限大！ 在实践中，不同情况下的最大值是不同的。 如果场景包含非常明亮的光源，那么最大值就会非常大。

尽管我们已经使用多个图像恢复了相对亮度信息，但是我们现在又面临了新的挑战：将这些信息保存为 24 位图像用于显示。

>
> 将高动态范围（HDR）图像转换为 8 位单通道图像的过程称为色调映射。这个过程的同时还需要保留尽可能多的细节。
>
>
>

有几种色调映射算法。 OpenCV 实现了其中的四个。 要记住的是没有一个绝对正确的方法来做色调映射。 通常，我们希望在色调映射图像中看到比任何一个曝光图像更多的细节。 有时色调映射的目标是产生逼真的图像，而且往往是产生超现实图像的目标。 在 OpenCV 中实现的算法倾向于产生现实的并不那么生动的结果。

我们来看看各种选项。 以下列出了不同色调映射算法的一些常见参数。

1. <ruby> 伽马 <rt>  gamma </rt></ruby>：该参数通过应用伽马校正来压缩动态范围。 当伽马等于 1 时，不应用修正。 小于 1 的伽玛会使图像变暗，而大于 1 的伽马会使图像变亮。
2. <ruby> 饱和度 <rt>  saturation </rt></ruby>：该参数用于增加或减少饱和度。 饱和度高时，色彩更丰富，更浓。 饱和度值接近零，使颜色逐渐消失为灰度。
3. <ruby> 对比度 <rt>  contrast </rt></ruby>：控制输出图像的对比度（即 `log(maxPixelValue/minPixelValue)`）。

让我们来探索 OpenCV 中可用的四种色调映射算法。

#### Drago 色调映射

Drago 色调映射的参数如下所示：

```
createTonemapDrago
(
float   gamma = 1.0f,
float   saturation = 1.0f,
float   bias = 0.85f
)

```

这里，`bias` 是 `[0, 1]` 范围内偏差函数的值。 从 0.7 到 0.9 的值通常效果较好。 默认值是 0.85。 有关更多技术细节，请参阅这篇[论文](http://resources.mpi-inf.mpg.de/tmo/logmap/logmap.pdf)。

C++ 和 Python 代码如下所示。 参数是通过反复试验获得的。 最后的结果乘以 3 只是因为它给出了最令人满意的结果。

**C++**

```
// 使用Drago色调映射算法获得24位彩色图像
Mat ldrDrago;
Ptr<TonemapDrago> tonemapDrago = createTonemapDrago(1.0, 0.7);
tonemapDrago->process(hdrDebevec, ldrDrago);
ldrDrago = 3 * ldrDrago;
imwrite("ldr-Drago.jpg", ldrDrago * 255);

```

**Python**

```
# 使用Drago色调映射算法获得24位彩色图像
tonemapDrago = cv2.createTonemapDrago(1.0, 0.7)
ldrDrago = tonemapDrago.process(hdrDebevec)
ldrDrago = 3 * ldrDrago
cv2.imwrite("ldr-Drago.jpg", ldrDrago * 255)

```

结果如下：

[](http://www.learnopencv.com/wp-content/uploads/2017/10/hdr-Drago.jpg)

*使用Drago算法的HDR色调映射*

#### Durand 色调映射

Durand 色调映射的参数如下所示：

```
createTonemapDurand
(
  float     gamma = 1.0f,
  float     contrast = 4.0f,
  float     saturation = 1.0f,
  float     sigma_space = 2.0f,
  float     sigma_color = 2.0f
);

```

该算法基于将图像分解为基础层和细节层。 使用称为双边滤波器的边缘保留滤波器来获得基本层。 `sigma_space` 和`sigma_color` 是双边滤波器的参数，分别控制空间域和彩色域中的平滑量。

有关更多详细信息，请查看这篇[论文](https://people.csail.mit.edu/fredo/PUBLI/Siggraph2002/DurandBilateral.pdf)。

**C++**

```
// 使用Durand色调映射算法获得24位彩色图像
Mat ldrDurand;
Ptr<TonemapDurand> tonemapDurand = createTonemapDurand(1.5,4,1.0,1,1);
tonemapDurand->process(hdrDebevec, ldrDurand);
ldrDurand = 3 * ldrDurand;
imwrite("ldr-Durand.jpg", ldrDurand * 255);

```

**Python**

```
# 使用Durand色调映射算法获得24位彩色图像
 tonemapDurand = cv2.createTonemapDurand(1.5,4,1.0,1,1)
 ldrDurand = tonemapDurand.process(hdrDebevec)
 ldrDurand = 3 * ldrDurand
 cv2.imwrite("ldr-Durand.jpg", ldrDurand * 255)

```

结果如下：

[](http://www.learnopencv.com/wp-content/uploads/2017/10/hdr-Durand.jpg)

*使用Durand算法的HDR色调映射*

#### Reinhard 色调映射

```

createTonemapReinhard
(
float   gamma = 1.0f,
float   intensity = 0.0f,
float   light_adapt = 1.0f,
float   color_adapt = 0.0f
)

```

`intensity` 参数应在 `[-8, 8]` 范围内。 更高的亮度值会产生更明亮的结果。 `light_adapt` 控制灯光，范围为 `[0, 1]`。 值 1 表示仅基于像素值的自适应，而值 0 表示全局自适应。 中间值可以用于两者的加权组合。 参数 `color_adapt` 控制色彩，范围为 `[0, 1]`。 如果值被设置为 1，则通道被独立处理，如果该值被设置为 0，则每个通道的适应级别相同。中间值可以用于两者的加权组合。

有关更多详细信息，请查看这篇[论文](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.106.8100&amp;rep=rep1&amp;type=pdf)。

**C++**

```
// 使用Reinhard色调映射算法获得24位彩色图像
Mat ldrReinhard;
Ptr<TonemapReinhard> tonemapReinhard = createTonemapReinhard(1.5, 0,0,0);
tonemapReinhard->process(hdrDebevec, ldrReinhard);
imwrite("ldr-Reinhard.jpg", ldrReinhard * 255);

```

**Python**

```
# 使用Reinhard色调映射算法获得24位彩色图像
tonemapReinhard = cv2.createTonemapReinhard(1.5, 0,0,0)
ldrReinhard = tonemapReinhard.process(hdrDebevec)
cv2.imwrite("ldr-Reinhard.jpg", ldrReinhard * 255)

```

结果如下：

[](http://www.learnopencv.com/wp-content/uploads/2017/10/hdr-Reinhard.jpg)

*使用Reinhard算法的HDR色调映射*

#### Mantiuk 色调映射

```
createTonemapMantiuk
(
float   gamma = 1.0f,
float   scale = 0.7f,
float   saturation = 1.0f
)

```

参数 `scale` 是对比度比例因子。 从 0.7 到 0.9 的值通常效果较好

有关更多详细信息，请查看这篇[论文](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.60.4077&amp;rep=rep1&amp;type=pdf)。

**C++**

```
// 使用Mantiuk色调映射算法获得24位彩色图像
Mat ldrMantiuk;
Ptr<TonemapMantiuk> tonemapMantiuk = createTonemapMantiuk(2.2,0.85, 1.2);
tonemapMantiuk->process(hdrDebevec, ldrMantiuk);
ldrMantiuk = 3 * ldrMantiuk;
imwrite("ldr-Mantiuk.jpg", ldrMantiuk * 255);

```

**Python**

```
# 使用Mantiuk色调映射算法获得24位彩色图像
tonemapMantiuk = cv2.createTonemapMantiuk(2.2,0.85, 1.2)
ldrMantiuk = tonemapMantiuk.process(hdrDebevec)
ldrMantiuk = 3 * ldrMantiuk
cv2.imwrite("ldr-Mantiuk.jpg", ldrMantiuk * 255)

```

结果如下：

[](http://www.learnopencv.com/wp-content/uploads/2017/10/hdr-Mantiuk.jpg)

*使用Mantiuk算法的HDR色调映射*

### 订阅然后下载代码

如果你喜欢这篇文章，并希望下载本文中使用的代码（C++ 和 Python）和示例图片，请[订阅](https://bigvisionllc.leadpages.net/leadbox/143948b73f72a2%3A173c9390c346dc/5649050225344512/)我们的电子杂志。 您还将获得免费的[计算机视觉资源](https://bigvisionllc.leadpages.net/leadbox/143948b73f72a2%3A173c9390c346dc/5649050225344512/)指南。 在我们的电子杂志中，我们分享了用 C++ 还有 Python 编写的 OpenCV 教程和例子，以及计算机视觉和机器学习的算法和新闻。

[点此订阅](https://bigvisionllc.leadpages.net/leadbox/143948b73f72a2%3A173c9390c346dc/5649050225344512/)

图片致谢

本文中使用的四个曝光图像获得 [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) 许可，并从[维基百科的 HDR 页面](https://en.wikipedia.org/wiki/High-dynamic-range_imaging)下载。 图像由 Kevin McCoy拍摄。

---

作者简介：

我是一位热爱计算机视觉和机器学习的企业家，拥有十多年的实践经验（还有博士学位）。

2007 年，在完成博士学位之后，我和我的顾问 David Kriegman 博士还有 Kevin Barnes 共同创办了 TAAZ 公司。 我们的计算机视觉和机器学习算法的可扩展性和鲁棒性已经经过了试用了我们产品的超过 1 亿的用户的严格测试。

---

via: <http://www.learnopencv.com/high-dynamic-range-hdr-imaging-using-opencv-cpp-python/>

作者：[SATYA MALLICK](http://www.learnopencv.com/about/) 译者：[Flowsnow](https://github.com/Flowsnow) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出


使用 Stratis 从命令行管理 Linux 存储

Summary: 通过从命令行运行它，得到这个易于使用的 Linux 存储工具的主要用途。

为什么2G的硬盘挂载后df显示1TB空间，写满2G文件后还可以继续写入，重启后无法挂载分区，使用VMware虚拟机

来自河北唐山的 Microsoft Edge 18.18362|Windows 10 用户

通过从命令行运行它，得到这个易于使用的 Linux 存储工具的主要用途。

# 使用 Stratis 从命令行管理 Linux 存储

>
> 通过从命令行运行它，得到这个易于使用的 Linux 存储工具的主要用途。
>
>
>

<img src="content_image/album_201806_17_000945iop8quph8z11f1a8.jpg" alt="" title="">

正如本系列的[第一部分](/article-9736-1.html)和[第二部分](/article-9743-1.html)中所讨论的，Stratis 是一个具有与 [ZFS](https://en.wikipedia.org/wiki/ZFS) 和 [Btrfs](https://en.wikipedia.org/wiki/Btrfs) 相似功能的卷管理文件系统。在本文中，我们将介绍如何在命令行上使用 Stratis。

### 安装 Stratis

对于非开发人员，现在尝试 Stratis 最简单的方法是在 [Fedora 28](https://fedoraproject.org/wiki/Releases/28/Schedule) 中。

你可以用以下命令安装 Stratis 守护进程和 Stratis 命令行工具：

```
# dnf install stratis-cli stratisd

```

### 创建一个池

Stratis 有三个概念：blockdevs、池和文件系统。 Blockdevs 是组成池的块设备，例如磁盘或磁盘分区。一旦创建池，就可以从中创建文件系统。

假设你的系统上有一个名为 `vdg` 的块设备，它目前没有被使用或挂载，你可以在它上面创建一个 Stratis 池：

```
# stratis pool create mypool /dev/vdg

```

这假设 `vdg` 是完全清零并且是空的。如果它没有被使用，但有旧数据，则可能需要使用 `pool create` 的 `-force` 选项。如果正在使用，请勿将它用于 Stratis。

如果你想从多个块设备创建一个池，只需在 `pool create` 命令行中列出它们。你也可以稍后使用 `blockdev add-data` 命令添加更多的 blockdevs。请注意，Stratis 要求 blockdevs 的大小至少为 1 GiB。

### 创建文件系统

在你创建了一个名为 `mypool` 的池后，你可以从它创建文件系统：

```
# stratis fs create mypool myfs1

```

从 `mypool` 池创建一个名为 `myfs1` 的文件系统后，可以使用 Stratis 在 `/dev/stratis` 中创建的条目来挂载并使用它：

```
# mkdir myfs1
# mount /dev/stratis/mypool/myfs1 myfs1

```

文件系统现在已被挂载在 `myfs1` 上并准备可以使用。

### 快照

除了创建空文件系统之外，你还可以创建一个文件系统作为现有文件系统的快照：

```
# stratis fs snapshot mypool myfs1 myfs1-experiment

```

这样做后，你可以挂载新的 `myfs1-experiment`，它将初始包含与 `myfs1` 相同的文件内容，但它可能随着文件系统的修改而改变。无论你对 `myfs1-experiment` 所做的任何更改都不会反映到 `myfs1` 中，除非你卸载了 `myfs1` 并将其销毁：

```
# umount myfs1
# stratis fs destroy mypool myfs1

```

然后进行快照以重新创建并重新挂载它：

```
# stratis fs snapshot mypool myfs1-experiment myfs1
# mount /dev/stratis/mypool/myfs1 myfs1

```

### 获取信息

Stratis 可以列出系统中的池：

```
# stratis pool list

```

随着文件系统写入更多数据，你将看到 “Total Physical Used” 值的增加。当这个值接近 “Total Physical Size” 时要小心。我们仍在努力处理这个问题。

列出池中的文件系统：

```
# stratis fs list mypool

```

列出组成池的 blockdevs：

```
# stratis blockdev list mypool

```

目前只提供这些最少的信息，但它们将在未来提供更多信息。

### 摧毁池

当你了解了 Stratis 可以做什么后，要摧毁池，首先确保从它创建的所有文件系统都被卸载并销毁，然后使用 `pool destroy` 命令：

```
# umount myfs1
# umount myfs1-experiment (if you created it)
# stratis fs destroy mypool myfs1
# stratis fs destroy mypool myfs1-experiment
# stratis pool destroy mypool

```

`stratis pool list` 现在应该显示没有池。

就是这些！有关更多信息，请参阅手册页：“man stratis”。

---

via: <https://opensource.com/article/18/5/stratis-storage-linux-command-line>

作者：[Andy Grover](https://opensource.com/users/agrover) 选题：[lujun9972](https://github.com/lujun9972) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出