并发编程--进程间通信(IPC)概览以及匿名管道

进程间通信(IPC)概览以及匿名管道

    • 1. 进程间通信(IPC)概览
    • 2. 匿名管道PIPE
      • 2.1 基本逻辑
      • 2.2 函数接口
      • 2.3 管道的读写特性
      • 2.4 管道的阻塞特性

1. 进程间通信(IPC)概览

在Linux/Unix系统中,进程间通信方式(Inter-Process Comunication)通常有如下若干中方式:

  • 管道
    • 匿名管道 pipe:适用于亲缘关系进程间的、一对一的通信
    • 具名管道 fifo :适用于任何进程间的一对一、多对一的通信
  • 套接字 socket:适用于跨网络的进程间通信
  • 信号:异步通信方式
  • system-V IPC对象
    • 共享内存:效率最高的通信方式
    • 消息队列:相当于带标签的增强版管道
    • 信号量组:也称为信号灯,用来协调进程间或线程间的执行进度
  • POSIX信号量
    • POSIX匿名信号量:适用于多线程,参数简单,接口明晰,童叟无欺
    • POSIX具名信号量:适用于多进程,参数简单,接口明晰,老少咸宜

这些通信机制统称IPC,它们各有特色,各有适用的场合。

2. 匿名管道PIPE

2.1 基本逻辑

不管是匿名管道还是具名管道,在Linux系统下都属于文件的范畴,区别是匿名管道没有名称,因此无法使用open创建或打开,事实上匿名管道有自己独特的创建接口,但其读写方式与普通的文件一样,支持read()/write()操作。

管道文件事实上还包括网络编程中的核心概念套接字,所谓的管道指的是这些文件不能进行“定位”,只能顺序对其读写数据,就像一根水管,拧开水龙头不断读取,就可以源源不断读到水管中的数据,但如果没有水出来那只能继续等待,不能试图“跳过”部分文件去读写水管的中间地带,这是管道的最基本的特性。

并发编程--进程间通信(IPC)概览以及匿名管道_第1张图片

水管

2.2 函数接口

创建匿名管道的函数接口非常简单,如下所示:

#include 

int pipe( int fd[2] );
  • 注意1: 由于匿名管道拥有两个文件描述符,一个专用于读fd[0],一个专用于写fd[1],因此上述接口需要传递一个至少包含两个整型元素的数组过去,用来存放这两个特定的描述符。
  • 注意2: 匿名管道描述符,只能通过继承的方式传递给后代进程,因此只能用于亲缘进程间的通信,由于没有文件名,其他非亲缘进程无法获取匿名管道的描述符。
  • 注意3: 不能有多个进程同时对匿名管道进行写操作,否则数据有可能被覆盖。

总结一句话,匿名管道适用于一对一的、具有亲缘关系的进程间的通信。

下面以父子进程使用匿名管道通信的例子对PIPE的使用加以说明,假设父进程先创建一条匿名管道,然后产生一个子进程,此时子进程自然继承了这条管道的读写端描述符,进而它们就可以通信了。

并发编程--进程间通信(IPC)概览以及匿名管道_第2张图片

父子进程使用匿名管道通信

示例代码如下:

#include 
#include 
#include 
#include 
#include 

int main(void)
{
    // 创建匿名管道
	int fd[2];
	pipe(fd);

    // 子进程
	if(fork() == 0)
	{
        // 向父进程打招呼
		char *msg = "hello parent!";
		write(fd[1], msg, strlen(msg));

		exit(0);
	}

    // 父进程
	else
	{
	    char buf[50];
        bzero(buf, 50);

        // 静静地等待子进程的消息 
		read(fd[0], buf, 50);

		printf("来自子进程: %s\n", buf);
		exit(0);
	}
}

注意,匿名管道的读写端是严格区分的,任何不规范的操作都是不允许的,其结果都是不确定的。

另外还应该注意到,一般而言,不需要用到的文件描述符都最好及时关闭,避免不必要的副作用或浪费系统资源。例如上述程序中,子进程只用到了管道的写端,因此它的fd[0]可以也应该要关闭,相反父进程只用到了管道的读端,因此它的fd[1]可以也应该关闭。

代码可以改成:

int main(void)
{
    // 创建匿名管道
    int fd[2];
    pipe(fd);

    // 子进程
    if(fork() == 0)
    {
        // 关掉不必要的读端
        close(fd[0]);

        ...
    }

    // 父进程
    else
    {
        // 关掉不必要的写端
        close(fd[1]);

        ...
    }
}

2.3 管道的读写特性

当我们对一个管道文件(包括匿名管道、具名管道和网络socket)进行读写操作时,我们需要知道将会发生什么,比如读一个空管道会怎么样?对一个缓冲区已满的管道执行写入操作会怎么样等等,可以对这些读写操作做一个统一的整理。

  • 术语约定:
    • 读者: 对管道拥有读权限的进程
    • 写者: 对管道拥有写权限的进程

注意,所谓的读者、写者不是只正在读或者正在写的进程,而是只要拥有读写权限就称为管道的读者写者,比如如下进程关闭了匿名管道的读端,因此它只能称为匿名管道的写者:

// 创建匿名管道
int fd[2];
pipe(fd);

// 关闭读端,剩下写端
close(fd[0]);

又如下面这个进程,使用读写权限打开了具名管道,因此该进程既是读者也是写者:

int fd = open("fifo", O_RDWR);

下面是读写特性对照表:

并发编程--进程间通信(IPC)概览以及匿名管道_第3张图片
管道的读写特性

2.4 管道的阻塞特性

仔细看管道读写特性的表会发现,当试图读取一个空管道,或者试图写入一个缓冲区已满的管道时,读写操作默认会进入所谓“阻塞(se)”的状态。所谓的阻塞实际上就是系统将该进程挂起,等待资源就绪再继续调度的一种状态,这种阻塞的状态有利于系统中别的进程可高效地使用闲置CPU资源,提高系统的吞吐量。

对于阻塞而言,有如下特性需要记忆:

  • 普通文件,默认是非阻塞的,且不可修改。
  • 管道文件,默认是阻塞的,可修改。

以下是设置管道文件阻塞特性的代码:

#include 
#include 
#include 
#include 
#include 

int main()
{
    // 管道默认为阻塞
    int fd[2];
    pipe(fd);

    // 1,将管道设置为非阻塞
    long flag = fcntl(fd[0], F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(fd[0], F_SETFL, flag);

    int n;
    char buf[20];

    // 此处,读不到数据将立即返回
    n = read(fd[0], buf, 20);
    if(n < 0)
        perror("read failed");

    // 2,将管道重新设置为阻塞
    flag = fcntl(fd[0], F_GETFL);
    flag &= ~O_NONBLOCK;
    fcntl(fd[0], F_SETFL, flag);

    // 此处,读不到数据将持续等待
    n = read(fd[0], buf, 20);
    if(n < 0)
        perror("read failed");

    return 0;
}

注意:
管道打开时,必须同时有读者和写者,否则 open 也会阻塞。

你可能感兴趣的:(进程,并发编程,c语言,开发语言)