《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()把任务加到运行队列。

2017-2018-1 20179205《Linux内核原理与设计》第四周作业的更多相关文章

  1. 2017-2018-1 20179205《Linux内核原理与设计》第十周作业

    <Linux内核原理与设计>第十周作业 教材17.19.20章学习及收获 1.在Linux以及所有unix系统中,设备被分为以下三种:块设备(blkdev)以块为单位寻址,通过块设备节点来 ...

  2. 2017-2018-1 20179205《Linux内核原理与设计》第九周作业

    <Linux内核原理与设计>第九周作业 视频学习及代码分析 一.进程调度时机与进程的切换 不同类型的进程有不同的调度需求,第一种分类:I/O-bound 会频繁的进程I/O,通常会花费很多 ...

  3. 2017-2018-1 20179205《Linux内核原理与设计》第八周作业

    <Linux内核原理与设计>第八周作业 视频学习及操作分析 预处理.编译.链接和目标文件的格式 可执行程序是怎么来的? 以C语言为例,经过编译器预处理.编译成汇编代码.汇编器编译成目标代码 ...

  4. 2017-2018-1 20179205《Linux内核原理与设计》第七周作业

    <Linux内核原理与设计>第七周作业 视频学习及操作分析 创建一个新进程在内核中的执行过程 fork.vfork和clone三个系统调用都可以创建一个新进程,而且都是通过调用do_for ...

  5. 2017-2018-1 20179205《Linux内核原理与设计》第六周作业

    <Linux内核原理与设计> 视频学习及操作 给MenuOS增加time和time-asm命令的方法: 1.更新menu代码到最新版 rm menu -rf //强制删除menu, rm ...

  6. 2017-2018-1 20179205《Linux内核原理与设计》第五周作业

    <Linux内核原理与设计>第五周作业 视频学习及操作分析 一.用户态.内核态和中断 内核态在CPU执行中对应高执行级别,执行级别为0级,具有特权指令,可以访问任意物理地址:用户态执行级别 ...

  7. 2017-2018-1 《Linux内核原理与设计》第十二周作业

    <linux内核原理与设计>第十二周作业 Sql注入基础原理介绍 分组: 和20179215袁琳完成实验 一.实验说明   SQL注入攻击通过构建特殊的输入作为参数传入Web应用程序,而这 ...

  8. 《Linux内核原理与设计》第十一周作业 ShellShock攻击实验

    <Linux内核原理与设计>第十一周作业 ShellShock攻击实验 分组: 和20179215袁琳完成实验及博客攥写 实验内容:   Bash中发现了一个严重漏洞shellshock, ...

  9. 2019-2020-1 20199303<Linux内核原理与分析>第二周作业

    2019-2020-1 20199303第二周作业 1.汇编与寄存器的学习 寄存器是中央处理器内的组成部份.寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令.数据和位址.在中央处理器的控制部件中 ...

  10. 2019-2020-1 20199303 《Linux内核原理分析》 第一周作业

    2019-2020-1 20199303 <Linux内核原理分析> 第一周作业 1. 环境准备 在众多的Linux发行版中,Ubuntu,小红帽还有类Unix系统的BSD系统,我选择了目 ...

随机推荐

  1. [Leetcode] 3.Longest Substring Without Repeating Characters(unordered_map)

    通过把未访问的结点放到unordered_map中来判断是否重复,代码如下: class Solution { public: int lengthOfLongestSubstring(string ...

  2. 第68天:原型prototype方法

    一.原型prototype方法声明 构造函数有一个prototype属性,指向实例对象的原型对象.通过同一个构造函数实例化的多个对象具有相同的原型对象.经常使用原型对象来实现继承 <!DOCTY ...

  3. 【EF】Entity Framework 6新特性:全局性地自定义Code First约定

    应用场景 场景一:EF Code First默认使用类名作为表名,如果我们需要给表名加个前缀,例如将类名Category映射到表Shop_Category.将Product映射到Shop_Produc ...

  4. openstack之horizon部署

    登录官网 www.openstack.org 查看安装文档 https://docs.openstack.org/newton/install-guide-rdo/horizon.html 第一步yu ...

  5. BZOJ4771 七彩树(dfs序+树上差分+主席树)

    考虑没有深度限制怎么做.显然的做法是直接转成dfs序上主席树,但如果拓展到二维变成矩形数颜色数肯定没法做到一个log. 另一种做法是利用树上差分.对于同种颜色的点,在每个点处+1,dfs序相邻点的lc ...

  6. Windows的三种截屏方法

    「发表于知乎」戳

  7. BZOJ1086:[SCOI2005]王室联邦——题解

    http://www.lydsy.com/JudgeOnline/problem.php?id=1086 题面源于洛谷. 题目描述 “余”人国的国王想重新编制他的国家.他想把他的国家划分成若干个省,每 ...

  8. 数组分组chunk的一种写法

    lodash的_.chunk函数可以将数组按照数量分成若干组, 例如: const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; const groupByN ...

  9. 34张史上最全IT架构师技术知识图谱 最新下载

    本文是笔者多年来积累和收集的知识技能图谱,小编极力推荐分享给身边的技术人儿,希望这份技术知识图谱能够帮助到每一位奋斗在技术路上的小伙伴. 下面是笔者多年来积累和收集的知识技能图谱,有的是笔者原创总结的 ...

  10. SQL Server 2008设置主键为自增

    环境:SQL Server 2008 问题:设置主键,将主键设为自增. 解决:点击table->选中表->design->选中需要设置主键的字段,单击右键"设置主键&quo ...