关于嵌入式开发的一些信息汇总:嵌入式C开发人员、嵌入式系统Linux
- 1 关于嵌入式 C 开发人员
- 1.1 嵌入式 C 开发人员必须具备的一些基本技能是:
- 1.2 嵌入式C开发的应用案例
- 2 如何学习用于嵌入式系统的 Linux
- 2.1 如何学习Linux
- 2.1.1 第一步:创建 VM 并安装桌面 Linux 发行版
- 2.1.2 第二步:熟悉命令行、文件系统、目录结构和进程组织
- 2.1.3 第三步:使用GCC、GDB、make
- 2.1.4 第四步:Linux开发资料来源
- 2.1.5 第五步:Linux 内核源代码的组织方式、如何配置内核以及如何使用您选择的选项构建新内核。
- 2.1.6 第六步:可加载模块LKM,通常用于设备驱动程序
- 2.1.7 第七步:构建一个简单的字符设备驱动程序
这篇文章是关于嵌入式开发的一些基本信息,供想入行的人参考。有一些作者本人的想法,以及来自外网的大拿的文章翻译而来,原文链接在此Learning Linux for embedded systems,再次感谢,支持原创。
1 关于嵌入式 C 开发人员
普通C开发人员和嵌入式C开发人员之间的基本区别在于,因为嵌入式C程序被设计为能够与硬件通信并使硬件设备更有意义,而普通C开发人员往往只编写桌面应用程序。嵌入式 c 开发人员要使用硬件设备和微处理器,例如基于微控制器的应用程序。
1.1 嵌入式 C 开发人员必须具备的一些基本技能是:
1、有C语言能力。这是必须的,因为没有C的知识,就上手嵌入式c编程也是学不会的。
2、有编程基础知识,例如面向对象编程及其四个基本概念(聚合、封装等)以及其他关键功能,例如覆盖和重载。
3、嵌入式 C 开发人员必须在计算机体系结构以及数据结构和算法领域具有广泛的知识。
4、操作系统的知识是必须的,在 power shell 环境中工作与学习的能力与会基本的 c 语言一样必要。
5、必须具备微处理器基础知识(汇编代码和寄存器等)和微控制器基础知识(DMA、ADC 和定时器等)。
6、使用 I2C 和 LIN 等的网络编程基础,以及 SATA 和 MOST 等高级网络编程。
7、必须具备并发、并行工作能力,熟悉系统调用库函数。
8、嵌入式 C 开发人员必须知道如何处理未收集的垃圾环境,例如悬挂指针。
9、了解其他编程语言,如 python、Java 和 Android 以及基本 FPGA/ASIC 设计、基本 DSP 的技能
10、最后,了解软件工程原理是一个加分项,因为它使版本控制、错误跟踪和整体测试等工作变得更加容易。
1.2 嵌入式C开发的应用案例
许多项目由普通的 C 开发人员处理,其中包括硬件设备,这些硬件设备需要用嵌入式 c 语言编写底层程序才能正常运行。示例如下:
1、环境监控系统:这是一个内置湿度传感器、金属探测器和火灾传感器的系统。该系统通过用户的命令进行监控,通过单个端口发送特殊信号来控制多通道射频发射器。
2、无线医疗监控系统:该系统确保通过温度和湿度传感器等传感器检测患者的数据,并收集这些数据以发送到控制中心。此类硬件和软件同时通信的应用程序由嵌入式 c 开发人员开发的程序控制。
3、Vehicle Tax pay and access system:另一个硬件和软件通过程序通信的系统是由嵌入式c开发人员开发的。在这个系统中,收集有关车辆出入信息的过程是自动完成的,并使用智能卡技术。
4、基于微控制器的移动干扰器:这个工作在 900MHz 到 1200MHz 的项目只是为了确保信号在这些干扰器放置的特定区域被丢弃和完全切断。该技术可用于讨论机密信息且安全是第一要务的地方。蜂窝移动 SIM 卡与其蜂窝基站之间的连接被切断。这些只是一些嵌入了程序的硬件设备,这些程序使它们能够阻止传入和传出的信号,也是由嵌入式 c 开发人员开发的。
2 如何学习用于嵌入式系统的 Linux
一个人如果具备 8 位处理器(如 PIC)和 32 位处理器(如 PowerPC)嵌入式系统编程经验,但是没有 Linux 经验,那么要如何学习以及如何使用嵌入式 Linux呢?
像这样的嵌入式系统程序员推荐的是:将嵌入式 Linux 视为两个部分,即嵌入式部分和 Linux 部分。
2.1 如何学习Linux
让我们首先考虑 Linux 部分。
无论您使用什么开发主机,无论是 Linux、Windows 还是 Mac,您都需要学习如何使用目标操作系统进行编程。在这方面,使用嵌入式 Linux 与使用 VXworks、WindowCE 或其他操作系统没有太大区别。
1) 您需要了解操作系统是如何设计的、如何配置操作系统以及如何使用其应用程序编程接口 (API) 进行编程。
2) Linux 区别于其他操作系统的最重要因素是所有系统都使用相同的内核,从最小的嵌入式主板到桌面系统,再到大型服务器群。这意味着您可以在桌面环境中学习大量 Linux 编程,这比使用目标板连接到目标、下载测试程序和运行测试的所有复杂性要灵活得多。
3) 对于桌面 Linux 和嵌入式 Linux,所有基本概念和大多数 API 都是相同的。
2.1.1 第一步:创建 VM 并安装桌面 Linux 发行版
在您当前的开发系统上创建一个虚拟机环境。
1) 对于 Windows 主机,您可以安装 VMware Player 或 VirtualBox,使用虚拟机可为您提供更大的灵活性。您可以安装桌面 Linux 发行版,例如 Ubuntu 或 Fedora。
2) 您可以使用此发行版来熟悉基本的 Linux 概念、学习命令 shell 以及如何构建和运行程序。您可以重新配置内核或加载驱动程序,而不必担心会使桌面系统崩溃。
3) 您可以构建整个内核和应用程序环境,类似于您可能对嵌入式 Linux 目标的交叉开发环境所做的事情。
4) 如果运行 Linux 的 VM 崩溃,您只需重新启动 VM。崩溃不会影响您在开发系统上可能正在做的其他事情,例如阅读有关如何构建和安装驱动程序的网页,或者向众多支持邮件列表之一发送电子邮件。创建一个您可以轻松学习 Linux 概念和编程的环境。
2.1.2 第二步:熟悉命令行、文件系统、目录结构和进程组织
1) 大多数 Linux 系统配置和管理是从命令行执行的,在桌面系统上,这意味着打开一个终端窗口并使用 Bash shell。
- 命令以命令名称开头,通常接受选项(通常以连字符开头)后跟文件名。许多命令名称都很简洁(如 ls 或 cp),并且可以有多个选项,其中大部分您很少会用到。如果您熟悉 Windows CMD shell(或它从中演变而来的 MSDOS shell),许多命令是相似的(如 cd),但经常存在细微差别。至少,您需要知道如何列出文件(cat、less、file)、列出目录并在目录之间移动(ls、cd、pwd),以及如何获得帮助(man、info、apropos)。
- 桌面 Linux 系统有数千条命令,你只需要知道一小部分。但是如果你想做某事,很可能有一个命令可以做到。apropos 命令是查找可能执行您想执行的操作的命令的好方法。尝试从命令行运行“man apropos”。
- 您还需要熟悉可在命令 shell 中使用的编辑器,例如 vi。在嵌入式 Linux 系统上,您很可能没有窗口系统。您将与 BusyBox 和 Ash shell(一个小型命令行解释器)进行交互,BusyBox 将大约 200 个命令打包到了一个可执行程序中。
2)Unix 和 Linux 的设计哲学之一是围绕分层文件系统进行组织。
- 这个文件系统的根名为“/”,文件系统中的所有内容都可以从这里开始找到。自然地,文件系统保存包含文本或数据的常规文件以及程序。
- 此外,它还包含各种特殊的“文件”,这些文件可能代表物理设备(如硬盘驱动器)、驱动程序创建的接口(如虚拟终端)、网络连接等。
- 其他操作系统可能会为有关进程或内存的内部信息提供编程接口,而 Linux 则通过将此信息表示为文本文件来提供一个非常简单的接口。例如:常用目录为/boot,包含引导程序;/bin 和 /sbin,包含通常由系统管理员 root 运行的程序;/dev,包含设备(真实的和虚拟的);/etc,包含系统配置文件;/home,包含用户文件;/proc 和 /sys,带有系统信息;/lib,包含库;/usr,不包含用户文件,而是可以由用户运行的程序;/tmp,包含临时文件,最后是 /var,包含系统日志。
- 嵌入式 Linux 系统将具有相同的组织结构,尽管有时某些目录可能会合并。它将比桌面系统拥有更少的文件。
3) Linux(和 Unix)有一个层次化的进程结构。
- 第一个进程 init 的进程 ID (PID) 为 1,由 Linux 内核在系统启动时创建。反过来,Init 创建子进程,允许您登录系统,再依次启动窗口系统或命令 shell,这又会产生其他进程。
- 如果在命令窗口中键入“ps”,您将看到在该窗口中运行的顶级进程的简要列表,通常只是 ps 命令本身和 bash 命令行解释器。
- 键入“ps -l”,将为您提供更多信息,包括每个进程父进程的进程 ID (PPID)、运行程序的用户 ID (UID) 等更多信息。
2.1.3 第三步:使用GCC、GDB、make
几乎每个项目都需要使用 GNU 编译器集合 (GCC)、GNU 二进制实用程序 (Binutils) 和 make,用于构建程序和处理依赖项。
1) Linux 确实有几个 IDE(集成开发环境),如 Eclipse,但它们都是围绕这些命令行工具构建的。与通常使用 Visual Studio 的 Windows 开发不同,许多 Linux 开发人员不使用 IDE。
-
要使用 gcc 编译 C 程序,请使用您喜欢的文本编辑器(vi、emacs、gedit、kwrite 等)编写您的程序,并使用后缀 .c 保存它(在下面的示例中,我们使用标准的第一个 C 程序来自 K&R 并将其保存为 hello.c)。然后输入以下命令:
$ gcc -o hello -g -O1 hello.c
gcc将会调用 C 编译器来翻译您的程序,如果成功且没有错误,它将继续调用具有正确系统库的链接器以创建名为 hello 的可执行文件。 -
其他操作系统通过后缀识别可执行文件,如 .exe。Linux 可执行文件通常没有后缀,由文件系统标志指示文件可以执行。
可执行文件的名称遵循 -o 选项,- g 选项表示生成调试信息,-O1(即字母 O 后跟数字 1)表示生成优化代码。GCC 有大量不同的选项,但这些都是必要的基础。为了便于调试,您可能需要指定 -O0(字母 O 后跟数字 0)或省略 -O 选项以在不进行优化的情况下进行编译。 -
您可能会发现您的 Linux 安装缺少一些默认情况下未安装的组件,如 GCC 或 C 库的头文件。
如果是这种情况,您可以使用系统的包管理器来添加这些组件。- 在 Fedora 系统上,这意味着使用 yum 或者 packagekit GUI;
- 在 Ubuntu 系统上,您可以使用 apt-get 或 synaptic GUI。
- 这些包管理器将处理您请求的组件的下载和安装,以及可能需要的任何依赖项。
您可以通过输入程序名称从命令行执行程序,它要在您的路径搜索列表中,或者您通过命令行给出文件的路径。
对于我们的示例,我们可以执行以下操作:
$ ./helloHello World!
在这种情况下,由于我们的当前目录未在 $PATH 环境变量中列出,因此我们使用点 (.) 来指示当前目录,然后是文件名,由目录规范中的斜杠分隔。
这时可以使用 GDB 调试器运行程序,无论您是在进行内核、驱动程序还是应用程序开发,您都可能需要使用 GDB 调试程序。GDB 有一个命令行界面,学习执行基本操作(如打印变量值、设置断点和单步执行程序)的命令是个好主意。其他 GUI 包括 Eclipse CDT IDE、Insight,甚至是 Emacs 文本编辑器的扩展。
您可以SftyE2eCompst :$ gdb hello [ start up messages from gdb ] (gdb) break main Breakpoint 1 at 0x400530: file hello.c, line 5. (gdb) run Starting program: /tmp/hello Breakpoint 1, main () at hello.c:5 5 printf ("Hello world!n"); (gdb) cont Continuing. Hello world! (gdb) quit
在上面的示例中,gdb的输出以粗体显示;我们的命令是普通类型。
-
除了来自gdb的初始启动消息之外,可能还有一些关于缺少系统库调试信息的其他消息或关于程序退出的消息。
-
在嵌入式 Linux 环境中,您将以类似于本机 Linux 开发的方式使用 GCC、GDB 和 make。
-
在大多数情况下,您将使用针对您正在使用的处理器的不同版本的 GCC 和 GDB。程序名称可能不同,例如arm-none-eabi-gcc,它使用 EABI(嵌入式 ABI)为 ARM 生成代码。
-
另一个区别是您很可能在交叉开发环境中工作,您在主机系统上编译并将程序下载到目标系统。如果您有嵌入式编程经验,那么您应该熟悉在交叉开发环境中工作。
2.1.4 第四步:Linux开发资料来源
Linux 有许多库可用于嵌入式系统,要使用包管理器来安装库****和包含标头的相关开发包。一些库有静态和动态两种版本,静态库与您的程序链接,而动态库是独立的,在执行程序时根据需要加载。
- 除了最简单的应用程序之外,每当我编写任何应用程序时,多年来我的参考资料一直是 W. Richard Stevens的《UNIX 环境中的高级编程》.Linux 采用了 Unix 的大部分 API 和接口,尽管实现可能有所不同。
- 另一个参考是The Linux Programming Interface, 迈克尔·克瑞斯克著。
两者都不会成为床边读物,但如果您需要了解文件或进程操作、信号、线程、进程间、或进程内、或网络通信和同步的基本细节等等,这些都是很好的起点。 - 还有Stack Overflow等帮助论坛。
- 在桌面和嵌入式环境中,有一个广泛的开源基础设施支持 Linux。自由软件基金会的GNU 项目维护着大量广泛使用的实用程序和库。您可以从http://gnu.mirrors.pair.com/gnu下载这些包,它会自动将您连接到您附近的镜像。
- SourceForge拥有超过 300,000 个项目的源代码,其中许多非常重要。
- Freecode还拥有数以千计的开源应用程序。
- 我的候选名单上的最后一个目的地是GitHub,它为数千个项目的代码存储库提供托管服务。
大多数库或程序都是使用 GNU make 实用程序以及 bash 脚本或支持实用程序(如automake 和autoconf )构建的。最简单的是,make 检查哪些文件需要编译并管理(使用开发人员编写的 Makefile)执行这些编译的顺序。Makefile 可能非常复杂,Makefile 调用make 来构建子目录或递归调用自身。Automake 旨在生成 Makefile,识别依赖项并调用libtool,这是一个创建共享库的实用程序。
自动配置 允许为不同的目标和操作系统或使用不同的选项编译库或程序。
为您的 Linux 系统构建大多数标准库或程序的标准顺序是下载源代码,通常以tar 文件的形式,可能用gzip、bzip2或xz压缩。如果我想构建自己的diff副本,我会首先 从 GNU 镜像下载diffutils包。通常我会使用浏览器来保存包,但我也可以使用wget 实用程序:
$ cd /tmp
$ wget ftp://ftp.gnu.org/gnu/diffutils/diffutils-3.3.tar.xz
解压文件并cd 到生成的目录:
$ tar xfv diffutils-3.3.tar.xz
$ cd diffutils-3.3
(如果包有 .gz 或 .tgz 后缀,则需要在“xfv”后添加“z”选项。如果它有 .bz 后缀,请添加“j”选项。)
许多软件包都有一个 README 文件。在构建之前,您应该阅读此内容。自述文件将告诉您如何构建包。构建大多数包,如 diffutils,很简单:您输入以下命令:
$ ./configure $ make $ make install
- 第一个命令调用 configure ,它将分析您的系统并创建一个 Makefile 来构建您的库或为您的系统量身定制的程序。
- 第二个命令调用make,它将编译和链接工作目录中的库或程序。
- 第三条命令将安装库或程序。
当然,有时这些步骤中的每一步都会出错。Configure 可能会告诉您需要安装其他包,或者可能是已安装库的标头。make步骤可能因编译错误而 停止。最后一步可能会尝试将库或程序安装在受保护的系统目录中。如果是这种情况,您可以以 root 身份运行最后一步,或者在前面加上sudo 命令临时承担 root 权限。或者,您可以指定 –prefix 选项来配置并指向未受保护的目录:
$ ./configure --prefix=~/mydiff
当您运行make install时,该程序和任何其他文件将安装在前缀选项中提到的目录中,在本例中为我的主目录中的 mydiff。
有一些我们将在未来讨论的注意事项,这意味着您在本机 x86 Linux 环境上构建的库和程序也可以为其他处理器(如 ARM、PowerPC 或 MIPS)或其他 Linux 配置构建,使用许多相同的工具和技术。
2.1.5 第五步:Linux 内核源代码的组织方式、如何配置内核以及如何使用您选择的选项构建新内核。
大多数嵌入式项目都需要配置 Linux 内核,并且可能需要开发设备驱动程序以匹配您的硬件。由于我们在虚拟机中运行的桌面 Linux 系统使用的内核与您在嵌入式系统中使用的内核相同,因此我们可以在桌面系统上构建自定义内核和简单的驱动程序,其方式与开发板的方式大致相同.
每个主要发行版都描述了它们如何构建内核。对于 Fedora,描述在Fedora Wiki上,对于 Ubuntu,在社区站点上也有资源,其中每一个都将安装用于构建 VM 系统的源代码。他们使用像 rpmbuild 或脚本这样的程序来使这个过程变得简单,尽管可能不足够透明。
我建议您登录到您的 VM 并按照脚本构建并安装新内核。在安装之前,您可能需要拍摄 VM 的快照,以防万一出现问题时您想返回到已知的稳定系统。
当您按照这些说明进行操作时,它们将创建一个目录,其中包含用于构建您已安装的版本的内核。
这是“vanilla”内核,可以从 kernel.org 下载,加上发行版选择的一些补丁。如果你像我一样使用 Fedora,你可以在~/rpmbuild/BUILD/kernel-中找到vanilla内核和补丁内核 . 让我们将打过补丁的内核复制到一个单独的目录中:
$ cp -r ~/rpmbuild/BUILD/kernel*/linux* ~/linux
- 较早的指南经常会说将内核复制到 /usr/src/linux,这是内核的传统存放位置,
- 你不需要这样做;
- 内核可以构建在任何位置。
- 通常 /usr/src 是一个受保护的目录,只能由 root 写入。
- 您无需成为 root 用户即可构建内核。
让我们简单浏览一下 ~/linux 下的一些文件和子目录:
Makefile. 用于控制配置和构建内核Arch. 包含体系结构特定文件Drivers. 包含在内核的驱动源码Include. 用于构建内核和驱动程序的头文件Kernel.“核心”目标独立部分README. 目录的描述,带有 make 指令的Scripts. 用于构建内核和驱动程序的 Bash 脚本
有几个包含内核部分的目录,如 net(网络)、mm(内存管理)、fs(文件系统)等。
arch 目录包含超过两打不同处理器架构的子目录,包括非常流行的 ARM 和 Intel x86 处理器以及一些鲜为人知的处理器。这些目录包含特定于目标的代码,允许 Linux 在如此多的不同处理器上运行。您会发现与内核下名称相同的目录,其中包含特定于目标的代码以支持与目标无关的功能。
构建 Linux 内核的核心是 .config 文件。这是一个隐藏文件(开头的点表示隐藏,但是输入“ls -a”就可以看到)这是当前的配置。configs 目录下有示例配置文件。X86 只有两个,一个用于 32 位,一个用于 64 位。ARM 有几十个用于许多不同的处理器配置。如果你打开 arch/x86/configs/i386_defconfig,你会看到这样:
CONFIG_EXPERIMENTAL=y# CONFIG_LOCALVERSION_AUTO is not set CONFIG_SYSVIPC=yCONFIG_POSIX_MQUEUE=yCONFIG_BSD_PROCESS_ACCT=yCONFIG_TASKSTATS=yCONFIG_TASK_DELAY_ACCT=yCONFIG_TASK_XACCT=y
每个 y 都表示启用了一个选项,注释掉或丢失的配置选项未启用。
从头开始创建其中一个配置文件将是一项艰巨的任务,我们不必这样做。您为VM下载的源包含一个 .config,或者可能有关于从 /boot 目录复制与您的安装相对应的配置文件的说明。
要开始构建内核,您需要运行
$ make oldconfig
如果您想查看所有选项(并使用简单的基于文本的实用程序选择要启用或禁用的选项),请运行
$ make menuconfig
继续探索随后显示的 GUI(右图)。箭头键允许您在列表中上下移动。要设置或取消设置选项,请按空格键。要转到子菜单(由 -> 箭头指示),请按 Enter。左右箭头选择窗口底部的选项。选择帮助会告诉您有关该选项的一些信息。当您想要退出子菜单时,选择退出。在顶级菜单中,选择保存以保存对 .config 的更改。
在我们构建我们的内核之前,让我们给它一个(稍微)不同的名字。如果您在编辑器中打开 Makefile,前几行将指定 VERSION、PATCHLEVEL、SUBLEVEL 和 EXTRAVERSION 值。前三个将与内核版本相同。在我的例子中,这是 3.11.9,与我下载的内核源 RPM 名称上的值相同。将您的姓名或号码添加到 .config,这将用于使新构建的内核与安装的内核不同。
要构建内核,最好从头开始:
$ make clean
如果您希望继续构建可能因编译失败而停止的构建,则无需执行此操作。
编译内核需要相当多的时间,即使在快速系统上也是如此:
$ make
每次编译只打印一行。如果您想要详细信息(很多详细信息),请在 make 命令末尾添加 V=1 。
接下来,您需要构建未链接到内核的可加载模块。这要快得多:
$ make modules
在 /lib 下安装模块,然后在 /boot 中安装新内核。这两个命令需要以 root 身份执行,因为它们会修改受保护的系统目录:
$ su
Password:
make modules_install
make install
查看 /lib/modules 和 /boot 以查看 Makefile 安装的内容。您现在可以重新启动您的 VM:
# reboot
从 GRUB 引导菜单中选择您的新内核。系统重新启动并登录后,您可以通过运行“uname -1”来检查版本。您应该看到新内核的版本号,包括您的姓名或您添加到 EXTRAVERSION 的编号。您可能会修改配置、重建和安装内核。每次更新 EXTRAVERSION 以帮助跟踪哪个内核是哪个。如果您创建了一个崩溃的内核,请不要担心。您始终可以在 GRUB 引导菜单中选择一个旧版本,或者,如果您听取了我的建议以保存 VM 的快照,则可以从快照恢复并从中断的地方继续。
为嵌入式 Linux 构建 Linux 内核时存在一些差异,但是构建 Linux 内核的文件组织、配置和整个过程对于桌面 Linux 系统和嵌入式 Linux 系统是相似的。
2.1.6 第六步:可加载模块LKM,通常用于设备驱动程序
您可能已经注意到,构建内核的结果是在 /boot 中安装了三个文件。这些文件中的每一个都在其名称后附加了版本号。
vmlinuz — 压缩内核
initramfs(或 initrd)——用于引导内核的初始文件系统
System.map — 导出的内核符号及其地址的列表。
解释一下,
- vmlinuz 是内核的可执行映像,已使用 gzip(或其他压缩算法)压缩。它还包含一个解压缩存根,用于在加载内核时解压缩内核。您可以使用可以在内核脚本目录中找到的 extract-vmlinux 解压缩它。
- 临时根文件系统 initramfs(或其前身 initrd)在引导过程中加载到内存中。此根文件系统包含挂载根文件系统可能需要的设备驱动程序或其他程序,但在加载内核后可能不需要它们。这允许创建一个通用内核,它可以在具有不同硬件的各种系统上运行。
Linux 内核被设计为一个单体系统,具有一个单一的大型二进制映像。组成内核的所有东西都被编译并链接在一起以形成这个映像。例外情况是有些独立的 Linux 内核模块 (LKM)。
- LKM 是可以加载到内核中以扩展其功能的代码片段。
- LKM 可以作为内核构建的一部分进行编译(当您选择它们作为内核配置的一部分时),也可以单独进行编译。它们可能被链接到内核映像(在内核配置中指定),或者它们可以在系统启动和内核运行后稍后加载。
- LKM 有多种用途,包括支持不同的文件系统;添加新功能或系统调用,或者最常见的是添加对新硬件的支持。
- LKM 与特定版本的内核紧密耦合,必须为每个版本的内核重新编译。您可以通过运行“lsmod”命令或列出 /proc/modules 查看安装在 Linux 系统上的 LKM 列表。
我们将重点关注 LKM 对设备驱动程序的使用,因为这是嵌入式 Linux 中最常见的用途。
Linux 中有几类设备:字符、块和网络设备是最常见的类型。字符设备作为连续的字节流读取或写入,如键盘或打印机。块设备用于支持文件系统,并作为固定大小的块进行读取或写入。网络设备用于与发送或接收数据包的不同系统进行通信。字符设备和块设备由 /dev 下的文件系统条目表示;例如,网络接口的名称类似于“eth0”。
让我们逐步构建一个简单的 LKM。
我们将从最简单的 LKM 开始,我们将其命名为lkm.c —
/* Simple Linux Kernel Module */
#include int init_module()
{ printk(KERN_ALERT "LKM initn"); return 0;
}
void cleanup_module()
{printk(KERN_ALERT "LKM stoppedn");
}
它有一个函数 init_module( ),当 LKM 加载到内核时调用它。还有另一个函数cleanup_module( ),当它被删除时调用它。Printk 类似于printf,只是它将输出定向到系统日志/var/log/messages。
我们的 makefile 名为Makefile,也很简单:
obj-m += lkm.o
KERNELDIR=/lib/modules/
$(shell uname -r)/buildall: make -C ${KERNELDIR} M=$(PWD)
modulesclean: make -C ${KERNELDIR} M=$(PWD) clean
我们的 makefile 设置参数以告知要编译哪些文件并调用内核 Makefile 来完成繁重的工作并实际构建 LKM。结果是一个名为 lkm.ko 的文件,“ko”后缀表明这是一个内核模块。
我们可以使用“modinfo”命令查看模块记录的信息:
$ modinfo lkm.ko
filename: /home/eager/lkm/lkm.ko
depends: vermagic: 3.12.8-200.fc19.x86_64 SMP mod_unload
我们可以使用 insmod 命令将模块加载到正在运行的内核中:
#insmod lkm.ko
请注意,虽然我们可以在用户模式下编译内核模块(这总是一个好主意),但您需要以 root 身份运行 insmod。您可以在另一个 shell 中运行此命令,在该 shell 中您使用“su”命令假定了超级用户权限,或者您可以在 insmod 命令前加上“sudo”前缀,假设您已经设置了 /etc/sudoers。
除非出现错误,否则 insmod 不会发出任何消息。要查看我们的模块是否已加载到内核中,我们使用 lsmod 命令:
$ lsmod | grep lkm lkm 12426 0
这表明我们的 LKM 安装在内核中,其大小为 12426 字节,并且没有其他 LKM 依赖于它。一个 LKM 可以依赖于另一个,低级驱动程序为更高级别的驱动程序提供服务。如果您运行如下命令,您可以看到这一点:
$ lsmod | grep par
parport_pc 28048 0 parport 40425 2 ppdev,parport_pc
在这里我们可以看到 parport(并行端口)驱动程序被另外两个驱动程序使用——ppdev 和 parport_pc。
我们可以再次以超级用户身份使用 rmmod 命令从内核中删除 LKM:
# rmmod lkm.ko
如果我们查看系统日志,我们可以看到 LKM 初始化和删除时发出的消息:
# tail -n 2 /var/log/messagesJan 26 17:58:36 fedora19 kernel: [27688.186462] LKM initJan 26 18:00:14 fedora19 kernel: [27786.528267] LKM stopped
您可以通过将 LKM 复制到正确的系统目录来将其安装到内核中。如果查看 /lib/modules,您将看到已安装的每个内核都有一个子目录。如果查看当前正在执行的内核的目录 /lib/modules/$(uname -r)/kernel,您会看到许多用于各种内核模块的子目录,例如加密、驱动程序、内存管理和声音。
我们将以 root 身份将 LKM 复制到 drivers/misc 目录:
# cp lkm.ko /lib/modules/kernel/$(uname -r)/kernel/drivers/misc
接下来我们执行 depmod 命令更新 /lib/modules/$(uname -r) 下的模块映射文件:
#depmod -a
这将在 modules.dep 中创建一个条目,modprobe 使用它来查找 LKM:
# modprobe lkm
我们还可以使用 modprobe 删除模块:
# modprobe -r lkm
Modprobe 是一个更高级别的命令,它调用 insmod 和 rmmod 来完成繁重的工作。它理解并解决模块先决条件,支持别名,并由内核用于根据需要加载 LKM
当您查看系统日志时,您可能已经注意到一些事情:
Jan 26 18:38:34 fedora19 kernel: [118400.641718] lkm: module license 'unspecified' taints kernel.Jan 26 18:38:34 fedora19 kernel: [118400.641724] Disabling lock debugging due to kernel taintJan 26 18:38:34 fedora19 kernel: [118400.642077] lkm: module verification failed: signature and/or required key missing - tainting kernelJan 26 18:38:34 fedora19 kernel: [118400.642981] LKM initJan 26 18:41:38 fedora19 kernel: [118585.066364] LKM stopped
内核模块加载器****检查我们正在加载的 LKM 是否具有与 Linux 内核兼容的许可证。我们简单的 LKM 没有提到许可,事实上,日志消息说它是“未指定的”,还说它“污染”了内核。这不是某种难闻的气味,就像太旧的牛奶一样。这意味着内核现在包含一个非开源代码。
如果您向内核开发人员寻求有关错误的帮助,并且他或她注意到“受污染的内核”消息,您可能会被要求在没有污染内核的 LKM 的情况下重现该问题。
Linux 内核开发人员有兴趣推广开放源代码,查看仅包含开放源代码且不包含专有代码的内核中的错误是实现这一目标的一种方式。
对于一个微不足道的内核模块来说就这么多了.
2.1.7 第七步:构建一个简单的字符设备驱动程序
LKM 最常见的用途是创建驱动程序以支持新硬件或创建虚拟设备。
最简单的虚拟设备是 /dev/null,它会丢弃所有写入它的数据,并在读取时立即返回一个 EOF。您可以在 linux/drivers/char/mem.c 中找到 /dev/null 驱动程序的源代码。
我们将编写一个驱动程序来创建一个虚拟设备,该设备在读取时将返回一个伪随机数。这是一个类似于 /dev/random 的虚拟设备,但更简单,并且不用于实际程序中。
这是我们的驱动程序的基本框架,名为myrandom.c:
/* Random number virtual device. */
#include
#define DRIVER_AUTHOR "Michael J. Eager "
#define DRIVER_DESC "random number generator device"
/* Declare license and provide authorship and description. */
MODULE_LICENSE("GPL");
MODULE_AUTHOR(DRIVER_AUTHOR);
MODULE_DESCRIPTION(DRIVER_DESC);/* Declare all functions. */
static int init_myrandom(void);
static void cleanup_myrandom(void);
static int init_myrandom(void){ printk(KERN_ALERT "myrandom initn"); return 0;}
static void cleanup_myrandom(void){ printk(KERN_ALERT "myrandom stoppedn");}
module_init(init_myrandom);
module_exit(cleanup_myrandom);
这看起来很像我们在上一期中创建的简单 LKM,我们需要在 Makefile 中更改的只是替换lkm.o 为myrandom.o.
我们添加了一个 MODULE_LICENSE 宏,说明该驱动程序是根据 GPL 许可的,以及一个列出作者和描述的宏,这些将在您运行modinfo myrandom.ko 文件时显示。我们还在末尾添加了两个宏,module_init 和 module_exit,它们允许我们指定 init 和清理例程的名称,而不是使用默认值。
当您运行insmod myrandom.ko (以 root 身份)和tail /var/log/messages时,您将看到驱动程序在加载时发出的消息。运行rmmod myrandom 将删除驱动程序,并将停止消息添加到系统日志中。
1)创建设备
接下来,让我们告诉内核支持我们的myrandom 设备:
在文件顶部,添加:#include
并添加一些数据结构:static int Major;static struct file_operations fops = {};
更新初始化和清理例程,如下所示:
static int init_myrandom(void)
{ printk(KERN_ALERT "myrandom initn"); Major = register_chrdev(0, "myrandom", &fops); if (Major < 0) { printk(KERN_ALERT "Registering myrandom device failed: %dn", Major); return Major; } printk("Myrandom assigned major number %dn", Major); return 0;
}
static void cleanup_myrandom(void)
{ /* Unregister myrandom device. */ unregister_chrdev(Major, "myrandom"); printk(KERN_ALERT "myrandom stoppedn");
}
当我们使用 insmod 加载这个驱动时,它会注册一个字符设备驱动。
设备表示为 /dev 下的特殊文件,其中有一个表示设备类别的主设备号和一个表示该类中特定设备的次设备号。
如果运行cat /proc/devices ,您将看到系统中定义的设备类别列表,包括myrandom.
当我们调用register_chrdev时,第一个参数为零,我们要求内核分配一个可用的主设备号。
注册设备驱动程序不会在 /dev 下创建设备文件。有几种方法可以做到这一点,我们将使用最简单的方法并以 root 身份运行 mknod 命令,并使用 chmod 使其可供所有用户访问:
# mknod /dev/myrandom c 249 0
# chmod 0666 /dev/myrandom
# ls -l /dev/myrandomcrw-rw-rw- 1 root root 249, 0 Mar 25 08:00 /dev/myrandom
内核为 my driver 分配了主设备号 249;你的号码可能不同。
创建设备文件的其他方法是在我们注册驱动程序时调用内核的 mknod() 函数,这与 mknod 命令的作用相同。
一种更优雅、更灵活的方法是使用 udev,Udev 是一个用户空间守护进程,在注册新设备时由内核通知, 然后它使用 /etc/udev/rules.d 中的规则来创建设备节点。
- 添加设备功能
我们希望我们的设备驱动程序支持打开、关闭和读取操作,并且我们希望写入我们的设备可以返回错误。
让我们定义将执行这些操作的例程:
static int open_myrandom(struct inode *, struct file *);
static int close_myrandom(struct inode *, struct file *);
static ssize_t read_myrandom(struct file *, char *, size_t, loff_t *);
static ssize_t write_myrandom(struct file *, const char *, size_t, loff_t *);
static int Major;
static struct file_operations fops =
{ .read = read_myrandom, .release = close_myrandom, .open = open_myrandom, .write = write_myrandom
};
static int myrandom_in_use = 0;
static int open_myrandom(struct inode *inode, struct file *file)
{ if (myrandom_in_use) return -EBUSY; myrandom_in_use++; return 0;
}
static int close_myrandom(struct inode *inode, struct file *file)
{ if (myrandom_in_use) myrandom_in_use--; return 0;
}
static ssize_t read_myrandom(struct file *filp, char *buf, size_t len, loff_t *ofs)
{ return 0;
}
static ssize_t write_myrandom(struct file *filp, const char *buf, size_t len, loff_t *ofs)
{ return 0;
}
open 例程增加了一个内部标志,以便一次只有一个程序可以打开设备,尝试打开设备的第二个程序将收到busy 错误,关闭例程重置此标志。目前,读取和写入例程是什么都不做的假人。
如果你 insmod 驱动程序,你可以使用dd 命令打开设备并从中读取:
$ dd if=/dev/myrandom0+0 records in0+0 records out0 bytes (0 B) copied, 4.5739e-05 s, 0.0 kB/s
dd 命令打开设备并立即收到 EOF。
- 传输数据
让我们通过在读取设备时返回一些数据来完成这个示例驱动程序:
在文件顶部附近,添加:#include static unsigned char myrandom(void);
让我们创建一个简单的随机字符生成器,每次调用它时返回一个随机字母或数字:
/* Generate random letters and numbers. Algorithm from Wikipedia. */
static unsigned char myrandom(void)
{ static char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; static unsigned int m_w = 0x12345678; static unsigned int m_z = 0x87654321; int myrand; m_z = 36969 * (m_z & 65535) + (m_z >> 16); m_w = 18000 * (m_w & 65535) + (m_w >> 16); myrand = (m_z << 16) + m_w; myrand = (myrand >> 16) % sizeof(letters); return letters[myrand];
}
让我们充实read例程:
static ssize_t read_myrandom(struct file *filp, char *buf, size_t len, loff_t *ofs)
{ unsigned char rand_val; int count = 0; /* Return EOF when all bytes read. */ if (bytes_read == 100) return 0; while (len-- > 0 && bytes_read++ < 100) { rand_val = myrandom(); put_user(rand_val, buf++); count++; } /* Return number of bytes transferred. */ return count;
}
最后,在 open 例程中,让我们在设备打开时将计数清零:
static int open_myrandom(struct inode *inode, struct file *file)
{ if (myrandom_in_use) return -EBUSY; myrandom_in_use++; bytes_read = 0; return 0;
}
当我们读取 /dev/myrandom 时,我们将收到我们请求的随机字符数,最多 100 个。当我们写入设备时,我们会立即收到一条设备已满的消息。
$ dd if=/dev/myrandom bs=10 count=1QFF5tmwf3i1+0 records in1+0 records out10 bytes (10 B) copied, 0.000252246 s, 39.6 kB/s
$ dd if=/dev/myrandome9LDb6se6jJZnrS6prxpbkyTwIaaTlU1YDaz7buUtbvDXw1hxSgImzTc84zF28SZqUtS6tfRO8kl1iQCXEXSGjOTftygRqzV0+1 records in0+1 records out100 bytes (100 B) copied, 0.000145579 s, 687 kB/s
$ dd if=/dev/zero of=/dev/myrandomdd: writing to /dev/myrandom: No space left on device1+0 records in0+0 records out0 bytes (0 B) copied, 0.000461547 s, 0.0 kB/s
请注意,由于从 /dev/myrandom 返回的字符串不是零终止的,因此来自 dd 的消息将附加到输出中。
我们没有讨论的一个主题是将参数传递给驱动程序,可以直接使用 modprobe 或使用 /etc/modprobe.conf 来完成。在我们的示例中,我们可能已经指定了随机数种子。
一个真正的驱动程序可能会将设备地址或选项指定为参数。
我们只涉及了编写设备驱动程序的基础知识,没有涉及硬件相关的问题。我推荐Alessandro Rubini、Jon Corbet 和 Greg Kroah-Hartman 合写的Linux Device Drivers ,由 O’Reilly 出版。第 4 版应该会在 7 月出版,作者名单中会添加 Jessica McKeller。
下一篇:关于嵌入式开发的一些信息汇总:开发模型以及自托管开发(一)