【Linux】进程间通信(管道:匿名管道、命名管道、实战练习)
知其然,知其所以然
什么是进程间通信:
进程间通信是不同进程间交换信息的一种机制。进程可能在同一台计算机上,也可能在网络中的不同计算机上。那我们为什么要有这种机制:
为什么进程间要通信:
①数据共享:多个进程需要访问共享数据,通过通信来保证数据的一致性和有效性
②同步和协调:在并发程序中,某些进程必须等待其他进程完成某项任务,以确保程序的正确性。
③资源管理:在有限的资源(如内存、文件、设备等)下,进程需要协调使用这些资源,避免竞争和冲突。
④模块化设计:通过将功能分散到多个程序中,可以提高程序的可管理性和可拓展性,但分散到多个程序中,势必要求多个进程能够访问同一资源、数据。
进程间通信的方式有:管道(Pipes)、消息队列(Message Queues)、共享内存(Shared Memory)、信号(Signals)、套接字(Sockets)。其中套接字虽然可以在同一台计算机上的进程间使用,但主要是用于网络上不同计算机之间的通信,本节我们呢不涉及该模块的内容。
通信方式--管道
用于一对进程之间进行数据流转,常用于父进程与子进程之间的通信
管道是Unix中最古老的进程间通信的形式,我们把:从一个进程连接到另一个进程的数据流称为一个“管道”。例如:我们之间使用的管道符,统计登录用户的个数:who | wc -l
其中,who命令和wc命令是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”里,wc进程再通过标准输入从“管道”中读取数据,至此,便完成了数据的传输,进而完成数据的进一步加工处理。
一般来讲,管道的特征是单向通信(实现方式为关闭不用的文件描述符)。管道有读端和写端
在C编程中的应用:
管道实际上分为两种:匿名管道和命名管道
在此之前,我们想要进行通信,我们能想到的只有通过文件来传递信息。
int main(int argc, char* argv[])
{
int fd_w=open("./test.txt",O_WRONLY| O_CREAT| O_TRUNC ,0644);//写端
int fd_r=open("./test.txt",O_RDONLY);//读端
pid_t fpid=fork();//创建子进程
if(fpid<0)exit(-1);//创建子进程失败
else if(fpid==0){//子进程
close(fd_r);//关闭读端
// char buf_w[1024];//写入缓冲区
// memset(buf_w,'\0',sizeof(buf_w));
char *str="hello world";
int ret = write(fd_w,str,strlen(str));
if(ret<0)perror("write err");
}
else if(fpid>0){//父进程
close(fd_w);//关闭写端
char buf_r[1024];//读出缓冲区
memset(buf_r,'\0',sizeof(buf_r));
int cnt=0;
sleep(1);//等待子进程写入
while( cnt = read(fd_r,buf_r,sizeof(buf_r)) ){
if(cnt<0)perror("read err");
else printf("sucess: %s\n",buf_r);
}
}
return 0;
}
在该示例中,我们将文件使用两种方式打开,分别使用两个文件描述符管理,在父子进程中分别只保留一个。然后一个进程进行写入操作,另一个进程进行读出操作,为了保证正确写入,我们使用sleep函数进行手动强制等待。但是这个方法局限性太大,不知道具体的执行时间,所以这里我们也可以使用进程等待进行优化(将sleep(1)换成wait(NULL)即可)。父子间能直接进行通信的本质是使用了公共的资源-文件描述符,然后通过文件来进行数据的通信。
匿名管道
--父子进程间的通信管道
匿名管道用于进程间通信,且仅限于本地关联进程之间的通信,通常也就是所谓的父子。
进程间通信的本质就是,让不同的进程看到同一份资源,使用匿名管道实现父子进程间通信的原理就是,让父子进程先看到一份被打开的文件资源,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
这里,父子进程看到的同一份文件资源是由操作系统维护的,所以父子进程对该文件进行写入操作时,该文件缓冲区当中的数据并不会进行写时拷贝。
管道虽然用的是文件的方案,但操作系统一定不会把进程进行通信的数据刷新到磁盘当中,因为这样做有IO参与会降低效率,而且也没有必要。也就是说,这种文件是一批不会把数据写到磁盘当中的文件,换句话说,磁盘文件和内存文件不一定是一一对应的,有些文件只会在内存当中存在,而不会在磁盘当中存在。
创建匿名管道:int pipe(int pipefd[2]);
int main(int argc, char* argv[])
{
int fd[2]; pipe(fd);
int fd_w=fd[1];//写端
int fd_r=fd[0];//读端
pid_t fpid=fork();//创建子进程
if(fpid<0)exit(-1);//创建子进程失败
else if(fpid==0){//子进程
close(fd_r);//关闭读端
// char buf_w[1024];//写入缓冲区
// memset(buf_w,'\0',sizeof(buf_w));
char *str="hello world";
int ret = write(fd_w,str,strlen(str));
if(ret<0)perror("write err");
}
else if(fpid>0){//父进程
close(fd_w);//关闭写端
char buf_r[1024];//读出缓冲区
memset(buf_r,'\0',sizeof(buf_r));
int cnt=0;
sleep(1);//等待子进程写入
while( cnt = read(fd_r,buf_r,sizeof(buf_r)) ){
if(cnt<0)perror("read err");
else printf("sucess: %s\n",buf_r);
}
}
return 0;
}
我们发现,这种方式和上面的文件通信的方式非常的像完全可以说是一个模子里刻出来的。是的,管道的本质也是文件通信,只不过二者有着本质的区别:管道文件实际上不是一个磁盘文件,是一个内存文件。
从管道写端写入的数据会被存到内核缓冲,直到从管道的读端被读取。而管道是有大小的,这就意味着,管道可以被存满。①如果管道已满,写端进程会阻塞在write上,读端read返回成功时才可以继续write写入。②如果管道没满(存在数据),write写入,read读出正常进行。③管道中不存在数据,也就是read先于write执行,那么read时会阻塞。
练习:使用管道实现ls / | wc -l
int main(){
int pipefd[2];
int ret = pipe(pipefd);
if(ret<0)perror("create pipe err");
pid_t fpid=fork();
if(fpid<0)perror("fork err");
else if(fpid==0){//子进程
close(pipefd[0]);
dup2(pipefd[1],STDOUT_FILENO);//将管道的写端 重定向到输出端
execlp("ls","ls","/",NULL);
}
else if(fpid>0){//父进程
close(pipefd[1]);
dup2(pipefd[0],STDIN_FILENO);//将管道的读端 重定向到输入端
execlp("wc","wc","-l",NULL);
}
}
命名管道
匿名管道只能用于具有亲缘关系的进程之间的通信。通常,一个管道由一个进程创建,然后该进程调用fork,此后父子进程之间就可应用该管道。
如果要实现两个毫不相干的进程之间的通信,可以使用民命管道来做到。命名管道就是一种特殊类型的文件,两个进程通过命名管道的文件名打开同一管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。
命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的映像,也是因此使得两个不同的进程都可以找到这个管道,但是这个映像的大小永远为0,因为命名管道和匿名管道一样,不会将通信数据刷新到磁盘中。
创建命名管道文件:使用mkfifo命令。
然后创建两个.c文件,执行其中一个达到写入的目的,执行另一个达到读出的目的。
//write:写入端
int main(int argc, char* argv[])
{
int fd=open("./fifo",O_WRONLY);
if(fd<0)perror("open err");
char *str="hello world";
write(fd,str,strlen(str));
return 0;
}
//read:读取端
int main(int argc, char* argv[])
{
int fd = open("./fifo", O_RDONLY);
if (fd < 0)perror("open err");
char buf[1024];int cnt=0;
memset(buf,'\0',sizeof(buf));
while(cnt=read(fd,buf,sizeof(buf))){
if(buf<0)perror("read err");
else printf("%s",buf);
}
printf("\n");
return 0;
}
执行时的注意点:
1.读进程打开FIFO,没有打开写进程时:
如果文件属性有O_NONBLOCK,返回成功,否则阻塞直到有写进程打开FIFO
2.写进程打开FIFO,没有打开读进程时:
如果文件属性有O_NONBLOCK,返回失败,否则阻塞直到有读进程打开FIFO
3.正常情况:
先打开写进程,此时处于第二种情况下的阻塞情景
后打开读进程,此时写进程返回成功,读进程读取全部内容后返回成功
练习:两个程序使用fifo互发消息聊天
阶段1.单向发送和接收固定的一条消息
//send.c:发送端
int main(int argc, char* argv[])
{
int fd=open("./fifo",O_WRONLY);
if(fd<0)perror("open err");
char *str="hello world";
write(fd,str,strlen(str));
return 0;
}
//receive.c:接收端
int main(int argc, char* argv[])
{
int fd = open("./fifo", O_RDONLY);
if (fd < 0)perror("open err");
char buf[1024];int cnt=0;
memset(buf,'\0',sizeof(buf));
while(cnt=read(fd,buf,sizeof(buf))){
if(buf<0)perror("read err");
else printf("%s",buf);
}
printf("\n");
return 0;
}
阶段2.单向发送和接收终端输入的一条消息
//send:发送端
int main(int argc, char* argv[])
{
int fd_send=open("./fifo",O_WRONLY);
if(fd_send < 0)perror("open err");
char send_msg[1024];
memset(send_msg,'\0',sizeof(send_msg));
int count = read(STDIN_FILENO,send_msg,sizeof(send_msg));
write(fd_send,send_msg,count);
return 0;
}
//receive:接收端
int main(int argc, char* argv[])
{
int fd_receive = open("./fifo", O_RDONLY);
if (fd_receive < 0)perror("open err");
char buf[1024];int cnt=0;
memset(buf,'\0',sizeof(buf));
while(cnt=read(fd_receive,buf,sizeof(buf))){
if(buf<0)perror("read err");
else printf("%s",buf);
}
return 0;
}
阶段3.单向循环发送和接收终端输入的消息
//send:发送端
int main(int argc, char* argv[])
{
int fd_send=open("./fifo",O_WRONLY);
if(fd_send<0)perror("open err");
while(1){
char send_msg[1024];int cnt=0;
memset(send_msg,'\0',sizeof(send_msg));
cnt = read(STDIN_FILENO,send_msg,sizeof(send_msg));
write(fd_send,send_msg,cnt);
}
return 0;
}
//receive:接收端
int main(int argc, char* argv[])
{
int fd_receive = open("./fifo", O_RDONLY);
if (fd_receive < 0)perror("open err");
while(1){
char receive_msg[1024];int cnt=0;
memset(receive_msg,'\0',sizeof(receive_msg));
while(cnt=read(fd_receive,receive_msg,sizeof(receive_msg))){
if(receive_msg < 0)perror("read err");
else write(STDOUT_FILENO,receive_msg,cnt);
}
}
return 0;
}
阶段4.双向循环发送和接收终端输入的消息
int main(int argc, char* argv[])
{
//send端: (如果是receive端,打开的管道交换一下即可)
int fd_send=open("./fifo_1to2",O_WRONLY);if(fd_send<0)perror("open err");
int fd_receive = open("./fifo_2to1", O_RDONLY);if (fd_receive < 0)perror("open err");
pid_t fpid=fork();if(fpid<0)perror("fork err");
else if(fpid==0){//子进程--发送
close(fd_receive);//只发送不接收
while(1){
char send_msg[1024];int cnt=0;
memset(send_msg,'\0',sizeof(send_msg));
cnt = read(STDIN_FILENO,send_msg,sizeof(send_msg));
write(fd_send,send_msg,cnt);
}
close(fd_send);//完事结束发送端
}
else if(fpid>0){//父进程--接收
close(fd_send);//只接受不发送
while (1){
char receive_msg[1024];int cnt = 0;
memset(receive_msg, '\0', sizeof(receive_msg));
while (cnt = read(fd_receive, receive_msg, sizeof(receive_msg))){
if (receive_msg < 0)perror("read err");
else write(STDOUT_FILENO, receive_msg, cnt);
}
}
close(fd_receive);//完事关闭接收端
}
return 0;
}
感谢大家!!!