在本章中,您不仅将了解 Linux 内核的一般知识,还将了解有关它的具体内容。 本章将从快速介绍 Linux 的历史及其作用开始,然后继续解释它的各种特性。 用于与 Linux 内核源代码交互的步骤将不会被省略。 您将只了解从源代码获取 Linux 内核映像所需的步骤,还将了解移植新的ARM 机器意味着什么,以及用于调试通常在使用 Linux 内核源代码时可能出现的各种问题的一些方法。 最后,上下文将切换到 Yocto 项目,以展示如何为给定的机器构建 Linux 内核,以及如何在以后从根文件系统映像集成和使用外部模块。
本章将让您了解 Linux 内核和 Linux 操作系统。 如果没有历史部分,这个陈述是不可能的。 Linux 和 UNIX 通常被放在相同的历史背景下,但是尽管 Linux 内核在 1991 年出现,Linux 操作系统很快成为 UNIX 操作系统的替代品,但这两个操作系统是同一家族的成员。 考虑到这一点,UNIX 操作系统的历史不可能是从另一个地方开始的。 这意味着我们需要回到 40 多年前,更准确地说,大约 45 年前到 1969 年,那时丹尼斯·里奇和肯·汤普森开始了 UNIX 的开发。
UNIX 的前身是Multiplexed Information and Computing Service(Multics),这是一个多用户操作系统项目,当时并不处于最佳状态。 由于 Multics 在 1969 年夏天成为贝尔实验室计算机科学研究中心的一个不可行的解决方案,一种文件系统设计诞生了,后来它变成了今天所知的 UNIX。 随着时间的推移,由于其设计和源代码与其一起分发的事实,它被移植到多台机器上。 UNIX 最多产的贡献者是加州大学伯克利分校。 他们还开发了自己的 UNIX 版本,称为Berkeley Software Distribution(BSD),该版本于 1977 年首次发布。 直到 20 世纪 90 年代,许多公司都开发并提供了自己的 UNIX 发行版,它们的主要灵感来自 Berkeley 或 AT&T。所有这些都帮助 UNIX 成为一个稳定、健壮和强大的操作系统。 在使 UNIX 成为操作系统的强大功能中,可以提到以下几点:
- UNIX 很简单。 它使用的系统调用的数量减少到只有几百个,而且它们的设计是基本的
- 在 UNIX 中,所有内容都被视为一个文件,这使得数据和设备的操作变得更简单,并且最大限度地减少了用于交互的系统调用。
- 更快的进程创建时间和
fork()
系统调用。 - UNIX 内核和用 C 语言编写的实用程序,以及使其易于移植和访问的属性。
- 简单而健壮的进程间通信(IPC)原语有助于创建快速而简单的程序,这些程序只以最佳可用方式完成一件事。
如今,UNIX 是一个成熟的操作系统,支持虚拟内存、TCP/IP 网络、按需分页抢占式多处理和多线程等特性。 这些功能分布广泛,从小型嵌入式设备到拥有数百个处理器的系统,不一而足。 它的发展已经超越了 UNIX 是一个研究项目的想法,它已经成为一个通用的、几乎可以满足任何需要的操作系统。 所有这一切都要归功于它优雅的设计和公认的简单性。 它能够在不失去保持简单的能力的情况下进化。
Linux 是名为Minix的 UNIX 变体的替代解决方案,该变体是为教学目的而创建的操作系统,但它缺乏与系统源代码的轻松交互。 由于 Minix 的许可,对源代码所做的任何更改都不容易集成和分发。 Linus Torvalds 首先开始在终端仿真器上工作,以连接他所在大学的其他 UNIX 系统。 在同一学年内,仿真器演变成了成熟的 UNIX。 1991 年,他将其发布,供所有人使用。
Linux 最吸引人的特性之一是它是一个开源操作系统,其源代码可以在 GNU GPL 许可下获得。 在编写 Linux 内核时,Linus Torvalds 使用了 UNIX 操作系统内核变体中提供的最佳设计选择和特性作为灵感的来源。 正是它的执照推动它成为今天的强国。 它雇佣了大量的开发人员,他们帮助进行代码增强、错误修复等工作。
今天,Linux 是一个经验丰富的操作系统,能够在多种架构上运行。 它可以在比手表还小的设备上运行,也可以在超级计算机集群上运行。 它是我们这个时代的新感觉,正以一种日益多样化的方式被世界各地的公司和开发者采用。 人们对 Linux 操作系统的兴趣非常浓厚,这不仅意味着多样性,而且还提供了大量的好处,从安全性、新功能、嵌入式解决方案到服务器解决方案选项等等,不一而足。
Linux 已经成为一个真正的协作项目,它是由一个庞大的社区在互联网上开发的。 虽然这个项目做了很多改变,但 Linus 仍然是它的创造者和维护者。 变化是我们周围一切事物中一个恒定的因素,这适用于 Linux 和它的维护者,他现在被称为 Greg Kroah-Hartman,现在已经成为它的内核维护者两年了。 似乎在 Linus 出现的那个时期,Linux 内核是一个松散的开发人员社区。 这可能是因为莱纳斯的刺耳言论举世闻名。 自从 Greg 被指定为内核维护者后,这个图像开始逐渐淡出。 我期待着未来的岁月。
Linux 内核拥有令人印象深刻的行代码,是最著名的开放源码项目之一,同时也是可用的最大的开放源码项目之一。 Linux 内核构成了一个帮助硬件接口的软件,它是在每个人的 Linux 操作系统上运行的最低级别的代码。 它用作其他用户空间应用的接口,如下图所述:
Linux 内核的主要角色如下:
- 它提供了一组可移植的硬件和体系结构 API,为用户空间应用提供了使用必要硬件资源的可能性
- 它有助于管理硬件资源,如 CPU、输入/输出外设和内存
- 它用于管理并发访问和不同应用对必要硬件资源的使用。
为了确保很好地理解前面的角色,一个示例将非常有用。 让我们考虑这样一种情况:在给定的 Linux 操作系统中,许多应用需要访问相同的资源、网络接口或设备。 对于这些元素,内核需要对资源进行多路复用,以确保所有应用都可以访问它。
本节将介绍 Linux 内核中的一些可用特性。 它还将涵盖有关每个功能的信息、如何使用它们、它们代表什么,以及关于每个特定功能的任何其他相关信息。 每个特性的介绍将使您熟悉 Linux 内核中一些可用特性的主要作用,如以及 Linux 内核及其源代码。
一般而言,Linux 内核拥有的一些最有价值的特性如下:
- 稳定性和可靠性
- 可伸缩性
- 可移植性和硬件支持
- 符合标准
- 各种标准之间的互操作性
- 模块化
- 易于编程
- 社会各界的全面支持
- 安全 / 抵押品 / 保证 / 证券
前面的功能并不构成实际的功能,但在项目的开发过程中起到了帮助作用,至今仍在帮助它。 话虽如此,但还是实现了很多特性,比如快速用户空间互斥锁(Futex)、网络过滤器、简化的强制访问控制内核(SMACK)等等。 这些的完整列表可在http://en.wikipedia.org/wiki/Category:Linux_kernel_features访问和研究。
当讨论 Linux 中的内存时,我们可以将其称为物理内存和虚拟内存。 RAM 内存的分区用于容纳 Linux 内核变量和数据结构,其余内存用于动态分配,如下所述:
物理内存定义了能够维护内存的算法和数据结构,它是由虚拟内存在页面级相对独立地完成的。 这里,每个物理页都有一个与其关联的struct page
描述符,该描述符用于合并有关该物理页的信息。 每页都定义了一个struct page
描述符。 此结构的一些字段如下所示:
_count
:这表示页面计数器。 当它达到0
值时,该页面将被添加到空闲页面列表中。virtual
:这表示与物理页关联的虚拟地址。 始终映射ZONE_DMA和ZONE_NORMAL页面,而不总是映射ZONE_HIGHMEN页面。flags
:这表示一组描述页面属性的标志。
物理内存的区域以前已经存在。 物理内存被分成具有公共物理地址空间和快速本地内存访问的多个节点。 它们中最小的是ZONE_DMA,介于 0 到 16MB 之间。 下一个是zone_Normal,它是 16MB 到 896Mb 之间的 LowMem 区域,最大的是zone_HIGHMEM,它的大小在 900MB 到 4 GB/64 GB 之间。 此信息在前面和后面的图像中都可见:
虚拟内存同时在用户空间和内核空间中使用。 内存区的分配意味着物理页面的分配以及地址空间区域的分配;这既可以在页表中完成,也可以在操作系统内部可用的内部结构中完成。 页表的使用因体系结构类型的不同而不同。 对于复杂指令集计算(CISC)体系结构,页表由处理器使用,但在精简指令集计算(RISC)体系结构上,页表由内核用于页查找和转换后备缓冲器(TLB)加法操作。 每个区域描述符用于区域映射。 它指定如果区域为只读、写入时复制等,则是否映射该区域以供文件使用。 操作系统使用地址空间描述符来维护高级信息。
内存分配在用户空间和内核空间上下文之间是不同的,因为内核空间内存分配不能以简单的方式分配内存。 这种差异主要是由于内核上下文中的错误管理不容易完成,或者至少不是在与用户空间上下文相同的关键字中。 这是本节将与解决方案一起介绍的问题之一,因为它有助于读者理解如何在 Linux 内核上下文中进行内存管理。
内核用于内存处理的方法是这里要讨论的第一个主题。 这样做是为了确保您理解内核用来获取内存的方法。 虽然处理器的最小可寻址单元是字节,即存储器管理单元(MMU),但负责虚拟到物理转换的最小可寻址单元是页。 页面的大小因体系结构不同而不同。 它负责维护系统的页表。 大多数 32 位架构使用 4KB 页面,而 64 位架构通常使用 8KB 页面。 对于 Atmel SAMA5D3-XPlaed 电路板,struct page
结构定义如下:
struct page {
unsigned long flags;
atomic_t _count;
atomic_t _mapcount;
struct address_space *mapping;
void *virtual;
unsigned long debug_flags;
void *shadow;
int _last_nid;
};
这是页面结构中最重要的字段之一。 以为例,flags
字段表示页面的状态;它保存诸如页面是否脏、是否锁定或处于另一个有效状态等信息。 与此标志关联的值在include/linux/page-flags-layout.h
头文件中定义。 virtual
字段表示与页面相关联的虚拟地址,count
字段表示通常可通过page_count()
函数间接访问的页面的计数值。 所有其他字段都可以在include/linux/mm_types.h
头文件中访问。
内核将硬件划分为不同的内存区,主要是因为物理内存中有一些页对于许多任务是不可访问的。 例如,有些硬件设备可以执行 DMA。 这些操作是通过仅与物理内存区(简称为ZONE_DMA
)交互来完成的。 对于 x86 架构,它可以在 0-16 Mb 之间访问。
有四个主内存区可用,另外两个不太重要的内存区是在include/linux/mmzone.h
头文件的内核源代码中定义的。 对于 Atmel SAMA5D3-XPlaed 主板,区域映射也取决于体系结构。 我们定义了以下区域:
enum zone_type {
#ifdef CONFIG_ZONE_DMA
/*
* ZONE_DMA is used when there are devices that are not able
* to do DMA to all of addressable memory (ZONE_NORMAL). Then we
* carve out the portion of memory that is needed for these devices.
* The range is arch specific.
*
* Some examples
*
* Architecture Limit
* ---------------------------
* parisc, ia64, sparc <4G
* s390 <2G
* arm Various
* alpha Unlimited or 0-16MB.
*
* i386, x86_64 and multiple other arches
* <16M.
*/
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
/*
* x86_64 needs two ZONE_DMAs because it supports devices that are
* only able to do DMA to the lower 16M but also 32 bit devices that
* can only do DMA areas below 4G.
*/
ZONE_DMA32,
#endif
/*
* Normal addressable memory is in ZONE_NORMAL. DMA operations can be
* performed on pages in ZONE_NORMAL if the DMA devices support
* transfers to all addressable memory.
*/
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
/*
* A memory area that is only addressable by the kernel through
* mapping portions into its own address space. This is for example
* used by i386 to allow the kernel to address the memory beyond
* 900MB. The kernel will set up special mappings (page
* table entries on i386) for each page that the kernel needs to
* access.
*/
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
__MAX_NR_ZONES
};
有一些分配需要与多个个区域交互。 一个这样的例子是能够使用ZONE_DMA
或ZONE_NORMAL
的普通分配。 最好使用ZONE_NORMAL
,因为它不会干扰直接内存访问,不过当内存完全使用时,内核可能会使用正常情况下使用的区域以外的其他可用区域。 可用的内核是定义每个区域的相关信息的struct zone结构。 对于 Atmel SAMA5D3-XPlaed 主板,此结构如下所示:
struct zone {
unsigned long watermark[NR_WMARK];
unsigned long percpu_drift_mark;
unsigned long lowmem_reserve[MAX_NR_ZONES];
unsigned long dirty_balance_reserve;
struct per_cpu_pageset __percpu *pageset;
spinlock_t lock;
int all_unreclaimable;
struct free_area free_area[MAX_ORDER];
unsigned int compact_considered;
unsigned int compact_defer_shift;
int compact_order_failed;
spinlock_t lru_lock;
struct lruvec lruvec;
unsigned long pages_scanned;
unsigned long flags;
unsigned int inactive_ratio;
wait_queue_head_t * wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn;
unsigned long spanned_pages;
unsigned long present_pages;
unsigned long managed_pages;
const char *name;
};
正如你所看到的,定义这个结构的区域是一个令人印象深刻的区域。 一些最有趣的字段由watermark
变量表示,该变量包含定义的区域的高、中和低水位线。 present_pages
属性表示区域内的个可用页面。 name
字段表示分区的名称,以及其他字段,例如lock
字段,这是一种保护分区结构以供同时访问的自旋锁。 在 Atmel SAMA5D3 XPlaed 主板的相应include/linux/mmzone.h
头文件中可以识别的所有其他字段。
有了这些信息,我们就可以继续前进,了解内核是如何实现内存分配的。 通常,内存分配和内存交互所需的所有可用函数都在linux/gfp.h
头文件中。 其中一些功能包括:
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)
此函数用于在连续位置分配物理页面。 最后,如果分配成功,则由第一页结构的指针表示返回值,如果出现错误,则由NULL
表示:
void * page_address(struct page *page)
此函数用于获取相应内存页的逻辑地址:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
此函数类似于alloc_pages()
函数,但不同之处在于struct page * alloc_page(gfp_t gfp_mask)
返回参数中提供了返回变量:
unsigned long __get_free_page(gfp_t gfp_mask)
struct page * alloc_page(gfp_t gfp_mask)
前面两个函数是类似函数的封装器,不同之处在于该函数只返回一个页面信息。 此函数的顺序具有zero
值:
unsigned long get_zeroed_page(unsigned int gfp_mask)
前面的函数如其名称所示。 它返回充满zero
值的页面。 此函数与__get_free_page()
函数的不同之处在于,释放后,页面将填充zero
值:
void __free_pages(struct page *page, unsigned int order)
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)
前面的函数用于释放给定的已分配页面。 页面的传递应该小心,因为内核无法检查提供给它的信息。
通常磁盘比物理内存慢,这是内存比磁盘存储更受欢迎的原因之一。 这同样适用于处理器的高速缓存级别:它离处理器越近,I/O 访问就越快。 将数据从磁盘移动到物理内存的过程称为页缓存。 逆过程被定义为页写回。 这这两个概念将在这一小节中介绍,但它主要是关于内核上下文的吗?
内核第一次调用read()
系统调用时,会验证数据是否存在于页缓存中。 在 RAM 中找到页面的过程称为缓存命中。 如果在那里不可用,则需要从磁盘读取数据,此过程称为高速缓存未命中。
当内核发出Write()系统调用时,与此系统调用相关的缓存交互有多种可能。 最简单的方法是不缓存写系统调用操作,只将数据保存在磁盘中。 这种情况称为无写缓存。 当写入操作同时更新物理存储器和磁盘数据时,该操作称为直写式高速缓存。 第三个选项由回写高速缓存表示,其中页面被标记为脏。 它会被添加到脏列表中,随着时间的推移,它会被放到磁盘上并标记为不脏。 DIRESS 关键字的最佳同义词由 SYNCHRONED 关键字表示。
除了自己的物理内存外,内核还负责用户空间进程和内存管理。 分配给每个用户空间进程的内存称为进程地址空间,它包含给定进程可寻址的虚拟内存。 它还包含进程在与虚拟内存交互时使用的相关地址。
通常,进程接收平面 32 位或 64 位地址空间,其大小取决于体系结构类型。 然而,有些操作系统分配分段地址空间。 为线程提供了在操作系统之间共享地址空间的可能性。 虽然一个进程可以访问很大的内存空间,但它通常只有权访问一段内存。 这称为,称为内存区,这意味着进程只能访问位于可行内存区内的内存地址。 如果它以某种方式试图管理其有效内存区之外的内存地址,内核将通过分段故障通知终止该进程。
存储器区域包含以下内容:
text
部分映射源代码data
部分映射初始化的全局变量bss
部分映射未初始化的全局变量zero page
部分用于处理用户空间堆栈shared libraries text
、bss
和特定于数据的部分- 映射的文件
- 匿名内存映射通常与函数链接,如
malloc()
- 共享内存段
进程地址空间是通过内存描述符在 Linux 内核源内部定义的。 此结构称为struct mm_struct
,它定义在include/linux/mm_types.h
头文件中,包含与进程地址空间相关的信息,如使用地址空间的进程数、内存区列表、最后使用的内存区、可用内存区的数量、代码、数据、堆和堆栈节的开始和结束地址。
对于内核线程,没有关联的进程地址空间;对于内核,进程描述符结构定义为NULL
。 这样,内核就会提到内核线程没有用户上下文。 内核线程只能访问与所有其他进程相同的内存。 内核线程在用户空间中没有任何页面,也没有对用户空间内存的访问权。
由于处理器只使用物理地址,因此需要在物理内存和虚拟内存之间进行转换。 这些操作由页表完成,页表将虚拟地址拆分成较小的组件,并带有用于指向目的的关联索引。 通常,在大多数可用的主板和体系结构中,页表查找都是由硬件处理的;内核负责设置页表。
如前所述,进程是 Linux 操作系统中的基本单元,同时也是一种抽象形式。 事实上,它是一个正在执行的程序,但程序本身并不是一个过程。 它需要处于活动状态,并且具有关联的资源。 进程可以通过使用fork()
函数成为父进程,这将生成子进程。 父进程和子进程都位于不同的地址空间中,但它们具有相同的内容。 exec()
系列函数能够执行不同的程序,创建地址空间,并将其加载到该地址空间中。
当使用fork()
时,将为子进程再现父进程拥有的资源。 该函数以一种非常有趣的方式实现;它使用clone()
系统调用,在其基础上包含copy_process()
函数。 此函数执行以下操作:
- 调用
dup_task_struct()
函数以创建新的内核堆栈。task_struct
和thread_info
结构是为新工艺创建的。 - 检查该子对象是否未超出内存区的限制。
- 子进程与其父进程不同。
- 它被设置为
TASK_UNINTERRUPTIBLE
以确保它不运行。 - 更新标志。
PID
与子进程相关联。- 检查已经设置的标志,并针对它们的值执行适当的操作。
- 当获得子进程指针时,在结束时执行清理进程。
Linux 中的线程与进程非常相似。 它们被视为共享各种资源(如内存地址空间、打开的文件等)的进程。 线程的创建类似于普通任务,不同之处在于clone()
函数,该函数传递提及共享资源的标志。 例如,克隆函数调用的线程是clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
,而普通的 fork 看起来类似于clone(SIGCHLD, 0)
。
内核线程的概念是作为涉及在内核上下文背景中运行的任务的问题的解决方案出现的。 内核线程没有地址空间,只能在内核上下文中使用。 它具有与普通进程相同的属性,但仅用于特殊任务,如ksoftirqd
、flush
等。
在执行结束时,需要终止进程,以便可以释放资源,并且需要通知正在执行的进程的父进程。 最常用于终止进程的方法是通过调用exit()
系统调用来完成的。 此过程需要执行多个步骤:
- 设置
PF_EXITING
标志。 - 调用
del_timer_sync()
函数来删除内核计时器。 - 写入记账和记录信息时调用
acct_update_integrals()
函数。 - 调用
exit_mm()
以释放进程的mm_struct
结构。 - 调用
exit_sem()
将进程从 IPC 信号量中出列。 - 调用
exit_files()
和exit_fs()
函数来删除指向各种文件描述符的链接。 - 应设置任务退出代码。
- 调用
exit_notify()
通知父级并将任务退出状态设置为EXIT_ZOMBIE
。 - 调用
schedule()
切换到新进程。
执行完上述步骤后,与此任务关联的对象将被释放,并且它将变得不可运行。 它的记忆仅作为其父代的信息而存在。 在其父内存声明此信息对其没有用处后,将释放此内存供系统使用。
进程调度器决定为可运行进程分配哪些资源。 它是一款软件,负责多任务处理,为各种进程分配资源,并决定如何最好地设置资源和处理器时间。 它还决定接下来应该运行哪些进程。
Linux 调度器的第一个设计非常简单。 当进程数量增加时,它无法正确扩展,因此从 2.5 内核版本开始,开发了一个新的调度器。 它称为,称为O(1)调度器,并提供用于时间片计算的恒定时间算法和基于每个处理器定义的运行队列。 尽管它非常适合大型服务器,但它不是普通桌面系统的最佳解决方案。 从 2.6 内核版本开始,对 O(1)调度器进行了改进,比如公平调度的概念后来从内核版本 2.6.23 变成了完全公平调度器(CFS),成为事实上的调度器。
CFC 背后有一个简单的想法。 它的行为就像我们有一个完美的多任务处理器,其中每个进程获得处理器时间的1/n
片,而这个时间片非常小。 n
值表示正在运行的进程数。 Con Kolivas 是为公平调度实现做出贡献的澳大利亚程序员,也被称为旋转楼梯截止日期调度器(RSDL)。 它的实现需要一个用于自平衡优先级的红黑树,以及一个在纳秒级别计算的时间片。 与 O(1)调度器类似,CFS 应用了权重的概念,这意味着某些进程比其他进程等待的时间更长。 这是基于加权公平排队算法的。
进程调度程序构成了 Linux 内核最重要的组件之一,因为它通常定义了用户与操作系统的交互。 Linux 内核 CFS 是吸引开发人员和用户的调度器,因为它以最合理的方式提供了可伸缩性和性能。
对于要与系统交互的进程,应该提供一个接口,以使用户空间应用能够与硬件和其他processes.System
调用进行交互。 它们用作硬件和用户空间之间的接口。 一般而言,它们还用于确保稳定性、安全性和抽象性。 这些是与陷阱和异常一起构成内核入口点的通用层,如下所述:
与 Linux 系统内可用的大多数系统调用的交互是使用 C 库完成的。 它们能够定义许多参数,并返回一个显示它们是否成功的值。 值zero
通常意味着执行以成功结束,如果出现错误,errno
变量中将提供错误代码。 系统调用完成后,将执行以下步骤:
- 切换到内核模式。
- 消除了对内核空间访问的任何限制。
- 来自用户空间的堆栈被传递到内核空间。
- 来自用户空间的任何参数都会被检查并复制到内核空间。
- 识别并运行系统调用的关联例程。
- 切换到用户空间,继续执行应用。
系统调用有一个与之关联的syscall
号,这是一个唯一的号码,用作不可更改的系统调用的引用(不可能实现系统调用)。 在<sys/syscall.h>
头文件中提供了每个系统调用号的符号常量。 要检查系统调用是否存在,可以使用sys_ni_syscall()
,它为无效的系统调用返回ENOSYS
错误。
Linux 操作系统能够支持多种文件系统选项。 这是由于虚拟文件系统(VFS)的存在,它能够为大量文件系统类型提供公共接口并处理与它们相关的系统调用。
VFS 支持的文件系统类型可以分为以下三类:
- 基于磁盘的文件系统:它们管理本地磁盘或用于磁盘仿真的设备上的内存。 其中一些最广为人知的例子是:
- Linux 文件系统,例如第二扩展文件系统(Ext2)、第三扩展文件系统(Ext3)和第四扩展文件系统(Ext4)
- UNIX 文件系统,如 sysv 文件系统、ufs、Minix 文件系统等
- Microsoft 文件系统,如 MS-DOS、NTFS(从 Windows NT 开始提供)和 VFAT(从 Windows 95 开始提供)
- ISO966 CD-ROM 文件系统和磁盘格式 DVD 文件系统
- 专有文件系统,例如来自 Apple、IBM 和其他公司的文件系统
- 网络文件系统:允许它们通过网络访问其他计算机上的各种文件系统类型。 最广为人知的之一是 NFS。 当然,还有其他的,但他们没有那么出名。 其中包括Andrew 文件系统(AFS)、Novel 的 NetWare 核心协议(NCP)、恒定数据可用性(Coda)等等。
- 特殊文件系统:
/proc
文件系统是这类文件系统的完美示例。 这种类型的文件系统使系统应用能够更容易地访问内核的数据结构并实现各种功能。
虚拟文件系统系统调用实现在下图中进行了很好的总结:
在前面的图中,可以看到从一种文件系统类型到另一种文件系统类型处理副本是多么容易。 它只使用可用于所有其他文件系统交互的基本open()
、close()
、read()
、write()
函数。 但是,它们都实现了所选文件系统的底层特定功能。 例如,open()
系统调用sys_open()
,它接受与open()
相同的参数并返回相同的结果。 sys_open()
和open()
之间的区别在于sys_open()
是一个更宽松的函数。
其他三个系统调用都有内部调用的相应sys_read()
、sys_write()
和sys_close()
函数。
中断是改变处理器执行的指令序列的事件的表示。 中断意味着由硬件生成的电信号,用于通知已经发生的事件,例如按键、复位等。 中断根据其参考系统分为更多类别,如下所示:
- 软件中断:这些通常是从外部设备和用户空间程序触发的异常[T1
- 硬件中断:这些是来自系统的信号,通常指示处理器特定指令
Linux 中断处理层通过全面的 API 函数为各种设备驱动程序提供了中断处理的抽象。 它用于请求、启用、禁用和释放中断,确保在多个平台上保证可移植性。 它处理所有可用的中断控制器硬件。
通用中断处理使用__do_IRQ()
处理程序,该处理程序能够处理所有可用的中断逻辑类型。 处理层分为两个组件:
- 上半部分组件用于响应中断
- 下半部分组件由上半部分安排在稍后运行
它们之间的区别是所有可用中断都被允许在下半部分上下文中动作。 这有助于上半部分在下半部分工作时响应另一个中断,这意味着它能够将其数据保存在特定的缓冲区中,并且它允许下半部分在安全的环境中操作。
对于下半部处理,有四种定义的机制可用:
- 软中断
- 微线程
- 工作队列
- 内核线程
下面很好地介绍了可用的机制:
尽管上半部分和下半部分中断机制的模型看起来很简单,但它有一个非常复杂的函数调用机制模型。 此示例显示了 ARM 体系结构的这一事实:
对于中断的上半部分组件,中断源代码中有三个抽象级别。 第一个是具有request_irq()
、free_irq
、disable_irq()
、enable_irq()
等功能的高级驱动程序 API。 第二个由高级 IRQ 流处理程序表示,这是一个通用层,具有预定义的或特定于体系结构的中断流处理程序,用于在设备初始化或引导时响应各种中断。 它定义了许多预定义函数,如handle_level_irq()
、handle_simple_irq()
、handle_percpu_irq()
等。 第三种是芯片级硬件封装。 它定义了struct irq_chip
结构,该结构保存 IRQ 流实现中使用的与芯片相关的函数。 其中一些函数是irq_ack()
、irq_mask()
和irq_unmask()
。
需要一个模块来注册中断通道,然后将其释放。 支持的请求总数从0
值到 IRQ-1 的数量计算。 此信息位于<asm/irq.h>
头文件中。 注册完成后,处理程序标志将传递给request_irq()
函数,以指定中断处理程序的类型,如下所示:
SA_SAMPLE_RANDOM
:这表明中断可以通过采样不可预测的事件(如鼠标移动、按键间时间、磁盘中断等)来贡献熵池,即具有具有很强随机性的位的池SA_SHIRQ
:这表明中断可以在设备之间共享。SA_INTERRUPT
:这表示快速中断处理程序,因此在当前处理器上禁用中断-这并不代表非常理想的情况
将讨论的关于下半部分中断处理的第一种机制由softirqs
表示。 它们很少使用,但可以在kernel/softirq.c
文件中的 Linux 内核源代码中找到。 当涉及到实现时,它们是在编译步骤静态分配的。 它们是在include/linux/interrupt.h
头文件中添加条目时创建的,并且它们提供的系统信息在/proc/softirqs
文件中可用。 虽然不经常使用,但它们可以在异常、中断、系统调用之后执行,也可以在调度程序运行ksoftirkd
守护进程时执行。
列表中的下一个是微线程。 尽管它们构建在softirqs
之上,但它们更常用于下半部分中断处理。 以下是这样做的一些原因:
- 他们跑得很快
- 它们可以动态创建和销毁
- 它们有原子代码和非阻塞代码
- 它们在软中断上下文中运行
- 它们在预定的同一处理器上运行
Tasklet 有一个struct tasklet_struct结构可用。 这些在include/linux/interrupt.h
头文件中也可用,并且与softirqs
不同,微线程是不可重入的。
列表中的第三个是工作队列,与前面提出的机制相比,它们代表了一种不同形式的执行分配的工作。 主要区别如下:
- 它们可以在多个 CPU 上同时运行
- 他们被允许睡觉
- 它们在流程上下文中运行
- 它们可以被调度或抢占
尽管它们的延迟可能比微线程稍大一些,但前面的这些特性确实很有用。 微线程围绕struct workqueue_struct
结构构建,可在kernel/workqueue.c
文件中使用。
最后也是最新添加到下半部分机制选项的是内核线程,它们完全在内核模式下操作,因为它们是由内核创建/销毁的。 它们出现在 2.6.30 内核发布期间,并且还具有与工作队列相同的优势,以及一些额外的特性,比如拥有自己的上下文的可能性。 预计内核线程最终将取代工作队列和微线程,因为它们类似于用户空间线程。 驱动程序可能希望请求线程中断处理程序。 在这种情况下,它所需要做的就是以类似于request_irq()
的方式使用request_threaded_irq()
。 函数request_threaded_irq()
提供了传递处理程序的可能性,thread_fn
提供了将中断处理代码分成两部分的可能性。 除此之外,还会调用quick_check_handler
来检查中断是否是从设备调用的;如果是这样,它将需要调用IRQ_WAKE_THREAD
来唤醒处理程序线程并执行thread_fn
。
内核正在处理的请求数量与服务器必须接收的请求数量相提并论。 这种情况可以处理竞争条件,因此需要一个好的同步方法。 通过定义内核控制路径,可以使用许多策略来控制内核的行为方式。 以下是内核控制路径的示例:
上图清楚地说明了为什么需要同步。 例如,当多个内核控制路径相互链接时,可能会出现争用情况。 为了保护这些关键区域,应该采取一系列措施。 此外,还应考虑到中断处理程序不能中断,并且softirqs
不应交错。
许多同步原语已经诞生:
- 每 CPU 变量:这是最简单高效的同步方法之一。 它将数据结构相乘,以便每个 CPU 都可以使用每个数据结构。
- 原子操作:此指的是原子读-修改-写指令。
- 内存屏障:这样可以确保在屏障之前完成的所有操作在开始之后的操作之前都已完成。
- 自旋锁:这个表示一种实现崩溃等待的锁。
- 信号量:这个是一种实现休眠或阻塞等待的锁定形式。
- seqlock:此类似于旋转锁,但基于访问计数器。
- 本地中断禁用:这将禁止在单个 CPU 上使用可以推迟的功能。
- 读取-复制-更新(RCU):这是一种旨在保护用于读取的最常用数据结构的方法。 它使用指针提供对共享数据结构的无锁访问。
使用前面的方法,会尝试修复竞争条件的情况。 开发人员的工作是识别和解决可能出现的所有最终同步问题。
在 Linux 内核周围,有大量受时间影响的函数。 从调度程序到系统正常运行时间,它们都需要时间参考,包括绝对时间和相对时间。 例如,需要为将来安排的事件表示相对时间,这实际上意味着有一种方法用于计算时间。
计时器实现可以根据事件类型的不同而不同。 周期性实现由系统定时器定义,该定时器在固定的时间段发出中断。 系统定时器是以给定频率发出定时器中断以更新系统时间并执行必要任务的硬件组件。 另一种可以使用的是实时时钟,这是一种附加了电池的芯片,可以在系统关闭很长一段时间后继续计时。 除了系统时间之外,还有一些动态计时器可用,由内核动态管理以计划特定时间过后运行的事件。
定时器中断有一个发生窗口,对于 ARM,它是每秒 100 次。 这称为系统定时器频率或滴答率,其测量单位为赫兹(Hz)。 滴答率因架构不同而不同。 如果其中大多数的值为 100 Hz,则还有其他值为 1024 Hz,例如 Alpha 和 Itanium(IA-64)体系结构。 默认值当然可以更改和增加,但此操作有其优点和缺点。
更高频率的一些优势包括:
- 计时器将执行得更准确、数量更多
- 使用超时的系统调用以更精确的方式执行
- 正常运行时间测量和其他类似测量正变得更加精确
- 进程抢占更准确
另一方面,更高频率的缺点意味着更高的开销。 处理器在定时器中断上下文中花费的时间更多;此外,由于进行了更多的计算,功耗也会增加。
Linux 操作系统自开始引导以来的总滴答数存储在include/linux/jiffies.h
头文件内的名为Jiffies的变量中。 在引导时,该变量被初始化为零,每次中断发生时,该变量的值加 1。 因此,系统正常运行时间的实际值可以以 Jiffies/Hz 的形式计算。
到目前为止,您已经了解了 Linux 内核的一些特性。 现在,是时候介绍更多关于开发过程、版本控制方案、社区贡献以及与 Linux 内核的交互的信息了。
Linux 内核是一个著名的开源项目。 为了确保开发人员知道如何与其交互,将提供有关如何与此项目进行git
交互的信息,同时还将介绍有关其开发和发布过程的一些信息。 该项目已经演变,其开发过程和发布过程也随之演变。
在介绍实际的开发过程之前,有必要回顾一下历史。 在 Linux 内核项目的 2.6 版本之前,每两年或三年发布一次,每个版本都由偶数中间数字标识,如 1.0.x、2.0.x 和 2.6.x。 相反,开发分支是使用偶数定义的,例如 1.1.x、2.1.x 和 2.5.x,它们被用来集成各种特性和功能,直到准备好一个主要版本并准备好发布。 所有的次要版本都有名字,比如 2.6.32 和 2.2.23,它们都是在主要发布周期之间发布的。
这种工作方式一直保持到 2.6.0 版本,在每个小版本中内核中都添加了大量功能,并且所有这些功能都很好地组合在一起,不需要分支到新的开发分支。 这意味着更快的发布速度和更多可用的功能。 因此,自 2.6.14 内核发布以来,出现了以下变化:
- 所有新的次要发布版本(如 2.6.x)都包含一个两周的合并窗口,在此窗口中可能会在下一个版本中引入许多功能
- 此合并窗口将使用名为 2.6.(X+1)-RC1 的发布测试版本关闭
- 然后是 6-8 周的错误修复期,届时添加的功能引入的所有错误都应该得到修复
- 在错误修复间隔内,对发布候选版本运行测试,发布了 2.6.(X+1)-RCY 测试版本
- 在完成最终测试并且认为最后一个候选版本足够稳定之后,将创建一个名称为 2.6.(X+1)的新版本,并且此过程将再次继续
这个过程运行得很好,但唯一的问题是错误修复只针对 Linux 内核的最新稳定版本发布。 人们需要针对旧版本的长期支持版本和安全更新,以及有关长期支持的这些版本的一般信息,等等。
这一过程随着时间的推移发生了变化,2011 年 7 月,3.0 Linux 内核版本出现了。 它似乎有几个小的更改,旨在改变交互的方式,以解决前面提到的请求。 对编号方案进行了更改,如下所示:
- 内核官方版本将命名为 3.x(3.0、3.1、3.2 等)
- 稳定版本将命名为 3.x.y(3.0.1、3.1.3 等)
虽然它只从编号方案中去掉了一个数字,但这一改变是必要的,因为它标志着 Linux 内核问世 20 周年。
由于 Linux 内核中每天都包含大量的补丁和功能,因此很难跟踪所有的更改以及总体情况。 随着时间的推移,这种情况发生了变化,因为像http://kernelnewbies.org/LinuxChanges和http://lwn.net/这样的站点似乎帮助开发人员与 Linux 内核的世界保持联系。
除了这些链接,git
版本控制系统还可以提供非常需要的信息。 当然,这需要在工作站上存在 Linux 内核源克隆。 提供大量信息的一些命令包括:
git log
:此列出所有提交,最新提交位于列表顶部git log –p
:列出所有提交及其对应的diffs
git tag –l
:列出可用标签git checkout <tagname>
:这将从工作存储库中签出分支或标记git log v2.6.32..master
:列出给定标签和最新版本之间的所有更改git log –p V2.6.32..master MAINTAINERS
:这列出了MAINTAINERS
文件中两个给定分支之间的所有差异
当然,这只是一个包含有用命令的小列表。 所有其他命令都可从http://git-scm.com/docs/获得。
Linux 内核支持多种 CPU 架构。 每个架构和单个线路板都有自己的维护人员,此信息可在MAINTAINERS
文件中获得。 此外,板移植之间的差异主要由体系结构决定,PowerPC 与 ARM 或 x86 有很大的不同。 由于本书重点介绍的开发板是采用 ARM Cortex-A5 内核的 Atmel,因此本节将重点介绍 ARM 架构。
在我们的例子中,主要关注的是arch/arm
目录,它包含子目录,如boot
、common
、configs
、crypto
、firmware
、kernel
、kvm
、lib
、mm
、net
、nwfpe
、oprofile
、tools
、vfp
和xen
。 它还包含许多特定于不同 CPU 系列的重要目录,如mach-*
目录或plat-*
目录。 第一个mach-*
类别包含对 CPU 和几个使用该 CPU 的主板的支持,第二个plat-*
类别包含特定于平台的代码。 一个例子是plat-omap
,它包含mach-omap1
和mach-omap2
的通用代码。
自 2011 年以来,ARM 架构的发展经历了巨大的变化。 如果在此之前 ARM 没有使用设备树,那是因为它需要将大部分代码保存在mach-*
特定目录中,并且对于在 Linux 内核中具有支持的每个板,都会关联一个唯一的机器 ID,并且每个包含特定信息和一组回调的板都会关联一个机器结构。 引导加载程序将此机器 ID 传递给特定的 ARM 注册表,通过这种方式,内核可以识别主板。
ARM 架构的流行是随着工作的重构和设备树的引入而来的,设备树极大地减少了mach-*
目录中可用的代码量。 如果 Linux 内核支持 SoC,那么添加对电路板的支持就像在/arch/arm/boot/dts
目录中用适当的名称定义一个设备树一样简单。 例如,对于<soc-name>-<board-name>.d
,如有必要,请包括相关的dtsi
文件。 通过将设备树包含到ARM/ARM/BOOT/DTS/Makefile中,确保您构建了设备树 BLOB(DTB),并为线路板添加了缺少的设备驱动程序。
如果主板在 Linux 内核中没有支持,则需要在mach-*
目录中进行适当的添加。 在每个mach-*
目录中,有三种类型的文件可用:
- 通用代码文件:这些通常只有一个单词名称,如
clock.c
、led.c
等 - CPU 特定代码:此用于机器 ID,通常具有
<machine-ID>*.c
形式-例如,at91sam9263.c
、at91sam9263_devices.c
、sama5d3.c
等 - 板码:这个通常定义为 board-*.c,如
board-carmeva.c
、board-pcontrol-g20.c
、board-pcontrol-g20.c
等
对于给定的板,应首先在arch/arm/mach-*/Kconfig
文件内进行正确的配置;为此,应识别板 CPU 的机器 ID。 配置完成后,即可开始编译,因此,也应使用所需的文件更新arch/arm/mach-*/Makefile
,以确保电路板支持。 另一个步骤由定义线路板的机器结构和需要在board-<machine>.c
文件中定义的机器类型号表示。
机器结构使用两个宏:MACHINE_START
和MACHINE_END
。 两者都在arch/arm/include/asm/march/arch.h
内部定义,并用于定义machine_desc
结构。 机器型号可在arch/arm/tools/mach_types
文件中找到。 该文件用于生成线路板的include/asm-arm/mach-types.h
文件。
机器类型的更新后的编号列表可在http://www.arm.linux.org.uk/developer/machines/download.php获得。
在第一种情况下启动引导过程时,只需要将dtb
传递给引导加载程序并加载即可初始化 Linux 内核,而在第二种情况下,需要将机器类型号加载到R1
寄存器中。 在早期引导过程中,__lookup_machine_type
查找machine_desc
结构并加载它以初始化电路板。
在将此信息呈现给您之后,如果您渴望为 Linux 内核做出贡献,那么接下来应该阅读本节内容。 如果您真的想为 Linux 内核项目做出贡献,那么在开始这项工作之前应该执行几个步骤。 这主要与文献记载和对该主题的调查有关。 没有人想免费发送一个重复的补丁或复制别人的工作,所以在互联网上搜索你感兴趣的主题可以节省很多时间。 其他有用的建议是,在您熟悉了主题之后,避免发送变通方法。 试着解决问题并提供解决方案。 如果没有,请报告问题并详细描述。 如果找到了解决方案,则在补丁中提供问题和解决方案。
开放源码社区中最有价值的事情之一就是您可以从他人那里获得的帮助。 分享你的问题和问题,但不要忘了提及解决方案。 在适当的邮件列表中询问问题,如果可能,尽量避开维护人员。 他们通常非常忙,有成百上千封电子邮件要阅读和回复。 在寻求帮助之前,试着研究一下你想要提出的问题,这不仅在阐述问题时会有帮助,而且还能提供答案。 如果可能的话,可以使用 IRC 来解决较小的问题,最后,但最重要的是,尽量不要过度使用 IRC。
在准备补丁时,请确保在相应的分支上完成补丁,并首先读取Documentation/BUG-HUNTING
文件。 识别错误报告(如果有),并确保将补丁链接到它们。 在发送之前,请毫不犹豫地阅读Documentation/SubmittingPatches
指南。 此外,在正确测试更改之前,不要发送更改。 总是在你的补丁上签名,并使第一行描述尽可能具有提示性。 在发送补丁时,请找到合适的邮件列表和维护人员,并等待回复。 解决注释并在需要时重新提交它们,直到补丁程序被认为是可接受的。
linux 内核的官方位置在http://www.kernel.org,但是有很多较小的社区使用他们的特性来贡献 linux 内核,甚至维护他们自己的版本。
虽然 Linux 内核包含调度器、内存管理和其他功能,但它的大小相当小。 极其大量的设备驱动程序、体系结构和主板支持,再加上文件系统、网络协议和所有其他组件,使得 Linux 内核变得非常庞大。 通过查看 Linux 目录的大小可以看出这一点。
Linux 源代码结构包含以下目录:
arch
:此包含依赖于体系结构的代码block
:此包含块层核心crypto
:此包含加密库drivers
:此收集除声音驱动程序之外的所有设备驱动程序实现fs
:此收集文件系统的所有可用实现include
:此包含内核头init
:这个有 Linux 初始化代码ipc
:此保存进程间通信实现代码kernel
:这是 Linux 内核的核心lib
:此包含各种库,如zlibc
、crc
等mm
:这个包含内存管理的源代码net
:此提供对 Linux 内部支持的所有网络协议实现的访问samples
:此提供了许多示例实现,如kfifo
、kobject
等scripts
:这是内部和外部使用的security
:这个有很多安全实现,比如apparmor
、selinux
、smack
等等sound
:此包含声音驱动程序和支持代码usr
:这是生成源代码的initramfs cpio
归档文件virt
:这个包含虚拟化支持的源代码COPYING
:此表示 Linux 许可和定义复制条件CREDITS
:这个代表 Linux 的主要贡献者的集合Documentation
:本包含内核源代码的相应文档Kbuild
:此表示顶级内核构建系统Kconfig
:这是配置参数的顶级描述符MAINTAINERS
:这是一个包含每个内核组件的维护者的列表Makefile
:此表示顶级生成文件README
:这个文件描述了什么是 Linux,它是理解项目的起点REPORTING-BUGS
:本提供有关错误报告程序的信息
可以看出,Linux 内核的源代码相当大,因此需要一个浏览工具。 可以使用的工具有很多,例如Cscope、Kscope或 Web 浏览器Linux 交叉引用(LXR)。 CSCOPE 是一个巨大的项目,还可以使用vim
和emacs
的扩展。
在构建 Linux 内核映像之前,需要进行适当的配置。 考虑到我们可以访问成百上千个组件,如驱动程序、文件系统和其他项目,这很难做到。 选择过程是在配置阶段内完成的,这可以通过依赖项定义来实现。 用户有机会使用和定义多个启用的选项,以便定义将用于构建特定主板的 Linux 内核映像的组件。
所有特定于支持的电路板的配置都位于一个配置文件中,简单地命名为.config
,它位于与先前提供的文件和目录位置相同的级别。 它们的形式通常表示为configuration_key=value
。 当然,这些配置之间存在依赖关系,因此它们是在Kconfig
文件中定义的。
以下是一些可用于配置密钥的变量选项:
bool
:这些选项可以具有 TRUE 或 FALSE 值tristate
:除了 TRUE 和 FALSE 选项外,它还显示为模块选项int
:这些值不是那个价差,但它们通常有一个确定的值范围string
:这些值也不是分布最广的值,但通常包含一些非常基本的信息
关于Kconfig
文件,有两个选项可用。 第一个选项使选项 A 仅在选项 B 启用且定义为依赖于时可见,第二个选项提供启用选项 A 的可能性。这是在选项自动启用并定义为SELECT时完成的。
除了手动配置.config
文件之外,配置对于开发人员来说是最糟糕的选择,主要是因为它可能会错过某些配置之间的依赖关系。 我建议开发人员使用 makemenuconfig
命令,该命令将启动用于配置内核映像的文本控制台工具。
在完成配置之后,可以开始编译过程。 我想给出的一条建议是,如果主机提供这种可能性,请尽可能多地使用线程,因为这将有助于构建过程。 构建过程开始命令的一个示例是make –j 8
。
在构建过程结束时,将提供一个vmlinux
映像,并且在 ARM 体系结构的特定于体系结构的文件中还会提供一些与体系结构相关的映像。 这一结果在arch/arm/boot/*Image
内部可用。 此外,Atmel SAMA5D3-XPlaed 主板将提供可在arch/arm/boot/dts/*.dtb
中使用的特定设备树文件。 如果vmlinux
映像文件是包含调试信息的 ELF 文件,并且该文件的调试信息只能用于调试目的,则arch/arm/boot/*Image
文件就是用于此目的的解决方案。
在为任何其他应用进行开发时,安装是下一步。 同样的事情也发生在 Linux 内核上,但是在嵌入式环境中,这一步似乎没有必要。 对于 Yocto 的爱好者来说,这一步也是可用的。 然而,在这一步中,对内核源代码进行了适当的配置,并且为部署步骤执行存储的依赖项将使用头部。
交叉编译一章中提到的内核模块稍后需要用于编译器构建。 内核模块的安装可以使用 makemodules_install
命令完成,这提供了在/lib/modules/<linux-kernel-version>
目录中安装包含所有模块依赖项、符号和别名的源代码的可能性。
在嵌入式开发中,编译过程意味着交叉编译,与本机编译过程最明显的区别在于它在命名中带有可用的目标体系结构的前缀。 前缀设置可以使用定义目标板架构名称的ARCH
变量和定义交叉编译工具链前缀的CROSS_COMPILE
变量来完成。 它们都是在顶级Makefile
中定义的。
最佳的选项是将这些变量设置为环境变量,以确保不为主机运行 make 进程。 虽然它只在目前的终端上工作,但在没有自动化工具可用于这些任务的情况下,例如 Yocto 项目,它将是最好的解决方案。 但是,如果您计划在主机上使用多个工具链,则不建议更新.bashrc
外壳变量。
正如我前面提到的,Linux 内核有很多内核模块和驱动程序,这些模块和驱动程序已经在 Linux 内核的源代码中实现并可用。 其中有许多是在 Linux 内核源代码之外也可以获得的。 将它们放在外部不仅通过在引导时不对它们进行初始化来缩短引导时间,而且还可以根据用户的请求和需要进行初始化。 唯一的区别是加载和卸载模块需要 root 访问权限。
加载 Linux 内核模块并与之交互需要提供日志记录信息。 任何内核模块依赖项都会发生同样的情况。 日志记录信息可通过dmesg
命令获得,日志记录级别允许使用loglevel
参数手动配置,也可以使用 Quest 参数禁用。 此外,对于内核依赖关系,可以在/lib/modules/<kernel-version>/modules.dep
文件中找到有关它们的信息。
对于模块交互,可以使用用于多个操作的多个实用程序,例如modinfo
,它用于收集有关模块的信息;当给定内核模块的填充路径时,insmod
可以加载模块。 模块也有类似的实用程序可用。 其中之一称为modprobe
,而modprobe
中的区别是不需要完整路径的,因为它负责在加载之前加载所选内核对象的依赖模块。 modprobe
提供的另一个功能是–r
选项。 正是 Remove 功能提供了对删除模块及其所有依赖项的支持。 另一种替代方法是rmmod
实用程序,它可以删除不再使用的模块。 最后一个可用的实用程序是lsmod
,它列出了加载的模块。
可以编写的最简单的内核模块示例如下所示:
#define MODULE
#define LINUX
#define __KERNEL__
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int hello_world_init(void)
{
printk(KERN_ALERT "Hello world!\n");
return 0;
}
static void hello_world_exit(void)
{
printk(KERN_ALERT "Goodbye!\n");
}
module_init(hello_world_init);
module_exit(hello_world_exit);
MODULE_LICENSE("GPL");
这是一个简单的hello world kernel
模块。 从前面的示例中可以收集到的有用信息是,每个内核模块都需要一个在前面的示例中定义为hello_world_init()
的启动函数。 它在插入模块时调用,在删除模块时调用名为hello_world_exit()
的清理函数。
从 Linux 内核版本 2.2 开始,就有可能以这种方式使用_init and __exit
宏:
static int __init hello_world_init (void)
static void __exit hello_world_exit (void)
前面的宏被删除,第一个宏在初始化之后,,第二个宏在模块内置在 Linux 内核源代码中时被删除。
有关 Linux 内核模块的更多信息可以在 Linux内核模块编程指南中找到,该指南可从http://www.tldp.org/LDP/lkmpg/2.6/html/index.html获得。
如前所述,内核模块不仅在 Linux 内核内部可用,而且在 Linux 内核树之外也可用。 对于内置内核模块,编译过程类似于其他可用的内核模块,开发人员可以从其中一个模块中启发其工作。 在 Linux 内核驱动程序和构建过程之外可用的内核模块需要访问 Linux 内核的源代码或内核头。
对于在 Linux 内核源代码之外可用的内核模块,可以使用Makefile
示例,如下所示:
KDIR := <path/to/linux/kernel/sources>
PWD := $(shell pwd)
obj-m := hello_world.o
all:
$(MAKE) ARCH=arm CROSS_COMPILE=<arm-cross-compiler-prefix> -C
$(KDIR) M=$(PWD)
对于在 Linux 内核中实现的模块,模块的配置需要在具有正确配置的相应Kconfig
文件中可用。 此外,需要更新Kconfig
文件附近的Makefile
,以便让Makefile
系统知道模块的配置何时更新,何时需要构建源。 在这里,我们将看到内核设备驱动程序的此类示例。
Kconfig
文件的示例如下:
config HELLO_WORLD_TEST
tristate "Hello world module test"
help
To compile this driver as a module chose the M option.
otherwise chose Y option.
下面是Makefile
的一个示例:
obj-$(CONFIG_ HELLO_WORLD_TEST) += hello_world.c
在这两个示例中,源代码文件都是hello_world.c
,如果结果内核模块不是内置的,则称为hello_world.ko
。
驱动程序通常用作与公开许多硬件功能的框架的接口,或者与用于检测硬件并与硬件进行通信的总线接口。 最好的示例如下所示:
由于有多种使用设备驱动程序的场景,并且有三种设备模式结构可用:
struct bus_type
:此表示总线类型,如 I2C、SPI、USB、PCI、MMC 等struct device_driver
:此表示用于处理总线上特定设备的驱动程序struct device
:此用于表示连接到总线的设备
继承机制用于从更通用的结构(如每个总线子系统的struct device_driver
和struct device
)创建专用的结构。 总线驱动程序是负责表示每种类型的总线并将相应的设备驱动程序与检测到的设备匹配的驱动程序,检测通过适配器驱动程序来完成。 对于不可发现的设备,在设备树或 Linux 内核的源代码中进行描述。 它们由支持平台驱动程序的平台总线处理,并反过来处理平台设备。
必须调试 Linux 内核并不是最容易的任务,但需要完成这项任务以确保开发过程向前推进。 当然,理解 Linux 内核是前提条件之一。 一些可用的错误很难解决,可能会在 Linux 内核中存在很长一段时间。
对于大多数琐碎的问题,应该采取以下步骤。 首先,正确识别错误;它不仅在定义问题时很有用,而且还有助于重现问题。 第二步是找到问题的根源。 这里,我指的是首次报告该错误的第一个内核版本。 对 Linux 内核的 bug 或源代码有很好的了解总是很有用的,因此在开始处理代码之前,请确保您理解了代码。
Linux 内核中的错误具有广泛的传播性。 它们从变量未正确存储到竞态条件或硬件管理问题各不相同,表现形式多种多样,并有一系列事件。 然而,调试它们并不像听起来那么困难。 除了一些特定的问题(如竞争条件和时间限制)外,调试与任何大型用户空间应用的调试非常相似。
调试内核的第一个、最简单、最方便的方法是使用printk()
函数。 它与printf()
C 库函数非常相似,虽然很旧,有些人不推荐它,但它确实起到了作用。 新的优选方法涉及使用pr_*()
函数,例如pr_emerg()
、pr_alert()
、pr_crit()
、pr_debug()
等。 另一种方法涉及使用dev_*()
函数,如dev_emerg()
、dev_alert()
、dev_crit()
、dev_dbg()
等。 它们对应于每个日志记录级别,并且还具有额外的函数,这些函数是为调试目的而定义的,并在启用CONFIG_DEBUG
时编译。
有关pr_*()
和dev_*()
系列函数的更多信息可以在Documentation/dynamic-debug-howto.txt
处的 Linux 内核源代码中找到。 您还可以在Documentation/kernel-parameters.txt
上找到有关loglevel
的更多信息。
当内核Oops崩溃时,它发出内核出错的信号。 由于无法修复或终止自身,它提供了对一系列信息的访问,例如有用的错误消息、注册内容和回溯信息。
Magic SysRq
键是调试中使用的另一种方法。 它由CONFIG_MAGIC_SYSRQ config
启用,可用于调试和恢复内核信息,而与其活动无关。 它提供了一系列命令行选项,可用于各种操作,从更改 NICE 级别到重新启动系统。 此外,还可以通过更改/proc/sys/kernel/sysrq
文件中的值来打开或关闭它。 有关系统请求密钥的更多信息,请参见Documentation/sysrq.txt
。
尽管 Linus Torvalds 和 Linux 社区认为内核调试器的存在不会对项目有太大好处,但对代码有更好的理解是任何项目的最佳方法。 仍然有一些调试器解决方案可供使用。 GNU 调试器(gdb
)是第一个调试器,它可以像任何其他进程一样使用。 另一个是kgdb
,它是gdb
上的一个补丁,允许调试串行连接。
如果前面的方法都不能帮助解决问题,并且您已经尝试了所有方法,但似乎无法找到解决方案,那么您可以联系开放源码社区寻求帮助。 那里总会有开发者向你伸出援手。
要获得更多关于 Linux 内核的信息,可以参考几本书。 我将在这里介绍几个他们的名字:Christopher Hallinan 的Embedded Linux Primer,Robert Love 的Linux Kernel Development,Greg Kroah-Hartman 的Linux Kernel in A Nutshell,以及 Daniel P.Boove 和 Marco Cesati 的Undering the Linux Kernel。
接下来来看 Yocto 项目,我们提供了内部可用的每个内核版本的菜谱、每个受支持主板的 BSP 支持以及构建在 Linux 内核源代码树之外的内核模块的菜谱。
Atmel SAMA5D3-XPlaed 主板使用linux-yocto-custom
内核。 这是在conf/machine/sama5d3-xplained.conf
机器配置文件中使用PREFERRED_PROVIDER_virtual/kernel
变量定义的。 没有提到PREFERRED_VERSION
,因此首选最新版本;在本例中,我们谈论的是linux-yocto-custom_3.10.bb
配方。
linux-yocto-custom_3.10.bb
配方获取 Linux Torvalds 的git
存储库中可用的内核源代码。 在do_fetch
任务完成后快速查看源代码之后,可以观察到 Atmel 存储库实际上是被获取的。 答案可以在linux-yocto-custom_3.10.bbappend
文件中找到,该文件提供了另一个SR_URI
位置。 您可以从这里收集到的其他有用信息是 bbappend 文件中提供的信息,其中很好地说明了 SAMA5D3 XPlaed 机器是一台COMPATIBLE_MACHINE
:
KBRANCH = "linux-3.10-at91"
SRCREV = "35158dd80a94df2b71484b9ffa6e642378209156"
PV = "${LINUX_VERSION}+${SRCPV}"
PR = "r5"
FILESEXTRAPATHS_prepend := "${THISDIR}/files/${MACHINE}:"
SRC_URI = "git://github.com/linux4sam/linux-at91.git;protocol=git;branch=${KBRANCH};nocheckout=1"
SRC_URI += "file://defconfig"
SRCREV_sama5d4-xplained = "46f4253693b0ee8d25214e7ca0dde52e788ffe95"
do_deploy_append() {
if [ ${UBOOT_FIT_IMAGE} = "xyes" ]; then
DTB_PATH="${B}/arch/${ARCH}/boot/dts/"
if [ ! -e "${DTB_PATH}" ]; then
DTB_PATH="${B}/arch/${ARCH}/boot/"
fi
cp ${S}/arch/${ARCH}/boot/dts/${MACHINE}*.its ${DTB_PATH}
cd ${DTB_PATH}
mkimage -f ${MACHINE}.its ${MACHINE}.itb
install -m 0644 ${MACHINE}.itb ${DEPLOYDIR}/${MACHINE}.itb
cd -
fi
}
COMPATIBLE_MACHINE = "(sama5d4ek|sama5d4-xplained|sama5d3xek|sama5d3-xplained|at91sam9x5ek|at91sam9rlek|at91sam9m10g45ek)"
配方首先定义与存储库相关的信息。 它是通过变量定义的,例如SRC_URI
和SRCREV
。 它还通过KBRANCH
变量指示存储库的分支,以及需要将defconfig
放入源代码以定义.config
文件的位置。 如配方中所示,内核配方的do_deploy
任务进行了更新,将设备驱动程序添加到内核映像和其他二进制文件旁边的tmp/deploy/img/sama5d3-xplained
目录。
内核配方继承了kernel.bbclass
和kernel-yocto.bbclass
文件,这两个文件定义了它的大部分任务操作。 由于它还会生成设备树,因此需要访问linux-dtb.inc
,后者在meta/recipes-kernel/linux
目录中可用。 linux-yocto-custom_3.10.bb
配方中提供的信息非常通用,并被bbappend
文件覆盖,如下所示:
SRC_URI = "git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git;protocol=git;nocheckout=1"
LINUX_VERSION ?= "3.10"
LINUX_VERSION_EXTENSION ?= "-custom"
inherit kernel
require recipes-kernel/linux/linux-yocto.inc
# Override SRCREV to point to a different commit in a bbappend file to
# build a different release of the Linux kernel.
# tag: v3.10 8bb495e3f02401ee6f76d1b1d77f3ac9f079e376"
SRCREV = "8bb495e3f02401ee6f76d1b1d77f3ac9f079e376"
PR = "r1"
PV = "${LINUX_VERSION}+git${SRCPV}"
# Override COMPATIBLE_MACHINE to include your machine in a bbappend
# file. Leaving it empty here ensures an early explicit build failure.
COMPATIBLE_MACHINE = "(^$)"
# module_autoload is used by the kernel packaging bbclass
module_autoload_atmel_usba_udc = "atmel_usba_udc"
module_autoload_g_serial = "g_serial"
在通过运行bitbake virtual/kernel
命令构建内核之后,内核映像将在tmp/deploy/img/sama5d3-xplained
目录中的zImage-sama5d3-xplained.bin
名称下可用,该名称是指向全名文件的符号链接,并且具有更大的名称标识符。 内核映像是从执行 Linux 内核任务的位置部署到这里的。 发现那个地方最简单的方法是运行bitbake –c devshell virtual/kernel
。 用户可以使用开发外壳与 Linux 内核源代码直接交互并访问任务脚本。 这种方法是首选的,因为开发人员可以访问与bitbake
相同的环境。
另一方面,如果内核模块没有内置在 Linux 内核源代码树中,那么它的行为会有所不同。 对于构建在源代码树外部的模块,需要编写一个新的配方,也就是继承另一个bitbake
类(这次称为module.bbclass
的配方)。 在recipes-kernel/hello-mod
目录中的meta-skeleton
层中提供了一个外部 Linux 内核模块的示例:
SUMMARY = "Example of how to build an external Linux kernel module"
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://COPYING;md5=12f884d2ae1ff87c09e5b7ccc2c4ca7e"
inherit module
PR = "r0"
PV = "0.1"
SRC_URI = "file://Makefile \
file://hello.c \
file://COPYING \
"
S = "${WORKDIR}"
# The inherit of module.bbclass will automatically name module packages with
# "kernel-module-" prefix as required by the oe-core build environment.
正如 Linux 内核外部模块的示例中所提到的,每个外部或内部内核模块的最后两行都打包有kernel-module-
前缀,以确保当IMAGE_INSTALL
变量可用时,值 kernel-module 会被添加到/lib/modules/<kernel-version>
目录内所有可用的内核模块中。 内核模块配方与任何可用配方非常相似,主要区别在于继承模块的形式,如行 Inherit MODULE 中所示。
在 Yocto 项目中,有多个命令可用于与内核和内核模块食谱交互。 当然,最简单的命令是bitbake``<recipe-name>
,但是对于 Linux 内核,有许多命令可以简化交互。 最常用的是bitbake -c menuconfig virtual/kernel
操作,它提供了对内核配置菜单的访问。
除了开发过程中最常用的已知任务(如configure
、compile
和devshell
)外,还有其他任务(如diffconfig
),它使用 Linux 内核scripts
目录中的diffconfig
脚本。 Yocto 项目的实现与 Linux 内核的可用脚本之间的区别在于前者增加了内核config
创建阶段。 作为自动化过程的一部分,这些config
片段用于将内核配置添加到.config
文件中。
在本章中,您了解了 Linux 内核的一般知识,了解了它的特性以及与它交互的方法。 还提供了有关调试和移植功能的信息。 所有这一切都是为了确保你在与整个生态系统互动之前获得关于整个生态系统的足够信息。 我的观点是,如果你先了解整体情况,就会更容易专注于更具体的事情。 这也是 Yocto 项目参考一直保留到最后的原因之一。 向您介绍了 Linux 内核配方和 Linux 内核外部模块是如何在稍后由给定机器定义和使用的。 关于 Linux 内核的更多信息也将在下一章中提供,它将收集所有以前提供的信息,并向您展示开发人员如何与 Linux 操作系统映像交互。
除了这些信息,在下一章中,还将解释根文件系统的组织以及它背后的原则、内容和设备驱动程序。 Busybox 是将讨论的另一个有趣的主题,也是对可用的文件系统的各种支持。 由于它往往会变得更大,因此还将介绍有关最小文件系统应该是什么样子的信息。 话虽如此,我们将进入下一章。