文件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函数返回值有几种情况:

  1. 比指定大小要小的非0整数值:read函数系统调用被信号打断、可供读取的数据少于指定值、管道被破坏;
  2. 返回值为0:read文件位置指针已经读到文件末尾EOF;
  3. 返回值为-1:
    1. errno被设置为EINTR,read函数被信号打断,可以重新调用read();
    2. errno被设置为EAGAIN,在read非阻塞模式下表示还未有数据可以读。稍后应该再进行read函数的调用;
    3. 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(异步):

对于写入操作:进程发起写操作请求就返回,无论是否有数据还在用户空间缓冲区。

对于读取操作:进程发起读操作请求就返回,无论是否有数据可供读取。

原文地址:https://www.cnblogs.com/jialin0x7c9/p/12031877.html