本文共 2947 字,大约阅读时间需要 9 分钟。
1, 什么是文件
任何一个进程在对文件进行读写操作前,都需要先打开该文件。操作系统内核为每个进程维护一个打开文件的列表,该列表是一个指针数组,指向每个打开文件的元数据,其中包括指向文件inode的内存拷贝,文件位置和访问模式等),数组的索引即文件描述符(filedescriptors),进程通过文件描述符即可进行文件的读写。
同其他所有类Unix系统一样,Linux也本着“一切皆文件”的设计原则。除了保存在磁盘上的普通文件,Linux的其他的资源也以访问文件的方式。这些特殊的文件包括设备文件、管道、目录、futex(Fast User Space Mutex)、FIFO以及Socket。这么做可以大量减少系统调用的数量,简化文件和设备的操作。
子进程继承父进程的文件表。每个进程默认打开三个文件描述符:0、1、2,分别是stdin、stdout、stderr,demon进程通常需要将标准错误、输出关闭或者重定向到日志文件。默认情况下文件的大小1024,即一个进程最多可以同时打开1024个文件。对一个文件操作完毕后应该及时关闭,否则造成文件描述符泄漏,在到达上限时将不能打开新的文件。对于需要同时打开更多文件的进程可以提高资源限制RLIMIT_NOFILE。Linux进程创建了一个文件,该文件的所有者是该进程的用户id。文件所有用户组是该组id。当文件创建时,mode参数提供新建文件的权限。系统并不在该次打开文件时检查权限,所以你可以进行相反的操作,例如设置文件为只读权限,但却在打开文件后进行写操作。权限通常用八进制数掩码表示,创建文件默认最大权限为666。这个权限是根据umask掩码生成的一般来说,umask命令是在/etc/profile文件中设置的,每个用户在登录时都会引用这个文件,所以如果希望改变所有用户的umask,可以在该文件中加入相应的条目。,它与chmod的效果刚好相反,umask设置的是权限“补码”,而chmod设置的是文件权限码。用read读取文件一定要放在循环中并判断返回值,因为read可能返回一个比len小的非零正整数对于read()来说是合法的。出现这种情况,可能有各种各样的原因,例如:可供读取的字节数本来就比len要少,系统调用可能被信号打断,管道可能被破坏(如果fd是个管道),等等。为了达到目的,你需要一个循环和一些条件语句。
非阻塞读是程序员不希望当没有可读数据时让read()调用阻塞。相反,他们倾向于在没有可读数据时,让调用立即返回。这种情况被称为非阻塞I/O;它允许应用在从不阻塞的状况下进行I/O操作,如果是在操作多个文件时,不至于丢失其他文件中的可用数据。相对于read()的返回部分读的情况,write()不太可能返回一个部分写的结果。而且,对write()系统调用来说没有EOF情况。对于普通文件,就不需要进行循环写了。然而对于其他类型,例如套接字,需要有个循环来保证写入了所有请求的字节。
追加模式下打开文件时(通过指定O_APPEND参数),写操作就不从文件描述符的当前位置开始,而是从当前文件末尾开始。追加模式可以避免多进程同时写一个文件。非阻塞模式下打开文件时(通过设置O_NONBLOCK参数),并且发起的写操作会正常阻塞时,write()系统调用返回-1,并设置errno值为EAGAIN。请求应该在稍后重新发起。通常处理普通文件时不会出现这种事,套接字可能因为对端问题出现这种不可写入的情况。write()调用返回时,内核仅仅将进程所提供的数据复制到了内核缓冲区中,但却没有保证数据已写到目的磁盘文件。稍后内核在后台收集所有这样的“脏”缓冲区,将它们排好序,并写入到磁盘上(此过程称为回写)。这使得write调用马上被调用并立刻返回,内核可以将写入操作推迟到空闲阶段,并将很多写操作一起处理。写缓冲提供了巨大的性能改进,Linux同时也提供了同步写入磁盘的方法。如果有应用想要控制数据被写入磁盘的时间,可以通过调用fsync、fdatasync实现同步化IO。fsync()可以保证fd对应文件的脏数据回写到磁盘上,在驱动器确认数据已经全部写入之前不会返回,但数据仍然可能还在磁盘驱动器的缓存上。fdatasync只同步数据,不同步元数据,可能会相对较快。但这两个系统调用不保证文件的目录项也写到磁盘上,也就是说,这段时间内的指向该文件的链接存在不可用的短暂状态。
前两个系统调用只针对一个文件,sync()系统调用可以用来对磁盘上的所有缓冲区进行同步, 尽管其效率不高,但仍然被广泛使用。另一个方法是open()文件时使用O_SYNC标志,让所有在该文件上的I/O 操作同步。 但这种时间开销增长是非常可观的,所以同步I/O—般是在无计可施情况下的最后选择。与其他现代操作系统内核一样,Linux内核实现了一个复杂的缓存、缓冲以及设备和应用之间的I/O管理的层次结构。一个高性能应用可能希望越过这些复杂的层次结构并进行独立的I/O管理,如数据库系统,比较倾向于使用他们自己的缓存机制,以尽可能的减少操作系统的影响。
系统提供O_DIRECT标志给open系统调用,会绕过内核的页面缓存,直接启动用户空间的缓冲区与设备之间的IO,所有IO将会同步,直到操作完成后才返回。执行直接IO,请求的长度、缓冲区的调整、文件的偏移量必须是底层设备扇区的大小。close()调用解除了已打开的文件描述符的关联,并分离进程和文件的关联。关闭文件和文件被写入磁盘没什么关系,内核仍然会将缓冲区的数据延迟回写到磁盘。
lseek()系统调用能够对给定文件描述符引用的文件位置设定指定值。 lseek()最常见的用法是来定位当前文件的开始和末尾,或者确定某个文件描述符引用的当前文件位置。
lseek()是可以在文件指针超过文件末尾之后进行查找的。然而如果在接下来对该位置有一个写请求,则会在新旧长度之间建立新的空间,并由零来填充。这种零填充方式称为“空洞”(hole)。在Unix风格的文件系统上,空洞不占用任何物理上的磁盘空间。这暗示着文件系统上所有文件的大小加起来可以超过磁盘的物理大小。带空洞的文件叫做“稀疏文件”(sparse file)。稀疏文件可以节省可观的空间并提升效率,因为操作那些空洞并不引发任何物理I/O。Linux提供了两种read()和write()的变体pread()、pwrite()来替代lseek(),每个调用都以需要读写的文件位置为参数。其特点是:
第一,这些调用更加简单易用,尤其是在文件中做反向移动和随机移动这种技巧性很强的操作时更是如此。第二,当操作完成时,不修改文件位置指针。第三,多进程共享文件时,避免了使用lseek()可能出现的潜在竞争。ftruncate、truncate可以将文件截短到len指定的长度,也可以将文件“截短”到比原长度更长,类似于前面“查找到文件末尾之后”中查找加上写操作的结合。扩展出的字节将全部填充为零。这两个操作均不修改当前文件位置。
转载于:https://blog.51cto.com/13376824/2055454