进(线)程间通信-各种锁实现

进程间通信

本文你会了解到计算机系统中一些锁的实现原理.文中进程和线程可以互相替换.

竞争条件

概念:两个或者多个进程(或线程)共享读写某块资源的时候,因为cpu的切换,进程的运行时序会发生变化,从而可能影响最后的读写结果.

举例:
有两台打印机A,B,需要对一个文件内的文件进行打印.假如打印到第5个文件之前都没发生错误,此时第5个文件首先被A标记为了要打印.这时候CPU发生切换,进程切换到了B,B此时也认为文件5可以打印,并且将下一个打印对象标记为文件6.此时A从中断过程中恢复了,于是将要打印的文件6覆盖为了文件5,然后继续执行.等B恢复了,之后发现之前标记文件6不见了,变成了文件5,那么这时候就出现了竞争问题.

临界区

临界区概念 : 对共享内存进行访问的程序片段.解决竞争的方式就是实现互斥,让两个进程不同时在临界区.

有四个条件:

  • 任何时候两个进程不能同时处于临界区.
  • 不应对CPU的数量和速度做任何假设.
  • 临界区外运行的程序不能阻塞其他进程.
  • 不得使进程无限期等待进入临界区
基于忙等待的互斥

1.忙等待

概念 : CPU在发生时钟中断或者其他中断的时候才会切换进程,可以利用这一点当一个进程在访问共享文件时,将中断屏蔽掉,等当前进程执行完毕之后再回复中断,这样就可以避免两个线程同处临界区了.

缺点 :

  • 可能造成无限等待,万一某个进程不打开中断,后果可能会很严重.
  • 如果是多核CPU,那么当前CPU屏蔽了中断,但其他CPU可能没屏蔽,那么依然可能会造成竞争条件.

2.锁变量

概念 : 设想有一个共享的锁变量,其初始值为0;当一个进程想进入临界区的时候,首先判断这把锁的值,如果是0就表明可以进入,并设置变量为1;如果当前变量是1,就等待变量为0.0就表示可以进入临界区,1表示需要等待不能进入.

缺点 : 如果进程A将锁变量设置为1前,发生了中断,B进程切换过来,发现当前变量为0,此时A,B都进入了临界区,也会产生竞争条件.

3.严格轮换法

概念 : 通过一个整形变量turn来标记当前哪个线程进入了临界区.开始时,进程0检查turn,发现为0,进入临界区.此时进程1也发现turn为0,于是不停的循环检查turn什么时候变为1.这个循环过程就是忙等待(比较浪费CPU时间,等待时间较短的情况下使用,又叫自旋锁).

缺点 : 当进程0是一个快速操作,而进程1是一个耗时操作时,如果进程0快速执行完操作后,将turn置为1,此时进程1依然在临界区外执行其他操作,那么这时候进程0就无法进入临界区了,直到进程1进入临界区并执行完毕.此种情况违背了上述情况3,临界区外阻塞了其他进程.

4.Peterson解法

概念 : 在使用进入临界区前,各个进程都使用其ID(0或1)作为参数来调用enter_region()这个函数.该函数在需要时将使进程等待,直到能安全进入临界区.进入临界区的进程完成临界区内的操作后,调用leave_region()这个函数表示操作完成,其他进程可进入临界区.

算法原型如下

#define FALSE 0
#define TRUE 1
#define N 2

int turn; // 该谁进入临界区
int interested[N]; // 保存当前进程是否要进入临界区的状态
void enter_region(int process){
    int other; // 其他进程
    other = 1 - process; // 其他进程id
    interested[process] = TRUE; // 当前进程要进入临界区
    turn = process; // 当前的轮次为process
    while(turn == process && interested[other] == TRUE); // 如果是其    他进程进入了,就空循环等待
}
// 直到调用了leave_region()方法,下一个进程才能进入
void leave_region(int process){
    interested[process] = FALSE;
}

分析:如果进程0先进入了enter_region()函数,那么运行到while的时候,此时

process = 0;
other = 1;
interested[process] = interested[0] = TRUE;
turn = process;
interested[other] = interested[1] = FALSE;

这里进程0,不进入空循环,于是进入了临界区开始工作.
此时进程1也进入了enter_region()函数,那么此时

process = 1;
other = 0;
interested[process] = interested[1] = TRUE;
turn = process;
interested[other] = interested[0] = TRUE;

这里interested[other] = interested[0] = TRUE,所以进入了空循环,那么进程1就在这里进行挂起,直到进程0调用leave_region()函数之后,才会进入临界区.

5.TSL指令

概念 : 这是需要硬件支持的一种方案.利用到汇编的一些指令:TSL RX,LOCK.它将一个内存字lock读到寄存器RX中,然后在该内存地址上存一个非零值.读和写必须是一体的,该指令结束前其他CPU不允许访问该lock.执行TSL指令的CPU将把内存总线锁住,从而禁止其他CPU在指令结束前访问内存.

这里屏蔽中断和锁总线是有区别的

  • 当前CPU屏蔽中断之后,总线上其他CPU也可以访问临界区,中断屏蔽之后对其他CPU没有任何影响.
  • 当前CPU锁住总线之后,其他CPU是没有办法在当前总线进行调度的,直到解锁.
enter_region:
TSL REGISTER,LOCK // 给lock变量赋值为1
CMP REGISTER,#0 // 判断lock是否为0
JNE enter_region // 不是 0就是说明有锁,继续等待循环
RET // 返回调用者,进入了临界区

leave_region:
MOVE LOCK,#0 // lock设为0
RET // 返回调用者

思路和忙等待差不多,只不过是通过硬件支持锁了总线而非屏蔽中断.

睡眠与唤醒

Peterson和TSL算法都是正确的思路,但都是基于忙等待的.本质就是:当一个进程想进入临界区的时候,先检查是否允许进入,不允许就让进程等待,直到允许为止.可以想象,如果等待时间较长,那么很容易浪费CPU时间,并且还有优先级反转问题.

1.优先级反转问题
很简单的例子,有两条进程:HIGH和LOW,HIGH优先级高于LOW.当LOW进入临界区的时候,HIGH此时也想进入,于是进行忙等,如果LOW无法离开临界区,此时HIGH就在一直等待.

2.生产者-消费者问题
先明白两个概念:
sleep : 将一个引起系统阻塞的进程挂起,直到另一个进程将其唤醒.
wakeup : 唤醒某个进程.

生产者和消费者的概念:
假定在一个工厂内,有生产者,消费者,和一个仓库.

  • 生产者 : 生产产品然后放入仓库.
  • 消费者 : 从仓库内拿出产品.
  • 仓库 : 拥有一个容积的标识:count,标识产品数量.

如果当前仓库产品数量已满,那么此时生产者发现仓库满了,于是进入睡眠状态.此时消费者在睡眠,被唤醒后,判断仓库容积是否为0,如果为0,就进入睡眠,不为0就拿出产品.如此反复.C语言代码原型如下:

#define N 100 // 仓库容量

int count = 0; // 当前产品数量
// 生产者函数
void producer(){
    int item; // 新的产品
    while(TRUE){
        item = produce_item(); // 生产新产品
        if (count == N) sleep(); // 仓库满了,就睡眠
        insert_item(item); // 未满,假如仓库
        count += 1; // 数量加一
        if (count == 1) wakeup(consumer); // 仓库有货了,唤醒消费者
   }
}
// 消费者函数
void consumer(){
    int item; // 消费的产品
    while(TRUE){
        if (count == 0) sleep(); // 仓库空了,睡眠
        item = remove_item(); // 要消费的产品
        count -= 1; // 数量减一
        if (count == N - 1) wakeup(producer); // 仓库不满了,唤醒生产者
        consume_item(item); // 消费货物
    }
}

这里由于没有对count加以限制,也可能会产生竞争条件.

  • 如果仓库为空,消费者刚读取到的仓库信息为0.
  • 此时程序发生了中断,暂停消费者并且启动生产者.生产者向仓库添加了一个产品,此时count为1.
  • 这时候生产者判断刚才count为0,所以消费者一定在睡眠,于是调用wakeup()来唤醒消费者.
  • 但此时消费者因为发生了中断,实际上并没有进入睡眠状态中,于是之前生产者发送的wakeup()信号会丢失掉.
  • 当中断结束后,系统重新调度消费者时,消费者之前读到的count为0,所以立即进入睡眠中.
  • 而生产者迟早会将仓库填满,进入睡眠,于是两个进程都会进入到睡眠中.

如何解决这种情况呢,比较容易想到的就是给还未睡眠的进程加一个标识status,又叫唤醒等待位.

  • wakeup()信号发给一个清醒的进程A时,将status置为1.
  • 如果进程A此时要进入睡眠状态,并且status为1,则将该status置为0.这里实际上是把wakeup()信号保存在了status里,从而让它不丢失.此时消费者因为有这个status的标识,就知道自己收到了一个唤醒信号,所以也不会进入睡眠.

问题似乎解决了,但这种情况下又会产生另外一个问题,如果有多个进程的话,可能就需要多个唤醒等待位了.很明显,这种方式缺少灵活性.

信号量(semaphore)

概念 : 使用一个整型的变量来累计唤醒次数,供以后使用.

对信号量有两个操作:downup,通常来说对应sleepwakeup.
先明白原子操作的概念:指一组相关联的操作要么都不简短地执行,要么都不执行

  • down : 检查信号量的值是否大于0,若大于0,就将其值减1(用掉一个保存的唤醒信号).若值为0了,进程就将进入睡眠,此时down操作并未结束.整个过程是一个原子操作过程,信号量操作开始之后到完成前,其他进程是无法访问这个信号量的.
  • up : 对信号量的值加1.如果一个或多个进程想进入临界区,因为该信号量值为0,所以这些进程都将被阻塞.等当前临界区的进程完成临界区的操作之后,会执行up操作,信号量的值加1,同时唤醒某个进程进入临界区(唤醒的进程是系统进行调度选择的一个更为重要的进程).这个过程也是原子操作.

信号量与忙等的区别在于:信号量执行的只需要几个毫秒,而忙等待可能需要的时间会更长

互斥量(mutex)

概念 : 互斥量是一个可以处于两态之一的变量:加锁和解锁.通常0表示解锁,其他值表示加锁.一般用于处理线程操作.

调用过程:

  • 当一个线程需要访问临界区的时候,调用mutex_lock.如果该互斥量是0,则线程允许进入临界区.
  • 如果该互斥量已经加锁,则要进入临界区的线程被阻塞,直到临界区的线程完成并调用mutex_unlock.此时如果遇到忙等,导致时钟超时,那么系统将随机选择一个线程获得锁,从而保证锁迟早能被释放接触.

互斥量和TSL操作的区别就在于:如果发生忙等,导致时钟超时后,互斥量算法中会调度另一个线程(thread_yield)获得锁,从而避免了忙等.iOS中的pthread_mutex就是利用互斥量实现的.

后面还有消息发送屏障(barrier)两种方式,暂时不介绍了,有兴趣的可以自行去了解.

参考来源:计算机组成原理第三版

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容

  • 文/tangsl(简书作者) 原文链接:http://www.jianshu.com/p/2b993a4b913e...
    西葫芦炒胖子阅读 3,684评论 0 5
  • 又来到了一个老生常谈的问题,应用层软件开发的程序员要不要了解和深入学习操作系统呢? 今天就这个问题开始,来谈谈操...
    tangsl阅读 4,004评论 0 23
  • 一、 【例3-1-4】在操作系统中,要对并发进程进行同步的原因是 。 A. 进程必须在有限的时间内完成 B. 进程...
    ZoeyeoZ阅读 4,453评论 0 9
  • 1. 操作系统的资源管理技术 资源管理解决物理资源数量不足和合理分配资源这两个问题。 操作系统虚拟机为用户提供了一...
    joyeyoung阅读 10,637评论 1 5
  • 我们在镜子面前可以肆无忌惮的笑 可以肆无忌惮的哭 镜子可以看见另一个我 一个不用在哪里伪心的笑的我 不用伪装成很开...
    水下月1阅读 196评论 2 0