分类 技术 下的文章

以前,我们介绍 Ubuntu 推出的 Snaps。Snaps 是由 Canonical 公司为 Ubuntu 开发的,并随后移植到其他的 Linux 发行版,如 Arch、Gentoo、Fedora 等等。由于一个 snap 包中含有软件的二进制文件和其所需的所有依赖和库,所以可以在无视软件版本、在任意 Linux 发行版上安装软件。和 Snaps 类似,还有一个名为 Flatpak 的工具。也许你已经知道,为不同的 Linux 发行版打包并分发应用是一件多么费时又复杂的工作,因为不同的 Linux 发行版的库不同,库的版本也不同。现在,Flatpak 作为分发桌面应用的新框架可以让开发者完全摆脱这些负担。开发者只需构建一个 Flatpak app 就可以在多种发行版上安装使用。这真是又酷又棒!

用户也完全不用担心库和依赖的问题了,所有的东西都和 app 打包在了一起。更重要的是 Flatpak app 们都自带沙箱,而且与宿主操作系统的其他部分隔离。对了,Flatpak 还有一个很棒的特性,它允许用户在同一个系统中安装同一应用的多个版本,例如 VLC 播放器的 2.1 版、2.2 版、2.3 版。这使开发者测试同一个软件的多个版本变得更加方便。

在本文中,我们将指导你如何在 GNU/Linux 中安装 Flatpak。

安装 Flatpak

Flatpak 可以在大多数的主流 Linux 发行版上安装使用,如 Arch Linux、Debian、Fedora、Gentoo、Red Hat、Linux Mint、openSUSE、Solus、Mageia 还有 Ubuntu。

在 Arch Linux 上,使用这一条命令来安装 Flatpak:

$ sudo pacman -S flatpak

对于 Debian 用户,Flatpak 被收录进 Stretch 或之后版本的默认软件源中。要安装 Flatpak,直接执行:

$ sudo apt install flatpak

对于 Fedora 用户,Flatpak 是发行版默认安装的软件。你可以直接跳过这一步。

如果因为某种原因没有安装的话,可以执行:

$ sudo dnf install flatpak

对于 RHEL 7 用户,安装 Flatpak 的命令为:

$ sudo yum install flatpak

如果你在使用 Linux Mint 18.3,那么 Flatpat 也随系统默认安装,所以跳过这一步。

在 openSUSE Tumbleweed 中,使用 Zypper 包管理来安装 Flatpak:

$ sudo zypper install flatpak

而 Ubuntu 需要添加下面的软件源再安装 Flatpak,命令如下:

$ sudo add-apt-repository ppa:alexlarsson/flatpak
$ sudo apt update
$ sudo apt install flatpak

Gnome 提供了一个 Flatpak 插件,安装它就可以使用图形界面来安装 Flatpak app 了。插件的安装命令为:

$ sudo apt install gnome-software-plugin-flatpak

如果你是用发行版没有在上述的说明里,请你参考官方安装指南

开始使用 Flatpak

有不少流行应用都支持 Flatpak 安装,如 Gimp、Kdenlive、Steam、Spotify、Visual Sudio Code 等。

下面让我来一起学习 flatpak 的基本操作命令。

首先,我们需要添加远程仓库。

添加软件仓库

添加 Flathub 仓库:

Flathub 是一个包含了几乎所有 flatpak 应用的仓库。运行这条命令来启用它:

$ sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo

对于流行应用来说,Flathub 已经可以满足需求。如果你想试试 GNOME 应用的话,可以添加 GNOME 的仓库。

添加 GNOME 仓库:

GNOME 仓库包括了所有的 GNOME 核心应用,它提供了两种版本: 稳定版 stable 每日构建版 nightly

使用下面的命令来添加 GNOME 稳定版仓库:

$ wget https://sdk.gnome.org/keys/gnome-sdk.gpg
$ sudo flatpak remote-add --gpg-import=gnome-sdk.gpg --if-not-exists gnome-apps https://sdk.gnome.org/repo-apps/

需要注意的是,GNOME 稳定版仓库中的应用需要 3.20 版本的 org.gnome.Platform 运行时环境

安装稳定版运行时环境,请执行:

$ sudo flatpak remote-add --gpg-import=gnome-sdk.gpg gnome https://sdk.gnome.org/repo/

如果想使用每日构建版的 GNOME 仓库,使用如下的命令:

$ wget https://sdk.gnome.org/nightly/keys/nightly.gpg
$ sudo flatpak remote-add --gpg-import=nightly.gpg --if-not-exists gnome-nightly-apps https://sdk.gnome.org/nightly/repo-apps/

同样,每日构建版的 GNOME 仓库也需要 org.gnome.Platform 运行时环境的每日构建版本

执行下面的命令安装每日构建版的运行时环境:

$ sudo flatpak remote-add --gpg-import=nightly.gpg gnome-nightly https://sdk.gnome.org/nightly/repo/

查看软件仓库

要查看已经添加的软件仓库,执行下面的命令:

$ flatpak remotes
Name Options
flathub system
gnome system
gnome-apps system
gnome-nightly system
gnome-nightly-apps system

如你所见,上述命令会列出你添加到系统中的软件仓库。此外,执行结果还表明了软件仓库的配置是 用户级 per-user 还是 系统级 system-wide

删除软件仓库

要删除软件仓库,例如 flathub,用这条命令:

$ sudo flatpak remote-delete flathub

这里的 flathub 是软件仓库的名字。

安装 Flatpak 应用

这一节,我们将学习如何安装 flatpak 应用。

要安装一个应用,只要一条命令就能完成:

$ sudo flatpak install flathub com.spotify.Client

所有的稳定版 GNOME 软件仓库中的应用,都使用“stable”作为版本名。

例如,想从稳定版 GNOME 软件仓库中安装稳定版 Evince,就执行:

$ sudo flatpak install gnome-apps org.gnome.Evince stable

所有的每日构建版 GNOME 仓库中的应用,都使用“master”作为版本名。

例如,要从每日构建版 GNOME 软件仓库中安装 gedit 的每次构建版本,就执行:

$ sudo flatpak install gnome-nightly-apps org.gnome.gedit master

如果不希望应用安装在 系统级 system-wide ,而只安装在 用户级 per-user ,那么你可以这样安装软件:

$ flatpak install --user <name-of-app>

所有的应用都会被存储在 $HOME/.var/app/ 目录下.

$ ls $HOME/.var/app/
com.spotify.Client

执行 Flatpak 应用

你可以直接使用 应用启动器 application launcher 来运行已安装的 Flatpak 应用。如果你想从命令行启动的话,以 Spotify 为例,执行下面的命令:

$ flatpak run com.spotify.Client

列出已安装的 Flatpak 应用

要查看已安装的应用程序和运行时环境,执行:

$ flatpak list

想只查看已安装的应用,那就用这条命令:

$ flatpak list --app

如果想查询已添加的软件仓库中的可安装程序和可安装的运行时环境,使用命令:

$ flatpak remote-ls

只列出可安装的应用程序的命令是:

$ flatpak remote-ls --app

查询指定远程仓库中的所有可安装的应用程序和运行时环境,这里以 gnome-apps 为例,执行命令:

$ flatpak remote-ls gnome-apps

只列出可安装的应用程序,这里以 flathub 为例:

$ flatpak remote-ls flathub --app

更新应用程序

更新所有的 Flatpak 应用程序,执行:

$ flatpak update

更新指定的 Flatpak 应用程序,执行:

$ flatpak update com.spotify.Client

获取应用详情

执行下面的命令来查看已安装应用程序的详细信息:

$ flatpak info io.github.mmstick.FontFinder

输出样例:

Ref: app/io.github.mmstick.FontFinder/x86_64/stable
ID: io.github.mmstick.FontFinder
Arch: x86_64
Branch: stable
Origin: flathub
Date: 2018-04-11 15:10:31 +0000
Subject: Workaround appstream issues (391ef7f5)
Commit: 07164e84148c9fc8b0a2a263c8a468a5355b89061b43e32d95008fc5dc4988f4
Parent: dbff9150fce9fdfbc53d27e82965010805f16491ec7aa1aa76bf24ec1882d683
Location: /var/lib/flatpak/app/io.github.mmstick.FontFinder/x86_64/stable/07164e84148c9fc8b0a2a263c8a468a5355b89061b43e32d95008fc5dc4988f4
Installed size: 2.5 MB
Runtime: org.gnome.Platform/x86_64/3.28

删除应用程序

要删除一个 Flatpak 应用程序,这里以 spotify 为例,执行:

$ sudo flatpak uninstall com.spotify.Client

如果你需要更多信息,可以参考 Flatpak 的帮助。

$ flatpak --help

到此,希望你对 Flatpak 有了一些基础了解。

如果你觉得这篇指南有些帮助,请在你的社交媒体上分享它来支持我们。

稍后还有更多精彩内容,敬请期待~


via: https://www.ostechnix.com/flatpak-new-framework-desktop-applications-linux/

作者:SK 选题:lujun9972 译者:wwhio 校对:wxy

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

OK01 课程讲解了树莓派如何入门,以及在树莓派上如何启用靠近 RCA 和 USB 端口的 OK 或 ACT 的 LED 指示灯。这个指示灯最初是为了指示 OK 状态的,但它在第二版的树莓派上被改名为 ACT。

1、入门

我们假设你已经访问了下载页面,并且已经获得了必需的 GNU 工具链。也下载了一个称为操作系统模板的文件。请下载这个文件并在一个新目录中解开它。

2、开始

现在,你已经展开了这个模板文件,在 source 目录中创建一个名为 main.s 的文件。这个文件包含了这个操作系统的代码。具体来看,这个文件夹的结构应该像下面这样:

build/
   (empty)
source/
   main.s
kernel.ld
LICENSE
Makefile

用文本编辑器打开 main.s 文件,这样我们就可以输入汇编代码了。树莓派使用了称为 ARMv6 的汇编代码变体,这就是我们即将要写的汇编代码类型。

扩展名为 .s 的文件一般是汇编代码,需要记住的是,在这里它是 ARMv6 的汇编代码。

首先,我们复制下面的这些命令。

.section .init
.globl _start
_start:

实际上,上面这些指令并没有在树莓派上做任何事情,它们是提供给汇编器的指令。汇编器是一个转换程序,它将我们能够理解的汇编代码转换成树莓派能够理解的机器代码。在汇编代码中,每个行都是一个新的命令。上面的第一行告诉汇编器 1 在哪里放我们的代码。我们提供的模板中将它放到一个名为 .init 的节中的原因是,它是输出的起始点。这很重要,因为我们希望确保我们能够控制哪个代码首先运行。如果不这样做,首先运行的代码将是按字母顺序排在前面的代码!.section 命令简单地告诉汇编器,哪个节中放置代码,从这个点开始,直到下一个 .section 或文件结束为止。

在汇编代码中,你可以跳行、在命令前或后放置空格去提升可读性。

接下来两行是停止一个警告消息,它们并不重要。 2

3、第一行代码

现在,我们正式开始写代码。计算机执行汇编代码时,是简单地一行一行按顺序执行每个指令,除非明确告诉它不这样做。每个指令都是开始于一个新行。

复制下列指令。

ldr r0,=0x20200000
ldr reg,=val 将数字 val 加载到名为 reg 的寄存器中。

那是我们的第一个命令。它告诉处理器将数字 0x20200000 保存到寄存器 r0 中。在这里我需要去回答两个问题, 寄存器 register 是什么?0x20200000 是一个什么样的数字?

寄存器在处理器中就是一个极小的内存块,它是处理器保存正在处理的数字的地方。处理器中有很多寄存器,很多都有专门的用途,我们在后面会一一接触到它们。最重要的有十三个(命名为 r0r1r2、…、r9r10r11r12),它们被称为通用寄存器,你可以使用它们做任何计算。由于是写我们的第一行代码,我们在示例中使用了 r0,当然你可以使用它们中的任何一个。只要后面始终如一就没有问题。

树莓派上的一个单独的寄存器能够保存任何介于 04,294,967,295(含)之间的任意整数,它可能看起来像一个很大的内存,实际上它仅有 32 个二进制比特。

0x20200000 确实是一个数字。只不过它是以十六进制表示的。下面的内容详细解释了十六进制的相关信息:

延伸阅读:十六进制解释

十六进制是另一种表示数字的方式。你或许只知道十进制的数字表示方法,十进制共有十个数字:0123456789。十六进制共有十六个数字:0123456789abcdef

你可能还记得十进制是如何用位制来表示的。即最右侧的数字是个位,紧接着的左边一位是十位,再接着的左边一位是百位,依此类推。也就是说,它的值是 100 × 百位的数字,再加上 10 × 十位的数字,再加上 1 × 个位的数字。

567 is 5 hundreds, 6 tens and 7 units.

从数学的角度来看,我们可以发现规律,最右侧的数字是 10 0 = 1s,紧接着的左边一位是 10 1 = 10s,再接着是 10 2 = 100s,依此类推。我们设定在系统中,0 是最低位,紧接着是 1,依此类推。但如果我们使用一个不同于 10 的数字为幂底会是什么样呢?我们在系统中使用的十六进制就是这样的一个数字。

567 is 5x10^2+6x10^1+7x10^0

567 = 5x10^2+6x10^1+7x10^0 = 2x16^2+3x16^1+7x16^0

上面的数学等式表明,十进制的数字 567 等于十六进制的数字 237。通常我们需要在系统中明确它们,我们使用下标 10 表示它是十进制数字,用下标 16 表示它是十六进制数字。由于在汇编代码中写上下标的小数字很困难,因此我们使用 0x 来表示它是一个十六进制的数字,因此 0x237 的意思就是 237 16

那么,后面的 abcdef 又是什么呢?好问题!在十六进制中为了能够写每个数字,我们就需要额外的东西。例如 9 16 = 9×16 0 = 9 10 ,但是 10 16 = 1×16 1 + 1×16 0 = 16 10 。因此,如果我们只使用 0、1、2、3、4、5、6、7、8 和 9,我们就无法写出 10 10 、11 10 、12 10 、13 10 、14 10 、15 10 。因此我们引入了 6 个新的数字,这样 a 16 = 10 10 、b 16 = 11 10 、c 16 = 12 10 、d 16 = 13 10 、e 16 = 14 10 、f 16 = 15 10

所以,我们就有了另一种写数字的方式。但是我们为什么要这么麻烦呢?好问题!由于计算机总是工作在二进制中,事实证明,十六进制是非常有用的,因为每个十六进制数字正好是四个二进制数字的长度。这种方法还有另外一个好处,那就是许多计算机的数字都是十六进制的整数倍,而不是十进制的整数倍。比如,我在上面的汇编代码中使用的一个数字 20200000 16 。如果我们用十进制来写,它就是一个不太好记住的数字 538968064 10

我们可以用下面的简单方法将十进制转换成十六进制:

Conversion example

  1. 我们以十进制数字 567 为例来说明。
  2. 将十进制数字 567 除以 16 并计算其余数。例如 567 ÷ 16 = 35 余数为 7。
  3. 在十六进制中余数就是答案中的最后一位数字,在我们的例子中它是 7。
  4. 重复第 2 步和第 3 步,直到除法结果的整数部分为 0。例如 35 ÷ 16 = 2 余数为 3,因此 3 就是答案中的下一位。2 ÷ 16 = 0 余数为 2,因此 2 就是答案的接下来一位。
  5. 一旦除法结果的整数部分为 0 就结束了。答案就是反序的余数,因此 567 10 = 237 16

转换十六进制数字为十进制,也很容易,将数字展开即可,因此 237 16 = 2×16 2 + 3×16 1 +7 ×16 0 = 2×256 + 3×16 + 7×1 = 512 + 48 + 7 = 567。

因此,我们所写的第一个汇编命令是将数字 20200000 16 加载到寄存器 r0 中。那个命令看起来似乎没有什么用,但事实并非如此。在计算机中,有大量的内存块和设备。为了能够访问它们,我们给每个内存块和设备指定了一个地址。就像邮政地址或网站地址一样,它用于标识我们想去访问的内存块或设备的位置。计算机中的地址就是一串数字,因此上面的数字 20200000 16 就是 GPIO 控制器的地址。这个地址是由制造商的设计所决定的,他们也可以使用其它地址(只要不与其它的冲突即可)。我之所以知道这个地址是 GPIO 控制器的地址是因为我看了它的手册, 3 地址的使用没有专门的规范(除了它们都是以十六进制表示的大数以外)。

4、启用输出

A diagram showing key parts of the GPIO controller.

阅读了手册可以得知,我们需要给 GPIO 控制器发送两个消息。我们必须用它的语言告诉它,如果我们这样做了,它将非常乐意实现我们的意图,去打开 OK 的 LED 指示灯。幸运的是,它是一个非常简单的芯片,为了让它能够理解我们要做什么,只需要给它设定几个数字即可。

mov r1,#1
lsl r1,#18
str r1,[r0,#4]

mov reg,#val 将数字 val 放到名为 reg 的寄存器中。

lsl reg,#val 将寄存器 reg 中的二进制操作数左移 val 位。

str reg,[dest,#val] 将寄存器 reg 中的数字保存到地址 dest + val 上。

这些命令的作用是在 GPIO 的第 16 号插针上启用输出。首先我们在寄存器 r1 中获取一个必需的值,接着将这个值发送到 GPIO 控制器。因此,前两个命令是尝试取值到寄存器 r1 中,我们可以像前面一样使用另一个命令 ldr 来实现,但 lsl 命令对我们后面能够设置任何给定的 GPIO 针比较有用,因此从一个公式中推导出值要比直接写入来好一些。表示 OK 的 LED 灯是直接连线到 GPIO 的第 16 号针脚上的,因此我们需要发送一个命令去启用第 16 号针脚。

寄存器 r1 中的值是启用 LED 针所需要的。第一行命令将数字 1 10 放到 r1 中。在这个操作中 mov 命令要比 ldr 命令快很多,因为它不需要与内存交互,而 ldr 命令是将需要的值从内存中加载到寄存器中。尽管如此,mov 命令仅能用于加载某些值。 4 在 ARM 汇编代码中,基本上每个指令都使用一个三字母代码表示。它们被称为助记词,用于表示操作的用途。mov 是 “move” 的简写,而 ldr 是 “load register” 的简写。mov 是将第二个参数 #1 移动到前面的 r1 寄存器中。一般情况下,# 肯定是表示一个数字,但我们已经看到了不符合这种情况的一个反例。

第二个指令是 lsl(逻辑左移)。它的意思是将第一个参数的二进制操作数向左移第二个参数所表示的位数。在这个案例中,将 1 10 (即 1 2 )向左移 18 位(将它变成 1000000000000000000 2=262144 10 )。

如果你不熟悉二进制表示法,可以看下面的内容:

延伸阅读: 二进制解释

与十六进制一样,二进制是写数字的另一种方法。在二进制中只有两个数字,即 01。它在计算机中非常有用,因为我们可以用电路来实现它,即电流能够通过电路表示为 1,而电流不能通过电路表示为 0。这就是计算机能够完成真实工作和做数学运算的原理。尽管二进制只有两个数字,但它却能够表示任何一个数字,只是写起来有点长而已。

567 in decimal = 1000110111 in binary

这个图片展示了 567 10 的二进制表示是 1000110111 2 。我们使用下标 2 来表示这个数字是用二进制写的。

我们在汇编代码中大量使用二进制的其中一个巧合之处是,数字可以很容易地被 2 的幂(即 124816)乘或除。通常乘法和除法都是非常难的,而在某些特殊情况下却变得非常容易,所以二进制非常重要。

13*4 = 52, 1101*100=110100

将一个二进制数字左移 n 位就相当于将这个数字乘以 2 n。因此,如果我们想将一个数乘以 4,我们只需要将这个数字左移 2 位。如果我们想将它乘以 256,我们只需要将它左移 8 位。如果我们想将一个数乘以 12 这样的数字,我们可以有一个替代做法,就是先将这个数乘以 8,然后再将那个数乘以 4,最后将两次相乘的结果相加即可得到最终结果(N × 12 = N × (8 + 4) = N × 8 + N × 4)。

53/16 = 3, 110100/10000=11

右移一个二进制数 n 位就相当于这个数除以 2 n 。在右移操作中,除法的余数位将被丢弃。不幸的是,如果对一个不能被 2 的幂次方除尽的二进制数字做除法是非常难的,这将在 课程 9 Screen04 中讲到。

Binary Terminology

这个图展示了二进制常用的术语。一个 比特 bit 就是一个单独的二进制位。一个“ 半字节 nibble “ 是 4 个二进制位。一个 字节 byte 是 2 个半字节,也就是 8 个比特。 半字 half 是指一个字长度的一半,这里是 2 个字节。 word 是指处理器上寄存器的大小,因此,树莓派的字长是 4 字节。按惯例,将一个字最高有效位标识为 31,而将最低有效位标识为 0。顶部或最高位表示最高有效位,而底部或最低位表示最低有效位。一个 kilobyte(KB)就是 1000 字节,一个 megabyte 就是 1000 KB。这样表示会导致一些困惑,到底应该是 1000 还是 1024(二进制中的整数)。鉴于这种情况,新的国际标准规定,一个 KB 等于 1000 字节,而一个 Kibibyte(KiB)是 1024 字节。一个 Kb 是 1000 比特,而一个 Kib 是 1024 比特。

树莓派默认采用小端法,也就是说,从你刚才写的地址上加载一个字节时,是从一个字的低位字节开始加载的。

再强调一次,我们只有去阅读手册才能知道我们所需要的值。手册上说,GPIO 控制器中有一个 24 字节的集合,由它来决定 GPIO 针脚的设置。第一个 4 字节与前 10 个 GPIO 针脚有关,第二个 4 字节与接下来的 10 个针脚有关,依此类推。总共有 54 个 GPIO 针脚,因此,我们需要 6 个 4 字节的一个集合,总共是 24 个字节。在每个 4 字节中,每 3 个比特与一个特定的 GPIO 针脚有关。我们想去启用的是第 16 号 GPIO 针脚,因此我们需要去设置第二组 4 字节,因为第二组的 4 字节用于处理 GPIO 针脚的第 10-19 号,而我们需要第 6 组 3 比特,它在上面的代码中的编号是 18(6×3)。

最后的 str(“store register”)命令去保存第一个参数中的值,将寄存器 r1 中的值保存到后面的表达式计算出来的地址上。这个表达式可以是一个寄存器,在上面的例子中是 r0,我们知道 r0 中保存了 GPIO 控制器的地址,而另一个值是加到它上面的,在这个例子中是 #4。它的意思是将 GPIO 控制器地址加上 4 得到一个新的地址,并将寄存器 r1 中的值写到那个地址上。那个地址就是我们前面提到的第二组 4 字节的位置,因此,我们发送我们的第一个消息到 GPIO 控制器上,告诉它准备启用 GPIO 第 16 号针脚的输出。

5、生命的信号

现在,LED 已经做好了打开准备,我们还需要实际去打开它。意味着需要给 GPIO 控制器发送一个消息去关闭 16 号针脚。是的,你没有看错,就是要发送一个关闭的消息。芯片制造商认为,在 GPIO 针脚关闭时打开 LED 更有意义。 5 硬件工程师经常做这种反常理的决策,似乎是为了让操作系统开发者保持警觉。可以认为是给自己的一个警告。

mov r1,#1
lsl r1,#16
str r1,[r0,#40]

希望你能够认识上面全部的命令,先不要管它的值。第一个命令和前面一样,是将值 1 推入到寄存器 r1 中。第二个命令是将二进制的 1 左移 16 位。由于我们是希望关闭 GPIO 的 16 号针脚,我们需要在下一个消息中将第 16 比特设置为 1(想设置其它针脚只需要改变相应的比特位即可)。最后,我们写这个值到 GPIO 控制器地址加上 40 10 的地址上,这将使那个针脚关闭(加上 28 将打开针脚)。

6、永远幸福快乐

似乎我们现在就可以结束了,但不幸的是,处理器并不知道我们做了什么。事实上,处理器只要通电,它就永不停止地运转。因此,我们需要给它一个任务,让它一直运转下去,否则,树莓派将进入休眠(本示例中不会,LED 灯会一直亮着)。

loop$:
b loop$

name: 下一行的名字。

b label 下一行将去标签 label 处运行。

第一行不是一个命令,而是一个标签。它给下一行命名为 loop$,这意味着我们能够通过名字来指向到该行。这就称为一个标签。当代码被转换成二进制后,标签将被丢弃,但这对我们通过名字而不是数字(地址)找到行比较有用。按惯例,我们使用一个 ​$ 表示这个标签只对这个代码块中的代码起作用,让其它人知道,它不对整个程序起作用。b(“branch”)命令将去运行指定的标签中的命令,而不是去运行它后面的下一个命令。因此,下一行将再次去运行这个 b 命令,这将导致永远循环下去。因此处理器将进入一个无限循环中,直到它安全关闭为止。

代码块结尾的一个空行是有意这样写的。GNU 工具链要求所有的汇编代码文件都是以空行结束的,因此,这就可以你确实是要结束了,并且文件没有被截断。如果你不这样处理,在汇编器运行时,你将收到烦人的警告。

7、树莓派上场

由于我们已经写完了代码,现在,我们可以将它上传到树莓派中了。在你的计算机上打开一个终端,改变当前工作目录为 source 文件夹的父级目录。输入 make 然后回车。如果报错,请参考排错章节。如果没有报错,你将生成三个文件。 kernel.img 是你的编译后的操作系统镜像。kernel.list 是你写的汇编代码的一个清单,它实际上是生成的。这在将来检查程序是否正确时非常有用。kernel.map 文件包含所有标签结束位置的一个映射,这对于跟踪值非常有用。

为安装你的操作系统,需要先有一个已经安装了树莓派操作系统的 SD 卡。如果你浏览 SD 卡中的文件,你应该能看到一个名为 kernel.img 的文件。将这个文件重命名为其它名字,比如 kernel_linux.img。然后,复制你编译的 kernel.img 文件到 SD 卡中原来的位置,这将用你的操作系统镜像文件替换现在的树莓派操作系统镜像。想切换回来时,只需要简单地删除你自己的 kernel.img 文件,然后将前面重命名的文件改回 kernel.img 即可。我发现,保留一个原始的树莓派操作系统的备份是非常有用的,万一你要用到它呢。

将这个 SD 卡插入到树莓派,并打开它的电源。这个 OK 的 LED 灯将亮起来。如果不是这样,请查看故障排除页面。如果一切如愿,恭喜你,你已经写出了你的第一个操作系统。课程 2 OK02 将指导你让 LED 灯闪烁和关闭闪烁。


  1. 是的,我说错了,它告诉的是链接器,它是另一个程序,用于将汇编器转换过的几个代码文件链接到一起。直接说是汇编器也没有大问题。
  2. 其实它们对你很重要。由于 GNU 工具链主要用于开发操作系统,它要求入口点必须是名为 _start 的地方。由于我们是开发一个操作系统,无论什么时候,它总是从 _start 开时的,而我们可以使用 .section .init 命令去设置它。因此,如果我们没有告诉它入口点在哪里,就会使工具链困惑而产生警告消息。所以,我们先定义一个名为 _start 的符号,它是所有人可见的(全局的),紧接着在下一行生成符号 _start 的地址。我们很快就讲到这个地址了。
  3. 本教程的设计减少了你阅读树莓派开发手册的难度,但是,如果你必须要阅读它,你可以在这里 SoC-Peripherals.pdf 找到它。由于添加了混淆,手册中 GPIO 使用了不同的地址系统。我们的操作系统中的地址 0x20200000 对应到手册中是 0x7E200000。
  4. mov 能够加载的值只有前 8 位是 1 的二进制表示的值。换句话说就是一个 0 后面紧跟着 8 个 10
  5. 一个很友好的硬件工程师是这样向我解释这个问题的:

原因是现在的芯片都是用一种称为 CMOS 的技术来制成的,它是互补金属氧化物半导体的简称。互补的意思是每个信号都连接到两个晶体管上,一个是使用 N 型半导体的材料制成,它用于将电压拉低,而另一个使用 P 型半导体材料制成,它用于将电压升高。在任何时刻,仅有一个半导体是打开的,否则将会短路。P 型材料的导电性能不如 N 型材料。这意味着三倍大的 P 型半导体材料才能提供与 N 型半导体材料相同的电流。这就是为什么 LED 总是通过降低为低电压来打开它,因为 N 型半导体拉低电压比 P 型半导体拉高电压的性能更强。

还有一个原因。早在上世纪七十年代,芯片完全是由 N 型材料制成的(NMOS),P 型材料部分使用了一个电阻来代替。这意味着当信号为低电压时,即便它什么事都没有做,芯片仍然在消耗能量(并发热)。你的电话装在口袋里什么事都不做,它仍然会发热并消耗你的电池电量,这不是好的设计。因此,信号设计成 “活动时低”,而不活动时为高电压,这样就不会消耗能源了。虽然我们现在已经不使用 NMOS 了,但由于 N 型材料的低电压信号比 P 型材料的高电压信号要快,所以仍然使用了这种设计。通常在一个 “活动时低” 信号名字上方会有一个条型标记,或者写作 SIGNAL_n/SIGNAL。但是即便这样,仍然很让人困惑,那怕是硬件工程师,也不可避免这种困惑!


via: https://www.cl.cam.ac.uk/projects/raspberrypi/tutorials/os/ok01.html

作者:Robert Mullins 选题:lujun9972 译者:qhwdw 校对:wxy

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

一篇涵盖了在 Ubuntu 和其他 Linux 发行版中使用 PPA 的几乎所有问题的深入的文章。

如果你一直在使用 Ubuntu 或基于 Ubuntu 的其他 Linux 发行版,例如 Linux Mint、Linux Lite、Zorin OS 等,你可能会遇到以下三种神奇的命令:

sudo add-apt-repository ppa:dr-akulavich/lighttable
sudo apt-get update
sudo apt-get install lighttable-installer

许多网站推荐使用类似于以上几行的形式 在 Ubuntu 中安装应用程序。这就是所谓的使用 PPA 安装应用程序。

但什么是 PPA?为什么要用它?使用 PPA 安全吗?如何正确使用 PPA?如何删除 PPA?

我将在这个详细的指南中回答上述所有问题。即使你已经了解了一些关于 PPA 的事情,我相信这篇文章仍然会让你了解这方面的更多知识。

请注意我正在使用 Ubuntu 撰写本文。因此,我几乎在各个地方都使用了 Ubuntu 这个术语,但文中的说明和步骤也适用于其他基于 Debian/Ubuntu 的发行版。

什么是 PPA?为什么要使用 PPA?

Everything you need to know about PPA in Ubuntu Linux

PPA 表示 个人软件包存档 Personal Package Archive

这样说容易理解吗?可能不是很容易。

在了解 PPA 之前,你应该了解 Linux 中软件仓库的概念。关于软件仓库,在这里我不会详述。

软件仓库和包管理的概念

软件仓库是一组文件,其中包含各种软件及其版本的信息,以及校验和等其他一些详细信息。每个版本的 Ubuntu 都有自己的四个官方软件仓库:

  • Main - Canonical 支持的自由开源软件。
  • Universe - 社区维护的自由开源软件。
  • Restricted - 设备的专有驱动程序。
  • Multiverse - 受版权或法律问题限制的软件。

你可以在 这里 看到所有版本的 Ubuntu 的软件仓库。你可以浏览并转到各个仓库。例如,可以在 这里 找到 Ubuntu 16.04 的主存储库(Main)。

所以,PPA 基本上是一个包含软件信息的网址。那你的系统又是如何知道这些仓库的位置的呢?

这些信息存储在 /etc/apt 目录中的 sources.list 文件中。如果查看此文件的内容,你就会看到里面有软件仓库的网址。# 开头的行将被忽略。

这样的话,当你运行 sudo apt update 命令时,你的系统将使用 APT 工具 来检查软件仓库并将软件及其版本信息存储在缓存中。当你使用 sudo apt install package_name 命令时,它通过该信息从实际存储软件的网址获取该软件包。

如果软件仓库中没有关于某个包的信息,你将看到如下错误:

E: Unable to locate package

此时,建议阅读我的 apt 命令使用指南 一文,这将帮你更好地理解 aptupdate 等命令。

以上是关于软件仓库的内容。但什么是 PPA?PPA 和软件仓库又有什么关联呢?

为什么要用 PPA?

如你所见,Ubuntu 对系统中的软件进行管理,更重要的是控制你在系统上获得哪个版本的软件。但想象一下开发人员发布了软件的新版本的情况。

Ubuntu 不会立即提供该新版本的软件。需要一个步骤来检查此新版本的软件是否与系统兼容,从而可以确保系统的稳定性。

但这也意味着它需要经过几周才能在 Ubuntu 上可用,在某些情况下,这可能需要几个月的时间。不是每个人都想等待那么长时间才能获得他们最喜欢的软件的新版本。

类似地,假设有人开发了一款软件,并希望 Ubuntu 将该软件包含在官方软件仓库中。在 Ubuntu 做出决定并将其包含在官方存软件仓库之前,还需要几个月的时间。

另一种情况是在 beta 测试阶段。即使官方软件仓库中提供了稳定版本的软件,软件开发人员也可能希望某些终端用户测试他们即将发布的版本。他们是如何使终端用户对即将发布的版本进行 beta 测试的呢?

通过 PPA!

如何使用 PPA?PPA 是怎样工作的?

正如我已经告诉过你的那样,PPA 代表 个人软件包存档 Personal Package Archive 。在这里注意 “个人” 这个词,它暗示了这是开发人员独有的东西,并没有得到分发的正式许可。

Ubuntu 提供了一个名为 Launchpad 的平台,使软件开发人员能够创建自己的软件仓库。终端用户,也就是你,可以将 PPA 仓库添加到 sources.list 文件中,当你更新系统时,你的系统会知道这个新软件的可用性,然后你可以使用标准的 sudo apt install 命令安装它。

sudo add-apt-repository ppa:dr-akulavich/lighttable
sudo apt-get update
sudo apt-get install lighttable-installer

概括一下上面三个命令:

  • sudo add-apt-repository <PPA_info> <- 此命令将 PPA 仓库添加到列表中。
  • sudo apt-get update <- 此命令更新可以在当前系统上安装的软件包列表。
  • sudo apt-get install <package_in_PPA> <- 此命令安装软件包。

你会发现使用 sudo apt update 命令非常重要,否则你的系统将无法知道新软件包何时可用。

现在让我们更详细地看一下第一个命令。

sudo add-apt-repository ppa:dr-akulavich/lighttable

你会注意到此命令没有软件仓库的 URL。这是因为该工具被设计成将 URL 信息抽象之后再展示给你。

小小注意一下:如果你添加的是 ppa:dr-akulavich/lighttable,你会得到 Light Table。但是如果你添加 ppa:dr-akulavich,你将得到 “上层软件仓库” 中的所有仓库或软件包。它是按层级划分的。

基本上,当您使用 add-apt-repository 添加 PPA 时,它将执行与手动运行这些命令相同的操作:

deb http://ppa.launchpad.net/dr-akulavich/lighttable/ubuntu YOUR_UBUNTU_VERSION_HERE main
deb-src http://ppa.launchpad.net/dr-akulavich/lighttable/ubuntu YOUR_UBUNTU_VERSION_HERE main

以上两行是将任何软件仓库添加到你系统的 sources.list 文件的传统方法。但 PPA 会自动为你完成这些工作,无需考虑确切的软件仓库 URL 和操作系统版本。

此处不那么重要的一点是,当你使用 PPA 时,它不会更改原始的 sources.list 文件。相反,它在 /etc/apt/sources.d 目录中创建了两个文件,一个 .list 文件和一个带有 .save 后缀的备份文件。

Using a PPA in Ubuntu

PPA 创建了单独的 sources.list 文件

带有后缀 .list 的文件含有添加软件仓库的信息的命令。

PPA add repository information

一个 PPA 的 source.list 文件的内容

这是一种安全措施,可以确保添加的 PPA 不会和原始的 sources.list 文件弄混,它还有助于移除 PPA。

为什么使用 PPA?为何不用 DEB 包

你可能会问为什么要使用 PPA,PPA 需要通过命令行使用,而不是每个人都喜欢用命令行。为什么不直接分发可以图形方式安装的 DEB 包呢?

答案在于更新的过程。如果使用 DEB 包安装软件,将无法保证在运行 sudo apt updatesudo apt upgrade 命令时,已安装的软件会被更新为较新的版本。

这是因为 apt 的升级过程依赖于 sources.list 文件。如果文件中没有相应的软件条目,则不会通过标准软件更新程序获得更新。

那么这是否意味着使用 DEB 安装的软件永远不会得到更新?不是的。这取决于 DEB 包的创建方式。

一些开发人员会自动在 sources.list 中添加一个条目,这样软件就可以像普通软件一样更新。谷歌 Chrome 浏览器就是这样一个例子。

某些软件会在运行时通知你有新版本可用。你必须下载新的 DEB 包并再次运行,来将当前软件更新为较新版本。Oracle Virtual Box 就是这样一个例子。

对于其余的 DEB 软件包,你必须手动查找更新,这很不方便,尤其是在你的软件面向 Beta 测试者时,你需要频繁的添加很多更新。这正是 PPA 要解决的问题。

官方 PPA vs 非官方 PPA

你或许听过官方 PPA 或非官方 PPA 这个词,二者有什么不同呢?

开发人员为他们的软件创建的 PPA 称为官方 PPA。很明显,这是因为它来自项目开发者。

但有时,个人会创建由其他开发人员所创建的项目的 PPA。

为什么会有人这样做? 因为许多开发人员只提供软件的源代码,而且你也知道 在 Linux 中从源代码安装软件 是一件痛苦的事情,并不是每个人都可以或者会这样做。

这就是志愿者自己从这些源代码创建 PPA 以便其他用户可以轻松安装软件的原因。毕竟,使用这 3 行命令比从源代码安装要容易得多。

确保你的 Linux 发行版本可以使用 PPA

当在 Ubuntu 或任何其他基于 Debian 的发行版中使用 PPA 时,你应该记住一些事情。

并非每个 PPA 都适用于你的特定版本。你应该知道正在使用 哪个版本的 Ubuntu。版本的开发代号很重要,因为当你访问某个 PPA 的页面时,你可以看到该 PPA 都支持哪些版本的 Ubuntu。

对于其他基于 Ubuntu 的发行版,你可以查看 /etc/os-release 的内容来 找出 Ubuntu 版本 的信息。

Verify PPA availability for Ubuntu version

检查 PPA 是否适用于你的 Ubuntu 版本

如何知道 PPA 的网址呢?只需在网上搜索 PPA 的名称,如 ppa:dr-akulavich/lighttable,第一个搜索结果来自 Launchpad,这是托管 PPA 的官方平台。你也可以转到 Launchpad 并直接在那里搜索所需的 PPA。

如果不验证是否适用当前的版本就添加 PPA,当尝试安装不适用于你的系统版本的软件时,可能会看到类似下面的错误。

E: Unable to locate package

更糟糕的是,因为它已经添加到你的 source.list 中,每次运行软件更新程序时,你都会看到 “无法下载软件仓库信息” 的错误。

Failed to download repository information Ubuntu 13.04

如果你在终端中运行 sudo apt update,错误提示将包含导致此问题的仓库的更多详细信息。你可以在 sudo apt update 的输出内容结尾看到类似的内容:

W: Failed to fetch http://ppa.launchpad.net/venerix/pkg/ubuntu/dists/raring/main/binary-i386/Packages  404  Not Found
E: Some index files failed to download. They have been ignored, or old ones used instead.

上面的错误提示说的很明白,是因为系统找不到当前版本对应的仓库。还记得我们之前看到的仓库结构吗?APT 将尝试在 http://ppa.launchpad.net/<PPA_NAME>/ubuntu/dists/<Ubuntu_Version> 中寻找软件信息。

如果特定版本的 PPA 不可用,它将永远无法打开 URL,你会看到著名的 404 错误。

为什么 PPA 不适用于所有 Ubuntu 发行版?

这是因为 PPA 的作者必须编译软件并在特定版本上创建 PPA。考虑到每六个月发布一个新的 Ubuntu 版本,为每个版本的 Ubuntu 更新 PPA 是一项繁琐的任务,并非所有开发人员都有时间这样做。

如果 PPA 不适用于你的系统版本,该如何安装应用程序?

尽管 PPA 不适用于你的 Ubuntu 版本,你仍然可以下载 DEB 文件并安装应用程序。

比如说,你访问 Light Table 的 PPA 页面,使用刚刚学到的有关 PPA 的知识,你会发现 PPA 不适用于你的特定 Ubuntu 版本。

你可以点击 “查看软件包详细信息”。

Get DEB file from PPA

在这里,你可以单击软件包以显示更多详细信息,还可以在此处找到包的源代码和 DEB 文件。

Download DEB file from PPA

我建议 使用 Gdebi 安装这些 DEB 文件 而不是通过软件中心,因为 Gdebi 在处理依赖项方面要好得多。

请注意,以这种方式安装的软件包可能无法获得任何将来的更新。

我认为你已经阅读了足够多的关于添加 PPA 的内容,那么如何删除 PPA 及其安装的软件呢?

如何删除 PPA?

我过去曾写过 删除 PPA 的教程,这里写的也是同样的方法。

我建议在删除 PPA 之前删除从 PPA 安装的软件。如果只是删除 PPA,则已安装的软件仍保留在系统中,但不会获得任何更新。这不是你想要的,不是吗?

那么,问题来了,如何知道是哪个 PPA 安装了哪个应用程序?

查找 PPA 安装的软件包并将其移除

Ubuntu 软件中心无法移除 PPA 安装的软件包,你必须使用具有更多高级功能的 Synaptic 包管理器。

可以从软件中心安装 Synaptic 或使用以下命令进行安装:

sudo apt install synaptic

安装后,启动 Synaptic 包管理器并选择 “Origin”。你会看到添加到系统的各种软件仓库。PPA 条目将以前缀 PPA 进行标识,单击以查看 PPA 可用的包。已安装的软件前面会有恰当的符号进行标识。

Managing PPA with Synaptic package manager

查找通过 PPA 安装的软件包

找到包后,你可以从 Synaptic 删除它们。此外,也始终可以选择使用命令行进行移除:

sudo apt remove package_name

删除 PPA 安装的软件包后,你可以继续从 sources.list 中删除PPA。

以图形界面的方式删除 PPA

在设置中打开 “软件和更新”,然后点击 “其他软件” 选项卡。查找要删除的 PPA:

Delete a PPA from Software Source

此处你可以进项两项操作,可以取消选择 PPA 或选择 “删除” 选项。

区别在于,当你取消选择 PPA 条目时,系统将在 /etc/apt/sources.list.d 中的ppa_name.list 文件中注释掉仓库条目;但如果选择 “删除” 选项,将会删除 /etc/apt/sources.list.d目录中 ppa_name.list 文件里的仓库条目。

在这两种情况下,文件 ppa_name.list 都保留在所在的目录中,即使它是空的。

使用 PPA 安全吗?

这是一个主观问题。纯粹主义者厌恶 PPA,因为大多数时候 PPA 来自第三方开发者。但与此同时,PPA 在 Debian/Ubuntu 世界中很受欢迎,因为它们提供了更简单的安装选项。

就安全性而言,很少见到因为使用 PPA 之后你的 Linux 系统被黑客攻击或注入恶意软件。到目前为止,我不记得发生过这样的事件。

官方 PPA 可以不加考虑的使用,使用非官方 PPA 完全是你自己的决定。

根据经验,如果程序需要 sudo 权限,则应避免通过第三方 PPA 进行安装。

你如何看待使用 PPA?

我知道这篇文章需要挺长时间来阅读,但我想让你更好地了解 PPA。我希望这份详细指南能够回答你关于使用 PPA 的大部分问题。

如果你对 PPA 有更多疑问,请随时在评论区提问。

如果你发现任何技术或语法错误,或者有改进的建议,请告诉我。


via: https://itsfoss.com/ppa-guide/

作者:Abhishek Prakash 选题:lujun9972 译者:jlztan 校对:wxy

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

.NET Core 是微软提供的免费、跨平台和开源的开发框架,可以构建桌面应用程序、移动端应用程序、网络应用程序、物联网应用程序和游戏应用程序等。如果你是 Windows 平台下的 dotnet 开发人员的话,使用 .NET core 可以很轻松就设置好任何 Linux 和类 Unix 操作系统下的开发环境。本分步操作指南文章解释了如何在 Linux 中安装 .NET Core SDK 以及如何使用 .NET 开发出第一个应用程序。

Linux 中安装 .NET Core SDK

.NET Core 支持 GNU/Linux、Mac OS 和 Windows 系统,可以在主流的 GNU/Linux 操作系统上安装运行,包括 Debian、Fedora、CentOS、Oracle Linux、RHEL、SUSE/openSUSE 和 Ubuntu 。在撰写这篇教程时,其最新版本为 2.2

Debian 9 系统上安装 .NET Core SDK,请按如下步骤进行。

首先,需要注册微软的密钥,接着把 .NET 源仓库地址添加进来,运行的命令如下:

$ wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.asc.gpg
$ sudo mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/
$ wget -q https://packages.microsoft.com/config/debian/9/prod.list
$ sudo mv prod.list /etc/apt/sources.list.d/microsoft-prod.list
$ sudo chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg
$ sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list

注册好密钥及添加完仓库源后,就可以安装 .NET SDK 了,命令如下:

$ sudo apt-get update
$ sudo apt-get install dotnet-sdk-2.2

Debian 8 系统上安装:

增加微软密钥,添加 .NET 仓库源:

$ wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.asc.gpg
$ sudo mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/
$ wget -q https://packages.microsoft.com/config/debian/8/prod.list
$ sudo mv prod.list /etc/apt/sources.list.d/microsoft-prod.list
$ sudo chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg
$ sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list

安装 .NET SDK:

$ sudo apt-get update
$ sudo apt-get install dotnet-sdk-2.2

Fedora 28 系统上安装:

增加微软密钥,添加 .NET 仓库源:

$ sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
$ wget -q https://packages.microsoft.com/config/fedora/27/prod.repo
$ sudo mv prod.repo /etc/yum.repos.d/microsoft-prod.repo
$ sudo chown root:root /etc/yum.repos.d/microsoft-prod.repo

现在, 可以安装 .NET SDK 了:

$ sudo dnf update
$ sudo dnf install dotnet-sdk-2.2

Fedora 27 系统下:

增加微软密钥,添加 .NET 仓库源,命令如下:

$ sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
$ wget -q https://packages.microsoft.com/config/fedora/27/prod.repo
$ sudo mv prod.repo /etc/yum.repos.d/microsoft-prod.repo
$ sudo chown root:root /etc/yum.repos.d/microsoft-prod.repo

接着安装 .NET SDK ,命令如下:

$ sudo dnf update
$ sudo dnf install dotnet-sdk-2.2

CentOS/Oracle 版本的 Linux 系统上:

增加微软密钥,添加 .NET 仓库源,使其可用:

$ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm

更新源仓库,安装 .NET SDK:

$ sudo yum update
$ sudo yum install dotnet-sdk-2.2

openSUSE Leap 版本的系统上:

添加密钥,使仓库源可用,安装必需的依赖包,其命令如下:

$ sudo zypper install libicu
$ sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
$ wget -q https://packages.microsoft.com/config/opensuse/42.2/prod.repo
$ sudo mv prod.repo /etc/zypp/repos.d/microsoft-prod.repo
$ sudo chown root:root /etc/zypp/repos.d/microsoft-prod.repo

更新源仓库,安装 .NET SDK,命令如下:

$ sudo zypper update
$ sudo zypper install dotnet-sdk-2.2

Ubuntu 18.04 LTS 版本的系统上:

注册微软的密钥和 .NET Core 仓库源,命令如下:

$ wget -q https://packages.microsoft.com/config/ubuntu/18.04/packages-microsoft-prod.deb
$ sudo dpkg -i packages-microsoft-prod.deb

使 Universe 仓库可用:

$ sudo add-apt-repository universe

然后,安装 .NET Core SDK ,命令如下:

$ sudo apt-get install apt-transport-https
$sudo apt-get update
$ sudo apt-get install dotnet-sdk-2.2

Ubuntu 16.04 LTS 版本的系统上:

注册微软的密钥和 .NET Core 仓库源,命令如下:

$ wget -q https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-prod.deb
$ sudo dpkg -i packages-microsoft-prod.deb

然后安装 .NET core SDK:

$ sudo apt-get install apt-transport-https
$ sudo apt-get update
$ sudo apt-get install dotnet-sdk-2.2

创建你的第一个应用程序

我们已经成功的在 Linux 机器中安装了 .NET Core SDK。是时候使用 dotnet 创建第一个应用程序了。

接下来的目的,我们会创建一个名为 ostechnixApp 的应用程序。为此,可以简单的运行如下命令:

$ dotnet new console -o ostechnixApp

示例输出:

Welcome to .NET Core!
---------------------
Learn more about .NET Core: https://aka.ms/dotnet-docs
Use 'dotnet --help' to see available commands or visit: https://aka.ms/dotnet-cli-docs

Telemetry
---------
The .NET Core tools collect usage data in order to help us improve your experience. The data is anonymous and doesn't include command-line arguments. The data is collected by Microsoft and shared with the community. You can opt-out of telemetry by setting the DOTNET_CLI_TELEMETRY_OPTOUT environment variable to '1' or 'true' using your favorite shell.

Read more about .NET Core CLI Tools telemetry: https://aka.ms/dotnet-cli-telemetry

ASP.NET Core
------------
Successfully installed the ASP.NET Core HTTPS Development Certificate.
To trust the certificate run 'dotnet dev-certs https --trust' (Windows and macOS only). For establishing trust on other platforms refer to the platform specific documentation.
For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?linkid=848054.
Getting ready...
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on ostechnixApp/ostechnixApp.csproj...
Restoring packages for /home/sk/ostechnixApp/ostechnixApp.csproj...
Generating MSBuild file /home/sk/ostechnixApp/obj/ostechnixApp.csproj.nuget.g.props.
Generating MSBuild file /home/sk/ostechnixApp/obj/ostechnixApp.csproj.nuget.g.targets.
Restore completed in 894.27 ms for /home/sk/ostechnixApp/ostechnixApp.csproj.

Restore succeeded.

正如上面的输出所示的,.NET 已经为我们创建一个控制台类型的应用程序。-o 参数创建了一个名为 “ostechnixApp” 的目录,其包含有存储此应用程序数据所必需的文件。

让我们切换到 ostechnixApp 目录,看看里面有些什么。

$ cd ostechnixApp/
$ ls
obj ostechnixApp.csproj Program.cs

可以看到有两个名为 ostechnixApp.csprojProgram.cs 的文件,以及一个名为 obj 的目录。默认情况下, Program.cs 文件包含有可以在控制台中运行的 “Hello World” 程序代码。可以看看此代码:

$ cat Program.cs 
using System;

namespace ostechnixApp
{
     class Program
     {
       static void Main(string[] args)
       {
         Console.WriteLine("Hello World!");
       }
   }
}

要运行此应用程序,可以简单的使用如下命令:

$ dotnet run
Hello World!

很简单,对吧?是的,就是如此简单。现在你可以在 Program.cs 这文件中写上自己的代码,然后像上面所示的执行。

或者,你可以创建一个新的目录,如例子所示的 mycode 目录,命令如下:

$ mkdir ~/.mycode
$ cd mycode/

然后运行如下命令,使其成为你的新开发环境目录:

$ dotnet new console

示例输出:

The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on /home/sk/mycode/mycode.csproj...
Restoring packages for /home/sk/mycode/mycode.csproj...
Generating MSBuild file /home/sk/mycode/obj/mycode.csproj.nuget.g.props.
Generating MSBuild file /home/sk/mycode/obj/mycode.csproj.nuget.g.targets.
Restore completed in 331.87 ms for /home/sk/mycode/mycode.csproj.

Restore succeeded.

上的命令会创建两个名叫 mycode.csprojProgram.cs 的文件及一个名为 obj 的目录。用你喜欢的编辑器打开 Program.cs 文件, 删除或修改原来的 “hello world” 代码段,然后编写自己的代码。

写完代码,保存,关闭 Program.cs 文件,然后运行此应用程序,命令如下:

$ dotnet run

想要查看安装的 .NET core SDK 的版本的话,可以简单的运行:

$ dotnet --version
2.2.101

要获得帮助,请运行:

$ dotnet --help

使用微软的 Visual Studio Code 编辑器

要编写代码,你可以任选自己喜欢的编辑器。同时微软自己也有一款支持 .NET 的编辑器,其名为 “Microsoft Visual Studio Code”。它是一款开源、轻量级、功能强大的源代码编辑器。其内置了对 JavaScript、TypeScript 和 Node.js 的支持,并为其它语言(如 C++、C#、Python、PHP、Go)和运行时态(如 .NET 和 Unity)提供了丰富的扩展,已经形成一个完整的生态系统。它是一款跨平台的代码编辑器,所以在微软的 Windows 系统、GNU/Linux 系统和 Mac OS X 系统都可以使用。如果对其感兴趣,就可以使用。

想了解如何在 Linux 上安装和使用,请参阅以下指南。

Linux 中安装 Microsoft Visual Studio Code

关于 Visual Studio Code editor 中 .NET Core 和 .NET Core SDK 工具的使用,此网页有一些基础的教程。想了解更多就去看看吧。

Telemetry

默认情况下,.NET core SDK 会采集用户使用情况数据,此功能被称为 Telemetry。采集数据是匿名的,并根据知识共享署名许可分享给其开发团队和社区。因此 .NET 团队会知道这些工具的使用状况,然后根据统计做出决策,改进产品。如果你不想分享自己的使用信息的话,可以使用顺手的 shell 工具把名为 DOTNET_CLI_TELEMETRY_OPTOUT 的环境变量参数设置为 1true,这样就简单的关闭此功能了。

就这样。你已经知道如何在各 Linux 平台上安装 .NET Core SDK 以及知道如何创建基本的应用程序了。想了解更多 .NET 使用知识的话,请参阅此文章末尾给出的链接。

会爆出更多干货的。敬请关注!

祝贺下!

资源


via: https://www.ostechnix.com/how-to-install-microsoft-net-core-sdk-on-linux/

作者:SK 选题:lujun9972 译者:runningwater 校对:wxy

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

简介

这个实验是默认你能够自己完成的最终项目。

现在你已经有了一个文件系统,一个典型的操作系统都应该有一个网络栈。在本实验中,你将继续为一个网卡去写一个驱动程序。这个网卡基于 Intel 82540EM 芯片,也就是众所周知的 E1000 芯片。

预备知识

使用 Git 去提交你的实验 5 的源代码(如果还没有提交的话),获取课程仓库的最新版本,然后创建一个名为 lab6 的本地分支,它跟踪我们的远程分支 origin/lab6

athena% cd ~/6.828/lab
athena% add git
athena% git commit -am 'my solution to lab5'
nothing to commit (working directory clean)
athena% git pull
Already up-to-date.
athena% git checkout -b lab6 origin/lab6
Branch lab6 set up to track remote branch refs/remotes/origin/lab6.
Switched to a new branch "lab6"
athena% git merge lab5
Merge made by recursive.
 fs/fs.c |   42 +++++++++++++++++++
 1 files changed, 42 insertions(+), 0 deletions(-)
athena%

然后,仅有网卡驱动程序并不能够让你的操作系统接入互联网。在新的实验 6 的代码中,我们为你提供了网络栈和一个网络服务器。与以前的实验一样,使用 git 去拉取这个实验的代码,合并到你自己的代码中,并去浏览新的 net/ 目录中的内容,以及在 kern/ 中的新文件。

除了写这个驱动程序以外,你还需要去创建一个访问你的驱动程序的系统调用。你将要去实现那些在网络服务器中缺失的代码,以便于在网络栈和你的驱动程序之间传输包。你还需要通过完成一个 web 服务器来将所有的东西连接到一起。你的新 web 服务器还需要你的文件系统来提供所需要的文件。

大部分的内核设备驱动程序代码都需要你自己去从头开始编写。本实验提供的指导比起前面的实验要少一些:没有框架文件、没有现成的系统调用接口、并且很多设计都由你自己决定。因此,我们建议你在开始任何单独练习之前,阅读全部的编写任务。许多学生都反应这个实验比前面的实验都难,因此请根据你的实际情况计划你的时间。

实验要求

与以前一样,你需要做实验中全部的常规练习和至少一个挑战问题。在实验中写出你的详细答案,并将挑战问题的方案描述写入到 answers-lab6.txt 文件中。

QEMU 的虚拟网络

我们将使用 QEMU 的用户模式网络栈,因为它不需要以管理员权限运行。QEMU 的文档的这里有更多关于用户网络的内容。我们更新后的 makefile 启用了 QEMU 的用户模式网络栈和虚拟的 E1000 网卡。

缺省情况下,QEMU 提供一个运行在 IP 地址 10.2.2.2 上的虚拟路由器,它给 JOS 分配的 IP 地址是 10.0.2.15。为了简单起见,我们在 net/ns.h 中将这些缺省值硬编码到网络服务器上。

虽然 QEMU 的虚拟网络允许 JOS 随意连接互联网,但 JOS 的 10.0.2.15 的地址并不能在 QEMU 中的虚拟网络之外使用(也就是说,QEMU 还得做一个 NAT),因此我们并不能直接连接到 JOS 上运行的服务器,即便是从运行 QEMU 的主机上连接也不行。为解决这个问题,我们配置 QEMU 在主机的某些端口上运行一个服务器,这个服务器简单地连接到 JOS 中的一些端口上,并在你的真实主机和虚拟网络之间传递数据。

你将在端口 7(echo)和端口 80(http)上运行 JOS,为避免在共享的 Athena 机器上发生冲突,makefile 将为这些端口基于你的用户 ID 来生成转发端口。你可以运行 make which-ports 去找出是哪个 QEMU 端口转发到你的开发主机上。为方便起见,makefile 也提供 make nc-7make nc-80,它允许你在终端上直接与运行这些端口的服务器去交互。(这些目标仅能连接到一个运行中的 QEMU 实例上;你必须分别去启动它自己的 QEMU)

包检查

makefile 也可以配置 QEMU 的网络栈去记录所有的入站和出站数据包,并将它保存到你的实验目录中的 qemu.pcap 文件中。

使用 tcpdump 命令去获取一个捕获的 hex/ASCII 包转储:

tcpdump -XXnr qemu.pcap

或者,你可以使用 Wireshark 以图形化界面去检查 pcap 文件。Wireshark 也知道如何去解码和检查成百上千的网络协议。如果你在 Athena 上,你可以使用 Wireshark 的前辈:ethereal,它运行在加锁的保密互联网协议网络中。

调试 E1000

我们非常幸运能够去使用仿真硬件。由于 E1000 是在软件中运行的,仿真的 E1000 能够给我们提供一个人类可读格式的报告、它的内部状态以及它遇到的任何问题。通常情况下,对祼机上做驱动程序开发的人来说,这是非常难能可贵的。

E1000 能够产生一些调试输出,因此你可以去打开一个专门的日志通道。其中一些对你有用的通道如下:

标志含义
tx包发送日志
txerr包发送错误日志
rx到 RCTL 的日志通道
rxfilter入站包过滤日志
rxerr接收错误日志
unknown未知寄存器的读写日志
eeprom读取 EEPROM 的日志
interrupt中断和中断寄存器变更日志

例如,你可以使用 make E1000_DEBUG=tx,txerr 去打开 “tx” 和 “txerr” 日志功能。

注意:E1000_DEBUG 标志仅能在打了 6.828 补丁的 QEMU 版本上工作。

你可以使用软件去仿真硬件,来做进一步的调试工作。如果你使用它时卡壳了,不明白为什么 E1000 没有如你预期那样响应你,你可以查看在 hw/e1000.c 中的 QEMU 的 E1000 实现。

网络服务器

从头开始写一个网络栈是很困难的。因此我们将使用 lwIP,它是一个开源的、轻量级 TCP/IP 协议套件,它能做包括一个网络栈在内的很多事情。你能在 这里 找到很多关于 lwIP 的信息。在这个任务中,对我们而言,lwIP 就是一个实现了一个 BSD 套接字接口和拥有一个包输入端口和包输出端口的黑盒子。

一个网络服务器其实就是一个有以下四个环境的混合体:

  • 核心网络服务器环境(包括套接字调用派发器和 lwIP)
  • 输入环境
  • 输出环境
  • 定时器环境

下图展示了各个环境和它们之间的关系。下图展示了包括设备驱动的整个系统,我们将在后面详细讲到它。在本实验中,你将去实现图中绿色高亮的部分。

Network server architecture

核心网络服务器环境

核心网络服务器环境由套接字调用派发器和 lwIP 自身组成的。套接字调用派发器就像一个文件服务器一样。用户环境使用 stubs(可以在 lib/nsipc.c 中找到它)去发送 IPC 消息到核心网络服务器环境。如果你看了 lib/nsipc.c,你就会发现核心网络服务器与我们创建的文件服务器 i386_init 的工作方式是一样的,i386_init 是使用 NSTYPENS 创建的 NS 环境,因此我们检查 envs,去查找这个特殊的环境类型。对于每个用户环境的 IPC,网络服务器中的派发器将调用相应的、由 lwIP 提供的、代表用户的 BSD 套接字接口函数。

普通用户环境不能直接使用 nsipc_* 调用。而是通过在 lib/sockets.c 中的函数来使用它们,这些函数提供了基于文件描述符的套接字 API。以这种方式,用户环境通过文件描述符来引用套接字,就像它们引用磁盘上的文件一样。一些操作(connectaccept 等等)是特定于套接字的,但 readwriteclose 是通过 lib/fd.c 中一般的文件描述符设备派发代码的。就像文件服务器对所有的打开的文件维护唯一的内部 ID 一样,lwIP 也为所有的打开的套接字生成唯一的 ID。不论是文件服务器还是网络服务器,我们都使用存储在 struct Fd 中的信息去映射每个环境的文件描述符到这些唯一的 ID 空间上。

尽管看起来文件服务器的网络服务器的 IPC 派发器行为是一样的,但它们之间还有很重要的差别。BSD 套接字调用(像 acceptrecv)能够无限期阻塞。如果派发器让 lwIP 去执行其中一个调用阻塞,派发器也将被阻塞,并且在整个系统中,同一时间只能有一个未完成的网络调用。由于这种情况是无法接受的,所以网络服务器使用用户级线程以避免阻塞整个服务器环境。对于每个入站 IPC 消息,派发器将创建一个线程,然后在新创建的线程上来处理请求。如果线程被阻塞,那么只有那个线程被置入休眠状态,而其它线程仍然处于运行中。

除了核心网络环境外,还有三个辅助环境。核心网络服务器环境除了接收来自用户应用程序的消息之外,它的派发器也接收来自输入环境和定时器环境的消息。

输出环境

在为用户环境套接字调用提供服务时,lwIP 将为网卡生成用于发送的包。lwIP 将使用 NSREQ_OUTPUT 去发送在 IPC 消息页参数中附加了包的 IPC 消息。输出环境负责接收这些消息,并通过你稍后创建的系统调用接口来转发这些包到设备驱动程序上。

输入环境

网卡接收到的包需要传递到 lwIP 中。输入环境将每个由设备驱动程序接收到的包拉进内核空间(使用你将要实现的内核系统调用),并使用 NSREQ_INPUT IPC 消息将这些包发送到核心网络服务器环境。

包输入功能是独立于核心网络环境的,因为在 JOS 上同时实现接收 IPC 消息并从设备驱动程序中查询或等待包有点困难。我们在 JOS 中没有实现 select 系统调用,这是一个允许环境去监视多个输入源以识别准备处理哪个输入的系统调用。

如果你查看了 net/input.cnet/output.c,你将会看到在它们中都需要去实现那个系统调用。这主要是因为实现它要依赖你的系统调用接口。在你实现了驱动程序和系统调用接口之后,你将要为这两个辅助环境写这个代码。

定时器环境

定时器环境周期性发送 NSREQ_TIMER 类型的消息到核心服务器,以提醒它那个定时器已过期。lwIP 使用来自线程的定时器消息来实现各种网络超时。

Part A:初始化和发送包

你的内核还没有一个时间概念,因此我们需要去添加它。这里有一个由硬件产生的每 10 ms 一次的时钟中断。每收到一个时钟中断,我们将增加一个变量值,以表示时间已过去 10 ms。它在 kern/time.c 中已实现,但还没有完全集成到你的内核中。

练习 1、为 kern/trap.c 中的每个时钟中断增加一个到 time_tick 的调用。实现 sys_time_msec 并增加到 kern/syscall.c 中的 syscall,以便于用户空间能够访问时间。

使用 make INIT_CFLAGS=-DTEST_NO_NS run-testtime 去测试你的代码。你应该会看到环境计数从 5 开始以 1 秒为间隔减少。-DTEST_NO_NS 参数禁止在网络服务器环境上启动,因为在当前它将导致 JOS 崩溃。

网卡

写驱动程序要求你必须深入了解硬件和软件中的接口。本实验将给你提供一个如何使用 E1000 接口的高度概括的文档,但是你在写驱动程序时还需要大量去查询 Intel 的手册。

练习 2、为开发 E1000 驱动,去浏览 Intel 的 软件开发者手册。这个手册涵盖了几个与以太网控制器紧密相关的东西。QEMU 仿真了 82540EM。

现在,你应该去浏览第 2 章,以对设备获得一个整体概念。写驱动程序时,你需要熟悉第 3 到 14 章,以及 4.1(不包括 4.1 的子节)。你也应该去参考第 13 章。其它章涵盖了 E1000 的组件,你的驱动程序并不与这些组件去交互。现在你不用担心过多细节的东西;只需要了解文档的整体结构,以便于你后面需要时容易查找。

在阅读手册时,记住,E1000 是一个拥有很多高级特性的很复杂的设备,一个能让 E1000 工作的驱动程序仅需要它一小部分的特性和 NIC 提供的接口即可。仔细考虑一下,如何使用最简单的方式去使用网卡的接口。我们强烈推荐你在使用高级特性之前,只去写一个基本的、能够让网卡工作的驱动程序即可。

PCI 接口

E1000 是一个 PCI 设备,也就是说它是插到主板的 PCI 总线插槽上的。PCI 总线有地址、数据、和中断线,并且 PCI 总线允许 CPU 与 PCI 设备通讯,以及 PCI 设备去读取和写入内存。一个 PCI 设备在它能够被使用之前,需要先发现它并进行初始化。发现 PCI 设备是 PCI 总线查找已安装设备的过程。初始化是分配 I/O 和内存空间、以及协商设备所使用的 IRQ 线的过程。

我们在 kern/pci.c 中已经为你提供了使用 PCI 的代码。PCI 初始化是在引导期间执行的,PCI 代码遍历PCI 总线来查找设备。当它找到一个设备时,它读取它的供应商 ID 和设备 ID,然后使用这两个值作为关键字去搜索 pci_attach_vendor 数组。这个数组是由像下面这样的 struct pci_driver 条目组成:

struct pci_driver {
    uint32_t key1, key2;
    int (*attachfn) (struct pci_func *pcif);
};

如果发现的设备的供应商 ID 和设备 ID 与数组中条目匹配,那么 PCI 代码将调用那个条目的 attachfn 去执行设备初始化。(设备也可以按类别识别,那是通过 kern/pci.c 中其它的驱动程序表来实现的。)

绑定函数是传递一个 PCI 函数 去初始化。一个 PCI 卡能够发布多个函数,虽然这个 E1000 仅发布了一个。下面是在 JOS 中如何去表示一个 PCI 函数:

struct pci_func {
    struct pci_bus *bus;

    uint32_t dev;
    uint32_t func;

    uint32_t dev_id;
    uint32_t dev_class;

    uint32_t reg_base[6];
    uint32_t reg_size[6];
    uint8_t irq_line;
};

上面的结构反映了在 Intel 开发者手册里第 4.1 节的表 4-1 中找到的一些条目。struct pci_func 的最后三个条目我们特别感兴趣的,因为它们将记录这个设备协商的内存、I/O、以及中断资源。reg_basereg_size 数组包含最多六个基址寄存器或 BAR。reg_base 为映射到内存中的 I/O 区域(对于 I/O 端口而言是基 I/O 端口)保存了内存的基地址,reg_size 包含了以字节表示的大小或来自 reg_base 的相关基值的 I/O 端口号,而 irq_line 包含了为中断分配给设备的 IRQ 线。在表 4-2 的后半部分给出了 E1000 BAR 的具体涵义。

当设备调用了绑定函数后,设备已经被发现,但没有被启用。这意味着 PCI 代码还没有确定分配给设备的资源,比如地址空间和 IRQ 线,也就是说,struct pci_func 结构的最后三个元素还没有被填入。绑定函数将调用 pci_func_enable,它将去启用设备、协商这些资源、并在结构 struct pci_func 中填入它。

练习 3、实现一个绑定函数去初始化 E1000。添加一个条目到 kern/pci.c 中的数组 pci_attach_vendor 上,如果找到一个匹配的 PCI 设备就去触发你的函数(确保一定要把它放在表末尾的 {0, 0, 0} 条目之前)。你在 5.2 节中能找到 QEMU 仿真的 82540EM 的供应商 ID 和设备 ID。在引导期间,当 JOS 扫描 PCI 总线时,你也可以看到列出来的这些信息。

到目前为止,我们通过 pci_func_enable 启用了 E1000 设备。通过本实验我们将添加更多的初始化。

我们已经为你提供了 kern/e1000.ckern/e1000.h 文件,这样你就不会把构建系统搞糊涂了。不过它们现在都是空的;你需要在本练习中去填充它们。你还可能在内核的其它地方包含这个 e1000.h 文件。

当你引导你的内核时,你应该会看到它输出的信息显示 E1000 的 PCI 函数已经启用。这时你的代码已经能够通过 make gradepci attach 测试了。

内存映射的 I/O

软件与 E1000 通过内存映射的 I/O(MMIO)来沟通。你在 JOS 的前面部分可能看到过 MMIO 两次:CGA 控制台和 LAPIC 都是通过写入和读取“内存”来控制和查询设备的。但这些读取和写入不是去往内存芯片的,而是直接到这些设备的。

pci_func_enable 为 E1000 协调一个 MMIO 区域,来存储它在 BAR 0 的基址和大小(也就是 reg_base[0]reg_size[0]),这是一个分配给设备的一段物理内存地址,也就是说你可以通过虚拟地址访问它来做一些事情。由于 MMIO 区域一般分配高位物理地址(一般是 3GB 以上的位置),因此你不能使用 KADDR 去访问它们,因为 JOS 被限制为最大使用 256MB。因此,你可以去创建一个新的内存映射。我们将使用 MMIOBASE(从实验 4 开始,你的 mmio_map_region 区域应该确保不能被 LAPIC 使用的映射所覆盖)以上的部分。由于在 JOS 创建用户环境之前,PCI 设备就已经初始化了,因此你可以在 kern_pgdir 处创建映射,并且让它始终可用。

练习 4、在你的绑定函数中,通过调用 mmio_map_region(它就是你在实验 4 中写的,是为了支持 LAPIC 内存映射)为 E1000 的 BAR 0 创建一个虚拟地址映射。

你将希望在一个变量中记录这个映射的位置,以便于后面访问你映射的寄存器。去看一下 kern/lapic.c 中的 lapic 变量,它就是一个这样的例子。如果你使用一个指针指向设备寄存器映射,一定要声明它为 volatile;否则,编译器将允许缓存它的值,并可以在内存中再次访问它。

为测试你的映射,尝试去输出设备状态寄存器(第 12.4.2 节)。这是一个在寄存器空间中以字节 8 开头的 4 字节寄存器。你应该会得到 0x80080783,它表示以 1000 MB/s 的速度启用一个全双工的链路,以及其它信息。

提示:你将需要一些常数,像寄存器位置和掩码位数。如果从开发者手册中复制这些东西很容易出错,并且导致调试过程很痛苦。我们建议你使用 QEMU 的 e1000\_hw.h 头文件做为基准。我们不建议完全照抄它,因为它定义的值远超过你所需要,并且定义的东西也不见得就是你所需要的,但它仍是一个很好的参考。

DMA

你可能会认为是从 E1000 的寄存器中通过写入和读取来传送和接收数据包的,其实这样做会非常慢,并且还要求 E1000 在其中去缓存数据包。相反,E1000 使用直接内存访问(DMA)从内存中直接读取和写入数据包,而且不需要 CPU 参与其中。驱动程序负责为发送和接收队列分配内存、设置 DMA 描述符、以及配置 E1000 使用的队列位置,而在这些设置完成之后的其它工作都是异步方式进行的。发送包的时候,驱动程序复制它到发送队列的下一个 DMA 描述符中,并且通知 E1000 下一个发送包已就绪;当轮到这个包发送时,E1000 将从描述符中复制出数据。同样,当 E1000 接收一个包时,它从接收队列中将它复制到下一个 DMA 描述符中,驱动程序将能在下一次读取到它。

总体来看,接收队列和发送队列非常相似。它们都是由一系列的描述符组成。虽然这些描述符的结构细节有所不同,但每个描述符都包含一些标志和包含了包数据的一个缓存的物理地址(发送到网卡的数据包,或网卡将接收到的数据包写入到由操作系统分配的缓存中)。

队列被实现为一个环形数组,意味着当网卡或驱动到达数组末端时,它将重新回到开始位置。它有一个头指针和尾指针,队列的内容就是这两个指针之间的描述符。硬件就是从头开始移动头指针去消费描述符,在这期间驱动程序不停地添加描述符到尾部,并移动尾指针到最后一个描述符上。发送队列中的描述符表示等待发送的包(因此,在平静状态下,发送队列是空的)。对于接收队列,队列中的描述符是表示网卡能够接收包的空描述符(因此,在平静状态下,接收队列是由所有的可用接收描述符组成的)。正确的更新尾指针寄存器而不让 E1000 产生混乱是很有难度的;要小心!

指向到这些数组及描述符中的包缓存地址的指针都必须是物理地址,因为硬件是直接在物理内存中且不通过 MMU 来执行 DMA 的读写操作的。

发送包

E1000 中的发送和接收功能本质上是独立的,因此我们可以同时进行发送接收。我们首先去攻克简单的数据包发送,因为我们在没有先去发送一个 “I’m here!” 包之前是无法测试接收包功能的。

首先,你需要初始化网卡以准备发送,详细步骤查看 14.5 节(不必着急看子节)。发送初始化的第一步是设置发送队列。队列的详细结构在 3.4 节中,描述符的结构在 3.3.3 节中。我们先不要使用 E1000 的 TCP offload 特性,因此你只需专注于 “传统的发送描述符格式” 即可。你应该现在就去阅读这些章节,并要熟悉这些结构。

C 结构

你可以用 C struct 很方便地描述 E1000 的结构。正如你在 struct Trapframe 中所看到的结构那样,C struct 可以让你很方便地在内存中描述准确的数据布局。C 可以在字段中插入数据,但是 E1000 的结构就是这样布局的,这样就不会是个问题。如果你遇到字段对齐问题,进入 GCC 查看它的 “packed” 属性。

查看手册中表 3-8 所给出的一个传统的发送描述符,将它复制到这里作为一个示例:

  63            48 47   40 39   32 31   24 23   16 15             0
  +---------------------------------------------------------------+
  |                         Buffer address                        |
  +---------------|-------|-------|-------|-------|---------------+
  |    Special    |  CSS  | Status|  Cmd  |  CSO  |    Length     |
  +---------------|-------|-------|-------|-------|---------------+

从结构右上角第一个字节开始,我们将它转变成一个 C 结构,从上到下,从右到左读取。如果你从右往左看,你将看到所有的字段,都非常适合一个标准大小的类型:

struct tx_desc
{
    uint64_t addr;
    uint16_t length;
    uint8_t cso;
    uint8_t cmd;
    uint8_t status;
    uint8_t css;
    uint16_t special;
};

你的驱动程序将为发送描述符数组去保留内存,并由发送描述符指向到包缓冲区。有几种方式可以做到,从动态分配页到在全局变量中简单地声明它们。无论你如何选择,记住,E1000 是直接访问物理内存的,意味着它能访问的任何缓存区在物理内存中必须是连续的。

处理包缓存也有几种方式。我们推荐从最简单的开始,那就是在驱动程序初始化期间,为每个描述符保留包缓存空间,并简单地将包数据复制进预留的缓冲区中或从其中复制出来。一个以太网包最大的尺寸是 1518 字节,这就限制了这些缓存区的大小。主流的成熟驱动程序都能够动态分配包缓存区(即:当网络使用率很低时,减少内存使用量),或甚至跳过缓存区,直接由用户空间提供(就是“零复制”技术),但我们还是从简单开始为好。

练习 5、执行一个 14.5 节中的初始化步骤(它的子节除外)。对于寄存器的初始化过程使用 13 节作为参考,对发送描述符和发送描述符数组参考 3.3.3 节和 3.4 节。

要记住,在发送描述符数组中要求对齐,并且数组长度上有限制。因为 TDLEN 必须是 128 字节对齐的,而每个发送描述符是 16 字节,你的发送描述符数组必须是 8 个发送描述符的倍数。并且不能使用超过 64 个描述符,以及不能在我们的发送环形缓存测试中溢出。

对于 TCTL.COLD,你可以假设为全双工操作。对于 TIPG、IEEE 802.3 标准的 IPG(不要使用 14.5 节中表上的值),参考在 13.4.34 节中表 13-77 中描述的缺省值。

尝试运行 make E1000_DEBUG=TXERR,TX qemu。如果你使用的是打了 6.828 补丁的 QEMU,当你设置 TDT(发送描述符尾部)寄存器时你应该会看到一个 “e1000: tx disabled” 的信息,并且不会有更多 “e1000” 信息了。

现在,发送初始化已经完成,你可以写一些代码去发送一个数据包,并且通过一个系统调用使它可以访问用户空间。你可以将要发送的数据包添加到发送队列的尾部,也就是说复制数据包到下一个包缓冲区中,然后更新 TDT 寄存器去通知网卡在发送队列中有另外的数据包。(注意,TDT 是一个进入发送描述符数组的索引,不是一个字节偏移量;关于这一点文档中说明的不是很清楚。)

但是,发送队列只有这么大。如果网卡在发送数据包时卡住或发送队列填满时会发生什么状况?为了检测这种情况,你需要一些来自 E1000 的反馈。不幸的是,你不能只使用 TDH(发送描述符头)寄存器;文档上明确说明,从软件上读取这个寄存器是不可靠的。但是,如果你在发送描述符的命令字段中设置 RS 位,那么,当网卡去发送在那个描述符中的数据包时,网卡将设置描述符中状态字段的 DD 位,如果一个描述符中的 DD 位被设置,你就应该知道那个描述符可以安全地回收,并且可以用它去发送其它数据包。

如果用户调用你的发送系统调用,但是下一个描述符的 DD 位没有设置,表示那个发送队列已满,该怎么办?在这种情况下,你该去决定怎么办了。你可以简单地丢弃数据包。网络协议对这种情况的处理很灵活,但如果你丢弃大量的突发数据包,协议可能不会去重新获得它们。可能需要你替代网络协议告诉用户环境让它重传,就像你在 sys_ipc_try_send 中做的那样。在环境上回推产生的数据是有好处的。

练习 6、写一个函数去发送一个数据包,它需要检查下一个描述符是否空闲、复制包数据到下一个描述符并更新 TDT。确保你处理的发送队列是满的。

现在,应该去测试你的包发送代码了。通过从内核中直接调用你的发送函数来尝试发送几个包。在测试时,你不需要去创建符合任何特定网络协议的数据包。运行 make E1000_DEBUG=TXERR,TX qemu 去测试你的代码。你应该看到类似下面的信息:

e1000: index 0: 0x271f00 : 9000002a 0
...

在你发送包时,每行都给出了在发送数组中的序号、那个发送的描述符的缓存地址、cmd/CSO/length 字段、以及 special/CSS/status 字段。如果 QEMU 没有从你的发送描述符中输出你预期的值,检查你的描述符中是否有合适的值和你配置的正确的 TDBAL 和 TDBAH。如果你收到的是 “e1000: TDH wraparound @0, TDT x, TDLEN y” 的信息,意味着 E1000 的发送队列持续不断地运行(如果 QEMU 不去检查它,它将是一个无限循环),这意味着你没有正确地维护 TDT。如果你收到了许多 “e1000: tx disabled” 的信息,那么意味着你没有正确设置发送控制寄存器。

一旦 QEMU 运行,你就可以运行 tcpdump -XXnr qemu.pcap 去查看你发送的包数据。如果从 QEMU 中看到预期的 “e1000: index” 信息,但你捕获的包是空的,再次检查你发送的描述符,是否填充了每个必需的字段和位。(E1000 或许已经遍历了你的发送描述符,但它认为不需要去发送)

练习 7、添加一个系统调用,让你从用户空间中发送数据包。详细的接口由你来决定。但是不要忘了检查从用户空间传递给内核的所有指针。

发送包:网络服务器

现在,你已经有一个系统调用接口可以发送包到你的设备驱动程序端了。输出辅助环境的目标是在一个循环中做下面的事情:从核心网络服务器中接收 NSREQ_OUTPUT IPC 消息,并使用你在上面增加的系统调用去发送伴随这些 IPC 消息的数据包。这个 NSREQ_OUTPUT IPC 是通过 net/lwip/jos/jif/jif.c 中的 low_level_output 函数来发送的。它集成 lwIP 栈到 JOS 的网络系统。每个 IPC 将包含一个页,这个页由一个 union Nsipc 和在 struct jif_pkt pkt 字段中的一个包组成(查看 inc/ns.h)。struct jif_pkt 看起来像下面这样:

struct jif_pkt {
    int jp_len;
    char jp_data[0];
};

jp_len 表示包的长度。在 IPC 页上的所有后续字节都是为了包内容。在结构的结尾处使用一个长度为 0 的数组来表示缓存没有一个预先确定的长度(像 jp_data 一样),这是一个常见的 C 技巧(也有人说这是一个令人讨厌的做法)。因为 C 并不做数组边界的检查,只要你确保结构后面有足够的未使用内存即可,你可以把 jp_data 作为一个任意大小的数组来使用。

当设备驱动程序的发送队列中没有足够的空间时,一定要注意在设备驱动程序、输出环境和核心网络服务器之间的交互。核心网络服务器使用 IPC 发送包到输出环境。如果输出环境在由于一个发送包的系统调用而挂起,导致驱动程序没有足够的缓存去容纳新数据包,这时核心网络服务器将阻塞以等待输出服务器去接收 IPC 调用。

练习 8、实现 net/output.c

你可以使用 net/testoutput.c 去测试你的输出代码而无需整个网络服务器参与。尝试运行 make E1000_DEBUG=TXERR,TX run-net_testoutput。你将看到如下的输出:

Transmitting packet 0
e1000: index 0: 0x271f00 : 9000009 0
Transmitting packet 1
e1000: index 1: 0x2724ee : 9000009 0
...

运行 tcpdump -XXnr qemu.pcap 将输出:

reading from file qemu.pcap, link-type EN10MB (Ethernet)
-5:00:00.600186 [|ether]
    0x0000:  5061 636b 6574 2030 30                   Packet.00
-5:00:00.610080 [|ether]
    0x0000:  5061 636b 6574 2030 31                   Packet.01
...

使用更多的数据包去测试,可以运行 make E1000_DEBUG=TXERR,TX NET_CFLAGS=-DTESTOUTPUT_COUNT=100 run-net_testoutput。如果它导致你的发送队列溢出,再次检查你的 DD 状态位是否正确,以及是否告诉硬件去设置 DD 状态位(使用 RS 命令位)。

你的代码应该会通过 make gradetestoutput 测试。

问题 1、你是如何构造你的发送实现的?在实践中,如果发送缓存区满了,你该如何处理?

Part B:接收包和 web 服务器

接收包

就像你在发送包中做的那样,你将去配置 E1000 去接收数据包,并提供一个接收描述符队列和接收描述符。在 3.2 节中描述了接收包的操作,包括接收队列结构和接收描述符、以及在 14.4 节中描述的详细的初始化过程。

练习 9、阅读 3.2 节。你可以忽略关于中断和 offload 校验和方面的内容(如果在后面你想去使用这些特性,可以再返回去阅读),你现在不需要去考虑阈值的细节和网卡内部缓存是如何工作的。

除了接收队列是由一系列的等待入站数据包去填充的空缓存包以外,接收队列的其它部分与发送队列非常相似。所以,当网络空闲时,发送队列是空的(因为所有的包已经被发送出去了),而接收队列是满的(全部都是空缓存包)。

当 E1000 接收一个包时,它首先与网卡的过滤器进行匹配检查(例如,去检查这个包的目标地址是否为这个 E1000 的 MAC 地址),如果这个包不匹配任何过滤器,它将忽略这个包。否则,E1000 尝试从接收队列头部去检索下一个接收描述符。如果头(RDH)追上了尾(RDT),那么说明接收队列已经没有空闲的描述符了,所以网卡将丢弃这个包。如果有空闲的接收描述符,它将复制这个包的数据到描述符指向的缓存中,设置这个描述符的 DD 和 EOP 状态位,并递增 RDH。

如果 E1000 在一个接收描述符中接收到了一个比包缓存还要大的数据包,它将按需从接收队列中检索尽可能多的描述符以保存数据包的全部内容。为表示发生了这种情况,它将在所有的这些描述符上设置 DD 状态位,但仅在这些描述符的最后一个上设置 EOP 状态位。在你的驱动程序上,你可以去处理这种情况,也可以简单地配置网卡拒绝接收这种”长包“(这种包也被称为”巨帧“),你要确保接收缓存有足够的空间尽可能地去存储最大的标准以太网数据包(1518 字节)。

练习 10、设置接收队列并按 14.4 节中的流程去配置 E1000。你可以不用支持 ”长包“ 或多播。到目前为止,我们不用去配置网卡使用中断;如果你在后面决定去使用接收中断时可以再去改。另外,配置 E1000 去除以太网的 CRC 校验,因为我们的评级脚本要求必须去掉校验。

默认情况下,网卡将过滤掉所有的数据包。你必须使用网卡的 MAC 地址去配置接收地址寄存器(RAL 和 RAH)以接收发送到这个网卡的数据包。你可以简单地硬编码 QEMU 的默认 MAC 地址 52:54:00:12:34:56(我们已经在 lwIP 中硬编码了这个地址,因此这样做不会有问题)。使用字节顺序时要注意;MAC 地址是从低位字节到高位字节的方式来写的,因此 52:54:00:12 是 MAC 地址的低 32 位,而 34:56 是它的高 16 位。

E1000 的接收缓存区大小仅支持几个指定的设置值(在 13.4.22 节中描述的 RCTL.BSIZE 值)。如果你的接收包缓存够大,并且拒绝长包,那你就不用担心跨越多个缓存区的包。另外,要记住的是,和发送一样,接收队列和包缓存必须是连接的物理内存。

你应该使用至少 128 个接收描述符。

现在,你可以做接收功能的基本测试了,甚至都无需写代码去接收包了。运行 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-net_testinputtestinput 将发送一个 ARP(地址解析协议)通告包(使用你的包发送的系统调用),而 QEMU 将自动回复它,即便是你的驱动尚不能接收这个回复,你也应该会看到一个 “e1000: unicast match[0]: 52:54:00:12:34:56” 的消息,表示 E1000 接收到一个包,并且匹配了配置的接收过滤器。如果你看到的是一个 “e1000: unicast mismatch: 52:54:00:12:34:56” 消息,表示 E1000 过滤掉了这个包,意味着你的 RAL 和 RAH 的配置不正确。确保你按正确的顺序收到了字节,并不要忘记设置 RAH 中的 “Address Valid” 位。如果你没有收到任何 “e1000” 消息,或许是你没有正确地启用接收功能。

现在,你准备去实现接收数据包。为了接收数据包,你的驱动程序必须持续跟踪希望去保存下一下接收到的包的描述符(提示:按你的设计,这个功能或许已经在 E1000 中的一个寄存器来实现了)。与发送类似,官方文档上表示,RDH 寄存器状态并不能从软件中可靠地读取,因为确定一个包是否被发送到描述符的包缓存中,你需要去读取描述符中的 DD 状态位。如果 DD 位被设置,你就可以从那个描述符的缓存中复制出这个数据包,然后通过更新队列的尾索引 RDT 来告诉网卡那个描述符是空闲的。

如果 DD 位没有被设置,表明没有接收到包。这就与发送队列满的情况一样,这时你可以有几种做法。你可以简单地返回一个 ”重传“ 错误来要求对端重发一次。对于满的发送队列,由于那是个临时状况,这种做法还是很好的,但对于空的接收队列来说就不太合理了,因为接收队列可能会保持好长一段时间的空的状态。第二个方法是挂起调用环境,直到在接收队列中处理了这个包为止。这个策略非常类似于 sys_ipc_recv。就像在 IPC 的案例中,因为我们每个 CPU 仅有一个内核栈,一旦我们离开内核,栈上的状态就会被丢弃。我们需要设置一个标志去表示那个环境由于接收队列下溢被挂起并记录系统调用参数。这种方法的缺点是过于复杂:E1000 必须被指示去产生接收中断,并且驱动程序为了恢复被阻塞等待一个包的环境,必须处理这个中断。

练习 11、写一个函数从 E1000 中接收一个包,然后通过一个系统调用将它发布到用户空间。确保你将接收队列处理成空的。

.

小挑战!如果发送队列是满的或接收队列是空的,环境和你的驱动程序可能会花费大量的 CPU 周期是轮询、等待一个描述符。一旦完成发送或接收描述符,E1000 能够产生一个中断,以避免轮询。修改你的驱动程序,处理发送和接收队列是以中断而不是轮询的方式进行。

注意,一旦确定为中断,它将一直处于中断状态,直到你的驱动程序明确处理完中断为止。在你的中断服务程序中,一旦处理完成要确保清除掉中断状态。如果你不那样做,从你的中断服务程序中返回后,CPU 将再次跳转到你的中断服务程序中。除了在 E1000 网卡上清除中断外,也需要使用 lapic_eoi 在 LAPIC 上清除中断。

接收包:网络服务器

在网络服务器输入环境中,你需要去使用你的新的接收系统调用以接收数据包,并使用 NSREQ_INPUT IPC 消息将它传递到核心网络服务器环境。这些 IPC 输入消息应该会有一个页,这个页上绑定了一个 union Nsipc,它的 struct jif_pkt pkt 字段中有从网络上接收到的包。

练习 12、实现 net/input.c

使用 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-net_testinput 再次运行 testinput,你应该会看到:

Sending ARP announcement...
Waiting for packets...
e1000: index 0: 0x26dea0 : 900002a 0
e1000: unicast match[0]: 52:54:00:12:34:56
input: 0000 5254 0012 3456 5255 0a00 0202 0806 0001
input: 0010 0800 0604 0002 5255 0a00 0202 0a00 0202
input: 0020 5254 0012 3456 0a00 020f 0000 0000 0000
input: 0030 0000 0000 0000 0000 0000 0000 0000 0000

“input:” 打头的行是一个 QEMU 的 ARP 回复的十六进制转储。

你的代码应该会通过 make gradetestinput 测试。注意,在没有发送至少一个包去通知 QEMU 中的 JOS 的 IP 地址上时,是没法去测试包接收的,因此在你的发送代码中的 bug 可能会导致测试失败。

为彻底地测试你的网络代码,我们提供了一个称为 echosrv 的守护程序,它在端口 7 上设置运行 echo 的服务器,它将回显通过 TCP 连接发送给它的任何内容。使用 make E1000_DEBUG=TX,TXERR,RX,RXERR,RXFILTER run-echosrv 在一个终端中启动 echo 服务器,然后在另一个终端中通过 make nc-7 去连接它。你输入的每一行都被这个服务器回显出来。每次在仿真的 E1000 上接收到一个包,QEMU 将在控制台上输出像下面这样的内容:

e1000: unicast match[0]: 52:54:00:12:34:56
e1000: index 2: 0x26ea7c : 9000036 0
e1000: index 3: 0x26f06a : 9000039 0
e1000: unicast match[0]: 52:54:00:12:34:56

做到这一点后,你应该也就能通过 echosrv 的测试了。

问题 2、你如何构造你的接收实现?在实践中,如果接收队列是空的并且一个用户环境要求下一个入站包,你怎么办?

.

小挑战!在开发者手册中阅读关于 EEPROM 的内容,并写出从 EEPROM 中加载 E1000 的 MAC 地址的代码。目前,QEMU 的默认 MAC 地址是硬编码到你的接收初始化代码和 lwIP 中的。修复你的初始化代码,让它能够从 EEPROM 中读取 MAC 地址,和增加一个系统调用去传递 MAC 地址到 lwIP 中,并修改 lwIP 去从网卡上读取 MAC 地址。通过配置 QEMU 使用一个不同的 MAC 地址去测试你的变更。

.

小挑战!修改你的 E1000 驱动程序去使用 零复制 技术。目前,数据包是从用户空间缓存中复制到发送包缓存中,和从接收包缓存中复制回到用户空间缓存中。一个使用 ”零复制“ 技术的驱动程序可以通过直接让用户空间和 E1000 共享包缓存内存来实现。还有许多不同的方法去实现 ”零复制“,包括映射内容分配的结构到用户空间或直接传递用户提供的缓存到 E1000。不论你选择哪种方法,都要注意你如何利用缓存的问题,因为你不能在用户空间代码和 E1000 之间产生争用。

.

小挑战!把 “零复制” 的概念用到 lwIP 中。

一个典型的包是由许多头构成的。用户发送的数据被发送到 lwIP 中的一个缓存中。TCP 层要添加一个 TCP 包头,IP 层要添加一个 IP 包头,而 MAC 层有一个以太网头。甚至还有更多的部分增加到包上,这些部分要正确地连接到一起,以便于设备驱动程序能够发送最终的包。

E1000 的发送描述符设计是非常适合收集分散在内存中的包片段的,像在 lwIP 中创建的包的帧。如果你排队多个发送描述符,但仅设置最后一个描述符的 EOP 命令位,那么 E1000 将在内部把这些描述符串成包缓存,并在它们标记完 EOP 后仅发送串起来的缓存。因此,独立的包片段不需要在内存中把它们连接到一起。

修改你的驱动程序,以使它能够发送由多个缓存且无需复制的片段组成的包,并且修改 lwIP 去避免它合并包片段,因为它现在能够正确处理了。

.

小挑战!增加你的系统调用接口,以便于它能够为多于一个的用户环境提供服务。如果有多个网络栈(和多个网络服务器)并且它们各自都有自己的 IP 地址运行在用户模式中,这将是非常有用的。接收系统调用将决定它需要哪个环境来转发每个入站的包。

注意,当前的接口并不知道两个包之间有何不同,并且如果多个环境去调用包接收的系统调用,各个环境将得到一个入站包的子集,而那个子集可能并不包含调用环境指定的那个包。

这篇 外内核论文的 2.2 节和 3 节中对这个问题做了深度解释,并解释了在内核中(如 JOS)处理它的一个方法。用这个论文中的方法去解决这个问题,你不需要一个像论文中那么复杂的方案。

Web 服务器

一个最简单的 web 服务器类型是发送一个文件的内容到请求的客户端。我们在 user/httpd.c 中提供了一个非常简单的 web 服务器的框架代码。这个框架内码处理入站连接并解析请求头。

练习 13、这个 web 服务器中缺失了发送一个文件的内容到客户端的处理代码。通过实现 send_filesend_data 完成这个 web 服务器。

在你完成了这个 web 服务器后,启动这个 web 服务器(make run-httpd-nox),使用你喜欢的浏览器去浏览 http://host:port/index.html 地址。其中 host 是运行 QEMU 的计算机的名字(如果你在 athena 上运行 QEMU,使用 hostname.mit.edu(其中 hostname 是在 athena 上运行 hostname 命令的输出,或者如果你在运行 QEMU 的机器上运行 web 浏览器的话,直接使用 localhost),而 port 是 web 服务器运行 make which-ports 命令报告的端口号。你应该会看到一个由运行在 JOS 中的 HTTP 服务器提供的一个 web 页面。

到目前为止,你的评级测试得分应该是 105 分(满分为 105)。

小挑战!在 JOS 中添加一个简单的聊天服务器,多个人可以连接到这个服务器上,并且任何用户输入的内容都被发送到其它用户。为实现它,你需要找到一个一次与多个套接字通讯的方法,并且在同一时间能够在同一个套接字上同时实现发送和接收。有多个方法可以达到这个目的。lwIP 为 recv(查看 net/lwip/api/sockets.c 中的 lwip_recvfrom)提供了一个 MSG\_DONTWAIT 标志,以便于你不断地轮询所有打开的套接字。注意,虽然网络服务器的 IPC 支持 recv 标志,但是通过普通的 read 函数并不能访问它们,因此你需要一个方法来传递这个标志。一个更高效的方法是为每个连接去启动一个或多个环境,并且使用 IPC 去协调它们。而且碰巧的是,对于一个套接字,在结构 Fd 中找到的 lwIP 套接字 ID 是全局的(不是每个环境私有的),因此,比如一个 fork 的子环境继承了它的父环境的套接字。或者,一个环境通过构建一个包含了正确套接字 ID 的 Fd 就能够发送到另一个环境的套接字上。

问题 3、由 JOS 的 web 服务器提供的 web 页面显示了什么?

.

问题 4、你做这个实验大约花了多长的时间?

本实验到此结束了。一如既往,不要忘了运行 make grade 并去写下你的答案和挑战问题的解决方案的描述。在你动手之前,使用 git statusgit diff 去检查你的变更,并不要忘了去 git add answers-lab6.txt。当你完成之后,使用 git commit -am 'my solutions to lab 6’ 去提交你的变更,然后 make handin 并关注它的动向。


via: https://pdos.csail.mit.edu/6.828/2018/labs/lab6/

作者:csail.mit 选题:lujun9972 译者:qhwdw 校对:wxy

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

学习如何使 Ansible 自动对一系列台式机和笔记本应用配置。

Ansible 是一个令人惊讶的自动化的配置管理工具。其主要应用在服务器和云部署上,但在工作站上的应用(无论是台式机还是笔记本)却鲜少得到关注,这就是本系列所要关注的。

在这个系列的第一部分,我向你展示了 ansible-pull 命令的基本用法,我们创建了一个安装了少量包的剧本。它本身是没有多大的用处的,但是为后续的自动化做了准备。

在这篇文章中,将会达成闭环,而且在最后部分,我们将会有一个针对工作站自动配置的完整的工作解决方案。现在,我们将要设置 Ansible 的配置,这样未来将要做的改变将会自动的部署应用到我们的工作站上。现阶段,假设你已经完成了第一部分的工作。如果没有的话,当你完成的时候回到本文。你应该已经有一个包含第一篇文章中代码的 GitHub 库。我们将直接在之前创建的部分之上继续。

首先,因为我们要做的不仅仅是安装包文件,所以我们要做一些重新的组织工作。现在,我们已经有一个名为 local.yml 并包含以下内容的剧本:

- hosts: localhost
  become: true
  tasks:
  - name: Install packages
    apt: name={{item}}
    with_items:
      - htop
      - mc
      - tmux

如果我们仅仅想实现一个任务那么上面的配置就足够了。随着向我们的配置中不断的添加内容,这个文件将会变的相当的庞大和杂乱。最好能够根据不同类型的配置将我们的 动作 play 分为独立的文件。为了达到这个要求,创建一个名为 任务手册 taskbook 的东西,它和 剧本 playbook 很像但内容更加的流线型。让我们在 Git 库中为任务手册创建一个目录。

mkdir tasks

local.yml 剧本中的代码可以很好地过渡为安装包文件的任务手册。让我们把这个文件移动到刚刚创建好的 task 目录中,并重新命名。

mv local.yml tasks/packages.yml

现在,我们编辑 packages.yml 文件将它进行大幅的瘦身,事实上,我们可以精简除了独立任务本身之外的所有内容。让我们把 packages.yml 编辑成如下的形式:

- name: Install packages
  apt: name={{item}}
  with_items:
    - htop
    - mc
    - tmux

正如你所看到的,它使用同样的语法,但我们去掉了对这个任务无用没有必要的所有内容。现在我们有了一个专门安装包文件的任务手册。然而我们仍然需要一个名为 local.yml 的文件,因为执行 ansible-pull 命令时仍然会去找这个文件。所以我们将在我们库的根目录下(不是在 task 目录下)创建一个包含这些内容的全新文件:

- hosts: localhost
  become: true
  pre_tasks:
    - name: update repositories
      apt: update_cache=yes
      changed_when: False

  tasks:
    - include: tasks/packages.yml

这个新的 local.yml 扮演的是导入我们的任务手册的索引的角色。我已经在这个文件中添加了一些你在这个系列中还没见到的内容。首先,在这个文件的开头处,我添加了 pre_tasks,这个任务的作用是在其他所有任务运行之前先运行某个任务。在这种情况下,我们给 Ansible 的命令是让它去更新我们的发行版的软件库的索引,下面的配置将执行这个任务要求:

apt: update_cache=yes

通常 apt 模块是用来安装包文件的,但我们也能够让它来更新软件库索引。这样做的目的是让我们的每个动作在 Ansible 运行的时候能够以最新的索引工作。这将确保我们在使用一个老旧的索引安装一个包的时候不会出现问题。因为 apt 模块仅仅在 Debian、Ubuntu 及它们的衍生发行版下工作。如果你运行的一个不同的发行版,你要使用特定于你的发行版的模块而不是 apt。如果你需要使用一个不同的模块请查看 Ansible 的相关文档。

下面这行也需要进一步解释:

changed_when: False

在某个任务中的这行阻止了 Ansible 去报告动作改变的结果,即使是它本身在系统中导致的一个改变。在这里,我们不会去在意库索引是否包含新的数据;它几乎总是会的,因为库总是在改变的。我们不会去在意 apt 库的改变,因为索引的改变是正常的过程。如果我们删除这行,我们将在过程报告的后面看到所有的变动,即使仅仅库的更新而已。最好忽略这类的改变。

接下来是常规任务的阶段,我们将创建好的任务手册导入。我们每次添加另一个任务手册的时候,要添加下面这一行:

tasks:
  - include: tasks/packages.yml

如果你现在运行 ansible-pull 命令,它应该基本上像上一篇文章中做的一样。不同的是我们已经改进了我们的组织方式,并且能够更有效的扩展它。为了节省你到上一篇文章中去寻找,ansible-pull 命令的语法参考如下:

sudo ansible-pull -U https://github.com/<github_user>/ansible.git

如果你还记得话,ansible-pull 的命令拉取一个 Git 仓库并且应用它所包含的配置。

既然我们的基础已经搭建好,我们现在可以扩展我们的 Ansible 并且添加功能。更特别的是,我们将添加配置来自动化的部署对工作站要做的改变。为了支撑这个要求,首先我们要创建一个特殊的账户来应用我们的 Ansible 配置。这个不是必要的,我们仍然能够在我们自己的用户下运行 Ansible 配置。但是使用一个隔离的用户能够将其隔离到不需要我们参与的在后台运行的一个系统进程中,

我们可以使用常规的方式来创建这个用户,但是既然我们正在使用 Ansible,我们应该尽量避开使用手动的改变。替代的是,我们将会创建一个任务手册来处理用户创建任务。这个任务手册目前将会仅仅创建一个用户,但你可以在这个任务手册中添加额外的动作来创建更多的用户。我将这个用户命名为 ansible,你可以按照自己的想法来命名(如果你做了这个改变要确保更新所有出现地方)。让我们来创建一个名为 user.yml 的任务手册并且将以下代码写进去:

- name: create ansible user
  user: name=ansible uid=900

下一步,我们需要编辑 local.yml 文件,将这个新的任务手册添加进去,像如下这样写:

- hosts: localhost
  become: true
  pre_tasks:
    - name: update repositories
      apt: update_cache=yes
      changed_when: False

  tasks:
    - include: tasks/users.yml
    - include: tasks/packages.yml

现在当我们运行 ansible-pull 命令的时候,一个名为 ansible 的用户将会在系统中被创建。注意我特地通过参数 uid 为这个用户声明了用户 ID 为 900。这个不是必须的,但建议直接创建好 UID。因为在 1000 以下的 UID 在登录界面是不会显示的,这样是很棒的,因为我们根本没有需要去使用 ansibe 账户来登录我们的桌面。UID 900 是随便定的;它应该是在 1000 以下没有被使用的任何一个数值。你可以使用以下命令在系统中去验证 UID 900 是否已经被使用了:

cat /etc/passwd |grep 900

不过,你使用这个 UID 应该不会遇到什么问题,因为迄今为止在我使用的任何发行版中我还没遇到过它是被默认使用的。

现在,我们已经拥有了一个名为 ansible 的账户,它将会在之后的自动化配置中使用。接下来,我们可以创建实际的定时作业来自动操作。我们应该将其分开放到它自己的文件中,而不是将其放置到我们刚刚创建的 users.yml 文件中。在任务目录中创建一个名为 cron.yml 的任务手册并且将以下的代码写进去:

- name: install cron job (ansible-pull)
  cron: user="ansible" name="ansible provision" minute="*/10" job="/usr/bin/ansible-pull -o -U https://github.com/<github_user>/ansible.git > /dev/null"

cron 模块的语法几乎不需加以说明。通过这个动作,我们创建了一个通过用户 ansible 运行的定时作业。这个作业将每隔 10 分钟执行一次,下面是它将要执行的命令:

/usr/bin/ansible-pull -o -U https://github.com/<github_user>/ansible.git > /dev/null

同样,我们也可以添加想要我们的所有工作站部署的额外的定时作业到这个文件中。我们只需要在新的定时作业中添加额外的动作即可。然而,仅仅是添加一个定时的任务手册是不够的,我们还需要将它添加到 local.yml 文件中以便它能够被调用。将下面的一行添加到末尾:

- include: tasks/cron.yml

现在当 ansible-pull 命令执行的时候,它将会以用户 ansible 每隔十分钟设置一个新的定时作业。但是,每个十分钟运行一个 Ansible 作业并不是一个好的方式,因为这个将消耗很多的 CPU 资源。每隔十分钟来运行对于 Ansible 来说是毫无意义的,除非我们已经在 Git 仓库中改变一些东西。

然而,我们已经解决了这个问题。注意我在定时作业中的命令 ansible-pill 添加的我们之前从未用到过的参数 -o。这个参数告诉 Ansible 只有在从上次 ansible-pull 被调用以后库有了变化后才会运行。如果库没有任何变化,它将不会做任何事情。通过这个方法,你将不会无端的浪费 CPU 资源。当然在拉取存储库的时候会使用一些 CPU 资源,但不会像再一次应用整个配置的时候使用的那么多。当 ansible-pull 执行的时候,它将会遍历剧本和任务手册中的所有任务,但至少它不会毫无目的的运行。

尽管我们已经添加了所有必须的配置要素来自动化 ansible-pull,它仍然还不能正常的工作。ansible-pull 命令需要 sudo 的权限来运行,这将允许它执行系统级的命令。然而我们创建的用户 ansible 并没有被设置为以 sudo 的权限来执行命令,因此当定时作业触发的时候,执行将会失败。通常我们可以使用命令 visudo 来手动的去设置用户 ansible 去拥有这个权限。然而我们现在应该以 Ansible 的方式来操作,而且这将会是一个向你展示 copy 模块是如何工作的机会。copy 模块允许你从库复制一个文件到文件系统的任何位置。在这个案列中,我们将会复制 sudo 的一个配置文件到 /etc/sudoers.d/ 以便用户 ansible 能够以管理员的权限执行任务。

打开 users.yml,将下面的的动作添加到文件末尾。

- name: copy sudoers_ansible
  copy: src=files/sudoers_ansible dest=/etc/sudoers.d/ansible owner=root group=root mode=0440

正如我们看到的,copy模块从我们的仓库中复制一个文件到其他任何位置。在这个过程中,我们正在抓取一个名为 sudoers_ansible(我们将在后续创建)的文件并将它复制为 /etc/sudoers/ansible,并且拥有者为 root

接下来,我们需要创建我们将要复制的文件。在你的仓库的根目录下,创建一个名为 files 的目录:

mkdir files

然后,在我们刚刚创建的 files 目录里,创建名为 sudoers_ansible 的文件,包含以下内容:

ansible ALL=(ALL) NOPASSWD: ALL

就像我们正在这样做的,在 /etc/sudoer.d 目录里创建一个文件允许我们为一个特殊的用户配置 sudo 权限。现在我们正在通过 sudo 允许用户 ansible 不需要密码提示就拥有完全控制权限。这将允许 ansible-pull 以后台任务的形式运行而不需要手动去运行。

现在,你可以通过再次运行 ansible-pull 来拉取最新的变动:

sudo ansible-pull -U https://github.com/<github_user>/ansible.git

从这里开始,ansible-pull 的定时作业将会在后台每隔十分钟运行一次来检查你的仓库是否有变化,如果它发现有变化,将会运行你的剧本并且应用你的任务手册。

所以现在我们有了一个完整的可工作方案。当你第一次设置一台新的笔记本或者台式机的时候,你要去手动的运行 ansible-pull 命令,但仅仅是在第一次的时候。从第一次之后,用户 ansible 将会在后台接手后续的运行任务。当你想对你的机器做变动的时候,你只需要简单的去拉取你的 Git 仓库来做变动,然后将这些变化回传到库中。接着,当定时作业下次在每台机器上运行的时候,它将会拉取变动的部分并应用它们。你现在只需要做一次变动,你的所有工作站将会跟着一起变动。这方法尽管有一点不同寻常,通常,你会有一个包含你的机器列表和不同机器所属规则的清单文件。然而,ansible-pull 的方法,就像在文章中描述的,是管理工作站配置的非常有效的方法。

我已经在我的 Github 仓库中更新了这篇文章中的代码,所以你可以随时去浏览来对比检查你的语法。同时我将前一篇文章中的代码移到了它自己的目录中。

第三部分,我们将通过介绍使用 Ansible 来配置 GNOME 桌面设置来结束这个系列。我将会告诉你如何设置你的墙纸和锁屏壁纸、应用一个桌面主题以及更多的东西。

同时,到了布置一些作业的时候了,大多数人都有我们所使用的各种应用的配置文件。可能是 Bash、Vim 或者其他你使用的工具的配置文件。现在你可以尝试通过我们在使用的 Ansible 库来自动复制这些配置到你的机器中。在这篇文章中,我已将向你展示了如何去复制文件,所以去尝试以下看看你是都已经能应用这些知识。


via: https://opensource.com/article/18/3/manage-your-workstation-configuration-ansible-part-2

作者:Jay LaCroix 选题:lujun9972 译者:FelixYFZ 校对:wxy

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