目录
1、Linux进程间的通信继承
2、Linux进程之间的通信种类
3、管道
3.1 管道概述
3.2 管道文件
3.3 管道特点
3.4 通信框架
3.5 对管道文件进行操作
4、标准流管道
5、无名管道 PIPE
5.1 无名管道特点
5.2 创建管道函数
6、有名管道(FIFO)
6.1 创建、删除FIFO文件
6.2 打开、关闭FIFO文件
6.3 读写FIFO文件
1、Linux进程间的通信继承
Linux 下的进程通信手段基本上是从 UNIX 平台上的进程通信手段继承而来的。 而对 UNIX 发展做出重大贡献的两大主力 AT&T 贝尔实验室及 BSD(加州大学伯克利分校的伯 克利软件发布中心)在进程间的通信方面的侧重点有所不同。 前者是对 UNIX 早期的进程间通信手段进行了系统的改进和扩充,形成了“system V IPC”, 其通信进程主要局限在单个计算机内。 后者则跳过了该限制,形成了基于套接口(socket)的进程间通信机制。
而 Linux 则把两者的优势都继承了下来,如下图所示,
2、Linux进程之间的通信种类
linux 进程之间的通信主要有下面几种:
通信方式 | 描述 |
管道 pipe, 命名管道 named pipe | 管道允许亲缘关系进程间的通信。 命名管道还允许无亲缘关系进程间通信。 |
信号 signal | 在软件层模拟中断机制,通知进程某事发生。 它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程 收到一个信号与处理器收到一个中断请求效果上可以说是一样的。 |
消息队列 message queue | 是消息的链接表,包括 posix 消息队列和 SystemV 消息队列,它克服了前两种通信方式中信息量有限的缺点。 具有写权限的进程可以按照一定的规则向消息队列中添加新消息。 对消息队列有读权限的进程则可以从消息队列中读取消息。 |
共享内存 Shared memory | 可以说是最有用的进程间通信方式,是最快的可用 ipc 形式。 是针对其他通信机制运行效率较低而设计。 它使得多个进程可以访问同一块内存空间,不同进程可以及时看到 对方进程中对共享内存中数据的更新。 这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。 |
信号量 Semaphore | 进程间同步。 主要作为进程之间以及同一进程的不同线程之间的同步和互斥手 段。 |
套接字 socket | 用于网络中不同机器之间的进程通信 |
3、管道
3.1 管道概述
管道好比一条水管,有两个端口,一端进水,另一端出水。 管道是 Linux 进程间通信的一种方式,如命令 ps -ef | grep ntp。
3.2 管道文件
我们软件的管道文件也有两个端口,分别是读端和写端。进水可看成数据从写端被写入,出水 可看数据从读端被读出。 在 linux 下文件类型为 p 的文件就是管道文件。
3.3 管道特点
(1)管道通信是单向的,有固定的读端和写端;
(2)数据被进程在管道读出后,管道中的数据就不存在了;
(3)当进程去读取空管道的时候,进程会阻塞;
(4)当进程往满管道写入数据时,进程会阻塞;
(5)管道容量为 64KB (#define PIPE_BUFFERS 16 include/linux/pipe);
3.4 通信框架
3.5 对管道文件进行操作
我们分别用 read、 write 函数来对管道的读端和写端进行读写,所以必须要知道读写两端 分别对应的文件描述符。
这两个文件描述符我们通常保存在一个有两个整型元素的数组中,如 int fds[2]; 然后调用函数 pipe(fds),这个函数会创建一个管道,并且数组 fds 中的两个元素会成为管 道读端和写端对应的两个文件描述符。
即 fds[0]和读端相对应, fds[1]和写端相对应。 fds[0]有可读属性, fds[1]有可写的属性。
4、标准流管道
像文件操作有标准 io 流一样,管道也支持文件流模式。 用来创建连接到另一进程的管道 popen 函数和关闭管道函数 pclose。
如果 open_mode 是“r”,被调用程序的输出就可以被调用程序使用,调用程序利用 popen 函数返回的 FILE*文件流指针,就可以通过常用的 stdio 库函数(如 fread)来读取被调用程 序的输出。 如果 open_mode 是“w”,调用程序就可以用 fwrite 向被调用程序发送数据,而被调用程 序可以在自己的标准输入上读取这些数据。
这两个函数应用于 Linux 执行 shell 命令的场景。
(1) popen(comm, type)函数会创建一个管道,再 fork 一个子进程,在子进程中执行 execX 函数来执行 comm 命令(因为 execX 执行新程序后新程序的进程空间会覆盖原进程的进程空间,所 以开一个子进程来执行 execX 家族函数),然后想要返回 stdout 或者 stdin 的文件指针(取决于 type); (2) 因为 comm 命令是通过子进程的执行的,那么 stdin 或者 stdout 文件指针也是子进程的 进程片空间的,要将其返回给父进程,这就需要管道了;
(3) stdin 是供程序写数据的,stdout 是供程序读数据的。这里设计的巧妙之处在于,管道 的读端跟 stdout 绑定,管道的写端跟 stdin 绑定;
(4) 读写管道操作的无非就是管道(文件)的 fd(文件描述符),这里将 fd 封装到文件流指针 fp 中; (5) popen 返回的是 stdout,那么 type 为”r”,表示创建一个管道且该管道文件的读端赋 给 fpr; (6) popen 返回的是 stdin,那么 type 为”w”,表示创建一个管道且该管道文件的写端赋 给 fpw; (7) 这样子,读 fpr(管道的读端)等于读子进程的 stdout,写 fpw(管道的写端)等于写子进 程的 stdin。
实例:从标准管道流中读取打印//etc/profile的内容;
#include <stdio.h>
int main()
{
char buf[512] = {0};
FILE* fp = popen("cat /etc/profile", "r"); //执行完这行代码,标准输出就装
满,这里这个标准输出标记为 out1,管道指向 out1,fp 指向管道的读端
//while ((ret = fread(buf, 1, sizeof(buf), fp)) > 0)
//从 out1 中读取 512 个字节数据,存放在 buf
while(fgets(buf, sizeof(buf), fp)){
puts(buf); //输出到终端
}
pclose(fp);
return 0;
}
5、无名管道 PIPE
5.1 无名管道特点
1)只能在亲缘关系进程间通信(父子或兄弟)。
2)半双工(固定的读端和固定的写端)。
3)它是特殊的文件,可以用 read、write 等函数操作,这种文件只能在内存中。
5.2 创建管道函数
管道函数原型:
#include <unistd.h>
int pipe(int fds[2]);
函数 pipe 用于创建一个无名管道,如果成功, fds[0]存放可读的文件描述符,fds[1]存 放可写文件描述符,并且成功返回 0,否则返回-1。 通过调用 pipe 函数获取这对打开的文件描述符后,一个进程就可以从 fds[0]中读数据,而 另一个进程就可以往 fds[1]中写数据。 当然两进程间必须有继承关系,才能继承这对打开的文件描述符。
管道不象真正的物理文件,不是持久的,即两进程终止后,管道也自动消失了。
示例:创建父子进程,创建无名管道,父写子读。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main()
{
int fds[2] = {0};
pipe(fds);
char buf[32];
if(fork() == 0){
sleep(2); //保证父进程有机会把数据写入
read(fds[0], buf, sizeof(buf)); //子进程从读端读数据
puts(buf);
close(fds[0]);
close(fds[1]);
}
else{
write(fds[1], "hello", 6); //父进程向写端写数据
waitpid(-1, NULL, 0); //等子退出
close(fds[0]);
close(fds[1]);
}
return 0;
}
注意: 管道两端的关闭是有先后顺序的。 如果先关闭写端则从另一端读数据时,read 函数将返回 0,表示管道已经关闭; 但是如果先关闭读端,则从另一端写数据时,将会使写数据的进程接收到 SIGPIPE 信号,如 果写进程不对该信号进行处理,将导致写进程终止,如果写进程处理了该信号,则写数据的 write 函数返回一个负值,表示管道已经关闭。
6、有名管道(FIFO)
无名管道只能在亲缘关系的进程间通信,这大大限制了管道的使用,有名管道突破了这个限制, 通过指定路径名的形式实现不相关进程间的通信。
6.1 创建、删除FIFO文件
创建 FIFO 文件与创建普通文件很类似,只是创建后的文件用于 FIFO。
1)用函数创建和删除 FIFO 文件 创建 FIFO 文件的函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
形参:
pathname 要创建的 FIFO 文件的全路径名;
mode 为文件访问权限,比如 0666。
返回值:
如果创建成功,则返回 0,否则-1。
删除 FIFO 文件的函数原型如下:
#include int unlink(const char *pathname);
跟 mkfifo 的第一个形参一样。
示例:用函数创建FIFO文件
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[]) //演示通过命令行传递参数
{
if(argc != 2){
puts("arg cnt err:");
return -1;
}
if(mkfifo(argv[1], 0666) == -1){
perror("mkfifo fail");
return -2;
}
//unlink(argv[1]);//加上这句会将创建的 FIFO 文件删除。
return 0;
}
/*
说明 :创建名字为 2 的 FIFO 文件
[root@localhost test]# gcc -o main main.c
[root@localhost test]# ./main 2 */
2)用命令创建和删除 FIFO 文件 用命令 mkfifo 创建 fifo 文件,不能重复创建。 用命令 unlink 删除 fifo 文件。 创建完毕之后,就可以访问 FIFO 文件了:
一个终端:cat < myfifo //输出 另一个终端:echo “hello” > myfifo //输入
6.2 打开、关闭FIFO文件
对 FIFO 类型的文件的打开/关闭跟普通文件一样,都是使用 open 和 close 函数。
1)如果打开时使用 O_WRONLY 选项,则打开 FIFO 的写入端,
2)如果使用 O_RDONLY 选项,则打开 FIFO 的读取端,
3)写入端和读取端都可以被几个进程同时打开。
该管道可以通过路径名来指出,并且在文件系统中是可见的。 在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便。
注意:
1)FIFO 是严格地遵循先进先出规则。
2)对管道及 FIFO 的读总是从该文件开始处返回数据。
3)对它们的写则把数据添加到该文件末尾。
4)它们不支持如 lseek()等文件定位操作。
6.3 读写FIFO文件
可以采用与普通文件相同的读写方式读写 FIFO。
示例: 先执行#mkfifo f.fifo 命令,创建一个 FIFO 文件。 编写 write.c,如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main()
{
int fd = open("f.fifo", O_WRONLY); //1. 打开(判断是否成功打开略)
write(fd, "hello", 6); //2. 写
close(fd); //3. 关闭
return 0;
}