我的操作系统复习——进程(上)
上一篇博文复习了操作系统总的概述——我的操作系统复习——操作系统概述 ,包括对操作系统的定义、发展历程以及操作系统结构。接下来我们就开始详细复习计算机知识,包括进程、处理器、存储器等等。本篇首先对进程这个及其重要的概念进行复习,这是进程系列的上篇。
一、什么是并发
并发是什么?很简单,前面介绍的多道批处理系统就是典型的并发执行。这里再次过一遍高性能的多道批处理系统,其本质在于保持对系统资源的占用,CPU运行一个任务,若这个任务中断,如需要IO请求之类的,那么CPU直接去运行其他任务,原任务的IO请求由IO设备自己处理。有一个著名的图——表示并发:
如图,假设计算机有输入、计算、输出这三个部件,一组任务顺序执行,并发就是如图流水线一样的各部件配合。某一时刻,只有一个程序在占用CPU(计算设备)。那么什么是并行呢?并行是建立在多核的基础上的,即多个CPU同时运行,那么有几个CPU,同一时刻就有几个程序同时运行。所以电脑只有一个CPU的苦逼程序员只能并发了。
现代系统的实际并发比上图要复杂的多,现代计算机系统的并发是以时间片为基础的,即在很短的时间内,每个进程都分别运行一次。这样,宏观上,每个进程都在不断的运行,而实际上每个进程的运行都是间断运行的。那么问题来了,什么是进程呢?
二、什么是进程
进程的目的就是为了对并发执行的程序进行控制。进程实体由程序段、数据段、PCB三部分构成。我们知道计算机运行的本质就是对数据的处理的机器。
——数据段就是各种数据
——程序段就是一系列操作计算机的指令,即操作数据的方法策略
——PCB 即进程控制块(Process Control Block),控制运行程序段的时机。
书本上是这样定义进程的:“进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位”。
让我们理清楚思绪,进程是什么?进程就是进程实体的运行过程,是一个过程。就是说,进程实体不运行,那就不叫进程。一个没有被调用的进程实体,不叫进程。所以说,进程有动态的特征。上面提到,进程的目的就是为了对并发执行的程序进行控制。为了在一个时间片内,运行多个程序才引进进程。所以说,进程有并发的特征。进程实体是一个拥有独立的资源(程序段和数据段)、(因为PCB)能独立地接受调度并独立的运行的基本单位。所以说,进程有独立的特征。进程运行的过程中,由于涉及到的资源众多、运行环境不一定,也受到其他进程的影响,所以,进程的运行情况是不可具体预知的。所以书本定义,进程按各自独立的、不可预知的速度向前推进。所以说,进程有异步的特征。如上文所说,进程是进程实体这一数据结构被调用运行。所以说,进程有结构的特征。
可以这么说,真正理解了进程的这五个特征,才算理解了进程这个概念。高手跟我们这些菜鸟的最大区别,不就在于对系统的理解吗?
三、进程的状态
进程有3种基本状态:
(1)就绪(Ready)状态
此时的进程拥有完整的进程实体,只要获得CPU,即只要被调用就能马上执行,这种状态被称为就绪状态。处于这种状态的进程都会被放进就绪队列,以便随时接受CPU调用。
(2)执行状态
此时的进程已经获得CPU,正在执行。
(3)阻塞状态
执行中的进程,因为某种原因(IO请求)无法继续执行的一种暂停状态,暂停完毕就会变成就绪状态。
除了以上的3种基本状态,有的系统额外还增加了一种状态。
(4)挂起状态
为什么需要挂起状态?因为有时候希望某些正在执行的线程暂停下来,持续一段时间后,让它回到之前的状态。
挂起状态是一种静止的状态,相当于把某个进程从执行的流水线上拿出来,等到需要的时候再把它放进去继续执行。我们来看前三种基本状态,就绪 ->执行 -> 阻塞,阻塞完毕又回到就绪。由于线程的异步性,阻塞是会在不确定的有限时间内结束的。就是说,三种基本状态是动态的,通常不存在一个线程一直处于某种状态。挂起状态相对于它们来说,是静止的,因为它是被控制的,是对以不可预知的速度前进的线程的一种干扰。
此外,为了管理的需要,通常还有两种比较常见的状态。
(5)创建状态
我们知道进程实体包括程序段、数据段和PCB。创建状态指的是PCB已经被创建,因为某些原因(程序段或数据段未放入内存等),进程还未被放入就绪队列的这种状态。
(6)终止状态
线程的终止也是有个过程的。终止状态指的是线程除了PCB以外的系统资源都被回收后的状态。此时线程真正终止。
四、进程的核心PCB
(1)PCB是什麽?
作为进程实体的一部分,PCB是用来控制进程运行的一种数据结构。它包含了进程的状态、优先级、运行的状态、处理机状态、程序数据的内存地址等各种信息,一旦被操作系统调用,操作系统就从PCB中获取的信息,来恢复进程阻塞前的现场,继续执行。PCB一般都保存在CPU的寄存器中。
(2)PCB包含的信息
1)进程标识符。用来标识唯一的一个进程。包括方便系统调用的内部标识符和方便用户调用的外部标识符。进程标识符通常还包括父、子进程,以及所属用户等信息。
2)处理机状态信息。
处理机状态信息指的是处理机调用线程时的环境信息。处理机处理调用进程时,运行过程中的许多信息都放在处理机的寄存器中。进程阻塞或挂起时,寄存器中的运行信息会保存到PCB中,以便进程下次被调用时恢复之前的运行现场。
3)进程调度信息。
进程调度信息指的是本进程调度所需的必要信息。包括,本进程的状态(6种之一)、进程优先级、进程等待时间、进程执行时间(可能决定优先级)、阻塞原因、父子进程关系等。
4)进程控制信息。
进程控制信息指的是进程的资源信息和进程切换时的所需信息,包括进程的程序和数据的内存地址、进程同步和通信的机制、进程资源的清单、指向下一个进程PCB的指针(若PCB的组织方式是链接方式)等。
(3)PCB的具体信息(结构)
这里我们来看一下Unix中,PCB的具体结构,以便对PCB有一个清晰的认识。这玩意其实就这么一回事:
(下面代码摘自http://blog.sina.com.cn/s/blog_65403f9b0100gs3a.html)
struct task_struct { volatile long state; //说明了该进程是否可以执行,还是可中断等信息 unsigned long flags; //Flage 是进程号,在调用fork()时给出 int sigpending; //进程上是否有待处理的信号 mm_segment_t addr_limit; //进程地址空间,区分内核进程与普通进程在内存存放的位置不同 //0-0xBFFFFFFF for user-thead //0-0xFFFFFFFF for kernel-thread //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度 volatile long need_resched; int lock_depth; //锁深度 long nice; //进程的基本时间片 //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR, 分时进程:SCHED_OTHER unsigned long policy; struct mm_struct *mm; //进程内存管理信息 int processor; //若进程不在任何CPU上运行, cpus_runnable 的值是0,否则是1 这个值在运行队列被锁时更新 unsigned long cpus_runnable, cpus_allowed; struct list_head run_list; //指向运行队列的指针 unsigned long sleep_time; //进程的睡眠时间 //用于将系统中所有的进程连成一个双向循环链表, 其根是init_task struct task_struct *next_task, *prev_task; struct mm_struct *active_mm; struct list_head local_pages; //指向本地页面 unsigned int allocation_order, nr_local_pages; struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式 int exit_code, exit_signal; int pdeath_signal; //父进程终止是向子进程发送的信号 unsigned long personality; //Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序 int did_exec:1; pid_t pid; //进程标识符,用来代表一个进程 pid_t pgrp; //进程组标识,表示进程所属的进程组 pid_t tty_old_pgrp; //进程控制终端所在的组标识 pid_t session; //进程的会话标识 pid_t tgid; int leader; //表示进程是否为会话主管 struct task_struct *p_opptr,*p_pptr,*p_cptr,*p_ysptr,*p_osptr; struct list_head thread_group; //线程链表 struct task_struct *pidhash_next; //用于将进程链入HASH表 struct task_struct **pidhash_pprev; wait_queue_head_t wait_chldexit; //供wait4()使用 struct completion *vfork_done; //供vfork() 使用 unsigned long rt_priority; //实时优先级,用它计算实时进程调度时的weight值 //it_real_value,it_real_incr用于REAL定时器,单位为jiffies, 系统根据it_real_value //设置定时器的第一个终止时间. 在定时器到期时,向进程发送SIGALRM信号,同时根据 //it_real_incr重置终止时间,it_prof_value,it_prof_incr用于Profile定时器,单位为jiffies。 //当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送 //信号SIGPROF,并根据it_prof_incr重置时间. //it_virt_value,it_virt_value用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种 //状态下,每个tick都使it_virt_value值减一当减到0时,向进程发送信号SIGVTALRM,根据 //it_virt_incr重置初值。 unsigned long it_real_value, it_prof_value, it_virt_value; unsigned long it_real_incr, it_prof_incr, it_virt_value; struct timer_list real_timer; //指向实时定时器的指针 struct tms times; //记录进程消耗的时间 unsigned long start_time; //进程创建的时间 //记录进程在每个CPU上所消耗的用户态时间和核心态时间 long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; //内存缺页和交换信息: //min_flt, maj_flt累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换 //设备读入的页面数); nswap记录进程累计换出的页面数,即写到交换设备上的页面数。 //cmin_flt, cmaj_flt, cnswap记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。 //在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中 unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap; int swappable:1; //表示进程的虚拟地址空间是否允许换出 //进程认证信息 //uid,gid为运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid //euid,egid为有效uid,gid //fsuid,fsgid为文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件 //系统的访问权限时使用他们。 //suid,sgid为备份uid,gid uid_t uid,euid,suid,fsuid; gid_t gid,egid,sgid,fsgid; int ngroups; //记录进程在多少个用户组中 gid_t groups[NGROUPS]; //记录进程所在的组 //进程的权能,分别是有效位集合,继承位集合,允许位集合 kernel_cap_t cap_effective, cap_inheritable, cap_permitted; int keep_capabilities:1; struct user_struct *user; struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息 unsigned short used_math; //是否使用FPU char comm[16]; //进程正在运行的可执行文件名 //文件系统信息 int link_count, total_link_count; //NULL if no tty 进程所在的控制终端,如果不需要控制终端,则该指针为空 struct tty_struct *tty; unsigned int locks; //进程间通信信息 struct sem_undo *semundo; //进程在信号灯上的所有undo操作 struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作 //进程的CPU状态,切换时,要保存到停止进程的task_struct中 struct thread_struct thread; //文件系统信息 struct fs_struct *fs; //打开文件信息 struct files_struct *files; //信号处理函数 spinlock_t sigmask_lock; struct signal_struct *sig; //信号处理函数 sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位 struct sigpending pending; //进程上是否有待处理的信号 unsigned long sas_ss_sp; size_t sas_ss_size; int (*notifier)(void *priv); void *notifier_data; sigset_t *notifier_mask; u32 parent_exec_id; u32 self_exec_id; spinlock_t alloc_lock; void *journal_info; };
(4)PCB的组织形式
系统中拥有众多PCB,对应着众多进程,那么这些PCB怎么组织的呢?一般有两种组织方式:链接方式和索引方式。这两种方式的共同点在于,正在执行的PCB,都有一个执行指针指向它。不同在于,链接方式的就绪队列、阻塞队列等,通过指针链接的方式组织。进程切换时,直接取就绪队列指针即可,因为它指向的就是当前优先级最高的就绪的PCB,随后就绪队列指针指向其指向的下一个PCB。索引方式的就绪队列、阻塞队列等,通过一个表的形式来组织,就绪队列指针指向这个表的第一条数据。这个表本质是一个指针数组,第一个指针指向的当然是优先级最高的就绪进程。
五、进程控制
进程控制是什麽?本质就是切换进程状态的控制。 一般由操作系统中的原语实现。原语即具有“原子操作”这种属性的若干指令集合,说白了,就是这些指令集合,要么全部执行,要么全部不执行。不同操作系统的原语也是有区别的。
(1)、进程创建
进程可能是由系统内核收到请求而创建,也可能由进程本身创建,由进程本身创建的进程一般是子进程,它继承父进程拥有的全部资源。创建进程由进程创建原语实现,通常由下面几个步骤:
(2)、进程终止
进程的终止是由操作系统执行的。当一个进程因各种原因结束时,会通知操作系统。操作系统会调用进程终止原语来终止对应进程:
(3)进程阻塞
进程的阻塞是由进程自身主动执行的。但进程发现自身无法继续执行时,就主动调用进程阻塞原语,把自己阻塞:
(4)进程唤醒
进程的唤醒通常由其他线程执行。但其他线程由于某些事件希望执行线程执行时,会调用进程唤醒原语将指定进程唤醒:
值得注意的是,进程唤醒和进程阻塞是一对作用刚好相反的原语。阻塞的进程必须由进程唤醒操作才能继续执行。
(5)进程挂起和激活
进程挂起由自身或其他进程执行。进程激活由其他进程执行。过程很简单,就不画图了:
1)进程挂起:若进程为活动就绪,就将其改为静止就绪;若进程为活动阻塞,就将其改为静止阻塞;若进程正在执行,则让调度程序重新调度。
(PS:由于没有挂起队列,所以需要把进程的PCB复制到指定的内存区域)
2)进程激活:若进程为静止就绪,就将其改为活动就绪;若进程为静止阻塞,就将其改为活动阻塞;
参考:《计算机操作系统(汤子瀛)》
* ————————如果觉得本博文还行,别忘了推荐一下哦,谢谢!
* 作者:钱书康
* 欢迎转载,请保留此段声明。
* 出处:http://www.cnblogs.com/zrtqsk/
*/