2025年信号量, 互斥量,自旋锁

信号量, 互斥量,自旋锁一 信号量 信号量又称为信号灯 它是用来协调不同进程间的数据对象的 而最主要的应用是共享内存方式的进程间通信 本质上 信号量是一个计数器 它用来记录对某个资源 如共享内存 的存取状况 一般说来 为了获得共享资源 进程需要执行下列操作

大家好,我是讯享网,很高兴认识大家。

一、信号量

信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作:
   (1) 测试控制该资源的信号量。
   (2) 若此信号量的值为正,则允许进行使用该资源。进程将信号量减1。
   (3) 若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)。
   (4) 当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。
    维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件/usr/src/linux/include/linux/sem.h 中看到内核用来维护信号量状态的各个结构的定义。信号量是一个数据集合,用户可以单独使用这一集合的每个元素。要调用的第一个函数是semget,用以获得一个信号量ID。Linux2.6.26下定义的信号量结构体:

struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
从以上信号量的定义中,可以看到信号量底层使用到了spin lock的锁定机制,这个spinlock主要用来确保对count成员的原子性的操作(count–)和测试(count > 0)。

1.信号量的P操作:
(1).void down(struct semaphore *sem);
(2).int down_interruptible(struct semaphore *sem);
(3).int down_trylock(struct semaphore *sem);

int down_interruptible(struct semaphore *sem) { unsigned long flags; int result = 0; spin_lock_irqsave(&sem->lock, flags); if (likely(sem->count > 0)) sem->count--; else result = __down_interruptible(sem); spin_unlock_irqrestore(&sem->lock, flags); return result; }

讯享网

对此函数的理解:在保证原子操作的前提下,先测试count是否大于0,如果是说明可以获得信号量,这种情况下需要先将count–,以确保别的进程能否获得该信号量,然后函数返回,其调用者开始进入临界区。如果没有获得信号量,当前进程利用struct semaphore 中wait_list加入等待队列,开始睡眠。

对于需要休眠的情况,在__down_interruptible()函数中,会构造一个struct semaphore_waiter类型的变量(struct semaphore_waiter定义如下:

struct semaphore_waiter
{
struct list_head list;
struct task_struct *task;
int up;
};
),将当前进程赋给task,并利用其list成员将该变量的节点加入到以sem中的wait_list为头部的一个列表中,假设有多个进程在sem上调用down_interruptible,则sem的wait_list上形成的队列如下图:
这里写图片描述
讯享网

(注:将一个进程阻塞,一般的经过是先把进程放到等待队列中,接着改变进程的状态,比如设为TASK_INTERRUPTIBLE,然后调用调度函数schedule(),后者将会把当前进程从cpu的运行队列中摘下)

(3)试图去获得一个信号量,如果没有获得,函数立刻返回1而不会让当前进程进入睡眠状态。

2.信号量的V操作

void up(struct semaphore *sem);

原型如下:
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); 

}

如果没有其他线程等待在目前即将释放的信号量上,那么只需将count++即可。如果有其他线程正因为等待该信号量而睡眠,那么调用__up.

__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);
}

二、互斥体

Linux 2.6.26中mutex的定义:

struct mutex { /* 1: unlocked, 0: locked, negative: locked, possible waiters */ atomic_t count; spinlock_t wait_lock; struct list_head wait_list; #ifdef CONFIG_DEBUG_MUTEXES struct thread_info *owner; const char *name; void *magic; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif };

如果要定义一个静态mutex型变量,应该使用DEFINE_MUTEX

如果在程序运行期要初始化一个mutex变量,可以使用mutex_init(mutex),mutex_init是个宏,在该宏定义的内部,会调用__mutex_init函数。
#define mutex_init(mutex) \
do { \
static struct lock_class_key __key; \
\
__mutex_init((mutex), #mutex, &__key); \
} while (0)
__mutex_init定义如下:

void
__mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key)
{
atomic_set(&lock->count, 1);
spin_lock_init(&lock->wait_lock);
INIT_LIST_HEAD(&lock->wait_list);

讯享网 debug_mutex_init(lock, name, key); 

}

从__mutex_init的定义可以看出,在使用mutex_init宏来初始化一个mutex变量时,应该使用mutex的指针型。

mutex上的P,V操作:void mutex_lock(struct mutex *lock)和void __sched mutex_unlock(struct mutex *lock)

从原理上讲,mutex实际上是count=1情况下的semaphore,所以其PV操作应该和semaphore是一样的。但是在实际的Linux代码上,出于性能优化的角度,并非只是单纯的重用down_interruptible和up的代码。以ARM平台的mutex_lock为例,实际上是将mutex_lock分成两部分实现:fast path和slow path,主要是基于这样一个事实:在绝大多数情况下,试图获得互斥体的代码总是可以成功获得。所以Linux的代码针对这一事实用ARM V6上的LDREX和STREX指令来实现fast path以期获得**的执行性能。

三、自旋锁

自旋锁它是为为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

自旋锁适用情况

typedef struct spinlock { union { struct raw_spinlock rlock; #ifdef CONFIG_DEBUG_LOCK_ALLOC # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) struct{ u8 __padding[LOCK_PADSIZE]; struct lockdep_map dep_map; }; #endif }; } spinlock_t;

mutux底层支持:

futex_wait(addr, val):检查*addr == val ?,若相等则将当前线程放入等待队列addr中随眠(陷入到内核态等待唤醒),否则调用失败并返回错误(依旧处于用户态)
futex_wake(addr, val):唤醒val个处于等待队列addr中的线程(从内核态回到用户态) 因此采用futex(2)的互斥锁不必每次加解锁都从用户态切换到内核态,效率较高.
Windows:底层的CRITICAL_SECTION嵌入了一小段自旋锁,如果不能立即拿到锁,它先会自旋一小段时间,如果还拿不到,才挂起当前线程.

加锁和解锁的底层实现

加锁解锁过程在多核上的实现原理:TSL(或XCHG) + 锁变量

防止多个CPU并发的执行加解锁操作:加锁和解锁过程底层使用汇编实现的原子操作,由于屏蔽中断并不能对其它CPU造成任何影响,唯一的方法便是在加锁的过程中锁住内存总线。因此系统采用TSL或XCHG指令,这两个指令都将一个内存字lock读到寄存器中,然后在该内存地址上存上一个非零值表示加锁,整个读写内存字的操作即使在多核系统上也是原子的 —— 因为执行TSL或XCHG指令时CPU会锁住内存总线,以禁止其它CPU在本指令结束之前访问内存。
防止多个进程(同一CPU或不同CPU上的进程)并发的进入同一临界区:利用下面的汇编代码,由于TST、XCHG以及MOVE指令都是原子的,因此当第二个进程试图进入临界区时,第一个进程必然已经完成加锁操作,从而第二个进程无法进入同一临界区(根据比较结果可知第二个进程可能会阻塞或自旋)。解锁同理。—— 不同于屏蔽中断,使用该方法时同一CPU可以进程间切换,但切换后的进程却无法访问同一临界区。

互斥锁的加锁和解锁汇编实现:

mutex_lock:
TSL REGISTER, MUTEX // 该指令为原子操作,系统将互斥量MUTEX拷贝到寄存器,并将互斥量置为1(表示加锁),同时锁住MUTEX所在的总线,使得其它CPU无法访问该互斥量

JZE ok // 此时表示原来是解锁状态,因此只需返回处理临界区代码即可
CALL thread_yield // 此时表示原来是加锁状态,需要将进程阻塞,释放CPU的使用权,并调度到另一个线程thread_yield
JMP mutex_lock // 此时进程被唤醒,继续尝试
ok: RET
mutex_unlock:
MOVE MUTEX, 0 // 该指令也为原子操作,即将互斥锁MUTEX置为0,表示解锁
RET

自旋锁的加锁和解锁汇编实现:

spin_lock:
TSL REGISTER, LOCK // 将互斥量MUTEX拷贝到寄存器,并将互斥量置为1(表示加锁),同时锁住MUTEX所在的总线,使得其它CPU无法访问该互斥量
CMP REGISTER, 0 // 判断原互斥量是否为0, 若为0,则表示原来是解锁状态可用;否则表示原来是加锁状态,需要阻塞进程
JNE spin_lock // 此时表示原来是加锁状态,返回spin_lock的入口地址继续尝试加锁
RET
spin_unlock:
MOVE LOCK, 0 // 将互斥量LOCK置为0,表示解锁
RET

自旋锁的临界区内不允许任务睡眠:自旋锁分两种,一种是在关抢占的同时顺带关中断的spinlock_irqsave,另一种是仅关抢占而开中断的spinlock。对于顺带关中断的自旋锁来说,显而易见在临界区内使不能睡眠的;而对于开中断的自旋锁来说,可能发生如下两种情况:

死锁:任务A获得自旋锁之后睡眠,接着又发生了中断,而中断处理程序内部又打算获取同一个自旋锁,则此时会发生自死锁 —— 这也是为什么自旋锁是不可重入的。
CPU浪费:倘若中断处理程序内部没有获取同一个自旋锁的操作,则理论上可以产生调度。假设进程B打算获取CPU的控制权,但由于此时是关抢占的(因为进程A还没有解自旋锁,此时依旧处于自旋锁的临界区中),导致进程B无法运行。也就是说CPU将无法运行任何程序,一直处于无事可做的状态,造成CPU的浪费。
为什么关中断时不能睡眠?:唤醒一个睡眠的进程依赖于调度器,而调度器是通过时钟中断来判断合适唤醒进程的,倘若在关闭中断的时候进程睡眠,则调度器将再也无法收到时钟中断(因为开中断的操作也是由该进程控制的),从而永远都无法唤醒睡眠的进程。也就是说该进程将处于睡死状态。
获取自旋锁和禁止中断的时候为什么不能睡眠?
自旋锁不允许递归使用:由于外层递归已将资源锁定,因此即使内层递归不断的申请这个资源也无法获得资源,从而进入死循环。

四、信号量、互斥体和自旋锁的区别

信号量/互斥体和自旋锁的区别

信号量/互斥体允许进程睡眠属于睡眠锁,自旋锁则不允许调用者睡眠,而是让其循环等待,所以有以下区别应用
1)、信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因而自旋锁适合于保持时间非常短的情况
2)、自旋锁可以用于中断,不能用于进程上下文(会引起死锁)。而信号量不允许使用在中断中,而可以用于进程上下文
3)、自旋锁保持期间是抢占失效的,自旋锁被持有时,内核不能被抢占,而信号量和读写信号量保持期间是可以被抢占的

另外需要注意的是
1)、信号量锁保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区,因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一进程企图获取本自旋锁,死锁就会发生。
2)、在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。

信号量和互斥体之间的区别

概念上的区别:

信号量:是进程间(线程间)同步用的,一个进程(线程)完成了某一个动作就通过信号量告诉别的进程(线程),别的进程(线程)再进行某些动作。有二值和多值信号量之分。

互斥锁:是线程间互斥用的,一个线程占用了某一个共享资源,那么别的线程就无法访问,直到这个线程离开,其他的线程才开始可以使用这个共享资源。可以把互斥锁看成二值信号量。

上锁时:

信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value值加一。一句话,信号量的value>=0。

互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源。如果没有锁,获得资源成功,否则进行阻塞等待资源可用。一句话,线程互斥锁的vlaue可以为负数。

小讯
上一篇 2025-02-17 17:35
下一篇 2025-02-05 14:18

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/73168.html