1、管道
1.1 匿名管道
主要用于有亲缘关系的进程间通信
我们先来看一条Linux语句:
test1 | grep 8080
如果你还没忘记Linux命名,那一定知道 “ | ”的意思,其实它就是管道的意思,他的作用就是把前一个命令的输出作为后一条命令的输入。
以上代码其实就是把test1的输出作为grep 8080这条命令的输入,这里的“ | ”其实是一个匿名管道,只能用于有亲缘关系进程的通信。
如果两个进程需要通信的话就得创建两个管道。因为这种通信方式是单向的以下是关于匿名管道的实例:
在程序中,我们创建了一个管道,父进程关闭了写通道,子进程关闭读通道,子进程向管道写入字符串,父进程向管道读取字符串并输出。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#define MAX_LEN 128
int main(void)
{
/*0为读,1为写*/
int fd[2] = {0}; //描述符
pid_t pid = 0;
char line[MAX_LEN] = {0};
int n = 0;
/*创建管道,需要传入两个文件描述符*/
if(pipe(fd) < 0)
{
perror("create pipe failed\n");
return -1;
}
/*fork子进程*/
if((pid = fork()) < 0)
{
perror("fork failed\n");
return -1;
}
/*父进程*/
else if(pid > 0)
{
/*关闭管道的写描述符*/
close(fd[1]);
/*从管道读取数据*/
n = read(fd[0],line,MAX_LEN);
printf("read %d bytes from pipe :%s\n",n,line);
}
/*子进程*/
else
{
/*关闭管道的读描述符*/
close(fd[0]);
/*向管道写入数据*/
write(fd[1],"test",sizeof("test"));
}
return 0;
}
1.2、命名管道
命名管道可以用于任意进程间通信。
以下是创建一个命名管道等方法:
mkfifo test
这条命令创建了一个名为test的管道,接下来我们用一个进程向这个管道写数据,然后另一个进程将写入的数据读出:
echo “this is a pipe” >test
如果该管道的数据没有被读出,那它将一直等待,直到有其他进程经数据读出:
cat < test //读数据
只有数据被其他进程读出,上一条写数据的才算结束。
这里也体现出管道的缺点——效率低下,我给你传数据,但是只有你把数据拿走了,我才能返回,这样是很慢的,不适合频繁通信的进程。但同时也有一个优点,那就是可以保证你是真的拿到了我的数据。
以下是一个关于命名管道完整代码的实例:
我们首先创建一个命名管道,如果成功,则关闭他的读端,向里面写入数据。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#define FIFO "/tmp/fifo"
#define MAX_LEN 128
int main(void)
{
int writeFd;
char line[MAX_LEN] = {0};
if(mkfifo(FIFO,S_IRUSR|S_IWUSR) < 0 && (errno != EEXIST))
{
perror("make fifo failed:");
return -1;
}
/*关闭管道的读描述符*/
writeFd = open(FIFO,O_WRONLY,0);
/*向管道写入数据*/
write(writeFd,"www.yanbinghu.com",sizeof("www.yanbinghu.com"));
close(writeFd);
return 0;
}
然后再新的终端打开一个刚才创建的命名管道,从里面读取数据:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include<fcntl.h>
#define FIFO "/tmp/fifo"
#define MAX_LEN 128
int main(void)
{
int readFd,n;
char line[MAX_LEN] = {0};
/*打开FIFO,这里打开可能失败,应该要对返回值处理*/
readFd = open(FIFO,O_RDONLY,0);
/*从FIFO读取数据*/
n = read(readFd,line,MAX_LEN);
printf("read %d bytes from pipe :%s\n",n,line);
close(readFd);
/*删除FIFO*/
unlink(FIFO);
return 0;
}
然后运行写进程和读进程,这样两个没有亲缘关系的进程就可以通过匿名管道进行通信了。
总结:
- 半双工,即就是不能够同时在两个方向上传输数据,有的系统可能支持全双工。
- 匿名管道只能用于有亲缘关系的进程进行通信
- 命名管道可以用于任意进程间通信
2、消息队列
学习了以上管道我们知道,a向b 写数据,只有b把数据读出,a才能返回,那我们能不能不等待,我把数据传给你,然后我立即返回呢?
其实是可以的,消息队列就可以解决这样的问题,a只要把给b的数据放进消息队列里面就可以了,b要用数据直接从消息队列取出。
- 缺点:如果a给b发送的数据过大,并且通信频繁,那消息队列就不合适了,因为数据过大,a发送数据的时间长,b去拷贝数据花费时间也长,效率就会变低。
- 优点:管道一旦相关进程退出,那里面的数据也就没有了,但消息队列不一样,一个进程向里面写入数据后退出,另一个进程仍然可以取数据。
3、共享内存
针对消息队列的缺点,共享内存允许多个进程共享一个给定的存储区,由于他们是共享一块内存数据,减少了内存拷贝的时间,因此速度非常快。
但这里我们可能会问:每个进程不是独立的吗?怎么可以共享内存呢?
其实,系统加载一个进程时,分配给进程的内存并不是实际的物理内存,而是虚拟内存空间,我们可以让两个进程各自拿出一块虚拟地址空间来,然后映射到相同的物理内存中,这样虽然两个进程有独立的虚拟地址空间,但有一部分是映射的相同的物理内存,这样就完成了内存共享机制,如下图所示:
缺点:多进程竞争内存,类似于我们平常说的线程安全。
4、信号量
一句话概括:信号量的本质就是一个计数器,用来实现进程之间的同步与互斥。
例如信号量的初始值为1,然后a进程来访问共享内存的时候,我们把信号量的值设为0,然后b进程来访问共享内存的时候,看到信号量的值为0,就知道已经有人来访问内存了,那b就访问不了了。所以说信号量也是进程间的一种通信方式。
5、Socket
以上我们所说的管道,消息队列,共享内存,信号量都是同一主机之间的通信,那两个相隔几千里的进程怎么进行通信呢?
答案就是Socket,例如平时我们通过浏览器发起一个http请求,然后服务器给你返回对应的数据,这种就是采用了Socket的通信方式,也就是说:它能用于不同计算机之间的不同进程间的通信。
总结
- 对于管道:速度慢,最后一个引用它的进程终止的时候,留在管道的数据也会被删除。匿名管道用于有亲缘关系的进程,命名管道用于任意键进程通信。半双工通信,一个写数据,一个读数据,只能是单向的。
- 消息队列:内核中的优先级队列,多个进程通过向同一个队列中放置队列结点或者获取节点实现通信
- 共享内存:各个进程通过映射相同的物理地址实现通信,减少了内存拷贝时间。但是要注意多进程通信的安全
- 信号量:通过计数器来检测内存的访问情况,如果有进程访问内存,计数变为0,没有进程访问,计数变为1.计数只有0/1两种状态。
- Socket用于不同计算机之间不同进程的通信,是目前应用最广泛的进程间通信方式。