3.1 引言
本章讨论文件IO——打开、读写、关闭文件等,且都是不带缓冲IO。包括:open、read、write、lseek、close
后面将讨论在如何多个进程之间共享文件,以及dup、fcntl、sync、fsync和ioctl
函数
3.2 文件描述符
对于内核而言,所有打开的文件都通过文件描述符
标识,并返回给进程。当读写一个文件时,使用open
或create
返回的文件描述符标识该文件,将其作为参数传递给read
或write
.
文件描述符的变化范围是0~OPEN_MAX-1,现在系统的上限增加至63,即允许每个进程打开64个文件。
3.3 函数open和openat
调用open或openat可以打开或创建一个文件。
#include <fcntl.h>
int open(const char *path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);
最后一个参数是...
,说明余下的参数的数量和类型是可变的。对于open
函数而言,仅当创建新文件时才会使用这个参数。
path
参数是要打开或创建文件的名字,oflag
参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag
参数,这些参数在<fcntl.h>
中定义。
O_RDONLY
只读打开O_WRONLY
只写打开O_RDWR
读写打开O_EXEC
只执行打开O_SEARCH
只搜索打开
以上5个常量必须制定且只能指定一个。下列常量则是可选的。
O_APPEND
每次写时都追加到文件末尾O_CLOEXEC
把FD_CLOEXEC
常量设置为文件描述符标志O_CRAET
若文件不存在则创建它。使用此选项时open
函数需要说明第三个参数mode
,用于指定该文件的访问权限位,openat
函数则需要说明第四个参数mode
O_DIRECTORY
如果path引用的不是目录,则出错O_EXCL
如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使得创建和测试成为一个原子操作。O_NONBLOCK
如果path引用的是一个FIFIO,一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续IO操作设置为非阻塞方式O_SYNC
同步模式,使每次write等待物理IO操作完成,包括由该write操作引起的文件属性更新所需的IOO_TRUNC
如果此文件存在,而且为只写或读写成功打开,则将其长度截断为0
参数fd
把open
和openat
区分开,共有三种可能性。
(1)path参数指定的是相对路径名,在这种情况下,fd
参数可以被忽略,openat
函数就相当于open
函数。
(2)path参数指定的是相对路径名,fd
参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。
(3)path参数指定的是相对路径名,fd
参数具有特殊值AT_FDCWD. 这时路径名在当前工作目录中获取,openat
函数在操作上与open
函数类似。
3.3.1 文件名和路径名截断
如果NAME_MAX
是14,而试图创建一个包含15个字符的文件时,有些系统会将名字截断为14,而不报任何错误。有些系统则将errno
设置为为ENAMETOOLONG
.
我们可以使用fpathconf或者pathconf来查询目录具体支持何种行为,到底是截断还是返回错误。
若_POSIX_NO_TRUNC
有效,则在整个路径名超过PATH_MAX
,或路径名中任一文件名超过NAME_MAX
时,出错返回,并将errno
设置为ENAMETOOLONG
3.4 函数creat
也可以使用creat
函数创建一个新文件
#include <fcntl.h>
int creat(const char* path, mode_t mode)
此函数等效于:
open(path, O_WRONLY|O_CREAT|O_TRUNC, mode);
但是creat只能以只写
的方式打开所创建的文件,如果要创建一个文件并要读写,则必须先调用creat、close,然后再open.
在4.5节我们将详细说明如何指定mode
.
3.5 close
关闭一个打开的文件
#include <unistd.h>
int close (int fd);
返回值:成功返回0,失败返回-1
fd
为先前由open
()或creat
()所返回的文件描述符,进程终止时,内核自动关闭它所有打开的文件
3.6 lseek
使用’文件偏移量’来表示打开文件从起始处到当前位置的字节数。读写操作都从当前文件偏移量位置处开始,并使偏移量增加读写的字节数。
打开一个文件时,除非使用O_APPEND
打开,否则偏移量被设置为0.
可以使用lseek
函数为打开的文件设置一个偏移量.
#include <unistd.h>
off_t lseek(int fd, off_t offset,int whence);
whence
参数:
SEEK_SET
,则将文件偏移量设置为offset
SEEK_CUR
,则将文件偏移量+offset
,offset
可正可负SEEK_END
,将文件偏移量设置为文件长度+offset
,offset
可正可负
允许设置文件偏移量大于文件当前长度,这会创建一个空洞区,被读为0,但不占据磁盘空间.
可用下列方式确定当前偏移量:
off_t offset = lseek(fd, 0, SEEK_CUR)
可用下列方式判断是否可以设置偏移量:
if(lseek(fd, 0, SEEK_CUR) < -1)
printf("can't seek\n");
3.7 read
读操作会使文件偏移加上读到的字节数
返回值:实际读到的字节数,到达文件尾返回0,出错返回-1
入参:
fd: 文件描述符
buf: 存放的地址
nbytes: 设置读取的字节数
#include <unistd.h>
ssize read(int fd, void *buf, size_t nbytes);
3.8 write
返回值:写的字节数,通常与nbytes相同,出错返回-1
入参:
fd: 文件描述符
buf: 内容的地址
nbytes: 设置要写字节数
#include <unistd.h>
ssize write(int fd, void *buf, size_t nbytes);
3.9 IO效率
当将缓冲区BUFSIZE设置的太小时,会由于循环次数太多,使得读写时间过长。
但实验发现缓冲区大小设置为32字节和65536字节的差距并不大,这是因为文件系统的预读技术,当发现正顺序读取时,会读取比应用要求要更大的数据.
3.10 文件共享
UNIX操作系统支持支持不同进程之间共享打开文件,机制如下:
- 每个进程维护一个进程表项列表,一个进程在文件描述符3上打开该文件,另一个进程在文件描述符4上打开该文件。
- 内核为每个进程打开该文件的一个文件表项,
- 文件表项包括当前偏移量、当前状态和 v 节点指针,指向 v 节点表项
- v 节点包括文件类型和对该文件进行各种操作的函数,并指向 i 节点
- i 节点包括文件所有者、长度、磁盘位置.
当多个进程同时写一个文件时,可能产生预想不到的结果,这需要原子操作.
3.11 原子操作
1. 无原子操作的问题
当进程A和B对同一文件同时进行追加写操作,但不是使用的O_APPEND标志
- 进程A调用lssek函数,将文件偏移量设置为1500字节
- 内核切换进程B
- 进程B调用lseek函数,并执行了write函数写了100字节,内核将i节点中的当前文件长度设置为1600.
- 内核切换回进程A
- 从文件偏移量1500处开始写,将覆盖进程B所写的内容.
2. 函数pread和pwrite
#include <unistd.h>
返回值:读到的字节数,若到达文件尾返回0,出错返回-1
入参:
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
返回值:已写的字节数,出错返回-1
入参:
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
调用pread
相当于调用lseek
后调用read
,区别是:
- 调用pread时,内核无法中断其定位和读操作
- 不更新当前文件偏移量
3. 创建一个文件
对open函数同时指定O_CREAT
和O_EXCL
时,而该文件已经存在时open将会失败,这个操作是作为一个原子操作执行的,如果没有这样的原子操作,将会编写代码如下:
/* 首先尝试打开,若不存在才创建 */
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>
返回值: 当前所有描述符中的最小值
入参: 要复制的文件描述符
作用: 使返回值文件描述符 = fd
int dup(int fd);
返回值:
入参:
fd:要复制的文件描述符
fd2:指定新描述符的值
int dup2(int fd, int fd2);
如果fd2
已经打开,则现将其关闭,如果fd = fd2
,则dup2
返回fd2
,而不关闭它.
复制文件描述符不单单是复制一个数值,而同时复制文件描述符对应的指向文件表的指针,如下图
3.13 sync、fsync和fdatasync
内核需要将缓冲区中的延迟写
数据写入磁盘,为了保证磁盘上实际文件系统与缓冲区内容的一致性,UNIX提供了这三个函数.
#include <unistd.h>
void sync(void); //将块缓冲区排入队列就返回,不等待实际写操作结束
int fsync(int fd); //只对由文件描述符fd指定的一个文件起作用,并且等待磁盘操作结束才返回
int fdatasync(int fd); //类似于fsync, 但只对文件的数据部分起作用,fsync还会同步更新文件属性
返回值:成功返回 0, 出错返回 -1
3.14 fctnl
作用:获取和设置打开文件的属性
#include <fcntl.h>
//第三个参数一般是一个整数,但也可能是一个指向结构体的指针
int fcntl(int fd, int cmd, .../int arg * /);
返回值:若成功返回值依赖于cmd参数; 若出错则返回-1
序号 | cmd | 作用 |
---|---|---|
1 | F_DUPFD或F_DUPFD_CLOEXEC | 复制一个已有的描述符 |
2 | F_GETFD或F_SETFD | 获取/设置文件描述符标志 |
3 | F_GETFL或F_SETFL | 获取/设置文件状态标志 |
4 | F_GETOWN或F_SETOWN | 获取/设置异步IO所有权 |
5 | F_GET_LK、F_SETLK或F_SETLKW | 获取/设置记录锁 |
F_DUPFD
:复制文件描述符fd
,新文件描述符作为函数值返回。他是尚未打开的各描述符中大于等于第三个参数值中各值的最小值。与fd
共享同一个文件表项,但是新文件描述符有自己的文件描述符标志
。但是其文件描述符FD_CLOEXEC
标志被清除,这表示该描述符在exec时仍保持有效。FD_DUPFD_CLOEXEC
:复制文件描述符,新文件描述符关联着文件描述符标志FD_CLOEXEC
FD_GETFD
:返回文件描述符
fd对应的文件描述符标志
,当前只定义了一个FD_CLOEXEC
FD_SETFD
:对于fd设置文件描述符标志
。新标志按第三个参数设置。F_GETFL
:返回文件状态标志
,包括:
但是(O_RDONLY
,O_WRONLY
,O_RDWR
,O_EXEC
, andO_SEARCH
)并不各占一位,而互斥的。因此也必须使用屏蔽字O_ACCMODE
取得访问方式位。然后将结果与这5个值中的每一个相比较
7.F_SETFL
:将文件状态标志设置为第3个参数的值,可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC.
8.F_GETOWN
:获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID。
9. F_SETOWN
:设置接收SIGIO
和SIGURG
信号的进程ID或进程组ID。正的arg
指定一个进程ID,负的arg
表示等于arg绝对值的一个进程组ID。
3.14.1 实例
实例1
/* 打印文件状态标志,不是文件描述符标志 */
#include "apue.h"
#include <fcntl.h>
int main()
{
int val = 1;
if(argc != 2)
err_quit("Usage: a.out <descriptor#>");
if((val = fcntl(atoi(argv[1]), F_GETFL, 0))<0)
err_sys("fcntl error for fd: %d", atoi(argv[1]));
switch(val & O_ACCMODE){//O_ACCMODE可以把后面其他属性对应位全0,来获得这一位的值
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
printf("write only");
break;
case O_RDWR:
printf(" read write");
break;
default:
err_dump("unknown access mode");
}
if(val & O_APPEND)
printf(",append");
if(val & O_NONBLOCK)
printf(",nonblocking");
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
if(val & O_FSYNC)
printf(", syncchoronous writes");
#endif
putchar('\n');
exit(0);
}
使用了功能测试宏,POSIX_C_SOURCE,并且条件编译了POSIX.1中没有定义的文件访问标志。
实例2
在修改文件描述符标志
或文件状态标志
时必须谨慎,先获取标志然后再设置标志值,直接执行F_SETFD
或F_SETFL
会冲掉以前的标志值。
#include <apue.h>
#include <fcntl.h>
void set_fl(int fd, int flags){
int val;
if((val = fcntl(fd, F_GETFL, 0)) < 0)
err_sys("fcntl F_GETFL error");
val |= flags;//在原标志位上添加新标志
if(fcntl(fd, F_SETFL, val) < 0)
err_sys("fcntl F_SETFL error");
}
3.15 ioctl
这个函数是IO操作的杂物箱,用其他IO操作不能完成的操作都用ioctl
完成,终端IO是使用最多的。其他的包括,磁带、套接字、文件、盘标号的IO操作。
#include <unistd.h>
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...)
而且还需要一个另外的设备专用头文件。例如下表:
以第二个表为准,对着第一个看翻译。
3.16 /dev/fd
较新的系统都提供名为 /dev/fd 的目录,其目录项是名为 0、1、2等的文件。打开文件/dev/fd/n等效于复制描述符n (假定描述符n是打开的)。
fd = open("dev/fd/0", mode)
等效于fd = dup(0);
,所以描述符0和fd共享同一文件表项。例如描述符0先前被打开为只读,则fd也只能进行读操作