clone系统调用及示例

我们继续介绍下一个函数。在 getsockopt 之后,根据您提供的列表,下一个函数是 clone。

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

clone 函数

1. 函数介绍

clone 是一个 Linux 特有的系统调用,它提供了一种非常灵活且底层的方式来创建新的进程或线程。它比标准的 fork 函数更加强大和复杂,允许调用者精确地控制子进程(或线程)与父进程(调用进程)之间共享哪些资源(如虚拟内存空间、文件描述符表、信号处理程序表等)。

你可以把 clone 想象成一个高度可定制的复制品制造机:

  • 你有一个原始对象(父进程)。

  • 你可以告诉机器(clone):复制这个对象,但让新对象(子进程)和原对象共享某些部件(如内存、文件),而独立拥有另一些部件(如寄存器状态、栈)。

  • 通过设置不同的参数,你可以制造出几乎完全独立的副本(类似 fork),或者共享大量资源的紧密副本(类似线程)。

实际上,Linux 上的 pthread 线程库在底层就是通过调用 clone 来创建线程的。

2. 函数原型

1
2
3
4
5
6
7
8
9
10
11
12
#define _GNU_SOURCE // 必须定义以使用 clone
#include <sched.h> // 必需
#include <signal.h> // 定义了 SIGCHLD 等常量

// 标准形式 (通过宏定义)
int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
/* pid_t *parent_tid, void *tls, pid_t *child_tid */ );

// 更底层的系统调用形式 (通常由库函数包装)
long syscall(SYS_clone, unsigned long flags, void *stack,
int *parent_tid, int *child_tid, unsigned long tls);

注意: clone 的接口比较复杂,并且存在不同版本。上面展示的是最常用的、由 glibc 提供的包装函数形式。

3. 功能

  • 创建新执行流: 创建一个新的执行流(可以看作一个轻量级进程或线程)。

  • 控制资源共享: 通过 flags 参数,精确控制新创建的执行流与调用者共享哪些内核资源。

  • 指定执行函数: 与 pthread_create 类似,clone 允许你指定一个函数 fn,新创建的执行流将从该函数开始执行。

  • 指定栈空间: 调用者必须为新执行流提供一块栈空间(通过 stack 参数),这与 pthread_create 自动分配栈不同。

  • 传递参数: 可以通过 arg 参数向新执行流的入口函数 fn 传递一个参数。

4. 参数

由于 clone 的复杂性,我们重点介绍 glibc 包装函数的常用参数:

int (*fn)(void *): 这是一个函数指针,指向新创建的执行流将要执行的入口函数。

  • 该函数接受一个 void * 类型的参数,并返回一个 int 类型的值。

  • 当这个函数返回时,新创建的执行流(子进程/线程)就会终止。

void *stack: 这是一个指针,指向为新执行流分配的栈空间的顶部(高地址)。

  • 非常重要: 调用者必须自己分配并管理这块栈内存。clone 不会自动分配。

  • 栈是从高地址向低地址增长的,所以这个指针应该指向分配的栈空间的末尾。

int flags: 这是最重要的参数,是一个位掩码(bitmask),用于指定新执行流与父进程共享哪些资源。常用的标志包括:

  • CLONE_VM: 共享虚拟内存空间。如果设置,子进程和父进程将运行在同一个内存地址空间中(类似线程)。

  • CLONE_FS: 共享文件系统信息(根目录、当前工作目录等)。

  • CLONE_FILES: 共享文件描述符表。如果设置,子进程将继承父进程打开的文件描述符,并且后续在任一进程中打开/关闭文件都会影响另一个。

  • CLONE_SIGHAND: 共享信号处理程序表。如果设置,子进程将继承父进程的信号处理设置。

  • CLONE_PTRACE: 如果父进程正在被跟踪(ptrace),则子进程也将被跟踪。

  • CLONE_VFORK: 暂停父进程的执行,直到子进程调用 exec 或 _exit。这模拟了 vfork 的语义。

  • CLONE_PARENT: 新子进程的父进程将是调用进程的父进程,而不是调用进程本身。

  • CLONE_THREAD: 将子进程置于调用进程的线程组中。这通常与 CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND 一起使用来创建线程。

  • CLONE_NEW* (如 CLONE_NEWNS, CLONE_NEWUSER): 用于创建命名空间(Namespace),这是容器技术(如 Docker)的基础。

  • SIGCHLD: 这不是一个 CLONE_* 标志,但它经常与 clone 一起使用(按位或 |)。它指定当子进程退出时,应向父进程发送 SIGCHLD 信号。

void *arg: 这是一个通用指针,它将作为参数传递给入口函数 fn。

… (可变参数): 后面可能还有几个参数,用于更高级的用途(如设置线程本地存储 TLS、获取子进程 ID 等),在基础使用中通常可以忽略或传入 NULL。

5. 返回值

clone 的返回值比较特殊,因为它在父进程和子进程(新创建的执行流)中是不同的:

在父进程中:

  • 如果成功,返回新创建子进程的**线程 ID **(Thread ID, TID)。在 Linux 中,TID 通常与 PID 相同(对于主线程),但对于使用 CLONE_THREAD 创建的线程,它们有相同的 PID 但不同的 TID。

  • 如果失败,返回 -1,并设置 errno。

在子进程中 (新创建的执行流):

  • 直接执行 fn(arg) 函数。

  • fn 函数的返回值将成为 clone 系统调用在子进程中的返回值。

  • 如果 fn 函数返回,子进程通常应该调用 _exit() 而不是 exit() 来终止,以避免刷新 stdio 缓冲区等可能影响父进程的操作。

6. 相似函数,或关联函数

  • fork: 创建一个新进程,子进程是父进程的一个完整副本,拥有独立的资源。clone 可以通过不设置任何共享标志来模拟 fork 的行为。

  • vfork: 类似于 fork,但在子进程调用 exec 或 _exit 之前会暂停父进程。clone 可以通过设置 CLONE_VFORK 标志来模拟 vfork。

  • pthread_create: POSIX 线程库函数,用于创建线程。在 Linux 上,它底层就是调用 clone,并自动处理栈分配、设置共享标志等。

  • _exit: 子进程在 fn 函数中执行完毕后,应调用 _exit 退出。

  • wait / waitpid: 父进程可以使用这些函数来等待由 clone(设置了 SIGCHLD)创建的子进程结束。

7. 示例代码

示例 1:使用 clone 模拟 fork (不共享任何资源)

这个例子演示了如何使用 clone 来创建一个与父进程几乎完全独立的子进程,效果类似于 fork。

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
// clone_fork_like.c
#define _GNU_SOURCE // 必须定义以使用 clone
#include <sched.h> // clone
#include <sys/wait.h> // waitpid
#include <unistd.h> // getpid
#include <stdio.h> // printf, perror
#include <stdlib.h> // exit, malloc, free
#include <signal.h> // SIGCHLD
#include <string.h> // strerror
#include <errno.h> // errno

#define STACK_SIZE (1024 * 1024) // 1MB 栈空间

// 子进程要执行的函数
int child_function(void *arg) {
char *msg = (char *)arg;
printf("Child process (TID: %d) executing.\n", getpid());
printf("Child received message: %s\n", msg);

// 子进程可以执行自己的任务
for (int i = 0; i < 3; ++i) {
printf(" Child working... %d\n", i);
sleep(1);
}

printf("Child process (TID: %d) finished.\n", getpid());
// 子进程结束,返回值将成为 clone 在子进程中的返回值
return 42;
}

int main() {
char *stack; // 指向栈空间的指针
char *stack_top; // 指向栈顶的指针 (clone 需要)
pid_t ctid; // 子进程的 TID

// 1. 为子进程分配栈空间
// 注意:栈是从高地址向低地址增长的
stack = malloc(STACK_SIZE);
if (stack == NULL) {
perror("malloc stack failed");
exit(EXIT_FAILURE);
}
// stack 指向分配内存的起始地址
// stack_top 应该指向内存的末尾地址
stack_top = stack + STACK_SIZE;

printf("Parent process (PID: %d) starting.\n", getpid());

// 2. 调用 clone 创建子进程
// flags = SIGCHLD: 子进程退出时发送 SIGCHLD 信号给父进程
// (没有设置 CLONE_VM, CLONE_FILES 等,所以资源不共享,类似 fork)
ctid = clone(child_function, stack_top, SIGCHLD, "Hello from parent to child!");
// 注意:这里的 SIGCHLD 是一个常见的用法,表示子进程结束后通知父进程

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

printf("Parent process (PID: %d) created child with TID: %d\n", getpid(), ctid);

// 3. 父进程继续执行自己的任务
printf("Parent process doing its own work...\n");
for (int i = 0; i < 5; ++i) {
printf(" Parent working... %d\n", i);
sleep(1);
}

// 4. 父进程等待子进程结束
int status;
pid_t wpid = waitpid(ctid, &status, 0); // 等待特定的子进程
if (wpid == -1) {
perror("waitpid failed");
free(stack);
exit(EXIT_FAILURE);
}

if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
printf("Parent: Child (TID %d) exited with status/code: %d\n", ctid, exit_code);
} else {
printf("Parent: Child (TID %d) did not exit normally.\n", ctid);
}

// 5. 清理资源
free(stack);
printf("Parent process (PID: %d) finished.\n", getpid());

return 0;
}

代码解释:

定义了栈大小 STACK_SIZE 为 1MB。

定义了子进程的入口函数 child_function。这个函数接受一个 void * 参数,打印信息,做一些工作,然后返回 42。

在 main 函数中:

  • 使用 malloc 分配栈空间。

  • 计算栈顶指针 stack_top。因为栈向下增长,clone 需要栈顶地址。

调用 clone(child_function, stack_top, SIGCHLD, “Hello from parent to child!”)。

  • child_function: 子进程入口。

  • stack_top: 子进程的栈顶。

  • SIGCHLD: 标志,表示子进程结束后发送信号。

  • “Hello…”: 传递给 child_function 的参数。

clone 在父进程中返回子进程的 TID。

父进程执行自己的任务。

调用 waitpid(ctid, …) 等待子进程结束。

检查子进程的退出状态。WEXITSTATUS(status) 获取子进程 child_function 的返回值(42)。

释放栈内存。

示例 2:使用 clone 创建共享内存的执行流 (类似线程)

这个例子演示了如何使用 clone 创建一个与父进程共享内存空间的执行流,模拟线程的部分行为。

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
// clone_thread_like.c
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>
#include <string.h>

#define STACK_SIZE (1024 * 1024)

// 全局变量,用于演示内存共享
volatile int shared_counter = 0;

// 子执行流函数
int thread_like_function(void *arg) {
char *name = (char *)arg;
printf("Thread-like process '%s' (TID: %d) started.\n", name, getpid());

for (int i = 0; i < 100000; ++i) {
// 修改共享变量
shared_counter++;
}
printf("Thread-like process '%s' finished. Shared counter: %d\n", name, shared_counter);
return 0;
}

int main() {
char *stack1, *stack2;
char *stack_top1, *stack_top2;
pid_t tid1, tid2;

stack1 = malloc(STACK_SIZE);
stack2 = malloc(STACK_SIZE);
if (!stack1 || !stack2) {
perror("malloc stacks failed");
free(stack1);
free(stack2);
exit(EXIT_FAILURE);
}
stack_top1 = stack1 + STACK_SIZE;
stack_top2 = stack2 + STACK_SIZE;

printf("Main process (PID: %d) creating two thread-like processes.\n", getpid());
printf("Initial shared counter: %d\n", shared_counter);

// 创建第一个"线程"
// CLONE_VM: 共享虚拟内存 (包括全局变量 shared_counter)
tid1 = clone(thread_like_function, stack_top1, CLONE_VM | SIGCHLD, "Thread-1");
if (tid1 == -1) {
perror("clone thread 1 failed");
free(stack1);
free(stack2);
exit(EXIT_FAILURE);
}

// 创建第二个"线程"
tid2 = clone(thread_like_function, stack_top2, CLONE_VM | SIGCHLD, "Thread-2");
if (tid2 == -1) {
perror("clone thread 2 failed");
free(stack1);
free(stack2);
exit(EXIT_FAILURE);
}

printf("Main process created TID1: %d, TID2: %d\n", tid1, tid2);

// 等待两个"线程"结束
// 注意:由于共享内存,最后的 shared_counter 值是不确定的(竞态条件)
waitpid(tid1, NULL, 0);
waitpid(tid2, NULL, 0);

printf("Main process finished. Final shared counter: %d (may be < 200000 due to race condition)\n", shared_counter);

free(stack1);
free(stack2);
return 0;
}

代码解释:

定义了一个 volatile int shared_counter 全局变量。volatile 告诉编译器不要优化对它的访问,因为在多执行流环境下它的值可能随时改变。

thread_like_function 是两个”线程”将执行的函数。它们都对 shared_counter 进行大量递增操作。

在 main 函数中:

  • 分配两个独立的栈空间。

  • 调用两次 clone 创建两个执行流。

关键: flags 参数是 CLONE_VM | SIGCHLD。

  • CLONE_VM: 这使得子执行流与父进程共享虚拟内存地址空间。因此,它们访问的 shared_counter 是同一个变量。

父进程等待两个子执行流结束。

重要: 由于两个执行流共享内存并同时修改 shared_counter,而 shared_counter++ 不是原子操作,这会导致竞态条件(Race Condition)。最终的 shared_counter 值很可能小于 200000。这展示了在共享内存编程中进行同步(如使用互斥锁)的重要性。

示例 3:与 pthread_create 的对比

这个例子通过代码片段对比 clone 和更高级的 pthread_create。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用 clone (底层,复杂)
int thread_func(void *arg) {
// ... thread work ...
return 0;
}
void* wrapper_func(void *arg) {
return (void*)(long)thread_func(arg);
}
// In main:
char *stack = malloc(STACK_SIZE);
char *stack_top = stack + STACK_SIZE;
clone(thread_func, stack_top, CLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD, arg);

// 使用 pthread_create (高层,简单)
void* pthread_func(void *arg) {
// ... thread work ...
return NULL;
}
// In main:
pthread_t thread;
pthread_create(&thread, NULL, pthread_func, arg);
pthread_join(thread, NULL);

解释:

  • clone 需要手动管理栈、设置多个标志位、处理返回值等,非常底层。

  • pthread_create 自动处理了栈分配、设置了正确的共享标志、提供了简单的 pthread_t 标识符和 pthread_join 等待机制,更易于使用。

重要提示与注意事项:

底层且复杂: clone 是一个非常底层的系统调用,直接使用它非常复杂且容易出错。除非有特殊需求(如实现自己的线程库、容器技术),否则应优先使用 fork/vfork 或 pthread_create。

栈管理: 调用者必须自己分配和释放子进程/线程的栈空间。忘记释放会导致内存泄漏。

标志位: flags 参数是 clone 的核心。理解各种 CLONE_* 标志的含义及其组合效果至关重要。

CLONE_THREAD: 如果使用 CLONE_THREAD 创建线程,该线程将成为调用进程的线程组的一部分。线程组中的所有线程具有相同的 PID,但有不同的 TID。对线程组中的任何一个线程调用 exit 会杀死整个线程组。等待线程需要使用 pthread_join 类似的机制,而不是 wait/waitpid。

_exit vs exit: 子进程(线程)在执行函数返回后,应调用 _exit() 而非 exit()。exit() 会刷新 stdio 缓冲区等,可能对共享内存的父进程产生意外影响。

信号: 理解 SIGCHLD 标志以及如何正确等待子进程非常重要。

可移植性: clone 是 Linux 特有的系统调用,在其他 Unix 系统上不可用。

总结:

clone 是 Linux 提供的一个功能强大但使用复杂的系统调用,用于创建新的执行流(进程或线程)。它通过精细的标志位控制资源的共享,是实现线程库和高级进程管理功能(如容器)的基础。虽然直接使用它需要深入了解系统底层知识,但理解其工作原理对于掌握 Linux 进程和线程模型非常有帮助。

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