第3章 Linux内核以及内核编程(宋宝华Linux设备驱动开发详解)

第3章 Linux内核以及内核编程

  以下是读了宋宝华Linux设备驱动开发详解所做的一些学习笔记,在这里只是摘抄了一部分,也是碰到了不少陌生的东西

3.2 Linux 2.6后的内核特点

​ 引入CFS(Completely Fair Scheduler,完全公平)算法

1、新的调度算法

  • Linux内核中的完全公平调度算法(Completely Fair Scheduler,简称CFS)是一种用于进程调度的算法,它的目标是为所有运行的进程分配公平的CPU时间,确保每个进程都能获得合理的时间片来运行。(以下是AI对于CFS的介绍)

    1. CFS的设计目标

    CFS的核心目标是实现“完全公平”——即让所有可运行的进程都能获得大致相等的CPU时间。它通过计算每个进程应获得的CPU时间份额来实现这一点,而不是简单地按照时间片轮转的方式调度进程。

    2. 运行队列和虚拟运行时间

    CFS使用了一个红黑树(一种自平衡二叉查找树)来管理所有可运行的进程。每个进程都有一个虚拟运行时间(vruntime),它表示该进程已经运行的时间。CFS会优先调度vruntime最小的进程,即那些运行时间最少的进程。

    • 虚拟运行时间的计算:CFS会根据进程的优先级和权重来调整vruntime增加速度。优先级高的进程vruntime增加得更慢,因此会更频繁地被调度。
    • 红黑树的作用:红黑树用于快速查找vruntime最小的进程。每次调度时,CFS都会从红黑树的最左节点(vruntime最小的节点)选择进程运行。

    3. 时间片的动态调整

    CFS摒弃了传统的时间片轮转调度中固定时间片的概念。它通过动态计算每个进程的vruntime来决定调度顺序。进程运行时,其vruntime会不断增加,当一个进程的vruntime变得较大时,它会被暂时放到等待队列中,等待其他进程运行后再重新调度。

    4. 进程优先级的影响

    CFS支持实时进程和普通进程的调度。对于普通进程,CFS会根据进程的nice值(优先级)来调整其vruntime的增加速度。nice值越小,优先级越高,vruntime增加得越慢,从而获得更多的CPU时间。

    对于实时进程(使用SCHED_FIFOSCHED_RR调度策略),CFS会优先调度它们,因为实时进程通常对响应时间有严格要求。

    5. CFS的优点

    • 公平性:CFS能够确保所有进程都能获得公平的CPU时间,避免了某些进程因时间片过短或过长而无法正常运行的问题。
    • 动态适应性:CFS可以根据进程的优先级和运行时间动态调整调度策略,适应不同的系统负载情况。
    • 高效性:使用红黑树管理进程队列,能够快速找到需要调度的进程,提高了调度效率。

    6. CFS的局限性

    • 复杂性:CFS的实现相对复杂,涉及到红黑树的操作和vruntime的动态计算,这可能会增加调度的开销。
    • 实时性:虽然CFS支持实时进程的调度,但在高负载情况下,实时进程的响应时间可能会受到影响。

    7. 应用场景

    CFS广泛应用于多任务操作系统中,特别适合需要公平调度的场景,如多用户环境、服务器系统等。它能够确保系统资源被合理分配,提高系统的整体性能和响应能力。

    总之,CFS是一种高效的调度算法,通过动态计算进程的虚拟运行时间和使用红黑树管理进程队列,实现了对所有进程的公平调度。

2、内核抢占

​ 书本主要介绍了内核任务可以被抢占,提高了系统的实时性。但也存在一些无法抢占的,如中断上下文、软中断上下文和自选锁定区间。

3、改进的线程模型

​ 引入NPTL(Native POSIX Thread Library, 本地POSIX线程库)模型,操作速度得以极大提高

6、总线、设备和驱动模型

​ 总线是三者联系起来的基础,通过一种总线类型,将设备和驱动联系起来。总线类型中的match()函数用来匹配设备和驱动,当匹配操作完成之后就会执行驱动程序中的probe()函数。

3.3 Linux内核的组成

3.3.1 Linux内核源代码的目录结构

Linux内核源代码包含如下目录。

  • arch:包含和硬件体系结构相关的代码,每种平台占一个相应的目录,如1386、arm、arm64、powerpc、mips等。Linux内核目前已经支持30种左右的体系结构。在arch目录下存放的是各个平台以及各个平台的芯片对Linux内核进程调度、内存管理中断等的支持,以及每个具体的SoC 和电路板的板级支持代码。
  • block:块设备驱动程序I/0调度。
  • crypto:常用加密和散列算法(如AESSHA等),还有一些压缩和CRC 校验算法。
  • documentation:内核各部分的通用解释和注释。
  • drivers:设备驱动程序,每个不同的驱动占用一个子目录,如char、block、net、mtd、i2c等。
  • fs:所支持的各种文件系统,如EXT、FAT、NTFS、JFFS2等。
  • include:头文件,与系统相关的头文件放置在include/linux子目录下。
  • init:内核初始化代码。著名的start kernel()就位于 init/main.c文件中。
  • ipc:进程间通信的代码。
  • kernel:内核最核心的部分,包括进程调度、定时器等,而和平台相关的一部分代码放在arch/*/kernel目录下。
  • lib:库文件代码。
  • mm:内存管理代码,和平台相关的一部分代码放在arch/*/mm 目录下。
  • net:网络相关代码,实现各种常见的网络协议。
  • scripts:用于配置内核的脚本文件。
  • security:主要是一个 SELinux的模块。
  • sound:ALSA、OSS 音频设备的驱动核心代码和常用设备驱动。
  • USr:实现用于打包和压缩的cpio等。
  • include:内核API级别头文件。

3.3.2 Linux内核的组成部分

​ Linux内核主要由进程调度(SCHED)、内存管理(MM)、虚拟文件系统(VFS)、网络接口(NET)和进程间通信(IPC)5个子系统组成。

在这里插入图片描述

1、进程调度

​ 在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,并使本进程进入睡眠状态,直到请求的资源被释放,才会被唤醒而进入就绪状态。

​ 在Linux内核中,使用task_struct结构体来描述进程,该结构体中包含描述该进程内存资源、文件系统、tty资源、信号处理等指针。Linux线程采用轻量级进程模型来实现,在用户空间通过pthread_create() API创建线程的时候,本质上内核只是创建了一个新的task_struct,并将新task_struct的所有资源指针都指向创建它的那个task_struct的资源指针。(进程是操作系统资源分配资源的最小单位,线程共享进程的资源)

2、内存管理

​ 内存管理的主要作用是控制多个进程安全地共享主内存区域。

3、虚拟文件系统

​ Linux虚拟文件系统隐藏了各种硬件的具体细节,为所有设备提供了统一的接口。比如为上层的应用程序提供了统一的vfs_read()、vfs_write()等接口,并调用具体底层文件系统或者设备驱动中实现的file_operarion结构体的成员函数

​ 以下是Linux虚拟文件系统(VFS)与底层文件系统的结构解析(我只是浅浅了解一下,因为现在只是概述,一下子深入容易看不下去)


  • 🌐 工作流程示例(读文件)

  1. 用户调用read() → 触发系统调用进入内核。
  2. VFS层
    • 根据文件路径查找dentry → 获取inode
    • 调用file_operations.read指向的具体文件系统函数(如ext4_read())。
  3. 具体文件系统
    • 根据inode定位数据块磁盘地址。
    • 通过块设备驱动读取磁盘数据。
  4. 数据返回用户
    经VFS缓冲后复制到用户空间。

  • 💎 总结

Linux通过VFS抽象层统一了文件访问接口,使上层应用无需感知底层差异;具体文件系统则负责实现磁盘数据管理(如Ext4的inode表、XFS的B+树结构)。这种分层设计实现了对多种文件系统(Ext4/NFS/proc等)的无缝支持。

4、网络接口

​ 网络接口提供了对各种网络标准的存取和各种网络硬件的支持。

5、进程间通信

​ 进程间通信支持进程之间的通信,Linux支持进程间的多种通信机制,包含信号量、共享内存、消息队列、管道、UNIX域套接字等。

Linux内核5个组成部分之间的依赖关系如下。

  • 进程调度与内存管理之间的关系:这两个子系统互相依赖。在多程序环境下,程序要

    运行,则必须为之创建进程,而创建进程的第一件事情,就是将程序和数据装入内存。

  • 进程间通信与内存管理的关系:进程间通信子系统要依内存管理支持共享内存通信

    机制,这种机制允许两个进程除了拥有自己的私有空间之外,还可以存取共同的内存

    区域。

  • 虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统

    (NFS),也利用内存管理支 RAMDISK 设备。

  • 内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,交换进

    程定期由调度程序调度,这也是内存管理依赖于进程调度的原因。当一个进程存取的

  • 内存映射被换出时,内存管理向虚拟文件系统发出请求,同时,挂起当前正在运行的

    进程。

3.3.3 Linux内核空间与用户空间

​ 现代CPU内部往往实现了 不同操作系统模式(级别),不同模式有不同功能,高层程序往往不能访问低级功能,而必须以某种方式切换到低级模式。例如ARM处理器分成7种工作模式。

3.4 Linux内核的编译及加载

3.4.1 Linux内核的编译

​ 可用make config(最传统方式),make menuconfig(最值得推荐),make xconfig(依赖qt),make gconfig(要求GTK+被安装)配置内核。

​ 内核配置包含的条目相当多,arch/arm/configs/xxx_deconfig文件包含了许多电路板的默认配置。只需运行make ARCH=arm xxx_deconfig就可以为xxx开发板配置内核。如下图所示

​ 编译内核和模块的方法是make ARCH=arm zImage,make ARCH=arm module。在这边如果ARCH = arm已经作为环境变量导出,则可省略。
在这里插入图片描述

​ Linux内核的配置系统由以下三部分组成。

  • Makefile:分布在Linux内核源码中,定义Linux内核的编译规则

  • 配置文件(Kconfig):给用户提供配置选择的功能

  • 配置工具:包括配置命令解释器(对配置脚本中使用配置命令进行解释)和配置用户界面(提供字符界面和图形界面)。这些配置工具使用的都是脚本语言,如用Tcl/TK、Perl等

    使用make config、make menuconfig等命令后,会生成一个.config配置文件,记录哪些部分被编译入内核、哪些部分被编译为模块。

​ 运行 make menuconfig 等时,配置工具首先分析与体系结构对应的 /arch/xxx/Kconfig 文件( xxx 即为传入的 ARCH 参数), /arch/xxx/Kconfig 文件中除本身包含一些与体系结构相关的配置项和配置菜单以外,还通过 source 语句引入了一系列 Kconfig 文件,而这些 Kconfig又可能再次通过 source 引入下一层的 Kconfig,配置工具依据 Kconfig 包含的菜单和条目即可描绘出一个如图 3.9 (进入menuconfig的界面)所示的分层结构

3.4.2 Kconfig和Makefile

​ 在 Linux 内核中增加程序需要完成以下 3 项工作。(估计正点原子就是这样添加内核文件的)

● 将编写的源代码复制到 Linux 内核源代码的相应目录中。

● 在目录的 Kconfig 文件中增加关于新源代码对应项目的编译配置选项。

● 在目录的 Makefile 文件中增加对新源代码的编译条目。

1、实例引导: TTY_PRINTK 字符设备

​ 首先,在 drivers/char 目录中包含了 TTY_PRINTK 设备驱动的源代码 drivers/char/ttyprintk.c。而在该目录的 Kconfig 文件中包含关于 TTY_PRINTK 的配置项:

​ conf ig TTY_PRINTK

​ tristate “TTY driver to output user messages via printk”

​ depends on EXPERT && TTY

​ default n

​ —help—

​ If you say Y here, the support for writing user messages (i.e. console messages) via printk is available.

​ The feature is useful to inline user messages with kernel messages.

​ In order to use this feature, you should output user messages to /dev/ttyprintk or redirect console to this TTY.

​ If unsure, say N.

​ 上述 Kconfig 文件的这段脚本意味着只有在 EXPERT 和 TTY 被配置的情况下,才会出现 TTY_PRINTK 配置项,这个配置项为三态(可编译入内核,可不编译,也可编译为内核模块,选项分别为“ Y”、“ N”和“ M”),菜单上显示的字符串为“ TTY driver to output user messages via printk”,“ help”后面的内容为帮助信息。图 3.10 显示了 TTY_PRINTK 菜单以及 help 在运行 make menuconfig 时的情况。

在这里插入图片描述

​ 在目录的 Makefile 中关于 TTY_PRINTK 的编译项为:

​ obj-$(CONFIG_TTY_PRINTK) += ttyprintk.o

​ 上述脚本意味着如果 TTY_PRINTK 配置选项被选择为“ Y”或“ M”,即 obj-$(CONFIG_ TTY_PRINTK) 等同于 obj-y 或 obj-m,则编译 ttyprintk.c,选“ Y”时会直接将生成的目标代码连接到内核,选“ M”时则会生成模块 ttyprintk.ko ;如果 TTY_PRINTK 配置选项被选择为“ N”, 即 obj-$(CONFIG_TTY_PRINTK) 等同于 obj-n,则不编译 ttyprintk.c。

​ 一般而言,驱动开发者会在内核源代码的 drivers 目录内的相应子目录中增加新设备驱动的源代码或者在 arch/arm/mach-xxx 下新增加板级支持的代码,同时增加或修改 Kconfig 配置脚本和 Makefile 脚本,具体执行完全仿照上述过程即可。

2、Makefile

​ 这里主要对内核源代码各级子目录中的kbuild(内核的编译系统)Makefile进行简单介绍。这部分是内核模块或设备驱动开发者最常接触到的。Makefile 的语法包括如下几个方面。

( 1)目标定义

​ 目标定义就是用来定义哪些内容要作为模块编译,哪些要编译并链接进内核。例如:

obj-y += foo.o

​ 表示要由 foo.c 或者 foo.s 文件编译得到 foo.o 并链接进内核(无条件编译,所以不需要Kconfig 配置选项),而 obj-m 则表示该文件要作为模块编译。 obj-n 形式的目标不会被编译。更常见的做法是根据 make menuconfig 后生成的 config 文件的 CONFIG_ 变量来决定文件的编译方式,如:

obj-$(CONF iG_ISDN) += isdn.o 

obj-$(CONF iG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o  

( 2)多文件模块的定义。

​ 最简单的 Makefile 仅需一行代码就够了。如果一个模块由多个文件组成,会稍微复杂一些,这时候应采用模块名加 -y 或 -objs 后缀的形式来定义模块的组成文件,如下:

#
# Makef ile for the linux ext2-f ilesystem routines.
#
obj-$(CONF iG_EXT2_FS) += ext2.o
ext2-y := balloc.o dir.o f ile.o fsync.o ialloc.o inode.o \
ioctl.o namei.o super.o symlink.o
ext2-$(CONF iG_EXT2_FS_XATTR) += xattr.o xattr_user.o xattr_trusted.o
ext2-$(CONF iG_EXT2_FS_POSIX_ACL) += acl.o
ext2-$(CONF iG_EXT2_FS_SECURITY) += xattr_security.o
ext2-$(CONF iG_EXT2_FS_XIP) += xip.o

​ 模块的名字为 ext2,由 balloc.o、 dir.o、 file.o 等多个目标文件最终链接生成 ext2.o 直至ext2.ko 文件,并且是否包括 xattr.o、 acl.o 等则取决于内核配置文件的配置情况,例如,如果CONFIG_ EXT2_FS_POSIX_ACL 被选择,则编译 acl.c 得到 acl.o 并最终链接进 ext2。

3、Kconfig

4、应用实例:在内核中新增驱动代码目录和子目录(还没搞)

3.4.3 Linux内核引导

​ 引导 Linux 系统的过程包括很多阶段,这里将以引导 ARM Linux 为例来进行讲解(见图 3.11)。一般的 SoC 内嵌入了 bootrom,上电时 bootrom 运行。对于 CPU0 而言, bootrom 会去引导 bootloader,而其他 CPU 则判断自己是不是 CPU0,进入 WFI (WFI 是 “Wait For Interrupt”(等待中断)的缩写,它是 ARM 架构中的一种指令。)的状态等待 CPU0 来唤醒它。 CPU0 引导 bootloader, bootloader 引导Linux 内核,在内核启动阶段, CPU0 会发中断唤醒 CPU1,之后 CPU0 和 CPU1 都投入运行。 CPU0 导致用户空间的 init 程序被调用, init 程序再派生其他进程,派生出来的进程再派生其他进程。 CPU0 和 CPU1 共担这些负载,进行负载均衡。

​ bootrom 是 各 个 SoC 厂 家 根 据 自 身 情况 编 写 的, 目 前 的 SoC 一 般 都 具 有 从 SD、eMMC、 NAND、 USB 等 介 质 启 动 的 能 力,这证明这些 bootrom 内部的代码具备读 SD、NAND 等能力。
在这里插入图片描述

​ zImage的内核镜像实际上是没有压缩的解压算法和被压缩的内核组成,所以在bootloader跳入zImage以后,它自身的解压缩逻辑就把内核的镜像解压缩出来

3.5 Linux下 的C编程特点

3.5.1 Linux编程风格(懒得记了,知道有这东西就好,方便查找)

​ 不再像WIndow一样采用首字母大写来区分,采用下划线。

​ 对于结构体,if/for/while/switch语句,“ { ”不另起一行

3.5.2 GNU C与ANSI C

​ 1、零长度和变量长度数组

​ GNU C允许使用零长度数组,

  • 在 GNU C 中,零长度数组(Zero-Length Array) 是一种特殊语法扩展(如 char data[0]),其核心作用是为结构体末尾的可变长数据提供连续内存访问的入口,且自身不占用存储空间。你提到的 struct var_data 示例是网络协议、数据缓冲等场景的经典用法。下面分步解析其原理和意义:


    🔍 1. 零长度数组的本质

    • 语法形式char data[0] 声明一个长度为 0 的数组。

    • 内存占用:

      • 零长度数组本身不分配任何内存空间,因此 sizeof(struct var_data) 仅计算成员 int len 的大小(通常为 4 字节)。

      • 验证代码:

        struct var_data {
            int len;
            char data[0];
        };
        printf("结构体大小: %zu\n", sizeof(struct var_data));  // 输出结果为 4
        

    🧩 2. 零长度数组的工作原理

    零长度数组的作用是指向结构体之后的内存区域,实现动态数据与结构体的连续存储:

    1. 动态分配内存:

      分配时需手动计算总空间,包括结构体本身 + 实际数据长度:

      int data_size = 100; // 实际数据长度
      struct var_data *p = malloc(sizeof(struct var_data) + data_size);
      p->len = data_size;
      
    2. 访问数据:通过 p->data[index] 可直接访问 len 之后的内存区域(如下图):

      | len (4字节) | data[0] 指向的内存区域 (动态分配的100字节) |
      
      • data[0] 本质是编译器提供的一个偏移量标识符,其地址等于 &p->len + sizeof(int)

    ⚙️ 3. 零长度数组 vs. 指针方案的对比

    若改用指针方案:

    struct pointer_data {
        int len;
        char *data; // 额外占用指针空间(8字节)
    };
    
    • 内存占用对比:

      方案结构体大小数据存储方式访问效率
      零长度数组4 字节结构体后连续内存⭐⭐⭐⭐️(直接访问)
      指针方案12 字节额外分配独立内存块⭐⭐(需两次访存)
    • 优势:零长度数组减少内存碎片,提升访问效率(无需通过指针跳转)。


    ⚠️ 4. 注意事项

    1. 非标准特性:

      零长度数组是 GNU C 扩展,非标准 C 语法。若需可移植代码,建议改用 C99 柔性数组(char data[]):

      struct flex_data {
          int len;
          char data[]; // C99 标准语法
      };
      
    2. 内存越界风险
      需确保分配的空间足够(sizeof(struct) + 数据长度),否则 data[index] 会越界。


    💎 总结

    • char data[0] 的意义
      它是一个​​零开销的占位符​​,标记结构体末尾动态数据的起始地址,使 data[index] 能直接访问连续内存。
    • sizeof(struct var_data) = sizeof(int) 的原因
      编译器将零长度数组视为 ​​0 字节成员​​,不参与结构体大小计算。
    • 适用场景
      高效管理网络数据包、二进制流等​​变长结构​​(如 Linux 内核的 sk_buff 结构)。

3.6 工具链

​ 一个典型的 ARM Linux 工具链包含 arm-linux-gnueabihf-gcc( 后续工具省略前缀)、 strip、gcc、 objdump、 ld、 gprof、 nm、 readelf、 addr2line 等。

​ 用 strip 可以删除可执行文件中的符号表和调试信息等来实现缩减程序体积的目的。

​ gprof 在编译过程中在函数入口处插入计数器以收集每个函数的被调用情况和被调用次数,检查程序计数器并在分析时找出与程序计数器对应的函数来统计函数占用的时间。

​ objdump 是反汇编工具。

​ nm 则用于显示关于对象文件、可执行文件以及对象文件库里的符号信息。

​ 其中,前缀中的“ hf”显示该工具链是完全的硬浮点,由于目前主流的 ARM 芯片都自带 VFP 或者 NEON 等浮点处理单元(FPU),所以对硬浮点的需求就更加强烈。 Linux 的浮点处理可以采用完全软浮点,也可以采用与软浮点兼容,但是使用 FPU 硬件的 softfp,以及完全硬浮点。具体的 ABI( Application Binary Interface,应用程序二进制接口)通过 -mfloat-abi= 参数指定, 3 种情况下的参数分别是 -mfloat-abi=soft/ softfp/hard。

3.9 总结

​ 本章主要讲解了 Linux 内核和 Linux 内核编程的基础知识,为读者在进行 Linux 驱动开发打下软件基础。

​ 在 Linux 内核方面,主要介绍了 Linux 内核的发展史、组成、特点、源代码结构、内核编译方法及内核引导过程。

​ 由于 Linux 驱动编程本质属于内核编程,因此掌握内核编程的基础知识显得尤为重要。本章在这方面主要讲解了在内核中新增程序、目录和编写 Kconfig 和 Makefile 的方法,并分析了 Linux 下 C 编程习惯以及 Linux 所使用的 GNU C 针对标准 C 的扩展语法。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值