进程间的通讯方式(二)

简介: 进程间的通讯方式

2、原型:

1 #include <unistd.h>
2 int pipe(int fd[2]);    // 返回值:若成功返回0,失败返回-1

当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。如下图:

要关闭管道只需将这两个文件描述符关闭即可。

单个进程中的管道几乎没有任何用处。所以,通常调用 pipe 的进程接着调用 fork,这样就创建了父进程与子进程之间的 IPC 通道。如下图所示:

若要数据流从父进程流向子进程,则关闭父进程的读端(fd[0])与子进程的写端(fd[1]);反之,则可以使数据流从子进程流向父进程。

对于从父进程到子进程的管道,父进程关闭读端(fd[0]),子进程关闭写端(fd[1]);对于从子进程到父进程的管道,子进程关闭读端(fd[0]),父进程关闭写端(fd[1])。

当管道的一端被关闭后,会出现下面的几种情况:

  1. 当读一个写端被关闭的管道时,在所有数据都被读取后,read返回0,表示文件结束;如果写端没有被关闭,但是没有数据,则读端读完数据后阻塞;
  2. 当写一个读端被关闭的管道时,则产生信号SIGPIPE,write返回-1,errno设置为EPIPE;如果读端没有被关闭,写端写满数据后,则写端阻塞。
#include<stdio.h>
#include<unistd.h>
int main()
{
    int fd[2];  // 两个文件描述符
    pid_t pid;
    char buff[20];
    if(pipe(fd) < 0)  // 创建管道
        printf("Create Pipe Error!\n");
    if((pid = fork()) < 0)  // 创建子进程
        printf("Fork Error!\n");
    else if(pid > 0)  // 父进程
    {
        close(fd[0]); // 关闭读端
        write(fd[1], "hello world\n", 12);
    }
    else
    {
        close(fd[1]); // 关闭写端
        read(fd[0], buff, 20);
        printf("%s", buff);
    }
    return 0;
}

缺点:只能限于父子进程之间的通信

2、高级管道通信

高级管道(popen):将另一个程序当做一个新的进程在当前程序进程中启动,则它算是当前程序的子进程,这种方式我们成为高级管道方式。

3、FIFO(命名管道)

管道应用的一个重大缺陷就是没有名字,因此只能用于亲缘进程之间的通信。后来从管道为基础提出命名管道(named pipe,FIFO)的概念,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。FIFO,也称为命名管道,它是一种文件类型。

POSIX标准中的FIFO又名有名管道或命名管道。我们知道前面讲述的POSIX标准中管道是没有名称的,所以它的最大劣势是只能用于具有亲缘关系的进程间的通信。FIFO最大的特性就是每个FIFO都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过FIFO进行通信。所以,FIFO的两个特性:

  • 和管道一样,FIFO仅提供半双工的数据通信,即只支持单向的数据流;
  • 和管道不同的是,FIFO可以支持任意两个进程间的通信。

命名管道的用途主要有:(1)shell命名使用FIFO将数据从一条管道传送到另一条时,无须创建中间临时文件;(2)在客户进程和服务器进程间传送数据。

2.1 特点

  1. FIFO可以在无关的进程之间交换数据,与无名管道不同。
  2. FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。

2.2 原型

1 #include <sys/stat.h>
2 // 返回值:成功返回0,出错返回-1
3 int mkfifo(const char *pathname, mode_t mode);

其中的 mode 参数与open函数中的 mode 相同。一旦创建了一个 FIFO,就可以用一般的文件I/O函数操作它。

当 open 一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:

  • 若没有指定O_NONBLOCK(默认),只读 open 要阻塞到某个其他进程为写而打开此 FIFO。类似的,只写 open 要阻塞到某个其他进程为读而打开它。
  • 若指定了O_NONBLOCK,则只读 open 立即返回。而只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO。

2.3 例子

FIFO的通信方式类似于在进程中使用文件来传输数据,只不过FIFO类型文件同时具有管道的特性。在数据读出时,FIFO管道中同时清除数据,并且“先进先出”。下面的例子演示了使用 FIFO 进行 IPC 的过程:

write_fifo.c

#include<stdio.h>
#include<stdlib.h>   // exit
#include<fcntl.h>    // O_WRONLY
#include<sys/stat.h>
#include<time.h>     // time
int main()
{
    int fd;
    int n, i;
    char buf[1024];
    time_t tp;
    printf("I am %d process.\n", getpid()); // 说明进程ID
    if((fd = open("fifo1", O_WRONLY)) < 0) // 以写打开一个FIFO
    {
        perror("Open FIFO Failed");
        exit(1);
    }
    for(i=0; i<10; ++i)
    {
        time(&tp);  // 取系统当前时间
        n=sprintf(buf,"Process %d's time is %s",getpid(),ctime(&tp));
        printf("Send message: %s", buf); // 打印
        if(write(fd, buf, n+1) < 0)  // 写入到FIFO中
        {
            perror("Write FIFO Failed");
            close(fd);
            exit(1);
        }
        sleep(1);  // 休眠1秒
    }
    close(fd);  // 关闭FIFO文件
    return 0;
}

read_fifo.c

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<fcntl.h>
#include<sys/stat.h>
int main()
{
    int fd;
    int len;
    char buf[1024];
    if(mkfifo("fifo1", 0666) < 0 && errno!=EEXIST) // 创建FIFO管道
        perror("Create FIFO Failed");
    if((fd = open("fifo1", O_RDONLY)) < 0)  // 以读打开FIFO
    {
        perror("Open FIFO Failed");
        exit(1);
    }
    while((len = read(fd, buf, 1024)) > 0) // 读取FIFO管道
        printf("Read message: %s", buf);
    close(fd);  // 关闭FIFO文件
    return 0;
}

在两个终端里用 gcc 分别编译运行上面两个文件,可以看到输出结果如下:

[cheesezh@localhost]$ ./write_fifo
I am 5954 process.
Send message: Process 5954's time is Mon Apr 20 12:37:28 2015
Send message: Process 5954's time is Mon Apr 20 12:37:29 2015
Send message: Process 5954's time is Mon Apr 20 12:37:30 2015
Send message: Process 5954's time is Mon Apr 20 12:37:31 2015
Send message: Process 5954's time is Mon Apr 20 12:37:32 2015
Send message: Process 5954's time is Mon Apr 20 12:37:33 2015
Send message: Process 5954's time is Mon Apr 20 12:37:34 2015
Send message: Process 5954's time is Mon Apr 20 12:37:35 2015
Send message: Process 5954's time is Mon Apr 20 12:37:36 2015
Send message: Process 5954's time is Mon Apr 20 12:37:37 2015
[cheesezh@localhost]$ ./read_fifo
Read message: Process 5954's time is Mon Apr 20 12:37:28 2015
Read message: Process 5954's time is Mon Apr 20 12:37:29 2015
Read message: Process 5954's time is Mon Apr 20 12:37:30 2015
Read message: Process 5954's time is Mon Apr 20 12:37:31 2015
Read message: Process 5954's time is Mon Apr 20 12:37:32 2015
Read message: Process 5954's time is Mon Apr 20 12:37:33 2015
Read message: Process 5954's time is Mon Apr 20 12:37:34 2015
Read message: Process 5954's time is Mon Apr 20 12:37:35 2015
Read message: Process 5954's time is Mon Apr 20 12:37:36 2015
Read message: Process 5954's time is Mon Apr 20 12:37:37 2015

命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据

4、消息队列

前面说到管道的通信方式是效率低的,因此管道不适合进程间频繁地交换数据。

对于这个问题,消息队列的通信模式就可以解决。比如,A 进程要给 B 进程发送消息,A 进程把数据放在对应的消息队列后就可以正常返回了,B 进程需要的时候再去读取数据就可以了。同理,B 进程要给 A 进程发送消息也是如此。

再来,消息队列是保存在内核中的消息链表,在发送数据时,会分成一个一个独立的数据单元,也就是消息体(数据块),消息体是用户自定义的数据类型,消息的发送方和接收方要约定好消息体的数据类型,所以每个消息体都是固定大小的存储块,不像管道是无格式的字节流数据。如果进程从消息队列中读取了消息体,内核就会把这个消息体删除。

消息队列生命周期随内核,如果没有释放消息队列或者没有关闭操作系统,消息队列会一直存在,而前面提到的匿名管道的生命周期,是随进程的创建而建立,随进程的结束而销毁。

消息这种模型,两个进程之间的通信就像平时发邮件一样,你来一封,我回一封,可以频繁沟通了。

但邮件的通信方式存在不足的地方有两点,一是通信不及时,二是附件也有大小限制,这同样也是消息队列通信不足的点。

消息队列不适合比较大数据的传输,因为在内核中每个消息体都有一个最大长度的限制,同时所有队列所包含的全部消息体的总长度也是有上限。在 Linux 内核中,会有两个宏定义 MSGMAXMSGMNB,它们以字节为单位,分别定义了一条消息的最大长度和一个队列的最大长度。

消息队列通信过程中,存在用户态与内核态之间的数据拷贝开销,因为进程写入数据到内核中的消息队列时,会发生从用户态拷贝数据到内核态的过程,同理另一进程读取内核中的消息数据时,会发生从内核态拷贝数据到用户态的过程。

5、共享内存

消息队列的读取和写入的过程,都会有发生用户态与内核态之间的消息拷贝过程。那共享内存的方式,就很好的解决了这一问题。

现代操作系统,对于内存管理,采用的是虚拟内存技术,也就是每个进程都有自己独立的虚拟内存空间,不同进程的虚拟内存映射到不同的物理内存中。所以,即使进程 A 和 进程 B 的虚拟地址是一样的,其实访问的是不同的物理内存地址,对于数据的增删查改互不影响。

  共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。

相关文章
|
2月前
|
Linux Shell
【Linux 进程间通讯 管道】使用Linux管道进行linux进程间通信
【Linux 进程间通讯 管道】使用Linux管道进行linux进程间通信
33 1
|
2月前
|
消息中间件 存储 网络协议
Linux IPC 进程间通讯方式的深入对比与分析和权衡
Linux IPC 进程间通讯方式的深入对比与分析和权衡
73 0
|
2月前
|
消息中间件 网络协议 API
Python语言的进程通讯及网络
Python语言的进程通讯及网络
|
2月前
|
消息中间件 Linux API
跨进程通信设计:Qt 进程间通讯类全面解析
跨进程通信设计:Qt 进程间通讯类全面解析
82 0
|
2月前
|
安全 Linux 数据安全/隐私保护
探索 Linux 高级进程间通讯 D-Bus的神秘妙用
探索 Linux 高级进程间通讯 D-Bus的神秘妙用
74 1
|
5月前
|
存储
进程间通讯-共享内存
进程间通讯-共享内存
22 0
|
8月前
|
程序员 C++ Windows
Windows C++ 使用PIPE命名管道实现进程间通讯
本文章是介绍在Windows下,使用PIPE管道实现进程间通讯,可是实现两个进程之前相互发送读取消息。
102 0
|
9月前
|
Linux
Linux信号:进程间通讯与事件处理的基石
在Linux操作系统中通过信号,一个进程可以向另一个进程发送某种类型的通知,以便触发特定的动作。本文将深入探讨Linux信号的概念、种类、处理方式以及代码示例。
62 0
|
11月前
|
消息中间件 缓存 网络协议
进程间的通讯方式(三)
进程间的通讯方式
84 0
|
11月前
|
缓存 关系型数据库 MySQL
进程间的通讯方式(一)
进程间的通讯方式
58 0