《Linux内核设计与实现》读书笔记——内核同步方法

原子操作

原子操作是其它同步方法的基石。

内核提供了两组原子操作接口,分别是针对整数针对单独的位的操作。

针对整数的原子操作只能对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()禁止内核抢占。

 

猜你喜欢

转载自blog.csdn.net/jiangwei0512/article/details/106148682