精华内容
下载资源
问答
  • 深入理解linux内核架构
    千次阅读
    2022-03-13 21:12:15

    前言

    基于深入理解Linux内核 第三版 陈莉俊译

    操作系统基本概念

    任何计算机系统都包含一个名为操作系统的基本程序集合。在这个集合里,最重要的程序称为内核(Kernel)。当操作系统启动时,内核被装入到RAM中,内核中包含了系统运行所必不可少的很多核心过程(procedure)。

    操作系统必须完成主要的两个重要目标:

    • 与硬件部分交互,为包含在硬件平台上的所有低层可编程部件提供服务。
    • 为运行在计算机系统上的应用程序(用户程序)提供执行环境。

    类Unix操作系统把与计算机物理组织相关的所有底层细节都对用户运行的程序进行隐藏。当程序想要使用硬件资源时,必须项操作系统发出请求。内核对这个请求进行评估,如果允许使用这个资源,那么,内核代表应用程序与相关的硬件部分进行交互。

    为了实现这种机制,现代操作系统依靠特殊的硬件特性来禁止用户程序直接与底层硬件部分交互,或者禁止直接访问任意的物理地址。特别是,硬件为CPU引入了至少两种不同的执行模式:用户程序的非特权模式和内核的特权模式。Unix把它们分别称为用户态内核态

    多用户系统

    多用户系统就是一台能并发和独立地执行分别属于两个或多个用户地若干应用程序地计算机
    “并发”意味着几个应用程序能同时处于活动状态并竞争各种资源,如CPU,内存,硬盘等。
    “独立”意味着每个应用程序能执行自己地任务,而无需考虑其他用户的应用程序在做什么。
    当然从一个应用程序切换到另一个应用程序,会使每一个应用程序的速度有所减慢,从而影响用户看到的响应时间。

    多用户操作系统必须包含以下几个特点

    • 核实用户身份的认证机制
    • 防止有错误的用户程序妨碍其他应用程序在系统中运行的保护机制。
    • 防止有恶意的用户程序干涉或窥探其他用户的活动保护机制。
    • 限制分配给每个用户的资源数的计帐机制。

    为了确保实现这些安全保护机制,操作系统必须利用与CPU特权模式相关的硬件保护机制,否则,用户程序将能直接访问系统电路并克服强加于它的这些限制。

    用户和组

    在多用户操作系统中,每个用户在机器上都有私用空间;典型地,他拥有一定数量地磁盘空间来存储文件、接收私人邮件信息等等。操作系统必须保证用户空间地私有部分仅仅对其拥有者可见。

    所有用户由一个唯一的数字来标识,这个数字叫用户标识符(UID)。通常一个计算机系统只能由有限的人使用。当其中一个用户开始一个工作会话时,操作系统会有一个认证机制。

    为了和其他用户有选择地共享资料,每个用户是一个或多个用户组成员,组由唯一的用户组标识符(user group ID)标识。每个文件也恰好与一个组相对应。

    任何地类Unix操作系统都有一个特殊用户,root(超级用户)。操作系统对root用户不使用通常地保护机制,可以访问任意一个文件,干涉每一个正在执行地用户程序。

    进程

    所有的操作系统都使用一种基本的抽象:进程。
    一个进程可以定义为:“程序执行时的一个实例”,或者一个运行程序的执行上下文
    在传统的操作系统中,一个进程在地址空间中执行一个单独的指令序列。地址空间是允许进程引用的内存地址集合。现代操作系统允许具有多个执行流的进程。也就是说,在相同的地址空间可执行多个指令序列

    运行进程并发活动的系统称为多道程序系统或多处理器系统。区分程序和进程是非常重要的:几个进程能并发的执行同一程序,而同一个进程能顺序地执行几个程序。

    每个进程都自以为他是系统中唯一的进程,可以独占操作系统所提供的服务。只要进程发出系统调用,硬件就会把特权模式由用户态变为内核态,然后进程以非常有限的目的开始一个内核过程的执行。当内核过程完成,进程又退回到用户态,然后进程从系统调用的下一条指令继续执行。

    内核体系结构

    单块内核:从整体上把内核作为一个大过程来实现,同时也运行在一个单独的地址空间上。因此单内核通常以单个静态二进制文件的形式存放于磁盘中。而进程管理、内存管理等是其中的一个个模块,所有内核模块都在这样的一个大内核地址空间上运行。模块之间可以直接调用相关的函数。效率高,紧凑性强。大多数的Unix系统都设计为单内核。Linux也是一个单内核,也就是说,Linux内核运行在单独的内核地址空间上。

    微内核
    微内核并不作为一个单独的大过程来实现,微内核的功能被划分为多个独立的进程程,进程程之间保持独立并运行在各自的地址空间上。
    微内核是一种功能更贴近硬件的核心软件,它一般仅仅包括基本的内存管理、同步原语、进程间通信机制、IO操作和中断管理(只是将OS中最核心的功能加入内核),这样做有利于提高可扩展性和可移植性,但微内核与文件管理、设备驱动、虚拟内存管理、进程管理等其他上层模块之间需要有较高的通信开销。

    所以为了达到微内核理论上的很多优点而又不影响性能,linux内核提供了模块(module)。模块是一个目标文件,其代码可以运行时链接到内核或从内核解除链接。这种目标代码通常由一组函数组成,用来实现文件系统、驱动程序或其他内核上层功能。与微内核操作系统外层不同,模块不是作为一个特殊的进程执行。相反,与任何其他静态链接的内核函数一样,它代表当前进程在内核状态下执行。

    使用模块化的主要优点:
    模块化方法:
    因为任何模块都可以在运行时被链接或解除链接,因此,系统程序员必须提出明确的软件接口以访问由模块处理的数据结构。这使得开发新模块变得容易。
    平台无关性:
    即使模块依赖于某些特殊的硬件特点,但它不依赖于某个固定的硬件平台。
    节省内存使用:
    当需要模块功能时,把它链接到正在运行的内核中,否则,将该模块解除链接。这种机制对于小型嵌入式系统是非常有用的。
    无性能损失:
    模块的目标代码一旦被链接到内核,其作用与静态链接的内核目标代码完全等价。因此,当模块的函数被调用时,无需显式地进行消息传递。

    Unix文件系统概述

    Unix操作系统的设计集中反应在其文件系统上

    文件

    Unix文件是以字节序列组成的信息载体,内核不解释文件的内容。从用户的角度看,文件被组织在一个树结构的命名空间中。如下
    在这里插入图片描述

    除了叶节点之外,树的所有节点都表示目录。与树的根相对应的目录叫根目录。
    绝对路径:由/开头,即从顶层的根目录开始的路径,就是绝对路径
    相对路径:由./或…/开头,即从当前路径或父目录路径开始的路径,称为相对路径。

    硬链接和软链接

    硬链接创建命令
    ln p1 p2
    即为由路径p1标识的文件创建一个路径为p2的硬链接。作用就是为了防止误删系统中的重要文件。其本质就是一个inode结点。和源文件的inode节点一模一样的。删除了源文件路径,依旧可以通过该硬链接访问文件,文件并不会删除。

    硬链接的限制:

    • 不允许给目录创建硬链接。因为这可能把目录树变为环形图,从而就不可能通过名字定位文件。
    • 只有在同一个文件系统中的文件之间才能创建硬链接。现代Unix系统可能包含了多种文件系统,这些文件系统位于不同的磁盘或分区,用户也许无法知道它们之间的物理划分。

    为了克服硬链接的这些限制,就此诞生了软链接
    创建命令:
    ln -s p1 p2
    表示:创建一个p2新的软链接,指向路径名为p1.
    软链接就好比Windows下的快捷方式,通过软链接可以导向文件真实存在的路径,删掉软链接并不会真的删除文件。(但是删除源文件,软链接就无效了,这就区别于硬链接)。

    适用于任何的文件系统,且可以给目录创建软链接,会自动导向真实路径。

    文件类型

    Unix文件可以分为以下类型

    • 普通文件(-)
    • 目录(d)
    • 符号链接(l)
    • 面向块的设备文件(b)
    • 面向字符设备的文件(c)
    • 管道和命名管道文件(p)
    • 套接字Socket(s)
      前三种文件是Unix文件系统的基本类型

    设备文件与I/O设备以及集成到内核中的设备驱动程序相关。例如,当程序访问设备文件时,它直接访问与那个文件相关的I/O设备

    管道和套接字是用于进程间通信的特殊文件

    文件描述符与索引节点

    Unix对文件的内容和描述文件的信息给出了清楚的区分。除了设备文件和特殊文件系统文件外,每个文件都是由字符序列组成。文件内容不包含任何控制信息,如文件长度或文件结束符(EOF)

    文件系统处理文件需要的所有信息包含在一个名为索引节点(inode)的数据结构中。每个文件都有自己的索引节点,文件系统用索引节点来标识文件。

    索引节点包含属性:

    • 文件类型
    • 与文件相关的硬链接数
    • 以字节为单位的文件长度
    • 设备标识符(即包含文件的设备的标识符)
    • 在文件系统中的标识文件的索引节点号
    • 文件拥有者的UID
    • 文件的用户组ID
    • 几个时间戳,表示索引节点状态改变的时间、最后访问时间及最后修改时间
    • 访问权限和文件模式

    访问权限和文件模式

    文件的潜在用户分为三种类型:

    • 作为文件拥有者的用户
    • 同组用户,不包含所有者
    • 所有剩余的用户(其他用户)

    有三种类型的权限:读、写、可执行 通过wrx表示,所以一共有9种不同的二进制来标记。
    还有三种附加的标记,suid,sgid,sticky用来定义文件的模式。当这些标记用到在可执行文件时有如下含义:
    suid: 设置可执行文件的suid标志位,就获得了该文件拥有者的UID
    sgid:设置了可执行文件的sgid的标志位,就获得了该文件用户组的ID
    sticky:设置了sticky标志位的可执行文件相当于向内核发送一个请求,当程序执行结束后,依然将他保留在内存中。(这个标志已经过时,目前使用基于代码页共享的其他方法)

    文件操作的系统调用

    当用户访问一个普通文件或目录文件的内容时,他实际上是访问存储在硬件块设备上的一些数据。从这个意义上来说,文件系统是硬盘分区物理组织的用户视图。因为处于用户态的进程不能直接和底层硬件打交道,所以每个实际的文件操作必须在内核态下进行。因此,Unix操作系统定义了几个与文件操作有关的系统调用

    打开文件

    进程只能访问“打开的”文件。问了打开一个文件,进程调用系统调用:
    fd=open(path,flag,mode)
    path:打开文件的路径
    flag:打开文件的方式(读、写、读/写,追加)
    mode:指新创建的文件的访问权限。
    返回值是一个文件描述符(文件对象),注意打开文件的同步问题,如果需要上锁,可以通过flock()函数实现上锁。

    为了创建一个新的文件,进程也可以调用create()系统调用,他与open()非常相似,都是由内核来处理。

    访问打开文件

    对于普通Unix文件,可以顺序访问,也可以随机访问,而对设备文件和命名管道文件,通常只能顺序地访问。在这两种访问方式中,内核把文件指针存放在打开文件对象中,也就是说,当前位置就是下一次进行读或写地位置。

    顺序访问是文件默认访问方式,即read(),write()系统调用总是从文件指针地当前位置开始读/写。为了修改文件指针的值,必须在程序中显式地调用
    lseek()系统调用。
    nowoffset=lseek(fd,offset,whence)
    fd:文件描述符
    offset:偏移量,有符号值
    whence:表示文件指针新位置,有文件头,当前位置,文件末尾三个选项。

    read()系统调用需要以下参数
    nread=read(fd,buf,count);
    fd:文件描述符
    buf:读取出的数据存储缓冲区
    count:一次读操作读取的数据字节个数。

    write和read相似,当读到文件结束符EOF,read就会返回EOF表示已经读到文件末尾(读完了),同时在读的过程中,文件指针会自动地加1。

    关闭文件

    当进程无需访问文件时,就调用系统调用
    res=close(fd)
    fd:文件描述符
    当一个进程终止时,内核会关闭其打开的所有仍然打开着的文件

    更名及删除文件

    系统调用更名:
    res=rename(oldpath,newpath);
    只是改了文件链接的名字。
    删除文件系统调用:
    res=unlink(pathname);减少文件链接数,删除了对应的目录项。只有当链接项为0时,文件才被真正删除。

    Unix内核概述

    Unix内核提供了应用程序可以运行的执行环境。因此,内核必须实现一组服务及相应的接口。应用程序使用这些接口,而且通常不会与硬件资源直接交互。

    进程/内核模式

    当一个程序在用户态下执行时,他不能直接访问内核数据结构或内核的程序。然而,当应用程序在内核态下运行时,这些限制不再有效。一个程序大部分时间都处于用户态下,只有需要内核所提供的服务时才切换到内核态。当内核态满足了程序的请求后,它让程序又回到用户态下。

    进程是动态的实体,在系统内通常只有有限的生存期。创建、撤消及同步现有进程的任务都委托给内核的一组例程来完成。

    内核本身并不是一个进程,而是进程的管理者。进程/内核模式假定:请求内核服务的进程使用所谓系统调用的特殊编程机制。每个系统调用都设置了一组识别进程进程请求的参数,然后执行与硬件相关的CPU指令完成从用户态到内核态的转换。

    进程实现

    为了让内核管理进程,每个进程由一个进程描述符表示,这个进程描述符包含有关进程当前状态的信息。

    当内核暂停一个进程执行时,就把几个相关处理器寄存器的内容保存在进程描述符中。这些寄存器包括:

    • 程序计数器(PC)和栈指针(SP)寄存器
    • 通用寄存器
    • 浮点寄存器
    • 包含CPU状态信息的处理器控制寄存器
    • 用来跟踪进程对RAM访问的内存管理寄存器

    当内核决定恢复执行一个进程时,它用进程描述符中合适的字段来装载CPU寄存器。因为程序计数器中所存的值指向下一条将要执行的指令,所以进程从它停止的地方恢复执行。

    可重入内核

    所有的Unix内核都是可重入的,这意味着若干进程可以同时在内核态下执行。

    提供可重入的一种方式是编写函数,以便这些函数只能修改局部变量,而不能改变全局数据结构。这样的函数叫可重入函数。

    如果一个硬件中断发生,可重入内核能挂起当前正在执行的进程,即使这个进程处于内核态。

    内核控制路径表示内核处理系统调用,异常或中断所执行的指令序列。

    进程地址空间

    每个进程运行在它的私有地址空间。在用户态下运行的进程涉及到私有栈,数据区和代码区。当在内核态运行时,进程访问内核的数据区和代码区,但是使用另外的私有栈。

    进程间也能共享一部分地址空间,以实现一种进程间通信,这就是System V引入并且已经被Linux支持的“共享内存”技术。

    同步和临界区

    实现可重入内核需要利用同步机制:如果内核控制路径对某个内核数据结构进行操作时被挂起,那么,其他的内核控制路径就不应当再对该数据结构进行操作,除非它已经被重新设置成一致性状态。否则,两个控制路径的交互作用将破坏所存储的信息。

    非抢占式内核

    非抢占式内核就是指的进程在内核态执行时,它不能被挂起,也不能被另一个进程替代。只要进入了内核态,就一直到满足需求退回到用户态为止。但是这种内核设计在多处理器系统上运行是抵消的。

    禁止中断

    单处理器系统上的另一种同步机制是:在进入一个临界区之前禁止所有硬件中断,离开时再重新启动中断。这种机制尽管简单,但是不是最佳。如果临界区较大,那么在临界区中停留的时间相对较长,这一段较长的时间内持续禁止中断可能使所有的硬件活动处于冻结状态。

    此外,由于多处理器系统中禁止本地CPU上的中断是不够的,所以必须使用其他同步技术。

    信号量

    广泛使用的一种同步技术就是是信号量。信号量仅仅是与一个数据结构相关的计数器。所有内核线程在试图访问这个数据结构之前,都要检查这个信号量。可以把每个信号量看成一个对象,其组成如下:

    • 一个整数变量
    • 一个等待进程的链表
    • 两个原子方法:down()和up()
      down()的方法对信号量的值减一,如果这个新值小于0,该方法就把正在运行的进程加入到这个信号量链表,然后阻塞该进程(即调用调度程序)。up()方法对信号量的值加1,如果这个新值大于或等于0,则激活这个信号链表中的一个或多个进程。

    每个要保护的数据结构都有自己的信号量,其初始值为1.

    自旋锁

    自旋锁和信号量相似,但没有进程链表;当一个进程发现锁被另一个进程锁着时,他就会不停的“旋转”,执行一个紧凑的循环指令直到锁打开。

    避免死锁

    死锁:进程P1获得了访问数据a的权限,但是在等待进程P2释放b的访问权限,进程P2拿到了b的访问权限,但是在等待P1释放a的访问权限。这样一个互等的状态就是死锁。

    信号和进程间通信

    Unix信号(signal)提供了把系统事件报告给进程的一种机制。每种事件都有自己的信号编号,通常用一个符号常量来表示。

    进程可以用两种方式对接收到的信号做出反应:

    • 忽略信号
    • 异步执行一个指定的过程(信号处理程序)
      如果进程不指定选择何种方式,内核就根据信号的编号执行一个默认的操作。五种默认操作是:
    • 终止进程
    • 将执行上下文和进程地址空间内容写入一个文件(核心转储),并终止进程
    • 忽略信号
    • 挂起进程
    • 如果进程曾被暂停,则恢复它的执行

    SIGKILL和SIGSTOP信号不能直接由进程处理,也不能直接忽略

    AT&T的Unix System V引入了在用户态下其他种类的进程间通信机制,很多Unix内核也采用了这些机制:信号量、消息队列以及共享内存。它们被统称为System V IPC。

    进程管理

    Unix在进程和它正在执行的程序之间做出了清晰的划分。fork()和_exit()系统调用发别来表示创建一个新进程和终止一个进程,而调用exec()类系统调用则是装入一个新程序。
    exit() 和_exit()函数的区别就是_exit()不会刷新流,exit是一个C库函数,_exit()是一个系统调用

    调用fork()进程的是父进程,而新进程是它的子进程。父子进程能相互找到对方,因为描述每个进程的数据结构包含两个指针,一个指向父亲,一个指向子进程。
    实现fork()一种天真的方式就是将父进程的数据与代码都复制,并把这个拷贝赋予子进程
    _exit()系统调用终止一个进程。内核对这个系统调用的处理就是通过释放进程所拥有的资源并向父进程发送SIGCHILD信号来实现

    僵死进程

    父进程通过wait()系统调用函数来查询子进程是否已经终止了。wait()系统掉调用允许进程等待,直到其中的一个子进程结束;它返回已终止子进程的进程标识符(PID)。
    孤儿进程:就是父进程已经结束了,但是子进程还没结束,则子进程就没了父亲。出现这种情况一般该子进程会被init进程收养,但是有的设置的是被最近的祖宗收养。
    僵死进程:子进程结束了,但是父进程没有接收到子进程结束的信号,导致该子进程的资源无法被回收。

    进程组和登录会话

    每个进程描述符包括一个包含进程组ID的字段。每个进程组可以有一个领头进程(即其PID与这个进程组ID相同的进程)。新创建的进程最初被插入到其父进程的进程组中。

    内存管理

    虚拟内存

    所有新近的Unix系统都提供了一种有用的抽象,叫虚拟内存。虚拟内存作为一种逻辑层,处于应用程序的内存请求与硬件内存管理单元(MMU)之间。虚拟内存有很多用途和优点:

    • 若干进程可以并发地执行
    • 应用程序所需内存大于可用物理内存时也可以运行。
    • 程序只有部分代码装入内存时进程可以执行它
    • 允许每个进程访问可用物理内存的子集。
    • 进程可以共享库函数或程序的一个单独内存映象。
    • 程序是可重定位的,也就是说,可以把程序放在物理内存的任何地方。
    • 程序员可以编写与机器无关的代码,因为它们不必关心有关物理内存的组织结构。

    虚拟内存子系统的主要成分是虚拟地址空间的概念。进程所用的一组内存地址不同于物理内存地址。当一个进程使用虚拟地址时,内核和MMU协同定位其在内存中的实际物理位置。

    随机访问存储器(RAM)的使用

    所有的Unix操作系统都将RAM毫无意义地划分为两部分,其中若干兆字节专门用于存放内核映象。RAM的其余部分通常由虚拟内存系统来处理,并且用在以下三种可能的方面:

    • 满足内核对缓冲区,描述符及其他动态内核数据结构的请求。
    • 满足进程对一般内存区的请求及对文件内存映射的请求。
    • 借助于高速缓存从磁盘及其他缓冲设备获得较好的性能。

    内核内存分配器(KMA)

    内存内核分配器是一个子系统,它试图满足系统中所有部分对内存的请求。其中一些请求来自内核其他子系统,它们需要一些内核使用的内存。还有一些来自用户程序的系统调用,用来增加用户进程的地址空间。一个好的KMA应该具有如下特点:

    • 必须快,因为它由所有的子系统调用
    • 必须把内存的浪费减到最少
    • 必须努力减轻内存碎片问题
    • 必须能与其他内存管理子系统合作,以便借用和释放页框。

    几种KMA算法:

    • 资源图分配算法
    • 2的幂次方空闲链表
    • McKusick-Karels分配算法
    • 伙伴系统
    • Mach的区域分配算法
    • Dynix分配算法
    • Solaris的Slab分配算法

    进程虚拟地址空间处理

    内核通常用一组内存区描述符描述进程虚拟地址空间。内核分配给进程的虚拟地址空间由以下内存区组成:

    • 程序的可执行代码
    • 程序的初始化数据
    • 程序的未初始化数据
    • 初始程序栈(用户态栈)
    • 所需共享库的可执行代码和数据
    • 堆(由程序动态请求内存)

    高速缓存

    物理内存的一大优势就是用作磁盘和其他块设备的高速缓存。因为硬盘非常的慢,与RAM的访问时间相比,太长了。所以设置了一个高速缓存Cache。用于提高对磁盘中的数据访问。

    通过sync()系统调用把所有"脏"的缓冲区(即缓冲区的内容与对应磁盘块的内容不一样)写入磁盘中来强制磁盘同步。(周期性的)

    设备驱动程序

    内核通过设备驱动程序与I/O设备交互。设备驱动程序包含在内核中,由控制一个或多个设备的数据结构和函数组成,这些设备包括硬盘,键盘,鼠标,监视器,网络接口以及连接到SCSI总线上的设备。通过特点的接口,每个驱动程序与内核中的其余部分(甚至与其他驱动程序)相互作用这种方式的优点有如下:

    • 可以把特定设备的代码封装在特定的模块中
    • 厂商可以在不了解内核源代码而只知道接口规范的情况下,就能增加新设备。
    • 可以把设备驱动程序写成模块,并动态的加载到内核而不用重新启动内核。
      在这里插入图片描述
    更多相关内容
  • 深入Linux内核.pdf

    2021-09-07 01:00:33
    深入Linux内核.pdf
  • 深入理解Linux内核架构

    万次阅读 2016-12-30 21:44:36
    命名空间,可以简单理解为划分多个地区,在每个地区单独命名。 命名空间的作用: (1)只使用一个内核,在一台计算机上运作,所有全局资源都通过命名空间抽象起来,这使得可以将一组进程放置到容器中,个容器彼此...

    2.1 进程优先级

     粗略分,实时进程和非实时进程。
    
     实时进程:(1)硬实时进程,必须在可保证的时间范围内得到处理。
                     (2)软实时,仍然需要尽快处理,晚一点也可以接受。
    
     普通进程,非实时的,根据重要性来划分,交互式的要尽快响应,冗长的编译或计算优先级可以低一点。
    
    
    
     调度器的两次进化
     (1) O(1)调度器,可以在常数时间内完成工作,不依赖系统运行的进程数目。
    
     (2)完全公平调度器,试图尽可能模仿理想情况下的公平调度,还能处理更一般行的调度实体。
    

    2.2 进程的生命周期

    进程的状态:
    (1)运行:该进程正在执行。
    (2)等待:进程可运行,但没得到许可,当前cpu给了另外的进程,调度器下次可以选择当前进程。
    (3)睡眠:进程正在睡眠,无法运行,等待外部事件,调度器下次切换任务时不可选该进程。

    进程的状态切换
    这里写图片描述

    图 2-2的每个路径的描述
    ①进程必须等待事件,则从“运行”改变为“睡眠”。
    ②调度器从进程收回CPU资源,则从“运行”变为“等待”。
    ③“睡眠”状态,不能直接变为“运行”,在进程等待的时间发生后,先变回“等待”。
    ④在调度器授予CPU时间之前,进程一直保持“等待”,分配CPU时间后,状态才能改为“运行”。
    ⑤程序执行终止后,就从“运行”变为“终止”。
    
    
    僵尸进程:进程已经终止了,但是进程表终仍然有对应的表项。
    产生的原因:进程由另外的进程或者用户杀死,父进程,在子进程终止时,没有调用wait释放子进程保留的资源。
    

    linux进程管理中的两种状态:用户状态和核心态。
    用户态进程:权限受限,只能访问自身数据,不会干扰其他程序,如果要访问系统数据或者功能,必须切换到核心态。
    核心态进程:有无限的权限。

    从用户态切换到核心态的方法:
    (1)系统调用,这个是由用户程序调用。
    (2)中断,这个是自动触发的。

    内核抢占是调度,抢占规则:
    (1)普通进程总是可被抢占的,甚至是由其他进程抢占。
    (2)系统处于核心态, 正在处理系统调用,其他进程是无法抢占CPU的,调度器必须等到系统调用结束。
    (3)中断可以暂停处于用户态和核心态的进程,优先级最高。

    2.3 进程表示

    核心数据结构tack_struct,相当庞大,可以分为如下部分
    (1)状态和执行信息,如待决信号,pid,父进程指针。
    (2)已分配的虚拟内存信息。
    (3)进程身份凭据,用户ID,组ID及权限。
    (4)使用的文件,包括程代码的二进制文件。
    (5)线程信息,记录该进程特定于CPU的运行时间数据。
    (6)进程间通信相关的信息。
    (7)信号处理程序。

    linux资源限制机制,对进程使用系统资源施加某些限制,用rlim数组。
    struct rlimit{
    unsigned long rlim_cur;
    unsigned long rlim_max;
    }

     rlim_cur,进程当前资源限制,也称软限制。
     rlim_max,最大容许值,也称硬限制。
    
     getrlimits和setrlimit分别用来读写,当前限制。可以查看进程的限制,cat /proc/pid/limits
    

    2.3.1 进程类型
    典型的unix进程包括:二进制代码组成的应用程序、单线程、分配给应用程序的一组资源。
    进程的产生方式:
    (1)fork,生成当前进程的一个相同副本,称子进程,原进程的所有资源都以适当的方式复制到子进程。
    原来的进程有两个独立的实例,包括同一组打开文件、同样的工作目录、内存中的同样数据。

     (2)exec,从一个可执行的二进制文件加载另外一个程序,来代替当前运行的进程。exec不创建新进程,所以
    要先fork复制一个旧程序,在exec创建另外一个应用程序。
    
     (3)clone,原理基本和fork相同,新进程不是独立于父进程的,而是可以与父进程共享某些制定需要的资源。
          一般用于实现线程。
    

    2.3.2 命名空间

    传统的全局管理资源,例如系统所有进程,通过pid标识,所以内核管理一个全局的pid列表。
    

    用户id的管理方式类似,全局id使得内核有选择允许货拒绝某些特权。如果想让某个用户为root,其他用户又不想受到干涉,那就太难了,这样他们每个都独立一个主机才行。这种做法存在局限性。

    命名空间,可以简单理解为划分多个地区,在每个地区单独命名。

    命名空间的作用:
    (1)只使用一个内核,在一台计算机上运作,所有全局资源都通过命名空间抽象起来,这使得可以将一组进程放置到容器中,个容器彼此隔离。
    (2)隔离使得容器的成员与其他容器毫无关系。
    (3)可以通过允许容器进行一定的共享,来降低容器之间的分隔。

    本质上,命名空间建立了系统的不同视图,每一项全局资源,都必须包含到容器的数据结构中,
    只有资源和包涵资源的命名空间,构成的二元组才是全局唯一的。
    这里写图片描述

    命名空间的创建方法:
    (1)fork或clone创建进程时,有选项可以控制是与父进程共享命名空间,还是建立新的命名空间。
    (2)unshare系统调用,将进程的某些部分从进程分离,其中包括命名空间。

    子进程从父进程命名空间分离后,从子进程看,改变全局属性不会传播到父进程命名空间,而父进程的修改也不会传播到子进程,文件系统的另当别论。

    命名空间如何实现?
    (1)每个子系统的命名空间结构将此前所有的全局组件包装到命名空间中。
    (2)将给定进程关联到所属命名空间的机制。

    每个内核子系统的全局属性都封装到命名空间,用一个数据接口,将所有通过命名空间形式提供的对象集中起来,就是struct nsproxy。

    这里写图片描述

     从struct task_struct {
    ……
        struct nsproxy *nsproxy;
    ……
    }可知,多个进程可以共享一个命名空间。
    

    2.3.3 进程ID号

    在命名空间中用于唯一标识一个进程的,是进程ID,简称pid,fork货clone产生的每个进程都由内核分配一个新的pid值。
    

    1、进程id有很多类型:
    (1)进程里面的线程组id。
    (2)进程组组长id。
    (3)几个进程组合成一个会话,所以每个进程有个会话id。

    全局ID和局部ID的区分:在建立一个新的命名空间时,该命名空间中的所有pid对父命名空间时可见的,但子命名空间无法看到父命名空间的皮带。这表示某些进程有多个pid,凡可以看到该进程的命名空间都会为其分配一个pid。

    全局ID:对每个ID类型,都有一个给定的全局ID,保证在整个系统中的唯一性。
    局部ID:对每个ID类型,它们在所属的命名空间内部有效,但类型相同、值也相同的ID可能出现在不同的命名空间中。

    2、管理pid

    pid分配器,用于加速新Id的分配。这里的id是广义上的,包括组id,组长id等。

    先看看pid命名空间的表示方式
    struct pid_namespace {
    ……
    struct task_struck *child_reaper;
    ……
    int level;
    struct pid_namespace *parent;
    };

    这里有2个关键的地方:
    (1)每个pid命名空间都有个进程,如上述的child_reaper就用于指向这个进程,该进程的作用相当于init进程,目的是对孤儿进程调用wait4,命名空间局部的init变体也需要完成该工作。

    (2)parent指向父命名空间,层次表示当前命名空间在命名空间的层次结构中的深度。初始命名空间的深度为0。

    pid管理围绕struct pid(内核对pid的内部表示)和struct upid(特定的命名空间中的可见信息)展开。

    struct upid{
    int nr;//id的数值
    struct pid_namespace *ns;//该id所属的命名空间指针
    struct hlist_node pid_chain;//将所有的upid链接在散列表的链表。
    };

    struct pid
    {
    atomic_t count;
    struct hlist_head tasks[PIDTYPE_MAX];//每个项对应于一个id类型,作为一个散列表头。
    int level;
    };

    对tasks中的每项,因为一个ID可能用于几个进程(如图2-5),所有共享同一给定ID的task_struct实例都吐过该
    列表链接起来。

    enum pid_type
    {
    PIDTYPE_PID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX
    };
    这里写图片描述

    用函数可以操作和扫描上述复杂结构:
    (1)给出局部数字id和对应的命名空间,查找此二元组的task_struct;
    (2)给出task_struct、id类型、命名空间,取得命名空间局部的数字ID;

    3、生成唯一的pid
    具体做法:为跟踪已经分配和仍然可用的pid,内核使用一个大的位图,其中每个pid由一个比特标识,pid的值可以通过对应比特在位图的位置来计算。其他的id都可以派生于pid。

    2.3.4 进程关系

    一张图说明,父子关系,兄弟关系。
    

    这里写图片描述

    struct task_struct{
    ……
    struct list_head children;
    struct list_head sibling;
    ……
    }
    

    2.4 进程管理相关的系统调用

    2.4.1 进程复制
    进程复制有3个方法:
    (1)fork,建立一个父进程的完整副本,然后作为子进程执行。linux使用写实复制。
    
    (2)vfork,不创建父进程的副本,父子进程之间共享数据,一个进程修改,另外的会知道。
    

    主要用于vfork后,调用execve加载新程序,子进程开始或退出前,父进程处于堵塞状态。

    (3)clone产生线程,可以对父子进程之间的共享、复制进行精确控制。
    
    
    1、写时复制
     诞生的原因:复制父进程副本使用内存多,耗费时间长,很多情况下不需要复制。
     作用是可以节省时间、内存空间。
     在父、子进程确实要写入,会产生缺页异常,然后再由内核分配内存空间。
    
    2、执行系统调用
     fork  —— sys_fork
    vfork  —— sys_vfork
    clone — — sys_clone
    几个系统调用对应的入口函数,最终这些入口函数,调用体系结构无关的do_fork,通过clone_flags这个标志集合区分不同入口。
    
    
    3、do_fork的实现
    do_fork
        |—>copy_process
        |—>确定pid
        |—>初始化vfork的完成程序(在设置了CLONE_VFORK的情况下)和ptrace标志。
        |—>wake_up_new_task
        |—>是否设置了CLONE_VFORK标志?—>wait_for_completion
    
    
       子进程生产成功后,需要执行一下操作:
    (1)如果有CLONE_PID标志,fork操作先要创建新的pid命名空间。然后在新命名空间获取pid。否则直接获取局部pid。
    (2)如果用了ptrace监控,创建进程后,就向它发送SIGSTOP信号,让调试器检查数据。
    (3)子进程使用wake_up_new_task唤醒,也就是将它的task_struct添加到调度器队列,让她有机会执行。
    (4)如果启用vfork机制,就要启用子进程的完成机制,子进程的task_struct的vfork_done成员即用于该墓地,父进程用wait_for_completion函数在该变量中进入睡眠,直至子进程退出。子进程终止时,内核调用complete(vfork_done)唤醒因该变量睡眠的进程。
    
    4、复制进程
    该过程主要受到标志的控制,来进行操作,内容较多,但比较简单。
    
    5、创建线程时的特别问题
    也是一些标志控制问题。
    
    2.4.2 内核线程
    
    内核线程的定义:直接由内核本身启动的进程,实际上是将内核函数委托给独立的进程,与系统其他进程并行执行,经常被称为内核守护进程。如,内存与块设备同步进程、系统事务日志进程。
    
    内核线程,主要两类,要么就是启动后,一直等待内核让它执行某一些操作,要么就是周期性运行,检查特定资源,是否符合预设限制,然后根据检查结果执行操作。
    
    内核线程的特别之处:
    

    (1)它们在cpu管态执行,而不是用户态。
    (2)它们只能访问内核部分的虚拟地址空间,不能访问用户空间。由于这个原因,内核上下文切换时,不需要倒换用户层的虚拟地址,等到下次执行的是与切换前不一样的进程才需要切换。

    内核线程的实现:
    (1)将一个函数传递给kernel_thread,内核调用daemonize,从内核释放父进程的所有资源。
    (2)daemonize阻塞信号的接收。
    (3)将init作为守护进程的父进程。

    更简单的方法是kernel_create。

    2.4.3 启动新程序

    1、ececve的实现
    该系统调用的入口点是体系结构相关的sys_execve,最后将工作委托给do_execve。
    do_execve的主要工作:
    (1)打开要执行的文件。
    (2)bprm_init,申请进程空间并初始化。
    (3)prepare_binprm,提供父进程相关的值,也是用于初始化。
    (4)search_binary_handler,查找一种适当的二进制格式,用于所要执行的特定文件。
    如释放原进程的所有资源,将应用程序映射到虚拟地址空间,参数和环境也映射到虚拟地址空间,
    设置进程的指令指针和其他特定于体系结构的寄存器。
    
    2、解释二进制格式
    解释数据结构体 linux_binfmt,每种二进制格式,都要用register_binfmt向内核注册。
    
    主要接口:
    (1)load_binary加载程序。
    (2)load_shlib价值普通程序。
    (3)core_dump用于在程序错误的情况下输出内存转储,用于调试分析。
    

    2.4.4 进程的退出
    进程调用exit退出,内核能有机会讲资源释放回系统。

    2.5 调度器的实现

    2.5.1 概观

    调度器的任务是在程序之间共享cpu时间,该任务分为调度策略和上下文切换两部分。
    
    当前的调度器只考虑进程的等待时间,即进程在就绪队列中已经等待的时间,对cpu时间需求最严格的进程被调用,这样进程的不公平等待不会积累,不公平会均匀分布到系统的所有进程。
    
    组成结构:所有可运行的进程都按等待时间在一个红黑树中排序,等待cpu时间最长的进程是在最左侧,调度器下次会考虑该进程,等待时间稍短的进程在该树上从左至右排序。时间复杂度时O(log n)。
    
    虚拟时钟:该时钟的时间流逝速度慢于实际的时钟,精确的速度依赖于当前等待调度器挑选的进程数目。如4个进程,在就绪队列实际等待20秒,相当于虚拟时间5秒。
    
    就绪队列的虚拟时间由fair_clock确定,进程的等待时间保存在wait_runtime,在红黑树排序时,食用
    

    fair_clock-wait_runtime,当然是用绝对值了。另外,程序运行时,将从wait_runtime减去已经运行的时间,这样按时间排序时,它就会往右移动一点了。当前还会受到进程的优先级,和是否频繁切换因素影响。

    2.5.2 数据结构

    调度子系统的概观
    

    这里写图片描述

    激活调度的方法:
    (1)直接的,进程打算睡眠或其他因素放弃cpu。
    (2)周期性的,以固定频率运行,检查是否有必要进行进程切换。
    用这两种方法 ,分别是上面的通用调度器、核心调度器。

    调度类:用于判断后面调用哪个进程,内核支持不同的调度策略(完全公平、实时调度、空闲时调度空闲进程),调度类能以模块化实现这些策略。调度器被调用时,它会查询调度器类,确定接下来运行哪个进程。也就是调度器类。

    进程切换:在选中将运行的程序后,要执行底层任务切换。

    注意:每个进程都属于某一调度类,每个调度类负责管理所属的进程,调度器本身不涉及进程管理,工作都委托给调度类。

    1、task_struct的成员
    struct task_struct{
    ……
    int prio;
    int normal_prio;
    int static_prio;
    unsigned int rt_priority;
    const struct sched_class *sched_class;
    unsigned int policy;
    cpumask_t cpus_allowed;
    unsigned int time_slice;
    ……
    };

    动态优先级:
    (1)主要有两类prio和normal_prio。
    (2)其中normal_prio表示基于进程的静态优先级和调度策略计算的优先级,因此即使普通进程和实时进程有相同静态优先级,其普通优先级也是不同的。
    (3)调度器的优先级保存在prio。

    注意,进程分支时,子进程会继承普通优先级。

    rt_priority表示实时进程的优先级,该值不会代替前面prio、normal_prio等值,最低的优先级为0,最高为99。

    sched_class表示进程所属的调度类。

    调度器不限于调度进程,还可以调度更大的实体,如进程组调度,这里将cpu时间在进程组之间分配,接下来再在组内分配。这里调度器不直接操作进程,而是处理可调度实体,由sched_entity表示。

    policy保存进程的调度策略,如完全公平的调度。

    cpu_allowed是个位域,在多处理器时,用于限制进程可在哪个cpu运行。

    run_list和time_slice时循环实时调度器所需要的,但不用于完全公平调度器。

    2、调度器类

    对每个调度类,都必须提供sched_class的一个实例,调度器请求的操作都有指针表示,调度器不要了解不同调度类的内部工作。

    调度类之间的层次优先级:实时进程 > 完全公平调度 > 空闲进程,sched_class的next,就是按这个顺序链接起来的,这个层次结构在编译时已经建立,不能在运行时增加新调度类的机制。

    3、就绪队列

    就绪队列:核心调度器用于管理活动进程的主要数据结构,每个活动进程只出现在一个就绪队列。另外,进程不是就绪队列的成员直接管理的,进程由调度类管理,就绪队列潜入了特定于调度类的子就绪队列。

    就绪队列的核心成员及解释:
    这里写图片描述

    nr_running,指定了队列上可运行的进程数目,不考虑优先级和调度类。
    load 就绪队列当前负荷的度量。
    cpu_load 用于跟踪此前的负荷状态。
    cfs和rt是嵌入的子就绪队列,分别用于完全公平调度器和实时调度器。
    curr 指向当前运行的进程的task_struct实例。
    idle 指向空闲进程的task_struct实例。
    clock和prev_raw_clock用于实现就绪队列自身的时钟,每次调用周期性调度器时,都会更新clock的值。
    

    4、调度实体

    load 指定了权重,决定了实体占队列总负荷的比例。
    run_node 标准的树结点,实体可以在红黑树上排序。
    on_rq 表示该实体当前是否在就绪队列上接受调度。
    sum_exec_runtime用于记录进程运行时,消耗的cpu时间,用于完全公平调度。
    在进程被撤销cpu时,当前sum_exec_runtime的值保存到prev_exec_runtime中。
    

    2.5.3 处理优先级

    1、优先级的内核表示
    进程的nice值在-20和+19之间(包含),值越低优先级越高。
    内核用0至139(包含),表示内部优先级,值越低优先级越高。0-99用于实时进程,nice值[-20,19]映射到范围[100,139],实时进程优先级总是比普通进程优先级高。
    
    2、计算优先级
    进程的优先级计算,要考虑static_prio、normal_prio、prio三种优先级。
    调用相关函数计算结果,
    

    这里写图片描述

    3、计算负荷权重
    进程的重要性由进程优先级和task_struct->se.load的负荷权重。进程每降低一个nice值,则多获得10%的cpu时间,每升高一个nice值,就放弃10%的cpu时间,也就是优先级加1,权重就减少。进程加入到就绪队列时,就绪队列的负荷权重也会增加。
    
     2.5.4 核心调度器
    
    1、周期性调度器
    (1)在scheduler_tick中实现,系统活动时,内核以频率hz自动调用该函数。
    (2)没进程等待时,供电不足情况下,可以关闭周期性调度器。
    (3)主要任务是管理内核中与系统和每个进程的调度相关的统计量,另外就是激活负责当前进程的调度类的周期性调度方法。
    
    __update_rq_clock 更新rq的时钟时间戳。
    update_cup_load,更新rq->cup_load数组。
    
    2、主调度器
    将当前cpu分配给另一个进程,要调用主调度器函数schedule,从该系统调用返回后也要检查当前进程是否设置了重调度标志TIF_NEED_RESCHEDULE,如果有,内核会调用schedule。
    

    __sched前缀的用处:有该前缀的函数,都是可能调用schedule的函数,包括schedule自身。该前缀目的是将相关代码的函数编译后,放到目标文件的特定段中,.sched.text中。该信息使内核在现实栈转储或类似信息时,忽略所有与调度有关的调用。由于调度器函数调用不是普通代码流程的部分,所以这种情况下是无意义的。

    asmlinkage void __sched schedule( void );该函数的过程:
    (1)将就绪队列的当前活动进程指针保存在prev中,prev = rq->curr;
    (2)更新就绪队列的时钟,清除当前进程task_struct的重调度标志TIF_NEED_RESCHED。
    (3)判断当前进程是否在可中断睡眠状态,而且现在接收到信号,那么它将再次提升为可运行。否则,用
    deactivate_task讲进程停止。
    (4)再用put_prev_task通知调度类,但前进程要被另一进程代替。pick_next_task,选择下一个要执行的进程。
    (5)只有1个进程,是不要切换的,还让它留在cpu。要是能选择另外的进程,就用context_switch进行上下文切换。
    (6)当前进程,被重新调度回来时,检测是否要重新调度,如果要,就又重复前面(1)至(5)的步骤了。
    
    
    3、与fork的交互
    
    用fork或其变体新建进程时,调度器用sched_fork函数挂钩到该进程。在用wake_up_new_task唤醒进程时,内核调用调度类的task_new将新进程加入相应类的就绪队列。
    
    单处理器,sched_fork执行如下:
    (1)初始化新进程与调度相关的字段。
    (2)建立数据结构。
    (3)确定进程的动态优先级。
    
    4、上下文切换
    context_switch要进行如下操作:
    (1)prepare_task_switch,执行特定于体系结构的代码,为切换做准备。
    (2)switch_mm更换task_struct->mm描述的内存管理上下文。
    (3)switch_to切换处理器寄存器和内核栈。
    (4)切换前,用户空间进程的寄存器进入和心态时保存在内核栈上,在上下文切换时,内核栈的值自动回复寄存器数据,再返回用户空间。
    
    展开全文
  • 深入理解linux内核架构(内存管理)

    千次阅读 2017-02-20 15:43:52
    1:在内核使用高端内存页之前,必须使用下文讨论的kmap和kunmap函数将其映射到内存虚拟地址空间中。 2:UMA计算机(一致内存访问,uniform memory access)将可用内存以连续方式组织起来。 3:NUMA计算机(非一致性...
    1:在内核使用高端内存页之前,必须使用下文讨论的kmap和kunmap函数将其映射到内存虚拟地址空间中。
    2:UMA计算机(一致内存访问,uniform memory access)将可用内存以连续方式组织起来。
    3:NUMA计算机(非一致性内存访问,non-uniform memory access)系统的各个CPU都有本地内存,可支持特别快速的访问,各个处理器之间通过总线连接起来,以支持其他CPU的本地内存的访问。


    4:内核会区分三种配置选项:FLATTMEM,DISCONTIGMEM,SPARSEMEM,DISCONTIGMEM.


    5:内存划分为结 。每个节点关联到系统中的一个处理器。在内核中表示为pg_data_t的实例。
    6:各个结点又划分为 内存域,是内存域的进一步细分。
    7:
    注:zonelist:指向zonelist数据结构的指针,该数据结构按照优先次序描述了适于内存分配的内存管理区。
    8:


    1)ZONE_DMA标记适合DMA的内存域。
    2)ZONE_DMA32标记了使用32位地址字可寻址,适合DMA的内存域。
    3)ZONE_NORMAL标记了可直接映射到内核段的普通内存域,这是在所有体系结构上保证都会存在的唯一内存域,但无法保证该地址范围对应了实际的物理内存。
    4)ZONE_HIGHMEM标记了超出内核段的物理内存。
    5)伪内存域ZONE_MOVABLE.
    6)MAX_NR_ZONES充当结束标志,在内核想要迭代系统中的所有内存域时,会用到该常量。
    7)各个内存域都关联了一个数组,用来组织属于该内存域的物理内存页(页帧)。对每个页帧,都分配一个struct page实例以及所需的管理数据。
    8)每个节点都提供了一个备用列表(借助struct zonelist)。该列表包含了其他节点(和相关的内存域),可用于代替当前节点分配内存。

    3.2.2数据结构
    1)结点管理
         pg_data_t用于表示节点的基本元素。

      
    1. <mmzone.h>
    2. typedef struct pglist_data{
    3. struct zone node_zones[MAX_NR_ZONES];
    4. struct zonelist node_zonelists[MAX_ZONELISTS];
    5. int nr_zones;
    6. struct page*node_mem_map;
    7. struct bootmem_data *bdata;
    8. unsigned long node_start_pfn;
    9. unsigned long node_present_pages;/*物理内存页的总数*/
    10. unsighed long node_spanned_pages;/*物理内存页的总数,包含洞在内*/
    11. int node_id;
    12. struct pglist_data *pgdat_next;
    13. wait_queue_head_t kswapd_wait;
    14. struct task_struct *ksward;
    15. int ksward_max_order;
    16. }pg_data_t;
    注:1)node_zones是一个数组,包含了节点中各内存域的数据结构。
           2)node_zonelists指定了备用节点及其内存域的列表,以便在当前节点没有可用空间时,在备用节点分配内存。
           3)节点中不同内存域的数目保存在nr_zones中
           4)node_mem_map是指向page实例数组的指针,用于描述节点的所有物理内存页,它包含了节点中所有内存域的页。
           5)bdata指向自举内存分配器数据结构的实例。
           6)node_start_pfn是该NUMA节点第一个页帧的逻辑编号。所有页帧是依次编号的,每个页帧的号码都是全局唯一的。
                 在UMA中总是0.
           7)node_present_pages指定了节点中页帧的数目,node_spanned_pages则给出了该节点以页帧 为单位计算的长度。
           8)node_id是全局节点ID。
           9)pgdat_next连接到下一个内存节点,系统中所有的内存节点都通过单链表连接起来,其末尾通过空指针标记。
          10)kswapd_wait是交换守护进程(swap daemon)的等待队列,将在页帧换出节点时会用到。
                  kswapd指向负责该节点的交换守护进程的task_struct.
                  kswapd_max_order用于页交换子系统的实现,用来定义需要释放的区域的长度。
          11)节点的内存域保存在node_zones[MAX_NR_ZONES].该数组总是有3项。即使节点没有那么多内存域,若不足3个,则其余的数组项用0填充。
       
    1. <nodemask.h>
    2. enum node_states{
    3. N_POSSIBLES, /*节点在某个时候可能变成联机*/
    4. N_ONLINE, /*节点是联机的*/
    5. N_NORMAL_MEMORY, /*几点有普通内存域*/
    6. #ifdef CONFIG_HIGHMEM
    7. N_HIGH_MEMORY, /*节点有普通或高端内存域*/
    8. #else
    9. N_HIGH_MEMORY = N_NORMAL_MEMORY,
    10. #endif
    11. N_CPU, /*节点有一个或者多个CPU*/
    12. NR_NODE_STATES
    13. };
    注:如果节点有普通或高端内存则使用N_HIGH_MEMORY,仅当节点没有高端内存才设置N_NORMAL_MEMORY.
    2)内存域
    内核使用zone结构来描述内存域。
       
    1. <mmzone.h>
    2. struct zone{
    3. /*通常由页分配器访问的字段*/
    4. unsigned long pages_min,pages_low,pages_high; 注:1)若空闲页的数目多于pages_high,则内存域的状态是理想的
    5. 2)若空闲页的数目低于pages_low,则内核开始将页换出到硬盘
    6. 3)若空闲页的数目低于pages_min,内存域急需空闲页,需页回收 
    7. 4)数据结构中水印值得填充由init_per_zone_pages_min处理。
    8. 5)setup_per_zone_pages_min设置struct zone的pages_min,
    9. pages_low,pages_high成员。
    10. unsigned long lowmem_reserve[MAX_NR_ZONES]; 注:该数组分别为各种内存域指定了若干项,用于一些无论如何都不能失败 的关键性内存分配。
    11. struct per_cpu_pageset pageset[NR_CPUS]; 注:该数组用于实现每个CPU热/冷页帧列表。内核使用这些列表保存可 用于满足实现的“新鲜”页。
    12. 热页帧:在高速缓存中,可以快速访问,
    13. 冷页帧:不在高速缓存中的页帧
    14. NR_CPUS是一个可以在编译时间配置的宏常数。
    15. 注:数组元素的类型为per_cpu_pageset
    16.        
      1. <mmzone.h>
      2. struct per_cpu_pageset{
      3. struct per_cpu_pages pcp[2];/*索引0对应热页,索引1对应冷页*/
      4. }__cacheline_aligned_in_smp;
      注:该结构由一个带有数组项的数组构成,第一项管理热页。第二页管理冷页。
      有用的数据保存在per_cpu_pages中。
             
      1. <mmzones.h>
      2. struct per_cpu_pages{
      3. int count; /*与该列表相关的页的数目*/
      4. int high; /*high是页数上限水印,在需要的时候清空列表。若count的值超出high,即列表中的 页数太多*/
      5. int batch; /*添加/删除多项页的时候,块的大小*/
      6. struct list_head list; /*list是页的双链表,保存当前CPU的冷页或者热页*/
      7. }


    1. /*
    2. *不同长度的空闲区域
    3. */
    4. spinlock_t lock;
    5. struct free_area free_area[MAX_OEDER]; 注:是同名数据结构的数组,用于实现伙伴系统,每个数组元素都表示某种 固定长度的一些连续的内存区。对于包含在每个区域中的空闲内存页 的管理。free_area是一个起点。
    6. ZONE_PAGGING(_pad1_)
    7. /*通常由页面收回扫描程序访问的字段*/
    8. spinlock_t lru_lock;
    9. struct list_head active_list; 注:是活动页的集合
    10. struct list_head inactive_list; 注:是不活动页的集合
    11. unsighed long nr_scan_active; 注:回收内存时需要扫描的活动页的数目
    12. unsighed long nr_scan_inactive; 注:回收内存时需要扫描的不活动页的数目
    13. unsighed long pages_scanned; 注:上次回收以来扫描过的页
    14. unsighed long flags 注:描述内存域当前状态
    15.        
      1. <mmzone.h>
      2. typedef enum {
      3. ZONE_ALL_UNERCLAIMABLE, /*所有的页都已经钉住,不能回收*/
      4. ZONE_RECLAIM_LOCKED, /*防止并发回收*/
      5. ZONE_OOM_LOCKED, /*内存域即可被回收*/
      6. }zone_flags_t;
    16.  
    17. /*内存域统计量*/
    18. atmoic_long_t vm_stat[NR_VM_STAT_ITEMS]; 注:维护了大量有关内存域的统计信息。辅助函数zone_page_state用来 读取vm_stat中的信息
    19. int prev_priority; 注:存储了上一次扫描操作扫描该内存域的优先级。扫描操作是由try_to _free_pages进行的,直至释放了足够多的页帧。
    20. ZONE_PAGGING(_pad2_)
    21. /*很少使用或大多数情况下只读的字段*/
    22. wait_queue_head_t *wait_table; 注:是一个等待队列,可用于等待某一页变为可用进程。进程排成一个队  
    23.     unsighed long wait_table_hash_nr_entries; 列,等待某些条件, 在条件为真时,内核会通知进程恢复工作。
    24. unsighed long wait_table_bits;
    25. /*支持不连续内存模型的字段*/
    26. struct pglist_data *zone_pgdat; 注:内存域和父节点之间的关联由zone_pgdat建立,zone_pgdat指向对 应的pg_list_data实例
    27. unsighed long zone_start_pfn; 注:内存域第一个页帧的索引
    28. unsighed long spanned_pages; /*总长度,包括空洞*/
    29. unsighed long present_pages; /*总长度,不包括空洞*/实际上可用的页数目
    30. /*
    31. *很少使用的字段
    32. */
    33. char *name; 注:是一个字符串,保存该内存域的惯用名称。3个选项可用,Normal,
    34. DMA,HighMem
    35. }__cacheline_maxaligned_in_smp;

    3.内存域水印的计算
    在计算水印之前,内核首先确定需要为关键性分配保留的内存空间的最小值。该值随可用内存的大小而非线性增长,并保留在全局变量min_free_kbytes中。


    注:1)高端内存域的下界SWAP_CLUSTER_MAX.
            2)它定义了分组的大小。
            3)setup_per_zone_lowmem_reserve计算出lowmem_reserve
    5.页帧
    1)页帧代表系统内存的最小单位,对内存中的每个页都会创建struct page的一个实例。
    2)page的定义
       
    1. <mm.h>
    2. struct page{
    3. unsigned long flags; /*原子标志,用于描述页的属性,有些情况下会异步更新*/
    4. atomic_t _count; /*使用计数,表示内核中引用该页的次数。若_count=0,page实例当前 不可用,可以删除。若_count>0,该实例不会从内存中删除*/
    5. union{
    6. atomic_t _mapcount; /*内存管理子系统中映射的页表项计数,用于表示页是否已经被映射,还 用于限制逆向映射搜索。在页表中有多少项指向该页。*/
    7. 注:atomic_t是个32个比特位,允许以原子方式修改其值,即不受并发访 问的影响。
    8. unsigned int inuse; /*用于slub分配器,对象的数目*/
    9. };
    10. union{
    11. struct{
    12. unsigned long private; /*由映射私有,不透明数据,内存管理会忽略该数据
    13. 1)PagePrivate,通常用buffer_heads;
    14. 2)PageSwapCache,则用于swp_entry_t;
    15. 3)PG_buddy,则用于表示伙伴系统中的阶。*/
    16. struct address_space *mapping; /*如果最低位为0,则指向inode address_space,或为NULL。
    17. 如果页映射为匿名内存,最低位置位,即mapping=1,该指针指向
    18. anon_vma对象
    19. mapping指定了页帧所在的地址空间
    20. mapping不仅能够保存一个指针,还能包含一些额外的信息,用于判断 页是否属于未关联到地址空间的某个匿名内存区*/
    21. 注:对该指针的使用时可能的,因为address_space实例总是对齐到
    22. sizeof(long)。如果该指针指向address_space实例,则可以直接使 用,如果使用了技巧将最低位设置为1,内核可以使用以下操作来恢复 指针。
    23. anon_vma=(struct anon_vma*)(mapping-PAGE_MAPPING_ANON)
    24. };
    25. ...
    26. struct kmem_cache *slab; /*用于slub分配器,指向slab的指针*/
    27. struct page *first_page; /*用于符合页的尾页,指向首页*/
    28. };
    29. union{
    30. pgoff_t index; /*在映射内的偏移量。index是页帧在映射内部的偏移量*/
    31. void *freelist;
    32. };
    33. struct list_head lru; /*换出页列表,lru是一个表头,用于在各种链表上维护该页,以便将页 按照不同类别分组,最重要的类别是活动不活动页。*/
    34. #if defined(WANT_PAGE_VIRTUAL)
    35. void *virtual; /*内核虚拟地址*/用于高端内存区域中的页,既无法直接映射到内核内存 中的页。virtual用于存储该页的虚拟地址。
    36. #endif /*WANT_PAGE_VIRTUAL*/    
    37. };
    PG_locked常数定义了标志中用于指定页锁定与否的比特位置
        1)PageLocked查询比特位是否置位。
        2)SetPageLocked设置PG_locked位,不考虑先前的状态。
        3)TestSetPageLocked设置比特位,而且返回原值。
        4)ClearPageLocked清楚比特位,不考虑先前的状态。
        5)TestClearPageLocked清除比特位,返回原值。
    PG_locked指定了页是否锁定,如果该比特位置位,内核的其他部分不允许访问该页,这防止了内存管理出现竞态条件。
    PG_error:若该页的I/O操作期间发生错误。
    PG_referenced和PG_active控制了系统使用该页的活跃程度
    PG_uptodate表示页的数据已经从块设备读取,期间没有出错。
    PG_dirty:脏位,即与磁盘上的数据相比,页的内容已经被改变。
    PG_lru实现页面回收和切换,内核使用两个最近最少使用(least recently used,lru)链表来区别活动和不活动页,如果页在其中一个链表中,则设置该比特位。
    PG_active:如果页在活动链表中,则设置该比特位。
    PG_highmem:表示页在高端内存中,无法持久映射到内存中。
    PG_private:如果page结构的private成员非空,设置该位。用于I/O的页,可使用该字段将页细分为多个缓冲区,但内核的其他部分也有各种不同的方法,将私有数据附加到页上。
    PG_writeback:如果页的内容处于向块设备回写的过程中。
    PG_slab:页时slab分配器的一部分。
    PG_swapcache:页处于交换缓存。此时,private包含一个类型为swap_entry_t的项。
    PG_reclaim:可用的内存数量变少时,内核试图周期性的回首页,即剔除不活动,未用的页。内核决定回收某个特定的页之后,设置该位。
    PG_buddy:如果页空闲且包含在伙伴系统的列表中,设置该位,伙伴系统是页分配机制的核心。
    PG_compound表示该页属于一个更大的复合页,符合页有多个毗邻的普通页组成,
    PageXXX(page)会检查页是否设置了PG_XXX位,
    SetPageXXX在某个比特位没有设置的情况下,设置该比特位,并返回原值。
    ClearPageXXX无条件的清除某个特定的比特位。
    TestClearPageXXX清除某个设置的比特位,并返回原值。

    3.3页表
    1)页表用于建立用户进程的虚拟地址空间和系统物理内存(内存,页帧)之间的关联。
    2)页表用于向每个进程提供一致的虚拟地址空间。应用程序看到的地址空间是一个连续的内存区,该表也将虚拟内存页映射到物理内存,因而支持共享内存的实现,还可以在不额外增加物理内存的情况下,将页换出到块设备来增加有效的可用内存空间。
    3)页表管理分为两个部分,第一部分依赖于体系结构,第二部分是体系结构无关的。

    3.3.1数据结构
    1)void *数据类型用于定义可能指向内存中任何字节位置的指针。
    2)内存源代码假定void *和unsigned long类型所需的比特位相同,他们之间可以进行强制类型转换
         sizeof(void *) = = sizeof(unsigned long)
    1.内存地址的分解
    BITS_PER_LONG定义用于unsigned long变量的比特位数目,因而也适用于指向虚拟地址空间的通用指针。


    注:1)PAGE_SHIFT:指针末端的几个比特位,用于指定所选页帧内部的位置,比特位的具体数目由PAGE_SHIFT指定。
            2)PMD_SHIFT:指定了页内偏移量和最后一级页表项所需比特位的总数,该值减去PAGE_SHIFT,可得最后一级页表项索引所需比特位的数目。该值表明了一个中间层页表项管理的部分地址空间的大小,即2^(PMD_SHIFT)字节。
           3)PUD_SHIFT由PMD_SHIFT加上中间层页表索引所需的比特位长度,而PGDIR_SHIFT则由PUD_SHIFT加上上层页表索引所需的比特位长度
           4)对全局页目录中的一项所能寻址的部分地址空间长度计算以2为底的对数,即为PGDIR_SHIFT.
           5)宏定义:PTRS_PER_PGD指定了全局页目录中项的数目
                                PTRS_PER_PMD对应于中间页目录
                                PTRS_PER_PUD对应于上层页目录中项的数目
                                PTRS_PER_PTE则是页表中项的数目
    PTRS_PER_XXX指定了给定目录项能够代表多少指针。

    #define PAGE_SIZE (1UL<<PAGE_SHIFT)

    #define PAGE_MASK (~(PAGE_SIZE -1))

    这里显示的是#define的另一个用途,预处理指令"#define"不仅可以定义常量,还可以定义表达式。这里的“1UL”代表的就是无符号长整形的

    意思,将“1”左移PAGE_SHIFT位实际就是2的PAGE_SHIFT次方。可以看到,在Linux内核中,若想要表达2的N次方,通常使用位移操作来实现

    。             

    PAGE_MASK是用于产生页表掩码的,当PAGE_SHIFT为12时,PAGE_SIZE的值就为0x1000,而PAGE_MASK是将PAGE_SIZE先减1,再取反,

    因此,它的值为0xfffff000.一个线性地址通过和它想与可以屏蔽掉所有的偏移位(Offset字段)。


    2.页表的格式
    1)pgd_t用于全局页目录项
    2)pud_t用于上层页目录项
    3)pmd_t用于中间页目录项
    4)pte_t用于直接页表项
    注:pmd_offset需要全局页目录项(src _pgd)和一个内存地址作为参数。它从某个中间页目录项返回一项。
           src_pgd = pmd_offset(src_pgd, address)
           内核使用32或者64位类型来表示页表项,这意味着并非表项的所有比特位都存储了有用的数据,即下一级表的基地址,多余的比特位用于保存额外的信息。

    3.特定于PTE的信息
    1)最后一级页表中的项不仅包含了指向页的内存位置的指针,还在上述的多余比特位包含了与页有关的附加信息。
         _PAGE_PRESENT指定了虚拟内存页是否存在于内存中。
        _PAGE_ACCESSED:CPU每次访问页时,会定期检查该比特位。以确认页使用的活跃程度。在读或者写之后会设置该比特位。
        _PAGE_DIRTY:表示该页是否是脏的,即页的内容是否已经修改过。
        _PAGE_FILE:数值与_PAGE_DIRTY相同,但用于不同的上下文中,即页不在内存的时候。显然,不存在的页不可能是脏的,因此可以重新解释该比特位,如果没有设置,则该项指向一个换出页的位置。
        _PAGE_USER,则允许用户空间代码访问该页,否则只有内核才可以访问。
        _PAGE_READ,_PAGE_WRITE,_PAGE_EXECUTE:指定了普通的用户进程是否允许读取,写入,执行该页中的机器代码。
        _PAGE_BIT_NX:用于将页标记为不可执行的。
        pte_present检查页表指向的页是否存在于内存中
        pte_dirty检查与页表项相关的页是否是脏的,及其内容在上次内核检查之后是否已经被修改过。
        pte_write检查内核是否写入到页
        pte_file用于非线性映射,通过操作页表提供了文件内容的一种不同视图。在pte_present返回false时,才能调用pte_file




    3.3.2.页表项的创建和操作


    3.4初始化内存管理
    3.4.1建立数据结构
    1)先决条件
         内存在mm/page_alloc.c中定义了一个pg_data_t实例(称作contig_page_data)管理所有的系统内存。
         
    1. <mmzone.h>
    2. #define NONE_DATA(nid) (&contig_page_data)
    注:尽管该宏有一个形式参数用于选择NUMA节点,但在UMA系统中只有一个伪节点,因此总是返回到同样的数据。
     2)系统启动




    注:

    3)节点和内存域初始化
         build_all_zonelists建立管理节点及其内存域所需的数据结构
        
    1. mm/page_alloc.c
    2. static int __build_all_zonelists(void *dummy) /*由于UMA系统只有一个节点,build_zonelists只调用了一次,就对内存建立了内 存域列表
    3. NUMA系统调用该函数的次数等同于节点的数目,每次调用对一个不同节点生成内 存域数据*/
    4. {
    5. int nid;
    6. for_each_online_node(nid){ /*遍历了系统中所有的活动节点*/
    7. pg_data_t *pgdat = NODE_DATA(nid); /*返回contig_page_data地址*/
    8. build_zonelists(pgdat); /*需要一个指向pgdat_t实例的指针作为参数,其中包含了节点内存配置的所有现 存信息,而新建的数据结构也会放置在其中*/
    9. ...
    10. }
    11. return 0;
    12. }
    内存想要分配高端内存,先在当前节点的高端内存中找到一个大小适当的空闲段,若失败,则查看该节点的普通内存域,如果还失败,
    则试图在该节点的DMA内存域执行分配。如果在3个本地内存域都无法找到空闲内存,则查看其他节点。
    高端内存是最廉价的,因为内核没有任何部分依赖于该内存域分配的内存。
    许多内核数据必须保存在普通内存域,而不能放置在高端内存域,所以只要高端内存没用尽,都不会从普通内存域分配内存。
    最昂贵的是DMA内存域,因为它用于外设和系统之间的数据传输。
        
    1. <mmzone.h>
    2. typedef struct pglist_data{
    3. ...
    4. struct zonelist node_zonelists[MAX_ZONELISTS]; /*对每种可能的内存域类型,都配置了一个独立的数组项,数 组项包含了类型为zonelist的一个备用列表。
    5. */
    6. ...
    7. }pg_data_t; /*建立备用层次结构的任务委托给build_zonelists,该函数 为每个NUMA节点都创建了相应的数据结构,他需要指向相关 的pg_data_t实例的指针作为参数。*/
    8. #define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES *MAX_NR_ZONES) /*该备用列表必须包含所有节点的所有域,因此由
    9. MAX_NUMNODES *MAX_NZ_ZONES项组成,外加一个用于标记列 别结束的空指针*/
    10. struct zonelist{ /*一个大的外部循环首先迭代所有的节点内存域,每个循环在
    11. zonelist数组中找到第i个zonelist,对第i个内存域计算备 用列表*/
    12. ...
    13. struct zone *zones[MAX_ZONES_PER_ZONELIST + 1];//NULL分离
    14. }
         
    1. mm/page_alloc.c
    2. static void __init build_zonelists(pg_data_t *pgdat)
    3. {
    4. int node, local_node;
    5. enum zone_type i, j;
    6. local_node = pgdat->node_id;
    7. for (i = 0; i < MAX_NR_ZONES; i++){
    8. struct zonelist *zonelist;
    9. zonelist = pgdat->node_zonelists + i; /*node_zonelists的数组元素通过指针操作寻址,实际工作则委托给build_ zonelist_node.在调用时,它首先生成本地节点内分配内存时的备用次序。*/
    10. j = (pgdat,zonelist,0,i);
    11. ...
    12. }
          
    1. mm/page_alloc.c
    2. static int __init build_zonelists_node(pg_data_t *pgdat,struct zonelist *zonelist,int nr_zones,
    3. enum zone_type zone_type)
    4. /*1)nr_zone表示从备用列表中的哪个位置开始填充新项。
    5. 2)备用列表的各项是借助于zone_type参数排序的,该参数指定了最先选择 哪个内存域,该参数的初始值是外层循环的控制变量i,其值可能是
    6. ZONE_HIGHMEM ,ZONE_NORMAL,ZONE_DMA或者ZONE_DMA32.
    7. 3)内核在build_zonelists中按分配代价从昂贵到低廉的次序,迭代了节 点中所有的内存域。
    8. 4)在build_zonelists_node中,则按照分配代价从低廉到昂贵的次序,迭 代了分配代价不低于当前内存域的内存域。*/
    9. {
    10. struct zone*zone;
    11. do{
    12. zone = pgdat->node_zones + zone_type;
    13. if (populated_zone(zone)){ /*在build_zonelists_node的每一步中,都对所选的内存域调用populated_z one,确认zone->present_pages大于0,即确认内存域中确实有页存在。将指 向zone实例的指针添加到zonelist->zones中的当前位置,后备列表的当前 位置保存在nr_zone*/
    14. zonelist->zones[nr_zones++] = zone;
    15. }
    16. zone_type--; /*在每一步结束时,都将内存域类型减1,即设置为一个更昂贵的内存域类型*/
    17. }while(zone_type >= 0);
    18. return nr_zones;
    19. }
           
    1. build_zonelists_node时,会执行以下赋值
    2. zonelist->zones[0] = ZONE_HIGHMEM;
    3. zonelist->zones[1] = ZONE_NORMAL;
    4. zonelist->zones[2] = ZONE_DMA;


    第一步之后,列表中的分配目标是高端内存,接下来是第二个节点的普通和DMA内存域。
        
    1. mm/page_alloc.c
    2. static void __init build_zonelist(pg_data_t *pgdat)
    3. {
    4. ...
    5. for (node = local_node + 1;node < MAX_NUMNODES;node++){
    6. j = build_zonelist_node(NODE_DATA(node),zonelist j i); /*依次迭代大于当前节点编号的所有节点*/
    7. }
    8. for (node = 0;node < local_node;node++){ /*对所有编号小于当前节点的节点生成备用列表项*/
    9. j = build_zonelists_node(NODE_DATA(node),ji);
    10. }
    11. zonelist->zones[j] = NULL;
    12. }
    13. }
    注:1)第一个循环依次迭代大于当前节点编号的所有节点。
            2)有四个节点编号副本为0,1,2,3.此时只剩下节点3
            3)新的项通过build_zonelists_node被加到备用列表,此时j的作用就体现出来了。在本地节点的备用目标找到之后,该变量的值是3,该值用作新项的起始位置。
            4)如果节点3也由内存域组成,备用列表在第二个循环之后情况如图3-9的第二步所示。
            5)如果这些节点也有3个内存域,则循环完之后备用列表的情况如图3-9下半部分所示。
            6)列表的最后一项赋值为空指针,显示标记列表结束。
            7) 对总数N个节点中节点m来说,内核生成备用列表时,选择备用节点的顺序总是:m,m+1,m+2,......,N-1,0,1,.......m-1


    3.4.2特定于体系结构的设置
    1)内核在内存中的布局
    1)配置选项PHYSICAL_START用于确定内核在内存中的位置,会受到配置选项PHYSICAL_ALIGN设置的物理对齐方式的影响。
    2)内核可以连编为可重定位二进制程序,在这种情况下完全忽略编译时给定的物理起始地址。


    注:1)前4KB是第一个页帧,通常留给BIOS使用。
            2)使用0x100000作为起始地址,这对应于第二兆字节的开始处。
            3)内核占据的内存分为几个段,其边界保存在变量中。
                  1)_text和_etext是代码段的起始和结束地址,包含了编译后的内核代码。
                  2)数据段位于_etext和_edata之间,保存了大部分内核变量。
                  3)初始化数据在内核启动过程结束后不再需要,保存在最后一段,从_edata到_end.
                  4)在内核初始化完成后,其中的大部分数据都可以从内存删除,给应用程序留出更多空间。这一段内存区划分为更小的子区间,以控制哪些可以删除,哪些不能删除。
                  5)只有起始地址_text总是相同的。
                  6)每次编译内核时,都生成一个文件System.map并保存在源代码目录下,除了所有其他(全局)变量,内核定义的函数和例程的地址,该文件还包含以上常量。
       
    1. $cat System.map 查看_text,_etext,_edata
    2. $cat /proc/iomem 查看物理内存划分的各个段的信息
    2.初始化步骤 


    注:1)该图只包括与内存管理相关的函数调用。
           2)start_kernel内部调用setup_arch
           3)调用machine_specific_memory_setup,创建一个列表,包括系统占据的内存区和空闲内存区
           4)内核提供了一个特定于机器的函数,定义在include/asm-x86/mach-type/setup.c.type中,type可以是default,voyager或者visws。
           5)在系统启动时,找到的内存区由内核函数print_memory_map显示。
           6)内核接下来用parse_cmdline_early分析命令行,主要关注类似mem=XXX[KkmM], high mem=XXX[KkmM],
    memmap=XXX[KkmM], @ XXX[KkmM]之类的参数。
           7)highmem允许修改检测到的高端内存域长度值。
           8)下一个主要步骤在setup_memory中进行。一个用于不连续内存系统(在arch/x86/mm/disconting_32.c),一个是连续内存系统( 在arch/x86/kernel/setup_32.c
                 1)确定(每个节点)可用的物理内存页的数目
                 2)初始化bootmem分配器
                 3)接下来分配各种内存区
           9)paging_init初始化内核页表并启用内核分页。
                   (1)pagetable_init该函数确保了直接映射到内核地址空间的物理内存被初始化。
          11)低端内存中的所有页帧都直接映射到PAGE_OFFSET之上的虚拟内存区。这使得内核无需处理页表,即可寻址相当一部分可用内存。
          12)调用zone_sizes_init会初始化系统中所有节点的pgdat_t实例。
                   (1)使用add_active_range,对可用的物理内存建立一个相对简单的列表。
                   (2)体系结构无关的函数free_area_init_nodes接下来使用该信息建立完备的内核数据结构。

    AMD64计算机内存有关的初始化次序


    注:1)基本的内容设置并不需要任何特定于计算机类型的处理,总是可以调用setup_memory_region完成。
           2)调用add_active创建可用内存的一个简单列表
           3)内核接下来调用init_memory_mapping将可用的物理内存直接映射到虚拟地址空间中从PAGE_OFFSET开始的内核部分。
           4)contig_initmem_init负责激活bootmem分配器。
           5)最后一个函数paging_init,它并不初始化分页机制,只是处理一些稀疏内存系统的设置例程。
                     该函数调用free_area_init_nodes,负责初始化内核管理物理页帧的数据结构。这是一个体系结构无关的函数,依赖于前面add_active_range提供的信息。

    3.分页机制的初始化
    1)paging_init负责建立只能用于内核的页表,用户空间无法访问。
    2)内核总是将总4GB可用虚拟空间按3:1的比例划分,低端3GB用于用户态应用程序,而高端的1GB则用于内核。     
    3)在用户应用程序的执行切换到核心态时,内核必须装载在一个可靠的环境中,因此有必要将地址空间的一部分分配给内核专用。
    4)物理内存页则映射到内核地址空间的起始处,以便内核直接使用。而无需复杂的页表操作。

         虽然用于用户层过程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的。


         (1)地址空间的划分
                  按3:1的比例划分地址空间,内核地址空间自身又分为各个段。

                   
    注:该图给出了用来管理虚拟地址空间的第4个G字节的页表项的结构,它表明了虚拟地址空间的各个区域的用途,这与物理内存的分配无关。
        
    1. 简单的说:
    2. kmallocvmalloc是分配的是内核的内存,malloc分配的是用户的内存
    3. kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,malloc不保证任何东西(这点是自己猜测的,不一定正确)
    4. kmalloc能分配的大小有限,vmallocmalloc能分配的大小相对较大。内存只有在要被DMA访问的时候才需要物理上连续
    5. vmallockmalloc要慢
    6. 详细的解释:
    7. 对于提供了MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB
    8. 进程的4GB内存空间被人为的分为两个部分--用户空间与内核空间。用户空间地址分布从03GB(PAGE_OFFSET,在0x86中它等于0xC0000000),3GB4GB为内核空间。
    9. 内核空间中,从3Gvmalloc_start这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等),比如我们使用 VMware虚拟系统内存是160M,那么3G3G+160M这片内存就应该映射物理内存。在物理内存映射区之后,就是vmalloc区域。对于 160M的系统而言,vmalloc_start位置应在3G+160M附近(在物理内存映射区与vmalloc_start期间还存在一个8Mgap 来防止跃界),vmalloc_end的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射)
    10. kmallocget_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址:
    11. #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
    12. extern inline unsigned long virt_to_phys(volatile void * address)
    13. {
    14.  return __pa(address);
    15. }
    16. 上面转换过程是将虚拟地址减去3GPAGE_OFFSET=0XC000000)。
    17. 与之对应的函数为phys_to_virt(),将内核物理地址转化为虚拟地址:
    18. #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
    19. extern inline void * phys_to_virt(unsigned long address)
    20. {
    21.  return __va(address);
    22. }
    23. virt_to_phys()和phys_to_virt()都定义在include/asm-i386/io.h中。
    24. vmalloc申请的内存则位于vmalloc_startvmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。
    25. 我们用下面的程序来演示kmallocget_free_pagevmalloc的区别:
    26. #include <linux/module.h>
    27. #include <linux/slab.h>
    28. #include <linux/vmalloc.h>
    29. MODULE_LICENSE("GPL");
    30. unsigned char *pagemem;
    31. unsigned char *kmallocmem;
    32. unsigned char *vmallocmem;
    33. int __init mem_module_init(void)
    34. {
    35. //最好每次内存申请都检查申请是否成功
    36. //下面这段仅仅作为演示的代码没有检查
    37. pagemem = (unsigned char*)get_free_page(0);
    38. printk("<1>pagemem addr=%x", pagemem);
    39. kmallocmem = (unsigned char*)kmalloc(100, 0);
    40. printk("<1>kmallocmem addr=%x", kmallocmem);
    41. vmallocmem = (unsigned char*)vmalloc(1000000);
    42. printk("<1>vmallocmem addr=%x", vmallocmem);
    43. return 0;
    44. }
    45. void __exit mem_module_exit(void)
    46. {
    47. free_page(pagemem);
    48. kfree(kmallocmem);
    49. vfree(vmallocmem);
    50. }
    51. module_init(mem_module_init);
    52. module_exit(mem_module_exit);
    53. 我们的系统上有160MB的内存空间,运行一次上述程序,发现pagemem的地址在0xc7997000(约3G+121M)、kmallocmem 地址在0xc9bc1380(约3G+155M)、vmallocmem的地址在0xcabeb000(约3G+171M)处,符合前文所述的内存布局。
    注:1)地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中,由于内核地址空间从偏移量0xc0000000开始,及经常提到的3GB,每个虚拟地址x都对应于物理地址x-0xc0000000,因此这是一个简单的线性平移。
           2)直接映射区域从0xc0000000到high_memory地址。
           3)若物理内存超过896MB,则内核无法直接映射全部的物理内存,该值可能比此前提到的最大限制1GB还小,因为内核必须保留地址空间的最后128MB用于其他目的。
           4)将这128MB+直接映射的896MB内存,则得到内核虚拟地址空间的总数为1024MB=1GB。
           5)内核使用两个经常使用的缩写normal和highmem,来区分是否可以直接映射的页帧。
           6)内核移植的每个体系结构必须提供两个宏,用于一致映射的内核虚拟内存部分,进行物理和虚拟地址之间的转换。
                __pa(vaddr)返回与虚拟地址vaddr相关的物理地址
                __va(vaddr)则计算出对于物理地址paddr的虚拟地址
               - - - - - - - - 两个函数都用void指针和unsigned long操作。只能用于其中的一直映射部分,不适用于处理虚拟地址空间的任意地址。
         
    1. 页帧映射到从PAGE_OFFSET开始的虚拟地址空间。
    2. include/asm-x86/page_32.h
    3. #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET) /* __pa(vaddr)返回与虚拟地址vaddr相关的物理地址*/
    4. #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET)) /*__va(vaddr)则计算出对于物理地址paddr的虚拟地址*/
    内核最后的128MB
    (1)虚拟内存中连续,但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理地址。
    (2) 持久映射用于将高端内存域中的非持久页映射到内核中。
    (3) 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择。
    __VMALLOC_RESREVE设置了vmalloc区域的长度
    MAXMEM则表示内核可以直接寻址的物理内存的最大可能数量。
          
    1. 直接映射的边界由high_memory指定
    2. arch/x86/kernel/setup_32.c
    3. static unsigned long __init setup_memory(void)
    4. {
    5. ...
    6. #ifdef CONFIG_HIGHMEM
    7. high_memory = (void *) __va(highstart_pfn * PAGE_SIZE-1)+1;
    8. #else
    9. high_memory = (void *) __va(max_low_pfn *PAGE_SIZE-1)+1; /*max_low_pfn指定了物理内存数量小于896MB的系统上的
    10. 内存页的数目。该值的上界受限于896MB可容纳的最大页数
    11. (具体的计算在find_max_low_pfn给出)。如果启用高端
    12. 内存的支持,则high-memory表示两个内存区之间的边界,
    13. 总是896MB*/
    14. #endif
    15. ...
    16. }
    若VMALLOC_OFFSET取最小值,那么在直接映射的所有内存页和用于非连续分配的区域之间,会出现一个缺口。
          
    1. include/asm-x86/pgtable_32.h
    2. #define VMALLOC_OFFSET (8*1024*1024)
    这个缺口可用作针对任何内核故障的保护措施,如果访问越界地址,则访问失败并产生一个异常。
          
    1. VMALLOC_STARTVMALLOC_END定义了vmalloc区域的开始和结束,该区域用于物理上不连续的内核映射。
    2. #define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1))
    3. #ifdef CONFIG_HIGHMEM
    4. #define VMALLOC_END (PKMAP_BASE-2*PAGE_SIZE)
    5. #else
    6. #define VMALLOC_END (FIXADDR_START-2*PAGE_SIZE)
    7. #endif
    注:1)vmalloc区域的起始地址。取决于在直接映射物理内存时,使用了多少虚拟地址空间内存。
           2)两个区域之间至少为VMALLOC_OFFSET的一个缺口,而且vmalloc区域从可被VMALLOC_OFFSET整除的地方开始。
           3)vmalloc区域在何处结束取决于是否启用了高端内存支持。如果没有启用,那么就不需要持久映射区域,因为整个物理内存都可以直接映射。
          4)根据不同的配置,该区域结束于持久内核映射或者固定映射区域的起始处。


    注:PKMAP_BASE定义了其起始地址
           LAST_PKMAP定义了容纳该映射所需的页数。
          最后一个内存段由固定映射占据,这些地址指向物理内存中的随机位置,固定映射区域延伸到虚拟地址空间的顶端。






    ***备选划分方式
    ***划分虚拟地址空间




    注:1)pagetable_init首先初始化系统的页表,以swapper_pg_dir为基础(该变量此前用于保存临时数据)
                ***对超大页的支持,这些特别标记的页,其长度为4MB,而不是普通的4KB。该选项用于不会换出的内核页。增加页的大小,意味着需要的页表项变少,这对地址转换后备缓冲器(TLB)的影响是正面的,可以减少其中来自内核的缓存项
                ***如有可能,内核页会设置另一个属性(_PAGE_GLOBAL),这也是__PAGE_KERNEL和__PAGE_KERNEL_EXEC变量中__PAGE_GLOBAL比特位已经置位的原因。这些变量指定内核身份分配页帧时的标志集,因此这些设置会自动的应用到内核页。
               在上下文切换期间,设置了_PAGE_GLOBAL位的项,对应的TLB缓存项不从TLB刷出。内核总是出现在虚拟地址空间中同样的位置。
                ***借助于kernel_physical_mapping_init,将物理内存页(或前896MB)映射到虚拟地址空间中从PAGE_OFFSET开始的位置。内核接下来扫描各个页目录项的所有相关项,将指针设置为正确的值。
                ***接下来建立固定映射项和持久内核映射对应的内存区。
                在用pagetable_init完成页表中初始化之后,则将CR3寄存器设置为指向全局页目录(swapper_pg_dir)的指针,此时必须激活新的页。
                ***由于TLB缓存项仍然包括启动时分配的一些内存地址数据,此时也必须刷出。__flush_all_tlb可完成所需的工作。与上下文切换期间相反,设置了__PAGE_GLOBAL位的页也要刷出。
               kmap_init初始化全局变量kmap_pte.
                ***在从高端内存域将页映射到内核地址空间时,会使用该变量存入相应内存区的页表项。
                ***用于高端内存内核映射的第一个固定映射内存区的地址保存在全局变量kmem_vstart中。
    ***冷热缓存的初始化
          per-CPU(或冷热)缓存,我们来处理相关数据结构的初始化,以及用于控制缓存填充行为的“水印”的计算。
          zone_pcp_init负责初始化该缓存。
          该函数由free_area_init_nodes调用。
          
    1. mm/page_alloc.c
    2. static __devinit void zone_pcp_init(struct zone *zone) /*负责初始化该缓存*/
    3. {
    4. int cpu
    5. unsigned long batch = zone_batchsize(zone); /*算出批量大小(用于计算最小和最大填充水平的基础)后,代码将遍 历所有的CPU,同时调用setup_pageset*/
    6. for (cpu = 0;cpu<NR_CPUS;cpu++){
    7. setup_pageset(zone_pcp(zone,cpu),batch); /*遍历所有的CPU,调用setup_pageset填充每个per_cpu_pageset实 例的常量。在调用该函数时,使用了zone_pcp宏来选择与当前CPU相 关的内存域的pageset实例。/
    8. }
    9. if (zone->present_pages)
    10. printk(KERN_DEBUG "%s zone:%lu pages,LIFO batch:%lu\n",zone->name,zone->present_pages,batch)
           
    1. mm/page_alloc.c
    2. static int __devinit zone_batchsize(struct zone *zone)
    3. {
    4. int batch;
    5. batch = zone->present_pages / 1024;
    6. if (batch * PAGE_SIZE > 512 * 1024)
    7. batch = (512 * 1024) / PAGE_SIZE;
    8. batch /= 4;
    9. if (batch < 1)
    10. batch = 1;
    11. batch = (1 << (fls(batch + batch/2)-1))-1; /*移位操作确保计算结果具有2^n-1的形式。fls是一个特定计算机的操作, 用于算出一个值中置位的最低比特位。*/
    12. return batch;
    13. }
    注:内存域中的内存数量超出512MB时,批量大小并不增长。
          
    1. mm/page_alloc.c
    2. inline void setup_pageset(struct_cpu_pageset *p,unsigned long batch)
    3. {
    4. struct per_cpu_pages *pcp;
    5. memset(p,0,sizeof(*p));
    6. pcp = &p->pcp[0]; /*热页。对热页来说,下限为0,上限为6*batch,缓存中页的平均数量大约是4*batch
    7. batch*4相当于内存域中页数的千分之一(这也是zone_batchsize试图将批量大小优化到总 页数0.25%的原因。)*/
    8. pcp->count = 0;
    9. pcp->high = 6*batch;
    10. pcp->batch = max(1UL,1*batch); /*无符号长整形1*/
    11. INIT_LIST_HEAD(&pcp->list);
    12. pcp = &p->pcp[1]; /*冷页。冷页列表的水印稍低一点。因为冷页并不放置到缓存中,只用于一些不太关注性能的操 作,其上限值是batch值的两倍。*/
    13. pcp->sount = 0;
    14. pcp->high = 2*batch;
    15. pcp->batch = max(1UL,batch/2); /*决定了再重新填充列表时,有多少页会立即使用。一般会向列表中添加连续的多页,而不是 单页。*/
    16. INIT_LIST_HEAD(&pcp->list);
    17. }
    18. /*在zone_pcp_init结束后,会输出各个内存域的页数以及计算的批量的大小,从启动日志可 以看到。*/

    ***注册活动内存区
    各个体系结构只需要注册所有活动内存的一个简单表,通用代码则据此生成主数据结构。
    任何一个体系结构,如果打算利用内核提供的一般性框架,则需要设置配置选项ARCH_POPULATES_NODE_MAP,在注册所有活动内存区之后,其余的工作由通用的内核代码完成。
    活动内存区就是不包含空洞的内存区,必须使用add_active_range在全局变量early_node_map中注册内存区。

          
    1. mm/page_alloc.c
    2. static struct node_active_region __meminitdata early_node_map[MAX_ACTIVE_REGIONS]; /*不同内存区的最大数目由
    3. MAX_ACTIVE_REGION给出。该值可以由
    4. 特定于体系结构的代码使用
    5. CONFIG_MAX_ACTIVE_REGIONS设置。/
    6. static int __meminitate nr_nodemap_entries; /*当前注册的内存区数目记载在
    7. nr_nodemap_entries中。*/
           
    1. <mmzone.h>
    2. struct node_active_region{
    3. unsigned long start_pfn; /*start_pfn标记了连续内存区中的第一个页帧*/
    4. unsigned long end_pfn; /*end_pfn标记了连续内存区中的最后一个页帧*/
    5. int nid; /*nid是该内存区所属节点的NUMA ID。UMA设置为0*/
    6. }
            
    1. 活动内存区是使用add_active_range注册的。
    2. mm/page_alloc.c
    3. void __init add_active_range(unsigned int nid,unsigned long start_pfn,unsigned long end_pfn)
    4. 注:在注册两个毗邻的内存区时,add_active_region会确保将他们合并为一个。此外该函数不提供其他的额外的功能特性。
    ***在IA-32上注册内存区
    除了调用add_active_range之外,zone_size_init函数以页帧为单位,存储了不同内存区的边界。
          
    1. arch/x86/kernel/setup_32.c
    2. void __init zone_sizes_init(void)
    3. {
    4. unsigned long max_zone_pfns[MAX_NR_ZONES];
    5. memset (max_zone_pfns, 0,sizeof(max_zone_pfns));
    6. max_zone_pfns[ZONE_DMA] = virt_to_phys((char *)MAX_DMA_ADDRESS >> PAGE_SHIFT; /*DMA操作的最高内存地址。该常数声 
    7. 明为PAGE_OFFSET+0X1000000
    8. 物理内存页映射到从PAGE_OFFSET开始的虚拟地址空间 ,而物理内存的前16MB适合于DMA操作,16进制表示就 是前0x1000000字节。用virt_to_phys转换,可以获得 物理内存地址,而右移PAGE_SHIFT位则相当于除以页的 大小。计算到最后得到适用于DMA的页数。*/
    9. max_zone_pfns[ZONE_NORMAL] = max_low_pfn; /*全局变量,指定了低端内存中最高的页号。*/
    10. #ifdef CONFIG_HIGHMEM
    11. max_zone_pfns[ZONE_HIGHMEM] = highend_pfn/*全局变量,指定了低端内存中最高的页号。*/
    12. add_active_range(0, 0,highend_pfn);
    13. #else
    14. add_active_range(0, 0,max_low_pfn);
    15. #endif
    16. free_area_init_nodes(max_zone_pfns); /*合并early_mem_map和max_zone_pfns中的信息。 分别选择各个内存域中的活动内存页区,并构建体 系结构无关的数据结构*/
    17. }
    18.           
      1. max_zone_pfns值得设置由paging_init处理
      2. arch/x86/mm/init_64.c
      3. void __init paging_init(void)
      4. {
      5. unsigned long max_zone_pfns[MAX_NR_ZONES];
      6. memset(max_zone_pfns,0,sizeof(max_zone_pfns));
      7. max_zone_pfns[ZONE_DMA] = MAX_DMA_PFN;
      8. max_zone_pfns[ZONE_DMA32] = MAX_DMA32_PFN;
      9. max_zone_pfns[ZONE_NORMAL] = end_pfn;
      10. ...
      11. free_area_init_nodes(max_zone_pfns);
      12. }
      13.               
        1. 16位和32DMA内存域的页帧边界保存在处理符号中,分别对应于16MB4GB转换为页帧的值:
        2. include/asm-x86/dms_64.h
        3. /* 16MB ISA DMA内存域 */
        4. ##define MAX_DMA_PFN((16*1024*1024) >> PAGE_SHIFT)
        5. /* 4GB PCI/AGP硬件总线主控器内存域 */
        6. #define MAX_DMA_PFN ((4UL*1024*1024*1024) >> PAGE_SHIFT)
        7. end_pfn检测到的最大页帧号。由于AMD64并不需要高端内存域,max_zone_pfns中对应的项是NULL

    ***在AMD64上注册内存区
    在AMD64上注册内存区的工作分为两个函数,活动内存区的注册如下:
         
    1. arch/x86/kernel/e820_64.c
    2. e820_register_active_regions(int nid,unsigned long start_pfn,unsigned long end_pfn)
    3. {
    4. unsigned long ei_startpfn;
    5. unsigned long ei_endpfn;
    6. int i;
    7. for(i = 0;i<e820.nr_map;i++)
    8. if (e820_find_active_region(&e820.map[i],start_pfn,end_pfn.&ei_startpfn,&ei_endpfn))
    9. add_active_range(nid,ei_startpfn,ei_endpfn);
    10. }
    5.AMD64地址空间的设置
    处理器必须隐藏对未实现的地址空间的访问。一种做法是禁止使用超出物理地址空间的虚拟地址。
    硬件所采用的方案, 符号扩展(sign extension)


    注:1)虚拟地址的低47位,即[0,46],可以任意设置。
            2)比特位[47,63]的值总是相同的:或者全0,或者全1.此类地址称之为规范的。
            3)整个空间划分为3部分,下半部,上半部,二者之间的禁用区。
            4)上下两部分共同构成跨越2^48字节的一个地址空间。
            5)地址空间的下半部是[0x0,0x0000 7FFF FFFF FFFF]
            6 ) 地址空间的上半部是[0xFFF 800 0000 0000,0XFFFF FFFF FFFF FFFF].
            7)0x0000 7FFF FFFF FFFF是一个二进制数,低47位都是1,其他位都是0,因此是非可寻址区域之前的最后一个地址。
            8)0xFFFF 8000 0000 0000中,比特位[47,63]置位,从而是上半部的第一个有效地址。


    注:1)可访问的地址空间的整个下半部用作用户空间,而整个上半部专用于内核。
           2)内核地址空间起始于一个起防护作用的空洞,以防止偶然访问地址空间的非规范部分。
           3)若出现这种情况,处理器会引发一个一般性保护异常(general protection exception)
           4)物理内存页则一致性映射到从PAGE_OFFSET开始的内核空间
           5)2^46字节(由MAXMEM指定)专用于物理页帧。
          
    1. include/asm-x86/pgtable_64.h
    2. #define __AC(X,Y) (X##Y)
    3. #define _AC(X,Y) __AC(X,Y) /*_用于对给定的常数标记后缀。eg._AC(17,UL)变为(17UL)相当于把常数标记为
    4. unsigned long类型。*/
    5. #define __PAGE_OFFSET _AC(0xffff810000000000,UL)
    6. #define PAGE_OFFSET __PAGE_OFFSET
    7. #define MAXMEM _AC(0X3fffffffffffUL)
           
    1. 另一个防护性空洞位于一致性映射区和vmalloc内存区之间,后者的范围从VMALLOC_STARTVMALLOC_END
    2. 虚拟内存映射(virtual memory mapVMM)内存区紧接着vmalloc内存区之后。
    3. 只有内核使用了稀疏内存模型。
    4. VMM内存区的页表进行特定的设置,使得物理内存中所有的struct page 实例都映射到没有空洞的内存区中。
    5. include/asm-x86/pgtable_64.h
    6. #define VMALLOC_START _AC(0Xffffc20000000000,UL)
    7. #define VMALLOC_END _AC(0xffffe1ffffffffffUL)
            
    1. include/asm-x86/page_64.h
    2. #define __PHYSICAL_START CONFIG_PHYSICAL_START
    3. #define __KERNEL_ALIGN 0x200000
    4. #define __START_KERNEL (__START_KERNEL_map + __PHYSICAL_START)
    5. #define __START_KERNEL_map _AC(0xffffffff80000000,UL)
    6. #define KERNEL_TEXT_SIZE (40*1024*1024)
    7. #define KERNEL_TEXT_START _AC(0xffffffff80000000,UL)
    8. 映射模块的内存区从MODULES_VADDRMODULES_END
    9. #define MODULES_VADDR _AC(0xffffffff88000000UL)
    10. #deifne MODULES_END _AC(0xfffffffffffff00000,UL)
    11. #define MODULES_LEN(MODULES_END - MODULES_VADDR) /*该内存区可用的内存数量由MODULES_LEN计算*/
    3.4.3启动过程期间的内存管理
    1)bootmem分配器用于在启动阶段早期分配内存。
    2) 最先适配(first-fit)分配器用于在启动阶段管理内存。
                 该分配器使用一个位图来管理页,位图比特位的数目与系统中物理内存页的数目相同,比特位为1,表示已用页,比特位为 
                 0,表示空闲页。
    3) 最先最佳(first-best)或者最先适配位置
                在需要分配内存时,分配器逐位扫描位图,直到找到一个能提供足够连续页的位置,
                该过程不高效。每次分配都从头扫描比特链,因此在内核完全初始化后,不能将该分配器用于内存管理。
    1.数据结构
    1)内核(为系统中的每个节点都)提供了一个bootmem_data结构的实例来管理一些数据。
    2)该结构所需的内存无法动态分配,必须在编译时分配给内核。
    3)内存不连续的系统可能需要多个bootmem分配器。如果物理地址空间中散步着空洞,也可以为每个连续内存区注册一个bootmem分配器。
          
    1. <bootmem.h>
    2. typedef struct bootmem_data{
    3. unsigned long node_boot_start; /*保存了系统中第一个页的编号,大多数体系结构下都是0*/
    4. unsigned long node_low_pfn; /*可以直接管理的物理地址空间中最后一页的编号,即ZONE_NORMAL的结束页*/
    5. void *node_bootmem_map; /*指向存储分配位图的内存区的指针。*/
    6. unsigned long last_offset; /*如果没有请求分配整个页,则last_offset用作该页内部的偏移量。这使得bootmem分配 器可以分配小于一整页的内存区*/
    7. unsigned long last_pos; /*是上一次分配的页的编号。*/
    8. unsigned long last_success; /*指定位图中上一次成功分配内存的位置,新的分配将由此开始。*/
    9. struct list_head list;
    10. }bootmem_data_t;
    注册新的自举分配器可使用init_bootmem_core,所有注册的分配器保存在一个链表中,表头是全局变量bdata_list
    在UMA系统上,只需一个bootmem_t实例,即contig_bootmem_data.它通过bdata成员与contig_page_data关联起来
          
    1. mm/page_alloc.c
    2. static bootmem_data_t contig_bootmem_data;
    3. struct pglist_data contig_page_data = {.bdata = &contig_bootmem_data};
    ***初始化
    IA-32使用setup_memory,该函数又调用setup_memory,该函数又调用setup_bootmem_allocator来初始化bootmem分配器。而AMD64则使用contig_initmem_init


    注:1)setup_memory分析检测到的内存区,以找到低端内存区中最大的页帧号。
            2)全局变量max_low_pfn保存了可映射的最高页的编号。
            3 )setup_bootmem_allocator负责发起所有有必要的步骤,已初始化bootmem分配器。他首先调用函数init_bootmem,该函数是 init_bootmem_core的一个前端。
            4) init_bootmem_core 的目的在于执行bootmem分配器的第一个初始化步骤。





    3对内核的接口(Application Programming Interface,API)
    ***分配内存
    1)alloc_bootmem(size)和alloc_bootmem_pages(size)指按指定大小在ZONE_NORMAL内存域分配内存。
                   ————数据是对齐的,这使得内存或者 从可适用于L1高速缓存的理想位置开始,或者从边界开始。
                   ———— _pages是指数据的对齐方式。
    2)alloc_bootmem_low和alloc_bootmem_low_pages在ZONE_DMA内存域分配内存。
    3)基本上NUMA系统的API相同,但函数名增加了_node后缀,与UMA系统的函数相比,还需要一个额外的参数,指定用于内存分配的节点。
    4)这些函数都是__alloc_bootmem的前端,后者将实际工作委托给__alloc_bootmem_nopanic.
                   ————这些分配器都保存在一个全局链表中,__alloc_bootmem_core会遍历所有的分配器,直至分配成功为止。
    5)在NUMA系统上,_alloc_bootmem_node则用于实现该API函数,首先工作传递到_alloc_bootmem_core,尝试该节点的bootmem分配器进行分配,如果失败,则后退到_alloc_bootmem,并将尝试所有节点。

        
    1. mm/bootmem.c
    2. void *_init _alloc_bootmem(unsigned long size,unsigned long align,unsigned long goal)
    3. /*size是所需内存区的长度,align表示数据的对齐方式。goal指定了开始搜索适当空闲内存区的起始地址。*/
         
    1. <bootmem.h>
    2. #define alloc_bootmem(x) __alloc_bootmem((x),SMP_CACHE_BYTES, __pa(MAX_DMA_ADDRESS))/*所需分配内存的长度(x)未做任何改变直接传递
    3. 给_alloc_bootmem,但内核对齐方式有两个选
    4. */
    5. #define alloc_bootmem_low(x) __alloc_bootmem((x),SMP_CACHE_BYTES,0) /*SMP_CACHE_BYTES会对齐数据,使之在大多数
    6. 体系结构上能够理想的置于L1高速缓存中。*/
    7. #define alloc_bootmem_pages(x) __alloc_bootmem((x),PAGE_SIZE,__pa(MAX_DMA_ADDRESS)) /*PAGE_SIZE将数据对齐到页边界*/
    8. #define alloc_bootmem_low_pages(x) _alloc_bootmem((x),PAGE_SIZE,0) /*后一种对齐方式是用于分配一个或多个整页*/
    注:1)低端DMA内存与普通内存的区别在于其起始地址,搜索适用于DMA的内存从地址0开始,而请求普通内存时则从MAX_DMA_ADDRESS向上(_pa将内存地址转换为页号)
            _alloc_bootmem_core函数
         (1)从goal开始,扫描位图,查找满足分配请求的空闲内存区。
         (2)如果目标页紧接着上一次分配的页,即bootmem_data->last_pos,内核会检查bootmem_data->last_offset,判断所需的内存(包括对齐数据所需的空间)是否能够在上一页分配或从上一页开始分配。
         (3)新分配的页在位图对应的比特位设置1,最后一页的数目也保存在bootmem_data->last_pos.如果该页未完全分配,则相应的偏移量保存在bootmem_data->last_offset,否则该值设置为0.
    ***释放内存
    内核提供了free_bootmem函数来释放内存。它需要两个参数,需要释放的内存区的起始地址和长度。
        
    1. <bootmem.h>
    2. void free_bootmem(unsigned long addr,unsigned long size);
    3. void free_bootmem_node(pg_data_t *pgdat,unsigned long addr,unsigned long size);
    1)两个版本都将其工作委托给__free_bootmem_core.因为bootmem分配器没有保存有关页划分的任何信息。
    2)内核使用__free_bootmem_core首先计算完全包含在该内存区中,将被释放的页。
    3)位图中对应的项设置为0,完成的页的释放。

    4.停用bootmem分配器
    (UMA) free_all_bootmem&(NUMA)free_all_bootmem_node
    1)扫描bootmem分配器的页位图,释放每个未使用的页。
    2)到伙伴系统的接口是__free_pages_bootmem函数,该函数对每个空闲页调用。
    3)该函数依赖于标准函数__free_page
    4)它使得这些页并入伙伴系统的数据结构,在其中作为空闲页管理,可用于分配。
    5)在页位图已经完全扫描之后,它占据的内存空间也必须释放,此后,只有伙伴系统可用于内存分配。

    5释放初始化数据
    1)内核提供了两个属性(__init和__initcall)用于标记初始化函数和数据,这些必须置于函数或数据的声明之前。


    3.5物理内存的管理
    3.5.1伙伴系统的结构
    系统内存中的每个物理内存页(页帧),都对应于一个struct page实例。每个内存域都关联了一个struct zone实例。其中保存了用于管理伙伴数据的主要数组。
        
    1. <mmzone.h>
    2. struct zone{
    3. ...
    4. /*
    5. *不同长度的空闲区域
    6. */
    7. struct free_area free_area[MAX_ORDER]; /*free_area[]数组中的各个元素的索引也解释为阶,用于指定对应链表中的连续内存区 包含多少个页帧。eg.第0个链表包含的内存区为单页(2^0=1)...第3个管理的内存区 为4页,以此类推*/
    8. ...
    9. };
    10. free_area是一个辅助数据结构
    11. 定义:
    12. struct free_area{
    13. struct list_head free_list[MIGRATE_TYPES]; /*用于连续空闲页的链表,页表包含大小相同的连续内存区*/
    14. unsigned long nr_free; /*指定了当前内存区中空闲页块的数目*/
    15. };
    内存块的长度是2^order,其中order的范围从0到MAX_ORDER.
         
    1. <mmzone.h>
    2. #ifndef CONFIG_FORCE_MAX_ZONEORDER
    3. #define MAX_ORDER 11 /*该常数通常是11,即一次分配可以请求的页数最大是2^11=2048*/
    4. #else
    5. #define MAX_ORDER CONFIG_FORCE_MAX_ZONEORDER
    6. #endif
    7. #define MAX_ORDER_NR_PAGES(1<<(MAX_ORDER-1))
    内存区中第一页内的链表元素,可用于将内存区维持在链表中。


    注:1)伙伴不必是彼此连接的。
            2)若一个内存区在分配期间分解为两半,内核会自动将未用的一半加入到对应的链表中。若某个时刻,由于内存的释放,两个内存区都处于空闲状态,可通过其地址判断其是否为伙伴。
            3)基于伙伴系统的内存管理专注于某个节点的某个内存域。DMA或者高端内存域。但所有的内存域和节点的伙伴系统都通过备用分配列表连接起来。
            4)在首选的内存域或节点无法满足内存分配请求时,首先尝试同一个节点的另一个内存域,接下来再尝试另一个节点,直至满足请求。



    3.5.2避免碎片
    反碎片(anti-fragmentation):试图从一开始尽可能防止碎片。
    工作原理:
           1)不可移动页。在内存中有固定的位置,不能移动到其他的地方。
           2)可回收页。不能直接移动,但可以删除,其内容可以从某些源重新生成。
           3)可移动页。可以随意的移动。属于用户空间应用程序的页属于该类别。它们是通过页表映射的,如果他们复制到新的位置,页表项可以相应的更新,应用程序不会注意到任何事。
        
    1. <mmzone.h>
    2. #define MIGRATE_UNMOVABLE 0
    3. #define MIGRATE_RECLAIMABLE 1
    4. #define MIGRATE_MOVABLE 2
    5. #define MIGRATE_RESERVE 3
    6. #define MIGRATE_ISOLATE 4 /*不能从这里分配*/
    7. #define MIGRATE_TYPES 5 /*表示迁移类型的数目,不代表具体的区域*/
         
    1. 对伙伴系统数据结构的调整,是将空闲列表分解为MIGRATE_TYPE个列表
    2. <mmzone.h>
    3. struct free_area{
    4. struct list_head free_list[MIGRATE_TYPES];
    5. unsigned long nr_free; /*统计了所有列表上空闲页的数目,每一种迁移类型都对应于一个空闲列表*/
    6. };
          
    1. mm/page_alloc.c
    2. /*
    3. *该数组描述了指定迁移类型的空闲列表耗尽时,其他空闲列表在备用列表中的次序。
    4. */
    5. static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1]={
    6. [MIGRATE_UNMOVABLE] = {MIGRATE_RECLAIMABLE,MIGRATE_MOVABLE,MIGRATE_RESERVE},
    7. [MIGRATE_RECLAIMABLE] = {MIGRATE_UNMOVABLE,MIGRATE_MOVABLE,MIGRATE_RESERVE},
    8. [MIGRATE_MOVABLE] = {MIGRATE_RECLAIMABLE,MIGRATE_UNMOVABLE,MIGRATE_RESERVE},
    9. [MIGRATE_RESERVE] = {MIGRATE_RESERVE,MIGRATE_RESERVE,MIGRATE_RESERVE},
    10. }
    11. /*该数据结构大体上是自明的:在内核想要分配不可移动页时,如果对应链表为空,则后退到可回收页链表,接下来到可移动页链表,最后到紧急分配链表*/








    展开全文
  • 理解Linux内核最好预备的知识点:懂C语言懂一点操作系统的知识熟悉少量相关算法懂计算机体系结构Linux内核的特点:结合了unix操作系统的一些基础概念Linux内核的任务:1.从技术层面讲,内核是硬件与软
  • 深入Linux 内核架构

    2019-04-01 21:46:28
    作者:Wolfgang Mauerer 翻译:郭旭 高清电子扫面版,可以pdf文字搜索,方便查询所需要的信息。 本人又使用了Adobe Acrobat 优化了网页浏览速度 欢迎大家下载,祝大家学习愉快,开卷有益
  • 深入理解linux内核第三版 中文
  • 一文看懂Linux内核!Linux内核架构和工作原理详解

    千次阅读 多人点赞 2021-01-29 15:14:27
    5个方面分析linux内核架构,让你对内核不再陌生 90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc的原理 手把手带你实现一个Linux内核文件系统 简介 作用是将应用层序的请求传递给硬件,并充当底层驱动...

    linux内核相关视频解析:

    5个方面分析linux内核架构,让你对内核不再陌生
    90分钟了解Linux内存架构,numa的优势,slab的实现,vmalloc的原理
    手把手带你实现一个Linux内核文件系统

    简介

    作用是将应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。目前支持模块的动态装卸(裁剪)。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内核源代码包括三个主要部分:

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

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

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

    使用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 ---- 帮助、说明文档。

    【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)
    在这里插入图片描述

    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内核的体系结构
    在这里插入图片描述

    图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底层原理的中高级运维人员和嵌入式开发人员。解释透彻:深入浅出地将Linux内核架构与底层原理和盘托出。实践性强:对Linux内核形成了深刻、独到的理解。指路明灯:将...
  • 深入理解linux内核(第三版中文)
  • struct sched_entity { struct load_weight load; /* 用于负载均衡 */ struct rb_node run_node; unsigned int on_rq; u64 exec_start; u64 sum_exec_runtime; u64 vruntime; ... ......}
  • 深入理解Linux学习分享
  • 笔记:深入理解Linux内核(二)

    千次阅读 2021-12-01 21:33:39
    笔记:深入理解Linux内核(二) 二零二一年十月二十四日 文章目录笔记:深入理解Linux内核(二)第二章:内存寻址内存地址硬件中的分段段选择符和段选择器段描述符快速访问段描述符分段单元Linux中的分段Linux ...
  • Linux内核层还提供网络防火墙的框架netfilter,基于netfilter框架编写网络过滤程序是 Linux 环境下内核层网络处理的常用方法。 视频教程:内核网络协议栈架构详解 1、Linux内核源代码结构 Linux 的内核源代码可以从 ...
  •  《深入理解linux内核》第三版将引领你畅游内核中用到的最主要数据结构、算法和编程技巧。如果你的确想了解计算机内部的实现机理,那么作者透过现象探寻本质,提供了颇有价值的深入分析。本书针对具体的intel平台,...
  • 深入理解Linux内核之进程睡眠(上)

    千次阅读 2021-08-15 00:29:32
    1开场白环境:处理器架构:arm64内核源码:linux-5.10.50ubuntu版本:20.04.1代码阅读工具:vim+ctags+cscope无论是任务处于用户态还是内核态,经常会...
  • 本书很多观点分新颖,言简意赅,《深入理解Linux内核》名声不错,但过于教科书化,而且只讲数据结构不讲算法,不太直观。《Linux内核设计与实现》讲的过于笼统,内容组织也不合理。 强烈推荐此书。 本书缺点: (1) ...
  • 深入Linux内核架构(中文版)pdf

    千次阅读 2017-05-29 10:09:00
    本书讨论了Linux内核的概念、结构和实现。主要内容包括多任务、调度和进程管理,物理内存的管理以及内核与相关硬件的交互,用户空间的进程如何访问虚拟内存,如何编写设备驱动程序,模块机制以及虚拟文件系统,Ext...
  • Linux是什么? 它和Windows XP、Windows 2003、2008什么的一样就是一个操作系统而已! Linux能干什么? 它能当服务器,服务器上安装着各种企业应用、服务。 比如:Web服务(apache,就是能架设网站的)、数据库...
  • 本书很多观点分新颖,言简意赅,《深入理解Linux内核》名声不错,但过于教科书化,而且只讲数据结构不讲算法,不太直观。《Linux内核设计与实现》讲的过于笼统,内容组织也不合理。 强烈推荐此书。 本书缺点: (1) ...
  • 深入理解Linux内核进程上下文切换

    千次阅读 多人点赞 2020-09-28 08:39:19
    作者简介韩传华,就职于南京大鱼半导体有限公司,主要从事linux相关系统软件开发工作,负责Soc芯片BringUp及系统软件开发,乐于分享喜欢学习,喜欢专研Linux内核源代码。我都知道...
  • 深入理3.10 P101, 第三段 勘误: 第三段和第一段重复, 而且还漏掉了很多东西, 原书第三段翻译如下: 处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE 状态的进程被分成很多类, 每一类代表一...解Linux内核正版书籍所有勘误
  • 内核和处理器建立了这种多任务的错觉(也就是可以并行操作) 但是,这也引入了若干问题 多任务引入的问题 独立:除非明确要求,进程之间不能互相干扰 共享:公平地共享CPU,但是其中一些程序可能更重要 由此,...
  • proc文件系统属于虚拟文件系统,即该文件系统的数据,由内核动态生成,并不会存放在持久存储数据中。 proc文件系统,使得内核可以生成与系统状态和配置有关的信息。该信息可以由用户和系统程序从普通文件读取,而...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 27,068
精华内容 10,827
关键字:

深入理解linux内核架构