setns 系统调用及示例

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

1. 函数介绍

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

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

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 让一个正在运行的进程可以“穿越”到另一个隔离的环境(命名空间)中去。

重要提示:

需要权限:加入某些类型的命名空间(尤其是 User 命名空间)可能需要特殊权限或遵循复杂的规则。

文件描述符:setns 不是直接通过命名空间的名字或 ID 来操作,而是通过一个指向该命名空间的文件描述符。这个文件描述符通常是通过打开 /proc/[pid]/ns/ 目录下的特殊符号链接文件获得的。

部分加入:一个进程可以同时属于多个不同类型的命名空间。setns 每次只加入一个指定类型的命名空间。

2. 函数原型

1
2
3
4
5
6
7
// 标准 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)

  • EBADF: fd 不是一个有效的文件描述符。

  • EINVAL: fd 是有效的,但它不指向一个命名空间文件,或者 nstype 指定的类型与文件描述符指向的命名空间类型不匹配。

  • ENOMEM: 内核内存不足,无法完成操作。

  • EPERM: 调用者没有权限加入该命名空间。例如,尝试加入一个 User 命名空间可能受到严格限制。

7. 相似函数或关联函数

  • unshare: 允许调用进程脱离当前的某个命名空间,并加入一个新创建的、空的同类型命名空间。

  • clone: 创建新进程时,可以通过传递 CLONE_NEW* 标志,使新进程在新的命名空间中启动。

  • /proc/[pid]/ns/: 这个目录包含了进程所处的各种命名空间的符号链接文件。通过打开这些文件可以获得命名空间的文件描述符。

  • nsenter: 命令行工具,可以在指定的命名空间中执行命令,它在底层使用了 setns。

8. 示例代码

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

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

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
#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&#91;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&#91;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&#91;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&#91;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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 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

编译和运行:

1
2
3
4
5
6
7
8
9
# 假设代码保存在 setns_example.c 中
# 需要 root 权限来运行涉及 mount 和 setns 的操作

# 编译
gcc -o setns_example setns_example.c

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

预期输出 (片段):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--- 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)是更常见的与命名空间交互的方式。

https://www.calcguide.tech/2025/08/20/awk分析nginx日志/

https://www.calcguide.tech/2025/08/18/getgroups系统调用及示例/

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

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

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