文章目录
- 🧸 什么是重定向
- 🐡 文件描述符的分配规则
- 🐡 重定向在日常使用中的简单示例
- 🧸 实现重定向的底层机制
- 🐡 dup2()
- 🐡 利用dup2()实现重定向
- 🧸 在自定义Shell当中添加重定向功能
- 🐡 重定向与进程替换
- 🧸 标准输出重定向与标准错误重定向
🧸 什么是重定向
重定向是计算机编程和操作系统重的一个概念;
一般指的是改变数据流的方向或将程序的输出从一个位置转移到另一个方式;
一般在shell
当中的重定向有
- 标准输出重定向
- 标准错误重定向
- 标准输入重定向
🐡 文件描述符的分配规则
在博客 『 Linux 』基础IO/文件IO (万字) 当中介绍了在Linux当中若是打开了一个文件则对应的将会为该文件分配一个int
类型的文件描述符用来保证该文件的唯一性;
同时介绍了对应的文件描述符的分配规则;
-
存在一段代码
#define FILENAME "log.txt" int main() { close(0); // close(1); // close(2); int fd = open(FILENAME, O_CREAT | O_TRUNC | O_WRONLY, 0666); printf("%d\n", fd); return 0; }
在该程序当中,将以 O_CREAT | O_TRUNC | O_WRONLY
的方式打开文件,且对应的创建文件时的权限为0666
;
同时运行三次程序,分别使用close()
接口关闭文件描述符0
,1
,2
;
当关闭文件描述符0
并运行该程序时将打印0
;
当关闭文件描述符1
并运行该程序时将不作打印(文件描述符1
对应的是标准输出,而printf
正是标准输出操作);
当关闭文件描述符2
并运行该程序时将打印2
;
这也验证了文件描述符的分配规则:
- 文件描述符的分配规则为为新打开的文件分配最小的且未被使用的文件描述符;
其他关于文件描述符的内容在此不再进行赘述;
🐡 重定向在日常使用中的简单示例
在上文当中提到重定向的种类;
存在一个目录;
在该目录当中的shell
命令行当中使用ls
对应的将打印出当前目录下的所有目录与文件;
$ ls
makefile mytest test.c
其中mytest
文件是由test.c
文件编译而来;
具体的test.c
文件内容暂时不重要;
-
输出重定向
ls > log.txt
在这行命令当中,使用了
ls
将当前目录的目录文件显示到显示屏当中;然而在这里使用了重定向
>
,对应的信息并没有显示到显示屏当中;而是被写入至重定向是所指定的
log.txt
文件当中;同时在上一篇博客当中提到;
实际上
>
与>>
重定向的区别即为打开文件的方式不同;>
将以只写的方式打开文件,而>>
将以追加的方式打开文件;$ cat log.txt log.txt makefile mytest test.c
无论这条命令被执行多少次,对应的当使用
cat
将文件当中的内容显示至显示屏当中都会只有一次命令的内容(打开文件时使用了O_TRUNC
选项); -
追加重定向
追加重定向与输出重定向不同;
在上文当中提到追加重定向与输出重定向的打开文件的方式不同;
ls >> log.txt
故假设这里使用两次该命令;
ls
命令所显示出的目录信息将以追加的方式被重定向至log.txt
文件当中;$ cat log.txt log.txt makefile mytest test.c log.txt makefile mytest test.c
-
输入重定向
以上文中的例子为例,
log.txt
文件中当前是存在内容的;与上述的输出重定向不同,输入重定向是从文件当中读取数据的;
使用
cat
命令可以查看文件内容,连接文件并打印等等功能;$ cat aaaaa aaaaa vvvvv vvvvv bbbbb bbbbb ^C
当使用
cat
时,进程将会进入阻塞状态;再利用键盘向终端打印的信息将对应的被
cat
显示出来;当其在读取文件时在读到文件结束标志
EOF
时将会终止进程;cat < log.txt
而在这一行命令当中,使用重定向将标准输入文件流重定向至
log.txt
文件当中;对应的将会直接读取
log.txt
文件内的信息;$ cat < log.txt log.txt makefile mytest test.c log.txt makefile mytest test.c
对应的当
cat
读取到log.txt
文件中的文件结束标志EOF
时将会停止打印;
🧸 实现重定向的底层机制
在上文当中提到了重定向在日常使用中的简单例子,同时也讲了关于输出重定向与追加重定向实际上只是与底层中的打开文件的方式不同;
存在两段代码:
-
存在一段代码
#define FILENAME "log.txt" int main() { const char* massage = "hello world\n"; close(1); int fd = open(FILENAME, O_CREAT | O_TRUNC | O_WRONLY, 0666); if(fd<0){ perror("open fail"); } ssize_t wret = write(1, massage, strlen(massage)); if(wret<0){ perror("write fail"); } close(fd); return 0; }
在这段代码当中设置了一个
message
字符串其内容为hello world\n
;并关闭文件描述符
1
;在以写入的方式将
massage
中的内容写入至文件描述符1
当中;当执行该程序时,对应的信息并没有被打印到显示器当中;
而在此时使用
cat
显示对应的log.txt
文件时对应的显示出massage
当中的信息;$ cat log.txt hello world
从上述代码以及最终结果中可以观察到,原本需要被打印在显示屏当中的信息最终被写入至了文件log.txt
当中;
原因与上文当中所提到的文件描述符的分配有关;
- 当打开一个新文件时,将会为这个文件分配一个最小的且未被使用的文件描述符;
故当关闭文件描述符1
后表示该文件描述符并未被占用;
而在此时打开一个新的文件时将为该文件分配文件描述符1
;
而在进行写入操作时进程并不知道其本身的文件描述符已经被替换而仍然继续使用该文件描述符操作;
这样的现象可以理解为这种抽象层为写入操作和文件管理进行了解耦合;
而此时将代码当中的open()
的打开选项中的O_TRUNC
换成O_APPEND
并再次运行该程序;
int fd = open(FILENAME, O_CREAT | O_APPEND | O_WRONLY, 0666);
由于在上面的操作当中log.txt
文件当中已经存在了内容;
而这时将修改后的代码重新编译并运行;
$ cat log.txt
hello world
hello world
从结果可以看出结果与上述的代码结果没有太大区别;
唯一的区别就是由于打开文件的方式选项不同而该段代码采用追加的方式打开文件故在进行写入的时候将以追加的方式进行写入;
与上文中的重定向指令的结果进行比较可以看出实际上两者别无二致;
实际上也是如此,Shell
当中重定向的底层实际上就是替换对应的文件描述符从而使得其能够将原本需要显示在显示器上的内容写入至文件当中;
🐡 dup2()
在上文当中提到了重定向大致的底层机制;
那么若是以这种方式实现重定向是否过于复杂,即需要将先将文件描述符进行关闭再打开一个新文件;
- 是否存在更加便捷的方式使得能够修改文件描述符的指向?
在<unistd.h>
头文件当中存在这样的接口,即dup()
,dup2()
,dup3()
;
其中dup2()
较为常用;
NAME
dup, dup2, dup3 - duplicate a file descriptor
SYNOPSIS
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);
|
dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
* If oldfd is not a valid file descriptor, then the call
fails, and newfd is not closed.
* If oldfd is a valid file descriptor, and newfd has the
same value as oldfd, then dup2() does nothing, and
returns newfd.
RETURN VALUE
On success, these system calls return the new descriptor. On
error, -1 is returned, and errno is set appropriately.
-
从
man
手册可以看出其对dup2()
的解释即使
newfd
称为oldfd
的副本并关闭newfd
;但是在传参的时候需要注意;
若是
oldfd
不为有效的文件描述符,则调用失败且不关闭newfd
;若是
oldfd
与newfd
相同,dup2()
则将不进行操作,并且返回newfd
;当调用成功时将会返回保留下的文件描述符,即
oldfd
;若是调用失败则会返回
-1
并设置error
;
在调用dup2()
需要注意函数中的参数;
以正常的理解来说oldfd
,newfd
既然需要进行覆盖那一半是newfd
去覆盖oldfd
;
而真正在手册当中的解释是makes newfd be the copy of oldfd, closing newfd first if necessary
;
即使oldfd
覆盖newfd
的内容;
主打一个狸猫换太子;
最终两个作为参数传入的文件描述符将只剩下oldfd
;
同时这里的覆盖并不是将文件描述符进行覆盖,已知文件描述符实际上是文件描述符表(指针数组)的一个下标,而下标是常量是不能被进行修改的;
故这里的覆盖指的是将指针数组中下标对应的元素(指针)进行覆盖从而达到改变其指向的效果;
🐡 利用dup2()实现重定向
-
输出重定向
存在一段代码:
#define FILENAME "log.txt" int main() { const char* massage = "hello massage\n"; int fd = open(FILENAME, O_WRONLY | O_TRUNC | O_CREAT, 0666); dup2(fd, 1); write(1, massage, strlen(massage)); //省略了调用失败的错误处理 close(fd); return 0; }
在这段代码当中定义了一个内容为
hello massage\n
的字符串massage
;并以 只读 的方式将文件进行打开;
在打开文件过后调用dup2()
函数从而实现文件描述符1
拷贝新打开的文件描述符的指向从而指向新打开的文件;
当调用dup2()
后再向向文件描述符1
中写入数据时对应的文件描述符1
已经指向了log.txt
文件当中,故原本应写至显示器当中的信息被写入至了log.txt
文件当中;
$ cat log.txt
hello massage
该操作即为输出重定向;
对于追加重定向而言,其一样是将对应的将文件描述1
重定向至新打开的文件当中,只是打开文件的方式不同(以O_APPEND
追加的方式打开文件),即open(FILENAME, O_WRONLY | O_APPEND | O_CREAT, 0666)
在此不作赘述;
-
输入重定向
存在一段代码
#define FILENAME "log.txt" //在FILENAME文件存在且当中存在信息的前提下 int main() { char buf[1024]; int fd = open(FILENAME, O_RDONLY); dup2(fd, 0); int len = read(0,buf,sizeof(buf)); // 省略了调用失败的错误处理 buf[len] = '\0'; printf("%s\n", buf); close(fd); return 0; }
在这段代码当中,已知
FILENAME
文件存在且文件内存在信息;并定义了一个
buf[]
数组作为缓冲区用于存储read()
接口从文件当中获取的信息;使用
read()
读取数据并存放至缓冲区buf
当中最后将其打印出来;
该段代码的实现方式与输出重定向的机制相同;
都是使用dup2()
接口将文件描述符进行重定向;
在正常的情况中(未调用dup2()
的前提下);
由于文件描述符0
指向的文件为键盘设备,故当执行程序时对应的进程将进入阻塞状态并且等待用户从键盘输入信息;
但由于使用了dup2()
接口函数从而将文件描述符0
重定向至了log.txt
文件当中;
故在这段程序当中将从log.txt
文件当中读取数据并放置缓冲区当中接而打印;
该操作即为输入重定向;
在上文当中了解到,实际上dup2()
接口并不是进行替换,而是将对应的文件描述符当中的内容进行拷贝覆盖;
那么在这种情况下;
- 是否会造成占用过多的文件描述符从而导致文件描述符枯竭?
答案是在正常的情况下使用dup2()
进行重定向操作时并不会导致过多占用文件描述符从而导致文件描述符枯竭;
而若是调用了过多次dup2()
且并未将原先的文件描述符关闭,由于dup2()
操作并不是将文件描述符进行替换而是进行拷贝覆盖,原先的文件描述符将持续存在于文件描述符表当中;
当到达一定限制时,dup2()
的调用将会因为文件描述符枯竭而调用失败;
故在一般情况下若是使用dup2()
接口后需要使用close()
将冗余的文件描述符进行关闭从而防止文件描述符枯竭的问题;
🧸 在自定义Shell当中添加重定向功能
在博客『 Linux 』进程替换( Process replacement ) 及 简单Shell的实现(万字)当中实现了一个简易的Shell
;
但总体来说,这个简易的Shell
写的并不好;
无论是从代码结构,可读性,拓展性等等;
-
在此更新一个版本:
#include <assert.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #define LEFT "[" #define RIGHT "]" #define LINE_SIZE 1024 // 命令行缓冲区大小 #define ARGV_SIZE 32 #define DELIM " \t" #define EXIT_CODE 33 // 获取环境变量为标识符 /* [ USER@HOSTNAME PWD ]label */ int lastcode = 0; int quit = 0; char commandline[LINE_SIZE]; char* argv[ARGV_SIZE]; char pwd[LINE_SIZE]; char currvariables[LINE_SIZE]; const char* getUsername() { return getenv("USER"); } const char* getHostname() { return getenv("HOSTNAME"); } void getpwd() { // return getenv("PWD"); getcwd(pwd, sizeof(pwd)); } void interact(char* cline, size_t size) { getpwd(); printf(LEFT "%s@%s %s" RIGHT "# ", getUsername(), getHostname(), pwd); char* s = fgets(cline, size, stdin); // 从键盘获取 size_t len = strlen(cline); if (len > 1) cline[len - 1] = '\0'; assert(s); (void)s; } int splitstring(char* cline, char** argv) { int i = 0; argv[i++] = strtok(cline, DELIM); while ((argv[i++] = strtok(NULL, DELIM))) { ; } return i - 1; } void normalExecute(char** _argv) { // pid_t id = fork(); if (id < 0) { // perror("fork error"); return; } else if (id == 0) { // 子进程执行程序 execvp(_argv[0], _argv); exit(EXIT_CODE); // 进程替换失败 } else { // 父进程等待子进程 int status = 0; int ret = waitpid(id, &status, 0); // 等待成功返回pid 失败则返回-1 if (ret == id) { lastcode = WEXITSTATUS(status); } } } int buildCommand(int _argc, char** _argv) { getpwd(); if (_argc == 2 && (strcmp(_argv[0], "cd") == 0)) { int dirret = chdir(_argv[1]); if (dirret != 0) { // 说明chdir调用失败 perror("chdir error\n"); } else { sprintf(getenv("PWD"), "%s", pwd); } return 1; } else if (_argc == 2 && strcmp(_argv[0], "export") == 0) { // 两种方法都适用 // ① /* char* _name = strtok(_argv[1],"="); char* _value = strtok(NULL, ""); if(_name&&_value){ setenv(_name, _value, 1); //使用putenv的话只是单纯的将字符串的指针写在了环境变量表之中 //而argv当中的数据是不停变化的 // 故若是使用putenv的话由于argv的内容在不停变化 //则会使这个环境变量表的指针指向一个无效的位置 //故要么将这个环境变量表的指针单独存放一个位置 要么就使用setenv }*/ // ② strcpy(currvariables, _argv[1]); putenv(currvariables); return 1; } else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) { if (strcmp(_argv[1], "$?") == 0) { printf("%d\n", lastcode); lastcode = 0; } else if (_argv[1][0] == '$') { // 说明需要打印的是环境变量 char* val = getenv(_argv[1] + 1); if (val) printf("%s\n", val); } else { // 说明不属于内建命令可以直接打印 printf("%s\n", _argv[1]); } return 1; } else if (strcmp(_argv[0], "ls") == 0) { _argv[_argc++] = "--color"; _argv[_argc] = NULL; } return 0; } int main() { while (!quit) { // 获取命令行信息 interact(commandline, sizeof(commandline)); // debug 打印测试输入信息是否正确 printf("echo: %s\n", commandline); // 分割命令行信息 int argc = splitstring(commandline, argv); // 分割字符串 if (argc == 0) continue; /* 如果未进行判断的话 将会发生下列问题: 当只输入分隔符的情况下数据将不被分割 对应的argv当中则只有NULL 由于没有中断此次循环将会继续向下执行 而在接下来的执行当中 程序将期望存在一个有效的命令 然而argv[0]为NULL 将会进行报错 */ // //用于debug 对指针数组进行打印 // for (int i = 0; argv[i] != NULL; ++i) { // printf("%s\n", argv[i]); // } // 内建命令 int flag = buildCommand(argc, argv); // // 分割命令后将对命令进行处理 if (!flag) normalExecute(argv); } return 0; }
在这个
Shell
当中以不同接口函数的方式将功能进行分割从而提升其模块化使其更容易理解与维护;同时在该程序当中对部分 内建命令 进行了特殊处理;
在该节当中重点更新该程序当中的重定向接口;
同时对于重定向接口重点对通常命令进行处理,即代码当中的
normalExecute()
;
既然是重定向,那么则需要判断命令当中是否存在重定向符号>
,>>
或<
;
// 以下的几个宏表示重定向的属性
#define IN_RDIR 0 // 输入重定向
#define OUT_RDIR 1 // 输出重定向
#define APPEND_RDIR 2 // 追加重定向
#define NONE -1 // 无属性 即非重定向
char* rdirfilename = NULL; // 文件名
int rdir = NONE; // rdir默认为-1 表示非重定向
void check_redir(char* cmd) {
// 检查字符串是否存在重定向
char* pos = cmd;
while (*pos) {
if (*pos == '>') {
// 输出重定向'>' 或是追加重定向 '>>'
if (*(pos+1) == '>') {
/* 追加重定向 */
// 追加重定向>>
*pos++ = '\0';
*pos++ = '\0';
while (isspace(*pos)) {
pos++;
}
rdirfilename = pos;
// 将全局变量中的文件名设置为pos字符串
rdir = APPEND_RDIR;
// 追加重定向
break;
}
*pos++ = '\0';
while (isspace(*pos)) {
pos++;
}
rdirfilename = pos;
// 将全局变量中的文件名设置为pos字符串
rdir = OUT_RDIR;
// 输出重定向
break;
} else if (*pos == '<') {
// 输出重定向
*pos++ = '\0';
while (isspace(*pos)) {
pos++;
}
rdirfilename = pos;
// 将全局变量中的文件名设置为pos字符串
rdir = IN_RDIR;
// 输入重定向
break;
} else {
// 非重定向
// do nothing
}
pos++;
}
}
使用#define
为重定向操作选项定义几个宏;
-
#define IN_RDIR 0
该选项表示输入重定向
-
#define OUT_RDIR 1
该选项表示输出重定向
-
#define APPEND_RDIR 2
该选项表示追加重定向
-
#define NONE -1
该选项表示不存在重定向
并定义了两个变量为char* rdirfilename = NULL
,int rdir = NONE
;
其中rdirfilename
表示重定向当中的文件名;
而rdir
表示是否存在重定向的选项;
在判断是否重定向时采用将用户输入的命令从右向左进行遍历的方式;
当判断到存在重定向操作时需要将用户输入的命令中的重定向符号>
,>>
,<
修改为\0
并设置当前重定向的状态(是否需要重定向);
而在该程序当中重点对通常命令进行重定向的更新故需要再normalExecute()
接口当中设置当判断为重定向时具体的操作;
即根据文件描述符的属性并调用dup2()
重定向对应的文件描述符;
-
且需要注意
由于程序当中的
rdirfilename
与rdir
属于全局变量;为了确保只有当真正存在重定向操作时才进行对应操作时需要在
main()
当中或是在check_rdir()
当中将这两个变量置空; -
整体代码演示(供参考)
#include <assert.h> #include <ctype.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #define LEFT "[" #define RIGHT "]" #define LINE_SIZE 1024 // 命令行缓冲区大小 #define ARGV_SIZE 32 #define DELIM " \t" #define EXIT_CODE 33 // 以下的几个宏表示重定向的属性 #define IN_RDIR 0 // 输入重定向 #define OUT_RDIR 1 // 输出重定向 #define APPEND_RDIR 2 // 追加重定向 #define NONE -1 // 无属性 即非重定向 char* rdirfilename = NULL; // 文件名 int rdir = NONE; // rdir默认为-1 表示非重定向 int lastcode = 0; int quit = 0; char commandline[LINE_SIZE]; char* argv[ARGV_SIZE]; char pwd[LINE_SIZE]; char currvariables[LINE_SIZE]; const char* getUsername() { return getenv("USER"); } const char* getHostname() { return getenv("HOSTNAME"); } extern int buildCommand(int _argc, char** _argv); void getpwd() { // return getenv("PWD"); getcwd(pwd, sizeof(pwd)); } void check_redir(char* cmd) { // 检查字符串是否存在重定向 char* pos = cmd; while (*pos) { if (*pos == '>') { // 输出重定向'>' 或是追加重定向 '>>' if (*(pos + 1) == '>') { /* 追加重定向 */ // 追加重定向>> *pos++ = '\0'; *pos++ = '\0'; while (isspace(*pos)) { pos++; } rdirfilename = pos; // 将全局变量中的文件名设置为pos字符串 rdir = APPEND_RDIR; // 追加重定向 break; } *pos++ = '\0'; while (isspace(*pos)) { pos++; } rdirfilename = pos; // 将全局变量中的文件名设置为pos字符串 rdir = OUT_RDIR; // 输出重定向 break; } else if (*pos == '<') { // 输出重定向 *pos++ = '\0'; while (isspace(*pos)) { pos++; } rdirfilename = pos; // 将全局变量中的文件名设置为pos字符串 rdir = IN_RDIR; // 输入重定向 break; } else { // 非重定向 // do nothing } pos++; } } void interact(char* cline, size_t size) { // 获取环境变量为标识符 /* [ USER@HOSTNAME PWD ]label */ getpwd(); printf(LEFT "%s@%s %s" RIGHT "# ", getUsername(), getHostname(), pwd); char* s = fgets(cline, size, stdin); // 从键盘获取 size_t len = strlen(cline); if (len > 1) cline[len - 1] = '\0'; assert(s); (void)s; check_redir(cline); return; } int splitstring(char* cline, char** argv) { int i = 0; argv[i++] = strtok(cline, DELIM); while ((argv[i++] = strtok(NULL, DELIM))) { ; } return i - 1; } void normalExecute(char** _argv) { // pid_t id = fork(); if (id < 0) { // perror("fork error"); return; } else if (id == 0) { // 子进程执行程序 int fd = 0; int dupret = 0; // 判断是否存在重定向 即rdir是否具有参数 (非NONE) if (rdir == IN_RDIR) { fd = open(rdirfilename, O_RDONLY); dupret = dup2(fd, 0); } else if (rdir == OUT_RDIR) { fd = open(rdirfilename, O_WRONLY | O_CREAT | O_TRUNC, 0666); dupret = dup2(fd, 1); } else if (rdir == APPEND_RDIR) { fd = open(rdirfilename, O_WRONLY | O_CREAT | O_APPEND, 0666); dupret = dup2(fd, 1); } else { // rdir == NONE } if (fd < 0) { perror("open fail\0"); return; } if (dupret < 0) { perror("dup fail\0"); return; } execvp(_argv[0], _argv); exit(EXIT_CODE); // 进程替换失败 } else { // 父进程等待子进程 int status = 0; int ret = waitpid(id, &status, 0); // 等待成功返回pid 失败则返回-1 if (ret == id) { lastcode = WEXITSTATUS(status); } } } int buildCommand(int _argc, char** _argv) { getpwd(); if (_argc == 2 && (strcmp(_argv[0], "cd") == 0)) { int dirret = chdir(_argv[1]); if (dirret != 0) { // 说明chdir调用失败 perror("chdir error\n"); } else { sprintf(getenv("PWD"), "%s", pwd); } return 1; } else if (_argc == 2 && strcmp(_argv[0], "export") == 0) { // 两种方法都适用 // ① /* char* _name = strtok(_argv[1],"="); char* _value = strtok(NULL, ""); if(_name&&_value){ setenv(_name, _value, 1); //使用putenv的话只是单纯的将字符串的指针写在了环境变量表之中 //而argv当中的数据是不停变化的 // 故若是使用putenv的话由于argv的内容在不停变化 //则会使这个环境变量表的指针指向一个无效的位置 //故要么将这个环境变量表的指针单独存放一个位置 要么就使用setenv }*/ // ② strcpy(currvariables, _argv[1]); putenv(currvariables); return 1; } else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) { if (strcmp(_argv[1], "$?") == 0) { printf("%d\n", lastcode); lastcode = 0; } else if (_argv[1][0] == '$') { // 说明需要打印的是环境变量 char* val = getenv(_argv[1] + 1); if (val) printf("%s\n", val); } else { // 说明不属于内建命令可以直接打印 printf("%s\n", _argv[1]); } return 1; } else if (strcmp(_argv[0], "ls") == 0) { _argv[_argc++] = "--color"; _argv[_argc] = NULL; } return 0; } int main() { rdir = NONE; rdirfilename = NULL; while (!quit) { // 获取命令行信息 interact(commandline, sizeof(commandline)); // debug 打印测试输入信息是否正确 printf("echo: %s\n", commandline); // 分割命令行信息 int argc = splitstring(commandline, argv); // 分割字符串 if (argc == 0) continue; /* 如果未进行判断的话 将会发生下列问题: 当只输入分隔符的情况下数据将不被分割 对应的argv当中则只有NULL 由于没有中断此次循环将会继续向下执行 而在接下来的执行当中 程序将期望存在一个有效的命令 然而argv[0]为NULL 将会进行报错 */ // //用于debug 对指针数组进行打印 // for (int i = 0; argv[i] != NULL; ++i) { // printf("%s\n", argv[i]); // } // 内建命令 int flag = buildCommand(argc, argv); // 重定向暂不考虑内建命令 // 重定向只修改对应的通常命令 // // 分割命令后将对命令进行处理 if (!flag) normalExecute(argv); // 重定向将在该接口内进行处理 } return 0; }
🐡 重定向与进程替换
在上文当中已经完成了shell
当中重定向的接口,在此不作赘述;
那么思考一个问题:
- 进程替换
execl
接口为什么不会影响已经完成的dup2()
重定向操作?
以该图为例;
进程本身就是在磁盘当中的一个可执行文件通过运行从而产生的;
当运行后对应的操作系统将会为了维护这个进程而创建对应的PCB
结构体,如图所示即为其中的task_struct
;
而其中的task_struct
当中存在着一个struct file_struct*files
指针;
这个指针指向了一个名为struct file_struct
结构体;
在这个结构体当中存在着一个指针数组,即struct file*fd_array[]
,也就是在上篇博客当中提到的文件描述符表;
其中文件描述符表当中的每个元素(指针)都指向了一个file
结构体,每打开一个文件对应的OS
将为该文件创建对应的该结构体并进行管理;
上述的这一块的内容在OS
当中被称作 文件管理 ;
mm_struct
,页表,磁盘等被称作 内存管理 ;
而在 『 Linux 』进程替换( Process replacement ) 及 简单Shell的实现(万字)中提到,进程在进行进程替换的时候并不是创建一个新的进程,而是当进程替换成功时将原本进程当中的代码与数据与替换的代码数据进行替换;
这意味着是同一个容器而容器当中的数据是不同的;
新的代码和是数据将继续延用原本的进程;
也意味着由于并没有创建新的进程故并不会将原本进程当中设置的重定向操作进行恢复或者重置;
而也是这种设定也使得Linux
中的 文件管理 与 内存管理 之间的解耦合;
🧸 标准输出重定向与标准错误重定向
在上文当中提到了输出重定向也分为了 标准输出重定向 与 标准错误重定向 ;
根据名词即可以了解,标准输出 与 标准错误 实际上也仅仅是文件描述符的不同;
- 那么什么是标准错误重定向?
存在一段代码:
int main() {
fprintf(stdout, "normal stdout\n");
fprintf(stdout, "normal stdout\n");
fprintf(stdout, "normal stdout\n");
fprintf(stderr, "error stderr\n");
fprintf(stderr, "error stderr\n");
fprintf(stderr, "error stderr\n");
return 0;
}
在这段代码当中调用了fprintf()
分别使用stdout
与stderr
作为参数打印出normal stdout\n
与error stderr\n
;
当运行程序后结果将与预期相同分别打印出对应的消息至显示器当中;
$ ./mytest
normal stdout
normal stdout
normal stdout
error stderr
error stderr
error stderr
而若是运行该程序并将对应的信息重定向至一个名为noraml.txt
文件当中会发生什么?
$ ./mytest > normal.txt
error stderr
error stderr
error stderr
$ cat normal.txt
normal stdout
normal stdout
normal stdout
从结果来看,当使用重定向将打印出来的信息写入至normal.txt
文件时;
对应的error stderr\n
并未被写入至normal.txt
文件当中;
原因即为重定向>
与>>
的机制是将文件描述符1
重定向至文件当中;
而对应的stderr
对应的文件描述符为2
,故不能将其重定向至normal.txt
文件当中;
而实际上对应的标准错误也有重定向,即为2>
与2>>
;
其与标准输出重定向大致相同,唯一不同的只是文件描述符不同;
若是使用2>
与2>>
将输出的内容重定向至error.txt
文件当中时;
$ ./mytest 2> error.txt
normal stdout
normal stdout
normal stdout
$ ./mytest 2>> error.txt
normal stdout
normal stdout
normal stdout
$ cat error.txt
error stderr
error stderr
error stderr
error stderr
error stderr
error stderr
从结果来看与>
不同,当使用标准错误重定向时对应的文件描述符1
所打印的内容将不会被写入至error.txt
文件当中;
其底层原理与标准输出重定向相同;
而实际上标准输出重定向其符号为1>
与1>>
;
只不过在使用时其1
可以进行省略;
一般而言文件描述符2
是用来输出错误的内容,而若是需要将一个程序当中的正常输出与错误内容进行区分成两个文件时则可以使用:
./a.out > NORMALFILENAME 2> ERRORFILENAME #./a.out 1> NORMALFILENAME 2> ERRORFILENAME
-
以上面的程序为例:
$ ./mytest > normal.txt 2> error.txt $ cat normal.txt normal stdout normal stdout normal stdout $ cat error.txt error stderr error stderr error stderr
当使用该命令时对应的将根据不同的文件描述符分别重定向到不同的文件当中;
而若是需要将不同文件描述符对应输出的内容写入同一个文件则可以使用:
./a.out > ALLFILENAME 2>&1 #./a.out 1> ALLFILENAME 2>&1
-
以上面的程序为例
$ ./mytest 1> all.txt 2>&1 $ cat all.txt error stderr error stderr error stderr normal stdout normal stdout normal stdout
以这个例子为例,即表示将文件描述符1
重定向到文件all.txt
当中;
然后将文件描述符2
重定向到已经指向all.txt
的文件描述符1
当中;
但这里的文件描述符的重定向与上文相同,并不是将文件描述符进行重定向(文件描述符为下标是常量),而是利用dup
接口进行实现以至于改变指针的指向;