linux pipe函数 重定向,Shell实现:重定向和管道

66b52468c121889b900d4956032f1009.png

8种机械键盘轴体对比

本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选?

介绍

Shell实现:基本功能一文介绍了如何实现shell的基本功能,本文介绍如何实现I/O重定向和管道。

I/O重定向使得程序可以自由地指定数据的流向,不一定从键盘读取数据或输出结果到屏幕上;管道使得一条命令的输出可以作为另一条命令的输入,多条命令可以配合完成一项任务。例如下面的命令:1

2

3

4

5

6

7

8

9$ ls -l > 1.txt

$ cat 1.txt

total 24

-rw-r--r-- 1 krist users 0 Apr 15 16:37 1.txt

-rw-r--r-- 1 krist users 86 Apr 15 09:56 Makefile

-rw-r--r-- 1 krist users 13177 Apr 15 09:56 psh.c

-rw-r--r-- 1 krist users 21 Apr 15 10:05 README.md

$ cat < 1.txt | wc -l

5

第一条命令中的>符号表示将ls -l命令的输出结果重定向到文件1.txt。最后一条命令中的

由于在下暂时才疏学浅,只好先实现简单的功能,传达基本精神即可。因此,本文的shell实现对输入的命令进行限制:一次只能有一种操作,即最多只能包含一个或|符号,所以cat < 1.txt | wc -l是不合法的

三种符号的前后必须有空格,类似于ls>1.txt或cat 1.txt| wc -l是不合法的

主流程

Shell的主进程会fork一个子程序运行用户输入的命令。子进程首先检查命令中是否包含或|符号,以确定命令的类型,然后用做相应的处理。主流程如下: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

28pid_t pid = fork();

switch (pid) {

case -1:

perror("fork");

exit(EXIT_FAILURE);

case 0:

// 识别命令的类型

int symbol_pos; // 符号的位置

int cmd_type = check_cmd(arg_vec, &symbol_pos);

//

switch (cmd_type) {

case CMD_NORMAL:

exec_normal(arg_vec); // 普通命令

case CMD_INPUT_REDIRECT:

exec_input_redirect(arg_vec, symbol_pos); // 输入重定向

case CMD_OUTPUT_REDIRECT:

exec_output_redirect(arg_vec, symbol_pos); // 输出重定向

case CMD_PIPELINE:

exec_pipeline(arg_vec, symbol_pos); // 管道

case CMD_INVALID:

fprintf(stderr, "Invalid command!n"); // 命令不合法

exit(EXIT_FAILURE);

default:

break;

}

default:

while (wait(&status) != pid); // 父进程等待子进程运行完毕

}

arg_vec变量是字符串数组,保存处理之后的用户命令。如果输入命令为ls -al | wc -l,那么该数组的内容为:1char *arg_vec[] = {"ls", "-al", "|", "wc", "-l", NULL};

实现原理

所有的系统调用(system call)都通过文件描述符(file descriptor)对各种类型的文件进行I/O操作。每个进程都维护自己的一组文件描述符。

一般来说,所有的程序都会使用三个标准的文件描述符0、1和2,分别对应着标准输入、标准输出和标准错误。当通过shell运行命令时,这三个描述符在程序运行之前就会打开。准确来说,是程序继承了shell的描述符,而shell会一直保持这三个描述符是打开的。

程序会从标准输入读入数据,输出结果到标准输出,输出错误到标准错误。当我们使用交互式的shell时,这三个描述符都连接到shell所运行的终端上,所以程序会从键盘读取数据,然后运行,最后把结果和错误打印到屏幕上。所以,要进行I/O重定向和管道操作,就要重定向相应的文件描述符。

I/O重定向

输入重定向

程序从标准输入读取数据,如果将文件描述符0定位到一个文件上,那么此文件就成了标准输入的源。实现上述功能要用到dup2函数:1int (int oldfd, int newfd);

dup2函数将oldfd文件描述符复制给newfd,如果newfd之前打开了,dup2会先将它关闭。将文件描述符0重定向到文件的步骤如下:1

2

3int fd = open(filename, O_RDONLY); // 打开文件,描述符fd对应文件

dup2(fd, 0); // 将fd复制到0,此时0和fd都指向文件

close(fd); // 关闭fd,此时只有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

27void exec_input_redirect(char **arg_vec, int pos)

{

// arg_vec保存用户输入的命令

// pos是符号'

char *filename = arg_vec[pos+1]; // 符号'

arg_vec[pos] = NULL; // 符号'

// 打开文件,将标准输入重定向到文件

int fdin = open(filename, O_RDONLY);

if (fdin == -1) {

perror("open");

exit(EXIT_FAILURE);

}

if (dup2(fdin, STDIN_FILENO) == -1) {

perror("dup2");

exit(EXIT_FAILURE);

}

if (close(fdin) == -1) {

perror("close");

exit(EXIT_FAILURE);

}

// 执行命令,文件内容成为命令的输入源

execvp(arg_vec[0], arg_vec);

perror("execvp");

exit(EXIT_FAILURE);

}

输出重定向

类似地,实现输出重定向需将文件描述符1定位到文件上。完整代码如下: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

28void exec_output_redirect(char **arg_vec, int pos)

{

// arg_vec保存用户输入的命令

// pos是符号'>'的位置

char *filename = arg_vec[pos + 1]; // 符号'>'后面是文件名

arg_vec[pos] = NULL; // 符号'>'前面是命令

// 打开文件,将标准输出重定向到文件

int fdout = open(filename, O_WRONLY | O_CREAT | O_TRUNC,

S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);

if (fdout == -1) {

perror("open");

exit(EXIT_FAILURE);

}

if (dup2(fdout, STDOUT_FILENO) == -1) {

perror("dup2");

exit(EXIT_FAILURE);

}

if (close(fdout) == -1) {

perror("close");

exit(EXIT_FAILURE);

}

// 执行命令,结果会输出到文件中

execvp(arg_vec[0], arg_vec);

perror("execvp");

exit(EXIT_FAILURE);

}

管道

管道(pipe)是进程间通信的重要手段之一。调用pipe函数创建一个管道,并将其两端连接到两个文件描述符,其中pipefd[0]为读数据端的文件描述符,pipefd[1]为写数据端的文件描述符:1int pipe(int pipefd[2])

当进程创建一个管道之后,该进程就有了连向管道两端的连接(即为两个文件描述符)。当该进程fork一个子进程时,子进程也继承了这两个连向管道的连接,如下面左图所示。父进程和子进程都可以将数据写到管道的写数据端口,并从读数据端口将数据读出。两个进程都可以读写管道,但当一个进程读,另一个进程写时,管道的使用效率是最高的,因此,每个进程最好关闭管道的一端,如下面右图所示。f61dea5a98c633a9f18551e1997acc27.pngshell_pipeline1

Shell要实现管道功能,需将前一条命令的输出作为后一条命令的输入。那么以上面右图为基础,还需将前一进程的标准输出重定向到管道的写数据端,将后一进程的标准输入重定向到管道的读数据端,如下图所示:3190076926c02bd217dc3fcd901105a3.pngshell_pipeline2

我们将运行整条命令的子进程称为进程A,本文shell的实现中,进程A并不执行命令,而是再fork两个进程,称之为进程B1和B2,分别执行两条命令。两个进程都从进程A继承了管道两端的连接,可通过该管道通信。进程A不再需要管道连接,于是关闭两个文件描述符,然后等待进程B1和B2执行完毕。完整代码如下: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

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97void exec_pipeline(char **arg_vec, int pos)

{

// arg_vec保存用户输入的命令

// pos是符号'|'的位置

char **arg_vec1 = &arg_vec[0]; // 第一条命令CMD 1

arg_vec[pos] = NULL; // 两条命令的分界

char **arg_vec2 = &arg_vec[pos+1]; // 第二条命令CMD 2

// 创建管道

int pfd[2];

if (pipe(pfd) == -1) {

perror("pipe");

exit(EXIT_FAILURE);

}

// 创建进程B1,执行CMD 1

switch (fork()) {

case -1:

perror("fork");

exit(EXIT_FAILURE);

case 0:

// 关闭管道的读数据端

if (close(pfd[0]) == -1) {

perror("close");

exit(EXIT_FAILURE);

}

if (pfd[1] != STDOUT_FILENO) { // 防御性编程

// 将标准输出重定向到管道的写数据端

if (dup2(pfd[1], STDOUT_FILENO) == -1) {

perror("dup2");

exit(EXIT_FAILURE);

}

if (close(pfd[1]) == -1) {

perror("close");

exit(EXIT_FAILURE);

}

}

// 执行CMD 1,运行结果将写入管道

execvp(arg_vec1[0], arg_vec1);

perror("execvp");

exit(EXIT_FAILURE);

default:

break;

}

// 创建进程B2,执行CMD 2

switch (fork()) {

case -1:

perror("fork");

exit(EXIT_FAILURE);

case 0:

// 关闭管道的写数据端

if (close(pfd[1]) == -1) {

perror("close");

exit(EXIT_FAILURE);

}

if (pfd[0] != STDIN_FILENO) { // 防御性编程

// 将标准输入重定向到管道的读数据端

if (dup2(pfd[0], STDIN_FILENO) == -1) {

perror("dup2");

exit(EXIT_FAILURE);

}

if (close(pfd[0]) == -1) {

perror("close");

exit(EXIT_FAILURE);

}

}

// 运行CMD 2,从管道中读数据作为输入

execvp(arg_vec2[0], arg_vec2);

perror("execvp");

exit(EXIT_FAILURE);

default:

break;

}

// 进程A不需要管道通信,关闭管道的两端

if (close(pfd[0]) == -1) {

perror("close");

exit(EXIT_FAILURE);

}

if (close(pfd[1]) == -1) {

perror("close");

exit(EXIT_FAILURE);

}

// 进程A等待两个子进程执行完毕

if (wait(NULL) == -1) {

perror("wait");

exit(EXIT_FAILURE);

}

if (wait(NULL) == -1) {

perror("wait");

exit(EXIT_FAILURE);

}

exit(EXIT_SUCCESS);

}

完整的代码请参考这里。

参考

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值