<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>操作系统 | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/category/operatingsystem/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Mon, 12 Dec 2022 02:56:31 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2</generator>
	<item>
		<title>eBPF 介绍</title>
		<link>https://coolshell.cn/articles/22320.html</link>
					<comments>https://coolshell.cn/articles/22320.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 10 Dec 2022 02:38:51 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[Debug]]></category>
		<category><![CDATA[eBPF]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=22320</guid>

					<description><![CDATA[<p>很早前就想写一篇关于eBPF的文章，但是迟迟没有动手，这两天有点时间，所以就来写一篇，这文章主要还是简单的介绍eBPF 是用来干什么的，并通过几个示例来介绍是怎...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/22320.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/22320.html">eBPF 介绍</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright wp-image-22329 size-full" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.jpeg" alt="" width="301" height="167" />很早前就想写一篇关于eBPF的文章，但是迟迟没有动手，这两天有点时间，所以就来写一篇，这文章主要还是简单的介绍eBPF 是用来干什么的，并通过几个示例来介绍是怎么玩的，这个技术非常非常之强，Linux 操作系统的观测性实在是太强大了，并在 BCC 加持下变得一览无余。这个技术不是一般的运维人员或是系统管理员可以驾驭的，这个还是要有底层系统知识并有一定开发能力的技术人员才能驾驭的了的。<strong>我在这篇文章的最后给了个彩蛋。</strong></p>
<h4>介绍</h4>
<p>eBPF（extened Berkeley Packet Filter）是一种内核技术，它允许开发人员在不修改内核代码的情况下运行特定的功能。eBPF 的概念源自于 Berkeley Packet Filter（BPF），后者是由贝尔实验室开发的一种网络过滤器，可以捕获和过滤网络数据包。</p>
<p>出于对更好的 Linux 跟踪工具的需求，eBPF 从 <a href="https://illumos.org/books/dtrace/chp-intro.html">dtrace</a>中汲取灵感，dtrace 是一种主要用于 Solaris 和 BSD 操作系统的动态跟踪工具。与 dtrace 不同，Linux 无法全面了解正在运行的系统，因为它仅限于系统调用、库调用和函数的特定框架。<a href="https://www.kernel.org/doc/html/latest/bpf/index.html">在Berkeley Packet Filter</a>  (BPF)（一种使用内核 VM 编写打包过滤代码的工具）的基础上，一小群工程师开始扩展 BPF 后端以提供与 dtrace 类似的功能集。 eBPF 诞生了。<strong>2014 年随 Linux 3.18 首次限量发布，充分利用 eBPF 至少需要 Linux 4.4 以上版本</strong>。</p>
<p><span id="more-22320"></span></p>
<p>eBPF 比起传统的 BPF 来说，传统的 BPF 只能用于网络过滤，而 eBPF 则可以用于更多的应用场景，包括网络监控、安全过滤和性能分析等。另外，eBPF 允许常规用户空间应用程序将要在 Linux 内核中执行的逻辑打包为字节码，当某些事件（称为挂钩）发生时，内核会调用 eBPF 程序。此类挂钩的示例包括系统调用、网络事件等。用于编写和调试 eBPF 程序的最流行的工具链称为 <a href="https://github.com/iovisor/bcc">BPF 编译器集合</a> (BCC)，它基于 LLVM 和 CLang。</p>
<p>eBPF 有一些类似的工具。例如，SystemTap 是一种开源工具，可以帮助用户收集 Linux 内核的运行时数据。它通过动态加载内核模块来实现这一功能，类似于 eBPF。另外，DTrace 是一种动态跟踪和分析工具，可以用于收集系统的运行时数据，类似于 eBPF 和 SystemTap。<code>[1]</code></p>
<p>以下是一个简单的比较表格，可以帮助您更好地了解 eBPF、SystemTap 和 DTrace 这三种工具的不同之处：<code>[1]</code></p>
<table>
<thead>
<tr>
<th>工具</th>
<th>eBPF</th>
<th>SystemTap</th>
<th>DTrace</th>
</tr>
</thead>
<tbody>
<tr>
<td>定位</td>
<td>内核技术，可用于多种应用场景</td>
<td>内核模块</td>
<td>动态跟踪和分析工具</td>
</tr>
<tr>
<td>工作原理</td>
<td>动态加载和执行无损编译过的代码</td>
<td>动态加载内核模块</td>
<td>动态插接分析器，通过 probe 获取数据并进行分析</td>
</tr>
<tr>
<td>常见用途</td>
<td>网络监控、安全过滤、性能分析等</td>
<td>系统性能分析、故障诊断等</td>
<td>系统性能分析、故障诊断等</td>
</tr>
<tr>
<td>优点</td>
<td>灵活、安全、可用于多种应用场景</td>
<td>功能强大、可视化界面</td>
<td>功能强大、高性能、支持多种编程语言</td>
</tr>
<tr>
<td>缺点</td>
<td>学习曲线高，安全性依赖于编译器的正确性</td>
<td>学习曲线高，安全性依赖于内核模块的正确性</td>
<td>配置复杂，对系统性能影响较大</td>
</tr>
</tbody>
</table>
<p style="text-align: center;">对比表格<code>[1]</code></p>
<p>从上表可以看出，eBPF、SystemTap 和 DTrace 都是非常强大的工具，可以用于收集和分析系统的运行情况。<code>[1]</code></p>
<h4>用途</h4>
<p>eBPF 是一种非常灵活和强大的内核技术，可以用于多种应用场景。下面是 eBPF 的一些常见用途：<code>[1]</code></p>
<ul>
<li>网络监控：eBPF 可以用于捕获网络数据包，并执行特定的逻辑来分析网络流量。例如，可以使用 eBPF 程序来监控网络流量，并在发现异常流量时进行警报。<code>[1]</code></li>
<li>安全过滤：eBPF 可以用于对网络数据包进行安全过滤。例如，可以使用 eBPF 程序来阻止恶意流量的传播，或者在发现恶意流量时对其进行拦截。<code>[1]</code></li>
<li>性能分析：eBPF 可以用于对内核的性能进行分析。例如，可以使用 eBPF 程序来收集内核的性能指标，并通过特定的接口将其可视化。这样，可以更好地了解内核的性能瓶颈，并进行优化。<code>[1]</code></li>
<li>虚拟化：eBPF 可以用于虚拟化技术。例如，可以使用 eBPF 程序来收集虚拟机的性能指标，并进行负载均衡。这样，可以更好地利用虚拟化环境的资源，提高系统的性能和稳定性。<code>[1]</code></li>
</ul>
<p>总之，eBPF 的常见用途非常广泛，可以用于网络监控、安全过滤、性能分析和虚拟化等多种应用场景。<code>[1]</code></p>
<h4>工作原理</h4>
<p>eBPF 的工作原理主要分为三个步骤：加载、编译和执行。</p>
<p>eBPF 需要在内核中运行。这通常是由用户态的应用程序完成的，它会通过系统调用来加载 eBPF 程序。在加载过程中，内核会将 eBPF 程序的代码复制到内核空间。</p>
<p>eBPF 程序需要经过编译和执行。这通常是由Clang/LLVM的编译器完成，然后形成字节码后，将用户态的字节码装载进内核，Verifier会对要注入内核的程序进行一些内核安全机制的检查,这是为了确保 eBPF 程序不会破坏内核的稳定性和安全性。在检查过程中，内核会对 eBPF 程序的代码进行分析，以确保它不会进行恶意操作，如系统调用、内存访问等。如果 eBPF 程序通过了内核安全机制的检查，它就可以在内核中正常运行了，其会通过通过一个JIT编译步骤将程序的通用字节码转换为机器特定指令集，以优化程序的执行速度。</p>
<p>下图是其架构图。</p>
<p><img decoding="async" class="aligncenter" src="https://imgopt.infoq.com/fit-in/1200x2400/filters:quality(80)/filters:no_upscale()/articles/gentle-linux-ebpf-introduction/en/resources/47image005-1619704397592.jpg" /></p>
<p style="text-align: center;">（图片来自：<a href="https://www.infoq.com/articles/gentle-linux-ebpf-introduction/" target="_blank" rel="noopener">https://www.infoq.com/articles/gentle-linux-ebpf-introduction/</a>）</p>
<p>在内核中运行时，eBPF 程序通常会挂载到一个内核钩子（hook）上，以便在特定的事件发生时被执行。例如，</p>
<ul>
<li><span>系统调用——当用户空间函数将执行转移到内核时插入</span></li>
<li><span>函数进入和退出——拦截对预先存在的函数的调用</span></li>
<li><span>网络事件 &#8211; 在收到数据包时执行</span></li>
<li><span>Kprobes 和 uprobes &#8211; 附加到内核或用户函数的探测器</span></li>
</ul>
<p>最后 eBPF Maps，允许eBPF程序在调用之间保持状态，以便进行相关的数据统计，并与用户空间的应用程序共享数据。一个eBPF映射基本上是一个键值存储，其中的值通常被视为任意数据的二进制块。它们是通过带有BPF_MAP_CREATE参数的<code>bpf_cmd</code>系统调用来创建的，和Linux世界中的其他东西一样，它们是通过文件描述符来寻址。与地图的交互是通过查找/更新/删除系统调用进行的</p>
<p>总之，eBPF 的工作原理是通过动态加载、执行和检查<strong>无损编译</strong>过的代码来实现的。<code>[1]</code></p>
<h4>示例</h4>
<p>eBPF 可以用于对内核的性能进行分析。下面是一个基于 eBPF 的性能分析的 step-by-step 示例：</p>
<p>第一步：准备工作：首先，需要确保内核已经支持 eBPF 功能。这通常需要在内核配置文件中启用 eBPF 相关的选项，并重新编译内核。检查是否支持 eBPF，你可以用这两个命令查看 <code>ls /sys/fs/bpf</code> 和 <code>lsmod | grep bpf</code></p>
<p>第二步：写 eBPF 程序：接下来，需要编写 eBPF 程序，用于收集内核的性能指标。eBPF 程序的语言可以选择 C 或者 Python，它需要通过特定的接口访问内核的数据结构，并将收集到的数据保存到指定的位置。</p>
<p>下面是一个Python 示例（其实还是C语言，用python来加载一段C程序到Linux内核）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#!/usr/bin/python3

from bcc import BPF
from time import sleep

# 定义 eBPF 程序
bpf_text = """
#include &lt;uapi/linux/ptrace.h&gt;

BPF_HASH(stats, u32);

int count(struct pt_regs *ctx) {
    u32 key = 0;
    u64 *val, zero=0;
    val = stats.lookup_or_init(&amp;key, &amp;zero);
    (*val)++;
    return 0;
}
"""

# 编译 eBPF 程序
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"])

# 加载 eBPF 程序
b.attach_kprobe(event="tcp_sendmsg", fn_name="count")

name = {
  0: "tcp_sendmsg"
}
# 输出统计结果
while True:
    try:
        #print("Total packets: %d" % b["stats"][0].value)
        for k, v in b["stats"].items():
           print("{}: {}".format(name[k.value], v.value))
        sleep(1)
    except KeyboardInterrupt:
        exit()</pre>
<p>这个 eBPF 程序的功能是统计网络中传输的数据包数量。它通过定义一个 <code>BPF_HASH</code> 数据结构来保存统计结果（eBPF Maps），并通过捕获 <code>tcp_sendmsg</code> 事件来实现实时统计。最后，它通过每秒输出一次统计结果来展示数据。这个 eBPF 程序只是一个简单的示例，实际应用中可能需要进行更复杂的统计和分析。</p>
<p>第三步：运行 eBPF 程序：接下来，需要使用 eBPF 编译器将 eBPF 程序编译成内核可执行的格式（这个在上面的Python程序里你可以看到——Python引入了一个bcc的包，然后用这个包，把那段 C语言的程序编译成字节码加载在内核中并把某个函数 attach 到某个事件上）。这个过程可以使用 BPF Compiler Collection（BCC）工具来完成。BCC 工具可以通过命令行的方式将 eBPF 程序编译成内核可执行的格式，并将其加载到内核中。</p>
<p>下面是运行上面的 Python3 程序的步骤：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">sudo apt install python3-bpfcc</pre>
<p>注：在Python3下请不要使用 <code>pip3 install bcc</code> （参看：<a href="https://github.com/iovisor/bcc/issues/2278#issuecomment-825356087" target="_blank" rel="noopener">这里</a>）</p>
<p>如果你是 Ubuntu 20.10 以上的版本，最好通过源码安装（否则程序会有编译问题），参看：<a href="https://github.com/iovisor/bcc/issues/3993#issuecomment-1228217609" target="_blank" rel="noopener">这里</a>：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="bash">apt purge bpfcc-tools libbpfcc python3-bpfcc
wget https://github.com/iovisor/bcc/releases/download/v0.25.0/bcc-src-with-submodule.tar.gz
tar xf bcc-src-with-submodule.tar.gz
cd bcc/
apt install -y python-is-python3
apt install -y bison build-essential cmake flex git libedit-dev   libllvm11 llvm-11-dev libclang-11-dev zlib1g-dev libelf-dev libfl-dev python3-distutils
apt install -y checkinstall
mkdir build
cd build/
cmake -DCMAKE_INSTALL_PREFIX=/usr -DPYTHON_CMD=python3 ..
make
checkinstall</pre>
<p>接下来，需要将上面的 Python 程序保存到本地，例如保存到文件 netstat.py。运行程序：最后，可以通过执行以下命令来运行 Python 程序：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ chmod +x ./netstat.py
$ sudo ./netstat.py
tcp_sendmsg: 29
tcp_sendmsg: 216
tcp_sendmsg: 277
tcp_sendmsg: 379
tcp_sendmsg: 419
tcp_sendmsg: 468
tcp_sendmsg: 574
tcp_sendmsg: 645
tcp_sendmsg: 29
</pre>
<p>程序开始运行后，会在控制台输出网络数据包的统计信息。可以通过按 Ctrl+C 组合键来结束程序的运行。</p>
<p>下面我们再看一个比较复杂的示例，这个示例会计算TCP的发包时间（示例参考于Github上 <a href="https://github.com/iovisor/bcc/issues/2972" target="_blank" rel="noopener">这个issue</a>里的程序）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#!/usr/bin/python3

from bcc import BPF
import time

# 定义 eBPF 程序
bpf_text = """
#include &lt;uapi/linux/ptrace.h&gt;
#include &lt;net/sock.h&gt;
#include &lt;net/inet_sock.h&gt;
#include &lt;bcc/proto.h&gt;

struct packet_t {
    u64 ts, size;
    u32 pid;
    u32 saddr, daddr;
    u16 sport, dport;
};

BPF_HASH(packets, u64, struct packet_t);

int on_send(struct pt_regs *ctx, struct sock *sk, struct msghdr *msg, size_t size)
{
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id;

    // 记录数据包的时间戳和信息
    struct packet_t pkt = {}; // 结构体一定要初始化，可以使用下面的方法
                              //__builtin_memset(&amp;pkt, 0, sizeof(pkt)); 
    pkt.ts = bpf_ktime_get_ns();
    pkt.size = size;
    pkt.pid = pid;
    pkt.saddr = sk-&gt;__sk_common.skc_rcv_saddr;
    pkt.daddr = sk-&gt;__sk_common.skc_daddr;
    struct inet_sock *sockp = (struct inet_sock *)sk;
    pkt.sport = sockp-&gt;inet_sport;
    pkt.dport = sk-&gt;__sk_common.skc_dport;

    packets.update(&amp;id, &amp;pkt);
    return 0;
}

int on_recv(struct pt_regs *ctx, struct sock *sk)
{
    u64 id = bpf_get_current_pid_tgid();
    u32 pid = id;

    // 获取数据包的时间戳和编号
    struct packet_t *pkt = packets.lookup(&amp;id);
    if (!pkt) {
        return 0;
    }

    // 计算传输时间
    u64 delta = bpf_ktime_get_ns() - pkt-&gt;ts;

    // 统计结果
    bpf_trace_printk("tcp_time: %llu.%llums, size: %llu\\n", 
       delta/1000, delta%1000%100, pkt-&gt;size);

    // 删除统计结果
    packets.delete(&amp;id);

    return 0;
}
"""

# 编译 eBPF 程序
b = BPF(text=bpf_text, cflags=["-Wno-macro-redefined"])

# 注册 eBPF 程序
b.attach_kprobe(event="tcp_sendmsg", fn_name="on_send")
b.attach_kprobe(event="tcp_v4_do_rcv", fn_name="on_recv")

# 输出统计信息
print("Tracing TCP latency... Hit Ctrl-C to end.")
while True:
    try:
        (task, pid, cpu, flags, ts, msg) = b.trace_fields()
        print("%-18.9f %-16s %-6d %s" % (ts, task, pid, msg))
    except KeyboardInterrupt:
        exit()</pre>
<p>上面这个程序通过捕获每个数据包的时间戳来统计传输时间。在捕获 <code>tcp_sendmsg</code> 事件时，记录数据包的发送时间；在捕获 <code>tcp_v4_do_rcv</code> 事件时，记录数据包的接收时间；最后，通过比较两个时间戳来计算传输时间。</p>
<p>从上面的两个程序我们可以看到，eBPF 的一个编程的基本方法，这样的在Python里向内核的某些事件挂载一段 &#8220;C语言” 的方式就是 eBPF 的编程方式。实话实说，这样的代码很不好写，而且有很多非常诡异的东西，一般人是很难驾驭的（上面的代码我也不是很容易都能写通的，把 Google 都用了个底儿掉，读了很多晦涩的文档……）好在这样的代码已经有人写了，我们不必再写了，在 <a href="https://github.com/iovisor/bcc/tree/master/tools" target="_blank" rel="noopener">Github 上的 bcc 库下的 tools 目录</a>有很多……</p>
<p>BCC（<a href="https://github.com/iovisor/bcc" target="_blank" rel="noopener">BPF Compiler Collection</a>）是一套开源的工具集，可以在 Linux 系统中使用 BPF（Berkeley Packet Filter）程序进行系统级性能分析和监测。BCC 包含了许多实用工具，如：</p>
<ol>
<li>bcc-tools：一个包含许多常用的 BCC 工具的软件包。</li>
<li>bpftrace：一个高级语言，用于编写和执行 BPF 程序。</li>
<li>tcptop：一个实时监控和分析 TCP 流量的工具。</li>
<li>execsnoop：一个用于监控进程执行情况的工具。</li>
<li>filetop：一个实时监控和分析文件系统流量的工具。</li>
<li>trace：一个用于跟踪和分析函数调用的工具。</li>
<li>funccount：一个用于统计函数调用次数的工具。</li>
<li>opensnoop：一个用于监控文件打开操作的工具。</li>
<li>pidstat：一个用于监控进程性能的工具。</li>
<li>profile：一个用于分析系统 CPU 使用情况的工具。</li>
</ol>
<p>下面这张图你可能见过多次了，你可以看看他可以干多少事，内核里发生什么事一览无余。</p>
<p><img decoding="async" src="https://github.com/iovisor/bcc/raw/master/images/bcc_tracing_tools_2019.png" /></p>
<h4>延伸阅读</h4>
<p>一些经典的文章和书籍关于 eBPF 包括：</p>
<ul>
<li>Brendan Gregg 的《<a href="https://book.douban.com/subject/34467459/" target="_blank" rel="noopener">BPF Performance Tools: Linux System and Application Observability</a>》一书是一个全面的指南，涵盖了 eBPF 的基础知识和实践应用。</li>
<li>eBPF 的官网：<a href="https://ebpf.io/" target="_blank" rel="noopener">https://ebpf.io/</a> 由 <a href="https://cilium.io/" rel="nofollow">Cilium</a> 建立</li>
<li><a href="http://docs.cilium.io/en/latest/bpf/" rel="nofollow">Cilium&#8217;s BPF and XDP Reference Guide</a></li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/index.html" rel="nofollow">BPF Documentation</a></li>
<li><a href="https://www.kernel.org/doc/html/latest/bpf/bpf_design_QA.html" rel="nofollow">BPF Design Q&amp;A</a></li>
<li>还有 Github 上的 <a href="https://github.com/zoidbergwill/awesome-ebpf" target="_blank" rel="noopener">Awesome eBPF</a></li>
</ul>
<h4>彩蛋</h4>
<p>最后来到彩蛋环节。因为最近 ChatGPT 很火，于是，我想通过 ChatGPT 来帮助我书写这篇文章，一开始我让ChatGPT 帮我列提纲，并根据提纲生成文章内容，并查找相关的资料，非常之顺利，包括生成的代码，我以为我们以很快地完成这篇文章。</p>
<p>但是，到了代码生成的时候，我发现，ChatGPT 生成的代码的思路和方法都是对的，但是是比较老的，而且是跑不起来的，<strong>出现了好些低级错误，如：使用了未声明的变量，没有引用完整的C语言的头文件，没有正确地初始化变量，错误地获取数据，类型没有匹配……等等</strong>，在程序调试上，挖了很多的坑，C语言本来就不好搞，挖的很多运行时的坑很难察觉，所以，耗费了我大量的时间来排除各种各样的问题，其中有环境上的问题，还有代码上的问题，这些问题即便是通过 Google 也不容易找到解决方案，我找到的解决方案都放在文章中了，尤其是第二个示例，让我调试了3个多小时，读了很多 bcc 上的issue和相关的晦涩的手册和文档，才让程序跑通。</p>
<p>到了文章收关的阶段，我让ChatGPT 给我几个延伸阅读，也是很好的，但是没有给出链接，于是我只得人肉 Google 了一下，然后让我吃惊的是，<strong>好多ChatGPT给出来的文章是根本不存在的，完全是它伪造的</strong>。我连让它干了两次都是这样，这个让我惊掉大牙。这让我开始怀疑它之前生成的内容，于是，我不得我返回仔细Review我的文章，尤其是“介绍”、“用途”和“工作原理”这三个章节，基本都是ChatGPT生成的，在Review完后，我发现了ChatGPT 给我生造了一个叫 “无损编译器”的术语，这个术语简直了，于是我开始重写我的文章。我把一些段落重写了，有一些没有，保留下来的我都标记上了 <code>[1]</code>，大家读的时候要小心阅读。</p>
<p>最后，<strong>我的结论是，ChatGPT只是一个不成熟的玩具，只能回答一些没有价值的日常聊天的问题，要说能取代Google，我觉得不可能，因为Google会基于基本的事实，而ChatGPT会基于内容生成的算法，在造假方面称得上是高手，可以列为电信诈骗的范畴了，我以后不会再使用ChatGPT生成文章内容或是作我的帮手了。StackOverflow把其ban了真是不能太赞了！</strong></p>
<p><strong>附件一：ChatGPT的造假载图和样本</strong></p>
<table width="100%">
<tbody>
<tr>
<td>
<p><figure id="attachment_22325" aria-describedby="caption-attachment-22325" style="width: 195px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1.png"><img decoding="async" loading="lazy" class="wp-image-22325 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-195x300.png" alt="" width="195" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-195x300.png 195w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-665x1024.png 665w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-768x1183.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-997x1536.png 997w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1-175x270.png 175w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.1.png 1096w" sizes="(max-width: 195px) 100vw, 195px" /></a><figcaption id="caption-attachment-22325" class="wp-caption-text">点击看大图</figcaption></figure></td>
<td>
<p><figure id="attachment_22326" aria-describedby="caption-attachment-22326" style="width: 286px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2.png"><img decoding="async" loading="lazy" class="wp-image-22326 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-286x300.png" alt="" width="286" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-286x300.png 286w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-978x1024.png 978w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-768x804.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2-258x270.png 258w, https://coolshell.cn/wp-content/uploads/2022/12/eBPF.ref_.2.png 1096w" sizes="(max-width: 286px) 100vw, 286px" /></a><figcaption id="caption-attachment-22326" class="wp-caption-text">点击看大图</figcaption></figure></td>
</tr>
</tbody>
</table>
<p><strong>ChatGPT 生成的样本一</strong></p>
<ul>
<li>Brendan Gregg 的《BPF Performance Tools: Linux System and Application Observability》一书是一个全面的指南，涵盖了 eBPF 的基础知识和实践应用。</li>
<li>Alexei Starovoitov 在 2015 年的 LPC 大会上发表的《eBPF and XDP: fast programmable datapath in the Linux kernel》一文，是一个介绍 eBPF 和 XDP（eXpress Data Path）的综述。</li>
<li>谷歌的《BPF: The Future of Linux Monitoring and Observability》一文介绍了 eBPF 的基本概念，并探讨了它对于 Linux 监控和可观察性的影响。</li>
<li>微软的《A Survey of Techniques for Hardware Acceleration of BPF》一文，介绍了 eBPF 在硬件加速方面的研究现状。</li>
<li>Daniel Borkmann 的《Linux kernel networking: implementation and theory》一书涵盖了 eBPF 的实现和理论。</li>
<li>Martin KaFai Lau 在 2015 年的 SOSP 大会上发表的《eBPF: a new execution environment for the Linux kernel》一文，介绍了 eBPF 在 Linux 内核中的新执行环境。</li>
<li>来自两名谷歌工程师的《eBPF: A New Frontier in Performance Analysis》一文，探讨了 eBPF 对性能分析的影响。</li>
<li>《BPF Performance Tools: Linux System and Application Observability》一书中的第三章，讨论了 eBPF 的编译器和运行时系统。</li>
<li>来自三名谷歌工程师的《eBPF: The New Solaris DTrace》一文，比较了 eBPF 和 Solaris DTrace 系统的区别。</li>
</ul>
<p><strong>ChatGPT 生成的样本二</strong></p>
<ol>
<li>eBPF 程序开发指南（<a href="https://cilium.io/doc/ebpf/ebpf-guide/" target="_new" rel="noopener">https://cilium.io/doc/ebpf/ebpf-guide/</a>）</li>
<li>eBPF 官方文档（<a href="https://www.kernel.org/doc/Documentation/networking/filter.txt" target="_new" rel="noopener">https://www.kernel.org/doc/Documentation/networking/filter.txt</a>）</li>
<li>Linux 内核 eBPF 介绍（<a href="https://www.ibm.com/developerworks/cn/linux/kernel/ebpf/index.html" target="_new" rel="noopener">https://www.ibm.com/developerworks/cn/linux/kernel/ebpf/index.html</a>）</li>
<li>eBPF 性能优化技巧（<a href="https://www.infoq.com/cn/news/2018/08/ebpf-performance-tips/" target="_new" rel="noopener">https://www.infoq.com/cn/news/2018/08/ebpf-performance-tips/</a>）</li>
<li>eBPF 实战经验分享（<a href="https://blog.cloudflare.com/ebpf-in-practice/" target="_new" rel="noopener">https://blog.cloudflare.com/ebpf-in-practice/</a>）</li>
<li>eBPF 在网络安全中的应用（<a href="https://www.zdnet.com/article/ebpf-how-to-use-it-for-network-security/" target="_new" rel="noopener">https://www.zdnet.com/article/ebpf-how-to-use-it-for-network-security/</a>）</li>
<li>eBPF 实现 Linux 系统性能监控（<a href="https://www.percona.com/blog/2017/08/15/how-to-use-ebpf-to-monitor-linux-system-performance/" target="_new" rel="noopener">https://www.percona.com/blog/2017/08/15/how-to-use-ebpf-to-monitor-linux-system-performance/</a>）</li>
<li>eBPF 入门教程（<a href="https://sysdig.com/blog/ebpf-getting-started/" target="_new" rel="noopener">https://sysdig.com/blog/ebpf-getting-started/</a>）</li>
<li>eBPF 与 BPF 比较（<a href="https://lwn.net/Articles/724647/" target="_new" rel="noopener">https://lwn.net/Articles/724647/</a>）</li>
<li>eBPF 提高课程（<a href="https://www.pluralsight.com/courses/ebpf-advanced" target="_new" rel="noopener">https://www.pluralsight.com/courses/ebpf-advanced</a>）</li>
</ol>
<p><strong>附件二：发明的术语：无损编译器</strong></p>
<table width="100%">
<tbody>
<tr>
<td>
<p><figure id="attachment_22328" aria-describedby="caption-attachment-22328" style="width: 180px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/fake.png"><img decoding="async" loading="lazy" class="wp-image-22328 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/fake-180x300.png" alt="" width="180" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/fake-180x300.png 180w, https://coolshell.cn/wp-content/uploads/2022/12/fake-616x1024.png 616w, https://coolshell.cn/wp-content/uploads/2022/12/fake-768x1277.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/fake-924x1536.png 924w, https://coolshell.cn/wp-content/uploads/2022/12/fake-162x270.png 162w, https://coolshell.cn/wp-content/uploads/2022/12/fake.png 1080w" sizes="(max-width: 180px) 100vw, 180px" /></a><figcaption id="caption-attachment-22328" class="wp-caption-text">点击看大图</figcaption></figure></td>
<td>
<p><figure id="attachment_22335" aria-describedby="caption-attachment-22335" style="width: 223px" class="wp-caption aligncenter"><a href="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_.png"><img decoding="async" loading="lazy" class="wp-image-22335 size-medium" src="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-223x300.png" alt="" width="223" height="300" srcset="https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-223x300.png 223w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-761x1024.png 761w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-768x1033.png 768w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-1142x1536.png 1142w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_-201x270.png 201w, https://coolshell.cn/wp-content/uploads/2022/12/fake.term_.png 1176w" sizes="(max-width: 223px) 100vw, 223px" /></a><figcaption id="caption-attachment-22335" class="wp-caption-text">点击看大图</figcaption></figure></td>
</tr>
</tbody>
</table>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" id="wp_rp_first"><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/1379.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/13.jpg" alt="如何调试bash脚本" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1379.html" class="wp_rp_title">如何调试bash脚本</a></li><li ><a href="https://coolshell.cn/articles/19219.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/03/linux.ninja_-150x150.png" alt="打造高效的工作环境 &#8211; Shell 篇" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19219.html" class="wp_rp_title">打造高效的工作环境 &#8211; Shell 篇</a></li><li ><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1-150x150.png" alt="记一次Kubernetes/Docker网络排障" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_title">记一次Kubernetes/Docker网络排障</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/systemd-1-150x150.jpeg" alt="Linux PID 1 和 Systemd" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_title">Linux PID 1 和 Systemd</a></li><li ><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2016/07/cache-150x150.png" alt="缓存更新的套路" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17416.html" class="wp_rp_title">缓存更新的套路</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/22320.html">eBPF 介绍</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/22320.html/feed</wfw:commentRss>
			<slash:comments>18</slash:comments>
		
		
			</item>
		<item>
		<title>从一次经历谈 TIME_WAIT 的那些事</title>
		<link>https://coolshell.cn/articles/22263.html</link>
					<comments>https://coolshell.cn/articles/22263.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Tue, 19 Jul 2022 06:43:39 +0000</pubDate>
				<category><![CDATA[Go 语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[编程语言]]></category>
		<category><![CDATA[SO_LINGER]]></category>
		<category><![CDATA[TCP]]></category>
		<category><![CDATA[TIME_WAIT]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=22263</guid>

					<description><![CDATA[<p>今天来讲一讲TCP 的 TIME_WAIT 的问题。这个问题尽人皆知，不过，这次遇到的是不太一样的场景，前两天也解决了，正好写篇文章，顺便把 TIME_WAIT...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/22263.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/22263.html">从一次经历谈 TIME_WAIT 的那些事</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright" src="https://coolshell.cn/wp-content/uploads/2022/07/wall_clock-300x167.jpeg" alt="" width="400" height="222" />今天来讲一讲TCP 的 <code>TIME_WAIT</code> 的问题。这个问题尽人皆知，不过，这次遇到的是不太一样的场景，前两天也解决了，正好写篇文章，顺便把 <code>TIME_WAIT</code> 的那些事都说一说。对了，这个场景，跟我开源的探活小工具 <a href="https://github.com/megaease/easeprobe">EaseProbe</a> 有关，我先说说这个场景里的问题，然后，顺着这个场景跟大家好好说一下这个事。</p>
<h4>问题背景</h4>
<p>先说一下背景，<a href="https://github.com/megaease/easeprobe">EaseProbe</a> 是一个轻量独立的用来探活服务健康状况的小工具，支持http/tcp/shell/ssh/tls/host以及各种中间件的探活，然后，直接发送通知到主流的IM上，如：Slack/Telegram/Discrod/Email/Team，包括国内的企业微信/钉钉/飞书， 非常好用，用过的人都说好 <img src="https://s.w.org/images/core/emoji/14.0.0/72x72/1f60f.png" alt="😏" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>
<p>这个探活工具在每次探活的时候，必须要从头开始建立整个网络链接，也就是说，需要从头开始进行DNS查询，建立TCP链接，然后进行通信，再关闭链接。这里，我们不会设置 TCP 的 KeepAlive 重用链接，因为探活工具除了要探活所远端的服务，还要探活整个网络的情况，所以，每次探活都需要从新来过，这样才能捕捉得到整个链路的情况。</p>
<p><span id="more-22263"></span></p>
<p>但是，这样不断的新建链接和关闭链接，根据TCP的状态机，我们知道这会导致在探测端这边出现的 <code>TIME_WAIT</code> 的 TCP 链接，根据 TCP 协议的定义，这个 TIME_WAIT 需要等待 2倍的MSL 时间，TCP 链接都会被系统回收，在回收之前，这个链接会占用系统的资源，主要是两个资源，一个是文件描述符，这个还好，可以调整，另一个则是端口号，这个是没法调整的，因为作为发起请求的client来说，在对同一个IP上理论上你只有64K的端口号号可用（实际上系统默认只有近30K，从32,768 到 60,999 一共 60999+1-32768=28,232，你可以通过 <code>sysctl net.ipv4.ip_local_port_range</code> 查看  ），如果 <code>TIME_WAIT</code> 过多，会导致TCP无法建立链接，还会因为资源消耗太多导致整个程序甚至整个系统异常。</p>
<p>试想，如果我们以 10秒为周期探测10K的结点，如果TIME_WAIT的超时时间是120秒，那么在第60秒后，等着超时的 <code>TIME_WAIT</code> 我们就有可能把某个IP的端口基本用完了，就算还行，系统也有些问题。（注意：我们不仅仅只是TCP，还有HTTP协议，所以，大家不要觉得TCP的四元组只要目标地址不一样就好了，一方面，我们探的是域名，需要访问DNS服务，所以，DNS服务一般是一台服务器，还有，因为HTTPS一般是探API，而且会有网关代理API，所以链接会到同一个网关上。另外就算还可以建出站连接，但是本地程序会因为端口耗尽无法bind了。所以，现实情况并不会像理论情况那样只要四元组不冲突，端口就不会耗尽）</p>
<h4>为什么要 TIME_WAIT</h4>
<p>那么，为什么TCP在 <code>TIME_WAIT</code> 上要等待一个2MSL的时间？<code></code></p>
<p>以前写过篇比较宏观的《TCP的那些事》（<a title="TCP 的那些事儿（上）" href="https://coolshell.cn/articles/11564.html" target="_blank" rel="noopener">上篇</a>，<a title="TCP 的那些事儿（下）" href="https://coolshell.cn/articles/11609.html" target="_blank" rel="noopener">下篇</a>），这个访问在“上篇”里讲过，这里再说一次，TCP 断链接的时候，会有下面这个来来回回的过程。</p>
<p>我们来看主动断链接的最后一个状态 <code>TIME_WAIT</code> 后就不需要等待对端回 ack了，而是进入了超时状态。这主要是因为，在网络上，如果要知道我们发出的数据被对方收到了，那我们就需要对方发来一个确认的Ack信息，那问题来了，对方怎么知道自己发出去的ack，被收到了？难道还要再ack一下，这样ack来ack回的，那什么谁也不要玩了……是的，这就是比较著名的【两将军问题】——两个将军需要在一个不稳定的信道上达成对敌攻击时间的协商，A向B派出信鸽，我们明早8点进攻，A怎么知道B收到了信？那需要B向A派出信鸽，ack说我收到了，明早8点开干。但是，B怎么知道A会收到自己的确认信？是不是还要A再确认一下？这样无穷无尽的确认导致这个问题是没有完美解的（我们在《<a href="https://coolshell.cn/articles/10910.html#Two_Generals_Problem%EF%BC%88%E4%B8%A4%E5%B0%86%E5%86%9B%E9%97%AE%E9%A2%98%EF%BC%89" target="_blank" rel="noopener">分布式事务</a>》一文中说过这个问题，这里不再重述）</p>
<p>所以，我们只能等一个我们认为最大小时来解决两件个问题：</p>
<p>1） 为了 <strong>防止来自一个连接的延迟段</strong>被依赖于相同四元组（源地址、源端口、目标地址、目标端口）的稍后连接接受（被接受后，就会被马上断掉，TCP状态机紊乱）。虽然，可以通过指定 TCP 的 sequence number 一定范围内才能被接受。但这也只是让问题发生的概率低了一些，对于一个吞吐量大的的应用来说，依然能够出现问题，尤其是在具有大接收窗口的快速连接上。<a title="RFC 1337：TCP 中的 TIME-WAIT 暗杀危险" href="https://tools.ietf.org/html/rfc1337">RFC 1337</a>详细解释了当 <code>TIME-WAIT</code>状态不足时会发生什么。<sup id="fnref-rfc1337"></sup><code>TIME-WAIT</code>以下是如果不缩短状态可以避免的示例：</p>
<figure id="attachment_22267" aria-describedby="caption-attachment-22267" style="width: 456px" class="wp-caption aligncenter"><img decoding="async" loading="lazy" class="wp-image-22267" src="https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment.png" alt="" width="456" height="467" srcset="https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment.png 800w, https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment-293x300.png 293w, https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment-768x787.png 768w, https://coolshell.cn/wp-content/uploads/2022/07/duplicate-segment-263x270.png 263w" sizes="(max-width: 456px) 100vw, 456px" /><figcaption id="caption-attachment-22267" class="wp-caption-text">由于缩短的 TIME-WAIT 状态，后续的 TCP 段已在不相关的连接中被接受（<a href="https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux" target="_blank" rel="noopener">来源</a>）</figcaption></figure>
<p>&nbsp;</p>
<p>2）另一个目的是确保<strong>远端已经关闭了连接</strong>。当最后一个<em>ACK</em>​​ 丢失时，对端保持该<code>LAST-ACK</code>状态。<sup id="fnref-lastack"></sup>在没有<code>TIME-WAIT</code>状态的情况下，可以重新打开连接，而远程端仍然认为先前的连接有效。当它收到一个<em>SYN</em>段（并且序列号匹配）时，它将以<em>RST</em>应答，因为它不期望这样的段。新连接将因错误而中止：</p>
<p>&nbsp;</p>
<figure id="attachment_22268" aria-describedby="caption-attachment-22268" style="width: 559px" class="wp-caption aligncenter"><img decoding="async" loading="lazy" class="wp-image-22268" src="https://coolshell.cn/wp-content/uploads/2022/07/last-ack.png" alt="" width="559" height="375" srcset="https://coolshell.cn/wp-content/uploads/2022/07/last-ack.png 783w, https://coolshell.cn/wp-content/uploads/2022/07/last-ack-300x201.png 300w, https://coolshell.cn/wp-content/uploads/2022/07/last-ack-768x515.png 768w, https://coolshell.cn/wp-content/uploads/2022/07/last-ack-403x270.png 403w" sizes="(max-width: 559px) 100vw, 559px" /><figcaption id="caption-attachment-22268" class="wp-caption-text">如果远端因为最后一个 ACK​​ 丢失而停留在 LAST-ACK 状态，则打开具有相同四元组的新连接将不起作用 （<a href="https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux" target="_blank" rel="noopener">来源</a>）</figcaption></figure>
<p><code>TIME_WAIT</code> 的这个超时时间的值如下所示：</p>
<ul>
<li>在 macOS 上是15秒， <code>sysctl net.inet.tcp | grep net.inet.tcp.msl</code></li>
<li>在 Linux 上是 60秒 <code>cat /proc/sys/net/ipv4/tcp_fin_timeout</code></li>
</ul>
<h4>解决方案</h4>
<p>要解决这个问题，网上一般会有下面这些解法</p>
<ul>
<li>把这个超时间调小一些，这样就可以把TCP 的端口号回收的快一些。但是也不能太小，如果流量很大的话，TIME_WAIT一样会被耗尽。</li>
<li>设置上 <code>tcp_tw_reuse</code> 。<a title="RFC 1323：高性能 TCP 扩展" href="https://tools.ietf.org/html/rfc1323">RFC 1323</a>提出了一组 TCP 扩展来提高高带宽路径的性能。除其他外，它定义了一个新的 TCP 选项，带有两个四字节<strong>时间戳字段</strong>。第一个是发送选项的 TCP 时间戳的当前值，而第二个是从远程主机接收到的最新时间戳。如果新时间戳严格大于为前一个连接记录的最新时间戳。Linux 将重用该状态下的现有 <code>TIME_WAIT</code> 连接用于<strong>出站的链接</strong>。也就是说，<strong>这个参数对于入站连接是没有任何用图的。</strong></li>
<li>设置上 <code>tcp_tw_recycle</code> 。 这个参数同样依赖于时间戳选项，但会影响进站和出站链接。这个参数会影响NAT环境，也就是一个公司里的所有员工用一个IP地址访问外网的情况。在这种情况下，时间戳条件将禁止在这个公网IP后面的所有设备在一分钟内连接，因为它们不共享相同的时间戳时钟。毫无疑问，禁用此选项要好得多，因为它会导致 <strong>难以检测</strong>和<strong>诊断</strong>问题。（注：从 Linux 4.10 (commit <a title="tcp：为每个连接随机化 tcp 时间戳偏移" href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=95a22caee396cef0bb2ca8fafdd82966a49367bb">95a22caee396</a> ) 开始，Linux 将为每个连接随机化时间戳偏移量，从而使该选项完全失效，无论有无<abbr title="网络地址解读">NAT</abbr>。它已从 Linux 4.12中完全删除）</li>
</ul>
<p>对于服务器来说，上述的三个访问都不能解决服务器的 <code>TIME_WAIT</code> 过多的问题，真正解决问题的就是——<strong>不作死就不会死，也就是说，服务器不要主动断链接，而设置上KeepAlive后，让客户端主动断链接，这样服务端只会有<code>CLOSE_WAIT</code></strong>。</p>
<p>但是对于用于建立出站连接的探活的 EaseProbe来说，设置上 <code>tcp_tw_reuse</code> 就可以重用 <code>TIME_WAIT</code> 了，但是这依然无法解决 <code>TIME_WAIT</code> 过多的问题。</p>
<p>然后，过了几天后，我忽然想起来以前在《UNIX 网络编程》上有看到过一个Socket的参数，叫 <code data-enlighter-language="raw" class="EnlighterJSRAW">&lt;code&gt;SO_LINGER</code></code>，我的编程生涯中从来没有使用过这个设置，这个参数主要是为了延尽关闭来用的，也就是说你应用调用 <code>close()</code>函数时，如果还有数据没有发送完成，则需要等一个延时时间来让数据发完，但是，如果你把延时设置为 0  时，Socket就丢弃数据，并向对方发送一个 <code>RST</code> 来终止连接，因为走的是 RST 包，所以就不会有 <code>TIME_WAIT</code> 了。</p>
<p>这个东西在服务器端永远不要设置，不然，你的客户端就总是看到 TCP 链接错误 “connnection reset by peer”，但是这个参数对于 EaseProbe 的客户来说，简直是太完美了，当EaseProbe 探测完后，直接 reset connection， 即不会有功能上的问题，也不会影响服务器，更不会有烦人的 <code> TIME_WAIT</code> 问题。</p>
<h4>Go 实际操作</h4>
<p>在 Golang的标准库代码里，<code>net.TCPConn</code> 有个方法 <code>SetLinger()</code>可以完成这个事，使用起来也比较简单：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">conn, _ := net.DialTimeout("tcp", t.Host, t.Timeout())

if tcpCon, ok := conn.(*net.TCPConn); ok {
    tcpCon.SetLinger(0)
}</pre>
<p>你需要把一个 <code>net.Conn</code>  转型成 <code>net.TCPConn</code>，然后就可以调用方法了。</p>
<p>但是对于Golang 的标准库中的 HTTP 对象来说，就有点麻烦了，Golang的 http 库把底层的这边连接对象全都包装成私有变量了，你在外面根本获取不到。这篇《<a href="https://iximiuz.com/en/posts/go-net-http-setsockopt-example/" target="_blank" rel="noopener">How to Set Go net/http Socket Options &#8211; setsockopt() example</a> 》中给出了下面的方法：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">dialer := &amp;net.Dialer{
    Control: func(network, address string, conn syscall.RawConn) error {
        var operr error
        if err := conn.Control(func(fd uintptr) {
            operr = syscall.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.TCP_QUICKACK, 1)
        }); err != nil {
            return err
        }
        return operr
    },
}

client := &amp;http.Client{
    Transport: &amp;http.Transport{
        DialContext: dialer.DialContext,
    },
}</pre>
<p>上面这个方法非常的低层，需要直接使用setsocketopt这样的系统调用，我其实，还是想使用 <code>TCPConn.SetLinger(0)</code> 来完成这个事，即然都被封装好了，最好还是别破坏封闭性碰底层的东西。</p>
<p>经过Golang http包的源码阅读和摸索，我使用了下面的方法：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">client := &amp;http.Client{
    Timeout: h.Timeout(),
    Transport: &amp;http.Transport{
      TLSClientConfig:   tls,
      DisableKeepAlives: true,
      DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
        d := net.Dialer{Timeout: h.Timeout()}
        conn, err := d.DialContext(ctx, network, addr)
        if err != nil {
          return nil, err
        }
        tcpConn, ok := conn.(*net.TCPConn)
        if ok {
          tcpConn.SetLinger(0)
          return tcpConn, nil
        }
        return conn, nil
      },
    },
  }</pre>
<p>然后，我找来了全球 T0p 100W的域名，然后在AWS上开了一台服务器，用脚本生成了 TOP 10K 和 20K 的网站来以5s, 10s, 30s, 60s的间隔进行探活，搞到Cloudflare 的 1.1.1.1 DNS 时不时就把我拉黑，最后的测试结果也非常不错，根本 没有 TIME_WAIT 的链接，相关的测试方法、测试数据和测试报告可以参看：<a href="https://github.com/megaease/easeprobe/blob/main/docs/Benchmark.md" target="_blank" rel="noopener">Benchmark Report</a></p>
<h4>总结</h4>
<p>下面是几点总结</p>
<ul>
<li><code>TIME_WAIT</code> 是一个TCP 协议完整性的手段，虽然会有一定的副作用，但是这个设计是非常关键的，最好不要妥协掉。</li>
<li>永远不要使用  <code>tcp_tw_recycle</code> ，这个参数是个巨龙，破坏力极大。</li>
<li>服务器端永远不要使用  <code>SO_LINGER(0)</code>，而且使用 <code>tcp_tw_reuse</code> 对服务端意义不大，因为它只对出站流量有用。</li>
<li>在服务端上最好不要主动断链接，设置好KeepAlive，重用链接，让客户端主动断链接。</li>
<li>在客户端上可以使用 <code>tcp_tw_reuse</code>  和 <code>SO_LINGER(0)</code>。</li>
</ul>
<p>最后强烈推荐阅读这篇文章 &#8211; <a href="https://vincent.bernat.ch/en/blog/2014-tcp-time-wait-state-linux" target="_blank" rel="noopener">Coping with the TCP TIME-WAIT state on busy Linux servers</a></p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/11564.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/05/tin-can-phone-150x150.jpg" alt="TCP 的那些事儿（上）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11564.html" class="wp_rp_title">TCP 的那些事儿（上）</a></li><li ><a href="https://coolshell.cn/articles/19840.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/10/HTTP-770x513-300x200-1-150x150.jpg" alt="HTTP的前世今生" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19840.html" class="wp_rp_title">HTTP的前世今生</a></li><li ><a href="https://coolshell.cn/articles/11609.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/05/xin_2001040422167711230318-150x150.jpg" alt="TCP 的那些事儿（下）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11609.html" class="wp_rp_title">TCP 的那些事儿（下）</a></li><li ><a href="https://coolshell.cn/articles/9859.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/06/Alan-Cox-150x150.jpg" alt="Alan Cox：单向链表中prev指针的妙用" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9859.html" class="wp_rp_title">Alan Cox：单向链表中prev指针的妙用</a></li><li ><a href="https://coolshell.cn/articles/7490.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/06/f1-150x150.jpg" alt="性能调优攻略" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7490.html" class="wp_rp_title">性能调优攻略</a></li><li ><a href="https://coolshell.cn/articles/1484.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/09/tcp1-150x150.jpg" alt="TCP网络关闭的状态变换时序图" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1484.html" class="wp_rp_title">TCP网络关闭的状态变换时序图</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/22263.html">从一次经历谈 TIME_WAIT 的那些事</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/22263.html/feed</wfw:commentRss>
			<slash:comments>31</slash:comments>
		
		
			</item>
		<item>
		<title>Unix 50 年：Ken Thompson 的密码</title>
		<link>https://coolshell.cn/articles/19996.html</link>
					<comments>https://coolshell.cn/articles/19996.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sun, 03 Nov 2019 06:12:54 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[技术读物]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[BSD]]></category>
		<category><![CDATA[Dennis Ritchie]]></category>
		<category><![CDATA[Ken Thompson]]></category>
		<category><![CDATA[Unix]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=19996</guid>

					<description><![CDATA[<p>50年前，除了Apollo上天之外，还有一个大事的发生，就是Unix操作系统的诞生，若干年前我写过《Unix的传奇，上篇，下篇》，Unix是我入行前十年伴我成长...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/19996.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/19996.html">Unix 50 年：Ken Thompson 的密码</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium" src="https://coolshell.cn/wp-content/uploads/2019/11/ken.dennis-300x186.jpeg" alt="" width="300" height="186" />50年前，除了Apollo上天之外，还有一个大事的发生，就是Unix操作系统的诞生，若干年前我写过《Unix的传奇，<a href="https://coolshell.cn/articles/2322.html" target="_blank" rel="noopener noreferrer">上篇</a>，<a href="https://coolshell.cn/articles/2324.html" target="_blank" rel="noopener noreferrer">下篇</a>》，Unix是我入行前十年伴我成长的操作系统，虽然现在Linux早已接过了Unix的时代交接棒，但是，Unix文化对我个人的技术观影响是非常大的（注：《<a href="https://book.douban.com/subject/1467587/" target="_blank" rel="noopener noreferrer">Unix编程艺术</a>》是一本对影响我很深的书），而对于 <a href="https://en.wikipedia.org/wiki/Ken_Thompson">Ken Thompson</a> 和 <a href="https://en.wikipedia.org/wiki/Dennis_Ritchie" target="_blank" rel="noopener noreferrer">Dennis Ritchie</a> 这两位 Unix 的缔造者，也是计算机圈中的神一般的人物。今天，Dennis已经去逝，Ken在Google里跟 Rob Pike和 Robert Griesemer 这两位大神在开发Go语言。</p>
<p>P.S. 今年，我一直想写篇Unix 50周年纪念的文章，但一直无从下手，因为不想写过大的命题，如果能写个轶事最好不过。正好过完国庆节，技术圈里有个“热搜”——Ken Thompson的密码。但一直没有时间，所以拖到今天才写下来。</p>
<p>正文开始，2014年，有个叫Leah Neukirchen的程序员（<a href="https://leahneukirchen.org/blog/" target="_blank" rel="noopener noreferrer">blog</a>）在 BSD 3 的源代码中的 <code><a href="https://leahneukirchen.org/blog/archive/2019/10/ken-thompson-s-unix-password.html" target="_blank" rel="noopener noreferrer">/etc/passwd</a></code> 看到了早年Unix黑客们的被 hash了的密码，该文件如下所示：</p>
<p><span id="more-19996"></span></p>
<pre>root:OVCPatZ8RFmFY:0:10:Ernie Co-vax,4156427925:/:
daemon:*:1:1:The devil himself:/:
bill:.2xvLVqGHJm8M:8:10:&amp; Joy,4156424948:/usr/bill:/bin/csh
ozalp:m5syt3.lB5LAE:40:10:&amp; Babaoglu,4156423806:/usr/ozalp:/bin/csh
sklower:8PYh/dUBQT9Ss:2:10:Keith &amp;,4156424972:/usr/staff/sklower:/bin/csh
kridle:4BkcEieEtjWXI:3:10:Bob &amp;,4156426744:/usr/staff/kridle:/bin/csh
kurt:olqH1vDqH38aw:4:10:&amp; Shoens,4156420572:/usr/staff/kurt:/bin/csh
schmidt:FH83PFo4z55cU:7:10:Eric &amp;,4156424951:/usr/staff/schmidt:/bin/csh
hpk:9ycwM8mmmcp4Q:9:10:Howard Katseff,2019495337:/usr/staff/hpk:/bin/csh
tbl:cBWEbG59spEmM:10:10:Tom London,2019492006:/usr/staff/tbl:
jfr:X.ZNnZrciWauE:11:10:John Reiser:/usr/staff/jfr:
mark:Pb1AmSpsVPG0Y:12:10:&amp; Horton,4156428311:/usr/staff/mark:/bin/csh
dmr:gfVwhuAMF0Trw:42:10:Dennis Ritchie:/usr/staff/dmr:
ken:ZghOT0eRm4U9s:52:10:&amp; Thompson:/usr/staff/ken:
sif:IIVxQSvq1V9R2:53:10:Stuart Feldman:/usr/staff/sif:
scj:IL2bmGECQJgbk:60:10:Steve Johnson:/usr/staff/scj:
pjw:N33.MCNcTh5Qw:61:10:Peter J. Weinberger,2015827214:/usr/staff/pjw:/bin/csh
bwk:ymVglQZjbWYDE:62:10:Brian W. Kernighan,2015826021:/usr/staff/bwk:
uucp:P0CHBwE/mB51k:66:10:UNIX-to-UNIX Copy:/usr/spool/uucp:/usr/lib/uucp/uucico
srb:c8UdIntIZCUIA:68:10:Steve Bourne,2015825829:/usr/staff/srb:
finger::199:199:The &amp; Program:/usr/ucb:/usr/ucb/finger
who::199:199:The &amp; Program:/usr/ucb:/bin/who
w::199:199:The &amp; Program:/usr/ucb:/usr/ucb/w
mckusick:AAZk9Aj5/Ue0E:201:10:Kirk &amp;,4156424948:/usr/staff/mckusick:/bin/csh
peter:Nc3IkFJyW2u7E:202:10:&amp; Kessler,4156424948:/usr/staff/peter:/bin/csh
henry:lj1vXnxTAPnDc:203:10:Robert &amp;,4156424948:/usr/staff/henry:/bin/csh
jkf:9ULn5cWTc0b9E:209:10:John Foderaro,4156424972:/usr/staff/jkf:/bin/csh
fateman:E9i8fWghn1p/I:300:10:Richard &amp;,4156421879:/usr/staff/fateman:/bin/csh
fabry:d9B17PTU2RTlM:305:10:Bob &amp;,4156422714:/usr/staff/fabry:/bin/csh
network:9EZLtSYjeEABE:501:50:*:/usr/net/network:/usr/net/network/nsh
tty::504:50::/:/bin/tty我</pre>
<p>（注，以前Unix是一个服务器，所有人都用一个终端到服务器上进行操作，于是，这个服务上的 <code>/etc/passwd</code> 下保存着所有的人的登录密码，能让所有的人都能读到，为了不让别人猜到，这个文件中的密码保存（第二列）被做过哈希处理）</p>
<p>这位程序员一看，这些个用户不就是<a href="https://en.wikipedia.org/wiki/Dennis_Ritchie" target="_blank" rel="noopener noreferrer">Dennis Ritchie</a>, <a href="https://en.wikipedia.org/wiki/Ken_Thompson">Ken Thompson</a>, <a href="https://en.wikipedia.org/wiki/Brian_Kernighan" target="_blank" rel="noopener noreferrer">Brian W. Kernighan</a>, <a href="https://en.wikipedia.org/wiki/Stephen_R._Bourne" target="_blank" rel="noopener noreferrer">Steve Bourne</a>, <a href="https://en.wikipedia.org/wiki/Bill_Joy" target="_blank" rel="noopener noreferrer">Bill Joy</a> 这些神人的密码吗？！于是，他想看看这些人用什么样的密码。考虑到当时的加密算法用的是基于DES的 <a href="https://minnie.tuhs.org/cgi-bin/utree.pl?file=V7/usr/man/man3/crypt.3">crypt(3)</a> 算法（这个算法今天还在用，像Perl/PHP/Python/Ruby都提供<code>crypt()</code> 函数），而且当时的密码最长只支持8个长度，所以，感觉还是很容易暴力破解的。</p>
<p>一般来说，暴力破解的这种hash密码的工具主要是用<a href="https://hashcat.net/" target="_blank" rel="noopener noreferrer">hashcat</a> 或 <a href="https://www.openwall.com/john/" target="_blank" rel="noopener noreferrer">john</a> ，很快，Leah 破解了大多数人的密码，因为大多数都使用的是比较弱的密码，比如： <a href="https://en.wikipedia.org/wiki/Brian_Kernighan" target="_blank" rel="noopener noreferrer">Brian W. Kernighan</a> （<code>bwk</code>）使用了 <code>/.,/.,</code> 这样的密码，而 <a href="https://en.wikipedia.org/wiki/Dennis_Ritchie" target="_blank" rel="noopener noreferrer">Dennis Ritchie</a> （<code>dmr</code>）则使用了 <code>dmac</code> 这样的密码。然后，在破解到 Ken Thompson的密码时，搞不定了，花了好几天穷举完了所有的小写字母+数字都没有找到。</p>
<p>因为这个<code>crypt</code>的算法也是Ken Thompson 和 Robert Morris 写的，他们在40年前就发现，原来的hash算法太快了，这样很容易被暴力穷举，于是在第七版的Unix（1979年发布），他们把算法改成DES的算法，就是要让这个算法变慢。详细地说，用户密码被截断为八个字符，每个字符仅被压缩为7位。这形成56位DES密钥。然后，该密钥用于加密全零位块，然后再次使用相同的密钥对密文进行加密，依此类推，总共进行了25次DES加密。感觉跟区块链的“挖矿”有点像。<strong>在最早的Unix计算机上，这个算法需要花了整整一秒钟的时间来计算密码哈希</strong>。</p>
<p>这几十年来，计算机的计算速度根据摩尔定律至少double了20次，所以，DES算法已经很容被攻击了，然而，对于Ken Thompson的密码，在2014年还是很不容易被破解的，因为，<strong>如果要加上所有的大小写字符数字和其它特殊字符，那么，在2014年，就算用最快的GPU来穷举所有的8位长度的密码，也需要花上至少2年以上的时间</strong>。</p>
<p>在2019年10月份，在 <a href="https://www.tuhs.org/">The Unix Heritage Society</a> 这个社区中，<a href="https://inbox.vuxu.org/tuhs/6dceffe228804a76de1e12f18d1fc0dc@inventati.org/" target="_blank" rel="noopener noreferrer">这个事又被人问起来</a>，说以前有个人破解这些密码，不知道有没有全破解出来了？于是Leah看到了，就回应说，那个人是我，但是还是没干出来……于是好些人进来留言。</p>
<p>5天后，2019年10月08日，一个来自澳大利亚的程序员Nigel Williams说，<a href="https://inbox.vuxu.org/tuhs/CACCFpdx_6oeyNkgH_5jgfxbxWbZ6VtOXQNKOsonHPF2=747ZOw@mail.gmail.com/" target="_blank" rel="noopener noreferrer">Ken的密码我破解出来了</a>，哈希串<code>ZghOT0eRm4U9s</code> 明文是 <code>p/q2-q4!</code>（果然是有数字有特殊字符），小伙说，我在 AMD Radeon Vega 64 的 GPU上运行了 <code>hashcat</code> 这个命令，干了我 4天多，每秒钟的“配速”是930MH/s （每秒钟9亿3千万次hash运算）。然后，<a href="https://inbox.vuxu.org/tuhs/CAG=a+rj8VcXjS-ftaj8P2_duLFSUpmNgB4-dYwnTsY_8g5WdEA@mail.gmail.com/" target="_blank" rel="noopener noreferrer">Ken Thompson 也留言到 “恭喜”</a> ，这样，Ken 的密码在40年后被破解了……</p>
<p>马上，就有人问到，这个密码是不是国际象棋的走棋？嗯，很像中国象棋中的“车五进一”，“马三退一”，这个密码中的 <code>p</code> 代表 <code>pawn</code> 小兵，从 <code>q2</code> 的位置走到 <code>q4</code>，这个看来是国际象棋中的开局进兵——用来做登录密码，非常合适。而且，Ken Thompson 在 Unix中写下的一个国际象棋的程序 <a href="https://en.wikipedia.org/wiki/Belle_(chess_machine)" target="_blank" rel="noopener noreferrer">Belle</a>，在1978年首次参加<a href="https://en.wikipedia.org/wiki/North_American_Computer_Chess_Championship">计算机协会的北美计算机国际象棋锦标赛</a>时，它获得了第一个冠军头衔，其搜索深度为八层。之后又赢得了四次冠军。1983年，它也成为第一台获得国际象棋“大师”称号的计算机。所以，Ken用这个做密码相当make sense!</p>
<p style="text-align: center;"><img decoding="async" loading="lazy" class="aligncenter size-full" src="https://coolshell.cn/wp-content/uploads/2019/11/ken.chess_.jpg" alt="" width="600" height="800" />Ken在贝尔实验室调程序（图片来源：<a href="https://spectrum.ieee.org/tech-history/silicon-revolution/in-1983-this-bell-labs-computer-was-the-first-machine-to-become-a-chess-master" target="_blank" rel="noopener noreferrer">IEEE SPECTRUM</a>）</p>
<p>当然，还有一个人的密码是所有人里最难破解的，这个人就是<a href="https://en.wikipedia.org/wiki/Bill_Joy" target="_blank" rel="noopener noreferrer">Bill Joy</a>，他最初作为加州大学伯克利分校的研究生，在校期间着手改进Unix 内核，并管理BSD发行版。他最著名的贡献是ex和vi编辑器以及C shell。在Sun公司成立6个月后，他正式成为公司的联合创始人，他在Sun公司的推动了NFS，SPARC处理器，以及Java语言。他还是一个风险投资人员。</p>
<p>在Ken的密被破解后两周（2019年10月19日），有人号称已经破解了Bill的密码，他在<a href="https://minnie.tuhs.org/pipermail/tuhs/2019-October/019124.html" target="_blank" rel="noopener noreferrer">邮件组中这样写到</a>：</p>
<blockquote><p>一开始，我使用了大小写字符和数字，8位长度来破解所有的组合，花了我6天的时间，失败了。然后，我开始尝试只用小写字母和控制字符，结果在40分钟内就破解了。但是因为Bill现健在，所以，只要bill同意他才公布这个密码。</p></blockquote>
<p>在密码里存控制字符？这脑洞，Ctrl+C么？破解者还说，他在一个有三个结点的DELL 的HPC集群上完成这个工作，每个结点包括两个 Tesla V100 nVidia GPU 的显卡，一共30720个CUDA核…… 关于这个显卡多少钱，你可以上网搜吧…… 相当于一块劳力士吧……（我估计这组机器平时是用来挖矿的……[狗头]）</p>
<p>好了，我们来看一下这个 <code>/etc/passwd</code> 中的这些人的密码是什么样的，<strong>但最主要的是向这些为人类做过巨大贡献的程序员科学家们致敬</strong>！</p>
<ul>
<li><strong><a href="https://en.wikipedia.org/wiki/Ken_Thompson" target="_blank" rel="noopener noreferrer">Ken Thompson</a></strong><br />
除了是Unix、B语言和Go语言作者之外，他还贡献过正则表达式，QED/ed编辑器，UTF-8编码定义，以及计算机国际象棋Belle……</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>ken</code></td>
<td colspan="1" rowspan="1"><code>ZghOT0eRm4U9s</code></td>
<td colspan="1" rowspan="1"><code>p/q2-q4!</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Dennis_Ritchie" target="_blank" rel="noopener noreferrer">Dennis Ritchie</a></strong><br />
Unix和C语言之父，与Ken于1983年获图灵奖，1990年美国国家海明奖章，于2011年去世。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>dmr</code></td>
<td colspan="1" rowspan="1"><code>gfVwhuAMF0Trw</code></td>
<td colspan="1" rowspan="1"><code>dmac</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Brian_Kernighan" target="_blank" rel="noopener noreferrer">Brian W. Kernighan</a></strong><br />
AWK的作者，是AWK中的“K”，也是与Dennis写的K&amp;C的C语言编程书中的“K”，他还编写了很多Unix的其它程序，如：<code>ditroff</code>，而且，设计了著名的<a href="https://en.wikipedia.org/wiki/Heuristic" target="_blank" rel="noopener noreferrer">启发式算法</a>。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>bwk</code></td>
<td colspan="1" rowspan="1"><code>ymVglQZjbWYDE</code></td>
<td colspan="1" rowspan="1"><code>/.,/.,</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Stephen_R._Bourne" target="_blank" rel="noopener noreferrer">Stephen R. Bourne</a></strong><br />
Bourne shell（<code>sh</code>）的作者，Unix Shell作者，同时也是Unix调试器的作者。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>srb</code></td>
<td colspan="1" rowspan="1"><code>c8UdIntIZCUIA</code></td>
<td colspan="1" rowspan="1"><code>bourne</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Eric_Schmidt" target="_blank" rel="noopener noreferrer">Eric Schmidt</a></strong><br />
你可能知道他是Google的CEO，苹果的董事，但是你可能不知道，他当年是是贝尔实施室的实习生，他对Unix的词法分析器 Lex 进行为了完全的重写。他的密码是中的wendy应该是他的妻子。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>schmidt</code></td>
<td colspan="1" rowspan="1"><code>FH83PFo4z55cU</code></td>
<td colspan="1" rowspan="1"><code>wendy!!!</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Stuart_Feldman" target="_blank" rel="noopener noreferrer">Stuart Feldman</a></strong><br />
他除了是Unix系统小组的成员，他还是第一个Fortran 77 编译器的作者，也是 <code>make</code> 的作者。他还是楼上Shmidt慈善基金会的科学负责人，在Google/IBM Research任过职，也担任过ACM的主席。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>sif</code></td>
<td colspan="1" rowspan="1"><code>IIVxQSvq1V9R2</code></td>
<td colspan="1" rowspan="1"><code>axolotl</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Mary_Ann_Horton" target="_blank" rel="noopener noreferrer">Mark Horton</a></strong><br />
Unix贡献者，包括vi和curses，后来变性为女性，新的名字叫Mary Ann Horton。原来的照片在<a href="http://www.ugu.com/sui/ugu/show?I=info.Mark_R._Horton" target="_blank" rel="noopener noreferrer">Unix Guru Universe</a></p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>mark</code></td>
<td colspan="1" rowspan="1"><code>Pb1AmSpsVPG0Y</code></td>
<td colspan="1" rowspan="1"><code>uio</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Marshall_Kirk_McKusick" target="_blank" rel="noopener noreferrer">Kirk McKusick</a></strong><br />
BSD贡献者，主要负责文件系统UFS以及fsck命令，同时也是<code>gprof</code>的贡献者，公开的同性恋者。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>mckusick</code></td>
<td colspan="1" rowspan="1"><code>AAZk9Aj5/Ue0E</code></td>
<td colspan="1" rowspan="1"><code>foobar</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Richard_Fateman" target="_blank" rel="noopener noreferrer">Richard Fateman</a></strong><br />
他在伯克利的VAX UNIX系统的开发工作中发挥了重要作用，以及开发了<a href="https://en.wikipedia.org/wiki/Franz_Lisp" target="_blank" rel="noopener noreferrer"> Franz Lisp</a>。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>fateman</code></td>
<td colspan="1" rowspan="1"><code>E9i8fWghn1p/I</code></td>
<td colspan="1" rowspan="1"><code>apr1744</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong>Peter Kessler</strong><br />
这位老兄能在网上查到的资料基本没有，可以查到他是 <code>gprof</code> 的贡献者，以及有名字的<a href="https://web.eecs.umich.edu/~weimerw/2009-4610/reading/graham-gprof.pdf" target="_blank" rel="noopener noreferrer">gprof的一篇论文</a></p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>peter</code></td>
<td colspan="1" rowspan="1"><code>Nc3IkFJyW2u7E</code></td>
<td colspan="1" rowspan="1"><code>...hello</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong>Kurt Shoens</strong><br />
BSD电子邮件开发者。Unix早期版本中使用 <code>uux</code> 和 <code>sendmail</code> 来进行远程消息传递，1978年，Kurt为Unix编写了一个邮件用户代理 Berkeley Mail。相关的历史可以参看<a href="http://heirloom.sourceforge.net/mailx_history.html" target="_blank" rel="noopener noreferrer">这篇文章</a>。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>kurt</code></td>
<td colspan="1" rowspan="1"><code>olqH1vDqH38aw</code></td>
<td colspan="1" rowspan="1"><code>sacristy</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://franz.com/about/press_room/foderaro_2-2-2015.lhtml" target="_blank" rel="noopener noreferrer">John Foderaro</a></strong><br />
他为Berkeley的Lisp语言编写原始的编译器，Lisp语言是一种类似于数据代数的语言，在计算机历史上有和C语言一样的作用。后来他成立了Franz公司，主要开发和部署图形搜索解决方案。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>jkf</code></td>
<td colspan="1" rowspan="1"><code>9ULn5cWTc0b9E</code></td>
<td colspan="1" rowspan="1"><code>sherril.</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Peter_J._Weinberger" target="_blank" rel="noopener noreferrer">Peter J. Weinberger</a></strong><br />
他就是AWK中的那个“W”，同时也是Fortan编译器f77的贡献者，后来是<a title="" href="https://en.wikipedia.org/wiki/Renaissance_Technologies">Renaissance Technologies</a> （一家对冲基金）的CTO，现在在Google工作，</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>pjw</code></td>
<td colspan="1" rowspan="1"><code>N33.MCNcTh5Qw</code></td>
<td colspan="1" rowspan="1"><code>uucpuucp</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong>John Reiser</strong><br />
他主要工作是将Unix和C移植到了DEC VAX上，这个机器在学术界相当流行（陈皓注：我在1994年上大学的时候，就是在这个机器上学习的C语言）。这扩大了Unix和C的影响力。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>jfr</code></td>
<td colspan="1" rowspan="1"><code>X.ZNnZrciWauE</code></td>
<td colspan="1" rowspan="1"><code>5%ghj</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Stephen_C._Johnson" target="_blank" rel="noopener noreferrer">Steve Johnson</a></strong><br />
曾在贝尔实验室和AT＆T工作近20年。他以Yacc，Lint，spell和Portable C编译器而闻名。后来他去了硅谷，加入了一些创业公司，主要从事编译器的工作，以及2D和3D图形，大规模并行系统和嵌入式系统的开发工作。现在他在Wave Computing从事机器学习的工作。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>scj</code></td>
<td colspan="1" rowspan="1"><code>IL2bmGECQJgbk</code></td>
<td colspan="1" rowspan="1"><code>pdq;dq</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong>Bob Kridle</strong><br />
这位老兄的资料在没有太多，只能在 <a href="https://www.oreilly.com/openbook/opensources/book/kirkmck.html_original" target="_blank" rel="noopener noreferrer">Berkeley Unix 20 年</a> 上看到他跟Ken Thompson混过一段时间。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>kridle</code></td>
<td colspan="1" rowspan="1"><code>4BkcEieEtjWXI</code></td>
<td colspan="1" rowspan="1"><code>jilland1</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://people.eecs.berkeley.edu/~sklower/" target="_blank" rel="noopener noreferrer">Keith Sklower</a></strong><br />
BSD 的一个程序员。从他的主页上可以看到他目前在Berkeley大学，信息分析师，主要研究一些网络通信相关的技术。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>sklower</code></td>
<td colspan="1" rowspan="1"><code>8PYh/dUBQT9Ss</code></td>
<td colspan="1" rowspan="1"><code>theik!!!</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong>Robert Henry</strong><br />
网上的资料不多，只在<a href="https://www.tuhs.org/Archive/Documentation/Books/Life_with_Unix.pdf" target="_blank" rel="noopener noreferrer">Life with Unix</a>这本电子书中查到，他写了 <code>error</code></p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>henry</code></td>
<td colspan="1" rowspan="1"><code>lj1vXnxTAPnDc</code></td>
<td colspan="1" rowspan="1"><code>sn74193n</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong>Howard Katseff</strong><br />
网上的资料不多，只在<a href="https://www.tuhs.org/Archive/Documentation/Books/Life_with_Unix.pdf" target="_blank" rel="noopener noreferrer">Life with Unix</a>这本电子书中查到，他写了 <code>sdb</code> 和 <code>last</code></p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>hpk</code></td>
<td colspan="1" rowspan="1"><code>9ycwM8mmmcp4Q</code></td>
<td colspan="1" rowspan="1"><code>graduat;</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/%C3%96zalp_Babao%C4%9Flu" target="_blank" rel="noopener noreferrer">Özalp Babaoğlu</a></strong><br />
土耳其计算机科学家，1981年在Berkeley担任 BSD Unix的首席设计师，曾经与Sun的创造人Bill Joy在BSD上实现了虚拟内存。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>ozalp</code></td>
<td colspan="1" rowspan="1"><code>m5syt3.lB5LAE</code></td>
<td colspan="1" rowspan="1"><code>12ucdort</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong><a href="https://en.wikipedia.org/wiki/Bob_Fabry" target="_blank" rel="noopener noreferrer">Bob Fabry</a></strong><br />
他主要推动美国国防部高级研究计划局DARPA采用了Unix系统</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>fabry</code></td>
<td colspan="1" rowspan="1"><code>d9B17PTU2RTlM</code></td>
<td colspan="1" rowspan="1"><code>561cml..</code></td>
</tr>
</tbody>
</table>
</li>
<li><strong>Tom London</strong><br />
他和John Reiser在把Unix移植到了VAX-11机上。</p>
<table>
<tbody>
<tr>
<td colspan="1" rowspan="1">登录名</td>
<td colspan="1" rowspan="1"><b>哈希串</b></td>
<td colspan="1" rowspan="1"><b>密码</b></td>
</tr>
<tr>
<td colspan="1" rowspan="1"><code>tbl</code></td>
<td colspan="1" rowspan="1"><code>cBWEbG59spEmM</code></td>
<td colspan="1" rowspan="1"><code>..pnn521</code></td>
</tr>
</tbody>
</table>
</li>
</ul>
<p>最后，再首尾呼应一下，在我的技术生涯中，Unix文化对我个人的技术观影响是非常大的，<strong>我个人认为 Unix 就像摇滚乐一样，上世纪60年代-80年代，是整个人类最经典最光亮的时代，值得我们每个人向那个时代的人和事致敬！</strong></p>
<p>————————————————————————</p>
<p>P.S.</p>
<p>你可以浏览 Github 的 <a href="https://github.com/dspinellis/unix-history-repo/tree/BSD-3-Snapshot-Development" target="_blank" rel="noopener noreferrer">unix-history-repo</a> 目录（注：本文给的这个链接不在master分支上），这个repo是40年前的代码，涵盖了从1970年创建时的2.5万行内核和26条命令到2017年为止广泛使用的2700万行系统。1.1GB的存储库包含大约一百万次提交和两千多次合并。通过<a href="http://www.dmst.aueb.gr/dds/pubs/jrnl/2016-EMPSE-unix-history/html/unix-history.html" target="_blank" rel="noopener noreferrer">这个链接</a>你可以了解一下这个代码的历史！</p>
<p>下载这些代码需要你的1.5GB的硬盘空间，你可以查看各个大神写的代码，包括 Ken Thompson 和 Dennis的，以及相关的注释。</p>
<p>根据这些，你还可以找到 Ken Thompson的 Github账号 <a href="https://github.com/ken" target="_blank" rel="noopener noreferrer">https://github.com/ken</a> 以及别人为dmr建的github帐号 <a href="https://github.com/dmr-1941-2011">https://github.com/dmr-1941-2011</a></p>
<p>P.S.S</p>
<p>下面是一些和Unix相关的维基百科资料</p>
<ul>
<li><a href="https://en.wikipedia.org/wiki/History_of_Unix" target="_blank" rel="nofollow noopener noreferrer">History of Unix</a></li>
<li><a href="https://en.wikipedia.org/wiki/List_of_Unix_systems" target="_blank" rel="nofollow noopener noreferrer">List of Unix systems</a></li>
<li><a href="https://en.wikipedia.org/wiki/List_of_Unix_commands" target="_blank" rel="nofollow noopener noreferrer">List of Unix commands</a></li>
<li><a href="https://en.wikipedia.org/wiki/List_of_Unix_daemons" target="_blank" rel="nofollow noopener noreferrer">List of Unix daemons</a></li>
<li><a href="https://en.wikipedia.org/wiki/Research_Unix" target="_blank" rel="nofollow noopener noreferrer">Research Unix</a></li>
<li><a href="http://en.wikipedia.org/wiki/BSD_Unix" target="_blank" rel="nofollow noopener noreferrer">Berkeley Software Distribution</a></li>
<li><a href="https://en.wikipedia.org/wiki/Unix_philosophy" target="_blank" rel="nofollow noopener noreferrer">Unix philosophy</a></li>
</ul>
<p>还有Unix的社区：TUHS: The Unix Heritage Society &#8211; <a href="http://minnie.tuhs.org/cgi-bin/utree.pl" rel="nofollow">The Unix Tree</a></p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/2322.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2010/04/o_unixrichiethompson-150x150.jpg" alt="Unix传奇(上篇)" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2322.html" class="wp_rp_title">Unix传奇(上篇)</a></li><li ><a href="https://coolshell.cn/articles/9410.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/04/figure1-150x150.gif" alt="Unix考古记：一个“遗失”的shell" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9410.html" class="wp_rp_title">Unix考古记：一个“遗失”的shell</a></li><li ><a href="https://coolshell.cn/articles/1761.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/11/spell_it_with_e-150x150.jpg" alt="Go语言源码的一个改动" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1761.html" class="wp_rp_title">Go语言源码的一个改动</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/systemd-1-150x150.jpeg" alt="Linux PID 1 和 Systemd" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_title">Linux PID 1 和 Systemd</a></li><li ><a href="https://coolshell.cn/articles/12103.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-150x150.gif" alt="vfork 挂掉的一个问题" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12103.html" class="wp_rp_title">vfork 挂掉的一个问题</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/19996.html">Unix 50 年：Ken Thompson 的密码</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/19996.html/feed</wfw:commentRss>
			<slash:comments>30</slash:comments>
		
		
			</item>
		<item>
		<title>打造高效的工作环境 &#8211; Shell 篇</title>
		<link>https://coolshell.cn/articles/19219.html</link>
					<comments>https://coolshell.cn/articles/19219.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sun, 17 Mar 2019 05:53:01 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[编程工具]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Programmer]]></category>
		<category><![CDATA[Shell]]></category>
		<category><![CDATA[程序员]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=19219</guid>

					<description><![CDATA[<p>注：本文由雷俊(Javaer/Emacser)和我一起编辑，所以文章版权归雷俊与我共同所有，转载者必需注明出处和我们两位作者。原文最早发于酷壳微信公众号，后来我...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/19219.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/19219.html">打造高效的工作环境 – Shell 篇</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script></p>
<blockquote><p><strong>注：本文由<a href="https://github.com/rayjun" target="_blank" rel="noopener noreferrer">雷俊</a>(Javaer/Emacser)和我一起编辑，所以文章版权归雷俊与我共同所有，转载者必需注明出处和我们两位作者。原文最早发于酷壳微信公众号，后来我又做了一些修改，再发到博客这边。</strong></p></blockquote>
<p><img decoding="async" loading="lazy" class="alignright size-full wp-image-19230" src="https://coolshell.cn/wp-content/uploads/2019/03/linux.ninja_.png" alt="" width="255" height="220" />程序员是一个很懒的群体，总想着能够让代码为自己干活，他们不断地把工作生活中的一些事情用代码自动化了，从而让整个社会的效率运作地越来越高。所以，程序员在准备去优化这个世界的时候，都会先要优化自己的工作环境，是所谓“工欲善其事，必先利其器”。</p>
<p>我们每个程序员都应该打造一套让自己更为高效的工作环境。那怕就是让你少输入一次命令，少按一次键，少在鼠标和键盘间切换一次，都会让程序员的工作变得更为的高效。所以，程序员一般需要一台性能比较好，不会因为开了太多的网页或程序就卡得不行的电脑，还要配备多个显示器，一个显示器写代码，一个查文档，一个测试运行结果，而不必在各种窗口来来回回的切换……在大量的窗口间切换经常会迷路，而且也容易出错（分不清线上或测试环境）……</p>
<p>除了硬件上的装备，软件上也是能够提升程序员生产力的地方，<strong>在软件层面提升程序员生产力的东西有一个很重要的事就是命令行和脚本</strong>，使用鼠标和图形界面则会大大降低程序员的生产力。酷壳以前也写过一些，如《<a href="https://coolshell.cn/articles/8619.html" target="_blank" rel="noopener noreferrer">你可能不知道的Shell</a>》和《 <a href="https://coolshell.cn/articles/8883.html" target="_blank" rel="noopener noreferrer">应该知道的Linux技巧</a>》，但是Unix/Linux Shell就是一个大宝库，怎么写也写不完，不然，怎么会有“Where is the Shell, there is a way”。</p>
<p><span id="more-19219"></span></p>
<h4>命令行</h4>
<p>在不同的操作系统下，都有着很不错的命令行工具，比如 Mac 下的 <strong>Iterm2</strong>，Linux 下的原生命令行，如果你是在 Windows 下工作，问题也不大，因为 Windows 下现在有了 <strong>WSL</strong>。WSL 提供了一个由微软开发的Linux兼容的内核接口（不包含Linux内核代码），然后可以在其上运行GNU用户空间，例如 Ubuntu，openSUSE，SUSE Linux Enterprise Server，Debian和Kali Linux。这样的用户空间可能包含 Bash shell 和命令语言，使用本机 GNU/Linux 命令行工具（sed，awk 等），编程语言解释器（Ruby，Python 等），甚至是图形应用程序（使用主机端的X窗口系统）。</p>
<p>使用命令行可以完成所有日常的操作，新建文件夹（mkdir）、新建文件（touch）、移动（mv）、复制（cp）、删除（rm）等等。而且使用 Linux/Unix 命令行最好的方式是可以用 <code>awk</code>、<code>sed</code>、<code>grep</code>、<code>xargs</code>、<code>find</code>、<code>sort</code> 等等这样的命令，然后用管道把其串起来，就可以完成一个你想要的功能，尤其是一些简单的数据统计功能。这是Linux命令行不可比拟的优势。比如：</p>
<ul>
<li>查看连接你服务器 top10 用户端的 IP 地址：</li>
</ul>
<p><code>netstat -nat | awk '{print $5}' | awk -F ':' '{print $1}' | sort | uniq -c | sort -rn | head -n 10</code></p>
<ul>
<li>查看一下你最常用的10个命令：</li>
</ul>
<p><code>cat .bash_history | sort | uniq -c | sort -rn | head -n 10 (or cat .zhistory | sort | uniq -c | sort -rn | head -n 10</code></p>
<p>（注：<code>awk</code> 和 <code>sed</code> 是两大神器，所以，我以前的也有两篇文章来介绍它们——《<a href="https://coolshell.cn/articles/9070.html" target="_blank" rel="noopener noreferrer">awk简明教程</a>》和《<a href="https://coolshell.cn/articles/9104.html" target="_blank" rel="noopener noreferrer">sed简明教程</a>》，你可以前往一读）</p>
<p>在命令行中使用 <strong>alias</strong> 可以将使用频率很高命令或者比较复杂的命令合并成一个命令，或者修改原生的命令。</p>
<p>下面这几个命令，可能是你天天都在敲的。所以，你应该设置成 alias 来提高效率</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
alias nis=&quot;npm install --save &quot;
alias svim=&#039;sudo vim&#039;
alias mkcd=&#039;foo(){ mkdir -p &quot;$1&quot;; cd &quot;$1&quot; }; foo &#039;
alias install=&#039;sudo apt get install&#039;
alias update=&#039;sudo apt-get update; sudo apt-get upgrade&#039;
alias ..=&quot;cd ..&quot;
alias ...=&quot;cd ..; cd ..&quot;
alias www=&#039;python -m SimpleHTTPServer 8000&#039;
alias sock5=&#039;ssh -D 8080 -q -C -N -f user@your.server&#039;
</pre>
<p>你还可以参考如下的一些文章，看看别人是怎么用好 <code>alias</code> 的</p>
<ul>
<li><a href="https://www.cyberciti.biz/tips/bash-aliases-mac-centos-linux-unix.html" rel="nofollow">30 Handy Bash Shell Aliases For Linux / Unix / Mac OS X</a></li>
<li><a href="https://www.digitalocean.com/community/questions/what-are-your-favorite-bash-aliases" rel="nofollow">What are your favorite bash aliases?</a></li>
<li><a href="https://www.linuxtrainingacademy.com/23-handy-bash-shell-aliases-for-unix-linux-and-mac-os-x/" rel="nofollow">23 Handy Bash Shell Aliases For Unix, Linux, and Mac OS X</a></li>
<li><a href="https://brettterpstra.com/2013/03/31/a-few-more-of-my-favorite-shell-aliases/" rel="nofollow">A few more of my favorite Bash aliases</a></li>
</ul>
<p>命令行中除了原生的命令之外，还有很多可以提升使用体验的工具。下面罗列一些很不错的命令，把原生的命令增强地很厉害:</p>
<ul>
<li><a href="https://github.com/clvv/fasd" target="_blank" rel="noopener noreferrer"><strong>fasd</strong></a> 增强了 <code>cd</code> 命令 。</li>
<li><a href="https://github.com/sharkdp/bat" target="_blank" rel="noopener noreferrer"><strong>bat</strong></a> 增强了 <code>cat</code> 命令 。如果你想要有语法高亮的 <code>cat</code>，可以试试 <a href="https://github.com/jingweno/ccat" target="_blank" rel="noopener noreferrer"><strong>ccat</strong></a> 命令。</li>
<li><a href="https://github.com/ogham/exa" target="_blank" rel="noopener noreferrer"><strong>exa</strong></a> 增强了 <code>ls</code> 命令，如果你需要在很多目录上浏览各种文件 ，<a href="https://github.com/ranger/ranger" target="_blank" rel="noopener noreferrer"><strong>ranger</strong></a> 命令可以比 <code>cd</code> 和 <code>cat</code> 更有效率，甚至可以在你的终端预览图片。</li>
<li><a href="https://github.com/sharkdp/fd" target="_blank" rel="noopener noreferrer"><strong>fd</strong></a> 是一个比 <code>find</code> 更简单更快的命令，他还会自动地忽略掉一些你配置在 <code>.gitignore</code> 中的文件，以及 <code>.git</code> 下的文件。</li>
<li><a href="https://github.com/junegunn/fzf" target="_blank" rel="noopener noreferrer"><strong>fzf</strong></a> 会是一个很好用的文件搜索神器，其主要是搜索当前目录以下的文件，还可以使用 <code>fzf --preview 'cat {}'</code>边搜索文件边浏览内容。</li>
<li><code>grep</code> 是一个上古神器，然而，<a href="https://beyondgrep.com/" target="_blank" rel="noopener noreferrer"><strong>ack</strong></a>、<a href="https://github.com/ggreer/the_silver_searcher" target="_blank" rel="noopener noreferrer"><strong>ag</strong></a> 和 <a href="https://github.com/BurntSushi/ripgrep" target="_blank" rel="noopener noreferrer"><strong>rg</strong></a> 是更好的grep，和上面的 <code>fd</code>一样，在递归目录匹配的时候，会使用你配置在 <code>.gitignore</code> 中的规则。</li>
<li><code>rm</code> 是一个危险的命令，尤其是各种 <code>rm -rf …</code>，所以，<a href="https://github.com/andreafrancia/trash-cli/" target="_blank" rel="noopener noreferrer"><strong>trash</strong></a> 是一个更好的删除命令。</li>
<li><code>man</code> 命令是好读文档的命令，但是man的文档有时候太长了，所以，你可以试试 <a href="https://github.com/tldr-pages/tldr" target="_blank" rel="noopener noreferrer"><strong>tldr</strong></a> 命令，把文档上的一些示例整出来给你看。</li>
<li>如果你想要一个图示化的<code>ping</code>，你可以试试 <a href="https://github.com/denilsonsa/prettyping" target="_blank" rel="noopener noreferrer"><strong>prettyping</strong></a> 。</li>
<li>如果你想搜索以前打过的命令，不要再用 Ctrl +R 了，你可以使用加强版的 <a href="https://github.com/dvorka/hstr" target="_blank" rel="noopener noreferrer"><strong>hstr</strong></a>  。</li>
<li><a href="https://hisham.hm/htop/" target="_blank" rel="noopener noreferrer"><strong>htop</strong></a>  是 top 的一个加强版。然而，还有很多的各式各样的top，比如：用于看IO负载的 <a href="http://guichaz.free.fr/iotop/" target="_blank" rel="noopener noreferrer"><strong>iotop</strong></a>，网络负载的 <a href="http://www.ex-parrot.com/~pdw/iftop/" target="_blank" rel="noopener noreferrer"><strong>iftop</strong></a>, 以及把这些top都集成在一起的 <a href="https://github.com/Atoptool/atop" target="_blank" rel="noopener noreferrer"><strong>atop</strong></a>。</li>
<li><a href="https://dev.yorhel.nl/ncdu" target="_blank" rel="noopener noreferrer"><strong>ncdu</strong></a>  比 du 好用多了用。另一个选择是 <a href="https://github.com/jarun/nnn" target="_blank" rel="noopener noreferrer">nnn</a>。</li>
<li>如果你想把你的命令行操作建录制成一个 SVG 动图，那么你可以尝试使用 <a href="https://asciinema.org/" target="_blank" rel="noopener noreferrer"><strong>asciinema</strong></a> 和 <a href="https://github.com/marionebl/svg-term-cli" target="_blank" rel="noopener noreferrer"><strong>svg-trem</strong></a> 。</li>
<li><a href="https://github.com/jakubroztocil/httpie" target="_blank" rel="noopener noreferrer"><strong>httpie</strong></a> 是一个可以用来替代 <code>curl</code> 和 <code>wget</code> 的 http 客户端，<code>httpie</code> 支持 json 和语法高亮，可以使用简单的语法进行 http 访问: <code>http -v github.com</code>。</li>
<li><a href="https://github.com/tmux/tmux" target="_blank" rel="noopener noreferrer"><strong>tmux</strong></a> 在需要经常登录远程服务器工作的时候会很有用，可以保持远程登录的会话，还可以在一个窗口中查看多个 shell 的状态。</li>
<li><a href="https://github.com/klaussinani/taskbook" target="_blank" rel="noopener noreferrer"><strong>Taskbook</strong></a> 是可以完全在命令行中使用的任务管理器 ，支持 ToDo 管理，还可以为每个任务加上优先级。</li>
<li><a href="https://github.com/Russell91/sshrc" target="_blank" rel="noopener noreferrer"><strong>sshrc</strong></a> 是个神器，在你登录远程服务器的时候也能使用本机的 shell 的 rc 文件中的配置。</li>
<li><a href="https://github.com/allinurl/goaccess" target="_blank" rel="noopener noreferrer"><strong>goaccess</strong></a>  这个是一个轻量级的分析统计日志文件的工具，主要是分析各种各样的 access log。</li>
</ul>
<p>关于这些增加命令，主要是参考自下面的这些文章</p>
<ol>
<li><a href="https://dev.to/_darrenburns/10-tools-to-power-up-your-command-line-4id4" target="_blank" rel="nofollow noopener noreferrer">10 Tools To Power Up Your Command Line</a></li>
<li><a href="https://dev.to/_darrenburns/tools-to-power-up-your-command-line-part-2-2737" target="_blank" rel="nofollow noopener noreferrer">5 More Tools To Power Up Your Command Line (Part 2 Of Series)</a></li>
<li><a href="https://dev.to/_darrenburns/power-up-your-command-line-part-3-4o53" target="_blank" rel="nofollow noopener noreferrer">Power Up Your Command Line, Part 3</a></li>
<li><a href="https://darrenburns.net/posts/tools/" target="_blank" rel="nofollow noopener noreferrer">Power Up Your Command Line</a></li>
<li><a href="https://hacker-tools.github.io/" target="_blank" rel="nofollow noopener noreferrer">Hacker Tools</a></li>
</ol>
<h4>Shell 和脚本</h4>
<p>shell 是可以与计算机进行高效交互的文本接口。shell 提供了一套交互式的编程语言（脚本），shell的种类很多，比如 <strong>sh</strong>、<strong>bash</strong>、<strong>zsh</strong> 等。</p>
<p>shell 的生命力很强，在各种高级编程语言大行其道的今天，很多的任务依然离不开 shell。比如可以使用 shell 来执行一些编译任务，或者做一些批处理任务，初始化数据、打包程序等等。</p>
<p>现在比较流行的是 <strong>zsh</strong> + <a href="https://ohmyz.sh/" target="_blank" rel="noopener noreferrer"><strong>oh-my-zsh</strong></a> + <a href="https://github.com/zsh-users/zsh-autosuggestions" target="_blank" rel="noopener noreferrer"><strong>zsh-autosuggestions</strong></a> 的组合，你也可以试试看。其中 zsh 和 oh-my-zsh 算是常规操作了，但是 zsh-autosuggestions 特别有用，可以超级快速的帮你补全你输入过的命令，让命令行的操作更加高效。</p>
<p>另外，<strong><a href="https://fishshell.com/" target="_blank" rel="noopener noreferrer">fish</a> </strong>也是另外一个牛逼的shell，比如：命令行自动完成（根据历史记录），命令行命令高亮，当你要输入命令行参数的时候，自动提示有哪些参数…… fish在很多地方也是用起来很爽的。和上面的 oh-my-zsh 有点不分伯仲了。</p>
<p>你也许会说，用 Python 脚本或 PHP 来写脚本会比 Shell 更好更没有 bug，但我要申辩一下:</p>
<ul>
<li>其一，如果你有一天要维护线上机器的时候，或是到了银行用户的系统（与外网完全隔离，而且服务器上没有安装 Python/PHP 或是他们的的高级库，那么，你只有 Shell 可以用了）。</li>
<li>其二，而且，如果要跟命令行交互很多的话，Shell 是不二之选，试想一下，如果你要去 100 台远程的机器上查access.log 日志中有没有某个错误，完成这个工作你是用 PHP/Python 写脚本快还是用 Shell 写脚本快呢？</li>
</ul>
<p>所以，<strong>我们还要学会只使用传统的grep/awk/sed等等这些POSIX的原生的系统默认安装的命令</strong>。</p>
<p>当然，要写好一个脚本并不容易，下面有一些小模板供你参考：</p>
<p>处理命令行参数的一个样例</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">while [ &quot;$1&quot; != &quot;&quot; ]; do
    case $1 in
        -s  )   shift	
		SERVER=$1 ;;  
        -d  )   shift
		DATE=$1 ;;
	--paramter|p ) shift
		PARAMETER=$1;;
        -h|help  )   usage # function call
                exit ;;
        * )     usage # All other parameters
                exit 1
    esac
    shift
done </pre>
<p>命令行菜单的一个样例</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
#!/bin/bash
# Bash Menu Script Example

PS3=&#039;Please enter your choice: &#039;
options=(&quot;Option 1&quot; &quot;Option 2&quot; &quot;Option 3&quot; &quot;Quit&quot;)
select opt in &quot;${options[@]}&quot;
do
    case $opt in
        &quot;Option 1&quot;)
            echo &quot;you chose choice 1&quot;
            ;;
        &quot;Option 2&quot;)
            echo &quot;you chose choice 2&quot;
            ;;
        &quot;Option 3&quot;)
            echo &quot;you chose choice $REPLY which is $opt&quot;
            ;;
        &quot;Quit&quot;)
            break
            ;;
        *) echo &quot;invalid option $REPLY&quot;;;
    esac
done
</pre>
<p>颜色定义，你可以使用 <code>echo -e "${Blu}blue ${Red}red ${RCol}etc...."</code> 进行有颜色文本的输出</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
RCol=&#039;\e[0m&#039;    # Text Reset

# Regular           Bold                Underline           High Intensity      BoldHigh Intens     Background          High Intensity Backgrounds
Bla=&#039;\e[0;30m&#039;;     BBla=&#039;\e[1;30m&#039;;    UBla=&#039;\e[4;30m&#039;;    IBla=&#039;\e[0;90m&#039;;    BIBla=&#039;\e[1;90m&#039;;   On_Bla=&#039;\e[40m&#039;;    On_IBla=&#039;\e[0;100m&#039;;
Red=&#039;\e[0;31m&#039;;     BRed=&#039;\e[1;31m&#039;;    URed=&#039;\e[4;31m&#039;;    IRed=&#039;\e[0;91m&#039;;    BIRed=&#039;\e[1;91m&#039;;   On_Red=&#039;\e[41m&#039;;    On_IRed=&#039;\e[0;101m&#039;;
Gre=&#039;\e[0;32m&#039;;     BGre=&#039;\e[1;32m&#039;;    UGre=&#039;\e[4;32m&#039;;    IGre=&#039;\e[0;92m&#039;;    BIGre=&#039;\e[1;92m&#039;;   On_Gre=&#039;\e[42m&#039;;    On_IGre=&#039;\e[0;102m&#039;;
Yel=&#039;\e[0;33m&#039;;     BYel=&#039;\e[1;33m&#039;;    UYel=&#039;\e[4;33m&#039;;    IYel=&#039;\e[0;93m&#039;;    BIYel=&#039;\e[1;93m&#039;;   On_Yel=&#039;\e[43m&#039;;    On_IYel=&#039;\e[0;103m&#039;;
Blu=&#039;\e[0;34m&#039;;     BBlu=&#039;\e[1;34m&#039;;    UBlu=&#039;\e[4;34m&#039;;    IBlu=&#039;\e[0;94m&#039;;    BIBlu=&#039;\e[1;94m&#039;;   On_Blu=&#039;\e[44m&#039;;    On_IBlu=&#039;\e[0;104m&#039;;
Pur=&#039;\e[0;35m&#039;;     BPur=&#039;\e[1;35m&#039;;    UPur=&#039;\e[4;35m&#039;;    IPur=&#039;\e[0;95m&#039;;    BIPur=&#039;\e[1;95m&#039;;   On_Pur=&#039;\e[45m&#039;;    On_IPur=&#039;\e[0;105m&#039;;
Cya=&#039;\e[0;36m&#039;;     BCya=&#039;\e[1;36m&#039;;    UCya=&#039;\e[4;36m&#039;;    ICya=&#039;\e[0;96m&#039;;    BICya=&#039;\e[1;96m&#039;;   On_Cya=&#039;\e[46m&#039;;    On_ICya=&#039;\e[0;106m&#039;;
Whi=&#039;\e[0;37m&#039;;     BWhi=&#039;\e[1;37m&#039;;    UWhi=&#039;\e[4;37m&#039;;    IWhi=&#039;\e[0;97m&#039;;    BIWhi=&#039;\e[1;97m&#039;;   On_Whi=&#039;\e[47m&#039;;    On_IWhi=&#039;\e[0;107m&#039;;
</pre>
<p>取当前运行脚本绝对路径的示例：（注：Linux下可以用 <code>dirname $(readlink -f $0)</code> ）</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
FILE=&quot;$0&quot;
while [[ -h ${FILE} ]]; do
    FILE=&quot;`readlink &quot;${FILE}&quot;`&quot;
done
pushd &quot;`dirname &quot;${FILE}&quot;`&quot; &gt; /dev/null
DIR=`pwd -P`
popd &gt; /dev/null
</pre>
<p>如何在远程服务器运行一个本地脚本</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">#无参数
ssh user@server &#039;bash -s&#039; &lt; local.script.sh

#有参数
ssh user@server ARG1=&quot;arg1&quot; ARG2=&quot;arg2&quot; &#039;bash -s&#039; &lt; local_script.sh
</pre>
<p>如何检查一个命令是否存在，用 <code>which</code> 吗？最好不要用，因为很多操作系统的 <code>which</code> 命令没有设置退出状态码，这样你不知道是否是有那个命令。所以，你应该使用下面的方式。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
# POSIX 兼容:
command -v [the_command]

# bash 环境:
hash [the_command]
type [the_command]

# 示例：
gnudate() {
    if hash gdate 2&gt; /dev/null; then
        gdate &quot;$@&quot;
    else
        date &quot;$@&quot;
    fi
}
</pre>
<p>然后，如果要写出健壮性更好的脚本，下面是一些相关的技巧：</p>
<ul>
<li>使用 <code>-e</code> 参数，如：<code>set -e</code> 或是 <code>#!/bin/sh -e</code>，这样设置会让你的脚本出错就会停止运行，这样一来可以防止你的脚本在出错的情况下还在拼拿地干活停不下来。</li>
<li>使用 <code>-u</code> 参数，如： <code>set -eu</code>，这意味着，如果你代码中有变量没有定义，就会退出。</li>
<li>对一些变理，你可以使用默认值。如：<code>${FOO:-'default'}</code></li>
<li>处理你代码的退出码。这样方便你的脚本跟别的命令行或脚本集成。</li>
<li>尽量不要使用 <code>;</code> 来执行多个命令，而是使用 <code>&amp;&amp;</code>，这样会在出错的时候停止运行后续的命令。</li>
<li>对于一些字符串变量，使用引号括起，避免其中有空格或是别的什么诡异字符。</li>
<li>如果你的脚有参数，你需要检查脚本运行是否带了你想要的参数，或是，你的脚本可以在没有参数的情况下安全的运行。</li>
<li>为你的脚本设置 <code>-h</code> 和 <code>--help</code> 来显示帮助信息。千万不要把这两个参数用做为的功能。</li>
<li>使用 <code>$()</code> 而不是 <code data-enlighter-language="raw" class="EnlighterJSRAW"></code> 来获得命令行的输出，主要原因是易读。</li>
<li>小心不同的平台，尤其是 MacOS 和 Linux 的跨平台。</li>
<li>对于 <code>rm -rf</code> 这样的高危操作，需要检查后面的变量名是否为空，比如：<code>rm -rf $MYDIDR/*</code> 如果 <code>$MYDIR</code>为空，结果是灾难性的。</li>
<li>考虑使用 &#8220;find/while&#8221; 而不是 “for/find”。如：<code>for F in $(find . -type f) ; do echo $F; done</code> 写成 <code>find . -type f | while read F ; do echo $F ; done</code> 不但可以容忍空格，而且还更快。</li>
<li>防御式编程，在正式执行命令前，把相关的东西都检查好，比如，文件目录有没有存在。</li>
</ul>
<p>你还可以使用ShellCheck 来帮助你检查你的脚本。</p>
<ul>
<li><a href="https://www.shellcheck.net/" target="_blank" rel="noopener noreferrer">https://www.shellcheck.net/</a></li>
</ul>
<p>最后推荐一些 Shell 和脚本的参考资料。</p>
<p>各种有意思的命令拼装，一行命令走天涯:</p>
<ul>
<li><a href="http://www.bashoneliners.com/" target="_blank" rel="nofollow noopener noreferrer">http://www.bashoneliners.com/</a></li>
<li><a href="http://www.shell-fu.org/" target="_blank" rel="nofollow noopener noreferrer">http://www.shell-fu.org/</a></li>
<li><a href="http://www.commandlinefu.com/" target="_blank" rel="nofollow noopener noreferrer">http://www.commandlinefu.com/</a></li>
</ul>
<p>下面是一些脚本集中营，你可以在里面淘到各种牛X的脚本：</p>
<ul>
<li><a href="http://www.shelldorado.com/scripts/" target="_blank" rel="nofollow noopener noreferrer">http://www.shelldorado.com/scripts/</a></li>
<li><a href="https://snippets.siftie.com/public/tag/bash/" target="_blank" rel="nofollow noopener noreferrer">https://snippets.siftie.com/public/tag/bash/</a></li>
<li><a href="https://bash.cyberciti.biz/" target="_blank" rel="nofollow noopener noreferrer">https://bash.cyberciti.biz/</a></li>
<li><a href="https://github.com/alexanderepstein/Bash-Snippets" target="_blank" rel="noopener noreferrer">https://github.com/alexanderepstein/Bash-Snippets</a></li>
<li><a href="https://github.com/miguelgfierro/scripts" target="_blank" rel="noopener noreferrer">https://github.com/miguelgfierro/scripts</a></li>
<li><a href="https://github.com/epety/100-shell-script-examples" target="_blank" rel="noopener noreferrer">https://github.com/epety/100-shell-script-examples</a></li>
<li><a href="https://github.com/ruanyf/simple-bash-scripts" target="_blank" rel="noopener noreferrer">https://github.com/ruanyf/simple-bash-scripts</a></li>
</ul>
<p>甚至写脚本都可以使用框架:</p>
<ul>
<li>写bash脚本的框架 <a href="https://github.com/Bash-it/bash-it" target="_blank" rel="noopener noreferrer">https://github.com/Bash-it/bash-it</a></li>
</ul>
<p>Google的Shell脚本的代码规范：</p>
<ul>
<li><a href="https://google.github.io/styleguide/shell.xml" target="_blank" rel="nofollow noopener noreferrer">https://google.github.io/styleguide/shell.xml</a></li>
</ul>
<p>最后，别忘了几个和shell有关的索引资源：</p>
<ul>
<li><a href="https://github.com/alebcay/awesome-shell" target="_blank" rel="noopener noreferrer">https://github.com/alebcay/awesome-shell</a></li>
<li><a href="https://github.com/awesome-lists/awesome-bash" target="_blank" rel="noopener noreferrer">https://github.com/awesome-lists/awesome-bash</a></li>
<li><a href="https://terminalsare.sexy/" target="_blank" rel="nofollow noopener noreferrer">https://terminalsare.sexy/</a></li>
</ul>
<p>最后，如果你还有什么别的更好的玩的东西，欢迎在评论区留言，或是到 <a href="https://github.com/coolshellx/articles" target="_blank" rel="noopener noreferrer">coolshellx/ariticles @ github</a> 修改本文。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/8088.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/7.jpg" alt="对技术的态度" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8088.html" class="wp_rp_title">对技术的态度</a></li><li ><a href="https://coolshell.cn/articles/4990.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2011/07/programmer-150x150.png" alt="程序员技术练级攻略" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4990.html" class="wp_rp_title">程序员技术练级攻略</a></li><li ><a href="https://coolshell.cn/articles/4102.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/25.jpg" alt="如何学好C语言" width="150" height="150" /></a><a href="https://coolshell.cn/articles/4102.html" class="wp_rp_title">如何学好C语言</a></li><li ><a href="https://coolshell.cn/articles/22298.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/10/communication-150x150.png" alt="聊聊团队协同和协同工具" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22298.html" class="wp_rp_title">聊聊团队协同和协同工具</a></li><li ><a href="https://coolshell.cn/articles/22173.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/02/http_method-150x150.png" alt="“一把梭：REST API 全用 POST”" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22173.html" class="wp_rp_title">“一把梭：REST API 全用 POST”</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/19219.html">打造高效的工作环境 – Shell 篇</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/19219.html/feed</wfw:commentRss>
			<slash:comments>46</slash:comments>
		
		
			</item>
		<item>
		<title>记一次Kubernetes/Docker网络排障</title>
		<link>https://coolshell.cn/articles/18654.html</link>
					<comments>https://coolshell.cn/articles/18654.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 08 Dec 2018 03:57:35 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Kubernetes]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[network]]></category>
		<category><![CDATA[Systemd]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=18654</guid>

					<description><![CDATA[<p>昨天周五晚上，临下班的时候，用户给我们报了一个比较怪异的Kubernetes集群下的网络不能正常访问的问题，让我们帮助查看一下，我们从下午5点半左右一直跟进到晚...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/18654.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/18654.html">记一次Kubernetes/Docker网络排障</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-full wp-image-18662" src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1.png" alt="" width="300" height="238" />昨天周五晚上，临下班的时候，用户给我们报了一个比较怪异的Kubernetes集群下的网络不能正常访问的问题，让我们帮助查看一下，我们从下午5点半左右一直跟进到晚上十点左右，在远程不能访问用户机器只能远程遥控用户的情况找到了的问题。这个问题比较有意思，我个人觉得其中的调查用到的的命令以及排障的一些方法可以分享一下，所以写下了这篇文章。</p>
<h4>问题的症状</h4>
<p>用户直接在微信里说，他们发现在Kuberbnetes下的某个pod被重启了几百次甚至上千次，于是开启调查这个pod，发现上面的服务时而能够访问，时而不能访问，也就是有一定概率不能访问，不知道是什么原因。而且并不是所有的pod出问题，而只是特定的一两个pod出了网络访问的问题。用户说这个pod运行着Java程序，为了排除是Java的问题，用户用 <code>docker exec -it</code> 命令直接到容器内启了一个 Python的 SimpleHttpServer来测试发现也是一样的问题。</p>
<p>我们大概知道用户的集群是这样的版本，Kuberbnetes 是1.7，网络用的是flannel的gw模式，Docker版本未知，操作系统CentOS 7.4，直接在物理机上跑docker，物理的配置很高，512GB内存，若干CPU核，上面运行着几百个Docker容器。</p>
<p><span id="more-18654"></span></p>
<h4>问题的排查</h4>
<h5>问题初查</h5>
<p>首先，我们排除了flannel的问题，因为整个集群的网络通信都正常，只有特定的某一两个pod有问题。而用 <code>telnet ip port</code> 的命令手工测试网络连接时有很大的概率出现 <code>connection refused</code> 错误，大约 1/4的概率，而3/4的情况下是可以正常连接的。</p>
<p>当时，我们让用户抓个包看看，然后，用户抓到了有问题的TCP连接是收到了 <code>SYN</code> 后，立即返回了 <code>RST, ACK</code></p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-18655" src="https://coolshell.cn/wp-content/uploads/2018/12/tcpdump.png" alt="" width="700" height="80" /></p>
<p>我问一下用户这两个IP所在的位置，知道了，<code>10.233.14.129</code> 是 <code>docker0</code>，<code>10.233.14.145</code> 是容器内的IP。所以，这基本上可以排除了所有和kubernets或是flannel的问题，这就是本地的Docker上的网络的问题。</p>
<p>对于这样被直接 Reset 的情况，在 <code>telnet</code> 上会显示 <code>connection refused</code> 的错误信息，对于我个人的经验，这种 <code>SYN</code>完直接返回 <code>RST, ACK</code>的情况只会有三种情况：</p>
<ol>
<li> TCP链接不能建立，不能建立连接的原因基本上是标识一条TCP链接的那五元组不能完成，绝大多数情况都是服务端没有相关的端口号。</li>
<li>TCP链接建错误，有可能是因为修改了一些TCP参数，尤其是那些默认是关闭的参数，因为这些参数会导致TCP协议不完整。</li>
<li>有防火墙iptables的设置，其中有 <code>REJECT</code> 规则。</li>
</ol>
<p>因为当时还在开车，在等红灯的时候，我感觉到有点像 NAT 的网络中服务端开启了 <code>tcp_tw_recycle</code> 和 <code>tcp_tw_reuse</code> 的症况（详细参看《<a href="https://coolshell.cn/articles/11564.html" target="_blank" rel="noopener noreferrer">TCP的那些事（上）</a>》），所以，让用户查看了一上TCP参数，发现用户一个TCP的参数都没有改，全是默认的，于是我们排除了TCP参数的问题。</p>
<p>然后，我也不觉得容器内还会设置上iptables，而且如果有那就是100%的问题，不会时好时坏。所以，我怀疑容器内的端口号没有侦听上，但是马上又好了，这可能会是应用的问题。于是我让用户那边看一下，应用的日志，并用 <code>kublet describe</code>看一下运行的情况，并把宿主机的 iptables 看一下。</p>
<p>然而，我们发现并没有任何的问题。这时，<strong>我们失去了所有的调查线索，感觉不能继续下去了……</strong></p>
<h5>重新梳理</h5>
<p>这个时候，回到家，大家吃完饭，和用户通了一个电话，把所有的细节再重新梳理了一遍，这个时候，用户提供了一个比较关键的信息—— “<strong>抓包这个事，在 <code>docker0</code> 上可以抓到，然而到了容器内抓不到容器返回 <code>RST, ACK</code> </strong>” ！然而，根据我的知识，我知道在 <code>docker0</code> 和容器内的 <code>veth</code> 网卡上，中间再也没有什么网络设备了（参看《<a href="https://coolshell.cn/articles/17029.html" target="_blank" rel="noopener noreferrer">Docker基础技术：LINUX NAMESPACE（下）</a>》）!</p>
<p>于是这个事把我们逼到了最后一种情况 —— IP地址冲突了！</p>
<p>Linux下看IP地址冲突还不是一件比较简单事的，而在用户的生产环境下没有办法安装一些其它的命令，所以只能用已有的命令，这个时候，我们发现用户的机器上有 <code>arping</code> 于是我们用这个命令来检测有没有冲突的IP地址。使用了下面的命令：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
$ arping -D -I docker0 -c 2 10.233.14.145
$ echo $?
</pre>
<p>根据文档，<code>-D</code> 参数是检测IP地址冲突模式，如果这个命令的退状态是 <code>0</code> 那么就有冲突。结果返回了 <code>1</code> 。而且，我们用 <code>arping</code> IP的时候，没有发现不同的mac地址。 <strong>这个时候，似乎问题的线索又断了</strong>。</p>
<p>因为客户那边还在处理一些别的事情，所以，我们在时断时续的情况下工作，而还一些工作都需要用户完成，所以，进展有点缓慢，但是也给我们一些时间思考问题。</p>
<h5>柳暗花明</h5>
<p>现在我们知道，IP冲突的可能性是非常大的，但是我们找不出来是和谁的IP冲突了。而且，我们知道只要把这台机器重启一下，问题一定就解决掉了，但是我们觉得这并不是解决问题的方式，因为重启机器可以暂时的解决掉到这个问题，而如果我们不知道这个问题怎么发生的，那么未来这个问题还会再来。而重启线上机器这个成本太高了。</p>
<p>于是，我们的好奇心驱使我们继续调查。我让用户 <code>kubectl delete</code> 其中两个有问题的pod，因为本来就服务不断重启，所以，删掉也没有什么问题。删掉这两个pod后（一个是IP为 <code>10.233.14.145</code> 另一个是 <code>10.233.14.137</code>），我们发现，kubernetes在其它机器上重新启动了这两个服务的新的实例。然而，<strong>在问题机器上，这两个IP地址居然还可以ping得通</strong>。</p>
<p>好了，IP地址冲突的问题可以确认了。因为<code>10.233.14.xxx</code> 这个网段是 docker 的，所以，这个IP地址一定是在这台机器上。所以，我们想看看所有的 network namespace 下的 veth 网卡上的IP。</p>
<p>在这个事上，我们费了点时间，因为对相关的命令也 很熟悉，所以花了点时间Google，以及看相关的man。</p>
<ul>
<li>首先，我们到 <code>/var/run/netns</code>目录下查看系统的network namespace，发现什么也没有。</li>
<li>然后，我们到 <code>/var/run/docker/netns</code> 目录下查看Docker的namespace，发现有好些。</li>
<li>于是，我们用指定位置的方式查看Docker的network namespace里的IP地址</li>
</ul>
<p>这里要动用 <code>nsenter</code> 命令，这个命令可以进入到namespace里执行一些命令。比如</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
$ nsenter --net=/var/run/docker/netns/421bdb2accf1 ifconfig -a
</pre>
<p>上述的命令，到 <code>var/run/docker/netns/421bdb2accf1</code> 这个network namespace里执行了 <code>ifconfig -a</code> 命令。于是我们可以用下面 命令来遍历所有的network namespace。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
$ ls /var/run/docker/netns | xargs -I {} nsenter --net=/var/run/docker/netns/{} ip addr 
</pre>
<p>然后，我们发现了比较诡异的事情。</p>
<ul>
<li><code>10.233.14.145</code> 我们查到了这个IP，说明，docker的namespace下还有这个IP。</li>
<li><code>10.233.14.137</code>，这个IP没有在docker的network namespace下查到。</li>
</ul>
<p>有namespace leaking？于是我上网查了一下，发现了一个docker的bug &#8211; 在docker remove/stop 一个容器的时候，没有清除相应的network namespace，这个问题被报告到了 <a href="https://github.com/moby/moby/issues/31597">Issue#31597</a> 然后被fix在了 <a href="https://github.com/moby/moby/pull/31996">PR#31996</a>，并Merge到了 Docker的 17.05版中。而用户的版本是 17.09，应该包含了这个fix。不应该是这个问题，感觉又走不下去了。</p>
<p>不过， <code>10.233.14.137</code> 这个IP可以ping得通，说明这个IP一定被绑在某个网卡，而且被隐藏到了某个network namespace下。</p>
<p>到这里，要查看所有network namespace，只有最后一条路了，那就是到 <code>/proc/</code> 目录下，把所有的pid下的 <code>/proc/&lt;pid&gt;/ns</code> 目录给穷举出来。好在这里有一个比较方便的命令可以干这个事 ： <code>lsns</code></p>
<p>于是我写下了如下的命令：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
$ lsns -t net | awk ‘{print $4}&#039; | xargs -t -I {} nsenter -t {}&amp;nbsp;-n ip addr | grep -C 4 &quot;10.233.14.137&quot;
</pre>
<p>解释一下。</p>
<ul>
<li><code>lsns -t net</code> 列出所有开了network namespace的进程，其第4列是进程PID</li>
<li>把所有开过network namespace的进程PID拿出来，转给 <code>xargs</code> 命令</li>
<li>由 <code>xargs</code> 命令把这些PID 依次传给 <code>nsenter</code> 命令，
<ul>
<li><code>xargs -t</code> 的意思是会把相关的执行命令打出来，这样我知道是那个PID。</li>
<li><code>xargs -I {}</code>  是声明一个占位符来替换相关的PID</li>
</ul>
</li>
</ul>
<p>最后，我们发现，虽然在 <code>/var/run/docker/netns</code> 下没有找到 <code>10.233.14.137</code> ，但是在 <code>lsns</code> 中找到了三个进程，他们都用了<code>10.233.14.137</code> 这个IP（冲突了这么多），<strong>而且他们的MAC地址全是一样的！</strong>（怪不得arping找不到）。通过<code>ps</code> 命令，可以查到这三个进程，有两个是java的，还有一个是<code>/pause</code> （这个应该是kubernetes的沙盒）。</p>
<p>我们继续乘胜追击，穷追猛打，用<code>pstree</code>命令把整个进程树打出来。发现上述的三个进程的父进程都在多个同样叫 <code>docker-contiane</code> 的进程下！</p>
<p><strong>这明显还是docker的，但是在<code>docker ps</code> 中却找不道相应的容器，什么鬼！快崩溃了……</strong></p>
<p>继续看进程树，发现，这些 <code>docker-contiane</code> 的进程的父进程不在 <code>dockerd</code> 下面，而是在 <code>systemd</code> 这个超级父进程PID 1下，我靠！进而发现了一堆这样的野进程（这种野进程或是僵尸进程对系统是有害的，至少也是会让系统进入亚健康的状态，因为他们还在占着资源）。</p>
<p><code>docker-contiane</code> 应该是 <code>dockerd</code> 的子进程，被挂到了 <code>pid 1</code> 只有一个原因，那就是父进程“飞”掉了，只能找 pid 1 当养父。这说明，这台机器上出现了比较严重的 <code>dockerd</code> 进程退出的问题，而且是非常规的，因为 <code>systemd</code> 之所以要成为 pid 1，其就是要监管所有进程的子子孙孙，居然也没有管理好，说明是个非常规的问题。（注，关于 systemd，请参看《<a href="https://coolshell.cn/articles/17998.html" target="_blank" rel="noopener noreferrer">Linux PID 1 和 Systemd </a>》，关于父子进程的事，请参看《Unix高级环境编程》一书）</p>
<p>接下来就要看看 <code>systemd</code> 为 <code>dockerd</code> 记录的日志了…… （然而日志只有3天的了，这3天<code>dockerd</code>没有任何异常）</p>
<h4>总结</h4>
<p>通过这个调查，可以总结一下，</p>
<p>1） 对于问题调查，需要比较扎实的基础知识，知道问题的成因和范围。</p>
<p>2）如果走不下去了，要重新梳理一下，回头仔细看一下一些蛛丝马迹，认真推敲每一个细节。</p>
<p>3） 各种诊断工具要比较熟悉，这会让你事半功倍。</p>
<p>4）系统维护和做清洁比较类似，需要经常看看系统中是否有一些僵尸进程或是一些垃圾东西，这些东西要及时清理掉。</p>
<p>最后，多说一下，很多人都说，<strong>Docker适合放在物理机内运行，这并不完全对，因为他们只考虑到了性能成本，没有考虑到运维成本，在这样512GB中启动几百个容器的玩法，其实并不好，因为这本质上是个大单体，因为你一理要重启某些关键进程或是机器，你的影响面是巨大的</strong>。</p>
<p>&nbsp;</p>
<p>———————— 更新 2018/12/10 —————————</p>
<h4>问题原因</h4>
<p>这两天在自己的环境下测试了一下，发现，只要是通过 <code>systemctl start/stop docker</code> 这样的命令来启停 Docker， 是可以把所有的进程和资源全部干掉的。这个是没有什么问题的。我唯一能重现用户问题的的操作就是直接 <code>kill -9 &lt;dockerd pid&gt;</code> 但是这个事用户应该不会干。而 Docker 如果有 crash 事件时，Systemd 是可以通过 <code>journalctl -u docker</code> 这样的命令查看相关的系统日志的。</p>
<p>于是，我找用户了解一下他们在Docker在启停时的问题，用户说，<strong>他们的执行 <code>systemctl stop docker</code> 这个命令的时候，发现这个命令不响应了，有可能就直接按了 <code>Ctrl +C</code> 了</strong>！</p>
<p>这个应该就是导致大量的 <code>docker-containe</code> 进程挂到 <code>PID 1</code> 下的原因了。前面说过，用户的一台物理机上运行着上百个容器，所以，那个进程树也是非常庞大的，我想，停服的时候，系统一定是要遍历所有的docker子进程来一个一个发退出信号的，这个过程可能会非常的长。导致操作员以为命令假死，而直接按了 <code>Ctrl + C</code> ，最后导致很多容器进程并没有终止……</p>
<p>&nbsp;</p>
<h4>其它事宜</h4>
<p>有同学问，为什么我在这个文章里写的是 <code>docker-containe</code> 而不是 <code>containd</code> 进程？这是因为被 <code>pstree</code> 给截断了，用 <code>ps</code> 命令可以看全，只是进程名的名字有一个 <code>docker-</code>的前缀。</p>
<p>下面是这两种不同安装包的进程树的差别（其中 <code>sleep</code> 是我用 <code>buybox</code> 镜像启动的）</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
systemd───dockerd─┬─docker-contained─┬─3*[docker-contained-shim─┬─sleep]
                  │                 │                    └─9*[{docker-containe}]]
                  │                 ├─docker-contained-shim─┬─sleep
                  │                 │                 └─10*[{docker-containe}]
                  │                 └─14*[{docker-contained-shim}]
                  └─17*[{dockerd}]
</pre>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
systemd───dockerd─┬─containerd─┬─3*[containerd-shim─┬─sleep]
                  │            │                 └─9*[{containerd-shim}]
                  │            ├─2*[containerd-shim─┬─sleep]
                  │            │                    └─9*[{containerd-shim}]]
                  │            └─11*[{containerd}]
                  └─10*[{dockerd}]

</pre>
<p>顺便说一下，自从 Docker 1.11版以后，Docker进程组模型就改成上面这个样子了.</p>
<ul>
<li><code>dockerd</code> 是 Docker Engine守护进程，直接面向操作用户。<code>dockerd</code> 启动时会启动 <code>containerd</code> 子进程，他们之前通过RPC进行通信。</li>
<li><code>containerd</code> 是<code>dockerd</code>和<code>runc</code>之间的一个中间交流组件。他与 <code>dockerd</code> 的解耦是为了让Docker变得更为的中立，而支持OCI 的标准 。</li>
<li><code>containerd-shim</code>  是用来真正运行的容器的，每启动一个容器都会起一个新的shim进程， 它主要通过指定的三个参数：容器id，boundle目录（containerd的对应某个容器生成的目录，一般位于：<code>/var/run/docker/libcontainerd/containerID</code>）， 和运行命令（默认为 <code>runc</code>）来创建一个容器。</li>
<li><code>docker-proxy</code> 你有可能还会在新版本的Docker中见到这个进程，这个进程是用户级的代理路由。只要你用 <code>ps -elf</code> 这样的命令把其命令行打出来，你就可以看到其就是做端口映射的。如果你不想要这个代理的话，你可以在 <code>dockerd</code> 启动命令行参数上加上：  <code>--userland-proxy=false</code> 这个参数。</li>
</ul>
<p>更多的细节，大家可以自行Google。这里推荐两篇文章：</p>
<ul>
<li><a href="https://hackernoon.com/docker-containerd-standalone-runtimes-heres-what-you-should-know-b834ef155426" target="_blank" rel="noopener noreferrer">Docker, Containerd &amp; Standalone Runtimes — Here’s What You Should Know</a></li>
<li><a href="http://alexander.holbreich.org/docker-components-explained/" target="_blank" rel="noopener noreferrer">Docker components explained</a></li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/systemd-1-150x150.jpeg" alt="Linux PID 1 和 Systemd" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_title">Linux PID 1 和 Systemd</a></li><li ><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin-150x150.jpg" alt="Docker基础技术：DeviceMapper" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_title">Docker基础技术：DeviceMapper</a></li><li ><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-150x150.png" alt="Docker基础技术：AUFS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_title">Docker基础技术：AUFS</a></li><li ><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/filter-150x150.png" alt="Docker基础技术：Linux CGroup" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_title">Docker基础技术：Linux CGroup</a></li><li ><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/isolation-150x150.jpg" alt="Docker基础技术：Linux Namespace（上）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_title">Docker基础技术：Linux Namespace（上）</a></li><li ><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/jail_cell-150x150.jpg" alt="Docker基础技术：Linux Namespace（下）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_title">Docker基础技术：Linux Namespace（下）</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/18654.html">记一次Kubernetes/Docker网络排障</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/18654.html/feed</wfw:commentRss>
			<slash:comments>51</slash:comments>
		
		
			</item>
		<item>
		<title>Linux PID 1 和 Systemd</title>
		<link>https://coolshell.cn/articles/17998.html</link>
					<comments>https://coolshell.cn/articles/17998.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sun, 16 Jul 2017 13:40:55 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Systemd]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[Upstart]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17998</guid>

					<description><![CDATA[<p>要说清 Systemd，得先从Linux操作系统的启动说起。Linux 操作系统的启动首先从 BIOS 开始，然后由 Boot Loader 载入内核，并初始化...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17998.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17998.html">Linux PID 1 和 Systemd</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-full" src="https://coolshell.cn/wp-content/uploads/2017/07/systemd.jpeg" alt="" width="275" height="183" />要说清 Systemd，得先从Linux操作系统的启动说起。Linux 操作系统的启动首先从 BIOS 开始，然后由 Boot Loader 载入内核，并初始化内核。内核初始化的最后一步就是启动 init 进程。这个进程是系统的第一个进程，PID 为 1，又叫超级进程，也叫根进程。它负责产生其他所有用户进程。所有的进程都会被挂在这个进程下，如果这个进程退出了，那么所有的进程都被 kill 。如果一个子进程的父进程退了，那么这个子进程会被挂到 PID 1 下面。（注：PID 0 是内核的一部分，主要用于内进换页，参看：<a href="http://en.wikipedia.org/wiki/Process_identifier" target="_blank" rel="noopener noreferrer">Process identifier</a>）</p>
<h4>SysV Init</h4>
<p>PID 1 这个进程非常特殊，其主要就任务是把整个操作系统带入可操作的状态。比如：启动 UI &#8211; Shell 以便进行人机交互，或者进入 X 图形窗口。传统上，PID 1 和传统的 Unix System V 相兼容的，所以也叫 <code>sysvinit</code>，这是使用得最悠久的 init 实现。Unix System V 于1983年 release。</p>
<p>在 <code>sysvint</code> 下，有好几个运行模式，又叫 <code>runlevel</code>。比如：常见的 3 级别指定启动到多用户的字符命令行界面，5 级别指定启起到图形界面，0 表示关机，6 表示重启。其配置在 <code>/etc/inittab</code> 文件中。</p>
<p><span id="more-17998"></span></p>
<p>与此配套的还有 <code>/etc/init.d/</code> 和 <code>/etc/rc[X].d</code>，前者存放各种进程的启停脚本（需要按照规范支持 <code>start</code>，<code>stop</code>子命令），后者的 X 表示不同的 runlevel 下相应的后台进程服务，如：<code>/etc/rc3.d</code> 是 runlevel=3 的。 里面的文件主要是 link 到  <code>/etc/init.d/</code> 里的启停脚本。其中也有一定的命名规范：S 或 K 打头的，后面跟一个数字，然后再跟一个自定义的名字，如：<code>S01rsyslog</code>，<code>S02ssh</code>。S 表示启动，K表示停止，数字表示执行的顺序。</p>
<h4>UpStart</h4>
<p>Unix 和 Linux 在 <code>sysvint</code> 运作多年后，大约到了2006年的时候，Linux内核进入2.6时代，Linux有了很多更新。并且，Linux开始进入桌面系统，而桌面系统和服务器系统不一样的是，桌面系统面临频繁重启，而且，用户会非常频繁的使用硬件的热插拔技术。于是，这些新的场景，让 <code>sysvint</code> 受到了很多挑战。</p>
<p>比如，打印机需要CUPS等服务进程，但是如果用户没有打机印，启动这个服务完全是一种浪费，而如果不启动，如果要用打印机了，就无法使用，因为<code>sysvint</code> 没有自动检测的机制，它只能一次性启动所有的服务。另外，还有网络盘挂载的问题。在 <code>/etc/fstab</code> 中，负责硬盘挂载，有时候还有网络硬盘（NFS 或 iSCSI）在其中，但是在桌面机上，有很可能开机的时候是没有网络的， 于是网络硬盘都不可以访问，也无法挂载，这会极大的影响启动速度。<code>sysvinit</code> 采用 <code>netdev</code> 的方式来解决这个问题，也就是说，需要用户自己在 <code>/etc/fstab</code> 中给相应的硬盘配置上 <code>netdev</code> 属性，于是 <code>sysvint</code> 启动时不会挂载它，只有在网络可用后，由专门的 <code>netfs</code> 服务进程来挂载。这种管理方式比较难以管理，也很容易让人掉坑。</p>
<p>所以，Ubuntu 开发人员在评估了当时几个可选的 init 系统后，决定重新设计这个系统，于是，这就是我们后面看到的 <code>upstart</code> 。 <code>upstart</code> 基于事件驱动的机制，把之前的完全串行的同步启动服务的方式改成了由事件驱动的异步的方式。比如：如果有U盘插入，<code>udev</code> 得到通知，<code>upstart</code> 感知到这个事件后触发相应的服务程序，比如挂载文件系统等等。因为使用一个事件驱动的玩法，所以，启动操作系统时，很多不必要的服务可以不用启动，而是等待通知，lazy 启动。而且事件驱动的好处是，可以并行启动服务，他们之间的依赖关系，由相应的事件通知完成。</p>
<p>upstart 有着很不错的设计，其中最重要的两个概念是 Job 和 Event。</p>
<p><strong>Job</strong> 有一般的Job，也有service的Job，并且，<code>upstart</code> 管理了整个 Job 的生命周期，比如：Waiting, Starting, pre-Start, Spawned, post-Start, Running, pre-Stop, Stopping, Killed, post-Stop等等，并维护着这个生命周期的状态机。</p>
<p><strong>Event</strong> 分成三类，<code>signal</code>, <code>method</code> 和 <code>hooks</code>。<code>signal</code> 就是异步消息，<code>method</code> 是同步阻塞的。<code>hooks</code> 也是同步的，但介于前面两者之间，发出hook事件的进程必须等到事件完成，但不检查是否成功。</p>
<p>但是，<code>upstart</code> 的事件非常复杂，也非常纷乱，各种各样的事件（事件没有归好类）导致有点凌乱。不过因为整个事件驱动的设计比之前的 <code>sysvinit</code> 来说好太多，所以，也深得欢迎。</p>
<h4>Systemd</h4>
<p>直到2010的有一天，一个在 RedHat工作的工程师 <a title="Lennart Poettering" href="https://en.wikipedia.org/wiki/Lennart_Poettering" target="_blank" rel="noopener noreferrer">Lennart Poettering</a> 和 <a title="Kay Sievers" href="https://en.wikipedia.org/wiki/Kay_Sievers">Kay Sievers</a> ，开始引入了一个新的 <code>init</code> 系统—— <code>systemd</code>。这是一个非常非常有野心的项目，这个项目几乎改变了所有的东西，<code>systemd</code> 不但想取代已有的 init 系统，而且还想干更多的东西。</p>
<p>Lennart 同意 <code>upstart</code> 干的不错，代码质量很好，基于事件的设计也很好。但是他觉得 <code>upstart</code> 也有问题，其中最大的问题还是不够快，虽然 <code>upstart</code> 用事件可以达到一定的启动并行度，但是，本质上来说，这些事件还是会让启动过程串行在一起。  如：<code>NetworkManager</code> 在等 <code>D-Bus</code> 的启动事件，而 <code>D-Bus</code> 在等 <code>syslog</code> 的启动事件。</p>
<p>Lennart 认为，实现上来说，<code>upstart</code> 其实是在管理一个逻辑上的服务依赖树，但是这个服务依赖树在表现形式上比较简单，你只需要配置——“启动 B好了就启动A”或是“停止了A后就停止B”这样的规则。但是，Lennart 说，这种简单其实是有害的（this simplification is actually detrimental）。他认为，</p>
<ul>
<li>从一个系统管理的角度出来，他一开始会设定好整个系统启动的服务依赖树，但是这个系统管理员要人肉的把这个本来就非常干净的服务依整树给翻译成计算机看的懂的 Event/Action 形式，而且 Event/Action 这种配置方式是运行时的，所以，你需要运行起来才知道是什么样的。</li>
</ul>
<ul>
<li>Event逻辑从头到脚到处都是，这个事件扩大了运维的复杂度，还不如之前的 <code>sysvint</code>。 也就是说，当用户配置了 “启动 <code>D-Bus</code> 后请启动 <code>NetworkManager</code>”， 这个 <code>upstart</code> 可以干，但是反过来，如果，用户启动 <code>NetworkManager</code>，我们应该先去启动他的前置依赖 <code>D-Bus</code>，然而你还要配置相应的反向 Event。本来，我只需要配置一条依赖的，结果现在我要配置很多很多情况下的Event。</li>
</ul>
<ul>
<li>最后，<code>upstart</code> 里的 Event 的并不标准，很混乱，没有良好的定义。比如：既有，进程启动，运行，停止的事件，也有USB设备插入、可用、拔出的事件，还有文件系统设备being mounted、 mounted 和 umounted 的事件，还有AC电源线连接和断开的事件。你会发现，这进程启停的、USB的、文件系统的、电源线的事件，看上去长得很像， 但是没有被标准化抽像出来掉，因为绝大多数的事件都是三元组：start, condition, stop 。这种概念设计模型并没有在 <code>upstart</code> 中出现。因为 <code>upstart</code> 被设计为单一的事件，而忽略了逻辑依赖。</li>
</ul>
<p>当然，如果 <code>systemd</code> 只是解决 <code>upstart</code> 的问题，他就改造 <code>upstart</code> 就好了，但是 Lennart 的野心不只是想干个这样的事，他想干的更多。</p>
<p>首先，<code>systemd</code> 清醒的认识到了 init 进程的首要目标是要让用户快速的进入可以操作OS的环境，所以，这个速度一定要快，越快越好，所以，<code>systemd</code> 的设计理念就是两条：</p>
<ul>
<li>To start <b>less</b>.</li>
<li>And to start <b>more</b> in <i>parallel</i>.</li>
</ul>
<p>也就是说，按需启动，能不启动就不启动，如果要启动，能并行启动就并行启动，包括你们之间有依赖，我也并行启动。按需启动还好理解，那么，有依赖关系的并行启动，它是怎么做到的？这里，<code>systemd</code> 借鉴了 MacOS 的 <code>Launchd</code> 的玩法（在Youtube上有一个分享——<a href="https://www.youtube.com/watch?v=SjrtySM9Dns" target="_blank" rel="noopener noreferrer">Launchd: One Program to Rule them All</a>，在苹果的开源网站上也有相关的设计文档——<a href="https://developer.apple.com/library/content/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/Introduction.html" target="_blank" rel="noopener noreferrer">About Daemons and Services</a>）</p>
<p>要解决这些依赖性，systemd 需要解决好三种底层依赖—— Socket， D-Bus ，文件系统。</p>
<ul>
<li><strong>Socket依赖</strong>。如果服务C依赖于服务S的socket，那么就要先启动S，然后再启动C，因为如果C启动时找不到S的Socket，那么C就会失败。<code>systemd</code> 可以帮你在S还没有启动好的时候，建立一个socket，用来接收所有的C的请求和数据，并缓存之，一旦S全部启动完成，把systemd替换好的这个缓存的数据和Socket描述符替换过去。</li>
</ul>
<p>&nbsp;</p>
<ul>
<li><strong>D-Bus依赖</strong>。<code>D-Bus</code> 全称 Desktop Bus，是一个用来在进程间通信的服务。除了用于用户态进程和内核态进程通信，也用于用户态的进程之前。现在，很多的现在的服务进程都用 <code>D-Bus</code> 而不是Socket来通信。比如：<code>NetworkManager</code> 就是通过 <code>D-Bus</code> 和其它服务进程通讯的，也就是说，如果一个进程需要知道网络的状态，那么就必需要通过 <code>D-Bus</code> 通信。<code>D-Bus</code> 支持 “Bus Activation”的特性。也就是说，A要通过 <code>D-Bus</code> 服务和B通讯，但是B没有启动，那么 <code>D-Bus</code> 可以把B起来，在B启动的过程中，<code>D-Bus</code> 帮你缓存数据。<code>systemd</code> 可以帮你利用好这个特性来并行启动 A 和 B。</li>
</ul>
<p>&nbsp;</p>
<ul>
<li><strong>文件系统依赖</strong>。系统启动过程中，文件系统相关的活动是最耗时的，比如挂载文件系统，对文件系统进行磁盘检查（fsck），磁盘配额检查等都是非常耗时的操作。在等待这些工作完成的同时，系统处于空闲状态。那些想使用文件系统的服务似乎必须等待文件系统初始化完成才可以启动。<code>systemd</code> 参考了 <code>autofs</code> 的设计思路，使得依赖文件系统的服务和文件系统本身初始化两者可以并发工作。<code>autofs</code> 可以监测到某个文件系统挂载点真正被访问到的时候才触发挂载操作，这是通过内核 <code>automounter</code> 模块的支持而实现的。比如一个 <code>open()</code> 系统调用作用在某个文件系统上的时候，而这个文件系统尚未执行挂载，此时 <code>open()</code> 调用被内核挂起等待，等到挂载完成后，控制权返回给 <code>open()</code> 系统调用，并正常打开文件。这个过程和 <code>autofs</code> 是相似的。</li>
</ul>
<p>&nbsp;</p>
<p>下图来自 Lennart 的演讲里的一页PPT，展示了不同 init 系统的启动。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full" src="https://coolshell.cn/wp-content/uploads/2017/07/boot.png" alt="" width="467" height="308" /></p>
<p>除此之外，systemd 还在启动时管理好了一些下面的事。</p>
<p>用C语言取代传统的脚本式的启动。前面说过，<code>sysvint</code> 用 <code>/etc/rcX.d</code> 下的各种脚本启动。然而这些脚本中需要使用 <code>awk</code>, <code>sed</code>, <code>grep</code>, <code>find</code>, <code>xargs</code> 等等这些操作系统的命令，这些命令需要生成进程，生成进程的开销很大，关键是生成完这些进程后，这个进程就干了点屁大的事就退了。换句话说就是，我操作系统干了那么多事为你拉个进程起来，结果你就把个字串转成小写就退了，把我操作系统当什么了？</p>
<p>在正常的一个 <code>sysvinit</code> 的脚本里，可能会有成百上千个这样的命令。所以，慢死。因此，<code>systemd</code> 全面用 C 语言全部取代了。一般来说，<code>sysvinit</code> 下，操作系统启动完成后，用 <code>echo $$</code> 可以看到，pid 被分配到了上千的样子，而 <code>systemd</code> 的系统只是上百。</p>
<p>另外，systemd 是真正一个可以管住服务进程的——可以跟踪上服务进程所fork/exec出来的所有进程。</p>
<ul>
<li>我们知道， 传统 Unix/Linux 的 Daemon 服务进程的最佳实践基本上是这个样子的 （具体过程可参看这篇文章“<a href="http://0pointer.de/public/systemd-man/daemon.html#SysV%20Daemons" target="_blank" rel="noopener noreferrer">SysV Daemon</a>”）——
<ol>
<li>进程启动时，关闭所有的打开的文件描述符（除了标准描述符0,1,2），然后重置所有的信号处理。</li>
<li>调用 <code>fork()</code> 创建子进程，在子进程中 <code>setsid()</code>，然后父进程退出（为了后台执行）</li>
<li>在子进程中，再调用一次 <code>fork()</code>，创建孙子进程，确定没有交互终端。然后子进程退出。</li>
<li>在孙子进程中，把标准输入标准输出标准错误都连到 <code>/dev/null</code> 上，还要创建 pid 文件，日志文件，处理相关信号 ……</li>
<li>最后才是真正开始提供服务。</li>
</ol>
</li>
</ul>
<p>&nbsp;</p>
<ul>
<li>在上面的这个过程中，服务进程除了两次 <code>fork</code> 外还会 <code>fork</code> 出很多很多的子进程（比如说一些Web服务进程，会根据用户的请求链接来 <code>fork</code> 子进程），这个进程树是相当难以管理的，因为，一旦父进程退出来了，子进程就会被挂到 PID 1下，所以，基本上来说，你无法通过服务进程自已给定的一个pid文件来找到所有的相关进程（这个对开发者的要求太高了），所以，在传统的方式下用脚本启停服务是相当相当的 Buggy 的，因为无法做对所有的服务生出来的子子孙孙做到监控。</li>
</ul>
<p>&nbsp;</p>
<ul>
<li>为了解决这个问题，<code>upstart</code> 通过变态的 <code>strace</code> 来跟踪进程中的 <code>fork()</code> 和 <code>exec()</code> 或 <code>exit()</code> 等相关的系统调用。这种方法相当笨拙。 <code>systemd</code> 使用了一个非常有意思的玩法来 tracking 服务进程生出来的所有进程，那就是用 <code>cgroup</code> （我在 <a href="https://coolshell.cn/articles/17049.html" target="_blank" rel="noopener noreferrer">Docker 的基础技术“cgroup篇”</a>中讲过这个东西）。cgroup主要是用来管理进程组资源配额的事，所以，无论服务如何启动新的子进程，所有的这些相关进程都会同属于一个 <code>cgroup</code>，所以，<code>systemd</code> 只需要简单的去遍历一下相应的 <code>cgroup</code> 的那个虚文件系统目录下的文件，就可以正确的找到所有的相关进程，并将他们一一停止。</li>
</ul>
<p>&nbsp;</p>
<p>另外，<code>systemd</code> 简化了整个 daemon 开发的过程：</p>
<ul>
<li>不需要两次 <code>fork()</code>，只需要实现服务本身的主逻辑就可以了。</li>
<li>不需要 <code>setsid()</code>，<code>systemd</code> 会帮你干</li>
<li>不需要维护 <code>pid文件</code>，<code>systemd</code> 会帮处理。</li>
<li>不需要管理日志文件或是使用<code>syslog</code>，或是处理<code>HUP</code>的日志reload信号。把日志打到 <code>stderr</code> 上，<code>systemd</code> 帮你管理。</li>
<li>处理 <code>SIGTERM</code> 信号，这个信号就是正确退出当前服务，不要做其他的事。</li>
<li>……</li>
</ul>
<p>除此之外，<code>systemd</code> 还能——</p>
<ul>
<li>自动检测启动的服务间有没有环形依赖。</li>
<li>内建 autofs 自动挂载管理功能。</li>
<li>日志服务。<code>systemd</code> 改造了传统的 syslog 的问题，采用二进制格式保存日志，日志索引更快。</li>
<li>快照和恢复。对当前的系统运行的服务集合做快照，并可以恢复。</li>
<li>……</li>
</ul>
<p>还有好多好多，他接管很多很多东西，于是就让很多人不爽了，因为他在干了很多本不属于 PID 1 的事。</p>
<h4>Systemd 争论和八卦</h4>
<p>于是 <code>systemd</code> 这个东西成了可能是有史以来口水战最多的一个开源软件了。<code>systemd</code> 饱受各种争议，最大的争议就是他破坏了 Unix 的设计哲学（相关的哲学可以读一下《<a href="https://book.douban.com/subject/1467587/" target="_blank" rel="noopener noreferrer">Unix编程艺术</a>》），干了一个大而全而且相当复杂的东西。当然，Lennart 并不同意这样的说法，他后来又写一篇blog “<a href="http://0pointer.de/blog/projects/the-biggest-myths.html" target="_blank" rel="noopener noreferrer">The Biggest Myths</a>”来解释 <code>systemd</code> 并不是这样的，大家可以前往一读。</p>
<p>这个争议大到什么样子呢？2014 年，Debian Linux 因为想准备使用 <code>systemd</code> 来作为标准的 init 守护进程来替换 <code>sysvinit</code> 。而围绕这个事的争论达到了空前的热度，争论中充满着仇恨，<code>systemd</code> 的支持者和反对者都在互相辱骂，导致当时 Debian 阵营开始分裂。还有人给 Lennart 发了死亡威胁的邮件，用比特币雇凶买杀手，扬言要取他的性命，在Youbute上传了侮辱他的歌曲，在IRC和各种社交渠道上给他发下流和侮辱性的消息。这已经不是争议了，而是一种不折不扣的仇恨！</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full" src="https://coolshell.cn/wp-content/uploads/2017/07/systemd_shewantsit.jpg" alt="" width="1000" height="421" /></p>
<p>于是，Lennart 在 <a href="https://plus.google.com/+LennartPoetteringTheOneAndOnly/posts/J2TZrTvu7vd" target="_blank" rel="noopener noreferrer">Google Plus 上发了贴子</a>，批评整个 Linux 开源社区和 Linus 本人。他大意说，</p>
<blockquote><p>这个社区太病态了，全是 ass holes，你们不停用各种手段在各种地方用不同的语言和方式来侮辱和漫骂我。我还是一个年轻人，我从来没有经历过这样的场面，但是今天我已经对这种场面很熟悉了。我有时候说话可能不准确，但是我不会像他样那样说出那样的话，我也没有被这些事影响，因为我脸皮够厚，所以，为什么我可以在如何大的反对声面前让 <code>systemd</code> 成功，但是，你们 Linux 社区太可怕了。你们里面的有精神病的人太多了。另外，对于Linus Torvalds，你是这个社区的 Role Model，但可惜你是一个 Bad Role Model，你在社区里的刻薄和侮辱性的言行，基本从一定程度上鼓励了其它人跟你一样，当然，并不只是你一个人的问题，而是在你周围聚集了一群和你一样的这样干的人。送你一句话—— A fish rots from the head down ！一条鱼是从头往下腐烂的……</p></blockquote>
<p>这篇契文很长，喜欢八卦的同学可以前往一读。感受一下 Lennart 当时的心态（我觉得能算上是非常平稳了）。</p>
<p>Linus也在被一媒体问起 <code>systemd</code> 这个事来（参看“<a href="https://www.itwire.com/business-it-news/open-source/65402-torvalds-says-he-has-no-strong-opinions-on-systemd" target="_blank" rel="noopener noreferrer">Torvalds says he has no strong opinions on systemd</a>”），Linus在采访里说，</p>
<blockquote><p>我对 <code>systemd</code> 和 Lennart 的贴子没有什么强烈的想法。虽然，传统的 Unix 设计哲学—— “Do one thing and Do it well”，很不错，而且我们大多数人也实践了这么多年，但是这并不代表所有的真实世界。在历史上，也不只有<code>systemd</code> 这么干过。但是，我个人还是 old-fashioned 的人，至少我喜欢文本式的日志，而不是二进制的日志。但是 <code>systemd</code> 没有必要一定要有这样的品味。哦，我说细节了……</p></blockquote>
<p>今天，<code>systemd</code> 占据了几乎所有的主流的 Linux 发行版的默认配置，包括：Arch Linux、CentOS、CoreOS、Debian、Fedora、Megeia、OpenSUSE、RHEL、SUSE企业版和 Ubuntu。而且，对于 CentOS, CoreOS, Fedora, RHEL, SUSE这些发行版来说，不能没有 <code>systemd</code>。（Ubuntu 还有一个不错的wiki &#8211; <a href="https://wiki.ubuntu.com/SystemdForUpstartUsers" target="_blank" rel="noopener noreferrer">Systemd for Upstart Users</a> 阐述了如何在两者间切换）</p>
<p>&nbsp;</p>
<h4>其它</h4>
<p>还记得在《<a href="https://coolshell.cn/articles/17416.html" target="_blank" rel="noopener noreferrer">缓存更新的套路</a>》一文中，我说过，<strong>如果你要做好架构，首先你得把计算机体系结构以及很多老古董的基础技术吃透了</strong>。因为里面会有很多可以借鉴和相通的东西。那么，你是否从这篇文章里看到了一些有分布式架构相似的东西？</p>
<p>比如：从 <code>sysvinit</code> 到 <code>upstart</code> 再到 <code>systemd</code>，像不像是服务治理？Linux系统下的这些服务进程，是不是很像分布式架构中的微服务？还有那个D-Bus，是不是很像SOA里的ESB？而 init 系统是不是很像一个控制系统？甚至像一个服务编排（Service Orchestration）系统？</p>
<p>分布式系统中的服务之间也有很多依赖，所以，在启动一个架构的时候，如果我们可以做到像 systemd 那样并行启动的话，那么是不是就像是一个微服务的玩法了？</p>
<p>嗯，你会发现，技术上的很多东西是相通的，也是互相有对方的影子，所以，其实技术并不多。关键是我们学在了表面还是看到了本质。</p>
<p>&nbsp;</p>
<h4>延伸阅读</h4>
<ul>
<li>Lennert 的博文：<a href="http://0pointer.de/blog/projects/systemd.html" target="_blank" rel="noopener noreferrer">Rethinking PID 1</a></li>
<li>Lennert 的演讲：<a href="https://www.youtube.com/watch?v=TyMLi8QF6sw" target="_blank" rel="noopener noreferrer">systemd, beyond init</a> （ <a href="http://www.linux-kongress.org/2010/slides/systemd-poettering.pdf" target="_blank" rel="noopener noreferrer">PPT</a> ）</li>
<li><a href="https://en.wikipedia.org/wiki/Systemd" target="_blank" rel="noopener noreferrer">Wikipedia：Systemd</a></li>
<li>LinuxVoice：<a href="https://www.linuxvoice.com/interview-lennart-poettering/" target="_blank" rel="noopener noreferrer">Lennart Poettering 专访</a></li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="http://coolshell.cn/articles/12103.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-150x150.gif" alt="vfork 挂掉的一个问题" width="150" height="150" /></a><a href="http://coolshell.cn/articles/12103.html" class="wp_rp_title">vfork 挂掉的一个问题</a></li><li ><a href="http://coolshell.cn/articles/17061.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-150x150.png" alt="Docker基础技术：AUFS" width="150" height="150" /></a><a href="http://coolshell.cn/articles/17061.html" class="wp_rp_title">Docker基础技术：AUFS</a></li><li ><a href="http://coolshell.cn/articles/17010.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/isolation-150x150.jpg" alt="Docker基础技术：Linux Namespace（上）" width="150" height="150" /></a><a href="http://coolshell.cn/articles/17010.html" class="wp_rp_title">Docker基础技术：Linux Namespace（上）</a></li><li ><a href="http://coolshell.cn/articles/2322.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2010/04/o_unixrichiethompson-150x150.jpg" alt="Unix传奇(上篇)" width="150" height="150" /></a><a href="http://coolshell.cn/articles/2322.html" class="wp_rp_title">Unix传奇(上篇)</a></li><li ><a href="http://coolshell.cn/articles/17029.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/jail_cell-150x150.jpg" alt="Docker基础技术：Linux Namespace（下）" width="150" height="150" /></a><a href="http://coolshell.cn/articles/17029.html" class="wp_rp_title">Docker基础技术：Linux Namespace（下）</a></li><li ><a href="http://coolshell.cn/articles/17049.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/filter-150x150.png" alt="Docker基础技术：Linux CGroup" width="150" height="150" /></a><a href="http://coolshell.cn/articles/17049.html" class="wp_rp_title">Docker基础技术：Linux CGroup</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17998.html">Linux PID 1 和 Systemd</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17998.html/feed</wfw:commentRss>
			<slash:comments>49</slash:comments>
		
		
			</item>
		<item>
		<title>缓存更新的套路</title>
		<link>https://coolshell.cn/articles/17416.html</link>
					<comments>https://coolshell.cn/articles/17416.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Wed, 27 Jul 2016 08:25:28 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[cache]]></category>
		<category><![CDATA[Design]]></category>
		<category><![CDATA[design pattern]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17416</guid>

					<description><![CDATA[<p>看到好些人在写更新缓存数据代码时，先删除缓存，然后再更新数据库，而后续的操作会把数据再装载的缓存中。然而，这个是逻辑是错误的。试想，两个并发操作，一个是更新操作...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17416.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17416.html">缓存更新的套路</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-17422" src="https://coolshell.cn/wp-content/uploads/2016/07/cache-300x158.png" alt="cache" width="300" height="158" srcset="https://coolshell.cn/wp-content/uploads/2016/07/cache-300x158.png 300w, https://coolshell.cn/wp-content/uploads/2016/07/cache-514x270.png 514w, https://coolshell.cn/wp-content/uploads/2016/07/cache.png 600w" sizes="(max-width: 300px) 100vw, 300px" />看到好些人在写更新缓存数据代码时，<strong>先删除缓存，然后再更新数据库</strong>，而后续的操作会把数据再装载的缓存中。<strong>然而，这个是逻辑是错误的</strong>。试想，两个并发操作，一个是更新操作，另一个是查询操作，更新操作删除缓存后，查询操作没有命中缓存，先把老数据读出来后放到缓存中，然后更新操作更新了数据库。于是，在缓存中的数据还是老的数据，导致缓存中的数据是脏的，而且还一直这样脏下去了。</p>
<p>我不知道为什么这么多人用的都是这个逻辑，当我在微博上发了这个贴以后，我发现好些人给了好多非常复杂和诡异的方案，所以，我想写这篇文章说一下几个缓存更新的Design Pattern（让我们多一些套路吧）。</p>
<p>这里，我们先不讨论更新缓存和更新数据这两个事是一个事务的事，或是会有失败的可能，我们先假设更新数据库和更新缓存都可以成功的情况（我们先把成功的代码逻辑先写对）。</p>
<p>更新缓存的的Design Pattern有四种：Cache aside, Read through, Write through, Write behind caching，我们下面一一来看一下这四种Pattern。</p>
<p><span id="more-17416"></span></p>
<h4>Cache Aside Pattern</h4>
<p>这是最常用最常用的pattern了。其具体逻辑如下：</p>
<ul>
<li><strong>失效</strong>：应用程序先从cache取数据，没有得到，则从数据库中取数据，成功后，放到缓存中。</li>
</ul>
<ul>
<li><strong>命中</strong>：应用程序从cache中取数据，取到后返回。</li>
</ul>
<ul>
<li><strong>更新</strong>：先把数据存到数据库中，成功后，再让缓存失效。</li>
</ul>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-17438 size-full" src="https://coolshell.cn/wp-content/uploads/2016/07/Cache-Aside-Design-Pattern-Flow-Diagram-e1470471723210.png" alt="Cache-Aside-Design-Pattern-Flow-Diagram" width="600" height="188" /></p>
<p><img decoding="async" loading="lazy" class="aligncenter wp-image-17437 size-full" src="https://coolshell.cn/wp-content/uploads/2016/07/Updating-Data-using-the-Cache-Aside-Pattern-Flow-Diagram-1-e1470471761402.png" alt="Updating-Data-using-the-Cache-Aside-Pattern-Flow-Diagram-1" width="600" height="186" /></p>
<p>注意，我们的更新是先更新数据库，成功后，让缓存失效。那么，这种方式是否可以没有文章前面提到过的那个问题呢？我们可以脑补一下。</p>
<p>一个是查询操作，一个是更新操作的并发，首先，没有了删除cache数据的操作了，而是先更新了数据库中的数据，此时，缓存依然有效，所以，并发的查询操作拿的是没有更新的数据，但是，更新操作马上让缓存的失效了，后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题，后续的查询操作一直都在取老的数据。</p>
<p>这是标准的design pattern，包括Facebook的论文《<a href="https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf" target="_blank">Scaling Memcache at Facebook</a>》也使用了这个策略。为什么不是写完数据库后更新缓存？你可以看一下Quora上的这个问答《<a href="https://www.quora.com/Why-does-Facebook-use-delete-to-remove-the-key-value-pair-in-Memcached-instead-of-updating-the-Memcached-during-write-request-to-the-backend">Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?</a>》，主要是怕两个并发的写操作导致脏数据。</p>
<p>那么，是不是Cache Aside这个就不会有并发问题了？不是的，比如，一个是读操作，但是没有命中缓存，然后就到数据库中取数据，此时来了一个写操作，写完数据库后，让缓存失效，然后，之前的那个读操作再把老的数据放进去，所以，会造成脏数据。</p>
<p>但，这个case理论上会出现，不过，实际上出现的概率可能非常低，因为这个条件需要发生在读缓存时缓存失效，而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多，而且还要锁表，而读操作必需在写操作前进入数据库操作，而又要晚于写操作更新缓存，所有的这些条件都具备的概率基本并不大。</p>
<p><strong>所以，这也就是Quora上的那个答案里说的，要么通过2PC或是Paxos协议保证一致性，要么就是拼命的降低并发时脏数据的概率，而Facebook使用了这个降低概率的玩法，因为2PC太慢，而Paxos太复杂。当然，最好还是为缓存设置上过期时间。</strong></p>
<h4>Read/Write Through Pattern</h4>
<p>我们可以看到，在上面的Cache Aside套路中，我们的应用代码需要维护两个数据存储，一个是缓存（Cache），一个是数据库（Repository）。所以，应用程序比较啰嗦。而Read/Write Through套路是把更新数据库（Repository）的操作由缓存自己代理了，所以，对于应用层来说，就简单很多了。<strong>可以理解为，应用认为后端就是一个单一的存储，而存储自己维护自己的Cache。</strong></p>
<h5>Read Through</h5>
<p>Read Through 套路就是在查询操作中更新缓存，也就是说，当缓存失效的时候（过期或LRU换出），Cache Aside是由调用方负责把数据加载入缓存，而Read Through则用缓存服务自己来加载，从而对应用方是透明的。</p>
<h5>Write Through</h5>
<p>Write Through 套路和Read Through相仿，不过是在更新数据时发生。当有数据更新的时候，如果没有命中缓存，直接更新数据库，然后返回。如果命中了缓存，则更新缓存，然后再由Cache自己更新数据库（这是一个同步操作）</p>
<p>下图自来Wikipedia的<a href="https://en.wikipedia.org/wiki/Cache_(computing)">Cache词条</a>。其中的Memory你可以理解为就是我们例子里的数据库。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17417" src="https://coolshell.cn/wp-content/uploads/2016/07/460px-Write-through_with_no-write-allocation.svg_.png" alt="Write-through_with_no-write-allocation" width="460" height="620" srcset="https://coolshell.cn/wp-content/uploads/2016/07/460px-Write-through_with_no-write-allocation.svg_.png 460w, https://coolshell.cn/wp-content/uploads/2016/07/460px-Write-through_with_no-write-allocation.svg_-223x300.png 223w" sizes="(max-width: 460px) 100vw, 460px" /></p>
<h4>Write Behind Caching Pattern</h4>
<p>Write Behind 又叫 Write Back。<strong>一些了解Linux操作系统内核的同学对write back应该非常熟悉，这不就是Linux文件系统的Page Cache的算法吗？是的，你看基础这玩意全都是相通的。</strong>所以，基础很重要，我已经不是一次说过基础很重要这事了。</p>
<p>Write Back套路，一句说就是，在更新数据的时候，只更新缓存，不更新数据库，而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比（因为直接操作内存嘛 ），因为异步，write backg还可以合并对同一个数据的多次操作，所以性能的提高是相当可观的。</p>
<p>但是，其带来的问题是，数据不是强一致性的，而且可能会丢失（我们知道Unix/Linux非正常关机会导致数据丢失，就是因为这个事）。在软件设计上，我们基本上不可能做出一个没有缺陷的设计，就像算法设计中的时间换空间，空间换时间一个道理，有时候，强一致性和高性能，高可用和高性性是有冲突的。软件设计从来都是取舍Trade-Off。</p>
<p>另外，Write Back实现逻辑比较复杂，因为他需要track有哪数据是被更新了的，需要刷到持久层上。操作系统的write back会在仅当这个cache需要失效的时候，才会被真正持久起来，比如，内存不够了，或是进程退出了等情况，这又叫lazy write。</p>
<p>在wikipedia上有一张write back的流程图，基本逻辑如下：</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17428" src="https://coolshell.cn/wp-content/uploads/2016/07/Write-back_with_write-allocation.png" alt="Write-back_with_write-allocation" width="640" height="820" srcset="https://coolshell.cn/wp-content/uploads/2016/07/Write-back_with_write-allocation.png 640w, https://coolshell.cn/wp-content/uploads/2016/07/Write-back_with_write-allocation-234x300.png 234w" sizes="(max-width: 640px) 100vw, 640px" /></p>
<p>&nbsp;</p>
<h4>再多唠叨一些</h4>
<p>1）上面讲的这些Design Pattern，其实并不是软件架构里的mysql数据库和memcache/redis的更新策略，这些东西都是计算机体系结构里的设计，比如CPU的缓存，硬盘文件系统中的缓存，硬盘上的缓存，数据库中的缓存。<strong>基本上来说，这些缓存更新的设计模式都是非常老古董的，而且历经长时间考验的策略</strong>，所以这也就是，工程学上所谓的Best Practice，遵从就好了。</p>
<p>2）有时候，我们觉得能做宏观的系统架构的人一定是很有经验的，其实，宏观系统架构中的很多设计都来源于这些微观的东西。比如，云计算中的很多虚拟化技术的原理，和传统的虚拟内存不是很像么？Unix下的那些I/O模型，也放大到了架构里的同步异步的模型，还有Unix发明的管道不就是数据流式计算架构吗？TCP的好些设计也用在不同系统间的通讯中，仔细看看这些微观层面，你会发现有很多设计都非常精妙……所以，<strong>请允许我在这里放句观点鲜明的话——如果你要做好架构，首先你得把计算机体系结构以及很多老古董的基础技术吃透了</strong>。</p>
<p>3）在软件开发或设计中，我非常建议在之前先去参考一下已有的设计和思路，<strong>看看相应的guideline，best practice或design pattern，吃透了已有的这些东西，再决定是否要重新发明轮子</strong>。千万不要似是而非地，想当然的做软件设计。</p>
<p>4）上面，我们没有考虑缓存（Cache）和持久层（Repository）的整体事务的问题。比如，更新Cache成功，更新数据库失败了怎么吗？或是反过来。关于这个事，如果你需要强一致性，你需要使用“两阶段提交协议”——prepare, commit/rollback，比如Java 7 的<a href="http://docs.oracle.com/javaee/7/api/javax/transaction/xa/XAResource.html" target="_blank">XAResource</a>，还有MySQL 5.7的 <a href="http://dev.mysql.com/doc/refman/5.7/en/xa.html" target="_blank">XA Transaction</a>，有些cache也支持XA，比如<a href="http://www.ehcache.org/documentation/3.0/xa.html" target="_blank">EhCache</a>。当然，XA这样的强一致性的玩法会导致性能下降，关于分布式的事务的相关话题，你可以看看《<a href="https://coolshell.cn/articles/10910.html" target="_blank">分布式系统的事务处理</a>》一文。</p>
<p>（全文完）</p>
<p>&nbsp;</p>
<p>&nbsp;<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/9949.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/07/inverted-bookshelf_thumb-150x150.jpg" alt="IoC/DIP其实是一种管理思想" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9949.html" class="wp_rp_title">IoC/DIP其实是一种管理思想</a></li><li ><a href="https://coolshell.cn/articles/8961.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/01/kiss-150x150.png" alt="从面向对象的设计模式看软件设计" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8961.html" class="wp_rp_title">从面向对象的设计模式看软件设计</a></li><li ><a href="https://coolshell.cn/articles/7236.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/05/Bannière-Unix-linux-150x150.jpg" alt="用Unix的设计思想来应对多变的需求" width="150" height="150" /></a><a href="https://coolshell.cn/articles/7236.html" class="wp_rp_title">用Unix的设计思想来应对多变的需求</a></li><li ><a href="https://coolshell.cn/articles/6950.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/21.jpg" alt="需求变化与IoC" width="150" height="150" /></a><a href="https://coolshell.cn/articles/6950.html" class="wp_rp_title">需求变化与IoC</a></li><li ><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF-150x150.jpeg" alt="eBPF 介绍" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_title">eBPF 介绍</a></li><li ><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/12/bachelor-mechanical-eng-icon@72x-150x150.png" alt="我做系统架构的一些原则" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_title">我做系统架构的一些原则</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17416.html">缓存更新的套路</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17416.html/feed</wfw:commentRss>
			<slash:comments>186</slash:comments>
		
		
			</item>
		<item>
		<title>Docker基础技术：DeviceMapper</title>
		<link>https://coolshell.cn/articles/17200.html</link>
					<comments>https://coolshell.cn/articles/17200.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Wed, 26 Aug 2015 00:21:09 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[Device Mapper]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Thin Provisioning]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17200</guid>

					<description><![CDATA[<p>在上一篇介绍AUFS的文章中，大家可以看到，Docker的分层镜像是怎么通过UnionFS这种文件系统做到的，但是，因为Docker首选的AUFS并不在Linu...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17200.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17200.html">Docker基础技术：DeviceMapper</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-17217" src="https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin-300x150.jpg" alt="how_to_set_up_an_iSCSI_LUN_with_thin" width="300" height="150" srcset="https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin-300x150.jpg 300w, https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin-540x270.jpg 540w, https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin.jpg 600w" sizes="(max-width: 300px) 100vw, 300px" />在上一篇<a href="https://coolshell.cn/articles/17061.html" target="_blank">介绍AUFS的文章</a>中，大家可以看到，Docker的分层镜像是怎么通过UnionFS这种文件系统做到的，但是，因为Docker首选的AUFS并不在Linux的内核主干里，所以，对于非Ubuntu的Linux分发包，比如CentOS，就无法使用AUFS作为Docker的文件系统了。于是作为第二优先级的DeviceMapper就被拿出来做分层镜像的一个实现。</p>
<h4>Device Mapper 简介</h4>
<p>DeviceMapper自Linux 2.6被引入成为了Linux最重要的一个技术。它在内核中支持逻辑卷管理的通用设备映射机制，它为实现用于存储资源管理的块设备驱动提供了一个高度模块化的内核架构，它包含三个重要的对象概念，Mapped Device、Mapping Table、Target device。</p>
<p>Mapped Device 是一个逻辑抽象，可以理解成为内核向外提供的逻辑设备，它通过Mapping Table描述的映射关系和 Target Device 建立映射。Target device 表示的是 Mapped Device 所映射的物理空间段，对 Mapped Device 所表示的逻辑设备来说，就是该逻辑设备映射到的一个物理设备。</p>
<p>Mapping Table里有 Mapped Device 逻辑的起始地址、范围、和表示在 Target Device 所在物理设备的地址偏移量以及Target 类型等信息（注：这些地址和偏移量都是以磁盘的扇区为单位的，即 512 个字节大小，所以，当你看到128的时候，其实表示的是128*512=64K）。</p>
<p><span id="more-17200"></span></p>
<p>DeviceMapper 中的逻辑设备Mapped Device不但可以映射一个或多个物理设备Target Device，还可以映射另一个Mapped Device，于是，就是构成了一个迭代或递归的情况，就像文件系统中的目录里除了文件还可以有目录，理论上可以无限嵌套下去。</p>
<p>DeviceMapper在内核中通过一个一个模块化的 Target Driver 插件实现对 IO 请求的过滤或者重新定向等工作，当前已经实现的插件包括软 Raid、加密、多路径、镜像、快照等，这体现了在 Linux 内核设计中策略和机制分离的原则。如下图所示。从图中，我们可以<strong>看到DeviceMapper只是一个框架，在这个框架上，我们可以插入各种各样的策略</strong>（让我不自然地想到了面向对象中的策略模式），在这诸多“插件”中，<strong>有一个东西叫Thin Provisioning Snapshot，这是Docker使用DeviceMapper中最重要的模块</strong>。</p>
<figure id="attachment_17204" aria-describedby="caption-attachment-17204" style="width: 640px" class="wp-caption aligncenter"><img decoding="async" loading="lazy" class="size-full wp-image-17204" src="https://coolshell.cn/wp-content/uploads/2015/08/device.mapper.2.gif" alt="图片来源：http://people.redhat.com/agk/talks/FOSDEM_2005/" width="640" height="494" /><figcaption id="caption-attachment-17204" class="wp-caption-text">图片来源：<a href="http://people.redhat.com/agk/talks/FOSDEM_2005/" target="_blank">http://people.redhat.com/agk/talks/FOSDEM_2005/</a></figcaption></figure>
<h4><strong>Thin Provisioning 简介</strong></h4>
<p>Thin Provisioning要怎么翻译成中文，真是一件令人头痛的事，我就不翻译了。这个技术是虚拟化技术中的一种。它是什么意思呢？<strong>你可以联想一下我们计算机中的内存管理中用到的——“虚拟内存技术”</strong>——操作系统给每个进程N多N多用不完的内址地址（32位下，每个进程可以有最多2GB的内存空间），但是呢，我们知道，物理内存是没有那么多的，如果按照进程内存和物理内存一一映射来玩的话，那么，我们得要多少的物理内存啊。所以，操作系统引入了虚拟内存的设计，<strong>意思是，我逻辑上给你无限多的内存，但是实际上是实报实销</strong>，因为我知道你一定用不了那么多，于是，达到了内存使用率提高的效果。（今天云计算中很多所谓的虚拟化其实完全都是在用和“虚拟内存”相似的Thin Provisioning的技术，所谓的超配，或是超卖）</p>
<p>&nbsp;</p>
<p>好了，话题拉回来，我们这里说的是存储。看下面两个图（<a href="http://www.architecting.it/2009/06/04/enterprise-computing-why-thin-provisioning-is-not-the-holy-grail-for-utilisation/" target="_blank">图片来源</a>），第一个是Fat Provisioning，第二个是Thin Provisioning，其很好的说明了是个怎么一回事（和虚拟内存是一个概念）</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17206" src="https://coolshell.cn/wp-content/uploads/2015/08/thin-provisioning-1.jpg" alt="thin-provisioning-1" width="606" height="399" srcset="https://coolshell.cn/wp-content/uploads/2015/08/thin-provisioning-1.jpg 606w, https://coolshell.cn/wp-content/uploads/2015/08/thin-provisioning-1-300x198.jpg 300w" sizes="(max-width: 606px) 100vw, 606px" /> <img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17207" src="https://coolshell.cn/wp-content/uploads/2015/08/thin-provisioning-2.jpg" alt="thin-provisioning-2" width="606" height="389" srcset="https://coolshell.cn/wp-content/uploads/2015/08/thin-provisioning-2.jpg 606w, https://coolshell.cn/wp-content/uploads/2015/08/thin-provisioning-2-300x193.jpg 300w" sizes="(max-width: 606px) 100vw, 606px" /></p>
<p>那么，Docker是怎么使用Thin Provisioning这个技术做到像UnionFS那样的分层镜像的呢？答案是，Docker使用了Thin Provisioning的Snapshot的技术。下面我们来介绍一下Thin Provisioning的Snapshot。</p>
<h4>Thin Provisioning Snapshot 演示</h4>
<p>下面，我们用一系列的命令来演示一下Device Mapper的Thin Provisioning Snapshot是怎么玩的。</p>
<p>首先，我们需要先建两个文件，一个是data.img，一个是meta.data.img：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo dd if=/dev/zero of=/tmp/data.img bs=1K count=1 seek=10M
1+0 records in
1+0 records out
1024 bytes (1.0 kB) copied, 0.000621428 s, 1.6 MB/s

~hchen$ sudo dd if=/dev/zero of=/tmp/meta.data.img bs=1K count=1 seek=1G
1+0 records in
1+0 records out
1024 bytes (1.0 kB) copied, 0.000140858 s, 7.3 MB/s</pre>
<p>注意命令中<code>seek</code>选项，其表示为略过<code>of</code>选项指定的输出文件的前10G个output的bloksize的空间后再写入内容。因为bs是1个字节，所以也就是10G的尺寸，但其实在硬盘上是没有占有空间的，占有空间只有1k的内容。当向其写入内容时，才会在硬盘上为其分配空间。我们可以用ls命令看一下，实际分配了12K和4K。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo ls -lsh /tmp/data.img
12K -rw-r--r--. 1 root root 11G Aug 25 23:01 /tmp/data.img

~hchen$ sudo ls -slh /tmp/meta.data.img
4.0K -rw-r--r--. 1 root root 101M Aug 25 23:17 /tmp/meta.data.img</pre>
<p>然后，我们为这个文件创建一个loopback设备。（loop2015和loop2016是我乱取的两个名字）</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo losetup /dev/loop2015 /tmp/data.img
~hchen$ sudo losetup /dev/loop2016 /tmp/meta.data.img

~hchen$ sudo losetup -a
/dev/loop2015: [64768]:103991768 (/tmp/data.img)
/dev/loop2016: [64768]:103991765 (/tmp/meta.data.img)</pre>
<p>现在，我们为这个设备建一个Thin Provisioning的Pool，用dmsetup命令：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo dmsetup create hchen-thin-pool \
                  --table &quot;0 20971522 thin-pool /dev/loop2016 /dev/loop2015 \
                           128 65536 1 skip_block_zeroing&quot;</pre>
<p>其中的参数解释如下（更多信息可参看<a href="https://github.com/torvalds/linux/blob/master/Documentation/device-mapper/thin-provisioning.txt" target="_blank">Thin Provisioning的man page</a>）：</p>
<ul>
<li>dmsetup create是用来创建thin pool的命令</li>
<li>hchen-thin-pool 是自定义的一个pool名，不冲突就好。</li>
<li>&#8211;table是这个pool的参数设置
<ul>
<li>0代表起的sector位置</li>
<li>20971522代码结句的sector号，前面说过，一个sector是512字节，所以，20971522个正好是10GB</li>
<li>/dev/loop2016是meta文件的设备（前面我们建好了）</li>
<li>/dev/loop2015是data文件的设备（前面我们建好了）</li>
<li>128是最小的可分配的sector数</li>
<li>65536是最少可用sector的water mark，也就是一个threshold</li>
<li>1 代表有一个附加参数</li>
<li>skip_block_zeroing是个附加参数，表示略过用0填充的块</li>
</ul>
</li>
</ul>
<p>然后，我们就可以看到一个Device Mapper的设备了：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo ll /dev/mapper/hchen-thin-pool
lrwxrwxrwx. 1 root root 7 Aug 25 23:24 /dev/mapper/hchen-thin-pool -&gt; ../dm-4</pre>
<p>接下来，我们的初始还没有完成，还要创建一个Thin Provisioning 的 Volume：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo dmsetup message /dev/mapper/hchen-thin-pool 0 &quot;create_thin 0&quot;
~hchen$ sudo dmsetup create hchen-thin-volumn-001 \
            --table &quot;0 2097152 thin /dev/mapper/hchen-thin-pool 0&quot;</pre>
<p>其中：</p>
<ul>
<li>第一个命令中的create_thin是关键字，后面的0表示这个Volume的device 的 id</li>
<li>第二个命令，是真正的为这个Volumn创建一个可以mount的设备，名字叫hchen-thin-volumn-001。2097152只有1GB</li>
</ul>
<p>好了，在mount前，我们还要格式化一下：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo mkfs.ext4 /dev/mapper/hchen-thin-volumn-001
mke2fs 1.42.9 (28-Dec-2013)
Discarding device blocks: done
Filesystem label=
OS type: Linux
Block size=4096 (log=2)
Fragment size=4096 (log=2)
Stride=16 blocks, Stripe width=16 blocks
65536 inodes, 262144 blocks
13107 blocks (5.00%) reserved for the super user
First data block=0
Maximum filesystem blocks=268435456
8 block groups
32768 blocks per group, 32768 fragments per group
8192 inodes per group
Superblock backups stored on blocks:
32768, 98304, 163840, 229376

Allocating group tables: done
Writing inode tables: done
Creating journal (8192 blocks): done
Writing superblocks and filesystem accounting information: done</pre>
<p>好了，我们可以mount了（下面的命令中，我还创建了一个文件）</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo mkdir -p /mnt/base
~hchen$ sudo mount /dev/mapper/hchen-thin-volumn-001 /mnt/base
~hchen$ sudo echo &quot;hello world, I am a base&quot; &gt; /mnt/base/id.txt
~hchen$ sudo cat /mnt/base/id.txt
hello world, I am a base</pre>
<p>好了，接下来，我们来看看snapshot怎么搞：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo dmsetup message /dev/mapper/hchen-thin-pool 0 &quot;create_snap 1 0&quot;
~hchen$ sudo dmsetup create mysnap1 \
                   --table &quot;0 2097152 thin /dev/mapper/hchen-thin-pool 1&quot;

~hchen$ sudo ll /dev/mapper/mysnap1
lrwxrwxrwx. 1 root root 7 Aug 25 23:49 /dev/mapper/mysnap1 -&gt; ../dm-5</pre>
<p>上面的命令中：</p>
<ul>
<li>第一条命令是向hchen-thin-pool发一个create_snap的消息，后面跟两个id，第一个是新的dev id，第二个是要从哪个已有的dev id上做snapshot（0这个dev id是我们前面就创建了了）</li>
</ul>
<ul>
<li>第二条命令是创建一个mysnap1的device，并可以被mount。</li>
</ul>
<p>下面我们来看看：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo mkdir -p /mnt/mysnap1
~hchen$ sudo mount /dev/mapper/mysnap1 /mnt/mysnap1

~hchen$ sudo ll /mnt/mysnap1/
total 20
-rw-r--r--. 1 root root 25 Aug 25 23:46 id.txt
drwx------. 2 root root 16384 Aug 25 23:43 lost+found

~hchen$ sudo cat /mnt/mysnap1/id.txt
hello world, I am a base</pre>
<p>我们来修改一下/mnt/mysnap1/id.txt，并加上一个snap1.txt的文件：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo echo &quot;I am snap1&quot; &gt;&gt; /mnt/mysnap1/id.txt
~hchen$ sudo echo &quot;I am snap1&quot; &gt; /mnt/mysnap1/snap1.txt

~hchen$ sudo cat /mnt/mysnap1/id.txt
hello world, I am a base
I am snap1

~hchen$ sudo cat /mnt/mysnap1/snap1.txt
I am snap1</pre>
<p>我们再看一下/mnt/base，你会发现没有什么变化：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo ls /mnt/base
id.txt      lost+found
~hchen$ sudo cat /mnt/base/id.txt
hello world, I am a base</pre>
<p>你是不是已经看到了分层镜像的样子了？</p>
<p>你还要吧继续在刚才的snapshot上再建一个snapshot</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo dmsetup message /dev/mapper/hchen-thin-pool 0 &quot;create_snap 2 1&quot;
~hchen$ sudo dmsetup create mysnap2 \
                   --table &quot;0 2097152 thin /dev/mapper/hchen-thin-pool 2&quot;

~hchen$ sudo ll /dev/mapper/mysnap2
lrwxrwxrwx. 1 root root 7 Aug 25 23:52 /dev/mapper/mysnap1 -&gt; ../dm-7

~hchen$ sudo mkdir -p /mnt/mysnap2
~hchen$ sudo mount /dev/mapper/mysnap2 /mnt/mysnap2
~hchen$ sudo  ls /mnt/mysnap2
id.txt  lost+found  snap1.txt </pre>
<p>好了，我相信你看到了分层镜像的样子了。</p>
<p>看完演示，我们再来补点理论知识吧：</p>
<ul>
<li>Snapshot来自LVM（Logic Volumn Manager），它可以在不中断服务的情况下为某个device打一个快照。</li>
<li>Snapshot是Copy-On-Write的，也就是说，只有发生了修改，才会对对应的内存进行拷贝。</li>
</ul>
<p>另外，这里有篇文章<a href="http://searchstorage.techtarget.com/tip/Storage-thin-provisioning-benefits-and-challenges" target="_blank">Storage thin provisioning benefits and challenges</a>可以前往一读。</p>
<h4>Docker的DeviceMapper</h4>
<p>上面基本上就是Docker的玩法了，我们可以看一下docker的loopback设备：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen $ sudo losetup -a
/dev/loop0: [64768]:38050288 (/var/lib/docker/devicemapper/devicemapper/data)
/dev/loop1: [64768]:38050289 (/var/lib/docker/devicemapper/devicemapper/metadata)</pre>
<p>其中data 100GB，metadata 2.0GB</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen $ sudo ls -alhs /var/lib/docker/devicemapper/devicemapper
506M -rw-------. 1 root root 100G Sep 10 20:15 data
1.1M -rw-------. 1 root root 2.0G Sep 10 20:15 metadata </pre>
<p>下面是相关的thin-pool。其中，有个当一大串hash串的device是正在启动的容器：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen $ sudo ll /dev/mapper/dock*
lrwxrwxrwx. 1 root root 7 Aug 25 07:57 /dev/mapper/docker-253:0-104108535-pool -&gt; ../dm-2
lrwxrwxrwx. 1 root root 7 Aug 25 11:13 /dev/mapper/docker-253:0-104108535-deefcd630a60aa5ad3e69249f58a68e717324be4258296653406ff062f605edf -&gt; ../dm-3</pre>
<p>我们可以看一下它的device id（Docker都把它们记下来了）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen $ sudo cat /var/lib/docker/devicemapper/metadata/deefcd630a60aa5ad3e69249f58a68e717324be4258296653406ff062f605edf
{&quot;device_id&quot;:24,&quot;size&quot;:10737418240,&quot;transaction_id&quot;:26,&quot;initialized&quot;:false}</pre>
<p>device_id是24，size是10737418240，除以512，就是20971520 个 sector，我们用这些信息来做个snapshot看看（注：我用了一个比较大的dev id &#8211; 1024）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">~hchen$ sudo dmsetup message &quot;/dev/mapper/docker-253:0-104108535-pool&quot; 0 \
                                    &quot;create_snap 1024 24&quot;
~hchen$ sudo dmsetup create dockersnap --table \
                    &quot;0 20971520 thin /dev/mapper/docker-253:0-104108535-pool 1024&quot;
~hchen$ sudo mkdir /mnt/docker
~hchen$ sudo mount /dev/mapper/dockersnap /mnt/docker/
~hchen$ sudo ls /mnt/docker/
id lost+found rootfs
~hchen$ sudo ls /mnt/docker/rootfs/
bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var</pre>
<p>我们在docker的容器里用findmnt命令也可以看到相关的mount的情况（因为太长，下面只是摘要）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"># findmnt
TARGET                SOURCE               
/                 /dev/mapper/docker-253:0-104108535-deefcd630a60[/rootfs]
/etc/resolv.conf  /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/resolv.conf]
/etc/hostname     /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/hostname]
/etc/hosts        /dev/mapper/centos-root[/var/lib/docker/containers/deefcd630a60/hosts]</pre>
<h4>Device Mapper 行不行？</h4>
<p>Thin Provisioning的文档中说，这还处理实验阶段，不要上Production.</p>
<blockquote><p>These targets are very much still in the EXPERIMENTAL state. Please do not yet rely on them in production.</p></blockquote>
<p>另外，Jeff Atwood在Twitter上发过这样的一推</p>
<p><a href="https://twitter.com/codinghorror/status/604096348682485760"><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17214" src="https://coolshell.cn/wp-content/uploads/2015/08/Jeff.Atwood.DeviceMapper.png" alt="Jeff.Atwood.DeviceMapper" width="607" height="311" srcset="https://coolshell.cn/wp-content/uploads/2015/08/Jeff.Atwood.DeviceMapper.png 607w, https://coolshell.cn/wp-content/uploads/2015/08/Jeff.Atwood.DeviceMapper-300x154.png 300w" sizes="(max-width: 607px) 100vw, 607px" /></a></p>
<p>这个推指向的<a href="https://forums.docker.com/t/rmi-not-freeing-disk-space-in-devicemapper-sparse-file-centos-6-6/1640/3" target="_blank">这个讨论</a>中，其中指向了这个<a href="https://github.com/discourse/discourse_docker/commit/48f22d14f39496c8df446cbc65ee04b258c5a1a0" target="_blank">code diff</a>，基本上就是说，DeviceMapper这种东西问题太多了，我们应该把其加入黑名单。Doker的Founder也这样回复到：</p>
<p><a href="https://twitter.com/solomonstre/status/604055267303636992"><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17215" src="https://coolshell.cn/wp-content/uploads/2015/08/Solomon.Hykeys.DeviceMapper.png" alt="" width="620" height="229" srcset="https://coolshell.cn/wp-content/uploads/2015/08/Solomon.Hykeys.DeviceMapper.png 620w, https://coolshell.cn/wp-content/uploads/2015/08/Solomon.Hykeys.DeviceMapper-300x111.png 300w" sizes="(max-width: 620px) 100vw, 620px" /></a></p>
<p>所以，如果你在使用loopback的devicemapper的话，当你的存储出现了问题后，正确的解决方案是：</p>
<p style="text-align: center;">rm -rf /var/lib/docker</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1-150x150.png" alt="记一次Kubernetes/Docker网络排障" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_title">记一次Kubernetes/Docker网络排障</a></li><li ><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-150x150.png" alt="Docker基础技术：AUFS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_title">Docker基础技术：AUFS</a></li><li ><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/filter-150x150.png" alt="Docker基础技术：Linux CGroup" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_title">Docker基础技术：Linux CGroup</a></li><li ><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/isolation-150x150.jpg" alt="Docker基础技术：Linux Namespace（上）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_title">Docker基础技术：Linux Namespace（上）</a></li><li ><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/jail_cell-150x150.jpg" alt="Docker基础技术：Linux Namespace（下）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_title">Docker基础技术：Linux Namespace（下）</a></li><li ><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF-150x150.jpeg" alt="eBPF 介绍" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_title">eBPF 介绍</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17200.html">Docker基础技术：DeviceMapper</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17200.html/feed</wfw:commentRss>
			<slash:comments>25</slash:comments>
		
		
			</item>
		<item>
		<title>Docker基础技术：AUFS</title>
		<link>https://coolshell.cn/articles/17061.html</link>
					<comments>https://coolshell.cn/articles/17061.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Mon, 24 Aug 2015 00:01:13 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[AUFS]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[UnionFS]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17061</guid>

					<description><![CDATA[<p>AUFS是一种Union File System，所谓UnionFS就是把不同物理位置的目录合并mount到同一个目录中。UnionFS的一个最主要的应用是，把...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17061.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17061.html">Docker基础技术：AUFS</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><a href="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw.png"><img decoding="async" loading="lazy" class="alignright size-medium wp-image-17194" src="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-300x225.png" alt="docker-filesystems-busyboxrw" width="300" height="225" srcset="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-300x225.png 300w, https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-768x576.png 768w, https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-360x270.png 360w, https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw.png 800w" sizes="(max-width: 300px) 100vw, 300px" /></a>AUFS是一种Union File System，所谓UnionFS就是把不同物理位置的目录合并mount到同一个目录中。UnionFS的一个最主要的应用是，把一张CD/DVD和一个硬盘目录给联合 mount在一起，然后，你就可以对这个只读的CD/DVD上的文件进行修改（当然，修改的文件存于硬盘上的目录里）。</p>
<p>AUFS又叫Another UnionFS，后来叫Alternative UnionFS，后来可能觉得不够霸气，叫成Advance UnionFS。是个叫Junjiro Okajima（岡島順治郎）在2006年开发的，AUFS完全重写了早期的UnionFS 1.x，其主要目的是为了可靠性和性能，并且引入了一些新的功能，比如可写分支的负载均衡。AUFS在使用上全兼容UnionFS，而且比之前的UnionFS在稳定性和性能上都要好很多，后来的UnionFS 2.x开始抄AUFS中的功能。但是他居然没有进到Linux主干里，就是因为Linus不让，基本上是因为代码量比较多，而且写得烂（相对于只有3000行的union mount和10000行的UnionFS，以及其它平均下来只有6000行代码左右的VFS，AUFS居然有30000行代码），所以，岡島不断地改进代码质量，不断地提交，不断地被Linus拒掉，所以，到今天AUFS都还进不了Linux主干（今天你可以看到AUFS的代码其实还好了，比起OpenSSL好N倍，要么就是Linus对代码的质量要求非常高，要么就是Linus就是不喜欢AUFS）。</p>
<p>不过，好在有很多发行版都用了AUFS，比如：Ubuntu 10.04，Debian6.0, Gentoo Live CD支持AUFS，所以，也OK了。</p>
<p>好了，扯完这些闲话，我们还是看一个示例吧（环境：Ubuntu 14.04）</p>
<p><span id="more-17061"></span></p>
<p>首先，我们建上两个目录（水果和蔬菜），并在这两个目录中放上一些文件，水果中有苹果和蕃茄，蔬菜有胡萝卜和蕃茄。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ tree
.
├── fruits
│   ├── apple
│   └── tomato
└── vegetables
    ├── carrots
    └── tomato

</pre>
<p>然后，我们输入以下命令：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"># 创建一个mount目录
$ mkdir mnt

# 把水果目录和蔬菜目录union mount到 ./mnt目录中
$ sudo mount -t aufs -o dirs=./fruits:./vegetables none ./mnt

#  查看./mnt目录
$ tree ./mnt
./mnt
├── apple
├── carrots
└── tomato</pre>
<p>我们可以看到在./mnt目录下有三个文件，苹果apple、胡萝卜carrots和蕃茄tomato。水果和蔬菜的目录被union到了./mnt目录下了。</p>
<p>我们来修改一下其中的文件内容：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ echo mnt &gt; ./mnt/apple
$ cat ./mnt/apple
mnt
$ cat ./fruits/apple
mnt</pre>
<p>上面的示例，我们可以看到./mnt/apple的内容改了，./fruits/apple的内容也改了。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ echo mnt_carrots &gt; ./mnt/carrots
$ cat ./vegetables/carrots 

$ cat ./fruits/carrots
mnt_carrots
</pre>
<p>上面的示例，我们可以看到，我们修改了./mnt/carrots的文件内容，./vegetables/carrots并没有变化，反而是./fruits/carrots的目录中出现了carrots文件，其内容是我们在./mnt/carrots里的内容。</p>
<p>也就是说，我们在mount aufs命令中，我们没有指它vegetables和fruits的目录权限，默认上来说，命令行上第一个（最左边）的目录是可读可写的，后面的全都是只读的。（一般来说，最前面的目录应该是可写的，而后面的都应该是只读的）</p>
<p>所以，如果我们像下面这样指定权限来mount aufs，你就会发现有不一样的效果（记得先把上面./fruits/carrots的文件删除了）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ sudo mount -t aufs -o dirs=./fruits=rw:./vegetables=rw none ./mnt

$ echo &quot;mnt_carrots&quot; &gt; ./mnt/carrots 

$ cat ./vegetables/carrots
mnt_carrots

$ cat ./fruits/carrots
cat: ./fruits/carrots: No such file or directory</pre>
<p>现在，在这情况下，如果我们要修改./mnt/tomato这个文件，那么究竟是哪个文件会被改写？</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ echo &quot;mnt_tomato&quot; &gt; ./mnt/tomato 

$ cat ./fruits/tomato
mnt_tomato

$ cat ./vegetables/tomato
I am a vegetable</pre>
<p>可见，如果有重复的文件名，在mount命令行上，越往前的就优先级越高。</p>
<p>你可以用这个例子做一些各种各样的试验，我这里主要是给大家一个感性认识，就不展开试验下去了。</p>
<p>那么，这种UnionFS有什么用？</p>
<p>历史上，有一个叫<a href="http://zh.wikipedia.org/wiki/Knoppix" target="_blank">Knoppix的Linux发行版</a>，其主要用于Linux演示、光盘教学、系统急救，以及商业产品的演示，不需要硬盘安装，直接把CD/DVD上的image运行在一个可写的存储设备上（比如一个U盘上），其实，也就是把CD/DVD这个文件系统和USB这个可写的系统给联合mount起来，这样你对CD/DVD上的image做的任何改动都会在被应用在U盘上，于是乎，你可以对CD/DVD上的内容进行任意的修改，因为改动都在U盘上，所以你改不坏原来的东西。</p>
<p>我们可以再发挥一下想像力，你也可以把一个目录，比如你的源代码，作为一个只读的template，和另一个你的working directory给union在一起，然后你就可以做各种修改而不用害怕会把源代码改坏了。有点像一个ad hoc snapshot。</p>
<p>Docker把UnionFS的想像力发挥到了容器的镜像。你是否还记得我在<a title="Docker基础技术：Linux Namespace（上）" href="https://coolshell.cn/articles/17010.html" target="_blank">介绍Linux Namespace上篇</a>中用mount namespace和chroot山寨了一镜像。现在当你看过了这个UnionFS的技术后，你是不是就明白了，你完全可以用UnionFS这样的技术做出分层的镜像来。</p>
<p>下图来自Docker的官方文档<a href="http://docs.docker.com/terms/layer/" target="_blank">Layer</a>，其很好的展示了Docker用UnionFS搭建的分层镜像。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17064" src="https://coolshell.cn/wp-content/uploads/2015/04/docker-filesystems-multilayer.png" alt="docker-filesystems-multilayer" width="500" height="375" srcset="https://coolshell.cn/wp-content/uploads/2015/04/docker-filesystems-multilayer.png 800w, https://coolshell.cn/wp-content/uploads/2015/04/docker-filesystems-multilayer-300x225.png 300w" sizes="(max-width: 500px) 100vw, 500px" /></p>
<p>关于docker的分层镜像，除了aufs，docker还支持btrfs, devicemapper和vfs，你可以使用 -s 或 &#8211;storage-driver= 选项来指定相关的镜像存储。在Ubuntu 14.04下，docker默认Ubuntu的 aufs（在CentOS7下，用的是devicemapper，关于devicemapper，我会以以后的文章中讲解）你可以在下面的目录中查看相关的每个层的镜像：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">/var/lib/docker/aufs/diff/&lt;id&gt; </code></p>
<p>在docker执行起来后（比如：docker run -it ubuntu /bin/bash ），你可以从/sys/fs/aufs/si_[id]目录下查看aufs的mount的情况，下面是个示例：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">#ls /sys/fs/aufs/si_b71b209f85ff8e75/
br0      br2      br4      br6      brid1    brid3    brid5    xi_path
br1      br3      br5      brid0    brid2    brid4    brid6 

# cat /sys/fs/aufs/si_b71b209f85ff8e75/*
/var/lib/docker/aufs/diff/87315f1367e5703f599168d1e17528a0500bd2e2df7d2fe2aaf9595f3697dbd7=rw
/var/lib/docker/aufs/diff/87315f1367e5703f599168d1e17528a0500bd2e2df7d2fe2aaf9595f3697dbd7-init=ro+wh
/var/lib/docker/aufs/diff/d0955f21bf24f5bfffd32d2d0bb669d0564701c271bc3dfc64cfc5adfdec2d07=ro+wh
/var/lib/docker/aufs/diff/9fec74352904baf5ab5237caa39a84b0af5c593dc7cc08839e2ba65193024507=ro+wh
/var/lib/docker/aufs/diff/a1a958a248181c9aa6413848cd67646e5afb9797f1a3da5995c7a636f050f537=ro+wh
/var/lib/docker/aufs/diff/f3c84ac3a0533f691c9fea4cc2ceaaf43baec22bf8d6a479e069f6d814be9b86=ro+wh
/var/lib/docker/aufs/diff/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158=ro+wh
64
65
66
67
68
69
70
/run/shm/aufs.xino</pre>
<p>你会看到只有最顶上的层（branch）是rw权限，其它的都是ro+wh权限只读的。</p>
<p>关于docker的aufs的配置，你可以在/var/lib/docker/repositories-aufs这个文件中看到。</p>
<h4>AUFS的一些特性</h4>
<p>AUFS有所有Union FS的特性，把多个目录，合并成同一个目录，并可以为每个需要合并的目录指定相应的权限，实时的添加、删除、修改已经被mount好的目录。而且，他还能在多个可写的branch/dir间进行负载均衡。</p>
<p>上面的例子，我们已经看到AUFS的mount的示例了。下面我们来看一看被union的目录（分支）的相关权限：</p>
<ul>
<li>rw表示可写可读read-write。</li>
<li>ro表示read-only，如果你不指权限，那么除了第一个外ro是默认值，对于ro分支，其永远不会收到写操作，也不会收到查找whiteout的操作。</li>
<li>rr表示real-read-only，与read-only不同的是，rr标记的是天生就是只读的分支，这样，AUFS可以提高性能，比如不再设置inotify来检查文件变动通知。</li>
</ul>
<p>权限中，我们看到了一个术语：whiteout，下面我来解释一下这个术语。</p>
<p>一般来说ro的分支都会有wh的属性，比如 &#8220;[dir]=ro+wh&#8221;。所谓whiteout的意思，如果在union中删除的某个文件，实际上是位于一个readonly的分支（目录）上，那么，在mount的union这个目录中你将看不到这个文件，但是read-only这个层上我们无法做任何的修改，所以，我们就需要对这个readonly目录里的文件作whiteout。AUFS的whiteout的实现是通过在上层的可写的目录下建立对应的whiteout隐藏文件来实现的。</p>
<p>看个例子：</p>
<p>假设我们有三个目录和文件如下所示（test是个空目录）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"># tree
.
├── fruits
│   ├── apple
│   └── tomato
├── test
└── vegetables
    ├── carrots
    └── tomato</pre>
<p>我们如下mount：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"># mkdir mnt

# mount -t aufs -o dirs=./test=rw:./fruits=ro:./vegetables=ro none ./mnt

# # ls ./mnt/
apple  carrots  tomato </pre>
<p>现在我们在权限为rw的test目录下建个whiteout的隐藏文件.wh.apple，你就会发现./mnt/apple这个文件就消失了:</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"> # touch ./test/.wh.apple

# ls ./mnt
carrots  tomato</pre>
<p>上面这个操作和 rm ./mnt/apple是一样的。</p>
<h5>相关术语</h5>
<p><b>Branch</b> – 就是各个要被union起来的目录（就是我在上面使用的dirs的命令行参数）</p>
<ul>
<li>Branch根据被union的顺序形成一个stack，一般来说最上面的是可写的，下面的都是只读的。</li>
<li>Branch的stack可以在被mount后进行修改，比如：修改顺序，加入新的branch，或是删除其中的branch，或是直接修改branch的权限</li>
</ul>
<p><b>Whiteout</b> 和 <b>Opaque</b></p>
<ul>
<li>如果UnionFS中的某个目录被删除了，那么就应该不可见了，就算是在底层的branch中还有这个目录，那也应该不可见了。</li>
</ul>
<ul>
<li>Whiteout就是某个上层目录覆盖了下层的相同名字的目录。用于隐藏低层分支的文件，也用于阻止readdir进入低层分支。</li>
</ul>
<ul>
<li>Opaque的意思就是不允许任何下层的某个目录显示出来。</li>
</ul>
<ul>
<li>在隐藏低层档的情况下，whiteout的名字是’.wh.&lt;filename&gt;’。</li>
</ul>
<ul>
<li>在阻止readdir的情况下，名字是’.wh..wh..opq’或者 ’.wh.__dir_opaque’。</li>
</ul>
<h5>相关问题</h5>
<p>看到上面这些，你一定会有几个问题：</p>
<p><strong>其一、你可能会问，要有文件在原来的地方被修改了会怎么样？</strong>mount的目录会一起改变吗？答案是会的，也可以是不会的。因为你可以指定一个叫udba的参数（全称：User’s Direct Branch Access），这个参数有三个取值：</p>
<ul>
<li><strong>udba=none</strong> – 设置上这个参数后，AUFS会运转的更快，因为那些不在mount目录里发生的修改，aufs不会同步过来了，所以会有数据出错的问题。</li>
<li><strong>udba=reval</strong> – 设置上这个参数后，AUFS会去查文件有没有被更新，如果有的话，就会把修改拉到mount目录内。</li>
<li><strong>udba=notify</strong> – 这个参数会让AUFS为所有的branch注册inotify，这样可以让AUFS在更新文件修改的性能更高一些。</li>
</ul>
<p><strong>其二、如果有多个rw的branch（目录）被union起来了，那么，当我创建文件的时候，aufs会创建在哪里呢？</strong> aufs提供了一个叫create的参数可以供你来配置相当的创建策略，下面有几个例子。</p>
<p><strong>create=rr | round−robin</strong> 轮询。下面的示例可以看到，新创建的文件轮流写到三个目录中</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
hchen$ sudo mount -t aufs  -o dirs=./1=rw:./2=rw:./3=rw -o create=rr none ./mnt
hchen$ touch ./mnt/a ./mnt/b ./mnt/c
hchen$ tree
.
├── 1
│   └── a
├── 2
│   └── c
└── 3
    └── b</pre>
<p><strong>create=mfs[:second] | most−free−space[:second]</strong> 选一个可用空间最好的分支。可以指定一个检查可用磁盘空间的时间。</p>
<p><strong>create=mfsrr:low[:second]</strong> 选一个空间大于low的branch，如果空间小于low了，那么aufs会使用 round-robin 方式。</p>
<p>更多的关于AUFS的细节使用参数，大家可以直接在Ubuntu 14.04下通过<a href="http://aufs.sourceforge.net/aufs3/man.html" target="_blank"> man aufs </a>来看一下其中的各种参数和命令。</p>
<h4>AUFS的性能</h4>
<p>AUFS的性能慢吗？也慢也不慢。因为AUFS会把所有的分支mount起来，所以，在查找文件上是比较慢了。因为它要遍历所有的branch。是个O(n)的算法（很明显，这个算法有很大的改进空间的）所以，branch越多，查找文件的性能也就越慢。但是，一旦AUFS找到了这个文件的inode，那后以后的读写和操作原文件基本上是一样的。</p>
<p>所以，如果你的程序跑在在AUFS下，open和stat操作会有明显的性能下降，branch越多，性能越差，但是在write/read操作上，性能没有什么变化。</p>
<p>IBM的研究中心对Docker的性能给了一份非常不错的性能报告（PDF）《<a href="http://domino.research.ibm.com/library/cyberdig.nsf/papers/0929052195DD819C85257D2300681E7B/$File/rc25482.pdf" target="_blank">An Updated Performance Comparison of Virtual Machinesand Linux Containers</a>》</p>
<p>我截了两张图出来，第一张是顺序读写，第二张是随机读写。基本没有什么性能损失的问题。而KVM在随机读写的情况也就有点慢了（但是，如果硬盘是SSD的呢？）</p>
<p><a href="https://coolshell.cn/wp-content/uploads/2015/08/docker.seq_.jpg"><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17191" src="https://coolshell.cn/wp-content/uploads/2015/08/docker.seq_.jpg" alt="" width="368" height="256" srcset="https://coolshell.cn/wp-content/uploads/2015/08/docker.seq_.jpg 368w, https://coolshell.cn/wp-content/uploads/2015/08/docker.seq_-300x209.jpg 300w" sizes="(max-width: 368px) 100vw, 368px" /></a></p>
<p>&nbsp;</p>
<p style="text-align: center;"><strong>顺序读写</strong></p>
<p><a href="https://coolshell.cn/wp-content/uploads/2015/08/docker.rand_.jpg"><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17190" src="https://coolshell.cn/wp-content/uploads/2015/08/docker.rand_.jpg" alt="" width="363" height="236" srcset="https://coolshell.cn/wp-content/uploads/2015/08/docker.rand_.jpg 363w, https://coolshell.cn/wp-content/uploads/2015/08/docker.rand_-300x195.jpg 300w" sizes="(max-width: 363px) 100vw, 363px" /></a></p>
<p>&nbsp;</p>
<p style="text-align: center;"><strong>随机读写</strong></p>
<h4>延伸阅读</h4>
<ul>
<li><a href="http://www.linuxjournal.com/article/7714" target="_blank">Introduce UnionFS</a></li>
<li><a href="http://lwn.net/Articles/325369/" target="_blank">Union file systems: Implementations, part I</a></li>
<li><a href="http://lwn.net/Articles/327738/" target="_blank">Union file systems: Implementations, part 2</a></li>
<li><a href="http://lwn.net/Articles/403012/" target="_blank">Another union filesystem approach</a></li>
<li><a href="http://lwn.net/Articles/324291/" target="_blank">Unioning file systems: Architecture, features, and design choices</a></li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1-150x150.png" alt="记一次Kubernetes/Docker网络排障" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_title">记一次Kubernetes/Docker网络排障</a></li><li ><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin-150x150.jpg" alt="Docker基础技术：DeviceMapper" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_title">Docker基础技术：DeviceMapper</a></li><li ><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/filter-150x150.png" alt="Docker基础技术：Linux CGroup" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_title">Docker基础技术：Linux CGroup</a></li><li ><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/isolation-150x150.jpg" alt="Docker基础技术：Linux Namespace（上）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_title">Docker基础技术：Linux Namespace（上）</a></li><li ><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/jail_cell-150x150.jpg" alt="Docker基础技术：Linux Namespace（下）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_title">Docker基础技术：Linux Namespace（下）</a></li><li ><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF-150x150.jpeg" alt="eBPF 介绍" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_title">eBPF 介绍</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17061.html">Docker基础技术：AUFS</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17061.html/feed</wfw:commentRss>
			<slash:comments>42</slash:comments>
		
		
			</item>
		<item>
		<title>Docker基础技术：Linux CGroup</title>
		<link>https://coolshell.cn/articles/17049.html</link>
					<comments>https://coolshell.cn/articles/17049.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Fri, 17 Apr 2015 01:03:57 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[杂项资源]]></category>
		<category><![CDATA[cgroup]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Linux]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17049</guid>

					<description><![CDATA[<p>前面，我们介绍了Linux Namespace，但是Namespace解决的问题主要是环境隔离的问题，这只是虚拟化中最最基础的一步，我们还需要解决对计算机资源使...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17049.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17049.html">Docker基础技术：Linux CGroup</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2015/04/filter.png" alt="filter" width="224" height="225" class="alignright size-full wp-image-17097" srcset="https://coolshell.cn/wp-content/uploads/2015/04/filter.png 224w, https://coolshell.cn/wp-content/uploads/2015/04/filter-150x150.png 150w, https://coolshell.cn/wp-content/uploads/2015/04/filter-200x200.png 200w" sizes="(max-width: 224px) 100vw, 224px" />前面，我们介绍了<a title="Docker基础技术：Linux Namespace" href="https://coolshell.cn/articles/17010.html" target="_blank">Linux Namespace</a>，但是Namespace解决的问题主要是环境隔离的问题，这只是虚拟化中最最基础的一步，我们还需要解决对计算机资源使用上的隔离。也就是说，虽然你通过Namespace把我Jail到一个特定的环境中去了，但是我在其中的进程使用用CPU、内存、磁盘等这些计算资源其实还是可以随心所欲的。所以，我们希望对进程进行资源利用上的限制或控制。这就是Linux CGroup出来了的原因。</p>
<p>Linux CGroup全称Linux Control Group， 是Linux内核的一个功能，用来限制，控制与分离一个进程组群的资源（如CPU、内存、磁盘输入输出等）。这个项目最早是由Google的工程师在2006年发起（主要是Paul Menage和Rohit Seth），最早的名称为进程容器（process containers）。在2007年时，因为在Linux内核中，容器（container）这个名词太过广泛，为避免混乱，被重命名为cgroup，并且被合并到2.6.24版的内核中去。然后，其它开始了他的发展。</p>
<p>Linux CGroupCgroup 可​​​让​​​您​​​为​​​系​​​统​​​中​​​所​​​运​​​行​​​任​​​务​​​（进​​​程​​​）的​​​用​​​户​​​定​​​义​​​组​​​群​​​分​​​配​​​资​​​源​​​ &#8212; 比​​​如​​​ CPU 时​​​间​​​、​​​系​​​统​​​内​​​存​​​、​​​网​​​络​​​带​​​宽​​​或​​​者​​​这​​​些​​​资​​​源​​​的​​​组​​​合​​​。​​​您​​​可​​​以​​​监​​​控​​​您​​​配​​​置​​​的​​​ cgroup，拒​​​绝​​​ cgroup 访​​​问​​​某​​​些​​​资​​​源​​​，甚​​​至​​​在​​​运​​​行​​​的​​​系​​​统​​​中​​​动​​​态​​​配​​​置​​​您​​​的​​​ cgroup。</p>
<p>主要提供了如下功能：</p>
<p><span id="more-17049"></span></p>
<ul>
<li><strong>Resource limitation</strong>: 限制资源使用，比如内存使用上限以及文件系统的缓存限制。</li>
<li><strong>Prioritization</strong>: 优先级控制，比如：CPU利用和磁盘IO吞吐。</li>
<li><strong>Accounting</strong>: 一些审计或一些统计，主要目的是为了计费。</li>
<li><strong>Control</strong>: 挂起进程，恢复执行进程。</li>
</ul>
<p>使​​​用​​​ cgroup，系​​​统​​​管​​​理​​​员​​​可​​​更​​​具​​​体​​​地​​​控​​​制​​​对​​​系​​​统​​​资​​​源​​​的​​​分​​​配​​​、​​​优​​​先​​​顺​​​序​​​、​​​拒​​​绝​​​、​​​管​​​理​​​和​​​监​​​控​​​。​​​可​​​更​​​好​​​地​​​根​​​据​​​任​​​务​​​和​​​用​​​户​​​分​​​配​​​硬​​​件​​​资​​​源​​​，提​​​高​​​总​​​体​​​效​​​率​​​。</p>
<p>在实践中，系统管理员一般会利用CGroup做下面这些事（有点像为某个虚拟机分配资源似的）：</p>
<ul>
<li>隔离一个进程集合（比如：nginx的所有进程），并限制他们所消费的资源，比如绑定CPU的核。</li>
<li>为这组进程 分配其足够使用的内存</li>
<li>为这组进程分配相应的网络带宽和磁盘存储限制</li>
<li>限制访问某些设备（通过设置设备的白名单）</li>
</ul>
<p>那么CGroup是怎么干的呢？我们先来点感性认识吧。</p>
<p>首先，Linux把CGroup这个事实现成了一个file system，你可以mount。在我的Ubuntu 14.04下，你输入以下命令你就可以看到cgroup已为你mount好了。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">hchen@ubuntu:~$ mount -t cgroup
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (rw,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (rw,relatime,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,relatime,blkio)
cgroup on /sys/fs/cgroup/net_prio type cgroup (rw,net_prio)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,relatime,hugetlb)</pre>
<p>或者使用lssubsys命令：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ lssubsys  -m
cpuset /sys/fs/cgroup/cpuset
cpu /sys/fs/cgroup/cpu
cpuacct /sys/fs/cgroup/cpuacct
memory /sys/fs/cgroup/memory
devices /sys/fs/cgroup/devices
freezer /sys/fs/cgroup/freezer
blkio /sys/fs/cgroup/blkio
net_cls /sys/fs/cgroup/net_cls
net_prio /sys/fs/cgroup/net_prio
perf_event /sys/fs/cgroup/perf_event
hugetlb /sys/fs/cgroup/hugetlb</pre>
<p>我们可以看到，在/sys/fs下有一个cgroup的目录，这个目录下还有很多子目录，比如： cpu，cpuset，memory，blkio……这些，这些都是cgroup的子系统。分别用于干不同的事的。</p>
<p>如果你没有看到上述的目录，你可以自己mount，下面给了一个示例：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">mkdir cgroup
mount -t tmpfs cgroup_root ./cgroup
mkdir cgroup/cpuset
mount -t cgroup -ocpuset cpuset ./cgroup/cpuset/
mkdir cgroup/cpu
mount -t cgroup -ocpu cpu ./cgroup/cpu/
mkdir cgroup/memory
mount -t cgroup -omemory memory ./cgroup/memory/</pre>
<p>一旦mount成功，你就会看到这些目录下就有好文件了，比如，如下所示的cpu和cpuset的子系统：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">hchen@ubuntu:~$ ls /sys/fs/cgroup/cpu /sys/fs/cgroup/cpuset/ 
/sys/fs/cgroup/cpu:
cgroup.clone_children  cgroup.sane_behavior  cpu.shares         release_agent
cgroup.event_control   cpu.cfs_period_us     cpu.stat           tasks
cgroup.procs           cpu.cfs_quota_us      notify_on_release  user

/sys/fs/cgroup/cpuset/:
cgroup.clone_children  cpuset.mem_hardwall             cpuset.sched_load_balance
cgroup.event_control   cpuset.memory_migrate           cpuset.sched_relax_domain_level
cgroup.procs           cpuset.memory_pressure          notify_on_release
cgroup.sane_behavior   cpuset.memory_pressure_enabled  release_agent
cpuset.cpu_exclusive   cpuset.memory_spread_page       tasks
cpuset.cpus            cpuset.memory_spread_slab       user
cpuset.mem_exclusive   cpuset.mems</pre>
<p>你可以到/sys/fs/cgroup的各个子目录下去make个dir，你会发现，一旦你创建了一个子目录，这个子目录里又有很多文件了。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">hchen@ubuntu:/sys/fs/cgroup/cpu$ sudo mkdir haoel
[sudo] password for hchen: 
hchen@ubuntu:/sys/fs/cgroup/cpu$ ls ./haoel
cgroup.clone_children  cgroup.procs       cpu.cfs_quota_us  cpu.stat           tasks
cgroup.event_control   cpu.cfs_period_us  cpu.shares        notify_on_release</pre>
<p>好了，我们来看几个示例。</p>
<h4>CPU 限制</h4>
<p>假设，我们有一个非常吃CPU的程序，叫deadloop，其源码如下：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">int main(void)
{
    int i = 0;
    for(;;) i++;
    return 0;
}</pre>
<p>用sudo执行起来后，毫无疑问，CPU被干到了100%（下面是top命令的输出）</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND     
 3529 root      20   0    4196    736    656 R 99.6  0.1   0:23.13 deadloop   </pre>
<p>然后，我们这前不是在/sys/fs/cgroup/cpu下创建了一个haoel的group。我们先设置一下这个group的cpu利用的限制：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">hchen@ubuntu:~# cat /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us 
-1
root@ubuntu:~# echo 20000 &gt; /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us</pre>
<p>我们看到，这个进程的PID是3529，我们把这个进程加到这个cgroup中：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW"># echo 3529 &gt;&gt; /sys/fs/cgroup/cpu/haoel/tasks</code></p>
<p>然后，就会在top中看到CPU的利用立马下降成20%了。（前面我们设置的20000就是20%的意思）</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">  PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND     
 3529 root      20   0    4196    736    656 R 19.9  0.1   8:06.11 deadloop    </pre>
<p>下面的代码是一个线程的示例：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#define _GNU_SOURCE         /* See feature_test_macros(7) */

#include &lt;pthread.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;sys/stat.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;
#include &lt;sys/syscall.h&gt;


const int NUM_THREADS = 5;

void *thread_main(void *threadid)
{
    /* 把自己加入cgroup中（syscall(SYS_gettid)为得到线程的系统tid） */
    char cmd[128];
    sprintf(cmd, &quot;echo %ld &gt;&gt; /sys/fs/cgroup/cpu/haoel/tasks&quot;, syscall(SYS_gettid));
    system(cmd); 
    sprintf(cmd, &quot;echo %ld &gt;&gt; /sys/fs/cgroup/cpuset/haoel/tasks&quot;, syscall(SYS_gettid));
    system(cmd);

    long tid;
    tid = (long)threadid;
    printf(&quot;Hello World! It&#039;s me, thread #%ld, pid #%ld!\n&quot;, tid, syscall(SYS_gettid));
    
    int a=0; 
    while(1) {
        a++;
    }
    pthread_exit(NULL);
}
int main (int argc, char *argv[])
{
    int num_threads;
    if (argc &gt; 1){
        num_threads = atoi(argv[1]);
    }
    if (num_threads&lt;=0 || num_threads&gt;=100){
        num_threads = NUM_THREADS;
    }

    /* 设置CPU利用率为50% */
    mkdir(&quot;/sys/fs/cgroup/cpu/haoel&quot;, 755);
    system(&quot;echo 50000 &gt; /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us&quot;);

    mkdir(&quot;/sys/fs/cgroup/cpuset/haoel&quot;, 755);
    /* 限制CPU只能使用#2核和#3核 */
    system(&quot;echo \&quot;2,3\&quot; &gt; /sys/fs/cgroup/cpuset/haoel/cpuset.cpus&quot;);

    pthread_t* threads = (pthread_t*) malloc (sizeof(pthread_t)*num_threads);
    int rc;
    long t;
    for(t=0; t&lt;num_threads; t++){
        printf(&quot;In main: creating thread %ld\n&quot;, t);
        rc = pthread_create(&amp;threads[t], NULL, thread_main, (void *)t);
        if (rc){
            printf(&quot;ERROR; return code from pthread_create() is %d\n&quot;, rc);
            exit(-1);
        }
    }

    /* Last thing that main() should do */
    pthread_exit(NULL);
    free(threads);
}
</pre>
<h4>内存使用限制</h4>
<p>我们再来看一个限制内存的例子（下面的代码是个死循环，其它不断的分配内存，每次512个字节，每次休息一秒）：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;unistd.h&gt;

int main(void)
{
    int size = 0;
    int chunk_size = 512;
    void *p = NULL;

    while(1) {

        if ((p = malloc(p, chunk_size)) == NULL) {
            printf(&quot;out of memory!!\n&quot;);
            break;
        }
        memset(p, 1, chunk_size);
        size += chunk_size;
        printf(&quot;[%d] - memory is allocated [%8d] bytes \n&quot;, getpid(), size);
        sleep(1);
    }
    return 0;
}</pre>
<p>然后，在我们另外一边：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW"># 创建memory cgroup
$ mkdir /sys/fs/cgroup/memory/haoel
$ echo 64k &gt; /sys/fs/cgroup/memory/haoel/memory.limit_in_bytes

# 把上面的进程的pid加入这个cgroup
$ echo [pid] &gt; /sys/fs/cgroup/memory/haoel/tasks </pre>
<p>你会看到，一会上面的进程就会因为内存问题被kill掉了。</p>
<h4>磁盘I/O限制</h4>
<p>我们先看一下我们的硬盘IO，我们的模拟命令如下：（从/dev/sda1上读入数据，输出到/dev/null上）</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">sudo dd if=/dev/sda1 of=/dev/null</code></p>
<p>我们通过iotop命令我们可以看到相关的IO速度是55MB/s（虚拟机内）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO&gt;    COMMAND          
 8128 be/4 root       55.74 M/s    0.00 B/s  0.00 % 85.65 % dd if=/de~=/dev/null...</pre>
<p>然后，我们先创建一个blkio（块设备IO）的cgroup</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">mkdir /sys/fs/cgroup/blkio/haoel</code></p>
<p>并把读IO限制到1MB/s，并把前面那个dd命令的pid放进去（注：8:0 是设备号，你可以通过ls -l /dev/sda1获得）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">root@ubuntu:~# echo &#039;8:0 1048576&#039;  &gt; /sys/fs/cgroup/blkio/haoel/blkio.throttle.read_bps_device 
root@ubuntu:~# echo 8128 &gt; /sys/fs/cgroup/blkio/haoel/tasks</pre>
<p>再用iotop命令，你马上就能看到读速度被限制到了1MB/s左右。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO&gt;    COMMAND          
 8128 be/4 root      973.20 K/s    0.00 B/s  0.00 % 94.41 % dd if=/de~=/dev/null...</pre>
<h4>CGroup的子系统</h4>
<p>好了，有了以上的感性认识我们来，我们来看看control group有哪些子系统：</p>
<ul>
<li>blkio &#8212; 这​​​个​​​子​​​系​​​统​​​为​​​块​​​设​​​备​​​设​​​定​​​输​​​入​​​/输​​​出​​​限​​​制​​​，比​​​如​​​物​​​理​​​设​​​备​​​（磁​​​盘​​​，固​​​态​​​硬​​​盘​​​，USB 等​​​等​​​）。</li>
<li>cpu &#8212; 这​​​个​​​子​​​系​​​统​​​使​​​用​​​调​​​度​​​程​​​序​​​提​​​供​​​对​​​ CPU 的​​​ cgroup 任​​​务​​​访​​​问​​​。​​​</li>
<li>cpuacct &#8212; 这​​​个​​​子​​​系​​​统​​​自​​​动​​​生​​​成​​​ cgroup 中​​​任​​​务​​​所​​​使​​​用​​​的​​​ CPU 报​​​告​​​。​​​</li>
<li>cpuset &#8212; 这​​​个​​​子​​​系​​​统​​​为​​​ cgroup 中​​​的​​​任​​​务​​​分​​​配​​​独​​​立​​​ CPU（在​​​多​​​核​​​系​​​统​​​）和​​​内​​​存​​​节​​​点​​​。​​​</li>
<li>devices &#8212; 这​​​个​​​子​​​系​​​统​​​可​​​允​​​许​​​或​​​者​​​拒​​​绝​​​ cgroup 中​​​的​​​任​​​务​​​访​​​问​​​设​​​备​​​。​​​</li>
<li>freezer &#8212; 这​​​个​​​子​​​系​​​统​​​挂​​​起​​​或​​​者​​​恢​​​复​​​ cgroup 中​​​的​​​任​​​务​​​。​​​</li>
<li>memory &#8212; 这​​​个​​​子​​​系​​​统​​​设​​​定​​​ cgroup 中​​​任​​​务​​​使​​​用​​​的​​​内​​​存​​​限​​​制​​​，并​​​自​​​动​​​生​​​成​​​​​内​​​存​​​资​​​源使用​​​报​​​告​​​。​​​</li>
<li>net_cls &#8212; 这​​​个​​​子​​​系​​​统​​​使​​​用​​​等​​​级​​​识​​​别​​​符​​​（classid）标​​​记​​​网​​​络​​​数​​​据​​​包​​​，可​​​允​​​许​​​ Linux 流​​​量​​​控​​​制​​​程​​​序​​​（tc）识​​​别​​​从​​​具​​​体​​​ cgroup 中​​​生​​​成​​​的​​​数​​​据​​​包​​​。​​​</li>
<li>net_prio &#8212; 这个子系统用来设计网络流量的优先级</li>
<li>hugetlb &#8212; 这个子系统主要针对于HugeTLB系统进行限制，这是一个大页文件系统。</li>
<p>​​​</ul>
<p>注意，你可能在Ubuntu 14.04下看不到net_cls和net_prio这两个cgroup，你需要手动mount一下：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ sudo modprobe cls_cgroup
$ sudo mkdir /sys/fs/cgroup/net_cls
$ sudo mount -t cgroup -o net_cls none /sys/fs/cgroup/net_cls

$ sudo modprobe netprio_cgroup
$ sudo mkdir /sys/fs/cgroup/net_prio
$ sudo mount -t cgroup -o net_prio none /sys/fs/cgroup/net_prio</pre>
<p>关于各个子系统的参数细节，以及更多的Linux CGroup的文档，你可以看看下面的文档：</p>
<ul>
<li><a href="https://www.kernel.org/doc/Documentation/cgroups/" target="_blank">Linux Kernel的官方文档</a></li>
<li><a href="https://access.redhat.com/documentation/zh-CN/Red_Hat_Enterprise_Linux/6/html-single/Resource_Management_Guide/index.html#ch-Subsystems_and_Tunable_Parameters" target="_blank">Redhat的官方文档</a></li>
</ul>
<h4>CGroup的术语</h4>
<p>CGroup有下述术语：</p>
<ul>
<li><strong>任务（Tasks）</strong>：就是系统的一个进程。</li>
<li><strong>控制组（Control Group）</strong>：一组按照某种标准划分的进程，比如官方文档中的Professor和Student，或是WWW和System之类的，其表示了某进程组。Cgroups中的资源控制都是以控制组为单位实现。一个进程可以加入到某个控制组。而资源的限制是定义在这个组上，就像上面示例中我用的haoel一样。简单点说，cgroup的呈现就是一个目录带一系列的可配置文件。</li>
<li><strong>层级（Hierarchy）</strong>：控制组可以组织成hierarchical的形式，既一颗控制组的树（目录结构）。控制组树上的子节点继承父结点的属性。简单点说，hierarchy就是在一个或多个子系统上的cgroups目录树。</li>
<li><strong>子系统（Subsystem）</strong>：一个子系统就是一个资源控制器，比如CPU子系统就是控制CPU时间分配的一个控制器。子系统必须附加到一个层级上才能起作用，一个子系统附加到某个层级以后，这个层级上的所有控制族群都受到这个子系统的控制。Cgroup的子系统可以有很多，也在不断增加中。</li>
</ul>
<h4>下一代的CGroup</h4>
<p>上面，我们可以看到，CGroup的一些常用方法和相关的术语。一般来说，这样的设计在一般情况下还是没什么问题的，除了操作上的用户体验不是很好，但基本满足我们的一般需求了。</p>
<p>不过，对此，有个叫Tejun Heo的同学非常不爽，他在Linux社区里<a href="https://lwn.net/Articles/484254/" target="_blank">对cgroup吐了一把槽</a>，还引发了内核组的各种讨论。</p>
<p>对于Tejun Heo同学来说，cgroup设计的相当糟糕。他给出了些例子，大意就是说，如果有多种层级关系，也就是说有多种对进程的分类方式，比如，我们可以按用户来分，分成Professor和Student，同时，也有按应用类似来分的，比如WWW和NFS等。那么，当一个进程即是Professor的，也是WWW的，那么就会出现多层级正交的情况，从而出现对进程上管理的混乱。另外，一个case是，如果有一个层级A绑定cpu，而层级B绑定memory，还有一个层级C绑定cputset，而有一些进程有的需要AB，有的需要AC，有的需要ABC，管理起来就相当不易。 </p>
<p>层级操作起来比较麻烦，而且如果层级变多，更不易于操作和管理，虽然那种方式很好实现，但是在使用上有很多的复杂度。你可以想像一个图书馆的图书分类问题，你可以有各种不同的分类，分类和图书就是一种多对多的关系。</p>
<p>所以，在Kernel 3.16后，引入了<a href="http://lwn.net/Articles/601840/" target="_blank">unified hierarchy</a>的新的设计，这个东西引入了一个叫<strong>__DEVEL__sane_behavior</strong>的特性（这个名字很明显意味目前还在开发试验阶段），它可以把所有子系统都挂载到根层级下，只有叶子节点可以存在tasks，非叶子节点只进行资源控制。</p>
<p>我们mount一下看看：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ sudo mount -t cgroup -o __DEVEL__sane_behavior cgroup ./cgroup

$ ls ./cgroup
cgroup.controllers  cgroup.procs  cgroup.sane_behavior  cgroup.subtree_control 

$ cat ./cgroup/cgroup.controllers
cpuset cpu cpuacct memory devices freezer net_cls blkio perf_event net_prio hugetlb</pre>
<p>我们可以看到有四个文件，然后，你在这里mkdir一个子目录，里面也会有这四个文件。<strong>上级的cgroup.subtree_control控制下级的cgroup.controllers。</strong></p>
<p>举个例子：假设我们有以下的目录结构，b代表blkio，m代码memory，其中，A是root，包括所有的子系统（）。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
# A(b,m) - B(b,m) - C (b)
#               \ - D (b) - E

# 下面的命令中， +表示enable， -表示disable

# 在B上的enable blkio
# echo +blkio &gt; A/cgroup.subtree_control

# 在C和D上enable blkio 
# echo +blkio &gt; A/B/cgroup.subtree_control

# 在B上enable memory  
# echo +memory &gt; A/cgroup.subtree_control</pre>
<p>在上述的结构中，</p>
<ul>
<li>cgroup只有上线控制下级，无法传递到下下级。所以，C和D中没有memory的限制，E中没有blkio和memory的限制。而本层的cgroup.controllers文件是个只读的，其中的内容就看上级的subtree_control里有什么了。</li>
<li><strong>任何被配置过subtree_control的目录都不能绑定进程，根结点除外</strong>。所以，A,C,D,E可以绑上进程，但是B不行。</li>
</ul>
<p>我们可以看到，<strong>这种方式干净的区分开了两个事，一个是进程的分组，一个是对分组的资源控制</strong>（以前这两个事完全混在一起），在目录继承上增加了些限制，这样可以避免一些模棱两可的情况。</p>
<p>当然，这个事还在演化中，cgroup的这些问题这个事目前由cgroup的吐槽人Tejun Heo和华为的Li Zefan同学负责解决中。总之，这是一个系统管理上的问题，而且改变会影响很多东西，但一旦方案确定，老的cgroup方式将一去不复返。</p>
<h4>参考</h4>
<ul>
<li><a href="https://www.kernel.org/doc/Documentation/cgroups/" target="_blank">Linux Kernel Cgroup Documents</a></li>
<li><a href="https://access.redhat.com/documentation/zh-CN/Red_Hat_Enterprise_Linux/6/html-single/Resource_Management_Guide/index.html" target="_blank">Reahat Resource Management Guide</a></li>
<li><a href="https://lwn.net/Articles/484251/" target="_blank">Fixing control groups</a></li>
<li><a href="http://lwn.net/Articles/601840/" target="_blank">The unified control group hierarchy in 3.16</a></li>
<li><a href="http://events.linuxfoundation.org/sites/events/files/slides/2014-KLF.pdf" target="_blank">Cgroup v2(PDF)</a></li>
</ul>
<p>（全文完）<br />
<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1-150x150.png" alt="记一次Kubernetes/Docker网络排障" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_title">记一次Kubernetes/Docker网络排障</a></li><li ><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin-150x150.jpg" alt="Docker基础技术：DeviceMapper" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_title">Docker基础技术：DeviceMapper</a></li><li ><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-150x150.png" alt="Docker基础技术：AUFS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_title">Docker基础技术：AUFS</a></li><li ><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/isolation-150x150.jpg" alt="Docker基础技术：Linux Namespace（上）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_title">Docker基础技术：Linux Namespace（上）</a></li><li ><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/jail_cell-150x150.jpg" alt="Docker基础技术：Linux Namespace（下）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_title">Docker基础技术：Linux Namespace（下）</a></li><li ><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF-150x150.jpeg" alt="eBPF 介绍" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_title">eBPF 介绍</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17049.html">Docker基础技术：Linux CGroup</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17049.html/feed</wfw:commentRss>
			<slash:comments>87</slash:comments>
		
		
			</item>
		<item>
		<title>Docker基础技术：Linux Namespace（上）</title>
		<link>https://coolshell.cn/articles/17010.html</link>
					<comments>https://coolshell.cn/articles/17010.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 16 Apr 2015 02:20:08 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Mount]]></category>
		<category><![CDATA[Namespace]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17010</guid>

					<description><![CDATA[<p>时下最热的技术莫过于Docker了，很多人都觉得Docker是个新技术，其实不然，Docker除了其编程语言用go比较新外，其实它还真不是个新东西，也就是个新瓶...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17010.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17010.html">Docker基础技术：Linux Namespace（上）</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-full wp-image-17085" src="https://coolshell.cn/wp-content/uploads/2015/04/isolation.jpg" alt="isolation" width="359" height="237" srcset="https://coolshell.cn/wp-content/uploads/2015/04/isolation.jpg 359w, https://coolshell.cn/wp-content/uploads/2015/04/isolation-300x198.jpg 300w" sizes="(max-width: 359px) 100vw, 359px" />时下最热的技术莫过于Docker了，很多人都觉得Docker是个新技术，其实不然，Docker除了其编程语言用go比较新外，其实它还真不是个新东西，也就是个新瓶装旧酒的东西，所谓的The New &#8220;Old Stuff&#8221;。Docker和Docker衍生的东西用到了很多很酷的技术，我会用几篇 文章来把这些技术给大家做个介绍，希望通过这些文章大家可以自己打造一个山寨版的docker。</p>
<p>当然，文章的风格一定会尊重时下的“流行”——<strong>我们再也没有整块整块的时间去看书去专研，而我们只有看微博微信那样的碎片时间</strong>（那怕我们有整块的时间，也被那些在手机上的APP碎片化了）。所以，这些文章的风格必然坚持“马桶风格”（希望简单到占用你拉一泡屎就时间，而且你还不用动脑子，并能学到些东西）</p>
<p>废话少说，我们开始。先从Linux Namespace开始。</p>
<h4> 简介</h4>
<p>Linux Namespace是Linux提供的一种内核级别环境隔离的方法。不知道你是否还记得很早以前的Unix有一个叫chroot的系统调用（通过修改根目录把用户jail到一个特定目录下），chroot提供了一种简单的隔离模式：chroot内部的文件系统无法访问外部的内容。Linux Namespace在此基础上，提供了对UTS、IPC、mount、PID、network、User等的隔离机制。</p>
<p><span id="more-17010"></span></p>
<p>举个例子，我们都知道，Linux下的超级父亲进程的PID是1，所以，同chroot一样，如果我们可以把用户的进程空间jail到某个进程分支下，并像chroot那样让其下面的进程 看到的那个超级父进程的PID为1，于是就可以达到资源隔离的效果了（不同的PID namespace中的进程无法看到彼此）</p>
<p><b>Linux Namespace 有如下种类</b>，官方文档在这里《<a href="http://lwn.net/Articles/531114/" target="_blank" rel="noopener noreferrer">Namespace in Operation</a>》</p>
<table width="100%">
<thead>
<tr>
<th>分类</th>
<th>系统调用参数</th>
<th>相关内核版本</th>
</tr>
</thead>
<tbody>
<tr>
<td><b>Mount namespaces</b></td>
<td>CLONE_NEWNS</td>
<td><a href="http://lwn.net/2001/0301/a/namespaces.php3" target="_blank" rel="noopener noreferrer">Linux 2.4.19</a></td>
</tr>
<tr>
<td><b>UTS namespaces</b></td>
<td>CLONE_NEWUTS</td>
<td><a href="http://lwn.net/Articles/179345/" target="_blank" rel="noopener noreferrer">Linux 2.6.19</a></td>
</tr>
<tr>
<td><b>IPC namespaces</b></td>
<td>CLONE_NEWIPC</td>
<td><a href="http://lwn.net/Articles/187274/" target="_blank" rel="noopener noreferrer">Linux 2.6.19</a></td>
</tr>
<tr>
<td><b>PID namespaces</b></td>
<td>CLONE_NEWPID</td>
<td><a href="http://lwn.net/Articles/259217/" target="_blank" rel="noopener noreferrer">Linux 2.6.24</a></td>
</tr>
<tr>
<td><b>Network namespaces</b></td>
<td>CLONE_NEWNET</td>
<td><a href="http://lwn.net/Articles/219794/" target="_blank" rel="noopener noreferrer">始于Linux 2.6.24 完成于 Linux 2.6.29</a></td>
</tr>
<tr>
<td><b>User namespaces</b></td>
<td>CLONE_NEWUSER</td>
<td><a href="http://lwn.net/Articles/528078/" target="_blank" rel="noopener noreferrer">始于 Linux 2.6.23 完成于 Linux 3.8)</a></td>
</tr>
</tbody>
</table>
<p>主要是三个系统调用</p>
<ul>
<li><b><code>clone</code></b><b>() </b>&#8211; 实现线程的系统调用，用来创建一个新的进程，并可以通过设计上述参数达到隔离。</li>
<li><b><code>unshare</code></b><b>() </b>&#8211; 使某进程脱离某个namespace</li>
<li><b><code>setns</code></b><b>() </b>&#8211; 把某进程加入到某个namespace</li>
</ul>
<p>unshare() 和 setns() 都比较简单，大家可以自己man，我这里不说了。</p>
<p>下面还是让我们来看一些示例（以下的测试程序最好在Linux 内核为3.8以上的版本中运行，我用的是ubuntu 14.04）。</p>
<h4>clone()系统调用</h4>
<p>首先，我们来看一下一个最简单的clone()系统调用的示例，（后面，我们的程序都会基于这个程序做修改）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">#define _GNU_SOURCE
#include &lt;sys/types.h&gt;
#include &lt;sys/wait.h&gt;
#include &lt;stdio.h&gt;
#include &lt;sched.h&gt;
#include &lt;signal.h&gt;
#include &lt;unistd.h&gt;

/* 定义一个给 clone 用的栈，栈大小1M */
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];

char* const container_args[] = {
    "/bin/bash",
    NULL
};

int container_main(void* arg)
{
    printf("Container - inside the container!\n");
    /* 直接执行一个shell，以便我们观察这个进程空间里的资源是否被隔离了 */
    execv(container_args[0], container_args); 
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent - start a container!\n");
    /* 调用clone函数，其中传出一个函数，还有一个栈空间的（为什么传尾指针，因为栈是反着的） */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD, NULL);
    /* 等待子进程结束 */
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}</pre>
<p>从上面的程序，我们可以看到，这和pthread基本上是一样的玩法。但是，对于上面的程序，父子进程的进程空间是没有什么差别的，父进程能访问到的子进程也能。</p>
<p>下面， 让我们来看几个例子看看，Linux的Namespace是什么样的。</p>
<h4>UTS Namespace</h4>
<p>下面的代码，我略去了上面那些头文件和数据结构的定义，只有最重要的部分。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c" data-enlighter-highlight="4,14">int container_main(void* arg)
{
    printf("Container - inside the container!\n");
    sethostname("container",10); /* 设置hostname */
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent - start a container!\n");
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | SIGCHLD, NULL); /*启用CLONE_NEWUTS Namespace隔离 */
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}</pre>
<pre>运行上面的程序你会发现（需要root权限），子进程的hostname变成了 container。</pre>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~$ sudo ./uts
Parent - start a container!
Container - inside the container!
root@container:~# hostname
container
root@container:~# uname -n
container</pre>
<h4>IPC Namespace</h4>
<p>IPC全称 Inter-Process Communication，是Unix/Linux下进程间通信的一种方式，IPC有共享内存、信号量、消息队列等方法。所以，为了隔离，我们也需要把IPC给隔离开来，这样，只有在同一个Namespace下的进程才能相互通信。如果你熟悉IPC的原理的话，你会知道，IPC需要有一个全局的ID，即然是全局的，那么就意味着我们的Namespace需要对这个ID隔离，不能让别的Namespace的进程看到。</p>
<p>要启动IPC隔离，我们只需要在调用clone时加上CLONE_NEWIPC参数就可以了。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);</pre>
<p>首先，我们先创建一个IPC的Queue（如下所示，全局的Queue ID是0）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~$ ipcmk -Q 
Message queue id: 0

hchen@ubuntu:~$ ipcs -q
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0xd0d56eb2 0          hchen      644        0            0</pre>
<p>如果我们运行没有CLONE_NEWIPC的程序，我们会看到，在子进程中还是能看到这个全启的IPC Queue。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~$ sudo ./uts 
Parent - start a container!
Container - inside the container!

root@container:~# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    
0xd0d56eb2 0          hchen      644        0            0</pre>
<p>但是，如果我们运行加上了CLONE_NEWIPC的程序，我们就会下面的结果：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">root@ubuntu:~$ sudo./ipc
Parent - start a container!
Container - inside the container!

root@container:~/linux_namespace# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages</pre>
<p>我们可以看到IPC已经被隔离了。</p>
<h4>PID Namespace</h4>
<p>我们继续修改上面的程序：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c" data-enlighter-highlight="4,16">
int container_main(void* arg)
{
    /* 查看子进程的PID，我们可以看到其输出子进程的 pid 为 1 */
    printf("Container [%5d] - inside the container!\n", getpid());
    sethostname("container",10);
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent [%5d] - start a container!\n", getpid());
    /*启用PID namespace - CLONE_NEWPID*/
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL); 
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}</pre>
<p>运行结果如下（我们可以看到，子进程的pid是1了）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
hchen@ubuntu:~$ sudo ./pid
Parent [ 3474] - start a container!
Container [ 1] - inside the container!
root@container:~# echo $$
1</pre>
<p>你可能会问，PID为1有个毛用啊？我们知道，在传统的UNIX系统中，PID为1的进程是init，地位非常特殊。他作为所有进程的父进程，有很多特权（比如：屏蔽信号等），另外，其还会为检查所有进程的状态，我们知道，如果某个子进程脱离了父进程（父进程没有wait它），那么init就会负责回收资源并结束这个子进程。所以，要做到进程空间的隔离，首先要创建出PID为1的进程，最好就像chroot那样，把子进程的PID在容器内变成1。</p>
<p><strong>但是，我们会发现，在子进程的shell里输入ps,top等命令，我们还是可以看得到所有进程</strong>。说明并没有完全隔离。这是因为，像ps, top这些命令会去读/proc文件系统，所以，因为/proc文件系统在父进程和子进程都是一样的，所以这些命令显示的东西都是一样的。</p>
<p>所以，我们还需要对文件系统进行隔离。</p>
<h4>Mount Namespace</h4>
<p>下面的例程中，我们在启用了mount namespace并在子进程中重新mount了/proc文件系统。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c" data-enlighter-highlight="6,17">int container_main(void* arg)
{
    printf("Container [%5d] - inside the container!\n", getpid());
    sethostname("container",10);
    /* 重新mount proc文件系统到 /proc下 */
    system("mount -t proc proc /proc");
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent [%5d] - start a container!\n", getpid());
    /* 启用Mount Namespace - 增加CLONE_NEWNS参数 */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}</pre>
<p>运行结果如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
hchen@ubuntu:~$ sudo ./pid.mnt
Parent [ 3502] - start a container!
Container [    1] - inside the container!
root@container:~# ps -elf 
F S UID        PID  PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root         1     0  0  80   0 -  6917 wait   19:55 pts/2    00:00:00 /bin/bash
0 R root        14     1  0  80   0 -  5671 -      19:56 pts/2    00:00:00 ps -elf
</pre>
<p>上面，我们可以看到只有两个进程 ，而且pid=1的进程是我们的/bin/bash。我们还可以看到/proc目录下也干净了很多：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
root@container:~# ls /proc
1          dma          key-users   net            sysvipc
16         driver       kmsg        pagetypeinfo   timer_list
acpi       execdomains  kpagecount  partitions     timer_stats
asound     fb           kpageflags  sched_debug    tty
buddyinfo  filesystems  loadavg     schedstat      uptime
bus        fs           locks       scsi           version
cgroups    interrupts   mdstat      self           version_signature
cmdline    iomem        meminfo     slabinfo       vmallocinfo
consoles   ioports      misc        softirqs       vmstat
cpuinfo    irq          modules     stat           zoneinfo
crypto     kallsyms     mounts      swaps
devices    kcore        mpt         sys
diskstats  keys         mtrr        sysrq-trigger
</pre>
<p>下图，我们也可以看到在子进程中的top命令只看得到两个进程了。</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17020" src="https://coolshell.cn/wp-content/uploads/2015/04/mount.namespace.jpg" alt="" width="570" height="300" srcset="https://coolshell.cn/wp-content/uploads/2015/04/mount.namespace.jpg 740w, https://coolshell.cn/wp-content/uploads/2015/04/mount.namespace-300x158.jpg 300w" sizes="(max-width: 570px) 100vw, 570px" /></p>
<p>这里，多说一下。在通过CLONE_NEWNS创建mount namespace后，父进程会把自己的文件结构复制给子进程中。而子进程中新的namespace中的所有mount操作都只影响自身的文件系统，而不对外界产生任何影响。这样可以做到比较严格地隔离。</p>
<p><!--另外，如果你熟悉mount命令，你会知道，mount命令有以下这些参数：


<ul>


<ol>--make-shared ： 共享方式的mount，主要是为了文件的共享和镜像。</ol>




<ol>--make-slave ： 这种mount方式更大的意义是为了“只读”的场景，也就是从动式的mount。</ol>




<ol>--make-private：这种mount方式主要就是为了隔离。如proc文件系统。</ol>




<ol>--make-unbindable：标记为不可绑定。</ol>


</ul>


--></p>
<p>你可能会问，我们是不是还有别的一些文件系统也需要这样mount? 是的。</p>
<h4>Docker的 Mount Namespace</h4>
<p>下面我将向演示一个“山寨镜像”，其模仿了Docker的Mount Namespace。</p>
<p>首先，我们需要一个rootfs，也就是我们需要把我们要做的镜像中的那些命令什么的copy到一个rootfs的目录下，我们模仿Linux构建如下的目录：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~/rootfs$ ls
bin  dev  etc  home  lib  lib64  mnt  opt  proc  root  run  sbin  sys  tmp  usr  var</pre>
<p>然后，我们把一些我们需要的命令copy到 rootfs/bin目录中（sh命令必需要copy进去，不然我们无法 chroot ）</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
hchen@ubuntu:~/rootfs$ ls ./bin ./usr/bin
 
./bin:
bash   chown  gzip      less  mount       netstat  rm     tabs  tee      top       tty
cat    cp     hostname  ln    mountpoint  ping     sed    tac   test     touch     umount
chgrp  echo   ip        ls    mv          ps       sh     tail  timeout  tr        uname
chmod  grep   kill      more  nc          pwd      sleep  tar   toe      truncate  which

./usr/bin:
awk  env  groups  head  id  mesg  sort  strace  tail  top  uniq  vi  wc  xargs
</pre>
<p>注：你可以使用ldd命令把这些命令相关的那些so文件copy到对应的目录：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
hchen@ubuntu:~/rootfs/bin$ ldd bash
  linux-vdso.so.1 =>  (0x00007fffd33fc000)
  libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f4bd42c2000)
  libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f4bd40be000)
  libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4bd3cf8000)
  /lib64/ld-linux-x86-64.so.2 (0x00007f4bd4504000)
</pre>
<p>下面是我的rootfs中的一些so文件：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
hchen@ubuntu:~/rootfs$ ls ./lib64 ./lib/x86_64-linux-gnu/

./lib64:
ld-linux-x86-64.so.2

./lib/x86_64-linux-gnu/:
libacl.so.1      libmemusage.so         libnss_files-2.19.so    libpython3.4m.so.1
libacl.so.1.1.0  libmount.so.1          libnss_files.so.2       libpython3.4m.so.1.0
libattr.so.1     libmount.so.1.1.0      libnss_hesiod-2.19.so   libresolv-2.19.so
libblkid.so.1    libm.so.6              libnss_hesiod.so.2      libresolv.so.2
libc-2.19.so     libncurses.so.5        libnss_nis-2.19.so      libselinux.so.1
libcap.a         libncurses.so.5.9      libnss_nisplus-2.19.so  libtinfo.so.5
libcap.so        libncursesw.so.5       libnss_nisplus.so.2     libtinfo.so.5.9
libcap.so.2      libncursesw.so.5.9     libnss_nis.so.2         libutil-2.19.so
libcap.so.2.24   libnsl-2.19.so         libpcre.so.3            libutil.so.1
libc.so.6        libnsl.so.1            libprocps.so.3          libuuid.so.1
libdl-2.19.so    libnss_compat-2.19.so  libpthread-2.19.so      libz.so.1
libdl.so.2       libnss_compat.so.2     libpthread.so.0
libgpm.so.2      libnss_dns-2.19.so     libpython2.7.so.1
libm-2.19.so     libnss_dns.so.2        libpython2.7.so.1.0
</pre>
<p>包括这些命令依赖的一些配置文件：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
hchen@ubuntu:~/rootfs$ ls ./etc
bash.bashrc  group  hostname  hosts  ld.so.cache  nsswitch.conf  passwd  profile  
resolv.conf  shadow
</pre>
<p>你现在会说，我靠，有些配置我希望是在容器起动时给他设置的，而不是hard code在镜像中的。比如：/etc/hosts，/etc/hostname，还有DNS的/etc/resolv.conf文件。好的。那我们在rootfs外面，我们再创建一个conf目录，把这些文件放到这个目录中。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~$ ls ./conf
hostname     hosts     resolv.conf</pre>
<p>这样，我们的父进程就可以动态地设置容器需要的这些文件的配置， 然后再把他们mount进容器，这样，容器的镜像中的配置就比较灵活了。</p>
<p>好了，终于到了我们的程序。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

static char container_stack[STACK_SIZE];
char* const container_args[] = {
    "/bin/bash",
    "-l",
    NULL
};

int container_main(void* arg)
{
    printf("Container [%5d] - inside the container!\n", getpid());

    //set hostname
    sethostname("container",10);

    //remount "/proc" to make sure the "top" and "ps" show container's information
    if (mount("proc", "rootfs/proc", "proc", 0, NULL) !=0 ) {
        perror("proc");
    }
    if (mount("sysfs", "rootfs/sys", "sysfs", 0, NULL)!=0) {
        perror("sys");
    }
    if (mount("none", "rootfs/tmp", "tmpfs", 0, NULL)!=0) {
        perror("tmp");
    }
    if (mount("udev", "rootfs/dev", "devtmpfs", 0, NULL)!=0) {
        perror("dev");
    }
    if (mount("devpts", "rootfs/dev/pts", "devpts", 0, NULL)!=0) {
        perror("dev/pts");
    }
    if (mount("shm", "rootfs/dev/shm", "tmpfs", 0, NULL)!=0) {
        perror("dev/shm");
    }
    if (mount("tmpfs", "rootfs/run", "tmpfs", 0, NULL)!=0) {
        perror("run");
    }
    /* 
     * 模仿Docker的从外向容器里mount相关的配置文件 
     * 你可以查看：/var/lib/docker/containers/<container_id>/目录，
     * 你会看到docker的这些文件的。
     */
    if (mount("conf/hosts", "rootfs/etc/hosts", "none", MS_BIND, NULL)!=0 ||
          mount("conf/hostname", "rootfs/etc/hostname", "none", MS_BIND, NULL)!=0 ||
          mount("conf/resolv.conf", "rootfs/etc/resolv.conf", "none", MS_BIND, NULL)!=0 ) {
        perror("conf");
    }
    /* 模仿docker run命令中的 -v, --volume=[] 参数干的事 */
    if (mount("/tmp/t1", "rootfs/mnt", "none", MS_BIND, NULL)!=0) {
        perror("mnt");
    }

    /* chroot 隔离目录 */
    if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){
        perror("chdir/chroot");
    }

    execv(container_args[0], container_args);
    perror("exec");
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    printf("Parent [%5d] - start a container!\n", getpid());
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}
</pre>
<p>sudo运行上面的程序，你会看到下面的挂载信息以及一个所谓的“镜像”：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">
hchen@ubuntu:~$ sudo ./mount 
Parent [ 4517] - start a container!
Container [    1] - inside the container!
root@container:/# mount
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
none on /tmp type tmpfs (rw,relatime)
udev on /dev type devtmpfs (rw,relatime,size=493976k,nr_inodes=123494,mode=755)
devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
tmpfs on /run type tmpfs (rw,relatime)
/dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/hosts type ext4 (rw,relatime,errors=remount-ro,data=ordered)
/dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/hostname type ext4 (rw,relatime,errors=remount-ro,data=ordered)
/dev/disk/by-uuid/18086e3b-d805-4515-9e91-7efb2fe5c0e2 on /etc/resolv.conf type ext4 (rw,relatime,errors=remount-ro,data=ordered)

root@container:/# ls /bin /usr/bin
/bin:
bash   chmod  echo  hostname  less  more  mv   ping  rm   sleep  tail  test    top   truncate  uname
cat    chown  grep  ip        ln    mount  nc   ps    sed  tabs   tar   timeout  touch  tty     which
chgrp  cp     gzip  kill      ls    mountpoint  netstat  pwd   sh   tac    tee   toe    tr   umount

/usr/bin:
awk  env  groups  head  id  mesg  sort  strace  tail  top  uniq  vi  wc  xargs
</pre>
<p>关于如何做一个chroot的目录，这里有个工具叫<a href="https://wiki.ubuntu.com/DebootstrapChroot" target="_blank" rel="noopener noreferrer">DebootstrapChroot</a>，你可以顺着链接去看看（英文的哦）</p>
<p>接下来的事情，你可以自己玩了，我相信你的想像力 。：）</p>
<p>在下一篇，我将向你介绍User Namespace、Network Namespace以及Namespace的其它东西。</p>
<p align="center"><strong> <a title="Docker基础技术：Linux Namespace（下）" href="https://coolshell.cn/articles/17029.html" target="_blank" rel="noopener noreferrer"> &lt;&lt;&lt;&lt; Docker基础技术：Linux Namespace（下）&gt;&gt;&gt;&gt; </a></strong></p>
<p>（上篇完，<a title="Docker基础技术：Linux Namespace（下）" href="https://coolshell.cn/articles/17029.html" target="_blank" rel="noopener noreferrer">请参看下篇</a>）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/jail_cell-150x150.jpg" alt="Docker基础技术：Linux Namespace（下）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17029.html" class="wp_rp_title">Docker基础技术：Linux Namespace（下）</a></li><li ><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1-150x150.png" alt="记一次Kubernetes/Docker网络排障" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_title">记一次Kubernetes/Docker网络排障</a></li><li ><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin-150x150.jpg" alt="Docker基础技术：DeviceMapper" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_title">Docker基础技术：DeviceMapper</a></li><li ><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-150x150.png" alt="Docker基础技术：AUFS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_title">Docker基础技术：AUFS</a></li><li ><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/filter-150x150.png" alt="Docker基础技术：Linux CGroup" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_title">Docker基础技术：Linux CGroup</a></li><li ><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF-150x150.jpeg" alt="eBPF 介绍" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_title">eBPF 介绍</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17010.html">Docker基础技术：Linux Namespace（上）</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17010.html/feed</wfw:commentRss>
			<slash:comments>113</slash:comments>
		
		
			</item>
		<item>
		<title>Docker基础技术：Linux Namespace（下）</title>
		<link>https://coolshell.cn/articles/17029.html</link>
					<comments>https://coolshell.cn/articles/17029.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 16 Apr 2015 02:19:23 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Namespace]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=17029</guid>

					<description><![CDATA[<p>在 Docker基础技术：Linux Namespace（上篇）中我们了解了，UTD、IPC、PID、Mount 四个namespace，我们模仿Docker做...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/17029.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/17029.html">Docker基础技术：Linux Namespace（下）</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-full wp-image-17084" src="https://coolshell.cn/wp-content/uploads/2015/04/jail_cell.jpg" alt="jail_cell" width="350" height="252" srcset="https://coolshell.cn/wp-content/uploads/2015/04/jail_cell.jpg 350w, https://coolshell.cn/wp-content/uploads/2015/04/jail_cell-300x216.jpg 300w" sizes="(max-width: 350px) 100vw, 350px" />在 <strong><a title="Docker基础技术：Linux Namespace（上）" href="https://coolshell.cn/articles/17010.html" target="_blank" rel="noopener noreferrer">Docker基础技术：Linux Namespace（上篇）</a></strong>中我们了解了，UTD、IPC、PID、Mount 四个namespace，我们模仿Docker做了一个相当相当山寨的镜像。在这一篇中，主要想向大家介绍Linux的User和Network的Namespace。</p>
<p>好，下面我们就介绍一下还剩下的这两个Namespace。</p>
<h4>User Namespace</h4>
<p>User Namespace主要是用了CLONE_NEWUSER的参数。使用了这个参数后，内部看到的UID和GID已经与外部不同了，默认显示为65534。那是因为容器找不到其真正的UID所以，设置上了最大的UID（其设置定义在/proc/sys/kernel/overflowuid）。</p>
<p>要把容器中的uid和真实系统的uid给映射在一起，需要修改 <strong>/proc/&lt;pid&gt;/uid_map</strong> 和 <strong>/proc/&lt;pid&gt;/gid_map</strong> 这两个文件。这两个文件的格式为：</p>
<p><code><code></code></code><strong>ID-inside-ns ID-outside-ns length</strong></p>
<p>其中：</p>
<p><span id="more-17029"></span></p>
<ul>
<li>第一个字段ID-inside-ns表示在容器显示的UID或GID，</li>
<li>第二个字段ID-outside-ns表示容器外映射的真实的UID或GID。</li>
<li>第三个字段表示映射的范围，一般填1，表示一一对应。</li>
</ul>
<p>比如，把真实的uid=1000映射成容器内的uid=0</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ cat /proc/2465/uid_map
         0       1000          1</pre>
<p>再比如下面的示例：表示把namespace内部从0开始的uid映射到外部从0开始的uid，其最大范围是无符号32位整形</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">$ cat /proc/$$/uid_map
         0          0          4294967295</pre>
<p>另外，需要注意的是：</p>
<ul>
<li>写这两个文件的进程需要这个namespace中的CAP_SETUID (CAP_SETGID)权限（可参看<a href="http://man7.org/linux/man-pages/man7/capabilities.7.html" target="_blank" rel="noopener noreferrer">Capabilities</a>）</li>
<li>写入的进程必须是此user namespace的父或子的user namespace进程。</li>
<li>另外需要满如下条件之一：1）父进程将effective uid/gid映射到子进程的user namespace中，2）父进程如果有CAP_SETUID/CAP_SETGID权限，那么它将可以映射到父进程中的任一uid/gid。</li>
</ul>
<p>这些规则看着都烦，我们来看程序吧（下面的程序有点长，但是非常简单，如果你读过《Unix网络编程》上卷，你应该可以看懂）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">#define _GNU_SOURCE
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/wait.h&gt;
#include &lt;sys/mount.h&gt;
#include &lt;sys/capability.h&gt;
#include &lt;stdio.h&gt;
#include &lt;sched.h&gt;
#include &lt;signal.h&gt;
#include &lt;unistd.h&gt;

#define STACK_SIZE (1024 * 1024)

static char container_stack[STACK_SIZE];
char* const container_args[] = {
    "/bin/bash",
    NULL
};

int pipefd[2];

void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");
    if (NULL == mapfd) {
        perror("open file error");
        return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}

void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

int container_main(void* arg)
{

    printf("Container [%5d] - inside the container!\n", getpid());

    printf("Container: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());

    /* 等待父进程通知后再往下执行（进程间的同步） */
    char ch;
    close(pipefd[1]);
    read(pipefd[0], &amp;ch, 1);

    printf("Container [%5d] - setup hostname!\n", getpid());
    //set hostname
    sethostname("container",10);

    //remount "/proc" to make sure the "top" and "ps" show container's information
    mount("proc", "/proc", "proc", 0, NULL);

    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    const int gid=getgid(), uid=getuid();

    printf("Parent: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());

    pipe(pipefd);
 
    printf("Parent [%5d] - start a container!\n", getpid());

    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);

    
    printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);

    //To map the uid/gid, 
    //   we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
    //The file format is
    //   ID-inside-ns   ID-outside-ns   length
    //if no mapping, 
    //   the uid will be taken from /proc/sys/kernel/overflowuid
    //   the gid will be taken from /proc/sys/kernel/overflowgid
    set_uid_map(container_pid, 0, uid, 1);
    set_gid_map(container_pid, 0, gid, 1);

    printf("Parent [%5d] - user/group mapping done!\n", getpid());

    /* 通知子进程 */
    close(pipefd[1]);

    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}</pre>
<p>上面的程序，我们用了一个pipe来对父子进程进行同步，为什么要这样做？因为子进程中有一个execv的系统调用，这个系统调用会把当前子进程的进程空间给全部覆盖掉，我们希望在execv之前就做好user namespace的uid/gid的映射，这样，execv运行的/bin/bash就会因为我们设置了uid为0的inside-uid而变成#号的提示符。</p>
<p>整个程序的运行效果如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~$ id
uid=1000(hchen) gid=1000(hchen) groups=1000(hchen)

hchen@ubuntu:~$ ./user #&lt;--以hchen用户运行
Parent: eUID = 1000;  eGID = 1000, UID=1000, GID=1000 
Parent [ 3262] - start a container!
Parent [ 3262] - Container [ 3263]!
Parent [ 3262] - user/group mapping done!
Container [    1] - inside the container!
Container: eUID = 0;  eGID = 0, UID=0, GID=0 #&lt;---Container里的UID/GID都为0了
Container [    1] - setup hostname!

root@container:~# id #&lt;----我们可以看到容器里的用户和命令行提示符是root用户了
uid=0(root) gid=0(root) groups=0(root),65534(nogroup)</pre>
<p>虽然容器里是root，但其实这个容器的/bin/bash进程是以一个普通用户hchen来运行的。这样一来，我们容器的安全性会得到提高。</p>
<p>我们注意到，User Namespace是以普通用户运行，但是别的Namespace需要root权限，那么，如果我要同时使用多个Namespace，该怎么办呢？一般来说，我们先用一般用户创建User Namespace，然后把这个一般用户映射成root，在容器内用root来创建其它的Namesapce。</p>
<h4>Network Namespace</h4>
<p>Network的Namespace比较啰嗦。在Linux下，我们一般用ip命令创建Network Namespace（Docker的源码中，它没有用ip命令，而是自己实现了ip命令内的一些功能——是用了Raw Socket发些“奇怪”的数据，呵呵）。这里，我还是用ip命令讲解一下。</p>
<p>首先，我们先看个图，下面这个图基本上就是Docker在宿主机上的网络示意图（其中的物理网卡并不准确，因为docker可能会运行在一个VM中，所以，这里所谓的“物理网卡”其实也就是一个有可以路由的IP的网卡）</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-17040" src="https://coolshell.cn/wp-content/uploads/2015/04/network.namespace.jpg" alt="network.namespace" width="407" height="300" srcset="https://coolshell.cn/wp-content/uploads/2015/04/network.namespace.jpg 886w, https://coolshell.cn/wp-content/uploads/2015/04/network.namespace-300x221.jpg 300w" sizes="(max-width: 407px) 100vw, 407px" /></p>
<p>上图中，Docker使用了一个私有网段，172.40.1.0，docker还可能会使用10.0.0.0和192.168.0.0这两个私有网段，关键看你的路由表中是否配置了，如果没有配置，就会使用，如果你的路由表配置了所有私有网段，那么docker启动时就会出错了。</p>
<p>当你启动一个Docker容器后，你可以使用ip link show或ip addr show来查看当前宿主机的网络情况（我们可以看到有一个docker0，还有一个veth22a38e6的虚拟网卡——给容器用的）：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c" data-enlighter-highlight="6,8">hchen@ubuntu:~$ ip link show
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state ... 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc ...
    link/ether 00:0c:29:b7:67:7d brd ff:ff:ff:ff:ff:ff
3: docker0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 ...
    link/ether 56:84:7a:fe:97:99 brd ff:ff:ff:ff:ff:ff
5: veth22a38e6: &lt;BROADCAST,UP,LOWER_UP&gt; mtu 1500 qdisc ...
    link/ether 8e:30:2a:ac:8c:d1 brd ff:ff:ff:ff:ff:ff</pre>
<p>那么，要做成这个样子应该怎么办呢？我们来看一组命令：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">## 首先，我们先增加一个网桥lxcbr0，模仿docker0
brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfig lxcbr0 192.168.10.1/24 up #为网桥设置IP地址

## 接下来，我们要创建一个network namespace - ns1

# 增加一个namesapce 命令为 ns1 （使用ip netns add命令）
ip netns add ns1 

# 激活namespace中的loopback，即127.0.0.1（使用ip netns exec ns1来操作ns1中的命令）
ip netns exec ns1   ip link set dev lo up 

## 然后，我们需要增加一对虚拟网卡

# 增加一个pair虚拟网卡，注意其中的veth类型，其中一个网卡要按进容器中
ip link add veth-ns1 type veth peer name lxcbr0.1

# 把 veth-ns1 按到namespace ns1中，这样容器中就会有一个新的网卡了
ip link set veth-ns1 netns ns1

# 把容器里的 veth-ns1改名为 eth0 （容器外会冲突，容器内就不会了）
ip netns exec ns1  ip link set dev veth-ns1 name eth0 

# 为容器中的网卡分配一个IP地址，并激活它
ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up


# 上面我们把veth-ns1这个网卡按到了容器中，然后我们要把lxcbr0.1添加上网桥上
brctl addif lxcbr0 lxcbr0.1

# 为容器增加一个路由规则，让容器可以访问外面的网络
ip netns exec ns1     ip route add default via 192.168.10.1

# 在/etc/netns下创建network namespce名称为ns1的目录，
# 然后为这个namespace设置resolv.conf，这样，容器内就可以访问域名了
mkdir -p /etc/netns/ns1
echo "nameserver 8.8.8.8" &gt; /etc/netns/ns1/resolv.conf</pre>
<p>上面基本上就是docker网络的原理了，只不过，</p>
<ul>
<li>Docker的resolv.conf没有用这样的方式，而是用了<a title="Docker基础技术：Linux Namespace（上）" href="https://coolshell.cn/articles/17010.html" target="_blank" rel="noopener noreferrer">上篇中的Mount Namesapce的那种方式</a></li>
<li>另外，docker是用进程的PID来做Network Namespace的名称的。</li>
</ul>
<p>了解了这些后，你甚至可以为正在运行的docker容器增加一个新的网卡：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">ip link add peerA type veth peer name peerB 
brctl addif docker0 peerA 
ip link set peerA up 
ip link set peerB netns ${container-pid} 
ip netns exec ${container-pid} ip link set dev peerB name eth1 
ip netns exec ${container-pid} ip link set eth1 up ; 
ip netns exec ${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1 ;</pre>
<p>上面的示例是我们为正在运行的docker容器，增加一个eth1的网卡，并给了一个静态的可被外部访问到的IP地址。</p>
<p>这个需要把外部的“物理网卡”配置成混杂模式，这样这个eth1网卡就会向外通过ARP协议发送自己的Mac地址，然后外部的交换机就会把到这个IP地址的包转到“物理网卡”上，因为是混杂模式，所以eth1就能收到相关的数据，一看，是自己的，那么就收到。这样，Docker容器的网络就和外部通了。</p>
<p>当然，无论是Docker的NAT方式，还是混杂模式都会有性能上的问题，NAT不用说了，存在一个转发的开销，混杂模式呢，网卡上收到的负载都会完全交给所有的虚拟网卡上，于是就算一个网卡上没有数据，但也会被其它网卡上的数据所影响。</p>
<p>这两种方式都不够完美，我们知道，真正解决这种网络问题需要使用VLAN技术，于是Google的同学们为Linux内核实现了一个<a href="https://lwn.net/Articles/620087/" target="_blank" rel="noopener noreferrer">IPVLAN的驱动</a>，这基本上就是为Docker量身定制的。</p>
<h4>Namespace文件</h4>
<p>上面就是目前Linux Namespace的玩法。 现在，我来看一下其它的相关东西。</p>
<p>让我们运行一下上篇中的那个pid.mnt的程序（也就是PID Namespace中那个mount proc的程序），然后不要退出。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">$ sudo ./pid.mnt 
[sudo] password for hchen: 
Parent [ 4599] - start a container!
Container [    1] - inside the container!</pre>
<p>我们到另一个shell中查看一下父子进程的PID：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~$ pstree -p 4599
pid.mnt(4599)───bash(4600)</pre>
<p>我们可以到proc下（/proc//ns）查看进程的各个namespace的id（内核版本需要3.8以上）。</p>
<p>下面是父进程的：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~$ sudo ls -l /proc/4599/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -&gt; ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -&gt; mnt:[4026531840]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -&gt; net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -&gt; pid:[4026531836]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -&gt; user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -&gt; uts:[4026531838]</pre>
<p>下面是子进程的：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="shell">hchen@ubuntu:~$ sudo ls -l /proc/4600/ns
total 0
lrwxrwxrwx 1 root root 0  4月  7 22:01 ipc -&gt; ipc:[4026531839]
lrwxrwxrwx 1 root root 0  4月  7 22:01 mnt -&gt; mnt:[4026532520]
lrwxrwxrwx 1 root root 0  4月  7 22:01 net -&gt; net:[4026531956]
lrwxrwxrwx 1 root root 0  4月  7 22:01 pid -&gt; pid:[4026532522]
lrwxrwxrwx 1 root root 0  4月  7 22:01 user -&gt; user:[4026531837]
lrwxrwxrwx 1 root root 0  4月  7 22:01 uts -&gt; uts:[4026532521]</pre>
<p>我们可以看到，其中的ipc，net，user是同一个ID，而mnt,pid,uts都是不一样的。如果两个进程指向的namespace编号相同，就说明他们在同一个namespace下，否则则在不同namespace里面。</p>
<p>这些文件还有另一个作用，那就是，一旦这些文件被打开，只要其fd被占用着，那么就算PID所属的所有进程都已经结束，创建的namespace也会一直存在。比如：我们可以通过：mount &#8211;bind /proc/4600/ns/uts ~/uts 来hold这个namespace。</p>
<p>另外，我们在上篇中讲过一个setns的系统调用，其函数声明如下：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int setns(int fd, int nstype);</pre>
<p>其中第一个参数就是一个fd，也就是一个open()系统调用打开了上述文件后返回的fd，比如：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">fd = open("/proc/4600/ns/nts", O_RDONLY);  // 获取namespace文件描述符
setns(fd, 0); // 加入新的namespace</pre>
<h4>参考文档</h4>
<ul>
<li style="list-style-type: none;">
<ul>
<li><a href="http://lwn.net/Articles/531114/" target="_blank" rel="noopener noreferrer">Namespaces in operation</a></li>
<li><a href="http://man7.org/linux/man-pages/man7/namespaces.7.html" target="_blank" rel="noopener noreferrer">Linux Namespace Man Page</a></li>
<li><a href="http://crosbymichael.com/creating-containers-part-1.html" target="_blank" rel="noopener noreferrer">Creat Containers &#8211; Part 1</a></li>
<li><a href="https://blog.jtlebi.fr/2013/12/22/introduction-to-linux-namespaces-part-1-uts/" target="_blank" rel="noopener noreferrer">Introduction to Linux namespaces</a></li>
</ul>
</li>
</ul>
<p>（应网友card323加入）</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/isolation-150x150.jpg" alt="Docker基础技术：Linux Namespace（上）" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17010.html" class="wp_rp_title">Docker基础技术：Linux Namespace（上）</a></li><li ><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/12/docker-networking-1-150x150.png" alt="记一次Kubernetes/Docker网络排障" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18654.html" class="wp_rp_title">记一次Kubernetes/Docker网络排障</a></li><li ><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/how_to_set_up_an_iSCSI_LUN_with_thin-150x150.jpg" alt="Docker基础技术：DeviceMapper" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17200.html" class="wp_rp_title">Docker基础技术：DeviceMapper</a></li><li ><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/08/docker-filesystems-busyboxrw-150x150.png" alt="Docker基础技术：AUFS" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17061.html" class="wp_rp_title">Docker基础技术：AUFS</a></li><li ><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2015/04/filter-150x150.png" alt="Docker基础技术：Linux CGroup" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17049.html" class="wp_rp_title">Docker基础技术：Linux CGroup</a></li><li ><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/12/eBPF-150x150.jpeg" alt="eBPF 介绍" width="150" height="150" /></a><a href="https://coolshell.cn/articles/22320.html" class="wp_rp_title">eBPF 介绍</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/17029.html">Docker基础技术：Linux Namespace（下）</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/17029.html/feed</wfw:commentRss>
			<slash:comments>51</slash:comments>
		
		
			</item>
		<item>
		<title>vfork 挂掉的一个问题</title>
		<link>https://coolshell.cn/articles/12103.html</link>
					<comments>https://coolshell.cn/articles/12103.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Thu, 20 Nov 2014 16:48:27 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[fork]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Unix]]></category>
		<category><![CDATA[vfork]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=12103</guid>

					<description><![CDATA[<p>在知乎上，有个人问了这样的一个问题——为什么vfork的子进程里用return，整个程序会挂掉，而且exit()不会？并给出了如下的代码，下面的代码一运行就挂掉...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/12103.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/12103.html">vfork 挂掉的一个问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright wp-image-12105" src="https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-298x300.gif" alt="tux-fork" width="199" height="200" srcset="https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-298x300.gif 298w, https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-150x150.gif 150w, https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-200x200.gif 200w, https://coolshell.cn/wp-content/uploads/2014/11/tux-fork-268x270.gif 268w" sizes="(max-width: 199px) 100vw, 199px" />在知乎上，有个人问了这样的<a href="http://www.zhihu.com/question/26591968" target="_blank">一个问题</a>——为什么vfork的子进程里用return，整个程序会挂掉，而且exit()不会？并给出了如下的代码，下面的代码一运行就挂掉了，但如果把子进程的return改成exit(0)就没事。</p>
<p>我受邀后本来不想回答这个问题的，因为这个问题明显就是RTFM的事，后来，发现这个问题放在那里好长时间，而挂在下面的几个答案又跑偏得比较严重，我觉得可能有些朋友看到那样的答案会被误导，所以就上去回答了一下这个问题。</p>
<p>下面我把问题和我的回答发布在这里，也供更多的人查看。</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
int main(void) {
    int var;
    var = 88;
    if ((pid = vfork()) &lt; 0) {
        printf(&quot;vfork error&quot;);
        exit(-1);
    } else if (pid == 0) { /* 子进程 */
        var++;
        return 0;
    }
    printf(&quot;pid=%d, glob=%d, var=%d\n&quot;, getpid(), glob, var);
    return 0;
}
</pre>
<p><span id="more-12103"></span></p>
<h4><b>基础知识</b></h4>
<p>首先说一下fork和vfork的差别：</p>
<ul>
<li>fork 是 创建一个子进程，并把父进程的内存数据copy到子进程中。</li>
<li>vfork是 创建一个子进程，并和父进程的内存数据share一起用。</li>
</ul>
<p>这两个的差别是，一个是copy，一个是share。（关于fork，可以参看酷壳之前的《<a title="一个fork的面试题" href="https://coolshell.cn/articles/7965.html" target="_blank">一道fork的面试题</a>》）</p>
<p>你 man vfork 一下，你可以看到，vfork是这样的工作的，</p>
<p style="padding-left: 30px;">1）保证子进程先执行。<br />
2）当子进程调用exit()或exec()后，父进程往下执行。</p>
<p>那么，为什么要干出一个vfork这个玩意？ 原因在man page也讲得很清楚了：</p>
<blockquote><p><strong>Historic Description</strong></p>
<p>Under Linux, fork(2) is implemented using copy-on-write pages, so the only penalty incurred by fork(2) is the time and memory required to duplicate the parent’s page tables, and to create a unique task structure for the child. <b>However, in the bad old days a fork(2) would require making </b><b>a complete copy of the caller’s data space, often needlessly, since usually immediately afterwards an exec(3) is done. Thus, for greater efficiency, BSD introduced the vfork() system call, which did not fully copy the address space of the parent process, but borrowed the parent’s mem</b><b>ory and thread of control until a call to execve(2) or an exit occurred.</b> The parent process was suspended while the child was using its resources. The use of vfork() was tricky: for example, not modifying data in the parent process depended on knowing which variables are held in a register.</p></blockquote>
<p>意思是这样的—— <b>起初只有fork，但是很多程序在fork一个子进程后就exec一个外部程序，于是fork需要copy父进程的数据这个动作就变得毫无意了，而且这样干还很重</b>（注：后来，fork做了优化，详见本文后面）<b>，所以，BSD搞出了个父子进程共享的 vfork，这样成本比较低。因此，vfork本就是为了exec而生。</b></p>
<h4><b>为什么return会挂掉，exit()不会？</b></h4>
<p>从上面我们知道，<b>结束子进程的调用是exit()而不是return，如果你在vfork中return了，那么，这就意味main()函数return了，注意因为函数栈父子进程共享，所以整个程序的栈就跪了。</b></p>
<p>如果你在子进程中return，那么基本是下面的过程：</p>
<p style="padding-left: 30px;"><b>1）子进程的main() 函数 return了，于是程序的函数栈发生了变化。</b></p>
<p style="padding-left: 30px;"><b>2）而main()函数return后，通常会调用 exit()或相似的函数</b>（如：_exit()，exitgroup()）</p>
<p style="padding-left: 30px;"><b>3）这时，父进程收到子进程exit()，开始从vfork返回，但是尼玛，老子的栈都被你子进程给return干废掉了，你让我怎么执行？</b>（注：栈会返回一个诡异一个栈地址，对于某些内核版本的实现，直接报“栈错误”就给跪了，然而，对于某些内核版本的实现，于是有可能会再次调用main()，于是进入了一个无限循环的结果，直到vfork 调用返回 error）</p>
<p>好了，现在再回到 return 和 exit，return会释放局部变量，并弹栈，回到上级函数执行。exit直接退掉。如果你用c++ 你就知道，return会调用局部对象的析构函数，exit不会。（注：exit不是系统调用，是glibc对系统调用 _exit()或_exitgroup()的封装）</p>
<p>可见，<b>子进程调用exit() 没有修改函数栈，所以，父进程得以顺利执行</b>。</p>
<p><strong>但是！注意！如果你调用 exit() 函数，还是会有问题的，正确的方法应该是调用 _exit() 函数，因为 exit() 函数 会 flush 并 close 所有的 标准 I/O ，这样会导致父进程受到影响。（这个情况在fork下也会受到影响，会导致一些被buffer的数据被flush两次，这里可以参看《<a href="https://coolshell.cn/articles/7965.html" target="_blank">一个fork的面试题</a>》）</strong></p>
<h4>关于fork的优化</h4>
<p>很明显，fork太重，而vfork又太危险，所以，就有人开始优化fork这个系统调用。优化的技术用到了著名的<b>写时拷贝（COW）</b>。</p>
<p>也就是说，<strong>对于fork后并不是马上拷贝内存，而是只有你在需要改变的时候，才会从父进程中拷贝到子进程中，这样fork后立马执行exec的成本就非常小了</strong>。所以，Linux的Man Page中并不鼓励使用vfork() ——</p>
<blockquote><p>“ It is rather unfortunate that Linux revived this specter from the past. The BSD man page states: &#8220;This system call will be eliminated when proper system sharing mechanisms are implemented. Users should not depend on the memory sharing semantics of vfork() as it will, in that case, be made synonymous to fork(2).&#8221;”</p></blockquote>
<p>于是，从BSD4.4开始，他们让vfork和fork变成一样的了</p>
<p>但在后来，NetBSD 1.3 又把传统的vfork给捡了回来，说是vfork的性能在 Pentium Pro 200MHz 的机器（这机器好古董啊）上有可以提高几秒钟的性能。详情见——“<a class=" wrap external" href="http://www.netbsd.org/docs/kernel/vfork.html" target="_blank" rel="nofollow noreferrer">NetBSD Documentation: Why implement traditional vfork()<i class="icon-external"></i></a>”</p>
<p>今天的Linux下，fork和vfork还是各是各的，不过，还是建议你不要用vfork，除非你非常关注性能。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/systemd-1-150x150.jpeg" alt="Linux PID 1 和 Systemd" width="150" height="150" /></a><a href="https://coolshell.cn/articles/17998.html" class="wp_rp_title">Linux PID 1 和 Systemd</a></li><li ><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/08/puzzle-150x150.png" alt="谜题的答案和活动的心得体会" width="150" height="150" /></a><a href="https://coolshell.cn/articles/11847.html" class="wp_rp_title">谜题的答案和活动的心得体会</a></li><li ><a href="https://coolshell.cn/articles/9104.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/02/sed-superman-150x150.png" alt="sed 简明教程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9104.html" class="wp_rp_title">sed 简明教程</a></li><li ><a href="https://coolshell.cn/articles/9070.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/02/awk-150x150.jpg" alt="AWK 简明教程" width="150" height="150" /></a><a href="https://coolshell.cn/articles/9070.html" class="wp_rp_title">AWK 简明教程</a></li><li ><a href="https://coolshell.cn/articles/8883.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2013/01/linux-bash-300x225-150x150.jpg" alt="应该知道的Linux技巧" width="150" height="150" /></a><a href="https://coolshell.cn/articles/8883.html" class="wp_rp_title">应该知道的Linux技巧</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/12103.html">vfork 挂掉的一个问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/12103.html/feed</wfw:commentRss>
			<slash:comments>46</slash:comments>
		
		
			</item>
		<item>
		<title>State Threads 回调终结者</title>
		<link>https://coolshell.cn/articles/12012.html</link>
					<comments>https://coolshell.cn/articles/12012.html#comments</comments>
		
		<dc:creator><![CDATA[Leo]]></dc:creator>
		<pubDate>Sun, 12 Oct 2014 14:48:57 +0000</pubDate>
				<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[Web开发]]></category>
		<category><![CDATA[程序设计]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[coroutine]]></category>
		<category><![CDATA[EDSM]]></category>
		<category><![CDATA[IA]]></category>
		<category><![CDATA[process]]></category>
		<category><![CDATA[thread]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[协程]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=12012</guid>

					<description><![CDATA[<p>（感谢网友 @我的上铺叫路遥 投稿） 上回写了篇《一个“蝇量级”C语言协程库》，推荐了一下Protothreads，通过coroutine模拟了用户级别的mul...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/12012.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/12012.html">State Threads 回调终结者</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>（感谢网友 </strong><a href="http://weibo.com/fullofbull" target="_blank"><strong>@我的上铺叫路遥</strong></a><strong> 投稿）</strong></p>
<p>上回写了篇<a title="一个“蝇量级” C 语言协程库" href="https://coolshell.cn/articles/10975.html" target="_blank">《一个“蝇量级”C语言协程库》</a>，推荐了一下<a title="Protothreads" href="http://dunkels.com/adam/pt/" target="_blank">Protothreads</a>，通过coroutine模拟了用户级别的multi-threading模型，虽然本身足够“轻”，杜绝了系统开销，但这个库本身应用场合主要是内存限制的嵌入式领域，提供原生态组件太少，使用限制太多，比如依赖其它调用产生阻塞等。</p>
<p>这回又替大家在开源界淘了个宝，推荐一个轻量级网络应用框架<strong>State Threads</strong>（以下简称ST），总共也就3000行C代码，跟Protothreads不同在于ST针对的就是<strong>高性能可扩展服务器</strong>领域（值得一提的是Protothreads官网<a title="参考链接" href="http://dunkels.com/adam/pt/links.html" target="_blank">参考链接</a>上第一条就是ST的官网）。在其<a title="FAQ" href="http://state-threads.sourceforge.net/docs/faq.html" target="_blank">FAQ</a>页面上一句引用&#8221;Perfection is achieved not when there is nothing more to add, but rather when there is nothing more to take away.&#8221;可以视为开发人员对ST源码质量的自信。</p>
<h4>历史渊源</h4>
<p>首先介绍一下这个库的历史渊源，从代码贡献者来看，ST不是个人作品，而是有着雄厚的商业支持和应用背景，比如服务器领域，在<a href="http://state-threads.sourceforge.net/news.html" target="_blank">这里</a>你可以看到ST曾作为Apache的多核应用模块发布。其诞生最初是由网景（Netscape）公司的MSPR（Netscape Portable Runtime library）项目中剥离出来，后由SGI（Silicon Graphic Inc）还有Yahoo!公司（前者是主力）开发维护的独立线程库。历史版本方面，作为<a title="SourceForge" href="http://sourceforge.net/projects/state-threads/files/" target="_blank">SourceForge</a>上开源项目，由2001年发布v1.0以来一直到2009年v1.9稳定版后未再变动。在平台移植方面，从Makefile的配置选项中可知ST支持多种Unix-like平台，还有专门针对Win32的源码改写。源码例子中，提供了web server、proxy以及dns三种编程实例供参考。可以说代码质量应该是相当的稳定和可靠的。</p>
<p><span id="more-12012"></span></p>
<p>至于许可证方面，有必要略作说明。出于历史原因，网景最初发布时选择了MPL1.1许可证，而后SGI在维护中又混进了GPLv2许可证，照理说这两种许可证是互不兼容的（MPL1.1后续版本是GPL兼容的），也就是说用双许可证打包发布理论上是非法无效的，见GNU官网上<a title="GPL兼容" href="http://www.gnu.org/licenses/license-list.html#MPL" target="_blank">MPL兼容性</a>一节。但这里有值得商榷的地方，因为文中又提及，根据MPL1.1中某条款第13节，如果整段或部分代码允许采用另一许可证作为备用（alternate）选择，比如GPL及其兼容，那么整个库的许可证就可视为GPL兼容的。如此一来所谓GPL兼容性一般解释为你不能在GPLv2的代码中混入MPL1.1，而不是说你不能在MPL1.1代码中混入GPLv2，也就是说GPLv2在MPL1.1之后是可以接受的，事实上SGI就采用了后面的做法，尚未引起版权上的纠纷。为此我还考证了一下FAQ上<a title="license" href="http://state-threads.sourceforge.net/docs/faq.html" target="_blank">license</a>一节的说法，说ST既可以在MPL和GPL之间选择一种，也可以继续用双许可证，还补了一句在non-free项目使用上也没有限制，但对ST源码所做改动必须对用户可见。在源码文件中的SGI的附加声明还解释了将ST转为GPL代码的做法，就是可以删除前面MPL的声明，否则后续用户仍可以在两者之间二选一。个人觉得既然SGI都这样发话了，那么可解释为反之删除GPL的声明继续采用MPL也是可以接受的，如果你对双许可证承诺仍不放心的话。</p>
<h4>基于事件驱动状态机（EDSM）</h4>
<p>好了，下面该进入技术性话题了。前面说了ST的目标是<strong>高性能可扩展</strong>，其技术特征一言以蔽之就是</p>
<blockquote><p><strong>&#8220;It combines the simplicity of the multi-threaded programming paradigm, in which one thread supports each simultaneous connection, with the performance and scalability of an event-driven state machine (EDSM) architecture.&#8221;</strong></p></blockquote>
<p>我们先来纵向比较ST与传统的EDSM区别，再来横向比较与其它线程库（比如Pthread）的区别（注：以下图片全部来自<a title="ST FAQ" href="http://state-threads.sourceforge.net/docs/faq.html" target="_blank">State Threads Library FAQ</a>）。</p>
<p>传统EDSM最常见的方式就是I/O事件的<strong>异步回调</strong>。基本上都会有一个叫做dispatcher的单线程主循环（又叫event loop），用户通过向dispatcher注册回调函数（又叫event handler）来实现异步通知，从而不必在原地空耗资源干等，在dispatcher主循环中通过select()/poll()系统调用来等待各种I/O事件的发生，当内核检测到事件触发并且数据可达或可用时，select()/poll()会返回从而使dispatcher调用相应的回调函数来对处理用户的请求。所以异步回调与其说是通知，不如说用委托更恰当。</p>
<p>整个过程都是单线程的。<strong>这种处理本质上就是将一堆互不相交（disjoint）的回调实现同步控制，就像串联在一个顺序链表上。</strong>见图1，黑色的双箭头表示I/O事件复用，回调是个筐，里面装着对各种请求的处理（当然不是每个请求都有回调，一个请求也可以对应不同的回调），每个回调被串联起来由dispatcher激活。这里请求等价于thread的概念（不是操作系统的线程），只不过“上下文切换”（context switch）发生在每个回调结束之时（假设不同请求对应不同回调），注册下一个回调以待事件触发时恢复其它请求的处理。至于dispatcher的执行状态（execute state）可作为回调函数的参数保存和传递。</p>
<p><img decoding="async" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2014/10/edsm.gif" alt="EDSM" /></p>
<p>异步回调的缺陷在于<strong>难以实现和扩展</strong>，虽然已经有libevent这样的通用库，以及其它actor/reacotor的设计模式及其框架，但正如Dean Gaudet（Apache开发者）所说：“其内在的复杂性——<strong>将线性思维分解成一堆回调的负担</strong>（breaking up linear thought into a bucketload of callbacks）——仍然存在”。从上图可见，<strong>回调之间请求例程不是连续的，比如回调之间的切换会打断部分请求，又比如有新的请求需要重新注册。</strong></p>
<p><strong>ST本质上仍然是基于EDSM模型，但旨在取代传统的异步回调方式。</strong>ST将请求抽象为thread概念以更接近自然编程模式（所谓的linear thought吧，就像操作系统的线程之间切换那样自然）。ST的调度器（scheduler）对于用户来说是透明的，不像dispatcher那种将执行状态（execute state）暴露给回调方式。每个thread的现场环境可以保存在栈上（一段连续的大小确定的内存空间），由C的运行环境管理。从图2看到，<strong>ST的threads可以并发地线性地处理I/O事件，模型比异步回调简单得多。</strong></p>
<p><img decoding="async" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2014/10/st_edsm.gif" alt="State Threads" /></p>
<p>这里稍微解释一下ST调度工作原理，ST运行环境维护了四种队列，分别是IOQ、RUNQ、SLEEPQ以及ZOMBIEQ，<strong>当每个thread处于不同队列中对应不同的状态（ST顾名思义所谓thread状态机）。</strong>比如polling请求的时候，当前thread就加入IOQ表示等待事件（如果有timeout同时会被放到SLEEPQ中），当事件触发时，thread就从IOQ（如果有timeout同时会从SLEEPQ）移除并转移到RUNQ等待被调度，成为当前的running thread，相当于操作系统的就绪队列，跟传统EDSM对应起来就是注册回调以及激活回调。再比如模拟同步控制wait/sleep/lock的时候，当前thread会被放入SLEEPQ，直到被唤醒或者超时再次进入RUNQ以待调度。</p>
<p><strong>ST的调度具备性能与内存双重优点</strong>：在性能上，ST实现自己的setjmp/longjmp来模拟调度，无任何系统开销，并且context（就是jmp_buf）针对不同平台和架构用底层语言实现的，可移植性媲美libc。下面放一段代码解释一下调度实现：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">/*
 * Switch away from the current thread context by saving its state 
 * and calling the thread scheduler
 */
#define _ST_SWITCH_CONTEXT(_thread)       \
    ST_BEGIN_MACRO                        \
    if (!MD_SETJMP((_thread)-&gt;context)) { \
      _st_vp_schedule();                  \
    }                                     \
    ST_END_MACRO

/*
 * Restore a thread context that was saved by _ST_SWITCH_CONTEXT 
 * or initialized by _ST_INIT_CONTEXT
 */
#define _ST_RESTORE_CONTEXT(_thread)   \
    ST_BEGIN_MACRO                     \
    _ST_SET_CURRENT_THREAD(_thread);   \
    MD_LONGJMP((_thread)-&gt;context, 1); \
    ST_END_MACRO

void _st_vp_schedule(void)
{
    _st_thread_t *thread;

    if (_ST_RUNQ.next != &amp;_ST_RUNQ) {
        /* Pull thread off of the run queue */
        thread = _ST_THREAD_PTR(_ST_RUNQ.next);
        _ST_DEL_RUNQ(thread);
    } else {
        /* If there are no threads to run, switch to the idle thread */
        thread = _st_this_vp.idle_thread;
    }
    ST_ASSERT(thread-&gt;state == _ST_ST_RUNNABLE);

    /* Resume the thread */
    thread-&gt;state = _ST_ST_RUNNING;
    _ST_RESTORE_CONTEXT(thread);
}
</pre>
<p>如果你熟悉setjmp/longjmp的用法，你就知道当前thread在调用MD_SETJMP将现场上下文保存在jmp_buf中并返回返回0，然后自己调用_st_vp_schedule()将自己调度出去。调度器先从RUNQ上找，如果队列为空就找idle thread，这是在整个ST初始化时创建的一个特殊thread，然后将当前线程设为自己，再调用MD_LONGJMP切换到其上次调用MD_SETJMP的地方，从thread-&gt;context恢复现场并返回1，该thread就接着往下执行了。<strong>整个过程就同EDSM一样发生在操作系统单线程下，所以没有任何系统开销与阻塞。</strong></p>
<p><strong>其实真正的阻塞是发生在等待I/O事件复用上，也就是select()/poll()，这是整个ST唯一的系统调用。</strong>ST当前的状态是，整个环境处于空闲状态，所有threads的请求处理都已经完成，也就是RUNQ为空。这时在_st_idle_thread_start维护了一个主循环（类似于event loop），主要负责三种任务：1.对IOQ所有thread进行I/O复用检测；2.对SLEEPQ进行超时检查；3.将idle thread调度出去，代码如下：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">
void *_st_idle_thread_start(void *arg)
{
    _st_thread_t *me = _ST_CURRENT_THREAD();

    while (_st_active_count &gt; 0) {
        /* Idle vp till I/O is ready or the smallest timeout expired */
        _ST_VP_IDLE();

        /* Check sleep queue for expired threads */
        _st_vp_check_clock();

        me-&gt;state = _ST_ST_RUNNABLE;
        _ST_SWITCH_CONTEXT(me);
    }

    /* No more threads */
    exit(0);

    /* NOTREACHED */
    return NULL;
}</pre>
<p>这里的me就是idle thread，因为_st_idle_thread_start就是创建idle thread的启动点，每从上次_ST_SWITCH_CONTEXT()切换回来的时候，接着在_ST_VP_IDLE()里轮询I/O事件的发生，一旦检测到发生了别的thread事件或者SLEEPQ里面发生超时，再用_ST_SWITCH_CONTEXT()把自己切换出去，如果此时RUNQ中非空的话就切换到队列第一个thread。这里主循环是不会退出的。</p>
<p>在内存方面，<strong>ST的执行状态作为局部变量保存在栈上，而不是像回调需要动态分配，</strong>用户可能分别这样使用thread模式和callback模式：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">/* thread land */
int foo()
{
    int local1;
    int local2;
    do_some_io();
}

/* callback land */
struct foo_data {
    int local1;
    int local2;
};

void foo_cb(void *arg)
{
    struct foo_data *locals = arg;
    ...
}

void foo()
{
    struct foo_data *locals = malloc(sizeof(struct foo_data));
    register(foo_cb, locals);
}
</pre>
<h4>基于Mult-Threading范式</h4>
<p>同样基于multi-threading编程范式，ST同其它线程库又有和有点呢？比如Posix Thread（以下简称PThread）是个通用的线程库，它是<strong>将用户级线程（thread）同内核执行对象（kernel execution entity，有些书又叫lightweight processes）做了1:1或m:n映射，</strong>从而实现multi-threading模式。<strong>而ST是单线程（n:1映射），它的thread实际上就是协程（coroutine）。</strong>通常的网络应用上，多线程范式绕不开操作系统，但在某些特定的服务器领域，线程间的共享资源会带来额外复杂度，锁、竞态、并发、文件句柄、全局变量、管道、信号等，面对这些Pthread的灵活性会大打折扣。<strong>而ST的调度是精确的，它只会在明确的I/O和同步函数调用点上发生上下文切换，这正是协程的特性，如此一来ST就不需要互斥保护了，进而也可以放心使用任何静态变量和不可重入库函数了</strong>（这在同样作为协程的Protothreads里是不允许的，因为那是stack-less的，无法保存上下文），极大的简化了编程和调试同时增加了性能。</p>
<p>对于同样用户级线程如GNU Pth和MIT Phread比起来呢？有两点，一是ST的thread是<strong>无优先级的非抢占式调度</strong>，也就是说ST基于EDSM的，每个thread都是事件或数据驱动，迟早会把自己调度出去，而且调度点是明确的，并非按时间片来的，从而简化了thread管理；二是ST会<strong>忽略所有信号处理</strong>，在_st_io_init中会把sigact.sa_handler设为SIG_IGN，这样做是因为将thread资源最小化，避免了signal mask及其系统调用（在ucontext上是避免不了的）。但这并不意味着ST就不能处理信号，实际上ST建议将信号写入pipe的方式转化为普通I/O事件处理，示例详见<a title="signal handling" href="http://state-threads.sourceforge.net/docs/notes.html#signals" target="_blank">这里</a>。</p>
<p>这里顺便说一句，<strong>C语言实现的协程据我所知只有三种方式</strong>：Protothread为代表利用switch-case语义跳转，以ST为代表不依赖libc的setjmp/longjmp上下文切换，以及依赖glibc的ucontext接口（<a title="云风的coroutine" href="https://github.com/cloudwu/coroutine" target="_blank">云风的coroutine</a>）。第一种最轻，但受限最大，第三种耗资源性能慢（陈皓注：glibc的ucontext接口的实现中有一个和信号有关的系统调用，所以会慢，估计在一些情况下会比pthread还慢），目前看来ST是最好使的。</p>
<h4>基于多核环境</h4>
<p>下面来聊聊ST在多核环境下的应用。服务器领域多核的优势在于实现了物理上真正的并发，所以如何充分利用系统优势也是线程库的一大难点。这对ST来说也许正是它的拿手好戏，前面提及ST曾作为Apache的多核引擎模块发布。这里要补充一下前面漏掉的ST的一个重要概念——<strong>虚拟处理器</strong>（virtual processor，简称vp），见图3，多个cpu通过内核的SMP模拟出多个“核”（core），一个core对应一个内核任务（kernel task），同时对应一个用户进程（process），一个process对应ST的一个vp，每个vp下就是ST的thread（是协程不是线程），结合前面所述，vp初始化先创建idle thread，然后根据I/O事件驱动其它threads，这就是ST的多核架构。</p>
<p><img decoding="async" class="aligncenter" src="https://coolshell.cn/wp-content/uploads/2014/10/st_app.gif" alt="multi-core" /></p>
<p>这里要指出的是，<strong>ST只负责自身thread调度，进程管理是应用程序的事情，</strong>也就是说由用户来决定fork多少进程，每个进程分配多少资源，如何进行IPC等。这种架构的好处就是每个vp有自己独立的空间，避免了资源同步竞态（比如杜绝了多进程里的多线程这样混乱的模型）。我们知道这种<strong>基于进程的架构是非常健壮的，一个进程奔溃不会影响到其它进程，同时充分利用多核硬件的高并发。</strong>同时对于具体逻辑业务使用vp里的thread处理，这是基于EDSM的，如此一来做到了<strong>逻辑业务与内核执行对象之间的解耦</strong>，没必要因为1K个连接去创建1K的进程。这就是ST的扩展性和灵活性。</p>
<h4>使用限制</h4>
<p>ST的主要限制在于，应用程序所有I/O操作必须使用ST提供的API，因为只有这样thread才能被调度器管理，并且避免阻塞。</p>
<p>另一个限制在于thread调试，这本身不容易，好在v1.9的ST提供了DEBUG参数，使用TREADQ以及_st_iterate_threads接口检测thread调度情况，用户还可自定义_st_show_thread_stack接口dump每个thread的栈，在GDB使能_st_iterate_threads_flag变量，这些都在Readme中对调试方法有具体说明。按下不表。</p>
<h4>总结</h4>
<p>这篇文章写得有点短了，主要是通过对比来介绍ST的，其实还有大段原理可以讲，大段源码以及实战用例可以贴，但这一下子又写不过来，ST还是有点技术含量的。说白了，<strong>ST的核心思想就是利用multi-threading的简单优雅范式胜过传统异步回调的复杂晦涩实现，又利用EDSM的性能和解耦架构避免了multi-threading在系统上的开销和暗礁。</strong>学习ST告诉我们一个道理：<strong>未来技术的趋势永远都是融合的。</strong></p>
<h4>参考</h4>
<ul>
<li>在<a title="sourceforge源码" href="http://sourceforge.net/projects/state-threads/files/" target="_blank">SourceForge</a>以及<a title="github源码" href="https://github.com/winlinvip/state-threads" target="_blank">github</a>上的源码：前者有历史版本及win32版本，后者只有v1.9。</li>
</ul>
<ul>
<li><a title="State Threads for Internet Applications" href="http://state-threads.sourceforge.net/docs/st.html" target="_blank">State Threads for Internet Applications</a>：介绍原理的，值得一看，<a title="中文翻译" href="http://blog.csdn.net/win_lin/article/details/8242653" target="_blank">这里</a>有篇中文翻译附加单元测试（在单CPU 512M内存上创建数万个thread，CPU占用率约5%，内存约4.3K/thread）。</li>
</ul>
<ul>
<li><a title="State Threads Library FAQ" href="http://state-threads.sourceforge.net/docs/faq.html" target="_blank">State Threads Library FAQ</a>：本文基于此而写。</li>
</ul>
<ul>
<li><a title="API手册" href="http://state-threads.sourceforge.net/docs/reference.html" target="_blank">Complete reference</a>：API完全手册。</li>
</ul>
<ul>
<li><a title="注意事项" href="http://state-threads.sourceforge.net/docs/notes.html" target="_blank">Programing Notes</a>：编程注意事项，包括信号处理，IPC，非网络I/O事件等。</li>
</ul>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="http://coolshell.cn/articles/10975.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/6.jpg" alt="一个“蝇量级” C 语言协程库" width="150" height="150" /></a><a href="http://coolshell.cn/articles/10975.html" class="wp_rp_title">一个“蝇量级” C 语言协程库</a></li><li ><a href="http://coolshell.cn/articles/8711.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/12/200906020837401710-150x150.jpg" alt="程序员疫苗：代码注入" width="150" height="150" /></a><a href="http://coolshell.cn/articles/8711.html" class="wp_rp_title">程序员疫苗：代码注入</a></li><li ><a href="http://coolshell.cn/articles/5987.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="如何设计“找回用户帐号”功能" width="150" height="150" /></a><a href="http://coolshell.cn/articles/5987.html" class="wp_rp_title">如何设计“找回用户帐号”功能</a></li><li ><a href="http://coolshell.cn/articles/8309.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/11.jpg" alt="C/C++语言中闭包的探究及比较" width="150" height="150" /></a><a href="http://coolshell.cn/articles/8309.html" class="wp_rp_title">C/C++语言中闭包的探究及比较</a></li><li ><a href="http://coolshell.cn/articles/8239.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/09/lock_free_bicycle-150x150.jpg" alt="无锁队列的实现" width="150" height="150" /></a><a href="http://coolshell.cn/articles/8239.html" class="wp_rp_title">无锁队列的实现</a></li><li ><a href="http://coolshell.cn/articles/7965.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/07/fork01jpg-150x150.jpg" alt="一个fork的面试题" width="150" height="150" /></a><a href="http://coolshell.cn/articles/7965.html" class="wp_rp_title">一个fork的面试题</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/12012.html">State Threads 回调终结者</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/12012.html/feed</wfw:commentRss>
			<slash:comments>50</slash:comments>
		
		
			</item>
		<item>
		<title>bash代码注入的安全漏洞</title>
		<link>https://coolshell.cn/articles/11973.html</link>
					<comments>https://coolshell.cn/articles/11973.html#comments</comments>
		
		<dc:creator><![CDATA[陈皓]]></dc:creator>
		<pubDate>Sat, 27 Sep 2014 23:56:46 +0000</pubDate>
				<category><![CDATA[Unix/Linux]]></category>
		<category><![CDATA[技术新闻]]></category>
		<category><![CDATA[操作系统]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[Bash]]></category>
		<category><![CDATA[env]]></category>
		<category><![CDATA[export]]></category>
		<category><![CDATA[安全]]></category>
		<category><![CDATA[安全补丁]]></category>
		<category><![CDATA[环境变量]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=11973</guid>

					<description><![CDATA[<p>很多人或许对上半年发生的安全问题“心脏流血”（Heartbleed Bug）事件记忆颇深，这两天，又出现了另外一个“毁灭级”的漏洞——Bash软件安全漏洞。这个...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/11973.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/11973.html">bash代码注入的安全漏洞</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright size-medium wp-image-11979" src="https://coolshell.cn/wp-content/uploads/2014/09/bashbug-300x152.jpg" alt="bashbug" width="300" height="152" srcset="https://coolshell.cn/wp-content/uploads/2014/09/bashbug-300x152.jpg 300w, https://coolshell.cn/wp-content/uploads/2014/09/bashbug.jpg 315w" sizes="(max-width: 300px) 100vw, 300px" />很多人或许对上半年发生的安全问题“心脏流血”（Heartbleed Bug）事件记忆颇深，这两天，又出现了另外一个“毁灭级”的漏洞——Bash软件安全漏洞。这个漏洞由法国GNU/Linux爱好者Stéphane Chazelas所发现。随后，美国电脑紧急应变中心（US-CERT）、红帽以及多家从事安全的公司于周三（北京时间9月24日）发出警告。 关于这个安全漏洞的细节可参看美国政府计算安全的这两个漏洞披露：<a href="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-6271">CVE-2014-6271</a> 和 <a href="http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-7169">CVE-2014-7169</a>。</p>
<p>这个漏洞其实是非常经典的“注入式攻击”，也就是可以向 bash注入一段命令，从bash1.14 到4.3都存在这样的漏洞。我们先来看一下这个安全问题的症状。</p>
<h4>Shellshock (CVE-2014-6271)</h4>
<p>下面是一个简单的测试：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">$ env VAR=&#039;() { :;}; echo Bash is vulnerable!&#039; bash -c &quot;echo Bash Test&quot;</code></p>
<p>如果你发现上面这个命令在你的bash下有这样的输出，那你就说明你的bash是有漏洞的：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW" data-enlighter-highlight="1">Bash is vulnerable!
Bash Test</pre>
<p>简单地看一下，其实就是向环境变量中注入了一段代码 <strong>echo Bash is vulnerable</strong>。关于其中的原理我会在后面给出。</p>
<p>很快，CVE-2014-6271的官方补丁出来的了——<a href="https://lists.gnu.org/archive/html/bug-bash/2014-09/msg00081.html" target="_blank">Bash-4.3 Official Patch 25</a>。</p>
<p><span id="more-11973"></span></p>
<h4>AfterShock &#8211; CVE-2014-7169 （又叫Incomplete fix to Shellshock）</h4>
<p>但随后，马上有人在Twitter上发贴——<a href="http://twitter.com/taviso/statuses/514887394294652929" target="_blank">说这是一个不完整的fix</a>，并给出了相关的攻击方法。</p>
<p><a href="http://twitter.com/taviso/statuses/514887394294652929" target="_blank"><img decoding="async" loading="lazy" class="aligncenter size-medium wp-image-11974" src="https://coolshell.cn/wp-content/uploads/2014/09/bash-300x153.jpg" alt="" width="300" height="153" srcset="https://coolshell.cn/wp-content/uploads/2014/09/bash-300x153.jpg 300w, https://coolshell.cn/wp-content/uploads/2014/09/bash.jpg 582w" sizes="(max-width: 300px) 100vw, 300px" /></a></p>
<p>也就是下面这段测试代码（注意，其中的sh在linux下等价于bash）：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">env X=&#039;() { (a)=&gt;\&#039; sh -c &quot;echo date&quot;; cat echo</code></p>
<p>上面这段代码运行起来会报错，但是它要的就是报错，报错后会在你在当前目录下生成一个echo的文件，这个文件的内容是一个时间文本。下面是上面 这段命令执行出来的样子。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ env X=&#039;() { (a)=&gt;\&#039; sh -c &quot;echo date&quot;; cat echo
sh: X: line 1: syntax error near unexpected token `=&#039;
sh: X: line 1: `&#039;
sh: error importing function definition for `X&#039;
Sat Sep 27 22:06:29 CST 2014</pre>
<p>这段测试脚本代码相当的诡异，就像“天书”一样，我会在后面详细说明这段代码的原理。</p>
<h4>原理和技术细节</h4>
<p>要说清楚这个原理和细节，我们需要从 bash的环境变量开始说起。</p>
<h5>bash的环境变量</h5>
<p>环境变量大家知道吧，这个不用我普及了吧。环境变量是操作系统运行shell中的变量，很多程序会通过环境变量改变自己的执行行为。在bash中要定义一个环境变量的语法很简单（注：=号的前后不能有空格）：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">$ var=&quot;hello world&quot;</code></p>
<p>然后你就可以使用这个变量了，比如：echo $var什么的。但是，我们要知道，这个变量只是一个当前shell的“局部变量”，只在当前的shell进程中可以访问，这个shell进程fork出来的进程是访问不到的。</p>
<p>你可以做这样的测试：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">
$ var=&quot;hello coolshell&quot;
$ echo $var
hello coolshell
$ bash
$ echo $var
</pre>
<p>上面的测试中，第三个命令执行了一个bash，也就是开了一个bash的子进程，你就会发现var不能访问了。</p>
<p>为了要让shell的子进程可以访问，我们需要export一下：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">$ export var=&quot;hello coolshell&quot;</code></p>
<p>这样，这个环境变量就会在其子进程中可见了。</p>
<p>如果你要查看一下有哪些环境变量可以在子进程中可见（也就是是否被export了），你可使用<strong>env</strong>命令。不过，env命令也可以用来定义export的环境变量。如下所示：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">$ env var=&quot;hello haoel&quot;</code></p>
<p>有了这些基础知识还不够，我们还要知道一个基础知识——shell的函数。</p>
<h5>bash的函数</h5>
<p>在bash下定义一个函数很简单，如下所示：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ foo(){ echo &quot;hello coolshell&quot;; }
$ foo
hello coolshell</pre>
<p>有了上面的环境变量的基础知识后，你一定会想试试这个函数是否可以在子进程中调用，答案当然是不行的。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ foo(){ echo &quot;hello coolshell&quot;; }
$ foo
hello coolshell
$ bash
$ foo
bash: foo: command not found</pre>
<p>你看，和环境变量是一样的，如果要在子进程中可以访问的话，那么，还是一样的，需要export，export有个参数 -f，意思是export一个函数。如：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW" data-enlighter-highlight="4">$ foo(){ echo &quot;hello coolshell&quot;; }
$ foo
hello coolshell
$ export -f foo
$ bash
$ foo
hello coolshell</pre>
<p>好了，我讲了这么半天的基础知识，别烦，懂了这些，你才会很容易地理解这两个漏洞是怎么回事。</p>
<p>好，现在要进入正题。</p>
<h5>bash的bug</h5>
<p>从上面我们可以看到，bash的变量和函数用了一模一样的机制，如果你用env命令看一下export出来的东西，你会看到上面我们定义的变量和函数都在，如下所示（我省略了其它的环境变量）：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ env
var=hello coolshell
foo=() { echo &quot;hello coolshell&quot;
}</pre>
<p>原来，都用同样的方式啊——<strong>无论是函数还是变量都是变量啊</strong>。于是，看都不用看bash的源代码，聪明的黑客就能猜得到——<strong>bash判断一个环境变量是不是一个函数，就看它的值是否以&#8221;()&#8221;开始</strong>。于是，一股邪念涌上心头。</p>
<p>黑客定义了这样的环境变量（注：() 和 { 间的空格不能少）：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">$ export X=&#039;() { echo &quot;inside X&quot;; }; echo &quot;outside X&quot;;&#039;</code></p>
<p>env一下，你会看到X已经在了：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ env
X=(){ echo &quot;inside X&quot;; }; echo &quot;outside X&quot;;</pre>
<p>然后，<strong>当我们在当前的bash shell进程下产生一个bash的子进程时，新的子进程会读取父进程的所有export的环境变量，并复制到自己的进程空间中，很明显，上面的X变量的函数的后面还注入了一条命令：echo &#8220;outside X&#8221;，这会在父进程向子进程复制的过程中被执行吗？</strong>（关于fork相关的东西你可以看一下我以前写的《<a title="一个fork的面试题" href="https://coolshell.cn/articles/7965.html" target="_blank">fork的一个面试题</a>》）</p>
<p>答案是肯定的。</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ export X=&#039;() { echo &quot;inside X&quot;; }; echo &quot;outside X&quot;;&#039;
$ bash
outside X</pre>
<p>你看，一个代码注入就这样完成了。这就是bash的bug—— <strong>函数体外面的代码被默认地执行了</strong>。</p>
<p>我们并不一定非要像上面那样创建另一个bash的子进程，我们可以使用bash -c的参数来执行一个bash子进程命令。就像这个安全漏洞的测试脚本一样：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">env VAR=&#039;() { :;}; echo Bash is vulnerable!&#039; bash -c &quot;echo Bash Test&quot;</code></p>
<p>其中，() { :;} 中的冒号就相当于/bin/true，返回true并退出。而bash -c其实就是在spawn一个bash的echo的子进程，用于触发函数体外的echo命令。所以，更为友好一点的测试脚本应该是：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">env VAR=&#039;() { :;}; echo Bash is vulnerable!&#039; bash -c &quot;echo 如果你看到了vulnerable字样说明你的bash有安全问题&quot;</code></p>
<p>OK，你应该明白这个漏洞是怎么一回事了吧。</p>
<h4>bash漏洞的影响有多大</h4>
<p>在网上看到好多人说这个漏洞不大，还说这个事只有那些陈旧的执行CGI脚本的网站才会有，现在已经没有网站用CGI了。我靠，这真是无知者无畏啊。</p>
<p>我举个例子，如果你的网站中有调用操作系统的shell命令，比如你用PHP执行个exec之类的东西。这样的需求是有的，特别是对于一些需要和操作系统交互的重要的后台用于系统管理的程序。于是就会开一个bash的进程来执行。</p>
<p>我们还知道，现在的HTTP服务器基本上都是以子进程式的，所以，其中必然会存在export 一些环境变量的事，而有的环境变量的值是从用户端来的，比如：HTTP_USER_AGENT这样的环境变量，只由浏览器发出的。其实这个变量你想写成什么就写成什么。</p>
<p>于是，我可以把这个HTTP_USER_AGENT的环境变量设置成上述的测试脚本，只不过，我会把echo Bash is vulnerable!这个东西换成别的更为凶残的命令。呵呵。</p>
<p>关于这个漏洞会影响哪些已有的系统，你可以自己Google，几乎所有的报告这个漏洞的文章都说了（比如：<a href="https://securityblog.redhat.com/2014/09/24/bash-specially-crafted-environment-variables-code-injection-attack/" target="_blank">这篇</a>，<a href="https://www.digitalocean.com/community/tutorials/how-to-protect-your-server-against-the-shellshock-bash-vulnerability" target="_blank">这篇</a>），我这里就不复述了。</p>
<p>注：如果你要看看你的网站有没有这样的问题，你可以用这个在线工具测试一下：<a href="http://shellshock.brandonpotter.com/">&#8216;ShellShock&#8217; Bash Vulnerability CVE-2014-6271 Test Tool</a>。</p>
<p>现在，你知道这事可能会很大了吧。还不赶快去打补丁。（注，yum update bash 把bash版本升级到 4.1.2-15.el6_5.2 ，<!-- <strong>但是这个版本还没有fix CVE-2014-7169，载止本文发布之时，目前还没有正式的CVE-2014-7169的补丁，你可以<a href="https://access.redhat.com/security/cve/CVE-2014-7169" target="_blank">关注Redhat的官方关于CVE-2014-7169 的 ticket</a></strong>--> ）</p>
<h4>关于 AfterShock &#8211; CVE-2014-7169 测试脚本的解释</h4>
<p>很多同学没有看懂下面这个测试脚本是什么意思，我这里解释一下。</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">env X=&#039;() { (a)=&gt;\&#039; sh -c &quot;echo date&quot;; cat echo</code></p>
<ul>
<li>X='() { (a)=&gt;\&#8217; 这个不用说了，定义一个X的环境变量。但是，这个函数不完整啊，是的，这是故意的。另外你一定要注意，\&#8217;不是为了单引号的转义，X这个变量的值就是 <strong>() { (a)=&gt;\</strong></li>
</ul>
<ul>
<li>其中的 (a)=这个东西目的就是为了让bash的解释器出错（语法错误）。</li>
</ul>
<ul>
<li>语法出错后，在缓冲区中就会只剩下了 “&gt;\”这两个字符。</li>
</ul>
<ul>
<li>于是，这个神奇的bash会把后面的命令echo date换个行放到这个缓冲区中，然后执行。</li>
</ul>
<p>相当于在shell 下执行了下面这个命令：</p>
<pre data-enlighter-language="shell" class="EnlighterJSRAW">$ &gt;\
echo date</pre>
<p>如果你了解bash，你会知道 \ 是用于命令行上换行的，于是相当于执行了：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW"> $ &gt;echo date</code></p>
<p>这不就是一个重定向么？上述的命令相当于：</p>
<p><code data-enlighter-language="shell" class="EnlighterJSRAW">$ date &gt; echo </code></p>
<p>于是，你的当前目录下会出现一个echo的文件，这个文件的内容就是date命令的输出。</p>
<p><strong>能发现这个种玩法的人真是个变态，完全是为bash的源代码量身定制的一个攻击</strong>。</p>
<p>（全文完）<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="http://coolshell.cn/articles/8619.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/11/shell.01-150x150.png" alt="你可能不知道的Shell" width="150" height="150" /></a><a href="http://coolshell.cn/articles/8619.html" class="wp_rp_title">你可能不知道的Shell</a></li><li ><a href="http://coolshell.cn/articles/1399.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2009/09/bashprompts-hurring-150x150.jpg" alt="8个实用而有趣Bash命令提示行" width="150" height="150" /></a><a href="http://coolshell.cn/articles/1399.html" class="wp_rp_title">8个实用而有趣Bash命令提示行</a></li><li ><a href="http://coolshell.cn/articles/7965.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/07/fork01jpg-150x150.jpg" alt="一个fork的面试题" width="150" height="150" /></a><a href="http://coolshell.cn/articles/7965.html" class="wp_rp_title">一个fork的面试题</a></li><li ><a href="http://coolshell.cn/articles/11021.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/02/Github-Security-150x150.png" alt="从“黑掉Github”学Web安全开发" width="150" height="150" /></a><a href="http://coolshell.cn/articles/11021.html" class="wp_rp_title">从“黑掉Github”学Web安全开发</a></li><li ><a href="http://coolshell.cn/articles/11466.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2014/04/c99-150x150.jpg" alt="C语言的整型溢出问题" width="150" height="150" /></a><a href="http://coolshell.cn/articles/11466.html" class="wp_rp_title">C语言的整型溢出问题</a></li><li ><a href="http://coolshell.cn/articles/6976.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2012/04/61e04755jw1drlo96bsktj-150x150.jpg" alt="谈谈数据安全和云存储" width="150" height="150" /></a><a href="http://coolshell.cn/articles/6976.html" class="wp_rp_title">谈谈数据安全和云存储</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/11973.html">bash代码注入的安全漏洞</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/11973.html/feed</wfw:commentRss>
			<slash:comments>136</slash:comments>
		
		
			</item>
	</channel>
</rss>
