yo!这里是文件IO入门介绍

目录

前言

预备知识点

文件相关系统调用

open

close

write

read

文件描述符

本质

重定向实现

缓冲区

文件系统

软硬链接

软链接

硬链接

动静态库

静态库

动态库

后记


前言

        在结束了进程相关重要知识点之后,下一个我们来到文件的输入输出的相关知识点,因为整个IO只是体系很庞大,涉及到的知识点非常之多,所以本篇文章也只是属于一个入门级别的对IO的理解,虽然说是入门级别,但是难度与复杂度一直在线,需要仔细深入理解,希望能够帮助到大家理清楚一些思路和混淆的概念,在进入到Linux下的文件IO介绍之前呢,先来了解几个预备知识点,建立一个基本或者共同的认知。

预备知识点

①文件由文件内容和文件属性组成;

②涉及文件的所有操作要么是对于内容,要么对于属性;

③文件在磁盘上(硬件)上存放,按理来说只有os能够访问,或者有权利访问,进一步也可以说是进程在访问,如果普通用户也想访问(硬件),那必须由os提供接口使得用户得以访问硬件,这系列接口就是属于文件类的系统调用接口,但是这类接口一是较难,学习成本高(不直接使用,但所有语言层面的文件接口都封装的是它,很重要,所以要了解学习),二是不跨平台(一旦使用了系统调用接口编写代码,就无法在其他平台直接运行,因为跨平台的原理就是通过封装将所有平台代码实现一遍,再通过条件编译实现跨平台),所以一般在语言层面都会封装这些接口,降低使用难度;

④站在系统的角度,文件可以说是是一种设备,这种设备能够被input读取到内存,或者从内存output写出,比如显示器、键盘、网卡、声卡、显卡、键盘等,如图所示:

yo!这里是文件IO入门介绍_第1张图片

当前路径:当一个进程运行起来时,每个进程都会记录自己当前所在的工作路径,这个工作路径叫做为当前路径。

⑥c语言文件接口使用

写文件的相关操作:

yo!这里是文件IO入门介绍_第2张图片

读文件的相关操作:

yo!这里是文件IO入门介绍_第3张图片

文件相关系统调用

        前面提到过,语言层面的文件读写操作函数是对系统调用接口做了封装,在上文熟悉了c语言的文件操作函数之后,下面的系统接口就会感觉到似曾相识。

  • open

yo!这里是文件IO入门介绍_第4张图片

参数:

        pathname:要打开的文件名

        flags:打开文件的方式,可填入以下选项(可通过【|】结合,但前三个必须且只能有一个)

                O_RDONLY:只读打开

                O_WRONLY:只写打开

                O_RDWR:读写打开

                O_CREAT:若文件不存在则创建,加上mode参数可指定文件权限
                O_APPEND:追加写

                O_TRUNC:清空写

        mode:文件不存在时创建文件时文件的权限八进制形式

返回值:

        若成功,打开文件的文件描述符,否则返回-1

注意:

        flags参数必须需要解释一下,先举个例子,int的32位比特位,每位只能是0/1,将其视为一种状态,为1时代表状态打开,为0代表状态关闭。比如,0001就代表ONE这种状态打开了,0010代表TWO状态打开了(此时ONE状态关闭),0100代表THREE状态打开,那如何表示ONE、TWO、THREE状态都打开呢?可以将其或在一起,即ONE|TWO|THREE就代表三种状态都打开了。类似地,上面的O_WRONLY等选项也是代表一种状态,将其或在一起即可。

常用:

        O_RDONLY:对应c语言中的r

        O_WRONLY|O_CREAT|O_TRUNC:对应c语言中的w

        O_WRONLY|O_CREAT|O_APPEND:对应c语言中的a

eg:

  • close

yo!这里是文件IO入门介绍_第5张图片

        close不做过多解释,与在c语言中学的fclose一致,都是打开一个文件,执行完读写操作之后将其关闭。

  • write

yo!这里是文件IO入门介绍_第6张图片

参数:

        fd:要写的文件的文件描述符;

        buf:数据的存储空间首地址;

        count:要写的字节数

返回值:

        成功返回实际写入的字节数 ,失败返回-1

eg:

yo!这里是文件IO入门介绍_第7张图片

  • read

yo!这里是文件IO入门介绍_第8张图片

参数:

        fd:要写的文件的文件描述符;

        buf:存储空间首地址;

        count:要读的字节数

返回值:

        成功返回实际读取的字节数 ,失败返回-1

eg:

yo!这里是文件IO入门介绍_第9张图片

文件描述符

  • 本质

        file descriptor是文件描述符,简称fd,我们知道open函数返回fd,是一个int类型,所以fd是一个整数。当我们使用open接口打开一个文件时,os就会创建相关数据结构来存放文件属性等基本信息,如此就有了file结构体(并不是c语言中的FILE结构体)。

        同时呢,一个进程可能会打开多个文件,而进程需要将这些文件组织起来,所以进程的task_struct中有一个files指针指向一个files_struct,这是一个内核结构体,里面有一个指针数组,里面存放file*,即存放多个文件的file的地址。

        我们在c阶段时也听过标准输入、标准输出、标准错误,分别对应键盘、显示器、显示器,前面提到过,这三个外设在os看来都是文件,所以每个进程在打开时都会默认打开这三个文件,所以每个files_struct中都会默认存放这三个文件的地址,下标分别为0、1、2,而这三个文件的fd也正是对应0,1,2。是的,你没想错,文件在files_struct这个指针数组的下标正是对应的fd文件描述符,而且新打开的文件会依次向后放,对应fd也分别为3、4、5.......,见下图方便理解以上文字:

yo!这里是文件IO入门介绍_第10张图片

  • 重定向实现

        新打开的文件的fd一定是从3开始吗,如果我们使用close接口将前三个的任一个关闭,那么之后新打开的文件的fd是多少呢?见下图,我们将标准输出1关闭,紧接着打开一个文件,可见原本应该打印到显示屏上的数据打印到了文件中,所以对于files_struct这个指针数组是先找到没有使用的最小的下标位置存放新文件的file,此下标作为文件的fd。

yo!这里是文件IO入门介绍_第11张图片

        根据上面的发现,我们可以模拟实现以下重定向的功能,包括输出重定向>、追加重定向>>、输入重定向<,但是在此之前,需要介绍一下dup2这个系统调用。

yo!这里是文件IO入门介绍_第12张图片

        参数就是传入两个文件的fd,功能就是在files_struct指针数组中,将下标为oldfd的值赋给下标为newfd的元素,也就是都会变成下标为oldfd的值,下图可辅助理解:

yo!这里是文件IO入门介绍_第13张图片

        有了此系统调用,就无需使用先close再创建新文件的粗暴方法了。对于输入重定向,正常输入是从键盘(文件)输入,重定向之后就是从指定文件输入,而标准输入是stdin(0),所以使用dup2(fd,0)即可将原本指向键盘的file指针指向文件描述符为fd的文件;对于输出重定向,正常输出是输出到显示屏上,重定向之后就是输出到指定文件中,而标准输出是stdout(0),所以使用dup2(fd,1)即可将原本指向显示屏的file指针指向文件描述符为fd的文件,追加重定向与输出重定向同理,代码如下:

    //输入重定向   //正常输入是从键盘输入,重定向之后就是从指定地方输入
    int fd =open("draft.txt",O_RDONLY);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    printf("打开成功,fd:%d\n",fd);
    dup2(fd,0);
    char tmp[64]={0};
    fgets(tmp,sizeof(tmp),stdin);
    printf("%s\n",tmp);
    close(fd);

    //追加重定向
    //int fd =open("draft.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    //输出重定向   //正常输出是输出到显示器,重定向之后就是输出到指定地方
    int fd =open("draft.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    printf("打开成功,fd:%d\n",fd);
    dup2(fd,1);
    const char* str="hello fprintf\n";
    fprintf(stdout,"%s",str);//经过dup2,原本1(stdout)指向的显示器,就指向了draft.txt
    
    fflush(stdout);
    close(fd);

缓冲区

        缓冲区是一段内存空间,实际上,语言库和os中都有自己的缓冲区,语言库的缓冲区存在于c标准库的FILE结构体中,OS的缓冲区存在于内核file结构体中,一般说的缓冲区都是语言层面的缓冲区,对于os中的缓冲区不过多关注。

缓冲区的刷新策略:

        ①立即刷新;

        ②行刷新(行缓冲),一般情况下是显示器的刷新规则;

        ③满刷新(全缓冲),一般情况下是磁盘文件的刷新规则;

        ④特殊情况,包括用户强制刷新(fflush)、进程退出,

注意:一般所有设备都倾向于全缓冲,因为等到缓冲区满了再刷新可以减少IO操作,要知道,更少对外设的访问,整机效率会更高,而其他策略只是针对于个别情况做的改变,比如,输出到显示器的内容是需要给用户看到的,所以是行缓冲,一方面要考虑效率,一方面考虑用户体验。

文件系统

        想理解文件系统,就得从存放文件的磁盘说起,向磁盘写入数据的本质就是os使用磁头(如图2所示)改变磁盘上的正负性,且存储数据的基本单位是扇区(如图2、3所示),一般为512字节,os通过chs寻址(先确定在哪个磁盘面上,再确定哪个磁道上,然后确定哪个扇区)的方法将数据写入指定扇区。

yo!这里是文件IO入门介绍_第14张图片

        从逻辑层面来看,磁盘上的圆形磁道,可以想象成一个数组(类似磁带),只要知道数组下标就能访问一个扇区,将数据存储到磁道就是存到该数组上,找到磁盘中特定扇区的位置就是找到数组特定的位置,对磁盘的管理就变成了对数组的管理。

        如下图所示,将磁道进行分区,对于每个分区,Boot Block 叫做启动块,存储开机信息等,每个分区都有一份(目的是备份),之后又将分区分为若干个Block Group(块组),对于每个块组也都有着相应的构成,包括:

        ①Super Block(超级块):存放文件系统本身的结构信息,包括bolck 和 inode的总量, 未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息,如果Super Block的信息被破坏,可以说整个文件系统结构就被破坏了;

        ②Group Descriptor Table(块组描述符):描述块组属性信息,如块组大小、inode数量;

        ③Block Bitmap(块位图):记录Data Block中哪个数据块已经被占用,哪个数据块没有被占用,一个bit位对应一个block;

        ④inode Bitmap(inode位图):与块位图一致,每个bit对应一个inode。

        ⑤inode Table(i节点表):该块组中所有inode空间的集合,存放文件属性,如文件大小,所有者,最近修改时间等;

        ⑥Data Blocks(数据区):是多个4kb(8*扇区)大小空间的集合,存放文件内容。

yo!这里是文件IO入门介绍_第15张图片

注意(重要):

        inode:是大小为128字节的空间,保存文件属性,每个inode都有一个inode编号。一般地,一个文件一个inode一个inode编号,但一个文件不止有一个data block。通过inode编号找到inode,里面存放着一个数组,此数组记录着所有内容所在的data block的块号,进而找到所有内容,当然,data block中不止可以存放数组,也可存放其他data block的块号,当文件内容很大时,就可以通过这种方式进行“扩容”存放剩下的内容。

        inode与文件名:上文讲到,找到inode编号,进而找到inode,也就找到了文件的属性和内容,那如何知道inode编号呢?从目录的结构中!

        我们知道,同一个目录下不能有重复的文件名,文件名可以是文件的标识符,但在inode的属性中并无文件名的说法。由“一切皆文件”,得目录也是个文件,目录的inode中可以存储目录的相关属性,但它的data block中存储什么呢?其实,其中存储的就是文件名和inode的映射关系(可以理解为kv结构中k与v的关系),在某目录下访问一个文件,根据文件名在目录的data block中找到此文件的inode编号,进而得到inode,也就得到了此文件的属性和内容了。

        同时,也能解释目录的权限问题了,比如,为什么在目录下创建文件需要写权限,因为要将文件名机器与inode编号的映射关系写进目录的data block中。

软硬链接

  • 软链接

        软连接有独立的inode(ls指令加上-i选项可以看到inode),是一个独立的文件,data block里存储的是指向文件的所在路径,相当于c++语法中的引用,也相当于window下的快捷方式。

建立软连接:ln -s 源文件 目标文件

说明:源文件是被链接的文件,目标文件就是起的别名(新),如图一所示

注意:若是链接的是文件名,则只能是在与源文件相同的目录下使用,最好链接的是源文件的路径,当移动到其他目录下时也能使用,如图二、三所示。

eg:

yo!这里是文件IO入门介绍_第16张图片

yo!这里是文件IO入门介绍_第17张图片

yo!这里是文件IO入门介绍_第18张图片

删除软连接:使用rm指令或者unlink 文件名,删除下面的硬链接也是如此。 

  • 硬链接

        硬链接没有独立的inode,不是一个独立的文件,与软连接相对应的是,硬链接是通过inode引用另外一个文件,值得注意的是,硬链接不是没有inode,只是没有独立的inode。

建立硬链接:ln 源文件 目标文件

说明:源文件是被链接的文件,目标文件是硬链接名,如图一所示

注意:硬链接是可以放到任意目录下使用的,如图二所示。

eg:

yo!这里是文件IO入门介绍_第19张图片

        到底如何理解硬链接没有独立的inode,不是个独立的文件,或者说硬链接究竟是做了什么?

        我们在文件系统中说过,通过文件名,找到inode编号,进而找到inode,以找到属性和内容。其实,真正找到文件属性和内容的并不是文件名,而是inode,Linux是可以让多个文件名对应同一个inode,而硬链接就是创建了一个文件名,以及在当前目录下的data block中存储文件名及其与inode的映射关系。

         如下图,当我们使用ll指令查看当前目录所有文件信息时,有一个数(圈出来)在之前没讲,这个数就是硬链接数,可以看到,当创建了一个硬链接之后,这个数字就会++,删除就会--,实质上时采用了引用计数计数,可知道与此inode关联的文件名的个数,当此数减为0时,这个文件才是真正意义上的删除(即没有文件名与此inode关联了)。值得注意的是,可以看到硬链接创建出来的文件名的信息与被链接的文件名的信息完全一样。

yo!这里是文件IO入门介绍_第20张图片

硬链接的用处:

        可以想想,默认创建的目录,硬链接数为啥是2(如下图一)?因为有两个目录名与此inode关联,一个是当前目录名dir,一个是dir内的【 . 】目录名与此inode关联;当在dir中再创建一个目录d1(如图二),为啥dir的硬连接数变成了3,因为除了上面两个目录名与此inode关联之外,还有一个是d1中【 .. 】目录名与此inode关联。

yo!这里是文件IO入门介绍_第21张图片

动静态库

  • 静态库

        静态库以【.a】为后缀,程序在编译链接时把库的代码链接到可执行文件中,在运行的时候不再需要静态库(与动态库对比着来说的)。

生成静态库:ar -rc lib+库名+.a 所有.o文件

说明:lib+库名+.a中,前面的lib和后面的.a是规定要加上,中间的库名可任意起,eg:libproject.a,所有.o文件就是在后面加上所有需要的.o文件,也就是你写的代码编译后的文件

eg:ar -rc libproject.a test.o add.o

注意:其实将所有.o文件和对应头文件直接发给别人(即不生成静态库),别人与自己写的主函数程序一链接也可运行,实质上生成静态库类似一个打包.o文件的过程,可认为在发送过程中防止丢失。

        如图一,借助Makefile解释如何生成静态库文件,将自己写的.c文件编译成.o文件,再用生成静态库的指令生成静态库;

        如图二,借助Makefile解释如何发送给别人,直接将生成的静态库发送过去对方肯定不能用,要把对应的头文件也得发过去。创建一个total文件夹,包括include文件夹和lib库文件夹,在include文件夹放入头文件,在lib文件夹放入.a静态库,将total文件夹整个打包给别人。(如图情况下,直接make total 即可打包)

yo!这里是文件IO入门介绍_第22张图片

yo!这里是文件IO入门介绍_第23张图片

         以上是将静态库和对应头文件发送给了别人,那别人拿到了total文件夹又如何使用呢?直接编译,但需要指定文件,如下图,需要使用-I和-L选项找到指定目录下的头文件和库,后面的【-lproject】,其中,-l是规定要加的,后面就是库名,以上即可成功运行主函数。

yo!这里是文件IO入门介绍_第24张图片

  • 动态库

        动态库以【.so】为后缀,程序在运行时才去链接动态库的代码,多个程序可共享使用库的代码。

创建动态库:gcc -shared 所有.o文件 -o lib+库名.so

说明:-shared 表示生成共享库模式;生成.o文件时必须加上-fPIC,表示产生位置无关码;而对于lib+库名.so,lib和.so都是固定必须要加上的。

        如图一,借助Makefile解释如何生成动态库文件,发送给别人也是如同静态库一样,将.so文件放进lib目录中,然后将total发送给别人。

yo!这里是文件IO入门介绍_第25张图片

        当别人收到total目录之后,又该如何使用呢?还是像静态库那样吗?不是的,使用的方法有很多,这里我使用一种,如下图一,就是使用软链接的方法,将动态库文件链接到lib64目录下(注意是动态库所在路径),然后再使用gcc命令去编译运行主程序(注意gcc命令与使用静态库时一样,加上-o mymain只是改了个可执行程序的名),可以看到也是成功运行(如下图二)。

        我们可以看到,使用动静态库时编译使用的gcc语句都是一样的,那么当lib目录下既有静态库文件又有动态库文件,使用gcc语句是怎么样呢?

        如下图,如果将生成的静态库文件和动态库文件打包在一起,一块发给别人(使用make output),当同时存在动静态库时,会默认使用动态库,但又没有前面软链接的步骤,就会使得运行时报错(有静态库文件也没用);若非要用静态库,就得在gcc语句的最后加上-static的选项,就会使用静态库文件。

yo!这里是文件IO入门介绍_第26张图片

后记

        本篇讲了文件相关的系统调用接口、文件系统、软硬链接、动静态库等相关知识点,重点在于对于文件系统的理解,比较晦涩难懂,需要反复斟酌,加上软硬链接和动静态库的知识点的繁多,使得学习难度有了一定的提升,但是文件IO的知识点并不只有这些,这些只是为后面知识点的学习打下一个基础,希望可以反复阅读此文章,特别是加粗部分的文字,加油拜拜。


你可能感兴趣的:(linux,后端,c语言,职场和发展,c++,服务器)