内核中的同步—内核同步的措施

为避免并发,防止竞争。内核提供一组同步方法来提供对共享数据的保护。Linux使用的同步机制随内核版本的不断发展而完善。从原子操作到信号量,从大内核锁到自旋锁。这些同步机制的发展伴随着Linux从单处理器到多处理器的过渡;从非抢占式内核到抢占式内核的过渡。锁机制越来越有效,也越来越复杂。

内核中的同步方法很多,主要介绍原子操作、自旋锁和信号量。

一、原子操作

原子操作保证指令以原子(不可再分)的方式被执行,执行过程不被打断。例如,之前提到的原子方式的加操作,通过把读取和增加变量的行为包含在一个单步中执行,防止了竞争状态发生,保证了操作结果总是一致的(假定i初值为1):

内核任务1                                                                                                 内核任务2

增加i(1->2)                                                                                           ——

                                                                                                                  增加i(2->3)

最后得到结果是3,为正确结果。两个原子操作绝对不可能同时访问同一个变量,这样的加操作绝对不可能引起竞争状态。

Linux内核提供了一个atomic_t类型(一个原子访问计数器),其定义如下:

typedef struct {
    int counter; /* 一个原子访问计数器 */
} atomic_t;

备注:为何将原子类型这样定义?原因是让GCC在编译时加以更加严格的类型检查,防止原子类型变量被误操作(如果为普通类型,则普通的运算符也可以操作)。

Linux内核提供一些专门的函数和宏作用于atomic_t类型的变量。原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的arch/arm/include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。

volatile修饰字段告诉gcc编译器不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而非对寄存器的访问。

Linux中的原子操作

ATOMIC_INIT(i)                                         在声明一个atomic_t变量时,将它初始化为i

atomic_read(v)                                           返回*v

atomic_set(v,i)                                            把*v设置成i

atomic_add(i, v)                                          给*v增加i

atomic_sub(i, v)                                          从*v中减去i

atomic_sub_and_test(i, v)                          从*v中减去i,如果结果为0,则返回1;否则,返回0

atomic_inc(v)                                              给*v加1

atomic_dec(v)                                             从*v中减1

atomic_dec_and_test(v)                             从*v中减1,如果结果为0,则返回1;否则,返回0

atomic_inc_and_test(v)                               给*v加1,如果结果为0,则返回1;否则,返回0

atomic_add_negative(i,v)                            给*v增加i,如果结果为负,则返回1;否则,返回0

atomic_add_return(i, v)                               给*v增加i,并返回相加之后的值

atomic_sub_return(i, v)                               从*v中减i,并返回相减之后的值

除此以外,还有一组操作64位原子变量的变体,以及一些位操作宏及函数。

下面举例说明这些函数的用法。

定义一个atomic_t类型的数据很简单,还可以定义时设定初值:

atomic_t u;/* 定义u */

atomic_t v = ATOMIC_INIT(0) ;/* 定义v并把它初始化为0 */

对其操作:

atomic_set(&v,4);  /* v = 4 (原子地) */

atomic_add(2, &v);/* v = 4 + 2 = 6(原子地) */

atomic_inc(&v);/* v = 6 + 1 = 7(原子地) */

如果要读取原子类型atomic_t的值,可以使用atomic_read()来完成:

printk(KERN_ALERT "v = %d\n", atomic_read(&v));

编写一个内核模块,如何使用这些函数。

#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>

static int __init test_init(void)
{
    atomic_t v = ATOMIC_INIT(0);
    atomic_set(&v,4);
    printk(KERN_ALERT "v = %d\n", atomic_read(&v));
    atomic_add(2, &v);
    printk(KERN_ALERT "v1 = %d\n", atomic_read(&v));
    atomic_inc(&v);
    printk(KERN_ALERT "v2 = %d\n", atomic_read(&v));
    printk(KERN_ALERT "test_init.\n");
    
    return 0;
    
}

static void __exit test_exit(void)
{
    printk(KERN_ALERT "test_exit.\n");
}


module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");
MODULE_VERSION("v1.0");
MODULE_AUTHOR("xz@vichip.com.cn");

Makefile

ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
#$(warning "11111111111111111111111)
all:
        $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
        $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
else
        obj-m := test.o

测试结果:

[192565.708113] v = 4
[192565.708116] v1 = 6
[192565.708117] v2 = 7

原子整数操作最常见的用途就是实现计数器。使用复杂的锁机制来保护一个计数器很笨拙的,所以,最好使用atomic_inc()和atomic_dec()这两个相对来说轻便一点的操作。还可以用原子整数操作原子地执行一个操作并检查结果。原子减操作和检查:

atomic_dec_and_test(&v);

这个函数让给定的原子变量减1,如果结果为0,则返回1;否则,返回0。

Linux内核也提供了位原子操作,相应的操作函数如下:

#define set_bit(nr,p)            ATOMIC_BITOP(set_bit,nr,p)
#define clear_bit(nr,p)            ATOMIC_BITOP(clear_bit,nr,p)
#define change_bit(nr,p)        ATOMIC_BITOP(change_bit,nr,p)
#define test_and_set_bit(nr,p)        ATOMIC_BITOP(test_and_set_bit,nr,p)
#define test_and_clear_bit(nr,p)    ATOMIC_BITOP(test_and_clear_bit,nr,p)
#define test_and_change_bit(nr,p)    ATOMIC_BITOP(test_and_change_bit,nr,p)

这些函数应用于内存管理、设备驱动

2、自旋锁

自旋锁是专为防止多处理器并行而引入的一种锁,自旋锁在内核中大量应用于中断处理等部分,对于单处理器,采用关闭中断的方式来防止中断服务程序的并发执行。

自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被持有的自旋锁,那么这个任务就会一直进行忙循环,也就是旋转,等待锁重新可用。如果锁未被持有,请求锁的内核任务立刻得到锁并且继续执行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,自旋锁可有效地避免多处理器上并行运行的内核任务竞争共享资源。

设计自旋锁的初衷是在短期内进行轻量级的锁定。一个被持有的自旋锁使得请求该锁的任务在等待锁重新可用期间进行自旋(特别浪费处理器的时间),自旋锁不应该被持有的时间过长。如果需要长时间锁定,最好使用信号量

自旋锁定义如下。

typedef struct raw_spinlock {
    arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
    unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned int magic, owner_cpu;
    void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} raw_spinlock_t;

typedef struct spinlock {
    union {
        struct raw_spinlock rlock;

#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
        struct {
            u8 __padding[LOCK_PADSIZE];
            struct lockdep_map dep_map;
        };
#endif
    };
} spinlock_t;

使用自旋锁的基本形式如下:

DEFINE_SPINLOCK(lock); /* 定义一个自旋锁 */

spin_lock(&lock);

/* 临界区 */

spin_unlock(&lock);

自旋锁在同一时刻最多只能由一个内核任务持有,所以一个时刻只允许有一个任务在临界区中。这满足了SMP需要加锁的服务。

在单处理器系统上,虽然在编译时锁机制被抛弃了,但在上面例子中仍需要关闭中断,以禁止中断服务程序访问共享数据。

自旋锁在内核中有很多变种,如对下半部,可使用spin_lock_bh()来获得特定锁并关闭下半部执行。开锁操作则由spin_unlock_bh()来执行;如果把临界区的访问从逻辑上分为读和写这两种模式,那么可使用读者和写者自旋锁

DEFINE_RWLOCK(lock);

在读者的代码分支中使用如下函数:

read_lock(&lock);

/* 只读临界区 */

read_unlock(&lock);
 

在写者的代码分支中使用如下函数:

write_lock(&lock);

/* 写临界区 */

write_unlock(&lock);

备注:自旋锁在内核中主要用来防止多处理器并行访问临界区,防止内核抢占造成的竞争。自旋锁不允许任务睡眠,持有自旋锁的任务睡眠会造成自死锁,这是因为睡眠有可能造成持有锁的内核任务被重新调度,从而再次申请自己已持有的锁。因此,自旋锁能够在中断上下文中执行。

所谓中断上下文是指内核在执行一个中断服务程序或下半部时所处的执行环境

3、信号量

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将该任务推入等待队列,然后让它睡眠。这时,处理器获得自由而去执行其他代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而可以获得这个信号量。

信号量成为一种常见的锁机制。信号量支持两个原子操作P()和V(),P()操作叫做测试操作(探查),V()操作叫做增加操作。后来的系统把这两种操作分别叫做down()和up(),Linux也遵从这种叫法。down()操作通过对信号量计数减1来请求获得一个信号量。如果结果是0或大于0,信号量锁被获得,任务可以进入临界区。如果结果是负数,任务被放入等待队列,处理器执行其他任务。一次down()操作就等于获取该信号量。当临界区中的操作完成后,up()操作用来释放信号量,增加信号量的计数值。如果在该信号量上的等待队列不为空,处于队列中等待的任务在被唤醒的同时获得信号量。

信号量具有睡眠特性,这使得信号量适用于锁被长时间持有的情况,因此,信号量只能在进程上下文中使用,而不能在中断上下文中使用,因为中断上下文不能被调度;另外,当任务持有信号量时,不可以再持有自旋锁。

下面说明信号量的定义和使用。内核中对信号量的定义如下:

include/linux/semaphore.h

/* Please don't access any members of this structure directly */
struct semaphore {
    spinlock_t        lock;
    unsigned int        count;
    struct list_head    wait_list;
};

count:存放unsigned int类型的一个值。如果count大于0,那么资源就是空闲的,该资源可以使用。如果count等于0,那么信号量是忙的,但没有进程等待这个被保护的资源。如果count小于0,那么资源不可用,并至少有一个进程等待资源。

lock:自旋锁。为了防止多处理器并行造成错误。

wait_list:存放等待队列链表的地址,当前等待资源的所有睡眠进程都放在这个链表中。如果count大于或等于0,等待队列为空。

备注:在使用信号量时,不要直接访问semaphore结构体内的成员,要使用内核提供的函数来操作。

1、相关操作函数

为满足各种不同的需求,Linux内核提供操作函数,对于获取信号量的操作,有down(),down_interruptible()等函数。

void down(struct semaphore *sem)
{
    unsigned long flags;

    spin_lock_irqsave(&sem->lock, flags);/* 加锁,使信号量的操作在关闭中断状态下进行,防止SMP并发操作造成错误 */
    if (likely(sem->count > 0)) /* 如果信号量可用,则将引用计数减1 */
        sem->count--;
    else /* 如果无信号量可用,调用__down()函数进入睡眠等待状态 */
        __down(sem);
    spin_unlock_irqrestore(&sem->lock, flags);/* 对信号量操作的解锁 */
}

从上面的分析可以看出,如果没有信号量可用,则当前进程进入睡眠状态。

static inline int __sched __down_common(struct semaphore *sem, long state,
                                long timeout)
{
    struct task_struct *task = current;
    struct semaphore_waiter waiter;

    list_add_tail(&waiter.list, &sem->wait_list);/* 将waiter.list添加到信号量sem的等待队列链表的尾部 */
    waiter.task = task;
    waiter.up = 0;

    for (;;) {
        if (signal_pending_state(state, task))/* 如果当前进程被唤醒,则返回 */
            goto interrupted;
        if (timeout <= 0)/* 如果等待超时,则返回 */
            goto timed_out;
        __set_task_state(task, state);/* 设置进程状态 */
        spin_unlock_irq(&sem->lock);/* 释放自旋锁,并使能中断 */
        timeout = schedule_timeout(timeout);/* 执行调度程序,进程切换 */
        spin_lock_irq(&sem->lock);/* 当进程被唤醒时,如果再次进入获取信号量操作,则对其进行加锁 */
        if (waiter.up)/* 如果进程是被信号量等待队列的其他进程唤醒,则返回 */
            return 0;
    }

 timed_out:/* 进程获取信号量等待超时,返回 */
    list_del(&waiter.list);
    return -ETIME;

 interrupted:/* 进程等待获取信号量时被信号中断,返回 */
    list_del(&waiter.list);
    return -EINTR;
}

其中semaphore_waiter的定义为:

struct semaphore_waiter {
    struct list_head list;
    struct task_struct *task;
    int up;
};

对于不同的信号量获取函数,传递给__down_common()函数的参数不同,对于down()操作调用__down_common的形式为:

__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);

可见,使用down()操作获取信号量时,如果信号量不可获取,则进程进入睡眠等待状态,并且不可被信号打断。而down_interruptible()调用__down_common()的形式为:

__down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);

可见,进程在获取信号量时可以被信号打断的。

当进程获取信号量睡眠锁,只能用在进程上下文中),访问问临界区之后,需要释放信号量。释放信号量操作函数为up():
/**
 * up - release the semaphore
 * @sem: the semaphore to release
 *
 * Release the semaphore.  Unlike mutexes, up() may be called from any
 * context and even by tasks which have never called down().
 */
void up(struct semaphore *sem)
{
    unsigned long flags;

    spin_lock_irqsave(&sem->lock, flags);/* 对信号量操作进行加锁 */
    if (likely(list_empty(&sem->wait_list)))/* 如果该信号量的等待队列为空,则释放信号量 */
        sem->count++;/* 信号量引用计数加1 */
    else/* 否则唤醒该信号量的等待队列队列头的进程 */
        __up(sem);
    spin_unlock_irqrestore(&sem->lock, flags);/* 对信号量操作进行解锁 */
}
EXPORT_SYMBOL(up);

static noinline void __sched __up(struct semaphore *sem)
{
    struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
                        struct semaphore_waiter, list);
    list_del(&waiter->list);
    waiter->up = 1;
    wake_up_process(waiter->task);/* 唤醒该信号量的等待队列队列头的进程 */
}

noinline关键字用来通知编译器不要内联这个函数。inline关键字仅仅是建议编译器做内联展开处理,而不是强制。

2、信号量的使用

要使用信号量,需要包含头文件<linux/semaphore.h>,其中有信号量的定义和操作函数。

1)信号量的创建和初始化

内核提供多种方法来创建信号量。如果要定义一个互斥信号量,则可以使用:

DECLARE_MUTEX(name);

参数name为要定义的信号量的名字。

如果信号量已经被定义,只需将其进行初始化,则可以使用以下函数:

sema_init(struct semaphore *sem, int val);  /* 初始化信号量 sem的值为val */

init_MUTEX(sem);/* 初始化信号量 sem为未锁定的互斥信号量 */

init_MUTEX_LOCKED(sem);/* 初始化信号量sem为锁定的互斥信号量 */

2)信号量的使用

信号量的一般使用形式为:

static DECLARE_MUTEX(sem);/* 声明并初始化互斥信号量sem */

if(down_interruptible(&sem))

        /* 信号被接收,信号量还未获取 */

/* 临界区 */

up(&sem);

函数down_interruptible()试图获取指定的信号量,如果获取失败,它将以TASK_INTERRUPTIBLE状态睡眠。这种进程状态意味着任务可以被信号唤醒,一般来说这是好事。如果进程在等待获取信号量时接收到了信号,那么该进程就会被唤醒,而函数down_interruptible()会返回-EINTR,说明这次任务没有获得所需资源。如果down_interruptible()正常结束并得到了需要的资源,就返回0。

信号量在内核中也有许多变种,比如读者-写者信号量。

信号量的操作函数列表

down(struct semaphore *sem)                        获取信号量,如果不可获取,则进入不可中断睡眠状态(目前不建议使用)

down_interruptible(struct semaphore *sem)    获取信号量,如果不可获取,则进入可中断睡眠状态

down_killable(struct semaphore *sem)            获取信号量,如果不可获取,则进入可被致命信号中断的睡眠状态

down_trylock(struct semaphore *sem)            尝试获取信号量,如果不能获取,则立即返回

down_timeout(struct semaphore *sem,long jiffies)  在给定时间jiffies内获取信号量,如果不能够获取,则返回

up(struct semaphore *sem)                             释放信号量

3、自旋锁和信号量的区别

了解使用自旋锁,何时使用信号量对编写高质量代码很重要,但是多数情况下,不需要太多的考虑,因为在中断上下文中只能使用自旋锁,而在任务睡眠时只能使用信号量。

下面列举自旋锁和信号量的对比

需求                                                                               建议的加锁方法

低开销加锁                                                                   优先使用自旋锁

短期锁定                                                                       优先使用自旋锁

长期加锁                                                                       优先使用信号量

中断上下文中加锁                                                        使用自旋锁

进程上下文中持有锁时需要睡眠、调度                                             使用信号量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值