Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道

Linux 系统编程 学习:02-进程间通信1:Unix IPC(1)管道

背景

上一讲我们介绍了创建子进程的方式。我们都知道,创建子进程是为了与父进程协作(或者是为了执行新的程序,参考 Linux exec族函数解析

我们也知道,进程之间的资源在默认情况下是无法共享的,所以我们需要借助系统提供的 进程间通信(IPC, InterProcess Communication) 有关的接口。

进程间通信

由于进程间的地址空间相对独立。进程与进程间不能像线程间通过全局变量通信,所以进程之间要交换数据必须通过内核。

在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信。

IPC的方式通常有:

  • Unix IPC包括:管道(pipe)、命名管道(FIFO)与信号(Signal)
  • System V IPC:消息队列、信号量、共享内存
  • Socket(支持不同主机上的两个进程IPC)

我们在这一讲介绍Unix IPC,包括:管道(pipe)、命名管道(FIFO)与信号(Signal)。

注意:对于管道来说,只有读端存在,写端才有意义;如果读端不在,写端向FIFO或者PIPE写数据,内核将向对应的进程发送SIGPIPE信号(默认终止进程)。

无名管道(pipe)

无名管道,是 UNIX 系统IPC最古老的形式,内部是用环形队列实现的。

在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。

特点:

1)无名,在文件系统中找不到它的存在,只存在于内存中。

2)必须得是亲缘关系的进程(父子,兄弟,祖孙等)

3)操作是不具备原子性(即,不具备完整性)

4)不能用lseek来定位

5)半双工(读写只能一端进行,另一端等待),具有固定的读端和写端。

6)具备阻塞(有读者有写者,当读没有数据或者是写满了的时候会卡在那里等待)

使用下面的函数创建 pipe管道。用于亲缘进程通信,其他操作与普通文件相同。

#include <unistd.h>

int pipe(int pipefd[2]);

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h> int pipe2(int pipefd[2], int flags); //
/*
flags 包括O_CLOEXEC、O_DIRECT、O_NONBLOCK。
- CLOEXEC:close-on-exec即当调用exec()函数成功后,文件描述符会自动关闭
- O_DIRECT:任何读写操作都只在用户态地址空间和磁盘之间传送而不经过page cache
- O_NONBLOCK:非阻塞模式,在读取不到数据或是写入缓冲区已满会马上return,而不会阻塞等待。
*/

成功:返回0,同时为 pipefd 赋值。

  • pipefd[0]:读端
  • pipefd[1]:写端

失败:返回-1

读写情况:

  • 有数据时,无论有无写者(持有文件可写权限的描述符的进程,无的意思是指:进程退出,不再持有对应的fd;下同)都能够正常读;
  • 无数据时:如果 有 写者,则阻塞等待;如果 无 写着,则立即返回。

  • 如果 有 读者(读者,持有文件可读权限的描述符的进程):缓冲未满,正常写入;缓冲已满,阻塞等待。
  • 如果 无 读者,无论缓冲区如何,立即收到 SIGPIPE 信号(可以通过先注册的信号捕获 SIGPIPE 进行处理,默认是终止进程)

匿名管道 原理

管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。

一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。

当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

匿名管道如何实现亲缘进程间的通信

1)父进程创建管道,得到两个文件描述符指向管道的两端。

2)父进程fork出子进程,子进程也有两个文件描述符指向同一管道。

3)写进程关闭 pipefd[0],读进程关闭 pipefd[1](因为管道只支持单向通信,关闭会比较好)。

数据从写端流入从读端流出,这样就实现了进程间通信。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> int main(int argc, char *argv[])
{
pid_t pid = -1;
int i;
int ret;
int status;
char buffer[20]={'a', 0};
int pipefd[2] = {0}; ret = pipe(pipefd);
if(!ret)
{
printf("Reader fd is : %d\n",pipefd[0]);
printf("Writer fd is : %d\n",pipefd[1]);
} pid = fork();
if(pid == 0)
{
printf("Son as reader\n");
close(pipefd[1]);// 关闭写端
for (i = 0; i < 5; ++i) {
read(pipefd[0], buffer, sizeof(buffer));
printf("%s\n", buffer);
}
exit(0xaa);
}else if(pid > 0)
{
sleep(1);
printf("Father as Writer\n");
close(pipefd[0]);// 关闭读端
for (i = 0; i < 5; ++i) {
write(pipefd[1], buffer, sizeof(buffer));
buffer[i] = 'a' + i;
} ret = wait(&status);
printf("%x\n", WEXITSTATUS(status));
} return 0;
}

命名管道(fifo)

FIFO (First in, First out)为一种特殊的文件类型(通过 stat 结构的 st_mode 成员可以知道文件是否是 FIFO 类型,可以用 S_ISFIFO 宏对此进行测试),它在文件系统中有对应的路径。

当一个进程以读(r)的方式打开该文件,而另一个进程以写(w)的方式打开该文件,那么内核就会在这两个进程之间建立管道,所以FIFO实际上也由内核管理,不与硬盘打交道

FIFO 存在于文件系统中,内容存放在内存中,如果使用 ls -l 观察时,它的文件大小永远是0。

实际上,Linux中的命令的 " | " 就是 fifo。

之所以叫FIFO,是因为管道本质上是一个先进先出的队列数据结构,最早放入的数据被最先读出来,从而保证信息交流的顺序。FIFO只是借用了文件系统来为管道命名。

写模式的进程向FIFO文件中写入,而读模式的进程从FIFO文件中读出。当删除FIFO文件时,管道连接也随之消失。

FIFO的好处在于我们可以通过文件的路径来识别管道,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信;

创建 FIFO 类似于创建文件,它名字对应于一个磁盘索引节点,有了这个文件名,任何进程有相应的权限都可以对它进行访问。它。()

FIFO严格遵循先进先出(first in first out), 对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。

Linux中通过系统调用mknod()或makefifo()来创建一个命名管道。最简单的方式是通过直接使用shell:mkfifo myfifo(等价于mknod myfifo p)

特点

1)有名

2)任一个进程都可以交互

3)会诞生一个类型p的管道文件

4)操作具备原子性

5)全双工

6)不能lseek来定位

使用下面的函数创建 fifo。读写之前必须先open。

mkfifoat 中的 dirfd 请参考: dirfd参数 有关解析

#include <sys/types.h>
#include <sys/stat.h> int mkfifo(const char *pathname, mode_t mode); #include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h> int mkfifoat(int dirfd, const char *pathname, mode_t mode);

参数解析

mode :参数的说明同 open 的 mode 相同。如果open时没有使用O_NONBLOCK参数,不论读端还是写端先打开,先打开者都会阻塞,一直阻塞到另一端打开。

当 open 一个 FIFO,非阻塞标志 O_NONBLOCK 会产生下列影响:

  • 在没有指定该标志的情况下,只读 open 要阻塞到某个其他进程为写而打开这个 FIFO 为止,只写 open 要阻塞到某个其他进程为读而打开它为止。
  • 指定了该标志时,则只读 open 立即返回。但如果没有进程为读而打开一个 FIFO,那么只写 open 将返回 -1,并将 errno 设置成 ENXIO。

    类似于管道,
  • 若 write 一个尚无进程为读而打开的 FIFO,则产生信号 SIGPIPE。
  • 若某个 FIFO 的最后一个写进程关闭了该 FIFO,则将为该 FIFO 的读进程产生一个文件结束标志。
  • 一个给定的 FIFO 可能有多个写进程,如果不希望多个进程所写的数据交叉,则必须考虑原子写操作,可被原子地写到 FIFO 的最大数据量也是通过常量 PIPE_BUF 来指定的。

如果mkfifo的路径已经存在时,会返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO的函数就可以了。

#include <stdio.h>

#include <stdlib.h>
#include <unistd.h> #include <sys/stat.h>
#include <fcntl.h> #include <sys/types.h>
#include <sys/wait.h> #include <errno.h> #define FIFO_FILE "fifo_test" int main(int argc, char *argv[])
{
pid_t pid = -1;
int i;
int ret;
int status;
char buffer[20]={'a', 0};
int fd = 0; ret = mkfifo(FIFO_FILE, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
if(ret == -1)
{
if(errno == EEXIST)
{
printf("FIFO FILE existed.\n");
}else
{
perror("fifo");
return -1;
}
} pid = fork();
if(pid == 0)
{
printf("Son as reader\n");
printf("[Son] Waiting Writer\n");
fd = open(FIFO_FILE, O_RDONLY);
printf("Reader Pass\n"); for (i = 0; i < 5; ++i) {
read(fd, buffer, sizeof(buffer));
printf("%s\n", buffer);
}
exit(0xaa);
}else if(pid > 0)
{
sleep(1);
printf("Father as Writer\n");
printf("[Dad] Waiting Reader\n");
fd = open(FIFO_FILE, O_WRONLY);
printf("Writer pass\n");
for (i = 0; i < 5; ++i) {
write(fd, buffer, sizeof(buffer));
buffer[i] = 'a' + i;
} ret = wait(&status);
printf("%x\n", WEXITSTATUS(status));
} return 0;
}
上一篇:Eclipse/MyEclipse下如何Maven管理多个Mapreduce程序?(企业级水平)


下一篇:(19)模型层 -ORM之msql 跨表查询(正向和反向查询)