set_tid_address系统调用及示例

我们来深入学习 set_tid_address 系统调用

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

set_tid_address系统调用及示例-CSDN博客

在 Linux 系统中,进程和线程是程序执行的基本单位。每个线程都有一个唯一的标识符,叫做 Thread ID (TID)。对于主线程(也就是进程本身),它的 TID 通常和 Process ID (PID) 是相同的。但对于通过 clone() 或 pthread_create() 创建的子线程,它们会有自己独立的 TID。

有时候,一个线程(或进程)需要知道另一个线程何时退出。例如,在一个多线程服务器中,主线程可能需要清理已退出的工作线程的资源。

set_tid_address 系统调用提供了一种机制来实现这一点。当一个线程调用 set_tid_address 并传入一个内存地址(我们称之为“TID 地址”或 tidptr)后,内核会记住这个地址。当这个线程最终退出时,内核会执行一个非常重要的操作:将这个地址处的内存值(通常是一个 int 或 pid_t 变量)清零(设置为 0)。

这样,程序的其他部分(比如父线程)就可以通过检查这个 tidptr 指向的内存位置的值来判断线程是否已经退出:如果值是 0,说明线程退出了;如果值非 0,说明线程还在运行(或者刚好是它的 TID)。

简单来说,set_tid_address 就是告诉内核:“当我死(退出)的时候,请帮我把这个地方的数字清零。”这样别人(通常是创建我的线程)就能通过看这个数字知道我是不是挂了。

重要提示:用户空间程序通常不会直接调用 set_tid_address。当你使用 pthread_create() 创建线程时,底层的 C 库(如 glibc NPTL)会自动为你调用 set_tid_address,并管理好这个 tidptr。这个系统调用主要是供 C 库实现线程功能时使用的。

对于 Linux 编程小白:你只需要知道,当你使用标准的 POSIX 线程库(pthread)时,线程退出通知机制(例如 pthread_join 能知道线程何时结束)在底层可能就是通过 set_tid_address 实现的。直接调用它比较少见,除非你在编写自己的线程库或者进行非常底层的系统编程。

2. 函数原型

1
2
3
4
5
6
7
// 标准 C 库通常不提供直接的包装函数
// 需要通过 syscall 直接调用
#include <sys/syscall.h> // 包含系统调用号 SYS_set_tid_address
#include <unistd.h> // 包含 syscall 函数

long syscall(SYS_set_tid_address, int *tidptr);

3. 功能

设置当前线程在退出时用于清除的用户空间地址。内核会记录这个地址,当线程终止时,内核会将该地址指向的内存单元(通常是一个 int)的值设置为 0。

4. 参数

tidptr:

  • int * 类型。

  • 一个指向用户空间内存地址的指针。这个地址处通常存放一个 int 或 pid_t 类型的变量。调用线程启动时,这个变量通常被初始化为其自身的 TID。当线程退出时,内核会将这个地址处的值清零。

5. 返回值

  • 成功: 返回调用线程的 Thread ID (TID)。这使得调用者可以方便地知道自己(或被创建的线程)的 TID 是多少。

  • 失败: 理论上这个系统调用不应该失败,但如果 tidptr 指向无效内存,可能会返回错误。但在实践中,它几乎总是成功返回 TID。

6. 相似函数或关联函数

  • clone: 用于创建进程或线程的底层系统调用。clone 可以接受 CLONE_CHILD_CLEARTID 标志,该标志会使得新创建的子进程/线程在退出时自动调用类似 set_tid_address 的操作。

  • pthread_create / pthread_join: POSIX 线程库函数。pthread_create 会创建线程并可能在底层使用 set_tid_address,pthread_join 会等待线程结束,其底层实现可能依赖于 set_tid_address 提供的机制(例如通过 futex 等待 tidptr 变为 0)。

  • gettid: 获取当前线程的 TID。可以通过 syscall(SYS_gettid) 调用。

  • futex: 快速用户空间互斥锁系统调用。pthread_join 等函数可能使用 futex 来高效地等待由 set_tid_address 清零的变量。

  • wait / waitpid: 用于等待子进程结束。这是针对进程的,而 set_tid_address 是针对线程的。

7. 示例代码

由于 set_tid_address 通常由 C 库内部使用,直接调用它需要手动管理线程的创建和同步,这比较复杂。下面的示例将演示如何结合 clone 系统调用和 set_tid_address 来手动创建一个线程,并利用 set_tid_address 的机制来等待它退出。

警告:这是一个非常底层的示例,展示了 set_tid_address 和 clone 的用法。在实际编程中,强烈建议使用 pthread 库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#define _GNU_SOURCE // 启用 GNU 扩展以使用 clone
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h> // 包含 syscall, SYS_set_tid_address, SYS_gettid, SYS_clone
#include <sys/wait.h>
#include <sched.h> // 包含 CLONE_* 常量
#include <errno.h>
#include <stdatomic.h>
#include <linux/futex.h> // 包含 futex 操作码
#include <sys/time.h> // 包含 timespec
#include <limits.h> // 包含 INT_MAX

// 定义线程栈大小
#define STACK_SIZE (1024 * 1024) // 1MB

// 线程函数
int thread_function(void *arg) {
long thread_num = (long)arg;
pid_t my_tid = syscall(SYS_gettid); // 获取自己的 TID
printf("Child thread %ld (TID: %d) started.\n", thread_num, my_tid);

// 模拟一些工作
for (int i = 0; i < 5; ++i) {
printf("Child thread %ld working... %d\n", thread_num, i);
sleep(1);
}

printf("Child thread %ld (TID: %d) finishing.\n", thread_num, my_tid);
// 线程函数返回时,内核会根据 set_tid_address 设置的地址,
// 将该地址处的值清零,并可能唤醒等待的 futex。
return 0;
}

// 模拟 pthread_join 的简单等待函数
// 等待 *tidptr 变为 0
void wait_for_thread_exit(int *tidptr) {
// 循环检查 tidptr 指向的值
// 在实际的库实现中,这里会使用 futex 系统调用来高效等待
while(__atomic_load_n(tidptr, __ATOMIC_ACQUIRE) != 0) {
printf("Main thread: Waiting for thread with TID %d to exit (tidptr=%d)...\n",
*tidptr, *tidptr);
// 简单的轮询等待(效率低)
// 实际库会用 syscall(SYS_futex, tidptr, FUTEX_WAIT, 0, NULL, NULL, 0);
sleep(1);
}
printf("Main thread: Detected thread has exited (tidptr is now 0).\n");
}

int main() {
char *stack; // 指向子线程栈的指针
char *stack_top; // 指向子线程栈顶的指针
pid_t child_tid; // 存储 clone 返回的子线程 TID
int tid_location = 0; // 用于 set_tid_address 的变量

printf("--- Demonstrating set_tid_address with clone ---\n");
printf("Main thread PID/TID: %d\n", getpid()); // 主线程 PID 和 TID 相同

// 1. 为子线程分配栈空间
// 注意:栈是向下增长的,所以我们需要分配后调整指针
stack = malloc(STACK_SIZE);
if (stack == NULL) {
perror("malloc");
exit(EXIT_FAILURE);
}
stack_top = stack + STACK_SIZE; // 栈顶指针

printf("Allocated stack for child thread at %p (top at %p)\n", stack, stack_top);

// 2. 使用 clone 创建子线程
// CLONE_VM: 子线程与父线程共享进程的内存描述符 (虚拟内存空间)
// CLONE_FS: 共享文件系统信息
// CLONE_FILES: 共享文件描述符表
// CLONE_SIGHAND: 共享信号处理函数表
// CLONE_THREAD: 将子进程置于父进程的线程组中
// CLONE_SYSVSEM: 共享 System V 信号量 undo 信息
// CLONE_PARENT_SETTID: 将子线程的 TID 写入 ptid (我们不使用 ptid)
// CLONE_CHILD_CLEARTID: 子线程退出时,清零 ctid 指向的值 (即 tid_location)
// stack_top: 子线程的栈指针
// &tid_location: ctid 参数,指向 tid_location
child_tid = syscall(SYS_clone,
CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
CLONE_THREAD | CLONE_SYSVSEM |
CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID,
stack_top, NULL, &tid_location);

if (child_tid == -1) {
perror("clone");
free(stack);
exit(EXIT_FAILURE);
}

if (child_tid == 0) {
// --- 在子线程中 ---
// 子线程的第一件事通常是调用 set_tid_address
// 但实际上,当我们使用 CLONE_CHILD_CLEARTID 标志时,
// 内核已经在 clone 时为我们处理了类似 set_tid_address 的操作
// 这里我们显式调用一次,展示其效果
pid_t my_tid = syscall(SYS_gettid);
long returned_tid = syscall(SYS_set_tid_address, &tid_location);
printf("In child thread: My TID is %d, set_tid_address returned %ld\n", my_tid, returned_tid);
// 初始化 tid_location 为自己的 TID
__atomic_store_n(&tid_location, my_tid, __ATOMIC_RELEASE);
printf("In child thread: Set tid_location to %d\n", tid_location);

// 调用线程函数
thread_function((void*)1);

// 线程函数返回,线程退出
// 内核会将 &tid_location 处的值清零
free(stack); // 子线程释放栈(简化处理)
exit(EXIT_SUCCESS); // 或者直接 return
}

// --- 回到主线程 ---
printf("Main thread: clone returned child TID: %d\n", child_tid);
printf("Main thread: tid_location variable address: %p\n", &tid_location);
printf("Main thread: Initial value of tid_location: %d\n", tid_location);

// 等待一小会儿,让子线程设置 tid_location
sleep(1);
printf("Main thread: Value of tid_location after child setup: %d\n", tid_location);

// 3. 等待子线程退出
// 我们可以轮询 tid_location,或者使用 futex (推荐)
printf("Main thread: Waiting for child thread to exit...\n");
wait_for_thread_exit(&tid_location);
// 或者使用 futex: syscall(SYS_futex, &tid_location, FUTEX_WAIT, 0, NULL, NULL, 0);

printf("Main thread: Child thread has exited.\n");
printf("Main thread: Final value of tid_location: %d\n", tid_location);

// 4. 清理 (主线程不再需要栈了,因为子线程已经退出并释放了)
// free(stack); // 子线程已释放

printf("Main thread: Program finished.\n");
return 0;
}

使用标准 pthread 的对比示例 (推荐方式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

void* worker_thread(void *arg) {
long thread_num = (long)arg;
printf("Worker thread %ld started.\n", thread_num);

for (int i = 0; i < 5; ++i) {
printf("Worker thread %ld working... %d\n", thread_num, i);
sleep(1);
}

printf("Worker thread %ld finishing.\n", thread_num);
return NULL;
}

int main() {
pthread_t thread1, thread2;

printf("--- Using standard pthread (Recommended) ---\n");
printf("Main thread PID/TID: %d\n", getpid());

// 创建线程
if (pthread_create(&thread1, NULL, worker_thread, (void*)1) != 0 ||
pthread_create(&thread2, NULL, worker_thread, (void*)2) != 0) {
perror("pthread_create");
exit(EXIT_FAILURE);
}

printf("Main thread: Created worker threads.\n");

// 等待线程结束 (这在底层可能使用了 set_tid_address 提供的机制)
printf("Main thread: Waiting for worker threads to join...\n");
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

printf("Main thread: Both worker threads have finished.\n");
printf("Main thread: Program finished using standard pthread library.\n");

return 0;
}

编译和运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 假设代码保存在 set_tid_address_example.c 和 pthread_example.c 中
# 需要链接 pthread 库

# 编译 pthread 示例 (推荐,简单,可移植)
gcc -o pthread_example pthread_example.c -lpthread

# 编译 set_tid_address 示例 (底层,复杂)
gcc -o set_tid_address_example set_tid_address_example.c

# 运行 pthread 示例
./pthread_example

# 运行 set_tid_address 示例
./set_tid_address_example

总结:对于 Linux 编程新手,请优先学习和使用标准的 pthread 库来创建和管理线程。set_tid_address 是一个底层系统调用,主要用于 C 库实现线程功能,它提供了一种高效的线程退出通知机制。直接使用它需要深入了解系统调用、内存管理和线程同步,通常只在编写系统级代码时才会涉及。

https://www.calcguide.tech/2025/08/23/set-tid-address系统调用及示例/

data-ad-format="auto" data-full-width-responsive="true">