banner
Rick Sanchez

Rick Sanchez

OS && DB 爱好者,深度学习炼丹师,蒟蒻退役Acmer,二刺螈。

Golang中管道的用法總結

1. 什麼是管道#

管道是一種用於 進程間通信 (IPC) 的機制,它允許一個進程的輸出直接作為下一個進程的輸入,它是一種通過 內核緩衝區 實現的通信方式。

它的特點:

  1. 單向通信:標準管道中,數據只能沿一個方向流動(雙向通信可以用命名管道實現)
  2. 半雙工:數據可以在兩個方向上流動,但不能同時進行
  3. 臨時性:管道通常用於短期的進程間通信
  4. 內核緩衝區:數據通過內核緩衝區傳遞,而不是共享內存的方式

2. 使用重定向 IO 的方式實現管道#

我們要實現一個類似 ps -aux | grep top 的功能,在 Golang 中我們可以使用下面的方式。

簡單來說,就是我們創建一個緩衝區,然後重定向進程的 stdin/stdout 到這個緩衝區,這樣就實現了共享內存的通信方法。

Tip

在這裡,我們緩衝區使用 Golang bytes包中的bytes.Buffer對象,它提供了一個緩衝區,用於高效的構建和操作字節切片,它可以動態增長,適用於構建和處理頻繁拼接和修改的字節數據。

package main

import (
	"bytes"
	"fmt"
	"os/exec"
)

func main() {
	cmd1 := exec.Command("ps", "aux")
	cmd2 := exec.Command("grep", "xxx")

	var outputBuf1 bytes.Buffer
	cmd1.Stdout = &outputBuf1

	if err := cmd1.Start(); err != nil {
		fmt.Printf("Error: The first command can't be start up %s\n", err)
		return
	}
	if err := cmd1.Wait(); err != nil {
		fmt.Printf("Error: Couldn't wait for the first command: %s\n", err)
		return
	}

	cmd2.Stdin = &outputBuf1
	var outputBuf2 bytes.Buffer
	cmd2.Stdout = &outputBuf2
	if err := cmd2.Start(); err != nil {
		fmt.Printf("Error: The second command can't be start up %s\n", err)
		return
	}

	if err := cmd2.Wait(); err != nil {
		fmt.Printf("Error: Couldn't wait for the second command: %s\n", err)
		return
	}

	fmt.Printf("%s\n", outputBuf2.String())
}

3. Golang 中管道的用法#

Golang 中提供了 2 種管道的使用方法,os.Pipe()io.Pipe(),這是兩種不同的實現方法,前者依賴操作系統的管道機制,後者是使用 Golang 實現的,他們都是匿名管道。

特性os.Pipe()io.Pipe()
實現層面操作系統級別管道,使用底層系統調用創建。純 Go 實現的內存管道,不涉及操作系統調用。
使用場景適合與外部進程通信或在不同的操作系統線程間通信。主要用於同一 Go 程序的不同 Goroutine 間的數據傳遞。
性能對於大量數據傳輸,較高效,利用操作系統的緩衝區。小數據量傳輸較快,但不適合大量數據傳輸。
跨平台兼容性行為可能因操作系統不同而異。跨平台行為一致,因為純 Go 實現。
文件描述符返回 *os.File 類型,包含底層文件描述符。返回 io.Readerio.Writer 接口,不涉及文件描述符。
關閉行為需要手動關閉讀端和寫端的文件描述符。一端關閉時,另一端自動返回 EOF。
多路復用支持操作系統級多路復用(如 selectpollepoll),適合處理多個 I/O 源。不直接支持操作系統多路復用,但可通過 channelselect 實現類似效果。
原子操作操作系統保證小於等於 PIPE_BUF(通常為 4096 字節)的寫入操作是原子的。大於此的寫操作可能被分割。所有寫操作原子性由 Go 運行時確保,利用互斥鎖保證並發安全。
  • os.Pipe() 更適合用於系統級別的任務,比如跨進程通信、標準輸入輸出的重定向等。
  • io.Pipe() 更適合 Go 內部的並發編程,用於 Goroutine 間的數據流傳遞,可以與 Go 的並發特性(如 channelselect)無縫結合。

3.1 os.Pipe()#

package main

import (
	"bytes"
	"fmt"
	"os"
	"sync"
)

func main() {
	reader, writer, err := os.Pipe()
	var wg sync.WaitGroup

	if err != nil {
		fmt.Printf("Error creating pipe: %v\n", err)
		return
	}

	wg.Add(1)
	// 管道這裡不管是讀還是寫都存在阻塞
	go func() {
		defer wg.Done()
		output := make([]byte, 64)
		n, err := reader.Read(output)
		if err != nil {
			fmt.Printf("Error reading from pipe: %v\n", err)
			return
		}
		fmt.Printf("Read %d bytes\n", n)
	}()

	var inputs bytes.Buffer
	for i := 65; i <= 90; i++ {
		inputs.WriteByte(byte(i))
	}
	n, err := writer.Write(inputs.Bytes())
	if err != nil {
		fmt.Printf("Error writing to pipe: %v\n", err)
		return
	}
	fmt.Printf("Wrote %d bytes\n", n)
	wg.Wait()
}

3.2 io.Pipe()#

io.Pipe() 是一個純內存管道,由 Go 語言在內存中實現。其性能限制主要來源於以下幾個方面:

  • 內存緩衝io.Pipe() 沒有底層操作系統的支持,而是通過 Go 的緩衝和同步機制在內存中實現。由於沒有直接的內核支持,它需要通過互斥鎖(Mutex)和條件變量(Cond)來實現讀寫同步和阻塞,這在高頻、大量數據傳輸時會成為性能瓶頸。

  • Goroutine 調度開銷io.Pipe() 的設計目標是在 Goroutine 間傳遞數據。因此,數據傳遞和阻塞喚醒都在 Go 運行時的 Goroutine 調度中進行。頻繁的數據傳遞或大量 Goroutine 的場景下,這種調度開銷會降低性能。

  • 系統級緩衝缺乏:操作系統的內核層面通常會為文件描述符分配緩衝區(例如 os.Pipe()),而 io.Pipe() 缺乏此支持。在傳輸大量數據時,由於沒有內核緩衝,數據需要在內存中反復讀寫,增加了內存分配和垃圾回收的負擔。

io.Pipe()的使用方法和os.Pipe()基本一致,例子如下:

package main

import (
	"bytes"
	"fmt"
	"io"
	"sync"
)

func main() {
	reader, writer := io.Pipe()
	var wg sync.WaitGroup

	wg.Add(1)

	go func() {
		defer wg.Done()
		output := make([]byte, 256)
		n, err := reader.Read(output)
		if err != nil {
			fmt.Printf("Error reading from reader: %v\n", err)
			return
		}
		fmt.Printf("Read %d bytes\n", n)
	}()

	var inputs bytes.Buffer
	for i := 65; i <= 90; i++ {
		inputs.WriteByte(byte(i))
	}
	n, err := writer.Write(inputs.Bytes())
	if err != nil {
		fmt.Printf("Error writing to writer: %v\n", err)
		return
	}
	fmt.Printf("Wrote %d bytes\n", n)
	wg.Wait()
}

4. 多路復用#

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。