我是如何学习写一个操作系统(七):进程的同步与信号量

前言

在多进程的运行环境下,进程是并发执行的,不同进程间存在着不同的相互制约关系。为了协调进程之间的相互制约关系,达到资源共享和进程协作,避免进程之间的冲突,引入了进程同步的概念。

临界资源

多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程所使用,我们把一次只允许一个进程使用的资源成为临界资源。
对临界资源的访问,必须互斥的进行。每个进程中,访问临界资源的那段代码成为临界区。

为了保证临界资源的正确使用,可以把临界资源的访问过程分为四个部分。

  • 进入区。为了进入临界区使用临界资源,在进入去要检查可否进入临界区。
  • 临界区。进程中访问临界资源的那段代码。
  • 退出区。将正在访问临界区的标志清除。
  • 剩余区。代码中的其余部分。

一般实现进程的同步有这几种方法:

  • 提过硬件提供的实现
  • 信号量
  • 管程

生产者-消费者实例

下面的代码就分别是生产者进程和消费者进程,而buffer就是临界资源

  • 当生产者要访问临界资源时,会先判断buffer是不是已经满了,而消费者则判断buffer是不是空的,这就是访问临界资源的进入区

  • 而中间对buffer的操作就是临界区

  • 最后对counter的加减就是设置对临界区访问的标志

  • 但是这里依旧也有可能出现问题,比如当进程走到in = (in + 1) % BUFFER_SIZE;的时候,这时候操作系统进行调度,这时候的counter的值可能还是0,所以消费者进程可能就会出现问题

这里的处理可以是利用关闭中断来限制进程的切换,但是在多核CPU下一样不管用

这里就涉及到了临界区的保护了

1
2
3
4
#define BUFFER_SIZE 10
typedef struct { . . . } item;
item buffer[BUFFER_SIZE];
int in = out = counter = 0
1
2
3
4
5
6
7
while (true) {
while(counter== BUFFER_SIZE)
;
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
counter++;
}
1
2
3
4
5
6
7
while (true) {
while(counter== 0)
;
item = buffer[out];
out = (out + 1) % BUFFER_SIZE;
counter--;
}

信号量

什么是信号量

为了防止出现因多个程序同时访问一个共享资源而引发的一系列问题,我们需要一种方法,它可以通过生成并使用令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区。

为了避免像上面一样会发生竞争条件,程序对信号量访问都是原子操作,且只允许对它进行等待(即P(信号变量))和发送(即V(信号变量))信息操作。

信号量的工作原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

  • P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行
  • V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

举个例子,就是两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。

Linux 0.11的进程同步

在Linux 0.11里是没有实现信号量的,考虑后面会自己实现一个。这里先看一下Linux 0.11里用来进行进程同步的两个函数

sleep_on

  • p实际上指的是一个等待队列

  • 如果当前进程是进程0或者无效,就直接退出

  • 然后把要等待的进程放到等待队列的头节点,把状态设置为不可中断的等待状态

    这里队列的形成非常非常的隐蔽,首先把用tmp指向之前的进程,在把当前要睡眠的进程放入,而之所以能形成队列,是因为现在放入队列的进程的tmp作为局部变量是保存在这个进程的堆栈中的,这样在把进程切换回来的时候,tmp就自然的指向上一个进程了。

  • 最后当这个进程被唤醒的时候,会回到if语句唤醒等待队列中所有进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void sleep_on(struct task_struct **p)
{
struct task_struct *tmp;

if (!p)
return;
if (current == &(init_task.task))
panic("task[0] trying to sleep");
tmp = *p;
*p = current;
current->state = TASK_UNINTERRUPTIBLE;
schedule();
if (tmp)
tmp->state=0;
}

wake_up

  • 唤醒 *p 指向的任务。 *p是任务等待队列头指针。由于新等待任务是插入在等待队列头指针处的,
    1
    2
    3
    4
    5
    6
    7
    void wake_up(struct task_struct **p)
    {
    if (p && *p) {
    (**p).state=0; // 置为就绪(可运行)状态TASK_RUNNING.
    *p=NULL;
    }
    }

小结

首先竟然有了多进程,那在访问共享资源的时候自然就会发生制约关系,所以才引入了进程同步的概念。

而进程同步的关键就是对临界区的保护,信号量就是一种可以很好的实现对临界区保护的方法