UTS Namespace

在 Linux 中,UTS (UNIX Time-sharing System) namespace 允许单个系统上的不同进程拥有各自独立的系统标识符,例如主机名(hostname)和域名(NIS domain name)。这意味着在 UTS namespace 中,一个进程可以更改其主机名而不会影响到系统上其他进程的主机名。

clone(): 这是一个 Linux 系统调用,用于创建一个新的进程。clone() 调用比 fork() 提供更多的选项,因为它允许通过指定不同的标志来控制新进程与原进程之间资源的共享范围。这些标志可以决定新进程属于哪些命名空间。例如,使用标志 CLONE_NEWPID 可以为新进程创建一个新的进程ID命名空间。创建的新进程以及它之后的所有子进程都会包含在指定的命名空间中。

unshare(): 这个系统调用允许一个已经存在的进程从某个命名空间中脱离出来(即使之前是与其他进程共享的)。如一个进程想要创建一个与当前网络命名空间分离的新网络命名空间,它可以通过 unshare() 和相应的标志(比如 CLONE_NEWNET)来实现这一点。

setns(): setns() 系统调用用于将一个进程加入到已经存在的命名空间中。这个调用需要一个文件描述符来指定目标命名空间。它通常被用在容器技术中,比如将一个进程从主机的命名空间加入到容器的命名空间。

unshare命令和unshare系统调用

unshare 命令和 unshare() 系统调用在 Linux 系统中都用于隔离(isolation)和虚拟化(virtualization)的上下文环境中,但它们的使用方式和领域有所不同:

unshare 命令: unshare 是一个用于从命令行运行的用户级别工具,它用来在无需启动新的 init 进程的情况下,创建独立的命名空间。命名空间(namespaces)是 Linux 系统用于隔离资源的一种机制,如进程、网络、用户 ID 和文件系统等。当使用 unshare 命令时,用户可以为特定的进程或脚本创建新的命名空间,并且不会影响系统中的其他部分。

unshare 命令的常见用法示例:


unshare --创建命名空间类型 程序

unshare --uts sh 创建UTS 的namespace,并在其中运行sh

unshare() 系统调用: unshare() 是一个由 C 语言编写的低级程序接口(API),属于 Linux 内核提供的系统调用之一。开发者可以在应用程序中直接调用 unshare() 来改变现有进程的命名空间成员身份,从而将其从某些资源的共享关系中分离出来。
unshare() 系统调用原型:


#include <sched.h>

int unshare(int flags);

这个调用接收 flags 参数,用于指定要解除共享的资源类型。当成功时返回 0,失败时返回 -1 并设置 errno 以指示错误原因。

两者都是实现 Linux 容器化和操作系统级别虚拟化的基础工具和调用。例如,Docker 这样的容器技术就在底层大量依赖了命名空间和 cgroups 等机制,而 unshare 命令和 unshare() 调用就是操作命名空间的方式之一。

root用户没有普通用户go命令的解决

由于直接go运行该程序是不允许的,需要切换到root用户才行或者使用sudo

首先 su  切换到root用户
添加以下行到 root 用户的 shell 配置文件(例如 /root/.bashrc 或 /root/.bash_profile)中:

export PATH=$PATH:/usr/local/go/bin

之后,运行以下命令来应用更改:

source /root/.bashrc

但麻烦的是切换su后又没有该要运行的utsnamespace.go文件了

使用sudo go

当你运行 echo $PATH 命令时,我们看到 /usr/local/go/bin 包含在你的用户的 PATH 环境变量中,这意味着你的用户应该能够找到 go 命令。

然而,当你以 sudo 运行 go 命令时,你遇到了 sudo: go: command not found 错误。这可能是因为当使用 sudo 命令时,sudo 可能没有使用你用户的 PATH 环境变量,它可能使用了一个不包含 /usr/local/go/bin 的不同的 PATH。

你可以用下面的命令来查看 sudoPATH 环境变量:

sudo env | grep PATH
 
如果从这个命令的输出中看不到 /usr/local/go/bin 目录,这会解释为什么 sudo 不能找到 go 命令。一个比较快捷的解决方法是指定 go 命令的完整路径:

sudo /usr/local/go/bin/go run utsnamespace.go
 
如果这个方法有效并且你想要修正 sudoPATH 问题,一个更持久的解决方案是编辑 /etc/sudoers 文件,你可以使用 visudo 命令来安全地编辑这个文件:

sudo visudo
 
在 visudo 编辑器中,寻找 secure_path 这一行并且确保 /usr/local/go/bin 被包含在里面。例如:

Defaults    secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/go/bin"

代码解析

os包与os/exec包

exec是os/exec包的一部分,正确的导入方式应该是:

import "os/exec"

os包: os包提供了一系列与文件系统交互的功能,比如文件和目录的创建、删除、读取和写入,以及环境变量的获取和设置等。这些都是直接在你的程序和操作系统之间进行的标准交互,不需要启动或与外部的、独立的程序进行交互。

os/exec包: os/exec包用于执行外部的命令和程序。它允许你的 Go 程序启动另一个独立的进程,运行在操作系统外部的命令或脚本,如 Shell 命令或其他可执行文件。os/exec可以捕获这些外部进程的输出、提供输入给它们、处理它们的错误输出,并且可以控制它们的执行(比如开始、等待结束、杀死进程等)。

用一个简单例子来说明它们的区别:

假设你想要读取一个文件并打印其内容。你只需要os包来完成这个任务:打开文件、读取内容、然后关闭文件。
现在假设你想要运行操作系统上的ls命令(在 Unix 中列出目录内容)或dir命令(在 Windows 中)。这个操作需要一个 Shell 环境来解释和执行命令。在这种情况下,os/exec就是你需要的,你的 Go 程序将使用它来启动一个新的外部进程来运行这个命令。
因此,exec包能完成的一些与外部进程交互的事情,不能仅仅使用os包来完成,因为os包没有提供这样的能力来启动和控制外部进程。反之亦真,os/exec包不是为了处理文件系统级的操作,而os包则专门设计来做这类事情。

log.Fatal(err)

在 Go 语言中,log.Fatal 函数用于记录错误信息并随后终止程序。这是 log 包提供的一个方便的函数,它先将参数(通常是错误信息)输出到日志,然后调用 os.Exit(1) 来终止程序。具体来看这行代码:

log.Fatal(err)

这行代码的作用是检查 err 变量是否不为 nil(即是否存在一个错误)。如果有错误发生:

  1. log.Fatal(err) 将错误信息 err 输出到标准错误(stderr)。输出的格式通常包括当前时间戳和文件名称,这有助于调试程序。

  2. 在错误信息被记录后,log.Fatal 会调用 os.Exit(1) 来终止程序的执行。传递给 os.Exit 的参数 1 是程序退出的状态码。在 UNIX 和类 UNIX 系统中,退出状态码 0 通常表示程序成功执行,而非零值表示执行过程中遇到错误。因此 1 表示程序因错误而终止。

SysProcAttr

Cmd 结构体用于表示将要执行的外部命令,在 Go 语言的 os/exec 包中负责封装与执行相关的信息。当使用Cmd 的 Run 或者 Start 方法时,会创建一个新的进程来执行指定的命令。SysProcAttr 结构体中的字段用于调整这个新进程的系统级属性。

关于 SysProcAttr 结构体的 Cloneflags 字段,这里是它的含义和作用:

  • Cloneflags:该字段用来传递一系列位标志给底层的系统调用,在 Linux 中通常是 clone。这些标志定义了进程启动时的特殊行为,包括它将如何与其他进程共享资源,以及它是否应该在不同的命名空间中运行等。

  • syscall.CLONE_NEWUTS:这个特定的位标志要求操作系统为新创建的进程创建一个新的 UTS(UNIX Time-sharing System)命名空间。UTS 命名空间允许进程有自己的系统标识,如主机名和域名,这样在这个命名空间内部更改主机名不会影响到其他的命名空间,增强了隔离性。

所以,上述 Go 代码片段中的 cmd.SysProcAttr 设置为带有 syscall.CLONE_NEWUTS 标志的 &syscall.SysProcAttr{},意指当 cmd 表示的命令运行时:

  1. 将会创建一个新的进程. 这个新进程的创建将包括一个新的 UTS 命名空间,使它能够拥有一个独立的主机名和域名,与其他进程隔离。
  2. 执行这个命令。

cmd的输入输出

  1. cmd.Stdin=os.Stdin
    这行代码设置命令的标准输入来自操作系统的标准输入,通常指的是终端或命令行界面的输入。这意味着外部命令可以读取用户通过终端输入的数据。

  2. cmd.Stdout=os.Stdout
    这行代码设置命令的标准输出到操作系统的标准输出,通常指的是终端或命令行界面的输出。这意味着运行的命令将会将它的输出数据发送回终端,让用户可以直接看到。

  3. cmd.Stderr=os.Stderr
    这行代码设置命令的标准错误到操作系统的标准错误,也通常是终端或命令行界面。标准错误是用来输出错误信息的,通过将标准错误设置到 os.Stderr,命令的错误信息将被直接显示在当前的错误输出设备上,通常也是终端。

总体来说,上述代码的作用是将命令的输入、输出、错误输出流重定向到当前的终端,这样运行该命令时用户可以通过终端与命令交互,并且可以在终端上看到命令的输出和错误信息。这是执行外部命令时一个非常常见的做法,他确保了命令行工具的交互体验。

go 代码

核心逻辑:创建一个新进程,并且该进程运行在一个新建立的uts namespace的命名空间中(运行在一个拥有一个独立的主机名和域名,与其他进程隔离的命名空间),然后新进程运行sh命令,

package main

import
(
	"exec"
	"syscall"
	"os"
	"log"
)

func main(){
	cmd:=exec.Command("sh")
	cmd.SysProcAttr=&syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWUTS,}
	cmd.Stdin=os.Stdin
	cmd.Stdout=os.Stdout
	cmd.Stderr=os.Stderr

	if err:=cmd.Run();err!=nil{
		log.Fatal(err)
	}
}

pstree -pl

在这里插入图片描述

结果演示

在这里插入图片描述

在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐