当前位置:   article > 正文

Linux快速入门之 管道(11)_close关闭管道会删除文件描述符吗

close关闭管道会删除文件描述符吗

管道

管道是进程间通信的一种方式,管道的本质是在内核中的一块内存(也叫内核缓冲区),这块缓冲区中的数据存储在一个环形队列中,由于管道在内核区,我们不能直接对其操作;

在这里插入图片描述

管道是通过队列维护,管道中数据具有以下特点:

  • 管道对应的内核缓冲区大小是固定的,默认4k

  • 管道分为两部分:读端写端(队列的两端),数据从写端进入管道,从读端流出管道

  • 管道中的数据只能读一次,做一次读操作之后数据就没了**(读数据相当于出队列);**

  • 管道是单向的,数据只能单向流动,数据从写端流向读端;

  • 对管道的操作 (读、写)默认是阻塞的

    1. 读管道:管道中没有数据,读操作被阻塞,当管道中有数据后阻塞才能被解除;
       2. 写管道:管道被写满了,写操作被阻塞 ,当管道变为不满的状态,写阻塞解除;
    
    • 1
    • 2

管道在内核区不能直接对其操作,如何使用?

管道操作本质就是文件IO操作内核中管道两端分别对应两个文件描述符,通过写端的文件描述符把数据写入管道中,通过读端的文件描述符将数据从管道中读出来,读写管道的函数就是Linux中的文件IO函数;

// 读管道
ssize_t read(int fd, void *buf, size_t count);
// 写管道的函数
ssize_t write(int fd, const void *buf, size_t count);
  • 1
  • 2
  • 3
  • 4

在这里插入图片描述

小结:管道独立于任何进程,并充当了两个进程用于数据通信的载体,只要两个进程能够得到同一个管道的读写文件描述符,那么他们之间就可以操作管道进行数据交互;

1.1 匿名管道

1.1.1 创建匿名管道

匿名管道是管道的一种,匿名也就说明该管道没有名字,但是其本质依然是内核中的一块内存具有上述管道的全部特性。但是, 匿名管道只能实现有血缘关系的进程间通信血缘关系进程:父子进程,兄弟进程、爷孙进程、叔侄进程。

//函数原型
#include <unistd.h>
//创建一个匿名管道,得到两个文件描述符
int pipe(int pipefd[2]);
  • 1
  • 2
  • 3
  • 4

参数: 传出参数,需要传递一个整形数组的地址,数组大小为2

pipefd[0] : 对应管道读端的文件描述符, 通过它可以将数据从管道中读出;

pipefd[1] : 对应管道写端的文件描述符, 通过它可以将数据写入到管道中;

返回值: 成功返回 0 ; 失败返回 -1;

1.1.2 进程间通信

使用匿名管道只能实现有血缘关系的进程间通信

==需求描述:==

在父进程中创建一个子线程,父子进程分别执行不同的操作;

子进程执行一个shell命令 “ ps aux” ,将命令结果传递给父进程;

父进程将子进程命令的结果输出到终端;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

需求分析:

  • 子进程中执行shell命令相当于启动一个磁盘程序,需要使用execl / execlp函数

execlp("ps","ps","aux",NULL);

  • 子进程执行完shell命令直接就在终端输出结果,如何将这些信息传递给父进程?

    1. 数据传递需要使用管道, 子进程需要将数据写入到管道;

    2. 将默认输出到终端的数据写入到管道就需要进行输出的重定向,需要使用dup2()函数

      dup2(fd[1],STDOUT_FILENO);

  • 父进程需要读管道,将从管道中读出的数据打印到终端;

  • 父进程最后需要释放子进程资源 ,防止出现僵尸进程;

注意:

在使用管道进行进程间通信需要注意:必须保证数据在管道中的单向流动;

第一步: 在父进程中创建匿名管道,得到了两个分配的文件描述符,fd3操作管道的读端,fd4操作管道的写端;

在这里插入图片描述

第二步: 父进程创建子进程,父进程的文件描述符被拷贝,在子进程的文件描述符表中可得到被分配的可使用的文件描述符,通过fd3读管道 ,fd4写管道。通过下图可以看到管道中数据的流动不是单向的,有以下几种情况:

  1. 父进程通过fd4将数据写入管道,然后父进程再通过fd3将数据从管道中读出;
  2. 父进程通过fd4将数据写入管道,然后子进程再通过fd3将数据从管道中读出;
  3. 子进程通过fd4将数据写入管道,然后子进程再通过fd3将数据从管道中读出;
  4. 子进程通过fd4将数据写入管道,然后父进程再通过fd3将数据从管道中读出;

假设子进程通过写端将数据写入管道,父进程的读端将数据读出,这样子进程的读端就读不到数据,导致子进程阻塞在读管道的操作上;这样就会给程序的执行造成一些不必要的影响。如果我们本来也没有打算让进程读或者写管道,那么就可以将进程操作的读端或者写端关闭。

在这里插入图片描述

第三步: 为了避免两个进程都读管道,但是可能其中某个进程由于读不到数据而阻塞的情况,我们可以关闭进程中用不到的那一端的文件描述符,这样数据就只能单向的从一端流向另外一端了,如下图,我们关闭了父进程的写端,关闭了子进程的读端:

在这里插入图片描述

根据上面的分析,最终可以写出下面的代码:

// 管道的数据是单向流动的:
// 操作管道的是两个进程, 进程A读管道, 需要关闭管道的写端, 进程B写管道, 需要关闭管道的读端
// 如果不做上述的操作, 会对程序的结果造成一些影响, 对管道的操作无法结束
#include <fcntl.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>

int main()
{
    // 1. 创建匿名管道, 得到两个文件描述符
    int fd[2];
    int ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe");
        exit(0);
    }
    // 2. 创建子进程 -> 能够操作管道的文件描述符被复制到子进程中
    pid_t pid = fork();
    if(pid == 0)
    {
        // 关闭读端
        close(fd[0]);
        // 3. 在子进程中执行 execlp("ps", "ps", "aux", NULL);
        // 在子进程中完成输出的重定向, 原来输出到终端现在要写管道
        // 进程打印数据默认输出到终端, 终端对应的文件描述符: stdout_fileno
        // 标准输出 重定向到 管道的写端      
        execlp("ps", "ps", "aux", NULL);
        dup2(fd[1], STDOUT_FILENO);
        perror("execlp");
    }

    // 4. 父进程读管道
    else if(pid > 0)
    {
        // 关闭管道的写端
        close(fd[1]);
        // 5. 父进程打印读到的数据信息
        char buf[4096];
        // 读管道
        // 如果管道中没有数据, read会阻塞
        // 有数据之后, read解除阻塞, 直接读数据
        // 需要循环读数据, 管道是有容量的, 写满之后就不写了
        // 数据被读走之后, 继续写管道, 那么就需要再继续读数据
        while(1)
        {
            memset(buf, 0, sizeof(buf));
            int len = read(fd[0], buf, sizeof(buf));
            if(len == 0)
            {
                // 管道的写端关闭了, 如果管道中没有数据, 管道读端不会阻塞
                // 没数据直接返回0, 如果有数据, 将数据读出, 数据读完之后返回0
                break;
            }
            printf("%s, len = %d\n", buf, len);
        }
        close(fd[0]);

        // 回收子进程资源
        wait(NULL);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

1.2 有名管道

1.2.1 创建有名管道

有名管道同样具有管道所有特性 ,因为在磁盘上有实体文件所以称为有名管道,文件类型为 p ,有名管道文件大小为0,因为有名管道也是将数据储存在内存的缓冲区中,打开这个磁盘上的管道文件就得到可以操作有名管道的文件描述符,通过文件描述符读写管道存储在内核中的数据;

有名管道也称为fifo使用有名管道既可以进行有血缘关系的进程间通信,也可以进行没有血缘关系的进程间通信创建有名管道的方式有两种,一种通过命令,一种通过函数;

  • 通过命令:
$ mkfifo   有名管道的名字
  • 1
  • 通过函数
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname , mode_t mode);
  • 1
  • 2
  • 3

参数:

pathname: 要创建的有名管道的名字;

==mode :==文件的操作权限,和open()函数第三个参数作用一样(权限:mode &~umask)

==返回值:==创建成功返回 0 ; 失败返回 -1;

2.2.2 进程间通信

使用有名管道实现进程通信,原理在两个进程中分别以读、写的方式打开磁盘上的管道文件,得到用于读管道、写管道的文件描述符, 就可以调用对应的read()、write()函数进行读写操作。

注意:

有名管道操作需要通过open()函数得到读写管道的文件描述符,如果只是读端打开了或只是写端打开了,进程会阻塞不会向下执行,直到在另一个进程中将管道的对端打开,当前进程的阻塞也就解除了。所以发现进程阻塞在open()函数上不要惊讶;

  1. 写管道的进行:
/*
1. 创建有名管道  mkfifo()
2. 打开有名管道文件 , 打开方式 O_WRONLY              int wfd = open("xx",O_ERONLY);
3. 调用write函数写文件 -> 数据被写入管道中                write(wfd ,data ,strlen(data));
4. 写完之后关闭文件描述符                                            close(wfd);
*/     
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main()
{
    // 1. 创建有名管道文件
    int ret = mkfifo("./testfifo", 0664);
    if(ret == -1)
    {
        perror("mkfifo");
        exit(0);
    }
    printf("管道文件创建成功...\n");

    // 2. 打开管道文件
    // 因为要写管道, 所有打开方式, 应该指定为 O_WRONLY
    // 如果先打开写端, 读端还没有打开, open函数会阻塞, 当读端也打开之后, open解除阻塞
    int wfd = open("./testfifo", O_WRONLY);
    if(wfd == -1)
    {
        perror("open");
        exit(0);
    }
    printf("以只写的方式打开文件成功...\n");

    // 3. 循环写管道
    int i = 0;
    while(i<100)
    {
        char buf[1024];
        sprintf(buf, "hello, fifo, 我在写管道...%d\n", i);
        write(wfd, buf, strlen(buf));
        i++;
        sleep(1);
    }
    close(wfd);

    return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  1. 读管道的进程
/*
	1. 这两个进程需要操作相同的管道文件
	2. 打开有名管道文件, 打开方式是 o_rdonly
		int rfd = open("xx", O_RDONLY);
	3. 调用read函数读文件 ==> 读管道中的数据
		char buf[4096];
		read(rfd, buf, sizeof(buf));
	4. 读完之后关闭文件描述符
		close(rfd);
*/
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>

int main()
{
 
    //打开有名管道文件
    int rfd = open("./testfifo",O_RDONLY);
    if (rfd<0)
    {
        perror("open");
        exit(0);
    }
    printf("以只读的方式打开文件\n");

    //循环读管道
    while (1)
    {
        char buf[4096];
        memset(buf,0,sizeof(buf));
        //读是阻塞的,当文件中没有数据,read阻塞,有数据解除阻塞
        int len = read(rfd,buf,sizeof(buf));
        printf("读出的数据:%s \n",buf);
        if (len ==0)
        {
            printf("管道已经关闭....\n");
            break;
        }
    }
     close(rfd);;
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38

1.3.管道的读写行为

无论是有名管道还是匿名管道,在进行读写的时候,表现的行为是一致的;

总结:

读管道需要根据写端的状态进行分析:

  • 写端没有关闭(操作管道写端的文件描述符没有被关闭)
  1. 如果管道中没有数据->(读阻塞),如果管道中写入数据,阻塞解除;
  2. 如果管道中有数据->(不阻塞),管道中的数据被读完了,再继续读管道还会阻塞;

  • 写端已经关闭(没有可用的文件描述符可以写管道)
  1. 管道中没有数据 ->读端解除阻塞 ,read函数返回0;
  2. 管道中有数据 ->read 先将数据读出,数据读完之后返回0,不会阻塞;

写管道,需要根据读端的状态进行分析:

  • 读端没有关闭

    如果管道有存储的空间,一直写数据

    如果管道写满了,写操作就阻塞,当读端将管道数据读走了,解除阻塞继续写

  • 读端关闭了,管道破裂 (异常), 进程直接退出

管道的两端默认是阻塞的,如何将管道设置为非阻塞呢?管道的读写两端的非阻塞操作是相同的,下面的代码中将匿名的读端设置为了非阻塞:

// 通过fcntl 修改就可以, 一般情况下不建议修改
// 管道操作对应两个文件描述符, 分别是管道的读端 和 写端

// 1. 获取读端的文件描述符的flag属性
int flag = fcntl(fd[0], F_GETFL);
// 2. 添加非阻塞属性到 flag中
flag |= O_NONBLOCK;
// 3. 将新的flag属性设置给读端的文件描述符
fcntl(fd[0], F_SETFL, flag);
// 4. 非阻塞读管道
char buf[4096];
read(fd[0], buf, sizeof(buf));

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/212062
推荐阅读
相关标签
  

闽ICP备14008679号