首页 技术 正文
技术 2022年11月18日
0 收藏 472 点赞 5,055 浏览 7101 个字

《Linux内核原理与分析》


视频学习及实验操作

Linux内核源代码

视频中提到了三个我们要重点专注的目录下的代码,一个是arch目录下的x86,支持不同cpu体系架构的源代码;第二个是init目录下的main.c,是整个linux内核启动的起点,不过这里面不是main()函数,而是start_kernel,start_kernel函数相当于普通c程序的main函数,linux内核的核心代码在kernel目录中;最后是kernel 下进程调度相关的代码。

构造一个简单的Linux系统MenuOS

使用实验楼的虚拟机打开shell,输入命令,启动MenuOS系统:

cd LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

内核启动完成后进入menu程序,在qemu窗口下输入help,发现这个系统支持三个命令:help、version、quit:

使用gdb跟踪调试Linux内核的方法

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S

关于-S -s选项的说明:

-S freeze CPU at startup (use ’c’ to start execution)

表示CPU开始初始化之前将其冻结,目的是阻止cpu执行接下来的指令

-s shorthand for -gdb tcp::1234

在tcp1234窗口上创建一个gdbserver作为通讯;若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项

使用-S 之后,系统开始启动,qemu显示stopped,表示cpu已被冻结。

再打开另一个shell窗口,右键-水平分割,启动gdb来跟踪调试。输入:

file linux-3.18.6/vmlinux
target remote:1234

通过刚刚设置的端口号建立gdb和gdbserver之间的连接,我理解的是将下面这个窗口和上面冻结的窗口建立联系。接下来通过break设置断点,现在在内核启动的起点start_kernel函数处设置一个断点:

break start_kernel

我们可以通过按c,让冻结的qemu上的linux继续运行,此时发现,系统运行了一些命令在breakpoint1 start_kernel处停下了,说明断点设置有效。

同理可以设置第二个第三个断点,按c继续执行。

简单分析start_kernel

start_kernel在init目录下的main.c,整个start_kernel,就完成调用一系列的初始化函数,完成内核本身的设置:设置与体系结构相关的环境、进程调度器初始化、内存初始化等各种初始化。在Start_kernel函数的最后调用了rest_init()函数,在rest_init中建立了init线程,并在最后调用cpu_idle()函数。

对于视频中讲述的道生一,一生二,二生三,三生万物,我不是特别理解。从网上查了一下PID 0(idle), 1(init), 2(kthreadd)3个进程,有了大概的认识:

(1)init_task(0号进程的前世)在创建了init进程后,就变成了idle进程(0号进程的今生),执行一次调度后,切换当前进程到init;

(2)kernel_init(1号内核线程)执行内核的部分初始化工作及进行系统配置,变成了init用户进程;

(3)init_task(0号进程)又创建了Kthreadd内核线程(2号进程),它的任务是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数

教材第四章 进程调度

进程调度程序,是确保进程能有效工作的内核子系统。本章介绍了调度程序设计的基础和完全公平调度程序如何运用、如何设计、如何实现以及与它相关的系统调用。

4.1 多任务

多任务操作系统就是能同时并发地交互执行多个进程的操作系统。无论单处理器或者多处理器及其上,多任务操作系统都能使多个进程处于堵塞或者睡眠状态。

多任务系统划分为两类:非抢占式多任务和抢占式多任务。由调度程序决定什么时候停止一个进程的运行,以便其他进程能得到执行机会,这个强制的挂起动作叫抢占;相反,在非抢占式多任务模式下,除非进程自己主动停止运行,否则会一直执行下去,这个主动的挂起动作叫让步。 进程在被抢占之前能够运行的时间是预先设置好的,叫进程的时间片。

4.2 Linux的进程调度

O(1)调度器:对于大服务器的工作负载很理想,但缺少交互进程。

完全公平调度算法(CFS):“RSDL”,吸取了队列理论,将公平调度的概念引入Linux调度程序。

4.3 策略

策略决定调度程序在何时让什么进程运行,负责优化使用处理器时间。

4.3.1 I/O消耗型和处理器消耗型的进程

I/O消耗型:进程的大部分时间用来提交I/O请求或者等待I/O请求,经常处于可运行状态但是运行时间很短,等待更多的请求时最后总会阻塞。

处理器消耗型:把时间大多用在执行代码上,除非被抢占,否则通常都会不停运行。对于这类处理器消耗型的进程,调度策略通常会降低它们的调度频率,而延长其运行时间。

调度策略通常要在两个矛盾的目标中间寻找平衡:

·进程响应迅速(响应时间短);

·最大系统利用率(高吞吐量)

4.3.2 进程优先级

调度算法最基本的是基于优先级的调度,根据进程的价值和其对处理器时间的需求来对进程分级。通常按照优先级高低来运行,相同优先级的进程按轮转方式进行调度。

·nice值:

范围[-20,19],默认值为0;

nice值越大,优先级越低;

Linux系统中nice值代表时间片的比例;

ps-el命令查看系统中进程列表,NI列为nice值。

·实时优先级:

默认范围[0,99],值可以配置,;

值越高,优先级越高;

任何实时进程的优先级都高于普通的进程。

通过命令:

ps-eo state,uid,pid,ppid,rtprio,time,comm

查看到你系统中的进程列表,进程列表“-”说明它不是实时进程。

4.3.3 时间片

时间片是一个数值,是进程在被抢占前所能持续运行的时间。I/O消耗型不需要很长的时间片,而处理器消耗型的进程则希望越长越好。

Linux系统是抢占式的,是否要抢占当前进程,是完全由进程优先级和是否有时间片决定的。

CFS调度器:其抢占时机取决于新的可运行程序消耗了多少处理器使用比。

Linux调度算法

4.4.1 调度器

Linux调度器以模块方式提供,允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。每个调度器有一个优先级,会按照优先级顺序遍历调度类,选择优先级最高的调度器类。

完全公平调度CFS是一个针对普通进程的调度类。

4.4.3 公平调度

CFS做法允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程,不再给每个进程分配时间片。在所有可运行进程总数基础上计算出一个进程应该运行多久。nice值作为进程获得的处理器运行比的权重。

4.5 Linux调度的实现

CFS调度算法实现的四个组成部分:

(一)时间记账

(二)进程选择

(三)调度器入口

(四)睡眠和唤醒

(一)时间记账

所有的调度器都必须对进程运行时间做记账。CFS不再有时间片的概念,但要使用调度器实体结构(定义在文件<linux/sched>的struct_sched_entity中)来追踪记账,进程描述符中的se变量。

CFS使用vruntime变量来记录一个程序到底运行了多长时间以及它还应该再运行多久,定义在kernel/sched_fair.c文件中的update_curr()函数实现了该记账功能,update_curr()计算了当前进程的执行时间,并且将其存放在变量delta_exec中。

_update_curr(cfs_rq, curr, delta_exec)

update_curr()是由系统定时器周期性调用的,根据这种方式,vruntime可以准确地测量给定进程的运行时间。

(二)进程选择

CFS调度算法的核心是选择具有最小vruntime的任务,CFS使用rbtree来组织可运行进程队列,并利用其迅速找到最小vruntime值的进程。rbtree是一个自平衡二叉搜索树,是一种以树节点形式存储的数据,这些数据会对应一个键值,可以通过这些键值来快速检索节点上的数据,而且检索速度与整个树的节点规模成指数比关系。

挑战下一个任务:CFS运行rbtree树中最左边叶子节点所代表的那个进程。实现这一函数是__pick_next_entity():

这个函数不会遍历树找到最左叶子节点,该值缓存在rb_leftmost字段中,

函数返回值就是CFS选择的下一个运行进程。

如果返回NULL,表示树空,没有可运行进程,这时选择idle任务运行。

向树中加入进程:发生在进程被唤醒或者通过fork调用第一次创建进程时:

函数enqueue_entity():更新运行时间和其他一些统计数据,然后调用__enqueue_entity();

函数__enqueue_entity():进行繁重的插入工作,把数据项真正插入到rbtree中:

(把一个调度实体插入到rbrtree中)

static void _enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)

从树中删除进程:删除动作发生在进程堵塞或终止时。和给红黑树添加进程一样,实际工作由辅助函数dequeue_entity()和__dequeue_entity()完成。

(三)调度器入口

进程调度的主要入口点函数是schedule():

schedule()函数会调用pick_next_task();

pick_next_task()会以优先级为序依次检查每一个调度类,并且选择最高优先级的进程;

pick_next_task()会返回指向下一个可运行进程的指针,没有时返回NULL;

pick_next_task()函数实现会调用pick_next_entity();

pick_next_entity()会调用__pick_next_entity()

(四)睡眠和唤醒

睡眠: 进程把自己标记成休眠状态,从可执行rbtree中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程;

进程通过执行以下几个步骤将自己加入到一个等待队列中:

1)调用宏DEFINE_WAIT()创建一个等待队列的选项。

2)调用add _ wait _ queue()把自己加入队列中。

3)调用prepare _ to _ wait()方法将进程的状态变更为TASK _ INTERRUPTIBLE或TASK _ UNINTERRUPTIBLE(休眠)

4)如果状态被设置成TASK_INTERRUPTIBLE,则信号唤醒进程。(伪唤醒:唤醒不是因为事件的发生。)

5)当进程被唤醒的时候,会再次检查条件是否为真,真则退出循环,否则再次调用schedule()并且一直重复这步动作。

6当条件满足后,进程将自己设置为TASK _ RUNNING并调用finish _ wait()方法把自己移出等待序列。函数inotify _ read():负责从通知文件描述符中读取信息。

唤醒:进程被设置为可执行状态,然后再从等待队列中移到可执行rbtree中。

4.6 抢占和上下文切换

上下文切换由context _ switch()函数负责,每当一个新进程被选出准备投入运行时,schedule()会调用context _ switch()。

它完成了两项基本工作:

·调用switch_mm(),该函数负责把虚拟内存从上一个进程映射到新进程中。

·调用switch _ to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。

这包括保存、恢复栈信息和寄存器信息,还有其他任何与体系结构相关的状态信息,都必须以每个进程为对象进行管理和保存。

抢占分为用户抢占和内核抢占。用户抢占都用到need_resched,会导致schedule()被调用,切换到新进程;而内核抢占,受Linux完整的支持,即调度程序没有办法在一个内核级的任务正在执行的时候重新调度。

内核的抢占:

1、为每个进程的thread _ info中引入preempt _ count计数器,初值为0,使用锁+1,释放锁-1,数值为0时,内核可执行抢占。

2、从中断返回内核空间时,先检查need_resched标志,如果被设置表示需要被调度,然后检查preempt_count计数器,如果为0,表示可以被抢占,这时调用调度程序。否则,内核直接从中断返回当前执行进程。

3、当前进程持有的锁全部被释放,这时preempt_count重新为0,释放锁的代码会检查need_resched是否被设置,如果是就调用调度程序。

4、如果内核中的进程被阻塞了,或它显式地调用了schedule(),内核抢占也会显式地发生。

4.7 实时调度策略

Linux提供了两种实时调度策略:SCHED _ FIFO和SCHED _ RR:

SCHED _ FIFO是一种先入先出的调度算法,不使用时间片,该进程一旦处于可执行状态,就会一直执行,直到受阻或显示释放处理器;

SCHED _ RR是带有时间片的实时轮流调度算法,该进程在耗尽实现分配给它的时间后就不再继续执行。

优先级范围

实时:0~[MAX _ RT _ PRIO-1]

默认MAX _ RT _ PRIO=100,所以默认实时优先级范围为[0,99]

SCHED _ NORMAL:[MAX _ RT _ PRIO]~[MAX _ RT _ PRIO+40]。

默认情况下,nice值从-20到+19对应的是从100到139的实时优先级范围。

4.8与调度相关的系统应用

放弃处理器时间:

sched_yield() 让进程显式地将处理器时间让给其他等待执行进程。普通进程移到过期队列中,实时进程移到优先级队列最后。

内核先调用yield,确定给定进程确实处于可执行状态,然后调用sched _ yield()。

用户空间可以直接调用sched _ yield()。

教材第六章 内核数据结构

对于数据结构的知识,之前大学本科没有接触过,结合教材和网上资料,谈谈自己的认识。内核数据结构贯穿于整个内核代码中,利用这4个基本的数据结构,可以在编写内核代码时节约大量时间。

本章主要讲述了数据结构中最有用的四个:

*链表

*队列

*映射

*二叉树

链表

通过对比内核链表和传统链表,可以发现,内核链表更好的解决了传统链表不好共通化的问题:

(1)在传统双向链表中,每个节点(node)除了包含2个指针(pre,next),还包含必要的数据,这些数据存在每个节点中,最大的缺点就是不好共通化;

(2)linux的链表不是将用户数据保存在链表节点中,而是将链表节点保存在用户数据中。

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

队列

内核中的队列是以字节形式保存数据的,所以获取数据的时候,需要知道数据的大小。Linux内核通用队列实现成为kfifo,它实现在kernel/kfifo.c中,内核中关于队列定义的头文件位于:<linux/kfifo.h> include/linux/kfifo.h。kfifo提供两个主要操作:enqueue(入队列)和dequeue(出队列),kfifo对象维护了两个偏移量:入口偏移和出口偏移两个偏移量。

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

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

映射

Linux内核提供了简单、有效的映射数据结构,有点想像他语言(C#或者python)中的字典类型,每个唯一的UID对应到一个指针。映射的使用需要注意的是,给自定义的数据结构申请一个id的时候,不能直接申请id,先要分配id(函数idr_pre_get),分配成功后,在获取一个id(函数idr_get_new)。

红黑树

红黑树或许遵循起来比较简单,但一系列规则看起来比较复杂,其插入,删除性能也还不错。红黑树中最长的路径就是红黑交替的路径,最短的路径是全黑节点的路径,再加上根节点和叶子节点都是黑色,

从而可以保证红黑树中最长路径的长度不会超过最短路径的2倍。内核中关于红黑树定义的头文件位于:<linux/rbtree.h> include/linux/rbtree.h

头文件中定义的函数的实现位于:

lib/rbtree.c

之前没有学过数据结构,教材中的代码我不是很看得懂,网上搜到一些资料,对于红黑树的定义和宏定义,有了一定的认识。

参考博客:内核数据结构(红黑树)

总结:

本章学习内核数据结构第一次接触,速度有点慢,尤其红黑树,宏定义这些理解起来比较慢,花费时间比较长,要加强数据结构的学习。

问题与反思:

1、在另一个shell中输入命令

target_remote:1234

为什么会出现连接超时?因为在上一个gdb中,生成的冻结窗口被自己关闭了,要重新-s -S打开qemu(stopped)冻结窗口,重新输入命令,设置断点,继续执行。

2、TASK_INTERRUPTIBLE接受到一个信号,会被提前唤醒并响应该信号,和TASK_RUNNING的关系区别?

TASK_INTERRUPTIBLE接受信号,提前唤醒但并不执行,其任务是不可行的;而TASK_RUNNING接收到信号,任务是可行的,再调用active_task()把任务加到运行队列。

相关推荐
python开发_常用的python模块及安装方法
adodb:我们领导推荐的数据库连接组件bsddb3:BerkeleyDB的连接组件Cheetah-1.0:我比较喜欢这个版本的cheeta…
日期:2022-11-24 点赞:878 阅读:9,104
Educational Codeforces Round 11 C. Hard Process 二分
C. Hard Process题目连接:http://www.codeforces.com/contest/660/problem/CDes…
日期:2022-11-24 点赞:807 阅读:5,580
下载Ubuntn 17.04 内核源代码
zengkefu@server1:/usr/src$ uname -aLinux server1 4.10.0-19-generic #21…
日期:2022-11-24 点赞:569 阅读:6,428
可用Active Desktop Calendar V7.86 注册码序列号
可用Active Desktop Calendar V7.86 注册码序列号Name: www.greendown.cn Code: &nb…
日期:2022-11-24 点赞:733 阅读:6,200
Android调用系统相机、自定义相机、处理大图片
Android调用系统相机和自定义相机实例本博文主要是介绍了android上使用相机进行拍照并显示的两种方式,并且由于涉及到要把拍到的照片显…
日期:2022-11-24 点赞:512 阅读:7,835
Struts的使用
一、Struts2的获取  Struts的官方网站为:http://struts.apache.org/  下载完Struts2的jar包,…
日期:2022-11-24 点赞:671 阅读:4,918