内核同步介绍
概念:
内核同步重要的原因:linux支持多处理器,2.6引入了抢占式内核
临界区:指访问和操作共享数据的代码段
竞争条件:如果两个线程有可能处于同一临界区中同时运行
内核同步的方法:
1,原子变量
2,加锁
造成并发的原因:
1,linux支持多处理器
2,2.6引入了内核抢占
3,中断
4,内核线程睡眠使得调度程序调度另外一个进程
确定哪些数据不需要被访问
1,内核线程的局部数据,内核栈中的数据
2,某些数据只被特定进程访问,因为一个进程只会在一个处理器上运行
死锁:
1,自死锁,如果一个线程试图获得一个自己已经持有的锁将导致死锁
产生死锁的四个必要条件:
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
内核抢占的含义
抢占是指高优先级的进程可以强占低优先级的进程的运行资源。
抢占式内核在哪些状态下是不可以被抢占的:
1,当内核运行中断处理程序或者异常处理程序的时候,,在中断里不允许再中断。
2,内核态运行临界区的代码的时候。临界区通常被自旋锁 spin_lock保护起来了
3,内核正在执行调度的时候,即实现scheduler()函数的时候
4,内核对每一个cpu私有数据操作的时候,(Per CPU structure)
5,内核正在中断底半部分的时候,bottom half(工作队列里还是可以抢占的)
抢占内核为每一个进程(task_struct)结构体引入了preempt_count变量,称为内核抢占锁,每当内核进入以上五种状态,preempt_count加一,退出以上五种状态的时候preempt_count减一,每当进行抢占内核调度的时候,先判断preempt_count与0的大小。
linux内核同步方法的具体实现
1,原子变量
typedef struct{
volatile int counter; //volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。
}atomic_t;
使用原子变量的步骤
atomic_t read_excl;
2,设置原子变量的值
atomic_set(&read_excl,0);
3,在临界区的开始和结束部分加锁和解锁
dy_lock(&read_excl);
'''''
''''
临界区
'''''
dy_unlock(&read_excl);
static inline int dy_lock(atomic_t *read_excl)
{
if(atomic_inc_return(&read_excl) ==1)
return 0;
else
return -1;
}
原子量使用实例:内核的struct page结构体中有如下一个成员,atomic_t __count,表示了内核使用该页的次数,当为0的时候page实例不再使用,可以删除,
2,自旋锁:
自旋锁最多只能被一个可执行线程持有,如果试图获取自旋锁的线程发现锁已经被占有,那么该线程一直处于忙等待状态,直到获取到该锁,所以自旋锁不应该被长时间占有。自旋锁是不可以递归的,如果你试图获得一个你正持有的锁,你必须自旋等待自己释放这个锁,但是自己处于自旋忙等待中,永远没机会释放该锁。
自旋锁的使用:
spinlock_t lock;
spin_lock(&mr_lock);
''''
临界区
'''''
spin_unlock(&mr_lock);
在中断处理程序中在获取自旋锁前一定要先禁止本地中断(当前处理器的中断请求),如果自旋锁在中断处理函数中被用到,那么在获取该锁之前需要关闭本地中断,spin_lock_irqsave 只是下列动作的一个便利接口:1 保存本地中断状态2 关闭本地中断3 获取自旋锁解锁时通过 spin_unlock_irqrestore完成释放锁、恢复本地中断到之前的状态等工作
DEFINE_SPINLOCK(mr_lock);
unsigned long flags;
spin_lock_irqsave(&mr_lock,flags);
'''''
临界区
''''
spin_unlock_irqrestore(&mr_lock,flags);
如果确定在获取锁之前本地中断是开启的,那么就不需要保存中断状态,解锁的时候直接将本地中断启用就可以,这样可以用如下的接口
DEFINE_SPINLOCK(mr_lock);
spin_lock_irq(&mr_lock,);
'''''
临界区
''''
spin_unlock_irqr(&mr_lock,);
自旋锁在单核和多核的区别
1. 在单cpu,不可抢占内核中,自旋锁为空操作。
2. 在单cpu,可抢占内核中,自旋锁实现为“禁止内核抢占”,并不实现“自旋”。
3. 在多cpu,可抢占内核中,自旋锁实现为“禁止内核抢占” + “自旋”。
DEFINE_RWLOCK(mr_rwlock);
read_lock(&mr_rwlock);
''''''
临界区
'''''
read_unlock(&mr_rwlock);
. 当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞;当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是以写模式对它进行枷锁的线程将阻塞;读写锁的实现原理:维护一个很大的内存计数器读写锁的原理
使用实例:
256 static struct file_system_type *__get_fs_type(const char *name, int len)
257 {
258 struct file_system_type *fs;
259
260 read_lock(&file_systems_lock);
261 fs = *(find_filesystem(name, len));
262 if (fs && !try_module_get(fs->owner))
263 fs = NULL;
264 read_unlock(&file_systems_lock);
265 return fs;
266 }
4,信号量
linux中的信号量是一种睡眠锁,如果一个线程试图获取一个已经被抢占的信号量时,信号量会将其推进一个等待队列,然后让其睡眠,这个时候处理器可以去执行其他代码,当信号量可用的时候,处于等待队列的线程会被唤醒,并获取该信号量。由于等待信号量的线程会睡眠,所以适用于锁会被长时间占有的情况,由于信号量允许睡眠,所以信号量比自旋锁开销更大。在占用信号量的同时不能占用自旋锁,因为你等到的信号量可能会睡眠,而在持有自旋锁的时候是不能睡眠的。
自旋锁在一个时刻只能运行一个数量的持有者,而信号量在一个时刻可以允许任意数量的持有者,该数量是在信号量初始化的时候指定,也就是同一时刻可能有多个进程同时访问临界区。
互斥信号量的使用静态创建和初始化信号量
static DECLARE_MUTEX(mr_sem);
动态初始化信号量:
sema_init(&mr_sem,count);
if (down_interruptible(&mr_sem)) //试图获取指定的信号量,如果信号量不可用,它将会把调用进程设置为TASK_INTERRUPTIBLE状态并进入睡眠,睡眠的进程可以被唤醒,而down()会让进程状态设置为TASK_UNINTERRUPTIBLE进入睡眠
/*临界区*/
up(&mr_sem);
down()的操作通过对信号量计数减1来请求一个信号量,如果结果是0或者大于0就会获得信号量,任务就可以进入临界区,如果结果为负数,任务将被放入等待队列,up()和down()的目前实现还允许这两个函数在同一个信号量上并发,因此可能存在错误,
5,互斥锁
互斥锁和信号量为1的信号量含义类似,可以允许睡眠
struct mutex mutex;
mutex_init(&mutex);
mutex_lock(&mutex);
mutex_unlock(&mutex);
6,完成变量
完成量是基于等到队列机制的~~
如果在内核中一个任务需要发出信号通知另一个任务发生了某个特定的事件,利用完成变量可以做这个活。是使两个任务得以同步的简单方法。
完成变量由结构体completion表示,定义在
创建及初始化:
静态:DECLARE_COMPLETION(mr_comp);
动态:init_completion();
在一个指定的完成变量上,需要等待的任务调用wait_for_completion()来等待特定事件。特定事件发生后,产生时间的任务调用complete()来发送信号唤醒正在等待的任务
A B(等A中的complete完成)
工作 等待:wait_for_complete()
... 执行工作
complete()
7,linux RCU机制
点击打开链接
RCU(Read-Copy Update),顾名思义就是读-拷贝修改,它是基于其原理命名的。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。
一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作,
RCU与rwlock的不同之处是:它既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是
否可以有多个写者并行访问取决于写者之间使用的同步机制)
8,wakelock机制
wakelock可以被内核空间 申请和释放。如果申请的是非超时锁wake_lock,需要相应的调用wake_unlock来释放,而超时锁则不需要手工释放(当然你也可以手工释放),超时后kernel系统会自动释放锁,在内核空间可以直接调用wake_lock, wake_lock_timeout 申请锁 Android kernel为用户空间提供了申请和释放wakelock的接口,实现在kernel/power/userwakelock.c中。 驱动程序中使用wakelock的步骤:
1:首先申明一个wake_lock
static struct wake_lock alarm_rtc_wake_lock;
2:在init函数中对wake_lock初始化
wake_lock_init(&alarm_rtc_wake_lock, WAKE_LOCK_SUSPEND, "alarm_rtc");//WAKE_LOCK_SUSPEND 为suspend lock还有一种idle lock
WAKE_LOCK_SUSPEND, /* Prevent suspend */
WAKE_LOCK_IDLE, /* Prevent low power idle*/
3:使用wake_lock()或者wake_lock_timeout()
wake_lock_timeout(&alarm_rtc_wake_lock, 1 * HZ);
wake_lock(&alarm_rtc_wake_lock);
4:使用wake_unlock()
wake_unlock(&alarm_rtc_wake_lock);
5:在exit中调用wake_lock_destroy()
wake_lock_destroy(&alarm_rtc_wake_lock);
9,Per-CPU变量
Per-cpu变量详细解释