进程(Process)
它是操作系统的CPU调度和资源分配的单位。
操作系统执行各种程序:
- 批处理系统-作业(Job)
- 分时系统-用户程序或任务(Task)
作业:被组装成一个整体运行的一组计算步骤
任务:进程或线程
作业和进程在这本书中基本可互换
进程:
- 执行中的程序
- 进程的执行必须以顺序方式进行
- 另一个说法:一个程序在一个数据集上的一次运行
例子:Windows XP
内存中的进程
进程包括:
(1)代码(text);
(2)当前活动:
- 程序计数器(PC)-指向当前要执行的指令(地址)
- 堆栈(Stack):存放函数参数、临时变量等临时数据
- 数据(Data):全局变量,处理的文件
- 堆(Heap):动态内存分配
进程和程序
- 进程是程序的一个实例,是程序的一次执行
- 一个程序可对应一个或多个进程
- 一个进程可对应一个或多个程序
- 程序是进程的代码部分
- 进程是活动实体,程序静止(被动)实体
- 进程在内存,程序在外存
进程状态
进程执行时,改变状态
- 就绪:进程等待分配处理器
- 新建:在创建进程
- 运行:指令在执行
- 等待:进程等待某些事件发生
- 终止:进程执行完毕
从进程新建开始,一旦新建成功,就会处于就绪状态,处于就绪态的进程,如果被调度程序选中,获得处理机,则转为运行态。处于运行态的进程,有三种变化可能,一是被中断程序打断,或时间片运行完毕,变为就绪态;二是运行一段时间后,等待事件的发生,如I/O操作,变为等待态;三是进程运行结束,变为终止态。
处于等待态的进程,如果等待的事件或I/O完成了,则变为就绪态。等待态不能直接转为运行态。
进程控制块(PCB)
为了管理进程,系统为每个进程设计了一个数据结构,进程控制块(PCB)。
PCB包含同进程有关的信心,包括:
- 进程状态
- 程序计数器
- CPU寄存器
- CPU调度信息
- 内存管理信息
- 记账信息
- I/O状态信息
不同系统的进程控制块的设计是不同的,
如Linux系统中的PCB是C结构:task_struct,保存了与进程有关的信息。为了便于管理,系统中所有进程的PCB用一个双向链表串起来,并有指针指示当前正在运行的进程。
Windows系统的PCB较复杂,每个Win32进程都由一个执行体进程块(executive process block)EPROCESS:PID,PCB,Access Token,Base Priority,句柄表,指向进程环境块PEB指针,默认和处理器集合等;
Windows的PCB称为内核进程对象KPROCESS;
执行体进程对象EPOCESS和KPROCESS位于内核空间;
进程环境块PEB(Process Environment Block),PEB位于用户空间。
进程上下文切换
进程的并发执行需要PCB保存和恢复现场。
这张图说明了两个进程之间的具体切换过程,即上下文切换。
图中P0进程正在CPU中运行,当有中断或系统调用到来时,需要切换到进程P1至CPU中运行。切换时,先将P0的现场信息保存至PCB0中,再将PCB1中的信息恢复至现场,然后运行进程P1。
如果系统继续推进,P0切换P1过程类似,先保存现场至PCB1,再从P0中恢复现场,这里的现场主要是指CPU寄存器的内容、程序计数器的值、进程状态等。
上下文切换的时间开销较重,在切换时系统没有做有用的工作,而上下文切换时间取决于硬件支持。如Sun公司的spark芯片上有两组寄存器,切换时这两组寄存器轮流使用,减少了切换时间。
进程操作
1. 进程创建
父进程创建子进程,如此轮流创建进程下去,构成一棵进程树。
资源共享三种模式:
- 父进程子进程共享所有的资源
- 子进程共享父进程资源的子集
- 父进程和子进程无资源共享
执行模式:
- 父进程与子进程并发执行
- 父进程等待,直到子进程终止
地址空间:
- 子女复制双亲
- 子女有一个程序被调入
UNIX例子:
- fork系统调用创建新进程
- 在fork用一个新程序替代了进程的内存空间之后,采用exec系统调用
原子操作:
- 不允许中途被打断的操作,要么创建成功,要么不成功
- 一旦开始就一直运行到结束,中间不会有任何上下文切换
- 进程创建是原子操作
原子性必须需要硬件的支持
- X86:CPU在指令执行期间对总线加锁的手段
- CPU引线#HLOCK pin
2. 进程终止
(1)进程执行完最后一项并退出(exit)
- 从子进程向父进程输出的数据(通过系统调用wait)
- 操作系统收回进程的资源,如物理和虚拟内存,打开文件、I/O缓冲
(2)父进程可中止子进程的执行(系统调用abort)
- 子进程超量分配资源
- 赋予子进程的任务不再需要
- 如果父进程结束,一些系统不允许子进程继续存在,所有子进程都需要终止——级联终止
(3)父进程可以等子进程结束
- 调用wait()系统调用
Windows进程操作
(1)CreateProcess:进程创建
- 新进程可以继承:打开文件的句柄、各种对象(如进程、线程、信号量、管道等)的句柄、环境变量、当前目录、原进程的控制终端、原进程的进程组(用于发送Ctrl+C或Ctrl+Break信号给多个进程)——每个句柄在创建或打开时能指定是否可继承
- 新进程不能继承:优先权类、内存句柄、DLL模块句柄
(2)ExitProcess和TerminateProcess:进程退出
ExitProcess——主动终止
TerminateProcess——强制终止
- 如果某个process想自己停止执行,可调用ExitProcess()。C程序库中的exit(),exit()在自动执行一些清除垃圾工作后,再调用ExitProcess():void ExitProcess(UNT uExitCode)
- 如果process A想要process B停止执行,可在取得process Bd handle后,调用TerminateProcess():BOOL TerminateProcess(HANDLE hProcess,UNT uExitCode)
(3)WaitForSingleObject:等待子进程结束
linux进程创建
(1)fork函数:
#include <unistd.h>
pid_t fork();
fork函数奇妙之处在于:仅仅被调用一次却能够返回两次,而且可能有不同的返回值。
(2)当一个进程调用fork后会创建一个子进程
(3)这个子进程和父进程不同:进程ID
父进程和子进程
区分父进程和子进程:
跟踪fork返回值:失败,返回-1;否则,父进程fork返回子进程的ID,fork子进程返回0
所以可根据返回值来确定父子进程。
执行其他程序
exec族调用有着6个函数:
# include <unistd.h>
int execl(const char * path, const char * arg,.…);
int execlp(const char * file, const char * arg.…);
int execle(const char * path, const char * arg,.…, char * const envp[]);
int execv(const char * path, char * const argv]);
int execvp(const char * file, char * const argv]);
int execve(const char * path, char * const argv[], char * const envp[]);
exec族作用:根据指定的文件名找到可执行文件,并用它来取代调用进程的内容(在调用进程内部执行一个可执行文件,这里的可执行文件既可以是二进制文件,也可以是Linux系统下可执行的脚本文件)
exec函数族的函数执行成功后不会返回。因为调用进程那时起,包括代码段、数据段和堆栈都已经被新内容取代,只留下进程ID等一些表面上的信息仍保持原样。
等待
父进程阻塞直到子进程完成任务。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *stat_loc);
pid_t wait(pid_t pid, int *stat_loc, int options);
该方式可以通过调用wait或者waitpid系统调用实现:
进程一旦调用了wait函数,将立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
wait要与fork配套使用,如果在使用fork之前调用wait,wait的返回值为-1,正常情况下,wait的返回值为子进程的pid。当父进程没有使用wait函数等待已终止的子进程时,子进程就会进入一种无父进程清理自己的状态,此时子进程就是僵尸进程,不能在内核中清理尸体的情况。
例子:
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <errno.h>
int main()
{
int rtm;//子进程的返回数值
if(fork()==0){
//子进程执行此命令
execlp("/bin/ls","ls-al",(char*)0);
//如果exec函数返回,表明没有正常执行命令,打印错误信息V
exit(1);
}
else{
/*父进程,等待子进程结束,并打印子进程的返回值*/
wait(&rtn);
printf("child process return%d\n".rtn);
}
}
进程通信
操作系统内并发执行的进程可以是独立进程或协同进程。
独立进程:不会影响另一个进程的执行或被另一个进程执行影响。
协同进程:可能影响另一个进程的执行或被另一个进程执行影响
进程协同的优点:信息共享、加速运算、模块化、方便
进程间通信(IPC)
协同进程需要一种进程间通信(IPC),用于进程通信的机制,同步其间的活动。
两种基本模式:
- 共享内存
允许以最快的速度进行方便的通信。仅在建立共享内存时需要系统调用,一旦建立,所有的访问都被处理为常规的内存访问,不需要内核的帮助。 - 消息传递
交换较少数量的数据。通过系统调用来实现,需要更多的内核介入,消耗时间多
如下图,a图共享内存模式,b图消息传递模式
共享内存系统
采用共享内存的进程间通信需要通信进程建立共享内存区域。
- 一块内存在多个进程间共享;
一块共享内存区域驻留在生成共享内存段进程的地址空间,其他希望使用这块内存的进程必须将此放到他们自己的地址空间上。 - 通信由应用程序自己控制
- 一般用于大数据通信
- 实现手段:文件映射、管道、剪贴板
例子:生产者-消费者
生产者进程生产消息,供消费者进程消费。
为了允许生产者进程和消费者进程并发执行,必须要有一个缓冲区被生产者填充,被消费者使用。可以使用两种缓冲:
- 无界缓冲(Unbounded-buffer):没有对缓冲区大小的限制。
- 有界缓冲(Bounded-buffer):对缓冲区大小作了限定。如果缓冲区为空,消费者必须等待;缓冲区为满,生产者必须等待。
有界缓冲共享变量:
#define BUFFER SIZE 10
Typedef struct{
...
}item;
item buffer[BUFFER_SIZE];
int in=0;
int out=0;
in指向缓冲区中下一个空位;out指向缓冲区中第一个非空位,但最多只能填满缓冲区的BUFFER_SIZE-1个项。
生产者进程:
item nextProduced
while(true){
/*Produce an item in nextProcuded */
while((in=(in +1)% BUFFER SIZE count)==out)
;/*do nothing--no free buffers */
buffer[in]=nextProduced;
in=(in+1)%BUFFER SIZE;
消费者进程:
item nextConsumed
while(true){
while(in==out);//do nothing-nothing to consume
// remove an item from the buffer
nextConsumed=buffer[out];
out=(out +1)% BUFFER SIZE;
/*consume the item in nextConsumed*/
消息传递
- 消息传递在微内核中的应用
该方法由操作系统提供机制,让协同进程能通过消息传递工具来进行通信。 - 远程通信无法采用共享内存
该机制允许进程不必通过共享地址空间来实现通信,这在分布式环境中非常有用,因为分布式环境下通信进程可能位于由网络连接起来的不同计算机上。 - 两个操作:
发送send(message)——固定或可变大小消息
接受receive(message) - 若P和Q要通信,需要:建立通信连接,通过send/receive交换消息
- 通信连接的实现:物理的(如,共享存储,硬件总线);逻辑的(如,逻辑特性)
直接通信
(1)进程必须显式的命名
send(P,message)——向进程P发消息
receive(Q,message)——从进程Q收消息
(2)通信连接的特性
- 连接自动建立
- 连接精确地与一对通信进程相关
- 在每一对通信进程间存在一个连接
- 连接可单向,但通常双向
间接通信
(1)消息导向至信箱并从信箱接收
每个信箱有一个唯一的id;
仅当共享一个信箱时进程才能通信。
(2)通信连接的特性:
- 仅当进程共有一个信箱时连接才能建立
- 连接可同多个进程相关
- 每一对进程可共享多个通信连接
- 连接可单向或双向
(3)进程和操作系统都可以拥有邮箱,如邮箱为进程所有,则邮箱是进程地址空间的一部分,为操作系统所拥有的邮箱是独立存在的,不属于某个特定的进程。因此,操作系统必须提供机制,允许进程进行如下操作:
- 创建新的信箱
- 通过信箱发送和接收消息
- 销毁信箱
(4)两个原语被定义:
- send(A,message)——发送消息到信箱A
- receive(A,message)——从信箱A接收消息
(5)信箱共享:
- P1,P2与P3共享信箱A
- P1发送;P2与P3接受
- 谁得到消息?
(6)解决方案:
- 允许一个连接最多同2个进程相关
- 只允许一个时刻有一个进程执行接受操作
- 允许系统任意选择接收者,发送者被通知谁是接收者
同步
(1)消息传递可阻塞(blocking)或非阻塞(non-blocking)
(2)阻塞-同步
- 阻塞send:发送进程阻塞,直到消息被接收
- 阻塞receive:接受者进程阻塞,直到有消息可用
(3)非阻塞-异步
- 非阻塞send:发送进程发送消息并继续操作
- 非阻塞receive:接收者收到一个有效消息或无效消息