在内核中,可能出现多个进程(通过系统调用进入内核模式)访问同一个对象、进程
和硬中断访问同一个对象、进程和软中断访问同一个对象、多个处理器访问同一个对象等
现象,我们需要使用互斥技术,确保在给定的时刻只有一个主体可以进入临界区访问对象。
如果临界区的执行时间比较长或者可能睡眠,可以使用下面这些互斥技术。
(1)信号量,大多数情况下我们使用互斥信号量。
(2)读写信号量。
(3)互斥锁。
(4)实时互斥锁。
申请这些锁的时候,如果锁被其他进程占有,进程将会睡眠等待,代价很高。
如果临界区的执行时间很短,并且不会睡眠,那么使用上面的锁不太合适,因为进程
切换的代价很高,可以使用下面这些互斥技术。
(1)原子变量。
(2)自旋锁。
(3)读写自旋锁,它是对自旋锁的改进,允许多个读者同时进入临界区。
(4)顺序锁,它是对读写自旋锁的改进,读者不会阻塞写者。
申请这些锁的时候,如果锁被其他进程占有,进程自旋等待(也称为忙等待)。
进程还可以使用下面的互斥技术。
(1)禁止内核抢占,防止被当前处理器上的其他进程抢占,实现和当前处理器上的其
他进程互斥。
(2)禁止软中断,防止被当前处理器上的软中断抢占,实现和当前处理器上的软中断
互斥。
(3)禁止硬中断,防止被当前处理器上的硬中断抢占,实现和当前处理器上的硬中断
互斥。
在多处理器系统中,为了提高程序的性能,需要尽量减少处理器之间的互斥,使处理
器可以最大限度地并行执行。从互斥信号量到读写信号量的改进,从自旋锁到读写自旋锁
的改进,允许读者并行访问临界区,提高了并行性能,但是我们还可以进一步提高并行性
能,使用下面这些避免使用锁的互斥技术。
(1)每处理器变量。
(2)每处理器计数器。
(3)内存屏障。
(4)读-复制更新(Read-Copy Update,RCU)。
(5)可睡眠 RCU。
使用锁保护临界区,如果使用不当,可能出现死锁问题。内核里面的锁非常多,定位
很难,为了方便定位死锁问题,内核提供了死锁检测工具 lockdep。
信号量
信号量允许多个进程同时进入临界区,大多数情况下只允许一个进程进入临界区,把
信号量的计数值设置为 1,即二值信号量,这种信号量称为互斥信号量。
和自旋锁相比,信号量适合保护比较长的临界区,因为竞争信号量时进程可能睡眠和
再次唤醒,代价很高。
内核使用的信号量定义如下。
include/linux/semaphore.h
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
成员 lock 是自旋锁,用来保护信号量的其他成员。
成员 count 是计数值,表示还可以允许多少个进程进入临界区。
成员 wait_list 是等待进入临界区的进程链表。
初始化静态信号量的方法如下。
(1)__SEMAPHORE_INITIALIZER(name, n):指定名称和计数值,允许 n 个进程同时
进入临界区。
(2)DEFINE_SEMAPHORE(name):初始化一个互斥信号量。
在运行时动态初始化信号量的方法如下:
static inline void sema_init(struct semaphore *sem, int val);
参数 val 指定允许同时进入临界区的进程数量。
获取信号量的函数如下。
(1)void down(struct semaphore *sem);
获取信号量,如果计数值是 0,进程深度睡眠。
(2)int down_interruptible(struct semaphore *sem);
获取信号量,如果计数值是 0,进程轻度睡眠。
(3)int down_killable(struct semaphore *sem);
获取信号量,如果计数值是 0,进程中度睡眠。
(4)int down_trylock(struct semaphore *sem);
获取信号量,如果计数值是 0,进程不等待。
(5)int down_timeout(struct semaphore *sem, long jiffies);
获取信号量,指定等待的时间。
释放信号量的函数如下:
void up(struct semaphore *sem);
下面介绍内核信号量的3个操作,分别是down(), down_interruptible(), up().
1.down()
/**
* down - acquire the semaphore
* @sem: the semaphore to be acquired
*
* Acquires the semaphore. If no more tasks are allowed to acquire the
* semaphore, calling this function will put the task to sleep until the
* semaphore is released.
*
* Use of this function is deprecated, please use down_interruptible() or
* down_killable() instead.
*/
void down(struct semaphore *sem)
{
unsigned long flags;
might_sleep();
//申请原始自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
__down(sem);
//释放原始自旋锁,并且恢复当前处理器的硬中断状态。
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(down);
解释:down - 获取信号量; @sem: 要获取的信号量;
down()用于获取一个信号量。如果不允许其他 线程/任务 获取这个信号量,那么调用这个函数将使该进程进入睡眠状态,直到释放信号量。同时,该函数的调用不允许中断。
在此函数中首先进行信号量资源数的查看,如果信号量数据(count)不为0,则把其减1,并返回,调用成功;否则调用 __down -> __down_common 进行等待,调用者进行睡眠。
static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
2.down_interruptible()
down_interruptible()与down()的区别:该函数功能和down()类似,不同之处为,down()不会被信号(signal)打断,但down_interruptible()能被信号打断,因此该函数用返回值来区分是正常返回还是被信号中断。
0 代表正常返回:如果返回0,表示函数调用成功(进入临界区),获得信号量正常返回;
-EINTR 中断:如果睡眠被信号打断,返回-EINTR;
-ETIME 等待超时:
/**
* down_interruptible - acquire the semaphore unless interrupted
* @sem: the semaphore to be acquired
*
* Attempts to acquire the semaphore. If no more tasks are allowed to
* acquire the semaphore, calling this function will put the task to sleep.
* If the sleep is interrupted by a signal, this function will return -EINTR.
* If the semaphore is successfully acquired, this function returns 0.
*/
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
might_sleep();
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_interruptible(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
return result;
}
EXPORT_SYMBOL(down_interruptible);
解释:down_interruptible -获取信号量,除非被中断;@sem:要获取的信号量
尝试获取信号量。如果不允许其他任务获取这个信号量,那么调用这个函数将使该进程进入睡眠状态,直到释放信号量。
在此函数中同样是 先进行信号量资源数的查看,如果信号量数据(count)不为0,则把其减1,并返回0,调用成功;
否则进入__down_interruptible -> __down_common,加入到等待队列,将状态设置成为 TASK_INTERRUPTIBLE,并设置了调度的 Timeout:MAX_SCHEDULE_TIMEOUT
再调用了 schedule_timeout,使得进程进入了睡眠状态。
static noinline int __sched __down_interruptible(struct semaphore *sem)
{
return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
/*
* Because this function is inlined, the 'state' parameter will be
* constant, and thus optimised away by the compiler. Likewise the
* 'timeout' parameter for the cases without timeouts.
*/
static inline int __sched __down_common(struct semaphore *sem, long state,
long timeout)
{
struct semaphore_waiter waiter;
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = current;
waiter.up = false;
for (;;) {
if (signal_pending_state(state, current))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_current_state(state);
raw_spin_unlock_irq(&sem->lock);
timeout = schedule_timeout(timeout);
raw_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;
}
解释:在__down_common函数数执行了以下操作。
(1)将当前进程放到信号量成员变量wait_list所管理的队列中。
(2)在一个for循环中把当前的进程状态设置为TASK_INTERRUPTIBLE,在调用schedule_timeout使当前进程进入睡眠状态,函数将停留在schedule_timeout调用上,直到再次被调度执行。
(3) 当该进程再一次被调度时,按原因执行相应的操作:如果waiter.up不为0说明进程被该信号量的up操作所唤醒,进程可以获得信号量(返回-EINTR)。如果进程是因为被用户空间的信号所中断或超时信号所引起的唤醒,则返回相应的错误代码(返回-ETIME)。
- up()
/**
* up - release the semaphore
* @sem: the semaphore to release
*
* Release the semaphore. Unlike mutexes, up() may be called from any
* context and even by tasks which have never called down().
*/
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(up);
解释:如果sem的wait_list队列为空,则表明没有其他进程正在等待该信号量,那么只需要把sem的count加1即可。如果wait_list队列不为空,则说明有其他进程正睡眠在wait_list上等待该信号,此时调用__up(sem)来唤醒进程:
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 = true;
wake_up_process(waiter->task);
}
解释:在函数中,调用了wake_up_process来唤醒进程,这样进程就从之前的__down_interruptible调用中的timeout=schedule_timeout(timeout)处醒来,wait->up=1, __down_interruptible返回0,进程获得了信号量。