1.首先我们来聊一聊为什么要引入进程间的通讯。
在我们之前的学习当中,我们知道进程都是独立的个体,每个进程之间数据是不共享的,要实现进程之间数据的通讯,我们可以使用fork。因为父子进程可以共享fork之前打开的文件描述符。
例子:
父进程需要给子进程发送一个“hello world”的字符来说,按照fork的方式实现如下:
int main()
{
int fd = open("a.txt",O_RDWR);
pid_t pid = fork();
if(pid == 0)
{
sleep(1);
lseek(fd,0,SEEK_SET);
read(fd,buff,127);
}
else
{
write(fd,"hello world",127);
}
}
2.在这里我们来分析一下这段代码的特殊处理。
1、为什么要让子进程睡眠一秒?
原因就是保证父进程已经将字符串写到了文件中。
2、为什么在读取之前,需要把fd的读写偏移量偏移到起始位置?
因为父子进程共享的文件描述符中f_pos成员就是文件读写偏移量。在父进程的write完成后是在文件的末尾,所以在子进程的读取之前要将读写偏移量移到起始位置
3.上述处理方式的问题
基于上述进程间通信的不足,我们推出了进程间的通讯方式——管道、消息队列、信号量、共享内存
。这篇文章我们来了解一下管道和消息队列。
1、概念
在磁盘上有个管道文件标识,但是这个管道文件只会占据一个innode结点,但是这个管道文件任何时候不会占据block块(也就是说数据不会存储到磁盘中)。数据在传递过程中缓存在内存上。
简单点来说,管道文件的作用
仅仅是为了使得不同的进程(有权限操作的)能够共享
2、创建和打开
方法一:
通过mkfifo filename命令。操作如下:
方法二:
通过库方法int mkfifo()。该函数详情如下:
第一个参数代表路径,第二个参数代表权限值。
打开还是按照文件的一系列方式open read write
3、具体实现
实现在mainA.c中让终端写入数据,在mainB.c中读取终端写入的数据。其代码实现如下:
mainA.c
#include
#include
#include
#include
#include
#include
//写入数据
int main()
{
int fd = open("./FIFO",O_WRONLY);//只需要只写的方式打开管道文件
assert(fd != -1);
printf("Write open fifo success\n");
//获取操作者输入
while(1)
{
char buff[128] = {0};
printf("input: ");
fgets(buff,127,stdin);
if(strncmp(buff,"end",3) == 0)
{
break;
}
write(fd,buff,strlen(buff)-1);//将用户输入的数据写入到管道文件当中
}
close(fd);
}
mainB.c
#include
#include
#include
#include
#include
#include
int main()
{
int fd = open("./FIFO",O_RDONLY);//以只读的方式打开管道文件
assert(fd != -1);
printf("Read open fifo success\n");
while(1)
{
char buff[128] = {0};
int n = read(fd,buff,127);//从管道文件中读取文件放到buff中
if(n <= 0)
{
break;
}
printf("Read: %s\n",buff);//将读到的数据显示出来即可
}
close(fd);//将内存空间释放掉
}
测试1:
只执行进程A
我们会发现,程序没有再继续往下执行。其实就是阻塞到open这个方法的。
因为我们操作的是管道文件,对于管道文件而言,这是以只写的方式,当没有数据读的时候打开是没有意义的。
测试2:
写一个数据,再读出数据
操作完成后管道文件还是为0,所以不存在数据往磁盘中存的问题。
4、实现原理
我们可以花下面的图示来表示整个数据共享的过程。
从图中我们可以看到,磁盘中的inode区域指向了内核开辟的一块内存空间,而这个空间就是用来传递数据的,A,B两个进程一个是以读的方式一个是以写的方式打开,都存在一个读写偏移量,都指向该内存的起始位置。通过两个进程在磁盘中是共享的去访问FIFO的inode结点,通过inode结点去指向内核中开辟的这块空间,去写入和读取数据
这也可以解释为什么open会阻塞,因为只以一种(比如说只写)方式去打开他,如果没有读的话,其实开辟这块内存空间就没有意义了
总结如下:
open以一种方式打开管道文件会阻塞,直到进程以另一种方式打开此管道文件
如果管道对应的内存空间中没有数据,则read会阻塞,直到1内存中有数据,2写端关闭
如果管道对应的内存空间已满,则write就会阻塞,直到1内存中有空间2所有读端关闭
5、补充
1、概念
无名管道是借助父子进程共享fork之前打开的文件描述符来实现进程间的通讯。由父进程创建的子进程将会赋值父进程包括文件在内的一些资源。
如果父进程创建子进程之前创建了一个文件,那么这个文件的描述符就会被父进程在随后所创建的子进程所共享。也就是说,父、子进程可以通过这个文件进行通信。如果通信的双方一方只能进行读操作,而另一方只能进行写操作,那么这个文件就是一个只能单方向传送消息的管道,如下图所示:
2、无名管道的创建
进程可以通过调用函数==pipe()==创建一个管道。函数pipe()的原型如下:
int pipe(int fildes[2]);
3、通信原理
3.1 当一个进程调用函数pipe()创建一个管道后,管道的连接方式如下所示:
3.2 如果父进程创建一个管道之后,又创建了一个子进程,那么由于子进程继承了父进程的文件资源,于是管道在父子进程中的连接情况就变成如下图一样的情况了:
3.3 在确定管道的传输方向之后,在父进程中关闭(close())文件描述符fildes[0],在子进程中关闭(close())文件描述符fildes[1],于是管道的连接情况就变成如下情况的单向传输管道:
有了以上的通信原理,我们可以实现如下的代码实现数据间的通讯。
#include
#include
#include
#include
#include
int main()
{
int fds[2] = {-1,-1};
// pipe必须在fork之前调用
int res = pipe(fds); // 创建并打开一个无名管道,fd[0]为读端,fd[1]为写端
assert(res != -1);
pid_t n = fork();
assert(n != -1);
if(n == 0)
{
close(fds[1]);//子进程直接关闭管道的写端
while(1)//读取数据放到buff里面
{
char buff[128] = {0};
int n = read(fds[0], buff, 127);
if(n <= 0 || 0 == strncmp(buff, "end", 3))
{
break;
}
printf("child: %s\n", buff);
}
close(fds[0]);
}
else
{
close(fds[0]);//父进程中直接关闭管道的读端
while(1)
{
printf("please input:");
char buff[128] = {0};
fgets(buff, 128, stdin);
write(fds[1], buff, strlen(buff) - 1);
if(strncmp(buff, "end", 3) == 0)
{
break;
}
}
close(fds[1]);
}
}
测试:
为什么子进程打印的数据在input后面?
父进程里面把数据写入过后直接就printf(“please input:”);,他不会阻塞等待子进程去读。相应的在子进程中也是读到数据过后就直接打印。
4、特点