前言:
基于Linux,深入源码分析其进程模型,具体包含如下内容:
- 操作系统是怎么组织进程的
- 进程状态如何转换(给出进程状态转换图)
- 进程是如何调度的
- 谈谈自己对该操作系统进程模型的看法
一、操作系统是怎么组织进程的
1.进程的概念
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程应该包含以下内容:
-
程序的代码,既然进程是一个正在运行的程序,自然需要程序的代码
-
程序的数据
-
CPU寄存器的值,包括通用寄存器,程序计数器
-
堆(heap)是用来保存进程运行时动态分配的内存空间
-
栈(stack)有两个用途,1保存运行的上下文信息。2在函数调用时保存被调用函数的形参或者局部变量
-
进程所占用的一组系统资源,如打开的文件
2.进程的特性
3.进程的标识符
每个进程有进程标识符、用户标识符、组标识符。
域名 |
含义 |
Pid |
进程标识符 |
Uid、gid |
用户标识符、组标识符 |
Euid、egid |
有效用户标识符、有效组标识符 |
Suid、sgid |
备份用户标识符、备份组标识符 |
Fsuid、fsgid |
文件系统用户标识符、文件系统组标识符 |
4.进程控制块
系统为每个进程维护了一个进程控制块(Process Control Block,PCB),用来保存与该进程有关的各种状态信息。PCB是进程的唯一标识。每生成一个新的进程,就要为它创造一个PCB然后对其进行初始化。若要撤销一个进程时只要回收PCB即可。进程切换就可以通过操作PCB进行。PCB只是基本原理中的说法,对于Linux被叫做任务结构体(task struct)。定义如下:
1 struct task_struct 2 { 3 volatile long state; 4 pid_t pid; 5 unsigned long timestamp; 6 unsigned long rt_priority; 7 struct mm_struct *mm, *active_mm 8 }
5.进程查看
二、进程的状态如何转换
1.进程的状态
进程是一个动态的实体,所以他是有生命的。从创建到消亡,是一个进程的整个生命周期。在这个周期中,进程可能会经历各种不同的状态。一般来说,所有进程都要经历以下的3个状态:
(1)就绪态。指进程已经获得所有所需的其他资源,正在申请处理处理器资源,准备开始执行。这种情况下,称进程处于就绪态。
(2)阻塞态。指进程因为需要等待所需资源而放弃处理器,或者进程本不拥有处理器,且其他资源也没有满足,从而即使得到处理器也不能开始运行。这种情况下,进程处于阻塞态。阻塞状态也称休眠状态或者等待状态。
(3)运行态。进程得到了处理器,并不需要等待其他任何资源,正在执行的状态,称之为运行态。只有在运行态时,进程才可以使用所申请到的资源。
在Linux系统中,将各种状态进行了重新组织,由此得到了Linux进程的几个状态:
- RUNNING:正在运行或者在就绪队列中等待运行的进程。也就是上面提到的运行态和就绪态进程的综合。一个进程处于RUNNING状态,并不代表他一定在被执行。由于在多任务系统中,各个就绪进程需要并发执行,所以在某个特定时刻,这些处于RUNNING状态的进程之中,只有一个能得到处理器,而其他进程必须在一个就绪队列中等待。即使是在多处理器的系统中,Linux也只能同时让一个处理器执行任务。
- UNINTERRUPTABLE:不可中断阻塞状态。处于这种状态的进程正在等待队列中,当资源有效时,可由操作系统进行唤醒,否则,将一直处于等待状态。
- INTERRUPTABLE:可中断阻塞状态。与不可中断阻塞状态一样,处于这种状态的进程在等待队列中,当资源有效时,可以有操作系统进行唤醒。与不可中断阻塞状态有所区别的是,处于此状态中的进程亦可被其他进程的信号唤醒。
- STOPPED:挂起状态。进程被暂停,需要通过其它进程的信号才能被唤醒。导致这种状态的原因有两种。其一是受到相关信号(SIGSTOP,SIGSTP,SIGTTIN或SIGTTOU)的反应。其二是受到父进程ptrace调用的控制,而暂时将处理器交给控制进程。
- ZOMBIE:僵尸状态。表示进程结束但尚未消亡的一种状态。此时进程已经结束运行并释放掉大部分资源,但尚未释放进程控制块。
2.进程状态的转换
如下图所示:
三、进程是如何调度的
1.简介
进程调度:在操作系统中调度是指一种资源分配。
Linux进程调度采用的是抢占式多任务处理,所以进程之间的挂起和继续运行无需彼此之间的协作。
Linux在进行进程调度的时候把进程分为两种:1.普通进程;2.实时进程;实时进程的优先级永远比普通进程的优先级高。
2.进程调度的策略
-
SCHED_OTHER 分时调度策略。
-
SCHED_FIFO实时调度策略,先到先服务。
-
SCHED_RR实时调度策略,时间片轮转。
3.进程优先级
-
使用nice值:越大的nice值意味着更低的优先级。 (-19 ~ 20之间)
-
实时优先级:可配置,越高意味着进程优先级越高。
-
时间片:Linux中并不是以固定的时间值(如10ms)来分配时间片的,而是将处理器的使用比作为“时间片”划分给进程。
4.调度器
linux的调度程序由两个调度器组成:主调度器,周期性调度器(两者又统称为通用调度器(generic scheduler)或核心调度器(core scheduler))。
(1)主调度器
Linux主调度器的演变
字段 | 版本 |
---|---|
O(n)的始调度算法 | linux-0.11~2.4 |
O(1)调度器 | linux-2.5 |
CFS调度器 | linux-2.6~至今 |
sched_setscheduler函数
1 asmlinkage void __sched schedule(void) 2 { 3 long *switch_count;//进程切换的次数 4 task_t *prev, *next; 5 runqueue_t *rq; 6 prio_array_t *array; 7 struct list_head *queue; 8 unsigned long long now; 9 unsigned long run_time; 10 int cpu, idx; 11 12 /* 13 * Test if we are atomic. Since do_exit() needs to call into 14 * schedule() atomically, we ignore that path for now. 15 * Otherwise, whine if we are scheduling when we should not be. 16 */ 17 if (likely(!(current->state & (TASK_DEAD | TASK_ZOMBIE)))) { 18 if (unlikely(in_atomic())) { 19 printk(KERN_ERR "bad: scheduling while atomic!\n"); 20 dump_stack(); 21 } 22 } 23 24 need_resched: 25 preempt_disable();//禁用内核抢占 26 prev = current; 27 rq = this_rq();//获得当前CPU的运行队列 28 29 /* 30 * The idle thread is not allowed to schedule! 31 * Remove this check after it has been exercised a bit. 32 */ 33 if (unlikely(current == rq->idle) && current->state != TASK_RUNNING) { 34 printk(KERN_ERR "bad: scheduling from the idle thread!\n"); 35 dump_stack(); 36 } 37 38 release_kernel_lock(prev);//保证PREV不占用大内核锁 39 schedstat_inc(rq, sched_cnt); 40 now = sched_clock(); 41 if (likely(now - prev->timestamp < NS_MAX_SLEEP_AVG)) 42 run_time = now - prev->timestamp; 43 else 44 run_time = NS_MAX_SLEEP_AVG; 45 46 /* 47 * Tasks with interactive credits get charged less run_time 48 * at high sleep_avg to delay them losing their interactive 49 * status 50 */ 51 if (HIGH_CREDIT(prev)) 52 run_time /= (CURRENT_BONUS(prev) ? : 1);//PREV 所使用的CPU的时间长度 53 54 spin_lock_irq(&rq->lock);//在寻找新进程前先禁止本地中断 55 56 /* 57 * if entering off of a kernel preemption go straight 58 * to picking the next task. 59 */ 60 switch_count = &prev->nivcsw; 61 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {//PREV非可运行状态且没有被抢占 62 switch_count = &prev->nvcsw; 63 if (unlikely((prev->state & TASK_INTERRUPTIBLE) && 64 unlikely(signal_pending(prev))))//如果是在等待队列中,则置为运行状态 65 prev->state = TASK_RUNNING; 66 else 67 deactivate_task(prev, rq);//从运行队列中移除PREV 68 } 69 70 cpu = smp_processor_id(); 71 if (unlikely(!rq->nr_running)) {//如果运行队列中没有可运行的进程 72 go_idle: //则 73 idle_balance(cpu, rq); //从另外的CPU上迁移一些进程来 74 if (!rq->nr_running) {//如果还是没有可运行的进程 75 next = rq->idle; //则调用IDLE进程 76 rq->expired_timestamp = 0; 77 wake_sleeping_dependent(cpu, rq); 78 /* 79 * wake_sleeping_dependent() might have released 80 * the runqueue, so break out if we got new 81 * tasks meanwhile: 82 */ 83 if (!rq->nr_running) 84 goto switch_tasks; 85 } 86 } else { 87 if (dependent_sleeper(cpu, rq)) { 88 schedstat_inc(rq, sched_goidle); 89 next = rq->idle; 90 goto switch_tasks; 91 } 92 /* 93 * dependent_sleeper() releases and reacquires the runqueue 94 * lock, hence go into the idle loop if the rq went 95 * empty meanwhile: 96 */ 97 if (unlikely(!rq->nr_running)) 98 goto go_idle; 99 } 100 101 array = rq->active;//有可运行的进程 102 if (unlikely(!array->nr_active)) {//活动进程链表中没有可运行的 103 //则交换active和expired 104 /* 105 * Switch the active and expired arrays. 106 */ 107 schedstat_inc(rq, sched_switch); 108 rq->active = rq->expired; 109 rq->expired = array; 110 array = rq->active; 111 rq->expired_timestamp = 0; 112 rq->best_expired_prio = MAX_PRIO; 113 } else 114 schedstat_inc(rq, sched_noswitch); 115 //搜索活动进程集合位掩码的第一个非0位 116 idx = sched_find_first_bit(array->bitmap); 117 queue = array->queue + idx; 118 next = list_entry(queue->next, task_t, run_list);//找到这个进程描述符 119 120 if (!rt_task(next) && next->activated > 0) { 121 //更新睡眠时间和动态优先级 122 unsigned long long delta = now - next->timestamp; 123 124 if (next->activated == 1) 125 delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128; 126 127 array = next->array; 128 dequeue_task(next, array); 129 recalc_task_prio(next, next->timestamp + delta); 130 enqueue_task(next, array); 131 } 132 next->activated = 0; 133 switch_tasks: 134 prefetch(next); 135 clear_tsk_need_resched(prev);//清除TIF_NEED_RESCHED标志 136 rcu_qsctr_inc(task_cpu(prev)); 137 138 prev->sleep_avg -= run_time; 139 if ((long)prev->sleep_avg <= 0) { 140 prev->sleep_avg = 0; 141 if (!(HIGH_CREDIT(prev) || LOW_CREDIT(prev))) 142 prev->interactive_credit--; 143 } 144 prev->timestamp = prev->last_ran = now; 145 146 sched_info_switch(prev, next); 147 if (likely(prev != next)) {//如果PREV不等于NEXT则进行切换 148 next->timestamp = now; 149 rq->nr_switches++; 150 rq->curr = next; 151 ++*switch_count; 152 153 prepare_arch_switch(rq, next); 154 prev = context_switch(rq, prev, next); 155 barrier(); 156 157 finish_task_switch(prev); 158 } else 159 spin_unlock_irq(&rq->lock); 160 161 reacquire_kernel_lock(current); 162 preempt_enable_no_resched(); 163 if (unlikely(test_thread_flag(TIF_NEED_RESCHED)))//如果TIF_NEED_RESCHED置位则再次进行调度 164 goto need_resched; 165 }
(2)周期调度器
scheduler_tick函数
/* * This function gets called by the timer code, with HZ frequency. * We call it with interrupts disabled. */ void scheduler_tick(void) { /* 1. 获取当前cpu上的全局就绪队列rq和当前运行的进程curr */ /* 1.1 在于SMP的情况下,获得当前CPU的ID。如果不是SMP,那么就返回0 */ int cpu = smp_processor_id(); /* 1.2 获取cpu的全局就绪队列rq, 每个CPU都有一个就绪队列rq */ struct rq *rq = cpu_rq(cpu); /* 1.3 获取就绪队列上正在运行的进程curr */ struct task_struct *curr = rq->curr; sched_clock_tick(); /* 2 更新rq上的统计信息, 并执行进程对应调度类的周期性的调度 */ /* 加锁 */ raw_spin_lock(&rq->lock); /* 2.1 更新rq的当前时间戳.即使rq->clock变为当前时间戳 */ update_rq_clock(rq); /* 2.2 执行当前运行进程所在调度类的task_tick函数进行周期性调度 */ curr->sched_class->task_tick(rq, curr, 0); /* 2.3 更新rq的负载信息, 即就绪队列的cpu_load[]数据 * 本质是讲数组中先前存储的负荷值向后移动一个位置, * 将当前负荷记入数组的第一个位置 */ update_cpu_load_active(rq); /* 2.4 更新cpu的active count活动计数 * 主要是更新全局cpu就绪队列的calc_load_update*/ calc_global_load_tick(rq); /* 解锁 */ raw_spin_unlock(&rq->lock); /* 与perf计数事件相关 */ perf_event_task_tick(); #ifdef CONFIG_SMP /* 当前CPU是否空闲 */ rq->idle_balance = idle_cpu(cpu); /* 如果到是时候进行周期性负载平衡则触发SCHED_SOFTIRQ */ trigger_load_balance(rq); #endif rq_last_tick_reset(rq); }
4.调度算法
调度算法是指: 根据系统的资源分配策略所规定的资源分配算法。
先来先服务(FCFS)调度算法是一种最简单的调度算法,该算法既可用于作业调度,也可用于进程调度。当在作业调度中采用该算法时,每次调度都是从后备作业队列中选择一个或多个最先进入该队列的作业,将它们调入内存,为它们分配资源、创建进程,然后放入就绪队列。在进程调度中采用FCFS算法时,则每次调度是从就绪队列中选择一个最先进入该队列的进程,为之分配处理机,使之投入运行。该进程一直运行到完成或发生某事件而阻塞后才放弃处理机。
FIFO算法服务质量不佳,容易引起作业用户不满,常作为一种辅助调度算法。
特点:(1)有利于长作业,不利于短作业;
(2)有利于处理机繁忙的作业,不利于I/O繁忙的作业;
b.短作业(进程)优先调度算法SJ(P)F
短作业(进程)优先调度算法SJ(P)F,是指对短作业或短进程优先调度的算法。它们可以分别用于作业调度和进程调度。短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业,将它们调入内存运行。而短进程优先(SPF)调度算法则是从就绪队列中选出一个估计运行时间最短的进程,将处理机分配给它,使它立即执行并一直执行到完成,或发生某事件而被阻塞放弃处理机时再重新调度。
该算法虽可获得较好的调度性能,但难以准确地知道下一个CPU执行期,而只能根据每一个进程的执行历史来预测。
(2)高优先权优先调度算法
a.优先权调度算法的类型(FPF)
为了照顾紧迫型作业,使之在进入系统后便获得优先处理,引入了最高优先权优先(FPF)调度算法。此算法常被用于批处理系统中,作为作业调度算法,也作为多种操作系统中的进程调度算法,还可用于实时系统中。当把该算法用于作业调度时,系统将从后备队列中选择若干个优先权最高的作业装入内存。当用于进程调度时,该算法是把处理机分配给就绪队列中优先权最高的进程,这时,又可进一步把该算法分成如下两种。
1) 非抢占式优先权算法
在这种方式下,系统一旦把处理机分配给就绪队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可再将处理机重新分配给另一优先权最高的进程。这种调度算法主要用于批处理系统中;也可用于某些对实时性要求不严的实时系统中。
2) 抢占式优先权调度算法
在这种方式下,系统同样是把处理机分配给优先权最高的进程,使之执行。但在其执行期间,只要又出现了另一个其优先权更高的进程,进程调度程序就立即停止当前进程(原优先权最高的进程)的执行,重新将处理机分配给新到的优先权最高的进程。因此,在采用这种调度算法时,是每当系统中出现一个新的就绪进程i 时,就将其优先权Pi与正在执行的进程j 的优先权Pj进行比较。如果Pi≤Pj,原进程Pj便继续执行;但如果是Pi>Pj,则立即停止Pj的执行,做进程切换,使i 进程投入执行。显然,这种抢占式的优先权调度算法能更好地满足紧迫作业的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。
(3)基于时间片的轮转调度算法
a.时间片轮转法
前几种算法主要用于批处理系统中,不能作为分时系统中的主调度算法,在分时系统中,都采用时间片轮转法。
(4)几种算法的比较:
四、谈谈自己对该操作系统进程模型的看法
对该操作系统进程模型的看法
我对接触Linux不多,对Linux平台的开发更是一无所知。 而现在的趋势越来越表明,作为一个优秀的软件开发人员,或计算机IT行业从业人员, 掌握Linux是一种很重要的谋生资源与手段。回顾过去十年,Linux早期被使用者部署到各自的行业来增加竞争优势,特别在金融服务行业尤为普遍。如今,Linux作为云计算、大数据和移动时代的首选操作系统,重新树立行业发展趋势。因此,如同2004年的市场发展,未来Linux发展前途无量,可见学习Linux系统是如此之重要。
五、参考文献
https://blog.csdn.net/woshixuye/article/details/53931303
http://www.cnblogs.com/chenglei/archive/2009/11/20/1606881.html
http://www.bubuko.com/infodetail-2573299.html
https://blog.csdn.net/gatieme/article/details/51872561