文章目录
- 一、内核历史
- 1、内核简介
- 2、LINUX 内核与 UNIX 内核比较
- 3、LINUX内核版本命名
- 二、从内核出发
- 1、获取内核源码
- (1)查看Linux内核版本 uname -r
- (2)下载源码 https://www.kernel.org/
- (3)编译内核
- 2、内核源码树
- 3、编译源码
- 4、内核开发的特点
- (1)GNU C 和 ANSI C
- (2)内核没有内存保护机制,而且不分页
- (3)最好不要在内核中使用浮点数
- (4)内核程序的栈空间比较小,在编译时设置
- (5)内核支持抢占式调度
- (6)内核应该又高度的可移植性
- 5、C语言扩展的特性
- (1)内联函数 inline
- (2)内联汇编
- (3)分支声明
- 三、进程管理
- 四、进程调度
- 五、系统调用
- 问题记录
一、内核历史
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(),表明一个分支是经常出现还是极少出现。
三、进程管理
- 进程 (任务) 是处于执行期间的程序、是正在执行的程序的实时结果。
- 线程是在进程中活动的对象,线程拥有独立的计数器、栈、和寄存器。
- 内核调度的线程而不是进程。 Linux对线程和进程并不特别区分,线程是特殊的进程(轻量进程)。
- 进程提供两种虚拟机制:虚拟处理器和虚拟内存,虚拟处理器使得进程感觉自己独享处理器,虚拟内存使进程感觉自己独享整个内存。
- 线程共享进程的虚拟空间,但是线程有自己的虚拟处理器(线程1v1模型的原理)
- fork()系统调用从内核返回两次,一次返回父进程,一次返回主进程。
1、进程描述符和任务结构
(1)进程描述符:task_struct
结构体描述符存放在 /linux/sched.h
(2)内核中任务的组织结构
- 内核把进程存放在任务队列中,这个任务队列是双向循环链表的结构。
- task_struct比较大,在32位机中大约为1.7KB,它完整的描述了一个进程的所有状态。
- 通过预先分配和重复使用task_struct避免动态分配和释放带来的资源消耗
- Linux 通过slab分配器来分配task_struct,达到对象复用和缓存着色
- 在栈顶创建一个 thread_info的结构体,该结构体中有指向tsak_struct的指针。通过计算栈偏移量来寻找
- 内核通过PID标识一个进程。
- 通过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)进程家族树
- 所有的进程都是PID为1 的进程(init)的后代,内核在启动的最后阶段启动init进程。该进程读取系统的初始化脚本
- 进程间的关系存放在进程描述符中,通过parent指针指向父进程,通过children链表指向子进程
linuxfont_111">2、进程 与线程的创建(理解linux中线程与进程的重点)
在LINUX的内核中不分进程或者线程,在创建进程时调用clone函数,为进程分配资源,通过exec函数族加载一个程序到进程的地址空间,并采用写时拷贝来加快进程的创建速度。
在创建线程时 调用pthread_create,这个函数还是调用了clone函数。
创建进程和创建线程最终都是调用了clone函数,不同之处在于调用时的参数不通,在创建线程时
共享父进程的内存、文件描述都、信号等
在创建进程时
所以由此来看,线程和进程没有本质上的区别,在内核中都是以task_struct标识,不同之处在于task_struct的域信息不同。
这也是linux中线程也可以参加系统调度的原因。线程可以看作共享父进程一些资源的进程。
在不同的系统中实现多线程的机制是不同的。
一个进程的创建分为fork和exec两步,一个进程的回收也分为资源回收和进程描述符回收两部分。
3、孤儿进程
父进程先于自己终结的进程叫做孤儿进程,系统会为孤儿进程找一个同组进程或init进程作为新的父进程。
这个过程为
四、进程调度
进程调度程序可以看作内核的子系统,负责为可运行程序分配CPU时间。
调度器的通用概念:时间片 和 优先级
1、多任务
多任务系统分类:
- 非抢占式多任务 cooperative multitasking
- 抢占式多任务 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_SCHEDint 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)进程选择
- CFS选择进程时选择vruntime最小的进程,以达到公平。
- CFS使用红黑树 rbtree(自平衡二叉查找树) 来存储可运行的进程队列,利用红黑树迅速寻找最小的vruntime。
- 最小的vruntime就是最左边的叶子节点,并将该节点存储,这样每次不用查找直接获取它就可以了。
- 在进程变为可运行状态或fork后被加入CFS的红黑树。
- 在进程变为阻塞或终止时从CFS红黑树删除。
(3)调度器入口
linux内核调度器入口为 schedule() 函数,schedule()通常要和一个调度类关联,每个调度器有一个自己的任务队列。
在schedule内调用pick_next_task() 函数一次从优先级高到优先级低访问每个调度类,询问最高优先级的进程,最后选择最高优先级的调度类的最高优先级的进程。
(4)进程的休眠
进程休眠时将自己设置为阻塞状态,并将自己移除可执行进程红黑树,并加入等待队列。TASK_INTERRUPTIBLE和
TASK_UNINTERRUPTIBLE两种阻塞函数存放在同一个等待队列上。
等待队列由一个简单的链表实现,等待队列有多种,不同的事件发生时唤醒不同的进程等待队列。
(5)抢占和上下文切换
切换上下文
进程上下文的切换调用contex_switch()函数实现,contex_switch内调用两个函数分步骤完成切花
抢占
- 内核提供一个标志need_reched ,内核通过检查need_reched 标志来表明是否需要调用schedule()切换进程
- need_reched 标志存放在task_strucr中(不是全局的,应为当前task_struct存放在高速缓存中,访问更快)
- 内核抢占和用户抢占不同
(6)实时调度策略
- LINUX提供两种实时调度策略SCHED_FIFO SCHED_RR,有对应的调度类
- 普通的线程使用 SCHED_NORMAL 调度策略(CFS)
- 实时策略的调度类优先级永远比SCHED_NORMAL高
- SCHED_FIFO策略:不基于时间片,直到显示的调用schedule或阻塞后才会让出CPU,只能被优先级更高的SCHED_FIFO打断。
- SCHED_RR策略:带时间片的SCHED_FIFO。时间片耗尽也会调用同优先级的SCHED_RR。
- 实时调度策略永远不可能被优先级低的进程抢占成功。
(7)调度器相关的系统调用
sched函数组
优先级函数
五、系统调用
1、与内核通信
系统调用在用户进程和硬件设备之间添加了一个中间层,这个中间层的作用有:
2、使用API而不是直接使用系统调用
使用C口提供的API,API内部进行系统调用与内核交互。
使用API可以提高程序的可移植性。
C库提供相同的API,在不同架构上API内部实现各不相同。
3、系统调用
- 通过C库API访问系统调用
- 系统调用通常返回一个long型,表示调用结果 负值表示失败,并将结果写入C的全局errno
- 必须保证系统调用是可重入的,应为不同的进程可能会调用同一个系统调用。
- 定义系统调用,getpid的系统调用的实现
SYSCALL_DEFINE0 是一个宏,原型是这样的
asmlinkage 是一个编译指令,它要求从栈中提取该函数的参数,(cortex-m3/4 执行一个函数时 他的寄存器中是啥内容?)
所有系统调用都要用asmlinkage 修饰
为了兼容32和64位机,系统调用在内核返回long型,在用户返回int型。
所有系统调用以 sys_XXX 开头。
(1)系统调用号
- 每个系统调用有一个系统调用号,内核记录系统调用号表,存储在sys_call_table中。(感觉像stm32的中断号和中断向量表),从0 开始。
- 系统调用号是每个体系结构的ABI(应用程序二进制接口)
- 系统调用号必须定义在**<asm/unistd.h>**中
- 系统调用必须被编译进内核,编译之前放进 kernel/ 下的特定文件syss.c
(2)系统调用过程
1、在用户进程调用API
2、API中调用系统调用
3、通过内核陷入指令产生异常,进入系统调用处理函数。(处于内核态)
4、提取系统调用号和参数
5、判断参数有效性、合法性。内核根据系统调用号执行系统调用函数 ==> 这就是所说的内核代替用户执行
6、系统调用函数返回结果
7、返回系统调用处理函数
8、返回用户进程
4、如何实现一个系统调用
(1)<asm/unistd.h> 中添加一个系统调用号
(2)在系统调用表添加系统调用
(3)实现系统调用函数
问题记录
1、用户进程和内核进程有什么区别?用户进程可以被直接调度吗?是否也存在与用户进程对应绑定的内核进程。
2、实时进程和普通进程有什么区别?如何创建一个实时进程
3、如何理解接口设计—>提供机制而不是策略?
4、cortex-m3/4 执行一个函数时 他的寄存器中是啥内容?
cortex-m3/4寄存器有一下
在函数执行时
各个寄存器的作用