2019年3月

学术出版业每年的价值超过 260 亿美元。

有一个行业在采用数字化或开源工具方面已落后其它行业,那就是竞争与利润并存的学术出版业。根据 Stephen Buranyi 去年在 卫报 上发表的一份图表,这个估值超过 190 亿英镑(260 亿美元)的行业,即使是最重要的科学研究方面,至今其系统在选题、出版甚至分享方面仍受限于印刷媒介的诸多限制。全新的数字时代科技展现了一个巨大机遇,可以加速探索、推动科学协作而非竞争,以及将投入从基础建设导向有益于社会的研究。

非盈利性的 eLife 倡议 是由研究资金赞助方建立,旨在通过使用数字或者开源技术来走出上述僵局。除了为生命科学和生物医疗方面的重大成就出版开放式获取的期刊,eLife 已将自己变成了一个在研究交流方面的实验和展示创新的平台 —— 而大部分的实验都是基于开源精神的。

致力于开放出版基础设施项目给予我们加速接触、采用科学技术、提升用户体验的机会。我们认为这种机会对于推动学术出版行业是重要的。大而化之地说,开源产品的用户体验经常是有待开发的,而有时候这种情况会阻止其他人去使用它。作为我们在 OSS(开源软件)开发中投入的一部分,为了鼓励更多用户使用这些产品,我们十分注重用户体验。

我们所有的代码都是开源的,并且我们也积极鼓励社区参与进我们的项目中。这对我们来说意味着更快的迭代、更多的实验、更大的透明度,同时也拓宽了我们工作的外延。

我们现在参与的项目,例如 Libero (之前称作 eLife Continuum)和 可重现文档栈 Reproducible Document Stack 的开发,以及我们最近和 Hypothesis 的合作,展示了 OSS 是如何在评估、出版以及新发现的沟通方面带来正面影响的。

Libero

Libero 是面向出版商的服务及应用套餐,它包括一个后期制作出版系统、整套前端用户界面样式套件、Libero 的镜头阅读器、一个 Open API 以及一个搜索及推荐引擎。

去年我们采取了用户驱动的方式重新设计了 Libero 的前端,可以使用户较少地分心于网站的“陈设”,而是更多地集中关注于研究文章上。我们和 eLife 社区成员测试并迭代了该站点所有的核心功能,以确保给所有人最好的阅读体验。该网站的新 API 也为机器阅读能力提供了更简单的访问途径,其中包括文本挖掘、机器学习以及在线应用开发。

我们网站上的内容以及引领新设计的样式都是开源的,以鼓励 eLife 和其它想要使用它的出版商后续的产品开发。

可重现文档栈

在与 SubstanceStencila 的合作下,eLife 也参与了一个项目来创建可重现文档栈(RDS)—— 一个开放式的创作、编纂以及在线出版可重现的计算型手稿的工具栈。

今天越来越多的研究人员能够通过 R MarkdownPython 等语言记录他们的计算实验。这些可以作为实验记录的重要部分,但是尽管它们可以独立于最终的研究文章或与之一同分享,但传统出版流程经常将它们视为次级内容。为了发表论文,使用这些语言的研究人员除了将他们的计算结果用图片的形式“扁平化”提交外别无他法。但是这导致了许多实验价值和代码和计算数据可重复利用性的流失。诸如 Jupyter 这样的电子笔记本解决方案确实可以使研究员以一种可重复利用、可执行的简单形式发布,但是这种方案仍然是出版的手稿的补充,而不是不可或缺的一部分。

可重现文档栈 项目旨在通过开发、发布一个可重现原稿的产品原型来解决这些挑战,该原型将代码和数据视为文档的组成部分,并展示了从创作到出版的完整端对端技术栈。它将最终允许用户以一种包含嵌入代码块和计算结果(统计结果、图表或图形)的形式提交他们的手稿,并在出版过程中保留这些可视、可执行的部分。那时出版商就可以将这些做为出版的在线文章的组成部分而保存。

用 Hypothesis 进行开放式注解

最近,我们与 Hypothesis 合作引进了开放式注解,使得我们网站的用户们可以写评语、高亮文章重要部分以及与在线阅读的群体互动。

通过这样的合作,开源的 Hypothesis 软件被定制得更具有现代化的特性,如单次登录验证、用户界面定制,给予了出版商在他们自己网站上实现更多的控制。这些提升正引导着关于出版学术内容的高质量讨论。

这个工具可以无缝集成到出版商的网站,学术出版平台 PubFactory 和内容解决方案供应商 Ingenta 已经利用了它优化后的特性集。HighWireSilverchair 也为他们的出版商提供了实施这套方案的机会。

其它产业和开源软件

随着时间的推移,我们希望看到更多的出版商采用 Hypothesis、Libero 以及其它开源项目去帮助他们促进重要科学研究的发现以及循环利用。但是 eLife 的创新机遇也能被其它行业所利用,因为这些软件和其它 OSS 技术在其他行业也很普遍。

数据科学的世界离不开高质量、良好支持的开源软件和围绕它们形成的社区;TensorFlow 就是这样一个好例子。感谢 OSS 以及其社区,AI 和机器学习的所有领域相比于计算机的其它领域的提升和发展更加迅猛。与之类似的是以 Linux 作为云端 Web 主机的爆炸性增长、接着是 Docker 容器、以及现在 GitHub 上最流行的开源项目之一的 Kubernetes 的增长。

所有的这些技术使得机构们能够用更少的资源做更多的事情,并专注于创新而不是重新发明轮子上。最后,这就是 OSS 真正的好处:它使得我们从互相的失败中学习,在互相的成功中成长。

我们总是在寻找与研究和科技界面方面最好的人才和想法交流的机会。你可以在 eLife Labs 上或者联系 [email protected] 找到更多这种交流的信息。


via: https://opensource.com/article/18/3/scientific-publishing-software

作者:Paul Shanno 译者:tomjlw 校对:wxy

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

可视化模式使得在 Vim 中高亮显示和操作文本变得更加容易。

Ansible 剧本文件是 YAML 格式的文本文件,经常与它们打交道的人通过他们偏爱的编辑器和扩展插件以使格式化更容易。

当我使用大多数 Linux 发行版中提供的默认编辑器来教学 Ansible 时,我经常使用 Vim 的可视化模式。它可以让我在屏幕上高亮显示我的操作 —— 我要编辑什么以及我正在做的文本处理任务,以便使我的学生更容易学习。

Vim 的可视化模式

使用 Vim 编辑文本时,可视化模式对于识别要操作的文本块非常有用。

Vim 的可视模式有三个模式:字符、行和块。进入每种模式的按键是:

  • 字符模式: v (小写)
  • 行模式: V (大写)
  • 块模式: Ctrl+v

下面是使用每种模式简化工作的一些方法。

字符模式

字符模式可以高亮显示段落中的一个句子或句子中的一个短语,然后,可以使用任何 Vim 编辑命令删除、复制、更改/修改可视化模式识别的文本。

移动一个句子

要将句子从一个地方移动到另一个地方,首先打开文件并将光标移动到要移动的句子的第一个字符。

  • 按下 v 键进入可视化字符模式。单词 VISUAL 将出现在屏幕底部。
  • 使用箭头来高亮显示所需的文本。你可以使用其他导航命令,例如 w 高亮显示至下一个单词的开头,$ 来包含该行的其余部分。
  • 在文本高亮显示后,按下 d 删除文本。
  • 如果你删除得太多或不够,按下 u 撤销并重新开始。
  • 将光标移动到新位置,然后按 p 粘贴文本。

改变一个短语

你还可以高亮显示要替换的一段文本。

  • 将光标放在要更改的第一个字符处。
  • 按下 v 进入可视化字符模式。
  • 使用导航命令(如箭头键)高亮显示该部分。
  • 按下 c 可更改高亮显示的文本。
  • 高亮显示的文本将消失,你将处于插入模式,你可以在其中添加新文本。
  • 输入新文本后,按下 Esc 返回命令模式并保存你的工作。

行模式

使用 Ansible 剧本时,任务的顺序很重要。使用可视化行模式将 Ansible 任务移动到该剧本文件中的其他位置。

操纵多行文本

  • 将光标放在要操作的文本的第一行或最后一行的任何位置。
  • 按下 Shift+V 进入行模式。单词 VISUAL LINE 将出现在屏幕底部。
  • 使用导航命令(如箭头键)高亮显示多行文本。
  • 高亮显示所需文本后,使用命令来操作它。按下 d 删除,然后将光标移动到新位置,按下 p 粘贴文本。
  • 如果要复制该 Ansible 任务,可以使用 y(yank)来代替 d(delete)。

缩进一组行

使用 Ansible 剧本或 YAML 文件时,缩进很重要。高亮显示的块可以使用 >< 键向右或向左移动。

  • 按下 > 增加所有行的缩进。
  • 按下 < 减少所有行的缩进。

尝试其他 Vim 命令将它们应用于高亮显示的文本。

块模式

可视化块模式对于操作特定的表格数据文件非常有用,但它作为验证 Ansible 剧本文件缩进的工具也很有帮助。

Ansible 任务是个项目列表,在 YAML 中,每个列表项都以一个破折号跟上一个空格开头。破折号必须在同一列中对齐,以达到相同的缩进级别。仅凭肉眼很难看出这一点。缩进 Ansible 任务中的其他行也很重要。

验证任务列表缩进相同

  • 将光标放在列表项的第一个字符上。
  • 按下 Ctrl+v 进入可视化块模式。单词 VISUAL BLOCK 将出现在屏幕底部。
  • 使用箭头键高亮显示单个字符列。你可以验证每个任务的缩进量是否相同。
  • 使用箭头键向右或向左展开块,以检查其它缩进是否正确。

尽管我对其它 Vim 编辑快捷方式很熟悉,但我仍然喜欢使用可视化模式来整理我想要出来处理的文本。当我在讲演过程中演示其它概念时,我的学生将会在这个“对他们而言很新”的文本编辑器中看到一个可以高亮文本并可以点击删除的工具。


via: https://opensource.com/article/19/2/getting-started-vim-visual-mode

作者:Susan Lauber 选题:lujun9972 译者:MjSeven 校对:wxy

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

我通常不会加密文件,但如果我打算整理我的重要文件或凭证,加密程序就会派上用场。

你可能已经在使用像 GnuPG 这样的程序来帮助你加密/解密 Linux 上的文件。还有 EncryptPad 也可以加密你的笔记。

但是,我看到了一个名为 FinalCrypt 的新的免费开源加密工具。你可以在 GitHub 页面上查看其最新的版本和源码。

在本文中,我将分享使用此工具的经验。请注意,我不会将它与其他程序进行比较 —— 因此,如果你想要多个程序之间的详细比较,请在评论中告诉我们。

FinalCrypt

使用 FinalCrypt 加密文件

FinalCrypt 使用一次性密码本密钥生成密码来加密文件。换句话说,它会生成一个 OTP 密钥,你将使用该密钥加密或解密你的文件。

根据你指定的密钥大小,密钥是完全随机的。因此,没有密钥文件就无法解密文件。

虽然 OTP 密钥用于加密/解密简单而有效,但管理或保护密钥文件对某些人来说可能是不方便的。

如果要使用 FinalCrypt,可以从它的网站下载 DEB/RPM 文件。FinalCrypt 也可用于 Windows 和 macOS。

下载后,只需双击该 deb 或 rpm 文件就能安装。如果需要,你还可以从源码编译。

使用 FileCrypt

该视频演示了如何使用FinalCrypt:

安装 FinalCrypt 后,你将在已安装的应用列表中找到它。从这里启动它。

启动后,你将看到(分割的)两栏,一个进行加密/解密,另一个选择 OTP 文件。

Using FinalCrypt for encrypting files in Linux

首先,你必须生成 OTP 密钥。下面是做法:

finalcrypt otp

请注意你的文件名可以是任何内容 —— 但你需要确保密钥文件的大小大于或等于要加密的文件。我觉得这很荒谬,但事实就是如此。

生成文件后,选择窗口右侧的密钥,然后选择要在窗口左侧加密的文件。

生成 OTP 后,你会看到高亮显示的校验和、密钥文件大小和有效状态:

选择之后,你只需要点击 “Encrypt” 来加密这些文件,如果已经加密,那么点击 “Decrypt” 来解密这些文件。

你还可以在命令行中使用 FinalCrypt 来自动执行加密作业。

如何保护你的 OTP 密钥?

加密/解密你想要保护的文件很容易。但是,你应该在哪里保存你的 OTP 密钥?

如果你未能将 OTP 密钥保存在安全的地方,那么它几乎没用。

嗯,最好的方法之一是使用专门的 USB 盘保存你的密钥。只需要在解密文件时将它插入即可。

除此之外,如果你认为足够安全,你可以将密钥保存在云服务中。

有关 FinalCrypt 的更多信息,请访问它的网站:FinalCrypt

总结

它开始时看上去有点复杂,但它实际上是 Linux 中一个简单且用户友好的加密程序。如果你想看看其他的,还有一些其他的加密保护文件夹的程序。

你如何看待 FinalCrypt?你还知道其他类似可能更好的程序么?请在评论区告诉我们,我们将会查看的!


via: https://itsfoss.com/finalcrypt/

作者:Ankush Das 选题:lujun9972 译者:geekpi 校对:wxy

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

这篇文章将了解一下 & 符号及它在 Linux 命令行中的各种用法。

如果阅读过我之前的三篇文章(123),你会觉得掌握连接各个命令之间的连接符号用法也是很重要的。实际上,命令的用法并不难,例如 mkdirtouchfind 也分别可以简单概括为“建立新目录”、“更新文件”和“在目录树中查找文件”而已。

但如果要理解

mkdir test_dir 2>/dev/null || touch images.txt && find . -iname "*jpg" > backup/dir/images.txt &

这一串命令的目的,以及为什么要这样写,就没有这么简单了。

关键之处就在于命令之间的连接符号。掌握了这些符号的用法,不仅可以让你更好理解整体的工作原理,还可以让你知道如何将不同的命令有效地结合起来,提高工作效率。

在这一篇文章和接下来的文章中,我会介绍如何使用 & 号和管道符号(|)在不同场景下的使用方法。

幕后工作

我来举一个简单的例子,看看如何使用 & 号将下面这个命令放到后台运行:

cp -R original/dir/ backup/dir/

这个命令的目的是将 original/dir/ 的内容递归地复制到 backup/dir/ 中。虽然看起来很简单,但是如果原目录里面的文件太大,在执行过程中终端就会一直被卡住。

所以,可以在命令的末尾加上一个 & 号,将这个任务放到后台去执行:

cp -R original/dir/ backup/dir/ &

任务被放到后台执行之后,就可以立即继续在同一个终端上工作了,甚至关闭终端也不影响这个任务的正常执行。需要注意的是,如果要求这个任务输出内容到标准输出中(例如 echols),即使使用了 &,也会等待这些输出任务在前台运行完毕。

当使用 & 将一个进程放置到后台运行的时候,Bash 会提示这个进程的进程 ID。在 Linux 系统中运行的每一个进程都有一个唯一的进程 ID,你可以使用进程 ID 来暂停、恢复或者终止对应的进程,因此进程 ID 是非常重要的。

这个时候,只要你还停留在启动进程的终端当中,就可以使用以下几个命令来对管理后台进程:

  • jobs 命令可以显示当前终端正在运行的进程,包括前台运行和后台运行的进程。它对每个正在执行中的进程任务分配了一个序号(这个序号不是进程 ID),可以使用这些序号来引用各个进程任务。
$ jobs
[1]- Running cp -i -R original/dir/* backup/dir/ &
[2]+ Running find . -iname "*jpg" > backup/dir/images.txt &
  • fg 命令可以将后台运行的进程任务放到前台运行,这样可以比较方便地进行交互。根据 jobs 命令提供的进程任务序号,再在前面加上 % 符号,就可以把相应的进程任务放到前台运行。
$ fg %1 # 将上面序号为 1 的 cp 任务放到前台运行
cp -i -R original/dir/* backup/dir/

如果这个进程任务是暂停状态,fg 命令会将它启动起来。

  • 使用 ctrl+z 组合键可以将前台运行的任务暂停,仅仅是暂停,而不是将任务终止。当使用 fg 或者 bg 命令将任务重新启动起来的时候,任务会从被暂停的位置开始执行。但 sleep 命令是一个特例,sleep 任务被暂停的时间会计算在 sleep 时间之内。因为 sleep 命令依据的是系统时钟的时间,而不是实际运行的时间。也就是说,如果运行了 sleep 30,然后将任务暂停 30 秒以上,那么任务恢复执行的时候会立即终止并退出。
  • bg 命令会将任务放置到后台执行,如果任务是暂停状态,也会被启动起来。
$ bg %1
[1]+ cp -i -R original/dir/* backup/dir/ &

如上所述,以上几个命令只能在同一个终端里才能使用。如果启动进程任务的终端被关闭了,或者切换到了另一个终端,以上几个命令就无法使用了。

如果要在另一个终端管理后台进程,就需要其它工具了。例如可以使用 kill 命令从另一个终端终止某个进程:

kill -s STOP <PID>

这里的 PID 就是使用 & 将进程放到后台时 Bash 显示的那个进程 ID。如果你当时没有把进程 ID 记录下来,也可以使用 ps 命令(代表 process)来获取所有正在运行的进程的进程 ID,就像这样:

ps | grep cp

执行以后会显示出包含 cp 字符串的所有进程,例如上面例子中的 cp 进程。同时还会显示出对应的进程 ID:

$ ps | grep cp
14444 pts/3 00:00:13 cp

在这个例子中,进程 ID 是 14444,因此可以使用以下命令来暂停这个后台进程:

kill -s STOP 14444

注意,这里的 STOP 等同于前面提到的 ctrl+z 组合键的效果,也就是仅仅把进程暂停掉。

如果想要把暂停了的进程启动起来,可以对进程发出 CONT 信号:

kill -s CONT 14444

这个给出一个可以向进程发出的常用信号列表。如果想要终止一个进程,可以发送 TERM 信号:

kill -s TERM 14444

如果进程不响应 TERM 信号并拒绝退出,还可以发送 KILL 信号强制终止进程:

kill -s KILL 14444

强制终止进程可能会有一定的风险,但如果遇到进程无节制消耗资源的情况,这样的信号还是能够派上用场的。

另外,如果你不确定进程 ID 是否正确,可以在 ps 命令中加上 x 参数:

$ ps x| grep cp
14444 pts/3 D 0:14 cp -i -R original/dir/Hols_2014.mp4
  original/dir/Hols_2015.mp4 original/dir/Hols_2016.mp4
  original/dir/Hols_2017.mp4 original/dir/Hols_2018.mp4 backup/dir/

这样就可以看到是不是你需要的进程 ID 了。

最后介绍一个将 psgrep 结合到一起的命令:

$ pgrep cp
8
18
19
26
33
40
47
54
61
72
88
96
136
339
6680
13735
14444

pgrep 可以直接将带有字符串 cp 的进程的进程 ID 显示出来。

可以加上一些参数让它的输出更清晰:

$ pgrep -lx cp
14444 cp

在这里,-l 参数会让 pgrep 将进程的名称显示出来,-x 参数则是让 pgrep 完全匹配 cp 这个命令。如果还想了解这个命令的更多细节,可以尝试运行 pgrep -ax

总结

在命令的末尾加上 & 可以让我们理解前台进程和后台进程的概念,以及如何管理这些进程。

在 UNIX/Linux 术语中,在后台运行的进程被称为 守护进程 daemon 。如果你曾经听说过这个词,那你现在应该知道它的意义了。

和其它符号一样,& 在命令行中还有很多别的用法。在下一篇文章中,我会更详细地介绍。


via: https://www.linux.com/blog/learn/2019/2/and-ampersand-and-linux

作者:Paul Brown 选题:lujun9972 译者:HankChow 校对:wxy

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

介绍

作为一名 Emacs 人,我尽可能让所有的工作流都在 Org 模式 Org-mode 上进行 —— 我比较喜欢文本。

我倾向于将书签记录在 Org 模式 代办列表中,而 Org 协议 Org-protocol 则允许外部进程利用 Org 模式 的某些功能。然而,要做到这一点配置起来很麻烦。(搜索引擎上)有很多教程,Firefox 也有这类 扩展,然而我对它们都不太满意。

因此我决定将我现在的配置记录在这篇博客中,方便其他有需要的人使用。

配置 Emacs Org 模式

启用 Org 协议:

(require 'org-protocol)

添加一个 捕获模板 capture template —— 我的配置是这样的:

(setq org-capture-templates
      (quote (...
              ("w" "org-protocol" entry (file "~/org/refile.org")
               "* TODO Review %a\n%U\n%:initial\n" :immediate-finish)
               ...)))

你可以从 Org 模式 手册中 捕获模板 章节中获取帮助。

设置默认使用的模板:

(setq org-protocol-default-template-key "w")

执行这些新增配置让它们在当前 Emacs 会话中生效。

快速测试

在下一步开始前,最好测试一下配置:

emacsclient -n "org-protocol:///capture?url=http%3a%2f%2fduckduckgo%2ecom&title=DuckDuckGo"

基于的配置的模板,可能会弹出一个捕获窗口。请确保正常工作,否则后面的操作没有任何意义。如果工作不正常,检查刚才的配置并且确保你执行了这些代码块。

如果你的 Org 模式 版本比较老(老于 7 版本),测试的格式会有点不同:这种 URL 编码后的格式需要改成用斜杠来分割 url 和标题。在网上搜一下很容易找出这两者的不同。

Firefox 协议

现在开始设置 Firefox。浏览 about:config。右击配置项列表,选择 “New -> Boolean”,然后输入 network.protocol-handler.expose.org-protocol 作为名字并且将值设置为 true

有些教程说这一步是可以省略的 —— 配不配因人而异。

添加 Desktop 文件

大多数的教程都有这一步:

增加一个文件 ~/.local/share/applications/org-protocol.desktop

[Desktop Entry]
Name=org-protocol
Exec=/path/to/emacsclient -n %u
Type=Application
Terminal=false
Categories=System;
MimeType=x-scheme-handler/org-protocol;

然后运行更新器。对于 i3 窗口管理器我使用下面命令(跟 gnome 一样):

update-desktop-database ~/.local/share/applications/

KDE 的方法不太一样……你可以查询其他相关教程。

在 FireFox 中设置捕获按钮

创建一个书签(我是在工具栏上创建这个书签的),地址栏输入下面内容:

javascript:location.href="org-protocol:///capture?url="+encodeURIComponent(location.href)+"&title="+encodeURIComponent(document.title||"[untitled page]")

保存该书签后,再次编辑该书签,你应该会看到其中的所有空格都被替换成了 %20 —— 也就是空格的 URL 编码形式。

现在当你点击该书签,你就会在某个 Emacs 框架中,可能是一个任意的框架中,打开一个窗口,显示你预定的模板。


via: http://www.mediaonfire.com/blog/2017_07_21_org_protocol_firefox.html

作者:Andreas Viklund 选题:lujun9972 译者:lujun9972 校对:wxy

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

屏幕03 课程基于屏幕02 课程来构建,它教你如何绘制文本,和一个操作系统命令行参数上的一个小特性。假设你已经有了课程 7:屏幕02 的操作系统代码,我们将以它为基础来构建。

1、字符串的理论知识

是的,我们的任务是为这个操作系统绘制文本。我们有几个问题需要去处理,最紧急的那个可能是如何去保存文本。令人难以置信的是,文本是迄今为止在计算机上最大的缺陷之一。原本应该是简单的数据类型却导致了操作系统的崩溃,从而削弱其他方面的加密效果,并给使用其它字母表的用户带来了许多问题。尽管如此,它仍然是极其重要的数据类型,因为它将计算机和用户很好地连接起来。文本是计算机能够理解的非常好的结构,同时人类使用它时也有足够的可读性。

那么,文本是如何保存的呢?非常简单,我们使用一种方法,给每个字母分配一个唯一的编号,然后我们保存一系列的这种编号。看起来很容易吧。问题是,那个编号的数量是不固定的。一些文本段可能比其它的长。保存普通数字,我们有一些固有的限制,即:32 位,我们不能超过这个限制,我们要添加方法去使用该长度的数字等等。“文本”这个术语,我们经常也叫它“字符串”,我们希望能够写一个可用于可变长度字符串的函数,否则就需要写很多函数!对于一般的数字来说,这不是个问题,因为只有几种通用的数字格式(字节、字、半字节、双字节)。

可变数据类型(比如文本)要求能够进行很复杂的处理。

因此,如何判断字符串长度?我想显而易见的答案是存储字符串的长度,然后去存储组成字符串的字符。这称为长度前缀,因为长度位于字符串的前面。不幸的是,计算机科学家的先驱们不同意这么做。他们认为使用一个称为空终止符(NULL)的特殊字符(用 \0 表示)来表示字符串结束更有意义。这样确定简化了许多字符串算法,因为你只需要持续操作直到遇到空终止符为止。不幸的是,这成为了许多安全问题的根源。如果一个恶意用户给你一个特别长的字符串会发生什么状况?如果没有足够的空间去保存这个特别长的字符串会发生什么状况?你可以使用一个字符串复制函数来做复制,直到遇到空终止符为止,但是因为字符串特别长,而覆写了你的程序,怎么办?这看上去似乎有些较真,但是,缓冲区溢出攻击还是经常发生。长度前缀可以很容易地缓解这种问题,因为它可以很容易地推算出保存这个字符串所需要的缓冲区的长度。作为一个操作系统开发者,我留下这个问题,由你去决定如何才能更好地存储文本。

缓冲区溢出攻击祸害计算机由来已久。最近,Wii、Xbox 和 Playstation 2、以及大型系统如 Microsoft 的 Web 和数据库服务器,都遭受到缓冲区溢出攻击。

接下来的事情是,我们需要确定的是如何最好地将字符映射到数字。幸运的是,这是高度标准化的,我们有两个主要的选择,Unicode 和 ASCII。Unicode 几乎将每个有用的符号都映射为数字,作为代价,我们需要有很多很多的数字,和一个更复杂的编码方法。ASCII 为每个字符使用一个字节,因此它仅保存拉丁字母、数字、少数符号和少数特殊字符。因此,ASCII 是非常易于实现的,与之相比,Unicode 的每个字符占用的空间并不相同,这使得字符串算法更棘手。通常,操作系统上字符使用 ASCII,并不是为了显示给最终用户的(开发者和专家用户除外),给终端用户显示信息使用 Unicode,因为 Unicode 能够支持像日语字符这样的东西,并且因此可以实现本地化。

幸运的是,在这里我们不需要去做选择,因为它们的前 128 个字符是完全相同的,并且编码也是完全一样的。

表 1.1 ASCII/Unicode 符号 0-127

0123456789abcdef
00NULSOHSTXETXEOTENQACKBELBSHTLFVTFFCRSOSI
10DLEDC1DC2DC3DC4NAKSYNETBCANEMSUBESCFSGSRSUS
20!#$%&.()*+,-./
300123456789:;<=>?
40@ABCDEFGHIJKLMNO
50PQRSTUVWXYZ[\]^\_
60`abcdefghijklmno
70pqrstuvwxyz{ }~DEL

这个表显示了前 128 个符号。一个符号的十六进制表示是行的值加上列的值,比如 A 是 41 16。你可以惊奇地发现前两行和最后的值。这 33 个特殊字符是不可打印字符。事实上,许多人都忽略了它们。它们之所以存在是因为 ASCII 最初设计是基于计算机网络来传输数据的一种方法。因此它要发送的信息不仅仅是符号。你应该学习的重要的特殊字符是 NUL,它就是我们前面提到的空终止符。HT 水平制表符就是我们经常说的 tab,而 LF 换行符用于生成一个新行。你可能想研究和使用其它特殊字符在你的操行系统中的意义。

2、字符

到目前为止,我们已经知道了一些关于字符串的知识,我们可以开始想想它们是如何显示的。为了显示一个字符串,我们需要做的最基础的事情是能够显示一个字符。我们的第一个任务是编写一个 DrawCharacter 函数,给它一个要绘制的字符和一个位置,然后它将这个字符绘制出来。

这就很自然地引出关于字体的讨论。我们已经知道有许多方式去按照选定的字体去显示任何给定的字母。那么字体又是如何工作的呢?在计算机科学的早期阶段,字体就是所有字母的一系列小图片而已,这种字体称为位图字体,而所有的字符绘制方法就是将图片复制到屏幕上。当人们想去调整字体大小时就出问题了。有时我们需要大的字母,而有时我们需要的是小的字母。尽管我们可以为每个字体、每种大小、每个字符都绘制新图片,但这种作法过于单调乏味。所以,发明了矢量字体。矢量字体不包含字体的图像,它包含的是如何去绘制字符的描述,即:一个 o 可能是最大字母高度的一半为半径绘制的圆。现代操作系统都几乎仅使用这种字体,因为这种字体在任何分辨率下都很完美。

在许多操作系统中使用的 TrueType 字体格式是很强大的,它内置有它自己的汇编语言,以确保在任何分辨率下字母看起来都是正确的。

不幸的是,虽然我很想包含一个矢量字体的格式的实现,但它的内容太多了,将占用这个网站的剩余部分。所以,我们将去实现一个位图字体,可是,如果你想去做一个像样的图形操作系统,那么矢量字体将是很有用的。

在下载页面上的字体节中,我们提供了几个 .bin 文件。这些只是字体的原始二进制数据文件。为完成本教程,从等宽、单色、8x16 节中挑选你喜欢的字体。然后下载它并保存到 source 目录中并命名为 font.bin 文件。这些文件只是每个字母的单色图片,它们每个字母刚好是 8 x 16 个像素。所以,每个字母占用 16 字节,第一个字节是第一行,第二个字节是第二行,依此类推。

这个示意图展示了等宽、单色、8x16 的字符 A 的 “Bitstream Vera Sans Mono” 字体。在这个文件中,我们可以找到,它从第 41 16 × 10 16 = 410 16 字节开始的十六进制序列:

00, 00, 00, 10, 28, 28, 28, 44, 44, 7C, C6, 82, 00, 00, 00, 00

在这里我们将使用等宽字体,因为等宽字体的每个字符大小是相同的。不幸的是,大多数字体的复杂之处就是因为它的宽度不同,从而导致它的显示代码更复杂。在下载页面上还包含有几个其它的字体,并包含了这种字体的存储格式介绍。

我们回到正题。复制下列代码到 drawing.s 中的 graphicsAddress.int 0 之后。

.align 4
font:
.incbin "font.bin"
.incbin "file" 插入来自文件 “file” 中的二进制数据。

这段代码复制文件中的字体数据到标签为 font 的地址。我们在这里使用了一个 .align 4 去确保每个字符都是从 16 字节的倍数开始,这是一个以后经常用到的用于加快访问速度的技巧。

现在我们去写绘制字符的方法。我在下面给出了伪代码,你可以尝试自己去实现它。按惯例 >> 的意思是逻辑右移。

function drawCharacter(r0 is character, r1 is x, r2 is y)
  if character > 127 then exit
  set charAddress to font + character × 16
  for row = 0 to 15
  set bits to readByte(charAddress + row)
  for bit = 0 to 7
    if test(bits >> bit, 0x1)
    then setPixel(x + bit, y + row)
    next
  next
  return r0 = 8, r1 = 16
end function

如果直接去实现它,这显然不是个高效率的做法。像绘制字符这样的事情,效率是最重要的。因为我们要频繁使用它。我们来探索一些改善的方法,使其成为最优化的汇编代码。首先,因为我们有一个 × 16,你应该会马上想到它等价于逻辑左移 4 位。紧接着我们有一个变量 row,它只与 charAddressy 相加。所以,我们可以通过增加替代变量来消除它。现在唯一的问题是如何判断我们何时完成。这时,一个很好用的 .align 4 上场了。我们知道,charAddress 将从包含 0 的低位半字节开始。这意味着我们可以通过检查低位半字节来看到进入字符数据的程度。

虽然我们可以消除对 bit 的需求,但我们必须要引入新的变量才能实现,因此最好还是保留它。剩下唯一的改进就是去除嵌套的 bits >> bit

function drawCharacter(r0 is character, r1 is x, r2 is y)
  if character > 127 then exit
  set charAddress to font + character << 4
  loop
    set bits to readByte(charAddress)
    set bit to 8
    loop
      set bits to bits << 1
      set bit to bit - 1
      if test(bits, 0x100)
      then setPixel(x + bit, y)
    until bit = 0
    set y to y + 1
    set chadAddress to chadAddress + 1
  until charAddress AND 0b1111 = 0
  return r0 = 8, r1 = 16
end function

现在,我们已经得到了非常接近汇编代码的代码了,并且代码也是经过优化的。下面就是上述代码用汇编写出来的代码。

.globl DrawCharacter
DrawCharacter:
cmp r0,#127
movhi r0,#0
movhi r1,#0
movhi pc,lr

push {r4,r5,r6,r7,r8,lr}
x .req r4
y .req r5
charAddr .req r6
mov x,r1
mov y,r2
ldr charAddr,=font
add charAddr, r0,lsl #4

lineLoop$:

  bits .req r7
  bit .req r8
  ldrb bits,[charAddr]
  mov bit,#8
  
  charPixelLoop$:
  
    subs bit,#1
    blt charPixelLoopEnd$
    lsl bits,#1
    tst bits,#0x100
    beq charPixelLoop$
    
    add r0,x,bit
    mov r1,y
    bl DrawPixel
    
    teq bit,#0
    bne charPixelLoop$
  
  charPixelLoopEnd$:
  .unreq bit
  .unreq bits
  add y,#1
  add charAddr,#1
  tst charAddr,#0b1111
  bne lineLoop$

.unreq x
.unreq y
.unreq charAddr

width .req r0
height .req r1
mov width,#8
mov height,#16

pop {r4,r5,r6,r7,r8,pc}
.unreq width
.unreq height

3、字符串

现在,我们可以绘制字符了,我们可以绘制文本了。我们需要去写一个方法,给它一个字符串为输入,它通过递增位置来绘制出每个字符。为了做的更好,我们应该去实现新的行和制表符。是时候决定关于空终止符的问题了,如果你想让你的操作系统使用它们,可以按需来修改下面的代码。为避免这个问题,我将给 DrawString 函数传递一个字符串长度,以及字符串的地址,和 xy 的坐标作为参数。

function drawString(r0 is string, r1 is length, r2 is x, r3 is y)
  set x0 to x
  for pos = 0 to length - 1
    set char to loadByte(string + pos)
    set (cwidth, cheight) to DrawCharacter(char, x, y)
    if char = '\n' then
      set x to x0
      set y to y + cheight
    otherwise if char = '\t' then
      set x1 to x
      until x1 > x0
        set x1 to x1 + 5 × cwidth
      loop
    set x to x1
    otherwise
      set x to x + cwidth
    end if
  next
end function

同样,这个函数与汇编代码还有很大的差距。你可以随意去尝试实现它,即可以直接实现它,也可以简化它。我在下面给出了简化后的函数和汇编代码。

很明显,写这个函数的人并不很有效率(感到奇怪吗?它就是我写的)。再说一次,我们有一个 pos 变量,它用于递增及与其它东西相加,这是完全没有必要的。我们可以去掉它,而同时进行长度递减,直到减到 0 为止,这样就少用了一个寄存器。除了那个烦人的乘以 5 以外,函数的其余部分还不错。在这里要做的一个重要事情是,将乘法移到循环外面;即便使用位移运算,乘法仍然是很慢的,由于我们总是加一个乘以 5 的相同的常数,因此没有必要重新计算它。实际上,在汇编代码中它可以在一个操作数中通过参数移位来实现,因此我将代码改变为下面这样。

function drawString(r0 is string, r1 is length, r2 is x, r3 is y)
  set x0 to x
  until length = 0
    set length to length - 1
    set char to loadByte(string)
    set (cwidth, cheight) to DrawCharacter(char, x, y)
    if char = '\n' then
      set x to x0
      set y to y + cheight
    otherwise if char = '\t' then
      set x1 to x
      set cwidth to cwidth + cwidth << 2
      until x1 > x0
        set x1 to x1 + cwidth
      loop
      set x to x1
    otherwise
      set x to x + cwidth
    end if
    set string to string + 1
  loop
end function

以下是它的汇编代码:

.globl DrawString
DrawString:
x .req r4
y .req r5
x0 .req r6
string .req r7
length .req r8
char .req r9
push {r4,r5,r6,r7,r8,r9,lr}

mov string,r0
mov x,r2
mov x0,x
mov y,r3
mov length,r1

stringLoop$:
  subs length,#1
  blt stringLoopEnd$
  
  ldrb char,[string]
  add string,#1
  
  mov r0,char
  mov r1,x
  mov r2,y
  bl DrawCharacter
  cwidth .req r0
  cheight .req r1
  
  teq char,#'\n'
  moveq x,x0
  addeq y,cheight
  beq stringLoop$
  
  teq char,#'\t'
  addne x,cwidth
  bne stringLoop$
  
  add cwidth, cwidth,lsl #2
  x1 .req r1
  mov x1,x0
  
  stringLoopTab$:
    add x1,cwidth
    cmp x,x1
    bge stringLoopTab$
  mov x,x1
  .unreq x1
  b stringLoop$
stringLoopEnd$:
.unreq cwidth
.unreq cheight

pop {r4,r5,r6,r7,r8,r9,pc}
.unreq x
.unreq y
.unreq x0
.unreq string
.unreq length

这个代码中非常聪明地使用了一个新运算,subs 是从一个操作数中减去另一个数,保存结果,然后将结果与 0 进行比较。实现上,所有的比较都可以实现为减法后的结果与 0 进行比较,但是结果通常会丢弃。这意味着这个操作与 cmp 一样快。

subs reg,#val 从寄存器 reg 中减去 val,然后将结果与 0 进行比较。

4、你的意愿是我的命令行

现在,我们可以输出字符串了,而挑战是找到一个有意思的字符串去绘制。一般在这样的教程中,人们都希望去绘制 “Hello World!”,但是到目前为止,虽然我们已经能做到了,我觉得这有点“君临天下”的感觉(如果喜欢这种感觉,请随意!)。因此,作为替代,我们去继续绘制我们的命令行。

有一个限制是我们所做的操作系统是用在 ARM 架构的计算机上。最关键的是,在它们引导时,给它一些信息告诉它有哪些可用资源。几乎所有的处理器都有某些方式来确定这些信息,而在 ARM 上,它是通过位于地址 100 16 处的数据来确定的,这个数据的格式如下:

1. 数据是可分解的一系列的标签。
2. 这里有九种类型的标签:`core`、`mem`、`videotext`、`ramdisk`、`initrd2`、`serial`、`revision`、`videolfb`、`cmdline`。
3. 每个标签只能出现一次,除了 `core` 标签是必不可少的之外,其它的都是可有可无的。
4. 所有标签都依次放置在地址 `0x100` 处。
5. 标签列表的结束处总是有两个<ruby>字<rt>word</rt></ruby>,它们全为 0。
6. 每个标签的字节数都是 4 的倍数。
7. 每个标签都是以标签中(以字为单位)的标签大小开始(标签包含这个数字)。
8. 紧接着是包含标签编号的一个半字。编号是按上面列出的顺序,从 1 开始(`core` 是 1,`cmdline` 是 9)。
9. 紧接着是一个包含 5441<sub>16</sub> 的半字。
10. 之后是标签的数据,它根据标签不同是可变的。数据大小(以字为单位)+ 2 的和总是与前面提到的长度相同。
11. 一个 `core` 标签的长度可以是 2 个字也可以是 5 个字。如果是 2 个字,表示没有数据,如果是 5 个字,表示它有 3 个字的数据。
12. 一个 `mem` 标签总是 4 个字的长度。数据是内存块的第一个地址,和内存块的长度。
13. 一个 `cmdline` 标签包含一个 `null` 终止符字符串,它是个内核参数。

在目前的树莓派版本中,只提供了 corememcmdline 标签。你可以在后面找到它们的用法,更全面的参考资料在树莓派的参考页面上。现在,我们感兴趣的是 cmdline 标签,因为它包含一个字符串。我们继续写一些搜索这个命令行(cmdline)标签的代码,如果找到了,以每个条目一个新行的形式输出它。命令行只是图形处理器或用户认为操作系统应该知道的东西的一个列表。在树莓派上,这包含了 MAC 地址、序列号和屏幕分辨率。字符串本身也是一个由空格隔开的表达式(像 key.subkey=value 这样的)的列表。

几乎所有的操作系统都支持一个“命令行”的程序。它的想法是为选择一个程序所期望的行为而提供一个通用的机制。

我们从查找 cmdline 标签开始。将下列的代码复制到一个名为 tags.s 的新文件中。

.section .data
tag_core: .int 0
tag_mem: .int 0
tag_videotext: .int 0
tag_ramdisk: .int 0
tag_initrd2: .int 0
tag_serial: .int 0
tag_revision: .int 0
tag_videolfb: .int 0
tag_cmdline: .int 0

通过标签列表来查找是一个很慢的操作,因为这涉及到许多内存访问。因此,我们只想做一次。代码创建一些数据,用于保存每个类型的第一个标签的内存地址。接下来,用下面的伪代码就可以找到一个标签了。

function FindTag(r0 is tag)
  if tag > 9 or tag = 0 then return 0
  set tagAddr to loadWord(tag_core + (tag - 1) × 4)
  if not tagAddr = 0 then return tagAddr
  if readWord(tag_core) = 0 then return 0
  set tagAddr to 0x100
  loop forever
    set tagIndex to readHalfWord(tagAddr + 4)
    if tagIndex = 0 then return FindTag(tag)
    if readWord(tag_core+(tagIndex-1)×4) = 0
    then storeWord(tagAddr, tag_core+(tagIndex-1)×4)
    set tagAddr to tagAddr + loadWord(tagAddr) × 4
  end loop
end function

这段代码已经是优化过的,并且很接近汇编了。它尝试直接加载标签,第一次这样做是有些乐观的,但是除了第一次之外的其它所有情况都是可以这样做的。如果失败了,它将去检查 core 标签是否有地址。因为 core 标签是必不可少的,如果它没有地址,唯一可能的原因就是它不存在。如果它有地址,那就是我们没有找到我们要找的标签。如果没有找到,那我们就需要查找所有标签的地址。这是通过读取标签编号来做的。如果标签编号为 0,意味着已经到了标签列表的结束位置。这意味着我们已经查找了目录中所有的标签。所以,如果我们再次运行我们的函数,现在它应该能够给出一个答案。如果标签编号不为 0,我们检查这个标签类型是否已经有一个地址。如果没有,我们在目录中保存这个标签的地址。然后增加这个标签的长度(以字节为单位)到标签地址中,然后去查找下一个标签。

尝试去用汇编实现这段代码。你将需要简化它。如果被卡住了,下面是我的答案。不要忘了 .section .text

.section .text
.globl FindTag
FindTag:
tag .req r0
tagList .req r1
tagAddr .req r2

sub tag,#1
cmp tag,#8
movhi tag,#0
movhi pc,lr

ldr tagList,=tag_core
tagReturn$:
add tagAddr,tagList, tag,lsl #2
ldr tagAddr,[tagAddr]

teq tagAddr,#0
movne r0,tagAddr
movne pc,lr

ldr tagAddr,[tagList]
teq tagAddr,#0
movne r0,#0
movne pc,lr

mov tagAddr,#0x100
push {r4}
tagIndex .req r3
oldAddr .req r4
tagLoop$:
ldrh tagIndex,[tagAddr,#4]
subs tagIndex,#1
poplt {r4}
blt tagReturn$

add tagIndex,tagList, tagIndex,lsl #2
ldr oldAddr,[tagIndex]
teq oldAddr,#0
.unreq oldAddr
streq tagAddr,[tagIndex]

ldr tagIndex,[tagAddr]
add tagAddr, tagIndex,lsl #2
b tagLoop$

.unreq tag
.unreq tagList
.unreq tagAddr
.unreq tagIndex

5、Hello World

现在,我们已经万事俱备了,我们可以去绘制我们的第一个字符串了。在 main.s 文件中删除 bl SetGraphicsAddress 之后的所有代码,然后将下面的代码放进去:

mov r0,#9
bl FindTag
ldr r1,[r0]
lsl r1,#2
sub r1,#8
add r0,#8
mov r2,#0
mov r3,#0
bl DrawString
loop$:
b loop$

这段代码简单地使用了我们的 FindTag 方法去查找第 9 个标签(cmdline),然后计算它的长度,然后传递命令和长度给 DrawString 方法,告诉它在 0,0 处绘制字符串。现在可以在树莓派上测试它了。你应该会在屏幕上看到一行文本。如果没有,请查看我们的排错页面。

如果一切正常,恭喜你已经能够绘制文本了。但它还有很大的改进空间。如果想去写了一个数字,或内存的一部分,或操作我们的命令行,该怎么做呢?在 课程 9:屏幕04 中,我们将学习如何操作文本和显示有用的数字和信息。


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

作者:Alex Chadwick 选题:lujun9972 译者:qhwdw 校对:wxy

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