操作系统:进程同步三大经典问题

 

日期:2019/4/15

内容:进程同步;生产者与消费者;读写者;哲学家进餐;信号量机制。

 

一、生产者与消费者问题

1.1 版本1

  • 代码

void producer()

{

    while (count == n)

        ;

    buff[in] = produce_item();

    in = (in + 1) % n;

    count++;

}

void consumer()

{

    while (count == 0)

        ;

    item = buff[out];

    print(item);

    out = (out + 1) % n;

    count--;

}

  • 存在问题

    >>两个while循环一直在"忙等",不符合进程同步的"让权等待"原则。

    >>对于count变量的访问没有保护。(需要加锁保护)

1.2 版本2:使用信号量

  • 代码

semaphore empty = n, full = 0;

void producer()

{

    while (true)

    {

        wait(empty);

        buffer[in] = produce_item();

        in = (in + 1) % n;

        signal(full);

    }

}

void consumer()

{

    while (true)

    {

        wait(full);

        item = buffer[out];

        print(item);

        out = (out + 1) % n;

        signal(empty);

    }

}

  • 存在问题

    >>如果有2个producer进程,empty>=2时,同时进入wait(empty)之后的临界区,对于buff的写和in的写产生竞争。

    >>如果有2个consumer进程,full>=2时,同时进入wait(full)之后的临界区,对于out的写产生竞争。

1.3 版本3:临界区加锁(正确版本)

  • 代码

semaphore pmutex = 1, cmutex = 1;

semaphore empty = n, full = 0;

void producer()

{

    while (true)

    {

        wait(empty);

        wait(pmutex);

        buff[in] = produce_item();

        in = (in + 1) % n;

        signal(pmutex);

        signal(full);

    }

}

void consumer()

{

    while (true)

    {

        wait(full);

        wait(cmutex);

        item = buff[out];

        print(item);

        out = (out + 1) % n;

        signal(cmutex);

        signal(empty);

    }

}

注:教材对于producer和consumer的临界区都使用了同一个mutex,表示producer和consumer互斥进入临界区。但是个人感觉似乎没必要,因为producer和consumer对于buff的访问不存在竞争关系,只需要保证多个producer进程之间互斥,多个consumer进程之间互斥即可。

 

二、读者与写者问题

对于多个进程访问同一文件:

  • 写者:可以读,也可以写
  • 读者:只读
  • 允许多个读者同时读
  • 某一写者在写,不允许其他读写操作

R

R

1

R

W

0

W

R

0

W

W

0

 

与生产者消费者的区别:

  • 生产者不仅仅是写进程,还必须调整in指针,但是在读写者问题当中每个进程的文件读写指针是相互独立的。
  • 消费者同理。

2.1 版本1

  • 代码

semaphore mutex = 1;

void writer()

{

    while (true)

    {

        wait(mutex);

        write_operation();

        signal(mutex);

    }

}

void reader()

{

    while (true)

    {

        wait(mutex);

        read_operation();

        signal(mutex);

    }

}

  • 问题

    >>不满足"同时读"。

2.2 版本2:增加读者计数器

  • 代码

semaphore wmutex = 1;

void writer()

{

    while (true)

    {

        wait(wmutex);

        write_operation();

        signal(wmutex);

    }

}

void reader()

{

    while (true)

    {

        if (reader_count == 0)

            wait(wmutex);

        reader_count++;

 

        read_operation();

 

        reader_count--;

        if (reader_count == 0)

            signal(wmutex);

    }

}

  • 问题

    >>可以允许多个reader进程同时读,但是对reader_count的访问存在竞争。

2.3 版本3:给reader_count加锁

rmutex实际上只用于保护reader_count被正确更新。

  • 代码

semaphore wmutex = 1, rmutex = 1;

int reader_count = 0;

void writer()

{

    while (true)

    {

        wait(wmutex);

        write_operation();

        signal(wmutex);

    }

}

void reader()

{

    while (true)

    {

        if (reader_count == 0)

            wait(wmutex);

        wait(rmutex);

        reader_count++;

        signal(rmutex);

 

        read_operation();

 

        wait(rmutex);

        reader_count--;

        signal(rmutex);

        if (reader_count == 0)

            signal(wmutex);

    }

}

  • 问题

    >>举例说明红色部分代码问题,reader1和reader2同时执行,同时读取reader_count均为0,那么(假设)先执行reader1的wait1(wmutex),reader1进入阻塞队列;后执行reader2的wait2(wmutex),reader进入阻塞队列。(wait是原语操作,1和2必有先后之分)。但是在reader2执行的时候reader_count的值应为1(但实际是0),这就会使reader2成为僵死进程。

2.4 正确版本1:读者优先

  • 代码

semaphore wmutex = 1, rmutex = 1;

int reader_count = 0;

void writer()

{

    while (true)

    {

        wait(wmutex); //保证了WW互斥

        write_operation();

        signal(wmutex);

    }

}

void reader()

{

    while (true)

    {

        wait(rmutex); //保证只能有一个reader访问reader_count

        if (reader_count == 0)

            wait(wmutex);

        reader_count++;

        signal(rmutex);

 

        read_operation();

 

        wait(rmutex); //保证只能有一个reader访问reader_count

        reader_count--;

        if (reader_count == 0)

            signal(wmutex);

        signal(rmutex);

    }

}

  • 问题

    >>当读者进程≥1时,随后读者进程直接进入临界区,这是读者优先的表征。

    >>写者饿死问题。

    >>假设有进程{R1, W1, R2, R3, ..., Rn}

    >>>>执行R1,那么wmutex变为0,执行read_operation

    >>>>执行W1,wmutex变为-1,阻塞W1

    >>>>执行R2,wmutex不变,执行read_operation

    >>>>对于若干Ri,均是如此,如果CUP资源不足,Ri会进入就绪队列

    >>>>那么W1则很长时间无法调度,就算被siganl操作移入就绪队列也是在队列尾部,产生写者饿死问题。

2.5 正确版本2:写者优先

解决写者饿死问题:保证一个写进程想写时(即使它有可能进入阻塞队列),不允许新的读进程访问临界区。(注意这并不是为了解决"读和写不能同时进行")。上面读者优先的症结在于写进程想写,但是读进程优先,不断地进入临界区,即读的调度优先级比写高,从而导致读者饿死。

  • 代码

int reader_count = 0, writer_count = 0;

semaphore x = 1, y = 1, z = 1;

semaphore wmutex = 1, rmutex = 1;

void writer()

{

    while (true)

    {

        wait(y);

        if (writer_count == 0)

            wait(rmutex);

        writer_count++;

        signal(y);

 

        wait(wmutex);

        write_operation();

        signal(wmutex);

 

        wait(y);

        writer_count--;

        if (writer_count == 0)

            wait(rmutex);

        signal(y);

    }

}

void reader()

{

    while (true)

    {

        wait(z);

        wait(rmutex);

        wait(x);

        if (reader_count == 0)

            wait(wmutex);

        reader_count++;

        signal(x);

        signal(rmutex);

        signal(z);

 

        read_operation();

 

        wait(x);

        reader_count--;

        if (reader_count == 0)

            wait(wmutex);

        signal(x);

 

    }

}

  • 解析
    • x:控制reader_count的访问竞争。
    • y:控制writer_count的访问竞争。
    • wmutex:控制有写进程在写的时候,读进程不能进入临界区。
    • rmutex:写进程先把rmutex拿到,保证在写进程运行时,其他所有读进程均无法进入临界区(只能阻塞在rmutex队列上,见红色代码)。
    • z:保证了在rmutex的阻塞队列上,只有一个读进程在排队,其余所有读进程在等待rmutex之前,在z的队列上排队。(尝试把wait(z)和signal(z)去掉理解一下)如果没有z,则都在rmutex上排队。
    • 为什么需要z?在rmutex上不允许长的排队,否则写进程不能跳过这个长队列。
    • 举例说明:{R1, W1, R2, R3, ..., Rn}
      • 如果有z,reader1先wait1,rmutex=0;然后writer再wait(rmutex),rmutex=-1;后面尽管有再多的reader都在堵塞在z。此时,只需要等待reader1执行signal(rmutex),writer即能够进入就绪状态,优先于z中阻塞的reader。
      • 如果无z,writer和所有的reader都进入rmutex排队,实质上无法保证writer优先于reader。

三、哲学家进餐问题

  • 问题描述

    哲学家需要只吃饭和思考,需要用2把叉子才能吃饭。叉子只能用自己座位两侧的。需要避免死锁和饥饿。

3.1 版本1

  • 代码

semaphore fork[5] = {1, 1, 1, 1, 1};

void philosopher(int i)

{

    while (true)

    {

        think();

        wait(fork[i]);

        wait(fork[(i + 1) % 5]);

        eat();

        signal(fork[i]);

        signal(fork[(i + 1) % 5]);

    }

}

 

void main()

{

    for (int i = 0; i < 5; i++)

        create_process(philosopher(i));

}

  • 问题

    >>死锁。每个人同时拿起自己左边的叉子,即philosopher[i]拿起fork[i],那么每个人在wait(fork[(i + 1) % 5])上都会阻塞。

3.2 版本2(正确版本)

解决方案:同时只允许4个人进入房间就餐,那么即至少能保证有1人可以拿到2个fork。

  • 代码

semaphore fork[5] = {1, 1, 1, 1, 1};

semaphore room = 4;

void pholosopher(int i)

{

    think();

    wait(room);

    wait(fork[i]);

    wait(fork[(i + 1) % 5]);

    eat();

    signal(fork[i]);

    signal(fork[(i + 1) % 5]);

    signal(room);

}

void main()

{

    for (int i = 0; i < 5; i++)

        create_process(philosopher(i));

}

  • 解析

    保证不会死锁和饥饿。(管程解决方案待续)

posted @ 2019-04-15 22:00  sinkinben  阅读(3122)  评论(0编辑  收藏  举报