linux内核并发和竞态学习笔记

以前看了书,总是疏于记笔记,若干天后就忘差不不了。为了避免再犯以前的错误,今天把看完了就马上梳理一下放到这儿。

linux内核开发中,多线程并发的管理毫无疑问是重中之重;不可避免地,并发的相关缺陷也是最容易制造的,也是最难找的。即使是Linux内核专家也会偶尔制造并发相关的缺陷。为此,我们有必要好好学习和理解内核对并发的管理。

并发和竞态:

并发是多个执行单元被同时执行。竞态通常是作为对资源的共享访问而产生,竞态会导致对共享数据的非控制访问。并发是因,竞态是果。在现在Linux系统中存在大量的并发来源,因此会导致可能的竞态。多个正在运行的用户态程序并发访问我们的代码,SMP在多个处理上同时执行我们的代码,内核代码是可抢占的,中断处理的异步执行,内核提供的软中断机制,这些都可能导致竞态的产生。

为了尽可能避免竞态的发生,需要记住几个原则:

一、只要可能,就应该尽可能避免资源的共享(避开原则)。如果没有并发的访问,也就不会有竞态的产生。比如,尽可能避免使用全局变量。

二、资源共享不可避免情况下,有个资源共享的硬规则(加锁保护原则):在单个线程之外共享硬件和软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须显式地管理对资源的访问。对共享资源管理的常见技术称为“锁定”或者“互斥”——确保一次只有一个执行线程可操作共享资源。比如,办公室中两个同事都要使用网络打印机打印材料,但彼此都不知道对方也要用打印机,不做任何资源保护势必就会出问题。因此,需要使用互斥锁来保护对打印机的共享访问。

三、共享对象的使用原则(引用计数保护原则):当内核创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。可以这样理解:在对象上不能正确工作时,不能将其对内核可用。对内核可用之后,对其必须得到跟踪。对共享对象的跟踪常用的技术是引用计数。

刚开始写内核程序时,公司大牛就对我说,写好内核程序的关键是使用好锁和引用计数。读到这里,顿时有种大彻大悟。

信号量和互斥体

先解释几个概念

临界区:在任意给定的时刻,代码只能被一个线程执行。

“进入休眠”:当一个Linix进程到达某个时间点,此时它不能进行任何处理时,它将进入休眠(或“阻塞”)状态,这将把处理器让给其他执行线程直到将来它能继续完成自己的处理为止。需要注意:等待获取信号量时会导致线程”进入睡眠“;在拥有信号量时,线程也允许睡眠。

正确使用锁定机制的关键是,明确指定需要保护的资源,并确保每一个对这些资源的访问都使用正确的锁定。

信号量和互斥体,读取者/写入者信号量几乎无人不晓,所以也就不多写了。

completion

在任务完成时进行通信使用信号量性能将会受到影响,2.4.7版内核引入了”completion(完成)"接口。
completion是一种轻量级的机制,它允许一个线程告诉另一个线程工作已经完成。
为了使用completion,代码必须包含<linux/completion.h>。可以利用下面接口创建completion:
DECLARE_COMPLETION(my_completion);
或者,如果必须动态地创建和初始化completion,则使用下面的方法:
struct completion my_completion;
/* ... */
init_completion(&my_completion);
要等待completion,可进行如下调用:
void wait_for_completion(struct completion *c);
注意,该函数执行一个非中断的等待。如果代码调用了wait_for_completion且没有人会完成该任务,则将产生一个不可杀的进程。
另一方面,实际的completion事件可通过调用下面的函数之一来触发:
void complete(struct completion *c);
void complete_all(struct completion *c);
这两个函数在是否有多个线程在等待相同的completion事件上有所不同。compelete只会唤醒一个等待线程,而complete_all允许唤醒所有等待线程。在大多数情况下,只会有有一个等待者,因此这两个函数产生相同的效果。

自旋锁

“自旋锁(spinlock)” 在概念上非常简单。一个自旋锁就是一个设备,它只能有两个值:“锁定”和“解锁”。如果锁可用则“锁定”,代码继续进入临界区;相反,如果锁被其他人获得,则代码进入忙循环并重复检查这个锁,直到该锁可用为止。这个循环就是自旋锁的“自旋”部分。和信号量不同,自旋锁可在不能睡眠的代码中使用,比如中断处理例程。在正确使用的情况下,自旋锁通常可以提供比信号量更高的性能。
自旋锁的使用的两个原则:
一、任何拥有自旋锁的代码必须是原子的。它不能睡眠,事实上,它不能因为任何原因放弃处理器,除了服务中断之外(某些情况下此时也不能放弃处理器)。
涉及以下几种情况:内核抢占,避免休眠,异步中断处理。
内核抢占的情况由自旋锁代码本身处理。任何时候,只要内核拥有自旋锁,在相关处理器上的抢占就会被禁止。甚至单处理器系统上,也必须以同样的方式禁止抢占以避免竞态。
在拥有自旋锁的时候避免休眠很难做到;许多内核函数可以休眠,而且此行为也始终没有文档来很好地记录。比如,kmalloc等需要分配内存的函数均可能睡眠。因此,在编写需要在自旋锁下执行的代码时,必须注意每一个调用的函数。
在有些情形下,我们在拥有自旋锁时需要禁止中断(仅在本地CPU上)。用于禁止中断的自旋锁有许多变种。
二、自旋锁必须在可能的最短时间内拥有,时间越短越好。

自旋锁API

要使用自旋锁原语,需要包含头文件<linux/spinlock.h>。

spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /* 编译时初始化spinlock*/
void spin_lock_init(spinlock_t *lock); /* 运行时初始化spinlock*/

/* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/
void spin_lock(spinlock_t *lock); /* 获得spinlock*/
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); /* 获得spinlock,禁止本地cpu中断,保存中断标志于flags*/
void spin_lock_irq(spinlock_t *lock); /* 获得spinlock,禁止本地cpu中断*/
void spin_lock_bh(spinlock_t *lock) /* 获得spinlock,禁止软件中断,保持硬件中断打开*/
 
/* 以下是对应的锁释放函数*/
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);

OK,今天就写这么多。自旋锁的使用心得,以及锁的使用原则学习随后再总结。



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值