Linux驱动之内核编程

作者: konishi5202 | 来源:发表于2019-03-01 00:14 被阅读14次

    Linux操作系统是UNIX操作系统的一种克隆系统,诞生于1991年10月5日(第一次正式向外公布的时间)。Linux操作系统的诞生、发展和成长过程依赖者5个重要支柱:UNIX操作系统、Minix操作系统、GNU计划、Posix标准和Internet。

    一、Linux内核的组成

    1.1 Linux内核源码目录结构

    本书范例程序基于Linux 2.6.28.6内核源码,其目录结构如下:

    • arch:包含和硬件体系结构相关的代码,每种平台占一个相应的目录,如i386、arm、powerpc、mips等;
    • block:块设备驱动程序I/O调度;
    • crypto:常用加密和散列算法(如AES、SHA等),还有一些压缩和CRC校验算法;
    • Documentation:内核各部分的通用解释和注释;
    • drivers:设备驱动程序,每个不同的驱动占用一个子目录,如char、block、net、mtd、i2c等;
    • fs:支持的各种文件系统,如EXT、FAT、NTFS、JFFS2等;
    • include:头文件,与系统相关的头文件被放置在include/linux子目录下;
    • init:内核初始化代码;
    • ipc:进程间通信的代码;
    • kernel:内核的最核心部分,包括进程调度、定时器等,而和平台相关的一部分代码放在arch/*/kernel目录下;
    • lib:库文件代码;
    • mm:内存管理代码,和平台相关的一部分代码放在arch/*/mm目录下;
    • net:网络相关代码,实现了各种常见的网络协议;
    • scripts:用于配置内核的脚本文件;
    • security:主要是一个SELinux的模块;
    • sound:ALSA、OSS音频设备的驱动核心代码和常用设备驱动;
    • usr:实现了用于打包和压缩的cpio等;

    1.2 Linux内核的组成部分

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

    图1.1 Linux内核的组成部分与关系


    image

    1.2.1 进程调度

    进程调度控制系统中的多个进程对CPU的访问,使得多个进程能在CPU中“微观串行,宏观并行”地执行。进程调度处于系统的重心位置,内核中其他子系统都依赖它,因为每个子系统都需要挂起或恢复进程。

    图1.2 Linux进程状态转换


    image

    如图1.2所示,Linux的进程在几个状态间进行切换,在设备驱动编程中,当请求的资源不能得到满足时,驱动一般会调度其他进程执行,并使本进程进入睡眠状态,直到它请求的资源被释放,才会被唤醒而进入就绪态,睡眠分成可被打断的睡眠和不可被打断的睡眠,两者的区别在于可被打断的睡眠在收到信号的时候会醒。

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

    设备驱动中,如果需要几个并发执行的任务,可以启动内核线程,启动内核线程的函数为:

    pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags);
    

    1.2.2 内存管理

    内存管理的主要作用是控制多个进程安全地共享主内存区域。当CPU提供内存管理单元(MMU)时,Linux内存管理完成为每个进程进行虚拟内存到物理内存的转换。Linux 2.6引入了对无MMU CPU的支持。

    一般而言,Linux的每个进程享有4GB的内存空间,03GB属于用户空间,34GB属于内核空间,内核空间对常规内存、I/O设备内存以及高端内存存在不同的处理方式。

    1.2.3 虚拟文件系统

    如图1.3所示,Linux虚拟文件系统(VFS)隐藏了各种硬件的具体细节,为所有的设备提供了统一的接口。而且,它独立于各个具体的文件系统,是对各种文件系统的一个抽象,它使用超级块super block存放文件系统相关信息,使用索引节点inode存放文件的物理信息,使用目录项dentry存放文件的逻辑信息。

    图1.3 Linux文件系统


    image

    1.2.4 网络接口

    网络接口提供了对各种网络标准的存取和各种网络硬件的支持。如图1.4所示,在Linux中网络接口可分为网络协议和网络驱动程序,网络协议部分负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备通信,每一种可能的硬件设备都有相应的设备驱动程序。

    图1.4 Linux网络体系结构


    image

    1.2.5 进程通信

    进程通信支持提供进程之间的通信,Linux支持进程间的多种通信机制,包含信号量、共享内存、管道等。这些机制可协助多个进程、多资源的互斥访问、进程间的同步和消息传递。Linux内核的5个组成部分之间的依赖关系如下:

    • 进程调度与内存管理之间的关系:这两个子系统互相依赖。在多道程序环境下,程序要运行必须为之创建进程,而创建进程的第一件事情,就是将程序和数据装入内存;
    • 进程间通信与内存管理的关系:进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间,还可以存取共同的内存区域;
    • 虚拟文件系统与网络接口之间的关系:虚拟文件系统利用网络接口支持网络文件系统(NFS),利用内存管理支持RAMDISK设备;
    • 内存管理与虚拟文件系统之间的关系:内存管理利用虚拟文件系统支持交换,交换进程(swapd)定期由调度程序调度,这也是内存管理依赖于进程调度的唯一原因。当一个进程存取的内存映射被换出时,内存管理向文件系统发出请求,同时,挂起当前正在运行的进程。

    除了这些依赖关系外,内核中的所有子系统还要依赖于一些共同的资源。这些资源包括所有子系统都用到的例程,如分配和释放内存空间的函数、打印警告或错误信息的函数及系统提供的调试例程等。

    1.3 Linux内核空间与用户空间

    现代CPU内部往往实现了不同的操作模式(级别),不同的模式有不同的功能,高层程序往往不能访问低级功能,而必须以某种方式切换到低级模式。

    例如,ARM处理器分为7种工作模式:

    • 用户模式(usr):大多数的应用程序运行在用户模式下,当处理器运行在用户模式下时,某些被保护的系统资源是不能被访问的;
    • 快速中断模式(fiq):用于高速数据传输或通道处理;
    • 外部中断模式(irq):用于通用的中断处理;
    • 管理模式(svc):操作系统使用的保护模式;
    • 数据访问终止模式(abt):当数据或指令预取终止时进入该模式,可用于虚拟存储及存储保护;
    • 系统模式(sys):运行具有特权的操作系统任务;
    • 未定义指令中止模式(und):当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真。

    ARM Linux的系统调用实现原理是采用swi软中断从用户态usr模式转入内核态svc模式。

    又如,X86处理器包含4个不同的特权级,称为Ring0~Ring3。Ring0下,可以执行特权级指令,对任何I/O设备都有访问权等,而Ring3则被限制很多操作。 Linux系统充分利用CPU这一硬件特性,但它只使用了两级。在Linux系统中,内核可进行任何操作,而应用程序则被禁止对硬件的直接访问和对内存的未授权访问。例如,若使用X86处理器,则用户代码运行在特权级3,而系统内核代码则运行在特权级0。

    内核空间和用户空间这两个名词被用来区分程序执行的这两种不同状态,他们使用不同的地址空间。Linux只能通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。

    二、Linux内核的编译及引导

    2.1 Linux内核的编译

    Linux驱动工程师需要牢固地掌握Linux内核的编译方法以为嵌入式系统构建可运行的Linux操作系统映像。在编译Linux内核时,需要配置内核,可以使用下面的命令中的一个来配置:

    # make config  (基于文本的最为传统的配置界面,不推荐使用)
    # make menuconfig  (基于文本菜单的配置界面)
    # make xconfig  (要求QT被安装)
    # make gconfig  (要求GTK+被安装)
    

    在配置Linux 2.6内核所使用的make config、make menuconfig、make xconfig和make gconfig这4重方式中,最值得推荐的是make menuconfig,它不依赖于QT或GTK+,且非常直观。输入make menuconfig命令后进入配置界面如图3.5所示。

    内核配置包含的项目相当多,在arch/arm/configs/目录中包含了ARM芯片不同的demo板的默认配置。因此,在内核顶层目录下输入make board_defconfig就可以为demo配置内核,如输入:make smdk2410_defconfig就可以为SMDK2410配置默认内核。

    图 1.5 Linux内核编译配置


    image

    编译内核和模块的方法是:

    # make zImage
    # make modules
    

    执行完上述命令后,在源代码的根目录下会得到==未压缩的内核映像vmlinux和内核符号表文件System.map==,在arch/arm/boot/目录会得到压缩的内核映像zImage,在内核各对应目录得到选中的内核模块。

    Linux 2.6内核的配置系统由以下3个部分组成:

    • Makefile:分布在Linux内核源代码中的Makefile,定义Linux内核的编译规则;
    • 配置文件(Kconfig):给用户提供配置选择的功能;
    • 配置工具:包括配置命令解释器(对配置脚本中使用的配置命令进行解释)和配置用户界面(提供基于字符界面和图形界面)。这些配置工具都是使用脚本语言,如Tcl/TK、Perl等编写。

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

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

       1 #
       2 # For a description of the syntax of this configuration file,
       3 # see Documentation/kbuild/kconfig-language.txt.
       4 #
       5 
       6 mainmenu "Linux Kernel Configuration"
       7 
       8 config ARM
       9         bool
      10         default y
      11         select HAVE_AOUT
      12         select HAVE_IDE
      13         select RTC_LIB
      14         select SYS_SUPPORTS_APM_EMULATION
      15         select HAVE_OPROFILE
      16         select HAVE_ARCH_KGDB
      17         select HAVE_KPROBES if (!XIP_KERNEL)
      18         select HAVE_KRETPROBES if (HAVE_KPROBES)
      19         select HAVE_FUNCTION_TRACER if (!XIP_KERNEL)
      20         select HAVE_GENERIC_DMA_COHERENT
      21         help
      22           The ARM series is a line of low-power-consumption RISC chip designs
      23           licensed by ARM Ltd and targeted at embedded applications and
      24           handhelds such as the Compaq IPAQ.  ARM-based PCs are no longer
      25           manufactured, but legacy ARM-based PC hardware remains popular in
      26           Europe.  There is an ARM Linux project with a web page at
      27           <http://www.arm.linux.org.uk/>.
    ……
     198 config MMU
     199         bool "MMU-based Paged Memory Management Support"
     200         default y
     201         help
     202           Select if you want MMU-based virtualised addressing space
     203           support by paged memory management. If unsure, say 'Y'.
    ……
     614 config ARCH_S3C64XX
     615         bool "Samsung S3C64XX"
     616         select GENERIC_GPIO
     617         select HAVE_CLK
     618         select ARCH_HAS_CPUFREQ
     619         help
     620           Samsung S3C64XX series based systems
    ……
    

    2.2 Kconfig和Makefile

    在Linux内核中增加程序需要完成以下3项工作:

    • 将编写的源代码拷入Linux内核源代码的相应目录;
    • 在目录的Kconfig文件中增加关于新源代码对应的编译配置选项;
    • 在目录的Makefile文件中增加对新代码的编译条目。

    一般而言,驱动工程师只会在内核源代码的drivers目录的相应子目录中增加新设备驱动的源代码,并增加或修改Kconfig配置脚本和Makefile脚本即可。接下来就讲解一下Kconfig和Makefile的语法。

    1.Makefile

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

    (1)目标定义

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

    obj-y += foo.o
    

    表示要由foo.c或者foo.s文件编译得到foo.o并链接进内核,而obj-m则表示该文件要作为模块编译。除了y、m以外的obj-x形式的目标都不会被编译。

    而更常见的做法是根据.config文件的CONFIG_变量来决定文件的编译方式,如:

    obj-$(CONFIG_ISDN) += isdn.o
    obj-$(CONFIG_ISDN_BSDCOMP) += isdn_bsdcomp.o
    

    除了obj-形式的目标以外,还有lib-y library库,hostprogs-y主机程序等目标,但是基本都应用在特定的目录和场合下。

    (2)多文件模块的定义

    最简单的Makefile如前面的一句话的形式就够了,如果一个模块由多个文件组成,会稍微复杂一些,这时候应采用模块名加-y或-objs后缀的形式来定义模块的组成文件,如下面的例子:

    #
    # Makefile for the linux ext2-filesystem routines.
    #
    obj-$(CONFIG_EXT2_FS) += ext2.o
    ext2-y := balloc.o dir.o file.o fsync.o ialloc.o inode.o \
            ioctl.o namei.o super.o symlink.o
    ext2-$(CONFIG_EXT2_FS_XATTR) += xattr.o xattr_user.o xattr_trusted.o
    ext2-$(CONFIG_EXT2_FS_POSIX_ACL) += acl.o
    ext2-$(CONFIG_EXT2_FS_SECURITY) += xattr_security.o
    ext2-$(CONFIG_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)目录层次的迭代

    如下例:

    obj-$(CONFIG_EXT2_FS) += ext2/
    

    当CONFIG_EXT2_FS的值为y或m时,kbuild将会把ext2目录列入向下迭代的目标中。

    2.Kconfig

    内核配置脚本文件的语法也比较简单,主要包括以下几个方面:

    (1)菜单入口

    大多数的内核配置选项都对应Kconfig中的一个菜单入口:

     614 config ARCH_S3C64XX
     615         bool "Samsung S3C64XX"
     616         select GENERIC_GPIO
     617         select HAVE_CLK
     618         select ARCH_HAS_CPUFREQ
     619         help
     620           Samsung S3C64XX series based systems
    

    “config”关键字定义新的配置选项,之后的几行定义了该配置选项的属性。配置选项的属性包括类型、数据范围、输入提示、依赖关系、选择关系及帮助信息和默认值等。

    每个配置选项都必须指定类型,类型包括bool、tristate、string、hex和int,其中tristate和string是两种基本的类型,其他类型都基于这两种基本类型。类型定以后可以紧跟输入提示,下面的两段脚本是等价的:

    bool “Networking support”
    

    bool
    prompt “Networking support”
    

    输入提示的一般格式如下,其中可选的if用来表示该提示的依赖关系:

    prompt <prompt> [if <expr>]
    

    默认值的格式为:

    default <expr> [if <expr>]
    

    一个配置选项可以存在任意多个默认值,这种情况下,只有第一个被定义的值是可用的。如果用户不设置对应的选项,配置选项的值就是默认值。

    依赖关系的格式为:

    depends on(或者requires) <expr>
    

    如果定义了多重依赖关系,他们之间用“&&”间隔。依赖关系也可以应用到该菜单中所有的其他选项(同样接受if表达式),下面的两段脚本是等价的:

    bool “foo” if BAR
    default y if BAR
    

    depends on BAR
    bool “foo”
    default y
    

    选择关系(也称为反向依赖关系)的格式为:

    select <symbol> [if <expr>]
    

    A如果选择了B,则在A被选中的情况下,B自动被选中。

    kbuild Makefile中的expr(表达式)定义为:

    <expr> ::= <symbol>
                 <symbol> ‘=’ <symbol>
                 <symbol> ‘!=’ <symbol>
                 ‘(’ <expr> ‘)’
                 ‘!’ <expr>
                 <expr> ‘&&’ <expr>
                 <expr> ‘||’ <expr>
    

    也就是说expr是由symbol、两个symbol相等、两个symbol不等以及expr的赋值、非、与或运算构成。而symbol分为两类,一类是由菜单入口定义配置选项定义的非常数symbol,另一类是作为expr组成部分的常数symbol。

    数据范围的格式为:

    range <symbol> <symbol> [if <expr>]
    

    为int和hex类型的选项设置可以接受输入值范围,用户只能输入大于等于第一个symbol,小于等于第二个symbol的值。
    帮助信息的格式为:

    help(或---help---)
          开始
          ……
          结束
    

    帮助信息完全靠文本缩进识别结束。“---help---”和“help”在作用上没有区别,设计“---help---”的初衷在于将文本中的配置逻辑与给开发人员的提示分开。

    menuconfig关键字的作用与config类似,但它在config的基础上要求所有的子选项作为独立的行显示。

    (2)菜单结构

    菜单入口在菜单树结构中的位置可由两种方法决定,第一种方式为:

    menu “Network device support”
          depends on NET
    config NETDEVICES
       …
    endmenu
    

    所有处于“menu”和“endmenu”之间的菜单入口都会成为“Network device support”的子菜单。而且,所有子菜单选项都会继承父菜单的依赖关系,比如“Network device support”对“NET”的依赖会被加到了配置选项NETDEVICES的依赖列表中。

    注意menu后面跟的“Network device support”项目仅仅是1个菜单,没有对应真实的配置选项,也不具备3种不同的状态。这是它和config的区别。

    另一种方式是通过分析依赖关系生成菜单结构。如果菜单选项在一定程度上依赖于前面的选项,它就能成为该选项的子菜单。如果父选项为“N”,子选项不可见;如果父选项可见,子选项才能见。例如:

    config MODULES
          bool “Enable loadable module support”
    config MODVERSIONS
          bool “Set version information on all module symbols”
          depends on MODULES
       comment “module support disabled”
          depends on !MODULES
    

    MODVERSIONS直接依赖MODULES,只有MODULES不为“n”时,该选项才可见。

    除此之外,Kconfig中还可能使用“choices…endchoice”、“comment”、“if…endif”这样的语法结构,其中“choices…endchoice”的结构为:

      choice
      <choice options>
      <choice block>
      endchoice
    

    它定义一个选择群,其接受的选项(choice options)可以是前面描述的任何属性,例如S3C6410芯片设计的产品的VGA输出分辨率可以是1024768或者800600,在drivers/video/Samsung/Kconfig就定义了如下的choice:

      choice
      depends on FB_S3C_VGA
      prompt “Select VGA Resolution for S3C Framebuffer”
      default FB_S3C_VGA_1024_768
      config FB_S3C_VGA_1024_768
          bool “1024*768@60Hz”
          ---help---
          TBA
      config FB_S3C_VGA_640_480
          bool “640*480@60Hz”
          ---help---
          TBA
      endchoice
    

    Kconfig配置脚本和Makefile脚本编写更详细的信息,可分别查看内核温昂Documentation目录的kbuild子目录下的Kconfig-language.txt和Makefiles.txt文件。

    3.应用实例:在内核中新增驱动代码目录和子目录

    下面来看一个综合实例,假设我们要在内核源代码drivers目录下为ARM体系结构新增如下用于test driver的树形目录:

       | -- test
          | -- cpu
             | -- cpu.c
          | -- test.c
          | -- test_client.c
          | -- test_ioctl.c
          | -- test_proc.c
          | -- test_queue.c
    

    在内核中增加目录和子目录,我们需为相应的新增目录创建Makefile和Kconfig文件,而新增目录的父目录中的Kconfig和Makefile也许修改,以便新增的Kconfig和Makefile能被引用。

    在新增的test目录下,应该包含如下的Kconfig文件:

    # drivers/test/Kconfig
    # TEST driver configuration
    # 
    menu “TEST Driver”
    comment “ TEST Driver”
      
    config CONFIG_TEST
          bool “TEST support”
    config CONFIG_TEST_USER
          tristate “TEST user-space interface”
          depends on CONFIG_TEST
    endmenu
    

    由于test driver对于内核来说是新的功能,所以需首先创建一个菜单TEST Driver。然后,显示“TEST support”,等待用户选择;接下来判断用户是否选择了TEST Driver,如果是(CONFIG_TEST=y),则进一步显示子功能:用户接口与CPU功能支持;由于用户接口功能可以被编译成内核模块,所以这里的询问语句使用了tristate。

    为了使这个Kconfig能起作用,修改arch/arm/Kconfig文件,在对应位置增加:

    source “driver/test/Kconfig”
    

    脚本中的source意味着引用新的Kconfig文件。

    在新增的test目录下,应该包含如下Makefile文件:

    # driver/test/Makefile
    # Makefile for the TEST
    #
    obj-$(CONFIG_TEST) += test.o test_queue.o test_client.o
    obj-$(CONFIG_TEST_USER) += test_ioctl.o
    obj-$(CONFIG_PROC_FS) += test_proc.o
    obj-$(CONFIG_TEST_CPU) += cpu/
    

    该脚本根据配置变量的取值,构建obj-*列表,由于test目录中包含一个子目录cpu,当CONFIG_TEST_CPU=y时,需要将cpu目录加入列表。

    # drivers/test/cpu/Makefile
    # Makefile for the TEST CPU
    #
    obj-$(CONFIG_TEST_CPU) += cpu.o
    

    为了使得整个test目录能够被编译命令作用到,test目录父目录中的Makefile也需新增如下脚本才能够进入test目录:

    obj-$(CONFIG_TEST) += test/
    

    增加了Kconfig和Makefile之后的新的test树型目录为:

       | -- test
          | -- cpu
             | -- cpu.c
             | -- Makefile
          | -- test.c
          | -- test_client.c
          | -- test_ioctl.c
          | -- test_proc.c
          | -- test_queue.c
          | -- Makefile
          | -- Config
    

    2.3 Linux内核的引导

    引导Linux系统的过程包括很多阶段,这里将以引导X86 PC为例进行讲解。引导X86 PC上的Linux的过程和引导嵌入式系统上的Linux的过程基本类似。不过在X86 PC上有一个BIOS(基本输入/输出系统)转移到Bootloader的过程,而嵌入式系统往往复位后就直接运行Bootloader;在X86 PC上的Bootloader一般为GRUB,而在嵌入式系统中的Bootloader一般为U-Boot。

    图1.6所示为X86 PC上电/复位到运行Linux用户空间初始化进程的流程,在进入与Linux相关代码之间,会经历如下阶段。

    图1.6 X86 PC上的Linux引导流程


    image
    1. 当系统上电或复位时,CPU会将PC指针赋值为一个特定的地址0xFFFF0并执行该地址处的指令。在PC机中,该地址位于BIOS中,它保存在主板上的ROM或Flash中。
    2. BIOS运行时按照CMOS的设置定义的启动设备顺序来搜索处于活动状态并且可以引导的设备。若从硬盘启动,BIOS会将硬盘MBR(主引导记录)中的内容加载到RAM。MBR是一个512字节大小的扇区,位于磁盘上的第一个扇区中(0道0柱面1扇区)。当MBR被加载到RAM中之后,BIOS就会将控制权交给MBR。
    3. 主引导加载程序查找并加载次引导加载程序。它在分区表中查找活动分区,当找到一个活动分区时,扫描分区表中的其他分区,以确保他们都不是活动的。当这个过程验证完成之后,就将活动分区的引导记录从这个设备中读入RAM中并执行它。
    4. 次引导加载程序加载Linux内核和可选的初始RAM磁盘,将控制权交给Linux内核源代码。
    5. 运行被加载的内核,并启动用户空间应用程序。

    嵌入式系统中的Linux引导过程与之类似,但一般更加简洁。不论具体以怎样的方式实现,只要具备如下特性就可以称其为Bootloader:

    • 可以在系统上电或复位的时候以某种方式执行,这些方式包括被BIOS引导执行、直接在Nor Flash中执行、Nand Flash中的代码被MCU自动拷入内部或外部RAM执行等;
    • 能将U盘、磁盘、光盘、NOR/NAND Flash、ROM、SD卡等存储介质,甚或网口、串口中的操作系统加载到RAM并把控制权交给操作系统源代码执行。

    完成上述功能的Bootloader的实现方式非常多样化,甚至本身也可以是一个简化版的操作系统。著名的Linux Bootloader包括应用于PC的LILO和GRUB,应用于嵌入式系统的U-Boot、Redboot等。

    U-Boot的定位为“Universal Bootloader”,其功能比较强大,涵盖了包括PowerPC、ARM、MIPS和X86在内的绝大部分处理器架构,提供网卡、串口、Flash等外设驱动,提供必要的网络协议(BOOT、DHCP、TFTP等),能识别多种文件系统(cramfs、fat、jffs2和registerfs等),并附带了调试、脚本、引导等工具,应用十分广泛。

    Redboot是Redhat公司随eCos发布的Bootloader开源项目,处理包含U-Boot类似的强大功能外,它还包含GDB stub(插桩),因此能通过串口或网卡与GDB进行通信,调试GCC产生的任何程序(包括内核)。

    我们有必要对上述流程的第5个阶段进行更详细的分析,它完成启动内核并运行用户空间的init进程。

    当内核映像被加载到RAM之后,Bootloader的控制权被释放,内核阶段就开始了。内核映像并不是完全可直接执行的目标代码,而是一个压缩过的zImage(小内核)或bzImage(大内核,bzImage中的b是“big”的意思)。但是,并非zImage和bzImage映像中的一切都被压缩了,否则Bootloader把控制权交给这个内核映像它就“傻”了。实际上,映像中包含未被压缩的部分,这部分中包含解压缩程序,解压缩程序会解压映像中被压缩的部分。zImage和bzImage都是用gzip压缩的,他们不仅是一个压缩文件,而且在这两个文件的开头部分内嵌有gzip解压缩代码。

    如图1.7所示,当bzImage(用于i386映像)被调用时,它从/arch/i386/boot/head.S的start汇编例程开始执行。这个程序执行一些基本的硬件设置,并调用/arch/i386/boot/compressed/head.S中的startup_32例程。startup_32程序设置一些基本的运行环境(如堆栈)后,清除BSS段,调用/arch/i386/boot/compressed/misc.c中的decompress_kernel() C函数解压内核。内核被解压到内存中之后,会再调用/arch/i386/kernel/head.S文件中的startup_32例程,这个新的startup_32例程(称为清除程序或进程0)会初始化页表,并启动内存分页机制,接着为任何可选的浮点单元(FPU)检测CPU的类型,并将其存储起来供以后使用。这些都昨晚之后,/init/main.c中的start_kernel()函数被调用,进入与体系结构无关的Linux内核部分。

    图1.7 X86 PC上的Linux内核初始化
    [图片上传失败...(image-76a602-1551370457159)]

    start_kernel()会调用一系列初始化函数来设置终端,执行进一步的内存配置。之后,/arch/i386/kernel/process.c中的kernel_thread()被调用以启动第一个核心线程,该线程执行init()函数,而原执行序列会调用cpu_idle()等待调度。

    作为核心线程的init()函数完成外设及驱动程序的加载和初始化,挂载根文件系统。init()打开/dev/console设备,重定向stdin、stdout和stderr到控制台。之后,它搜索文件系统中的init程序(也可以由“init=”命令参数指定init程序),并使用execve()系统调用执行init程序。搜索init程序的顺序为:/sbin/init、/etc/init、/bin/init和/bin/sh。在嵌入式系统中,多数情况下,可以给内核传入一个简单的shell脚本来启动必须的嵌入式应用程序。

    至此,漫长的Linux内核应道和启动过程就此结束,而init()对应的这个由start_kernel()创建的第一个线程也进入用户模式。

    三、Linux下的C编程特点

    3.1 Linux编码风格

    Linux程序的命名习惯和Windows程序的命名习惯及著名的匈牙利命名法有很大的不同。在Windows程序中,习惯以如下方式命名宏、变量和函数:

    #define PI 2.141592653  /* 用大写字母代表宏 */
    int minValue, maxValue;  /* 变量:第一个单词全小写,其后的单词第一个字母大写 */
    void SendData(vodi);   /* 函数:所有单词第一个字母都大写定义 */
    

    这种命名方式在程序员中非常盛行,意思表达清晰且避免了匈牙利法的臃肿,单词之间通过首字母大写来区分。通过第1个单词的首字母是否大写可以区分名称属于变量还是属于函数,而看到整串的大写字母可以断定为宏。实际上,Windows的命名习惯并非仅限于Windows编程,大多数领域的程序开发都遵照此习惯。

    但是Linux不以这种习惯命名,对应于上面的一段程序,在Linux中会被命名为:

    #define PI 3.141592653
    int min_value, max_value;
    void send_data(void);
    

    上述命名方式中,下划线大行其道,不依照Windows所采用的首字母大写以区分单词的方式。Linux的命名习惯与Windows命名习惯各有千秋,但是既然本书和本书的读者立足于编写Linux程序,代码风格理应保持与Linux开发社区的一致性。

    Linux的代码缩进使用“TAB”(8个字符);Linux的代码括号“{”和“}”的使用原则如下:

    • 对于结构体、if/for/while/switch语句,“{”不另起一行,例如:
    struct var_data {
            int len;  /* 一个TAB,8个字符 */
            char data[0];
    };
    if ( a == b ) {
            a = c;
            d = a;
    }
    for ( i = 0; i < 10; i++ ) {
            a = c;
            d = a;
    }
    
    • 如果if、for循环后只有1行,不要加“{”和“}”,例如:
    for ( i = 0; i < 10; i++ )
            a = c;
    
    • if和else混用的情况下,else语句不另起一行,例如:
    if ( x == y ) {
            ……
    } else if ( x > y ) {
            ……
    } else {
            ……
    }
    

    对于函数,“{”另起一行,譬如:

    int add(int a, int b)
    {
            return a + b;
    }
    

    在switch/case语句方面,Linux建议switch和case对齐,例如:

    switch (suffix) {
    case ‘G’:
    case ‘g’:
            mem <<= 30;
            break;
    case ‘M’:
    case ‘m’:
            mem <<= 20;
            break;
    case ‘K’:
    case ‘k’:
            mem <<= 10;
            /* fall through */
    default:
            break;
    }
    

    内核下的==Documentation/CodingStyle==描述了Linux内核对编码风格的要求,内核下的==scripts/checkpatch.pl==提供了1个检查代码风格的脚本。如果我们使用script/checkpatch.pl检查包含如下代码块的源程序:

    for ( i = 0; I < 10; i++ ) {
            a = c;
    }
    

    就会产生“WARNING: braces {} are not necessary for single statement blocks”的警告。另外,==请注意代码中空格的应用==。

    3.2 GNU C与ANSI C

    Linux上可用的C编译器是GNU C编译器,它建立在自由软件基金会的编程许可证的基础上,因此可以自由发布。GNU C对标准C进行一系列扩展,以增强标准C的功能。

    1.零长度和变量长度数组

    GNU C允许使用零长度数组,在定义变长对象的头结构时,这个特性非常有用,例如:

    struct var_data {
            int len;
            char data[0]
    };
    

    char data[0]仅仅意味着程序中通过var_data结构体实例的data[index]成员可以访问len之后的第index个地址,它并没有为data[]数组分配内存,因此sizeof(struct var_data) = sizeof(int)。假设struct var_data的数据域就保存在struct var_data紧接着的内存区域,则通过如下代码可以遍历这些数据:

    struct var_data s;
    …
    for ( i = 0; i < s.len; i++ )
            printf(“%02x”, s.data[i]);
    

    GNU C中也可以使用1个变量定义数组,例如下面代码中定义的“double x[n]”:

    int main(int argc, char *argv[])
    {
            int i, n = argc;
            double x[n];
            for ( i = 0, i < n; i++ )
                    x[i] = i;
            return 0;
    }
    

    2.case范围

    GNU C支持case x…y这样的语法,区间[x,y]的数都会满足这个case的条件,请看下面的代码:

    switch (ch) {
    case ‘0’… ‘9’ : c -= ‘0’;
            break;
    case ‘a’… ‘f’ : c -= ‘a’ - 10;
            break;
    case ‘A’… ‘F’ : c -= ‘A’ - 10;
            break;
    }
    

    代码中的case ‘0’…‘9’等价于标准C中的:

    case ‘0’: case ‘1’: case ‘2’: case ‘3’: case ‘4’: 
    case ‘5’: case ‘6’: case ‘7’: case ‘8’: case ‘9’:
    

    3.语句表达式

    GNU C把包含在括号中的复合语句看做是一个表达式,称为语句表达式,它可以出现在任何允许表达式的地方。我们可以在语句表达式中使用原本只能在复合语句中使用的循环、局部变量等,例如:

    #define min_t(type,x,y) \
    ({type __x = (x)}; type __y = (y); __x < __y ? __x: __y; })
    int ia, ib, mini;
    float fa, fb, minf;
    mini = min_t(int, ia, ib);
    minf = min_t(float, fa,fb);
    

    因为重新定义了__x和__y这两个局部变量,所以上述方式定义的宏将不会有副作用。在标准C中,对应的如下宏则会产生副作用:

    #define min(x,y) ( (x) < (y) ? (x) : (y) )
    

    代码min(++ia, ++ib)会被展开为((++ia) < (++ib) ? (++ia) : (++ib)),传入宏的“参数”被增加2次。

    4.typeof关键字

    typeof()语句可以获得x的类型,因此,我们可以借助typeof重新定义min这个宏:

    #define min(x, y) ({ \
    const typeof(x) _x = (x);  \
    const typeof(y) _y = (y);  \
    (void) (&_x == &_y);   \
    _x < _y ? _x : _y;  })
    

    我们不需要像min_t(type,x,y)这个宏那样把type传入,因为通过typeof(x)、typeof(y)可以获得type。代码行(void) (&_x == &_y)的作用是检查_x和_y的类型是否一致。

    5.可变参数宏

    标准C就支持可变参数函数,意味着函数的参数是不固定的,例如printf()函数的原型为:

    int printf( const char *format [, argument]… );
    

    而在GNU C中,宏也可以接受可变数目的参数,例如:

    #define pr_debug(fmt, arg…) \
            printk(fmt, ##arg)
    

    这里arg表示其余的参数,可以是零个或多个,这些参数以及参数之间的逗号构成arg的值,在宏扩展时替换arg,例如下面代码:

    pr_debug(“%s:%d”, filename, line);
    

    会被扩展为:

    printk(“%s:%d”, filename, line);
    

    使用“##”的原因是处理arg不代表任何参数的情况,这时候,前面的逗号就变得多余了。使用“##”之后,GNU C预处理器会丢弃前面的逗号。这样,代码:

    pr_debug(“success!\n”);
        会被正确地扩展为:
    printk (“success!\n”);
        而不是:
    printk (“success!\n”,);
    

    这正是我们希望看到的。

    6.标号元素

    标准C要求数组或结构体的初始化值必须以固定的顺序出现,在GNU C中,通过指定索引或结构体成员名,允许初始化值以任意顺序出现。

    指定数组索引的方法是在初始化值钱添加“[INDEX] = ”,当然也可以用“[FIRST…LAST] = ”的形式指定一个范围。例如,下面的代码定义一个数组,并把其中的所有元素赋值为0:

    unsigned char data[MAX] = {[0…MAX-1] = 0};
    

    下面的代码借助结构体成员名初始化结构体:

    struct file_operations ext2_file_operations = {
       llseek: generic_file_llseek,
       read: generic_file_read,
       write: generic_file_write,
       ioctl: ext2_ioctl,
       mmap: generic_file_mmap,
       open: generic_file_open,
       release: ext2_release_file,
       fsync: ext2_sync_file,
    };
    

    但是,Linux 2.6推荐类似的代码应该尽量采用标准C的方式:

    struct file_operations ext2_file_operations = {
       .llseek = generic_file_llseek,
       .read = generic_file_read,
       .write = generic_file_write,
       .aio_read = generic_file_aio_read,
       .aio_write = generic_file_aio_write,
       .ioctl = ext2_ioctl,
       .mmap = generic_file_mmap,
       .open = generic_file_open,
       .release = ext2_release_file,
       .fsync = ext2_sync_file,
       .writev = generic_file_writev,
       .sendfile = generic_file_sendfile,
    };
    

    7.当前函数名

    GNU C预定义了两个标志符保存当前函数的名字,__FUNCTION__保存函数在源码中的名字,__PRETTY_FUNCTION__保存带语言特色的名字。在C函数中,这两个名字是相同的。

    void example()
    {
      printf(“This is function :%s”,__FUNCTION__);
    }
    

    代码中的__FUNCTION__意味着字符串“example”。C99已经支持__func__宏,因此建议在Linux编程中不再使用__FUNCTION__,转而使用__func__。

    8.特殊属性声明

    GNU C允许声明函数、变量和类型的特殊属性,以便进行手工的代码优化和定制代码检查的方法。要指定一个声明的属性,只需要在声明后添加__attribute__(( ATTRIBUTE ))。其中ATTRIBUTE为属性说明,如果存在多个属性,则以逗号分隔。GNU C支持noreturn、format、section、aligned、packed等十多个属性。

    noreturn属性作用于函数,表示该函数从不返回。这会让编译器优化代码,并消除不必要的警告信息。例如:

    #define ATTRIB_NORET __attribute__((noreturn)) …
    asmlinkage NORET_TYPE void do_exit(long error_code) ATTRIB_NORET;
    

    format属性也用于函数,表示函数使用printf、scanf或strftime风格的参数,指定format属性也可以让编译器根据格式串检查参数类型。例如:

    asmlinkage int printk(const char * fmt, …) __attribute__ ((format (printf, 1, 2)));
    

    上述代码中的第1个参数是格式串,从第2个参数开始都会根据printf()函数的格式串规则检查参数。

    unused属性作用于函数和变量,表示该函数或变量可能不会被用到,这个属性可以避免编译器产生警告信息。

    aligned属性用于变量、结构体或联合体,指定变量、结构体或联合体的对齐方式,以字节为单位,例如:

    struct example_struct{
       char a;
       int b;
       long c;
    }__attribute__((aligned(4)))
    

    表示该结构类型的变量以4字节对齐。

    packed属性作用于变量和类型,用于变量或结构体成员时表示使用最小可能的对界,用于枚举、结构体或联合体类型时表示该类型使用最小的内存。例如:

    struct example_struct {
       char a;
       int b;
       long c __attribute__((packed));
    };
    

    编译器对结构体成员及变量对界的目的是为了更快地访问结构体成员及变量占据的内存。例如,对于一个32位的整型变量,若以4字节方式存放(即低两位地址为00),则CPU在一个总线周期内就可以读取32位;若不然,CPU需要两次总线周期才能组合为一个32位整型。


    9.内建函数

    GNU C提供了大量的内建函数,其中大部分是标准C库函数的GNU C编译器内建版本,例如memcpy()等,它们与对应的标准C库函数功能相同。

    不属于库函数的其他内建函数的命名通常以__builtin开始,如下所示:

    • 内建函数__builtin_return_address(LEVEL)返回当前函数或其调用者的返回地址,参数LEVEL指定调用栈的级数,如0表示当前函数的返回地址,1表示当前函数的调用者的返回地址;
    • 内建函数__builtin_constant_p(EXP)用于判断一个值是否为编译时常数,如果参数EXP的值是常数,函数返回1,否则返回0;
    • 内建函数__builtin_expect(EXP, C)用于为编译器提供分支预测信息,其返回值是整数表达式EXP的值,C的值必须是编译时常数。

    例如,下面的检测第1个参数是否为编译时常数以确定采用参数版本还是非参数版本的代码:

    #define test_bit(nr, addr) \
    (__builtin_constant_p(nr)) ? \
    constant_test_bit((nr) , (addr)) : \
    variable_test_bit((nr) , (addr))
    

    在使用gcc编译C程序的时候,如果使用“-ansi -pedantic”编译选项,则会告诉编译器不使用GNU扩展语法。例如对于下面C程序test.c:

    struct var_data {
       int len;
       char data[0];
    };
    struct var_data a;
    

    直接编译可以通过:

    gcc -c test.c
    

    如果使用“-ansi -pedantic”编译选项,编译会报警:

    gcc -ansi -pedantic -c test.c
    test.c : 3 :warning: ISO C forbids zero-size array ‘data’
    

    3.3 do{} while(0)

    在Linux内核中,经常会看到do{} while(0)这样的语句,许多人开始都会疑惑,认为do{} while(0)毫无意义,因为它只会执行一次,加不加do{} while(0)效果是完全一样的。其实do{} while(0)的用法主要用于宏定义中。

    这里用一个简单的宏来演示:

    #define SAFE_FREE(P) do{ free(p); p = NULL;} while(0)
    

    假设这里去掉do…while(0),即定义SAFE_DELETE为:

    #define SAFE_FREE(p) free(p);p = NULL;
    

    那么以下代码:

    if(NULL != p)
        SAFE_DELETE(p)
    else
        …/* do something */
    

    会被展开为:

    if(NULL != P)
        free(p); p = NULL;
    else
        …/* do something */
    

    展开的代码中存在两个问题:

    • 因为if分支后面有两个语句,导致else分支没有对应的if,编译失败;
    • 假设没有else分支,则SAFE_FREE中的第二个语句无论if测试是否通过,都会执行。

    的确,将SAFE_FREE的定义加上{}就可以解决上述问题了,即:

    #define SAFE_FREE(P) { free(p); p = NULL; }
        这样,代码展开为:
    if(NULL != P)
        {free(p); p = NULL;}
    else
        …/* do something */
    

    但是,在C程序中,每个语句后面加分号是一种约定俗成的习惯,那么,如下代码:

    if(NULL != p)
        SAFE_DELETE(p);
    else
        …/* do something */
    

    会被展开为:

    if(NULL != P)
        {free(p); p = NULL;};
    else
        …/* do something */
    

    这样,else分支就又没有对应的if了,编译将无法通过。假设用了do{} while(0),情况就不一样了,同样的代码会被展开为:

    if(NULL != P)
        do{ free(p); p = NULL; }while(0);
    else
        …/* do something */
    

    不会再出现编译问题。do{} while(0)的使用完全是为了保证宏定义的使用者能无编译错误的使用宏,它不对其使用者做任何假设。

    3.4 goto

    用不用goto一直是一个著名的争议话题,Linux内核源代码中对goto的应用非常广泛,但是一般只限于错误处理中,其结构如:

       if(register_a() != 0)
          goto err;
       if(register_b() != 0)
          goto err1;
       if(register_c() != 0)
          goto err2;
       if(register_d() != 0)
          goto err3;
    ……
       unregister_d();
    err3:
       unregister_c();
    err2:
       unregister_b();
    err1:
       unregister_a();
    err:
       return ret;
    

    这种goto用于错误处理的用法实在是简单而高效,只需保证在错误处理时注销、资源释放等与正常的注册、资源申请顺序相反。

    相关文章

      网友评论

        本文标题:Linux驱动之内核编程

        本文链接:https://www.haomeiwen.com/subject/hmtruqtx.html