文件I/O(2)
文件共享
内核使用三种数据结构表示打开的文件,他们之间的关系决定了在文件共享方面一个进程对还有一个进程可能产生的影响。如图1所看到的。
1) 每一个进程在进程表中都有一个记录项。记录项中包括一张打开文件描写叙述表,可将其视为一个矢量,每一个描写叙述符占用一项。
与每一个文件描写叙述符相关联的是:
a) 文件描写叙述符标志
b) 指向一个文件表项的指针
2) 内核为全部打开文件维持一张文件表。
每一个文件表项包括:
a) 文件状态标志(读、写、读写、添些、同步和堵塞等)
b) 当前文件偏移量
c) 指向文件v节点表项的指针
3) 每一个打开文件(或设备)都有一个v节点(v-node)结构。
v节点包括了文件类型和对照文件进行各种操作的函数的指针。对于大多数文件,v节点还包括了该文件的i节点。
i节点包括文件全部者、文件长度、文件所在的设备、指向文件实际数据块在磁盘上所在位置的指针等。
图1:打开文件的内核数据结构
假设两个进程各自打开了同一个文件,则如图2所看到的。
假定第一个进程在文件描写叙述符3打开上该文件。而还有一个进程在文件描写叙述符4上打开该文件。每一个进程都得得到一个文件表项,但对一个给定的文件仅仅有一个v节点表项。每一个进程都有自己的文件表项的一个理由是:使每一个进程都有自己对该问价的当前偏移量。
如今对前一节文件I/O(1)的几个操作进一步说明:
1. 完毕write之后,文件里当前偏移量即所添加的字节数。假设当前偏移量大于文件长度。则将i节点中当前文件长度设为当前文件偏移量。
2. 用O_APPEND打开一个文件,对应标志会被设置到文件状态标识中。每次写时,当前偏移量会被设置为i节点中的文件长度
3. lseek定位到文件尾端时。则文件当前偏移量会被设置为当前文件长度。
可能有多个文件描写叙述符指向同一文件表项。调用dup和fork时都能看到这一点。
多个进程读同一文件能正确工作。但多个进程写同一文件时,可能产生预期不到的后果。能够利用原子操纵避免这样的情况。
原子操作
一般而言,原子操作指的是由多部组成的操作。假设该院自地运行,要么运行完所以步骤,要么一步也不运行。
1. 加入至一个文件
考虑一个进程。它要讲数据加入到一个文件尾端。
早期UNIX不支持open,所以能够例如以下实现:
if(lseek(fd, 0L, 2)<0)
err_sys(“lseekerror”);
if(write(fd, buf, 100) != 100)
err_sys(“writeerror”);
对于单个进程,这段程序能正常工作。
但多个进程就不一定。结社进程A和B都对同一文件进行加入操作。每一个进程都打开该文件。此时数据结构之间关系如图2中所看到的。
假定A调用lseek,将A的当前偏移量设置为1500。进程B执行lseek也将其当前偏移量设为1500。
然后B调用write,将当前偏移量增至1600。
然后内核又进行进程切换使进程A恢复执行,当A调用write时,从其当前偏移量1500处将数据写入。将替换B刚写入到该文件里的数据。
问题出在逻辑操作“定位到文件尾端处,然后写“使用了两个分开的函数调用。解决的方法是使这两个操作成为一个原子操作。O_APPEND标识,使内核每次对文件进行写之前。都将进程当前偏移量设置到该文件的尾端处。
2.pread和pwrite函数
原子性地定位搜索和运行I/0。
#include <unistd.h>
ssize_t pread(int fd, void *buf, size_tcount, off_t offset);
ssize_t pwrite(int fd, const void *buf,size_t count, off_t offset);
ssize_t pread(int fd, void *buf, size_tcount, off_t offset);
ssize_t pwrite(int fd, const void *buf,size_t count, off_t offset);
dup和dup2函数
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
上面两个函数都可用来复制一个现存的文件描写叙述符。
由dup返回的新文件描写叙述符一定是当前可用文件描写叙述符中的最小数值。用dup2则能够用newfd參数指定新描写叙述符的数值。假设newfd已经打开,则先将其关闭。假设newfd等于oldfd,则dup2返回newfd而不关闭它。
图3.3显示了这样的情况。
假定我们的进程运行了:
newfd = dup(1);
当此函数运行时,如果下一个可用的描写叙述符是3。由于这两个描写叙述符指向同一个文件表项。所以他们共享文件标志以及同一文件偏移量。
sync、fsync和fdatasync
#include <unistd.h>
void sync(void);
int fsync(int fd);
int fdatasync(int fd);
当将数据写入文件时。内核通常将数据拷贝到一个缓冲区。直到缓冲区写满。再将缓冲区排路输出队列。然后等待其到达队首,才进行实际的I/O操作。这样的输出防暑被称为延迟写。延迟写降低了磁盘的读写次数。但却降低了文件内容的跟新速度。当系统发生问题时,延迟写可能造成文件跟新内容的丢失。
为了保证磁盘上实际文件系统与缓冲区快速缓存中内容一致性。UNIX系统提供了sync、fsync和fdatasync 三个函数。
fcntl函数
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
能够改变已经打开文件的性质。
复制一个现有的描写叙述符(cmd=F_DUPFD)
获得或设置文件描写叙述符(cmd=F_GETFD|F_SETFD)
获得或设置文件状态标志(cmd=F_GETFL|F_SETFL)
获得或设置异步I/O全部权(cmd=F_GETOWN|F_SETOWN)
获得或设置记录锁(cmd=F_GETLK|F_SETLK、F_SETLKW)
能够用fcntl函数设置文件状态,经常使用设置套接字描写叙述符为非堵塞O_NONBLOCK
ioctl函数
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
提供了一个用于控制设备及其描写叙述符行为和配置底层服务的接口。
/dev/fd
打开文件/dev/fd/n等效于复制描写叙述符n。