2018年12月

本文将教你如何在 Linux 命令行终端中使用 boxes 工具绘制字符形状图形来包装你的文字让其更突出。

现在正值假期,每个 Linux 终端用户都该得到一点礼物。无论你是庆祝圣诞节还是庆祝其他节日,或者什么节日也没有,都没有关系。我将在接下来的几周内介绍 24 个 Linux 命令行小玩具,供你把玩或者与朋友分享。让我们享受乐趣,让这个月过得快乐一点,因为对于北半球来说,这个月有点冷并且沉闷。

对于我要讲述的内容,可能你之前就有些了解。但是,我还是希望我们都有机会学到一些新的东西(我做了一点研究,确保可以分享 24 个小玩具)。

24 个 Linux 终端小玩具中的第一个是叫做 boxes 的小程序。为何从 boxes 说起呢?因为在没有它的情况下很难将所有其他命令礼物包装起来!

在我的 Fedora 机器上,默认没有安装 boxes 程序,但它在我的普通仓库中可以获取到,所以用如下命令就可安装:

$ sudo dnf install boxes -y

如果你在使用其他 Linux 发行版,一般也都可以在默认仓库中找到 boxes

boxes 是我真正希望在高中和大学计算机课程中就使用的实用程序,因为善意的老师要求我在每个源文件、函数、代码块等开头添加一些特定外观的备注信息。

/***************/
/* Hello World */
/***************/

事实证明,一旦你需要在框内添加几行文字,并且格式化的将它们统一风格就会变得很乏味。而 boxes 是一个简单实用程序,它使用 ASCII 艺术风格的字符形状框来包围文本。其字符形状默认风格是源代码注释风格,但也提供了一些其他选项。

它真的很容易使用。使用管道,便可以将一个简短问候语塞进字符形状盒子里。

$ cat greeting.txt | boxes -d diamonds -a c

上面的命令输出结果如下:

       /\          /\          /\
    /\//\\/\    /\//\\/\    /\//\\/\
 /\//\\\///\\/\//\\\///\\/\//\\\///\\/\
//\\\//\/\\///\\\//\/\\///\\\//\/\\///\\
\\//\/                            \/\\//
 \/                                  \/
 /\      I'm wishing you all a       /\
//\\     joyous holiday season      //\\
\\//     and a Happy Gnu Year!      \\//
 \/                                  \/
 /\                                  /\
//\\/\                            /\//\\
\\///\\/\//\\\///\\/\//\\\///\\/\//\\\//
 \/\\///\\\//\/\\///\\\//\/\\///\\\//\/
    \/\\//\/    \/\\//\/    \/\\//\/
       \/          \/          \/

或者玩点更有趣的,比如:

echo "I am a dog" | boxes -d dog -a c

不要惊讶,它将会输出如下:

          __   _,--="=--,_   __
         /  \."    .-.    "./  \
        /  ,/  _   : :   _  \/` \
        \  `| /o\  :_:  /o\ |\__/
         `-'| :="~` _ `~"=: |
            \`     (_)     `/
     .-"-.   \      |      /   .-"-.
.---{     }--|  /,.-'-.,\  |--{     }---.
 )  (_)_)_)  \_/`~-===-~`\_/  (_(_(_)  (
(              I am a dog               )
 )                                     (
'---------------------------------------'

boxes 程序提供了很多选项 用于填充、定位甚至处理正则表达式。你可以在其 项目主页 上了解更多有关 boxes 的信息,或者转到 GitHub 去下载源代码或者贡献你自己的盒子形状。说到此,如果你想给你的提交找个好点子,我已经有了一个想法:为什么不能是一个节日礼物盒子?

         _  _
        /_\/_\
 _______\_\/_/_______
|       ///\\\       |
|      ///  \\\      |
|                    |
|     "Happy pull    |
|       request!"    |
|____________________|

boxes 是基于 GPLv2 许可证的开源项目。

你有特别喜欢的命令行小玩具需要我介绍的吗?这个系列要介绍的小玩具大部分已经落实,但还预留了几个空位置。如果你有特别想了解的可以评论留言,我会查看的。如果还有空位置,我会考虑介绍它的。即使要介绍的小玩具已经有 24 个了,但如果我得到了一些很好的意见,我会在最后做一些有价值的提及。

你可以通过 Drive a locomotive through your Linux terminal 来查看明天会介绍的命令行小玩具。


via: https://opensource.com/article/18/12/linux-toy-boxes

作者:Jason Baker 选题:lujun9972 译者:jrg 校对: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中国 荣誉推出

如果你正在寻找将音频文件格式转换为 wav、mp3、ogg 或任何其他格式,SoundConverter 是你在 Linux 中需要的工具。

Audio Converter in Linux

最近我购买了一些没有 DRM 的音乐。我是从 SaReGaMa 那里买的,这是一家印度历史最悠久,规模最大的音乐品牌。下载的文件采用高清质量的 WAV 格式。

不幸的是,Rhythmbox 无法播放 WAV。最重要的是,单个文件大小约为 70MB。想象一下,将这么大的音乐传输到智能手机。它会不必要地占用大量空间。

所以我认为是时候将 WAV 文件转换为 MP3 —— 这个长青且最流行的音乐文件格式。

为此,我需要一个在 Linux 中的音频转换器。在这个教程中,我将向你展示如何使用名为 SoundCoverter 的 GUI 工具轻松地将音频文件从一种格式转换为另一种格式。

在 Linux 中安装 SoundConverter

SoundConverter 是一款流行的自由开源软件。它应该可以在大多数 Linux 发行版的官方仓库中找到。

Ubuntu/Linux Mint 用户只需在软件中心搜索 SoundConverter 并从那里安装即可。

SoundConverter application in Software Center of Ubuntu

SoundConverter 可以从软件中心安装

或者,你可以使用命令行方式。在基于 Debian 和 Ubuntu 的系统中,你可以使用以下命令:

sudo apt install soundconverter

在 Arch、Fedora 和其他非基于 Debian 的发行版中,你可以使用你的发行版的软件中心或软件包管理器。

在 Linux 中使用 SoundConverter 转换音频文件格式

安装完 SoundConverter 后,在菜单中搜索并启动它。

默认界面看起来像这样,它不能比这简单:

SoundConverter application interface in Linux

简单的界面

转换音频文件格式只要选择文件并单击转换。

但是,我建议你至少在第一次运行时检查下默认设置。默认情况下,它会将音频文件转换为 OGG 文件格式,你可能不希望这样。

Preferences in SoundConverter

可以在“首选项”中更改默认输出设置

要更改默认输出设置,请单击界面上的“首选项”图标。你会在这里看到很多可更改的选择。

你可以更改默认输出格式、比特率、质量等。你还可以选择是否要将转换后的文件保存在与原始文件相同的文件夹中。

转换后还可以选择自动删除原始文件。我不认为你应该使用那个选项。

你还可以更改输出文件名。默认情况下,它只会更改后缀,但你也可以选择根据曲目编号、标题、艺术家等进行命名。为此,原始文件中应包含适当的元数据。

说到元数据,你听说过 MusicBrainz Picard 吗?此工具可帮助你自动更新本地音乐文件的元数据。

总结

我之前用讨论过用一个小程序 在 Linux 中录制音频。这些很棒的工具通过专注某个特定的任务使得生活更轻松。你可以使用成熟和更好的音频编辑工具,如 Audacity,但对于较小的任务,如转换音频文件格式,它可能用起来很复杂。

我希望你喜欢 SoundConverter。如果你使用其他工具,请在评论中提及,我会在 FOSS 中提及。使用开心!


via: https://itsfoss.com/sound-converter-linux/

作者:Abhishek Prakash 选题:lujun9972 译者:geekpi 校对: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中国 荣誉推出

知识共享为艺术家提供访问权限和原始素材。大公司也从中受益。

我毕业于电影学院,毕业后在一所电影学校教书,之后进入一家主流电影工作室,我一直在从事电影相关的工作。创意产业的方方面面面临着同一个问题:创作者需要原材料。有趣的是,自由文化运动提出了解决方案,具体来说是在自由文化运动中出现的 知识共享 Creative Commons 组织。

知识共享能够为我们提供展示片段和小样

和其他事情一样,创造力也需要反复练习。幸运的是,在我刚开始接触电脑时,就在一本关于渲染工场的专业杂志中接触到了开源这个存在。当时我并不理解所谓的“开源”是什么,但我知道只有开源工具能帮助我在领域内稳定发展。对我来说,知识共享也是如此。知识共享可以为艺术家们提供充满丰富艺术资源的工作室。

我在电影学院任教时,经常需要给学生们准备练习编辑、录音、拟音、分级、评分的示例录像。在 Jim Munroe 的独立作品 Infest Wisely 中和 Vimeo 上的知识共享内容里我总能找到我想要的。这些逼真的镜头覆盖内容十分广泛,从独立制作到昂贵的高品质的升降镜头(一般都会用无人机代替)都有。

对实验主义艺术来说,确有无尽可能。知识共享提供了丰富的素材,这些材料可以用来整合、混剪等等,可以满足一位视觉先锋能够想到的任何用途。

在接触知识共享之前,如果我想要使用写实镜头,如果在大学,只能用之前的学生和老师拍摄的或者直接使用版权库里的镜头,或者使用有受限的版权保护的镜头。

坚守版权的底线很重要

知识共享同样能够创造经济效益。在某大型计算机公司的渲染工场工作时,我负责在某些硬件设施上测试渲染的运行情况,而这个测试时刻面临着被搁置的风险。做这些测试时,我用的都是大雄兔的资源,因为这个电影和它的组件都是可以免费使用和分享的。如果没有这个小短片,在接触写实资源之前我都没法完成我的实验,因为对于一个计算机公司来说,雇佣一只 3D 艺术家来按需布景是不太现实的。

令我震惊的是,与开源类似,知识共享已经用我们难以想象的方式支撑起了大公司。知识共享的使用可能会也可能不会影响公司的日常流程,但它填补了不足,让工作流程顺利进行。我没见到谁在他们的书中将流畅工作归功于知识共享的应用,但它确实无处不在。

我也见过一些开放版权的电影,比如辛特尔,在最近的电视节目中播放了它的短片,电视的分辨率已经超过了标准媒体。

知识共享可以提供大量原材料

艺术家需要原材料。画家需要颜料、画笔和画布。雕塑家需要陶土和工具。数字内容编辑师需要数字内容,无论它是剪贴画还是音效或者是电子游戏里的现成的精灵。

数字媒介赋予了人们超能力,让一个人就能完成需要一组人员才能完成的工作。事实上,我们大部分都好高骛远。我们想做高大上的项目,想让我们的成果不论是视觉上还是听觉上都无与伦比。我们想塑造的是宏大的世界,紧张的情节,能引起共鸣的作品,但我们所拥有的时间精力和技能与之都不匹配,达不到想要的效果。

是知识共享再一次拯救了我们,在 Freesound.orgOpenclipart.orgOpenGameArt.org 等等网站上都有大量的开放版权艺术材料。通过知识共享,艺术家可以使用各种他们自己没办法创造的原材料,来完成他们原本完不成的工作。

最神奇的是,不用自己投资,你放在网上给大家使用的原材料就能变成精美的作品,而这是你从没想过的。我在知识共享上面分享了很多音乐素材,它们现在用于无数的专辑和电子游戏里。有些人用了我的材料会通知我,有些是我自己发现的,所以这些材料的应用可能比我知道的还有多得多。有时我会偶然看到我亲手画的标志出现在我从没听说过的软件里。我见到过我为 Opensource.com 写的文章在别处发表,有的是论文的参考文献,白皮书或者参考资料中。

知识共享所代表的自由文化也是一种文化

“自由文化”这个说法过于累赘,文化,从概念上来说,是一个有机的整体。在这种文化中社会逐渐成长发展,从一个人到另一个。它是人与人之间的互动和思想交流。自由文化是自由缺失的现代世界里的特殊产物。

如果你也想对这样的局限进行反抗,想把你的思想、作品、你自己的文化分享给全世界的人,那么就来和我们一起,使用知识共享吧!


via: https://opensource.com/article/18/1/creative-commons-real-world

作者:Seth Kenlon 译者:Valoniakim 校对:wxy

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