分类 软件开发 下的文章

了解 Java 中的内存管理。

用 C 或 C++ 这样的编程语言写一个应用时,需要编写代码来销毁内存中不再需要的对象。当应用程序扩展得越来越复杂时,未使用对象被忽略释放的可能性就越大。这会导致内存泄露,最终内存耗尽,在某个时刻将没有更多的内存可以分配。结果就是应用程序运行失败并出现 OutOfMemoryError 错误。但在 Java 中, 垃圾收集器 Garbage Collection (GC)会在程序执行过程中自动运行,减轻了手动分配内存和可能的内存泄漏的任务。

垃圾收集器并不只有一种,Java 虚拟机(JVM)有七种不同的垃圾收集器,了解每种垃圾收集器的目的和优点是很有用的。

1、Serial 收集器

Serial threaded garbage collection

垃圾收集器的原始实现,使用单线程。当垃圾收集器运行时,会停止应用程序(通常称为“stop the world”事件)。适用于能够承受短暂停顿的应用程序。该垃圾收集器占用内存空间比较小,因此这是嵌入式应用程序的首选垃圾收集器类型。在运行时使用以下命令启用该垃圾收集器:

$ java -XX:+UseSerialGC

2、Parallel 收集器

Parallel garbage collection

像 Serial 收集器一样,Parallel 收集器也使用“stop the world”方法。这意味着,当垃圾收集器运行时,应用程序线程会停止。但是不同的是,Parallel 收集器运行时有多个线程执行垃圾收集操作。这种类型的垃圾收集器适用于在多线程和多处理器环境中运行中到大型数据集的应用程序。

这是 JVM 中的默认垃圾收集器,也被称为吞吐量收集器。使用该垃圾收集器时可以通过使用各种合适的 JVM 参数进行调优,例如吞吐量、暂停时间、线程数和内存占用。如下:

  • 线程数:-XX:ParallelGCThreads=<N>
  • 暂停时间:-XX:MaxGCPauseMillis=<N>
  • 吞吐量(垃圾收集花费的时间与实际应用程序执行的时间相比):-XX:GCTimeRatio=<N>
  • 最大堆内存:-Xmx<N>

Parallel 收集器可以使用该命令显式启用:java -XX:+UseParallelGC 。使用这个命令,指定在新生代中通过多个线程进行垃圾回收,而老年代中的垃圾收集和内存压缩仍使用单个线程完成的。

还有一个版本的的 Parallel 收集器叫做 “Parallel Old GC”,它对新生代和老年代都使用多线程,启用命令如下:

$ java -XX:+UseParallelOldGC

3、Concurrent Mark Sweep(CMS)收集器

Concurrent garbage collection

Concurrent Mark Sweep(CMS)垃圾收集器与应用程序并行运行。对于新生代和老年代都使用了多线程。在 CMS 垃圾收集器删除无用对象后,不会对存活对象进行内存压缩。该垃圾收集器和应用程序并行运行,会降低应用程序的响应时间,适用于停顿时间较短的应用程序。这个收集器在 Java8 已过时,并在 Java14 中被移除。如果你仍在使用有这个垃圾收集器的 Java 版本,可以使用如下命令启用:

$ java -XX:+UseConcMarkSweepGC

在 CMS 垃圾收集器使用过程中,应用程序将暂停两次。首次暂停发生在标记可直接访问的存活对象时,这个暂停被称为初始标记。第二次暂停发生在 CMS 收集器结束时期,来修正在并发标记过程中,应用程序线程在 CMS 垃圾回收完成后更新对象时被遗漏的对象。这就是所谓的重新标记

4、G1 收集器

Garbage first

G1 垃圾收集器旨在替代 GMS。G1 垃圾收集器具备并行、并发以及增量压缩,且暂停时间较短。与 CMS 收集器使用的内存布局不同,G1 收集器将堆内存划分为大小相同的区域,通过多个线程触发全局标记阶段。标记阶段完成后,G1 知道哪个区域可能大部分是空的,并首选该区域作为清除/删除阶段。

在 G1 收集器中,一个对象如果大小超过半个区域容量会被认为是一个“大对象” 。这些对象被放置在老年代中,在一个被称为“humongous region”的区域中。 启用 G1 收集器的命令如下:

$ java -XX:+UseG1GC

5、Epsilon 收集器

该垃圾收集器是在 Java11 中引入的,是一个 no-op(无操作)收集器。它不做任何实际的内存回收,只负责管理内存分配。Epsilon 只在当你知道应用程序的确切内存占用情况并且不需要垃圾回收时使用。启用命令如下:

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC

6、Shenandoah 收集器

Shenandoah 是在 JDK12 中引入的,是一种 CPU 密集型垃圾收集器。它会进行内存压缩,立即删除无用对象并释放操作系统的空间。所有的这一切与应用程序线程并行发生。启用命令如下:

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

7、ZGC 收集器

ZGC 为低延迟需要和大量堆空间使用而设计,允许当垃圾回收器运行时 Java 应用程序继续运行。ZGC 收集器在 JDK11 引入,在 JDK12 改进。在 JDK15,ZGC 和 Shenandoah 都被移出了实验阶段。启用 ZGC 收集器使用如下命令:

$ java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC

灵活的垃圾收集器

Java 为我们提供了灵活的内存管理方式,熟悉不同的可用方法有助于为正在开发或运行的应用程序选择最合适的内存管理方式。


via: https://opensource.com/article/22/7/garbage-collection-java

作者:Jayashree Huttanagoudar 选题:lkxed 译者:Veryzzj 校对:wxy

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

GNU 调试器是一个发现程序缺陷的强大工具。

如果你是一个程序员,想在你的软件增加某些功能,你首先考虑实现它的方法:例如写一个方法、定义一个类,或者创建新的数据类型。然后你用编译器或解释器可以理解的编程语言来实现这个功能。但是,如果你觉得你所有代码都正确,但是编译器或解释器依然无法理解你的指令怎么办?如果软件大多数情况下都运行良好,但是在某些环境下出现缺陷怎么办?这种情况下,你得知道如何正确使用调试器找到问题的根源。

GNU 调试器 GNU Project Debugger GDB)是一个发现项目缺陷的强大工具。它通过追踪程序运行过程中发生了什么来帮助你发现程序错误或崩溃的原因。(LCTT 校注:GDB 全程是“GNU Project Debugger”,即 “GNU 项目调试器”,但是通常我们简称为“GNU 调试器”)

本文是 GDB 基本用法的实践教程。请跟随示例,打开命令行并克隆此仓库:

git clone https://github.com/hANSIc99/core_dump_example.git

快捷方式

GDB 的每条命令都可以缩短。例如:显示设定的断点的 info break 命令可以被缩短为 i break。你可能在其他地方看到过这种缩写,但在本文中,为了清晰展现使用的函数,我将所写出整个命令。

命令行参数

你可以将 GDB 附加到每个可执行文件。进入你克隆的仓库(core_dump_example),运行 make 进行编译。你现在能看到一个名为 coredump 的可执行文件。(更多信息,请参考我的文章《创建和调试 Linux 的转储文件》。)

要将 GDB 附加到这个可执行文件,请输入: gdb coredump

你的输出应如下所示:

gdb coredump output

返回结果显示没有找到调试符号。

调试信息是 目标文件 object file (可执行文件)的组成部分,调试信息包括数据类型、函数签名、源代码和操作码之间的关系。此时,你有两种选择:

  • 继续调试汇编代码(参见下文“无符号调试”)
  • 使用调试信息进行编译,参见下一节内容

使用调试信息进行编译

为了在二进制文件中包含调试信息,你必须重新编译。打开 Makefile,删除第 9 行的注释标签(#)后重新编译:

CFLAGS =-Wall -Werror -std=c++11 -g

-g 告诉编译器包含调试信息。运行 make clean,接着运行 make,然后再次调用 GDB。你得到如下输出后就可以调试代码了:

GDB output with symbols

新增的调试信息会增加可执行文件的大小。在这种情况下,执行文件增加了 2.5 倍(从 26,088 字节 增加到 65,480 字节)。

输入 run -c1,使用 -c1 开关启动程序。当程序运行到达 State_4 时将崩溃:

gdb output crash on c1 switch

你可以检索有关程序的其他信息,info source 命令提供了当前文件的信息:

gdb info source output

  • 101 行代码
  • 语言: C++
  • 编译器(版本、调优、架构、调试标志、语言标准)
  • 调试格式:DWARF 2
  • 没有预处理器宏指令(使用 GCC 编译时,宏仅在 使用 -g3 标志编译 时可用)。

info shared 命令打印了动态库列表机器在虚拟地址空间的地址,它们在启动时被加载到该地址,以便程序运行:

gdb info shared output

如果你想了解 Linux 中的库处理方式,请参见我的文章 在 Linux 中如何处理动态库和静态库

调试程序

你可能已经注意到,你可以在 GDB 中使用 run 命令启动程序。run 命令接受命令行参数,就像从控制台启动程序一样。-c1 开关会导致程序在第 4 阶段崩溃。要从头开始运行程序,你不用退出 GDB,只需再次运行 run 命令。如果没有 -c1 开关,程序将陷入死循环,你必须使用 Ctrl+C 来结束死循环。

gdb output stopped by sigint

你也可以一步一步运行程序。在 C/C++ 中,入口是 main 函数。使用 list main 命令打开显示 main 函数的部分源代码:

gdb output list main

main 函数在第 33 行,因此可以输入 break 33 在 33 行添加断点:

gdb output breakpoint added

输入 run 运行程序。正如预期的那样,程序在 main 函数处停止。输入 layout src 并排查看源代码:

gdb output break at main

你现在处于 GDB 的文本用户界面(TUI)模式。可以使用键盘向上和向下箭头键滚动查看源代码。

GDB 高亮显示当前执行行。你可以输入 nextn)命令逐行执行命令。如果你没有指定新的命令,GBD 会执行上一条命令。要逐行运行代码,只需按回车键。

有时,你会发现文本的输出有点显示不正常:

gdb output corrupted

如果发生这种情况,请按 Ctrl+L 重置屏幕。

使用 Ctrl+X+A 可以随时进入和退出 TUI 模式。你可以在手册中找到 其他的键绑定

要退出 GDB,只需输入 quit

设置监察点

这个示例程序的核心是一个在无限循环中运行的状态机。n_state 变量枚举了当前所有状态:

while(true){
        switch(n_state){
        case State_1:
                std::cout << "State_1 reached" << std::flush;
                n_state = State_2;
                break;
        case State_2:
                std::cout << "State_2 reached" << std::flush;
                n_state = State_3;
                break;
        
        (.....)
        
        }
}

如果你希望当 n_state 的值为 State_5 时停止程序。为此,请在 main 函数处停止程序并为 n_state 设置监察点:

watch n_state == State_5

只有当所需的变量在当前上下文中可用时,使用变量名设置监察点才有效。

当你输入 continue 继续运行程序时,你会得到如下输出:

gdb output stop on watchpoint_1

如果你继续运行程序,当监察点表达式评估为 false 时 GDB 将停止:

gdb output stop on watchpoint_2

你可以为一般的值变化、特定的值、读取或写入时来设置监察点。

更改断点和监察点

输入 info watchpoints 打印先前设置的监察点列表:

gdb output info watchpoints

删除断点和监察点

如你所见,监察点就是数字。要删除特定的监察点,请先输入 delete 后输入监察点的编号。例如,我的监察点编号为 2;要删除此监察点,输入 delete 2

注意: 如果你使用 delete 而没有指定数字,所有 监察点和断点将被删除。

这同样适用于断点。在下面的截屏中,我添加了几个断点,输入 info breakpoint 打印断点列表:

gdb output info breakpoints

要删除单个断点,请先输入 delete 后输入断点的编号。另外一种方式:你可以通过指定断点的行号来删除断点。例如,clear 78 命令将删除第 78 行设置的断点号 7。

禁用或启用断点和监察点

除了删除断点或监察点之外,你可以通过输入 disable,后输入编号禁用断点或监察点。在下文中,断点 3 和 4 被禁用,并在代码窗口中用减号标记:

disabled breakpoints

也可以通过输入类似 disable 2 - 4 修改某个范围内的断点或监察点。如果要重新激活这些点,请输入 enable,然后输入它们的编号。

条件断点

首先,输入 delete 删除所有断点和监察点。你仍然想使程序停在 main 函数处,如果你不想指定行号,可以通过直接指明该函数来添加断点。输入 break main 从而在 main 函数处添加断点。

输入 run 从头开始运行程序,程序将在 main 函数处停止。

main 函数包括变量 n_state_3_count,当状态机达到状态 3 时,该变量会递增。

基于 n_state_3_count 的值添加一个条件断点,请输入:

break 54 if n_state_3_count == 3

Set conditional breakpoint

继续运行程序。程序将在第 54 行停止之前运行状态机 3 次。要查看 n_state_3_count 的值,请输入:

print n_state_3_count

print variable

使断点成为条件断点

你也可以使现有断点成为条件断点。用 clear 54 命令删除最近添加的断点,并通过输入 break 54 命令添加一个简单的断点。你可以输入以下内容使此断点成为条件断点:

condition 3 n_state_3_count == 9

3 指的是断点编号。

modify breakpoint

在其他源文件中设置断点

如果你的程序由多个源文件组成,你可以在行号前指定文件名来设置断点,例如,break main. cpp:54

捕捉点

除了断点和监察点之外,你还可以设置捕获点。捕获点适用于执行系统调用、加载共享库或引发异常等事件。

要捕获用于写入 STDOUT 的 write 系统调用,请输入:

catch syscall write

catch syscall write output

每当程序写入控制台输出时,GDB 将中断执行。

在手册中,你可以找到一整章关于 断点、监察点和捕捉点 的内容。

评估和操作符号

print 命令可以打印变量的值。一般语法是 print <表达式> <值>。修改变量的值,请输入:

set variable <variable-name> <new-value>.

在下面的截屏中,我将变量 n_state_3_count 的值设为 123

catch syscall write output

/x 表达式以十六进制打印值;使用 & 运算符,你可以打印虚拟地址空间内的地址。

如果你不确定某个符号的数据类型,可以使用 whatis 来查明。

whatis output

如果你要列出 main 函数范围内可用的所有变量,请输入 info scope main :

info scope main output

DW_OP_fbreg 值是指基于当前子程序的堆栈偏移量。

或者,如果你已经在一个函数中并且想要列出当前堆栈帧上的所有变量,你可以使用 info locals :

info locals output

查看手册以了解更多 检查符号 的内容。

附加调试到一个正在运行的进程

gdb attach <进程 ID> 命令允许你通过指定进程 ID(PID)附加到一个已经在运行的进程进行调试。幸运的是,coredump 程序将其当前 PID 打印到屏幕上,因此你不必使用 pstop 手动查找 PID。

启动 coredump 应用程序的一个实例:

./coredump

coredump application

操作系统显示 PID 为 2849。打开一个单独的控制台窗口,移动到 coredump 应用程序的根目录,然后用 GDB 附加到该进程进行调试:

gdb attach 2849

attach GDB to coredump

当你用 GDB 附加到进程时,GDB 会立即停止进程运行。输入 layout srcbacktrace 来检查调用堆栈:

layout src and backtrace output

输出显示在 main.cpp 第 92 行调用 std::this_thread::sleep_for<...>(. ..) 函数时进程中断。

只要你退出 GDB,该进程将继续运行。

你可以在 GDB 手册中找到有关 附加调试正在运行的进程 的更多信息。

在堆栈中移动

在命令窗口,输入 up 两次可以在堆栈中向上移动到 main.cpp :

moving up the stack to main.cpp

通常,编译器将为每个函数或方法创建一个子程序。每个子程序都有自己的栈帧,所以在栈帧中向上移动意味着在调用栈中向上移动。

你可以在手册中找到有关 堆栈计算 的更多信息。

指定源文件

当调试一个已经在运行的进程时,GDB 将在当前工作目录中寻找源文件。你也可以使用 目录命令 手动指定源目录。

评估转储文件

阅读 创建和调试 Linux 的转储文件 了解有关此主题的信息。

参考文章太长,简单来说就是:

  1. 假设你使用的是最新版本的 Fedora
  2. 使用 -c1 开关调用 coredump:coredump -c1

Crash meme

  1. 使用 GDB 加载最新的转储文件:coredumpctl debug
  2. 打开 TUI 模式并输入 layout src

coredump output

backtrace 的输出显示崩溃发生在距离 main.cpp 五个栈帧之外。回车直接跳转到 main.cpp 中的错误代码行:

up 5 output

看源码发现程序试图释放一个内存管理函数没有返回的指针。这会导致未定义的行为并引起 SIGABRT

无符号调试

如果没有源代码,调试就会变得非常困难。当我在尝试解决逆向工程的挑战时,我第一次体验到了这一点。了解一些 汇编语言 的知识会很有用。

我们用例子看看它是如何运行的。

找到根目录,打开 Makefile,然后像下面一样编辑第 9 行:

CFLAGS =-Wall -Werror -std=c++11 #-g

要重新编译程序,先运行 make clean,再运行 make,最后启动 GDB。该程序不再有任何调试符号来引导源代码的走向。

no debugging symbols

info file 命令显示二进制文件的内存区域和入口点:

info file output

.text 区段始终从入口点开始,其中包含实际的操作码。要在入口点添加断点,输入 break *0x401110 然后输入 run 开始运行程序:

breakpoint at the entry point

要在某个地址设置断点,使用取消引用运算符 * 来指定地址。

选择反汇编程序风格

在深入研究汇编之前,你可以选择要使用的 汇编风格。 GDB 默认是 AT&T,但我更喜欢 Intel 语法。变更风格如下:

set disassembly-flavor intel

changing assembly flavor

现在输入 layout asm 调出汇编代码窗口,输入 layout reg 调出寄存器窗口。你现在应该看到如下输出:

layout asm and layout reg output

保存配置文件

尽管你已经输入了许多命令,但实际上还没有开始调试。如果你正在大量调试应用程序或尝试解决逆向工程的难题,则将 GDB 特定设置保存在文件中会很有用。

该项目的 GitHub 存储库中的 gdbinit 配置文件包含最近使用的命令:

set disassembly-flavor intel
set write on
break *0x401110
run -c2
layout asm
layout reg

set write on 命令使你能够在程序运行期间修改二进制文件。

退出 GDB 并使用配置文件重新启动 GDB : gdb -x gdbinit coredump

阅读指令

应用 c2 开关后,程序将崩溃。程序在入口函数处停止,因此你必须写入 continue 才能继续运行:

continuing execution after crash

idiv 指令进行整数除法运算:RAX 寄存器中为被除数,指定参数为除数。商被加载到 RAX 寄存器中,余数被加载到 RDX 中。

从寄存器角度,你可以看到 RAX 包含 5,因此你必须找出存储堆栈中位置为 rbp-0x4 的值。

读取内存

要读取原始内存内容,你必须指定比读取符号更多的参数。在汇编输出中向上滚动一点,可以看到堆栈的划分:

stack division output

你最感兴趣的应该是 rbp-0x4 的值,因为它是 idiv 的存储参数。你可以从截图中看到rbp-0x8 位置的下一个变量,所以 rbp-0x4 位置的变量是 4 字节宽。

在 GDB 中,你可以使用 x 命令查看任何内存内容:

x/ < 可选参数 nfu > < 内存地址 addr >

可选参数:

  • n:单元大小的重复计数(默认值:1)
  • f:格式说明符,如 printf
  • u:单元大小

    • b:字节
    • h:半字(2 个字节)
    • w: 字(4 个字节)(默认)
    • g: 双字(8 个字节)

要打印 rbp-0x4 的值,请输入 x/u $rbp-4 :

print value

如果你能记住这种模式,则可以直接查看内存。参见手册中的 查看内存 部分。

操作汇编

子程序 zeroDivide() 发生运算异常。当你用向上箭头键向上滚动一点时,你会找到下面信息:

0x401211 <_Z10zeroDividev>              push   rbp
0x401212 <_Z10zeroDividev+1>            mov    rbp,rsp

这被称为 函数前言

  1. 调用函数的基指针(rbp)存放在栈上
  2. 栈指针(rsp)的值被加载到基指针(rbp

完全跳过这个子程序。你可以使用 backtrace 查看调用堆栈。在 main 函数之前只有一个堆栈帧,所以你可以用一次 up 回到 main :

Callstack assembly

在你的 main 函数中,你会找到下面信息:

0x401431 <main+497>     cmp    BYTE PTR [rbp-0x12],0x0
0x401435 <main+501>     je     0x40145f <main+543>
0x401437 <main+503>     call   0x401211<_Z10zeroDividev>

子程序 zeroDivide() 仅在 jump equal (je)true 时进入。你可以轻松地将其替换为 jump-not-equal (jne) 指令,该指令的操作码为 0x75(假设你使用的是 x86/64 架构;其他架构上的操作码不同)。输入 run 重新启动程序。当程序在入口函数处停止时,设置操作码:

set *(unsigned char*)0x401435 = 0x75

最后,输入 continue 。该程序将跳过子程序 zeroDivide() 并且不会再崩溃。

总结

你会在许多集成开发环境(IDE)中发现 GDB 运行在后台,包括 Qt Creator 和 VSCodium 的 本地调试 扩展。

GDB in VSCodium

了解如何充分利用 GDB 的功能很有用。一般情况下,并非所有 GDB 的功能都可以在 IDE 中使用,因此你可以从命令行使用 GDB 的经验中受益。


via: https://opensource.com/article/21/1/gnu-project-debugger

作者:Stephan Avenwedde 选题:lkxed 译者:Maisie-x 校对:wxy

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

R 语言有非常多的绘图和数据可视化的包,比如 graphicslatticeggplot2 等。这是 R 语言系列的第 9 篇文章,我们会介绍 R 中用来绘图的各种函数。

本文使用的 R 是 4.1.2 版本,运行环境为 Parabola GNU/Linux-libre (x86-64)。

$ R --version

R version 4.1.2 (2021-11-01) -- "Bird Hippie"
Copyright (C) 2021 The R Foundation for Statistical Computing
Platform: x86_64-pc-linux-gnu (64-bit)

R 是自由软件,没有任何担保责任。只要遵守 GNU 通用公共许可证的版本 2 或者版本 3,你就可以对它进行(修改和)再分发。详情见 https://www.gnu.org/licenses/

折线图

我们以印度全境消费者物价指数(CPI -- 乡村/城市)数据集为研究对象,它可以从 https://data.gov.in/catalog/all-india-consumer-price-index-ruralurban-0 下载。选择“截止到 2021 年 11 月” 的版本,用 read.csv 函数读取下载好的文件,如下所示:

> cpi <- read.csv(file="CPI.csv", sep=",")

> head(cpi)
Sector Year Name Andhra.Pradesh Arunachal.Pradesh Assam Bihar
1 Rural 2011 January 104 NA 104 NA
2 Urban 2011 January 103 NA 103 NA
3 Rural+Urban 2011 January 103 NA 104 NA
4 Rural 2011 February 107 NA 105 NA
5 Urban 2011 February 106 NA 106 NA
6 Rural+Urban 2011 February 105 NA 105 NA
Chattisgarh Delhi Goa Gujarat Haryana Himachal.Pradesh Jharkhand Karnataka
1 105 NA 103 104 104 104 105 104
2 104 NA 103 104 104 103 104 104
3 104 NA 103 104 104 103 105 104
4 107 NA 105 106 106 105 107 106
5 106 NA 105 107 107 105 107 108
6 105 NA 104 105 106 104 106 106
...

以 Punjab 州为例,对每年各月份的 CPI 值求和,然后用 plot 函数画一张折线图:

> punjab <- aggregate(x=cpi$Punjab, by=list(cpi$Year), FUN=sum)

> head(punjab)
Group.1 x
1 2011 3881.76
2 2012 4183.30
3 2013 4368.40
4 2014 4455.50
5 2015 4584.30
6 2016 4715.80

> plot(punjab$Group.1, punjab$x, type="l", main="Punjab Consumer Price Index upto November 2021", xlab="Year", ylab="Consumer Price Index")

plot 函数可以传入如下参数:

参数描述
x向量类型,用于绘制 x 轴的数据
y向量或列表类型,用于绘制 y 轴的数据
type设置绘图类型:p 画点;l 画线;o 同时画点和线,且相互重叠;s 画阶梯线;h 画铅垂线
xlimx 轴范围
ylimy 轴范围
main标题
sub副标题
xlabx 轴标题
ylaby 轴标题
axes逻辑型,是否绘制坐标轴

结果如图 1。

Figure 1: Line chart

自相关图

自相关图能在时序分析中展示一个变量是否具有自相关性,可以用 R 中的 acf 函数绘制。acf 函数可以设置三种自相关类型:correlationcovariancepartial。图 2 是 Punjab 州 CPI 值的自相关图,x 表示 CPI。

acf(punjab$x,main='x')

Figure 2: ACF chart

acf 函数可以传入以下参数:

参数描述
x一个单变量或多变量的时序对象,或者一个数值向量或数值矩阵
lag.max最大滞后阶数
type字符型,设置所计算的自相关类型:correlationcovariancepartial
plot逻辑性,若 TRUE 则绘制图像,若 FALSE 则打印传入数据的描述信息
i一组要保留的时差滞后
j一组要保留的名称或数字

柱状图

R 中画柱状图的函数是 barplot。下面的代码用来画 Punjab 州 CPI 的柱状图,如图3:

> barplot(punjab$x, main="Punjab Consumer Price Index", sub="Upto November 2021", xlab="Year", ylab="Consumer Price Index", col="navy")

Figure 3: Line chart of Punjab's CPI

barplot 函数的使用方法非常灵活,可以传入以下参数:

参数描述
height数值向量或数值矩阵,包含用于绘图的数据
width数值向量,用于设置柱宽
space柱间距
beside逻辑型,若 FALSE 则绘制堆积柱状图,若 TRUE 则绘制并列柱状图
density数值型,设置阴影线的填充密度(条数/英寸),默认为 NULL,即不填充阴影线
angle数值型,填充线条的角度,默认为 45
border柱形边缘的颜色
main标题
sub副标题
xlabx 轴标题
ylaby 轴标题
xlimx 轴范围
ylimy 轴范围
axes逻辑型,是否绘制坐标轴

help 命令可以查看 barplot 函数的详细信息:

> help(barplot)

barplot                package:graphics                R Documentation

Bar Plots

Description:

     Creates a bar plot with vertical or horizontal bars.

Usage:

     barplot(height, ...)

     ## Default S3 method:
     barplot(height, width = 1, space = NULL,
             names.arg = NULL, legend.text = NULL, beside = FALSE,
             horiz = FALSE, density = NULL, angle = 45,
             col = NULL, border = par("fg"),
             main = NULL, sub = NULL, xlab = NULL, ylab = NULL,
             xlim = NULL, ylim = NULL, xpd = TRUE, log = "",
             axes = TRUE, axisnames = TRUE,
             cex.axis = par("cex.axis"), cex.names = par("cex.axis"),
             inside = TRUE, plot = TRUE, axis.lty = 0, offset = 0,
             add = FALSE, ann = !add && par("ann"), args.legend = NULL, ...)

     ## S3 method for class 'formula'
     barplot(formula, data, subset, na.action,
             horiz = FALSE, xlab = NULL, ylab = NULL, ...)

饼图

绘制饼图时要多加注意,因为饼图不一定能展示出各扇形间的区别。(LCTT 译注:根据统计学家和一些心理学家的调查结果,这种以比例展示数据的统计图形 实际上是很糟糕的可视化方式,因此,R 关于饼图的帮助文件中清楚地说明了并不推荐使用饼图,而是使用条形图或点图作为替代。) 用 subset 函数获得 Gujarat 州在 2021 年 1 月 Rural、Urban、Rurual+Urban 的 CPI 值:

> jan2021 <- subset(cpi, Name=="January" & Year=="2021")

> jan2021$Gujarat
[1] 153.9 151.2 149.1

> names <- c('Rural', 'Urban', 'Rural+Urban')

使用 pie 函数为 Gujarat 州的 CPI 值生成饼图,如下所示:

> pie(jan2021$Gujarat, names, main="Gujarat CPI Rural and Urban Pie Chart")

Figure 4: Pie chart

pie 函数可以传入以下参数:

参数描述
`x元素大于 0 的数值向量
label字符向量,用于设置每个扇形的标签
radius饼图的半径
clockwise逻辑型,若 TRUE 则顺时针绘图,若 FALSE 则逆时针绘图
density数值型,设置阴影线的填充密度(条数/英寸),默认为 NULL,即不填充阴影线
angle数值型,填充线条的角度,默认为 45
col数值向量,用于设置颜色
lty每个扇形的线条类型
main标题

箱线图

(LCTT 译注:箱线图主要是 从四分位数的角度出发 描述数据的分布,它通过最大值(Q4)、上四分位数(Q3)、中位数(Q2)、下四分位数(Q1) 和最小值(Q0)五处位置来获取一维数据的分布概况。我们知道,这五处位置之间依次包含了四段数据,每段中数据量均为总数据量的 1/4。通过每一段数据占据的长度,我们可以大致推断出数据的集中或离散趋势。长度越短,说明数据在该区间上越密集,反之则稀疏。)

箱线图能够用“ 须线 whisker ” 展示一个变量的 四分位距 Interquartile Range (简称 IQR=Q3-Q1)。用上下四分位数分别加/减内四分位距,再乘以一个人为设定的倍数 range(见下面的参数列表),得到 range * c(Q1-IQR, Q3+IQR),超过这个范围的数据点就被视作离群点,在图中直接以点的形式表示出来。

boxplot 函数可以传入以下参数:

参数描述
data数据框或列表,用于参数类型为公式的情况
x数值向量或者列表,若为列表则对列表中每一个子对象依次作出箱线图
width设置箱子的宽度
outline逻辑型,设置是否绘制离群点
names设置每个箱子的标签
border设置每个箱子的边缘的颜色
range延伸倍数,设置箱线图末端(须)延伸到什么位置
plot逻辑型,设置是否生成图像,若 TRUE 则生成图像,若 FALSE 则打印传入数据的描述信息
horizontal逻辑型,设置箱线图是否水平放置

boxplot 函数绘制部分州的箱线图:

> names <- c ('Andaman and Nicobar', 'Lakshadweep', 'Delhi', 'Goa', 'Gujarat', 'Bihar')
> boxplot(cpi$Andaman.and.Nicobar, cpi$Lakshadweep, cpi$Delhi, cpi$Goa, cpi$Gujarat, cpi$Bihar, names=names)

Figure 5: Box plot

QQ 图

QQ 图 Quantile-Quantile plot 可以用来对比两个数据集,也可以用来检查数据是否服从某种理论分布。qqnorm 函数能绘制正态分布 QQ 图,可以检验数据是否服从正态分布,用下面的代码绘制 Punjab 州 CPI 数据的 QQ 图:

> qqnorm(punjab$x)

Figure 6: Q-Q plot

qqline 函数可以向正态分布 QQ 图上添加理论分布曲线,它可以传入以下参数:

参数描述
x第一个数据样本
y第二个数据样本
datax逻辑型,设置是否以 x 轴表示理论曲线的值,默认为 FALSE
probs长度为 2 的数值向量,代表概率
xlabx 轴标题
ylaby 轴标题
qtype[1,9] 内的整数,设置分位计算类型,详情见 help(quantile) 的类型小节

等高图

等高图可以描述三维数据,在 R 中对应的函数是 contour,这个函数也可以用来向已有的图表添加等高线。等高图常与其他图表一起使用。我们用 contour 对 R 中的 volcano 数据集(奥克兰的火山地形信息)绘制等高图,代码如下:

> contour(volcano)

Figure 7: Volcano

contour 函数的常用参数如下:

参数描述
x,yz 中数值对应的点在平面上的位置
z数值向量
nlevels设置等高线的条数,调整等高线的疏密
labels等高线上的标记字符串,默认是高度的数值
xlim设置 x 轴的范围
ylim设置 y 轴的范围
zlim设置 z 轴的范围
axes设置是否绘制坐标轴
col设置等高线的颜色
lty设置线条的类型
lwd设置线条的粗细
vfont设置标签字体

等高线之间的区域可以用颜色填充,每种颜色表示一个高度范围,如下所示:

> filled.contour(volcano, asp = 1)
# asp 为图形纵横比,即 y 轴上的 1 单位长度和 x 轴上 1 单位长度的比率

填充结果见图 8。

Figure 8: Filled volcano

掌握上述内容后,你可以尝试 R 语言 graphics 包中的其他函数和图表(LCTT 译注:用 help(package=graphics) 可以查看 graphics 包提供的函数列表)。


via: https://www.opensourceforu.com/2022/05/plotting-data-in-r-graphs/

作者:Shakthi Kannan 选题:lkxed 译者:tanloong 校对:wxy

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

学习如何用静态链接库将多个 C 目标文件结合到一个单个的可执行文件之中。

使用 C 编写的应用程序时,通常有多个源码文件,但最终你需要编译成单个的可执行文件。

你可以通过两种方式来完成这项工作:通过创建一个 静态 static 库 或 一个 动态 dynamic 库(也被称为 共享 shared 库)。从创建和链接的方式来看,它们是两种不同类型的库。选择使用哪种方式取决于你的的具体场景。

上一篇文章 中,我演示了如何创建一个动态链接的可执行文件,这是一种更通用的方法。在这篇文章中,我将说明如何创建一个静态链接的可执行文件。

使用静态库链接器

链接器 linker 是一个命令,它将一个程序的多个部分结合在一起,并为它们重新组织内存分配。

链接器的功能包括:

  • 整合一个程序的所有的部分
  • 计算出一个新的内存组织结构,以便所有的部分组合在一起
  • 恢复内存地址,以便程序可以在新的内存组织结构下运行
  • 解析符号引用

链接器通过这些功能,创建了一个名称为可执行文件的一个可运行程序。

静态库是通过复制一个程序中的所有依赖库模块到最终的可执行镜像来创建的。链接器将链接静态库作为编译过程的最后一步。可执行文件是通过解析外部引用、将库例程与程序代码结合在一起来创建的。

创建目标文件

这里是一个静态库的示例以及其链接过程。首先,创建带有这些函数识别标志的头文件 mymath.h :

int add(int a, int b);
int sub(int a, int b);
int mult(int a, int b);
int divi(int a, int b);

使用这些函数定义来创建 add.csub.cmult.cdivi.c 文件。我将把所有的代码都放置到一个代码块中,请将其分为四个文件,如注释所示:

// add.c
int add(int a, int b){
return (a+b);
}

//sub.c
int sub(int a, int b){
return (a-b);
}

//mult.c
int mult(int a, int b){
return (a*b);
}

//divi.c
int divi(int a, int b){
return (a/b);
}

现在,使用 GCC 来生成目标文件 add.osub.omult.odivi.o

(LCTT 校注:关于“ 目标文件 object file ”,有时候也被称作“对象文件”,对此,存在一些译法混乱情形,称之为“目标文件”的译法比较流行,本文采用此译法。)

$ gcc -c add.c sub.c mult.c divi.c

-c 选项跳过链接步骤,而只创建目标文件。

创建一个名称为 libmymath.a 的静态库,接下来,移除目标文件,因为它们不再被需要。(注意,使用一个 trash 命令比使用一个 rm 命令更安全。)

$ ar rs libmymath.a add.o sub.o mult.o divi.o
$ trash *.o
$ ls
add.c  divi.c  libmymath.a  mult.c  mymath.h  sub.c

现在,你已经创建了一个名称为 libmymath 的简单数学示例库,你可以在 C 代码中使用它。当然,也有非常复杂的 C 库,这就是他们这些开发者来生成最终产品的工艺流程,你和我可以安装这些库并在 C 代码中使用。

接下来,在一些自定义代码中使用你的数学库,然后链接它。

创建一个静态链接的应用程序

假设你已经为数学运算编写了一个命令。创建一个名称为 mathDemo.c 的文件,并将这些代码复制粘贴至其中:

#include <mymath.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
  int x, y;
  printf("Enter two numbers\n");
  scanf("%d%d",&x,&y);
 
  printf("\n%d + %d = %d", x, y, add(x, y));
  printf("\n%d - %d = %d", x, y, sub(x, y));
  printf("\n%d * %d = %d", x, y, mult(x, y));

  if(y==0){
    printf("\nDenominator is zero so can't perform division\n");
      exit(0);
  }else{
      printf("\n%d / %d = %d\n", x, y, divi(x, y));
      return 0;
  }
}

注意:第一行是一个 include 语句,通过名称来引用你自己的 libmymath 库。

针对 mathDemo.c 创建一个名称为 mathDemo.o 的对象文件:

$ gcc -I . -c mathDemo.c

-I 选项告诉 GCC 搜索在其后列出的头文件。在这个实例中,你通过单个点(.)来指定当前目录。

链接 mathDemo.olibmymath.a 来生成最终的可执行文件。这里有两种方法来向 GCC 告知这一点。

你可以指向文件:

$ gcc -static -o mathDemo mathDemo.o libmymath.a

或者,你可以具体指定库的路径及名称:

$ gcc -static -o mathDemo -L . mathDemo.o -lmymath

在后面的那个示例中,-lmymath 选项告诉链接器来链接对象文件 mathDemo.o 和对象文件 libmymath.a 来生成最终的可执行文件。-L 选项指示链接器在下面的参数中查找库(类似于你使用 -I 所做的工作)。

分析结果

使用 file 命令来验证它是静态链接的:

$ file mathDemo
mathDemo: ELF 64-bit LSB executable, x86-64...
statically linked, with debug_info, not stripped

使用 ldd 命令,你将会看到该可执行文件不是动态链接的:

$ ldd ./mathDemo
        not a dynamic executable

你也可以查看 mathDemo 可执行文件的大小:

$ du -h ./mathDemo
932K    ./mathDemo

在我 前一篇文章 的示例中,动态链接的可执行文件只占有 24K 大小。

运行该命令来看看它的工作内容:

$ ./mathDemo
Enter two numbers
10
5

10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2

看起来令人满意!

何时使用静态链接

动态链接可执行文件通常优于静态链接可执行文件,因为动态链接会保持应用程序的组件模块化。假如一个库接收到一次关键安全更新,那么它可以很容易地修补,因为它存在于应用程序的外部。

当你使用静态链接时,库的代码会“隐藏”在你创建的可执行文件之中,意味着在库每次更新时(相信我,你会有更好的东西),仅有的一种修补方法是重新编译和发布一个新的可执行文件。

不过,如果一个库的代码,要么存在于它正在使用的具有相同代码的可执行文件中,要么存在于不会接收到任何更新的专用嵌入式设备中,那么静态连接将是一种可接受的选项。


via: https://opensource.com/article/22/6/static-linking-linux

作者:Jayashree Huttanagoudar 选题:lkxed 译者:robsean 校对:turbokernel

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

学习如何用动态链接库将多个 C 目标文件结合到一个单个的可执行文件之中。

当使用 C 编程语言编写一个应用程序时,你的代码通常有多个源文件代码。

最终,这些文件必须被编译到一个单个的可执行文件之中。你可以通过创建静态或动态库(后者也被称为 共享 shared 库)来实现这一点。这两种类型的库在创建和链接的方式上有所不同。两者都有缺点和优点,这取决于你的使用情况。

动态链接是最常见的方法,尤其是在 Linux 系统上。动态链接会保持库模块化,因此,很多应用程序可以共享一个库。应用程序的模块化也允许单独更新其依赖的共享库。

在这篇文章中,我将演示动态链接是如何工作的。在后期的文章中,我将演示静态链接。

链接器

链接器 linker 是一个命令,它将一个程序的数个部分结合在一起,并为它们重新组织内存分配。

链接器的功能包括:

  • 整合一个程序的所有的部分
  • 计算出一个新的内存组织结构,以便所有的部分组合在一起
  • 恢复内存地址,以便程序可以在新的内存组织结构下运行
  • 解析符号引用

链接器通过这些功能,创建了一个名为 可执行文件 executable 的可以运行的程序。在你创建一个动态链接的可执行文件前,你需要一些用来链接的库,和一个用来编译的应用程序。准备好你 最喜欢的文本编辑器 并继续。

创建目标文件

首先,创建带有这些函数签名的头文件 mymath.h

int add(int a, int b);
int sub(int a, int b);
int mult(int a, int b);
int divi(int a, int b);

使用这些函数定义来创建 add.csub.cmult.cdivi.c 文件。我将把所有的代码都放置到一个代码块中,请将其分为四个文件,如注释所示:

// add.c
int add(int a, int b){
return (a+b);
}

//sub.c
int sub(int a, int b){
return (a-b);
}

//mult.c
int mult(int a, int b){
return (a*b);
}

//divi.c
int divi(int a, int b){
return (a/b);
}

现在,使用 GCC 来创建目标文件 add.osub.omult.odivi.o

(LCTT 校注:关于“ 目标文件 object file ”,有时候也被称作“对象文件”,对此,存在一些译法混乱情形,称之为“目标文件”的译法比较流行,本文采用此译法。)

$ gcc -c add.c sub.c mult.c divi.c

-c 选项跳过链接步骤,并且只创建目标文件。

创建一个共享的目标文件

在最终的可执行文件的执行过程中将链接动态库。在最终的可执行文件中仅放置动态库的名称。实际上的链接过程发生在运行时,在此期间,可执行文件和库都被放置到了主内存中。

除了可共享外,动态库的另外一个优点是它减少了最终的可执行文件的大小。在一个应用程序最终的可执行文件生成时,其使用的库只包括该库的名称,而不是该库的一个多余的副本。

你可以从你现有的示例代码中创建动态库:

$ gcc -Wall -fPIC -c add.c sub.c mult.c divi.c

选项 -fPIC 告诉 GCC 来生成 位置无关的代码 position-independent code (PIC)。-Wall 选项不是必需的,并且与代码的编译方式是无关的。不过,它却是一个有价值的选项,因为它会启用编译器警告,这在排除故障时是很有帮助的。

使用 GCC ,创建共享库 libmymath.so

$ gcc -shared -o libmymath.so add.o sub.o mult.o divi.o

现在,你已经创建了一个简单的示例数学库 libmymath.so ,你可以在 C 代码中使用它。当然,也有非常复杂的 C 库,这就是他们这些开发者来生成最终产品的工艺流程,你和我可以安装这些库并在 C 代码中使用。

接下来,你可以在一些自定义代码中使用你的新数学库,然后链接它。

创建一个动态链接的可执行文件

假设你已经为数学运算编写了一个命令。创建一个名称为 mathDemo.c 的文件,并将这些代码复制粘贴至其中:

#include <mymath.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
  int x, y;
  printf("Enter two numbers\n");
  scanf("%d%d",&x,&y);
 
  printf("\n%d + %d = %d", x, y, add(x, y));
  printf("\n%d - %d = %d", x, y, sub(x, y));
  printf("\n%d * %d = %d", x, y, mult(x, y));

  if(y==0){
    printf("\nDenominator is zero so can't perform division\n");
      exit(0);
  }else{
      printf("\n%d / %d = %d\n", x, y, divi(x, y));
      return 0;
  }
}

注意:第一行是一个 include 语句,通过名称来引用你自己的 libmymath 库。要使用一个共享库,你必须已经安装了它,如果你没有安装你将要使用的库,那么当你的可执行文件在运行并搜索其包含的库时,将找不到该共享库。如果你需要在不安装库到已知目录的情况下编译代码,这里有 一些方法可以覆盖默认设置。不过,对于一般使用来说,我们希望库存在于已知的位置,因此,这就是我在这里演示的东西。

复制文件 libmymath.so 到一个标准的系统目录,例如:/usr/lib64, 然后运行 ldconfigldconfig 命令创建所需的链接,并缓存到标准库目录中发现的最新共享库。

$ sudo cp libmymath.so /usr/lib64/
$ sudo ldconfig

编译应用程序

从你的应用程序源文件代码(mathDemo.c)中创建一个名称为 mathDemo.o 的目标文件:

$ gcc -I . -c mathDemo.c

-I 选项告诉 GCC 来在其后所列出的目录中搜索头文件(在这个示例中是 mymath.h)。在这个示例中,你指定的是当前目录,通过一个单点(.)来表示。创建一个可执行文件,使用 -l 选项来通过名称来引用你的共享数学库:

$ gcc -o mathDynamic mathDemo.o -lmymath

GCC 会找到 libmymath.so ,因为它存在于一个默认的系统库目录中。使用 ldd 来查证所使用的共享库:

$ ldd mathDemo
    linux-vdso.so.1 (0x00007fffe6a30000)
    libmymath.so => /usr/lib64/libmymath.so (0x00007fe4d4d33000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fe4d4b29000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fe4d4d4e000)

看看 mathDemo 可执行文件的大小:

$ du ./mathDynamic
24   ./mathDynamic

当然,它是一个小的应用程序,它所占用的磁盘空间量也反映了这一点。相比之下,相同代码的一个静态链接版本(正如你将在我后期的文章所看到的一样)是 932K !

$ ./mathDynamic
Enter two numbers
25
5

25 + 5 = 30
25 - 5 = 20
25 * 5 = 125
25 / 5 = 5

你可以使用 file 命令来查证它是动态链接的:

$ file ./mathDynamic
./mathDynamic: ELF 64-bit LSB executable, x86-64,
dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2,
with debug_info, not stripped

成功!

动态链接

因为链接发生在运行时,所以,使用一个共享库会产生一个轻量型的可执行文件。因为它在运行时解析引用,所以它会花费更多的执行时间。不过,因为在日常使用的 Linux 系统上绝大多数的命令是动态链接的,并且在现代硬件上,所能节省的时间是可以忽略不计的。对开发者和用户来说,它的固有模块性是一种强大的功能。

在这篇文章中,我描述了如何创建动态库,并将其链接到一个最终可执行文件。在我的下一篇文章中,我将使用相同的源文件代码来创建一个静态链接的可执行文件。


via: https://opensource.com/article/22/5/dynamic-linking-modular-libraries-linux

作者:Jayashree Huttanagoudar 选题:lkxed 译者:robsean 校对:wxy

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

Tig 是审查 Git 仓库的绝佳工具,它鼓励你探索日志,而无需构建冗长且有时复杂的查询。

如果你发现浏览你的 Git 仓库非常复杂,我已经为你准备好了工具,来了解一下 Tig。

Tig 是一个 基于 ncurses 的 Git 文本模式界面,它允许你浏览 Git 仓库中的更改。它还可以充当各种 Git 命令输出的分页器。使用这个工具可以让我很好地了解在哪个提交中发生了哪些更改,最新的提交合并是什么等等。请跟随这个简短的教程,亲自尝试一下。

安装 Tig

在 Linux 上,你可以使用包管理器安装 Tig。例如,在 Fedora 和 Mageia 上:

$ sudo dnf install tig

在 Debian、Linux Mint、Elementary、Pop\_OS 和其他基于 Debian 的发行版上:

$ sud apt install tig

在 macOS 上,使用 MacPortsHomebrew。 Tig 的完整安装指南可在 Tig 手册 中找到。

使用 Tig

Tig 提供了常见 Git 输出的交互式视图。例如,使用 Git,你可以使用命令 git show-ref 查看所有引用:

$ git show-ref
98b108... refs/heads/master
6dae95... refs/remotes/origin/1010-internal-share-partition-format-reflexion
84e1f8... refs/remotes/origin/1015-add-libretro-openlara
e62c7c... refs/remotes/origin/1016-add-support-for-retroarch-project-cd
1c29a8... refs/remotes/origin/1066-add-libretro-mess
ffd3f53... refs/remotes/origin/1155-automatically-generate-assets-for-external-installers
ab4d14... refs/remotes/origin/1160-release-on-bare-metal-servers
28baa9... refs/remotes/origin/1180-ipega-pg-9118
8dff1d... refs/remotes/origin/1181-add-libretro-dosbox-core-s
81a7fe... refs/remotes/origin/1189-allow-manual-build-on-master
[...]

使用 Tig,你可以在可滚动列表中获取该信息以及更多信息,此外还可以使用键盘快捷键来打开其他视图,其中包含每个引用的详细信息。

分页模式

当输入来自标准输入时,Tig 进入分页模式。当指定 show 子命令并给出 --stdin 选项时,标准输入被假定为提交 ID 列表,它被转发到 git-show

$ git rev-list --author=sumantrom HEAD | tig show –stdin

日志和差异视图

当你在 Tig 的日志视图中时,你可以按键盘上的 d 键来显示差异。这将显示提交中更改的文件以及删除和添加的行。

交互式 Git 数据

Tig 是对 Git 的一个很好的补充。它鼓励你探索日志,而无需构建冗长且有时复杂的查询,从而可以轻松查看你的 Git 仓库。

立即将 Tig 添加到你的 Git 工具包中!


via: https://opensource.com/article/22/7/visualize-git-workflow-tig

作者:Sumantro Mukherjee 选题:lkxed 译者:geekpi 校对:wxy

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