Linux中的文件IO
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux中的文件IO相关的知识,希望对你有一定的参考价值。
一.应用框架介绍 1.什么是应用编程
(1)典型的嵌入式产品就是基于嵌入式Linux操作系统来工作的。研发过程:第一步让Linux系统在硬件上跑起来(系统移植工作),第二步基于Linux系统来开发应用程序实现产品功能。
(2)基于Linux去做应用编程,其实就是通过调用Linux的系统API来实现应用需要完成的任务。
2.什么是文件的IO
文件的input和output,就是读写文件。
二.文件操作的主要接口API
1.什么是操作系统API
(1)API是一些函数,这些函数是有Linux系统提供支持的,由应用程序来使用。
(2)应用层程序通过调用API来调用操作系统中的各种功能。
(3)学习一个操作系统,其实就是学习使用这个操作系统的API。
2.Linux常用文件IO接口
(1)open、close、write、read、lseek
3.文件操作的一般步骤
(1)一般是先open打开一个文件,得到一个文件描述符,然后对文件进行读写操作(或其他操作),最后close关闭文件即可。
(2)强调一点:对文件进行操作时,一定要先打开文件,打开成功之后才能去操作;读写完成后一定要close关闭文件,否则可能会造成文件损坏。
(3)文件平时是存在块设备中的文件系统中的,我们把这种文件叫静态文件。当我们去open打开一个文件时,Linux内核做的操作包括:内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请一段内存,并且将静态文件的内容从块设备中读取到内存中特定地址管理存放(叫动态文件)。
(4)打开文件后,以后对这个文件的读写操作,都是针对内存中这一份动态文件的,而并不是针对静态文件的。当我们对动态文件进行读写后,此时内存中的动态文件和块设备中的静态文件就不同步了,当我们close关闭动态文件时,close内部内核将内存中的动态文件的内容去更新(同步)块设备中的静态文件。
(5)常见的一些现象:
第一个:打开一个大文件时比较慢
第二个:写了一半的文件,如果没有点保存直接关机/断电,重启后文件内容丢失。
(6)为什么要这么设计?
因为块设备本身有读写限制(NandFlash、SD卡等块设备的读写特征),本身对块设备进行操作非常不灵活。而内存可以按字节单位来操作,而且可以随机操作(内存就叫RAM,random),很灵活,所以内核设计文件操作时就这么设计了。
4.文件描述符
(1)文件描述符其实质是一个数字,这个数字在一个进程中表示一个特定的含义,当我们open打开一个文件时,操作系统在内存中构建了一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符,这个数字就和我们内存中维护这个动态文件的这些数据结构绑定上了,以后我们应用程序如果要操作这个动态文件,只需要用这个文件描述符进行区分。
(2)文件描述符就是用来区分一个程序打开的多个文件的。
(3)文件描述符的作用域就是当前进程,出了当前进程这个文件描述符就没有意义了。
5.一个简单的文件读写示例
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{
int fd = -1; //定义一个整型变量来保存fd的值
int ret = -1; //定义一个整型变量来保存read或write的返回值
char readBuf[100] = {0};//定义各个buf来保存读取出来的值
char writeBuf[30] = "good good study,day day up";//用来保存要写入的内容
//第一步:打开文件
//函数原型:int open(const char *pathname, int flags);pathname:文件名,包含路径;flags:文件访问模式;
//函数原型:int open(const char *pathname, int flags, mode_t mode);
//flags:访问模式必须包含O_RDONLY, O_WRONLY, or O_RDWR其中的一个
fd = open("test.text",O_RDWR);
//fd:如果返回值是-1,表示打开失败,fd是一个大于0的正整数。
if(fd == -1)
{
printf("open error!\n");
return -1;
}
//第二步:读写文件
//写文件
//函数原型:ssize_t write(int fd, const void *buf, size_t count);
//buf:要写入的内容
//count:写入多少字节
//返回值:同read函数
ret = write(fd,writeBuf,strlen(writeBuf));
if(ret < 0)
{
printf("write error!\n");
return -1;
}
else
{
printf("写入成功,写入了%d个字节\n",ret);
}
//读文件
//函数原型:ssize_t read(int fd, void *buf, size_t count);
//buf:文件内容读取到哪里;count:读多少个字节;ssize_t:成功读取的字节数,并不一定等于count
//返回值:-1 表示读取失败;0 表示读取到文件末尾
/* ret = read(fd,readBuf,20);
if(ret < 0)
{
printf("read error!\n");
return -1;
}
else
{
printf("读取成功:读取了%d个字节,内容是:[%s]\n",ret,readBuf);
} */
//第三步:关闭文件
close(fd);
return 0;
}
6.open函数的flag参数详解
(1)O_RDWR 读写文件
(2)O_RDONLY 只读文件
(3)O_WRONLY 只写文件
(4)O_TRUNC 覆盖文件
(5)O_APPEND 追加文件
(6)O_CREAT 创建文件(如果源文件存在则打开,并清空文件内容;如果不存在,则创建一个新的文件)
(7)O_EXCL|O_CREAT 配合使用时,如果文件存在,则报错提示文件存在;如果不存在,则创建新的文件。
(8)open函数在使用 O_CREAT标志去创建文件时,可以使用第三个参数来指定文件的权限。mode参数: 权限的3位数字
例如:open("a.txt",O_RDWR|O_CREAT|O_EXCL,0666);
7.O_NONBLOCK
(1)阻塞与非阻塞。如果一个函数是阻塞式的,则调用这个函数时当前进程有可能被卡住(阻塞住,实质是这个函数内部要完成的事情条件不具备,当前没法做,要等条件成熟),函数被阻塞住了就不能立刻返回;如果一个函数是非阻塞式的,那么调用这个函数后一定会立即返回,但是函数有没有完成任务不一定。
阻塞与非阻塞式两种不同的设计思路,并没有好坏。阻塞式的结果有保障但是时间没有保障;非阻塞的时间有保障但是结果没保障。
打开一个文件默认就是阻塞式的,如果希望以非阻塞式的方式打开,则flag中要加O_NONBLOCK
(2)只用于设备文件,而不用于普通文件
8.O_SYNC
(1)write阻塞等待底层完成写入才返回到应用层
(2)无O_SYNC时write只是将内容写入底层缓冲区即可返回,然后底层(操作系统中负责实现open、write这些操作的那些代码,也包含OS中读写硬盘灯底层硬件的代码)在合适的时候会将buf中的内容一次性同步到硬盘中。这种设计是为了提升硬件操作的性能和销量,提升硬件寿命;但是有时候我们希望硬件不要等待,直接将我们的内容写入到硬盘中,这时候就可以用O_SYNC标志。
9.终止或结束程序可以使用exit()、_exit()、或者_Exit(),参数是返回值
10.文件读写的一些细节
(1)errno和perror
errno 就是error number 错误号码。Linux系统中对各种常见错误做了个编号,当函数执行错误时,函数会返回一个特定的errno编号来告诉我们这个函数到底哪里错了。
errno是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了一个什么错误。
errno本身实质就是一个int类型的数字,每个数字编号对应一种错误。当我们只看errno时只能得到一个错误编号数字。
Linux系统提供了一个函数perror(print error),这个perror函数内部会自动读取errno并将这个数字转成对应的错误信息字符串,然后打印出来。
perror(const char *s);参数是错误提示符。
11.Linux系统如何管理文件
(1)硬盘中的静态文件和inode(i节点)
文件平时是以一种固定形式放在硬盘中的,我们把这个叫做静态文件。
一块硬盘可以分为两大区域:一个是硬盘内容管理表项,一个是真正存储内容的区域。操作系统访问硬盘时先去读取硬盘内容管理表,从中找到我们要访问的那个文件的扇区级别的信息,然后再用这个信息去查询真正存储内容的区域,最后得到我们要的文件。
操作系统最初拿到的是信息是文件名,最终得到的是文件内容。第一步就是去查询硬盘内容管理表,这个管理表以文件为单位记录了各个文件的各种信息,每一个文件有一个信息表(我们叫inode,i节点,其实质是一个结构体,这个结构体有很多元素,每一元素记录了这个文件的一些信息,其中就包括文件名、文件在硬盘中对应的扇区号、块号等)
注意:硬盘管理的时候是以文件为单位的,每个文件一个inode,每个Inode有一个数字编号,对应一个结构体,结构体中记录了各种信息。
平时格式化硬盘发现:快速格式化和底层格式化。快速格式化非常快,其实快速格式化只是删除了硬盘中的硬盘内容管理表(其实就是Inode),真正存储的内容没有动,这种格式化的内容是有可能被找回的。
(2)内存中被打开的文件和vnode(v节点)
一个程序的运行就是一个进程,我们在程序中打开的文件就属于某个进程。每个进程都有一个数据结构来记录这个进程的所有信息(叫进程信息表),表中有一个指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件及其相关信息。文件管理表中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件的管理结构体vnode。
一个vnode就记录了一个被打开的文件的各种信息,而且我们只要知道这个文件的fd,就可以很容易的找到这个文件的vnode进而对这个文件进行各种操作。
(3)文件与流的概念
流(stream):文件操作中,文件类似是一个大包裹,里面装了一堆字符,但是文件被读出/写入时都只能一个字符一个字符的进行,而不能一次性的读写,那么一个文件中N多个字符挨个一次读写时,这些字符就构成一个字符流。
流这个概念是动态的,不是静态的。
编程中提到流这个概念,一般都是IO相关的,所以经常叫IO流。文件操作时就构成了一个IO流。
12.lseek函数
(1)文件指针:当我们要对一个文件进行读写时,一定需要先打开这个文件,所以我们读写的所有文件都是动态文件。动态文件在内容中的形态就是文件流的形式。
(2)文件流很长,里面有很多个字节,那么怎么知道当前正在操作的是哪个位置呢?GUI模式下软件用光标来标识这个当前正在操作的位置。
(3)在动态文件中,我们会通过文件指针来表征这个正在操作的位置。所谓文件指针,就是我们文件管理表这个结构体里面的一个指针。所以文件指针其实是vnode中的一个元素,这个指针表示当前我们正在操作文件流的哪个位置。这个指针不能被直接访问,Linux系统用lseek函数来访问这个文件指针。
(4)当我们打开一个空文件时,默认情况下文件指针指向文件流的开始。所以这时候去write时写入就是从文件开头开始的。write和read函数本身自带移动文件指针的功能,所以当我们write了n个字节后,文件指针会自动向后移动n位。如果要人为的改变文件指针,就只能通过lseek函数了。通常,读、写操作都从当前文件偏移量开始,并使偏移量增加所读写的字节数。按系统默认情况,当打开一个文件时,除非指定 O_APPEND 选项,否则该偏移量设置为 0。
(5)off_t lseek(int fd,off_t offset,int whence);返回值:从文件开头的偏移字节数。
offset 偏移量
whence:
SEEK_SET 文件开头开始偏移
SEEK_CUR 当前文件指针所处位置开始偏移
SEEK_END 文件末尾开始偏移
1) 欲将读写位置移到文件开头时:
lseek(int fildes,0,SEEK_SET);2) 欲将读写位置移到文件尾时:lseek(int fildes,0,SEEK_END);返回值就是文件的长度3) 想要取得目前文件位置时:lseek(int fildes,0,SEEK_CUR);
(6)如果使用write写入文件,然后立马使用read去读,读出来的内容是空的,就是因为文件指针在write后,文件指针处于文件末尾,要想读取前一步写入的内容,就必须使用lseek函数将文件指针进行偏移。
(7)lseek构建空洞文件
打开一个文件后,用lseek往后跳过一段,再write写入一段,就会构成一个空洞文件。
空洞文件方法对多线程共同操作文件是及其有用的。有时候我们创建一个很大的文件,如果从头开始依次构建时间很长。有一种思路就是将文件分为多段,然后多线程来操作每个线程负责其中一段的写入。
13.多次打开同一个文件与O_APPEND
(1)操作系统对文件打开的数量(句柄)是有一定限制的,并不能无限制的同时打开同一个文件。
(2)一个进程中同时打开一个文件2次,得到fd1和fd2,得到的结论是分别写和分别读。因为fd1和fd2所对应的文件指针是不同的2个独立的指针,文件指针是包含在动态文件的文件管理表中的,所有可以看出Linux系统的进程中不同的fd对应的是不同的独立的文件管理表。
(3)加上O_APPEND解决覆盖问题
在同时打开2个文件进行写入时,加上O_APPEND标志就可以实现接续的写入,而不会是分别写,被覆盖。
(4)O_APPEND的实现原理和其原子操作性说明
O_APPEND为什么能够将分别写改为接续写?关键的核心的东西时文件指针。分别写的内部原理就是2个fd拥有不同的文件指针,并且彼此只考虑自己的位移,但是O_APPEND标志可以让write和read函数内部多做一件事情,就是移动自己的文件指针的同时也去把别人的文件指针同时移动(也就是说即使加了O_APPEND,fd1和fd2还是各自拥有一个独立的文件指针,但是这两个文件指针关联起来了,一个懂了会通知另一个跟着动)
O_APPEND对文件指针的影响,对文件读写是原子的。
原子操作的含义是:整个操作一旦开始是不能打断的,必须直到操作结束其他代码才能得以调度运行。每种操作系统中都有一些机制来实现原子操作,以保证那些需要原子操作的任务可以运行。
14.文件共享
(1)3种实现方式:
第一种:同一个进程中多次使用open打开同一个文件
第二种:在不同的进程中使用open打开同一个文件(这时候fd再不同的进程,所以可能相同也可能不同)
第三种:Linux提供了dup和dup2两个API来让进程复制文件描述符
15.文件描述符
(1)Linux每一个进程都有一个文件描述符表,里面放的就是当前进程打开的文件,这个文件描述符表其实就是一个数组,数组的Index就是fd,对应的值就是文件管理表,早期的Linux对这个数组有限制,大小是20个,就是说一个进程中最多可以打开20个文件,但是现在的Linux肯定不只。
(2)当一个进程在创建的时候默认会打开3个文件,stdin、stdout、stderro,因此当进程再打开文件时,文件描述符是从3开始的。当open打开一个文件时,返回的fd总是数组中最小的那个index。
16.文件描述符的复制
(1)dup
dup系统调用对fd进行复制,会返回一个新的文件描述符。原来的fd和新的fd都指向打开的那个文件,形成了文件共享。可以使用close和dup配合使用来实现标准输出的重定位。
(2)dup2
dup2可以指定一个新的文件描述符fd,这个fd是一个可用的fd
以上是关于Linux中的文件IO的主要内容,如果未能解决你的问题,请参考以下文章
是否有一种方法可以将实时记录的音频片段连续发送到Flutter.io中的后端服务器?
java内存流:java.io.ByteArrayInputStreamjava.io.ByteArrayOutputStreamjava.io.CharArrayReaderjava.io(代码片段
java缓冲字符字节输入输出流:java.io.BufferedReaderjava.io.BufferedWriterjava.io.BufferedInputStreamjava.io.(代码片段