首页 > 代码库 > APUE学习笔记:第三章 文件I/O

APUE学习笔记:第三章 文件I/O

3.1 引言

术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。这些不带缓冲的I/O函数不是ISO C的组成部分,但是,它们是POSIX.1和Single UNIX Specification的组成部分

3.2 文件描述符

UNIX系统shell使用文件描述符0与进程的标准输入相关联、文件描述符1与标准输出相关联、文件描述符2与标准出错输出相关联。

在依从POSIX的应用程序中,幻数0、1、2应当替换成符号常量STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO.这些常量都定义在<unistd.h>中

3.3 open函数

调用open函数可以打开或创建一个文件

1 #include<fcntl.h>2 int open(const char *pathname,int oflag, .../*mode_t mode */);3                                         返回值:若成功则返回文件描述符,若出错则返回-1

pathname是要打开或创建文件的名字

oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数(这些参数常量定义在<fcntl.h>头文件中)

O_RDONLY  只读打开

O_WRONLY    只写打开

O_RDWR     读写打开

大多数实现将O_RDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2,以与早期的程序兼容。在这三个常量必须指定一个且只能指定一个)

下列常量是可选择的:

O_APPEND  每次写时都会追加到文件的尾端

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

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

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

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

下面三个标志也是可选的。它们是Single UNIX Specification(以及POSIX.1)中同步输入和输出选项的一部分:

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

O_RSYNC   使每一个以文件描述符作为参数的read操作等待,直至任何对文件同一部分进行的未决写操作都完成(即read等待所有写入同一区域的写操作全部完成后再进行)

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

当文件用O_DSYNC标志打开,在重写其现有的部分内容时,文件时间属性不会同步更新。与此相反,如果文件是用O_SYNC标志打开,那么对该文件的每一次write操作都将在write返回前更新文件时间

3.4 creat函数

也可调用creat函数创建一个新文件

#include<fcntl.h>int creat(const char *pathname,mode_t mode);                               返回值:若成功则返回为只写打开的文件描述符,若出错则返回-1

此函数等效于:open(pathname,O_WRONLY | O_CREAT | O_TRUNC,mode);//所以现在不再需要creat函数

 

3.5 close函数

可调用close函数关闭一个打开的文件:

1 #include<unistd.h>2 int close(int filedes);3                         返回值:若成功则返回0,若出错则返回-1

当一个进程终止时,内核自动关闭它所有打开的文件。很多程序都利用了这一功能而不显式地用close关闭打开文件。

 

3.6 lseek函数

可以调用lseek显示地为一个打开的文件设置其偏移量

1 #include<unistd.h>2 off_t lseek(int filedes,off_t offset,int whence);3                                     返回值:若成功则返回新的文件偏移量,若出错则返回-1

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

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

若whence是SEEK_CUR,则将该文件的偏移量设置为当前值加offset,offset可为正或负

若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可或负

可以用下列方式确定打开文件的当前偏移量:

    off_t currpos;    currpos = lseek(fd,0,SEEK_CUR);

这种方法也可以用来确定所涉及的文件是否可以设置偏移量。如果文件描述符引用的是一个管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE(illegal seek)

实例:3_1 测试能否对标准输入设置偏移量

1 #include"apue.h"2 int main()3 {4     if(lseek(STDIN_FILENO,0,SEEK_CUR)==-1)5     printf("cannot seek\n");6     else7     printf("seek OK\n");8     exit(0);9 }

通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,则其偏移量必须是非负值。因为偏移量可能是负值,所以在比较lseek的返回值时应当谨慎,不要测试它是否小于0,而要测试它是否等于-1

 

文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。

实例:3_2 创建一个具有空洞的文件

 1 #include"apue.h" 2 #include<fcntl.h> 3 #include<stdio.h> 4 #include<unistd.h> 5  6 char buf1[]="abcdefghij"; 7 char buf2[]="ABCDEFGHIJ"; 8 int main() 9 {10     int fd;11     if((fd=creat("file.hole",FILE_MODE))<0)12     err_sys("creat error");13     if(write(fd,buf1,10)!=10)14     err_sys("buf1 write error");15     if(lseek(fd,16384,SEEK_SET)==-1)16     err_sys("lseek error");17     if(write(fd,buf2,10)!=10)18     err_sys("buf2 write error");19     exit(0);20 }

编译运行后,可用$ od -c file.hole查看文件的实际内容

 

3.7 read函数

调用read函数从打开文件中读数据

1 #include<unistd.h>2 3 ssize_t read(int fiedes,void *buf,size_t nbytes);4                     5             返回值:若成功则返回读到的字节数,若已到文件结尾则返回0,若出错则返回-1

有多种情况可使实际读到的字节数少于要求读的字节数:

-读普通文件时,在读到要求字节数之前已达到了文件尾端

-当从终端设备读时,通常一次最多读一行

-当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数

-当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数

-当从某些面向记录的设备(例如磁带)读时,一次最多返回一个记录

-当某一信号造成中断,而已经读了部分数据量时。

 

3.8 write函数

调用write函数想打开的文件写数据

#include<unistd.h>ssize_t write(int filedes,const void *buf,size_t nbytes);                                            返回值:若成功则返回已写的字节数,若出错则返回-1

其返回值通常与参数nbytes的值相同,否则表示出错。

write出错的一个常见原因是:磁盘已写满,或者超过了一个给定进程的文件长度限制

 

3.9 I/O的效率

大多数文件系统为改善其性能都采用某种预取技术,当检测到正进行顺序读取时,系统就试图读入比应用程序要求的更多数据,并假想应用程序很快会读这些数据。

 

3.10 文件共享

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

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

a.文件描述符标示

b.指向一个文件表项的指针

(2)内核为所有文件维持一张文件表。每个文件表包含:

a.文件状态标志(读、写、添写、同步和非阻塞等)

b.当前文件偏移量

c.指向该文件v节点表项的指针

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

(linux没有使用v节点,而是使用了通用i节点结构。虽然两种实现有所不同,但在概念上,v节点与i节点是一样的。两者都指向文件系统特有的i节点结构)

 

给出了数据结构后,现在对前面所述的操作作进一步说明:

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

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

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

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

 

3.11 原子操作

1.添加一个文件

早期的UNIX系统版本并不支持open的O_APPEND选项,所以程序被编写成下列形式:

if(lseek(fd,0L,2)<0)         /*position to EOF*/err_sys("lseek error");if(write(fd,buf,100)!=100)    /*and write*/err_sys("write error");    

这段程序对于单个进程而言可以正常运行,但是对于多进程而言,就会产生问题,因为不是原子操作。

UNIX提供了一种方法使这种操作成为原子操作,该方法是在打开文件时设置O_APPEND标志。这就使内核对这种文件进行写之前,都将进程的当前偏移量设置到该文件的尾端处,于是在每次写之前都不在需要调用lseek

 

2.pread和pwrite函数

SUS包括了XSI扩展,该扩展允许原子性地定位搜索(seek)和执行I/O。pread和pwrite就是这种扩展

#include<unistd.h>ssize_t pread(int fileds,void *buf,size_t nbytes, off_t offset);                    返回值:读到的字节数,若已到文件结尾则返回0,若出错则返回-1ssize_t pwrite(int filedes,const void *buf,size_t nbytes,off_t offset);                    返回值:若成功则返回已写的字节数,若出错则返回-1

调用pread相当于顺序调用lseek和read,但是pread又与这种顺序调用有以下区别:

-调用pread时,无法中断其定位和读操作

-不更新文件指针

(调用pwrite情况也类似)

 

3.创建一个文件

检查文件是否存在以及创建该文件这两个操作是作为一个原子操作执行的。如果没有这样一个原子操作,那么可能会编写些列程序段:

if((fd=open(pathname,O_WRONLY))<0){    if(errno== ENOENT){        if((fd=creat(pathname,mode))<0)                err_sys("creat error");        }else{            err_sys("open error");        }}

如果在open和creat之间,另一个进程创建了该文件,那么就会引起问题。例如,若在这两个函数调用之间,另一个进程创建了该文件,并且写进了一些数据,然后,原先的进程执行这段程序中的creat,这是,刚由另一个进程写上去的数据就会被擦去。

一般而言,原子操作指的是由多步组成的操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

 

3.12 dup和dup2函数

下面两个函数都可用来复制一个现存的文件描述符:

#include<unistd.h>int dup(int filedes);int dup2(int filedes, int filedes2);            两函数的返回值:若成功则返回新的文件描述符,若出错则返回-1

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。用dup2则可以用filedes2参数指定新描述符的数值。如果filedes2已经打开,则先将其关闭。如若filedes等于filedes2,则dup2返回filedes2,而不关闭它。

这些函数返回的新文件描述符与参数filedes共享同一个文件表项(即返回的文件描述符与原来的文件描述符共用同一个文件表项)

复制一个描述符的另一种方法是使用fcntl函数。

dup(filedes)     等效于  fcntl(filedes,F_DUPFD,0);

dup2(filedes,filedes2);  等效于  close(filedes2);   fcntl(filedes,F_DUPFD,filedes2);

 

3.13 sync、fsync和fdatasync函数

这三个函数,主要是为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性。

#include<unistd>int fsync(int filedes);int fdatasync(int filedes);void sync(void);                        返回值:若成功则返回0,若出错则返回-1

 

 sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束

(通常称为update的系统守护进程会周期性地(一般每隔30秒)调用那个sync函数。这就保证了定期冲洗内核的块缓冲区。)

fsync函数只对由文件描述符filedes指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。(fsync可用于数据库这样的应用程序,这种应用程序需要确保将修改过的块立即写到磁盘上)

fdatasync函数类似于fsync,但它值影响文件的数据部分。(而除数据外,fsync还会同步更新文件属性)

 

3.14 fcntl函数

fcntl函数可以改变已打开文件的性质

#include<fcntl.h>int fcntl(int filedes,int cmd,.../*int arg*/);                    返回值:若成功则依赖于cmd,若出错则返回-1

fcntl函数有5种功能:

(1)复制一个现有的描述符(cmd=F_DUPFD)

(2)获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。

(3)获得/设置文件状态标志(cmd=F_GETFL或F_SETFL)。

(4)获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).

(5)获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).

F_DUPFD   复制文件描述符filedes。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第三个参数值中各值的最小值。

F_GETFD   对应于filedes的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC.

F_SETFD   对于filedes设置文件描述符标志。新标志值按第三个参数(取为整数)设置

F_GETFL    对应于filedes的文件状态标志作为函数值返回。

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

实例:3_4 对于指定的描述符打印文件标志

 1 #include"apue.h" 2 #include<fcntl.h> 3 int main(int argc,char *argv[]) 4 { 5     int val; 6     if(argc!=2) 7     err_quit("usage:a.out < descriptor#>"); 8     if((val=fcntl(atoi(argv[1]),F_GETFL,0))<0) 9     err_sys("fcntl error for fd %d",atoi(argv[1]));    10     switch(val & O_ACCMODE){   //三个访问标志位并不各占一位,因此首先必须用屏蔽字O_ACCMODE取得访问模式位11     case O_RDONLY:12         printf("read only");13         break;14     case O_WRONLY:15         printf("write only");16         break;17     case O_RDWR:18         printf("read write");19         break;20     default:21         err_dump("unknown access mode");22     }23     if(val & O_APPEND)24         printf(", append");25     if(val & O_NONBLOCK)26         printf(", nonblocking");27     #if defined(O_SYNC)28         if(val & O_SYNC)29         printf(", synchronous writes");30     #endif31     #if !defined(_POSIX_C_SOURCE)&&defined(O_FSYNC)32         if(val & O_FSYNC)33         printf(", synchronous writes");34     #endif35         putchar(\n);36         exit(0);37 }

 

实例:

在修改文件描述符标志位或文件状态标志时必须谨慎,先要取得现有标志值,然后根据需要修改它,最后设置信标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。

程序3_5 对一个文件描述符打开一个或多个文件状态标志

 1 #include"apue.h" 2 #include<fcntl.h> 3 void set_fl(int fd,int flag) 4 { 5     int val; 6     if((val=fcntl(fd,F_GETFL,0))<0) 7         err_sys("fcntl F_GETFL error"); 8     val |=flags;     //turn on flags 9     if(fcntl(fd,F_SETFL,val)<0)10         err_sys("fcntl F_SETFL error");11 }

如果将中间的一条语句改为:val &= ~flags; //turn flags off

就构成另一个函数,我们称其为clr_fl。

如果在程序开始处,加上下面一行调用set_fl,则打开了同步写标志:set_fl(STDOUT_FILENO,O_SYNC);

这就使每次write都要等待,直至数据已写到磁盘上再返回。

程序运行时,设置O_SYNC标志会增加时钟时间。在写磁盘时,系统时间增加了,其原因是内核需要从进程中复制数据,并将数据排入队列以便由磁盘驱动器将其写到磁盘上。当写至磁盘文件时,我们期望时钟时间也会增加。

 

3.15 ioctl函数

ioctl函数是I/O函数的杂物箱。

#include<unistd.h>#include<sys/ioctl.h>#include<stropts.h>int ioctl(int filedes ,int request,...);                    返回值:若出错则返回-1,若成功则返回其他值

每个设备驱动程序都可以定义它自己专用的一组ioctl命令。系统则为不同种类的设备提供通用的ioctl命令

 

3.16 /dev/fd

较新的系统都提供名为/dev/fd的目录,其目录项是名为0,1,2等的文件。打开文件/dev/fd/n等效于复制描述符n

某些系统提供路径名/dev/stdin,/dev/stdout,/dev/stderr,这些等效于/dev/fd/0,/dev/fd/1,/dev/fd/2

/dev/fd文件主要由shell使用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同方式处理标准输入和输出。