Linux内核工程导论——基础架构

时间:2022-08-28 21:44:24

基础功能元素


模块支持

模块概述

         可访问地址空间,可使用资源,

      模块是Linux支持动态功能扩展的最主要机制。内核代码中有很多模块,如果有了当前使用的内核的代码树,用户也可以编写外部的模块,动态添加到内核中执行。但是Linux内核的代码是GPL的,其内部暴露的调用有的是GPL的,有的不是。如果用户编程要使用GPL的系统调用,就需要将自己的模块完整的GPL公开,这就是GPL的传染机制。而,大部分的核心系统调用都是GPL的。

     很多公司,例如做ntfs文件系统内核模块驱动的公司tuxera,其最著名的是用户端开源的ntfs文件系统驱动ntfs-3g,然而,这个的效率渣的可以。不是其没能力优化,而是不愿意公开,其内部提供闭源版的ntfs内核模块驱动,如果要使用需要购买。这个模块的效率就比同作者的ntfs-3g高出一大截。再比如kcodes的打印机模块,该模块可以识别和处理几乎所有的打印机,非常强大,但是也是闭源的,需要购买。很多路由器公司内部的打印模块都是购买的这家公司的产品。

    那么为何这些公司可以闭源而不必遵守GPL呢?原因是他们大都采用两个模块的方式来规避,一个是GPL模块,封装内核的GPL调用,对外提供私有的非GPL调用,另一个模块则可以直接调用封装了的没有GPL要求的系统调用了。GPL协议的传染性不能穿透二进制(否则你用gcc写的所有程序都得开源)。

     内核的各个内部模块通过一系列的导出操作将自己的接口导出给其他模块使用,这里的模块并不是指.ko的物理模块,而是逻辑划分的功能模块。EXPORT_SYMBOL(),这个宏也是将函数导出让所有模块都可以使用,而EXPORT_SYMBOL_GPL()这个宏主要是给有GPL认证的模块使用。

    模块的执行原理与其他功能组件一样,都是预定好函数钩子,在添加、关闭的时候执行钩子函数。


static int __init init(void)

{

    printk("Hi module!\n");

    return 0;

}

static void __exit exit(void)

{

    printk("Bye module!\n");

}

module_init(init);

module_exit(exit);

        就构成了一个模块。容易看出来module_init和module_exit就是注册钩子函数的调用。模块内部定义的钩子函数式static的,目的是只是内部可见。__init和__exit是gcc的特性。作为一个编译器,提供回收明确表示无用代码的能力是很好的功能。标记为__init的函数会被放入.init.text代码段,这个段在模块加载完后会被回收节省内存,因为不会再用刀。

符号表查看

   内核符号表符号表是内核内部各个功能模块之间互相调用的纽带,各个模块互相靠这些函数调用通信。各个功能模块必须要导出符号表才能被模块使用。然而,技术上,不需要导出就使用也是可行的,但是内核在制度上反对。还有一点就是动态加载的模块的链接需求,在加载时符号表是对内核其他部分描述本模块的最好方式。加载的模块导出的函数通过导出操作就可以被其他模块定位并调用。

      cat /proc/kallsyms会打印出内核当前的符号表,通过more /boot/System.map 可以查看内核符号列表。可以显示编译好内核后所有在内核中的符号,模块中的要另行查看。通过nm vmlinux也可以查看内核符号列表,可以显示编译好内核后所有在内核中的符号,模块中的要另行查看。通过nm module_name可以查看模块的符号列表,但是得到是相对地址,只有加载后才会分配绝对地址。比如:e1000模块,如果e1000中的符号经过EXPORT_SYMBOL处理,等加载后,我们可以通过more /boot/System.map和nm vmlinux命令查看到,但是没有EXPORT_SYMBOL的,不能查看。

模块参数

         用户空间通过"echo-n ${value} > /sys/module/${modulename}/parameters/${parm}" 修改模块参数。

模块的加载和卸载

       模块机制存在的意义就是动态加载与卸载,原则上内核模块在被使用的过程中不可以被卸载,但也可以强制。而加载的时候也必须保证模块与运行中内核的相容。insmod、rmmod分别是约定的用户端加载和卸载模块的命令。

模块签名

      由于模块可以是外部代码,内核的版本又有很多个,内核必须确保该模块是使用当前内核代码编译出来的,否则执行会造成莫名其妙的错误。每个模块在编译时都会从内核目录中获得版本号,写入编译的模块,运行中的内核在插入新的模块时会检测签名是否一致,不一致就不会加载。


workqueue

         linux下的工作队列时一种将工作推后执行的方式,其可以被睡眠、调度,与内核线程表现基本一致,但又比内核线程使用简单,一般用来处理任务内容比较动态的任务链。workqueue有个特点是自动的根据CPU不同生成不同数目的队列。每个workqueue都可以添加多个work(使用queue_work函数)。

       系统有一个默认的work queue,然而用户可以自己定义自己的work queue。也正是由于后面的这个特点,很多用户定义了自己的work queue,而每一个work queue都对应一个内核线程,但不是每个work queue都是活跃到和其他work queue所需要的资源一样的。再考虑到一些wq在使用过程中的其他问题,内核开发者实现了一个内核线程池,动态的绑定到work queue上,如此每个work queue就不需要创建自己的内核线程,这个机制叫做cmwq(concurrency Managed Workqueue)。

      关于work queue 的更多信息参考:

Concurrency Managed Workqueue之(一):workqueue的基本概念

Concurrency Managed Workqueue之(二):CMWQ概述

Concurrency Managed Workqueue之(三):创建workqueue代码分析

Concurrency Managed Workqueue之(四):workqueue如何处理work

Tasklet

中断系统

         linux中中断分为3个层次。

        最低的层次是在arch下与各个平台相关的代码,一般位于平台代码下面的irq.c文件中,该部分代码直接与硬件相关,最后都要调用do_IRQ(__do_IRQ)进行执行。

          do_IRQ就是中断系统的中层,其根据下层传来的中断号找到对应的中断处理函数,处理多CPU访问和中断重入问题,然后调用真实的中断处理函数,也就是中断的上层。但是,这里内核做了区别,如果内核判断如果中断发生了嵌套(同时发生的中断多),则将中断处理函数以内核线程的形式运行,否则直接运行。所以,我们经常可以在PS命令的输出中看到:

如图这些软中断内核线程。

对于最上层,与各个中断的具体功能相关。

多CPU中断

中断亲和度

中断域

Linux kernel的中断子系统之(一):综述


DMA系统


特殊硬件框架

RAPID I/O

        是一种物理连接方式,也有对应的软件驱动。用于芯片到芯片,板到板的连接,可作为嵌入式系统的背板连接。在非行业专用系统中少见。

 

FPGA的使用:XillyBus

         Linux硬件系统中可以包含FPGA芯片,由于FPGA可被硬件随意编程为实现特定功能的组件,其实现的功能是纯硬件的,但其又要被Linux操作系统所能利用,所以就需要一个内核中存在的基础设施来驱动FPGA以导出给用户使用。

         这个驱动组件就是XillyBus。XillyBus的使用者必须在FPGA中将XillyBus模块的IP核放入FPGA硬件,内核中会运行一个XillyBus的数据转发模块,导出到用户空间供用户使用。由于这是一个通用性的组件,所以无法确切的指导数据流动的特点,因此其数据采用FIFO缓存。在用户空间的设备为:/dev/xillybus_*,

$ cat mydata > /dev/xillybus_thisfifo

$ cat /dev/xillybus_thatfifo > hisdata

         如此就可以读写其中的数据。

rpmsg、remoteproc

         一个板子上可能有多个cpu同时在跑多个操作系统,这些操作系统可能可以共享物理内存,可能是分割的。这些板上共享内存的操作系统之间也需要通信,但是如果采用传统的socket通信,那么代价太大。内核需要一种可以让两个CPU直接访问的缓存作为通信空间,从而创造一个通信协议,这个机制叫做rpmsg。这种机制在很多上游厂商的SDK中都有类似的实现。一般的大型嵌入式系统都会自己实现一个CPU间通信的协议,但大致上都是使用内存,将一块内存划分为一块块信道。

         这种板子还有一个需求,就是谁先启动的问题。一般的做法是一个这样的系统只启动一个操作系统,另外一个操作系统由先启动的操作系统启动。启动别人的操作系统叫做主操作系统系统,不但其可以控制启动,还可以控制关闭重启,远程过程调用等。这个框架功能就叫做remoteproc。

PWN

         用于控制电机、LED等的通用接口。类似软甲层次的GPIO。

PIN Controller

         很多硬件设备都有很多可以配置的引脚,通常是通过一系列寄存器对其进行配置和管理,而这些可用配置的种类又大致是相同的,因此就产生了抽象化的需求。这些引脚的配置空间抽象化为pin controller注册到内核的pin controll子系统中,统一管理。

VFIO、UIO

         VFIO是用来取代UIO的框架,允许用户端直接访问设备细节,也就是说让用户端设备驱动成为可能。其主要的工作成果是用户端可以可以配置IOMMU,让用户端也可以编程使用DMA。不过由于是新事物,其目前还仅支持PCI设备的驱动访问(vfio-pci模块),另外对CPU的IOMMU配置,也只实现了x86和PowerPC两种。

         用户端的设备文件是/dev/vfio/N。用户可以使用这个实现完全的设备驱动程序,目前的主要用途是虚拟机时的设备驱动透明访问。

SysRq

         sysrq类似Windows的Ctrl+Alt+del,只要系统不是出于完全锁死的状态,都会优先响应这个命令。在Linux中这个功能本身是可以打开关闭或配置的,在/proc/sys/kernel/sysrq中。Linux中调用这系列命令的方式SysRq键+命令。SysRq在大部分键盘上一般是Print Screen按键的副功能,需要使用Alt调用。与Windows不同的是,Windows一定是在按键后跳出图形界面,而Linux允许直接使用按键命令执行特定操作:SysRq+

'b':立即重启电脑

'c':立即产生一个系统级的crash dump(使用NULL指针访问)

'd':显示当前使用中的所有锁。

'e':发送SIGTERM给出了init之外的全部进程

'f':手动调用oom killer杀死一个最能用CPU的进程

'g':被kgdb使用

'h':显示SysRq的使用帮助

'i':发送SIGKILL信号给除了init外的所有进程

'j'    - Forcibly "Just thaw it" - filesystems frozen by the FIFREEZEioctl.

'k':杀掉当前虚拟终端上开启的所有进程

'l'    - Shows a stack backtrace for all active CPUs.

'm':导出当前的内存信息

'n'    - Used to make RT tasks nice-able

'o'    - Will shut your system off (if configured and supported).

'p'    - Will dump the current registers and flags to your console.

'q'    - Will dump per CPU lists of all armed hrtimers (but NOT regular

         timer_list timers) and detailed information about all

         clockevent devices.

'r'    - Turns off keyboard raw mode and sets it to XLATE.

's'    - Will attempt to sync all mounted filesystems.

't'    - Will dump a list of current tasks and their information to your

         console.

'u'    - Will attempt to remount all mounted filesystems read-only.

'v'    - Forcefully restores framebuffer console

'v'    - Causes ETM buffer dump [ARM-specific]

'w'    - Dumps tasks that are in uninterruptable (blocked) state.

'x'    - Used by xmon interface on ppc/powerpc platforms.

         Show global PMU Registers on sparc64.

'y':打印所有寄存器

'z':导出ftrace buffer

'0'-'9':设置内核的log级别

 

         这些命令视内核的配置而部分有效。

SysCtl

 

时钟

高精度时钟同步:PTP

         IEEE1588定义了一种新的时钟同步方式。该方式的出现是因为局域网内的高精度同步没有很好的产品。NTP和SNTP的精度不能满足需求。PTP借鉴自NTP,主要思想是通过一个同步信号周期性的与全网络中的设备同步校准。一个网络中只有一个主时钟,用来产生最高精度的信号,其他的都为边界时钟,用来接收主时钟的同步信息来调整自己。

PPS

         PPS设备每一秒钟会发送一个脉冲。系统可以使用这种设备做到时钟同步,或其他定时操作。

Watchdog

/dev/watchdog

RTC

         PC电脑都有一个离线还可以运行的时钟,非PC电脑可能有多个。这个时钟在运行期可以看做是准确的,实时的,但是硬件原因,长期运行产生偏差也是不可避免的。Linux内核在启动的时候会去查询这个值,并用来维护自己的时间信息,然后启动后,大部分linux都会使用网络时间来重新确定本机的时间,还会向RTC硬件写入,用来校准。完成这个工作的内核子系统叫做RTC。

         由于RTC硬件的时间是存储在寄存器中的,一般存储的都是自某一个时间(1900或1970)以来的秒数,而寄存器的大小是有限的,所以不同系统对这个算法的做法就不一样。例如uboot读取这个值加上1900年就是现在的时间,但是linux除此之外会判断如果小于1969年,会加上100年得到现在的时间。由于算法不一样,所以在bootloader中和linux中看到的时钟时间不一样是正常的。

         RTC子系统的存在,使得不同的硬件时钟对于系统软件透明,省去了编程的麻烦。与其他模块类似的,rtc也定义了设备,可以供用户在/dev目录下访问,叫做rtc或rtcN(n为数,一个硬件系统可能会有多个rtc时钟,但大部分PC只有一个)。大多数的rtc带有中断功能,常见的x86系统中的8号中断就是时钟中断,内核可以使用该中断功能周期性的执行自己的任务。用户端也可以通过rtc设备使用这个中断机制。打开这个设备文件后,使用ioctrl设置频率后,周期性去读取这个设备值,就能测量时间。因为设置频率就是设置了该时钟触发8号中断的频率,读取设备值得到的就是自上次读操作至今的中断数目。因此,每读一次就可以得到当前过去的时间。这个时间的粒度和准确度是可以由设置不同的频率和读取频率控制的。rtc用户端设备文件一次只允许一个用户独占的打开。

         所以,如果你只是正常的使用linux时间,不需要特别精准的基于时间的中断操作,又有互联网接入,你可以不使用rtc。你也可以使用一个程序周期性去读取网络时间,通过保持同步,向外发出时间信号来做到基于时间的中断。由于系统启动后有晶振,所以一般的系统也会使用此晶振来作为时间的计量工具。因此,RTC存在的必要性在很多情况下并不大。

 

PADATA

并行数据处理

namaspace

magic number

引用计数组件:kref

         内核中很多地方都有使用引用计数的需求。涉及到资源回收和资源竞争或者是访问统计等。这种需求一般是使用一个整数,自己编写的时候控制其增加或减少。而控制的时候又要考虑并发冲突等很多情况,通常要自己封装函数。Linux就实现了一种通用的数据结构和相关函数调用,使用者直接使用接口即可。

struct my_data

{

       .

       .

       struct kref refcount;

       .

       .

};

binfmt_misc

         我们在shell中敲入的命令必须是内置的或者是位于PATH变量路径中的的elf可执行文件。然而,linux不止可以支持elf格式的文件,例如通过python解释器可以执行python的程序,emac程序或者java程序等都是通过在命令行中先输入执行程序,然后键入具体要执行的命令程序。

         内核提供了一种方法允许将例如java这种程序与elf一致看待。用户只需要在shell中敲入java程序名(或者python程序名),只要该程序在PATH下就可以像elf格式可执行程序一样被执行。做到这样的方式是使用binfmt_misc 机制,该机制通过proc文件系统操作,要使用首先要先mount上去:mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc。然后向/proc/sys/fs/binfmt_misc/register中写入规定格式的字符串即可。

:name:type:offset:magic:mask:interpreter:flags

         具体的各个含义查看帮助。