为什么要进行进程间通信?
Linux作为一个多任务多进程的操作系统,各个进程之间的信息交互(事件通知,数据传输,进程控制)是不可避免的。进程间通信就是要在不同的进程之间传播或者交换消息。但是由于进程的独立性,所以导致进程间进行数据通信将变得非常麻烦。操作系统不得不提供方法来使进程间能够通信 。
操作系统为我们提供进程间通信的方式其实不止一种,因为通信的公共介质有所不同,因此进程间的通信方式不止一种。Linux的进程间通信方法有管道、消息队列、信号量、共享内存、套接口等。其中管道分为命名管道和无名管道。消息队列、信号量、共享内存通称为系统(POSIX 和 System V系统)IPC。管道、消息队列、信号量、共享内存用于本地进程间通信,而套接口用于远程进程间通信。
管道(Pipe)及命名管道(named pipe):管道可用于具有亲缘关系间的通信,命名管道克服了管道没有名字这个限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系的进程间的通信
进程间通信方式
01 管道
概念
管道(Pipe):也称为匿名管道,是Linux下的最常见的进程间通信方式之一,是一种很经典的进程间通信方式。其优点在于简单易用,其缺点在于功能简单,有很多限制。
管道的创建与关闭
创建函数原型: int pipe(int fd[2]);
返回值:成功返回0,失败返回-1。参数fd[2]是一个长度为2的文件描述符数组,fd[0]是读出端的文件描述符,fd[1]是写入端的文件描述符。当函数成功返回后,则自动维护了一个从fd[1]到fd[0]的数据通道。
比如下面这段代码就是用pipe函数创建管道和关闭管道
管道的读写
从上面的实例可以看出,单独一个进程操作管道是没有意义的,管道的应用一般体现在父子进程或者兄弟进程之间的通信。使用read函数和write函数对管道进行读写操作,但是要注意管道的两端是固定了任务的,即管道的读出端只能用于读写数据,管道的写入端只能用于写入数据。
- 读写特性
管道的读写操作默认是阻塞操作
管道自带同步与互斥:读写大小不超过PIPE_BUF大小
如果管道没有数据,那么read则一直等待,直到有数据
如果管道数据满了,那么write则一直等待,直到有数据被取出去
如果所有写入端关闭了,read读完所有数据之后,返回0
如果所有的管道读取端关闭了,那么写入端会触发异常 ,操作系统发送SIGPIPE信号,通知我们,读取端关闭了,(这个信号会导致write端进程退出)
读取数据量不大于PIPE_BUF时,管道可以保证读写的原子性(读写操作不会被打断,造成数据混乱的问题)
下面这个例子就是父进程利用管道向子进程发送消息
#include<stdio.h>
#include<unistd.h>
#include <errno.h>
#include <string.h>
//函数原型:
//int pipe(int fd[2])
int main()
{
pid_t pid;
int fd[2] = {0};
if(pipe(fd) < 0)
{
perror("create fail:");
return -1;
}
//创建子进程 fork()
if((pid = fork()) < 0)
{
perror("fork fail:");
return -1;
}
//父进程 写
else if(pid > 0)
{
char* str = "troye sivan";
close(fd[0]);
write(fd[1],str,strlen(str));
close(fd[1]);
}
//子进程 读
else
{
close(fd[1]);
char buff[256] = {0};
read(fd[0],buff,11);
printf("%s",buff);
close(fd[0]);
}
return 0;
}
注:pipe函数一定要在fork之前,因为子进程自动继承父进程的数据段,所以父子进程可以同时拥有管道的操作权。
02 命名管道
概念
命名管道(named pipe)也称为FIFO,它是一种文件类型,在文件系统中可以看到它,创建一个FIFO文件类似于创建一个普通文件。管道的一个很大限制就是它没有名字。因此,只能用于具有亲缘关系的进程间通信。命名管道提出后克服了该限制,FIFO不同于管道之处在于它提供了一个路径名与之关联,以FIFO 文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信。因此,通过FIFO不相关的进程也能交换数据。
命名管道的创建
两种方式:
- 在shell中可以使用mkfifo 命令建立一个管道 mkfifo[-m mode] name mode指出将创建FIFO的八进制模式,注意它也会受到umask修正。
- 通过函数创建 函数形式 : int mkfifo(const char* pathname,mode_t mode); pathname是一个普通的路径名,也就是创建后FIFO文件的名字。mode与open函数中的mode参数相同。如果mkfifo的第一个参数是已经存在的路径名时,则返回EEXIST错误,所以一般典型的调用代码首先会检查是否返回该错误,如果确实返回该错误,那么只要调用打开FIFO函数就可以了。
下面这个例子是使用FIFO来进行两个进程间的通信
read_fifo.c
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
int main()
{
umask(0);
if(mkfifo("troye",0664) < 0)
{
if(errno == EEXIST)
{
printf("fifo exit.\n");
}
perror("fifo:");
return -1;
}
int fd = open("troye",O_RDONLY);
if(fd < 0)
{
perror("open error:");
return -1;
}
printf("open fifo successful!\n");
while(1){
char buf[1024];
memset(buf, 0x00, 1024);
int ret = read(fd,buf,1024);
if(ret > 0)
{
printf("client say:%s\n",buf);
}
}
close(fd);
return 0;
}
write_fifo.c
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
int fd = open("troye",O_WRONLY);
if(fd < 0)
{
perror("open error:");
return -1;
}
printf("open fifo successful!\n");
while(1){
char buf[1024] = {0};
printf("input:");
fflush(stdout);
scanf("%s",buf);
write(fd,buf,1024);
}
close(fd);
return 0;
}
在程序read_fifo.c中先通过mkfifo创建一个FIFO,然后打开一个名为troye的FIFO的管道。读取数据并输出到标准输出在程序write_fifo.c中先打开troye文件,再通过标准输入写入数据。