第5章重定向与管道在Linux系统中,系统会默认为命令或程序打开三个标准I/O文件,保证命令或程序可以与用户进行交互。除此之外,还可以将这三个标准文件重定向,借此可以实现从指定的位置获取输入信息或将输出信息、错误信息保存在指定的文件中。同时,Linux系统还提供管道的功能,可以将多条命令或程序之间的输出和输入相衔接,实现灵活的Shell编程。
5.1重定向和管道命令〖4/5〗5.1.1重定向命令所有的UNIX I/O重定向都是基于标准数据流的原理。在Shell中键入命令运行时,内核将会为命令进程打开三个标准I/O设备文件,并且用文件���述符0、1、2与它们关联,其中文件描述符0对应标准输入设备,1对应标准输出设备,2对应标准错误文件。进程在运行过程中,默认从标准输入设备文件读取数据,将输出数据写入标准输出设备文件,如果出错的话,错误信息将会写入标准错误文件。例如,使用cat命令时,系统将会把结果显示在标准输出设备上。通常标准输入设备文件为键盘,标准输出设备文件和标准错误文件为显示器,如图5.1所示。
图5.1进程和标准设备文件的联系
在某种情况下,用户希望能够将信息输出到某个文件中,而不是显示在标准输出设备上,此时可以将该进程的标准输出进行重定向。重定向命令分为输入重定向、输出重定向和错误重定向。〖1〗Linux环境**程序设计第5章重定向与管道[3]〖3〗1. 输入重定向
输入重定向使用符号“<”来表示,它表示将进程的文件描述符0关联到指定的文件上去。输入重定向命令的格式为: command <file例如,命令mail s test hr@163.com<file就是一条输入重定向命令,它表示以file文件为邮件内容,向hr@163.com邮箱发送一封标题为test的邮件。使用“<”符号时,也可以把文件描述符0加在前面,例如,cat 0<file1,该命令同样表示将cat命令的输入重定向到file1文件。
2. 输出重定向
符号“>”或“>>”都可以用来表示输出重定向,两者的差异在于: 前者以覆盖的方式输出,后者以追加的方式输出。输出重定向命令的格式为: command>file或 command>>file例如,命令cat file1 file2>file3表示将文件file1和file2的内容合并输出到文件file3中,这条命令也可以使用以下两条命令来替换: cat file1>file3
cat file2>>file3如果重定向使用的是符号“>!”,那么表示输出重定向强制覆盖文件原有的内容。
3. 错误重定向
错误重定向可以使用符号“2>”或“2>>”来表示,两个符号的差异与输出重定向类似。错误重定向的具体命令格式为: command 2>file或command 2>>file使用错误重定向后,如果在命令执行的过程中有错误发生,错误信息将会记录在文件file中。
除了以上介绍的重定向符号外,还可以使用“>&”“1>”“2>&1”等符号。使用重定向符号时,也并不**于只能在命令末尾处出现重定向符号。这些重定向符号可以单独使用,也可以组合使用,例如: wc <file1 >result.wc 2>error.txt以上命令表示统计file1文件的字符、单词和行数,将结果记录在result.wc文件中,如果出错,错误信息记录在error.txt文件中。
5.1.2管道命令
如果某些数据必须要经过几次命令操作之后才能得到所想要的结果,此时可以使用管道符号将这些命令连接起来。管道命令会将符号先后的命令连接起来,将前一条命令的输出作为后一条命令的输入。符号“|”称为管道操作符,管道命令的具体格式如下: command1|command2|command3|……例如: grep root /etc/passwd | sort图5.2管道命令示意图
该命令表示在/etc/passwd文件中搜索出含有root的内容,并将这些内容排序。这条管道命令的示意图如图5.2所示。
又如: ls -l|grep hr|lpr该命令表示列出当前目录中包含hr模式串的文件名称,将结果打印出来。
5.2实现重定向〖4/5〗5.2.1重定向的实施者在实现重定向命令之前,需要借由一个示例程序先来明确一下重定向是由待执行的命令或程序还是Shell来实现的。 \[示例程序5.1 for_redirect.c\]
#include<stdio.h>
main(int argc, char argv\[ \])
{
int i;
char buf\[80\];
scanf("%s",buf);
printf("info from file:%s\\n",buf);
printf("arg list\\n");
for(i=0;i<argc;i )
printf("argv\[%d\]:%s\\n",i,argv\[i\]);
fprintf(stderr,"where do you find this?\\n");
}在示例程序5.1中,分别使用scanf、printf和fprintf函数对标准输入、标准输出和标准错误文件进行了读、写操作,并且还使用printf函数输出了执行程序时所附带的参数。当使用两种不同的方式来运行该程序时,请观察一下重定向符号是否会被Shell当作参数传递给用户程序。
将程序编译后按照带重定向和不带重定向两种方式运行,得到结果如下: root@ubuntu:~#./for_redirect para1 para2 para3
Hello
info from file:Hello
arg list
argv\[0\]:./for_redirect
argv\[1\]:para1
argv\[2\]:para2
argv\[3\]:para3
where do you find this?
root@ubuntu:~#./for_redirect para1 para2 para3>result 2>err.txt
Hello
root@ubuntu:~#cat result
info from file:Hello
arg list
argv\[0\]:./for_redirect
argv\[1\]:para1
argv\[2\]:para2
argv\[3\]:para3
root@ubuntu:~#cat err.txt
where do you find this?从程序两次运行的输出结果来看,输出的参数中并没有重定向符号和文件名称,因此可以确定: 重定向并不是由命令或用户程序实现的,而是由Shell实现的。因此可以明确: 要实现重定向命令,就需要改写第4章所编写的简单Shell程序,在Shell执行命令之前添加实现重定向的功能。
5.2.2实现重定向的前提条件
当着手设计带有重定向功能的Shell程序时,还要谨记: 之所以能够实现重定向,是因为Linux环境具有以下几个特性:
(1) 系统为每个进程所打开的标准I/O设备文件对应着值*小的三个文件描述符0、1、2。实际上一个进程打开的所有文件的信息是储存在一个结构体数组之中的,而打开文件对应的文件描述符就是该文件信息在结构体数组中的存储位置,即数组的下标。
(2) 当进程使用open、dup等文件操作时,新分配的文件描述符遵循*低可用文件描述符原则。即当打开一个文件时,系统为此文件安排的文件描述符总是可用的文件描述符中值*小的那一个。
(3) 在一个进程中,如果在文件打开操作以后使用了eXec族函数,那么eXec函数将不会影响执行前打开的文件描述符集合。
5.2.3dup和dup2
dup和dup2是在Linux中实现重定向命令时经常会使用到的两个函数。两者都可用于复制文件描述符,dup函数的接口规范说明如表5.1所示。续表表5.1dup函数的接口规范说明
函数名称dup函数功能复制一个文件描述符头文件#include<unistd.h>函数原型int dup(int oldfd);参数oldfd: 被复制的文件描述符返回值>-1: 复制成功,返回新的文件描述符
-1: 出错dup函数用来复制一个文件描述符,参数oldfd指向一个打开的文件,函数的返回值返回复制后的新文件描述符,新的文件描述符也指向oldfd所指向的文件列表项,如图5.3所示,如果该进程没有打开其他文件,那么在执行了dup(0)之后,文件描述符3将会指向0所对应的文件。
图5.3dup(0) 后文件描述符的关系
dup2也可用于复制文件描述符,它与dup的不同之处在于,dup2可以指定要把信息复制给哪一个文件描述符。dup2函数的接口规范说明如表5.2所示。表5.2dup2函数的接口规范说明
函数名称dup2函数功能复制一个文件描述符头文件#include<unistd.h>函数原型int dup2(int oldfd, int newfd);参数oldfd: 被复制的文件描述符
newfd: 新的文件描述符返回值> -1: 复制成功,返回新的文件描述符
-1: 出错说明: dup2在复制文件描述符时,如果newfd已分配给某个打开的文件,那么系统会先关闭newfd,切断与原先文件的联系,然后再进行复制。
示例程序5.2说明了如何使用dup和dup2来复制文件描述符。 \[示例程序5.2 exp_dup.c\]
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
main()
{
int fd,fd1,fd2;
char buf\[10\];
fd=open("text.txt",O_RDONLY);
if(fd<0)
{
perror("open");
exit(EXIT_FAILURE);
}
fd1=dup(fd);
if(fd1<0)
{
perror("dup");
exit(EXIT_FAILURE);
}
fd2=dup2(fd,5);
if(fd2<0)
{
perror("dup2");
exit(EXIT_FAILURE);
}
if(read(fd,buf,10)>0)
write(STDOUT_FILENO,buf,10);
if(read(fd1,buf,10)>0)
write(STDOUT_FILENO,buf,10);
if(read(fd2,buf,10)>0)
write(STDOUT_FILENO,buf,10);
close(fd);
close(fd1);
close(fd2);
}编译后运行,得到程序的运行结果如下: root@ubuntu:~#cat text.txt
This is a test text,
Can you see my greeting?
root@ubuntu:~#./exp_dup
This is a test text,
Can you s从运行结果可以看出,fd通过open函数分配给了文件text.txt,文件描述符fd1和fd2都复制了fd,因此三个文件描述符指向同一个文件。尽管fd、fd1和fd2是三个不同的文件描述符,但它们使用的是同一个打开的文件表项(struct file)。因此不管通过哪个文件描述符改变了文件读写指针的位置,都会对其他两个文件描述符造成影响。从这个示例的运行结果也能够看到,输出结果并不是三次从头开始的10个字符,而是从头开始连续的30个字符。
5.2.4重定向的三种方法
通过上一节的学习,可以分析出实现重定向的流程: 0、1、2三个文件描述符原先是与标准I/O设备文件关联的,需要重定向时,可以使用open、dup或dup2等函数将文件描述符0、1或2与指定的重定向文件相关联,这样就可以实现标准I/O设备文件的重定向。在Linux系统中,可以使用三种方法来实现重定向功能,分别是:
close then open: 关闭指定的标准I/O设备文件,打开重定向文件。
open close dup close: 打开重定向文件,关闭指定的标准I/O设备文件,复制重定向文件的文件描述符,关闭**步打开的文件描述符。
open dup2 close: 打开重定向文件,将指定的标准I/O设备文件描述符作为参数,复制重定向文件的文件描述符,关闭**步打开的文件描述符。
针对以上三种重定向的方法,我们来一一讲解。
1. close then open
在这种方法中,程序调用close函数关闭指定的文件描述符与标准设备文件的联系,此时该文件描述符就处于空闲可分配状态;随后使用open函数打开指定的重定向文件,由于open遵循*低可用文件描述符原则,打开的文件将获得**步操作释放出来的文件描述符。示例程序5.3实现了将标准输入重定向: \[示例程序5.3 exp_redirect1.c\]
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
main()
{
int fd;
char buf\[80\];
close(0);
if((fd=open("./text.txt",O_RDONLY))!=0)
{
perror("open");
exit(EXIT_FAILURE);
}
read(0,buf,80);
write(1,buf,80);
}该程序将标准输入重定向到当前目录中的text.txt文件,从中读取80个字节的数据,输出在标准输出设备上。文件描述符0关联的文件变化情况如图5.4所示。
图5.4close then open方法的示意图
2. open close dup close
这种方法首先使用open函数打开指定的重定向文件,获取该文件的文件描述符fd;随后,使用close函数关闭标准I/O文件,释放其对应的文件描述符;之后使用dup函数复制fd,此时由于dup函数遵循*低可用文件描述符原则,因此将会把fd复制给第二步中close释放出来的文件描述符;*后使用close关闭fd即可。示例程序5.4使用第二种方法来实现重定向,其功能和示例程序5.3一样,将输入重定向到当前目录中的text.txt文件上。 \[示例程序5.4 exp_redirect2.c\]
main()
{
int fd,newfd;
char buf\[80\];
fd=open("./text.txt ",O_RDONLY);
close(0);
newfd=dup(fd);
if(newfd!=0)
{
perror("dup");
exit(EXIT_FAILURE);
}
close(fd);
read(0,buf,80);
write(1,buf,80);
}在这个示例程序中,文件描述符关联文件的变化情况如图5.5所示。
图5.5open close dup close的过程
3. open dup2 close
这种方法与open close dup close方法类似,不同之处在于,使用dup2将open close dup close方法的第二步和第三步合而为一,直接关闭了标准I/O文件并进行了文件描述符的复制。示例程序5.5为使用方法三实现输入重定向的代码。 \[示例程序5.5 exp_redirect3.c\]
main()
{
int fd,newfd;
char buf\[80\];
fd=open("./text.txt ",O_RDONLY);
newfd=dup2(fd,0);
if(newfd!=0)
{
perror("dup2");
exit(EXIT_FAILURE);
}
close(fd);
read(0,buf,80);
write(1,buf,80);
}以上三种方法均可以实现重定向操作,具体使用哪种方法可以根据实际情况来决定。如果已知重定向文件的文件名,那么三种方法都可以实现;但如果在程序运行过程中只能获取重定向文件的文件描述符,那么就要考虑使用后面两种方法。
5.2.5ls l>list.txt
为了使读者能够更好地掌握如何在简单Shell中实现重定向功能,我们先来学习一个具体的重定向命令如何实现: ls -l>list.txt首先,先来分析一下Shell在实现ls l命令时的过程是怎样的。从第4章了解到当Shell执行一条前台命令时,将会为命令创建一个子进程;随后在进程中使用eXec函数来执行命令程序,此时Shell进程处于阻塞状态等待命令进程结束;当命令进程结束后,Shell将会显示命令进程运行的结果。从这个过程中可以发现:
(1) 命令进程需要进行重定向,但是Shell进程并不需要重定向。
(2) 为了执行命令,Shell需要调用fork函数创建子进程,子进程需要调用eXec函数来执行ls l命令。这就要求必须在创建了子进程之后,调用eXec函数之前将子进程的输出重定向到文件list.txt。这样一来,重定向仅仅是针对子进程的,并且子进程在调用eXec函数时,并不会影响到在eXec函数执行前已打开的文件描述符列表。
(3) 由于list.txt有可能是一个不存在的文件,还需要将重定向的三种方法中的open函数换成creat函数。
程序代码如示例程序5.6所示。 \[示例程序5.6 exp_lsre.c\]
main()
{
int pid,fd;
printf("This is to show how to redirect!\\n");
if((pid=fork())==-1)
{
perror("fork");
exit(EXIT_FAILURE);
}
else if(pid==0)
{
close(1);
fd=creat("list.txt",0644);
if(execlp("ls","ls","-l",NULL)<0)
{
perror("exec");
exit(EXIT_FAILURE);
}
}
else if(pid!=0)
{
wait(NULL);
system("cat list.txt");
}
}示例程序5.6使用了close then open来实现重定向。请读者思考一下,命令ls l>>list.txt、ls l 2>err.txt应该如何实现?
从示例程序5.6中,可以分析出在简单Shell程序中实现重定向的流程如下:
(1) 以字符串的方式读入命令后,将命令字符串分解为命令名称、参数、选项、重定向等各个分项。
(2) 如果存在重定向符号,则需要根据重定向符号的类型,分解出重定向文件名和需要重定向的标准I/O文件。
(3) 调用fork创建子进程,在子进程中按照重定向的要求,重定向标准I/O文件,父进程调用wait等待子进程结束。