linux中的文件IO
1、应用编程框架介绍
(1)什么是应用编程
嵌入式linux包括裸机编程、C语言高级、uboot和系统移植、linux应用编程和网络编程、驱动开发典型的嵌入式产品就是基于嵌入式linux操作系统来工作的
产品研发过程:linux系统在硬件上跑起来(系统移植工作)、基于linux系统来开发应用程序实现产品功能 基于linux去应用编程,其实就是通过调用linux的系统API来实现应用需要完成的任务
(2)什么是文件IO
IO就是input/output,输入/输出, 读写文件
2、文件操作的主要接口API
(1)什么是操作系统API
API是一些函数,由linux系统提供支持,由应用层程序来使用应用层程序通过调用API来调用操作系统中各种功能来干活学习一个操作系统,就是学习使用这个操作系统的API要使用linux系统来读写文件,就要学习linux系统API中和文件IO有关的那几个API接口
(2)linux系统常用的文件IO接口
open、close、write、read、lseek
(3)文件操作的一般步骤
linux系统中要操作一个文件:
首先open打开文件,得到文件描述符对文件进行读写操作(或其他操作)最后close关闭文件 需要注意的是:在操作文件之前一定要先打开文件,打开成功才能进行操作,如果打开失败,就没必要再继续进行了;在操作完文件之后一定要close关闭文件,否则可能造成文件损坏。重要概念:文件描述符
文件描述符的实质是:一个数字,这个数字在一个进程中表示一个特定的含义。当open打开一个文件时,操作系统在内存中会构建一些数据结构来表示这个动态文件,然后返回给应用程序一个数字作为文件描述符。这个数字就和内存中维护的这个动态文件的这些数据结构关联起来了,以后应用程序如果想操作这个动态文件只需要用这个文件描述符进行区分。文件描述符是用来区分一个程序打开的多个文件的文件描述符的作用域就是当前进程,出了当前进程这个文件描述符就没有意义了
(4)文件操作的实质
静态文件:文件平时是存放在块设备中的文件系统中的,这个问价就是静态文件动态文件:当open打开文件时,linux内核在进程中建立了一个打开文件的数据结构,记录下我们打开的这个文件;内核在内存中申请了一段空间,并且将静态文件的内容从块设备中读取到内存中特定的地址管理存放打开文件后,对文件的读写操作其实是针对内存中的这一份动态文件的,而并不是块设备中的静态文件。在对动态文件进行读写之后,内存中的动态文件和块设备中的静态文件就不同步了。当我们close关闭动态文件后,close内部进行的操作是内核将内存中的动态文件的内容更新(同步)到块设备中的静态文件。
(5)为什么这样设计
块设备本身有读写限制,NandFlash、SD等块设备是按照块来读写的,本身对块设备的操作就不灵活,而内存(RAM)可以按字节为单位进行读写,而且可以随机操作很灵活,所以内核设计文件操作就是这样的
3、一个简单的文件读写实例
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd = -1; // fd 就是file descriptor,文件描述符
char buf[100] = {0};
char writebuf[20] = "l love linux";
int ret = -1;
// 第一步:打开文件
fd = open("a.txt", O_RDWR);
if (-1 == fd) // 有时候也写成: (fd < 0)
{
printf("文件打开错误\n");
}
else
{
printf("文件打开成功,fd = %d.\n", fd);
}
// 第二步:读写文件
// 写文件
ret = write(fd, writebuf, strlen(writebuf));
if (ret < 0)
{
printf("write失败.\n");
}
else
{
printf("write成功,写入了%d个字符\n", ret); // 真正读出的字节数为ret
}
/*
// 读文件
ret = read(fd, buf, 5);
if (ret < 0)
{
printf("read失败\n");
}
else
{
printf("实际读取了%d字节.\n", ret);
printf("文件内容是:[%s].\n", buf);
}
*/
// 刚才明明写入了12个字节,然后读出成功看,但读出的却是0(文件指针的问题,文件指针指向了文末尾,读出来就是0)
// 第三步:关闭文件
close(fd);
return 0;
}
(1)打开与关闭文件
int open(const char *pathname, int flags, mode_t mode);
linux中的文件描述符fd的合法范围是0或者一个正整数,不可能是负数open返回的fd,程序必须记录好,以后向这个文件的所有操作都要靠这个fd去对应这个文件,最后关闭文件也需要fd去指定关闭这个文件。如果在古纳比文件前fd去掉了,就不能对这个文件进行关闭以及读写的操作
(2)实时的查man手册----得到头文件、查看API接口原型
man 1 xx // 查shell命令man 2 xx // 查API接口man 3 xx // 查库函数
(3)读取文件内容
ssize_t read(int fd, void *buf, size_t count); // API原型
fd:要读取那个文件,fd一般由前面的open返回得到buf:应用程序自己提供的一段内存缓冲区,用来存储读出的内容count:要读取的字节数返回值ssize_t 类型:Linux内核用typedef重定义的一个类型(其实就是int),返回值表示成功读取到的字节数(真正要读出的字节数),返回值为负数,说明读取失败
(4)向文件中写入
ssize_t write(int fd, const void *buf, size_t count); // API原型
write的原型和理解与read相似const void *buf中的const,说明这个形参是输入型参数注意buf的指针类型是void *,不确定的类型
4、退出进程的API
程序在前面的步骤操作失败导致后面的操作都没有可能进行下去的时候,应该在前面的错误检测中结束整个程序,不应该让程序运行下去。如何退出程序?
在main中return。程序正常终止return 0;程序异常终止return -1;正式终止进程(程序)应该使用:
exit(0); exit(-1); // man 3 库函数_exit(0); _exit(-1); // man 2 API系统调用_Exit(0); _Exit(-1); // man 2 API系统调用 注:man 1 xx 是shell命令的查询
5、open函数flags详解
(1)读写权限:O_RDONLY、O_WRONLY、O_RDWR
linux中文件的读写权限,在open打开时可以附带一定的权限说明
O_RDONLY:以只读方式打开O_WRONLY:以只写方式打开O_RDWR:可读可写方式打开附带了权限后,打开的文件只能按照这种权限来操作
(2)打开存在的文件并且有内容时:O_APPEND、O_TRUNC
打开一个已经存在并且有内容的文件时(仅仅是打开,不进行读写),可能有一下几种处理:
①新内容替换原来的内容,原内容被丢弃(不见了)②新内容添加在原内容之后,原内容还在前边③新内容添加在原内容之前,原内容还在后边④不读不写时,原文件中的内容保持不变
O_TRUNC trunc"截断、截取"
使用O_TRUNC属性打开一个已经存在并且有内容的文件时,原来的内容会被丢弃(仅仅打开一次文件,其内容就不见了),对应①;向文件写入新内容时,文件中就是刚刚写入的内容。
O_APPEND append"附加、增补"
使用O_APPEND属性打开一个已经存在并且有内容的文件时,新写入的内容添加在原内容之后,对应②
默认不使用O_TRUNC和O_APPEND属性时,不读不写原文件中的内容保持不变,对应④;写入文件的新内容会将原内容从头开始覆盖
fd = open("a.txt", O_RDWR);
如果O_TRUNC和O_APPEND同时使用时,O_TRUNC起作用,也就是新内容替换原来的内容,原内容被丢弃
fd = open("a.txt", O_RDWR | O_TRUNC | O_APPEND);
(3)打开不存在的文件时:O_CREAT、O_EXCL
问题一:打开一个不存在的文件会怎样?
结论:打开文件错误vi或者windows下的notepad++,都可以直接打开一个尚未存在的文件open的flags:O_CREAT creat 以"创建"来理解
O_CREAT就是为了应对这种打开一个并不存在的文件,表示当前打开的文件并不存在,要去创建并且打开它
问题二:当open使用了O_CREAT,但要打开的文件是已经存在的情况会是怎样?
结论:open中加上O_CREAT,不管原来这个文件是否存在都能够打开文件成功。如果原来这个文件不存在就会创建一个空的文件并且打开;如果原来这个文件是存在的,则会重新创建这个文件,原来的内容将会直接被清除掉(类似于先删除原来的文件再创建一个新的文件)
问题三:问题二引发的问题:本来想创建一个新文件,但把问文件名搞错成了一个已有的文件名,结果已有的文件就会被意外删除了。
我们希望的效果是:如果使用O_CREAT要创建的是一个已经存在的名字的文件,则会报错不会创建。
解决:O_EXCL和O_CREAT结合使用: excl “不包括”
fd = open("a.txt", O_RDWR | O_CREAT | O_EXCL);
// 这两个标志一起使用的时候,在没有文件时创建文件,有这个文件时就会报错,不会去创建
问题四:创建文件是需要文件的权限的,在open函数中指定文件权限
使用第三个参数mode来指定要创建的文件权限。mode使用4个数字来指定权限,后面三个很重要,对应我们要创建的这个文件的权限标志,如一般要创建一个可读可写不可执行的文件就用0666
fd = open("a.txt", O_RDWR | O_CREAT | O_EXCL, 0666);
(4)阻塞与非阻塞:O_NONBLOCK
阻塞与非阻塞的概念: nonblock “非阻塞”
阻塞:如果一个函数是阻塞式的,则我们调用这个函数时当前进程有可能被卡住(也就是阻塞住,实质是这个函数内部要完成的事情条件不具备,当前没法做,要等待条件成熟),函数被阻塞住就不能立刻返回。非阻塞:如果一个函数是非阻塞式的,那么我们调用这个函数后一定会立即返回,但是函数有没有执行完就无法保证了。 阻塞与非阻塞的比较:
阻塞与非阻塞是两种不同的设计思路,并没有好坏之分。总的来说,阻塞式的结果有保障但是时间没保障;非阻塞式的时间有保障但是结果没保障。 操作系统提供的API和由API封装而成的库函数,有很多本身就是被设计成为阻塞式或者非阻塞式的,所以在应用程序调用这些函数的时候心里要清楚函数是哪一种。我们打开一个文件默认就是阻塞式的,如果希望以非阻塞式的方式打开文件,则flags中要加O_NONBLOCK标志O_NONBLOCK只用于设备文件,而不用于普通文件(如:.txt文件)
(5)不等待直接将内容写入硬盘:O_SYNC
write阻塞等待底层完成写入才返回到应用层无O_SYNC时,write只是将内容写入底层缓冲区即可返回,然后底层(操作系统中负责实现open、write这些操作的代码,也包含OS中读写硬盘等底层硬件的代码)在合适的时候会将buf中的内容一次性的同步(sync)到硬盘中。这种设计是为了提升硬件操作的性能和质量,提升硬件的寿命。但是,有时候我们希望硬件不要等待,直接将我们的内容写入到硬盘中,这时就要使用O_SYNC标志
6、报错信息的打印库函数perror
errno解析
errno就是error number,错误号码
linux操作系统中对各种常见的错误做了编号,当函数执行错误时,函数会返回一个特定的error编号来告诉我们这个函数错在哪里errno是由OS来维护的一个全局变量,任何OS内部函数都可以通过设置errno来告诉上层调用者究竟刚才发生了什么错误errno本身实质是一个int类型的数字,每个数字编号对应一种错误.当我们只看erron时只能得到一个错误编号数字(如-37) ,不适于人看
perror解析
linux系统提供了一个函数perror(print error),perror函数内部会读取errno并且将这个不好认的数字直接给转换成对应的错误信息字符串,然后print打印出来
// 当open函数执行错误时,使用perror打印open函数返回的错误信息
perror("open");
// 错误信息如下:
open: File exists
7、文件读写(read、write)的一些细节
(1)read和write的count
count和返回值的关系
count参数表示我们想要写入或者读出的字节数,返回值表示实际完成的已经写入或者读出的字节数。实现的有可能等于想要读写的字节数,也有可能小于(说明没完成任务) count和阻塞与非阻塞结合起来
如果一个函数是阻塞式的,则我们要读取30个,结果暂时只有20个时就会被阻塞住,等待剩余的10个可以读取时再进行读取(假如说一个文件中只有20个字节,read函数count传参为30,read函数是阻塞式的,那么它读不够30,就会一直阻塞着,程序就会卡死在read函数) 在正式写程序时,我们要读取或者写入的是一个很庞大的文件(如文件有2MB),我们不可能把count设置为2* 1024*1024,而应该把count设置为一个合适的数字(如2048、4096),然后通过多次读取来实现全部读取(使用for循环来读取全部)
8、linux系统如何管理文件
(1)硬盘中的静态文件和inode(i节点)
文件平时都是存放在硬盘当中的,硬盘中存储的文件是以一种固定的形式存放的,叫做静态文件一个硬盘可以分为两大区域:硬盘内容管理表项、真正存储的区域
操作系统访问硬盘时,是先读取硬盘内容管理表,从中找到我们要访问的那个文件所在的扇区的信息,然后再用这个信息去查询真正存储的区域,最后得到我们要的文件 操作系统从硬盘最初拿到的信息是文件名,最终得到的是文件内容
第一步:去查询硬盘内容管理表,这个管理表中以文件为单位记录了各个文件的各种信息,每个文件都有一个信息列表。这个信息列表:inode,i节点,其实质是一个结构体,其中有很多元素,每个元素都记录的是这个文件的信息,其中就包括文件名、文件在硬盘上的对应的扇区号、块号等……强调:硬盘管理时是以文件为单位的,每个文件都有一个inode,每个inode有一个数字编号,对应一个结构体,结构体记录了文件的各种信息 联系实际:格式化硬盘(U盘)发现,有快速格式化和底层格式化两种。快速格式化就是只删除了U盘的硬盘内容管理表(就是inode),真正存储的内容是没有被删除的,这种格式化之后的内容是有可能被找回的。底层格式化也就会将真正的内容删除。
(2)内存中被打开的文件和vnode(v节点)
一个程序的运行就是一个进程,我们在程序中打开的文件就属于某个进程,每个进程都有一个数据结构用来记录这个进程的所有信息(叫进程信息表),进程信息表中有一个指针会指向一个文件管理表,文件管理表中记录了当前进程打开的所有文件及其相关信息文件管理表中用来索引各个打开的文件的index就是文件描述符fd,我们最终找到的就是一个已经被打开的文件的管理结构体vnode(v节点)一个vnode中记录了一个被打开的文件的各种信息,而且我们只要知道这个文件的fd,就可以容易的找到这个文件的vnode进而对这个文件进行操作
(3)文件流的概念
流(stream)对应自然界中的流水文件操作中,文件类似是一个大包裹,里面装了一堆字符,但是文件被读出/写入时都只能一个字符一个字符的进行,而不能一股脑的读写,那么一个文件中N多个字符被挨个一次读出/写入时,这些字符就构成了字符流流的概念是动态的,不是静态的编程中提到流这个概念,一般都是IO相关的。所以经常叫IO流。文件操作时就构成了一个IO流
9、文件指针lseek详解
(1)lseek函数介绍
文件指针:当我们要对一个文件进行读写时,一定需要先打开这个文件,所以读写的所有文件都是动态文件。动态文件在内存中就是文件流的形式
文件流很长,里面有很多个字节。我们当前正在操作的是哪个位置呢?
GUI模式下的软件,使用光标来识别这个当前正在操作的位置,给人看的。在动态文件中,我们会通过文件指针来表征这个正在操作的位置。这个指针就是文件管理表这个结构体里面的一个指针,所以文件其实就是vnode中的一个元素。这个指针表示当前我们正在操作文件流的那个位置。这个指针是不能被直接访问的,linux系统中使用lseek函数来访问这个文件指针
当我们打开一个文件时,默认的情况下文件指针指向文件流的开始。所以这时去write写入就是从文件的开头开始的。write和read函数本身自带移动文件指针的功能,所以当write了n个字节后,文件指针会自动向后移动n位。如果需要人为的随意更改文件指针,就只能通过lseek函数。
off_t lseek(int fd, off_t offset, int whence);
// off_t 返回值,相当于SEEK_SET来看的偏移量(实际得位置)
// offset 偏移量
// whence: SEEK_SET(开头)、SEEK_CUR(当前)、SEEK_END(末尾)
read和write函数都是从当前文件指针处开始操作的,所以当我们用lseek显式的将文件指针移动后,那么再去read/write时就是从移动后的位置开始的。
回顾之前讲的,从空文件先write写了12字节,然后read时是空的,但是此时我们打开文件后发现12字节确实写进来了,但读出来确实空的。这是因为在write后文件指针移动到了末尾,read时,也就是从末尾开始读的,那当然是空的,所以在read之前需要先将文件指针移动到开头才能读取到文件内容。
fd = open("a.txt", O_RDWR);
if(-1 == fd) // 有时候这么写:if(fd < 0)
{
// printf("文件打开错误\n");
perror("open");
// return -1;
_exit(-1);
}
else
{
printf("文件打开成功,fd = %d.\n", fd);
}
ret = lseek(fd, 3, SEEK_SET); // 从文件开头移动3字节
printf("lseek, ret = %d.\n", ret);
(2)sleek的使用1——计算文件长度
linux中没有一个函数可以直接返回一个文件的长度,项目中经常会需要知道文件长度,利用lseek来写一个函数:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <unistd.h>
int cal_len(const char *pathname)
{
int fd = -1; // fd 就是file descriptor,文件描述符
int ret = -1;
// 第一步:打开文件
fd = open(pathname, O_RDWR);
if(-1 == fd) // 有时候这么写:if(fd < 0)
{
perror("文件打开错误");
return -1;
}
// 此时文件指针指向文件开头
// 我们用lseek将文件指针移动到末尾,然后返回值就是文件指针距离文件开头的偏移量,也就是文件的长度了
ret = lseek(fd, 0, SEEK_END);
printf("文件长度是: %d字节\n", ret);
}
int main(int argc, char *argv[])
{
int fd = -1; // fd 就是file descriptor,文件描述符
int ret = -1;
if (argc != 2)
{
printf("usage: %s filename\n", argv[0]);
_exit(-1);
}
cal_len(argv[1]);
return 0;
}
(3)lseek的使用2——构建空洞文件
空洞文件:有一段空白的文件
普通文件中间是不可能有空的,因为write时文件指针是依次从前到后去移动的,不可能绕过前面直接到后面
打开一个文件后,用lseek往后跳一段,再write写一段,就构成了一个空洞文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd = -1; // fd 就是file descriptor,文件描述符
char buf[100] = {0};
char write_buf[20] = "abcd";
int ret = -1;
// 第一步:打开文件
// fd = open("a.txt", O_WRONLY | O_CREAT | O_EXCL, 0666);
fd = open("123.txt", O_RDWR | O_CREAT, 0666);
if(-1 == fd) // 有时候这么写:if(fd < 0)
{
// printf("文件打开错误\n");
perror("open");
// return -1;
_exit(-1);
}
else
{
printf("文件打开成功,fd = %d.\n", fd);
}
ret = lseek(fd, 10, SEEK_SET);
printf("lseek, ret = %d.\n", ret);
// 第二步:读写文件
#if 1 // 写文件
ret = write(fd, write_buf, strlen(write_buf));
if(ret < 0)
{
// printf("写入失败\n");
perror("write");
// return -1;
_exit(-1);
}
else
{
printf("write成功,实际写入了%d字节.\n", ret);
}
#endif
#if 1
// 读文件
ret = read(fd, buf, 20);
if(ret < 0)
{
printf("读取失败\n");
// return -1;
_exit(-1);
}
else
{
printf("实际读取了%d字节.\n", ret);
printf("文件内容是:[%s].\n", buf);
}
#endif
// 第三步:关闭文件
close(fd);
_exit(0);
}
空洞文件方法对多线程共同操作文件是及其有用的,有时候我们创建一个很大的文件,如果从头开始依次构建时间很长。有一种思路就是将文件分成多段,然后多线程操作,每个线程负责其中一段的写入
10、文件IO和标准IO
(1)文件IO效率和标准IO
文件IO指的是open、close、write、read、lseek等API函数构成一套用来读写文件的体系,这套体系可以很好的完成文件读写,但是效率并不是很高的应用层C语言库函数提供了一些用来做文件读写的函数列表,叫标准IO。标准IO由一系列的C库函数构成,fopen、fclose、fread、fwrite,这些标准IO函数其实是由文件IO封装而来的。(fopen内部其实调用的还是open,fwrite内部还是通过write来完成写入的)标准IO加了封装之后主要是为了在应用层添加一个缓冲机制,这样我们通过fwrite写入的内容不是直接进入内核中的buf,而是先进入应用层标准IO库自己维护的buf中,然后标准IO库自己根据操作系统单次write的最佳count来选择好的时机来完成write到内核中的buf,内核中的buf再根据硬盘的特性来选择好的时机去最终写入硬盘中。
(2)标准IO和文件IO的区别
看起来使用时都是函数,但是标准IO是C库函数,而文件IO是linux系统的APIC语言库函数是由API封装而来的,库函数内部也是通过调用API来完成操作的,但是库函数因为多了一层封装,所以比API要更加好用一些库函数比API还有一个又是:API在不同的操作系统之间是不能通用的,但是C库函数在不同操作系统中几乎是一样的。所以C库函数具有可移植性而API不具有可移植性性能上和易用性上看,C库函数一般要更好一些。如IO,文件IO是不带缓存的,而标准IO是带有缓存的,因此标准IO比文件IO性能更高
(3)常用标准IO(库函数)介绍
常见的标准IO库函数有:fopen、fclose、fwrite、fread、fseek(文件指针操纵)、ffulsh(刷新(同步),立刻将应用层中缓冲区buf的数据写入系统buf中)
FILE *fopen(const char *path, const char *mode);
// FILE 返回值,返回一个文件指针
// mode r、r+、w、w+、a、a+