本文目录
前述
我们可以在系统上使用ipcs
命令查看当前正在使用的进程通信方式。下面的内容是因为我自己本文章的写代码时所创建的。
一、linux 进程之间的通信种类
序号 | 通信方式 | 描述 |
---|---|---|
1 | 管道(无名管道、有名管道) | 无名管道允许亲缘关系进程间的通信。有名命名管道还允许无亲缘关系进程间通信。 |
2 | 信号 signal | 在软件层模拟中断机制,通知进程某事发生。它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一样的。 |
3 | 消息队列 Messge Queue | 是消息的链接表,包括 posix 消息队列和 SystemV 消息队列。它克服了前两种通信方式中信息量有限的缺点。 |
4 | 共享内存 Shared memory | 可以说是最有用的进程间通信方式,是最快的可用 ipc 形式。是针对其他通信机制运行效率较低而设计。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种通信方式需要依靠某种同步机制,如互斥锁和信号量等。 |
5 | 信号量Semaphore | 进程间同步。主要作为进程之间以及同一进程的不同线程之间的同步和互斥手段。 |
6 | 套接字 socket | 用于网络中不同机器间进程通信。 |
二、管道
1. 管道的概述
管道好比一条水管,有两个端口,一端进水,另一端出水。 管道是 Linux 进程间通信的一种方式,如管道命令
ls -l | grep anaconda3
,意思是从ls -l中搜索含有anaconda3的文件内容。
2. 什么是管道文件?
我们软件的管道文件也有两个端口,分别是读端和写端。进水可看成数据从写端被写入,出水可看数据从读端被读出。
3. 管道的特点
(1)管道通信是单向的,有固定的读端和写端;
(2)数据被进程在管道读出后,管道中的数据就不存在了;
(3)当进程去读取空管道的时候,进程会阻塞;
(4)当进程往满管道写入数据时,进程会阻塞;
(5)管道容量为 64KB;
4. 管道类型
管道类型分为无名管道、命名(有名)管道两类。无论是哪种管道我们都用 read、 write 函数来对管道进行读写。对于不同的管道类型有不同的方法。
对于无名管道,由于读端和写端处于血缘关系的进程中(同一个main函数中),所以必须要知道读写两端分别对应的文件描述符。
而对于命名(有名)管道,读端和写端处于毫无关系的两个进程中,所以需要创建一个文件作为管道,来对文件进行读写操作。注意:有名管道不支持创建在共享目录下,因为共享目录里属于windows系统,不支持此类文件。且管道文件大小为0,并不会存储内容,只是作为介质存在。
(1)无名管道(pipe)
无名管道用于在一个main里的进程中,必须是父子进程或兄弟进程(一个父进程创建的多个子进程之间的关系)。
问题:那么既然在同一个main函数里进程通信为什么一定要用管道呢?直接定义一个全局变量的文件进行读写不行吗?要知道使用fork创建进程时,会复制一份完全一样的内容,子进程对文件的读写并不会影响父进程中文件的状态。那既然这样使用vfork创建进程不就行了吗?子进程和父进程共享同一份文件。但是使用vfork创建的进程,必须子进程结束后,才会执行父进程。如果想要读写两端同时进行的话,这种方式显然不行。所以就引入了无名管道进行这类进程之间的通信。
例如创建无名管道时,我们使用pipe()
来创建无名管道。对于无名管道的读写文件描述符我们通常保存在一个有两个整型元素的数组中,如 int fds[2]。然后调用函数 pipe(fds),这个函数会创建一个管道,并且数组 fds 中的两个元素会成为管道读端和写端对应的两个文件描述符。即 fds[0]为读端文件描述符, fds[1]为写端文件描述符。
无名管道的特点:
① 只能在亲缘关系进程间通信(父子或兄弟)。
② 半双工(固定的读端和固定的写端)。
③ 它是特殊的文件,可以用 read、write 等函数操作,这种文件只能在内存中。
管道两端的关闭是有先后顺序的。如果先关闭写端则从另一端读数据时,read 函数将返回 0,表示管道已经关闭;但是如果先关闭读端,则从另一端写数据时,将会使写数据的进程接收到 SIGPIPE 信号,如果写进程不对该信号进行处理,将导致写进程终止,如果写进程处理了该信号,则写数据的 write 函数返回一个负值,表示管道已经关闭。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc,char **argv)
{
int rec;
int pipefd[2]; //pipefd[0]读 pipefd[1]写
rec= pipe(pipefd);
if(rec < 0) {
printf("error!\n");
}
pid_t pid;
pid=fork(); //创建进程
int i=0;
if(pid==0){
//子进程写
char buff[64];
while(1){
i++;
memset(buff,0,strlen(buff)); //清空数组
fgets(buff, sizeof(buff),stdin); //从终端获取字符给buff
write(pipefd[1], buff, sizeof(buff)); //将buff数组写入管道
if(i==3) break;
}
close(pipefd[1]); //关闭管道
close(pipefd[0]);
exit(0); //退出子进程
}
else if(pid>0){
//父进程读
char data[64];
int n;
wait(&n); //等待子进程结束,防止成为僵尸进程。
while(1)
{
i++;
memset(data,0,strlen(data));
read(pipefd[0], data, sizeof(data)); //从管道中读取内容传给data数组
printf("%s",data);
if(i==3) break;
}
close(pipefd[1]);
close(pipefd[0]);
exit(0);
}
else {
printf("error!\n");
}
}
(2)有名(命名)管道(fifo)
无名管道只能在亲缘关系的进程间通信,这大大限制了管道的使用,有名管道突破了这个限制,通过指定路径名的形式实现不相关进程间的通信。
这里我们使用两个不相关的进程分别来进行通信,即使用两个命令窗口分别运行管道读端的程序和写端的程序。首先我们需要在写端使用命令mkfifo
创建管道。其第一个参数为创建的管道文件名,第二个参数为文件的权限。
FIFO管道必须读写两端都要打开。打开管道文件时默认选择为阻塞模式,则没有进程打开 FIFO 进行读取,写操作将会被阻塞,直到有进程打开 FIFO 进行读取为止,反之也一样。但是FIFO 本身有一个内核缓冲区,写入的小量数据可能会暂存在缓冲区中,等待读端读取。如果选择非阻塞模型打开(O_NONBLOCK
),则会open调用会立即返回失败。
问题:既然是创建文件作为管道,那么这个管道文件和普通文件有什么区别呢?
答:普通文件:用于存储数据,数据可以随机访问,可以读写多次,数据在文件关闭后依然存在。命名管道:用于进程间通信,数据是流式的,主要用于一次性读写,数据在被读取后即被移除,不持久存储。数据以流的形式传输,写入的数据只能按顺序读取,通常一个进程写入数据后,另一个进程立即读取。
●有名管道写端(write)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
// 有名管道:用于两个无亲属关系的进程间的通信。 例如fifo_w 和fifo_r 间的通信。
int main(int argc, char **argv)
{
int fd;
char buff[64];
//创建一个有名管道文件,文件权限为可读可写
mkfifo("/tmp/fifo.cmd",0666);
//系统io来打开文件 ,不是标准io。打开有名管道的写端。
fd=open("/tmp/fifo.cmd",O_WRONLY);
if(fd < 0) printf("error!\n");
while(1)
{
memset(buff, 0, sizeof(buff));
fgets(buff, sizeof(buff), stdin); //从终端获取字符给buff
write(fd, buff, strlen(buff));
}
close(fd);
//删除有名管道文件
// unlink("/tmp/fifo.cmd");
return 0;
}
●有名管道读端(read)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
//管道文件不支持创建在共享目录下,因为共享目录也属于windows
//有名管道:无亲属关系的进程间通信(不在同一个main函数中的进程)
// fopen:标准io open:系统io
int main(int