管道(pipe)是一种半双工的(或者说是单向的)通讯方式,它只能被用于父进程和子进程以及同祖先的子进程之间的通讯。
使用管道需注意以下四种情况:
- 如果所有指向管道写端的文件描述符都关闭了,仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
- 如果有指向管道写端的文件描述符没关闭,持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
- 如果所有指向管道读端的文件描述符都关闭了,这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
- 如果有指向管道读端的文件描述符没关闭,持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
匿名管道
实现linux上管道“|”的效果
例:ps aux | grep pipe
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os/exec"
)
func main() {
UnixPipe()
}
//将命令ps aux 的输出管道连接到grep pipe的输入管道
//并把输出管道里的数据全部写到输入管道里
func UnixPipe() {
fmt.Println("Run command `ps aux | grep apipe`: ")
cmd1 := exec.Command("ps", "aux")
cmd2 := exec.Command("grep", "apipe")
stdout1, err := cmd1.StdoutPipe() //cmd1上建立一个输出管道,为*io.Reader类型
if err != nil {
fmt.Printf("Error: Can not obtain the stdout pipe for command: %s", err)
return
}
if err := cmd1.Start(); err != nil {
fmt.Printf("Error: The command can not running: %s\n", err)
return
}
outputBuf1 := bufio.NewReader(stdout1) //避免数据过多带来的困扰,使用带缓冲的读取器来获取输出管道中的数据
stdin2, err := cmd2.StdinPipe() //cmd2上建立一个输入管道
if err != nil {
fmt.Printf("Error: Can not obtain the stdin pipe for command: %s\n", err)
return
}
outputBuf1.WriteTo(stdin2) //将缓冲读取器里的输出管道数据写入输入管道里
var outputBuf2 bytes.Buffer //获取cmd2的输出数据的字节缓冲器
cmd2.Stdout = &outputBuf2 //将缓冲器赋值给cmd2的输出字段,这样cmd2的所有输出内容就会被写入到缓冲器中
if err := cmd2.Start(); err != nil {
fmt.Printf("Error: The command can not be startup: %s\n", err)
return
}
err = stdin2.Close() //关闭cmd2的输入管道
if err != nil {
fmt.Printf("Error: Can not close the stdio pipe: %s\n", err)
return
}
if err := cmd2.Wait(); err != nil { //为了获取cmd2的所有输出内容,调用Wait()方法一直阻塞到其所属所有命令执行完
fmt.Printf("Error: Can not wait for the command: %s\n", err)
return
}
fmt.Printf("%s\n", outputBuf2.Bytes()) //输出执行结果
}
命名管道
命名管道(name pipe)又称做FIFO,
命名管道与匿名管道不同的是命名管道以文件的形式存在于文件系统中,使用它的方式和使用文件很类似。任何进程都可以使用命名管道交换数据。
例:
# mkfifo -m 644 fifo
# tee dst.txt < fifo &
[1]22342
# cat src.txt > fifo
示例中首先使用了mkfifo命令创建了一条命名管道fifo,该管道做为dst.txt的输入管道,src.txt的输出管道,将src.txt的数据全部写入到dst.txt中。在此基础上我们还可以实现数据的过滤和转换,和管道的多路复用功能。
命名管道是阻塞式的,更具体的说就是只有当管道的读操作和写操作都就绪后,数据才进行传输。相对于匿名管道,命名管道的最大优势就是通讯双方可以毫不相关。并且我们可以使用它进行非线性的多路复用。但要注意,命名管道仍是单向的。又由于我们可以在命名官道上实现多路复用,所以有时候也要考虑多个进程同时向命名管道写数据的情况下的原子操作。
在Go语言代码标准库包os中包含了可以创建这种独立管道的API,创建一个命名管道非常简单,如下:
reader, writer, err := os.Pipe()
函数Pipe()会返回三个值,第一个是代表了该管道输出端的*os.File类型值,第二个是代表了该管道输入端的*os.File类型值。
假设有两段这样的代码:
output := make([]byte, 100)
n, err := reader.Read(output)
if err != nil {
fmt.Printf("Error: Can not read data from the named pipe: %s\n", err)
}
fmt.Printf("Read %d byte(s). [file-based pipe]\n", n)
和
n, err := writer.Write(input)
if err != nil {
fmt.Printf("Error: Can not write data to the named pipe: %s\n", err)
}
fmt.Printf("Written %d byte(s). [file-based pipe]\n", n)
如果他们是被并发运行的,那么我们在reader上调用Read方法就能获取到通过writer调用的Write方法写入的数据。为什么强调是并发运行?因为命名管道默认会在其中一端还未就绪的时候阻塞另一端的进程。Go语言在这里提供给我们的命名管道也是如此。所以,如果我们顺序的执行上面的两段代码,那么程序肯定会永远阻塞在
n, err := reader.Read(output)
或
n, err := writer.Write(input)
出现的地方,具体阻塞在哪取决于调用表达式reader.Read(output)和writer.Write(input)的顺序。
另外,我们知道管道是单向的。因此我们不能反过来用reader和writer,也就是说,在reader上调用Write方法和在writer上调用Read方法,返回的err值都不为空,err值里的信息会告诉我们这样的访问是不被允许的。另外,不论我们在哪一方调用Close方法,都不会影响另一方的读取或写入数据的操作。
实际上,我们在类型值上exec.Cmd上调用StdoutPipe和StdinPipe方法后的到的输入管道或者输出管道也是通过os.Pipe生成的,只不过这两个方法对os.Pipe生成的管道做了些附加处理,输入管道的输出端会在所属命令启动后被立即关闭,输入端会在所属命名结束后关闭。输出管道同理,输入端会在所属命令启动后被立即关闭,输出端会在所属命名结束后关闭。不过要注意,有些命令会等到输入管道关闭之后才会结束运行。所以,在这种情况下我们就应该在数据被读取之后尽早地手动关闭输入管道。在前面的示例中,我们已经有过演示:
if err := cmd2.Start(); err != nil {
fmt.Printf("Error: The command can not be startup: %s\n", err)
return
}
err = stdin2.Close() //关闭cmd2的输入管道
if err != nil {
fmt.Printf("Error: Can not close the stdio pipe: %s\n", err)
return
}
if err := cmd2.Wait(); err != nil { //为了获取cmd2的所有输出内容,调用Wait()方法一直阻塞到其所属所有命令执行完
fmt.Printf("Error: Can not wait for the command: %s\n", err)
return
}
在必要时依照上面这样的操作顺序,由于输出管道实际上也是由os.Pipe函数生成的,所以我们在使用exec.Cmd类型值上的输出管道时也要注意。例如,我们不能在读取完输出管道数据之前调用该值的wait方法。又例如,只要我们建立了对应的输出管道,就不能使用Run方法来启动该命令,而应该使用Start方法。
命名管道可以被多路复用,所以当有多个输入端同时写入数据的时候我们就不得不考虑操作的原子性问题。由于os.Pipe函数生成的管道在底层是由操作系统级别的管道来支持的,操作系统提供的管道并不提供原子操作支持。为此,Go语言标准库代码包io中提供了一种被存在于内存中、具备原子操作的保证的管道(以下称为内存管道)。创建方式如下:
reader, writer := io.Pipe()
函数io.Pipe返回两个结果值,第一个代表了输出端的
*PipeReader类型值,第二个代表了输入端的*PipeWriter类型值。*PipeReader和*PipeWriter分别对管道的输出端和输入端做了很好的限制操作。即在*PipeReader上我们只能使用Read方法读取管道中的数据,在*PipeWriter上我们只能使用Write方法向管道写入数据。这样就有效避免了使用者反向使用管道。另一方面,我们在使用Close方法关闭管道的某一端之后,另一端在写数据或者读数据的时候就会收到一个预定义的error类型值,我们可以调用CloseWithError来自定义另一端收到的error类型值。
在内存管道内部是通过充分使用sync代码包中的API来保证管道操作的原子性,所以我们可以放心的使用内存管道并发地读取或者写入数据。另外,这种管道并不是基于文件系统的,并没有作为中介的缓冲区,所以通过它传递的数据只会被复制一次,这也就更进一步提高了数据传输的效率。
参考资料
《Go并发编程实战》
《实现进程间通信:匿名管道和命名管道》http://10541556.blog.51cto.com/10531556/1762101