一、文件IO概念
linux文件IO操作有两套大类的操作方式:不带缓存的文件IO操作,带缓存的文件IO操作。不带缓存的属于直接调用系统调用(system call)的方式,高效完成文件输入输出。它以文件标识符(整型)作为文件唯一性的判断依据。这种操作不是ASCI标准的,与系统有关,移植有一定的问题。而带缓存的是在不带缓存的基础之上封装了一层,维护了一个输入输出缓冲区,使之能跨OS,成为ASCI标准,称为标准IO库。不带缓存的方式频繁进行用户态 和内核态的切换,高效但是需要程序员自己维护;带缓冲的方式因为有了缓冲区,不是非常高效,但是易于维护。由此,不带缓冲区的通常用于文件设备的操作,而带缓冲区的通常用于普通文件的操作。
文件平时存储在块存储设备中的文件系统中(静态文件),open打开一个文件时,linux内核在进程中建立一个打开文件的数据结构,记录打开的文件的信息;内核在内存中申请建立一段内存,并将静态文件的内容从块存储设备读取到特定地址管理存放(动态文件)。
打开文件后对这个文件的读写操作都是针对内存中的动态文件,当对动态文件进行读写后,动态文件和块存储设备中的静态文件不同步,close时关闭动态文件,内核将内存中的动态文件的内容同步到块存储设备的静态文件。
二、文件描述符
文件描述符是一个非负整数,用来标识一个进程中打开或创建的文件。当打开一个现有文件或创建一个新文件时,内核向应用程序进程返回一个文件描述符。当读或写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传递给read或write等操作函数。文件描述符的作用域限于当前应用程序的进程,文件关闭close后,文件描述符将被释放。在遵从POSIX的应用程序中,文件描述符0、1、2分别对应STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO,因此一个应用程序进程中文件描述符总是从3开始的。
三、常用文件IO函数
1、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);
Open函数返回打开、创建文件的文件描述符,如果失败返回-1。
Flags:
O_RDONLY //只读打开
O_WRONLY //只写打开
O_RDWR //读、写打开
O_APPEND //每次写时都追加到文件的尾端
O_CREAT //若此文件不存在,则创建它。使用时,需要第三个参数mode
O_EXCL //如果同时指定了O_CREAT,而文件已经存在,则会出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件。
O_TRUNC //如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0。
O_NONBLOCK //如果pathname指的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次操作和后续的I/O操作设置非阻塞模式。只用于设备文件,不能用于普通文件。
O_SYNC //使每次write都等到物理I/O操作完成,包括由write操作引起的文件属性更新所需的I/O。
Mode:
使用四个数字指定创建文件的权限,与linux的权限设置相同,如0755
2、close
#include <unistd.h>
int close(int fd);
关闭文件描述符fd指向的动态文件,并存储文件和刷新缓存。
3、read
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
Read成功返回读取的字节数,失败返回-1
4、write
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
Write成功返回写入的字节数,失败返回-1。
5、lseek
每一个已打开的文件都有一个读写位置,当打开文件时通常其读写位置是指向文件开头,若是以附加的方式打开文件(如O_APPEND),则读写位置会指向文件尾。当read()或write()时,读写位置会随之增加,lseek()便是用来控制该文件的读写位置。
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
Lseek成功,返回当前的位置,即当前位置距离文件开头的字节数,失败返回-1
Offset:偏移量
Whence:偏移基址
SEEK_SET:将读写位置指向文件头后再增加offset个位移量
SEEK_CUR:以目前的读写位置往后增加offset个位移量
SEEK_END:将读写位置指向文件尾后再增加offset个位移量
A、 欲将读写位置移到文件开头时
lseek(int fd,0,SEEK_SET);
返回0
B、欲将读写位置移到文件尾时
lseek(int fd,0,SEEK_END);
返回文件长度
C、想要取得目前文件位置时
lseek(int fd,0,SEEK_CUR);
返回当前文件指针相对于文件开头的偏移量
D、计算文件长度
int get_file_length(const char *filename)
{
unsigned int n = 0;
unsigned int fd = open(filename, O_RDONLY);
if(fd < 0)
{
fprintf(stderr, "error:%s PID: %d\n", strerror(errno), getpid());
return -1;
}
n = lseek(fd, 0, SEEK_END);
close(fd);
return n;
}
E、创建空洞文件
int create_null_file(const char *filename, unsigned int len)
{
unsigned int fd = open(filename, O_RDWR | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
fprintf(stderr, "error:%s PID: %d", strerror(errno), getpid());
return -1;
}
lseek(fd, len, SEEK_SET);
write(fd, "\0",1);
close(fd);
return 0;
}
四、错误信息
errno就是error number。
如果程序代码中包含 #include <errno.h>,函数调用失败的时候,系统会自动用用错误代码填充errno这个全局变量,取读errno全局变量可以获得失败原因。函数调用失败是否会设置errno全局变量由函数决定,并不是所有函数调用失败都会设置errno变量。
#include <string.h>
char *strerror(int errnum);
fprintf(stderr,"error :%s, PID: %d ",strerror(errno),getpid());
根据errnum错误码返回一个指向描述errnum错误码信息的字符串指针。
#include <stdio.h>
void perror(const char *s);
perror ( )用来将上一个函数发生错误的原因输出到标准错误(stderr),参数s 所指的字符串会先打印出,后面再加上错误原因 字符串。
五、文件共享
文件共享的是三种实现方式:
1、同一个进程中多次打开同一个文件
2、不同进程中多次打开同一个文件
3、Dup和dup2让进程复制文件描述符
同一个进程中多次打开同一个文件,返回的文件描述符不同,同时对这个文件进行写操作时,分别写入内容,后边写入的内容将覆盖前边写入的内容,此时不同的文件描述符拥有自己的文件指针。当open创建、打开文件时采用O_APPEND,则不同的文件描述符的不同文件指针会实现同步。O_APPEND是原子操作的。所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断。
文件描述符的复制
#include <unistd.h>
int dup(int oldfd);//返回新分配的文件描述符,失败返回-1
int dup2(int oldfd, int newfd);//newfd为指定的新的文件描述符,返回新分配的文件描述符,失败返回-1
dup函数复制的文件描述符不同,但文件指针相同,所以对文件的操作是原子操作的。
六、文件IO与标准IO库的区别
文件IO与标准IO库的区别:文件IO是linux系统的API,标准IO库是C语言库函数,标准IO库函数由linux API封装而来,函数内部通过调用linux API完成。API在不同的操作系统之间不能通用,C语言库函数可以在不同操作系统之间移植。文件IO函数不带缓存,标准IO库函数带缓存。