进程间通信(一)管道

devtools/2024/10/25 6:16:03/

文章目录

  • 进程间通信
    • 进程间通信概述
    • 进程间通信的方式
      • 管道通信
        • 示例--基于管道的父子进程通信
        • 示例--使用管道进程兄弟进程通信
      • 管道的读写特性
        • 示例--不完整管道(读一个写端关闭的管道)
        • 示例--不完整管道(写一个读端关闭的管道)
      • 标准库中的管道操作
        • 示例--使用popen函数进行管道的读写
      • 命名管道(FIFO)的创建
        • 示例--使用FIFO进行进程间通信
      • 匿名管道和命名管道的异同

进程间通信

进程间通信概述

  • 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间(例如一个进程要将某一个计算结果发送给另外一个进程使用,这时候就会用到进程间通信)。
  • 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立即看到(例如就像线程中的全局变量一样,一个线程修改别的线程立马就能看到)。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件(例如子进程在结束时会产生一个SIGCHILD信号通知父进程去回收子进程的资源)。
  • 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供锁和同步机制(例如多个进程在操作同一个文件的时候就会涉及到资源共享的问题,需要靠文件锁等机制来实现进程之间的同步和互斥).
  • 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变(例如常用的gdb调试)

进程间通信的方式

  1. 匿名管道(pipe)和命名管道(FIFO)
  2. 信号(signal)
  3. 消息队列
  4. 共享内存
  5. 信号量(进程的信号量和线程信号量不一样)
  6. 套接字(socket)

管道通信

  • 管道是针对于本地计算机的两个进程之间的通信而设计的通信方法,管道建立后,实际获得两个文件描述符,一个用于读取一个用于写入。后续对管道的操作都可以像对普通文件一样使用readwrite函数对管道进行读取和写入。
  • 最常见的IPC机制,通过pipe系统调用
  • 管道是单工的,也就是说数据只能向一个方向流动,需要双工通信时,需要建立起两个管道。
  • 数据的读出和写入:一个进程向管道中写的内容被另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据(管道实际上就是位于内核中的一个缓冲区,当数据被从缓冲区读走以后,数据会从缓冲区移除,即管道中的数据读走即无)。

管道分类

  • 匿名管道

    • 通过系统调用pipe()函数创建管道
    • 在有血缘关系的进程中进行(例如父进程和子进程、兄弟进程之间
    • 管道位于内核空间,实质是一块缓存
  • 命名管道

    • 两个没有任何关系的进程之间通信可通过命名管道进行数据传输,本质是内核中的一块缓存,另外文件系统中以一个特殊的设备文件(管道文件,前边将文件系统的时候有讲过)存在。只要对管道文件进行读写就能够同步到内核空间的那片缓存中去。

    • 通过指令mkfifo或者系统调用mkfifo()函数创建

      image-20241011083824639

    匿名管道的创建

    #include <unistd.h>int pipe(int fd[2]);/*功能:创建一个管道,用于进程间的通信参数:一个数组,用于存储读端文件描述符和写端文件描述符返回值:成功执行返回0,失败返回-1fd[0]用于从管道中读取数据,fd[1]用于向管道中写入数据
    */
    

    image-20241011084806491

    管道的读写

    匿名管道主要用于有血缘关系的进程,尤其是父子进程之间的关系。父进程调用pipe()函数创建一个管道,然后调用fork()函数创建一个子进程。

    这里有两点需要注意:

    • 通过之前的章节可知当通过fork()函数创建子进程的时候,子进程会继承父进程的代码段、数据段、堆、栈等内容,所以当在父进程中定义了一个数组用于存储读端和写端的文件描述符的时候,由于定义的数组位于栈区,所以这个数组也会被子进程继承。而父子进程中的数组是相同的内容,它们指向了管道的读端和写端,所以父子进程可以通过管道进行通信。
    • 根据管道的特性可知,管道是单工的。所以即使这里父子进程都有读端和写端,数据也只能从一个方向进行传输。也就是说同一时间只能有父进程向管道中写入子进程从管道中读取或者子进程向管道中写入父进程从管道中读取,所以通过fork()函数创建子进程后父子进程必须关闭一端然后参能进行通信。如果想要同一时间进行双方通信,必须要建立两个管道。
示例–基于管道的父子进程通信
#include "header.h"int main(void)
{int pipe_fd[2];pid_t pid;//父进程通过pipe函数创建管道用于父子进程间通信if(pipe(pipe_fd) != 0){perror("pipe error");exit(EXIT_FAILURE);}if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}//父进程向管道中写入,关闭读端else if(pid > 0){int start = 1, end = 100;close(pipe_fd[0]);			//父进程关闭读端if(write(pipe_fd[1], &start, sizeof(int)) != sizeof(int)){perror("write error");exit(EXIT_FAILURE);}if(write(pipe_fd[1], &end, sizeof(int)) != sizeof(int)){perror("write error");exit(EXIT_FAILURE);}close(pipe_fd[1]);		//写入完成后关闭写端wait(NULL);			//等待子进程退出并回收它的资源}//子进程从管道中读取,关闭写端//父进程从尾部写入,子进程从头部读取else{int start, end;close(pipe_fd[1]);if(read(pipe_fd[0], &start, sizeof(int)) < 0){perror("read error");exit(EXIT_FAILURE);}if(read(pipe_fd[0], &end, sizeof(int)) < 0){perror("read error");exit(EXIT_FAILURE);}close(pipe_fd[0]);			//读取完成后关闭读端printf("child process read data start:%d end:%d\n",start,end);}return 0;
}

image-20241011092657080

示例–使用管道进程兄弟进程通信

在Linux系统中经常会用到grep这个指令,grep指令的作用是在文件中搜索文本或者字符串,并打印出匹配的行。它经常会配合别的指令一起使用,例如:cat /etc/passwd | grep root,这个指令的作用是查看passwd这个文件里边有关root的文本。catgrep是两个指令,前边有讲过在shell上执行的指令都属于这个shell的子进程,所以它其实相当于执行了两个进程,进程1使用cat指令来将passwd这个文件里边的内容全部输出出来,进程2使用grep指令根据这些内容过滤出有关root字样的文本。在中间有一个字符|表示的就是一个管道,因为cat指令默认是输出到标准输出(屏幕),而grep指令默认是从标准输入(键盘)中获取,所以这里加一个管道|,将cat指令执行的结果写入到管道里,然后grep指令从管道中读取并将与root字样有关的文本打印出来

#include "header.h"int main(void)
{int pipe_fd[2];int i = 0;pid_t pid;char *cmd1[] = {"cat", "/etc/passwd", NULL};char *cmd2[] = {"grep", "root", NULL};//父进程创建管道用于在兄弟进程中通信if(pipe(pipe_fd) < 0){perror("pipe error");	exit(EXIT_FAILURE);}	//父进程创建子进程for(; i < 2; i++){if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}else if(pid == 0){//子进程1用于获取cat指令执行的结果并写到管道中去if(i == 0){close(pipe_fd[0]);			//子进程1用于向管道中写入,关闭读端//将标准输出重定向到管道的写入端,将cat的内容写入到管道中去//重定向后标准输出就指向了管道的写入端if(dup2(pipe_fd[1], STDOUT_FILENO) != STDOUT_FILENO){perror("dup2 error");exit(EXIT_FAILURE);}close(pipe_fd[1]);			//此时标准输出的指向和之歌相同,所以关闭原来的文件描述符if(execvp(cmd1[0], cmd1) == -1){perror("execvp error");exit(EXIT_FAILURE);}				}//子进程2用于从管道中获取并过滤相应的关键字	if(i == 1){close(pipe_fd[1]);			//子进程2用于从管道中读取,关闭写端//grep指令默认会从标准输入中读取,所以要将标准输入重定向到管道的读端//然后利用grep过滤出来if(dup2(pipe_fd[0], STDIN_FILENO) != STDIN_FILENO){perror("dup2 error");exit(EXIT_FAILURE);}close(pipe_fd[0]);			//经过重定向后标准输入和管道的读端文件描述符指向相同,所以关闭原来的文件描述符if(execvp(cmd2[0], cmd2) == -1){perror("execvp error");exit(EXIT_FAILURE);}}break;}else {if(i == 1){//父进程只做创建管道和创建子进程的事情,不对管道进行操作//所以这里关闭父进程中的文件描述符close(pipe_fd[0]);close(pipe_fd[1]);//等待两个子进程退出并回收它的资源wait(NULL);wait(NULL);}}}	return 0;
}
cat /etc/passwd | grep root

image-20241012103740950

通过编译执行可以看到两个执行结果是相同的,在shell终端执行的命令|就等同于pipe()系统调用,通过管道配合grep指令可以过滤出用户所需要的文本。

代码中有几点需要注意:

  1. cat指令它默认是输出到标准输出里的,而grep指令它默认是从标准输入中去获取的,所以这里要使用dup2函数将标准输出重定向到管道的写端,将标准输入重定向到管道的读端。通过这个操作两个兄弟进程就能够通过管道进行通信。
  2. 在这个代码中,父进程的作用是建立管道和创建子进程,所以它并没有对管道进行操作。在父进程中就要把管道的读端和写端关闭,但是一定要等待创建子进程全部创建完成后才能关闭文件描述符,若子进程还没有创建完成就关闭文件描述符,那么后来的子进程它的文件描述符就是无效的。

管道的读写特性

  • 通过打开的两个管道来创建一个双向的管道(管道是单工通信的,若想要实现双方之间的通信,就要建立两个管道实现双方之间的收发)

  • 管道是阻塞性的,当进程从管道中读取数据,若没有数据进程会阻塞(和之前的普通文件不一样,若普通文件中没有数据,读取的时候会直接返回。若是一个进程读取一个没有数据的管道时它会阻塞知道管道中有数据写入它才会退出阻塞状态)

  • 当一个进程往管道中不断地写入数据但是没有进程去读取数据,此时只要管道没有满是可以的,但如果管道中放满数据则会报错。

  • 不完整管道

    • 完整性管道指的就是写端和读端都打开的管道(上边说的特性都是针对完整性管道的),而不完整管道就是读端或者写端中的任意一端被关闭的管道。
    • 当读一个写端已经被关闭的管道时,在所有数据被读取后,read返回0,以表示到达了文件尾部。
    • 如果写一个读端已经被关闭的管道,则产生信号SIGPIPE,如果忽略或者捕捉该信号并从处理程序中返回,则write返回-1,同时errno设置为EPIPE
    • 不完整管道的应用场景:后边的网络编程它是两个进程(客户端和服务器端)基于网络的通信,通信的方式类似于管道。当其中的某一方出现问题导致网络通信出现异常的时候,就可以像判断不完整管道的方式来判断网络通信是否出现了问题,然后基于此问题做相应的操作
示例–不完整管道(读一个写端关闭的管道)
#include "header.h"/**创建不完整管道:读一个写端关闭的管道*/int main(void)
{int pipe_fd[2];pid_t pid;if(pipe(pipe_fd) < 0){perror("pipe error");exit(EXIT_FAILURE);}if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}else if(pid > 0)		//父进程等待子进程写入,然后从管道中读取{sleep(2);			close(pipe_fd[1]);			//关闭管道的写端char c;while(1){if(read(pipe_fd[0], &c, 1) != 0){printf("%c",c);}else{printf("\nread the end of pipe\n");break;}}wait(NULL);}else			//子进程关闭读端,先向管道中写入数据后关闭写端 {close(pipe_fd[0]);char *s = "1234";if(write(pipe_fd[1], s, strlen(s)) != strlen(s))		//向管道中写入数据{perror("write error");exit(EXIT_FAILURE);}close(pipe_fd[1]);		//关闭管道的写端}return 0;}

image-20241014112337558

通过编译执行可以发现读一个写端已经被关闭的管道,当读到管道的末尾会返回0

示例–不完整管道(写一个读端关闭的管道)
#include "header.h"/**	创建不完整管道:写一个读端关闭的管道*/void sig_handler(int signum)
{if(signum == 13){printf("receive a signal is SIGPIPE\n");}
}int main(void)
{int pipe_fd[2];pid_t pid;//向内核注册信号和信号处理函数,如果发生信号就去执行相应的处理函数if(signal(SIGPIPE, sig_handler) == SIG_ERR){perror("signal error");}//父进程创建管道if(pipe(pipe_fd) < 0){perror("pipe error");exit(EXIT_FAILURE);}if((pid = fork()) < 0){perror("fork error");exit(EXIT_FAILURE);}else if(pid > 0)		//父进程等待子进程关闭读端然后再向管道中写入{sleep(2);			//睡眠2秒,保证子进程已经将读端关闭close(pipe_fd[0]);		//父进程关闭读端char *s = "1234";if(write(pipe_fd[1], s, strlen(s)) != strlen(s)){fprintf(stderr,"%s:%s\n",strerror(errno),(errno == EPIPE)?"EPIPE":"unknown");		//当向已经关闭读端的管道中写入的时候会产生SIGPIPE信号,同时errno被设置为EPIPE}close(pipe_fd[1]);		//写完后关闭写端wait(NULL);						}else					//子进程将读端和写端的管道都关闭{close(pipe_fd[0]);close(pipe_fd[1]);}return 0;
}

image-20241014112538968

通过编译执行可以看到当写一个读端被关闭的管道时会产生一个SIGPIPE信号,同时errno会被置为EPIPE

标准库中的管道操作

通过上边的案例可以看出基于匿名管道的进程间通信较为复杂,在标准库中有一个将管道的操作封装成一个函数popen()

标准库中的管道操作

#include <stdio.h>FILE *popen(const char *command, const char *mode);
int pclose(FILE *stream);/*功能:popen函数创建一个管道以启动新进程并与其进行通信参数:command		要执行的命令字符串,该字符串包含一个shell命令,	以NULL结尾mode		指向一个以NULL结尾的字符串的指针,该字符串必须是r(只读)或w(只写)之一stream		文件流指针返回值:popen()如果执行成功,popen返回一个文件流指针,如果失败返回
NULLpclose 如果执行成功返回传递给popen()的参数cmd指定的子进程的终止状态,如果失败,则pclose返回-1,并设置errno来指示错误
*/

popen函数的内部实现流程

popen函数的内部实现原理.drawio

示例–使用popen函数进行管道的读写
#include "header.h"int main(void)
{FILE *fp;	char buffer[128];//命令执行的结果放置在fp指向的结构体指针中//内部实现流程:子进程去执行popen传入的cmd,然后将执行的结果重定向到管道的写端//父进程然后使用标准库函数从管道中读取,然后将数据存放到fp所指向的缓存中去fp = popen("cat /etc/passwd | grep root", "r");if(fp == NULL){perror("fopen error");exit(EXIT_FAILURE);}memset(buffer, '\0', sizeof(buffer));		while(fgets(buffer, sizeof(buffer), fp)){printf("%s",buffer);}pclose(fp);printf("--------------------------------------\n");//父进程将内容写入到文件流指针所指向的缓存区域,然后popen函数会将内容写入到管道中,子进程会做一个将标准输入重定向到管道的读端的操作然后从管道中获取数据,最后根据cmd指令指向x应的fp = popen("wc -l", "w");fprintf(fp, "%s", "hello\nhaha\nlala\n");pclose(fp);return 0;
}

image-20241014202353042

通过编译执行可以发现这个函数的执行类似于system函数,但是system函数它的运行是直接输出到标准输出去的,并没有输入到管道。但是如果需要将指令的执行结果存放到某个地方的话,使用popen函数比较适合。

命名管道(FIFO)的创建

#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);/*功能:创建命名管道用于进程间的通信参数:pathname		要创建管道的路径mode				创建管道的权限返回值:若成功执行返回0,出错返回-1
*/
  • 命令mkfifo创建命名管道(命令内部调用mkfifo函数)
  • 命名管道和 匿名管道一样,实质上是内核中的一块缓存,但是命名管道会在文件系统中存在一种特殊的设备文件(管道文件)。用户进程对管道文件进行读写操作会直接同步到内核中的缓存。
  • 只要对FIFO有适当的权限,FIFO可用在任何两个没有血缘关系的进程之间通信,和匿名管道不一样,它通过系统调用pipe()函数创建只能用于有血缘关系的进程(父子进程和兄弟进程)。
  • 管道文件系统中只有一个索引块存放文件的路径,没有数据块,所有的数据存放在内核中。和之前的普通文件系统不一样,之前的文件有一个索引和数据块,数据就存放在数据块中。
  • 命名管道必须读和写同时打开,否则单独读或者单独写会引发阻塞
  • FIFO的操作与操作普通文件一样,使用mkfifo创建了一个FIFO,就可以用open打开它,一般的文件I/O函数(readwriteclose…)等都可用于FIFO
  • FIFO相关出错信息
    • EACCESS(无存取权限)
    • EEXIST(指定文件不存在)
    • ENAMETOOLONG(路径名太长)
    • ENOENT(包含的目录不存在)
    • ENOSPC(文件系统剩余空间不足)
    • ENOTDIR(文件路径无效)
    • EROFS(指定的文件只存在于只读文件系统中)
示例–使用FIFO进行进程间通信
//fifo_r.c#include "header.h"int main(int argc, char **argv)
{if(argc < 2){fprintf(stderr,"usage:%s pathname\n",argv[0]);exit(EXIT_FAILURE);}//创建管道,管道的权限为文件的拥有者和同组人有可读可写可执行的权限,其他人只有可读权限if(mkfifo(argv[1], S_IRWXU | S_IRWXG | S_IROTH) < 0){perror("mkfifo error");exit(EXIT_FAILURE);}int fd;char buffer[32];fd = open(argv[1], O_RDONLY);	//以只读的权限打开管道if(fd < 0){perror("open file error");exit(EXIT_FAILURE);}printf("open pipe read....\n");memset(buffer, '\0', sizeof(buffer));if(read(fd, buffer, sizeof(buffer)) < 0){perror("read error");exit(EXIT_FAILURE);}printf("%s\n",buffer);close(fd);				//操作完文件后将文件描述符关闭return 0;
}
//fifo_w.c#include "header.h"int main(int argc, char **argv)
{if(argc < 2){fprintf(stderr,"usage:%s pathname\n",argv[0]);exit(EXIT_FAILURE);}int fd = open(argv[1], O_WRONLY);char *str = "hello world";if(fd < 0){perror("open file error");exit(EXIT_FAILURE);}printf("open file write....\n");if(write(fd, str, strlen(str)) != strlen(str)){perror("write error");exit(EXIT_FAILURE);}close(fd);return 0;
}

image-20241015234610708

通过编译执行可以发现对于命名管道的操作实际上和操作普通文件无异,对于管道的来说它是阻塞性的,所以当管道里没有数据的时候读端会阻塞。而对于命名管道来说,当只有读端或者写端执行的时候它会阻塞,必须读端和写端都运行才能够从管道里获取数据。

匿名管道和命名管道的异同

  • 相同点

    • 作用相同:都用于不同进程间的通信

    • 数据流向:匿名管道创建两个管道后允许数据双向传输,命名管道也支持数据的双向传输

    • 缓冲区:都可以使用内核缓冲区来存储传输的数据,允许发送和接收过程之间的解耦。

  • 不同点

    • 创建与管理:
      • 命名管道:通过系统调用mkfifo()或者使用指令mkfifo创建,创建后会生成一个管道文件,后续都可以基于这个文件进行操作(和操作普通文件一样,使用open,read,write,close等API进行操作),后续使用完管道以后还需要使用close函数关闭文件描述符。
      • 匿名管道:通过系统调用pipe()函数创建,创建后会返回两个文件描述符,其中fd[0]表示管道的读端,fd[1]表示管道的写端。通过这两个文件描述符来实现基于匿名管道的进程间通信。使用完后会自动将匿名管道销毁。
    • 命名
      • 命名管道:由于创建命名管道后会在文件系统中存在一个特殊的设备文件(管道文件),所以叫做命名管道,它们的生命周期可以独立于创建它们的进程直到用户显式操作将其删除。
      • 匿名管道:没有名字
    • 使用范围:
      • 命名管道:可用于任何两个进程之间的通信,通常在不同的用户会话或系统的进程之间使用。
      • 匿名管道:只能用于有血缘关系的进程,例如兄弟进程和父子进程。

http://www.ppmy.cn/devtools/128614.html

相关文章

Java实现文件上传功能

目录 1、准备工作 2、注意事项 3、jsp页面代码 4、Servlet 5、注册Servlet 1、准备工作 导入依赖&#xff1a;commons-fileupload和commons-io 2、注意事项 ①为保证服务器安全&#xff0c;上传文件应该放在外界无法直接访问的目录下&#xff0c;比如WEB-INF目录下 ②为…

Lua变量

软考鸭微信小程序 过软考,来软考鸭! 提供软考免费软考讲解视频、题库、软考试题、软考模考、软考查分、软考咨询等服务 Lua是一种轻量级的脚本语言&#xff0c;以其简单、高效和易于嵌入的特性而广受欢迎。在Lua中&#xff0c;变量是存储数据的容器&#xff0c;可以存储不同类型…

基于KU115+ZU19EG+C6678 的高性能6U VPX 载板

基于KU115ZU19EGC6678 的高性能6U VPX 载板&#xff0c;板载 2 个 HPC 形式的FMC 连接器&#xff08;用于外部信号扩展&#xff09;。板卡选用了 1 片Xilinx 公司的Kintex UltraScale 系列 FPGA 家族中的XCKU115-2FLVA1517I 和 1 片 Zynq UltraScale MPSoC 家族的XCZU19EG-2FFV…

Spring 相关技术要点整理

以下是对 Bean 的作用域和生命周期的详细说明&#xff1a; 一、Bean 的作用域 singleton&#xff08;单例&#xff09;&#xff1a; 这是默认的作用域。在整个应用中&#xff0c;对于特定的 Bean 类型&#xff0c;只会创建一个实例。无论在应用的哪个地方获取该 Bean&#xff…

nginx中的HTTP 负载均衡

HTTP 负载均衡&#xff1a;如何实现多台服务器的高效分发 为了让流量均匀分配到两台或多台 HTTP 服务器上&#xff0c;我们可以通过 NGINX 的 upstream 代码块实现负载均衡。 方法 在 NGINX 的 HTTP 模块内使用 upstream 代码块对 HTTP 服务器实施负载均衡&#xff1a; upstr…

线性可分支持向量机的原理推导 转为拉格朗日函数式 公式解析

本文是将文章《线性可分支持向量机的原理推导》中的公式单独拿出来做一个详细的解析&#xff0c;便于初学者更好的理解。 公式 9-7 引入了拉格朗日乘子法&#xff0c;这是支持向量机&#xff08;SVM&#xff09;优化问题的重要步骤&#xff0c;目的是将原来的带有约束条件的优化…

修改huggingface的缓存目录以及镜像源

执行以下语句查看当前配置 huggingface-cli env默认输出应该如下 (py39-transformers) PS D:\py_project\transformers_demo> huggingface-cli envCopy-and-paste the text below in your GitHub issue.- huggingface_hub version: 0.26.1 - Platform: Windows-10-10.0.22…

文理学院数据库应用技术实验报告0

文理学院数据库应用技术实验报告0 实验内容 打开cmd,利用MySQL命令连接MySQL服务器。 mysql -u root -p查看当前MySQL服务实例使用的字符集(character)。 SHOW VARIABLES LIKE character_set_server;查看当前MySQL服务实例支持的字符序(collation)。 SHOW VARIABLES LIKE c…