最近在工作中遇到了一个比较难解决的问题,简单来说就是,要在嵌入式设备里跑两个程序,一个是后台服务程序要常驻在系统中,另一个程序相当于是这个后台程序的入口程序,通过它向后台程序发送数据和命令以实现相应的功能。以往遇到这类的问题我都是用grpc来解决的,但是这回不能用grpc了,甚至连http服务都搭不起来,一来这个设备的性能非常差跑个网络服务会比较占用资源,二来我也试过编写一些后端的程序,只要涉及网络服务的在编译后都无法在设备上运行,好像是指令集缺东西,不兼容。
这时我就想起了当年学操作系统的时候背的进程间通信的几种方式:信号、信号量、共享内存、消息队列、管道、套接字。其中除了管道没在编程时用过,其他的几种都已经用过了,信号能传递的数据太少了,信号量一般用于进程间同步或者资源的分配,共享内存、消息队列用起来比较复杂,套接字其实就是用的网络服务的那一套,需要开端口来通信。而管道的话可以当成一个操作系统提供的队列,用起来简单,正好适合我的这个场景:一个进程负责读,一个进程负责写。
对于Linux、Mac等类Unix系统,go语言的标准库提供了读写管道的函数,在类Unix系统中,管道是以文件的形式存在的,所以对管道操作的函数和文件的类似。
下面简单地举个Linux、Mac下管道操作的例子:
接收数据的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport ( "bufio" "fmt" "log" "os" "syscall" ) const pipeFile = "/tmp/pipeFile.ipc" func main () { os.RemoveAll(pipeFile) err := syscall.Mkfifo(pipeFile, 0666 ) if err != nil { log.Fatal("create named pipe error:" , err) } pipe, err := os.OpenFile(pipeFile, os.O_RDWR, os.ModeNamedPipe) if err != nil { log.Fatal("open named pipe error:" , err) } reader := bufio.NewReader(pipe) fmt.Println("receiving message!" ) for { line, err := reader.ReadBytes('\n' ) if err != nil { log.Fatal("read named pipe error:" , err) } fmt.Printf("read message:%v" , string (line)) } }
首先要创建一个管道,一般由接收消息的程序来创建,创建好后,程序从管道中接收消息并进行处理。
发送数据的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "fmt" "log" "os" "strconv" "time" ) const pipeFile = "/tmp/pipeFile.ipc" func main () { pipe, err := os.OpenFile(pipeFile, os.O_RDWR, os.ModePerm) if err != nil { log.Fatal("open named pipe error:" , err) } var i = 0 fmt.Println("sending message!" ) for { _, err = pipe.WriteString("hello " + strconv.Itoa(i) + "\n" ) if err != nil { log.Fatal("write named pipe error:" , err) } time.Sleep(time.Second) i++ } }
发送数据时,先打开管道文件,然后再写入数据即可。
如果是 windows 系统的话go语言标准库中没有提供对管道操作的函数,我们可以用微软官方的winio库,github.com/Microsoft/go-winio Windows系统的很多理念和类Unix系统是不同的,Windows中的管道也是文件,不过不同的是,它与普通的文件的管理方式不同,普通文件放在硬盘中,而管道存放在“命名管道文件系统”(Named Pipe File System,NPFS)中,管道的本质是一块共享内存。
用winio库对管道进行操作的示例如下:
接收数据的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "fmt" "log" winio "github.com/Microsoft/go-winio" ) const pipePath = `\\.\pipe\demo` func main () { pipe, err := winio.ListenPipe(pipePath, nil ) if err != nil { log.Fatal("create named pipe error:" , err) } defer pipe.Close() conn, err := pipe.Accept() if err != nil { log.Fatal("accept error:" , err) } defer conn.Close() fmt.Println("receiving message!" ) for { buf := make ([]byte , 512 ) n, err := conn.Read(buf) if err != nil { log.Fatal("read named pipe error:" , err) } log.Printf("read message:%v" , string (buf[:n])) } }
发送数据的程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "log" "strconv" "time" winio "github.com/Microsoft/go-winio" ) const pipePath = `\\.\pipe\demo` func main () { conn, err := winio.DialPipe(pipePath, nil ) if err != nil { log.Fatal("open named pipe error:" , err) } defer conn.Close() var i int for { _, err := conn.Write([]byte ("hello " + strconv.Itoa(i) + "\n" )) if err != nil { log.Fatal("write named pipe error:" , err) } i++ time.Sleep(time.Second) } }
从这个例子中可以看出,winio库操作管道有点类似网络编程,winio库的介绍中也说了用法与go标准库中的net类似:“This code is similar to Go’s net package, and uses IO completion ports to avoid blocking IO on system threads, allowing Go to reuse the thread to schedule other goroutines.”。
今天这篇介绍了在go语言中怎样用管道实现进程间通信,对于不太复杂的进程间通信用管道能很优雅简单地解决。这一篇就到这里啦。欢迎大家点赞、转发、私信。