1 spin_lock
自旋锁的实现是为了保护一段短小的临界区操作代码,保证这个临界区的操作是原子的,从而避免并发的竞争冒险。在Linux内核中,自旋锁通常用于包含内核数据结构的操作,你可以看到在许多内核数据结构中都嵌入有spinlock,这些大部分就是用于保证它自身被操作的原子性,在操作这样的结构体时都经历这样的过程:上锁-操作-解锁。
如果内核控制路径发现自旋锁“开着”(可以获取),就获取锁并继续自己的执行。相反,如果内核控制路径发现锁由运行在另一个CPU上的内核控制路径“锁着”,就在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被释放。 自旋锁是循环检测“忙等”,即等待时内核无事可做(除了浪费时间),进程在CPU上保持运行,所以它保护的临界区必须小,且操作过程必须短。不过,自旋锁通常非常方便,因为很多内核资源只锁1毫秒的时间片段,所以等待自旋锁的释放不会消耗太多CPU的时间。
自旋锁需要阻止在代码运行过程中出现的任何并发干扰。这些“干扰”包括: 中断,包括硬件中断和软件中断 (仅在中断代码可能访问临界区时需要),内核抢占(仅存在于可抢占内核中),其他处理器对同一临界区的访问 (仅SMP系统),spin lock需要在芯片底层实现物理上的内存地址独占访问,并且在实现上使用特殊的汇编指令访问。请看参考资料中对于自旋锁的实现分析。以arm为例,从存在SMP的ARM构架指令集开始(V6、V7),采用LDREX和STREX指令实现真正的自旋等待。
深入分析_linux_spinlock_实现机制 Linux内核同步 - spin_lock
2 semaphore
由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.
举个例子,就是 两个进程共享信号量sv,一旦其中一个进程执行了P(sv)操作,它将得到信号量,并可以进入临界区,使sv减1。而第二个进程将被阻止进入临界区,因为 当它试图执行P(sv)时,sv为0,它会被挂起以等待第一个进程离开临界区域并执行V(sv)释放信号量,这时第二个进程就可以恢复执行。
信号量其相应的接口也有两种(posix信号量)(systemv信号量 )这两者之间略有区别, systemv实际上是一个信号量组,posix信号量),两者有很大的区别。
1 system V的信号量是信号量集,可以包括多个信号灯(有个数组),每个操作可以同时操作多个信号灯posix是单个信号灯,POSIX有名信号灯支持进程间通信,无名信号灯放在共享内存中时可以用于进程间通信。
2 POSIX信号量在有些平台并没有被实现,比如:SUSE8,而SYSTEM V大多数LINUX/UNIX都已经实现。两者都可以用于进程和线程间通信。但一般来说,system v信号量用于 进程间同步、有名信号灯既可用于线程间的同步,又可以用于进程间的同步、posix无名用于同一个进程的不同线程间,如果无名信号量要用于进程间同步,信号量要放在共享内存中。
3 POSIX有两种类型的信号量,有名信号量和无名信号量。有名信号量像system v信号量一样由一个名字标识。
4 POSIX通过sem_open单一的调用就完成了信号量的创建、初始化和权限的设置,而system v要两步。也就是说posix 信号是多线程,多进程安全的,而system v不是,可能会出现问题。
5 system V信号量通过一个int类型的值来标识自己(类似于调用open()返回的fd),而sem_open函数返回sem_t类型(长整形)作为posix信号量的标识值。
6 对于System V信号量你可以控制每次自增或是自减的信号量计数,而在Posix里面,信号量计数每次只能自增或是自减1。
7 Posix无名信号量提供一种非常驻的信号量机制。
8 相关进程: 如果进程是从一已经存在的进程创建,并最终操作这个创建进程的资源,那么这些进程被称为相关的。
基于system V是一个信号量集并且不单单只能自增自减1,我们就可以用两个信号量一个读信号量和写信号量组成一个信号量集实现一个基于进程的读写锁,很简‘单,读的时候把读信号量加一(p操作),写的时候把写信号量加一,但是读之前要等写信号量为减为0(v操作),写之前要等到读信号量和写信号量都减为0。
linux 2.6 内核结构
struct semaphore {
spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
semaphore工作原理及其使用案例 进程间通信之-信号量semaphore--linux内核剖析
3 mutex
mutex是用作互斥的,而semaphore是用作同步的。如果不去深究linux内核mutex和信号量的底层实现,在其功能上可以理解为mutex是一个受限制的信号量,也就是说,mutex一定是为0或者1,而semaphore可以是任意的数,所以如果使用mutex,那第一个进入临界区的进程一定可以执行,而其他的进程必须等待。而semaphore则不一定,如果一开始初始化为0,则所有进程都必须等待。互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
linux 2.6 内核结构
struct mutex {
atomic_t count; //引用计数器,1: 所可以利用,小于等于0:该锁已被获取,需要等待
spinlock_t wait_lock;//自旋锁类型,保证多cpu下,对等待队列访问是安全的。
spinlock_t wait_lock; //等待队列,如果该锁被获取,任务将挂在此队列上,等待调度。
struct list_head wait_list;
};
linux 2.6 互斥锁的实现-源码分析
信号量mutex是sleep-waiting。 就是说当没有获得mutex时,会有上下文切换,将自己、加到忙等待队列中,直到另外一个线程释放mutex并唤醒它,而这时CPU是空闲的,可以调度别的任务处理。
而自旋锁spin lock是busy-waiting。就是说当没有可用的锁时,就一直忙等待并不停的进行锁请求,直到得到这个锁为止。这个过程中cpu始终处于忙状态,不能做别的任务。
Mutex适合对锁操作非常频繁的场景,并且具有更好的适应性。尽管相比spin lock它会花费更多的开销(主要是上下文切换),但是它能适合实际开发中复杂的应用场景,在保证一定性能的前提下提供更大的灵活度。spin lock的lock/unlock性能更好(花费更少的cpu指令),但是它只适应用于临界区运行时间很短的场景。
而在实际软件开发中,除非程序员对自己的程序的锁操作行为非常的了解,否则使用spin lock不是一个好主意(通常一个多线程程序中对锁的操作有数以万次,如果失败的锁操作(contended lock requests)过多的话就会浪费很多的时间进行空等待)。更保险的方法或许是先(保守的)使用 Mutex,然后如果对性能还有进一步的需求,可以尝试使用spin lock进行调优。毕竟我们的程序不像Linux kernel那样对性能需求那么高(Linux Kernel最常用的锁操作是spin lock和rw lock)。
应用场景
低开销加锁 优先使用自旋锁
短期锁定 优先使用自旋锁
长期加锁 优先使用信号量
中断上下文中加锁 使用自旋锁
持有锁是需要睡眠、调度 使用信号量
最后说到futex
当然上面提到的内核数据结构如果我们不是开发linux内核,我们在实际开发中不可能直接操作这些数据结构,我们只能调用glibc接口。glibc是GNU发布的libc库,即c运行库。glibc是linux系统中最底层的api,几乎其它任何运行库都会依赖于glibc。glibc除了封装linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。由于 glibc 囊括了几乎所有的 UNIX通行的标准,可以想见其内容包罗万象。不管任何语言实现的任何各种各样的锁都是基于glibc和汇编来实现的,因为几乎所有的现代编程语言都是用c或者c++来写的,当你去看glibc的源码时你会看到pthread和信号量等接口的实现都出现了futex这个东西。这个东西有什么用呢?
Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是通过在用户态的检查,(motivation)如果了解到没有竞争就不用陷入内核了,大大提高了low-contention时候的效率。
为什么要有futex, 经研究发现,很多同步是无竞争的,即某个进程进入互斥区,到再从某个互斥区出来这段时间,常常是没有进程也要进这个互斥区或者请求同一同步变量的。但是在这种情况下,这个进程也要陷入内核去看看有没有人和它竞争,退出的时侯还要陷入内核去看看有没有进程等待在同一同步变量上。这些不必要的系统调用(或者说内核陷入)造成了大量的性能开销。为了解决这个问题,Futex就应运而生。前面的概念已经说了,futex是一种用户态和内核态混合同步机制,为什么会是用户态+内核态,听起来有点复杂,由于我们应用程序很多场景下多线程都是非竞争的,也就是说多任务在同一时刻同时操作临界区的概率是比较小的,大多数情况是没有竞争的,在早期内核同步互斥操作必须要进入内核态,由内核来提供同步机制,这就导致在非竞争的情况下,互斥操作扔要通过系统调用进入内核态。
其实就是一句话,通过futex我们可以避免无谓的互斥操作,大大增加同步效率。
linux内核级同步机制--futex futex 手册摘要