[读书笔记]并发和竞态(第五章)

综述

并发问题是编程中经常遇到的难题,我们需要学会针对并发产生的竞态进行编程

一、信号量和互斥体

Linux上信号量的实现

1.信号量:本质上是一个整数值,它和一对函数联合使用,这对函数通常称为P和V,希望进入临界区的进程将在相关信号量上调用P,如果信号量大于0,该值减1,进程可以进入临界区代码运行;相反,如果信号量的值<=0,进程必须等待其他人释放该信号量。解锁信号量调用V函数,该函数增加信号量的值,并在必要时唤醒等待的进程。

当信号量用于互斥时(即避免多个进程同时在一个临界区中运行)
信号量应初始化为1
这种信号量在任何时刻只能由单个进程或者线程拥有,这种模式下,
信号量也成为互斥体
2.声明和初始化
void sema_init(strcut semaphore *sem,int val);
sem代表信号量,val代表初始值,一般初始化为1.
DECLARE_MUTEX(name):一个名称为name的信号量被初始化为1
DECLARE_MUTEX_LOCKED(name):一个名称为name的信号量被初始化为0

void init_MUTEX(stuct semaphore *sem):
void init_MUTEX_LOCKED(stuct semaphore *sem):
LINUX2.6版本后已经被遗弃 无法使用

Linux中的P函数是down(),该函数会减少信号量的值,必要时会一直等待。
void down(struct semaphore *sem)
减少信号量的值 并在必要时一直等待,操作不可中断
void down_interruptible(struct semaphore *sem)
完成和down相同的工作,但是操作是可中断,通常推荐使用,如果操作被中断,该函数返回非零值,而调用者不会拥有该信号,使用时注意检查返回值,并且做出相应的操作。
int down_trylock(struct semaphore *sem)
永远不会休眠,如果信号量在调用时不可获得,该函数会立即返回一个非零值。

当一个函数成功调用上述的down函数,就称为该线程拥有(获得、拿到)了该信号量,这样该线程就被赋予访问由该信号量保护的临界区的权利,当互斥操作完成后,必须返回该信号量,即调用V函数

void up(strcut semaphore *sem);
调用后,调用者不在拥有该信号量。

使用信号量实例

步骤1:定义信号量
在自己的定义的结构体加入semaphore *sem;//互斥信号量
步骤2:初始化信号量
sema_init(sem,1);
默认初始化信号量sem的值为1
步骤3:
在要保护的资源调用dowm_interruptible();
if(dowm_interruptible(&sem))
retrun -ERESTARTSYS;
//这里是要保护资源的代码
out:
up(&sem);
在函数调用up最后释放信号量

strcut hello_dev{
  int val;
  semaphore *sem;//互斥信号量
}
static hello_init()
{
//初始化信号量
sema_init(dev->sem,1);
}
/*读取寄存器设备 val的值*/
static ssize_t hello_read(struct file *filp,char __user *buf,
size_t count,loff_t *f_ops) {
    ssize_t err = 0;
    struct hello_dev *dev = filp->private_data;

    /*同步访问*/
    if(down_interruptible(&(dev->sem)));
        return -ERESTARTSYS;
    
    if(count < sizeof(dev->val)){
        goto out;
    }
    /*将寄存器val的值拷贝到用户提供的缓存区*/
    if(copy_to_user(buf,&(dev->val),sizeof(dev->val))){
        err = -EFAULT;
        goto out;
    }

    out:
    up(&(dev->sem));
    return err;
}

读取者/写入者信号量

许多任务可分为:
1.只需要读取受保护的数据(多个进程和线程可以同时并发访问)
2.写入受保护的数据
为此,Linux提供了特殊的信号量"rwsem"(或者reader/writer semaphore),rwsem使用很少,偶尔有用

相关定义包含在<linux/rwsem.h>头文件中
struct rw_semaphore *sem;
初始化:
void init_rwsem(struct rw_semaphore *sem);

对于只读访问,可用接口如下:
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);;
void up_read(struct rw_semaphore *sem);
对于写入访问,可用接口如下:
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
downgrade_write允许其他读取者访问

完成量

completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成了,包含在<linux/completion.h>中
1.创建和初始化completion
DECLARE_COMPLETION(my_com);
或者动态创建和初始化
strcut completion my_com;
inti_completion(&my_com);
2.等待completion
void wait_for_completion(strcut completion *c);
注意,该函数执行一个非中断等待,如果代码调用了该函数且没人能完成该信号量,则会产生一个不可杀进程!
3.唤醒completion
void completion(struct completion *c);
void complete_all(struct completion *c);
快速重新初始化某个复用的completion
INIT_COMPLETION(struct completion c);

自旋锁

信号量在互斥中是非常有用的工具,内核还提供另一种工具--自旋锁。
和信号量不同,自旋锁可以在休眠的代码中使用,如中断处理例程,正确使用的情况下,自旋锁性能比信号量好!

自旋锁API

初始化
自旋锁相关定义包含在头文件<linux/spinlock.h>中
编译时初始化
strcuvt spinlock_t my_lock;
my_lock = SPIN_LOCK_UNLOCKED;
或者
运行时初始化
void spin_lock_init(spinlock_t *lock);
进入临界区之前,必须调用以下函数获取锁
void spin_lock(spinlock_t *lock);
注意:所以自旋锁本质上都是不可中断的,一旦调用了spin_lock,在获取锁之前一直处于自旋状态

释放锁函数
void spin_unlock(spinlock_t *lock);

注意:为了避免
在中断例程自旋时,非中断代码将没有机会释放这种个自旋锁,导致处理器将永远自旋下去的情况
我们需要在拥有自旋锁时禁止中断(仅本地CPU上),下面的函数可以实现用于禁止中断的自旋锁函数。
另一个重要原则:自旋转必须的在尽可能短的时间内拥有!

自旋锁函数
void spin_lock(spinlock_t *lock):允许中断的自旋锁
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags)
在获得自旋锁之前禁止中断(只在本地cup上),先前中断报错在flags中
void spin_lock_irq(spinlock_t *lock)
如果我们能确保没有任何其他代码禁止本地处理器的中断,则可以使用spin_lock_irq,而无需跟踪标志!
void spin_lock_bh(spinlock_t *lock)
在获得锁之前禁止软件中断,允许硬件中断打开
该函数可以安全的避免死锁问题,还能服务硬件中断
释放锁函数

void spin_unlock(spinlock_t *lock):
void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags)

void spin_unlock_irq(spinlock_t *lock)
void spin_unlock_bh(spinlock_t *lock)

非阻塞式自旋锁
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
两个函数在成功获取自旋锁时,返回非零值,失败时返回零,对应禁止中断的情况没有对应的try版本
读取者/写入者自旋锁
和rwsem信号量很相似,但是可能会造成读取者饥饿,导致性能变低!

注意防止死锁情况
死锁1:当某个获得锁的函数要调用其他同样试图获取这个锁的函数,代码就会死锁,无论是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁

死锁2:线程1拥有锁1,线程2拥有锁2,这时候,当这两个线程都试图获取另外线程的的锁时,这两个线程将处于死锁状态
最好的办法是避免同时需要多个锁的情况

锁之外的办法

1.免锁算法

常用于免锁算法的生产者/消费者任务的数据结构就是循环缓冲区!

2.原子变量

共享资源是一个简单的整数型时,内核提供了一种原子的整数类型
称为atomic_t 定义在<asm/atomic.h>中
初始化
void atomic_set(atomic *v,int t);
或者
atomic_t v = ATOMIC_INIT(0);初始化为0
还有读写函数,运算操作函数,位操作函数就不一一列举了

3.seqlock

当要保护的资源很小、很简单、会被频繁读取访问且写入访问很少发生且必须快速时,就可以使用内核提供的seqlock
允许读取者自由访问,但是需要读取者检测是否和写入者冲突
seqlock通常不能包含在含有指针的数据结构中,因为在写入者修改数据结构的同时,读取者可能会追随一个无效的指针。

seqlock定义在<linux/seqlock.h>
初始化方法2种
1.seqlock_t lock1 = SEQLOCK_UNLOCKED;
2.seqlock_t lock2;
seqlock_init(&lock2);

读取时会访问通过一个(无符号的)整数顺序值而进入临界区,在退出时,该顺序值和当前值比较,如果不相等,必须重试读取访问。

unsigned int seq;
do {
  seq = read_seqbegin(&the_lock);
  /*完成需要做的工作*/
}while read_seqretry(&the_lock,seq);

如果在中断处理例程中使用seqlock,则应该使用IRQ安全的版本

unsigned int read_seqbegin_irqsave(seqlock_t *lock,unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,
                                                        unsigned long flags);

写入者必须进入由seqlock包含的临界区时获得一个互斥锁,因此要调用以下函数:
void write_sequnlock(seqlock_t *lock);
还有其他常见自旋锁的变种函数

void write_seqlock_irqsave(seqlock_t *lock,unsigned long flags);
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);

void write_sequnlock_irqrestore(seqlock_t *lock,unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);

另外
如果write_tryseqlock()可以获得自旋锁,它也会返回非0值。

读取-复制-更新(read-copy-update,RCU)

RCU是一种高级的互斥机制,正确使用下,也可获得很高的性能,但很少在驱动程序中使用。
RCU针对经常发生读取而很少写入的情形做了优化,被保护的资源应通过指针访问,而对这些资源的引用必须由原子代码拥有!

#include <linux/rcupdate.h>
使用读取-复制-更新(RCU)机制是需要包含的头文件
void rcu_read_lock();
void rcu_read_unlock();
获取对受RCU保护资源的读取访问的宏
void call_rcu(srcut rcu_head head,void (func)(void *arg),void *arg);
准备用于安全示范受RCU保护的资源的回调函数,该函数将在所有的处理器被调度后运行!

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

推荐阅读更多精彩内容