0%

《操作系统》IPC 通信

IPC 通信是指进程间通信

单个进程中,线程之间可以通过全局变量通信,但是进程拥有自己的用户空间,与其他进程相互隔离

无法通过变量通信,所以出现各种方式来解决 IPC 通信的问题

文件

存储在磁盘上的记录可以由多个进程访问,可以用来实现 IPC 通信。

管道

管道实际上就是内核控制的一个内存缓冲区,既然是缓冲区,就有容量上限。

我们把管道一次最多可以缓存的数据量大小叫做 PIPESIZE。

内核在处理管道数据的时候,底层也要调用类似 read 和 write 这样的方法进行数据拷贝,这种内核操作每次可以操作的数据量也是有限的,一般的操作长度为一个 page,即默认为 4k 字节。

我们把每次可以操作的数据量长度叫做 PIPEBUF。

POSIX 标准中,对 PIPEBUF 有长度限制,要求其最小长度不得低于 512 字节。

PIPEBUF 的作用是,内核在处理管道的时候,如果每次读写操作的数据长度不大于 PIPEBUF 时,保证其操作是原子的。

而 PIPESIZE 的影响是,大于其长度的写操作会被阻塞,直到当前管道中的数据被读取为止。

管道可以实现半双工通信(即通信的双方都可以发送信息,但不能双方同时发送(当然也就不能同时接收))

管道的两端都可能有多个进程进行读写处理。如果再加上线程,则事情可能变得更复杂。实际上,我们在使用管道的时候,并不推荐这样来用

管道推荐的使用方法是其单工模式:即只有两个进程通信,一个进程只写管道,另一个进程只读管道。

匿名管道 PIPE

通过 pipe 系统调用创建管道,不会产生实体文件

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"fmt"
"io/ioutil"
"log"
"os/exec"
)

func main() {
cmd1 := exec.Command("ps", "aux")
stdout1, err := cmd1.StdoutPipe() // 创建管道,stdout1是读,cmd1.Stdout是写
if err != nil {
log.Fatal(err)
}

cmd2 := exec.Command("grep", "usr")
cmd2.Stdin = stdout1 // stdout1作为cmd2的输入
stdout2, err := cmd2.StdoutPipe() // 创建管道,stdout2是读,cmd2.Stdout是写
if err != nil {
log.Fatal(err)
}

err = cmd1.Start() // 执行cmd1
if err != nil {
log.Fatal(err)
}
fmt.Println("cmd1 started")

err = cmd2.Start() // 执行cmd2,cmd2开始了cmd2.Stdin才能输入,否则cmd1.Wait会一直等
if err != nil {
log.Fatal(err)
}
fmt.Println("cmd2 started")

err = cmd1.Wait() // 等待cmd1的结果全部输入通道,成为了cmd2的输入,完成后关闭通道
if err != nil {
log.Fatal(err)
}
fmt.Println("cmd1 ended")

content, err := ioutil.ReadAll(stdout2) // 读出cmd2的执行结果到标准输出,这里会阻塞直到EOF。要在Wait之前,因为Wait后通道会被关闭,读不到数据
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))

err = cmd2.Wait()
if err != nil {
log.Fatal(err)
}
fmt.Println("cmd2 ended")
}

命名管道 FIFO

命名管道在底层的实现跟匿名管道完全一致,区别是

命名管道会有一个全局可见的文件名以供实现毫无关系的进程之间的通信,而匿名管道只能在父子进程之间通信

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
38
39
40
41
42
43
44
45
46
47
package main

import (
"fmt"
"io/ioutil"
"log"
"os"
"syscall"
)

func main() {
arg := os.Args[1]

info, err := os.Stat("test.fifo")
if err != nil {
if os.IsNotExist(err) && !info.IsDir() { // 文件不存在则创建FIFO
err = syscall.Mkfifo("test.fifo", 0644)
if err != nil {
log.Fatal(err)
}
} else {
log.Fatal(err)
}
}

if arg == "write" {
fs, err := os.OpenFile("test.fifo", os.O_RDWR, os.ModeNamedPipe)
if err != nil {
log.Fatal(err)
}
_, err = fs.WriteString("test")
if err != nil {
log.Fatal(err)
}
} else if arg == "read" {
fs, err := os.OpenFile("test.fifo", os.O_RDONLY, os.ModeNamedPipe)
if err != nil {
log.Fatal(err)
}

content, err := ioutil.ReadAll(fs) // 没有内容会阻塞
if err != nil {
log.Fatal(err)
}
fmt.Println(string(content))
}
}

先执行 go run ./cmd/main/ read,然后新开独立窗口执行 go run ./cmd/main/ write,可以看到 read 的窗口输出 test

注意:例中代码只为说明使用方式,没有充分的鲁棒性,均不可直接用于生产环境

信号

可以通过 kill 系统调用发送信号给另外一个进程实现 IPC 通信

socket

可以通过监听 socket,通过网络通信实现 IPC 通信

共享内存

使用 shmget(传递一个 key 表示共享内存,多个进程指定同一个 key 即可实现公用,实现 IPC)以及 shmat(Shared Memory Attach)可以向操作系统申请共享内存并使用它

操作系统不提供任何对共享内存的并发控制,所以通信时还需要用到锁的机制,比如信号量




微信关注我,及时接收最新技术文章