嵌入式-Linux-项目指南-使用-Yocto-项目-全-

嵌入式 Linux 项目指南:使用 Yocto 项目(全)

原文:annas-archive.org/md5/973dcf79f7ad34e0b3ec1efedb2f8b67

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Linux 内核是当今设计的众多嵌入式产品的核心。过去 10 年里,这一操作系统从主导服务器市场发展成为嵌入式系统中使用最广泛的操作系统,即使是那些具有实时需求的嵌入式系统也是如此。在这过程中,Linux 不断发展,嵌入式行业也认识到它具有一些关键且独特的特性:

  • Linux 迅速适应新技术,它是创新首先发生的地方。

  • 它非常稳健,开发社区对问题的反应非常迅速。

  • 它是安全的,漏洞的发现和处理速度远远快于竞争对手的专有产品。

  • 它是开放的,这意味着您的公司可以拥有、修改并理解这项技术。

  • 最后,Linux 是免费的。

所有这些都使它成为嵌入式开发的一个非常有吸引力的选择。

但同时,嵌入式 Linux 产品不仅仅是 Linux 内核。公司需要在操作系统之上构建嵌入式系统,而这正是嵌入式 Linux 难以找到立足点的地方——直到 Yocto 出现。

Yocto 项目将 Linux 的所有优势带入了嵌入式系统的开发中。它提供了一个标准的构建系统,使您能够以快速、可靠且可控的方式开发嵌入式产品。正如 Linux 在嵌入式开发中有其强项一样,Yocto 也有它自己的一套优势:

  • Yocto 是安全的,因为它使用最新的源代码,并提供了快速应用安全漏洞补丁的手段。

  • 它非常稳健,因为它得到了一个庞大的社区的支持,该社区对问题的反应非常迅速。

  • 它是开放的,因此您的公司可以拥有这项技术,理解它,并使其适应特定需求。

  • 它是免费的。

借助 Yocto 项目的 6 个月稳定版本发布过程、软件包管理更新和灵活性,您将能够专注于您的嵌入式应用程序,知道您是在一个可信赖的系统基础上构建它。这样,您将加速开发周期,并生产出卓越的产品。

但 Yocto 是一项新技术,开发者需要适应它。本书旨在为具有基本 Linux 和 Yocto 知识的读者提供实用指南,帮助他们开发基于 ARM 架构的生产就绪型工业系统。

本书内容

第一章,构建系统,描述了 Poky 构建系统的使用,并通过 Freescale BSP 社区层对其进行了扩展。它还描述了用于优化目标镜像构建的常见构建系统配置和功能。

第二章,BSP 层,指导您如何为自己的产品定制 BSP。然后,它解释了如何配置、修改、构建和调试 U-Boot 引导加载程序、Linux 内核及其设备树。

第三章, 软件层,描述了创建新的软件层以容纳新的应用程序、服务或现有包的修改的过程,并讨论了许可证合规的发布过程。

第四章, 应用开发,从工具链和应用开发工具包(ADT)开始,详细讲解了应用程序开发,包括开发环境,如 Eclipse 和 Qt Creator。

第五章, 调试、追踪与性能分析,讨论了调试工具和技术,并探讨了 Linux 内核提供的追踪功能,以及一些利用这些功能的用户空间追踪和性能分析工具。

本书所需的条件

本书假设读者具有基本的 GNU/Linux 系统工作经验;熟悉 bash shell 及其衍生版本,以及标准工具如 grep、patch、diff 等。本书中的示例已在 Ubuntu 14.04 LTS 系统上测试,但任何 Yocto 项目支持的 Linux 发行版都可以使用。

本书并不是 Yocto 项目的入门书籍,建议阅读一本入门书籍,例如由 Otavio Salvador 和 Daiane Angolini 编写的《Yocto 项目嵌入式 Linux 开发》,该书同样由 Packt 出版。

本书的结构遵循了嵌入式 Linux 产品的常见开发工作流,但各章节或单个配方可以独立阅读。

配方采用实用的动手操作方式,使用基于 Freescale i.MX6 的系统 wandboard-quad 作为基础硬件。然而,任何其他基于 i.MX 的硬件也可以用于跟随示例。

本书适合谁阅读

本书是嵌入式开发者学习嵌入式 Linux 和 Yocto 项目的理想方式,通过实际应用的示例帮助读者提高技能,拓宽知识面,立即可用于嵌入式开发。

有经验的嵌入式 Yocto 开发者将获得关于工作方法和 ARM 特定开发能力的新见解。

章节

本书中,你会发现几个经常出现的标题(准备工作、如何做、工作原理、更多内容以及另见)。

为了提供清晰的配方完成指导,我们使用以下章节:

准备工作

本节告诉你该配方中应期待的内容,并描述如何设置任何软件或为配方所需的任何初步设置。

如何做…

本节包含了完成配方所需的步骤。

工作原理…

本节通常包含对上一节内容的详细解释。

更多内容…

本节包含关于配方的额外信息,以帮助你更深入了解该配方。

另见

本节提供了有助于配方的其他有用信息的链接。

约定

本书中,你会看到多种文本样式,用以区分不同类型的信息。以下是这些样式的示例以及它们的含义说明。

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名如下所示:“你可以通过将其添加到RM_WORK_EXCLUDE变量来排除清理的包列表。”

一段代码如下所示:

SRC_URI = "file://helloworld.c"

S = "${WORKDIR}"

当我们希望特别提醒你注意代码块中的某一部分时,相关的行或项目会以粗体显示:

SRC_URI = "file://helloworld.c"
DEPENDS = "lttng-ust"

S = "${WORKDIR}"

任何命令行输入或输出如下所示:

$ ls sources/meta-fsl*/conf/machine/*.conf

新术语重要词汇以粗体显示。你在屏幕上看到的单词,例如在菜单或对话框中,文本中的表现方式如下:“通过导航到项目 | 构建项目来构建项目。”

注意

警告或重要说明以如下框体呈现。

提示

提示和技巧以这种形式出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们你对本书的看法——喜欢或不喜欢的地方。读者反馈对我们很重要,因为它帮助我们开发出你真正能从中受益的书籍。

若要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中注明书名。

如果你在某个领域有专长,并且有兴趣撰写或贡献书籍,请参考我们的作者指南:www.packtpub.com/authors

客户支持

既然你已经成为一本 Packt 书籍的骄傲拥有者,我们为你提供了一些帮助,帮助你从购买中获得最大收益。

下载示例代码

你可以从你的账户下载示例代码文件,地址:www.packtpub.com,适用于你购买的所有 Packt Publishing 书籍。如果你从其他地方购买了本书,可以访问www.packtpub.com/support,并注册直接通过电子邮件将文件发送给你。

本书中的示例代码可以通过多个 GitHub 仓库访问:github.com/yoctocookbook。按照 GitHub 上的说明获取计算机中的源代码副本。

勘误

虽然我们已经尽一切努力确保内容的准确性,但错误难免会发生。如果您在我们的书籍中发现错误——无论是文字还是代码错误——我们将非常感激您向我们报告。通过这样做,您可以帮助其他读者避免困扰,并帮助我们改进后续版本。如果您发现任何勘误,请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入勘误的详细信息。一旦您的勘误得到验证,您的提交将被接受,并且勘误将被上传到我们的网站或添加到该书籍标题的勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索框中输入书名。所需的信息将出现在勘误部分。

盗版

网络上的版权材料盗版问题在所有媒体中都存在。我们在 Packt 公司非常重视对版权和许可证的保护。如果您在互联网上遇到我们作品的任何非法副本,请立即提供其位置地址或网站名称,以便我们采取补救措施。

请通过<copyright@packtpub.com>与我们联系,并附上涉嫌盗版材料的链接。

我们感谢您在保护我们的作者和确保我们能够为您提供有价值内容方面的帮助。

问题

如果您在本书的任何方面遇到问题,您可以通过<questions@packtpub.com>与我们联系,我们将尽力解决问题。

第一章:构建系统

在本章中,我们将涵盖以下教程:

  • 设置主机系统

  • 安装 Poky

  • 创建构建目录

  • 构建你的第一个镜像

  • 解释 Freescale Yocto 生态系统

  • 安装 Freescale 硬件的支持

  • 构建 Wandboard 镜像

  • 排查你的 Wandboard 第一次启动问题

  • 配置网络启动以进行开发设置

  • 共享下载

  • 共享共享状态缓存

  • 设置软件包源

  • 使用构建历史

  • 使用构建统计信息

  • 调试构建系统

介绍

Yocto 项目(www.yoctoproject.org/)是一个嵌入式 Linux 发行版构建工具,利用了其他多个开源项目。

Yocto 项目提供了一个嵌入式 Linux 的参考构建系统,称为 Poky,它以 BitBakeOpenEmbedded-CoreOE-Core)项目为基础。Poky 的目的是构建嵌入式 Linux 产品所需的组件,具体包括:

  • 启动加载程序镜像

  • 一个 Linux 内核镜像

  • 一个根文件系统镜像

  • 用于应用程序开发的工具链和软件开发工具包SDK

通过这些,Yocto 项目满足了系统和应用程序开发人员的需求。当 Yocto 项目作为引导加载程序、Linux 内核和用户空间应用程序的集成环境时,我们称之为系统开发。

对于应用程序开发,Yocto 项目构建 SDK,使得应用程序可以独立于 Yocto 构建系统进行开发。

Yocto 项目每六个月发布一个新版本。本文写作时的最新版本是 Yocto 1.7.1 Dizzy,本书中的所有示例都指的是 1.7.1 版本。

一个 Yocto 发行版包括以下组件:

  • Poky,参考构建系统

  • 一个构建设备;即一个准备好使用 Yocto 的主机系统的 VMware 镜像

  • 适用于主机系统的 应用程序开发工具包ADT)安装程序

  • 以及针对不同支持的平台:

    • 预构建工具链

    • 预构建的打包二进制文件

    • 预构建镜像

Yocto 1.7.1 版本可从 downloads.yoctoproject.org/releases/yocto/yocto-1.7.1/ 下载。

设置主机系统

本教程将说明如何设置主机 Linux 系统以使用 Yocto 项目。

准备工作

开发嵌入式 Linux 系统的推荐方式是使用本地 Linux 工作站。虽然虚拟机可以用于演示和测试目的,但不推荐用于开发工作。

Yocto 从零开始构建之前提到的所有组件,包括交叉编译工具链和它所需的本地工具,因此 Yocto 的构建过程对处理能力以及硬盘空间和 I/O 都有较高要求。

尽管 Yocto 在配置较低的机器上也能正常工作,但对于专业开发人员的工作站,建议使用对称多处理SMP)系统,配置应为 8 GB 或更大内存,并配备大容量、高速硬盘。构建服务器可以采用分布式编译,但这不在本书的范围内。由于构建过程中的不同瓶颈,似乎在超过 8 个 CPU 或大约 16 GB 内存时,性能提升并不显著。

第一次构建时,系统还会从互联网下载所有源代码,因此建议使用快速的互联网连接。

如何操作...

Yocto 支持多种发行版,每个 Yocto 版本都会记录支持的发行版列表。尽管强烈建议使用支持的 Linux 发行版,但 Yocto 也能在任何 Linux 系统上运行,只要它具备以下依赖项:

  • Git 1.7.8 或更高版本

  • Tar 1.24 或更高版本

  • Python 2.7.3 或更高版本(但不支持 Python 3)

Yocto 还提供了一种安装这些工具正确版本的方法,方法是下载 buildtools-tarball 或在支持的机器上构建一个。这使得几乎任何 Linux 发行版都可以运行 Yocto,同时确保未来能够复制你的 Yocto 构建系统。这对具有长期可用性要求的嵌入式产品非常重要。

本书将使用 Ubuntu 14.04 长期稳定版LTS)Linux 发行版进行所有示例的操作。其他 Linux 发行版的安装说明可以在 Yocto 项目开发手册支持的 Linux 发行版 部分找到,但所有示例仅在 Ubuntu 14.04 LTS 上进行过测试。

为了确保你安装了 Yocto 所需的包依赖,并能够跟随本书中的示例进行操作,请从你的 shell 中运行以下命令:

$ sudo apt-get install gawk wget git-core diffstat unzip texinfo gcc- multilib build-essential chrpath socat libsdl1.2-dev xterm make xsltproc docbook-utils fop dblatex xmlto autoconf automake libtool libglib2.0-dev python-gtk2 bsdmainutils screen

提示

下载示例代码

你可以从 www.packtpub.com 账户下载所有已购买的 Packt 图书的示例代码文件。如果你在其他地方购买了本书,可以访问 www.packtpub.com/support 并注册,文件将直接通过电子邮件发送给你。

本书中的示例代码可以通过多个 GitHub 仓库访问,地址是 github.com/yoctocookbook。按照 GitHub 上的说明获取源代码副本。

如何操作...

上述命令将使用 apt-get,即高级包装工具APT)的命令行工具。它是dpkg包管理器的前端,包含在 Ubuntu 发行版中。它将安装所有必需的包及其依赖项,以支持 Yocto 项目的所有功能。

更多内容...

如果构建时间对你来说是一个重要因素,在准备磁盘时,有一些步骤可以进一步优化:

  • build目录放置在独立的磁盘分区或快速外部驱动器上。

  • 使用 ext4 文件系统,但配置其不在 Yocto 专用分区上使用日志记录。请注意,电源丧失可能会损坏你的构建数据。

  • 挂载文件系统时,确保读取时不记录读写时间,禁用写入屏障,并使用以下挂载选项延迟提交文件系统更改:

    noatime,barrier=0,commit=6000.
    
  • 不要在网络挂载的驱动器上进行构建。

这些更改减少了数据完整性保护,但由于build目录已分离到独立磁盘,失败只会影响临时构建数据,这些数据可以被删除并重新生成。

另见

安装 Poky

本食谱将解释如何使用 Poky 设置你的 Linux 主机系统,Poky 是 Yocto 项目的参考系统。

准备工作

Poky 使用 OpenEmbedded 构建系统,因此使用 BitBake 工具,BitBake 是一个用 Python 编写的任务调度器,源自 Gentoo 的 Portage 工具。你可以将 BitBake 视为 Yocto 中的 make 工具。它将解析配置和食谱元数据,调度任务列表,并执行其中的任务。

BitBake 也是 Yocto 的命令行接口。

Poky 和 BitBake 是 Yocto 使用的两个开源项目。Poky 项目由 Yocto 社区维护。你可以从其 Git 仓库下载 Poky,网址是git.yoctoproject.org/cgit/cgit.cgi/poky/

你可以通过访问开发邮件列表lists.yoctoproject.org/listinfo/poky来跟踪和参与开发讨论。

另一方面,BitBake 由 Yocto 和 OpenEmbedded 社区共同维护,因为这两个社区都使用该工具。你可以从其 Git 仓库下载 BitBake,网址是git.openembedded.org/bitbake/

你可以通过访问开发邮件列表lists.openembedded.org/mailman/listinfo/bitbake-devel来跟踪和参与开发讨论。

Poky 构建系统仅支持以下架构的虚拟化 QEMU 机器:

  • ARM (qemuarm)

  • x86 (qemux86)

  • x86-64 (qemux86-64)

  • PowerPC (qemuppc)

  • MIPS (qemumips, qemumips64)

除了这些,Poky 还支持一些板级支持包BSPs),这些 BSP 代表了刚刚列出的架构。以下是这些 BSP:

  • Texas Instruments Beaglebone (beaglebone)

  • Freescale MPC8315E-RDB (mpc8315e-rdb)

  • 基于 Intel x86 的 PC 和设备(genericx86 和 genericx86-64)

  • Ubiquiti Networks EdgeRouter Lite (edgerouter)

如果要在不同的硬件上开发,你需要为 Poky 补充硬件特定的 Yocto 层。这部分内容将在后续介绍。

如何操作...

Poky 项目包含了一个稳定的 BitBake 版本,因此要开始使用 Yocto,我们只需要在我们的 Linux 主机系统中安装 Poky。

注意

请注意,你也可以通过发行版的包管理系统独立安装 BitBake。虽然可以这样做,但不推荐这么做,因为 BitBake 需要与 Yocto 中使用的元数据兼容。如果你通过发行版安装了 BitBake,请将其卸载。

当前的 Yocto 版本是 1.7.1,也叫 Dizzy,所以我们将把它安装到主机系统中。我们将使用 /opt/yocto 文件夹作为安装路径:

$ sudo install -o $(id -u) -g $(id -g) -d /opt/yocto
$ cd /opt/yocto
$ git clone --branch dizzy git://git.yoctoproject.org/poky

它是如何工作的...

上述说明将使用 Git(源代码管理系统命令行工具)来克隆包含 BitBake 的 Poky 仓库,并将其指向 Dizzy 稳定分支,将仓库克隆到当前路径下的新 poky 目录中。

还有更多...

Poky 包含三个元数据目录,分别是 metameta-yoctometa-yocto-bsp,以及一个名为 meta-skeleton 的模板元数据层,可用作新层的基础。以下是 Poky 的三个元数据目录的解释:

  • meta:此目录包含 OpenEmbedded-Core 元数据,支持 ARM、x86、x86-64、PowerPC、MIPS 和 MIPS64 架构以及 QEMU 仿真硬件。你可以从其 Git 仓库下载:git.openembedded.org/openembedded-core/

    你可以访问开发邮件列表,关注和参与开发讨论,网址:lists.openembedded.org/mailman/listinfo/openembedded-core

  • meta-yocto:此目录包含 Poky 的发行版特定元数据。

  • meta-yocto-bsp:此目录包含参考硬件板的元数据。

另见

  • 有关 Git 这个分布式版本控制系统的文档可以在git-scm.com/doc找到。

创建构建目录

在构建第一个 Yocto 镜像之前,我们需要为它创建一个 build 目录。

构建过程,按照之前所述,在主机系统上可能需要长达一小时,并且需要大约 20 GB 的硬盘空间来构建仅限控制台的镜像。像core-image-sato这样的图形镜像,构建过程可能需要长达 4 小时,并占用大约 50 GB 的空间。

如何操作...

我们需要做的第一件事是为我们的项目创建一个 build 目录,构建输出将在该目录中生成。有时,build 目录也可能被称为项目目录,但 build 目录才是 Yocto 中的标准术语。

当你有多个项目时,build目录的结构没有绝对正确的方式,但一个好的做法是每个架构或机器类型都有一个build目录。它们可以共享一个公共的downloads文件夹,甚至一个共享的状态缓存(稍后会介绍),因此将它们分开不会影响构建性能,但会允许你同时开发多个项目。

要创建一个build目录,我们使用由 Poky 提供的oe-init-build-env脚本。需要在当前的 shell 中执行该脚本,它会设置好使用 OpenEmbedded/Yocto 构建系统的环境,包括将 BitBake 工具添加到你的路径中。你可以指定一个build目录来使用,否则它会默认使用build目录。我们在这个示例中使用qemuarm

$ cd /opt/yocto/poky
$ source oe-init-build-env qemuarm

脚本会切换到指定的目录。

注意

由于oe-init-build-env只配置当前的 shell,因此每次开启新 shell 时都需要重新执行它。但是,如果你将脚本指向现有的build目录,它会设置好你的环境,但不会改变你已有的配置。

提示

BitBake 采用客户端/服务器的抽象设计,因此我们也可以启动一个内存驻留的服务器,并将客户端连接到它。通过这种设置,可以避免每次加载缓存和配置信息,从而节省一些开销。要运行一个始终可用的内存驻留 BitBake,你可以按如下方式使用oe-init-build-env-memres脚本:

$ source oe-init-build-env-memres 12345 qemuarm

这里的12345是将要使用的本地端口。

不要同时使用两种 BitBake 变体,因为这可能会导致问题。

然后,你可以通过执行以下命令终止内存驻留的 BitBake:

$ bitbake -m

它是如何工作的...

这两个脚本都调用了poky目录中的scripts/oe-setup-builddir脚本来创建build目录。

在创建时,build目录包含一个conf目录,其中有以下三个文件:

  • bblayers.conf:该文件列出了要考虑的元数据层,用于该项目。

  • local.conf:该文件包含项目特定的配置变量。你可以使用site.conf文件为不同的项目设置通用配置变量,但默认情况下不会创建该文件。

  • templateconf.cfg:该文件包含用于创建项目的模板配置文件所在的目录。默认情况下,它使用指向你 Poky 安装目录中的templateconf文件所指定的目录,默认路径为meta-yocto/conf

注意

要从头开始进行构建,build目录只需要这些。

删除这些文件以外的所有内容将会重新创建你的构建环境。

$ cd /opt/yocto/poky/qemuarm
$ rm -Rf tmp sstate-cache

还有更多...

在创建build目录时,你可以通过TEMPLATECONF变量指定一个不同的模板配置文件,例如:

$ TEMPLATECONF=meta-custom/config source oe-init-build-env <build- dir>

TEMPLATECONF变量需要指向一个包含local.confbblayer.conf模板的目录,但文件名应为local.conf.samplebblayers.conf.sample

对于我们的目的,我们可以使用未修改的默认项目配置文件。

构建您的第一个镜像

在构建我们的第一个镜像之前,我们需要决定要构建哪种类型的镜像。本配方将介绍一些可用的 Yocto 镜像,并提供构建简单镜像的说明。

准备工作

Poky 包含一组默认的目标镜像。您可以通过执行以下命令列出它们:

$ cd /opt/yocto/poky
$ ls meta*/recipes*/images/*.bb

不同镜像的完整描述可以在Yocto 项目参考手册中找到。通常,这些默认镜像作为基础使用,并根据您自己的项目需求进行定制。最常用的基础默认镜像有:

  • core-image-minimal:这是一个最小的基于 BusyBox、sysvinit 和 udev 的仅控制台镜像

  • core-image-full-cmdline:这是一个基于 BusyBox 的仅控制台镜像,提供完整的硬件支持和更完整的 Linux 系统,包括 bash

  • core-image-lsb:这是一个仅控制台镜像,符合 Linux 标准基础(LSB)标准

  • core-image-x11:这是一个基于 X11 窗口系统的基础镜像,带有图形终端

  • core-image-sato:这是一个基于 X11 窗口系统的镜像,带有 SATO 主题和 GNOME 移动桌面环境

  • core-image-weston:这是一个基于 Wayland 协议和 Weston 参考合成器的镜像

您还会找到以下后缀的镜像:

  • dev:这些镜像适用于开发工作,因为它们包含头文件和库文件。

  • sdk:这些镜像包括一个完整的 SDK,可用于目标开发。

  • initramfs:这是一个可以用于基于 RAM 的根文件系统的镜像,且可以选择与 Linux 内核一起嵌入。

如何操作...

要构建镜像,我们需要配置要构建的MACHINE并将其名称传递给 BitBake。例如,对于qemuarm机器,我们将运行以下命令:

$ cd /opt/yocto/poky/qemuarm
$ MACHINE=qemuarm bitbake core-image-minimal

或者,我们可以通过以下命令将MACHINE变量导出到当前的 shell 环境:

$ export MACHINE=qemuarm

但是,首选且持久的做法是编辑conf/local.conf配置文件,将默认机器更改为qemuarm

- #MACHINE ?= "qemuarm"
+ MACHINE ?= "qemuarm"

然后,您可以执行以下操作:

$ bitbake core-image-minimal

它是如何工作的...

当您将目标配方传递给 BitBake 时,它首先解析以下配置文件:

  • conf/bblayers.conf:此文件用于查找所有配置的层

  • conf/layer.conf:此文件用于每个配置的层

  • meta/conf/bitbake.conf:此文件用于其自身的配置

  • conf/local.conf:此文件用于用户对当前构建的任何其他配置

  • conf/machine/<machine>.conf:此文件是机器配置文件;在我们的例子中,这是qemuarm.conf

  • conf/distro/<distro>.conf:此文件是分发政策;默认情况下,这是poky.conf文件

然后,BitBake 会解析所提供的目标配方及其依赖项。最终结果是一组相互依赖的任务,BitBake 将按照顺序执行这些任务。

还有更多...

大多数开发者不会对保留每个包的整个构建输出感兴趣,因此建议你在conf/local.conf文件中进行如下配置,来移除它:

INHERIT += "rm_work"

但与此同时,为所有包配置意味着你将无法开发或调试它们。

你可以通过将包添加到RM_WORK_EXCLUDE变量中,来添加一个要从清理中排除的包列表。例如,如果你要进行 BSP 工作,一个好的设置可能是:

RM_WORK_EXCLUDE += "linux-yocto u-boot"

记住,你可以在自己的层中使用自定义模板local.conf.sample配置文件来保存这些配置,并将它们应用到所有项目中,以便可以在所有开发者之间共享。

一旦构建完成,你可以在build目录下的tmp/deploy/images/qemuarm目录中找到输出的镜像。

默认情况下,镜像不会从deploy目录中删除,但你可以通过在conf/local.conf文件中添加以下内容来配置你的项目删除先前构建的相同镜像版本:

RM_OLD_IMAGE = "1"

你可以通过执行以下命令在 QEMU 模拟器上测试运行你的镜像:

$ runqemu qemuarm core-image-minimal

Poky 的script目录中包含的runqemu脚本是一个启动 QEMU 机器模拟器的封装器,简化了其使用。

解释 Freescale Yocto 生态系统

正如我们所见,Poky 元数据以metameta-yoctometa-yocto-bsp层开始,并可以通过使用更多层来扩展。

兼容 Yocto 项目的 OpenEmbedded 层的索引可在layers.openembedded.org/上查阅。

嵌入式产品的开发通常从使用制造商参考板设计进行硬件评估开始。除非你正在使用 Poky 已经支持的某个参考板,否则你需要扩展 Poky 以支持你的硬件。

准备工作

第一步是选择你的设计将基于哪种基础硬件。我们将使用一款基于 Freescale i.MX6 系统级芯片SoC)的板卡作为我们嵌入式产品设计的起点。

本食谱概述了 Yocto 项目对 Freescale 硬件的支持。

如何操作...

SoC 制造商(在本例中为 Freescale)提供了一系列参考设计板可供购买,以及基于 Yocto 的官方软件发布。同样,其他使用 Freescale SoC 的制造商也提供参考设计板及其自有的基于 Yocto 的软件发布。

选择适合的硬件来作为你的设计基础是嵌入式产品中最重要的设计决策之一。根据你的产品需求,你将决定:

  • 使用生产就绪的板卡,例如单板计算机SBC

  • 使用模块并围绕它设计自定义载板

  • 直接使用 Freescale 的 SoC 并设计你自己的板卡

大多数情况下,生产就绪的板卡无法满足专业嵌入式系统的具体要求,使用 Freescale SoC 设计完整载板的过程也会非常耗时。因此,使用一个已经解决了大部分技术挑战设计问题的合适模块是一个常见选择。

需要考虑的一些重要特性包括:

  • 工业温度范围

  • 电源管理

  • 长期可用性

  • 预认证的无线和蓝牙(如果适用)

支持 Freescale 基板的 Yocto 社区层被称为 meta-fsl-armmeta-fsl-arm-extrasmeta-fsl-arm 层支持的板卡仅限于 Freescale 参考设计,如果你考虑围绕 Freescale 的 SoC 设计自己的载板,参考设计将是起点。其他厂商的板卡则由 meta-fsl-arm-extras 层维护。

还有其他嵌入式厂商使用 meta-fsl-arm,但他们没有将自己的板卡集成到 meta-fsl-arm-extras 社区层中。这些厂商将保留自己的 BSP 层,该层依赖于 meta-fsl-arm,并针对其硬件提供特定支持。一个例子是 Digi International 及其基于 i.MX6 SoC 的 ConnectCore 6 模块。

它的工作原理...

为了理解 Freescale Yocto 生态系统,我们需要从 Freescale 社区 BSP 开始,其中包括支持 Freescale 参考板的 meta-fsl-arm 层及其配套的 meta-fsl-arm-extra 层,后者支持其他厂商的板卡,以及它与 Freescale 为其参考设计提供的官方 Freescale Yocto 发布版之间的区别。

社区发布版和 Freescale Yocto 发布版之间有一些关键的区别:

  • Freescale 发布版由 Freescale 内部开发,未涉及社区参与,并用于在 Freescale 参考板上进行 BSP 验证。

  • Freescale 发布版经过内部 QA 和验证测试流程,并由 Freescale 支持进行维护。

  • Freescale 为特定平台发布的版本会达到一个成熟的阶段,之后不再继续开发。此时,所有的开发工作已被集成到社区层中,平台将由 Freescale BSP 社区继续维护。

  • Freescale Yocto 发布版与 Yocto 不兼容,而社区发布版则兼容。

Freescale 的工程团队与 Freescale BSP 社区紧密合作,确保他们在官方发布版中的所有开发工作都能以可靠且快速的方式集成到社区层中。

通常,最佳选择是使用 Freescale BSP 社区发布版,但仍然使用作为厂商稳定 BSP 发布版一部分的 U-Boot 和 Linux 内核版本。

这意味着你将从制造商那里获得最新的 Linux 内核和 U-Boot 更新,同时也从社区那里获得根文件系统的最新更新,延长产品的生命周期,并确保你能够获得最新的应用程序、bug 修复和安全更新。

这利用了制造商的质量控制过程,针对接近硬件的系统组件,并使得在获得来自社区的用户空间更新的同时,能够使用制造商的支持。Freescale BSP 社区也非常响应和活跃,因此通常可以与他们共同解决问题,以便惠及所有方面。

还有更多...

Freescale BSP 社区通过以下层扩展了 Poky:

  • meta-fsl-arm:这是支持 Freescale 参考设计的社区层。它依赖于 OpenEmbedded-Core。即使 Freescale 停止对其进行积极开发,该层中的机器仍会得到维护。你可以从其 Git 仓库下载meta-fsl-arm,网址为git.yoctoproject.org/cgit/cgit.cgi/meta-fsl-arm/

    可以通过访问开发邮件列表lists.yoctoproject.org/listinfo/meta-freescale来跟踪和参与开发讨论。

    meta-fsl-arm层通过以下链接从 Freescale 的仓库获取 Linux 内核和 U-Boot 的源代码:

    还有其他 Linux 内核和 U-Boot 版本可用,但建议保持使用制造商支持的版本。

    meta-fsl-arm层包含 Freescale 的专有二进制文件,以启用某些硬件功能——最显著的是其硬件图形、 multimedia 和加密功能。为了使用这些功能,最终用户需要接受 Freescale 的最终用户许可协议EULA),该协议包含在meta-fsl-arm层中。要接受该许可,需将以下行添加到项目的conf/local.conf配置文件中:

    ACCEPT_FSL_EULA = "1"
    
  • meta-fsl-arm-extra:这个层增加了对其他社区维护的板子的支持;例如,Wandboard。要下载该层的内容,你可以访问github.com/Freescale/meta-fsl-arm-extra/

  • meta-fsl-demos:这个层为演示目标镜像添加了一个元数据层。要下载该层的内容,你可以访问github.com/Freescale/meta-fsl-demos

Freescale 在上述层的基础上,还使用了一个额外的层用于其官方软件发布:meta-fsl-bsp-release

  • meta-fsl-bsp-release:这是一个 Freescale 维护的层,用于官方 Freescale 软件发布。它包含了对 meta-fsl-armmeta-fsl-demos 的修改,并不属于社区发布的一部分。

另见

安装对 Freescale 硬件的支持

在本食谱中,我们将安装社区版 Freescale BSP Yocto 版本,该版本为我们的 Yocto 安装添加对 Freescale 硬件的支持。

准备就绪

由于层数较多,手动克隆每一层并将它们添加到项目的 conf/bblayers.conf 文件中非常繁琐。社区使用了 Google 为其社区 Android 开发的 repo 工具来简化 Yocto 的安装过程。

要在主机系统中安装 repo,请输入以下命令:

$ sudo curl http://commondatastorage.googleapis.com/git-repo- downloads/repo > /usr/local/sbin/repo
$ sudo chmod a+x /usr/local/sbin/repo

repo 工具是一个 Python 实用程序,它解析一个名为 manifest 的 XML 文件,文件中列出了 Git 仓库。然后使用 repo 工具来整体管理这些仓库。

它是如何做的...

例如,我们将使用 repo 下载前面食谱中列出的所有仓库到我们的主机系统。为此,我们将指向 Freescale 社区 BSP manifest,用于 Dizzy 发布版:

<?xml version="1.0" encoding="UTF-8"?>
<manifest>
  <default sync-j="4" revision="master"/>
  <remote fetch="git://git.yoctoproject.org" name="yocto"/>
  <remote fetch="git://github.com/Freescale" name="freescale"/>
  <remote fetch="git://git.openembedded.org" name="oe"/>
  <project remote="yocto" revision="dizzy" name="poky" path="sources/poky"/>
  <project remote="yocto" revision="dizzy" name="meta-fsl-arm" path="sources/meta-fsl-arm"/>
  <project remote="oe" revision="dizzy" name="meta-openembedded" path="sources/meta-openembedded"/>
  <project remote="freescale" revision="dizzy" name="fsl- community-bsp-base" path="sources/base">
        <copyfile dest="README" src="img/README"/>
        <copyfile dest="setup-environment" src="img/setup- environment"/>
  </project>
  <project remote="freescale" revision="dizzy" name="meta-fsl-arm- extra" path="sources/meta-fsl-arm-extra"/>
  <project remote="freescale" revision="dizzy" name="meta-fsl- demos" path="sources/meta-fsl-demos"/>
  <project remote="freescale" revision="dizzy" name="Documentation" path="sources/Documentation"/>
</manifest>

manifest 文件显示所有将要安装的不同组件的安装路径和仓库源。

它是如何工作的...

manifest 文件是 Freescale 社区 BSP 版本所需的不同层的列表。我们现在可以使用 repo 来安装它。运行以下命令:

$ mkdir /opt/yocto/fsl-community-bsp
$ cd /opt/yocto/fsl-community-bsp
$ repo init -u https://github.com/Freescale/fsl-community-bsp- platform -b dizzy
$ repo sync

如果你有多核机器并希望进行多线程操作,可以选择性地传递 -jN 参数进行同步;例如,在一个 8 核的主机系统中,你可以传递 repo sync -j8

还有更多...

要列出不同层支持的硬件板,我们可以运行:

$ ls sources/meta-fsl*/conf/machine/*.conf

要列出新引入的目标镜像,请使用以下命令:

$ ls sources/meta-fsl*/recipes*/images/*.bb

社区 Freescale BSP 发布引入了以下新目标镜像:

  • fsl-image-mfgtool-initramfs:这是一个小型的基于内存的 initramfs 镜像,用于与 Freescale 制造工具配合使用。

  • fsl-image-multimedia:这是一个仅限控制台的镜像,包含 gstreamer 多媒体框架,并通过帧缓冲区提供支持(如果适用)。

  • fsl-image-multimedia-full:这是 fsl-image-multimedia 的扩展,但扩展了 gstreamer 多媒体框架,包含所有可用的插件。

  • fsl-image-machine-test:这是 fsl-image-multimedia-full 的扩展,用于测试和基准测试。

  • qte-in-use-image:这是一个图形镜像,包含对通过帧缓冲区支持的 Qt4 的支持。

  • qt-in-use-image:这是一个图形镜像,包含对通过 X11 Windows 系统支持的 Qt4 的支持。

另见

构建 Wandboard 镜像

为支持的板(例如,Wandboard Quad)构建镜像的过程与我们之前描述的 QEMU 机器的过程相同,唯一的区别是使用了 setup-environment 脚本,它是 oe-init-build-env 的封装。

如何操作...

要为 wandboard-quad 机器构建镜像,使用以下命令:

$ cd /opt/yocto/fsl-community-bsp
$ mkdir -p wandboard-quad
$ MACHINE=wandboard-quad source setup-environment wandboard-quad
$ bitbake core-image-minimal

注意

当前版本的 setup-environment 脚本仅在 build 目录位于安装文件夹下时有效;在我们的例子中,即 /opt/yocto/fsl-community-bsp

如何操作...

setup-environment 脚本将创建一个 build 目录,设置 MACHINE 变量,并提示你接受前面提到的 Freescale EULA。你的 conf/local.conf 配置文件将更新,包含指定的机器和 EULA 接受变量。

注意

请记住,如果你关闭了终端会话,你需要重新设置环境才能使用 BitBake。你可以安全地重新运行之前看到的 setup-environment 脚本,因为它不会修改现有的 conf/local.conf 文件。运行以下命令:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad

生成的镜像 core-image-minimal.sdcard,它是在 build 目录下创建的,可以被编程到 microSD 卡中,然后插入到 Wandboard CPU 板的主槽中,并使用以下命令进行启动:

$ cd /opt/yocto/fsl-community-bsp/wandboard- quad/tmp/deploy/images/wandboard-quad/
$ sudo dd if=core-image-minimal.sdcard of=/dev/sdN bs=1M && sync

在这里,/dev/sdN 对应的是在你的主机系统中分配给 microSD 卡的设备节点。

提示

在运行 dd 命令时要小心,因为它可能会损坏你的机器。你必须确保 sdN 设备对应的是你的 microSD 卡,而不是开发机器上的驱动器。

另请参见

故障排除 Wandboard 的首次启动

如果你在启动镜像时遇到问题,按照这个方法进行故障排除。

准备工作

  1. 在未插入 microSD 卡的情况下,将 microUSB 转 USB 数据线插入到 Wandboard 的 USB OTG 接口。检查你的 Linux 主机上的 lsusb 工具,查看 Wandboard 是否以以下方式显示:

    Bus 002 Device 006: ID 15a2:0054 Freescale Semiconductor, Inc. i.MX6Q SystemOnChip in RecoveryMode
    

    如果你没有看到此内容,尝试更换电源。电源应为 5V,10W。

  2. 确保你使用 NULL 调制解调器串行电缆连接 Wandboard 目标板上的 RS232 接口和你的 Linux 主机的串口。然后,使用以下命令打开一个终端程序,如 minicom:

    $ minicom -D /dev/ttyS0 -b 115200
    
    

    注意

    你需要将你的用户添加到 dialout 组,或者尝试以 sudo 身份运行命令。这应该会打开一个 115200 8N1 的串口连接。串口设备在你的 Linux 主机中可能有所不同。例如,USB 转串口适配器可能会被识别为/dev/ttyUSB0。同时,确保硬件和软件流控制都已禁用。

如何操作...

  1. 将 microSD 卡插入模块插槽,而不是基板插槽,因为后者仅用于存储而不是启动,然后为其供电。你应该在 minicom 会话的输出中看到 U-Boot 横幅。

  2. 如果没有,你可能会遇到串口通信问题。默认情况下,FSL 社区 BSP 映像中的以太网接口配置为通过 DHCP 请求地址,因此你可以用它来连接到目标。

    确保在测试网络中目标设备所在的网络上运行 DHCP 服务器。

    你可以使用像Wireshark这样的包嗅探器,在 Linux 主机上捕获网络跟踪并过滤类似bootp协议的包。至少,你应该看到目标设备的一些广播,如果你使用以太网集线器,你还应该看到 DHCP 回复。

    可选地,你可以登录到你的 DHCP 服务器并检查日志,看是否分配了新的 IP 地址。如果你看到 IP 地址被分配,你可能想考虑向你的 core-image-minimal 映像中添加 SSH 服务器,如Dropbear,这样你就可以与目标建立网络连接。你可以通过将以下行添加到conf/local.conf配置文件来完成:

    IMAGE_INSTALL_append = " dropbear"
    

    注意初始引号后的空格。

    构建并重新编程后,你可以从 Linux 主机启动一个 SSH 会话到 Wandboard,命令如下:

    $ ssh root@<ip_address>
    
    

    连接应该会自动登录,而无需密码提示。

  3. 尝试从www.wandboard.org/index.php/downloads编程默认的 microSD 卡映像,以确保硬件和你的设置有效。

  4. 尝试重新编程你的 microSD 卡。确保你使用的是正确的映像(例如,不要混合双核和四核映像)。同时,尝试使用不同的卡和卡读卡器。

这些步骤将使你的 Wandboard 开始启动,你应该在串口连接中看到一些输出。

还有更多...

如果其他方法都失败了,你可以验证启动加载程序在 microSD 卡上的位置。你可以通过以下命令转储 microSD 卡前几个块的内容:

$ sudo dd if=/dev/sdN of=/tmp/sdcard.img count=10

你应该在偏移量 0x400 处看到一个 U-Boot 头部。这个偏移量是 i.MX6 启动 ROM 在从 microSD 接口引导时查找引导加载程序的位置。使用以下命令:

$ head /tmp/sdcard.img | hexdump
0000400 00d1 4020 0000 1780 0000 0000 f42c 177f

你可以通过转储构建的 U-Boot 映像来识别 U-Boot 头部。运行以下命令:

$ head u-boot-wandboard-quad.imx | hexdump
0000000 00d1 4020 0000 1780 0000 0000 f42c 177f

为开发环境配置网络启动

大多数专业的 i.MX6 开发板都具有内置的 嵌入式 MMC (eMMC) 闪存,这是启动固件的推荐方式。Wandboard 其实并不是一个为专业用途设计的产品,因此它没有此功能。但无论是 eMMC 还是 microSD 卡都不适合用于开发工作,因为任何系统更改都会涉及重新编程固件镜像。

准备就绪

开发工作的理想设置是,在您的主机系统中同时使用 TFTP 和 NFS 服务器,并且仅在 eMMC 或 microSD 卡中存储 U-Boot 启动加载程序。通过这种设置,启动加载程序将从 TFTP 服务器获取 Linux 内核,而内核将从 NFS 服务器挂载根文件系统。对内核或根文件系统的更改可以在不重新编程的情况下进行。只有启动加载程序的开发工作需要重新编程物理介质。

安装 TFTP 服务器

如果您尚未运行 TFTP 服务器,请按照以下步骤在您的 Ubuntu 14.04 主机上安装和配置 TFTP 服务器:

$ sudo apt-get install tftpd-hpa

tftpd-hpa 配置文件安装在 /etc/default/tftpd-hpa 中。默认情况下,它使用 /var/lib/tftpboot 作为根 TFTP 文件夹。使用以下命令更改文件夹权限,使其对所有用户可访问:

$ sudo chmod 1777 /var/lib/tftpboot

现在,按如下方式将 Linux 内核和设备树从您的 build 目录复制过来:

$ cd /opt/yocto/fsl-community-bsp/wandboard- quad/tmp/deploy/images/wandboard-quad/
$ cp zImage-wandboard-quad.bin zImage-imx6q-wandboard.dtb /var/lib/tftpboot

安装 NFS 服务器

如果您尚未运行 NFS 服务器,请按照以下步骤在您的 Ubuntu 14.04 主机上安装和配置 NFS 服务器:

$ sudo apt-get install nfs-kernel-server

我们将使用 /nfsroot 目录作为 NFS 服务器的根目录,因此我们将在此目录中“解压”目标的根文件系统,从我们的 Yocto build 目录中:

$ sudo mkdir /nfsroot
$ cd /nfsroot
$ sudo tar xvf /opt/yocto/fsl-community-bsp/wandboard- quad/tmp/deploy/images/wandboard-quad/core-image-minimal-wandboard- quad.tar.bz2

接下来,我们将配置 NFS 服务器来导出 /nfsroot 文件夹:

/etc/exports:
/nfsroot/ *(rw,no_root_squash,async,no_subtree_check)

然后,我们将重新启动 NFS 服务器,以使配置更改生效:

$ sudo service nfs-kernel-server restart

如何操作...

启动 Wandboard,并通过在串口控制台按任意键停止在 U-Boot 提示符处。然后执行以下步骤:

  1. 通过 DHCP 获取 IP 地址:

    > dhcp
    
    

    另外,您可以使用以下命令配置静态 IP 地址:

    > setenv ipaddr <static_ip>
    
    
  2. 配置您的主机系统的 IP 地址,TFTP 和 NFS 服务器已在此系统上设置:

    > setenv serverip <host_ip>
    
    
  3. 配置根文件系统挂载:

    > setenv nfsroot /nfsroot
    
    
  4. 配置 Linux 内核和设备树文件名:

    > setenv image zImage-wandboard-quad.bin
    > setenv fdt_file zImage-imx6q-wandboard.dtb
    
    
  5. 如果您已经配置了静态 IP 地址,您需要通过运行以下命令禁用 DHCP 启动:

    > setenv ip_dyn no
    
    
  6. 将 U-Boot 环境保存到 microSD 卡:

    > saveenv
    
    
  7. 执行网络启动:

    > run netboot
    
    

Linux 内核和设备树将从 TFTP 服务器获取,而根文件系统将在从您的网络获取 DHCP 地址后由内核从 NFS 共享挂载(除非使用静态 IP 地址)。

您应该能够使用 root 用户登录,而无需密码提示。

共享下载

您通常会同时处理多个项目,可能针对不同的硬件平台或不同的目标镜像。在这种情况下,通过共享 downloads 来优化构建时间是非常重要的。

准备就绪

构建系统会在多个地方搜索下载的源代码:

  • 它会尝试使用本地的downloads文件夹。

  • 它会检查配置的预镜像,这些镜像通常是本地的,属于你的组织。

  • 然后,它会尝试从在包配方中配置的上游源获取数据。

  • 最后,它会检查配置的镜像。镜像是源代码的公共备用位置。

如果在这四个地方都没有找到包源,包构建将会失败并报错。当从上游获取失败并尝试镜像时,还会发出构建警告,以便可以查看上游问题。

Yocto 项目维护了一组镜像,以将构建系统与上游服务器的问题隔离开来。然而,在添加外部层时,你可能会添加一些不在 Yocto 项目镜像服务器或其他配置镜像中的包,因此建议你保持一个本地预镜像,以避免源代码不可用的问题。

新项目的默认 Poky 设置是将下载的包源存储在当前的build目录中。这是构建系统首先搜索源downloads的地方。这个设置可以在项目的conf/local.conf文件中通过DL_DIR配置变量来进行配置。

如何操作...

为了优化构建时间,建议在所有项目之间共享一个downloads目录。meta-fsl-arm层的setup-environment脚本会将默认的DL_DIR更改为repo工具创建的fsl-community-bsp目录。在这种设置下,downloads文件夹将在主机系统中的所有项目之间共享。配置如下:

DL_DIR ?= "${BSPDIR}/downloads/"

一种更具可扩展性的设置(例如,适用于远程分布的团队)是配置一个预镜像。例如,可以在你的conf/local.conf文件中添加以下内容:

INHERIT += "own-mirrors"
SOURCE_MIRROR_URL = "http://example.com/my-source-mirror"

一种常见的设置是让构建服务器提供它的downloads目录。构建服务器可以配置为准备 Git 目录的 tarball 文件,以避免从上游服务器执行 Git 操作。这个设置会影响构建性能,但通常在构建服务器中是可以接受的。添加以下内容:

BB_GENERATE_MIRROR_TARBALLS = "1"

这种设置的一个优势是,构建服务器的downloads文件夹也可以备份,以保证未来产品的源代码可用性。这在需要长期可用性的嵌入式产品中尤为重要。

为了测试这个设置,你可以检查是否仅通过使用预镜像就能进行构建,方法如下:

BB_FETCH_PREMIRRORONLY = "1"

这个设置可以通过在项目创建过程中使用TEMPLATECONF变量,将conf/local.conf文件中的设置分发到团队成员。

共享共享状态缓存

Yocto 项目从源代码开始构建一切。当你创建一个新项目时,只会创建配置文件。然后,构建过程会从头开始编译所有内容,包括交叉编译工具链和一些对构建重要的本地工具。

这个过程可能需要很长时间,Yocto 项目实现了一个共享状态缓存机制,用于增量构建,旨在仅为给定更改构建严格必要的组件。

为了使这一点生效,构建系统会计算给定任务的输入数据的校验和。如果输入数据发生变化,则需要重新构建该任务。简单来说,构建过程为每个任务生成一个可以进行校验和对比的运行脚本。它还会跟踪任务的输出,以便可以重用。

一个包食谱可以修改共享状态缓存到某个任务;例如,通过将其标记为 nostamp 来强制始终重新构建。有关共享状态缓存机制的更深入解释,请参见 Yocto 项目参考手册,网址为 www.yoctoproject.org/docs/1.7.1/ref-manual/ref-manual.html

如何操作……

默认情况下,构建系统将在你的 build 目录中使用一个名为 sstate-cache 的共享状态缓存目录来存储缓存数据。你可以通过在 conf/local.conf 文件中配置 SSTATE_DIR 来更改这一点。缓存数据存储在以哈希值前两位字符命名的目录中。目录内的文件名包含完整的任务校验和,因此可以通过查看文件名来确定缓存的有效性。构建过程设置场景任务将评估缓存数据并在有效时使用它来加速构建。

当你想从一个干净的状态开始构建时,你需要删除 sstate-cache 目录和 tmp 目录。

你还可以通过在运行 BitBake 时使用 --no-setscene 参数,指示 BitBake 忽略共享状态缓存。

备份干净的共享状态缓存是一个好习惯(例如,从构建服务器备份),以防共享状态缓存损坏时可以使用。

还有更多内容……

共享共享状态缓存是可能的;然而,这需要小心处理。并非所有更改都会被共享状态缓存实现检测到,当发生这种情况时,部分或所有缓存需要失效。这可能会在共享状态缓存被共享时引发问题。

在这种情况下的建议取决于使用场景。致力于 Yocto 元数据的开发人员应将共享状态缓存保持为默认设置,并按项目分开存储。

然而,验证和测试工程师、内核和引导加载程序开发人员以及应用程序开发人员可能会从一个维护良好的共享状态缓存中受益。

要配置一个 NFS 共享驱动器供开发团队共享以加速构建,你可以将以下内容添加到你的 conf/local.conf 配置文件中:

SSTATE_MIRRORS ?= "\
     file://.* file:///nfs/local/mount/sstate/PATH"

这个例子中的 PATH 表达式将被构建系统替换为一个以哈希值的前两个字符命名的目录。

设置软件包源

嵌入式系统项目很少需要对 Yocto 构建系统进行更改。大部分时间和精力都花费在应用程序开发上,接下来是可能的内核和引导加载程序开发。

因此,整个系统的重建可能很少进行。一个新项目通常是从一个预构建的共享状态缓存中构建的,应用程序开发工作仅需对少数几个包执行完整或增量构建。

一旦软件包构建完成,它们需要安装到目标系统上进行测试。模拟机对于应用程序开发来说是可以的,但大多数与硬件相关的工作需要在嵌入式硬件上完成。

准备就绪

一种选择是手动将构建的二进制文件复制到目标的根文件系统中,可以将其复制到目标从主机系统挂载其根文件系统的 NFS 共享中(如之前在为开发环境配置网络启动配方中所述),或者使用 SCP、FTP,甚至 microSD 卡等其他方法。

这个方法也被像 Eclipse 这样的集成开发环境(IDE)用于调试你正在开发的应用程序。然而,当你需要安装多个软件包和依赖项时,这种方法的扩展性较差。

下一个选项是将打包后的二进制文件(即 RPM、deb 或 ipk 包)复制到目标的文件系统中,然后使用目标的包管理系统进行安装。要使此方法有效,目标的文件系统需要内建包管理工具。做到这一点很简单,只需将 package-management 功能添加到根文件系统中;例如,你可以将以下行添加到项目的 conf/local.conf 文件中:

EXTRA_IMAGE_FEATURES += "package-management"

所以对于 RPM 包,你将它复制到目标设备上,并使用 rpmsmart 工具来安装它。smart 包管理工具是 GPL 许可证的,可以与多种包格式一起使用。

然而,最优化的方法是将主机系统包的输出目录转换为一个包源。例如,如果你使用的是默认的 RPM 包格式,你可以将 build 目录中的 tmp/deploy/rpm 转换为目标可以用来更新的包源。

为了使这一切正常工作,你需要在你的计算机上配置一个 HTTP 服务器来提供软件包。

版本管理软件包

你还需要确保生成的包具有正确的版本,这意味着每次更改时都需要更新配方修订版本,PR。虽然可以手动完成此操作,但推荐的—也是如果你想使用软件包源时必须使用的—方法是使用 PR 服务器。

然而,PR 服务器默认是未启用的。没有 PR 服务器生成的软件包彼此之间是一致的,但对于已运行的系统没有更新保证。

最简单的 PR 服务器配置是将其本地运行在你的主机系统上。为此,你需要将以下内容添加到你的conf/local.conf文件中:

PRSERV_HOST = "localhost:0"

使用此设置,可以保证更新的一致性。

如果你想与其他开发者共享你的软件包源,或者你正在配置构建服务器或软件包服务器,可以通过运行以下命令来运行一个 PR 服务器的单实例:

$ bitbake-prserv --host <server_ip> --port <port> --start

然后,你将更新项目的构建配置,使用集中式 PR 服务器,并编辑conf/local.conf如下:

PRSERV_HOST = "<server_ip>:<port>"

此外,如果你之前使用了共享状态缓存,所有共享状态缓存的贡献者都需要使用相同的 PR 服务器。

一旦确保了软件包源的完整性,我们需要配置一个 HTTP 服务器来提供该源。

如何操作...

我们将在本示例中使用lighttpd,因为它轻量且易于配置。按照以下步骤操作:

  1. 安装 Web 服务器:

    $ sudo apt-get install lighttpd
    
    
  2. 默认情况下,/etc/lighttpd/lighttpd.conf配置文件中指定的文档根目录为/var/www/,因此我们只需要为我们的软件包源创建一个符号链接:

    $ sudo mkdir /var/www/wandboard-quad
    $ sudo ln -s /opt/yocto/fsl-community-bsp/wandboard- quad/tmp/deploy/rpm /var/www/wandboard-quad/rpm
    
    

    接下来,按如下方式重新加载配置:

    $ sudo service lighttpd reload
    
    
  3. 刷新软件包索引。这需要手动执行,以便在每次构建后更新软件包源:

    $ bitbake package-index
    
    
  4. 然后,我们需要使用新的软件包源配置目标文件系统:

    # smart channel --add all type=rpm-md \ baseurl=http://<server_ip>/wandboard-quad/rpm/all
    
    # smart channel --add wandboard_quad type=rpm-md \ baseurl=http://<server_ip>/wandboard-quad/rpm/wandboard_quad
    
    # smart channel --add cortexa9hf_vfp_neon type=rpm-md \ baseurl=http://<server_ip>/wandboard- quad/rpm/cortexa9hf_vfp_neon
    
    
  5. 一旦设置完成,我们将能够通过以下方式查询和更新目标根文件系统中的软件包:

    # smart update
    # smart query <package_name>
    # smart install <package_name>
    
    

为了使此更改在目标的根文件系统中保持持久性,我们可以在编译时通过在conf/local.conf中使用PACKAGE_FEED_URIS变量来配置软件包源,如下所示:

PACKAGE_FEED_URIS = "http://<server_ip>/wandboard-quad"

另见

使用构建历史

在维护嵌入式产品的软件时,你需要一种方法来了解什么内容发生了变化,以及它将如何影响你的产品。

在 Yocto 系统上,你可能需要更新软件包修订版本(例如,修复安全漏洞),并且你需要确保这项更改的影响;例如,软件包依赖关系和根文件系统的更改。

构建历史让你能够做到这一点,我们将在本教程中详细探讨。

如何操作...

要启用构建历史,请将以下内容添加到你的conf/local.conf文件中:

INHERIT += "buildhistory"

以下配置启用了信息收集,包括依赖关系图:

BUILDHISTORY_COMMIT = "1"

上述代码行启用了将构建历史存储到本地 Git 仓库的功能。

Git 仓库的位置可以通过BUILDHISTORY_DIR变量来设置,默认情况下,该变量设置为build目录下的buildhistory目录。

默认情况下,buildhistory跟踪包、镜像和 SDK 的变化。通过BUILDHISTORY_FEATURES变量可以进行配置。例如,仅跟踪镜像的变化,可以在conf/local.conf中添加如下内容:

BUILDHISTORY_FEATURES = "image"

它还可以跟踪特定文件,并将其复制到buildhistory目录。默认情况下,这仅包括/etc/passwd/etc/groups,但它也可以用于跟踪任何重要文件,如安全证书。需要在你的conf/local.conf文件中使用BUILDHISTORY_IMAGE_FILES变量添加这些文件,如下所示:

BUILDHISTORY_IMAGE_FILES += "/path/to/file"

构建历史会减慢构建速度,增加构建大小,并可能使 Git 目录变得无法管理。建议在构建服务器上启用它,用于软件发布,或在特定情况下启用,比如更新生产环境中的软件。

它是如何工作的...

启用时,它会以 Git 仓库的形式记录每个包和镜像的变化,且可以被探索和分析。

对于一个包,它记录以下信息:

  • 包和配方修订版

  • 依赖关系

  • 包的大小

  • 文件

对于一个镜像,它记录以下信息:

  • 构建配置

  • 依赖关系图

  • 包含文件列表,其中包括所有权和权限

  • 已安装包的列表

对于 SDK,它记录以下信息:

  • SDK 配置

  • 主机和目标文件的列表,包括所有权和权限

  • 依赖关系图

  • 已安装包的列表

查看构建历史

使用构建历史检查 Git 目录有多种方式:

  • 使用像 gitk 或 git log 这样的 Git 工具。

  • 使用buildhistory-diff命令行工具,显示人类可读格式的差异。

  • 使用基于 Django-1.4 的 Web 界面。每次构建后,你需要将构建历史数据导入到应用程序的数据库中。详细信息请参阅git.yoctoproject.org/cgit/cgit.cgi/buildhistory-web/tree/README

还有更多...

为了维护构建历史,重要的是优化它并避免其随着时间增长。定期备份构建历史并清理旧数据,对于保持构建历史仓库的可管理大小至关重要。

一旦buildhistory目录已被备份,以下过程将修剪它并保留最新的历史:

  1. 将你的仓库复制到临时的 RAM 文件系统(tmpfs)中以加速操作。使用df -h命令检查输出,查看哪些目录是tmpfs文件系统,并查看它们的可用空间,然后选择一个。例如,在 Ubuntu 中,/run/shm目录是可用的。

  2. 为一个没有父节点的提交添加一个一个月前的合并点:

    $ git rev-parse "HEAD@{1 month ago}" > .git/info/grafts
    
    
  3. 使合并点永久生效:

    $ git filter-branch
    
    
  4. 克隆一个新仓库以清理剩余的 Git 对象:

    $ git clone file://${tmpfs}/buildhistory buildhistory.new
    
    
  5. 用新的清理过的buildhistory目录替换旧的目录:

    $ rm -rf buildhistory
    $ mv buildhistory.new buildhistory
    
    

使用构建统计信息

构建系统可以按任务和镜像收集构建信息。数据可用于识别构建时间的优化领域和瓶颈,特别是当系统中添加新的配方时。本配方将解释构建统计信息的工作原理。

如何执行……

要启用统计数据收集,您的项目需要通过将 buildstats 类添加到 conf/local.conf 文件中的 USER_CLASSES 来继承它。默认情况下,fsl-community-bsp 构建项目已配置为启用它们。

USER_CLASSES ?= "buildstats"

您可以使用 BUILDSTATS_BASE 变量配置这些统计信息的位置,默认情况下,它被设置为 buildstats 文件夹,该文件夹位于 build 目录下的 tmp 目录中(tmp/buildstats)。

buildstats 文件夹包含每个镜像的文件夹,镜像下有一个 timestamp 文件夹,里面会有每个包的子目录,并且会包含一个 build_stats 文件,其中包含:

  • 主机系统信息

  • 根文件系统的位置和大小

  • 构建时间

  • 平均 CPU 使用率

  • 磁盘统计信息

它是如何工作的……

数据的准确性取决于下载目录 DL_DIR 和共享状态缓存目录 SSTATE_DIR 是否存在于同一分区或卷中,因此如果您计划使用构建数据,可能需要相应地配置它们。

一个示例 build-stats 文件如下所示:

Host Info: Linux agonzal 3.13.0-35-generic #62-Ubuntu SMP Fri Aug 15 01:58:42 UTC 2014 x86_64 x86_64
Build Started: 1411486841.52
Uncompressed Rootfs size: 6.2M  /opt/yocto/fsl-community- bsp/wandboard-quad/tmp/work/wandboard_quad-poky-linux- gnueabi/core-image-minimal/1.0-r0/rootfs
Elapsed time: 2878.26 seconds
CPU usage: 51.5%
EndIOinProgress: 0
EndReadsComp: 0
EndReadsMerged: 55289561
EndSectRead: 65147300
EndSectWrite: 250044353
EndTimeIO: 14415452
EndTimeReads: 10338443
EndTimeWrite: 750935284
EndWTimeIO: 816314180
EndWritesComp: 0
StartIOinProgress: 0
StartReadsComp: 0
StartReadsMerged: 52319544
StartSectRead: 59228240
StartSectWrite: 207536552
StartTimeIO: 13116200
StartTimeReads: 8831854
StartTimeWrite: 3861639688
StartWTimeIO: 3921064032
StartWritesComp: 0

这些磁盘统计信息来自 Linux 内核磁盘 I/O 统计信息(www.kernel.org/doc/Documentation/iostats.txt)。不同的元素在这里进行了说明:

  • ReadsComp:这是已完成的读取操作总数

  • ReadsMerged:这是合并的相邻读取的总数

  • SectRead:这是读取的扇区总数

  • TimeReads:这是读取操作花费的总毫秒数

  • WritesComp:这是已完成的写入操作的总数

  • SectWrite:这是已写入的扇区总数

  • TimeWrite:这是写入操作花费的总毫秒数

IOinProgress:这是读取 /proc/diskstats 时正在进行的 I/O 操作总数

  • TimeIO:这是执行 I/O 操作所花费的总毫秒数

  • WTimeIO:这是执行 I/O 时所花费的加权时间总数

在每个包内,我们会列出一些任务;例如,对于 ncurses-5.9-r15.1,我们有以下任务:

  • do_compile

  • do_fetch

  • do_package

  • do_package_write_rpm

  • do_populate_lic

  • do_rm_work

  • do_configure

  • do_install

  • do_packagedata

  • do_patch

  • do_populate_sysroot

  • do_unpack

每个任务都包含以下内容,格式与之前相同:

  • 构建时间

  • CPU 使用率

  • 磁盘统计信息

还有更多……

您还可以使用 Poky 源代码中包含的 pybootchartgui.py 工具获取数据的图形表示。从您的项目的 build 文件夹中,您可以执行以下命令,在 /tmp 中获取 bootchart.png 图形:

$ ../sources/poky/scripts/pybootchartgui/pybootchartgui.py tmp/buildstats/core-image-minimal-wandboard-quad/ -o /tmp

调试构建系统

在本章的最后一个食谱中,我们将探索可用于调试构建系统及其元数据问题的不同方法。

准备工作

让我们首先介绍一些调试会话中常见的用例。

查找食谱

检查当前层中是否支持某个特定包的一种好方法是如下搜索:

$ find -name "*busybox*"

这将递归地搜索所有层,查找 BusyBox 模式。你可以通过执行以下命令将搜索限制在食谱和附加文件中:

$ find -name "*busybox*.bb*"

转储 BitBake 的环境

在开发或调试包或镜像食谱时,通常会要求 BitBake 列出其环境,既可以是全局环境,也可以是特定目标的环境,无论是包还是镜像。

要转储全局环境并使用 grep 查找感兴趣的变量(例如,DISTRO_FEATURES),请使用以下命令:

$ bitbake -e | grep -w DISTRO_FEATURES

可选地,若要定位某个特定包食谱(如 BusyBox)的源目录,请使用以下命令:

$ bitbake -e busybox | grep ^S=

你还可以执行以下命令来定位某个包或镜像食谱的工作目录:

$ bitbake -e <target> | grep ^WORKDIR=

使用开发 shell

BitBake 提供了 devshell 任务来帮助开发者。它可以通过以下命令执行:

$ bitbake -c devshell <target>

它将解包并修补源代码,并在目标源目录中打开一个新的终端(它会自动检测你的终端类型,或者可以通过 OE_TERMINAL 设置),该目录的环境已正确设置。

注意

在图形环境中,devshell 会打开一个新的终端或控制台窗口,但如果我们在非图形环境中工作,如 telnet 或 SSH,你可能需要在 conf/local.conf 配置文件中指定 screen 作为终端,如下所示:

OE_TERMINAL = "screen"

在 devshell 中,你可以运行开发命令如 configuremake,或直接调用交叉编译器(使用已设置好的 $CC 环境变量)。

如何操作...

调试包构建错误的起点是 BitBake 在构建过程中打印的错误信息。通常这会指向我们未能成功构建的任务。

要列出给定食谱中所有可用的任务及其描述,请执行以下命令:

$ bitbake -c listtasks <target>

如果你需要重现错误,可以使用以下命令强制进行构建:

$ bitbake -f <target>

或者你可以要求 BitBake 强制只运行特定任务,使用以下命令:

$ bitbake -c compile -f <target>

任务日志和运行文件

为了调试构建错误,BitBake 会为每个 shell 任务创建两种有用的文件,并将它们存储在工作目录的 temp 文件夹中。以 BusyBox 为例,我们可以查看:

 /opt/yocto/fsl-community-bsp/wandboard-quad/tmp/work/cortexa9hf- vfp-neon-poky-linux-gnueabi/busybox/1.22.1-r32/temp

然后找到日志和运行文件的列表。文件名格式为:

log.do_<task>.<pid>

以及 run.do_<task>.<pid>

幸运的是,我们还有符号链接,去掉了 pid 部分,链接到最新版本。

日志文件将包含任务的输出,通常这是我们调试问题所需的唯一信息。运行文件包含 BitBake 执行的实际代码,用来生成之前提到的日志。这仅在调试复杂构建问题时需要。

另一方面,Python 任务目前并不会像之前描述的那样写入文件,尽管未来计划这样做。Python 任务是在内部执行并将信息记录到终端。

为配方添加日志记录

BitBake 配方接受 bash 或 Python 代码。Python 日志记录通过 bb 类完成,并使用标准的 Python 日志库模块。它包含以下组件:

  • bb.plain:这个函数使用 logger.plain。它可以用于调试,但不应提交到源代码中。

  • bb.note:这个函数使用 logger.info

  • bb.warn:这个函数使用 logger.warn

  • bb.error:这个函数使用 logger.error

  • bb.fatal:这个函数使用 logger.critical 并退出 BitBake。

  • bb.debug:此函数应该作为第一个参数传递日志级别,并使用 logger.debug

为了在我们的配方中打印来自 bash 的调试输出,我们需要通过执行以下命令来使用 logging 类:

inherit logging

logging 类默认由所有包含 base.bbclass 的配方继承,因此我们通常不需要显式继承它。继承后,我们将可以访问以下 bash 函数,这些函数会将输出记录到之前描述过的工作目录内 temp 目录中的日志文件(而非控制台):

  • bbplain:这个函数字面输出传递给它的内容。它可以用于调试,但不应提交到配方源代码中。

  • bbnote:这个函数会以 NOTE 前缀打印信息。

  • bbwarn:这个函数打印一个带有 WARNING 前缀的非致命警告。

  • bberror:这会打印一个带有ERROR前缀的非致命错误。

  • bbfatal:该函数会中止构建并打印错误消息,类似于 bberror

  • bbdebug:这个函数根据作为第一个参数传递的日志级别打印调试消息。使用格式如下:

    bbdebug [123] "message"
    

    提示

    这里提到的 bash 函数不会记录到控制台,只会记录到日志文件中。

查看依赖关系

你可以通过以下命令让 BitBake 打印当前及提供的包版本:

$ bitbake --show-versions

另一个常见的调试任务是删除不需要的依赖项。

要查看拉取的依赖项概览,可以通过运行以下命令来使用 BitBake 的详细输出:

$ bitbake -v <target>

要分析一个包所拉取的依赖项,我们可以让 BitBake 创建描述这些依赖关系的 DOT 文件,通过运行以下命令:

$ bitbake -g <target>

DOT 格式是一种图形描述的文本语言,可以被 GraphViz 开源包及其所有使用该包的工具理解。DOT 文件可以被可视化或进一步处理。

你可以从图中省略依赖项,以产生更易读的输出。例如,要省略来自 glibc 的依赖项,可以运行以下命令:

$ bitbake -g <target> -I glibc

一旦执行了前面的命令,我们将在当前目录中得到三个文件:

  • package-depends.dot:此文件显示了运行时目标之间的依赖关系

  • pn-depends.dot:此文件显示了食谱之间的依赖关系

  • task-depends.dot:此文件显示了任务之间的依赖关系

还有一个 pn-buildlist 文件,其中列出了给定目标将要构建的包。

要将 .dot 文件转换为 PostScript 文件(.ps),你可以执行:

$ dot -Tps filename.dot -o outfile.ps

然而,显示依赖数据的最有用方式是让 BitBake 通过依赖关系探索器以图形方式显示它,如下所示:

$ bitbake -g -u depexp <target>

结果可以在以下截图中看到:

查看依赖关系

调试 BitBake

调试 BitBake 本身并不常见,但你可能会在 BitBake 中发现一个 bug,并希望在报告给 BitBake 社区之前自己探索它。对于这种情况,你可以使用 -D 标志要求 BitBake 输出三种不同级别的调试信息。要显示所有调试信息,请运行以下命令:

$ bitbake -DDD <target>

错误报告工具

有时候,你会在没有修改的 Yocto 食谱中发现构建错误。检查错误的第一个地方是社区本身,但在启动邮件客户端之前,先访问 errors.yoctoproject.org

这是一个用户报告错误的中央数据库。在这里,你可以检查是否有其他人也遇到相同的问题。

你可以将自己的构建失败提交到数据库,帮助社区调试问题。为此,你可以使用 report-error 类。在你的 conf/local.conf 文件中添加以下内容:

INHERIT += "report-error"

默认情况下,错误信息存储在 build 目录下的 tmp/log/error-report 中,但你可以通过 ERR_REPORT_DIR 变量设置一个特定的位置。

当错误报告工具被激活时,构建错误将被捕获到 error-report 文件夹中的一个文件里。构建输出还会打印出一个命令,用于将错误日志发送到服务器:

$ send-error-report ${LOG_DIR}/error-report/error-report_${TSTAMP}

当执行此命令时,它将返回一个链接,指向上游错误。

你可以设置一个本地错误服务器,并通过传递服务器参数来使用它。错误服务器的代码和设置详细信息可以在 git.yoctoproject.org/cgit/cgit.cgi/error-report-web/tree/README 找到。

还有更多……

虽然你可以使用 Linux 工具来解析 Yocto 的元数据和构建输出,但 BitBake 缺少一个用于常见任务的命令行基础 UI。一个旨在提供此功能的项目是 bb,可以在 github.com/kergoth/bb 找到。

要使用它,你需要通过执行以下命令将仓库克隆到本地:

$ cd /opt/yocto/fsl-community-bsp/sources
$ git clone https://github.com/kergoth/bb.git

然后运行 bb/bin/bb init 命令,它会提示你将一个 bash 命令添加到 ~/.bash_profile 文件中。

你可以选择执行该操作,或者在当前的 shell 中按如下方式执行:

$ eval "$(/opt/yocto/fsl-community-bsp/sources/bb/bin/bb init -)"

你需要像往常一样先设置好环境:

$ cd /opt/yocto/fsl-community-bsp
$ source setup-environment wandboard-quad

注意

有些命令只有在工作目录已填充时才能使用,因此如果你想使用 bb,可能需要删除 rm_work 类。

bb 工具可以简化的部分任务包括:

  • 探索包的内容:

    $ bb contents <target>
    
    
  • 在配方中搜索某个模式:

    $ bb search <pattern>
    
    
  • 显示全局 BitBake 环境或某个特定包的环境,并使用 grep 查找特定变量:

    $ bb show -r <recipe> <variable>
    
    

第二章。BSP 层

在本章中,我们将涵盖以下配方:

  • 创建定制的 BSP 层

  • 介绍系统开发工作流

  • 添加自定义内核和引导加载程序

  • 构建 U-Boot 引导加载程序

  • 解释 Yocto 对 Linux 内核的支持

  • 描述 Linux 的构建系统

  • 配置 Linux 内核

  • 构建 Linux 源代码

  • 构建外部内核模块

  • 调试 Linux 内核和模块

  • 调试 Linux 内核引导过程

  • 使用内核追踪系统

  • 管理设备树

  • 调试设备树问题

介绍

一旦我们的构建环境准备好并且 Yocto 项目已经就绪,就可以开始考虑开始开发我们的嵌入式 Linux 项目。

大多数嵌入式 Linux 项目都需要定制硬件和软件。开发过程中的一项早期任务是测试不同的硬件参考板,并选择一个作为我们设计的基础。我们选择了 Wandboard,这是一个基于 Freescale i.MX6 的开发平台,它价格实惠且开源,非常适合我们的需求。

在嵌入式项目中,通常最好尽早开始软件开发,可能在硬件原型准备好之前,这样就可以直接开始与参考设计一起工作。

但是,在某个阶段,硬件原型将准备就绪,需要在 Yocto 中引入更改以支持新硬件。

本章将解释如何创建一个 BSP 层来包含这些硬件特定的更改,并展示如何与 U-Boot 引导加载程序和 Linux 内核进行工作,这些组件可能需要进行最多的定制工作。

创建定制的 BSP 层

这些定制更改保存在一个单独的 Yocto 层中,称为板支持包BSP)层。这种分离方式对系统的未来更新和补丁是最有利的。一个 BSP 层可以支持任意数量的新机器和任何与硬件相关的新软件功能。

如何做到这一点...

按照约定,Yocto 层的名称以meta开头,表示元数据。BSP 层可以添加bsp关键字,最后加上一个唯一的名称。我们将把我们的层命名为meta-bsp-custom

有几种方法可以创建一个新层:

  • 手动操作,一旦你知道需要做什么

  • 通过复制 Poky 中包含的meta-skeleton

  • 通过使用yocto-layer命令行工具

你可以查看 Poky 中的meta-skeleton层,看到它包含以下元素:

  • 一个layer.conf文件,用于设置层的配置变量

  • 一个COPYING.MIT许可证文件

  • 许多名为recipes前缀的目录,其中包含 BusyBox、Linux 内核和示例模块的配方,示例服务配方,示例用户管理配方以及多库示例。

它是如何工作的...

我们将在接下来的几个食谱中介绍一些可用示例中的用例,因此对于我们的需求,我们将使用 yocto-layer 工具,它允许我们创建一个最小化的层。

打开一个新的终端并切换到 fsl-community-bsp 目录。然后按如下方式设置环境:

$ source setup-environment wandboard-quad

注意

请注意,一旦创建了 build 目录,MACHINE 变量已经在 conf/local.conf 文件中配置好了,可以从命令行中省略。

切换到 sources 目录并运行:

$ yocto-layer create bsp-custom

请注意,yocto-layer 工具会为你的层添加 meta 前缀,因此你不需要手动添加。它会提示几个问题:

  • 层优先级用于决定当同一个食谱(同名)同时存在于多个层中时,哪个层的优先级更高。它还用于决定如果多个层同时追加同一个食谱,bbappends 应该按照什么顺序应用。请保留默认值 6。这个值将存储在该层的 conf/layer.conf 文件中,作为 BBFILE_PRIORITY

  • 是否创建示例食谱并追加文件。暂时我们保持默认值 no

我们的新层具有以下结构:

meta-bsp-custom/
 conf/layer.conf
 COPYING.MIT
 README

还有更多…

第一步是将这个新层添加到你项目的 conf/bblayer.conf 文件中。最好也将其添加到模板配置目录的 bblayers.conf.sample 文件中,以便在创建新项目时正确地附加它。以下代码中的高亮行展示了将该层添加到 conf/bblayers.conf 文件中的方式:

LCONF_VERSION = "6"

BBPATH = "${TOPDIR}"
BSPDIR := "${@os.path.abspath(os.path.dirname(d.getVar('FILE', True)) + '/../..')}"

BBFILES ?= ""
BBLAYERS = " \
  ${BSPDIR}/sources/poky/meta \
  ${BSPDIR}/sources/poky/meta-yocto \
  \
  ${BSPDIR}/sources/meta-openembedded/meta-oe \
  ${BSPDIR}/sources/meta-openembedded/meta-multimedia \
  \
  ${BSPDIR}/sources/meta-fsl-arm \
  ${BSPDIR}/sources/meta-fsl-arm-extra \
  ${BSPDIR}/sources/meta-fsl-demos \
  ${BSPDIR}/sources/meta-bsp-custom \
"

现在,BitBake 将解析 bblayers.conf 文件,并从你的层中找到 conf/layers.conf 文件。在其中,我们可以找到以下行:

BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \
        ${LAYERDIR}/recipes-*/*/*.bbappend"

它告诉 BitBake 需要解析哪些目录以获取食谱和追加文件。你需要确保这个新层中的目录和文件层级符合给定的模式,否则你需要进行修改。

BitBake 还会找到以下内容:

BBPATH .= ":${LAYERDIR}"

BBPATH 变量用于定位 bbclass 文件以及通过 includerequire 指令包含的配置文件和文件。搜索会在找到第一个匹配项时结束,因此最好保持文件名唯一。

我们可能会考虑在 conf/layer.conf 文件中定义的其他一些变量包括:

LAYERDEPENDS_bsp-custom = "fsl-arm"
LAYERVERSION_bsp-custom = "1"

LAYERDEPENDS 字面量是一个以空格分隔的其他层的列表,表示你的层依赖的其他层,而 LAYERVERSION 字面量指定你的层的版本,以防其他层希望添加对特定版本的依赖。

COPYING.MIT 文件指定了该层中元数据的许可证。Yocto 项目采用 MIT 许可证,该许可证与 通用公共许可证GPL)兼容。此许可证仅适用于元数据,因为构建中包含的每个软件包都有自己的许可证。

README 文件需要根据你的特定层进行修改。通常在文件中描述该层并提供任何其他层的依赖关系和使用说明。

添加新机器

当自定义你的 BSP 时,通常是为你的硬件引入一个新的机器配置。这些配置文件保存在你的 BSP 层中的conf/machine目录下。通常做法是基于参考设计。例如,wandboard-quad有如下的机器配置文件:

include include/wandboard.inc

SOC_FAMILY = "mx6:mx6q:wandboard"

UBOOT_MACHINE = "wandboard_quad_config"

KERNEL_DEVICETREE = "imx6q-wandboard.dtb"

MACHINE_FEATURES += "bluetooth wifi"

MACHINE_EXTRA_RRECOMMENDS += " \
  bcm4329-nvram-config \
  bcm4330-nvram-config \
"

基于 Wandboard 设计的机器可以定义自己的机器配置文件,wandboard-quad-custom.conf,内容如下:

include conf/machine/include/wandboard.inc

SOC_FAMILY = "mx6:mx6q:wandboard"

UBOOT_MACHINE = "wandboard_quad_custom_config"

KERNEL_DEVICETREE = "imx6q-wandboard-custom.dtb"

MACHINE_FEATURES += "wifi"

现在,wandboard.inc文件位于一个不同的层中,因此为了让 BitBake 找到它,我们需要在相应层中从BBPATH变量指定完整路径。此机器除定义自己的 U-Boot 配置文件和 Linux 内核设备树外,还定义了自己的一组机器特性。

向 Linux 内核添加自定义设备树

为了将这个设备树文件添加到 Linux 内核中,我们需要将设备树文件添加到 Linux 内核源代码下的arch/arm/boot/dts目录,并且还需要修改 Linux 构建系统中的arch/arm/boot/dts/Makefile文件,以便像下面这样构建它:

dtb-$(CONFIG_ARCH_MXC) += \
+imx6q-wandboard-custom.dtb \

这段代码使用了 diff 格式,其中带有减号前缀的行是删除的,带有加号前缀的行是新增的,而没有前缀的行作为参考保留。

一旦补丁准备好,它可以被添加到meta-bsp-custom/recipes-kernel/linux/linux-wandboard-3.10.17/目录,并且 Linux 内核配方会附加一个meta-bsp-custom/recipes-kernel/linux/linux-wandboard_3.10.17.bbappend文件,内容如下:

SRC_URI_append = " file://0001-ARM-dts-Add-wandboard-custom-dts- file.patch"

添加自定义设备树到 Linux 内核的示例补丁可以在随书籍附带的源代码中找到。

添加自定义的 U-Boot 机器

以同样的方式,可以对 U-Boot 源代码进行补丁,以添加一个新的自定义机器。然而,启动加载程序的修改不像内核修改那样常见,大多数自定义平台将保持启动加载程序不变。补丁将被添加到meta-bsp-custom/recipes-bsp/u-boot/u-boot-fslc-v2014.10/目录,并且 U-Boot 配方会附加一个meta-bsp-custom/recipes-bsp/u-boot/u-boot-fslc_2014.10.bbappend文件,内容如下:

SRC_URI_append = " file://0001-boards-Add-wandboard-custom.patch"

一个将自定义机器添加到 U-Boot 的示例补丁可以在随书籍附带的源代码中找到。

添加自定义的 formfactor 文件

自定义平台还可以定义自己的formfactor文件,包含构建系统无法从其他来源获取的信息,例如定义是否有触摸屏或定义屏幕方向。这些文件在我们的meta-bsp-custom层中的recipes-bsp/formfactor/目录下定义。对于我们的新机器,我们可以定义一个meta-bsp-custom/recipes-bsp/formfactor/formfactor_0.0.bbappend文件,内容如下:

FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"

而与机器相关的meta-bsp-custom/recipes-bsp/formfactor/formfactor/wandboard-quadcustom/machconfig文件将如下所示:

HAVE_TOUCHSCREEN=1

引入系统开发工作流

在自定义软件时,有一些常用的系统开发工作流,我们将在这个配方中介绍它们。

如何操作...

我们将概述以下开发工作流:

  • 外部开发

  • 工作目录开发

  • 外部源代码开发

它们在不同的场景下使用。

它是如何工作的...

让我们分别理解这些开发工作流的用途。

外部开发

在这个工作流中,我们不使用 Yocto 构建系统来构建我们的包,只使用 Yocto 工具链和包的自有构建系统。

生成的源代码可以通过以下方式集成到 Yocto 中:

  • 使用一个从发布的 tarball 获取的配方。

  • 使用一个直接从源代码控制仓库获取的配方。

外部开发通常是 U-Boot 和 Linux 内核开发的首选方法,因为它们可以轻松交叉编译。Yocto 中的第三方包也是以这种方式开发的。

然而,第三方包可能很难交叉编译,而 Yocto 构建系统正是让这变得容易。所以,如果我们不是包的主要开发者,只是想引入一些修复或修改,我们可以使用 Yocto 来帮助我们。接下来将解释的两个工作流都使用了 Yocto 构建系统。

工作目录开发

在这个工作流中,我们使用 build 目录中的工作目录,tmp/work。正如我们所知,当 Yocto 构建一个包时,它使用工作目录来提取、修补、配置、构建和打包源代码。我们可以直接在这个目录中修改源代码,并使用 Yocto 系统来构建它。

这种方法通常用于偶尔调试第三方包时。

工作流如下:

  1. 删除包的 build 目录以从头开始:

    $ bitbake -c cleanall <target>
    
    
  2. 告诉 BitBake 获取、解压并修补包,但在此停止:

    $ bitbake -c patch <target>
    
    
  3. 进入包的源代码目录并修改源代码。通常,我们会创建一个临时的本地 Git 目录来帮助我们开发,并轻松提取补丁。

    $ bitbake -c devshell <target>
    
    
  4. 在不丢失我们修改的情况下构建它:

    $ bitbake -C compile <target>
    
    

    注意大写的C。这指示 BitBake 执行编译任务以及后续的所有任务。这与运行以下命令是一样的:

    $ bitbake -c compile <target>
    $ bitbake <target>
    
    
  5. 通过将包复制到运行中的系统并使用目标的包管理系统安装来测试它。当你从 NFS 根文件系统运行系统时,只需将它复制到那里并运行以下命令(假设使用默认的 RPM 包格式):

    $ rpm -i <package>
    
    

    可选地,你也可以像在 第一章 构建系统 中的 设置包源 配方中看到的那样使用包源,在这种情况下,你将重新构建索引,命令如下:

    $ bitbake package-index
    
    

    然后使用目标上的智能包管理工具安装包,如之前所示。

  6. 提取补丁并将其添加到配方的 bbappend 文件中。

外部源代码开发

在这种工作流程中,我们将使用 Yocto 构建系统来构建包含源码的外部目录。这个外部目录通常是受版本控制的,以帮助我们进行开发。

这是在源码已经与 Yocto 构建系统集成后进行广泛包开发的常用方法。

工作流程如下:

  1. 我们在这个外部版本控制的目录上进行开发,并在本地提交我们的更改。

  2. 我们配置 Yocto 构建系统,以使用主机系统中的一个目录来获取源码,并可选择在其中构建。这保证了我们的任何操作都不会使 Yocto 构建系统中的更改丢失。稍后我们将看到一些例子。

  3. 使用 Yocto 构建它:

    $ bitbake <target>
    
    
  4. 通过将包复制到运行中的系统并使用目标的软件包管理系统进行安装来进行测试。

  5. 提取补丁并添加到配方的bbappend文件中。

添加自定义内核和引导程序

在 U-Boot 和 Linux 内核中的开发通常是在 Yocto 之外进行的,因为它们很容易使用工具链进行构建,比如 Yocto 提供的工具链。

然后将开发工作集成到 Yocto 中的一种方法:

  • 使用添加到内核和 U-Boot bbappend文件的补丁。这种方法将构建与我们正在使用的基础参考设计板相同的源,并对其应用我们的更改。

  • 使用不同的 Git 存储库,从 Linux 内核和 U-Boot Git 存储库分叉,并使用bbappend文件将配方指向它。这样,我们可以直接提交更改到存储库,而 Yocto 构建系统将会构建它们。

通常只有在硬件更改非常重大并且 Linux 内核和引导加载程序的工作将非常广泛时,才需要分叉 Git 存储库。建议的做法是从补丁开始,只有当它们难以管理时才使用分叉存储库。

准备就绪

在开始对 Linux 内核和 U-Boot 进行修改的工作时,第一个问题是如何找出哪些可用的配方正在为您的构建使用。

找到 Linux 内核源码

要找到 Linux 内核源码,我们可以使用几种方法。因为我们知道正在为wandboard-quad机器进行构建,所以要做的第一件事是找到机器配置文件:

$ cd /opt/yocto/fsl-community-bsp/sources
$ find -name wandboard-quad.conf
./meta-fsl-arm-extra/conf/machine/wandboard-quad.conf

上述机器配置文件反过来包括一个wandboard.inc文件:

include conf/machine/include/imx-base.inc
include conf/machine/include/tune-cortexa9.inc

PREFERRED_PROVIDER_virtual/kernel ?= "linux-wandboard"
PREFERRED_VERSION_linux-wandboard ?= "3.10.17"

在这里,我们发现一个 Linux 内核配方被指定为虚拟/kernel 的首选提供者。像这样的虚拟包在一个功能或元素由多个配方提供时使用。这允许我们选择最终使用的所有配方中的哪一个。关于选择特定软件版本和提供者配方的详细解释将在第三章的软件层中进一步说明。

我们可以检查之前core-image-minimal构建的实际输出:

$ find tmp/work -name "*linux-wandboard*"
tmp/work/wandboard_quad-poky-linux-gnueabi/linux-wandboard

由于 linux-wandboard 目录存在于我们的 work 文件夹中,我们可以确定该配方已经被使用。

我们可以通过以下命令检查可用的 Linux 配方:

$ find -name "*linux*.bb"

我们有很多选项,但我们可以利用一些已获得的知识来过滤它们。让我们排除 pokymeta-openembedded 目录,因为我们知道 BSP 支持已经包含在 Freescale 社区 BSP 层中:

$ find -path ./poky -prune -o -path ./meta-openembedded -prune -o -name "*linux*.bb"

最后,我们还可以使用 Poky 中包含的 bitbake-layers 脚本:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake-layers show-recipes 'linux*'

并非所有这些内核都完全支持 Wandboard 机器,但它们都支持 Freescale ARM 机器,因此它们对进行比较很有用。

查找 U-Boot 源代码

如果我们继续拉取包含链,我们会看到 imx-base.inc,它本身包括 fsl-default-providers.inc,在其中我们可以找到:

PREFERRED_PROVIDER_u-boot ??= "u-boot-fslc"
PREFERRED_PROVIDER_virtual/bootloader ??= "u-boot-fslc"

所以 u-boot-fslc 就是我们正在寻找的 U-Boot 配方。

使用 Git 仓库分支进行开发

我们将展示如何追加一个配方,使用一个分叉的仓库进行开发。我们将以 Linux 内核为例,但这一概念同样适用于 U-Boot 或任何其他软件包,尽管具体细节会有所不同。

我们将分叉或创建参考设计中使用的仓库,并用它来指定配方的 SRC_URI

如何操作...

在这个例子中,我已经将仓库分叉到 github.com/yoctocookbook/linux,所以我的 recipes-kernel/linux/linux-wandboard_3.10.17.bbappend 文件将包含以下更改:

# Copyright Packt Publishing 2015
WANDBOARD_GITHUB_MIRROR = "git://github.com/yoctocookbook/linux.git"
SRCBRANCH = "wandboard_imx_3.10.17_1.0.2_ga-dev"
SRCREV = "${AUTOREV}"

提示

注意 URL 需要以 git:// 开头。这是为了让 BitBake 能够识别它为 Git 源。现在我们可以清理并构建 Linux 内核,源代码将从分叉的仓库中获取。

它是如何工作的...

让我们来看一下 linux-wandboard_3.10.17.bb 配方:

include linux-wandboard.inc
require recipes-kernel/linux/linux-dtb.inc

DEPENDS += "lzop-native bc-native"

# Wandboard branch - based on 3.10.17_1.0.2_ga from Freescale git
SRCBRANCH = "wandboard_imx_3.10.17_1.0.2_ga"
SRCREV = "be8d6872b5eb4c94c15dac36b028ce7f60472409"
LOCALVERSION = "-1.0.2-wandboard"

COMPATIBLE_MACHINE = "(wandboard)"

第一个有趣的地方是同时包含了 linux-wandboard.inclinux-dtb.inc。我们稍后会查看第一个,而另一个是一个类,它允许我们编译 Linux 内核设备树。我们将在本章稍后的 管理设备树 配方中讨论设备树。

然后它声明了两个软件包依赖关系,lzop-nativebc-nativenative 部分告诉我们这些是在主机系统中使用的,因此它们在 Linux 内核构建过程中被使用。lzop 工具用于创建 cpio 压缩文件,这些文件在 initramfs 系统中使用,该系统从基于内存的根文件系统启动,bc 被引入是为了避免在生成某些内核文件时产生 Perl 核心依赖。

然后它设置了分支和修订版本,最后将 COMPATIBLE_MACHINE 设置为 wandboard。我们将在第三章的 添加新软件包 配方中讲解机器兼容性,软件层

现在让我们来看一下 linux-wandboard.inc 包含文件:

SUMMARY = "Linux kernel for Wandboard"
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://COPYING;md5=d7810fab7487fb0aad327b76f1be7cd7"

require recipes-kernel/linux/linux-imx.inc

# Put a local version until we have a true SRCREV to point to
SCMVERSION ?= "y"

SRCBRANCH ??= "master"
LOCALVERSION ?= "-${SRCBRANCH}"

# Allow override of WANDBOARD_GITHUB_MIRROR to make use of
# local repository easier
WANDBOARD_GITHUB_MIRROR ?= "git://github.com/wandboard- org/linux.git"

# SRC_URI for wandboard kernel
SRC_URI = "${WANDBOARD_GITHUB_MIRROR};branch=${SRCBRANCH} \
 file://defconfig \
" 

这实际上就是我们要找的文件。最初,它指定了内核源代码的许可证,并指向它,设置了默认分支和本地版本的内核字符串,并设置了 SCR_URI 变量,这是源代码获取的地方。

它提供了WANDBOARD_GITHUB_MIRROR变量,我们可以在 bbappend 文件中修改它。

所以,逻辑上应该是创建一个 GitHub 账户并从提供的 wandboard-org Linux 仓库进行分叉。

一旦分支创建完毕,我们需要修改 WANDBOARD_GITHUB_MIRROR 变量。正如我们之前看到的,配方配置了特定的修订版和分支。我们希望在这里进行开发,因此我们需要将其更改为我们创建的新开发分支。我们可以将其命名为 wandboard_imx_3.10.17_1.0.2_ga-dev,并将修订版设置为自动获取该分支中的最新版本。

构建 U-Boot 引导加载程序

在这个配方中,我们将通过之前描述的几种开发工作流,使用 U-Boot 引导加载程序作为示例。

如何操作...

我们将看到以下开发工作流如何应用于 U-Boot:

  • 外部开发

  • 外部源代码开发

  • 工作目录开发

它是如何工作的...

现在我们准备好详细解释之前提到的三种工作流。

外部开发

我们将使用 Yocto 工具链,在 Yocto 构建系统外部构建 U-Boot 源代码。

  1. 下载并安装 Yocto 项目的交叉编译工具链,方法是访问以下链接:

    downloads.yoctoproject.org/releases/yocto/yocto-1.7.1/toolchain/

    提示

    选择 32 位或 64 位版本,并执行安装脚本,接受默认安装位置。建议不要更改默认位置,以避免位置迁移问题。

  2. 找到上游 Git 仓库:

    $ bitbake -e u-boot-fslc | grep ^SRC_URI=
    SRC_URI="git://github.com/Freescale/u-boot- imx.git;branch=patches-2014.10"
    
    
  3. 从上游仓库克隆 U-Boot 的源代码:

    $ cd /opt/yocto/
    $ git clone git://github.com/Freescale/u-boot-imx.git
    $ cd u-boot-imx
    
    

    默认分支应该是 patches-2014.10,但如果不是,你可以使用以下命令进行更改:

    $ git checkout -b patches-2014.10 origin/patches-2014.10
    
    
  4. 使用工具链附带的脚本设置环境:

    $ source /opt/poky/1.7.1/environment-setup-armv7a-vfp-neon- poky-linux-gnueabi
    
    
  5. wandboard-quad 配置 U-Boot:

    $ make wandboard_quad_config
    
    
  6. 如果你尝试构建 U-Boot,它会失败。这是因为默认的 Yocto 环境设置并不满足 U-Boot 的需求。快速查看 U-Boot 配方可以发现,它在构建之前清除了一些标志,因此我们需要做如下操作:

    $ unset LDFLAGS CFLAGS CPPFLAGS
    
    
  7. 现在我们准备构建。U-Boot 配方还将 CC 传递给 make 工具,作为 EXTRA_OEMAKE 标志,因为 U-Boot 不会从环境中读取它,因此我们还需要运行:

    $ make CC="${CC}"
    
    

    注意

    你可以选择传递 -jN 参数来进行多线程编译。这里,N 是 CPU 核心数。

它是如何工作的...

U-Boot 的 Makefile 使用以下命令查找 libgcc

PLATFORM_LIBGCC := -L $(shell dirname `$(CC) $(CFLAGS) -print-libgcc- file-name`
) -lgcc

如果我们没有定义 CC,该表达式不会正确展开为工具链中 libgcc 库的位置,因为没有将 sysroot 选项传递给编译器。

注意

更新版本的 U-Boot 已经修复了这个问题,但我们决定将指令保留如下:

$ make CC="${CC}"

这对于旧版本的 U-Boot 同样有效。

另一个避免问题的方法是定义USE_PRIVATE_LIBGCC U-Boot 配置变量,但这将使用内部的libgcc库到 U-Boot,这可能不是我们想要的。

然后,我们需要将镜像复制到目标设备以测试我们的更改,这很快就会看到。

外部源开发

我们将从一个本地目录中使用 Yocto 构建系统,通过克隆参考设计中使用的源代码的本地副本,并配置我们的项目以使用它作为外部源。然后,我们将从中开发,提取补丁,并将其添加到我们的 BSP 层中的bbappend文件中。

我们将使用之前克隆的 U-Boot 源码。

要配置我们的conf/local.conf文件以从克隆源码工作,按以下方式修改它:

INHERIT += "externalsrc"
EXTERNALSRC_pn-u-boot-fslc = "/opt/yocto/u-boot-imx"
EXTERNALSRC_BUILD_pn-u-boot-fslc = "/opt/yocto/u-boot-imx"

EXTERNALSRC变量定义了源代码位置(S),而EXTERNALSRC_BUILD变量定义了构建位置(B)。这段代码也将在外部源位置上构建,因为u-boot-fslc配方目前不支持源代码和构建目录的分离。

提示

记得在尝试下一个方法时删除前述的配置,该方法在接下来的这个配方中解释了工作目录开发方法。

现在我们可以在新的 shell 上进行构建:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake u-boot-fslc

在从外部源构建时,SRCPV的扩展会失败并显示错误。在启用外部源编译时,需要暂时修改配方以使用静态版本控制。对于 U-Boot,我们会在meta-fsl-arm/recipes-bsp/u-boot/u-boot-fslc_2014.10.bb文件中进行以下更改:

- PV = "v2014.10+git${SRCPV}"
+ PV = "v2014.10"

这使用差异格式,带有减号前缀的行将被删除,带有加号前缀的行将被添加。

可以在附带书籍的源代码中找到一个允许我们执行外部源开发的 U-Boot 示例补丁。

开发工作现在可以提交到本地 Git 存储库,并可以使用git format-patch生成补丁。例如,我们可以为 Wandboard 更改板信息并创建0001-wandboard-Change-board-info.patch文件:

diff --git a/board/wandboard/wandboard.c b/board/wandboard/wandboard.c
index 3c8b7a5d2d0a..a466d4c74b8f 100644
--- a/board/wandboard/wandboard.c
+++ b/board/wandboard/wandboard.c
@@ -404,7 +404,7 @@ int board_init(void)

 int checkboard(void)
 {
-       puts("Board: Wandboard\n");
+      puts("Board: Wandboard custom\n");

        return 0;
 }

要将此补丁添加到 Yocto 的 U-Boot 配方中,我们创建一个meta-bsp-custom/recipes-bsp/u-boot/u-boot-fslc_2014.10.bbappend文件,内容如下:

# Copyright Packt Publishing 2015
FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}-${PV}:"
SRC_URI_append = " file://0001-wandboard-Change-board-info.patch"

补丁需放置在meta-bsp-custom/recipes-bsp/u-boot/u-boot-fslc-v2014.10/下,正如FILESEXTRAPATHS变量中指定的。

被添加到SRC_URI变量的以patchdiff前缀结尾的文件将按照它们被发现的顺序应用。您也可以通过在SRC_URI中指定apply=yes属性来强制将文件视为patch

工作目录开发

在进行小修改时的典型工作流程将会是:

  1. 从头开始编译 U-Boot 包:

    $ bitbake -c cleanall virtual/bootloader
    
    

    这将清除 build 文件夹、共享状态缓存和下载的包源。

  2. 启动开发 shell:

    $ bitbake -c devshell virtual/bootloader
    
    

    这将获取、解压和打补丁 U-Boot 源码,并启动一个新的 shell,环境已准备好进行 U-Boot 编译。新 shell 会切换到包含本地 Git 仓库的 U-Boot build 目录。

  3. 在本地 Git 仓库中执行修改。

  4. 保持 devshell 打开,并使用另一个终端编译源代码,而不擦除我们的修改:

    $ bitbake -C compile virtual/bootloader
    
    

    注意大写的 C。这会触发编译任务,但也会触发随后的所有任务。

    新编译的 U-Boot 镜像可在 tmp/deploy/images/wandboard-quad 下找到。

  5. 测试您的更改。通常,这意味着我们需要将引导加载程序重新编程到 microSD 卡(如 Wandboard 所示)或内置的 emmc(如果可用),并在正确的偏移量处进行编程。我们可以从目标设备或主机计算机上进行此操作。

    从主机计算机,我们将使用 dd 命令将新的 U-Boot 镜像复制到偏移量为 0x400 的位置,这是 i.MX6 bootrom 期望找到它的地方。

    sudo dd if=u-boot.imx of=/dev/sdN bs=512 seek=2 && sync
    

    这会以 2 个块的偏移量进行写入,考虑到每块大小为 512 字节,偏移量为 0x400(1024)字节。

    提示

    在运行 dd 命令时要小心,因为它可能会损坏您的机器。您需要确保 sdN 设备对应的是您的 microSD 卡,而不是开发机器上的某个驱动器。

    从设备本身,我们可以使用 U-Boot 的 mmc 命令,如下所示:

    • 将 U-Boot 镜像加载到内存中:

      > setenv ipaddr <target_ip>
      > setenv serverip <host_ip>
      > tftp ${loadaddr} u-boot.imx
      
      

    TFTP 传输的十六进制文件大小保存在 filesize 环境变量中,我们稍后会使用它。

    • 选择要操作的 MMC 设备。您可以使用 mmc 部分来发现哪个是正确的设备。

      > mmc dev 0
      > mmc part
      Partition Map for MMC device 0  --   Partition Type: DOS
      Part    Start Sector    Num Sectors     UUID            Type
       1     8192            16384           0003b9dd-01     0c
       2     24576           131072          0003b9dd-02     83
      
      

    我们可以看到分区 1 从扇区 8192 开始,留下足够的空间来编程 U-Boot。

    • 使用 512 字节的块大小,我们按如下方式计算块数:

      > setexpr filesizeblks $filesize / 0x200
      > setexpr filesizeblks $filesizeblks + 1
      
      
    • 然后我们将写入两个块的偏移量,使用我们镜像占用的块数。

      > mmc write ${loadaddr} 0x2 ${filesizeblks}
      
      
  6. 回到 devshell 并将您的更改提交到本地 Git 仓库。

    $ git add --all .
    $ git commit -s -m "Well thought commit message"
    
    
  7. 生成一个补丁到 U-Boot 配方的补丁目录,如下所示:

    $ git format-patch -1 -o /opt/yocto/fsl-community- bsp/sources/meta-bsp-custom/recipes-bsp/u-boot/u-boot-fslc- v2014.10/
    
    
  8. 最后,将补丁添加到之前解释过的 U-Boot 配方中。

解释 Yocto 的 Linux 内核支持

Yocto 项目提供了一个内核框架,允许我们以不同的方式与 Linux 内核进行工作:

  • 从 Git 仓库获取源代码并应用补丁。这是 Freescale 社区 BSP 支持的内核所走的路径,正如我们之前所看到的。

  • linux-yocto 样式的内核从一组 Git 分支和叶子生成内核源代码。特定的功能在分支中开发,叶子则跟随一个完整的功能集。

在这个配方中,我们将展示如何使用 linux-yocto 样式的内核。

如何操作...

要使用linux-yocto风格的内核,内核配方会继承linux-yocto.inc文件。linux-yocto风格内核的 Git 仓库包含元数据,通常在配方中或内核 Git 树中,分支以meta前缀命名。

linux-yocto风格的内核配方全部命名为linux-yocto,并遵循上游内核开发,基于kernel.org仓库。一旦新的 Yocto 发布周期开始,就会选择一个最新的上游内核版本,并保持前一个 Yocto 发布周期的内核版本。较旧的版本则根据长期支持计划LTSI)发布进行更新。还有一个linux-yocto-dev包,它始终跟随最新的上游内核开发。

Yocto 内核与上游内核源代码分开维护,并为嵌入式系统开发人员添加功能和 BSP。

虽然 Freescale 社区的 BSP 不包含linux-yocto风格的内核,但其他一些 BSP 层包含此类内核。

用于定义构建的元数据变量包括:

  • KMACHINE:这个通常与MACHINE变量相同,但并非总是如此。它定义了内核的机器类型。

  • KBRANCH:这显式设置要构建的内核分支。它是可选的。

  • KBRANCH_DEFAULT:这是 KBRANCH 的默认值,初始值为master

  • KERNEL_FEATURES:这添加了额外的元数据,用于指定配置和补丁。它出现在定义的KMACHINEKBRANCH上方。它在系列配置控制SCC)文件中定义,稍后会详细说明。

  • LINUX_KERNEL_TYPE:默认为标准类型,但也可以是tinypreempt-rt。它在其自己的 SCC 描述文件中定义,或者在 SCC 文件中使用KTYPE变量明确指定。

工作原理...

Linux 内核中的元数据管理配置和源选择,以支持多个 BSP 和内核类型。管理这些元数据的工具构建在kern-tools包中。

元数据可以在配方中设置,用于小的修改,或者如果你没有访问某个内核仓库的权限,通常是在内核 Git 仓库中的meta分支中设置。要使用的meta分支默认位于与源代码相同仓库分支中的meta目录中,但也可以在内核配方中使用KMETA变量指定。如果它不在与内核源代码相同的分支中,它会被保存在孤立分支中;也就是说,这是一个具有自己历史的分支。要创建孤立分支,请使用以下命令:

$ git checkout --orphan meta
$ git rm -rf .
$ git commit --allow-empty -m "Meta branch"

你的配方必须包括SRCREV_meta,以指向要使用的meta分支的修订版本。

元数据在 SCC 文件中描述,其中可以包含一系列命令:

  • kconf:该命令将一个配置片段应用到内核配置中。

  • patch:该命令应用指定的补丁。

  • define:这引入变量定义。

  • include:这包含另一个 SCC 文件。

  • git merge:将指定的分支合并到当前分支。

  • branch:根据当前分支创建一个新的分支,通常是KTYPE或按指定内容。

SCC 文件大致分为以下逻辑分组:

  • configuration (cfg):包含一个或多个内核配置片段以及描述它们的 SCC 文件。例如:

    cfg/spidev.scc:
            define KFEATURE_DESCRIPTION "Enable SPI device support"
            kconf hardware spidev.cfg
    
    cfg/spidev.cfg:
            CONFIG_SPI_SPIDEV=y
    
  • 补丁:包含一个或多个内核补丁以及描述它们的 SCC 文件。例如:

    patches/fix.scc:
            patch fix.patch
    
    patches/fix.patch
    
  • features:包含混合配置和补丁以定义复杂的特性。它还可以包括其他描述文件。例如:

    features/feature.scc
            define KFEATURE_DESCRIPTION "Enable feature"
    
            patch 0001-feature.patch
    
            include cfg/feature_dependency.scc
            kconf non-hardware feature.cfg
    
  • 内核类型:包含定义高级内核策略的特性。默认情况下,SCC 文件中定义了三种内核类型:

    • standard:这是一个通用的内核定义策略。

    • tiny:这是一个最小的内核定义策略,独立于标准类型。

    • preempt-rt:继承自标准类型,用于定义应用了PREEMPT-RT补丁的实时内核。

    其他内核类型可以通过在 SCC 文件中使用KTYPE变量来定义。

  • Board Support PackagesBSP):内核类型和硬件特性的组合。BSP 类型应包括 KMACHINE(内核机器)和 KARCH(内核架构)。

另见

描述 Linux 的构建系统

Linux 内核是单内核,因而共享同一地址空间。尽管它能够在运行时加载模块,但内核必须在编译时包含模块所需的所有符号。一旦模块被加载,它将共享内核的地址空间。

内核构建系统或kbuild使用条件编译来决定内核的哪些部分需要编译。内核构建系统独立于 Yocto 构建系统。

在本指南中,我们将解释内核构建系统是如何工作的。

如何操作...

内核配置存储在内核根目录下的.config文本文件中。kbuild 系统读取该配置文件来构建内核。.config文件被称为内核配置文件。定义内核配置文件有多种方式:

  • 手动编辑.config文件,尽管不推荐这样做。

  • 使用内核提供的用户接口之一(键入 make help 命令以查看其他选项):

    • menuconfig:基于 ncurses 的菜单界面(make menuconfig

    • xconfig:基于 Qt 的界面(make xconfig

    • gconfig:基于 GTK 的界面(make gconfig

    注意

    请注意,为了构建和使用这些接口,您的 Linux 主机需要具有适当的依赖项。

  • 通过像 Yocto 这样的构建系统自动化执行。

每台机器还在内核树中定义了一个默认配置。对于 ARM 平台,这些存储在 arch/arm/configs 目录中。要配置 ARM 内核,即从默认配置生成一个 .config 文件,您运行:

$ make ARCH=arm <platform>_defconfig

例如,我们可以通过运行以下命令为 Freescale i.MX6 处理器构建一个默认配置:

$ make ARCH=arm imx_v6_v7_defconfig

工作原理如下…

Kbuild 使用 MakefileKconfig 文件来构建内核源代码。Kconfig 文件定义配置符号和属性,而 Makefile 文件将配置符号与源文件匹配。

运行以下命令可以查看 kbuild 系统的选项和目标:

$ make ARCH=arm help

还有更多…

在最新的内核中,默认配置包含了扩展为完整配置文件所需的所有信息。它是一个最小内核配置文件,其中删除了所有依赖项。要从当前的 .config 文件创建一个默认配置文件,您运行:

$ make ARCH=arm savedefconfig

这将在当前内核目录中创建一个 defconfig 文件。这个 make 目标可以看作是之前介绍的 <platform>_defconfig 目标的反义。前者从最小配置创建一个配置文件,后者将最小配置扩展为完整配置文件。

配置 Linux 内核

在本教程中,我们将解释如何使用 Yocto 构建系统配置 Linux 内核。

准备就绪

在配置内核之前,我们需要为我们的机器提供一个默认配置,这是 Yocto 项目用来配置内核的方式。在你的 BSP 层中定义新的机器时,需要提供一个 defconfig 文件。

Wandboard 的 defconfig 文件存储在 sources/meta-fsl-arm-extra/recipes-kernel/linux/linux-wandboard-3.10.17/defconfig 下。

这将是我们自定义硬件的基本 defconfig 文件,因此我们将其复制到我们的 BSP 层中:

$ cd /opt/yocto/fsl-community-bsp/sources
$ mkdir -p meta-bsp-custom/recipes-kernel/linux/linux-wandboard- 3.10.17/
$ cp meta-fsl-arm-extra/recipes-kernel/linux/linux-wandboard- 3.10.17/defconfig meta-bsp-custom/recipes-kernel/linux/linux- wandboard-3.10.17/

然后,我们将其添加到我们的内核中,使用 meta-bsp-custom/recipes-kernel/linux/linux-wandboard_3.10.17.bbappend 如下:

# Copyright Packt Publishing 2015
FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}-${PV}:"
SRC_URI_append = " file://defconfig"

内核配置更改可以直接在这个 defconfig 文件中进行。

如何做…

要从机器 defconfig 文件创建一个 .config 文件,请执行以下命令:

$ bitbake -c configure virtual/kernel

这也将运行 oldconfig 内核 make 目标,以验证与 Linux 源的配置。

然后,我们可以使用以下 BitBake 命令行配置 Linux 内核:

$ bitbake -c menuconfig virtual/kernel

menuconfig 用户界面以及其他内核配置用户界面具有搜索功能,允许您通过名称定位配置变量。看下面的屏幕截图:

如何做…

注意

在接下来的章节中,我们会提到特定的内核配置变量,比如 CONFIG_PRINTK,而不会详细说明配置变量的完整路径。可以使用不同 UI 的搜索接口来定位配置变量路径。

当你保存更改时,一个新的.config文件会在内核的build目录中创建,你可以使用以下命令找到它:

$ bitbake -e virtual/kernel | grep ^B=

你也可以使用图形界面修改配置,但不能从 BitBake 命令行修改。这是因为图形界面需要主机依赖项,而 Yocto 没有原生构建这些依赖项。

为了确保你的 Ubuntu 系统具备所需的依赖项,请执行以下命令:

$ sudo apt-get install git-core libncurses5 libncurses5-dev libelf- dev asciidoc binutils-dev qt3-dev-tools libqt3-mt-dev libncurses5 libncurses5-dev fakeroot build-essential crash kexec-tools makedumpfile libgtk2.0-dev libglib2.0-dev libglade2-dev

然后切换到之前找到的内核build目录,使用以下命令:

$ cd /opt/yocto/fsl-community-bsp/wandboard- quad/tmp/work/wandboard_quad-poky-linux-gnueabi/linux- wandboard/3.10.17-r0/git

接下来,运行以下命令:

$ make ARCH=arm xconfig

注意

如果遇到编译错误,请尝试从一个未使用setup-environment脚本配置过环境的新终端运行。

将会打开一个新窗口,显示下一个截图中的图形配置用户界面:

如何操作...

当你保存更改时,.config文件将被更新。

要使用更新的配置,你需要确保 BitBake 在构建时不会还原你的更改。有关更多细节,请参考本章中的构建 Linux 内核配方。

还有更多...

你可以通过以下步骤使内核更改永久生效:

  1. 从内核源目录的.config文件和一个干净的环境(未使用setup-environment脚本进行配置)创建默认配置,方法是运行:

    $ make ARCH=arm savedefconfig
    
    
  2. defconfig文件从你的内核build文件夹复制到内核配方的defconfig文件,如下所示:

    $ cp defconfig /opt/yocto/fsl-community-bsp/sources/meta-bsp- custom/recipes-kernel/linux/linux-wandboard-3.10.17
    
    

或者,你可以按如下方式从build目录使用 BitBake:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake -c savedefconfig virtual/kernel

这也会在 Linux 内核的源代码目录中创建一个defconfig文件,必须将其复制到你的配方中。

使用配置片段

linux-yocto风格的内核还可以应用在内核配置片段中定义的隔离内核配置更改;例如:

spidev.cfg:
  CONFIG_SPI_SPIDEV=y

内核配置片段以相同的方式附加到SRC_URI,并覆盖defconfig文件。

linux-yocto风格的内核(不过不包括 Wandboard 的那个)还提供了一套管理内核配置的工具:

  • 要从defconfig文件和提供的配置片段配置内核,请执行:

    $ bitbake -f -c kernel_configme linux-yocto
    
    
  • 要创建一个包含你更改的配置片段,请执行:

    $ bitbake -c diffconfig linux-yocto
    
    
  • 要验证内核配置,你可以运行:

    $ bitbake -f -c kernel_configcheck linux-yocto
    
    

构建 Linux 内核

在这个配方中,我们将通过使用 Linux 内核作为示例,展示前面描述的开发工作流程。

如何操作...

我们将看到以下开发工作流程是如何应用于 Linux 内核的:

  • 外部开发

  • 工作目录开发

  • 外部源开发

它是如何工作的...

让我们详细解释之前列出的三种方法。

外部开发

当在 Yocto 构建环境外编译时,我们仍然可以使用 Yocto 提供的工具链进行构建。过程如下:

  1. 我们将使用已经安装在主机上的 Yocto 项目交叉编译工具链。

  2. 本地克隆 wandboard-org linux-wandboard 仓库:

    $ cd /opt/yocto
    $ git clone https://github.com/wandboard-org/linux.git linux-wandboard
    $ cd linux-wandboard
    
    
  3. 转到 linux-wandboard_3.10.17.bb 配方中指定的分支:

    $ git checkout -b wandboard_imx_3.10.17_1.0.2_ga origin/wandboard_imx_3.10.17_1.0.2_ga
    
    
  4. 按照以下步骤编译内核源代码:

    • 按照以下步骤准备环境:

      $ source /opt/poky/1.7.1/environment-setup-armv7a-vfp-neon- poky-linux-gnueabi
      
      
    • 使用默认机器配置配置内核:

      $ cp /opt/yocto/fsl-community-bsp/sources/meta-bsp-custom/recipes-kernel/linux/linux-wandboard-3.10.17/defconfig arch/arm/configs/wandboard-quad_defconfig
      $ make wandboard-quad_defconfig
      
      
    • 使用以下命令编译内核镜像、模块和设备树文件:

      $ make
      
      

      你可以选择传递 -jN 参数给 make 以进行多线程构建。

      这将构建内核的 zImage、模块和设备树文件。

      注意

      较旧的 Yocto 环境设置脚本将 LD 变量设置为使用 gcc,但 Linux 内核使用的是 ld。如果你的编译失败,请在运行 make 之前尝试以下操作:

      $ unset LDFLAGS
      
      

      若要仅构建模块,你可以运行:

      $ make modules
      
      

      如果只需要构建设备树文件,你可以运行:

      $ make dtbs
      
      
    • 将内核镜像和设备树文件复制到 TFTP 根目录,以便使用网络启动进行测试:

      $ cp arch/arm/boot/zImage arch/arm/boot/dts/imx6q- wandboard.dtb /var/lib/tftpboot
      $ make LOADADDR=0x10800000 uImage
      UBOOT_ENTRYPOINT_mx6  = "0x10008000"
      

    一些其他嵌入式 Linux 目标可能需要编译 uImage,特别是当 U-Boot 引导加载程序没有编译支持 zImage 启动时:

    $ make LOADADDR=0x10800000 uImage
    

    注意

    mkimage 工具是 Yocto 工具链的一部分,当使用 FSL 社区 BSP 构建时。我们将在第四章的 准备和使用 SDK 配方中看到如何构建和安装 SDK,应用开发

    如果它不包含在你的工具链中,你可以使用以下命令在主机上安装该工具:

    $ sudo apt-get install u-boot-tools
    
    

    LOADADDR 是 U-Boot 的入口点;也就是 U-Boot 将内核放置到内存中的地址。它在 meta-fsl-arm imx-base.inc 文件中定义:

    UBOOT_ENTRYPOINT_mx6 = "0x10008000"
    
    

外部源代码开发

正如我们之前在 U-Boot 中所做的,我们将使用 Yocto 构建系统,指向包含 Linux 源代码仓库克隆的本地目录。我们将使用之前部分克隆的本地 Git 仓库。

我们在 conf/local.conf 文件中使用以下代码来配置外部开发:

INHERIT += "externalsrc"
EXTERNALSRC_pn-linux-wandboard = "/opt/yocto/linux-wandboard"
EXTERNALSRC_BUILD_pn-linux-wandboard = "/opt/yocto/linux- wandboard"

提示

记得在使用下文解释的工作目录开发方法时,删除此配置。

但是,正如之前一样,编译在 U-Boot 上失败。在这种情况下,linux-wandboard 配方由于不是 linux-yocto 样式的配方,因此没有准备好进行外部源代码编译,并且在配置任务时失败。

内核开发者通常更倾向于外部编译内核,正如我们之前看到的,因此这种情况可能不会很快得到修复。

工作目录开发

通常在我们有少量更改或不拥有源代码仓库时,我们会使用补丁并采用这种开发工作流。

当处理修改时,典型的工作流会是:

  1. 从头开始启动内核包的编译:

    $ cd /opt/yocto/fsl-community-bsp/
    $ source setup-environment wandboard-quad
    $ bitbake -c cleanall virtual/kernel
    
    

    这将清除 build 文件夹、共享状态缓存以及下载的包源。

  2. 按照以下步骤配置内核:

    $ bitbake -c configure virtual/kernel
    
    

    这将把机器 defconfig 文件转换为 .config 文件,并调用 oldconfig 来验证与内核源代码的配置。

    你可以选择性地通过以下方式添加自定义配置更改:

    $ bitbake -c menuconfig virtual/kernel
    
    
  3. 在内核上启动开发 shell:

    $ bitbake -c devshell virtual/kernel
    
    

    这将获取、解压并修补内核源代码,并启动一个新的 shell,环境已准备好进行内核编译。新的 shell 将切换到包含本地 Git 仓库的内核build目录。

  4. 执行我们的修改,包括内核配置更改。

  5. 保持 devshell 打开,然后返回到已加载 Yocto 环境的终端,以便在不删除我们修改的情况下编译源代码,如下所示:

    $ bitbake -C compile virtual/kernel
    
    

    请注意大写字母C。这将调用编译任务以及其后所有任务。

    新编译的内核镜像可在tmp/deploy/images/wandboard-quad下找到。

  6. 测试您的更改。通常,我们会在一个网络引导的系统上工作,因此我们会将内核镜像和设备树文件复制到 TFTP 服务器根目录,并使用以下命令引导目标:

    $ cd tmp/deploy/images/wandboard-quad/
    $ cp zImage-wandboard-quad.bin zImage-imx6q-wandboard.dtb /var/lib/tftpboot
    
    

    参考第一章中的配置网络引导开发环境食谱,了解详细信息,构建系统

    另外,U-Boot 引导加载程序可以使用以下语法,从内存中引导带有相应设备树的 Linux zImage 内核:

    > bootz <kernel_addr> - <dtb_addr>
    
    

    例如,我们可以从 TFTP 获取镜像并按如下方式引导 Wandboard 镜像:

    > tftp ${loadaddr} ${image}
    > tftp ${fdt_addr} ${fdt_file}
    > bootz ${loadaddr} - ${fdt_addr}
    
    

    如果我们使用的是 initramdisk,我们将其作为第二个参数传递。由于我们没有使用,我们改用破折号。

    从内存中引导 uImage Linux 内核镜像的命令应使用bootm,例如:

    > bootm <kernel_addr> - <dtb_addr>
    
    
  7. 返回到 devshell 并将您的更改提交到本地 Git 仓库:

    $ git add --all .
    $ git commit -s -m "Well thought commit message"
    
    
  8. 将补丁生成到内核食谱的补丁目录中:

    $ git format-patch -1 -o /opt/yocto/fsl-community- bsp/sources/meta-bsp-custom/recipes-kernel/linux/linux- wandboard-3.10.17
    
    
  9. 最后,将补丁添加到内核食谱中,如前所述。

构建外部内核模块

Linux 内核具有在运行时加载扩展内核功能的模块的能力。内核模块共享内核的地址空间,并且必须与其将要加载的内核进行链接。Linux 内核中的大多数设备驱动程序可以被编译成内核本身(内建)或作为需要放置在根文件系统下/lib/modules目录中的可加载内核模块。

开发和分发内核模块的推荐方法是与内核源代码一起进行。内核树中的模块使用内核的 kbuild 系统来构建自己,因此,只要它在内核配置中被选为模块,并且内核启用了模块支持,Yocto 就会构建它。

然而,并非总是可以在内核中开发模块。常见的例子是硬件制造商为多种内核版本提供 Linux 驱动程序,并且它们的内部开发过程与内核社区分离。内部开发工作通常首先作为外部树外模块发布,尽管一些或所有这些内部开发最终会进入主流内核。然而,上游提交是一个缓慢的过程,因此硬件公司通常会优先在内部开发。

值得记住的是,Linux 内核受 GPLv2 许可证保护,因此 Linux 内核模块应当以兼容的许可证发布。我们将在后续章节中详细介绍许可证问题。

准备中

要使用 Yocto 编译外部内核模块,首先需要了解如何将模块源代码与内核本身链接。外部内核模块也是使用它将要链接的 Linux 内核的 kbuild 系统构建的,因此我们首先需要一个Makefile

obj-m:= hello_world.o

SRC := $(shell pwd)
all:
        $(MAKE) -C $(KERNEL_SRC) M=$(SRC)

modules_install:
        $(MAKE) -C $(KERNEL_SRC) M=$(SRC) modules_install

clean:
        rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c
        rm -f Module.markers Module.symvers modules.order
        rm -rf .tmp_versions Modules.symvers

Makefile文件仅包装了用于在 Linux 系统上编译模块的make命令:

make -C $(KERNEL_SRC) M=$(SRC)

在这里,make指令会在内核源代码所在的位置进行构建,而M参数告诉 kbuild 它正在指定位置构建一个模块。

然后我们编写模块源代码本身(hello_world.c):

/ *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#include <linux/module.h>

static int hello_world_init(void)
{
        printk("Hello world\n");
        return 0;
}

static void hello_world_exit(void)
{
        printk("Bye world\n");
}
module_init(hello_world_init);
module_exit(hello_world_exit);

MODULE_LICENSE("GPL v2");

值得记住的是,我们需要使用已经构建的内核源代码进行编译。使用以下步骤进行编译:

  1. 我们使用 Yocto 工具链环境设置脚本来准备环境:

    $ source /opt/poky/1.7.1/environment-setup-armv7a-vfp-neon- poky-linux-gnueabi
    
    
  2. 接下来我们构建模块。从模块源目录执行以下命令:

    $ KERNEL_SRC=/opt/yocto/linux-wandboard make
    
    

如何操作...

一旦我们知道如何外部编译模块,我们就准备好为其准备一个 Linux 内核模块 Yocto 配方。

我们将模块源文件和Makefile放置在meta-bsp-custom层中的recipes-kernel/hello-world/files/目录下。然后,我们创建一个recipes-kernel/hello-world/hello-world.bb文件,内容如下:

# Copyright (C) 2015 Packt Publishing.

SUMMARY = "Simplest hello world kernel module."
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL- 2.0;md5=801f80980d171dd6425610833a22dbe6"

inherit module

SRC_URI = " \
    file://hello_world.c \
    file://Makefile \
"

S = "${WORKDIR}"

COMPATIBLE_MACHINE = "(wandboard)"

该配方在继承module类后定义了源目录和两个模块文件,该类负责所有事务。Makefile中的KERNEL_SRC参数由模块类设置为STAGING_KERNEL_DIR,即内核类放置用于外部模块编译所需的 Linux 内核头文件的位置。

我们使用以下命令进行构建:

$ bitbake hello-world

生成的模块名为hello_world.kobbclass会自动在包名中添加kernel-module前缀。

还有更多内容...

前面的指令将构建模块,但不会将其安装到根文件系统中。为此,我们需要将根文件系统添加为依赖项。通常在机器配置文件中使用MACHINE_ESSENTIAL(用于启动时所需的模块)或MACHINE_EXTRA(如果它们对启动不是必需的,但其他情况下需要)变量来完成此操作。

  • 那些对启动是必需的依赖项有:

    • MACHINE_ESSENTIAL_EXTRA_RDEPENDS:如果找不到它们,构建将失败

    • MACHINE_ESSENTIAL_EXTRA_RRECOMMENDS:如果找不到它们,构建不会失败

  • 那些对启动不是必需的依赖项有:

    • MACHINE_EXTRA_RDEPENDS:如果找不到它们,构建将失败

    • MACHINE_ESSENTIAL_EXTRA_RRECOMMENDS:如果找不到它们,构建不会失败

调试 Linux 内核和模块

我们将重点介绍内核开发人员常用的一些调试内核问题的方法。

如何操作...

最重要的是,调试 Linux 内核仍然是一个手动过程,最重要的开发者工具是能够打印调试信息。

内核使用printk函数,它在语法上与标准 C 库中的printf函数调用非常相似,唯一不同的是它增加了一个可选的日志级别。允许的格式在内核源代码的Documentation/printk-formats.txt中有详细记录。

printk功能需要在内核中通过CONFIG_PRINTK配置变量进行编译。你还可以通过CONFIG_PRINTK_TIME配置变量配置 Linux 内核为每条消息前面加上精确的时间戳,或者更好地,通过printk.time内核命令行参数或通过/sys/module/printk/parameters下的 sysfs 来实现。通常所有内核都包含printk支持,Wandboard 内核也包括,尽管它通常会在生产内核中为小型嵌入式系统去除。

printk函数可以在任何上下文中使用,包括中断、不可屏蔽中断NMI)或调度器。请注意,在中断上下文中使用它并不推荐。

一个在开发过程中有用的调试语句可能是:

printk(KERN_INFO "[%s:%d] %pf -> var1: %d var2: %d\n", __FUNCTION__, __LINE__, __builtin_return_address(0), var1, var2);

首先要注意的是,日志级别宏和打印格式之间没有逗号。然后,我们打印调试语句所在的函数和行号,以及父函数。最后,打印我们实际关注的变量。

它是如何工作的...

以下是printk中可用的日志级别:

类型 符号 描述
紧急 KERN_EMERG 系统不稳定且即将崩溃
警告 KERN_ALERT 需要立即采取行动
严重 KERN_CRIT 严重的软件或硬件故障
错误 KERN_ERR 错误条件
警告 KERN_WARNING 没有严重问题,但可能暗示有问题
通知 KERN_NOTICE 没有什么严重问题,但用户应注意
信息 KERN_INFO 系统信息
调试 KERN_DEBUG 调试信息

如果没有指定日志级别,将使用内核配置中配置的默认日志消息。默认情况下,这是KERN_WARNING

所有的printk语句都会写入内核日志缓冲区,该缓冲区可能会覆盖,调试语句除外,只有在定义了DEBUG符号时才会出现。稍后我们将看到如何启用内核调试消息。printk日志缓冲区的大小必须是 2 的幂,并且其大小应该在CONFIG_LOG_BUF_SHIFT内核配置变量中设置。你可以使用log_buf_len内核命令行参数进行修改。

我们使用dmesg命令打印内核日志缓冲区。此外,Yocto 用户空间将运行一个内核日志守护进程,该进程会将内核消息记录到/var/log/messages中的磁盘。

超过当前控制台日志级别的消息也会立即出现在控制台上。ignore_loglevel 内核命令行参数(也可以在 /sys/module/printk/parameters 下找到)可用于打印所有内核消息到控制台,无论日志级别如何。

你还可以通过 proc 文件系统在运行时更改日志级别。/proc/sys/kernel/printk 文件包含当前日志级别、默认日志级别、最小日志级别以及启动时的默认日志级别。要将当前日志级别更改为最大值,请执行:

$ echo 8 > /proc/sys/kernel/printk

你还可以通过 dmesg 工具如下设置控制台日志级别:

$ dmesg -n 8

要使更改持久化,你可以将日志级别命令行参数传递给内核,或者在某些 Yocto 根文件系统镜像中,你也可以使用 /etc/sysctl.conf 文件(这些镜像安装了 procps 包)。

还有更多内容...

Linux 驱动程序不会直接使用 printk 函数。它们按优先顺序使用特定子系统的消息(如 netdevv4l)或 dev_*pr_* 函数族。后者在下表中描述:

设备消息 一般消息 Printk 符号
dev_emerg pr_emerg KERN_EMERG
dev_alert pr_alert KERN_ALERT
dev_crit pr_crit KERN_CRIT
dev_err pr_err KERN_ERR
dev_warn pr_warn KERN_WARNING
dev_notice pr_notice KERN_NOTICE
dev_info pr_info KERN_INFO
dev_dbg pr_debug KERN_DEBUG

要启用驱动程序中的调试消息,你可以执行以下任意操作:

  • 在驱动源代码中,在任何其他头文件之前定义 DEBUG 宏,如下所示:

    #define DEBUG
    
  • 使用动态调试内核功能。然后,你可以通过 debugfs 按粒度启用/禁用所有 dev_dbgpr_debug 调试消息。

使用动态调试

要在 Linux 内核中使用动态调试功能,请按以下步骤操作:

  1. 确保你的内核已启用动态调试功能(CONFIG_DYNAMIC_DEBUG)。

  2. 如果尚未挂载调试文件系统,请先挂载:

    $ mount -t debugfs nodev /sys/kernel/debug
    
    
  3. 通过 dynamic_debug/control 文件夹配置调试。它接受由空格分隔的单词序列:

    • func <function name>

    • file <filename>

    • module <module name>

    • format <pattern>

    • line <line or line range>

    • + <flag>:添加指定的标志

    • - <flag>:移除指定的标志

    • = <flag>:设置指定的标志

    各种标志定义如下:

    • f:此标志在消息中包含函数名

    • l:此标志在消息中包含行号

    • m:此标志在消息中包含模块名

    • p:此标志启用调试消息

    • t:此标志在非中断上下文消息中包含线程 ID

  4. 默认情况下,所有调试消息都是禁用的。控制文件包含所有可用的调试点,默认情况下它们没有启用任何标志(标记为 =_)。

  5. 现在我们将按如下方式启用调试:

    • 启用文件中的所有调试语句:

      echo -n 'file <filename> +p' > /sys/kernel/debug/dynamic_debug/control
      
      
    • 可选地,您可以运行特定的调试语句:

      $ echo -n 'file <filename> line nnnn +p' > /sys/kernel/debug/dynamic_debug/control
      
      
  6. 要列出所有启用的调试语句,我们使用以下命令:

    $ awk '$3 != "=_"' /sys/kernel/debug/dynamic_debug/control
    
    

要使调试更持久,我们可以将 dyndbg="<query>"module.dyndbg="<query>" 传递给内核在命令行参数中。

请注意,查询字符串需要用引号括起来传递,以便正确解析。 您可以使用分号将多个查询连接在命令行参数中; 例如,dyndbg="file mxc_v4l2_capture.c +pfl; file ipu_bg_overlay_sdc.c +pfl"

限速调试消息

对于 dev_*pr_*printk 函数族,有限速和单次扩展:

  • printk_ratelimited()pr_*_ratelimited()dev_*_ratelimited() 在 5 * HZ 的间隔内最多打印 10 次

  • printk_once()pr_*_once()dev_*_once() 只会打印一次。

您还可以使用十六进制转储缓冲区的实用函数; 例如,print_hex_dump_bytes().

另见

  • 动态调试在 Linux 内核源码中有详细记录,位于 Documentation/dynamic-debug-howto.txt

调试 Linux 内核引导过程

我们已经看到了调试 Linux 内核的最常见技术。 然而,在一些特殊场景中,需要使用不同的方法。 在嵌入式 Linux 开发中最常见的场景之一是调试引导过程。 本文将解释用于调试内核引导过程的一些技术。

如何做...

引导时内核崩溃通常不会在控制台上提供任何输出。 尽管这看起来很令人生畏,但我们可以使用技术提取调试信息。 早期崩溃通常发生在串行控制台初始化之前,因此即使有日志消息,我们也看不到它们。 我们将首先展示如何启用不需要串行驱动程序的早期日志消息。

如果这还不够,我们还将展示访问内存中日志缓冲区的技术。

如何运作...

调试引导问题有两个明显的阶段,在串行控制台初始化之前和之后。 在串行控制台初始化之后,我们可以看到内核的串行输出,可以使用早期描述的技术进行调试。

然而,在串行初始化之前,ARM 内核中有基本的 UART 支持,允许您从早期引导开始使用串行。 此支持与 CONFIG_DEBUG_LL 配置变量一起编译进去。

这增加了对调试专用系列的汇编函数的支持,允许您向 UART 输出数据。 低级支持是平台特定的,对于 i.MX6,可以在 arch/arm/include/debug/imx.S 中找到。 该代码允许通过 CONFIG_DEBUG_IMX_UART_PORT 配置变量配置此低级 UART。

我们可以通过以下方式直接使用 printascii 函数:

extern void printascii(const char *);
printascii("Literal string\n");

然而,更推荐的做法是使用 early_print 函数,该函数利用前面解释的函数,并接受 printf 风格的格式化输入;例如:

early_print("%08x\t%s\n", p->nr, p->name);

从引导加载程序中转储内核的 printk 缓冲区

调试 Linux 内核启动时崩溃的另一种有用方法是分析崩溃后的内核日志。这只有在 RAM 内存在重启后保持持久性并且不被引导加载程序初始化的情况下才可能实现。

由于 U-Boot 保持内存不变,我们可以使用此方法查看内核登录内存,以寻找线索。

查看内核源代码,我们可以看到日志环形缓冲区是如何在kernel/printk/printk.c中设置的,并且注意到它存储在__log_buf中。

为了找到内核缓冲区的位置,我们将使用 Linux 构建过程创建的 System.map 文件,它通过以下命令映射符号与虚拟地址:

$grep __log_buf System.map
80f450c0 b __log_buf

要将虚拟地址转换为物理地址,我们查看 ARM 中 __virt_to_phys() 是如何定义的:

x - PAGE_OFFSET + PHYS_OFFSET

PAGE_OFFSET 变量在内核配置中定义如下:

config PAGE_OFFSET
        hex
        default 0x40000000 if VMSPLIT_1G
        default 0x80000000 if VMSPLIT_2G
        default 0xC0000000

一些 ARM 平台,如 i.MX6,会在运行时动态修补 __virt_to_phys() 转换,因此 PHYS_OFFSET 将取决于内核加载到内存中的位置。由于这一点可能有所不同,刚才看到的计算是平台特定的。

对于 Wandboard,0x80f450c0 的物理地址是 0x10f450c0。

然后我们可以通过强制重启使用一个神奇的 SysRq 键,这个键需要在内核配置中启用 CONFIG_MAGIC_SYSRQ,但是在 Wandboard 上默认启用:

$ echo b > /proc/sysrq-trigger

然后,我们可以通过以下方式从 U-Boot 转储该内存地址:

> md.l 0x10f450c0
10f450c0: 00000000 00000000 00210038 c6000000    ........8.!.....
10f450d0: 746f6f42 20676e69 756e694c 6e6f2078    Booting Linux on
10f450e0: 79687020 61636973 5043206c 78302055     physical CPU 0x
10f450f0: 00000030 00000000 00000000 00000000    0...............
10f45100: 009600a8 a6000000 756e694c 65762078    ........Linux ve
10f45110: 6f697372 2e33206e 312e3031 2e312d37    rsion 3.10.17-1.
10f45120: 2d322e30 646e6177 72616f62 62672b64    0.2-wandboard+gb
10f45130: 36643865 62323738 20626535 656c6128    e8d6872b5eb (ale
10f45140: 6f6c4078 696c2d67 2d78756e 612d7068    x@log-linux-hp-a
10f45150: 7a6e6f67 20296c61 63636728 72657620    gonzal) (gcc ver
10f45160: 6e6f6973 392e3420 2820312e 29434347    sion 4.9.1 (GCC)
10f45170: 23202920 4d532031 52502050 504d4545     ) #1 SMP PREEMP
10f45180: 75532054 6546206e 35312062 3a323120    T Sun Feb 15 12:
10f45190: 333a3733 45432037 30322054 00003531    37:37 CET 2015..
10f451a0: 00000000 00000000 00400050 82000000    ........P.@.....
10f451b0: 3a555043 4d524120 50203776 65636f72    CPU: ARMv7 Proce

还有更多内容...

另一种方法是将内核日志消息和内核 panic 或 oops 信息存储到持久存储中。Linux 内核的持久存储支持(CONFIG_PSTORE)允许你将数据记录到跨重启保持的持久内存中。

要将 panic 和 oops 消息记录到持久内存中,我们需要配置内核并启用 CONFIG_PSTORE_RAM 配置变量,而要记录内核消息,我们则需要配置内核启用 CONFIG_PSTORE_CONSOLE

然后我们需要配置持久存储的位置,选择一个未使用的内存位置,但保持最后 1 MB 内存为空。例如,我们可以传递以下内核命令行参数,预留一个从 0x30000000 开始的 128 KB 区域:

ramoops.mem_address=0x30000000 ramoops.mem_size=0x200000

然后,我们会通过将其添加到 /etc/fstab 中来挂载持久存储,以便在下次启动时也可以使用:

/etc/fstab:
pstore  /pstore  pstore  defaults  0  0

然后,我们按如下方式挂载它:

# mkdir /pstore
# mount /pstore

接下来,我们通过神奇的 SysRq 键强制重启:

# echo b > /proc/sysrq-trigger

在重启时,我们会在 /pstore 中看到一个文件:

-r--r--r--  1 root root 4084 Sep 16 16:24 console-ramoops

这将包含如下内容:

SysRq : Resetting
CPU3: stopping
CPU: 3 PID: 0 Comm: swapper/3 Not tainted 3.14.0-rc4-1.0.0-wandboard-37774-g1eae
[<80014a30>] (unwind_backtrace) from [<800116cc>] (show_stack+0x10/0x14)
[<800116cc>] (show_stack) from [<806091f4>] (dump_stack+0x7c/0xbc)
[<806091f4>] (dump_stack) from [<80013990>] (handle_IPI+0x144/0x158)
[<80013990>] (handle_IPI) from [<800085c4>] (gic_handle_irq+0x58/0x5c)
[<800085c4>] (gic_handle_irq) from [<80012200>] (__irq_svc+0x40/0x70)
Exception stack(0xee4c1f50 to 0xee4c1f98)

我们应该将其移出 /pstore 或完全删除,以避免占用内存。

使用内核函数跟踪系统

最新版本的 Linux 内核包含一组跟踪器,通过对内核进行插桩,允许你分析不同的领域,比如:

  • 中断延迟

  • 抢占延迟

  • 调度延迟

  • 进程上下文切换

  • 事件追踪

  • 系统调用

  • 最大堆栈

  • 块层

  • 函数

当追踪器未启用时,追踪器不会带来性能开销。

准备就绪...

追踪系统可以用于多种调试场景,但最常用的追踪器之一是函数追踪器。它通过 NOP 调用对每个内核函数进行插桩,NOP 调用在启用追踪点时被替换并用于追踪内核函数。

要在内核中启用函数追踪器,请使用CONFIG_FUNCTION_TRACERCONFIG_FUNCTION_GRAPH_TRACER配置变量。

内核追踪系统通过debug文件系统中的tracing文件进行控制,默认情况下 Yocto 的默认镜像会挂载该文件系统。如果没有,你可以通过以下命令挂载:

$ mount -t debugfs nodev /sys/kernel/debug

我们可以通过执行以下命令列出内核中可用的追踪器:

$ cat /sys/kernel/debug/tracing/available_tracers
function_graph function nop

如何操作...

你可以通过将追踪器的名称回显到current_tracer文件中来启用追踪器。默认情况下没有启用任何追踪器:

$ cat /sys/kernel/debug/tracing/current_tracer
nop

你可以通过执行以下命令来禁用所有追踪器:

$ echo -n nop > /sys/kernel/debug/tracing/current_tracer

我们使用echo -n来避免在通过echo命令向sysfs文件写入时产生换行符。

要启用函数追踪器,你需要执行:

$ echo -n function > /sys/kernel/debug/tracing/current_tracer

使用函数图形追踪器可以获得更美观的图形,方法如下:

$ echo -n function_graph  > /sys/kernel/debug/tracing/current_tracer

它是如何工作的...

你可以通过tracetrace_pipe文件查看捕获的可读格式追踪数据,后者在read时会阻塞并消耗数据。

函数追踪器提供如下输出:

$ cat  /sys/kernel/debug/tracing/trace_pipe
root@wandboard-quad:~# cat /sys/kernel/debug/tracing/trace_pipe
 sh-394   [003] ...1    46.205203: mutex_unlock <- tracing_set_tracer
 sh-394   [003] ...1    46.205215: __fsnotify_parent <- 
 vfs_write
 sh-394   [003] ...1    46.205218: fsnotify <-vfs_write
 sh-394   [003] ...1    46.205220: __srcu_read_lock <- fsnotify
 sh-394   [003] ...1    46.205223: preempt_count_add <- __srcu_read_lock
 sh-394   [003] ...2    46.205226: preempt_count_sub <- __srcu_read_lock
 sh-394   [003] ...1    46.205229: __srcu_read_unlock <- fsnotify
 sh-394   [003] ...1    46.205232: __sb_end_write <- vfs_write
 sh-394   [003] ...1    46.205235: preempt_count_add <- __percpu_counter_add
 sh-394   [003] ...2    46.205238: preempt_count_sub <- __percpu_counter_add
 sh-394   [003] d..1    46.205247: gic_handle_irq <- __irq_usr
 <idle>-0     [002] d..2    46.205247: ktime_get <- cpuidle_enter_state

函数追踪器输出的格式是:

task-PID [cpu-nr] irqs-off need-resched hard/softirq preempt-depth delay-timestamp function

图形化函数追踪器的输出如下:

$ cat /sys/kernel/debug/tracing/trace_pipe
 3)   ==========> |
 3)               |  gic_handle_irq() {
 2)   ==========> |
 2)               |  gic_handle_irq() {
 3)   0.637 us    |    irq_find_mapping();
 2)   0.712 us    |    irq_find_mapping();
 3)               |    handle_IRQ() {
 2)               |    handle_IRQ() {
 3)               |      irq_enter() {
 2)               |      irq_enter() {
 3)   0.652 us    |        rcu_irq_enter();
 2)   0.666 us    |        rcu_irq_enter();
 3)   0.591 us    |        preempt_count_add();
 2)   0.606 us    |        preempt_count_add();

图形化函数追踪器输出的格式是:

cpu-nr) timestamp | functions

还有更多内容...

内核追踪系统允许我们通过使用trace_printk函数调用在代码中插入追踪。它与printk的语法相同,可以在相同的场景中使用,如中断、NMI 或调度器上下文。

它的优点是,由于它将数据输出到内存中的追踪缓冲区,而不是输出到控制台,因此比printk具有更低的延迟,因此它对于调试printk影响系统行为的场景非常有用;例如,调试时序错误。

一旦配置了追踪器,追踪就会被启用,但是否将追踪数据写入环形缓冲区是可以控制的。要禁用写入缓冲区,请使用以下命令:

$ echo 0 > /sys/kernel/debug/tracing/tracing_on

要重新启用它,请使用以下命令:

$ echo 1 > /sys/kernel/debug/tracing/tracing_on

你还可以通过使用tracing_ontracing_off函数从内核空间启用和禁用追踪。

插入的追踪信息会出现在任何追踪器中,包括function追踪器,在这种情况下,它将作为注释出现。

过滤函数追踪

你可以通过使用动态跟踪器来获得更细粒度的跟踪功能,动态跟踪器可以通过配置变量CONFIG_DYNAMIC_FTRACE启用。默认情况下,跟踪功能启用时会自动启用此功能。它会增加两个文件,set_ftrace_filterset_ftrace_notrace。将函数添加到set_ftrace_filter中将仅跟踪这些函数,而将它们添加到set_ftrace_notrace中则不会跟踪它们,即使它们也添加到set_ftrace_filter中。

可供筛选的函数名集合可以通过执行以下命令获得:

$ cat /sys/kernel/debug/tracing/available_filter_functions

函数可以通过以下方式添加:

$ echo -n <function_name> >>  /sys/kernel/debug/tracing/set_ftrace_filter

注意,我们使用连接操作符(>>)将新函数追加到现有函数中。

函数也可以通过以下方式移除:

$ echo -n '!<function>' >>  /sys/kernel/debug/tracing/set_ftrace_filter

要移除所有函数,只需将空行写入文件:

$ echo >  /sys/kernel/debug/tracing/set_ftrace_filter

有一种特殊的语法可以为筛选提供额外的灵活性:<function>:<command>:[<parameter>]

让我们分别解释每个组件:

  • function:这指定了函数名,支持通配符。

  • command:具有以下属性:

    • mod:这只会在参数指定的模块中启用给定的函数名。

    • traceon/traceoff:当指定的函数被触发给定次数时,这将启用或禁用跟踪,如果没有提供参数,则始终启用或禁用。

    • dump:当触发指定函数时,转储跟踪缓冲区的内容。

下面是一些示例:

$ echo -n 'ipu_*:mod:ipu' > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo -n 'suspend_enter:dump' > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo -n 'suspend_enter:traceon' > /sys/kernel/debug/tracing/set_ftrace_filter

启用跟踪选项

跟踪器有一组可以单独启用的选项,这些选项位于/sys/kernel/debug/tracing/options目录中。一些最有用的选项包括:

  • print-parent:此选项也会显示调用者函数。

  • trace_printk:此选项禁用trace_printk的写入。

在 oops 上使用函数跟踪器

另一种在 oops 或 panic 时记录内核消息的替代方法是配置函数跟踪器,将其缓冲区内容转储到控制台,以便分析导致崩溃的事件。使用以下命令:

$ echo 1 > /proc/sys/kernel/ftrace_dump_on_oops

sysrq-z组合键也会将跟踪缓冲区的内容转储到控制台,调用内核代码中的ftrace_dump()也是如此。

获取给定函数的堆栈跟踪

跟踪代码可以为每个调用的函数创建回溯。然而,这是一个危险的特性,应该只在筛选后的函数选择中使用。请查看以下命令:

$ echo -n <function_name> > /sys/kernel/debug/tracing/set_ftrace_filter
$ echo -n function > /sys/kernel/debug/tracing/current_tracer
$ echo 1 > /sys/kernel/debug/tracing/options/func_stack_trace
$ cat /sys/kernel/debug/tracing/trace
$ echo 0 > /sys/kernel/debug/tracing/options/func_stack_trace
$ echo > /sys/kernel/debug/tracing/set_ftrace_filter

在启动时配置函数跟踪器

函数跟踪器可以在内核命令行参数中进行配置,并尽早在启动过程中启动。例如,配置图形函数跟踪器并筛选一些函数,我们可以通过以下参数从 U-Boot 引导加载程序传递给内核:

ftrace=function_graph ftrace_filter=mxc_hdmi*,fb_show*

另见

  • 更多细节可以在内核源代码的文档文件夹Documentation/trace/ftrace.txt中找到。

管理设备树

设备树是一种数据结构,传递给 Linux 内核,用于描述系统中的物理设备。

在本教程中,我们将解释如何使用设备树。

准备工作

CPU 无法发现的设备由 Linux 内核中的平台设备 API 处理。设备树取代了旧有的平台数据,其中硬件特性被硬编码在内核源代码中,允许实例化平台设备。在设备树使用之前,启动加载程序(例如 U-Boot)必须告诉内核正在启动的是哪种机器类型。此外,它还必须传递其他信息,如内存大小和位置、内核命令行等。

设备树不应与 Linux 内核配置混淆。设备树指定了哪些设备可用以及如何访问这些设备,而不是硬件是否被使用。

设备树最早由 PowerPC 架构使用,后来被 ARM 和其他架构(除了 x86)采用。它由 Open Firmware 规范定义,该规范在 Power.org 嵌入式电源架构平台需求标准 (ePAPR) 中定义了扁平化的设备树格式,该标准描述了引导程序与客户端之间的接口。

平台自定义更改通常会在设备树中进行,而无需修改内核源代码。

如何操作……

设备树在一个人类可读的设备树语法(.dts)文本文件中定义。每个开发板都有一个或多个 DTS 文件,分别对应不同的硬件配置。

这些 DTS 文件会被编译成 设备树二进制文件 (DTB) 二进制块,具有以下特点:

  • 它们是可重定位的,因此内部不会使用指针。

  • 它们允许动态插入和移除节点。

  • 它们体积小巧。

设备树二进制文件可以附加到内核二进制文件中(以保持向后兼容性),或者像通常做的那样,通过 U-Boot 等引导加载程序传递给内核。

为了编译它们,我们使用 设备树编译器 (DTC),它包含在内核源代码中的 scripts/dtc 目录,并与内核一起编译,或者我们也可以将其作为发行版的一部分安装。建议使用内核树中包含的 DTC 编译器。

设备树可以独立编译,也可以与 Linux 内核的 kbuild 系统一起编译,正如我们之前所看到的。然而,当独立编译时,现代设备树需要先由 C 预处理器进行预处理。

需要注意的是,DTC 目前只执行语法检查,而不执行绑定检查,因此无效的 DTS 文件可能会被编译,而生成的 DTB 文件可能导致内核无法启动。无效的 DTB 文件通常会让 Linux 内核在启动早期挂起,因此不会有串口输出。

启动加载程序也可能在将设备树传递给内核之前对其进行修改。

工作原理……

wandboard-quad变种的 DTS 文件位于arch/arm/boot/dts/imx6q-wandboard.dts,其内容如下:

#include "imx6q.dtsi"
#include "imx6qdl-wandboard.dtsi"

/ {
    model = "Wandboard i.MX6 Quad Board";
    compatible = "wand,imx6q-wandboard", "fsl,imx6q";

    memory {
        reg = <0x10000000 0x80000000>;
    };
};

我们看到的是设备树的根节点,它没有父节点。其余节点都会有父节点。节点的结构可以表示如下:

node@0{
  an-empty-property;
  a-string-property = "a string";
  a-string-list-property = "first string", "second string";
  a-cell-property = <1>;
  a-cell-property = <0x1 0x2>;
  a-byte-data-property = [0x1 0x2 0x3 0x4];
  a-phandle-property = <&node1>;
}

节点属性可以是:

  • 包含一个或多个字符串。

  • 包含一个或多个无符号 32 位数字,称为单元

  • 包含二进制字节流。

  • 引用另一个节点,称为phandle

设备树最初由 C 预处理器解析,并且可以包含其他 DTS 文件。这些include文件具有相同的语法,通常以dtsi后缀结尾。文件包含也可以通过设备树的/include/操作符来执行,尽管推荐使用#include,并且它们不应该混合使用。在这种情况下,imx6q.dtsiimx6qdl-wandboard.dtsi的内容会覆盖imx6q-wandboard.dts

设备树节点在内核源代码的Documentation/devicetree/bindings/目录中的绑定文档中有详细说明。新节点必须包含相应的绑定,并且这些绑定必须经过设备树维护者的审查和接受。从理论上讲,所有绑定都需要维护,尽管未来可能会放宽这一要求。

兼容性属性。

设备树节点中最重要的属性是compatible属性。在根节点中,它定义了设备树兼容的机器类型。我们刚才看到的 DTS 文件按优先顺序与wand,imx6q-wandboardfsl,imx6q机器类型兼容。

在非根节点上,它将定义设备树节点的驱动匹配,将设备与驱动程序绑定。例如,一个平台驱动程序,如果与一个定义了兼容fsl,imx6q-tempmon属性的节点绑定,则会包含如下摘录:

static const struct of_device_id of_imx_thermal_match[] = {
    { .compatible = "fsl,imx6q-tempmon", },
    { /* end */ }
};
MODULE_DEVICE_TABLE(of, of_imx_thermal_match);

static struct platform_driver imx_thermal = {
    .driver = {
        .name   = "imx_thermal",
        .owner  = THIS_MODULE,
        .of_match_table = of_imx_thermal_match,
    },
    .probe      = imx_thermal_probe,
    .remove   = imx_thermal_remove,
};
module_platform_driver(imx_thermal);

Wandboard 设备树文件。

通常,首先要包含的 DTSI 文件是skeleton.dtsi,这是启动所需的最小设备树,一旦添加了兼容性属性。

/ {
    #address-cells = <1>;
    #size-cells = <1>;
    chosen { };
    aliases { };
    memory { device_type = "memory"; reg = <0 0>; };
};

下面是其他常见的顶级节点:

  • chosen:该节点定义了启动时设置的固定参数,如 Linux 内核命令行或initramfs内存位置。它取代了传统上通过 ARM 标签(ATAGS)传递的信息。

  • memory:该节点用于定义 RAM 的位置和大小,通常由引导加载程序填充。

  • 别名:这定义了指向其他节点的快捷方式。

  • address-cellssize-cells:这两个属性用于内存可寻址性,稍后将讨论。

以下是imx6q-wandboard.dts文件的摘要表示,仅显示选定的总线和设备:

#include "skeleton.dtsi"

/ {
    model = "Wandboard i.MX6 Quad Board";
    compatible = "wand,imx6q-wandboard", "fsl,imx6q";

    memory {};

    aliases {};

    intc: interrupt-controller@00a01000 {};

    soc {
        compatible = "simple-bus";

        dma_apbh: dma-apbh@00110000 {};

        timer@00a00600 {};

        L2: l2-cache@00a02000 {};

        pcie: pcie@0x01000000 {};

        aips-bus@02000000 { /* AIPS1 */
            compatible = "fsl,aips-bus", "simple-bus";

            spba-bus@02000000 {
                compatible = "fsl,spba-bus", "simple-bus";
            };

            aipstz@0207c000 {};

            clks: ccm@020c4000 {};

            iomuxc: iomuxc@020e0000 {};
        };

        aips-bus@02100000 {
            compatible = "fsl,aips-bus", "simple-bus";
        };
    };
};

在这个 DTS 中,我们可以找到几个节点定义了片上系统SoC)总线和其他几个节点定义了板载设备。

定义总线和内存可寻址设备。

总线通常通过compatible属性或simple-bus属性(用于定义一个没有特定驱动程序绑定的内存映射总线)或两者定义。simple-bus属性是必需的,这样总线的子节点才能注册为平台设备。

例如,soc节点定义如下:

soc {
    compatible = "simple-bus";
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;

    aips-bus@02000000 { /* AIPS1 */
        compatible = "fsl,aips-bus", "simple-bus";
        reg = <0x02000000 0x100000>;
    }
}

soc节点上的属性用于指定子节点的内存寻址方式。

  • address-cells:此属性指示在reg属性中需要多少个基地址单元。

  • size-cells:此属性指示在reg属性中需要多少个大小单元。

  • ranges:这个属性描述了父子总线之间的地址转换。在这里,没有转换,父子地址是相同的。

在这种情况下,soc的任何子节点都需要通过reg属性定义其内存地址,该属性包含一个单元表示地址,一个单元表示大小。aips-bus节点通过以下属性实现:

reg = <0x02000000 0x100000>;

还有更多内容...

当 Linux 内核将设备树二进制数据加载到内存中时,它会展开成一个扁平化的设备树,通过偏移量访问。fdt_*内核函数用于访问扁平化的设备树。这个fdt随后被解析并转化为一个可以通过of_*系列函数(前缀来源于 Open Firmware)高效访问的树形内存结构。

在 Yocto 中修改和编译设备树

要在 Yocto 构建系统中修改设备树,我们执行以下一组命令:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake -c devshell virtual/kernel

我们接着编辑arch/arm/boot/dts/imx6q-wandboard.dts并使用以下命令编译更改:

$ make dtbs

如果我们想创建一个具有额外空间的设备树,比如 1024 字节(例如,添加节点如下一食谱所示),我们需要通过 DTC 标志来指定它,如下所示:

DTC_FLAGS="-p 1024" make dtbs

为了部署它,我们退出 devshell 并从项目的build目录中构建内核:

$ bitbake -c deploy -f virtual/kernel

另请参见

调试设备树问题

本食谱将展示一些调试设备树常见问题的技巧。

如何执行...

如前所述,设备树文件的语法问题通常会导致内核在启动过程中崩溃。其他类型的问题更为隐蔽,通常在驱动程序使用设备树提供的信息时出现。对于这两类问题,能够查看不仅是设备树语法文件,还能查看设备树二进制数据(U-Boot 和 Linux 内核读取的内容)是非常有帮助的。使用 U-Boot 提供的工具动态修改设备树也可能会很有用。

它是如何工作的...

从 U-Boot 查看设备树

U-Boot 引导加载程序提供了fdt命令来与设备树二进制数据交互。在 Wandboard 的默认环境中,有两个与设备树相关的变量:

  • fdt_file:此变量包含使用的设备树文件的名称

  • fdt_addr:此变量包含加载设备树的内存位置

为了从 TFTP 服务器位置获取 Wandboard 的设备树并将其放入内存,我们使用以下命令:

> tftp ${fdt_addr} ${fdt_file}

一旦设备树二进制文件加载到内存中,我们告诉 U-Boot 它的位置:

> fdt addr ${fdt_addr}

然后,我们可以通过设备树的完整路径从根节点开始检查节点。要检查选定的级别,我们使用 list 命令,要打印完整的子树,我们使用 print 命令:

> fdt list /cpus
cpus {
 #address-cells = <0x00000001>;
 #size-cells = <0x00000000>;
 cpu@0 {
 };
};
> fdt print /cpus
cpus {
 #address-cells = <0x00000001>;
 #size-cells = <0x00000000>;
 cpu@0 {
 compatible = "arm,cortex-a9";
 device_type = "cpu";
 reg = <0x00000000>;
 next-level-cache = <0x0000001d>;
 [omitted]
 };
};

U-Boot 也可以将新的节点附加到设备树中,前提是设备树中有额外的空间:

> fdt mknode / new-node
> fdt list /new-node 
new-node {
};

它还可以创建或删除属性:

> fdt set /new-node testprop testvalue
> fdt print /new-node 
new-node {
 testprop = "testvalue";
};
> fdt rm /new-node testprop 
> fdt print /new-node 
new-node {
};

例如,通过选定的节点修改内核命令行是很有用的。

从 Linux 内核查看设备树

一旦 Linux 内核启动后,将设备树暴露给用户空间以便进行探索是很有用的。可以通过配置 Linux 内核的 CONFIG_PROC_DEVICETREE 配置变量来实现。Wandboard Linux 内核已经预配置为在 /proc/device-tree 中暴露设备树,如下所示:

# ls /proc/device-tree/cpus/
#address-cells  cpu@0           cpu@2           name
#size-cells     cpu@1           cpu@3

第三章:软件层

在本章中,我们将介绍以下配方:

  • 探索镜像的内容

  • 添加新的软件层

  • 选择特定的包版本和提供者

  • 添加支持的包

  • 添加新包

  • 添加数据、脚本或配置文件

  • 管理用户和组

  • 使用 sysvinit 初始化系统

  • 使用 systemd 初始化系统

  • 安装包安装脚本

  • 减小 Linux 内核镜像大小

  • 减小根文件系统镜像大小

  • 发布软件

  • 分析系统合规性

  • 使用开源和专有代码

介绍

随着硬件特定更改的到来,下一步是定制目标根文件系统;即在 Linux 内核下运行的软件,也称为 Linux 用户空间。

通常的做法是从一个可用的核心镜像开始,然后根据嵌入式项目的需求进行优化和定制。通常选择的起始镜像是 core-image-minimalcore-image-sato,但任何一个都可以。

本章将展示如何添加软件层以包含这些更改,并解释一些常见的定制,如大小优化。还将展示如何向根文件系统添加新包,包括许可考虑事项。

探索镜像的内容

我们已经看到如何使用构建历史功能来获取包含在镜像中的包和文件列表。在这个配方中,我们将解释根文件系统是如何构建的,以便能够跟踪其组件。

准备工作

当包构建完成后,它们会根据架构在项目的工作目录(tmp/work)中进行分类。例如,在 wandboard-quad 构建中,我们会看到以下目录:

  • all-poky-linux:用于架构无关的包

  • cortexa9hf-vfp-neon-poky-linux-gnueabi:用于 cortexa9 硬浮动点包

  • wandboard_quad-poky-linux-gnueabi:用于机器特定包;在此情况下,是 wandboard-quad

  • x86_64-linux:用于构成主机 sysroot 的包

BitBake 将在其自己的目录中构建其依赖列表中的所有包。

如何做到...

要查找给定包的 build 目录,可以执行以下命令:

$ bitbake -e <package> | grep ^WORKDIR=

build 目录中,我们可以找到一些子目录(假设未使用 rm_work),这些子目录是构建系统在打包任务中使用的。这些子目录包括以下内容:

  • deploy-rpms:这是存储最终包的目录。我们可以在这里找到可以本地复制到目标并安装的单个包。这些包被复制到 tmp/deploy 目录,在 Yocto 构建根文件系统镜像时也会使用这些包。

  • image:这是默认的目标目录,do_install任务会将组件安装到该目录。它可以通过配方中的D配置变量进行修改。

  • package:这个包含了实际的包内容。

  • package-split:这里是根据最终的包名将内容分类到各个子目录中。配方可以根据PACKAGES变量指定将包内容拆分为多个最终包。除了默认包名称外,默认的包有:

    • dbg:安装用于调试的组件

    • dev:安装用于开发的组件,如头文件和库

    • staticdev:安装用于静态编译的库和头文件

    • doc:这是文档所在的目录

    • locale:安装本地化组件

每个包中要安装的组件由FILES变量选择。例如,要添加到默认包,可以执行以下命令:

FILES_${PN} += "${bindir}/file.bin"

如果要添加到开发包,可以使用以下命令:

FILES_${PN}-dev += "${libdir}/lib.so"

它是如何工作的……

一旦 Yocto 构建系统完成其依赖包列表中的所有单独包的构建,它会运行do_rootfs任务,填充sysroot并构建根文件系统,然后创建最终的包镜像。你可以通过执行以下命令找到根文件系统的位置:

$ bitbake -e core-image-minimal | grep ^IMAGE_ROOTFS=

请注意,IMAGE_ROOTFS变量不可配置,且不应更改。

该目录中的内容稍后将根据IMAGE_FSTYPES配置变量中配置的镜像类型准备成一个镜像。如果某些内容已安装在此目录中,它将被安装到最终镜像中。

添加新的软件层

根文件系统的定制涉及向基础镜像中添加或修改内容。与这些内容相关的元数据将进入一个或多个软件层,具体取决于定制的需求量。

一个典型的嵌入式项目通常只有一个包含所有非硬件特定定制的软件层。但也可以为图形框架或系统范围的元素添加额外的层。

准备工作

在开始工作之前,最好检查是否有其他人提供了类似的层。此外,如果你试图集成一个开源项目,检查是否已有现成的层。可以在layers.openembedded.org/找到可用层的索引。

如何操作……

然后,我们可以通过yocto-layer命令创建一个新的meta-custom层,正如我们在第二章的创建自定义 BSP 层配方中所学到的。从sources目录中执行以下命令:

$ yocto-layer create custom

别忘了将该层添加到项目的conf/bblayers.conf文件以及模板的conf目录中,以便在所有新项目中使用。

默认的conf/layer.conf配置文件如下所示:

# We have a conf and classes directory, add to BBPATH
BBPATH .= ":${LAYERDIR}"

# We have recipes-* directories, add to BBFILES
BBFILES += "${LAYERDIR}/recipes-*/*/*.bb \
        ${LAYERDIR}/recipes-*/*/*.bbappend"

BBFILE_COLLECTIONS += "custom"
BBFILE_PATTERN_custom = "^${LAYERDIR}/"
BBFILE_PRIORITY_custom = "6"

我们在第二章的创建自定义 BSP 层配方中讨论了所有相关变量,BSP 层

它是如何工作的...

在向新软件层添加内容时,我们需要记住我们的层需要与 Yocto 项目中的其他层良好兼容。因此,在定制配方时,我们将始终使用附加文件,并且仅在完全确定无法通过附加文件添加所需定制时,才会重载现有配方。

为了帮助我们管理多个层中的内容,我们可以使用以下bitbake-layers命令行工具:

  • $ bitbake-layers show-layers:此命令将显示配置的层,按 BitBake 的视角展示。这有助于检测conf/bblayer.conf文件中的错误。

  • $ bitbake-layers show-recipes:此命令将显示所有可用的配方及提供它们的层。它可以用来验证 BitBake 是否识别了你新创建的配方。如果没有显示,请验证文件系统层级是否与层的BBFILES变量在conf/layer.conf中的定义一致。

  • $ bitbake-layers show-overlayed:此命令将显示所有被另一个具有相同名称但优先级更高层中的配方所覆盖的配方。它有助于检测配方冲突。

  • $ bitbake-layers show-appends:此命令将列出所有可用的附加文件及其应用到的配方文件。它可以用来验证 BitBake 是否识别了你的附加文件。此外,和配方一样,如果它们没有显示,你需要检查文件系统层级和你层中的BBFILES变量。

  • $ bitbake-layers flatten <output_dir>:此命令将创建一个目录,其中包含所有配置层的内容,且没有覆盖的配方,并应用所有附加文件。这是 BitBake 将看到的元数据。这个扁平化目录有助于发现与层的元数据冲突。

还有更多...

我们有时会添加特定于某个开发板或机器的定制内容。这些内容不总是与硬件相关,因此它们可能出现在 BSP 或软件层中。

在这样做时,我们会尽量保持我们的定制尽可能具体。一个典型的例子是针对特定机器或机器系列进行定制。如果你需要为wandboard-quad机器添加补丁,可以使用以下代码行:

SRC_URI_append_wandboard-quad = " file://mypatch.patch"

如果补丁适用于所有基于 i.MX6 的开发板,你可以使用以下命令:

SRC_URI_append_mx6 = " file://mypatch.patch"

要使用机器系列重载,机器配置文件需要包含一个SOC_FAMILY变量,例如在meta-fsl-arm-extra中用于wandboard-quad的配置。参考以下代码行:

conf/machine/wandboard-quad.conf:SOC_FAMILY = "mx6:mx6q:wandboard"

为了让它出现在MACHINEOVERRIDES变量中,需要包含soc-family.inc文件,正如在meta-fsl-arm中的配置。以下是conf/machine/include/imx-base.inc文件中的相关代码摘录:

include conf/machine/include/soc-family.inc
MACHINEOVERRIDES =. "${@['', '${SOC_FAMILY}:']['${SOC_FAMILY}' != '']}"

BitBake 将搜索预定义的路径,查找包工作目录内的文件,该目录由FILESPATH变量定义,作为以冒号分隔的列表。具体来说:

${PN}-${PV}/${DISTRO}
${PN}/${DISTRO}
files/${DISTRO}

${PN}-${PV}/${MACHINE}
${PN}/${MACHINE}
files/${MACHINE}

${PN}-${PV}/${SOC_FAMILY}
${PN}/${SOC_FAMILY}
files/${SOC_FAMILY}

${PN}-${PV}/${TARGET_ARCH}
${PN}/${TARGET_ARCH}
files/${TARGET_ARCH}

${PN}-${PV}/
${PN}/
files/

wandboard-quad的具体情况下,这将转换为以下内容:

${PN}-${PV}/poky
${PN}/poky
files/poky
${PN}-${PV}/wandboard-quad
${PN}/wandboard-quad
files/wandboard-quad
${PN}-${PV}/wandboard
${PN}/wandboard
files/wandboard
${PN}-${PV}/mx6q
${PN}/mx6q
files/mx6q
${PN}-${PV}/mx6
${PN}/mx6
files/mx6
${PN}-${PV}/armv7a
${PN}/armv7a
files/armv7a
${PN}-${PV}/arm
${PN}/arm
files/arm
${PN}-${PV}/
${PN}/
files/

在这里,PN是包名,PV是包版本。

最好将补丁放在这些层中最具体的地方,比如wandboard-quad,然后是wandboardmx6qmx6armv7aarm,最后是通用的PN-PVPNfiles

注意,搜索路径指的是 BitBake 配方的位置,因此在添加内容时,追加文件始终需要添加路径。如果需要,我们的追加文件可以通过以下方式在FILESEXTRAPATHS变量中追加或前置,来添加额外的文件夹到该搜索路径:

FILESEXTRAPATHS_prepend := "${THISDIR}/folder:"

注意

注意立即操作符(:=),它会立即扩展THISDIR,以及前置操作符,它将你添加的路径放在其他路径之前,这样你的补丁和文件可以首先在搜索中被找到。

此外,我们也看到在配置文件中使用+==+风格的操作符,但应避免在配方文件中使用,应该优先使用追加和前置操作符,如之前的示例代码所解释,以避免顺序问题。

选择特定的包版本和提供者

我们的层可以为同一包的不同版本提供配方。例如,meta-fsl-arm层包含多个不同类型的 Linux 源:

如前所述,所有配方默认提供包名(例如,linux-imxlinux-fslc),但所有 Linux 配方还必须提供virtual/kernel虚拟包。构建系统将根据构建的要求,如目标机器,解析virtual/kernel到最合适的 Linux 配方名称。

而在这些配方中,linux-imx例如,包含了 2.6.35.3 和 3.10.17 的配方版本。

在这个配方中,我们将展示如何告诉 Yocto 构建系统构建哪个特定的包和版本。

如何操作...

要指定我们要构建的精确包,构建系统允许我们指定要使用的提供者和版本。

我们如何选择使用哪个提供者?

我们可以通过使用PREFERRED_PROVIDER变量来告诉 BitBake 使用哪个配方。为了在我们的 Wandboard 机器上为virtual/kernel虚拟包设置首选提供者,我们需要在其机器配置文件中添加以下内容:

PREFERRED_PROVIDER_virtual/kernel = "linux-imx"

我们如何选择使用哪个版本?

在特定的提供者中,我们还可以告诉 BitBake 使用哪个版本,方法是使用 PREFERRED_VERSION 变量。例如,要为所有基于 i.MX6 的机器设置特定的 linux-imx 版本,我们将在 conf/local.conf 文件中添加以下内容:

PREFERRED_VERSION_linux-imx_mx6 = "3.10.17"

% 通配符被接受来匹配任何字符,正如我们在这里看到的:

PREFERRED_VERSION_linux-imx_mx6 = "3.10%"

然而,通常情况下,这种类型的配置会在机器配置文件中完成,在这种情况下,我们就不会使用 _mx6 的附加操作符了。

我们如何选择不使用哪个版本?

我们可以使用将 DEFAULT_PREFERENCE 变量设置为 -1 的方式来指定除非通过 PREFERRED_VERSION 变量显式设置,否则不使用某个版本。这在软件包的开发版本中很常见。

DEFAULT_PREFERENCE = "-1"

添加支持的包

通常,我们希望向已经在包含的 Yocto 层中有现成配方的镜像中添加新软件包。

当目标镜像与提供的核心镜像差异较大时,建议定义一个新的镜像,而不是定制现有的镜像。

这个配方将展示如何通过向现有镜像中添加支持的包来定制它,但如果需要,也可以创建一个全新的镜像配方。

准备工作

为了发现我们需要的软件包是否包含在我们配置的层中,以及支持哪些特定版本,我们可以使用来自构建目录的 bitbake-layers,正如我们之前所见:

$ bitbake-layers show-recipes | grep -A 1 htop
htop:
 meta-oe              1.0.3

或者,我们也可以像下面这样使用 BitBake:

$ bitbake -s | grep htop
htop                                                :1.0.3-r0

或者,我们可以在 sources 目录中使用 find Linux 命令:

$ find . -type f -name "htop*.bb"
./meta-openembedded/meta-oe/recipes-support/htop/htop_1.0.3.bb

一旦我们知道要包含哪些软件包在最终的镜像中,接下来我们来看看如何将它们添加到镜像中。

如何做到这一点...

在开发过程中,我们将使用我们项目的 conf/local.conf 文件来添加自定义内容。要向所有镜像中添加软件包,我们可以使用以下代码行:

IMAGE_INSTALL_append = " htop"

注意

请注意,第一个引号后有一个空格,用于将新包与现有包分开,因为附加操作符不会自动添加空格。

我们还可以通过以下方式将添加限制到特定镜像:

IMAGE_INSTALL_append_pn-core-image-minimal = " htop"

另一种简单的定制方式是通过利用 特性。特性是软件包的逻辑分组。例如,我们可以创建一个新的特性叫做 debug-utils,它将添加一整套调试工具。我们可以在配置文件或类中按如下方式定义我们的特性:

FEATURE_PACKAGES_debug-utils = "strace perf"

然后我们可以通过在 conf/local.conf 文件中添加 EXTRA_IMAGE_FEATURES 变量来将此特性添加到我们的镜像中,如下所示:

EXTRA_IMAGE_FEATURES += "debug-utils"

如果你将其添加到镜像配方中,你将使用 IMAGE_FEATURES 变量。

通常,特性作为 packagegroup 配方被添加,而不是单独列出作为软件包。让我们展示如何在 recipes-core/packagegroups/packagegroup-debug-utils.bb 文件中定义一个 packagegroup 配方:

SUMMARY = "Debug applications packagegroup"
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://${COREBASE}/LICENSE;md5=3f40d7994397109285ec7b81fdeb3b58"

inherit packagegroup

RDEPENDS_${PN} = "\
    strace \
    perf \
"

然后你将它添加到 FEATURE_PACKAGES 变量中,如下所示:

FEATURE_PACKAGES_debug-utils = "packagegroup-debug-utils"

我们可以使用packagegroups来创建更复杂的示例。有关详细信息,请参考Yocto 项目开发手册www.yoctoproject.org/docs/1.7.1/dev-manual/dev-manual.html

它是如何工作的...

自定义镜像的最佳方法是使用现有镜像作为模板来创建我们自己的镜像。我们可以使用core-image-minimal.bb,它包含以下代码:

SUMMARY = "A small image just capable of allowing a device to boot."

IMAGE_INSTALL = "packagegroup-core-boot ${ROOTFS_PKGMANAGE_BOOTSTRAP} ${CORE_IMAGE_EXTRA_INSTALL}"

IMAGE_LINGUAS = " "

LICENSE = "MIT"

inherit core-image

IMAGE_ROOTFS_SIZE ?= "8192"

然后扩展为我们自己的版本,以允许通过添加以下meta-custom/recipes-core/images/custom-image.bb镜像文件来自定义IMAGE_FEATURES

require recipes-core/images/core-image-minimal.bb
IMAGE_FEATURES += "ssh-server-dropbear package-management"

当然,我们也可以使用现有的镜像作为模板,从头定义一个新的镜像。

还有更多...

自定义镜像的最终方法是通过添加 shell 函数,这些函数在镜像创建后执行。你可以通过在镜像配方或conf/local.conf文件中添加以下内容来实现:

ROOTFS_POSTPROCESS_COMMAND += "function1;...;functionN"

你可以在命令中使用IMAGE_ROOTFS变量来指定根文件系统的路径。

类别将使用IMAGE_POSTPROCESS_COMMAND变量,而不是ROOTFS_POSTPROCESS_COMMAND

一个用例示例可以在image.bbclass中的debug-tweaks功能中找到,当镜像被调整以允许无密码 root 登录时。这个方法也通常用于自定义目标镜像的 root 密码。

配置包

正如我们在第二章的配置 Linux 内核一节中看到的那样,BSP 层,一些包(比如 Linux 内核)提供了一个配置菜单,可以通过menuconfig BitBake 命令进行配置。

另一个值得提到的、带有配置界面的包是 BusyBox。我们将展示如何配置 BusyBox,例如添加pgrep,这是一个通过名称查找进程 ID 的工具。具体步骤如下:

  1. 配置 BusyBox:

    $ bitbake -c menuconfig busybox
    
    
  2. 进程工具中选择pgrep

  3. 编译 BusyBox:

    $ bitbake -C compile busybox
    
    
  4. 将 RPM 包复制到目标:

    $ bitbake -e busybox | grep ^WORKDIR=
    $ scp ${WORKDIR}/deploy-rpms/cortexa9hf_vfp_neon/busybox- 1.22.1-r32.cortexa9hf_vfp_neon.rpm root@<target_ip>:/tmp
    
    
  5. 在目标上安装 RPM 包:

    # rpm --force -U /tmp/busybox-1.22.1- r32.cortexa9hf_vfp_neon.rpm
    
    

    请注意,由于配置更改后包的版本没有增加,我们强制更新。

添加新包

我们已经看到如何自定义我们的镜像,以便我们可以向其中添加支持的包。当我们找不到现有的配方,或者需要集成我们自己开发的新软件时,我们将需要创建一个新的 Yocto 配方。

准备就绪

在开始编写新配方之前,我们需要问自己一些问题:

  • 源代码存储在哪里?

  • 它是源代码控制的,还是以 tarball 形式发布的?

  • 源代码许可证是什么?

  • 它使用什么构建系统?

  • 它需要配置吗?

  • 我们能否按原样交叉编译,还是需要打补丁?

  • 需要部署到根文件系统的文件有哪些,它们应该放在哪里?

  • 是否需要进行任何系统更改,比如新增用户或init脚本?

  • 是否有任何依赖项需要提前安装到sysroot中?

一旦我们知道了这些问题的答案,就可以开始编写我们的食谱了。

如何操作...

最好从如下的空白模板开始,而不是从一个类似的食谱开始并修改它,因为这样做的结果会更加干净,并且仅包含严格需要的指令。

添加最小食谱的一个良好起点是:

SUMMARY = "The package description for the package management system"

LICENSE = "The package's licenses typically from meta/files/common-licenses/"
LIC_FILES_CHKSUM = "License checksum used to track open license changes"
DEPENDS = "Package list of build time dependencies"

SRC_URI = "Local or remote file or repository to fetch"
SRC_URI[md5sum] = "md5 checksums for all remote fetched files (not for repositories)"
SRC_URI[sha256sum] = "sha256 checksum for all remote fetched files (not for repositories)"

S = "Location of the source in the working directory, by default ${WORKDIR}/${PN}-${PV}."

inherit <class needed for some functionality>

# Task overrides, like do_configure, do_compile and do_install, or nothing.

# Package splitting (if needed).

# Machine selection variables (if needed).

它是如何工作的...

我们将在接下来的部分中更详细地解释每个食谱部分。

包许可证

每个食谱都需要包含LICENSE变量。LICENSE变量允许你指定多个、替代的以及按包类型的许可证,如下例所示:

  • 对于 MIT 或 GPLv2 替代许可证,我们将使用:

    LICENSE = "GPL-2.0 | MIT"
    
  • 对于 ISC 和 MIT 许可证,我们将使用:

    LICENSE = "ISC & MIT"
    
  • 对于拆分包,所有包都是 GPLv2,除了文档部分,它是由知识共享协议保护的,我们将使用:

    LICENSE_${PN} = "GPLv2"
    LICENSE_${PN}-dev = "GPLv2"
    LICENSE_${PN}-dbg = "GPLv2"
    LICENSE_${PN}-doc = "CC-BY-2.0"
    

开源包通常会在源代码中包含许可证,如READMECOPYINGLICENSE文件,甚至在源代码头文件中。

对于开源许可证,我们还需要为所有许可证指定LIC_FILES_CHECKSUM,以便构建系统在许可证发生变化时通知我们。要添加它,我们定位包含许可证的文件或文件部分,并提供其相对于源目录的路径以及其 MD5 校验和。例如:

LIC_FILES_CHKSUM = "file://${COREBASE}/meta/files/common- licenses/GPL-2.0;md5=801f80980d171dd6425610833a22dbe6"
LIC_FILES_CHKSUM = "file://COPYING;md5=f7bdc0c63080175d1667091b864cb12c"
LIC_FILES_CHKSUM = "file://usr/include/head.h;endline=7;md5=861ebad4adc7236f8d1905338 abd7eb2"
LIC_FILES_CHKSUM = "file://src/file.c;beginline=5;endline=13;md5=6c7486b21a8524b1879f a159578da31e"

专有代码的许可证应设置为CLOSED,不需要为其提供LIC_FILES_CHECKSUM

获取包内容

SRC_URI变量列出了要获取的文件。构建系统将根据文件前缀使用不同的获取器。这些获取器可以是:

  • 与元数据一起包含的本地文件(file://)。如果本地文件是补丁,SRC_URI变量可以通过补丁特定的参数扩展,如下所示:

    • striplevel:默认的补丁去除级别是 1,但可以通过此参数进行修改

    • patchdir:此参数指定应用补丁的目录位置,默认值为源目录

    • apply:此参数控制是否应用补丁,默认情况下会应用补丁

  • 存储在远程服务器上的文件(通常为http(s)://ftp://ssh://)。

  • 存储在远程仓库中的文件(通常为git://svn://hg://bzr://)。这些文件也需要一个SRCREV变量来指定修订版本。

存储在远程服务器上的文件(不是本地文件或远程仓库)需要指定两个校验和。如果有多个文件,可以通过name参数来区分;例如:

SRCREV = "04024dea2674861fcf13582a77b58130c67fccd8"
SRC_URI = "git://repo.com/git/ \
           file://fix.patch;name=patch \
           http://example.org/archive.data;name=archive"
SRC_URI[archive.md5sum] = "aaf32bde135cf3815aa3221726bad71e"
SRC_URI[archive.sha256sum] = "65be91591546ef6fdfec93a71979b2b108eee25edbc20c53190caafc9a92d4e7"

源目录文件夹S指定了源文件的位置。仓库将在此处检出,或者 tarball 会解压到此位置。如果 tarball 解压到标准的${PN}-${PV}位置,则可以省略,因为这是默认值。对于仓库,必须始终指定;例如:

S = "${WORKDIR}/git"

指定任务覆盖

所有食谱都继承了base.bbclass类,该类定义了以下任务:

  • do_fetch:该方法通过SRC_URI变量选择获取器,获取源代码。

  • do_unpack:该方法将代码解包到由S变量指定的位置。

  • do_configure:该方法根据需要配置源代码。默认情况下不执行任何操作。

  • do_compile:该方法默认编译源代码并运行 GNU make 目标。

  • do_install:该方法将构建结果从build目录B复制到目标目录D。默认情况下不执行任何操作。

  • do_package:该方法将交付物拆分成多个包。默认情况下不执行任何操作。

通常,仅重写配置、编译和安装任务,并且大多数情况下是通过继承autotools类隐式完成的。

对于不使用构建系统的自定义配方,您需要在相应的do_configuredo_compiledo_install重写方法中提供所需的配置(如果有)、编译和安装指令。以下是这种类型配方的示例,位于meta-custom/recipes-example/helloworld/helloworld_1.0.bb

DESCRIPTION = "Simple helloworld application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://helloworld.c"

S = "${WORKDIR}"

do_compile() {
             ${CC} helloworld.c -o helloworld
}

do_install() {
             install -d ${D}${bindir}
             install -m 0755 helloworld ${D}${bindir}
}

meta-custom/recipes-example/helloworld/helloworld-1.0/helloworld.c源文件如下:

#include <stdio.h>

int main(void)
{
    return printf("Hello World");
}

我们将在下一章中看到使用最常见构建系统的示例配方。

配置包

Yocto 构建系统提供了PACKAGECONFIG变量,用于通过定义多个特性来帮助配置包。您的配方通过以下方式定义单个特性:

PACKAGECONFIG ??= "feature"
PACKAGECONFIG[feature] = "--with-feature,--without-feature,build- deps-feature,rt-deps-feature"

PACKAGECONFIG变量包含一个以空格分隔的特性名称列表,并且可以在bbappend文件中进行扩展或重写;请查看以下示例:

PACKAGECONFIG_append = " feature1 feature2"

要从发行版或本地配置文件扩展或重写它,您需要使用以下语法:

PACKAGECONFIG_pn-<package_name> = "feature1 feature2"
PACKAGECONFIG_append_pn-<package_name> = " feature1 feature2"

接下来,我们使用四个有序参数来描述每个特性:

  • 启用特性时的额外配置参数(对于EXTRA_OECONF

  • 启用特性时的额外配置参数(对于EXTRA_OECONF

  • 启用特性时的额外构建依赖(对于DEPENDS

  • 启用特性时的额外运行时依赖(对于RDEPENDS

这四个参数是可选的,但必须保持其顺序,并且需要保留分隔的逗号。

例如,wpa-supplicant配方定义了两个特性,gnutlsopenssl,但默认仅启用gnutls,如下所示:

PACKAGECONFIG ??= "gnutls"
PACKAGECONFIG[gnutls] = ",,gnutls"
PACKAGECONFIG[openssl] = ",,openssl"

拆分成多个包

将配方内容分成不同的包以满足不同的需求是常见做法。典型的例子是将文档包含在doc包中,将头文件和/或库包含在dev包中。我们可以通过使用FILES变量来实现这一点,示例如下:

FILES_${PN} += "List of files to include in the main package"
FILES_${PN}-dbg += "Optional list of files to include in the debug package"
FILES_${PN}-dev += "Optional list of files to include in the development package"
FILES_${PN}-doc += "Optional list of files to include in the documentation package"

设置特定于机器的变量

每个配方都有一个PACKAGE_ARCH变量,用于将配方分类到一个软件包源中,正如我们在探索镜像内容的配方中看到的那样。大多数时候,它们由 Yocto 构建系统自动分类。例如,如果配方是内核、内核模块配方、镜像配方,或者是交叉编译或构建本地应用程序,Yocto 构建系统将相应地设置软件包架构。

BitBake 还会查看SRC_URI的机器覆盖并调整软件包架构,如果你的配方使用allarch类,它将把软件包架构设置为all

因此,在处理仅适用于特定机器或机器系列的配方时,或者涉及特定机器或机器系列的更改时,我们需要检查软件包是否被分类到适当的软件包源中。如果没有,则需要通过在配方中明确指定以下代码行来指定软件包架构:

PACKAGE_ARCH = "${MACHINE_ARCH}"

此外,当一个配方仅用于特定的机器类型时,我们使用COMPATIBLE_MACHINE变量来指定。例如,为了让它仅兼容mxs、mx5 和 mx6 SoC 系列,我们会使用以下内容:

COMPATIBLE_MACHINE = "(mxs|mx5|mx6)"

添加数据、脚本或配置文件

所有配方都继承了基类,并执行默认的任务集合。继承基类后,配方知道如何执行诸如获取和编译之类的任务。

由于大多数配方旨在安装某种可执行文件,基类知道如何构建它。但有时我们所需要的仅仅是将数据、脚本或配置文件安装到文件系统中。

如果数据或配置与应用程序相关,最合逻辑的做法是将其与应用程序的配方本身打包在一起,如果我们认为分开安装更好,甚至可以将其拆分成单独的包。

但有时,数据或配置与应用程序无关,可能适用于整个系统,或者我们只想为其提供一个单独的配方。根据需要,我们甚至可能希望安装一些不需要编译的 Perl 或 Python 脚本。

如何操作…

在这些情况下,我们的配方应该继承allarch类,该类被那些不产生特定架构输出的配方继承。

这种类型的配方示例meta-custom/recipes-example/example-data/example-data_1.0.bb可以在这里查看:

DESCRIPTION = "Example of data or configuration recipe"
SECTION = "examples"

LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://${COREBASE}/meta/files/common-licenses/GPL-2.0;md5=801f80980d171dd6425610833a22dbe6"

SRCREV = "${AUTOREV}"
SRC_URI = "git://github.com/yoctocookbook/examples.git \
           file://example.data"

S = "${WORKDIR}/git"

inherit allarch

do_compile() {
}

do_install() {
        install -d ${D}${sysconfdir}
        install -d ${D}${sbindir}
        install -m 0755 ${WORKDIR}/example.data ${D}/${sysconfdir}/
        install -m 0755 ${S}/python-scripts/* ${D}/${sbindir}
}

它假设虚构的examples.git仓库包含一个我们想要包括在根文件系统中的python-scripts文件夹。

一个可用的配方示例可以在与本书附带的源代码中找到。

管理用户和组

在很多情况下,我们还需要向文件系统中添加或修改用户和组。这个配方解释了如何做到这一点。

准备工作

用户信息存储在/etc/passwd文件中,这是一个文本文件,用作系统用户信息的数据库。passwd文件是人类可读的。

每一行对应系统中的一个用户,格式如下:

<username>:<password>:<uid>:<gid>:<comment>:<home directory>:<login shell>

让我们看看这种格式的每个参数:

  • username:一个唯一的字符串,用于标识用户在登录时的身份

  • uid:用户 ID,Linux 用来识别用户的数字

  • gid:组 ID,Linux 用来识别用户主组的数字

  • comment:以逗号分隔的值,用来描述账户,通常是用户的联系方式

  • home directory:用户主目录的路径

  • login shell:为交互式登录启动的 Shell

默认的passwd文件存储在base-passwd包中,格式如下:

root::0:0:root:/root:/bin/sh
daemon:*:1:1:daemon:/usr/sbin:/bin/sh
bin:*:2:2:bin:/bin:/bin/sh
sys:*:3:3:sys:/dev:/bin/sh
sync:*:4:65534:sync:/bin:/bin/sync
games:*:5:60:games:/usr/games:/bin/sh
man:*:6:12:man:/var/cache/man:/bin/sh
lp:*:7:7:lp:/var/spool/lpd:/bin/sh
mail:*:8:8:mail:/var/mail:/bin/sh
news:*:9:9:news:/var/spool/news:/bin/sh
uucp:*:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:*:13:13:proxy:/bin:/bin/sh
www-data:*:33:33:www-data:/var/www:/bin/sh
backup:*:34:34:backup:/var/backups:/bin/sh
list:*:38:38:Mailing List Manager:/var/list:/bin/sh
irc:*:39:39:ircd:/var/run/ircd:/bin/sh
gnats:*:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:*:65534:65534:nobody:/nonexistent:/bin/sh

所有账户的直接登录功能都被禁用,密码字段上会显示一个星号,除 root 用户外,root 没有密码。这是因为默认情况下,镜像是通过启用debug-tweaks功能构建的,该功能启用了 root 用户的无密码登录功能。若启用 root 密码,则会显示加密后的 root 密码。

注意

不要忘记从生产镜像中移除debug-tweaks功能。

在相同时间安装的/etc/group文件包含系统组的信息。

core-image-minimal镜像不包括影子密码保护,但其他镜像,如core-image-full-cmdline,是包括的。启用时,所有密码字段会包含一个* x *,加密后的密码存储在/etc/shadow文件中,该文件只有超级用户才能访问。

系统所需的任何用户,如果不在我们之前看到的列表中,也需要被创建。

如何操作……

食谱添加或修改系统用户或组的标准方法是使用useradd类,该类使用以下变量:

  • USERADD_PACKAGES:此变量指定食谱中需要添加用户或组的独立包。对于主包,您可以使用以下内容:

    USERADD_PACKAGES = "${PN}"
    
  • USERADD_PARAM:此变量对应传递给 Linux useradd命令的参数,用于向系统添加新用户。

  • GROUPADD_PARAM:此变量对应传递给 Linux groupadd命令的参数,用于向系统添加新组。

  • GROUPMEMS_PARAM:此变量对应传递给 Linux groupmems命令的参数,用于管理用户主组的成员。

以下是使用useradd类的食谱示例片段:

inherit useradd

PASSWORD ?= "miDBHFo2hJSAA"
USERADD_PACKAGES = "${PN}"
USERADD_PARAM_${PN} = "--system --create-home \
                       --groups tty \
                       --password ${PASSWORD} \
                       --user-group ${PN}"

密码可以通过在主机上使用mkpasswd Linux 命令行工具生成,该工具随着whois Ubuntu 包一起安装。

还有更多……

在使用useradd类生成用户和组时,uidgid值会在包安装过程中动态分配。如果不希望这样,可以通过提供自己的passwd和组文件来分配全局静态的uidgid值。

要做到这一点,你需要在conf/local.conf文件中定义USERADDEXTENSION变量,如下所示:

USERADDEXTENSION = "useradd-staticids"

构建系统将搜索 BBPATH 变量以获取 files/passwdfiles/group 文件,以获取 uidgid 值。这些文件具有先前定义的标准 passwd 布局,密码字段被忽略。

可以使用 USERADD_UID_TABLESUSERADD_GID_TABLES 变量重写默认文件名。

您还需要定义以下内容:

USERADD_ERROR_DYNAMIC = "1"

这样做是为了当提供的文件中找不到所需的 uidgid 值时,构建系统产生错误。

注意

请注意,如果在已构建的项目中使用 useradd 类,您需要删除 tmp 目录并从 sstate-cache 目录重新构建,否则将出现构建错误。

还有一种方法可以添加与特定配方无关但与映像相关的用户和组信息 - 使用 extrausers 类。它通过映像配方中的 EXTRA_USERS_PARAMS 变量配置,并如下使用:

inherit extrausers

EXTRA_USERS_PARAMS = "\
  useradd -P password root; \
  "

这将 root 密码设置为 password

使用 sysvinit 初始化管理器

初始化管理器是根文件系统的重要部分。这是内核执行的第一件事,并且有责任启动系统的其余部分。

该配方将介绍 sysvinit 初始化管理器。

准备工作

这是 Yocto 中的默认初始化管理器,并且自操作系统起源以来一直在 Linux 中使用。内核传递一个 init 命令行参数,通常为 /sbin/init,然后启动它。此 init 进程具有 PID 1 并且是所有进程的父进程。init 进程可以由 BusyBox 实现,也可以是与 sysvinit 包独立安装的独立程序。它们都以相同的方式工作,基于 运行级别 的概念,即定义要运行哪些进程的机器状态。

init 进程将读取一个 inittab 文件,并查找默认运行级别。默认的 inittab 文件安装在 sysvinit-inittab 包中,并且如下所示:

# /etc/inittab: init(8) configuration.
# $Id: inittab,v 1.91 2002/01/25 13:35:21 miquels Exp $

# The default runlevel.
id:5:initdefault:

# Boot-time system configuration/initialization script.
# This is run first except when booting in emergency (-b) mode.
si::sysinit:/etc/init.d/rcS

# What to do in single-user mode.
~~:S:wait:/sbin/sulogin

# /etc/init.d executes the S and K scripts upon change
# of runlevel.
#
# Runlevel 0 is halt.
# Runlevel 1 is single-user.
# Runlevels 2-5 are multi-user.
# Runlevel 6 is reboot.

l0:0:wait:/etc/init.d/rc 0
l1:1:wait:/etc/init.d/rc 1
l2:2:wait:/etc/init.d/rc 2
l3:3:wait:/etc/init.d/rc 3
l4:4:wait:/etc/init.d/rc 4
l5:5:wait:/etc/init.d/rc 5
l6:6:wait:/etc/init.d/rc 6
# Normally not reached, but fallthrough in case of emergency.
z6:6:respawn:/sbin/sulogin

然后,init 运行 /etc/rcS.d 目录中以 S 开头的所有脚本,然后运行 /etc/rcN.d 目录中以 S 开头的所有脚本,其中 N 是运行级别值。

因此,init 进程只执行初始化并忽略进程。如果发生故障并且进程被终止,没有人会关心。如果系统不响应,系统看门狗将重新启动系统,但通常需要具有某种类型进程监视器以响应系统健康的应用程序,但 sysvinit 不提供这些类型的机制。

然而,sysvinit 是一个被充分理解和可靠的初始化管理器,建议保留,除非需要一些额外的功能。

如何做到...

在使用 sysvinit 作为初始化管理器时,Yocto 提供 update-rc.d 类作为辅助工具,安装初始化脚本,以便在需要时启动和停止它们。

使用此类时,你需要指定INITSCRIPT_NAME变量,该变量指定要安装的脚本名称,INITSCRIPT_PARAMS指定传递给update-rc.d工具的选项。你可以选择性地使用INITSCRIPT_PACKAGES变量列出包含初始化脚本的包。默认情况下,这只包含主包,如果提供多个包,则需要为每个包指定INITSCRIPT_NAMEINITSCRIPT_PARAMS,并使用覆盖方式。以下是一个示例片段:

INITSCRIPT_PACKAGES = "${PN}-httpd ${PN}-ftpd"
INITSCRIPT_NAME_${PN}-httpd = "httpd.sh"
INITSCRIPT_NAME_${PN}-ftpd = "ftpd.sh"
INITSCRIPT_PARAMS_${PN}-httpd = "defaults"
INITSCRIPT_PARAMS_${PN}-ftpd = "start 99 5 2 . stop 20 0 1 6 ."

当初始化脚本未绑定到特定的食谱时,我们可以为其添加一个特定的食谱。例如,以下食谱将在recipes-example/sysvinit-mount/sysvinit-mount_1.0.bb文件中运行mount.sh脚本。

DESCRIPTION = "Initscripts for mounting filesystems"
LICENSE = "MIT"

LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://mount.sh"

INITSCRIPT_NAME = "mount.sh"
INITSCRIPT_PARAMS = "start 09 S ."

inherit update-rc.d

S = "${WORKDIR}"

do_install () {
    install -d ${D}${sysconfdir}/init.d/
    install -c -m 755 ${WORKDIR}/${INITSCRIPT_NAME} ${D}${sysconfdir}/init.d/${INITSCRIPT_NAME}
}

使用systemd初始化管理器

作为sysvinit的替代方案,你可以配置项目使用systemd作为初始化管理器,尽管systemd包含更多功能。

准备工作

systemd初始化管理器正在取代大多数 Linux 发行版中的sysvinit和其他初始化管理器。它基于单元的概念,单元是所有与系统启动和维护相关的元素的抽象,而目标是将单元分组,并且可以视为运行级别的等价物。systemd定义的一些单元包括:

  • 服务

  • 套接字

  • 设备

  • 挂载点

  • 快照

  • 定时器

  • 路径

默认目标及其对应的运行级别在下表中定义:

Sysvinit 运行级别 Systemd 目标 备注
0 runlevel0.target poweroff.target 关闭系统。
1, s, single runlevel1.target rescue.target 单用户模式。
2, 4 runlevel2.target, runlevel4.target multi-user.target 用户定义的/特定站点的运行级别。默认情况下,与3相同。
3 runlevel3.target multi-user.target 多用户、非图形模式。用户通常可以通过多个控制台或网络登录。
5 runlevel5.target graphical.target 多用户、图形模式。通常包括运行级别 3 的所有服务以及图形登录。
6 runlevel6.target reboot.target 重启系统。

systemd初始化管理器设计上与sysvinit兼容,包括使用sysvinit init脚本。

systemd的一些特点包括:

  • 允许更快启动时间的并行化功能

  • 通过套接字和 D-Bus 进行服务初始化,以便仅在需要时启动服务

  • 允许进程失败恢复的进程监控

  • 系统状态快照和恢复

  • 挂载点管理

  • 基于事务依赖的单元控制,其中单元之间建立了依赖关系

如何操作...

要配置你的系统使用systemd,你需要通过将以下内容添加到分发配置文件中,在poky分发的默认配置文件 sources/poky/meta-yocto/conf/distro/poky.conf中,或者在你项目的conf/local.conf文件中,来将systemd分发功能添加到项目中:

DISTRO_FEATURES_append = " systemd"

注意

注意开始引号后的空格。

VIRTUAL-RUNTIME_init_manager = "systemd"

此配置示例允许你定义一个带有systemd的主镜像和一个带有sysvinit的救援镜像,前提是它不使用VIRTUAL-RUNTIME_init_manager变量。因此,救援镜像不能使用packagegroup-core-bootpackagegroup-core-full-cmdline食谱。例如,本章稍后将介绍的减少根文件系统镜像大小食谱中的镜像大小已被减小,可以作为救援镜像的基础。

要完全从系统中删除sysvinit,你需要执行以下操作:

DISTRO_FEATURES_BACKFILL_CONSIDERED = "sysvinit"
VIRTUAL-RUNTIME_initscripts = ""

功能回填是自动扩展机器和分发功能,以保持向后兼容性。sysvinit分发功能会自动回填,因此,要删除它,我们需要将其添加到DISTRO_FEATURES_BACKFILL_CONSIDERED变量中,如前所述。

提示

请注意,如果你正在使用现有项目,并且按照前文所述更改了DISTRO_FEATURES变量,你需要删除tmp目录并使用 sstate-cache重新构建,否则构建将失败。

还有更多...

不仅根文件系统需要配置,Linux 内核也需要特别配置,以满足systemd所需的所有功能。systemd源代码 README 文件中有一份详细的内核配置变量列表。举例来说,要扩展我们将在本章稍后介绍的减少 Linux 内核镜像大小食谱中的最小内核配置,以支持 Wandboard 的systemd,我们需要在arch/arm/configs/wandboard-quad_minimal_defconfig文件中添加以下配置更改:

+CONFIG_FHANDLE=y
+CONFIG_CGROUPS=y
+CONFIG_SECCOMP=y
+CONFIG_NET=y
+CONFIG_UNIX=y
+CONFIG_INET=y
+CONFIG_AUTOFS4_FS=y
+CONFIG_TMPFS=y
+CONFIG_TMPFS_POSIX_ACL=y
+CONFIG_SCHEDSTATS=y

为 Wandboard 提供的默认内核配置将正常启动一个带有systemdcore-image-minimal镜像。

安装 systemd 单元文件

Yocto 提供了systemd类作为辅助工具来安装单元文件。默认情况下,单元文件会安装在目标目录的${systemd_unitdir}/system路径下。

使用此类时,需要指定SYSTEMD_SERVICE_${PN}变量,值为要安装的单元文件的名称。你也可以选择使用SYSTEMD_PACKAGES变量来列出包含单元文件的包。默认情况下,这只包含主包,如果提供多个包,则需要使用覆盖方式指定SYSTEMD_SERVICE变量。

服务默认配置为在启动时自动启动,但你可以通过SYSTEMD_AUTO_ENABLE变量来更改此设置。

以下是一个示例代码片段:

SYSTEMD_PACKAGES = "${PN}-syslog"
SYSTEMD_SERVICE_${PN}-syslog = "busybox-syslog.service"
SYSTEMD_AUTO_ENABLE = "disabled"

安装包安装脚本

支持的包格式(RPM、ipk 和 deb)支持在包安装过程中添加安装脚本,这些脚本可以在不同的时间运行。在本配方中,我们将看到如何安装这些脚本。

准备工作

有不同类型的安装脚本:

  • 预安装脚本 (pkg_preinst):这些脚本在软件包解包之前调用。

  • 后安装脚本 (pkg_postinst):这些脚本在软件包解包后调用,并且会配置依赖关系。

  • 卸载前脚本 (pkg_prerm):这些脚本在已安装或至少部分安装的软件包时调用。

  • 卸载后脚本 (pkg_postrm):这些脚本在软件包的文件被移除或替换之后调用。

如何实现……

以下是一个在配方中安装预安装脚本的示例片段:

     pkg_preinst_${PN} () {
         # Shell commands
     }

所有安装脚本的工作方式相同,不同之处在于,后安装脚本可以在主机上创建根文件系统镜像时、在目标设备上(对于无法在主机上执行的操作),或者在软件包直接安装到目标设备时运行。请查看以下代码:

 pkg_postinst_${PN} () {
     if [ x"$D" = "x" ]; then
          # Commands to execute on device
     else
          # Commands to execute on host
     fi
 }

如果后安装脚本成功,软件包将被标记为已安装。如果脚本失败,软件包将被标记为未解包,脚本将在镜像再次启动时执行。

它是如何工作的……

一旦配方定义了安装脚本,特定包类型的类将在遵循特定格式的打包规则的同时安装该脚本。

对于后安装脚本,当在主机上运行时,D 被设置为目标目录,因此比较测试将失败。但当在目标设备上运行时,D 将为空。

注意

如果可能,建议在主机上执行后安装脚本,因为我们需要考虑到某些根文件系统将是只读的,因此无法在目标设备上执行某些操作。

减少 Linux 内核镜像大小

在根文件系统定制之前或与之并行,嵌入式项目通常需要进行镜像大小优化,以减少启动时间和内存使用。

更小的镜像意味着更少的存储空间、传输时间和编程时间,从而在制造和现场更新中节省成本。

默认情况下,wandboard-quad 的压缩 Linux 内核镜像(zImage)大约为 5.2 MB。这个配方将展示我们如何减少该镜像的大小。

如何实现……

一个能够从 microSD 卡根文件系统启动的 Wandboard 的最小内核配置示例是以下的 arch/arm/configs/wandboard-quad_minimal_defconfig 文件:

CONFIG_KERNEL_XZ=y
CONFIG_NO_HZ=y
CONFIG_HIGH_RES_TIMERS=y
CONFIG_BLK_DEV_INITRD=y
CONFIG_CC_OPTIMIZE_FOR_SIZE=y
CONFIG_EMBEDDED=y
CONFIG_SLOB=y
CONFIG_ARCH_MXC=y
CONFIG_SOC_IMX6Q=y
CONFIG_SOC_IMX6SL=y
CONFIG_SMP=y
CONFIG_VMSPLIT_2G=y
CONFIG_AEABI=y
CONFIG_CPU_FREQ=y
CONFIG_ARM_IMX6_CPUFREQ=y
CONFIG_CPU_IDLE=y
CONFIG_VFP=y
CONFIG_NEON=y
CONFIG_DEVTMPFS=y
CONFIG_DEVTMPFS_MOUNT=y
CONFIG_PROC_DEVICETREE=y
CONFIG_SERIAL_IMX=y
CONFIG_SERIAL_IMX_CONSOLE=y
CONFIG_REGULATOR=y
CONFIG_REGULATOR_ANATOP=y
CONFIG_MMC=y
CONFIG_MMC_SDHCI=y
CONFIG_MMC_SDHCI_PLTFM=y
CONFIG_MMC_SDHCI_ESDHC_IMX=y
CONFIG_DMADEVICES=y
CONFIG_IMX_SDMA=y
CONFIG_EXT3_FS=y

该配置生成一个 886 KB 压缩的 Linux 内核镜像(zImage)。

它是如何工作的……

除了硬件设计考虑因素(如从 NOR 闪存运行 Linux 内核,并使用就地执行 (XIP) 来避免将镜像加载到内存中),内核大小优化的第一步是审查内核配置并移除所有多余的功能。

要分析内核块的大小,我们可以使用:

$ size vmlinux */built-in.o
text    data     bss     dec     hex filename
8746205  356560  394484 9497249  90eaa1 vmlinux
117253    2418    1224  120895   1d83f block/built-in.o
243859   11158      20  255037   3e43d crypto/built-in.o
2541356  163465   34404 2739225  29cc19 drivers/built-in.o
1956       0       0    1956     7a4 firmware/built-in.o
1728762   18672   10544 1757978  1ad31a fs/built-in.o
20361   14701     100   35162    895a init/built-in.o
29628     760       8   30396    76bc ipc/built-in.o
576593   20644  285052  882289   d7671 kernel/built-in.o
106256   24847    2344  133447   20947 lib/built-in.o
291768   14901    3736  310405   4bc85 mm/built-in.o
1722683   39947   50928 1813558  1bac36 net/built-in.o
34638     848     316   35802    8bda security/built-in.o
276979   19748    1332  298059   48c4b sound/built-in.o
138       0       0     138      8a usr/built-in.o

在这里,vmlinux是 Linux 内核 ELF 镜像,可以在 Linux 的build目录中找到。

一些通常需要排除的内容包括:

  • 移除 IPv6(CONFIG_IPV6)和其他多余的网络功能

  • 如果不需要,请移除块设备(CONFIG_BLOCK

  • 如果未使用,请移除加密特性(CONFIG_CRYPTO

  • 审查支持的文件系统类型并移除不需要的文件系统,例如在没有闪存设备上使用的闪存文件系统

  • 避免模块,如果可能,移除内核中的模块支持(CONFIG_MODULES)。

一个好的策略是从一个最小的内核开始,逐步添加必要的功能直到你得到一个工作系统。从allnoconfig GNU make 目标开始,并审查CONFIG_EXPERTCONFIG_EMBEDDED下的配置项,因为这些配置项不包含在allnoconfig设置中。

以下是一些可能不太明显,但在不移除功能的情况下显著减少镜像大小的配置更改:

  • 将默认压缩方法从Lempel–Ziv–Oberhumer (LZO)改为 XZ(CONFIG_KERNEL_XZ)。不过,解压速度可能稍微慢一些。

  • 将分配器从 SLUB 更改为简单块列表 (SLOB)(CONFIG_SLOB),适用于内存较小的小型嵌入式系统。

  • 除非你有 4GB 或更多内存,否则请不要使用高内存(CONFIG_HIGHMEM)。

你可能还希望为生产和开发系统设置不同的配置,因此你可以从生产镜像中移除以下内容:

  • printk支持(CONFIG_PRINTK

  • tracing支持(CONFIG_FTRACE

在编译方面,使用CONFIG_CC_OPTIMIZE_FOR_SIZE进行大小优化。

一旦基本配置完成,我们需要分析内核功能,找出进一步的优化空间。你可以使用以下命令打印一个已排序的内核符号列表:

$ nm --size-sort --print-size -r vmlinux | head
 808bde04 00040000 B __log_buf
 8060f1c0 00004f15 r kernel_config_data
 80454190 000041f0 T hidinput_connect
 80642510 00003d40 r drm_dmt_modes
 8065cbbc 00003414 R v4l2_dv_timings_presets
 800fbe44 000032c0 T __blockdev_direct_IO
 80646290 00003100 r edid_cea_modes
 80835970 00003058 t imx6q_clocks_init
 8016458c 00002e74 t ext4_fill_super
 8056a814 00002aa4 T hci_event_packet

然后,你需要查看内核源码以找到优化点。

可以通过运行中的 Wandboard 内核日志获取未压缩内核在内存中实际使用的空间,如下所示:

$ dmesg | grep -A 3 "text"
 .text : 0x80008000 - 0x80a20538   (10338 kB)
 .init : 0x80a21000 - 0x80aae240   ( 565 kB)
 .data : 0x80ab0000 - 0x80b13644   ( 398 kB)
 .bss  : 0x80b13644 - 0x80b973fc   ( 528 kB)

从这里开始,.text部分包含代码和常量数据,.data部分包含变量的初始化数据,.bss部分包含所有未初始化的数据。.init部分仅包含在 Linux 初始化期间使用的全局变量,这些变量在之后会被释放,如下所示的 Linux 内核启动信息所示:

Freeing unused kernel memory: 564K (80a21000 - 80aae000)

当前正在进行的工作旨在减少 Linux 内核的大小,因此预计新版本的内核将更小,并且能够更好地定制以用于嵌入式系统。

减小根文件系统镜像大小

默认情况下,wandboard-quad 解包后的 core-image-minimal 大约为 45 MB,而 core-image-sato 大约为 150 MB。本文将探讨减小它们大小的方法。

如何操作...

下面显示了一个小映像示例 core-image-small,不包括 packagegroup-core-boot 配方,并且可用作根文件系统映像的基础,recipes-core/images/core-image-small.bb

DESCRIPTION = "Minimal console image."

IMAGE_INSTALL= "\
        base-files \
        base-passwd \
        busybox \
        sysvinit \
        initscripts \
        ${ROOTFS_PKGMANAGE_BOOTSTRAP} \
        ${CORE_IMAGE_EXTRA_INSTALL} \
"

IMAGE_LINGUAS = " "

LICENSE = "MIT"

inherit core-image

IMAGE_ROOTFS_SIZE ?= "8192"

该配方生成约 6.4 MB 的映像。如果使用 poky-tiny 发行版,可以通过在 conf/local.conf 文件中添加以下内容进一步减小。

DISTRO = "poky-tiny"

poky-tiny 发行版进行了一系列大小优化,可能会限制您可以包含在映像中的软件包集。要成功构建此映像,您必须跳过 Yocto 构建系统执行的某个完整性检查之一,方法是添加以下内容:

INSANE_SKIP_glibc-locale = "installed-vs-shipped"

使用 poky-tiny,映像的大小进一步减小至约 4 MB。

可以进一步减小映像的大小;例如,我们可以将 sysvinit 替换为 tiny-init,但这留给读者作为练习。

大小缩小的映像还与生产映像一起用于救援系统和制造测试流程。它们还非常适合作为 initramfs 映像,即 Linux 内核从内存中挂载的映像,甚至可以打包成单个 Linux 内核映像二进制文件。

工作原理...

从适当的映像(如 core-image-minimal)开始,并分析依赖项,如 第一章 中的 调试构建系统 配方所示,决定哪些不需要。您还可以使用映像构建历史中列出的文件大小,如 使用构建历史 配方中所见,也在 第一章 中的 构建系统 中,对文件大小按照 files-in-image.txt 文件的第四列进行逆序排序。

$ sort -r -g  -k 4,4 files-in-image.txt -o sorted-files-in-image.txt
sorted-files-in-image.txt:
-rwxr-xr-x root       root          1238640 ./lib/libc-2.19.so
-rwxr-xr-x root       root           613804 ./sbin/ldconfig
-rwxr-xr-x root       root           539860 ./bin/busybox.nosuid
-rwxr-xr-x root       root           427556 ./lib/libm-2.19.so
-rwxr-xr-x root       root           130304 ./lib/ld-2.19.so
-rwxr-xr-x root       root            88548 ./lib/libpthread-2.19.so
-rwxr-xr-x root       root            71572 ./lib/libnsl-2.19.so
-rwxr-xr-x root       root            71488 ./lib/libresolv-2.19.so
-rwsr-xr-x root       root            51944 ./bin/busybox.suid
-rwxr-xr-x root       root            42668 ./lib/libnss_files- 2.19.so
-rwxr-xr-x root       root            30536 ./lib/libnss_compat- 2.19.so
-rwxr-xr-x root       root            30244 ./lib/libcrypt-2.19.so
-rwxr-xr-x root       root            28664 ./sbin/init.sysvinit
-rwxr-xr-x root       root            26624 ./lib/librt-2.19.so

从中我们可以观察到 glic 是文件系统大小的最大贡献者。在仅限控制台系统上可以节省空间的其他地方包括:

  • 使用 IPK 软件包管理器,因为它是最轻量的,或者更好的是,完全从生产根文件系统中删除 package-management 功能。

  • conf/local.conf 文件中指定 BusyBox 的 mdev 设备管理器,而不是 udev,如下所示:

    VIRTUAL-RUNTIME_dev_manager = "mdev"
    

    请注意,这仅适用于包括 packagegroup-core-boot 的核心映像。

  • 如果我们在块设备上运行根文件系统,请在没有日志的情况下使用 ext2 而不是 ext3 或 ext4。

  • 通过在 bbappend 中提供自己的配置文件,仅配置 BusyBox 的基本应用程序:

  • 审查glibc配置,可以通过DISTRO_FEATURES_LIBC分发配置变量进行更改。它的使用示例可以在poky-tiny分发中找到,该分发包含在poky源代码中。poky-tiny分发可以作为定制小型系统分发的模板。

  • 考虑切换到比默认的glibc更轻量的C库。曾经,uclibc被用作替代方案,但该库似乎在过去几年未再维护,并且当前的 Wandboard 的core-image-minimal镜像无法使用它进行构建。

    注意

    最近,muslwww.musl-libc.org/)有了一些新的动向,它是一个新的 MIT 许可证的C库。要启用它,你需要将以下内容添加到你的conf/local.conf文件中:

    TCLIBC = "musl"

    你需要将meta-musl层(github.com/kraj/meta-musl)添加到你的conf/bblayers.conf文件中。

    它目前为 QEMU 目标构建core-image-minimal,但仍然需要一些工作才能在真实硬件(如 Wandboard)上使用它。

  • 使用-Os编译你的应用程序,以优化大小。

发布软件

当发布基于 Yocto 项目的产品时,我们必须考虑到我们是建立在许多不同的开源项目之上的,每个项目都有不同的许可要求。

至少,你的嵌入式产品将包含一个引导加载程序(可能是 U-Boot)、Linux 内核以及一个包含一个或多个应用程序的根文件系统。U-Boot 和 Linux 内核都使用通用公共许可证第 2 版GPLv2)授权。根文件系统可能包含具有不同许可证的各种程序。

所有开源许可证都允许你销售一个商业产品,该产品可以同时包含专有和开源许可证的混合,只要它们是独立的,并且产品符合所有开源许可证的要求。我们将在稍后的与开源和专有代码共存食谱中讨论开源与专有代码的共存。

在将你的产品发布到公众之前,理解所有许可的影响非常重要。Yocto 项目提供了工具,使得处理许可要求变得更加简单。

准备工作

我们首先需要明确我们在使用 Yocto 项目构建的产品中需要遵守的要求。对于最严格的开源许可证,这通常意味着:

  • 源代码分发,包括修改

  • 许可文本分发

  • 分发构建和运行软件所用的工具

如何操作...

我们可以使用archiver类来提供需要分发的成果物,以遵守许可要求。我们可以将其配置为:

  • 提供原始未修补源代码作为 tar 包

  • 提供应用于原始源代码的补丁

  • 提供用于构建源代码的配方

  • 提供有时必须附带的许可证文本(根据某些许可证的要求)

为了使用前面提到的archiver类,我们需要在conf/local.conf文件中添加以下内容:

INHERIT += "archiver"
ARCHIVER_MODE[src] = "original"
ARCHIVER_MODE[diff] = "1"
ARCHIVER_MODE[recipe] = "1"
COPY_LIC_MANIFEST = "1"
COPY_LIC_DIRS = "1"

源代码将在tmp/deploy/sources目录下的一个许可证子目录层次中提供。

对于wandboard-quad,我们可以在tmp/deploy/sources下找到以下目录:

  • allarch-poky-linux

  • arm-poky-linux-gnueabi

在查看分发的 Linux 内核源代码时,作为 GPLv2 软件包,我们可以在tmp/deploy/sources/arm-poky-linux-gnueabi/linux-wandboard-3.10.17-r0下找到以下内容:

  • defconfig

  • github.com.wandboard-org.linux.git.tar.gz

  • linux-wandboard-3.10.17-r0-recipe.tar.gz

所以我们有了内核配置、源代码压缩包,以及用于构建它的配方,其中包括:

  • linux-wandboard_3.10.17.bb

  • linux-dtb.inc

  • linux-wandboard.inc

根文件系统软件包的许可证文本也将包含在根文件系统中的/usr/share/common-licenses目录下,并按照软件包的目录层次进行存放。

该配置将为所有构建软件包提供交付物,但我们真正想做的是仅为那些许可证要求我们提供交付物的软件包提供它们。

当然,我们不希望盲目地分发source目录中的所有内容,因为它还会包含我们的专有源代码,而我们很可能不希望分发这些代码。

我们可以配置archiver类,仅为 GPL 和 LGPL 软件包提供源代码,方法如下:

COPYLEFT_LICENSE_INCLUDE = "GPL* LGPL*"
COPYLEFT_LICENSE_EXCLUDE = "CLOSED Proprietary"

此外,对于嵌入式产品,我们通常只关心实际出现在产品中的软件,因此我们可以通过以下方式限制要归档的配方类型,仅限于目标镜像:

COPYLEFT_RECIPE_TYPES = "target"

我们应当获得法律建议,以决定哪些软件包的许可证要求我们进行源代码分发。

还有其他配置选项,例如提供补丁或配置过的源代码,而不是分开的原始源代码和补丁,或者提供源rpm包而不是源代码压缩包。有关更多详情,请参见archiver类。

还有更多内容…

我们还可以选择分发整个构建环境。通常做到这一点的最佳方法是将我们的 BSP 和软件层发布到公共 Git 仓库中。我们的软件层可以提供bblayers.conf.samplelocal.conf.sample,用于设置即用型build目录。

另请参见

  • 这里没有讨论的其他要求包括选择的分发机制。建议在发布产品之前获得法律建议,以确保所有许可证义务都已履行。

分析您的系统以确保合规性

Yocto 构建系统使得向我们的法律顾问提供审计信息变得更加容易。此配方将解释如何操作。

如何做...

tmp/deploy/licenses下,我们可以找到一个包含软件包(及其相应许可证)的目录列表,以及一个包含软件包和许可证清单的image文件夹。

对于之前提供的示例镜像core-image-small,我们有以下内容:

tmp/deploy/licenses/core-image-small-wandboard-quad-<timestamp>/package.manifest
base-files
base-passwd
busybox
busybox-syslog
busybox-udhcpc
initscripts
initscripts-functions
libc6
run-postinsts
sysvinit
sysvinit-inittab
sysvinit-pidof
update-alternatives-opkg
update-rc.d

相应的tmp/deploy/licenses/core-image-small-wandboard-quad-<timestamp>/license.manifest文件摘录如下:

PACKAGE NAME: base-files
PACKAGE VERSION: 3.0.14
RECIPE NAME: base-files
LICENSE: GPLv2

PACKAGE NAME: base-passwd
PACKAGE VERSION: 3.5.29
RECIPE NAME: base-passwd
LICENSE: GPLv2+

这些文件可以用来分析构成我们根文件系统的所有不同包。我们还可以对它们进行审核,以确保在公开发布产品时遵守许可证。

还有更多内容

你可以通过使用INCOMPATIBLE_LICENSE配置变量来指示 Yocto 构建系统特意避免某些许可证。通常使用它的方法是通过在conf/local.conf文件中添加以下内容来避免使用 GPLv3 类型的许可证:

INCOMPATIBLE_LICENSE = "GPL-3.0 LGPL-3.0 AGPL-3.0"

这将构建core-image-minimalcore-image-base镜像,只要没有包含额外的镜像特性。

使用开源和专有代码

嵌入式产品通常是基于像 Yocto 这样的开源系统构建的,并且包含增加价值和专门化产品的专有软件。这个专有部分通常是知识产权,需要得到保护,了解它如何与开源软件共存非常重要。

本食谱将讨论一些常见的开源包示例,这些包通常出现在嵌入式产品中,并简要解释如何将专有软件与它们一起使用。

如何做到这一点...

开源许可证可以大致分为两类,取决于它们是否:

  • 宽松性:这些许可证类似于互联网软件协会ISC)、MIT 和 BSD 许可证。它们附带的要求较少,只要求我们保留版权声明。

  • 限制性:这些许可证类似于 GPL,它们要求我们不仅要分发源代码和修改版本,无论是与二进制文件一起还是在后期分发,还需要分发构建、安装和运行源代码的工具。

然而,某些许可证可能会“污染”修改和衍生作品的条件,这些通常被称为病毒性许可证,而其他一些则不会。例如,如果你将应用程序链接到 GPL 许可的代码,那么你的应用程序也将受到 GPL 的约束。

GPL 的病毒性特性让一些人对使用 GPL 许可的软件保持警惕,但需要注意的是,专有软件可以与 GPL 软件共存,只要理解并遵守许可证条款。

例如,违反 GPLv2 许可证将意味着失去将来分发 GPLv2 代码的权利,即使进一步的分发符合 GPLv2 规定。在这种情况下,能够再次分发代码的唯一方法是请求版权持有者的许可。

它是如何工作的...

接下来,我们将提供有关一些常见用于嵌入式产品的开源包的许可要求的指南。这并不构成法律建议,如前所述,在公开发布之前,应该对你的产品进行适当的法律审查。

U-Boot 引导程序

U-Boot 采用 GPLv2 许可证,但由它启动的任何程序不会继承其许可证。因此,你可以自由使用 U-Boot 启动专有操作系统。例如,你可以使用 U-Boot 启动一个专有操作系统。然而,你的最终产品必须遵守关于 U-Boot 的 GPLv2 条款,因此必须提供 U-Boot 的源代码和修改。

Linux 内核

Linux 内核也采用 GPLv2 许可证。任何在 Linux 内核用户空间运行的应用程序都不会继承其许可证,因此你可以在 Linux 上自由运行你的专有软件。然而,Linux 内核模块是 Linux 内核的一部分,因此必须遵守 GPLv2 许可证。此外,你的最终产品必须发布 Linux 内核源代码和修改,包括在你的产品中运行的外部模块。

Glibc

GNU C库采用较宽松通用公共许可证LGPL),允许动态链接而无需继承许可证。因此,你的专有代码可以与glibc动态链接,但当然你仍然需要遵守关于glibc的 LGPL。不过,值得注意的是,静态链接你的应用程序会使其与 LGPL 绑定。

BusyBox

BusyBox 也采用 GPLv2 许可证。该许可证允许不相关的软件与它一起运行,因此你的专有软件可以与 BusyBox 自由运行。如前所述,关于 BusyBox,你需要遵守 GPLv2 并分发其源代码和修改。

Qt 框架

Qt 有三种不同的许可证,这对于开源项目来说是很常见的。你可以选择是否需要商业许可证(在这种情况下,你的专有应用程序将受到保护),LGPL 许可证(如前所述,通过允许你的应用程序与 Qt 框架动态链接,只要你遵守 LGPL 条款,也能保护你的专有软件),或者 GPLv3 许可证(它将被继承到你的应用程序中)。

X Windows 系统

X.Org源代码采用宽松的 MIT 风格许可证。因此,只要声明使用情况并保留版权声明,你的专有软件可以自由使用它。

还有更多内容...

让我们看看如何将我们的专有许可证代码集成到 Yocto 构建系统中。在为我们的应用程序准备配方时,我们可以采取几种许可证方式:

  • LICENSE标记为封闭。这是专有应用程序的常见做法。我们使用以下方式:

    LICENSE = "CLOSED"
    
  • LICENSE标记为专有,并包括某种类型的许可协议。这通常在发布二进制文件时使用,附带某种终端用户协议,并在配方中引用。例如,meta-fsl-arm使用这种类型的许可证来遵守 Freescale 的终端用户许可协议。以下是一个示例:

    LICENSE = "Proprietary"
    LIC_FILES_CHKSUM = "file://EULA.txt;md5=93b784b1c11b3fffb1638498a8dde3f6"
    
  • 提供多种许可选项,例如开源许可证和商业许可证。在这种情况下,LICENSE 变量用于指定开源许可证,而 LICENSE_FLAGS 变量用于商业许可证。一个典型的例子是 Poky 中的gst-plugins-ugly包:

    LICENSE = "GPLv2+ & LGPLv2.1+ & LGPLv2+"
    LICENSE_FLAGS = "commercial"
    LIC_FILES_CHKSUM = "file://COPYING;md5=a6f89e2100d9b6cdffcea4f398e37343 \ file://gst/synaesthesia/synaescope.h;beginline=1;endline=20 ;md5=99f301df7b80490c6ff8305fcc712838 \  file://tests/check/elements/xingmux.c;beginline=1;endline=2 1;md5=4c771b8af188724855cb99cadd390068 \  file://gst/mpegstream/gstmpegparse.h;beginline=1;endline=18 ;md5=ff65467b0c53cdfa98d0684c1bc240a9"
    

当在配方中设置了 LICENSE_FLAGS 变量时,除非许可证也出现在 LICENSE_FLAGS_WHITELIST 变量中,否则包将不会被构建,通常该变量在你的 conf/local.conf 文件中定义。对于前面的例子,我们需要添加:

LICENSE_FLAGS_WHITELIST = "commercial"

LICENSELICENSE_FLAGS_WHITELIST 变量可以精确匹配以进行非常狭窄的匹配,也可以像前面例子那样广泛匹配,匹配所有以 commercial 开头的许可证。对于狭窄匹配,包名必须附加到许可证名称上;例如,如果我们只希望将前面例子中的 gst-plugins-ugly 包加入白名单,而不包括其他包,我们可以使用如下方式:

LICENSE_FLAGS_WHITELIST = "commercial_gst-plugins-ugly"

另见

  • 你应该参考具体的许可证,以完整理解它们所施加的要求。你可以在spdx.org/licenses/找到完整的开源许可证列表及其文档。

第四章:应用程序开发

在本章中,我们将覆盖以下配方:

  • 介绍工具链

  • 准备和使用 SDK

  • 使用应用程序开发工具包

  • 使用 Eclipse IDE

  • 开发 GTK+ 应用程序

  • 使用 Qt Creator IDE

  • 开发 Qt 应用程序

  • 描述应用程序开发的工作流

  • 使用 GNU make

  • 使用 GNU 构建系统

  • 使用 CMake 构建系统

  • 使用 SCons 构建工具

  • 使用库进行开发

  • 使用 Linux 帧缓冲区

  • 使用 X Windows 系统

  • 使用 Wayland

  • 添加 Python 应用程序

  • 集成 Oracle Java 运行时环境

  • 集成 Open Java 开发工具包

  • 集成 Java 应用程序

介绍

专用应用程序是定义嵌入式产品的关键,Yocto 提供了有用的应用程序开发工具,以及与流行的 集成开发环境IDE)如 Eclipse 和 Qt Creator 集成的功能。它还提供了广泛的工具类,帮助将完成的应用程序集成到构建系统和目标映像中。

本章将介绍 IDE,并向我们展示如何在真实硬件上使用它们构建和调试 C 和 C++ 应用程序,同时探索应用程序开发,包括图形框架和 Yocto 集成,不仅适用于 C 和 C++,还包括 Python 和 Java 应用程序。

介绍工具链

工具链是一组用于构建应用程序以在计算机平台上运行的工具、二进制文件和库。在 Yocto 中,工具链基于 GNU 组件。

准备就绪

一个 GNU 工具链包含以下组件:

  • 汇编器(GNU as):这是 binutils 包的一部分

  • 链接器(GNU ld):这也是 binutils 包的一部分

  • 编译器(GNU gcc):支持 C、C++、Java、Ada、Fortran 和 Objective C

  • 调试器(GNU gdb):这是 GNU 调试器

  • 二进制文件工具(objdump、nm、objcopy、readelf、strip 等):这些是 binutils 包的一部分。

这些组件足以构建裸机应用程序、引导程序(如 U-Boot)或操作系统(如 Linux 内核),因为它们不需要 C 库,并且实现了所需的 C 库函数。然而,对于 Linux 用户空间应用程序,则需要一个符合 POSIX 标准的 C 库。

GNU C 库,glibc,是 Yocto 项目中使用的默认 C 库。Yocto 正在引入对 musl(一个较小的 C 库)的支持,但正如我们之前提到的,仍需进行一些工作,才能在 FSL 社区层支持的硬件平台上使用。

但是在嵌入式系统中,我们不仅需要一个工具链,还需要一个交叉编译工具链。这是因为我们在主机计算机上构建,但将生成的二进制文件在目标机器上运行,而目标机器通常具有不同的架构。实际上,工具链有几种类型,基于构建工具链的机器架构(构建机器)、运行工具链的机器架构(主机机器)和运行工具链构建的二进制文件的机器架构(目标机器)。最常见的组合有:

  • 本地:一个例子是运行在 x86 机器上的工具链,该工具链也是在 x86 机器上构建的,并生成用于在 x86 机器上运行的二进制文件。这在桌面计算机中很常见。

  • 交叉编译:这是嵌入式系统中最常见的情况;例如,一台 x86 机器运行着在 x86 机器上构建的工具链,但生成用于在不同架构上运行的二进制文件,如 ARM。

  • 交叉本地:这通常是运行在目标上的工具链。例如,工具链是在 x86 机器上构建的,但它在 ARM 上运行并生成 ARM 用的二进制文件。

  • 加拿大:很少见,这里构建、主机和目标机器都是不同的。

构建交叉编译工具链的过程复杂且容易出错,因此自动化的工具链构建工具应运而生,如buildrootcrosstool-NG。Yocto 构建系统在每次构建时都会编译自己的工具链,正如我们将看到的,你也可以使用这个工具链进行应用程序开发。

但是交叉编译工具链和 C 库并不是构建应用程序所需的唯一内容;我们还需要一个sysroot,即在主机上具有与目标根文件系统相同的库和头文件的根文件系统。

交叉编译工具链、sysroot,以及有时的其他开发工具(如 IDE)的组合被称为 SDK,即软件开发工具包。

如何做……

使用 Yocto 项目获取 SDK 的方式有几种:

  • 使用应用程序开发工具包ADT)。

    如果你使用的是 Poky 支持的硬件平台(即虚拟化的 QEMU 机器或其中一块参考板),建议使用 ADT,它会为你安装所有所需的 SDK 组件。

  • 下载预编译的工具链。

    获取支持平台的交叉编译工具链最简单的方法是下载一个预编译的版本;例如,可以从 Yocto 项目下载站点获取:downloads.yoctoproject.org/releases/yocto/yocto-1.7.1/toolchain/。Yocto 项目为 32 位和 64 位 i686 主机提供预构建的工具链,并为armv5armv7架构提供预构建的 ARM 工具链。这些工具链包含与core-image-sato目标映像匹配的sysroot。然而,预构建的sysroot是软浮点的,因此无法与 FSL 社区层为基于 i.MX6 的平台构建的目标映像一起使用,因为它们是硬浮点的。要为 x86_64 主机安装预构建的 armv7 工具链,请运行以下命令:

    $ wget http://downloads.yoctoproject.org/releases/yocto/yocto- 1.7.1/toolchain/x86_64/poky-glibc-x86_64-core-image-sato- armv7a-vfp-neon-toolchain-1.7.1.sh
    $ chmod a+x poky-glibc-x86_64-core-image-sato-armv7a-vfp-neon- toolchain-1.7.1.sh
    $ ./poky-glibc-x86_64-core-image-sato-armv7a-vfp-neon- toolchain-1.7.1.sh
    
    
  • 构建你自己的工具链安装程序。

    在大多数嵌入式 Linux 项目中,你的机器将由外部层支持,并且你将拥有一个定制的根文件系统,你的sysroot需要与其匹配。因此,当你有一个定制的根文件系统时,推荐构建你自己的工具链安装程序。例如,适合与 Wandboard 配合使用的理想工具链应该是Cortex-A9特定的,并且针对生成硬浮点二进制文件。

  • 使用 Yocto 项目构建系统。

    最后,如果你的主机上已经安装了 Yocto 构建系统,你也可以用它来进行应用程序开发。通常,应用程序开发人员不需要 Yocto 构建系统安装的复杂性,因此一个针对目标系统的工具链安装程序就足够了。

准备和使用 SDK

Yocto 构建系统可以用来为目标系统生成交叉编译工具链和匹配的sysroot

准备就绪

我们将使用之前使用过的wandboard-quad构建目录,并按照以下方式加载setup-environment脚本:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad

如何操作...

使用 Yocto 构建系统构建 SDK 有几种方式:

  • meta-toolchain目标。

    这种方法将构建一个与目标平台匹配的工具链,以及一个基本的sysroot,但它不会与目标根文件系统匹配。然而,这个工具链可以用来构建裸机软件,比如 U-Boot 引导加载程序或 Linux 内核,它们不需要sysroot。Yocto 项目为支持的硬件平台提供可下载的sysroot。你也可以使用以下方法自行构建这个工具链:

    $ bitbake meta-toolchain
    
    

    构建完成后,可以使用以下命令安装:

    $ cd tmp/deploy/sdk
    $ ./poky-glibc-x86_64-meta-toolchain-cortexa9hf-vfp-neon- toolchain-1.7.1.sh
    
    
  • populate_sdk任务。

    这是构建与目标平台匹配的工具链,并且sysroot匹配目标根文件系统的推荐方式。你可以使用以下命令进行构建:

    $ bitbake core-image-sato -c populate_sdk
    
    

    你应该将core-image-sato替换为你希望sysroot匹配的目标根文件系统映像。构建完成的工具链可以使用以下命令安装:

    $ cd tmp/deploy/sdk
    $ ./poky-glibc-x86_64-core-image-sato-cortexa9hf-vfp-neon- toolchain-1.7.1.sh
    
    

    此外,如果你希望工具链能够构建静态应用程序,则需要向其中添加静态库。你可以通过将特定的静态库添加到目标镜像来实现,静态库也可以用于本地编译。例如,要添加静态的glibc库,可以在conf/local.conf文件中添加以下内容:

    IMAGE_INSTALL_append =  " glibc-staticdev"
    

    然后,按照之前的说明构建工具链,以匹配你的根文件系统。

    通常,你不会希望将静态库添加到镜像中,但如果你希望能够交叉编译静态应用程序,则可以通过添加以下内容将所有静态库添加到工具链中:

    SDKIMAGE_FEATURES_append = " staticdev-pkgs"
    
  • meta-toolchain-qt目标。

    该方法将扩展meta-toolchain以构建 Qt 应用程序。稍后我们将看到如何构建 Qt 应用程序。要构建此工具链,请执行以下命令:

    $ bitbake meta-toolchain-qt
    
    

    一旦构建完成,可以使用以下命令进行安装:

    $ cd tmp/deploy/sdk
    $ ./poky-glibc-x86_64-meta-toolchain-qt-cortexa9hf-vfp-neon- toolchain-qt-1.7.1.sh
    
    

    生成的工具链安装程序将位于tmp/deploy/sdk目录下,适用于此处提到的所有情况。

  • meta-ide-support目标。

    该方法不会生成工具链安装程序,但它会准备当前的构建项目以使用其自己的工具链。它将在 tmp 目录中生成一个environment-setup脚本。

    $ bitbake meta-ide-support
    
    

    要使用捆绑的工具链,你现在可以按如下方式引入该脚本:

    $ source tmp/environment-setup-cortexa9hf-vfp-neon-poky-linux- gnueabi
    
    

使用应用程序开发工具包

ADT 是一个 SDK 安装脚本,用于在 Poky 支持的硬件平台上安装以下内容:

  • 一个预构建的交叉编译工具链,如前所述

  • core-image-sato目标镜像匹配的sysroot

  • QEMU 模拟器

  • 用于系统分析的其他开发用户空间工具(将在后续章节中讨论)

准备就绪

要安装 ADT,可以选择以下两种方式之一:

  • 使用以下命令从 Yocto 项目的下载站点下载预编译的 tarball:

    $ wget http://downloads.yoctoproject.org/releases/yocto/yocto- 1.7.1/adt-installer/adt_installer.tar.bz2
    
    
  • 使用你的 Yoctobuild目录构建一个。

ADT 安装程序是一个自动化脚本,用于安装预编译的 Yocto SDK 组件,因此无论你是下载预构建版本还是自己构建,都将是相同的。

然后,你可以在运行之前配置它,以定制安装。

请注意,ADT 仅适用于 Poky 支持的平台。例如,除非提供自己的组件,否则它对于像wandboard-quad这样的外部硬件并没有多大用处。

如何操作...

要从你的 Yoctobuild目录构建 ADT,请打开一个新的 shell 并执行以下命令:

$ cd /opt/yocto/poky
$ source oe-init-build-env qemuarm
$ bitbake adt-installer

ADT 的 tarball 将位于tmp/deploy/sdk目录下。

它是如何工作的...

要安装它,请按照以下步骤操作:

  1. 将 tarball 提取到你选择的位置:

    $ cd /opt/yocto
    $ cp /opt/yocto/poky/qemuarm/tmp/deploy/sdk/adt_installer.tar.bz2 /opt/yocto
    $ tar xvf adt_installer.tar.bz2
    $ cd /opt/yocto/adt-installer
    
    
  2. 通过编辑adt_installer.conf文件来配置安装。一些选项包括:

    • YOCTOADT_REPO:这是一个包含软件包和根文件系统的仓库。默认情况下,它使用 Yocto 项目网站上的仓库,adtrepo.yoctoproject.org/1.7.1/,但是你也可以自己设置一个,包含自定义的软件包和根文件系统。

    • YOCTOADT_TARGETS:定义 SDK 的机器目标。默认情况下,这是 ARM 和 x86。

    • YOCTOADT_QEMU:此选项控制是否安装 QEMU 模拟器。默认情况下会安装它。

    • YOCTOADT_NFS_UTIL:此选项控制是否安装用户模式 NFS。如果你打算在基于 QEMU 的机器上使用 Eclipse IDE,建议启用此选项。默认情况下会安装它。

    然后针对特定目标架构(仅针对 ARM 显示):

    • YOCTOADT_ROOTFS_arm:定义从 ADT 仓库下载的特定根文件系统镜像。默认情况下,安装 minimalsato-sdk 镜像。

    • YOCTOADT_TARGET_SYSROOT_IMAGE_arm:这是用于创建 sysroot 的根文件系统。此选项必须包含在之前说明过的 YOCTOADT_ROOTFS_arm 选择中。默认情况下,这是 sato-sdk 镜像。

    • YOCTOADT_TARGET_MACHINE_arm:这是下载镜像的机器类型。默认情况下,这是 qemuarm

    • YOCTOADT_TARGET_SYSROOT_LOC_arm:这是主机上安装目标 sysroot 的路径。默认情况下是 $HOME/test-yocto/

  3. 按照以下方式运行 ADT 安装程序:

    $ ./adt_installer
    
    

    安装过程中将提示你选择安装位置(默认是 /opt/poky/1.7.1),并询问是否以交互模式或静默模式运行。

使用 Eclipse IDE

Eclipse 是一个开源集成开发环境(IDE),主要使用 Java 编写,并根据 Eclipse 公共许可证EPL)发布。它可以通过插件进行扩展,Yocto 项目发布了一个 Yocto 插件,允许我们使用 Eclipse 进行应用开发。

准备工作

Yocto 1.7 为两个不同版本的 Eclipse 提供了 Yocto 插件,分别是 Juno 和 Kepler。它们可以从 downloads.yoctoproject.org/releases/yocto/yocto-1.7.1/eclipse-plugin/ 下载。我们将使用 Kepler 4.3,因为它是最新的。我们将从 Eclipse Kepler 标准版开始,并安装所需的所有插件。

推荐在 Oracle Java 1.7 下运行 Eclipse,尽管其他 Java 提供商也受支持。你可以从 Oracle 网站安装 Oracle Java 1.7,www.java.com/en/,或者使用 Ubuntu Java 安装程序 PPA,launchpad.net/~webupd8team/+archive/ubuntu/java。后者将 Java 与你的包管理系统集成,因此更为推荐。安装步骤如下:

$ sudo add-apt-repository ppa:webupd8team/java
$ sudo apt-get update
$ sudo apt-get install oracle-java7-set-default

要下载并安装适用于 x86_64 主机的 Eclipse Kepler 标准版,请按以下步骤操作:

  1. 从 Eclipse 下载站点获取 tar 包,eclipse.org/downloads/packages/release/Kepler/SR2。例如:

     $ wget http://download.eclipse.org/technology/epp/downloads/release/kepler/SR2/eclipse-standard-kepler-SR2-linux-gtk-x86_64.tar.gz
    
    
  2. 将其解压到你选择的位置,如下所示:

    $ tar xvf eclipse-standard-kepler-SR2-linux-gtk-x86_64.tar.gz
    
    
  3. 启动 Eclipse IDE,使用以下命令:

    $ nohup eclipse/eclipse &
    
    
  4. 帮助下拉菜单中选择安装新软件。然后选择Kepler - http://download.eclipse.org/releases/kepler源。

  5. 安装以下 Eclipse 组件:

    • Linux 工具:

      LTTng - Linux 跟踪工具包

    • 移动和设备开发:

      C/C++ 远程启动

      远程系统浏览器最终用户运行时

      远程系统浏览器用户操作

      目标管理终端

      TCF 远程系统浏览器插件

      TCF 目标浏览器

    • 编程语言:

      C/C++ 自动工具支持

      C/C++ 开发工具

  6. 通过添加此仓库源来安装 Eclipse Yocto 插件:downloads.yoctoproject.org/releases/eclipse-plugin/1.7.1/kepler,如以下截图所示:准备就绪

  7. 选择Yocto 项目 ADT 插件并忽略未签名的内容警告。我们不会覆盖其他插件扩展。

如何操作...

要配置 Eclipse 使用 Yocto 工具链,请转到 窗口 | 首选项 | Yocto 项目 ADT

ADT 配置提供了两种交叉编译器选项:

  1. 独立预构建工具链:当你通过工具链安装程序或 ADT 安装程序安装了工具链时,请选择此项。

  2. 基于构建系统的工具链:当使用之前提到的通过 meta-ide-support 准备的 Yocto build 目录时,请选择此项。

它还提供了两个目标选项:

  1. QEMU 模拟器:如果你使用的是虚拟化机器上的 Poky,并且已经使用 ADT 安装程序安装了 qemuarm Linux 内核和根文件系统,请选择此项。

  2. 外部硬件:如果你使用的是像 wandboard-quad 这样的真实硬件,请选择此选项。此选项对于嵌入式开发最为有用。

使用 ADT 安装程序及其默认配置时的示例配置是选择独立预构建的工具链选项,并配合 QEMU 模拟器,如下所示:

  • 交叉编译器选项:

    • 独立预构建的工具链:

      工具链根位置/opt/poky/1.7.1

      Sysroot 位置${HOME}/test-yocto/qemuarm

      目标架构armv5te-poky-linux-gnueabi

    • 目标选项:

      QEMU 内核/tmp/adt-installer/download_image/zImage-qemuarm.bin

    如何操作...

对于使用 wandboard-quad 参考板的基于构建系统的工具链,你将需要以下内容:

  • 交叉编译器选项:

    • 基于构建系统的工具链:

      工具链根位置/opt/yocto/fsl-community-bsp/wandboard-quad

      Sysroot 位置/opt/yocto/fsl-community-bsp/wandboard-quad/tmp/sysroots/wandboard-quad

    如何操作...

还有更多...

为了在远程目标上进行调试,目标需要运行 tcf-agent 守护进程。它默认包含在 SDK 镜像中,但你也可以通过在 conf/local.conf 文件中添加以下内容将其包含到其他镜像中:

EXTRA_IMAGE_FEATURES += "eclipse-debug"

另见

开发 GTK+ 应用程序

本教程将展示如何使用 Eclipse IDE 构建、运行和调试图形化 GTK+ 应用程序。

准备工作

  1. 如下所示,将 eclipse-debug 功能添加到项目的 conf/local.conf 文件中:

    EXTRA_IMAGE_FEATURES += "eclipse-debug"
    
  2. 如下所示,构建一个 core-image-sato 目标镜像:

    $ cd /opt/yocto/fsl-community-bsp/
    $ source setup-environment wandboard-quad
    $ bitbake core-image-sato
    
    
  3. 如下所示构建一个 core-image-sato 工具链:

    $ bitbake -c populate_sdk core-image-sato
    
    
  4. 如下所示安装工具链:

    $ cd tmp/deploy/sdk
    $ ./poky-glibc-x86_64-core-image-sato-cortexa9hf-vfp-neon- toolchain-1.7.1.sh
    
    

在启动 Eclipse IDE 之前,我们可以检查是否能够手动构建并启动一个 GTK 应用程序。我们将构建以下 GTK+ Hello World 应用程序:

以下是 gtk_hello_world.c 的代码:

#include <gtk/gtk.h>

int main(int argc, char *argv[])
{
  GtkWidget *window;
  gtk_init (&argc, &argv);
  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_widget_show (window);
  gtk_main ();
  return 0;
}

要构建它,我们使用之前描述的安装好的 core-image-sato 工具链:

$ source /opt/poky/1.7.1/environment-setup-cortexa9hf-vfp-neon-poky- linux-gnueabi
$ ${CC} gtk_hello_world.c -o helloworld `pkg-config --cflags --libs gtk+-2.0`

该命令使用 pkg-config 辅助工具读取与 GTK 库一起安装在 sysroot 中的 .pc 文件,以确定编译使用 GTK 的程序所需的编译器选项(--cflags 用于 include 目录,--libs 用于要链接的库)。

我们可以手动将生成的二进制文件复制到 Wandboard,并在通过 NFS 启动 core-image-sato 时,从目标控制台运行:

# DISPLAY=:0 helloworld

这将打开一个 GTK+ 窗口,显示在 SATO 桌面上。

如何操作...

我们现在可以使用前面描述的独立工具链配置 Eclipse ADT 插件,或者我们也可以决定使用派生自构建系统的工具链。

如何操作...

按照以下步骤构建并运行一个示例 Hello World 应用程序:

  1. 创建一个新的 Hello World GTK Autotools 项目。在项目创建向导中接受所有默认选项。浏览到 文件 | 新建 | 项目 | C/C++ | C 项目 | Yocto 项目 ADT Autotools 项目 | Hello World GTK C Autotools 项目

    提示

    在为项目命名时,请避免使用破折号等特殊字符,因为它们可能会导致构建工具出现问题。

  2. 通过访问 项目 | 构建项目 来构建项目。如何操作...

  3. 即使项目成功构建,您也可能会看到源代码和 问题 标签中标记的错误。这是因为 Eclipse 的代码分析功能无法解析项目的所有符号。为了解决这个问题,您需要通过以下路径将所需的 include 头文件添加到项目属性中:项目 | 属性 | C/C++ 常规 | 路径和符号 | 包含如何操作...

  4. 运行 | 运行配置下,你应该有一个名为<project_name>_gdb_arm-poky-linux-gnueabiC/C++远程应用程序与 TCF 目标。如果没有,创建一个。如何操作...

  5. 使用新建...按钮在选项卡中创建一个到目标 IP 地址的新 TCF 连接。如何操作...

  6. C/C++应用程序的远程绝对文件路径字段中填写二进制文件的路径,并包括二进制文件名;例如,/gtk_hello_world

  7. 应用程序执行前的命令字段中,输入export DISPLAY=:0如何操作...

  8. 运行应用程序并以root用户登录,密码为空。你应该能够在 SATO 桌面上看到 GTK 应用程序,并在控制台标签中看到以下输出:如何操作...

提示

如果你在连接目标时遇到问题,可以通过在目标的控制台输入以下命令来验证它是否正在运行 tcf-agent

# ps w | grep tcf
735 root     11428 S    /usr/sbin/tcf-agent -d -L- -l0

如果你遇到登录问题,可以使用 Eclipse 的远程系统资源管理器RSE)视图来清除密码并调试与目标的连接。一旦能够建立连接,并且通过 RSE 浏览目标的文件系统,你可以返回到运行配置。

还有更多...

要调试应用程序,请按照以下步骤进行:

  1. 进入运行 | 调试配置

  2. 调试器标签下,验证 GDB 调试器路径是否指向正确的工具链调试器位置。

    /opt/poky/1.7.1/sysroots/x86_64-pokysdk-linux/usr/bin/arm- poky-linux-gnueabi/arm-poky-linux-gnueabi-gdb
    
    

    如果没有,指向正确的位置。

    还有更多...

  3. 双击源文件中的main函数以添加断点。侧边栏将显示一个蓝点。

  4. 点击调试按钮,调试视图出现,并且应用程序在远程 Wandboard 硬件上执行。还有更多...

    提示

    如果遇到文本文件正忙错误,请记得关闭我们在前一点运行的应用程序。

使用 Qt Creator IDE

Qt Creator 是一个多平台的 IDE,属于 Qt 应用程序开发框架 SDK 的一部分。它是 Qt 应用程序开发的首选 IDE,并提供多种许可证,包括 GPLv3、LGPLv2 和商业许可证。

准备就绪

  1. 从 Qt 项目下载网站下载并安装适合你主机的 Qt Creator 3.3.0 版本。对于 x86_64 Linux 主机的下载和安装,你可以使用以下命令:

    $ wget http://download.qt.io/official_releases/qtcreator/3.3/3.3.0/qt -creator-opensource-linux-x86_64-3.3.0.run
    $ chmod u+x qt-creator-opensource-linux-x86_64-3.3.0.run
    $ ./qt-creator-opensource-linux-x86_64-3.3.0.run
    
    
  2. 构建一个准备好开发 Qt 应用程序的工具链,使用以下命令:

    $ cd /opt/yocto/fsl-community-bsp/
    $ source setup-environment wandboard-quad
    $ bitbake meta-toolchain-qt
    
    
  3. 按以下方式安装:

    $ cd tmp/deploy/sdk
    $ ./poky-glibc-x86_64-meta-toolchain-qt-cortexa9hf-vfp-neon- toolchain-qt-1.7.1.sh
    
    

如何操作...

在启动 Qt Creator 之前,我们需要设置开发环境。为了在启动 Qt Creator 时自动完成这一过程,我们可以通过在bin/qtcreator.sh文件的开头添加以下行来修补其初始化脚本:

source /opt/poky/1.7.1/environment-setup-cortexa9hf-vfp-neon-poky- linux-gnueabi
#! /bin/sh

注意

请注意,环境初始化脚本位于哈希标记之前。

现在我们可以按照以下方式运行 Qt Creator:

$ ./bin/qtcreator.sh &

并通过转到工具 | 选项并按照以下步骤进行配置:

  1. 首先,我们为我们的 Wandboard 配置一个新设备。在设备 | 添加下,选择通用 Linux 设备如何操作...

    通过在目标的根控制台使用passwd命令设置根密码,并将其输入密码字段。

  2. 构建与运行下,我们配置一个新的编译器,指向我们刚刚安装的 Yocto meta-toolchain-qt编译器路径。下面的截图显示了该路径:

    /opt/poky/1.7.1/sysroots/x86_64-pokysdk-linux/usr/bin/arm- poky-linux-gnueabi/arm-poky-linux-gnueabi-g++
    
    

    如何操作...

  3. 对于交叉调试器,下面是路径,截图中也提到过:

    /opt/poky/1.7.1/sysroots/x86_64-pokysdk-linux/usr/bin/arm- poky-linux-gnueabi/arm-poky-linux-gnueabi-gdb
    
    

    如何操作...

  4. 然后我们通过从工具链中选择qmake构建器来配置 Qt。以下是该路径,截图中也提到过:

    /opt/poky/1.7.1/sysroots/x86_64-pokysdk-linux/usr/bin/qmake
    
    

    如何操作...

  5. 最后,我们按照以下步骤配置一个新的开发套件:

    1. 选择通用 Linux 设备并将其sysroot配置为:

      /opt/poky/1.7.1/sysroots/cortexa9hf-vfp-neon-poky-linux- gnueabi/
      
      
    2. 选择我们刚刚定义的编译器、调试器和 Qt 版本。如何操作...

      提示

      在 Ubuntu 中,Qt Creator 将其配置存储在用户的主目录下的.config/QtProject/目录中。

开发 Qt 应用程序

本教程将展示如何使用 Qt Creator 构建、运行和调试一个图形化的 Qt 应用程序。

准备就绪

在启动 Qt Creator 之前,我们检查是否能够手动构建并启动一个 Qt 应用程序。我们将构建一个 Qt hello world 应用程序。

这里是qt_hello_world.cpp的代码:

#include <QApplication>
#include <QPushButton>

 int main(int argc, char *argv[])
 {
     QApplication app(argc, argv);

     QPushButton hello("Hello world!");

     hello.show();
     return app.exec();
 }

要构建它,我们使用之前描述的meta-toolchain-qt

$ source /opt/poky/1.7.1/environment-setup-cortexa9hf-vfp-neon-poky- linux-gnueabi
$ qmake -project
$ qmake
$ make

这使用qmake来创建一个项目文件和一个Makefile文件,包含文件夹中的所有相关代码文件。

为了运行它,我们首先需要构建一个支持 Qt 的文件系统。我们首先按照以下步骤准备环境:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad

然后我们通过在conf/local.conf中添加以下内容来配置我们的项目,启用qt4-pkgs额外功能:

EXTRA_IMAGE_FEATURES += "qt4-pkgs"

对于 Qt 应用程序,我们还需要国际化组件 UnicodeICU)库,因为 Qt 库是以支持该库进行编译的。

IMAGE_INSTALL_append = " icu"

然后通过以下命令构建:

$ bitbake core-image-sato

完成后,我们可以编程 microSD 卡镜像并启动 Wandboard。将qt_hello_world二进制文件复制到目标设备并运行:

# DISPLAY=:0 qt_hello_world

你应该能在 X11 桌面上看到 Qt hello world 窗口。

如何操作...

按照这些步骤构建并运行一个示例 hello world 应用程序:

  1. 通过转到文件 | 新建文件或项目 | 其他项目 | 空 qmake 项目来创建一个新的空项目。

  2. 仅选择我们刚创建的wandboard-quad开发板套件。如何操作...

  3. 通过转到文件 | 新建文件或项目 | C++ | C++源文件,添加一个新的 C++文件,qt_hello_world.cpp

  4. qt_hello_world.cpp文件的内容粘贴到 Qt Creator 中,如下图所示:如何操作...

  5. 通过将以下内容添加到 hw.pro 文件中,配置你的项目与目标安装的详细信息:

    SOURCES += \
       qt_hello_world.cpp
    
    TARGET =  qt_hello_world
       target.files =  qt_hello_world
       target.path = /
    
    INSTALLS += target
    

    qt_hello_world 替换为你的项目名称。

  6. 构建项目。如果遇到构建错误,检查 Yocto 构建环境是否正确设置。

    提示

    在启动 Qt Creator 之前,你可以尝试手动运行工具链的 environment-setup 脚本。

  7. 转到 项目 | 运行,检查你的项目设置。如何操作...

  8. 如屏幕截图所示,Qt Creator 将使用 SFTP 协议将文件传输到目标设备。默认情况下,core-image-sato 上运行的 dropbear SSH 服务器不支持 SFTP。我们需要通过将 openssh-sftp-server 软件包添加到项目的 conf/local.conf 文件中,将其添加到镜像中,以使 Qt Creator 能够正常工作。

    IMAGE_INSTALL_append =  " openssh-sftp-server"
    

    然而,我们还需要其他工具,例如 gdbserver,如果我们希望调试我们的应用程序,那么添加 eclipse-debug 功能会更方便,它将把所有需要的应用程序添加到目标镜像中。

    EXTRA_IMAGE_FEATURES += "eclipse-debug"
    
  9. 现在,你可以运行项目了。如何操作...

    提示

    如果应用程序在部署时出现登录错误,检查是否已按照之前的配方在目标设备中设置了 root 密码,或者是否正在使用 SSH 密钥认证。

现在,你应该能看到示例 Qt hello world 应用程序在你的 SATO 桌面上运行。

还有更多...

为了调试应用程序,可以在源代码上设置断点并点击 调试 按钮。

还有更多...

描述应用程序开发的工作流

应用程序开发的工作流类似于我们之前在第二章,BSP 层,看到的 U-Boot 和 Linux 内核的工作流。

如何操作...

我们将看到以下开发工作流是如何应用于应用程序开发的:

  • 外部开发

  • 工作目录开发

  • 外部源开发

它是如何工作的...

外部开发

这就是我们在之前通过命令行使用独立工具链构建时,看到的配方中的使用方式,也适用于使用 Eclipse 和 Qt Creator IDE 时的情况。该工作流会生成需要单独复制到硬件上才能运行和调试的二进制文件。它可以与其他工作流结合使用。

工作目录开发

当应用程序通过 Yocto 构建系统构建时,我们使用这个工作流来调试偶发性问题。然而,它并不是长期开发的推荐工作流。但请注意,它通常是调试第三方软件包时的第一步。

我们将使用在第三章,软件层,中的 添加新软件包 配方中看到的 helloworld_1.0.bb 自定义配方 meta-custom/recipes-example/helloworld/helloworld_1.0.bb 作为示例。

DESCRIPTION = "Simple helloworld application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://helloworld.c"

S = "${WORKDIR}"

do_compile() {
             ${CC} helloworld.c -o helloworld
}

do_install() {
             install -d ${D}${bindir}
             install -m 0755 helloworld ${D}${bindir}
}

这里,helloworld.c 源文件如下:

#include <stdio.h>

int main(void)
{
   return printf("Hello World");
}

工作流步骤如下:

  1. 从头开始启动软件包编译。

    $ cd /opt/yocto/fsl-community-bsp/
    $ source setup-environment wandboard-quad
    $ bitbake -c cleanall helloworld
    
    

    这将删除软件包的构建文件夹、共享状态缓存和下载的软件包源。

  2. 启动开发环境:

    $ bitbake -c devshell helloworld
    
    

    这将获取、解压并修补helloworld源代码,并启动一个新的 shell,环境已准备好进行编译。新 shell 将切换到软件包的build目录。

  3. 根据SRC_URI变量,软件包的build目录可能已经受版本控制。如果没有(如本示例所示),我们将按如下方式创建一个本地 Git 版本库:

    $ git init
    $ git add helloworld.c
    $ git commit -s -m "Original revision"
    
    
  4. 执行我们需要的修改;例如,将helloworld.c更改为如下内容,以打印Howdy world

    #include <stdio.h>
    
    int main(void)
    {
     return printf("Howdy World");
    }
    
    
  5. 退出devshell并构建软件包,而不删除我们的修改。

    $ bitbake -C compile helloworld
    
    

    提示

    注意大写的C(它触发编译任务),以及所有跟随它的任务。

  6. 在硬件上测试您的更改,方法是复制生成的软件包并安装它。因为您只修改了一个软件包,其他依赖项应该已经安装在运行中的根文件系统中。运行以下命令:

    $ bitbake -e helloworld | grep ^WORKDIR=
    WORKDIR="/opt/yocto/fsl-community-bsp/wandboard- quad/tmp/work/cortexa9hf-vfp-neon-poky-linux- gnueabi/helloworld/1.0-r0"
    $ scp ${WORKDIR_PATH}/deploy-rpms/deploy- rpms/cortexa9hf_vfp_neon/helloworld-1.0- r0.cortexa9hf_vfp_neon.rpm root@<target_ip_address>:/
    $ rpm -i /helloworld-1.0-r0.cortexa9hf_vfp_neon.rpm
    
    

    这假设目标的根文件系统已通过package-management功能构建,并且在使用rm_work类时,helloworld软件包已添加到RM_WORK_EXCLUDE变量中。

  7. 返回devshell并将更改提交到本地 Git 版本库,如下所示:

    $ bitbake -c devshell helloworld
    $ git add  helloworld.c
    $ git commit -s -m "Change greeting message"
    
    
  8. 生成补丁到配方的补丁目录中:

    $ git format-patch -1 -o /opt/yocto/fsl-community- bsp/sources/meta-custom/recipes- example/helloworld/helloworld-1.0
    
    
  9. 最后,将补丁添加到配方的SRC_URI变量中,如下所示:

    SRC_URI  =  "file://helloworld.c \
               file://0001-Change-greeting-message.patch"
    

外部源开发

这种工作流建议在将应用程序集成到 Yocto 构建系统后用于开发工作。例如,可以与使用 IDE 进行的外部开发结合使用。

在我们之前看到的示例配方中,源文件与元数据一起放置在meta-custom层中。

更常见的是让配方直接从版本控制系统(如 Git)中获取,因此我们将修改meta-custom/recipes-example/helloworld/helloworld_1.0.bb文件,以从 Git 目录中获取,如下所示:

DESCRIPTION = "Simple helloworld application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "git://github.com/yoctocookbook/helloworld"

S = "${WORKDIR}/git"

do_compile() {
             ${CC} helloworld.c -o helloworld
}

do_install() {
             install -d ${D}${bindir}
             install -m 0755 helloworld ${D}${bindir}
}

然后我们可以将其克隆到本地目录,如下所示:

$ cd /opt/yocto/
$ git clone git://github.com/yoctocookbook/helloworld

使用本地版本控制库是使用远程版本控制库的替代方法。为此,请按照以下步骤操作:

  1. 创建一个本地 Git 版本库,用于存放源代码:

    $ mkdir -p /opt/yocto/helloworld
    $ cd /opt/yocto/helloworld
    $ git init
    
    
  2. 将我们的helloworld.c文件复制到这里,并将其添加到版本库中:

    $ git add helloworld.c
    
    
  3. 最后,使用签名和消息提交:

    $ git commit -s -m "Original revision"
    
    

无论如何,我们都将版本控制的源代码放在本地目录中。然后我们将配置我们的conf/local.conf文件以从中工作,如下所示:

INHERIT += "externalsrc"
EXTERNALSRC_pn-helloworld = "/opt/yocto/helloworld"
EXTERNALSRC_BUILD_pn-helloworld = "/opt/yocto/helloworld"

然后使用以下命令进行构建:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake helloworld

然后我们可以直接在本地文件夹中工作,而无需担心 BitBake 意外删除我们的代码。开发完成后,conf/local.conf的修改将被删除,配方将从其原始SRC_URI位置获取源代码。

使用 GNU make 工作

GNU make 是 Linux 系统上的一种 make 实现。它被许多开源项目使用,包括 Linux 内核。构建由Makefile管理,Makefile指示 make 如何构建源代码。

如何操作...

Yocto 配方继承base.bbclass,因此它们的默认行为是查找MakefilemakefileGNU Makefile并使用 GNU make 来构建软件包。

如果你的软件包已经包含Makefile,那么你只需要关注需要传递给 make 的参数。make 的参数可以通过EXTRA_OEMAKE变量传递,且需要提供一个调用oe_runmake安装的do_install重写,否则将执行空安装。

例如,logrotate配方基于一个Makefile,如下所示:

SUMMARY = "Rotates, compresses, removes and mails system log files"
SECTION = "console/utils"
HOMEPAGE = "https://fedorahosted.org/logrotate/"
LICENSE = "GPLv2"

DEPENDS="coreutils popt"

LIC_FILES_CHKSUM = "file://COPYING;md5=18810669f13b87348459e611d31ab760"

SRC_URI = "https://fedorahosted.org/releases/l/o/logrotate/logrotate- ${PV}.tar.gz \"
SRC_URI[md5sum] = "99e08503ef24c3e2e3ff74cc5f3be213"
SRC_URI[sha256sum] = "f6ba691f40e30e640efa2752c1f9499a3f9738257660994de70a45fe00d12b64"

EXTRA_OEMAKE = ""

do_install(){
    oe_runmake install DESTDIR=${D} PREFIX=${D} MANDIR=${mandir}
    mkdir -p ${D}${sysconfdir}/logrotate.d
    mkdir -p ${D}${sysconfdir}/cron.daily
    mkdir -p ${D}${localstatedir}/lib
    install -p -m 644 examples/logrotate-default ${D}${sysconfdir}/logrotate.conf
    install -p -m 755 examples/logrotate.cron ${D}${sysconfdir}/cron.daily/logrotate
    touch ${D}${localstatedir}/lib/logrotate.status
}

另见

与 GNU 构建系统的合作

当你总是在相同系统上构建和运行软件时,Makefile是一个不错的解决方案,前提是已知glibcgcc版本以及可用的库版本。然而,大多数软件需要在不同系统上构建和运行。

准备工作

GNU 构建系统或autotools是一组工具,旨在为你的软件创建适用于各种系统的Makefile。它由三个主要工具组成:

  • autoconf:解析configure.ac文件的内容,该文件描述了要构建的源代码,并生成一个configure脚本。然后,这个脚本将用于生成最终的Makefile

  • automake:用于解析Makefile.am文件的内容,并将其转换为Makefile.in文件。然后,早先生成的configure脚本将使用该文件来生成config.status脚本,自动执行该脚本以获得最终的Makefile

  • libtools:用于管理静态和动态库的创建。

如何操作...

Yocto 构建系统包含了构建autotools软件包所需的类。你的配方只需要继承autotools类,并配置要传递给configure脚本的参数,这些参数存储在EXTRA_OECONF变量中。通常,autotools系统知道如何安装软件,因此你不需要重写do_install

有许多开源项目使用autotools作为构建系统。

一个示例,meta-custom/recipes-example/hello/hello_2.9.bb,不需要任何额外的配置选项,如下所示:

DESCRIPTION = "GNU helloworld autotools recipe"
SECTION = "examples"

LICENSE = "GPLv3"
LIC_FILES_CHKSUM = "file://${COREBASE}/meta/files/common- licenses/GPL-3.0;md5=c79ff39f19dfec6d293b95dea7b07891"

SRC_URI = "${GNU_MIRROR}/hello/hello-${PV}.tar.gz"
SRC_URI[md5sum] = "67607d2616a0faaf5bc94c59dca7c3cb"
SRC_URI[sha256sum] = "ecbb7a2214196c57ff9340aa71458e1559abd38f6d8d169666846935df191ea7"

inherit autotools gettext

另见

与 CMake 构建系统的合作

GNU make 系统在你仅为 Linux 系统构建时是一个很棒的工具。然而,一些软件包是跨平台的,需要一种方式来管理不同操作系统上的 Makefile 文件。CMake 是一个跨平台的构建系统,它不仅可以与 GNU make 配合使用,还可以与微软 Visual Studio 和苹果的 Xcode 配合使用。

准备工作

CMake 工具解析每个目录中的 CMakeLists.txt 文件,以控制构建过程。以下是编译 hello world 示例的 CMakeLists.txt 文件示例:

cmake_minimum_required(VERSION 2.8.10)
project(helloworld)
add_executable(helloworld helloworld.c)
install(TARGETS helloworld RUNTIME DESTINATION bin)

如何实现...

Yocto 构建系统还包含了用于构建 CMake 包的必需类。你的配方只需继承 cmake 类并配置传递给 configure 脚本的参数,这些参数存储在 EXTRA_OECMAKE 变量中。通常,CMake 系统知道如何安装软件,因此你不需要覆盖 do_install 任务。

一个示例配方,用于构建 helloworld.C 示例应用程序,meta-custom/recipes-example/helloworld-cmake/helloworld-cmake_1.0.bb,如下所示:

DESCRIPTION = "Simple helloworld cmake application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://CMakeLists.txt \
           file://helloworld.c"
S = "${WORKDIR}"

inherit cmake

EXTRA_OECMAKE = ""

另见

使用 SCons 构建器

SCons 也是一个跨平台的构建系统,使用 Python 编写,配置文件也是用同样的语言编写。它还支持微软 Visual Studio 等其他功能。

准备工作

SCons 解析 SConstruct 文件,并且默认情况下不会将环境变量传递给构建系统。这是为了避免由于环境差异而引发的构建问题。这对于 Yocto 来说是一个复杂的地方,因为它使用交叉编译工具链设置来配置环境。

SCons 没有定义支持交叉编译的标准方式,因此每个项目都会有所不同。以简单的 hello world 程序为例,我们可以像下面这样从外部环境初始化 CCPATH 变量:

import os
env = Environment(CC = os.environ['CC'],
                  ENV = {'PATH': os.environ['PATH']})
env.Program("helloworld", "helloworld.c")

如何实现...

Yocto 构建系统还包含了用于构建 SCons 包的必需类。你的配方只需继承 SCons 类并配置传递给配置脚本的参数,这些参数存储在 EXTRA_OESCONS 变量中。尽管一些使用 SCons 的软件包可能会通过 SCons 类要求的安装别名处理安装,但你的配方大多需要提供 do_install 任务的覆盖。

一个示例配方,用于构建 helloworld.c 示例应用程序,meta-custom/recipes-example/helloworld-scons/helloworld-scons_1.0.bb,如下所示:

DESCRIPTION = "Simple helloworld scons application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://SConstruct \
           file://helloworld.c"

S = "${WORKDIR}"

inherit scons

EXTRA_OESCONS = ""

do_install() {
    install -d ${D}/${bindir}
    install -m 0755 helloworld ${D}${bindir}
}

另见

使用库开发

大多数应用程序使用共享库,这可以节省系统内存和磁盘空间,因为它们在不同的应用程序之间共享。将代码模块化成库还可以更容易地进行版本控制和代码管理。

本教程将解释如何在 Linux 和 Yocto 中处理静态库和共享库。

准备工作

按照惯例,库文件以lib前缀开头。

基本上有两种库类型:

  • 静态库.a):当目标代码被链接并成为应用程序的一部分时

  • 动态库.so):在编译时链接,但不包含在应用程序中,因此它们需要在运行时可用。多个应用程序可以共享动态库,从而减少磁盘空间占用。

库文件放置在以下标准根文件系统位置:

  • /lib:启动时所需的库文件

  • /usr/lib:大多数系统库

  • /usr/local/lib:非系统库

动态库遵循运行系统中的某些命名约定,以便多个版本可以共存,因此库可以通过不同的名称引用。以下是一些解释:

  • 链接器的名称以.so结尾;例如,libexample.so

  • 完全限定名或soname,是指向库名称的符号链接。例如,libexample.so.x,其中x是版本号。增加版本号意味着该库与之前的版本不兼容。

  • 真正的名称。例如,libexample.so.x.y[.z],其中x是主版本号,y是次版本号,z(可选)是发布号。增加次版本号或发布号将保持兼容性。

在 GNU glibc中,启动 ELF 二进制文件时会调用程序加载器/lib/ld-linux-X。其中,X是版本号,加载器会找到所有需要的共享库。这个过程使用了几个有趣的文件:

  • /etc/ld.so.conf:存储加载器搜索的目录

  • /etc/ld.so.preload:用于覆盖库文件

ldconfig工具读取ld.so.conf文件并创建一个缓存文件(/etc/ld.so.cache)以提高访问速度。

以下环境变量也可能有帮助:

  • LD_LIBRARY_PATH:这是一个冒号分隔的目录列表,用于搜索库文件。它在调试或使用非标准库位置时非常有用。

  • LD_PRELOAD:用于覆盖共享库。

构建静态库

我们将从两个源文件hello.cworld.c构建一个静态库libhelloworld,并用它来构建一个 Hello World 应用程序。库的源文件如下所示。

以下是hello.c文件的代码:

char * hello (void)
{
  return "Hello";
}

这是world.c文件的代码:

char * world (void)
{
  return "World";
}

构建库时,按照以下步骤操作:

  1. 配置构建环境:

    $ source /opt/poky/1.7.1/environment-setup-cortexa9hf-vfp- neon-poky-linux-gnueabi
    
    
  2. 编译并链接库:

    ${CC} -c hello.c world.c
    ${AR} -cvq libhelloworld.a hello.o world.o
    
    
  3. 验证库的内容:

    ${AR} -t libhelloworld.a
    
    

接下来展示应用程序源代码。

  • 对于helloworld.c文件,以下是代码:

    #include <stdio.h>
    int main (void)
    {
      return printf("%s %s\n",hello(),world());
    }
    
  • 要构建它,我们运行:

    ${CC} -o helloworld helloworld.c libhelloworld.a
    
    
  • 我们可以使用readelf检查它链接的库:

    $ readelf -d helloworld
    Dynamic section at offset 0x534 contains 24 entries:
     Tag        Type                         Name/Value
     0x00000001 (NEEDED)                     Shared library: [libc.so.6]
    
    

构建共享动态库

要从相同的源构建动态库,我们可以运行:

${CC} -fPIC -g -c hello.c world.c
${CC} -shared -Wl,-soname,libhelloworld.so.1 -o libhelloworld.so.1.0 hello.o world.o

我们可以用它来构建我们的helloworld C应用程序,具体如下:

${CC} helloworld.c libhelloworld.so.1.0 -o helloworld

再次,我们可以使用readelf检查动态库,如下所示:

$ readelf -d helloworld
Dynamic section at offset 0x6ec contains 25 entries:
 Tag        Type                         Name/Value
 0x00000001 (NEEDED)                     Shared library: [libhelloworld.so.1]
 0x00000001 (NEEDED)                     Shared library: [libc.so.6]

如何操作...

以下是我们刚才看到的静态库示例的配方,meta-custom/recipes-example/libhelloworld-static/libhelloworldstatic_1.0.bb

DESCRIPTION = "Simple helloworld example static library"
SECTION = "libs"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://hello.c \
           file://world.c \
           file://helloworld.pc"
S = "${WORKDIR}"

do_compile() {
        ${CC} -c hello.c world.c
        ${AR} -cvq libhelloworld.a hello.o world.o
}

do_install() {
        install -d ${D}${libdir}
        install -m 0755 libhelloworld.a ${D}${libdir}
}

默认情况下,meta/conf/bitbake.conf中的配置将所有静态库放入-staticdev包中。它也被放置在sysroot中,以便可以使用。

对于动态库,我们将使用以下配方,meta-custom/recipes-example/libhelloworld-dyn/libhelloworlddyn_1.0.bb

meta-custom/recipes-example/libhelloworld-dyn/libhelloworlddyn_1.0.bb
DESCRIPTION = "Simple helloworld example dynamic library"
SECTION = "libs"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://hello.c \
           file://world.c \
           file://helloworld.pc"

S = "${WORKDIR}"

do_compile() {
       ${CC} -fPIC -g -c hello.c world.c
       ${CC} -shared -Wl,-soname,libhelloworld.so.1 -o libhelloworld.so.1.0 hello.o world.o
}

do_install() {
       install -d ${D}${libdir}
       install -m 0755 libhelloworld.so.1.0 ${D}${libdir}
       ln -s libhelloworld.so.1.0 ${D}/${libdir}/libhelloworld.so.1
       ln -s libhelloworld.so.1 ${D}/${libdir}/libhelloworld.so
}

通常我们会在RDEPENDS变量中列出库的依赖项(如果有的话),但这并不总是必要的,因为构建系统会通过检查库文件和pkg-config文件自动执行一些依赖检查,并将其发现的依赖项自动添加到RDEPENDS中。

同一库的多个版本可以在运行系统中共存。为此,您需要提供具有相同包名但不同包修订版的不同配方。例如,我们将有libhelloworld-1.0.bblibhelloworld-1.1.bb

为了使用静态库构建应用程序,我们将创建一个配方meta-custom/recipes-example/helloworld-static/helloworldstatic_1.0.bb,如下所示:

DESCRIPTION = "Simple helloworld example"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

DEPENDS = "libhelloworld-static"

SRC_URI = "file://helloworld.c"

S = "${WORKDIR}"

do_compile() {
        ${CC} -o helloworld helloworld.c ${STAGING_LIBDIR}/libhelloworld.a
}

do_install() {
        install -d ${D}${bindir}
        install -m 0755 helloworld ${D}${bindir}
}

若要使用动态库进行构建,我们只需要在meta-custom/recipes-example/helloworld-shared/helloworldshared_1.0.bb中更改配方,改为meta-custom/recipes-example/helloworld-shared/helloworldshared_1.0.bb

meta-custom/recipes-example/helloworld-shared/helloworldshared_1.0.bb
DESCRIPTION = "Simple helloworld example"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

DEPENDS = "libhelloworld-dyn"

SRC_URI = "file://helloworld.c"

S = "${WORKDIR}"

do_compile() {
        ${CC} -o helloworld helloworld.c -lhelloworld
}

do_install() {
        install -d ${D}${bindir}
        install -m 0755 helloworld ${D}${bindir}
}

如何工作...

库应该提供使用它们所需的信息,如include头文件和library依赖项。Yocto 项目为库提供构建设置提供了两种方式:

  • binconfig类。这是一个遗留类,用于那些提供-config脚本来提供构建设置的库。

  • pkgconfig类。这是库提供构建设置的推荐方法。

一个pkg-config构建设置文件具有.pc后缀,随库一起分发,并安装在pkg-config工具已知的常见位置。

动态库的helloworld.pc文件如下所示:

prefix=/usr/local
exec_prefix=${prefix}
includedir=${prefix}/include
libdir=${exec_prefix}/lib

Name: helloworld
Description: The helloworld library
Version: 1.0.0
Cflags: -I${includedir}/helloworld
Libs: -L${libdir} -lhelloworld

然而,对于静态库,我们需要将最后一行更改为:

Libs: -L${libdir} libhelloworld.a

一个想要使用这个.pc文件的包将继承pkgconfig类。

还有更多...

对于既构建库又构建可执行文件但不希望它们一起安装的包,提供了相应的解决方案。通过继承lib_package类,包将创建一个独立的-bin包来包含可执行文件。

另请参见

与 Linux 帧缓冲区一起工作

Linux 内核提供了一个图形硬件抽象层,以帧缓冲设备的形式呈现。它们允许应用程序通过一个明确定义的 API 访问图形硬件。帧缓冲区还被用来为 Linux 内核提供一个图形控制台,例如,它可以显示颜色和徽标。

在本食谱中,我们将探讨应用程序如何使用 Linux 帧缓冲区来显示图形和视频。

准备工作

一些应用程序,特别是在嵌入式设备中,能够通过映射内存并直接访问帧缓冲区来访问它。例如,gstreamer框架能够直接在帧缓冲区上工作,Qt 图形框架也是如此。

Qt 是一个跨平台的应用程序框架,使用 C++编写,由 Digia 公司(以 Qt 公司名义)和开源 Qt 项目社区共同开发。

对于 Qt 应用程序,Poky 提供了qt4e-demo-image,而 FSL 社区 BSP 提供了qte-in-use-image,这两个镜像都包括对 Qt4 扩展在帧缓冲区上的支持。提供的框架还包括硬件加速支持——不仅支持视频,还支持通过 OpenGL 和 OpenVG API 提供的 2D 和 3D 图形加速。

如何操作...

要编译我们之前在开发 Qt 应用程序食谱中看到的 Qt Hello World 应用程序,我们可以使用以下meta-custom/recipes-qt/qt-helloworld/qt-helloworld_1.0.bb Yocto 食谱:

DESCRIPTION = "Simple QT helloworld example"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

RDEPENDS_${PN} += "icu"

SRC_URI = "file://qt_hello_world.cpp \
           file://qt_hello_world.pro"

S = "${WORKDIR}"

inherit qt4e

do_install() {
         install -d ${D}${bindir}
         install -m 0755 qt_hello_world ${D}${bindir}
}

这里是meta-custom/recipes-qt/qt-helloworld/qt-helloworld-1.0/qt_hello_world.cpp源文件:

#include <QApplication>
#include <QPushButton>

 int main(int argc, char *argv[])
 {
     QApplication app(argc, argv);

     QPushButton hello("Hello world!");

     hello.show();
     return app.exec();
 }

另外,meta-custom/recipes-qt/qt-helloworld/qt-helloworld-1.0/qt_hello_world.pro项目文件如下:

SOURCES += \
   qt_hello_world.cpp

然后我们通过在项目的conf/local.conf文件中使用以下内容将其添加到镜像中:

IMAGE_INSTALL_append = " qt-helloworld"

然后我们使用以下命令构建镜像:

$ bitbake qt4e-demo-image

然后我们可以将 SD 卡镜像编程,启动它,登录到 Wandboard,并通过运行以下命令启动应用程序:

# qt_hello_world -qws

运行服务器应用程序时需要-qws命令行选项。

它是如何工作的...

帧缓冲设备位于/dev目录下。默认的帧缓冲设备是/dev/fb0,如果图形硬件提供多个帧缓冲设备,它们将按顺序编号。

默认情况下,Wandboard 启动时会有两个帧缓冲设备,fb0fb1。第一个是默认的视频显示,第二个是叠加平面,可用于在显示器上组合内容。

然而,i.MX6 SoC 支持最多四个显示器,因此它除了两个叠加帧缓冲区外,还可以有最多四个帧缓冲设备。

你可以通过FRAMEBUFFER环境变量更改应用程序使用的默认帧缓冲区。例如,如果你的硬件支持多个帧缓冲区,你可以通过运行以下命令使用第二个帧缓冲区:

# export FRAMEBUFFER=/dev/fb1

帧缓冲设备是内存映射的,你可以在它们上执行文件操作。例如,你可以通过运行以下命令清除屏幕内容:

# cat /dev/zero > /dev/fb0

或者使用以下命令复制它:

# cat /dev/fb0 > fb.raw

你甚至可以通过以下命令恢复内容:

# cat fb.raw > /dev/fb0

用户空间程序也可以通过 ioctls 查询帧缓冲区或以编程方式修改其配置,或者通过控制台使用 fbset 应用程序,这个程序包含在 Yocto 的核心镜像中,作为 BusyBox 小程序。

# fbset -fb /dev/fb0
mode "1920x1080-60"
 # D: 148.500 MHz, H: 67.500 kHz, V: 60.000 Hz
 geometry 1920 1080 1920 1080 24
 timings 6734 148 88 36 4 44 5
 accel false
 rgba 8/16,8/8,8/0,0/0
endmode

你可以通过从 U-Boot 引导加载程序传递 video 命令行选项到 Linux 内核,来配置帧缓冲 HDMI 设备的特定分辨率、每像素位数和刷新率。具体格式取决于设备的帧缓冲驱动,对于 Wandboard,格式如下:

video=mxcfbn:dev=hdmi,<xres>x<yres>M[@rate]

其中:

  • n 是帧缓冲区的编号

  • xres 是水平分辨率

  • yres 是垂直分辨率

  • M 表示要使用 VESA 协调视频时序来计算时序,而不是从查找表中获取。

  • rate 是刷新率

例如,对于 fb0 帧缓冲区,你可以使用:

video=mxcfb0:dev=hdmi,1920x1080M@60

提示

请注意,在一段时间的不活动后,虚拟控制台将会消失。要恢复显示,可以使用:

# echo 0 > /sys/class/graphics/fb0/blank

还有更多...

FSL 社区 BSP 层还提供了一个 fsl-image-multimedia 目标镜像,包含 gstreamer 框架和利用 i.MX6 SoC 内硬件加速特性的插件。此外,还提供了一个 fsl-image-multimedia-full 镜像,扩展了对更多 gstreamer 插件的支持。

要构建带有帧缓冲区支持的 fsl-image-multimedia 镜像,你需要通过将以下内容添加到 conf/local.conf 文件来移除图形分发特性:

DISTRO_FEATURES_remove = "x11 directfb wayland"

然后用以下命令构建镜像:

$ bitbake fsl-image-multimedia

生成的 fsl-image-multimedia-wandboard-quad.sdcard 镜像位于 tmp/deploy/images,可以将其写入 microSD 卡并启动。

默认的 Wandboard 设备树定义了一个 mxcfb1 节点,如下所示:

       mxcfb1: fb@0 {
                compatible = "fsl,mxc_sdc_fb";
                disp_dev = "hdmi";
                interface_pix_fmt = "RGB24";
                mode_str ="1920x1080M@60";
                default_bpp = <24>;
                int_clk = <0>;
                late_init = <0>;
        };

因此,连接一个 1920x1080 的 HDMI 显示器应该会显示一个带有 Poky 登录提示符的虚拟终端。

然后,我们可以使用 gstreamer 命令行工具 gst-launch 来构建 gstreamer 管道。例如,要在帧缓冲区上查看硬件加速的视频,你可以下载 Big Bunny 预告片的全高清文件,并通过 gstreamer 框架的 gst-launch 命令行工具播放该视频,命令如下:

# cd /home/root
# wget http://video.blendertestbuilds.de/download.blender.org/peach/trailer_ 1080p.mov
# gst-launch playbin2 uri=file:///home/root/trailer_1080p.mov

视频将使用 Freescale 的 h.264 视频解码插件 vpudec,它利用 i.MX6 SoC 中的硬件视频处理单元来解码 h.264 视频。

你可以通过运行以下命令来查看可用的 i.MX6 特定插件列表:

# gst-inspect | grep imx
h264.imx:  mfw_h264decoder: h264 video decoder
audiopeq.imx:  mfw_audio_pp: audio post equalizer
aiur.imx: webm: webm
aiur.imx:  aiurdemux: aiur universal demuxer
mpeg2dec.imx:  mfw_mpeg2decoder: mpeg2 video decoder
tvsrc.imx:  tvsrc: v4l2 based tv src
ipucsc.imx:  mfw_ipucsc: IPU-based video converter
mpeg4dec.imx:  mfw_mpeg4aspdecoder: mpeg4 video decoder
vpu.imx:  vpudec: VPU-based video decoder
vpu.imx:  vpuenc: VPU-based video encoder
mp3enc.imx:  mfw_mp3encoder: mp3 audio encoder
beep.imx: ac3: ac3
beep.imx: 3ca: ac3
beep.imx:  beepdec: beep audio decoder
beep.imx:  beepdec.vorbis: Vorbis decoder
beep.imx:  beepdec.mp3: MP3 decoder
beep.imx:  beepdec.aac: AAC LC decoder
isink.imx:  mfw_isink: IPU-based video sink
v4lsink.imx:  mfw_v4lsink: v4l2 video sink
v4lsrc.imx:  mfw_v4lsrc: v4l2 based camera src
amrdec.imx:  mfw_amrdecoder: amr audio decoder

参见

使用 X Windows 系统

X Windows 系统为 GUI 环境提供了框架——例如在显示器上绘制和移动窗口,以及与鼠标、键盘和触摸屏等输入设备进行交互。其协议版本已经有二十多年历史,因此也被称为 X11。

准备工作

X Windows 系统的参考实现是X.Org服务器,发布采用 MIT 等宽松许可协议。它使用客户端/服务器模型,服务器与多个客户端程序进行通信,处理用户输入并接受图形输出。X11 协议具有网络透明性,意味着客户端和服务器可以运行在不同的机器上,拥有不同的架构和操作系统。然而,大多数情况下,它们都运行在同一台机器上,并通过本地套接字进行通信。

X11 中未定义用户界面规范(如按钮或菜单样式),这将由其他窗口管理器应用程序定义,这些窗口管理器通常是桌面环境的一部分,如 Gnome 或 KDE。

X11 具有输入和视频驱动程序来处理硬件。例如,它有一个帧缓冲驱动程序fbdev,可以输出到非加速的 Linux 帧缓冲区;还有evdev,一个通用的 Linux 输入设备驱动程序,支持鼠标、键盘、平板和触摸屏。

X11 Windows 系统的设计使其对嵌入式设备来说较为繁重,尽管像四核 i.MX6 这样强大的设备使用时没有问题,许多嵌入式设备还是选择其他图形替代方案。然而,许多图形应用程序,主要来自桌面环境,仍然运行在 X11 Windows 系统上。

FSL 社区的 BSP 层为 i.MX6 SoC 提供了一个硬件加速的 X 视频驱动程序xf86-video-imxfb-vivante,该驱动程序包含在基于 X11 的core-image-sato目标镜像和其他图形镜像中。

X 服务器通过/etc/X11/xorg.conf文件进行配置,该文件将加速设备配置如下:

Section "Device"
    Identifier  "i.MX Accelerated Framebuffer Device"
    Driver      "vivante"
    Option      "fbdev"     "/dev/fb0"
    Option      "vivante_fbdev" "/dev/fb0"
    Option      "HWcursor"  "false"
EndSection

图形加速由 i.MX6 SoC 中包含的 Vivante GPU 提供。

不建议进行低级 X11 开发,推荐使用 GTK+和 Qt 等工具包。我们将看到如何将这两种类型的图形应用程序集成到我们的 Yocto 目标镜像中。

如何做……

SATO 是基于Gnome Mobile and EmbeddedGMAE)的 Poky 发行版的默认视觉样式。它是一个基于 GTK+的桌面环境,使用 matchbox-window-manager。其特点是一次只显示一个全屏窗口。

要构建我们之前介绍的 GTK Hello World 应用程序meta-custom/recipes-graphics/gtk-helloworld/gtk-helloworld-1.0/gtk_hello_world.c,可以按照如下方式进行:

#include <gtk/gtk.h>

int main(int argc, char *argv[])
{
    GtkWidget *window;
    gtk_init (&argc, &argv);
    window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
    gtk_widget_show (window);
    gtk_main ();
    return 0;
}

我们可以使用以下meta-custom/recipes-graphics/gtk-helloworld/gtk-helloworld_1.0.bb配方:

DESCRIPTION = "Simple GTK helloworld application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://gtk_hello_world.c"

S = "${WORKDIR}"

DEPENDS = "gtk+"

inherit pkgconfig

do_compile() {
    ${CC} gtk_hello_world.c -o helloworld `pkg-config --cflags -- libs gtk+-2.0`
}

do_install() {
    install -d ${D}${bindir}
    install -m 0755 helloworld ${D}${bindir}
}

然后,我们可以通过以下命令将该软件包添加到core-image-sato镜像中:

IMAGE_INSTALL_append = " gtk-helloworld"

我们可以通过以下命令从串口终端构建、编程并运行应用程序:

# export DISPLAY=:0
# helloworld

还有更多……

Qt 框架也支持加速图形输出,可以直接在帧缓冲区上实现(就像我们之前看到的qt4e-demo-image目标中那样),或者使用core-image-sato中提供的 X11 服务器。

要在前一个示例中介绍的 Qt hello world 源码上构建 X11,我们可以使用以下 Yocto 配方meta-custom/recipes-qt/qtx11-helloworld/qtx11-helloworld_1.0.bb

DESCRIPTION = "Simple QT over X11 helloworld example"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

RDEPENDS_${PN} += "icu"

SRC_URI = "file://qt_hello_world.cpp \
           file://qt_hello_world.pro"

S = "${WORKDIR}"

inherit qt4x11

do_install() {
         install -d ${D}${bindir}
         install -m 0755 qt_hello_world ${D}${bindir}
}

然后我们还需要将 Qt4 框架添加到目标图像以及应用程序。

EXTRA_IMAGE_FEATURES += "qt4-pkgs"
IMAGE_INSTALL_append = " qtx11-helloworld"

然后我们可以使用以下命令构建core-image-sato

$ bitbake core-image-sato

程序和启动我们的目标。然后使用以下命令运行应用程序:

# export DISPLAY=:0
# qt_hello_world

另请参阅

  • 更多关于 X.Org 服务器的信息,请访问www.x.org

  • Qt 应用程序框架的文档可以在qt-project.org/找到

  • 更多关于 GTK+的信息和文档,请访问www.gtk.org/

使用 Wayland

Wayland 是一个显示服务器协议,旨在取代 X Window 系统,其采用 MIT 许可证。

本篇将概述 Wayland,包括与 X Window 系统的一些关键区别,并展示如何在 Yocto 中使用它。

准备工作

Wayland 协议采用客户端/服务器模型,其中客户端是请求在屏幕上显示像素缓冲区的图形应用程序,服务器或合成器是控制这些缓冲区显示的服务提供者。

Wayland 合成器可以是 Linux 显示服务器、X 应用程序或特殊的 Wayland 客户端。Weston 是 Wayland 项目中的参考合成器。它用 C 语言编写,并与 Linux 内核 API 一起工作。它依赖于evdev来处理输入事件。

Wayland 在 Linux 内核中使用Direct Rendering Manager (DRM),不需要像 X 服务器那样的东西。客户端通过自身的渲染库或类似 Qt 或 GTK+的引擎将窗口内容渲染到与合成器共享的缓冲区中。

Wayland 缺乏 X 的网络透明特性,但未来可能会添加类似功能。

它还比 X 具有更好的安全功能,并设计为提供保密性和完整性。Wayland 不允许应用程序查看其他程序的输入、捕获其他输入事件或生成虚假输入事件。它还更好地保护窗口输出。然而,这也意味着它目前无法提供桌面 X 系统中我们习惯的某些功能,如屏幕捕获或辅助功能程序中常见的功能。

比 X.Org 更轻量且更安全,Wayland 更适合在嵌入式系统中使用。如果需要,X.Org 可以作为 Wayland 的客户端以实现向后兼容性。

然而,Wayland 并没有像 X11 那样成熟,而且 Poky 中基于 Wayland 的图像没有像基于 X11 的那些得到那么多社区关注。

如何操作...

Poky 提供了一个包含 Weston 合成器的core-image-weston镜像。

将我们的 GTK Hello World 示例从使用 X Windows 系统的配方修改为使用 GTK3 并用 Weston 运行是直接的。

DESCRIPTION = "Simple GTK3 helloworld application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://gtk_hello_world.c"

S = "${WORKDIR}"

DEPENDS = "gtk+3"

inherit pkgconfig

do_compile() {
    ${CC} gtk_hello_world.c -o helloworld `pkg-config --cflags -- libs gtk+-3.0`
}

do_install() {
    install -d ${D}${bindir}
    install -m 0755 helloworld ${D}${bindir}
}

为了构建它,配置您的conf/local.conf文件,通过如下方式删除x11发行版特性:

DISTRO_FEATURES_remove = "x11"

注意

当更改DISTRO_FEATURES变量时,您需要从头开始构建,删除tmpsstate-cache目录。

使用以下命令将应用程序添加到镜像中:

IMAGE_INSTALL_append = " gtk3-helloworld"

然后使用以下命令构建镜像:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake core-image-weston

一旦构建完成,您将在tmp/deploy/images/wandboard-quad下找到准备好编程的 microSD 卡镜像。

然后,您可以通过运行以下命令启动该应用程序:

# export XDG_RUNTIME_DIR=/var/run/user/root
# helloworld

还有更多内容...

FSL 社区的 BSP 发布版支持在 Wayland 中使用 i.MX6 SoC 中包含的 Vivante GPU 进行硬件加速图形处理。

这意味着像gstreamer这样的应用程序在与 Weston 合成器一起运行时,将能够提供硬件加速的输出。

Wayland 支持也可以在像 Clutter 和 GTK3+这样的图形工具包中找到。

另见

添加 Python 应用程序

在 Yocto 1.7 中,Poky 支持构建 Python 2 和 Python 3 应用程序,并在meta/recipes-devtools/python目录中包含了一小套 Python 开发工具。

meta-python层中提供了更多种类的 Python 应用程序,该层作为meta-openembedded的一部分,您可以将其添加到conf/bblayers.conf文件中。

准备就绪

打包 Python 模块的标准工具是distutils,它同时适用于 Python 2 和 Python 3。Poky 包括distutils类(Python 3 中的distutils3),用于构建使用distutils的 Python 包。meta-python中有一个使用distutils类的示例配方:meta-python/recipes-devtools/python/python-pyusb_1.0.0a2.bb

SUMMARY = "PyUSB provides USB access on the Python language"
HOMEPAGE = "http://pyusb.sourceforge.net/"
SECTION = "devel/python"
LICENSE = "BSD"
LIC_FILES_CHKSUM = "file://LICENSE;md5=a53a9c39efcfb812e2464af14afab013"
DEPENDS = "libusb1"
PR = "r1"

SRC_URI = "\
    ${SOURCEFORGE_MIRROR}/pyusb/${SRCNAME}-${PV}.tar.gz \
"
SRC_URI[md5sum] = "9136b3dc019272c62a5b6d4eb624f89f"
SRC_URI[sha256sum] = "dacbf7d568c0bb09a974d56da66d165351f1ba3c4d5169ab5b734266623e1736"

SRCNAME = "pyusb"
S = "${WORKDIR}/${SRCNAME}-${PV}"

inherit distutils

然而,distutils不安装包依赖项,不允许卸载包,也不允许安装同一包的多个版本,因此仅推荐用于简单需求。因此,setuptools被开发出来以扩展distutils。它不包含在标准的 Python 库中,但在 Poky 中是可用的。Poky 中也有一个setuptools类(Python 3 中是setuptools3),用于构建与setuptools一起分发的 Python 包。

如何操作...

为了使用setuptools构建 Python Hello World 示例应用程序,我们可以使用以下 Yocto meta-custom/recipes-python/python-helloworld/pythonhelloworld_1.0.bb配方:

DESCRIPTION = "Simple Python setuptools hello world application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://setup.py \
      file://python-helloworld.py \
      file://helloworld/__init__.py \
              file://helloworld/main.py"

S = "${WORKDIR}"

inherit setuptools

do_install_append () {
    install -d ${D}${bindir}
    install -m 0755 python-helloworld.py ${D}${bindir}
}

为了创建一个示例的 Hello World 包,我们创建如下截图所示的目录结构:

如何操作...

这是相同目录结构的代码:

$ mkdir -p meta-custom/recipes-python/python-helloworld/python- helloworld-1.0/helloworld/
$ touch meta-custom/recipes-python/python-helloworld/python- helloworld-1.0/helloworld/__init__.py

然后创建以下meta-custom/recipes-python/python-helloworld/python-helloworld-1.0/setup.py Python 设置文件:

import sys
from setuptools import setup

setup(
    name = "helloworld",
    version = "0.1",
    packages=["helloworld"],
    author="Alex Gonzalez",
    author_email = "alex@example.com",
    description = "Hello World packaging example",
    license = "MIT",
    keywords= "example",
    url = "",
)

以及meta-custom/recipes-python/python-helloworld/python-helloworld-1.0/helloworld/main.py Python 文件:

import sys

def main(argv=None):
    if argv is None:
        argv = sys.argv
    print "Hello world!"
    return 0

以及一个meta-custom/recipes-python/python-helloworld/python-helloworld-1.0/python-helloworld.py测试脚本,利用该模块:

#!/usr/bin/env python
import sys
import helloworld.main

if __name__ == '__main__':
       sys.exit(helloworld.main.main())

然后我们可以通过以下命令将其添加到我们的镜像中:

IMAGE_INSTALL_append = " python-helloworld"

然后使用以下命令构建:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake core-image-minimal

编程并启动后,我们可以通过运行示例脚本来测试该模块:

# /usr/bin/python-helloworld.py
Hello world!

还有更多……

meta-python中,你还可以找到python-pip食谱,它会将pip工具添加到目标镜像中。pip可以用于从Python 包索引PyPI)安装软件包。

你可以使用以下命令将其添加到镜像中:

IMAGE_INSTALL_append  = " python-pip python-distribute"

你需要将meta-openembedded/meta-python层添加到conf/bblayers.conf文件中,以便构建镜像,还需要python-distribute依赖项,它是python-pip所必需的。然后,你可以使用以下命令为core-image-minimal镜像构建:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake core-image-minimal

安装完成后,你可以按照以下方式从目标设备使用它:

# pip search <package_name>
# pip install <package_name>

集成 Oracle Java 运行时环境

Oracle 提供了两种专门用于嵌入式开发的 Java 版本:

  • Java SE 嵌入式:这是标准 Java SE 桌面版的一个大型子集。与标准版本相比,它包含了针对大小和内存使用的优化,以适应中型嵌入式设备的需求。

  • Java 微型版ME):该版本面向无头的低端和中端设备,是 Java SE 的一个子集,符合连接受限设备配置CLDC),并为嵌入式市场提供一些额外的功能和工具。Oracle 提供了几个参考实现,但 Java ME 必须从源代码中单独集成到特定平台。

我们将专注于 Java SE 嵌入式,它可以从 Oracle 的下载站点以二进制格式下载。

Java SE 嵌入式是商业授权的,并且在嵌入式部署中需要支付版税。

准备就绪

Yocto 有一个meta-oracle-java层,旨在帮助集成官方 Oracle Java 运行时环境JRE)版本 7。然而,由于 Oracle 的网页需要登录并接受其许可协议,因此无法实现无人干预的安装。

在 Java SE 嵌入式版本 7 中,Oracle 提供了软浮点和硬浮点版本的无头和有头 JRE,用于 ARMv6/v7,以及为 ARMv5 提供的软浮点用户空间的无头版本 JRE。Java SE 嵌入式版本 7 为 ARM Linux 提供了两种不同的Java 虚拟机JVM):

  • 一个针对响应性的客户端 JVM

  • 一个与客户端 JVM 相同的服务器 JVM,但优化了长时间运行的应用程序

截至本文撰写时,meta-oracle-java 层仅有一个针对带客户端 JVM 的无头硬浮动点版本的配方。我们将为最新的 Java 7 SE 嵌入式版本(更新 75)添加配方,涵盖无头和带头部的硬浮动点 JRE,这些版本适用于像 wandboard-quad 这样的基于 i.MX6 的板。

如何操作...

要安装 Java SE 嵌入式运行时环境,首先需要将 meta-oracle-java 层克隆到我们的源目录中,并将其添加到 conf/bblayers.conf 文件中,方法如下:

$ cd /opt/yocto/fsl-community-bsp/sources
$ git clone git://git.yoctoproject.org/meta-oracle-java

然后我们需要通过在 conf/local.conf 文件中添加以下内容,明确接受 Oracle Java 许可证:

LICENSE_FLAGS_WHITELIST += "oracle_java"

我们希望构建最新的更新,因此我们将以下 meta-custom/recipes-devtools/oracle-java/oracle-jse-ejre-arm-vfphflt-client-headless_1.7.0.bb 配方添加到我们的 meta-custom 层中:

SUMMARY = "Oracle Java SE runtime environment binaries"

JDK_JRE = "ejre"
require recipes-devtools/oracle-java/oracle-jse.inc

PV_UPDATE = "75"
BUILD_NUMBER = "13"

LIC_FILES_CHKSUM = "\
       file://${WORKDIR}/${JDK_JRE}${PV}_${PV_UPDATE}/COPYRIGHT;md5=0b204 bd2921accd6ef4a02f9c0001823 \
       file://${WORKDIR}/${JDK_JRE}${PV}_${PV_UPDATE}/THIRDPARTYLICENSERE ADME.txt;md5=f3a388961d24b8b72d412a079a878cdb \
       "

SRC_URI = "http://download.oracle.com/otn/java/ejre/7u${PV_UPDATE}- b${BUILD_NUMBER}/ejre-7u${PV_UPDATE}-fcs-b${BUILD_NUMBER}-linux- arm-vfp-hflt-client_headless-18_dec_2014.tar.gz"
SRC_URI[md5sum] = "759ca6735d77778a573465b1e84b16ec"
SRC_URI[sha256sum] = "ebb6499c62fc12e1471cff7431fec5407ace59477abd0f48347bf6e89c6bff3b"

RPROVIDES_${PN} += "java2-runtime"

尝试使用以下命令构建配方:

$ bitbake oracle-jse-ejre-arm-vfp-hflt-client-headless

你会看到我们遇到了校验和不匹配的错误。这是由于在 Oracle 网站上的许可证接受步骤所致。为了解决这个问题,我们需要手动下载文件到 downloads 目录中,路径由我们项目的 DL_DIR 配置变量指定。

然后我们可以将 JRE 添加到目标镜像中:

IMAGE_INSTALL_append = " oracle-jse-ejre-arm-vfp-hflt-client- headless"

然后使用以下命令构建:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake core-image-minimal

现在我们可以登录目标设备并运行它,使用命令:

# /usr/bin/java -version
java version "1.7.0_75"
Java(TM) SE Embedded Runtime Environment (build 1.7.0_75-b13, headless)
Java HotSpot(TM) Embedded Client VM (build 24.75-b04, mixed mode)

我们还可以使用以下 meta-custom/recipes-devtools/oracle-java/oracle-jse-ejre-arm-vfphflt-client-headful_1.7.0.bb 配方构建带头部版本:

SUMMARY = "Oracle Java SE runtime environment binaries"

JDK_JRE = "ejre"
require recipes-devtools/oracle-java/oracle-jse.inc

PV_UPDATE = "75"
BUILD_NUMBER = "13"

LIC_FILES_CHKSUM = "\
       file://${WORKDIR}/${JDK_JRE}${PV}_${PV_UPDATE}/COPYRIGHT;md5=0b204 bd2921accd6ef4a02f9c0001823 \
       file://${WORKDIR}/${JDK_JRE}${PV}_${PV_UPDATE}/THIRDPARTYLICENSERE ADME.txt;md5=f3a388961d24b8b72d412a079a878cdb \
       "

SRC_URI = "http://download.oracle.com/otn/java/ejre/7u${PV_UPDATE}- b${BUILD_NUMBER}/ejre-7u${PV_UPDATE}-fcs-b${BUILD_NUMBER}-linux- arm-vfp-hflt-client_headful-18_dec_2014.tar.gz"

SRC_URI[md5sum] = "84dba4ffb47285b18e6382de2991edfc"
SRC_URI[sha256sum] = "5738ffb8ce2582b6d7b39a3cbe16137d205961224899f8380eebe3922bae5c61"

RPROVIDES_${PN} += "java2-runtime"

然后使用以下命令将其添加到目标镜像中:

IMAGE_INSTALL_append =  " oracle-jse-ejre-arm-vfp-hflt-client- headful"

然后使用以下命令构建 core-image-sato

$ cd cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake core-image-sato

在这种情况下,报告的 Java 版本是:

# /usr/bin/java -version
java version "1.7.0_75"
Java(TM) SE Embedded Runtime Environment (build 1.7.0_75-b13)
Java HotSpot(TM) Embedded Client VM (build 24.75-b04, mixed mode)

还有更多内容...

截至本文撰写时,最新版本是 Java SE 嵌入式版本 8 更新 33(8u33)。

Oracle 仅提供 JDK 下载,且需要使用一个主机工具 jrecreate 来配置并创建适当的 JRE。该工具允许我们在不同的 JVM(最小版、客户端版和服务器版)以及软硬浮动点 ABI、JavaFX 扩展、本地化和 JVM 的其他一些调优选项之间进行选择。

Oracle Java SE 嵌入式版本 8 提供对仅适用于 ARMv7 硬浮动点用户空间的 X11 开发的支持,支持使用 Swing、AWT 和 JavaFX,且包括对 Freescale i.MX6 处理器上 JavaFX(旨在替代 Swing 和 AWT 的图形框架)的支持。

截至本文撰写时,还没有 Yocto 配方可以集成 Java 版本 8。

集成开放 Java 开发工具包

Oracle Java SE 嵌入式的开源替代品是 开放 Java 开发工具包OpenJDK),它是 Java SE 的开源实现,基于 GPLv2 许可证并附带类路径例外,这意味着应用程序可以链接而不受 GPL 许可证的约束。

这个配方将展示如何使用 Yocto 构建 OpenJDK,并将 JRE 集成到我们的目标镜像中。

准备工作

OpenJDK 的主要组件包括:

  • 热点 Java 虚拟机

  • Java 类库JCL

  • Java 编译器,javac

最初,OpenJDK 需要使用专有的 JDK 进行构建。然而,IcedTea项目使我们能够使用 GNU 类库、GNU 的 Java 编译器(GCJ)来构建 OpenJDK,并引导 JDK 来构建 OpenJDK。它还通过补充一些 Java SE 中缺失的组件(如网页浏览器插件和 Web Start 实现)来完善 OpenJDK。

Yocto 可以使用meta-java层构建 OpenJDK,其中包括使用 IcedTea 进行交叉编译 OpenJDK 的配方。

你可以从其 Git 仓库下载 OpenJDK,地址为git.yoctoproject.org/cgit/cgit.cgi/meta-java/

开发讨论可以通过访问开发邮件列表lists.openembedded.org/mailman/listinfo/openembedded-devel进行跟踪和参与。

meta-java层还包括多种 Java 库和虚拟机的配方,以及应用开发工具,如antfastjar

如何实现...

要构建 OpenJDK 7,你需要按如下方式克隆meta-java层:

$ cd /opt/yocto/fsl-community-bsp/sources/
$ git clone http://git.yoctoproject.org/cgit/cgit.cgi/meta-java/

在写这篇文章时,还没有 1.7 版本的 Dizzy 分支,所以我们将直接使用主分支。

将该层添加到你的conf/bblayers.conf文件中:

+ ${BSPDIR}/sources/meta-java \
 "

然后通过将以下内容添加到你的conf/local.conf文件中来配置项目:

PREFERRED_PROVIDER_virtual/java-initial = "cacao-initial"
PREFERRED_PROVIDER_virtual/java-native = "jamvm-native"
PREFERRED_PROVIDER_virtual/javac-native = "ecj-bootstrap-native"
PREFERRED_VERSION_openjdk-7-jre = "25b30-2.3.12"
PREFERRED_VERSION_icedtea7-native = "2.1.3"

然后你可以通过以下方式将 OpenJDK 包添加到你的镜像中:

IMAGE_INSTALL_append = " openjdk-7-jre"

并构建你选择的镜像:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake core-image-sato

当你运行目标镜像时,你将看到以下 Java 版本:

# java -version
java version "1.7.0_25"
OpenJDK Runtime Environment (IcedTea 2.3.12) (25b30-2.3.12)
OpenJDK Zero VM (build 23.7-b01, mixed mode)

它是如何工作的...

为了测试 JVM,我们可以在主机上字节编译一个 Java 类并将其复制到目标系统上执行。例如,我们可以使用以下简单的HelloWorld.java示例:

class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello World!");
  }
}

为了在主机上进行字节编译,我们需要安装 Java SDK。在 Ubuntu 中安装 Java SDK,只需运行:

$ sudo apt-get install openjdk-7-jdk

要字节编译示例,我们执行:

$ javac HelloWorld.java

要运行它,我们将HelloWorld.class复制到目标系统,并在同一文件夹中运行:

# java HelloWorld

还有更多...

在生产系统上使用 OpenJDK 时,建议始终使用最新的发布版本,其中包含了错误修复和安全更新。在写这篇文章时,最新的 OpenJDK 7 发布版本是更新 71(jdk7u71b14),可以使用 IcedTea 2.5.3 构建,因此meta-java配方应进行更新。

另见

集成 Java 应用程序

meta-java层还提供了帮助类,以便简化将 Java 库和应用程序集成到 Yocto 中的过程。在本配方中,我们将看到一个使用提供的类构建 Java 库的示例。

准备工作

meta-java层提供了两个主要的类,帮助集成 Java 应用程序和库:

  • Java bbclass:这提供了默认的目标目录和一些辅助功能,即:

    • oe_jarinstall:这将安装并创建 JAR 文件的符号链接

    • oe_makeclasspath:这是一个根据 JAR 文件名生成类路径字符串的工具。

    • oe_java_simple_wrapper:这是一个将 Java 应用程序封装在 shell 脚本中的工具。

  • java-library bbclass:该类继承了 Java 的 bbclass,并扩展其功能以创建和安装 JAR 文件。

如何实现……

我们将使用以下的meta-custom/recipes-java/java-helloworld/java-helloworld-1.0/HelloWorldSwing.java图形化 Swing Hello World 作为示例:

import javax.swing.JFrame;
import javax.swing.JLabel;

public class HelloWorldSwing {
    private static void createAndShowGUI() {
        JFrame frame = new JFrame("Hello World!");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        JLabel label = new JLabel("Hello World!");
        frame.getContentPane().add(label);

        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });
    }
}

要集成这个HelloWorldSwing应用程序,我们可以使用一个 Yocto meta-custom/recipes-java/java-helloworld/java-helloworld_1.0.bb食谱,如下所示:

DESCRIPTION = "Simple Java Swing hello world application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

RDEPENDS_${PN} = "java2-runtime"

SRC_URI = "file://HelloWorldSwing.java"

S = "${WORKDIR}"

inherit java-library

do_compile() {
        mkdir -p build
        javac -d build `find . -name "*.java"`
        fastjar cf ${JARFILENAME} -C build .
}

BBCLASSEXTEND = "native"

该食谱也可以为主机本地架构构建。我们可以通过提供一个单独的java-helloworld-native食谱,该食谱继承native类,或者像之前一样使用BBCLASSEXTEND变量来实现。在这两种情况下,我们可以使用_class-native_class-target覆盖选项,以区分本地和目标功能。

即使 Java 是字节编译的,且编译后的类在两者之间是相同的,但显式地添加本地支持仍然是有意义的。

它是如何工作的……

java-library类将创建一个名为lib<package>-java的库包。

要将其添加到目标镜像中,我们将使用:

IMAGE_INSTALL_append = " libjava-helloworld-java"

然后我们可以决定是否使用 Oracle JRE 或 OpenJDK 运行应用程序。对于 OpenJDK,我们将向我们的镜像中添加以下软件包:

IMAGE_INSTALL_append = " openjdk-7-jre openjdk-7-common"

对于 Oracle JRE,我们将使用以下内容:

IMAGE_INSTALL_append = " oracle-jse-ejre-arm-vfp-hflt-client- headful"

目前可用的 JRE 无法在帧缓冲或 Wayland 上运行,因此我们将使用基于 X11 的图形镜像,如core-image-sato

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake core-image-sato

然后我们可以启动目标系统,登录并通过运行以下命令使用 OpenJDK 执行示例:

# export DISPLAY=:0
# java -cp /usr/share/java/java-helloworld.jar HelloWorldSwing

还有更多内容……

在写本文时,从meta-java层主分支构建的 OpenJDK 无法运行 X11 应用程序,并将出现以下异常:

Exception in thread "main" java.awt.AWTError: Toolkit not found: sun.awt.X11.XToolkit
        at java.awt.Toolkit$2.run(Toolkit.java:875)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.awt.Toolkit.getDefaultToolkit(Toolkit.java:860)
        at java.awt.Toolkit.getEventQueue(Toolkit.java:1730)
        at java.awt.EventQueue.invokeLater(EventQueue.java:1217)
        at javax.swing.SwingUtilities.invokeLater(SwingUtilities.java:1287)
        at HelloWorldSwing.main(HelloWorldSwing.java:17)

然而,预编译的 Oracle JRE 能够无问题地运行该应用程序:

# export DISPLAY=:0
# /usr/bin/java -cp /usr/share/java/java-helloworld.jar HelloWorldSwing

注意

如果在使用 Oracle JRE 构建软件包时遇到构建错误,可以尝试使用其他包格式,例如 IPK,通过在conf/local.conf配置文件中添加以下内容:

PACKAGE_CLASSES = "package_ipk"

这是由于meta-oracle-java层中的 RPM 包管理器存在依赖问题,具体情况请参见该层的 README 文件。

第五章:调试、追踪和分析

在本章中,我们将涵盖以下配方:

  • 分析核心转储

  • 本地 GDB 调试

  • 跨平台 GDB 调试

  • 使用 strace 进行应用程序调试

  • 使用内核的性能计数器

  • 使用静态内核追踪

  • 使用动态内核追踪

  • 使用动态内核事件

  • 探索 Yocto 的追踪和分析工具

  • 使用 perf 进行追踪和分析

  • 使用 SystemTap

  • 使用 OProfile

  • 使用 LTTng

  • 使用 blktrace

介绍

嵌入式 Linux 产品的调试不仅是开发过程中常见的任务,在部署后的生产系统中也同样重要。

嵌入式 Linux 中的应用程序调试与传统嵌入式设备中的调试不同,因为我们没有一个操作系统和应用程序共享相同地址空间的平坦内存模型。相反,我们有一个虚拟内存模型,Linux 操作系统共享地址空间,并为运行中的进程分配虚拟内存区域。

在这种模型中,内核和用户空间调试使用的机制有所不同。例如,传统的基于 JTAG 的硬件调试器在内核调试中非常有用,但除非它了解用户空间进程的内存映射,否则无法调试用户空间应用程序。

应用程序调试通过使用用户空间调试服务来进行。我们已经看到过这种方法在 Eclipse GDB 中使用的 TCF 代理的实例。另一个常用的代理是gdbserver,我们将在本章中使用它。

最后,我们将探索追踪和分析的领域。追踪是对频繁的系统事件进行低级别的日志记录,而这些捕获的日志的统计分析被称为分析。

我们将使用嵌入式 Linux 和 Yocto 提供的某些工具来追踪和分析我们的系统,使其达到最大潜力。

分析核心转储

即使经过广泛的质量保证测试,嵌入式系统在现场也可能发生故障,需要进行调试。此外,故障往往不是可以在实验室环境中轻松重现的,因此我们只能在生产环境中调试,通常是已经硬化过的系统。

假设我们已经设计了符合上述情境的系统,我们的第一个调试选择通常是提取尽可能多的关于故障系统的信息——例如,通过获取并分析有问题进程的核心转储。

做好准备

在调试嵌入式 Linux 系统的过程中,我们可以使用与标准 Linux 系统相同的工具箱。工具之一使得应用程序在崩溃时可以将内存核心转储到磁盘。这要求我们有足够的磁盘空间来存储应用程序的完整内存映射,并且磁盘写入速度足够快,避免系统停滞。

一旦生成内存核心转储,我们使用主机的 GDB 来分析核心转储。GDB 需要有调试信息可用。调试信息可以包含在可执行文件本身中——例如,当我们安装-dbg版本的包,或者我们配置项目不剥离二进制文件——也可以保存在一个单独的文件中。要将调试信息与可执行文件分开安装,我们使用dbg-pkgs功能。默认情况下,这将把包的调试信息安装到与可执行文件本身相同位置的.debug目录中。要为目标镜像中的所有包添加调试信息,我们需要在conf/local.conf配置文件中添加以下内容:

EXTRA_IMAGE_FEATURES += "dbg-pkgs"

然后我们可以构建一个适当的工具链,与我们的文件系统匹配,就像我们在第四章中的准备和使用 SDK食谱中看到的那样。核心转储包含崩溃时正在使用的可执行文件和库的构建 ID,因此将工具链与目标镜像匹配非常重要。

如何操作...

我们可以使用ulimit工具显示系统范围内的资源限制。我们关注的是核心文件的大小,默认情况下设置为零,以避免创建应用程序核心转储。在我们失败的系统中,最好在测试环境中,使应用程序在崩溃时通过以下命令生成内存核心转储:

$ ulimit -c unlimited

然后你可以通过以下命令验证更改:

$ ulimit -a
-f: file size (blocks)             unlimited
-t: cpu time (seconds)             unlimited
-d: data seg size (kb)             unlimited
-s: stack size (kb)                8192
-c: core file size (blocks)        unlimited
-m: resident set size (kb)         unlimited
-l: locked memory (kb)             64
-p: processes                      5489
-n: file descriptors               1024
-v: address space (kb)             unlimited
-w: locks                          unlimited
-e: scheduling priority            0
-r: real-time priority             0

在这个示例中,我们将在一个真实的段错误场景中使用wvdial应用程序。目的是展示核心转储分析的使用方法,而不是调试应用程序本身;因此,不提供应用程序特定配置和系统设置的详细信息。不过,作为一个真实的崩溃,示例具有更强的说明性。

要在目标上运行wvdial,请使用以下代码:

# wvdial
--> WvDial: Internet dialer version 1.61
--> Initializing modem.
--> Sending: ATZ
ATZ
OK
--> Sending: ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
OK
--> Sending: AT+CGDCONT=1,"IP","internet"
AT+CGDCONT=1,"IP","internet"
OK
--> Modem initialized.
--> Idle Seconds = 3000, disabling automatic reconnect.
Segmentation fault (core dumped)

应用程序将在相同的文件夹中创建一个核心文件,你可以将其复制到主机系统进行分析。

提示

你也可以通过向正在运行的进程发送SIGQUIT信号来模拟核心转储。例如,你可以通过以下方式强制sleep命令生成核心转储:

 $ ulimit -c unlimited
 $ sleep 30 &
 $ kill -QUIT <sleep-pid>

它是如何工作的...

一旦获得核心转储,使用主机中的交叉 GDB 加载它,并通过以下步骤获取一些有用的信息,比如回溯:

  1. 首先在主机中设置环境:

    $ cd /opt/poky/1.7.1/
    $ source environment-setup-cortexa9hf-vfp-neon-poky-linux- gnueabi
    
    
  2. 然后你可以启动交叉 GDB 调试器,传递一个调试版本的应用程序。调试版本存储在与未剥离的二进制文件相同位置的sysroot文件中,但在.debug目录下。

    以下是完整的 GDB 横幅,但在以后的示例中将省略。

    $ arm-poky-linux-gnueabi-gdb /opt/yocto/fsl-community- bsp/wandboard-quad/tmp/work/cortexa9hf-vfp-neon-poky-linux- gnueabi/wvdial/1.61-r0/packages-split/wvdial- dbg/usr/bin/.debug/wvdial core
    GNU gdb (GDB) 7.7.1
    Copyright (C) 2014 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "--host=x86_64-pokysdk-linux -- target=arm-poky-linux-gnueabi".
    Type "show configuration" for configuration details.
    For bug reporting instructions, please see:
    <http://www.gnu.org/software/gdb/bugs/>.
    Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.
    For help, type "help".
    Type "apropos word" to search for commands related to "word"...
    Reading symbols from /opt/yocto/fsl-community-bsp/wandboard- quad/tmp/work/cortexa9hf-vfp-neon-poky-linux- gnueabi/wvdial/1.61-r0/packages-split/wvdial- dbg/usr/bin/.debug/wvdial...done.
    [New LWP 1050]
    
    warning: Could not load shared library symbols for 14 libraries, e.g. /usr/lib/libwvstreams.so.4.6.
    Use the "info sharedlibrary" command to see the complete listing.
    Do you need "set solib-search-path" or "set sysroot"?
    Core was generated by `wvdial'.
    Program terminated with signal SIGSEGV, Segmentation fault.
    #0  0x76d524c4 in ?? ()
    
  3. 现在将 GDB 指向工具链sysroot的所在位置:

    (gdb) set sysroot /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi/
    Reading symbols from /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi/usr/lib/libwvstreams.so.4.6...Reading symbols from /opt/poky/1.7.1/sysroots/cortexa9hf-vfp-neon-poky-linux- gnueabi/usr/lib/.debug/libwvstreams.so.4.6...done.
    done.
    Loaded symbols for /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi/usr/lib/libwvstreams.so.4.6
    Reading symbols from /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi/usr/lib/libwvutils.so.4.6...Reading symbols from /opt/poky/1.7.1/sysroots/cortexa9hf-vfp-neon- poky-linux-gnueabi/usr/lib/.debug/libwvutils.so.4.6...done.
    done.
    [...]
    Loaded symbols for /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi/lib/libdl.so.2
    
  4. 现在你可以使用以下命令查询 GDB 获取应用程序的回溯:

    (gdb) bt
    #0  0x76d524c4 in WvTaskMan::_stackmaster () at utils/wvtask.cc:416
    #1  0x00000000 in ?? ()
    

另见

本地 GDB 调试

在像 Wandboard 这样强大的设备上,本地调试也是调试间歇性故障的一个选择。本食谱将探讨本地调试方法。

准备工作

对于本地开发和调试,Yocto 提供了 -dev-sdk 目标镜像。为了将开发工具添加到 -dev 镜像中,我们可以使用 tools-sdk 功能。我们还需要安装调试信息和调试工具,方法是将 dbg-pkgstools-debug 功能添加到镜像中。例如,对于 core-image-minimal-dev,我们可以将以下内容添加到 conf/local.conf 文件中:

EXTRA_IMAGE_FEATURES += "tools-sdk dbg-pkgs tools-debug"

要准备一个开发就绪的 core-image-minimal-dev 目标镜像版本,我们将执行以下命令:

$ cd /opt/yocto/fsl-community-bsp/
$ source setup-environment wandboard-quad
$ bitbake core-image-minimal-dev

我们接下来将把开发镜像写入目标设备。

如何做...

一旦目标设备启动,您可以通过本地 GDB 使用以下步骤启动 wvdial 应用程序:

  1. 在目标设备的命令提示符中,使用应用程序作为参数启动 GDB 调试器:

    $ gdb wvdial
    
    
  2. 现在指示 GDB 运行应用程序:

    (gdb) run
    Starting program: /usr/bin/wvdial
    Cannot access memory at address 0x0
    Cannot access memory at address 0x0
    
    Program received signal SIGILL, Illegal instruction.
    0x7698afe8 in ?? () from /lib/libcrypto.so.1.0.0
    (gdb) sharedlibrary libcrypto
    Symbols already loaded for /lib/libcrypto.so.1.0.0
    
  3. 然后请求打印回溯信息:

    (gdb) bt
    #0  0x7698afe8 in ?? () from /lib/libcrypto.so.1.0.0
    #1  0x769878e8 in OPENSSL_cpuid_setup () from /lib/libcrypto.so.1.0.0
    #2  0x76fe715c in ?? () from /lib/ld-linux-armhf.so.3
    Cannot access memory at address 0x48535540
    

    这不是您在分析核心转储时获得的相同回溯信息。那么这里发生了什么?线索在于 libcrypto,它是 OpenSSL 库的一部分。OpenSSL 通过尝试每个功能并捕获非法指令错误来探测系统的功能。因此,您在启动过程中看到的 SIGILL 信号是正常的,您应该指示 GDB 继续执行。

  4. 指示 GDB 继续执行:

    (gdb) c
    Continuing.
    --> WvDial: Internet dialer version 1.61
    --> Initializing modem.
    --> Sending: ATZ
    ATZ
    OK
    --> Sending: ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
    ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
    OK
    --> Sending: AT+CGDCONT=1,"IP","internet"
    AT+CGDCONT=1,"IP","internet"
    OK
    --> Modem initialized.
    --> Idle Seconds = 3000, disabling automatic reconnect.
    
    Program received signal SIGSEGV, Segmentation fault.
    0x76db74c4 in WvTaskMan::_stackmaster() () from /usr/lib/libwvbase.so.4.6
    

该结果现在与您在前一个食谱中看到的核心转储兼容。

还有更多...

在调试应用程序时,有时通过减少编译器使用的优化级别会非常有用。这会降低应用程序的性能,但通过提高调试信息的准确性,有助于调试。您可以通过将以下代码行添加到 conf/local.conf 文件中来配置构建系统,减少优化并添加调试信息:

DEBUG_BUILD = "1"

通过使用此配置,优化级别从 FULL_OPTIMIZATION (-O2) 降低到 DEBUG_OPTIMIZATION (-O -fno-omit-frame-pointer)。但有时这还不够,您可能希望在没有优化的情况下进行构建。您可以通过全局或针对特定食谱覆盖 DEBUG_OPTIMIZATION 变量来实现这一点。

另请参见

  • 接下来的 交叉 GDB 调试 食谱中的调试优化构建示例

交叉 GDB 调试

当我们在主机上运行交叉编译的 GDB,并连接到在目标设备上运行的本地 gdbserver 时,这被称为交叉调试。这与我们之前在 使用 Eclipse IDE 食谱中看到的场景相同,唯一不同的是 Eclipse 使用 目标通信框架TCF)。交叉调试的优势在于不需要目标镜像上的调试信息,因为这些信息已经在主机上可用。

本食谱将展示如何使用交叉 GDB 和 gdbserver。

准备工作

要在目标镜像中包含 gdbserver,你可以使用 -sdk 镜像,或者通过将以下内容添加到 conf/local.conf 配置文件中,向镜像中添加 tools-debug 功能:

EXTRA_IMAGE_FEATURES += "tools-debug"

为了让 GDB 能访问共享库和可执行文件的调试信息,请在 conf/local.conf 文件中添加以下内容:

EXTRA_IMAGE_FEATURES += "dbg-pkgs"

在目标上运行的镜像与工具链的 sysroot 需要匹配。例如,如果你使用的是 core-image-minimal 镜像,则工具链需要在同一项目中生成,且:

$ bitbake -c populate_sdk core-image-minimal

这将生成一个包含二进制文件和库调试信息的 sysroot

如何操作...

一旦工具链安装完成,你可以在目标机上使用 gdbserver 运行待调试的应用程序——在本例中是 wvdial——并执行以下步骤:

  1. 使用应用程序作为参数启动 gdbserver:

    # gdbserver localhost:1234 /usr/bin/wvdial
    Process wvdial created; pid = 879
    Listening on port 1234
    
    

    gdbserver 启动并在 localhost 上监听一个随机的 1234 端口,等待远程 GDB 的连接。

  2. 在主机上,你现在可以使用最近安装的工具链来设置环境:

    $ cd /opt/poky/1.7.1/
    $ source environment-setup-cortexa9hf-vfp-neon-poky-linux- gnueabi
    
    

    然后你可以启动跨平台 GDB,传递给它应用程序的调试版本的绝对路径,该版本位于 sysroot 中的 .debug 目录下:

    $ arm-poky-linux-gnueabi-gdb /opt/poky/1.7.1/sysroots/cortexa9hf-vfp-neon-poky-linux- gnueabi/usr/bin/.debug/wvdial
    Reading symbols from /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi/usr/bin/.debug/wvdial...done.
    (gdb)
    
    
  3. 接下来,配置 GDB 将所有文件视为可信,以便它自动加载所需的内容:

    (gdb) set auto-load safe-path /
    
  4. 正如你所知,wvdial 会生成一个 SIGILL 信号,打断我们的调试会话,指示 GDB 在遇到该信号时不要停止:

    (gdb) handle SIGILL nostop
    
  5. 然后你可以通过以下命令连接到 1234 端口的远程目标:

    (gdb) target remote <target_ip>:1234
    Remote debugging using 192.168.128.6:1234
    Cannot access memory at address 0x0
    0x76fd7b00 in ?? ()
    
  6. 首先要做的是设置 sysroot,使 GDB 能找到动态加载的库:

    (gdb) set sysroot /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi
    Reading symbols from /opt/poky/1.7.1/sysroots/cortexa9hf- vfp-neon-poky-linux-gnueabi/lib/ld-linux- armhf.so.3...done.
    Loaded symbols for /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi/lib/ld-linux-armhf.so.3
    
  7. 输入 c 以继续程序的执行。你将看到 wvdial 在目标机上继续执行:

    --> WvDial: Internet dialer version 1.61
    --> Initializing modem.
    --> Sending: ATZ
    ATZ
    OK
    --> Sending: ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
    ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
    OK
    --> Sending: AT+CGDCONT=1,"IP","internet"
    AT+CGDCONT=1,"IP","internet"
    OK
    --> Modem initialized.
    --> Idle Seconds = 3000, disabling automatic reconnect.
    
    
  8. 然后你会看到 GDB 在主机上拦截到 SIGILLSEGSEGV 信号:

    Program received signal SIGILL, Illegal instruction.
    
    Program received signal SIGSEGV, Segmentation fault.
    0x76dc14c4 in WvTaskMan::_stackmaster () at utils/wvtask.cc:416
    416     utils/wvtask.cc: No such file or directory.
    
  9. 现在你可以请求查看回溯信息:

    (gdb) bt
    #0  0x76dc14c4 in WvTaskMan::_stackmaster () at utils/wvtask.cc:416
    #1  0x00000000 in ?? ()
    

    虽然有限,这个回溯仍然对调试应用程序有所帮助。

它是如何工作的...

我们看到的回溯信息有限,因为编译后的二进制文件不适合调试,它们省略了栈帧。为了保留栈帧信息,请在 conf/local.conf 配置文件中添加以下内容:

DEBUG_BUILD = "1"

这将把编译标志更改为调试优化,具体如下:

DEBUG_OPTIMIZATION = "-O -fno-omit-frame-pointer ${DEBUG_FLAGS} - pipe"

-fno-omit-frame-pointer 标志会告诉 gcc 保留栈帧。编译器还会减少优化级别,以提供更好的调试体验。

调试构建还可以让你追踪变量,设置断点和观察点,以及其他常见的调试功能。

在重新构建和安装目标镜像及工具链之后,你现在可以按照前述步骤中的相同步骤继续操作:

  1. 使用以下代码连接到远程目标:

    (gdb) target remote <target_ip>:1234
    Remote debugging using 192.168.128.6:1234
    warning: Unable to find dynamic linker breakpoint function.
    GDB will be unable to debug shared library initializers
    and track explicitly loaded dynamic code.
    Cannot access memory at address 0x0
    0x76fdd800 in ?? ()
    

    如下设置 sysroot

    (gdb) set sysroot /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi
    Reading symbols from /opt/poky/1.7.1/sysroots/cortexa9hf- vfp-neon-poky-linux-gnueabi/lib/ld-linux- armhf.so.3...done.
    Loaded symbols for /opt/poky/1.7.1/sysroots/cortexa9hf-vfp- neon-poky-linux-gnueabi/lib/ld-linux-armhf.so.3
    
  2. 完成设置后,按照如下方式指示程序继续运行:

    (gdb) c
    Continuing.
    
    Program received signal SIGILL, Illegal instruction.
    
    Program received signal SIGABRT, Aborted.
    0x76b28bb4 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
    55      ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
    (gdb) bt
    #0  0x76b28bb4 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:55
    #1  0x76b2cabc in __GI_abort () at abort.c:89
    #2  0x76decfa8 in __assert_fail (__assertion=0x76df4600 "magic_number == -0x123678",
        __file=0x1 <error: Cannot access memory at address 0x1>, __line=427,
        __function=0x76df4584 <WvTaskMan::_stackmaster()::__PRETTY_FUNCTION__> "static void WvTaskMan::_stackmaster()")
        at utils/wvcrashbase.cc:98
    #3  0x76dc58c8 in WvTaskMan::_stackmaster () at utils/wvtask.cc:427
    Cannot access memory at address 0x123678
    #4  0x00033690 in ?? ()
    Cannot access memory at address 0x123678
    Backtrace stopped: previous frame identical to this frame (corrupt stack?)
    

    你现在可以看到完整的回溯信息。

使用 strace 进行应用程序调试

调试并不总是与源代码打交道。有时候,是外部因素的变化引起了问题。

Strace 是一个有用的工具,适用于我们在寻找二进制文件本身以外的问题时;例如配置文件、输入数据和内核接口。本章节将解释如何使用它。

准备工作

要在你的系统中包含 strace,请将以下内容添加到你的 conf/local.conf 配置文件中:

IMAGE_INSTALL_append = " strace"

Strace 也包含在 tools-debug 镜像特性中,因此你也可以通过以下方式添加它:

EXTRA_IMAGE_FEATURES += "tools-debug"

Strace 也包含在 -sdk 镜像中。

在开始之前,我们还将包括 pgrep,这是一个进程工具,它通过名称查找进程 ID,从而让我们的调试更为轻松。为此,请将以下内容添加到你的 conf/local.conf 配置文件中:

IMAGE_INSTALL_append = " procps"

如何操作...

当打印系统调用时,strace 会打印传递给内核或从内核返回的值。详细模式选项会为某些系统调用打印更多的细节。

例如,过滤来自单个 ping 的 sendto() 系统调用,如下所示:

# strace -f -t -e sendto /bin/bash -c "ping -c 1 127.0.0.1"
5240  17:18:04 sendto(0, "\10\0;\220x\24\0\0\225m\256\355\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\ 0\0"..., 64, 0, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("127.0.0.1")}, 28) = 64

它是如何工作的...

Strace 允许监视运行中的进程的系统调用并将其传递到 Linux 内核中。它使用 ptrace() 系统调用来实现这一功能。这意味着,其他使用 ptrace() 的程序,如 gdb,将无法同时运行。

Strace 是一种具有破坏性的监控工具,监控的进程将会变慢,并且会产生更多的上下文切换。对给定程序运行 strace 的通用方式如下:

strace -f -e <filter> -t -s<num> -o <log file>.strace <program>

下面解释了这些参数:

  • f:告诉 strace 跟踪所有子进程。

  • e:过滤输出,仅显示以逗号分隔的系统调用。

  • t:打印绝对时间戳。使用 r 以获取相对于上一个系统调用的时间戳,使用 T 添加系统调用的执行时间。

  • s:增加字符串的最大长度,默认值为 32

  • o:将输出重定向到一个文件,供离线分析。

它还可以使用以下命令附加到正在运行的进程:

$ strace -p $( pgrep <program> )

或者使用以下命令来查看某个进程的多个实例:

$ strace $( pgrep <program> | sed 's/^/-p' )

要分离,只需按 Ctrl + C

另见

使用内核的性能计数器

硬件性能计数器非常适合代码优化,特别是在只有单一工作负载的嵌入式系统中。它们被广泛用于各种追踪和分析工具中。本章节将介绍 Linux 性能计数器子系统,并展示如何使用它。

准备工作

Linux 内核性能计数子系统LPC),通常称为 linux_perf,是一个用于不同 CPU 特定性能测量的抽象接口。perf_events 子系统不仅暴露来自 CPU 的硬件性能计数器,还通过相同的 API 提供内核软件事件。它还允许将事件映射到进程,尽管这会带来性能开销。此外,它提供了跨架构通用的事件。

事件可以分为三大类:

  • 软件事件:基于内核计数器,这些事件用于跟踪上下文切换、次要故障等。

  • 硬件事件:这些来自处理器的 CPU 性能监控单元PMU),用于跟踪特定于架构的项目,如周期数、缓存未命中等。它们随每种处理器类型而变化。

  • 硬件缓存事件:这些是常见的硬件事件,只有在它们实际上映射到 CPU 硬件事件时才会可用。

要了解你的平台是否支持 perf_event,你可以检查 /proc/sys/kernel/perf_event_paranoid 文件的存在。该文件还用于限制对性能计数器的访问,默认情况下允许内核和用户测量。它可以有以下值:

  • 2: 仅允许用户空间测量

  • 1: 允许内核和用户测量(默认)

  • 0: 允许访问特定于 CPU 的数据,但不包括原始追踪点样本

  • -1: 无限制

i.MX6 SoC 配备了 Cortex-A9 CPU,其中包含一个 PMU,提供六个计数器,用于收集处理器和内存操作的统计数据,每个计数器都可以监控 58 个可用事件中的任何一个。

你可以在 Cortex-A9 技术参考手册 中找到可用事件的描述。

i.MX6 性能计数器不允许仅对用户或仅对内核测量进行独占访问。此外,不幸的是,i.MX6 SoC 设计师将所有 CPU 核心的 PMU 中断合并在一起,而理想情况下它们应该仅由触发它们的 CPU 处理。你可以使用 maxcpus=1 内核命令行参数启动 i.MX6,只使用一个核心,这样你仍然可以使用 perf_events 接口。

要配置 Linux 内核使用单核启动,请在 U-Boot 提示符下停止并按如下方式更改 mmcargs 环境变量:

> setenv mmcargs 'setenv bootargs console=${console},${baudrate} root=${mmcroot} ${extra_bootargs}; run videoargs'
> setenv extra_bootargs maxcpus=1

注意

mmcargs 环境变量仅在从 MMC 设备(如 microSD 卡)启动时使用。如果目标是从其他来源启动,如网络,则需要更改相应的环境变量。你可以使用 printenv U-Boot 命令转储整个 U-Boot 环境,并使用 setenv 更改所需的变量。

如何操作...

该接口引入了sys_perf_event_open()系统调用,计数器通过ioctls启动和停止,读取可以通过read()调用或mmapping样本写入循环缓冲区。perf_event_open()系统调用定义如下:

#include <linux/perf_event.h>
#include <linux/hw_breakpoint.h>

int perf_event_open(struct perf_event_attr *attr,
                    pid_t pid, int cpu, int group_fd,
                    unsigned long flags);

由于没有C库包装器,因此需要使用syscall()调用它。

它是如何工作的...

以下是一个例子,perf_example.c,该程序修改自perf_event_open手册页面,用于测量printf调用的指令数:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/perf_event.h>
#include <asm/unistd.h>

static long
perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                int cpu, int group_fd, unsigned long flags)
{
    int ret;

    ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
                   group_fd, flags);
    return ret;
}

int
main(int argc, char **argv)
{
    struct perf_event_attr pe;
    long long count;
    int fd;

    memset(&pe, 0, sizeof(struct perf_event_attr));
    pe.type = PERF_TYPE_HARDWARE;
    pe.size = sizeof(struct perf_event_attr);
    pe.config = PERF_COUNT_HW_INSTRUCTIONS;
    pe.disabled = 1;

    fd = perf_event_open(&pe, 0, -1, -1, 0);
    if (fd == -1) {
       fprintf(stderr, "Error opening leader %llx\n", pe.config);
       exit(EXIT_FAILURE);
    }

    ioctl(fd, PERF_EVENT_IOC_RESET, 0);
    ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

    printf("Measuring instruction count for this printf\n");

    ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
    read(fd, &count, sizeof(long long));

    printf("Used %lld instructions\n", count);

    close(fd);

    return 0;
}

要在外部编译此程序,我们可以使用以下命令:

$ source /opt/poky/1.7.1/environment-setup-cortexa9hf-vfp-neon-poky- linux-gnueabi
$ ${CC} perf_example.c -o perf_example

将二进制文件复制到目标设备后,您可以通过以下代码执行它:

# ./perf_example
Measuring instruction count for this printf
Used 0 instructions

显然,使用零指令来调用printf()是不正确的。经过检查,发现 i.MX6 处理器上有一个已记录的错误(ERR006259),该错误指出为了使用 PMU,SoC 在上电重置后需要至少接收 4 个 JTAG 时钟周期。

重新连接 JTAG 并重新运行该示例:

# ./perf_example
Measuring instruction count for this printf
Used 3977 instructions

还有更多...

即使你可以像前面例子中那样直接访问perf_events接口,推荐的使用方法是通过用户空间应用程序(如 perf),我们将在本章的使用 perf 进行跟踪和分析示例中看到。

另见

使用静态内核跟踪

Linux 内核不断通过静态探针点(称为tracepoints)进行插桩,这些探针在禁用时会带来非常小的开销。它们允许我们记录比我们在第二章中看到的函数跟踪器更多的信息,BSP 层。Tracepoints 被 Yocto 中的多个跟踪和分析工具使用。

本示例将解释如何独立于用户空间工具使用和定义静态跟踪点。

准备工作

静态探针可以通过自定义内核模块进行插桩,也可以通过事件跟踪基础设施插桩。启用内核中的任何跟踪功能将创建一个/sys/kernel/debug/tracing/目录;例如,前面在第二章中解释的函数跟踪功能,BSP 层

因此,在继续本示例之前,您需要如前所述配置 Linux 内核中的函数跟踪功能。

如何操作...

静态跟踪功能通过debugfs文件系统暴露。该接口提供的功能包括:

  • 列出事件

    您可以查看通过sysfs暴露的可用跟踪点,并按子系统目录进行排序,方法是:

    # ls /sys/kernel/debug/tracing/events/
    asoc          ftrace        migrate       rcu           spi
    block         gpio          module        regmap        sunrpc
    cfg80211      header_event  napi          regulator     task
    compaction    header_page   net           rpm           timer
    drm           irq           oom           sched         udp
    enable        jbd           power         scsi          vmscan
    ext3          jbd2          printk        signal        workqueue
    ext4          kmem          random        skb           writeback
    filemap       mac80211      raw_syscalls  sock
    
    

    或者在available_events文件中使用<subsystem>:<event>格式,通过以下命令:

    #  grep 'net'  /sys/kernel/debug/tracing/available_events 
    net:netif_rx
    net:netif_receive_skb
    net:net_dev_queue
    net:net_dev_xmit
    
    
  • 描述事件

    每个事件都有一个特定的打印格式,描述了日志事件中包含的信息,如下所示:

    #cat /sys/kernel/debug/tracing/events/net/netif_receive_skb/format
    name: netif_receive_skb
    ID: 378
    format:
     field:unsigned short common_type;  offset:0;  size:2; signed:0;
     field:unsigned char common_flags;  offset:2;  size:1; signed:0;
     field:unsigned char common_preempt_count;  offset:3; size:1;  signed:0;
     field:int common_pid;  offset:4;  size:4;  signed:1;
    
     field:void * skbaddr;  offset:8;  size:4;  signed:0;
     field:unsigned int len;	offset:12;  size:4;  signed:0;
     field:__data_loc char[] name;	offset:16;  size:4;  signed:0;
    
    print fmt: "dev=%s skbaddr=%p len=%u", __get_str(name), REC- >skbaddr, REC->len
    
    
  • 启用和禁用事件

    您可以通过以下几种方式启用或禁用事件:

    • 通过向事件enable文件中回显 0 或 1:

      # echo 1 > /sys/kernel/debug/tracing/events/net/netif_receive_skb/enable
      
      
    • 按子系统目录,可以启用或禁用该目录/子系统中的所有跟踪点:

      # echo 1 > /sys/kernel/debug/tracing/events/net/enable
      
      
    • 通过将唯一的跟踪点名称回显到set_event文件中:

      # echo netif_receive_skb >> /sys/kernel/debug/tracing/set_event
      
      

    请注意,追加操作>>用于不清除事件。

    • 通过向事件名称追加感叹号,可以禁用事件:

      # echo '!netif_receive_skb' >> /sys/kernel/debug/tracing/set_event
      
      
    • 事件也可以通过子系统来启用/禁用:

      # echo 'net:*' > /sys/kernel/debug/tracing/set_event
      
      
    • 要禁用所有事件:

      # echo > /sys/kernel/debug/tracing/set_event
      
      

    您还可以通过传递trace_event=<用逗号分隔的事件列表>内核命令行参数,从启动时启用跟踪点。

  • 将事件添加到跟踪缓冲区

    要在跟踪缓冲区上看到跟踪点的出现,请打开跟踪功能:

    # echo 1 > /sys/kernel/debug/tracing/tracing_on
    
    

    跟踪点事件集成到ftrace子系统中,因此,如果启用跟踪点,在运行跟踪器时,它会出现在跟踪中。请看以下命令:

    # cd /sys/kernel/debug/tracing
    # echo 1 > events/net/netif_receive_skb/enable
    # echo netif_receive_skb > set_ftrace_filter
    # echo function > current_tracer
    # cat trace
     <idle>-0     [000] ..s2  1858.542206: netif_receive_skb <-napi_gro_receive
     <idle>-0     [000] ..s2  1858.542214: netif_receive_skb: dev=eth0 skbaddr=dcb5bd80 len=168
    
    

它的工作原理...

跟踪点是通过使用TRACE_EVENT宏插入的。它在内核源代码中插入一个回调,该回调使用跟踪点参数作为参数进行调用。使用TRACE_EVENT宏添加的跟踪点允许ftrace或任何其他跟踪器使用它们。回调将跟踪插入调用跟踪器的环形缓冲区。

要将新的跟踪点插入到 Linux 内核中,需要定义一个具有特殊格式的新头文件。默认情况下,跟踪点内核文件位于include/trace/events中,但内核具有功能,可以将头文件放置在不同的路径中。这在定义内核模块中的跟踪点时非常有用。

要使用跟踪点,必须在插入跟踪点的任何文件中包含头文件,并且单个 C 文件必须定义CREATE_TRACE_POINT。例如,要扩展我们在前一章中看到的hello worldLinux 内核模块并添加跟踪点,可以将以下代码添加到meta-bsp-custom/recipes-kernel/hello-world-tracepoint/files/hello_world.c

#include <linux/module.h>
#include "linux/timer.h"
#define CREATE_TRACE_POINTS
#include "trace.h"

static struct timer_list hello_timer;

void hello_timer_callback(unsigned long data)
{
        char a[] = "Hello";
        char b[] = "World";
        printk("%s %s\n",a,b);
      /* Insert the static tracepoint */
 trace_log_dbg(a, b);
      /* Trigger the timer again in 8 seconds */
        mod_timer(&hello_timer, jiffies + msecs_to_jiffies(8000));
}

static int hello_world_init(void)
{
      /* Setup a timer to fire in 2 seconds */
        setup_timer(&hello_timer, hello_timer_callback, 0);
        mod_timer(&hello_timer, jiffies + msecs_to_jiffies(2000));
        return 0;
}

static void hello_world_exit(void)
{
      /* Delete the timer */
        del_timer(&hello_timer);
}

module_init(hello_world_init);
module_exit(hello_world_exit);

MODULE_LICENSE("GPL v2");

meta-bsp-custom/recipes-kernel/hello-world-tracepoint/files/trace.h中的跟踪点头文件将是:

#undef TRACE_SYSTEM
#define TRACE_SYSTEM log_dbg

#if !defined(_HELLOWORLD_TRACE) || defined(TRACE_HEADER_MULTI_READ)
#define _HELLOWORLD_TRACE

#include <linux/tracepoint.h>

TRACE_EVENT(log_dbg,
            TP_PROTO(char *a, char *b),
            TP_ARGS(a, b),
            TP_STRUCT__entry(
                    __string(a, a)
                    __string(b, b)),
            TP_fast_assign(
                    __assign_str(a, a);
                    __assign_str(b, b);),
            TP_printk("log_dbg: a %s b %s",
                      __get_str(a), __get_str(b))
        );
#endif

/* This part must be outside protection */
#undef TRACE_INCLUDE_PATH
#undef TRACE_INCLUDE_FILE
#define TRACE_INCLUDE_PATH .
#define TRACE_INCLUDE_FILE trace
#include <trace/define_trace.h>

meta-bsp-custom/recipes-kernel/hello-world-tracepoint/files/Makefile中的模块Makefile文件将如下所示:

obj-m   := hello_world.o
CFLAGS_hello_world.o    += -I$(src)

SRC := $(shell pwd)

all:
        $(MAKE) -C "$(KERNEL_SRC)" M="$(SRC)"

modules_install:
        $(MAKE) -C "$(KERNEL_SRC)" M="$(SRC)" modules_install

clean:
        rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c
        rm -f Module.markers Module.symvers modules.order
        rm -rf .tmp_versions Modules.symvers

请注意,突出显示的行包括当前文件夹在include文件的搜索路径中。

我们现在可以像在第二章的构建外部内核模块食谱中看到的那样,外部构建模块,BSP 层。对应的 Yocto 食谱包含在随书附带的源码中。以下是相应的代码:

$ cd /opt/yocto/fsl-community-bsp/sources/meta-bsp-custom/recipes- kernel/hello-world-tracepoint/files/
$ source /opt/poky/1.7.1/environment-setup-cortexa9hf-vfp-neon-poky- linux-gnueabi
$ KERNEL_SRC=/opt/yocto/linux-wandboard make

将生成的hello_world.ko模块复制到 Wandboard 的根文件系统后,您可以使用以下命令加载它:

# insmod hello_world.ko
Hello World

您现在可以在/sys/kernel/debug/tracing/events目录下看到一个新的log_dbg目录,其中包含一个log_dbg事件跟踪点,格式如下:

# cat /sys/kernel/debug/tracing/events/log_dbg/log_dbg/format
name: log_dbg
ID: 622
format:
 field:unsigned short common_type;       offset:0;       size:2; signed:0;
 field:unsigned char common_flags;       offset:2;       size:1; signed:0;
 field:unsigned char common_preempt_count;       offset:3;    size:1; signed:0;
 field:int common_pid;   offset:4;       size:4; signed:1;

 field:__data_loc char[] a;      offset:8;       size:4; signed:0;
 field:__data_loc char[] b;      offset:12;      size:4; signed:0;

print fmt: "log_dbg: a %s b %s", __get_str(a), __get_str(b)

然后你可以在 hello_timer_callback 函数上启用函数追踪器:

# cd /sys/kernel/debug/tracing
# echo 1 > events/log_dbg/log_dbg/enable
# echo 1 > /sys/kernel/debug/tracing/tracing_on
# cat trace
 <idle>-0     [000] ..s2    57.425040: log_dbg: log_dbg: a Hello b World

还有更多内容...

静态追踪点也可以进行过滤。当事件与设置的过滤器匹配时,事件会被保留,否则会被丢弃。没有过滤器的事件总是被保留。

例如,要为前面代码中插入的 log_dbg 事件设置匹配过滤器,你可以匹配 ab 变量:

# echo "a == \"Hello\"" > /sys/kernel/debug/tracing/events/log_dbg/log_dbg/filter

另见

使用动态内核追踪

kprobes 是一个内核调试工具,允许我们动态地插入几乎任何内核函数(除了 kprobe 本身)以非破坏性地收集调试和性能分析信息。一些架构会维护一个黑名单函数数组,这些函数无法通过 kprobe 进行探测,但在 ARM 架构中,该列表为空。

因为 kprobes 可以用来修改函数的数据和寄存器,所以它应该只在开发环境中使用。

有三种类型的探测器:

  • kprobes:这是内核探测器,可以插入到任何位置,若需要,可以在同一位置添加多个 kprobe

  • jprobe:这是插入内核函数入口点的跳转探测器,用于访问其参数。每个位置只能添加一个 jprobe

  • kretprobe:这是一个返回探测器,在函数返回时触发。此外,每个位置只能添加一个 kretprobe

它们被打包成一个内核模块,init 函数负责注册探测器,exit 函数负责注销探测器。

本教程将解释如何使用所有类型的动态探测器。

准备工作

要为 Linux 内核配置 kprobes 支持,你需要:

  • 定义 CONFIG_KPROBES 配置变量

  • 定义 CONFIG_MODULESCONFIG_MODULE_UNLOAD 以便可以使用模块来注册探测器。

  • 定义 CONFIG_KALLSYMSCONFIG_KALLSYMS_ALL(推荐)以便能够查找内核符号

  • 可选地,定义 CONFIG_DEBUG_INFO 配置变量,这样探测器可以作为从入口点的偏移插入到函数中。要找到插入点,你可以使用 objdump,如下所示的 do_sys_open 函数片段:

    arm-poky-linux-gnueabi-objdump -d -l vmlinux | grep do_sys_open
    8010bfa8 <do_sys_open>:
    do_sys_open():
    8010c034:       0a000036        beq     8010c114 <do_sys_open+0x16c>
    8010c044:       1a000031        bne     8010c110 <do_sys_open+0x168>
    

    kprobes API 定义在 kprobes.h 文件中,包括三种类型探测器的注册/注销以及启用/禁用函数,具体如下:

    #include <linux/kprobes.h>
    int register_kprobe(struct kprobe *kp);
    int register_jprobe(struct jprobe *jp)
    int register_kretprobe(struct kretprobe *rp);
    
    void unregister_kprobe(struct kprobe *kp);
    void unregister_jprobe(struct jprobe *jp);
    void unregister_kretprobe(struct kretprobe *rp);
    

    默认情况下,kprobe 探测器在注册时会被启用,除非传入 KPROBE_FLAG_DISABLED 标志。以下函数定义用于启用或禁用探测器:

    int disable_kprobe(struct kprobe *kp);
    int disable_kretprobe(struct kretprobe *rp);
    int disable_jprobe(struct jprobe *jp);
    
    int enable_kprobe(struct kprobe *kp);
    int enable_kretprobe(struct kretprobe *rp);
    int enable_jprobe(struct jprobe *jp);
    

    注册的 kprobe 探针可以通过 debugfs 列出:

    $ cat /sys/kernel/debug/kprobes/list
    

    它们可以全局启用或禁用:

    $ echo 0/1 > /sys/kernel/debug/kprobes/enabled
    

如何操作...

在注册时,kprobe 探针会在被探测指令的起始位置放置一个断点(如果经过优化,则为跳转)指令。当断点被触发时,发生陷阱,寄存器被保存,控制权转交给 kprobes,并调用前处理程序。接着,它会单步执行断点,并调用后处理程序。如果发生故障,则会调用故障处理程序。如果需要,处理程序可以为 NULL。

kprobe 探针可以插入到函数符号或地址中,使用偏移字段,但不能同时插入两者。

注意

有时,kprobe 对于调试某些问题仍然过于干扰,因为它会减慢函数执行,可能影响调度,并且在中断上下文中调用时可能会引发问题。

例如,要在 open 系统调用中放置一个 kprobe 探针,我们可以使用 meta-bsp-custom/recipes-kernel/open-kprobe/files/kprobe_open.c 自定义模块:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>

static struct kprobe kp = {
  .symbol_name  = "do_sys_open",
};

static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
  pr_info("pre_handler: p->addr = 0x%p, lr = 0x%lx,"
    " sp = 0x%lx\n",
  p->addr, regs->ARM_lr, regs->ARM_sp);

  /* A dump_stack() here will give a stack backtrace */
  return 0;
}

static void handler_post(struct kprobe *p, struct pt_regs *regs,
      unsigned long flags)
{
  pr_info("post_handler: p->addr = 0x%p, status = 0x%lx\n",
    p->addr, regs->ARM_cpsr);
}

static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
  pr_info("fault_handler: p->addr = 0x%p, trap #%dn",
    p->addr, trapnr);
  /* Return 0 because we don't handle the fault. */
  return 0;
}

static int kprobe_init(void)
{
  int ret;
  kp.pre_handler = handler_pre;
  kp.post_handler = handler_post;
  kp.fault_handler = handler_fault;

  ret = register_kprobe(&kp);
  if (ret < 0) {
    pr_err("register_kprobe failed, returned %d\n", ret);
    return ret;
  }
  pr_info("Planted kprobe at %p\n", kp.addr);
  return 0;
}

static void kprobe_exit(void)
{
  unregister_kprobe(&kp);
  pr_info("kprobe at %p unregistered\n", kp.addr);
}

module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

我们通过 Yocto 配方编译它,正如 第二章 中 构建外部内核模块 配方所解释的那样,BSP 层。以下是 meta-bsp-custom/recipes-kernel/open-kprobe/open-kprobe.bb Yocto 配方文件的代码:

SUMMARY = "kprobe on do_sys_open kernel module."
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/GPL- 2.0;md5=801f80980d171dd6425610833a22dbe6"

inherit module

PV = "0.1"

SRC_URI = " \
    file://kprobe_open.c \
    file://Makefile \
"

S = "${WORKDIR}"

meta-bsp-custom/recipes-kernel/open-kprobe/files/Makefile 文件内容如下:

obj-m  := kprobe_open.o

SRC := $(shell pwd)

all:
  $(MAKE) -C "$(KERNEL_SRC)" M="$(SRC)"

modules_install:
  $(MAKE) -C "$(KERNEL_SRC)" M="$(SRC)" modules_install

clean:
  rm -f *.o *~ core .depend .*.cmd *.ko *.mod.c
  rm -f Module.markers Module.symvers modules.order
  rm -rf .tmp_versions Modules.symvers

将其复制到运行相同内核的目标上,并使用以下命令加载:

$ insmod kprobe_open.ko
Planted kprobe at 8010da84

当打开文件时,我们现在可以看到处理程序在控制台中打印出来:

pre_handler: p->addr = 0x8010da84, lr = 0x8010dc34, sp = 0xdca75f98
post_handler: p->addr = 0x8010da84, status = 0x80070013

还有更多内容...

jprobe 探针是通过 kprobe 实现的。它会在给定的符号或地址处设置一个断点(但必须是函数的第一条指令),并复制一部分堆栈。当触发时,它会跳转到处理程序,使用与被探测函数相同的寄存器和堆栈。处理程序必须与被探测函数具有相同的参数列表和返回类型,并在返回之前调用 jprobe_return(),以将控制权传回 kprobes。然后恢复原始堆栈和 CPU 状态,并调用被探测的函数。

以下是 meta-bsp-custom/recipes-kernel/open-jprobe/files/jprobe_open.c 文件中 open 系统调用的 jprobe 示例:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
static long jdo_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
  pr_info("jprobe: dfd = 0x%x, filename = 0xs "
    "flags = 0x%x mode umode %x\n", dfd, filename, flags, mode);

  /* Always end with a call to jprobe_return(). */
  jprobe_return();
  return 0;
}

static struct jprobe my_jprobe = {
  .entry        = jdo_sys_open,
  .kp = {
    .symbol_name  = "do_sys_open",
  },
};

static int jprobe_init(void)
{
  int ret;

  ret = register_jprobe(&my_jprobe);
  if (ret < 0) {
    pr_err("register_jprobe failed, returned %d\n", ret);
    return -1;
  }
  pr_info("Planted jprobe at %p, handler addr %p\n",
        my_jprobe.kp.addr, my_jprobe.entry);
  return 0;
}

static void jprobe_exit(void)
{
  unregister_jprobe(&my_jprobe);
  pr_info("jprobe at %p unregistered\n", my_jprobe.kp.addr);
}

module_init(jprobe_init)
module_exit(jprobe_exit)
MODULE_LICENSE("GPL");

kretprobe 探针会在给定的符号或函数地址处设置一个 kprobe,当触发时,会用一个 trampoline(通常是一个 nop 指令)替换返回地址,kprobe 在此注册。当被探测函数返回时,trampoline 上的 kprobe 探针会被触发,调用返回处理程序,并在恢复执行之前设置回原始返回地址。

以下是 meta-bsp-custom/recipes-kernel/open-kretprobe/files/kretprobe_open.c 文件中 open 系统调用的 kretprobe 探针示例:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/ktime.h>
#include <linux/limits.h>
#include <linux/sched.h>

/* per-instance private data */
struct my_data {
  ktime_t entry_stamp;
};

static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
  struct my_data *data;

  if (!current->mm)
    return 1;  /* Skip kernel threads */

  data = (struct my_data *)ri->data;
  data->entry_stamp = ktime_get();
  return 0;
}

static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
  int retval = regs_return_value(regs);
  struct my_data *data = (struct my_data *)ri->data;
  s64 delta;
  ktime_t now;

  now = ktime_get();
  delta = ktime_to_ns(ktime_sub(now, data->entry_stamp));
  pr_info("returned %d and took %lld ns to execute\n",
        retval, (long long)delta);
  return 0;
}

static struct kretprobe my_kretprobe = {
  .handler    = ret_handler,
  .entry_handler    = entry_handler,
  .data_size    = sizeof(struct my_data),
  .maxactive    = 20,
};

static int kretprobe_init(void)
{
  int ret;

  my_kretprobe.kp.symbol_name = "do_sys_open";
  ret = register_kretprobe(&my_kretprobe);
  if (ret < 0) {
    pr_err("register_kretprobe failed, returned %d\n",
        ret);
    return -1;
}
  pr_info("Planted return probe at %s: %p\n",
  my_kretprobe.kp.symbol_name,            my_kretprobe.kp.addr);
  return 0;
}

static void kretprobe_exit(void)
{
  unregister_kretprobe(&my_kretprobe);
  pr_info("kretprobe at %p unregistered\n",
      my_kretprobe.kp.addr);

  /* nmissed > 0 suggests that maxactive was set too low. */
  pr_info("Missed probing %d instances of %s\n",
    my_kretprobe.nmissed, my_kretprobe.kp.symbol_name);
}

module_init(kretprobe_init)
module_exit(kretprobe_exit)
MODULE_LICENSE("GPL");

高亮显示的maxactive变量是kretprobe探针中用于返回地址的预留存储数量,默认情况下,它等于 CPU 的数量(或者在抢占系统中为 CPU 数量的两倍,最多为 10)。如果maxactive值过低,一些探针将会被漏掉。

完整的示例,包括 Yocto 食谱,可以在随书提供的源代码中找到。

另见

使用动态内核事件

尽管动态追踪是一个非常有用的功能,但自定义内核模块并不是一个用户友好的接口。幸运的是,Linux 内核通过支持kprobe事件进行了扩展,允许我们使用debugfs接口设置kprobes探针。

准备就绪

要使用此功能,我们需要使用CONFIG_KPROBE_EVENT配置变量配置我们的内核。

如何操作...

debugfs接口通过/sys/kernel/debug/tracing/kprobe_events文件添加探针。例如,要将名为example_probekprobe添加到do_sys_open函数中,可以执行以下命令:

# echo 'p:example_probe do_sys_open dfd=%r0 filename=%r1 flags=%r2 mode=%r3' > /sys/kernel/debug/tracing/kprobe_events

探针将根据函数的声明参数列表,打印该函数的参数列表,如下所示:

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode);

然后,您可以通过 sysfs 管理kprobes,具体如下:

  • 查看所有已注册的探针:

    # cat /sys/kernel/debug/tracing/kprobe_events
    p:kprobes/example_probe do_sys_open dfd=%r0 filename=%r1 flags=%r2 mode=%r3
    
    
  • 要打印探针格式:

    # cat /sys/kernel/debug/tracing/events/kprobes/example_probe/format
    name: example_probe
    ID: 1235
    format:
     field:unsigned short common_type;       offset:0;      size:2; signed:0;
     field:unsigned char common_flags;       offset:2;      size:1; signed:0;
     field:unsigned char common_preempt_count;       offset:3;       size:1; signed:0;
     field:int common_pid;   offset:4;       size:4; signed:1;
     field:unsigned long __probe_ip; offset:8;       size:4; signed:0;
     field:u32 dfd;  offset:12;      size:4; signed:0;
     field:u32 filename;     offset:16;      size:4; signed:0;
     field:u32 flags;        offset:20;      size:4; signed:0;
     field:u32 mode; offset:24;      size:4; signed:0;
    print fmt: "(%lx) dfd=%lx filename=%lx flags=%lx mode=%lx", REC->__probe_ip, REC->dfd, REC->filename, REC->flags, REC- >mode
    
    
  • 要启用探针,请使用以下命令:

    # echo 1 > /sys/kernel/debug/tracing/events/kprobes/example_probe/enable
    
    
  • 要查看探针输出,请在tracetrace_pipe文件中查看:

    # cat /sys/kernel/debug/tracing/trace
    # tracer: nop
    #
    # entries-in-buffer/entries-written: 59/59   #P:4
    #
    #                              _-----=> irqs-off
    #                             / _----=> need-resched
    #                            | / _---=> hardirq/softirq
    #                            || / _--=> preempt-depth
    #                            ||| /     delay
    #           TASK-PID   CPU#  ||||    TIMESTAMP  FUNCTION
    #              | |       |   ||||       |         |
     sh-737   [000] d...  1610.378856: example_probe: (do_sys_open+0x0/0x184) dfd=ffffff9c filename=f88488 flags=20241 mode=16
     sh-737   [000] d...  1660.888921: example_probe: (do_sys_open+0x0/0x184) dfd=ffffff9c filename=f88a88 flags=20241 mode=16
    
    
  • 清除探针(在禁用后):

    # echo '-:example_probe' >> /sys/kernel/debug/tracing/kprobe_events
    
    
  • 清除所有探针:

    # echo > /sys/kernel/debug/tracing/kprobe_events
    
    
  • 检查命中和漏掉事件的数量:

    # cat /sys/kernel/debug/tracing/kprobe_profile
    example_probe                             78               0
    
    

    格式如下:

    <event name> <hits> <miss-hits>
    
    

它是如何工作的...

要设置探针,我们使用以下语法:

<type>:<event name> <symbol> <fetch arguments>

让我们解释一下提到的每个参数:

  • type:这可以是p表示kprobe,或者r表示返回探针。

  • event name:这是可选的,格式为<group/event>。如果省略了组名,默认使用kprobes,如果省略了事件名,则根据符号自动生成事件名。给定事件名时,它将在/sys/kernel/debug/tracing/events/kprobes/下创建一个目录,内容如下:

    • id:这是探针事件的 ID

    • filter:这指定了用户的过滤规则

    • format:这是探针事件的格式

    • enabled:用于启用或禁用探针事件

  • symbol:这是符号名称,后跟一个可选的偏移量,或者是要插入探针的内存地址。

  • fetch arguments:这些是可选的,表示要提取的信息,最多可提取 128 个参数。它们具有以下格式:

    <name>=<offset>(<argument>):<type>
    
    

    让我们解释一下提到的每个参数:

    • name:这设置了参数名称

    • offset:这会向地址参数添加一个偏移量

    • argument:这可以是以下格式:

      %<register>:这会获取指定的寄存器。对于 ARM 来说,这些是:

      r0 to r10

      fp

      ip

      sp

      lr

      pc

      cpsr

      ORIG_r0

    @<address>:这会获取指定内核地址的内存

    @<symbol><offset>:这会获取指定符号和可选偏移量的内存

    $stack:这会获取栈地址

    $stack<N>:这会获取栈中的第n个条目

    对于返回探针,我们有:

    $retval:这会获取返回值

    • type:此选项设置kprobe用于访问内存的参数类型,以下是可用选项:

      u8, u16, u32, u64,适用于无符号类型

      s8, s16, s32, s64,适用于有符号类型

      string,适用于以空字符结尾的字符串

      位域,格式如下:

      b<bit-width>@<bit-offset>/<container-size>
      

还有更多...

当前版本的 Linux 内核(从 v3.14 开始)也支持用户空间探针事件(uprobes),其接口与kprobes事件相似。

探索 Yocto 的跟踪和性能分析工具

跟踪和性能分析工具用于提高应用程序和系统的性能、效率和质量。用户空间的跟踪和性能分析工具利用 Linux 内核提供的性能计数器以及静态和动态跟踪功能,正如我们在前面的示例中看到的那样。

正在准备

跟踪使我们能够记录应用程序的活动,以便分析、优化和修正其行为。

Yocto 提供了包括以下几种跟踪工具:

  • trace-cmd:这是ftrace内核子系统的命令行接口,以及kernelshark,一个图形化界面,用于与 trace-cmd 配合使用。

  • perf:这是一个起源于 Linux 内核的工具,它提供了命令行接口,用于性能计数器事件子系统。此后,它已扩展并添加了其他几种跟踪机制。

  • blktrace:这是一个提供块层输入/输出信息的工具。

  • Linux Trace Toolkit Next Generation (LTTng):这是一个工具,用于对 Linux 内核、应用程序和库进行相关联的跟踪。Yocto 还包括babeltrace,一个将跟踪数据转换为人类可读日志的工具。

  • SystemTap:这是一个用于动态插桩 Linux 内核的工具。

性能分析是指一组技术,用于衡量应用程序消耗的资源和执行应用程序所需的时间。然后,这些数据被用来改善应用程序的性能并进行优化。一些前面提到的工具,如 perf 和 SystemTap,已经发展成为强大的跟踪和性能分析工具。

除了已列出的跟踪工具外,它们也可以用于性能分析,Yocto 还提供了其他几种性能分析工具:

  • OProfile:这是一个 Linux 的统计性能分析工具,它以低开销分析所有正在运行的代码。

  • Powertop:这是一个用于分析系统功耗和电源管理的工具。

  • Latencytop:这是一个用于分析系统延迟的工具。

  • Sysprof:该工具为 X11 图形镜像中的 Intel 架构提供,不能在 ARM 架构上使用。

如何操作...

这些工具可以单独添加到目标镜像中,或者通过tools-profile功能进行添加。为了使用这些工具,我们还需要在应用程序中包含调试信息。为此,我们应使用包的-dbg版本,或者更好地配置 Yocto,使得通过dbg-pkgs镜像功能生成调试信息。要将这两个功能添加到镜像中,请将以下内容添加到项目的conf/local.conf文件中:

EXTRA_IMAGE_FEATURES = "tools-profile dbg-pkgs"

目标镜像的-sdk版本已经添加了这些功能。

还有更多...

除了这些工具,Yocto 还提供了在 Linux 系统上可用的标准监控工具。一些示例如下:

  • htop:这个工具包含在meta-oe层中,提供进程监控功能。

  • iotop:这个工具也包含在meta-oe层中,它提供按进程分类的块设备 I/O 统计信息。

  • procps:这个工具包在 Poky 中可用,包含以下工具:

    • ps:这个工具用于列出并提供进程状态信息。

    • vmstat:用于虚拟内存统计。

    • uptime:用于负载平均值监控。

    • free:用于监视内存使用情况。记得考虑内核缓存。

    • slabtop:该工具提供内核 slab 分配器的内存使用统计信息。

  • sysstat:这个工具包在 Poky 中可用,包含以下工具:

    • pidstat:另一个用于进程统计信息的工具。

    • iostat:提供块 I/O 统计信息。

    • mpstat:该工具提供多处理器统计信息。

Yocto 还提供以下网络工具:

  • tcpdump:这个网络工具包含在meta-networking层的meta-openembedded中。它用于捕获和分析网络流量。

  • netstat:这是 Poky 中net-tools包的一部分。它提供网络协议统计信息。

  • ss:这个工具包含在 Poky 的iproute2包中。它提供套接字统计信息。

使用 perf 进行跟踪和分析

perf Linux 工具可以使用硬件和软件性能计数器事件,以及静态和动态内核跟踪点来对 Linux 内核进行仪器化。为此,它使用我们在前面的食谱中看到的内核功能,提供一个通用接口来管理它们。

该工具可用于调试、故障排除、优化和衡量应用程序、工作负载或整个系统,涵盖处理器、内核和应用程序。perf 可能是可用于 Linux 系统的最完整的跟踪和分析工具。

准备工作

perf 源代码是 Linux 内核的一部分。要在系统中包含 perf,请将以下内容添加到conf/local.conf文件中:

IMAGE_INSTALL_append = " perf"

perf 也是tools-profile镜像功能的一部分,因此你也可以通过以下方式添加它:

EXTRA_IMAGE_FEATURES += "tools-profile"

perf 也包含在-sdk镜像中。

为了最大化利用此工具,我们需要在用户空间应用程序、库以及 Linux 内核中都有符号。为此,我们需要避免剥离二进制文件,方法是在 conf/local.conf 配置文件中添加以下内容:

INHIBIT_PACKAGE_STRIP = "1"

另外,建议通过添加以下内容来增加应用程序的调试信息:

EXTRA_IMAGE_FEATURES += "dbg-pkgs"

默认情况下,调试信息会放在与其对应的二进制文件相同位置的 .debug 目录中。但是 perf 需要一个中央位置来查找所有调试信息。因此,为了使用 perf 理解的结构配置调试信息,我们还需要在 conf/local.conf 配置文件中添加以下内容:

PACKAGE_DEBUG_SPLIT_STYLE = 'debug-file-directory'

最后,使用 CONFIG_DEBUG_INFO 配置变量配置 Linux 内核以包含调试信息,使用 CONFIG_KALLSYMS 将调试符号添加到内核中,使用 CONFIG_FRAME_POINTER 以便能够看到完整的堆栈跟踪。

注意

正如我们在 使用内核性能计数器 章节中看到的,我们还需要将 maxcpus=1(或 maxcpus=0 禁用 SMP)传递给 Linux 内核,以便使用 i.MX6 PMU,因为 PMU 中断在所有核心之间共享。此外,为了在 i.MX6 处理器上使用 PMU,SoC 在上电复位后需要至少 4 个 JTAG 时钟周期。这在 ERR006259 错误报告中有文档说明。

在撰写本文时,Yocto 1.7 的 meta-fsl-arm 层禁用了部分 perf 功能。为了能够跟随接下来的示例,请从 meta-fsl-arm 层的 /opt/yocto/fsl-community-bsp/sources/meta-fsl-arm/conf/machine/include/imx-base.inc 文件中删除以下行:

-PERF_FEATURES_ENABLE = ""

更新的 Yocto 版本默认会包括此功能。

如何操作...

Perf 可以用于提供特定工作负载的默认事件统计信息:

# perf stat <command>

例如,单次 ping 会提供以下输出:

# perf stat ping -c 1 192.168.1.1
PING 192.168.1.1 (192.168.1.1): 56 data bytes
64 bytes from 192.168.1.1: seq=0 ttl=64 time=6.489 ms

--- 192.168.1.1 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 6.489/6.489/6.489 ms

 Performance counter stats for 'ping -c 1 192.168.1.1':

 8.984333 task-clock                #    0.360 CPUs utilized
 15 context-switches          #    0.002 M/sec
 0 cpu-migrations            #    0.000 K/sec
 140 page-faults               #    0.016 M/sec
 3433188 cycles                    #    0.382 GHz
 123948 stalled-cycles-frontend   #    3.61% frontend cycles idle 
 418329 stalled-cycles-backend    #   12.18% backend  cycles idle 
 234497 instructions              #    0.07  insns per cycle 
 #    1.78  stalled cycles per insn
 22649 branches                  #    2.521 M/sec
 8123 branch-misses             #   35.86% of all branches 

 0.024962333 seconds time elapsed

如果我们只对特定的一组事件感兴趣,可以使用 -e 选项指定我们想要输出信息的事件。

我们还可以采样数据并存储,以便后续分析:

# perf record <command>

更好的是,我们可以使用 -g 选项添加堆栈回溯:

# perf record -g -- ping -c 1 192.168.1.1

结果将存储在 perf.data 文件中,我们接着可以用以下命令分析:

# perf report

其输出可以在以下屏幕截图中看到:

如何操作...

函数的顺序可以通过 --sort 选项自定义。

我们可以看到 perf 如何解析用户空间和内核符号。Perf 会从 /boot 目录下的 Linux 内核 ELF 文件读取内核符号。如果它存储在非标准位置,我们可以选择通过 -k 选项传递其位置。如果未找到,它将回退到使用 /proc/kallsyms,这是 Linux 内核在启用 CONFIG_KALLSYMS 配置变量时将内核符号导出到用户空间的地方。

注意

如果 perf 报告中没有显示内核符号,可能是因为 ELF 文件与正在运行的内核不匹配。你可以尝试重命名它,看看使用 /proc/kallsyms 是否有效。

此外,为了获取完整的回溯信息,应用程序需要通过使用DEBUG_BUILD配置变量进行调试优化编译,正如我们在本章之前所看到的那样。

默认情况下,Perf 使用 newt 界面(TUI),它需要 expand 工具,这是 coreutils 的一部分。如果你的根文件系统中没有包含 coreutils,你可以请求仅文本输出,如下所示:

# perf report –stdio

执行上述命令后,我们得到以下输出:

如何操作...

我们可以通过以下列显示所有调用的函数:

  • 开销:表示与该函数相关的采样数据的百分比。

  • 命令:指传递给 perf record 的命令名称。

  • 共享对象:表示 ELF 图像名称(内核会显示 kernel.kallsyms)。

  • 特权级别:它有以下几种模式:

    • 表示用户模式

    • k 表示内核模式

    • g 表示虚拟化来宾内核

    • u 表示虚拟化主机用户空间

    • H 表示虚拟机监控器

  • 符号:这是解析后的符号名称。

在 TUI 界面中,我们可以按下回车键选择一个函数名称进入子菜单,系统会给出如下输出:

如何操作...

通过这点,我们可以,例如,如下截图所示注释代码:

如何操作...

如果使用文本模式,我们还可以通过以下命令获得带注释的输出:

# perf annotate -d <command>

Perf 还可以进行系统范围的分析,而不仅仅关注特定的工作负载。例如,要监控系统五秒钟,我们可以执行以下命令:

# perf stat -a sleep 5
Performance counter stats for 'sleep 5':
 5006.660002 task-clock                #    1.000 CPUs utilized[100.00%]
 324 context-switches          #    0.065 K/sec [100.00%]
 0 cpu-migrations            #    0.000 K/sec [100.00%]
 126 page-faults               #    0.025 K/sec
 12200175 cycles                    #    0.002 GHz [100.00%]
 2844703 stalled-cycles-frontend   #   23.32% frontend cycles idle    [100.00%]
 9152564 stalled-cycles-backend    #   75.02% backend  cycles idle    [100.00%]
 4645466 instructions              #    0.38  insns per cycle 
 #    1.97  stalled cycles per insn [100.00%]
 479051 branches                  #    0.096 M/sec [100.00%]
 222903 branch-misses             #   46.53% of all branches 

 5.006115001 seconds time elapsed

或者,要采样系统五秒钟,我们可以执行以下命令:

# perf record -a -g -- sleep 5

使用系统范围测量时,命令仅作为测量时长。为此,sleep 命令不会消耗额外的周期。

它是如何工作的...

Perf 工具提供了系统中用户和内核事件的统计信息。它可以在两种模式下进行仪器化:

  • 事件计数perf stat):在内核上下文中计数事件并在结束时打印统计信息。它的开销最小。

  • 事件采样perf record):此命令将在给定的采样周期内将收集到的数据写入文件。然后可以将数据读取为分析数据(perf report)或跟踪数据(perf script)。将数据收集到文件可能会消耗较多资源,并且文件大小会迅速增长。

默认情况下,perf 会统计给定命令中所有线程的事件,包括子进程,直到命令执行完成或被中断。

运行 perf 的通用方式如下:

perf stat|record [-e <comma separated event list> --filter '<expr>'] [-o <filename>] [--] <command> [<arguments>]

让我们详细解释前面的代码:

  • e:指定要使用的事件列表,而不是默认的事件集合。也可以指定事件过滤器,其语法可在 Linux 内核源代码文档中的 Documentation/trace/events.txt 找到。

  • o:指定输出文件名,默认是perf.data

  • --:当命令需要参数时,用作分隔符。

它还可以通过传递-p <pid>选项来启动或采样正在运行的进程。

我们可以通过执行以下命令来获取所有可用事件的列表:

# perf list

或使用以下命令在特定子系统上运行:

# perf list '<subsystem>:*'

你还可以通过使用r<event>事件直接访问原始 PMU 事件,例如,要读取 ARM 核心上的数据缓存未命中的情况:

# perf stat -e r3 sleep 5

除非另有指定,否则 perf record 会以平均 1000 Hz 的频率采样硬件事件,但可以通过-F <freq>参数修改采样频率。每次发生时,跟踪点都会被计数。

阅读跟踪数据

Perf 记录采样并将跟踪数据存储到文件中。原始时间戳跟踪数据可以通过以下命令查看:

# perf script

执行命令后,我们得到以下输出:

阅读跟踪数据

正如我们所看到的,我们可以使用 perf 报告查看格式化的采样数据进行剖析分析,但我们也可以生成 python 脚本,然后修改它们以更改数据的展示方式,方法是运行以下代码行:

# perf script -g python

这将生成一个perf-script.py脚本,内容如下:

阅读跟踪数据

要运行脚本,请使用以下命令:

# perf script -s perf-script.py

你需要在目标镜像中安装perf-python包。你可以通过以下命令将其添加到镜像中:

IMAGE_INSTALL_append = " perf-python"

现在你将得到类似于之前perf脚本的输出。但现在你可以修改 python 代码中的打印语句,以后处理采样数据,满足你的特定需求。

还有更多...

Perf 可以使用动态事件扩展事件列表,将其添加到任何可以放置kprobe的位置。为此,需要配置内核以支持kprobeuprobe(如果可用),如前面使用动态内核事件一节所示。

要在特定函数中添加探测点,执行以下命令:

# perf probe --add "tcp_sendmsg"
Added new event:
 probe:tcp_sendmsg    (on tcp_sendmsg)

现在你可以在所有 perf 工具中使用它,比如剖析文件下载:

# perf record -e probe:tcp_sendmsg -a -g -- wget http://downloads.yoctoproject.org/releases/yocto/yocto- 1.7.1/RELEASENOTES
Connecting to downloads.yoctoproject.org (198.145.29.10:80)
RELEASENOTES         100% |**************************************************************************************| 11924   0:00:00 ETA
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.025 MB perf.data (~1074 samples) ]

然后,你可以执行以下命令查看剖析数据:

# perf report

然后你会得到以下输出:

还有更多...

注意

你可能需要在目标系统中配置 DNS 服务器,以便wget命令正常工作,如前面的代码所示。要使用 Google 的公共 DNS 服务器,你可以将以下内容添加到/etc/resolv.conf文件中:

nameserver 8.8.8.8
nameserver 8.8.4.4

然后你可以通过以下命令删除探测:

# perf probe --del tcp_sendmsg
/sys/kernel/debug//tracing/uprobe_events file does not exist - please rebuild kernel with CONFIG_UPROBE_EVENT.
Removed event: probe:tcp_sendmsg

剖析图表

可以使用 perf 时间图可视化系统行为。要收集数据,请运行:

# perf timechart record -- <command> <arguments>

并使用以下命令将其转换为svg文件:

# perf timechart

使用 perf 作为 strace 替代品

Perf 可以作为 strace 的替代品,但具有更小的开销,使用以下语法:

# perf trace record <command>

然而,Yocto 中 perf 的食谱目前并没有构建此支持。我们可以在编译日志中看到缺失的库:

Makefile:681: No libaudit.h found, disables 'trace' tool, please install audit-libs-devel or libaudit-dev

另见

使用 SystemTap

SystemTap 是一个 GPLv2 许可证的系统级工具,它允许您从正在运行的 Linux 系统收集跟踪和分析数据。用户编写systemtap脚本,然后将其编译为一个 Linux 内核模块,该模块与它将要运行的内核源代码链接。

脚本设置事件和处理程序,当指定的事件触发时,内核模块会调用这些处理程序。为此,它使用内核中的kprobesuprobes(如果可用)接口,正如我们在使用动态内核事件配方中看到的那样。

准备就绪

要使用 SystemTap,我们需要通过特定添加它到目标镜像中,例如:

IMAGE_INSTALL_append = " systemtap"

我们还可以通过使用tools-profile镜像功能或-sdk镜像来添加它。

我们还需要在目标上运行一个 SSH 服务器。-sdk镜像中已经包含了这个功能;如果没有,我们可以通过以下方式将其添加到我们的镜像中:

EXTRA_IMAGE_FEATURES += "ssh-server-openssh"

我们还需要通过配置CONFIG_DEBUG_INFO变量来编译内核,以包含调试信息、性能事件计数器和kprobes,如前面配方中所述。

如何操作...

要在 Yocto 系统上使用 systemtap,我们需要在主机上运行 crosstap 工具,传递给它要运行的systemtap脚本。例如,要运行sys_open.stp示例脚本,我们可以运行以下代码:

probe begin
{
        print("Monitoring starts\n")
        printf("%6s %6s %16s\n", "UID", "PID", "NAME");
}

probe kernel.function("sys_open")
{
          printf("%6d %6d %16s\n", uid(), pid(), execname());
}

probe timer.s(60)
{
        print("Monitoring ends\n")
        exit()
}

我们将运行以下命令:

$ source setup-environment wandboard-quad
$ crosstap root@<target_ip> sys_open.stp

Yocto 不支持在目标上运行脚本,因为这需要在目标上构建模块,而这是未经测试的。

它是如何工作的...

SystemTap 脚本使用其自己的类似 C/awk 的语言编写。它们使我们能够通过在不同位置对内核代码进行插桩,来跟踪事件,例如:

  • 系统 Tap 会话的开始和结束

  • 内核和用户空间函数的入口、返回或特定偏移

  • 定时器事件

  • 性能硬件计数事件

它们还使我们能够提取数据,例如:

  • 线程、进程或用户 ID

  • 当前 CPU

  • 进程名称

  • 时间

  • 局部变量

  • 内核和用户空间回溯

此外,SystemTap 还提供了分析收集到的数据的能力,并且不同的探针可以协同工作。SystemTap 包括大量示例脚本,并提供一个框架来创建可以共享的脚本库。这些 tapsets 是默认安装的,并可以通过用户自己的脚本扩展。当脚本中未定义某个符号时,SystemTap 将搜索 tapset 库以查找该符号。

另见

使用 OProfile

OProfile 是一个统计分析工具,采用 GNU GPL 许可证发布。Yocto 1.7 版本中包含的版本是一个系统范围的分析器,它使用传统的分析模式,通过内核模块采样硬件性能计数器数据,并通过用户空间守护进程将其写入文件。更新的 Yocto 版本使用性能事件子系统,我们在使用内核的性能计数器食谱中介绍过,因此它们可以分析进程和工作负载。

Yocto 1.7 中包含的版本由一个内核模块、一个用于收集样本数据的用户空间守护进程和几个用于分析捕获数据的分析工具组成。

本食谱将重点介绍在 1.7 版本 Yocto 中包含的 OProfile 版本。

准备工作

要将 OProfile 包含到您的系统中,请将以下内容添加到conf/local.conf文件中:

 IMAGE_INSTALL_append += " oprofile"

OProfile 也是tools-profile镜像功能的一部分,因此您也可以通过以下方式添加它:

EXTRA_IMAGE_FEATURES += "tools-profile"

OProfile 也包含在-sdk镜像中。

除非需要注释结果,否则 OProfile 不需要应用程序中的调试符号。对于调用图分析,二进制文件必须包含堆栈帧信息,因此应该通过在conf/local.conf文件中设置DEBUG_BUILD变量来使用调试优化构建:

DEBUG_BUILD = "1"

要构建内核驱动程序,请配置 Linux 内核以支持分析,设置CONFIG_PROFILING,并将CONFIG_OPROFILE配置变量设置为构建 OProfile 模块。

OProfile 使用 SoC 中的硬件计数器支持,但它也可以在基于定时器的模式下工作。要使用定时器模式,您需要将oprofile.timer=1内核参数传递给 Linux 内核,或者使用以下命令加载 OProfile 模块:

# modprobe oprofile timer=1

注意

由于 OProfile 依赖于 i.MX6 性能计数器,我们仍然需要以maxcpus=1启动才能正常工作。这限制了 i.MX6 SoC 中的分析只能在一个核心上进行。

如何操作...

要分析单个 ping,请按以下步骤启动分析会话:

# opcontrol --start --vmlinux=/boot/vmlinux --callgraph 5
Using 2.6+ OProfile kernel interface.
Reading module info.
Using log file /var/lib/oprofile/samples/oprofiled.log
Daemon started.
Profiler running.

然后运行需要分析的工作负载,例如,一个简单的 ping:

# ping -c 1 192.168.1.1
PING 192.168.1.1 (192.168.1.1): 56 data bytes
64 bytes from 192.168.1.1: seq=0 ttl=64 time=5.421 ms

--- 192.168.1.1 ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 5.421/5.421/5.421 ms

停止数据收集的命令如下:

 # opcontrol --stop

提示

如果内核镜像名称包含特殊字符,我们会遇到解析错误。为了避免这种情况,我们可以使用符号链接,如下所示:

# ln -s /boot/vmlinux-3.10.17-1.0.2- wandboard+gbe8d6872b5eb /boot/vmlinux

另外,如果您看到以下错误:

Count 100000 for event CPU_CYCLES is below the minimum 1500000

您需要将CPU_CYCLES事件的重置计数更改为该最小值,命令如下:

# opcontrol --setup --event=CPU_CYCLES:1500000

然后可以使用以下命令查看收集到的数据:

# opreport -f
Using /var/lib/oprofile/samples/ for samples directory.
CPU: ARM Cortex-A9, speed 996000 MHz (estimated)
Counted CPU_CYCLES events (CPU cycle) with a unit mask of 0x00 (No unit mask) count 1500000
CPU_CYCLES:150...|
 samples|      %|
------------------
 401 83.0228 /boot/vmlinux-3.10.17-1.0.2-wandboard+gbe8d6872b5eb
 31  6.4182 /bin/bash
 28  5.7971 /lib/libc-2.20.so
 18  3.7267 /lib/ld-2.20.so
 3  0.6211 /usr/bin/oprofiled
 1  0.2070 /usr/bin/ophelp
 1  0.2070 /usr/sbin/sshd

以下是带有调用图和符号的输出摘录:

# opreport -cl
Using /var/lib/oprofile/samples/ for samples directory.
warning: [heap] (tgid:790 range:0x3db000-0x4bc000) could not be found.
warning: [stack] (tgid:785 range:0x7ee11000-0x7ee32000) could not be found.
CPU: ARM Cortex-A9, speed 996000 MHz (estimated)
Counted CPU_CYCLES events (CPU cycle) with a unit mask of 0x00 (No unit mask) count 1500000
samples  %        app name                 symbol name
-------------------------------------------------------------------------------
 102      48.8038  vmlinux-3.10.17-1.0.2-wandboard+gbe8d6872b5eb __do_softirq
 107      51.1962  vmlinux-3.10.17-1.0.2-wandboard+gbe8d6872b5eb do_softirq
102      21.1180  vmlinux-3.10.17-1.0.2-wandboard+gbe8d6872b5eb __do_softirq
 102      47.4419  vmlinux-3.10.17-1.0.2-wandboard+gbe8d6872b5eb __do_softirq
 102      47.4419  vmlinux-3.10.17-1.0.2-wandboard+gbe8d6872b5eb __do_softirq [self]
 7         3.2558  vmlinux-3.10.17-1.0.2-wandboard+gbe8d6872b5eb net_rx_action
 4         1.8605  vmlinux-3.10.17-1.0.2-wandboard+gbe8d6872b5eb run_timer_softirq
--------------------------------------------------------------------- ----------
31        6.4182  bash                     /bin/bash

它是如何工作的...

OProfile 守护进程会持续记录数据,累积来自多个运行的数据。使用--start--stop选项来开始和停止新数据的累积。如果您希望从零开始收集数据,请首先使用--reset选项。

在运行性能分析会话之前,您需要配置 OProfile 守护进程以启用或禁用内核分析。指定内核分析选项是唯一的强制配置变量。

为了配置 OProfile 守护进程,首先停止它(如果正在运行),使用--shutdown选项。--stop选项只会停止数据收集,但不会终止守护进程。

要配置没有内核分析的 OProfile,您可以执行以下命令:

opcontrol --no-vmlinux <options>

要配置内核分析,您可以运行以下命令:

opcontrol --vmlinux=/boot/path/to/vmlinux <options>

这两者都将配置守护进程并加载 OProfile 内核模块(如果需要)。一些常用选项包括:

  • --separate=<type>: 这控制如何将分析数据分隔成不同的文件,类型包括:

    • none: 这不会分隔配置文件。

    • library: 这将按应用程序分隔共享库配置文件。示例文件名将包括库的名称和可执行文件。

    • kernel: 这会添加内核分析。

    • thread: 这会添加每个线程的分析配置。

    • cpu: 这会添加每个 CPU 的分析配置。

    • all: 这将执行上述所有操作。

  • --callgraph=<depth>: 这将记录被调用函数和调用函数以及函数中的花费时间。

一旦守护进程配置完成,您可以开始性能分析会话。

要检查当前配置,您可以执行:

# opcontrol --status
Daemon not running
Session-dir: /var/lib/oprofile
Separate options: library kernel
vmlinux file: /boot/vmlinux
Image filter: none
Call-graph depth: 5

采样的数据存储在 /var/lib/oprofile/samples/ 目录中。

然后,我们可以使用以下命令分析收集到的数据:

opreport <options>

一些有用的选项包括:

  • -c: 如果可用,这将显示调用图信息。

  • -g: 这将显示每个符号的源文件和行号。

  • -f: 这将显示完整的对象路径。

  • -o: 这将把输出提供到指定文件,而不是 stdout

OProfile 在 /dev/oprofile 挂载一个伪文件系统,用于报告和接收来自用户空间的配置。它还包含一个字符设备节点,用于将内核模块中的采样数据传递给用户空间守护进程。

还有更多内容...

Yocto 包括一个可以在主机上运行的 OProfile 图形用户界面。然而,它不是 Poky 的一部分,需要单独下载和安装。

请参阅oprofileui库:git.yoctoproject.org/cgit/cgit.cgi/oprofileui/ 以获取包含说明的 README,或者参阅Yocto 项目的性能分析和跟踪手册www.yoctoproject.org/docs/1.7.1/profile-manual/profile-manual.html

另请参见

使用 LTTng

LTTng 是一组双许可(GPLv2 和 LGPL)的跟踪和性能分析工具,适用于应用程序和内核。它生成生产优化的紧凑跟踪格式(CTF)的二进制跟踪文件,可以通过诸如babeltrace等工具进行分析。

准备工作

要在系统中包含不同的 LTTng 工具,请将以下内容添加到您的 conf/local.conf 文件中:

IMAGE_INSTALL_append = " lttng-tools lttng-modules lttng-ust"

它们还包含在 tools-profile 图像功能中,因此您也可以使用以下方法添加它们:

EXTRA_IMAGE_FEATURES += "tools-profile"

这些也包含在 -sdk 镜像中。

提示

在撰写本文时,Yocto 1.7 将 lttng-modules 从 ARM 的 tools-profile 功能和 sdk 镜像中排除,因此必须手动添加。

LTTng 命令行工具是 LTTng 的主要用户界面。它可以用于跟踪 Linux 内核(使用我们在前面示例中看到的内核跟踪接口),以及带有仪表的用户空间应用程序。

如何执行...

内核性能分析会话工作流程如下:

  1. 使用以下命令创建性能分析会话:

    # lttng create test-session
    Session test-session created.
    Traces will be written in /home/root/lttng-traces/test- session-20150117-174945
    
    
  2. 使用以下命令启用您要跟踪的事件:

    # lttng enable-event --kernel sched_switch,sched_process_fork
    Warning: No tracing group detected
    Kernel event sched_switch created in channel channel0
    Kernel event sched_process_fork created in channel channel0
    
    

    您可以使用以下命令获取可用的内核事件列表:

    # lttng list --kernel
    
    

    这对应于 Linux 内核中可用的静态跟踪点事件。

  3. 现在,您已准备好开始采样性能数据:

    # lttng start
    Tracing started for session test-session
    
    
  4. 运行要进行性能分析的工作负载:

    # ping -c 1 192.168.1.1
    
    
  5. 当命令执行完成或中断时,停止收集性能数据:

    # lttng stop
    Waiting for data availability.
    Tracing stopped for session test-session
    
    
  6. 最后,使用以下命令销毁性能分析会话。请注意,这将保留跟踪数据,只销毁会话。

    # lttng destroy
    Session test-session destroyed
    
    
  7. 要使跟踪数据可供人类阅读,请使用以下命令启动 babeltrace

    # babeltrace /home/root/lttng-traces/test-session-20150117- 174945
    
    

也可以将性能分析数据复制到主机以进行分析。

用户空间应用程序和库需要被仪表化,以便进行性能分析。这是通过将它们与 liblttng-ust 库进行链接来完成的。

应用程序可以利用 tracef 函数调用来输出跟踪信息,其格式与 printf() 相同。例如,要为我们在前几章中看到的示例 helloworld.c 应用程序进行仪表化,修改 meta-custom/recipes-example/helloworld/helloworld-1.0/helloworld.c 源码如下:

#include <stdio.h>
#include <lttng/tracef.h>

main(void)
{
    printf("Hello World");
    tracef("I said: %s", "Hello World");
}

meta-custom/recipes-example/helloworld/helloworld_1.0.bb 中修改其 Yocto 配方如下:

DESCRIPTION = "Simple helloworld application"
SECTION = "examples"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4 f302"

SRC_URI = "file://helloworld.c"
DEPENDS = "lttng-ust"

S = "${WORKDIR}"

do_compile() {
             ${CC} helloworld.c -llttng-ust -o helloworld
}

do_install() {
             install -d ${D}${bindir}
             install -m 0755 helloworld ${D}${bindir}
}

然后构建软件包,将其复制到目标,然后按以下方式开始性能分析会话:

  1. 通过执行以下命令创建性能分析会话:

    # lttng create test-user-session
    Session test-user-session created.
    Traces will be written in /home/root/lttng-traces/test-user- session-20150117-185731
    
    
  2. 启用您要进行性能分析的事件——在本例中,所有用户空间事件:

    # lttng enable-event -u -a
    Warning: No tracing group detected
    All UST events are enabled in channel channel0
    
    
  3. 开始收集性能数据:

    # lttng start
    Tracing started for session test-user-session
    
    
  4. 运行工作负载——在本例中,是经过仪表化的 hello world 示例程序:

    # helloworld
    Hello World
    
    
  5. 一旦完成,停止收集数据:

    # lttng stop
    Waiting for data availability.
    Tracing stopped for session test-user-session
    
    
  6. 在不销毁会话的情况下,您可以执行以下命令启动 babeltrace

    # lttng view
    [18:58:22.625557512] (+0.001278334) wandboard-quad lttng_ust_tracef:event: { cpu_id = 0 }, { _msg_length = 19, msg = "I said: Hello World" }
    
    
  7. 最后,可以销毁性能分析会话:

    # lttng destroy test-user-session
    Session test-user-session destroyed
    
    

工作原理...

使用 Linux 内核中可用的跟踪功能进行内核跟踪,如我们在前面示例中所见。为了使以下示例生效,Linux 内核必须根据早期相应的示例进行适当配置。

LTTng 提供了一个通用用户界面,用于控制我们之前看到的一些内核追踪功能,例如以下内容:

  • 静态 追踪事件

    你可以使用以下命令启用特定的静态追踪事件:

    # lttng enable-event <comma separated event list> -k
    
    

    你可以启用所有追踪点,方法如下:

    # lttng enable-event -a -k --tracepoint
    
    

    你也可以启用所有系统调用,方法如下:

    # lttng enable-event -a -k --syscall
    
    

    你可以启用所有追踪点和系统调用,方法如下:

    # lttng enable-event -a -k
    
    
  • 动态 追踪事件

    你也可以添加动态追踪点,方法如下:

    # lttng enable-event <probe_name> -k --probe <symbol>+<offset>
    
    

    你也可以使用以下命令添加它们:

    # lttng enable-event <probe_name> -k --probe <address>
    
    
  • 函数 追踪

    你也可以使用内核的函数追踪功能,方法如下:

    # lttng enable-event <probe_name> -k --function <symbol>
    
    
  • 性能 计数器事件

    使用以下命令获取硬件性能计数器,例如 CPU 周期:

    # lttng add-context -t perf:cpu:cpu-cycles -k
    
    

    使用 add-context --help 选项列出更多上下文选项和性能计数器。

扩展应用程序性能分析

通过编写模板文件(.tp)并使用 lttng-gen-tp 脚本与源文件一起,可以进一步提升应用程序追踪的灵活性。这将生成一个对象文件,随后可以将其链接到你的应用程序中。

在撰写本文时,Yocto 没有标准的方式对用户空间应用程序进行跨平台仪器化,但可以通过本地使用 -sdk 映像,或者向 conf/local.conf 文件添加以下映像功能来实现:

EXTRA_IMAGE_FEATURES += "tools-sdk dev-pkgs"

例如,定义一个追踪点 hw.tp 文件,如下所示:

TRACEPOINT_EVENT(
    hello_world_trace_provider,
    hw_tracepoint,
    TP_ARGS(
        int, my_integer_arg,
        char*, my_string_arg
    ),
    TP_FIELDS(
        ctf_string(my_string_field, my_string_arg)
        ctf_integer(int, my_integer_field, my_integer_arg)
    )
)

通过 lttng-gen-tp 工具传递该文件,获取 hw.chw.hhw.o 文件:

# lttng-gen-tp hw.tp

提示

请注意,lttng-gen-tp 工具并不包含在 lttng-ust 包中,而是包含在 lttng-ust-bin 包中。必须将其添加到目标映像中,例如,通过在 conf/local.conf 文件中添加以下内容:

IMAGE_INSTALL_append = " lttng-ust-bin"

现在,你可以将 hw.h 头文件添加到 helloworld.c 文件中的 hello world 应用程序中,并按如下方式使用 tracepoint() 调用:

#include <stdio.h>
#include "hw.h"

main(void)
{
    printf("Hello World");
    tracepoint(hello_world_trace_provider,  hw_tracepoint, 1, "I said: Hello World");
}

现在,按如下方式将应用程序与本地 gcc 链接:

# gcc -o hw helloworld.c hw.o -llttng-ust -ldl

提示

请注意,为了在目标上使用 gcc,我们需要构建一个 -sdk 映像,或者向我们的映像添加一些额外的功能,例如:

EXTRA_IMAGE_FEATURES = "tools-sdk dev-pkgs"

要对你的应用程序进行性能分析,请执行以下操作:

  1. 创建一个性能分析会话:

    # lttng create test-session
    Spawning a session daemon
    Warning: No tracing group detected
    Session test-session created.
    Traces will be written in /home/root/lttng-traces/test- session-20150117-195930
    
    
  2. 启用你想要分析的特定事件:

    # lttng enable-event --userspace hello_world_trace_provider:hw_tracepoint
    Warning: No tracing group detected
    UST event hello_world_trace_provider:hw_tracepoint created in channel channel0
    
    
  3. 开始收集性能分析数据:

    # lttng start
    Tracing started for session test-session
    
    
  4. 运行负载进行性能分析——在此案例中是 hello world 应用程序:

    #./hw
    Hello World
    
    
  5. 停止收集数据:

    # lttng stop
    
    
  6. 现在启动 babeltrace

    # lttng view
    [20:00:43.537630037] (+?.?????????) wandboard-quad hello_world_trace_provider:hw_tracepoint: { cpu_id = 0 }, { my_string_field = "I said: Hello World", my_integer_field = 1 }
    
    
  7. 最后,销毁分析会话:

    # lttng destroy test-session
    
    

还有更多...

你还可以使用 Trace Compass 应用程序或 Eclipse 插件通过访问 projects.eclipse.org/projects/tools.tracecompass/downloads 来分析主机中的追踪。在撰写本文时,稳定版本尚未发布。

另见

使用 blktrace

有一些工具可以用来执行块设备的 I/O 监控和分析。

从我们在 探索 Yocto 的追踪与分析工具 配方中提到的 iotop 开始,它能大致了解系统和特定进程的吞吐量。或者使用 iostat,它提供更多关于 CPU 使用率和设备利用率的统计信息,但不提供每个进程的细节。最后是 blktrace,一个 GPLv2 许可的工具,它在低级别监控特定块设备的 I/O,并且还能计算 每秒 I/O 操作 (IOPS)。

本配方将解释如何使用 blktrace 来追踪块设备,并使用 blkparse 将追踪数据转换为人类可读的格式。

准备工作

要使用 blktraceblkparse,你可以通过明确添加它们到目标镜像中,像这样:

IMAGE_INSTALL_append = " blktrace"

另外,你也可以使用 tools-profile 镜像功能,或者一个 -sdk 镜像。

你还需要配置 Linux 内核,启用 CONFIG_FTRACECONFIG_BLK_DEV_IO_TRACE,才能追踪块 I/O 操作。

在分析块设备时,重要的是尽量减少追踪对结果的影响;例如,不在被分析的块设备上存储追踪数据。

有几种方法可以实现这一点:

  • 从不同的块设备运行追踪。

  • 从基于 RAM 的 tmpfs 设备(如 /var/volatile)运行追踪。从内存设备上运行将限制可存储的追踪数据量。

  • 从网络挂载的文件系统运行追踪。

  • 通过网络运行追踪。

此外,用于分析的块设备的文件系统也是一个重要因素,因为文件系统的特性(如日志记录)会扭曲 I/O 统计数据。即使是以块设备形式呈现给用户空间的闪存文件系统,也无法使用 blktrace 进行分析。

如何实现...

假设你想要分析 Wandboard 上 microSD 卡设备的 I/O。通过从网络启动系统,如在第一章《构建系统》中的 为开发环境配置网络启动 配方所示,你可以避免系统对设备的多余访问。

对于这个例子,我们将挂载为 ext2 分区,以避免日志记录,但可能还需要其他调整,以有效分析特定的工作负载:

# mount -t ext2 /dev/mmcblk0p2 /mnt
EXT2-fs (mmcblk0p2): warning: mounting ext3 filesystem as ext2
EXT2-fs (mmcblk0p2): warning: mounting unchecked fs, running e2fsck is recommended

分析特定工作负载的工作流程如下:

  1. 启动 blktrace,收集 /dev/mmcblk0 设备的追踪数据:

    # blktrace /dev/mmcblk0
    
    
  2. 启动需要分析的工作负载,例如创建一个 10 KB 的文件。打开 SSH 连接到目标并执行:

    # dd if=/dev/urandom of=/mnt/home/root/random-10k-file bs=1k count=10 conv=fsync
    10+0 records in
    10+0 records out
    10240 bytes (10 kB) copied, 0.00585167 s, 1.7 MB/s
    
    
  3. 使用Ctrl + C停止控制台上的分析。这将在相同目录下创建一个名为mmcblk0.blktrace.0的文件。你将看到以下输出:

    ^C=== mmcblk0 ===
      CPU  0:                   30 events,        2 KiB data
     Total:                    30 events (dropped 0),        2 KiB data
    
    

    一些有用的blktrace选项包括:

    • -w: 这用于只运行指定秒数

    • -a: 这会将一个掩码添加到当前文件中,掩码可以是:

      • barrier: 这是指障碍属性

      • complete: 这是指由驱动程序完成的操作

      • fs: 这些是文件系统请求

      • issue: 这个选项指的是发出的操作

      • pc: 这是指数据包命令事件

      • queue: 这个选项代表队列操作

      • read: 这是指读取跟踪

      • requeue: 这是用于重新排队操作

      • sync: 这表示同步属性

      • write: 这是指写入跟踪

它是如何工作的...

一旦收集到跟踪数据,你可以使用blkparse进行处理,如下所示:

# blkparse mmcblk0

这将提供所有收集数据的stdout输出,以及最终的总结,如下所示:

Input file mmcblk0.blktrace.0 added
179,0    0        1     0.000000000   521  A   W 1138688 + 8 <- (179,2) 1114112
179,0    0        2     0.000003666   521  Q   W 1138688 + 8 [kworker/u8:0]
179,0    0        3     0.000025333   521  G   W 1138688 + 8 [kworker/u8:0]
179,0    0        4     0.000031000   521  P   N [kworker/u8:0]
179,0    0        5     0.000044666   521  I   W 1138688 + 8 [kworker/u8:0]
179,0    0        0     0.000056666     0  m   N cfq519A  insert_request
179,0    0        0     0.000063000     0  m   N cfq519A  add_to_rr
179,0    0        6     0.000081000   521  U   N [kworker/u8:0] 1
179,0    0        0     0.000121000     0  m   N cfq workload slice:6
179,0    0        0     0.000132666     0  m   N cfq519A  set_active wl_class:0 wl_type:0
179,0    0        0     0.000141333     0  m   N cfq519A  Not idling. st->count:1
179,0    0        0     0.000150000     0  m   N cfq519A  fifo=  (null)
179,0    0        0     0.000156000     0  m   N cfq519A  dispatch_insert
179,0    0        0     0.000167666     0  m   N cfq519A  dispatched a request
179,0    0        0     0.000175000     0  m   N cfq519A  activate rq, drv=1
179,0    0        7     0.000181333    83  D   W 1138688 + 8 [mmcqd/2]
179,0    0        8     0.735417000    83  C   W 1138688 + 8 [0]
179,0    0        0     0.739904333     0  m   N cfq519A  complete rqnoidle 0
179,0    0        0     0.739910000     0  m   N cfq519A  set_slice=4
179,0    0        0     0.739912000     0  m   N cfq schedule dispatch
CPU0 (mmcblk0):
 Reads Queued:           0,        0KiB  Writes Queued:1,4KiB
 Read Dispatches:        0,        0KiB  Write Dispatches:1,4KiB
 Reads Requeued:         0               Writes Requeued:0
 Reads Completed:        0,        0KiB  Writes Completed:1,4KiB
 Read Merges:            0,        0KiB  Write Merges:0,0KiB
 Read depth:             0               Write depth:1
 IO unplugs:             1               Timer unplugs:0

Throughput (R/W): 0KiB/s / 5KiB/s
Events (mmcblk0): 20 entries
Skips: 0 forward (0 -   0.0%)

blkparse的输出格式为:

179,0    0        7     0.000181333    83  D   W 1138688 + 8 [mmcqd/2]

这对应于:

<mayor,minor> <cpu> <seq_nr> <timestamp> <pid> <actions> <rwbs> <start block> + <nr of blocks> <command>

各列对应于:

  • A: I/O 被重定向到不同设备

  • B: I/O 被回跳

  • C: I/O 完成

  • D: I/O 发往驱动程序

  • F: I/O 前端与队列中的请求合并

  • G: 获取请求

  • I: I/O 插入到请求队列中

  • M: I/O 后端与队列中的请求合并

  • P: 插入请求

  • Q: I/O 由请求队列代码处理

  • S: 睡眠请求

  • T: 超时导致的拔出

  • U: 拔出请求

  • X: 分割

RWBS 字段对应于:

  • R: 读取

  • W: 写入

  • B: 障碍

  • S: 同步

另一种非破坏性跟踪方式是使用实时监控,即直接将blktrace的输出通过管道传递给blkparse,而不写入任何内容到磁盘,如下所示:

# blktrace /dev/mmcblk0 -o - | blkparse -i -

这也可以仅用一行命令完成:

# btrace /dev/mmcblk0

还有更多...

blktrace命令还可以通过网络发送跟踪数据,这样它就可以存储在不同的设备上。

为此,按照以下方式在目标系统上启动blktrace

# blktrace -l /dev/mmcblk0

在另一台设备上,按照以下方式运行另一个实例:

$ blktrace -d /dev/mmcblk0 -h <target_ip>

回到目标,现在你可以执行你想要跟踪的特定工作负载:

# dd if=/dev/urandom of=/mnt/home/root/random-10k-file bs=1k count=10 conv=fsync
10+0 records in
10+0 records out
10240 bytes (10 kB) copied, 0.00585167 s, 1.7 MB/s

完成后,使用Ctrl + C中断远程blktrace。总结将同时在目标和主机上打印。

现在,你可以运行blkparse来处理收集到的数据。

posted @ 2025-07-05 15:47  绝不原创的飞龙  阅读(5)  评论(0)    收藏  举报