Linux2.6.33内核同步以及用户同步

并发原因

  • 中断:中断可以在任何时候发生,因此可以随时打断当前正在执行的代码
  • 软中断和tasklet:内核能在任何时刻唤醒或调度软中断和tasklet,打断正在运行的代码
  • 内核抢占:内核的任务可能被另一任务抢占
  • 睡眠与用户空间同步:在内核执行的进程可能会睡眠,从而导致调度一个新的用户进程执行
  • 对称多处理:两个或多个处理器可以同时执行代码

内核同步方法

原子操作

原子整数操作

#include <linux/types.h>
typedef struct {
	volatile int counter;
} atomic_t;
#include <asm/atomic.h>
//在声明一个atomic_t变量时,初始化为i
ATOMIC_INIT(int i);
//原子读取整数变量v
int atomic_read(atomic_t *v);
//原子设置v值为i
void atomic_set(atomic_t *v, int i);
//原子给v加i
void atomic_add(int i, atomic_t *v);
//原子从v减i
void atomic_sub(int i, atomic_t *v);
//原子给v加1
void atomic_inc(atomic_t *v);
//原子给v减1
void atomic_dec(atomic_t *v);
//原子从v减i,如果结果等于0,返回真,否则返回假
int atomic_sub_and_test(int i, atomic_t *v);
//原子给v加i,如果结果是负数,返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v);
//原子给v加i,且返回结果
int atomic_add_return(int i, atomic_t *v);
//原子给v加1,且返回结果
int atomic_inc_return(atomic_t *v);
//原子给v减i,且返回结果
int atomic_sub_return(int i, atomic_t *v);
//原子给v减1,且返回结果
int atomic_dec_return(atomic_t *v);
//原子从v减1,如果结果等于0,返回真,否则返回假
int atomic_dec_and_test(atomic_t *v);
//原子从v加1,如果结果等于0,返回真,否则返回假
int atomic_inc_and_test(atomic_t *v);

64位原子操作

typedef struct {
	volatile long counter;
} atomic64_t;

//在声明一个atomic64_t变量时,初始化为i
ATOMIC64_INIT(int i);
//原子读取整数变量v
int atomic64_read(atomic64_t *v);
//原子设置v值为i
void atomic64_set(atomic64_t *v, int i);
//原子给v加i
void atomic64_add(int i, atomic64_t *v);
//原子从v减i
void atomic64_sub(int i, atomic64_t *v);
//原子给v加1
void atomic64_inc(atomic64_t *v);
//原子给v减1
void atomic64_dec(atomic64_t *v);
//原子从v减i,如果结果等于0,返回真,否则返回假
int atomic64_sub_and_test(int i, atomic64_t *v);
//原子给v加i,如果结果是负数,返回真,否则返回假
int atomic64_add_negative(int i, atomic64_t *v);
//原子给v加i,且返回结果
int atomic64_add_return(int i, atomic64_t *v);
//原子给v加1,且返回结果
int atomic64_inc_return(atomic64_t *v);
//原子给v减i,且返回结果
int atomic64_sub_return(int i, atomic64_t *v);
//原子给v减1,且返回结果
int atomic64_dec_return(atomic64_t *v);
//原子从v减1,如果结果等于0,返回真,否则返回假
int atomic64_dec_and_test(atomic64_t *v);
//原子从v加1,如果结果等于0,返回真,否则返回假
int atomic64_inc_and_test(atomic64_t *v);

原子位操作

  • 位操作函数是对普通内存地址进行操作的,参数是一个指针和一个位号
  • 原子位操作多数情况下是对一个字长的内存进行访问,因而位号应该位于0~31(64位机器是0~43),但是对位号的范围并没有限制
#include <asm/bitops.h>
//原子设置addr所指对象的第nr位
void set_bit(int nr, void *addr);
//原子清空addr所指对象的第nr位
void clear_bit(int nr, void *addr);
//原子翻转addr所指对象的第nr位
void change_bit(int nr, void *addr);
//原子设置addr所指对象的第nr位,并返回原先的值
int test_and_set_bit(int nr, void *addr);
//原子清空addr所指对象的第nr位,并返回原先的值
int test_and_clear_bit(int nr, void *addr);
//原子翻转addr所指对象的第nr位,并返回原先的值
int test_and_change_bit(int nr, void *addr);
//原子返回addr所指对象的第nr位
int test_bit(int nr, void *addr);
  • 内核还提供操作对应的非原子位函数,只是不保证原子性,其操作函数的名字前缀多两个下划线,如test_bit()对应的非原子位操作函数位__test_bit()
  • 内核还提供从指定的地址开始搜索第一个被设置(或未被设置)的位
//第一个参数是一个指针,第二个参数是要搜索的总位数,返回值是第一个被设置的(或没被设置的)位的位号
int find_first_bit(unsigned long *addr, unsigned int size);
int find_fisrt_zero_bit(unsigned long *addr, unsigned int size);

自旋锁

  • 自旋锁在同一时刻至多被一个执行线程持有
  • 如果一个线程试图获取已被持有的自旋锁,那么该线程一直进行忙循环-旋转-等待锁可用状态
  • 自旋锁可以使用在中断处理程序中,在中断处理程序使用自旋锁,一定获取锁之前,首先禁止本地中断,否则中断处理程序就会打断正持有锁的内核代码
  • 保护数据而不是代码

自旋锁方法

//与体系结构相关的代码文件
#include <asm/spinlock.h>
//实际用到的接口定义文件
#include <linux/spinlock.h>
spinlock_t lock;
DEFINE_SPINLOCK(lock);
spin_lock(&lock);
/*
临界区代码
*/
spin_unlock(&lock);

//内核提供禁止中断同时请求锁的接口
unsigned long flags;
spin_lock_irqsave(&lock, flags);
/*
临界区代码
*/
spin_unlock_irqrestore(&lock, flags);
方法描述
spin_lock()获取指定的自旋锁
spin_lock_irq()禁止本地中断并获取指定的锁,不建议使用
spin_lock_irqsave()保存本地中断的当前状态,禁止本地中断,并获取指定的锁
spi_unlock()释放指定的锁
spin_unlock_irq()释放指定的锁,并激活本地中断
spin_unlock_irqrestore()释放指定的锁,并让本地中断恢复到以前状态
spin_lock_init()动态初始化指定的spinlock_t
spin_trylock()试图获取指定的锁,如果未获得,返回非0,而不等待自旋锁被释放
spin_is_lock()如果指定的锁当前已被占用,返回非0,值作为判断,实际不占用自旋锁

自旋锁和下半部

//获取指定锁,并禁止所有下半部的执行
spin_lock_bh();
//释放指定说,并允许所有下半部执行
spin_unlock_bh();
  • 如果下半部和进程上下文共享数据,必须对进程上下文的共享数据进行保护
  • 因中断处理程序可以抢占下半部,所有如果中断处理程序和下半部共享数据时,就必须获取锁的同时禁止中断
  • 同类的tasklet不可能同时运行,所以对同类的tasklet的共享数据不需要保护,但是当数据被两个不同种类的tasklet共享时,就需要访问下半部的数据前获取自旋锁
  • 对于软中断,无论是否同种类型,如果数据被软中断共享,就必须获取锁的保护,但是不需要禁止下半部执行

读-写自旋锁

  • 一个或多个读任务可以并发持有读自旋锁
  • 用于写自旋锁最多只能被一个写任务持有,而且此时不能有并发的读操作
  • 当读自旋锁被持有时,写操作为了互斥访问只能自旋等待
  • 读自旋锁和写自旋锁位于完全分割开的代码分支中
  • 如果读和写不能清晰分开,请使用一般的自旋锁
rwlock_t rwlock;
//读写锁初始化
DEFINE_RWLOCK(rwlock);
//读自旋锁的代码分支
read_lock(&rwlock);
/*
临界区
*/
read_unloc(&relock);

//写自旋锁的代码分支
write_lock(&rwlock);
/*
临界区
*/
write_unloc(&relock);
方法描述
read_lock()获取指定的读锁
read_lock_irq()禁止本地中断并获取指定的读锁
read_lock_irqsave()存储本地中断的当前状态,禁止本地中断并获取指定的读锁
read_unlock()释放指定的锁
read_unlock_irq()释放指定的读锁并激活本地中断
read_unlock_irqrestore()释放指定的读锁并将本地中断恢复到之前状态
write_lock()获取指定的写锁
write_lock_irq()禁止本地中断并获取指定的写锁
write_lock_irqsave()存储本地中断的当前状态,禁止本地中断并获取指定的写锁
write_unlock()释放指定的写锁
write_unlock_irq()释放指定的写锁并激活本地中断
write_unlock_irqrestore()释放指定的写锁并将本地中断恢复到之前状态
write_trylock()试图获取指定的写锁,如果写锁不可用,返回非0值
rwlock_init()初始化指定的rwlock_t

信号量

  • Linux的信号量是一种睡眠锁,如果一个任务试图获取一个不可用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠
  • 当持有的信号量可用(被释放)后,处于等待队列中的任务将被唤醒,并获得该信号量
  • 在同一时刻仅允许一个锁持有者,此时信号量计数等于1,被称为互斥信号量
  • 如果信号量初始化时被设计大于1的非零值,被称为计数信号量

创建和初始化信号量

//信号量的实现与体系结构相关,具体定义文件
#include <asm/semaphore.h>
struct semaphore name;
//静态创建互斥信号量
static DECLARE_MUTEX(name);
//动态创建信号量,name是信号量变量名,count是信号量的使用数量
sema_init(&name, count);
//动态创建互斥量
init_MUTEX(name);

使用信号量

  • 函数down_interruptible()试图获取指定的信号量,如果信号量不可用,将调用进程设置成TASK_INTERRUPTIBLE状态——进入睡眠,意味该进程可以被信号唤醒。如果进程等待回去信号量的时候接收到信号,那么该进程被唤醒,返回-EINTR
  • down()函数会让进程在TASK_UNINTERRUPTIBLE状态下睡眠,进程在等到信号量的时候就不会再响应信号
  • down_trylock()函数,尝试以堵塞方式获取指定的信号量,在信号量被占用时,立刻返回非0值,否则返回0,并且成功持有信号量
//定义并声明一个互斥信号量
static DECLARE_MUTEX(sem);
//试图获取信号量
if (down_interruptible(&sem) {
	//信号被接收,信号量还未获取
}
/*
临界区
*/
//释放给定的信号量
up(&sem);
方法描述
sema_init(struct semaphore * sem, int value)以指定的计数值初始化动态创建的信号量
init_MUTEX(struct semaphore * sem)以计数值1初始化动态创建的信号量
init_MUTEX_LOCKED(struct semaphore * sem)以计数值0初始化动态创建的信号量(初始为加锁状态)
down_interruptible(struct semaphore * sem)试图获取指定的信号量,如果信号量已被占用,则进入可中断睡眠状态
down(struct semaphore * sem)试图获取指定的信号量,如果信号量已被占用,则进入不可中断睡眠状态
down_trylock(struct semaphore * sem)试图获取指定的信号量,如果信号量已被占用,则立刻返回非0值
up(struct semaphore * sem)释放指定的信号量,如果睡眠队列不为空,则唤醒其中一个任务

读-写信号量

  • 读写信号量在内核由rw_semaphore结构表示,定义在文件<linux/rwsem.h>
  • 所有的读写信号量都是互斥量,引用计数等于1
  • 并发持有读锁的读取进程数量不限,只有唯一的写锁的写入进程(没有读取进程占用读写锁)可以获取写锁
  • 所有读写锁的睡眠都不会被信号打断,所有只有一个版本的down()操作
  • 读写锁必须使用在不同的分支代码,如果无法在分支代码中使用,请使用普通的互斥信号量
//创建静态的读——写信号量
static DECLARE_RWSEM(name)
//动态创建的读——写信号量初始化
init_rwsem(struct rw_semaphore *sem);
static DECLARE_RWSEM(rwsem)
//试图获取信号量用于读
down_read(&rwsem);
/*
临界区
*/
//释放信号量
up_read(&rwsem);

//试图获取信号量用于写
down_write(&rwsem);
/*
临界区
*/
//释放信号量
up_write(&rwsem);
  • down_read_trylock()和down_write_trylock(),如果成功获取信号量,则返回非0值,如果信号量被占用,则返回0(与普通信号量返回值相反)
  • downgrade_write()函数可以动态将获取的写锁转换为读锁

互斥体

  • 任何时刻只有一个任务可以持有mutex
  • 给mutex上锁者必须负责其解锁——不能再一个上下文中锁定一个mutex,而在另一个上下文中解锁
  • 递归地上锁和解锁是不允许的
  • 当持有一个mutex时,进程不可以退出
  • mutex不能在中断或者下半部中使用,即使使用mutex_trylock()也不行
  • mutex只能通过API管理,不可被拷贝、手动初始化或重复初始化
//静态定义mutex
DEFINE_MUTEX(name);
//动态初始化mutex
mutex_init(&mutex);

mutex_lock(&mutex);
/*
临界区
*/
mutex_unlock(&mutex);
方法描述
mutex_lock(struct mutex *name)给指定的mutex上锁,如果锁不可用,则睡眠
mutex_unlock(struct mutex *name)为指定的mutex解锁
mutex_trylock(struct mutex *name)试图获取指定的mutex,如果成功则返回1,否则锁被获取,返回值为0
mutex_is_locked(struct mutex *name)如果锁已经被占用,则返回1,否则返回0

completion

  • 一个任务进入睡眠状态等待另一个任务完成指定的动作后唤醒
  • 通用做法,将completion作为数据结构成员
#include <linux/completion.h>
//静态创建completion并初始化
DECLARE_COMPLETION(comp);
//动态创建并初始化
init_completion(&comp);
//完整案例,可以参考kernel/sched.c和kernel/fork.c
方法藐视
init_completion(struct completion *cmp)初始化指定的动态创建的completion
wait_for_completion(struct completion *cmp)等待指定的completion信号
complete(struct completion *cmp)发信号唤醒任何等待任务

BLK:大内核锁

  • 持有BKL的任务仍然可以睡眠,因为当前任务无法被调度时,加锁会自动被丢弃,当任务被调度时,锁又会被重新获得
  • BLK是一种递归锁,一个进程可以多次请求一个锁,并不会产生死锁情况
  • BLK只可以用在进程上下文中
  • 新用户不可使用BLK
  • BLK被持有时禁止内核抢占
lock_kernel();
/*
临界区,对所有其他的BLK用户进行同步
可以在此睡眠,锁自动释放
当任务可以调度时,锁自动获取
*/
unlock_kernel()
函数描述
lock_kernel()获取BLK
unlock_kernel()释放BLK
kernel_locked()如果锁被持有,则返回非0值,否则返回0

顺序锁

  • 用于读写共享数据,依靠一个序列计数器
  • 当有意义的数据被写入时,会得到一个锁,并且序列值会增加
  • 在读取数据之前和之后,序列号都被读取,如果读取的序列号相同,说明读取操作过程进行的过程中没有被写操作打断
  • 如果读取的值时偶数,那么表明写操作没有发生(因为锁的初值为0,写锁会使值成奇数,释放的时候变成偶数)
  • 只有没有其他写入操作,写锁总是能成功获取,读取操作不影响写锁
//定义一个seq锁
seqlock_t seq_lock = DEFINE_SEQLOCK(seq_lock);
//写锁的方法
write_seqlock(&seq_lock);
/*写锁被获取*/
write_sequnlock(&seq_lock);

//读锁操作
unsigned long seq;
do {
	seq = read_seqbegin(&seq_lock);
	/*读取这里的数据*/
} while(read_seqretry(&seq_lock, seq));

案例

//获取Linux机器启动到当前的时间函数
u64 get_jiffies_64(void)
{
	unsigned long seq;
	u64 ret = 0;
	do {
		seq = read_seqbegin(&xtime_lock);
		ret = jiffies_64
	} while(read_seqretry(&xtime_lock, seq));
	return ret;
}

//定时器中断更新jiffies的值,此刻需要使用seq锁变量
write_seqlock(&xtime_lock);
jiffies_64 += 1;
write_sequnlock(&xtime_lock);

禁止抢占

  • 内核抢占代码使用自旋锁作为非抢占区域的标志
  • preempt_disable()禁止内核抢占,可以嵌套调用,每次调用都必须有一个相应的preempt_enable()调用
preempt_disbale();
/*抢占被禁止*/
preempt_enable();
函数描述
preempt_disbale()增加抢占计数值,从而禁止内核抢占
preempt_enable()减少抢占计数,并当该值降为0时检查和执行被挂起的需要调度的任务
preemt_enable_no_resched()激活内核抢占但不再检查任何被挂起的需要调度任务
preempt_count()返回抢占计数

顺序和屏障

  • rmb()提供一个“读”内存屏障
  • wmb()提供一个“写”内存屏障
  • mb()提供一个“读”内存屏障,也提供一个“写”内存屏障
  • 宏smp_rmb()、smp_wmb()、smp_mb()和smp_read_barrier_depends()提供一个有用的优化,在SMP内核中被定义成常用的内存屏障,而在单核处理器内核中,被定义编译器的屏障
  • barrier()方法可以防止编译器跨越对载入或存储操作进行优化
    | 方法 | 描述 |
    |–|--|
    | rmb() | 阻止跨越屏障的载入动作发生重排序 |
    | read_barrier_depends() | 阻止跨越屏障的具有数据依赖关系的载入动作重排序 |
    | wmb() | 阻止跨越屏障的存储动作发生重排序 |
    | mb() | 阻止跨越屏障的载入和存储动作重排序 |
    | smp_rmb() | 在SMP上提供rmb()功能,在UP上提供barrier()功能 |
    | smp_read_barrier_depends() | 在SMP上提供read_barrier_depends()功能,在UP上提供barrier()功能 |
    | smp_wmb() | 在SMP上提供wmb()功能,在UP上提供barrier()功能 |
    | smp_mb() | 在SMP上提供mb()功能,在UP上提供barrier()功能 |
    | barrier() | 阻止编译器跨越屏障对载入或存储操作进行优化 |
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值