要理解管道操作,必须知道Linux的文件和文件描述符概念。
关于这两个概念的理解,在我的两个浅文《Linux系统编程笔记-文件描述符》和《Linux Shell脚本攻略-重定向理解与补充》都有提及。虽然啰嗦的我都想吐,但为了文意连贯,有助于理解,还不得不再提一下要点。
关于Linux文件和文件描述符
简单提一下几个要点:
(1)在Linux中,一切都是文件。屏幕是文件,键盘是文件,系统错误也被抽象成了一个文件。当然,各种磁盘文件、各种设备,都是文件。
(2)是文件都有其对应的名字,名字是静态的。比如:键盘文件、屏幕文件、系统错误,它们的名字分别是:/dev/stdin、/dev/stdout、/dev/stderr。当然,各种磁盘文件、各种设备,也都有对应的名字。
(3)但是在Linux运行时,进程或内核需要打开某个文件时,内核用文件描述符的方式来跟踪处理相对应的文件。所以,文件描述符是动态概念。文件描述符是个非负整数。
(4)在Linux中,标准文件描述符有三个,分别是0(stdin)、1(stdout)、2(stderr)
std其实就是指键盘设备文件,stdout就指的是屏幕设备文件, stderr就是指错误文件。这三个之所以称之为“标准”,意思就是说它们是系统预留的,相当于每个进程的全局变量,也就是说每个进程无需刻意打开这三个文件描述符,就可以使用这三个标准文件。除了这三个,其它的,你要用,就必须自已open,自已open所得到的文件描述符值都大于或等于3。
Linux 管道“|”操作说明
通过内容过滤将输出重定向到文件是我们平日里的基本任务之一。
除了文件输入重定向和输出重定向。Linux Shell 还有一种功能,就是可以将两个或者多个命令(程序或者进程)连接到一起,把一个命令的输出作为下一个命令的输入,以这种方式连接的两个或者多个命令就形成了管道(pipe)。
Linux 管道使用竖线“|”连接多个命令,这被称为管道符。Linux 管道的具体语法格式如下:
cmd1 | cmd2
cmd1 | cmd2 [ | cmdN... ]
当在两个命令之间设置管道时,管道符|左边命令的输出就变成了右边命令的输入。只要第一个命令向标准输出写入,而第二个命令是从标准输入读取,那么这两个命令就可以形成一个管道。大部分的 Linux 命令都可以用来形成管道。
这里需要注意,cmd1 必须有正确输出,而 cmd2 必须可以处理 cmd1 的输出结果;而且 cmd2 只能处理 cmd1 的正确输出结果,不能处理 cmd1 的错误信息。cmd3也能够接收cmd2的正确输出,依次类推。
以上关于Linux管道操作的说明,其是还是模糊和抽象的。下面我们用现有shell命仅来实际举例说明。
管道操作举例
ls 命令输出文件列表,执行:ls -l /dev能显示/dev目录下面好几百个文件。
grep 命令用于查找文件里符合条件的字符串。执行:ls -l /dev | grep std之后,得到了三个名有std的文件
cat 显示文件,执行:ls -l /dev | grep std | cat -n之后,把上一步的三个文件加上行号
管道操作解释
cmd1:ls -l /dev上面说过,它“必须有正确的输出”,输出是啥意思,就是ls命令输出到标准输出文件stdout,其实就是输出到屏幕。上面操作说明的“第一个命令向标准输出写入”正是这个意思。
cmd2:grep std它从标准输入读出数据,进行字符串比对。也就是说,这个grep命令是从stdin读取的数据。上面操作说明的“而第二个命令是从标准输入读取”正是这个意思。
一般新手肯定有这个疑问,既然“ls -l /dev”向屏幕文件stdout发送数据,grep是如何从stdin得到这个数据的呢?其实,这正是管道操作“|”干的活,管道操作“|”将“cmd1:ls -l /dev”的向stdout发送的数据,转而写入stdin,故此,“cmd2: grep std” 才能正常接收到数据。
也就是说,管道操作“|”就是把左边命令的stdout数据,转而写到stdin,供右边的命令使用。
管道操作的程序验证
我们空说无凭,程序来实证。
C代码:write_to_stdout.c
#include
#include
#include
#include
#include
#define READ_MAX_LEN 1024
int main(int argc,char *argv)
{
ssize_t write_len;
if ( ( argc < 2) || ( strcmp(argv[1],"--help") == 0 ) )
{
printf("命令语法: %s inputString\n",argv[0]) ;
exit(1);
}
write_len =write(1,argv[1],strlen(argv[1]));
if ( write_len < 0 )
{
perror("错误:file write error ");
exit(1);
}
return 0;
}
编译:gcc write_to_stdout.c -o write.out
程序write.out,可以把你输入的参数字符串,输出到屏幕上。代码的内部,“write(1,argv[1],strlen(argv[1]));”的意思,就是写到stdout上面。
C代码:read_from_stdin.c
#include 部分,略
#define BUF_LEN 1024
int main(void)
char read_buf[BUF_LEN +1];
char digit_buf[BUF_LEN +1];
char alpha_buf[BUF_LEN +1];
ssize_t len_read;
size_t len_digit=0,len_alpha=0;
len_read =read(0,read_buf,BUF_LEN);
if ( len_read < 0 )
perror("perror:file read error ");
exit(1);
for(int i=0;i
if (isdigit(read_buf))
digit_buf[len_digit++]=read_buf;
else
alpha_buf[len_alpha++]=read_buf;
digit_buf[len_digit] = '\0';
alpha_buf[len_alpha] = '\0';
printf("read stdin, number part:%s\n alpha part:%s \n",digit_buf,alpha_buf);
return 0;
编译:gcc read_from_stdin.c -o read.out
程序read.out,从stdin读入内容,凡数字的拼接在一起,凡是字母的拼接在一起,分类输出到屏幕上。代码的内部,“read(0,read_buf,BUF_LEN);”的意思,就是从stdin标准输入上面读取数据。
这个两个自制小程序,实证了管道操作的本质:就是把管道符“|”左边命令的stdout数据,转而写到stdin,供右边的命令使用。
或许有问:代码中的“write(1,argv[1],strlen(argv[1]));”和“read(0,read_buf,BUF_LEN);”,你怎么就知道是1和0,就对应标准输出stdout和标准输入stdin?请参看《Linux Shell脚本攻略-重定向理解与补充》中的内容,此处没必要重复。
另外,如果想用 "/dev/stdin"或“/dev/stdio”,就要“fd =open("/dev/stdin",...)”之后,再调用read、write函数来进一步测试。结果是一样的。 |