clone系统调用及示例

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

clone 函数


1. 函数介绍

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

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

  • 你有一个原始对象(父进程)。
  • 你可以告诉机器(clone):复制这个对象,但让新对象(子进程)和原对象共享某些部件(如内存、文件),而独立拥有另一些部件(如寄存器状态、栈)。
  • 通过设置不同的参数,你可以制造出几乎完全独立的副本(类似 fork),或者共享大量资源的紧密副本(类似线程)。

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


2. 函数原型

#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_VMCLONE_FSCLONE_FILESCLONE_SIGHAND 一起使用来创建线程。
    • CLONE_NEW* (如 CLONE_NEWNSCLONE_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

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

代码解释:

  1. 定义了栈大小 STACK_SIZE 为 1MB。
  2. 定义了子进程的入口函数 child_function。这个函数接受一个 void * 参数,打印信息,做一些工作,然后返回 42。
  3. 在 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 创建一个与父进程共享内存空间的执行流,模拟线程的部分行为。

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

代码解释:

  1. 定义了一个 volatile int shared_counter 全局变量。volatile 告诉编译器不要优化对它的访问,因为在多执行流环境下它的值可能随时改变。
  2. thread_like_function 是两个”线程”将执行的函数。它们都对 shared_counter 进行大量递增操作。
  3. 在 main 函数中:
    • 分配两个独立的栈空间。
    • 调用两次 clone 创建两个执行流。
    • 关键flags 参数是 CLONE_VM | SIGCHLD
      • CLONE_VM: 这使得子执行流与父进程共享虚拟内存地址空间。因此,它们访问的 shared_counter 是同一个变量。
    • 父进程等待两个子执行流结束。
  4. 重要: 由于两个执行流共享内存并同时修改 shared_counter,而 shared_counter++ 不是原子操作,这会导致竞态条件(Race Condition)。最终的 shared_counter 值很可能小于 200000。这展示了在共享内存编程中进行同步(如使用互斥锁)的重要性。

示例 3:与 pthread_create 的对比

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

// 使用 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 等待机制,更易于使用。

重要提示与注意事项:

  1. 底层且复杂clone 是一个非常底层的系统调用,直接使用它非常复杂且容易出错。除非有特殊需求(如实现自己的线程库、容器技术),否则应优先使用 fork/vfork 或 pthread_create
  2. 栈管理: 调用者必须自己分配和释放子进程/线程的栈空间。忘记释放会导致内存泄漏。
  3. 标志位flags 参数是 clone 的核心。理解各种 CLONE_* 标志的含义及其组合效果至关重要。
  4. CLONE_THREAD: 如果使用 CLONE_THREAD 创建线程,该线程将成为调用进程的线程组的一部分。线程组中的所有线程具有相同的 PID,但有不同的 TID。对线程组中的任何一个线程调用 exit 会杀死整个线程组。等待线程需要使用 pthread_join 类似的机制,而不是 wait/waitpid
  5. _exit vs exit: 子进程(线程)在执行函数返回后,应调用 _exit() 而非 exit()exit() 会刷新 stdio 缓冲区等,可能对共享内存的父进程产生意外影响。
  6. 信号: 理解 SIGCHLD 标志以及如何正确等待子进程非常重要。
  7. 可移植性clone 是 Linux 特有的系统调用,在其他 Unix 系统上不可用。

总结:

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

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

发表回复

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