第一次作业:深入Linux源码分析其进程模型

一、进程

1.进程的概念

(1)进程:Process,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

(2)进程由程序、数据和进程控制块PCB组成。当系统创建一个进程时,实际上是建立一个PCB。当进程消失时,实际上是撤销PCB。在进程活动的整个生命周期内,系统通过PCB对进程进行管理和调度。

2.查看进程状态

(1)ps指令(常用组合:aux、ef、eFH、-eo、axo)

(2)示例

# ps  aux:显示所有与终端有无关联的进程信息

# ps   -ef:以完整格式显示所有的进程信息

二、进程的组织

在Linux中,每个进程都有自己的task_struct结构。在2.4.0版中,系统拥有的进程取决于物理内存的大小。因此进程数可能达到成千上万个。为了对系统中的很多进程及处于不同状态的进程进行管理,Linux采用了以下几种组织方式来管理进程:

1.哈希表

哈希表是进行快速查找的一种有效的组织方式,在include/linux/sched.h中定义如下:

struct task_struct* pidhash[PIDHASH_SZ];

linux用一个宏pid_hashfn()将PID转换成表的索引,通过pid_hashfn()可以把进程的PID均匀的散列在哈希表中。

给定一个进程号PID寻找其对应的PCB的查找函数如下:

static inline struct task_struct * find_task_by_pid(int pid){
    struct task_struct *p, **htable = &pidhash[pid_hashfn()];
    for(p = *htable; p && p->pid != pid; p = pidhash->next)
    return p;
}

2.进程链表

用双向循环链表将进程联系起来,定义如下:

struct task_struct{
    struct list_head tasks;
    char comm[TASK_COM_LEN];//可执行程序的名字带路径
}

每个进程task_struct结构中的prev_task和next_task成员用来实现这种链表,链表的头和尾都是init_task(即0号进程),这个进程永远不会被撤销地被静态分配在内核数据段中。

通过宏for_each_task可以方便地搜索所有进程:

#define for_each_task(p)
    for(p=&init_task;(p=p->next_task)!=&init_task;)

3.就绪队列

把所有可运行状态的进程组成的一个双向链表叫做就绪队列,该队列通过task_struct结构中的两个指针run_list链表来维护:

struct task_struct{
    struct list_head run_list;
    ,,,
}

就绪队列的定义及相关操作在/kernel/sched.c文件中:

static LIST_HEAD(runqueue_head);//定义就绪队列的头指针为runqueue_head
    static inline void add_to_runqueue(struct task_struct *p){
         list_add_tail(&p->run_list, &runqueue_head);
         nr_running++;
    }
    static inline void move_last_runqueue(struct task_struct *p){
        list_del(&p->run_list);
        list_add_tail(&p->run_list, &runqueue_head);
    }

三、进程的状态转换

1.三种基本状态

(1)运行态(running):进程正在处理机上运行。

(2)就绪态(ready):进程具备运行条件,等待系统分配处理器以便运行。

(3)等待态(blocked):不具备运行条件,正在等待某个事件的完成。

2.四种状态转换

(1)运行态→等待态:当进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如I/O操作的完成)时,它就从运行状态转换为等待状态。

(2)等待态→就绪态:当进程等待的事件到来时(如I/O操作结束或中断结束),中断处理程序必须把相应进程的状态由等待状态转换为就绪状态。

(3)运行态→就绪态:处于运行状态的进程在时间片用完后,不得不让出处理机,从而进程由运行状态转换为就绪状态。

(4)就绪态→运行态:处于就绪状态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪状态转换为运行状态。

3.状态转换图

(1)三态模型

(2)多态模型

四、进程的调度

1.调度方式

(1)非剥夺调度方式(非抢占方式):实现简单,系统开销小,适用于大多数的批处理系统,但它不能用于分时系统和大多数的实时系统。

(2)剥夺调度方式(抢占方式):要遵循一定的原则,主要有:优先级、短进程优先和时间片原则。

2.调度算法

(1)先来先服务(FCFS first come first serve):属于不可剥夺算法。算法每次从后备作业队列中选择最先进入该队列的一个或几个作业进行处理。

特点:算法简单,效率低,对长作业有利,对短作业不利。

(2)短作业优先(SJF short job first):算法从后备队列中选择一个或若干个估计运行时间最短的作业处理。直到完成作业或发生某事件而阻塞时,才释放处理机。

缺点:(1)对长作业不利,造成“饥饿”现象(2)未考虑作业紧迫程度(3)由于运行时间是估计所得,所以并不一定能做到短作业优先。

(3)优先级:可分为(1)非剥夺式(2)剥夺式;其中优先级可分为:(1)静态优先级(2)动态优先级

(4)高响应比优先:响应比=(等待时间+处理时间)/处理时间=1+等待时间/处理时间

(5)时间片轮转

五、CFS调度器

1.设计思想:根据各个进程的权重分配运行时间

2.虚拟运行时间

vruntime = (调度周期 * 进程权重 / 所有进程总权重) * 1024 / 进程权重

=调度周期 * 1024 / 所有进程总权重

通过公式可知,所有进程的vruntime增长速度宏观上看是同时推进的,那么就可以用这个vruntime来选择运行的进程,vruntime值越小说明以前占用cpu的时间越短,受到了“不公平”的对待,因此下一个运行进程就是它。这样既能公平选择进程,又能保证高优先级进程获得较多的运行时间。

3.调度实体

调度实体sched_entity,代表一个调度单位,在组调度关闭的时候可以把他等同为进程。
每一个task_struct中都有一个sched_entity,进程的vruntime和权重都保存在这个结构中。所有的sched_entity以vruntime为key插入到红黑树中,同时缓存树的最左侧节点,也就是vruntime最小的节点,这样可以迅速选中vruntime最小的进程。

关系图如下:

4.主要代码

(1)创建进程

进程创建时CFS相关变量的初始化:

void wake_up_new_task(struct task_struct *p, unsigned long clone_flags)
{
    .....
    if (!p->sched_class->task_new || !current->se.on_rq) {
        activate_task(rq, p, 0);
    } else {
        /*
         * Let the scheduling class do new task startup
         * management (if any):
         */
        p->sched_class->task_new(rq, p);
        inc_nr_running(rq);
    }
    check_preempt_curr(rq, p, 0);
    .....
}  

Linux创建进程使用fork或者clone或者vfork等系统调用,最终都会到do_fork。
如果没有设置CLONE_STOPPED,则会进入wake_up_new_task函数。

(2)唤醒进程

static int try_to_wake_up(struct task_struct *p, unsigned int state, int sync)
{
    int cpu, orig_cpu, this_cpu, success = 0;
    unsigned long flags;
    struct rq *rq;
    rq = task_rq_lock(p, &flags);
    if (p->se.on_rq)
        goto out_running;
    update_rq_clock(rq);
    activate_task(rq, p, 1);
    success = 1;
out_running:
    check_preempt_curr(rq, p, sync);
    p->state = TASK_RUNNING;
out:
    current->se.last_wakeup = current->se.sum_exec_runtime;
    task_rq_unlock(rq, &flags);
    return success;
}

update_rq_clock就是更新cfs_rq的时钟,保持与系统时间同步。
重点是activate_task,它将进程加入红黑树并且对vruntime做一些调整,然后用check_preempt_curr检查是否构成抢占条件,如果可以抢占则设置TIF_NEED_RESCHED标识。

(3)进程调度

asmlinkage void __sched schedule(void)
{
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq *rq;
    int cpu;
need_resched:
    preempt_disable(); //在这里面被抢占可能出现问题,先禁止它
    cpu = smp_processor_id();
    rq = cpu_rq(cpu);
    rcu_qsctr_inc(cpu);
    prev = rq->curr;
    switch_count = &prev->nivcsw;
    release_kernel_lock(prev);
need_resched_nonpreemptible:
    spin_lock_irq(&rq->lock);
    update_rq_clock(rq);
    clear_tsk_need_resched(prev); //清除需要调度的位
    /*state==0是TASK_RUNNING,不等于0就是准备睡眠,正常情况下应该将它移出运行队列
    但是还要检查下是否有信号过来,如果有信号并且进程处于可中断睡眠就唤醒它
    对于需要睡眠的进程,这里调用deactive_task将其移出队列并且on_rq也被清零*/
    if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
        if (unlikely(signal_pending_state(prev->state, prev)))
            prev->state = TASK_RUNNING;
        else
            deactivate_task(rq, prev, 1);
        switch_count = &prev->nvcsw;
    }
    if (unlikely(!rq->nr_running))
        idle_balance(cpu, rq);
    prev->sched_class->put_prev_task(rq, prev);
    next = pick_next_task(rq, prev);
    if (likely(prev != next)) {
        sched_info_switch(prev, next);
        rq->nr_switches++;
        rq->curr = next;
        ++*switch_count;
        //完成进程切换
        context_switch(rq, prev, next); /* unlocks the rq */
        /*
         * the context switch might have flipped the stack from under
         * us, hence refresh the local variables.
         */
        cpu = smp_processor_id();
        rq = cpu_rq(cpu);
    } else
        spin_unlock_irq(&rq->lock);
    if (unlikely(reacquire_kernel_lock(current) < 0))
        goto need_resched_nonpreemptible;
    preempt_enable_no_resched();
    //这里新进程也可能有TIF_NEED_RESCHED标志,如果新进程也需要调度则再调度一次
    if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))
        goto need_resched;
}

(4)时钟中断

时钟中断在time_init_hook中初始化,中断函数为timer_interrupt。

entity_tick函数:更新状态信息,检测是否满足抢占条件。

static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
    /*
     * Update run-time statistics of the ‘current‘.
     */
    update_curr(cfs_rq);
    //....无关代码
    if (cfs_rq->nr_running > 1 || !sched_feat(WAKEUP_PREEMPT))
        check_preempt_tick(cfs_rq, curr);
}

5.CFS小结

CFS还有一个重要特点,即调度粒度小。CFS之前的调度器中,除了进程调用了某些阻塞函数而主动参与调度之外,每个进程都只有在用完了时间片或者属于自己的时间配额之后才被抢占。而CFS则在每次tick都进行检查,如果当前进程不再处于红黑树的左边,就被抢占。在高负载的服务器上,通过调整调度粒度能够获得更好的调度性能。

六、对Linux进程模型的看法

普通进程的调度策略和非实时进程相比较为麻烦,因为它不能简单地只看优先级,必须公平的占有CPU,否则容易出现进程饥饿,造成用户响应慢的问题。因此,Linux在发展历程中不断对调度器进行改善,希望寻找一个最接近于完美的调度策略来公平快速地调度进程。CFS是Linux内核2.6.23版本开始采用的进程调度器,核心思想是“完全公平”,它将所有的进程都统一对待,实现了所有进程的公平调度。虽然CFS性能优越,避免了上一代调度器O(1)带来的很多问题,但以Linux精益求精的精神来看,我相信今后将会出现一个更优秀的调度器来取代CFS,满足更多的需求。

七、参考资料

1.http://blog.51cto.com/xuding/1741861 Linux进程管理

2.https://blog.csdn.net/qwe6112071/article/details/70473905 操作系统之进程的状态和转换详解

3.https://blog.csdn.net/yusiguyuan/article/details/39404399 linux内核CFS进程调度策略

原文地址:https://www.cnblogs.com/marsur/p/8970637.html

时间: 2024-10-08 23:28:17

第一次作业:深入Linux源码分析其进程模型的相关文章

第一次作业:深入源码分析xv6进程模型

1.进程 1.1 进程的概念 1) 狭义定义:进程是正在运行的程序的实例. 2) 广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动.它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元. 1.2 进程的组成 1.3 进程控制块 引用来自:https://blog.csdn.net/hgnuxc_1993/article/details/54847732 2.操作系统如何组织进程 在这里把组织进程理解为管理和控制进程 操作系统通过P

第一次作业:深入源码分析进程模型(Linux kernel 2.6.32)

1.前言 本文基于Linux 2.6.32分析其进程模型,包括进程的概念.组织.转换.调度等内容,帮助对操作系统课程及Linux相关知识的理解和学习. 附Linux Kernel 2.6.32源码下载地址: https://mirrors.edge.kernel.org/pub/linux/kernel/v2.6/linux-2.6.32.tar.gz 2.进程的概念 2.1什么是进程? 在正式开始结合源代码分析进程模型之前,我们首先需要搞清楚进程的究竟是什么. 维基百科上对于进程的定义如下:

第一次作业:深入源码分析进程模型(linux)

一.什么是进程 计算机上有许多可以运行的软件,其中也包括操作系统,这些软件运行时,就产生了一个或多个进程. 二.Linux系统中进程的组织与转换 1>Linux中进程的描述符(即用来描述一个进程的结构体) struct task_struct { ...... volatile long state; // 描述进程的运行状态 void *stack; // 指向内核栈 struct list_head tasks; // 用于加入进程链表 ...... struct mm_struct *mm

第一次作业:Linux 2.6.32的进程模型与调度器分析

1.前言 本文分析的是Linux 2.6.32版的进程模型以及调度器分析.在线查看 源码下载 本文主要讨论以下几个问题: 什么是进程?进程是如何产生的?进程都有那些? 在操作系统中,进程是如何被管理以及它们是怎样被调用的? 2.进程模型 2.1进程的概念 在我的理解中,一个程序就相当于一个进程,程序的启动意味着产生了一个新的进程,程序的关闭也就意味着一个进程的消亡. 那么专业定义应该是: 在计算中,进程是正在执行的计算机程序的一个实例. 它包含程序代码及其当前活动. 根据操作系统(OS),一个进

第一次作业:深入源码分析进程模型

前言:          这是一篇关于linux操作系统的简单介绍.linux本身不能算是操作系统,只是一个内核,基于linux内核的操作系统有很多,比如流行的android,ubuntu,红旗linux等等.Linux以它的高效性和灵活性著称.它能够在PC计算机上实现全部的Unix特性,具有多任务.多用户的能力.Linux是在GNU公共许可权限下免费获得的,是一个符合POSIX标准的操作系统.Linux操作系统软件包不仅包括完整的Linux操作系统,而且还包括了文本编辑器.高级语言编译器等应用

linux源码分析之字节序(5)-- swab.h

在linux源码分析之字节序(3).linux源码分析之字节序(4)中都有看到,源码中包含了 #include <linux/swab.h> 该头函数里面介绍了字节交换的具体方法.我们来看看具体代码: --------------------------------------------------------------------------------------------------------------- #ifndef _LINUX_SWAB_H #define _LINUX

linux源码分析之字节序(2)-- types.h

这一节主要讲linux的数据类型,主要是为了方便理解接下来将大端.小段字节序定义的源码. 首先,来看看 include/linux/types.h 源码: ------------------------------------------------------------------ #ifndef _LINUX_TYPES_H #define _LINUX_TYPES_H #include <asm/types.h> #ifndef __ASSEMBLY__ #include <l

linux源码分析之字节序(4)-- little_endian.h

本节主要分析小端字节顺序. 首先,我们要回顾上一节讲过的大端.小端的概念: 字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有小端.大端两种字节顺序.小端字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处:大端字节序是高字节数据存放在低地址处,低字节数据存放在高地址处.基于X86平台的PC机是小端字节序的,而有的嵌入式平台则是大端字节序的.因而对int.uint16.uint32等多于1字节类型的数据,在这些嵌入式平台上应该变换其存储顺序.通常我们认为,在空中传输

Android源码分析--system_server进程分析

在上一篇博文中我们进行了有关Zygote进程的分析,我们知道Zygote进程创建了一个重要的进程–system_server进程后就进入了无限循环中,之后Android系统中的重要任务就交给了system_server进程,作为zygote的嫡长子进程,system_server进程的意义非凡,今天我们来分析一下system_server进程. 创建system_server进程 在ZygoteInit中main方法中,通过调用startSystemServer方法开启了system_serve