diff --git a/published/20160917 A Web Crawler With asyncio Coroutines.md b/published/20160917 A Web Crawler With asyncio Coroutines.md new file mode 100644 index 0000000000..a18cdd49af --- /dev/null +++ b/published/20160917 A Web Crawler With asyncio Coroutines.md @@ -0,0 +1,1105 @@ +一个使用 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/ 。 + +### 介绍 + +经典的计算机科学强调高效的算法,尽可能快地完成计算。但是很多网络程序的时间并不是消耗在计算上,而是在等待许多慢速的连接或者低频事件的发生。这些程序暴露出一个新的挑战:如何高效的等待大量网络事件。一个现代的解决方案是异步 I/O。 + +这一章我们将实现一个简单的网络爬虫。这个爬虫只是一个原型式的异步应用,因为它等待许多响应而只做少量的计算。一次爬的网页越多,它就能越快的完成任务。如果它为每个动态的请求启动一个线程的话,随着并发请求数量的增加,它会在耗尽套接字之前,耗尽内存或者线程相关的资源。使用异步 I/O 可以避免这个的问题。 + +我们将分三个阶段展示这个例子。首先,我们会实现一个事件循环并用这个事件循环和回调来勾画出一只网络爬虫。它很有效,但是当把它扩展成更复杂的问题时,就会导致无法管理的混乱代码。然后,由于 Python 的协程不仅有效而且可扩展,我们将用 Python 的生成器函数实现一个简单的协程。在最后一个阶段,我们将使用 Python 标准库“asyncio”中功能完整的协程, 并通过异步队列完成这个网络爬虫。(在 [PyCon 2013](http://pyvideo.org/video/1667/keynote) 上,Guido 介绍了标准的 asyncio 库,当时称之为“Tulip”。) + +### 任务 + +网络爬虫寻找并下载一个网站上的所有网页,也许还会把它们存档,为它们建立索引。从根 URL 开始,它获取每个网页,解析出没有遇到过的链接加到队列中。当网页没有未见到过的链接并且队列为空时,它便停止运行。 + +我们可以通过同时下载大量的网页来加快这一过程。当爬虫发现新的链接,它使用一个新的套接字并行的处理这个新链接,解析响应,添加新链接到队列。当并发很大时,可能会导致性能下降,所以我们会限制并发的数量,在队列保留那些未处理的链接,直到一些正在执行的任务完成。 + +### 传统方式 + +怎么使一个爬虫并发?传统的做法是创建一个线程池,每个线程使用一个套接字在一段时间内负责一个网页的下载。比如,下载 xkcd.com 网站的一个网页: + +```python +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 框架在一个线程中完成并发操作。让我们看看这是怎么做到的。 + +异步框架使用*非阻塞*套接字。异步爬虫中,我们在发起到服务器的连接前把套接字设为非阻塞: + +```python +sock = socket.socket() +sock.setblocking(False) +try: + sock.connect(('xkcd.com', 80)) +except BlockingIOError: + pass +``` + +对一个非阻塞套接字调用 `connect` 方法会立即抛出异常,即使它可以正常工作。这个异常复现了底层 C 语言函数令人厌烦的行为,它把 `errno` 设置为 `EINPROGRESS`,告诉你操作已经开始。 + +现在我们的爬虫需要一种知道连接何时建立的方法,这样它才能发送 HTTP 请求。我们可以简单地使用循环来重试: + +```python +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 注册它。 + +```python +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 提醒时我们处理它们。 + +```python +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 操作和当操作准备好时调用回调函数。异步*框架*,它在单线程中执行并发操作,其建立在两个功能之上,非阻塞套接字和事件循环。 + +我们这里达成了“并发性(concurrency)”,但不是传统意义上的“并行性(parallelism)”。也就是说,我们构建了一个可以进行重叠 I/O 的微小系统,它可以在其它操作还在进行的时候就开始一个新的操作。它实际上并没有利用多核来并行执行计算。这个系统是用于解决 I/O 密集(I/O-bound)问题的,而不是解决 CPU 密集(CPU-bound)问题的。(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 集合。 + +```python +urls_todo = set(['/']) +seen_urls = set(['/']) +``` + +`seen_urls` 集合包括 `urls_todo` 和已经完成的 URL。用根 URL `/` 初始化它们。 + +获取一个网页需要一系列的回调。在套接字连接建立时会触发 `connected` 回调,它向服务器发送一个 GET 请求。但是它要等待响应,所以我们需要注册另一个回调函数;当该回调被调用,它仍然不能读取到完整的请求时,就会再一次注册回调,如此反复。 + +让我们把这些回调放在一个 `Fetcher` 对象中,它需要一个 URL,一个套接字,还需要一个地方保存返回的字节: + +```python +class Fetcher: + def __init__(self, url): + self.response = b'' # Empty array of bytes. + self.url = url + self.sock = None +``` + +我们的入口点在 `Fetcher.fetch`: + +```python + # 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` 方法从连接一个套接字开始。但是要注意这个方法在连接建立前就返回了。它必须将控制返回到事件循环中等待连接建立。为了理解为什么要这样做,假设我们程序的整体结构如下: + +```python +# 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` 方法的实现: + +```python + # 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`,它处理服务器的响应: + +```python + # 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`,用它来控制这个循环: + +```python +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 操作,它明确的把未来所需的状态保存下来,然后返回。你需要考虑如何写这个状态保存的代码。 + +让我们来解释下这到底是什么意思。先来看一下在线程中使用通常的阻塞套接字来获取一个网页时是多么简单。 + +```python +# 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 + 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 +``` + +这个堆栈回溯只能显示出事件循环调用了一个回调。我们不知道是什么导致了这个错误。这条链的两边都被破坏:不知道从哪来也不知到哪去。这种丢失上下文的现象被称为“堆栈撕裂(stack ripping)”,经常会导致无法分析原因。它还会阻止我们为回调链设置异常处理,即那种用“try / except”块封装函数调用及其调用树。(对于这个问题的更复杂的解决方案,参见 http://www.tornadoweb.org/en/stable/stack_context.html ) + +所以,除了关于多线程和异步哪个更高效的长期争议之外,还有一个关于这两者之间的争论:谁更容易跪了。如果在同步上出现失误,线程更容易出现数据竞争的问题,而回调因为"堆栈撕裂(stack ripping)"问题而非常难于调试。 + +### 协程 + +还记得我们对你许下的承诺么?我们可以写出这样的异步代码,它既有回调方式的高效,也有多线程代码的简洁。这个结合是同过一种称为协程(coroutine)的模式来实现的。使用 Python3.4 标准库 asyncio 和一个叫“aiohttp”的包,在协程中获取一个网页是非常直接的( `@asyncio.coroutine` 修饰符并非魔法。事实上,如果它修饰的是一个生成器函数,并且没有设置 `PYTHONASYNCIODEBUG` 环境变量的话,这个修饰符基本上没啥用。它只是为了框架的其它部分方便,设置了一个属性 `_is_coroutine` 而已。也可以直接使用 asyncio 和裸生成器,而没有 `@asyncio.coroutine` 修饰符): + +```python + @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 函数是怎么工作的。正常情况下,当一个函数调用一个子过程,这个被调用函数获得控制权,直到它返回或者有异常发生,才把控制权交给调用者: + +```python +>>> def foo(): +... bar() +... +>>> def bar(): +... pass +``` + +标准的 Python 解释器是用 C 语言写的。一个 Python 函数被调用所对应的 C 函数是 `PyEval_EvalFrameEx`。它获得一个 Python 栈帧结构并在这个栈帧的上下文中执行 Python 字节码。这里是 `foo` 函数的字节码: + +```python +>>> 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` 函数中保存当前的栈帧,交互式的看看这种现象: + +```python +>>> 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' +``` + +![Figure 5.1 - Function Calls](http://aosabook.org/en/500L/crawler-images/function-calls.png) + +现在该说 Python 生成器了,它使用同样构件——代码对象和栈帧——去完成一个不可思议的任务。 + +这是一个生成器函数: + +```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` 语句就知道这是一个生成器函数而不是普通的函数。它就会设置一个标志来记住这个事实: + +```python +>>> # The generator flag is bit position 5. +>>> generator_bit = 1 << 5 +>>> bool(gen_fn.__code__.co_flags & generator_bit) +True +``` + +当你调用一个生成器函数,Python 看到这个标志,就不会实际运行它而是创建一个生成器: + +```python +>>> gen = gen_fn() +>>> type(gen) + +``` + +Python 生成器封装了一个栈帧和函数体代码的引用: + +```python +>>> gen.gi_code.co_name +'gen_fn' +``` + +所有通过调用 `gen_fn` 的生成器指向同一段代码,但都有各自的栈帧。这些栈帧不再任何一个C函数栈中,而是在堆空间中等待被使用: + +![Figure 5.2 - Generators](http://aosabook.org/en/500L/crawler-images/generator.png) + +栈帧中有一个指向“最后执行指令”的指针。初始化为 -1,意味着它没开始运行: + +```python +>>> gen.gi_frame.f_lasti +-1 +``` + +当我们调用 `send` 时,生成器一直运行到第一个 `yield` 语句处停止,并且 `send` 返回 1,因为这是 `gen` 传递给 `yield` 表达式的值。 + +```python +>>> gen.send(None) +1 +``` + +现在,生成器的指令指针是 3,所编译的Python 字节码一共有 56 个字节: + +```python +>>> gen.gi_frame.f_lasti +3 +>>> len(gen.gi_code.co_code) +56 +``` + +这个生成器可以在任何时候、任何函数中恢复运行,因为它的栈帧并不在真正的栈中,而是堆中。在调用链中它的位置也是不固定的,它不必遵循普通函数先进后出的顺序。它像云一样自由。 + +我们可以传递一个值 `hello` 给生成器,它会成为 `yield` 语句的结果,并且生成器会继续运行到第二个 `yield` 语句处。 + +```python +>>> gen.send('hello') +result of yield: hello +2 +``` + +现在栈帧中包含局部变量 `result`: + +```python +>>> gen.gi_frame.f_locals +{'result': 'hello'} +``` + +其它从 `gen_fn` 创建的生成器有着它自己的栈帧和局部变量。 + +当我们再一次调用 `send`,生成器继续从第二个 `yield` 开始运行,以抛出一个特殊的 `StopIteration` 异常为结束。 + +```python +>>> gen.send('goodbye') +result of 2nd yield: goodbye +Traceback (most recent call last): + File "", line 1, in +StopIteration: done +``` + +这个异常有一个值 `"done"`,它就是生成器的返回值。 + +### 使用生成器构建协程 + +所以生成器可以暂停,可以给它一个值让它恢复,并且它还有一个返回值。这些特性看起来很适合去建立一个不使用那种乱糟糟的意面似的回调异步编程模型。我们想创造一个这样的“协程”:一个在程序中可以和其他过程合作调度的过程。我们的协程将会是标准库 `asyncio` 中协程的一个简化版本,我们将使用生成器,futures 和 `yield from` 语句。 + +首先,我们需要一种方法去代表协程所需要等待的 future 事件。一个简化的版本是: + +```python +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 解决后,生成(yield)的协程应该马上恢复而不是暂停,但是在我们的代码中却不没有这样做。参见 asyncio 的 Future 类以了解其完整实现。) + +让我们用 future 和协程来改写我们的 fetcher。我们之前用回调写的 `fetch` 如下: + +```python +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` 回调函数,它会在套接字建立连接后调用。现在我们使用协程把这两步合并: + +```python + 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”: + +```python +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 请求,然后读取服务器响应。不再需要哪些分散在各处的回调函数,我们把它们放在同一个生成器函数中: + +```python + 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` 登场了。它能让一个生成器*委派*另一个生成器。 + +让我们先回到原来那个简单的生成器例子: + +```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' +... +``` + +为了从其他生成器调用这个生成器,我们使用 `yield from` 委派它: + +```python +>>> # 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` 表现的完全一致: + +```python +>>> 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 "", line 1, in +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` 语句的值。 + +```python + rv = yield from gen +``` + +前面我们批评过基于回调的异步编程模式,其中最大的不满是关于 “堆栈撕裂(stack ripping)”:当一个回调抛出异常,它的堆栈回溯通常是毫无用处的。它只显示出事件循环运行了它,而没有说为什么。那么协程怎么样? + +```python +>>> def gen_fn(): +... raise Exception('my error') +>>> caller = caller_fn() +>>> caller.send(None) +Traceback (most recent call last): + File "", line 1, in + File "", line 3, in caller_fn + File "", line 2, in gen_fn +Exception: my error +``` + +这还是非常有用的,当异常抛出时,堆栈回溯显示出 `caller_fn` 委派了 `gen_fn`。令人更欣慰的是,你可以在一次异常处理器中封装这个调用到一个子过程中,像正常函数一样: + +```python +>>> 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`: + +```python +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` 协程读取整个信息: + +```python +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`: + +```python +class Fetcher: + def fetch(self): + # ... connection logic from above, then: + sock.send(request.encode('ascii')) + self.response = yield from read_all(sock) +``` + +神奇的是,Task 类不需要做任何改变,它像以前一样驱动外部的 `fetch` 协程: + +```python +Task(fetcher.fetch()) +loop() +``` + +当 `read` yield 一个 future 时,task 从 `yield from` 管道中接收它,就像这个 future 直接从 `fetch` yield 一样。当循环解决一个 future 时,task 把它的结果送给 `fetch`,通过管道,`read` 接受到这个值,这完全就像 task 直接驱动 `read` 一样: + +![Figure 5.3 - Yield From](http://aosabook.org/en/500L/crawler-images/yield-from.png) + +为了完善我们的协程实现,我们再做点打磨:当等待一个 future 时,我们的代码使用 yield;而当委派一个子协程时,使用 yield from。不管是不是协程,我们总是使用 yield form 会更精炼一些。协程并不需要在意它在等待的东西是什么类型。 + +在 Python 中,我们从生成器和迭代器的高度相似中获得了好处,将生成器进化成 caller,迭代器也可以同样获得好处。所以,我们可以通过特殊的实现方式来迭代我们的 Future 类: + +```python + # Method on Future class. + def __iter__(self): + # Tell Task to resume me here. + yield self + return self.result +``` + +future 的 `__iter__` 方法是一个 yield 它自身的一个协程。当我们将代码替换如下时: + +```python +# f is a Future. +yield f +``` + +以及……: + +```python +# 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 编写应用,这些都不会出现在你的代码中。我们承诺过,你可以像这样下载一个网页: + +```python + @asyncio.coroutine + def fetch(self, url): + response = yield from self.session.get(url) + body = yield from response.read() +``` + +对我们的探索还满意么?回到我们原始的任务:使用 asyncio 写一个网络爬虫。 + +### 使用协程 + +我们将从描述爬虫如何工作开始。现在是时候用 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): + +```python +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` 完成: + +```python +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)` 序对放入队列中。(为什么要这样做,请看下文) + +```python +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` 方法: + +```python +loop.run_until_complete(crawler.crawl()) +``` +`crawl` 协程把 worker 们赶起来干活。它像一个主线程:阻塞在 `join` 上直到所有任务完成,同时 worker 们在后台运行。 + +```python + @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` 又是如何工作的呢?生成器还有一个我们还没介绍的特点。你可以从外部抛一个异常给它: + + +```python +>>> gen = gen_fn() +>>> gen.send(None) # Start the generator as usual. +1 +>>> gen.throw(Exception('error')) +Traceback (most recent call last): + File "", line 3, in + File "", line 2, in gen_fn +Exception: error +``` + +生成器被 `throw` 恢复,但是它现在抛出一个异常。如过生成器的调用堆栈中没有捕获异常的代码,这个异常被传递到顶层。所以注销一个协程: + +```python + # Method of Task class. + def cancel(self): + self.coro.throw(CancelledError) +``` + +任何时候生成器暂停,在某些 `yield from` 语句它恢复并且抛出一个异常。我们在 task 的 `step` 方法中处理注销。 + +```python + # 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,它就退出。同时事件循环看见这个协程结束了(我们后面会见到的),也就退出。 + +```python +loop.run_until_complete(crawler.crawl()) +``` + +`crawl` 方法包含了所有主协程需要做的事。而 worker 则完成从队列中获取 URL、获取网页、解析它们得到新的链接。每个 worker 独立地运行 `work` 协程: + +```python + @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: + +```python + 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`方法上,现在它就可以结束了。 + +我们承诺过要解释为什么队列中要使用序对,像这样: + +```python +# URL to fetch, and the number of redirects left. +('http://xkcd.com/353', 10) +``` + +新的 URL 的重定向次数是10。获取一个特别的 URL 会重定向一个新的位置。我们减小重定向次数,并把新的 URL 放入队列中。 + +```python +# URL with a trailing slash. Nine redirects left. +('http://xkcd.com/353/', 9) +``` + +我们使用的 `aiohttp` 默认会跟踪重定向并返回最终结果。但是,我们告诉它不要这样做,爬虫自己来处理重定向,以便它可以合并那些目的相同的重定向路径:如果我们已经在 `self.seen_urls` 看到一个 URL,说明它已经从其他的地方走过这条路了。 + +![Figure 5.4 - Redirects](http://aosabook.org/en/500L/crawler-images/redirects.png) + +crawler 获取“foo”并发现它重定向到了“baz”,所以它会加“baz”到队列和 `seen_urls` 中。如果它获取的下一个页面“bar” 也重定向到“baz”,fetcher 不会再次将 “baz”加入到队列中。如果该响应是一个页面,而不是一个重定向,`fetch` 会解析它的链接,并把新链接放到队列中。 + +```python + @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 不能从已解决返回变成待决。) + +```python +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` 调用开始: + +```python +loop.run_until_complete(self.crawler.crawl()) +``` + +程序如何结束?因为 `crawl` 是一个生成器函数,调用它返回一个生成器。为了驱动它,asyncio 把它包装成一个 task: + +```python +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。 + +```python +class Task(Future): + """A coroutine wrapped in a Future.""" +``` + +通常,一个 future 被别人调用 `set_result` 解决。但是 task,当协程结束时,它自己解决自己。记得我们解释过当 Python 生成器返回时,它抛出一个特殊的 `StopIteration` 异常: + +```python + # 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`: + +```python + # 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 协程是如何工作的了,现在你可以忘记大部分的细节。这些机制隐藏在一个整洁的接口下。但是你对这基本原理的理解能让你在现代异步环境下正确而高效的编写代码。 + +-------------------------------------- +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/) 荣誉推出 diff --git a/published/20161018 Suspend to Idle.md b/published/20161018 Suspend to Idle.md new file mode 100644 index 0000000000..62a9f9b81c --- /dev/null +++ b/published/20161018 Suspend to Idle.md @@ -0,0 +1,146 @@ +浅述内核中“挂起到空闲”的实现 +=============== + +### 简介 + +Linux 内核提供了多种睡眠状态,各个状态通过设置系统中的不同部件进入低耗电模式来节约能源。目前总共有四种睡眠状态,分别是:挂起到空闲(suspend to idle)、加电待机(power-on standby(standby))、挂起到内存(suspend to ram)和挂起到磁盘(suspend to disk)。这些状态分别对应 ACPI 的 4 种状态:S0,S1,S3 和 S4。挂起到空闲(suspend to idle)是纯软件实现的,用于将 CPU 维持在尽可能深的 idle 状态。加电待机(power-on standby(standby))则使设备处于低功耗状态,并且关闭所有非引导 CPU。挂起到内存(suspend to ram)就更进一步,关闭所有 CPU 并且设置 RAM 进入自刷新模式。挂起到磁盘(suspend to disk)则是最省功耗的模式,关闭尽可能多的系统,包括关闭内存。然后内存中的内容会被写到硬盘,待唤醒计算机的时候将硬盘中的内容重新恢复到内存中。 + +这篇博文主要介绍挂起到空闲(suspend to idle)的实现。如上所说,它主要通过软件实现。一般平台的挂起过程包括冻结用户空间并将外围设备调至低耗电模式。但是,系统并不是直接关闭和热插拔掉 CPU,而是静静地强制将 CPU 进入空闲(idle)状态。随着外围设备进入了低耗电模式,除了唤醒相关的中断外不应有其他中断产生。唤醒中断包括那些设置用于唤醒系统的计时器(比如 RTC,普通计时器等)、或者电源开关、USB 和其它外围设备等。 + +在冻结过程中,当系统进入空闲状态时会调用一个特殊的 cpu 空闲函数。这个 `enter_freeze()` 函数可以和调用使 cpu 空闲的 `enter()` 函数一样简单,也可以复杂得多。该函数复杂的程度由将 SoC 置为低耗电模式的条件和方法决定。 + +### 先决条件 + +#### `platform_suspend_ops` + +一般情况,为了支持 S2I,系统必须实现 `platform_suspend_ops` 并提供最低限度的挂起支持。这意味着至少要完成 `platform_suspend_ops` 中的 `valid()` 函数。如果挂起到空闲(suspend to idle)和挂起到内存(suspend to ram)都要支持,valid 函数中应使用 `suspend_valid_only_mem`。 + +不过,最近内核增加了对 S2I 的自动支持。Sudeep Holla 提出了一个变更,可以让系统不需要满足 `platform_suspend_ops` 条件也能提供 S2I 支持。这个补丁已经被接收并将合并在 4.9 版本中,该补丁可从这里获取: [https://lkml.org/lkml/2016/8/19/474][1]。 + +如果定义了 `suspend_ops`,那么可以通过查看 `/sys/power/state` 文件得知系统具体支持哪些挂起状态。如下操作: + +``` +# cat /sys/power/state +freeze mem +``` + +这个示例的结果显示该平台支持 S0(挂起到空闲(suspend to idle))和 S3(挂起到内存(suspend to ram))。按 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 文件配置的另一个唤醒源配置就是设备驱动,如果设备驱动自身在代码里面配置了唤醒支持,那么就会使用该默认唤醒配置。 + +### 实施 + +#### 冻结功能 + +如果系统希望能够充分使用挂起到空闲(suspend to idle),那么应该在 CPU 空闲驱动代码中定义 `enter_freeze()` 函数。`enter_freeze()` 与 `enter()` 的函数原型略有不同。因此,不能将 `enter()` 同时指定给 `enter` 和 `enter_freeze`。至少,系统会直接调用 `enter()`。如果没有定义 `enter_freeze()`,系统会挂起,但是不会触发那些只有当 `enter_freeze()` 定义了才会触发的函数,比如 `tick_freeze()` 和 `stop_critical_timing()` 都不会发生。这会导致计时器中断唤醒系统,但不会导致系统恢复,因为系统处理完中断后会继续挂起。 + +在挂起过程中,中断越少越好(最好一个也没有)。 + +下图显示了能耗和时间的对比。图中的两个尖刺分别是挂起和恢复。挂起前后的能耗尖刺是系统退出空闲态进行记录操作,进程调度,计时器处理等。因延迟的缘故,系统进入更深层次空闲状态需要花费一段时间。 + + ![blog-picture-one](http://www.linaro.org/wp-content/uploads/2016/10/blog-picture-one-1024x767.png) + +*能耗使用时序图* + +下图为 ftrace 抓取的 4 核 CPU 在系统挂起和恢复操作之前、之中和之后的活动。可以看到,在挂起期间,没有请求或者中断被处理。 + +![blog-picture-2](http://www.linaro.org/wp-content/uploads/2016/10/blog-picture-2-1024x577.png) + +*Ftrace 抓取的挂起/恢复活动图* + +#### 空闲状态 + +你必须确定哪个空闲状态支持冻结。在冻结期间,电源相关代码会决定用哪个空闲状态来实现冻结。这个过程是通过在每个空闲状态中查找谁定义了 `enter_freeze()` 来决定的。CPU 空闲驱动代码或者 SoC 挂起相关代码必须确定哪种空闲状态实现冻结操作,并通过给每个 CPU 的可应用空闲状态指定冻结功能来进行配置。 + +例如, Qualcomm 会在平台挂起代码的挂起初始化函数处定义 `enter_freeze` 函数。这个工作是在 CPU 空闲驱动已经初始化后进行,以便所有结构已经定义就位。 + +#### 挂起/恢复相关驱动支持 + +你可能会在第一次成功挂起操作后碰到驱动相关的 bug。很多驱动开发者没有精力完全测试挂起和恢复相关的代码。你甚至可能会发现挂起操作并没有多少工作可做,因为 `pm_runtime` 已经做了你要做的挂起相关的一切工作。由于用户空间已经被冻结,设备此时已经处于休眠状态并且 `pm_runtime` 已经被禁止。 + +### 测试相关 + +测试挂起到空闲(suspend to idle)可以手动进行,也可以使用脚本/进程等实现自动挂起、自动睡眠,或者使用像 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。有些设备驱动在挂起的时候有很多工作要做,比如将数据刷出到硬盘,或者其他耗时的操作等。 + +如果系统定义了冻结(freeze),那么系统将尝试挂起操作,如果没有冻结功能,那么你会看到下面的提示: + +``` +/ # echo freeze > /sys/power/state +sh: write error: Invalid argument +/ # +``` + +### 未来的发展 + +目前在 ARM 平台上的挂起到空闲(suspend to idle)有两方面的工作需要做。第一方面工作在前面 `platform_suspend_ops` 小节中提到过,是总允许接受冻结状态以及合并到 4.9 版本内核中的工作。另一方面工作是冻结功能的支持。 + +如果你希望设备有更好的响应及表现,那么应该继续完善冻结功能的实现。然而,由于很多 SoC 会使用 ARM 的 CPU 空闲驱动,这使得 ARM 的 CPU 空闲驱动完善它自己的通用冻结功能的工作更有意义了。而事实上,ARM 正在尝试添加此通用支持。如果 SoC 供应商希望实现他们自己的 CPU 空闲驱动或者需要在进入更深层次的冻结休眠状态时提供额外的支持,那么只有实现自己的冻结功能。 + + +-------------------------------------------------------------------------------- + +via: http://www.linaro.org/blog/suspend-to-idle/ + +作者:[Andy Gross][a] +译者:[beyondworld](https://github.com/beyondworld) +校对:[jasminepeng](https://github.com/jasminepeng) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]:http://www.linaro.org/author/andygross/ +[1]:https://lkml.org/lkml/2016/8/19/474 diff --git a/translated/tech/20161207 Manage Samba4 AD Domain Controller DNS and Group Policy from Windows – Part 4.md b/published/20161207 Manage Samba4 AD Domain Controller DNS and Group Policy from Windows – Part 4.md similarity index 64% rename from translated/tech/20161207 Manage Samba4 AD Domain Controller DNS and Group Policy from Windows – Part 4.md rename to published/20161207 Manage Samba4 AD Domain Controller DNS and Group Policy from Windows – Part 4.md index 9013125450..635e32f0f3 100644 --- a/translated/tech/20161207 Manage Samba4 AD Domain Controller DNS and Group Policy from Windows – Part 4.md +++ b/published/20161207 Manage Samba4 AD Domain Controller DNS and Group Policy from Windows – Part 4.md @@ -1,58 +1,59 @@ -Manage Samba4 AD Domain Controller DNS and Group Policy from Windows – Part 4 +Samba 系列(四):在 Windows 下管理 Samba4 AD 域管制器 DNS 和组策略 ============================================================ -在 Windows 系统下管理 Samba4 AD 域管制器 DNS 和组策略(四) -接着前一篇教程写的关于[使用 Windows 10 系统的 RSAT 工具来管理 Samba4 活动目录架构][4],在这篇文章中我们将学习如何使用微软 DNS 管理器远程管理我们的 Samba AD 域控制器的 DNS 服务器,如何创建 DNS 记录,如何创建反向查找区域以及如何通过组策略管理工具来创建域策略。 +接着前一篇教程写的关于[使用 Windows 10 的 RSAT 工具来管理 Samba4 活动目录架构][4],在这篇文章中我们将学习如何使用微软 DNS 管理器远程管理我们的 Samba AD 域控制器的 DNS 服务器,如何创建 DNS 记录,如何创建反向查找区域以及如何通过组策略管理工具来创建域策略。 -#### 需求 +#### 要求 -1、 [在 Ubuntu16.04 系统上使用 Samba4 软件来创建活动目录架构(一)][1] -2、 [在 Linux 命令行下管理 Samba4 AD 架构(二)][2] -3、 [使用 Windows 10 系统的 RSAT 工具来管理 Samba4 活动目录架构 (三)][3] +1、 [在 Ubuntu 16.04 系统上使用 Samba4 软件来创建活动目录架构(一)][1] + +2、 [在 Linux 命令行下管理 Samba4 AD 架构(二)][2] + +3、 [使用 Windows 10 的 RSAT 工具来管理 Samba4 活动目录架构 (三)][3] ### 第 1 步:管理 Samba DNS 服务器 -Samba4 AD DC 使用内部的 DNS 解析模块,该模块在初始化域提供的过程中被创建完成(如果 BIND9 DLZ 模块未特定使用的情况下)。 +Samba4 AD DC 使用内部的 DNS 解析器模块,该模块在初始化域提供的过程中创建(如果 BIND9 DLZ 模块未指定使用的情况下)。 -Samba4 内部的 DNS 域模块支持 AD 域控制器所必须的基本功能。有两种方式来管理域 DNS 服务器,直接在命令行下通过 samba-tool 接口来管理,或者使用已加入域的微软工作站中的 RSAT DNS 管理器远程进行管理。 +Samba4 内部的 DNS 模块支持 AD 域控制器所必须的基本功能。有两种方式来管理域 DNS 服务器,直接在命令行下通过 samba-tool 接口来管理,或者使用已加入域的微软工作站中的 RSAT DNS 管理器远程进行管理。 在这篇文章中,我们使用第二种方式来进行管理,因为这种方式很直观,也不容易出错。 1、要使用 RSAT 工具来管理域控制器上的 DNS 服务器,在 Windows 机器上,打开控制面板 -> 系统和安全 -> 管理工具,然后运行 DNS 管理器工具。 -当打开这个工具时,它会询问你将要连接到哪台正在运行的 DNS 服务器。选择使用下面的计算机,输入域名(IP 地址或 FQDN 地址都可以使用),勾选‘现在连接到指定计算机’,然后单击 OK 按钮以开启 Samba DNS 服务。 +当打开这个工具时,它会询问你将要连接到哪台正在运行的 DNS 服务器。选择“使用下面的计算机”,输入域名(IP 地址或 FQDN 地址都可以使用),勾选“现在连接到指定计算机”,然后单击 OK 按钮以开启 Samba DNS 服务。 [ ![Connect Samba4 DNS on Windows](http://www.tecmint.com/wp-content/uploads/2016/12/Connect-Samba4-DNS-on-Windows.png) ][5] -在 Windows 系统上连接 Samba4 DNS 服务器 +*在 Windows 系统上连接 Samba4 DNS 服务器* -2、为了添加一条 DNS 记录(比如我们添加一条指向 LAN 网关的记录 ‘A'),打开 DNS 管理器,找到域正向查找区,在右侧单击右键选择新的主机(’A‘ 或 ’AAA‘)。 +2、为了添加一条 DNS 记录(比如我们添加一条指向 LAN 网关的 A 记录),打开 DNS 管理器,找到域正向查找区,在右侧单击右键选择新的主机(A 或 AAAA)。 [ ![Add DNS A Record on Windows](http://www.tecmint.com/wp-content/uploads/2016/12/Add-DNS-A-Record.png) ][6] -在 Windows 下添加一条 DNS 记录 +*在 Windows 下添加一条 DNS 记录* -3、在打开的新主机窗口界面,输入 DNS 服务器的主机名和 IP 地址。 DNS 管理器工具会自动填写完成 FQDN 地址。填写完成后,点击添加主机按钮,之后会弹出一个新的窗口提示你 DNS A 记录已经创建完成。 +3、在打开的新主机窗口界面,输入 DNS 服务器的主机名和 IP 地址。 DNS 管理器工具会自动填写完成 FQDN 地址。填写完成后,点击“添加主机”按钮,之后会弹出一个新的窗口提示你 DNS A 记录已经创建完成。 -确保你添加的 DNS A 记录是你们网络中的资源[已配置静态 IP][7]。不要为那些从 DHCP 服务器自动获取 IP 地址或者经常变换 IP 地址的主机添加 DNS A 记录。 +确保仅为你的网络中[已配置静态 IP][7]的资源(设备)添加 DNS A 记录。不要为那些从 DHCP 服务器自动获取 IP 地址或者经常变换 IP 地址的主机添加 DNS A 记录。 [ ![Configure Samba Host on Windows](http://www.tecmint.com/wp-content/uploads/2016/12/Configure-Samba-Host-on-Windows.png) ][8] -在 Windows 系统下配置 Samba 主机 +*在 Windows 系统下配置 Samba 主机* -要更新一条 DNS 记录只需要双击那条记录,然后输入更改原因即可。要删除一条记录时,只需要在这条记录上单击右键,选择从菜单删除即可。 +要更新一条 DNS 记录只需要双击那条记录,然后输入更改即可。要删除一条记录时,只需要在这条记录上单击右键,选择从菜单删除即可。 -同样的方式,你也可以为你的域添加其它类型的 DNS 记录,比如说 CNAME 记录(也称为 DNS 别名记录),MX 记录(在邮件服务器上非常有用)或者其它类型的记录(SPE、TXT、SRVetc类型)。 +同样的方式,你也可以为你的域添加其它类型的 DNS 记录,比如说 CNAME 记录(也称为 DNS 别名记录),MX 记录(在邮件服务器上非常有用)或者其它类型的记录(SPE、TXT、SRV 等类型)。 ### 第 2 步:创建反向查找区域 -默认情况下, Samba4 AD DC 不会自动为你的域添加一个反向查找区域和 PTR 记录,因为这些类型的记录对于域控制器的正常工作来说是无关紧要的。 +默认情况下,Samba4 AD DC 不会自动为你的域添加一个反向查找区域和 PTR 记录,因为这些类型的记录对于域控制器的正常工作来说是无关紧要的。 相反,DNS 反向区和 PTR 记录在一些重要的网络服务中显得非常有用,比如邮件服务,因为这些类型的记录可以用于验证客户端请求服务的身份。 @@ -64,56 +65,56 @@ Samba4 内部的 DNS 域模块支持 AD 域控制器所必须的基本功能。 ![Create Reverse Lookup DNS Zone](http://www.tecmint.com/wp-content/uploads/2016/12/Create-Reverse-Lookup-DNS-Zone.png) ][9] -创建 DNS 反向查找区域 +*创建 DNS 反向查找区域* -5、下一步,单击下一步按钮,然后从区域类型向导中选择主区域。 +5、下一步,单击下一步按钮,然后从区域类型向导中选择主区域(Primary)。 [ ![Select DNS Zone Type](http://www.tecmint.com/wp-content/uploads/2016/12/Select-DNS-Zone-Type.png) ][10] -选择 DNS 区域类型 +*选择 DNS 区域类型* -6、下一步,在 AD 区域复制范围中选择复制到该域里运行在域控制器上的所有的 DNS 服务器,选择 IPv4 反向查找区域然后单击下一步继续。 +6、下一步,在 “AD 区域复制范围”中选择复制到该域里运行在域控制器上的所有的 DNS 服务器,选择 “IPv4 反向查找区域”然后单击下一步继续。 [ ![Select DNS for Samba Domain Controller](http://www.tecmint.com/wp-content/uploads/2016/12/Select-DNS-for-Samba-Domain-Controller.png) ][11] -为 Samba 域控制器选择 DNS 服务器 +*为 Samba 域控制器选择 DNS 服务器* [ ![Add Reverse Lookup Zone Name](http://www.tecmint.com/wp-content/uploads/2016/12/Add-Reverse-Lookup-Zone-Name.png) ][12] -添加反向查找区域名 +*添加反向查找区域名* 7、下一步,在网络ID 框中输入你的 LAN IP 地址,然后单击下一步继续。 -资源在这个区域内添加的所有 PTR 记录仅指向 192.168.1.0/24 网络段。如果你想要为一个不在该网段中的服务器创建一个 PTR 记录(比如邮件服务器位于 10.0.0.0/24 这个网段的时候),那么你还得为那个网段创建一个新的反向查找区域。 +在这个区域内添加的所有资源(设备)的 PTR 记录仅能指向 192.168.1.0/24 网络段。如果你想要为一个不在该网段中的服务器创建一个 PTR 记录(比如邮件服务器位于 10.0.0.0/24 这个网段的时候),那么你还得为那个网段创建一个新的反向查找区域。 [ ![Add IP Address of Reverse Lookup DNS Zone](http://www.tecmint.com/wp-content/uploads/2016/12/Add-IP-Address-of-Reverse-DNS-Zone.png) ][13] -添加 DNS 反向查找区域的 IP 地址 +*添加 DNS 反向查找区域的 IP 地址* -8、在下一个截图中选择仅允许安全的动态更新,单击下一步继续,最后单击完成按钮以完成反向查找区域的创建。 +8、在下一个截图中选择“仅允许安全的动态更新”,单击下一步继续,最后单击完成按钮以完成反向查找区域的创建。 [ ![Enable Secure Dynamic Updates](http://www.tecmint.com/wp-content/uploads/2016/12/Enable-Secure-Dynamic-Updates.png) ][14] -启用安全动态更新 +*启用安全动态更新* [ ![New DNS Zone Summary](http://www.tecmint.com/wp-content/uploads/2016/12/New-DNS-Zone-Summary.png) ][15] -新 DNS 区域概述 +*新 DNS 区域概览* 9、此时,你已经为你的域环境创建完成了一个有效的 DNS 反向查找区域。为了在这个区域中添加一个 PTR 记录,在右侧右键单击,选择为网络资源创建一个 PTR 记录。 -这个时候,我们已经为网关创建了一个指向。为了测试这条记录对于客户端是否添加正确和工作正常,打开命令行提示符执行 nslookup 查询资源名,再执行另外一条命令查询 IP 地址。 +这个时候,我们已经为网关创建了一个指向。为了测试这条记录对于客户端是否添加正确和工作正常,打开命令行提示符执行 `nslookup` 查询资源名,再执行另外一条命令查询 IP 地址。 两个查询都应该为你的 DNS 资源返回正确的结果。 @@ -121,19 +122,21 @@ Samba4 内部的 DNS 域模块支持 AD 域控制器所必须的基本功能。 nslookup gate.tecmint.lan nslookup 192.168.1.1 ping gate -``` +``` + [ ![Add DNS PTR Record and Query PTR](http://www.tecmint.com/wp-content/uploads/2016/12/Add-DNS-PTR-Record-and-Query.png) ][16] -添加及查询 PTR 记录 +*添加及查询 PTR 记录* + ### 第 3 步:管理域控制策略 10、域控制器最重要的作用就是集中控制系统资源及安全。使用域控制器的域组策略功能很容易实现这些类型的任务。 遗憾的是,在 Samba 域控制器上唯一用来编辑或管理组策略的方法是通过微软的 RSAT GPM 工具。 -在下面的实例中,我们将看到通过组策略来实现在 Samba 域环境中为域用户创建一种交互式的登录方式是多么的简单。 +在下面的实例中,我们将看到通过组策略来实现在 Samba 域环境中为域用户创建一种交互式的登录提示是多么的简单。 要访问组策略控制台,打开控制面板 -> 系统和安全 -> 管理工具,然后打开组策略管理控制台。 @@ -143,9 +146,9 @@ ping gate ![Manage Samba Domain Group Policy](http://www.tecmint.com/wp-content/uploads/2016/12/Manage-Samba-Domain-Group-Policy.png) ][17] -管理 Samba 域组策略 +*管理 Samba 域组策略* -11、在组策略管理编辑器窗口中,进入到电脑配置 -> 组策略 -> Windows 设置 -> 安全设置 -> 本地策略 -> 安全选项,你将在右侧看到一个新的选项列表。 +11、在组策略管理编辑器窗口中,进入到计算机配置 -> 组策略 -> Windows 设置 -> 安全设置 -> 本地策略 -> 安全选项,你将在右侧看到一个新的选项列表。 在右侧查询并编辑你的定制化设置,参考下图中的两条设置内容。 @@ -153,7 +156,7 @@ ping gate ![Configure Samba Domain Group Policy](http://www.tecmint.com/wp-content/uploads/2016/12/Configure-Samba-Domain-Group-Policy.png) ][18] -配置 Samba 域组策略 +*配置 Samba 域组策略* 12、这两个条目编辑完成后,关闭所有窗口,打开 CMD 窗口,执行以下命令来强制应用组策略。 @@ -164,14 +167,15 @@ gpupdate /force ![Update Samba Domain Group Policy](http://www.tecmint.com/wp-content/uploads/2016/12/Update-Samba-Domain-Group-Policy.png) ][19] -更新 Samba 域组策略 +*更新 Samba 域组策略* + +13、最后,重启你的电脑,当你准备登录进入系统的时候,你就会看到登录提示生效了。 -13、最后,重启你的电脑,当你准备登录进入系统的时候,你就会看到登录提示生效了。 [ ![Samba4 AD Domain Controller Logon Banner](http://www.tecmint.com/wp-content/uploads/2016/12/Samba4-Domain-Controller-User-Login.png) ][20] -Samba4 AD 域控制器登录提示 +*Samba4 AD 域控制器登录提示* 就写到这里吧!组策略是一个操作起来很繁琐和很谨慎的主题,在管理系统的过程中你得非常的小心。还有,注意你设置的组策略不会以任何方式应用到已加入域的 Linux 系统中。 @@ -184,17 +188,17 @@ Samba4 AD 域控制器登录提示 via: http://www.tecmint.com/manage-samba4-dns-group-policy-from-windows/ -作者:[Matei Cezar ][a] +作者:[Matei Cezar][a] 译者:[rusking](https://github.com/rusking) -校对:[校对者ID](https://github.com/校对者ID) +校对:[wxy](https://github.com/wxy) 本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 [a]:http://www.tecmint.com/author/cezarmatei/ -[1]:http://www.tecmint.com/install-samba4-active-directory-ubuntu/ -[2]:http://www.tecmint.com/manage-samba4-active-directory-linux-command-line/ -[3]:http://www.tecmint.com/manage-samba4-ad-from-windows-via-rsat/ -[4]:http://www.tecmint.com/manage-samba4-ad-from-windows-via-rsat/ +[1]:https://linux.cn/article-8065-1.html +[2]:https://linux.cn/article-8070-1.html +[3]:https://linux.cn/article-8097-1.html +[4]:https://linux.cn/article-8097-1.html [5]:http://www.tecmint.com/wp-content/uploads/2016/12/Connect-Samba4-DNS-on-Windows.png [6]:http://www.tecmint.com/wp-content/uploads/2016/12/Add-DNS-A-Record.png [7]:http://www.tecmint.com/set-add-static-ip-address-in-linux/ diff --git a/published/201701/20120301 The Beginner’s Guide to Start Using Vim - part 1.md b/published/201701/20120301 The Beginner’s Guide to Start Using Vim - part 1.md new file mode 100644 index 0000000000..07eda5aaa7 --- /dev/null +++ b/published/201701/20120301 The Beginner’s Guide to Start Using Vim - part 1.md @@ -0,0 +1,126 @@ +Vim 初学者入门指南 +============================================================ +![](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2012/03/vim-beginner-guide-featured.jpg "Vim 初学者入门指南") + + +这篇文章是 [VIM 用户指南][12] 系列文章中的一篇: + +* Vim 初学者入门指南 +* [Vim 快捷键速查表][3] +* [5 个针对有经验用户的 Vim 技巧][4] +* [3 个针对高级用户的 Vim 编辑器有用技巧][5] + +对一个程序员来说,选择一个文本编辑器是一件非常重要的事。因为不同编辑器之间有着不少的差异:图形界面或者非图形界面、不同的快捷键、不同的编程语言支持、不同的插件以及自定义设置等等。我建议不是去搜索最棒的编辑器,而是去选择最适合你的习惯且最适应你的任务的那一个。假如你打算在一个团体中工作,那么最好和你的共事者选择一样的编辑器。这样的话,一旦你在使用中遇到问题,你就可以去向他们寻求帮助。 + +这正是我在几年之前开始使用 Vim 的原因。通常来说,Vim 会被置于传说中的 Emacs 的对立面。我承认我对 Emacs 知之甚少,但是对于它俩,你需要知道的是它们都可以被深度定制,并且在初学时也都非常令人困惑。这个教程并不会介绍有关 Vim 的所有内容,而是将介绍一些基础以使你在最初就能正确使用它,随后还会展示一些小技巧,借此(希望能)让你有能力自己去探索学习。 + +Vim 一词来源于 “VI iMproved”。Vi 是一个被广泛安装于 Unix 系统的非图形界面文本编辑器,并且它也被默认安装在了 Linux 系统中。Vim 是这个原始编辑器的增强版,但是不同于 Vi,并不是每个发行版都默认安装了它。 + +### 安装 + +在 Ubuntu 中可以使用如下命令来安装 Vim: + +``` +sudo apt-get install vim +``` + +如果你已经对某些插件有了兴趣,使用以下命令: + +``` +sudo apt-cache search vim +``` + +这命令将给你输出一个很长的和 Vim 有关的包列表。在这之中,有针对不同编程语言的工具,有插件管理器,等等。 + +在这系列教程中,我将会在 Ubuntu 上使用最新版的 Vim(7.3.154,LCTT 译注:现在最新版为 8.0)。当然你也可以使用其它任何版本。 + +### 热身 + +在终端输入 `vim` 命令,你将会看到一个非常棒的欢迎界面。 + + ![vim-welcome](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2012/02/vim-welcome.jpg "vim-welcome") + +(LCTT 译注:看到了欢迎界面中那行“Help poor children in Uganda!” 了吗?) + +如果你之前从未使用过 Vi 或者 Vim,那么你很可能甚至不知道该怎么退出它... 是的,这是事实。**任何你常用的快捷键在 Vim 中都将失去原有的效果**。(LCTT 译注:网上有个流传的笑话——“如何制造乱码”,“让新手退出 vi”) + +首先,要使用任何命令式的功能,像保存(save)或者退出(exit),你都先得输入一个冒号(`:`)。保存是 `:w` 而退出是 `:q`。如果你想不保存文件就退出,那么就要使用强制退出命令 `:q!`。Vim 中非常棒的一点是你不需要分开输入各个命令,换言之,如果你想保存然后退出,你就可以直接使用 `:wq`。 + +现在,我们退出 Vim 再打开一个文本文件。为此,你只需把想要编辑的文件名加在命令后面即可: + +``` +vim [文本文件名] +``` + + ![vim-file](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2012/02/vim-file.jpg "vim-file") + +一般而言,当你打开一个文本文件,你将会处在查看模式。这使得 Vim 与众不同并且最初会让人感到困惑。Vim 主要由两种模式构成:查看模式和编辑模式。查看模式用于查看内容并且使用一些命令。想要进入编辑模式,只需按 `i` 键进行插入(insert)或者 `a` 键进行添加(add)。想要返回到查看模式或者进行命令式功能的操作,按 `Escape` 键即可。插入(insert)和添加(add)的差异仅仅在于你是想在光标位置之前还是在光标之后进入编辑模式并进行文字输入。要想彻底地明白,你应该亲自去尝试一下。我的建议是:仅在行尾使用添加(add),而在其它时候使用插入(insert)。 + +(LCTT 译注:此段落中“查看模式”原文是 “visual mode”,疑为“view mode”,在此模式下可以查看文本,但是不能进行编辑;而“visual mode” 是编辑模式的一种,可以按 `v` 键进入,然后就可以用方向键从当前光标位置开始进行选择,并以反白的视觉效果显示,通常选择后可以按 `y` 进行复制、按 `d` 进行剪切等操作。) + +要想在文本之中移动光标,你通常可以使用键盘上的方向键,它们无论是在查看模式还是在编辑模式都可以生效。不过,一个真正的纯粹主义者将会告诉你使用按键 `h` 向左,`j` 向下,`k` 向上,`l` 向右来(在查看模式)进行移动。 + +现在你已经明白了如何和简单地控制 Vim,我们再来更加深入一些。 + +### 一些简单命令 + +现在你已经熟悉了在正常模式和插入模式之间进行切换,下面是一些可以在正常模式中使用的命令: + +* `x`:删除一个字符 +* `u`:撤销一个操作(相当与 `Ctrl+z`) +* `dd`:删除一行内容 +* `dw`:删除一个单词 +* `yy`:复制一行内容 +* `yw`:复制一个单词 +* `p`:粘贴一个之前删除或复制的行或者单词 +* `e`:跳到下个单词(LCTT 译注:词尾)(比单纯用方向键更快) +* `r`:替换一个字母(按 `r`,松开,然后再按新字母) + +当然不止这些,不过这些对现在来说已经足够了。如果你掌握了上面的全部,你将能你很顺溜地使用 Vim 了。 + +对于那些还想知道更多的人,我再多提一下。你可以在任何这些命令之前加上一个数值,那么这个命令将被重复执行相应的次数。例如,`5x` 将在当前行连续删除 5 个字母,而 `3p` 将会粘贴 3 次。 + +### 高级命令 + +最后,作为对你自己继续探索的鼓励和示例,这里给出几个高级且常用的命令: + +* `/所搜索的内容`:在文中搜索特定内容 +* `:sp 文本文件名`:将屏幕水平分割成上下两半,新文件展示在另一半。想要在两侧切换焦点,可以使用 `Ctrl+w` 快捷键。 + + ![vim-sp](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2012/02/vim-sp.jpg "vim-sp") +* `:vsp 文本文件名`:同上,但是是垂直分割屏幕 +* `Ctrl+Shift+C` 和 `Ctrl+Shift+V`:在终端中复制和粘贴文本 +* `:! 命令名`:在 Vim 中运行 Vim 外的终端命令,直接发送给 shell。例如,`:! ls` 将在不退出编辑器的同时,显示你当前目录内的文件。 + + ![vim-ls](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2012/02/vim-ls.jpg "vim-ls") + +### 结论 + +我觉得你现在应该已经有了足够的准备来开始使用 Vim。你还可以通过安装各种插件,编辑 `~.vimrc` 文件,或者在 shell 中输入 `vimtutor` 命令来使用交互式教程以学到更多。 + +如果你有任何你想分享的关于 Vim 的其它命令,请在评论中告知我们。 + +-------------------------------------------------------------------------------- + +via: https://www.maketecheasier.com/start-with-vim-linux/ + +作者:[Himanshu Arora][a] +译者:[Yinr](https://github.com/Yinr) +校对:[jasminepeng](https://github.com/jasminepeng) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]:https://www.maketecheasier.com/author/himanshu/ +[1]:https://www.maketecheasier.com/author/adrienbrochard/ +[2]:https://www.maketecheasier.com/start-with-vim-linux/#comments +[3]:https://www.maketecheasier.com/vim-keyboard-shortcuts-cheatsheet/ +[4]:https://www.maketecheasier.com/vim-tips-tricks-for-experienced-users/ +[5]:https://www.maketecheasier.com/vim-tips-tricks-advanced-users/ +[6]:https://www.maketecheasier.com/category/linux-tips/ +[7]:http://www.facebook.com/sharer.php?u=https%3A%2F%2Fwww.maketecheasier.com%2Fstart-with-vim-linux%2F +[8]:http://twitter.com/share?url=https%3A%2F%2Fwww.maketecheasier.com%2Fstart-with-vim-linux%2F&text=The+Beginner%26%238217%3Bs+Guide+to+Start+Using+Vim +[9]:mailto:?subject=The%20Beginner%E2%80%99s%20Guide%20to%20Start%20Using%20Vim&body=https%3A%2F%2Fwww.maketecheasier.com%2Fstart-with-vim-linux%2F +[10]:https://www.maketecheasier.com/turn-dropbox-into-a-blogging-tool-with-scriptogram/ +[11]:https://www.maketecheasier.com/4-sms-back-up-applications-to-keep-your-messages-safe-android/ +[12]:https://www.maketecheasier.com/series/vim-user-guide/ +[13]:https://support.google.com/adsense/troubleshooter/1631343 diff --git a/published/201701/20131227 Vim Keyboard Shortcuts Cheatsheet - part2.md b/published/201701/20131227 Vim Keyboard Shortcuts Cheatsheet - part2.md new file mode 100644 index 0000000000..eede6720b2 --- /dev/null +++ b/published/201701/20131227 Vim Keyboard Shortcuts Cheatsheet - part2.md @@ -0,0 +1,211 @@ +Vim 快捷键速查表 +============================================================ + + + ![](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2013/12/vim-shortcut-cheatsheet-featured.jpg "Vim Keyboard Shortcuts Cheatsheets") + +本文是 [Vim 用户指南][12] 系列的其中一篇: + +* [Vim 初学者入门指南][3] +* Vim 快捷键速查表 +* [5 个针对有经验用户的 Vim 技巧][4] +* [3 个针对高级用户的 Vim 编辑器实用技巧][5] + +Vim 编辑器是一个基于命令行的工具,是传奇编辑器 vi 的增强版。尽管图形界面的富文本编辑有很多,但是熟悉 Vim 对于每一位 Linux 的使用者都能有所帮助——无论你是经验丰富的系统管理员,还是刚上手树莓派的新手用户。 + +这个轻量级的编辑器是个非常强大的工具。在有经验的使用者手中,它能完成不可思议的任务。除了常规的文本编辑功能以外,它还支持一些进阶特性。例如,基于正则表达式的搜索和替换、编码转换,以及语法高亮、代码折叠等的编程特性。 + +使用 Vim 时有一个非常重要的一点需要注意,那就是按键的功能取决于编辑器当前的“模式”。例如,在“普通模式”输入字母`j`时,光标会向下移动一行。而当你在“插入模式”下输入字符,则只是正常的文字录入。 + +下面就是速查表,以便于你充分利用 Vim。 + +### 基本操作 + +| 快捷键 | 功能 | +| --- | --- | +| `Esc` | 从当前模式转换到“普通模式”。所有的键对应到命令。 | +| `i` | “插入模式”用于插入文字。回归按键的本职工作。 | +| `:` | “命令行模式” Vim 希望你输入类似于保存该文档命令的地方。 | + + +### 方向键 + +| 快捷键 | 功能 | +| --- | --- | +| `h` | 光标向左移动一个字符 | +| `j` 或 `Ctrl + J` | 光标向下移动一行 | +| `k` 或 `Ctrl + P` | 光标向上移动一行 | +| `l` | 光标向右移动一个字符 | +| `0` | (数字 0)移动光标至本行开头 | +| `$` | 移动光标至本行末尾 | +| `^` | 移动光标至本行第一个非空字符处 | +| `w` | 向前移动一个词 (上一个字母和数字组成的词之后) | +| `W` | 向前移动一个词 (以空格分隔的词) | +| `5w` | 向前移动五个词 | +| `b` | 向后移动一个词 (下一个字母和数字组成的词之前) | +| `B` | 向后移动一个词 (以空格分隔的词) | +| `5b` | 向后移动五个词 | +| `G` | 移动至文件末尾 | +| `gg` | 移动至文件开头 | + + +### 浏览文档 + +| 快捷键 | 功能 | +| --- | --- | +| `(` | 跳转到上一句 | +| `)` | 跳转到下一句 | +| `{` | 跳转到上一段 | +| `}` | 跳转到下一段 | +| `[[` | 跳转到上一部分 | +| `]]` | 跳转到下一部分 | +| `[]` | 跳转到上一部分的末尾 | +| `][` | 跳转到上一部分的开头 | + + +### 插入文本 + +| 快捷键 | 功能 | +| --- | --- | +| `a` | 在光标后插入文本 | +| `A` | 在行末插入文本 | +| `i` | 在光标前插入文本 | +| `o` | (小写字母 o)在光标下方新开一行 | +| `O` | (大写字母 O)在光标上方新开一行 | + + + +### 特殊插入 + +| 快捷键 | 功能 | +| --- | --- | +| `:r [filename]` | 在光标下方插入文件 [filename] 的内容 | +| `:r ![command]` | 执行命令 [command] ,并将输出插入至光标下方 | + + + +### 删除文本 + +| 快捷键 | 功能 | +| --- | --- | +| `x` | 删除光标处字符 | +| `dw` | 删除一个词 | +| `d0` | 删至行首 | +| `d$` | 删至行末 | +| `d)` | 删至句末 | +| `dgg` | 删至文件开头 | +| `dG` | 删至文件末尾 | +| `dd` | 删除该行 | +| `3dd` | 删除三行 | + +### 简单替换文本 + +| 快捷键 | 功能 | +| --- | --- | +| `r{text}` | 将光标处的字符替换成 {text} | +| `R` | 进入覆写模式,输入的字符将替换原有的字符 | + +### 复制/粘贴文本 + +| 快捷键 | 功能 | +| --- | --- | +| `yy` | 复制当前行至存储缓冲区 | +| `["x]yy` | 复制当前行至寄存器 x | +| `p` | 在当前行之后粘贴存储缓冲区中的内容 | +| `P` | 在当前行之前粘贴存储缓冲区中的内容 | +| `["x]p` | 在当前行之后粘贴寄存器 x 中的内容 | +| `["x]P` | 在当前行之前粘贴寄存器 x 中的内容 | + +### 撤销/重做操作 + +| 快捷键 | 功能 | +| --- | --- | +| `u` | 撤销最后的操作 | +| `Ctrl+r` | 重做最后撤销的操作 | + +### 搜索和替换 + +| 快捷键 | 功能 | +| --- | --- | +| `/search_text` | 检索文档,在文档后面的部分搜索 search_text | +| `?search_text` | 检索文档,在文档前面的部分搜索 search_text | +| `n` | 移动到后一个检索结果 | +| `N` | 移动到前一个检索结果 | +| `:%s/original/replacement` | 检索第一个 “original” 字符串并将其替换成 “replacement” | +| `:%s/original/replacement/g` | 检索并将所有的 “original” 替换为 “replacement” | +| `:%s/original/replacement/gc` | 检索出所有的 “original” 字符串,但在替换成 “replacement” 前,先询问是否替换 | + +### 书签 + +| 快捷键 | 功能 | +| --- | --- | +| `m {a-zA-Z}` | 在当前光标位置设置书签,书签名可用一个大小写字母({a-zA-Z}) | +| `:marks` | 列出所有书签 | +| `{a-zA-Z}` | 跳转到书签 {a-zA-Z} | + + +### 选择文本 + +| 快捷键 | 功能 | +| --- | --- | +| `v` | 进入逐字可视模式 | +| `V` | 进入逐行可视模式 | +| `Esc` | 退出可视模式 | + + +### 改动选中文本 + +| 快捷键 | 功能 | +| --- | --- | +| `~` | 切换大小写 | +| `d` | 删除一个词 | +| `c` | 变更 | +| `y` | 复制 | +| `>` | 右移 | +| `<` | 左移 | +| `!` | 通过外部命令进行过滤 | + + +### 保存并退出 + +| 快捷键 | 功能 | +| --- | --- | +| `:q` | 退出 Vim,如果文件已被修改,将退出失败 | +| `:w` | 保存文件 | +| `:w new_name` | 用 new_name 作为文件名保存文件 | +| `:wq` | 保存文件并退出 Vim | +| `:q!` | 退出 Vim,不保存文件改动 | +| `ZZ` | 退出 Vim,如果文件被改动过,保存改动内容 | +| `ZQ` | 与 :q! 相同,退出 Vim,不保存文件改动 | + +### 下载 Vim 快捷键速查表 + +仅仅是这样是否还不足以满足你?别担心,我们已经为你整理好了一份下载版的速查表,以备不时之需。 + +[点此下载(英文)][14] + +-------------------------------------------------------------------------------- + +via: https://www.maketecheasier.com/vim-keyboard-shortcuts-cheatsheet/ + +作者:[Himanshu Arora][a] +译者:[martin2011qi](https://github.com/martin2011qi) +校对:[wxy](https://github.com/wxy) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]:https://www.maketecheasier.com/author/himanshu/ +[1]:https://www.maketecheasier.com/author/mayank/ +[2]:https://www.maketecheasier.com/vim-keyboard-shortcuts-cheatsheet/#comments +[3]:https://linux.cn/article-8143-1.html +[4]:https://www.maketecheasier.com/vim-tips-tricks-for-experienced-users/ +[5]:https://www.maketecheasier.com/vim-tips-tricks-advanced-users/ +[6]:https://www.maketecheasier.com/category/linux-tips/ +[7]:http://www.facebook.com/sharer.php?u=https%3A%2F%2Fwww.maketecheasier.com%2Fvim-keyboard-shortcuts-cheatsheet%2F +[8]:http://twitter.com/share?url=https%3A%2F%2Fwww.maketecheasier.com%2Fvim-keyboard-shortcuts-cheatsheet%2F&text=Vim+Keyboard+Shortcuts+Cheatsheet +[9]:mailto:?subject=Vim%20Keyboard%20Shortcuts%20Cheatsheet&body=https%3A%2F%2Fwww.maketecheasier.com%2Fvim-keyboard-shortcuts-cheatsheet%2F +[10]:https://www.maketecheasier.com/locate-system-image-tool-in-windows-81/ +[11]:https://www.maketecheasier.com/create-system-image-in-windows8/ +[12]:https://www.maketecheasier.com/series/vim-user-guide/ +[13]:https://support.google.com/adsense/troubleshooter/1631343 +[14]:http://www.maketecheasier.com/cheatsheet/vim-keyboard-shortcuts-cheatsheet/ diff --git a/published/201701/20140812 5 Vim Tips and Tricks for Experienced Users - part3.md b/published/201701/20140812 5 Vim Tips and Tricks for Experienced Users - part3.md new file mode 100644 index 0000000000..0d231d81b0 --- /dev/null +++ b/published/201701/20140812 5 Vim Tips and Tricks for Experienced Users - part3.md @@ -0,0 +1,133 @@ +5 个针对有经验用户的 Vim 实用技巧 +============================================================ + ![](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2014/08/vim-tips-tricks-featured.jpg "5 Vim Tips and Tricks for Experienced Userss") + +这篇文章是 [Vim 用户指南][12]系列文章中的一篇: + +* [Vim 初学者入门指南][3] +* [Vim 快捷键速查表][4] +* 5 个针对有经验用户的 Vim 实用技巧 +* [3 个针对高级用户的 Vim 编辑器实用技巧][5] + +Vim 编辑器提供了很多的特性,要想全部掌握它们很困难。然而,花费更多的时间在命令行编辑器上总是有帮助的。毫无疑问,和 Vim 用户们进行交流能够让你更快地学习新颖有创造性的东西。 + + +**注:** 本文中用到的例子,使用的 Vim 版本是 7.4.52 。 + +### 1、 同时编辑多个文件 + +如果你是一名软件开发者或者把 Vim 作为主要的编辑器,那么可能很多时候你需要同时编辑多个文件。“紧跟(following)”是在同时编辑多个文件时可用的实用技巧。 + +不需要在多个 shell 界面中打开多个文件,你可以通过把多个文件的文件名作为 Vim 命令的参数从而在一个 shell 界面中打开多个文件。比如: + +``` +vim 文件1 文件2 文件3 +``` + +第一个文件(例子中的文件1)将成为当前文件并被读入缓冲区。 + +在编辑器中,使用 `:next` 或 `:n` 命令来移动到下一个文件,使用 `:prev` 或 `:N` 命令返回上一个文件。如果想直接切换到第一个文件或最后一个文件,使用 `:bf` 和 `:bl` 命令。特别地,如果想打开另外的文件并编辑,使用 `:e` 命令并把文件名作为参数(如果该文件不在当前目录中则需要完整路径做为参数)。 + +任何时候如果需要列出当前打开的所有文件,使用 `:ls` 命令。看下面展示的屏幕截图。 + + ![vim-ls](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2014/08/vim-ls.png "vim-ls") + +注意 ”%a” 表示文件在当前活动窗口,而 “#” 表示上一个活动窗口的文件。 + +### 2、 通过自动补全节约时间 + +想节约时间并提高效率吗?使用缩写吧。使用它们能够快速写出文件中多次出现、复杂冗长的词。在 Vim 中缩写命令写就是 `ab` 。 + +比如,当你运行下面的命令以后: + +``` +:ab asap as soon as possible +``` + +文件中出现的每一个 `asap` 都会被自动替换为 `as soon as possible` ,就像你自己输入的一样。 + +类似地,你可以使用缩写来更正常见的输入错误。比如,下面的命令 + +``` +:ab recieve receive +``` + +将会自动更正拼写错误,就像你自己输入的一样。如果在一次特殊情况下你想阻止缩写展开或更正发生,那么你只需要在输入一个单词的最后一个字母以后按 `Ctrl + V` ,然后按空格键。 + +如果你想把刚才使用的缩写保存下来,从而当你下次使用 Vim 编辑器的时候可以再次使用,那么只需将完整的 `ab` 命令(没有起始的冒号)添加到 `/etc/vim/vimrc` 文件中。如果想删除某个缩写,你可以使用 `una` 命令。比如: `una asap` 。 + + +### 3、 切分窗口便于复制/粘贴 + +有时,你需要从一个文件将一段代码或文本的一部分复制到另一个。当使用 GUI(图形界面)编辑器的时候,这很容易实现,但是当使用一个命令行编辑器的时候,这就变得比较困难并且很费时间。幸运的是, Vim 提供了一种高效、节约时间的方式来完成这件事。 + +打开两个文件中的一个然后切分 Vim 窗口来打开另一个文件。可以通过使用 `split` 命令并以文件名作为参数来完成这件事。比如: + +``` +:split test.c +``` + +上面的命令将分离窗口并打开文件 “test.c” + + ![vim-split](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2014/08/vim-split.png "vim-split") + +注意到 `split` 命令水平分离 Vim 窗口。如果你想垂直分离窗口,那么你可以使用 `vsplit` 命令。当同时打开了两个文件并从一个文件中复制好内容以后,按 `Ctrl + W` 切换到另一个文件,然后粘贴。 + +### 4、 保存一个没有权限的已编辑文件 + +有时候当你对一个文件做了大量更改以后才会意识到你对该文件仅有 `只读` 权限。 + + ![vim-sudo](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2014/08/vim-sudo.png "vim-sudo") + +虽然把文件关闭,获取权限以后再重新打开是一种解决方法。但是如果你已经做了大量更改,这样做会很浪费时间,因为在这个过程中所有的更改都会丢失。 Vim 提供了一种方式来处理这种情况:你可以在编辑器中在保存文件前更改文件权限。命令是: + +``` +:w !sudo tee % +``` + +这个命令将会向你询问密码,就像在命令行中使用 `sudo` 一样,然后就能保存更改。 + +**一个相关的技巧**:在 Vim 中编辑一个文件的时候,如果想快速进入命令行提示符,可以在编辑器中运行 `:sh` 命令,从而你将进入一个交互的 shell 中。完成以后,运行 `exit` 命令可以快速回到 Vim 模式中。 + +### 5、 在复制/粘贴过程中保持缩进 + +大多数有经验的程序员在 Vim 上工作时都会启用自动缩进。虽然这是一个节约时间的做法,但是在粘贴一段已经缩进了的代码的时候会产生新的问题。比如,下图是我把一段已缩进代码粘贴到一个在自动缩进的 Vim 编辑器中打开的文件中时遇到的问题: + + ![vim-indentation](https://maketecheasier-2d0f.kxcdn.com/assets/uploads/2014/08/vim-indentation.png "vim-indentation") + +这个问题的解决方法是 `pastetoggle` 选项。在 `/etc/vim/vimrc` 文件中加入下面这行内容: + +``` +set pastetoggle= +``` + +然后当你在 `插入` 模式中准备粘贴代码前先按 `F2` 键,就不会再出现上图中的问题,这样会保留原始的缩进。注意,你可以用其他的任何键来代替 `F2`,如果它已经映射到了别的功能上。 + +### 结论 + +更进一步的提高你的 Vim 编辑器技巧的唯一方法是,在你日复一日的工作中使用命令行编辑器。留意那些耗时多的操作,然后尝试去寻找是否有编辑器命令可以很快地完成这个操作。 + +-------------------------------------------------------------------------------- + +via: https://www.maketecheasier.com/vim-tips-tricks-for-experienced-users/ + +作者:[Himanshu Arora][a] +译者:[ucasFL](https://github.com/ucasFL) +校对:[wxy](https://github.com/wxy) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]:https://www.maketecheasier.com/author/himanshu/ +[1]:https://www.maketecheasier.com/author/himanshu/ +[2]:https://www.maketecheasier.com/vim-tips-tricks-for-experienced-users/#comments +[3]:https://www.maketecheasier.com/start-with-vim-linux/ +[4]:https://www.maketecheasier.com/vim-keyboard-shortcuts-cheatsheet/ +[5]:https://www.maketecheasier.com/vim-tips-tricks-advanced-users/ +[6]:https://www.maketecheasier.com/category/linux-tips/ +[7]:http://www.facebook.com/sharer.php?u=https%3A%2F%2Fwww.maketecheasier.com%2Fvim-tips-tricks-for-experienced-users%2F +[8]:http://twitter.com/share?url=https%3A%2F%2Fwww.maketecheasier.com%2Fvim-tips-tricks-for-experienced-users%2F&text=5+Vim+Tips+and+Tricks+for+Experienced+Users +[9]:mailto:?subject=5%20Vim%20Tips%20and%20Tricks%20for%20Experienced%20Users&body=https%3A%2F%2Fwww.maketecheasier.com%2Fvim-tips-tricks-for-experienced-users%2F +[10]:https://www.maketecheasier.com/enable-two-step-verification-apple-icloud-account/ +[11]:https://www.maketecheasier.com/mistakes-wordpress-user-should-avoid/ +[12]:https://www.maketecheasier.com/series/vim-user-guide/ +[13]:https://support.google.com/adsense/troubleshooter/1631343 diff --git a/published/201701/20150629 Aria2 Vs Wget – Choose your Download Manager.md b/published/201701/20150629 Aria2 Vs Wget – Choose your Download Manager.md new file mode 100644 index 0000000000..625a6ceb8e --- /dev/null +++ b/published/201701/20150629 Aria2 Vs Wget – Choose your Download Manager.md @@ -0,0 +1,73 @@ +aria2 与 wget :选择你的下载管理器 +============================================================ + +任何没有下载管理器的 Linux 操作系统是不完整的。多年来,基于 Linux 的发行版使用 wget 作为默认下载管理器。它是一个很棒的小程序,可以在命令行下工作,如果你需要安装东西、下载东西、运行 shell 脚本等,某种程度上都可以在 wget 中完成任务。在过去的很多年里,我们发现 wget 缺乏一些高级的功能,而它的替代品 ** aria2** ,由于满足了高级 Linux 用户的渴望而受到了许多用户的关注。我们将在本文中回顾 **aria2** 的安装过程以及 wget 和 aria2 之间的区别,因此你可以决定哪个下载管理器最符合你的需要。 + +### 安装 aria2 + +**在 Ubuntu/Debian 中安装 aria2:** + +只要在 Ubuntu 中运行下面的命令安装: + +``` +sudo apt-get install aria2 +``` + +[ + ![aria2](http://linuxpitstop.com/wp-content/uploads/2015/06/aria2.png) +][1] + +**在 Fedora/RHEL/Centos 中安装 aria2:** + +运行下面的命令在 Fedora/RHEL 和基于 Centos 的系统中安装: + +``` +sudo yum install aria2 +``` + +**在 Arch Linux 中安装 aria2:** + +运行下面的命令在基于 Arch Linux 的系统中安装。 + +``` +sudo pacman -Sy aria2 +``` + +### aria2 的重要功能 + +让我们来讨论 aria2 中使它如此受欢迎的重要功能: + +* 通过使用多个连接下载文件,最大限度地利用可用带宽。 +* 同时下载多个文件和同时下载的能力。 +* torrent 客户端提供的所有功能都可以在这个小程序中找到。 +* 它提供 meta 链接下载。 +* 支持使用 JSON-RPC 和 XML-RPC 协议的远程过程调用。 +* 无需等待当前下载完成,轻松批量下载文件。 + +### aria2 的一些副作用: + +aria2 的多线程机制可能会使目标服务器过载。相比下来 wget 就轻量级多了,wget 比 aria2 消耗资源少 20%。aria2 尚未经受 wget 那样巨大的使用规模的测试,因此可能完全准备好成为默认下载管理器。 + +### wget 的重要特性 + +* 当然它是最广泛使用和测试的下载管理器。 +* 它是一个简单的程序,具有较少的功能,但稳定工作了几十年。 +* 默认所有 Linux 发行版上都有,不需要繁重的安装。 +* 与 aria2 相比更轻量级。 + +### 总结 + +虽然 wget 没有丰富的功能,但仍然工作得相当不错,然而,高级用户肯定会喜欢 aria2,因为它满足更快和并发下载的需要。aria2 可能需要很长时间来取代 wget 成为默认下载管理器,而目前 wget 用在几乎所有 linux 发行版的安装程序脚本中。 + +-------------------------------------------------------------------------------- + +via: http://linuxpitstop.com/aria-2-vs-wget/ + +作者:[Aun][a] +译者:[geekpi](https://github.com/geekpi) +校对:[wxy](https://github.com/wxy) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]:http://linuxpitstop.com/author/aun/ +[1]:http://linuxpitstop.com/wp-content/uploads/2015/06/aria2.png diff --git a/published/201701/20151001 Powerline – Adds Powerful Statuslines and Prompts to Vim Editor and Bash Terminal.md b/published/201701/20151001 Powerline – Adds Powerful Statuslines and Prompts to Vim Editor and Bash Terminal.md new file mode 100644 index 0000000000..d826b716c6 --- /dev/null +++ b/published/201701/20151001 Powerline – Adds Powerful Statuslines and Prompts to Vim Editor and Bash Terminal.md @@ -0,0 +1,256 @@ +Powerline:给 Vim 和 Bash 提供更棒的状态行和提示信息 +================================================= + +Powerline 是一个极棒的 [Vim 编辑器][1]的状态行插件,这个插件是使用 Python 开发的,主要用于显示状态行和提示信息,适用于很多软件,比如 bash、zsh、tmux 等等。 + +[ + ![Install Powerline Statuslines in Linux](http://www.tecmint.com/wp-content/uploads/2015/10/Install-Powerline-Statuslines-in-Linux-620x297.png) +][2] + +*Powerline 使 Linux 终端更具威力* + +### 特色 + +1. 使用 python 编写,使其更具扩展性且功能丰富 +2. 稳定易测的代码库,兼容 python 2.6+ 和 python 3 +3. 支持多种 Linux 功能及工具的提示和状态栏 +4. 通过 JSON 保存配置和颜色方案 +5. 快速、轻量级,具有后台守护进程支持,提供更佳的性能 + +### Powerline 效果截图 + +[ + ![Powerline Vim Statuslines](http://www.tecmint.com/wp-content/uploads/2015/10/Powerline-Vim-Statuslines.png) +][3] + +*Vim 中 Powerline 状态行效果* + +在本文中,我会介绍如何安装 Powerline 及其字体,以及如何在 RedHat 和 Debian 类的系统中使 Bash 和 Vim 支持 Powerline。 + +### 第一步:准备好安装 Powerline 所需的软件 + +由于和其它无关项目之间存在命名冲突,因此 powerline 只能放在 PyPI(Python Package Index)中的 `powerline-status` 包下. + +为了从 PyPI 中安装该包,需要先准备好 `pip`(该工具专门用于 Python 包的管理)工具。所以首先要在 Linux 系统下安装好 `pip` 工具。 + +#### 在 Debian、Ubuntu 和 Linux Mint 中安装 pip + +``` +# apt-get install python-pip +``` +**示例输出:** + +``` +Reading package lists... Done +Building dependency tree +Reading state information... Done +Recommended packages: +python-dev-all python-wheel +The following NEW packages will be installed: +python-pip +0 upgraded, 1 newly installed, 0 to remove and 533 not upgraded. +Need to get 97.2 kB of archives. +After this operation, 477 kB of additional disk space will be used. +Get:1 http://archive.ubuntu.com/ubuntu/ trusty-updates/universe python-pip all 1.5.4-1ubuntu3 [97.2 kB] +Fetched 97.2 kB in 1s (73.0 kB/s) +Selecting previously unselected package python-pip. +(Reading database ... 216258 files and directories currently installed.) +Preparing to unpack .../python-pip_1.5.4-1ubuntu3_all.deb ... +Unpacking python-pip (1.5.4-1ubuntu3) ... +Processing triggers for man-db (2.6.7.1-1ubuntu1) ... +Setting up python-pip (1.5.4-1ubuntu3) ... +``` + +#### 在 CentOS、RHEL 和 Fedora 中安装 pip + +在 Fedora 类系统中,需要先打开 [epel 仓库][4],然后按照如下方法安装 pip 包。 + +``` +# yum install python-pip +# dnf install python-pip [Fedora 22+ 以上] +``` + +**示例输出:** + +``` +Installing: +python-pip noarch 7.1.0-1.el7 epel 1.5 M +Transaction Summary +================================================================================= +Install 1 Package +Total download size: 1.5 M +Installed size: 6.6 M +Is this ok [y/d/N]: y +Downloading packages: +python-pip-7.1.0-1.el7.noarch.rpm | 1.5 MB 00:00:01 +Running transaction check +Running transaction test +Transaction test succeeded +Running transaction +Installing : python-pip-7.1.0-1.el7.noarch 1/1 +Verifying : python-pip-7.1.0-1.el7.noarch 1/1 +Installed: +python-pip.noarch 0:7.1.0-1.el7 +Complete! +``` + +### 第二步:在 Linux 中安装 Powerline + +现在可以从 Git 仓库中安装 Powerline 的最新开发版。在此之前系统需要安装好 Git 工具以便可以从仓库拉下代码。 + +``` +# apt-get install git +# yum install git +# dnf install git +``` + +然后你可以通过 `pip` 命令安装 Powerline。 + +``` +# pip install git+git://github.com/Lokaltog/powerline +``` + +**示例输出:** + +``` +Cloning git://github.com/Lokaltog/powerline to /tmp/pip-WAlznH-build +Running setup.py (path:/tmp/pip-WAlznH-build/setup.py) egg_info for package from git+git://github.com/Lokaltog/powerline +warning: no previously-included files matching '*.pyc' found under directory 'powerline/bindings' +warning: no previously-included files matching '*.pyo' found under directory 'powerline/bindings' +Installing collected packages: powerline-status +Found existing installation: powerline-status 2.2 +Uninstalling powerline-status: +Successfully uninstalled powerline-status +Running setup.py install for powerline-status +warning: no previously-included files matching '*.pyc' found under directory 'powerline/bindings' +warning: no previously-included files matching '*.pyo' found under directory 'powerline/bindings' +changing mode of build/scripts-2.7/powerline-lint from 644 to 755 +changing mode of build/scripts-2.7/powerline-daemon from 644 to 755 +changing mode of build/scripts-2.7/powerline-render from 644 to 755 +changing mode of build/scripts-2.7/powerline-config from 644 to 755 +changing mode of /usr/local/bin/powerline-config to 755 +changing mode of /usr/local/bin/powerline-lint to 755 +changing mode of /usr/local/bin/powerline-render to 755 +changing mode of /usr/local/bin/powerline-daemon to 755 +Successfully installed powerline-status +Cleaning up... +``` + +### 第三步:在 Linux 中安装 Powerline 的字体 + +Powerline 使用特殊的符号来为开发者显示特殊的箭头效果和符号内容。因此你的系统中必须要有符号字体或者补丁过的字体。 + +通过下面的 [wget][5] 命令下载最新的系统字体及字体配置文件。 + +``` +# wget https://github.com/powerline/powerline/raw/develop/font/PowerlineSymbols.otf +# wget https://github.com/powerline/powerline/raw/develop/font/10-powerline-symbols.conf +``` + +然后你将下载的字体放到字体目录下 `/usr/share/fonts` 或者 `/usr/local/share/fonts`,或者你可以通过 `xset q` 命令找到一个有效的字体目录。 + +``` +# mv PowerlineSymbols.otf /usr/share/fonts/ +``` + +接下来你需要通过如下命令更新你系统的字体缓存。 + +``` +# fc-cache -vf /usr/share/fonts/ +``` + +其次安装字体配置文件。 + +``` +# mv 10-powerline-symbols.conf /etc/fonts/conf.d/ +``` + +注意:如果相应的符号没有出现,可以尝试关闭终端会话并重启 X window,这样就会生效了。 + +### 第四步:给 Bash Shell 和 Vim 状态行设置 Powerline + +在这一节将介绍 bash shell 和 vim 编辑器中关于 Powerline 的配置。首先通过在 `~/.bashrc` 中添加如下内容以便设置终端为 256 色。 + +``` +export TERM="screen-256color" +``` + +#### 打开 Bash Shell 中的 Powerline + +如果希望在 bash shell 中默认打开 Powerline,可以在 `~/.bashrc` 中添加如下内容。 + +首先通过如下命令获取 powerline 的安装位置。 + +``` +# pip show powerline-status +Name: powerline-status +Version: 2.2.dev9999-git.aa33599e3fb363ab7f2744ce95b7c6465eef7f08 +Location: /usr/local/lib/python2.7/dist-packages +Requires: +``` + +一旦找到 powerline 的具体位置后,根据你系统的情况替换到下列行中的 `/usr/local/lib/python2.7/dist-packages` 对应的位置。 + +``` +powerline-daemon -q +POWERLINE_BASH_CONTINUATION=1 +POWERLINE_BASH_SELECT=1 +. /usr/local/lib/python2.7/dist-packages/powerline/bindings/bash/powerline.sh +``` + +然后退出后重新登录,现在 powerline 的状态行应该如下显示了。 + +[ + ![Bash Powerline Statuslines](http://www.tecmint.com/wp-content/uploads/2015/10/Bash-Powerline-Statuslines.gif) +][6] + +现在切换目录并注意显示你当前路径的面包屑导航提示的变化。 + +如果远程 Linux 服务器上安装了 powerline,你能看到后台挂起的任务,当你用 ssh 登录上去时,会看到该提示增加了主机名。 + +#### 在 Vim 中打开 Powerline + +如果你喜欢使用 vim,正好有一个 vim 的强力插件。可以在 `~/.vimrc` 中添加如下内容打开该插件(LCTT 译注:注意同样需要根据你的系统情况修改路径)。 + +``` +set rtp+=/usr/local/lib/python2.7/dist-packages/powerline/bindings/vim/ +set laststatus=2 +set t_Co=256 +``` + +然后你打开 vim 后会看到一个新的状态行: + +[ + ![Vim Powerline Statuslines](http://www.tecmint.com/wp-content/uploads/2015/10/Vim-Powerline-Statuslines.gif) +][7] + +### 总结 + +Powerline 可以在某些软件中提供颜色鲜艳、很优美的状态行及提示内容,这对编程环境有利。希望这篇指南对您有帮助,如果您需要帮助或者有任何好的想法,请留言给我。 + +-------------------------------------------------------------------------------- + +作者简介: + +![](http://1.gravatar.com/avatar/7badddbc53297b2e8ed7011cf45df0c0?s=128&d=blank&r=g) + +我是Ravi Saive,TecMint的作者。一个喜欢分享诀窍和想法的电脑极客及Linux专家。我的大部分服务都运行在开源平台Linux中。关注我的Twitter,Facebook和Google+。 + +-------------------------------------------------------------------------------- + +via: http://www.tecmint.com/powerline-adds-powerful-statuslines-and-prompts-to-vim-and-bash/ + +作者:[Ravi Saive][a] +译者:[beyondworld](https://github.com/beyondworld) +校对:[wxy](https://github.com/wxy) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]:http://www.tecmint.com/author/admin/ +[1]:http://www.tecmint.com/vi-editor-usage/ +[2]:http://www.tecmint.com/wp-content/uploads/2015/10/Install-Powerline-Statuslines-in-Linux.png +[3]:http://www.tecmint.com/wp-content/uploads/2015/10/Powerline-Vim-Statuslines.png +[4]:https://linux.cn/article-2324-1.html +[5]:http://www.tecmint.com/10-wget-command-examples-in-linux/ +[6]:http://www.tecmint.com/wp-content/uploads/2015/10/Bash-Powerline-Statuslines.gif +[7]:http://www.tecmint.com/wp-content/uploads/2015/10/Vim-Powerline-Statuslines.gif diff --git a/published/201701/20160110 3 Ways to Permanently and Securely Delete ‘Files and Directories in Linux.md b/published/201701/20160110 3 Ways to Permanently and Securely Delete ‘Files and Directories in Linux.md new file mode 100644 index 0000000000..c62d07c0b1 --- /dev/null +++ b/published/201701/20160110 3 Ways to Permanently and Securely Delete ‘Files and Directories in Linux.md @@ -0,0 +1,212 @@ +3 个在 Linux 中永久并安全删除文件和目录的方法 +============================================================ + +在大多数情况下,我们习惯于使用 `Delete` 键、垃圾箱或 `rm` 命令[从我们的计算机中删除文件][1],但这不是永久安全地从硬盘中(或任何存储介质)删除文件的方法。 + +该文件只是对用户隐藏,它驻留在硬盘上的某个地方。它有可能被数据窃贼、执法取证或其它方式来恢复。 + +假设文件包含密级或机密内容,例如安全系统的用户名和密码,具有必要知识和技能的攻击者可以轻松地[恢复删除文件的副本][2]并访问这些用户凭证(你可以猜测到这种情况的后果)。 + +在本文中,我们将解释一些命令行工具,用于永久并安全地删除 Linux 中的文件。 + +### 1、 shred – 覆盖文件来隐藏内容 + +`shred` 会覆盖文件来隐藏它的内容,并且也可以选择删除它。 + +``` +$ shred -zvu -n 5 passwords.list +``` + +在下面的命令中,选项有: + +1. `-z` - 用零覆盖以隐藏碎片 +2. `-v` - 显示操作进度 +3. `-u` - 在覆盖后截断并删除文件 +4. `-n` - 指定覆盖文件内容的次数(默认值为3) + +[ + ![shred - overwrite a file to hide its contents](http://www.tecmint.com/wp-content/uploads/2017/01/shred-command-example.png) +][3] + +*shred - 覆盖文件来隐藏它的内容* + +你可以在 `shred` 的帮助页中找到更多的用法选项和信息: + +``` +$ man shred +``` + +### 2、 wipe – 在 Linux 中安全删除文件 + +`wipe` 命令可以安全地擦除磁盘中的文件,从而不可能[恢复删除的文件或目录内容][4]。 + +首先,你需要安装 `wipe` 工具,运行以下适当的命令: + +``` +$ sudo apt-get install wipe [Debian 及其衍生版] +$ sudo yum install wipe [基于 RedHat 的系统] +``` + +下面的命令会销毁 private 目录下的所有文件。 + +``` +$ wipe -rfi private/* +``` + +当使用下面的标志时: + +1. `-r` - 告诉 `wipe` 递归地擦除子目录 +2. `-f` - 启用强制删除并禁用确认查询 +3. `-i` - 显示擦除进度 + +[ + ![Wipe - Securely Erase Files in Linux](http://www.tecmint.com/wp-content/uploads/2017/01/Wipe-Securely-Erase-Files.png) +][5] + +*wipe – 在 Linux 中安全擦除文件* + +注意:`wipe` 仅可以在磁性存储上可以可靠地工作,因此对固态磁盘(内存)请使用其他方法。 + +阅读 `wipe` 手册以获取其他使用选项和说明: + +``` +$ man wipe +``` + +### 3、 Linux 中的安全删除工具集 + +secure-delete 是一个安全文件删除工具的集合,它包含用于安全删除文件的 `srm`(secure_deletion)工具。 + +首先,你需要使用以下相关命令安装它: + +``` +$ sudo apt-get install secure-delete [On Debian and its derivatives] +$ sudo yum install secure-delete [On RedHat based systems] +``` + +安装完成后,你可以使用 `srm` 工具在 Linux 中安全地删除文件和目录。 + +``` +$ srm -vz private/* +``` + +下面是使用的选项: + +1. `-v` – 启用 verbose 模式 +2. `-z` – 用0而不是随机数据来擦除最后的写入 + +[ + ![srm - Securely Delete Files in Linux ](http://www.tecmint.com/wp-content/uploads/2017/01/srm-securely-delete-Files-in-Linux.png) +][6] + +*srm – 在 Linux 中安全删除文件* + +阅读 srm 手册来获取更多的使用选项和信息: + +``` +$ man srm +``` + +### 4、 sfill -安全免费的磁盘 / inode 空间擦除器 + +`sfill` 是 secure-deletetion 工具包的一部分,是一个安全免费的磁盘和 inode 空间擦除器,它以安全的方法删除可用磁盘空间中的文件。 `sfill` 会[检查指定分区上的可用空间][7],并使用来自 `/dev/urandom` 的随机数据填充它。 + +以下命令将在我的根分区上执行 `sfill`,使用 `-v' 选项启用 verbose 模式: + +``` +$ sudo sfill -v /home/aaronkilik/tmp/ +``` + +假设你创建了一个单独的分区 `/home` 来存储正常的系统用户主目录,你可以在该分区上指定一个目录,以便在其上应用 `sfill`: + +``` +$ sudo sfill -v /home/username +``` + +你可以在 sfill 的手册上看到一些限制,你也可以看到额外的使用标志和命令: + +``` +$ man sfill +``` + +注意:secure-deletetion 工具包中的另外两个工具(`sswap` 和 `sdmem`)与本指南的范围不直接相关,但是,为了将来的使用和传播知识的目的,我们会在下面介绍它们。 + +### 5、 sswap – 安全 swap 擦除器 + +它是一个安全的分区擦除器,`sswap` 以安全的方式删除 swap 分区上存在的数据。 + +警告:请记住在使用 `sswap` 之前卸载 swap 分区! 否则你的系统可能会崩溃! + +要找到交换分区(并检查分页和交换设备/文件是否已经使用,请使用 `swapon` 命令),接下来,使用 `swapoff` 命令禁用分页和交换设备/文件(使 swap 分区不可用)。 + +然后在(关闭的) swap 分区上运行 `sswap` 命令: + +``` +$ cat /proc/swaps +$ swapon +$ sudo swapoff /dev/sda6 +$ sudo sswap /dev/sda6 #this command may take some time to complete with 38 default passes +``` + +[ + ![sswap - Secure Swap Wiper](http://www.tecmint.com/wp-content/uploads/2017/01/sswap-Secure-Swap-Wiper.png) +][8] + +*sswap – 安全 swap 擦除器* + +阅读 `sswap` 的手册来获取更多的选项和信息: + +``` +$ man sswap +``` + +### 6、 sdmem – 安全内存擦除器 + +`sdmem` 是一个安全的内存擦除器,其设计目的是以安全的方式删除存储器(RAM)中的数据。 + +它最初命名为 [smem][9],但是因为在 Debain 系统上存在另一个包 [smem - 报告每个进程和每个用户的内存消耗][10],开发人员决定将它重命名为 `sdmem`。 + +``` +$ sudo sdmem -f -v +``` + +关于更多的使用信息,阅读 `sdmen` 的手册: + +``` +$ man sdmem +``` + +**推荐阅读:** [在 Linux 系统下使用 PhotoRec & TestDisk 工具来恢复文件][11]。 + +就是这样了!在本文中,我们查看了一系列可以永久安全地删除 Linux 中的文件的工具。像往常一样,通过下面的评论栏发表你对本篇文章的想法或建议。 + +-------------------------------------------------------------------------------- + +作者简介: + +![](http://1.gravatar.com/avatar/4e444ab611c7b8c7bcb76e58d2e82ae0?s=128&d=blank&r=g) + +Aaron Kili 是一个 Linux 系统及 F.O.S.S 爱好者,即将成为一名系统管理员及 Web 开发人员,他现在是 TecMint 网站的内容创建者,他喜欢使用电脑来工作,并且他坚信分享知识是一种美德。 + +-------------------------------------------------------------------------------- + +via: http://www.tecmint.com/permanently-and-securely-delete-files-directories-linux/ + +作者:[Aaron Kili][a] +译者:[geekpi](https://github.com/geekpi) +校对:[jasminepeng](https://github.com/jasminepeng) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]:http://www.tecmint.com/author/admin/ +[1]:https://linux.cn/article-7954-1.html +[2]:https://linux.cn/article-8122-1.html +[3]:http://www.tecmint.com/wp-content/uploads/2017/01/shred-command-example.png +[4]:https://linux.cn/article-7974-1.html +[5]:http://www.tecmint.com/wp-content/uploads/2017/01/Wipe-Securely-Erase-Files.png +[6]:http://www.tecmint.com/wp-content/uploads/2017/01/srm-securely-delete-Files-in-Linux.png +[7]:https://linux.cn/article-8024-1.html +[8]:http://www.tecmint.com/wp-content/uploads/2017/01/sswap-Secure-Swap-Wiper.png +[9]:https://linux.cn/article-7681-1.html +[10]:https://linux.cn/article-7681-1.html +[11]:https://linux.cn/article-8122-1.html diff --git a/published/201701/20160610 Setting Up Real-Time Monitoring with Ganglia.md b/published/201701/20160610 Setting Up Real-Time Monitoring with Ganglia.md new file mode 100755 index 0000000000..0f3bf6ae9a --- /dev/null +++ b/published/201701/20160610 Setting Up Real-Time Monitoring with Ganglia.md @@ -0,0 +1,220 @@ +使用 Ganglia 对 linux 网格和集群服务器进行实时监控 +=========== + +从系统管理员接手服务器和主机管理以来,像应用监控这样的工具就成了他们的好帮手。其中比较有名的有 [Nagios][11]、 [Zabbix][10]、 [Icinga][9] 和 Centreon。以上这些是重量级的监控工具,让一个新手管理员来设置,并使用其中的高级特性是有些困难的。 + +本文将向你介绍 Ganglia,它是一个易于扩展的监控系统。使用它可以实时查看 Linux 服务器和集群(图形化展示)中的各项性能指标。 + +![Install Gangila Monitoring in Linux](http://www.tecmint.com/wp-content/uploads/2016/06/Install-Gangila-Monitoring-in-Linux.png) + +*在 Linux 上安装 Ganglia* + +**Ganglia** 能够让你以**集群**(按服务器组)和**网格**(按地理位置)的方式更好地组织服务器。 + +这样,我们可以创建一个包含所有远程主机的网格,然后将那些机器按照其它标准分组成小的集合。 + +此外, Ganglia 的 web 页面对移动设备进行过优化,也允许你导出 `csv` 和 `.json` 格式的数据。 + +我们的测试环境包括一个安装 Ganglia 的主节点服务器 CentOS 7 (IP 地址 192.168.0.29),和一个作为被监控端的 Ubuntu 14.04 主机(192.168.0.32)。我们将通过 Ganglia 的 Web 页面来监控这台 Ubuntu 主机。 + +下面的例子可以给大家提供参考,CentOS7 作为主节点,Ubuntu 作为被监控对象。 + +### 安装和配置 Ganglia + +请遵循以下步骤,在主节点服务器安装监控工具。 + +1、 启用 [EPEL 仓库][7] ,然后安装 Ganglia 和相关工具: + +``` +# yum update && yum install epel-release +# yum install ganglia rrdtool ganglia-gmetad ganglia-gmond ganglia-web +``` + +在上面这步随 Ganglia 将安装一些应用,它们的功能如下: + +- `rrdtool`,Round-Robin 数据库,它是一个储存并图形化显示随着时间变化的数据的工具; +- `ganglia-gmetad` 一个守护进程,用来收集被监控主机的数据。被监控主机与主节点主机都要安装 Ganglia-gmond(监控守护进程本身); +- `ganglia-web` 提供 Web 前端,用于显示监控系统的历史数据和图形。 +   +2、 使用 Apache 提供的基本认证功能,为 Ganglia Web 界面(`/usr/share/ganglia`)配置身份认证。 + +如果你想了解更多高级安全机制,请参阅 Apache 文档的 [授权与认证][6]部分。 + +为完成这个目标,我们需要创建一个用户名并设定一个密码,以访问被 Apache 保护的资源。在本例中,我们先来创建一个叫 `adminganglia` 的用户名,然后给它分配一个密码,它将被储存在 `/etc/httpd/auth.basic`(可以随意选择另一个目录 和/或 文件名, 只要 Apache 对此有读取权限就可以。)  + +``` +# htpasswd -c /etc/httpd/auth.basic adminganglia +``` + +给 adminganglia 输入两次密码完成密码设置。 + +3、 修改配置文件 `/etc/httpd/conf.d/ganglia.conf`: + +``` +Alias /ganglia /usr/share/ganglia + +AuthType basic +AuthName "Ganglia web UI" +AuthBasicProvider file +AuthUserFile "/etc/httpd/auth.basic" +Require user adminganglia + +``` + +4、 编辑 `/etc/ganglia/gmetad.conf`: + +首先,使用 `gridname` 指令来为网格设置一个描述性名称。 + +``` +gridname "Home office" +``` + +然后,使用 `data_source` 指令,后面跟集群名(服务器组)、轮询时间间隔(秒)、主节点主机和被监控节点的 IP 地址: + +``` +data_source "Labs" 60 192.168.0.29:8649 # 主节点 +data_source "Labs" 60 192.168.0.32 # 被监控节点 +``` + +5、 编辑 `/etc/ganglia/gmond.conf`。 + +a) 确保集群的配置类似下面。 + +``` +cluster { +name = "Labs" # gmetad.conf 中的 data_source 指令的名字 +owner = "unspecified" +latlong = "unspecified" +url = "unspecified" +} +``` + +b) 在 `udp_send_chanel` 中,注释掉 `mcast_join`: + +``` +udp_send_channel { +# mcast_join = 239.2.11.71 +host = localhost +port = 8649 +ttl = 1 +} +``` + +c) 在 `udp_recv_channel` 中,注释掉 `mcast_join` 和 `bind` 部分: + +``` +udp_recv_channel { +# mcast_join = 239.2.11.71 ## comment out +port = 8649 +# bind = 239.2.11.71 ## comment out +} +``` + +保存并退出。 + +6、打开 8649/udp 端口,使用 SELinux 确保 php 脚本(通过 Apache 运行)能够连接到网络: + +``` +# firewall-cmd --add-port=8649/udp +# firewall-cmd --add-port=8649/udp --permanent +# setsebool -P httpd_can_network_connect 1 +``` + +7、重启 Apache、gmetad、gmond,并确保它们启用了“开机启动”。 + +``` +# systemctl restart httpd gmetad gmond +# systemctl enable httpd gmetad httpd +``` + +至此,我们现在能够打开 Ganglia 的 Web 页面 `http://192.168.0.29/ganglia` 并用步骤 2 中设置的凭证登录。 + + [![Gangila Web Interface](http://www.tecmint.com/wp-content/uploads/2016/06/Gangila-Web-Interface.png)][5] + +*Gangila Web 页面* + +8、 在 **Ubuntu** 主机上,只需安装 Ganglia-monitor,等同于 CentOS 上的 ganglia-gmond: + +``` +$ sudo aptitude update && aptitude install ganglia-monitor + +``` + +9、 编辑被监控主机的 `/etc/ganglia/gmond.conf` 文件。与主节点主机上是相同的文件,除了被注释掉的 `cluster`, `udp_send_channel` , `udp_recv_channel` 这里不应被注释: + +``` +cluster { +name = "Labs" # The name in the data_source directive in gmetad.conf +owner = "unspecified" +latlong = "unspecified" +url = "unspecified" +} +udp_send_channel { +mcast_join = 239.2.11.71 +host = localhost +port = 8649 +ttl = 1 +} +udp_recv_channel { +mcast_join = 239.2.11.71 ## comment out +port = 8649 +bind = 239.2.11.71 ## comment out +} +``` + +之后重启服务。 + +``` +$ sudo service ganglia-monitor restart +``` + +10、 刷新页面,你将在 Home office grid / Labs cluster 中看到两台主机的各种统计及图形化的展示(用下拉菜单选择集群,本例中为 Labs): + +[![Ganglia Home Office Grid Report](http://www.tecmint.com/wp-content/uploads/2016/06/Ganglia-Home-Office-Grid-Report.png)][4] + +*Ganglia 中 Home office 网格报告* + +使用菜单按钮(如上指出的),你可以获取到每台服务器和集群的信息。还可以使用 对比主机Compare Hosts选项卡来比较集群中所有服务器的状态。 + +可以使用正则表达式选择一组服务器,立刻就可以看到它们性能的对比: + +[![Ganglia Host Server Information](http://www.tecmint.com/wp-content/uploads/2016/06/Ganglia-Server-Information.png)][3] + +*Ganglia 服务器信息* + +我最喜欢的一个特点是对移动端有友好的总结界面,可以通过 Mobile 选项来访问。选择你感兴趣的集群,然后选中一个主机。 + +[![Ganglia Mobile Friendly Summary View](http://www.tecmint.com/wp-content/uploads/2016/06/Ganglia-Mobile-View.png)][2] + +*Ganglia 移动端总结截图* + +### 总结 + +本篇文章向大家介绍了 Ganglia,它是一个功能强大、扩展性很好的监控工具,主要用来监控集群和网格。它可以随意安装,便捷的组合各种功能(你甚至可以尝试一下[官方网站][1] 提供的 Demo)。 + +此时,你可能会发现许多知名的 IT 或非 IT 的企业在使用 Ganglia。除了我们在文章中提及的之外,还有很多理由这样做,其中易用性,统计的图形化(在名字旁附上脸部照片更清晰,不是吗)可能是最重要的原因。 + +但是请不要拘泥于本篇文章,尝试一下自己去做。如果你有任何问题,欢迎给我留言。 + +-------------------------------------------------------------------------------- + +via: http://www.tecmint.com/install-configure-ganglia-monitoring-centos-linux/ + +作者:[Gabriel Cánepa][a] +译者:[ivo-wang](https://github.com/ivo-wang) +校对:[jasminepeng](https://github.com/jasminepeng) + +本文由 [LCTT](https://github.com/LCTT/TranslateProject) 组织编译,[Linux中国](https://linux.cn/) 荣誉推出 + +[a]: http://www.tecmint.com/author/gacanepa/ +[1]:http://ganglia.info/ +[2]:http://www.tecmint.com/wp-content/uploads/2016/06/Ganglia-Mobile-View.png +[3]:http://www.tecmint.com/wp-content/uploads/2016/06/Ganglia-Server-Information.png +[4]:http://www.tecmint.com/wp-content/uploads/2016/06/Ganglia-Home-Office-Grid-Report.png +[5]:http://www.tecmint.co m/wp-content/uploads/2016/06/Gangila-Web-Interface.png +[6]:http://httpd.apache.org/docs/current/howto/auth.html +[7]:https://linux.cn/article-2324-1.html +[8]:http://www.tecmint.com/wp-content/uploads/2016/06/ Install-Gangila-Monitoring-in-Linux.png +[9]:http://www.tecmint.com/install-icinga-in-centos-7/ +[10]:http://www.tecmint.com/install-and-configure-zabbix-monitoring-on-debian-centos-rhel/ +[11]:http://www.tecmint.com/install-nagios-in-linux/ diff --git a/published/201701/20160823 Getting Started with HTTP2 - Part 1.md b/published/201701/20160823 Getting Started with HTTP2 - Part 1.md new file mode 100644 index 0000000000..62358240f2 --- /dev/null +++ b/published/201701/20160823 Getting Started with HTTP2 - Part 1.md @@ -0,0 +1,62 @@ +初识 HTTP/2(一) +============================================================ + ![](https://static.viget.com/_284x284_crop_center-center/http2-pizza.png?mtime=20160822160641) + +> 用披萨来说明当你订单数很大的时候 HTTP/2 是怎么打败 HTTP/1.1 的。 + +在建立网站和应用的方式上 HTTP/2 有些令人惊叹的改变,在 HTTP/2 发布后的一年半,几乎 [10% 的网站使用了 HTTP/2][4]。它绝对值得采用,但是这篇文章应该首先推给使用 HTTP/2 的前端开发者。这个连载的文章是指导前端开发者怎么转换到 HTTP/2。 + +本文涵盖了 HTTP/2 对 HTTP/1.1 来说有什么提高的内容,并且向前端开发者介绍了 HTTP/2。 + +### 再次让我想起什么是 HTTP ... + +超文本传输协议,也就是 HTTP,这个协议决定了 web 内容怎么传输。HTTP/1.1 在 1999 年被标准化,那时候的 web 和现在有很大的不同,表格霸占了整个网络。样式通常被内联在元素中,如果网站管理员更加的细致,他们会在头部写个 `