robust-futex-1-内核文档翻译
一、robust-futexes.txt
注: 翻译自 msm-4.14/Documentation/robust-futexes.txt
===========================================
关于什么是 robust futex 的描述
===========================================
发起人:Ingo Molnar <mingo@redhat.com>
背景
----------
什么是 robust futex(健壮 futex)?要回答这个问题,我们首先需要了解什么是 futex:普通 futex 是一种特殊类型的锁,在非竞争情况下,可以从用户空间获取/释放,而无需进入内核。
futex 本质上是一个用户空间地址,例如一个 32 位的锁变量字段。如果用户空间发现争用(锁已被拥有,但其他人也想抢占它),则该锁会被标记为“有等待者”,并调用 sys_futex(FUTEX_WAIT) 系统调用等待其他人释放它。内核会在内部创建一个“futex queue”,以便稍后将等待者和唤醒者进行匹配——而无需他们彼此了解。当所有者线程释放 futex 时,它会(通过变量值)注意到有等待者的等待,并调用 sys_futex(FUTEX_WAKE) 系统调用唤醒他们。一旦所有等待者都获取并释放了锁,futex 就会再次回到“无争用”状态,并且不会再有与之关联的内核状态。内核会完全忘记该地址上曾经存在过 futex。这种方法使得 futex 非常轻量且可扩展。
“稳健性”是指在持有锁时处理崩溃:如果一个进程在持有 pthread_mutex_t 锁时过早退出,而该锁也与其他进程共享(例如,yum 在持有 pthread_mutex_t 时发生段错误,或者 yum 被 kill -9-ed),那么需要通知该锁的等待者,锁的最后一个所有者以某种不规则的方式退出了。
旧的废弃的方法:
为了解决此类问题,创建了“健壮互斥锁(robust mutex)”用户空间 API:如果所有者过早退出,pthread_mutex_lock() 将返回错误值 - 并且新所有者可以决定是否可以安全地恢复受锁保护的数据。
然而,基于 futex 的互斥锁存在一个很大的概念问题:内核会销毁 Owner task(例如由于 SEGFAULT),但内核却无法进行清理:如果没有“futex queue”(大多数情况下都没有,因为 futex 是快速轻量级的锁)就是没有任务被阻塞的情况下,那么在持有锁之后内核就没有任何信息可以清理!用户空间也没有机会在锁之后进行清理——用户空间是崩溃的一方,所以它没有机会进行清理。这真是个两难的困境。
实际上,例如当 yum 被 kill -9 (或出现段错误)时,需要重启系统才能释放基于 futex 的锁。这是 yum 的主要 bug 报告之一。
为了解决这个问题,传统的方法是扩展 vma(虚拟内存区域描述符)的概念,使其包含“挂起的健壮 futex 连接到此区域”的概念。这种方法需要为 sys_futex() 添加三个新的系统调用变体:FUTEX_REGISTER、FUTEX_DEREGISTER 和 FUTEX_RECOVER(注:msm-4.14内核上没有这三个命令)。在 do_exit() 时,会搜索所有 vma,检查它们是否设置了 robust_head。这种方法存在两个根本问题:
- 它存在相当复杂的锁定和竞争场景。基于 vma 的方法虽然已经搁置多年,但仍然不够可靠。
- 它们必须在 sys_exit() 时扫描每个线程的所有 vma!
第二个缺点是真正的致命伤:pthread_exit() 在 Linux 上大约需要 1 微秒,但由于有数千(或数万)个 vmas,因此每个 pthread_exit() 都需要一毫秒或更长时间,还会完全破坏 CPU 的 L1 和 L2 缓存!
即使对于正常进程 sys_exit_group() 调用,这一点也非常明显:内核必须无条件地执行 vma 扫描!(这是因为内核不知道有多少 robust futex 需要清理,因为 robust futex 可能已经在另一个任务中注册,而 futex 变量可能只是通过 mmap() 映射到这个进程的地址空间中)。####
这种巨大的开销迫使我们创建了 CONFIG_FUTEX_ROBUST(注:msm-4.14中也没有这个配置),以便普通内核可以将其关闭。但更糟糕的是:这种开销使得robust futex 对于任何类型的通用 Linux 发行版来说都不切实际。
所以必须采取措施。
robust futex 的新方法
---------------------------
这种新方法的核心是,每个线程都有一个私有的健壮锁列表,这些锁由用户空间持有(由 glibc 维护)——该用户空间列表通过新的系统调用注册到内核(此注册在线程生命周期内最多进行一次)。在 do_exit() 时,内核会检查这个用户空间列表:是否有需要清理的 robust futex 锁?
通常情况下,在 do_exit() 时,没有注册列表,因此 robust futex 的开销只是简单的 current->robust_list != NULL 比较。如果线程注册了一个列表,通常列表为空。如果线程/进程崩溃或以某种错误方式终止,列表可能不为空:在这种情况下,内核会仔细检查列表(不信任它),并使用 FUTEX_OWNER_DIED 位标记该线程拥有的所有锁,并唤醒一个等待者(如果有)。
该列表保证在 do_exit() 时是私有的并且是每个线程的,因此内核可以以无锁的方式访问它。
不过,可能存在一种竞争:由于在 glibc 获取 futex 之后,列表的添加和删除操作都会完成,因此线程(或进程)可能会因为一些指令窗口而挂起,导致 futex 挂起。为了防止这种情况发生,用户空间(glibc)还维护了一个简单的每线程“list_op_pending”字段,以便在线程获取锁后、即将将自己添加到列表之前挂起时,内核可以进行清理。Glibc 在尝试获取 futex 之前会设置这个 list_op_pending 字段,并在列表添加(或列表删除)完成后清除它。
这就是所需的一切——所有剩余的 robust-futex 清理工作都在用户空间完成(就像之前的补丁一样)。
Ulrich Drepper 已经为这一新机制实现了必要的 glibc 支持,从而完全支持健壮互斥体。
与基于 vma 的方法相比,这种基于用户空间列表的方法的主要区别如下:
- 速度更快:线程退出时,无需像基于VM的方法那样循环遍历每个vma(!)。只需执行一个非常简单的“判断列表是否为空”操作即可。
- 无需更改VM - 'struct address_space' 保持不变。
- 无需注册单个锁:健壮互斥锁(robust mutexes)不需要任何额外的每锁的系统调用。因此,健壮互斥锁成为一种非常轻量级的原语 - 因此它们不会迫使应用程序设计者在性能和健壮性之间做出艰难的选择 - 健壮互斥锁的速度同样快。
- 无需为每个锁分配内核空间。
- 无需资源限制。
- 无需内核空间恢复调用 (FUTEX_RECOVER)。
- 实现和锁定“显而易见”,并且无需与虚拟机交互。
性能
-----------
我使用新方法(在 2GHz CPU 上)对内核处理 100 万个(!)已持有锁列表所需的时间进行了基准测试:
- 设置 FUTEX_WAIT [争用互斥锁]:130 毫秒
- 不设置 FUTEX_WAIT [非争用互斥锁]:30 毫秒
我还测量了另一种方法,其中 glibc 负责锁通知(目前它对 !pshared 健壮互斥锁也这么做),耗时 256 毫秒——显然更慢,因为用户空间必须执行 100 万次 FUTEX_WAKE 系统调用。
(100 万个已持有锁的情况是闻所未闻的——我们预计一次最多只会持有少量锁。尽管如此,很高兴知道这种方法具有良好的可扩展性。)
实现细节
----------------------
该补丁添加了两个新的系统调用:一个用于注册用户空间列表,另一个用于查询已注册的列表指针:
asmlinkage long sys_set_robust_list(struct robust_list_head __user *head, size_t len); asmlinkage long sys_get_robust_list(int pid, struct robust_list_head __user **head_ptr, size_t __user *len_ptr);
列表注册非常快:指针只需存储在 current->robust_list 中即可。[请注意,如果未来 robust futex 变得广泛,我们可以扩展 sys_clone() ,为新线程注册一个健壮列表头,而无需再次进行系统调用。]
因此,对于不使用健壮 futex 的任务来说,开销几乎为零。即使对于使用健壮 futex 的任务来说,每个线程生命周期也只需要一次额外的系统调用,并且清理操作(如果发生)快速而直接。内核在健壮 futex 和普通 futex 之间没有任何内部区别。
如果在退出时发现 futex 被持有,内核会设置 futex 字的以下位:
#define FUTEX_OWNER_DIED 0x40000000
并唤醒下一个 futex 等待者(如果有)。用户空间完成其余的清理工作。
否则,glibc 会通过将 TID 原子地放入 futex 字段来获取健壮的 futex。等待者会设置 FUTEX_WAITERS 位:
#define FUTEX_WAITERS 0x80000000
其余位用于 TID。
测试,架构支持
-----------------------------
我已经在 x86 和 x86_64 上测试了新的系统调用,并确保即使列表被故意破坏,用户空间列表的解析仍然是健壮的 [ ;-) ]。
i386 和 x86_64 的系统调用目前已连接,Ulrich 已经在 x86_64 和 i386 上测试了新的 glibc 代码,并且它适用于他的健壮互斥测试用例。
所有其他架构也应该可以顺利构建 - 但它们目前还没有新的系统调用。
架构需要在编写系统调用之前实现新的 futex_atomic_cmpxchg_inatomic() 内联函数。
二、robust-futex-ABI.txt
注: 翻译自 msm-4.14/Documentation/robust-futex-ABI.txt
====================
robust futex ABI
=====================
作者:Paul Jackson <pj@sgi.com> 发起
Robust_futexes 提供了一种机制,除了普通的 futexes 之外,还可以用于在任务退出时协助内核清理持有的锁。
关于线程持有哪些 futexes 的数据保存在用户空间的一个链表中,在获取和释放锁时,这些数据可以高效地更新,而无需内核干预。除了 futexes 所需的额外内核干预之外,robust_futexes 唯一需要的额外内核干预是:
1) 每个线程调用一次,告知内核其持有的 robust_futexes 列表从哪里开始;
2) 退出时调用内核内部代码,处理退出线程持有的任何挂在列表上的锁。
现有的常规 futex 已经提供了“快速用户空间锁定”机制,该机制无需系统调用即可处理无竞争锁定,并通过在内核中维护等待线程列表来处理竞争锁定。 sys_futex(2) 系统调用中的选项支持等待特定的 futex,以及唤醒特定 futex 上的下一个等待线程。
为了使 robust_futexes 正常工作,用户代码(通常位于与应用程序链接的库中,例如 glibc)必须按照内核的期望管理和放置必要的列表元素。如果未能做到这一点,则错误列出的锁在退出时将不会被清理,这可能会导致等待同一锁的其他线程出现死锁或其他类似的故障。
预计可能使用 robust_futexes 的线程应首先发出系统调用:
asmlinkage long sys_set_robust_list(struct robust_list_head __user *head, size_t len);
指针“head”指向线程地址空间中由三个字组成的结构体。每个字在 32 位架构下为 32 位,在 64 位架构下为 64 位,并采用本地字节序。每个线程应拥有自己私有的线程“head”。
如果一个线程在 64 位原生架构内核上以 32 位兼容模式运行,那么它实际上可以有两个这样的结构 - 一个使用 32 位字表示 32 位兼容模式,另一个使用 64 位字表示 64 位原生模式。如果内核是支持 32 位兼容模式的 64 位内核,并且已调用相应的 sys_set_robust_list() 设置该列表,则内核将在每次任务退出时尝试处理这两个列表。
(1) 内存结构中“head”处的第一个字包含一个指向“锁条目”单链表的指针,每个锁对应一个条目(该线程持有的每个锁对应一个条目,没持有的没有),如下所述。如果链表为空,则该指针将指向自身,即“head”。最后一个“锁条目”则指向“head”。
(2) 第二个字称为“offset”,指定与关联“锁条目”地址的偏移量,加上或减去“lock word”的值。与上述其他字不同,“lock word”始终是一个 32 位字。“lock word”的高 3 位包含 3 个标志位,低 29 位包含持有锁的线程的线程 ID (TID)。有关标志位的描述,请参阅下文。
(3) 第三个字称为“list_op_pending”,包含列表插入和删除期间“锁定条目”地址的临时副本,并且需要正确解决在锁定或解锁操作过程中线程退出时的竞争。
从“head”开始的单链表中的每个“锁条目”仅由一个单字(single word)组成,指向下一个“锁条目”,如果没有其他条目,则指回“head”。此外,在每个“锁条目”附近,在“offset”字段指定的“锁条目”偏移量处,都有一个“lock word”。
“lock word”始终为 32 位,旨在与 futex 机制结合使用,并与 robust_futexes 使用的 32 位锁变量相同。只有当下一个线程使用 futex 机制向内核注册了该“lock word”的地址时,内核才能在线程退出时唤醒下一个等待锁的线程。
对于当前由线程持有的每个 futex 锁,如果该线程希望使用 robust_futex 支持来清理该锁,则该锁应该在此列表中拥有一个“锁条目”,并在指定的“offset”处与其关联的“lock word”关联。如果某个线程在持有任何此类锁时死亡,内核将遍历此列表,用一个位标记任何此类锁,以指示其持有者已死亡,并使用 futex 机制唤醒下一个等待该锁的线程。
当一个线程调用上述系统调用来表明它预期使用 robust_futexes 时,内核会存储该任务传入的 head 指针。该任务稍后可以使用以下系统调用来获取该值:
asmlinkage long sys_get_robust_list(int pid, struct robust_list_head __user **head_ptr, size_t __user *len_ptr);
预计线程将使用嵌入在更大的用户级锁定结构中的 robust_futexes,每个锁对应一个。内核 robust_futex 机制不关心该结构中还有什么,只要该线程使用的所有 robust_futexes 的“offset”与“lock word”相同即可。线程应该使用“锁条目”指针链接其当前持有的锁。锁之间也可能存在其他链接,例如双向链表的反向链接,但这对内核来说无关紧要。
通过以这种方式保持其锁链接,在以内核已知的“head”指针开头的列表中,内核可以为线程提供 robust_futexes 可用的基本服务,这有助于清理在(可能意外地)退出时持有的锁。//I: 某个线程在持锁后执行完或调用exit()主动退出?TODO:这个要实验一下。
在正常操作期间,实际的锁定和解锁完全由竞争线程中的用户级代码处理,并由现有的 futex 机制等待和唤醒锁。内核在 robust_futexes 中唯一必要的操作是记住列表“head”的位置,并在线程退出时遍历列表,处理离开线程仍然持有的锁,如下所述。
在给定时间点,线程共享内存中各种数据结构上可能存在数千个 futex 锁结构。只有当前线程持有的锁结构才应该在给定时间内位于该线程的 robust_futex 锁链表中。####
用户共享内存区域中的给定 futex 锁结构可能在不同时间被任何有权访问该区域的线程持有。当前持有此类锁的线程(如果有)会在其“lock word”的低 29 位中用线程 TID 进行标记。
当从其持有锁列表中添加或删除锁时,为了使内核能够正确处理锁清理,而不管任务何时退出(也许在操作此列表的过程中收到意外的信号 9),用户代码必须遵守有关“锁条目”插入和删除的以下协议:####
插入时:
1) 将 'list_op_pending' 字段设置为要插入的 'lock entry' 的地址;
2) 获取 futex 锁;
3) 将 'lock word' 的低 29 位表示该锁条目的线程ID (TID),添加到从 'head' 开始的链表中;
4) 清除 'list_op_pending' 字段。
移除时:
1) 将 'list_op_pending' 字段设置为要移除的 'lock entry' 的地址;
2) 从 'head' 列表中移除此锁的锁条目;
3) 释放 futex 锁;
4) 清除 'lock_op_pending' 字段。
退出时,内核会考虑存储在“list_op_pending”中的地址,以及从“head”开始遍历列表找到的每个“lock word”的地址。对于每个这样的地址,如果该地址偏移量为“offset”处的“lock word”的低 29 位等于退出线程的 TID,则内核将执行以下两项操作:
1) 如果该字中的第 31 位 (0x80000000 = FUTEX_WAITERS) 被设置,则尝试在该地址上进行 futex 唤醒,这将唤醒下一个已使用 futex 机制在该地址上等待的线程;
2) 原子地设置“lock word”中的第 30 位 (0x40000000 = FUTEX_OWNER_DIED)。
上例中,该锁上的 futex 等待者设置了第 31 位,表示它们正在等待;内核设置了第 30 位,表示锁拥有者在持有锁时死亡。
如果出现以下情况,内核退出代码将静默停止进一步扫描列表:
1) “head”指针或后续链表指针不是用户空间字的有效地址;
2) “lock word”的计算位置(地址加上“offset”)不是 32 位用户空间字的有效地址;
3) 如果列表包含的元素超过 100 万个(未来内核配置可能会有所更改)。
当内核发现某个列表条目的“lock word”的低 29 位不包含当前线程的 TID 时,它将不对该条目执行任何操作,而是继续处理下一个条目。
“lock word”的第 29 位 (0x20000000) 保留以备将来使用。
三、man set_robust_list
man get_robust_list 是同一个注释文档。
1. NAME
get_robust_list, set_robust_list - 获取/设置健壮 futexes 列表
2. SYNOPSIS
#include <linux/futex.h> #include <sys/types.h> #include <syscall.h> long set_robust_list(struct robust_list_head *head, size_t len); long get_robust_list(int pid, struct robust_list_head **head_ptr, size_t *len_ptr);
3. DESCRIPTION
这些系统调用处理每个线程的健壮 futex 列表。这些列表在用户空间进行管理,内核只知道列表头的位置####。线程可以使用 set_robust_list() 通知内核其健壮 futex 列表的位置。线程的健壮 futex 列表的地址可以使用 get_robust_list() 获取。
健壮 futex 列表的目的是确保如果一个线程在终止或调用 execve(2) 之前意外解锁 futex 失败,另一个正在等待该 futex 的线程会收到通知,告知该 futex 的前一个所有者已死亡####。此通知包含两部分:在 futex 字中设置 FUTEX_OWNER_DIED 位,并且内核对其中一个正在等待该 futex 的线程执行 futex(2) 的 FUTEX_WAKE 操作####。
get_robust_list() 系统调用返回由 pid 指定的线程 ID 对应的线程的健壮 futex 列表头。如果 pid 为 0,则返回调用线程的列表头。列表头存储在 head_ptr 指向的位置。**head_ptr 指向的对象的大小存储在 len_ptr 中。//TODO:内核给用户分配空间吗?
使用 get_robust_list() 的权限由 ptrace 访问模式 PTRACE_MODE_READ_REALCREDS 检查控制;参见 ptrace(2)。
set_robust_list() 系统调用请求内核记录调用线程拥有的健壮 futex 列表头。head 参数是需要记录的列表头。len 参数应为 sizeof(*head)。
4. RETURN VALUE
当操作成功时,set_robust_list() 和 get_robust_list() 系统调用返回零,否则返回错误代码。
四、补充
1. robust futex 只是多出了两个设置和获取 robust list 的系统调用。整个 futex.c 只提供了 set_robust_list()/get_robust_list()/futex () 三个系统调用函数。
msm-4.14/kernel$ cat futex.c | grep SYSCALL_DEFINE //固定有的系统调用: SYSCALL_DEFINE2(set_robust_list, struct robust_list_head __user *, head, SYSCALL_DEFINE3(get_robust_list, int, pid, SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val, //需要 CONFIG_COMPAT 才存在的系统调用: COMPAT_SYSCALL_DEFINE2(set_robust_list, COMPAT_SYSCALL_DEFINE3(get_robust_list, int, pid, COMPAT_SYSCALL_DEFINE6(futex, u32 __user *, uaddr, int, op, u32, val,
posted on 2025-05-15 18:00 Hello-World3 阅读(52) 评论(0) 收藏 举报