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

文件的基础理解

  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时间不会实时更新。修改文件的内容,同时也会影响文件的属性,比如修改数据、文件大小同时也会发生改变。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python+reportlab创建PDF文件

目录 字体导入 画布写入 创建画布对象 写入文本内容 写入图片内容 新增页 画线 表格 保存 模板写入 创建模板对象 段落及样式 表格及样式 画框 图片 页眉页脚 添加图形 构建pdf文件 reportlab库支持创建包含文本、图像、图形和表格的复杂PDF文档。 安装&…

软件项目需求分析的实践探索(1)

一、项目启动与规划 组建团队 包括项目经理、系统分析师、业务分析师以及可能涉及的最终用户代表和领域专家等。例如&#xff0c;开发一个医疗管理软件&#xff0c;就需要有医疗行业的专家参与&#xff0c;确保对医疗业务流程有深入理解。明确各成员的职责&#xff0c;如系统分…

Windows下ESP32-IDF开发环境搭建

Windows下ESP32-IDF开发环境搭建 文章目录 Windows下ESP32-IDF开发环境搭建一、软件安装二、搭建IDF开发环境2.1 安装VS Code插件&#xff1a;2.2 配置ESP-IDF插件&#xff1a;2.3 下载例程源码&#xff1a; 三、编译和烧录代码四、Windows下使用命令行编译和烧录程序4.1 配置环…

从 GitLab.com 到 JihuLab.com 的迁移指南

本文分享从 GitLab.com 到 JihuLab.com 的迁移指南。 近期&#xff0c;GitLab Inc. 针对其 SaaS 产品做了限制&#xff0c;如果被判定为国内用户&#xff0c;则会建议使用其在国内的发布版本极狐GitLab。从 GitLab SaaS 产品&#xff08;GitLab.com&#xff09;迁移到极狐GitL…

Mysql-索引的数据结构

为什么要使用索引 索引是存储引擎用于快速找到数据记录的一种数据结构&#xff0c;就好比一本教科书的目录部分&#xff0c;通过目录中找到对应文章的页码&#xff0c;便可快速定位到需要的文章。MySQL中也是一样的道理&#xff0c;进行数据查找时&#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;…

秒鲨后端之MyBatis【1】环境的搭建和核心配置文件详解

​ 别忘了请点个赞收藏关注支持一下博主喵&#xff01;&#xff01;&#xff01;! ! ! Mybatis简介 MyBatis历史 MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下&#xff…

【Go】Go数据类型详解—map

1. 前言 本篇博客将会介绍Go语言当中的另一大核心数据类型——map&#xff08;映射&#xff09;&#xff0c;当然在介绍这个数据类型之前我们还是要思考引入这个数据类型的原因&#xff1a; ❓ 需求&#xff1a;要求完成对一个班级所有同学的信息管理&#xff08;包括但不限于…

Agent 案例分析:金融场景中的智能体-蚂蚁金服案例(10/30)

Agent 案例分析&#xff1a;金融场景中的智能体 —蚂蚁金服案例 一、引言 在当今数字化时代&#xff0c;金融行业正经历着深刻的变革。随着人工智能技术的飞速发展&#xff0c;智能体&#xff08;Agent&#xff09;在金融场景中的应用越来越广泛。蚂蚁金服作为金融科技领域的…

十五、新一代大模型推理架构Mamba

Mamba架构:下一代大模型架构的可能性? 随着深度学习的快速发展,Transformer 架构在过去几年中成为了自然语言处理(NLP)和生成式AI模型的主流架构。然而,Transformer并非完美,其计算效率、长序列建模能力等方面依然存在瓶颈。近期出现的Mamba架构被认为是对这些问题的潜…

LabVIEW中什么和C 语言指针类似?

在LabVIEW中&#xff0c;与C语言指针类似的概念是 引用 (Reference)。 引用在LabVIEW中主要用于以下几个方面&#xff1a; 数据引用&#xff1a;LabVIEW通过引用传递数据&#xff0c;而不是复制数据。通过引用&#xff0c;多个VIs可以共享数据而不需要复制整个数据结构&#xf…

前端编程图表化助手!Echarts入门

Echarts-一个基于javaScript的开源可视化图表库 在日常编程中&#xff0c;我们经常会用到类似饼图、柱状图等&#xff0c;而在网页中使用原生html和css很难做到类似效果。那么作为前端工程师&#xff0c;我们如何做出来一份好看而且实用的图标呢&#xff1f; 那么接下来&…

企业AI助理背后的技术架构:从数据到智能决策

在当今数字化时代&#xff0c;企业AI助理已经成为推动企业数字化转型和智能化升级的重要工具。它们通过整合企业内外部数据资源&#xff0c;运用先进的算法和模型&#xff0c;为企业提供高效、精准的智能决策支持。本文将深入探讨企业AI助理背后的技术架构&#xff0c;从数据收…

Node.js 工具:在 Windows 11 中配置 Node.js 的详细步骤

一、概述 记录时间 [2024-12-25] 本文讲述如何在 Windows 11 中进行 Node.js 工具的安装和配置。 以下是详细的步骤和说明。 二、安装 Node.js 1. 官网下载 通过官网&#xff0c;下载 Node.js&#xff0c;上面有好几种下载方式&#xff0c;文中下载的是 zip 压缩包。 如图&…

【Rabbitmq篇】高级特性----TTL,死信队列,延迟队列

目录 一.TTL 1.设置消息的TTL 2.设置队列的TTL 3.俩者区别 二.死信队列 定义&#xff1a; 消息成为死信的原因&#xff1a; 1.消息被拒绝&#xff08;basic.reject 或 basic.nack&#xff09; 2.消息过期&#xff08;TTL&#xff09; 3.队列达到最大长度 ​编辑 …

Solon v3.0.5 发布!(Spring 可以退休了吗?)

Solon 框架&#xff01; 新一代&#xff0c;面向全场景的 Java 应用开发框架。从零开始构建&#xff08;非 java-ee 架构&#xff09;&#xff0c;有灵活的接口规范与开放生态。 追求&#xff1a; 更快、更小、更简单提倡&#xff1a; 克制、高效、开放、生态 有什么特点&am…

【PCIe 总线及设备入门学习专栏 1.1 -- PCIe 基础知识 lane和link介绍】

文章目录 OverivewLane 和 LinkRC 和 RPPCIe controllerPCIE ControllerPHY模块 Inbound 和 OutboundPCIe transaction modelPIODMAP2P Overivew PCIe&#xff0c;即PCI-Express总线&#xff08;Peripheral Component Interconnect Express&#xff09;&#xff0c;是一种高速…

安卓 SystemServer 启动流程

目录 引言 Android系统服务启动顺序 zygote fork SystemServer 进程 SystemServer启动流程 1、SystemServer.main() 2、SystemServer.run() 3、初始化系统上下文 4、创建系统服务管理 5、启动系统各种服务 总结 引言 开机启动时 PowerManagerService 调用 AudioSer…

117.【C语言】数据结构之排序(选择排序)

目录 1.知识回顾 2.分析 设想的思路 代码 执行结果 ​编辑 错误排查和修复 详细分析出错点 执行结果 3.正确的思路 4.其他问题 1.知识回顾 参见42.5【C语言】选择排序代码 点我跳转 2.分析 知识回顾里所提到的文章的选择排序一次循环只比一个数字,和本文接下来要…

嵌入式驱动开发详解21(网络驱动开发)

文章目录 前言以太网框架ENET 接口简介MAC接口MII \ RMII 接口MDIO 接口RJ45 接口 PHY芯片以太网驱动驱动挂载wifi模块挂载后续 前言 linux驱动主要是字符设备驱动、块设备驱动还有网络设备驱动、字符设备驱动在本专栏前面已经详细将解了&#xff0c;网络设备驱动本文会做简要…