分类 技术 下的文章

CMake 是一个跨平台的编译、测试和打包软件,即使你以前从来没有使用过构建系统,也可以轻松上手。

 title=

在我以前的文章 Autotools 入门 一文中,我说明了如何使用 Autotools 来管理和打包代码。这是一个强大且通用的平台,可轻松集成到许多打包系统中,包括 RPM、APT、pkgsrc 等等。它的语法和结构可能会令人困惑,但幸运的是,我们还有其他选择,开源的 CMake 就是其中一个。

CMake 是一个用于构建、测试和打包软件的跨平台套件。它使用简单而清晰的语法,因此即使你以前从未使用过构建系统,也很容易开始使用。

安装 CMake

CMake 可能已经安装在你的 Linux 系统上。如果没有,你可以使用发行版的程序包管理器进行安装:

$ sudo dnf install cmake

在 Debian 或者其他相似的系统上:

$ sudo apt install cmake

在 Mac 上,你可以使用 MacPorts 或者 Homebrew 来安装:

$ sudo port install cmake

在 Windows 上,你可以使用 Chocolatey 或者直接从 CMake 网站 下载二进制来安装。

使用 CMake

对于想要从源代码构建软件的开发人员或用户来说,CMake 是一种快速简便的编译和安装方法。 CMake 分阶段工作:

  1. 首先,在 cmake 步骤中,CMake 扫描计算机查看一些默认设置。默认设置包括库的位置以及在系统上安装软件的位置。
  2. 接下来,使用系统上的 make 命令(在 Linux 上是 GUN Make,在 NetBSD 上是 NetBSD Make)来编译程序。这个过程通常是将人类可读的源代码转换成机器语言。
  3. 最后,在 make install 一步中,那些编译过的文件将被拷贝到(在 cmake 步骤中扫描出来的)计算机上合适的位置。

这看起来很简单,当你使用 CMake 时就是这样。

CMake 的可移植性

CMake 在设计时就考虑了可移植性。虽然它不能使你的项目在所有 POSIX 平台上都能正常工作(这取决于作为开发者的你),但它可以确保将标记为要安装的文件安装到已知平台上最合适的位置。而且由于有了 CMake 之类的工具,对于高级用户而言,根据其系统需求自定义和覆盖任何不合适的选项都很容易。

使用 CMake,你只需要知道将哪些文件安装到哪个常规位置即可。它会照顾其他一切。不再需要自定义安装脚本,它们有可能在任何未经测试的操作系统上失败。

打包

像 Autotools 一样,CMake 也得到了很好的打包支持。无论它们是打包成 RPM 还是 DEB 或 TGZ(或其他任何东西),将带有 CMake 的项目交给打包者,他们的工作既简单又直接。打包工具支持 CMake,因此可能不需要进行任何修补或者调整。在许多情况下,可以自动将 CMake 项目整合到工作流中。

如何使用 CMake

要在项目中使用 CMake,只需在项目目录中创建 CMakeLists.txt 文件。首先,声明最低要求的 CMake 版本以及项目名称和版本。CMake 会努力在尽可能长时间内保持兼容性,但是随着你使用的时间越长,并且关注它最新的开发动态,你就会知道哪些特性是你所依赖的。

cmake_minimum_required(VERSION 3.10)

project(Hello VERSION 1.0)

如你可能已经看到的那样,CMake 的语法是一个带有括号和参数的命令。大写的 VERSION 字符串不是任意的,也不只是格式。它们是 project 命令中的有效参数。

在继续之前,先写一个简单的 C 或者 C++ 的 hello world 程序。为了简单,我就写了六行 C 代码,并把它保存在 hello.c 中(为了匹配我在 CMakeLists.txt 中可执行文件的名字)。

#include <stdio.h>

int main() {
   printf("Hello open source\n");
   return 0;
}

不过,不要搞错了,CMake 不仅适用于 C 和 C++。它可以处理任意文件,并且有许多可用的命令,因此它可以帮助你维护许多不同形式的项目。

CMake 网站中记录了所有有效的内置命令及其可用参数,因此无论你要做什么,都可以轻松发现所需的功能。不过,这是一个简单的示例,因此,你需要的下一个命令是必不可少的 —— 你必须为 CMake 定义要构建的代码:

add_executable(Hello hello.c)

这个命令指定了你编译后的二进制文件的名字为 Hello。因此,它与你在终端中执行带有 -o Hellogcc 命令是一样的。

在一些比较复杂的项目中,你可能还需要使用库文件,你可以使用 add library 命令来链接库文件。

在你设置了你想要构建和标记为安装的文件之后,你必须要告诉 CMake 一旦用户安装了程序,最终的应用程序应该在哪个位置。

在这个简单的例子里,你仅需要做的一件事就是在你的 CMakeLists.txt 文件里添加 install 命令。install 命令接受几个参数。但是在这个例子中,你仅需要使用 TARGET 命令来指定你要安装文件的名字。

install(TARGETS Hello)

向 CMake 工程添加一些文件

一个软件项目向用户交付的往往不仅仅只有代码,还有一些其他的文件数据,例如手册或者是信息页、示例项目,或者是配置文件。你可以使用与包含编译文件时类似的工作流程,将任意数据包含在 CMake 项目中:在 CMakelists.txt 文件中使用 file 命令,然后说明一下这些文件要安装在哪里。

例如,你可以在这个项目中包含一个 assets 目录,你可以使用 file 命令,后面跟上 COPYDESTINATION 参数来告诉 CMake 将这些额外的文件复制到你的分发包中。

file(COPY assets DESTINATION "${CMAKE_CURRENT_BINARY_DIR}")

这个 ${CMAKE_CURRENT_BINARY_DIR} 变量是一个特殊的 CMake 内置变量,表示 CMake 正在处理的目录。换句话说,你的任何文件都会被复制到编译目录(在你运行 cmake 命令后,这个过程会更加清晰,到时候回过头来看一下)。

因为这些额外的数据文件有些杂乱不堪(如果你不信的话,可以看一下 /usr/share 这个目录)。对于你自己的项目创建一个子文件夹对谁都有好处。最好也带上版本名字。你可以通过在 CMAKE_CURRENT_BINARY_DIR 中指定一个新的目录,使用你选择的项目名称,后面跟一个为你的项目命名的特殊变量和你在项目声明中为它设置的 VERSION

file(COPY assets DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/Hello-${Hello_VERSION}")

定义安装位置

你已经定义你要编译的文件,因此现在你要告诉 CMake 你的程序要安装在哪个位置。比如你的主程序,这个要程使用 install 命令:

install(DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Hello-${Hello_VERSION}" TYPE DATA)

这里有一些新的参数。DIRECTORY 参数指定了数据文件是一个目录,而不是一个文件(FILE)或者脚本(SCRIPT)。你使用的参数和复制一些额外文件到编译目录时是一样。另外,在 install 命令中 TYPE 或者 DESTINATION 必须要指定其一。TYPE 参数指定了通用的文件类型,这些文件通常将会被放到合适的位置。在 Linux 系统上,TYPE DATA 一般是 /usr/local/share 或者 /usr/share,除非用户定义了其他的位置。

这是诸如 CMake 之类的良好构建系统的强大功能之一。你不必担心文件的确切位置,因为你知道用户可以更改 CMake 的首选默认设置,并且 CMake 将构建代码以使其正常工作。

运行 CMake

CMake 有多种方式来让你执行命令,你可以在终端或者在一个可交互的程序上执行命令,或者你也可以使用它的图形界面(GUI)。我比较偏向于使用终端命令,但是我也喜欢使用一些其他的方式(相比与在 Makefile 中查找那些晦涩的变量然后去修改它们更胜一筹)。

对于编译过开源 C++ 项目的任何人,都熟悉的第一步是创建一个 build 目录,进入到该目录,然后运行 cmake .. 命令。 我是一个懒惰的打字员,所以我将构建目录命名为 b,但是你可以使用最合适的方式:

$ mkdir b
$ cd b
$ cmake ..
-- The C compiler identification is GNU 11.1.1
-- The CXX compiler identification is GNU 11.1.1
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /var/home/seth/demo-hello/b
$

这或多或少相当于经典的 ./configure; make; make install 中的 ./configure。看一下你的构建目录,CMake 已经帮你生成了几个新的文件,来让你的项目更完整。这里生成了 CMake 的数据文件、一个常规的 Makefile 文件(这是一个免费提供的 247 行的文件,但对于越复杂的项目,行数要多得多),还有一个包含这个示例程序的任意非编译数据的 Hello-1.0 目录。

$ ls
CMakeCache.txt
CMakeFiles
Makefile
Hello-1.0
cmake_install.cmake

接下来,你可以进行构建。你可以使用 CMake 的 --build 选项来做这件事,使用当前的构建目录作为源目录。

$ cmake --build .
Scanning dependencies of target Hello
[ 50%] Building C object CMakeFiles/Hello.dir/hello.c.o
[100%] Linking C executable Hello
[100%] Built target Hello

或者你可以运行 make 命令。这将读取由 CMake 生成的 Makefile 文件。在这个例子中,make 默认的行为就是由源程序 hello.c 生成目标文件。

$ make
Scanning dependencies of target Hello
[ 50%] Building C object CMakeFiles/Hello.dir/hello.c.o
[100%] Linking C executable Hello
[100%] Built target Hello
$

如你所料,Hello 二进制可执行文件现在存在于当前的构建目录中。因为它是一个简单的自包含应用程序,所以你可以运行它进行测试:

$ ./Hello
Hello open source
$

最后,你可以用 --install 选项进行安装。因为我不希望我的简单的 “hello world” 应用程序真的被安装到我的系统上,我设置了 --prefix 选项,将 CMake 的目标从根目录(/)重定向到 /tmp 的一个子目录。

$ cmake --install . --prefix /tmp/hello/
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/local/bin/Hello
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file1

另外,你也可以运行 make install 来调用 Makefile 的安装动作。同样,为了避免在我的系统上安装一个演示程序,我在这个例子中设置了 DESTDIR 变量,将安装目标重定向到 /tmp 的一个子目录:

$ mkdir /tmp/dist-hello
$ make install DESTDIR=/tmp/dist-hello
[100%] Built target Hello
Install the project...
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/local/bin/Hello
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/local/share/Hello-1.0/assets/file1

看一下输出的内容,来确定它具体的安装位置,这个程序已经安装好了。

快速自定义

CMake 的安装前缀(由 CMAKE_INSTALL_PREFIX 变量指定)默认是在 /usr/local 这个位置,但是所有的 CMake 变量都可以在你运行 cmake 命令的时候,加一个 -D 选项来改变它。

$ cmake -DCMAKE_INSTALL_PREFIX=/usr ..
$ make install DESTDIR=/tmp/dist-hello
$ make install DESTDIR=/tmp/dist-hello
[100%] Built target Hello
Install the project...
-- Install configuration: ""
-- Installing: /tmp/dist-hello/usr/bin/Hello
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0/assets/file0
-- Installing: /tmp/dist-hello/usr/share/Hello-1.0/assets/file1

所有由 CMake 使用的变量都可以通过这种方式来修改。

交互式的 CMake

CMake 的交互模式是一种用于配置安装环境的友好而有用的方法。要让用户知道该项目使用的所有可能的 CMake 变量是一件工作量很大的事,因此 CMake 交互式界面是他们无需查看 MakefileCMakeLists 即可发现自定义选项的简便方法。

为了调用这个交互式的 CMake,使用 ccmake 命令,在这个简单的项目里没有太多的东西。但是对于像 Rosegarden 这样的大型项目,这将非常有用。

 title=

CMake 的更多知识

还有很多很多的 CMake 知识需要去了解。作为一个开发者,我非常喜欢它简洁的语法、详尽的文档、可扩展性以及便捷性。作为一个用户我非常喜欢 CMake 友好且实用的错误提示信息还有它的用户界面,如果你的项目还未开始使用构建系统,请了解一下 CMake 吧。你以及以后尝试打包你应用程序的任何人都不会后悔。


via: https://opensource.com/article/21/5/cmake

作者:Seth Kenlon 选题:lujun9972 译者:amwps290 校对:wxy

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

了解 sed 的基本用法,然后下载我们的备忘单,方便快速地参考 Linux 流编辑器。

 title=

很少有 Unix 命令像 sedgrepawk 一样出名,它们经常组合在一起,可能是因为它们具有奇怪的名称和强大的文本解析能力。它们还在一些语法和逻辑上有相似之处。虽然它们都能用于文本解析,但都有其特殊性。本文研究 sed 命令,它是一个 流编辑器 stream editor

我之前写过关于 sed 以及它的远亲 ed 的文章。要熟悉 sed,对 ed 有一点了解是有帮助的,因为这有助于你熟悉缓冲区的概念。本文假定你熟悉 sed 的基本知识,这意味着你至少已经运行过经典的 s/foo/bar 风格的查找和替换命令。

安装 sed

如果你使用的是 Linux、BSD 或 macOS,那么它们已经安装了 GNU 的或 BSD 的 sed。这些是原始 sed 命令的独特重新实现。虽然它们很相似,但也有一些细微的差别。本文已经在 Linux 和 NetBSD 版本上进行了测试,所以你可以使用你的计算机上找到的任何 sed,但是对于 BSD sed,你必须使用短选项(例如 -n 而不是 --quiet)。

GNU sed 通常被认为是功能最丰富的 sed,因此无论你是否运行 Linux,你可能都想要尝试一下。如果在 Ports 树中找不到 GNU sed(在非 Linux 系统上通常称为 gsed),你可以从 GNU 网站 下载源代码。 安装 GNU sed 的好处是,你可以使用它的额外功能,但是如果需要可移植性,还可以限制它以遵守 sed 的 POSIX 规范。

MacOS 用户可以在 MacPortsHomebrew 上找到 GNU sed。

在 Windows 上,你可以通过 Chocolatey安装 GNU sed

了解模式空间和保留空间

sed 一次只能处理一行。因为它没有可视化模式,所以会创建一个 模式空间 pattern space ,这是一个内存空间,其中包含来自输入流的当前行(删除了尾部的任何换行符)。填充模式空间后,sed 将执行你的指令。当命令执行完时,sed 将模式空间中的内容打印到输出流,默认是 标准输出,但是可以将输出重定向到文件,甚至使用 --in-place=.bak 选项重定向到同一文件。

然后,循环从下一个输入行再次开始。

为了在遍历文件时提供一点灵活性,sed 还提供了 保留空间 hold space (有时也称为 保留缓冲区 hold buffer ),即 sed 内存中为临时数据存储保留的空间。你可以将保留空间当作剪贴板,实际上,这正是本文所演示的内容:如何使用 sed 复制/剪切和粘贴。

首先,创建一个示例文本文件,其内容如下:

Line one
Line three
Line two

复制数据到保留空间

要将内容放置在 sed 的保留空间,使用 hH 命令。小写的 h 告诉 sed 覆盖保留空间中的当前内容,而大写的 H 告诉 sed 将数据追加到保留空间中已经存在的内容之后。

单独使用,什么都看不到:

$ sed --quiet -e '/three/ h' example.txt
$

--quiet(缩写为 -n)选项禁止显示所有输出,但 sed 执行了我的搜索需求。在这种情况下,sed 选择包含字符串 three 的任何行,并将其复制到保留空间。我没有告诉 sed 打印任何东西,所以没有输出。

从保留空间复制数据

要了解保留空间,你可以从保留空间复制内容,然后使用 g 命令将其放入模式空间,观察会发生什么:

$ sed -n -e '/three/h' -e 'g;p' example.txt

Line three
Line three

第一个空白行是因为当 sed 第一次复制内容到模式空间时,保留空间为空。

接下来的两行包含 Line three 是因为这是从第二行开始的保留空间。

该命令使用两个唯一的脚本(-e)纯粹是为了帮助提高可读性和组织性。将步骤划分为单独的脚本可能会很有用,但是从技术上讲,以下命令与一个脚本语句一样有效:

$ sed -n -e '/three/h ; g ; p' example.txt

Line three
Line three

将数据追加到模式空间

G 命令会将一个换行符和保留空间的内容添加到模式空间。

$ sed -n -e '/three/h' -e 'G;p' example.txt
Line one

Line three
Line three
Line two
Line three

此输出的前两行同时包含模式空间(Line one)的内容和空的保留空间。接下来的两行与搜索文本(three)匹配,因此它既包含模式空间又包含保留空间。第三行的保留空间没有变化,因此在模式空间(Line two)的末尾是保留空间(仍然是 Line three)。

用 sed 剪切和粘贴

现在你知道了如何将字符串从模式空间转到保留空间并再次返回,你可以设计一个 sed 脚本来复制、删除,然后在文档中粘贴一行。例如,将示例文件的 Line three 挪至第三行,sed 可以解决这个问题:

$ sed -n -e '/three/ h' -e '/three/ d' \
-e '/two/ G;p' example.txt
Line one
Line two
Line three
  • 第一个脚本找到包含字符串 three 的行,并将其从模式空间复制到保留空间,替换当前保留空间中的任何内容。
  • 第二个脚本删除包含字符串 three 的任何行。这样就完成了与文字处理器或文本编辑器中的 剪切 动作等效的功能。
  • 最后一个脚本找到包含字符串 two 的行,并将保留空间的内容\_追加\_到模式空间,然后打印模式空间。

任务完成。

使用 sed 编写脚本

再说一次,使用单独的脚本语句纯粹是为了视觉和心理上的简单。剪切和粘贴命令作为一个脚本同样有效:

$ sed -n -e '/three/ h ; /three/ d ; /two/ G ; p' example.txt
Line one
Line two
Line three

它甚至可以写在一个专门的脚本文件中:

#!/usr/bin/sed -nf

/three/h
/three/d
/two/ G
p

要运行该脚本,将其加入可执行权限,然后用示例文件尝试:

$ chmod +x myscript.sed
$ ./myscript.sed example.txt
Line one
Line two
Line three

当然,你需要解析的文本越可预测,则使用 sed 解决问题越容易。发明 sed 操作(例如复制和粘贴)的“配方”通常是不切实际的,因为触发操作的条件可能因文件而异。但是,你对 sed 命令的使用越熟练,就越容易根据需要解析的输入来设计复杂的动作。

重要的事情是识别不同的操作,了解 sed 何时移至下一行,并预测模式和保留空间包含的内容。

下载备忘单

sed 很复杂。虽然它只有十几个命令,但它灵活的语法和原生功能意味着它充满了无限的潜力。为了充分利用 sed,我曾经参考过一些巧妙的单行命令,但是直到我开始发明(有时是重新发明)自己的解决方案时,我才觉得自己真正开始学习 sed 了 。如果你正在寻找命令提示和语法方面的有用技巧,下载我们的 sed 备忘单,然后开始一劳永逸地学习 sed!


via: https://opensource.com/article/21/3/sed-cheat-sheet

作者:Seth Kenlon 选题:lujun9972 译者:MjSeven 校对:wxy

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

通过在 GNOME 3 和 Wayland 上重新映射你的键盘,提高你的打字和导航速度,避免重复性压力伤害。

 title=

对我来说,有许多改变生活的 Linux 时刻,但大多数都在成为现状后淡忘了。有一个 Linux 教给我的键盘小技巧,每次我使用它的时候(也许每天有 1000 次),我都会想起这件事,那就是把大写锁定键转换为 Ctrl 键。

我从不使用大写锁定键,但我整天使用 Ctrl 键进行复制、粘贴、在 Emacs 内导航,以及 调用 BashGNU Screentmux 等操作。大写锁定键在我的键盘上占据了宝贵的空间,而将实际上有用的 Ctrl 键挤到了难以触及的底部角落。

 title=

这看起来就痛苦

重新映射 Ctrl 提高了我的打字和导航速度,并可能使我免受重复性压力伤害。

消失的控制

系好安全带,这是个过山车式的历史课。

对于像我这样的大写锁定键交换者来说,不幸的是,当 GNOME 3 问世时,它几乎删除了改变 Ctrl 键位置的功能。

幸运的是,优秀的 GNOME Tweaks 应用程序带回了这些 “失踪” 的控制面板。

不幸的是,GNOME 40 没有 GNOME Tweaks 应用程序(还没有?)

另外,不幸的是,过去在 X11 上可以工作的老的 xmodmap 技巧在新的 Wayland 显示服务器 上没有用。

有一小段时间(最多一个下午),我觉得对于那些讨厌大写锁定键的人来说人生都灰暗了。然后我想起我是一个开源的用户,总有一种方法可以解决诸如被忽略的 GUI 控制面板之类的简单问题。

dconf

GNOME 桌面使用 dconf,这是一个存储重要配置选项的数据库。它是 GSettings 的后端,GSettings 是 GNOME 系统应用程序需要发现系统偏好时的接口。你可以使用 gsetting 命令查询 dconf 数据库,也可以使用 dconf 命令直接设置 dconf 的键值。

GSettings

dconf 数据库不一定是你可能称为可发现的数据库。它是一个不起眼的数据库,你通常不需要去考虑它,它包含了许多通常无需直接交互的数据。然而,如果你想更好地了解 GNOME 所要管理的所有偏好选项,那么浏览它是很有趣的。

你可以用 list-schemas 子命令列出所有 dconf 的模式。在浏览了数百个模式之后,你可以使用 grep 将你的注意力缩小到一些看起来特别相关的东西上,比如 org.gnome.desktop

$ gsettings list-schemas | grep ^org.gnome.desktop
[...]
org.gnome.desktop.background
org.gnome.desktop.privacy
org.gnome.desktop.remote-desktop.vnc
org.gnome.desktop.interface
org.gnome.desktop.default-applications.terminal
org.gnome.desktop.session
org.gnome.desktop.thumbnailers
org.gnome.desktop.app-folders
org.gnome.desktop.notifications
org.gnome.desktop.sound
org.gnome.desktop.lockdown
org.gnome.desktop.default-applications.office

无论是通过手动搜索还是通过 阅读 GSetting 文档,你可能会注意到 org.gnome.desktop.input-sources 模式,它有助于定义键盘布局。从设计上来说,GSetting 模式包含了键和值。

用 dconf 重新映射大写字母锁

xkb-options 键包含了可选的键盘覆写。要设置这个键值,请使用dconf,将上面模式中的点(.)转换为斜线(/),因为 dconf 数据库需要使用 /

$ dconf write /org/gnome/desktop/input-sources/xkb-options "['caps:ctrl_modifier']"

我把 caps 设置为 ctrl_modifier,因为我使用 Ctrl 修饰键的次数多于其他修饰键,但 Vim 用户可能喜欢把它设置为 escape

查看你的设置

这个改变会立即生效,并在重启后仍然生效。这是你在 GNOME 中定义的首选项,在你改变它之前一直有效。

你可以通过 gsettings 查看 dconf 中的新值。首先,查看可用的键:

$ gsettings list-keys \
    org.gnome.desktop.input-sources
xkb-options
mru-sources
show-all-sources
current
per-window
sources

然后用 xkb-options 键名查看设置:

$ gsettings get \
    org.gnome.desktop.input-sources \
    xkb-options
['caps:ctrl_modifier']

选项丰富

我在我的 GNOME 3.4 系统上使用这个小技巧来设置大写锁定键以及 Compose 键(compose:ralt)。虽然我相信正在开发中的 GUI 控件可以控制这些选项,但我也不得不承认,能以编程方式设置这些选项的能力是我的荣幸。作为以前没有可靠方法来调整桌面设置的系统的管理员,能够用命令修改我的首选项使得设置新桌面变得快速而容易。

GSettings 提供了很多有用的选项,而且文档也很详尽。如果你有想要改变的东西,可以看看有什么可用的。


via: https://opensource.com/article/21/5/remap-caps-lock-key-linux

作者:Seth Kenlon 选题:lujun9972 译者:wxy 校对:wxy

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

软件库是重复使用代码的一种简单而合理的方式。

 title=

软件库是一种是一直以来长期存在的、简单合理的复用代码的方式。这篇文章解释了如何从头开始构建库并使得其可用。尽管这两个示例库都以 Linux 为例,但创建、发布和使用这些库的步骤也可以应用于其它类 Unix 系统。

这些示例库使用 C 语言编写,非常适合该任务。Linux 内核大部分由 C 语言和少量汇编语言编写(Windows 和 Linux 的表亲如 macOS 也是如此)。用于输入/输出、网络、字符串处理、数学、安全、数据编码等的标准系统库等主要由 C 语言编写。所以使用 C 语言编写库就是使用 Linux 的原生语言来编写。除此之外,C 语言的性能也在一众高级语言中鹤立鸡群。

还有两个来访问这些库的示例 客户程序 client (一个使用 C,另一个使用 Python)。毫无疑问可以使用 C 语言客户程序来访问 C 语言编写的库,但是 Python 客户程序示例说明了一个由 C 语言编写的库也可以服务于其他编程语言。

静态库和动态库对比

Linux 系统存在两种类型库:

  • 静态库(也被称为归档库):在编译过程中的链接阶段,静态库会被编译进程序(例如 C 或 Rust)中。每个客户程序都有属于自己的一份库的拷贝。静态库有一个显而易见的缺点 —— 当库需要进行一定改动时(例如修复一个 bug),静态库必须重新链接一次。接下来要介绍的动态库避免了这一缺点。
  • 动态库(也被称为共享库):动态库首先会在程序编译中的链接阶段被标记,但是客户程序和库代码在运行之前仍然没有联系,且库代码不会进入到客户程序中。系统的动态加载器会把一个共享库和正在运行的客户程序进行连接,无论该客户程序是由静态编译语言(如 C)编写,还是由动态解释语言(如 Python)编写。因此,动态库不需要麻烦客户程序便可以进行更新。最后,多个客户程序可以共享同一个动态库的单一副本。

通常来说,动态库优于静态库,尽管其复杂性较高而性能较低。下面是两种类型的库如何创建和发布:

  1. 库的源代码会被编译成一个或多个目标模块,目标模块是二进制文件,可以被包含在库中并且链接到可执行的二进制中。
  2. 目标模块会会被打包成一个文件。对于静态库,标准的文件拓展名是 .a 意为“ 归档 archive ”;对于动态库,标准的文件拓展名是 .so 意为“ 共享目标 shared object ”。对于这两个相同功能的示例库,分别发布为 libprimes.a (静态库)和 libshprimes.so (动态库)。两种库的文件名都使用前缀 lib 进行标识。
  3. 库文件被复制到标准目录下,使得客户程序可以轻松地访问到库。无论是静态库还是动态库,典型的位置是 /usr/lib 或者 /usr/local/lib,当然其他位置也是可以的。

构建和发布每种库的具体步骤会在下面详细介绍。首先我将介绍两种库里涉及到的 C 函数。

示例库函数

这两个示例库都是由五个相同的 C 函数构建而成的,其中四个函数可供客户程序使用。第五个函数是其他四个函数的一个工具函数,它显示了 C 语言怎么隐藏信息。每个函数的源代码都很短,可以将这些函数放在单个源文件中,尽管也可以放在多个源文件中(如四个公布的函数都有一个文件)。

这些库函数是基本的处理函数,以多种方式来处理质数。所有的函数接收无符号(即非负)整数值作为参数:

  • is_prime 函数测试其单个参数是否为质数。
  • are_coprimes 函数检查了其两个参数的 最大公约数 greatest common divisor (gcd)是否为 1,即是否为互质数。
  • prime_factors:函数列出其参数的质因数。
  • glodbach:函数接收一个大于等于 4 的偶数,列出其可以分解为两个质数的和。它也许存在多个符合条件的数对。该函数是以 18 世纪数学家 克里斯蒂安·哥德巴赫 Christian Goldbach 命名的,他的猜想是任意一个大于 2 的偶数可以分解为两个质数之和,这依旧是数论里最古老的未被解决的问题。

工具函数 gcd 留在已部署的库文件中,但是在没有包含这个函数的文件无法访问此函数。因此,一个使用库的客户程序无法调用 gcd 函数。仔细观察 C 函数可以明白这一点。

更多关于 C 函数的内容

每个在 C 语言中的函数都有一个存储类,它决定了函数的范围。对于函数,有两种选择。

  • 函数默认的存储类是 extern,它给了函数一个全局域。一个客户程序可以调用在示例库中用 extern 修饰的任意函数。下面是一个带有显式 extern 声明的 are_coprimes 函数定义:
extern unsigned are_coprimes(unsigned n1, unsigned n2) {
  ...
}
  • 存储类 static 将一个函数的的范围限制到函数被定义的文件中。在示例库中,工具函数 gcd 是静态的(static):
static unsigned gcd(unsigned n1, unsigned n2) {
  ...
}

只有在 primes.c 文件中的函数可以调用 gcd,而只有 are_coprimes 函数会调用它。当静态库和动态库被构建和发布后,其他的程序可以调用外部的(extern)函数,如 are_coprimes ,但是不可以调用静态(static)函数 gcd。静态(static)存储类通过将函数范围限制在其他库函数内,进而实现了对库的客户程序隐藏 gcd 函数。

primes.c 文件中除了 gcd 函数外,其他函数并没有指明存储类,默认将会设置为外部的(extern)。然而,在库中显式注明 extern 更加常见。

C 语言区分了函数的 定义 definition 声明 declaration ,这对库来说很重要。接下来让我们开始了解定义。C 语言仅允许命名函数不允许匿名函数,并且每个函数需要定义以下内容:

  • 一个唯一的名字。一个程序不允许存在两个同名的函数。
  • 一个可以为空的参数列表。参数需要指明类型。
  • 一个返回值类型(例如:int 代表 32 位有符号整数),当没有返回值时设置为空类型(void)。
  • 用一对花括号包围起来的函数主体部分。在一个特制的示例中,函数主体部分可以为空。

程序中的每个函数必须要被定义一次。

下面是库函数 are_coprimes 的完整定义:

extern unsigned are_coprimes(unsigned n1, unsigned n2) { /* 定义 */
  return 1 == gcd(n1, n2); /* 最大公约数是否为 1? */
}

函数返回一个布尔值(0 代表假,1 代表真),取决于两个整数参数值的最大公约数是否为 1。工具函数 gcd 计算两个整数参数 n1n2 的最大公约数。

函数声明不同于定义,其不需要主体部分:

extern unsigned are_coprimes(unsigned n1, unsigned n2); /* 声明 */

声明在参数列表后用一个分号代表结束,它没有被花括号包围起来的主体部分。程序中的函数可以被多次声明。

为什么需要声明?在 C 语言中,一个被调用的函数必须对其调用者可见。有多种方式可以提供这样的可见性,具体依赖于编译器如何实现。一个必然可行的方式就是当它们二者位于同一个文件中时,将被调用的函数定义在在它的调用者之前。

void f() {...}     /* f 定义在其被调用前 */
void g() { f(); }  /* ok */

当函数 f 被在调用前声明,此时函数 f 的定义可以移动到函数 g 的下方。

void f();         /* 声明使得函数 f 对调用者可见 */
void g() { f(); } /* ok */
void f() {...}    /* 相较于前一种方式,此方式显得更简洁 */

但是当如果一个被调用的函数和调用它的函数不在同一个文件中时呢?因为前文提到一个函数在一个程序中需要被定义一次,那么如何使得让一个文件中被定义的函数在另一个文件中可见?

这个问题会影响库,无论是静态库还是动态库。例如在这两个质数库中函数被定义在源文件 primes.c 中,每个库中都有该函数的二进制副本,但是这些定义的函数必须要对使用库的 C 程序可见,该 C 程序有其自身的源文件。

函数声明可以帮助提供跨文件的可见性。对于上述的“质数”例子,它有一个名为 primes.h 的头文件,其声明了四个函数使得它们对使用库的 C 程序可见。

/** 头文件 primes.h:函数声明 **/
extern unsigned is_prime(unsigned);
extern void prime_factors(unsigned);
extern unsigned are_coprimes(unsigned, unsigned);
extern void goldbach(unsigned);

这些声明通过为每个函数指定其调用语法来作为接口。

为了客户程序的便利性,头文件 primes.h 应该存储在 C 编译器查找路径下的目录中。典型的位置有 /usr/include/usr/local/include。一个 C 语言客户程序应使用 #include 包含这个头文件,并尽可能将这条语句其程序源代码的首部(头文件将会被导入另一个源文件的“头”部)。C 语言头文件可以被导入其他语言(如 Rust 语言)中的 bindgen,使其它语言的客户程序可以访问 C 语言的库。

总之,一个库函数只可以被定义一次,但可以在任何需要它的地方进行声明,任一使用 C 语言库的程序都需要该声明。头文件可以包含函数声明,但不能包含函数定义。如果头文件包含了函数定义,那么该文件可能会在一个 C 语言程序中被多次包含,从而破坏了一个函数在 C 语言程序中必须被精确定义一次的规则。

库的源代码

下面是两个库的源代码。这部分代码、头文件、以及两个示例客户程序都可以在 我的网页 上找到。

#include <stdio.h>
#include <math.h>

extern unsigned is_prime(unsigned n) {
  if (n <= 3) return n > 1;                   /* 2 和 3 是质数 */
  if (0 == (n % 2) || 0 == (n % 3)) return 0; /* 2 和 3 的倍数不会是质数 */

  /* 检查 n 是否是其他 < n 的值的倍数 */
  unsigned i;
  for (i = 5; (i * i) <= n; i += 6)
    if (0 == (n % i) || 0 == (n % (i + 2))) return 0; /* 不是质数 */

  return 1; /* 一个不是 2 和 3 的质数 */
}

extern void prime_factors(unsigned n) {
  /* 在数字 n 的质因数分解中列出所有 2 */
  while (0 == (n % 2)) {  
    printf("%i ", 2);
    n /= 2;
  }

  /* 数字 2 已经处理完成,下面处理奇数 */
  unsigned i;
  for (i = 3; i <= sqrt(n); i += 2) {
    while (0 == (n % i)) {
      printf("%i ", i);
      n /= i;
    }
  }

  /* 还有其他质因数?*/
  if (n > 2) printf("%i", n);
}

/* 工具函数:计算最大公约数 */
static unsigned gcd(unsigned n1, unsigned n2) {
  while (n1 != 0) {
    unsigned n3 = n1;
    n1 = n2 % n1;
    n2 = n3;
  }
  return n2;
}

extern unsigned are_coprimes(unsigned n1, unsigned n2) {
  return 1 == gcd(n1, n2);
}

extern void goldbach(unsigned n) {
  /* 输入错误 */
  if ((n <= 2) || ((n & 0x01) > 0)) {
    printf("Number must be > 2 and even: %i is not.\n", n);
    return;
  }

  /* 两个简单的例子:4 和 6 */
  if ((4 == n) || (6 == n)) {
    printf("%i = %i + %i\n", n, n / 2, n / 2);
    return;
  }
 
  /* 当 n > 8 时,存在多种可能性 */
  unsigned i;
  for (i = 3; i < (n / 2); i++) {
    if (is_prime(i) && is_prime(n - i)) {
      printf("%i = %i + %i\n", n, i, n - i);
      /* 如果只需要一对,那么用 break 语句替换这句 */
    }
  }
}

库函数

这些函数可以被库利用。两个库可以从相同的源代码中获得,同时头文件 primes.h 是两个库的 C 语言接口。

构建库

静态库和动态库在构建和发布的步骤上有一些细节的不同。静态库需要三个步骤,而动态库需要增加两个步骤即一共五个步骤。额外的步骤表明了动态库的动态方法具有更多的灵活性。让我们先从静态库开始。

库的源文件 primes.c 被编译成一个目标模块。下面是命令,百分号 % 代表系统提示符,两个井字符 # 是我的注释。

% gcc -c primes.c ## 步骤1(静态)

这一步生成目标模块是二进制文件 primes.o-c 标志意味着只编译。

下一步是使用 Linux 的 ar 命令将目标对象归档。

% ar -cvq libprimes.a primes.o ## 步骤2(静态)

-cvq 三个标识分别是“创建”、“详细的”、“快速添加”(以防新文件没有添加到归档中)的简称。回忆一下,前文提到过前缀 lib 是必须的,而库名是任意的。当然,库的文件名必须是唯一的,以避免冲突。

归档已经准备好要被发布:

% sudo cp libprimes.a /usr/local/lib ## 步骤3(静态)

现在静态库对接下来的客户程序是可见的,示例在后面。(包含 sudo 可以确保有访问权限将文件复制进 /usr/local/lib 目录中)

动态库还需要一个或多个对象模块进行打包:

% gcc primes.c -c -fpic ## 步骤1(动态)

增加的选项 -fpic 指示编译器生成与位置无关的代码,这意味着不需要将该二进制模块加载到一个固定的内存位置。在一个拥有多个动态库的系统中这种灵活性是至关重要的。生成的对象模块会略大于静态库生成的对象模块。

下面是从对象模块创建单个库文件的命令:

% gcc -shared -Wl,-soname,libshprimes.so -o libshprimes.so.1 primes.o ## 步骤2(动态)

选项 -shared 表明了该库是一个共享的(动态的)而不是静态的。-Wl 选项引入了一系列编译器选项,第一个便是设置动态库的 -soname,这是必须设置的。soname 首先指定了库的逻辑名字(libshprimes.so),接下来的 -o 选项指明了库的物理文件名字(libshprimes.so.1)。这样做的目的是为了保持逻辑名不变的同时允许物理名随着新版本而发生变化。在本例中,在物理文件名 libshprimes.so.1 中最后的 1 代表是第一个库的版本。尽管逻辑文件名和物理文件名可以是相同的,但是最佳做法是将它们命名为不同的名字。一个客户程序将会通过逻辑名(本例中为 libshprimes.so)来访问库,稍后我会进一步解释。

接下来的一步是通过复制共享库到合适的目录下使得客户程序容易访问,例如 /usr/local/lib 目录:

% sudo cp libshprimes.so.1 /usr/local/lib ## 步骤3(动态)

现在在共享库的逻辑名(libshprimes.so)和它的物理文件名(/usr/local/lib/libshprimes.so.1)之间设置一个符号链接。最简单的方式是将 /usr/local/lib 作为工作目录,在该目录下输入命令:

% sudo ln --symbolic libshprimes.so.1 libshprimes.so ## 步骤4(动态)

逻辑名 libshprimes.so 不应该改变,但是符号链接的目标(libshrimes.so.1)可以根据需要进行更新,新的库实现可以是修复了 bug,提高性能等。

最后一步(一个预防措施)是调用 ldconfig 工具来配置系统的动态加载器。这个配置保证了加载器能够找到新发布的库。

% sudo ldconfig ## 步骤5(动态)

到现在,动态库已为包括下面的两个在内的示例客户程序准备就绪了。

一个使用库的 C 程序

这个示例 C 程序是一个测试程序,它的源代码以两条 #include 指令开始:

#include <stdio.h>  /* 标准输入/输出函数 */
#include <primes.h> /* 我的库函数 */

文件名两边的尖括号表示可以在编译器的搜索路径中找到这些头文件(对于 primes.h 文件来说在 /usr/local/inlcude 目录下)。如果不包含 #include,编译器会抱怨缺少 is_primeprime_factors 等函数的声明,它们在两个库中都有发布。顺便提一句,测试程序的源代码不需要更改即可测试两个库中的每一个库。

相比之下,库的源文件(primes.c)使用 #include 指令打开以下头文件:

#include <stdio.h>
#include <math.h>

math.h 头文件是必须的,因为库函数 prime_factors 会调用数学函数 sqrt,其在标准库 libm.so 中。

作为参考,这是测试库程序的源代码:

#include <stdio.h>
#include <primes.h>

int main() {
  /* 是质数 */
  printf("\nis_prime\n");
  unsigned i, count = 0, n = 1000;
  for (i = 1; i <= n; i++) {
    if (is_prime(i)) {
      count++;
      if (1 == (i % 100)) printf("Sample prime ending in 1: %i\n", i);
    }
  }
  printf("%i primes in range of 1 to a thousand.\n", count);

  /* prime_factors */
  printf("\nprime_factors\n");
  printf("prime factors of 12: ");
  prime_factors(12);
  printf("\n");
 
  printf("prime factors of 13: ");
  prime_factors(13);
  printf("\n");
 
  printf("prime factors of 876,512,779: ");
  prime_factors(876512779);
  printf("\n");

  /* 是合数 */
  printf("\nare_coprime\n");
  printf("Are %i and %i coprime? %s\n",
         21, 22, are_coprimes(21, 22) ? "yes" : "no");
  printf("Are %i and %i coprime? %s\n",
         21, 24, are_coprimes(21, 24) ? "yes" : "no");

  /* 哥德巴赫 */
  printf("\ngoldbach\n");
  goldbach(11);    /* error */
  goldbach(4);     /* small one */
  goldbach(6);     /* another */
  for (i = 100; i <= 150; i += 2) goldbach(i);

  return 0;
}

测试程序

在编译 tester.c 文件到可执行文件时,难处理的部分时链接选项的顺序。回想前文中提到两个示例库都是用 lib 作为前缀开始,并且每一个都有一个常规的拓展后缀:.a 代表静态库 libprimes.a.so 代表动态库 libshprimes.so。在链接规范中,前缀 lib 和拓展名被忽略了。链接标志以 -l (小写 L)开始,并且一条编译命令可能包含多个链接标志。下面是一个完整的测试程序的编译指令,使用动态库作为示例:

% gcc -o tester tester.c -lshprimes -lm

第一个链接标志指定了库 libshprimes.so,第二个链接标志指定了标准数学库 libm.so

链接器是懒惰的,这意味着链接标志的顺序是需要考虑的。例如,调整上述实例中的链接顺序将会产生一个编译时错误:

% gcc -o tester tester.c -lm -lshprimes ## 危险!

链接 libm.so 库的标志先出现,但是这个库中没有函数被测试程序显式调用;因此,链接器不会链接到 math.so 库。调用 sqrt 库函数仅发生在 libshprimes.so 库中包含的 prime_factors 函数。编译测试程序返回的错误是:

primes.c: undefined reference to 'sqrt'

因此,链接标志的顺序应该是通知链接器需要 sqrt 函数:

% gcc -o tester tester.c -lshprimes -lm ## 首先链接 -lshprimes

链接器在 libshprimes.so 库中发现了对库函数 sqrt 的调用,所以接下来对数学库 libm.so做了合适的链接。链接还有一个更复杂的选项,它支持链接的标志顺序。然而在本例中,最简单的方式就是恰当地排列链接标志。

下面是运行测试程序的部分输出结果:

is_prime
Sample prime ending in 1: 101
Sample prime ending in 1: 401
...
168 primes in range of 1 to a thousand.

prime_factors
prime factors of 12: 2 2 3
prime factors of 13: 13
prime factors of 876,512,779: 211 4154089

are_coprime
Are 21 and 22 coprime? yes
Are 21 and 24 coprime? no

goldbach
Number must be &gt; 2 and even: 11 is not.
4 = 2 + 2
6 = 3 + 3
...
32 =  3 + 29
32 = 13 + 19
...
100 =  3 + 97
100 = 11 + 89
...

对于 goldbach 函数,即使一个相当小的偶数值(例如 18)也许存在多个一对质数之和的组合(在这种情况下,5+13 和 7+11)。因此这种多个质数对是使得尝试证明哥德巴赫猜想变得复杂的因素之一。

封装使用库的 Python 程序

与 C 不同,Python 不是一个静态编译语言,这意味着 Python 客户示例程序必须访问动态版本而非静态版本的 primes 库。为了能这样做,Python 中有众多的支持 外部语言接口 foreign function interface (FFI)的模块(标准的或第三方的),它们允许用一种语言编写的程序来调用另一种语言编写的函数。Python 中的 ctypes 是一个标准的、相对简单的允许 Python 代码调用 C 函数的 FFI。

任何 FFI 都面临挑战,因为对接的语言不大可能会具有完全相同的数据类型。例如:primes 库使用 C 语言类型 unsigned int,而 Python 并不具有这种类型;因此 ctypes FFI 将 C 语言中的 unsigned int 类型映射为 Python 中的 int 类型。在 primes 库中发布的四个 extern C 函数中,有两个在具有显式 ctypes 配置的 Python 中会表现得更好。

C 函数 prime_factorsgoldbach 返回 void 而不是返回一个具体类型,但是 ctypes 默认会将 C 语言中的 void 替换为 Python 语言中的 int。当从 Python 代码中调用时,这两个 C 函数会从栈中返回一个随机整数值(因此,该值无任何意义)。然而,可以对 ctypes 进行配置,让这些函数返回 None (Python 中为 null 类型)。下面是对 prime_factors 函数的配置:

primes.prime_factors.restype = None

可以用类似的语句处理 goldbach 函数。

下面的交互示例(在 Python3 中)展示了在 Python 客户程序和 primes 库之间的接口是简单明了的。

>>> from ctypes import cdll

>>> primes = cdll.LoadLibrary("libshprimes.so") ## 逻辑名

>>> primes.is_prime(13)
1
>>> primes.is_prime(12)
0

>>> primes.are_coprimes(8, 24)
0
>>> primes.are_coprimes(8, 25)
1

>>> primes.prime_factors.restype = None
>>> primes.goldbach.restype = None

>>> primes.prime_factors(72)
2 2 2 3 3

>>> primes.goldbach(32)
32 = 3 + 29
32 = 13 + 19

primes 库中的函数只使用一个简单数据类型:unsigned int。如果这个 C 语言库使用复杂的类型如结构体,如果库函数传递和返回指向结构体的指针,那么比 ctypes 更强大的 FFI 更适合作为一个在 Python 语言和 C 语言之间的平滑接口。尽管如此,ctypes 示例展示了一个 Python 客户程序可以使用 C 语言编写的库。值得注意的是,用作科学计算的流行的 Numpy 库是用 C 语言编写的,然后在高级 Python API 中公开。

简单的 primes 库和高级的 Numpy 库强调了 C 语言仍然是编程语言中的通用语言。几乎每一个语言都可以与 C 语言交互,同时通过 C 语言也可以和任何其他语言交互。Python 很容易和 C 语言交互,作为另外一个例子,当 Panama 项目 成为 Java Native Interface(JNI)一个替代品后,Java 语言和 C 语言交互也会变的很容易。


via: https://opensource.com/article/21/2/linux-software-libraries

作者:Marty Kalin 选题:lujun9972 译者:萌新阿岩 校对:wxy

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

有很多令人信服的理由来用 Pulp 来托管你自己的容器注册中心。下面是其中的一些。

 title=

Linux 容器极大地简化了软件发布。将一个应用程序与它运行所需的一切打包的能力有助于提高环境的稳定性和可重复性。

虽然有许多公共注册中心可以上传、管理和分发容器镜像,但有许多令人信服的论据支持托管自己的容器注册中心。让我们来看看为什么自我托管是有意义的,以及 Pulp,一个自由开源项目,如何帮助你在企业内部环境中管理和分发容器。

为什么要托管你自己的容器注册中心

你可以考虑托管自己的容器注册中心,原因有很多:

  • 体积:一些容器镜像是相当大的。如果你有多个团队下载同一个镜像,这可能需要大量的时间,并给你的网络和预算带来压力。
  • 带宽:如果你在一个带宽有限的地区工作,或在一个出于安全原因限制访问互联网的组织中工作,你需要一个可靠的方法来管理你工作的容器。
  • 金钱:服务条款可以改变。外部容器注册中心能引入或增加速率限制阈值,这可能会对你的操作造成极大的限制。
  • 稳定性:托管在外部资源上的容器镜像可能会因为一些原因消失几天。小到你所依赖的更新容器镜像,可能会导致你想要避免的重大更改。
  • 隐私:你可能也想开发和分发容器,但你不想在公共的第三方注册中心托管。

使用 Pulp 进行自我托管

使用 Pulp,你可以避免这些问题并完全控制你的容器。

1、避免速率限制

在 Pulp 中创建容器镜像的本地缓存,可以让你组织中的每个人都能拉取到 Pulp 上托管的容器镜像,而不是从外部注册中心拉取。这意味着你可以避免速率限制,只有当你需要新的东西时才从外部注册中心进行同步。当你确实需要从外部注册中心同步容器时,Pulp 首先检查内容是否已经存在,然后再从远程注册中心启动同步。如果你受到注册中心的速率限制,你就只镜像你需要的内容,然后用 Pulp 在整个组织中分发它。

2、整理你的容器

使用 Pulp,你可以创建一个仓库,然后从任何与 Docker Registry HTTP API V2 兼容的注册中心镜像和同步容器。这包括 Docker、Google Container registry、Quay.io 等,也包括另一个 Pulp 服务器。对于你结合来自不同注册中心的镜像容器的方式,没有任何限制或约束。你可以自由地混合来自不同来源的容器。这允许你整理一套公共和私人容器,以满足你的确切要求。

3、无风险的实验

在 Pulp 中,每当你对仓库进行修改时,就会创建一个新的不可变的版本。你可以创建多个版本的仓库,例如,development、test、stage 和 production,并在它们之间推送容器。你可以自由地将容器镜像的最新更新从外部注册中心同步到 Pulp,然后让最新的变化在开发或其他环境中可用。你可以对你认为必要的仓库进行任何修改,并促进容器内容被测试团队或其他环境使用。如果出了问题,你可以回滚到早期版本。

4、只同步你需要的内容

如果你想使用 Pulp 来创建一个容器子集的本地缓存,而不是一个完整的容器注册中心,你可以从一个远程源过滤选择容器。使用 Pulp,有多种内容同步选项,以便你只存储你需要的内容,或配置你的部署,按需缓存内容。

5、在断线和空气隔离的环境中工作

如果你在一个断线或受限制的环境中工作,你可以从一个连接的 Pulp 实例中同步更新到你断连的 Pulp。目前,有计划为 Pulp 实现一个原生的空气隔离功能,以促进完全断线的工作流程。同时,作为一种变通方法,你可以使用 Skopeo 等工具来下载你需要的容器镜像,然后将它们推送到你断线的 Pulp 容器注册中心。

还有更多!

通过 Pulp,你还可以从容器文件中构建容器,将私有容器推送到仓库,并在整个组织中分发这些容器。我们将在未来的文章中对这个工作流程进行介绍。

如何开始

如果你对自我托管你的容器注册中心感兴趣,你现在就可以 安装 Pulp。随着 Pulp Ansible 安装程序的加入,安装过程已经被大量自动化和简化了。

Pulp 有一个基于插件的架构。当你安装 Pulp 时,选择容器插件和其他任何你想管理的内容插件类型。如果你想测试一下 Pulp,你今天就可以评估 Pulp 的容器化版本。

如果你有任何问题或意见,请随时在 Freenode IRC 的 #pulp 频道与我们联系,我们也很乐意在我们的邮件列表 pulp-list@redhat.com 中接受问题。


via: https://opensource.com/article/21/5/container-management-pulp

作者:Melanie Corr 选题:lujun9972 译者:geekpi 校对:wxy

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

你想在你的 Ubuntu 服务器上安装 GUI 吗?大部分情况下你是可以安装的,在本教程中我会详细介绍安装的步骤。

在正式开始之前,我来告诉你为什么服务器版的 Ubuntu 不带 GUI,以及在什么情况下你可以在服务器上安装 GUI。

为什么 Ubuntu 服务器没有 GUI?

你对比过 Ubuntu 的桌面版和服务器版会发现,两者的主要区别是服务器版缺少 GUI(比如 桌面环境)。Ubuntu 服务器基本上就是桌面版去掉图形模块后的降级版本。

这是刻意为之的。Linux 服务器需要占用系统资源来运行服务。图形化桌面环境会消耗大量的系统资源,因此服务器操作系统默认不包含桌面环境。

你可以在只有 512 MB RAM 的机器上使用 Ubuntu 服务器,但是 Ubuntu 桌面需要至少 2 GB 的 RAM 才能提供正常的功能。在服务器运行桌面环境被认为是浪费资源。

作为一个服务器使用者(或系统管理员),你应该通过命令行来使用和管理你的系统。为了达到这个水平,你需要掌握丰富的 Linux 命令相关的知识。

Typically, you have to manage a server from the command line

你是否真正需要在你的服务器上安装 GUI?

有些用户可能不太习惯在终端下使用命令行来完成工作。毕竟大部分用户是有条件通过图形界面操作计算机的。

你可能会在你的服务器上安装桌面环境并使用图形界面。大部分人不会这么干,但这是可行的。

但是这只有在你可以直接操作服务器时才行得通。假设你是在物理机器上运行它,比如服务器、台式机或笔记本电脑,抑或类似树莓派的设备。如果你可以直接操作宿主机系统,那么你还可以在运行在虚拟机上的服务器上安装。

如果你是通过 云服务器提供商如 Linode、DigitalOcean 或 AWS 部署的服务器,那么安装 GUI 就行不通了。如果你想通过图形界面来管理你的远程服务器,你可以使用 Webmin 或 Cockpit 等工具。你可以在 Web 浏览器中通过这些工具使用和管理你的服务器。相比于成熟的桌面环境,它能大大降低资源消耗。

Tools like Cockpit allow managing Linux servers graphically

如何在 Ubuntu 服务器上安装 GUI?

当你了解了基础知识后,我们一起来看看在 Ubuntu 服务器上安装桌面环境的步骤。

你需要做以下准备:

  • 已经配置好 Ubuntu 服务器,且 RAM 至少 2 GB
  • 管理员权限(你需要用 sudo 执行命令)
  • 网络连接正常(你需要下载和安装新包)

我是在虚拟机上安装的 Ubuntu 服务器,并且我可以直接操作宿主机器。我使用同样的方法在树莓派上安装了 Ubuntu 服务器

注意!

如果你是出于学习和调研等实验性的目的,那么你可以进行这些操作。请不要在生产环境的服务器上添加 GUI。后续删除 GUI 时可能会导致依赖问题,有些情况会破坏系统。

准备系统

首先,因为你将要做一些系统级的修改,因此先进行更新和升级以确保我们系统的包是最新的:

sudo apt update && sudo apt upgrade

安装桌面环境

更新结束后,你就可以安装桌面环境了。

有两种方法:

  • 使用 apt 来安装包
  • 使用一个名为 tasksel 的 Debian 工具,这个工具可以通过一条龙处理(任务)方式来安装多个包

任何一种方法都可以用完整包的方式来安装完整的桌面环境,就跟你从头安装桌面版本一样。我的意思是你可以得到跟桌面版本一样的所有的默认应用程序和工具。

如果你想使用 tasksel,需要先用下面的命令安装它:

sudo apt install tasksel

执行结束后,你就可以用 tasksel 来安装桌面环境(也叫 DE)了。

你可能知道有 很多可用的桌面环境。你可以选择自己喜欢的一个。有些桌面环境对系统资源占用得多(像 GNOME),有些占用得少(像 Xfce、MATE 等等)。

你可以自己决定使用哪个 DE。我会安装 GNOME 桌面,因为它是 Ubuntu 默认的桌面。之后我也会介绍其他桌面的安装。

如果你使用的是 tasksel,执行下面这条命令:

sudo tasksel install ubuntu-desktop

如果你使用 apt,执行下面这条命令:

sudo apt install ubuntu-desktop

这个过程可能会持续几分钟到一个小时,执行速度取决于你的网速和硬件。

我想提醒下,上面两个命令执行后都会安装完整的 GNOME 桌面环境。在本文中我两个命令都会执行,两个命令的结果是一样的。

安装和配置显示管理器

安装完成后,你需要一个名为 显示管理器 或“登录管理器”的组件。这个工具的功能是在管理用户对话和鉴权时启动 显示服务器 并加载桌面。

GNOME 桌面默认使用 GDM3 作为显示管理器,但从资源角度考虑它有点重。你可以使用更轻量级和资源友好的管理器。这里我们使用一个平台无关的显示管理器 lightdm。使用 apt 安装它:

sudo apt install lightdm

安装 lightdm 时系统会让我们选择默认的显示管理器,因为即使你可以安装多个管理器,也只能运行一个。

Use the arrow key to select an option and then use the tab key to select  and press enter

选择列表中的 “lightdm” 并点击 “”。这应该用不了几分钟。完成后你可以用下面的命令启动显示管理器并加载 GUI:

sudo service lightdm start

你可以使用下面的命令来检查当前的显示管理器:

cat /etc/X11/default-display-manager

运行后得到的结果类似这样:

Checking the default Display Manager

如果一切顺利,你现在会来到欢迎界面。

Greetings screen of GNOME Desktop with LightDM on an Ubuntu server

输入你的凭证,你的桌面就运行起来了。

GNOME Desktop fully loaded on Ubutnu server

如果你想关闭 GUI,那么打开一个终端并输入:

sudo service lightdm stop

安装其他的桌面环境(可选)

前面我说过我们可以选择不同的桌面。我们一起来看看一些其他的选项:

MATE

MATE 是基于 GNOME2 源码的轻量级桌面,它完全开源,是一个不错的选项。

用下面的命令来安装 MATE:

sudo tasksel install ubuntu-mate-core

sudo apt install ubuntu-mate-core
Lubuntu / LXDE/LXQT

如果你的系统资源有限或者电脑很旧,那么我推荐另一个轻量级的 Lubuntu。使用下面的命令安装它:

sudo tasksel install lubuntu-core

sudo apt install lubuntu-core
Xubuntu / Xfce

Xubuntu 是基于 Xfce 的 Ubuntu 衍生版,轻量、简单、稳定但可高度定制。如果你想使用它,执行下面的命令:

sudo tasksel install xubuntu-core

sudo apt install xubuntu-core

还有一些桌面没有列出来,像 KDECinnamonBudgie,不代表它们不好,它们也都是非常卓越的,你可以自己尝试安装它们。

如何从 Ubuntu 服务器上删除 GUI?

如果你觉得桌面环境占用了太多的计算资源,你可以把之前安装的包删除掉。

请注意在某些情况下删除 GUI 可能会带来依赖问题,因此请备份好重要数据或创建一个系统快照。

sudo apt remove ubuntu-desktop
sudo apt remove lightdm
sudo apt autoremove
sudo service lightdm stop

现在重启你的系统。你应该回到了正常的命令行登录。

结语

在大多数场景下是可以安装桌面 GUI 的。如果你不适应命令行,那么请使用类似 YunoHost 的发行版的服务器,YunoHost 基于 Debian 系统,你可以通过 GUI 来管理服务器。

上面说了,如果你是从头安装系统,那么我建议你使用桌面版本以避免后续的步骤。

如果你有任何问题,请在评论区留言。你会在服务器上使用 GUI 吗?参照本文后你遇到了什么问题吗?


via: https://itsfoss.com/install-gui-ubuntu-server/

作者:Chris Patrick Carias Stas 选题:lujun9972 译者:lxbwolf 校对:wxy

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