• fork()+pipe() --> 父子进程间通过管道通信 Linux系统编程pipe()


    fork()+pipe() --> 父子进程间通过管道通信

    1.fork()函数:创建新进程

    头文件:#include <unistd.h>

        #include<sys/types.h>

    函数原型:pid_t fork( void);

    返回值:一个是子进程返回0,第二个是父进程的返回值大于0,错误返回-1

    功能:创建一个新的进程。(pid_t 是一个宏定义,其实质是int 被定义在#include<sys/types.h>中)

    来看一下fork之后,发生了什么事情。

    由fork创建的新进程被称为子进程(child process)。该函数被调用一次,但返回两次。两次返回的区别是子进程的返回值是0,而父进程的返回值则是新进程(子进程)的进程 id。将子进程id返回给父进程的理由是:因为一个进程的子进程可以多于一个,没有一个函数使一个进程可以获得其所有子进程的进程id。对子进程来说,之所以fork返回0给它,是因为它随时可以调用getpid()来获取自己的pid;也可以调用getppid()来获取父进程的id。(进程id为0的总是由交换进程使用,所以一个子进程的进程id不可能为0)。

    fork之后,操作系统会复制一个与父进程完全相同的子进程。虽说是父子关系,但是在操作系统看来,他们更像兄弟关系,这2个进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝,指令指针也完全相同,子进程拥有父进程当前运行到的位置(两进程的程序计数器pc值相同。也就是说,子进程是从fork返回处开始执行的),但有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,如果fork不成功,父进程会返回错误。

    可以这样想象,2个进程一直同时运行,而且步调一致,在fork之后,他们分别作不同的工作,也就是分岔了。

    注意:fork()函数主要是以父进程为蓝本复制一个进程,其ID号和父进程的ID号不同。对于结果fork出来的子进程的父进程ID号是执行fork()函数的进程的ID号;

    例如:

    父进程, fork返回值是:17025, ID:17024 ,父进程ID:16879

    子进程, fork返回值是:0, ID:17025 ,父进程ID:17024

    使用管道有一些限制:
    两个进程通过一个管道只能实现单向通信,如果有时候也需要子进程写父进程读,就必须另开一个管道。
    管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。也就是需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。

    使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
    1. 如果所有指向管道写端的文件描述符都关闭了(管道写端的引用计数等于0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
    2. 如果有指向管道写端的文件描述符没关闭(管道写端的引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
    3. 如果所有指向管道读端的文件描述符都关闭了(管道读端的引用计数等于0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。
    4. 如果有指向管道读端的文件描述符没关闭(管道读端的引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

    2.pipe()函数:建立管道

    头文件: #include<unistd.h>
    函数原型:int pipe(int filedes[2]);
    函数说明:pipe()会建立管道,并将文件描述词由参数filedes数组返回。
    filedes[0]为管道里的读取端
    filedes[1]则为管道的写入端。
    返回值: 若成功则返回零,否则返回-1,错误原因存于errno中。

    错误代码:

    EMFILE 进程已用完文件描述词最大量
    ENFILE 系统已无文件描述词可用。
    EFAULT 参数 filedes 数组地址不合法。

    调用pipe函数时在内核中开辟一块缓冲区(称为管道)用于通信,它有一个读端一个写端,然后通过filedes参数传出给用户程序两个文件描述 符,filedes[0]指向管道的读端,filedes[1]指向管道的写端(很好记,就像0是标准输入1是标准输出一样)。所以管道在用户程序看起来 就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]);向这个文件读写数据其实是在读写内核缓冲区。

    注意:int pipe(int filedes[2]) 中的两个文件描述符被强制规定filedes[0]只能指向管道的读端,如果进行写操作就会出现错误;同理filedes[1]只能指向管道的写端,如果进行读操作就会出现错误。

    3.父子进程通过管道通信

    每个进程各自有不同的用户地址空间,任何一个进程的全局变量在另一个进程中都看不到,所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。

     

    开辟了管道之后如何实现两个进程间的通信呢?比如可以按下面的步骤通信。

     

    1. 父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
    2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
    3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。

    之前一直没明白为什么在这里父进程要关闭管道读端,并且子进程要关闭管道写端,想了很久终于想通了...,原因如下:因为上面的这个程序是要模拟父进程和子进程的管道读写操作,其中父进程用于向管道中写入数据,子进程用于向管道中读取数据,因此开始要关闭父进程的读文件描述符filedes[0], 以及关闭子进程的写文件描符filedes[1],这是为了模拟这个过程。

    至于为什么父进程关闭管道的读文件描述符filedes[0]后子进程还能读取管道的数据,是因为系统维护的是一个文件的文件描述符表的计数,父子进程都各自有指向相同文件的文件描述符,当关闭一个文件描述符时,相应计数减一,当这个计数减到0时,文件就被关闭,因此虽然父进程关闭了其文件描述符filedes[0],但是这个文件的文件描述符计数还没等于0,所以子进程还可以读取。也可以这么理解,父进程和子进程都有各自的文件描述符,因此虽然父进程中关闭了filedes[0],但是对子进程中的filedes[0]没有影响。

    文件表中的每一项都会维护一个引用计数,标识该表项被多少个文件描述符(fd)引用,在引用计数为0的时候,表项才会被删除。所以调用close(fd)关闭子进程的文件描述符,只会减少引用计数,但是不会使文件表项被清除,所以父进程依旧可以访问。

    最后需要注意,在linux的pipe管道下,在写端进行写数据时,不需要关闭读端的缓冲文件(即不需要读端的文件描述符计数为0),但是在读端进行读数据时必须先关闭写端的缓冲文件(即写端的文件描述符计数为0)然后才能读取数据。

    简单例子:

    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<unistd.h>
    #include<string.h>

    int main(void){
      pid_t pid;
      int i=0;
      int result = -1;
      int fd[2],nbytes;
      char string[100];
      char readbuffer[80];
      int *write_fd = &fd[1];
      int *read_fd = &fd[0];
      printf("Please input data:");
      scanf("%s",string);
      result = pipe(fd);
      if(-1 == result)
      {
        perror("pipe");
      return -1;
      }

      pid=fork();
      if(-1 == pid) //此处为了验证父子进程是否创建成功,如果未创建成功,则返回-1
      {
        perror("fork");
        return -1;
      }
      else if(0 == pid)
      {
        printf(“this is child %d ”, getpid());
        close(*read_fd);
        result = write(*write_fd,string,strlen(string));
        return 0;
      }
      else
      {
        printf(“this is parent %d ”, getpid());
        close(*write_fd);
        nbytes = read(*read_fd,readbuffer,sizeof(readbuffer)-1);
        printf("receive %d data:%s ",nbytes,readbuffer);
      }
      return 0;
    }
    执行结果:

     

    Linux系统编程pipe()

    管道的概述

    管道也叫无名管道,它是是 UNIX 系统 IPC(进程间通信) 的最古老形式,所有的 UNIX 系统都支持这种通信机制。

    无名管道有如下特点:

    1、半双工,数据在同一时刻只能在一个方向上流动。
    2、数据只能从管道的一端写入,从另一端读出。
    3、写入管道中的数据遵循先入先出的规则。
    4、管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
    5、管道不是普通的文件,不属于某个文件系统,其只存在于内存中。
    6、管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
    7、从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。
    8、管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用 (非常重点)。

    对于无名管道特点的理解,我们可以类比现实生活中管子,管子的一端塞东西,管子的另一端取东西。
    无名管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。
    在这里插入图片描述

    管道的操作

    头文件:

    #include <unistd.h>
    int pipe(int filedes[2]);

    功能:

    创建无名管道。

    参数:

    filedes: 为 int 型数组的首地址,其存放了管道的文件描述符 filedes[0]、filedes[1]。

    当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O 的函数都可以用来操作管道( lseek() 除外)。

    返回值:

    成功:0

    失败:-1

    下面我们写这个一个例子,子进程通过无名管道给父进程传递一个字符串数据:

    
    #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 *argv[])
    {
    	int fd_pipe[2] = {0};
    	pid_t pid;
    	if( pipe(fd_pipe) < 0 ){// 创建无名管道
    		perror("pipe");
    	}
    
    	pid = fork(); // 创建进程
    	if( pid < 0 ){ // 出错
    		perror("fork");
    		exit(-1);
    	}
    
    	if( pid == 0 ){ // 子进程
    		char buf[] = "I am mike";
    		// 往管道写端写数据
    		write(fd_pipe[1], buf, strlen(buf));
    		_exit(0);
    	}else if( pid > 0){// 父进程
    		wait(NULL);	// 等待子进程结束,回收其资源
    		char str[50] = {0};
    		// 从管道里读数据
    		read(fd_pipe[0], str, sizeof(str));
    		printf("str=[%s]
    ", str); // 打印数据
    	}
    
    	return 0;
    
    }
    

    运行结果如下:
    在这里插入图片描述

    管道的特点

    每个管道只有一个页面作为缓冲区,该页面是按照环形缓冲区的方式来使用的。这种访问方式是典型的“生产者——消费者”模型。当“生产者”进程有大量的数据需要写时,而且每当写满一个页面就需要进行睡眠等待,等待“消费者”从管道中读走一些数据,为其腾出一些空间。相应的,如果管道中没有可读数据,“消费者” 进程就要睡眠等待,具体过程如下图所示:
    在这里插入图片描述

    默认的情况下,从管道中读写数据,最主要的特点就是阻塞问题(这一特点应该记住),当管道里没有数据,另一个进程默认用 read() 函数从管道中读数据是阻塞的。

    测试代码如下:

    
    #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 *argv[])
    {
    	int fd_pipe[2] = {0};
    	pid_t pid;
    	if( pipe(fd_pipe) < 0 ){// 创建无名管道
    		perror("pipe");
    	}
    
    	pid = fork(); // 创建进程
    	if( pid < 0 ){ // 出错
    		perror("fork");
    		exit(-1);
    	}
    
    	if( pid == 0 ){ // 子进程
    		_exit(0);
    	}else if( pid > 0){// 父进程
    		wait(NULL);	// 等待子进程结束,回收其资源
    		char str[50] = {0};
    		printf("before read
    ");
    		// 从管道里读数据,如果管道没有数据, read()会阻塞
    		read(fd_pipe[0], str, sizeof(str));
    		printf("after read
    ");
    		printf("str=[%s]
    ", str); // 打印数据
    	}
    	return 0;
    }
    

    运行结果:

    在这里插入图片描述

    程序堵塞,没有往下执行
    当然,我们编程时可通过 fcntl() 函数设置文件的阻塞特性。

    设置为阻塞:fcntl(fd, F_SETFL, 0);
    设置为非阻塞:fcntl(fd, F_SETFL, O_NONBLOCK);

    测试代码如下:

    
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <fcntl.h>
     
    int main(int argc, char *argv[])
    {
    	int fd_pipe[2] = {0};
    	pid_t pid;
    
    	if( pipe(fd_pipe) < 0 ){// 创建无名管道
    		perror("pipe");
    	}
    
    	pid = fork(); // 创建进程
    	if( pid < 0 ){ // 出错
    		perror("fork");
    		exit(-1);
    	}
    
    	if( pid == 0 ){ // 子进程
    		sleep(3);
    		char buf[] = "hello, mike";
    		write(fd_pipe[1], buf, strlen(buf)); // 写数据
    		_exit(0);
    
    	}else if( pid > 0){// 父进程
    		fcntl(fd_pipe[0], F_SETFL, O_NONBLOCK); // 非阻塞
    		//fcntl(fd_pipe[0], F_SETFL, 0); // 阻塞
    		while(1){
    			char str[50] = {0};
    			read( fd_pipe[0], str, sizeof(str) );//读数据
    			printf("str=[%s]
    ", str);
    			sleep(1);
    		}
    	}
    	return 0;
    
    }
    

    在这里插入图片描述

    默认的情况下,从管道中读写数据,还有如下特点(知道有这么回事就够了,不用刻意去记这些特点):
    1)调用 write() 函数向管道里写数据,当缓冲区已满时 write() 也会阻塞。

    测试代码如下:

    
    #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 *argv[])
    {
    	int fd_pipe[2] = {0};
    	pid_t pid;
    	char buf[1024] = {0};
    	memset(buf, 'a', sizeof(buf)); // 往管道写的内容
    	int i = 0;
    
    	if( pipe(fd_pipe) < 0 ){// 创建无名管道
    		perror("pipe");
    	}
    	pid = fork(); // 创建进程
    	if( pid < 0 ){ // 出错
    		perror("fork");
    		exit(-1);
    	}
    
    	if( pid == 0 ){ // 子进程
    		while(1){
    			write(fd_pipe[1], buf, sizeof(buf));
    			i++;
    			printf("i ======== %d
    ", i);
    		}
    		_exit(0);
    	}else if( pid > 0){// 父进程
    		wait(NULL);	// 等待子进程结束,回收其资源
    	}	
    
    	return 0;
    }
    

    上面的省略
    blog.csdnimg.cn/20190109190258351.png)

    2)通信过程中,别的进程先结束后,当前进程读端口关闭后,向管道内写数据时,write() 所在进程会(收到 SIGPIPE 信号)退出,收到 SIGPIPE 默认动作为中断当前进程。

    
    #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 *argv[])
    {
    	int fd_pipe[2] = {0};
    	pid_t pid;
    
    	if( pipe(fd_pipe) < 0 ){// 创建无名管道
    		perror("pipe");
    	}
    
    	pid = fork(); // 创建进程
    	if( pid < 0 ){ // 出错
    		perror("fork");
    		exit(-1);
    	}
    
    	if( pid == 0 ){ // 子进程
    		//close(fd_pipe[0]);
    		_exit(0);
    	}else if( pid > 0 ){// 父进程
    		wait(NULL);	// 等待子进程结束,回收其资源
    		close(fd_pipe[0]); // 当前进程读端口关闭
    		char buf[50] = "12345";
    		// 当前进程读端口关闭
    		// write()会收到 SIGPIPE 信号,默认动作为中断当前进程
    		write(fd_pipe[1], buf, strlen(buf));
    		while(1);	// 阻塞
    	}
    	return 0;
    }
    

    测试结果:什么都没输出,程序直接断掉。
    在这里插入图片描述


    ————————————————
    原文链接:https://blog.csdn.net/wumenglu1018/article/details/54019755

  • 相关阅读:
    阿里巴巴在线笔试题——第一象限坐标点中,原点处能看到的个数
    不改变元素的顺序的情况下,删除数组中重复的元素
    输入一个字符串,判断最后一个单词的长度
    纯虚函数与虚函数
    如何删除数组中重复的元素
    20180317细节收获
    静态数据变量
    cin的用法
    关于free和delete的使用
    关于malloc/free用法
  • 原文地址:https://www.cnblogs.com/cqx6388/p/15155356.html
Copyright © 2020-2023  润新知