Linux知识点 – 基础IO(二)
文章目录
- Linux知识点 -- 基础IO(二)
- 一、重定向
- 1.输出重定向
- 2.输入重定向
- 3.追加重定向
- 4.重定向系统调用
- 5.minishell支持重定向
- 6.stdout和stderr的区别
- 7.常规的重定向操作
- 8.perror的实现
- 二、Linux下一切皆文件
- 三.缓冲区
- 1.缓冲区
- 2.关于缓冲区的认识
- 3.用户缓冲区与内核缓冲区
- 4.用户缓冲区的位置
- 5.自己设计用户缓冲区
一、重定向
1.输出重定向
在上面的代码中,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,因此就向该文件中打印了;
2.输入重定向
运行结果:
3.追加重定向
运行结果:
4.重定向系统调用
oldfd copy to the newfd -> 最后要和oldfd一样
最终重定向的fd要是3,dup2的运行结果是newfd和oldfd一样,因此这里3是oldfd,1是newfd;
-
使用dup2实现输出重定向:
运行结果:
输出重定向到了log.txt中; -
使用dup2实现追加重定向:
5.minishell支持重定向
在进程控制章节我们自己写了shell程序,这里我们在其中添加重定向功能;
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <assert.h>
#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;
}
6.stdout和stderr的区别
上面的代码分别向stdout和stderr文件写入了字符;
- 直接运行的结果是所有字符全部打印到显示器上,说明stdout和stderr都对应的是显示器文件;
- 如果将打印的结果重定向到log.txt中,结果会发生变化,并不是所有的字符都写入了log.txt;
可以看出:重定向过后,只有向1号fd中写的内容被重定向写入到文件中,2号fd的内容依然打印在显示器上;
这是因为:1和2号fd对应的都是显示器文件,但是是不同的,可以认为是同一个显示器文件被打开了两次;
因此重定向后只有stdout的内容写入了log.txt,而stderr的内容依然打印到了屏幕上;
7.常规的重定向操作
-
上面的代码中2号fd 的文件无法重定向到普通文件中,经过如下操作:
执行了这条语句后,1号和2号fd的文件内容都重定向到了log.txt中;
其中2>&1的意思是把1的地址拷贝给2,则2也指向1的显示器文件了,1和2指向的是同一个显示器文件; -
文件拷贝:
这条指令的意思是先将log.txt的内容重定向输入给cat打印出来,再将打印的结果重定向到back.txt,就相当于把log.txt的内容拷贝给back.txt;
8.perror的实现
perror是会打印出错误信息的,这是因为函数中使用了strerror接口,来打印错误信息;
二、Linux下一切皆文件
所有的Linux文件结构体中都会有读函数和写函数的指针;
虽然底层不同的硬件,一定对应的是不同的操作方法;
但是上面的设备都是外设,每一个设备的核心访问函数都可以是read、write,每一个文件中的读写函数指针都可以指向这两个函数;
读写代码的实现是不一样的,但是在操作系统看来,都是读写,没有任何硬件的差别了;
因此,Linux下一切皆文件;
三.缓冲区
1.缓冲区
- 由上可知:使用dup2进行输出重定向时,运行程序后,使用cat指令打印log.txt能够直接打印出来;
- 如果使用系统指令进行重定向:
直接打印是打印不出来的;
如果在输出重定向后加上fflush刷新缓冲区,就可以将内容输出到log.txt了:
这种现象与缓冲区有关;
- 缓冲区:就是一段内存空间;
- 缓冲区的存在主要是为了提高整机效率,提高用户的响应速度;
- 缓冲区的刷新策略主要有:
(1)立即刷新;
(2)行刷新(行缓冲 \n)
(3)满刷新(全缓冲)
特殊情况:
(1)用户强制刷新(fflush)
(2)进程退出
2.关于缓冲区的认识
一般而言:
- 行缓冲的设备文件 – 显示器
- 全缓冲的设备文件 – 磁盘文件
所有的设备,永远都倾向于全缓冲;缓冲区满了,才刷新,这样就需要更少次数的IO操作,更少次的外设访问,能够提高效率;
和外部设备IO的时候,数据量的大小不是主要矛盾,和外设预备IO的过程是最耗费时间的;
显示器,是要给用户看的,一方面要照顾效率,一方面还要照顾用户体验;
3.用户缓冲区与内核缓冲区
下面一段代码:
- 正常打印:
打印出4条; - 重定向到log.txt打印:
就会打印出7条;
我们是在最后调用的fork,创建子进程之前,上面的语句已经被执行完了;
向显示器打印时,只打印出了4行文本;
而向普通文件(磁盘上)打印时,就变成了7行:
C语言的IO接口是打印了两次;
系统接口只打印了一次;
上面的代码,并不影响系统接口,如果有缓冲区,那这个缓冲区一定是由C标准库维护的,因为如果是由OS维护的,那上面的代码应该都是一样的效果;
- 用户缓冲区:
C标准库为我们提供了用户级的缓冲区,我们平常使用的就是这个,在执行IO操作时,我们先将数据写入用户缓冲区中,再调用系统的IO接口(read、write等)将数据从用户缓冲区写入到内核缓冲区中,而不是直接写入到文件中;
一旦拷贝完成,该数据就属于内核数据了,再由OS写入文件; - 内核缓冲区
操作系统中也有内核级的缓冲区,用来接收用户缓冲区的数据,并写入到文件中
- 解释现象:
如果向显示器中打印,刷新策略是行刷新,那么最后执行fork的时候,一定是函数执行完了,且数据已经被刷新了;
如果对应的程序进行了重定向,要向磁盘文件中打印,隐形的将刷新策略变成了全缓冲,那么字符串最后的\n就没有意义了,这是用来进行行缓冲的;
因此,在fork的时候,一定是函数已经执行完了,但是数据还没有刷新,还在当前进程对应的C标准库的缓冲区中,这部分数据就是父进程的数据;
而fork一旦执行,创建子进程时发生了写时拷贝,父进程的数据拷贝给了子进程,代码结束后刷新缓冲区,就会将C接口的数据打印两份给磁盘文件;
4.用户缓冲区的位置
FILE结构体中不仅封装了文件描述符fd,也封装了该文件fd对应的语言层缓冲区结构;
- 如果在fork之前强制刷新:
就会变成打印4条:
- fflush只需传入stdout就能够将数据刷新到缓冲区,就是因为我们打开的文件在进程中的FILE结构体中封装了用户缓冲区的结构;
C语言中打开的FILE文件流,必须包含:
- 文件描述符fd;
- 缓冲区buffer;
5.自己设计用户缓冲区
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <stdlib.h>
#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;
}
运行结果: