《UNIX环境高级编程》第3章 文件I/O

文件I/O

3.1 引言

本章开始讨论UNIX系统,先说明可用的文件IO函数——打开文件、读文件、写文件等。大多数文件IO只要用到5个函数:open、read、write、lseek和close。
本章描述的函数经常被称为不带缓冲的I/O(unbuffered IO,区别于标准IO,stdio)。术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。这些不带缓冲的IO不是ISO C的组成部分,却是POSIX 和SUS的组成部分。
只要涉及在多个进程间贡献资源,原子操作的概念就变得非常重要。我们将讨论这部分内容,并且讨论如何在多个进程中共享文件。

3.2 文件描述符

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

3.3 函数open和openat

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

#include <fcnt1.h>
int open(const char *path,int oflag,...);
int openat(int fd,const char *path,int oflag,...);  
//成功返回文件描述符,错误返回-1

最后一个参数…说明函数是可变参函数,对open而言,只有在创建新文件时才使用最后这个参数
path参数时要打开或创建文件的名字。
oflag参数可以用来说明此函数的多个选项;用下列的多个常量进行“或”运算构成oflag参数(定义在<fcnt1.h>中)。

定义
O_RDONLY只读打开;
O_WRONLY只写打开;
O_RDWR读写打开;
O_EXEC只执行打开;
O_SEARCH只搜索打开(应用于目录);

以上常量中必须只能指定一个。下列常量则是可选的。

定义
O_APPEND每次写时都追加到尾端;
O_CLOEXEC。。。;
O_CREATE若此文件不存在则创建它。此时要使用第三个参数来说明文件的权限;
O_DIRECTORY若干path不是目录,则出错;
O_EXCL若同时指定了O_CREATE, 而文件已经存在,则出错。可以用来测试一个文件是否存在,如果不存在则创建,这使测试和创建成为一个原子操作。
O_NOCTTY如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端;
O_NOFOLLOW如果path引用的是一个符号链接,这出错。
O_NONBLOCK??太啰嗦一遍没看懂??
O_SYNC使每次write等待物理I/O才操作完成,包括由该write操作引起的文件属性更新所需要的I/O
O_TRUNC如果此文件存在,而且为只写或读写成功打开,则将其长度截断为0;
O_TTY_INIT??太啰嗦一遍没看懂??
O_DSYNC??太啰嗦一遍没看懂??
O_RSYNC??太啰嗦一遍没看懂??

fd参数把open和openat函数区分开,共有3种可能性:
(1)path参数是绝对路径名,在这种情况下,fd参数被忽略,openat就相当于open;
(2)path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。(意思是先通过open获得一个目录的文件描述符fd,然后使用这个文件描述符作为openat的第一个参数,这时候path的路径就是相对于这个fd的。
(3)path参数指定的是相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取。

3.4 函数create

也可以使用create函数创建一个新文件。

#include <fcnt1.h>
int crate(const char *path,mode_t mode);

此函数等效于:

open(const char *path,O_WRONLY|O_CREATE|O_TRUNC,mode);

早起的UNIX中open的第二个参数只能是0/1/2也就是不能打开一个不存在的文件,所有要先创建在打开,现在有了O_CREATE,也就不再单独使用create函数了。

3.5 函数close

可以用close函数关闭一个打开文件。

#include <unistd.h>
int close(int fd);

关闭一个文件时还会释放改进程加在该文件上的所有记录锁。
当进程终止时,内核自动关闭它所有的打开文件。很多程序都利用这一点而不显式地用close关闭打开的文件。

3.6 函数lseek

每个打开文件都有一个与其相关的“当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算字节数。通常,读、写操作从当前文件偏移量处开始,并是偏移量增加所读写的字节数。按系统默认情况,当打开一个文件时,除非指定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_CUR,则将该文件的偏移量设置为文件长度加offset,offset可正可负。

3.7 函数read

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

#include <unistd.h>
ssize_t read(int fd,void *buf,size_t nbytes);//成功返回读到的字节数,失败返回-1

3.8 函数write

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

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

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

3.9 IO的效率

大多数文件系统为改善性能都采用某种预读(read ahead)技术。当检测到正进行顺序读取时,系统就试图读取更多的数据,并假想会很快用到这些数据。
(这里原书上给出了特定buffer下IO读写效率。)

3.10 文件共享

UNIX系统支持在不同的进程间共享打开的文件。在介绍dup函数之前,先要说明这种共享行为。为此先介绍内核用于所有IO的数据结构。
内核用3种数据结构表示打开文件,3种数据结构之间的关系决定了文件共享时进程间可能的相互影响。
(1))每个进程在进程表中都有一个记录项***(进程表项)***,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个描述符相关联的是:
a.文件描述符标志(close_on_exec)
b.指向一个文件表项的指针。
(2)内核为所有打开文件维持一张文件表***(文件表项)***,每个文件表项包含:
a.文件状态标志(读、写、添加、同步、非阻塞等)
b.当前文件偏移量;
c.指向该文件v节点表项的指针;
(3)每个打开文件(或设备)都有一个v(virtual的意思visual file
system)节点(v-node)结构***(v节点表项)***。v节点包含了文件类型和对此文件进行各种操作的函数指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。

这里写图片描述
我们假定第一个进程在文件描述符3上打开该文件,另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为可以使每个进程都有它自己的对该文件的当前偏移量。
现在对前面所描述的操作做进一步说明。
*在完成每个write后,在文件表项的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件的长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了)。(阅注:就是说,文件表项起到了记录各个进程打开的文件偏移量的作用,而vnode表项记录了文件的大小,在vnode表项与进程表项中间加的一层文件表项,是为进程间文件共享服务的)
*如果O_APPEND标志打开一个文件,则相应标志 也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度,这就使得每次写入的数据都追加到文件的当前尾端处。
*若一个文件使用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。
*lseek函数值修改文件表项中的当前文件偏移量,不进行任何IO操作。
可能有多个文件描述符指向同一文件表项。在讨论dup函数是会看到这一点,在fork后也会发生同样的情况,此时父进程、子进程各自打开的每一个文件描述符共享同一个文件表项。
前面说描述的一切,对多个进程读取同一文件都能正常工作,每个进程都有自己的文件表项,其中包含了自己的当前文件偏移量。但是,当多个线程写同一个文件时,则可能产生预想不到的结果。为了避免这种情况,需要理解原子操作的概念。


读到此处发现作者上下文衔接的非常好,重点在理解3个表项(进程表项、文件表项、v节点表项)的作用;

3.11 原子操作

1.追加到一个文件
当一个进程打开一个文件并向文件尾端添加数据时,每个进程都有独立的文件表项,但是若没有使用open函数的O_APPEND选项,则每次向文件尾添加数据的操作都会如下执行:

if(lseek(fileid,0,SEEK_END)<0)
    err_sys("lseek error");
if(write(fileid,buf,100)!=100)
    err_sys("write error");

若同时有多个进程在执行这一操作,则会出现竞争问题。因为在执行lseek和write函数之间OS可能会切换上下文;
UNIX系统为这样的操作提供了一种原子操作方法,即在打开文件时设置O_APPEND标志,这样做使得内核在每次写操作之前都将进程的当前偏移量设置到该文件的尾端,于是在每次写之前就不再需要调用lseek。

2.函数pread和pwrite
SUS的扩展允许原子性的定位并执行IO。

#include <unistd.h>
ssizez_t pread(int fd,void *buf,size_t nbytes,off_t offset);
ssizez_t pwrite(int fd,void *buf,size_t nbytes,off_t offset);

调用pread相当于调用lseek后调用read,但是又有以下重要区别:
*调用pread时,无法中断其定位和读操作。
*不更新当前文件偏移量。(why????)
调用pwrite时类似;

3.创建一个文件
对open函数的O_CREAT和O_EXCL选项进行说明时,我们已见到另一个有关原子操作的例子。当同时指定这两个选项,而该文件已经存在时,open将失败。检查是否存在和创建文件这两个操作是作为一个原子操作执行的。

3.12 函数dup和dup2

下面两个函数都可以用来复制一个现有的文件描述符。
dup和dup2的作用都是用来复制一个文件的描述符。它们经常用来重定向进程的stdin、stdout和stderr。

#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd,int newfd);

由dup返回的新文件描述符一定是当前可用描述符中的最小值。
dup2函数可用newfd指定新的描述符值。
返回的新描述符与就描述符共享一个文件表项,如图:
这里写图片描述

3.13 函数sync 、fsync和fdatasync

传统的UNIX系统实现,在内核中设有缓冲区告诉缓冲或页高速缓存,大多数磁盘IO都通过缓冲区进行。当我们向文件系统写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延时写(delayed write)。
通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延时写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。

#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void):

sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。通常,updata系统守护进程30秒调用一次sync函数,这样就保证了定期冲洗(flush)内核的块缓冲区。
fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束f才返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而fsync还会同步更新文件的属性。

3.14 函数fcntl

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

#include<fcntl.h>
int fcntl(int fd,int cmd,.../*int arg*/);

fcntl函数有以下5种功能。
(1)复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)
(2)获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD)
(3)获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)
(4)获取/设置异步IO所有权(cmd=F_GETOWN或F_SETOWN)
(5)获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)
接下来讨论进程表项中的文件描述符标志文件表项中的文件状态标志

cmd参数定义解释
F_DUPFD复制文件描述符fd。新文件描述符作为函数值返回。新文件描述符与fd共享同一文件表项,但是新文件描述符有它自己的文件描述符标志,其FD_CLOEXEC文件描述符标志被清除(这表示该描述符在exec时仍保持有效)FD_CLOEXEC文件描述符标志=1时,exec会关闭fd,FD_CLOEXEC=0时会保留fd
F_DUPFD_CLOEXEC复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符
F_GETFD返回当前的文件描述符标志位,当前只有一个文件描述符标志位FD_CLOEXEC
F_SETFD设置文件描述符标志位很多程序都不用FD_CLOEXEC,而是将此标志位设置为0(exec时不关闭)或1(exec时关闭)
F_GETFL返回对应fd的文件状态标志。如O_RDONLY,O_WRONLY,O_RDWR等。由于这些常量并不各占一位,因此必须使用屏蔽字O_ACCMODE取得访问方式位,然后再比较结果
F_SETFL设置文件状态标志位
F_GETOWN
F_SETOWN

3.15 函数ioctl

ioctl函数一直是IO操作的杂物箱,不能用本章的其他函数表示的IO操作通常都能用ioctl表示。

int ioctl(int fd ,int request,...);

每个设备驱动程序可以订阅它之间专用的一组ioctl命令,系统则为不同种类的设备提供通用的ioctl命令。

3.16 /dev/fd

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

fd=open("/dev/fd/0",mode);
//等效于以下
fd=dum(0);
//此时fd和0共享一个文件表项

3.17 小结

1.本章说明了UNIX系统提供的基本IO函数。因为read和write都在内核执行,所以称这些函数为不带缓冲的IO函数。
2.在只能使用read和write的情况下,我们观察了不同IO长度对读文件所需时间的影响。
3.也讨论了不同的将已写入的数据冲洗到磁盘上的方法,以及它们对应用程序性能的影响。
4.介绍了内核用来共享打开文件信息的数据结构。
5.在说明多个进程对同一文件进行追加写操作以及多个进程创建同一文件时,所采用的原子操作。
6.介绍了fcntl和ioctl函数。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值