进程间的通信方式
(2020.11.02 Mon)
进程有其独立性,每个进程的数据停留在自己的进程空间里,互不干涉。有时需要进程间分享数据,进行进程间通信(Inter-proess communication, IPC)。
一种原始的IPC方式:通过文件交换信息
一个进程往文件里写数据,另一个从中读出数据。
$ping localhost > log.txt &
$while true; do tail -l log.txt; done;
信号作为IPC
一个进程发出信号,放入目标进程的描述符,另一个进程进行系统调用时,在自己的描述符中看到该信号。两个进程通过信号进行了简单的数据交换。缺点是无法大量交换数据。内核空间和进程空间独立,所以绕开了进程空间相互独立的限制。
管道pipe
管道变更文本流的方向,管道的目的地是另一个进程。可以把一个进程的输出变成另一个进程的输入。管道直接把一个进程的输出和另一个进程的输入连接起来。用|表示管道。
$echo hello | grep lo
管道把echo的输出导入grep命令,grep是从文本流中寻找'lo'字符串。由于输入的hello包含了lo,所以grep命令打印出hello。
该命令会同时启动两个进程。
基于管道的进程间数据交换发生在内核空间。通过内核来绕开进程空间的独立性限制。创建管道之后,内核空间会有一个专用的缓冲区用于存放要传输的数据。输出进程向缓冲区不停的写数据,输入进程从缓冲区按顺序取出数据。效果上看,数据沿着管子有序的从一个进程到另一个进程。
缓冲区被设计成环形(circular array?)的数据结构。旧的数据已经取出时,新的数据就可以占用这块内存空间。如果缓存区放满,则尝试进入的数据的进程会等待,直到另一端的进程取出消息。两个进程终结,管道会自动消失,缓冲区的内存空间也会收回。
管道的创建
利用fork机制。关于资源的fork,进程会把打开文件的信息复制给子进程。这样,进程输入和输出端口的信息会复制到子进程。因此,一个进程会像新建文件一样,先创建一个管道,该进程的输入和输出,直接接到这个管道上。
完成上述步骤,进程会fork。随着资源fork,进程到管道的两个连接也复制到了子进程上,两个进程同时接上同一个管道。随后,每个进程关闭自己不需要的一个连接。parent关闭从管道来的输入连接,即子进程输出到管道的连接。这样,剩下的连接就行程了可以从一个进程到另一个进程传输的管道。
这种创建管道的方式基于fork机制,因此管道必须用于parent和child进程之间,或者拥有相同祖先的两个子进程之间。为解决这一问题,Linux提供了命名管道(named pipe)。
命名管道的基础是FIFO,一种特殊的文件类型,它在文件系统中有对应的路径。如果一个进程以读方式打开fifo文件,另一个以写方式打开,则内核就会在这两个进程间建立管道。FIFO存活于内核空间,其效率比存储器文件的IPC高很多。
只要两个进程读写同一个FIFO文件,就可以自由的建立管道,可以直接用命令来创建命名管道:
$mknod {file_name} p
比如,在临时文件夹创建一个命名管道
mknod /tmp/named_pipe p
打开两个命令行窗口,在窗口1输入
$tail -f /tmp/named_pipe
在窗口2输入
$echo hello >> /tmp/named_pipe
返回窗口1,会看到echo的内容被命名管道传递到了这边。
用rm指令删除FIFO文件,named pipe连接也会随之消失。
在Linux系统中,管道共用了存储器文件的API,所以创建到使用都很方便。
(2020.11.03 Tues)
其他IPC方式
不同的IPC方式都实现了进程间的资源共享,包括消息队列,共享内存和套接字
消息队列message queue以及对比管道
与pipe相似,在内核空间把数据排好队,先入先出。MQ的数据单元是一个长度不定的消息,而pipe是以字节为单位的数据流。pipe两端分别只能连接一个进程,而mq允许多个进程参与,既可以有多个进程往mq中放入消息,也可以有多个进程从mq中取出消息。mq不依附于特定的进程,也不会像pipe那样自动消失。mq一旦被创建,就会一直停留在内核空间,知道某个进程删除该queue。
消息队列可按数据类型取出消息。消息放入mq时,可以带有一个整数,作为该消息的类型。当进程取出消息时,可以提供参数类型,按照FIFO的顺序,只取出这一类型的消息。此时,类型就起到了筛选消息的功能。
下面是C语言的mq传输数据的程序。sender。
#include
#include
#include
struct msg_struct {
int type;
char content[100];
} message;
int main() {
//生成IPC key
key_t key = ftok('path', 65);
//创建一个mq,获得message queue ID
int mqid = msgget(key, 0666 | IPC_CREAT);
//输入文本
printf('please input the text to be sent: ');
fgets(message.content, 100, stdin);
message.type = 1;
//发送数据
msgsnd(mqid, &message, sizeof(message), 0);
printf('data sent: %s', message.content);
return 0;
}
receiver.
#include
#include
#include
struct msg_struct {
int type;
char content[100];
} message;
int main() {
//生成IPC key
key_t key = ftok('path',65);
//创建一个mq,获得message queue id
int mqid = msgget(key, 0666 | IPC_CREAT);
//收取数据
msgrcv(mqid, &message, sizeof(message),1 ,0);
printf('Data received: %s', message.content);
//销毁mq
msgctl(mqid, IPC_RMID, NULL);
return 0;
}
共享内存shared memory
打破了进程空间的独立性。一个进程可以将自己内存空间的一部分拿出来作为共享内存,允许其他进程直接读写。数据始终停留在同一个地方,不需要迁移到存储器空间或内核空间,是效率最高的IPC方式。适用于大数据量的场景,比如图像处理。父子进程间共享数据的C代码如下。
#include
#include
#include
#include
#include
#define ANSI_COLOR_CYAN '\x1b[36m'
#define ANSI_COLOR_REST '\x1b[0m'
void *create_shared_memory(size_t size)
{
//将共享内存设置成可读可写
int protection = PROT_READ | PROT_WRITE;
//将共享内存设置成共享(第三方进程可读)和匿名(第三方进程无法得到访问地址)
int visibility = MAP_ANONYMOUS | MAP_SHARED;
//创建共享内存
return mmap(NULL, size, protection, visibility, 0, 0);
}
int main() {
setbuf(stdout, NULL);
void *shmem = create_shared_memory(128);
int pid = fork();
if (pid == 0)
{ while(1)
{
char message[100];
printf('input: ');
fgets(message, 100, stdin);
memcpy(shmem, message, sizeof(message));
printf('child wrote data successfully: %s\n', shmem);
}
}
else
{
while (1)
{
printf(ANSI_COLOR_CYAN 'parent mem data: %s\n' ANSI_COLOR_REST, shmem);
sleep(1);
}
}
}
运行结束,父进程以蓝色输出,子进程以默认颜色输出。
套接字socket
多个进程分布在不同的电脑上,通过网络通信。
Reference
1 Vamei,周昕梓著,树莓派开始玩转Linux,中国工信出版集团,电子工业出版社