我的操作系统复习——进程(上)

  上一篇博文复习了操作系统总的概述——我的操作系统复习——操作系统概述 ,包括对操作系统的定义、发展历程以及操作系统结构。接下来我们就开始详细复习计算机知识,包括进程、处理器、存储器等等。本篇首先对进程这个及其重要的概念进行复习,这是进程系列的上篇。

 

一、什么是并发

  并发是什么?很简单,前面介绍的多道批处理系统就是典型的并发执行。这里再次过一遍高性能的多道批处理系统,其本质在于保持对系统资源的占用,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中获取的信息,来恢复进程阻塞前的现场,继续执行。

(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;
};
View Code

 (4)PCB的组织形式

  系统中拥有众多PCB,对应着众多进程,那么这些PCB怎么组织的呢?一般有两种组织方式:链接方式和索引方式。这两种方式的共同点在于,正在执行的PCB,都有一个执行指针指向它。不同在于,链接方式的就绪队列、阻塞队列等,通过指针链接的方式组织。进程切换时,直接取就绪队列指针即可,因为它指向的就是当前优先级最高的就绪的PCB,随后就绪队列指针指向其指向的下一个PCB。索引方式的就绪队列、阻塞队列等,通过一个表的形式来组织,就绪队列指针指向这个表的第一条数据。这个表本质是一个指针数组,第一个指针指向的当然是优先级最高的就绪进程。

 

五、进程控制

  进程控制是什麽?本质就是切换进程状态的控制。 一般由操作系统中的原语实现。原语即具有“原子操作”这种属性的若干指令集合,说白了,就是这些指令集合,要么全部执行,要么全部不执行。不同操作系统的原语也是有区别的。

 (1)、进程创建

  进程可能是由系统内核收到请求而创建,也可能由进程本身创建,由进程本身创建的进程一般是子进程,它继承父进程拥有的全部资源。创建进程由进程创建原语实现,通常由下面几个步骤:

 

 技术分享

 

 

 

 

 

 

 

 (2)、进程终止

   进程的终止是由操作系统执行的。当一个进程因各种原因结束时,会通知操作系统。操作系统会调用进程终止原语来终止对应进程:

 技术分享

 

 (3)进程阻塞

  进程的阻塞是由进程自身主动执行的。但进程发现自身无法继续执行时,就主动调用进程阻塞原语,把自己阻塞:

 技术分享

 (4)进程唤醒

  进程的唤醒通常由其他线程执行。但其他线程由于某些事件希望执行线程执行时,会调用进程唤醒原语将指定进程唤醒:

  技术分享

   值得注意的是,进程唤醒和进程阻塞是一对作用刚好相反的原语。阻塞的进程必须由进程唤醒操作才能继续执行。

(5)进程挂起和激活

  进程挂起由自身或其他进程执行。进程激活由其他进程执行。过程很简单,就不画图了:

  1)进程挂起:若进程为活动就绪,就将其改为静止就绪;若进程为活动阻塞,就将其改为静止阻塞;若进程正在执行,则让调度程序重新调度。

  (PS:由于没有挂起队列,所以需要把进程的PCB复制到指定的内存区域)

  2)进程激活:若进程为静止就绪,就将其改为活动就绪;若进程为静止阻塞,就将其改为活动阻塞;

 

 

 参考:《计算机操作系统(汤子瀛)》

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。