1.前言

本文主要基于Linux 2.6源代码分析进程模型。源代码下载地址:https://elixir.bootlin.com/linux/v2.6.39/source

2.进程

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

3.Linux系统进程的组织

进程是由进程控制块(PCB)、程序段、数据段三部分组成。

3.1 进程控制块

进程控制块(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态,是系统为了管理进程设置的一个专门的数据结构。在Linux中,这个结构叫做task_struct。task_struct被定义在/include/linux/sched.h中。

源代码地址

PCB包含信息:

  • 进程标识符:每个进程都必须有一个唯一的标识符
    pid_t pid; //进程的唯一标识
    pid_t tgid; //线程组的领头线程的pid成员的值
  • 进程状态
    volatile long state;
  • 进程优先级
    int prio, static_prio, normal_prio;
    unsigned int rt_priority;

    prio表示进程的动态优先级,static_prio表示进程的静态优先级,normal_prio表示基于进程的静态优先级和调度策略计算出的优先级,rt_priority表示实时进程的优先级。

  • CPU现场保护区
  • 进程相应的程序和数据地址
  • 进程资源清单
  • 信号处理信息
  • 文件系统的信息
  • 与进程有关的其他信息

3.2 程序段

程序段:被CPU执行的程序代码

3.3 数据段

数据段:进程对应的程序中原来的数据或程序执行后产生的结果。

4.Linux系统中进程的状态及转换

4.1 进程的状态

  • 进程的三种基本状态:

          运行态(该时刻进程实际占用CPU)

          就绪态(可运行,但因为其他进程正在运行而暂时停止)

          阻塞态(除非某种外部事件发生,否则进程不能运行)

  • 在Linux系统中,进程的状态有以下几种,定义在/include/linux/sched.h中。

          源代码地址:https://elixir.bootlin.com/linux/v2.6.37/source/include/linux/sched.h#L182 

#define TASK_RUNNING        0      //可执行状态
#define TASK_INTERRUPTIBLE    1    //可中断的睡眠状态
#define TASK_UNINTERRUPTIBLE    2  //不可中断的睡眠状态
#define __TASK_STOPPED        4    //暂停状态
#define __TASK_TRACED        8     //跟踪状态
/* in tsk->exit_state */
//终止状态
#define EXIT_ZOMBIE        16
#define EXIT_DEAD        32
TASK_RUNNING是就绪态,进程当前只等待CPU资源。
TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE都是阻塞态,进程当前正在等待除CPU外的其他系统资源;前者可以被信号唤醒,后者不可以。
ZOMBIE是僵尸进程,进程已经结束运行,但是进程控制块尚未注销。
TASK_STOPPED是挂起状态,主要用于调试目的。进程接收到SIGSTOP信号后会进入该状态,在接收到SIGCONT后又会恢复运行。

4.2 进程的创建

在Linux中主要提供了fork()、vfork()、clone()三个进程创建方法。

在Linux系统中可以使用fork()来创建一个进程,fork()函数用于从已存在的进程中创建一个新的进程。新进程为子进程,原进程为父进程。使用fork()函数得到的子进程时父进程的一个复制品,它从父进程处继承了整个进程的地址空间,包括进程上下文,代码段,进程堆栈,内存信息,文件描述符,信号控制设定,进程优先级,进程组号,当前工作目录,根目录,资源限制和控制终端等,而子进程所独有的只有它的进程号,资源使用和计时器等子进程几乎是父进程的完全复制,所以父子进程会同时运行一个程序。vfork()函数则只复制task_struct和内核堆栈。

4.3 状态的转换

进程状态转换图如下图所示:

 

1.进程因为等待输入而被阻塞

2.调度程序选择另一个进程

3.调度程序选择这个进程

4.出现有效输入

5.进程结束

4.4进程的终止

  • 正常退出
  • 出错退出
  • 严重错误
  • 被其他进程杀死

5.进程的调度

当计算机系统是多道程序设计系统时,通常就会有多个进程通时竞争CPU。只要有两个或更多的进程处于就绪状态,这种情形就会发生。如果只有一个CPU可用,那么就必须选择下一个要运行的进程。完成选择工作的部分称为调度,使用的算法称为调度算法。

5.1 何时调度

      有关调度的一个关键问题是何时进行调度决策。

  • 在创建一个新进程之后,需要决定是运行父进程还是运行子进程。这两种进程都处于就绪状态,可以任意决定。
  • 在一个进程退出时必须做出调度决策。从就绪进程中选择某个进程,如果没有就绪的进程,通常会运行一个系统提供的空闲进程。
  • 当一个进程阻塞在I/O和信号量上或由于其他原因阻塞时,必须选择另一个进程运行。
  • 在一个I/O中断发生时,必须做出调度决策。

 

5.2 如何调度

 

schedule():进程调度函数,由它来完成进程的调度。该函数的主要流程如下:先关闭内核抢占,找到当前CPU上的就绪队列,检查prev的状态,如果是非运行状态的,且在内核中没有被抢占,从队列rq中删除。但如果prev有挂起信号,设置其状态为TASK_RUNNING状态,保留在队列rq中。然后挑选优先级高的下一个进程,并且通知调度器即将进行切换,更新队列中保存的进程信息,最后通知调度类,完成进程切换。

源代码地址:https://elixir.bootlin.com/linux/v2.6.39/source/kernel/sched.c

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();        //找到当前CPU上的就绪队列rq
    rq = cpu_rq(cpu);                
    rcu_note_context_switch(cpu);
    prev = rq->curr;                 //将正在运行的进程保存在prev

    schedule_debug(prev);

    if (sched_feat(HRTICK))
        hrtick_clear(rq);

    raw_spin_lock_irq(&rq->lock);

    switch_count = &prev->nivcsw;   //切换次数记录
    if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { //当前进程非运行状态,并且非内核抢占
       
        if (unlikely(signal_pending_state(prev->state, prev))) { //若不是非挂起信号,设置进程为就绪状态
            prev->state = TASK_RUNNING;
        } else { //若为非挂起信号,则将其从队列中移出
            /*
             * If a worker is going to sleep, notify and
             * ask workqueue whether it wants to wake up a
             * task to maintain concurrency.  If so, wake
             * up the task.
             */
            if (prev->flags & PF_WQ_WORKER) {
                struct task_struct *to_wakeup;

                to_wakeup = wq_worker_sleeping(prev, cpu);
                if (to_wakeup)
                    try_to_wake_up_local(to_wakeup);
            }
            deactivate_task(rq, prev, DEQUEUE_SLEEP);

            /*
             * If we are going to sleep and we have plugged IO queued, make
             * sure to submit it to avoid deadlocks.
             */
            if (blk_needs_flush_plug(prev)) {
                raw_spin_unlock(&rq->lock);
                blk_schedule_flush_plug(prev);
                raw_spin_lock(&rq->lock);
            }
        }
        switch_count = &prev->nvcsw;
    }

    pre_schedule(rq, prev); //通知调度器,即将发生进程切换

    if (unlikely(!rq->nr_running))
        idle_balance(cpu, rq);

    put_prev_task(rq, prev); //通知调度器,即将用另一个进程替换当前进程
    next = pick_next_task(rq); //挑选可运行的任务
    clear_tsk_need_resched(prev); //清除pre的TIF_NEED_RESCHED标志
    rq->skip_clock_update = 0;

    if (likely(prev != next)) { //如果不是同一个进程
        rq->nr_switches++;      
        rq->curr = next;        //将当前进程切换成挑选的那个进程
        ++*switch_count;        //切换次数更新

        context_switch(rq, prev, next); /* unlocks the rq */ //进程上下文切换
        /*
         * The context switch have flipped the stack from under us
         * and restored the local variables which were saved when
         * this task called schedule() in the past. prev == current
         * is still correct, but it can be moved to another cpu/rq.
         */
        cpu = smp_processor_id();
        rq = cpu_rq(cpu);
    } else
        raw_spin_unlock_irq(&rq->lock);

    post_schedule(rq); //通知调度类,完成进程切换

    preempt_enable_no_resched();
    if (need_resched())  //如果该进程被其他进程设置了TIF_NEED_RESCHED标志,则函数重新执行进行调度
        goto need_resched;
}

 

6.对操作系统模型的看法

      操作系统是实现资源管理,程序控制和人机交互的计算机程序。系统的设备资源和信息资源是操作系统根据用户需求按一定的策略来进行分配和调度的 ;一个用户程序的执行自始至终是在操作系统控制下进行的;操作系统的人机交互功能可以使计算机系统更“友善”。

7.参考资料

https://baike.baidu.com/item/%E8%BF%9B%E7%A8%8B/382503?fr=aladdin

https://blog.csdn.net/u013592097/article/details/52530129

https://blog.csdn.net/hzk8656511/article/details/52204016

http://blog.sina.com.cn/s/blog_9ca3f6e70102wkwq.html

 

 posted on 2018-05-01 18:00  林小琼  阅读(139)  评论(0编辑  收藏  举报