精华内容
下载资源
问答
  • Linux内核结构详解

    2020-08-04 01:29:43
    本文主要介绍的是linux系统内核结构,初学者可以参考此文来学习linux
  •  如下图所示,Linux体系结构,从大的方面可以分为用户空间(User Space)和内核空间(Kernel Space)。  用户空间中包含了C库,用户的应用程序。在某些体系结构图中还包含了shell,当然shell脚本也是Linux...
  • linux内核结构

    2019-09-25 11:22:10
    linux内核结构图,从硬件,到设备控制,再到bridge,虚拟成,用户接口层等,用图形把内核结构描述出来,是不可多得的了解linux内核的好图片。
  • Linux内核结构详解教 程 Linux内核教程 linux内核就像人的心脏灵魂指挥中心 内核是一个操作系统的核心它负责管理系统的进程内存设备驱动程序文件和网络系统决定着 系统的性能和稳定性内核以独占的方式执行最底层任务...
  • 第十二讲 Linux内核结构及移植;Linux内核结构及移植;Linux内核结构及移植;Linux内核结构;Linux 内核可以划分成三层 系统调用接口SCI实现了一些基本的调用功能例如 read 和 write等 内核代码独立于体系结构的内核代码...
  • linux内核就像人的心脏,灵魂,指挥中心。 内核是一个操作系统的核心,它负责管理系统的进程,内存,设备驱动程序,文件和网络系统,决定着 系统的性能和稳定性。内核以独占的方式执行最底层任务,保证系统正常运行。...
  • Linux_Kernel核心中文手册(内核图解).pdf,linux内核结构详解,C,C++源码.zip
  • Linux 内核结构与进程管理,想对linux深入了解的朋友进来看看!!
  • 详细描述了linux2.4/2.6内核版本中的网络子系统。解释了协议的工作方式、建立了Linux网络体系结构中的多种重要概念——从设备驱动程序概念一直到应用程序接口的概念。能帮助读者更容易理解 Linux网络架构的进程和...
  • 包含了linux内核各个数据结构的图解,是广大内核爱好者学习内核源代码的辅助材料
  • 漫画 —— Linux 内核结构

    千次阅读 2018-08-12 11:00:29
    Daniel Stori 用漫画的方式将 Linux 内核结构清楚的展示出来,赞! User Space Election Adopt a good cause, DON'T SIGKILL

    Daniel Stori 用漫画的方式将 Linux 内核的结构清楚的展示出来,赞!

    User Space Election

    User Space Election geek comic

    Adopt a good cause, DON'T SIGKILL

    Adopt a good cause, DON'T SIGKILL geek comic

    展开全文
  • Linux内核就是基于这个策略实现的。Linux进程1.采用层次结构,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程。该进程负责进一步的系统初始化操作。init进程是进程树的根,所有的进程都直接或者间接...

    简介

    作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。目前支持模块的动态装卸(裁剪)。Linux内核就是基于这个策略实现的。Linux进程1.采用层次结构,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程。该进程负责进一步的系统初始化操作。init进程是进程树的根,所有的进程都直接或者间接起源于该进程。virt/ ---- 提供虚拟机技术的支持。

     

    全文导读


    Linux内核预备工作

    理解Linux内核最好预备的知识点:

    懂C语言
    懂一点操作系统的知识
    熟悉少量相关算法
    懂计算机体系结构

    Linux内核的特点:

    结合了unix操作系统的一些基础概念
     


    Linux内核的任务:

    1.从技术层面讲,内核是硬件与软件之间的一个中间层。作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。

    2.从应用程序的层面讲,应用程序与硬件没有联系,只与内核有联系,内核是应用程序知道的层次中的最底层。在实际工作中内核抽象了相关细节。

    3.内核是一个资源管理程序。负责将可用的共享资源(CPU时间、磁盘空间、网络连接等)分配得到各个系统进程。

    4.内核就像一个库,提供了一组面向系统的命令。系统调用对于应用程序来说,就像调用普通函数一样。

    内核实现策略:

    1.微内核。最基本的功能由中央内核(微内核)实现。所有其他的功能都委托给一些独立进程,这些进程通过明确定义的通信接口与中心内核通信。

    2.宏内核。内核的所有代码,包括子系统(如内存管理、文件管理、设备驱动程序)都打包到一个文件中。内核中的每一个函数都可以访问到内核中所有其他部分。目前支持模块的动态装卸(裁剪)。Linux内核就是基于这个策略实现的。

    哪些地方用到了内核机制?

    1.进程(在cpu的虚拟内存中分配地址空间,各个进程的地址空间完全独立;同时执行的进程数最多不超过cpu数目)之间进行通   信,需要使用特定的内核机制。

    2.进程间切换(同时执行的进程数最多不超过cpu数目),也需要用到内核机制。

    进程切换也需要像FreeRTOS任务切换一样保存状态,并将进程置于闲置状态/恢复状态。

    3.进程的调度。确认哪个进程运行多长的时间。

    Linux进程

    1.采用层次结构,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程。该进程负责进一步的系统初始化操作。init进程是进程树的根,所有的进程都直接或者间接起源于该进程。

    2.通过pstree命令查询。实际上得系统第一个进程是systemd,而不是init(这也是疑问点)

    3.系统中每一个进程都有一个唯一标识符(ID),用户(或其他进程)可以使用ID来访问进程。

    Linux内核源代码的目录结构

    Linux内核源代码包括三个主要部分:

    1. 内核核心代码,包括第3章所描述的各个子系统和子模块,以及其它的支撑子系统,例如电源管理、Linux初始化等

    2. 其它非核心代码,例如库文件(因为Linux内核是一个自包含的内核,即内核不依赖其它的任何软件,自己就可以编译通过)、固件集合、KVM(虚拟机技术)等

    3. 编译脚本、配置文件、帮助文档、版权说明等辅助性文件

    使用ls命令看到的内核源代码的顶层目录结构,具体描述如下。

    include/ ---- 内核头文件,需要提供给外部模块(例如用户空间代码)使用。

    kernel/ ---- Linux内核的核心代码,包含了3.2小节所描述的进程调度子系统,以及和进程调度相关的模块。

    mm/ ---- 内存管理子系统(3.3小节)。

    fs/ ---- VFS子系统(3.4小节)。

    net/ ---- 不包括网络设备驱动的网络子系统(3.5小节)。

    ipc/ ---- IPC(进程间通信)子系统。

    arch// ---- 体系结构相关的代码,例如arm, x86等等。
        arch//mach- ---- 具体的machine/board相关的代码。
        arch//include/asm ---- 体系结构相关的头文件。
        arch//boot/dts ---- 设备树(Device Tree)文件。

    init/ ---- Linux系统启动初始化相关的代码。
    block/ ---- 提供块设备的层次。
    sound/ ---- 音频相关的驱动及子系统,可以看作“音频子系统”。
    drivers/ ---- 设备驱动(在Linux kernel 3.10中,设备驱动占了49.4的代码量)。

    lib/ ---- 实现需要在内核中使用的库函数,例如CRC、FIFO、list、MD5等。
    crypto/ ----- 加密、解密相关的库函数。
    security/ ---- 提供安全特性(SELinux)。
    virt/ ---- 提供虚拟机技术(KVM等)的支持。
    usr/ ---- 用于生成initramfs的代码。
    firmware/ ---- 保存用于驱动第三方设备的固件。

    samples/ ---- 一些示例代码。
    tools/ ---- 一些常用工具,如性能剖析、自测试等。

    Kconfig, Kbuild, Makefile, scripts/ ---- 用于内核编译的配置文件、脚本等。

    COPYING ---- 版权声明。
    MAINTAINERS ----维护者名单。
    CREDITS ---- Linux主要的贡献者名单。
    REPORTING-BUGS ---- Bug上报的指南。

    Documentation, README ---- 帮助、说明文档。

    Linux内核体系结构简析简析

    图1 Linux系统层次结构

    最上面是用户(或应用程序)空间。这是用户应用程序执行的地方。用户空间之下是内核空间,Linux 内核正是位于这里。GNU C Library (glibc)也在这里。它提供了连接内核的系统调用接口,还提供了在用户空间应用程序和内核之间进行转换的机制。这点非常重要,因为内核和用户空间的应用程序使用的是不同的保护地址空间。每个用户空间的进程都使用自己的虚拟地址空间,而内核则占用单独的地址空间。

    Linux 内核可以进一步划分成 3 层。最上面是系统调用接口,它实现了一些基本的功能,例如 read 和 write。系统调用接口之下是内核代码,可以更精确地定义为独立于体系结构的内核代码。这些代码是 Linux 所支持的所有处理器体系结构所通用的。在这些代码之下是依赖于体系结构的代码,构成了通常称为 BSP(Board Support Package)的部分。这些代码用作给定体系结构的处理器和特定于平台的代码。

    Linux 内核实现了很多重要的体系结构属性。在或高或低的层次上,内核被划分为多个子系统。Linux 也可以看作是一个整体,因为它会将所有这些基本服务都集成到内核中。这与微内核的体系结构不同,后者会提供一些基本的服务,例如通信、I/O、内存和进程管理,更具体的服务都是插入到微内核层中的。每种内核都有自己的优点,不过这里并不对此进行讨论。 

    随着时间的流逝,Linux 内核在内存和 CPU 使用方面具有较高的效率,并且非常稳定。但是对于 Linux 来说,最为有趣的是在这种大小和复杂性的前提下,依然具有良好的可移植性。Linux 编译后可在大量处理器和具有不同体系结构约束和需求的平台上运行。一个例子是 Linux 可以在一个具有内存管理单元(MMU)的处理器上运行,也可以在那些不提供 MMU 的处理器上运行。

    Linux 内核的 uClinux 移植提供了对非 MMU 的支持。

     

    图2 Linux内核体系结构

    Linux内核的主要组件有:系统调用接口、进程管理、内存管理、虚拟文件系统、网络堆栈、设备驱动程序、硬件架构的相关代码。

    (1)系统调用接口

    SCI 层提供了某些机制执行从用户空间到内核的函数调用。正如前面讨论的一样,这个接口依赖于体系结构,甚至在相同的处理器家族内也是如此。SCI 实际上是一个非常有用的函数调用多路复用和多路分解服务。在 ./linux/kernel 中您可以找到 SCI 的实现,并在 ./linux/arch 中找到依赖于体系结构的部分。

    (2)进程管理

    进程管理的重点是进程的执行。在内核中,这些进程称为线程,代表了单独的处理器虚拟化(线程代码、数据、堆栈和 CPU 寄存器)。在用户空间,通常使用进程 这个术语,不过 Linux 实现并没有区分这两个概念(进程和线程)。内核通过 SCI 提供了一个应用程序编程接口(API)来创建一个新进程(fork、exec 或 Portable Operating System Interface [POSIX] 函数),停止进程(kill、exit),并在它们之间进行通信和同步(signal 或者 POSIX 机制)。

    进程管理还包括处理活动进程之间共享 CPU 的需求。内核实现了一种新型的调度算法,不管有多少个线程在竞争 CPU,这种算法都可以在固定时间内进行操作。这种算法就称为 O(1) 调度程序,这个名字就表示它调度多个线程所使用的时间和调度一个线程所使用的时间是相同的。O(1) 调度程序也可以支持多处理器(称为对称多处理器或 SMP)。您可以在 ./linux/kernel 中找到进程管理的源代码,在 ./linux/arch 中可以找到依赖于体系结构的源代码。

    (3)内存管理

    内核所管理的另外一个重要资源是内存。为了提高效率,如果由硬件管理虚拟内存,内存是按照所谓的内存页 方式进行管理的(对于大部分体系结构来说都是 4KB)。Linux 包括了管理可用内存的方式,以及物理和虚拟映射所使用的硬件机制。不过内存管理要管理的可不止 4KB 缓冲区。Linux 提供了对 4KB 缓冲区的抽象,例如 slab 分配器。这种内存管理模式使用 4KB 缓冲区为基数,然后从中分配结构,并跟踪内存页使用情况,比如哪些内存页是满的,哪些页面没有完全使用,哪些页面为空。这样就允许该模式根据系统需要来动态调整内存使用。为了支持多个用户使用内存,有时会出现可用内存被消耗光的情况。由于这个原因,页面可以移出内存并放入磁盘中。这个过程称为交换,因为页面会被从内存交换到硬盘上。内存管理的源代码可以在 ./linux/mm 中找到。

    (4)虚拟文件系统

    虚拟文件系统(VFS)是 Linux 内核中非常有用的一个方面,因为它为文件系统提供了一个通用的接口抽象。VFS 在 SCI 和内核所支持的文件系统之间提供了一个交换层(请参看图4)。

    图3 Linux文件系统层次结构

    在 VFS 上面,是对诸如 open、close、read 和 write 之类的函数的一个通用 API 抽象。在 VFS 下面是文件系统抽象,它定义了上层函数的实现方式。它们是给定文件系统(超过 50 个)的插件。文件系统的源代码可以在 ./linux/fs 中找到。文件系统层之下是缓冲区缓存,它为文件系统层提供了一个通用函数集(与具体文件系统无关)。这个缓存层通过将数据保留一段时间(或者随即预先读取数据以便在需要是就可用)优化了对物理设备的访问。缓冲区缓存之下是设备驱动程序,它实现了特定物理设备的接口。

    (5)网络堆栈

    网络堆栈在设计上遵循模拟协议本身的分层体系结构。回想一下,Internet Protocol (IP) 是传输协议(通常称为传输控制协议或 TCP)下面的核心网络层协议。TCP 上面是 socket 层,它是通过 SCI 进行调用的。socket 层是网络子系统的标准 API,它为各种网络协议提供了一个用户接口。从原始帧访问到 IP 协议数据单元(PDU),再到 TCP 和 User Datagram Protocol (UDP),socket 层提供了一种标准化的方法来管理连接,并在各个终点之间移动数据。内核中网络源代码可以在 ./linux/net 中找到。

    (6)设备驱动程序

    Linux 内核中有大量代码都在设备驱动程序中,它们能够运转特定的硬件设备。Linux 源码树提供了一个驱动程序子目录,这个目录又进一步划分为各种支持设备,例如 Bluetooth、I2C、serial 等。设备驱动程序的代码可以在 ./linux/drivers 中找到。

    (7)依赖体系结构的代码

    尽管 Linux 很大程度上独立于所运行的体系结构,但是有些元素则必须考虑体系结构才能正常操作并实现更高效率。./linux/arch 子目录定义了内核源代码中依赖于体系结构的部分,其中包含了各种特定于体系结构的子目录(共同组成了 BSP)。对于一个典型的桌面系统来说,使用的是 x86 目录。每个体系结构子目录都包含了很多其他子目录,每个子目录都关注内核中的一个特定方面,例如引导、内核、内存管理等。这些依赖体系结构的代码可以在 ./linux/arch 中找到。

    如果 Linux 内核的可移植性和效率还不够好,Linux 还提供了其他一些特性,它们无法划分到上面的分类中。作为一个生产操作系统和开源软件,Linux 是测试新协议及其增强的良好平台。Linux 支持大量网络协议,包括典型的 TCP/IP,以及高速网络的扩展(大于 1 Gigabit Ethernet [GbE] 和 10 GbE)。Linux 也可以支持诸如流控制传输协议(SCTP)之类的协议,它提供了很多比 TCP 更高级的特性(是传输层协议的接替者)。

    Linux 还是一个动态内核,支持动态添加或删除软件组件。被称为动态可加载内核模块,它们可以在引导时根据需要(当前特定设备需要这个模块)或在任何时候由用户插入。

    Linux 最新的一个增强是可以用作其他操作系统的操作系统(称为系统管理程序)。最近,对内核进行了修改,称为基于内核的虚拟机(KVM)。这个修改为用户空间启用了一个新的接口,它可以允许其他操作系统在启用了 KVM 的内核之上运行。除了运行 Linux 的其他实例之外, Microsoft Windows也可以进行虚拟化。惟一的限制是底层处理器必须支持新的虚拟化指令。

    Linux体系结构和内核结构区别

    1.当被问到Linux体系结构(就是Linux系统是怎么构成的)时,我们可以参照下图这么回答:从大的方面讲,Linux体系结构可以分为两块:

    (1)用户空间:用户空间中又包含了,用户的应用程序,C库

    (2)内核空间:内核空间包括,系统调用,内核,以及与平台架构相关的代码

    2.Linux体系结构要分成用户空间和内核空间的原因:

    1)现代CPU通常都实现了不同的工作模式,

    以ARM为例:ARM实现了7种工作模式,不同模式下CPU可以执行的指令或者访问的寄存器不同:

    (1)用户模式 usr 

    (2)系统模式  sys

    (3)管理模式 svc

    (4)快速中断  fiq

    (5)外部中断  irq

    (6)数据访问终止 abt

    (7)未定义指令异常

    以(2)X86为例:X86实现了4个不同级别的权限,Ring0—Ring3 ;Ring0下可以执行特权指令,可以访问IO设备;Ring3则有很多的限制  

    2)所以,Linux从CPU的角度出发,为了保护内核的安全,把系统分成了2部分;

    3.用户空间和内核空间是程序执行的两种不同状态,我们可以通过“系统调用”和“硬件中断“来完成用户空间到内核空间的转移

    4.Linux的内核结构(注意区分LInux体系结构和Linux内核结构)

    Linux驱动的platform机制

    Linux的这种platform driver机制和传统的device_driver机制相比,一个十分明显的优势在于platform机制将本身的资源注册进内核,由内核统一管理,在驱动程序中使用这些资源时通过platform_device提供的标准接口进行申请并使用。这样提高了驱动和资源管理的独立性,并且拥有较好的可移植性和安全性。下面是SPI驱动层次示意图,Linux中的SPI总线可理解为SPI控制器引出的总线:

    和传统的驱动一样,platform机制也分为三个步骤:

    1、总线注册阶段:

    内核启动初始化时的main.c文件中的kernel_init()→do_basic_setup()→driver_init()→platform_bus_init()→bus_register(&platform_bus_type),注册了一条platform总线(虚拟总线,platform_bus)。

    2、添加设备阶段:

    设备注册的时候Platform_device_register()→platform_device_add()→(pdev→dev.bus = &platform_bus_type)→device_add(),就这样把设备给挂到虚拟的总线上。

    3、驱动注册阶段:

    Platform_driver_register()→driver_register()→bus_add_driver()→driver_attach()→bus_for_each_dev(), 对在每个挂在虚拟的platform bus的设备作__driver_attach()→driver_probe_device(),判断drv→bus→match()是否执行成功,此时通过指针执行platform_match→strncmp(pdev→name , drv→name , BUS_ID_SIZE),如果相符就调用really_probe(实际就是执行相应设备的platform_driver→probe(platform_device)。)开始真正的探测,如果probe成功,则绑定设备到该驱动。

    从上面可以看出,platform机制最后还是调用了bus_register() , device_add() , driver_register()这三个关键的函数。

    下面看几个结构体:

    struct platform_device           
    (/include/linux/Platform_device.h)
    {        
    const char    * name;        
    int        id;        
    struct device    dev;        
    u32        num_resources;        
    struct resource    * resource;
    };

    Platform_device结构体描述了一个platform结构的设备,在其中包含了一般设备的结构体struct device  dev;设备的资源结构体struct resource   * resource;还有设备的名字const char * name。(注意,这个名字一定要和后面platform_driver.driver àname相同,原因会在后面说明。)

    该结构体中最重要的就是resource结构,这也是之所以引入platform机制的原因。

    struct resource                            
    ( /include/linux/ioport.h)
    {        
    resource_size_t start;        
    resource_size_t end;        
    const char *name;        
    unsigned long flags;        
    struct resource *parent, *sibling, *child;
    };
    其中 flags位表示该资源的类型,start和end分别表示该资源的起始地址和结束地址(/include/linux/Platform_device.h):
    struct platform_driver              
    {        
    int (*probe)(struct platform_device *);        
    int (*remove)(struct platform_device *);        
    void (*shutdown)(struct platform_device *);        
    int (*suspend)(struct platform_device *, pm_message_t state);        
    int (*suspend_late)(struct platform_device *, pm_message_t state);        
    int (*resume_early)(struct platform_device *);        
    int (*resume)(struct platform_device *);        
    struct device_driver driver;
    };
    Platform_driver结构体描述了一个platform结构的驱动。其中除了一些函数指针外,还有一个一般驱动的device_driver结构。

    名字要一致的原因:

    上面说的驱动在注册的时候会调用函数bus_for_each_dev(), 对在每个挂在虚拟的platform bus的设备作__driver_attach()→driver_probe_device(),在此函数中会对dev和drv做初步的匹配,调用的是drv->bus->match所指向的函数。platform_driver_register函数中drv->driver.bus = &platform_bus_type,所以drv->bus->match就为platform_bus_type→match,为platform_match函数,该函数如下:

    static int platform_match(struct device * dev, struct device_driver * drv)   
    {       
    struct platform_device *pdev = container_of(dev, struct platform_device, dev);
    return (strncmp(pdev->name, drv->name, BUS_ID_SIZE) == 0);
    }


    是比较dev和drv的name,相同则会进入really_probe()函数,从而进入自己写的probe函数做进一步的匹配。所以dev→name和driver→drv→name在初始化时一定要填一样的。

    不同类型的驱动,其match函数是不一样的,这个platform的驱动,比较的是dev和drv的名字,还记得usb类驱动里的match吗?它比较的是Product ID和Vendor ID。

    个人总结Platform机制的好处:

    1、提供platform_bus_type类型的总线,把那些不是总线型的soc设备都添加到这条虚拟总线上。使得,总线——设备——驱动的模式可以得到普及。

    2、提供platform_device和platform_driver类型的数据结构,将传统的device和driver数据结构嵌入其中,并且加入resource成员,以便于和Open Firmware这种动态传递设备资源的新型bootloader和kernel 接轨。

     

    Linux内核体系结构

    因为Linux内核是单片的,所以它比其他类型的内核占用空间最大,复杂度也最高。这是一个设计特性,在Linux早期引起了相当多的争论,并且仍然带有一些与单内核固有的相同的设计缺陷。

    为了解决这些缺陷,Linux内核开发人员所做的一件事就是使内核模块可以在运行时加载和卸载,这意味着您可以动态地添加或删除内核的特性。这不仅可以向内核添加硬件功能,还可以包括运行服务器进程的模块,比如低级别虚拟化,但也可以替换整个内核,而不需要在某些情况下重启计算机。
    想象一下,如果您可以升级到Windows服务包,而不需要重新启动……

    内核模块

    如果Windows已经安装了所有可用的驱动程序,而您只需要打开所需的驱动程序怎么办?这本质上就是内核模块为Linux所做的。内核模块,也称为可加载内核模块(LKM),对于保持内核在不消耗所有可用内存的情况下与所有硬件一起工作是必不可少的。

    模块通常向基本内核添加设备、文件系统和系统调用等功能。lkm的文件扩展名是.ko,通常存储在/lib/modules目录中。由于模块的特性,您可以通过在启动时使用menuconfig命令将模块设置为load或not load,或者通过编辑/boot/config文件,或者使用modprobe命令动态地加载和卸载模块,轻松定制内核。

    第三方和封闭源码模块在一些发行版中是可用的,比如Ubuntu,默认情况下可能无法安装,因为这些模块的源代码是不可用的。该软件的开发人员(即nVidia、ATI等)不提供源代码,而是构建自己的模块并编译所需的.ko文件以便分发。虽然这些模块像beer一样是免费的,但它们不像speech那样是免费的,因此不包括在一些发行版中,因为维护人员认为它通过提供非免费软件“污染”了内核。

    内核并不神奇,但对于任何正常运行的计算机来说,它都是必不可少的。Linux内核不同于OS X和Windows,因为它包含内核级别的驱动程序,并使许多东西“开箱即用”。希望您能对软件和硬件如何协同工作以及启动计算机所需的文件有更多的了解。

    Linux 内核学习经验总结

    开篇

    学习内核,每个人都有自己的学习方法,仁者见仁智者见智。以下是我在学习过程中总结出来的东西,对自身来说,我认为比较有效率,拿出来跟大家交流一下。

    内核学习,一偏之见;疏漏难免,恳请指正。

    为什么写这篇博客

    刚开始学内核的时候,不要执着于一个方面,不要专注于一个子系统就一头扎到实际的代码行中去,因为这样的话,牵涉的面会很广,会碰到很多困难,容易产生挫败感,一个函数体中(假设刚开始的时候正在学习某个方面的某个具体的功能函数)很可能掺杂着其他各个子系统方面设计理念(多是大量相关的数据结构或者全局变量,用于支撑该子系统的管理工作)下相应的代码实现,这个时候看到这些东西,纷繁芜杂,是没有头绪而且很不理解的,会产生很多很多的疑问,(这个时候如果对这些疑问纠缠不清,刨根问底,那么事实上就是在学习当前子系统的过程中频繁的去涉足其他子系统,这时候注意力就分散了),而事实上等了解了各个子系统后再回头看这些东西的话,就简单多了,而且思路也会比较清晰。所以,要避免 “只见树木,不见森林”,不要急于深入到底层代码中去,不要过早研究底层代码。

    我在大二的时候刚开始接触内核,就犯了这个错误,一头扎到内存管理里头,去看非常底层的实现代码,虽然也是建立在内存管理的设计思想的基础上,但是相对来说,比较孤立,因为此时并没有学习其它子系统,应该说无论是视野还是思想,都比较狭隘,所以代码中牵涉到的其它子系统的实现我都直接跳过了,这一点还算聪明,当然也是迫不得已的。

    我的学习方法

    刚开始,我认为主要的问题在于你知道不知道,而不是理解不理解,某个子系统的实现采用了某种策略、方法,而你在学习中需要做的就是知道有这么一回事儿,然后才是理解所描述的策略或者方法。

    根据自己的学习经验,刚开始学习内核的时候,我认为要做的是在自己的脑海中建立起内核的大体框架,理解各个子系统的设计理念和构建思想,这些理念和思想会从宏观上呈献给你清晰的脉络,就像一个去除了枝枝叶叶的大树的主干,一目了然;当然,肯定还会涉及到具体的实现方法、函数,但是此时接触到的函数或者方法位于内核实现的较高的层次,是主(要)函数,已经了解到这些函数,针对的是哪些设计思想,实现了什么样的功能,达成了什么样的目的,混个脸熟的说法在这儿也是成立的。至于该主函数所调用的其它的辅助性函数就等同于枝枝叶叶了,不必太早就去深究。此时,也就初步建立起了内核子系统框架和代码实现之间的关联,关联其实很简单,比如一看到某个函数名字,就想起这个函数是针对哪个子系统的,实现了什么功能。

    我认为此时要看的就是LKD3,这本书算是泛泛而谈,主要就是从概念,设计,大的实现方法上描述各个子系统,而对于具体的相关的函数实现的代码讲解很少涉及(对比于ULK3,此书主要就是关于具体函数代码的具体实现的深入分析,当然,你也可以看,但是过早看这本书,会感觉很痛苦,很枯燥无味,基本上都是函数的实现),很少,但不是没有,这就很好,满足我们当前的需求,还避免我们过早深入到实际的代码中去。而且本书在一些重要的点上还给出了写程序时的注意事项,算是指导性建议。主要的子系统包括:内存管理,进程管理和调度,系统调用,中断和异常,内核同步,时间和定时器管理,虚拟文件系统,块I/O层,设备和模块。(这里的先后顺序其实就是LKD3的目录的顺序)。

    我学习的时候是三本书交叉着看的,先看LKD3,专于一个子系统,主要就是了解设计的原理和思想,当然也会碰到对一些主要函数的介绍,但大多就是该函数基于前面介绍的思想和原理完成了什么样的功能,该书并没有就函数本身的实现进行深入剖析。然后再看ULK3和PLKA上看同样的子系统,但是并不仔细分析底层具体函数的代码,只是粗略地、不求甚解地看,甚至不看。因为,有些时候,在其中一本书的某个点上,卡壳了,不是很理解了,在另外的书上你可能就碰到对同一个问题的不同角度的描述,说不准哪句话就能让你豁然开朗,如醍醐灌顶。我经常碰到这种情况。

    并不是说学习过程中对一些函数体的实现完全就忽略掉,只要自己想彻底了解其代码实现,没有谁会阻止你。我是在反复阅读过程中慢慢深入的。比如VFS中文件打开需要对路径进行分析,需要考虑的细节不少(.././之类的),但是其代码实现是很好理解的。再比如,CFS调度中根据shedule latency、队列中进程个数及其nice值(使用的是动态优先级)计算出分配给进程的时间片,没理由不看的,这个太重要了,而且也很有意思。

    ULK3也会有设计原理与思想之类的概括性介绍,基本上都位于某个主题的开篇段落。但是更多的是对支持该原理和思想的主要函数实现的具体分析,同样在首段,一句话综述函数的功能,然后对函数的实现以1、2、3,或者a、b、c步骤的形式进行讲解。我只是有选择性的看,有时候对照着用source insight打开的源码,确认一下代码大体上确实是按书中所描述的步骤实现的,就当是增加感性认识。由于步骤中掺杂着各种针对不同实现目的安全性、有效性检查,如果不理解就先跳过。这并不妨碍你对函数体功能实现的整体把握。

    PLKA介于LKD3和ULK3之间。我觉得PLKA的作者(看照片,真一德国帅小伙,技术如此了得)肯定看过ULK,无论他的本意还是有意,总之PLKA还是跟ULK有所不同,对函数的仔细讲解都做补充说明,去掉函数体中边边角角的情况,比如一些特殊情况的处理,有效性检查等,而不妨碍对整个函数体功能的理解,这些他都有所交代,做了声明;而且,就像LKD3一样,在某些点上也给出了指导性编程建议。作者们甚至对同一个主要函数的讲解的着重点都不一样。这样的话,对我们学习的人而言,有助于加深理解。另外,我认为很重要的一点就是PLKA针对的2.6.24的内核版本,而ULK是2.6.11,LKD3是2.6.34。在某些方面PLKA比较接近现代的实现。其实作者们之所以分别选择11或者24,都是因为在版本发行树中,这两个版本在某些方面都做了不小的变动,或者说是具有标志性的转折点(这些信息大多是在书中的引言部分介绍的,具体的细节我想不起来了)。

    Intel V3,针对X86的CPU,本书自然是系统编程的权威。内核部分实现都可以在本书找到其根源。所以,在读以上三本书某个子系统的时候,不要忘记可以在V3中相应章节找到一些基础性支撑信息。

    在读书过程中,会产生相当多的疑问,这一点是确信无疑的。大到搞不明白一个设计思想,小到不理解某行代码的用途。各个方面,各种疑问,你完全可以把不理解的地方都记录下来(不过,我并没有这么做,没有把疑问全部记下来,只标记了很少一部分我认为很关键的几个问题),专门写到一张纸上,不对,一个本上,我确信会产生这么多的疑问,不然内核相关的论坛早就可以关闭了。其实,大部分的问题(其中很多问题都是你知道不知道有这么一回事的问题)都可以迎刃而解,只要你肯回头再看,书读百遍,其义自现。多看几遍,前前后后的联系明白个七七八八是没有问题的。我也这么做了,针对某些子系统也看了好几遍,切身体会。

    当你按顺序学习这些子系统的时候,前面的章节很可能会引用后面的章节,就像PLKA的作者说的那样,完全没有向后引用是不可能的,他能做的只是尽量减少这种引用而又不损害你对当前问题的理解。不理解,没关系,跳过就行了。后面的章节同样会有向前章节的引用,不过这个问题就简单一些了 ,你可以再回头去看相应的介绍,当时你不太理解的东西,很可能这个时候就知道了它的设计的目的以及具体的应用。不求甚解只是暂时的。比如说,内核各个子系统之间的交互和引用在代码中的体现就是实现函数穿插调用,比如你在内存管理章节学习了的内存分配和释放的函数,而你是了解内存在先的,在学习驱动或者模块的时候就会碰到这些函数的调用,这样也就比较容易接受,不至于太过茫然;再比如,你了解了系统时间和定时器的管理,再回头看中断和异常中bottom half的调度实现,你对它的理解就会加深一层。

    子系统进行管理工作需要大量的数据结构。子系统之间交互的一种方式就是各个子系统各自的主要数据结构通过指针成员相互引用。学习过程中,参考书上在讲解某个子系统的时候会对数据结构中主要成员的用途解释一下,但肯定不会覆盖全部(成员比较多的情况,例如task_struct),对其它子系统基于某个功能实现的引用可能解释了,也可能没做解释,还可能说这个变量在何处会做进一步说明。所以,不要纠结于一个不理解的点上,暂且放过,回头还可以看的。之间的联系可以在对各个子系统都有所了解之后再建立起来。其实,我仍然在强调先理解概念和框架的重要性。

    等我们完成了建立框架这一步,就可以选择一个比较感兴趣的子系统,比如驱动、网络,或者文件系统之类的。这个时候你再去深入了解底层代码实现,相较于一开始就钻研代码,更容易一些,而且碰到了不解之处,或者忘记了某个方面的实现,此时你完全可以找到相应的子系统,因为你知道在哪去找,查漏补缺,不仅完成了对当前函数的钻研,而且可以回顾、温习以前的内容,融会贯通的时机就在这里了。

    《深入理解linux虚拟内存》(2.4内核版本),LDD3,《深入理解linux网络技术内幕》,几乎每一个子系统都需要一本书的容量去讲解,所以说,刚开始学习不宜对某个模块太过深入,等对各个子系统都有所了解了,再有针对性的去学习一个特定的子系统。这时候对其它系统的援引都可以让我们不再感到茫然、复杂,不知所云。

    比如,LDD3中的以下所列章节:构造和运行模块,并发和竞态,时间、延迟及延缓操作,分配内存,中断处理等,都属于驱动开发的支撑性子系统,虽说本书对这些子系统都专门开辟一个章节进行讲解,但是详细程度怎么能比得上PLKA,ULK3,LKD3这三本书,看完这三本书,你会发现读LDD3这些章节的时候简直跟喝白开水一样,太随意了,因为LDD3的讲解比之LKD3更粗略。打好了基础,PCI、USB、TTY驱动,块设备驱动,网卡驱动,需要了解和学习的东西就比较有针对性了。这些子系统就属于通用子系统,了解之后,基于这些子系统的子系统的开发—驱动(需进一步针对硬件特性)和网络(需进一步理解各种协议)—相对而言,其学习难度大大降低,学习进度大大加快,学习效率大大提升。说着容易做来难。达到这样一种效果的前提就是:必须得静下心来,认真读书,要看得进去,PLKA,ULK3厚得都跟砖头块儿一样,令人望之生畏,如果没有兴趣,没有热情,没有毅力,无论如何都是不行,因为需要时间,需要很长时间。我并不是说必须打好了基础才可以进行驱动开发,只是说打好了基础的情况下进行开发会更轻松,更有效率,而且自己对内核代码的驾驭能力会更强大。这只是我个人见解,我自己的学习方式,仅供参考。

    语言

    PLKA是个德国人用德语写的,后来翻译成英文,又从英文翻译成中文,我在网上书店里没有找到它的纸质英文版,所以就买了中文版的。ULK3和LKD3都是英文版的。大牛们写的书,遣词造句真的是简洁,易懂,看原版对我们学习计算机编程的程序员来说完全不成问题,最好原汁原味。如果一本书确实翻译地很好,我们当然可以看中文版的,用母语进行学习,理解速度和学习进度当然是很快的,不作他想。看英文的时候不要脑子里想着把他翻译成中文,没必要。

    API感想

    “比起知道你所用技术的重要性,成为某一个特别领域的专家是不重要的。知道某一个具体API调用一点好处都没有,当你需要他的时候只要查询下就好了。”这句话源于我看到的一篇翻译过来的博客。我想强调的就是,这句话针应用型编程再合适不过,但是内核API就不完全如此。

    内核相当复杂,学习起来很不容易,但是当你学习到一定程度,你会发现,如果自己打算写内核代码,到最后要关注的仍然是API接口,只不过这些API绝大部分是跨平台的,满足可移植性。内核黑客基本上已经标准化、文档化了这些接口,你所要做的只是调用而已。当然,在使用的时候,最好对可移植性这一话题在内核中的编码约定烂熟于心,这样才会写出可移植性的代码。就像应用程序一样,可以使用开发商提供的动态库API,或者使用开源API。同样是调用API,不同点在于使用内核API要比使用应用API了解的东西要多出许多。

    当你了解了操作系统的实现—这些实现可都是对应用程序的基础性支撑啊—你再去写应用程序的时候,应用程序中用到的多线程,定时器,同步锁机制等等等等,使用共享库API的时候,联系到操作系统,从而把对该API的文档描述同自己所了解到的这些方面在内核中的相应支撑性实现结合起来进行考虑,这会指导你选择使用哪一个API接口,选出效率最高的实现方式。对系统编程颇有了解的话,对应用编程不无益处,甚至可以说是大有好处。

    设计实现的本质,知道还是理解

    操作系统是介于底层硬件和应用软件之间的接口,其各个子系统的实现很大程度上依赖于硬件特性。书上介绍这些子系统的设计和实现的时候,我们读过了,也就知道了,如果再深入考虑一下,为什么整体架构要按照这种方式组织,为什么局部函数要遵循这样的步骤处理,知其然,知其所以然,如果你知道了某个功能的实现是因为芯片就是这么设计的,CPU就是这么做的,那么你的疑问也就基本上到此为止了。再深究,就是芯片架构方面的设计与实现,对于程序员来讲,无论是系统还是应用程序员,足迹探究到这里,已经解决了很多疑问,因为我们的工作性质偏软,而这些东西实在是够硬。

    比如,ULK3中讲解的中断和异常的实现,究其根源,那是因为Intel x86系列就是这么设计的,去看看Intel V3手册中相应章节介绍,都可以为ULK3中描述的代码实现方式找到注解。还有时间和定时器管理,同样可以在Intel V3 对APIC的介绍中获取足够的信息,操作系统就是依据这些硬件特性来实现软件方法定义的。

    又是那句话,不是理解不理解的问题,而是知道不知道的问题。有时候,知道了,就理解了。在整个学习过程中,知道,理解,知道,理解,知道……,交叉反复。为什么开始和结尾都是知道,而理解只是中间步骤呢?世界上万事万物自有其规律,人类只是发现而已,实践是第一位的,实践就是知道的过程,实践产生经验,经验的总结就是理论,理论源于实践,理论才需要理解。我们学习内核,深入研究,搞来搞去,又回到了芯片上,芯片是物质的,芯片的功用基于自然界中物质本有的物理和电子特性。追本溯源,此之谓也。

    动手写代码

    纸上得来终觉浅,绝知此事要躬行。只看书是绝对不行的,一定要结合课本给出的编程建议自己敲代码。刚开始就以模块形式测试好了,或者自己编译一个开发版本的内核。一台机器的话,使用UML方式调试,内核控制路走到哪一步,单步调试看看程序执行过程,比书上的讲解更直观明了。一定要动手实际操作。

    参考书

    LDD3          Linux Device Driver 3rd

    LKD3          Linux Kernel Development 3rd

    ULK3          Understanding the Linux Kernel 3rd

    PLKA          Professional Linux Kernel Architecture

    UML            User Mode Linux

    Intel V3       Intel? 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide

    作者在写书的时候,都是以自己的理解组织内容,从自己的观点看待一个主题,关注点跟作者自身有很大的关系。出书的时间有先后,后来人针对同一个主题想要出书而又不落入窠臼,最好有自己的切入方式,从自己的角度讲解相关问题,这才值得出这本书,千篇一律是个掉价的行为,书就不值钱了。

    尽信书不如无书。        

    http://lwn.net/Articles/419855/ 此处是一篇关于LKD3的书评,指出了其中的错误,当你读完的时候,不妨去找找,看一下自己在其中所描述的地方有什么特别的印象。   

    http://lwn.net/Articles/161190/此处是一篇对ULK3的介绍,我认为其中很关键的几句话就可以给本书定位:    

    Many of the key control paths in the kernel are described, step by step;

    一步一步地讲述内核控制路径的实现。

    The level of detail sometimes makes it hard to get a sense for the big picture, but it does help somebody trying to figure out how a particular function works.

    对代码讲解的详细程度有时候很难让读者把握住它的主旨大意,但是确实有助于读者理解一个特定的函数到底是如何工作的。

    Indeed, that is perhaps the key feature which differentiates this book. It is very much a “how it works” book, designed to help people understand the code.

    事实上,这也正是本书与众不同的地方。更像一个“如何工作”的书,帮助读者理解代码实现。

    It presents kernel functions and data structures, steps the reader through them, but does not, for example, emphasize the rules for using them. UTLK is a study guide, not a programming manual.

    本书描述了内核函数和数据结构,引导读者穿行于其间,但是,并没有着重强调使用它们的法则。UTLK是一本学习指南,而不是编程手册。

    这几句话对本书的描述非常到位。基于此,作为指导性原则,我们就可以很有效率地使用它了。

    看一本技术书籍,书中的序言部分绝对是首先应该翻阅的,其次就是目录。我发现在阅读过程中我会频繁的查看目录,甚至是喜欢看目录。

    展开全文
  • linux内核结构电子书

    2008-08-21 19:52:11
    详细介绍了linux内核结构,喜欢linux的值得一看。
  • 基于 PCA架构的嵌入式Linux内核结构分析
  • Linux内核结构详解教程,经典的教程,pdf版
  • Linux内核0.11——内核体系结构

    千次阅读 2017-08-31 11:54:04
    Linux内核体系结构linux内核主要由五部分组成:进程调度模块、内存管理模块、文件系统模块、进程间通信模块、网络接口模块。如上图包括了各个部分的依赖关系,也大概表明了其在内核目录中的分布,其中进程调度模块是...

    Linux内核体系结构


    linux内核主要由五部分组成:进程调度模块、内存管理模块、文件系统模块、进程间通信模块、网络接口模块。

    这里写图片描述

    如上图包括了各个部分的依赖关系,也大概表明了其在内核目录中的分布,其中进程调度模块是比较重要的一部分,所有模块都和它存在着依赖关系,相当于一个计算机资源的总管家。


    内存管理和使用


    这一部分不太好理解,建议多看几遍书,这里说一些我对书上的理解。

    这里写图片描述

    从上面这个物理内存分布中不难看出,并不是所有物理内存都是可用的(对于普通用户和上层应用开发来说),这就是为什么我们买电脑的时候人家说是4G的运行内存,而实际上开机之后你能用的总是那么多,远远不足4G。
    所以说计算机的物理内存是远远不够用的,于是就产生了几种内存管理机制:分段系统、分页系统。分页是可选的,由系统编程人员决定,linux系统则是同时采用了这两种机制。

    内存地址空间概念


    想要搞懂linux内存管理,首先要弄清楚几个概念,不然那些什么虚拟地址、逻辑地址会把你搞得头昏。

    这里写图片描述

    • 虚拟地址:由段选择符和段内偏移地址组成,因为这两部分并没有直接指向物理内存,需要经过分段变换机制(具体后面会说)才对应到物理内存,所以叫虚拟地址。虚拟地址空间包括GDT全局地址空间和LDT局部地址空间,选择符占13个bit,还有1bit是区分GDT or LDT,所以一共有2的14次方16384个选择符,若每个段取最大4G寻址空间,则整个虚拟地址空间有4 x 16384 = 64G。
    • 逻辑地址:一般是指程序代码段限长内的偏移地址,一般程序员只需知道逻辑地址,因为分段分页机制对于他们是透明的,由系统编程人员管理,所以一些资料上对虚拟地址和逻辑地址不区分。
    • 线性地址:虚拟到物理地址之间变换的中间层,也就是逻辑地址加上段选择符就是线性地址,如果不经分页机制,就直接得到的线性地址就是物理地址。

    在linux0.11内核中,为每个进程分配了64M的虚拟内存空间,因此程序逻辑地址是0x0000000-0x4000000

    好,那为什么要搞这些乱七八糟的又分段又分页的机制呢,整个物理内存就那么多,还要被系统占用一部分,windows用户还会说经常有流氓软件在后台运行占用内存和网络流量时不时出现内存占用率达到90以上的情况。
    可是为什么我们还能在资源这么紧缺的情况下一边打撸一边看电影呢,答案就是虚拟内存系统。举个例子,你从北京到上海不需要很长的铁轨就能顺利完成任务,比如我们可以将后面的铁轨铺到前面,只要你够快够准。学过操作系统的应该会更了解虚拟内存机制(其实就是在进程暂时不用内存的时候给其他进程用,用一会再还回来嘛,或者再找别的进程借。),这个虚拟内存机制充分利用了CPU的地址总线,就拿上面的段来说,一个段最大4G空间,所以总的虚拟内存空间会比实际物理地址空间要大很多。

    当然了真正的实现没这么简单,只是一个比喻。


    分段机制

    • 实时模式下寻址一个内存地址用一个段值加上一个段内偏移,段值存放在段寄存器(如ds),段长固定为64KB,偏移值存放在任一个可用于寻址的寄存器(如si),通过这两个寄存器的值就可以计算处实际地址
    • 保护模式下段寄存器存放的不再是段基址,而是一个段描述符表中某一描述符项在表中的索引值,该描述符项中包含段基址、段长(可变,定义最大长度4G)、段的访问特权级别等。

      这里写图片描述

    保存描述符项的描述符表有三种类型:

    • GDT全局描述符表
    • IDT 中断描述符表
    • LDT局部描述符表

    GDT和LDT构成了整个虚拟地址空间,为了能够寻址这三个表,在gdtr、idtr、ldtr三个寄存器中分别存放了表基址和限长。

    这里写图片描述


    分页机制


    上面说过线性地址如果不经过分页转换,那得到的就是物理地址,下面这张图告诉我们如果采用分页机制,线性地址是怎么转换的:

    这里写图片描述

    CR3保存页目录表基址在物理内存中的基址

    内存分页管理机制主要原理就是,CPU将整个线性内存地址分为4096个字节为一页的内存页面,其实现机制从上图可以看出和分段机制类似,但没那么完善。在80x86体系结构中如果采用分页机制,需要将CR0的高位也就是bit31置位。
    在80386中采用了分页管理,每个页目录表项或页表项格式基本相同都占4个字节,每个页目录表或页表必须只能包含1024个页表项,因此一个页目录表项或页表项占用4 x 1024个字节,也就是一个页面。
    换句话说,一个页表项可以映射一个页面(也就是4096个字节=4KB),一个页表呢包含1024个表项,一个页目录表项包含1024个表项,就是说一个页目录表可以映射1024x1024x4KB=4GB的线性地址,也就是整个线性地址就可以用一个页目录表项映射了,神奇吧。

    由于linux0.11中内核和所有任务都共用一个页目录表,所以所有的映射函数都是一样的,为了防止不互相干扰,二者必须从虚拟地址映射到线性地址的不同位置。

    在0.11中每个进程最大可用虚拟地址空间为64MB,全局描述符表有256个表项,2项空闲2项系统使用,每个进程使用2项,也就是此时系统最多容纳(256-4)/2=126个任务,虚拟地址范围126x64MB=8GB。但0.11中人工定义的最大任务数是64个,所以全部线性地址空间为64x64MB=4GB。

    篇幅限制就先把分段分页的机制的理解写在这里,后面还有一些内存管理的知识需要大家自己多看一下,这些基本的机制搞懂了后面的应用扩展就没什么问题了。


    中断机制


    当设备向处理器发出服务请求时(中断请求IRQ),处理器在结束当前指令后会立即应答并转向设备的相关服务程序(中断服务程序ISR),这个服务程序执行完成后会立即返回之前被中断的服务,这个方式称为中断方法。

    对于linux内核来说,中断信号分为:软件中断(异常)、硬件中断,每个中断由0-255之间的数字标识,对于int0-int31(0x00-0x1f)是由Intel公司固定设定或保留使用的,属于软中断,Intel称为异常(通常还分为故障fault和陷阱traps)。剩下的int32-int255(0x20-0xff)可由用户自己设定。

    这里写图片描述

    在BIOS执行初始化操作时会通过硬件设置中断向量(4字节)来提供中断服务,而后在系统引导加载内核时还会通过设置中断向量表来重新设置中断向量。对于linux会在加载内核之前的setup.s中重新初始化硬件,在head.s中设置一张中断向量表(中断描述符表IDT),完全抛弃了BIOS的中断设置。

    如果中断的概念还不知道的话,一定要好好了解一下,很重要


    Linux的系统调用


    系统调用接口

    系统调用(syscalls)是内核与上层应用通信的唯一接口。从中断机制中可知,用户程序通过调用中断int 0x80,并在eax寄存器中指定系统调用功能号,就可以使用内核和系统硬件资源了。不过一般应用程序都是通过C库函数间接使用内核系统调用。比如:

    这里写图片描述

    而这些系统调用功能号定义在include/unistd.h中,并且与include/linux/sys.h中定义的系统调用指针数组表sys中的sys_call_table[]的索引值一一对应。
    下面列出部分源码,可以看出所以内核系统调用处理函数都是以”sys_”开头的。

    /* 
     * /include/unistd.h
     */
    #define __NR_setup  0   /* used only by init, to get system going */
    #define __NR_exit   1
    #define __NR_fork   2
    #define __NR_read   3
    #define __NR_write  4
    #define __NR_open   5
    #define __NR_close  6
    #define __NR_waitpid    7
    #define __NR_creat  8
    #define __NR_link   9
    #define __NR_unlink 10
    #define __NR_execve 11
    #define __NR_chdir  12
    #define __NR_time   13
    #define __NR_mknod  14
    #define __NR_chmod  15
    #define __NR_chown  16
    #define __NR_break  17
    #define __NR_stat   18
    #define __NR_lseek  19
    #define __NR_getpid 20
    ...
    ...
    
    /*
     * /include/linux/sys.h
     */
    extern int sys_setup();
    extern int sys_exit();
    extern int sys_fork();
    extern int sys_read();
    extern int sys_write();
    extern int sys_open();
    extern int sys_close();
    extern int sys_waitpid();
    extern int sys_creat();
    extern int sys_link();
    extern int sys_unlink();
    extern int sys_execve();
    extern int sys_chdir();
    extern int sys_time();
    extern int sys_mknod();
    extern int sys_chmod();
    extern int sys_chown();
    extern int sys_break();
    extern int sys_stat();
    extern int sys_lseek();
    ...
    ...
    fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
    sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
    sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
    sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
    sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
    sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
    sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
    sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
    sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
    sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
    sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
    sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
    sys_setreuid,sys_setregid };
    

    系统调用处理过程

    当应用程序通过库函数向内核发送一个中断请求int 0x80时,就开始执行一个系统调用,系统调用号在寄存器eax中,参数在ebx、ecx、edx中(最多三个),处理中断过程的程序是kernel/system_call.s中的system_call

    _system_call:
        cmpl $nr_system_calls-1,%eax
        ja bad_sys_call
        push %ds
        push %es
        push %fs
        pushl %edx
        pushl %ecx      # push %ebx,%ecx,%edx as parameters
        pushl %ebx      # to the system call
        movl $0x10,%edx        # set up ds,es to kernel space
        mov %dx,%ds
        mov %dx,%es
        movl $0x17,%edx        # fs points to local data space
        mov %dx,%fs
    
        #每项4字节,因此调用地址位_sys_call_table+%eax*4得到被调用处理函数的地址
        call _sys_call_table(,%eax,4)
        pushl %eax
        movl _current,%eax
        cmpl $0,state(%eax)        # state
        jne reschedule
        cmpl $0,counter(%eax)      # counter
        je reschedule
    

    内核在unistd.h中定义了宏函数_syscalln(),n代表参数个数,每个系统调用的宏都有2+n*2个参数,第一个是返回类型,第二个是系统调用名,后面即是参数。

    #define _syscall3(type,name,atype,a,btype,b,ctype,c) \
    type name(atype a,btype b,ctype c) \
    { \
    long __res; \
    __asm__ volatile ("int $0x80" \
        : "=a" (__res) \
        : "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
    if (__res>=0) \
        return (type) __res; \
    errno=-__res; \
    return -1; \
    }
    

    例如我们在程序中可以直接调用_syscall3(int, read, int, fd, char*, buf, int, n)而不通过库函数做中介,展开就是:

    int read(int fd, char *buf, int n)
    {
        long __res;
        __asm__volatile(
            "int $0x80"
            : "a" (__res)
            : "0" (__NR_read), "b"((long)(fd)), "c" ((long)(buf)), "d" ((long)(n));
        if (__res>0)
            return int __res;
        errno -__res;
        return -1;
    }

    嵌入汇编语句以功能号__NR_read(3)执行linux系统中断调用0x80,该中断调用在eax(__res)寄存器中返回实际读取字节数,若出错将出错号存入全局变量errno中并返回-1。


    Linux进程控制


    进程(process)是一个执行中的程序实例。

    任务数据结构

    内核中的进程称为任务task,用户空间的程序称为进程。

    内核一直在维护一个进程表对进程进行管理,linux中是一个task_struct任务结构指针(include/linux/sched.h),一般称为进程控制块PCB或进程描述符PD。主要保存进程当前运行的状态信息、信号、进程号、父进程号、运行时间、正在使用的文件、本任务的局部描述符、任务状态段信息(之前提到过的TSS)。

    struct task_struct {
    /* these are hardcoded - don't touch */
        long state; /* -1 unrunnable, 0 runnable, >0 stopped */
        long counter;
        long priority;
        long signal;
        struct sigaction sigaction[32];
        long blocked;   /* bitmap of masked signals */
    /* various fields */
        int exit_code;
        unsigned long start_code,end_code,end_data,brk,start_stack;
        long pid,father,pgrp,session,leader;
        unsigned short uid,euid,suid;
        unsigned short gid,egid,sgid;
        long alarm;
        long utime,stime,cutime,cstime,start_time;
        unsigned short used_math;
    /* file system info */
        int tty;        /* -1 if no tty, so it must be signed */
        unsigned short umask;
        struct m_inode * pwd;
        struct m_inode * root;
        struct m_inode * executable;
        unsigned long close_on_exec;
        struct file * filp[NR_OPEN];
    /* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
        struct desc_struct ldt[3];
    /* tss for this task */
        struct tss_struct tss;
    };

    当一个进程正在执行时,CPU中所有寄存器值、进程状态、堆栈中内容称为进程上下文。当内核需要切换进程时就需要保存当前进程的上下文以便恢复,linux中进程上下文就保存在这个任务数据结构中。

    进程运行状态

    进程的生存期内的进程状态如下,进程状态保存在任务结构中的state字段中

    这里写图片描述

    僵死状态:当进程已经停止运行,但其父进程还没调用wait()询问其状态时,该进程处于僵死状态,为了父进程还能获取其信息,此时子进程还需保留着任务数据结构

    进程初始化


    Linux系统中堆栈的使用


    • 系统引导初始化时临时使用的堆栈;
    • 进入保护模式后提供内核程序初始化使用的堆栈;
    • 每个任务通过系统调用执行内核程序时使用的堆栈,称为任务的内核态堆栈;
    • 任务在用户态执行的堆栈,位于任务逻辑地址空间末端。

    初始化阶段

    • 开机初始化(bootsect.s, setup.s)
      bootsect代码被ROM BIOS引导加载到物理内存0x7c00时并没有设置堆栈段,直到boostsect被移动到0x9000:0才将堆栈寄存器SS设置为0x9000,堆栈指针寄存器为0xff00,所有堆栈顶在0x9000:0xff00
    entry start
    start:
        mov ax,#BOOTSEG
        mov ds,ax
        mov ax,#INITSEG
        mov es,ax
        mov cx,#256
        sub si,si
        sub di,di
        rep
        movw
        jmpi    go,INITSEG
    go: mov ax,cs
        mov ds,ax
        mov es,ax
    ! put stack at 0x9ff00.
        mov ss,ax
        mov sp,#0xFF00      ! arbitrary value >>512
    
    ! load the setup-sectors directly after the bootblock.
    ! Note that 'es' is already set up.
    
    • 进入保护模式时(head.s)
      自此进入保护模式,此时堆栈段设置为内核数据段0x10,esp指向user_stack数组顶端,保留一页内存(4K)作为堆栈使用
      ——-未完

    linux-0.11文件系统

    内核的运行肯定离不开文件系统的支持,对于前面介绍的linux0.11就包含了两个映像文件:bootimage、rootimage。一个启动引导文件包含引导扇区代码、操作系统加载程序和内核执行代码,另一个则是根文件系统的映像,这两个合起来就是一个系统盘了。
    是linux文件系统一般是extn(n=2,3,4),其中包括操作系统的一些规定目录、配置文件、设备驱动、等等,具体就是根目录下的那些子目录,可以看一下linux根目录主要结构。


    linux-0.11内核目录结构

    这里写图片描述

    内核系统与应用程序的关系

    大家应该都见过这几个词:API、系统调用、库函数
    首先API即所谓的库函数,而系统调用是内核与外界接口的最高层,在内核中每个系统调用都由一个宏来标识(前面提到过),一个库函数可能与一个或多个系统调用(比如sys_open)对应来完成相应的操作(比如fopen),虽然系统调用会比库函数效率高,但是库函数具有移植性,而且还有较好的容错能力。
    比如说POSIX标准,对于它来说无论操作系统提供的系统调用多不同,只要遵循这个API标准,他就是可移植的。

    Makefile

    这里写图片描述
    详细注释就不在这里叙述了。

    展开全文
  • Linux网络体系结构 Linux内核中网络协议的设计与实现,Linux网络体系结构 Linux内核中网络协议的设计与实现
  • 编写本书是为了向学生和专业人员提供在Linux内核中实现网络功能时所需的基础知识,本书也适合所有希望深入理解操作系统内部网络特定进程的人。本书介绍了Linux内核的关键网络组件及机制,同时也介绍了通信系统的设计...
  • 十 使用SkyEye构建Linux内核调试环境 1 SkyEye的安装和μcLinux内核编译 2 使用SkyEye调试 3 使用SkyEye调试内核的特点和不足 十一 KDB 1 入门 2 初始化并设置环境变量 3 激活 KDB 4 KDB 命令 5 技巧和诀窍 6 结束语...
  • Linux内核结构与编程

    2010-03-21 01:19:04
    详细介绍LINUX系统内核结构与利用C语言分析内核编程
  • Linux内核数据结构

    千次阅读 2019-03-19 09:50:23
    内核数据结构贯穿于整个内核代码中,这里介绍4个基本的内核数据结构。...链表是linux内核中最简单,同时也是应用最广泛的数据结构。 内核中定义的是双向链表。 头文件简介 内核中关于链表定义的代码位于: includ...

    本文转自: https://www.cnblogs.com/wang_yb/archive/2013/04/16/3023892.html

    内核数据结构贯穿于整个内核代码中,这里介绍4个基本的内核数据结构。

    主要内容:

    • 链表
    • 队列
    • 映射
    • 红黑树

    链表

    链表是linux内核中最简单,同时也是应用最广泛的数据结构。

    内核中定义的是双向链表。

    头文件简介
    内核中关于链表定义的代码位于: include/linux/list.h

    list.h 文件中对每个函数都有注释,这里就不详细说了。

    其实刚开始只要先了解一个常用的链表操作(追加,删除,遍历)的实现方法,

    其他方法基本都是基于这些常用操作的。

    链表代码的注意点
    在阅读 list.h 文件之前,有一点必须注意:linux内核中的链表使用方法和一般数据结构中定义的链表是有所不同的。

    一般的双向链表一般是如下的结构,

    • 有个单独的头结点(head)
    • 每个节点(node)除了包含必要的数据之外,还有2个指针(pre,next)
    • pre指针指向前一个节点(node),next指针指向后一个节点(node)
    • 头结点(head)的pre指针指向链表的最后一个节点
    • 最后一个节点的next指针指向头结点(head)

    具体见下图:
    在这里插入图片描述
    传统的链表有个最大的缺点就是不好共通化,因为每个node中的data1,data2等等都是不确定的(无论是个数还是类型)。

    linux中的链表巧妙的解决了这个问题,linux的链表不是将用户数据保存在链表节点中,而是将链表节点保存在用户数据中。

    linux的链表节点只有2个指针(pre和next),这样的话,链表的节点将独立于用户数据之外,便于实现链表的共同操作。

    具体见下图:
    在这里插入图片描述

    整个list.h文件中,我觉得最复杂的代码就是获取用户数据的宏定义

    #define list_entry(ptr, type, member) \
        container_of(ptr, type, member)
    

    这个宏没什么特别的,主要是container_of这个宏

    #define container_of(ptr, type, member) ({          \
        const typeof(((type *)0)->member)*__mptr = (ptr);    \
                 (type *)((char *)__mptr - offsetof(type, member)); })
    

    这里面的type一般是个结构体,也就是包含用户数据和链表节点的结构体。

    ptr是指向type中链表节点的指针

    member则是type中定义链表节点是用的名字

    比如:

    struct student
    {
        int id;
        char* name;
        struct list_head list;
    };
    
    • type 是 struct student ptr 是指向 stuct
    • list 的指针,也就是指向 member 类型的指针
    • member 就是 list

    下面分析一下container_of宏:

    // 步骤1:将数字0强制转型为type*,然后取得其中的member元素
    ((type *)0)->member  // 相当于((struct student *)0)->list
    
    // 步骤2:定义一个临时变量__mptr,并将其也指向ptr所指向的链表节点
    const typeof(((type *)0)->member)*__mptr = (ptr);
    
    // 步骤3:计算member字段距离type中第一个字段的距离,也就是type地址和member地址之间的差
    // offset(type, member)也是一个宏,定义如下:
    #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
    
    // 步骤4:将__mptr的地址 - type地址和member地址之间的差
    // 其实也就是获取type的地址
    

    步骤1,2,4比较容易理解,下面的图以sturct student为例进行说明步骤3:

    首先需要知道 ((TYPE *)0) 表示将地址0转换为 TYPE 类型的地址

    由于TYPE的地址是0,所以((TYPE *)0)->MEMBER 也就是 MEMBER的地址和TYPE地址的差,如下图所示:

    在这里插入图片描述
    使用示例
    构造了一个内核模块来实际使用一下内核中的链表,代码在CentOS6.3 x64上运行通过。

    C代码:

    #include<linux/init.h>
    #include<linux/slab.h>
    #include<linux/module.h>
    #include<linux/kernel.h>
    #include<linux/list.h>
    
    MODULE_LICENSE("Dual BSD/GPL");
    struct student
    {
        int id;
        char* name;
        struct list_head list;
    };
    
    void print_student(struct student*);
    
    static int testlist_init(void)
    {
        struct student *stu1, *stu2, *stu3, *stu4;
        struct student *stu;
        
        // init a list head
        LIST_HEAD(stu_head);
    
        // init four list nodes
        stu1 = kmalloc(sizeof(*stu1), GFP_KERNEL);
        stu1->id = 1;
        stu1->name = "wyb";
        INIT_LIST_HEAD(&stu1->list);
    
        stu2 = kmalloc(sizeof(*stu2), GFP_KERNEL);
        stu2->id = 2;
        stu2->name = "wyb2";
        INIT_LIST_HEAD(&stu2->list);
    
        stu3 = kmalloc(sizeof(*stu3), GFP_KERNEL);
        stu3->id = 3;
        stu3->name = "wyb3";
        INIT_LIST_HEAD(&stu3->list);
    
        stu4 = kmalloc(sizeof(*stu4), GFP_KERNEL);
        stu4->id = 4;
        stu4->name = "wyb4";
        INIT_LIST_HEAD(&stu4->list);
    
        // add the four nodes to head
        list_add (&stu1->list, &stu_head);
        list_add (&stu2->list, &stu_head);
        list_add (&stu3->list, &stu_head);
        list_add (&stu4->list, &stu_head);
    
        // print each student from 4 to 1
        list_for_each_entry(stu, &stu_head, list)
        {
            print_student(stu);
        }
        // print each student from 1 to 4
        list_for_each_entry_reverse(stu, &stu_head, list)
        {
            print_student(stu);
        }
    
        // delete a entry stu2
        list_del(&stu2->list);
        list_for_each_entry(stu, &stu_head, list)
        {
            print_student(stu);
        }
    
        // replace stu3 with stu2
        list_replace(&stu3->list, &stu2->list);
        list_for_each_entry(stu, &stu_head, list)
        {
            print_student(stu);
        }
    
        return 0;
    }
    
    static void testlist_exit(void)
    {
        printk(KERN_ALERT "*************************\n");
        printk(KERN_ALERT "testlist is exited!\n");
        printk(KERN_ALERT "*************************\n");
    }
    
    void print_student(struct student *stu)
    {
        printk (KERN_ALERT "======================\n");
        printk (KERN_ALERT "id  =%d\n", stu->id);
        printk (KERN_ALERT "name=%s\n", stu->name);
        printk (KERN_ALERT "======================\n");
    }
    
    module_init(testlist_init);
    module_exit(testlist_exit);
    

    Makefile:

    obj-m += testlist.o
    
    #generate the path
    CURRENT_PATH:=$(shell pwd)
    #the current kernel version number
    LINUX_KERNEL:=$(shell uname -r)
    #the absolute path
    LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
    #complie object
    all:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
        rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
    #clean
    clean:
        rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
    

    安装,卸载内核模块以及查看内核模块的运行结果:

    insmod testlist.ko
    rmmod testlist
    dmesg | tail -100
    

    队列

    内核中的队列是以字节形式保存数据的,所以获取数据的时候,需要知道数据的大小。

    如果从队列中取得数据时指定的大小不对的话,取得数据会不完整或过大。

    头文件简介
    内核中关于队列定义的头文件位于:<linux/kfifo.h> include/linux/kfifo.h

    头文件中定义的函数的实现位于:kernel/kfifo.c

    队列代码的注意点
    内核队列编程需要注意的是:

    1. 队列的size在初始化时,始终设定为2的n次方
    2. 使用队列之前将队列结构体中的锁(spinlock)释放

    使用示例
    构造了一个内核模块来实际使用一下内核中的队列,代码在CentOS6.3 x64上运行通过。

    C代码:

    #include "kn_common.h"
    
    MODULE_LICENSE("Dual BSD/GPL");
    struct student
    {
        int id;
        char* name;
    };
    
    static void print_student(struct student*);
    
    static int testkfifo_init(void)
    {
        struct kfifo *fifo;
        struct student *stu1, *stu2, *stu3, *stu4;
        struct student *stu_tmp;
        char* c_tmp;
        int i;
        // !!importent  init a unlocked lock
        spinlock_t sl = SPIN_LOCK_UNLOCKED;
    
        // init kfifo
        fifo = kfifo_alloc(4*sizeof(struct student), GFP_KERNEL, &sl);
        
        stu1 = kmalloc(sizeof(struct student), GFP_KERNEL);
        stu1->id = 1;
        stu1->name = "wyb1";
        kfifo_put(fifo, (char *)stu1, sizeof(struct student));
    
        stu2 = kmalloc(sizeof(struct student), GFP_KERNEL);
        stu2->id = 1;
        stu2->name = "wyb2";
        kfifo_put(fifo, (char *)stu2, sizeof(struct student));
    
        stu3 = kmalloc(sizeof(struct student), GFP_KERNEL);
        stu3->id = 1;
        stu3->name = "wyb3";
        kfifo_put(fifo, (char *)stu3, sizeof(struct student));
    
        stu4 = kmalloc(sizeof(struct student), GFP_KERNEL);
        stu4->id = 1;
        stu4->name = "wyb4";
        kfifo_put(fifo, (char *)stu4, sizeof(struct student));
    
        c_tmp = kmalloc(sizeof(struct student), GFP_KERNEL);
        printk(KERN_ALERT "current fifo length is : %d\n", kfifo_len(fifo));
        for (i=0; i < 4; i++) {
    
            kfifo_get(fifo, c_tmp, sizeof(struct student));
            stu_tmp = (struct student *)c_tmp;
            print_student(stu_tmp);
            printk(KERN_ALERT "current fifo length is : %d\n", kfifo_len(fifo));
        }
        
        printk(KERN_ALERT "current fifo length is : %d\n", kfifo_len(fifo));
        kfifo_free(fifo);
        kfree(c_tmp);
        return 0;
    }
    
    static void print_student(struct student *stu)
    {
        printk(KERN_ALERT "=========================\n");
        print_current_time(1);
        printk(KERN_ALERT "id = %d\n", stu->id);
        printk(KERN_ALERT "name = %s\n", stu->name);
        printk(KERN_ALERT "=========================\n");
    }
    
    static void testkfifo_exit(void)
    {
        printk(KERN_ALERT "*************************\n");
        printk(KERN_ALERT "testkfifo is exited!\n");
        printk(KERN_ALERT "*************************\n");
    }
    
    module_init(testkfifo_init);
    module_exit(testkfifo_exit);
    

    其中引用的kn_common.h文件:

    #include<linux/init.h>
    #include<linux/slab.h>
    #include<linux/module.h>
    #include<linux/kernel.h>
    #include<linux/kfifo.h>
    #include<linux/time.h>
    
    void print_current_time(int);
    

    kn_common.h 对应的 kn_common.c:

    #include "kn_common.h"
    
    void print_current_time(int is_new_line)
    {
        struct timeval *tv;
        struct tm *t;
        tv = kmalloc(sizeof(struct timeval), GFP_KERNEL);
        t = kmalloc(sizeof(struct tm), GFP_KERNEL);
    
        do_gettimeofday(tv);
        time_to_tm(tv->tv_sec, 0, t);
    
        printk(KERN_ALERT "%ld-%d-%d %d:%d:%d",
               t->tm_year + 1900,
               t->tm_mon + 1,
               t->tm_mday,
               (t->tm_hour + 8) % 24,
               t->tm_min,
               t->tm_sec);
    
        if (is_new_line == 1)
            printk(KERN_ALERT "\n");
        
        kfree(tv);
        kfree(t);
    }
    

    Makefile:

    obj-m += fifo.o
    fifo-objs := testkfifo.o kn_common.o
    
    #generate the path
    CURRENT_PATH:=$(shell pwd)
    #the current kernel version number
    LINUX_KERNEL:=$(shell uname -r)
    #the absolute path
    LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
    #complie object
    all:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
        rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
    #clean
    clean:
        rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
    

    映射

    映射的有点想其他语言(C#或者python)中的字典类型,每个唯一的id对应一个自定义的数据结构。

    头文件简介
    内核中关于映射定义的头文件位于:<linux/idr.h> include/linux/idr.h

    头文件中定义的函数的实现位于:lib/idr.c

    映射代码的注意点
    映射的使用需要注意的是,给自定义的数据结构申请一个id的时候,不能直接申请id,先要分配id(函数idr_pre_get),分配成功后,在获取一个id(函数idr_get_new)。

    idr的结构比较复杂,我也没有很好的理解,但是csdn上有篇介绍linux idr结构的博客写的挺好,图文并茂:http://blog.csdn.net/paomadi/article/details/8539794

    使用示例
    构造了一个内核模块来实际使用一下内核中的映射,代码在CentOS6.3 x64上运行通过。

    C代码:

    #include<linux/idr.h>
    #include "kn_common.h"
    
    MODULE_LICENSE("Dual BSD/GPL");
    struct student
    {
        int id;
        char* name;
    };
    
    static int print_student(int, void*, void*);
    
    static int testidr_init(void)
    {
        DEFINE_IDR(idp);
        struct student *stu[4];
        //    struct student *stu_tmp;
        int id, ret, i;
    
        // init 4 struct student
        for (i=0; i<4; i++) {
    
            stu[i] = kmalloc(sizeof(struct student), GFP_KERNEL);
            stu[i]->id = i;
            stu[i]->name = "wyb";
        }
    
        // add 4 student to idr
        print_current_time(0);
        for (i=0; i < 4; i++) {
    
            do {
                if (!idr_pre_get(&idp, GFP_KERNEL))
                    return -ENOSPC;
                ret = idr_get_new(&idp, stu[i], &id);
                printk(KERN_ALERT "id=%d\n", id);
            } while(ret == -EAGAIN);
        }
    
        // display all student in idr
        idr_for_each(&idp, print_student, NULL);
    
        idr_destroy(&idp);
        kfree(stu[0]);
        kfree(stu[1]);
        kfree(stu[2]);
        kfree(stu[3]);
        return 0;
    }
    
    static int print_student(int id, void *p, void *data)
    {
        struct student* stu = p;
           
        printk(KERN_ALERT "=========================\n");
        print_current_time(0);
        printk(KERN_ALERT "id = %d\n", stu->id);
        printk(KERN_ALERT "name = %s\n", stu->name);
        printk(KERN_ALERT "=========================\n");
    
        return 0;
    }
    
    static void testidr_exit(void)
    {
        printk(KERN_ALERT "*************************\n");
        print_current_time(0);
        printk(KERN_ALERT "testidr is exited!\n");
        printk(KERN_ALERT "*************************\n");
    }
    
    module_init(testidr_init);
    module_exit(testidr_exit);
    

    注:其中用到的kn_common.h和kn_common.c文件与队列的示例中一样。

    Makefile:

    obj-m += idr.o
    idr-objs := testidr.o kn_common.o
    
    #generate the path
    CURRENT_PATH:=$(shell pwd)
    #the current kernel version number
    LINUX_KERNEL:=$(shell uname -r)
    #the absolute path
    LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
    #complie object
    all:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
        rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
    #clean
    clean:
        rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
    

    安装,卸载内核模块以及查看内核模块的运行结果:

    insmod idr.ko
    rmmod idr
    dmesg | tail -30
    

    红黑树

    红黑树由于节点颜色的特性,保证其是一种自平衡的二叉搜索树。

    红黑树的一系列规则虽然实现起来比较复杂,但是遵循起来却比较简单,而且红黑树的插入,删除性能也还不错。

    所以红黑树在内核中的应用非常广泛,掌握好红黑树,即有利于阅读内核源码,也可以在自己的代码中借鉴这种数据结构。

    红黑树必须满足的规则:

    • 所有节点都有颜色,要么红色,要么黑色
    • 根节点是黑色,所有叶子节点也是黑色
    • 叶子节点中不包含数据
    • 非叶子节点都有2个子节点
    • 如果一个节点是红色,那么它的父节点和子节点都是黑色的
    • 从任何一个节点开始,到其下叶子节点的路径中都包含相同数目的黑节点

    红黑树中最长的路径就是红黑交替的路径,最短的路径是全黑节点的路径,再加上根节点和叶子节点都是黑色,从而可以保证红黑树中最长路径的长度不会超过最短路径的2倍。

    头文件简介
    内核中关于红黑树定义的头文件位于:<linux/rbtree.h> include/linux/rbtree.h

    头文件中定义的函数的实现位于:lib/rbtree.c

    红黑树代码的注意点
    内核中红黑树的使用和链表(list)有些类似,是将红黑树的节点放入自定义的数据结构中来使用的。

    首先需要注意的一点是红黑树节点的定义:

    struct rb_node
    {
        unsigned long  rb_parent_color;
    #define    RB_RED        0
    #define    RB_BLACK    1
        struct rb_node *rb_right;
        struct rb_node *rb_left;
    } __attribute__((aligned(sizeof(long))));
    

    刚开始看到这个定义的时候,我觉得很奇怪,等到看懂了之后,才知道原来作者巧妙的利用内存对齐来将2个内容存入到一个字段中(不服不行啊_!)。

    字段 rb_parent_color 中保存了2个信息:

    1. 父节点的地址
    2. 本节点的颜色

    这2个信息是如何存入一个字段的呢?主要在于 __attribute__((aligned(sizeof(long))));

    这行代码的意思就是 struct rb_node 在内存中的地址需要按照4 bytes或者8 bytes对齐。

    注:sizeof(long) 在32bit系统中是4 bytes,在64bit系统中是8 bytes。

    struct rb_node的地址按4 bytes对齐,意味着分配的地址都是4的倍数。

    4 的二进制为 100 ,所以申请分配的 struct rb_node 的地址的最后2位始终是零,

    struct rb_node 的字段 rb_parent_color 就是利用最后一位来保存节点的颜色信息的。

    明白了这点之后,rb_tree.h 中很多宏的定义也就很好懂了。

    /* rb_parent_color 保存了父节点的地址和本节点的颜色 */
    
    /* 将 rb_parent_color 的最后2位置成0,即将颜色信息去掉,剩下的就是parent节点的地址 */
    #define rb_parent(r)   ((struct rb_node *)((r)->rb_parent_color & ~3))
    
    /* 取得 rb_parent_color 二进制表示的最后一位,即用于保存颜色信息的那一位 */
    #define rb_color(r)   ((r)->rb_parent_color & 1)
    
    /* 将 rb_parent_color 二进制表示的最后一位置为0,即置为红色 */
    #define rb_set_red(r)  do { (r)->rb_parent_color &= ~1; } while (0)
    
    /* 将 rb_parent_color 二进制表示的最后一位置为1,即置为黑色 */
    #define rb_set_black(r)  do { (r)->rb_parent_color |= 1; } while (0)
    

    还有需要重点看的就是rb_tree.c中的5个函数,下面对这5个函数进行一些注释:

    函数1:左旋操作,当右子树的长度过大导致树不平衡时,进行左旋操作

    /*
     *  左旋操作其实就3个动作:见图left
     *  1. node的右子树关联到right的左子树
     *  2. right的左子树关联到node
     *  3. right取代node的位置
     *  其他带代码都是一些相应的parent指针的变化
     */
    static void __rb_rotate_left(struct rb_node *node, struct rb_root *root)
    {
        /* 初始化相对于node节点的父节点(图中的P)和右节点(图中的R) */
        struct rb_node *right = node->rb_right;
        struct rb_node *parent = rb_parent(node);
    
        /* 步骤1  */
        if ((node->rb_right = right->rb_left))
            rb_set_parent(right->rb_left, node);
    
        /* 步骤2 */
        right->rb_left = node;
        rb_set_parent(right, parent);
    
        /* node的parent NOT NULL 时,right取代原先的node的位置 */
        if (parent)
        {
            if (node == parent->rb_left)
                parent->rb_left = right;
            else
                parent->rb_right = right;
        }
    
        /* node的parent NULL 时,说明node原先时root节点,将新的root指向root即可 */
        else
            root->rb_node = right;
        rb_set_parent(node, right);
    }
    

    左旋操作图解:
    在这里插入图片描述
    函数2:右旋操作,和左旋操作类似。

    函数3:追加节点后,设置此节点的颜色。

    /*
     *  本函数没有插入节点的功能,只是在插入新节点后,设置新节点的颜色,从而保证红黑树的平衡性。
     *  新插入的节点默认都是红色的。
     *  
     *  下面的代码看着复杂,其实只要时时记住红黑树的几个重要特性,就会发现下面的都是在尽量保持住红黑树的这些特性。
     *  1. 无论从哪个节点开始,到其叶子节点的路径中包含的黑色节点个数时一样的
     *  2. 不能有连续的2个红色节点,即父节点和子节点不能同时为红色
     *  所以最简单的情况就是:插入节点的父节点是黑色的。那么插入一个红节点后不会有任何影响。
     *  3. 左旋操作有减少右子树高度的作用
     *  4. 同理,右旋操作有减少左子树高度的作用
     */
    void rb_insert_color(struct rb_node *node, struct rb_root *root)
    {
        struct rb_node *parent, *gparent;
    
        while ((parent = rb_parent(node)) && rb_is_red(parent))
        {
            gparent = rb_parent(parent);
    
            /* parent 是 gparent的左子树时 */
            if (parent == gparent->rb_left)
            {
                {
                    /* gparent的左右子树的黑色节点都增加一个,仍然平衡 */
                    register struct rb_node *uncle = gparent->rb_right;
                    if (uncle && rb_is_red(uncle))
                    {
                        rb_set_black(uncle);
                        rb_set_black(parent);
                        rb_set_red(gparent);
                        node = gparent;
                        continue;
                    }
                }
    
                /* node为parent右子树时 */
                if (parent->rb_right == node)
                {
                    register struct rb_node *tmp;
                    /* 左旋后,parent的位置被node取代,然后再交换parent和node的位置,
                     * 相当于node是parent的左子树
                     * 由于node和parent都是红色(否则到不了这一步),parent左右子树的黑色节点数仍然是相等的
                     */
                    __rb_rotate_left(parent, root);
                    tmp = parent;
                    parent = node;
                    node = tmp;
                }
    
                /* parent 红->黑,gparent左子树比右子树多一个黑色节点
                 * 右旋后,gparent左子树高度减一,减少的节点即parent,减少了一个黑色节点,parent变为新的gparent。
                 * 所以右旋后,新的gparent的左右子树的黑色节点数再次平衡了
                 */
                rb_set_black(parent);
                rb_set_red(gparent);
                __rb_rotate_right(gparent, root);
            /* parent 是 gparent的右子树时,和上面的过程类似 */
            } else {
                {
                    register struct rb_node *uncle = gparent->rb_left;
                    if (uncle && rb_is_red(uncle))
                    {
                        rb_set_black(uncle);
                        rb_set_black(parent);
                        rb_set_red(gparent);
                        node = gparent;
                        continue;
                    }
                }
    
                if (parent->rb_left == node)
                {
                    register struct rb_node *tmp;
                    __rb_rotate_right(parent, root);
                    tmp = parent;
                    parent = node;
                    node = tmp;
                }
    
                rb_set_black(parent);
                rb_set_red(gparent);
                __rb_rotate_left(gparent, root);
            }
        }
    
        rb_set_black(root->rb_node);
    }
    

    函数4:删除一个节点,并且调整删除后各节点的颜色。其中调整节点颜色其实是另一个单独的函数。

    /* 删除节点时,如果被删除的节点左子树==NULL或右子树==NULL或左右子树都==NULL
     * 那么只要把被删除节点的左子树或右子树直接关联到被删节点的父节点上即可,剩下的就是调整各节点颜色。
     * 只有被删节点是黑色才需要调整颜色,因为删除红色节点不影响红黑树的特性。
     *
     * 被删节点左右子树都存在的情况下,其实就是用中序遍历中被删节点的下一个节点来替代被删节点。
     * 代码中的操作只是将各个指针指向新的位置而已。
     */
    void rb_erase(struct rb_node *node, struct rb_root *root)
    {
        struct rb_node *child, *parent;
        int color;
    
        if (!node->rb_left)
            child = node->rb_right;
        else if (!node->rb_right)
            child = node->rb_left;
        else
        {
            struct rb_node *old = node, *left;
    
            /* 寻找中序遍历中被删节点的下一个节点 */
            node = node->rb_right;
            while ((left = node->rb_left) != NULL)
                node = left;
    
            /* 替换要删除的节点old */
            if (rb_parent(old)) {
                if (rb_parent(old)->rb_left == old)
                    rb_parent(old)->rb_left = node;
                else
                    rb_parent(old)->rb_right = node;
            } else
                root->rb_node = node;
    
            child = node->rb_right;
            parent = rb_parent(node);
            color = rb_color(node);
    
            if (parent == old) {
                parent = node;
            } else {
                if (child)
                    rb_set_parent(child, parent);
                parent->rb_left = child;
    
                node->rb_right = old->rb_right;
                rb_set_parent(old->rb_right, node);
            }
    
            node->rb_parent_color = old->rb_parent_color;
            node->rb_left = old->rb_left;
            rb_set_parent(old->rb_left, node);
    
            goto color;
        }
    
        parent = rb_parent(node);
        color = rb_color(node);
    
        if (child)
            rb_set_parent(child, parent);
        if (parent)
        {
            if (parent->rb_left == node)
                parent->rb_left = child;
            else
                parent->rb_right = child;
        }
        else
            root->rb_node = child;
    
     color:
        if (color == RB_BLACK)
            __rb_erase_color(child, parent, root);
    }
    

    函数5:删除一个黑色节点后,重新调整相关节点的颜色。

    /* 这里的node就是上面函数中的child,所有node节点的左右子树肯定都是NULL
     * 不满足红黑树规则的就是从parent节点开始的子树,只要给从parent开始的子树增加一个黑色节点就行
     * 如果从parent节点开始的节点全是黑色,node和parent都继续向上移动
     */
    static void __rb_erase_color(struct rb_node *node, struct rb_node *parent,
                     struct rb_root *root)
    {
        struct rb_node *other;
    
        /* (node不为NULL 且 node是黑色的) 或者 node == NULL */
        while ((!node || rb_is_black(node)) && node != root->rb_node)
        {
            if (parent->rb_left == node)
            {
                other = parent->rb_right;
                if (rb_is_red(other))
                {
                    rb_set_black(other);
                    rb_set_red(parent);
                    __rb_rotate_left(parent, root);
                    other = parent->rb_right;
                }
                /* 如果从parent节点开始的节点全是黑色,node和parent都继续向上移动 */
                if ((!other->rb_left || rb_is_black(other->rb_left)) &&
                    (!other->rb_right || rb_is_black(other->rb_right)))
                {
                    rb_set_red(other);
                    node = parent;
                    parent = rb_parent(node);
                }
                else
                {
                    if (!other->rb_right || rb_is_black(other->rb_right))
                    {
                        rb_set_black(other->rb_left);
                        rb_set_red(other);
                        __rb_rotate_right(other, root);
                        other = parent->rb_right;
                    }
                    rb_set_color(other, rb_color(parent));
                    rb_set_black(parent);
                    rb_set_black(other->rb_right);
                    __rb_rotate_left(parent, root);
                    node = root->rb_node;
                    break;
                }
            }
            else
            {
                other = parent->rb_left;
                if (rb_is_red(other))
                {
                    rb_set_black(other);
                    rb_set_red(parent);
                    __rb_rotate_right(parent, root);
                    other = parent->rb_left;
                }
                if ((!other->rb_left || rb_is_black(other->rb_left)) &&
                    (!other->rb_right || rb_is_black(other->rb_right)))
                {
                    rb_set_red(other);
                    node = parent;
                    parent = rb_parent(node);
                }
                else
                {
                    if (!other->rb_left || rb_is_black(other->rb_left))
                    {
                        rb_set_black(other->rb_right);
                        rb_set_red(other);
                        __rb_rotate_left(other, root);
                        other = parent->rb_left;
                    }
                    rb_set_color(other, rb_color(parent));
                    rb_set_black(parent);
                    rb_set_black(other->rb_left);
                    __rb_rotate_right(parent, root);
                    node = root->rb_node;
                    break;
                }
            }
        }
        if (node)
            rb_set_black(node);
    }
    

    使用示例
    构造了一个内核模块来实际使用一下内核中的红黑树,代码在CentOS6.3 x64上运行通过。

    C代码:

    #include<linux/rbtree.h>
    #include <linux/string.h>
    #include "kn_common.h"
    
    MODULE_LICENSE("Dual BSD/GPL");
    struct student
    {
        int id;
        char* name;
        struct rb_node node;
    };
    
    static int insert_student(struct student*, struct rb_root*);
    static int remove_student(struct student*, struct rb_root*);
    static int display_student(struct rb_root*, int);
    static void display_student_from_small(struct rb_node*);
    static void display_student_from_big(struct rb_node*);
    static void print_student(struct student*);
    
    static int testrbtree_init(void)
    {
    #define N 10
        struct rb_root root = RB_ROOT;
        struct student *stu[N];
        char tmp_name[5] = {'w', 'y', 'b', '0', '\0'};
        int i;
    
        // init N struct student
        for (i=0; i<N; i++)
        {
            stu[i] = kmalloc(sizeof(struct student), GFP_KERNEL);
            stu[i]->id = i;
            stu[i]->name = kmalloc(sizeof(char)*5, GFP_KERNEL);
            tmp_name[3] = (char)(i+48);
            strcpy(stu[i]->name, tmp_name);
            // stu_name[3] = (char)(i+48);
            stu[i]->node.rb_left = NULL;
            stu[i]->node.rb_right = NULL;
        }
    
        for (i=0; i < N; ++i)
        {
            printk(KERN_ALERT "id=%d   name=%s\n", stu[i]->id, stu[i]->name);
        }
        
        // add N student to rbtree
        print_current_time(0);
        for (i=0; i < N; i++)
            insert_student(stu[i], &root);
    
        // display all students
        printk(KERN_ALERT "print from small to big!\n");
        display_student(&root, -1);
        printk(KERN_ALERT "print from big to small!\n");
        display_student(&root, 1);
    
        // delete student 8
        remove_student(stu[7], &root);
        display_student(&root, -1);
        
        // free all student
        for (i=0; i<N; ++i)
        {
            kfree(stu[i]->name);
            kfree(stu[i]);
        }
                        
        return 0;
    }
    
    static int insert_student(struct student* stu, struct rb_root* root)
    {
        struct rb_node* parent;
        struct rb_node* tmp_rb;
        struct student* tmp_stu;
    
        /* first time to insert node */
        if (!root->rb_node) 
        {
            root->rb_node = &(stu->node);
            rb_set_parent(&(stu->node), NULL);
            rb_set_black(&(stu->node));
            return 0;
        }
    
        /* find where to insert node */
        tmp_rb = root->rb_node;
        while(tmp_rb)
        {
            parent = tmp_rb;
            tmp_stu = rb_entry(tmp_rb, struct student, node);
    
            if (tmp_stu->id > stu->id) 
                tmp_rb = parent->rb_left;
            else if (tmp_stu->id < stu->id)
                tmp_rb = parent->rb_right;
            else
                break;
        }
    
        /* the student's id  is already in the rbtree */
        if (tmp_rb)
        {
            printk(KERN_ALERT "this student has been inserted!\n");
            return 1;
        }
        
        if (tmp_stu->id > stu->id)
            parent->rb_left = &(stu->node);
        else
            parent->rb_right = &(stu->node);
    
        rb_set_parent(&(stu->node), parent);
        rb_insert_color(&(stu->node), root);
        
        return 0;
    }
    
    static int remove_student(struct student* stu, struct rb_root* root)
    {
        rb_erase(&(stu->node), root);
        
        return 0;
    }
    
    static int display_student(struct rb_root *root, int order)
    {
        if (!root->rb_node)
            return 1;
        if (order < 0)
            display_student_from_small(root->rb_node);
        else
            display_student_from_big(root->rb_node);
        
        return 0;
    }
    
    static void display_student_from_small(struct rb_node* node)
    {
        struct student *tmp_stu;
        
        if (node)
        {
            display_student_from_small(node->rb_left);
            tmp_stu = rb_entry(node, struct student, node);
            print_student(tmp_stu);
            display_student_from_small(node->rb_right);
        }
    }
    
    static void display_student_from_big(struct rb_node* node)
    {
        struct student *tmp_stu;
        
        if (node)
        {
            display_student_from_big(node->rb_right);
            tmp_stu = rb_entry(node, struct student, node);
            print_student(tmp_stu);
            display_student_from_big(node->rb_left);
        }
    }
    
    static void print_student(struct student* stu)
    {
        printk(KERN_ALERT "=========================\n");
        print_current_time(0);
        printk(KERN_ALERT "id=%d\tname=%s\n", stu->id, stu->name);
        printk(KERN_ALERT "=========================\n");
    }
    
    static void testrbtree_exit(void)
    {
        printk(KERN_ALERT "*************************\n");
        print_current_time(0);
        printk(KERN_ALERT "testrbtree is exited!\n");
        printk(KERN_ALERT "*************************\n");
            
    }
    
    module_init(testrbtree_init);
    module_exit(testrbtree_exit);
    

    注:其中用到的kn_common.h和kn_common.c文件与队列的示例中一样。

    Makefile:

    obj-m += rbtree.o
    rbtree-objs := testrbtree.o kn_common.o
    
    #generate the path
    CURRENT_PATH:=$(shell pwd)
    #the current kernel version number
    LINUX_KERNEL:=$(shell uname -r)
    #the absolute path
    LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)
    #complie object
    all:
        make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
        rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned
    #clean
    clean:
        rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
    

    安装,卸载内核模块以及查看内核模块的运行结果:

    insmod rbtree.ko
    rmmod rbtree
    dmesg | tail -135
    
    展开全文
  • linux内核相关视频解析: 5个方面分析linux内核架构,让你对内核不再陌生 90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc的原理 手把手带你实现一个Linux内核文件系统 简介 作用是将应用层序的请求传递...
  • Linux内核的整体架构简介

    万次阅读 多人点赞 2017-10-23 17:44:42
    之后,会介绍Linux内核源文件的目录结构,并和各个软件子系统对应。 注:本文和其它的“Linux内核分析”文章都基于如下约定:  a) 内核版本为Linux 3.10.29(该版本是一个long term的版本,会被Linux社区持续维
  • Linux内核结构详解.rar

    2009-08-09 23:16:06
    Linux内核结构详解.rarLinux内核结构详解.rarLinux内核结构详解.rarLinux内核结构详解.rarLinux内核结构详解.rarLinux内核结构详解.rar
  • Linux的内核结构详细说明[摘要]Linux内核结构Linux内核主要由五个子系统组成:进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。1进程调度(SCHED):控制进程对CPU的访问。当需要选择下一个进程运行时,由...
  • 本书对早期的Linux操作系统内核(v0.11)全部源代码文件进行了详细的注释和 说明,旨在让读者能够在短时间内对Linux的工作机理获得全面而深刻的理解,为...然后依据内核源代码的组织结构对所有代码进 行了详细注释。
  • Linux内核结构与进程管理Linux内核结构与进程管理

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 306,546
精华内容 122,618
关键字:

linux内核结构

linux 订阅