【Linux】基础IO

目录

一.什么是文件操作

C语言中的文件操作 

Linux中的文件操作 

close系统调用 

open系统调用

标记位 

write系统调用

read系统调用

二.文件操作的本质 

三.文件描述符的分配规则 

重定向 

fud2系统调用 

四.缓冲区的理解 

模拟C语言中文件操作函数的实现 

MyStdio.h 

MyStdio.c 

main.c 

五.软硬链接

软链接 

硬链接 

引用计数 

六.acm时间 

一.什么是文件操作

如果学过其他编程语言,那应该都知道文件操作以及IO,那么在linux中,文件操作又是如何进行的呢?以及我们应该如何理解文件操作和IO?


C语言中的文件操作 

在讲解Linux的文件操作之前,我们先来看一下在C语言中,文件是如何操作的。同时在后面我也会稍微围绕C语言文件操作讲解。 

C语言的文件操作,博主之前写过一篇文章,所以这里就不再多说,可以直接看下面这篇博客。

【C语言】文件操作-CSDN博客

Linux中的文件操作 

熟悉完了C语言中的文件操作,那么在Linux中又是怎么样的呢?

首先我们来看一个问题,在众多的编程语言中,每一个语言文件操作都是不同的,但是归根结底,它们在底层的方式却是相同的,什么意思呢?

文件我们都知道,它是存在磁盘(硬盘)上的,既然是磁盘,那么它就是一个硬件层面上的东西,对于一个硬件层面上的东西,如果我们想要操作它,那就必须由操作系统来进行,所以说,不管是什么语言的文件操作,到最后都是由操作系统来进行的,即文件操作的底层都是一样的。只不过是不同的语言对系统调用进行了不同的封装罢了。

如下图所示:

系统接口比编程语言的函数更加底层。


既然不同的语言的文件操作归根结底都是去调用系统接口完成的,那我们现在来学一下在Linux中的文件操作的系统接口。 

close系统调用 

close系统调用是用于关闭文件的接口,当一个文件被打开后,就一定要关闭,我们通过这个系统接口来完成关闭文件。close接口如下: 

其中,fd是这个文件的描述符,文件描述符会在后边提到。

open系统调用

在Linux的系统调用中,有一个open的系统接口,这个接口就是将一个文件打开,我们先来看一下手册。 

我们可以看到,这里有三个接口,其中这里只介绍前两个。

首先,这些接口的返回值都是一个整形,指的是文件描述符,当文件打开失败时,返回-1,打开成功返回>=0的数,至于文件描述符到底是什么,我们后面再解释,同时这两个接口都有相同的两个参数,其中第一个很好理解,就是我们需要打开文件的路径,这个路径可以是相对路径,也可以是绝对路径,第二个参数是一个标记位,那么标记位是什么,我们来解释一下。

标记位 

标记位顾名思义就是用来做标记的,但具体怎么做呢?

一个整形由32个比特位组成,那么我可以将一个整形分为32种情况,每一种情况都是只有一位为1,其它位都为0,如下图所示:

同时在使用宏定义来完成我们的标记位。我们来看代码来理解。 

#include<stdio.h>

#define ONE   (1<<0)//十进制为1
#define TWO   (1<<1)//十进制为2
#define THREE (1<<2)//十进制为4
#define FOUR  (1<<3)//十进制为8

void show(int status)
{
    if(status & ONE)
    {
        printf("one\n");
    }
    if(status & TWO)
    {
        printf("two\n");
    }
    if(status & THREE)
    {
        printf("three\n");
    }
    if(status & FOUR)
    {
        printf("four\n");
    }
}

int main()
{
    show(ONE);
    printf("----------\n");
    show(TWO);
    printf("----------\n");
    show(THREE);
    printf("----------\n");
    show(FOUR);
    printf("----------\n");
    show(ONE | TWO | THREE);
    return 0;
}

运行结果:

通过上面的这种方式,就可以实现我们的标记位效果了。


理解了标记位后,我们在来看我们的open接口 

这个flags常用的标记位有这些:

        O_RDONLY:  只读,不能写

        O_WRONLY: 只写,不能读

        O_RDWR:      可读可写

        O_CREAT:     如果文件不存在,则创建这个文件

        O_APPEND:  在文件末尾追加

        O_TRUNC:     每次打开文件后,将文件的内容清空

当然远不止还有这些。

我们来演示一下用法。

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

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME,O_WRONLY | O_CREAT);//打开文件,且已读的形式打开,如果文件不存在,则创建这个文件
    if(fd == -1)//说明文件打开失败
    {
        perror("open:");
    }

    close(fd);//关闭文件

    return 0;
}

当我们的程序运行起来的时候,由于原本没有log.txt这个文件,所以就自动创建了一个,因为在status参数中,我们加了O_CREAT,但是创建出来的这个文件却是的。如下图所示。 

这是为什么呢?

首先,我们在创建文件的时候,文件都会有一个默认的权限,其中普通文件666目录777,但是我们这里创建文件的时候,并没有告诉操作系统,我们所创建的log.txt是什么权限,所以才会导致红的问题,所以这里就要用到open接口的第三个参数,传递一个权限值过去,如下图所示:

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

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);//打开文件,0666指创建的文件默认权限为666
    if(fd == -1)//说明文件打开失败
    {
        perror("open:");
    } 

    close(fd);//关闭文件

    return 0;
}

当运行这段代码的时候,我们的文件可以正常的被创建出来。 

write系统调用

当我们的文件打开成功后,就可以进行读写操作了,那么具体要怎么进行了,我们接着往下看。

首先我们来看一下write接口的声明:


这个系统调用有三个参数:文件描述符要写入内容的指针变量要写入的字节个数

对于这三个参数,我们一个一个来分析一下:

文件描述符:这文件描述符很好理解,就是open的返回值,所以write中的fd指的就是给这个文件进行写入操作 。

buf指针:指向要写入数据的缓冲区的指针,通俗的讲就是你要写入内容的起始地址

count:你要写入数据的字节数。

用法示例如下:

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

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_WRONLY | O_CREAT, 0666);//打开文件,0666指创建的文件默认权限为666
    if(fd == -1)//说明文件打开失败
    {
        perror("open:");
    }

    char arr[100];
    int cnt = 5;
    while(cnt)
    {
        sprintf(arr,"%s:%d\n","hello linux",cnt--);//将我们要写入的内容格式化输出到arr数组中
        write(fd,arr,strlen(arr));//将arr数组中的内容写到log.txt文件中
    }

    close(fd);//关闭文件

    return 0;
}

当我们运行起来后,就会在当前目录中创建一个log.txt的文件,以及文件的内容如下: 

  


在这里,在补充一些内容,虽然我们现在已经知道了如何写文件,但是这个过程是建立在这个文件是空文件的基础上的,如果我们在不是空文件的基础上,在进行写入操作,会出现问题,因为文件之前的内容不会被清空,所以对于这种情况,我们可以再打开文件的时候,再加一个标记位,如下图所示。

read系统调用

学习完写入的操作,现在再来看一下读的操作。同样的,我们先来看一下它的手册。


同样的read也有三个参数:要读文件的文件描述符将文件内容读到buf指针里要读几个字节

其中,当读到文件末尾时自动结束

我们要演示一下用法。 

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

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME, O_RDONLY, 0666);//打开文件,0666指创建的文件默认权限为666
    if(fd == -1)//说明文件打开失败
    {
        perror("open:");
    }

    char arr[100];
    read(fd,arr,sizeof(arr)-1);//将读到的内容放入arr数组中
    arr[strlen(arr)] = 0;//将arr数组的末尾加个\0,因为C语言规定的字符串是这样的
    printf("%s\n",arr);

    close(fd);

    return 0;
}

当这段程序运行起来的时候,会从log.txt文件中读取内容到arr数组中。 


看到这里,我们在Linux中用系统调用来进行文件操作就能进行了,但是文件的操作的系统调用还不止这些,在这里就不过多讲解了。 

二.文件操作的本质 

知道了在linux中用系统调用进行文件操作,那么文件操作的本质到底是什么呢?

文件操作的本质,可以说是进程和被打开文件之间的关系

首先,我们再来看一下文件描述符,在上面我们提到,当用open打开文件的时候,会返回一个文件描述符,这个文件描述符到底是什么呢?

我们先来把这个文件描述符打印出来看看。

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

#define FILE_NAME(NUM) "log.txt"#NUM

int main()
{
    int fd1 = open(FILE_NAME(1), O_WRONLY | O_CREAT, 0666);//打开文件,0666指创建的文件默认权限为666
    int fd2 = open(FILE_NAME(2), O_WRONLY | O_CREAT, 0666);
    int fd3 = open(FILE_NAME(3), O_WRONLY | O_CREAT, 0666);
    int fd4 = open(FILE_NAME(4), O_WRONLY | O_CREAT, 0666);
    int fd5 = open(FILE_NAME(5), O_WRONLY | O_CREAT, 0666);

    printf("%d\n",fd1);
    printf("%d\n",fd2);
    printf("%d\n",fd3);
    printf("%d\n",fd4);
    printf("%d\n",fd5);

    return 0;
}

我们多打开几个文件来看,这样更好解释 

运行结果:

发现运行结果是3,4,5,6,7,且跟我们的文件打开顺序是一样的,说明文件描述符是一个顺序的标识,那么既然是顺序的标识,那么0,1,2去那了呢?

当每个程序运行起来的时候,都会默认打开三个流stdin(标准输入流)   stdout(标准输出流)   stderr(标准错误流),这三个流分别对应键盘显示器显示器,文件描述符的0,1,2分别给到了这三个流,所以我们在打开一个文件的时候就是从3开始的。

那么我们用open打开一个文件,然后它有一个文件描述符,我们应该可以理解,那么为什么键盘和显示器也有文件描述符呢?

因为在Linux中有一个设计理念:在Linux下,一切皆文件,所以我们可以把键盘和显示器看出一个设备文件,所以它们也有文件描述符。


那从进程的角度去理解文件呢?

我们来看下图:

在我们的PCB进程控制块中,有一个struct files_struct *file的指针,这个指针指向的是一个数组,同时被指向的这个数组,里面存的也是指针,这里面的指针指向的每个被打开文件的struct file结构体,因为每一个被打开的文件,操作系统都会为其创建一个struct file的结构体,以便维护这个文件,所以文件描述符的本质就是struct files_struct这个结构体数组的下标

三.文件描述符的分配规则 

在上面我们知道了,在一个程序中如果我们打开一个文件,则它的文件描述符是从3开始的,因为0,1,2已经被占用了。

那如果我把0,1,2的文件关闭了呢?会发生什么?我们来写个代码来看一下。 

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

#define FILE_NAME "log.txt"

int main()
{
    close(0);//在打开文件之前,把stdin文件关闭掉

    int fd = open(FILE_NAME,O_RDONLY);//已读的形式打开文件
    assert(fd != -1);

    printf("fd:%d\n",fd);

    close(fd);

    return 0;
}

 运行结果:

通过我们的运行结果可以看到我们的log.txt文件的文件描述符变成了0,这个很好理解,因为本来stdin、stdout、stderr分别占用了0,1,2,但是现在把0号关闭掉了,所以0号空出来了位置,所以新打开的log.txt占用了0号的位置,同样的,如果把1或者2关掉,新打开的文件就会占用1或者2,所以这也说明了文件描述符是按照顺序寻找最小且没有占用的位置。

重定向 

什么是重定向呢?

我们先来看一段代码以及现象。 

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

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);//以写的形式打开文件,并且每次打开文件时都清空内容
    assert(fd != -1);

    printf("fd:%d\n",fd);//将这个文件的文件描述符输出

    close(fd);

    return 0;
}

 运行结果:

从这段代码和运行结果来看,很正常,没什么特别的。

但是现在在这段代码的基础上,我进行一个操作,把stdout关掉,看看会发生什么事情。 


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

#define FILE_NAME "log.txt"

int main()
{
    close(1);//把stdout关掉

    int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);//以写的形式打开文件,并且每次打开文件时都清空内容
    assert(fd != -1);

    printf("fd:%d\n",fd);

    fflush(stdout);

    close(fd);

    return 0;
}

运行结果:

从运行结果可以看到,把stdout关掉后,运行程序后没有东西输出,但是,我们发现,原本要输出内容输出到log.txt里面了。 

这是为什么呢?

因为printf函数就是向stdout中输出的,即输出到1号文件描述符中,但先把stdout关掉后,新打开的log.txt占用了1号文件描述符,所以当再输出时,printf函数就向log.txt中输出了,这种行为也叫输出重定向

如果在此基础上,打开文件的时候,再加上O_APPEND选项,那么再进行printf输出时,输出的内容就会追加到我们的log.txt文件中,这叫做追加重定向

同时的,如果我们将0号文件描述符关掉,把log.txt打开去占用0号文件描述符,当我们使用scanf去读的时候,就不会从键盘中读取了,而是从log.txt中读取。

如下面代码所示:

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

#define FILE_NAME "log.txt"

int main()
{

    close(0);
    int fd = open(FILE_NAME,O_RDONLY);
    assert(fd != -1);

    char arr[100];
    scanf("%s",&arr);
    puts(arr);

    return 0;
}

运行结果:

从运行结果可以看到,scanf函数变成了从log,.txt中读取内容, 这叫输入重定向


同时在我们的shell命令行解释器中,是可以直接支持这种操作的,例如:

输出重定向:ls > log.txt

追加重定向:ls >> log.txt

输出重定向:cat < log.txt 

fud2系统调用 

在上面,我们通过关闭0,1,2文件描述符的方式,可以达到重定向的功能,但是这种方法未免有点挫,所以在系统中,提供了一种系统调用给我们来实现重定向的功能,dup2系统调用。我们先来看一下它的声明。

可以看到dup有两个接口,但是我们重点谈第二个,因为第二个用的多。

dup2有两个参数,一个是oldfd,一个是newfd,即将oldfd拷贝到newfd中,什么意思呢?我们来看图

通过这个系统调用,我们就可以愉快的实现重定向功能了,如下面代码所示。 

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

#define FILE_NAME "log.txt"

int main()
{
    int fd = open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);
    assert(fd != -1);
    dup2(fd,1);//将文件log.txt的文件描述符拷贝给1号文件描述符

    printf("hello linux\n");

    close(fd);//关闭文件

    return 0;
}

运行结果:将hello linux输出到log.txt文件中

四.缓冲区的理解 

对于IO的部分,大家都应该有听说过缓冲区的概念,那么缓冲区到底是什么,我们又应该如何去理解呢?我们接着往下看。  


在讲之前,我们先来看一段代码以及现象。

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

#define FILE_NAME "log.txt"

int main()
{
    printf("printf :hello linux\n");
    fprintf(stdout,"fprintf:hello linux\n");
    fputs("fputs  :hello linux\n",stdout);

    const char arr[100] = "write  :hello linux\n";
    write(1,arr,strlen(arr));

    fork();

    return 0;
}

当我们默认用./test运行程序时,运行结果如下: 

现在看来代码和运行结果还很正常,所以接下来我们做一个将运行结果重定向到log.txt文件的操作。 


通过结果对比可以发现,当将程序运行,并默认输出到屏幕上的时候,只有四条内容,但是我们重定向后输出到log.txt中,就变成了7条内容,其中多出来的3条内容是C语言多打印出来的,也就是说C语言函数的输出,输出了两遍。

这是为什么呢?

首先可以明确给出,这个跟代码中的fork有关,并且与缓冲区也有关

在解释之前,我们来看理解一下什么是缓冲区。 


对于缓冲区的理解,可能大家都知道,当我们向显示器,磁盘或者其他外设进行写入的时候,是不会立即马上将数据写入到外设中的,会先将数据存在缓冲区中,当缓冲区满了,或者用户主动刷新等操作时,才会将数据从缓冲区写入到我们的外设中,其中缓冲区的刷新策略有3种情况,2种特殊情况。

1.立即刷新——无缓冲

2.行刷新——行缓冲,例如显示器

3.缓冲区满刷新——全缓冲,例如,磁盘

特殊情况:

1.用户强制刷新,如fflush函数

2.进程退出刷新,进程退出的时候,缓冲区会全部刷新


那么缓冲区到底是什么?

缓冲区是用户级语言层面提供给我们的缓冲区,例如,在C语言的输入输出中,它的缓冲区就是C语言提供给我们的缓冲区

C语言的文件操作中,当我们打开一个文件的时候,是要返回一个FILE的结构体指针

其中在这个结构体里面,就存储着很多char类型的指针,这些指针就是我们所说的缓冲区,所以说白了,在C语言中,缓冲区只不过是FILE结构体中的一个char类型的指针而已。

如下图所示,我将C语言库中的libio.h库头文件打开,找到里面的FILE结构体,可以看到在这个结构体中,有着大量的char类型的指针,这些指针指向的内容就是我们所谓的缓冲区。

这个时候在看待C语言的缓冲区问题,我们就很清楚了。但是这仅仅是C语言的缓冲区问题,那么对于我们的系统来说,又是怎么样的呢,我们知道所有语言的操作的底层都是去调用系统接口进行的,语言层面只不过是对系统接口进行了一个封装罢了,所以当我们的数据保存到了FILE结构体里面的缓冲区的时候,再更进一层的写入就是跟操作系统有关了。我们来看一下图。

所以我们的输入输出的过程,就大概像上图所示。

对于C语言的缓冲区来说,将C语言的缓冲区数据写入内核缓冲区中,我们有一个fflush函数可以直接将C语言缓冲区中的数据写入到内核缓冲区种,但是对于内核缓冲区来说,操作系统将内核缓冲区中的数据写入到磁盘时,是由操作系统自主决定的。

所以这时就有了一个问题,如果我们已经将数据从C语言的缓冲区写入到了内核缓冲区中,但是这个时候突然停电了,导致电脑关机,使得内核缓冲区中的数据还没写入到磁盘中, 导致数据丢失,显然这种情况时不允许发生的,那么怎么办呢?

操作系统给我们提供了一个系统调用,可以将内核缓冲区中的数据强制写入到磁盘中。

fsync,这个系统调用,可以直接强制刷新内核缓冲区,避免我们所说的情况。


那么现在我们再来解释一下开头那段代码的情况: 

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

#define FILE_NAME "log.txt"

int main()
{
    printf("printf :hello linux\n");
    fprintf(stdout,"fprintf:hello linux\n");
    fputs("fputs  :hello linux\n",stdout);

    const char arr[100] = "write  :hello linux\n";
    write(1,arr,strlen(arr));

    fork();

    return 0;
}

在这段代码中,当我们将运行结果重定向到log.txt时,C语言函数的输出会出现两次: 

但是如果不是重定向到log.txt时,是直接运行输出,却没有这种情况,而是很正常的一种情况:

 

首先我们来分析一下只输出四句的这种情况:

我们知道缓冲区的刷新策略有3种情况,2种特殊情况,在这里符合第二种,行缓冲,因为在我们的各种printf函数中,有\n,这种情况符合行缓冲,即在fork创建子进程之前,字符串就已经输出到我们的显示器上了。

接下来分析将输出重定向到log.txt的情况,因为这个时候不再是向显示器中输出,而是向磁盘中输出,符合缓冲区刷新策略的第三种,全缓冲,当缓冲区满了的时候,才输出,所以这个时候\n不再起作用了,即在我们fork之前,字符串还在FILE的缓冲区中,fork之后,创建了子进程,子进程继承了父进程的所有代码和数据,所以子进程的FILE缓冲区也有一份字符串,当进程结束的时候,父进程和子进程都将直接缓冲区中的字符串输出出来,所有才有了两份。

过程如下图所示:

这就是为什么上面的代码会出现7条的原因。 

模拟C语言中文件操作函数的实现 

对于缓冲区的理解,你可能有一些概念了,那么如何更好的理解呢?

我们来模拟实现C语言中的文件操作来试一下。 


在这部分中,我们将来模拟实现一下在C语言中,文件操作函数的模拟。

其中我们模拟下面这些内容:

FILE文件结构体、

fopen函数、

fclose函数、

fwrite函数。


且在这一部分中,我将会使用模块化编程。 

MyStdio.h 

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

#define SIZE 1024

//文件结构体
typedef struct FILE_ 
{
    int fileno;//文件描述符
    char buffer[SIZE];//缓冲区
    int pos;//记录缓冲区末尾的位置,指向\0的位置
}FILE_;

FILE_* fopen_(const char* pathname,const char* mode);//我自己的文件打开函数

void fwrite_(const void* ptr,int sz,FILE_* stream);//我自己的文件写入函数

void fflush_(FILE_* stream);//我直接的刷新缓冲区函数

void fclose_(FILE_* fp);//我自己的文件关闭函数

MyStdio.c 

#include"MyStdio.h"

//我自己的文件打开函数,其中参数为:要打开文件的名字,以什么方式打开
FILE_* fopen_(const char* pathname,const char* mode)
{
    //判断文件的打开方式
    int status = 0;
    if(strcmp(mode,"w") == 0)
    {
        status |= O_WRONLY | O_CREAT | O_TRUNC;
    }
    else if(strcmp(mode,"r") == 0)
    {
        status |= O_RDONLY;
    }
    else if(strcmp(mode,"a") == 0)
    {
        status |= O_WRONLY | O_CREAT | O_APPEND;
    }

    //判断打开文件时,是否需要创建文件
    int fd = 0;//存储文件描述符
    if((status | O_WRONLY) > 0)
    {
        fd = open(pathname,status,0666);
    }
    else 
    {
        fd = open(pathname,status);
    }
    
    //说明文件打开失败
    if(fd < 0)
    {
        const char* err = strerror(errno);
        printf("open:%s",err);
        return NULL;
    }

    FILE_* file = (FILE_*)malloc(sizeof(FILE_));//开辟文件FILE_结构体

    file->fileno =  fd;
    memset(file->buffer,0,sizeof(file->buffer));//将缓冲区数组中的内容全部置0
    file->pos = 0;
    return file;
}

//我自己的刷新缓冲区函数,其中参数为:那个文件的结构体
void fflush_(FILE_* stream)
{
    if(stream->pos > 0)//说明缓冲区中有数据,将缓冲区中全部数据写入
    {
        write(stream->fileno,stream->buffer,stream->pos);
        fsync(stream->fileno);//强制刷新内核缓冲区的数据
    }
    stream->pos = 0;//将缓冲区末尾位置置0
}

//我自己的写入数据函数,其中参数为:要写入内容的起始指针位置,要写入的字节数,写到那个文件中
void fwrite_(const void* ptr,int sz,FILE_* stream)
{
    strcpy((char*)stream->buffer + stream->pos,(char*)ptr);
    stream->pos += sz;

    //如果缓冲区中有\n,说明符合行缓冲,进程缓冲区的刷新
    if(stream->buffer[stream->pos-1] == '\n')
    {
        fflush_(stream);
    }
    
    //如果缓冲区满了,进行缓冲区刷新
    if(stream->pos > 100)
    {
        fflush_(stream);
    }

}

//我自己的文件关闭函数,其中参数为:要关闭那个文件
void fclose_(FILE_* fp)
{
    fflush_(fp);//文件关闭前,刷新缓冲区
    close(fp->fileno);
    free(fp);//关闭文件最后,释放内存
}

main.c 

#include"MyStdio.h"

#define FILE_NAME "log.txt"

int main()
{
    FILE_* fp = fopen_(FILE_NAME,"a");
    
    const char* arr = "hello linux\n";

    fwrite_(arr,strlen(arr),fp);

    fclose_(fp);
    return 0;
}

写到这里,我们的自己模拟实现的C语言文件操作函数就完成了,不过我这里实现的是非常简单的,经不起推敲,实现这个只不过是为了加深文件操作过程的理解,和缓冲区的理解罢了。 

五.软硬链接

什么是软硬链接,在讲解之前,我们先来简单的了解以下文件系统

在一个目录中,从我们人的视角来看,文件名可以唯一标识一个文件,这看起来很简单,但是对于计算机而言,就复杂的多了,但是我也不详细讲,就简单的了解以下。 


我们先来看一个现象,首先我们先用        ll -i        命令来将当前路径的内容显示出来:

通过现象我们可以看到,在每个文件的前面都有一串数字,那么这串数字又是什么呢?

这串数字叫做inode,且每个inode唯一标识一个文件,也就是说,对于我们人来看,我们找一个文件是通过文件名来找的,但是对于操作系统而言,找一个文件是通过inode来找的,inode和目录和文件的关系,我们可以简单的用下图来表示。 

在我们的每一个目录中,都会存储着一个映射关系表,这个映射关系表里面有存储着每一个文件的文件名和inode的映射关系,通过这种映射关系,我们的目录就可以找到对应文件的inode,再通过inode就可以找到对应的文件了。

这只不过是非常简单的理解而已,如果要深入理解会非常复杂,还要理解一下硬件层面的东西,但是在这里,就不讲了。


大概简单的了解了一下文件系统,那么软硬链接到底是什么呢?

我们来举一些例子来看一下。 

软链接 

首先我们来看一下软链接

在我们的当前目录中,有下面这样文件。 

其中,我们的log.txt文件中还有一串“hello linux”的字符串。

这个时候我们来创建一个软链接,创建软链接的方式是:

ln   -s   <target>   <link_name> 

其中,<target>为我们要创建软链接的目标文件,<link_name>为要创建软链接的名称

如上图所示,我们给log.txt文件创建一个软链接,并其名称为tmp,可以看到,在当前目录下,多一条        tmp -> log.txt        的内容,这就是我们软链接所创建的,同时我们查看一下tmp的内容,可以发现,其内容和log.txt是一样的

这种方式就好比我们在windows中的快捷方式一样

同时我们在查看一下它们的inode,会发现log.txt和tmp的inode是不一样的。

也就是说,软链接会给该快捷方式创建一个新的inode,同时在当前目录中,添加新的映射关系,如下图所示。

如果这个时候,我们将log.txt删除掉会发生什么事情呢?

我们来看看。 

当把log.txt删除后,tmp就会一直闪红,因为tmp只不过是log.txt的快捷方式罢了,删除log.txt,tmp也就自然而然找不到log.txt了。 


那么软链接有什么用呢?

答案应该很明显了,软链接就像我们在windows下的快捷方式一样,如果某个程序或者文件的路径很深的时候,不方便我们查找,我们就可以用软链接的方式将它简化。 

硬链接 

看完了软链接,我们来看一下硬链接

硬链接的创建方式是:

ln   <target>   <link_name>

与软链接的用法一致,只不过是没有  -s  选项

通过对log.txt进行硬链接后,我们可以看到在当前目录中,多了一个alias的文件,这个文件其实就是我们的log.txt文件,只不过是起了一个别名罢了。这就是硬链接的用法。

这个时候,同样的,我们来看一下log.txt和alias的inode来看看。

可以看到,log.txtaliasinode是相同的,硬链接的原理是在当前目录中,多加一个文件名和inode的映射关系罢了,如下图所示:

所以软链接和硬链接的区别就是,有没有创建一个新的inode,其中,软链接是会创建新的inode的,而硬链接不会。


这个时候,我们也来试一下,把log.txt删掉会怎么样。 

将log.txt删掉后,我们可以看到,对应的alias还在,同时这个alias是不受影响的,我把它的内容输出出来是一点问题都没有的。

软链接我们可以看到,把log.txt删掉后,软链接就直接用不了了,而硬链接还能用,这是为什么呢?

在解释这个问题之前,我们先来看另一个现象。 

引用计数 

什么是引用计数

在每一个inode中,都会存储着一个引用计数,这个引用计数指的是有多少个文件名引用了这个文件,光说很难理解,我们来详细看看。 


首先,我找一个空目录,在这个空目录中,创建两个文件:test.c和log.txt。

再把这个目录的内容列出来。

在文件的详细信息中,我们可以看到,有一栏数字内容,那么这个数字是什么意思呢?

这个数字表示该文件被引用的次数,即,log.txt文件被引用了一次,可能这样还是很难理解,不怕,我们接着往下看。

我们创建一个目录看看。

  

从上图我们可以看到,当是目录的时候引用计数为2,这是为什么呢?

因为当前tmp目录被两个文件名引用了,一个是tmp,另一个是目录中的

这就是所谓的引用计数。


现在再回顾头来看我们的硬链接遗留的问题 

把log.txt删掉会怎么样。 

将log.txt删掉后,我们可以看到,对应的alias还在,同时这个alias是不受影响的,我把它的内容输出出来是一点问题都没有的。

当我们把log.txt删掉的时候,只不过是将对应的inode引用计数减减,减减之后,inode的引用计数为1,而不为0,所以源文件log.txt是不会删除的,删除的真正意义是inode的引用计数为0才删除。

如下图所示。

该删除log.txt的过程如下:

六.acm时间 

在这里补充一下,什么是acm时间。

首先我们使用        stat   <一个文件>        命令将文件的详细信息时间列出来。

可以看到,我们的文件有三个时间Access、Modify、Change

这三个时间分别对应:访问时间修改时间更改时间

我们先来看后两个,修改时间更改时间有什么区别呢?

更改时间指的是文件属性最近一次被更改的时间,例如下图所示:

接下来是修改时间,修改时间指的是文件内容最近一次被修改的时间,如下图所示:

我们使用vim来修改log.txt的内容。修改后,Modify时间发生了改变,同时Change时间也改变了,因为文件大小变大了,文件的大小也是文件属性的一部分,所以Change时间也改变了。

最后是Access时间,是指访问时间,当我们访问这个文件的时候文件,访问时间就会更新,但是不一定会更新。为什么呢?

因为在我们的日常操作中,访问一个文件是很频繁的,如果每访问一次,就去更新Access时间,就会存在大量IO去修改文件的信息,这样会导致效率低下,所以为了避免效率低下,Linux采用一种方式来更新Access时间,就是当访问了一定的次数或者隔了一定的时间时,才更新Access时间。  

  • 15
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值