自学内容网 自学内容网

LINUX 内核设计于实现 阅读记录(2025.01.14)

文章目录

一、内核历史

1、内核简介

LINUX内核是一个大的完整的C程序,提供可热插拔的模块。

2、LINUX 内核与 UNIX 内核比较

3、LINUX内核版本命名

主版本号 次版本号 修订版本号

二、从内核出发

1、获取内核源码

(1)查看Linux内核版本 uname -r

在这里插入图片描述

(2)下载源码 https://www.kernel.org/

(3)编译内核

解压 xz 包

xz -d linux-5.15.176.tar.xz

解压 tar 包

tar xvf linux-5.15.176.tar

依赖库下载

sudo apt install make gcc libncurses-dev flex bison libssl-dev libelf-dev

清理源码

make mrproper

清理目标文件

make clean

配置内核选项
编译、安装

2、内核源码树

3、编译源码

4、内核开发的特点

(1)GNU C 和 ANSI C

(2)内核没有内存保护机制,而且不分页

(3)最好不要在内核中使用浮点数

(4)内核程序的栈空间比较小,在编译时设置

(5)内核支持抢占式调度

(6)内核应该又高度的可移植性

LINUX作为一个可移植的操作系统,其实现代码大部分应该是于体系结构无关的,于体系结构相关的代码应该分离出来。

5、C语言扩展的特性

(1)内联函数 inline

C99 和 CNU C 均支持 内联函数,内联函数在函数调用处展开,可以消除函数调用和返回带来的开销(寄存器的恢复和存储)。但这会增加程序占用的空间和指令缓存。
一般在要求时间、简短的函数定义为nline。
语法

static inline fun(){}

需要static修饰,必须在使用之前定义好否则无法展开,内核中优先使用内联函数而不是宏定义

(2)内联汇编

gcc支持汇编代码,用 asm声明

(3)分支声明

gcc 将分支声明封装成宏 ==》likely() unlikely(),表明一个分支是经常出现还是极少出现。
在这里插入图片描述

三、进程管理

  1. 进程 (任务) 是处于执行期间的程序、是正在执行的程序的实时结果。
  2. 线程是在进程中活动的对象,线程拥有独立的计数器、栈、和寄存器。
  3. 内核调度的线程而不是进程。 Linux对线程和进程并不特别区分,线程是特殊的进程(轻量进程)。
  4. 进程提供两种虚拟机制:虚拟处理器和虚拟内存,虚拟处理器使得进程感觉自己独享处理器,虚拟内存使进程感觉自己独享整个内存。
  5. 线程共享进程的虚拟空间,但是线程有自己的虚拟处理器(线程1v1模型的原理)
  6. fork()系统调用从内核返回两次,一次返回父进程,一次返回主进程。

1、进程描述符和任务结构

(1)进程描述符:task_struct

结构体描述符存放在 /linux/sched.h

(2)内核中任务的组织结构

  1. 内核把进程存放在任务队列中,这个任务队列是双向循环链表的结构。
  2. task_struct比较大,在32位机中大约为1.7KB,它完整的描述了一个进程的所有状态。
  3. 通过预先分配和重复使用task_struct避免动态分配和释放带来的资源消耗
  4. Linux 通过slab分配器来分配task_struct,达到对象复用和缓存着色
  5. 在栈顶创建一个 thread_info的结构体,该结构体中有指向tsak_struct的指针。通过计算栈偏移量来寻找
  6. 内核通过PID标识一个进程。
  7. 通过current宏查找当前执行的进程的进程描述符

(3)进程状态

进程描述符的state域描述了进程的当前状态。
进程有5个状态
TASK_RUNNING:运行或就绪
TASK_INTERRUPTIBLE:可中断(阻塞)
TASK_UNINTERRUPTIBLE:不可中断(阻塞),但不可被唤醒,例如sleep期间
__TASK_TRACED:被其他进程追踪
__TASK_STOPPED:进程停止,如收到STOP信号。
在这里插入图片描述
设置进程状态

set_task_state(task,state)  == set_current_state(state)

(4)进程上下文(待补充,理解不到位)

系统调用和异常是内核留给用户的接口,当系统调用时,陷入内核,内核代表进程执行,并处于进程的上下文中。
上下文:进程程序执行的环境(CPU寄存器保存的值)
上下文切换:将不通进程的内容写入寄存器并保存旧的寄存器值到前一个进程的栈中。
内核代表进程执行:内核访问内核资源,并将结果通过系统调用传递给用户进程。
https://blog.csdn.net/zysharelife/article/details/7276998

(5)进程家族树

  1. 所有的进程都是PID为1 的进程(init)的后代,内核在启动的最后阶段启动init进程。该进程读取系统的初始化脚本
    在这里插入图片描述
  2. 进程间的关系存放在进程描述符中,通过parent指针指向父进程,通过children链表指向子进程

2、进程 与线程的创建(理解linux中线程与进程的重点)

在LINUX的内核中不分进程或者线程,在创建进程时调用clone函数,为进程分配资源,通过exec函数族加载一个程序到进程的地址空间,并采用写时拷贝来加快进程的创建速度。
在创建线程时 调用pthread_create,这个函数还是调用了clone函数。
创建进程和创建线程最终都是调用了clone函数,不同之处在于调用时的参数不通,在创建线程时
在这里插入图片描述
共享父进程的内存、文件描述都、信号等
在创建进程时
在这里插入图片描述
所以由此来看,线程和进程没有本质上的区别,在内核中都是以task_struct标识,不同之处在于task_struct的域信息不同。
这也是linux中线程也可以参加系统调度的原因。线程可以看作共享父进程一些资源的进程。

在不同的系统中实现多线程的机制是不同的。
一个进程的创建分为fork和exec两步,一个进程的回收也分为资源回收和进程描述符回收两部分。

3、孤儿进程

父进程先于自己终结的进程叫做孤儿进程,系统会为孤儿进程找一个同组进程或init进程作为新的父进程。
这个过程为

  1. 先在同组遍历
  2. 找不到合适进程,就将父进程设置为init进程,找到则将找到的进程设置为父进程。
  3. 遍历所有子进程,设置父进程

四、进程调度

进程调度程序可以看作内核的子系统,负责为可运行程序分配CPU时间。
调度器的通用概念:时间片 和 优先级

1、多任务

多任务系统分类:

  1. 非抢占式多任务 cooperative multitasking
  2. 抢占式多任务 preemptive multitasking (UNIX 使用):在分配的时间片未耗尽时打断进程的执行,去执行另一个进程。

2、LINUX的进程调度

LINUX 2.5 ==>> O(1)调度程序:静态时间片算法、对于每个CPU的任务队列,但是对时间敏感的(交互进程)响应不理想。所以在服务器运行尚可,在桌面环境运行不太理想。(O1 表示时间复杂度)
LINUX 2.6 ==>> 反转楼梯最后期限调度算法 RSDL,被称为完全公平的算法 CFS。

3、策略

策略决定进程调度的时机,策略决定系统的运行效率。

(1)IO消耗型进程和CPU消耗型进程

IO消耗型进程:处理IO请求,每次运行的时间短,等待的时间长
CPU消耗型进程:多数时间在执行代码或计算数据,对于这种进程应该尽量降低它的调度频率,让其运行时间延长。
调度策略要在两者中寻求一个平衡。
LINUX 更加倾向

(2)进程优先级

LINUX采用两种范围的优先级:nice值实时优先级,两个没有关联。
nice:值范围是 -20 ~ 19 值越低优先级越高,默认0,代表可获得的时间片比例。
实时优先级:0 ~ 99 值越高优先级越高 ,实时进程的优先级高于普通进程。
在这里插入图片描述

(3)时间片

时间片是一个常值,但是这个值的指定却不好确定,时间片太大,就会导致实时性降低,时间片太小就会消耗大量时间在进程的上下文切换中。
但是LINUX的CFS调度算法并不直接将时间片分配到进程,而是将CPU的使用比例分配给进程,这样不同的进程就会的到不通的CPU时间,这个比例的值收到nice值的影响。
LINUX的抢占式调度收到进程优先级和是否有时间片决定,当来了一个新的可运行进程,如果新进程的CPU使用(消耗)比例小于当前进程且优先级更高,当前进程就会被新进程抢占。
CPU使用(消耗)比例:一定时间内进程使用CPU的时间的比值。

4、LINUX调度算法

(1)调度器类

在Linux中调度器以模块的方式提供,被称为调度器类,这样做的目的是为了给内核提供多种不通算法的调度器,每种调度器管理自己范围内的进程,每次有多个可运行进程时,先比较他的调度器的优先级,优先级高的调度器先执行自己的进程。
CFS只是针对普通进程的调度器。

(2)CFS

CFS允许所有进程(n个)平分所有的CPU时间 1/n,循环调度
CFS抢占时以新进程的运行时间是否小于当前进程的运行时间为判断条件。
CFS不是依靠nice值计算时间片,而是用nice值进程获得CPU时间的权重。
CFS规定了获取CPU时间比例的最小标准==>最小粒度
CFS规定目标延迟:每个可运行任务在处理器上至少运行一次所需的最短时间,即最小粒度
在CFS中进程所获得的CPU时间由它自己和其他所有的程的nice值得相对差值决定。

5、调度的实现

(1)时间记账

所有的调度器都必须为进程记录运行时间。
CFS使用调度器实体结构 struct sched_entity 来追踪进程运行记账。

struct sched_entity {
      /* For load-balancing: */
      struct load_weight      load;
      struct rb_node          run_node;
      struct list_head        group_node;
      unsigned int            on_rq;
  
      u64             exec_start;
      u64             sum_exec_runtime;
      u64             vruntime;
      u64             prev_sum_exec_runtime;
  
      u64             nr_migrations;
  
  #ifdef CONFIG_FAIR_GROUP_SCHED
      int             depth;
      struct sched_entity     *parent;
      /* rq on which this entity is (to be) queued: */
      struct cfs_rq           *cfs_rq;
      /* rq "owned" by this entity/group: */
      struct cfs_rq           *my_q;
      /* cached value of my_q->h_nr_running */
      unsigned long           runnable_weight;
  #endif
  
  #ifdef CONFIG_SMP
      /*
       * Per entity load average tracking.
       *
       * Put into separate cache line so it does not
       * collide with read-mostly values above.
       */
      struct sched_avg        avg;
  #endif
  };

struct sched_entity 作为一个se成员嵌入到 tsak_struct 中。
调度器实体的 vruntime成员以纳秒为单位记录进程运行的虚拟时间(获取cpu的时间),vruntime的更新由系统周期性的调用updata_curr来实现,根据vruntime可以准确的测量出进程的运行时间,确定下一个运行进程是哪一个。

(2)进程选择

  1. CFS选择进程时选择vruntime最小的进程,以达到公平。
  2. CFS使用红黑树 rbtree(自平衡二叉查找树) 来存储可运行的进程队列,利用红黑树迅速寻找最小的vruntime。
  3. 最小的vruntime就是最左边的叶子节点,并将该节点存储,这样每次不用查找直接获取它就可以了。
  4. 在进程变为可运行状态或fork后被加入CFS的红黑树。
  5. 在进程变为阻塞或终止时从CFS红黑树删除。

(3)调度器入口

linux内核调度器入口为 schedule() 函数,schedule()通常要和一个调度类关联,每个调度器有一个自己的任务队列。
在schedule内调用pick_next_task() 函数一次从优先级高到优先级低访问每个调度类,询问最高优先级的进程,最后选择最高优先级的调度类的最高优先级的进程。

(4)进程的休眠

进程休眠时将自己设置为阻塞状态,并将自己移除可执行进程红黑树,并加入等待队列。TASK_INTERRUPTIBLE和
TASK_UNINTERRUPTIBLE两种阻塞函数存放在同一个等待队列上。
等待队列由一个简单的链表实现,等待队列有多种,不同的事件发生时唤醒不同的进程等待队列。

(5)抢占和上下文切换

切换上下文
进程上下文的切换调用contex_switch()函数实现,contex_switch内调用两个函数分步骤完成切花

  1. switch_mm()函数将虚拟内存从上一个进程映射到新进程。
  2. switch_to() 函数负责将处理器状态从上一个进程切换到新进程,包括旧现场的保存和新现场的恢复。

抢占

  1. 内核提供一个标志need_reched ,内核通过检查need_reched 标志来表明是否需要调用schedule()切换进程
  2. need_reched 标志存放在task_strucr中(不是全局的,应为当前task_struct存放在高速缓存中,访问更快)
  3. 内核抢占和用户抢占不同

(6)实时调度策略

  1. LINUX提供两种实时调度策略SCHED_FIFO SCHED_RR,有对应的调度类
  2. 普通的线程使用 SCHED_NORMAL 调度策略(CFS)
  3. 实时策略的调度类优先级永远比SCHED_NORMAL高
  4. SCHED_FIFO策略:不基于时间片,直到显示的调用schedule或阻塞后才会让出CPU,只能被优先级更高的SCHED_FIFO打断。
  5. SCHED_RR策略:带时间片的SCHED_FIFO。时间片耗尽也会调用同优先级的SCHED_RR。
  6. 实时调度策略永远不可能被优先级低的进程抢占成功。

(7)调度器相关的系统调用

sched函数组
在这里插入图片描述
优先级函数
在这里插入图片描述
在这里插入图片描述

五、系统调用

在这里插入图片描述
在这里插入图片描述

1、与内核通信

系统调用在用户进程和硬件设备之间添加了一个中间层,这个中间层的作用有:

  1. 为用户进程提供硬件的抽象访问接口(一切皆文件)
  2. 保证系统的稳定,裁决访问
    系统调用是用户空间访问内核的唯一手段,也是唯一的内核访问的合法入口

2、使用API而不是直接使用系统调用

使用C口提供的API,API内部进行系统调用与内核交互。
使用API可以提高程序的可移植性。
C库提供相同的API,在不同架构上API内部实现各不相同。

3、系统调用

  1. 通过C库API访问系统调用
  2. 系统调用通常返回一个long型,表示调用结果 负值表示失败,并将结果写入C的全局errno
  3. 必须保证系统调用是可重入的,应为不同的进程可能会调用同一个系统调用。
  4. 定义系统调用,getpid的系统调用的实现
    在这里插入图片描述
    SYSCALL_DEFINE0 是一个宏,原型是这样的
    在这里插入图片描述
    asmlinkage 是一个编译指令,它要求从栈中提取该函数的参数,(cortex-m3/4 执行一个函数时 他的寄存器中是啥内容?)
    所有系统调用都要用asmlinkage 修饰
    为了兼容32和64位机,系统调用在内核返回long型,在用户返回int型。
    所有系统调用以 sys_XXX 开头。

(1)系统调用号

  1. 每个系统调用有一个系统调用号,内核记录系统调用号表,存储在sys_call_table中。(感觉像stm32的中断号和中断向量表),从0 开始。
  2. 系统调用号是每个体系结构的ABI(应用程序二进制接口)
  3. 系统调用号必须定义在**<asm/unistd.h>**中
  4. 系统调用必须被编译进内核,编译之前放进 kernel/ 下的特定文件syss.c
    在这里插入图片描述

(2)系统调用过程

1、在用户进程调用API
2、API中调用系统调用
3、通过内核陷入指令产生异常,进入系统调用处理函数。(处于内核态)
4、提取系统调用号参数
5、判断参数有效性、合法性。内核根据系统调用号执行系统调用函数 ==> 这就是所说的内核代替用户执行
6、系统调用函数返回结果
7、返回系统调用处理函数
8、返回用户进程

4、如何实现一个系统调用

(1)<asm/unistd.h> 中添加一个系统调用号

在这里插入图片描述

(2)在系统调用表添加系统调用

(3)实现系统调用函数

五、内核数据结构

1、链表

  1. 单向链表
  2. 双向链表
  3. 环形链表(Linux 内核采用 双向循环链表)

Linux 中链表与数据结点的管理:不是将数据结构串到链表上,而是将链表结点写入数据结构结构。 何解?
将数据结构串到链表:next指针指向结构体,每个结点是一个结构体
在这里插入图片描述
将链表结点写入数据结构结构:结构体中有一个节点结构体,它指向成员节点而不是结构体
在这里插入图片描述
通过NODE节点将结构体串成链表,其实是NODE节点形成的链表,结构体数据挂在链表上,通过对NODE节点的地址的偏移操作,就可以得到其他数据结构成员的地址,或者结构体的首地址,这样就可以访问结构体数据了。这和FreeRTOS管理任务链表的实现思想是相同的。
Linux中内核链表通过头指针操作链表,头指针是指向链表某个节点(或第一个节点)的指针(是否用二级指针?)。

2、队列

3、映射(关联数组)

Linux用搜索二叉树实现映射,通过在二叉树搜索键,得到值。

4、二叉树

Linux中主要使用rbtree(红黑树:一种自平衡的搜索二叉树)

5、数据结构的选择

遍历:链表
迅速检索:二叉树
对应:map
生产者和消费者模型:队列。

七、中断和中断处理

1、概念

中断:随时产生,通知内核有外设产生了事件需要处理,对内核来说是异步的
异常:内核自己产生的中断,对内核来说是同步的
中断号
中断服务程序
中断请求
中断嵌套
中断向量表
中断上下文:不可打断

2、ISR 中断服务程序

ISR是硬件设备的设备驱动的一部分设备驱动是用于管理硬件设备的内核代码。
Linux中ISR是特定函数类型的C函数。
ISR应该快进快出

3、ISR的上半部和下半部(顶半和底半)

为了ISR快进快出,减小对系统的影响,在中断中只做紧急的事情,比如给硬件回应等,不紧急的事情放在另一个函数中完成,通过在顶半中标记事件(信号等),内核通过事件的检测来调用ISR的下半部分。

4、ISR的注册

设备驱动通过request_irq函数,为系统注册一个ISR,并使能中断线路。
在这里插入图片描述

5、ISR的卸载

在这里插入图片描述

问题记录

1、用户进程和内核进程有什么区别?用户进程可以被直接调度吗?是否也存在与用户进程对应绑定的内核进程。

进程 == 单线程
创建线程和创建进程都是调用clone函数,只是传入的标志不同。所以用户进程的实现也是基于1V1的线程模型的。在为了能够调度,内核中为进程和线程都创建了task_struct结构体,只是结构体中有些域的标志不同。在内核看来只有线程,没有进程。在用户看来,可以独享资源的单线程就是进程,不能独享资源的就是多线程。

2、实时进程和普通进程有什么区别?如何创建一个实时进程

3、如何理解接口设计—>提供机制而不是策略?

4、cortex-m3/4 执行一个函数时 他的寄存器中是啥内容?

cortex-m3/4寄存器有一下
在这里插入图片描述
在函数执行时
在这里插入图片描述
各个寄存器的作用
在这里插入图片描述

5、内核熵池

负责从各种随机事件中推导出真正的随机事件。

6、进程通过进程上下文连接到内核?

进程执行时通过将数据装载到CPU寄存器中运行程序指令,运行进程时的CPU寄存器数据就是进程上下文(现场)。


原文地址:https://blog.csdn.net/weixin_46236517/article/details/145145893

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!