2021年7月

测试期间 M1 Mac 的超长电池续航表现曾令苹果高管认为出了 bug

M1 MacBook Air 和 M1 MacBook Pro 带来了扎扎实实的两位数电池续航时间。苹果公司产品营销副总裁在测试期间一度认为,操作系统的电池指示器出了 bug:“当我们看到第一台系统被把玩了几个小时后,电池指示器甚至没有多大动静,我们想‘哦,伙计,那是一个错误,电池指示器坏了’。然后 Tim 在笑着说,‘不,这就是它应该有的样子’,这是很惊人的。”

这真是出了一个 “bug”,居然能做到性能和续航双开花。

多数情况下 Windows 11 的 WSL2 的性能几乎与原生 Ubuntu 相同

近日,外媒 Phoronix 对 Windows 11 的 WSL2 的性能进行了测试。测试表明,对于 CPU 敏感的工作负载,WSL2 的性能与 Ubuntu Linux 本身相当。但在一些情况下,Windows 11 的 WSL2 实现还不如 Windows 10 下的性能好,当然,估计 Windows 11 正式推出时会有改善。尤其是当涉及到 I/O 时,WSL2 的性能往往比裸机 Linux 慢得多,但至少比原来的 WSL1 性能好。

真是有点忧虑 Windows 会这样“吞噬”了 Linux。

受到抵制后,日本政府取消传真机的计划实际上失败了

日本的一个促进行政改革的内阁机构决定在本月底前废除传真机的使用,并在日本的各部委和机构改用电子邮件。此举将使更多的人能够在家工作,在冠状病毒大流行期间仍有太多的人去办公室发送和接收传真。然而,在遇到“传真爱好者”官员的抵制后,该计划实际上被取消了。数以百计的政府办公室非但没有拥抱数字时代,反而为这种备受指责的机器进行辩护,他们担心敏感信息的安全和“对通信环境的焦虑”。日本内阁机构的一名官员说:“尽管许多部委和机构可能已经停止使用传真机,但我不能自豪地说,我们成功地摆脱了大多数传真机。”

这是越原始越安全么?我看其实还是懒于改变现状的心态吧。

PlantNet 将开源技术与众包知识结合起来,帮助你成为业余植物学家。

 title=

在我居住的地方很多小路和道路两旁都有花草树木。我所在的社区因其每年的枫树节而闻名,枫树对我来说很容易识别。然而,还有许多其他的树我无法识别名字。花也是如此:蒲公英很容易发现,但我不知道在我的步行道上的野花的名字。

最近,我的妻子告诉我了 PlantNet,一个可以识别这些花草和树木的移动应用。它可以在 iOS 和 Android 上使用,而且是免费的,所以我决定试试。

以开源的方式识别植物

我在手机上下载了这个应用程序,开始用它来识别我在村子周围散步时的一些花草和树木。随着我对这个应用的熟悉,我注意到我拍摄的图片(以及其他用户拍摄的图片)是以知识共享署名-相同方式共享(CC-BY-SA)的许可方式共享的。进一步的调查显示,PlantNet 是 开源 的。如果你喜欢,你可以匿名使用该应用,或者成为社区的注册成员。

根据 Cos4Cloud 公民科学项目,“PlantNet 是一个参与性的公民科学平台,用于收集、分享和审查基于自动识别的植物观察结果。它的目标是监测植物的生物多样性,促进公众对植物知识的获取”。它使用图像识别技术来清点生物多样性。

该项目的开发始于 2009 年,由法国的植物学家和计算机科学家进行。它最初是一个 Web 应用,而智能手机应用程序于 2013 年推出。该项目是 Floris'Tic 倡议的一部分,这是法国的另一个项目,旨在促进植物科学的科学、技术和工业文化。

PlantNet 允许用户利用智能手机的摄像头来收集视觉标本,并由软件和社区进行识别。然后,这些照片将与全世界数百万加入 PlantNet 网络的人分享。

该项目说:“PlantNet 系统的工作原理是,比较用户通过他们寻求鉴定的植物器官(花、果实、叶……)的照片传送的视觉模式。这些图像被分析,并与每天协作制作和充实的图像库进行比较。然后,该系统提供一个可能的物种清单及其插图”。

使用 PlantNet

该应用很容易使用。从你的智能手机上的应用图标启动它。

 title=

当应用打开时,你会看到你已经在资料库中收集的标本。显示屏底部的相机图标允许你使用你的相机将图片添加到你的照片库。

 title=

选择“相机”选项,将手机的摄像头对准你想识别的树木或花草。拍完照后,点击与你想识别的标本相匹配的选项(叶、花、树皮、果实等)。

 title=

例如,如果你想通过叶子的特征来识别一个标本,请选择叶子。PlantNet 对其识别的确定程度进行了分配,从高到低的百分比不等。你还可以使用你的智能手机的 GPS 功能,将位置信息自动添加到你的数据收集中,你还可以添加注释。

 title=

你可以在你的智能手机上或通过你的用户 ID(如果你创建了一个账户)登录网站,访问你上传的所有观测数据,并跟踪社区是否批准了它们。从网站界面上,你也可以下载 CSV 或电子表格格式的观察记录。

 title=

很好的户外活动

我特别喜欢 PlantNet 与维基百科的链接,这样我就可以阅读更多关于我收集的植物数据的信息。

目前全球大约有 1200 万 PlantNet 用户,所以数据集一直在增长。该应用是免费使用的,每天最多可以有 500 个请求。它还有一个 API,以 JSON 格式提供数据,所以你甚至可以把 Pl antNet 的视觉识别引擎作为一个 Web 服务使用。

PlantNet 的一个非常好的地方是,它结合了众包知识和开源技术,将用户相互联系起来,并与很好的户外活动联系起来。没有比这更好的理由来支持开源软件了。

关于该应用及其开发者的完整描述可在 YouTube 上找到(有法语、英文字幕)。你也可以在 PlantNet 的网站上了解更多该项目。


via: https://opensource.com/article/21/7/open-source-plantnet

作者:Don Watkins 选题:lujun9972 译者:geekpi 校对:wxy

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

虽然有一个 FreeDOS 版的 tar,但 DOS 上事实上的标准归档工具是 Zip 和 Unzip。

 title=

在 Linux 上,你可能熟悉标准的 Unix 归档命令:tar。FreeDOS 上也有 tar 的版本(还有其他一些流行的归档程序),但 DOS 上事实上的标准归档程序是 Zip 和 Unzip。Zip 和 Unzip 都默认安装在 FreeDOS 1.3 RC4 中。

Zip 文件格式最初是由 PKWARE 的 Phil Katz 在 1989 年为 PKZIP 和 PKUNZIP 这对 DOS 归档工具构思的。Katz 将 Zip 文件的规范作为一个开放标准发布,因此任何人都可以创建 Zip 档案。作为开放规范的结果,Zip 成为 DOS 上的一个标准归档格式。Info-ZIP 项目实现了一套开源的 ZIPUNZIP 程序。

对文件和目录进行压缩

你可以在 DOS 命令行中使用 ZIP 来创建文件和目录的归档。这是一个方便的方法,可以为你的工作做一个备份,或者发布一个“包”,在未来的 FreeDOS 发布中使用。例如,假设我想为我的项目源码做一个备份,其中包含这些源文件:

 title=

我想把这些文件归档

ZIP 有大量的命令行选项来做不同的事情,但我最常使用的命令行选项是 -r 来处理目录和子目录 递归,以及使用 -9 来提供可能的最大压缩。ZIPUNZIP 使用类似 Unix 的命令行,所以你可以在破折号后面组合选项:-9r 将提供最大压缩并在 Zip 文件中包括子目录。

 title=

压缩一个目录树

在我的例子中,ZIP 能够将我的源文件从大约 33KB 压缩到大约 22KB,为我节省了 11KB 的宝贵磁盘空间。你可能会得到不同的压缩率,这取决于你给 ZIP 的选项,或者你想在 Zip 文件中存储什么文件(以及有多少)。一般来说,非常长的文本文件(如源码)会产生良好的压缩效果,而非常小的文本文件(如只有几行的 DOS “批处理”文件)通常太短,无法很好地压缩。

解压文件和目录

将文件保存到 Zip 文件中是很好的,但你最终会需要将这些文件解压到某个地方。让我们首先检查一下我们刚刚创建的 Zip 文件里有什么。为此,使用 UNZIP命令。你可以在 UNZIP中使用一堆不同的选项,但我发现我只使用几个常用的选项。

要列出一个 Zip 文件的内容,使用 -l (“list”) 选项。

 title=

用 unzip 列出归档文件的内容

该输出允让我看到 Zip 文件中的 14 个条目:13 个文件加上 SRC 目录。

如果我想提取整个 Zip 文件,我可以直接使用 UNZIP 命令并提供 Zip 文件作为命令行选项。这样就可以从我当前的工作目录开始提取 Zip 文件了。除非我正在恢复某个东西的先前版本,否则我通常不想覆盖我当前的文件。在这种情况下,我希望将 Zip 文件解压到一个新的目录。你可以用 -d (“destination”) 命令行选项指定目标路径。

 title=

你可以用 -d 来解压到目标路径

有时我想从一个 Zip 文件中提取一个文件。在这个例子中,假设我想提取一个 DOS 可执行程序 TEST.EXE。要提取单个文件,你要指定你想提取的 Zip 文件的完整路径。默认情况下,UNZIP 将使用 Zip 文件中提供的路径解压该文件。要省略路径信息,你可以添加 -j(“junk the path”) 选项。

你也可以组合选项。让我们从 Zip 文件中提取 SRC\TEST.EXE 程序,但省略完整路径并将其保存在 TEMP 目录下:

 title=

unzip 组合选项

因为 Zip 文件是一个开放的标准,所以我们会今天继续看到 Zip 文件。每个 Linux 发行版都可以通过 Info-ZIP 程序支持 Zip 文件。你的 Linux 文件管理器可能也支持 Zip 文件。在 GNOME 文件管理器中,你应该可以右击一个文件夹并从下拉菜单中选择“压缩”。你可以选择创建一个包括 Zip 文件在内的新的归档文件。

创建和管理 Zip 文件是任何 DOS 用户的一项关键技能。你可以在 Info-ZIP 网站上了解更多关于 ZIPUNZIP 的信息,或者在命令行上使用 h(“帮助”)选项来打印选项列表。


via: https://opensource.com/article/21/6/archive-files-freedos

作者:Jim Hall 选题:lujun9972 译者:geekpi 校对:wxy

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

想学 Linux 吗?免费的那种

相信你对 Linux 有所了解。Linux 操作系统(发行版),全称 GNU/Linux,是一种免费使用和自由传播的类 UNIX 操作系统。它具有自由、开源、可靠、安全、稳定、多平台等特点。与被更多人所熟知的 Windows 不同的是,Linux 是一套开放源代码的、可以自由传播的类 Unix 操作系统软件。

这样的特点也让 Linux 操作系统在很多场景内都备受欢迎。

但正因为它的“自由、开放”,也让 Linux 的知识点和学习变得复杂琐碎起来。在人人都在快餐学习的时代,怎样去更快捷、系统的学习有关 Linux 的知识?怎么高效的学习 Linux 知识?如何在学习中形成自己的知识脉络?...... 这些都是有志于从事技术工作和改变职业发展道路的你所面临的问题。

如何学习 Linux?

如果你对如何学习 Linux 有困惑,感觉到无从着手,不用担心 ,“Linux 中国”开源技术社区十余年如一日地致力于让 Linux 可以在中国更普及,我们联合阿里云开发者社区,历时数月,投入十余位专家工程师,共同打造了这份《Linux 技术图谱真正帮助不懂 Linux 而又希望学习 Linux 的你,更好更快的上手 Linux。

什么是《Linux 技术图谱》?

这份《Linux 技术图谱》是由 Linux 中国的各位特聘专家和社区成员,结合自身工作经验和业界习惯,特地制定和研发的一套学习路线。对于 Linux 学习者来说,可以按图索骥,跟随图谱安排进行 Linux 的学习,从一个不懂 Linux 的人,成长为一个懂 Linux、用 Linux ,直至成为以此为专业职业的工程师。

这份《Linux 技术图谱涵盖了《Linux 基础知识》《服务器硬件基础》Linux 系统管理进阶》Linux 企业常用服务》《高性能集群负载》等共计 19 门课程,11 门在线实验室和 5 本电子书,4 门线上学习训练营内容,涉及到在技术工作中的绝大多数场景,可以帮助你更好的完成自己的学习和工作。

不仅如此,Linux 中国还为大家制作了相应的视频课程、电子书手册、线上体验实验室,帮助你学好 Linux ,将 Linux 真正落实到你的工作当中去。

如果现在的你,正面临着如何学习 Linux 的困扰,那就不妨从这份《Linux 技术图谱》开始,好好学习 Linux!

三个理由,让你选择《Linux 技术图谱》

这份《Linux 技术图谱》突出了以下三个特点:

  1. 《Linux 技术图谱》由 Linux 中国与阿里巴巴的技术专家共同拟定:它是由 Linux 中国负责起草,并与阿里巴巴的技术专家多次沟通修改,最终确定下来的技术图谱内容。技术图谱中的内容得到了业界专家的认可,值得学习。
  2. 《Linux 技术图谱》所提供的各项资料,均为知名企业的专家工程师撰写:为了能够让技术图谱更有价值,我们特别邀请了一些曾经或现在任职在知名企业工作的优秀工程师,来与我们共建内容,帮助大家学习 Linux 技术。
  3. 《Linux 技术图谱》内容结构丰富立体,可以帮助你更好的学习 Linux:它提供了学习路线、视频课程、电子书、实验体验,可以让你从观看视频、课后辅导和训练,到掌握 Linux 技能,真正将 Linux 内化为自己的一项技能。

免费的?!没错,完全免费!

我们知道很多人在 Linux 学习的路上踩过很多坑。一方面,互联网上充斥着各种良莠不齐的 Linux 培训内容,花了钱,可能还没学到什么东西;另一方面,好不容易碰到一个免费的课程,却发现只是一个付费课程的前奏,刚刚让你看到 Linux 的大门,后续的内容却需要缴纳不菲的费用来学习。

我们知道大家所面临的这些困扰,我们也一直将推广 Linux 作为愿景,所以,我们选择将这份 Linux 技术图谱以及相关课程、电子书、实验室免费开放给大家,让大家可以放心学习、开心学习,再也不需要全网去找盗版资源了,现在你可以直接在网上免费学习全部的完整课程内容。

此外,我们还邀请课程老师为大家安排了后续的课程训练营活动。如果你对于后续的课程训练营活动感兴趣,可以关注 LInux 中国微信公众号的后续活动。我们将会在后续的活动中,带你学习 Linux,手把手教你掌握 Linux 的奥秘!这些训练营也是免费的!

这份《Linux 技术图谱》得到了阿里云开发者社区的大力支持,得以完全免费地提供给大家。不仅仅是课程、电子书、实验室,还有后继的训练营,全部完全免费。之前我们免费推出一些训练营时,也有很多人不敢置信地一再确认是否“完全”免费,没错,就是完全免费!

Linux 中国倾心打造的 19 门课程,现在正式上线了

点此进入阿里云技术社区,获取完整《Linux 技术图谱》。

OnePlus 承认以“省电”的名义限制了 300 个流行应用的性能

这 300 个流行应用的性能被人为限制运行在低速核心而不是快速核心上。这些流行应用被加入到了最快核心的黑名单里,而基准测试应用和不知名应用则能充分利用全部性能。测试应用 Geekbench 随后以操纵基准测试的理由移除了 OnePlus 9 和 9 Pro 的测试结果。

OnePlus 之后承认在发售新手机之后收到了部分用户希望改进电池寿命和热管理的反馈,它的研发团队所采取的改进方法是“优化”包括 Chrome 在内的 300 个流行应用的性能。这导致 Chrome 的性能下降了 75-85%。

我是不会买这种手机的。

REvil 恶意软件被发现会避开以俄语为主要语言的系统

NBC News 报道称:近期活跃的大规模 REvil 勒索软件活动,被发现会避开俄语等前苏联为主要语言的机器。恶意软件的开发者,似乎不想惹恼俄罗斯当局,否则他们将耗费更长的前期准备时间。目前全球约 1500 家公司受到影响,初步预估其为史上最大规模的勒索软件攻击。有专家建议,可通过在 Windows 计算机上安装俄语虚拟键盘的方式,来防止他们的计算机感染 REvil 恶意软件。

这就很难说明它们和俄罗斯完全没关系了。

微软 12 个月内已向安全专家支付 1360 万美元漏洞赏金

根据目前所有科技厂商公开的赏金金额,微软支付的赏金是最高的。这些赏金是 2020 年 7 月 1 日至 2021 年 6 月 30 日期间发放的,共有 341 名安全专家为 17 个 BUG 悬赏项目提交了 1261 个漏洞。最高的赏金是 20 万美元,是报告微软操作系统虚拟化技术 Hyper-V 中的一个漏洞。平均每个项目平均赏金超过 10000 美元。大多数漏洞报告来自居住在中国、美国和以色列的研究人员。

而在去年同期,微软也支付了差不多的赏金。向 327 名安全研究人员授予了 1370 万美元,用于 15 个漏洞赏金项目的 1226 个漏洞报告。

微软因系统广泛使用而漏洞被挖掘的最多,微软为此花费的资金还是非常值得的。

如果你看一下 新的 Datadog Agent,你可能会注意到大部分代码库是用 Go 编写的,尽管我们用来收集指标的检查仍然是用 Python 编写的。这大概是因为 Datadog Agent 是一个 嵌入了 CPython 解释器的普通 Go 二进制文件,可以在任何时候按需执行 Python 代码。这个过程通过抽象层来透明化,使得你可以编写惯用的 Go 代码而底层运行的是 Python。

在 Go 应用程序中嵌入 Python 的原因有很多:

  • 它在过渡期间很有用;可以逐步将现有 Python 项目的部分迁移到新语言,而不会在此过程中丢失任何功能。
  • 你可以复用现有的 Python 软件或库,而无需用新语言重新实现。
  • 你可以通过加载去执行常规 Python 脚本来动态扩展你软件,甚至在运行时也可以。

理由还可以列很多,但对于 Datadog Agent 来说,最后一点至关重要:我们希望做到无需重新编译 Agent,或者说编译任何内容就能够执行自定义检查或更改现有检查。

嵌入 CPython 非常简单,而且文档齐全。解释器本身是用 C 编写的,并且提供了一个 C API 以编程方式来执行底层操作,例如创建对象、导入模块和调用函数。

在本文中,我们将展示一些代码示例,我们将会在与 Python 交互的同时继续保持 Go 代码的惯用语,但在我们继续之前,我们需要解决一个间隙:嵌入 API 是 C 语言,但我们的主要应用程序是 Go,这怎么可能工作?

介绍 cgo

很多好的理由 说服你为什么不要在堆栈中引入 cgo,但嵌入 CPython 是你必须这样做的原因。cgo 不是语言,也不是编译器。它是 外部函数接口 Foreign Function Interface (FFI),一种让我们可以在 Go 中使用来调用不同语言(特别是 C)编写的函数和服务的机制。

当我们提起 “cgo” 时,我们实际上指的是 Go 工具链在底层使用的一组工具、库、函数和类型,因此我们可以通过执行 go build 来获取我们的 Go 二进制文件。下面是使用 cgo 的示例程序:

package main

// #include <float.h>
import "C"
import "fmt"

func main() {
    fmt.Println("Max float value of float is", C.FLT_MAX)
}

在这种包含头文件情况下,import "C" 指令上方的注释块称为“ 序言 preamble ”,可以包含实际的 C 代码。导入后,我们可以通过“C”伪包来“跳转”到外部代码,访问常量 FLT_MAX。你可以通过调用 go build 来构建,它就像普通的 Go 一样。

如果你想查看 cgo 在这背后到底做了什么,可以运行 go build -x。你将看到 “cgo” 工具将被调用以生成一些 C 和 Go 模块,然后将调用 C 和 Go 编译器来构建目标模块,最后链接器将所有内容放在一起。

你可以在 Go 博客 上阅读更多有关 cgo 的信息,该文章包含更多的例子以及一些有用的链接来做进一步了解细节。

现在我们已经了解了 cgo 可以为我们做什么,让我们看看如何使用这种机制运行一些 Python 代码。

嵌入 CPython:一个入门指南

从技术上讲,嵌入 CPython 的 Go 程序并没有你想象的那么复杂。事实上,我们只需在运行 Python 代码之前初始化解释器,并在完成后关闭它。请注意,我们在所有示例中使用 Python 2.x,但我们只需做很少的调整就可以应用于 Python 3.x。让我们看一个例子:

package main

// #cgo pkg-config: python-2.7
// #include <Python.h>
import "C"
import "fmt"

func main() {
    C.Py_Initialize()
    fmt.Println(C.GoString(C.Py_GetVersion()))
    C.Py_Finalize()
}

上面的例子做的正是下面 Python 代码要做的事:

import sys
print(sys.version)

你可以看到我们在序言加入了一个 #cgo 指令;这些指令被会被传递到工具链,让你改变构建工作流程。在这种情况下,我们告诉 cgo 调用 pkg-config 来收集构建和链接名为 python-2.7 的库所需的标志,并将这些标志传递给 C 编译器。如果你的系统中安装了 CPython 开发库和 pkg-config,你只需要运行 go build 来编译上面的示例。

回到代码,我们使用 Py_Initialize()Py_Finalize() 来初始化和关闭解释器,并使用 Py_GetVersion C 函数来获取嵌入式解释器版本信息的字符串。

如果你想知道,所有我们需要放在一起调用 C 语言 Python API的 cgo 代码都是模板代码。这就是为什么 Datadog Agent 依赖 go-python 来完成所有的嵌入操作;该库为 C API 提供了一个 Go 友好的轻量级包,并隐藏了 cgo 细节。这是另一个基本的嵌入式示例,这次使用 go-python:

package main

import (
    python "github.com/sbinet/go-python"
)

func main() {
    python.Initialize()
    python.PyRun_SimpleString("print 'hello, world!'")
    python.Finalize()
}

这看起来更接近普通 Go 代码,不再暴露 cgo,我们可以在访问 Python API 时来回使用 Go 字符串。嵌入式看起来功能强大且对开发人员友好,是时候充分利用解释器了:让我们尝试从磁盘加载 Python 模块。

在 Python 方面我们不需要任何复杂的东西,无处不在的“hello world” 就可以达到目的:

# foo.py
def hello():
    """
    Print hello world for fun and profit.
    """
    print "hello, world!"

Go 代码稍微复杂一些,但仍然可读:

// main.go
package main

import "github.com/sbinet/go-python"

func main() {
    python.Initialize()
    defer python.Finalize()

    fooModule := python.PyImport_ImportModule("foo")
    if fooModule == nil {
        panic("Error importing module")
    }

    helloFunc := fooModule.GetAttrString("hello")
    if helloFunc == nil {
        panic("Error importing function")
    }

    // The Python function takes no params but when using the C api
    // we're required to send (empty) *args and **kwargs anyways.
    helloFunc.Call(python.PyTuple_New(0), python.PyDict_New())
}

构建时,我们需要将 PYTHONPATH 环境变量设置为当前工作目录,以便导入语句能够找到 foo.py 模块。在 shell 中,该命令如下所示:

$ go build main.go && PYTHONPATH=. ./main
hello, world!

可怕的全局解释器锁

为了嵌入 Python 必须引入 cgo ,这是一种权衡:构建速度会变慢,垃圾收集器不会帮助我们管理外部系统使用的内存,交叉编译也很难。对于一个特定的项目来说,这些问题是否是可以争论的,但我认为有一些不容商量的问题:Go 并发模型。如果我们不能从 goroutine 中运行 Python,那么使用 Go 就没有意义了。

在处理并发、Python 和 cgo 之前,我们还需要知道一些事情:它就是 全局解释器锁 Global Interpreter Lock ,即 GIL。GIL 是语言解释器(CPython 就是其中之一)中广泛采用的一种机制,可防止多个线程同时运行。这意味着 CPython 执行的任何 Python 程序都无法在同一进程中并行运行。并发仍然是可能的,锁是速度、安全性和实现简易性之间的一个很好的权衡,那么,当涉及到嵌入时,为什么这会造成问题呢?

当一个常规的、非嵌入式的 Python 程序启动时,不涉及 GIL 以避免锁定操作中的无用开销;在某些 Python 代码首次请求生成线程时 GIL 就启动了。对于每个线程,解释器创建一个数据结构来存储当前的相关状态信息并锁定 GIL。当线程完成时,状态被恢复,GIL 被解锁,准备被其他线程使用。

当我们从 Go 程序运行 Python 时,上述情况都不会自动发生。如果没有 GIL,我们的 Go 程序可以创建多个 Python 线程,这可能会导致竞争条件,从而导致致命的运行时错误,并且很可能出现分段错误导致整个 Go 应用程序崩溃。

解决方案是在我们从 Go 运行多线程代码时显式调用 GIL;代码并不复杂,因为 C API 提供了我们需要的所有工具。为了更好地暴露这个问题,我们需要写一些受 CPU 限制的 Python 代码。让我们将这些函数添加到前面示例中的 foo.py 模块中:

# foo.py
import sys

def print_odds(limit=10):
    """
    Print odds numbers < limit
    """
    for i in range(limit):
        if i%2:
            sys.stderr.write("{}\n".format(i))

def print_even(limit=10):
    """
    Print even numbers < limit
    """
    for i in range(limit):
        if i%2 == 0:
            sys.stderr.write("{}\n".format(i))

我们将尝试从 Go 并发打印奇数和偶数,使用两个不同的 goroutine(因此涉及线程):

package main

import (
    "sync"

    "github.com/sbinet/go-python"
)

func main() {
    // The following will also create the GIL explicitly
    // by calling PyEval_InitThreads(), without waiting
    // for the interpreter to do that
    python.Initialize()

    var wg sync.WaitGroup
    wg.Add(2)

    fooModule := python.PyImport_ImportModule("foo")
    odds := fooModule.GetAttrString("print_odds")
    even := fooModule.GetAttrString("print_even")

    // Initialize() has locked the the GIL but at this point we don't need it
    // anymore. We save the current state and release the lock
    // so that goroutines can acquire it
    state := python.PyEval_SaveThread()

    go func() {
        _gstate := python.PyGILState_Ensure()
        odds.Call(python.PyTuple_New(0), python.PyDict_New())
        python.PyGILState_Release(_gstate)

        wg.Done()
    }()

    go func() {
        _gstate := python.PyGILState_Ensure()
        even.Call(python.PyTuple_New(0), python.PyDict_New())
        python.PyGILState_Release(_gstate)

        wg.Done()
    }()

    wg.Wait()

    // At this point we know we won't need Python anymore in this
    // program, we can restore the state and lock the GIL to perform
    // the final operations before exiting.
    python.PyEval_RestoreThread(state)
    python.Finalize()
}

在阅读示例时,你可能会注意到一个模式,该模式将成为我们运行嵌入式 Python 代码的习惯写法:

  1. 保存状态并锁定 GIL。
  2. 执行 Python。
  3. 恢复状态并解锁 GIL。

代码应该很简单,但我们想指出一个微妙的细节:请注意,尽管借用了 GIL 执行,有时我们通过调用 PyEval_SaveThread()PyEval_RestoreThread() 来操作 GIL,有时(查看 goroutines 里面)我们对 PyGILState_Ensure()PyGILState_Release() 来做同样的事情。

我们说过当从 Python 操作多线程时,解释器负责创建存储当前状态所需的数据结构,但是当同样的事情发生在 C API 时,我们来负责处理。

当我们用 go-python 初始化解释器时,我们是在 Python 上下文中操作的。因此,当调用 PyEval_InitThreads() 时,它会初始化数据结构并锁定 GIL。我们可以使用 PyEval_SaveThread()PyEval_RestoreThread() 对已经存在的状态进行操作。

在 goroutines 中,我们从 Go 上下文操作,我们需要显式创建状态并在完成后将其删除,这就是 PyGILState_Ensure()PyGILState_Release() 为我们所做的。

释放 Gopher

在这一点上,我们知道如何处理在嵌入式解释器中执行 Python 的多线程 Go 代码,但在 GIL 之后,另一个挑战即将来临:Go 调度程序。

当一个 goroutine 启动时,它被安排在可用的 GOMAXPROCS 线程之一上执行,参见此处 可了解有关该主题的更多详细信息。如果一个 goroutine 碰巧执行了系统调用或调用 C 代码,当前线程会将线程队列中等待运行的其他 goroutine 移交给另一个线程,以便它们有更好的机会运行; 当前 goroutine 被暂停,等待系统调用或 C 函数返回。当这种情况发生时,线程会尝试恢复暂停的 goroutine,但如果这不可能,它会要求 Go 运行时找到另一个线程来完成 goroutine 并进入睡眠状态。 goroutine 最后被安排给另一个线程,它就完成了。

考虑到这一点,让我们看看当一个 goroutine 被移动到一个新线程时,运行一些 Python 代码的 goroutine 会发生什么:

  1. 我们的 goroutine 启动,执行 C 调用并暂停。GIL 被锁定。
  2. 当 C 调用返回时,当前线程尝试恢复 goroutine,但失败了。
  3. 当前线程告诉 Go 运行时寻找另一个线程来恢复我们的 goroutine。
  4. Go 调度器找到一个可用线程并恢复 goroutine。
  5. goroutine 快完成了,并在返回之前尝试解锁 GIL。
  6. 当前状态中存储的线程 ID 来自原线程,与当前线程的 ID 不同。
  7. 崩溃!

所幸,我们可以通过从 goroutine 中调用运行时包中的 LockOSThread 函数来强制 Go runtime 始终保持我们的 goroutine 在同一线程上运行:

go func() {
    runtime.LockOSThread()

    _gstate := python.PyGILState_Ensure()
    odds.Call(python.PyTuple_New(0), python.PyDict_New())
    python.PyGILState_Release(_gstate)
    wg.Done()
}()

这会干扰调度器并可能引入一些开销,但这是我们愿意付出的代价。

结论

为了嵌入 Python,Datadog Agent 必须接受一些权衡:

  • cgo 引入的开销。
  • 手动处理 GIL 的任务。
  • 在执行期间将 goroutine 绑定到同一线程的限制。

为了能方便在 Go 中运行 Python 检查,我们很乐意接受其中的每一项。但通过意识到这些权衡,我们能够最大限度地减少它们的影响,除了为支持 Python 而引入的其他限制,我们没有对策来控制潜在问题:

  • 构建是自动化和可配置的,因此开发人员仍然需要拥有与 go build 非常相似的东西。
  • Agent 的轻量级版本,可以使用 Go 构建标签,完全剥离 Python 支持。
  • 这样的版本仅依赖于在 Agent 本身硬编码的核心检查(主要是系统和网络检查),但没有 cgo 并且可以交叉编译。

我们将在未来重新评估我们的选择,并决定是否仍然值得保留 cgo;我们甚至可以重新考虑整个 Python 是否仍然值得,等待 Go 插件包 成熟到足以支持我们的用例。但就目前而言,嵌入式 Python 运行良好,从旧代理过渡到新代理再简单不过了。

你是一个喜欢混合不同编程语言的多语言者吗?你喜欢了解语言的内部工作原理以提高你的代码性能吗?


via: https://www.datadoghq.com/blog/engineering/cgo-and-python/

作者:Massimiliano Pippi 译者:Zioyi 校对:wxy

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