首页 > 代码库 > UNIX环境高级编程(6):文件I/O(2)
UNIX环境高级编程(6):文件I/O(2)
文件共享:
UNIX系统支持在不同进程间共享打开的文件。
内核使用三种数据结构表示打开的文件。他们之间的关系决定了在文件共享方面一个进程对还有一个进程可能产生的影响:
(1)每一个进程在进程表中都有一个记录项。记录项中包括有一张打开文件描写叙述符表。可将其视为一个矢量。每一个描写叙述符都占用一项,与每一个文件描写叙述符相关联的是:
- 文件描写叙述符标志
- 指向一个文件表项的指针
(2)内核为全部打开文件维护一张文件表,每一个文件表项包含:
- 文件状态标志(读,写,添写,同步和非堵塞等);
- 当前文件偏移量;
- 指向该文件v节点表项的指针;
(3)每一个打开文件(或设备)都有一个v节点结构。v节点包括了文件类型和对此文件进行各种操作的函数的指针;对于大多数文件,v节点还包括了该文件的i节点(i-node,索引节点)。这些信息是打开文件时从磁盘上读入内存的。所以全部关于文件的信息都是高速可供使用的。
比如。i节点内包括了文件的全部者。文件长度。文件所在设备,指向文件实际数据在磁盘上所在位置的指针等等。
上面的讨论是概念性的,与特定实现可能匹配。也可能不匹配(Linux没有使用v节点,而是使用了通用i节点结构,可是在概念上,v节点与i节点是一样的。两者都指向文件系统特有的i节点结构,创建v节点结构的目的是对在一个计算机系统上的多文件系统类型提供支持。sun称这样的文件系统为虚拟文件系统,称与文件系统类型无关的i节点部分为v节点)。
假设两个独立进程各自打开了同一个文件,打开该文件的每一个进程都得到一个文件表项,但对一个给定的文件仅仅有一个v节点表项。每一个文件都有自己的文件表项的一个理由是:这样的安排使每一个进程都有它自己的对该文件的当前偏移量。
可能有多个文件描写叙述符项指向同一个文件表项。
比如dup函数,或者在fork后也会发生相同的情况,此时父、子进程对于每个打开文件文件描写叙述符共享同一个文件表项。
因此多个进程读同一文件都能正确工作。由于每一个进程都有它自己的文件表项,当中也有它自己的当前文件偏移量。可是多个进程写同一个文件时,则可能产生预期不到的结果。
原子操作:
添写至一个文件:
要在一个文件里进行添写,第一种方法是“定位到文件尾端,然后写”,它使用两个分开的函数调用(lseek。write)。这样的方法会出问题。不论什么一个须要多个函数调用的操作都不可能是原子操作,由于在两个函数之间,内核有可能会暂时挂起该进程。解决这个问题的方法是使这两个操作对于其它进程而言成为一个原子操作。
UNIX系统提供了一种方法使这样的操作成为原子操作,该方法是在打开文件时设置O_APPEND标志。这就使得内核每次对该文件进行写之前,都将进程的当前文件偏移量设置到该文件的尾端处,于是在每次写之前就不再须要调用lseek。
pread和pwrite:
SUS包括了XSI扩展。该扩展同意原子性地定位搜索(seek)与运行I/O。pread和pwrite就是这样的扩展:
#include <unistd.h>
ssize_t pread(int fieldes, void *buf, size_t nbytes, off_t offset);
返回值,读到的字节数,若已到文件结尾则返回0。若出错则返回-1;
ssize_t pwrite(int fieldes, const void *buf, size_t nbytes, off_t offset);
返回值,若成功则返回已写的字节数,若出错。则返回-1;
创建一个文件:
之前已经讲过open函数的O_CREAT和O_EXCL选项。当同一时候指定这两个选项。而该文件又已经存在时。open将失败。
这两个选项使得检查文件是否存在以及创建该文件这两个操作是作为一个原子操作运行的。
综上所述,原子操作指的是由多步组成的操作,假设该操作原子地运行,则要么运行全然部步骤。要么一步也不运行。不可能仅仅运行全部步骤的一个子集。
dup和dup2函数:
以下两个函数都可用来复制一个现存的文件描写叙述符:
#include <unistd.h>
int dup(int fieldes);
int dup2(int fieldes, int fieldes2);
两函数的返回值:若成功则返回新的文件描写叙述符,若出错则返回-1。
由dup返回的新文件描写叙述符一定是当前可用文件描写叙述符中的最小数值,用dup2则能够用fieldes2參数指定新描写叙述符的数值。假设fieldes2已经打开,则先将其关闭。
假设fieldes等于fieldes2,则dup2返回filedes2。而不关闭它。
这些函数返回的新文件描写叙述符与參数filedes共享同一个文件表项。由于两个描写叙述符指向同一文件表项,所以它们共享同一文件状态标志(读、写、添写等)以及同一当前文件偏移量。
可是每一个文件描写叙述符都有它自己的一套文件描写叙述符标志。
拷贝文件描写叙述符的还有一种方法是使用fcntl函数:
- 调用dup(filedes) 等效于fcntl(filedes, F_DUPFD, 0);
- 而调用dup2(filedes, filedes2)等效于close(filedes2); fcntl(filedes, F_DUPFD, filedes2);
它们之间的差别在于:
- dup2是一个原子操作。而close及fcntl则包括两个函数调用;
- dup2和fcntl有某些不同的errno。
sync、fsync、fdatasync函数:
传统的UNIX实如今内核中设有缓冲区快速缓存或页面快速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据拷贝到当中一个缓冲区中,假设该缓冲区尚未写满。则并不将其排入输出队列,而是等待其写满或者内核须要重用该缓冲区以存放其他磁盘块数据时。再将该缓冲排入到输出队列,然后待其到达队首时,才进行实际的I/O操作。
这样的输出方式被称为“延迟写”。
延迟写降低了磁盘读写次数。可是却降低了文件内容的更新速度,使得欲写到文件里的数据在一段时间内并没有写到磁盘上。当系统发生问题时,这样的延迟可能造成文件更新内容的丢失。
为了保证磁盘上实际文件系统与缓冲区快速缓存中内容的一致性,UNIX系统提供了sync,fsync和fdatasync三个函数。
#include <unistd.h>
int fsync(int filedes);
int fdatasync(int filedes);
返回值:若成功则返回0。若出错则返回-1;
void sync(void)。
sync函数仅仅是将全部改动过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
通常称为update的系统守护进程会周期性地调用sync函数,命令sync(1)也调用sync函数。fsync函数仅仅对由文件描写叙述符filedes指定的单一文件起作用,而且等待写磁盘操作结束,然后返回。fdatasync函数类似于fsync。但它仅仅影响文件的数据部分。而fsync还会同步更新文件的属性。
fcntl函数:
fcntl函数能够改变已打开文件的性质:
#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /* arg */);
返回值:若成功则依赖于cmd。若出错则返回-1。
fcntl函数有5种功能:
- 复制一个现有的描写叙述符(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)。
F_DUPFD:在上面已经讲过了。拷贝文件描写叙述符filedes,新文件描写叙述符作为函数值返回,它是尚未打开的各描写叙述符中大于或等于第三个參数值中各值的最小值。
F_GETFD:相应于filedes的文件描写叙述符标志作为函数值返回。当前仅仅定义了一个文件描写叙述符标志FD_CLOEXEC;
F_SETFD:对于filedes设置文件描写叙述符标志,新标志值按第三个參数设置。
F_GETFL:相应于filedes的文件状态标志作为函数值返回。在说明open函数时。已经说明了文件状态标志(注意,O_RDONLY、O_WRONLY、O_RDWR三个訪问方式标志并不各占一位。因此首先要用屏蔽字O_ACCMODE取得訪问模式位。然后将结果与这三种值中的不论什么一种作比較)。
F_SETFL:将文件状态标志设置为第三个參数的值,能够更改的几个标志是:O_APPEND,O_NONBLOCK,O_SYNC。O_DSYNC。O_RSYNC。O_FSYNC、O_ASYNC。
F_GETOWN:取当前接收SIGIO和SIGURG信号的进程ID或进程组ID;
F_SETOWN:设置接收SIGIO和SIGURG信号的进程ID和进程组ID,正的arg指定一个进程ID。负的arg表示等于arg绝对值的一个进程组ID;
下列程序接受一个文件描写叙述符作为參数,打印该文件描写叙述符所指向的文件表项中的文件状态标志:
/* * Copyright (C) fuchencong@163.com */ #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> int main(int argc, char *argv[]) { int val; if (argc != 2) { printf("usage: ./fcntl fd\n"); exit(1); } if ( (val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0) { printf("fcntl error\n"); exit(1); } switch (val & O_ACCMODE) { case O_RDONLY: printf("read only"); break; case O_WRONLY: printf("write only"); break; case O_RDWR: printf("read write"); break; default: printf("unknown access mode"); break; } if (val & O_APPEND) { printf(", append"); } if (val & O_NONBLOCK) { printf(", nonblocking"); } #if defined(O_SYNC) if (val & O_SYNC) { printf(", synchronous writes"); } #endif #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) if (val & O_FSYNC) { printf(", synchronous writes"); } #endif putchar(‘\n‘); exit(0); }
上述使用了功能測试宏_POSIX_C_SOURCE,而且条件编译了POSIX.1中未定义的文件訪问标志。以下是该程序在bash中运行结果,当中5<>temp.foo表示在文件描写叙述符5上打开文件temp.foo以供读写:
在改动文件描写叙述符标志或文件状态标志时必须慎重,先要取得现有的标志值,然后依据须要改动它。最后设置新标志值。不能仅仅是运行F_SETFD或F_SETFL。这样会关闭曾经设置的标志位:
下列程序显示了对一个文件描写叙述符设置一个或多个文件状态标志的范例:
/* * Copyright (C) fuchencong@163.com */ #include <fcntl.h> #include <stdlib.h> #include <stdio.h> void set_fl(int fd, int flags) { int val; if ( (val = fcntl(fd, F_GETFL, 0)) < 0) { printf("get file flag error\n"); return ; } val |= flags; if (fcntl(fd, F_SETFL, val) < 0) { printf("set file flag error\n"); } }
假设将 var |= flags 改为 val &= ~flags则用来关闭某个标志位。
程序执行时,设置O_SYNC标志会添加时钟时间。由于每次write操作都要等待,直至数据已写到磁盘上再返回。
所以当支持同步写时,系统时间和时钟时间应当会显著添加。
尽管能够在调用open函数时就设置文件状态标志。可是fcntl函数仍然很有必要。fcntl函数同意在仅知道文件描写叙述符的情况下改动其性质。
比如,标准输出是由shell打开的,因此我们无法通过open函数来设置其文件状态标志,可是通过fcntl函数能够做到。
ioctl函数:
ioctl函数是I/O操作的杂物箱,不能用其他函数表示的I/O操作通常都能用ioctl表示,终端I/O是iotcl的最大使用方面。
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
#include <stropts> /* XSI STREAMS */
int ioctl(int filedes,int request,...);
iotcl函数仅仅是SUS标准的一个扩展,以便处理STREAMS设备,可是UNIX系统实现用它进行非常多杂项设备操作。有些实现甚至将它扩展到用于普通文件。
在此函数原型中,我们表示的仅仅是iotcl函数本身所要求的头文件。通常,还要求另外的设备专用头文件。每一个设备驱动程序都能够定义它自己专用的一组ioctl命令,系统则为不同种类的设备提供通用的ioctl命令。
/dev/fd
较新的系统都提供名为/dev/fd的文件夹,其文件夹项是名为0,1,2等的文件,打开文件/dev/fd/n等效于复制描写叙述符n(假定描写叙述符n是打开的)。
在下列函数调用中:
fd = open("/dev/fd/0", mode);
大多数系统会忽略它所指定的mode,而另外一些则要求mode必须是所涉及的文件原先打开时所使用mode的子集。
上面的函数调用等效于fd = dup(0)。所以描写叙述符0和fd共享同一文件表项。
某些系统提供路径名/dev/stdin、/dev/stdout/和/dev/stderr,这些等效于/dev/fd/0、/dev/fd/1和/dev/fd/2。
/dev/fd文件主要由shell使用,它同意那些使用路径名作为调用參数的程序,能用处理其他路径名的同样方式处理标准输入和输出。尽管非常多程序都支持在命令行中使用"-"作为一个參数。特指标准输入或输出。可是/dev/fd则提高了文件名称參数的一致性,也更加清晰。
UNIX环境高级编程(6):文件I/O(2)