跳转至

clone 与线程

是什么 / 解决什么问题

clone 是 Linux 创建进程/线程的底层系统调用。通过不同的标志组合,可以精确控制父子之间共享哪些资源。pthread_create 底层就是调用 clone

使用模式

clone 直接使用(少见,通常用 pthread)

#define _GNU_SOURCE
#include <sched.h>
#include <signal.h>

#define STACK_SIZE (1024 * 1024)

int child_fn(void *arg) {
    printf("child tid=%d\n", gettid());
    return 0;
}

// 分配子线程栈(栈从高地址向低地址增长)
char *stack = malloc(STACK_SIZE);
char *stack_top = stack + STACK_SIZE;

pid_t tid = clone(child_fn, stack_top,
    CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD,
    NULL);

pthread 创建线程(推荐)

#include <pthread.h>

void *worker(void *arg) {
    int id = *(int *)arg;
    printf("thread %d running\n", id);
    return NULL;
}

pthread_t tid;
int arg = 42;
pthread_create(&tid, NULL, worker, &arg);

// 等待线程结束
void *ret;
pthread_join(tid, &ret);

// 或者分离(不需要 join)
pthread_detach(tid);

线程属性

pthread_attr_t attr;
pthread_attr_init(&attr);

// 设置栈大小(默认通常 8MB)
pthread_attr_setstacksize(&attr, 2 * 1024 * 1024);  // 2MB

// 设置分离状态(不需要 join)
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_create(&tid, &attr, worker, arg);
pthread_attr_destroy(&attr);

行为与语义

clone 的关键标志

标志 效果
CLONE_VM 共享虚拟内存空间
CLONE_FILES 共享文件描述符表
CLONE_FS 共享文件系统信息(cwd, root, umask)
CLONE_SIGHAND 共享信号处理函数
CLONE_THREAD 同一线程组(共享 PID,各有独立 TID)
CLONE_NEWNS 新的 mount namespace
CLONE_NEWPID 新的 PID namespace(容器基础)
CLONE_NEWNET 新的网络 namespace

线程 vs 进程的本质区别

"线程" = clone(CLONE_VM | CLONE_FILES | CLONE_FS | CLONE_SIGHAND | CLONE_THREAD)
"进程" = clone(0) ≈ fork()

Linux 内核不区分线程和进程——都是 task_struct。
区别只在于共享了多少资源。
资源 线程间 进程间
虚拟内存 共享 独立(COW)
fd 表 共享 独立副本
PID 相同 不同
TID 不同 不同
信号处理 共享 独立
独立 独立

线程 ID

#include <sys/syscall.h>

// pthread_t: 用户态线程库的标识(不透明类型)
pthread_t pt = pthread_self();

// TID: 内核级线程标识(用于 strace, /proc 等)
pid_t tid = gettid();  // 或 syscall(SYS_gettid)

// PID: 进程(线程组)标识
pid_t pid = getpid();  // 同进程的所有线程返回相同值

线程退出

// 方法1: 从线程函数 return
void *worker(void *arg) {
    return (void *)42;  // 返回值可被 pthread_join 获取
}

// 方法2: pthread_exit
pthread_exit((void *)42);

// 方法3: 被取消
pthread_cancel(tid);
// 被取消的线程在下一个取消点退出

// 注意:线程中调 exit() 会终止整个进程!

性能考量

线程 vs 进程的创建开销

线程创建:~50μs(只分配栈 + TLS,共享页表)
进程创建:~100-500μs(复制页表、fd 表等)

线程栈大小

// 默认 8MB 栈(虚拟地址空间预留,物理页按需分配)
// 如果线程很多(数千个),默认栈可能消耗大量虚拟地址空间

// 对于 I/O 密集、栈使用少的线程:
pthread_attr_setstacksize(&attr, 256 * 1024);  // 256KB 够用

// Go 语言的 goroutine 初始栈只有 2KB-8KB,动态增长

线程数量的权衡

CPU 密集型:线程数 ≈ CPU 核心数
I/O 密集型:线程数可以远大于核心数(等待 I/O 时让出 CPU)
过多线程的问题:
  - 上下文切换开销
  - 栈内存占用
  - 调度延迟增加
  - 缓存抖动

NPTL (Native POSIX Threads Library)

Linux 的 pthread 实现(glibc 2.3+): - 1:1 模型(每个 pthread 对应一个内核 task) - 线程创建/销毁快 - futex 做同步(见 futex 篇)

常见陷阱与 FAQ

1. 线程安全与数据竞争

// 错误:多线程共享变量无保护
int counter = 0;
void *worker(void *arg) {
    for (int i = 0; i < 1000000; i++)
        counter++;  // 数据竞争!结果不确定
}

// 正确:使用 mutex 或 atomic
#include <stdatomic.h>
atomic_int counter = 0;
void *worker(void *arg) {
    for (int i = 0; i < 1000000; i++)
        atomic_fetch_add(&counter, 1);
}

2. 传递局部变量地址给线程

// 错误:循环变量地址在 pthread_create 返回前可能已改变
for (int i = 0; i < N; i++) {
    pthread_create(&tids[i], NULL, worker, &i);
    // 多个线程可能看到相同的 i 值
}

// 正确:传值(转为 intptr_t)
for (int i = 0; i < N; i++) {
    pthread_create(&tids[i], NULL, worker, (void *)(intptr_t)i);
}

3. 不 join 也不 detach 导致资源泄漏

// 非 detached 线程退出后资源不会释放(类似 zombie)
// 必须 join 或者创建时设为 detached
pthread_detach(tid);
// 或创建时:
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

4. 信号与线程

// 信号发给进程,内核选择一个没有 mask 该信号的线程递送
// 通常做法:主线程处理信号,工作线程屏蔽所有信号

// 在创建工作线程前:
sigset_t set;
sigfillset(&set);
pthread_sigmask(SIG_BLOCK, &set, NULL);
// 之后创建的线程继承 mask
// 主线程用 sigwait 同步等待信号

观测与调试

# 查看进程的线程
ls /proc/<pid>/task/
ps -T -p <pid>

# 线程数
cat /proc/<pid>/status | grep Threads

# 查看线程栈大小
cat /proc/<pid>/task/<tid>/maps | grep stack

# strace 跟踪 clone 调用
strace -f -e trace=clone,clone3 ./app

# 查看线程 CPU 亲和性
taskset -p <tid>

延伸阅读

  • man 2 clone — 底层系统调用
  • man 7 pthreads — POSIX 线程概述
  • man 3 pthread_create / pthread_join / pthread_attr_*