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


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

  • 相关阅读:
    rocketmq的broker如何同步信息的?
    SO_LINGER 选项
    哈哈哈
    NAG博客目录
    事后分析$eta$
    项目展示$eta$
    测试报告$eta$
    发布声明$eta$
    Scrum meeting 10
    技术博客6--Django实现列表分页
  • 原文地址:https://www.cnblogs.com/wyz-1995/p/7707192.html
Copyright © 2020-2023  润新知