当你刚开始使用 Linux 时，你将发现与 Windows 的很多不同。其中一个“不同的东西”是 root 用户的概念。在这个初学者系列中，我将解释几个关于 Ubuntu 的 root 用户的重要的东西。请记住，尽管我正在从 Ubuntu 用户的角度编写这篇文章，它应该对大多数的 Linux 发行版也是有效的。你将在这篇文章中学到下面的内容：为什么在 Ubuntu 中禁用 root 用户像 root 用户一样使用命切换为 root 用户解锁 root 用户什么是 root 用户？为什么它在 Ubuntu 中被锁定？在 Linux 中，有一个称为 root 的超级用户。这是超级管理员账号，它可以做任何事以及使用系统的一切东西。它可以在你的 Linux 系统上访问任何文件和运行任何命令。能力越大，责任越大。root 用户给予你完全控制系统的能力，因此，它应该被谨慎地使用。root 用户可以访问系统文件，运行更改系统配置的命令。因此，一个错误的命令可能会破坏系统。这就是为什么 Ubuntu 和其它基于 Ubuntu 的发行版默认锁定 root 用户，以从意外的灾难中挽救你的原因。对于你的日常任务，像移动你家目录中的文件，从互联网下载文件，创建文档等等，你不需要拥有 root 权限。打个比方来更好地理解它。假设你想要切一个水果，你可以使用一把厨房用刀。假设你想要砍一颗树，你就得使用一把锯子。现在，你可以使用锯子来切水果，但是那不明智，不是吗？\_这意味着，你不能是 Ubuntu 中 root 用户或者不能使用 root 权限来使用系统吗？不，你仍然可以在 sudo 的帮助下来拥有 root 权限来访问(在下一节中解释)。要点： 使用于常规任务，root 用户权限太过强大。这就是为什么不建议一直使用 root 用户。你仍然可以使用 root 用户来运行特殊的命令。如何在 Ubuntu 中像 root 用户一样运行命令？对于一些系统的特殊任务来说，你将需要 root 权限。例如。如果你想通过命令行更新 Ubuntu，你不能作为一个常规用户运行该命令。它将给出权限被拒绝的错误。apt update Reading package lists... Done E: Could not open lock file /var/lib/apt/lists/lock - open (13: Permission denied) E: Unable to lock directory /var/lib/apt/lists/ W: Problem unlinking the file /var/cache/apt/pkgcache.bin - RemoveCaches (13: Permission denied) W: Problem unlinking the file /var/cache/apt/srcpkgcache.bin - RemoveCaches (13: Permission denied)那么，你如何像 root 用户一样运行命令？简单的答案是，在命令前添加 sudo，来像 root 用户一样运行。sudo apt updateUbuntu 和很多其它的 Linux 发行版使用一个被称为 sudo 的特殊程序机制。sudo 是一个以 root 用户（或其它用户）来控制运行命令访问的程序。实际上，sudo 是一个非常多用途的工具。它可以配置为允许一个用户像 root 用户一样来运行所有的命令，或者仅仅一些命令。你也可以配置为无需密码即可使用 sudo 运行命令。这个主题内容比较丰富，也许我将在另一篇文章中详细讨论它。就目前而言，你应该知道当你安装 Ubuntu 时，你必须创建一个用户账号。这个用户账号在你系统上以管理员身份来工作，并且按照 Ubuntu 中的默认 sudo 策略，它可以在你的系统上使用 root 用户权限来运行任何命令。sudo 的问题是，运行 sudo 不需要 root 用户密码，而是需要用户自己的密码。并且这就是为什么当你使用 sudo 运行一个命令，会要求输入正在运行 sudo 命令的用户的密码的原因：[email protected]:~$ sudo apt update [sudo] password for abhishek:正如你在上面示例中所见 abhishek 在尝试使用 sudo 来运行 apt update 命令，系统要求输入 abhishek 的密码。如果你对 Linux 完全不熟悉，当你在终端中开始输入密码时，你可能会惊讶，在屏幕上什么都没有发生。这是十分正常的，因为作为默认的安全功能，在屏幕上什么都不会显示。甚至星号（*）都没有。输入你的密码并按回车键。要点：为在 Ubuntu 中像 root 用户一样运行命令，在命令前添加 sudo。 当被要求输入密码时，输入你的账户的密码。当你在屏幕上输入密码时，什么都看不到。请继续输入密码，并按回车键。如何在 Ubuntu 中成为 root 用户？你可以使用 sudo 来像 root 用户一样运行命令。但是，在某些情况下，你必须以 root 用户身份来运行一些命令，而你总是忘了在命令前添加 sudo，那么你可以临时切换为 root 用户。sudo 命令允许你来模拟一个 root 用户登录的 shell ，使用这个命令：sudo -i[email protected]:~$ sudo -i [sudo] password for abhishek: [email protected]:~# whoami root 你将注意到，当你切换为 root 用户时，shell 命令提示符从 $（美元符号）更改为 #（英镑符号）。我开个（拙劣的）玩笑，英镑比美元强大。虽然我已经向你显示如何成为 root 用户，但是我必须警告你，你应该避免作为 root 用户使用系统。毕竟它有阻拦你使用 root 用户的原因。另外一种临时切换为 root 用户的方法是使用 su 命令：sudo su如果你尝试使用不带有的 sudo 的 su 命令，你将遇到 “su authentication failure” 错误。你可以使用 exit 命令来恢复为正常用户。exit如何在 Ubuntu 中启用 root 用户？现在你知道，root 用户在基于 Ubuntu 发行版中是默认锁定的。Linux 给予你在系统上想做什么就做什么的自由。解锁 root 用户就是这些自由之一。如果出于某些原因，你决定启用 root 用户，你可以通过为其设置一个密码来做到：sudo passwd root再强调一次，不建议使用 root 用户，并且我也不鼓励你在桌面上这样做。如果你忘记了密码，你将不能再次在 Ubuntu 中更改 root 用户密码。（LCTT 译注：可以通过单用户模式修改。）你可以通过移除密码来再次锁定 root 用户：sudo passwd -dl root最后…我希望你现在对 root 概念理解得更好一点。如果你仍然有些关于它的困惑和问题，请在评论中让我知道。我将尝试回答你的问题，并且也可能更新这篇文章。via: https://itsfoss.com/root-user-ubuntu/作者：Abhishek Prakash 选题：lujun9972 译者：robsean 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
开始使用开源待办事项清单管理器
待办事项清单是跟踪任务列表的强大方法。在我们的 20 个使用开源提升生产力的系列的第七篇文章中了解如何使用它。去年，我在 19 天里给你介绍了 19 个新（对你而言）的生产力工具。今年，我换了一种方式：使用你在使用或者还没使用的工具，构建一个使你可以在新一年更加高效的环境。使用 todo 跟踪任务任务管理和待办事项清单是我非常喜欢0的东西。我是一位生产效率的狂热粉丝（以至于我为此做了一个播客），我尝试了各种不同的应用。我甚至为此做了演讲并写了些文章。因此，当我谈到提高工作效率时，肯定会出现任务管理和待办事项清单工具。说实话，由于简单、跨平台且易于同步，用 todo.txt 肯定不会错。它是我不断反复提到的两个待办事项清单以及任务管理应用之一（另一个是 Org 模式）。让我反复使用它的原因是它简单、可移植、易于理解，并且有许多很好的附加组件，并且当一台机器有附加组件，而另一台没有，也不会破坏它。由于它是一个 Bash shell 脚本，我还没发现一个无法支持它的系统。设置 todo.txt首先，你需要安装基本 shell 脚本并将默认配置文件复制到 ~/.todo 目录：git clone https://github.com/todotxt/todo.txt-cli.git cd todo.txt-cli make sudo make install mkdir ~/.todo cp todo.cfg ~/.todo/config接下来，设置配置文件。一般，我想取消对颜色设置的注释，但必须马上设置的是 TODO_DIR 变量：export TODO_DIR="$HOME/.todo"添加待办事件要添加第一个待办事件，只需输入 todo.sh add <NewTodo> 就能添加。这还将在 $HOME/.todo/ 中创建三个文件：todo.txt、done.txt 和 reports.txt。添加几个项目后，运行 todo.sh ls 查看你的待办事项。管理任务你可以通过给项目设置优先级来稍微改善它。要向项目添加优先级，运行 todo.sh pri # A。数字是列表中任务的数量，而字母 A 是优先级。你可以将优先级设置为从 A 到 Z，因为这是它的排序方式。要完成任务，运行 todo.sh do # 来标记项目已完成并将它移动到 done.txt。运行 todo.sh report 会向 report.txt 写入已完成和未完成项的数量。所有这三个文件的格式都有详细的说明，因此你可以使用你的文本编辑器修改。todo.txt 的基本格式是：(Priority) YYYY-MM-DD Task该日期表示任务的到期日期（如果已设置）。手动编辑文件时，只需在任务前面加一个 x 来标记为已完成。运行 todo.sh archive 会将这些项目移动到 done.txt，你可以编辑该文本文件，并在有时间时将已完成的项目归档。设置重复任务我有很多重复的任务，我需要以每天/周/月来计划。这就是 todo.txt 的灵活性所在。通过在 ~/.todo.actions.d/ 中使用附加组件，你可以添加命令并扩展基本 todo.sh 的功能。附加组件基本上是实现特定命令的脚本。对于重复执行的任务，插件 ice\_recur 应该符合要求。按照其页面上的说明操作，你可以设置任务以非常灵活的方式重复执行。在该附加组件目录中有很多附加组件，包括同步到某些云服务，也有链接到桌面或移动端应用的组件，这样你可以随时看到待办列表。我只是简单介绍了这个代办事项清单功能，请花点时间深入了解这个工具的强大！它确实可以帮助我每天完成任务。via: https://opensource.com/article/20/1/open-source-to-do-list作者：Kevin Sonney 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
通过将联系人同步到本地从而更快访问它。在我们的 20 个使用开源提升生产力的系列的第六篇文章中了解该如何做。去年，我在 19 天里给你介绍了 19 个新（对你而言）的生产力工具。今年，我换了一种方式：使用你在使用或者还没使用的工具，构建一个使你可以在新一年更加高效的环境。用于联系人管理的开源工具在本系列之前的文章中，我解释了如何在本地同步你的邮件和日历。希望这些加速了你访问邮件和日历。现在，我将讨论联系人同步，你可以给他们发送邮件和日历邀请。我目前收集了很多邮件地址。管理这些数据可能有点麻烦。有基于 Web 的服务，但它们不如本地副本快。几天前，我谈到了用于管理日历的 vdirsyncer。vdirsyncer 还使用 CardDAV 协议处理联系人。vdirsyncer 除了可以使用文件系统存储日历外，还支持通过 google\_contacts 和 carddav 进行联系人同步，但 fileext 设置会被更改，因此你无法在日历文件中存储联系人。我在配置文件添加了一块配置，并从 Google 镜像了我的联系人。设置它需要额外的步骤。从 Google 镜像完成后，配置非常简单：[pair address_sync] a = "googlecard" b = "localcard" collections = ["from a", "from b"] conflict_resolution = "a wins" [storage googlecard] type = "google_contacts" token_file = "~/.vdirsyncer/google_token" client_id = "my_client_id" client_secret = "my_client_secret" [storage localcard] type = "filesystem" path = "~/.calendars/Addresses/" fileext = ".vcf"现在，当我运行 vdirsyncer discover 时，它会找到我的 Google 联系人，并且 vdirsyncer sync 将它们复制到我的本地计算机。但同样，这只进行到一半。现在我想查看和使用联系人。需要 khard 和 abook。为什么选择两个应用？因为每个都有它自己的使用场景，在这里，越多越好。khard 用于管理地址，类似于 khal 用于管理日历条目。如果你的发行版附带了旧版本，你可能需要通过 pip 安装最新版本。安装 khard 后，你需要创建 ~/.config/khard/khard.conf，因为 khard 没有与 khal 那样漂亮的配置向导。我的看起来像这样：[addressbooks] addresses path = ~/.calendars/Addresses/default/ [general] debug = no default_action = list editor = vim, -i, NONE merge_editor = vimdiff [contact table] display = first_name group_by_addressbook = no reverse = no show_nicknames = yes show_uids = no sort = last_name localize_dates = yes [vcard] preferred_version = 3.0 search_in_source_files = yes skip_unparsable = no这会定义源通讯簿（并给它一个友好的名称）、显示内容和联系人编辑程序。运行 khard list 将列出所有条目，khard list <some@email.adr> 可以搜索特定条目。如果要添加或编辑条目，add 和 edit 命令将使用相同的基本模板打开配置的编辑器，唯一的区别是 add 命令的模板将为空。abook 需要你导入和导出 VCF 文件，但它为查找提供了一些不错的功能。要将文件转换为 abook 格式，请先安装 abook 并创建 ~/.abook 默认目录。然后让 abook 解析所有文件，并将它们放入 ~/.abook/addresses 文件中：apt install abook ls ~/.calendars/Addresses/default/* | xargs cat | abook --convert --informat vcard --outformat abook > ~/.abook/addresses现在运行 abook，你将有一个非常漂亮的 UI 来浏览、搜索和编辑条目。将它们导出到单个文件有点痛苦，所以我用 khard 进行大部分编辑，并有一个 cron 任务将它们导入到 abook 中。abook 还可在命令行中搜索，并有大量有关将其与邮件客户端集成的文档。例如，你可以在 .config/alot/config 文件中添加一些信息，从而在 Nmuch 的邮件客户端 alot 中使用 abook 查询联系人：[accounts] Personal realname = Kevin Sonney address = kevin@sonney.com alias_regexp = kevin\+.+@sonney.com gpg_key = 7BB612C9 sendmail_command = msmtp --account=Personal -t # ~ expansion works sent_box = maildir://~/Maildir/Sent draft_box = maildir://~/Maildir/Drafts [abook] type = abook这样你就可以在邮件和日历中快速查找联系人了！via: https://opensource.com/article/20/1/sync-contacts-locally作者：Kevin Sonney 选题：lujun9972 译者：geekpi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这本免费的电子书使经验丰富的程序员更深入了解 Linux 中进程间通信（IPC）的核心概念和机制。让一个软件过程与另一个软件过程进行对话是一个微妙的平衡行为。但是，它对于应用程序而言可能是至关重要的功能，因此这是任何从事复杂项目的程序员都必须解决的问题。你的应用程序是否需要启动由其它软件处理的工作；监视外设或网络上正在执行的操作；或者检测来自其它来源的信号，当你的软件需要依赖其自身代码之外的东西来知道下一步做什么或什么时候做时，你就需要考虑 进程间通信 inter-process communication （IPC）。这在 Unix 操作系统上已经由来已久了，这可能是因为人们早期预期软件会来自各种来源。按照相同的传统，Linux 提供了一些同样的 IPC 接口和一些新接口。Linux 内核具有多种 IPC 方法，util-linux 包包含了 ipcmk、ipcrm、ipcs 和 lsipc 命令，用于监视和管理 IPC 消息。显示进程间通信信息在尝试 IPC 之前，你应该知道系统上已经有哪些 IPC 设施。lsipc 命令提供了该信息。RESOURCE DESCRIPTION LIMIT USED USE% MSGMNI Number of message queues 32000 0 0.00% MSGMAX Max size of message (byt.. 8192 - - MSGMNB Default max size of queue 16384 - - SHMMNI Shared memory segments 4096 79 1.93% SHMALL Shared memory pages 184[...] 25452 0.00% SHMMAX Max size of shared memory 18446744073692774399 SHMMIN Min size of shared memory 1 - - SEMMNI Number of semaphore ident 32000 0 0.00% SEMMNS Total number of semaphore 1024000.. 0 0.00% SEMMSL Max semaphores per semap 32000 - - SEMOPM Max number of operations p 500 - - SEMVMX Semaphore max value 32767 - -你可能注意到，这个示例清单包含三种不同类型的 IPC 机制，每种机制在 Linux 内核中都是可用的：消息（MSG）、共享内存（SHM）和信号量（SEM）。你可以用 ipcs 命令查看每个子系统的当前活动：$ ipcs ------ Message Queues Creators/Owners --- msqid perms cuid cgid [...] ------ Shared Memory Segment Creators/Owners shmid perms cuid cgid [...] 557056 700 seth users [...] 3571713 700 seth users [...] 2654210 600 seth users [...] 2457603 700 seth users [...] ------ Semaphore Arrays Creators/Owners --- semid perms cuid cgid [...]这表明当前没有消息或信号量阵列，但是使用了一些共享内存段。你可以在系统上执行一个简单的示例，这样就可以看到正在工作的系统之一。它涉及到一些 C 代码，所以你必须在系统上有构建工具。必须安装这些软件包才能从源代码构建软件，这些软件包的名称取决于发行版，因此请参考文档以获取详细信息。例如，在基于 Debian 的发行版上，你可以在 wiki 的构建教程部分了解构建需求，而在基于 Fedora 的发行版上，你可以参考该文档的从源代码安装软件部分。创建一个消息队列你的系统已经有一个默认的消息队列，但是你可以使用 ipcmk 命令创建你自己的消息队列：$ ipcmk --queue Message queue id: 32764编写一个简单的 IPC 消息发送器，为了简单，在队列 ID 中硬编码：#include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> #include <string.h> struct msgbuffer { char text[24]; } message; int main() { int msqid = 32764; strcpy(message.text,"opensource.com"); msgsnd(msqid, &message, sizeof(message), 0); printf("Message: %s\n",message.text); printf("Queue: %d\n",msqid); return 0; }编译该应用程序并运行：$ gcc msgsend.c -o msg.bin $ ./msg.bin Message: opensource.com Queue: 32769你刚刚向你的消息队列发送了一条消息。你可以使用 ipcs 命令验证这一点，可以使用 ——queue 选项将输出限制到该消息队列：$ ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 0x7b341ab9 0 seth 666 0 0 0x72bd8410 32764 seth 644 24 1你也可以检索这些消息：#include <sys/ipc.h> #include <sys/msg.h> #include <stdio.h> struct msgbuffer { char text[24]; } message; int main() { int msqid = 32764; msgrcv(msqid, &message, sizeof(message),0,0); printf("\nQueue: %d\n",msqid); printf("Got this message: %s\n", message.text); msgctl(msqid,IPC_RMID,NULL); return 0;编译并运行：$ gcc get.c -o get.bin $ ./get.bin Queue: 32764 Got this message: opensource.com下载这本电子书这只是 Marty Kalin 的《Linux 进程间通信指南》中课程的一个例子，可从 Opensource.com 下载的这本最新免费（且 CC 授权）的电子书。在短短的几节课中，你将从消息队列、共享内存和信号量、套接字、信号等中了解 IPC 的 POSIX 方法。认真阅读 Marty 的书，你将成为一个博识的程序员。而这不仅适用于经验丰富的编码人员，如果你编写的只是 shell 脚本，那么你将拥有有关管道（命名和未命名）和共享文件的大量实践知识，以及使用共享文件或外部消息队列时需要了解的重要概念。如果你对制作具有动态和具有系统感知的优秀软件感兴趣，那么你需要了解 IPC。让这本书做你的向导。via: https://opensource.com/article/20/1/inter-process-communication-linux作者：Seth Kenlon 选题：lujun9972 译者：laingke 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这是一个关于我如何在 WiFi 时代之前学习 Linux 的故事，那时的发行版还以 CD 的形式出现。大部分人可能不记得 1996 年时计算产业或日常生活世界的样子。但我很清楚地记得那一年。我那时候是堪萨斯中部一所高中的二年级学生，那是我的自由与开源软件（FOSS）旅程的开端。我从这里开始进步。我在 1996 年之前就开始对计算机感兴趣。我在我家的第一台 Apple ][e 上启蒙成长，然后多年之后是 IBM Personal System/2。（是的，在这过程中有一些代际的跨越。）IBM PS/2 有一个非常激动人心的特性：一个 1200 波特的 Hayes 调制解调器。我不记得是怎样了，但在那不久之前，我得到了一个本地 BBS 的电话号码。一旦我拨号进去，我可以得到本地的一些其他 BBS 的列表，我的网络探险就此开始了。在 1995 年，足够幸运的人拥有了家庭互联网连接，每月可以使用不到 30 分钟。那时的互联网不像我们现代的服务那样，通过卫星、光纤、有线电视同轴电缆或任何版本的铜线提供。大多数家庭通过一个调制解调器拨号，它连接到他们的电话线上。（这时离移动电话无处不在的时代还早得很，大多数人只有一部家庭电话。）尽管这还要取决你所在的位置，但我不认为那时有很多独立的互联网服务提供商（ISP），所以大多数人从仅有的几家大公司获得服务，包括 America Online，CompuServe 以及 Prodigy。你能获取到的服务速率非常低，甚至在拨号上网革命性地达到了顶峰的 56K，你也只能期望得到最高 3.5Kbps 的速率。如果你想要尝试 Linux，下载一个 200MB 到 800MB 的 ISO 镜像或（更加切合实际的）一套软盘镜像要贡献出时间、决心，以及减少电话的使用。我走了一条简单一点的路：在 1996 年，我从一家主要的 Linux 发行商订购了一套 “tri-Linux” CD 集。这些光盘提供了三个发行版，我的这套包含了 Debian 1.1（Debian 的第一个稳定版本）、Red Hat Linux 3.0.3 以及 Slackware 3.1（代号 Slackware ‘96）。据我回忆，这些光盘是从一家叫做 Linux Systems Labs 的在线商店购买的。这家在线商店如今已经不存在了，但在 90 年代和 00 年代早期，这样的发行商很常见。这些是多光盘 Linux 套件。这是 1998 年的一套光盘，你可以了解到他们都包含了什么：在 1996 年夏天一个命中注定般的日子，那时我住在堪萨斯一个新的并且相对较为乡村的城市，我做出了安装并使用 Linux 的第一次尝试。在 1996 年的整个夏天，我尝试了那套三张 Linux CD 套件里的全部三个发行版。他们都在我母亲的老 Pentium 75MHz 电脑上完美运行。我最终选择了 Slackware 3.1 作为我的首选发行版，相比其它发行版可能更多的是因为它的终端的外观，这是决定选择一个发行版前需要考虑的重要因素。我将系统设置完毕并运行了起来。我连接到一家 “不太知名的” ISP（一家这个区域的本地服务商），通过我家的第二条电话线拨号（为了满足我的所有互联网使用而订购）。那就像在天堂一样。我有一台完美运行的双系统（Microsoft Windows 95 和 Slackware 3.1）电脑。我依然拨号进入我所知道和喜爱的 BBS，游玩在线 BBS 游戏，比如 Trade Wars、Usurper 以及 Legend of the Red Dragon。我能够记得在 EFNet（IRC）上 #Linux 频道上渡过的日子，帮助其他用户，回答他们的 Linux 问题以及和版主们互动。在我第一次在家尝试使用 Linux 系统的 20 多年后，已经是我进入作为 Red Hat 顾问的第五年，我仍然在使用 Linux（现在是 Fedora）作为我的日常系统，并且依然在 IRC 上帮助想要使用 Linux 的人们。via: https://opensource.com/article/19/11/learning-linux-90s作者：Mike Harris 选题：lujun9972 译者：alim0x 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
使用公钥/私钥对设定免密的 Linux 登录方式
使用一组公钥/私钥对让你不需要密码登录到远程 Linux 系统或使用 ssh 运行命令，这会非常方便，但是设置过程有点复杂。下面是帮助你的方法和脚本。在 Linux 系统上设置一个允许你无需密码即可远程登录或运行命令的帐户并不难，但是要使它正常工作，你还需要掌握一些繁琐的细节。在本文，我们将完成整个过程，然后给出一个可以帮助处理琐碎细节的脚本。设置好之后，如果希望在脚本中运行 ssh 命令，尤其是希望配置自动运行的命令，那么免密访问特别有用。需要注意的是，你不需要在两个系统上使用相同的用户帐户。实际上，你可以把公用密钥用于系统上的多个帐户或多个系统上的不同帐户。设置方法如下。在哪个系统上启动？首先，你需要从要发出命令的系统上着手。那就是你用来创建 ssh 密钥的系统。你还需要可以访问远程系统上的帐户并在其上运行这些命令。为了使角色清晰明了，我们将场景中的第一个系统称为 “boss”，因为它将发出要在另一个系统上运行的命令。因此，命令提示符如下：boss$如果你还没有在 boss 系统上为你的帐户设置公钥/私钥对，请使用如下所示的命令创建一个密钥对。注意，你可以在各种加密算法之间进行选择。（一般使用 RSA 或 DSA。）注意，要在不输入密码的情况下访问系统，你需要在下面的对话框中的两个提示符出不输入密码。如果你已经有一个与此帐户关联的公钥/私钥对，请跳过此步骤。boss$ ssh-keygen -t rsa Generating public/private rsa key pair. Enter file in which to save the key (/home/myself/.ssh/id_rsa): Enter passphrase (empty for no passphrase): <== 按下回车键即可 Enter same passphrase again: <== 按下回车键即可 Your identification has been saved in /home/myself/.ssh/id_rsa. Your public key has been saved in /home/myself/.ssh/id_rsa.pub. The key fingerprint is: SHA256:1zz6pZcMjA1av8iyojqo6NVYgTl1+cc+N43kIwGKOUI myself@boss The key's randomart image is: +---[RSA 3072]----+ | . .. | | E+ .. . | | .+ .o + o | | ..+.. .o* . | | ... So+*B o | | + ...==B . | | . o . ....++. | |o o . . o..o+ | |=..o.. ..o o. | +----[SHA256]-----+上面显示的命令将创建公钥和私钥。其中公钥用于加密，私钥用于解密。因此，这些密钥之间的关系是关键的，私有密钥绝不应该被共享。相反，它应该保存在 boss 系统的 .ssh 文件夹中。注意，在创建时，你的公钥和私钥将会保存在 .ssh 文件夹中。下一步是将公钥复制到你希望从 boss 系统免密访问的系统。你可以使用 scp 命令来完成此操作，但此时你仍然需要输入密码。在本例中，该系统称为 “target”。boss$ scp .ssh/id_rsa.pub myacct@target:/home/myaccount myacct@target's password:你需要安装公钥在 target 系统（将运行命令的系统）上。如果你没有 .ssh 目录（例如，你从未在该系统上使用过 ssh），运行这样的命令将为你设置一个目录：target$ ssh localhost date target$ ls -la .ssh total 12 drwx------ 2 myacct myacct 4096 Jan 19 11:48 . drwxr-xr-x 6 myacct myacct 4096 Jan 19 11:49 .. -rw-r--r-- 1 myacct myacct 222 Jan 19 11:48 known_hosts仍然在目标系统上，你需要将从“boss”系统传输的公钥添加到 .ssh/authorized_keys 文件中。如果该文件已经存在，使用下面的命令将把它添加到文件的末尾；如果文件不存在，则创建该文件并添加密钥。target$ cat id_rsa.pub >> .ssh/authorized_keys下一步，你需要确保你的 authorized_keys 文件权限为 600。如果还不是，执行命令 chmod 600 .ssh/authorized_keys。target$ ls -l authorized_keys -rw------- 1 myself myself 569 Jan 19 12:10 authorized_keys还要检查目标系统上 .ssh 目录的权限是否设置为 700。如果需要，执行 chmod 700 .ssh 命令修改权限。target$ ls -ld .ssh drwx------ 2 myacct myacct 4096 Jan 14 15:54 .ssh此时，你应该能够从 boss 系统远程免密运行命令到目标系统。除非目标系统上的目标用户帐户拥有与你试图连接的用户和主机相同的旧公钥，否则这应该可以工作。如果是这样，你应该删除早期的（并冲突的）条目。使用脚本使用脚本可以使某些工作变得更加容易。但是，在下面的示例脚本中，你会遇到的一个烦人的问题是，在配置免密访问权限之前，你必须多次输入目标用户的密码。一种选择是将脚本分为两部分——需要在 boss 系统上运行的命令和需要在 target 系统上运行的命令。这是“一步到位”版本的脚本：#!/bin/bash # NOTE: This script requires that you have the password for the remote acct # in order to set up password-free access using your public key LOC=`hostname` # the local system from which you want to run commands from # wo a password # get target system and account echo -n "target system> " read REM echo -n "target user> " read user # create a key pair if no public key exists if [ ! -f ~/.ssh/id_rsa.pub ]; then ssh-keygen -t rsa fi # ensure a .ssh directory exists in the remote account echo checking for .ssh directory on remote system ssh $user@$REM "if [ ! -d /home/$user/.ssh ]; then mkdir /home/$user/.ssh; fi" # share the public key (using local hostname) echo copying the public key scp ~/.ssh/id_rsa.pub $user@$REM:/home/$user/$user-$LOC.pub # put the public key into the proper location echo adding key to authorized_keys ssh $user@$REM "cat /home/$user/$user-$LOC.pub >> /home/$user/.ssh/authorized_ke ys" # set permissions on authorized_keys and .ssh (might be OK already) echo setting permissions ssh $user@$REM "chmod 600 ~/.ssh/authorized_keys" ssh $user@$REM "chmod 700 ~/.ssh" # try it out -- should NOT ask for a password echo testing -- if no password is requested, you are all set ssh $user@$REM /bin/hostname脚本已经配置为在你每次必须输入密码时告诉你它正在做什么。交互看起来是这样的:$ ./rem_login_setup target system> fruitfly target user> lola checking for .ssh directory on remote system lola@fruitfly's password: copying the public key lola@fruitfly's password: id_rsa.pub 100% 567 219.1KB/s 00:00 adding key to authorized_keys lola@fruitfly's password: setting permissions lola@fruitfly's password: testing -- if no password is requested, you are all set fruitfly在上面的场景之后，你就可以像这样登录到 lola 的帐户：$ ssh lola@fruitfly [lola@fruitfly ~]$一旦设置了免密登录，你就可以不需要键入密码从 boss 系统登录到 target 系统，并且运行任意的 ssh 命令。以这种免密的方式运行并不意味着你的帐户不安全。然而，根据 target 系统的性质，保护你在 boss 系统上的密码可能变得更加重要。via: https://www.networkworld.com/article/3514607/setting-up-passwordless-linux-logins-using-publicprivate-keys.html作者：Sandra Henry-Stocker 选题：lujun9972 译者：laingke 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
这部分是关于在使用 Python 的 Pygame 模块开发的视频游戏总给你的玩家提供收集的宝物和经验值的内容。这是正在进行的关于使用 Python 3 的 Pygame 模块创建视频游戏的系列文章的第十部分。以前的文章有:通过构建一个简单的掷骰子游戏去学习怎么用 Python 编程使用 Python 和 Pygame 模块构建一个游戏框架如何在你的 Python 游戏中添加一个玩家用 Pygame 使你的游戏角色移动起来如何向你的 Python 游戏中添加一个敌人在 Pygame 游戏中放置平台在你的 Python 游戏中模拟引力为你的 Python 平台类游戏添加跳跃功能使你的 Python 游戏玩家能够向前和向后跑如果你已经阅读了本系列的前几篇文章，那么你已经了解了编写游戏的所有基础知识。现在你可以在这些基础上，创造一个全功能的游戏。当你第一次学习时，遵循本系列代码示例，这样的“用例”是有帮助的，但是，用例也会约束你。现在是时候运用你学到的知识，以新的方式应用它们了。如果说，说起来容易做起来难，这篇文章展示了一个如何将你已经了解的内容用于新目的的例子中。具体来说，就是它涵盖了如何使用你以前的课程中已经了解到的来实现奖励系统。在大多数电子游戏中，你有机会在游戏世界中获得“奖励”或收集到宝物和其他物品。奖励通常会增加你的分数或者你的生命值，或者为你的下一次任务提供信息。游戏中包含的奖励类似于编程平台。像平台一样，奖励没有用户控制，随着游戏世界的滚动进行，并且必须检查与玩家的碰撞。创建奖励函数奖励和平台非常相似，你甚至不需要一个奖励的类。你可以重用 Platform 类，并将结果称为“奖励”。由于奖励类型和位置可能因关卡不同而不同，如果你还没有，请在你的 Level 中创建一个名为 loot 的新函数。因为奖励物品不是平台，你也必须创建一个新的 loot_list 组，然后添加奖励物品。与平台、地面和敌人一样，该组用于检查玩家碰撞: def loot(lvl,lloc): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(300,ty*7,tx,ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list你可以随意添加任意数量的奖励对象；记住把每一个都加到你的奖励清单上。Platform 类的参数是奖励图标的 X 位置、Y 位置、宽度和高度（通常让你的奖励精灵保持和所有其他方块一样的大小最为简单），以及你想要用作的奖励的图片。奖励的放置可以和贴图平台一样复杂，所以使用创建关卡时需要的关卡设计文档。在脚本的设置部分调用新的奖励函数。在下面的代码中，前三行是上下文，所以只需添加第四行:enemy_list = Level.bad( 1, eloc ) ground_list = Level.ground( 1,gloc,tx,ty ) plat_list = Level.platform( 1,tx,ty ) loot_list = Level.loot(1,tx,ty)正如你现在所知道的，除非你把它包含在你的主循环中，否则奖励不会被显示到屏幕上。将下面代码示例的最后一行添加到循环中: enemy_list.draw(world) ground_list.draw(world) plat_list.draw(world) loot_list.draw(world)启动你的游戏看看会发生什么。你的奖励将会显示出来，但是当你的玩家碰到它们时，它们不会做任何事情，当你的玩家经过它们时，它们也不会滚动。接下来解决这些问题。滚动奖励像平台一样，当玩家在游戏世界中移动时，奖励必须滚动。逻辑与平台滚动相同。要向前滚动奖励物品，添加最后两行: for e in enemy_list: e.rect.x -= scroll for l in loot_list: l.rect.x -= scroll要向后滚动，请添加最后两行： for e in enemy_list: e.rect.x += scroll for l in loot_list: l.rect.x += scroll再次启动你的游戏，看看你的奖励物品现在表现得像在游戏世界里一样了，而不是仅仅画在上面。检测碰撞就像平台和敌人一样，你可以检查奖励物品和玩家之间的碰撞。逻辑与其他碰撞相同，除了撞击不会（必然）影响重力或生命值。取而代之的是，命中会导致奖励物品会消失并增加玩家的分数。当你的玩家触摸到一个奖励对象时，你可以从 loot_list 中移除该对象。这意味着当你的主循环在 loot_list 中重绘所有奖励物品时，它不会重绘那个特定的对象，所以看起来玩家已经获得了奖励物品。在 Player 类的 update 函数中的平台碰撞检测之上添加以下代码（最后一行仅用于上下文）： loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False)当碰撞发生时，你不仅要把奖励从它的组中移除，还要给你的玩家一个分数提升。你还没有创建分数变量，所以请将它添加到你的玩家属性中，该属性是在 Player 类的 __init__ 函数中创建的。在下面的代码中，前两行是上下文，所以只需添加分数变量: self.frame = 0 self.health = 10 self.score = 0当在主循环中调用 update 函数时，需要包括 loot_list： player.gravity() player.update()如你所见，你已经掌握了所有的基本知识。你现在要做的就是用新的方式使用你所知道的。在下一篇文章中还有一些提示，但是与此同时，用你学到的知识来制作一些简单的单关卡游戏。限制你试图创造的东西的范围是很重要的，这样你就不会埋没自己。这也使得最终的成品看起来和感觉上更容易完成。以下是迄今为止你为这个 Python 平台编写的所有代码:#!/usr/bin/env python3 # draw a world # add a player and player control # add player movement # add enemy and basic collision # add platform # add gravity # add jumping # add scrolling # GNU All-Permissive License # Copying and distribution of this file, with or without modification, # are permitted in any medium without royalty provided the copyright # notice and this notice are preserved. This file is offered as-is, # without any warranty. import pygame import sys import os ''' Objects ''' class Platform(pygame.sprite.Sprite): # x location, y location, img width, img height, img file def __init__(self,xloc,yloc,imgw,imgh,img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images',img)).convert() self.image.convert_alpha() self.rect = self.image.get_rect() self.rect.y = yloc self.rect.x = xloc class Player(pygame.sprite.Sprite): ''' Spawn a player ''' def __init__(self): pygame.sprite.Sprite.__init__(self) self.movex = 0 self.movey = 0 self.frame = 0 self.health = 10 self.collide_delta = 0 self.jump_delta = 6 self.score = 1 self.images = [] for i in range(1,9): img = pygame.image.load(os.path.join('images','hero' + str(i) + '.png')).convert() img.convert_alpha() img.set_colorkey(ALPHA) self.images.append(img) self.image = self.images[0] self.rect = self.image.get_rect() def jump(self,platform_list): self.jump_delta = 0 def gravity(self): self.movey += 3.2 # how fast player falls if self.rect.y > worldy and self.movey >= 0: self.movey = 0 self.rect.y = worldy-ty def control(self,x,y): ''' control player movement ''' self.movex += x self.movey += y def update(self): ''' Update sprite position ''' self.rect.x = self.rect.x + self.movex self.rect.y = self.rect.y + self.movey # moving left if self.movex < 0: self.frame += 1 if self.frame > ani*3: self.frame = 0 self.image = self.images[self.frame//ani] # moving right if self.movex > 0: self.frame += 1 if self.frame > ani*3: self.frame = 0 self.image = self.images[(self.frame//ani)+4] # collisions enemy_hit_list = pygame.sprite.spritecollide(self, enemy_list, False) for enemy in enemy_hit_list: self.health -= 1 #print(self.health) loot_hit_list = pygame.sprite.spritecollide(self, loot_list, False) for loot in loot_hit_list: loot_list.remove(loot) self.score += 1 print(self.score) plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.collide_delta = 0 # stop jumping self.movey = 0 if self.rect.y > p.rect.y: self.rect.y = p.rect.y+ty else: self.rect.y = p.rect.y-ty ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.movey = 0 self.rect.y = worldy-ty-ty self.collide_delta = 0 # stop jumping if self.rect.y > g.rect.y: self.health -=1 print(self.health) if self.collide_delta < 6 and self.jump_delta < 6: self.jump_delta = 6*2 self.movey -= 33 # how high to jump self.collide_delta += 6 self.jump_delta += 6 class Enemy(pygame.sprite.Sprite): ''' Spawn an enemy ''' def __init__(self,x,y,img): pygame.sprite.Sprite.__init__(self) self.image = pygame.image.load(os.path.join('images',img)) self.movey = 0 #self.image.convert_alpha() #self.image.set_colorkey(ALPHA) self.rect = self.image.get_rect() self.rect.x = x self.rect.y = y self.counter = 0 def move(self): ''' enemy movement ''' distance = 80 speed = 8 self.movey += 3.2 if self.counter >= 0 and self.counter <= distance: self.rect.x += speed elif self.counter >= distance and self.counter <= distance*2: self.rect.x -= speed else: self.counter = 0 self.counter += 1 if not self.rect.y >= worldy-ty-ty: self.rect.y += self.movey plat_hit_list = pygame.sprite.spritecollide(self, plat_list, False) for p in plat_hit_list: self.movey = 0 if self.rect.y > p.rect.y: self.rect.y = p.rect.y+ty else: self.rect.y = p.rect.y-ty ground_hit_list = pygame.sprite.spritecollide(self, ground_list, False) for g in ground_hit_list: self.rect.y = worldy-ty-ty class Level(): def bad(lvl,eloc): if lvl == 1: enemy = Enemy(eloc[0],eloc[1],'yeti.png') # spawn enemy enemy_list = pygame.sprite.Group() # create enemy group enemy_list.add(enemy) # add enemy to group if lvl == 2: print("Level " + str(lvl) ) return enemy_list def loot(lvl,tx,ty): if lvl == 1: loot_list = pygame.sprite.Group() loot = Platform(200,ty*7,tx,ty, 'loot_1.png') loot_list.add(loot) if lvl == 2: print(lvl) return loot_list def ground(lvl,gloc,tx,ty): ground_list = pygame.sprite.Group() i=0 if lvl == 1: while i < len(gloc): ground = Platform(gloc[i],worldy-ty,tx,ty,'ground.png') ground_list.add(ground) i=i+1 if lvl == 2: print("Level " + str(lvl) ) return ground_list def platform(lvl,tx,ty): plat_list = pygame.sprite.Group() ploc = [] i=0 if lvl == 1: ploc.append((20,worldy-ty-128,3)) ploc.append((300,worldy-ty-256,3)) ploc.append((500,worldy-ty-128,4)) while i < len(ploc): j=0 while j <= ploc[i][2]: plat = Platform((ploc[i][0]+(j*tx)),ploc[i][1],tx,ty,'ground.png') plat_list.add(plat) j=j+1 print('run' + str(i) + str(ploc[i])) i=i+1 if lvl == 2: print("Level " + str(lvl) ) return plat_list ''' Setup ''' worldx = 960 worldy = 720 fps = 40 # frame rate ani = 4 # animation cycles clock = pygame.time.Clock() pygame.init() main = True BLUE = (25,25,200) BLACK = (23,23,23 ) WHITE = (254,254,254) ALPHA = (0,255,0) world = pygame.display.set_mode([worldx,worldy]) backdrop = pygame.image.load(os.path.join('images','stage.png')).convert() backdropbox = world.get_rect() player = Player() # spawn player player.rect.x = 0 player.rect.y = 0 player_list = pygame.sprite.Group() player_list.add(player) steps = 10 forwardx = 600 backwardx = 230 eloc = [] eloc = [200,20] gloc = [] #gloc = [0,630,64,630,128,630,192,630,256,630,320,630,384,630] tx = 64 #tile size ty = 64 #tile size i=0 while i <= (worldx/tx)+tx: gloc.append(i*tx) i=i+1 enemy_list = Level.bad( 1, eloc ) ground_list = Level.ground( 1,gloc,tx,ty ) plat_list = Level.platform( 1,tx,ty ) loot_list = Level.loot(1,tx,ty) ''' Main loop ''' while main == True: for event in pygame.event.get(): if event.type == pygame.QUIT: pygame.quit(); sys.exit() main = False if event.type == pygame.KEYDOWN: if event.key == pygame.K_LEFT or event.key == ord('a'): print("LEFT") player.control(-steps,0) if event.key == pygame.K_RIGHT or event.key == ord('d'): print("RIGHT") player.control(steps,0) if event.key == pygame.K_UP or event.key == ord('w'): print('jump') if event.type == pygame.KEYUP: if event.key == pygame.K_LEFT or event.key == ord('a'): player.control(steps,0) if event.key == pygame.K_RIGHT or event.key == ord('d'): player.control(-steps,0) if event.key == pygame.K_UP or event.key == ord('w'): player.jump(plat_list) if event.key == ord('q'): pygame.quit() sys.exit() main = False # scroll the world forward if player.rect.x >= forwardx: scroll = player.rect.x - forwardx player.rect.x = forwardx for p in plat_list: p.rect.x -= scroll for e in enemy_list: e.rect.x -= scroll for l in loot_list: l.rect.x -= scroll # scroll the world backward if player.rect.x <= backwardx: scroll = backwardx - player.rect.x player.rect.x = backwardx for p in plat_list: p.rect.x += scroll for e in enemy_list: e.rect.x += scroll for l in loot_list: l.rect.x += scroll world.blit(backdrop, backdropbox) player.gravity() # check gravity player.update() player_list.draw(world) #refresh player position enemy_list.draw(world) # refresh enemies ground_list.draw(world) # refresh enemies plat_list.draw(world) # refresh platforms loot_list.draw(world) # refresh loot for e in enemy_list: e.move() pygame.display.flip() clock.tick(fps)via: https://opensource.com/article/20/1/loot-python-platformer-game作者：Seth Kenlon 选题：lujun9972 译者：heguangzhi 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
Wine 的一个新的主要版本发布了。使用 Wine 5.0，在 Linux 上运行 Windows 应用程序和游戏的体验得到进一步改进。通过一些努力，你可以使用 Wine 在 Linux 上运行 Windows 应用程序。当你必须使用一个仅在 Windows 上可用的软件时，Wine 是一个可以尝试的工具。它支持许多这样的软件。Wine 的一个新的主要发布版本已经降临，即 Wine 5.0，几乎距它的 4.0 发布一年之后。Wine 5.0 发布版本引进了几个主要特性和很多显著的更改/改进。在这篇文章中，我将重点介绍新的特性是什么，并且也将提到安装说明。在 Wine 5.0 中有什么新的特性？如他们的官方声明所述，这是 5.0 发布版本中的关键更改：PE 格式的内置模块。支持多显示器。重新实现了 XAudio2。支持 Vulkan 1.1。支持微软安装程序（MSI）补丁文件。性能提升。因此，随着 Vulkan 1.1 和对多显示器的支持 —— Wine 5.0 发布版本是一件大事。除了上面强调的这些关键内容以外，在新的版本中包含成千上万的更改/改进中，你还可以期待对控制器的支持更好。值得注意的是，此版本特别纪念了 Józef Kucia（vkd3d 项目的首席开发人员）。他们也已经在发布说明中提到这一点：这个发布版本特别纪念了 Józef Kucia，他于 2019 年 8 月去世，年仅 30 岁。Józef 是 Wine 的 Direct3D 实现的一个主要贡献者，并且是 vkd3d 项目的首席开发人员。我们都非常怀念他的技能和友善。如何在 Ubuntu 和 Linux Mint 上安装 Wine 5.0注意：如果你在以前安装过 Wine，你应该将其完全移除，以（如你希望的）避免一些冲突。此外，WineHQ 存储库的密钥最近已被更改，针对你的 Linux 发行版的更多的操作指南，你可以参考它的下载页面。Wine 5.0 的源码可在它的官方网站上获得。为了使其工作，你可以阅读更多关于构建 Wine 的信息。基于 Arch 的用户应该很快就会得到它。在这里，我将向你展示在 Ubuntu 和其它基于 Ubuntu 的发行版上安装 Wine 5.0 的步骤。请耐心，并按照步骤一步一步安装和使用 Wine。这里涉及几个步骤。请记住，Wine 安装了太多软件包。你会看到大量的软件包列表，下载大小约为 1.3 GB。在 Ubuntu 上安装 Wine 5.0（不适用于 Linux Mint）首先，使用这个命令来移除现存的 Wine：sudo apt remove winehq-stable wine-stable wine1.6 wine-mono wine-geco winetricks然后确保添加 32 位体系结构支持：sudo dpkg --add-architecture i386下载并添加官方 Wine 存储库密钥：wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -现在，接下来的步骤需要添加存储库，为此, 你需要首先知道你的 Ubuntu 版本。对于 Ubuntu 18.04 和 19.04，用这个 PPA 添加 FAudio 依赖， Ubuntu 19.10 不需要它：sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport现在使用此命令添加存储库：sudo apt-add-repository "deb https://dl.winehq.org/wine-builds/ubuntu $(lsb_release -cs) main"现在你已经添加了正确的存储库，可以使用以下命令安装 Wine 5.0：sudo apt update && sudo apt install --install-recommends winehq-stable请注意，尽管在软件包列表中将 Wine 5 列为稳定版，但你仍可能会看到 winehq-stable 的 wine 4.0.3。也许它不会传播到所有地理位置。从今天早上开始，我可以看到 Wine 5.0。在 Linux Mint 19.1、19.2 和 19.3 中安装 Wine 5.0正如一些读者通知我的那样，apt-add 存储库命令不适用于 Linux Mint 19.x 系列。这是添加自定义存储库的另一种方法。你必须执行与 Ubuntu 相同的步骤。如删除现存的 Wine 包：sudo apt remove winehq-stable wine-stable wine1.6 wine-mono wine-geco winetricks添加 32 位支持：sudo dpkg --add-architecture i386然后添加 GPG 密钥：wget -qO - https://dl.winehq.org/wine-builds/winehq.key | sudo apt-key add -添加 FAudio 依赖：sudo add-apt-repository ppa:cybermax-dexter/sdl2-backport现在为 Wine 存储库创建一个新条目：sudo sh -c "echo 'deb https://dl.winehq.org/wine-builds/ubuntu/ bionic main' >> /etc/apt/sources.list.d/winehq.list"更新软件包列表并安装Wine：sudo apt update && sudo apt install --install-recommends winehq-stable总结你尝试过最新的 Wine 5.0 发布版本吗？如果是的话，在运行中你看到什么改进？在下面的评论区域，让我知道你对新的发布版本的看法。via: https://itsfoss.com/wine-5-release/作者：Ankush Das 选题：lujun9972 译者：robsean 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
在 Rust 中使用类型级编程可以使硬件抽象更加安全。Rust 是一种日益流行的编程语言，被视为硬件接口的最佳选择。通常会将其与 C 的抽象级别相比较。本文介绍了 Rust 如何通过多种方式处理按位运算，并提供了既安全又易于使用的解决方案。语言诞生于官方描述总览C1972 年C 是一种通用编程语言，具有表达式简约、现代的控制流和数据结构，以及丰富的运算符集等特点。（来源：CS 基础知识）C 是（一种）命令式语言，旨在以相对简单的方式进行编译，从而提供对内存的低级访问。（来源：W3schools.in）Rust2010 年一种赋予所有人构建可靠、高效的软件的能力的语言（来源：Rust 网站）Rust 是一种专注于安全性（尤其是安全并发性）的多范式系统编程语言。（来源：维基百科)）在 C 语言中对寄存器值进行按位运算在系统编程领域，你可能经常需要编写硬件驱动程序或直接与内存映射设备进行交互，而这些交互几乎总是通过硬件提供的内存映射寄存器来完成的。通常，你通过对某些固定宽度的数字类型进行按位运算来与这些寄存器进行交互。例如，假设一个 8 位寄存器具有三个字段：+----------+------+-----------+---------+ | (unused) | Kind | Interrupt | Enabled | +----------+------+-----------+---------+ 5-7 2-4 1 0字段名称下方的数字规定了该字段在寄存器中使用的位。要启用该寄存器，你将写入值 1（以二进制表示为 0000_0001）来设置 Enabled 字段的位。但是，通常情况下，你也不想干扰寄存器中的现有配置。假设你要在设备上启用中断功能，但也要确保设备保持启用状态。为此，必须将 Interrupt 字段的值与 Enabled 字段的值结合起来。你可以通过按位操作来做到这一点：1 | (1 << 1)通过将 1 和 2（1 左移一位得到）进行“或”（|）运算得到二进制值 0000_0011 。你可以将其写入寄存器，使其保持启用状态，但也启用中断功能。你的头脑中要记住很多事情，特别是当你要在一个完整的系统上和可能有数百个之多的寄存器打交道时。在实践上，你可以使用助记符来执行此操作，助记符可跟踪字段在寄存器中的位置以及字段的宽度（即它的上边界是什么）下面是这些助记符之一的示例。它们是 C 语言的宏，用右侧的代码替换它们的出现的地方。这是上面列出的寄存器的简写。＆ 的左侧是该字段的起始位置，而右侧则限制该字段所占的位：#define REG_ENABLED_FIELD(x) (x << 0) & 1 #define REG_INTERRUPT_FIELD(x) (x << 1) & 2 #define REG_KIND_FIELD(x) (x << 2) & (7 << 2)然后，你可以使用这些来抽象化寄存器值的操作，如下所示：void set_reg_val(reg* u8, val u8); fn enable_reg_with_interrupt(reg* u8) { set_reg_val(reg, REG_ENABLED_FIELD(1) | REG_INTERRUPT_FIELD(1)); }这就是现在的做法。实际上，这就是大多数驱动程序在 Linux 内核中的使用方式。有没有更好的办法？如果能够基于对现代编程语言研究得出新的类型系统，就可能能够获得安全性和可表达性的好处。也就是说，如何使用更丰富、更具表现力的类型系统来使此过程更安全、更持久？在 Rust 语言中对寄存器值进行按位运算继续用上面的寄存器作为例子：+----------+------+-----------+---------+ | (unused) | Kind | Interrupt | Enabled | +----------+------+-----------+---------+ 5-7 2-4 1 0你想如何用 Rust 类型来表示它呢？你将以类似的方式开始，为每个字段的偏移定义常量（即，距最低有效位有多远）及其掩码。掩码是一个值，其二进制表示形式可用于更新或读取寄存器内部的字段：const ENABLED_MASK: u8 = 1; const ENABLED_OFFSET: u8 = 0; const INTERRUPT_MASK: u8 = 2; const INTERRUPT_OFFSET: u8 = 1; const KIND_MASK: u8 = 7 << 2; const KIND_OFFSET: u8 = 2;接下来，你将声明一个 Field 类型并进行操作，将给定值转换为与其位置相关的值，以供在寄存器内使用：struct Field { value: u8, } impl Field { fn new(mask: u8, offset: u8, val: u8) -> Self { Field { value: (val << offset) & mask, } } }最后，你将使用一个 Register 类型，该类型会封装一个与你的寄存器宽度匹配的数字类型。 Register 具有 update 函数，可使用给定字段来更新寄存器：struct Register(u8); impl Register { fn update(&mut self, val: Field) { self.0 = self.0 | field.value; } } fn enable_register(&mut reg) { reg.update(Field::new(ENABLED_MASK, ENABLED_OFFSET, 1)); }使用 Rust，你可以使用数据结构来表示字段，将它们与特定的寄存器联系起来，并在与硬件交互时提供简洁明了的工效。这个例子使用了 Rust 提供的最基本的功能。无论如何，添加的结构都会减轻上述 C 示例中的某些晦涩的地方。现在，字段是个带有名字的事物，而不是从模糊的按位运算符派生而来的数字，并且寄存器是具有状态的类型 —— 这在硬件上多了一层抽象。一个易用的 Rust 实现用 Rust 重写的第一个版本很好，但是并不理想。你必须记住要带上掩码和偏移量，并且要手工进行临时计算，这容易出错。人类不擅长精确且重复的任务 —— 我们往往会感到疲劳或失去专注力，这会导致错误。一次一个寄存器地手动记录掩码和偏移量几乎可以肯定会以糟糕的结局而告终。这是最好留给机器的任务。其次，从结构上进行思考：如果有一种方法可以让字段的类型携带掩码和偏移信息呢？如果可以在编译时就发现硬件寄存器的访问和交互的实现代码中存在错误，而不是在运行时才发现，该怎么办？也许你可以依靠一种在编译时解决问题的常用策略，例如类型。你可以使用 typenum 来修改前面的示例，该库在类型级别提供数字和算术。在这里，你将使用掩码和偏移量对 Field 类型进行参数化，使其可用于任何 Field 实例，而无需将其包括在调用处：#[macro_use] extern crate typenum; use core::marker::PhantomData; use typenum::*; // Now we'll add Mask and Offset to Field's type struct Field<Mask: Unsigned, Offset: Unsigned> { value: u8, _mask: PhantomData<Mask>, _offset: PhantomData<Offset>, } // We can use type aliases to give meaningful names to // our fields (and not have to remember their offsets and masks). type RegEnabled = Field<U1, U0>; type RegInterrupt = Field<U2, U1>; type RegKind = Field<op!(U7 << U2), U2>;现在，当重新访问 Field 的构造函数时，你可以忽略掩码和偏移量参数，因为类型中包含该信息：impl<Mask: Unsigned, Offset: Unsigned> Field<Mask, Offset> { fn new(val: u8) -> Self { Field { value: (val << Offset::U8) & Mask::U8, _mask: PhantomData, _offset: PhantomData, } } } // And to enable our register... fn enable_register(&mut reg) { reg.update(RegEnabled::new(1)); }看起来不错，但是……如果你在给定的值是否适合该字段方面犯了错误，会发生什么？考虑一个简单的输入错误，你在其中放置了 10 而不是 1：fn enable_register(&mut reg) { reg.update(RegEnabled::new(10)); }在上面的代码中，预期结果是什么？好吧，代码会将启用位设置为 0，因为 10＆1 = 0。那真不幸；最好在尝试写入之前知道你要写入字段的值是否适合该字段。事实上，我认为截掉错误字段值的高位是一种 1未定义的行为（哈）。出于安全考虑使用 Rust如何以一般方式检查字段的值是否适合其规定的位置？需要更多类型级别的数字！你可以在 Field 中添加 Width 参数，并使用它来验证给定的值是否适合该字段：struct Field<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> { value: u8, _mask: PhantomData<Mask>, _offset: PhantomData<Offset>, _width: PhantomData<Width>, } type RegEnabled = Field<U1,U1, U0>; type RegInterrupt = Field<U1, U2, U1>; type RegKind = Field<U3, op!(U7 << U2), U2>; impl<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> Field<Width, Mask, Offset> { fn new(val: u8) -> Option<Self> { if val <= (1 << Width::U8) - 1 { Some(Field { value: (val << Offset::U8) & Mask::U8, _mask: PhantomData, _offset: PhantomData, _width: PhantomData, }) } else { None } } }现在，只有给定值适合时，你才能构造一个 Field ！否则，你将得到 None 信号，该信号指示发生了错误，而不是截掉该值的高位并静默写入意外的值。但是请注意，这将在运行时环境中引发错误。但是，我们事先知道我们想写入的值，还记得吗？鉴于此，我们可以教编译器完全拒绝具有无效字段值的程序 —— 我们不必等到运行它！这次，你将向 new 的新实现 new_checked 中添加一个特征绑定（where 子句），该函数要求输入值小于或等于给定字段用 Width 所能容纳的最大可能值：struct Field<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> { value: u8, _mask: PhantomData<Mask>, _offset: PhantomData<Offset>, _width: PhantomData<Width>, } type RegEnabled = Field<U1, U1, U0>; type RegInterrupt = Field<U1, U2, U1>; type RegKind = Field<U3, op!(U7 << U2), U2>; impl<Width: Unsigned, Mask: Unsigned, Offset: Unsigned> Field<Width, Mask, Offset> { const fn new_checked<V: Unsigned>() -> Self where V: IsLessOrEqual<op!((U1 << Width) - U1), Output = True>, { Field { value: (V::U8 << Offset::U8) & Mask::U8, _mask: PhantomData, _offset: PhantomData, _width: PhantomData, } } }只有拥有此属性的数字才实现此特征，因此，如果使用不适合的数字，它将无法编译。让我们看一看！fn enable_register(&mut reg) { reg.update(RegEnabled::new_checked::<U10>()); } 12 | reg.update(RegEnabled::new_checked::<U10>()); | ^^^^^^^^^^^^^^^^ expected struct `typenum::B0`, found struct `typenum::B1` | = note: expected type `typenum::B0` found type `typenum::B1`new_checked 将无法生成一个程序，因为该字段的值有错误的高位。你的输入错误不会在运行时环境中才爆炸，因为你永远无法获得一个可以运行的工件。就使内存映射的硬件进行交互的安全性而言，你已经接近 Rust 的极致。但是，你在 C 的第一个示例中所写的内容比最终得到的一锅粥的类型参数更简洁。当你谈论潜在可能有数百甚至数千个寄存器时，这样做是否容易处理？让 Rust 恰到好处：既安全又方便使用早些时候，我认为手工计算掩码有问题，但我又做了同样有问题的事情 —— 尽管是在类型级别。虽然使用这种方法很不错，但要达到编写任何代码的地步，则需要大量样板和手动转录（我在这里谈论的是类型的同义词）。我们的团队想要像 TockOS mmio 寄存器之类的东西，而以最少的手动转录生成类型安全的实现。我们得出的结果是一个宏，该宏生成必要的样板以获得类似 Tock 的 API 以及基于类型的边界检查。要使用它，请写下一些有关寄存器的信息，其字段、宽度和偏移量以及可选的枚举类的值（你应该为字段可能具有的值赋予“含义”）：register! { // The register's name Status, // The type which represents the whole register. u8, // The register's mode, ReadOnly, ReadWrite, or WriteOnly. RW, // And the fields in this register. Fields [ On WIDTH(U1) OFFSET(U0), Dead WIDTH(U1) OFFSET(U1), Color WIDTH(U3) OFFSET(U2) [ Red = U1, Blue = U2, Green = U3, Yellow = U4 ] ] }由此，你可以生成寄存器和字段类型，如上例所示，其中索引：Width、Mask 和 Offset 是从一个字段定义的 WIDTH 和 OFFSET 部分的输入值派生的。另外，请注意，所有这些数字都是 “类型数字”；它们将直接进入你的 Field 定义！生成的代码通过为寄存器及字段指定名称来为寄存器及其相关字段提供名称空间。这很绕口，看起来是这样的：mod Status { struct Register(u8); mod On { struct Field; // There is of course more to this definition } mod Dead { struct Field; } mod Color { struct Field; pub const Red: Field = Field::<U1>new(); // &c. } }生成的 API 包含名义上期望的读取和写入的原语，以获取原始寄存器的值，但它也有办法获取单个字段的值、执行集合操作以及确定是否设置了任何（或全部）位集合的方法。你可以阅读完整生成的 API上的文档。粗略检查将这些定义用于实际设备会是什么样？代码中是否会充斥着类型参数，从而掩盖了视图中的实际逻辑？不会！通过使用类型同义词和类型推断，你实际上根本不必考虑程序的类型层面部分。你可以直接与硬件交互，并自动获得与边界相关的保证。这是一个 UART 寄存器块的示例。我会跳过寄存器本身的声明，因为包括在这里就太多了。而是从寄存器“块”开始，然后帮助编译器知道如何从指向该块开头的指针中查找寄存器。我们通过实现 Deref 和 DerefMut 来做到这一点：#[repr(C)] pub struct UartBlock { rx: UartRX::Register, _padding1: [u32; 15], tx: UartTX::Register, _padding2: [u32; 15], control1: UartControl1::Register, } pub struct Regs { addr: usize, } impl Deref for Regs { type Target = UartBlock; fn deref(&self) -> &UartBlock { unsafe { &*(self.addr as *const UartBlock) } } } impl DerefMut for Regs { fn deref_mut(&mut self) -> &mut UartBlock { unsafe { &mut *(self.addr as *mut UartBlock) } } }一旦到位，使用这些寄存器就像 read() 和 modify() 一样简单：fn main() { // A pretend register block. let mut x = [0_u32; 33]; let mut regs = Regs { // Some shenanigans to get at `x` as though it were a // pointer. Normally you'd be given some address like // `0xDEADBEEF` over which you'd instantiate a `Regs`. addr: &mut x as *mut [u32; 33] as usize, }; assert_eq!(regs.rx.read(), 0); regs.control1 .modify(UartControl1::Enable::Set + UartControl1::RecvReadyInterrupt::Set); // The first bit and the 10th bit should be set. assert_eq!(regs.control1.read(), 0b_10_0000_0001); }当我们使用运行时值时，我们使用如前所述的选项。这里我使用的是 unwrap，但是在一个输入未知的真实程序中，你可能想检查一下从新调用中返回的某些东西： 1 2 fn main() { // A pretend register block. let mut x = [0_u32; 33]; let mut regs = Regs { // Some shenanigans to get at `x` as though it were a // pointer. Normally you'd be given some address like // `0xDEADBEEF` over which you'd instantiate a `Regs`. addr: &mut x as *mut [u32; 33] as usize, }; let input = regs.rx.get_field(UartRX::Data::Field::Read).unwrap(); regs.tx.modify(UartTX::Data::Field::new(input).unwrap()); }解码失败条件根据你的个人痛苦忍耐程度，你可能已经注意到这些错误几乎是无法理解的。看一下我所说的不那么微妙的提醒：error[E0271]: type mismatch resolving `<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>, typenum::B0>, typenum::B0> as typenum::IsLessOrEqual<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UInt<typenum::UTerm, typenum::B1>, typenum::B0>, typenum::B1>, typenum::B0>>>::Output == typenum::B1` --> src/main.rs:12:5 | 12 | less_than_ten::<U20>(); | ^^^^^^^^^^^^^^^^^^^^ expected struct `typenum::B0`, found struct `typenum::B1` | = note: expected type `typenum::B0` found type `typenum::B1`expected struct typenum::B0, found struct typenum::B1 部分是有意义的，但是 typenum::UInt<typenum::UInt, typenum::UInt... 到底是什么呢？好吧，typenum 将数字表示为二进制 cons 单元！像这样的错误使操作变得很困难，尤其是当你将多个这些类型级别的数字限制在狭窄的范围内时，你很难知道它在说哪个数字。当然，除非你一眼就能将巴洛克式二进制表示形式转换为十进制表示形式。在第 U100 次试图从这个混乱中破译出某些含义之后，我们的一个队友简直《 疯了，地狱了，不要再忍受了 Mad As Hell And Wasn’t Going To Take It Anymore 》，并做了一个小工具 tnfilt，从这种命名空间的二进制 cons 单元的痛苦中解脱出来。tnfilt 将 cons 单元格式的表示法替换为可让人看懂的十进制数字。我们认为其他人也会遇到类似的困难，所以我们分享了 tnfilt。你可以像这样使用它：$ cargo build 2>&1 | tnfilt它将上面的输出转换为如下所示：error[E0271]: type mismatch resolving `<U20 as typenum::IsLessOrEqual<U10>>::Output == typenum::B1`现在这才有意义！结论当在软件与硬件进行交互时，普遍使用内存映射寄存器，并且有无数种方法来描述这些交互，每种方法在易用性和安全性上都有不同的权衡。我们发现使用类型级编程来取得内存映射寄存器交互的编译时检查可以为我们提供制作更安全软件的必要信息。该代码可在 bounded-registers crate（Rust 包）中找到。我们的团队从安全性较高的一面开始，然后尝试找出如何将易用性滑块移近易用端。从这些雄心壮志中，“边界寄存器”就诞生了，我们在 Auxon 公司的冒险中遇到内存映射设备的任何时候都可以使用它。此内容最初发布在 Auxon Engineering 博客上，并经许可进行编辑和重新发布。从技术上讲，从定义上看，从寄存器字段读取的值只能在规定的范围内，但是我们当中没有一个人生活在一个纯净的世界中，而且你永远都不知道外部系统发挥作用时会发生什么。你是在这里接受硬件之神的命令，因此与其强迫你进入“可能的恐慌”状态，还不如给你提供处理“这将永远不会发生”的机会。 ↩get_field 看起来有点奇怪。我正在专门查看 Field::Read 部分。Field 是一种类型，你需要该类型的实例才能传递给 get_field。更干净的 API 可能类似于：regs.rx.get_field::<UartRx::Data::Field>(); 但是请记住，Field 是一种具有固定的宽度、偏移量等索引的类型的同义词。要像这样对 get_field 进行参数化，你需要使用更高级的类型。 ↩via: https://opensource.com/article/20/1/c-vs-rust-abstractions作者：Dan Pittman 选题：lujun9972 译者：wxy 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出
了解有关在 Web 浏览器中运行任何代码的最新方法的更多信息。如果你还没有听说过 WebAssembly，那么你很快就会知道。这是业界最保密的秘密之一，但它无处不在。所有主流的浏览器都支持它，并且它也将在服务器端使用。它很快，它能用于游戏编程。这是主要的国际网络标准组织万维网联盟（W3C）的一个开放标准。你可能会说：“哇，这听起来像是我应该学习编程的东西！”你可能是对的，但也是错的。你不需要用 WebAssembly 编程。让我们花一些时间来学习这种通常被缩写为“Wasm”的技术。它从哪里来？大约十年前，人们越来越认识到，广泛使用的 JavaScript 不够快速，无法满足许多目的。JavaScript 无疑是成功和方便的。它可以在任何浏览器中运行，并启用了今天我们认为理所当然的动态网页类型。但这是一种高级语言，在设计时并没有考虑到计算密集型工作负载。然而，尽管负责主流 web 浏览器的工程师们对性能问题的看法大体一致，但他们对如何解决这个问题却意见不一。出现了两个阵营，谷歌开始了它的 原生客户端 Native Client 项目，后来又推出了 可移植原生客户端 Portable Native Client 变体，着重于允许用 C/C++ 编写的游戏和其它软件在 Chrome 的一个安全隔间中运行。与此同时，Mozilla 赢得了微软对 asm.js 的支持。该方法更新了浏览器，因此它可以非常快速地运行 JavaScript 指令的低级子集（有另一个项目可以将 C/C++ 代码转换为这些指令）。由于这两个阵营都没有得到广泛采用，各方在 2015 年同意围绕一种称为 WebAssembly 的新标准，以 asm.js 所采用的基本方法为基础，联合起来。如 CNET 的 Stephen Shankland 当时所写，“在当今的 Web 上，浏览器的 JavaScript 将这些指令转换为机器代码。但是，通过 WebAssembly，程序员可以在此过程的早期阶段完成很多工作，从而生成介于两种状态之间的程序。这使浏览器摆脱了创建机器代码的繁琐工作，但也实现了 Web 的承诺 —— 该软件将在具有浏览器的任何设备上运行，而无需考虑基础硬件的细节。”在 2017 年，Mozilla 宣布了它的最小可行的产品（MVP），并使其脱离预览版阶段。到该年年底，所有主流的浏览器都采用了它。2019 年 12 月，WebAssembly 工作组发布了三个 W3C 推荐的 WebAssembly 规范。WebAssembly 定义了一种可执行程序的可移植二进制代码格式、相应的文本汇编语言以及用于促进此类程序与其宿主环境之间的交互接口。WebAssembly 代码在低级虚拟机中运行，这个可运行于许多微处理器之上的虚拟机可模仿这些处理器的功能。通过即时（JIT）编译或解释，WebAssembly 引擎可以以近乎原生平台编译代码的速度执行。为什么现在感兴趣？当然，最近对 WebAssembly 感兴趣的部分原因是最初希望在浏览器中运行更多计算密集型代码。尤其是笔记本电脑用户，越来越多的时间都花在浏览器上（或者，对于 Chromebook 用户来说，基本上是所有时间）。这种趋势已经迫切需要消除在浏览器中运行各种应用程序的障碍。这些障碍之一通常是性能的某些方面，这正是 WebAssembly 及其前身最初旨在解决的问题。但是，WebAssembly 并不仅仅适用于浏览器。在 2019 年，Mozilla 宣布了一个名为 WASI（ WebAssembly 系统接口 WebAssembly System Interface ）的项目，以标准化 WebAssembly 代码如何与浏览器上下文之外的操作系统进行交互。通过将浏览器对 WebAssembly 和 WASI 的支持结合在一起，编译后的二进制文件将能够以接近原生的速度，跨不同的设备和操作系统在浏览器内外运行。WebAssembly 的低开销立即使它可以在浏览器之外使用，但这无疑是赌注；显然，还有其它不会引入性能瓶颈的运行应用程序的方法。为什么要专门使用 WebAssembly？一个重要的原因是它的可移植性。如今，像 C++ 和 Rust 这样的广泛使用的编译语言可能是与 WebAssembly 关联最紧密的语言。但是，各种各样的其他语言可以编译为 WebAssembly 或拥有它们的 WebAssembly 虚拟机。此外，尽管 WebAssembly 为其执行环境假定了某些先决条件，但它被设计为在各种操作系统和指令集体系结构上有效执行。因此，WebAssembly 代码可以使用多种语言编写，并可以在多种操作系统和处理器类型上运行。另一个 WebAssembly 优势源于这样一个事实：代码在虚拟机中运行。因此，每个 WebAssembly 模块都在沙盒环境中执行，并使用故障隔离技术将其与宿主机运行时环境分开。这意味着，对于其它部分而言，应用程序独立于其宿主机环境的其余部分执行，如果不调用适当的 API，就无法摆脱沙箱。WebAssembly 现状这一切在实践中意味着什么？如今在运作中的 WebAssembly 的一个例子是 Enarx。Enarx 是一个提供硬件独立性的项目，可使用 受信任的执行环境 Trusted Execution Environments （TEE）保护应用程序的安全。Enarx 使你可以安全地将编译为 WebAssembly 的应用程序始终交付到云服务商，并远程执行它。正如 Red Hat 安全工程师 Nathaniel McCallum 指出的那样：“我们这样做的方式是，我们将你的应用程序作为输入，并使用远程硬件执行认证过程。我们使用加密技术验证了远程硬件实际上是它声称的硬件。最终的结果不仅是我们对硬件的信任度提高了；它也是一个会话密钥，我们可以使用它将加密的代码和数据传递到我们刚刚要求加密验证的环境中。”另一个例子是 OPA， 开放策略代理 Open Policy Agent ，它发布于 2019 年 11 月，你可以编译他们的策略定义语言 Rego 为 WebAssembly。Rego 允许你编写逻辑来搜索和组合来自不同来源的 JSON/YAML 数据，以询问诸如“是否允许使用此 API？”之类的问题。OPA 已被用于支持策略的软件，包括但不限于 Kubernetes。使用 OPA 之类的工具来简化策略被认为是在各种不同环境中正确保护 Kubernetes 部署的重要步骤。WebAssembly 的可移植性和内置的安全功能非常适合这些工具。我们的最后一个例子是 Unity。还记得我们在文章开头提到过 WebAssembly 可用于游戏吗？好吧，跨平台游戏引擎 Unity 是 WebAssembly 的较早采用者，它提供了在浏览器中运行的 Wasm 的首个演示品，并且自 2018 年 8 月以来，已将 WebAssembly用作 Unity WebGL 构建目标的输出目标。这些只是 WebAssembly 已经开始产生影响的几种方式。你可以在 https://webassembly.org/ 上查找更多信息并了解 Wasm 的所有最新信息。via: https://opensource.com/article/20/1/webassembly作者：Mike Bursell 选题：lujun9972 译者：laingke 校对：wxy本文由 LCTT 原创编译，Linux中国 荣誉推出