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