大家好,又见面了,我是你们的朋友全栈君。
1、概念
futex: a sort of fast, user-space mutual exclusion primitive.
Futex是一种用户态和内核态混合的同步机制。首先,同步的进程间通过mmap共享一段内存,futex变量就位于这段共享的内存中且操作是原子的,当进程尝试进入互斥区或者退出互斥区的时候,先去查看共享内存中的futex变量,如果没有竞争发生,则只修改futex,而不用再执行系统调用了。当通过访问futex变量告诉进程有竞争发生,则还是得执行系统调用去完成相应的处理(wait 或者 wake up)。简单的说,futex就是通过在用户态的检查,(motivation)如果了解到没有竞争就不用陷入内核了,大大提高了low-contention时候的效率。
https://lwn.net/Articles/172149/
https://lwn.net/Articles/360699/
2、futex的由来
为什么要有futex,他解决什么问题?何时加入内核的?我们来看下
经研究发现,很多同步是无竞争的,即某个进程进入互斥区,到再从某个互斥区出来这段时间,常常是没有进程也要进这个互斥区或者请求同一同步变量的。但是在这种情况下,这个进程也要陷入内核去看看有没有人和它竞争,退出的时侯还要陷入内核去看看有没有进程等待在同一同步变量上。这些不必要的系统调用(或者说内核陷入)造成了大量的性能开销。为了解决这个问题,Futex就应运而生。
前面的概念已经说了,futex是一种用户态和内核态混合同步机制,为什么会是用户态+内核态,听起来有点复杂,由于我们应用程序很多场景下多线程都是非竞争的,也就是说多任务在同一时刻同时操作临界区的概率是比较小的,大多数情况是没有竞争的,在早期内核同步互斥操作必须要进入内核态,由内核来提供同步机制,这就导致在非竞争的情况下,互斥操作扔要通过系统调用进入内核态。
我们来看一下程序
程序1:
pthread_mutex_t lock;
int count = 0;
void thread1()
{
while(1)
{
pthread_mutex_lock(&lock);
/* do something */
count++;
pthread_mutex_unlock(&lock);
}
}
void thread2()
{
while(1)
{
sleep(60);
pthread_mutex_lock(&lock);
count = 0;
pthread_mutex_unlock(&lock);
}
}
pthread_create(&tid1, NULL, thread1, NULL);
pthread_create(&tid2, NULL, thread1, NULL);
假设系统中有2个程序,一个线程在滴答自增,一个线程周期性清除计数器。显然两个线程同时进入临界区的几率相当小,在未实现futex机制之前,每次调用pthread_mutex_lock和unlock都要通过syscall进入内核,内核检查该锁的拥有者,发现没有人持则返回到用户态,因为大部分时间2个线程并没有争抢互斥锁。显然大部分时间在做无用功,时间浪费在user->kernel和kernel→usr的切换,显然这个锁的性能不太好,因为存在大量user→kernel、kernel→usr的切换,例子可能不太恰当,get到点就好。
那么如何解决这个问题?
就像前面说的,采用用户态+内核态混合机制,在用户态使用原子操作,对持有锁的持有情况进行判断,如果锁可以占用,那么更新锁的状态并直接占用,不需要进入内核态,如果锁已经占用,则进入内核态挂起当前任务,事实上这些操作对程序员不可见的,一般都是由C库提实现好了。
Glibc中常用的线程同步方式举例:
Semaphore
变量定义: sem_t sem;
初始化: sem_init(&sem,0,1);
进入加锁: sem_wait(&sem);
退出解锁: sem_post(&sem);
Mutex
变量定义: pthread_mutex_t mut;
初始化: pthread_mutex_init(&mut,NULL);
进入加锁: pthread_mutex_lock(&mut);
退出解锁: pthread_mutex_unlock(&mut);
这些用于同步的函数和futex有什么关系?下面让我们来看一看:
以Semaphores为例,
进入互斥区的时候,会执行sem_wait(sem_t *sem),sem_wait的实现如下:
int sem_wait (sem_t *sem)
{
int *futex = (int *) sem;
if (atomic_decrement_if_positive (futex) > 0)
return 0;
int err = lll_futex_wait (futex, 0);
return -1;
)
atomic_decrement_if_positive()的语义就是如果传入参数是正数就将其原子性的减一并立即返回。如果信号量为正,在Semaphores的语义中意味着没有竞争发生,如果没有竞争,就给信号量减一后直接返回了。
如果传入参数不是正数,即意味着有竞争,调用lll_futex_wait(futex,0),lll_futex_wait是个宏,展开后为:
#define lll_futex_wait(futex, val) \
({ \
…
__asm __volatile (LLL_EBX_LOAD \
LLL_ENTER_KERNEL \
LLL_EBX_LOAD \
: “=a” (__status) \
: “0″ (SYS_futex), LLL_EBX_REG (futex), “S” (0), \
“c” (FUTEX_WAIT), “d” (_val), \
“i” (offsetof (tcbhead_t, sysinfo)) \
: “memory”); \
… \
})
可以看到当发生竞争的时候,sem_wait会调用SYS_futex系统调用,并在val=0的时候执行FUTEX_WAIT,让当前线程休眠。
2002年的ols文档,在linux-2.5.7引入了futex。
https://www.kernel.org/doc/ols/2002/ols2002-pages-479-495.pdf
2、内核配置:
在内核配置选项中可以看到,默认是勾选的。 CONFIG_FUTEX=Y
[*] Enable futex support
CONFIG_FUTEX: │
│ │
│ Disabling this option will cause the kernel to be built without │
│ support for “fast userspace mutexes”. The resulting kernel may not │
│ run glibc-based applications correctly.
从CONFIG_FUTEX的注释中我们可以发现,我们会发现一个问题,注释说如果CONFIG_FUTEX=N,则生成的内核可能不能正常运行glibc应用程序。为什么会这样?
从上面futex例子我们可以看出,在Semaphores和mutex的实现过程中使用了futex,说明glibc库使用了futex,而其他的常用的同步手段也是建立在futex机制上,包括用户态下的操作和核心态下的操作。
未完–待续
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/125323.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...