linux 内核 互斥技术(一)信号量

文章详细介绍了内核中如何使用互斥技术保证对共享资源的访问安全,包括信号量、自旋锁、读写自旋锁等多种机制,并讨论了它们在不同场景下的适用性。此外,还提到了内核的死锁检测工具lockdep对于调试的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在内核中,可能出现多个进程(通过系统调用进入内核模式)访问同一个对象、进程

和硬中断访问同一个对象、进程和软中断访问同一个对象、多个处理器访问同一个对象等

现象,我们需要使用互斥技术,确保在给定的时刻只有一个主体可以进入临界区访问对象。

如果临界区的执行时间比较长或者可能睡眠,可以使用下面这些互斥技术。

(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)。

  1. 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,进程获得了信号量。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值