纠结的龙 2021-02-12 09:54 采纳率: 33.3%
浏览 116
已采纳

操作系统, 如果一个进程试图向一个没有进程打开的(读取)管道写入会发生什么?

据了解管道(pipe)在写入之前会进行判断,必须满足 内存中有足够的空间可容纳所有要写入的数或内存没有被读程序锁定才能写入。

是否会有什么反馈, 系统会利用这些反馈知道什么信息和做出反应么?

 

希望有大佬详解,可追加

 

  • 写回答

3条回答 默认 最新

  • ProfSnail 2021-02-12 18:21
    关注

    兄弟,你提了一个好问题。这个问题看似简单,实际上需要牵扯到的内容比较丰富,我查阅了二十多页相关文献才敢给你做详细的解释。下面是我的解答。

    想要搞清楚“写入一个没有进程打开的管道”会发生什么。此外还需要搞清楚三件事情:首先需要了解管道是什么,其次了解管道是如何实现的,最后才能了解没有进程打开的管道会发生什么。由于在Windows系统中几乎看不到管道的相关东西,所以接下来的内容全部是在Linux系统中发生的。

    (一)管道是什么

    1. 简单而言,管道用于连接一个程序的输出和另一个程序的输入。对于Linux命令行而言,管道常见的用法是通过竖线将两个命令连接起来,将前一个命令的输出内容送给后一个命令的输入。例如:echo "Hello world" | od -c将"Hello World"作为od命令的输入,输出。
    2. 0000000   h   a   h   a   h  \n
      0000006
      
    3. 由于在Linux中,万物都可以视作为特殊的文件。对于一般程序而言,管道是一种可以从一头输入,另一头输出的管道文件。基于管道实现的方法不同,可以将管道分为有名管道(named pipe)和无名管道(unnamed pipe)。

    4. 因此,“写入一个没有进程打开的管道”,指的是“写入一个没有读取进程打开的管道”。因为只要在写,那么这个管道的书写端就是打开的,只有可能读入端是关闭状态。

    (二)管道是如何实现的。

    (1)第一种方式是调用popen()函数获取管道的字符流指针。从popen()中读取外部程序的输出;或将输出送往popen()。

    popen的函数定义是:

    FILE* popen(const char *command, const char *open_mode)。

    popen打开程序当前路径下的shell,并执行command命令,返回一个字符流指针。对于FILE *类型的指针,需要调用函数fwrite()和fread()以进行字符流的输入和输出。例如将命令行中获取当前文件列表的内容传递给一个C语言程序,你的程序中应该这样书写:

    FILE* read_fp = popen("ls -lf", "r");
    char buffer[1000];
    int chars_read = fread(buffer, sizeof(buffer), 999, read_fp);
    pclose(read_fp);

    这时候就生成了一个无名管道,我们虽然不知道这个管道叫什么名字,但是这个管道就已经实现了某个程序的输出传递到另一个程序的目的。此时,题主所问的“写入一个没有进程打开的管道”事件是不会发生的,因为管道是连同此程序一同产生的,只有进程打开的时候才会产生管道,不存在没有进程打开的管道这一说。从另一个角度考虑,"ls -lf"这条命令执行之后,就已经将内容输出到了read_fp对应的文件流指针中了,不管当前程序是否执行读取操作,都相当于打开了一个文本文档一样,文本文档的内容就是管道从其他程序获取的输入,并且该内容一直保存在内存中,直到程序结束内存被释放,或者程序中主动调用pclose(read_fp),将该内存空间关闭。

    第二种情况是,希望将本程序生成的内容通过管道输出到其他程序中,则需要这样做。

    char buffer[1000]="Hello, my bro~!"
    FILE* write_fp=popen("WrongProgram", "w");
    printf("666666\n");
    fwrite(buffer, sizeof(char), strlen(buffer), write_fp);
    pclose(write_fp);

     与刚才类似,需要通过一个FILE*的变量打开另一个程序,即使想要打开的程序不存在,这里的popen()也不会返回一个NULL,而是会继续创建一个文件符,至于有没有人调用它,popen并不在乎,输出的内容会放到管道中。

    (2)调用pipe()函数在不同程序间传递管道的文件描述符。

    pipe函数的函数原型是这样的:

    int pipe(int file_descriptor[2]);

    该函数的参数是由两个正数类型的文件描述符组成的数组的指针,该函数在数组中填写两个新的文件描述符后返回0,如果失败就会返回-1并设置errno表明失败的原因。操作文件描述符用到的是write()函数和read()函数。如果想要向管道中写,则对file_descriptor[1]对应的文件描述符进行写操作,如果从管道中读取文件,则需要向file_descriptor[0]对应的文件描述符进行读取操作。具体实现方式是:

    int write_num = write(file_pipes[1], some_data, strlen(some_data)); // 写操作

    int read_num = read(file_pipes[0], buffer, BUFSIZ); // 读取操作

    当一个管道被创建之后,file_pipes[0],file_pipes[1]的两个文件描述符都被同时打开,此时尚不会发生“写入一个没有进程打开的管道”,即使管道内的数据暂时没有被读出,数据依然会保持在管道所占用的内存空间中。但是文件描述符是可以关闭的,并且也可以创造文件描述符的副本内容。

    创造读取端的文件描述符的副本可以采用:

    int another_file_descriptor = dup(file_descriptors[0]);

    这时,anothrer_file_descriptor和file_descriptors[0]同时指向管道的读入位置,但是这时两个不同的文件描述符,且文件描述符都处于打开的状态。想要关闭文件描述符,则需要采用close(file_descriptors[0]);命令语句。只有当一个管道读取端文件描述符对应的所有副本内容全部被关闭时,才会出现题主所提问的“写入一个没有进程打开的管道”情况。这种情况下,向一个没有进程打开的管道中写数据,不会被写入这个管道中,会退出程序。

    (3)通过有名管道FIFO文件,实现进程之间的管道通信。

    在FIFO条件下,使用mkfifo(FIFO_NAME, 0777)创建一个管道文件,多个文件写入读出时候都需要经过这个FIFO文件。读取文件的时候采用open(const char *path, mode)打开文件。

    当向FIFO管道文件写内容的时候,可以有两种方式:

    第一种是open(const char *path, O_WRONLY),在这种情况下,open将会阻塞,直到有一个进程以读方式打开同一个FIFO文件为止。

    第二种是open(const char *path, O_WRONLY|O_NONBLOCK);这个函数调用总是会立刻返回,但是如果没有进程以读方式打开FIFO文件,open调用将返回一个错误-1并且FIFO也不会被打开。如果确实有一个进程以读方式打开FIFO文件,那么我们就可以通过他返回的文件描述符对这个FIFO文件进行写操作。

     

     

    ============================================

    2021/2/13更新。

    题主更新了关于问题的描述方法。下面解释一下在三种调用情况下,系统会发生什么。

    根据上文的解答,可以发现popen()方法打开的是文件流,对应的写入读出方法是fwrite(), fread()标准IO库函数下的函数方法。而pipe(), FIFO文件打开的是文件描述符,对应的写入读出方法是write(),read()底层系统调用。因此将pipe,FIFO合并解释。其实实质上,每一个文件流都和一个文件描述符相对应,只是操作上的形式不同而已。

    首先解释popen()的内容。我在本机的linux库中找到相应的定义代码。位置是/usr/include/stdio.h。popen()定义如下:

    #ifdef __USE_POSIX2
    /* Create a new stream connected to a pipe running the given command.
    
       This function is a possible cancellation point and therefore not
       marked with __THROW.  */
    extern FILE *popen (const char *__command, const char *__modes) __wur;
    
    /* Close a stream opened by popen and return the status of its child.
    
       This function is a possible cancellation point and therefore not
       marked with __THROW.  */
    extern int pclose (FILE *__stream);
    #endif

    大意是:popen(), pclose()是一对组合函数,打开shell并执行__command,modes选定为"r"或者"w"。Linux中规定,对于所有向缓冲区里写数据的标准IO函数来说,为数据分配空间和检查错误是程序员的责任。如果在运行过程中出现错误,Linux会改变errno的数值。errno数值的定义在我的机器中是这些目录。

    /usr/include/asm-generic/errno.h
    /usr/include/errno.h
    /usr/include/linux/errno.h
    /usr/include/x86_64-linux-gnu/bits/errno.h
    /usr/include/x86_64-linux-gnu/sys/errno.h
    /usr/include/x86_64-linux-gnu/asm/errno.h

    选取其中一个目录中的部分定义展示如下:

    #define    ENOTEMPTY    39    /* Directory not empty */
    #define    ELOOP        40    /* Too many symbolic links encountered */
    #define    EWOULDBLOCK    EAGAIN    /* Operation would block */
    #define    ENOMSG        42    /* No message of desired type */
    #define    EIDRM        43    /* Identifier removed */
    #define    ECHRNG        44    /* Channel number out of range */
    #define    EL2NSYNC    45    /* Level 2 not synchronized */
    #define    EL3HLT        46    /* Level 3 halted */
    #define    EL3RST        47    /* Level 3 reset */
    #define    ELNRNG        48    /* Link number out of range */
    #define    EUNATCH        49    /* Protocol driver not attached */
    #define    ENOCSI        50    /* No CSI structure available */
    #define    EL2HLT        51    /* Level 2 halted */
    #define    EBADE        52    /* Invalid exchange */
    #define    EBADR        53    /* Invalid request descriptor */
    #define    EXFULL        54    /* Exchange full */
    #define    ENOANO        55    /* No anode */
    #define    EBADRQC        56    /* Invalid request code */
    #define    EBADSLT        57    /* Invalid slot */

    这就是所谓的“程序员负责”,意思是你需要编写程序的时候时刻注意是否出现差错,并且及时检查errno的数值,并比对errno的定义,看看到底是哪一步出现了错误。这样说你可能有点遗憾,但是对于管道的写入,操作系统基本不怎么管。因为管道是一个文件,尽管他可以实现进程之间的通信,但说到底,怎么说这也只是一个程序员对一个文件进行的读写操作。

    可能会留给你判断是否出错的地方,是检查popen()的返回值,如果你想要向一个程序进行写,而popen()失败(很可能仅仅是因为内存不足、或者是系统内的文件描述符不够增加一个新的描述符、或者不够增加一个新的进程、或者是一些其他的意外导致的)会返回一个NULL,这时需要由你进行检测。对照查看errno是一个麻烦事,但是Linux的函数提供了这样一个方法,可以让你轻松查看到底是哪一步出现了错误。

    #include <string.h>

    char *strerror(int errnum);

    发生错误之后,比如

    FILE *fp = popen("something", "w");

    if(fp==NULL){//调用strerror}

    发生错误之后调用sterror,查看errno对应的错误内容等。或者使用另一个函数void perror(const char *s);把错误映射为一个字符串,并将它输出到标准错误输出流(stderr)。

    如果想要出现“ 向一个没有进程打开的(读取)管道写入 ”。假设当前你正在写一个C语言程序。可能的情况是(1),本程序向管道写入内容,但是没有另一方收数据。这等价于一直写数据,但是没人接受,这时候Linux会分配给你一个缓冲区,但是当缓冲区超限之后,程序就会自动关闭(上午我刚刚测试的结果),需要程序员查验哪里出现了错误。

    接下来介绍pipe和FIFO文件。在刚刚的解释过程中已经说明了,任何一个文件描述符其实都对应着一个字符串流,所以read(), write()只是fread(), fwrite()之下更底层的实现方法,多个文件描述符可以指向同一个字符流。

    如果想往管道中写数据,需要打开管道的写端。在打开写端的时候,用到了操作系统。open()调用的时候有两种方式,一种是阻塞态,一种是非阻塞打开。操作系统会检查有没有读取端正在打开管道。

    1)如果有文件正在读管道,那么阻塞、非阻塞方式都会立刻打开读端管道。

    2)否则,如果没有文件正在读取管道,那么阻塞方式会一直阻塞挂起,等待某个文件读取管道,要是一直没有数据以读方式打开管道,那么他就会一直等待阻塞,这段期间不会占用CPU资源,等到有读端打开管道,操作系统会通知写端的程序,并将其放入就绪队列等待其进入运行状态;非阻塞状态就会返回一个错误-1并且FIFO也不会打开。

    3)同理,如果已经有读端、写端正常打开了管道,而读端单方面关闭了管道,则写端就会根据打开管道时候约定的阻塞方式或非阻塞方式判断继续写数据或者是直接错误退出。值得一提的是,利用pipe打开一个管道时候,选择的一般都是非阻塞方式打开的(成对打开)。

    至此,试图向一个没有进程打开的(读取)管道写入会发生什么已经讲完了。

    恭喜题主在学习中想出了一个很好的问题,善于思考是一种优秀的品质!祝你学业顺利,心想事成!

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(2条)

报告相同问题?

悬赏问题

  • ¥15 MATLAB yalmip 可转移负荷的简单建模出错,如何解决?
  • ¥15 数学的三元一次方程求解
  • ¥20 iqoo11 如何下载安装工程模式
  • ¥15 本题的答案是不是有问题
  • ¥15 关于#r语言#的问题:(svydesign)为什么在一个大的数据集中抽取了一个小数据集
  • ¥15 C++使用Gunplot
  • ¥15 这个电路是如何实现路灯控制器的,原理是什么,怎么求解灯亮起后熄灭的时间如图?
  • ¥15 matlab数字图像处理频率域滤波
  • ¥15 在abaqus做了二维正交切削模型,给刀具添加了超声振动条件后输出切削力为什么比普通切削增大这么多
  • ¥15 ELGamal和paillier计算效率谁快?