全部博文(46)
分类: LINUX
2010-08-30 17:19:58
内核同步(并发与竞态):
1、针对整数的原子操作只能对atomic_t类型的数据进行处理。在
在Linux支持的所有机器上的整型数据都是32位的,但是使用atomic_t的代码只能将该类型的数据当做24位使用,这个限制是因为在APARC体系结构上,原子操作的实现不同于其它的体系构:32位int类型的低8位被嵌入到一个锁中,因为SPARC体系结构对于原子操作缺乏指令级的支持,所以只能利用该锁来避免对原子类型数据的并发访问,所以在SPARC机器上就只能使用24位,虽然在其他的机器上的代码完全可以使用这全部的32位但是在SPARC机上却可能造成一些奇怪和微妙的错误。
在
typedef struct {
volatile int counter;
} atomic_t;
volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对他的访问都是对内存的访问,而不是对寄存器的访问。
在
atomic_read(atomic_t * v);
该函数对原子类型的变量进行原子读操作,他返回原子类型的变量v的值。
atomic_set(atomic_t * v, int i);
该函数设置原子类型的变量v的值为i。
void atomic_add(int i, atomic_t *v);
该函数给原子类型的变量v增加值i。
atomic_sub(int i, atomic_t *v);
该函数从原子类型的变量v中减去i。
int atomic_sub_and_test(int i, atomic_t *v);
该函数从原子类型的变量v中减去i,并判断结果是否为0,如果为0,返回真,否则返回假。
void atomic_inc(atomic_t *v);
该函数对原子类型变量v原子地增加1。
void atomic_dec(atomic_t *v);
该函数对原子类型的变量v原子地减1。
int atomic_dec_and_test(atomic_t *v);
该函数对原子类型的变量v原子地减1,并判断结果是否为0,如果为0,返回真,否则返回假。
int atomic_inc_and_test(atomic_t *v);
该函数对原子类型的变量v原子地增加1,并判断结果是否为0,如果为0,返回真,否则返回假。
int atomic_add_negative(int i, atomic_t *v);
该函数对原子类型的变量v原子地增加I,并判断结果是否为负数,如果是,返回真,否则返回假。
int atomic_add_return(int i, atomic_t *v);
该函数对原子类型的变量v原子地增加i,并且返回指向v的指针。
int atomic_sub_return(int i, atomic_t *v);
该函数从原子类型的变量v中减去i,并且返回指向v的指针。
int atomic_inc_return(atomic_t * v);
该函数对原子类型的变量v原子地增加1并且返回指向v的指针。
int atomic_dec_return(atomic_t * v);
该函数对原子类型的变量v原子地减1并且返回指向v的指针。
2、自旋锁(Spinlock)是一种 Linux 内核中广泛运用的底层同步机制。自旋锁是一种工作于多处理器环境的特殊的锁,在单处理环境中自旋锁的操作被替换为空操作。当某个处理器上的内核执行线程申请自旋锁时,如果锁可用,则获得锁,然后执行临界区操作,最后释放锁;如果锁已被占用,线程并不会转入睡眠状态,而是忙等待该锁,一旦锁被释放,则第一个感知此信息的线程将获得锁。
在
typedef struct {
volatile unsigned int lock;
} raw_spinlock_t;
在实际应用中自旋锁代码只有几行,而持有自旋锁的时间也一般不会超过两次上下方切换,因线程一旦要进行切换,就至少花费切出切入两次,自旋锁的占用时间如果远远长于两次上下文切换,我们就可以让线程睡眠,这就失去了设计自旋锁的意义。 自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
自旋锁可以用在中断处理程序中,但是在使用时一定要在获取锁之前,首先禁止本地中断(当前处理器上的中断),否则中断处理程序就可能打断正持有锁的内核代码,有可能会试图支争用这个已经被持有的自旋锁。这样一来,中断处理程序就会自旋,等待该锁重新可用,但是锁的持有者在这个中断处理程序执行完毕之前不可能运行,这就会造成双重请求死锁。
自旋锁与下半部:由于下半部(中断程序下半部)可以抢占进程上下文中的代码,所以当下半部和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还要禁止下半部执行。同样,由于中断处理程序可以抢占下半部,所以如果中断处理程序和下半部共享数据,那么就必须在获取恰当的锁的同时还要禁止中断。对于软中断,无论是否同种类型,如果数据被软中断共享,那么它必须得到锁的保护,因为同种类型的两个软中断也可以同进运行在一个系统的多个处理器上。但是,同一个处理器上的一个软中断绝不会抢占另一个软中断,因此,根本不需要禁止下半部。
自旋锁提供的API 有:
自旋锁原语要求的包含文件是
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
或者在运行时使用:
void spin_lock_init(spinlock_t *lock);
在进入一个临界区前, 你的代码必须获得需要的 lock , 用:
void spin_lock(spinlock_t *lock);
注意所有的自旋锁等待是, 由于它们的特性, 不可中断的. 一旦你调用 spin_lock, 你将自旋直到锁变为可用.
为释放一个你已获得的锁, 传递它给:
void spin_unlock(spinlock_t *lock);
有很多其他的自旋锁函数, 我们将很快都看到. 但是没有一个背离上面列出的函数所展示的核心概念. 除了加锁和释放, 没有什么可对一个锁所作的. 但是, 有几个规则关于你必须如何使用自旋锁. 我们将用一点时间来看这些, 在进入完整的自旋锁接口之前.
3、信号量
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。
信号量基本使用形式为:
static DECLARE_MUTEX(mr_sem);//声明互斥信号量
if(down_interruptible(&mr_sem))
//可被中断的睡眠,当信号来到,睡眠的任务被唤醒
//临界区
up(&mr_sem);
如果代码需要睡眠——这往往是发生在和用户空间同步时——使用信号量是唯一的选择。由于不受睡眠的限制,使用信号量通常来说更加简单一些。如果需要在自旋锁和信号量中作选择,应该取决于锁被持有的时间长短。理想情况是所有的锁都应该尽可能短的被持有,但是如果锁的持有时间较长的话,使用信号量是更好的选择。另外,信号量不同于自旋锁,它不会关闭内核抢占(在使用自旋锁时,关闭内核抢占,自旋锁一般在SMP中使用),所以持有信号量的代码可以被抢占。这意味者信号量不会对影响调度反应时间带来负面影响。
信号量相关的API:
定义信号量 :struct semaphore sem;
初始化信号量 : void sema_init (struct semaphore
*sem, int val);
该函数初始化信号量,并设置信号量sem的值为val
void
init_MUTEX (struct semaphore *sem);
该函数用于初始化一个互斥锁,即它把信号量sem的值设置为1,等同于
sema_init
(struct semaphore *sem, 1);
void
init_MUTEX_LOCKED (struct semaphore *sem);
该函数也用于初始化一个互斥锁,但它把信号量sem的值设置为0,等同于
sema_init
(struct semaphore *sem, 0);
获得信号量
void
down(struct semaphore * sem);
该函数用于获得信号量sem,它会导致睡眠,因此不能在中断上下文使用;
int
down_interruptible(struct semaphore * sem);
该函数功能与down类似,不同之处为,down不能被信号打断,但down_interruptible能被信号打断; 可被中断的睡眠,当信号来到,睡眠的任务被唤醒
int
down_trylock(struct semaphore * sem);
该函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回非0值。它不会导致调用者睡眠,可以在中断上下文使用。
释放信号量
void
up(struct semaphore * sem);
该函数释放信号量sem,唤醒等待者。