文件I/O详解
对文件进行读写操作前,进程为什么要先打开文件?
内核会为每个进程配合一个文件表(file table),这文件表是以fd(文件描述符)进行索引的。对应的值是一个指向inode的指针和文件的一些元数据(文件内容位置,访问权限)。
进程利用fd文件描述符就能拿到inode指针得到文件的物理位置进行对文件的读写;
所以打开文件的操作就是让内核创建一个新的fd文件描述符和inode指针和元数据的索引;
目录与文件:
每个目录下的内容是一个个链接:文件名映射到inode的连接。
当在目录下访问一个文件内容时,内核找到文件名对应的inode编号,将inode编号发送给文件系统由文件系统拿到inode下记录文件在磁盘的物理位置。
文件:
read()函数:
read()函数对于:
1.普通文件:read函数是不会阻塞的,函数会在规定时间内返回结果;
2.终端:read函数会一直阻塞等待读取直到读取到换行符后才返回;
3.网络文件描述符:read函数会一直阻塞直到有数据到来;
一个简单且充满bug的read()函数使用:
char buf[10] = {0}; size_t len = sizeof(buf); ssize_t ret = read(fd, buf, len); if (-1 == ret) { //error }
read函数并不能保证返回值读取到的数据一定等于指定的sizeof(buf)大小;
read函数返回值有几种情况:
- 比指定大小要小的非0整数值:read函数系统调用被信号打断、可供读取的数据少于指定值、管道被破坏;
- 返回值为0:read文件位置指针已经读到文件末尾EOF;
- 返回值为-1:
- errno被设置为EINTR,read函数被信号打断,可以重新调用read();
- errno被设置为EAGAIN,在read非阻塞模式下表示还未有数据可以读。稍后应该再进行read函数的调用;
- errno被设置为非前两个,表示更严重的错误发生;
为了符合以上所有返回值情况并确保读取到所有应读取的数据,read函数的正确调用如下:
char buf[10] = {0}; char *p = buf; size_t len = sizeof(buf); ssize_t = ret; while(0 != (ret = read(fd, p, len))) { if (-1 == ret)
{
if (errno == EINTR)
continue;
else
break;
} len -= ret; p += ret; }
write()函数:
write()函数同理要考虑部分写的问题(read()函数考虑部分读问题);
write()系统调用以后数据是写到了内核的缓冲区但并没有立即写回磁盘,有几个系统调用用于写同步。即立马写回磁盘。
多路复用:
select()函数系列:
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_SET(int fd, fd_set *set); FD_ZERO(fd_set *set);
工作方式:
select将文件描述符分为三类集合:可读文件描述符集合、可写文件描述符集合、异常文件描述符集合。
将需要监视的文件描述符放入指定的集合中传给select函数,函数返回后会在三个集合中保留可进行无阻塞操作的文件描述符。下一次调用是应调用FD_ZERO对集合初始化清空。
用户空间(标准空间)I/O:
出现的原因:
所有的磁盘操作都是以块为单位进行的,如果I/O的大小是块的整数倍那么性能将有所提升。
另外将尽可能减少系统调用次数,进程陷入内核态再到用户态所耗费的时间对性能也有一定的影响。
因此就出现了用户空间(用户缓冲式)I/O。每次I/O其实是操作用户空间上的缓冲区,当用户空间的缓冲区大小等于块大小才执行系统调用进行磁盘操作。
操作:
fopen打开文件:一个已经打开的文件叫做一个流。fopen会新建一个文件描述符fd索引,返回指向文件描述符fd的指针。
fclose():关闭流。关闭前会将缓冲区内容刷新至磁盘。
操作流程:
标准I/O的写操作:从数据所在的缓冲区(用户空间上)——标准I/O缓冲区(用户空间上)——调用write()函数复制到内核缓冲区
标准I/O的读操作:从内核缓冲区——调用read()函数复制到标准I/O缓冲区——复制到程序缓冲区(用户空间)
系统I/O的写操作:从数据所在的缓冲区(用户空间上)——调用write()函数复制到内核缓冲区
系统I/O的读操作:从内核缓冲区——调用read()函数复制到程序缓冲区(用户空间)
mmap内存映射:
优点:
1.直接将磁盘数据映射到用户空间内存中,避免了标准I/O和系统调用I/O多余的数据拷贝。同时还减少了系统调用次数,性能提升明显。
2.多个进程映射到同一个文件对象的时候,数据是在进程间共享的。私有的内存区域写入时是写时复制。
缺点:
1.内存映射是以页为单位映射,若是文件太小则会造成过多的内存碎片。
几个术语:
synchronized(同步化):
对于写入操作:进程会等待数据刷新至磁盘后才返回。确保缓冲区数据和磁盘数据是一致的。
对于读取操作:读取操作总是同步化的,因为读取旧的数据没有意义。可以理解为读取磁盘的数据。
nonsynchronized(非同步化):
synchronous(同步):
对于写入操作:进程会等待写入操作写进内核缓冲区以后才返回。
对于读取操作:进程会等待所要读取的数据写到用户空间缓冲区后才返回。
asynchronous(异步):
对于写入操作:进程发起写操作请求就返回,无论是否有数据还在用户空间缓冲区。
对于读取操作:进程发起读操作请求就返回,无论是否有数据可供读取。