zoukankan      html  css  js  c++  java
  • 读书笔记Linux内核设计与实现part 1

    《Chapter 1 & 2》
    1 机制与策略分离:
    机制(mechanism)和策略(policy):
    a. 机制是指需要提供什么功能,策略是指如何使用这些功能。
    b. 机制是实现具体功能,策略是组合已有功能。
    c. 机制是战术,策略是战略。

    策略与机制是动态与静态的关系。机制意味着固定和自动,像是一个一个的积木块。策略是组合方法,怎样使用已有的积木搭出一个作品。

    策略往往是在机制的基础上进行具体问题的调整。

    2 单内核与微内核:
    Linux是单内核,但吸取了微内核的诸多优点:模块化设计,内核可抢占,支持内核线程,动态装在内核模块。至今,Linux是模块化,多线程,内核本身可调度的OS,实用主义再次占了上风。

    3 Git:Linus写的分布式的版本控制工具。

    4 内核源码树

    目录 描述
    arch 特定体系结构的源码
    block 块IO设备层
    crypto 加密API
    Documentation 内核源码文档
    drivers 设备驱动程序
    firmware 使用某些驱动程序而需要的设备固件
    fs VFS和各种文件系统
    include 内核头文件
    init 内核引导和初始化
    ipc 进程间通信代码
    kernel 核心子系统(像调度程序这样的)
    lib 通用内核函数
    mm 内存管理子系统和VM
    net 网络子系统
    samples 示例,示范代码
    scripts 编译内核所用的版本
    security Linux安全模块
    sound 语音子系统
    usr 早期用户空间代码(所谓的initramfs)
    tools 在Linux开发中有用的工具
    virt 虚拟化基础结构




     

     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     
     

     

     

     

     

     

    5 编译内核
    yes/no/module – module指编译成模块。
    编译命令:make (没错,就是一个make)。
    以前用过Ubuntu下的Linux2.4~2.6有其他的编译安装命令,也很简单。

    6 内核只给每个内核进程一个很小的定长的堆栈,所以一定要节省内存。

    7 像所有自视清高的Unix内核一样,Linux内核使用C语言写的。

    8 C语言中的static:
    static修饰的全局变量放在静态变量区,只能被当前.c文件访问。
    static修饰的局部变量也会被放在静态变量区,但仍然只能由该局部函数访问。
    static修饰的函数只能在当前.c文件中被访问。

    9 内联汇编:

    asm volatile(“rdtsc”:”=a(low)”,”=d(high)”);

     

    《Chapter 3 进程管理》

    1 线程是CPU调度的基本单位,进程是操作系统分配资源的基本单位。例如:同一个进程的线程共享一个虚拟内存,但是有各自的虚拟CPU(分时)。

    2 PCB:  task_struct 32位机器上1.7K大小,包括打开的文件,地址空间,挂起的信号,进程状态等。

    slab机制存放 task_struct –-- 预分配和重复使用。

    3 进程状态:TASK_RUNNING /  TASK_INTERRUPTABLE / TASK_UNINTERRUPTIBLE / TASK_TRACED / TASK_STOPED
    set_task_state(task,state);

    4 进程调用系统调用方式进入内核,内核“代表进程执行”并处于进程上下文中;VS
    中端上下文:中断上下文中,进程不代表进程执行,而是执行一个终端处理程序,此时不存在进程的上下文。

    5   fork()写时复制(copy-on-write COW)
    fork()通过clone()这个系统调用实现,clone需要指定父子进程需要共享哪些资源。
    clone()调用do_fork(),do_fork()调用copy_process()。copy_process()复制一个task_struct,分配pid,根据参数指定的共享哪些资源,进行复制。

    6 内核线程(kernel thread):内核线程与普通的进程的区别在于内核线程没有独立的地址空间(指向地址空间的mm指针被设置为NULL)。内核线程可以像普通进程一样被调度被抢占。内核线程也是由其他线程(kthreadd)创建的。

    struct trask_struct *kthread_create(int *(threadfn)(void *data), void *data,const char namefmt[],…);

    新创建的内核线程需要被唤醒(wake_up_process)。

    7 进程终结:
    do_exit() ->
        del_timer_sync();
        exit_mm();
        sem_exit();
        exit_file();
        exit_fs(); 
        exit_notify();//给进程找养父
        schedule();//切换到新进程

    do_exit()永不返回。此时进程不能再运行,处于EXIT_ZOMBIE退出状态,但仍占用一部分内存(内核栈,thread_info,task_struct),来向父进程提供信息。父进程检测到信息(或者通知内核这些信息不需要了),这部分内存就被释放了。
    如果孤儿进程没有养父,孤儿进程内存将会被浪费。


    《Chapter 4 进程调度》


    1 Linux调度的发展:
    Linux2.4及其之前都只有一个很简陋的调度算法。Linux2.5版本开发了一个O(1)的调度算法(名字就被称为O(1)调度程序),涉及到静态时间片算法和针对每处理器队列。该调度程序扩展性良好(几十个核)。O(1)调度程序存在一个问题,对于那些响应时间敏感的程序有一些先天不足,这使得O(1)调度程序在服务器上运行良好,但在桌面上表现不佳。Linux2.6.23中,O(1)调度被换成了CFS(完全公平调度)。

    2 调度策略通常需要在两个矛盾的目标之间寻找平衡:进程响应迅速(响应时间),最大系统利用率(高吞吐量)。Linux常常倾向于更多调度IO消耗型进程。
    IO密集型期望较短的时间片(多切换几次,迅速响应,每次响应不需要很长的时间片);
    计算密集型期望较长的时间片(少切换几次,连续计算一阵子,时间片长些比较好);

    3 两种优先级:nice值 和 实时优先级。

    4 公平调度CFS:
        4.1 Unix系统调度存在的问题:
        a. 若将nice值映射到时间片,则必需将nice单位值对应到处理器的绝对时间。例如,nice=0,19两个进程,对应时间片是100ms,5ms,则105ms内切换一次。nice=0,0对应时间片是100ms,100ms,则100ms切换一次,各50%。
            nice=19,19对应时间片是5ms,5ms,则10ms切换一次,各50%。
           高nice的进程常常是后台进程且多为计算密集型,低nice的进程常常是前台程序且多为IO密集型。于是跟初衷相背。
        b. nice对应到时间片的问题:0-100ms,1-95ms, … 18-10ms, 19-5ms。同样是差1,nice=0,1与nice=18,19时间差别的比例很大。
        c. nice映射到时间片,还会造成 依赖定时器节拍长度 的问题。
        d. 调度器为了优化交互任务而唤醒进程的问题。这种系统中,为了使进程更快地投入运行,会对新唤醒的进程提升优先级(即使它们的时间片)。这给了某些睡眠/唤醒用例一个机会,使得它们可以利用这一点获得更多的处理器时间。

        4.2 CFS的动机和逻辑:
        CFS的出发点:进程调度的效果应该如同系统具备一个理想中的完美多任务处理器。
        标准unix的调度模型中,进程P1先运行5ms,P2再运行5ms。但它们任何一个运行时都占用100%的时间片。而在理想情况下,完美的多任务处理器模型应该是这样的:我们能在10ms内同时运行两个进程,它们各自使用处理器一半的能力。

        CFS允许每个进程运行一段时间,循环轮转,选择运行时间最少的进程作为下一个运行进程,而不失依靠nice值来计算时间片了。nice在CFS中被用作进程运行时间的比重。

       4.3 CFS的实现:
       kernel/sched_fair.c
       四个组成部分:时间记账,进程选择,调度器入口,睡眠和唤醒。

            4.3.1 时间记账:
            标准Unix调度模型是每个进程一个时间片,每次时钟中断将时间片减少,减到0时进程被切换。CFS中没有时间片的概念(但仍需要处理时间记账)。
            进程调度实体: struct sched_entry (可由task_struct->se获得)。
            vruntime是se的一个字段(u64),用来记录一个进程已经运行的时间和还应该运行多长时间。该运行时间(花在运行上的时间和)的计算是经过了所有可运行进程总数的标准化(或者说是加权化)。虚拟时间以ns为单位,与节拍无关。注意:如果存在一个完美的处理器,则进程的vruntime应该是一致的。
            记账功能由 update_curr(struct cfs_rq *cfs_rq)函数实现(该函数在定时器中断时调用)。

            4.3.2 进程选择:
            如前所述,如果存在一个完美的处理器,则任意时刻各个进程的vruntime应该是一致的。在现实处理器中,只能是一个一个地运行,所以vruntime是有差别的。因此,CFS在选择下一个进程的时候,会挑选一个具有最小vruntime的进程。这是CFS调度算法的核心:选择具有最小vruntime的进程。
            具体实现:数据结构-红黑树。CFS使用红黑树来组织可运行进程队列,并使用这种数据结构迅速选择具有最小vruntime的进程。其上的操作有1.挑选下一个进程;2.向RBTree中加入进程(进程从阻塞变为可运行,或者刚刚fork创建);3.从树中删除进程(进程从可运行变为阻塞,或者进程执行结束)。

            4.3.3 调度器入口:
           调度器类;pick_next_task

           4.3.4 睡眠和唤醒。

           4.4 抢占和上下文切换: 
           每当一个新的进程被调度进来,schedule就会调用content_switch()函数来进行上下文切换。
           content_switch()主要完成两项基本工作:1.调用switch_mm()将虚拟内存从上一个进程切换到新进程;2.调用switch_to(),将上一个处理器状态切换到新进程的处理器状态(保存/恢复 栈信息,寄存器信息等)。
           什么时候调用schedule():内核设置了一个标记need_resched;某进程应被抢占时,更高优先级进程进入可运行状态时都会设置need_resched;在返回用户空间及从中断返回时会检查该标记;need_resched曾是一个全局变量,后来因为current一般都在cache中,所以每个进程中都加了这个标志位。
           用户抢占:内核即将返回用户空间时(中断处理程序/系统调用之后返回用户空间),如果need_sched被设置,则调用schedule()于是发生用户抢占。
           内核抢占:Linux完整支持内核抢占;如果重新调度是安全的(安全=没有持有锁),内核抢占就可能发生。调度前除了检查need_resched外,还要检查preempt_count==0(使用锁加1,释放锁减1)。如果内核被阻塞或者显式调用schedule()时,内核抢占也会发生。总之,内核抢占发生的时机:1.中断处理程序正在执行,且返回内核空间之前;2.内核代码再一次具有可抢占性的时候;3.内核任务显式调用schedule()的时候。4.内核任务阻塞。

            4.5 实时调度策略:
           SCHED_FIFO,SCHED_RR:实时调度;静态优先级。
           SCHED_NORMAL:普通的非实时调度。

           处理器绑定(Process Affinity):使进程在同一个处理器上执行。task_struct中的cpu_allowed标记(每个位对应一个CPU),有get/set*系统调用获取和设置这些标记位。


    《Chapter 5 系统调用》
    5.1 与内核通信
    5.2 API,POSIX,C库
    5.3 系统调用
    5.4 系统调用处理程序
    5.5 系统调用的实现
    capable()函数用来检查是否有权对特定资源进行操作。例如:
    capable(CAP_SYS_BOOT)
    5.6 系统调用上下文
    内核在执行系统调用时处于进程上下文,current指针指向当前任务。

     

    《Chapter 6 内核数据结构》

    使用Linux内核中已有的数据结构,不要重复制造轮子。
    6.1链表
    6.2队列
    6.3映射
    6.4二叉树(红黑树)


    《Chapter 7 中断和中断处理》
    一个设备中断处理代码是设备的驱动程序的一部分。
    又想中断处理程序运行得快,又想中断处理程序完成的工作量多,这是相互抵触的两个目标;所以将中断处理程序分为两个部分:上半部(top half,做严格限时的工作)和下半部(bottom half,可以稍后执行的工作)。

    注册中断处理程序:

    int request_irq( unsigned int irq,          /* 中断号 */
                     irq_handler_t handler,     /* 函数指针 */
                     unsigned long flag,        /* 标记 */
                     const char *name,          /* 设备名称,如keyboard*/
                     void *dev);                /*用于共享中断号的设备进行区分*/
    
    typedef irqreturn_t (*irq_handler_t)(int,void*);
    
    void free_irq(unsigned int irq, void* dev);
    
    static irqreturn_t intr_handler(int irq, void* dev);

    中断上下文:
    进程上下文是一种内核所处的操作模式,此事内核代表进程执行。
    中断上下文与进程没有瓜葛,因为没有后备进程,所以中断上下文不可以睡眠(否则怎么唤醒),所以中断上下文不能调用某些可能导致睡眠的函数。

    内核栈大小一般是两页(8K)。

    中断从硬件到内核的路由图:


    中断控制接口:

    local_irq_disable();
    lcoal_irq_enable();
    local_irq_save(flags);
    local_irq_restore(flags);
    
    disable_irq(unsigned int irq);
    disable_irq_nosync(unsigned int irq);
    enable_irq(unsigned int irq);
    synchronize_irq(unsigned int irq); //等待一个中断处理程序的退出
    
    in_interrupt();    //返回正在执行的中断类型
    in_irq();

     

     

    《Chapter 7 下半部和退后执行的工作》
    Linux2.6内核提供了三种不同形式的下半部实现机制:软中断,tasklet,工作队列。

    软中断:编译期间静态分配的。由结构体softirq_action表示。

    struct softirq_action {
        //函数指针(软中断处理程序)
        void (*action)(struct softirq_action *); 
    }
    
    static struct softirq_action softirq_vec[NR_SOFTIRQS];

    软中断处理程序

    调用: my_softirq->action(my_softirq);
    注:action参数为整个结构体,是为了将来在结构体中增加新的字段时方便。

    软中断执行时机:
    1. 从一个硬件中断代码返回时;
    2. 在ksoftieqd内核线程中;
    3. 在那些显式检查和执行待处理的软中断代码中(例如 网络子系统)

    软中断留给系统中最严格以及最重要的下半部使用。目前只有网络子系统和SCSI直接使用软中断;此外,内核定时器和tasklet都是建立在软中断之上的。

     

    tasklet:
    比软中断接口简单,锁保持要求也低。基于两种softirq(仅仅优先级不同)。

    struct tasklet_struct {
        struct tasklet_struct *next;
        unsigned long state;
        atomic_t count;
        void (*func)(unsigned long);
        unsigned long data;
    }

     

    工作队列(work queue)
    工作队列是由一个内核线程来执行的下半部机制。它总是运行在一个进程上下文中,因此可以睡眠(是否需要睡眠是用来决定选择tasklet还是工作队列的基本方法)。


    比较:

    下半部机制 上下文 顺序执行保障
    软中断 中断
    tasklet 中断  同类型不能执行
    工作队列 进程 无(和进程一样被调度)

     

      
      
     
      

     </END HERE>

  • 相关阅读:
    HDU5195 线段树+拓扑
    Codeforces Round #328 (Div. 2)D. Super M 虚树直径
    HDU5489 LIS变形
    BZOJ 1787: [Ahoi2008]Meet 紧急集合 LCA
    Codeforces Round #330 (Div. 2)B. Pasha and Phone 容斥
    Codeforces Round #330 (Div. 2) D. Max and Bike 二分
    Codeforces Round #277 (Div. 2) E. LIS of Sequence DP
    Codeforces Round #277 (Div. 2) D. Valid Sets DP
    内存对齐
    mui列表跳转到详情页优化方案
  • 原文地址:https://www.cnblogs.com/apprentice89/p/LK.html
Copyright © 2011-2022 走看看