进程的概念
进程(process) 就是正在执行的程序。进程完成其任务需要一定的资源,如cpu时间,内存,文件及I/O等。当进程被创建或执行时,这些资源就分配给了进程。操作系统的最基本人物就是进程管理。
进程的基本定义如下:进程是程序在一个数据集合上的运行过程,是系统进行资源分配和调度的一个独立单位。
进程与程序既有联系又有区别:程序是静止的,而进程是程序的一次动态执行过程。同一个程序可以对应多个不同的独立进程。
进程的特性:
- 动态性:有一定的生命周期,不同时期处于不同的状态。
- 并发性:多个进程实体同时存在于内存当中,并能在一段时间内都得到执行,从宏观上看是同时执行,从微观上来看是时间分成若干段。(应当与并行执行区分:并发是无论宏观还是微观上,两进程都是一起执行,它需要多个cpu的支持。)
- 独立性:一个独立运行的基本单位。
- 异步性(asynchronous):各进程按各自独立的、不可预知的速度向前推进。即按照异步的方式运行,这造成了进程间的相互制约。
- 结构特征:进程是由程序段、数据段、及进程控制块等部分组成的一个实体。
拓展:PCB(进程控制块)作为进程实体的一部分,记录操作系统所需的用于描述进程的当前情况以及管理进程运行的全部信息,是操作系统中最重要的记录型数据结构,一般包含几部分信息:
- 进程描述信息:用于唯一标识一个进程,包括进程标识符(内部标识符)、进程名、用户标识符。ubuntu下终端输入top,查看ubuntu进程信息,第一列为进程标识符ID,第二列为用户标识符ID,最后一列为进程名称。
- 进程调度信息:用于保存与进程调度有关的信息,包括当前状态、优先级和运行统计信息等。
- 进程控制信息:保存进程控制相关的信息,包括进程程序段和数据段的地质、进程间同步和通信信息,以及进程的链接指针等。
- 进程状态信息:保存当前处理器状态信息,包括寄存器状态信息和用户栈指针两部分。
linux常见的三种阻塞方式:
- sleep函数:将自己阻塞一段时间,时间结束后返回。
- wait函数:阻塞进程以等待子进程,第一个子进程结束后返回。
- pause函数:暂停进程,接受到信号后恢复执行。
现代操作系统中一如来多道程序设计技术,允许多个进程同时在系统内并发执行。但操作系统对进程的并发控制对于系统的安全性恶化稳定性是非常必要的。
进程并发控制的关键是控制进程的同步与互斥。
进程之间的交互方式:
- 竞争资源(互斥):进程彼此不知道对方的存在,逻辑上也没有关系。他们同时访问共享资源,这些资源有一个性质,一次仅允许一个进程使用,这样的共享资源被称为临界资源(Critical
Resource ),进程中访问临界资源的那段程序被称为临界区(Critical Section) - 共享合作(同步synchronous):各进程不知道对方的名称,但通过他们共享的对象(如某个I/O缓冲区)间接知道对方的存在,并相互协作共同完成一项任务。
- 通信合作:进程之间知道对方的存在,他们通过进程ID相互通信,交换信息,合作完成一项工作。
进程互斥要求:
- 空闲让进
- 忙则等待
- 让权等待
- 有限等待
信号量实现进程的同步与互斥
荷兰科学家Dijkstra(大大…大佬)提出解决同步与互斥的信号量方法。结构体信号量Semaphore和两个源语操作semWait、semSignal来实现。
typedef struct semaphore
{
int value; // 代表资源数,若为复数则代表队列中阻塞的进程个数,初值一般为1
Queue queue; // 阻塞队列
}semaphore;
//在申请资源前执行,若value<0则添加至阻塞队列当中。
void semWait(Semaphore s) //又称P操作、wait、DOWN操作
{
if (--s.value < 0)
{
将进程插入到队列queue中;
将当前进程阻塞;
}
}
//在某进程使用完资源时执行,当发现value<=0,则挑选一个进程进行唤醒,将其插入就绪队列当中
void semSignal(Semaphore s) //又称v操作、signal、UP操作
{
if (++s.value <= 0){
将队列queue中移除一个进程;
将该进程插入到就绪队列中;
}
- 那么接下来,我们利用信号量来实现互斥
设有n个进程,需要互斥访问共享资源,代码如下:
const int n = 进程数;
semaphore s = 1
void p(int i )
{
while(true)
{
semWait(s);
//临界区,此处访问共享资源
semSignal(s); // 推出区
//程序其他剩余部分
}
}
void main()
{
perbegin(p(1), p(2) ……, p(n));
}
信号量解决互斥问题的方法是在同一进程中,设置一个信号量,在进入临界区之前执行semWait,访问临界区之后执行semSignal。因此互斥信号量总是在同一个进程中成对出现。
- 在来一个利用信号量实现同步
设有两个进程p1,p2,p1每次进行一次计算,并将结果放入缓冲区;p2从缓冲区取出结果,并将结果打印出来,缓冲区只有一个。
分析:进程p1,p2通过缓冲区实现同步,p1负责放入数据,p2负责区数据。但是,p1不能将数据放入已满缓冲区,同样的p2不能从空缓冲区取出数据。因此需要设置两个信号量,s1和s2,代表缓冲区空和缓冲区满
semaphore s1 = 1, s2 = 0;
void p1()
{
while (true)
{
//计算;
semWait(s1);
//计算结果向缓冲区存储;
semSignal(s2);
}
}
void p2()
{
while(true)
{
semWait(s2);
// 从缓冲区获取数据;
semSignal(s1);
}
}
void main()
{
perbegin(p1, p2);
}
- 生产者消费者问题
分析:两类进程需要互斥访问缓冲区,因此设置一个信号量s用于缓冲区互斥;同上面的简单同步一样生产者和消费者需要对缓冲区进行同步控制:生产者不能往满的缓冲区存放数据,消费者不能从空的缓冲区中取数据。由于缓冲区的大小为N,因此设置信号量n表示有数据的缓冲区个数,e表示空格缓冲区个数;初始时,所有的缓冲区都为空。
const int sizebuffer = 缓冲区大小
semaphore s = 1, n = 0, e = sizebuffer
void producer()
{
while(true)
{
// 生产数据;
semWait(e);
semWait(s);
// 将数据存入缓冲区
semSignal(s);
semSignal(n);
}
}
void consumer()
{
while(true)
{
semWait(n);
semWait(s);
// 缓冲区中取出一个数据
semSignal(s);
semSignal(e);
/消费数据;
}
}
void main()
{
parbergin(producer, consumer)
}
注意以下几点:
-
同步信号量n、s的semWait和semSignal也必须成对出现,但是在不同的进程中
,生产者进场中有semWait(e),而semSignal(e)出现在消费者进程中;消费者进程中有semWait(n)、而semSignal(n)出现在生产者当中。 - 同一个进程中,既有同步控制、也有互斥控制,那么通常先进行同步控制,然后再进行互斥控制,否则会引起“死锁”。例如,生产者进程当中的semWait(n)与semWait(s)的次序不能颠倒。
- 若有多个释放资源的semsignal操作,则对次序 没有特殊的要求。
管程和消息传递
管程引入面向对象的思想,他把共享资源的数据结构以及一组对该资源的操作和其他相关操作封装在一起所构成的软件模块。进程只能通过管程定义的接口进入管程,访问共享资源。go语言当中的goroutine便是基于这种思想,收到了众多程序员们的青睐,成为了golang独特的多线程特点。
消息传递是消息为单位在进程间惊醒数据交换,其核心是通过一对原语实现在进程间消息的传递。
send(deftination, message)
receive(source, message)
只有当一个进程发送消息后,接受者才能收到消息。
参考书目:《操作系统原理及应用(linux)》,汪杭军主编,机械工业出版社