如何在 Linux 中使用 sFTP 上传或下载文件与文件夹

Summary: sFTP（安全文件传输程序）是一种安全的交互式文件传输程序，其工作方式与 FTP（文件传输协议）类似。 然而，sFTP 比 FTP 更安全；它通过加密 SSH 传输处理所有操作。

scp 完全可以满足日常需求，且非交互

# 如何在 Linux 中使用 sFTP 上传或下载文件与文件夹

[sFTP（安全文件传输程序）](http://www.tecmint.com/sftp-command-examples/)是一种安全的交互式文件传输程序，其工作方式与 FTP（文件传输协议）类似。 然而，sFTP 比 FTP 更安全；它通过加密 SSH 传输处理所有操作。

它可以配置使用几个有用的 SSH 功能，如[公钥认证](/article-6901-1.html)和压缩。 它连接并登录到指定的远程机器，然后切换到交互式命令模式，在该模式下用户可以执行各种命令。

在本文中，我们将向你展示如何使用 sFTP 上传/下载整个目录（包括其子目录和子文件）。

### 如何在 Linux 中使用 sFTP 传输文件/文件夹

默认情况下，SFTP 协议采用和 SSH 传输协议一样的方式建立到远程服务器的安全连接。虽然，用户验证使用类似于 SSH 默认设置的密码方式，但是，建议[创建和使用 SSH 无密码登录](/article-6901-1.html)，以简化和更安全地连接到远程主机。

要连接到远程 sftp 服务器，如下建立一个安全 SSH 连接并创建 SFTP 会话：

```
$ sftp tecmint@192.168.56.10

```

登录到远程主机后，你可以如下运行交互式的 sFTP 命令：

```
sftp> ls            #list directory
sftp> pwd           #print working directory on remote host
sftp> lpwd          #print working directory on local host
sftp> mkdir uploads     #create a new directory

```

* Linux 主机上运行 sFTP 命令\*

#### 如何使用 sFTP 上传文件夹

要将整个目录上传到远程 Linux 主机中，请使用 `put` 命令。但是，如果目录名称不存在于远程主机上的工作目录中，你将收到错误，如下面的屏幕截图所示。

因此，首先在远程主机上创建一个具有相同名称的目录，然后从本地主机上传它，`-r` 参数允许拷贝子目录和子文件：

```
sftp> put -r  Tecmint.com-articles
sftp> mkdir Tecmint.com-articles
sftp> put -r Tecmint.com-articles

```

*使用 sFTP 上传文件夹*

要保留修改时间、访问时间以及被传输的文件的模式，请使用 `-p` 标志。

```
sftp> put -pr Tecmint.com-articles

```

#### 如何使用 sFTP 下载文件夹

要从远程 Linux 主机下载整个 fstools-0.0 文件夹到本机中，如下所示使用 get 命令带上 `-r` 标志：

```
sftp> get -r fstools-0.0

```

*使用 sFTP 下载目录*

如果文件夹已经下载完成了，接着查看本机的工作目录。

要退出 sFTP shell，输入：

```
sftp> bye
或者
sftp> exit

```

此外，阅读这篇 [sFTP 命令和使用技巧](http://www.tecmint.com/sftp-command-examples/)。

请注意，为了防止用户访问远程主机上的整个文件系统，出于安全原因，你可以使用 chroot Jail [将 sFTP 用户限制到其主目录](http://www.tecmint.com/restrict-sftp-user-home-directories-using-chroot/)中。

就是这样了！在本文中，我们向你展示了如何使用 sFTP 上传/下载整个目录。使用下面的评论栏向我们提供你对本文/主题的看法。

---

作者简介：

Aaron Kili 是 Linux 和 F.O.S.S 爱好者，将来的 Linux 系统管理员和网络开发人员，目前是 TecMint 的内容创作者，他喜欢用电脑工作，并坚信分享知识。

---

via: <http://www.tecmint.com/sftp-upload-download-directory-in-linux/>

作者：[Aaron Kili](http://www.tecmint.com/author/aaronkili/) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

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


在树莓派 3 上运行 openSUSE：简单几步搭建一个实用系统

Summary: 在树莓派 3上部署 openSUSE 系统不是很复杂，不过这儿有一些小技巧教你更好地完成这个过程。

Debian Gnu/Linux 的树梅派有没有全软件包的ISO。方便建立本地源用！！！！

树莓派2是不是不能啊？

应该也可以的

排版出问题了吗？<br />
怎么现在的文章都这么不“漂亮”！

排版出问题了吗？<br />
怎么现在的文章都这么不“漂亮”！

说说你的具体看法？

安装可以正常使用~不过， 读取SD卡的指示灯不亮。

# 在树莓派 3 上运行 openSUSE：简单几步搭建一个实用系统

本文由 SUSE 文档团队的技术作者 Dmitri Popov 撰写。

>
> 在[树莓派 3](https://www.raspberrypi.org/)上部署 [openSUSE](https://www.opensuse.org/) 系统不是很复杂，不过这儿有一些小技巧教你更好地完成这个过程。
>
>
>

首先，你将会有一些版本可供选择。如果你打算使用树莓派 3 作为一个普通主机，那么带有图形界面的 openSUSE 将是你最好的选择。有几种不同的图形环境可供选择：[X11](https://www.x.org/wiki/)、[Enlightenment](https://www.enlightenment.org/)、[Xfce](https://www.xfce.org/) 或是 [LXQT](http://lxqt.org/)。openSUSE 还有一个 JeOS 版本能够提供最基础的系统，可以把树莓派 3 作为一个无显示器的服务器使用。更好的选择还有 openSUSE 的 [Leap](https://www.opensuse.org/#Leap) 或 [Tumbleweed](https://www.opensuse.org/#Tumbleweed) 版本。

首先你需要从 <https://en.opensuse.org/HCL:Raspberry_Pi3> 下载所需的 openSUSE 镜像，然后制作一张可启动的 microSD 卡。虽然可以使用命令行工具将下载好的镜像写入 microSD 卡，但 [Etcher](https://etcher.io/) 可以使这个过程更轻松安全。从该项目网站上获取该程序，解压下载的 .zip 文件，并使用以下命令把得到的 .AppImage 文件设置为可执行：

```
chmod +x Etcher-x.x.x-linux-x64.AppImage

```

将 microSD 卡插入电脑，双击运行 Etcher 软件，选择下载好的 .raw.xz 镜像文件，点击 **Flash!** 按钮。然后将显示器和键盘连接到树莓派 3，插入 microSD 卡，启动树莓派。第一次启动时，openSUSE 会自动扩展文件系统以充分利用 microSD 卡上的剩余空间。这时你将看到以下信息：

```
GPT data structures destroyed! You may now partition the disk using fdisk or other utilities
GPT 数据结构被破坏！您需要使用 fdisk 或其它工具对磁盘分区。

```

不用担心，稍等两分钟，openSUSE 将继续正常启动。当看到提示时，输入默认用户名 `root` 和默认密码 `linux` 登录系统。

如果你选择在树莓派 3 上部署 JeOS 版本，第一次启动时你不会看到屏幕上有任何输出。也就是说，屏幕会一直保持空白，直到系统完成对文件系统的扩展。你可以通过配置内核参数来显示输出，不过没有必要做这麻烦事。只需稍等片刻，你就能看到命令行提示。

由于 openSUSE 已经启用并且配置了 SSH 服务，所以启动树莓派时你可以不用显示器。这样的话，你就需要使用网卡接口将树莓派连接到网络中。给树莓派足够的时间来启动和扩展系统后，你就能够从同一网络中的其他主机，使用 `ssh root@linux.local` 命令，通过 SSH 服务连接树莓派。

默认情况下你将以 `root` 用户登录系统，所以创建一个普通用户是个不错的主意。你可以使用 YaST 配置工具轻松完成这件事。运行 `yast2` 命令，选择 <ruby>   安全与用户  <rt>  Security and Users </rt></ruby> -> **<ruby> 用户与用户组管理 <rt>  User and Group Management </rt></ruby>** 选项，就可以创建新用户了。你还可以选择 **<ruby> 系统 <rt>  System </rt></ruby>** -> **<ruby> 在线升级 <rt>  Online Update </rt></ruby>**选项来更新系统。完成之后，退出 YaST ，重启树莓派，然后使用新创建的用户登录系统。

一切搞定，不过还有一个重要的系统组件不能正常工作，那就是无线接口。当然，这个问题也可以轻松解决。首先使用以下命令安装 nano 文本编辑器：

```
sudo zypper in nano

```

然后运行以下命令修改 `raspberrypi_modules.conf` 文件：

```
sudo nano/etc/dracut.conf.d/raspberrypi_modules.conf

```

删除文件第一行的 `sdhci_iproc` ，再取消最后一行的注释。运行以下命令保存修改：

```
mkinitrd -f

```

最后，重启树莓派。

再次运行 YaST ，在**<ruby> 系统 <rt>  System </rt></ruby>** -> **<ruby> 网络设置 <rt>  Network Settings </rt></ruby>**区域，你应该能在网络接口列表中看到 `BCM43430 WLAN Card` 项。选择这一项，点击 **<ruby> 编辑 <rt>  Edit </rt></ruby>** 按钮。开启**<ruby> 动态地址分配 <rt>  Dynamic Address DHCP </rt></ruby>**选项，点击**下一步**，选择你想要连接的无线网络，配置所需的连接设置。点击**下一步**和**确定**保存设置。重启树莓派，它应该就能连接上特定的 Wi-Fi 网络了。

至此，你就完成了树莓派上的系统部署。

---

via: <https://www.suse.com/communities/blog/opensuse-raspberry-pi-3-zero-functional-system-easy-steps/>

作者：[chabowski](https://www.suse.com/communities/blog/author/chabowski/) 译者：[Cathon](https://github.com/Cathon) 校对：[jasminepeng](https://github.com/jasminepeng)

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


长期维护嵌入式 Linux 内核变得容易

嵌入式设备根本经不起更新，一更新刷系统失败，产品要寄回，成本大增。一般发货了，就不再提供更新了

的确，系统和应用的代码出现小改动，就有可能发生莫名其妙的问题

# 长期维护嵌入式 Linux 内核变得容易

<img src="content_image/album_201703_02_011322tsh9l27fnlezqgzn.png" alt="Jan Lübbe " title="Jan Lübbe ">

*Pengutronix 内核黑客 Jan Lübbe 总结了嵌入式 Linux 中正在不断增长的安全威胁，并在这次欧洲嵌入式 Linux 会议上概述了一个计划，以保持长期设备的安全和功能完整。* [Linux 基金会](https://www.linux.com/licenses/category/linux-foundation)

安全漏洞只发生在 Windows 上的好日子正在快速过去。恶意软件黑客和拒绝服务老手们正在越来越多地瞄准过时的嵌入式 Linux 设备，因此在 10 月的[<ruby> 欧洲嵌入式 Linux 会议 <rt>  Embedded Linux Conference Europe </rt></ruby>](http://events.linuxfoundation.org/events/archive/2016/embedded-linux-conference-europe)（ELCE）上的几个演讲的主题就与修复 Linux 安全漏洞相关。

最值得去听的讲演之一是 [Pengutronix](http://www.pengutronix.de/index_en.html) 内核黑客 Jan Lübbe 的《长期维护或管理（或免管理）嵌入式系统 10 年以上》。在总结嵌入式 Linux 中不断增长的安全威胁后，Lübbe 制定了一项计划，以确保长期设备的安全和功能完整。 Lübbe 说：“我们需要迁移到更新、更稳定的内核，并进行持续维护以修复关键漏洞。我们需要做上游更新和自动化流程，并建立一个可持续的工作流程。我们没有理由让系统中仍留有过时的软件。”

随着 Linux 设备变得越来越老，传统的生命周期过程已经不再适用。 Lübbe 说：“通常，你会从 SoC 供应商或主线上获取内核、构建系统，并添加到用户空间。你可以定制和添加程序，并做一些测试。但是，在此之后有 15 年的维护阶段，你最好期望平台不会发生变化、不会想要添加新的功能、不需要实施管理调整。”

所有这些变化，越来越多地导致你的系统暴露出新的错误，并需要大量更新以才能与上游软件保持同步。 Lübbe 说：“在内核中发生导致问题的错误并不总是无意的”。对于去年在 Allwinner 内核中发现的[后门](http://arstechnica.com/security/2016/05/chinese-arm-vendor-left-developer-backdoor-in-kernel-for-android-pi-devices/)，他又补充说：“这些供应商的内核从来不会执行主线内核社区的审查流程”。

Lübbe 继续说：“你不能认为你的供应商一直没问题。也许只有一两个工程师查看过后门代码这块。如果补丁发布在 Linux 内核邮件列表上，就不会有这种事，因为总会有人注意到。硬件供应商不关心安全或维护，也许你会在一两年后得到更新，但是即使这样，他们从一个固定版本开始开发，到他们发布稳定的版本通常需要几年的时间。如果你在这个基础上再开始开发，可能又过了半年，这就更过时了。”

越来越多的嵌入式开发人员在<ruby> 长期稳定 <rt>  Long Term Stable </rt></ruby>（LTS）内核上构建长期产品。但这并不意味着没事了。Lübbe 说：“一个产品发布后，人们经常不再遵循稳定的发行链，也不再应用安全补丁。这样你会得到两个最糟糕的结果：过时的内核和没有安全性。你失去了多人测试的好处。”

Lübbe 指出，使用像 Red Hat 这样的面向服务器的发行版的 Pengutronix 客户经常由于快速的定制、需要系统管理员干预的部署和升级系统而遇到问题。

“更新对一些东西有用，特别是在 x86 上，但每个项目基本上是自己建立基础设施来更新到新版本。”

许多开发人员选择把向后移植作为更新长期产品的解决方案。Lübbe 说：“开始时很容易，但是一旦你不处于项目的维护范围，他们就不会告诉你所使用的版本是否受到一个 bug 的影响，因此很难判断一个修复是否相关。于是你不停打补丁和更新，而 bug 也在不断累积，而这些你必须自己维护，因为其他人不使用这些补丁。使用开源软件的好处就丢失了。”

### 跟随上游项目

Lübbe 认为，最好的解决方案是跟踪由上游项目维护的版本。“我们主要关注基于主线内核的开发，所以我们在产品和主流内核及其他上游项目之间尽可能没有差别。长期系统在主线内核上得到很好的支持。大多数不使用 3D 图形的系统只需要很少的补丁。较新的内核版本还有很多[新的强化功能](https://www.linux.com/news/event/ELCE/2017hardening-kernel-protect-against-attackers)，这些可以减少漏洞的影响。

跟随主线发展对许多开发人员来说似乎令人畏惧，但是如果从一开始就这样，然后坚持下去，就会相对容易一些，Lübbe 说：“你需要为系统上做的一切制定流程。你总需要知道什么软件正在运行，这在使用良好的构建系统时会更容易。每个软件版本应定义完整的系统，以便你可以更新相关的一切。如果你不知道那里有什么，你就不能解决它。你也需要一个自动测试和自动部署更新。”

为了“减少更新周期”，Lübbe 建议在开始开发时使用最新的 Linux 内核，并且在进入测试时才转到稳定的内核。之后，他建议每年将系统中的所有软件（包括内核、构建系统、用户空间、glibc 和组件（如 OpenSSL））更新为当年上游项目支持的版本。

Lübbe 说：“得到更新并不意味着你需要部署。如果没有看到安全漏洞，你可以把补丁放在一边，需要时它再用就行。”

最后，Lübbe 建议每个月查看发布公告，并且每周检查 CVE 和主线列表上的安全公告。你只需要问自己“该安全公告是否影响到了你”。他补充说：“如果你的内核足够新，就没有太多的工作。你不会希望通过在新闻中看到你的设备才获得有关你的产品的反馈。”

---

via: <https://www.linux.com/news/event/ELCE/2017/long-term-embedded-linux-maintenance-made-easier>

作者：[ERIC BROWN](https://www.linux.com/users/ericstephenbrown) 译者：[geekpi](https://github.com/geekpi) 校对：[jasminepeng](https://github.com/jasminepeng)

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


LXD 2.0 系列（八）：LXD 中的 LXD

# LXD 2.0 系列（八）：LXD 中的 LXD

这是 LXD 2.0 系列介绍文章的第八篇。

1. [LXD 入门](/article-7618-1.html)
2. [安装与配置](/article-7687-1.html)
3. [你的第一个 LXD 容器](/article-7706-1.html)
4. [资源控制](/article-8072-1.html)
5. [镜像管理](/article-8107-1.html)
6. [远程主机及容器迁移](/article-8169-1.html)
7. [LXD 中的 Docker](/article-8235-1.html)
8. [LXD 中的 LXD](/article-8257-1.html)
9. [实时迁移](/article-8263-1.html)
10. [LXD 和 Juju](/article-8273-1.html)
11. [LXD 和 OpenStack](/article-8274-1.html)
12. [调试，及给 LXD 做贡献](/article-8282-1.html)

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

### 介绍

在上一篇文章中，我介绍了如何[在 LXD 中运行 Docker](/article-8235-1.html)，这是一个访问由 Docker 提供的应用程序组合的很好方式，同时 Docker 还运行在 LXD 提供的安全环境中。

我提到的一个情况是为你的用户提供一个 LXD 容器，然后让他们使用他们的容器来运行 Docker。那么，如果他们自己想要在其容器中使用 LXD 运行其他 Linux 发行版，或者甚至允许另一组人来访问运行在他们的容器中的 Linux 系统呢？

原来 LXD 使得用户运行嵌套容器变得非常简单。

### 嵌套 LXD

最简单的情况可以使用 Ubuntu 16.04 镜像来展示。 Ubuntu 16.04 云镜像预装了 LXD。守护进程本身没有运行，因为它是由套接字激活的，所以它不使用任何资源，直到你真正使用它。

让我们启动一个启用了嵌套的 Ubuntu 16.04 容器：

```
lxc launch ubuntu-daily:16.04 c1 -c security.nesting=true

```

你也可以在一个已有的容器上设置 `security.nesting`：

```
lxc config set <container name> security.nesting true

```

或者对所有的容器使用一个指定的配置文件：

```
lxc profile set <profile name> security.nesting true

```

容器启动后，你可以从容器内部得到一个 shell，配置 LXD 并生成一个容器：

```
stgraber@dakara:~$ lxc launch ubuntu-daily:16.04 c1 -c security.nesting=true
Creating c1
Starting c1

stgraber@dakara:~$ lxc exec c1 bash
root@c1:~# lxd init
Name of the storage backend to use (dir or zfs): dir

We detected that you are running inside an unprivileged container.
This means that unless you manually configured your host otherwise,
you will not have enough uid and gid to allocate to your containers.

LXD can re-use your container's own allocation to avoid the problem.
Doing so makes your nested containers slightly less safe as they could
in theory attack their parent container and gain more privileges than
they otherwise would.

Would you like to have your containers share their parent's allocation (yes/no)? yes
Would you like LXD to be available over the network (yes/no)? no
Do you want to configure the LXD bridge (yes/no)? yes
Warning: Stopping lxd.service, but it can still be activated by:
 lxd.socket
LXD has been successfully configured.

root@c1:~# lxc launch ubuntu:14.04 trusty
Generating a client certificate. This may take a minute...
If this is your first time using LXD, you should also run: sudo lxd init

Creating trusty
Retrieving image: 100%
Starting trusty

root@c1:~# lxc list
+--------+---------+-----------------------+----------------------------------------------+------------+-----------+
|  NAME  |  STATE  |         IPV4          |                     IPV6                     |    TYPE    | SNAPSHOTS |
+--------+---------+-----------------------+----------------------------------------------+------------+-----------+
| trusty | RUNNING | 10.153.141.124 (eth0) | fd7:f15d:d1d6:da14:216:3eff:fef1:4002 (eth0) | PERSISTENT | 0         |
+--------+---------+-----------------------+----------------------------------------------+------------+-----------+
root@c1:~#

```

就是这样简单。

### 在线演示服务器

因为这篇文章很短，我想我会花一点时间谈论我们运行中的[演示服务器](https://linuxcontainers.org/lxd/try-it/)。我们今天早些时候刚刚达到了 10000 个会话！

这个服务器基本上只是一个运行在一个相当强大的虚拟机上的正常的 LXD，一个小型的守护进程实现了我们的网站所使用的 REST API。

当你接受服务条款时，将为你创建一个新的 LXD 容器，并启用 `security.nesting`，如上所述。接着你就像使用 `lxc exec` 时一样连接到了那个容器，除了我们使用 websockets 和 javascript 来做这些。

你在此环境中创建的容器都是嵌套的 LXD 容器。如果你想，你可以进一步地嵌套。

我们全范围地使用了 [LXD 资源限制](https://www.stgraber.org/2016/03/26/lxd-2-0-resource-control-412/)，以防止一个用户的行为影响其他用户，并仔细监控服务器的任何滥用迹象。

如果你想运行自己的类似的服务器，你可以获取我们的网站和守护进程的代码：

```
git clone https://github.com/lxc/linuxcontainers.org
git clone https://github.com/lxc/lxd-demo-server

```

### 额外信息

LXD 的主站在： <https://linuxcontainers.org/lxd>

LXD 的 GitHub 仓库： <https://github.com/lxc/lxd>

LXD 的邮件列表： <https://lists.linuxcontainers.org>

LXD 的 IRC 频道： #lxcontainers on irc.freenode.net

---

作者简介：我是 Stéphane Graber。我是 LXC 和 LXD 项目的领导者，目前在加拿大魁北克蒙特利尔的家所在的Canonical 有限公司担任 LXD 的技术主管。

---

via: <https://www.stgraber.org/2016/04/14/lxd-2-0-lxd-in-lxd-812/>

作者：[Stéphane Graber](https://www.stgraber.org/author/stgraber/) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

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


# Samba 系列（四）：在 Windows 下管理 Samba4 AD 域管制器 DNS 和组策略

接着前一篇教程写的关于[使用 Windows 10 的 RSAT 工具来管理 Samba4 活动目录架构](/article-8097-1.html)，在这篇文章中我们将学习如何使用微软 DNS 管理器远程管理我们的 Samba AD 域控制器的 DNS 服务器，如何创建 DNS 记录，如何创建反向查找区域以及如何通过组策略管理工具来创建域策略。

#### 要求

1、 [在 Ubuntu 16.04 系统上使用 Samba4 软件来创建活动目录架构（一）](/article-8065-1.html)

2、 [在 Linux 命令行下管理 Samba4 AD 架构（二）](/article-8070-1.html)

3、 [使用 Windows 10 的 RSAT 工具来管理 Samba4 活动目录架构 （三）](/article-8097-1.html)

### 第 1 步：管理 Samba DNS 服务器

Samba4 AD DC 使用内部的 DNS 解析器模块，该模块在初始化域提供的过程中创建（如果 BIND9 DLZ 模块未指定使用的情况下）。

Samba4 内部的 DNS 模块支持 AD 域控制器所必须的基本功能。有两种方式来管理域 DNS 服务器，直接在命令行下通过 samba-tool 接口来管理，或者使用已加入域的微软工作站中的 RSAT DNS 管理器远程进行管理。

在这篇文章中，我们使用第二种方式来进行管理，因为这种方式很直观，也不容易出错。

1、要使用 RSAT 工具来管理域控制器上的 DNS 服务器，在 Windows 机器上，打开控制面板 -> 系统和安全 -> 管理工具，然后运行 DNS 管理器工具。

当打开这个工具时，它会询问你将要连接到哪台正在运行的 DNS 服务器。选择“使用下面的计算机”，输入域名（IP 地址或 FQDN 地址都可以使用），勾选“现在连接到指定计算机”，然后单击 OK 按钮以开启 Samba DNS 服务。

*在 Windows 系统上连接 Samba4 DNS 服务器*

2、为了添加一条 DNS 记录（比如我们添加一条指向 LAN 网关的 A 记录），打开 DNS 管理器，找到域正向查找区，在右侧单击右键选择新的主机（A 或 AAAA）。

*在 Windows 下添加一条 DNS 记录*

3、在打开的新主机窗口界面，输入 DNS 服务器的主机名和 IP 地址。 DNS 管理器工具会自动填写完成 FQDN 地址。填写完成后，点击“添加主机”按钮，之后会弹出一个新的窗口提示你 DNS A 记录已经创建完成。

确保仅为你的网络中[已配置静态 IP](http://www.tecmint.com/set-add-static-ip-address-in-linux/)的资源（设备）添加 DNS A 记录。不要为那些从 DHCP 服务器自动获取 IP 地址或者经常变换 IP 地址的主机添加 DNS A 记录。

*在 Windows 系统下配置 Samba 主机*

要更新一条 DNS 记录只需要双击那条记录，然后输入更改即可。要删除一条记录时，只需要在这条记录上单击右键，选择从菜单删除即可。

同样的方式，你也可以为你的域添加其它类型的 DNS 记录，比如说 CNAME 记录（也称为 DNS 别名记录），MX 记录（在邮件服务器上非常有用）或者其它类型的记录（SPE、TXT、SRV 等类型）。

### 第 2 步：创建反向查找区域

默认情况下，Samba4 AD DC 不会自动为你的域添加一个反向查找区域和 PTR 记录，因为这些类型的记录对于域控制器的正常工作来说是无关紧要的。

相反，DNS 反向区和 PTR 记录在一些重要的网络服务中显得非常有用，比如邮件服务，因为这些类型的记录可以用于验证客户端请求服务的身份。

实际上， PTR 记录的功能与标准的 DNS 记录功能相反。客户端知道资源的 IP 地址，然后去查询 DNS 服务器来识别出已注册的 DNS 名字。

4、要创建 Samba AD DC 的反向查找区域，打开 DNS 管理器，在左侧反向查找区域目录上单击右键，然后选择菜单中的新区域。

*创建 DNS 反向查找区域*

5、下一步，单击下一步按钮，然后从区域类型向导中选择主区域（Primary）。

*选择 DNS 区域类型*

6、下一步，在 “AD 区域复制范围”中选择复制到该域里运行在域控制器上的所有的 DNS 服务器，选择 “IPv4 反向查找区域”然后单击下一步继续。

*为 Samba 域控制器选择 DNS 服务器*

*添加反向查找区域名*

7、下一步，在网络ID 框中输入你的 LAN IP 地址，然后单击下一步继续。

在这个区域内添加的所有资源（设备）的 PTR 记录仅能指向 192.168.1.0/24 网络段。如果你想要为一个不在该网段中的服务器创建一个 PTR 记录（比如邮件服务器位于 10.0.0.0/24 这个网段的时候），那么你还得为那个网段创建一个新的反向查找区域。

*添加 DNS 反向查找区域的 IP 地址*

8、在下一个截图中选择“仅允许安全的动态更新”，单击下一步继续，最后单击完成按钮以完成反向查找区域的创建。

*启用安全动态更新*

*新 DNS 区域概览*

9、此时，你已经为你的域环境创建完成了一个有效的 DNS 反向查找区域。为了在这个区域中添加一个 PTR 记录，在右侧右键单击，选择为网络资源创建一个 PTR 记录。

这个时候，我们已经为网关创建了一个指向。为了测试这条记录对于客户端是否添加正确和工作正常，打开命令行提示符执行 `nslookup` 查询资源名，再执行另外一条命令查询 IP 地址。

两个查询都应该为你的 DNS 资源返回正确的结果。

```
nslookup gate.tecmint.lan
nslookup 192.168.1.1
ping gate

```

*添加及查询 PTR 记录*

### 第 3 步：管理域控制策略

10、域控制器最重要的作用就是集中控制系统资源及安全。使用域控制器的域组策略功能很容易实现这些类型的任务。

遗憾的是，在 Samba 域控制器上唯一用来编辑或管理组策略的方法是通过微软的 RSAT GPM 工具。

在下面的实例中，我们将看到通过组策略来实现在 Samba 域环境中为域用户创建一种交互式的登录提示是多么的简单。

要访问组策略控制台，打开控制面板 -> 系统和安全 -> 管理工具，然后打开组策略管理控制台。

展开你的域下面的目录，在默认组策略上右键，选择菜单中的编辑，将出现一个新的窗口。

*管理 Samba 域组策略*

11、在组策略管理编辑器窗口中，进入到计算机配置 -> 组策略 -> Windows 设置 -> 安全设置 -> 本地策略 -> 安全选项，你将在右侧看到一个新的选项列表。

在右侧查询并编辑你的定制化设置，参考下图中的两条设置内容。

*配置 Samba 域组策略*

12、这两个条目编辑完成后，关闭所有窗口，打开 CMD 窗口，执行以下命令来强制应用组策略。

```
gpupdate /force

```

*更新 Samba 域组策略*

13、最后，重启你的电脑，当你准备登录进入系统的时候，你就会看到登录提示生效了。

*Samba4 AD 域控制器登录提示*

就写到这里吧！组策略是一个操作起来很繁琐和很谨慎的主题，在管理系统的过程中你得非常的小心。还有，注意你设置的组策略不会以任何方式应用到已加入域的 Linux 系统中。

---

作者简介：我是一个电脑迷，开源软件及 Linux 系统爱好者，有近4年的 Linux 桌面和服务器系统及 bash 编程经验。

---

via: <http://www.tecmint.com/manage-samba4-dns-group-policy-from-windows/>

作者：[Matei Cezar](http://www.tecmint.com/author/cezarmatei/) 译者：[rusking](https://github.com/rusking) 校对：[wxy](https://github.com/wxy)

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


我需要在 AGPLv3 许可证下提供源码么？

在天朝，这种协议几乎什么用都没有。

天朝：程序员看别人代码，能算抄吗？

# 我需要在 AGPLv3 许可证下提供源码么？

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

[GNU Affero 通用公共许可证版本 3](https://www.gnu.org/licenses/agpl-3.0-standalone.html)（AGPLv3）是与 GPLv3 几乎相同的<ruby> 左版 <rp>  （ </rp> <rt>  copyleft </rt> <rp>  ） </rp></ruby>许可证。两个许可证具有相同的公共版权范围，但在一个重要方面有重大差异。 AGPLv3 的第 13 节规定了 GPLv2 或 GPLv3 中不存在的附加条件：

>
> 在本许可证的其它条款之外，如果你修改了程序，你必须把你修改的版本，给你的那些使用计算机网络远程（如果你的版本支持此类交互）与之交互的用户，明确提供一个通过一些标准或者常规的复制手段，从网络服务器上免费获得与你所修改的版本相匹配的源码的机会。
>
>
>

这个“通过计算机网络远程交互”的范围主要被认为是 SaaS 部署的情形，尽管其实际上读起来的意思超乎了惯例的 SaaS 部署情形。其目标是解决在用户在使用像 Web Services 这样的功能时，其代码没有公开的常规 GPL 协议所暴露出的漏洞。因此，该协议的第 13 节，在 GPLv2 第 3 节以及 GPLv3 和 AGPLv3 第 6 节中包含的目标代码分发的触发要求之外，提供了额外的源代码公开的要求。

常常被误解的是，AGPLv3 第 13 节中的源代码分发要求仅在 AGPLv3 软件已被“你”（例如，提供网络服务的实体）修改的地方才触发。我的理解是，只要“你”不修改 AGPLv3 的代码，许可证就不应该被理解为需要按照第 13 节规定的方式访问相应的源码。如我所见，尽管即使公开许可证中不要求公开的源代码也是一个好主意，但在 AGPL 下许多未修改以及标准部署的软件模块根本不会触发第 13 节。

如何解释 AGPL 的条款和条件，包括 AGPL 软件是否已被修改，可能需要根据具体情况的事实和细节进行法律层面的分析。

---

作者简介：

Jeffrey R. Kaufman 是全球领先的开源软件解决方案提供商 Red Hat 公司的开源 IP 律师。Jeffrey 也是托马斯·杰斐逊法学院的兼职教授。在入职 Red Hat 之前，Jeffrey 曾经担任高通公司的专利顾问，向首席科学家办公室提供开源顾问。Jeffrey 在 RFID、条形码、图像处理和打印技术方面拥有多项专利。

---

via: <https://opensource.com/article/17/1/providing-corresponding-source-agplv3-license>

作者：[Jeffrey Robert Kaufman](https://opensource.com/users/jkaufman) 译者：[geekpi](https://github.com/geekpi) 校对：[Bestony](https://github.com/Bestony)

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


这些计算机术语你读对了么？

Summary: 石墨文档在 GitHub 上做了一个有趣的仓库，专门收集了许多中国程序员容易发音错误的单词；还有一位 lexrus 同学根据此库的数据，做了一个开源的 iOS 应用。

读错了一般多……

我也错了一半多....没想到gui竟然是鬼

这 Linux，原来一直读错了。。。呵呵。。

你个鬼哦……<br />
孤以…

Linux 读音不考究，一直照维基百科上的读。

我自认为自己挺注意的了，可还是有不少读错的。

何止这些。。<br />
读错英语的现象应该十分普遍了

Linux 中的 ux 可以念 阿克斯，这是 Linus 本人在电视访谈节目中说的。<br />
Git 念 高it

受这篇文章启发，写了一点脚本

function say {

    mpv http://dict.youdao.com/dictvoice?audio=${1}'&'type=1 > /dev/null

    }

alias say='say $1'

贴入自己的.bashrc，然后打开终端，say linux

 欢迎补充。

PS：原来这里发链接需要登录，这次就不匿名了

读到一半才发现自己读了错误的一列

哈哈哈哈

赞赞赞！<br />
PS.，ID 不错 →_→

友情提示： function say() 后面要有括号。

有function就不用括号了,没有function才要求要有括号

读错了很多。。。。

linux 都没读对还怎么往下看

function say

        set text $argv[1]

        set lang zh

        for i in (echo $argv | sed 's|--*|\\'\n'|g' | sed 's/[ \t]*$//g' | grep -v '^$')

                echo $i | read -l option value

                switch $option

                        case t text

                                set text $value

                        case l lang

                                set lang $value

                        case s speed

                                set speed $value

                end

        end

        if [ $text ]

                mpv "https://fanyi.baidu.com/gettts?lan=$lang&text=$text&spd=$speed&source=web" > /dev/null

        else

                echo "say -t text -l language(zh cte en jp kor pt de spa ru fra) -s speed"

        end

end

fish 版本，用的百度接口，支持普粤英日韩葡德西俄法等多种语言，阿拉伯语和泰语似乎因为编码的原因返回为空

调用方法，打开终端，输入 say "任何你想读音的单词，句子，段落。"

say -t text -l language(zh cte en jp kor pt de spa ru fra) -s speed

很好哦，小编。

# 这些计算机术语你读对了么？

作为程序员，我们虽然不像港台剧那样处处用中文混着英文说话，但是在日常的工作学习中还是会大量接触到各种英文单词和缩写。除了一些很普通的英文单词由于某种神秘不可知的原因被屡屡错读之外，计算机世界是变化的如此之快，也经常出现一些新的缩写令非英语母语的中国程序员在懵圈之后随便选个姿势去读。

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

我猜测，或许是为了将广告野生程序员从这种尴尬之中解脱出来，[石墨文档](https://shimo.im/doc/G3ckHEVF3f4qANHk)在 GitHub 上做了一个有趣的[仓库](https://github.com/shimohq/chinese-programmer-wrong-pronunciation)，专门收集了许多中国程序员容易发音错误的单词。

| 单词 | 正确发音 | 错误发音 |
| --- | --- | --- |
| **access [♪](http://dict.youdao.com/dictvoice?audio=access&type=1)** | **✓ ['ækses]** | **✗ [ək'ses]** |
| **Angular [♪](http://dict.youdao.com/dictvoice?audio=Angular&type=1)** | **✓ ['æŋgjʊlə]** | **✗ ['æŋɡələ; 'æŋdʒʌlə]** |
| **AJAX [♪](http://dict.youdao.com/dictvoice?audio=AJAX&type=1)** | **✓ ['eidʒæks]** | **✗ [ə'dʒʌks]** |
| **Apache [♪](http://dict.youdao.com/dictvoice?audio=Apache&type=1)** | **✓ [ə'pætʃɪ]** | **✗ [ʌpʌtʃ]** |
| **app [♪](http://dict.youdao.com/dictvoice?audio=app&type=1)** | **✓ [æp]** |  |
| **archive [♪](http://dict.youdao.com/dictvoice?audio=archive&type=1)** | **✓ ['ɑːkaɪv]** | **✗ ['ətʃɪv]** |
| **array [♪](http://dict.youdao.com/dictvoice?audio=array&type=1)** | **✓ [ə'rei]** | **✗ [æ'rei]** |
| **avatar [♪](http://dict.youdao.com/dictvoice?audio=avatar&type=1)** | **✓ ['ævətɑː]** | **✗ [ə'vʌtɑ]** |
| **Azure [♪](http://dict.youdao.com/dictvoice?audio=azure&type=1)** | **✓ ['æʒə]** | **✗ [ˈæzʊʒə]** |
| **cache [♪](http://dict.youdao.com/dictvoice?audio=cache&type=1)** | **✓ [kæʃ]** | **✗ [kætʃ]** |
| **deque [♪](http://dict.youdao.com/dictvoice?audio=deque&type=1)** | **✓ ['dek]** | **✗ [di'kju]** |
| **digest [♪](http://dict.youdao.com/dictvoice?audio=digest&type=1)** | **✓ ['dɑɪdʒɛst]** | **✗ ['dɪgɛst]** |
| **Django [♪](http://dict.youdao.com/dictvoice?audio=Django&type=1)** | **✓ [ˈdʒæŋɡoʊ]** | **✗ [diˈdʒæŋɡoʊ]** |
| **doc [♪](http://dict.youdao.com/dictvoice?audio=doc&type=1)** | **✓ [dɒk]** | **✗ [daʊk]** |
| **facade [♪](http://dict.youdao.com/dictvoice?audio=facade&type=1)** | **✓ [fə'sɑːd]]** | **✗ ['feikeid]** |
| **Git [♪](http://dict.youdao.com/dictvoice?audio=git&type=1)** | **✓ [ɡɪt]** | **✗ [dʒɪt; jɪt]** |
| **GNU [♪](http://dict.youdao.com/dictvoice?audio=GNU&type=1)** | **✓ [gnu:]** |  |
| **GUI [♪](http://dict.youdao.com/dictvoice?audio=GUI&type=1)** | **✓ [ˈɡui]** |  |
| **height [♪](http://dict.youdao.com/dictvoice?audio=height&type=1)** | **✓ [haɪt]** | **✗ [heɪt]** |
| **hidden [♪](http://dict.youdao.com/dictvoice?audio=hidden&type=1)** | **✓ ['hɪdn]** | **✗ ['haɪdn]** |
| **image [♪](http://dict.youdao.com/dictvoice?audio=image&type=1)** | **✓ ['ɪmɪdʒ]** | **✗ [ɪ'meɪdʒ]** |
| **integer [♪](http://dict.youdao.com/dictvoice?audio=integer&type=1)** | **✓ ['ɪntɪdʒə]** | **✗ [ˈɪntaɪgə]** |
| **issue [♪](http://dict.youdao.com/dictvoice?audio=issue&type=1)** | **✓ ['ɪʃuː]** | **✗ [ˈaɪʃuː]** |
| **Java [♪](http://dict.youdao.com/dictvoice?audio=java&type=1)** | **✓ ['dʒɑːvə]** | **✗ ['dʒɑːvɑː]** |
| **jpg(jpeg) [♪](http://dict.youdao.com/dictvoice?audio=JPEG&type=1)** | **✓ ['dʒeɪpeɡ]** | **[ˈdʒeɪˈpi:ˈdʒiː]** |
| **Linux [♪](http://dict.youdao.com/dictvoice?audio=linux&type=1)** | **✓ ['lɪnəks]** | **✗ [ˈlɪnʌks; ˈlɪnjuːks]** |
| **main [♪](http://dict.youdao.com/dictvoice?audio=main&type=1)** | **✓ [meɪn]** | **✗ [mɪn]** |
| **margin [♪](http://dict.youdao.com/dictvoice?audio=margin&type=1)** | **✓ ['mɑːdʒɪn]** | **✗ ['mʌgɪn]** |
| **maven [♪](http://dict.youdao.com/dictvoice?audio=maven&type=1)** | **✓ ['meɪvn]** | **✗ ['maːvn]** |
| **module [♪](http://dict.youdao.com/dictvoice?audio=module&type=1)** | **✓ ['mɒdjuːl]** | **✗ ['məʊdl]** |
| **nginx** | **✓ Engine X** |  |
| **null [♪](http://dict.youdao.com/dictvoice?audio=null&type=1)** | **✓ [nʌl]** | **✗ [naʊ]** |
| **OS X** | **✓ OS ten** |  |
| **parameter [♪](http://dict.youdao.com/dictvoice?audio=parameter&type=1)** | **✓ [pə'ræmɪtə]** | **✗ ['pærəmɪtə]** |
| **putty [♪](http://dict.youdao.com/dictvoice?audio=putty&type=1)** | **✓ [ˈpʌti]** | **✗ [ˈpuːti]** |
| **query [♪](http://dict.youdao.com/dictvoice?audio=query&type=1)** | **✓ ['kwɪəri]** | **✗ ['kwaɪri]** |
| **Qt [♪](http://dict.youdao.com/dictvoice?audio=cute&type=1)** | **✓ [kjuːt]** |  |
| **resolved [♪](http://dict.youdao.com/dictvoice?audio=resolved&type=1)** | **✓ [rɪ'zɒlvd]** | **✗ [rɪ'səʊvd]** |
| **retina [♪](http://dict.youdao.com/dictvoice?audio=retina&type=1)** | **✓ ['retɪnə]** | **✗ [ri'tina]** |
| **san jose [♪](http://dict.youdao.com/dictvoice?audio=san%20jose&type=1)** | **✓ [sænhəu'zei]** | **✗ [sæn'ju:s]** |
| **safari [♪](http://dict.youdao.com/dictvoice?audio=safari&type=1)** | **✓ [sə'fɑːrɪ]** | **✗ [sæfərɪ]** |
| **scheme [♪](http://dict.youdao.com/dictvoice?audio=scheme&type=1)** | **✓ [skiːm]** | **✗ [s'kæmə]** |
| **sudo** | **✓ ['suːduː]** |  |
| **suite [♪](http://dict.youdao.com/dictvoice?audio=suite&type=1)** | **✓ [swiːt]** | **✗ [sjuːt]** |
| **typical [♪](http://dict.youdao.com/dictvoice?audio=typical&type=1)** | **✓ ['tɪpɪkl]** | **✗ ['taɪpɪkəl]** |
| **Ubuntu [♪](http://dict.youdao.com/dictvoice?audio=ubuntu&type=1)** | **✓ [ʊ'bʊntʊ]** | **✗ [juː'bʊntʊ]** |
| **variable [♪](http://dict.youdao.com/dictvoice?audio=variable&type=1)** | **✓ ['veəriəbl]** | **✗ [və'raiəbl]** |
| **vue [♪](http://dict.youdao.com/dictvoice?audio=vue&type=1)** | **✓ [v'ju:]** | **✗ [v'ju:i]** |
| **width [♪](http://dict.youdao.com/dictvoice?audio=width&type=1)** | **✓ [wɪdθ]** | **✗ [waɪdθ]** |
| **YouTube [♪](http://dict.youdao.com/dictvoice?audio=youtube&type=1)** | **✓ ['juː'tjuːb]** | **✗ ['juː'tʊbɪ]** |

本着简单的原则，又为了避免程序员们出现选择困难症，“正确音标”采用了最接近有道词典音频的英式 DJ 音标，不代表其唯一性。专业在线英语词典请参考[知乎链接:在线英语词典哪个比较好?](https://www.zhihu.com/question/19707759)

参考资料

1. <https://www.zhihu.com/question/19739907>
2. <https://www.v2ex.com/t/131094>
3. <https://www.v2ex.com/t/309350>
4. <https://www.v2ex.com/t/63781>
5. <https://www.v2ex.com/t/246033>
6. <https://www.v2ex.com/t/342087>

此外，还有一位 [lexrus](https://github.com/lexrus) 同学根据此库的数据，做了一个开源的 iOS 应用，这个应用的名字非常绝妙，叫做“[花灰](https://github.com/lexrus/Huahui)”——请捋直了舌头跟我念：H-U-A-花，H-U-I-灰，花灰。

作者说道：

>
> 这个小 App 用 UITableView 陈列了一些中国程序员经常读错的单词，用 AVFoundation 实现美音和英音的朗读，用 Playground 同步单词库。功能异常简陋，实现非常粗暴，仅供 iOS 程序员自行编译，纠正发音使用，懒得提交 App Store。
>
>
>

可从以下视频中略窥一斑，做的还是不错的，不过如果你的 iPhone 没越狱，显然是没法用的——或许你可以去作者的 issues 页面请求他上架？:-D

那么你有几个读错的呢？欢迎发表评论，也欢迎去[提交](https://github.com/shimohq/chinese-programmer-wrong-pronunciation)你发现易读错的计算机术语和缩写。

有同学表示，读对还不行，你经常用的一些单词术语，或许并不是你以为的那个意思。不信？你去看看“[程序员眼中的英语单词](/article-6302-1.html)”和它原本的意思差别有多大就知道了~


将 Tuleap 用于软件项目管理

Summary: Eclipse 基金会使用 Tuleap 取代了 Bugzilla。

# 将 Tuleap 用于软件项目管理

>
> [Eclipse 基金会](http://www.eclipse.org/)使用 Tuleap 取代了 Bugzilla。
>
>
>

Tuleap 是一个独特的开源项目管理工具，目前发展势头很好，现在，每个月它会出一个大版本。它还被列在 [2015 年五大开源项目管理工具](https://opensource.com/business/15/1/top-project-management-tools-2015)和 [2016 年十一个名列前茅项目管理工具](https://opensource.com/business/16/3/top-project-management-tools-2016)中。

Manuel Vacelet 是开发 Tuleap 项目的 Enalean 公司的联合创始人和 CTO，他说：“Tuleap 是一个完整用于托管软件项目的 GPLv2 平台，它提供了一个集中化的平台，在这里，团队可以找到他们所需的所有工具，追踪他们软件项目的生命周期。他们可以找到项目管理（Scrum、看板、瀑布、混合等等）、源码控制（git 和 svn）和代码审查（pull 请求和 gerrit）、持续集成、问题跟踪、wiki 和文档等的支持。”

在这次采访中，我会和 Manuel 讨论如何开始使用它，以及如何以开源方式管理 Tuleap。

**Nitish Tiwari（以下简称 NT）： 为什么 Tuleap 项目很重要？**

**Manuel Vacelet（以下简称 MV）：** Tuleap 很重要是因为我们坚信一个成功的（软件）项目必须涉及所有利益相关者：开发人员、项目经理、QA、客户和用户。

很久以前，我还是一个 SourceForge 衍生项目的实习生（当时 SourceForge 还是一个自由开源项目），几年后它变成了 Tuleap。 我的第一个贡献是将 PhpWiki 集成到该工具中（不要告诉任何人，代码写的很糟）。

现在，我很高兴作为首席技术官和产品负责人在 Enalean 工作，该公司是 Tuleap 项目的主要贡献公司。

**NT：让我们聊聊技术方面。**

**MV：** Tuleap 核心系统是基于 LAMP 并且架构于 CentOS 之上。如今的开发栈是 AngularJS (v1)、REST 后端（PHP）、基于 NodeJS 的实时推送服务器。但如果你想成为一名 Tuleap 全栈开发人员，你还将需要接触 bash、Perl、Python、Docker、Make 等等。

说到技术方面，需要重点强调的 Tuleap 的一个显著特征是它的可扩展性。一个运行在单服务器上的 Tuleap 单一实例、并且没有复杂的 IT 架构，可以处理超过 10000 人的访问。

**NT：给我们说下该项目的用户和社区。有谁参与？他们如何使用这个工具？**

**MV：** 用户非常多样化。从使用 Tuleap 跟踪他们的项目进度并管理他们的源代码的小型初创公司，到非常大的公司，如法国电信运营商 Orange，它为超过 17000 用户部署了它，并托管了 5000 个项目。

许多用户依靠 Tuleap 来促进敏捷项目并跟踪其进度。开发人员和客户共享同一个工作区。客户不需要学习如何使用 GitHub，也不需要开发人员做额外的工作，就可以将其工作转换到“客户可访问”平台。

今年， [Eclipse 基金会](http://www.eclipse.org/)使用 Tuleap 取代了 Bugzilla。

印度电子信息技术部使用 Tuleap 创建了印度政府开放电子政务的开放式协作开发平台。

Tuleap 有许多种不同的使用方式和配置。有些人使用它作为 Drupal 客户门户网站的后端; 它们通过 REST API 插入到 Tuleap 中以管理 bug 和服务请求。

甚至一些建筑师也使用它来管理他们的工作进度和 AutoCAD 文件。

**NT：Tuleap 是否做了一些特别的事，使社区更安全，更多样化？**

**MV：** 我们还没有创建“行为准则”；本社区非常平和而欢迎新人，但我们有计划这样做。Tuleap 的开发人员和贡献者来自不同的国家（例如加拿大、突尼斯、法国）。而且 35％ 的活跃开发者和贡献者是女性。

**NT：由社区提议的 Tuleap 功能的百分比是多少？**

**MV：** 几乎 100％ 的功能是由社区驱动的。

这是 Enalean 的关键挑战之一：找到一种商业模式，使我们能以正确的方式做开源软件。对我们来说，“开放核心”模式（其中应用程序的核心是开放的，但有趣和有用的部分是封闭源的）不是正确的方法，因为你最终还是要依赖闭源。因此，我们发明了 [OpenRoadmap](https://blog.enalean.com/enalean-open-roadmap-how-it-works/)，这种方式是我们从社区和最终用户那里收集需求，并找公司来为此买单。

---

作者简介：

Nitish 是一名专业的软件开发人员并对开源有热情。作为一本基于 Linux 的杂志的技术作者，他会尝试新的开源工具。他喜欢阅读和探索任何开源相关的事情。在他的空闲时间，他喜欢读励志书。他目前正在构建 DevUp - 一个让开发人员以真正的方式连接所有工具和拥抱 DevOps 的平台。你可以在 Twitter 上关注他 @tiwari\_nitish。

---

via: <https://opensource.com/article/17/1/interview-Tuleap-project>

作者：[Nitish Tiwari](https://opensource.com/users/tiwarinitish86) 译者：[geekpi](https://github.com/geeki) 校对：[jamsinepeng](https://github.com/jasminepeng)

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


在 Atomic 主机上远程使用 Docker

Summary: 来自 Atomic 项目 的 Atomic 主机是一个轻量级的容器基于的操作系统，它可以运行 Linux 容器。它已被优化为用作云环境的容器运行时系统。

# 在 Atomic 主机上远程使用 Docker

来自 [Atomic 项目](http://www.projectatomic.io/) 的 Atomic 主机是一个轻量级的容器基于的操作系统，它可以运行 Linux 容器。它已被优化为用作云环境的容器运行时系统。例如，它可以托管 Docker 守护进程和容器。有时，你可能需要在该主机上运行 docker 命令，并从其他地方管理服务器。本文介绍如何远程访问 Fedora Atomic 主机（[你可以在这里下载到它](https://getfedora.org/atomic/)）上的 [Docker](https://www.docker.com/) 守护进程。整个过程由 [Ansible](https://www.ansible.com/) 自动完成 - 在涉及到自动化的一切上，这真是一个伟大的工具！

### 安全备忘录

由于我们通过网络连接，所以我们使用 [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security) 保护 Docker 守护进程。此过程需要客户端证书和服务器证书。OpenSSL 包用于创建用于建立 TLS 连接的证书密钥。这里，Atomic 主机运行守护程序，我们的本地的 [Fedora Workstation](https://getfedora.org/en/workstation/) 充当客户端。

在你按照这些步骤进行之前，请注意，*任何*在客户端上可以访问 TLS 证书的进程在服务器上具有**完全的 root 访问权限**。 因此，客户端可以在服务器上做任何它想做的事情。我们需要仅向可信任的特定客户端主机授予证书访问权限。你应该将客户端证书仅复制到完全由你控制的客户端主机。但即使在这种情况下，客户端机器的安全也至关重要。

不过，此方法只是远程访问守护程序的一种方法。编排工具通常提供更安全的控制。下面的简单方法适用于个人实验，可能不适合开放式网络。

### 获取 Ansible role

[Chris Houseknecht](https://twitter.com/CHouseknecht) 写了一个 Ansible role，它会创造所需的所有证书。这样，你不需要手动运行 `openssl` 命令了。 这些在 [Ansible role 仓库](https://github.com/ansible/role-secure-docker-daemon)中提供。将它克隆到你当前的工作主机。

```
$ mkdir docker-remote-access
$ cd docker-remote-access
$ git clone https://github.com/ansible/role-secure-docker-daemon.git

```

### 创建配置文件

接下来，你必须创建 Ansible 配置文件、<ruby> 清单 <rp>  （ </rp> <rt>  inventory </rt> <rp>  ） </rp></ruby>和<ruby> 剧本 <rp>  （ </rp> <rt>  playbook </rt> <rp>  ） </rp></ruby>文件以设置客户端和守护进程。以下说明在 Atomic 主机上创建客户端和服务器证书。然后，获取客户端证书到本地。最后，它们会配置守护进程以及客户端，使它们能彼此交互。

这里是你需要的目录结构。如下所示，创建下面的每个文件。

```
$ tree docker-remote-access/
docker-remote-access/
├── ansible.cfg
├── inventory
├── remote-access.yml
└── role-secure-docker-daemon

```

`ansible.cfg`：

```
$ vim ansible.cfg

```

```
[defaults]
inventory=inventory

```

清单文件（`inventory`）：

```
$ vim inventory

```

```
[daemonhost]
'IP_OF_ATOMIC_HOST' ansible_ssh_private_key_file='PRIVATE_KEY_FILE'

```

将清单文件（`inventory`） 中的 `IP_OF_ATOMIC_HOST` 替换为 Atomic 主机的 IP。将 `PRIVATE_KEY_FILE` 替换为本地系统上的 SSH 私钥文件的位置。

剧本文件（`remote-access.yml`）：

```
$ vim remote-access.yml

```

```
- name: Docker Client Set up
  hosts: daemonhost
  gather_facts: no
  tasks:
    - name: Make ~/.docker directory for docker certs
      local_action: file path='~/.docker' state='directory'

    - name: Add Environment variables to ~/.bashrc
      local_action: lineinfile dest='~/.bashrc' line='export DOCKER_TLS_VERIFY=1\nexport DOCKER_CERT_PATH=~/.docker/\nexport DOCKER_HOST=tcp://{{ inventory_hostname }}:2376\n' state='present'

    - name: Source ~/.bashrc file
      local_action: shell source ~/.bashrc

- name: Docker Daemon Set up
  hosts: daemonhost
  gather_facts: no
  remote_user: fedora
  become: yes
  become_method: sudo
  become_user: root
  roles:
    - role: role-secure-docker-daemon
      dds_host: "{{ inventory_hostname }}"
      dds_server_cert_path: /etc/docker
      dds_restart_docker: no
  tasks:
    - name: fetch ca.pem from daemon host
      fetch:
        src: /root/.docker/ca.pem
        dest: ~/.docker/
        fail_on_missing: yes
        flat: yes
    - name: fetch cert.pem from daemon host
      fetch:
        src: /root/.docker/cert.pem
        dest: ~/.docker/
        fail_on_missing: yes
        flat: yes
    - name: fetch key.pem from daemon host
      fetch:
        src: /root/.docker/key.pem
        dest: ~/.docker/
        fail_on_missing: yes
        flat: yes
    - name: Remove Environment variable OPTIONS from /etc/sysconfig/docker
      lineinfile:
        dest: /etc/sysconfig/docker
        regexp: '^OPTIONS'
        state: absent

    - name: Modify Environment variable OPTIONS in /etc/sysconfig/docker
      lineinfile:
        dest: /etc/sysconfig/docker
        line: "OPTIONS='--selinux-enabled --log-driver=journald --tlsverify --tlscacert=/etc/docker/ca.pem --tlscert=/etc/docker/server-cert.pem --tlskey=/etc/docker/server-key.pem -H=0.0.0.0:2376 -H=unix:///var/run/docker.sock'"
        state: present

    - name: Remove client certs from daemon host
      file:
        path: /root/.docker
        state: absent

    - name: Reload Docker daemon
      command: systemctl daemon-reload
    - name: Restart Docker daemon
      command: systemctl restart docker.service

```

### 访问 Atomic 主机

现在运行 Ansible 剧本：

```
$ ansible-playbook remote-access.yml

```

确保 tcp 端口 2376 在你的 Atomic 主机上打开了。如果你在使用 Openstack，请在安全规则中添加 TCP 端口 2376。 如果你使用 AWS，请将其添加到你的安全组。

现在，在你的工作站上作为普通用户运行的 `docker` 命令与 Atomic 主机的守护进程通信，并在那里执行命令。你不需要手动 `ssh` 或在 Atomic 主机上发出命令。这可以让你远程、轻松、安全地启动容器化应用程序。

如果你想克隆 Ansible 剧本和配置文件，这里是 [git 仓库](https://github.com/trishnaguha/fedora-cloud-ansible/tree/master/docker-remote-access)。

---

via: <https://fedoramagazine.org/use-docker-remotely-atomic-host/>

作者：[Trishna Guha](http://trishnag.id.fedoraproject.org/) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

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


LXD 2.0 系列（九）：实时迁移

# LXD 2.0 系列（九）：实时迁移

这是 LXD 2.0 系列介绍文章的第九篇。

1. [LXD 入门](/article-7618-1.html)
2. [安装与配置](/article-7687-1.html)
3. [你的第一个 LXD 容器](/article-7706-1.html)
4. [资源控制](/article-8072-1.html)
5. [镜像管理](/article-8107-1.html)
6. [远程主机及容器迁移](/article-8169-1.html)
7. [LXD 中的 Docker](/article-8235-1.html)
8. [LXD 中的 LXD](/article-8257-1.html)
9. [实时迁移](/article-8263-1.html)
10. [LXD 和 Juju](/article-8273-1.html)
11. [LXD 和 OpenStack](/article-8274-1.html)
12. [调试，及给 LXD 做贡献](/article-8282-1.html)

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

### 介绍

LXD 2.0 中的有一个尽管是实验性质的但非常令人兴奋的功能，那就是支持容器检查点和恢复。

简单地说，检查点/恢复意味着正在运行的容器状态可以被序列化到磁盘，要么可以作为同一主机上的有状态快照，要么放到另一主机上相当于实时迁移。

### 要求

要使用容器实时迁移和有状态快照，你需要以下条件：

* 一个非常新的 Linux 内核，4.4 或更高版本。
* CRIU 2.0，可能需要一些 cherry-pick 的提交，具体取决于你确切的内核配置。
* 直接在主机上运行 LXD。 不能在容器嵌套下使用这些功能。
* 对于迁移，目标主机必须至少实现源主机的指令集，目标主机内核必须至少提供与源主机相同的系统调用，并且在源主机上挂载的任何内核文件系统也必须可挂载到目标主机上。

Ubuntu 16.04 LTS 已经提供了所有需要的依赖，在这种情况下，您只需要安装 CRIU 本身：

```
apt install criu

```

### 使用 CRIU

#### 有状态快照

一个普通的快照看上去像这样：

```
stgraber@dakara:~$ lxc snapshot c1 first
stgraber@dakara:~$ lxc info c1 | grep first
 first (taken at 2016/04/25 19:35 UTC) (stateless)

```

一个有状态快照看上去像这样：

```
stgraber@dakara:~$ lxc snapshot c1 second --stateful
stgraber@dakara:~$ lxc info c1 | grep second
 second (taken at 2016/04/25 19:36 UTC) (stateful)

```

这意味着所有容器运行时状态都被序列化到磁盘并且作为了快照的一部分。可以像你还原无状态快照那样还原一个有状态快照：

```
stgraber@dakara:~$ lxc restore c1 second
stgraber@dakara:~$

```

#### 有状态快照的停止/启动

比方说你由于升级内核或者其他类似的维护而需要重启机器。与其等待重启后启动所有的容器，你可以：

```
stgraber@dakara:~$ lxc stop c1 --stateful

```

容器状态将会写入到磁盘，会在下次启动时读取。

你甚至可以看到像下面那样的状态：

```
root@dakara:~# tree /var/lib/lxd/containers/c1/rootfs/state/
/var/lib/lxd/containers/c1/rootfs/state/
├── cgroup.img
├── core-101.img
├── core-102.img
├── core-107.img
├── core-108.img
├── core-109.img
├── core-113.img
├── core-114.img
├── core-122.img
├── core-125.img
├── core-126.img
├── core-127.img
├── core-183.img
├── core-1.img
├── core-245.img
├── core-246.img
├── core-50.img
├── core-52.img
├── core-95.img
├── core-96.img
├── core-97.img
├── core-98.img
├── dump.log
├── eventfd.img
├── eventpoll.img
├── fdinfo-10.img
├── fdinfo-11.img
├── fdinfo-12.img
├── fdinfo-13.img
├── fdinfo-14.img
├── fdinfo-2.img
├── fdinfo-3.img
├── fdinfo-4.img
├── fdinfo-5.img
├── fdinfo-6.img
├── fdinfo-7.img
├── fdinfo-8.img
├── fdinfo-9.img
├── fifo-data.img
├── fifo.img
├── filelocks.img
├── fs-101.img
├── fs-113.img
├── fs-122.img
├── fs-183.img
├── fs-1.img
├── fs-245.img
├── fs-246.img
├── fs-50.img
├── fs-52.img
├── fs-95.img
├── fs-96.img
├── fs-97.img
├── fs-98.img
├── ids-101.img
├── ids-113.img
├── ids-122.img
├── ids-183.img
├── ids-1.img
├── ids-245.img
├── ids-246.img
├── ids-50.img
├── ids-52.img
├── ids-95.img
├── ids-96.img
├── ids-97.img
├── ids-98.img
├── ifaddr-9.img
├── inetsk.img
├── inotify.img
├── inventory.img
├── ip6tables-9.img
├── ipcns-var-10.img
├── iptables-9.img
├── mm-101.img
├── mm-113.img
├── mm-122.img
├── mm-183.img
├── mm-1.img
├── mm-245.img
├── mm-246.img
├── mm-50.img
├── mm-52.img
├── mm-95.img
├── mm-96.img
├── mm-97.img
├── mm-98.img
├── mountpoints-12.img
├── netdev-9.img
├── netlinksk.img
├── netns-9.img
├── netns-ct-9.img
├── netns-exp-9.img
├── packetsk.img
├── pagemap-101.img
├── pagemap-113.img
├── pagemap-122.img
├── pagemap-183.img
├── pagemap-1.img
├── pagemap-245.img
├── pagemap-246.img
├── pagemap-50.img
├── pagemap-52.img
├── pagemap-95.img
├── pagemap-96.img
├── pagemap-97.img
├── pagemap-98.img
├── pages-10.img
├── pages-11.img
├── pages-12.img
├── pages-13.img
├── pages-1.img
├── pages-2.img
├── pages-3.img
├── pages-4.img
├── pages-5.img
├── pages-6.img
├── pages-7.img
├── pages-8.img
├── pages-9.img
├── pipes-data.img
├── pipes.img
├── pstree.img
├── reg-files.img
├── remap-fpath.img
├── route6-9.img
├── route-9.img
├── rule-9.img
├── seccomp.img
├── sigacts-101.img
├── sigacts-113.img
├── sigacts-122.img
├── sigacts-183.img
├── sigacts-1.img
├── sigacts-245.img
├── sigacts-246.img
├── sigacts-50.img
├── sigacts-52.img
├── sigacts-95.img
├── sigacts-96.img
├── sigacts-97.img
├── sigacts-98.img
├── signalfd.img
├── stats-dump
├── timerfd.img
├── tmpfs-dev-104.tar.gz.img
├── tmpfs-dev-109.tar.gz.img
├── tmpfs-dev-110.tar.gz.img
├── tmpfs-dev-112.tar.gz.img
├── tmpfs-dev-114.tar.gz.img
├── tty.info
├── unixsk.img
├── userns-13.img
└── utsns-11.img

0 directories, 154 files

```

还原容器也很简单：

```
stgraber@dakara:~$ lxc start c1

```

### 实时迁移

实时迁移基本上与上面的有状态快照的停止/启动相同，除了容器目录和配置被移动到另一台机器上。

```
stgraber@dakara:~$ lxc list c1
+------+---------+-----------------------+----------------------------------------------+------------+-----------+
| NAME |  STATE  |          IPV4         |                     IPV6                     |    TYPE    | SNAPSHOTS |
+------+---------+-----------------------+----------------------------------------------+------------+-----------+
| c1   | RUNNING | 10.178.150.197 (eth0) | 2001:470:b368:4242:216:3eff:fe19:27b0 (eth0) | PERSISTENT | 2         |
+------+---------+-----------------------+----------------------------------------------+------------+-----------+

stgraber@dakara:~$ lxc list s-tollana:
+------+-------+------+------+------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------+-------+------+------+------+-----------+

stgraber@dakara:~$ lxc move c1 s-tollana:

stgraber@dakara:~$ lxc list c1
+------+-------+------+------+------+-----------+
| NAME | STATE | IPV4 | IPV6 | TYPE | SNAPSHOTS |
+------+-------+------+------+------+-----------+

stgraber@dakara:~$ lxc list s-tollana:
+------+---------+-----------------------+----------------------------------------------+------------+-----------+
| NAME |  STATE  |          IPV4         |                     IPV6                     |    TYPE    | SNAPSHOTS |
+------+---------+-----------------------+----------------------------------------------+------------+-----------+
| c1   | RUNNING | 10.178.150.197 (eth0) | 2001:470:b368:4242:216:3eff:fe19:27b0 (eth0) | PERSISTENT | 2         |
+------+---------+-----------------------+----------------------------------------------+------------+-----------+

```

### 限制

正如我之前说的，容器的检查点/恢复还是非常新的功能，我们还在努力地开发这个功能、修复已知的问题。我们确实需要更多的人来尝试这个功能，并给我们反馈，但我不建议在生产中使用这个功能。

我们跟踪的问题列表在 [Launchpad上](https://bugs.launchpad.net/ubuntu/+source/criu/+bugs)。

我们估计在带有 CRIU 的 Ubuntu 16.04 上带有几个服务的基本的 Ubuntu 容器能够正常工作。然而在更复杂的容器、使用了设备直通、复杂的网络服务或特殊的存储配置下可能会失败。

要是有问题，CRIU 会尽可能地在转储时失败，而不是在恢复时。在这种情况下，源容器将继续运行，快照或迁移将会失败，并生成一个日志文件用于调试。

在极少数情况下，CRIU 无法恢复容器，在这种情况下，源容器仍然存在但将被停止，并且必须手动重新启动。

### 发送 bug 报告

我们正在跟踪 Launchpad 上关于 CRIU Ubuntu 软件包的检查点/恢复相关的错误。大多数修复 bug 工作是在上游的 CRIU 或 Linux 内核上进行，但是这种方式我们更容易跟踪。

要提交新的 bug 报告，请看这里。

请务必包括：

* 你运行的命令和显示给你的错误消息
* `lxc info` 的输出（\*）
* `lxc info <container name>`的输出
* `lxc config show -expanded <container name>` 的输出
* `dmesg`（\*）的输出
* `/proc/self/mountinfo` 的输出（\*）
* `lxc exec <container name> - cat /proc/self/mountinfo` 的输出
* `uname -a`（\*）的输出
* `/var/log/lxd.log`（\*）的内容
* `/etc/default/lxd-bridge`（\*）的内容
* `/var/log/lxd/<container name>/` 的 tarball（\*）

如果报告迁移错误，而不是状态快照或有状态停止的错误，请将上面所有含有（\*）标记的源与目标主机的信息发来。

### 额外信息

CRIU 的网站在： <https://criu.org>

LXD 的主站在： <https://linuxcontainers.org/lxd>

LXD 的 GitHub 仓库： <https://github.com/lxc/lxd>

LXD 的邮件列表： <https://lists.linuxcontainers.org>

LXD 的 IRC 频道： #lxcontainers on irc.freenode.net

---

作者简介：我是 Stéphane Graber。我是 LXC 和 LXD 项目的领导者，目前在加拿大魁北克蒙特利尔的家所在的Canonical 有限公司担任 LXD 的技术主管。

---

via: <https://stgraber.org/2016/04/25/lxd-2-0-live-migration-912/>

作者：[Stéphane Graber](https://www.stgraber.org/author/stgraber/) 译者：[geekpi](https://github.com/geekpi) 校对：[wxy](https://github.com/wxy)

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


如何用 R 语言的 Shiny 库编写 web 程序

Summary: Shiny 是一个来自 RStudio 的工具包，它让创建 web 程序变得更容易。它能从 R 控制台轻松安装，只需要一行，就可以加载好最新的稳定版本来使用。

# 如何用 R 语言的 Shiny 库编写 web 程序

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

我这个月在写一些更加长的文章，所以你们可以在几周后再来看看。本月，我想简要地提下我自己一直在玩的一个很棒的 R 库。

我的一个亲密朋友最近在用 R 编写东西。我一直都对它很感兴趣，也一直在试图挤时间，学习更多关于 R 的知识以及可用它做的事情。探索 R 的超强数字处理能力对我而言有些困难，因为我并不如我朋友那样有一个数学头脑。我进展有点慢，但我一直试图将它与我在其他领域的经验联系起来，我甚至开始考虑非常简单的 web 程序。

[Shiny](http://shiny.rstudio.com/) 是一个来自 RStudio 的工具包，它让创建 web 程序变得更容易。它能从 R 控制台轻松安装，只需要一行，就可以加载好最新的稳定版本来使用。这里有一个很棒的[教程](http://shiny.rstudio.com/tutorial)，它可以在前面课程基础上，带着你理解应用架设的概念。 Shiny 的授权是 GPLv3，源代码可以在 [GitHub](https://github.com/studio/shiny) 上获得。

这是一个用 Shiny 写的简单的小 web 程序：

```
library(shiny)

server <- function(input, output, session) {
    observe({
        myText <- paste("Value above is: ", input$textIn)
        updateTextInput(session, "textOut", value=myText)
    })
}

ui <- basicPage(
    h3("My very own sample application!"),
    textInput("textIn", "Input goes here, please."),
    textInput("textOut", "Results will be printed in this box")
)

shinyApp(ui = ui, server = server)

```

当你在输入框中输入文字时，它会被复制到输出框中提示语后。这并没有什么奇特的，但它向你展示了一个 Shiny 程序的基本结构。“server”部分允许你处理所有后端工作，如计算、数据库检索或程序需要发生的任何其他操作。“ui”部分定义了接口，它可以根据需要变得简单或复杂。

包括在 Shiny 中的 [Bootstrap](http://getbootstrap.com/) 有了大量样式和主题，所以在学习了一点后，就能用 R 创建大量功能丰富的 web 程序。使用附加包可以将功能扩展到更高级的 JavaScript 程序、模板等。

有几种方式处理 Shiny 的后端工作。如果你只是在本地运行你的程序，加载库就能做到。对于想要发布到网络上的程序，你可以在 [RStudio 的 Shiny 网站](http://shinyapps.io/)上共享它们，运行开源版本的 Shiny 服务器，或通过按年订阅服务从 RStudio 处购买 Shiny Server Pro。

经验丰富的 R 大牛可能已经知道 Shiny 了；它已经存在大约几年了。对于像我这样来自一个完全不同的编程语言，并且希望学习一点 R 的人来说，它是相当有帮助的。

---

作者简介：

D Ruth Bavousett - D Ruth Bavousett 作为一名系统管理员和软件开发人员已经很长时间了，她的专业生涯开始于 VAX 11/780。在她的职业生涯（迄今为止）中，她花费了大量的时间在满足库的需求上，她自 2008 年以来一直是 Koha 开源库自动化套件的贡献者. Ruth 目前在休斯敦的 cPanel 任 Perl 开发人员，他也作为首席员工效力于双猫公司。

---

via: <https://opensource.com/article/17/1/writing-new-web-apps-shiny>

作者：[D Ruth Bavousett](https://opensource.com/users/druthb) 译者：[geekpi](https://github.com/geekpi) 校对：[jasminepeng](https://github.com/jasminepeng)

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


一个使用 asyncio 协程的网络爬虫（一）

look look small weiba

# 一个使用 asyncio 协程的网络爬虫（一）

>
> 本文作者：
>
>
> A. Jesse Jiryu Davis 是纽约 MongoDB 的工程师。他编写了异步 MongoDB Python 驱动程序 Motor，也是 MongoDB C 驱动程序的开发领袖和 PyMongo 团队成员。 他也为 asyncio 和 Tornado 做了贡献，在 <http://emptysqua.re> 上写作。
>
>
> Guido van Rossum 是主流编程语言 Python 的创造者，Python 社区称他为 BDFL （<ruby> 仁慈的终生大独裁者 <rp>  （ </rp> <rt>  Benevolent Dictator For Life </rt> <rp>  ） </rp></ruby>）——这是一个来自 Monty Python 短剧的称号。他的主页是 [http://www.python.org/~guido/](http://www.python.org/%7Eguido/) 。
>
>
>

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

### 介绍

经典的计算机科学强调高效的算法，尽可能快地完成计算。但是很多网络程序的时间并不是消耗在计算上，而是在等待许多慢速的连接或者低频事件的发生。这些程序暴露出一个新的挑战：如何高效的等待大量网络事件。一个现代的解决方案是异步 I/O。

这一章我们将实现一个简单的网络爬虫。这个爬虫只是一个原型式的异步应用，因为它等待许多响应而只做少量的计算。一次爬的网页越多，它就能越快的完成任务。如果它为每个动态的请求启动一个线程的话，随着并发请求数量的增加，它会在耗尽套接字之前，耗尽内存或者线程相关的资源。使用异步 I/O 可以避免这个的问题。

我们将分三个阶段展示这个例子。首先，我们会实现一个事件循环并用这个事件循环和回调来勾画出一只网络爬虫。它很有效，但是当把它扩展成更复杂的问题时，就会导致无法管理的混乱代码。然后，由于 Python 的协程不仅有效而且可扩展，我们将用 Python 的生成器函数实现一个简单的协程。在最后一个阶段，我们将使用 Python 标准库“asyncio”中功能完整的协程， 并通过异步队列完成这个网络爬虫。（在 [PyCon 2013](http://pyvideo.org/video/1667/keynote) 上，Guido 介绍了标准的 asyncio 库，当时称之为“Tulip”。）

### 任务

网络爬虫寻找并下载一个网站上的所有网页，也许还会把它们存档，为它们建立索引。从根 URL 开始，它获取每个网页，解析出没有遇到过的链接加到队列中。当网页没有未见到过的链接并且队列为空时，它便停止运行。

我们可以通过同时下载大量的网页来加快这一过程。当爬虫发现新的链接，它使用一个新的套接字并行的处理这个新链接，解析响应，添加新链接到队列。当并发很大时，可能会导致性能下降，所以我们会限制并发的数量，在队列保留那些未处理的链接，直到一些正在执行的任务完成。

### 传统方式

怎么使一个爬虫并发？传统的做法是创建一个线程池，每个线程使用一个套接字在一段时间内负责一个网页的下载。比如，下载 xkcd.com 网站的一个网页：

```
def fetch(url):
    sock = socket.socket()
    sock.connect(('xkcd.com', 80))
    request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)
    sock.send(request.encode('ascii'))
    response = b''
    chunk = sock.recv(4096)
    while chunk:
        response += chunk
        chunk = sock.recv(4096)

    # Page is now downloaded.
    links = parse_links(response)
    q.add(links)

```

套接字操作默认是阻塞的：当一个线程调用一个类似 `connect` 和 `recv` 方法时，它会阻塞，直到操作完成。（即使是 `send` 也能被阻塞，比如接收端在接受外发消息时缓慢而系统的外发数据缓存已经满了的情况下）因此，为了同一时间内下载多个网页，我们需要很多线程。一个复杂的应用会通过线程池保持空闲的线程来分摊创建线程的开销。同样的做法也适用于套接字，使用连接池。

到目前为止，使用线程的是成本昂贵的，操作系统对一个进程、一个用户、一台机器能使用线程做了不同的硬性限制。在 作者 Jesse 的系统中，一个 Python 线程需要 50K 的内存，开启上万个线程就会失败。每个线程的开销和系统的限制就是这种方式的瓶颈所在。

在 Dan Kegel 那一篇很有影响力的文章“[The C10K problem](http://www.kegel.com/c10k.html)”中，它提出了多线程方式在 I/O 并发上的局限性。他在开始写道，

>
> 网络服务器到了要同时处理成千上万的客户的时代了，你不这样认为么？毕竟，现在网络规模很大了。
>
>
>

Kegel 在 1999 年创造出“C10K”这个术语。一万个连接在今天看来还是可接受的，但是问题依然存在，只不过大小不同。回到那时候，对于 C10K 问题，每个连接启一个线程是不切实际的。现在这个限制已经成指数级增长。确实，我们的玩具网络爬虫使用线程也可以工作的很好。但是，对于有着千万级连接的大规模应用来说，限制依然存在：它会消耗掉所有线程，即使套接字还够用。那么我们该如何解决这个问题？

### 异步

异步 I/O 框架在一个线程中完成并发操作。让我们看看这是怎么做到的。

异步框架使用*非阻塞*套接字。异步爬虫中，我们在发起到服务器的连接前把套接字设为非阻塞：

```
sock = socket.socket()
sock.setblocking(False)
try:
    sock.connect(('xkcd.com', 80))
except BlockingIOError:
    pass

```

对一个非阻塞套接字调用 `connect` 方法会立即抛出异常，即使它可以正常工作。这个异常复现了底层 C 语言函数令人厌烦的行为，它把 `errno` 设置为 `EINPROGRESS`，告诉你操作已经开始。

现在我们的爬虫需要一种知道连接何时建立的方法，这样它才能发送 HTTP 请求。我们可以简单地使用循环来重试：

```
request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)
encoded = request.encode('ascii')

while True:
    try:
        sock.send(encoded)
        break  # Done.
    except OSError as e:
        pass

print('sent')

```

这种方法不仅消耗 CPU，也不能有效的等待*多个*套接字。在远古时代，BSD Unix 的解决方法是 `select`，这是一个 C 函数，它在一个或一组非阻塞套接字上等待事件发生。现在，互联网应用大量连接的需求，导致 `select` 被 `poll` 所代替，在 BSD 上的实现是 `kqueue` ，在 Linux 上是 `epoll`。它们的 API 和 `select` 相似，但在大数量的连接中也能有较好的性能。

Python 3.4 的 `DefaultSelector` 会使用你系统上最好的 `select` 类函数。要注册一个网络 I/O 事件的提醒，我们会创建一个非阻塞套接字，并使用默认 selector 注册它。

```
from selectors import DefaultSelector, EVENT_WRITE

selector = DefaultSelector()

sock = socket.socket()
sock.setblocking(False)
try:
    sock.connect(('xkcd.com', 80))
except BlockingIOError:
    pass

def connected():
    selector.unregister(sock.fileno())
    print('connected!')

selector.register(sock.fileno(), EVENT_WRITE, connected)

```

我们不理会这个伪造的错误，调用 `selector.register`，传递套接字文件描述符和一个表示我们想要监听什么事件的常量表达式。为了当连接建立时收到提醒，我们使用 `EVENT_WRITE` ：它表示什么时候这个套接字可写。我们还传递了一个 Python 函数 `connected`，当对应事件发生时被调用。这样的函数被称为*回调*。

在一个循环中，selector 接收到 I/O 提醒时我们处理它们。

```
def loop():
    while True:
        events = selector.select()
        for event_key, event_mask in events:
            callback = event_key.data
            callback()

```

`connected` 回调函数被保存在 `event_key.data` 中，一旦这个非阻塞套接字建立连接，它就会被取出来执行。

不像我们前面那个快速轮转的循环，这里的 `select` 调用会暂停，等待下一个 I/O 事件，接着执行等待这些事件的回调函数。没有完成的操作会保持挂起，直到进到下一个事件循环时执行。

到目前为止我们展现了什么？我们展示了如何开始一个 I/O 操作和当操作准备好时调用回调函数。异步*框架*，它在单线程中执行并发操作，其建立在两个功能之上，非阻塞套接字和事件循环。

我们这里达成了“<ruby> 并发性 <rp>  （ </rp> <rt>  concurrency </rt> <rp>  ） </rp></ruby>”，但不是传统意义上的“<ruby> 并行性 <rp>  （ </rp> <rt>  parallelism </rt> <rp>  ） </rp></ruby>”。也就是说，我们构建了一个可以进行重叠 I/O 的微小系统，它可以在其它操作还在进行的时候就开始一个新的操作。它实际上并没有利用多核来并行执行计算。这个系统是用于解决<ruby> I/O 密集 <rp>  （ </rp> <rt>  I/O-bound </rt> <rp>  ） </rp></ruby>问题的，而不是解决 <ruby> CPU 密集 <rp>  （ </rp> <rt>  CPU-bound </rt> <rp>  ） </rp></ruby>问题的。（Python 的全局解释器锁禁止在一个进程中以任何方式并行执行 Python 代码。在 Python 中并行化 CPU 密集的算法需要多个进程，或者以将该代码移植为 C 语言并行版本。但是这是另外一个话题了。）

所以，我们的事件循环在并发 I/O 上是有效的，因为它并不用为每个连接拨付线程资源。但是在我们开始前，我们需要澄清一个常见的误解：异步比多线程快。通常并不是这样的，事实上，在 Python 中，在处理少量非常活跃的连接时，像我们这样的事件循环是慢于多线程的。在运行时环境中是没有全局解释器锁的，在同样的负载下线程会执行的更好。异步 I/O 真正适用于事件很少、有许多缓慢或睡眠的连接的应用程序。（Jesse 在“[什么是异步，它如何工作，什么时候该用它？](http://pyvideo.org/video/2565/what-is-async-how-does-it-work-and-when-should)”一文中指出了异步所适用和不适用的场景。Mike Bayer 在“[异步 Python 和数据库](http://techspot.zzzeek.org/2015/02/15/asynchronous-python-and-databases/)”一文中比较了不同负载情况下异步 I/O 和多线程的不同。）

### 回调

用我们刚刚建立的异步框架，怎么才能完成一个网络爬虫？即使是一个简单的网页下载程序也是很难写的。

首先，我们有一个尚未获取的 URL 集合，和一个已经解析过的 URL 集合。

```
urls_todo = set(['/'])
seen_urls = set(['/'])

```

`seen_urls` 集合包括 `urls_todo` 和已经完成的 URL。用根 URL `/` 初始化它们。

获取一个网页需要一系列的回调。在套接字连接建立时会触发 `connected` 回调，它向服务器发送一个 GET 请求。但是它要等待响应，所以我们需要注册另一个回调函数；当该回调被调用，它仍然不能读取到完整的请求时，就会再一次注册回调，如此反复。

让我们把这些回调放在一个 `Fetcher` 对象中，它需要一个 URL，一个套接字，还需要一个地方保存返回的字节：

```
class Fetcher:
    def __init__(self, url):
        self.response = b''  # Empty array of bytes.
        self.url = url
        self.sock = None

```

我们的入口点在 `Fetcher.fetch`：

```
    # Method on Fetcher class.
    def fetch(self):
        self.sock = socket.socket()
        self.sock.setblocking(False)
        try:
            self.sock.connect(('xkcd.com', 80))
        except BlockingIOError:
            pass

        # Register next callback.
        selector.register(self.sock.fileno(),
                          EVENT_WRITE,
                          self.connected)

```

`fetch` 方法从连接一个套接字开始。但是要注意这个方法在连接建立前就返回了。它必须将控制返回到事件循环中等待连接建立。为了理解为什么要这样做，假设我们程序的整体结构如下：

```
# Begin fetching http://xkcd.com/353/
fetcher = Fetcher('/353/')
fetcher.fetch()

while True:
    events = selector.select()
    for event_key, event_mask in events:
        callback = event_key.data
        callback(event_key, event_mask)

```

当调用 `select` 函数后，所有的事件提醒才会在事件循环中处理，所以 `fetch` 必须把控制权交给事件循环，这样我们的程序才能知道什么时候连接已建立，接着循环调用 `connected` 回调，它已经在上面的 `fetch` 方法中注册过。

这里是我们的 `connected` 方法的实现：

```
    # Method on Fetcher class.
    def connected(self, key, mask):
        print('connected!')
        selector.unregister(key.fd)
        request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(self.url)
        self.sock.send(request.encode('ascii'))

        # Register the next callback.
        selector.register(key.fd,
                          EVENT_READ,
                          self.read_response)

```

这个方法发送一个 GET 请求。一个真正的应用会检查 `send` 的返回值，以防所有的信息没能一次发送出去。但是我们的请求很小，应用也不复杂。它只是简单的调用 `send`，然后等待响应。当然，它必须注册另一个回调并把控制权交给事件循环。接下来也是最后一个回调函数 `read_response`，它处理服务器的响应：

```
    # Method on Fetcher class.
    def read_response(self, key, mask):
        global stopped

        chunk = self.sock.recv(4096)  # 4k chunk size.
        if chunk:
            self.response += chunk
        else:
            selector.unregister(key.fd)  # Done reading.
            links = self.parse_links()

            # Python set-logic:
            for link in links.difference(seen_urls):
                urls_todo.add(link)
                Fetcher(link).fetch()  # <- New Fetcher.

            seen_urls.update(links)
            urls_todo.remove(self.url)
            if not urls_todo:
                stopped = True

```

这个回调在每次 `selector` 发现套接字*可读*时被调用，可读有两种情况：套接字接受到数据或它被关闭。

这个回调函数从套接字读取 4K 数据。如果不到 4k，那么有多少读多少。如果比 4K 多，`chunk` 中只包 4K 数据并且这个套接字保持可读，这样在事件循环的下一个周期，会再次回到这个回调函数。当响应完成时，服务器关闭这个套接字，`chunk` 为空。

这里没有展示的 `parse_links` 方法，它返回一个 URL 集合。我们为每个新的 URL 启动一个 fetcher。注意一个使用异步回调方式编程的好处：我们不需要为共享数据加锁，比如我们往 `seen_urls` 增加新链接时。这是一种非抢占式的多任务，它不会在我们代码中的任意一个地方被打断。

我们增加了一个全局变量 `stopped`，用它来控制这个循环：

```
stopped = False

def loop():
    while not stopped:
        events = selector.select()
        for event_key, event_mask in events:
            callback = event_key.data
            callback()

```

一旦所有的网页被下载下来，fetcher 停止这个事件循环，程序退出。

这个例子让异步编程的一个问题明显的暴露出来：意大利面代码。

我们需要某种方式来表达一系列的计算和 I/O 操作，并且能够调度多个这样的系列操作让它们并发的执行。但是，没有线程你不能把这一系列操作写在一个函数中：当函数开始一个 I/O 操作，它明确的把未来所需的状态保存下来，然后返回。你需要考虑如何写这个状态保存的代码。

让我们来解释下这到底是什么意思。先来看一下在线程中使用通常的阻塞套接字来获取一个网页时是多么简单。

```
# Blocking version.
def fetch(url):
    sock = socket.socket()
    sock.connect(('xkcd.com', 80))
    request = 'GET {} HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'.format(url)
    sock.send(request.encode('ascii'))
    response = b''
    chunk = sock.recv(4096)
    while chunk:
        response += chunk
        chunk = sock.recv(4096)

    # Page is now downloaded.
    links = parse_links(response)
    q.add(links)

```

在一个套接字操作和下一个操作之间这个函数到底记住了什么状态？它有一个套接字，一个 URL 和一个可增长的 `response`。运行在线程中的函数使用编程语言的基本功能来在栈中的局部变量保存这些临时状态。这样的函数也有一个“continuation”——它会在 I/O 结束后执行这些代码。运行时环境通过线程的指令指针来记住这个 continuation。你不必考虑怎么在 I/O 操作后恢复局部变量和这个 continuation。语言本身的特性帮你解决。

但是用一个基于回调的异步框架时，这些语言特性不能提供一点帮助。当等待 I/O 操作时，一个函数必须明确的保存它的状态，因为它会在 I/O 操作完成之前返回并清除栈帧。在我们基于回调的例子中，作为局部变量的替代，我们把 `sock` 和 `response` 作为 Fetcher 实例 `self` 的属性来存储。而作为指令指针的替代，它通过注册 `connected` 和 `read_response` 回调来保存它的 continuation。随着应用功能的增长，我们需要手动保存的回调的复杂性也会增加。如此繁复的记账式工作会让编码者感到头痛。

更糟糕的是，当我们的回调函数抛出异常会发生什么？假设我们没有写好 `parse_links` 方法，它在解析 HTML 时抛出异常：

```
Traceback (most recent call last):
  File "loop-with-callbacks.py", line 111, in <module>
    loop()
  File "loop-with-callbacks.py", line 106, in loop
    callback(event_key, event_mask)
  File "loop-with-callbacks.py", line 51, in read_response
    links = self.parse_links()
  File "loop-with-callbacks.py", line 67, in parse_links
    raise Exception('parse error')
Exception: parse error

```

这个堆栈回溯只能显示出事件循环调用了一个回调。我们不知道是什么导致了这个错误。这条链的两边都被破坏：不知道从哪来也不知到哪去。这种丢失上下文的现象被称为“<ruby> 堆栈撕裂 <rp>  （ </rp> <rt>  stack ripping </rt> <rp>  ） </rp></ruby>”，经常会导致无法分析原因。它还会阻止我们为回调链设置异常处理，即那种用“try / except”块封装函数调用及其调用树。（对于这个问题的更复杂的解决方案，参见 <http://www.tornadoweb.org/en/stable/stack_context.html> ）

所以，除了关于多线程和异步哪个更高效的长期争议之外，还有一个关于这两者之间的争论：谁更容易跪了。如果在同步上出现失误，线程更容易出现数据竞争的问题，而回调因为"<ruby> 堆栈撕裂 <rp>  （ </rp> <rt>  stack ripping </rt> <rp>  ） </rp></ruby>"问题而非常难于调试。

（题图素材来自：[ruth-tay.deviantart.com](http://ruth-tay.deviantart.com/art/Bearded-Vulture-553800995)）

---

via: <http://aosabook.org/en/500L/pages/a-web-crawler-with-asyncio-coroutines.html>

作者：A. Jesse Jiryu Davis , Guido van Rossum 译者：[qingyunha](https://github.com/qingyunha) 校对：[wxy](https://github.com/wxy)

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


一个使用 asyncio 协程的网络爬虫（二）

Summary: 然后，由于 Python 的协程不仅有效而且可扩展，我们将用 Python 的生成器函数实现一个简单的协程。

# 一个使用 asyncio 协程的网络爬虫（二）

>
> 本文作者：
>
>
> A. Jesse Jiryu Davis 是纽约 MongoDB 的工程师。他编写了异步 MongoDB Python 驱动程序 Motor，也是 MongoDB C 驱动程序的开发领袖和 PyMongo 团队成员。 他也为 asyncio 和 Tornado 做了贡献，在 <http://emptysqua.re> 上写作。
>
>
> Guido van Rossum 是主流编程语言 Python 的创造者，Python 社区称他为 BDFL （仁慈的终生大独裁者 (Benevolent Dictator For Life)）——这是一个来自 Monty Python 短剧的称号。他的主页是 [http://www.python.org/~guido/](http://www.python.org/%7Eguido/) 。
>
>
>

###

### 协程

还记得我们对你许下的承诺么？我们可以写出这样的异步代码，它既有回调方式的高效，也有多线程代码的简洁。这个结合是同过一种称为<ruby> 协程 <rp>  （ </rp> <rt>  coroutine </rt> <rp>  ） </rp></ruby>的模式来实现的。使用 Python3.4 标准库 asyncio 和一个叫“aiohttp”的包，在协程中获取一个网页是非常直接的（ `@asyncio.coroutine` 修饰符并非魔法。事实上，如果它修饰的是一个生成器函数，并且没有设置 `PYTHONASYNCIODEBUG` 环境变量的话，这个修饰符基本上没啥用。它只是为了框架的其它部分方便，设置了一个属性 `_is_coroutine` 而已。也可以直接使用 asyncio 和裸生成器，而没有 `@asyncio.coroutine` 修饰符）：

```
    @asyncio.coroutine
    def fetch(self, url):
        response = yield from self.session.get(url)
        body = yield from response.read()

```

它也是可扩展的。在作者 Jesse 的系统上，与每个线程 50k 内存相比，一个 Python 协程只需要 3k 内存。Python 很容易就可以启动上千个协程。

协程的概念可以追溯到计算机科学的远古时代，它很简单，一个可以暂停和恢复的子过程。线程是被操作系统控制的抢占式多任务，而协程的多任务是可合作的，它们自己选择什么时候暂停去执行下一个协程。

有很多协程的实现。甚至在 Python 中也有几种。Python 3.4 标准库 asyncio 中的协程是建立在生成器之上的，这是一个 Future 类和“yield from”语句。从 Python 3.5 开始，协程变成了语言本身的特性（[“PEP 492 Coroutines with async and await syntax”](https://www.python.org/dev/peps/pep-0492/) 中描述了 Python 3.5 内置的协程）。然而，理解 Python 3.4 中这个通过语言原有功能实现的协程，是我们处理 Python 3.5 中原生协程的基础。

要解释 Python 3.4 中基于生成器的协程，我们需要深入生成器的方方面面，以及它们是如何在 asyncio 中用作协程的。我很高兴就此写点东西，想必你也希望继续读下去。我们解释了基于生成器的协程之后，就会在我们的异步网络爬虫中使用它们。

### 生成器如何工作

在你理解生成器之前，你需要知道普通的 Python 函数是怎么工作的。正常情况下，当一个函数调用一个子过程，这个被调用函数获得控制权，直到它返回或者有异常发生，才把控制权交给调用者：

```
>>> def foo():
...     bar()
...
>>> def bar():
...     pass

```

标准的 Python 解释器是用 C 语言写的。一个 Python 函数被调用所对应的 C 函数是 `PyEval_EvalFrameEx`。它获得一个 Python 栈帧结构并在这个栈帧的上下文中执行 Python 字节码。这里是 `foo` 函数的字节码：

```
>>> import dis
>>> dis.dis(foo)
  2           0 LOAD_GLOBAL              0 (bar)
              3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
              6 POP_TOP
              7 LOAD_CONST               0 (None)
             10 RETURN_VALUE

```

`foo` 函数在它栈中加载 `bar` 函数并调用它，然后把 `bar` 的返回值从栈中弹出，加载 `None` 值到堆栈并返回。

当 `PyEval_EvalFrameEx` 遇到 `CALL_FUNCTION` 字节码时，它会创建一个新的栈帧，并用这个栈帧递归的调用 `PyEval_EvalFrameEx` 来执行 `bar` 函数。

非常重要的一点是，Python 的栈帧在堆中分配！Python 解释器是一个标准的 C 程序，所以它的栈帧是正常的栈帧。但是 Python 的栈帧是在堆中处理。这意味着 Python 栈帧在函数调用结束后依然可以存在。我们在 `bar` 函数中保存当前的栈帧，交互式的看看这种现象：

```
>>> import inspect
>>> frame = None
>>> def foo():
...     bar()
...
>>> def bar():
...     global frame
...     frame = inspect.currentframe()
...
>>> foo()
>>> # The frame was executing the code for 'bar'.
>>> frame.f_code.co_name
'bar'
>>> # Its back pointer refers to the frame for 'foo'.
>>> caller_frame = frame.f_back
>>> caller_frame.f_code.co_name
'foo'

```

现在该说 Python 生成器了，它使用同样构件——代码对象和栈帧——去完成一个不可思议的任务。

这是一个生成器函数：

```
>>> def gen_fn():
...     result = yield 1
...     print('result of yield: {}'.format(result))
...     result2 = yield 2
...     print('result of 2nd yield: {}'.format(result2))
...     return 'done'
...

```

在 Python 把 `gen_fn` 编译成字节码的过程中，一旦它看到 `yield` 语句就知道这是一个生成器函数而不是普通的函数。它就会设置一个标志来记住这个事实：

```
>>> # The generator flag is bit position 5.
>>> generator_bit = 1 << 5
>>> bool(gen_fn.__code__.co_flags & generator_bit)
True

```

当你调用一个生成器函数，Python 看到这个标志，就不会实际运行它而是创建一个生成器：

```
>>> gen = gen_fn()
>>> type(gen)
<class 'generator'>

```

Python 生成器封装了一个栈帧和函数体代码的引用：

```
>>> gen.gi_code.co_name
'gen_fn'

```

所有通过调用 `gen_fn` 的生成器指向同一段代码，但都有各自的栈帧。这些栈帧不再任何一个C函数栈中，而是在堆空间中等待被使用：

栈帧中有一个指向“最后执行指令”的指针。初始化为 -1，意味着它没开始运行：

```
>>> gen.gi_frame.f_lasti
-1

```

当我们调用 `send` 时，生成器一直运行到第一个 `yield` 语句处停止，并且 `send` 返回 1，因为这是 `gen` 传递给 `yield` 表达式的值。

```
>>> gen.send(None)
1

```

现在，生成器的指令指针是 3，所编译的Python 字节码一共有 56 个字节：

```
>>> gen.gi_frame.f_lasti
3
>>> len(gen.gi_code.co_code)
56

```

这个生成器可以在任何时候、任何函数中恢复运行，因为它的栈帧并不在真正的栈中，而是堆中。在调用链中它的位置也是不固定的，它不必遵循普通函数先进后出的顺序。它像云一样自由。

我们可以传递一个值 `hello` 给生成器，它会成为 `yield` 语句的结果，并且生成器会继续运行到第二个 `yield` 语句处。

```
>>> gen.send('hello')
result of yield: hello
2

```

现在栈帧中包含局部变量 `result`：

```
>>> gen.gi_frame.f_locals
{'result': 'hello'}

```

其它从 `gen_fn` 创建的生成器有着它自己的栈帧和局部变量。

当我们再一次调用 `send`，生成器继续从第二个 `yield` 开始运行，以抛出一个特殊的 `StopIteration` 异常为结束。

```
>>> gen.send('goodbye')
result of 2nd yield: goodbye
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration: done

```

这个异常有一个值 `"done"`，它就是生成器的返回值。

### 使用生成器构建协程

所以生成器可以暂停，可以给它一个值让它恢复，并且它还有一个返回值。这些特性看起来很适合去建立一个不使用那种乱糟糟的意面似的回调异步编程模型。我们想创造一个这样的“协程”：一个在程序中可以和其他过程合作调度的过程。我们的协程将会是标准库 `asyncio` 中协程的一个简化版本，我们将使用生成器，futures 和 `yield from` 语句。

首先，我们需要一种方法去代表协程所需要等待的 future 事件。一个简化的版本是：

```
class Future:
    def __init__(self):
        self.result = None
        self._callbacks = []

    def add_done_callback(self, fn):
        self._callbacks.append(fn)

    def set_result(self, result):
        self.result = result
        for fn in self._callbacks:
            fn(self)

```

一个 future 初始化为“未解决的”，它通过调用 `set_result` 来“解决”。（这个 future 缺少很多东西，比如说，当这个 future 解决后，<ruby> 生成 <rp>  （ </rp> <rt>  yield </rt> <rp>  ） </rp></ruby>的协程应该马上恢复而不是暂停，但是在我们的代码中却不没有这样做。参见 asyncio 的 Future 类以了解其完整实现。）

让我们用 future 和协程来改写我们的 fetcher。我们之前用回调写的 `fetch` 如下：

```
class Fetcher:
    def fetch(self):
        self.sock = socket.socket()
        self.sock.setblocking(False)
        try:
            self.sock.connect(('xkcd.com', 80))
        except BlockingIOError:
            pass
        selector.register(self.sock.fileno(),
                          EVENT_WRITE,
                          self.connected)

    def connected(self, key, mask):
        print('connected!')
        # And so on....

```

`fetch` 方法开始连接一个套接字，然后注册 `connected` 回调函数，它会在套接字建立连接后调用。现在我们使用协程把这两步合并：

```
    def fetch(self):
        sock = socket.socket()
        sock.setblocking(False)
        try:
            sock.connect(('xkcd.com', 80))
        except BlockingIOError:
            pass

        f = Future()

        def on_connected():
            f.set_result(None)

        selector.register(sock.fileno(),
                          EVENT_WRITE,
                          on_connected)
        yield f
        selector.unregister(sock.fileno())
        print('connected!')

```

现在，`fetch` 是一个生成器，因为它有一个 `yield` 语句。我们创建一个未决的 future，然后 yield 它，暂停 `fetch` 直到套接字连接建立。内联函数 `on_connected` 解决这个 future。

但是当 future 被解决，谁来恢复这个生成器？我们需要一个协程*驱动器*。让我们叫它 “task”:

```
class Task:
    def __init__(self, coro):
        self.coro = coro
        f = Future()
        f.set_result(None)
        self.step(f)

    def step(self, future):
        try:
            next_future = self.coro.send(future.result)
        except StopIteration:
            return

        next_future.add_done_callback(self.step)

# Begin fetching http://xkcd.com/353/
fetcher = Fetcher('/353/')
Task(fetcher.fetch())

loop()

```

task 通过传递一个 None 值给 `fetch` 来启动它。`fetch` 运行到它 yeild 出一个 future，这个 future 被作为 `next_future` 而捕获。当套接字连接建立，事件循环运行回调函数 `on_connected`，这里 future 被解决，`step` 被调用，`fetch` 恢复运行。

### 用 yield from 重构协程

一旦套接字连接建立，我们就可以发送 HTTP GET 请求，然后读取服务器响应。不再需要哪些分散在各处的回调函数，我们把它们放在同一个生成器函数中：

```
    def fetch(self):
        # ... connection logic from above, then:
        sock.send(request.encode('ascii'))

        while True:
            f = Future()

            def on_readable():
                f.set_result(sock.recv(4096))

            selector.register(sock.fileno(),
                              EVENT_READ,
                              on_readable)
            chunk = yield f
            selector.unregister(sock.fileno())
            if chunk:
                self.response += chunk
            else:
                # Done reading.
                break

```

从套接字中读取所有信息的代码看起来很通用。我们能不把它从 `fetch` 中提取成一个子过程？现在该 Python 3 热捧的 `yield from` 登场了。它能让一个生成器*委派*另一个生成器。

让我们先回到原来那个简单的生成器例子：

```
>>> def gen_fn():
...     result = yield 1
...     print('result of yield: {}'.format(result))
...     result2 = yield 2
...     print('result of 2nd yield: {}'.format(result2))
...     return 'done'
...

```

为了从其他生成器调用这个生成器，我们使用 `yield from` 委派它:

```
>>> # Generator function:
>>> def caller_fn():
...     gen = gen_fn()
...     rv = yield from gen
...     print('return value of yield-from: {}'
...           .format(rv))
...
>>> # Make a generator from the
>>> # generator function.
>>> caller = caller_fn()

```

这个 `caller` 生成器的行为的和它委派的生成器 `gen` 表现的完全一致：

```
>>> caller.send(None)
1
>>> caller.gi_frame.f_lasti
15
>>> caller.send('hello')
result of yield: hello
2
>>> caller.gi_frame.f_lasti  # Hasn't advanced.
15
>>> caller.send('goodbye')
result of 2nd yield: goodbye
return value of yield-from: done
Traceback (most recent call last):
  File "<input>", line 1, in <module>
StopIteration

```

当 `caller` 自 `gen` 生成（`yield`），`caller` 就不再前进。注意到 `caller` 的指令指针保持15不变，就是 `yield from` 的地方，即使内部的生成器 `gen` 从一个 yield 语句运行到下一个 yield，它始终不变。（事实上，这就是“yield from”在 CPython 中工作的具体方式。函数会在执行每个语句之前提升其指令指针。但是在外部生成器执行“yield from”后，它会将其指令指针减一，以保持其固定在“yield form”语句上。然后其生成其 caller。这个循环不断重复，直到内部生成器抛出 StopIteration，这里指向外部生成器最终允许它自己进行到下一条指令的地方。）从 `caller` 外部来看，我们无法分辨 yield 出的值是来自 `caller` 还是它委派的生成器。而从 `gen` 内部来看，我们也不能分辨传给它的值是来自 `caller` 还是 `caller` 的外面。`yield from` 语句是一个光滑的管道，值通过它进出 `gen`，一直到 `gen` 结束。

协程可以用 `yield from` 把工作委派给子协程，并接收子协程的返回值。注意到上面的 `caller` 打印出“return value of yield-from: done”。当 `gen` 完成后，它的返回值成为 `caller` 中 `yield from` 语句的值。

```
    rv = yield from gen

```

前面我们批评过基于回调的异步编程模式，其中最大的不满是关于 “<ruby> 堆栈撕裂 <rp>  （ </rp> <rt>  stack ripping </rt> <rp>  ） </rp></ruby>”：当一个回调抛出异常，它的堆栈回溯通常是毫无用处的。它只显示出事件循环运行了它，而没有说为什么。那么协程怎么样？

```
>>> def gen_fn():
...     raise Exception('my error')
>>> caller = caller_fn()
>>> caller.send(None)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 3, in caller_fn
  File "<input>", line 2, in gen_fn
Exception: my error

```

这还是非常有用的，当异常抛出时，堆栈回溯显示出 `caller_fn` 委派了 `gen_fn`。令人更欣慰的是，你可以在一次异常处理器中封装这个调用到一个子过程中，像正常函数一样：

```
>>> def gen_fn():
...     yield 1
...     raise Exception('uh oh')
...
>>> def caller_fn():
...     try:
...         yield from gen_fn()
...     except Exception as exc:
...         print('caught {}'.format(exc))
...
>>> caller = caller_fn()
>>> caller.send(None)
1
>>> caller.send('hello')
caught uh oh

```

所以我们可以像提取子过程一样提取子协程。让我们从 fetcher 中提取一些有用的子协程。我们先写一个可以读一块数据的协程 `read`：

```
def read(sock):
    f = Future()

    def on_readable():
        f.set_result(sock.recv(4096))

    selector.register(sock.fileno(), EVENT_READ, on_readable)
    chunk = yield f  # Read one chunk.
    selector.unregister(sock.fileno())
    return chunk

```

在 `read` 的基础上，`read_all` 协程读取整个信息：

```
def read_all(sock):
    response = []
    # Read whole response.
    chunk = yield from read(sock)
    while chunk:
        response.append(chunk)
        chunk = yield from read(sock)

    return b''.join(response)

```

如果你换个角度看，抛开 `yield form` 语句的话，它们就像在做阻塞 I/O 的普通函数一样。但是事实上，`read` 和 `read_all` 都是协程。`yield from` `read` 暂停 `read_all` 直到 I/O 操作完成。当 `read_all` 暂停时，asyncio 的事件循环正在做其它的工作并等待其他的 I/O 操作。`read` 在下次循环中当事件就绪，完成 I/O 操作时，`read_all` 恢复运行。

最终，`fetch` 调用了 `read_all`：

```
class Fetcher:
    def fetch(self):
         # ... connection logic from above, then:
        sock.send(request.encode('ascii'))
        self.response = yield from read_all(sock)

```

神奇的是，Task 类不需要做任何改变，它像以前一样驱动外部的 `fetch` 协程：

```
Task(fetcher.fetch())
loop()

```

当 `read` yield 一个 future 时，task 从 `yield from` 管道中接收它，就像这个 future 直接从 `fetch` yield 一样。当循环解决一个 future 时，task 把它的结果送给 `fetch`，通过管道，`read` 接受到这个值，这完全就像 task 直接驱动 `read` 一样：

为了完善我们的协程实现，我们再做点打磨：当等待一个 future 时，我们的代码使用 yield；而当委派一个子协程时，使用 yield from。不管是不是协程，我们总是使用 yield form 会更精炼一些。协程并不需要在意它在等待的东西是什么类型。

在 Python 中，我们从生成器和迭代器的高度相似中获得了好处，将生成器进化成 caller，迭代器也可以同样获得好处。所以，我们可以通过特殊的实现方式来迭代我们的 Future 类：

```
    # Method on Future class.
    def __iter__(self):
        # Tell Task to resume me here.
        yield self
        return self.result

```

future 的 `__iter__` 方法是一个 yield 它自身的一个协程。当我们将代码替换如下时：

```
# f is a Future.
yield f

```

以及……：

```
# f is a Future.
yield from f

```

……结果是一样的！驱动 Task 从它的调用 `send` 中接收 future，并当 future 解决后，它发回新的结果给该协程。

在每个地方都使用 `yield from` 的好处是什么？为什么比用 `field` 等待 future 并用 `yield from` 委派子协程更好？之所以更好的原因是，一个方法可以自由地改变其实行而不影响到其调用者：它可以是一个当 future 解决后返回一个值的普通方法，也可以是一个包含 `yield from` 语句并返回一个值的协程。无论是哪种情况，调用者仅需要 `yield from` 该方法以等待结果就行。

亲爱的读者，我们已经完成了对 asyncio 协程探索。我们深入观察了生成器的机制，实现了简单的 future 和 task。我们指出协程是如何利用两个世界的优点：比线程高效、比回调清晰的并发 I/O。当然真正的 asyncio 比我们这个简化版本要复杂的多。真正的框架需要处理zero-copy I/0、公平调度、异常处理和其他大量特性。

使用 asyncio 编写协程代码比你现在看到的要简单的多。在前面的代码中，我们从基本原理去实现协程，所以你看到了回调，task 和 future，甚至非阻塞套接字和 `select` 调用。但是当用 asyncio 编写应用，这些都不会出现在你的代码中。我们承诺过，你可以像这样下载一个网页：

```
    @asyncio.coroutine
    def fetch(self, url):
        response = yield from self.session.get(url)
        body = yield from response.read()

```

对我们的探索还满意么？回到我们原始的任务：使用 asyncio 写一个网络爬虫。

（题图素材来自：[ruth-tay.deviantart.com](http://ruth-tay.deviantart.com/art/Bearded-Vulture-553800995)）

---

via: <http://aosabook.org/en/500L/pages/a-web-crawler-with-asyncio-coroutines.html>

作者：A. Jesse Jiryu Davis , Guido van Rossum 译者：[qingyunha](https://github.com/qingyunha) 校对：[wxy](https://github.com/wxy)

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


一个使用 asyncio 协程的网络爬虫（三）

Summary: 在最后一个阶段，我们将使用 Python 标准库“asyncio”中功能完整的协程， 并通过异步队列完成这个网络爬虫。

# 一个使用 asyncio 协程的网络爬虫（三）

>
> 本文作者：
>
>
> A. Jesse Jiryu Davis 是纽约 MongoDB 的工程师。他编写了异步 MongoDB Python 驱动程序 Motor，也是 MongoDB C 驱动程序的开发领袖和 PyMongo 团队成员。 他也为 asyncio 和 Tornado 做了贡献，在 <http://emptysqua.re> 上写作。
>
>
> Guido van Rossum 是主流编程语言 Python 的创造者，Python 社区称他为 BDFL （仁慈的终生大独裁者 (Benevolent Dictator For Life)）——这是一个来自 Monty Python 短剧的称号。他的主页是 [http://www.python.org/~guido/](http://www.python.org/%7Eguido/) 。
>
>
>

### 使用协程

我们将从描述爬虫如何工作开始。现在是时候用 asynio 去实现它了。

我们的爬虫从获取第一个网页开始，解析出链接并把它们加到队列中。此后它开始傲游整个网站，并发地获取网页。但是由于客户端和服务端的负载限制，我们希望有一个最大数目的运行的 worker，不能再多。任何时候一个 worker 完成一个网页的获取，它应该立即从队列中取出下一个链接。我们会遇到没有那么多事干的时候，所以一些 worker 必须能够暂停。一旦又有 worker 获取一个有很多链接的网页，队列会突增，暂停的 worker 立马被唤醒干活。最后，当任务完成后我们的程序必须马上退出。

假如你的 worker 是线程，怎样去描述你的爬虫算法？我们可以使用 Python 标准库中的[同步队列](https://docs.python.org/3/library/queue.html)。每次有新的一项加入，队列增加它的 “tasks” 计数器。线程 worker 完成一个任务后调用 `task_done`。主线程阻塞在 `Queue.join`，直到“tasks”计数器与 `task_done` 调用次数相匹配，然后退出。

协程通过 asyncio 队列，使用和线程一样的模式来实现！首先我们[导入它](https://docs.python.org/3/library/asyncio-sync.html)：

```
try:
    from asyncio import JoinableQueue as Queue
except ImportError:
    # In Python 3.5, asyncio.JoinableQueue is
    # merged into Queue.
    from asyncio import Queue

```

我们把 worker 的共享状态收集在一个 crawler 类中，主要的逻辑写在 `crawl` 方法中。我们在一个协程中启动 `crawl`,运行 asyncio 的事件循环直到 `crawl` 完成：

```
loop = asyncio.get_event_loop()

crawler = crawling.Crawler('http://xkcd.com',
                           max_redirect=10)

loop.run_until_complete(crawler.crawl())

```

crawler 用一个根 URL 和最大重定向数 `max_redirect` 来初始化，它把 `(URL, max_redirect)` 序对放入队列中。（为什么要这样做，请看下文）

```
class Crawler:
    def __init__(self, root_url, max_redirect):
        self.max_tasks = 10
        self.max_redirect = max_redirect
        self.q = Queue()
        self.seen_urls = set()

        # aiohttp's ClientSession does connection pooling and
        # HTTP keep-alives for us.
        self.session = aiohttp.ClientSession(loop=loop)

        # Put (URL, max_redirect) in the queue.
        self.q.put((root_url, self.max_redirect))

```

现在队列中未完成的任务数是 1。回到我们的主程序，启动事件循环和 `crawl` 方法：

```
loop.run_until_complete(crawler.crawl())

```

`crawl` 协程把 worker 们赶起来干活。它像一个主线程：阻塞在 `join` 上直到所有任务完成，同时 worker 们在后台运行。

```
    @asyncio.coroutine
    def crawl(self):
        """Run the crawler until all work is done."""
        workers = [asyncio.Task(self.work())
                   for _ in range(self.max_tasks)]

        # When all work is done, exit.
        yield from self.q.join()
        for w in workers:
            w.cancel()

```

如果 worker 是线程，可能我们不会一次把它们全部创建出来。为了避免创建线程的昂贵代价，通常一个线程池会按需增长。但是协程很廉价，我们可以直接把他们全部创建出来。

怎么关闭这个 `crawler` 很有趣。当 `join` 完成，worker 存活但是被暂停：他们等待更多的 URL，所以主协程要在退出之前清除它们。否则 Python 解释器关闭并调用所有对象的析构函数时，活着的 worker 会哭喊到：

```
ERROR:asyncio:Task was destroyed but it is pending!

```

`cancel` 又是如何工作的呢？生成器还有一个我们还没介绍的特点。你可以从外部抛一个异常给它：

```
>>> gen = gen_fn()
>>> gen.send(None)  # Start the generator as usual.
1
>>> gen.throw(Exception('error'))
Traceback (most recent call last):
  File "<input>", line 3, in <module>
  File "<input>", line 2, in gen_fn
Exception: error

```

生成器被 `throw` 恢复，但是它现在抛出一个异常。如过生成器的调用堆栈中没有捕获异常的代码，这个异常被传递到顶层。所以注销一个协程：

```
    # Method of Task class.
    def cancel(self):
        self.coro.throw(CancelledError)

```

任何时候生成器暂停，在某些 `yield from` 语句它恢复并且抛出一个异常。我们在 task 的 `step` 方法中处理注销。

```
    # Method of Task class.
    def step(self, future):
        try:
            next_future = self.coro.send(future.result)
        except CancelledError:
            self.cancelled = True
            return
        except StopIteration:
            return

        next_future.add_done_callback(self.step)

```

现在 task 知道它被注销了，所以当它被销毁时，它不再抱怨。

一旦 `crawl` 注销了 worker，它就退出。同时事件循环看见这个协程结束了（我们后面会见到的），也就退出。

```
loop.run_until_complete(crawler.crawl())

```

`crawl` 方法包含了所有主协程需要做的事。而 worker 则完成从队列中获取 URL、获取网页、解析它们得到新的链接。每个 worker 独立地运行 `work` 协程：

```
    @asyncio.coroutine
    def work(self):
        while True:
            url, max_redirect = yield from self.q.get()

            # Download page and add new links to self.q.
            yield from self.fetch(url, max_redirect)
            self.q.task_done()

```

Python 看见这段代码包含 `yield from` 语句，就把它编译成生成器函数。所以在 `crawl` 方法中，我们调用了 10 次 `self.work`，但并没有真正执行，它仅仅创建了 10 个指向这段代码的生成器对象并把它们包装成 Task 对象。task 接收每个生成器所 yield 的 future，通过调用 `send` 方法，当 future 解决时，用 future 的结果做为 `send` 的参数，来驱动它。由于生成器有自己的栈帧，它们可以独立运行，带有独立的局部变量和指令指针。

worker 使用队列来协调其小伙伴。它这样等待新的 URL：

```
    url, max_redirect = yield from self.q.get()

```

队列的 `get` 方法自身也是一个协程，它一直暂停到有新的 URL 进入队列，然后恢复并返回该条目。

碰巧，这也是当主协程注销 worker 时，最后 crawl 停止，worker 协程暂停的地方。从协程的角度，`yield from` 抛出`CancelledError` 结束了它在循环中的最后旅程。

worker 获取一个网页，解析链接，把新的链接放入队列中，接着调用`task_done`减小计数器。最终一个worker遇到一个没有新链接的网页，并且队列里也没有任务，这次`task_done`的调用使计数器减为0，而`crawl`正阻塞在`join`方法上，现在它就可以结束了。

我们承诺过要解释为什么队列中要使用序对，像这样：

```
# URL to fetch, and the number of redirects left.
('http://xkcd.com/353', 10)

```

新的 URL 的重定向次数是10。获取一个特别的 URL 会重定向一个新的位置。我们减小重定向次数，并把新的 URL 放入队列中。

```
# URL with a trailing slash. Nine redirects left.
('http://xkcd.com/353/', 9)

```

我们使用的 `aiohttp` 默认会跟踪重定向并返回最终结果。但是，我们告诉它不要这样做，爬虫自己来处理重定向，以便它可以合并那些目的相同的重定向路径：如果我们已经在 `self.seen_urls` 看到一个 URL，说明它已经从其他的地方走过这条路了。

crawler 获取“foo”并发现它重定向到了“baz”，所以它会加“baz”到队列和 `seen_urls` 中。如果它获取的下一个页面“bar” 也重定向到“baz”，fetcher 不会再次将 “baz”加入到队列中。如果该响应是一个页面，而不是一个重定向，`fetch` 会解析它的链接，并把新链接放到队列中。

```
    @asyncio.coroutine
    def fetch(self, url, max_redirect):
        # Handle redirects ourselves.
        response = yield from self.session.get(
            url, allow_redirects=False)

        try:
            if is_redirect(response):
                if max_redirect > 0:
                    next_url = response.headers['location']
                    if next_url in self.seen_urls:
                        # We have been down this path before.
                        return

                    # Remember we have seen this URL.
                    self.seen_urls.add(next_url)

                    # Follow the redirect. One less redirect remains.
                    self.q.put_nowait((next_url, max_redirect - 1))
             else:
                 links = yield from self.parse_links(response)
                 # Python set-logic:
                 for link in links.difference(self.seen_urls):
                    self.q.put_nowait((link, self.max_redirect))
                self.seen_urls.update(links)
        finally:
            # Return connection to pool.
            yield from response.release()

```

如果这是多进程代码，就有可能遇到讨厌的竞争条件。比如，一个 worker 检查一个链接是否在 `seen_urls` 中，如果没有它就把这个链接加到队列中并把它放到 `seen_urls` 中。如果它在这两步操作之间被中断，而另一个 worker 解析到相同的链接，发现它并没有出现在 `seen_urls` 中就把它加入队列中。这（至少）导致同样的链接在队列中出现两次，做了重复的工作和错误的统计。

然而，一个协程只在 `yield from` 时才会被中断。这是协程比多线程少遇到竞争条件的关键。多线程必须获得锁来明确的进入一个临界区，否则它就是可中断的。而 Python 的协程默认是不会被中断的，只有它明确 yield 时才主动放弃控制权。

我们不再需要在用回调方式时用的 fetcher 类了。这个类只是不高效回调的一个变通方法：在等待 I/O 时，它需要一个存储状态的地方，因为局部变量并不能在函数调用间保留。倒是 `fetch` 协程可以像普通函数一样用局部变量保存它的状态，所以我们不再需要一个类。

当 `fetch` 完成对服务器响应的处理，它返回到它的调用者 `work`。`work` 方法对队列调用 `task_done`，接着从队列中取出一个要获取的 URL。

当 `fetch` 把新的链接放入队列中，它增加未完成的任务计数器，并停留在主协程，主协程在等待 `q.join`，处于暂停状态。而当没有新的链接并且这是队列中最后一个 URL 时，当 `work 调用`task\_done`，任务计数器变为 0，主协程从`join` 中退出。

与 worker 和主协程一起工作的队列代码像这样（实际的 `asyncio.Queue` 实现在 Future 所展示的地方使用 `asyncio.Event` 。不同之处在于 Event 是可以重置的，而 Future 不能从已解决返回变成待决。）

```
class Queue:
    def __init__(self):
        self._join_future = Future()
        self._unfinished_tasks = 0
        # ... other initialization ...

    def put_nowait(self, item):
        self._unfinished_tasks += 1
        # ... store the item ...

    def task_done(self):
        self._unfinished_tasks -= 1
        if self._unfinished_tasks == 0:
            self._join_future.set_result(None)

    @asyncio.coroutine
    def join(self):
        if self._unfinished_tasks > 0:
            yield from self._join_future

```

主协程 `crawl` yield from `join`。所以当最后一个 worker 把计数器减为 0，它告诉 `crawl` 恢复运行并结束。

旅程快要结束了。我们的程序从 `crawl` 调用开始：

```
loop.run_until_complete(self.crawler.crawl())

```

程序如何结束？因为 `crawl` 是一个生成器函数，调用它返回一个生成器。为了驱动它，asyncio 把它包装成一个 task：

```
class EventLoop:
    def run_until_complete(self, coro):
        """Run until the coroutine is done."""
        task = Task(coro)
        task.add_done_callback(stop_callback)
        try:
            self.run_forever()
        except StopError:
            pass

class StopError(BaseException):
    """Raised to stop the event loop."""

def stop_callback(future):
    raise StopError
```

当这个任务完成，它抛出 `StopError`，事件循环把这个异常当作正常退出的信号。

但是，task 的 `add_done_callbock` 和 `result` 方法又是什么呢？你可能认为 task 就像一个 future，不错，你的直觉是对的。我们必须承认一个向你隐藏的细节，task 是 future。

```
class Task(Future):
    """A coroutine wrapped in a Future."""
```

通常，一个 future 被别人调用 `set_result` 解决。但是 task，当协程结束时，它自己解决自己。记得我们解释过当 Python 生成器返回时，它抛出一个特殊的 `StopIteration` 异常：

```
    # Method of class Task.
    def step(self, future):
        try:
            next_future = self.coro.send(future.result)
        except CancelledError:
            self.cancelled = True
            return
        except StopIteration as exc:

            # Task resolves itself with coro's return
            # value.
            self.set_result(exc.value)
            return

        next_future.add_done_callback(self.step)

```

所以当事件循环调用 `task.add_done_callback(stop_callback)`，它就准备被这个 task 停止。在看一次`run_until_complete`：

```
    # Method of event loop.
    def run_until_complete(self, coro):
        task = Task(coro)
        task.add_done_callback(stop_callback)
        try:
            self.run_forever()
        except StopError:
            pass

```

当 task 捕获 `StopIteration` 并解决自己，这个回调从循环中抛出 `StopError`。循环结束，调用栈回到`run_until_complete`。我们的程序结束。

### 总结

现代的程序越来越多是 I/O 密集型而不是 CPU 密集型。对于这样的程序，Python 的线程在两个方面不合适：全局解释器锁阻止真正的并行计算，并且抢占切换也导致他们更容易出现竞争。异步通常是正确的选择。但是随着基于回调的异步代码增加，它会变得非常混乱。协程是一个更整洁的替代者。它们自然地重构成子过程，有健全的异常处理和栈追溯。

如果我们换个角度看 `yield from` 语句，一个协程看起来像一个传统的做阻塞 I/O 的线程。甚至我们可以采用经典的多线程模式编程，不需要重新发明。因此，与回调相比，协程更适合有经验的多线程的编码者。

但是当我们睁开眼睛关注 `yield from` 语句，我们能看到协程放弃控制权、允许其它人运行的标志点。不像多线程，协程展示出我们的代码哪里可以被中断哪里不能。在 Glyph Lefkowitz 富有启发性的文章“[Unyielding](https://glyph.twistedmatrix.com/2014/02/unyielding.html)”：“线程让局部推理变得困难，然而局部推理可能是软件开发中最重要的事”。然而，明确的 yield，让“通过过程本身而不是整个系统理解它的行为（和因此、正确性）”成为可能。

这章写于 Python 和异步的复兴时期。你刚学到的基于生成器的的协程，在 2014 年发布在 Python 3.4 的 asyncio 模块中。2015 年 9 月，Python 3.5 发布，协程成为语言的一部分。这个原生的协程通过“async def”来声明, 使用“await”而不是“yield from”委托一个协程或者等待 Future。

除了这些优点，核心的思想不变。Python 新的原生协程与生成器只是在语法上不同，工作原理非常相似。事实上，在 Python 解释器中它们共用同一个实现方法。Task、Future 和事件循环在 asynico 中扮演着同样的角色。

你已经知道 asyncio 协程是如何工作的了，现在你可以忘记大部分的细节。这些机制隐藏在一个整洁的接口下。但是你对这基本原理的理解能让你在现代异步环境下正确而高效的编写代码。

（题图素材来自：[ruth-tay.deviantart.com](http://ruth-tay.deviantart.com/art/Bearded-Vulture-553800995)）

---

via: <http://aosabook.org/en/500L/pages/a-web-crawler-with-asyncio-coroutines.html>

作者：A. Jesse Jiryu Davis , Guido van Rossum 译者：[qingyunha](https://github.com/qingyunha) 校对：[wxy](https://github.com/wxy)

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


如何在 Ubuntu 上用 Yocto 创建你自己的嵌入式 Linux 发行版

收藏起来好好好学习学习

cqmailbox [Chrome Mobile 55.0|Android 7.1]

没想到还可以打印

哈哈，看来很多人不知道我们的打印版式啊，虽然这有点不环保，不过有时候还是挺需要的。

# 如何在 Ubuntu 上用 Yocto 创建你自己的嵌入式 Linux 发行版

本文主要聚焦在如何使用 Yocto 在 Ubuntu 上创建一个最小化的 Linux 发行版。Yocto 项目在嵌入式 Linux 的世界非常著名，这是因为它用起来非常灵活、方便。Yocto 的目标是为嵌入式软硬件开发商创建自己的 Linux 发行版。本文我们将会创建一个可以运行在 QEMU 上的最小化的 Linux，并且在 QEMU 上实际运行。

### 开发机的基本条件

* 最少 4-6 GB 内存
* 最新版的 Ubuntu 系统（本文使用了 16.04 LTS）
* 磁盘剩余空间至少 60-80 GB
* 在创建 Linux 发行版之前先安装下面的软件包
* 下载最新的 Yocto（Poky 是其最小开发环境）稳定分支

```
apt-get update

```

```
apt-get install wget git-core unzip make gcc g++ build-essential subversion sed autoconf automake texi2html texinfo coreutils diffstat python-pysqlite2 docbook-utils libsdl1.2-dev libxml-parser-perl libgl1-mesa-dev libglu1-mesa-dev xsltproc desktop-file-utils chrpath groff libtool xterm gawk fop

```

如下所示，开发环境要安装的软件包将近 1GB 大小。

在这个教程中，系统上克隆的是 poky 的 `morty` 稳定分支。

```
 git clone -b morty git://git.yoctoproject.org/poky.git

```

进入 `poky` 目录，然后运行下面的命令为 Yocto 开发环境设置（设置/导出）一些环境变量。

```
source oe-init-build-env

```

如下所示，在运行了 open embedded (oe) 的构建环境脚本之后，终端里的路径会自动切换到 `build` 目录，以便进行之后行发行版的的配置和构建。

上面的截屏展示了在 `conf` 目录下创建的文件 `local.conf`。这是 Yocto 用来设置目标机器细节和 SDK 的目标架构的配置文件。

如下所示，这里设置的目标机器是 `qemux86-64`。

如下面截图所示，在 `local.conf` 中取消下面参数的注释符号。

```
DL_DIR ?= "${TOPDIR}/downloads"

```

```
SSTATE_DIR ?= "${TOPDIR}/sstate-cache"

```

```
TMPDIR ?= "${TOPDIR}/tmp"

```

```
PACKAGE_CLASSES ?= "package_rpm"
SDKMACHINE ?= "i686"

```

如下所示，在 `local.conf` 中为基于 Yocto 的 Linux 设置空密码和后续的一些参数。否则的话用户就不能登录进新的发行版。

```
EXTRA_IMAGE_FEATURES ?= "debug-tweaks"

```

我们并不准备使用任何图形化工具来创建 Linux OS，比如 `toaster` （`hob` 已经不再支持了）。

### Yocto 编译构建过程

现在运行下面的 `bitbake` 工具命令开始为选定的目标机器下载和编译软件包。

```
bitbake core-image-minimal

```

**非常重要的是要在普通 Linux 用户下运行上面的命令，而不是使用 root 用户**。如下面截图所示，当你在 root 用户下运行 bitbake 命令会产生下面所示的错误。

再一次运行导出环境变量的脚本（`oe-init-build-env`），重新执行相同的命令来启动下载和编译过程。

如下所示，构建脚本组件的第一步工作是解析配置（`recipe`）。

下面的截图展示了构建脚本的解析过程。同时也显示了用来构建你的新的基于 yocto 的发行版的构建系统的细节。

在下载了 SDK 和必要的库之后，下一步工作是下载并编译软件包。如下截图展示了为构建新发行版而执行的任务。这一步将会执行 2-3 小时，因为首先要下载需要的软件包，然后还要为新的 Linux 发行版编译这些软件包。

下面的截图表明了任务列表执行完毕。

为目标机器类型 `qemux86-64` 编译好的新镜像位于 `build/tmp/deploy/images/qemux86-64`：

如下所示，上面的命令如果运行在 `Putty` 上会产生一个错误。

通过 `rdp` 在 Ubuntu 平台上再次运行上面的命令。

为运行新的基于 Yocto 的 Linux 发行版的 qemu 打开一个新屏幕。

下面展示了新发行版的登录界面，同时也显示了使用的 yocto 项目的版本号。默认的用户名是 `root` ，密码为空。

最后使用 `root` 用户名和空密码登录新发行版。如下截图所示，在这个最小版本的 Linux 上运行了基本的命令（`data` 、 `ifconfig` 和 `uname`）。

本文的目标是理解使用 Yocto 创建新的 Linux 发行版的过程。

---

via: <https://www.howtoforge.com/tutorial/how-to-create-your-own-linux-distribution-with-yocto-on-ubuntu/>

作者：[Ahmad](https://www.howtoforge.com/tutorial/how-to-create-your-own-linux-distribution-with-yocto-on-ubuntu/) 译者：[Ezio](https://github.com/oska874) 校对：[wxy](https://github.com/wxy)

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


为何 Linux 安装器需要添加安全功能？

我认为当前的 Linux 安装器需要解决的最大问题不是安全问题，而是用户体验问题。Linux 系统难用不说，安装过程简直是一场灾难。用户傻傻不知所云。Linux 各大发行版本要是能做到 WIndows 10 那样开箱即用的话，桌面占有率自然飙升。

# 为何 Linux 安装器需要添加安全功能？

>
> 由于安全问题越来越严重，Linux 发行版需要在安装程序中突出显示基本安全选项，而不是让用户稍后手动添加这些选项。
>
>
>

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

十二年前，Linux 发行版努力使安装变得简单。在 Ubuntu 和 Fedora 的引领下，它们很早就实现了这一目标。现在，随着对安全性越来越关注，它们需要稍微转变下方向，并在安装程序中突出显示基本安全选项，而不是让用户稍后手动添加这些选项。

当然，即便是在最好的情况下，说服用户来设置安全功能都是困难的。太多用户甚至不愿意添加如非特权用户帐户或密码这样简单的功能，他们显然更喜欢用重装或者以每小时 80 美元的价格咨询专家来减少风险。

然而，即便一般用户不会专门注意安全，但他也可能会在安装过程中注意。他们可能永远不会再想到它，但也许在安装过程中，当他们的注意力集中时，特别是如果有可见的在线帮助来解释其好处时，他们可能被说服选择一个复选框。

这种转变也并不伟大。许多安装程序已经提供了自动登录的选择 - 这对于不包含个人数据的安装来说或许是可以接受的功能，但更可能会被那些觉得登录不方便的用户使用。同样感谢 Ubuntu，它选择加密文件系统 - 至少在主目录中是这样 - 它已经成为许多安装程序的标准。我真正建议的也是这样的。

此外，外部安装程序如 Firefox 已经无缝合并了隐私浏览，而 [Signal Private Messenger](https://whispersystems.org/) 则是一个可替代标准 的 Android 手机和联系人的应用程序。

这些建议远不算激进。它只需要意志和想象力来实现它。

### Linux 安全第一步

应该将什么类型的安全功能添加到安装程序呢？

首先是防火墙。有许多图形界面程序可以设置防火墙。尽管十七年的经验，但是就像拜伦对柯尔律治的形而上的思想的讨论一样，我有时还是希望有人能来解释一下。

尽管出于好意，大多数防火墙工具对 iptables 的处理看起来都很直接。有一个现在已经停止维护的加固系统 [Bastille Linux](http://bastille-linux.sourceforge.net/) 可以用于安装一个基本的防火墙，我看不出为什么其他发行版做不到同样的事情。

一些工具可以用于安装后处理，并且对于安装器而言可以毫无困难地添加使用。例如，对于 [Grub 2](https://help.ubuntu.com/community/Grub2/Passwords)，这个大多数发行版使用的引导管理器包含了基本密码保护。诚然，密码可以通过 Live CD 绕过，但它仍然在包括远程登录在内的日常情况下提供一定程度的保护。

类似地，一个类似于 [pwgen](http://pwgen-win.sourceforge.net/downloads.html) 的密码生成器也可以添加到安装程序设置帐户的环节。这些工具强制可接受密码的长度、以及它们的大小写字母、数字和特殊字符的组合。它们许多都可以为你生成密码，有些甚至可以使生成的密码可拼读，以便你记住密码。

还有些工具也可以添加到安装过程的这个部分。例如，安装程序可以请求定期备份的计划，并添加一个计划任务和一个类似 [kbackup](http://kbackup.sourceforge.net/) 的简单的备份工具。

那么加密电子邮件怎么办？如今最流行的邮件阅读器包括了加密邮件的能力，但是设置和使用加密需要用户采取额外的设置，这使常见的任务复杂化，以至于用户会忽略它。然而，看看 Signal 在手机上的加密有多么简单，很显然，在笔记本电脑和工作站上加密会更容易。大多数发行版可能都喜欢对等加密，而不喜欢 Signal 那样的集中式服务器，但像 [Ring](https://savannah.gnu.org/projects/ring/) 这样的程序可以提供这种功能。

无论在安装程序中添加了什么功能，也许这些预防措施也可以扩展到生产力软件，如 LibreOffice。大多数安全工作都集中在电子邮件、网络浏览和聊天中，但文字处理程序和电子表格及其宏语言，是一个明显的恶意软件感染的来源和隐私关注点。除了像 [Qubes OS](https://www.qubes-os.org/) 或 [Subgraph](https://subgraph.com/sgos/) 这样的几个例外之外，很少有人努力将生产力软件纳入其安全预防措施 - 这可能会留下一个安全漏洞空缺。

### 适应现代

当然，在意安全的用户也许会采取一些安全的方法，这样的用户可以为自己负责。

我关心的是那些不太了解安全或不太愿意自己做修补的用户。我们越来越需要易于使用的安全性，并且亟待解决。

这些例子只是开始。所需要的工具大多数已经存在，只是需要以这样的方式来实现它们，使得用户不能忽略它们，并且能够不用懂什么就可以使用它们。可能实现所有这些只需要一个人月而已，包括原型、UI 设计和测试等等。

然而，在添加这些功能前，大多数主流的 Linux 发行版几乎不能说是关注到了安全性。毕竟，如果用户从不使用它们，那怎么会是好工具？

---

via: <http://www.datamation.com/security/why-linux-installers-need-to-add-security-features.html>

作者：[Bruce Byfield](http://www.datamation.com/author/Bruce-Byfield-6030.html) 译者：[geekpi](https://github.com/geekpi) 校对：[Bestony](https://github.com/Bestony), [wxy](https://github.com/wxy)

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


使用 Elizabeth 为应用生成随机样本数据

# 使用 Elizabeth 为应用生成随机样本数据

>
> Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu.
>
>
>

不，我的文章没有被 [Lorem ipsum](https://en.wikipedia.org/wiki/Lorem_ipsum) 生成器劫持（LCTT 译注：Lorem ipsum，中文又称“乱数假文”，只是一段用来测试排版效果的占位文字，没有实际的含义）。作为本月的 Nooks＆Crannies 专栏文章，我发现了一个有趣的小 Python 库，以帮助开发人员为其应用程序生成随机数据。它被称为 [Elizabeth](https://github.com/lk-geimfari/elizabeth)。

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

它由 Líkið Geimfari 编写，并在 MIT 许可证下发行，Elizabeth 以 21 个不同本地化信息提供了 18 种数据提供器，可用于生成随机信息（LCTT 译注：不仅是随机数），包括姓名和个人特征、地址、文本数据、交通信息、网络和 Internet 社交媒体数据、数字等等。安装它需要 [Python 3.2](https://www.python.org/) 或更高版本，您可以使用 `pip` 或从 `git` 仓库安装它。

在我的测试机上，我在一个全新安装的 [Debian](https://www.debian.org/) Jessie 上使用 pip 来安装它，要做的就是 `apt-get install python3-pip`，它将安装 Python 和所需的依赖项。然后 `pip install elizabeth`，之后就安装好了。

只是为好玩，让我们在 Python 的交互式解释器中为一个人生成一些随机数据：

```
>>> from elizabeth import Personal
>>> p=Personal('en')
>>> p.full_name(gender="male")
'Elvis Herring'
>>> p.blood_type()
'B+'
>>> p.credit_card_expiration_date()
'09/17'
>>> p.email(gender='male')
'jessie7517@gmail.com'
>>> p.favorite_music_genre()
'Ambient'
>>> p.identifier(mask='13064########')
'1306420450944'
>>> p.sexual_orientation()
'Heterosexual'
>>> p.work_experience()
39
>>> p.occupation()
'Senior System Designer'
>>>

```

在代码中使用它就像创建一个对象那样，然后调用要你需要填充数据的对应方法。

Elizabeth 内置了 18 种不同的生成工具，添加新的生成器并不困难；你只需要定义从 JSON 值集合中获取数据的例程。以下是一些随机文本字符串生成，再次打开解释器：

```
>>> from elizabeth import Text
>>> t=Text('en')
>>> t.swear_word()
'Rat-fink'
>>> t.quote()
'Let them eat cake.'
>>> t.words(quantity=20)
['securities', 'keeps', 'accessibility', 'barbara', 'represent', 'hentai', 'flower', 'keys', 'rpm', 'queen', 'kingdom', 'posted', 'wearing', 'attend', 'stack', 'interface', 'quite', 'elementary', 'broadcast', 'holland']
>>> t.sentence()
'She spent her earliest years reading classic literature, and writing poetry.'

```

使用 Elizabeth 填充 [SQLite](https://sqlite.org/) 或其它你可能需要用于开发或测试的数据库并不困难。其介绍文档给出了使用 [Flask](https://flask.pocoo.org/) 这个轻量级 web 框架的一个医疗应用程序示例。

我对 Elizabeth 印象很深刻 - 它超快、轻量级、易于扩展，它的社区虽然小，但是很活跃。截至本文写作时，项目已有 25 名贡献者，并且提交的问题处理迅速。Elizabeth 的[完整文档](http://elizabeth.readthedocs.io/en/latest/index.html)至少对于美国英语而言易于阅读和遵循，并提供了广泛的 API 参考。

我曾尝试通过修改链接来查找该文档是否有其他语言，但没有成功。因为其 API 在非英语区域中是不同的，所以记录这些变化将对用户非常有帮助。公平地说，通过阅读其代码并找出可用的方法并不难，即使你的 Python 功力并不深厚。对我来说，另一个明显的缺陷是缺乏阿拉伯语或希伯来语区域测试数据。这些是著名的从右到左的语言，对于试图使其应用程序国际化的开发者来说，适当地处理这些语言是一个主要的障碍。像 Elizabeth 这种在此方面可以协助的工具是值得拥有的。

对于那些在应用中需要随机样本数据的开发员而言，Elizabeth 是一个有价值的工具，而对于那些试图创建真正多语言、本地化应用程序的开发者来说，它可能是一个宝藏。

---

作者简介：

D Ruth Bavousett - D Ruth Bavousett 作为一名系统管理员和软件开发人员已经很长时间了，她的专业生涯开始于 VAX 11/780。在她的职业生涯（迄今为止）中，她在解决图书馆需求上有大量的经验，她自 2008 年以来一直是 Koha 开源图书馆自动化套件的贡献者。Ruth 目前在休斯敦的 cPanel 任 Perl 开发人员，她也作为首席员工效力于双猫公司。

---

via: <https://opensource.com/article/17/2/elizabeth-python-library>

作者：[D Ruth Bavousett](https://opensource.com/users/druthb) 译者：[geekpi](https://github.com/geekpi) 校对：[jasminepeng](https://github.com/jasminepeng)

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


浅述内核中“挂起到空闲”的实现

Summary: 它主要通过软件实现。一般平台的挂起过程包括冻结用户空间并将外围设备调至低耗电模式。但是，系统并不是直接关闭和热插拔掉 CPU，而是静静地强制将 CPU 进入空闲（idle）状态。

低功耗的需求日益增大，低功耗的硬件设备日益增多，linux 内核对 CPU 挂起这一块的支持度相当有限。不少 SoC 设备在使用低版本（<4.9）的 kernel 时，用户会发现开始时还好，但某个时间设备会突然“死机”。其实就是 CPU 进入了深层次的 Cstate 状态。我手头上的 Bay（悲）Trail（催）平板就是这德行。

Bay（悲）Trail（催）

# 浅述内核中“挂起到空闲”的实现

### 简介

Linux 内核提供了多种睡眠状态，各个状态通过设置系统中的不同部件进入低耗电模式来节约能源。目前总共有四种睡眠状态，分别是：<ruby> 挂起到空闲 <rp>  （ </rp> <rt>  suspend to idle </rt> <rp>  ） </rp></ruby>、<ruby> 加电待机 <rp>  （ </rp> <rt>  power-on standby（standby） </rt> <rp>  ） </rp></ruby>、<ruby> 挂起到内存 <rp>  （ </rp> <rt>  suspend to ram </rt> <rp>  ） </rp></ruby>和<ruby> 挂起到磁盘 <rp>  （ </rp> <rt>  suspend to disk </rt> <rp>  ） </rp></ruby>。这些状态分别对应 ACPI 的 4 种状态：S0，S1，S3 和 S4。<ruby> 挂起到空闲 <rp>  （ </rp> <rt>  suspend to idle </rt> <rp>  ） </rp></ruby>是纯软件实现的，用于将 CPU 维持在尽可能深的 idle 状态。<ruby> 加电待机 <rp>  （ </rp> <rt>  power-on standby（standby） </rt> <rp>  ） </rp></ruby>则使设备处于低功耗状态，并且关闭所有非引导 CPU。<ruby> 挂起到内存 <rp>  （ </rp> <rt>  suspend to ram </rt> <rp>  ） </rp></ruby>就更进一步，关闭所有 CPU 并且设置 RAM 进入自刷新模式。<ruby> 挂起到磁盘 <rp>  （ </rp> <rt>  suspend to disk </rt> <rp>  ） </rp></ruby>则是最省功耗的模式，关闭尽可能多的系统，包括关闭内存。然后内存中的内容会被写到硬盘，待唤醒计算机的时候将硬盘中的内容重新恢复到内存中。

这篇博文主要介绍<ruby> 挂起到空闲 <rp>  （ </rp> <rt>  suspend to idle </rt> <rp>  ） </rp></ruby>的实现。如上所说，它主要通过软件实现。一般平台的挂起过程包括冻结用户空间并将外围设备调至低耗电模式。但是，系统并不是直接关闭和热插拔掉 CPU，而是静静地强制将 CPU 进入<ruby> 空闲 <rp>  （ </rp> <rt>  idle </rt> <rp>  ） </rp></ruby>状态。随着外围设备进入了低耗电模式，除了唤醒相关的中断外不应有其他中断产生。唤醒中断包括那些设置用于唤醒系统的计时器（比如 RTC，普通计时器等）、或者电源开关、USB 和其它外围设备等。

在冻结过程中，当系统进入空闲状态时会调用一个特殊的 cpu 空闲函数。这个 `enter_freeze()` 函数可以和调用使 cpu 空闲的 `enter()` 函数一样简单，也可以复杂得多。该函数复杂的程度由将 SoC 置为低耗电模式的条件和方法决定。

### 先决条件

#### `platform_suspend_ops`

一般情况，为了支持 S2I，系统必须实现 `platform_suspend_ops` 并提供最低限度的挂起支持。这意味着至少要完成 `platform_suspend_ops` 中的 `valid()` 函数。如果<ruby> 挂起到空闲 <rp>  （ </rp> <rt>  suspend to idle </rt> <rp>  ） </rp></ruby>和<ruby> 挂起到内存 <rp>  （ </rp> <rt>  suspend to ram </rt> <rp>  ） </rp></ruby>都要支持，valid 函数中应使用 `suspend_valid_only_mem`。

不过，最近内核增加了对 S2I 的自动支持。Sudeep Holla 提出了一个变更，可以让系统不需要满足 `platform_suspend_ops` 条件也能提供 S2I 支持。这个补丁已经被接收并将合并在 4.9 版本中，该补丁可从这里获取： <https://lkml.org/lkml/2016/8/19/474>。

如果定义了 `suspend_ops`，那么可以通过查看 `/sys/power/state` 文件得知系统具体支持哪些挂起状态。如下操作：

```
# cat /sys/power/state
freeze mem

```

这个示例的结果显示该平台支持 S0（<ruby> 挂起到空闲 <rp>  （ </rp> <rt>  suspend to idle </rt> <rp>  ） </rp></ruby>）和 S3（<ruby> 挂起到内存 <rp>  （ </rp> <rt>  suspend to ram </rt> <rp>  ） </rp></ruby>）。按 Sudeep 的变更，那些没有实现 `platform_suspend_ops` 的平台将只显示 freeze 状态。

#### 唤醒中断

一旦系统处于某种睡眠状态，系统必须要接收某个唤醒事件才能恢复系统。这些唤醒事件一般由系统的设备产生。因此一定要确保这些设备驱动使用唤醒中断，并且将自身配置为接收唤醒中断后产生唤醒事件。如果没有正确识别唤醒设备，系统收到中断后会继续保持睡眠状态而不会恢复。

一旦设备正确实现了唤醒接口的调用，就可用来生成唤醒事件。请确保 DT 文件正确配置了唤醒源。下面是一个配置唤醒源示例，该文件来自（`arch/arm/boot/dst/am335x-evm.dts`）:

```
     gpio_keys: volume_keys@0 {
               compatible = “gpio-keys”;
               #address-cells = <1>;
               #size-cells = <0>;
               autorepeat;

               switch@9 {
                       label = “volume-up”;
                       linux,code = <115>;
                       gpios = <&gpio0 2 GPIO_ACTIVE_LOW>;
                       wakeup-source;
               };

               switch@10 {
                       label = “volume-down”;
                       linux,code = <114>;
                       gpios = <&gpio0 3 GPIO_ACTIVE_LOW>;
                       wakeup-source;
               };
       };
```

如上所示，有两个 gpio 键被配置为唤醒源，在系统挂起期间，其中任何一个键被按下都会产生一个唤醒事件。

可替代 DT 文件配置的另一个唤醒源配置就是设备驱动，如果设备驱动自身在代码里面配置了唤醒支持，那么就会使用该默认唤醒配置。

### 实施

#### 冻结功能

如果系统希望能够充分使用<ruby> 挂起到空闲 <rp>  （ </rp> <rt>  suspend to idle </rt> <rp>  ） </rp></ruby>，那么应该在 CPU 空闲驱动代码中定义 `enter_freeze()` 函数。`enter_freeze()` 与 `enter()` 的函数原型略有不同。因此，不能将 `enter()` 同时指定给 `enter` 和 `enter_freeze`。至少，系统会直接调用 `enter()`。如果没有定义 `enter_freeze()`，系统会挂起，但是不会触发那些只有当 `enter_freeze()` 定义了才会触发的函数，比如 `tick_freeze()` 和 `stop_critical_timing()` 都不会发生。这会导致计时器中断唤醒系统，但不会导致系统恢复，因为系统处理完中断后会继续挂起。

在挂起过程中，中断越少越好（最好一个也没有）。

下图显示了能耗和时间的对比。图中的两个尖刺分别是挂起和恢复。挂起前后的能耗尖刺是系统退出空闲态进行记录操作，进程调度，计时器处理等。因延迟的缘故，系统进入更深层次空闲状态需要花费一段时间。

*能耗使用时序图*

下图为 ftrace 抓取的 4 核 CPU 在系统挂起和恢复操作之前、之中和之后的活动。可以看到，在挂起期间，没有请求或者中断被处理。

*Ftrace 抓取的挂起/恢复活动图*

#### 空闲状态

你必须确定哪个空闲状态支持冻结。在冻结期间，电源相关代码会决定用哪个空闲状态来实现冻结。这个过程是通过在每个空闲状态中查找谁定义了 `enter_freeze()` 来决定的。CPU 空闲驱动代码或者 SoC 挂起相关代码必须确定哪种空闲状态实现冻结操作，并通过给每个 CPU 的可应用空闲状态指定冻结功能来进行配置。

例如， Qualcomm 会在平台挂起代码的挂起初始化函数处定义 `enter_freeze` 函数。这个工作是在 CPU 空闲驱动已经初始化后进行，以便所有结构已经定义就位。

#### 挂起/恢复相关驱动支持

你可能会在第一次成功挂起操作后碰到驱动相关的 bug。很多驱动开发者没有精力完全测试挂起和恢复相关的代码。你甚至可能会发现挂起操作并没有多少工作可做，因为 `pm_runtime` 已经做了你要做的挂起相关的一切工作。由于用户空间已经被冻结，设备此时已经处于休眠状态并且 `pm_runtime` 已经被禁止。

### 测试相关

测试<ruby> 挂起到空闲 <rp>  （ </rp> <rt>  suspend to idle </rt> <rp>  ） </rp></ruby>可以手动进行，也可以使用脚本/进程等实现自动挂起、自动睡眠，或者使用像 Android 中的 `wakelock` 来让系统挂起。如果手动测试，下面的操作会将系统冻结。

```
/ # echo freeze > /sys/power/state
[  142.580832] PM: Syncing filesystems … done.
[  142.583977] Freezing user space processes … (elapsed 0.001 seconds) done.
[  142.591164] Double checking all user space processes after OOM killer disable… (elapsed 0.000 seconds)
[  142.600444] Freezing remaining freezable tasks … (elapsed 0.001 seconds) done.
[  142.608073] Suspending console(s) (use no_console_suspend to debug)
[  142.708787] mmc1: Reset 0x1 never completed.
[  142.710608] msm_otg 78d9000.phy: USB in low power mode
[  142.711379] PM: suspend of devices complete after 102.883 msecs
[  142.712162] PM: late suspend of devices complete after 0.773 msecs
[  142.712607] PM: noirq suspend of devices complete after 0.438 msecs
< system suspended >
….
< wake irq triggered >
[  147.700522] PM: noirq resume of devices complete after 0.216 msecs
[  147.701004] PM: early resume of devices complete after 0.353 msecs
[  147.701636] msm_otg 78d9000.phy: USB exited from low power mode
[  147.704492] PM: resume of devices complete after 3.479 msecs
[  147.835599] Restarting tasks … done.
/ #
```

在上面的例子中，需要注意 MMC 驱动的操作占了 102.883ms 中的 100ms。有些设备驱动在挂起的时候有很多工作要做，比如将数据刷出到硬盘，或者其他耗时的操作等。

如果系统定义了<ruby> 冻结 <rp>  （ </rp> <rt>  freeze </rt> <rp>  ） </rp></ruby>，那么系统将尝试挂起操作，如果没有冻结功能，那么你会看到下面的提示：

```
/ # echo freeze > /sys/power/state
sh: write error: Invalid argument
/ #
```

### 未来的发展

目前在 ARM 平台上的<ruby> 挂起到空闲 <rp>  （ </rp> <rt>  suspend to idle </rt> <rp>  ） </rp></ruby>有两方面的工作需要做。第一方面工作在前面 `platform_suspend_ops` 小节中提到过，是总允许接受冻结状态以及合并到 4.9 版本内核中的工作。另一方面工作是冻结功能的支持。

如果你希望设备有更好的响应及表现，那么应该继续完善冻结功能的实现。然而，由于很多 SoC 会使用 ARM 的 CPU 空闲驱动，这使得 ARM 的 CPU 空闲驱动完善它自己的通用冻结功能的工作更有意义了。而事实上，ARM 正在尝试添加此通用支持。如果 SoC 供应商希望实现他们自己的 CPU 空闲驱动或者需要在进入更深层次的冻结休眠状态时提供额外的支持，那么只有实现自己的冻结功能。

---

via: <http://www.linaro.org/blog/suspend-to-idle/>

作者：[Andy Gross](http://www.linaro.org/author/andygross/) 译者：[beyondworld](https://github.com/beyondworld) 校对：[jasminepeng](https://github.com/jasminepeng)

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


安卓编年史（15）：安卓 3.0 蜂巢—平板和设计复兴

“尽管如此，一年之后，Palm 资金链断裂。Palm 公司未曾看到 iPhone 的到来，”此处是否为笔误，应为 iPad ？

原文如此，但是按时间线索。应该是 iPad。谢谢~

# 安卓编年史（15）：安卓 3.0 蜂巢—平板和设计复兴

### 安卓 3.0 蜂巢—平板和设计复兴

尽管姜饼中做了许多改变，安卓仍然是移动世界里的丑小鸭。相比于 iPhone，它的优雅程度和设计完全抬不起头。另一方面来说，为数不多的能与 iOS 的美学智慧相当的操作系统之一是 Palm 的 WebOS。WebOS 有着优秀的整体设计，创新的功能，而且被寄予期望能够从和 iPhone 的长期竞争中拯救公司。

尽管如此，在 WebOS 发布一年之后，Palm 资金链断裂。Palm 公司未曾看到 iPad 的到来（LCTT 译注：原文为 iPhone，根据时间推断，应该是指 2010 年发布的 iPad），到 WebOS 就绪的时候已经太晚了。2010 年 4 月，惠普花费 10 亿美元收购了 Palm。尽管惠普收购了一个拥有优秀用户界面的产品，但界面的首席设计师，Matias Duarte，并没有加入惠普公司。2010 年 5 月，就在惠普接手 Palm 之前，Duarte 加入了谷歌。惠普买下了面包，但谷歌雇佣了它的烘培师。

*第一部蜂巢设备，摩托罗拉 Xoom 10英寸平板。*

在谷歌，Duarte 被任命为安卓用户体验主管。这是第一次有人公开掌管安卓的外观。尽管 Matias 在安卓 2.2 发布时就来到了谷歌，第一个真正受他影响的安卓版本是 3.0 <ruby> 蜂巢 <rp>  （ </rp> <rt>  Honeycomb </rt> <rp>  ） </rp></ruby>，它在 2011 年 2 月发布。

按谷歌自己的说法，蜂巢是匆忙问世的。10 个月前，苹果发布了 iPad，让平板变得更加现代，谷歌希望能够尽快做出回应。蜂巢就是那个回应，一个运行在 10 英寸触摸屏上的安卓版本。悲伤的是，将这个系统推向市场是如此优先的事项，以至于边边角角都被砍去了以节省时间。

新系统只用于平板——手机不能升级到蜂巢，这加大了谷歌让系统运行在差异巨大的不同尺寸屏幕上的难度。但是，仅支持平板而不支持手机也使得蜂巢源码没有泄露。之前的安卓版本是开源的，这使得黑客社区能够将其最新版本移植到所有的不同设备之上。谷歌不希望应用开发者在支持不完美的蜂巢手机移植版本时感到压力，所以谷歌将源码留在自己手中，并且严格控制能够拥有蜂巢的设备。匆忙的开发还导致了软件问题。在发布时，蜂巢不是特别稳定，SD 卡不能工作，Adobe Flash——安卓最大的特色之一——还不被支持。

[摩托罗拉 Xoom](http://arstechnica.com/gadgets/2011/03/ars-reviews-the-motorola-xoom/) 是为数不多的拥有蜂巢的设备之一，它是这个新系统的旗舰产品。Xoom 是一个 10 英寸，16:9 的平板，拥有 1GB 内存和 1GHz Tegra 2 双核处理器。尽管是由谷歌直接控制更新的新版安卓发布设备，它并没有被叫做“Nexus”。对此最可能的原因是谷歌对它没有足够的信心称其为旗舰。

尽管如此，蜂巢是安卓的一个里程碑。在一个体验设计师的主管之下，整个安卓用户界面被重构，绝大多数奇怪的应用设计都得到改进。安卓的默认应用终于看起来像整体的一部分，不同的界面有着相似的布局和主题。然而重新设计安卓会是一个跨越了多个版本的项目——蜂巢只是将安卓塑造成型的开始。这第一稿为未来版本的安卓将如何运作奠定了基础，但它也用了过多的科幻主题，谷歌将花费接下来的数个版本来淡化它。

*蜂巢和姜饼的主屏幕。 [Ron Amadeo供图]*

姜饼只是在它的光子壁纸上试验了科幻外观，蜂巢整个系统的以电子为灵感的主题让它充满科幻意味。所有东西都是黑色的，如果你需要对比色，你可以从一些不同色调的蓝色中挑选。所有蓝色的东西还有“光晕”效果，让整个系统看起来像是外星科技创造的。默认背景是个六边形的全息方阵（一个蜂巢！明白了吗？），看起来像是一艘飞船上的传送阵的地板。

蜂巢最重要的变化是增加了系统栏。摩托罗拉 Xoom 除了电源和音量键之外没有配备实体按键，所以蜂巢添加了一个大黑色底栏到屏幕底部，用于放置导航按键。这意味着默认安卓界面不再需要特别的实体按键。在这之前，安卓没有实体的返回、菜单和 Home 键就不能正常工作。现在，软件提供了所有必需的按钮，任何带有触摸屏的设备都能够运行安卓。

新软件按键带来的最大的好处是灵活性。新的应用指南表明应用不再必需实体菜单按键，需要用到的时候，蜂巢会自动检测并添加四个按钮到系统栏让应用正常工作。另一个软件按键的灵活属性是它们可以改变设备的屏幕方向。除了电源和音量键之外，Xoom 的方向实际上不是那么重要。从用户的角度来看，系统栏始终处于设备的“底部”。代价是系统栏明显占据了一些屏幕空间。为了在10英寸平板上节省空间，状态栏被合并到了系统栏中。所有的常用状态指示放在了右侧——有电源、连接状态、时间还有通知图标。

主屏幕的整个布局都改变了，用户界面部件放在了设备的四个角落。屏幕底部左侧放置着之前讨论过的导航按键，右侧用于状态指示和通知，顶部左侧显示的是文本搜索和语音搜索，右侧有应用抽屉和添加小部件的按钮。

*新锁屏界面和最近应用界面。 [Ron Amadeo供图]*

（因为 Xoom 是一部 [较重] 的 10 英寸，16:9 平板设备，这意味着它主要是横屏使用。虽然大部分应用还支持竖屏模式，但是到目前为止，由于我们的版式限制，我们大部分使用的是竖屏模式的截图。请记住蜂巢的截图来自于 10 英寸的平板，而姜饼的截图来自 3.7 英寸的手机。二者所展现的信息密度是不能直接比较的。）

解锁界面——从菜单按钮到旋转式拨号盘再到滑动解锁——移除了解锁步骤的任何精度要求，它采用了一个环状解锁盘。从中间向任意方向向外滑动就能解锁设备。就像旋转式解锁，这种解锁方式更加符合人体工程学，而不用强迫你的手指完美地遵循一条笔直的解锁路径。

第二张图中略缩图条带是由新增的“最近应用”按钮打开的界面，现在处在返回键和 Home 键旁边。不像姜饼中长按 Home 键显示一组最近应用的图标，蜂巢在屏幕上显示应用图标和略缩图，使得在任务间切换变得更加方便。“最近应用”的灵感明显来自于 Duarte 在 WebOS 中的“卡片式”多任务管理，其使用全屏略缩图来切换任务。这个设计提供和 WebOS 的任务切换一样的易识别体验，但更小的略缩图允许更多的应用一次性显示在屏幕上。

尽管最近应用的实现看起来和你现在的设备很像，这个版本实际上是非常早期的。这个列表不能滚动，这意味着竖屏下只能显示七个应用，横屏下只能显示五个。任何超出范围的应用会从列表中去除。而且你也不能通过滑动略缩图来关闭应用——这只是个静态的列表。

从这里我们可以看到电子灵感影响的完整主题效果：略缩图的周围有蓝色的轮廓以及神秘的光晕。这张截图还展示软件按键的好处——上下文。返回按钮可以关闭略缩图列表，所以这里的箭头指向下方，而不是通常的样子。

---

[Ron Amadeo](http://arstechnica.com/author/ronamadeo) / Ron是Ars Technica的评论编缉，专注于安卓系统和谷歌产品。他总是在追寻新鲜事物，还喜欢拆解事物看看它们到底是怎么运作的。

[@RonAmadeo](https://twitter.com/RonAmadeo)

---

via: <http://arstechnica.com/gadgets/2016/10/building-android-a-40000-word-history-of-googles-mobile-os/16/>

译者：[alim0x](https://github.com/alim0x) 校对：[Bestony](https://github.com/Bestony)

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