分类 技术 下的文章

Linux 桌面环境使你可以根据需要轻松设置键盘。下面来演示如何去做。

对于许多使用计算机很多年的用户来说,自从第一批 PC 键盘从生产线上下线后不久,Ctrl 和大写锁定键就已经在错误的位置上了。对我来说,这张 1995 年 Sun 工作站的老式键盘照片上的两个键的位置才是正确的。(原谅我放了一张模糊的图片,它是在昏暗的光线下使用 Minox 间谍相机拍摄的。)

感兴趣的话,可以读一下维基百科上对于 Ctrl 键位置的历史 的介绍。我不打算讨论将 Ctrl 键放在“a”旁边而不是 Shift 键下方的各种理由,也不评论大写锁定键的无用性,也没有打算与那些主张使用手掌根来触发 Ctrl 键的人争论,即使在一些笔记本电脑键盘上不可能这样做到,因为有的键会位于腕托以下。

相反,我将假设我不是唯一喜欢把 Ctrl 键放在“a”旁边的人,并说明如何使用 Linux 自带的灵活性在各种桌面环境中交换 Ctrl 和大写锁定键的位置。请注意,下面的演示可能只有有限的有效期,因为调整桌面设置的方法经常发生变化,但我希望这为你开了一个好头。

GNOME 3

GNOME 3 桌面环境用户可以使用 Tweaks 工具交换大写锁定和 Ctrl 键,如下所示。

具体步骤如下:

  1. 从你的 Linux 发行版的软件仓库安装 Tweaks 工具。
  2. 启动 Tweaks 程序。
  3. 从左侧菜单中选择 “Keyboard & Mouse”。
  4. 单击 “Additional Layout Options”。
  5. 在打开的窗口中单击 “Ctrl position”,然后选择 “Swap Ctrl and Caps Lock”。

完成!顺便说一句,你可以使用 Tweaks 工具做很多很酷的事情。例如,我将我的右 Ctrl 键设置为 Compose 键,这让我可以使用键盘快捷键打出各种字符,例如通过 Compose+c+,Compose+e+'Compose+o+^ 以及 Compose+n+~ 分别键入 ç、é、ô 和 ñ。(LCTT 译注:可参考 Special characters listed by extended compose sequence

KDE

我不使用 KDE,但我的同事 Seth Kenlon 写的 将改变你的生命的 KDE tweaks 这篇文章的第 5 项演示了如何重新映射按键。

Xfce

据我所知,Xfce 桌面环境没有一个方便的工具来管理这些(指交换按键)设置。 但是,setxkbmap 命令的 ctrl:swapcaps 选项可以帮助你完成交换按键的修改。这个修改包含两部分:

  1. 弄清楚命令的用法;
  2. 找出调用命令的位置,以便在桌面启动时激活它。

第一部分非常简单,命令是:

/usr/bin/setxkbmap -option "ctrl:nocaps"

在终端窗口中执行此命令,以确保结果符合你的预期。

假设上述命令有效,应该在哪里调用此命令呢?这需要一些实验。一种可能是在用户主目录的 .profile 文件中;另一个可能是将命令添加到 Xfce 的自启动配置(在设置管理器中查找 “Session and Startup”)里。

还有一种可能性是在文件 /etc/default/keyboard 中使用相同的选项,最终可能看起来像这样:

# KEYBOARD CONFIGURATION FILE

# Consult the keyboard(5) manual page.

XKBMODEL="pc105"
XKBLAYOUT="us"
XKBVARIANT=""
XKBOPTIONS="ctrl:swapcaps"

BACKSPACE="guess"

注意,这个更改将影响所有用户,因此如果你和其他人共享计算机,请准备好进行一些说明。此外,系统更新可能会覆盖此文件,因此如果你的设置失效了,就需要再次编辑它。将相同的信息放在用户主目录中的 .keyboard 文件内,可以为每个用户进行设置。

最后请注意,这些更改需要重新启动 Xfce(除非在终端窗口中的命令行上运行,但这在会话结束之后便会失效)。

LXQt 和其他桌面环境

我没有用过 LXQt,但根据我使用 LXDE 的经验,我会尝试上面用于 Xfce 的方法。我也希望适用于 Xfce 的方法可以用于其他 Linux 桌面环境。当然了,在其他桌面环境上遇到问题的时候,可以通过你最喜欢的搜索引擎来查找解决办法。

控制台

我没有在控制台上进行过尝试,因为我很少有机会与控制台(你在服务器上看到的或你的窗口系统没有正确显示时出现的界面)进行交互。上面给出的方法以人们希望的方式(即与其他应用程序一致)调整终端窗口。

但是,如果像上面一样已经编辑了 /etc/default/keyboard 文件或 〜/.keyboard,则实用程序 setupcon 可以用于更改控制台的键盘设置,以便实现相同的功能。链接 1链接 2链接 3 给出了一些关于如何从这两个文件实现这些更改的想法。第三个链接还讨论了使用 dumpkeysloadkeys 来实现想要的效果。setupcon 的手册 简短而重要,值得阅读,再结合上面 StackExchange 问题的一些评论,应该足以得到一个解决办法。

其他环境

最后,上面 StackExchange 的链接中提到的这一点值得强调 —— 配置控制台与配置终端窗口不同;如前所述,后者是通过桌面管理器进行配置的。

setxkbmapxkeyboard-configkeyboardconsole-setupsetupcon 命令的手册都是有用的参考资料。或者,如果你不喜欢阅读手册,可以看一下 这篇极好的文章


via: https://opensource.com/article/18/11/how-swap-ctrl-and-caps-lock-your-keyboard

作者:Chris Hermansen 选题:lujun9972 译者:jlztan 校对:wxy

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

我们可以轻松地通过图形化界面查看当前电量百分比、是否在充电以及当前电量还可以使用多长时间等电池状态,但是却无法查看电池健康度等相关信息。

在这篇文章就是为了解决这些问题。

在 Linux 上有很多这样的实用工具,而且可以在命令行使用。

这篇文章今天就要探讨这个主题,我会尽我所能的覆盖尽可能多的信息。

每月检查一次你的电池健康度是一个很好的想法。它可以帮你检查你当前遇到的问题是否与电池或者充电相关。

同时,我们也可以查看电池模组名称、电源、厂商以及电池规格等。

电源管理是在不使用时关闭电源或者切换系统的组件到低耗模式的一种功能。

几种在 Linux 下检查电池状态的实用工具

  • upower:是一个命令行工具,其提供了罗列系统中电源的接口。
  • acpi:显示来自 /proc 或者 /sys 文件系统中的一些信息,例如电池状态或者热量信息。
  • batstat:是一个为 Linux 打印电池状态的命令行工具。
  • tlp:可以为你带来更高级的电源管理,而无需修改任何配置。
  • class file:这个 sysfs 文件系统是一个提供了内核数据结构接口的伪文件系统。

如何使用 upower 命令检查笔记本电池状态

upower 是一个命令行工具,其提供了罗列系统中电源的接口。它在你的电脑上可以控制不同操作的延迟,这可以为你节省很大一部分电量。

只需要在 Linux 中运行以下命令获取电池以及它所依赖的其他信息。

$ upower -i /org/freedesktop/UPower/devices/battery_BAT0
  native-path:          BAT0
  vendor:               SMP
  model:                L14M4P23
  serial:               756
  power supply:         yes
  updated:              Monday 03 December 2018 07:56:18 PM IST (95 seconds ago)
  has history:          yes
  has statistics:       yes
  battery
    present:             yes
    rechargeable:        yes
    state:               discharging
    warning-level:       none
    energy:              28.23 Wh
    energy-empty:        0 Wh
    energy-full:         52.26 Wh
    energy-full-design:  60 Wh
    energy-rate:         10.714 W
    voltage:             14.819 V
    time to empty:       2.6 hours
    percentage:          54%
    capacity:            87.1%
    technology:          lithium-ion
    icon-name:          'battery-good-symbolic'
  History (charge):
    1543847178  54.000  discharging
  History (rate):
    1543847178  10.714  discharging

使用下面的格式检查电池的特定信息。

$ upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -i "state\|percentage\|time to empty"
    state:               discharging
    time to empty:       2.1 hours
    percentage:          43%

这个类似于上面的那个,但是是在充电线缆的插入后运行,这也就是为什么下面会显示正在充电状态的原因。

$ upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -i "state\|percentage\|time to empty"
    state:               charging
    percentage:          41%

如何使用 TLP 命令检查笔记本电池状态

TLP 是一个自由开源的多功能的命令行工具,它可以优化笔记本电池而无需修改任何配置。

TLP 可以为你的 Linux 带来更高级的电源管理,而无需理解任何技术细节。TLP 默认附带了一个已经为你的电池优化好的配置,所以你可以安装好后就不再管它了。尽管 TLP 是一个可以根据你的需求高度可定制的工具。

TLP 在绝大多数 Linux 发行版,例如 Arch、Debian、Fedora、Gentoo、openSUSE 等的官方库中都可用。使用你的 Linux 发行版的包管理安装 TLP 即可。

只需要在 Linux 中运行以下命令获取电池以及其他所依赖的信息。

$ sudo tlp-stat -b
--- TLP 1.1 --------------------------------------------

+++ Battery Status
/sys/class/power_supply/BAT0/manufacturer                   = SMP
/sys/class/power_supply/BAT0/model_name                     = L14M4P23
/sys/class/power_supply/BAT0/cycle_count                    = (not supported)
/sys/class/power_supply/BAT0/energy_full_design             =  60000 [mWh]
/sys/class/power_supply/BAT0/energy_full                    =  52260 [mWh]
/sys/class/power_supply/BAT0/energy_now                     =  21950 [mWh]
/sys/class/power_supply/BAT0/power_now                      =  10923 [mW]
/sys/class/power_supply/BAT0/status                         = Discharging

Charge                                                      =   42.0 [%]
Capacity                                                    =   87.1 [%]

也可以查看其他的信息。

$ sudo tlp-stat -s
--- TLP 1.1 --------------------------------------------

+++ System Info
System         = LENOVO Lenovo ideapad Y700-15ISK 80NV
BIOS           = CDCN35WW
Release        = "Manjaro Linux"
Kernel         = 4.19.6-1-MANJARO #1 SMP PREEMPT Sat Dec 1 12:21:26 UTC 2018 x86_64
/proc/cmdline  = BOOT_IMAGE=/boot/vmlinuz-4.19-x86_64 root=UUID=69d9dd18-36be-4631-9ebb-78f05fe3217f rw quiet resume=UUID=a2092b92-af29-4760-8e68-7a201922573b
Init system    = systemd 
Boot mode      = BIOS (CSM, Legacy)

+++ TLP Status
State          = enabled
Last run       = 07:16:12  IST,   4362 sec(s) ago
Mode           = battery
Power source   = battery```

### 如何使用 ACPI 命令检查电池状态  

ACPI 代表<ruby>高级配置和电源接口<rt>Advanced Configuration and Power Interface</rt></ruby>模块,它们是不同 ACPI 部件的内核模块。它们启用特殊的 ACPI 函数向 `/proc` 或者 `/sys` 中添加信息。这些信息可以通过事件或者其他监控程序的 acpid 进行解析。   

$ acpi Battery 0: Charging, 43%, 01:05:11 until charged “`

查看电池容量。

$ acpi -i
Battery 0: Charging, 43%, 01:05:07 until charged
Battery 0: design capacity 3817 mAh, last full capacity 3324 mAh = 87%

查看更多有关电池及其相关的信息。

$ acpi -V
Battery 0: Charging, 43%, 01:05:07 until charged
Battery 0: design capacity 3815 mAh, last full capacity 3323 mAh = 87%
Adapter 0: on-line
Cooling 0: Processor 0 of 10
Cooling 1: Processor 0 of 10
Cooling 2: Processor 0 of 10
Cooling 3: iwlwifi 0 of 19
Cooling 4: Processor 0 of 10
Cooling 5: iwlwifi no state information available
Cooling 6: Processor 0 of 10
Cooling 7: Processor 0 of 10
Cooling 8: Processor 0 of 10
Cooling 9: intel_powerclamp no state information available
Cooling 10: x86_pkg_temp no state information available
Cooling 11: Processor 0 of 10

如何使用 Batstat 命令查看笔记本电池状态

batstat 是一个在 Linux 终端打印电池信息的命令行工具。

Status:                       Charging
Max energy:                   50.00 Wh
Energy left:                  24.50 Wh
Power Consumption:            26.40 W
Percentage left:              49.00%
Average power Consumption:    0.00 W
Time elapsed:                  0: 0:12 since 49.00%
= Time   ======== Percent ============================================
 0: 0: 0          49.00%

如何使用 sysfs 文件系统查看笔记本电池状态

sysfs 文件系统是一个提供了内核数据结构接口的伪文件系统。sysfs 下的文件提供有关设备、内核模块、文件系统和其他内核组件的信息。

sysfs 文件系统通常挂载在 /sys。通常来说,它会被系统自动挂载,但是也可以使用例如 mount -t sysfs sysfs /sys 命令进行手动挂载。

在 sysfs 文件系统中的很多文件都是只读的,但也有一些是可写的,允许更改内核变量。为了避免冗余,符号链接被大量用于连接文件系统数中的条目。

$ cat /sys/class/power_supply/BAT0/*
0
51
Normal
0
cat: /sys/class/power_supply/BAT0/device: Is a directory
52260000
60000000
26660000
SMP
L14M4P23
cat: /sys/class/power_supply/BAT0/power: Is a directory
27656000
1
  756
Charging
cat: /sys/class/power_supply/BAT0/subsystem: Is a directory
Li-ion
Battery
POWER_SUPPLY_NAME=BAT0
POWER_SUPPLY_STATUS=Charging
POWER_SUPPLY_PRESENT=1
POWER_SUPPLY_TECHNOLOGY=Li-ion
POWER_SUPPLY_CYCLE_COUNT=0
POWER_SUPPLY_VOLTAGE_MIN_DESIGN=14800000
POWER_SUPPLY_VOLTAGE_NOW=15840000
POWER_SUPPLY_POWER_NOW=27656000
POWER_SUPPLY_ENERGY_FULL_DESIGN=60000000
POWER_SUPPLY_ENERGY_FULL=52260000
POWER_SUPPLY_ENERGY_NOW=26660000
POWER_SUPPLY_CAPACITY=51
POWER_SUPPLY_CAPACITY_LEVEL=Normal
POWER_SUPPLY_MODEL_NAME=L14M4P23
POWER_SUPPLY_MANUFACTURER=SMP
POWER_SUPPLY_SERIAL_NUMBER=  756
14800000
15840000

via: https://www.2daygeek.com/check-laptop-battery-status-and-charging-state-in-linux-terminal/

作者:Magesh Maruthamuthu 选题:lujun9972 译者:dianbanjiu 校对:wxy

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

简介

在本实验中,你将在多个同时活动的用户模式环境之间实现抢占式多任务处理。

在 Part A 中,你将在 JOS 中添加对多处理器的支持,以实现循环调度。并且添加基本的环境管理方面的系统调用(创建和销毁环境的系统调用、以及分配/映射内存)。

在 Part B 中,你将要实现一个类 Unix 的 fork(),它将允许一个用户模式中的环境去创建一个它自已的副本。

最后,在 Part C 中,你将在 JOS 中添加对进程间通讯(IPC)的支持,以允许不同用户模式环境之间进行显式通讯和同步。你也将要去添加对硬件时钟中断和优先权的支持。

预备知识

使用 git 去提交你的实验 3 的源代码,并获取课程仓库的最新版本,然后创建一个名为 lab4 的本地分支,它跟踪我们的名为 origin/lab4 的远程 lab4 分支:

    athena% cd ~/6.828/lab
    athena% add git
    athena% git pull
    Already up-to-date.
    athena% git checkout -b lab4 origin/lab4
    Branch lab4 set up to track remote branch refs/remotes/origin/lab4.
    Switched to a new branch "lab4"
    athena% git merge lab3
    Merge made by recursive.
    ...
    athena%

实验 4 包含了一些新的源文件,在开始之前你应该去浏览一遍:

kern/cpu.h       Kernel-private definitions for multiprocessor support
kern/mpconfig.c  Code to read the multiprocessor configuration 
kern/lapic.c     Kernel code driving the local APIC unit in each processor
kern/mpentry.S   Assembly-language entry code for non-boot CPUs
kern/spinlock.h  Kernel-private definitions for spin locks, including the big kernel lock 
kern/spinlock.c  Kernel code implementing spin locks
kern/sched.c     Code skeleton of the scheduler that you are about to implement

实验要求

本实验分为三部分:Part A、Part B 和 Part C。我们计划为每个部分分配一周的时间。

和以前一样,你需要完成实验中出现的、所有常规练习和至少一个挑战问题。(不是每个部分做一个挑战问题,是整个实验做一个挑战问题即可。)另外,你还要写出你实现的挑战问题的详细描述。如果你实现了多个挑战问题,你只需写出其中一个即可,虽然我们的课程欢迎你完成越多的挑战越好。在动手实验之前,请将你的挑战问题的答案写在一个名为 answers-lab4.txt 的文件中,并把它放在你的 lab 目录的根下。

Part A:多处理器支持和协调多任务处理

在本实验的第一部分,将去扩展你的 JOS 内核,以便于它能够在一个多处理器的系统上运行,并且要在 JOS 内核中实现一些新的系统调用,以便于它允许用户级环境创建附加的新环境。你也要去实现协调的循环调度,在当前的环境自愿放弃 CPU(或退出)时,允许内核将一个环境切换到另一个环境。稍后在 Part C 中,你将要实现抢占调度,它允许内核在环境占有 CPU 一段时间后,从这个环境上重新取回对 CPU 的控制,那怕是在那个环境不配合的情况下。

多处理器支持

我们继续去让 JOS 支持 “对称多处理器”(SMP),在一个多处理器的模型中,所有 CPU 们都有平等访问系统资源(如内存和 I/O 总线)的权力。虽然在 SMP 中所有 CPU 们都有相同的功能,但是在引导进程的过程中,它们被分成两种类型:引导程序处理器(BSP)负责初始化系统和引导操作系统;而在操作系统启动并正常运行后,应用程序处理器(AP)将被 BSP 激活。哪个处理器做 BSP 是由硬件和 BIOS 来决定的。到目前为止,你所有的已存在的 JOS 代码都是运行在 BSP 上的。

在一个 SMP 系统上,每个 CPU 都伴有一个本地 APIC(LAPIC)单元。这个 LAPIC 单元负责传递系统中的中断。LAPIC 还为它所连接的 CPU 提供一个唯一的标识符。在本实验中,我们将使用 LAPIC 单元(它在 kern/lapic.c 中)中的下列基本功能:

  • 读取 LAPIC 标识符(APIC ID),去告诉那个 CPU 现在我们的代码正在它上面运行(查看 cpunum())。
  • 从 BSP 到 AP 之间发送处理器间中断(IPI) STARTUP,以启动其它 CPU(查看 lapic_startap())。
  • 在 Part C 中,我们设置 LAPIC 的内置定时器去触发时钟中断,以便于支持抢占式多任务处理(查看 apic_init())。

一个处理器使用内存映射的 I/O(MMIO)来访问它的 LAPIC。在 MMIO 中,一部分物理内存是硬编码到一些 I/O 设备的寄存器中,因此,访问内存时一般可以使用相同的 load/store 指令去访问设备的寄存器。正如你所看到的,在物理地址 0xA0000 处就是一个 IO 入口(就是我们写入 VGA 缓冲区的入口)。LAPIC 就在那里,它从物理地址 0xFE000000 处(4GB 减去 32MB 处)开始,这个地址对于我们在 KERNBASE 处使用直接映射访问来说太高了。JOS 虚拟内存映射在 MMIOBASE 处,留下一个 4MB 的空隙,以便于我们有一个地方,能像这样去映射设备。由于在后面的实验中,我们将介绍更多的 MMIO 区域,你将要写一个简单的函数,从这个区域中去分配空间,并将设备的内存映射到那里。

练习 1、实现 kern/pmap.c 中的 mmio_map_region。去看一下它是如何使用的,从 kern/lapic.c 中的 lapic_init 开始看起。在 mmio_map_region 的测试运行之前,你还要做下一个练习。
引导应用程序处理器

在引导应用程序处理器之前,引导程序处理器应该会首先去收集关于多处理器系统的信息,比如总的 CPU 数、它们的 APIC ID 以及 LAPIC 单元的 MMIO 地址。在 kern/mpconfig.c 中的 mp_init() 函数,通过读取内存中位于 BIOS 区域里的 MP 配置表来获得这些信息。

boot_aps() 函数(在 kern/init.c 中)驱动 AP 的引导过程。AP 们在实模式中开始,与 boot/boot.S 中启动引导加载程序非常相似。因此,boot_aps() 将 AP 入口代码(kern/mpentry.S)复制到实模式中的那个可寻址内存地址上。不像使用引导加载程序那样,我们可以控制 AP 将从哪里开始运行代码;我们复制入口代码到 0x7000MPENTRY_PADDR)处,但是复制到任何低于 640KB 的、未使用的、页对齐的物理地址上都是可以运行的。

在那之后,通过发送 IPI STARTUP 到相关 AP 的 LAPIC 单元,以及一个初始的 CS:IP 地址(AP 将从那儿开始运行它的入口代码,在我们的案例中是 MPENTRY_PADDR ),boot_aps() 将一个接一个地激活 AP。在 kern/mpentry.S 中的入口代码非常类似于 boot/boot.S。在一些简短的设置之后,它启用分页,使 AP 进入保护模式,然后调用 C 设置程序 mp_main()(它也在 kern/init.c 中)。在继续唤醒下一个 AP 之前, boot_aps() 将等待这个 AP 去传递一个 CPU_STARTED 标志到它的 struct CpuInfo 中的 cpu_status 字段中。

练习 2、阅读 kern/init.c 中的 boot_aps()mp_main(),以及在 kern/mpentry.S 中的汇编代码。确保你理解了在 AP 引导过程中的控制流转移。然后修改在 kern/pmap.c 中的、你自己的 page_init(),实现避免在 MPENTRY_PADDR 处添加页到空闲列表上,以便于我们能够在物理地址上安全地复制和运行 AP 引导程序代码。你的代码应该会通过更新后的 check_page_free_list() 的测试(但可能会在更新后的 check_kern_pgdir() 上测试失败,我们在后面会修复它)。

.

问题 1、比较 kern/mpentry.Sboot/boot.S。记住,那个 kern/mpentry.S 是编译和链接后的,运行在 KERNBASE 上面的,就像内核中的其它程序一样,宏 MPBOOTPHYS 的作用是什么?为什么它需要在 kern/mpentry.S 中,而不是在 boot/boot.S 中?换句话说,如果在 kern/mpentry.S 中删掉它,会发生什么错误? 提示:回顾链接地址和加载地址的区别,我们在实验 1 中讨论过它们。
每个 CPU 的状态和初始化

当写一个多处理器操作系统时,区分每个 CPU 的状态是非常重要的,而每个 CPU 的状态对其它处理器是不公开的,而全局状态是整个系统共享的。kern/cpu.h 定义了大部分每个 CPU 的状态,包括 struct CpuInfo,它保存了每个 CPU 的变量。cpunum() 总是返回调用它的那个 CPU 的 ID,它可以被用作是数组的索引,比如 cpus。或者,宏 thiscpu 是当前 CPU 的 struct CpuInfo 缩略表示。

下面是你应该知道的每个 CPU 的状态:

  • 每个 CPU 的内核栈

因为内核能够同时捕获多个 CPU,因此,我们需要为每个 CPU 准备一个单独的内核栈,以防止它们运行的程序之间产生相互干扰。数组 percpu_kstacks[NCPU][KSTKSIZE] 为 NCPU 的内核栈资产保留了空间。

在实验 2 中,你映射的 bootstack 所引用的物理内存,就作为 KSTACKTOP 以下的 BSP 的内核栈。同样,在本实验中,你将每个 CPU 的内核栈映射到这个区域,而使用保护页做为它们之间的缓冲区。CPU 0 的栈将从 KSTACKTOP 处向下增长;CPU 1 的栈将从 CPU 0 的栈底部的 KSTKGAP 字节处开始,依次类推。在 inc/memlayout.h 中展示了这个映射布局。

  • 每个 CPU 的 TSS 和 TSS 描述符

为了指定每个 CPU 的内核栈在哪里,也需要有一个每个 CPU 的任务状态描述符(TSS)。CPU i 的任务状态描述符是保存在 cpus[i].cpu_ts 中,而对应的 TSS 描述符是定义在 GDT 条目 gdt[(GD_TSS0 >> 3) + i] 中。在 kern/trap.c 中定义的全局变量 ts 将不再被使用。

  • 每个 CPU 当前的环境指针

由于每个 CPU 都能同时运行不同的用户进程,所以我们重新定义了符号 curenv,让它指向到 cpus[cpunum()].cpu_env(或 thiscpu->cpu_env),它指向到当前 CPU(代码正在运行的那个 CPU)上当前正在运行的环境上。

  • 每个 CPU 的系统寄存器

所有的寄存器,包括系统寄存器,都是一个 CPU 私有的。所以,初始化这些寄存器的指令,比如 lcr3()ltr()lgdt()lidt()、等待,必须在每个 CPU 上运行一次。函数 env_init_percpu()trap_init_percpu() 就是为此目的而定义的。

练习 3、修改 mem_init_mp()(在 kern/pmap.c 中)去映射每个 CPU 的栈从 KSTACKTOP 处开始,就像在 inc/memlayout.h 中展示的那样。每个栈的大小是 KSTKSIZE 字节加上未映射的保护页 KSTKGAP 的字节。你的代码应该会通过在 check_kern_pgdir() 中的新的检查。

.

练习 4、在 trap_init_percpu()(在 kern/trap.c 文件中)的代码为 BSP 初始化 TSS 和 TSS 描述符。在实验 3 中它就运行过,但是当它运行在其它的 CPU 上就会出错。修改这些代码以便它能在所有 CPU 上都正常运行。(注意:你的新代码应该还不能使用全局变量 ts

在你完成上述练习后,在 QEMU 中使用 4 个 CPU(使用 make qemu CPUS=4make qemu-nox CPUS=4)来运行 JOS,你应该看到类似下面的输出:

    ...
    Physical memory: 66556K available, base = 640K, extended = 65532K
    check_page_alloc() succeeded!
    check_page() succeeded!
    check_kern_pgdir() succeeded!
    check_page_installed_pgdir() succeeded!
    SMP: CPU 0 found 4 CPU(s)
    enabled interrupts: 1 2
    SMP: CPU 1 starting
    SMP: CPU 2 starting
    SMP: CPU 3 starting
锁定

mp_main() 中初始化 AP 后我们的代码快速运行起来。在你更进一步增强 AP 之前,我们需要首先去处理多个 CPU 同时运行内核代码的争用状况。达到这一目标的最简单的方法是使用大内核锁。大内核锁是一个单个的全局锁,当一个环境进入内核模式时,它将被加锁,而这个环境返回到用户模式时它将释放锁。在这种模型中,在用户模式中运行的环境可以同时运行在任何可用的 CPU 上,但是只有一个环境能够运行在内核模式中;而任何尝试进入内核模式的其它环境都被强制等待。

kern/spinlock.h 中声明大内核锁,即 kernel_lock。它也提供 lock_kernel()unlock_kernel(),快捷地去获取/释放锁。你应该在以下的四个位置应用大内核锁:

  • i386_init() 时,在 BSP 唤醒其它 CPU 之前获取锁。
  • mp_main() 时,在初始化 AP 之后获取锁,然后调用 sched_yield() 在这个 AP 上开始运行环境。
  • trap() 时,当从用户模式中捕获一个 陷阱 trap 时获取锁。在检查 tf_cs 的低位比特,以确定一个陷阱是发生在用户模式还是内核模式时。
  • env_run() 中,在切换到用户模式之前释放锁。不能太早也不能太晚,否则你将可能会产生争用或死锁。

练习 5、在上面所描述的情况中,通过在合适的位置调用 lock_kernel()unlock_kernel() 应用大内核锁。

如果你的锁定是正确的,如何去测试它?实际上,到目前为止,还无法测试!但是在下一个练习中,你实现了调度之后,就可以测试了。

.

问题 2、看上去使用一个大内核锁,可以保证在一个时间中只有一个 CPU 能够运行内核代码。为什么每个 CPU 仍然需要单独的内核栈?描述一下使用一个共享内核栈出现错误的场景,即便是在它使用了大内核锁保护的情况下。

小挑战!大内核锁很简单,也易于使用。尽管如此,它消除了内核模式的所有并发。大多数现代操作系统使用不同的锁,一种称之为细粒度锁定的方法,去保护它们的共享的栈的不同部分。细粒度锁能够大幅提升性能,但是实现起来更困难并且易出错。如果你有足够的勇气,在 JOS 中删除大内核锁,去拥抱并发吧!

由你来决定锁的粒度(一个锁保护的数据量)。给你一个提示,你可以考虑在 JOS 内核中使用一个自旋锁去确保你独占访问这些共享的组件:

  • 页分配器
  • 控制台驱动
  • 调度器
  • 你将在 Part C 中实现的进程间通讯(IPC)的状态

循环调度

本实验中,你的下一个任务是去修改 JOS 内核,以使它能够在多个环境之间以“循环”的方式去交替。JOS 中的循环调度工作方式如下:

  • 在新的 kern/sched.c 中的 sched_yield() 函数负责去选择一个新环境来运行。它按顺序以循环的方式在数组 envs[] 中进行搜索,在前一个运行的环境之后开始(或如果之前没有运行的环境,就从数组起点开始),选择状态为 ENV_RUNNABLE 的第一个环境(查看 inc/env.h),并调用 env_run() 去跳转到那个环境。
  • sched_yield() 必须做到,同一个时间在两个 CPU 上绝对不能运行相同的环境。它可以判断出一个环境正运行在一些 CPU(可能是当前 CPU)上,因为,那个正在运行的环境的状态将是 ENV_RUNNING
  • 我们已经为你实现了一个新的系统调用 sys_yield(),用户环境调用它去调用内核的 sched_yield() 函数,并因此将自愿把对 CPU 的控制禅让给另外的一个环境。

练习 6、像上面描述的那样,在 sched_yield() 中实现循环调度。不要忘了去修改 syscall() 以派发 sys_yield()

确保在 mp_main 中调用了 sched_yield()

修改 kern/init.c 去创建三个(或更多个!)运行程序 user/yield.c的环境。

运行 make qemu。在它终止之前,你应该会看到像下面这样,在环境之间来回切换了五次。

也可以使用几个 CPU 来测试:make qemu CPUS=2

...
Hello, I am environment 00001000.
Hello, I am environment 00001001.
Hello, I am environment 00001002.
Back in environment 00001000, iteration 0.
Back in environment 00001001, iteration 0.
Back in environment 00001002, iteration 0.
Back in environment 00001000, iteration 1.
Back in environment 00001001, iteration 1.
Back in environment 00001002, iteration 1.
...

在程序 yield 退出之后,系统中将没有可运行的环境,调度器应该会调用 JOS 内核监视器。如果它什么也没有发生,那么你应该在继续之前修复你的代码。

问题 3、在你实现的 env_run() 中,你应该会调用 lcr3()。在调用 lcr3() 的之前和之后,你的代码引用(至少它应该会)变量 e,它是 env_run 的参数。在加载 %cr3 寄存器时,MMU 使用的地址上下文将马上被改变。但一个虚拟地址(即 e)相对一个给定的地址上下文是有意义的 —— 地址上下文指定了物理地址到那个虚拟地址的映射。为什么指针 e 在地址切换之前和之后被解除引用?

.

问题 4、无论何时,内核从一个环境切换到另一个环境,它必须要确保旧环境的寄存器内容已经被保存,以便于它们稍后能够正确地还原。为什么?这种事件发生在什么地方?

.

小挑战!给内核添加一个小小的调度策略,比如一个固定优先级的调度器,它将会给每个环境分配一个优先级,并且在执行中,较高优先级的环境总是比低优先级的环境优先被选定。如果你想去冒险一下,尝试实现一个类 Unix 的、优先级可调整的调度器,或者甚至是一个彩票调度器或跨步调度器。(可以在 Google 中查找“彩票调度”和“跨步调度”的相关资料)

写一个或两个测试程序,去测试你的调度算法是否工作正常(即,正确的算法能够按正确的次序运行)。如果你实现了本实验的 Part B 和 Part C 部分的 fork() 和 IPC,写这些测试程序可能会更容易。

.

小挑战!目前的 JOS 内核还不能应用到使用了 x87 协处理器、MMX 指令集、或流式 SIMD 扩展(SSE)的 x86 处理器上。扩展数据结构 Env 去提供一个能够保存处理器的浮点状态的地方,并且扩展上下文切换代码,当从一个环境切换到另一个环境时,能够保存和还原正确的状态。FXSAVEFXRSTOR 指令或许对你有帮助,但是需要注意的是,这些指令在旧的 x86 用户手册上没有,因为它是在较新的处理器上引入的。写一个用户级的测试程序,让它使用浮点做一些很酷的事情。

创建环境的系统调用

虽然你的内核现在已经有了在多个用户级环境之间切换的功能,但是由于内核初始化设置的原因,它在运行环境时仍然是受限的。现在,你需要去实现必需的 JOS 系统调用,以允许用户环境去创建和启动其它的新用户环境。

Unix 提供了 fork() 系统调用作为它的进程创建原语。Unix 的 fork() 通过复制调用进程(父进程)的整个地址空间去创建一个新进程(子进程)。从用户空间中能够观察到它们之间的仅有的两个差别是,它们的进程 ID 和父进程 ID(由 getpidgetppid 返回)。在父进程中,fork() 返回子进程 ID,而在子进程中,fork() 返回 0。默认情况下,每个进程得到它自己的私有地址空间,一个进程对内存的修改对另一个进程都是不可见的。

为创建一个用户模式下的新的环境,你将要提供一个不同的、更原始的 JOS 系统调用集。使用这些系统调用,除了其它类型的环境创建之外,你可以在用户空间中实现一个完整的类 Unix 的 fork()。你将要为 JOS 编写的新的系统调用如下:

  • sys_exofork

这个系统调用创建一个新的空白的环境:在它的地址空间的用户部分什么都没有映射,并且它也不能运行。这个新的环境与 sys_exofork 调用时创建它的父环境的寄存器状态完全相同。在父进程中,sys_exofork 将返回新创建进程的 envid_t(如果环境分配失败的话,返回的是一个负的错误代码)。在子进程中,它将返回 0。(因为子进程从一开始就被标记为不可运行,在子进程中,sys_exofork 将并不真的返回,直到它的父进程使用 …. 显式地将子进程标记为可运行之前。)

  • sys_env_set_status

设置指定的环境状态为 ENV_RUNNABLEENV_NOT_RUNNABLE。这个系统调用一般是在,一个新环境的地址空间和寄存器状态已经完全初始化完成之后,用于去标记一个准备去运行的新环境。

  • sys_page_alloc

分配一个物理内存页,并映射它到一个给定的环境地址空间中、给定的一个虚拟地址上。

  • sys_page_map

从一个环境的地址空间中复制一个页映射(不是页内容!)到另一个环境的地址空间中,保持一个内存共享,以便于新的和旧的映射共同指向到同一个物理内存页。

  • sys_page_unmap

在一个给定的环境中,取消映射一个给定的已映射的虚拟地址。

上面所有的系统调用都接受环境 ID 作为参数,JOS 内核支持一个约定,那就是用值 “0” 来表示“当前环境”。这个约定在 kern/env.c 中的 envid2env() 中实现的。

在我们的 user/dumbfork.c 中的测试程序里,提供了一个类 Unix 的 fork() 的非常原始的实现。这个测试程序使用了上面的系统调用,去创建和运行一个复制了它自己地址空间的子环境。然后,这两个环境像前面的练习那样使用 sys_yield 来回切换,父进程在迭代 10 次后退出,而子进程在迭代 20 次后退出。

练习 7、在 kern/syscall.c 中实现上面描述的系统调用,并确保 syscall() 能调用它们。你将需要使用 kern/pmap.ckern/env.c 中的多个函数,尤其是要用到 envid2env()。目前,每当你调用 envid2env() 时,在 checkperm 中传递参数 1。你务必要做检查任何无效的系统调用参数,在那个案例中,就返回了 -E_INVAL。使用 user/dumbfork 测试你的 JOS 内核,并在继续之前确保它运行正常。

.

小挑战!添加另外的系统调用,必须能够读取已存在的、所有的、环境的重要状态,以及设置它们。然后实现一个能够 fork 出子环境的用户模式程序,运行它一小会(即,迭代几次 sys_yield()),然后取得几张屏幕截图或子环境的检查点,然后运行子环境一段时间,然后还原子环境到检查点时的状态,然后从这里继续开始。这样,你就可以有效地从一个中间状态“回放”了子环境的运行。确保子环境与用户使用 sys_cgetc()readline() 执行了一些交互,这样,那个用户就能够查看和突变它的内部状态,并且你可以通过给子环境给定一个选择性遗忘的状况,来验证你的检查点/重启动的有效性,使它“遗忘”了在某些点之前发生的事情。

到此为止,已经完成了本实验的 Part A 部分;在你运行 make grade 之前确保它通过了所有的 Part A 的测试,并且和以往一样,使用 make handin 去提交它。如果你想尝试找出为什么一些特定的测试是失败的,可以运行 run ./grade-lab4 -v,它将向你展示内核构建的输出,和测试失败时的 QEMU 运行情况。当测试失败时,这个脚本将停止运行,然后你可以去检查 jos.out 的内容,去查看内核真实的输出内容。

Part B:写时复制 Fork

正如在前面提到过的,Unix 提供 fork() 系统调用作为它主要的进程创建原语。fork() 系统调用通过复制调用进程(父进程)的地址空间来创建一个新进程(子进程)。

xv6 Unix 的 fork() 从父进程的页上复制所有数据,然后将它分配到子进程的新页上。从本质上看,它与 dumbfork() 所采取的方法是相同的。复制父进程的地址空间到子进程,是 fork() 操作中代价最高的部分。

但是,一个对 fork() 的调用后,经常是紧接着几乎立即在子进程中有一个到 exec() 的调用,它使用一个新程序来替换子进程的内存。这是 shell 默认去做的事,在这种情况下,在复制父进程地址空间上花费的时间是非常浪费的,因为在调用 exec() 之前,子进程使用的内存非常少。

基于这个原因,Unix 的最新版本利用了虚拟内存硬件的优势,允许父进程和子进程去共享映射到它们各自地址空间上的内存,直到其中一个进程真实地修改了它们为止。这个技术就是众所周知的“写时复制”。为实现这一点,在 fork() 时,内核将复制从父进程到子进程的地址空间的映射,而不是所映射的页的内容,并且同时设置正在共享中的页为只读。当两个进程中的其中一个尝试去写入到它们共享的页上时,进程将产生一个页故障。在这时,Unix 内核才意识到那个页实际上是“虚拟的”或“写时复制”的副本,然后它生成一个新的、私有的、那个发生页故障的进程可写的、页的副本。在这种方式中,个人的页的内容并不进行真实地复制,直到它们真正进行写入时才进行复制。这种优化使得一个fork() 后在子进程中跟随一个 exec() 变得代价很低了:子进程在调用 exec() 时或许仅需要复制一个页(它的栈的当前页)。

在本实验的下一段中,你将实现一个带有“写时复制”的“真正的”类 Unix 的 fork(),来作为一个常规的用户空间库。在用户空间中实现 fork() 和写时复制有一个好处就是,让内核始终保持简单,并且因此更不易出错。它也让个别的用户模式程序在 fork() 上定义了它们自己的语义。一个有略微不同实现的程序(例如,代价昂贵的、总是复制的 dumbfork() 版本,或父子进程真实共享内存的后面的那一个),它自己可以很容易提供。

用户级页故障处理

一个用户级写时复制 fork() 需要知道关于在写保护页上的页故障相关的信息,因此,这是你首先需要去实现的东西。对用户级页故障处理来说,写时复制仅是众多可能的用途之一。

它通常是配置一个地址空间,因此在一些动作需要时,那个页故障将指示去处。例如,主流的 Unix 内核在一个新进程的栈区域中,初始的映射仅是单个页,并且在后面“按需”分配和映射额外的栈页,因此,进程的栈消费是逐渐增加的,并因此导致在尚未映射的栈地址上发生页故障。在每个进程空间的区域上发生一个页故障时,一个典型的 Unix 内核必须对它的动作保持跟踪。例如,在栈区域中的一个页故障,一般情况下将分配和映射新的物理内存页。一个在程序的 BSS 区域中的页故障,一般情况下将分配一个新页,然后用 0 填充它并映射它。在一个按需分页的系统上的一个可执行文件中,在文本区域中的页故障将从磁盘上读取相应的二进制页并映射它。

内核跟踪有大量的信息,与传统的 Unix 方法不同,你将决定在每个用户空间中关于每个页故障应该做的事。用户空间中的 bug 危害都较小。这种设计带来了额外的好处,那就是允许程序员在定义它们的内存区域时,会有很好的灵活性;对于映射和访问基于磁盘文件系统上的文件时,你应该使用后面的用户级页故障处理。

设置页故障服务程序

为了处理它自己的页故障,一个用户环境将需要在 JOS 内核上注册一个页故障服务程序入口。用户环境通过新的 sys_env_set_pgfault_upcall 系统调用来注册它的页故障入口。我们给结构 Env 增加了一个新的成员 env_pgfault_upcall,让它去记录这个信息。

练习 8、实现 sys_env_set_pgfault_upcall 系统调用。当查找目标环境的环境 ID 时,一定要确认启用了权限检查,因为这是一个“危险的”系统调用。 “`
在用户环境中的正常和异常栈

在正常运行期间,JOS 中的一个用户环境运行在正常的用户栈上:它的 ESP 寄存器开始指向到 USTACKTOP,而它所推送的栈数据将驻留在 USTACKTOP-PGSIZEUSTACKTOP-1(含)之间的页上。但是,当在用户模式中发生页故障时,内核将在一个不同的栈上重新启动用户环境,运行一个用户级页故障指定的服务程序,即用户异常栈。其它,我们将让 JOS 内核为用户环境实现自动的“栈切换”,当从用户模式转换到内核模式时,x86 处理器就以大致相同的方式为 JOS 实现了栈切换。

JOS 用户异常栈也是一个页的大小,并且它的顶部被定义在虚拟地址 UXSTACKTOP 处,因此用户异常栈的有效字节数是从 UXSTACKTOP-PGSIZEUXSTACKTOP-1(含)。尽管运行在异常栈上,用户页故障服务程序能够使用 JOS 的普通系统调用去映射新页或调整映射,以便于去修复最初导致页故障发生的各种问题。然后用户级页故障服务程序通过汇编语言 stub 返回到原始栈上的故障代码。

每个想去支持用户级页故障处理的用户环境,都需要为它自己的异常栈使用在 Part A 中介绍的 sys_page_alloc() 系统调用去分配内存。

调用用户页故障服务程序

现在,你需要去修改 kern/trap.c 中的页故障处理代码,以能够处理接下来在用户模式中发生的页故障。我们将故障发生时用户环境的状态称之为捕获时状态。

如果这里没有注册页故障服务程序,JOS 内核将像前面那样,使用一个消息来销毁用户环境。否则,内核将在异常栈上设置一个陷阱帧,它看起来就像是来自 inc/trap.h 文件中的一个 struct UTrapframe 一样:

                      <-- UXSTACKTOP
    trap-time esp
    trap-time eflags
    trap-time eip
    trap-time eax     start of struct PushRegs
    trap-time ecx
    trap-time edx
    trap-time ebx
    trap-time esp
    trap-time ebp
    trap-time esi
    trap-time edi      end of struct PushRegs
    tf_err (error code)
    fault_va           <-- %esp when handler is run

然后,内核安排这个用户环境重新运行,使用这个栈帧在异常栈上运行页故障服务程序;你必须搞清楚为什么发生这种情况。fault_va 是引发页故障的虚拟地址。

如果在一个异常发生时,用户环境已经在用户异常栈上运行,那么页故障服务程序自身将会失败。在这种情况下,你应该在当前的 tf->tf_esp 下,而不是在 UXSTACKTOP 下启动一个新的栈帧。

去测试 tf->tf_esp 是否已经在用户异常栈上准备好,可以去检查它是否在 UXSTACKTOP-PGSIZEUXSTACKTOP-1(含)的范围内。

练习 9、实现在 kern/trap.c 中的 page_fault_handler 的代码,要求派发页故障到用户模式故障服务程序上。在写入到异常栈时,一定要采取适当的预防措施。(如果用户环境运行时溢出了异常栈,会发生什么事情?)
用户模式页故障入口点

接下来,你需要去实现汇编程序,它将调用 C 页故障服务程序,并在原始的故障指令处恢复程序运行。这个汇编程序是一个故障服务程序,它由内核使用 sys_env_set_pgfault_upcall() 来注册。

练习 10、实现在 lib/pfentry.S 中的 _pgfault_upcall 程序。最有趣的部分是返回到用户代码中产生页故障的原始位置。你将要直接返回到那里,不能通过内核返回。最难的部分是同时切换栈和重新加载 EIP。

最后,你需要去实现用户级页故障处理机制的 C 用户库。

练习 11、完成 lib/pgfault.c 中的 set_pgfault_handler()。 ”`
测试

运行 user/faultread(make run-faultread)你应该会看到:

    ...
    [00000000] new env 00001000
    [00001000] user fault va 00000000 ip 0080003a
    TRAP frame ...
    [00001000] free env 00001000

运行 user/faultdie 你应该会看到:

    ...
    [00000000] new env 00001000
    i faulted at va deadbeef, err 6
    [00001000] exiting gracefully
    [00001000] free env 00001000

运行 user/faultalloc 你应该会看到:

    ...
    [00000000] new env 00001000
    fault deadbeef
    this string was faulted in at deadbeef
    fault cafebffe
    fault cafec000
    this string was faulted in at cafebffe
    [00001000] exiting gracefully
    [00001000] free env 00001000

如果你只看到第一个 “this string” 行,意味着你没有正确地处理递归页故障。

运行 user/faultallocbad 你应该会看到:

    ...
    [00000000] new env 00001000
    [00001000] user_mem_check assertion failure for va deadbeef
    [00001000] free env 00001000

确保你理解了为什么 user/faultallocuser/faultallocbad 的行为是不一样的。

小挑战!扩展你的内核,让它不仅是页故障,而是在用户空间中运行的代码能够产生的所有类型的处理器异常,都能够被重定向到一个用户模式中的异常服务程序上。写出用户模式测试程序,去测试各种各样的用户模式异常处理,比如除零错误、一般保护故障、以及非法操作码。

实现写时复制 Fork

现在,你有个内核功能要去实现,那就是在用户空间中完整地实现写时复制 fork()

我们在 lib/fork.c 中为你的 fork() 提供了一个框架。像 dumbfork()fork() 应该会创建一个新环境,然后通过扫描父环境的整个地址空间,并在子环境中设置相关的页映射。重要的差别在于,dumbfork() 复制了页,而 fork() 开始只是复制了页映射。fork() 仅当在其中一个环境尝试去写入它时才复制每个页。

fork() 的基本控制流如下:

  1. 父环境使用你在上面实现的 set_pgfault_handler() 函数,安装 pgfault() 作为 C 级页故障服务程序。
  2. 父环境调用 sys_exofork() 去创建一个子环境。
  3. 在它的地址空间中,低于 UTOP 位置的、每个可写入页、或写时复制页上,父环境调用 duppage 后,它应该会映射页写时复制到子环境的地址空间中,然后在它自己的地址空间中重新映射页写时复制。[ 注意:这里的顺序很重要(即,在父环境中标记之前,先在子环境中标记该页为 COW)!你能明白是为什么吗?尝试去想一个具体的案例,将顺序颠倒一下会发生什么样的问题。] duppage 把两个 PTE 都设置了,致使那个页不可写入,并且在 “avail” 字段中通过包含 PTE_COW 来从真正的只读页中区分写时复制页。

然而异常栈是不能通过这种方式重映射的。对于异常栈,你需要在子环境中分配一个新页。因为页故障服务程序不能做真实的复制,并且页故障服务程序是运行在异常栈上的,异常栈不能进行写时复制:那么谁来复制它呢?

fork() 也需要去处理存在的页,但不能写入或写时复制。

  1. 父环境为子环境设置了用户页故障入口点,让它看起来像它自己的一样。
  2. 现在,子环境准备去运行,所以父环境标记它为可运行。

每次其中一个环境写一个还没有写入的写时复制页时,它将产生一个页故障。下面是用户页故障服务程序的控制流:

  1. 内核传递页故障到 _pgfault_upcall,它调用 fork()pgfault() 服务程序。
  2. pgfault() 检测到那个故障是一个写入(在错误代码中检查 FEC_WR),然后将那个页的 PTE 标记为 PTE_COW。如果不是一个写入,则崩溃。
  3. pgfault() 在一个临时位置分配一个映射的新页,并将故障页的内容复制进去。然后,故障服务程序以读取/写入权限映射新页到合适的地址,替换旧的只读映射。

对于上面的几个操作,用户级 lib/fork.c 代码必须查询环境的页表(即,那个页的 PTE 是否标记为 PET_COW)。为此,内核在 UVPT 位置精确地映射环境的页表。它使用一个 聪明的映射技巧 去标记它,以使用户代码查找 PTE 时更容易。lib/entry.S 设置 uvptuvpd,以便于你能够在 lib/fork.c 中轻松查找页表信息。

练习 12、在 lib/fork.c 中实现 forkduppagepgfault

使用 forktree 程序测试你的代码。它应该会产生下列的信息,在信息中会有 ‘new env'、'free env'、和 'exiting gracefully’ 这样的字眼。信息可能不是按如下的顺序出现的,并且环境 ID 也可能不一样。

        1000: I am ''
        1001: I am '0'
        2000: I am '00'
        2001: I am '000'
        1002: I am '1'
        3000: I am '11'
        3001: I am '10'
        4000: I am '100'
        1003: I am '01'
        5000: I am '010'
        4001: I am '011'
        2002: I am '110'
        1004: I am '001'
        1005: I am '111'
        1006: I am '101'

.

小挑战!实现一个名为 sfork() 的共享内存的 fork()。这个版本的 sfork() 中,父子环境共享所有的内存页(因此,一个环境中对内存写入,就会改变另一个环境数据),除了在栈区域中的页以外,它应该使用写时复制来处理这些页。修改 user/forktree.c 去使用 sfork() 而是不常见的 fork()。另外,你在 Part C 中实现了 IPC 之后,使用你的 sfork() 去运行 user/pingpongs。你将找到提供全局指针 thisenv 功能的一个新方式。

.

小挑战!你实现的 fork 将产生大量的系统调用。在 x86 上,使用中断切换到内核模式将产生较高的代价。增加系统调用接口,以便于它能够一次发送批量的系统调用。然后修改 fork 去使用这个接口。

你的新的 fork 有多快?

你可以用一个分析来论证,批量提交对你的 fork 的性能改变,以它来(粗略地)回答这个问题:使用一个 int 0x30 指令的代价有多高?在你的 fork 中运行了多少次 int 0x30 指令?访问 TSS 栈切换的代价高吗?等待 …

或者,你可以在真实的硬件上引导你的内核,并且真实地对你的代码做基准测试。查看 RDTSC(读取时间戳计数器)指令,它的定义在 IA32 手册中,它计数自上一次处理器重置以来流逝的时钟周期数。QEMU 并不能真实地模拟这个指令(它能够计数运行的虚拟指令数量,或使用主机的 TSC,但是这两种方式都不能反映真实的 CPU 周期数)。

到此为止,Part B 部分结束了。在你运行 make grade 之前,确保你通过了所有的 Part B 部分的测试。和以前一样,你可以使用 make handin 去提交你的实验。

Part C:抢占式多任务处理和进程间通讯(IPC)

在实验 4 的最后部分,你将修改内核去抢占不配合的环境,并允许环境之间显式地传递消息。

时钟中断和抢占

运行测试程序 user/spin。这个测试程序 fork 出一个子环境,它控制了 CPU 之后,就永不停歇地运转起来。无论是父环境还是内核都不能回收对 CPU 的控制。从用户模式环境中保护系统免受 bug 或恶意代码攻击的角度来看,这显然不是个理想的状态,因为任何用户模式环境都能够通过简单的无限循环,并永不归还 CPU 控制权的方式,让整个系统处于暂停状态。为了允许内核去抢占一个运行中的环境,从其中夺回对 CPU 的控制权,我们必须去扩展 JOS 内核,以支持来自硬件时钟的外部硬件中断。

中断规则

外部中断(即:设备中断)被称为 IRQ。现在有 16 个可能出现的 IRQ,编号 0 到 15。从 IRQ 号到 IDT 条目的映射是不固定的。在 picirq.c 中的 pic_init 映射 IRQ 0 - 15 到 IDT 条目 IRQ_OFFSETIRQ_OFFSET+15

inc/trap.h 中,IRQ_OFFSET 被定义为十进制的 32。所以,IDT 条目 32 - 47 对应 IRQ 0 - 15。例如,时钟中断是 IRQ 0,所以 IDT[IRQ\_OFFSET+0](即:IDT[32])包含了内核中时钟中断服务程序的地址。这里选择 IRQ_OFFSET 是为了处理器异常不会覆盖设备中断,因为它会引起显而易见的混淆。(事实上,在早期运行 MS-DOS 的 PC 上, IRQ_OFFSET 事实上是 0,它确实导致了硬件中断服务程序和处理器异常处理之间的混淆!)

在 JOS 中,相比 xv6 Unix 我们做了一个重要的简化。当处于内核模式时,外部设备中断总是被关闭(并且,像 xv6 一样,当处于用户空间时,再打开外部设备的中断)。外部中断由 %eflags 寄存器的 FL_IF 标志位来控制(查看 inc/mmu.h)。当这个标志位被设置时,外部中断被打开。虽然这个标志位可以使用几种方式来修改,但是为了简化,我们只通过进程所保存和恢复的 %eflags 寄存器值,作为我们进入和离开用户模式的方法。

处于用户环境中时,你将要确保 FL_IF 标志被设置,以便于出现一个中断时,它能够通过处理器来传递,让你的中断代码来处理。否则,中断将被屏蔽或忽略,直到中断被重新打开后。我们使用引导加载程序的第一个指令去屏蔽中断,并且到目前为止,还没有去重新打开它们。

练习 13、修改 kern/trapentry.Skern/trap.c 去初始化 IDT 中的相关条目,并为 IRQ 0 到 15 提供服务程序。然后修改 kern/env.c 中的 env_alloc() 的代码,以确保在用户环境中,中断总是打开的。

另外,在 sched_halt() 中取消注释 sti 指令,以便于空闲的 CPU 取消屏蔽中断。

当调用一个硬件中断服务程序时,处理器不会推送一个错误代码。在这个时候,你可能需要重新阅读 80386 参考手册 的 9.2 节,或 IA-32 Intel 架构软件开发者手册 卷 3 的 5.8 节。

在完成这个练习后,如果你在你的内核上使用任意的测试程序去持续运行(即:spin),你应该会看到内核输出中捕获的硬件中断的捕获帧。虽然在处理器上已经打开了中断,但是 JOS 并不能处理它们,因此,你应该会看到在当前运行的用户环境中每个中断的错误属性并被销毁,最终环境会被销毁并进入到监视器中。

处理时钟中断

user/spin 程序中,子环境首先运行之后,它只是进入一个高速循环中,并且内核再无法取得 CPU 控制权。我们需要对硬件编程,定期产生时钟中断,它将强制将 CPU 控制权返还给内核,在内核中,我们就能够将控制权切换到另外的用户环境中。

我们已经为你写好了对 lapic_initpic_init(来自 init.c 中的 i386_init)的调用,它将设置时钟和中断控制器去产生中断。现在,你需要去写代码来处理这些中断。

练习 14、修改内核的 trap_dispatch() 函数,以便于在时钟中断发生时,它能够调用 sched_yield() 去查找和运行一个另外的环境。

现在,你应该能够用 user/spin 去做测试了:父环境应该会 fork 出子环境,sys_yield() 到它许多次,但每次切换之后,将重新获得对 CPU 的控制权,最后杀死子环境后优雅地终止。

这是做回归测试的好机会。确保你没有弄坏本实验的前面部分,确保打开中断能够正常工作(即: forktree)。另外,尝试使用 make CPUS=2 target 在多个 CPU 上运行它。现在,你应该能够通过 stresssched 测试。可以运行 make grade 去确认。现在,你的得分应该是 65 分了(总分为 80)。

进程间通讯(IPC)

(严格来说,在 JOS 中这是“环境间通讯” 或 “IEC”,但所有人都称它为 IPC,因此我们使用标准的术语。)

我们一直专注于操作系统的隔离部分,这就产生了一种错觉,好像每个程序都有一个机器完整地为它服务。一个操作系统的另一个重要服务是,当它们需要时,允许程序之间相互通讯。让程序与其它程序交互可以让它的功能更加强大。Unix 的管道模型就是一个权威的示例。

进程间通讯有许多模型。关于哪个模型最好的争论从来没有停止过。我们不去参与这种争论。相反,我们将要实现一个简单的 IPC 机制,然后尝试使用它。

JOS 中的 IPC

你将要去实现另外几个 JOS 内核的系统调用,由它们共同来提供一个简单的进程间通讯机制。你将要实现两个系统调用,sys_ipc_recvsys_ipc_try_send。然后你将要实现两个库去封装 ipc_recvipc_send

用户环境可以使用 JOS 的 IPC 机制相互之间发送 “消息” 到每个其它环境,这些消息有两部分组成:一个单个的 32 位值,和可选的一个单个页映射。允许环境在消息中传递页映射,提供了一个高效的方式,传输比一个仅适合单个的 32 位整数更多的数据,并且也允许环境去轻松地设置安排共享内存。

发送和接收消息

一个环境通过调用 sys_ipc_recv 去接收消息。这个系统调用将取消对当前环境的调度,并且不会再次去运行它,直到消息被接收为止。当一个环境正在等待接收一个消息时,任何其它环境都能够给它发送一个消息 — 而不仅是一个特定的环境,而且不仅是与接收环境有父子关系的环境。换句话说,你在 Part A 中实现的权限检查将不会应用到 IPC 上,因为 IPC 系统调用是经过慎重设计的,因此可以认为它是“安全的”:一个环境并不能通过给它发送消息导致另一个环境发生故障(除非目标环境也存在 Bug)。

尝试去发送一个值时,一个环境使用接收者的 ID 和要发送的值去调用 sys_ipc_try_send 来发送。如果指定的环境正在接收(它调用了 sys_ipc_recv,但尚未收到值),那么这个环境将去发送消息并返回 0。否则将返回 -E_IPC_NOT_RECV 来表示目标环境当前不希望来接收值。

在用户空间中的一个库函数 ipc_recv 将去调用 sys_ipc_recv,然后,在当前环境的 struct Env 中查找关于接收到的值的相关信息。

同样,一个库函数 ipc_send 将去不停地调用 sys_ipc_try_send 来发送消息,直到发送成功为止。

转移页

当一个环境使用一个有效的 dstva 参数(低于 UTOP)去调用 sys_ipc_recv 时,环境将声明愿意去接收一个页映射。如果发送方发送一个页,那么那个页应该会被映射到接收者地址空间的 dstva 处。如果接收者在 dstva 已经有了一个页映射,那么已存在的那个页映射将被取消映射。

当一个环境使用一个有效的 srcva 参数(低于 UTOP)去调用 sys_ipc_try_send 时,意味着发送方希望使用 perm 权限去发送当前映射在 srcva 处的页给接收方。在 IPC 成功之后,发送方在它的地址空间中,保留了它最初映射到 srcva 位置的页。而接收方也获得了最初由它指定的、在它的地址空间中的 dstva 处的、映射到相同物理页的映射。最后的结果是,这个页成为发送方和接收方共享的页。

如果发送方和接收方都没有表示要转移这个页,那么就不会有页被转移。在任何 IPC 之后,内核将在接收方的 Env 结构上设置新的 env_ipc_perm 字段,以允许接收页,或者将它设置为 0,表示不再接收。

实现 IPC

练习 15、实现 kern/syscall.c 中的 sys_ipc_recvsys_ipc_try_send。在实现它们之前一起阅读它们的注释信息,因为它们要一起工作。当你在这些程序中调用 envid2env 时,你应该去设置 checkperm 的标志为 0,这意味着允许任何环境去发送 IPC 消息到另外的环境,并且内核除了验证目标 envid 是否有效外,不做特别的权限检查。

接着实现 lib/ipc.c 中的 ipc_recvipc_send 函数。

使用 user/pingponguser/primes 函数去测试你的 IPC 机制。user/primes 将为每个质数生成一个新环境,直到 JOS 耗尽环境为止。你可能会发现,阅读 user/primes.c 非常有趣,你将看到所有的 fork 和 IPC 都是在幕后进行。

.

小挑战!为什么 ipc_send 要循环调用?修改系统调用接口,让它不去循环。确保你能处理多个环境尝试同时发送消息到一个环境上的情况。

.

小挑战!质数筛选是在大规模并发程序中传递消息的一个很巧妙的用法。阅读 C. A. R. Hoare 写的 《Communicating Sequential Processes》,Communications of the ACM\_ 21(8) (August 1978), 666-667,并去实现矩阵乘法示例。

.

小挑战!控制消息传递的最令人印象深刻的一个例子是,Doug McIlroy 的幂序列计算器,它在 M. Douglas McIlroy,《Squinting at Power Series》,Software–Practice and Experience, 20(7) (July 1990),661-683 中做了详细描述。实现了它的幂序列计算器,并且计算了 sin ( x + x 3) 的幂序列。

.

小挑战!通过应用 Liedtke 的论文(通过内核设计改善 IPC 性能)中的一些技术、或你可以想到的其它技巧,来让 JOS 的 IPC 机制更高效。为此,你可以随意修改内核的系统调用 API,只要你的代码向后兼容我们的评级脚本就行。

Part C 到此结束了。确保你通过了所有的评级测试,并且不要忘了将你的小挑战的答案写入到 answers-lab4.txt 中。

在动手实验之前, 使用 git statusgit diff 去检查你的更改,并且不要忘了去使用 git add answers-lab4.txt 添加你的小挑战的答案。在你全部完成后,使用 git commit -am 'my solutions to lab 4’ 提交你的更改,然后 make handin 并关注它的动向。


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

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

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

数据科学家在创建机器学习模型后,必须将其部署到生产中。要在不同的基础架构上运行它,使用容器并通过 REST API 公开模型是部署机器学习模型的常用方法。本文演示了如何在 Podman 容器中使用 Connexion 推出使用 REST API 的 TensorFlow 机器学习模型。

准备

首先,使用以下命令安装 Podman:

sudo dnf -y install podman

接下来,为容器创建一个新文件夹并切换到该目录。

mkdir deployment_container && cd deployment_container

TensorFlow 模型的 REST API

下一步是为机器学习模型创建 REST API。这个 github 仓库包含一个预训练模型,以及能让 REST API 工作的设置。

使用以下命令在 deployment_container 目录中克隆它:

git clone https://github.com/svenboesiger/titanic_tf_ml_model.git

prediction.py 和 ml\_model/

prediction.py 能进行 Tensorflow 预测,而 20x20x20 神经网络的权重位于文件夹 ml\_model/ 中。

swagger.yaml

swagger.yaml 使用 Swagger规范 定义 Connexion 库的 API。此文件包含让你的服务器提供输入参数验证、输出响应数据验证、URL 端点定义所需的所有信息。

额外地,Connexion 还将给你提供一个简单但有用的单页 Web 应用,它演示了如何使用 Javascript 调用 API 和更新 DOM。

swagger: "2.0"
info:
  description: This is the swagger file that goes with our server code
  version: "1.0.0"
  title: Tensorflow Podman Article
consumes:
  - "application/json"
produces:
  - "application/json"


basePath: "/"

paths:
  /survival_probability:
    post:
      operationId: "prediction.post"
      tags:
        - "Prediction"
      summary: "The prediction data structure provided by the server application"
      description: "Retrieve the chance of surviving the titanic disaster"
      parameters:
        - in: body
          name: passenger
          required: true
          schema:
            $ref: '#/definitions/PredictionPost'
      responses:
        '201':
          description: 'Survival probability of an individual Titanic passenger'

definitions:
  PredictionPost:
    type: object

server.py 和 requirements.txt

server.py 定义了启动 Connexion 服务器的入口点。

import connexion

app = connexion.App(__name__, specification_dir='./')

app.add_api('swagger.yaml')

if __name__ == '__main__':
 app.run(debug=True)

requirements.txt 定义了运行程序所需的 python 包。

connexion
tensorflow
pandas

容器化!

为了让 Podman 构建映像,请在上面的准备步骤中创建的 deployment_container 目录中创建一个名为 Dockerfile 的新文件:

FROM fedora:28

# File Author / Maintainer
MAINTAINER Sven Boesiger <[email protected]>

# Update the sources
RUN dnf -y update --refresh

# Install additional dependencies
RUN dnf -y install libstdc++

RUN dnf -y autoremove

# Copy the application folder inside the container
ADD /titanic_tf_ml_model /titanic_tf_ml_model

# Get pip to download and install requirements:
RUN pip3 install -r /titanic_tf_ml_model/requirements.txt

# Expose ports
EXPOSE 5000

# Set the default directory where CMD will execute
WORKDIR /titanic_tf_ml_model

# Set the default command to execute
# when creating a new container
CMD python3 server.py

接下来,使用以下命令构建容器镜像:

podman build -t ml_deployment .

运行容器

随着容器镜像的构建和准备就绪,你可以使用以下命令在本地运行它:

podman run -p 5000:5000 ml_deployment

在 Web 浏览器中输入 http://0.0.0.0:5000/ui 访问 Swagger/Connexion UI 并测试模型:

当然,你现在也可以在应用中通过 REST API 访问模型。


via: https://fedoramagazine.org/create-containerized-machine-learning-model/

作者:Sven Bösiger 选题:lujun9972 译者:geekpi 校对:wxy

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

gorilla/mux 包以直观的 API 提供了 HTTP 请求路由、验证和其它服务。

Go 网络库包括 http.ServeMux 结构类型,它支持 HTTP 请求多路复用(路由):Web 服务器将托管资源的 HTTP 请求与诸如 /sales4today 之类的 URI 路由到代码处理程序;处理程序在发送 HTTP 响应(通常是 HTML 页面)之前执行适当的逻辑。 这是该体系的草图:

             +-----------+     +--------+     +---------+
HTTP 请求---->| web 服务器 |---->| 路由   |---->| 处理程序  |
             +-----------+     +--------+     +---------+

调用 ListenAndServe 方法后启动 HTTP 服务器:

http.ListenAndServe(":8888", nil) // args: port & router

第二个参数 nil 意味着 DefaultServeMux 用于请求路由。

gorilla/mux 库包含 mux.Router 类型,可替代 DefaultServeMux 或自定义请求多路复用器。 在 ListenAndServe 调用中,mux.Router 实例将代替 nil 作为第二个参数。 下面的示例代码很好的说明了为什么 mux.Router如此吸引人:

1、一个简单的 CRUD web 应用程序

crud web 应用程序(见下文)支持四种 CRUD(创建/读取/更新/删除)操作,它们分别对应四种 HTTP 请求方法:POST、GET、PUT 和 DELETE。 在这个 CRUD 应用程序中,所管理的资源是套话与反套话的列表,每个都是套话及其反面的的套话,例如这对:

Out of sight, out of mind. Absence makes the heart grow fonder.

可以添加新的套话对,可以编辑或删除现有的套话对。

CRUD web 应用程序:

package main

import (
   "gorilla/mux"
   "net/http"
   "fmt"
   "strconv"
)

const GETALL string = "GETALL"
const GETONE string = "GETONE"
const POST string   = "POST"
const PUT string    = "PUT"
const DELETE string = "DELETE"

type clichePair struct {
   Id      int
   Cliche  string
   Counter string
}

// Message sent to goroutine that accesses the requested resource.
type crudRequest struct {
   verb     string
   cp       *clichePair
   id       int
   cliche   string
   counter  string
   confirm  chan string
}

var clichesList = []*clichePair{}
var masterId = 1
var crudRequests chan *crudRequest

// GET /
// GET /cliches
func ClichesAll(res http.ResponseWriter, req *http.Request) {
   cr := &crudRequest{verb: GETALL, confirm: make(chan string)}
   completeRequest(cr, res, "read all")
}

// GET /cliches/id
func ClichesOne(res http.ResponseWriter, req *http.Request) {
   id := getIdFromRequest(req)
   cr := &crudRequest{verb: GETONE, id: id, confirm: make(chan string)}
   completeRequest(cr, res, "read one")
}

// POST /cliches
func ClichesCreate(res http.ResponseWriter, req *http.Request) {
   cliche, counter := getDataFromRequest(req)
   cp := new(clichePair)
   cp.Cliche = cliche
   cp.Counter = counter
   cr := &crudRequest{verb: POST, cp: cp, confirm: make(chan string)}
   completeRequest(cr, res, "create")
}

// PUT /cliches/id
func ClichesEdit(res http.ResponseWriter, req *http.Request) {
   id := getIdFromRequest(req)
   cliche, counter := getDataFromRequest(req)
   cr := &crudRequest{verb: PUT, id: id, cliche: cliche, counter: counter, confirm: make(chan string)}
   completeRequest(cr, res, "edit")
}

// DELETE /cliches/id
func ClichesDelete(res http.ResponseWriter, req *http.Request) {
   id := getIdFromRequest(req)
   cr := &crudRequest{verb: DELETE, id: id, confirm: make(chan string)}
   completeRequest(cr, res, "delete")
}

func completeRequest(cr *crudRequest, res http.ResponseWriter, logMsg string) {
   crudRequests<-cr
   msg := <-cr.confirm
   res.Write([]byte(msg))
   logIt(logMsg)
}

func main() {
   populateClichesList()

   // From now on, this gorountine alone accesses the clichesList.
   crudRequests = make(chan *crudRequest, 8)
   go func() { // resource manager
      for {
         select {
         case req := <-crudRequests:
         if req.verb == GETALL {
            req.confirm<-readAll()
         } else if req.verb == GETONE {
            req.confirm<-readOne(req.id)
         } else if req.verb == POST {
            req.confirm<-addPair(req.cp)
         } else if req.verb == PUT {
            req.confirm<-editPair(req.id, req.cliche, req.counter)
         } else if req.verb == DELETE {
            req.confirm<-deletePair(req.id)
         }
      }
   }()
   startServer()
}

func startServer() {
   router := mux.NewRouter()

   // Dispatch map for CRUD operations.
   router.HandleFunc("/", ClichesAll).Methods("GET")
   router.HandleFunc("/cliches", ClichesAll).Methods("GET")
   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesOne).Methods("GET")

   router.HandleFunc("/cliches", ClichesCreate).Methods("POST")
   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesEdit).Methods("PUT")
   router.HandleFunc("/cliches/{id:[0-9]+}", ClichesDelete).Methods("DELETE")

   http.Handle("/", router) // enable the router

   // Start the server.
   port := ":8888"
   fmt.Println("\nListening on port " + port)
   http.ListenAndServe(port, router); // mux.Router now in play
}

// Return entire list to requester.
func readAll() string {
   msg := "\n"
   for _, cliche := range clichesList {
      next := strconv.Itoa(cliche.Id) + ": " + cliche.Cliche + "  " + cliche.Counter + "\n"
      msg += next
   }
   return msg
}

// Return specified clichePair to requester.
func readOne(id int) string {
   msg := "\n" + "Bad Id: " + strconv.Itoa(id) + "\n"

   index := findCliche(id)
   if index >= 0 {
      cliche := clichesList[index]
      msg = "\n" + strconv.Itoa(id) + ": " + cliche.Cliche + "  " + cliche.Counter + "\n"
   }
   return msg
}

// Create a new clichePair and add to list
func addPair(cp *clichePair) string {
   cp.Id = masterId
   masterId++
   clichesList = append(clichesList, cp)
   return "\nCreated: " + cp.Cliche + " " + cp.Counter + "\n"
}

// Edit an existing clichePair
func editPair(id int, cliche string, counter string) string {
   msg := "\n" + "Bad Id: " + strconv.Itoa(id) + "\n"
   index := findCliche(id)
   if index >= 0 {
      clichesList[index].Cliche = cliche
      clichesList[index].Counter = counter
      msg = "\nCliche edited: " + cliche + " " + counter + "\n"
   }
   return msg
}

// Delete a clichePair
func deletePair(id int) string {
   idStr := strconv.Itoa(id)
   msg := "\n" + "Bad Id: " + idStr + "\n"
   index := findCliche(id)
   if index >= 0 {
      clichesList = append(clichesList[:index], clichesList[index + 1:]...)
      msg = "\nCliche " + idStr + " deleted\n"
   }
   return msg
}

//*** utility functions
func findCliche(id int) int {
   for i := 0; i < len(clichesList); i++ {
      if id == clichesList[i].Id {
         return i;
      }
   }
   return -1 // not found
}

func getIdFromRequest(req *http.Request) int {
   vars := mux.Vars(req)
   id, _ := strconv.Atoi(vars["id"])
   return id
}

func getDataFromRequest(req *http.Request) (string, string) {
   // Extract the user-provided data for the new clichePair
   req.ParseForm()
   form := req.Form
   cliche := form["cliche"][0]    // 1st and only member of a list
   counter := form["counter"][0]  // ditto
   return cliche, counter
}

func logIt(msg string) {
   fmt.Println(msg)
}

func populateClichesList() {
   var cliches = []string {
      "Out of sight, out of mind.",
      "A penny saved is a penny earned.",
      "He who hesitates is lost.",
   }
   var counterCliches = []string {
      "Absence makes the heart grow fonder.",
      "Penny-wise and dollar-foolish.",
      "Look before you leap.",
   }

   for i := 0; i < len(cliches); i++ {
      cp := new(clichePair)
      cp.Id = masterId
      masterId++
      cp.Cliche = cliches[i]
      cp.Counter = counterCliches[i]
      clichesList = append(clichesList, cp)
   }
}

为了专注于请求路由和验证,CRUD 应用程序不使用 HTML 页面作为请求响应。 相反,请求会产生明文响应消息:套话对的列表是对 GET 请求的响应,确认新的套话对已添加到列表中是对 POST 请求的响应,依此类推。 这种简化使得使用命令行实用程序(如 curl)可以轻松地测试应用程序,尤其是 gorilla/mux 组件。

gorilla/mux 包可以从 GitHub 安装。 CRUD app 无限期运行;因此,应使用 Control-C 或同等命令终止。 CRUD 应用程序的代码,以及自述文件和简单的 curl 测试,可以在我的网站上找到。

2、请求路由

mux.Router 扩展了 REST 风格的路由,它赋给 HTTP 方法(例如,GET)和 URL 末尾的 URI 或路径(例如 /cliches)相同的权重。 URI 用作 HTTP 动词(方法)的名词。 例如,在HTTP请求中有一个起始行,例如:

GET /cliches

意味着得到所有的套话对,而一个起始线,如:

POST /cliches

意味着从 HTTP 正文中的数据创建一个套话对。

在 CRUD web 应用程序中,有五个函数充当 HTTP 请求的五种变体的请求处理程序:

ClichesAll(...)    # GET: 获取所有的套话对
ClichesOne(...)    # GET: 获取指定的套话对
ClichesCreate(...) # POST: 创建新的套话对
ClichesEdit(...)   # PUT: 编辑现有的套话对
ClichesDelete(...) # DELETE: 删除指定的套话对

每个函数都有两个参数:一个 http.ResponseWriter 用于向请求者发送一个响应,一个指向 http.Request 的指针,该指针封装了底层 HTTP 请求的信息。 使用 gorilla/mux 包可以轻松地将这些请求处理程序注册到Web服务器,并执行基于正则表达式的验证。

CRUD 应用程序中的 startServer 函数注册请求处理程序。 考虑这对注册,router 作为 mux.Router 实例:

router.HandleFunc("/", ClichesAll).Methods("GET")
router.HandleFunc("/cliches", ClichesAll).Methods("GET")

这些语句意味着对单斜线 //cliches 的 GET 请求应该路由到 ClichesAll 函数,然后处理请求。 例如,curl 请求(使用 作为命令行提示符):

% curl --request GET localhost:8888/

会产生如下结果:

1: Out of sight, out of mind.  Absence makes the heart grow fonder.
2: A penny saved is a penny earned.  Penny-wise and dollar-foolish.
3: He who hesitates is lost.  Look before you leap.

这三个套话对是 CRUD 应用程序中的初始数据。

在这句注册语句中:

router.HandleFunc("/cliches", ClichesAll).Methods("GET")
router.HandleFunc("/cliches", ClichesCreate).Methods("POST")

URI 是相同的(/cliches),但动词不同:第一种情况下为 GET 请求,第二种情况下为 POST 请求。 此注册举例说明了 REST 样式的路由,因为仅动词的不同就足以将请求分派给两个不同的处理程序。

注册中允许多个 HTTP 方法,尽管这会影响 REST 风格路由的精髓:

router.HandleFunc("/cliches", DoItAll).Methods("POST", "GET")

除了动词和 URI 之外,还可以在功能上路由 HTTP 请求。 例如,注册

router.HandleFunc("/cliches", ClichesCreate).Schemes("https").Methods("POST")

要求对 POST 请求进行 HTTPS 访问以创建新的套话对。以类似的方式,注册可能需要具有指定的 HTTP 头元素(例如,认证凭证)的请求。

3、 Request validation

gorilla/mux 包采用简单,直观的方法通过正则表达式进行请求验证。 考虑此请求处理程序以获取一个操作:

router.HandleFunc("/cliches/{id:[0-9]+}", ClichesOne).Methods("GET")

此注册排除了 HTTP 请求,例如:

% curl --request GET localhost:8888/cliches/foo

因为 foo 不是十进制数字。该请求导致熟悉的 404(未找到)状态码。 在此处理程序注册中包含正则表达式模式可确保仅在请求 URI 以十进制整数值结束时才调用 ClichesOne 函数来处理请求:

% curl --request GET localhost:8888/cliches/3  # ok

另一个例子,请求如下:

% curl --request PUT --data "..." localhost:8888/cliches

此请求导致状态代码为 405(错误方法),因为 /cliches URI 在 CRUD 应用程序中仅在 GET 和 POST 请求中注册。 像 GET 请求一样,PUT 请求必须在 URI 的末尾包含一个数字 id:

router.HandleFunc("/cliches/{id:[0-9]+}", ClichesEdit).Methods("PUT")

4、并发问题

gorilla/mux 路由器作为单独的 Go 协程执行对已注册的请求处理程序的每次调用,这意味着并发性被内置于包中。 例如,如果有十个同时发出的请求,例如

% curl --request POST --data "..." localhost:8888/cliches

然后 mux.Router 启动十个 Go 协程来执行 ClichesCreate 处理程序。

GET all、GET one、POST、PUT 和 DELETE 中的五个请求操作中,最后三个改变了所请求的资源,即包含套话对的共享 clichesList。 因此,CRUD app 需要通过协调对 clichesList 的访问来保证安全的并发性。 在不同但等效的术语中,CRUD app 必须防止 clichesList 上的竞争条件。 在生产环境中,可以使用数据库系统来存储诸如 clichesList 之类的资源,然后可以通过数据库事务来管理安全并发。

CRUD 应用程序采用推荐的Go方法来实现安全并发:

  • 只有一个 Go 协程,资源管理器在 CRUD app startServer 函数中启动,一旦 Web 服务器开始侦听请求,就可以访问 clichesList
  • 诸如 ClichesCreateClichesAll 之类的请求处理程序向 Go 通道发送(指向)crudRequest 实例(默认情况下是线程安全的),并且资源管理器单独从该通道读取。 然后,资源管理器对 clichesList 执行请求的操作。

安全并发体系结构绘制如下:

            crudRequest                读/写

请求处理程序 -------------> 资源托管者 ------------> 套话列表

在这种架构中,不需要显式锁定 clichesList,因为一旦 CRUD 请求开始进入,只有一个 Go 协程(资源管理器)访问 clichesList

为了使 CRUD 应用程序尽可能保持并发,在一方请求处理程序与另一方的单一资源管理器之间进行有效的分工至关重要。 在这里,为了审查,是 ClichesCreate 请求处理程序:

func ClichesCreate(res http.ResponseWriter, req *http.Request) {
   cliche, counter := getDataFromRequest(req)
   cp := new(clichePair)
   cp.Cliche = cliche
   cp.Counter = counter
   cr := &crudRequest{verb: POST, cp: cp, confirm: make(chan string)}
   completeRequest(cr, res, "create")
}

ClichesCreate 调用实用函数 getDataFromRequest,它从 POST 请求中提取新的套话和反套话。 然后 ClichesCreate 函数创建一个新的 ClichePair,设置两个字段,并创建一个 crudRequest 发送给单个资源管理器。 此请求包括一个确认通道,资源管理器使用该通道将信息返回给请求处理程序。 所有设置工作都可以在不涉及资源管理器的情况下完成,因为尚未访问 clichesList

请求处理程序调用实用程序函数,该函数从 POST 请求中提取新的套话和反套话。 然后,该函数创建一个新的,设置两个字段,并创建一个 crudRequest 发送到单个资源管理器。 此请求包括一个确认通道,资源管理器使用该通道将信息返回给请求处理程序。 所有设置工作都可以在不涉及资源管理器的情况下完成,因为尚未访问它。

completeRequest 实用程序函数在 ClichesCreate 函数和其他请求处理程序的末尾调用:

completeRequest(cr, res, "create") // shown above

通过将 crudRequest 放入 crudRequests 频道,使资源管理器发挥作用:

func completeRequest(cr *crudRequest, res http.ResponseWriter, logMsg string) {
   crudRequests<-cr          // 向资源托管者发送请求
   msg := <-cr.confirm       // 等待确认
   res.Write([]byte(msg))    // 向请求方发送确认
   logIt(logMsg)             // 打印到标准输出
}

对于 POST 请求,资源管理器调用实用程序函数 addPair,它会更改 clichesList 资源:

func addPair(cp *clichePair) string {
   cp.Id = masterId  // 分配一个唯一的 ID 
   masterId++        // 更新 ID 计数器
   clichesList = append(clichesList, cp) // 更新列表
   return "\nCreated: " + cp.Cliche + " " + cp.Counter + "\n"
}

资源管理器为其他 CRUD 操作调用类似的实用程序函数。 值得重复的是,一旦 Web 服务器开始接受请求,资源管理器就是唯一可以读取或写入 clichesList 的 goroutine。

对于任何类型的 Web 应用程序,gorilla/mux 包在简单直观的 API 中提供请求路由、请求验证和相关服务。 CRUD web 应用程序突出了软件包的主要功能。


via: https://opensource.com/article/18/8/http-request-routing-validation-gorillamux

作者:Marty Kalin 选题:lujun9972 译者:yongshouzhang 校对:wxy

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

不要只测试已有系统,强安全要求更积极主动的策略。

我们当中有多少人曾说出过下面这句话:“我希望这能起到作用!”?

毫无疑问,我们中的大多数人可能都不止一次地说过这句话。这句话不是用来激发信心的,相反它揭示了我们对自身能力和当前正在测试的功能的怀疑。不幸的是,这句话非常好地描述了我们传统的安全模型。我们的运营基于这样的假设,并希望我们实施的控制措施 —— 从 web 应用的漏扫到终端上的杀毒软件 —— 防止恶意的病毒和软件进入我们的系统,损坏或偷取我们的信息。

渗透测试通过积极地尝试侵入网络、向 web 应用注入恶意代码或者通过发送钓鱼邮件来传播病毒等等这些步骤来避免我们对假设的依赖。由于我们在不同的安全层面上来发现和渗透漏洞,手动测试无法解决漏洞被主动打开的情况。在安全实验中,我们故意在受控的情形下创造混乱,模拟事故的情形,来客观地检测我们检测、阻止这类问题的能力。

“安全实验为分布式系统的安全性实验提供了一种方法,以建立对抗恶意攻击的能力的信心。”

在分布式系统的安全性和复杂性方面,需要反复地重申混沌工程界的一句名言,“希望不是一种有效的策略”。我们多久会主动测试一次我们设计或构建的系统,来确定我们是否已失去对它的控制?大多数组织都不会发现他们的安全控制措施失效了,直到安全事件的发生。我们相信“安全事件不是侦察措施”,而且“希望不要出事也不是一个有效的策略”应该是 IT 专业人士执行有效安全实践的口号。

行业在传统上强调预防性的安全措施和纵深防御,但我们的任务是通过侦探实验来驱动对安全工具链的新知识和见解。因为过于专注于预防机制,我们很少尝试一次以上地或者年度性地手动测试要求的安全措施,来验证这些控件是否按设计的那样执行。

随着现代分布式系统中的无状态变量的不断改变,人们很难充分理解他们的系统的行为,因为会随时变化。解决这个问题的一种途径是通过强大的系统性的设备进行检测,对于安全性检测,你可以将这个问题分成两个主要方面:测试,和我们称之为实验的部分。测试是对我们已知部分的验证和评估,简单来说,就是我们在开始找之前,要先弄清楚我们在找什么。另一方面,实验是去寻找获得我们之前并不清楚的见解和知识。虽然测试对于一个成熟的安全团队来说是一项重要实践,但以下示例会有助于进一步地阐述两者之间的差异,并对实验的附加价值提供一个更为贴切的描述。

示例场景:精酿啤酒

思考一个用于接收精酿啤酒订单的 web 服务或者 web 应用。

这是这家精酿啤酒运输公司的一项重要服务,这些订单来自客户的移动设备、网页,和通过为这家公司精酿啤酒提供服务的餐厅的 API。这项重要服务运行在 AWS EC2 环境上,并且公司认为它是安全的。这家公司去年成功地通过了 PCI 规则,并且每年都会请第三方进行渗透测试,所以公司认为这个系统是安全的。

这家公司有时一天两次部署来进行 DevOps 和持续交付工作,公司为其感到自豪。

在了解了混沌工程和安全实验方面的东西后,该公司的开发团队希望能确定,在一个连续不断的基础上,他们的安全系统对真实世界事件的有效性和快速恢复性怎么样。与此同时,确保他们不会把安全控件不能检测到的新问题引入到系统中。

该团队希望能小规模地通过评估端口安全和防火墙设置来让他们能够检测、阻止和警告他们 EC2 安全组上端口设置的错误配置更改。

  • 该团队首先对他们正常状态下的假设进行总结。
  • 在 EC2 实例里为端口安全进行一个假设。
  • 为未认证的端口改变实验选择和配置 YAML 文件。
  • 该配置会从已选择的目标中随机指定对象,同时端口的范围和数量也会被改变。
  • 团队还会设置进行实验的时间并缩小爆破攻击的范围,来确保对业务的影响最小。
  • 对于第一次测试,团队选择在他们的测试环境中运行实验并运行一个单独的测试。
  • 在真实的 游戏日 Game Day 风格里,团队在预先计划好的两个小时的窗口期内,选择 灾难大师 Master of Disaster 来运行实验。在那段窗口期内,灾难大师会在 EC2 实例安全组中的一个实例上执行这次实验。
  • 一旦游戏日结束,团队就会开始进行一个彻底的、免于指责的事后练习。它的重点在于针对稳定状态和原始假设的实验结果。问题会类似于下面这些:

事后验证问题

  • 防火墙是否检测到未经授权的端口更改?
  • 如果更改被检测到,更改是否会被阻止?
  • 防火墙是否会将有用的日志信息记录到日志聚合工具中?
  • SIEM 是否会对未经授权的更改发出警告?
  • 如果防火墙没有检测到未经授权的更改,那么配置的管理工具是否发现了这次更改?
  • 配置管理工具是否向日志聚合工具报告了完善的信息?
  • SIEM 最后是否进行了关联报警?
  • 如果 SIEM 发出了警报,安全运营中心是否能收到这个警报?
  • 获得警报的 SOC 分析师是否能对警报采取措施,还是缺少必要的信息?
  • 如果 SOC 确定警报是真实的,那么安全事件响应是否能简单地从数据中进行分类活动?

我们系统中对失败的承认和预期已经开始揭示我们对系统工作的假设。我们的使命是利用我们所学到的,并更加广泛地应用它。以此来真正主动地解决安全问题,来超越当前传统主流的被动处理问题的安全模型。

随着我们继续在这个新领域内进行探索,我们一定会发布我们的研究成果。如果您有兴趣想了解更多有关研究的信息或是想参与进来,请随时联系 Aaron Rinehart 或者 Grayson Brewer。

特别感谢 Samuel Roden 对本文提供的见解和想法。


via: https://opensource.com/article/18/4/new-approach-security-instrumentation

作者:Aaron Rinehart 选题:lujun9972 译者:hopefully2333 校对:wxy

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