基础I/O -> 如何谈文件与文件系统?

news2024/12/24 5:39:47

文件的基础理解

  1. 空文件也要在磁盘上占据空间。
  2. 文件 = 文件内容+文件属性。
  3. 文件操作 = 对内容的操作 + 对属性的操作或者是对内容和属性的操作。
  4. 标定一个文件,必须使用:文件路径 + 文件名(具有唯一性)。
  5. 如果没有指明对应的文件路径,默认是在当前路径(进程当前的路径)进行文件访问。
  6. 当我们把fopen、fclose、fwrite等接口写完之后,代码编译之后,形成二进制可执行程序后,但是没有运行,文件对应的操作有没有被执行呢?没有。对文件操作的本质是:进程对文件的操作!
  7. 一个文件要被访问,必须先被打开(用户+进程+OS)。用户调用相关函数接口、进程执行函数、OS访问磁盘文件。
  8. 所以文件操作的本质是:进程和被打开文件之间的关系。

C语言有文件操作的接口,C++、JAVA、Python、php、go等其他语言呢?

同样有,但是操作接口都不一样。

文件存在磁盘上,磁盘又是一个硬件,要访问硬件,只有操作系统能访问,所以要想访问磁盘就不能绕过OS,OS也必定要提供文件级别的系统调用接口。所以,无论上层语言如何变化:

  1. 库函数可以千变万化,但是底层不变。
  2. 库函数底层都必须调用系统调用接口。

C文件I/O

FILE * fopen ( const char * filename, const char * mode );
//成功返回指向FILE对象的指针,失败返回NULL。

int fclose ( FILE * stream );
//成功返回0,失败返回EOF。

size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );

size_t fread ( void * ptr, size_t size, size_t count, FILE * stream ); 

int puts ( const char * str );
int fputs ( const char * str, FILE * stream );
//fputs不会写入其他字符、puts会在自动在末尾附加换行符。

char * fgets ( char * str, int num, FILE * stream );
char * gets ( char * str );
//fgets在结果字符串中包含任何结束换行符、gets结果字符串中不包含任何字符。 

...

fseek、ftell、rewind......

写段简单的对文件操作的C语言代码:

int main()
{
    FILE* fp = fopen("log.txt","w");     //w 只写方式打开,不存在创建。存在将内容自动清空
   // FILE* fp = fopen("log.txt","r");   //r 只读方式打开
   // FILE* fp = fopen("log.txt","a");   //a 以追加方式打开
    if(NULL == fp)
    {
        perror("fopen");
        return 1;
    }
    
   // char buffer[64];
   // while(fgets(buffer,sizeof(buffer)-1,fp) != NULL)
   // {
   //     buffer[strlen(buffer)-1] = 0;
   //    // fputs(buffer,stdout);
   //     puts(buffer);//会在末尾自动添加\n
   // }

    int cnt = 5;
    while(cnt)
    {
        fprintf(fp,"%s:%d\n","hello world!",cnt--);
    }

    fclose(fp);

    return 0;
}

打开文件的方式:

系统文件I/O 

操作文件,除了上述C接口(其他语言也有),当然还可以采用系统接口来进行文件的访问。

打开文件:
#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);
int creat(const char *pathname, mode_t mode);

pathname:要打开或创建的目标文件
flags:打开文件时,可以传入多个参数选项,用一个或者多个常量进行“或”运算,构成flags。
 参数:O_RDONLY: 只读打开。
       O_WRONLY: 只写打开。
       O_RDWR:   读、写打开。这三个常量,必须指定一个且只能指定一个。
       O_APPEND: 追加写。
       O_CREAT:  若文件不存在,则创建它。需要使用mode选项,指明新文件的访问权限。 
       O_TRUNC:  若文件存在,则清空文件内容。
 返回值:
       成功:返回新打开的文件描述符。
       失败:返回-1。
关闭文件:
#include <unistd.h>
int close(int fd);

写入文件:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count)
//成功返回成功写入的字节个数,失败返回-1。

读取文件:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
//成功返回成功读取的字节个数,失败返回-1。

//注意:"const void *buf",返回值为void*。在其他语言中,写入文件的类型包括文本类、二进制类,是语言提供的文件读取的分类,但是在OS看来,不管写入的是什么,都是二进制。读也是一样,没有要读入数据的具体类型,就是要读几个字节,具体读到的是什么由自己决定。

写文件:

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

int main()
{
    umask(0);//系统默认文件mask为0002,也可以更改(更改的为子进程的mask,父进程shell不受影响)
    int fd = open("log.txt",O_WRONLY|O_CREAT,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    //"hello world:5" hello world是字符串,5、4、3、2、1是整数
    //所以要将"hello world:5"... 格式化为字符串
    int cnt = 5;
    char outBuffer[64];
    while(cnt)
    {
        sprintf(outBuffer,"%s:%d\n","hello world!",cnt--); //将格式化数据转化为字符串存储在outBuffer中
        write(fd,outBuffer,strlen(outBuffer));
        //向文件写入string时,要不要+1,即strlen(outBuffer)+1,将字符串末尾的"\0"写入 ?
        //不需要,以"\0"作为字符串结尾,是c语言规定的,和文件没有关系。文件要的是字符串的有效内容。
    }
    close(fd);
    
    return 0;
}

读文件:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

int main()
{
    int fd = open("log.txt",O_RDONLY);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    char buffer[1024];
    ssize_t num = read(fd,buffer,sizeof(buffer)-1);
    if(num > 0)
    {
        buffer[num] = 0; //将读取结果最后一个字符置为\0,证明读取到的为字符串。
    }
    printf("%s",buffer);
    close(fd);
    
    return 0;
}

总结:

C库函数接口:fopen、fclose、fwrite、fread、fseek...

系统调用接口:open、close、write、read、lseek...

可以认为库函数接口底层都是封装了系统调用接口,方便二次开发。

文件描述符

文件操作的本质:进程和被打开文件的关系。

进程可以打开多个文件,所以系统中一定会存在大量的被打开的文件,OS要将这些被打开的文件管理起来(以先描述,再组织的方式),OS为了管理对应的打开的文件,必定要为文件创建对应的内核数据结构来标识文件(也就是struct file{ },其中包含了文件的大部分属性)。

所以在操作系统内部就可以把每一个struct file{ }文件用链式结构链接起来,OS只需要找到struct file{ }的起始地址,对文件的管理就变成了对链表的增删查改。

那么,进程和被打开文件之间的关系是怎样维护的呢?

通过对open函数的了解,打开成功返回的是文件的文件描述符,是一个整数。

看下面这段代码:

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

#define FILE_NAME(number) "log.txt"#number //字符串拼接

int main()
{
    umask(0);
    int fd0 = open(FILE_NAME(1),O_WRONLY|O_CREAT,0666);
    int fd1 = open(FILE_NAME(2),O_WRONLY|O_CREAT,0666);
    int fd2 = open(FILE_NAME(3),O_WRONLY|O_CREAT,0666);
    int fd3 = open(FILE_NAME(4),O_WRONLY|O_CREAT,0666);
    int fd4 = open(FILE_NAME(5),O_WRONLY|O_CREAT,0666);
    
    printf("fd:%d\n",fd0); 
    printf("fd:%d\n",fd1);
    printf("fd:%d\n",fd2);
    printf("fd:%d\n",fd3);
    printf("fd:%d\n",fd4);
    
    close(fd0);
    close(fd1);
    close(fd2);
    close(fd3);
    close(fd4);

    return 0;
}

运行结果: 

fd为什么从3开始呢?0、1、2呢?

C程序默认会打开三个标准输入输出流:

  • stdin --> 键盘
  • stdout --> 显示器
  • stderr --> 显示器

所以输入输出还可以采用下面这种方式:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
    char buf[1024];
    ssize_t s = read(0, buf, sizeof(buf));
    if(s > 0)
    {
        buf[s] = 0;
        write(1, buf, strlen(buf));
        write(2, buf, strlen(buf));
    }
    return 0;
}

打开文件C语言使用的函数接口为FILE* fp = fopen( ),系统调用接口 int fd = open( )。

FILE其实就是一个结构体。上层库函数要访问文件必须调用底层系统调用接口,而系统调用要访问文件必须要通过文件描述符。那么,自然而然,FILE结构体中必定包含文件描述符这样的字段。C语言不仅封装了系统接口,而且也封装了数据类型。

FILE是一个结构体。stdin、stdout、stderr也是FILE类型的结构体,C程序又默认会打开这三个标准输入输出流,所以我们可以预测,文件描述符0、1、2就对应着默认打开的三个输入输出流。

printf("stdin->fd:%d\n",stdin->_fileno);
printf("stdout->fd:%d\n",stdout->_fileno);
printf("stderr->fd:%d\n",stderr->_fileno);
//...

总结:

  • Linux进程默认会打开三个文件描述符fd,标准输入0、标准输出1、标准错误2。
  • C语言不仅在访问文件接口方面封装了系统调用接口,FILE*类型的指针还封装了OS内的文件描述符。

那么文件描述符又为什么是0、1、2、3、4 ... 这样连续的小整数呢?

当我们打开文件时,OS要在内存中创建相应的数据结构来描述目标文件。所以就有了struct file{ }结构体,表示一个已经打开的文件对象,而进程执行open系统调用,必须让进程和文件关联起来。每个进程都有一个指针struct files_struct* files,指向一张表stuct files_struct{ }(文件描述符表),该表最重要的部分就是包含一个struct file* fd_arrar[ ]的指针数组,每个元素都是指向被打开文件的指针。

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

文件描述符分配规则

直接看代码:

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>

int main()
{
    umask(0);
    int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n",fd);
    close(fd);

    return 0;
}

输出结果fd:3(0、1、2默认被占用)。

如果0或2号文件:

int main()
{
    close(0);
    //close(2);
    umask(0);
    int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n",fd);
    close(fd);

    return 0;
}

关闭0: 

关闭2:

输出结果分别为fd:0、fd:2。

文件描述符的分配规则为:在struct files_struct结构体的数组(也就是文件描述符表struct file * fd_arry[ ])中,从小到大按顺序找到当前没有被使用的最小的下标,作为新文件的文件描述符。

重定向

如果关闭1号文件呢?

int main()
{
    //close(0);
    //close(2);
    close(1);
    umask(0);
    int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    printf("fd: %d\n",fd); //默认向stdout打印
    //fprintf(stdout,"fd: %d\n",fd); //向stdout打印
    close(fd);

    return 0;
}

运行结果:

本来应该输出到显示器上的fd:1,却写到了文件log.txt中,这种现象叫输出重定向

重定向的本质:上层使用的fd不变,在内核中更改fd对应的struct file*对应指向的地址。

系统调用dup2

系统也提供了支持重定向的接口:

#include<unistd.h>
int dup2 (int oldfd , int newfd);
//makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following。
//注意:是把oldfd文件描述符里的内容,拷贝到newfd文件描述符中。并不是拷贝文件描述符!

常见重定向

>    :输出重定向

>>  :追加重定向

<    :输入重定向

下面使用系统调用dup2分别实现输出重定向、追加重定向、输入重定向。

实现输出重定向:

int main()
{
    //close(0);
    //close(2);
    //close(1);
    umask(0);
    int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    dup2(fd,1);//输出重定向
    printf("fd: %d\n",fd);
    //fprintf(stdout,"fd: %d\n",fd);
    return 0;
}

可以看到,本来应该输出到显示器上的,此时写到了log.txt中。

实现追加重定向:

int main()
{
    umask(0);
    //int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    dup2(fd,1);
    printf("fd: %d\n",fd);

    const char* msg = "hello world\n";
    write(1,msg,strlen(msg));
    write(1,msg,strlen(msg));
    close(fd);        
    return 0;
}

实现输入重定向:

int main()
{
    umask(0);
    //int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    //int fd = open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    int fd = open("log.txt",O_RDONLY);
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    char line[64];
    dup2(fd,0);//输入重定向
    while(1)
    {
        printf("> ");
        if(fgets(line,sizeof(line)-1,stdin)==NULL) break;//stdin -> 0
        printf("%s",line);
    }
    //dup2(fd,1);
    //printf("fd: %d\n",fd);

    //const char* msg = "hello world\n";
    //write(1,msg,strlen(msg));
    //write(1,msg,strlen(msg));
    close(fd);        
    return 0;
}

没有使用系统调用dup2()之前,从键盘输入:

dup2()输入重定向之后,从log.txt文件输入:

例:myshell中添加重定向功能 

【Linux】C语言实现简易的Linux shell命令行解释器-CSDN博客

在shell命令行上,我们可以这样使用重定向:

那么shell是怎么做到的呢?我们可以在自己实现的myshell中添加重定向功能来模拟实现一下。

关于如何实现简易的Linux shell命令行解释器可以看上篇文章:

【Linux】C语言实现简易的Linux shell命令行解释器-CSDN博客

完整代码: 

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

#define NUM 1024
#define OPT_NUM 64

#define NONE_REDIR   0
#define INPUT_REDIR  1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
#define trimSpace(start) do{ while(isspace(*start)) start++;   }while(0)


char lineCommand[NUM];
char* myargv[OPT_NUM];//指针数组
int lastCode = 0;
int lastSig = 0;

int redirType = NONE_REDIR;//重定向类型
char* redirFile = NULL;//重定向目标文件

//eg: "ls -a -l > myfile.txt" -> "ls -a -l" "myfile.txt"
//将得到的字符串解析为两部分。顺序扫描/倒序扫描
void commandCheck(char* commands)
{
    assert(commands);
    char* start = commands;
    char* end = commands + strlen(commands);
    while(start < end)
    {
        if(*start == '>')
        {
            *start = '\0';
            start++;
            if(*start == '>') //追加重定向
            {
                //"ls -a >>  log.txt"
                redirType = APPEND_REDIR;
                start++;
            }
            else{
                //"ls -a >  log.txt"
                redirType = OUTPUT_REDIR;
            }
            trimSpace(start);
            redirFile = start;
            break;
        }
        else if(*start == '<') //输入重定向
        {
            //"ls -a <    log.txt"
            *start = '\0';
            start++;
            //跳过空格
            trimSpace(start);
            //填写重定向信息
            //
            redirType = INPUT_REDIR;
            redirFile = start;
            break;
        }
        else{
            start++;
        }
    }
}

int main()
{
    while(1)
     {
        redirType = NONE_REDIR;
        redirFile = NULL;
        errno = 0;

        //输出提示符
        printf("用户名@主机号 当前路径# ");
        fflush(stdout);
        //获取用户输入,输入的时候会输入\n
        char* s = fgets(lineCommand,sizeof(lineCommand)-1,stdin);
        assert(s!=NULL);
        //清除最后一个\n
        lineCommand[strlen(lineCommand)-1]=0;
        
        //解析字符串
        //"ls -a -l > myfile" ->  命令:"ls -a -l" 重定向目标文件:"myfile"
        commandCheck(lineCommand);
        //printf("test : %s\n",lineCommand);
        //切割字符串
        //"ls -a -l" -> "ls" "-a" "-l"
        myargv[0] = strtok(lineCommand," ");
        int i  = 1;
        //给ls加颜色
        if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
        {
            myargv[i++] = "--color=auto";
        }
        while(myargv[i++] = strtok(NULL," "));//如果没有子字符串要切割,strtok返回NULL,而恰好myargv[end]也一定要= NULL
        if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
        {
            if(myargv[1] != NULL)
            {
                chdir(myargv[1]);
            }
            continue;
        }
        if(myargv[0] != NULL && myargv[1] != NULL && strcmp(myargv[0],"echo") == 0)
        {
            if(strcmp(myargv[1],"$?") == 0)
            {
                printf("%d\n",lastCode);
            }
            else if(strcmp(myargv[1],"$?") != 0)
            {
                for(int i = 1; myargv[i]; i++)
                {
                     printf("%s ",myargv[i]);
                }
                printf("\n");
            }
            else
            {
                printf("%s\n",myargv[1]);
            }
            continue;//重新循环,不执行fork创建子进程
        }
        //测试切割是否成功,条件编译
#ifdef DEBUG 
        for(int i = 0; myargv[i]; i++)
        {
            printf("myargv[%d]: %s\n",i,myargv[i]);
        }
#endif
         //执行命令
        pid_t id = fork();
        assert(id != -1);
        if(id  == 0)
        {
            switch(redirType)
            {
                case NONE_REDIR:
                    //nothing to do
                    break;
                case INPUT_REDIR:
                    {
                        //打开输入重定向目标文件
                        int fd = open(redirFile,O_RDONLY);
                        if(fd < 0)
                        {
                            perror("open");
                            exit(errno);
                        }
                        //输入重定向
                        dup2(fd,0);
                    }
                    break;
                case OUTPUT_REDIR:
                case APPEND_REDIR:
                    {
                        umask(0);
                        int flags = O_WRONLY|O_CREAT;
                        if(redirType == OUTPUT_REDIR) flags |= O_TRUNC;
                        else flags |= O_APPEND;
                        int fd = open(redirFile,flags,0666);
                        if(fd < 0)
                        {
                            perror("open");
                            exit(errno);
                        }
                        //输入、追加重定向
                        dup2(fd,1);
                    }
                    break;
                default:
                    printf("bug?\n");
                    break;
            }
            execvp(myargv[0],myargv);
            exit(1);
        }
        int status = 0;
        pid_t ret =  waitpid(id,&status,0);
        assert(ret > 0);
        lastCode = ((status>>8) & 0xFF);
        lastSig = (status & 0x7F);

    }
}

测试: 

Linux下一切皆文件 

当一个硬件设备或文件被访问时,OS内核会为其分配一个struct file结构体,并将对应的文件操作函数指针指向该设备或文件系统的具体实现。这样,当上层应用程序调用如read()、write()等系统调用时,内核会根据当前的文件描述符找到对应的struct file结构体,并调用其指向的具体函数来直接指向具体方法来实现读写操作。

总结:

通过虚拟的文件系统,上层访问硬件时,就摒弃了底层硬件设备的差异,而统一使用文件接口来进行所有的文件操作。

理解缓冲区问题

看下面这段代码:

#include<stdio.h>
#include<unistd.h>
int main()
{
    //C接口
    printf("hello printf\n");
    fprintf(stdout,"%s","hello fprintf\n");
    const char* fputsString = "hello fputs\n";
    fputs(fputsString,stdout);

    //系统接口
    const char* writeString = "hello write\n";
    write(1,writeString,strlen(writeString));
    
    //
    fork();

    return 0;
}

测试运行: 

当把fork函数删除之后:

可以看到,当fork函数存在时,当向显示器输出的时候,C语言函数接口和系统函数接口都只打印了一次,而当输入重定向到log.txt文件中,C语言函数接口打印了两次,系统函数接口打印了一次。这种现象跟缓冲区有关。

缓冲区存在意义

缓冲区本质就是一段内存!

所以,缓冲区存在的意义就是:节省进程进行数据IO的时间!

缓冲区刷新策略

缓冲区什么时候将数据发送到磁盘呢?

如果有一块数据,一次写入到外设,还是多次少量的写入到外设效率高呢?

实际上,在进行IO的时候,数据从内存拷贝到外设花费时间的时间很少,大部分的时间都是在等外设准备好。所以,将数据一次写入到外设的效率高,因为只进行了一次IO。

缓冲区会结合具体的设备,制定自己的刷新策略:

  • 立即刷新          - - - -   无缓冲
  • 行刷新              - - - -   行缓冲   如:显示器
  • 缓冲区满刷新   - - - -   全缓冲   如:磁盘文件

两种特殊情况:

  1. 用户强制刷新
  2. 进程退出(一搬都要进行缓冲区刷新)

显示器同样是外设,为什么要行刷新呢?

虽然,全缓冲,缓冲区满了之后,再将缓冲区里的数据一次刷新到外设中的效率是最高的,但是,显示器是用给人来看的,按行显示,更符合人的阅读习惯。

缓冲区存放区域

那么缓冲区在哪里呢?由谁提供的呢?

删除fork之后:

根据此现象,可以得知,缓冲区一定不在OS内核中!因为,如果在内核中,write也要被打印两次!(因为库函数fwitre底层调用的是系统调用write)

所以,我们上面所谈论的缓冲区,都指的是用户级语言层面(这里C语言)给我们提供的缓冲区。(当然。OS也会提供内核级缓冲区,不过,我们不讨论)

当我们进行C语言输入输出的时候,默认会打开stdin、stdout、stderr,他们的类型都是FILE * 或者打开文件fopen,进行文件读写fwrite、fread时,都必须要传入FILE*,FILE是一个结构体,FILE中就封装了文件描述符和缓冲区!!!也就是说,我们在C程序上进行所谓的输入输出等操作,最终都会先将数据写进FILE*指向的FILE结构体内的缓冲区中!

所以,当我们要强制刷新缓冲区,fflush(文件指针FILE*),或者关闭文件fclose(文件指针FILE*),都要传入FILE*,原因就是FILE结构体中包含了缓冲区!

如果有兴趣,也可以看一下FILE结构体:

/usr/include/stdio.h

/usr/include/libio.h

基于上述对缓冲区的理解,我们现在再来解释一下,一开始代码fork之后出现的现象。

int main()
{
    //C接口
    printf("hello printf\n");
    fprintf(stdout,"%s","hello fprintf\n");
    const char* fputsString = "hello fputs\n";
    fputs(fputsString,stdout);

    //系统接口
    const char* writeString = "hello write\n";
    write(1,writeString,strlen(writeString));

    //代码结束之前,进行创建子进程
    //1、如果我们没有进行输出重定向> ,看到了4条消息
    //stdout 默认使用的是行刷新,在进程fork之前,3条函数已经将数据进行打印输出到显示器上(外设),FILE内部,进程内部不存在对应的数据了。
    //2、如果我们进程了输出重定向> ,写入文件不再是显示器,而是普通文件,采用的刷新策略是全缓冲,之前的3条C显示函数,虽然带了\n,但是不足以
    //将stdout缓冲区写满!数据并没有被刷新。
    //执行fork的时候,stdout属于父进程,创建子进程时,紧接着就是进程退出,谁先退出,一定要进行缓冲区刷新(就是修改)
    //所以,此时,会发生写时拷贝!数据最终会显示两份。
    //3、write为什么没有呢?上面的过程都和write无关,write是系统调用,write访问文件没有FILE结构体,而用的是fd,也就没有C提供的缓冲区。
    fork();

    return 0;
}

下面我们写一部分代码使用系统调用来封装一下C库的文件操作接口,再理解一下缓冲区以及它的刷新策略是怎样的。

DEMO

myStdio.h:

#pragma once

#include <assert.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define SIZE 1024
#define SYNC_NOW    1
#define SYNC_LINE   2
#define SYNC_FULL   4

typedef struct _FILE{
    int flags; //刷新方式
    int fileno;
    int cap; //buffer的总容量
    int size; //buffer当前的使用量
    char buffer[SIZE];
}FILE_;


FILE_ *fopen_(const char *path_name, const char *mode);
void fwrite_(const void *ptr, int num, FILE_ *fp);
void fclose_(FILE_ * fp);
void fflush_(FILE_ *fp);

myStdio.c:

#include "myStdio.h"

FILE_ *fopen_(const char *path_name, const char *mode)
{
    int flags = 0;
    int defaultMode=0666;

    if(strcmp(mode, "r") == 0)
    {
        flags |= O_RDONLY;
    }
    else if(strcmp(mode, "w") == 0)
    {
        flags |= (O_WRONLY | O_CREAT |O_TRUNC);
    }
    else if(strcmp(mode, "a") == 0)
    {
        flags |= (O_WRONLY | O_CREAT |O_APPEND);
    }
    else
    {
        //TODO
        //...
    }
    int fd = 0;

    if(flags & O_RDONLY) fd = open(path_name, flags);
    else fd = open(path_name, flags, defaultMode);
    if(fd < 0)
    {
        const char *err = strerror(errno);
        write(2, err, strlen(err));
        return NULL; // 也就是C为什么打开文件失败会返回NULL
    }
    FILE_ *fp = (FILE_*)malloc(sizeof(FILE_));
    assert(fp);

    fp->flags = SYNC_LINE; //演示。默认设置成为行刷新
    fp->fileno = fd;
    fp->cap = SIZE;
    fp->size = 0;
    memset(fp->buffer, 0 , SIZE);

    return fp; // 也就是为什么C打开一个文件,就会返回一个FILE *指针
}

void fwrite_(const void *ptr, int num, FILE_ *fp)
{
    // 1. 写入到缓冲区中
    memcpy(fp->buffer+fp->size, ptr, num); //这里我们不考虑缓冲区溢出的问题
    fp->size += num;

    // 2. 判断是否刷新
    if(fp->flags & SYNC_NOW)
    {
        write(fp->fileno, fp->buffer, fp->size);
        fp->size = 0; //清空缓冲区
    }
    else if(fp->flags & SYNC_FULL)
    {
        if(fp->size == fp->cap)
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    else if(fp->flags & SYNC_LINE)
    {
        if(fp->buffer[fp->size-1] == '\n') // abcd\nefg , 不考虑
        {
            write(fp->fileno, fp->buffer, fp->size);
            fp->size = 0;
        }
    }
    else{
            //....
    }
}

void fflush_(FILE_ *fp)
{
    if( fp->size > 0) write(fp->fileno, fp->buffer, fp->size);
    //TODO...
    //fsync(fp->fileno); //将数据,强制要求OS进行外设刷新!
    //fp->size = 0;
}

void fclose_(FILE_ * fp)
{
    fflush_(fp);
    close(fp->fileno);
}

 main.c:

#include "myStdio.h"
#include <stdio.h>

int main()
{
    FILE_ *fp = fopen_("./log.txt", "w");
    if(fp == NULL)
    {
        return 1;
    }
    int cnt = 10;    
    const char *msg = "hello world ";
    //const char *msg = "hello world ";
    while(1)
    {
        fwrite_(msg, strlen(msg), fp);
        //fflush_(fp);//将数据强制刷新到外设
        sleep(1);
        printf("count: %d\n", cnt);
        if(cnt == 5) fflush_(fp);
        cnt--;
        if(cnt == 0) break;
    }
    fclose_(fp);

    return 0;
}

写一个监控脚本监视运行一下,看到的现象如下: 

缓冲区与OS的关系

缓冲区(用户级)和操作系统OS有什么关系呢?

如果OS将数据从内核缓冲区刷新到外设的时候,OS宕机了呢?

会发生数据丢失。避免方法是,用户层可以直接强制OS将对应的文件内核缓冲区中的数据同步到磁盘。

了解磁盘

上面我们所谈论的文件是被打开的文件,如果一个文件没有被打开呢?该如何被OS管理?

没有被打开的文件,只能静静的在磁盘上放着。磁盘上有大量的文件,也是必须要被“静态”管理的,方便我们随时打开。

磁盘的物理结构

磁盘是计算中唯一的一个机械结构!硬盘是一个机械结构,同时也是外设,所以硬盘访问速度会很慢。但是在企业端,磁盘依旧是存储的主流。

磁盘的存储结构

磁盘在寻址的时候,基本单位不是bit,也不是byte,而是扇区(512byte)

如何在单面上,定位一个扇区?

如何在磁盘中,定位任何一个扇区?

磁盘的逻辑结构

为什么OS要进行将磁盘逻辑抽象,使用LAB线性地址访问扇区呢?直接使用磁盘的物理地址CHS不可以吗?

  • 便于管理。
  • 不想让OS的代码和硬件强耦合。

虽然磁盘访问的基本单位为扇区(512byte),但是依旧很小!磁头摆动,盘片旋转找对应的扇区的效率略低,这也就是为什么进程要访问磁盘,大部分的时间都是在等磁盘“准备好”。

所以OS系统内的文件系统会定制的进行多个扇区的读取,一次读取1KB或者2KB或者4KB(常用)为基本单位。哪怕只想读取或修改1个bit位,也必须将4KB加载到内存,进行读取或修改,必要的时候,再写回磁盘。以空间换时间的做法!

理解文件系统 

一个磁盘空间大小约几百个GB,例如500GB,那么如何管理这个磁盘呢?

500GB太大了,不好管理,所以先将磁盘进行分区,例如分为100GB、100GB、150GB、150GB四个分区。只需要将100GB的分区管理好,其余分区同样的方法也可以管理好。但是100GB同样很大,相对来说也不好管理,所以再将每个分区进行分组,例如100GB的分区分为20个5GB的分组。这时候只需要将5GB的空间管理好,同样的方法就可以管理好其余分组,同样可以管理好每个分区,进而能够管理好一个次磁盘。(分治的思想)

文件系统是如何管理一个分组的呢?

Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs-b选项可以设定block大小为102420484096字节。而上图中启动块(Boot Block)的大小是确定的。

  • Super Block:保存的是整个文件系统的信息。
文件 = 文件属性 + 文件内容。
Linux下文件属性和文件内容是分批存储的。
  • 文件属性存在inode块中,inode是固定大小。几乎一个文件,一个inode。这些inode为了区分彼此,每一个inode都有自己的ID编号!inode块中包含了几乎这个文件的所有属性,文件名并不在inode中存储。
  • data block里存放文件内容,data block随着应用类型的变化,大小也在变化。

如何在一个分组里面保存众多的文件呢?

  • inode Table:保存了分组内部所有的可用(已经使用+没有使用)inode。

当创建文件的时候,首先就需要区inode table中查找找没有使用的inode,再将文件属性填到inode中。

  • Data Blocks:保存的是分组内部所有文件的数据块(4kb大小为单位),哪些块是属于哪些文件的,也有办法来标识。

当创建文件的时候,然后需要在data block中查找没有被使用的数据块,进而申请数据块,就可以将文件内容写进数据块中。

在创建文件的时候无论是查找没有使用的inode,还是查找没有使用的data block。都需要进行查找操作。

  • inode Bitmap:inode对应的位图。每个bit位表示一个inode是否空闲可用(0/1)。
  • block Bitmap:数据块对应的位图,记录着data blocks中哪个数据块被占用或没有被占用。
  • Group Descriptor Table:块组描述表,对应分组的宏观属性。

例如:一共有多少个分组,每个分组是多大,一共多少个inode/data block,使用了多少个inode/data block,哪些没有被使用等等。

查找一个文件的时候,统一使用的是inode编号。(inode可以跨分组,但是不能跨分区)

首先在inode bitmap中查找inode编号对应的bit位是否为1,为1证明文件是存在的,再去inode table中查找对应的inode,就可以将文件属性查找出来,那么文件内容呢?去data block中查找,data block中有大量的数据块,怎么知道哪个数据块和我要查找的文件有关呢?

inode块中不仅包含着文件的属性,还包含了一个存放着数据块编号的数组。

创建文件,首先在inode map中将inode编号对应的bit位置为1,再去inode table找到对应的inode中将文件属性填写进去,然后将文件数据写到未使用的data block中,再将inode和data block之间建立联系,文件就创建成功了。

删除文件,只需要在inode map中将文件对应的inode编号映射的bit位置为0即可。删除之后,如果不做其他操作,如创建新文件,只要知道删除文件的inode编号,在inode map中将删除文件的indoe编号映射的bit位置1,还可以恢复文件。

在Linux下查找文件,我们也并没有使用inode编号啊!使用的是文件名!

普通文件是一个文件,任何一个文件一定在某一个目录里面。目录也是一个文件,也有自己的属性和内容,即同样有inode和数据块,那么目录的数据块放的是什么呢?

目录的数据块放的就是,当前目录下的文件名和inode的映射关系!

像ls查文件的时候,第一步一定是查找当前目录对应的数据块,将文件的文件名和inode提取出来...所以同一个目录下不可以存在文件名相同的文件!

  • 这也就是为什么创建文件的时候为什么一定要有写权限,因为一定要在当前目录的数据块里去写文件名和inode的映射关系!
  • 这也就是查看文件的时候为什么一定要有读权限,因为拿到的是文件名,必须得访问目录的数据块,根据文件名去找对应文件的inode!

软硬链接

一个文件对应一个inode,一个inode对应一个文件。

软硬链接的区别

  • 是否是具有独立的inode!
  • 软链接具有独立的inode,可以被当作独立的文件来看待。
  • 硬链接没有独立的inode。

硬链接

可以看到硬链接,与原文件有同样的inode以及属性和内容。

所以建立硬链接,根本就没有创建新文件!因为没有给硬链接分配独立的inode。既然没有创建文件,一定没有自己的属性集合和内容集合,用的一定是别人的inode以及属性和内容!

建立硬链接本质就是在指定的路径下,新增文件名和inode编号的映射关系。

所以当删除文件的时候,其实做了两件事情,1、在目录中将对应的记录删除。2、将硬链接数-1,如果为0,则将文件对应的磁盘空间释放,就彻底的删除了文件。

软链接

在这种情况下,当把软链接链接的目标文件删除之后,软链接就失效了,但是事实上,这个链接的目标文件还是存在的。

所以软链接链接文件并没有使用目标文件的inode来链接文件的,而使用的是目标文件的文件名!

硬链接是通过inode引用另外一个文件,软链接是通过文件名引用另外一个文件。

当把目标文件删除之后,当前目录也就没有对应的目标文件的文件名了,也就是说,软链接有查找这个目标文件的方式。我们在查找文件的时候,使用的是路径。软链接是一个独立的文件,具有独立的inode,也有数据块,它的数据块里面保存的是所指向目标文件的路径。

当把软链接删除之后,并不会影响目标文件。当把软链接链接的目标文件删除之后,软链接就失效了。

所以,在Linux下的软链接就相当于在Windows下的快捷方式!

软链接的应用

快速的访问某个文件或目录。

此时,我要执行mytest应用程序。

这样执行要带很长的一段路径,是不是太麻烦了。我们就可以给mytest建立一个软链接。

硬链接的应用

为什么普通文件的硬链接数是1呢?

因为一个普通文件,本身自己的文件名和自己的inode具有一个映射关系!

为什么这个空目录的硬链接数是2呢?

因为任何一个目录里都有两个隐藏目录,分别为“.”目录和“..”目录。

我们发现,empty目录里隐藏的“.”目录的inode和empty目录的inode相同!“.”目录也叫做当前目录。他们的inode相同,也就意味着,“.”目录就是当前目录empty的一个硬链接。

我们再在空目录empty里建立一个dir目录,再回到上级路径,发现empty的硬链接数变成了3:

原因就是empty目录里的dir目录里隐藏的还有一个“..”目录,它的inode和上级目录empty目录的inode相同,也叫做上级目录。

这也就是为什么“cd ..”能回到上级目录的原因:“..”目录,是上级目录的一个硬链接!

Linux为什么不允许普通用户给目录创建硬链接呢?

  • 防止目录循环引用:

  1. 硬链接与原文件共享同一个inode号。如果对目录进行硬链接,就可能有多个等效的入口点指向同一个目录,这样会导致目录树出现环形结构。
  2. 在这种环形结构中,像文件遍历这样的操作可能会陷入无限循环,进一步可能导致文件系统的损坏。例如,如果有两个目录A和B,它们之间存在循环链接,那么当尝试遍历A目录时,就可能导致无限循环。
  • 保护文件系统的结构完整性:

  1. 允许目录硬链接可能会使inode系统遇到复杂的父子关系以及所有权问题,因为多个父目录可能会指向同一个inode号。
  2. 这将导致文件系统的维护人员在处理删除目录、重命名目录等操作时,无法有效地处理这种inode的多父问题。
  • 简化文件系统的设计

  1. 允许硬链接到目录将引入复杂的处理逻辑,以保证文件系统的一致性,例如更新目录下文件的链接数、处理异常情况等。
  2. 简单的设计有助于减少bug,提高文件系统的稳定性。

促进了文件系统的健康性和易于维护性。

但是,“.”目录、“..”目录,不就是分别是当前目录和上级目录的硬链接吗?

这两个是特殊情况,是Linux操作系统创建的,但是不允许用户给目录创建硬链接。

文件的三个时间

  • Access: 文件最后访问的时间。
  • Modify:  文件内容最后修改的时间。
  • Change:文件属性最后修改的时间。

注意:

由于我们大多数操作文件的时候都是在读文件,如果每一次读,都要更新文件的访问时间,那么访问就太频繁了,数据在磁盘上,数据更新频繁,就会影响Linux的效率。所以Acess时间不会实时更新。修改文件的内容,同时也会影响文件的属性,比如修改数据、文件大小同时也会发生改变。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2264554.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

网络安全检测

实验目的与要求 (1) 帮助学生掌握木马和入侵的防护和检测方法、提高学习能力、应用能力和解决实际问题的能力。 (2) 要求学生掌握方法, 学会应用软件的安装和使用方法, 并能将应用结果展示出来。 实验原理与内容 入侵检测是通过对计算机网络或计算机系统中若干关键点收集信…

谷歌浏览器的资源管理功能详解

谷歌浏览器作为一款广受欢迎的网页浏览器&#xff0c;不仅以其快速、简洁和易用著称&#xff0c;还提供了强大的资源管理功能。本文将详细介绍如何在Chrome浏览器中进行资源管理&#xff0c;包括查看网页的渲染性能、禁用标签页的背景更新以及管理正在下载的文件。&#xff08;…

ARM异常处理 M33

1. ARMv8-M异常类型及其详细解释 ARMv8-M Exception分为两类&#xff1a;预定义系统异常(015)和外部中断(1616N)。 各种异常的状态可以通过Status bit查看&#xff0c;获取更信息的异常原因&#xff1a; CFSR是由UFSR、BFSR和MMFSR组成&#xff1a; 下面列举HFSR、MMFSR、…

Unity2021.3.16f1可以正常打开,但是Unity2017.3.0f3却常常打开闪退或者Unity2017编辑器运行起来就闪退掉

遇到问题&#xff1a; 从今年开始&#xff0c;不知道咋回事&#xff0c;电脑上的Unity2017像是变了个人似得&#xff0c;突然特别爱闪退掉&#xff0c;有时候还次次闪退&#xff0c;真是让人无语&#xff0c;一直以来我都怀疑是不是电脑上安装了什么别的软件了&#xff0c;导致…

SpringBoot核心:自动配置

有使用过SSM框架的&#xff0c;还记得曾经在spring-mybatis.xml配置了多少内容吗&#xff1f;数据源、连接池、会话工厂、事务管理&#xff0c;而现在Spring Boot告诉你这些都不需要了&#xff0c;简单的几个注解统统搞定&#xff0c;是不是很方便&#xff01; 前言 SpringBoo…

重温设计模式--享元模式

文章目录 享元模式&#xff08;Flyweight Pattern&#xff09;概述享元模式的结构C 代码示例1应用场景C示例代码2 享元模式&#xff08;Flyweight Pattern&#xff09;概述 定义&#xff1a; 运用共享技术有效地支持大量细粒度的对象。 享元模式是一种结构型设计模式&#xff0…

Taro小程序开发性能优化实践

我们团队在利用Taro进行秒送频道小程序的同时&#xff0c;一直在探索性能优化的最佳实践。随着需求的不断迭代&#xff0c;项目中的性能问题难免日积月累&#xff0c;逐渐暴露出来影响用户体验。适逢双十一大促&#xff0c;我们趁着这个机会统一进行了Taro性能优化实践&#xf…

纯血鸿蒙APP实战开发——textOverflow长文本省略

介绍 本示例实现了回复评论时&#xff0c;当回复人的昵称与被回复人的昵称长度都过长时&#xff0c;使用textOverflow和maxLines()实现昵称的长文本省略展示的功能。 效果图预览 使用说明 点击评论中的"回复"&#xff0c;在输入框中输入回复内容&#xff0c;点击发…

【java面向对象编程】第九弹----抽象类、接口、内部类

笔上得来终觉浅,绝知此事要躬行 &#x1f525; 个人主页&#xff1a;星云爱编程 &#x1f525; 所属专栏&#xff1a;javase &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 一、抽象类 1.1基本介绍 &…

Qt笔记:网络编程UDP

一、铺垫 1.Qt框架使用的网络结构的基础就是Linux学习的网络编程基础&#xff1b;所以使用Qt写客户端&#xff0c;使用Linux写服务端&#xff1b;两者是可以实现互联的 二、UDP 网络编程UDP使用套路&#xff1a; 1.首先在.pro文件中加上network&#xff0c;使Qt可以搭载网络…

Redis存在安全漏洞

Redis是美国Redis公司的一套开源的使用ANSI C编写、支持网络、可基于内存亦可持久化的日志型、键值&#xff08;Key-Value&#xff09;存储数据库&#xff0c;并提供多种语言的API。 Redis存在安全漏洞。攻击者利用该漏洞使用特制的Lua脚本触发堆栈缓冲区溢出漏洞&#xff0c;从…

【潜意识Java】蓝桥杯算法有关的动态规划求解背包问题

目录 背包问题简介 问题描述 输入&#xff1a; 输出&#xff1a; 动态规划解法 动态规划状态转移 代码实现 代码解释 动态规划的时间复杂度 例子解析 输出&#xff1a; 总结 作者我蓝桥杯&#xff1a;2023第十四届蓝桥杯国赛C/C大学B组一等奖&#xff0c;所以请听我…

ReactPress 1.6.0:重塑博客体验,引领内容创新

ReactPress 是一个基于Next.js的博客&CMS系统&#xff0c; Github项目地址&#xff1a;https://github.com/fecommunity/reactpress 欢迎Star。 体验地址&#xff1a;http://blog.gaoredu.com/ 今天&#xff0c;我们自豪地宣布ReactPress 1.6.0版本的正式发布&#xff0c;…

单元测试-Unittest框架实践

文章目录 1.Unittest简介1.1 自动化测试用例编写步骤1.2 相关概念1.3 用例编写规则1.4 断言方法 2.示例2.1 业务代码2.2 编写测试用例2.3 生成报告2.3.1 方法12.3.2 方法2 1.Unittest简介 Unittest是Python自带的单元测试框架&#xff0c;适用于&#xff1a;单元测试、Web自动…

带有 Elasticsearch 和 Langchain 的 Agentic RAG

作者&#xff1a;来自 Elastic Han Xiang Choong 讨论并实现 Elastic RAG 的代理流程&#xff0c;其中 LLM 选择调用 Elastic KB。 更多阅读&#xff1a;Elasticsearch&#xff1a;基于 Langchain 的 Elasticsearch Agent 对文档的搜索。 简介 代理是将 LLM 应用于实际用例的…

[react 3种方法] 获取ant组件ref用ts如何定义?

获取ant的轮播图组件, 我用ts如何定义? Strongly Type useRef with ElementRef | Total TypeScript import React, { ElementRef } from react; const lunboRef useRef<ElementRef<typeof Carousel>>(null); <Carousel autoplay ref{lunboRef}> 这样就…

stm32制作CAN适配器5--WinUsb上位机编写

上次我们要stm32制作了一个基于winusb有canfd适配器&#xff0c;今天我们来制作一个上位机程序来进行报文收发。 上位机还是用以前写好的&#xff0c;只是更改下dll文件。 项目链接器&#xff0c;输入&#xff0c;附加依赖项中增加winusb.lib winusb初始化&#xff1a;#incl…

C/C++圣诞树

系列文章 序号直达链接1C/C爱心代码2C/C跳动的爱心3C/C李峋同款跳动的爱心代码4C/C满屏飘字表白代码5C/C大雪纷飞代码6C/C烟花代码7C/C黑客帝国同款字母雨8C/C樱花树代码9C/C奥特曼代码10C/C精美圣诞树11C/C俄罗斯方块12C/C贪吃蛇13C/C孤单又灿烂的神-鬼怪14C/C闪烁的爱心15C…

图解HTTP-HTTP报文

参考资料&#xff1a;图解HTTP HTTP报文 用于HTTP协议交互的信息被称为HTTP报文。请求端的HTTP请求报文&#xff0c;响应端&#xff08;服务器端&#xff09;的叫做响应报文。HTTP报文本身是由多行&#xff08;CR LF作为换行符&#xff09;数据行构成的文本。 请求报文及响…

机器学习基础算法 (一)-线性回归

python 环境的配置参考 从零开始&#xff1a;Python 环境搭建与工具配置 线性回归的 Python 实现 线性回归是一种经典的机器学习算法&#xff0c;用于预测连续的目标变量。它假设目标变量和特征之间存在线性关系。本文将详细介绍线性回归的原理、Python 实现、模型评估和调优&…