https://ty-chen.github.io/linux-kernel-shm-semaphore/
Linux提供两种信号量:
-
内核信号量,由内核控制路径使用
-
用户态进程使用的信号量,这种信号量又分为POSIX信号量和SYSTEM V信号量。
- 对POSIX来说,信号量是个非负整数。
-
而SYSTEM V信号量则是一个或多个信号量的集合,它对应的是一个信号量结构体,这个结构体是为SYSTEM V IPC服务的,信号量只不过是它的一部分。常用于进程间同步。
-
POSIX信号量的引用头文件是
<semaphore.h>
,而SYSTEM V信号量的引用头文件是<sys/sem.h>
Linux内核的信号量在概念和原理上与用户态的System V的IPC机制信号量是一样的,但是它绝不可能在内核之外的中断使用,它是一种睡眠锁。
如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列(它不是站在外面痴痴地等待而是将自己的名字写在任务队列中)然后让其睡眠。
当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量
ps:在有的系统中Binary semaphore与Mutex是没有差异的。在有的系统上,主要的差异是mutex一定要由获得锁的进程来释放。而semaphore可以由其它进程释放(这时的semaphore实际就是个原子的变量,大家可以加或减),因此semaphore可以用于进程间同步。Semaphore的同步功能是所有 系统都支持的,而Mutex能否由其他进程释放则未定,因此建议mutex只用于保护criticalsection。而semaphore则用于保护某变量,或者同步。
平时用的不多!! 有时间继续看
信号量的核心思想是:内核空间分配一个data管理一个“文件id”, 进程A 和进程B 通过“文件id” 访问数据,同时加上原子操作等实现通信!!
PS:在信号量这种常用的同步互斥手段方面,POSIX在无竞争条件下是不会陷入内核的,而SYSTEM V则是无论何时都要陷入内核,因此性能稍差
- 1.System V信号量每次执行PV操作时,都需要进行用户态和内核态的切换。
- 2. POSIX pthread库实现的信号量执行PV操作时,仅当需要时才进行用户态和内核态的切换。具体表述如下:
2.1 P操作:a) 在用户态“信号量值减一,且值大于等于0”,则无需陷入内核;
b) 在用户态“信号量值减一,且值小于0”,则需要陷入内核,并将调用进程插入到该信号量的等待队列,睡眠;
2.2 V操作:a) 在用户态“信号量值加一,且值大于0”,则无需陷入内核;
b) 在用户态“信号量值加一,且值小于等于0”,则需要陷入内核,并唤醒该信号量等待队列上的一个进程;
来看看POSIX 的pthread_mutex 是怎么实现的!!毕竟用的多 性能要好!
这个用的比较多!!
pthread_mutex_t { int __lock; // 锁变量, 传给系统调用futex,用作用户空间的锁变量 mutex状态,0表示未占用,1表示占用 usigned int __count; // 可重入的计数 记录owner线程持有锁的次数 int __owner; // 被哪个线程占有了 owner线程ID int __kind; // 记录mutex的类型,有以下几个取值 PTHREAD_MUTEX_TIMED_NP 等---- ------------------------ }
pthread_mutex_lock
pthread_mutex_lock调用LLL_UNLOCK(基于Linux的futex), 去拿到锁或阻塞自己. 另外,对可重入的锁进行计数(也叫做递归锁,是指在一个线程中可以多次获取同一把锁).
代码见:https://code.woboq.org/userspace/glibc/nptl/pthread_mutex_lock.c.html
int __pthread_mutex_lock (pthread_mutex_t *mutex) { /* See concurrency notes regarding mutex type which is loaded from __kind in struct __pthread_mutex_s in sysdeps/nptl/bits/thread-shared-types.h. */ unsigned int type = PTHREAD_MUTEX_TYPE_ELISION (mutex); LIBC_PROBE (mutex_entry, 1, mutex); if (__builtin_expect (type & ~(PTHREAD_MUTEX_KIND_MASK_NP | PTHREAD_MUTEX_ELISION_FLAGS_NP), 0)) return __pthread_mutex_lock_full (mutex); if (__glibc_likely (type == PTHREAD_MUTEX_TIMED_NP))//一般默认缺省值,即普通锁 { FORCE_ELISION (mutex, goto elision); simple: /* Normal mutex. */ LLL_MUTEX_LOCK (mutex); assert (mutex->__data.__owner == 0); } else if (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex) == PTHREAD_MUTEX_RECURSIVE_NP, 1))//可重入 { /* Recursive mutex. */ pid_t id = THREAD_GETMEM (THREAD_SELF, tid); /* Check whether we already hold the mutex. */ if (mutex->__data.__owner == id) { /* Just bump the counter. */ if (__glibc_unlikely (mutex->__data.__count + 1 == 0)) /* Overflow of the counter. */ return EAGAIN; ++mutex->__data.__count; /* 若已经持有了此锁, 增加计数, 无需block此线程 */ return 0; } /* We have to get the mutex. */ LLL_MUTEX_LOCK (mutex);// 去判断锁变量, 如果不行, 被OS休眠掉 assert (mutex->__data.__owner == 0); mutex->__data.__count = 1;// 拿到了锁, 锁变量是ok的,则设置count } else if (__builtin_expect (PTHREAD_MUTEX_TYPE (mutex) == PTHREAD_MUTEX_ADAPTIVE_NP, 1)) { if (! __is_smp) goto simple; if (LLL_MUTEX_TRYLOCK (mutex) != 0) { int cnt = 0; int max_cnt = MIN (max_adaptive_count (), mutex->__data.__spins * 2 + 10); do { if (cnt++ >= max_cnt) { LLL_MUTEX_LOCK (mutex); break; } atomic_spin_nop (); } while (LLL_MUTEX_TRYLOCK (mutex) != 0); mutex->__data.__spins += (cnt - mutex->__data.__spins) / 8; } assert (mutex->__data.__owner == 0); } else { pid_t id = THREAD_GETMEM (THREAD_SELF, tid); assert (PTHREAD_MUTEX_TYPE (mutex) == PTHREAD_MUTEX_ERRORCHECK_NP); /* Check whether we already hold the mutex. */ if (__glibc_unlikely (mutex->__data.__owner == id)) return EDEADLK; goto simple; } pid_t id = THREAD_GETMEM (THREAD_SELF, tid); /* Record the ownership. */ mutex->__data.__owner = id; LIBC_PROBE (mutex_acquired, 1, mutex); return 0; }
2
pthread_mutex_unlock (pthread_mutex_t *mutex) { if (type == PTHREAD_MUTEX_TIMED_NP) { mutex->__data.__owner = 0; lll_unlock (mutex->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex)); return 0; } else { if (type == PTHREAD_MUTEX_RECURSIVE_NP) ------------------- }
pthread_mutex_lock/unlock主要是调用底层的lll_lock/lll_unlock, 其实就是调用futex的FUTEX_WAIT/FUTEX_WAKE操作, 来实现线程的休眠和唤醒工作
# define LLL_MUTEX_LOCK(mutex) lll_lock ((mutex)->__data.__lock, PTHREAD_MUTEX_PSHARED (mutex)) define __lll_lock(futex, private) ((void) ({ int *__futex = (futex); if (__glibc_unlikely (atomic_compare_and_exchange_bool_acq (__futex, 1, 0))) { if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) __lll_lock_wait_private (__futex); else __lll_lock_wait (__futex, private); } }))
atomic_compare_and_exchange_bool_acq 用于将_futex从0原子变为1,成功则返回0,从而获得锁退出。
失败则返回值>0(对应我们这里是1或者2),然后继续走分支。
futex变量的值有3种:0代表当前锁空闲,1代表有线程持有当前锁,2代表存在锁冲突。
futex的值初始化时是0;当调用mutex-lock的时候会利用cas操作改为1;
当调用lll_lock时,如果不存在锁冲突,则将其改为1,否则改为2。
根据值, 走__lll_lock_wait:
/* Note that we need no lock prefix. */ #define atomic_exchange_acq(mem, newvalue) ({ __typeof (*mem) result; if (sizeof (*mem) == 1) __asm __volatile ("xchgb %b0, %1" : "=q" (result), "=m" (*mem) : "0" (newvalue), "m" (*mem)); else if (sizeof (*mem) == 2) __asm __volatile ("xchgw %w0, %1" : "=r" (result), "=m" (*mem) : "0" (newvalue), "m" (*mem)); else if (sizeof (*mem) == 4) __asm __volatile ("xchgl %0, %1" : "=r" (result), "=m" (*mem) : "0" (newvalue), "m" (*mem)); else __asm __volatile ("xchgq %q0, %1" : "=r" (result), "=m" (*mem) : "0" ((atomic64_t) cast_to_integer (newvalue)), "m" (*mem)); result; })
void __lll_lock_wait (int *futex, int private) { if (*futex == 2) lll_futex_wait (futex, 2, private); /* Wait if *futex == 2. */ while (atomic_exchange_acq (futex, 2) != 0) lll_futex_wait (futex, 2, private); /* Wait if *futex == 2. */ }
同时附上老版本的ll_lock实现:
/* * CAS操作的核心宏,cas操作判断(mutex)->__data.__lock的值是否为0,如果为0,则置为1,ZF=1 * 1.判断是否在多线程环境下 * 2.如果不是多线程环境则直接调用cmpxchgl指令进行cas操作,如果是多线程则需要在cmpxchgl指令前 * 加上lock指令 * 3.如果cas成功则跳到标号18,如果cas失败则调用__lll_lock_wait子程序 */ # define __lll_lock_asm_start "cmpl $0, %%gs:%P6 " "je 0f " "lock " //!!!!!!lock 指令!!!!!!!!!!! "0: cmpxchgl %1, %2 " //LLL_PRIVATE为0,所以不会走第一个分支,走第二个分支 #define lll_lock(futex, private) (void) ({ int ignore1, ignore2; if (__builtin_constant_p (private) && (private) == LLL_PRIVATE) __asm __volatile (__lll_lock_asm_start "jz 18f " "1: leal %2, %%ecx " "2: call __lll_lock_wait_private " "18:" : "=a" (ignore1), "=c" (ignore2), "=m" (futex) : "0" (0), "1" (1), "m" (futex), "i" (MULTIPLE_THREADS_OFFSET) : "memory"); //!!!!!!指令影响内存!!!!!!!!!!! else { int ignore3; __asm __volatile (__lll_lock_asm_start "jz 18f " "1: leal %2, %%edx " "0: movl %8, %%ecx " "2: call __lll_lock_wait " "18:" : "=a" (ignore1), "=c" (ignore2), "=m" (futex), "=&d" (ignore3) : "1" (1), "m" (futex), "i" (MULTIPLE_THREADS_OFFSET), "0" (0), "g" ((int) (private)) : "memory"); } })
可知:xchgb 是一个原子操作在多核cpu 下也是的!!多核/多线程则需要在cmpxchgl指令前 加上lock指令 锁住地址总线 防止被修改:
具体分析可见:smp-volatile
futex(&__lock)的值从0原子变为2就成功。否则调用lll_futex_wait,阻塞。这里的atomic_exchange_acq是一个返回旧值的原子操作,直接采用了内敛汇编(xchg)的方式,并且根据变量类型从而选取linux下不同的汇编指令。
到了这里,只要这个原子xchg的是正确的,并且阻塞与唤醒(wake up)之间的协议是正确的,那么这个mutex的语义就得到保证
/* Wait while *FUTEXP == VAL for an lll_futex_wake call on FUTEXP. */ #define lll_futex_wait(futexp, val, private) lll_futex_timed_wait (futexp, val, NULL, private)
#define lll_futex_timed_wait(futexp, val, timeout, private) lll_futex_syscall (4, futexp, __lll_private_flag (FUTEX_WAIT, private), val, timeout)
见glibc/nptl/lowlevellock.c 文件以及其头文件
所以:lll_futex_syscall调用简化为
lll_futex_syscall (4, futexp, 0, 2, NULL)
1.根据锁类型进行对应的操作,如果是普通锁PTHREAD_MUTEX_TIMED_NP则进行CAS操作,如果CAS失败则进行sys_futex系统调用挂起当前线程
2.如果是递归锁PTHREAD_MUTEX_RECURSIVE_NP,则判断当前线程id是否和持有锁的线程id是否相等,如果相等说明是重入的,则将加锁次数加一。否则进行CAS操作获取锁,如果CAS失败则进行sys_futex系统调用挂起当前线程。
3.如果是适配/自适应 锁PTHREAD_MUTEX_ADAPTIVE_NP,在获取锁的时候会进行自旋操作,当自旋的次数超过最大值时,则进行sys_futex系统调用挂起当前线程。
4.如果是检错锁PTHREAD_MUTEX_ERRORCHECK_NP,则判断锁是否已经被当前线程获取,如果是则返回错误,否则进行CAS操作获取锁,检错锁可以避免普通锁出现的死锁情况。
在获取锁失败后,会将互斥量设置为2,然后进行系统调用进行挂起,这是为了让解锁线程发现有其它等待互斥量的线程需要被唤醒
pthread_mutex_unlock
其核心如下:
#define __lll_unlock(futex, private) ((void) ({ int *__futex = (futex); int __val = atomic_exchange_rel (__futex, 0); if (__builtin_expect (__val > 1, 0)) lll_futex_wake (__futex, 1, private); })) #define lll_unlock(futex, private) __lll_unlock(&(futex), private) #define lll_futex_wake(ftx, nr, private) ({ DO_INLINE_SYSCALL(futex, 3, (long) (ftx), __lll_private_flag (FUTEX_WAKE, private), (int) (nr)); _r10 == -1 ? -_retval : _retval; })
lll_unlock分为两个步骤:
- 将futex设置为0并拿到设置之前的值(用户态操作)
- 如果futex之前的值>1,代表存在锁冲突,也就是说有线程调用了FUTEX_WAIT在休眠,所以通过调用系统函数FUTEX_WAKE唤醒休眠线程
futex变量的值有3种:0代表当前锁空闲,1代表有线程持有当前锁,2代表存在锁冲突。
futex的值初始化时是0;当调用mutex-lock的时候会利用cas操作改为1;
当调用lll_lock时,如果不存在锁冲突,则将其改为1,否则改为2。
对比就知道为什么futex>1 就需要 futex_wake了
/** * futex_wait_setup() - 准备等待在一个futex变量上 * @uaddr: futex用户空间地址 * @val: 期望的值 * @flags: futex标识 (FLAGS_SHARED, etc.) * @q: 关联的futex_q * @hb: hash_bucket的指针 返回给调用者 * * Setup the futex_q and locate the hash_bucket. Get the futex value and * compare it with the expected value. Handle atomic faults internally. * Return with the hb lock held and a q.key reference on success, and unlocked * with no q.key reference on failure. * * Return: * 0 - uaddr contains val and hb has been locked; * <1 - -EFAULT or -EWOULDBLOCK (uaddr does not contain val) and hb is unlocked */ static int futex_wait_setup(u32 __user *uaddr, u32 val, unsigned int flags, struct futex_q *q, struct futex_hash_bucket **hb) { u32 uval; int ret; /* * Access the page AFTER the hash-bucket is locked. * Order is important: * * Userspace waiter: val = var; if (cond(val)) futex_wait(&var, val); * Userspace waker: if (cond(var)) { var = new; futex_wake(&var); } * * The basic logical guarantee of a futex is that it blocks ONLY * if cond(var) is known to be true at the time of blocking, for * any cond. If we locked the hash-bucket after testing *uaddr, that * would open a race condition where we could block indefinitely with * cond(var) false, which would violate the guarantee. * * On the other hand, we insert q and release the hash-bucket only * after testing *uaddr. This guarantees that futex_wait() will NOT * absorb a wakeup if *uaddr does not match the desired values * while the syscall executes. */ retry: //初始化futex_q 填充q->key ret = get_futex_key(uaddr, flags & FLAGS_SHARED, &q->key, VERIFY_READ); if (unlikely(ret != 0)) return ret; retry_private: //1.对q->key进行hash 然后通过&futex_queues[hash & ((1 << FUTEX_HASHBITS)-1)]找到futex_hash_bucket //2. spin_lock(&hb->lock)获得自旋锁 *hb = queue_lock(q); //原子的将uaddr的值拷贝到uval中 ret = get_futex_value_locked(&uval, uaddr); if (ret) { //拷贝操作失败 重试 ... goto retry; } // 如果uval的值(即uaddr)不等于期望值val 则表明其他线程在修改 // 直接返回无需等待 if (uval != val) { queue_unlock(q, *hb); ret = -EWOULDBLOCK; } out: if (ret) put_futex_key(&q->key); return ret; }
futex_wait_setup()
方法主要是为阻塞在uaddr上做准备(uaddr 地址 为pthread_mutex_t 结构中lock值对应的地址),主要步骤:
- 初始化
futex_q
,并初始化futex_q.key
的引用, - 对
futex_q.key
进行hash通过&futex_queues[hash & ((1 << FUTEX_HASHBITS)-1)]
找到futex_hash_bucket
- 调用spin_lock(&hb->lock)尝试获得自旋锁 失败则进行重试回到步骤1
- 判断
*uaddr
的值跟val
是否相等,不相等说明其他线程在修改则释放持有的hb.lock自旋锁,返回-EWOULDBLOCK
(#define EWOULDBLOCK 246 /* Operation would block (Linux returns EAGAIN) */
即linux中的EAGAIN)
** * futex_wait_queue_me() - queue_me() and wait for wakeup, timeout, or signal * @hb: the futex hash bucket, must be locked by the caller * @q: the futex_q to queue up on * @timeout: the prepared hrtimer_sleeper, or null for no timeout */ static void futex_wait_queue_me(struct futex_hash_bucket *hb, struct futex_q *q, struct hrtimer_sleeper *timeout) { // task状态保证在另一个task唤醒它之前被设置,set_current_state利用set_mb()实现 // 设置task状态为TASK_INTERRUPTIBLE CPU只会调度状态为TASK_RUNNING的任务 set_current_state(TASK_INTERRUPTIBLE); //将q插入到等待队列中然后释放自旋锁 queue_me(q, hb); //启动定时器 if (timeout) { hrtimer_start_expires(&timeout->timer, HRTIMER_MODE_ABS); if (!hrtimer_active(&timeout->timer)) timeout->task = NULL; } /* * If we have been removed from the hash list, then another task * has tried to wake us, and we can skip the call to schedule(). */ if (likely(!plist_node_empty(&q->list))) { /* * If the timer has already expired, current will already be * flagged for rescheduling. Only call schedule if there * is no timeout, or if it has yet to expire. */ // 未设置过期时间 或过期时间还未到期才进行调度 if (!timeout || timeout->task) //系统重新进行调度,此时CPU会去执行其他任务,当前任务会被阻塞 schedule(); } // 走到这里说明当前任务被CPU选中执行 __set_current_state(TASK_RUNNING); }
futex_wait_queue_me
主要做了几件事:
- 将设置task状态为
TASK_INTERRUPTIBLE
(CPU只会调度状态为TASK_RUNNING的任务) - 调用
queueme()
将futex_q
插入到等待队列 - 启动定时任务
- 若未设置过期时间或过期时间未到期 重新调度进程
- 获的执行资格 设置task状态为
TASK_RUNNING
总结一下futex_wait
的工作机制:futex_wait_setup()
方法会根据futex_q.key
hash找到对应futex_hash_bucket
,并通过spin_lock(futex_hash_bucket.lock)
获取自旋锁;futex_wait_queue_me()
方法先是将task设置为TASK_INTERRUPTIBLE
然后调用queue_me()
将futex_q
入队,然后才spin_unlock(&hb->lock);
释放持有的自旋锁。也就是说检查*uaddr
值和进程/线程挂起放在了一个临界区中,保证了条件和等待之间的原子性。
以上信息来自:https://jishuin.proginn.com/p/763bfbd55ad5
futex_wake
futex_wake()
唤醒阻塞在*uaddr
上的任务
static int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset) { struct futex_hash_bucket *hb; struct futex_q *this, *next; struct plist_head *head; union futex_key key = FUTEX_KEY_INIT; int ret; if (!bitset) return -EINVAL; //根据uaddr的值填充&key的内容 ret = get_futex_key(uaddr, flags & FLAGS_SHARED, &key, VERIFY_READ); if (unlikely(ret != 0)) goto out; //根据&key获取对应uaddr所在的futex_hash_bucket hb = hash_futex(&key); //对该hb加自旋锁 spin_lock(&hb->lock); head = &hb->chain; //遍历该hb的链表 注意链表中存储的节点时plist_node类型,而这里的this却是futex_q类型 //这种类型转换是通过c中的container_of机制实现的 plist_for_each_entry_safe(this, next, head, list) { if (match_futex (&this->key, &key)) { if (this->pi_state || this->rt_waiter) { ret = -EINVAL; break; } /* Check if one of the bits is set in both bitsets */ if (!(this->bitset & bitset)) continue; //唤醒对应进程 wake_futex(this); if (++ret >= nr_wake) break; } } //释放自旋锁 spin_unlock(&hb->lock); put_futex_key(&key); out: return ret; }
static void wake_futex(struct futex_q *q) { struct task_struct *p = q->task; if (WARN(q->pi_state || q->rt_waiter, "refusing to wake PI futex ")) return; // 在唤醒任务之前将q->lock_ptr设置为NULL // 如果另一个CPU发生了非futex方式的唤醒,则任务可能退出。p解引用则是一个不存在的task结构体 // 为避免这种case,需要持有p引用来进行唤醒 get_task_struct(p); //将q出队列 __unqueue_futex(q); /* * 只要写入q-> lock_ptr = NULL,等待的任务就可以释放futex_q,而无需获取任何锁。 * 这里需要一个内存屏障,以防止lock_ptr的后续存储超越plist_del。 */ smp_wmb(); q->lock_ptr = NULL; // 将TASK_INTERRUPTIBLE | TASK_UNINTERRUPTIBLE状态的task唤醒 wake_up_state(p, TASK_NORMAL); // task释放futex_q put_task_struct(p); }
futex_wake()
流程如下:
- 根据
*uaddr
填充futex_q->key
,调用hash_futex(&key);
查找对应的futex_hash_bucket
- 调用
spin_lock(&hb->lock);
尝试获取hb的自旋锁 - 遍历
futex_hash_bucket
挂载的链表,找到uaddr对应的节点 - 调用
wake_futex
唤起等待的进程,其实就是将task设置为TASK_RUNNING
并放入调度队列中等待CPU调度同时释放futex_q
- 释放自旋锁
此外futex
同步机制即可用于线程之间同步,也可用于进程之间同步。
- 用于线程比较简单,因为线程共享虚拟内存空间,futex变量由唯一的虚拟地址表示,线程即可用虚拟地址访问futex变量
- 用于进程稍微复杂一些,因为进程间是分配的独立的虚拟内存地址空间。需要通过
mmap
让进程可以共享一段地址空间来使用futex变量。故而每个进程访问futex的虚拟地址不一样,但是操作系统知道所有这些虚拟地址映射到同一个表示futex变量的物理地址
思考一下:
更好的方法是减少数据共享, 比如你这里分多个桶加锁就行了, 性能会比单纯用一套lock-free的数据结构更好. 在追求极限性能的场景中,一般考虑的是wait-free的算法和容器, 这能让所有线程同时做有用的事, 在关键代码上相比加锁和lock-free才有较大的区别.
来自:https://www.zhihu.com/question/53303879