原创 进程间通信(IPC)

2016-5-31 13:00 1351 18 21 分类: MCU/ 嵌入式 文集: Qt和Cpp
进程是程序运行的实例;每个进程可以并发运行多个线程。进程内(线程间)的数据可以共享,但是进程之间并不能互相访问,因为每个进程都维护着自己的text、stack、heap等等,它们的地址对其他进程不可见。

除了使用文件,还有其他几种方法可以实现进程间的数据共享。它们各自有各自的特点,适用于不同的应用场景。
(* 这里不涉及到网络套接字。)

Pipes
================================
管道通信,是Unix系统里非常古老的通信形式。它有两个限制:
 * 半双工(某些系统提供全双工);
 * 通信的进程必须有共同的ancester。

管道的应用非常常见,比如当我们在终端输入管道命令时,shell会为每个命令建立一个进程,并且在前一个命令的输出和后一个命令的输入之间建立管道通信。

每个进程有两个管道口,一个用作输入,一个用作输出。如果这个进程fork子进程,那么子进程同样具有两个管道口。父进程关掉自己的输入口,子进程关掉自己的输出口,父进程的输出就可以到达子进程的输入。当然,进程内部也可以使用管道通信(虽然它没什么作用^_^)。

pile函数的原型如下:
#include
int pipe(int fd[2]);

其中,fd[0]为管道输入,fd[1]为管道输出。

假设在一个种植月季的智能温室里,控制中心的电脑上运行着这样一个应用程序,名为RoseGarden,它启动多个进程,分别执行不同的任务,比如温度调节、光线调节、水分调节、肥料施用……

程序如下:
#include <iostream>
#include <unistd.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/shm.h>

using namespace std;

int main() {
    cout << "****************************" << endl;
    cout << "******** RoseGarden ********" << endl;
    cout << "****************************" << endl;

    int fd[2];
    if (pipe(fd) < 0) {
        cerr << "Error: pipe." << endl;
        return -1;
    }

    pid_t pid;
    if ((pid = fork()) < 0) {
        cerr << "Error: fork." << endl;
        return -1;

    } else if (pid == 0) {
        close(fd[1]);
        cout << " Start." << endl;

        char data[64];
        string str;
        while (1) {
            memset(data, 0, sizeof(data));
            read(fd[0], data, sizeof(data));
            str = " R: " + string(data);
            cout << str << endl;
        }

    } else {
        close(fd[0]);

        int n = 0;
        string str;
        while (1) {
            str = "RoseGarden " + to_string(n++) + " piping.";
            write(fd[1], str.data(), str.size());
            sleep(1);
        }
    }

    return 0;
}

执行结果:
$ ./RoseGarden
****************************
******** RoseGarden ********
****************************
Start.
R: RoseGarden 0 piping.
R: RoseGarden 1 piping.
R: RoseGarden 2 piping.
R: RoseGarden 3 piping.
R: RoseGarden 4 piping.
R: RoseGarden 5 piping.

这说明父进程RoseGarden的数据通过管道,到达了子进程Water中。

如果将pipe的文件描述符duplicate到标准输入/输出上,会发生非常有趣的事情。在文件IO中,dup函数用来复制文件描述符,函数原型如下:
#include
int dup(int fd);
int dup2(int fd, int fd2);

使用dup函数,那么其返回值和fd会指向内核中的同一个文件table,因此不论是操作其返回值还是fd,修改的是同一个文件。使用dup2函数,那么fd2会关闭自己(如果它已打开),然后指向fd的文件table。

将代码修改如下:
if ((pid = fork()) < 0) {
    cerr << "Error: fork." << endl;
    return -1;

} else if (pid == 0) {
    close(fd[1]);
    cout << " Start." << endl;

    if (fd[0] != STDIN_FILENO) {
        if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO) {
            cerr << " Error: dup2." << endl;
            exit(1);
        }
        close(fd[0]);
    }

    if (execlp("more", "more", (char *)0) < 0) {
        cerr << " Error: execlp." << endl;
        exit(1);
    }

} else {
    close(fd[0]);

    int n = 0;
    string str;
    while (1) {
        str = "RoseGarden " + to_string(n++) + " piping.\r\n";
        for (int i = 0; i < 10; i++)
            str += to_string(i) + " ++++++++++++\r\n";
        write(fd[1], str.data(), str.size());
        sleep(1);
    }
}

程序执行的结果:
$ ./RoseGarden
****************************
******** RoseGarden ********
****************************
Start.
RoseGarden 0 piping.
0 ++++++++++++
1 ++++++++++++
2 ++++++++++++
3 ++++++++++++
4 ++++++++++++
5 ++++++++++++
6 ++++++++++++
7 ++++++++++++
8 ++++++++++++
9 ++++++++++++
RoseGarden 1 piping.
0 ++++++++++++
1 ++++++++++++
2 ++++++++++++
3 ++++++++++++
4 ++++++++++++
5 ++++++++++++
6 ++++++++++++
--More--

这说明,父进程往pipe里面写的数据,由pipe的read端接收。此时more命令读取的STDIN_FILENO的数据,实际上就是读取管道的数据。此时more的命令,比如s、d、q……有效。

另外,虽然管道是单向的,我们可以建立两条管道,实现双向通信功能。

其实,大部分我们要实现的功能是类似的,比如创建一条管道、fork一个子进程、关闭不用的管道ends、exec一个命令、等待命令结束……我们可以使用popen函数,处理这些类似的功能。它的函数原型为:
#include
FILE *popen(const char *cmdstring, const char *type);
int pclose(FILE *fp);

因此之前的程序可以大大的被简化,如下所示:
#include <iostream>
#include <unistd.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/shm.h>

using namespace std;

int main() {
    cout << "****************************" << endl;
    cout << "******** RoseGarden ********" << endl;
    cout << "****************************" << endl;

    FILE *fp;
    if ((fp = popen("more", "w")) == NULL) {
        cerr << "Error: popen." << endl;
        exit(1);
    }

    int n = 0;
    string str;
    cout << "Start writing pipe: " << endl;
    while (1) {
        str = "RoseGarden " + to_string(n++) + " piping.\r\n";
        for (int i = 0; i < 10; i++)
            str += to_string(i) + " ++++++++++++\r\n";

        fprintf(fp, str.data());
        fflush(fp);
        sleep(1);
    }

    return 0;
}

ps,这里的fp是文件stream,因此别忘了fflush。

FIFOs
================================
FIFOs也被称为named pipes。
Unnamed pipes只能用在有相同ancestor的进程之间,就是说创建这个pipe的进程必须是同一个。
FIFOs也是文件。

FIFO的实验在终端下进行,首先在终端1号上执行:
$ mkfifo fifo1
$ cat fifo1
然后在终端2号上执行:
$ echo "hello world" >> fifo1

就会发现终端1号上打印:
$ cat fifo1
hello world

这说明终端2号往fifo1里面写入的数据,被终端1号读取到了。
FIFOs在终端里的作用非常大,它和之前的pipe一起,可以实现各种命令的组合。

XSI IPC: Message Queues
================================
消息队列是消息组成的链表,它存储在内核中,由一个identifier引用。
 * 《Unix环境高级编程》的作者,是有多不喜欢消息队列?以至于隔一会儿就把它批一顿,而且完全没有给出它的应用example,另外在最末尾还建议“we come to the conclusion that we shouldn't use them for new applications”。
 * 既然这样,就不做练习了!直接进入下一节吧。

XSI IPC: Semaphores
================================
信号量其实不是用于进程间通信的,它是一个counter,用来保护进程间的共享数据。
 * 同上。需要使用信号量的时候,再考虑考虑mutex和record locking。
 * 不做练习!直接进入下一节。

XSI IPC: Shared Memory
================================
共享内存是最快速的IPC方法。
假设有这样一个应用场景:RoseGarden每隔一小时拍摄一张照片,放置在共享内存中,供另外一个进程PictureDraw使用。由于照片的体积很大,因此拷贝照片耗费的时间太太,共享内存是非常合适的解决方法。
代码如下:
#include <iostream>
#include <unistd.h>
#include <memory.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/shm.h>

using namespace std;

#define SHM_SIZE    4000000

int main() {
    cout << "****************************" << endl;
    cout << "******** RoseGarden ********" << endl;
    cout << "****************************" << endl;

    pid_t pid;
    if ((pid = fork()) < 0) {
        cerr << "Error: fork." << endl;
        exit(1);

    } else if (pid == 0) {
        cout << " Start." << endl;

        int shmid;
        void *shmptr;
        if ((shmid = shmget(IPC_PRIVATE, SHM_SIZE, 0600)) < 0) {
            cerr << "Error: shgmget." << endl;
            exit(1);
        }
        if ((shmptr = shmat(shmid, 0, 0)) == (void *)-1) {
            cerr << "Error: shmptr." << endl;
            exit(2);
        }
        cout << " Shared-Memory start at " << shmptr << endl;
        cout << " shmid = " << shmid << endl;
        if (shmctl(shmid, IPC_RMID, 0) < 0) {
            cerr << "Error: shmctl." << endl;
            exit(1);
        }

    } else {

    }

    return 0;
}

执行结果:
$ ./RoseGarden
****************************
******** RoseGarden ********
****************************
Start.
Shared-Memory start at 0x7fd5d7bb3000
shmid = 11075601

ipc

文章评论3条评论)

登录后参与讨论

DiracFatCat 2016-6-6 13:32

确实还有其他的方法,除了socket(占很大比重)之外,还有消息队列和信号量。但是后两个建议使用pipes和mutex替代,所以就不加进来了。

用户1291786 2016-6-6 09:48

还应该有续集吧? 以前看过别人总结的IPC大概有将近10种方法

用户1678053 2016-6-1 08:40

看看
相关推荐阅读
DiracFatCat 2018-09-05 12:14
【博客大赛】卡尔曼滤波学习笔记(11)从位置估计速度
卡尔曼滤波器,不仅仅是一个低通滤波器,否则也不会持续发展50年。 示例:桑先生需要测试高速列车的性能。测试的目的是判断列车在直线上能否保持80m/s的速度。速度和位置每0.1秒测量一次,但是由于...
DiracFatCat 2018-08-31 19:32
【博客大赛】卡尔曼滤波学习笔记(10)一个简单的示例
《Kalman Filtering: Theory and Practice Using MATLAB》第三章,看不懂,暂时略过。《Kalman Filtering: Theory and Pract...
DiracFatCat 2018-07-19 15:09
对sed命令的练习
sed是流编辑器。它每次处理一个输入,因此很有效率。官方手册:https://www.gnu.org/software/sed/manual/sed.html学习Linux命令,当然要阅读官方手册,所...
DiracFatCat 2018-06-19 15:10
【博客大赛】卡尔曼滤波学习笔记(八)可观测性和可控制性 ...
可观测性是指,在给定模型的情况下,动力学系统的状态是否由它的输入输出唯一确定。可观测性是系统模型的特征。如果传感器矩阵H是可逆的,则本系统可观测,因为有:如果传感器矩阵H某些时候是不可逆的,则本系统仍...
DiracFatCat 2018-06-19 10:56
【博客大赛】卡尔曼滤波学习笔记(七)Z变换
如果我们仅仅对离散线性系统感兴趣,那么就使用下面这个表达式:如果u是常量,那么可以写成:为了简化表达式,我们可以将上面写成:离散线性时不变系统的Φ求解,可以使用Z变换。(* 由于本人已经忘记了Z变换的...
DiracFatCat 2018-06-19 10:54
【博客大赛】卡尔曼滤波学习笔记(六)拉普拉斯变换
对于线性时变/时不变系统,求解Φ(t)有多种方式,拉普拉斯变换是其中一种。(* 由于本人已经忘记了拉普拉斯变换的内容,因此本节待续。)...
我要评论
3
18
关闭 站长推荐上一条 /2 下一条