标签:previous 原子操作 explore 帮助 不清楚 either access str 声明
strace -c -f -S name docker run --rm -itd --name=kata-test --runtime kata-runtime busybox strace: Process 21490 attached strace: Process 21491 attached strace: Process 21492 attached strace: Process 21493 attached strace: Process 21494 attached strace: Process 21495 attached strace: Process 21496 attached strace: Process 21497 attached strace: Process 21498 attached strace: Process 21499 attached strace: Process 21500 attached strace: Process 21501 attached strace: Process 21502 attached strace: Process 21503 attached strace: Process 21504 attached strace: Process 21505 attached strace: Process 21506 attached strace: Process 21507 attached 87130c9c40a85446fb67d21a2704be785f284e8e53eb0cffa051f38a862e97b9 strace: Process 21513 attached % time seconds usecs/call calls errors syscall 0.00 0.000014 5 3 brk 0.00 0.000000 0 1 capget 6.12 0.019506 24 810 clock_gettime 0.08 0.000263 14 19 clone 0.03 0.000094 5 19 close 0.02 0.000052 13 4 2 connect 0.00 0.000013 13 1 epoll_create1 0.01 0.000016 2 8 epoll_ctl 3.44 0.010972 229 48 epoll_pwait 0.00 0.000000 0 1 execve 0.01 0.000042 5 9 9 faccessat 0.01 0.000034 4 9 fcntl 0.02 0.000057 5 12 fstat 80.76 0.257279 220 1172 109 futex 0.00 0.000000 0 2 getpeername 0.01 0.000019 10 2 getpid 0.00 0.000000 0 1 getrandom 0.00 0.000000 0 2 getsockname 0.04 0.000112 6 20 gettid 0.00 0.000000 0 1 gettimeofday 0.00 0.000011 11 1 getuid 0.00 0.000000 0 2 ioctl 0.00 0.000000 0 2 lseek 0.19 0.000600 7 90 madvise 0.19 0.000595 8 79 mmap 0.16 0.000524 10 54 mprotect 0.06 0.000207 12 17 munmap 7.69 0.024495 67 366 nanosleep 0.01 0.000031 1 36 34 newfstatat 0.05 0.000163 10 17 openat 0.00 0.000008 8 1 prlimit64 0.11 0.000351 14 26 6 read 0.02 0.000055 55 1 readlinkat 0.04 0.000121 1 128 rt_sigaction 0.51 0.001629 10 165 rt_sigprocmask 0.00 0.000007 7 1 sched_getaffinity 0.24 0.000779 5 146 sched_yield 0.03 0.000110 6 20 set_robust_list 0.00 0.000006 6 1 set_tid_address 0.00 0.000000 0 2 setsockopt 0.09 0.000297 7 40 sigaltstack 0.02 0.000062 16 4 socket 0.00 0.000014 14 1 uname 0.01 0.000040 8 5 write
Futex (全称 “fast userspace mutex”) 机制由 IBM 在 2002 年 1 提出;在 2003 年并入主线内核。它的核心思想是尽可能地减少内核的参与,在用户空间使用一种更有效的方法来进行线程的同步。
在这篇文章中给出了 futex 的概述:他们是怎么工作的,以及他们如何在高层次的 APIs 与语言中是实现同步原语的。
重要的声明:futexes 在 Linux 内核中属于非常低层次的特性,适合在诸如 C/C++ 标准库这种基础运行时组件中使用。非常不建议在应用代码中使用它。
在介绍 futex 之前,资源的互斥需要使用系统调用(比如 semop
)。因为需要从用户空间切换到内核空间,因此使用系统调用的代价相对昂贵;程序由串行转向并行,与锁有关的操作在总运行的时间中占据了很大的一部分比例。不幸的是,锁并不参与完成任何实际的工作(“业务逻辑”),所作的只有保证对共享资源的访问是安全的。
Futex 的提出基于一个观察:大部分情况下锁并没有被争用。如果一个线程试图获取一个空闲的锁,持有它的代价是非常的廉价,这是因为此时很有可能不存在其他的线程试图获取它。因此,我们可以通过先尝试更廉价的原子操作 2,在没有系统调用参与的情况下完成。原子操作很大可能会成功。
但是,总有可能真的出现另一个线程在同时尝试获取锁的事件,这种情况下原子操作可能会失败。此时有两种选择。一种是再次尝试,直到加锁成功(busy-loop);尽管全是用户空间的操作,但它也可能非常浪费,毕竟在加锁成功之前可能需要经过很长的时间,这段时间里会一直占有 CPU 资源。另一个可选的方法是在锁被解除(至少大概率下它未被持有)之前 “睡眠”;这需要内核的帮助,也就是 futex 使用的地方。
futex (2) 系统调用在单个 API 上复用了很多功能。我在这不会讨论任何的高级功能(一些功能相当深奥,也没有再官方文档中提及),仅仅针对 FUTEX_WAIT
与 FUTEX_WAKE
。手册页(man page)给了很好的描述:
The
futex()
system call provides a method for waiting until a certain
condition becomes true. It is typically used as a blocking construct
in the context of shared-memory synchronization. When using futexes,
the majority of the synchronization operations are performed in user
space. A user-space program employs thefutex()
system call only
when it is likely that the program has to block for a longer time
until the condition becomes true. Otherfutex()
operations can be
used to wake any processes or threads waiting for a particular
condition.
简单地说,futex 是内核的构造,用于帮助用户空间中的代码在共享的事件中同步。一些用户态的进程(或者线程)会等待事件(FUTEX_EVENT
)。其他用户态进程可以发送事件(FUTEX_WAKE
)来通知等待事件的进程。等待的效率很高 —— 在等待过程中它被内核停止执行,只有唤醒信号发来的时候它才被重新调度。
一定要阅读 futex
的手册页;这篇文章不适合当作文档!至少需要阅读 FUTEX_WAIT
与 FUTEX_WAKE
的调用、参数、返回值与可能的错误。
下面是一个例子,演示了 futex 在进程间同步的基本使用方法。main
函数启动了子进程,做了如下的事情:
0xA
的写入0xB
写入共享内存中同时,父进程:
0xA
写入共享内存0xB
的写入这是俩进程之间简单的握手。下面是相关代码:
1
|
int main(int argc, char** argv) {
|
Note,POSIX 共享内存 API 创建了一块共享内存,映射到了两个进程中。因为同一块物理内存在不同的进程中的虚拟地址可能不同 3,因此无法使用常规的指针。
这并非 futex
典型的使用方法,更好的方式是从某处等待一个值,而非到某处等待。这里仅仅是展示 futex
返回值的各种可能性。稍后会在实现 mutex 的时候展示更规范的用法。
这里是 wait_on_futex_value
:
1
|
void wait_on_futex_value(int* futex_addr, int val) {
|
这个函数的在 futex
系统调用之上添加循环的作用是,当唤醒是假的时候可以继续等待。这种情况发生在当 val
并非期待值,或者其他进程在该进程之前被唤醒(这种情况无法在演示中发生,单很可能在其他场景下发生)。
Futex 的语义很棘手 4。若 futex 地址中的值不等于 val
,FUTEX_WAIT
会立即返回。在我们的例子中,如果紫禁城在父进程写入 0xA
之前发起了等待,这种场景便会发生。例如,这种情况下,futex
调用会返回 EAGAIN
。
这是 wake_futex_blocking
:
1
|
void wake_futex_blocking(int* futex_addr) {
|
这是 FUTEX_WAKE
的封装,无论有多少等待者被唤醒他通常都会立即返回。在我们的例子中,这种等待是握手的一部分,但在许多情况下你不会看到它。
简单地说,futex 是内核为了便于用户空间代码管理的队列。它使不满足特定条件的用户空间代码睡眠,让其他用户空间代码发送消息唤醒等待的进程。之前我们提到使用 busy-loop 的方法来等待原子操作的成功执行;内核管理的队列作为一种可选的高效方案,解决了用户空间代码在循环等待过程中对 CPU 资源过多占用的情况。
下图来自 LWN “A futex overview and update”:
在 Linux 内核中,futex 相关源码位于 kernel/futex.c
。内核维护一个由地址作为关键字的 hash table,通过它来快速查找队列中正确的数据结构,再把调用的进程添加进查找到的等待队列中。当然,因此内核本身的细粒度锁以及各种高级特性,因此 futex 本身存在相当多的复杂性。
futex
系统调用有参数 timeout
,它可以使等待的进程超时返回。
futex-wait-timeout
例子展示了这种用法。下面是与子进程等待 futex 有关的源码:
1
|
printf("child waiting for A\n");
|
若等待时间超过 500ms,进程会再次等待。这个例子可以让你设置等待时间以便观察等待效果。
在开篇的动机部分,我解释了 futex 任何在常见的低争用情况下实现有效的互斥。是时候展示使用 futex 与原子操作对此的实现了。这基于 Ulrich Drepper 的 *”Futexes are Tricky”* 论文的第二种实现。
为了使用标准的原子操作,这里使用 C++(C++11 可用)。所有的这里下面是最重要的部分:
1
|
class Mutex {
|
cmpchg
是对 C++ 原子操作原语的封装:
1
|
// An atomic_compare_exchange wrapper with semantics expected by the paper‘s
|
代码中的注释解释了它如何工作的;十分建议阅读 Drepper 的论文,因为它通过一个更的错误实现来构建了这个实现。这段代码中,传递给 futex
系统调用的一个参数类 int *
,这它是由 std::atomic
类型的 atom_
地址强制转换得到的。这是因为 fute期望一个简单的地址,但是C++原子类型封装了未知的数据类型。这段代码可以在x64Linux上工作,却是不可移植的。为了使
std::atomic 与
futex 的可移植性更好,我须添加一个抽象层。但这并非实践中的需要——
futex` 与 C++11 的协同并非每个人都应的。这段代码仅仅用来演示。
有意思的是成员 atom_
的含义。回想一下,futex
系统调用不会赋值 —— 赋值操作取决用户。0、1、2 约定对 mutex 非常有用,也是 glibc 对低级锁(low-level)实现的方式。
这里带来的是 glibc 中 POSIX 线程与之有关的实现,它的类型是 pthread_mutex_t
。如篇所述,futex 并非用于普通用户的代码,它被低级的运行时(runtime)和库(libraries 来实现高级的原语。这里会看到 NPTL 中有关 mutex 的使用。在 glibc 中的源码树中,这部分代码在 nptl/pthread_mutex_lock.c
。
因为需要对不同类型的 mutex 支持,这部分代码异常复杂,但如果挖得足够深,我们会发许多熟悉的身影。除了上面提到的文件外,还有(针对 x86sysdeps/unix/sysv/linux/x86_64/lowlevellock.h
与 nptl/lowlevellock.c
。这些码复杂,单原子操作 compare-and-exchange 与 futex
的组合是显而易见的。低级锁机(lll_
或者 LLL_
前缀)被广泛用在 glibc 中,不仅仅是 POSIX 线程。
sysdeps/nptl/lowlevellock.h
中开头的注释应该是非常熟悉了:
1
|
/* Low-level locks use a combination of atomic operations (to acquire and
|
Go 的运行时大部分情况下不使用 libc。因此它在自己的代码中无法以来 POSIX 线程。它直接调用系统调用。
这也是另一个不错地学习 futex 使用方法的例子。它无法使用 pthread_mutex_t
,所以它必须实现自己的锁。相关的实现是 sync.Mutex
类型(在 src/sync/mutex.go
)中。
sync.Mutex
的 Lock
方法非常复杂。它首先使用原子交换尝试获取锁。如果必须等待,则会转向 runtime_SemacquireMutex
,在其中调用 runtime.lock
。这个函数定义在 src/runtime/lock_futex.go
5,其中定义的常量非常熟悉。
1
|
const (
|
runtime.lock
也尝试预测性地使用原子操作获取锁;这个函数在 Go 运行时中的某些地方使用,它的存在是有意义的,但是我不清楚当它被 Mutex.lock
调用的时候,是否无法优化两个连续的原子操作。
若它发现不得不睡眠,会跳转 futexsleep
,它是 OS 相关的代码,在 src/runtime/os_linux.go
中。这个函数调用具有 FUTEX_WAIT_PRIVATE
的 futex
系统调用(这对于单个进程的 Go 运行时足够了)。
cmpxhg
is an instruction. Whileit’s not as cheap as non-atomic instructions (especially in multi-core systems),it’s significantly cheaper than system calls.futex(2)
system call. The Go runtime has afallback onto the semaphore system calls if futex
is not supported.Futex system calls taking too much time
标签:previous 原子操作 explore 帮助 不清楚 either access str 声明
原文地址:https://www.cnblogs.com/dream397/p/13902388.html