【linux应用编程】-linux的文件I/O

 第一点

应用编程的框架

整个嵌入式linux核心课程包括5个点,按照学习顺序依次是:裸机、C高级、uboot和系统移植、linux应用编程和网络编程、驱动。
典型的嵌入式产品就是基于嵌入式linux操作系统来工作的。典型的嵌入式产品的研发过程就是;第一步让linux系统在硬件上跑起来(系统移植工作),第二步基于linux系统来开发应用程序实现产品功能。
基于linux去做应用编程,其实就是通过调用linux的系统API来实现应用需要完成的任务。

第二点

文件操作的一般步骤

在linux系统中要操作一个文件,一般是先open打开一个文件,得到一个文件描述符(文件描述符其实实质是一个数字,这个数字在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构挂钩绑定上了,以后我们应用程序如果要操作这一个动态文件,只需要用这个文件描述符进行区分。文件描述符fd的合法范围是0或者一个正正数,不可能是一个负数。,然后对文件进行读写操作(或其他操作),最后close关闭文件即可!

我们对文件进行操作时,一定要先打开文件,打开成功后才能去操作(如果打开本身失败,后面就不用操作了);最后读写完成之后一定要close关闭文件,否则可能会造成文件损坏。

文件平时是存在块设备中的文件系统中的,我们把这种文件叫静态文件当我们去open打开一个文件时,linux内核做的操作包括:内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放(叫动态文件)

打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件的,而并不是针对静态文件的。当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件。

man 1 xx查linux shell命令,man 2 xxx查API, man 3 xxx查库函数
 

linux常用文件I/O接口

open、close

【linux应用编程】-linux的文件I/O_第1张图片

 缺少读和写的第二步,后面补充!!

read、write

关于read

ssize_t read(int fd, void *buf, size_t count);
fd表示要读取哪个文件,fd一般由前面的open返回得到
buf是应用程序自己提供的一段内存缓冲区,用来存储读出的内容
count是我们要读取的字节数
返回值ssize_t类型是linux内核用typedef重定义的一个类型(其实就是int),返回值表示成功读取的字节数。

【linux应用编程】-linux的文件I/O_第2张图片

 关于write

写入用write系统调用,write的原型和理解方法和read相似
注意const在buf前面的作用,结合C语言高级专题中的输入型参数和输出型参数一节来理解。(说明buf里面的数只会被读,不会被写,这就是输入型参数
注意buf的指针类型为void,结合C语言高级专题中void类型含义的讲解
????刚才先写入12字节,然后读出结果读出是0(但是读出成功了),这个问题的答案后面章节会讲,大家先思考一下。

【linux应用编程】-linux的文件I/O_第3张图片

open函数的flag详解

读写权限:O_RDONLY 、 O_WRONLY 、O_RDWR

linux中文件有读写权限,我们在open打开文件时也可以附带一定的权限说明(譬如O_RDONLY就表示以只读方式打开,O_WRONLY表示以只写方式打开,O_RDWR表示以可读可写方式打开

打开存在并有内容的文件时:O_APPEND、O_TRUNC

O_TRUNC属性去打开文件时,如果这个文件中本来是有内容的,则原来的内容会被丢弃。O_APPEND属性去打开文件时,如果这个文件中本来是有内容的,则新写入的内容会接续到原来内容的后面

如果O_APPEND和O_TRUNC同时出现,O_TRUNC起作用,O_APPEND没起作用;默认不使用O_APPEND和O_TRUNC属性时,原来的文件中的内容保持不变

exit、_exit、_Exit退出进程

正式终止进程(程序)应该使用exit【C库函数】或者_exit或者_Exit之一。(后面这两个为系统调用)

打开不存在的文件时:O_CREAT、O_EXCL

open的flag O_CREAT就是为了应对这种打开一个并不存在的文件的。O_CREAT就表示我们当前打开的文件并不存在,我们是要去创建并且打开它。open中加入O_CREAT后,不管原来这个文件存在与否都能打开成功,如果原来这个文件不存在则创建一个空的新文件,如果原来这个文件存在则会重新创建这个文件,原来的内容会被消除掉(有点类似于先删除原来的文件再创建一个新的)

这样可能带来一个问题???我们本来是想去创建一个新文件的,但是把文件名搞错了弄成了一个老文件名,结果老文件就被意外修改了。我们希望的效果是:如果我CREAT要创建的是一个已经存在的名字的文件,则给我报错,不要去创建。这个效果就要靠O_EXCL标志和O_CREAT标志来结合使用。当这连个标志一起的时候,则没有文件时创建文件,有这个文件时会报错提醒我们。

阻塞和非阻塞(O_NONBLOCK)

如果一个函数是阻塞式的,则我们调用这个函数时当前进程有可能被卡住(阻塞住,实质是这个函数内部要完成的事情条件不具备,当前没法做,要等待条件成熟),函数被阻塞住了就不能立刻返回;如果一个函数是非阻塞式的那么我们调用这个函数后一定会立即返回,但是函数有没有完成任务不一定。(阻塞式的结果有保障但是时间没保障;非阻塞式的时间有保障但是结果没保障)

我们打开一个文件默认就是阻塞式的,如果你希望以非阻塞的方式打开文件,则flag中要加O_NONBLOCK标志(只用于设备文件,而不用于普通文件。)

O_SYNC

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

无O_SYNC时write只是将内容写入底层缓冲区即可返回,然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘等底层硬件的代码)在合适的时候会将buf中的内容一次性的同步到硬盘中。这种设计是为了提升硬件操作的性能和销量,提升硬件寿命;但是有时候我们希望硬件不好等待,直接将我们的内容写入硬盘中,这时候就可以用O_SYNC标志。

lseek详解

lseek的作用

我们对一个文件进行读写的时候,其实是在对这个文件的动态文件进行读写(动态文件在内存中),动态文件在内存中存在的方式是内存流的方式,因此我们需要有一个标记去知道我们现在读写到这个文件流的哪一个位置(就是widows系统中,我们读写文件时的鼠标光标),在linux种这个光标(文件指针)就是在vnode (文件管理结构体)的一个元素,但是这个指针不能直接被访问,需要用lseek来访问!!

当我们去打开一个空文件时,光标默认在首地址,然后当输入n个字节后,光标就在第n个字符的后面(write和read函数本身自带移动文件指针的功能)

这就能够解释上面同一颜色处文字的一问了!!!

用lseek测量长度

 

上面这个函数参数代表的意思就是:fd代表哪一个文件、0d代表偏移量、seek_end代表光标开始的地方。 

用lseek构建空洞文件

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

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

第三点

文件读写的一些细节

errno和perror

errno就是error number,意思就是错误号码。本身实质是一个int类型的数字,每个数字编号对应一种错误,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了。不适应于人看!

linux系统提供了一个函数perror(意思print error),perror函数内部会读取errno并且将这个不好认的数字直接给转成对应的错误信息字符串,然后print打印出来

具体的测试代码与测试结果如下:

【linux应用编程】-linux的文件I/O_第4张图片

 

read和write的count

count和返回值的关系。count参数表示我们想要写或者读的字节数,返回值表示实际完成的要写或者读的字节数。实现的有可能等于想要读写的,也有可能小于(说明没完成任务)

count再和阻塞非阻塞结合起来,就会更加复杂。如果一个函数是阻塞式的,则我们要读取30个,结果暂时只有20个时就会被阻塞住,等待剩余的10个可以读。

文件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再根据硬盘的特性来选择好的实际去最终写入硬盘中)。

【linux应用编程】-linux的文件I/O_第5张图片

第四点

linux如何管理文件

硬盘中的静态文件和inode(i节点)

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

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

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

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

内存中被打开的文件和vnode(v节点)

一个程序的运行就是一个进程,我们在程序中打开的文件就属于某个进程,。每个进程中都有一个数据结构用来记录这个进程的所有信息-进程信息表。表中有一个指针指向一个文件管理表,这个表中就记录了当前进程打开的所有文件及其相关信息。

文件管理表中用来索引各个打开的文件的额index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件管理结构体vnode
一个vnode中就记录了一个被打开的文件的各种信息,而且我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。

文件与流的概念

文件操作中,文件类似是一个大包裹,里面装了一堆字符,但是文件被读出/写入时都只能一个字符一个字符的进行,而不能一股脑儿的读写,那么一个文件中N多的个字符被挨个一次读出/写入时,这些字符就构成了一个字符流。

第五点

文件共享

多次对同一文件读写操作

在同一进程中对同一个文件进行读和写操作,无非的结果就是两种,一种是单独操作;一种是接连操作;

单独操作的案例:比如调用两个读或写函数  第一个函数向文件写\读入abababab八个字节,第二次再向这个文件写\读cdcdcdcd八个字节,最后的结果打印出来就只有cdcdcdcd,因为是单独操作的,所以呢,第二次操作的时候光标还是从头开始,所以第一次的数据就不见了!

有时候,我们需要在同一文件中接连写入,怎么办呢?

办法就是在open时加O_APPEND标志即可

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

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

文件共享的实现方式

文件共享就是同一个文件被多个独立的读写体去同时操作!!

文件共享三种形式

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

(1)同一个进程中多次使用open打开同一个文件。

(2)不同进程中去分别使用open打开同一个文件。(此时fd可相同可不同)

(3)linux系统提供了dup、dup2这两个API来让进程复制文件描述符。

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

【linux应用编程】-linux的文件I/O_第6张图片

                  【linux应用编程】-linux的文件I/O_第7张图片            

【linux应用编程】-linux的文件I/O_第8张图片

由以上三个图可得,一、二情况的节点指针都是不同的,所以他们肯定是单独操作,第三种情况节点指针一样,肯定接连操作!!

文件描述符的复制

dup函数和dup2函数

这两个函数都是对fd进行复制的 ,然后会返回一个新的文件描述符;

dup调用不能自己制定新的文件描述符,他所返回的文件描述符是操作系统内部自己分配的,分配的原则遵循fd的分配原则(在没有被占用的情况下,由小到大);

dup返回的文件描述符,和之前旧的文件描述符都指向了原先打开的那个动态文件,操作也都是队员来打开的文件进行操作,也就是文件共享。

dup2弥补了dup不能指定新的文件描述符的缺陷,这两个函数的参数原型可以通过查找man手册进行了解!

通过上面图示,我们知道dup和dup2得到的新的文件描述符虽然和旧的文件描述符不是一样的,但是他们都指向同一个文件指针,所以他们分别对文件操作时是接续操作,不是分别操作!!

关于重定位

我们知道,文件描述符 0 1 2这三个是被系统占用了,分别是标准输入、标准输出、错误通道!

但是我们可以通过close函数去关闭系统占用的这三个函数,然后我们再使用dup函数进行调用时,此时分配的文件描述符就是我们刚刚关闭的这些,比如是1.

那此时我们的标准输出就通过原来的printf函数输出不了了,那原本输出的内容去哪了?答案是直接输出在了我们刚才打开的那个文件里。因为1现在是我们原先旧的文件描述符的复制品,他们 同时指向一个文件!!!

这就是重定位!!!!!

除此之外,在命令行中也有重定位命令(>)

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

如果我在命令行中使用>重定位:ls>2.txt,此时我们的ls后的原本的打印菜单就不在命令行中心事了,而在2.txt中!

fcntl函数介绍

fcntl的原型和作用

参数解释:第一个为要操作的文件描述符,第二个是要进行那个命令操作,第三个是个变参数配合cmd使用。

fcntl函数是一个多功能文件管理的工具箱,cmd的样子类似于F_XXX,不同的cmd具有不同的功能

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

所以相比较dup2,这个函数的优势还是有的!

还有F_GETFL、F_SETFL,在设置非阻塞文件那块也有涉及,可以关注高级IO部分

标准I/O和文件I/O有什么区别

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

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

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

性能上和易用性上看,C库函数一般要好一些。譬如IO,文件IO是不带缓存的,而标准IO是带缓存的,因此标准IO比文件IO性能要更高。
常见的标准IO库函数有:fopen、fclose、fwrite、fread、ffulsh、fsee

你可能感兴趣的:(linux应用编程,linux)