您的位置:首页 > 编程语言

UNiX高级环境编程读书笔记(一)——文件I/O

2015-06-07 21:40 330 查看

参考链接:http://m.blog.csdn.net/blog/yusiguyuan/11960637

一、文件I/O

在对文件进行读写操作前,需要先打开文件。每个进程在进程表中都有一个记录项,每个记录项都有一张打开文件描述符,也就是内核为每个进程维护一个打开文件的列表,该表被称为文件表(file table)。该表由一些叫做文件描述符(file descriptors)的非负整数进行索引。列表中的每项均包括一个打开文件的信息,其中包括一个指向文件备份inode内存拷贝的指针和元数据(例如文件位置和访问模式等)。用户空间和内核空间都把文件描述符作为每个进程的唯一cookies。打开一个文件返回一个文件描述符,接下来的操作(读写等等)则把文件描述符作为基本参数。

子进程默认会获得一份父进程的文件表拷贝。其中打开文件列表、访问模式,当前文件位置等信息都是一致的。进程中文件表的变化(例如子进程关闭文件)也不会影响其他进程的文件表。可以让子进程和父进程共享后者的 文件表。


文件描述符不仅仅用于普通文件的访问,也用于访问设备文件、管道、目录以及快速用户空间锁、FIFOs和套接字。遵循一切皆文件的理念,任何你能读写的东西都可以用文件描述符来访问。

可用的文件I/O函数--打开文件、读文件、写文件等、unix系统中的大多数I/O需用到的五个函数:open、read、write、lseek以及close..这些函数经常被称为不带缓冲区的I/O(与下面将要讲述的标准I/O相对照)。术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。这些不带缓冲的I/O函数不是ISO
C的组成部分,但是,他们是POSIX.1和Single UNIX Specification的组成部分。还需要通过文件I/O和open函数的参数来讨论原子操作的概念,只要涉及在多个进程间共享资源,这个概念就很重要。多个进程间是如何贡献文件,以及所涉及的内核数据结构。还会说明dup、fcntl、sync、fsync和ioctl函数。

open文件

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

int open(const char *name,int flags);

int open(const char *name,int flags,mode_t mode);

flags必须是以下之一:O_RDONLY、O_WRONLY、O_RDWR

O_ASYNC

当指定文件可写或者可读时产生一个信号(默认为SIGIO)。这个标志仅用于终端盒套接字,不能用于普通文件。

O_CREAT

若此文件不存在,则创建它。使用此选项时,需要第三个参数mode,用其指定该新文件的访问权限位。

O_EXCL

如果同时指定了O_CREAT,而文件已经存在,则会出错。用此可以测试一个文件是都存在,如果不存在,则创建此文件,这使此时和创建两者称为一个原子操作。

O_TRUNC

如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0

O_NONBLOCK

如果pathname值的是一个FIFO,一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞模式。

O_NOCTTY

如果pathname指的是终端设备,则不将该设备分配作为此进程的控制终端。

O_DSYNC

使每次write等待物理I/O操作完成,但是如果写操作并不影响读取放写入的数据,则不等待文件属性被更新

O_RSYNC

使每一个以文件描述符作为参数的read操作等待,直至任何对文件同一部分进行未决写操作都完成。

O_SYNC

使每次write都等到物理I/O操作完成,包括由write操作引起的文件属性更新所需的I/O

当创建文件时,mode参数提供新建文件的权限。系统并不在该次打开文件时检查权限,mode参数是常见的UNIX权限位集合,像八进制数0644。当O_CREAT给出时则需要mode参数,一定要谨记。

O_WRONLY|O_CREAT|O_TRUNC组合经常被使用,以至于专门有个系统调用来实现。

#include <sys/types.h>

#include <sys/stat.h>

int creat(const char *name,mode_t mode);

fd=creat(file,0644);

等价于:

fd=open(file,O_WRONLY|O_CREATE|O_TRUNC,0644);

上述两个函数出错都返回-1,成功为一个文件描述符。

read函数

#include <unistd.h>

ssize_t read(int fd,void *buf,size_t len);

该系统调用从有fd指向的文件的当前偏移量至多读len个字节到buf中,成功后,将返回写入buf中的字节数。出错则返回-1,并设置errno。有些错误时可以恢复的,例如,当read()调用在未读取任何字符前被一个信号打断,它会返回-1(如果是0,则可能和EOF混),并设置errno为EINTR。在这种情况下,可以重新提交读取请求。

read()返回结构的可能值:

1)调用返回一个等于len的值。所有len字节存储到buf中。

2)返回一个大于零小于len的值。读取的字节存入buf中。这种情况出现在一个信号打断了读取过程,或在读取中发生了一个错误,有效字节大于零,但比len字节少时,或者在读入len个字节前已抵达EOF。再次进场读取(更新buf和len的值)将读入剩余字节到buf的剩余空间中,或者之处问题发生的原因(有待验证)。

3)返回0。标志着EOF,没有数据可读。

4)调用阻塞了,因为没有可用的用来读取的数据,这在非阻塞模式下不会发生。

5)返回-1,且errno为EINTR。表示在读入字节之前收到了一个信号,可以重新进行调用。

6)返回-1,且errno为EAGAIN。表示读取会因没有可用的数据而阻塞,而读请求应该在之后重开,在非阻塞模式下发生。

7)返回-1,且errno设置不同于EINTR或EAGAIN的值,表示某种更严重的错误。

write函数

#include <unistd.h>

ssize_t write(int fd,const void *char,ssize_t len);

write调用从由文件描述符fd引用文件的当前位置开始,将buf中至多count个字节写入文件中,成功时,返回写入字节数,并更新文件位置。错误时,返回-1,并将errno设置为响应的值。一个write()可以返回0,只是表示写入了另个字节。write()不太可能返回一个部分写的结果。而且,对write()系统掉哟个来说没有EOF情况,对于普通文件,除非发生一个错误,否则write()将保证写入所有的请求,但是write返回出现-1情况也有可能是应为EINTR的中断的原因。

在通过O_APPEND打开时,写操作从当前文件末尾开始写,这在两个进程同时操作一个文件时比较有用。它保证文件位置总是指向文件末尾,这样所有的写操作总是追加的,即便是多个写着。可以认为每个写请求之前的文件位置更新操作时原子操作。

当一个write()调用返回时,内核已将所提供的缓冲区数据复制到了内核缓冲区中,但却没有保证数据已写到目的文件。

文件偏移:按系统默认情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0.

lseek函数

#inlcude <unistd.h>

off_t lseek(int fileds,off_t offset,int whence);

对参数offset的解释与参数whence的值有关。

若whence是SEEK_SET,则该文件的偏移量设置为距文件开始offset个字节。

若whence是SEEK_CUR,则该文件的偏移量为从当前位置开始offset个字节,可正可负。

若whence是SEEK_END,则该文件的偏移量为从文件的末尾开始offset个字节,可正可负。

文件偏移量可以大于文件的当前长度,在这种情况下,对该问价的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0.文件中的空洞并不要求在磁盘上占用存储区。


[b] pread函数

[/b]

#include <unitd.h>

ssize_t pread(int fd,void *buf,size_t count,off_t pos);

这个调用从文件描述符fd的pos文件位置读取count个字节到buf中。

ssize_t pwrite(int fd,const void * buf,size_t count,off_t pos);

这个调用从文件描述符fd的pos文件位置写count个字节到buf中。

在调用结束后,它们不会修改文件位置。相当于在调用read()和write()前使用lseek()进行定位,不过这种操作性强;操作结束后不修改文件位置指针;同时也避免了任何在使用lseek()时可能出现的潜在竞争。

对于普通文件,写操作从文件的当前偏移量开始。如果在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处,在一次成功写之后,该文件偏移量增加实际写的字节数。

UNIX系统支持在不同进程间共享打开的文件。在说明共享之前,先介绍内核用于所有IO的数据结构。

内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

1)每个进程在进程表中都有一个记录项,记录项中包括有一张打开的文件描述表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:

文件描述符标志。
指向一个文件表项的指针。

2)内核为所有打开文件维护一张文件表,每个文件表项包含:

文件状态标志(读、写、添写、同步和非阻塞等)
当前文件偏移量
指向该文件v节点表项的指针。

3)每个打开文件或设备都有一个v节点结构。v节点包含了文件类型和对此文件进程各种操作的函数的指针。对于大多数文件,v节点还包含了该文件i节点。这些信息是在打开文件时从磁盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。例如:i节点包含了文件的所有者、文件长度、文件所在的设备、指向文件实际数据块在磁盘上所在位置的指针等等。

下图显示了一个进程的三张表之间的关系。该进程有两个不同的打开的文件:一个文件打开为标准输入,另一个打开为标准输出,这张安排对于不同进程之间共享文件的方式非常重要。





如果两个独立进程各自打开了同一个文件,假定第一个进程在文件描述符3上打开该文件,另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都得到一个文件表项,但对一个给定的文件只有一个v节点表项。每个进程都有自己的文件表项的一个理由:这种安排使每个进程都有他自己的对文件的当前偏移量。

1)在完成每个write后,在文件表项中的当前文件偏移量既增加缩写的字节数。如果这使文件偏移量超过了当前文件长度,则在i节点表项中的当前文件长度被设置为当前文件偏移量(也就是该文件加长了)

2)如果用O_APPEND标志打开了一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添写标志的文件执行写操作时,在文件表项中的当前文件偏移量首先被设置为i节点表项中的文件长度。这就使得每次写的数据添加到文件的当前末尾。

3)若一个文件用lseek定位到文件当前尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。

4)lseek函数只修改文件表项中的当前文件偏移量,没有进行任何I/O操作。


注意文件描述符标志和文件状态标志在作用域方面的区别,前者只用于一个进程的一个描述符,而后者则适用于指向给定文件表项的任何进程中的所有描述符。注意理解两者的区别。

上面讲述的一切对于多个进程读同一个文件都是能正确工作,每个进程都有它自己的文件表项,其中也有它自己的当前文件偏移量。但是,当多个进程写同一个文件时,则可能预期不到的结果。为此,原子操作的概念也就产生了,同时产生了两个函数。

同时UNIX实现在内核中设有缓冲区告诉缓存或页面告诉缓存,大多数磁盘I/O通过缓冲进行。当将数据写入文件时,内核通常先将数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则并不将其排入输出队列,而是等待期写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时,再将该缓冲排入输出队列,然后待其到达对首时,才进行实际的I/O操作。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: