6 分钟
容器核心技术(五) IPC Namespace
手册页面:ipc namespaces。
背景知识
IPC (Inter-Process Communication,进程间通讯) 有很多中方式,在 Linux 中主要有:
- 文件系统
- Signal 信号
- Pipe 管道
- FIFO 命名管道
- System V IPC
- 消息队列
- 信号量
- 共享内存
- POSIX IPC
- 消息队列
- 信号量
- 共享内存
- 网络 Socket
- Unix Domain Socket
更多参见:Linux IPC
描述
IPC Namespace 主要隔离了如下全局资源:
- System V IPC,包括消息队列、信号量、共享内存。
- POSIX IPC 的 消息队列,不包括信号量、共享内存(原因是信号量和共享内存是基于 tmpfs 文件系统的,已经通过 mount namespace 隔离过了)。
不同的 IPC Namespace 的如下 /proc
接口是隔离的:
/proc/sys/fs/mqueue
POSIX message queue 接口。/proc/sys/kernel
中的 System V IPC 接口,即:msgmax、msgmnb、msgmni、sem、shmall、shmmax、shmmni 和shm_rmid_forced。- System V IPC 接口
/proc/sysvipc
。
当 IPC 命名空间被销毁时(即,当最后一个进程是命名空间的成员终止),所有 IPC 对象在命名空间被自动销毁。
使用 IPC 命名空间需要一个配置有的内核 CONFIG_IPC_NS 选项。
实验
实验设计
启动一个具有新 IPC Namespace 的子进程,这个进程会设置 创建一个 System V 消息队列。然后分别在父子两个进程观察系统消息队列列表。
源码
C 语言描述
// gcc src/c/01-namespace/03-ipc/main.c && sudo ./a.out
#define _GNU_SOURCE // Required for enabling clone(2)
#include <sys/wait.h> // For waitpid(2)
#include <sys/mount.h> // For mount(2)
#include <sys/mman.h> // For mmap(2)
#include <sched.h> // For clone(2)
#include <signal.h> // For SIGCHLD constant
#include <stdio.h> // For perror(3), printf(3), perror(3)
#include <unistd.h> // For execv(3), sleep(3)
#include <stdlib.h> // For exit(3), system(3)
#include <sys/ipc.h> // For ftok(3), key_t
#include <sys/msg.h> // For msgget(2)
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
#define STACK_SIZE (1024 * 1024)
char *const child_args[] = {
"/bin/bash",
"-xc",
"ipcs -q",
NULL};
void create_msg_queue()
{
key_t k = ftok("/system_v_msg_queue_test_1", 1);
int msgid = msgget(k, IPC_CREAT);
}
int new_namespace_func(void *args)
{
// 首先,需要阻止挂载事件传播到其他 Mount Namespace,参见:https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
// 如果不执行这个语句, cat /proc/self/mountinfo 所有行将会包含 shared,这样在这个子进程中执行 mount 其他进程也会受影响
// 关于 Shared subtrees 更多参见:
// https://segmentfault.com/a/1190000006899213
// https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
// 下面语句的含义是:重新递归挂(MS_REC)载 / ,并设置为不共享(MS_SLAVE 或 MS_PRIVATE)
// 说明:
// MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
// 等价于执行:mount --make-rslave / 命令
if (mount(NULL, "/", NULL , MS_SLAVE | MS_REC, NULL) == -1)
errExit("mount-MS_SLAVE");
// 挂载当前 PID Namespace 的 proc
// 因为在新的 Mount Namespace 中执行,所有其他进程的目录树不受影响
// 等价命令为:mount -t proc proc /proc
// mount 函数声明为:
// int mount(const char *source, const char *target,
// const char *filesystemtype, unsigned long mountflags,
// const void *data);
// 更多参见:https://man7.org/linux/man-pages/man2/mount.2.html
if (mount("proc", "/proc", "proc", 0, NULL) == -1)
errExit("mount-proc");
// 创建一个 System V 消息队列
create_msg_queue();
printf("=== new ipc namespace process ===\n");
execv(child_args[0], child_args);
perror("exec");
exit(EXIT_FAILURE);
}
pid_t old_namespace_exec()
{
pid_t p = fork();
if (p == 0)
{
printf("=== old namespace process ===\n");
execv(child_args[0], child_args);
perror("exec");
exit(EXIT_FAILURE);
}
return p;
}
int main()
{
// 为子进程提供申请函数栈
void *child_stack = mmap(NULL, STACK_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
-1, 0);
if (child_stack == MAP_FAILED)
errExit("mmap");
// 创建新进程,并为该进程创建一个 IPC Namespace(CLONE_NEWIPC),并执行 new_namespace_func 函数
// clone 库函数声明为:
// int clone(int (*fn)(void *), void *stack, int flags, void *arg, ...
// /* pid_t *parent_tid, void *tls, pid_t *child_tid */);
// 更多参见:https://man7.org/linux/man-pages/man2/clone.2.html
// 为了测试方便,同时创建 Mount Namespace 和 PID Namespace
pid_t p1 = clone(new_namespace_func, child_stack + STACK_SIZE, SIGCHLD | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWPID, NULL);
if (p1 == -1)
errExit("clone");
sleep(5);
// 创建新的进程(不创建 Namespace),并执行测试命令
pid_t p2 = old_namespace_exec();
if (p2 == -1)
errExit("fork");
waitpid(p1, NULL, 0);
waitpid(p2, NULL, 0);
return 0;
}
Go 语言描述
//go:build linux
// sudo go run ./src/go/01-namespace/03-ipc/main.go
package main
import (
"fmt"
"log"
"os"
"os/exec"
"syscall"
"time"
)
const (
sub = "sub"
script = "ipcs -q"
)
func runTestScript(tip string) <-chan error {
fmt.Println(tip)
cmd := exec.Command("/bin/bash", "-cx", script)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
result := make(chan error)
go func() {
result <- cmd.Run()
}()
return result
}
func newNamespaceProccess() <-chan error {
cmd := exec.Command(os.Args[0], "sub")
// 创建新进程,并为该进程创建一个 IPC Namespace(syscall.CLONE_NEWIPC)
// 更多参见:https://man7.org/linux/man-pages/man2/clone.2.html
// 为了测试方便,同时创建 Mount Namespace 和 PID Namespace
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNS | syscall.CLONE_NEWPID | syscall.CLONE_NEWIPC,
}
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
result := make(chan error)
go func() {
result <- cmd.Run()
}()
return result
}
func createMsgQueue() {
// key_t k = ftok("/system_v_msg_queue_test_1", 1);
// int msgid = msgget(k, IPC_CREAT);
if err := exec.Command("sh", "-c", "ipcmk -Q").Run(); err != nil {
panic(err)
}
}
func newNamespaceProccessFunc() <-chan error {
// 首先,需要阻止挂载事件传播到其他 Mount Namespace,参见:https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#NOTES
// 如果不执行这个语句, cat /proc/self/mountinfo 所有行将会包含 shared,这样在这个子进程中执行 mount 其他进程也会受影响
// 关于 Shared subtrees 更多参见:
// https://segmentfault.com/a/1190000006899213
// https://man7.org/linux/man-pages/man7/mount_namespaces.7.html#SHARED_SUBTREES
// 下面语句的含义是:重新递归挂(MS_REC)载 / ,并设置为不共享(MS_SLAVE 或 MS_PRIVATE)
// 说明:
// MS_SLAVE 换成 MS_PRIVATE 也能达到同样的效果
// 等价于执行:mount --make-rslave / 命令
if err := syscall.Mount("", "/", "", syscall.MS_SLAVE|syscall.MS_REC, ""); err != nil {
panic(err)
}
// 挂载当前 PID Namespace 的 proc
// 因为在新的 Mount Namespace 中执行,所有其他进程的目录树不受影响
// 等价命令为:mount -t proc proc /proc
// 更多参见:https://man7.org/linux/man-pages/man8/mount.8.html
if err := syscall.Mount("proc", "/proc", "proc", 0, ""); err != nil {
panic(err)
}
// 创建一个 System V 消息队列
createMsgQueue()
return runTestScript("=== new ipc namespace process ===")
}
func oldNamespaceProccess() <-chan error {
return runTestScript("=== old namespace process ===")
}
func main() {
switch len(os.Args) {
case 1:
// 1. 执行 newNamespaceExec,启动一个具有新的 IPC Namespace 的进程
r1 := newNamespaceProccess()
time.Sleep(5 * time.Second)
// 3. 创建新的进程(不创建 Namespace),并执行测试脚本
r2 := oldNamespaceProccess()
err1, err2 := <-r1, <-r2
if err1 != nil {
panic(err1)
}
if err2 != nil {
panic(err2)
}
return
case 2:
if os.Args[1] == sub {
if err := <-newNamespaceProccessFunc(); err != nil {
panic(err)
}
return
}
}
log.Fatalf("usage: %s [sub]", os.Args[0])
}
Shell 描述
#!/usr/bin/env bash
# sudo ./src/shell/01-namespace/03-ipc/main.sh
script="ipcs -q"
# unshare -m -i -p 创建新进程,并为该进程创建一个 ipc Namespace(-i)
# 更多参见:https://man7.org/linux/man-pages/man1/unshare.1.html
# 为了测试方便,同时创建 Mount Namespace (-m) 和 PID Namespace (-p)
# 注意 unshare 会自动取消进程的所有共享,因此不需要手动执行:mount --make-rprivate /
# 更多参见:https://man7.org/linux/man-pages/man1/unshare.1.html 的 --propagation 参数说明
# mount -t proc proc /proc 挂载 proc 文件系统,等价于 mount("proc", "/proc", "proc", 0, NULL) 系统调用
# 更多参见:https://man7.org/linux/man-pages/man8/mount.8.html
# ipcmk -Q 创建一个 System V 消息队列
# 注意:bash 的最后一条命令将不会 fork 进程,所以在最后补充一个 sleep ,让命令在新的进程执行!
# https://unix.stackexchange.com/questions/466496/why-is-there-no-apparent-clone-or-fork-in-simple-bash-command-and-how-its-done
unshare -m -i -p /bin/bash -c "/bin/bash -c 'mount -t proc proc /proc \
&& ipcmk -Q \
&& echo \"=== new ipc namespace process ===\" && set -x && $script' && sleep 10" &
pid1=$!
sleep 5
# 创建新的进程(不创建 Namespace),并执行测试命令
/bin/bash -c "echo '=== old namespace process ===' && set -x && $script" &
pid2=$!
wait $pid1
wait $pid2
输出及分析
=== new ipc namespace process ===
+ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
0x8a0d2334 0 root 644 0 0
=== old namespace process ===
+ ipcs -q
------ Message Queues --------
key msqid owner perms used-bytes messages
可以看出 new ipc namespace 中创建的 System V 消息队列,在初始 Namespace 中并不存在。