Linux中的文件IO

(1).文件IOIO就是输入输出,文件IO就是读写文件。

文件IO & 标准IO

(1)文件IO就指的是我们当前在讲的open、close、write、read等API函数构成的一套用来读写文件的体系,这套体系可以很好的完成文件读写,但是效率并不是最高的。

(2)应用层C语言库函数提供了一些用来做文件读写的函数列表,叫标准IO。标准IO由一系列的C库函数构成(fopen、fclose、fwrite、fread),这些标准IO函数其实是由文件IO封装而来的(fopen内部其实调用的还是open,fwrite内部还是通过write来完成文件写入的)。标准IO加了封装之后主要是为了在应用层添加一个缓冲机制,这样我们通过fwrite写入的内容不是直接进入内核中的buf,而是先进入应用层标准IO库自己维护的buf中,然后标准IO库自己根据操作系统单次write的最佳count来选择好的时机来完成write到内核中的buf(内核中的buf再根据硬盘的特性来选择好的实际去最终写入硬盘中)。

标准IO和文件IO有什么区别

(1)看起来使用时都是函数,但是:标准IO是C库函数,而文件IO是linux系统的API

(2)C语言库函数是由API封装而来的。库函数内部也是通过调用API来完成操作的,但是库函数因为多了一层封装,所以比API要更加好用一些。

(3)库函数比API还有一个优势就是:API在不同的操作系统之间是不能通用的,但是C库函数在不同操作系统中几乎是一样的。所以C库函数具有可移植性而API不具有可移植性。

(4)性能上和易用性上看,C库函数一般要好一些。譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能要更高。

 

3.1.14.2、常用标准IO函数介绍

(1)常见的标准IO库函数有:fopen、fclose、fwrite、fread、ffulsh、fseek

 

 

(2)文件描述符(fd):

 

(1). 文件描述符就是用来区分一个程序打开的多个文件的。

文件描述符作用域:当前进程

open返回的fd程序必须记录好,如果在关闭文件前丢了文件描述符,该文件将不能读写和关闭

(2)文件描述符的本质是一个数字,这个数字本质上是进程表中文件描述符表的一个表项,进程通过文件描述符作为index去索引查表得到文件表指针,再间接访问得到这个文件对应的文件表。

(3)操作系统规定,fd从0开始依次增加。fd也是有最大限制的,在linux的早期版本中(0.11)fd最大是20,所以当时一个进程最多允许打开20个文件。linux中文件描述符表是个数组(不是链表),所以这个文件描述符表其实就是一个数组,fd是index,文件表指针是value

(4)当我们去open时,内核会从文件描述符表中挑选一个最小的未被使用的数字给我们返回。也就是说如果之前fd已经占满了0-9,那么我们下次open得到的一定是10.(但是如果上一个fd得到的是9,下一个不一定是10,这是因为可能前面更小的一个fd已经被close释放掉了)

(5)fd中0、1、2已经默认被系统占用了,因此用户进程得到的最小的fd就是3了。

(6)linux内核占用了0、1、2这三个fd是有用的,当我们运行一个程序得到一个进程时,内部就默认已经打开了3个文件,这三个文件对应的fd就是0、1、2。这三个文件分别叫stdin、stdout、stderr。也就是标准输入、标准输出、标准错误。

(7)标准输入一般对应的是键盘(可以理解为:0这个fd对应的是键盘的设备文件),标准输出一般是LCD显示器(可以理解为:1对应LCD的设备文件)

(8)printf函数其实就是默认输出到标准输出stdout上了。stdio中还有一个函数叫fpirntf,这个函数就可以指定输出到哪个文件描述符中。

 

 

. Linux常用的文件IO接口:open,close,read,write,lseek(文件指针)

1open打开一个文件,会分配一个文件描述符(fd

头文件:#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);

              fd = open("a.txt", O_RDWR | O_APPEND |O_TRUNC);

函数说明:

参数一:可以是文件名,字符串常量,字符串数组

参数 pathname 指向欲打开的文件路径字符串. 下列是参数flags (属性)所能使用的旗标:

O_RDONLY :以只读方式打开文件

O_WRONLY :以只写方式打开文件

O_RDWR      :以可读写方式打开文件.

上述三种旗标是互斥的, 也就是不可同时使用, 但可与下列的旗标利用OR(|)运算符组合.

O_CREAT    :open中加入O_CREAT后,不管原来这个文件存在与否都能打开成功,如果原来这个文件不存在则创建一个空的新文件,如果原来这个文件存在则会重新创建这个文件,原来的内容会被消除掉(有点类似于先删除原来的文件再创建一个新的)

O_EXCL      :如果O_CREAT 也被设置, 此指令会去检查文件是否存在. 文件若不存在则建立该文件, 否则将导致打开文件错误. 此外,

O_CREAT  |  O_EXCL: 文件不存在,则创建,文件存在,则提示文件创建错误。

 

O_TRUNC     :若文件存在并且以可写的方式打开时, 此旗标会令文件长度清为0,

而原来存于该文件的资料也会消失.

O_APPEND   : 当读写文件时,如果文件里面原来有内容,则新写入的内容会接续到原来内容的后面

O_APPEND  |  O_TRUNC

 

O_NONBLOCK :(1)以不可阻断的方式打开文件, 也就是无论有无数据读取或等待,

都会立即返回进程之中.【非阻塞IO】

(2)我们打开一个文件默认就是阻塞式的,如果你希望以非阻塞的方式打开文件,则flag中要加O_NONBLOCK标志。

(3). 非阻塞IO只用于设备文件,而不用于普通文件。

(4). O_NDELAY 同O_NONBLOCK.

O_SYNC

(1)write阻塞等待底层完成写入才返回到应用层。

(2)无O_SYNC时write只是将内容写入底层缓冲区即可返回,然后底层在合适的时候会将buf中的内容一次性的同步到硬盘中。但是有时候我们希望硬件不等待,直接将我们的内容写入硬盘中,这时候就可以用O_SYNC标志。

 

 

2read:文件读取后要判断是否读取成功(fd<0失败,fd>0成功)

头文件包含:              #include <unistd.h>

函数定义:                  ssize_t read(int fd, void *buf, size_t count);

       fd表示要读取哪个文件,fd一般由前面的open返回得到

buf是应用程序自己提供的一段内存缓冲区,用来存储读出的内容

count是我们要读取的字节数

返回值ssize_t类型是linux内核用typedef重定义的一个类型(其实就是int),返回值表示成功读取的字节数。

API调用:                  ret= read(fd, buf, sizeof(buf));

判断API是否调用成功:

                                   if (ret<0)                            //读取失败

                                   {

       perror(“read”);

       return -1;                      //-exit(-1);

}

打印:                         printf(“read: %s \n”,buf);      //将所读取的内容存放在buf中

(3). write:

头文件包含:              #include <unistd.h>

函数定义:                  ssize_t write(int fd, const void *buf, size_tcount);

API调用:                  ret= write(fd, writebuf, sizeof(writebuf));

判断API是否调用成功:

                                   if (ret<0)                            //读取失败

                                   {

       perror(“write”);

       return -1;                      //-exit(-1);

}

打印:                         printf(“write: %s \n”,buf);     //将所读取的内容存放在buf中

 

(4). close:关闭文件           close(fd);

头文件包含:              #include <unistd.h>

函数定义:                  int close(int fd);

API调用:                  close(fd);

 

(5). lseek:

(1)   用lseek计算文件长度          (Linux中并没有一个函数可以直接返回一个文件的长度)

// 此时文件指针指向文件开头

// 我们用lseek将文件指针移动到末尾,然后返回值就是文件指针距离文件开头的偏移量,也就是文件的长度了

    ret = lseek(fd, 0, SEEK_END);

 

(2).用lseek构建空洞文件

(1)空洞文件就是这个文件中有一段是空的。

(2)普通文件中间是不能有空的,因为我们write时文件指针是依次从前到后去移动的,不可能绕过前面直接到后面。

(3)我们打开一个文件后,用lseek往后跳过一段,再write写入一段,就会构成一个空洞文件。

(4)空洞文件方法对多线程共同操作文件是及其有用的。有时候我们创建一个很大的文件,如果从头开始依次构建时间很长。有一种思路就是将文件分为多段,然后多线程来操作每个线程负责其中一段的写入。(譬如:迅雷下载文件,即为多线程共同操作文件)

 

三、多次打开同一文件与O_APPEND

(1).重复打开同一文件的读取与写入

(1)、重复打开同一文件读取

(1)一个进程中两次打开同一个文件,然后分别读取,结果无非2种情况:一种是fd1fd2分别读,第二种是接续读。经过实验验证,证明了结果是fd1和fd2分别读。

(3)分别读说明:我们使用open两次打开同一个文件时,fd1和fd2所对应的文件指针是不同的2个独立的指针。文件指针是包含在动态文件的文件管理表中的,所以可以看出linux系统的进程中不同fd对应的是不同的独立的文件管理表。

 

(2)、重复打开同一文件写入

(1)一个进程中2个打开同一个文件,得到fd1和fd2.然后看是分别写

 

(2)、加O_APPEND解决覆盖问题

(1)接续写的解决办法就是在open时加O_APPEND标志即可

(2)O_APPEND的实现原理和其原子操作性说明

(1)O_APPEND为什么能够将分别写改为接续写?关键的核心的东西是文件指针。分别写的内部原理就是2个fd拥有不同的文件指针,并且彼此只考虑自己的位移。但是O_APPEND标志可以让write和read函数内部多做一件事情,就是移动自己的文件指针的同时也去把别人的文件指针同时移动。(也就是说即使加了O_APPENDfd1fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了,一个动了会通知另一个跟着动

(2)O_APPEND对文件指针的影响,对文件的读写是原子的。

(3)原子操作的含义是:整个操作一旦开始是不会被打断的,必须直到操作结束其他代码才能得以调度运行,这就叫原子操作。每种操作系统中都有一些机制来实现原子操作,以保证那些需要原子操作的任务可以运行。

 

四、文件共享

(1).什么是文件共享

(1)文件共享就是同一个文件(同一个文件指的是同一个inode,同一个pathname)被多个独立的读写体(几乎可以理解为多个文件描述符)去同时(一个打开尚未关闭的同时另一个去操作)操作。

(2)文件共享的意义有很多:譬如我们可以通过文件共享来实现多线程同时操作同一个大文件,以减少文件读写时间,提升效率。

(2).文件共享的3种实现方式

(1)文件共享的核心就是怎么弄出来多个文件描述符指向同一个文件。

(2)常见的有3种文件共享的情况:

第一种是同一个进程中多次使用open打开同一个文件

第二种是在不同进程中去分别使用open打开同一个文件(这时候因为两个fd在不同的进程中,所以两个fd的数字可以相同也可以不同),

第三种情况是后面要学的,linux系统提供了dupdup2两个API来让进程复制文件描述符。

(3). 我们分析文件共享时的核心关注点在于:分别写/读还是接续写/读                                                                                      

(3). 使用dup进行文件描述符复制

(1)dup系统调用对fd进行复制,会返回一个新的文件描述符(譬如原来的fd是3,返回的就是4)

(2)dup系统调用有一个特点,就是自己不能指定复制后得到的fd的数字是多少,而是由操作系统内部自动分配的,分配的原则遵守fd分配的原则。

(3)dup返回的fd和原来的oldfd都指向oldfd打开的那个动态文件,操作这两个fd实际操作的都是oldfd打开的那个文件。实际上构成了文件共享。

(4)dup返回的fd和原来的oldfd同时向一个文件写入时,结果是接续写

(5). 使用dup2进行文件描述符复制

(1)dup2和dup的作用是一样的,都是复制一个新的文件描述符。但是dup2允许用户指定新的文件描述符的数字。

int dup(int oldfd);

int dup2(int oldfd, intnewfd);

 (4).使用dup的缺陷分析

(1)dup并不能指定分配的新的文件描述符的数字,dup2系统调用修复了这个缺陷,所以平时项目中实际使用时根据具体情况来决定用dup还是dup2.

(2). 标准输出的重定位

可以使用close & dup配合进行文件的重定位【close(1)关闭标准输出,然后使用dup重新分配得到1这个fd, 时候就把oldfd打开的这个文件和我们1这个标准输出通道给绑定起来了。这就叫标准输出的重定位。】

(3). 命令行中重定位命令>

(1)linux中的shell命令执行后,打印结果都是默认进入stdout的(本质上是因为这些命令譬如ls、pwd等都是调用printf进行打印的),所以我们可以在linux的终端shell中直接看到命令执行的结果。

(2)能否想办法把ls、pwd等命令的输出给重定位到一个文件中(譬如2.txt)去,实际上linux终端支持一个重定位的符号>很简单可以做到这点。

(3)这个>的实现原理,其实就是利用open+close+dup,open打开一个文件2.txt,然后close关闭stdout,然后dup将1和2.txt文件关联起来即可。

(4). dup2共享文件交叉写入测试

dup2复制的文件描述符,和原来的文件描述符虽然数字不一样,但是这连个指向同一个打开的文件,交叉写入的时候,结果是接续写。

 

 

五、fcntl函数介绍

(1).fcntl的原型: int fcntl(int fd, int cmd, ... /* arg */ );

(2). fcntl函数的作用:

(1)fcntl函数是一个多功能文件管理的工具箱,接收2个参数+1个变参。第一个参数是fd表示要操作哪个文件,第二个参数是cmd表示要进行哪个命令操作。变参是用来传递参数的,要配合cmd来使用。

(2)cmd的样子类似于F_XXX,不同的cmd具有不同的功能。fcntl的常用cmd

(1)F_DUPFD这个cmd的作用是复制文件描述符(作用类似于dup和dup2),这个命令的功能是从可用的fd数字列表中找一个比arg大或者和arg一样大的数字作为oldfd的一个复制的fd,和dup2有点像但是不同。dup2返回的就是我们指定的那个newfd否则就会出错,但是F_DUPFD命令返回的是>=arg的最小的那一个数字。

(3)应用:fd2 = fcntl(fd1, F_DUPFD, 0);将文件描述符为fd1的文件复制一份,复制后的文件的文件描述符是fd2

六、Linux系统文件管理

文件描述符表中的指针指向某个inode,这中间省略了filedentry(目录项)对象

(1).硬盘中的静态文件&inode

(1)文件平时都在存放在硬盘中的,硬盘中存储的文件以一种固定的形式存放的,我们叫静态文件。

(2)一块硬盘中可以分为两大区域:一个是硬盘内容管理表项,另一个是真正存储内容的区域。操作系统访问硬盘时是先去读取硬盘内容管理表,从中找到我们要访问的那个文件的扇区级别的信息,然后再用这个信息去查询真正存储内容的区域,最后得到我们要的文件。

(3)操作系统最初拿到的信息是文件名,最终得到的是文件内容。第一步就是去查询硬盘内容管理表,这个管理表中以文件为单位记录了各个文件的各种信息,每一个文件有一个信息列表(我们叫inodei节点,其实质是一个结构体,这个结构体有很多元素,每个元素记录了这个文件的一些信息,其中就包括文件名、文件在硬盘上对应的扇区号、块号那些东西·····)

强调:硬盘管理的时候是以文件为单位的,每个文件一个inode,每个inode有一个数字编号,对应一个结构体,结构体中记录了各种信息。

(4)联系平时实践,大家格式化硬盘(U盘)时发现有:快速格式化和底层格式化。快速格式化非常快,格式化一个32GB的U盘只要1秒钟,普通格式化格式化速度慢。这两个的差异?其实快速格式化就是只删除了U盘中的硬盘内容管理表(其实就是inode),真正存储的内容没有动。这种格式化的内容是有可能被找回的。

 

(2).内存中被打开的文件&vnode

(1)一个程序的运行就是一个进程,我们在程序中打开的文件就属于某个进程。每个进程都有一个数据结构用来记录这个进程的所有信息(叫进程信息表),表中有一个指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件及其相关信息。文件管理表中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件的管理结构体vnode

(2)一个vnode中就记录了一个被打开的文件的各种信息,而且我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。

 

注意,同一个进程多次打开同一个文件时,内核会创建多个file对象。

当进程使用fork系统调用创建一个子进程后,子进程将继承父进程的文件描述符表,因此在父进程中打开的文件可以在子进程中用同一个描述符访问。

需要注意的几点如下所示:

1)进程每打开一个文件,就会有一个file结构与之对应。同一个进程可以多次打开同一个文件而得到多个不同的file结构,file结构描述被打开文件的属性,如文件的当前偏移量等信息。

2)两个不同的file结构可以对应同一个dentry结构。进程多次打开同一个文件时,对应的只有一个dentry结构。Dentry结构存储目录项和对应文件(inode)的信息。

3)在存储介质中,每个文件对应唯一的inode结点,但是每个文件又可以有多个文件名。【硬连接与符号链接】即可以通过不同的文件名访问同一个文件。这里多个文件名对应一个文件的关系在数据结构中表示就是dentry和inode的关系。

4)Inode中不存储文件的名字,它只存储节点号;而dentry则保存有名字和与其对应的节点号,所以就可以通过不同的dentry访问同一个inode。【相当于Windows系统下原安装目录 & 快捷方式的关系】

5)不同的dentry则是同个文件链接(ln命令)来实现的。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值