1. 方法和函数
1.1. os包
1.1.1. 系统信息
1. func Hostname() (name string, err error)Hostname返回内核提供的主机名。2. func Getenv(key string) stringGetenv检索并返回名为key的环境变量的值。如果不存在该环境变量会返回空字符串。3. func ExpandEnv(s string) stringExpandEnv函数替换s中的${var}或$var为名为var 的环境变量的值。引用未定义环境变量会被替换为空字符串。4. func Setenv(key, value string) errorSetenv设置名为key的环境变量。如果出错会返回该错误。
1.1.2. 文件和目录操作
1. func Getwd() (dir string, err error)Getwd返回一个对应当前工作目录的根路径。如果当前目录可以经过多条路径抵达(因为硬链接),Getwd会返回其中一个。2. func Chdir(dir string) errorChdir将当前工作目录修改为dir指定的目录。如果出错,会返回*PathError底层类型的错误3. func Chmod(name string, mode FileMode) errorChmod修改name指定的文件对象的mode。如果name指定的文件是一个符号链接,它会修改该链接的目的地文件的mode。如果出错,会返回*PathError底层类型的错误。4. func Chown(name string, uid, gid int) errorChmod修改name指定的文件对象的用户id和组id。如果name指定的文件是一个符号链接,它会修改该链接的目的地文件的用户id和组id。如果出错,会返回*PathError底层类型的错误5. func MkdirAll(path string, perm FileMode) errorMkdirAll使用指定的权限和名称创建一个目录,包括任何必要的上级目录,并返回nil,否则返回错误。权限位perm会应用在每一个被本函数创建的目录上。如果path指定了一个已经存在的目录,MkdirAll不做任何操作并返回nil。6. func Rename(oldpath, newpath string) errorRename修改一个文件的名字,移动一个文件。可能会有一些个操作系统特定的限制。7. func RemoveAll(path string) errorRemoveAll删除path指定的文件,或目录及它包含的任何下级对象。它会尝试删除所有东西,除非遇到错误并返回。如果path指定的对象不存在,RemoveAll会返回nil而不返回错误。8. func TempDir() stringTempDir返回一个用于保管临时文件的默认目录。
1,1.3. 错误信息
1. type PathErrortype PathError struct {Op stringPath stringErr error}PathError记录一个错误,以及导致错误的路径。2. func (e *PathError) Error() string返回 PathError 错误字符串3. func IsExist(err error) bool返回一个布尔值说明该错误是否表示一个文件或目录已经存在。ErrExist和一些系统调用错误会使它返回真。4. func IsPermission(err error) bool返回一个布尔值说明该错误是否表示因权限不足要求被拒绝。ErrPermission和一些系统调用错误会使它返回真。
1.1.4. 文件属性
1. type FileInfotype FileInfo interface {Name() string // 文件的名字(不含扩展名)Size() int64 // 普通文件返回值表示其大小;其他文件的返回值含义各系统不同Mode() FileMode // 文件的模式位ModTime() time.Time // 文件的修改时间IsDir() bool // 等价于Mode().IsDir()Sys() interface{} // 底层数据来源(可以返回nil)}FileInfo用来描述一个文件对象的接口2. func Stat(name string) (fi FileInfo, err error)Stat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接,返回的FileInfo描述该符号链接指向的文件的信息,本函数会尝试跳转该链接。如果出错,返回的错误值为*PathError类型。3. func Lstat(name string) (fi FileInfo, err error)Lstat返回一个描述name指定的文件对象的FileInfo。如果指定的文件对象是一个符号链接,返回的FileInfo描述该符号链接的信息,本函数不会试图跳转该链接。如果出错,返回的错误值为*PathError类型。
1.1.5. 文件读写
1. type File struct { }File代表一个打开的文件对象。2. type FileMode uint32FileMode代表文件的模式和权限位。这些字位在所有的操作系统都有相同的含义,因此文件的信息可以在不同的操作系统之间安全的移植。不是所有的位都能用于所有的系统,唯一共有的是用于表示目录的ModeDir位。const (// 单字符是被String方法用于格式化的属性缩写。ModeDir FileMode = 1 << (32 - 1 - iota) // d: 目录ModeAppend // a: 只能写入,且只能写入到末尾ModeExclusive // l: 用于执行ModeTemporary // T: 临时文件(非备份文件)ModeSymlink // L: 符号链接(不是快捷方式文件)ModeDevice // D: 设备ModeNamedPipe // p: 命名管道(FIFO)ModeSocket // S: Unix域socketModeSetuid // u: 表示文件具有其创建者用户id权限ModeSetgid // g: 表示文件具有其创建者组id的权限ModeCharDevice // c: 字符设备,需已设置ModeDeviceModeSticky // t: 只有root/创建者能删除/移动文件// 覆盖所有类型位(用于通过&获取类型位),对普通文件,所有这些位都不应被设置ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDeviceModePerm FileMode = 0777 // 覆盖所有Unix权限位(用于通过&获取类型位))
1. func Create(name string) (file *File, err error)Create采用模式0666创建一个名为name的文件,如果文件已存在会截断它。如果成功,返回的文件对象可用于I/O;对应的文件描述符具有O_RDWR模式。如果出错,错误底层类型是*PathError。2. func Open(name string) (file *File, err error)Open打开一个文件用于读取。如果操作成功,返回的文件对象的方法可用于读取数据;对应的文件描述符具有O_RDONLY模式。如果出错,错误底层类型是*PathError。3. func OpenFile(name string, flag int, perm FileMode) (file *File, err error)OpenFile是一个更一般性的文件打开函数,大多数调用者都应用Open或Create代替本函数。4. func (f *File) Close() errorClose关闭文件f,使文件不能用于读写。它返回可能出现的错误。5. func (f *File) Read(b []byte) (n int, err error)Read方法从f中读取最多len(b)字节数据并写入b。它返回读取的字节数和可能遇到的任何错误。文件终止标志是读取0个字节且返回值err为io.EOF。6. func (f *File) ReadAt(b []byte, off int64) (n int, err error)ReadAt从指定的位置(相对于文件开始位置)读取len(b)字节数据并写入b。它返回读取的字节数和可能遇到的任何错误。当n<len(b)时,本方法总是会返回错误;如果是因为到达文件结尾,返回值err会是io.EOF。7. func (f *File) Write(b []byte) (n int, err error)Write向文件中写入len(b)字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值n!=len(b),本方法会返回一个非nil的错误。8. func (f *File) WriteString(s string) (ret int, err error)WriteString类似Write,但接受一个字符串参数。9. func (f *File) WriteAt(b []byte, off int64) (n int, err error)WriteAt在指定的位置(相对于文件开始位置)写入len(b)字节数据。它返回写入的字节数和可能遇到的任何错误。如果返回值n!=len(b),本方法会返回一个非nil的错误。10. func (f *File) Seek(offset int64, whence int) (ret int64, err error)Seek设置下一次读/写的位置。offset为相对偏移量,而whence决定相对位置:0为相对文件开头,1为相对当前位置,2为相对文件结尾。它返回新的偏移量(相对开头)和可能的错误11. func (f *File) Sync() (err error)Sync递交文件的当前内容进行稳定的存储。一般来说,这表示将文件系统的最近写入的数据在内存中的拷贝刷新到硬盘中稳定保存。
1.1.6. 目录操作
1. func (f *File) Chdir() errorChdir将当前工作目录修改为f,f必须是一个目录。如果出错,错误底层类型是*PathError。2. func (f *File) Readdir(n int) (fi []FileInfo, err error)Readdir读取目录f的内容,返回一个有n个成员的[]FileInfo,这些FileInfo是被Lstat返回的,采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。如果n>0,Readdir函数会返回一个最多n个成员的切片。这时,如果Readdir返回一个空切片,它会返回一个非nil的错误说明原因。如果到达了目录f的结尾,返回值err会是io.EOF。如果n<=0,Readdir函数返回目录中剩余所有文件对象的FileInfo构成的切片。此时,如果Readdir调用成功(读取所有内容直到结尾),它会返回该切片和nil的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的FileInfo构成的切片和该错误。3. func (f *File) Readdirnames(n int) (names []string, err error)Readdir读取目录f的内容,返回一个有n个成员的[]string,切片成员为目录中文件对象的名字,采用目录顺序。对本函数的下一次调用会返回上一次调用剩余未读取的内容的信息。如果n>0,Readdir函数会返回一个最多n个成员的切片。这时,如果Readdir返回一个空切片,它会返回一个非nil的错误说明原因。如果到达了目录f的结尾,返回值err会是io.EOF。如果n<=0,Readdir函数返回目录中剩余所有文件对象的名字构成的切片。此时,如果Readdir调用成功(读取所有内容直到结尾),它会返回该切片和nil的错误值。如果在到达结尾前遇到错误,会返回之前成功读取的名字构成的切片和该错误。
1.1.7. 进程相关
1. type Process struct { Pid int }2. type ProcessState struct { }3. func Getpid() intGetpid返回调用者所在进程的进程ID。4. func Getppid() intGetppid返回调用者所在进程的父进程的进程ID。5. func Getuid() intGetuid返回调用者的用户ID。6. func Getgid() intGetgid返回调用者的组ID。7. func Exit(code int)Exit让当前程序以给出的状态码code退出。一般来说,状态码0表示成功,非0表示出错。程序会立刻终止,defer的函数不会被执行8. func (p *Process) Signal(sig Signal) errorSignal方法向进程发送一个信号。在windows中向进程发送Interrupt信号尚未实现。9. func (p *Process) Kill() errorKill让进程立刻退出。10. func (p *Process) Wait() (*ProcessState, error)Wait方法阻塞直到进程退出,然后返回一个描述ProcessState描述进程的状态和可能的错误。Wait方法会释放绑定到进程p的所有资源。在大多数操作系统中,进程p必须是当前进程的子进程。11. func FindProcess(pid int) (p *Process, err error)FindProcess根据进程id查找一个运行中的进程。函数返回的进程对象可以用于获取其关于底层操作系统进程的信息。13. func (p *ProcessState) Success() boolSuccess报告进程是否成功退出,如在Unix里以状态码0退出。14. func (p *ProcessState) Exited() boolExited报告进程是否已退出。
1.2. os/user
1. type Usertype User struct {Uid string // 用户IDGid string // 初级组IDUsername stringName stringHomeDir string}2. func Current() (*User, error)返回当前的用户帐户。3. func Lookup(username string) (*User, error)根据用户名查询用户。4. func LookupId(uid string) (*User, error)根据用户ID查询用户。
1.3. os/exec
exec包执行外部命令。它包装了os.StartProcess函数以便更容易的修正输入和输出,使用管道连接I/O,以及作其它的一些调整。
1.3.1. 错误信息
1. func LookPath(file string) (string, error)在环境变量PATH指定的目录中搜索可执行文件,如file中有斜杠,则只在当前目录搜索。返回完整路径或者相对于当前目录的一个相对路径。2. var ErrNotFound = errors.New("executable file not found in $PATH")如果路径搜索没有找到可执行文件时,就会返回本错误3. type Errortype Error struct {Name stringErr error}Error类型记录执行失败的程序名和失败的原因。4. func (e *Error) Error() string返回错误信息type ExitErrortype ExitError struct {*os.ProcessState}ExitError报告某个命令的一次未成功的返回。5. func (e *ExitError) Error() string返回错误信息
1.3.2. 命令执行
1. type Cmdtype Cmd struct {// Path是将要执行的命令的路径。// 该字段不能为空,如为相对路径会相对于Dir字段。Path string// Args保管命令的参数,包括命令名作为第一个参数;如果为空切片或者nil,相当于无参数命令。// 典型用法下,Path和Args都应被Command函数设定。Args []string// Env指定进程的环境,如为nil,则是在当前进程的环境下执行。Env []string// Dir指定命令的工作目录。如为空字符串,会在调用者的进程当前目录下执行。Dir string// Stdin指定进程的标准输入,如为nil,进程会从空设备读取(os.DevNull)Stdin io.Reader// Stdout和Stderr指定进程的标准输出和标准错误输出。// 如果任一个为nil,Run方法会将对应的文件描述符关联到空设备(os.DevNull)// 如果两个字段相同,同一时间最多有一个线程可以写入。Stdout io.WriterStderr io.Writer// ExtraFiles指定额外被新进程继承的已打开文件流,不包括标准输入、标准输出、标准错误输出。// 如果本字段非nil,entry i会变成文件描述符3+i。// BUG: 在OS X 10.6系统中,子进程可能会继承不期望的文件描述符。// http://golang.org/issue/2603ExtraFiles []*os.File// SysProcAttr保管可选的、各操作系统特定的sys执行属性。// Run方法会将它作为os.ProcAttr的Sys字段传递给os.StartProcess函数。SysProcAttr *syscall.SysProcAttr// Process是底层的,只执行一次的进程。Process *os.Process// ProcessState包含一个已经存在的进程的信息,只有在调用Wait或Run后才可用。ProcessState *os.ProcessState// 内含隐藏或非导出字段}2. func Command(name string, arg ...string) *Cmd函数返回一个*Cmd,用于使用给出的参数执行name指定的程序。返回值只设定了Path和Args两个参数。如果name不含路径分隔符,将使用LookPath获取完整路径;否则直接使用name。参数arg不应包含命令名。3. func (c *Cmd) Run() errorRun执行c包含的命令,并阻塞直到完成。如果命令成功执行,stdin、stdout、stderr的转交没有问题,并且返回状态码为0,方法的返回值为nil;如果命令没有执行或者执行失败,会返回*ExitError类型的错误;否则返回的error可能是表示I/O问题。4. func (c *Cmd) Start() errorStart开始执行c包含的命令,但并不会等待该命令完成即返回。Wait方法会返回命令的返回状态码并在命令返回后释放相关的资源。5. func (c *Cmd) Output() ([]byte, error)执行命令并返回标准输出的切片,内部包含run方法。6. func (c *Cmd) CombinedOutput() ([]byte, error)执行命令并返回标准输出和错误输出合并的切片,内部包含run方法。7. func (c *Cmd) StdinPipe() (io.WriteCloser, error)StdinPipe方法返回一个在命令Start后与命令标准输入关联的管道。Wait方法获知命令结束后会关闭这个管道。8. func (c *Cmd) StdoutPipe() (io.ReadCloser, error)StdoutPipe方法返回一个在命令Start后与命令标准输出关联的管道。Wait方法获知命令结束后会关闭这个管道,一般不需要显式的关闭该管道。但是在从管道读取完全部数据之前调用Wait是错误的,同样使用StdoutPipe方法时调用Run函数也是错误的9. func (c *Cmd) StderrPipe() (io.ReadCloser, error)StderrPipe方法返回一个在命令Start后与命令标准错误输出关联的管道。Wait方法获知命令结束后会关闭这个管道,一般不需要显式的关闭该管道。但是在从管道读取完全部数据之前调用Wait是错误的;同样使用StderrPipe方法时调用Run函数也是错误的10. func (c *Cmd) Wait() errorWait会阻塞直到该命令执行完成,该命令必须是被Start方法开始执行的。如果命令成功执行,stdin、stdout、stderr的转交没有问题,并且返回状态码为0,方法的返回值为nil;如果命令没有执行或者执行失败,会返回*ExitError类型的错误;否则返回的error可能是表示I/O问题。Wait方法会在命令返回后释放相关的资源。11. func (c *Cmd) String() string打印出 cmd 命令的字符串
2. 案例
2.1. os包
2.1.1. 创建和遍历目录
func main() {dirLs, _ := ioutil.ReadDir("../exec") // 遍历目录for _, i := range dirLs {fmt.Printf("name:%v;size:%v\n", i.Name(), i.Size())}dirName := "/tmp/a/b/c/"_ = os.MkdirAll(dirName, 0755) // 递归创建目录_ = os.Chdir(dirName) // 切换目录_ = os.Mkdir("xxx", 0755) // 创建单层目录_ = os.Rename("xxx", "abc") // 重命名目录_ = os.RemoveAll("abc") // 递归删除目录}
2.1.2. 获取文件信息
func getFileExist(file string) (stat bool) {info, err := os.Stat(file)if os.IsNotExist(err) {mylogger.Log.Error("file %s not exist", file)return}if os.IsPermission(err) {mylogger.Log.Error("file %s permission denied", file)return}mylogger.Log.Info("name:%s;size:%v;mode:%v;modified:%v", file, info.Size(), info.Mode(), info.ModTime())return true}func main() {getFileExist("../exec/cmd01.go")getFileExist("/etc/passwdxxx")getFileExist("/root/.bashrc")}
[duduniao@heyingsheng meta]$ go run file.go2020-05-23 23:27:33|INFO|file.go:getFileExist:18|name:../exec/cmd01.go;size:587;mode:-rw-r--r--;modified:2020-05-23 19:27:19.5998142 +0800 CST2020-05-23 23:27:33|ERROR|file.go:getFileExist:11|file /etc/passwdxxx not exist2020-05-23 23:27:33|ERROR|file.go:getFileExist:15|file /root/.bashrc permission denied
2.1.3. 文件读取
func main() {file := "/etc/passwd"readObj,err := os.Open(file)if err != nil {fmt.Printf("Open file %s failed, err:%v\n", file, err)return}defer readObj.Close()var tmp [128]bytevar content stringfor {n, err := readObj.Read(tmp[:]) // read 是一段一段的读取,每次长度不超过 len(tmp)if err == io.EOF {break}content = content + string(tmp[:n])}fmt.Print(content)}
2.1.4. 文件写入
经过测试,发现不需要刷盘也能正常保存文件。
func main() {file := "test.txt"writeObj, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)if err != nil {fmt.Printf("Open file %s failed, err:%v\n", file, err)return}defer writeObj.Close()_, err = writeObj.WriteString("root:x:0:0:root:/root:/bin/bash"+"\n") // 写字符串if err != nil {fmt.Printf("write file %s failed, err:%v\n", file, err)return}_, err = writeObj.Write([]byte("Hello world\n")) // 写切片if err != nil {fmt.Printf("write file %s failed, err:%v\n", file, err)return}}
2.2. exec包
Go语言调用系统命令时需要使用 os/exec 包中的内容,虽然一般用的不多,当时在部分场景中调用系统命令比调用接口代码量低很多,如做自动化运维开发中,使用Go调用ansible命令执行playbook就比调用ansible API方便太多了。
2.2.1. 常用的命令执行方式
var wg01 sync.WaitGroup// 执行命令并返回结果func command(exe string, args ...string) {defer wg01.Done()cmd := fmt.Sprint(exe, " ", strings.Join(args, " "))res, err := exec.Command(exe, args...).CombinedOutput()if err != nil {fmt.Printf("run command \"%s\" failed, err:%v\n", cmd, err)return}fmt.Printf("command \"%s\" res: %v", cmd, string(res))}func main() {wg01.Add(2)go command("date", "-d", "-10 min")go command("find", "/etc", "-maxdepth", "1", "-name", "passwd")wg01.Wait()}
[root@heyingsheng exec]# go run cmd01.gocommand "date -d -10 min" res: Sat May 23 19:17:21 CST 2020command "find /etc -maxdepth 1 -name passwd" res: /etc/passwd
2.2.2. 管道问题
exec 在执行 Cmd 是不能使用 shell 中的管道,可以使用以下方式处理:
var wg02 sync.WaitGroup// 执行命令并返回结果func command02(cmd string) {defer wg02.Done()res, err := exec.Command("sh", "-c", cmd).CombinedOutput()if err != nil {fmt.Printf("run command %s failed, err:%v\n", cmd, err)return}fmt.Print(string(res))}func main() {wg02.Add(2)go command02(fmt.Sprintf("find %s -type f | xargs -I {} readlink -f {} ", "../"))go command02("awk -F: '/root/{print $1}' /etc/passwd|tr 'a-z' 'A-Z'")wg02.Wait()}
[root@heyingsheng exec]# go run cmd02.goROOT/mnt/e/Projects/Go/src/learn/day12/os/exec/cmd01.go/mnt/e/Projects/Go/src/learn/day12/os/exec/cmd02.go
2.2.3. 命令输出处理
如果需要将输出的日志实时打印出来,要么将输出写入文件,再通过Go监控文件中的内容,要么使用以下方式Pipe管道方式实现:
var wg03 sync.WaitGroup// 日志打印功能func outToLog(output io.ReadCloser, level string) {var tmp [1024]bytefor {n, err := output.Read(tmp[:])if err != nil {break}if level == "info" {mylogger.Log.Info("%s", strings.Trim(string(tmp[:n]), "\n"))continue}mylogger.Log.Error("%s", strings.Replace(string(tmp[:n]), "\n", " ", -1))}}// 执行命令并返回结果func command03(shell, cmd string) {defer wg03.Done()exe := exec.Command(shell, "-c", cmd)stdout, err := exe.StdoutPipe()if err != nil {mylogger.Log.Error("set stdout err:%v", err)return}stderr, err := exe.StderrPipe()if err != nil {mylogger.Log.Error("set stderr err:%v", err)return}err = exe.Start()if err != nil {mylogger.Log.Error("start command err:%v", err)return}defer exe.Wait()go outToLog(stdout, "info")go outToLog(stderr, "error")}func main() {wg03.Add(2)_ = os.Chdir("/mnt/e/Projects/repo_dade555fca7c4f8bae7135c5c599752c/deploy-worker/vm_deploy_ansible")go command03("bash", "ansible-playbook -i inventory/cluster01/jumpserver-id.yml playbook/cluster.yaml -t create")go command03("bash", "ansible-playbook -i inventory/pvc01/pvc-id.yml playbook/pvc.yaml -t create")wg03.Wait()}
[root@heyingsheng exec]# go run cmd03.go2020-05-23 21:25:49|INFO|cmd03.go:outToLog:23|ansible|vm_pvc|create|set variabels|node_name0|True|2020-05-23 21:25:49|INFO|cmd03.go:outToLog:23|ansible|vm_pvc|create|create "pvc02"|node_name0|True|2020-05-23 21:25:49|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|set variabels|node_name0|True|2020-05-23 21:25:49|INFO|cmd03.go:outToLog:23|ansible|vm_pvc|create|return create "pvc02" result|node_name0|True|2020-05-23 21:25:50|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|set variabels|node_name1|True|2020-05-23 21:25:50|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|set variabels|node_name2|True|2020-05-23 21:25:50|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|set monitor varibales|node_name0|True|2020-05-23 21:25:50|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|set monitor varibales|node_name1|True|2020-05-23 21:25:50|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|set monitor varibales|node_name2|True|2020-05-23 21:25:51|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|update sources.list|node_name0|True|2020-05-23 21:25:51|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|update sources.list|node_name2|True|2020-05-23 21:25:51|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|update sources.list|node_name1|True|2020-05-23 21:25:54|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|update apt cache|node_name0|True|2020-05-23 21:25:54|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|update apt cache|node_name2|True|2020-05-23 21:25:55|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|update apt cache|node_name1|True|2020-05-23 21:25:56|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|install docker required packages|node_name0|True|2020-05-23 21:25:56|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|install docker required packages|node_name1|True|2020-05-23 21:25:56|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|install docker required packages|node_name2|True|2020-05-23 21:25:58|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|add docker GPG key|node_name2|True|2020-05-23 21:25:59|INFO|cmd03.go:outToLog:23|ansible|vm_cluster|create|add docker GPG key|node_name0|True|2020-05-23 21:25:59|ERROR|cmd03.go:outToLog:26|[WARNING]: Failure using method (v2_playbook_on_stats) in callback plugin (<ansible.plugins.callback.message_mq.CallbackModule object at 0x7fc73516b908>):
2.2.4. 僵尸进程问题
在容器中运行命令的时候,如果使用 cmd.Start() 却没有使用 cmd.Wait() 就会出现进程资源没有释放,从而出现僵尸进程。
