社区编辑申请
注册/登录
Unix的标准I/O与重定向的若干概念解析
系统 其他OS 系统运维
Unix默认从文件描述符0读取数据,写数据到文件描述符1,将错误信息输出到文件描述符2。重定向标准输入、标准输出和错误输出意味着改变文件描述符0、1和2的连接。管道是内核中的一个数据队列,其每一端连接一个文件描述符。程序通过pipe系统调用来创建管道。

标准I/O与重定向的若干概念

3个标准文件描述符

所有的Unix工具都使用文件描述符0、1和2。如下图所示,标准输入文件的描述符是0,标准输出的文件描述符是1,标准错误输出的文件描述符则是2。Unix假设文件描述符0、1和2都已经被打开,可以分别进行读、写和写的操作。

 

重定向I/O的是shell而不是程序

通过使用输出重定向标志,命令cmd>filename告诉shell将文件描述符1定位到文件。于是shell就将文件描述符与指定的文件连接起来。程序持续不断地将数据写到文件描述符1中,根本没有意识到数据的目的地已经改变了。listargs.c展示了程序甚至没有看到命令行中的重定向符号。

  1. #include <stdio.h> 
  2.  
  3. int main(int ac, char* av[]) { 
  4.     int i; 
  5.     printf("Number of args: %d, Args are: \n", ac); 
  6.     for(i = 0; i < ac; i++) { 
  7.         printf("args[%d] %s\n", i, av[i]); 
  8.     } 
  9.     fprintf(stderr, "This message is sent to stderr.\n"); 

 

程序listargs将命令行参数打印到标准输出。注意listargs并没有打印出重定向符号和文件名。

 

如上图所示验证了关于shell输出重定向的一些重要概念。

  • shell并不将重定向标记和文件名传递给程序。
  • 重定向可以出现在命令行中的任何地方,并且在重定向标识符周围并不需要空格来区分。例如上图命令./listargs testing >xyz one two 2>oops也可以写成./listargs >xyz testing one two 2>oops,如下图所示。

 

***可用文件描述符(Lowest-Available-fd)原则

文件描述符是一个数组的索引号。每个进程都有其打开的一组文件,这些打开的文件被保持在一个数组中。文件描述符即为某文件在此数组中的索引。并且,当打开文件时,为此文件安排的文件描述符总是此数组中***可用位置的索引。

将stdin重定向到文件

考虑如何将标准输入重定向以至可以从文件中读取数据。更加精确的说,进程并不是从文件读数据,而是从文件描述符读取数据。如果将文件描述符0重定向到一个文件,那么此文件就成为标准输入的源。

方法1:close-then-open

***种放方法是close-then-open策略,具体步骤如下:

  • 开始时,系统中采用的是典型的设置,即三种标准流是被连接到终端设备上的。输入的数据流经过文件描述符0而输出的流经过文件描述符1和2。
  • 接下来,调用close(0),将标准输入与终端设备的连接切断。
  • ***,使用open(filename, O_RDONLY)打开一个想连接到stdin上的文件。当前的***可用文件描述符是0,因此所打开的文件将被连接到标准输入上。任何从标准输入读取数据的函数都将从此文件中读取数据。

方法2:open-close-dup-close

Unix系统调用dup建立指向已经存在的文件描述符的第二个连接,这种方法需要4个步骤。

  • open(file),打开stdin将要重定向的文件。这个调用返回一个文件描述符fd,这个描述符并不是0,因为0在当前已经被打开了。
  • close(0),将文件描述符0关闭,现在文件描述符0已经空闲了。
  • dup(fd),系统调用dup(fd)将文件描述符fd做了一个复制。此处复制使用***可用的文件描述符号。因此获得的文件描述符是0。这样,就将磁盘文件与文件描述符0连接在一起了。
  • close(fd),使用close(fd)来关闭原始连接,只留下文件描述符0的连接。

dup在学习管道的时候非常重要,一个简单一点的方案是将close(0)和dup(fd)结合在一起作为一个单独的系统调用dup2。

重定向I/O:who>userlist

当输入who>userlist时,shell运行who程序,并将who的标准输出重定向到名为userlist的文件上。shell实现该重定向的关键之处在于fork和exec之间的时间间隙。在fork执行完后,子进程仍然在运行父进程也就是shell程序,并准备执行exec。exec将替换进程中运行的程序,但是它不会改变进程的属性和进程中所有的连接。也就是说,在运行exec之后,进程的用户ID不会改变,其优先级也不会改变,并且其文件描述符也和运行exec之前一样。因此,利用这个原则来实现重定向标准输出。

此时who就是子进程要执行的命令,当执行fork前,父进程的文件描述符1指向终端。当执行fork之后,子进程的文件描述符也喜欢指向终端,此时,子进程尝试执行close(1),close(1)之后,文件描述符1成为***未用文件描述符,子进程现在再执行creat(userlist, mode)打开文件userlist,文件描述符1被连接到文件userlist。因此,子进程的标准输出被重定向到文件userlist,子进程然后调用exec执行who。

子进程执行了who程序,于是子进程中的代码和数据都被who程序的代码和数据所替换了,然而文件描述符被保留下来。因为打开的文件并非是程序的代码也不是数据,它们属于进程的属性,因此exec调用并不改变它们。

管道编程

管道是内核中一个单向的数据通道,管道有一个读取端和一个写入端,可以用来连接一个进程的输出和另一个进程的输入。

创建管道

使用系统调用result = pipe(int array[2])来创建管道,并将其两端连接到两个文件描述符。如下图所示,array[0]为读取数据端的文件描述符,而array[1]则为写数据端的文件描述符。类似与open调用,pipe调用也使用***可用文件描述符。

 

程序pipedemo.c展示了如何创建管道并使用管道向自己发送数据。核心代码如下:

  1. int len, i, apipe[2]; 
  2.     char buf[BUFSIZ]; 
  3.  
  4.     if(pipe(apipe) == -1) { 
  5.         perror("could not make pipe."); 
  6.         exit(1); 
  7.     } 
  8.  
  9.     printf("Got a pipe! It is file descriptors: {%d %d}\n", apipe[0], apipe[1]); 
  10.  
  11.     while(fgets(buf, BUFSIZ, stdin)) { 
  12.         len = strlen(buf); 
  13.         if(write(apipe[1], buf, len) != len) { 
  14.             perror("writing to pipe."); 
  15.             break; 
  16.         } 
  17.         for(i = 0; i < len; i++) { 
  18.             buf[i] = 'X'
  19.         } 
  20.         len = read(apipe[0], buf, BUFSIZ); 
  21.         if(len == -1) { 
  22.             perror("reading from pipe."); 
  23.             break; 
  24.         } 
  25.         if(write(1, buf, len) != len) { 
  26.             perror("writing to stdout"); 
  27.             break; 
  28.         } 
  29.     } 

 

数据流从键盘到进程,从进程到管道,再从管道到进程以及从进程回到终端。

使用fork来共享管道

当进程创建一个管道之后,该进程就有了连向管道两端的连接。当这个进程调用fork的时候,它的子进程也得到了这两个连向管道的连接。父进程和子进程都可以将数据写到管道的写数据端口,并从读数据端口将数据读出。但是当一个进程读,而另一个进程写的时候,管道的使用效率是***的。程序pipedemo2.c说明了如何将pipe和fork结合起来,创建一对通过管道来通信的进程,核心代码如下:

  1. int pipefd[2]; 
  2.     int len; 
  3.     char buf[BUFSIZ]; 
  4.     int read_len; 
  5.  
  6.     if(pipe(pipefd) == -1) { 
  7.         oops("cannot get a pipe", 1); 
  8.     } 
  9.  
  10.     switch(fork()) { 
  11.         case -1: 
  12.             oops("cannot fork", 2); 
  13.         /*子进程*/ 
  14.         case 0: 
  15.             len = strlen(CHILD_MESS); 
  16.             while(1) { 
  17.                 if(write(pipefd[1], CHILD_MESS, len) != len) { 
  18.                     oops("write", 3); 
  19.                 } 
  20.                 sleep(5); 
  21.             } 
  22.         /*父进程*/ 
  23.         default
  24.             len = strlen(PAR_MESS); 
  25.             while(1) { 
  26.                 if(write(pipefd[1], PAR_MESS, len) != len) { 
  27.                     oops("write", 4); 
  28.                 }  
  29.                 sleep(1); 
  30.                 read_len = read(pipefd[0], buf, BUFSIZ); 
  31.                 if(read_len <= 0) { 
  32.                     break; 
  33.                 } 
  34.                 write(1, buf, read_len); 
  35.             } 
  36.     } 

 

技术细节

  • 从管道中读取数据
    当进程试图从管道读取数据时,进程被挂起直到数据被写进管道。
    当所有的写进程关闭了管道的写数据端时,试图从管道中读取数据的调用会返回0,这意味这文件的结束。
  • 向管道中写数据
    写入数据阻塞直到管道有空间去容纳新的数据。
    如果所有的读进程都已关闭了管道的读数据端,那么对管道的写入调用将会执行失败。

总结

  • Unix默认从文件描述符0读取数据,写数据到文件描述符1,将错误信息输出到文件描述符2。
  • 创建文件描述符的系统调用总是使用***可用文件描述符号。
  • 重定向标准输入、标准输出和错误输出意味着改变文件描述符0、1和2的连接。
  • 管道是内核中的一个数据队列,其每一端连接一个文件描述符。程序通过pipe系统调用来创建管道。
  • 当父进程调用fork的时候,管道的两端都被复制到子进程中。
  • 只有有共同父进程的进程之间才可以用管道连接。

代码

相关代码见Github

参考

责任编辑:庞桂玉 来源: segmentfault
相关推荐

2022-04-19 14:41:29

Oracle数据库SQL

2022-05-16 13:34:35

漏洞SonicWall攻击者

2022-05-13 09:15:21

三层交换机二层交换机VLAN

2022-05-12 14:44:38

数据中心IT云计算

2022-05-16 11:35:05

Cat.1蜂窝物联网5G RedCap

2022-05-11 12:12:32

ScapyPython网络包

2022-05-12 13:40:18

勒索软件数据泄露网络安全

2022-05-13 09:27:55

Widget机票业务App

2021-12-06 06:19:03

2022-05-05 14:01:02

DNS高危漏洞uClibc

2022-04-01 15:18:04

HarmonyHDF 驱动鸿蒙

2022-03-29 11:59:34

梳理标签体系

2022-04-12 08:43:21

Python内置模块

2022-04-26 07:49:23

Nagios开源监控

2022-04-21 09:26:41

FastDFS开源分布式文件系统

2022-04-21 08:09:18

Spark字段血缘Spark SQL

2022-03-07 15:22:16

classHarmony鸿蒙

2022-03-22 07:37:33

DNS域名IP

2022-04-01 15:26:06

Harmony操作系统鸿蒙

2022-04-12 11:20:11

C 语言Linux编程

同话题下的热门内容

谷歌为 Chrome OS 和浏览器推出新的 IT 安全集成

编辑推荐

软件卸载不干净怎么办?智能卸载轻松搞定!苹果凌晨1点最新推送iOS 11正式版 如何使用 printf 来格式化输出CentOS Linux 已死—Red Hat 称 Stream 不是替代品BIO、NIO 到多路复用的演进路径,你明白了吗?
我收藏的内容
点赞
收藏

51CTO技术栈公众号