内核同步
一.概念
1.临界区:访问临界资源的代码段
临界资源:可以被多个进程同时访问的资源
2.竞争状态
多个内核任务同时访问同一临界区的状态
3.共享队列和加锁
这是一种特殊的竞争状态。队列中的每个节点代表一个”请求“。有出队列和入队列两个函数对其进行操作。多个进程之间通过加锁防止竞争。
4.确定保护对象
<1>.不需要保护的对象:
a.内核任务的局部数据
b.只被特定进程访问的数据
<2>.需要加锁的对象:大多数内核数据结构都需要加锁
如果有其他内核任务可以访问这些数据,那么就给这些数据加上某种形式的锁;如果其他东西能看到它,那么就要锁住它。
5.死锁
6.并发执行的原因
<1>.中断
<2>内核抢占
<3>睡眠
<4>对称多处理
7.内核任务
内核状态下可以独立执行的内核例程,每个内核任务运行时都拥有一个独立的程序计数器,栈和一组寄存器。
内核任务可分为以下几类:内核线程,系统调用,中断服务程序,异常处理程序和下半部。
二.同步措施
内核中有三种同步措施:原子操作,自旋锁和信号量
1.原子操作
1.1数据结构
该数据结构定义在types.h文件里面,如下:
typedef struct { volatile int counter; } atomic_t;
可见该结构就是一个计数器。
加volatile表示变量counter的值每次都必须从内存中读取最新的值,防止数据的不一致。
另外一个问题是:该结构里面只有一个int型的数据,为什么非要将它放入到一个结构体中呢?这是为了强制编译时的类型检查,保证只能通过内核定义的相关函数进行更改或读取。
1.2.函数
与原子操作相关的函数都在atomic_32.h文件里面找到。atomic_read()和atomic_set()都比较简单。
下面是atomic_add()的代码,其中使用了嵌入式汇编。
1 static inline void atomic_add(int i, atomic_t *v) 2 { 3 asm volatile(LOCK_PREFIX "addl %1,%0" 4 : "+m" (v->counter) 5 : "ir" (i)); 6 }
LOCK_PREFIX的含义:加锁,并且支持SMP系统。(可以参考定义前面的英文注释)
这段嵌入式汇编代码的含义是将从寄存器中读取的变量i加到内存中读出的变量v->counter上去。
其他函数与此类似。
2.自旋锁
自旋锁是专门为多处理器设计的。但它适合于短期的加锁,因为等待它的进程会处于忙等状态,这是非常浪费资源的;要进行长期加锁,可使用信号量,它会让等待进程去睡眠。
2.1.数据结构
typedef struct { raw_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 } spinlock_t;
typedef struct { volatile unsigned int lock; } raw_spinlock_t;
2.2.函数
有两个基本的函数spin_lock和spin_unlock():
static inline void __spin_lock(spinlock_t *lock) { preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock); }
2.2.1.preempt_disable()宏
#define preempt_disable() \ do { \ inc_preempt_count(); \ barrier(); \ } while (0)
从字面上看,preempt_disable()是将CPU的抢占(preempt)功能取消掉。
首先,调用inc_preempt_count()给当前进程current的抢占计数器preempt_count加1,然后调用barrier()。关于barrier()可以看一下源代码:
#define barrier() __asm__ __volatile__("": : :"memory")
可见该宏的作用是将当前寄存器的内容写入内容中。
另一个问题是,为何要将preempt_disable()定义为do{...}while(0)的形式?这是为了防止宏替换出错,与给一个变量加括号是一个作用。
2.2.2.spin_acquire()
从字面上来看,该函数的作用应该是取得自旋锁。但是该宏最终被替换为do{ }while(0),即不执行任何语句。这儿有些不懂了。
2.2.3.LOCK_CONTENTDED()
#define LOCK_CONTENDED(_lock, try, lock) \ do { \ if (!try(_lock)) { \ lock_contended(&(_lock)->dep_map, _RET_IP_); \ lock(_lock); \ } \ lock_acquired(&(_lock)->dep_map, _RET_IP_); \ } while (0)
这儿的lock_contended()和lock_acquired()都被替换为do{ }while(0),还是不懂。
3.信号量
信号量适用于需要长期进行加锁的进程。
3.1.数据结构
struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; };
因为该结构中已经包含了自旋锁,所以不能让一个任务同时持有自旋锁和信号量。
3.2.函数
3.2.1.down()函数
down()获取一个信号量,但是建议使用down_interruptible()或down_killable().因为这两个函数支持中断信号。
这几个函数如果不能获取信号量的话,都会最终调用__down_common()函数:
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.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; }
该函数的流程是:
将当前进程加入到等待队列,然后进入循环:
如果被信号中断或者超时的话,返回;
否则,设置进程状态,并且使当前进程睡眠;
3.2.2.up()函数
当等待队列为空时,sem的count加1,否则调用__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); }
该函数的过程是:删除等待队列首元素,并唤醒对应的进程。
注意这里的list_first_entry()的含义:获取队列的首元素的地址。
#define list_first_entry(ptr, type, member) \ list_entry((ptr)->next, type, member)