系统编程之文件I/O

 

目录


1.文件描述符

2.文件共享

3.原子操作

4.函数open和openat

5.函数creat

6.函数close

7.函数lseek

8.函数read

9.函数write

10.函数dup和dup2

11.函数sync、fsync、fdatasync

12.函数fcntl

13.函数ioctl


文件描述符

        对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。 

        按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量都在头文件<unistd.h>中定义。


 文件共享

         内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
1.每个进程在进程表(每个进程一张表)中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,进程表的索引就是文件描述符。与每个文件描述符相关联的是:
a.文件描述符标志;
b.指向一个文件表项的指针。

2.内核为所有打开文件维持一张文件表(所有进程共享)。每个文件表项包含:
a.文件状态标志(读、写、添写、同步和非阻塞等);
b.当前文件偏移量;
c.指向该文件v节点表项的指针。

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

        下图就是打开文件的内核数据结构。 

fdca76505ec64cf4b1cdc354b563d7c5.png

        如果两个独立进程各自打开了同一文件,则有下图中所示的关系。第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。

1684cc751b4c4c64b621c1b87a38561f.png

        可能有多个文件描述符项指向同一文件表项。在使用dup 函数时,就能看到这一点。在fork后也发生同样的情况,此时父进程、子进程各自的每一个打开文件描述符共享同一个文件表项。

135ace2c6df44d66a44447feb44f8322.png

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

原子操作

 观察下面这段代码

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");

        对单个进程而言,这段程序能正常工作,但若有多个进程同时使用这种方法将数据追加写到同一文件,则会产生问题。

        假定有两个独立的进程A和B都对同一文件进行追加写操作。每个进程都已打开了该文件,但未使用O_APPEND标志。每个进程都有它自己的文件表项,但是共享一个v节点表项。假定进程A调用了lseek,它将进程A的该文件当前偏移量设置为1 500字节(当前文件尾端处)。然后内核切换进程,进程B运行。进程B执行lseek,也将其对该文件的当前偏移量设置为1 500字节(当前文件尾端处)。然后B调用write,它将 B的该文件当前文件偏移量增加至1 600。因为该文件的长度已经增加了,所以内核将v节点中的当前文件长度更新为1 600。然后,内核又进行进程切换,使进程A恢复运行。当A调用write时,就从其当前文件偏移量(1 500)处开始将数据写入到文件。这样也就覆盖了进程B刚才写入到该文件中的数据。
        问题出在逻辑操作“先定位到文件尾端,然后写”,它使用了两个分开的函数调用。解决问题的方法是使这两个操作对于其他进程而言成为一个原子操作。任何要求多于一个函数调用的操作都不是原子操作,因为在两个函数调用之间,内核有可能会临时挂起进程。
        UNIX系统为这样的操作提供了一种原子操作方法,即在打开文件时设置O_APPEND标志。这样做使得内核在每次写操作之前,都将进程的当前偏移量设置到该文件的尾端处,于是在每次写之前就不再需要调用lseek。Single UNIX Specification包括了XSI扩展,该扩展允许原子性地定位并执行I/O。pread和pwrite就是这种扩展。

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

(1)调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用有下列重要区别。
•调用pread时,无法中断其定位和读操作。
•不更新当前文件偏移量。
(2)调用pwrite相当于调用lseek后调用write,但也与它们有类似的区别。

        一般而言,原子操作指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。在学习link函数以及记录锁时,还将用到原子操作。


 函数open和openat

#include <fcntl.h>
int open(const char *path, int oflag,... /* mode_t mode */);
int openat(int f d, const char *path, int oflag, ... /* mode_t mode */ );
//两函数的返回值:若成功,返回文件描述符;若出错,返回−1

        path参数是要打开或创建文件的名字。

        oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数(这些常量在头文件<fcntl.h>中定义)。
O_RDONLY:只读打开。(定义为0)
O_WRONLY:只写打开。(定义为1)
O_RDWR:读、写打开。(定义为2)
O_EXEC:只执行打开。
O_SEARCH:只搜索打开(应用于目录)。
        O_SEARCH常量的目的在于在目录打开时验证它的搜索权限。对目录的文件描述符的后续操作就不需要再次检查对该目录的搜索权限。以上这5个常量中必须指定一个且只能指定一个(这5个常量互斥)。

        下列常量则是可选的。
O_APPEND:每次写时都追加到文件的尾端。
O_CLOEXEC:把FD_CLOEXEC常量设置为文件描述符标志。
O_CREAT:若此文件不存在则创建它。使用此选项时,open函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新文件的访问权限位。
O_DIRECTORY:如果path引用的不是目录,则出错。
O_EXCL:如果同时指定了 O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作。
O_NOCTTY:如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端。
O_NOFOLLOW:如果path引用的是一个符号链接,则出错。
O_NONBLOCK:如果path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式。

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

O_TRUNC:如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0。
O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标准 termios 参数值,使其符合Single UNIX Specification。
O_DSYNC:使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。
O_RSYNC:使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。

O_DSYNC 和 O_SYNC 标志有微妙的区别。仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多的数据)时,O_DSYNC标志才影响文件属性。而设置O_SYNC标志后,数据和属性总是同步更新。当文件用O_DSYN标志打开,在重写其现有的部分内容时,文件时间属性不会同步更新。与此相反,如果文件是用O_SYNC标志打开,那么对该文件的每一次write都将在write返回前更新文件时间,这与是否改写现有字节或追加写文件无关。

        由open和openat函数返回的文件描述符一定是最小的未用描述符数值。

        fd参数把open和openat函数区分开,共有3种可能性。
(1)path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。
(2)path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。
(3)path参数指定的是相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。
 


函数creat

#include <fcntl.h>
int creat(const char *path, mode_t mode);
//返回值:若成功,返回为只写打开的文件描述符;若出错,返回−1
//注意,此函数等效于:
open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);

        creat的一个不足之处是它以只写方式打开所创建的文件。在提供open的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用creat、close,然后再调用open。现在则可用下列方式调用open实现:

int open(path, O_RDWR|O_CREAT|O_TRUNC, mode);


函数close

#include <unistd.h>
int close (int fd);
//返回值:若成功,返回0;若出错,返回−1


函数lseek

        每个打开文件都有一个与其相关联的“当前文件偏移量”。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
        可以调用lseek显式地为一个打开文件设置偏移量。

#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
//返回值:若成功,返回新的文件偏移量;若出错,返回为−1

对参数offset的解释与参数whence的值有关。
•若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
•若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。
•若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
        lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。
        文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。文件中的空洞并不要求在磁盘上占用存储区。具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
        如下程序用于创建一个具有空洞的文件。

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>

#define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)
char buf1[]="abcdefghij";
char buf2[]="ABCDEFGHIJ";

int main(void)
{
    int fd;

    if((fd=creat("file.hold",FILE_MODE))<0)//offset=0
        printf("creat file error\n");
    if(write(fd,buf1,10)!=10)//offset=10
        printf("buf1 write error\n");
    if(lseek(fd,100,SEEK_SET)==-1)//offset=100
        printf("lseek erroe\n");
    if(write(fd,buf2,10)!=10)//offset=110
        printf("write error\n");
    exit(0);
}

运行结果如下:

8bf98da931db49c8976b708a1a3ae1cb.png

         使用od命令观察该文件的实际内容。命令行中的-c标志表示以字符方式打印文件内容。从中可以看到,文件中间未写入字节都被读成0。每一行开始的一个7位数是以八进制形式表示的字节偏移量。


函数read

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
//返回值:读到的字节数,若已到文件尾,返回0;若出错,返回−1

有多种情况可使实际读到的字节数少于要求读的字节数:
•读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前有30个字节,而要求读100个字节,则read返回30。下一次再调用read时,它将返回0(文件尾端)。
•当从终端设备读时,通常一次最多读一行(第18章将介绍如何改变这一点)。
•当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
•当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
•当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
•当一信号造成中断,而已经读了部分数据量时,以后讨论此种情况。

        读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。POSIX.1从几个方面对read函数的原型做了更改。经典的原型定义是:

int read(int fd, char *buf, unsigned nbytes);


 


函数write

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

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


函数dup和dup2

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

#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);//fd是oldfd,fd2是newfd
//两函数的返回值:若成功,返回新的文件描述符;若出错,返回−1

        由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于 dup2,可以用fd2参数指定新描述符的值。若参数fd2已经被程序使用,则系统就会将fd2所指的文件关闭。如若fd等于fd2,则dup2返回fd2,而不关闭它。否则,fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。

135ace2c6df44d66a44447feb44f8322.png

函数sync、fsync、fdatasync

        传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写。

        通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX 系统提供了 sync、fsync 和fdatasync三个函数。

#include<unistd.h>
int fsync(int fd);
int fdatasync(int fd);
//返回值:若成功,返回0;若出错,返回−1
void sync(void);

        fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
        fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
        sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
 


函数fcntl

        fcntl函数可以改变已经打开文件的属性。

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

fcntl函数有以下5种功能。
(1)复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)。
(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)。
第五终功能暂时先不讨论。

关于文件状态标志,在open函数中已经讨论过,这里再简单概括总结一下。

 下面介绍一下文件描述符标志

 当前只定义了一个文件描述符标志FDCLOEXEC
0: exec时不关闭已经打开的文件描述符
1: exec时关闭已经打开的文件描述符

接下来是fcntl函数有关的命令,命令的设置对应相应的功能。

F_DUPFD:复制文件描述符fd。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第3个参数值(取为整型值)中各值的最小值。新描述符与 fd共享同一文件表项。但是,新描述符有它自己的一套文件描述符标志,其 FD_CLOEXEC 文件描述符标志被清除(这表示该描述符在exec时仍保持有效)。
F_DUPFD_CLOEXEC: 复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符。
F_GETFD 对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC。
F_SETFD: 对于fd设置文件描述符标志。新标志值按第3个参数(取为整型值)设置。
F_GETFL: 对应于fd的文件状态标志作为函数值返回。我们在说明open函数时,已描述了文件状态标志。

F_SETFL:将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC。
F_GETOWN:获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID。14.5.2节将论述这两种异步I/O信号。
F_SETOWN:设置接收SIGIO和SIGURG信号的进程ID或进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID。

        在修改文件描述符标志或文件状态标志时必须谨慎,先要获得现在的标志值,然后按照期望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。下图是对于一个文件描述符设置一个或多个文件状态标志的函数。
 

 


函数ioctl

        ioctl函数一直是I/O操作的杂物箱。不能用本章中其他函数表示的I/O操作通常都能用ioctl表示。终端I/O是使用ioctl最多的地方

#include <unistd.h>  /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
int ioctl(int fd, int request, ...);
//返回值:若出错,返回−1;若成功,返回其他值

        每个设备驱动程序可以定义它自己专用的一组 ioctl 命令,系统则为不同种类的设备提供通用的ioctl命令。下图中总结了FreeBSD支持的通用ioctl命令的一些类别。

        磁带操作使我们可以在磁带上写一个文件结束标志、倒带、越过指定个数的文件或记录等,用本章中的其他函数(read、write、lseek 等)都难于表示这些操作,所以,对这些设备进行操作最容易的方法就是使用ioctl。
 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值