先要回答的问题
文件IO指的是什么?
本文主要讲述如何调用Linux OS所提供的相关的OS API,实现文件的读写。
如何理解文件IO?
IO就是input output的意思,文件io就是文件输入输出,也就是文件读写。
文件读写,读写的是什么?
是数据。
文件IO(Input Output),也就是输入输出是对什么而言的?参考点是什么?
是CPU
能不能越过OS,直接操作文件呢?
当有OS的时候,应用程序基于OS运行时,必须通过OS API假借OS之手,才能操作底层硬件,无法回避。
文件IO涉及到的OS API
①open函数:打开文件
②close函数:关闭文件
③read函数:从打开的文件读数据
④write函数:向打开的文件写数据
⑤lseek函数:移动在文件中要读写的位置
⑥dup函数:文件读写位置重定位函数,本来是写到这个文件,重定位后可以写到另一个文件里面
⑦fcntl函数:文件描述符设置函数
⑧ioctl函数:一个特殊的函数
文件读写的简单例子
文件操作三步曲
①打开文件 open函数
②读、写等操作文件 read、write函数
③关闭文件 close函数
代码演示
1 #include <stdio.h> 2 #include <sys/types.h> 3 #include <sys/stat.h> 4 #include <fcntl.h> 5 #include <unistd.h> 6 7 int main(void) 8 { 9 int fd = 0; 10 11 fd = open("./file.txt", O_RDWR); 12 if(-1 == fd) 13 { 14 printf("open fail\n"); 15 return 0; 16 } 17 else 18 { 19 printf("open ok\n"); 20 } 21 22 char buf1[] = "hello world"; 23 write(fd, (void *)buf1, 11); 24 25 lseek(fd, 0, SEEK_SET); 26 27 char buf2[30] = {0}; 28 read(fd, buf2, sizeof(buf2)); 29 30 printf("buf2 = %s\n", buf2); 31 32 close(fd); 33 34 return 0; 35 }
API
open
原型
#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);
功能
第一种:只能打开存在的文件,如果文件不存在就返回-1报错。
第二种:如果文件存在就直接打开,如果文件不存在,就按照mode指定的文件权限,创建一个该名字的新文件。也就是说三个参数时,不仅包含打开已存在文件的功能,还多了一个创建文件的功能。
参数
pathname:表示路径名,很简单
flags:
flags的作用?
flags用于指定文件的打开方式,这些宏还可以使用|组合,比如O_RDONLY | O_APPEND,同时指定多个宏。
这些宏对应的就是一些整形数,#define O_RDONLY 2。
这些宏被定义在了那里?
定义在了open所需要的头文件中,使用open函数时,必须要包含对应的头文件,否者,这些宏你就用不了。
宏的含义
(1)O_RDONLY:只读方式打开文件,只能对文件进行读
(2)O_WRONLY:只写方式打开文件,只能对文件记性写
(3)O_RDWR:可读可写方式打开文件,既能读文件,也能写文件
以上这三个在指定时,只能唯一指定,不可以组合,比如O_RDONLY|O_WRONLY。
(4)O_TRUNC:打开时将文件内容全部清零空
(5)O_APPEND:打开文件后,写数据时,原有数据保留,新写的数据追加到文件末尾,此选项很重要。如果不指定这个选项的话,新写入的数据会从文件头上开始写,覆盖原有的数据。
(6)O_CREAT
open两个参数时的缺点?
只能用于打开已经存在的文件,如果文件不存在就返回-1报错。
O_CREAT的作用
可以解决两个参数的缺点,指定O_CREAT时,如果:
文件存在:直接打开
文件不存在:创建该“名字”的文件。
不过指定O_CREAT,需要给open指定第三个参数mode,用于指定新创建文件的原始权限。
(7)O_EXCL
当O_EXCL与O_CREAT同时被指定,打开文件时,如果文件之前就存在的话,就报错。
意义:保证每次open的是一个新的文件,如果文件以前就存在,提醒你open的不是一个新文件。
mode:创建文件时,用于指定文件的原始权限,其实就是rwxrwxr--。
返回值
如果打开成功,返回一个非负整数的文件描述符。
如果打开失败,返回-1,并且设置错误号给系统定义的全局变量errno,用于标记函数到底出了什么错误。
close
原型
#include <unistd.h> int close(int fd);
功能
关闭打开的文件。
参数
fd:文件描述符
返回值
成功返回零,失败返回-1并设置errno
备注
就算不主动的调用close函数关闭打开的文件,进程结束时,也会自动关闭进程所打开的所有的文件。但是如果因为某种需求,你需要在进程结束之前关闭文件的话,就主动的调用close函数来实现。Linux c库的标准io函数fclose,向下调用时,调用就是close系统函数。
write
原型
#include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);
功能
向fd所指向的文件写入数据。
参数
fd:指向打开的文件
buf:保存数据的缓存空间的起始地址
count:从起地址开始算起,把缓存中count个字符,写入fd指向的文件
返回值
调用成功:返回所写的字符个数
调用失败:返回-1,并给errno自动设置错误号
数据中转过程
应用缓存(buf)————>open打开文件时开辟的内核缓存——————>驱动程序的缓存——————>块设备上的文件
read
原型
#include <unistd.h> ssize_t read(int fd, void *buf, size_t count);
功能
从fd指向的文件中,将数据读到应用缓存buf中
参数
fd:指向打开的文件
buf:读取到数据后,用于存放数据的应用缓存的起始地址
count:缓存大小(字节数)
返回值
调用成功:返回所写的字符个数
调用失败:返回-1,并给errno自动设置错误号
数据中转的过程
应用缓存(buf)<————open打开文件时开辟的内核缓存<——————驱动程序的缓存<——————块设备上的文件
lseek
原型
#include <sys/types.h> #include <unistd.h> off_t lseek(int fd, off_t offset, int whence);
功能
调整读写的位置,就像在纸上写字时,挪动笔尖所指位置是一样的。
C库的标准io函数里面有一个fseek函数,也是用于调整读写位置的,fseek就是对lseek系统函数封装后实现的
参数
fd:文件描述符,指向打开的文件
whence:
粗定位,选项有:
SEEK_SET:调到文件起始位置
SEEK_CUR:调到文件当前读写的位置
SEEK_END:调到文件末尾位置
offset:
精定位:微调位置,从whence指定的位置,向前或者向后移动指定字节数。
为负数:向前移动指定字节数
为正数:向后移动指定字节数
不过当whence被指定为SEEK_SET时,如果offset被指定为负数的话,是没有意义,为什么?
因为已经到文件头上了,在向前移动就越界了,不再当前文件的范围内了,如果非要向前调整,lseek函数会报错。
返回值
返回当前读写位置相对于文件开始位置的偏移量(字节)。
可以使用lseek函数获取文件的大小,怎么获取?
答:将文件读写的位置移动到最末尾,然后获取返回值,这个返回值就是文件头与文件尾之间的字节数,也就是文件大小。
调用失败,返回-1,并给errno设置错误号。
dup
原型
#include <unistd.h> int dup(int oldfd);
功能
复制某个已经打开的文件描述符,得到一个新的描述符,这个新的描述符,也指向被复制描述符所指向的文件。
比如:4指向了某个文件,从4复制出5,让5也指向4指向的文件。
复制某个已经打开的文件描述符,得到一个新的描述符,这个新的描述符,也指向被复制描述符所指向的文件。
参数
oldfd:会被复制的、已经存在的文件描述符。
返回值
成功:返回复制后的新文件描述符
失败:返回-1,并且errno被设置。
dup2
原型
#include <unistd.h> int dup2(int oldfd, int newfd);
功能
功能同dup,只不过在dup2里面,我们可以自己指定新文件描述符。如果这个新文件描述符已经被打开了,dup2会把它给关闭后,再使用。
比如:dup(2, 3);
从2复制出3,让3也指向2所指向的文件,如果3之前被打开过了,dup2会关闭它,然后在使用。
dup2和dup的不同之处在于:
dup:自己到文件描述符池中找新文件描述符
dup2:我们可以自己指定新文件描述符
参数
oldfd:会被复制的、已经存在的文件描述符。
newfd:新的文件描述符
返回值
成功:返回复制后的新文件描述符
失败:返回-1,并且errno被设置。
Linux错误号——errno
在前面的代码中,如果open失败了,只是笼统的打印出“打开文件失败了”,但是并没有提示具体出错的原因,没有详细的出错原因提示,遇到比较难排查的错误原因时,很难排查出具体的函数错误。open失败,如何具体打印出详细的出错信息呢?这就不得不提errno的作用了。
什么是ernno?
函数调用出错时,Linux系统使用错误编号(整形数)来标记具体出错的原因,每个函数有很多错误号,每个错误号代表了一种错误,产生这个错误时,会自动的将错误号赋值给errno这个全局变量。errno是Linux系统定义的全局变量,可以直接使用。
错误号和errno全局变量被定义在了哪里?
都被定义在了errno.h头文件,使用errno时需要包含这个头文件。
打印出具体的出错原因
perror
原型
#include <stdio.h> void perror(const char *s);
功能
perror(s) 用来将上一个函数发生错误的原因输出到标准设备(stderr)。参数 s 所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。
参数
s:在错误原因前面打印的一串字符
返回值
无
工作原理
perror函数可以自动将“错误号”换成对应的文字信息,并打印出来,方便我们理解。perror是一个C库函数,不是一个系统函数。调用perror函数时,它会自动去一张对照表,将errno中保存的错误号,换成具体的文字信息并打印出来,我们就调用perror函数时,它会自动去一张对照表,将errno中保存的错误号,换成具体的文字信息并打印出来,我们就能知道函数的具体错误原因了。
代码演示
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> int main(void) { int fd = 0; fd = open("./file.txt", O_RDWR); //fd = open("./file.txt", O_RDWR|O_CREAT|O_EXCL, 0664); if(-1 == fd) { printf("open fail: %d\n", errno); perror("open fail"); return 0; } else { printf("open ok\n"); printf("fd = %d\n", fd); } char buf1[] = "hello world"; write(fd, (void *)buf1, 11); lseek(fd, SEEK_SET, 0); char buf2[30] = {0}; read(fd, buf2, sizeof(buf2)); printf("buf2 = %s\n", buf2); close(fd); return 0; }
输出结果
open fail: 2 open fail: No such file or directory
perror会在入参字符串后加:,打印错误原因,在换行