C/C++中的进程同步
进程同步是一种克服对共享数据并发访问问题的技术,并发访问可能导致数据不一致。协作进程是可以影响或被其他进程影响的进程,这会导致进程数据不一致,因此需要进程同步来保证数据一致性。
临界区问题
每个进程都有一个保留的代码段,称为**临界区**。在这个区域中,进程可以更改公共变量、更新表、写入文件等。关于临界区的关键点是:当一个进程正在其临界区执行时,任何其他进程都不能在其临界区执行。每个进程必须在进入其临界区之前请求许可,实现此请求的代码部分是**入口区**,代码的末尾是**出口区**,其余代码是**剩余区**。
以下是特定进程 P1 临界区的结构
临界区必须满足三个要求
- **互斥** - 如果一个进程,例如 P1,正在其临界区执行,则任何其他进程,例如 P2,都不能在其临界区执行。
- **进展** - 如果没有进程在其临界区执行,并且有一些进程想要进入其临界区,则只有那些不在其剩余区执行的进程才能请求进入临界区,并且选择不会无限期推迟。
- **有界等待** - 在有界等待中,对进程在发出进入其临界区的请求后以及在该请求被授予之前可以进入其临界区的次数有限制。
操作系统中常用两种方法来处理临界区。
**抢占式内核** - 抢占式内核允许进程在内核模式下运行时被抢占。
**非抢占式内核** - 非抢占式内核不允许在内核模式下运行的进程被抢占。
Peterson 解法
Peterson 解法是解决临界区问题的经典基于软件的解决方案。它仅限于两个进程,它们在临界区和剩余区之间交替执行。Peterson 的解法需要两个在两个进程之间共享的数据项,即:
- Int turn;
- Boolean flag[2];
这里,变量 turn 指示谁轮到进入其临界区,flag 数组指示进程是否准备好进入其临界区。
如果 turn == i,则表示允许进程 Pi 进入其临界区。
如果 flag[j] 为 TRUE,则表示进程 j 准备好进入其临界区。
以下是 Peterson 解法中进程 P 的结构
Peterson 解法保留了所有三个条件:
- **互斥** - 每次只有一个进程可以访问临界区。
- **进展** - 临界区外的进程不会阻止其他进程进入临界区。
- **有界等待** - 每个进程都将有机会进入其临界区,而不会无限期等待。
同步硬件
它使用两种类型的指令实现:
- Test and Set()
- swap()
Test and Set() 是一种硬件解决方案,用于解决同步问题。其中,有一个由多个进程共享的共享变量,称为 Lock,它可以取 0 和 1 这两个值,其中 1 表示获得锁,0 表示释放锁。
每当进程尝试进入其临界区时,都需要查询锁的值。如果锁的值为 1,则它们必须等待,直到锁的值更改为 0。
以下是使用 TestAndSet() 实现的互斥。
信号量
信号量是一种同步工具,用于克服 TestAndSet() 和 Swap() 指令产生的问题。信号量 S 是一个整数变量,可以通过两个标准原子操作访问,即 wait() 和 signal()
wait() 函数
wait(S) { While S <= 0 ; // no operation S--; }
Signal() 函数
signal(S) { S++; }
当一个进程修改信号量的值时,任何其他进程都不能同时操作相同的信号量值。
以下是使用信号量实现的互斥。
操作系统使用两种类型的信号量:
**计数信号量** - 此类型信号量的值可以超过不受限制的域。
**二元信号量** - 此类型信号量的值可以在 0 和 1 之间变化。它们也称为互斥锁。操作系统利用它来解决多个进程中临界区的问题。