复杂的编程环境通常使用多个相关的进程来执行有关操作。进程之间必须进行通信,来共享资源和信息。因此,要求内核提供必要的机制,这些机制通常称为进程间通信(InterProcess Communication, IPC)。
一、Linux平台通信方式发展史
- 早期通信方式:早期的Unix IPC包括管道、FIFO和信号
- AT&T的贝尔实验室,对Unix早期的进程间通信进行了改进和扩充,形成了“system V IPC”,其通信进程主要局限在单个计算机内。
- BSD(加州大学伯克利分校的伯克利软件发布中心),跳过了只能在同一计算机通信的限制,形成了基于套接字(socket)的进程间通信机制。
二、进程间通信方式⭐⭐⭐
- 早期通信:无名管道(pipe),有名管道(fifo)、信号(sem)
- system V IPC:共享内存(share memory) 、信号灯集(semaphore)、消息队列(message queue)
- BSD:套接字(socket)
三、无名管道
3.1 特点⭐⭐⭐
- 只能用于具有亲缘关系的进程间进行通信
- 半双工通信,具有固定的读端与写端
(单工:只能单方面传输信息->广播
半双工:可以双向,但是同一时间只能一个方向传输信息
全双工:可以双向同时传输信息) - 无名管道可以看作一种特殊的文件,对它的读写采用文件IO:read、write
- 管道是基于文件描述符通信方式。当一个无名管道创建会自动创建两个文件描述符,分别的fd[0]、fd[1],其中fd[0]固定的读端,fd[1]固定的写端
3.2 函数pipe
int pipe(int fd[2])
- 功能:创建无名管道
- 参数:文件描述符(fd[0]:读端 fd[1]:写端)
- 返回值:成功 0;失败 -1
注📢:管道要用文件I/O进行操作(read,write,close)且管道创建后,fd[0]=3,fd[1]=4
例:
3.3 注意事项⭐⭐⭐
- 当管道中无数据时,读操作会阻塞;管道中无数据,将写端关闭,读操作会立即返回
- 管道中装满(管道大小64K)数据写阻塞,一旦有4k空间,写继续,直到写满为止
- 只有在管道的读端存在时,向管道中写入数据才有意义。否则,会导致管道破裂,向管道中写入数据的进程将收到内核传来的SIGPIPE信号 (通常Broken pipe错误)。(GDB调试可以查看到)
代码示例:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char const *argv[])
{
int fd[2] = {0};
if (pipe(fd) < 0)
{
perror("pipe error");
return -1;
}
printf("fd[0]:%d fd[1]:%d\n", fd[0], fd[1]);
char buf1[32] = {"hello world!"};
char buf2[32] = {0};
// write(fd[1],buf1,strlen(buf1)); //往管道写入buf1
// ssize_t s = read(fd[0],buf2,32); //从管道读取数据到buf2
// printf("%s %d\n",buf2,s);
// close(fd[0]);
// close(fd[1]);
#if 0
// 1.管道中无数据,读阻塞
read(fd[0], buf2, 32);
#endif
#if 0
// 2.将写端关闭,读操作会立即返回
close(fd[1]);
read(fd[0],buf2,32);
#endif
#if 1
//3.1 当无名管道中写满数据64k,写阻塞
char buf[65536] = {0};
write(fd[1], buf, 65536);
printf("full\n");
write(fd[1], "a", 1);
printf("write a ok\n");
//至少读出4k的空间,才能继续写
read(fd[0], buf, 4095);
write(fd[1], 'a', 1);
printf("write 'a' ok\n");
#endif
#if 1
// 3.1 将读端关闭,继续写
close(fd[0]);
write(fd[1], "a", 1);
printf("ok...\n");
#endif
// close(fd[0]);
// close(fd[1]);
return 0;
}
第三种情况管道破裂,通过GDB调试查看到的结果如下:
3.4 练习
父子进程实现通信,父进程循环从终端输入数据,子进程循环打印数据,输入一次打印一次,当输入quit结束,使用无名管道
/*
练习:父子进程实现通信,父进程循环从终端输入数据,
子进程循环打印数据,当输入quit结束,使用无名管道
*/
#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 const *argv[])
{
char buf[32] = {0};
int fd[2] = {0};
if(pipe(fd)<0) //创建无名管道
{
perror("pipe err");
return -1;
}
pid_t pid = fork();
if(pid < 0)
{
perror("fork err");
return -1;
}
else if(pid == 0)
{
while (1) //子进程循环从管道读取数据,管道为空阻塞
{
read(fd[0],buf,32);
if(strcmp(buf,"quit")==0)
exit(0);
printf("%s\n",buf);
}
}
else
{
while(1)//循环从终端输入数据,循环往管道写入数据
{
//scanf("%s",buf);
fgets(buf,32,stdin);
if(buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
write(fd[1],buf,strlen(buf)+1);
if(strcmp(buf,"quit")==0)
exit(0);
}
wait(NULL);
}
close(fd[0]);
close(fd[1]);
return 0;
}
四、有名管道
4.1 特点⭐⭐⭐
- 可以用于两个不相关的进程之间通信
- 有名管道可以通过路径名指出,在文件系统中可见,但内容存放在内存里
- 通过文件IO操作
- 遵循先进先出,故不支持lseek操作
- 半双工通信
4.2 函数 mkfifo
int mkfifo(const char *filename,mode_t mode);
- 功能:创健有名管道
- 参数:
- filename:有名管道文件名
- mode:权限
- 返回值:成功:0;失败:-1,并设置errno号
注意对错误的处理方式:📢📢
如果错误是file exist时,注意加判断,如:if(errno == EEXIST)
执行如下代码:
第一次运行:
再次运行:
处理方式:捕捉错误码,进行过滤即可👇
- 由上面的有名管道特点的第二条可以知道,写入有名管道的内容并非存放在文件中,而是存在内存,也就是说有名管道文件的大小为0,下面进行验证:
在写后面加一个while死循环,运行后会等写完阻塞,不读出数据。然后在终端可以查看当前管道文件的大小,结果是大小为0👇
4.3 注意事项⭐⭐
- 只写方式,写阻塞,直到另一个进程将读打开
- 只读方式,读阻塞,直到另一个进程将写打开
- 可读可写,管道中无数据,读阻塞。
验证1,2:
创建两个c文件,一个以只读方式打开有名管道,从中读数据;另一个以只写方式打开同一个有名管道,从中写数据。
只读
只写
通过运行可以看到,运行了其中任意一个,会发生阻塞,只有当再运行另一个才可以解除阻塞,两个程序得以顺利执行下去。验证了只写方式,写阻塞,直到另一个进程将读打开; 只读方式,读阻塞,直到另一个进程将写打开。
还可以得知,上面程序并不是在read或iwrite发生阻塞,而是在open函数处发生了阻塞。
补充:如果所有写进程都关闭命名管道,则只读进程的读操作会认为到达文件末尾,读操作解除阻塞并返回
验证:
以只读方式打开有名管道的程序代码1.c👇
以只写方式打开有名管道程序代码2.c👇
先执行1.c,会发生阻塞;再执行2.c,2.c不会往管道写入数据(保证1.c不会因为管道中有数据而解除阻塞),2.c间隔1秒关闭管道,可以看到原先阻塞的1.c也会在2.c执行1秒后解除阻塞,且read返回值为0。
4.4 练习
通过有名管道实现cp文件复制
方法一:两个c文件,一个只读管道,一个只写管道
3cp_MkfifoToDest.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if(argc != 2)
{
printf("Please input %s <des>\n",argv[0]);
return -1;
}
if(mkfifo("./fifo",0666) < 0)//创建有名管道
{
//处理文件已存在的情况
if(errno == EEXIST)//EEXTST=17
{
printf("file eexist\n");
}
else
{
perror("mkfifo err");
return -1;
}
}
//打开管道和目标文件
int fd = open("./fifo",O_RDONLY);
//此处一定不要用可读可写的方式打开
//若以可读可写的方式打开,管道中无数据则读阻塞
if(fd<0)
{
perror("open fifo err");
return -1;
}
int dest = open(argv[1],O_WRONLY|O_CREAT|O_TRUNC,0666);
if(fd<0)
{
perror("open dest err");
return -1;
}
//循环读管道,写目标文件
ssize_t s;
char buf[32] = {0};
while ((s=read(fd,buf,32)) != 0)
write(dest,buf,s);
close(fd);
close(dest);
return 0;
}
3cp_SrcToMkfifo.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
if(argc != 2)
{
printf("Please input %s <src>\n",argv[0]);
return -1;
}
if(mkfifo("./fifo",0666) < 0)//创建有名管道
{
//处理文件已存在的情况
if(errno == EEXIST)//EEXTST=17
{
printf("file eexist\n");
}
else
{
perror("mkfifo err");
return -1;
}
}
//打开管道和源文件
int fd = open("./fifo",O_WRONLY);
if(fd<0)
{
perror("open fifo err");
return -1;
}
int src = open(argv[1],O_RDONLY);
if(fd<0)
{
perror("open src err");
return -1;
}
ssize_t s;
char buf[32] = {0};
while ((s=read(src,buf,32)) != 0)
{
write(fd,buf,s);
}
close(fd);
close(src);
return 0;
}
运行结果:
方法二:单个c文件,用父子进程实现,父进程只写有名管道,子进程只读有名管道
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
if (argc != 3)
{
printf("Please iniput %s <src> <dest>\n", argv[0]);
return -1;
}
if (mkfifo("./fifo", 0666) < 0) //创建有名管道
{
//处理文件已存在的情况
if (errno == EEXIST) //EEXTST=17
{
printf("file eexist\n");
}
else
{
perror("mkfifo err");
return -1;
}
}
//打开管道、源文件、目标文件
int src = open(argv[1], O_RDONLY);
if (src < 0)
{
perror("open src err");
return -1;
}
int dest = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (dest < 0)
{
perror("open dest err");
return -1;
}
ssize_t s;
char buf[32] = {0};
pid_t pid = fork(); // 创建子进程
if (pid < 0)
{
perror("fork err");
return -1;
}
else if (pid == 0) //子进程从有名管道中读出数据,写到目标文件中
{
int fd = open("./fifo", O_RDONLY); //管道设置为只读
if (fd < 0)
{
perror("open fifo err");
return -1;
}
while ((s = read(fd, buf, 32)) != 0)
write(dest, buf, s);
printf("child end...\n");
close(fd);
exit(0);
}
else //父进程从源文件读出数据,写到有名管道中
{
int fd = open("./fifo", O_WRONLY); //管道设置为只写
if (fd < 0)
{
perror("open fifo err");
return -1;
}
while ((s = read(src, buf, 32)) != 0)
write(fd, buf, s);
printf("parent end...\n");
close(fd);
wait(NULL);
}
close(src);
close(dest);
return 0;
}
注:该程序容易被怀疑最后在子进程的read(fd, buf, 32)会发生阻塞,其实不然,这里用到了上面的一个要点:如果所有写进程都关闭命名管道,则只读进程的读操作会认为到达文件末尾,读操作解除阻塞并返回文章来源:https://www.toymoban.com/news/detail-645868.html
五、无名管道与有名管道对比⭐⭐
文章来源地址https://www.toymoban.com/news/detail-645868.html
到了这里,关于LinuxC编程——进程间通信(一)(管道)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!