Linux学习第29节,从C语言源码分析,信号量和自旋锁有何区别
发表于: 2019-03-09 21:51:17 | 已被阅读: 36 | 分类于: Linux笔记
上一节主要介绍了Linux内核中的自旋锁,知道了自旋锁是不能睡眠的,因此只适合用于短时间的保护临界区。如果需要较长时间的持有锁,就不应该再使用自旋锁了,因为这会大量消耗 cpu 的性能,大大降低整个系统的效率。
不过,在Linux内核开发中,不可避免的会遇到需要长时间保护的临界区,该怎么办呢?因此,Linux 内核还提供了一种同步共享数据的机制——信号量。
Linux 内核中的信号量的数据结构
首先来看下Linux 内核中的信号量使用的数据结构,它是由结构体 semaphore 描述的,相关的C语言代码如下,请看:
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
自旋锁则是依赖原子操作实现的,所以原子操作一节,我们提到原子操作是其他同步机制的基础。
上一节介绍的自旋锁,只能同时被一个线程持有,而信号量则不一定,它可以同时被 count 个线程持有。不过大多情况下 count 都等于 1,此时信号量被称作
创建信号量
可以使用 sema_init() 函数初始化一个信号量,它是一个 inline 函数,相关C语言代码如下,请看:
static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
init_MUTEX_LOCKED() 宏创建了一个 count 等于 0 的信号量,这说明该信号量一开始就是被初始化线程持有的。
使用信号量
早期的信号量支持两个原子操作 P() 和 V(),分别是指测试操作和增加操作,后来的系统则把这两种操作命名为 down() 和 up(),Linux 内核也遵从这种叫法。down() 函数负责申请信号量并将信号量的 count 减 1,显然,如果 count 大于 0,则任务就可以获得信号量并进入临界区。down() 函数的C语言代码如下,请看:
52 void down(struct semaphore *sem)
- 53 {
| 54 unsigned long flags;
| 55
| 56 spin_lock_irqsave(&sem->lock, flags);
| 57 if (likely(sem->count > 0))
| 58 sem->count--;
| 59 else
| 60 __down(sem);
| 61 spin_unlock_irqrestore(&sem->lock, flags);
| 62 }
static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
static noinline int __sched __down_interruptible(struct semaphore *sem)
{
return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
类似的还有 down_killable() 函数,它的C语言代码如下,请看:
static noinline int __sched __down_killable(struct semaphore *sem)
{
return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT);
}
在临界区完成工作后,up() 函数可以释放信号量,它的C语言代码如下,请看:
void up(struct semaphore *sem)
{
unsigned long flags;
spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
spin_unlock_irqrestore(&sem->lock, flags);
}
根据上述代码,能够发现,up() 函数会在任务队列为空的时候把信号量的引用计数count 加一,否则就会调用
小结
自旋锁提供了一种快速简单的加锁方法,不过它并不适合较长时间的保护临界区,这一需求最好借助可以睡眠的信号量。而如果锁仅会被短时间持有,再使用信号量就不太合适了,因为睡眠和维护等待队列,以及唤醒任务所花费的开销可能比锁占用的全部时间还要大。