futex机制介绍

如题所述

第1个回答  2022-06-30
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