
三、构造容器进阶
1、实现容器的后台运行
在Docker的早期版本,所有的容器init进程都是从docker daemon这个进程fork出来的,这就会导致一个问题,如果docker daemon挂掉,那么所有的容器都会宕掉。后来,docker使用了containerd,也就是现在的runC,及时daemon挂掉,容器也还在。
容器,在操作系统看来,其实就是一个进程。当前运行命令的 mydocker 是主进程,容器是被当前 mydocker 进程 fork 出来的子进程。子进程的结束和父进程的运行是一个异步的过程,即父进程永远不知道子进程到底什么时候结束。如果创建子进程的父进程退出,那么这个子进程就成了没人管的孩子,俗称孤儿进程。为了避免孤儿进程退出时无法释放所占用的资源而僵死,进程号为 1 的进程 init 就会接受这些孤儿进程。
这就是父进程退出而容器进程依然运行的原理。虽然容器刚开始是由当前运行的 mydocker 进程创建的,但是当 mydocker 进程退出后,容器进程就会被进程号为 init 进程接管,这时容器进程还是运行着的,这样就实现了 mydocker 退出、容器不岩掉的功能。
首先,需要在 main-command.go 里面添加 -d 标签,表示这个容器启动的时候后台在运行。
var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit
mydocker run -ti [command]`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ti",
Usage: "enable tty",
},
cli.BoolFlag{
Name: "d",
Usage: "detach container",
},
cli.StringFlag{
Name: "m",
Usage: "memory limit",
},
cli.StringFlag{
Name: "cpushare",
Usage: "cpushare limit",
},
cli.StringFlag{
Name: "cpuset",
Usage: "cpuset limit",
},
},
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Missing container command")
}
var cmdArray []string
for _, arg := range context.Args() {
cmdArray = append(cmdArray, arg)
}
createTty := context.Bool("ti")
detach := context.Bool("d")
if createTty && detach {
return fmt.Errorf("ti and d paramter can not both provided")
}
resConf := &subsystems.ResourceConfig{
MemoryLimit: context.String("m"),
CpuSet: context.String("cpuset"),
CpuShare:context.String("cpushare"),
}
log.Infof("createTty %v", createTty)
Run(createTty, cmdArray, resConf)
return nil
},
}
这里的 createTty 和 detach 不能共存,只能选择其一,后面会根据这个来做一些操作。
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig) {
parent, writePipe := container.NewParentProcess(tty)
if parent == nil {
log.Errorf("New parent process error")
return
}
if err := parent.Start(); err != nil {
log.Error(err)
}
// use mydocker-cgroup as cgroup name
cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
defer cgroupManager.Destroy()
cgroupManager.Set(res)
cgroupManager.Apply(parent.Process.Pid)
sendInitCommand(comArray, writePipe)
if tty {
parent.Wait()
}
}
此处添加了判断,原本的wait主要是父进程等待子进程结束,这在交互式创建容器的步骤里面是没有问题的,但现在,如果detach创建容器,就不能等待了,创建容器后,父进程就退出。然后由操作系统init进程接管容器。
2、实现查看运行中的容器
上一节实现了 mydocker run -d
命令,可以让容器脱离父进程在后台独立运行,那么我们怎么知道有哪些容器在运行呢,而且他们的信息又是什么呢?这里需要实现 mydocker ps
命令,主要去约定好的位置查询一下容器的信息数据,然后显示。
首先,需要准备一些关于容器的信息,例如PID,容器创建时间,容器运行命令等。在main函数的run参数加一个name标签,方便用户指定容器名字:
var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit ie: mydocker run -ti [command]`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ti",
Usage: "enable tty",
},
cli.BoolFlag{
Name: "d",
Usage: "detach container",
},
cli.StringFlag{
Name: "m",
Usage: "memory limit",
},
cli.StringFlag{
Name: "cpushare",
Usage: "cpushare limit",
},
cli.StringFlag{
Name: "cpuset",
Usage: "cpuset limit",
},
cli.StringFlag{
Name: "name",
Usage: "container name",
},
},
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Missing container command")
}
var cmdArray []string
for _, arg := range context.Args() {
cmdArray = append(cmdArray, arg)
}
createTty := context.Bool("ti")
detach := context.Bool("d")
if createTty && detach {
return fmt.Errorf("ti and d paramter can not both provided")
}
resConf := &subsystems.ResourceConfig{
MemoryLimit: context.String("m"),
CpuSet: context.String("cpuset"),
CpuShare: context.String("cpushare"),
}
log.Infof("createTty %v", createTty)
containerName := context.String("name")
Run(createTty, cmdArray, resConf, containerName)
return nil
},
}
然后,需要添加一个record方法记录容器的相关信息,在增加之前,需要一个ID生成器,标识容器,为了方便,使用10位数字标识一个容器ID:
func randStringBytes(n int) string {
letterBytes := "1234567890"
rand.Seed(time.Now().UnixNano())
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}
这里以时间戳为种子,每次生成一个10以内的数字作为letterBytes数组的下标,最后拼接生成整个容器的ID。
下面就是记录容器的基本信息了,然后默认把容器的信息以json形式存储在/var/run/mydocker/容器名/config.json里面:
type ContainerInfo struct {
Pid string `json:"pid"` //容器的init进程在宿主机上的 PID
Id string `json:"id"` //容器Id
Name string `json:"name"` //容器名
Command string `json:"command"` //容器内init运行命令
CreatedTime string `json:"createTime"` //创建时间
Status string `json:"status"` //容器的状态
}
var (
RUNNING string = "running"
STOP string = "stopped"
Exit string = "exited"
DefaultInfoLocation string = "/var/run/mydocker/%s/"
ConfigName string = "config.json"
)
func recordContainerInfo(containerPID int, commandArray []string, containerName string) (string, error) {
// 首先生成10位数字的容器ID
id := randStringBytes(10)
createTime := time.Now().Format("2006-01-02 15:04:05")
command := strings.Join(commandArray, "")
// 如果没有指定容器名字,就以容器ID作为容器名
if containerName == "" {
containerName = id
}
containerInfo := &container.ContainerInfo{
Id: id,
Pid: strconv.Itoa(containerPID),
Command: command,
CreatedTime: createTime,
Status: container.RUNNING,
Name: containerName,
}
// json序列化
jsonBytes, err := json.Marshal(containerInfo)
if err != nil {
log.Errorf("Record container info error %v", err)
return "", err
}
jsonStr := string(jsonBytes)
//拼接存储容器信息的路径,"/var/run/mydocker/容器名/"
dirUrl := fmt.Sprintf(container.DefaultInfoLocation, containerName)
if err := os.MkdirAll(dirUrl, 0622); err != nil {
log.Errorf("Mkdir error %s error %v", dirUrl, err)
return "", err
}
// 创建"/var/run/mydocker/容器名/config.json"
fileName := dirUrl + "/" + container.ConfigName
file, err := os.Create(fileName)
defer file.Close()
if err != nil {
log.Errorf("Create file %s error %v", fileName, err)
return "", err
}
// 把info json写入
if _, err := file.WriteString(jsonStr); err != nil {
log.Errorf("File write string error %v", err)
return "", err
}
return containerName, nil
}
这样就实现了把创建容器的信息存储到磁盘的"/var/run/mydocker/容器名/config.json"上面,最后在run加上这个函数调用:
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, containerName string) {
parent, writePipe := container.NewParentProcess(tty)
if parent == nil {
log.Errorf("New parent process error")
return
}
if err := parent.Start(); err != nil {
log.Error(err)
}
//record container info
containerName, err := recordContainerInfo(parent.Process.Pid, comArray, containerName)
if err != nil {
log.Errorf("Record container info error %v", err)
return
}
// use mydocker-cgroup as cgroup name
cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
defer cgroupManager.Destroy()
cgroupManager.Set(res)
cgroupManager.Apply(parent.Process.Pid)
sendInitCommand(comArray, writePipe)
if tty {
parent.Wait()
deleteContainerInfo(containerName)
}
}
如果是删除容器信息,直接删除对应目录就可以了:
func deleteContainerInfo(containerId string) {
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerId)
if err := os.RemoveAll(dirURL); err != nil {
log.Errorf("Remove dir %s error %v", dirURL, err)
}
}
至此完成了数据的准备工作,接下来实现docker ps:
app.Commands = []cli.Command{
initCommand,
runCommand,
listCommand,
}
var listCommand = cli.Command{
Name: "ps",
Usage: "list all the containers",
Action: func(context *cli.Context) error {
ListContainers()
return nil
},
}
func ListContainers() {
// 找到存储容器信息的路径:"/var/run/mydocker"
dirURL := fmt.Sprintf(container.DefaultInfoLocation, "")
dirURL = dirURL[:len(dirURL)-1]
// 读取该目录下所有的文件
files, err := ioutil.ReadDir(dirURL)
if err != nil {
log.Errorf("Read dir %s error %v", dirURL, err)
return
}
var containers []*container.ContainerInfo
// 遍历该文件夹下的所有文件
for _, file := range files {
// 获取对应容器的配置文件获取信息,然后转换成容器信息对象
tmpContainer, err := getContainerInfo(file)
if err != nil {
log.Errorf("Get container info error %v", err)
continue
}
containers = append(containers, tmpContainer)
}
// 使用tabwriter在控制台打印
w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
fmt.Fprint(w, "ID\tNAME\tPID\tSTATUS\tCOMMAND\tCREATED\n")
for _, item := range containers {
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
item.Id,
item.Name,
item.Pid,
item.Status,
item.Command,
item.CreatedTime)
}
if err := w.Flush(); err != nil {
log.Errorf("Flush error %v", err)
return
}
}
func getContainerInfo(file os.FileInfo) (*container.ContainerInfo, error) {
containerName := file.Name()
// 根据容器名找到对应目录
configFileDir := fmt.Sprintf(container.DefaultInfoLocation, containerName)
configFileDir = configFileDir + container.ConfigName
content, err := ioutil.ReadFile(configFileDir)
if err != nil {
log.Errorf("Read file %s error %v", configFileDir, err)
return nil, err
}
var containerInfo container.ContainerInfo
if err := json.Unmarshal(content, &containerInfo); err != nil {
log.Errorf("Json unmarshal error %v", err)
return nil, err
}
return &containerInfo, nil
}
至此,执行docker ps
后就会在指定的位置查找所有的容器json文件。
3、实现查看容器日志
一般来说,对于容器中正在运行的进程,使日志达到标准输出是一个非常好的实现方案,因此需要将容器中的标准输出保存下来,以便需要的时候访问。就用这个思路作为mydocker logs
。
将容器进程的标准输出挂载到"/var/run/mydocker/容器名/container.log"文件中,这样在调用mydocker logs
的时候就去读取这个文件。
首先,需要修改下原来的实现,在创建容器的时候,把进程的标准输出重新定向一下。
func NewParentProcess(tty bool, containerName string) (*exec.Cmd, *os.File) {
readPipe, writePipe, err := NewPipe()
if err != nil {
log.Errorf("New pipe error %v", err)
return nil, nil
}
cmd := exec.Command("/proc/self/exe", "init")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
if tty {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
// 生成容器对应的log文件
dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
if err := os.MkdirAll(dirURL, 0622); err != nil {
log.Errorf("NewParentProcess mkdir %s error %v", dirURL, err)
return nil, nil
}
stdLogFilePath := dirURL + ContainerLogFile
stdLogFile, err := os.Create(stdLogFilePath)
if err != nil {
log.Errorf("NewParentProcess create file %s error %v", stdLogFilePath, err)
return nil, nil
}
// 把容器的标准输出重定向到这个文件
cmd.Stdout = stdLogFile
}
cmd.ExtraFiles = []*os.File{readPipe}
cmd.Dir = "/root/busybox"
return cmd, writePipe
}
app.Commands = []cli.Command{
initCommand,
runCommand,
listCommand,
logCommand,
}
var logCommand = cli.Command{
Name: "logs",
Usage: "print logs of a container",
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Please input your container name")
}
containerName := context.Args().Get(0)
logContainer(containerName)
return nil
},
}
此处会判断一下输出参数,因为要根据这个输出参数到对应文件夹中寻找日志文件,以如果不指定或者指定错误是会报错的。具体的日志查看方法如下:
func logContainer(containerName string) {
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
logFileLocation := dirURL + container.ContainerLogFile
file, err := os.Open(logFileLocation)
defer file.Close()
if err != nil {
log.Errorf("Log container open file %s error %v", logFileLocation, err)
return
}
content, err := ioutil.ReadAll(file)
if err != nil {
log.Errorf("Log container read file %s error %v", logFileLocation, err)
return
}
fmt.Fprint(os.Stdout, string(content))
}
可以发现,docker logs
和docker ps
实现原理是一样的。
4、实现进入容器Namespace
通过上一节,已经可以查看后台运行的容器日志了,但是容器一旦创建后,就无法再次进入容器,因此我们需要能够再次进入容器内部的功能,这就是mydocker exec
。
(1)setns
setns是一个系统调用,可以根据提供的PID再次进入到指定的Namespace中。它需要先打开/proc/[pid]/ns/文件夹下对应的文件,然后使当前进程进入到指定的Namespace中。系统调用很简单,但是对Go有一点麻烦,对于Mount Namespace来说,一个具有多线程的进程无法使用setns调用进入到对应的命名空间,但是Go每启动一个程序就会进入多线程状态,因此无法简单的在Go中直接调用系统调用。需要借助C来实现。
(2)Cgo
Cgo允许go程序去调用C的函数和标准库。
例如:
(3)实现命令
首先,需要使用C根据指定的PID进入对应的命名空间,使用setns系统调用完成:
package nsenter
/*
#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
__attribute__((constructor)) void enter_namespace(void) {
char *mydocker_pid;
// 从环境变量获取需要进入的PID
mydocker_pid = getenv("mydocker_pid");
if (mydocker_pid) {
//fprintf(stdout, "got mydocker_pid=%s\n", mydocker_pid);
} else {
//fprintf(stdout, "missing mydocker_pid env skip nsenter");
return;
}
char *mydocker_cmd;
// 从环境变量获取需要执行的命令
mydocker_cmd = getenv("mydocker_cmd");
if (mydocker_cmd) {
//fprintf(stdout, "got mydocker_cmd=%s\n", mydocker_cmd);
} else {
//fprintf(stdout, "missing mydocker_cmd env skip nsenter");
return;
}
int i;
char nspath[1024];
// 需要让PID进入5中Namespace
char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };
for (i=0; i<5; i++) {
// 指定pid所在的Namespace的信息在/proc/[pid]/ns/[namespace]下
sprintf(nspath, "/proc/%s/ns/%s", mydocker_pid, namespaces[i]);
int fd = open(nspath, O_RDONLY);
// 真正使用setns系统调用进入对应的Namespace
if (setns(fd, 0) == -1) {
//fprintf(stderr, "setns on %s namespace failed: %s\n", namespaces[i], strerror(errno));
} else {
//fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]);
}
close(fd);
}
// 在进入的Namespace中执行指定的命令
int res = system(mydocker_cmd);
exit(0);
return;
}
*/
import "C"
里面有一个__attribute__((constructor))
是指,一旦这个包被引用了,那么这个函数就会被自动执行,类似于构造函数。
先找到指定PID所在的Namespace号,然后使用setns系统调用使当前进程进入对应Namespace,一个PID对应的几个Namespace都要进入。然后使用system系统调用执行指定命令。
var execCommand = cli.Command{
Name: "exec",
Usage: "exec a command into container",
Action: func(context *cli.Context) error {
//This is for callback
if os.Getenv(ENV_EXEC_PID) != "" {
log.Infof("pid callback pid %s", os.Getgid())
return nil
}
if len(context.Args()) < 2 {
return fmt.Errorf("Missing container name or command")
}
containerName := context.Args().Get(0)
var commandArray []string
for _, arg := range context.Args().Tail() {
commandArray = append(commandArray, arg)
}
ExecContainer(containerName, commandArray)
return nil
},
}
const ENV_EXEC_PID = "mydocker_pid"
const ENV_EXEC_CMD = "mydocker_cmd"
func ExecContainer(containerName string, comArray []string) {
pid, err := getContainerPidByName(containerName)
if err != nil {
log.Errorf("Exec container getContainerPidByName %s error %v", containerName, err)
return
}
cmdStr := strings.Join(comArray, " ")
log.Infof("container pid %s", pid)
log.Infof("command %s", cmdStr)
cmd := exec.Command("/proc/self/exe", "exec")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
os.Setenv(ENV_EXEC_PID, pid)
os.Setenv(ENV_EXEC_CMD, cmdStr)
if err := cmd.Run(); err != nil {
log.Errorf("Exec container %s error %v", containerName, err)
}
}
整体的逻辑是,执行docker exec后,父进程没有设置环境变量,所以不执行Cgo,执行ExecContainer,根据容器名字找到对应的pid,并且把Pid和需要执行的命令写入环境变量。之后再开启一个exec进程,发现环境变量已经设置好了,调用Cgo系统调用,进入指定namespace。所以父进程就是为了设置环境变量的。
5、实现停止容器
一个容器完整生命周期除了需要 run 运行还需要 stop 。下面需要完成容器的停止和清理的一些工作。其实,stop 容器的原理很简单,主要就是查找到它的主进程 PID 然后发送 SIGTERM 信号,等待进程结束就好。下面就来实现它。
var stopCommand = cli.Command{
Name: "stop",
Usage: "stop a container",
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Missing container name")
}
containerName := context.Args().Get(0)
stopContainer(containerName)
return nil
},
}
我们期望的使用方式是mydocker stop 容器名
,因此需要在一开始检测一下是否输入了容器名:
func getContainerInfoByName(containerName string) (*container.ContainerInfo, error) {
// 构造存放容器信息的路径
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
configFilePath := dirURL + container.ConfigName
contentBytes, err := ioutil.ReadFile(configFilePath)
if err != nil {
log.Errorf("Read file %s error %v", configFilePath, err)
return nil, err
}
var containerInfo container.ContainerInfo
if err := json.Unmarshal(contentBytes, &containerInfo); err != nil {
log.Errorf("GetContainerInfoByName unmarshal error %v", err)
return nil, err
}
return &containerInfo, nil
}
func stopContainer(containerName string) {
// 根据容器名获取对应的主进程PID
pid, err := GetContainerPidByName(containerName)
if err != nil {
log.Errorf("Get contaienr pid by name %s error %v", containerName, err)
return
}
pidInt, err := strconv.Atoi(pid)
if err != nil {
log.Errorf("Conver pid from string to int error %v", err)
return
}
// 系统调用kill 可以发送信号给机场南,杀死容器主进程
if err := syscall.Kill(pidInt, syscall.SIGTERM); err != nil {
log.Errorf("Stop container %s error %v", containerName, err)
return
}
// 杀死容器后,需要设置容器状态信息
containerInfo, err := getContainerInfoByName(containerName)
if err != nil {
log.Errorf("Get container %s info error %v", containerName, err)
return
}
containerInfo.Status = container.STOP
containerInfo.Pid = " "
newContentBytes, err := json.Marshal(containerInfo)
if err != nil {
log.Errorf("Json marshal %s error %v", containerName, err)
return
}
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
configFilePath := dirURL + container.ConfigName
if err := ioutil.WriteFile(configFilePath, newContentBytes, 0622); err != nil {
log.Errorf("Write file %s error", configFilePath, err)
}
}
func GetContainerPidByName(containerName string) (string, error) {
// 根据容器名找到容器info文件,把其中的pid拿出来
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
configFilePath := dirURL + container.ConfigName
contentBytes, err := ioutil.ReadFile(configFilePath)
if err != nil {
return "", err
}
var containerInfo container.ContainerInfo
if err := json.Unmarshal(contentBytes, &containerInfo); err != nil {
return "", err
}
return containerInfo.Pid, nil
}
这样使用mydocker stop 容器名
后就会Kill掉对应容器进程。
6、实现删除容器
其实在上一节mydocker stop
后,容器就已经停止了,但是容器的info文件还在,所以docker ps
还可以看到这个进程,希望通过remove操作,删除对应文件,至此容器的所有的生命周期就完结了。
var removeCommand = cli.Command{
Name: "rm",
Usage: "remove unused containers",
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Missing container name")
}
containerName := context.Args().Get(0)
removeContainer(containerName)
return nil
},
}
func removeContainer(containerName string) {
containerInfo, err := getContainerInfoByName(containerName)
if err != nil {
log.Errorf("Get container %s info error %v", containerName, err)
return
}
// 判断容器是不是stop了
if containerInfo.Status != container.STOP {
log.Errorf("Couldn't remove running container")
return
}
dirURL := fmt.Sprintf(container.DefaultInfoLocation, containerName)
// 直接删除容器的信息文件目录
if err := os.RemoveAll(dirURL); err != nil {
log.Errorf("Remove file %s error %v", dirURL, err)
return
}
}
7、实现通过容器制作镜像
之前实现了mydocker run -d
来启动多个容器,但是多个容器的只读层就是busybox文件夹这一个,可写层也就是writelayer这个文件夹,多个容器共用一个AUFS文件系统,会相互影响。
因此,需要实现:
为每个容器分配单独的隔离文件系统
修改
mydocker commit
,实现对不同容器进行打包镜像
核心点就是要让容器的读写层修改成writelayerUrl+容器名
的命名格式:
var (
RUNNING string = "running"
STOP string = "stopped"
Exit string = "exited"
DefaultInfoLocation string = "/var/run/mydocker/%s/"
ConfigName string = "config.json"
ContainerLogFile string = "container.log"
RootUrl string = "/root"
MntUrl string = "/root/mnt/%s"
WriteLayerUrl string = "/root/writeLayer/%s"
)
//Decompression tar image
func CreateReadOnlyLayer(imageName string) error {
unTarFolderUrl := RootUrl + "/" + imageName + "/"
imageUrl := RootUrl + "/" + imageName + ".tar"
exist, err := PathExists(unTarFolderUrl)
if err != nil {
log.Infof("Fail to judge whether dir %s exists. %v", unTarFolderUrl, err)
return err
}
if !exist {
if err := os.MkdirAll(unTarFolderUrl, 0622); err != nil {
log.Errorf("Mkdir %s error %v", unTarFolderUrl, err)
return err
}
if _, err := exec.Command("tar", "-xvf", imageUrl, "-C", unTarFolderUrl).CombinedOutput(); err != nil {
log.Errorf("Untar dir %s error %v", unTarFolderUrl, err)
return err
}
}
return nil
}
func CreateWriteLayer(containerName string) {
writeURL := fmt.Sprintf(WriteLayerUrl, containerName)
if err := os.MkdirAll(writeURL, 0777); err != nil {
log.Infof("Mkdir write layer dir %s error. %v", writeURL, err)
}
}
func MountVolume(volumeURLs []string, containerName string) error {
parentUrl := volumeURLs[0]
if err := os.Mkdir(parentUrl, 0777); err != nil {
log.Infof("Mkdir parent dir %s error. %v", parentUrl, err)
}
containerUrl := volumeURLs[1]
mntURL := fmt.Sprintf(MntUrl, containerName)
containerVolumeURL := mntURL + "/" + containerUrl
if err := os.Mkdir(containerVolumeURL, 0777); err != nil {
log.Infof("Mkdir container dir %s error. %v", containerVolumeURL, err)
}
dirs := "dirs=" + parentUrl
_, err := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", containerVolumeURL).CombinedOutput()
if err != nil {
log.Errorf("Mount volume failed. %v", err)
return err
}
return nil
}
func CreateMountPoint(containerName, imageName string) error {
mntUrl := fmt.Sprintf(MntUrl, containerName)
if err := os.MkdirAll(mntUrl, 0777); err != nil {
log.Errorf("Mkdir mountpoint dir %s error. %v", mntUrl, err)
return err
}
tmpWriteLayer := fmt.Sprintf(WriteLayerUrl, containerName)
tmpImageLocation := RootUrl + "/" + imageName
mntURL := fmt.Sprintf(MntUrl, containerName)
dirs := "dirs=" + tmpWriteLayer + ":" + tmpImageLocation
_, err := exec.Command("mount", "-t", "aufs", "-o", dirs, "none", mntURL).CombinedOutput()
if err != nil {
log.Errorf("Run command for creating mount point failed %v", err)
return err
}
return nil
}
可以发现,现在只读层不是原本的busybox命名了,而是/root/镜像名方式,写层也不是writelayer命名,而是/root/writelayer/容器名方式。mnt挂载目录也要根据容器名有区别,每个容器有不同的mnt目录。容器退出删除文件的时候,也要根据名字进行路径匹配删除。
接下来是commit
函数:
func commitContainer(containerName, imageName string) {
mntURL := fmt.Sprintf(container.MntUrl, containerName)
mntURL += "/"
imageTar := container.RootUrl + "/" + imageName + ".tar"
if _, err := exec.Command("tar", "-czf", imageTar, "-C", mntURL, ".").CombinedOutput(); err != nil {
log.Errorf("Tar folder %s error %v", mntURL, err)
}
}
8、实现容器指定环境变量运行
在原来的基础上,增加-e选项,允许用户指定环境变量:
var runCommand = cli.Command{
Name: "run",
Usage: `Create a container with namespace and cgroups limit ie: mydocker run -ti [image] [command]`,
Flags: []cli.Flag{
cli.BoolFlag{
Name: "ti",
Usage: "enable tty",
},
cli.BoolFlag{
Name: "d",
Usage: "detach container",
},
cli.StringFlag{
Name: "m",
Usage: "memory limit",
},
cli.StringFlag{
Name: "cpushare",
Usage: "cpushare limit",
},
cli.StringFlag{
Name: "cpuset",
Usage: "cpuset limit",
},
cli.StringFlag{
Name: "name",
Usage: "container name",
},
cli.StringFlag{
Name: "v",
Usage: "volume",
},
cli.StringSliceFlag{
Name: "e",
Usage: "set environment",
},
},
Action: func(context *cli.Context) error {
if len(context.Args()) < 1 {
return fmt.Errorf("Missing container command")
}
var cmdArray []string
for _, arg := range context.Args() {
cmdArray = append(cmdArray, arg)
}
//get image name
imageName := cmdArray[0]
cmdArray = cmdArray[1:]
createTty := context.Bool("ti")
detach := context.Bool("d")
if createTty && detach {
return fmt.Errorf("ti and d paramter can not both provided")
}
resConf := &subsystems.ResourceConfig{
MemoryLimit: context.String("m"),
CpuSet: context.String("cpuset"),
CpuShare: context.String("cpushare"),
}
log.Infof("createTty %v", createTty)
containerName := context.String("name")
volume := context.String("v")
envSlice := context.StringSlice("e")
Run(createTty, cmdArray, resConf, containerName, volume, imageName, envSlice)
return nil
},
}
func Run(tty bool, comArray []string, res *subsystems.ResourceConfig, containerName, volume, imageName string, envSlice []string) {
containerID := randStringBytes(10)
if containerName == "" {
containerName = containerID
}
// 将环境变量传给process
parent, writePipe := container.NewParentProcess(tty, containerName, volume, imageName, envSlice)
if parent == nil {
log.Errorf("New parent process error")
return
}
if err := parent.Start(); err != nil {
log.Error(err)
}
//record container info
containerName, err := recordContainerInfo(parent.Process.Pid, comArray, containerName, containerID, volume)
if err != nil {
log.Errorf("Record container info error %v", err)
return
}
// use mydocker-cgroup as cgroup name
cgroupManager := cgroups.NewCgroupManager("mydocker-cgroup")
defer cgroupManager.Destroy()
cgroupManager.Set(res)
cgroupManager.Apply(parent.Process.Pid)
sendInitCommand(comArray, writePipe)
if tty {
parent.Wait()
deleteContainerInfo(containerName)
container.DeleteWorkSpace(volume, containerName)
}
}
func NewParentProcess(tty bool, containerName, volume, imageName string, envSlice []string) (*exec.Cmd, *os.File) {
readPipe, writePipe, err := NewPipe()
if err != nil {
log.Errorf("New pipe error %v", err)
return nil, nil
}
cmd := exec.Command("/proc/self/exe", "init")
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET | syscall.CLONE_NEWIPC,
}
if tty {
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
} else {
dirURL := fmt.Sprintf(DefaultInfoLocation, containerName)
if err := os.MkdirAll(dirURL, 0622); err != nil {
log.Errorf("NewParentProcess mkdir %s error %v", dirURL, err)
return nil, nil
}
stdLogFilePath := dirURL + ContainerLogFile
stdLogFile, err := os.Create(stdLogFilePath)
if err != nil {
log.Errorf("NewParentProcess create file %s error %v", stdLogFilePath, err)
return nil, nil
}
cmd.Stdout = stdLogFile
}
// os.Environ来获取宿主机的环境变量,然后把自定义变量加进去
cmd.ExtraFiles = []*os.File{readPipe}
cmd.Env = append(os.Environ(), envSlice...)
NewWorkSpace(volume, imageName, containerName)
cmd.Dir = fmt.Sprintf(MntUrl, containerName)
return cmd, writePipe
}
这样在使用docker run -e时候指定的环境变量,在进入容器后也可以看到了:
但是如果使用docker run -d
参数后台运行,然后再次用docker exec
进入,查看环境变量,发现没有之前设置的。
这里不能使用 env 命令获取设置环境变量的原因是,因为 exec 令其实是 mydocker 发起的另外一个进程,这个进程的父进程其实是宿主机的,并不是容器内的。因为在 Cgo 里面使用了 setns 系统调用,才使得这个进程进入到了容器内的命名空间,但是由于环境变量是继承自父进程的,因此这个 exec 进程的环境变量其实是继承自宿主机的,所以在 exec 进程内看到的环境变量其实是宿主机的环境变量。但是,只要是容器内 PID 的进程,创建出来的进程都会继承它的环境变量。下面修改 exec 命令来直接使用 env 命令查看容器内环境变量的功能。
也就是说,docker exec开的新进程并不是有docker run启动的进程fork出来的。并不会继承之前的环境变量。
首先需要有一个函数,可以根据指定PID获取对应进程的环境变量:
func getEnvsByPid(pid string) []string {
// 进程的环境变量存储在 /proc/PID/environ 下
path := fmt.Sprintf("/proc/%s/environ", pid)
contentBytes, err := ioutil.ReadFile(path)
if err != nil {
log.Errorf("Read file %s error %v", path, err)
return nil
}
// 多个环境变量的分隔符是\u0000
envs := strings.Split(string(contentBytes), "\u0000")
return envs
}
环境变量会存储在/proc/[PID]/environ
func ExecContainer(containerName string, comArray []string) {
pid, err := GetContainerPidByName(containerName)
if err != nil {
log.Errorf("Exec container getContainerPidByName %s error %v", containerName, err)
return
}
cmdStr := strings.Join(comArray, " ")
log.Infof("container pid %s", pid)
log.Infof("command %s", cmdStr)
cmd := exec.Command("/proc/self/exe", "exec")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
os.Setenv(ENV_EXEC_PID, pid)
os.Setenv(ENV_EXEC_CMD, cmdStr)
// 获取指定Pid的环境变量
containerEnvs := getEnvsByPid(pid)
// 将宿主机的环境变量和容器的环境变量都放到exec进程
cmd.Env = append(os.Environ(), containerEnvs...)
if err := cmd.Run(); err != nil {
log.Errorf("Exec container %s error %v", containerName, err)
}
}
由于exec任然需要宿主机的一些环境变量,因此将宿主机的环境变量和容器的环境变量都放到exec进程中,就可以获取到容器的环境变量了。