Skip to content

Latest commit

 

History

History
1021 lines (673 loc) · 54 KB

File metadata and controls

1021 lines (673 loc) · 54 KB

十六、打包 Python

Python是机器学习最流行的编程语言。 再加上我们日常生活中机器学习的激增,在边缘设备上运行 Python 的愿望越来越强烈也就不足为奇了。 即使在这个转换程序和 WebAssembly 的时代,打包要部署的 Python 应用仍然是一个未解决的问题。 在本章中,您将了解将 Python 模块捆绑在一起有哪些选择,以及何时使用一种方法而不是另一种方法。

我们首先回顾一下当今 Python 打包解决方案的起源,从内置标准distutils到它的继任者setuptools。 接下来,我们先研究pip包管理器,然后转到适用于 Python 虚拟环境的venv,然后是主流的通用跨平台解决方案conda。 最后,我将向您展示如何使用 Docker 将 Python 应用与其用户空间环境捆绑在一起,以便快速部署到云中。

由于 Python 是一种解释型语言,因此不能像使用 Go 这样的语言那样将程序编译成独立的可执行文件。 这使得部署 Python 应用变得复杂。 运行 Python 应用需要安装一个 Python 解释器和几个运行时依赖项。 这些要求需要代码兼容,应用才能工作。 这需要对软件组件进行精确的版本控制。 解决这些部署问题就是 Python 打包的意义所在。

在本章中,我们将介绍以下主要主题:

  • 追溯 Python 打包的起源
  • 使用pip安装 Python 包
  • 使用venv管理 Python 虚拟环境
  • 使用conda安装预编译二进制文件
  • 使用 Docker 部署 Python 应用

技术要求

要按照示例操作,请确保在基于 Linux 的主机系统上安装了以下软件包:

  • Python:Python3 解释器和标准库
  • pip:Python 3 的软件包安装程序
  • venv:用于创建和管理轻量级虚拟环境的 Python 模块
  • Miniconda:conda包和虚拟环境管理器的最小安装程序
  • Docker:用于在容器内构建、部署和运行软件的工具

我推荐本章使用 Ubuntu20.04LTS 或更高版本。 尽管 Ubuntu20.04LTS 运行在 Raspberry Pi4 上,我仍然更喜欢在 x86-64 台式 PC 或笔记本电脑上进行开发。 我选择 Ubuntu 作为我的开发环境,因为发行版维护人员使 Docker 保持最新。 Ubuntu20.04LTS 还附带了已经安装的 Python3 和pip,因为 Python 在整个系统中被广泛使用。 不要卸载python3,否则会使 Ubuntu 无法使用。 要在 Ubuntu 上安装venv,请输入以下命令:

$ sudo apt install python3-venv

重要音符

在您读到conda一节之前,不要安装 Miniconda,因为它会干扰前面依赖于系统 Python 安装的pip练习。

现在,让我们安装 Docker。

获取 Docker

要在 Ubuntu 20.04 LTS 上安装 Docker,需要完成以下步骤:

  1. 更新程序包存储库:

    $ sudo apt update
  2. 安装 Docker:

    $ sudo apt install docker.io
  3. 启动 Docker 守护进程并使其在引导时启动:

    $ sudo systemctl enable --now docker
  4. 将您自己添加到docker组:

    $ sudo usermod -aG docker <username>

用您用户名替换最后步中的<username>。 我建议您创建自己的 Ubuntu 用户帐户,而不是使用默认的ubuntu用户帐户,后者应该保留用于管理任务。

追溯 Python 打包的起源

Python 打包环境是失败尝试和废弃工具的巨大墓地。 依赖项管理方面的最佳实践经常在 Python 社区内发生变化,今年推荐的解决方案可能在第二年就不可能实现了。 在研究此主题时,请记住查看信息发布的时间,不要相信任何可能过时的建议。

大多数 Python 库使用distutilssetuptools分发,包括在Python 包索引(PyPI)上找到的所有包。 这两种分发方法都依赖于setup.py项目规范文件,Python(pip)的包安装程序使用该规范文件来安装包。 pip还可以在安装 项目后生成或冻结精确的依赖项列表。 此可选的requirements.txt文件由pipsetup.py一起使用,以确保项目安装是可重复的。

distutils

distutils是 Python 最初的打包系统。 从 Python2.0 开始,它就包含在 Python 标准库中。 distutils提供可由setup.py脚本导入的同名 Python 包。 尽管distutils仍然与 Python 一起发布,但它缺乏一些基本特性,因此现在极力反对直接使用distutilssetuptools已经成为它的首选替代品。

虽然distutils可能会继续为简单的项目工作,但社区已经离开了。 今天,distutils之所以存活下来,主要是因为遗留的原因。 当distutils是镇上唯一的游戏时,许多 Python 库首次发布回。 现在将它们移植到setuptools需要付出相当大的努力,而且可能会破坏现有用户。

设置工具

setuptools通过添加对复杂构造的支持来扩展distutils,这些构造使得较大的应用更易于分发。 它已经成为 Python 社区内事实上的打包系统。 与distutils类似,setuptools提供了一个同名的 Python 包,您可以将其导入到您的setup.py脚本中。 distributesetuptools的一个雄心勃勃的分支,最终合并回setuptools 0.7,巩固了setuptools作为 Python 打包的最终选择的地位。

setuptools引入了名为easy_install(现已弃用)的命令行实用程序和名为pkg_resources的 Python 包,用于运行时包发现和访问资源文件。 setuptools还可以生成作为其他可扩展包(例如,框架和应用)插件的包。 您可以通过在setup.py脚本中注册入口点来实现这一点,以便为要导入的另一个主程序包注册入口点。

术语分布在 Python 上下文中的含义与不同。 发行版是用于发行发行版的包、模块和其他资源文件的版本化存档。 版本是在给定时间点拍摄的 Python 项目的版本快照。 更糟糕的是,术语分发超载,经常被 Pythonistas 交替使用。 出于我们的目的,假设发行版是您下载的内容,包是安装和导入的模块。

削减一个发行版可能会导致多个发行版,例如一个源代码发行版和一个或多个构建的发行版。 对于不同的平台,可以有不同的构建发行版,例如包含 Windows 安装程序的发行版。 术语构建的发行版表示在安装之前不需要任何构建步骤。 这并不一定意味着预编译。 例如,一些构建的分发格式(如Wheels(.whl))会排除编译后的 Python 文件。 包含编译的扩展的构建发行版称为二进制发行版

扩展模块是用 C 或 C++编写的 Python 模块。 每个扩展模块向下编译为单个动态加载库,例如 Linux 上的共享对象(.so)和 Windows 上的 DLL(.pyd)。 这与纯模块形成对比,纯模块必须完全用 Python 编写。 由setuptools引入的 Egg(.egg)构建的分发格式支持纯模块和扩展模块。 由于当 Python 解释器在运行时导入模块时,Python 源代码(.py)文件会向下编译为字节码(.pyc)文件,因此您可以看到构建的分发格式(如 Wheels)如何排除预编译的 Python 文件。

setup.py

假设您正在用 Python 开发一个小程序,可能是查询 远程 rest API 并将响应数据保存到本地 SQL 数据库的程序。 如何将您的程序及其依赖项打包在一起以进行部署? 首先定义setuptools可以用来安装程序的setup.py脚本。 使用setuptools进行部署是迈向更精细的自动化部署方案的第一步。

即使您的程序足够小,可以轻松地放入单个模块中,它也有可能不会保持很长时间。 假设您的程序由名为follower.py的单个文件组成,如下所示:

$ tree follower
follower
└── follower.py

然后,您可以通过将follower.py拆分为三个单独的模块,并将它们放入也名为follower的嵌套目录中,将此模块转换为包:

$ tree follower/
follower/
└── follower
    ├── fetch.py
    ├── __main__.py
    └── store.py

__main__.py模块是程序开始的地方,因此它主要包含顶级的、面向用户的功能。 fetch.py模块包含向远程 rest API 发送 HTTP 请求的函数,store.py模块包含将响应数据保存到本地 SQL 数据库的函数。 要将此程序包作为脚本运行,您需要将-m选项传递给 Python 解释器,如下所示:

$ PYTHONPATH=follower python -m follower

环境变量PYTHONPATH指向目标项目的包目录所在的目录。 -m选项后的follower参数告诉 Python 运行属于follower包的__main__.py模块。 在这样的项目目录中嵌套包目录为您的程序成长为一个更大的应用铺平了道路,该应用由多个包组成,每个包都有自己的命名空间。

项目的各个部分都已就位后,我们现在可以创建 一个最小的setup.py脚本,setuptools可以使用该脚本对其进行打包和部署:

from setuptools import setup
setup(
    name='follower',
    version='0.1',
    packages=['follower'],
    include_package_data=True,
    install_requires=['requests', 'sqlalchemy']
)

install_requires参数是需要自动安装以使项目在运行时工作的外部依赖项列表。 请注意,在我的示例中,我没有指定需要这些依赖项的哪些版本或从哪里获取它们。 我只要求在外观和行为上与requestssqlalchemy相似的库。 像这样将策略与实现分离使您可以轻松地将依赖项的官方 PyPI 版本与您自己的版本互换,以防您需要修复 bug 或添加特性。 在依赖项声明中添加可选的版本说明符是可以的,但在setup.py中硬编码分发 URL(如dependency_links)原则上是错误的。

packages参数告诉setuptools随项目版本一起分发的树内包。 因为每个包都定义在父项目目录的自己的子目录中,所以在这种情况下提供的唯一包是follower。 我在这个发行版中包含了数据文件和我的 Python 代码。 为此,需要将include_package_data参数设置为True,以便setuptools查找MANIFEST.in文件并安装其中列出的所有文件。 以下是MANIFEST.in文件的内容:

include data/events.db

如果data目录包含我们想要包括的数据的嵌套目录,我们可以使用recursive-include全局处理所有这些目录及其内容:

recursive-include data *

以下是最终的目录布局:

$ tree follower
follower
├── data
│   └── events.db
├── follower
│   ├── fetch.py
│   ├── __init__.py
│   └── store.py
├── MANIFEST.in
└── setup.py

setuptools擅长构建和分发依赖于其他包的 Python 包。 它能够做到这一点,这要归功于入口点和依赖项声明等特性,而这些特性在distutils中根本不存在。 setuptools可以很好地与pip配合使用,并且会定期发布setuptools的新版本。 创建控制盘构建分发格式是为了替换setuptools最初的鸡蛋格式。 这一努力在很大程度上取得了成功,因为增加了一个广受欢迎的setuptools扩展来构建轮子,以及pip对安装轮子的巨大支持。

使用 pip 安装 Python 包

现在您了解了如何在setup.py脚本中定义项目的依赖项。 但是,如何安装这些依赖项呢? 如何升级依赖项或在找到更好的依赖项时替换它? 您如何决定何时删除不再需要的依赖项是安全的? 管理项目依赖关系是一件棘手的事情。 幸运的是,Python 附带了一个名为pip的工具,可以提供帮助,特别是在项目的早期阶段。

pip最初的 1.0 版本于 2011 年 4 月 4 日发布,几乎与 Node.js 和npm的发布时间相同。 在它成为pip之前,该工具被命名为pyinstallpyinstall创建于 2008 年,作为当时与setuptools捆绑在一起的easy_install的替代方案。 easy_install现在已弃用,setuptools建议改用pip

由于pip包含在 Python 安装程序中,并且您可以在系统上安装多个版本的 Python(例如,2.7 和 3.8),因此它有助于了解您运行的是哪个版本的pip

$ pip --version

如果在您的系统上没有找到pip可执行文件,这可能意味着您使用的是 Ubuntu 20.04LTS 或更高版本,并且没有安装 Python2.7。 那很好。 在本节的其余部分中,我们只用pip3代替pip,用python3代替python

$ pip3 --version

如果有python3但没有pip3可执行文件,则按照基于 Debian 的发行版(如 Ubuntu)所示进行安装:

$ sudo apt install python3-pip

pip将软件包安装到名为site-packages的目录中。 要查找site-packages目录的位置,请运行以下命令:

$ python3 -m site | grep ^USER_SITE

重要音符

注意,从这里开始显示的pip3python3命令只对 Ubuntu 20.04LTS 或更高版本是必需的,它不再随安装了 Python2.7 的版本一起提供。 大多数 Linux 发行版仍然附带pippython可执行文件,因此如果您的 Linux 系统已经提供了pippython命令,请使用这些命令。

要获取系统上已安装的软件包的列表,请使用以下命令:

$ pip3 list

该列表显示pip只是另一个 Python 包,因此您可以使用pip进行自我升级,但我建议您不要这样做,至少从长远来看不是这样。 我将在下一节介绍虚拟环境时解释原因。

要获取安装在site-packages目录中的软件包列表,请使用以下命令:

$ pip3 list --user

此列表应该为空或比系统包列表短得多。

回到上一节中的示例项目。 cdsetup.py所在的父follower目录。 然后运行以下命令:

$ pip3 install --ignore-installed --user .

pip将使用setup.py获取install_requires声明的包并将其安装到您的site-packages目录。 --user选项指示pip将软件包安装到您的site-packages目录中,而不是全局安装。 --ignore-installed选项强制pip将系统上已经存在的所有必需软件包重新安装到site-packages,这样就不会丢失依赖项。 现在再次列出site-packages目录中的所有软件包:

$ pip3 list --user
Package    Version  
---------- ---------
certifi    2020.6.20
chardet    3.0.4    
follower   0.1      
idna       2.10     
requests   2.24.0   
SQLAlchemy 1.3.18   
urllib3    1.25.10

这一次,您应该看到requestsSQLAlchemy都在包列表中。

要查看您可能刚刚安装的SQLAlchemy软件包的详细信息,请发出以下命令:

$ pip3 show sqlalchemy

显示的详细信息包含RequiresRequired-by字段。 这两个都是相关软件包的列表。 您可以使用这些字段中的值和对pip show的连续调用来跟踪项目的依赖关系树。 但是,使用名为pipdeptree的命令行工具pip install并使用它可能会更容易一些。

Required-by字段变为空时,这是一个很好的指示,表明现在可以安全地从系统中卸载软件包。 如果没有其他软件包依赖于已删除软件包的Requires字段中的软件包,那么也可以安全地卸载这些软件包。 以下是如何使用pip卸载sqlalchemy

$ pip3 uninstall sqlalchemy -y

尾部的-y取消确认提示。 要一次卸载多个软件包,只需在-y前添加更多软件包名称。 这里省略了--user选项,因为pip足够智能,可以在全局安装 软件包时首先从site-packages卸载。

有时,您需要一个用于某种目的或利用特定技术的包,但您不知道它的名称。 您可以使用pip从命令行对 PyPI 执行关键字搜索,但这种方法通常会产生太多结果。 在 PyPI 网站(https://pypi.org/search/)上搜索包要容易得多,它允许您按各种分类器过滤结果。

要求.txt

pip install将安装最新发布的软件包版本,但通常您 希望安装您知道可以与您的项目代码一起使用的软件包的特定版本。 最终,您会希望升级项目的依赖项。 但是在 我向您展示如何做到这一点之前,我首先需要向您展示如何使用pip freeze来修复 依赖项。

要求文件允许您准确地指定应该为您的项目安装哪些包和版本pip。 按照惯例,项目需求文件总是命名为requirements.txt。 需求文件的内容只是枚举项目依赖项的pip install参数列表。 这些依赖项的版本是精确的,因此当有人尝试重新生成和部署您的项目时,不会出现意外情况。 最好将requirements.txt文件添加到项目的 repo 中,以确保构建可重现。

返回到我们的follower项目,既然我们已经安装了所有依赖项并验证了代码可以按预期工作,我们现在就可以冻结pip为我们安装的包的最新版本了。 pip有一个freeze命令,用于输出已安装的软件包及其版本。 您可以将此命令的输出重定向到requirements.txt文件:

$ pip3 freeze --user > requirements.txt

现在您有了一个requirements.txt文件,克隆您的项目的人员可以使用-r选项和需求文件的名称安装它的所有依赖项:

$ pip3 install --user -r requirements.txt

自动生成的需求文件格式默认为精确版本匹配(==)。 例如,像requests==2.22.0这样的行告诉pip要安装的requests版本必须正好是 2.22.0。 您还可以在需求文件中使用其他版本说明符,例如最低版本(>=)、版本排除(!=)和最高版本(<=)。 最低版本(>=)匹配大于或等于右侧的任何版本。 版本排除(!=)匹配除右侧以外的任何版本。 最高版本匹配任何小于或等于右侧的版本。

您可以在一行中组合多个版本说明符,使用逗号 分隔它们:

requests >=2.22.0,<3.0

pip安装需求文件中指定的包时,默认行为是从 PyPI 获取它们。 您可以通过在requirements.txt文件的顶部添加如下行,用替代的 https://pypi.org/simple/包索引的 URL(Python)覆盖 PyPI 的 URL:

--index-url http://pypi.mydomain.com/mirror

站起来并维护您自己的私有 PyPI 镜像所需的努力并不是无关紧要的。 当您需要做的只是修复一个 bug 或向项目依赖项中添加一个特性时,覆盖包源比覆盖整个包索引更有意义。

给小费 / 翻倒 / 倾覆

NVIDIA Jetson Nano 的 Jetpack SDK 4.3 版基于 Ubuntu 的 18.04 LTS 发行版。 Jetpack SDK 为 Nano 的 NVIDIA Maxwell 128 CUDA 内核增加了广泛的软件支持,如图形处理器驱动程序和其他运行时组件。 您可以使用pip从 NVIDIA 的程序包索引为 TensorFlow 安装 GPU 加速轮:

$ pip install --user --extra-index-url https://developer.download.nvidia.com/compute/redist/cn/v43 tensorflow-gpu==2.0.0+nv20.1

我在前面提到过,在setup.py内硬编码分发 URL 是多么错误。 您可以在需求文件中使用-e参数表单覆盖各个 包源:

-e git+https://github.com/myteam/flask.git#egg=flask

在本例中,我指示pip从我的团队的 GitHub 分支pallets/flask.git获取flask包源。 -e参数形式还采用 Git 分支名称、提交散列或标记名:

-e git+https://github.com/myteam/flask.git@master
-e git+https://github.com/myteam/flask.git@5142930ef57e2f0ada00248bdaeb95406d18eb7c
-e git+https://github.com/myteam/flask.git@v1.0

使用pip将项目的依赖项升级到发布在 PyPI 上的最新版本非常简单:

$ pip3 install --upgrade –user -r requirements.txt

使用pip:requirements.txt验证安装后,确认最新版本的依赖项不会破坏您的项目,然后可以将它们写回需求文件:

$ pip3 freeze --user > requirements.txt

确保冻结没有覆盖需求文件中的任何覆盖或特殊版本 处理。 撤消所有错误,并将更新后的requirements.txt文件提交给版本控制。

在某些情况下,升级项目依赖项会导致代码中断。 新的包版本可能会带来与您的项目的倒退或不兼容。 需求文件格式提供了处理这些情况的语法。 假设您一直在项目中使用版本 2.22.0 的requests,并且发布了版本 3.0。 根据语义版本控制的实践,递增主版本号表示requests的 3.0 版包含对该库 API 的破坏性更改。 您可以这样表达新版本的要求:

requests ~= 2.22.0

兼容版本说明符(~=)依赖于语义版本控制。 兼容表示大于或等于右侧且小于下一个版本主号(例如,>= 1.1== 1.*)。 您已经看到我清楚地表达了requests的这些相同版本要求,如下所示:

requests >=2.22.0,<3.0

如果一次只开发一个 Python 项目,这些pip依赖项管理技术就可以很好地工作。 但是,您可能会使用同一台计算机同时处理多个 Python 项目,每个项目都可能需要不同版本的 Python 解释器。 对多个项目仅使用pip的最大问题是,它会将所有包安装到特定版本的 Python 的同一用户site-packages目录中。 这使得将依赖项从一个项目隔离到下一个项目变得非常困难。

我们很快就会看到,pip与 Docker 很好地结合在一起,可以部署 Python 应用。 您可以将pip添加到基于 Buildroot 或 Yocto 的 Linux 映像中,但这只能实现快速的板载实验。 像pip这样的 Python 运行时包安装程序不适合于 Buildroot 和 Yocto 环境,在这些环境中,您希望在构建时定义嵌入式 Linux 映像的全部内容。 pip在像 Docker 这样的集装箱化环境中工作得很好,在这些环境中,构建时间和运行时之间的界限通常是模糊的。

第 7 章使用 Yocto进行开发中,您了解了meta-python层中可用的 Python 模块,以及如何为您自己的应用定义自定义层。 您可以使用pip freeze生成的requirements.txt文件通知从meta-python中为您自己的层配方选择从属关系。 Buildroot 和 Yocto 都以系统范围的方式安装 Python 包,因此我们接下来要讨论的虚拟环境技术不适用于嵌入式 Linux 版本。 但是,它们确实使生成准确的requirements.txt文件变得更容易。

使用 venv 管理 Python 虚拟环境

虚拟环境是自包含的目录树,包含用于特定版本 Python 的 Python 解释器、用于管理项目依赖性的pip可执行文件以及本地site-packages目录。 在虚拟环境之间切换会使 shell 误以为唯一可用的 Python 和pip可执行文件是活动虚拟环境中存在的可执行文件。 最佳实践要求您为每个项目创建不同的虚拟环境。 这种形式的隔离解决了依赖于同一包的不同版本的两个项目的问题。

虚拟环境对于 Python 来说并不新鲜。 Python 安装的系统范围的性质决定了它们是必需的。 除了使您能够安装同一软件包的不同版本之外,虚拟环境还为您提供了一种运行多个版本的 Python 解释器的简单方法。 有几个选项可用于管理 Python 虚拟环境。 仅仅两年前还非常流行的工具(pipenv)在撰写本文时就已经没用了。 与此同时,出现了一个新的竞争者(poetry),Python3 对虚拟环境的内置支持(venv)开始被更多人采用。

Venv自 3.3 版(2012 年发布)以来一直随 Python 提供。 因为它只与 Python3 安装捆绑在一起,所以venv与需要 Python2.7 的项目不兼容。 既然对 Python2.7 的支持已于 2020 年 1 月 1 日正式结束,那么 Python3 的限制就不再那么令人担忧了。 Venv基于流行的virtualenv工具,该工具仍在维护,并且在 PyPI 上可用。 如果您有一个或多个项目由于这样或那样的原因仍然需要 Python2.7,那么您可以使用virtualenv而不是venv来处理这些项目。

默认情况下,venv安装系统中找到的最新版本的 Python。 如果您的系统上有多个版本的 Python,您可以通过运行python3或在创建每个虚拟环境时想要的任何版本(Python Tutorialhttps://docs.python.org/3/tutorial/venv.html)来选择特定的 Python 版本。 使用最新版本的 Python 进行开发通常适用于新开发的项目,但对于大多数遗留软件和企业软件来说是不可接受的。 我们将使用您的 Ubuntu 系统附带的 Python3 版本来创建和使用 虚拟环境。

要创建虚拟环境,首先要确定要将其放在哪里,然后使用目标目录路径将venv模块作为脚本运行:

  1. 确保您的 Ubuntu 系统上安装了venv

    $ sudo apt install python3-venv
  2. 为您的项目创建新目录:

    $ mkdir myproject
  3. 切换到新目录:

    $ cd myproject
  4. 在名为venv

    $ python3 -m venv ./venv

    的子目录中创建虚拟环境

现在您已经创建了一个虚拟环境,下面是如何激活和验证它的:

  1. 如果尚未切换到项目目录,请切换到项目目录:

    $ cd myproject
  2. 检查系统的pip3可执行文件的安装位置:

    $ which pip3
    /usr/bin/pip3
  3. 激活项目的虚拟环境:

    $ source ./venv/bin/activate
  4. 检查项目的pip3可执行文件的安装位置:

    (venv) $ which pip3
    /home/frank/myproject/venv/bin/pip3
  5. 列出随虚拟环境一起安装的软件包:

    (venv) $ pip3 list
    Package       Version
    ------------- -------
    pip           20.0.2 
    pkg-resources 0.0.0 
    setuptools    44.0.0

如果从虚拟环境中输入which pip命令,您将看到pip现在指向与pip3相同的可执行文件。 在激活虚拟环境之前,pip可能没有指向任何东西,因为 Ubuntu20.04LTS 不再安装 Python2.7。 对于pythonpython3也可以这样说。 现在,在虚拟环境中运行pippython时,可以省略3

接下来,让我们将一个名为hypothesis的基于属性的测试库安装到现有的虚拟环境中:

  1. 如果尚未切换到项目目录,请切换到项目目录:

    $ cd myproject
  2. 如果项目的虚拟环境尚未处于活动状态,请重新激活:

    $ source ./venv/bin/activate
  3. 安装hypothesis软件包:

    (venv) $ pip install hypothesis
  4. 列出当前安装在虚拟环境中的软件包:

    (venv) $ pip list
    Package          Version
    ---------------- -------
    attrs            19.3.0 
    hypothesis       5.16.1 
    pip              20.0.2 
    pkg-resources    0.0.0 
    setuptools       44.0.0 
    sortedcontainers 2.2.2

请注意,除了hypothesisattrssortedcontainers之外,列表中还添加了两个新的包。 hypothesis取决于这两个套餐。 假设您有另一个依赖于版本 18.2.0 而不是版本 19.3.0 的sortedcontainers的 Python 项目。 这两个版本将是不兼容的,因此彼此冲突。 虚拟环境允许您安装同一软件包的两个版本, 两个项目的不同版本。

您可能已经注意到,退出项目目录并不会停用其虚拟环境。 别担心。 停用虚拟环境非常简单,如下所示:

(venv) $ deactivate
$

这会将您带回全局系统环境,您必须再次输入python3pip3。 现在,您已经了解了开始使用 Python 虚拟环境所需了解的所有内容。 现在使用 Python 进行开发时,在虚拟环境之间创建和切换是很常见的做法。 隔离环境使跨多个项目跟踪和管理依赖项变得更容易。 将 Python 虚拟环境部署到嵌入式 Linux 设备上进行生产意义不大 ,但是仍然可以使用名为dh-virtualenv(https://github.com/spotify/dh-virtualenv)的 Debian 打包工具完成。

使用 conda 安装预编译二进制文件

Conda是一个包和虚拟环境管理系统,由 PyData 社区的Anaconda发行版软件使用。 蟒蛇发行版包括 Python 以及几个难以构建的开源项目(如 PyTorch 和 TensorFlow)的二进制文件。 conda可以在没有非常大的的完整蟒蛇发行版或仍然超过 256MB 的最小的Miniconda发行版的情况下安装。

尽管它是在pip之后不久为 Python 创建的,但conda已经演变为 一个通用的包管理器,如 APT 或 Homebrew。 现在,它可以用来打包和分发任何语言的软件。 因为conda下载预编译的二进制文件,所以安装 Python 扩展模块轻而易举。 conda 的另一个大卖点是它是跨平台的,完全支持 Linux、MacOS、 和 Windows。

除了包管理,conda还是一个成熟的虚拟环境管理器。 Conda虚拟环境具有我们期望从 Pythonvenv环境中获得的所有好处,甚至更多。 与venv类似,conda允许您使用pip将包从 PyPI 安装到项目的本地site-packages目录中。 如果您愿意,您可以使用conda自己的包管理功能来安装来自不同频道的包。频道是由蟒蛇和其他软件发行版提供的包提要。

环境管理

venv不同,conda的虚拟环境管理器可以轻松地处理多个版本的 Python,包括 Python2.7。您需要在您的 Ubuntu 系统上安装 Miniconda 才能完成以下练习。您希望在虚拟环境中使用 Miniconda 而不是 Anaconda,因为 Anaconda 环境附带了许多预先安装的软件包,其中许多您永远不会需要。Miniconda 环境被精简,如果有必要,您可以轻松地安装 Anaconda 的任何软件包。

要在 Ubuntu 20.04 LTS 上安装和更新 Miniconda,请执行以下操作:

  1. 下载 Miniconda:

    $ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
  2. 发帖主题:Re:Колибри0.7.0

  3. 更新根环境中安装的所有软件包:

    (base) $ conda update --all

全新的 Miniconda 安装附带conda和一个根环境,其中包含安装的 Python 解释器和一些基本软件包。 默认情况下,conda的根环境的pythonpip可执行文件安装到您的主目录中。conda根环境称为base。您可以通过发出以下命令来查看其位置以及任何其他可用conda环境的位置:

(base) $ conda env list

在创建您自己的conda环境之前,请验证此根环境:

  1. 安装 Miniconda 后打开新的 shell。

  2. 检查根环境的python可执行文件的安装位置:

    (base) $ which python
  3. 检查 Python 的版本:

    (base) $ python --version
  4. 检查根环境的pip可执行文件的安装位置:

    (base) $ which pip
  5. 检查pip的版本:

    (base) $ pip --version
  6. 列出根环境中安装的软件包:

    (base) $ conda list

接下来,创建并使用您自己的名为py377conda环境:

  1. 创建名为py377

    (base) $ conda create --name py377 python=3.7.7

    的新虚拟环境

  2. 激活您的新虚拟环境:

    (base) $ source activate py377
  3. 检查您的环境的python可执行文件的安装位置:

    (py377) $ which python
  4. 检查 Python 版本是否为 3.7.7:

    (py377) $ python --version
  5. 列出您的环境中安装的软件包:

    (py377) $ conda list
  6. 停用您的环境:

    (py377) $ conda deactivate

使用conda创建安装了 Python 2.7 的虚拟环境就像 一样简单:

(base) $ conda create --name py27 python=2.7.17

再次查看您的conda环境,查看py377py27现在是否出现在列表中:

(base) $ conda env list

最后,让我们删除py27环境,因为我们不会使用它:

(base) $ conda remove --name py27 –all

既然您已经了解了如何使用conda来管理虚拟环境,那么让我们使用它来管理那些环境中的包。

套餐管理

因为conda支持虚拟环境,所以我们可以使用pip以隔离的方式管理从一个项目到另一个项目的 Python 依赖关系,就像我们使用venv一样。 作为一个通用的包管理器,conda拥有自己的管理依赖项的工具。 我们知道,conda list列出了conda在活动虚拟环境中安装的所有软件包。 我还提到了conda对包提要的使用,称为频道:

  1. 您可以通过输入以下命令获取配置为从中提取的通道 URL 列表conda

    (base) $ conda info
  2. 在继续下一步之前,让我们重新激活您在上一个练习中创建的py377虚拟环境:

    (base) $ source activate py377
    (py377) $ 
  3. 现在大多数 Python 开发都是在 Jupyter 笔记本中进行的,所以让我们先安装这些包:

    (py377) $ conda install jupyter notebook
  4. 提示时输入y。 这将安装jupyternotebook包以及它们的所有依赖项。 当您输入conda list时,您将看到已安装软件包的列表比以前长得多。 现在,让我们再安装一些计算机视觉项目所需的 Python 包:

    (py377) $ conda install opencv matplotlib
  5. 再次出现提示时,输入y。 这一次,安装的依赖项数量 较少。 opencvmatplotlib都依赖于numpy,因此conda会自动安装该软件包,而无需您指定它。 如果要指定旧版本的opencv,可以通过以下方式安装所需版本的软件包 :

    (py377) $ conda install opencv=3.4.1
  6. 然后,conda将尝试解析此依赖项的活动环境。 由于此活动虚拟环境中安装的其他包都不依赖于opencv,因此目标版本很容易解决。 如果是这样,那么您可能会遇到 软件包冲突,重新安装将失败。 解算后,conda将在降级opencv及其依赖项之前提示您。 输入yopencv降级到版本 3.4.1。

  7. 现在,假设您改变了主意,或者发布了更新版本的opencv,解决了您以前关心的问题。 这就是如何将opencv升级到蟒蛇发行版提供的最新版本:

    (py377) $ conda update opencv
  8. 这一次,conda将提示您是否要为最新版本更新opencv及其从属关系。 这次,输入n取消包更新。 与单独更新软件包相比,一次更新活动虚拟环境中安装的所有软件包通常更容易:

    (py377) $ conda update --all
  9. 删除个安装的软件包也很简单:

    (py377) $ conda remove jupyter notebook
  10. conda删除jupyternotebook时,它也会删除它们的所有悬挂依赖项。 悬空依赖关系是指没有其他已安装软件包依赖的已安装软件包。 与大多数通用包管理器一样,conda不会删除其他已安装包 仍然依赖的任何依赖项。

  11. 有时,您可能不知道要安装的软件包的确切名称。 亚马逊提供了一个用于 Python 的 AWS SDK,名为 Boto。 与许多 Python 库一样,有适用于 Python 2 的 Boto 版本和适用于 Python 3 的较新版本(Boto3)。要在 Anaconda 中搜索名称中包含单词boto的包,请输入以下命令:

```sh
(py377) $ conda search '*boto*'
```
  1. 您应该在搜索结果中看到boto3botocore。 在撰写本文时,蟒蛇上可用的最新版本boto3是 1.13.11。 要查看该特定版本boto3的详细信息,请输入以下命令:
```sh
(py377) $ conda info boto3=1.13.11
```
  1. 软件包详细信息显示,boto3版本 1.13.11 依赖于botocore(botocore >=1.16.11,<1.17.0),因此安装boto3可以同时获得这两个版本。

现在,假设您已经在 Jupyter 笔记本中安装了开发 OpenCV 项目所需的所有软件包。 您如何与他人共享这些项目需求,以便他们可以重新创建您的工作环境? 答案可能会让你大吃一惊:

  1. 将活动虚拟环境导出为 YAML 文件:

    (py377) $ conda env export > my-environment.yaml
  2. pip freeze生成的需求列表非常相似,conda导出的 YAML 是虚拟环境中安装的所有包及其固定版本的列表。 从环境文件创建conda虚拟环境需要-f选项和文件名:

    $ conda env create -f my-environment.yaml
  3. 环境名称包含在导出的 YAML 中,因此创建环境不需要--name选项。 现在,无论谁从my-environment.yaml创建虚拟环境,当他们发出conda env list命令时,都会在他们的环境列表中看到py377

conda是开发人员武器库中的一个非常强大的工具。 通过将通用软件包安装与虚拟环境相结合,它提供了一个引人入胜的部署案例。 conda实现了许多与 Docker(接下来)相同的目标,但没有使用容器。 就 Python 而言,它比 Docker 更有优势,因为它专注于数据科学社区。 由于领先的机器学习框架(如 PyTorch 和 TensorFlow)主要基于 CUDA,因此通常很难找到 GPU 加速的二进制代码。 conda通过提供包的多个预编译二进制版本 来解决此问题。

conda虚拟环境导出到 YAML 文件以安装在其他计算机上提供了另一种部署选项。 这个解决方案在数据科学界很受欢迎,但它在嵌入式 Linux 的生产中不起作用。 conda不是 Yocto 支持的三个包管理器之一。 即使可以选择conda,在 Linux 映像上容纳 Minconda 所需的存储空间也不太适合大多数资源受限的嵌入式系统。

如果您的开发板具有 NVIDIA GPU(如 NVIDIA Jetson 系列),那么您确实希望使用conda进行板载开发。 幸运的是,有一个名为Miniforge(https://github.com/conda-forge/miniforge)的conda安装程序可以在像 Jetsons 这样的 64 位 ARM 机器上运行。 有了condaonboard,您就可以安装jupyternumpypandasscikit-learn以及大多数其他流行的 Python 数据科学库。

使用 Docker 部署 Python 应用

Docker提供了另一种将 Python 代码与用其他语言编写的软件捆绑在一起的方法。 Docker 背后的理念是,不是将应用打包并安装到预配置的服务器环境中,而是与应用及其所有运行时依赖项一起构建和发布容器映像。 容器镜像更像虚拟环境而不是虚拟机。 虚拟机是包括内核和操作系统的完整的系统映像。 容器映像是一个最小的用户空间环境,它只附带运行应用所需的二进制文件。

虚拟机在模拟硬件的管理程序上运行。 容器直接在主机操作系统之上运行。 与虚拟机不同,容器能够在不使用硬件仿真的情况下共享相同的操作系统和内核。 相反,它们依靠 Linux 内核的两个特殊特性进行隔离:名称空间cgroup。 Docker 并没有发明容器技术,但他们是第一个制造出便于使用的工具的人。 既然 Docker 使构建和部署容器镜像变得如此简单,的疲惫借口在我的机器上就不再有效了

Dockerfile 的解剖

Dockerfile描述 Docker 镜像的内容。 每个 Dockerfile 都包含一组指定要使用的环境和要运行的命令的指令。 我们将使用现有的 Dockerfile 作为项目模板,而不是从头开始编写 Dockerfile。 此 Dockerfile 为一个非常简单的 Flask Web 应用生成一个 Docker 图像,您可以扩展该应用以满足您的需要。 Docker 镜像构建在 Alpine Linux 之上,这是一个非常纤细的 Linux 发行版,通常用于容器部署。 除了 Flask,Docker 镜像还包含 uWSGI 和 nginx,以获得更好的性能。

首先将 Web 浏览器指向 GitHub 上的uwsgi-nginx-flask-docker项目(https://github.com/tiangolo/uwsgi-nginx-flask-docker)。 然后,单击 README.md文件中指向python-3.8-alpineDockerfile 的链接。

现在看一下该 Dockerfile 中的第一行:

FROM tiangolo/uwsgi-nginx:python3.8-alpine

这个FROM命令告诉 Docker 从 Docker Hub 的tiangolo命名空间中拉出一个带有python3.8-alpine标签的名为uwsgi-nginx的图像。 Docker Hub 是一个公共注册中心,人们在这里发布他们的 Docker 镜像,以供他人获取和部署。 如果愿意,您可以使用 AWS、ECR 或 Quay 等服务设置自己的图像注册表。 您需要在命名空间前面插入注册表服务的名称,如下所示:

FROM quay.io/my-org/my-app:my-tag

否则,Docker 默认从 Docker Hub 拉取图片。 FROM类似于 Dockerfile 中的include语句。 它会将另一个 Dockerfile 的内容插入到您的 Dockerfile 中,这样您就可以在此基础上进行构建。 我喜欢把这种方式看作是层次分明的图像。 Alpine 是基础层,然后是 Python3.8,然后是 uWSGI 和 Nginx,最后是您的 Flask 应用。 您可以在https://hub.docker.com/r/tiangolo/uwsgi-nginx深入研究python3.8-alpineDocker 文件,了解有关图像分层工作原理的更多信息。

Dockerfile 中感兴趣的下一行内容如下:

RUN pip install flask

RUN指令运行命令。 Docker 会按顺序执行 Dockerfile 中包含的RUN指令,以构建生成的 Docker 镜像。 此RUN指令将烧瓶安装到 SYSTEMsite-packages目录中。 我们知道pip是可用的,因为阿尔卑斯山基础图像还包括 Python3.8。

让我们跳过 nginx 的环境变量,直接进行复制:

COPY ./app /app

这个特定的 Dockerfile 与其他几个文件和子目录一起位于 Git repo 中。 COPY指令将目录从宿主 Docker 运行时环境(通常是 repo 的 Git 克隆)复制到正在构建的容器中。

您正在查看的python3.8-alpine.dockerfile文件驻留在tiangolo/uwsgi-nginx-flask-docker存储库的docker-images子目录中。 在docker-images目录内是一个app子目录,其中包含 Hello World Flask Web 应用。 此COPY指令将示例 repo 中的app目录复制到 Docker 镜像的根目录中:

WORKDIR /app

WORKDIR指令告诉 Docker 从容器内部操作哪个目录。 在本例中,它刚刚复制的/app目录成为工作目录。 如果目标工作目录不存在,则WORKDIR会创建它。 因此,此 Dockerfile 中显示的任何后续非绝对路径都是相对于/app目录的。

现在让我们看看如何在容器内设置环境变量:

ENV PYTHONPATH=/app

ENV告诉 Docker 下面是一个环境变量定义。 PYTHONPATH是一个环境变量,它扩展为冒号分隔的路径列表,Python 解释器在该列表中查找模块和包。

接下来,让我们向下跳到第二条RUN指令:

RUN chmod +x /entrypoint.sh

RUN指令告诉 Docker 从 shell 运行命令。 在本例中,正在运行的命令是chmod,它更改文件权限。 在这里,它呈现 /entrypoint.sh可执行文件。

此 Dockerfile 中的下一行是可选的:

ENTRYPOINT ["/entrypoint.sh"]

ENTRYPOINT是此 Dockerfile 中最有趣的说明。 它在启动容器时将可执行文件暴露给 Docker 主机命令行。 这使您可以将参数从命令行向下传递到容器内的可执行文件。 您可以在命令行的docker run <image>后面追加这些参数。 如果 Dockerfile 中有多条ENTRYPOINT指令,则只执行最后一条ENTRYPOINT

Dockerfile 中的最后一行如下:

CMD ["/start.sh"]

ENTRYPOINT指令类似,CMD指令在容器开始时执行,而不是在构建时执行。 在 Dockerfile 中定义ENTRYPOINT指令时,CMD指令定义要传递给该ENTRYPOINT的默认参数。 在本例中,/start.sh路径是传递给/entrypoint.sh的参数。 /entrypoint.sh中的最后一行执行/start.sh

exec "$@"

/start.sh脚本来自uwsgi-nginx基本映像。 /start.sh/entrypoint.sh为 nginx 和 uWSGI 配置了容器运行时环境之后,启动 nginx 和 uWSGI。 当CMDENTRYPOINT结合使用时,可以从 Docker 主机命令行覆盖CMD设置的默认参数。

大多数 Dockerfile 没有ENTRYPOINT指令,因此 Dockerfile 的最后一行通常是在前台运行的CMD指令,而不是默认参数。 我使用这个 Dockerfile 技巧来保持一个用于开发的通用 Docker 容器运行:

CMD tail -f /dev/null

除了ENTRYPOINTCMD之外,本例中的所有指令python-3.8-alpineDockerfile 仅在构建容器时执行。

打造码头工人形象

在构建 Docker 镜像之前,我们需要一个 Dockerfile。 您的系统中可能已经有一些 Docker 映像。 要查看 Docker 镜像列表,请使用以下命令:

$ docker images

现在,让我们获取并构建我们刚刚解析的 Dockerfile:

  1. 克隆包含 Dockerfile 的 Repo:

    $ git clone https://github.com/tiangolo/uwsgi-nginx-flask-docker.git
  2. 切换到 repo:

    $ cd uwsgi-nginx-flask-docker/docker-images

    内的docker-images子目录

  3. python3.8-alpine.dockerfile复制到名为Dockerfile

    $ cp python3.8-alpine.dockerfile Dockerfile

    的文件

  4. 从 Dockerfile 构建映像:

    $ docker build -t my-image .

镜像构建完成后,它将出现在您的本地 Docker 镜像列表中:

$ docker images

列表中还应显示uwsgi-nginx基础映像以及新建的my-image。 请注意,自创建uwsgi-nginx基本映像以来经过的时间比创建my-image以来的时间要长得多。

运行 Docker 映像

我们现在已经构建了一个 Docker 映像,我们可以将其作为容器运行。 要获取系统上正在运行的容器的列表,请使用以下命令:

$ docker ps

要运行基于my-image的容器,请发出以下docker run命令:

$ docker run -d --name my-container -p 80:80 my-image

现在观察正在运行的容器的状态:

$ docker ps

您应该看到一个基于列表中名为my-image的图像的名为my-container的容器。 docker run命令中的-p选项将容器端口映射到主机端口。 因此,在本例中,容器端口80映射到主机端口80。 此 端口映射允许在容器内运行的 Flask Web 服务器为 HTTP 请求提供服务。

要停止my-container,请运行以下命令:

$ docker stop my-container

现在再次检查正在运行的容器的状态:

$ docker ps

my-container不应再出现在正在运行的容器列表中。 集装箱走了吗? 不,只是停了。 您仍然可以通过将-a选项添加到docker ps命令来查看my-container及其状态:

$ docker ps -a

稍后我们将介绍如何删除不再需要的容器。

获取 Docker 镜像

在本部分的前面部分,我介绍了 Docker Hub、AWS ECR 和 Quay 等映像注册中心。 事实证明,我们从克隆的 GitHub repo 本地构建的 Docker 映像已经发布在 Docker Hub 上。 从 Docker Hub 获取预置映像比自己在系统上构建要快得多。 该项目的 Docker 图像可以在https://hub.docker.com/r/tiangolo/uwsgi-nginx-flask找到。 要从 Docker Hub 拉取我们以my-image身份构建的同一个 Docker 镜像,请输入以下命令:

$ docker pull tiangolo/uwsgi-nginx-flask:python3.8-alpine

现在再次查看您的 Docker 图像列表:

$ docker images

您应该在列表中看到一个新的uwsgi-nginx-flask图像。

要运行这个新获取的映像,请发出以下docker run命令:

$ docker run -d --name flask-container -p 80:80 tiangolo/uwsgi-nginx-flask:python3.8-alpine

如果您不想键入完整的镜像名称,可以用docker images中的对应的镜像 ID(散列)替换前面docker run命令中的完整镜像名称(repo:tag)。

发布 Docker 镜像

要将 Docker 镜像发布到 Docker Hub,您必须先拥有帐号并登录。 您可以通过转到网站https://hub.docker.com并注册来在 Docker Hub 上创建帐户。 拥有帐户后,您可以将现有映像推送到 Docker Hub 存储库:

  1. 从命令行登录到 Docker Hub 映像注册表:

    $ docker login
  2. 出现提示时,输入您的 Docker Hub 用户名和密码。

  3. Tag an existing image with a new name that starts with the name of your repository:

    $ docker tag my-image:latest <repository>/my-image:latest

    将前面命令中的<repository>替换为您在 Docker Hub 上的存储库名称(与您的用户名相同)。 您还可以用您希望推送的另一个现有映像的名称替换my-image:latest

  4. Push the image to the Docker Hub image registry:

    $ docker push <repository>/my-image:latest

    同样,进行与步骤 3相同的替换。

默认情况下,推送到 Docker Hub 的镜像是公开的。 要访问新发布的图像的网页,请转到 https://hub.docker.com/repository/docker//my-IMAGE。 将前面 URL 中的<repository>替换为您在 Docker Hub 上的存储库名称(与您的用户名相同)。 如果不同,您还可以用您推送的实际镜像的名称替换my-image:latest。 如果您点击该网页上的标签选项卡,您应该看到用于获取该图像的docker pull命令。

清理

我们知道docker images列出图像,docker ps列出容器。 在删除 Docker 图像之前,我们必须先删除引用它的所有容器。 要删除 Docker 容器,首先需要知道容器的名称或 ID:

  1. 查找目标 Docker 容器的名称:

    $ docker ps -a 
  2. 如果容器正在运行,则停止该容器:

    $ docker stop flask-container
  3. 删除 Docker 容器:

    $ docker rm flask-container

将前面两个命令中的flask-container替换为步骤 1中的容器名称或 ID。 出现在docker ps下的每个容器也有一个与其关联的图像名称或 ID。 删除引用图像的所有容器后,即可删除该图像。

Docker 映像名称(repo:tag)可能会很长(例如,tiangolo/uwsgi-nginx-flask:python3.8-alpine)。 因此,我发现在删除时只复制和粘贴镜像的 ID(散列)会更容易:

  1. 找到 Docker 镜像的 ID:

    $ docker images
  2. 删除 Docker 镜像:

    $ docker rmi <image-ID>

将前面命令中的<image-ID>替换为步骤 1中的图像 ID。

如果您只是想清除系统上不再使用的所有容器和映像,则可以使用以下命令:

$ docker system prune -a

docker system prune删除所有停止的容器和悬挂图像。

我们已经了解了如何使用pip来安装 Python 应用的依赖项。 您只需将调用pip installRUN指令添加到 Dockerfile。 因为容器是沙箱环境,所以它们提供了许多与虚拟环境相同的好处。 但与condavenv虚拟环境不同,Buildroot 和 Yocto 都支持 Docker 容器。 Buildroot 有docker-enginedocker-cli包。 Yocto 有meta-virtualization层。 如果您的设备因为 Python 包冲突而需要隔离,那么您可以使用 Docker 来实现。

docker run命令提供选项 f 或向容器公开操作系统资源。 指定绑定挂载允许将主机上的文件或目录挂载到容器中以进行读写。 默认情况下,容器不向外部世界发布端口。 在运行my-container映像时,您使用了-p选项将端口80从容器发布到主机上的端口80--device选项将/dev下的主机设备文件添加到非特权容器。 如果您希望授予对主机上所有设备的访问权限,请使用--privileged选项。

Containers 擅长的是部署。 能够推送 Docker 镜像,然后可以轻松地拉入并在任何主要云平台上运行,这使DevOps运动发生了革命性的变化。 多亏了 Balena 等 OTA 更新解决方案,Docker 也在嵌入式 Linux 领域取得了进展。 Docker 的缺点之一是运行时的存储空间和内存开销。 Go 二进制程序有点臃肿,但 Docker 在四核 64 位 ARM SoC 上运行得很好,比如 Raspberry Pi 4。 如果您的目标设备有足够的电量,则在其上运行 Docker。 您的软件开发团队会感谢您的。

摘要

到目前为止,您可能会问自己,这些 Python 打包内容与嵌入式 Linux 有什么关系? 答案是不是很多,但请记住,编程这个词也恰好出现在本书的标题中。 这一章与现代编程有着千丝万缕的联系。 要在当今时代成为一名成功的开发人员,您需要能够快速、频繁地以可重复的方式将代码部署到生产环境中。 这意味着要小心地管理您的依赖项,并尽可能多地将流程自动化。 现在您已经了解了使用 Python 可以使用哪些工具来实现这一点。

在下一章中,我们将详细介绍 Linux 进程模型,并描述什么是进程,它如何与线程相关,它们如何协作,以及它们是如何调度的。 如果您想要创建一个健壮且可维护的嵌入式系统,了解这些事情是很重要的。

进一步阅读

以下资源提供了有关本章中介绍的主题的详细信息: