3、文件IO

3.1 引言

  本章讨论文件IO——打开、读写、关闭文件等,且都是不带缓冲IO。包括:open、read、write、lseek、close
  后面将讨论在如何多个进程之间共享文件,以及dup、fcntl、sync、fsync和ioctl函数

3.2 文件描述符

  对于内核而言,所有打开的文件都通过文件描述符标识,并返回给进程。当读写一个文件时,使用opencreate返回的文件描述符标识该文件,将其作为参数传递给readwrite.
  文件描述符的变化范围是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_CLOEXECFD_CLOEXEC常量设置为文件描述符标志
  • O_CRAET 若文件不存在则创建它。使用此选项时open函数需要说明第三个参数mode,用于指定该文件的访问权限位,openat函数则需要说明第四个参数mode
  • O_DIRECTORY 如果path引用的不是目录,则出错
  • O_EXCL 如果同时指定了O_CREAT,而文件已经存在,则出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使得创建和测试成为一个原子操作。
  • O_NONBLOCK 如果path引用的是一个FIFIO,一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续IO操作设置为非阻塞方式
  • O_SYNC 同步模式,使每次write等待物理IO操作完成,包括由该write操作引起的文件属性更新所需的IO
  • O_TRUNC 如果此文件存在,而且为只写或读写成功打开,则将其长度截断为0

参数fdopenopenat区分开,共有三种可能性。

(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,则将文件偏移量+offsetoffset可正可负
  • SEEK_END,将文件偏移量设置为文件长度+offsetoffset可正可负

  允许设置文件偏移量大于文件当前长度,这会创建一个空洞区,被读为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操作系统支持支持不同进程之间共享打开文件,机制如下:
在这里插入图片描述

  1. 每个进程维护一个进程表项列表,一个进程在文件描述符3上打开该文件,另一个进程在文件描述符4上打开该文件。
  2. 内核为每个进程打开该文件的一个文件表项,
  3. 文件表项包括当前偏移量、当前状态和 v 节点指针,指向 v 节点表项
  4. v 节点包括文件类型和对该文件进行各种操作的函数,并指向 i 节点
  5. 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_CREATO_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");

  由于这个不是原子操作,如果在opencreat之间另一个进程创建了该文件并写入了数据,则次进程后续的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作用
1F_DUPFD或F_DUPFD_CLOEXEC复制一个已有的描述符
2F_GETFD或F_SETFD获取/设置文件描述符标志
3F_GETFL或F_SETFL获取/设置文件状态标志
4F_GETOWN或F_SETOWN获取/设置异步IO所有权
5F_GET_LK、F_SETLK或F_SETLKW获取/设置记录锁
  1. F_DUPFD:复制文件描述符fd,新文件描述符作为函数值返回。他是尚未打开的各描述符中大于等于第三个参数值中各值的最小值。与fd共享同一个文件表项,但是新文件描述符有自己的文件描述符标志。但是其文件描述符FD_CLOEXEC标志被清除,这表示该描述符在exec时仍保持有效。
  2. FD_DUPFD_CLOEXEC :复制文件描述符,新文件描述符关联着文件描述符标志FD_CLOEXEC
  3. FD_GETFD:返回文件描述符fd对应的文件描述符标志,当前只定义了一个FD_CLOEXEC
  4. FD_SETFD:对于fd设置文件描述符标志。新标志按第三个参数设置。
  5. F_GETFL:返回文件状态标志,包括:
    在这里插入图片描述
    但是(O_RDONLY, O_WRONLY,O_RDWR, O_EXEC, and O_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:设置接收SIGIOSIGURG信号的进程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_SETFDF_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也只能进行读操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值