setns 系统调用及示例

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

1. 函数介绍

Linux 命名空间 (Namespaces) 是 Linux 内核的一个强大特性,它提供了隔离机制。通过命名空间,可以将一组进程及其资源(如网络接口、挂载点、进程 ID 等)与系统上的其他进程隔离开来,仿佛它们运行在独立的系统中一样。这是实现 容器 (Containers) 技术(如 Docker, LXC)的核心基础之一。

Linux 支持多种类型的命名空间,每种隔离不同类型的系统资源:

  • Mount (mnt): 隔离文件系统挂载点。
  • PID (pid): 隔离进程 ID 空间。
  • Network (net): 隔离网络设备、IP 地址、端口等。
  • Interprocess Communication (ipc): 隔离 System V IPC 和 POSIX 消息队列。
  • UTS (uts): 隔离主机名和域名 (nodename/domainname)。
  • User (user): 隔离用户和组 ID。
  • Cgroup (cgroup): 隔离控制组 (cgroups) 的视图。

setns (Set Namespace) 系统调用的作用是:将调用它的进程,加入到一个已经存在的命名空间中

想象一下,你手里有一把钥匙,这把钥匙可以打开一扇通往某个“隔离房间”的门。setns 就像是你使用这把钥匙(文件描述符)进入那个特定的“隔离房间”(命名空间)的过程。一旦进入,你就能看到并使用那个房间里的东西(资源),就像你属于那个房间一样。

简单来说,setns 让一个正在运行的进程可以“穿越”到另一个隔离的环境(命名空间)中去。

重要提示

  1. 需要权限:加入某些类型的命名空间(尤其是 User 命名空间)可能需要特殊权限或遵循复杂的规则。
  2. 文件描述符setns 不是直接通过命名空间的名字或 ID 来操作,而是通过一个指向该命名空间的文件描述符。这个文件描述符通常是通过打开 /proc/[pid]/ns/ 目录下的特殊符号链接文件获得的。
  3. 部分加入:一个进程可以同时属于多个不同类型的命名空间。setns 每次只加入一个指定类型的命名空间。

2. 函数原型

// 标准 C 库通常不提供直接包装,需要通过 syscall 调用
#include <sched.h>       // 包含 CLONE_* 常量,定义了命名空间类型
#include <sys/syscall.h> // 包含系统调用号 SYS_setns
#include <unistd.h>      // 包含 syscall 函数

long syscall(SYS_setns, int fd, int nstype);

3. 功能

将调用进程加入由文件描述符 fd 指向的命名空间。如果 nstype 不为 0,还会检查该命名空间的类型是否与 nstype 指定的类型匹配。

4. 参数

  • fd:
    • int 类型。
    • 一个指向命名空间的文件描述符。这个文件描述符通常是通过打开 /proc/[pid]/ns/[namespace_name] 文件(例如 /proc/self/ns/net)获得的。[pid] 可以是目标进程的 PID,也可以是 self(代表调用进程自身)。
  • nstype:
    • int 类型。
    • 指定要加入的命名空间的类型。这是一个检查机制。有效的值是 <sched.h> 中定义的 CLONE_NEW* 常量,例如:
      • CLONE_NEWIPC
      • CLONE_NEWNET
      • CLONE_NEWNS (Mount)
      • CLONE_NEWPID
      • CLONE_NEWUSER
      • CLONE_NEWUTS
      • CLONE_NEWCGROUP
    • 如果 nstype 设置为 0,则不进行类型检查。

5. 返回值

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因。

6. 错误码 (errno)

  • EBADFfd 不是一个有效的文件描述符。
  • EINVALfd 是有效的,但它不指向一个命名空间文件,或者 nstype 指定的类型与文件描述符指向的命名空间类型不匹配。
  • ENOMEM: 内核内存不足,无法完成操作。
  • EPERM: 调用者没有权限加入该命名空间。例如,尝试加入一个 User 命名空间可能受到严格限制。

7. 相似函数或关联函数

  • unshare: 允许调用进程脱离当前的某个命名空间,并加入一个新创建的、空的同类型命名空间。
  • clone: 创建新进程时,可以通过传递 CLONE_NEW* 标志,使新进程在新的命名空间中启动。
  • /proc/[pid]/ns/: 这个目录包含了进程所处的各种命名空间的符号链接文件。通过打开这些文件可以获得命名空间的文件描述符。
  • nsenter: 命令行工具,可以在指定的命名空间中执行命令,它在底层使用了 setns

8. 示例代码

由于命名空间涉及系统级隔离,创建和操作它们通常需要 root 权限或对 /proc 文件系统的访问。下面的示例将展示如何使用 setns 加入一个 Mount 命名空间

警告:此示例需要 root 权限,并且会创建挂载点。请在测试环境中运行。

#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/mount.h> // 包含 mount 函数
#include <sys/wait.h>  // 包含 waitpid

// 简单的子进程函数,用于在新命名空间中执行
int child_func(void* arg) {
    printf("Child process (PID: %d) started.\n", getpid());

    // 在子进程中挂载一个 tmpfs 到 /tmp/my_test_mount
    // 这个挂载操作只在子进程的 Mount Namespace 中可见
    const char* mount_point = "/tmp/my_test_mount";
    if (mkdir(mount_point, 0755) == -1 && errno != EEXIST) {
        perror("mkdir (child)");
        return 1;
    }

    if (mount("tmpfs", mount_point, "tmpfs", 0, NULL) == -1) {
        perror("mount (child)");
        return 1;
    }

    printf("Child process: Mounted tmpfs on %s\n", mount_point);

    // 在挂载点创建一个文件
    char file_path[256];
    snprintf(file_path, sizeof(file_path), "%s/test_file.txt", mount_point);
    FILE *f = fopen(file_path, "w");
    if (f) {
        fprintf(f, "Hello from child process in its own mount namespace!\n");
        fclose(f);
        printf("Child process: Created file %s\n", file_path);
    } else {
        perror("fopen (child)");
    }

    printf("Child process: Sleeping for 30 seconds. Check /tmp/my_test_mount from parent and child.\n");
    printf("Child process: You can run 'ls /tmp/my_test_mount' in another terminal as root.\n");
    sleep(30); // 睡眠,让我们有时间从外部观察

    // 清理 (可选,因为退出时会自动清理)
    // umount(mount_point);
    // rmdir(mount_point);

    printf("Child process exiting.\n");
    return 0;
}

int main() {
    pid_t child_pid;
    int parent_ns_fd, child_ns_fd;
    char ns_path[256];
    const int STACK_SIZE = 1024 * 1024; // 1MB 栈
    char *child_stack = malloc(STACK_SIZE);
    if (!child_stack) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    printf("--- Demonstrating setns with Mount Namespace ---\n");
    printf("Main process PID: %d\n", getpid());

    // 1. 获取父进程当前的 Mount Namespace 文件描述符
    snprintf(ns_path, sizeof(ns_path), "/proc/self/ns/mnt");
    parent_ns_fd = open(ns_path, O_RDONLY);
    if (parent_ns_fd == -1) {
        perror("open parent namespace");
        free(child_stack);
        exit(EXIT_FAILURE);
    }
    printf("Opened parent's mount namespace fd: %d\n", parent_ns_fd);

    // 2. 使用 clone 创建一个新进程,并让它拥有自己的 Mount Namespace
    // CLONE_NEWNS: 创建新的 Mount Namespace
    child_pid = clone(child_func, child_stack + STACK_SIZE,
                      CLONE_NEWNS | SIGCHLD, NULL);
    if (child_pid == -1) {
        perror("clone");
        close(parent_ns_fd);
        free(child_stack);
        exit(EXIT_FAILURE);
    }
    printf("Created child process with new mount namespace. PID: %d\n", child_pid);

    // 等待一小会儿,让子进程完成挂载
    sleep(2);

    // 3. 获取子进程的 Mount Namespace 文件描述符
    snprintf(ns_path, sizeof(ns_path), "/proc/%d/ns/mnt", child_pid);
    child_ns_fd = open(ns_path, O_RDONLY);
    if (child_ns_fd == -1) {
        perror("open child namespace");
        close(parent_ns_fd);
        // 杀死子进程
        kill(child_pid, SIGKILL);
        waitpid(child_pid, NULL, 0);
        free(child_stack);
        exit(EXIT_FAILURE);
    }
    printf("Opened child's mount namespace fd: %d\n", child_ns_fd);

    // 4. 在父进程中,检查 /tmp/my_test_mount 是否存在
    // (它应该不存在,因为父进程在不同的 Mount Namespace)
    if (access("/tmp/my_test_mount", F_OK) == 0) {
         printf("Parent process: /tmp/my_test_mount EXISTS in parent namespace (unexpected!).\n");
    } else {
         printf("Parent process: /tmp/my_test_mount does NOT exist in parent namespace (as expected).\n");
    }

    // 5. 现在,使用 setns 将父进程加入到子进程的 Mount Namespace
    printf("\n--- Calling setns to join child's mount namespace ---\n");
    if (syscall(SYS_setns, child_ns_fd, CLONE_NEWNS) == -1) {
        perror("setns");
        printf("Failed to join child's namespace. Do you have root privileges?\n");
        close(parent_ns_fd);
        close(child_ns_fd);
        kill(child_pid, SIGKILL);
        waitpid(child_pid, NULL, 0);
        free(child_stack);
        exit(EXIT_FAILURE);
    }
    printf("Parent process successfully joined child's mount namespace.\n");

    // 6. 再次检查 /tmp/my_test_mount
    // (现在它应该存在了,因为父进程已经加入了子进程的命名空间)
    if (access("/tmp/my_test_mount", F_OK) == 0) {
         printf("Parent process (after setns): /tmp/my_test_mount NOW EXISTS in current namespace.\n");
         printf("Parent process (after setns): You can now see the file created by the child.\n");
         // 尝试读取子进程创建的文件
         char file_path[256];
         snprintf(file_path, sizeof(file_path), "%s/test_file.txt", "/tmp/my_test_mount");
         FILE *f = fopen(file_path, "r");
         if (f) {
             char buffer[256];
             if (fgets(buffer, sizeof(buffer), f)) {
                 printf("Parent process (after setns): Read from file: %s", buffer);
             }
             fclose(f);
         } else {
             perror("fopen (parent after setns)");
         }
    } else {
         printf("Parent process (after setns): /tmp/my_test_mount STILL does not exist (unexpected!).\n");
    }

    // 7. 清理和等待
    printf("\n--- Cleaning up ---\n");
    close(parent_ns_fd);
    close(child_ns_fd);

    // 等待子进程结束
    int status;
    waitpid(child_pid, &status, 0);
    if (WIFEXITED(status)) {
        printf("Child process exited with status %d.\n", WEXITSTATUS(status));
    } else {
        printf("Child process did not exit normally.\n");
    }

    free(child_stack);
    printf("Main process finished.\n");

    return 0;
}

使用 nsenter 命令行工具的对比示例:

nsenter 是一个非常方便的命令行工具,它封装了 setns 的功能,允许你在指定的命名空间中运行命令。

# 1. 启动一个长时间运行的进程 (例如 sleep) 并让它进入新的 PID 命名空间
# (通常与 unshare 一起使用)
unshare -p -f --mount-proc sleep 300 &
UNSHARE_PID=$!

# 等待一下让 unshare 启动
sleep 1

# 2. 查看这个新进程的 PID 命名空间 inode
ls -li /proc/1/ns/pid /proc/$UNSHARE_PID/ns/pid

# 3. 使用 nsenter 进入这个进程的 PID 和 Mount 命名空间,并运行 ps
# 这会显示在那个命名空间内部看到的进程
nsenter -t $UNSHARE_PID -p -m ps aux

# 4. 清理
kill $UNSHARE_PID

编译和运行:

# 假设代码保存在 setns_example.c 中
# 需要 root 权限来运行涉及 mount 和 setns 的操作

# 编译
gcc -o setns_example setns_example.c

# 运行 (必须使用 sudo)
sudo ./setns_example

预期输出 (片段):

--- Demonstrating setns with Mount Namespace ---
Main process PID: 12345
Opened parent's mount namespace fd: 3
Created child process with new mount namespace. PID: 12346
Child process (PID: 12346) started.
Child process: Mounted tmpfs on /tmp/my_test_mount
Child process: Created file /tmp/my_test_mount/test_file.txt
Child process: Sleeping for 30 seconds. Check /tmp/my_test_mount from parent and child.
Opened child's mount namespace fd: 4
Parent process: /tmp/my_test_mount does NOT exist in parent namespace (as expected).

--- Calling setns to join child's mount namespace ---
Parent process successfully joined child's mount namespace.
Parent process (after setns): /tmp/my_test_mount NOW EXISTS in current namespace.
Parent process (after setns): You can now see the file created by the child.
Parent process (after setns): Read from file: Hello from child process in its own mount namespace!

--- Cleaning up ---
Child process exited with status 0.
Main process finished.

总结:
setns 是一个强大的系统调用,是 Linux 容器技术的基石之一。它允许进程动态地加入到已存在的命名空间中,从而获得该命名空间的视图和资源访问权限。对于 Linux 编程新手来说,理解命名空间的概念是第一步,setns 则是实现命名空间操作的关键工具。直接使用它进行编程比较复杂,通常在容器运行时(如 runc)或高级系统管理脚本中会用到。日常开发中,使用 nsenter 或容器管理工具(如 docker exec)是更常见的与命名空间交互的方式。

 setns 系统调用及示例 – LinuxGuide setns setns,setns-系统调用及示例LinuxGuide

setns系统调用及示例-CSDN博客

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

发表回复

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