并发性:互斥与同步

操作系统核心问题是关于进程与线程的管理。

多道程序设计技术:管理单处理器系统中的多个进程
多处理技术:管理多处理器中的多个进程
分布式处理技术:管理多台分布式计算机系统中的多个进程的执行

并发是所有问题的基础,也是操作系统设计的基础。
并发包括了很多的设计问题:进程间通信、资源共享与竞争、多个进程活动的同步、分配给进程的多个处理器时间等

支持并发进程的基本要求就是要加强互斥的能力,当一个进程被赋予互斥能力时,那么在活动期间,它具有排斥所有其他进程的能力。

关键术语:
原子操作:一个或多个指令的序列,对外是不可分的
临界区:是一段代码,在这段代码中进程将访问共享资源,当另一个进程在这段代码中运行时,这个进程就不能在这段代码中运行
死锁:两个或者两个以上的进程因为其中的每一进程都在等待其他进程做完某些事情而不能继续进行,这样的情形叫死锁
活锁:两个或者两个以上的进程为了响应其他进程中的变化而持续改变自己的状态但不做有用的工作,这样的情形叫活锁
互斥:当一个进程在临界区访问资源时,其它进程不能进入该临界区访问任何共享资源,这种情形叫互斥
竞争条件:多个线程或进程在读写一个共享数据时,结果依赖于他们执行的相对时间,这种情形叫竞争
饥饿:一个可运行的进程尽管能继续执行,但被调度器无限期地忽视,而不能被执行调度的情况

1.并发原理

在单处理器多道程序设计中,进程交替执行,表现出一种同时执行的外部特征。
在多处理器系统中,不仅可以交替执行进程,而且可以重叠执行进程

并发的解决方案:控制对共享资源的访问

并发带来的设计和关联问题:
1)操作系统必须记住各个活跃进程,可以通过使用进程控制块来实现
2)操作系统必须为每个活跃进程分配和释放各种资源,有时候多个进程都想访问相同的资源
    这些资源包括:处理器时间、存储器、文件、I/O设备
3)操作系统必须保护每个进程的数据和物理资源,避免其他进程的无意干涉
4)一个进程的功能和输出结果必须与执行速度无关

进程交互:
1)进程不知道对方的存在:不会一起工作,但有可能产生资源的竞争
2)进程间接知道对方的存在:进程间共享某些对象,表现为合作
3)进程直接知道对方的存在:可以通过进程ID通信,表现为合作

进程间资源竞争:
竞争进程间没有任何的消息交换,但是一个进程的执行可能影响到竞争进程的行为,如资源等待带来的执行时间的变慢。
竞争进程面临的三个控制问题:
1)互斥
    临界资源,不可共享资源,使用临界资源的那一部分程序成为程序的临界区
2)死锁
    相互需要对方拥有的资源,然而相互都不释放这些资源,造成死锁
3)饥饿
    无限期拒绝访问资源

进程间通过共享合作:
知道其他进程有可能修改同一数据,但是并不确切知道其他进程信息,为了保持共享数据的完整性。
可以采用两种模式(读和写)访问数据项,并且只有写操作必须保证互斥。

进程间通信合作:
各个进程与其他进程进行连接,通过消息进行通信提供了同步和协调活动的方法。

2.互斥:硬件的支持

1)中断禁用
    单处理器临界区中断禁用
2)专用机器指令
    在硬件级别上,对存储单元的访问排斥对相同单元的其它访问,但是效率较低

3.信号量

基本原理:
两个或多个进程可以通过简单的信号进行合作,一个进程可以被迫在某一位置停止,知道它接收到一个特定的信号。任何复杂的合作需求都可以通过适当的信号结构得到满足。
为了发信号,需要使用一个称作信号量的特殊变量。
为通过信号量s传送信号,进程执行原语Signal(),为通过s接收信号,进程执行原语Wait(),它们被设定为原子操作

信号量常被看做一个具有整数值的变量,在它之上的三个操作:(通常称为一般信号量或计数信号量)
1)可以初始化为非负数:发出Wait操作后可以立即继续执行的进程的数量
2)Wait()操作使信号量减1,如果值变为负数,执行Wait()进程被阻塞:
    当s等于0,那么下一个进行Wait操作的进程将被阻塞,s为负值时表示等待被解除阻塞的进程数量
3)Signal()操作使信号量加1,如果值小于等于零,被Wait()阻塞的进程被解除阻塞
    在s为负值的情况下,每一个Signal操作就会解除一个阻塞进程

二元信号量的三种操作:
1)二元信号量的值只能是0和1,可以被初始化为0或1
2)Wait操作检查信号的值,如果值为0,那么进程执行Wait就被阻塞,如果为1,就继续执行,s改变为0
3)Signal操作检查是否存在手足进程,如果有,那么唤醒被阻塞进程,如果没有那么值置为1

互斥量:与二元信号量相关,
区别在于为互斥量加锁和为互斥量解锁的进程必须是同一个进程,而二元信号量可以是不同的进程进行的加锁和解锁
使用互斥量解决一次只允许一个进程进入临界区的要求,而计数信号量可以允许多个进程进入临界区的需求

使用队列来保存在信号量上等待的进程,如果是使用先进先出的策略的信号量称为强信号量,没有规定策略的称为弱信号量

生产者/消费者问题
一个或多个生产者生产产品放置到缓冲区,而有一个消费者从缓冲区取产品,每次取一项;
系统保证避免对缓冲区的重复操作,任何时候只允许一个主体(生产者或消费者)访问缓冲区。

一种办法:
信号量s用于实施互斥,一次只能一个主体访问缓冲区;
信号量delay用于迫使消费者当缓冲区为空时等待wait,用数值n记录缓冲区中的产品个数

生产者行为:
添加产品时,先执行wait(s)操作,之后执行signal(s)操作,实现互斥使用缓冲区,同时在临界区中将n的值增1.
如果n=1,那么本次操作以前的缓冲区是空的,那么生产者执行signal(delay)通知消费者
消费者行为:
最开始消费者调用wait(delay)等待第一个产品,然后在缓冲区取得一个产品,将n减一,
缺陷是,在耗尽n时,delay值可能被多次减一,称为负值,因此消费者被迫等待生产者往缓冲区放置更多的产品

int n;
binary_semaphore s = 1,delay = 0;

void producer(){
    while(true){
        produce();
        wait(s);
            append; ++n;
            if(n == 1) signal(delay);        //如果上次缓冲区为空,那么意味着需要通知消费者可以尝试消费,解除消费者进程
        signal(s);
    }
}
void consumer(){
    int m;
    wait(delay);                        //最开始的时候缓冲区没有产品,必须等待,阻塞消费者进程
    while(true){
        wait(s);
            take(); --n;m = n;            //由于n值在过程中可能被修改,之后的过程中m值不能保证等于n
        signal(s);
        consume();
        if(m == 0) wait(delay);             //如果缓冲区产品被消耗尽,必须等待,阻塞消费者进程,但是当m=0时,n已经大于0了,消费者进程依旧wait被阻塞
    }
}

第二种做法:
使用计数信号量,但是设计一个正确的程序是很困难的。
如果wait(s)与wait(n)

semaphore n = 0,s = 1;        //s为二元信号量,标识缓冲区的使用,n为计数信号量,标识产品数量
void producer(){
    while(true){
        produce();

        wait(s);
            append();
        signal(s);

        signal(n);
    }
}
void consumer(){
    while(true){
        wait(n);

        wait(s);
            take();
        signal(s);

        consume();
    }
}

4.管程

管程是一个程序设计语言结构,它提供了信号量同样的功能,但是更易于控制,它允许程序员用管程锁定任何对象,对于链表可以用锁锁住整个链表,也可以是表中的某个元素。

4.1 使用信号的管程

管程是由一个或多个过程、一个初始化序列和局部数据组成的软件模块,特点:
1)局部数据变量只能被管程的过程访问,任何其他外部过程不能访问
2)一个进程通过调用管程的一个过程进入管程
3)在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用

由于其类似于面向对象的特点,所以面向对象操作系统或程序设计语言很容易实现。

管程提供了一种互斥机制,管程中的数据变量只能被一个进程访问到。
因此,可以把一个共享数据结构放入到管程中,从而提供对它的保护。
为进行并发处理,管程必须包含同步工具,如需要一种机制,使得该进程不仅被挂起,而且能释放管程,同时在恢复时能够重新获得管程。
管程通过条件变量支持同步,这些条件变量包含在管程中,并且只有在管程中才能被访问,只能使用wait(c)和signal(c)访问信号量。
wait(c):调用进程的执行在条件C上挂起,管程现在可被另一进程使用
signal(c):恢复执行在wait之后因为某些条件挂起的进程,如果有这样的进程选择一个,如果没有,什么也不做,丢弃信号

管程的结构:
假设只允许一个进程进入管程,那么其他试图进入管程的进程被阻塞并加入等待管程可用的进程队列中。
在管程中的进程可以通过wait()操作将自己阻塞在条件x上,随后它被放入等待条件改变以重新进入管程的进程队列中;
它也可以在发现条件改变时,通过signal操作通知相应的条件队列某条件已经发生改变。

生产者/消费者问题的管程解决方案:
要求signal操作只能作为管程过程中的最后一个操作

moniter boundedbuffer;            //缓冲区
char buffer[N];                //N为缓冲区size
int nextin,nextout;            //缓冲区指针
int count;                //缓冲区中数据个数
cond notfull,notempty;            //为同步设置的条件变量
void append(char x){
    if(count == N) wait(notfull);    //置notfull为假,缓冲区满
    buffer[nextin] = x;
    nextin= (nextin +1)%N;
    count++;
    signal(notempty);            //置notempyt为真,释放任何一个等待notempty的进程
}
void take(char x){
    if(count == 0) wait(notempty);    //置notempty为假,缓冲区为空
    x = buffer(nextout);
    nextout = (nextout +1)%N;
    count--;
    signal(notfull);            //置notfull为真,释放任何一个等待notfull的进程
}
注意:sigal操作的特殊性,如果没有在条件cond上等待的进程,那么就没有释放操作

void producer(){
    char x;
    while(true){
        produce(x);
        append(x);
    }
}
void consumer(){
    char x;
    while(ture){
        take(x);
        consume(x);
    }
}

void main(){
    parbegin(producer,consumer);
}

管程优于信号量之处:
所有的同步机制都被限制在管程内部,因此,不但易于验证同步的正确性,同时易于检测出错误。

4.2 使用通知和广播的管程

管程的定义要求在条件队列中至少有一个进程,当另一个进程为该条件产生signal时,该队列的进程立即运行,
因此,产生signal的进程必须立即退出管程,或者阻塞在管程上。

由此产生的缺陷:
1)如果产生signal的进程没有完成,那么需要发生额外的切换
2)与信号相关的进程调度必须非常可靠,立即激活并迅速进入管程

使用通知和广播的管程:
使用notify替代signal,执行notity(x)后,它使得x条件队列得到通知,但是发信号的进程继续运行(如果是signal则必须退出管程)。
通知的结果是使得位于相应条件队列头的进程在将来合适的时候且当处理器可用时被恢复执行。
这样就会产生一个等待的过程,这个过程中条件可能发生未知的改变,因此需要在等待过程中重新检查条件。
解决办法:使用while检查替代if检查,从而实现多次检查

void append(char x){
    while(count == N) wait(notfull);        //多次检查,缓冲区满
    buffer[nextin] = x;
    nextin = (nextin +1)%N;
    count++;
    notify(notempty);                //通知notempty等待队列
}
void take(char x){
    while(count == 0) wait(notempty);        //多次检查,缓冲区空
    x = buffer[nextout];
    nextout = (nextout + 1)%N;
    count--;
    notify(notfull);                //通知notfull等待队列
}
    
使用广播broadcast:
广播可以使所有在该条件上等待的进程都被设置为就绪状态,使用广播是因为此进程并不知道有多少进程会被激活

5.消息传递

为了实现合作,进程间需要交换信息,提供这些功能的一种方法是消息传递。
消息传递原语:
send(destination,message);
receive(source,message);

5.1 同步

发送方和接收方都可以阻塞或不阻塞,多种组合:
1)阻塞send,阻塞receive    进程间紧密同步
2)无阻塞send,阻塞receive    最有用的一种组合,允许一个进程可以给各个目标进程尽快地发送一条或多条信息
3)无阻塞send,无阻塞receive    不要求任何一方等待

无阻塞send最自然,但存在潜在危险,错误会导致进程重复发送消息,同时需要确认消息是否送到给程序员带来负担
阻塞receive最自然,存在潜在危险,如果进程已经接收一条消息,随后同类消息会被丢失

5.2 寻址

直接寻址:
send原语包含目标进程的标识符,而receive原语包含两种处理方式。
一种是要求进程显式指定源进程,一种是不可能指定期望的源进程

间接寻址:
消息不是直接发送给接收方,而是发送到一个共享数据结构,该结构由临时保存消息的队列组成,这些队列通常称为信箱mailbox
间接寻址通过解除发送方和接收方之间的耦合关系,在消息的使用上运行更大的灵活性。
发送方和接收方之间的关系可以是一对一、多对一、一对多或多对多。多对一的信箱通常称为一个端口。

5.3 消息格式
使用固定的消息头和可变长的消息主体组成的灵活消息格式

5.4 排队原则
最简单的排队原则:先进先出原则,可选的原则是允许指定消息的优先级
另一个选择是允许接收者检查消息队列并选择下一次接收哪个消息

5.5 互斥

假设使用阻塞receive和无阻塞send,一组并发进程共享一个信箱box,它可供所有进程发送和接收消息时使用,该信箱被初始化成一个无内容的信息。
希望进入临界区的进程首先试图接收一条消息,如果信箱为空,则该进程被阻塞,一旦进程获得消息,它执行它的临界区,然后把该消息放回信箱。
const int n = 进程数
void p(int i){
    message msg;
    while(true){
        receive(box,msg);
        进入临界区;
        send(box,msg);
        其他;
    }
}
void main(){
    create mailbox(box);
    send(box,null);
    parbegin(p(1),p(2),......p(n));
}
如果有一条消息,它仅仅被传递给一个进程,其他进程阻塞
如果消息队列为空,所有进程被阻塞,当一条消息可用时,只有一个阻塞进程被激活,并得到这个消息

使用消息传递解决生产者/消费者问题:
cosnt int capacity = 缓冲区容量
        null = 空消息
int i;
void producer(){
    message pmsg;
    while(true){
        receive(mayproduce,pmsg);
        msg = produce();
        send(mayconsume,pmsg);
    }
}
void consumer(){
    message cmsg;
    while(true){
        receive(mayconsume,cmsg);
        consume(cmsg);
        send(mayproduce,null);
    }
}
void main(){
    create_mailbox(mayproduce);
    create_mailbox(mayconsume);
    for(i = 1;i <= capacity;++i){
        send(mayproduce,null);
    }
    parbegin(producer,consumer);
}

6.读者-写者问题

定义:在一个多个进程共享的数据区,有一些进程只能读取这个数据区的数据,而另一些进程只往数据区写数据,满足一下条件:
1)任意多的读进程可以同时读这个文件
2)一次只有一个写进程可以写文件
3)如果一个写进程正在写文件,禁止任何读进程读文件
读进程是不需要排斥其它读进程,但是写进程是需要排斥其它所有进程

一般互斥问题:
允许任何进程读写数据区,此时,可以把该进程中访问数据区的部分声明为一个临界区,方法低效速度过慢

两种解决办法:

1)读者优先
int readcount;            //记录读进程数目
semaphore x=1,wsem =1;        //x确保readcount被更新,wsem写信号量
void reader(){
    while(true){
        wait(x);
        readcount++;
        if(readcount == 1){        //如果已经有读者,那么不能进行写操作
            wait(wsem);        
        }
        signal(x);

        readunit();

        wait(x);
        readcount--;
        if(readcount== 0){        //没有任何读者了,可以开始写操作
            signal(wsem);
        }
        signal(x);
    }
}
使用x,在wait(x)和signal(x)之间才能进行readcount的更新,所以需要两个由wait(x)和signal(x)包含的块。
这样的readcount也成了互斥资源,一次只能由一个进程进行更新。

void writer(){
    while(true){
        wait(wsem);        //一次能有一个进程进行写操作
        writeunit();
        signal(wsem);
    }
}
void main(){
    readcount = 0;
    parbegin(reader,writer);
}

2)写者优先

读者优先的缺点:只要有一个读进程,就会给其它读进程保留数据区的控制权,容易造成写进程一直饥饿
解决方案是:
如果一读进程想要写时,就不再允许新的读进程访问该数据区。对于写进程,在已经有的定义基础上还要增加下列信号量和变量:
信号量rsem,当至少有一个写进程准备访问数据区的时候,用于禁止所有的读进程
变量writecount,控制rsem的设置
信号y:控制writecount更新

int readcount , writecount;
semphore x =1,y =1,z =1,wsem = 1; rsem =1;        
//在rsem上不能排长队,否则写进程不能跳过这个队列,只允许一个读进程在rsem上排队,而其他读进程在等待rsem之前,在信号量z上排队
//x y 都用于控制变量的互斥读写,即我在更新,其他人暂时没有使用权
//wsem rsem标识读写权利,由于一次只能一个写进程,wsem不需要队列,而可以多个同时读,需要队列
//z是出于需要额外队列的考虑增加的新的信号量
void reader(){
    while(true){
        wait(z);                    //进程放置于z队列,其它进程暂时不能使用队列(我在试图插入队列,其它需要等待)
            wait(rsem);                //进程放置于rsem队列,
                wait(x);            //x用于控制readcount的更新
                readcount++;
                if(readcount == 1){        //存在读进程,阻塞写进程,一个写进程可以开始,wsem不需要队列
                    wait(wsem);
                }
                signal(x);
            signal(rsem);
        signal(z);

        readunit();

        wait(x);
            readcount--;
            if(readcount == 0) signal(wsem);    //没有了读进程,通知wsem可以开始写进程
        signal(x);
    }
}
void writer(){
    while(true){
        wait(y);                    //y用于控制writecount的更新
            writecount++;
            if(writecount == 1) wait(rsem);    //如果第一个写进程开始,不能再在rsem上排队,但是可以在z上排队
        signal(y);

        wait(wsem);                    //wsem控制写操作,由于只能运行写进程,所以没有队列
        writeuint();
        signal(wsem);

        wait(y);
            writecount--;
            if(writecoun == 0) signal(rsem);    //写进程结束,通知rsem队列一个进程可以排队
        signal(y);
    }
}
void main(){
    readcount = writecount = 0;
    parbegin(reader,writer);
}

使用消息机制方案:

有一个访问共享数据区的控制进程controller,其他想访问这个数据区的进程给控制进程发送请求消息,如果同意访问,则会收到一个应答消息”ok“,并通过一个”finished“表示访问完成。
控制进程设备备有三个信箱,每个信箱存放一种它接收到的消息。
为了赋予写进程优先权,控制进程先服务于写请求信息,后服务于读请求信息。
void reader(int i){
    message rmsg;                //request message
    while(true){
        rmsg = i;
        send(readrequest,rmsg);
        receive(mailbox[i],rmsg);
        readunit();
        rmsg = i;
        send(finished,rmsg);
    }
}
void writer(int i){
    message rmsg;
    while(true){
        rmsg = i;
        send(writerequest,rmsg);
        receive(mailbox[i],rmsg);
        writeunit();
        rmsg = i;
        send(finished,rmsg);
    }
}
void controller(){
    while(true){
        if(count > 0){                //count被初始化为100,表示可用进程数目
            if(!empty(finished)){        //首先服务所finished,如果完成可用进程++
                receive(finished,msg);
                count++;
            }
            else if(!empty(writerequest)){    //然后服务写请求,优先于读请求
                received(writerequest,msg);
                writer_id = msg.id;
                count = count - 100;        //将count置为负数,负值绝对值表示正在读的进程数
            }
            else(!empty(readrequest)){        //在已经有读进程的情况下,继续加入读进程
                receive(readrequest,msg);
                count--;            //加入一个读进程,每次可用进程--
                send(msg.id,"ok");        //向读进程发出消息,表示可以加入读了
            }
        }
        if(count == 0){                //表示唯一没有解决的请求就是写请求了
            send(writer_id,"ok");        //告诉读进程可以读了
            receive(finished,msg);        //等待读完,没有任何活动的进程了,重置count为初始值100
            count = 100;
        }
        while(count < 0){                //已经有写进程提出请求了,count++不停地累加,表示活动进程不停地完成了,直到0,表示就可以写操作了
            receive(finished,msg);
            count++;
        }
    }
}

小结:
    现代操作系统的核心是多道程序设计、多处理器和分布式处理器,这些方案的基础以及操作系统设计技术的基础是并发。
    当多个程序并发执行时,无论是多处理器系统,还是单处理器多道程序系统,都会产生冲突和合作的问题。

    并发进程可以按照多种方式交互,进程之间由于共享访问一个公共对象,产生重要问题:互斥和死锁。
    互斥是一组并发进程,一次只有一个进程能够访问给定的资源或执行给定的功能。

    互斥的支持
    
    硬件的支持
    1)中断禁用
        单处理器临界区中断禁用
    2)专用机器指令
        在硬件级别上,对存储单元的访问排斥对相同单元的其它访问,但是效率较低
    软件支持:
    3)信号量和消息机制

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值