第2章进程管理

Download Report

Transcript 第2章进程管理

第2章 Linux内核
——进程管理
本章主要介绍:
 进程概念
 进程的组成
 进程的状态和调度
 进程间关系
 中断处理与定时器
 系统调用
 进程间通信
1 进程概念
20世纪60年代,进程(process)一词首
先在麻省理工学院的MULTICS和IBM的
CTSS/360系统中被引入。
对进程下个准确定义不容易,但有必要
强调一下进程具有的两个重要特性。
1. 独立性
进程是系统中独立存在的实体,它可以拥有自
己独立的资源,比如文件和设备描述符等。
在没有经过进程本身允许的情况下,其他进程
不能访问到这些资源。这一点上和线程有很大的不
同。
线程是共享资源的程序实体,创建一个线程所
花费的系统开销要比创建一个进程小得多。
2. 动态性
进程与程序的区别在于,程序只是一个静态的
指令集合,而进程是一个正在系统中活动的指令集
合。
在进程中加入了时间的概念。进程具有自己的
生命周期和各种不同的状态,这些概念在程序中都
是不具备的。
由于以上两个性质,又可以衍生出进程
的第三个重要特性,即并发性。
若干个进程可以在单处理机状态上并发
执行。注意并发性(concurrency)和多处理机
并行(parallel)是两个不同的概念。
并行指在同一时刻内,有多条指令在多
个处理机上同时执行;
并发指在同一时刻内可能只有一条指令
执行,但多个进程的指令被快速轮换执行,
使得在宏观上具有多个进程同时执行的效果。
2
进程的组成
作为申请系统资源的基本单位,进程必
须有一个对应的物理内存空间。
而对这样的一块空间,首先要用数据结
构进行描述,才能进一步对之进行管理。
在Linux中,进程以进程号PID(process
ID)作为标识。
任何对进程进行的操作都要给予其相应
的PID号。
每个进程都属于一个用户,进程要配备
其所属的用户编号UID。
此外,每个进程都属于多个用户组,所
以进程还要配备其归属的用户组编号GID的
数组
UID和GID都分4种,
UID包括uid,euid,suid和fsuid,
GID包括gid,egid,sgid和fsgid。
一般来说
uid=euid=fsuid,
gid=egid=fsgid。
进程标识:
uid和gid
euid和egid又称为有效的uid和gid。
suid和sgid
fsuid和fsgid
进程运行的环境称为进程上下文(context)。
Linux中进程的上下文由进程控制块
PCB(process control block)、正文段(text
segment)、数据段(data segment)以及用户
堆栈(stack)组成。
—个称做进程表(process table)的链表
结构将系统中所有的PCB块联系起来,如图
2-1所示。
系统每次访问一个进程时,内核根据PID
在进程表中查找相应的进程PCB块(具体查找
过程通过一个PID的hash表实现),再通过
PCB块找到其对应的代码段与数据段,并进
行操作。
3
进程的状态和调度
Linux系统信号
信号主要用于通知进程异步事件的发生。
在Linux中可以识别29种不同的信号,这些信号
中的大部分都有了预先定义好的意义,
进程可以显式的用kill或killpg系统调用来
向另一个进程发信号。
进程可以通过提供信号处理函数来取代
对于任意信号的缺省反应,这种缺省反应一
般都是终止进程。
信号发生时,内核中断当前的进程,进
程执行处理函数来响应信号,结束后恢复正
常的进程处理。
信号有自己的名称和特定的编号,见表
3-1所示。
进程状态
进程是一个动态的实体,故而它是有生命的。
从创建到消亡,是一个进程的整个生命周期。
在这个周期中,进程可能会经历各种不同的
状态。
一般来说,所有进程都要经历以下3种状态。
◆ 就绪(ready)态:
指进程已经获得所有所需的其他资源,
并正在申请处理机资源,准备开始运行。
这种情况下,称进程处于就绪态。
◆ 阻塞(blocked)态:
指进程因为需要等待所需资源而放弃处
理机,或者进程本不拥有处理机,且其他资
源也没有满足,从而即使得到处理机资源也
不能开始运行。
这种情况下,称进程处于阻塞态。阻塞
状态又称休眠状态或者等待状态。
◆ 运行态:
进程得到了处理机,并不需要等待其他
任何资源,正在执行的状态,称之为运行态。
只有在运行态时,进程才可以使用所申
请到的资源。
进程结构

task_struct数据结构
 进程标志符PID
 进程所占的内存区域
 相关文件的文件描述符
 安全信息
 进程环境
 信号处理
 资源安排
 同步处理
 进程状态
include/Linux/sched.h
task_struct (include/linux/sch.h)
进程的状态
task_struct 中的state 表示进程当前的状态
Linux中的进程主要有5个状态:

(include/linux/sched.h)
#define TASK_RUNNING
#define TASK_INTERRUPTIBLE
#define TASK_UNINTERRUPTIBLE
#define TASK_STOPPED
#define TASK_ZOMBIE
0
1 //等待资源
2
4 //等待信号
8
◆ RUNNING:
正在运行,或者在就绪队列中等待运行
的进程。
也就是上面提到的运行态和就绪态进程
的综合。
一个进程处于RUNNING状态,并不代表
它一定在被执行。
由于在多任务系统中,各个就绪进程需
要并发执行,所以在某个特定时刻,这些处
于运行状态的进程之中,只有一个能够得到
处理机,而其他进程必须在一个就绪队列中
等待。
即使是在多处理机的系统中,Linux也只
能同时让一个处理机执行任务。
◆ UNINTERRUPTABLE:
不可中断阻塞状态。
处于这种状态的进程正在等待队列中,
当资源有效时,可由操作系统进行唤醒,否
则,将一直处于等待状态。
◆ INTERRUPTABLE:
可中断阻塞状态。
与不可中断阻塞状态一样,处于这种状
态的进程也在等待队列中,当资源有效时,
可以由操作系统进行唤醒。
与不可中断阻塞状态有所不同的是,处
于此状态中的进程亦可被其他进程的信号和
定时中断唤醒。
◆ STOPPED:
挂起状态。
进程被暂停,需要通过其他进程的信号
才能被唤醒。
导致这种状态的原因有两种。
其一是受到了相关信号(SIGSTOP、
SIGSTP、SIGTTIN 或SIGTTOU)的反应;
其二是受到父进程ptrace调用的控制,
而暂时将处理机交给控制进程。
◆ ZOMBIE:
僵尸状态。
表示进程结束但尚未消亡的一种状态。
此时进程已经结束运行并释放大部分资
源,但尚未释放进程控制块。
进程调度
调度程序(scheduler)用来实现进程状态
之间的转换。
在Linux中,调度程序由系统调用
schedule()来完成。
schedule()是一个怪异的函数,它与一般
C语言函数不同,因为它的调用和返回不在
同一个进程中。
用户进程由fork()系统调用实现。用户进
程由do_fork()函数创建,它也是fork系统调
用的执行者。
fork()创建一个新的进程,继承父进程的
现有资源,初始化进程时钟、信号、时间等
数据。
完成子进程初始化后,父进程将它挂到
就绪队列,返回子进程的PID。
进程创建时的状态为不可中断阻塞,在
fork()结束前被父进程唤醒后,变为
RUNNING。
处于RUNNING状态的进程被移到就绪队
列中,在适当时候由schedule()按处理机调
度算法选中,获得处理机。
获得处理机而正在运行的进程若申请不到
某个资源,则调用sleep()进行休眠,其PCB
挂到相应的等待队列,状态变为不可中断阻塞
或者可中断阻塞。
sleep()将调用schedule()函数把休眠进
程释放的处理机分配给就绪队列中的某个进程.
状态为可中断阻塞的休眠进程当它申请
的资源有效时被唤醒,也可以由信号或定时
中断唤醒。
而状态为不可中断阻塞的休眠进程只有
当它申请的资源有效时被唤醒,不能被信号
和定时中断唤醒。
唤醒后,进程状态改为RUNNING,并进
入就绪队列。
进程执行系统调用exit()或收到外部的杀
死进程信号SIG_KILL时,进程状态变为
ZOMBIE,释放所申请资源。
同时启动schedule()把处理机分配给就
绪队列中其他进程。
若进程通过系统调用设置了跟踪标志位,
则在系统调用返回前,进入跟踪状态,进程
状态变为STOPPED,处理机分配给就绪队
列中其他进程。
只有通过其他进程发送SIG_KILL信号或
继续信号SIG_CONT,才能把STOPPED进
程唤醒。重新进入就绪队列。
对每一个进程,其PCB块中都可以记录
一种调度策略。
进程调度算法可采用先进先出算法(FIFO)
或轮转法(round-robin),有实时(这里的“实
时”,只是一种说法。实际上,未经改造的
Linux很难实现“实时”)和非实时两种形式。
若采用Linux的轮转法,当时间片到时
(10ms的整数倍),由时钟中断触发,引起新
一轮调度,把当前进程挂到就绪队列队尾。
在schedule()中有一个goodness()函数,
可以用来保证实时的进程可以得到优先调用。
然而这只是在调用上优先,事实上在内
核态下,实时进程并不能对普通进程进行抢
占。
所以Linux中的实时并不是真正意义上的
实时。
4
进程间关系
Linux中除了0号进程是启动时由系统创建,
其余进程都是由其他进程自行创建的。
为了表示这种创建关系,用父进程指代缔
造者,用子进程指代被创建出的新进程。
如果进程A是进程B的间接父进程,则A称
做B的祖先,B为A的后代。
既然提到了父子关系,那么这两个进程之
间自然是有着如同父子一样的继承性。
 进程的“宗族”关系
树型组织
task_struct中的
struct task_struct *p_pptr,
*p_cptr, *p_ysptr, *p_osptr;
p_pptr: parent (父进程)
p_cptr: child (指向自己最年轻、最新的子进程)
p_ysptr:指向比自己年轻的兄弟进程
p_osptr:指向比自己老的兄弟进程
在数据结构上,父进程PCB中的指针
p_cptr指向最近创建的一个子进程的PCB块,
而每个子进程PCB中的指针p_pptr都指向其
父进程的PCB块。
这一对指针构成了进程的父子关系,如
图2-3所示。

task[]数组(实际是双向链表指针)
 包含指向系统中所有task_struct结构的指针
 数组大小限制了系统中的进程数目
 将所有任务串连起来

Pid hash表
 通过pid查找进程时,利用hash快速定位双向指针

run_list
 动态的将任务链入prio_array中的某个优先级队列中
 current指针
当前运行的进程的结构用current指针表示
 init进程
系统初始化后,建立的第一个进程
第一个task_struct: INIT_TASK
系统启动时,内核被加载到内存后,由
start_kernel函数(完成内核初始化工作)从无到有地
自行创建了一个内核进程,叫做0号进程,其所运
行的代码是init_task()函数,在很多链表中起表头
的作用。只有当没有其他进程处于可运行状态时,
调度程序才选择0号进程。
该进程的作用是作为一切其他进程的父进程,
就像亚当夏娃是一切人类的祖先那样。
0号进程不能自动生成,必须手工将其设置到
进程表中去,才能启动进程管理机制。
在启动进程管理机制以后,就可以由进程自行
创建新的子进程。
创建新进程的调用是fork()。fork一词在英文
中是“分叉”的意思。
同样,在Linux中,fork()调用也起了一个“分
叉”的作用。
当进程A调用fork()生成进程B时,fork()函数
同时在A和B两个进程中返回。
其中,父进程A里的fork()返回了子进程的PID,
而子进程B里的fork()返回0。如果出现错误,fork()
返回一具负值。
然而,fork()函数究竟做了些什么呢?
我们发现,经过fork()以后,父进程和子
进程拥有相同内容的代码段、数据段和用户
堆栈,就像父进程把自己克隆了一遍。
事实上,父进程只复制了自己的PCB块,
而代码段、数据段、用户堆栈内存空间并没
有复制一份,而是与子进程共享 .
fork函数演示见
文件fork.swf
只有当子进程在运行中出现写操作时,
才会产生中断,并为子进程分配内存空间。
由于父进程的PCB和子进程的一样,所
以在PCB中所记录的父进程占有的资源,也
是与子进程共享使用的。
这里的“共享”一词就意味着“竞争”。
有时候为了避免父进程和子进程竞争相
同的资源或者出于代码串行性考虑,我们希
望父进程可以等待子进程运行结束后再继续
执行。
调用vfork()可以使在子进程创建后,随
即向父进程发送SIG_STOP信号,使父进程
进入挂起状态,直到子进程发送信号表示其
已经结束为止。
如果系统中只提供fork()调用,那么整个操作
系统的所有进程就都只能运行同一个程序了,因为
其代码段都是复制或者共享的。
Linux为了创建进程运行新的程序,采用
fork+exec方法。
execve()支持在新的进程创建后,动态装入新
的可执行文件作为自己新的代码段。
并且execve()支持多种可执行文件的格式。
5
进程间通讯
用户态进程间处于并发状态。
为了协调进程的运行,需要实现进程之
间通信的机制。
在Linux中,进程间通信有以下几种方法:
1.管道机制
该机制最适用于解决生产者――消费者
问题。
管道是一种在进程之间单向流动数据的
结构。
源进程向管道写数据,而内核会自动将
这些数据引导向目标进程。
在POSIX标准中,管道必须是单向的。
虽然通过函数调用会产生两个描述符(写
管道和读管道),但是在写之前必须关闭读管
道,反之亦然。
而在Linux中,使用一个管道的同时还可
以使用另一个管道,这就大大增强了管道使
用的简便性。
2. 先进先出(FIFO)机制
管道机制的最大缺点是不能由多个进程
共享,除非此管道为这些进程共同的祖先所
创建。
为了解决这个问题,Linux中引入了
FIFO机制(又称为named pipe,命名管道)。
FIFO为“first in,first out”的简写,指
一个在磁盘上的文件,它可以被所有进程所
共享。
但是FIFO与一般文件不同,它还使用了
内核中的缓冲区,所以在效率上要比一般共
享文件快得多。
FIFO和管道都可以使用read()和write()
调用来进行读写操作。
3. IPC机制
IPC是“interprocess communication”
的缩写形式。
它包含了一系列系统调用,允许用户态
进程通过信号量进行同步,向其他进程发消
息,并且可以与其他进程共享一块内存空间.
IPC首先是在一个叫做“Columbus
Unix”的系统中实现的,其后在现代Unix类
操作系统中广为流行。
如上文所述,IPC资源包括信号量,消息
队列和共享内存几种。
(1) 消息队列
消息队列是由内核创建并维护的一个数
据结构,它是有标识的。
任何具有足够权限的进程都可以向消息
队列中放置一个消息,同样,任何具有足够
权限的进程都可以从中读取一个消息。
这样,不同的进程通过访问相同的消息
队列便可实现进程间通信。
(2) 共享内存
共享内存区是这几种进程间通信方式中最
快的一种。
它的特点除了速度快外,而且可传递的信
息量大。
它是通过将一段内存区映射到一个进程的
地址空间来实现。
用共享内存实现过程如下:
① 服务器取得访问该共享内存区的权限。
② 服务器从输入文件读取数据到该共享内存区。
③ 服务器读入数据完毕时,通知用户进程。
④ 用户从该共享内存区读出这些数据并输出。
(3) 信号量
信号量并不是一种IPC机制,它是用于提
供不同进程间或一给定进程的不同线程间同
步的一种手段。
信号量主要包括以下几种类型:
① 二值信号量:其值为0或1。资源被锁住而不
可用时,值为0;资源可用时,值为1。
② 计数信号量:其值在0和某个限制值之间。
它的值一般是可用的资源数。
③ 计数信号量集:由一个或多个信号量构成的
一个集合,其中每一个都是计数信号量。每
个集合的信号量数都有一个限制值,一般在
25个以上。
以计数信号量为例,在有信号量机制的
程序中,一个进程为了获得共享资源,需要
执行以下操作:
 测试控制该资源的信号量
 进程将信号量值减1,表示它使用了一个资源
 进程不再使用该资源时,信号量的值就加1。
一个IPC资源将永久地驻留在内存中,除
非进程显式地释放它。
并且,IPC资源不仅能被其创建进程的后
代所共享,任何进程都可以共享它。
6
进程同步
在Linux中进程具有并发性,即系统中所
有在就绪队列中的进程要按时间片轮流运行。
由于一些资源为所有进程所共享(比如文
件系统),在并发的情况厂,就需相关中断事
件所打断。
可串行性是数据库中广泛使用的一种正
确标准。
当事务交叉执行它们的操作,产生的结
果和它们串行执行的结果一致时,这些事务
便称为可串行性的。
同样,在操作系统中,假设两个进程同
时修改一个文件,进程A和进程B都分别从偏
移量x处读出数据,加1后写回。
为了避免不可串行的情况出现,操作系
统中需要将进程的读数据和加1两个操作定义
为一个原子操作,并进行互斥保护。
总之,Linux系统下,进程同步可以采用
互斥锁、读写锁、记录上锁、信号量和条件
变量等方法。