UNIX高级环境编程之文件I/O

1、文件描述符

        对于现有内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传递给read或write。UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准出错关联。各种shell都是这么做的,与内核无关!!!尽管如此,如果不遵循这种惯例,很多UNIX系统应用就不能正常工作。在符合POSIX.1的应用程序中,幻数0、1、2虽然已经被标准化,但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO以提高可读性。(这些常量都定义在头文件<unistd.h>中)

文件描述符的变化范围是0~OPEN_MAX(进程能打开最多文件数)-1。

(对于FreeBSD8.0、Linux3.2.0、Mac OS X 10.6.8以及Solaris10,文件描述符的变化范围几乎是无限的,它只受到系统配置的存储器总量、整型的字长以及系统管理员所配置的软限制和硬限制的约束。)

2、函数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 */);

    //两个函数的返回值:若成功,返回文件描述符;若出错,返回-1

(注:最后一个参数写成"...",ISO C用这种方法表明余下的参数的数量及其类型是可变的。)

        对于open函数而言,仅当创建新文件时才使用最后这个参数。这里在函数模型中将此参数放置在注释中。path参数是要打开或创建文件的名字。oflag参数可用来说明此函数的多个选项。使用下列一个或多个常量进行“或”运算构成oflag参数。

O_RDONLY    //以只读方式打开
O_WRONLY    //以只写方式打开
O_RDWR      //以读写方式打开
    //大多数实现将O_RDONLY定义为0,O_WRONLY定义为1,O_RDWR定义为2
O_EXEC      //以只执行打开
O_SEARCH    //以只搜索打开
    //O_SEARCH常量的目的在于在目录打开时验证它的搜索权限。对目录的文件描述符的操作就不需要再次检查对该目录的的搜索权限。(一般操作系统不支持O_SEARCH)

以上5个常量中必须制定一个且只能指定一个,以下常量才是可选的。

O_APPEND    //每次写时都追加到文件的尾端
O_CLOEXEC   //把FD_CLOEXEC常量设置为文件描述符标志

O_CREAT     //若此文件不存在则创建它。使用此参数时,必须同时指定mode参数说明新创建文件的权限位

O_DIRECRORY //如果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参数值,使其符合Sing UNIX Specification(以及POSIX.1)中同步输入和输出选项的一部分

(更多oflag参数可以通过man手册查询,在此不再多做陈列解释)

        特性:open和openat函数返回的文件描述符一定是最小的未用描述符数值。有一些应用程序利用该特点在标准输入、标准输出和标准出错上打开新的文件。例如,一个应用程序可以先关闭标准输出(通常文件描述符是1),然后打开另一个文件,执行打开操作前就能了解到该文件一定会在文件描述符1上打开。

        fd参数把open和openat函数区分开来,共有3种可能性:

(1)path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。

(2)path指明的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。

(3)path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。

openat函数式POSIX.1版本中新增的一类函数之一,解决着两个问题。①让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录,原本同一进程的所有线程共享相同的当前工作目录,因此很难让同一进程的多个不同线程在同一时间工作在不同的目录中。②可以避免time-of-check-to-time-of-use(TOCTTOU)错误。TOCTTOU错误是:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序是脆弱的。因为两个调用并不是原子操作,在两个函数调用之间文件可能发生了改变,这样也就造成了第一个调用的结果就不再有效,是的程序最终的结果是错误的。文件系统命名空间中的TOCTTOU错误通常处理的就是那些颠覆文件系统权限的,这种操作通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。有关TOCTTOU的缺陷有兴趣可以自行了解在此不做过多论述。

        文件名和路径名截断:如果NAME_MAX(文件名字符最长的最小值)是14,而我们却试图在当前目录中创建一个文件名包含15个字符的新文件,此时会发生什么呢?再起的System V版本(如SVB2)允许这种使用方法,但总是将文件名截断为14个字符,而且不给出任何信息。而BSD类的系统则返回出错状态,并将error设置为ENAMETOOLONG。无声无息地截断文件名会引起问题,而且它不仅仅影响到创建新文件。如果NAME_MAX是14,而存在一个文件名恰好就是14个字符的文件,那么以路径名作为其参数的任意函数(open、stat等)都无法确定该文件的原始名是什么。其原因就是这些函数都无法判断这些文件名是否被截断过。在POSIX.1中,常量_POSIX_NO_TRUNC决定是要截断过长的文件名或路径名,好事返回一个出错。根据文件系统的类型此值是可以变化的,我们可以用fpathconf或pathconf函数来查询目录具体支持何种行为,到底是截断过长的文件名还是返回出错。若_POSIX_NO_TRUNC有效,则在整个路径名超过PATH_MAX或路径名中的任一文件名超过NAME_MAX是,出错返回,并将error设置为ENAMETOOLONG。(现在大多数文件系统支持的文件名最大长度都有255,通常的文件名都比这个数值要小,因此大多数对于应用程序来说这个限制还未出现过什么问题。)

3、函数creat

        creat函数也可以创建一个新文件。

#include <fcntl.h>

int creat(const char *path, mode_t mode);

//返回值:若成功,返回为只写打开的文件描述符;若出错,返回-1

此函数效果上等效于:open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);//如果文件存在则截断它为0

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

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

4、函数close

        用于关闭一个一打开的文件。

#include <unistd.h>

int close(int fd);

//返回值:成功返回0,失败返回-1

关闭一个文件时还会释放该进程加载该文件上的所有记录锁。就算不调用close,当一个进程结束时,内核也会自动关闭所有打开文件。

5、函数lseek

        可以调用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可正可负)。

        如果文件描述符fd指向的是一个管道、FIFO或网络套接字,则lseek出错返回-1,并将error设置为ESPIPE。常量SEEK_SET、SEEK_CUR和SEKK_END也可以用0(绝对偏移量)、1(相对于当前位置的偏移量)和2(相对于文件尾端的偏移量)代替。通常文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是一个负值,所以在比较lseek的返回值应当谨慎,不要测试它小于0,而是是否等于-1。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main(void)
{
    if (-1 == lseek(STDIN_FILENO, 0, SEEK_CUR))
        printf("cannot seek\n");
    else
        printf("seek OK\n");
    exit(0);
}//测试标准输入能否被设置偏移量

还可以用交互方式调用此程序:

$ ./a.out < /etc/passwd
seek OK
$ cat < /etc/passwd | ./a.out
cannot seek
$ ./a.out < /home/zero/FIFO
cannot seek

空洞问题:

        lseek仅将当前的文件偏移量记录在内核中,他并不引起任何I/O操作。然后该偏移量用于下一个读或写操作。文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。

        文件中的空洞并要求在磁盘上暂用存储区,具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写的位置之间的部分则不需要分配磁盘块。

做个测试:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define FILE_MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int main(void)
{
	int fd = 0;
	if ((fd = creat("file.hole", FILE_MODE)) < 0)
		perror("creat error\n");
	if (write(fd, buf1, 10) != 10)
		perror("buf1 write error\n");
	
	if (-1 == lseek(fd, 16384, SEEK_SET))
		perror("lseek error\n");
	
	if (write(fd, buf2, 10) != 10)
		perror("buf2 write error\n");

	exit(0);
}

运行上面的代码可以得到:

//od命令观察文件的实际内容,[-c]参数表示以字符方式打印文件的内容。中间的未写入的字节都被读成了0,每一行开始的一个7位数是以八进制形式表示的字节偏移量。为了证明在该文件中确实存在一个空洞,可以将刚刚创建的文件与相同长度但无空洞的文件做对比:

 

 可以看到,虽然两个文件的长度相同都为16394,但无空洞文件占用了20个磁盘块,而具有空洞的file.hole文件只占用了8个磁盘块。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值