在上面的代码中,fprintf本来是向stdout中打印的,但是stdout关闭了,实际上fprintf事项fd是1的文件中打印,这里log.txt的fd就是1;
运行结果为:
这就叫做输出重定向;
上面的代码将stdout关闭了,并打开log.txt文件,则log.txt文件的fd就是1;
在系统中,stdout就代表着fd为1,所以默认就会向fd为1的文件中打印,而此时fd为1的文件是log.txt,因此就向该文件中打印了;
oldfd copy to the newfd -> 最后要和oldfd一样
最终重定向的fd要是3,dup2的运行结果是newfd和oldfd一样,因此这里3是oldfd,1是newfd;
在进程控制章节我们自己写了shell程序,这里我们在其中添加重定向功能;
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024
#define SIZE 32
#define SEP " "
//保存完整的命令行字符串
char cmd_line[NUM];
//保存打散之后的命令行字符串
char* g_argv[SIZE];
//用于保存环境变量,使其不被刷新覆盖
char g_myval[64];
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define NONE_REDIR 0
int redir_status = NONE_REDIR;
char* CheckRedir(char* start)
{
assert(start);
char* end = start + strlen(start) - 1;
while(end >= start)
{
if(*end == '>')
{
if(*(end - 1) == '>')//重定向类型是>> 追加
{
redir_status = APPEND_REDIR;
*(end - 1) = '\0';
end++;
break;
}
redir_status = OUTPUT_REDIR;
*end = '\0';
end++;
break;
}
else if(*end == '<')
{
redir_status = INPUT_REDIR;
*end = '\0';
end++;
break;
}
else
{
end--;
}
}
if(end >= start)
{
//有重定向
return end;//返回要打开的文件
}
else
{
return NULL;
}
}
//shell运行原理,让子进程执行命令,父进程等待&&解析命令
int main()
{
extern char** environ;//使用父进程的环境变量,可以通过main函数的参数 ,也可以导入environ指针
//命令行解释器:一定是一个常驻内存的进程,不退出
while(1)
{
//1.打印出提示信息 [lmx@localhost myshell]#
printf("[lmx@localhost myshell]# ");
fflush(stdout);//由于printf没有加\n,不刷新缓冲区,使用fflush刷> 新
memset(cmd_line, '\0', sizeof cmd_line);//sizeof可以不使用括号
//2.获取用户的键盘输入,输入的是各种指令和选项:"ls -a -l"
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
{
continue;
}
cmd_line[strlen(cmd_line) - 1] = '\0';//去掉输入时的\n
//2.1 分析是否有重定向
//"ls -a -l\n\0"
char* sep = CheckRedir(cmd_line);
//原理:从后往前检查命令字符串,发现有">, <, >>"的,就将该字符所 在位置变为\0,能够将命令分为两段,左边是命令, 右边是文件
//3.命令行字符串解析:"ls -a -l" -> "ls" "-a" "-l"
g_argv[0] = strtok(cmd_line, SEP);//第一次调用,要传入原始字符串
int index = 1;
if(strcmp(g_argv[0], "ls") == 0)
{
g_argv[index++] = "--color=auto";
}
if(strcmp(g_argv[0], "ll") == 0)
{
g_argv[0] = "ls";
g_argv[index++] = "-l";
g_argv[index++] = "--color=auto";
}
while(g_argv[index++] = strtok(NULL, SEP));//第二次,如果还要继> 续解析原始字符,传入NULL
//导入环境变量
if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
{
strcpy(g_myval, g_argv[1]);//将环境变量保存到全新字符串中,> 不让它被下一个指令刷新,以至于子进程拿不到环境变量
int ret = putenv(g_myval);
if(ret == 0)
{
printf("%s export success\n", g_argv[1]);
}
continue;
}
//4.TODO:内置命令,让父进程(shell)自己执行的命令,叫做内置命令 ,内建命令
//内置命令本质是shell中的一个函数调用
if(strcmp(g_argv[0], "cd") == 0)//如果命令是cd,改变工作目录,需 要在父进程实现
//子进程的cd变换的只是子进程的路 径,父进程不会变
{
if(g_argv[1] != NULL)
{
chdir(g_argv[1]);//改变工作目录
}
continue;
}
//5.fork()
pid_t id = fork();
if(id == 0)//child
{
if(sep != NULL)
{
int fd = -1;
switch(redir_status)
{
case INPUT_REDIR:
fd = open(sep, O_RDONLY);
dup2(fd, 0);
break;
case OUTPUT_REDIR:
fd = open(sep, O_WRONLY | O_TRUNC | O_CREAT, 066 6);
dup2(fd, 1);
break;
case APPEND_REDIR:
fd = open(sep, O_WRONLY | O_CREAT | O_APPEND, 06 66);
dup2(fd, 0);
break;
default:
printf("BUG?\n");
break;
}
}
// printf("child, MYVAL: %s\n", getenv("MYVAL"));
// printf("child, PATH: %s\n", getenv("PATH"));
execvp(g_argv[0], g_argv);
exit(1);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);//阻塞等待
if(ret > 0)
{
printf("exit code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
这是因为:1和2号fd对应的都是显示器文件,但是是不同的,可以认为是同一个显示器文件被打开了两次;
因此重定向后只有stdout的内容写入了log.txt,而stderr的内容依然打印到了屏幕上;
上面的代码中2号fd 的文件无法重定向到普通文件中,经过如下操作:
执行了这条语句后,1号和2号fd的文件内容都重定向到了log.txt中;
其中2>&1的意思是把1的地址拷贝给2,则2也指向1的显示器文件了,1和2指向的是同一个显示器文件;
文件拷贝:
这条指令的意思是先将log.txt的内容重定向输入给cat打印出来,再将打印的结果重定向到back.txt,就相当于把log.txt的内容拷贝给back.txt;
perror是会打印出错误信息的,这是因为函数中使用了strerror接口,来打印错误信息;
所有的Linux文件结构体中都会有读函数和写函数的指针;
虽然底层不同的硬件,一定对应的是不同的操作方法;
但是上面的设备都是外设,每一个设备的核心访问函数都可以是read、write,每一个文件中的读写函数指针都可以指向这两个函数;
读写代码的实现是不一样的,但是在操作系统看来,都是读写,没有任何硬件的差别了;
因此,Linux下一切皆文件;
这种现象与缓冲区有关;
一般而言:
所有的设备,永远都倾向于全缓冲;缓冲区满了,才刷新,这样就需要更少次数的IO操作,更少次的外设访问,能够提高效率;
和外部设备IO的时候,数据量的大小不是主要矛盾,和外设预备IO的过程是最耗费时间的;
显示器,是要给用户看的,一方面要照顾效率,一方面还要照顾用户体验;
上面的代码,并不影响系统接口,如果有缓冲区,那这个缓冲区一定是由C标准库维护的,因为如果是由OS维护的,那上面的代码应该都是一样的效果;
FILE结构体中不仅封装了文件描述符fd,也封装了该文件fd对应的语言层缓冲区结构;
C语言中打开的FILE文件流,必须包含:
#include
#include
#include
#include
#include
#include
#include
#include
#define NUM 1024
struct MyFILE_
{
int fd;//文件描述符
char buffer[NUM];//缓冲区
int end;//当前缓冲区的结尾
};
typedef struct MyFILE_ MyFILE;
MyFILE* fopen_(const char* pathname, const char* mode)
{
assert(pathname);
assert(mode);
MyFILE* fp = NULL;
if(strcmp(mode, "r") == 0)
{
}
else if(strcmp(mode, "r+") == 0)
{
}
else if(strcmp(mode, "w") == 0)
{
int fd = open(pathname, O_WRONLY | O_TRUNC | O_CREAT, 0666);
if(fd >= 0)
{
fp = (MyFILE*)malloc(sizeof(MyFILE));
memset(fp, 0, sizeof(MyFILE));
fp->fd = fd;
}
}
else if(strcmp(mode, "w+") == 0)
{
}
else if(strcmp(mode, "a") == 0)
{
}
else if(strcmp(mode, "a+") == 0)
{
}
else
{
}
return fp;
}
void fputs_(const char* message, MyFILE* fp)
{
assert(message);
assert(fp);
strcpy(fp->buffer + fp->end, message);
fp->end += strlen(message);
//暂时没有刷新,刷新策略是用户通过执行C标准库中的代码逻辑,来完成刷新>动作
//这里效率提高,因为C提供了缓冲区,我们可以通过刷新策略,较少了IO的执>行次数
if(fp->fd == 0)
{
//标注输入
}
else if(fp->fd == 1)
{
//标准输出
if(fp->buffer[fp->end - 1] == '\n')//如果缓冲区数据最后以\n结尾,>就立即刷新
{
write(fp->fd, fp->buffer, fp->end);
fp->end = 0;
}
}
else if(fp->fd == 2)
{
//标准错误
}
else
{
//其他文件
}
}
void fflush_(MyFILE* fp)
{
assert(fp);
if(fp->end != 0)
{
write(fp->fd, fp->buffer, fp->end);//将数据写入内核
syncfs(fp->fd);//将输入写入磁盘
fp->end = 0;
}
}
void fclose_(MyFILE* fp)
{
assert(fp);
fflush_(fp);
close(fp->fd);
free(fp);
}
int main()
{
MyFILE* fp = fopen_("./log.txt", "w");
if(fp == NULL)
{
perror("open file error");
return 1;
}
fputs_("lmx uio", fp);
fork();
fclose_(fp);
return 0;
}