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 进程的创建开销¶
线程栈大小¶
// 默认 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_*