Julia Evans 发布的文章

今天,我喜欢的 meetup 网站上有一篇我超爱的文章!Suchakra Sharma@tuxology 在 twitter/github)的一篇非常棒的关于传统 BPF 和在 Linux 中最新加入的 eBPF 的讨论文章,正是它促使我想去写一个 eBPF 的程序!

这篇文章就是 —— BSD 包过滤器:一个新的用户级包捕获架构

我想在讨论的基础上去写一些笔记,因为,我觉得它超级棒!

开始前,这里有个 幻灯片 和一个 pdf。这个 pdf 非常好,结束的位置有一些链接,在 PDF 中你可以直接点击这个链接。

什么是 BPF?

在 BPF 出现之前,如果你想去做包过滤,你必须拷贝所有的包到用户空间,然后才能去过滤它们(使用 “tap”)。

这样做存在两个问题:

  1. 如果你在用户空间中过滤,意味着你将拷贝所有的包到用户空间,拷贝数据的代价是很昂贵的。
  2. 使用的过滤算法很低效。

问题 #1 的解决方法似乎很明显,就是将过滤逻辑移到内核中。(虽然具体实现的细节并没有明确,我们将在稍后讨论)

但是,为什么过滤算法会很低效?

如果你运行 tcpdump host foo,它实际上运行了一个相当复杂的查询,用下图的这个树来描述它:

评估这个树有点复杂。因此,可以用一种更简单的方式来表示这个树,像这样:

然后,如果你设置 ether.type = IPip.src = foo,你必然明白匹配的包是 host foo,你也不用去检查任何其它的东西了。因此,这个数据结构(它们称为“控制流图” ,或者 “CFG”)是表示你真实希望去执行匹配检查的程序的最佳方法,而不是用前面的树。

为什么 BPF 要工作在内核中

这里的关键点是,包仅仅是个字节的数组。BPF 程序是运行在这些字节的数组之上。它们不允许有循环(loop),但是,它们 可以 有聪明的办法知道 IP 包头(IPv6 和 IPv4 长度是不同的)以及基于它们的长度来找到 TCP 端口:

x = ip_header_length
port = *(packet_start + x + port_offset) 

(看起来不一样,其实它们基本上都相同)。在这个论文/幻灯片上有一个非常详细的虚拟机的描述,因此,我不打算解释它。

当你运行 tcpdump host foo 后,这时发生了什么?就我的理解,应该是如下的过程。

  1. 转换 host foo 为一个高效的 DAG 规则
  2. 转换那个 DAG 规则为 BPF 虚拟机的一个 BPF 程序(BPF 字节码)
  3. 发送 BPF 字节码到 Linux 内核,由 Linux 内核验证它
  4. 编译这个 BPF 字节码程序为一个 原生 native 代码。例如,这是个ARM 上的 JIT 代码 以及 x86 的机器码
  5. 当包进入时,Linux 运行原生代码去决定是否过滤这个包。对于每个需要去处理的包,它通常仅需运行 100 - 200 个 CPU 指令就可以完成,这个速度是非常快的!

现状:eBPF

毕竟 BPF 出现已经有很长的时间了!现在,我们可以拥有一个更加令人激动的东西,它就是 eBPF。我以前听说过 eBPF,但是,我觉得像这样把这些片断拼在一起更好(我在 4 月份的 netdev 上我写了这篇 XDP & eBPF 的文章回复)

关于 eBPF 的一些事实是:

  • eBPF 程序有它们自己的字节码语言,并且从那个字节码语言编译成内核原生代码,就像 BPF 程序一样
  • eBPF 运行在内核中
  • eBPF 程序不能随心所欲的访问内核内存。而是通过内核提供的函数去取得一些受严格限制的所需要的内容的子集
  • 它们 可以 与用户空间的程序通过 BPF 映射进行通讯
  • 这是 Linux 3.18 的 bpf 系统调用

kprobes 和 eBPF

你可以在 Linux 内核中挑选一个函数(任意函数),然后运行一个你写的每次该函数被调用时都运行的程序。这样看起来是不是很神奇。

例如:这里有一个 名为 disksnoop 的 BPF 程序,它的功能是当你开始/完成写入一个块到磁盘时,触发它执行跟踪。下图是它的代码片断:

BPF_HASH(start, struct request *);
void trace_start(struct pt_regs *ctx, struct request *req) {
    // stash start timestamp by request ptr
    u64 ts = bpf_ktime_get_ns();
    start.update(&req, &ts);
}
...
b.attach_kprobe(event="blk_start_request", fn_name="trace_start")
b.attach_kprobe(event="blk_mq_start_request", fn_name="trace_start")

本质上它声明一个 BPF 哈希(它的作用是当请求开始/完成时,这个程序去触发跟踪),一个名为 trace_start 的函数将被编译进 BPF 字节码,然后附加 trace_start 到内核函数 blk_start_request 上。

这里使用的是 bcc 框架,它可以让你写 Python 式的程序去生成 BPF 代码。你可以在 https://github.com/iovisor/bcc 找到它(那里有非常多的示例程序)。

uprobes 和 eBPF

因为我知道可以附加 eBPF 程序到内核函数上,但是,我不知道能否将 eBPF 程序附加到用户空间函数上!那会有更多令人激动的事情。这是 在 Python 中使用一个 eBPF 程序去计数 malloc 调用的示例

附加 eBPF 程序时应该考虑的事情

  • 带 XDP 的网卡(我之前写过关于这方面的文章)
  • tc egress/ingress (在网络栈上)
  • kprobes(任意内核函数)
  • uprobes(很明显,任意用户空间函数??像带调试符号的任意 C 程序)
  • probes 是为 dtrace 构建的名为 “USDT probes” 的探针(像 这些 mysql 探针)。这是一个 使用 dtrace 探针的示例程序
  • JVM
  • 跟踪点
  • seccomp / landlock 安全相关的事情
  • 等等

这个讨论超级棒

在幻灯片里有很多非常好的链接,并且在 iovisor 仓库里有个 LINKS.md。虽然现在已经很晚了,但是我马上要去写我的第一个 eBPF 程序了!


via: https://jvns.ca/blog/2017/06/28/notes-on-bpf---ebpf/

作者:Julia Evans 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

(之前的 gdb 系列文章:gdb 如何工作(2016)三步上手 gdb(2014)

在这周,我发现我可以从 gdb 上调用 C 函数。这看起来很酷,因为在过去我认为 gdb 最多只是一个只读调试工具。

我对 gdb 能够调用函数感到很吃惊。正如往常所做的那样,我在 Twitter 上询问这是如何工作的。我得到了大量的有用答案。我最喜欢的答案是 Evan Klitzke 的示例 C 代码,它展示了 gdb 如何调用函数。代码能够运行,这很令人激动!

我(通过一些跟踪和实验)认为那个示例 C 代码和 gdb 实际上如何调用函数不同。因此,在这篇文章中,我将会阐述 gdb 是如何调用函数的,以及我是如何知道的。

关于 gdb 如何调用函数,还有许多我不知道的事情,并且,在这儿我写的内容有可能是错误的。

从 gdb 中调用 C 函数意味着什么?

在开始讲解这是如何工作之前,我先快速的谈论一下我是如何发现这件令人惊讶的事情的。

假如,你已经在运行一个 C 程序(目标程序)。你可以运行程序中的一个函数,只需要像下面这样做:

  • 暂停程序(因为它已经在运行中)
  • 找到你想调用的函数的地址(使用符号表)
  • 使程序(目标程序)跳转到那个地址
  • 当函数返回时,恢复之前的指令指针和寄存器

通过符号表来找到想要调用的函数的地址非常容易。下面是一段非常简单但能够工作的代码,我在 Linux 上使用这段代码作为例子来讲解如何找到地址。这段代码使用 elf crate。如果我想找到 PID 为 2345 的进程中的 foo 函数的地址,那么我可以运行 elf_symbol_value("/proc/2345/exe", "foo")

fn elf_symbol_value(file_name: &str, symbol_name: &str) -> Result<u64, Box<std::error::Error>> {
    // 打开 ELF 文件 
    let file = elf::File::open_path(file_name).ok().ok_or("parse error")?;
    // 在所有的段 & 符号中循环,直到找到正确的那个
    let sections = &file.sections;
    for s in sections {
        for sym in file.get_symbols(&s).ok().ok_or("parse error")? {
            if sym.name == symbol_name {
                return Ok(sym.value);
            }
        }
    }
    None.ok_or("No symbol found")?
}

这并不能够真的发挥作用,你还需要找到文件的内存映射,并将符号偏移量加到文件映射的起始位置。找到内存映射并不困难,它位于 /proc/PID/maps 中。

总之,找到想要调用的函数地址对我来说很直接,但是其余部分(改变指令指针,恢复寄存器等)看起来就不这么明显了。

你不能仅仅进行跳转

我已经说过,你不能够仅仅找到你想要运行的那个函数地址,然后跳转到那儿。我在 gdb 中尝试过那样做(jump foo),然后程序出现了段错误。毫无意义。

如何从 gdb 中调用 C 函数

首先,这是可能的。我写了一个非常简洁的 C 程序,它所做的事只有 sleep 1000 秒,把这个文件命名为 test.c

#include <unistd.h>

int foo() {
    return 3;
}
int main() {
    sleep(1000);
}

接下来,编译并运行它:

$ gcc -o test  test.c
$ ./test

最后,我们使用 gdb 来跟踪 test 这一程序:

$ sudo gdb -p $(pgrep -f test)
(gdb) p foo()
$1 = 3
(gdb) quit

我运行 p foo() 然后它运行了这个函数!这非常有趣。

这有什么用?

下面是一些可能的用途:

  • 它使得你可以把 gdb 当成一个 C 应答式程序(REPL),这很有趣,我想对开发也会有用
  • 在 gdb 中进行调试的时候展示/浏览复杂数据结构的功能函数(感谢 @invalidop
  • 在进程运行时设置一个任意的名字空间(我的同事 nelhage 对此非常惊讶)
  • 可能还有许多我所不知道的用途

它是如何工作的

当我在 Twitter 上询问从 gdb 中调用函数是如何工作的时,我得到了大量有用的回答。许多答案是“你从符号表中得到了函数的地址”,但这并不是完整的答案。

有个人告诉了我两篇关于 gdb 如何工作的系列文章:原生调试:第一部分原生调试:第二部分。第一部分讲述了 gdb 是如何调用函数的(指出了 gdb 实际上完成这件事并不简单,但是我将会尽力)。

步骤列举如下:

  1. 停止进程
  2. 创建一个新的栈框(远离真实栈)
  3. 保存所有寄存器
  4. 设置你想要调用的函数的寄存器参数
  5. 设置栈指针指向新的 栈框 stack frame
  6. 在内存中某个位置放置一条陷阱指令
  7. 为陷阱指令设置返回地址
  8. 设置指令寄存器的值为你想要调用的函数地址
  9. 再次运行进程!

(LCTT 译注:如果将这个调用的函数看成一个单独的线程,gdb 实际上所做的事情就是一个简单的线程上下文切换)

我不知道 gdb 是如何完成这些所有事情的,但是今天晚上,我学到了这些所有事情中的其中几件。

创建一个栈框

如果你想要运行一个 C 函数,那么你需要一个栈来存储变量。你肯定不想继续使用当前的栈。准确来说,在 gdb 调用函数之前(通过设置函数指针并跳转),它需要设置栈指针到某个地方。

这儿是 Twitter 上一些关于它如何工作的猜测:

我认为它在当前栈的栈顶上构造了一个新的栈框来进行调用!

以及

你确定是这样吗?它应该是分配一个伪栈,然后临时将 sp (栈指针寄存器)的值改为那个栈的地址。你可以试一试,你可以在那儿设置一个断点,然后看一看栈指针寄存器的值,它是否和当前程序寄存器的值相近?

我通过 gdb 做了一个试验:

(gdb) p $rsp
$7 = (void *) 0x7ffea3d0bca8
(gdb) break foo
Breakpoint 1 at 0x40052a
(gdb) p foo()
Breakpoint 1, 0x000000000040052a in foo ()
(gdb) p $rsp
$8 = (void *) 0x7ffea3d0bc00

这看起来符合“gdb 在当前栈的栈顶构造了一个新的栈框”这一理论。因为栈指针($rsp)从 0x7ffea3d0bca8 变成了 0x7ffea3d0bc00 —— 栈指针从高地址往低地址长。所以 0x7ffea3d0bca80x7ffea3d0bc00 的后面。真是有趣!

所以,看起来 gdb 只是在当前栈所在位置创建了一个新的栈框。这令我很惊讶!

改变指令指针

让我们来看一看 gdb 是如何改变指令指针的!

(gdb) p $rip
$1 = (void (*)()) 0x7fae7d29a2f0 <__nanosleep_nocancel+7>
(gdb) b foo
Breakpoint 1 at 0x40052a
(gdb) p foo()
Breakpoint 1, 0x000000000040052a in foo ()
(gdb) p $rip
$3 = (void (*)()) 0x40052a <foo+4>

的确是!指令指针从 0x7fae7d29a2f0 变为了 0x40052afoo 函数的地址)。

我盯着输出看了很久,但仍然不理解它是如何改变指令指针的,但这并不影响什么。

如何设置断点

上面我写到 break foo 。我跟踪 gdb 运行程序的过程,但是没有任何发现。

下面是 gdb 用来设置断点的一些系统调用。它们非常简单。它把一条指令用 cc 代替了(这告诉我们 int3 意味着 send SIGTRAP https://defuse.ca/online-x86-assembler.html),并且一旦程序被打断了,它就把指令恢复为原先的样子。

我在函数 foo 那儿设置了一个断点,地址为 0x400528

PTRACE_POKEDATA 展示了 gdb 如何改变正在运行的程序。

// 改变 0x400528 处的指令
25622 ptrace(PTRACE_PEEKTEXT, 25618, 0x400528, [0x5d00000003b8e589]) = 0
25622 ptrace(PTRACE_POKEDATA, 25618, 0x400528, 0x5d00000003cce589) = 0
// 开始运行程序
25622 ptrace(PTRACE_CONT, 25618, 0x1, SIG_0) = 0
// 当到达断点时获取一个信号
25622 ptrace(PTRACE_GETSIGINFO, 25618, NULL, {si_signo=SIGTRAP, si_code=SI_KERNEL, si_value={int=-1447215360, ptr=0x7ffda9bd3f00}}) = 0
// 将 0x400528 处的指令更改为之前的样子
25622 ptrace(PTRACE_PEEKTEXT, 25618, 0x400528, [0x5d00000003cce589]) = 0
25622 ptrace(PTRACE_POKEDATA, 25618, 0x400528, 0x5d00000003b8e589) = 0

在某处放置一条陷阱指令

当 gdb 运行一个函数的时候,它也会在某个地方放置一条陷阱指令。这是其中一条。它基本上是用 cc 来替换一条指令(int3)。

5908  ptrace(PTRACE_PEEKTEXT, 5810, 0x7f6fa7c0b260, [0x48f389fd89485355]) = 0
5908  ptrace(PTRACE_PEEKTEXT, 5810, 0x7f6fa7c0b260, [0x48f389fd89485355]) = 0
5908 ptrace(PTRACE_POKEDATA, 5810, 0x7f6fa7c0b260, 0x48f389fd894853cc) = 0

0x7f6fa7c0b260 是什么?我查看了进程的内存映射,发现它位于 /lib/x86_64-linux-gnu/libc-2.23.so 中的某个位置。这很奇怪,为什么 gdb 将陷阱指令放在 libc 中?

让我们看一看里面的函数是什么,它是 __libc_siglongjmp 。其他 gdb 放置陷阱指令的地方的函数是 __longjmp___longjmp_chkdl_main_dl_close_worker

为什么?我不知道!也许出于某种原因,当函数 foo() 返回时,它调用 longjmp ,从而 gdb 能够进行返回控制。我不确定。

gdb 如何调用函数是很复杂的!

我将要在这儿停止了(现在已经凌晨 1 点),但是我知道的多一些了!

看起来“gdb 如何调用函数”这一问题的答案并不简单。我发现这很有趣并且努力找出其中一些答案,希望你也能够找到。

我依旧有很多未回答的问题,关于 gdb 是如何完成这些所有事的,但是可以了。我不需要真的知道关于 gdb 是如何工作的所有细节,但是我很开心,我有了一些进一步的理解。


via: https://jvns.ca/blog/2018/01/04/how-does-gdb-call-functions/

作者:Julia Evans 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

大家好!今天,我开始进行我的 ruby 堆栈跟踪项目,我发觉我现在了解了一些关于 gdb 内部如何工作的内容。

最近,我使用 gdb 来查看我的 Ruby 程序,所以,我们将对一个 Ruby 程序运行 gdb 。它实际上就是一个 Ruby 解释器。首先,我们需要打印出一个全局变量的地址:ruby_current_thread

获取全局变量

下面展示了如何获取全局变量 ruby_current_thread 的地址:

$ sudo gdb -p 2983
(gdb) p & ruby_current_thread
$2 = (rb_thread_t **) 0x5598a9a8f7f0 <ruby_current_thread>

变量能够位于的地方有 heap stack 或者程序的 文本段 text 。全局变量是程序的一部分。某种程度上,你可以把它们想象成是在编译的时候分配的。因此,我们可以很容易的找出全局变量的地址。让我们来看看,gdb 是如何找出 0x5598a9a87f0 这个地址的。

我们可以通过查看位于 /proc 目录下一个叫做 /proc/$pid/maps 的文件,来找到这个变量所位于的大致区域。

$ sudo cat /proc/2983/maps | grep bin/ruby
5598a9605000-5598a9886000 r-xp 00000000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby
5598a9a86000-5598a9a8b000 r--p 00281000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby
5598a9a8b000-5598a9a8d000 rw-p 00286000 00:32 323508                     /home/bork/.rbenv/versions/2.1.6/bin/ruby

所以,我们看到,起始地址 5598a96050000x5598a9a8f7f0 很像,但并不一样。哪里不一样呢,我们把两个数相减,看看结果是多少:

(gdb) p/x 0x5598a9a8f7f0 - 0x5598a9605000
$4 = 0x48a7f0

你可能会问,这个数是什么?让我们使用 nm 来查看一下程序的符号表。

sudo nm /proc/2983/exe | grep ruby_current_thread
000000000048a7f0 b ruby_current_thread

我们看到了什么?能够看到 0x48a7f0 吗?是的,没错。所以,如果我们想找到程序中一个全局变量的地址,那么只需在符号表中查找变量的名字,然后再加上在 /proc/whatever/maps 中的起始地址,就得到了。

所以现在,我们知道 gdb 做了什么。但是,gdb 实际做的事情更多,让我们跳过直接转到…

解引用指针

(gdb) p ruby_current_thread
$1 = (rb_thread_t *) 0x5598ab3235b0

我们要做的下一件事就是解引用 ruby_current_thread 这一指针。我们想看一下它所指向的地址。为了完成这件事,gdb 会运行大量系统调用比如:

ptrace(PTRACE_PEEKTEXT, 2983, 0x5598a9a8f7f0, [0x5598ab3235b0]) = 0

你是否还记得 0x5598a9a8f7f0 这个地址?gdb 会问:“嘿,在这个地址中的实际内容是什么?”。2983 是我们运行 gdb 这个进程的 ID。gdb 使用 ptrace 这一系统调用来完成这一件事。

好极了!因此,我们可以解引用内存并找出内存地址中存储的内容。有一些有用的 gdb 命令,比如 x/40w 变量x/40b 变量 分别会显示给定地址的 40 个字/字节。

描述结构

一个内存地址中的内容可能看起来像下面这样。可以看到很多字节!

(gdb) x/40b ruby_current_thread
0x5598ab3235b0: 16  -90 55  -85 -104    85  0   0
0x5598ab3235b8: 32  47  50  -85 -104    85  0   0
0x5598ab3235c0: 16  -64 -55 115 -97 127 0   0
0x5598ab3235c8: 0   0   2   0   0   0   0   0
0x5598ab3235d0: -96 -83 -39 115 -97 127 0   0

这很有用,但也不是非常有用!如果你是一个像我一样的人类并且想知道它代表什么,那么你需要更多内容,比如像这样:

(gdb) p *(ruby_current_thread)
$8 = {self = 94114195940880, vm = 0x5598ab322f20, stack = 0x7f9f73c9c010,
    stack_size = 131072, cfp = 0x7f9f73d9ada0, safe_level = 0,    raised_flag = 0,
    last_status = 8, state = 0, waiting_fd = -1, passed_block = 0x0,
    passed_bmethod_me = 0x0, passed_ci = 0x0,    top_self = 94114195612680,
    top_wrapper = 0, base_block = 0x0, root_lep = 0x0, root_svar = 8, thread_id =
    140322820187904,

太好了。现在就更加有用了。gdb 是如何知道这些所有域的,比如 stack_size ?是从 DWARF 得知的。DWARF 是存储额外程序调试数据的一种方式,从而像 gdb 这样的调试器能够工作的更好。它通常存储为二进制的一部分。如果我对我的 Ruby 二进制文件运行 dwarfdump 命令,那么我将会得到下面的输出:

(我已经重新编排使得它更容易理解)

DW_AT_name                  "rb_thread_struct"
DW_AT_byte_size             0x000003e8
DW_TAG_member
  DW_AT_name                  "self"
  DW_AT_type                  <0x00000579>
  DW_AT_data_member_location  DW_OP_plus_uconst 0
DW_TAG_member
  DW_AT_name                  "vm"
  DW_AT_type                  <0x0000270c>
  DW_AT_data_member_location  DW_OP_plus_uconst 8
DW_TAG_member
  DW_AT_name                  "stack"
  DW_AT_type                  <0x000006b3>
  DW_AT_data_member_location  DW_OP_plus_uconst 16
DW_TAG_member
  DW_AT_name                  "stack_size"
  DW_AT_type                  <0x00000031>
  DW_AT_data_member_location  DW_OP_plus_uconst 24
DW_TAG_member
  DW_AT_name                  "cfp"
  DW_AT_type                  <0x00002712>
  DW_AT_data_member_location  DW_OP_plus_uconst 32
DW_TAG_member
  DW_AT_name                  "safe_level"
  DW_AT_type                  <0x00000066>

所以,ruby_current_thread 的类型名为 rb_thread_struct,它的大小为 0x3e8 (即 1000 字节),它有许多成员项,stack_size 是其中之一,在偏移为 24 的地方,它有类型 3131 是什么?不用担心,我们也可以在 DWARF 信息中查看。

< 1><0x00000031>    DW_TAG_typedef
                      DW_AT_name                  "size_t"
                      DW_AT_type                  <0x0000003c>
< 1><0x0000003c>    DW_TAG_base_type
                      DW_AT_byte_size             0x00000008
                      DW_AT_encoding              DW_ATE_unsigned
                      DW_AT_name                  "long unsigned int"

所以,stack_size 具有类型 size_t,即 long unsigned int,它是 8 字节的。这意味着我们可以查看该栈的大小。

如果我们有了 DWARF 调试数据,该如何分解:

  1. 查看 ruby_current_thread 所指向的内存区域
  2. 加上 24 字节来得到 stack_size
  3. 读 8 字节(以小端的格式,因为是在 x86 上)
  4. 得到答案!

在上面这个例子中是 131072(即 128 kb)。

对我来说,这使得调试信息的用途更加明显。如果我们不知道这些所有变量所表示的额外的元数据,那么我们无法知道存储在 0x5598ab325b0 这一地址的字节是什么。

这就是为什么你可以为你的程序单独安装程序的调试信息,因为 gdb 并不关心从何处获取这些额外的调试信息。

DWARF 令人迷惑

我最近阅读了大量的 DWARF 知识。现在,我使用 libdwarf,使用体验不是很好,这个 API 令人迷惑,你将以一种奇怪的方式初始化所有东西,它真的很慢(需要花费 0.3 秒的时间来读取我的 Ruby 程序的所有调试信息,这真是可笑)。有人告诉我,来自 elfutils 的 libdw 要好一些。

同样,再提及一点,你可以查看 DW_AT_data_member_location 来查看结构成员的偏移。我在 Stack Overflow 上查找如何完成这件事,并且得到这个答案。基本上,以下面这样一个检查开始:

dwarf_whatform(attrs[i], &form, &error);
    if (form == DW_FORM_data1 || form == DW_FORM_data2
        form == DW_FORM_data2 || form == DW_FORM_data4
        form == DW_FORM_data8 || form == DW_FORM_udata) {

继续往前。为什么会有 800 万种不同的 DW_FORM_data 需要检查?发生了什么?我没有头绪。

不管怎么说,我的印象是,DWARF 是一个庞大而复杂的标准(可能是人们用来生成 DWARF 的库稍微不兼容),但是我们有的就是这些,所以我们只能用它来工作。

我能够编写代码并查看 DWARF ,这就很酷了,并且我的代码实际上大多数能够工作。除了程序崩溃的时候。我就是这样工作的。

展开栈路径

在这篇文章的早期版本中,我说过,gdb 使用 libunwind 来展开栈路径,这样说并不总是对的。

有一位对 gdb 有深入研究的人发了大量邮件告诉我,为了能够做得比 libunwind 更好,他们花费了大量时间来尝试如何展开栈路径。这意味着,如果你在程序的一个奇怪的中间位置停下来了,你所能够获取的调试信息又很少,那么你可以对栈做一些奇怪的事情,gdb 会尝试找出你位于何处。

gdb 能做的其他事

我在这儿所描述的一些事请(查看内存,理解 DWARF 所展示的结构)并不是 gdb 能够做的全部事情。阅读 Brendan Gregg 的昔日 gdb 例子,我们可以知道,gdb 也能够完成下面这些事情:

  • 反汇编
  • 查看寄存器内容

在操作程序方面,它可以:

  • 设置断点,单步运行程序
  • 修改内存(这是一个危险行为)

了解 gdb 如何工作使得当我使用它的时候更加自信。我过去经常感到迷惑,因为 gdb 有点像 C,当你输入 ruby_current_thread->cfp->iseq,就好像是在写 C 代码。但是你并不是在写 C 代码。我很容易遇到 gdb 的限制,不知道为什么。

知道使用 DWARF 来找出结构内容给了我一个更好的心智模型和更加正确的期望!这真是极好的!


via: https://jvns.ca/blog/2016/08/10/how-does-gdb-work/

作者:Julia Evans 译者:ucasFL 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

如果你的同事问你一个不太清晰的问题,你会怎么回答?我认为提问题是一种技巧(可以看 如何提出有意义的问题) ,同时,合理地回答问题也是一种技巧,它们都是非常实用的。

一开始 —— 有时向你提问的人不尊重你的时间,这很糟糕。理想情况下,我们假设问你问题的人是一个理性的人并且正在尽力解决问题,而你想帮助他们。和我一起工作的人是这样,我所生活的世界也是这样。当然,现实生活并不是这样。

下面是有助于回答问题的一些方法!

如果他们的提问不清楚,帮他们澄清

通常初学者不会提出很清晰的问题,或者问一些对回答问题没有必要信息的问题。你可以尝试以下方法 澄清问题:

  • 重述为一个更明确的问题来回复他们(“你是想问 X 吗?”)
  • 向他们了解更具体的他们并没有提供的信息 (“你使用 IPv6 ?”)
  • 问是什么导致了他们的问题。例如,有时有些人会进入我的团队频道,询问我们的 服务发现 service discovery 如何工作的。这通常是因为他们试图设置/重新配置服务。在这种情况下,如果问“你正在使用哪种服务?可以给我看看你正在处理的‘拉取请求’吗?”是有帮助的。

这些方法很多来自如何提出有意义的问题中的要点。(尽管我永远不会对某人说“噢,你得先看完《如何提出有意义的问题》这篇文章后再来向我提问)

弄清楚他们已经知道了什么

在回答问题之前,知道对方已经知道什么是非常有用的!

Harold Treen 给了我一个很好的例子:

前几天,有人请我解释 “Redux-Sagas”。与其深入解释,不如说 “它们就像监听 action 的工人线程,并可以让你更新 Redux store。

我开始搞清楚他们对 Redux、action、store 以及其他基本概念了解多少。将这些概念都联系在一起再来解释会容易得多。

弄清楚问你问题的人已经知道什么是非常重要的。因为有时他们可能会对基础概念感到疑惑(“Redux 是什么?”),或者他们可能是专家,但是恰巧遇到了微妙的 极端情况 corner case 。如果答案建立在他们不知道的概念上会令他们困惑,但如果重述他们已经知道的的又会是乏味的。

这里有一个很实用的技巧来了解他们已经知道什么 - 比如可以尝试用“你对 X 了解多少?”而不是问“你知道 X 吗?”。

给他们一个文档

“RTFM” ( “去读那些他妈的手册” Read The Fucking Manual )是一个典型的无用的回答,但事实上如果向他们指明一个特定的文档会是非常有用的!当我提问题的时候,我当然很乐意翻看那些能实际解决我的问题的文档,因为它也可能解决其他我想问的问题。

我认为明确你所给的文档的确能够解决问题是非常重要的,或者至少经过查阅后确认它对解决问题有帮助。否则,你可能将以下面这种情形结束对话(非常常见):

  • Ali:我应该如何处理 X ?
  • Jada:<文档链接>
  • Ali: 这个没有实际解释如何处理 X ,它仅仅解释了如何处理 Y !

如果我所给的文档特别长,我会指明文档中那个我将会谈及的特定部分。bash 手册 有 44000 个字(真的!),所以如果只说“它在 bash 手册中有说明”是没有帮助的 :)

告诉他们一个有用的搜索

在工作中,我经常发现我可以利用我所知道的关键字进行搜索来找到能够解决我的问题的答案。对于初学者来说,这些关键字往往不是那么明显。所以说“这是我用来寻找这个答案的搜索”可能有用些。再次说明,回答时请经检查后以确保搜索能够得到他们所需要的答案 :)

写新文档

人们经常一次又一次地问我的团队同样的问题。很显然这并不是他们的错(他们怎么能够知道在他们之前已经有 10 个人问了这个问题,且知道答案是什么呢?)因此,我们会尝试写新文档,而不是直接回答回答问题。

  1. 马上写新文档
  2. 给他们我们刚刚写好的新文档
  3. 公示

写文档有时往往比回答问题需要花很多时间,但这是值得的。写文档尤其重要,如果:

a. 这个问题被问了一遍又一遍 b. 随着时间的推移,这个答案不会变化太大(如果这个答案每一个星期或者一个月就会变化,文档就会过时并且令人受挫)

解释你做了什么

对于一个话题,作为初学者来说,这样的交流会真让人沮丧:

  • 新人:“嗨!你如何处理 X ?”
  • 有经验的人:“我已经处理过了,而且它已经完美解决了”
  • 新人:”...... 但是你做了什么?!“

如果问你问题的人想知道事情是如何进行的,这样是有帮助的:

  • 让他们去完成任务而不是自己做
  • 告诉他们你是如何得到你给他们的答案的。

这可能比你自己做的时间还要长,但对于被问的人来说这是一个学习机会,因为那样做使得他们将来能够更好地解决问题。

这样,你可以进行更好的交流,像这:

  • 新人:“这个网站出现了错误,发生了什么?”
  • 有经验的人:(2分钟后)“oh 这是因为发生了数据库故障转移”
  • 新人: “你是怎么知道的??!?!?”
  • 有经验的人:“以下是我所做的!”:

    1. 通常这些错误是因为服务器 Y 被关闭了。我查看了一下 $PLACE 但它表明服务器 Y 开着。所以,并不是这个原因导致的。
    2. 然后我查看 X 的仪表盘 ,仪表盘的这个部分显示这里发生了数据库故障转移。
    3. 然后我在日志中找到了相应服务器,并且它显示连接数据库错误,看起来错误就是这里。

如果你正在解释你是如何调试一个问题,解释你是如何发现问题,以及如何找出问题的。尽管看起来你好像已经得到正确答案,但感觉更好的是能够帮助他们提高学习和诊断能力,并了解可用的资源。

解决根本问题

这一点有点棘手。有时候人们认为他们依旧找到了解决问题的正确途径,且他们只要再多一点信息就可以解决问题。但他们可能并不是走在正确的道路上!比如:

  • George:“我在处理 X 的时候遇到了错误,我该如何修复它?”
  • Jasminda:“你是正在尝试解决 Y 吗?如果是这样,你不应该处理 X ,反而你应该处理 Z 。”
  • George:“噢,你是对的!!!谢谢你!我回反过来处理 Z 的。”

Jasminda 一点都没有回答 George 的问题!反而,她猜测 George 并不想处理 X ,并且她是猜对了。这是非常有用的!

如果你这样做可能会产生高高在上的感觉:

  • George:“我在处理 X 的时候遇到了错误,我该如何修复它?”
  • Jasminda:“不要这样做,如果你想处理 Y ,你应该反过来完成 Z 。”
  • George:“好吧,我并不是想处理 Y 。实际上我想处理 X 因为某些原因(REASONS)。所以我该如何处理 X 。”

所以不要高高在上,且要记住有时有些提问者可能已经偏离根本问题很远了。同时回答提问者提出的问题以及他们本该提出的问题都是合理的:“嗯,如果你想处理 X ,那么你可能需要这么做,但如果你想用这个解决 Y 问题,可能通过处理其他事情你可以更好地解决这个问题,这就是为什么可以做得更好的原因。”

询问“那个回答可以解决您的问题吗?”

我总是喜欢在我回答了问题之后核实是否真的已经解决了问题:“这个回答解决了您的问题吗?您还有其他问题吗?”在问完这个之后最好等待一会,因为人们通常需要一两分钟来知道他们是否已经找到了答案。

我发现尤其是问“这个回答解决了您的问题吗”这个额外的步骤在写完文档后是非常有用的。通常,在写关于我熟悉的东西的文档时,我会忽略掉重要的东西而不会意识到它。

结对编程和面对面交谈

我是远程工作的,所以我的很多对话都是基于文本的。我认为这是沟通的默认方式。

今天,我们生活在一个方便进行小视频会议和屏幕共享的世界!在工作时候,在任何时间我都可以点击一个按钮并快速加入与他人的视频对话或者屏幕共享的对话中!

例如,最近有人问如何自动调节他们的服务容量规划。我告诉他们我们有几样东西需要清理,但我还不太确定他们要清理的是什么。然后我们进行了一个简短的视频会话并在 5 分钟后,我们解决了他们问题。

我认为,特别是如果有人真的被困在该如何开始一项任务时,开启视频进行结对编程几分钟真的比电子邮件或者一些即时通信更有效。

不要表现得过于惊讶

这是源自 Recurse Center 的一则法则:不要故作惊讶。这里有一个常见的情景:

  • 某甲:“什么是 Linux 内核”
  • 某乙:“你竟然不知道什么是 Linux 内核?!!!!?!!!????”

某乙的表现(无论他们是否真的如此惊讶)是没有帮助的。这大部分只会让某甲不好受,因为他们确实不知道什么是 Linux 内核。

我一直在假装不惊讶,即使我事实上确实有点惊讶那个人不知道这种东西。

回答问题真的很棒

显然并不是所有方法都是合适的,但希望你能够发现这里有些是有帮助的!我发现花时间去回答问题并教导人们是其实是很有收获的。

特别感谢 Josh Triplett 的一些建议并做了很多有益的补充,以及感谢 Harold Treen、Vaibhav Sagar、Peter Bhat Hatkins、Wesley Aptekar Cassels 和 Paul Gowder 的阅读或评论。


via: https://jvns.ca/blog/answer-questions-well/

作者:Julia Evans 译者:HardworkFish 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

在我刚开始学习 Kubernetes(大约是一年半以前吧?)时,我真的不明白为什么应该去关注它。

在我使用 Kubernetes 全职工作了三个多月后,我才逐渐明白了为什么我应该使用它。(我距离成为一个 Kubernetes 专家还很远!)希望这篇文章对你理解 Kubernetes 能做什么会有帮助!

我将尝试去解释我对 Kubernetes 感兴趣的一些原因,而不去使用 “ 原生云 cloud native ”、“ 编排系统 orchestration ”、“ 容器 container ”,或者任何 Kubernetes 专用的术语 :)。我去解释的这些观点主要来自一位 Kubernetes 操作者/基础设施工程师,因为,我现在的工作就是去配置 Kubernetes 和让它工作的更好。

我不会去尝试解决一些如 “你应该在你的生产系统中使用 Kubernetes 吗?”这样的问题。那是非常复杂的问题。(不仅是因为“生产系统”根据你的用途而总是有不同的要求)

Kubernetes 可以让你无需设置一台新的服务器即可在生产系统中运行代码

我首次被说教使用 Kubernetes 是与我的伙伴 Kamal 的下面的谈话:

大致是这样的:

  • Kamal: 使用 Kubernetes 你可以通过一条命令就能设置一台新的服务器。
  • Julia: 我觉得不太可能吧。
  • Kamal: 像这样,你写一个配置文件,然后应用它,这时候,你就在生产系统中运行了一个 HTTP 服务。
  • Julia: 但是,现在我需要去创建一个新的 AWS 实例,明确地写一个 Puppet 清单,设置服务发现,配置负载均衡,配置我们的部署软件,并且确保 DNS 正常工作,如果没有什么问题的话,至少在 4 小时后才能投入使用。
  • Kamal: 是的,使用 Kubernetes 你不需要做那么多事情,你可以在 5 分钟内设置一台新的 HTTP 服务,并且它将自动运行。只要你的集群中有空闲的资源它就能正常工作!
  • Julia: 这儿一定是一个“坑”。

这里有一种陷阱,设置一个生产用 Kubernetes 集群(在我的经险中)确实并不容易。(查看 Kubernetes 艰难之旅 中去开始使用时有哪些复杂的东西)但是,我们现在并不深入讨论它。

因此,Kubernetes 第一个很酷的事情是,它可能使那些想在生产系统中部署新开发的软件的方式变得更容易。那是很酷的事,而且它真的是这样,因此,一旦你使用一个运作中的 Kubernetes 集群,你真的可以仅使用一个配置文件就在生产系统中设置一台 HTTP 服务(在 5 分钟内运行这个应用程序,设置一个负载均衡,给它一个 DNS 名字,等等)。看起来真的很有趣。

对于运行在生产系统中的代码,Kubernetes 可以提供更好的可见性和可管理性

在我看来,在理解 etcd 之前,你可能不会理解 Kubernetes 的。因此,让我们先讨论 etcd!

想像一下,如果现在我这样问你,“告诉我你运行在生产系统中的每个应用程序,它运行在哪台主机上?它是否状态很好?是否为它分配了一个 DNS 名字?”我并不知道这些,但是,我可能需要到很多不同的地方去查询来回答这些问题,并且,我需要花很长的时间才能搞定。我现在可以很确定地说不需要查询,仅一个 API 就可以搞定它们。

在 Kubernetes 中,你的集群的所有状态 – 运行中的应用程序 (“pod”)、节点、DNS 名字、 cron 任务、 等等 —— 都保存在一个单一的数据库中(etcd)。每个 Kubernetes 组件是无状态的,并且基本是通过下列方式工作的:

  • 从 etcd 中读取状态(比如,“分配给节点 1 的 pod 列表”)
  • 产生变化(比如,“在节点 1 上运行 pod A”)
  • 更新 etcd 中的状态(比如,“设置 pod A 的状态为 ‘running’”)

这意味着,如果你想去回答诸如 “在那个可用区中有多少台运行着 nginx 的 pod?” 这样的问题时,你可以通过查询一个统一的 API(Kubernetes API)去回答它。并且,你可以在每个其它 Kubernetes 组件上运行那个 API 去进行同样的访问。

这也意味着,你可以很容易地去管理每个运行在 Kubernetes 中的任何东西。比如说,如果你想要:

  • 部署实现一个复杂的定制的部署策略(部署一个东西,等待 2 分钟,部署 5 个以上,等待 3.7 分钟,等等)
  • 每当推送到 github 上一个分支,自动化 启动一个新的 web 服务器
  • 监视所有你的运行的应用程序,确保它们有一个合理的内存使用限制。

这些你只需要写一个程序与 Kubernetes API(“controller”)通讯就可以了。

另一个关于 Kubernetes API 的令人激动的事情是,你不会局限于 Kubernetes 所提供的现有功能!如果对于你要部署/创建/监视的软件有你自己的方案,那么,你可以使用 Kubernetes API 去写一些代码去达到你的目的!它可以让你做到你想做的任何事情。

即便每个 Kubernetes 组件都“挂了”,你的代码将仍然保持运行

关于 Kubernetes 我(在各种博客文章中 :))承诺的一件事情是,“如果 Kubernetes API 服务和其它组件‘挂了’也没事,你的代码将一直保持运行状态”。我认为理论上这听起来很酷,但是我不确定它是否真是这样的。

到目前为止,这似乎是真的!

我已经断开了一些正在运行的 etcd,发生了这些情况:

  1. 所有的代码继续保持运行状态
  2. 不能做 新的 事情(你不能部署新的代码或者生成变更,cron 作业将停止工作)
  3. 当它恢复时,集群将赶上这期间它错过的内容

这样做意味着如果 etcd 宕掉,并且你的应用程序的其中之一崩溃或者发生其它事情,在 etcd 恢复之前,它不能够恢复。

Kubernetes 的设计对 bug 很有弹性

与任何软件一样,Kubernetes 也会有 bug。例如,到目前为止,我们的集群控制管理器有内存泄漏,并且,调度器经常崩溃。bug 当然不好,但是,我发现 Kubernetes 的设计可以帮助减轻它的许多核心组件中的错误的影响。

如果你重启动任何组件,将会发生:

  • 从 etcd 中读取所有的与它相关的状态
  • 基于那些状态(调度 pod、回收完成的 pod、调度 cron 作业、按需部署等等),它会去做那些它认为必须要做的事情

因为,所有的组件并不会在内存中保持状态,你在任何时候都可以重启它们,这可以帮助你减轻各种 bug 的影响。

例如,如果在你的控制管理器中有内存泄露。因为,控制管理器是无状态的,你可以每小时定期去重启它,或者,在感觉到可能导致任何不一致的问题发生时重启它。又或者,在调度器中遇到了一个 bug,它有时忘记了某个 pod,从来不去调度它们。你可以每隔 10 分钟来重启调度器来缓减这种情况。(我们并不会这么做,而是去修复这个 bug,但是,你可以这样做 :))

因此,我觉得即使在它的核心组件中有 bug,我仍然可以信任 Kubernetes 的设计可以让我确保集群状态的一致性。并且,总在来说,随着时间的推移软件质量会提高。唯一你必须去操作的有状态的东西就是 etcd。

不用过多地讨论“状态”这个东西 —— 而我认为在 Kubernetes 中很酷的一件事情是,唯一需要去做备份/恢复计划的东西是 etcd (除非为你的 pod 使用了持久化存储的卷)。我认为这样可以使 Kubernetes 运维比你想的更容易一些。

在 Kubernetes 之上实现新的分布式系统是非常容易的

假设你想去实现一个分布式 cron 作业调度系统!从零开始做工作量非常大。但是,在 Kubernetes 里面实现一个分布式 cron 作业调度系统是非常容易的!(仍然没那么简单,毕竟它是一个分布式系统)

我第一次读到 Kubernetes 的 cron 作业控制器的代码时,我对它是如此的简单感到由衷高兴。去读读看,其主要的逻辑大约是 400 行的 Go 代码。去读它吧! => cronjob\_controller.go <=

cron 作业控制器基本上做的是:

  • 每 10 秒钟:

    • 列出所有已存在的 cron 作业
    • 检查是否有需要现在去运行的任务
    • 如果有,创建一个新的作业对象去调度,并通过其它的 Kubernetes 控制器实际运行它
    • 清理已完成的作业
    • 重复以上工作

Kubernetes 模型是很受限制的(它有定义在 etcd 中的资源模式,控制器读取这个资源并更新 etcd),我认为这种相关的固有的/受限制的模型,可以使它更容易地在 Kubernetes 框架中开发你自己的分布式系统。

Kamal 给我说的是 “Kubernetes 是一个写你自己的分布式系统的很好的平台” ,而不是“ Kubernetes 是一个你可以使用的分布式系统”,并且,我觉得它真的很有意思。他做了一个 为你推送到 GitHub 的每个分支运行一个 HTTP 服务的系统 的原型。这花了他一个周末的时间,大约 800 行 Go 代码,我认为它真不可思议!

Kubernetes 可以使你做一些非常神奇的事情(但并不容易)

我一开始就说 “kubernetes 可以让你做一些很神奇的事情,你可以用一个配置文件来做这么多的基础设施,它太神奇了”。这是真的!

为什么说 “Kubernetes 并不容易”呢?是因为 Kubernetes 有很多部分,学习怎么去成功地运营一个高可用的 Kubernetes 集群要做很多的工作。就像我发现它给我了许多抽象的东西,我需要去理解这些抽象的东西才能调试问题和正确地配置它们。我喜欢学习新东西,因此,它并不会使我发狂或者生气,但是我认为了解这一点很重要 :)

对于 “我不能仅依靠抽象概念” 的一个具体的例子是,我努力学习了许多 Linux 上网络是如何工作的,才让我对设置 Kubernetes 网络稍有信心,这比我以前学过的关于网络的知识要多很多。这种方式很有意思但是非常费时间。在以后的某个时间,我或许写更多的关于设置 Kubernetes 网络的困难/有趣的事情。

或者,为了成功设置我的 Kubernetes CA,我写了一篇 2000 字的博客文章,述及了我不得不学习 Kubernetes 不同方式的 CA 的各种细节。

我觉得,像 GKE (Google 的 Kubernetes 产品) 这样的一些监管的 Kubernetes 的系统可能更简单,因为,他们为你做了许多的决定,但是,我没有尝试过它们。


via: https://jvns.ca/blog/2017/10/05/reasons-kubernetes-is-cool/

作者:Julia Evans 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

调试 C 程序,曾让我很困扰。然而当我之前在写我的操作系统时,我有很多的 Bug 需要调试。我很幸运的使用上了 qemu 模拟器,它允许我将调试器附加到我的操作系统。这个调试器就是 gdb

我得解释一下,你可以使用 gdb 先做一些小事情,因为我发现初学它的时候真的很混乱。我们接下来会在一个小程序中,设置断点,查看内存。

1、 设断点

如果你曾经使用过调试器,那你可能已经会设置断点了。

下面是一个我们要调试的程序(虽然没有任何 Bug):

#include <stdio.h>
void do_thing() {
    printf("Hi!\n");
}
int main() {
    do_thing();
}

另存为 hello.c. 我们可以使用 dbg 调试它,像这样:

bork@kiwi ~> gcc -g hello.c -o hello
bork@kiwi ~> gdb ./hello

以上是带调试信息编译 hello.c(为了 gdb 可以更好工作),并且它会给我们醒目的提示符,就像这样:

(gdb)

我们可以使用 break 命令设置断点,然后使用 run 开始调试程序。

(gdb) break do_thing 
Breakpoint 1 at 0x4004f8
(gdb) run
Starting program: /home/bork/hello 

Breakpoint 1, 0x00000000004004f8 in do_thing ()

程序暂停在了 do_thing 开始的地方。

我们可以通过 where 查看我们所在的调用栈。

(gdb) where
#0  do_thing () at hello.c:3
#1  0x08050cdb in main () at hello.c:6
(gdb) 

2、 阅读汇编代码

使用 disassemble 命令,我们可以看到这个函数的汇编代码。棒级了,这是 x86 汇编代码。虽然我不是很懂它,但是 callq 这一行是 printf 函数调用。

(gdb) disassemble do_thing
Dump of assembler code for function do_thing:
   0x00000000004004f4 <+0>:     push   %rbp
   0x00000000004004f5 <+1>:     mov    %rsp,%rbp
=> 0x00000000004004f8 <+4>:     mov    $0x40060c,%edi
   0x00000000004004fd <+9>:     callq  0x4003f0 
   0x0000000000400502 <+14>:    pop    %rbp
   0x0000000000400503 <+15>:    retq 

你也可以使用 disassemble 的缩写 disas

3、 查看内存

当调试我的内核时,我使用 gdb 的主要原因是,以确保内存布局是如我所想的那样。检查内存的命令是 examine,或者使用缩写 x。我们将使用x

通过阅读上面的汇编代码,似乎 0x40060c 可能是我们所要打印的字符串地址。我们来试一下。

(gdb) x/s 0x40060c
0x40060c:        "Hi!"

的确是这样。x/s/s 部分,意思是“把它作为字符串展示”。我也可以“展示 10 个字符”,像这样:

(gdb) x/10c 0x40060c
0x40060c:       72 'H'  105 'i' 33 '!'  0 '\000'        1 '\001'        27 '\033'       3 '\003'        59 ';'
0x400614:       52 '4'  0 '\000'

你可以看到前四个字符是 Hi!\0,并且它们之后的是一些不相关的东西。

我知道 gdb 很多其他的东西,但是我仍然不是很了解它,其中 xbreak 让我获得很多。你还可以阅读 do umentation for examining memory


via: https://jvns.ca/blog/2014/02/10/three-steps-to-learning-gdb/

作者:Julia Evans 译者:Torival 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出