## 介绍
采用管道的通信方式,在Go程序中运行 `子进程` 并实现通信。✉️
按照通用的规则,可以解耦程序,实现插件化。
字节开源的安全项目 `Elkeid` 就是使用的这种方式实现插件化。
https://github.com/bytedance/Elkeid/blob/main/agent/plugin/plugin_linux.go#L43
进程间的通信方式参考下面文章:
[进程间通信IPC (InterProcess Communication)](https://www.jianshu.com/p/c1015f5ffa74)
[进程间的通信方式——pipe(管道)](https://blog.csdn.net/skyroben/article/details/71513385)
[go os/exec 简明教程](https://colobu.com/2020/12/27/go-with-os-exec/)
## 示例
**pstree**
```shell
➜ testProject pstree 29475
-+= 29475 zhangshun ./server
\--- 29476 zhangshun /Users/zhangshun/hll/gitlab/testProject/test/pipe/client/plugin
```
**parent**
```golang
func main() {
workDir := "/Users/zhangshun/hll/gitlab/testProject/test/pipe/client"
execPath := path.Join(workDir, "plugin")
cmd := exec.Command(execPath)
// 创建两个管道,实现全双工
parentReader, childWriter, err := os.Pipe()
if err != nil {
panic(err)
}
childReader, parentWriter, err := os.Pipe()
if err != nil {
panic(err)
}
// 创建子进程的标准错误文件
errFile, err := os.OpenFile(execPath+".stderr", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0o644)
defer errFile.Close()
if err != nil {
panic(err)
}
cmd.Dir = workDir
cmd.Stderr = errFile
// 将父进程打开的文件传给子进程
// 除了标准输入输出0,1,2三个文件外,还可以将父进程的文件传给子进程
cmd.ExtraFiles = append(cmd.ExtraFiles, childReader, childWriter)
cmd.Start()
wg := &sync.WaitGroup{}
wg.Add(3)
// 等待子进程结束
go func() {
defer wg.Done()
cmd.Wait()
parentReader.Close()
parentWriter.Close()
}()
// 读取子进程
go func() {
defer wg.Done()
reader := bufio.NewReader(parentReader)
buf := make([]byte, 1024)
for {
n, err := reader.Read(buf)
if err != nil {
fmt.Println(err)
}
fmt.Print(string(buf[:n]))
}
}()
// 写入子进程
go func() {
defer wg.Done()
writer := bufio.NewWriterSize(parentWriter, 1024*256)
for p := 0; p < 10; p++ {
content := []byte(fmt.Sprintf("parent write data: %d\n", p))
_, err := writer.Write(content)
if err != nil {
fmt.Println(err)
}
err = writer.Flush()
if err != nil {
fmt.Println(err)
}
time.Sleep(time.Second * 1)
}
}()
wg.Wait()
}
```
**child**
```golang
func main() {
// 实例化父进程传过来的fd
// 除了标准输入输出0,1,2三个文件外,还可以将父进程的文件传给子进程
reader := bufio.NewReaderSize(os.NewFile(3, "pipe"), 1024*128)
writer := bufio.NewWriterSize(os.NewFile(4, "pipe"), 1024*128)
// 把父进程的输入写到文件中
logFile, err := os.OpenFile("plugin.log", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0o644)
defer logFile.Close()
if err != nil {
panic(err)
}
logWriter := bufio.NewWriterSize(logFile, 2)
wg := &sync.WaitGroup{}
wg.Add(2)
// 读取父进程
go func() {
defer wg.Done()
buf := make([]byte, 1024)
for {
n, err := reader.Read(buf)
if err != nil && err != io.EOF {
break
}
logWriter.Write(buf[:n])
logWriter.Flush()
}
}()
// 写入父进程
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
content := []byte(fmt.Sprintf("child write data: %d\n", i))
writer.Write(content)
writer.Flush()
time.Sleep(time.Second * 1)
}
}()
wg.Wait()
}
```
## cmd 对象
```golang
type Cmd struct {
Path string // 运行命令的路径,绝对路径或者相对路径
Args []string // 命令参数
Env []string // 进程环境,如果环境为空,则使用当前进程的环境
Dir string // 指定command的工作目录,如果dir为空,则comman在调用进程所在当前目录中运行
Stdin io.Reader // 标准输入,如果stdin是nil的话,进程从null device中读取(os.DevNull),stdin也可以时一个
// 文件,否则的话则在运行过程中再开一个goroutine去/读取标准输入
Stdout io.Writer // 标准输出
Stderr io.Writer // 错误输出,如果这两个(Stdout和Stderr)为空的话,则command运行时将响应的文件描述符连接到
// os.DevNull
ExtraFiles []*os.File // 除了标准输入输出0,1,2三个文件外,还可以将父进程的文件传给子进程,打开的文件描述符切片,可为进程添加fd,比如 socket
SysProcAttr *syscall.SysProcAttr // 系统的进程属性
Process *os.Process // Process是底层进程,只启动一次,就是 os.StartProcess 返回的进程对象
ProcessState *os.ProcessState // ProcessState包含一个退出进程的信息,当进程调用Wait或者Run时便会产生该信息.
}
```
Go 进程通信