Linux文件重定向&&文件缓冲区

目录

一、C文件接口

二、系统文件I/O

2.1认识系统文件I/O

2.2系统文件I/O

2.3系统调用和库函数

[2.4open( )的返回值--文件描述符](#2.4open( )的返回值--文件描述符)

2.5访问文件的本质

三、文件重定向

3.1认识文件重定向

3.2文件重定向的本质

3.3在shell中添加重定向功能

3.4stdout和stderr

[3.5如何理解"linux下一切皆文件" --以对外设的IO操作为例](#3.5如何理解“linux下一切皆文件” --以对外设的IO操作为例)

四、文件缓冲区

4.1认识FILE

4.2文件缓冲区引入

4.3文件缓冲区的原理

4.4解释现象

4.5总结


一、C文件接口

  • stdin & stdout & stderr
  • C默认会打开三个输入输出流,分别是stdin, stdout, stderr
  • 仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
  • fwrite向指定文件写入内容
  • fread从指定文件读取内容
  • fprintf根据指定的format(格式)发送信息(参数)到由stream(流)指定的文件,fprintf可以使得信息写入到指定的文件
  • 调用C文件接口,以w的形式打开,若文件不存在,会在当前目录下新建文件,当前路径就是进程的当前路径cwd,如果改变了进程的cwd就可以在其他目录下新建文件
  • w写入前都会对文件进行清空,a在文件结尾追加写,两者都是写入
  • C默认打开的三个输入输出流不是C语言的特性,而是操作系统的特性,进程会默认打开键盘,显示器,显示器

二、系统文件I/O

2.1认识系统文件I/O

  • 文件其实是在磁盘上的,磁盘是外设,对文件进行访问,就是对硬件进行访问

  • 任何用户都不能直接访问硬件的数据 ,而必须通过系统调用

  • 几乎所有的库只要是访问硬件设备,必须封装系统调用

  • C文件接口就是一种库函数,是对系统调用的封装

2.2系统文件I/O

  • open( )

  • #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    int open(const char *pathname, int flags);
    int open(const char *pathname, int flags, mode_t mode);
    pathname: 要打开或创建的目标文件
    flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行 " 或 " 运算,构成 flags
    参数 :
    O_RDONLY: 只读打开
    O_WRONLY: 只写打开
    O_RDWR : 读写打开
    O_CREAT : 若文件不存在,则创建它,需要使用 mode(例0666) 选项,来指明新文件的访问权限
    O_APPEND: 追加写
    O_TRUNC: 每一次写入都清空文件
    返回值:
    成功:新打开的文件描述符
    失败:-1

代码示例:

umask( )可以用来设置掩码的值

  • 比特方位式的标志位传递方式
  • 通过位运算来实现

2.3系统调用和库函数

  • 上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)
  • open close read write lseek 都属于系统提供的接口,称之为系统调用接口
  • 可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

2.4open( )的返回值--文件描述符

  • Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2
  • 0,1,2对应的物理设备一般是:键盘,显示器,显示器
  • linux下文件描述符的分配规则:从0下标开始,寻找最小没有被使用过的数组位置,它的下标就是新文件的文件描述符--结合访问文件的本质来说明

代码示例:

  • 因为C库函数是对系统接口的封装,系统接口下只认识文件描述符,所以C库自己提供的FILE结构体中必定也包含着文件描述符,用_fileno记录

如果关闭了1号文件,printf就无法向1号文件(显示器)写入了 ,但可以向3号文件写入,所以我们打印就只能看到n的值

2.5访问文件的本质

  • 任何一个被打开的文件在内存中都要被管理起来,操作系统如果管理被打开的文件?----先描述再组织

  • 当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件--file结构体(直接或间接包含如下属性:文件的基本属性,文件的内核缓冲区信息,引用计数,struct file*next,在磁盘的什么位置),表示一个已经打开的文件对象

  • 而进程执行open系统调用,所以必须让进程和文件关联起来,每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!

  • 所以,本质上,文件描述符就是该数组的下标,只要拿着文件描述符,就可以找到对应的文件

  • 当一个进程open()一个文件时,操作系统会在struct_file的指针数组中从下标为0的地方在开始寻找一个没有被使用过的数组位置,填入要打开文件的struct file*,再将数组下标返回给open( )调用,作为该文件的文件描述符fd
  • 当一个进程要向某个文件写入的时候,操作系统只认识文件描述符,根据文件描述符找到对应的数组下标,根据数组下标位置里的内容找到所对应的文件再写入
  • close关闭文件本质上是清空对应fd数组下标位置的内容,再将该fd内容指向的文件的引用计数--,引用计数为0才释放销毁相应的struct_ file

三、文件重定向

3.1认识文件重定向

  • 关闭1号文件再打开新文件 ,向1号文件写入内容

可以看到,原来要向1号文件(显示屏)打印的信息,被写入到了新打开的文件,其中,fd=1。这种现象叫做输出重定向

常见的重定向有:>输出重定向, >>追加重定向, <输入重定向

  • 追加重定向
  • 输入重定向

3.2文件重定向的本质

  • 文件重定向的本质:将1号文件描述符在指针数组中对应位置的内容,用log.txt文件描述符在指针数组中对应位置的内容进行覆盖,原本数组内的指向1号文件的文件指针就被替换成log.txt的文件指针,当我们再向1号文件描述符写入内容的时候,就是向文件指针指向的log.txt内写入而不再写到标准输出

  • dup2系统调用

  • 原本向显示屏打印的内容被写入到log.txt文件中

3.3在shell中添加重定向功能

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
#include<ctype.h>
#include<fcntl.h>

#define LEFT "["
#define RIGHT "]"
#define LABLE "#"
#define DELIM " \t"
#define LINE_SIZE 1024
#define ARGV_SIZE 32

#define NONE -1
#define IN_RDIR     0
#define OUT_RDIR    1
#define APPEND_RDIR 2

extern char** environ;
char commandline[LINE_SIZE];
char* argv[ARGV_SIZE];
char pwd[LINE_SIZE];
char myenv[LINE_SIZE];

int lastcode=0;
int quit=0;

char *rdirfilename = NULL;
int rdir = NONE;

const char* getuser()
{
    return getenv("USER");
}

const char* gethostname()
{
    return getenv("HOSTNAME");
}

void getpwd()
{
    getcwd(pwd,sizeof(pwd));
}

void check_redir(char *cmd)
{

    // ls -al -n
    // ls -al -n >/</>> filename.txt
    char *pos = cmd;
    while(*pos)
    {
        if(*pos == '>')
        {
            if(*(pos+1) == '>'){
                *pos++ = '\0';
                *pos++ = '\0';
                while(isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir=APPEND_RDIR;
                break;
            }
            else{
                *pos = '\0';
                pos++;
                while(isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir=OUT_RDIR;
                break;
            }
        }
        else if(*pos == '<')
        {
            *pos = '\0'; // ls -a -l -n < filename.txt
            pos++;
            while(isspace(*pos)) pos++;
            rdirfilename = pos;
            rdir=IN_RDIR;
            break;
        }
        else{
            //do nothing
        }
        pos++;
    }
}

void interact(char* cline,int size)
{
    getpwd();
    printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getuser(),gethostname(),pwd);
    char* s=fgets(cline,size,stdin);
    assert(s);
    (void)s;
    cline[strlen(cline)-1]='\0';

    //printf("echo : %s",cline);
    //ls -a -l > myfile.txt
    check_redir(cline);
}

int splitstring(char cline[],char* _argv[])
{
    int i=0;
    _argv[i++]=strtok(cline,DELIM);
    while(_argv[i++]=strtok(NULL,DELIM));

    return i-1;
}

void normalexcute(char* _argv[])
{
    pid_t id=fork();
    if(id<0)
    {
        perror("fork");
        //continue;
        return ;
    }
    else if(id==0)
    {

        int fd = 0;

        // 后面我们做了重定向的工作,后面我们在进行程序替换的时候,难道不影响吗???
        if(rdir == IN_RDIR)
        {
            fd = open(rdirfilename, O_RDONLY);
            dup2(fd, 0);
        }
        else if(rdir == OUT_RDIR)
        {
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
            dup2(fd, 1);
        }
        else if(rdir == APPEND_RDIR)
        {
            fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND, 0666);
            dup2(fd, 1);
        }
        //子进程执行指令
        //execvpe(argv[0],argv,environ);
        execvp(argv[0],argv);
    }
    else{
        int status=0;
        pid_t rid=waitpid(id,&status,0);
        if(rid==id)
        {
            lastcode=WEXITSTATUS(status);
        }
    }
}

int buildcommand(char* _argv[],int _argc)
{
    if(_argc==2&&strcmp(_argv[0],"cd")==0)
    {
        chdir(_argv[1]);
        getpwd();
        sprintf(getenv("PWD"),"%s",pwd);
        return 1;
    }
    else if(_argc==2&&strcmp(_argv[0],"export")==0)
    {
        strcpy(myenv,_argv[1]);
        putenv(myenv);
        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]=='$')
        {
            char* s=getenv(_argv[1]+1);
            if(s) printf("%s\n",s);
        }
        else{
            printf("%s\n",_argv[1]);
        }

        return 1;

    }

    //特殊处理ls
    if(_argc==2&&strcmp(_argv[0],"ls")==0)
    {
        _argv[_argc++]="--color";
        _argv[_argc]=NULL;
    }

    return 0;

}

int main()
{
    while(!quit)
    {
        //交互问题,获得命令行参数
        interact(commandline,sizeof commandline);

        //字符串分割,解析命令行参数
        int argc = splitstring(commandline,argv);
        if(argc==0) continue;

        //指令的判断
        int n=buildcommand(argv,argc);        

        //普通指令的执行
        if(!n)normalexcute(argv);
    }
        return 0;
}
  • 进程历史打开的文件以及文件的重定向关系,并不会被程序替换所影响!!进程程序替换之后影响页表右边的物理地址所指向的内容,虚拟地址并左边的部分并不会受到影响
  • 程序替换并不会影响文件访问

3.4stdout和stderr

  • stdout和stderr对应的硬件设备都是显示屏,访问的都是同一个文件(引用计数)
  • 在重定向的时候,默认只对stdout的fd进行重定向

代码示例:

  • 如果对1号和2号文件都要进行重定向呢?

示例:./mytest 1> log.txt 2>err.txt

示例:./mytest > log.txt 2>&1

3.5如何理解"linux下一切皆文件" --以对外设的IO操作为例

  • 不同的外设在进行IO操作时都有自己对应的读写方法,放在struct device里
  • 这些读写方法如何被找到?--由struct operation_func来对读写方法进行管理,该结构体里存在指向对应读写法的函数指针
  • 如何找到struct operation_func?--由struct file来对struct operation_func进行管理,file结构体存在指向struct operation_func的指针,基于struct file之上的被称为虚拟文件系统(VFS)--一切皆文件
  • 当我们打开一个文件的时候,通过进程的pcb数据结构找到struct struct_file,操作系统根据文件描述符的分配规则,在struct struct_file的指针数组中为该文件分配一个fd;当我们要访问一个外设的时候,根据该外设文件fd对应的数组下标内容找到该外设文件的struct file,根据file结构体找到对应的struct operation_func,由于访问的外设的不同,在struct operation_func中根据函数指针找到对应的读写方法,就可以对外设进行访问了

四、文件缓冲区

4.1认识FILE

  • 因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的
  • 所以C库当中的FILE结构体内部,必定封装了fd

4.2文件缓冲区引入

  • 对比有无fork( )的代码
  • 我们发现 printf 和 fwrite (库函数)都输出了 2 次,而 write 只输出了一次(系统调用),为什么呢?肯定和 fork有关!
  • 再来验证一个现象:

不加'\n'并且在最后close(1)

代码运行的结果是:只有系统调用接口写入的内容被打印出来了

加上'\n',结果又不一样了

4.3文件缓冲区的原理

  • C语言会提供一个缓冲区,我们调用C文件接口写入的数据会被暂存在这个缓冲区内,缓冲区的刷新方式有三种:
  1. 无缓冲:直接刷新,一般我们使用的fflush( )就是无缓冲的刷新方式
  2. 行缓冲:遇到'\n'才刷新,一般对应显示器
  3. 全缓冲:缓冲区满了才刷新,一般对应普通文件的写入
  4. 特殊说明:进程结束的时候会自动刷新缓冲区
  • 在操作系统的内核中也存在一个内核级别的缓冲区,目前认为,只要将数据刷新到了内核,数据就可以到硬件了,内核缓冲区也有自己的刷新方式
  • 为什么要有C层面的缓冲区?
  1. 用户不需要一步一步将数据写入到硬件中,而是可以直接调用C库为我们提供的读写方法,将数据交给库函数来处理,解决用户的效率问题
  2. 我们真正存到文件里的都是一个个的字符,调用C库的读写方法,可以在放入缓冲区之前将我们的数据格式化成字符串,再刷新到内核中进而写入文件,C层面的缓冲区可以配合格式化的工作
  • C为我们提供的缓冲区在FILE结构体里,FILE里面有相关缓冲区的字段和维护信息,FILE属于用户层面,而不属于操作系统
  • 文件写入的过程:
  1. 首先,在文件写入之前,进程会打开一个文件,通过对各种内核数据结构的访问和操作,获得该文件的文件描述符
  2. 如果使用系统调用接口来对文件进行写入,数据直接通过write和fd写入对应的内核级别缓冲区,默认最后都会刷新到硬件中
  3. 如果使用fwrite等库函数来对文件进行写入,首先,在语言层面会malloc出一个FILE结构体,FILE里面有对应的缓冲区信息以及文件的fd,然后内容会先被暂存在C层面的缓冲区,如果是无缓冲,数据直接被刷新到内核中,如果是行缓冲,遇到'\n'就会被刷新到内核中,如果是全缓冲,等缓冲区满了就被刷新到内核中
  4. 由于库函数是对系统调用接口的封装,用户通过write和fd将数据刷新到对应的文件的内核缓冲区内,再由该内核缓冲区刷新到外设

4.4解释现象

  • 为什么不加'\n'并且close(1)的时候,使用库函数写入的内容不会被显示?
  1. 不加'\n',调用库函数写入的数据都会被暂存在C层面的缓冲区

  2. close(1)后,即使进程退出后缓冲区会自动刷新,但是此时已经找不到1号文件的fd了,缓冲区内的数据也无法被写入到内核中,最后也不会显示到显示器上

  3. 加了'\n'即使最后close(1),遇到'\n'缓冲区就会立马将数据刷新到内核中,就会显示到显示器上

  • 为什么fork()之后重定向C接口会被调用两次?
  1. 重定向后,缓冲区的刷新方式会从行缓冲变成全缓冲,也就说,数据要么等到缓冲区满了再被刷新,要么等待进程结束后再刷新,所以我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
  2. fork( )之后,创建子进程,子进程会继承父进程的内核数据结构对象的内容,父子进程在一开始的时候数据和代码是共享的,缓冲区也属于数据
  3. 进程退出后,要对缓冲区的数据进行统一刷新,刷新就是对数据进行访问写入,此时父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据
  4. 由于write没有所谓的缓冲区,write()写入的数据直接在内核中,所以write( )的数据只有一份

4.5总结

  • printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区
  • 那这个用户级缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的"上层", 是对系统 调用的"封装",但是 write 没有缓冲区,而 printf fwrite 有,说明该缓冲区是二次加上的,由C标准库提供
相关推荐
无为之士2 分钟前
Linux自动备份Mysql数据库
linux·数据库·mysql
秋名山小桃子11 分钟前
Kunlun 2280服务器(ARM)Raid卡磁盘盘符漂移问题解决
运维·服务器
与君共勉1213812 分钟前
Nginx 负载均衡的实现
运维·服务器·nginx·负载均衡
岑梓铭19 分钟前
(CentOs系统虚拟机)Standalone模式下安装部署“基于Python编写”的Spark框架
linux·python·spark·centos
努力学习的小廉19 分钟前
深入了解Linux —— make和makefile自动化构建工具
linux·服务器·自动化
MZWeiei23 分钟前
Zookeeper基本命令解析
大数据·linux·运维·服务器·zookeeper
7yewh38 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
Arenaschi42 分钟前
在Tomcat中部署应用时,如何通过域名访问而不加端口号
运维·服务器
小张认为的测试42 分钟前
Linux性能监控命令_nmon 安装与使用以及生成分析Excel图表
linux·服务器·测试工具·自动化·php·excel·压力测试
waicsdn_haha1 小时前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk