第一次作业:基于Linux内核源码进程模型分析
Posted Cccyh
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第一次作业:基于Linux内核源码进程模型分析相关的知识,希望对你有一定的参考价值。
一、关于进程
1.1 什么是进程?
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
简单的说,一个进程就是一个正在运行的程序。
1.2 进程的生命周期
创建: 每个进程都是由其父进程创建进程可以创建子进程,子进程又可以创建子进程的子进程
运行: 多个进程可以同时存在进程间可以通信
撤销: 进程可以被撤销,从而结束一个进程的运行
(1)进程的创建:进程是通过调用::fork(),::vfork()和::clone()系统调用创建新进程。在内核中,它们都是调用do_fork实现的。传统的fork函数直接把父进程的所有资源复制给子进程。而Linux的::fork()使用写时拷贝页实现,也就是说,父进程和子进程共享同一个资源拷贝,只有当数据发生改变时,数据才会发生复制。通常的情况,子进程创建后会立即调用exec(),这样就避免复制父进程的全部资源。
(2)进程的撤销:进程通过调用exit()退出执行,这个函数会终结进程并释放所有的资源。父进程可以通过wait4()查询子进程是否终结。进程退出执行后处于僵死状态,直到它的父进程调用wait()或者waitpid()为止。父进程退出时,内核会指定线程组的其他进程或者init进程作为其子进程的新父进程。当进程接收到一个不能处理或忽视的信号时,或当在内核态产生一个不可恢复的CPU异常而内核此时正代表该进程在运行,内核可以强迫进程终止。
(3)在Linux系统中,shell命令ps -lA可以查看当前系统的进程,例如:
1.3进程的特性
动态性、独立性、并发性是进程的三大特性。
(1) 动态性
在程序运行的过程中,它的状态是在不断变化的。例如一个程序在运行过程中,它是一条指令接着一条指令执行,而每执行一条指令,CPU中那些通用寄存器的值也会发生变化,程序计数器(Program Counter)的值也在变化,每次都指向下一条即将执行的指令。另外堆和栈的内容也在不断变化,数据在不断进栈出栈,堆空间在不断分配和释放。总之变化无时无刻不在进行。
(2) 独立性
一个进程是一个独立的实体,是计算机系统资源的使用单位。每个进程都有"自己"的寄存器和内部状态,在它运行的时候独立于其他的进程。当然这个"自己"是带引号的,也就是说:在物理上,CPU中只存在一套寄存器,如PC寄存器只有一个,但是没有进程都有属于自己的逻辑上的PC。物理上的寄存器是真正的硬件寄存器。
(3) 并发性
对于单CPU的情况,从宏观上来看,每个进程是同时在系统中运行的,而实际上从微观上来看,在某一特定时刻,只有一个程序运行,换言之各个进程之间实际上是一个接一个顺序运行的。因为CPU是有一个,那么某一个时刻只能有一个进程去使用它。
二、进程的组织
在Linux中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(Process Control Block,简称PCB),在Linux中叫做任务结构体(task struct)(见下图),在linux/sched.h中定义。这之中包含了很多重要的信息,供系统调度和进程本身执行使用。所有进程的PCB都存放在内核空间中。PCB中最重要的信息就是进程PID,内核通过这个PID来唯一标识一个进程。PID可以循环使用,最大值是32768。init进程的pid为1,其他进程都是init进程的后代。除了进程控制块(PCB)以外,每个进程都有独立的内核堆栈(8k),一个进程描述符结构,这些数据都作为进程的控制信息储存在内核空间中;而进程的用户空间主要存储代码和数据。
三、进程的状态
3.1 进程的状态
(1)运行状态(TASK_RUNNING):指正在被CPU运行或者就绪的状态。这样的进程被成为runnning进程。运行态的进程可以分为3种情况:内核运行态、用户运行态、就绪态。
(2)可中断睡眠状态(TASK_INTERRUPTIBLE):处于等待状态中的进程,一旦被该进程等待的资源被释放,那么该进程就会进入运行状态。
(3)不可中断睡眠状态(TASK_UNINTERRUPTIBLE):该状态的进程只能用wake_up()函数唤醒。
(4)暂停状态(TASK_STOPPED):当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。
(5)僵死状态(TASK_ZOMBIE):当进程已经终止运行,但是父进程还没有询问其状态的情况。
只有当进程从“内核运行态”转移到“睡眠状态”时,内核才会进行进程切换操作。在内核态下运行的进程不能被其它进程抢占,而且一个进程不能改变另一个进程的状态。为了避免进程切换时造成内核数据错误,内核在执行临界区代码时会禁止一切中断。
态,它是进程正在等待某个事件或某个资源时所处的状态。 等待态进一步分为可中断的等待态和不可中断的等待态。处于可中断等待态的进程可以由信号(signal)解除其等待态。处于不可中断等待态的进程,一般是直接或间接等待硬件条件。 它只能用特定的方式来解除,例如使用唤醒函数wake_up()等。
3.2进程的状态转化图
四、进程的调度
4.1调度的基本原理
调度的实质就是资源的分配。系统通过不同的调度算法(Scheduling Algorithm)来实现这种资源的分配。通常来说,选择什么样的调度算法取决于的资源分配的策略(Scheduling Policy)。
4.2 Linux进程调度的目标
1.高效性:高效意味着在相同的时间下要完成更多的任务。调度程序会被频繁的执行,所以调度程序要尽可能的高效;
2.加强交互性能:在系统相当的负载下,也要保证系统的响应时间;
3.保证公平和避免饥渴;
4.SMP调度:调度程序必须支持多处理系统;
5.软实时调度:系统必须有效的调用实时进程,但不保证一定满足其要求;
4.3 进程饥饿
进程饥饿,即为Starvation,指当等待时间给进程推进和响应带来明显影响称为进程饥饿。当饥饿到一定程度的进程在等待到即使完成也无实际意义的时候称为饥饿死亡。
产生饥饿的主要原因:
在一个动态系统中,对于每类系统资源,操作系统需要确定一个分配策略,当多个进程同时申请某类资源时,由分配策略确定资源分配给进程的次序。
有时资源分配策略可能是不公平的,即不能保证等待时间上界的存在。在这种情况下,即使系统没有发生死锁,某些进程也可能会长时间等待.当等待时间给进程推进和响应带来明显影响时,称发生了进程饥饿,当饥饿到一定程度的进程所赋予的任务即使完成也不再具有实际意义时称该进程被饿死。
举个例子,当有多个进程需要打印文件时,如果系统分配打印机的策略是最短文件优先,那么长文件的打印任务将由于短文件的源源不断到来而被无限期推迟,导致最终的饥饿甚至饿死。
4.4 调度算法及其基本原理
1.时间片轮转调度算法
时间片(Time Slice)就是分配给进程运行的一段时间。在通常的轮转法中,系统将所有的可运行(即就绪)进程按先来先服务的原则,排成一个队列,每次调度时把CPU分配给队首进程,并令其执行一个时间片。当执行的时间片用完时,系统发出信号,通知调度程序,调度程序便据此信号来停止该进程的执行,并将它送到运行队列的末尾,等待下一次执行;然后,把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。这样就可以保证运行队列中的所有进程,在一个给定的时间内,均能获得一时间片的处理机执行时间。
2.优先权调度算法
为了照顾到紧迫型进程在进入系统后便能获得优先处理,引入了最高优先权调度算法。当将该算法用于进程调度时,系统将把处理机分配给运行队列中优先权最高的进程,这时,又可进一步把该算法分成两种方式:
(1) 非抢占式优先权算法(又称不可剥夺调度:Nonpreemptive Scheduling)
在这种方式下,系统一旦将处理机(CPU)分配给运行队列中优先权最高的进程后,该进程便一直执行下去,直至完成;或因发生某事件使该进程放弃处理机时,系统方可将处理机分配给另一个优先权高的进程。这种调度算法主要用于批处理系统中,也可用于某些对实时性要求不严的实时系统中。
(2) 抢占式优先权调度算法(又称可剥夺调度:Preemptive Scheduling)
该算法的本质就是系统中当前运行的进程永远是可运行进程中优先权最高的那个。在采用这种调度算法时,每当出现一新的可运行进程,就将它和当前运行进程进行优先权比较,如果高于当前进程,将触发进程调度。这种方式的优先权调度算法,能更好的满足紧迫进程的要求,故而常用于要求比较严格的实时系统中,以及对性能要求较高的批处理和分时系统中。Linux也采用这种调度算法。
3.多级反馈队列调度
这是时下最时髦的一种调度算法。其本质是:综合了时间片轮转调度和抢占式优先权调度的优点,即:优先权高的进程先运行给定的时间片,相同优先权的进程轮流运行给定的时间片。
4.实时调度
最后我们来看一下实时系统中的调度。什么叫实时系统,就是系统对外部事件有求必应、尽快响应。在实时系统中,广泛采用抢占调度方式,特别是对于那些要求严格的实时系统。因为这种调度方式既具有较大的灵活性,又能获得很小的调度延迟;但是这种调度方式也比较复杂。
4.5 Linux进程调度时机
Linux的调度程序是一个叫Schedule()的函数,这个函数被调用的频率很高,由它来决定是否要进行进程的切换,如果要切换的话,切换到哪个进程等等。我们先来看在什么情况下要执行调度程序,我们把这种情况叫做调度时机。
Linux调度时机主要有:
1、进程状态转换的时刻:进程终止、进程睡眠;
2、当前进程的时间片用完时(current->counter=0);
3、设备驱动程序主动调用schedule;
4、进程从中断、异常及系统调用返回到用户态时;
时机1,进程要调用sleep()或exit()等函数进行状态转换,这些函数会主动调用调度程序进行进程调度;
时机2,由于进程的时间片是由时钟中断来更新的,因此,这种情况和时机4是一样的。
时机3,当设备驱动程序执行长而重复的任务时,直接调用调度程序。在每次反复循环中,驱动程序都检查need_resched的值,如果必要,则调用调度程序schedule()主动放弃CPU。
时机4,如前所述,不管是从中断、异常还是系统调用返回,最终都调用ret_from_sys_call(),由这个函数进行调度标志的检测,如果必要,则调用调度程序。那么,为什么从系统调用返回时要调用调度程序呢?这当然是从效率考虑。从系统调用返回意味着要离开内核态而返回到用户态,而状态的转换要花费一定的时间,因此,在返回到用户态前,系统把在内核态该处理的事全部做完。
每个时钟中断(timer interrupt)发生时,由三个函数协同工作,共同完成进程的选择和切换,它们是:schedule()、do_timer()及ret_form_sys_call()。
schedule():进程调度函数,由它来完成进程的选择(调度);
do_timer():暂且称之为时钟函数,该函数在时钟中断服务程序中被调用,被调用的频率就是时钟中断的频率即每秒钟100次(简称100赫兹或100Hz);
ret_from_sys_call():系统调用返回函数。当一个系统调用或中断完成时,该函数被调用,用于处理一些收尾工作,例如信号处理、核心任务等等。
4.6 进程调度的实现
调度程序内核函数:
asmlinkage void schedule(void) { struct task_struct *prev, *next, *p; /* prev表示调度之前的进程, next表示调度之后的进程 */ struct list_head *tmp; int this_cpu, c; if (!current->active_mm) BUG();/*如果当前进程的的active_mm为空,出错*/ need_resched_back: prev = current; /*让prev成为当前进程 */ this_cpu = prev->processor; if (in_interrupt()) {/*如果schedule是在中断服务程序内部执行, 就说明发生了错误*/ printk("Scheduling in interrupt/n"); BUG(); } release_kernel_lock(prev, this_cpu); /*释放全局内核锁,并开this_cpu的中断*/ spin_lock_irq(&runqueue_lock); /*锁住运行队列,并且同时关中断*/ if (prev->policy == SCHED_RR) /*将一个时间片用完的SCHED_RR实时 goto move_rr_last; 进程放到队列的末尾 */ move_rr_back: switch (prev->state) { /*根据prev的状态做相应的处理*/ case TASK_INTERRUPTIBLE: /*此状态表明该进程可以被信号中断*/ if (signal_pending(prev)) { /*如果该进程有未处理的信号,则让其变为可运行状态*/ prev->state = TASK_RUNNING; break; } default: /*如果为不可中断的等待状态或僵死状态*/ del_from_runqueue(prev); /*从运行队列中删除*/ case TASK_RUNNING:;/*如果为可运行状态,继续处理*/ } prev->need_resched = 0; /*下面是调度程序的正文 */ repeat_schedule: /*真正开始选择值得运行的进程*/ next = idle_task(this_cpu); /*缺省选择空闲进程*/ c = -1000; if (prev->state == TASK_RUNNING) goto still_running; still_running_back: list_for_each(tmp, &runqueue_head) { /*遍历运行队列*/ p = list_entry(tmp, struct task_struct, run_list); if (can_schedule(p, this_cpu)) { /*单CPU中,该函数总返回1*/ int weight = goodness(p, this_cpu, prev->active_mm); if (weight > c) c = weight, next = p; } } /* 如果c为0,说明运行队列中所有进程的权值都为0,也就是分配给各个进程的 时间片都已用完,需重新计算各个进程的时间片 */ if (!c) { struct task_struct *p; spin_unlock_irq(&runqueue_lock);/*锁住运行队列*/ read_lock(&tasklist_lock); /* 锁住进程的双向链表*/ for_each_task(p) /* 对系统中的每个进程*/ p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice); read_unlock(&tasklist_lock); spin_lock_irq(&runqueue_lock); goto repeat_schedule; } spin_unlock_irq(&runqueue_lock);/*对运行队列解锁,并开中断*/ if (prev == next) { /*如果选中的进程就是原来的进程*/ prev->policy &= ~SCHED_YIELD; goto same_process; } /* 下面开始进行进程切换*/ kstat.context_swtch++; /*统计上下文切换的次数*/ { struct mm_struct *mm = next->mm; struct mm_struct *oldmm = prev->active_mm; if (!mm) { /*如果是内核线程,则借用prev的地址空间*/ if (next->active_mm) BUG(); next->active_mm = oldmm; } else { /*如果是一般进程,则切换到next的用户空间*/ if (next->active_mm != mm) BUG(); switch_mm(oldmm, mm, next, this_cpu); } if (!prev->mm) { /*如果切换出去的是内核线程*/ prev->active_mm = NULL;/*归还它所借用的地址空间*/ mmdrop(oldmm); /*mm_struct中的共享计数减1*/ } } switch_to(prev, next, prev); /*进程的真正切换,即堆栈的切换*/ __schedule_tail(prev); /*置prev->policy的SCHED_YIELD为0 */ same_process: reacquire_kernel_lock(current);/*针对SMP*/ if (current->need_resched) /*如果调度标志被置位*/ goto need_resched_back; /*重新开始调度*/ return; }
五、对Linux系统进程模型的看法
Linux是目前最流行的几个操作系统之一,通过对Linux进程模型的初步了解,为了管理进程,内核必须对每个进程所做的事情进行清楚的描述。因此,内核不是进程,而是进程的管理者。而调度程序利用task_struct之中的信息决定系统中哪个进程最应该运行,并结合进程的状态信息保证系统运转的公平和高效。方便了进程的管理,提高了cpu的效率。
六、参考资料
https://blog.csdn.net/sailor_8318/article/details/2452983
http://www.jb51.net/LINUXjishu/66846.html
https://www.ibm.com/developerworks/cn/linux/l-completely-fair-scheduler/index.html?ca=drs-cn-0125
https://blog.csdn.net/u013291303/article/details/68954599
https://www.cnblogs.com/biyeymyhjob/archive/2012/08/01/2617884.html
以上是关于第一次作业:基于Linux内核源码进程模型分析的主要内容,如果未能解决你的问题,请参考以下文章