set_tid_address系统调用及示例

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

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. 函数原型

// 标准 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. 参数

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_addresspthread_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 库。

#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 的对比示例 (推荐方式):

#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;
}

编译和运行:

# 假设代码保存在 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 库实现线程功能,它提供了一种高效的线程退出通知机制。直接使用它需要深入了解系统调用、内存管理和线程同步,通常只在编写系统级代码时才会涉及。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注