原子操作
原子操作是其它同步方法的基石。
内核提供了两组原子操作接口,分别是针对整数和针对单独的位的操作。
针对整数的原子操作只能对atomic_t类型的数据进行处理(include\linux\types.h)。
typedef struct {
volatile int counter;
} atomic_t;
还有个64位的版本,不过差不多。
针对位的原子操作没有特殊的数据类型。它接受两个参数,一个内存地址指针,一个位号。
位原子操作函数:
还有非原子位操作函数,它们的函数名是在上表函数名的前面增加__。
整数原子操作函数:
自旋锁
Linux内核中最常见的锁是自旋锁。
一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,这会浪费处理器时间。
这导致自旋转最好用在短时间、轻量级的情况。
自旋锁的实现与体系密切相关。一个例子:
static DEFINE_SPINLOCK(i8259_irq_lock);
inline void
i8259a_enable_irq(unsigned int irq)
{
spin_lock(&i8259_irq_lock);
i8259_update_irq_hw(irq, cached_irq_mask &= ~(1 << irq));
spin_unlock(&i8259_irq_lock);
}
单处理器机器上,编译的时候不会加入自旋锁。只有设置了内核抢占机制时才会考虑是否开启自旋转编译选项。
自旋锁是不可递归的,持有自旋锁之后就不能在等待自旋锁了,否则就死锁了。
自旋锁可以用在中断处理程序中,但是在获取锁之前一定要禁止本地中断,否则,中断处理程序就会打断正持有锁的内核代码,有可能会视图去争用这个已经被持有的自旋锁。
内核提供了禁止中断同时请求锁的函数:
static long
iommu_arena_alloc(struct device *dev, struct pci_iommu_arena *arena, long n,
unsigned int align)
{
unsigned long flags;
unsigned long *ptes;
long i, p, mask;
spin_lock_irqsave(&arena->lock, flags);
/* 中间略 */
spin_unlock_irqrestore(&arena->lock, flags);
return p;
}
当下半部与进程上下文共享数据时,必须对进程上下文中断的共享数据进行保护,所以需要加锁的同时还要禁止下半部的执行。
当中断处理程序和下半部共享数据,那么久必须要在获取恰当的锁的同时还要禁止中断。
有时锁的用途可以明确分为读取和写入两个场景。测试可以使用读-写自旋锁。它在某些情况下是对自旋锁的优化。
持有自旋锁时不允许睡眠。
自旋锁操作函数:
信号量
如果加锁时间不长并且代码不会睡眠,利用自旋锁是最佳选择;如果加锁时间可能很长或者在代码持有锁时有可能进入睡眠,那么最好使用信号量来完成加锁工作。
信号量是一种睡眠锁。
信号量比自旋锁有更大的开销。
可以在持有信号量时去睡眠。
占有信号量的同时不能占有自旋锁,因为前者可以睡眠,而后者不可以。
持有信号量的代码可以被抢占。
信号量可以同时允许任意数量的锁持有者,如果最多一个,则叫做互斥信号量,如果多于一个,则叫做计数信号量。
通常使用互斥信号量。
信号量有两个原子操作(include\linux\semaphore.h):
extern void down(struct semaphore *sem);
extern void up(struct semaphore *sem);
down()操作通过对信号量计数减1来请求获得一个信号量,如果结果是0或者大于0,获得信号量锁。
up()操作用来释放信号量。
信号量的初始化:
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
以及
#define init_MUTEX(sem) sema_init(sem, 1)
信号量通常作为一个大数据结构的一部分动态创建。
信号量使用的一个例子:
int dma_free_channel(DMA_Handle_t handle /* DMA handle. */
) {
int rc = 0;
DMA_Channel_t *channel;
DMA_DeviceAttribute_t *devAttr;
if (down_interruptible(&gDMA.lock) < 0) {
return -ERESTARTSYS;
}
// ...
out:
up(&gDMA.lock);
wake_up_interruptible(&gDMA.freeChannelQ);
return rc;
}
同样有读写信号量。
信号量操作函数:
互斥体(mutex)
互斥体是一种比信号量更简单的睡眠锁。
它是简化版的信号量。
静态初始化(include\linux\mutex.h):
#define __MUTEX_INITIALIZER(lockname) \
{ .count = ATOMIC_INIT(1) \
, .wait_lock = __SPIN_LOCK_UNLOCKED(lockname.wait_lock) \
, .wait_list = LIST_HEAD_INIT(lockname.wait_list) \
__DEBUG_MUTEX_INITIALIZER(lockname) \
__DEP_MAP_MUTEX_INITIALIZER(lockname) }
#define DEFINE_MUTEX(mutexname) \
struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)
动态初始化:
# define mutex_init(mutex) \
do { \
static struct lock_class_key __key; \
\
__mutex_init((mutex), #mutex, &__key); \
} while (0)
互斥体的锁定和解锁:
struct clk *clk_get_sys(const char *dev_id, const char *con_id)
{
struct clk *clk;
mutex_lock(&clocks_mutex);
clk = clk_find(dev_id, con_id);
if (clk && !__clk_get(clk))
clk = NULL;
mutex_unlock(&clocks_mutex);
return clk ? clk : ERR_PTR(-ENOENT);
}
任何时刻只能有一个任务可以持有mutex;
给mutex上锁着必须负责给其解锁;
递归地上锁和解锁是不允许的;
当持有一个mutex时,进程不可以退出;
mutex不能在中断或者下半部中使用;
mutex只能通过官方API管理;
相比信号量,应该优先使用mutex。
互斥体操作函数:
自旋锁和互斥体的比较:
完成变量
内核中一个任务需要发出信号通知另一个任务发生了某个事件,可以利用完成变量。
完成变量有completion表示(include\linux\completion.h):
struct completion {
unsigned int done;
wait_queue_head_t wait;
};
初始化使用:
#define DECLARE_COMPLETION(work) \
struct completion work = COMPLETION_INITIALIZER(work)
完成变量操作函数:
顺序锁(seq锁)
顺序锁提供了一个简单的机制,用于读写共享数据。
这种锁依赖于一个序列计数器。
当有共享数据写入时,会得到一个锁,并且序列值会增加;在读取数据之前和之后,序列值会被读取,如果读取的序列号值相同,说明在读操作进行的过程中没有被写操作打断过。
seq锁的结构体表示(include\linux\seqlock.h):
typedef struct {
unsigned sequence;
spinlock_t lock;
} seqlock_t;
定义一个seq锁:
__cacheline_aligned_in_smp DEFINE_SEQLOCK(xtime_lock);
#define DEFINE_SEQLOCK(x) \
seqlock_t x = __SEQLOCK_UNLOCKED(x)
写锁的使用:
write_seqlock(&xtime_lock);
do_something();
write_sequnlock(&xtime_lock);
读锁的使用:
u64 get_jiffies_64(void)
{
unsigned long seq;
u64 ret;
do {
seq = read_seqbegin(&xtime_lock);
ret = jiffies_64;
} while (read_seqretry(&xtime_lock, seq));
return ret;
}
选择seq锁的情况:
- 你的数据存在很多的读者;
- 你的数据写者少;
- 虽然写着少,但是你希望写优先于读,而且不允许读者让写者饥饿;
- 你的数据很简单,但是不能使用原子量;
上面的jiffies就是一个例子。
顺序和屏障
程序代码需要以指定的顺序发出读和写的指令,但是编译器和处理器为了提高效率,可能对种类的读写指令进行重新的排序,导致异常。
为了需要使用指令来确保顺序,这种指令称为屏障(barriers)。
rmb()读内存屏障。
wmb()写内存屏障。
还有一个mb()同时提供读写屏障。
在xmb()之前的载入操作不会被重新排在该调用之后,同理在xmb()之后载入的操作不会被重新排在该调用之前。
屏障方法:
内核抢占相关函数
如果共享数据时每个处理器独有的,可能就不需要锁。
可以使用preempt_disable()禁止内核抢占。