文件I/O

Posted mazinkaiser1991

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了文件I/O相关的知识,希望对你有一定的参考价值。

今天看得挺快的,一下子就把第二章看完了,不过第二章也确实看得不仔细,这一章其实在程序设计中还是非常重要的,因为这一章的内容决定了程序的可移植性。

好了,回到这一章的主题文件I/O。

 3.2节主要对文件描述符的概念进行了简单的介绍。根据APUE:文件描述符是一个非负整数。当进程打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。我也简单地翻了一下LKD和《深入理解linux内核》,其中对于文件描述符的讲解不是很多,所以对于文件描述符也谈不出来太深入理解,给大家还是分享一篇blog吧。

http://blog.csdn.net/cywosp/article/details/38965239

linux系统也将文件描述符0、1、2,分别与进程的标准输入、标准输出、标准错误相关联。在编程过程中,可使用如下定义:

#define	STDIN_FILENO	0	/* Standard input.  */
#define	STDOUT_FILENO	1	/* Standard output.  */
#define	STDERR_FILENO	2	/* Standard error output.  */

以上定义内容位于/usr/include/unistd.h中。

文件描述符的变化范围是0-OPEN_MAX-1。使用“ulimit -n”命令可以查询当前shell以及由它启动的进程可拥有的文件描述符数量。在我的机器上结果是1024,通过“ulimit -n n”命令就可以修改,其中最后一个n表示最大的文件描述符数量。但上述方法只能在当前终端有效,退出之后又恢复为默认值。还可以通过修改/etc/下的文件的方式进行修改,但我对这几个文件的关系还不太清晰,在此就不给大家分享了。以上是修改某个shell的最大文件描述符数量的方法,再来看看修改系统级数值的方法,修改之前先要查看一下相关内容,可通过如下命令进行查询“sudo cat /proc/sys/fs/file-max”,在我的机器上结果是“402307”。修改的方法是通过“6553560 > /proc/sys/fs/file-max”或“sysctl -w "fs.file-max=34166" ”命令,但以上命令在机器重启后失效,所以修改/etc文件的方法才是一劳永逸的方法。

上述修改内容学习自这篇blog:“http://coolnull.com/2796.html”。

以上都是有关于OPEN_MAX的内容,在<stdio.h>中还定义有“# define FOPEN_MAX 16”(确切的说这一定义位于/usr/include/x86_64-linux-gnu/bits/stdio-lim.h中)。

3.3节正式开始有关于编程的内容,首先是打开或创建一个文件,在我的机器中有如下定义:

#include <fcntl.h>
#ifndef __USE_FILE_OFFSET64
extern int open (const char *__file, int __oflag, ...) __nonnull ((1));
#else
# ifdef __REDIRECT
extern int __REDIRECT (open, (const char *__file, int __oflag, ...), open64)
     __nonnull ((1));
# else
#  define open open64
# endif
#endif
#ifdef __USE_LARGEFILE64
extern int open64 (const char *__file, int __oflag, ...) __nonnull ((1));
#endif

# ifndef __USE_FILE_OFFSET64
extern int openat (int __fd, const char *__file, int __oflag, ...)
     __nonnull ((2));
# else
#  ifdef __REDIRECT
extern int __REDIRECT (openat, (int __fd, const char *__file, int __oflag,
                ...), openat64) __nonnull ((2));
#  else
#   define openat openat64
#  endif
# endif
# ifdef __USE_LARGEFILE64
extern int openat64 (int __fd, const char *__file, int __oflag, ...)
     __nonnull ((2));
# endif
#endif
因为是64位的机器,所以有一些64位的函数,open64、openat64。关于文件名实在没什么可说的,关于oflag选项,给大家分享一点,这些常量定义位于/usr/include/fcntl.h(根据书中所写),实际上在该文件中还有一句“#include <bits/fcntl.h>”,这个文件中其实还不包括我们所要找的东西,/usr/include/x86_64-linux-gnu/bits/fcntl-linux.h文件中才是我们所要找的文件,具体内容就不给大家分享了,其中包括有“O_RDONLY”、“O_WRONLY”、“O_RDWR”三个选项,书中还给出了“O_EXEC”与“O_SEARCH”这两个选项,但在我的文件中并没有找到,所以结合书中的内容——“O_RDONLY”、“O_WRONLY”、“O_RDWR”这三个标志必须指定一个且只能指定一个。标志之间使用“|”运算。“...”参数代表文件访问权限的初始值,这一参数仅在 第二个参数中有O_CREAT时才用作用。若没有,则第三个参数可以忽略。来看几个例子:

首先是打开不存在的文件,源码如下:

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc,char *argv[])
{
	int n;
	if((n = open("./temp",O_RDWR))<0)
		perror(argv[0]);
	return 0;
}

运行结果如下,运行出错。

gcc -o test_opennotcreate test_opennotcreate.c
/test_opennotcreate 
./test_opennotcreate: No such file or directory

再来实验一下创建文件,其他选项用到的时候在研究吧,此处我故意仅用读权限创建文件,然后以读写模式打开文件,并向其中写入一些内容

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

int main(int argc,char *argv[])
{
	int n;
	if((n = open("./temp",O_RDWR|O_CREAT,S_IRUSR))<0)
		perror(argv[0]);
        if((n = write(fd,str,strlen(str)))<0)
                perror(argv[0]);
        return 0;
}
执行结果如下:

gcc -o test_opencreate test_opencreate.c
 ./test_opencreate 此时内容成功写入
./test_opencreate  再次执行程序
./test_opencreate: Permission denied
./test_opencreate: Bad file descriptor


出现了无权限创建文件的情况,不知道是由于权限设置的问题,还是由于O_CREATE标志不能用于已存在的文件。今天早上我又研究了一下,errno中有一个EEXIST的错误,代表文件已存在,但我的程序给出的错误码是-1(“无权限”),所以我觉得我应该是权限这一块还有问题没搞清楚,加上一个”用户写权限“试试:

int main(int argc,char *argv[])
{
	int fd,n;
	char str[] = "Hello,world";
	if((fd = open("./temp",O_RDWR|O_CREAT,S_IRUSR|S_IWUSR))<0){
		perror(argv[0]);
	}
	if((n = write(fd,str,strlen(str)))<0)
		perror(argv[0]);
	return 0;
}

运行结果如下:

./test_opencreate 
./test_opencreate 再次运行程序也可以正常运行

看来是由于之前没有写权限导致不能重复打开已存在的文件(此处还是要存下一个疑问,为什么加上一个写权限后就能重复打开打开文件),同时通过实验结果可以发现,每次创建或打开已经存在的文件,都会从文件起始处开始写入,另一方面上述程序也证明了O_CREAT同样可以用于打开已经存在文件,那么问题来了如果两次打开文件的权限不一样怎么办?

再来加上一个O_APPEND选项再试试:

 if((fd = open("./temp",O_RDWR|O_CREAT|O_APPEND,S_IRUSR|S_IWUSR))<0){ 
        perror(argv[0]);
} 

内容就可以追加到文件的结尾处。再加上一个O_TRUNC 选项试试:

if((fd = open("./temp",O_RDWR|O_CREAT|O_APPEND|O_TRUNC,S_IRUSR|S_IWUSR))<0){
		perror(argv[0]);
	}

此时O_TRUNC选项先发挥作用,将文件长度截断为0后,再使用O_APPEND模式,在文件起始处写入文件内容。

这里给大家谈一点我对O_CREAT与O_TRUNC区别的理解,若只有O_CREAT选项,那么文件的写入总是从文件起始处写入,若文件已经存在,那么会覆盖原有文件开头部分的内容,后面的内容可能被保留。若使用O_CREAT|O_TRUNC选项,若文件已经存在而且为只写或读写成功打开,那么首先将其长度截断为0,即原文件中的内容被全部删除。

根据APUE,使用fd参数把open和openat函数区分开,共有3三种可能性。

  1. path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。
  2. path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统的开始地址。openat函数的fd参数通过打开相对路径名所在的目录来获取。
  3. path参数指定了相对路径名,fd参数只用常量AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。

突然一看openat函数有些鸡肋,只是为了在某个文件夹下创建文件就要补充一个新的函数,具体openat函数有什么作用我也不是很清楚。APUE怎么说,我就先给大家直接分享过来。

  1. 让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。同一进程中的所有线程共享相同的当前工作目录,因此很难让同一进程的多个不同线程在同一时间工作在不同的目录。
  2. 避免time-of-check-to-time-of-use(TOCTTOU)错误。此处我想了想,貌似openat和TOCTTOU错误也没有关系。

3.4 creat函数原型如下:

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

该函数等效于:

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

3.5 close函数,该函数可用于关闭一个打开的文件。关闭文件时还会释放该进程加在该文件上的所有记录锁。当一个进程终止时,内核自动关闭它所有的打开文件。

3.6 lseek函数,I/O函数的读写操作通常都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND,否则该偏移量被设置为0,可以调用lseek显示地为一个打开文件设置偏移量,函数原型如下:

#include <unistd.h>
off_t lseek(int fd,off_t offset,int whence)
若成功返回新的偏移量;若出错则返回-1。

whence参数共包括三种选择,分别是:

SEEK_SET:将该文件的偏移量设置为距文件开始处offset个字节。

SEEK_CUR:将该文件的偏移量设置为其当前值加offset,offset可正可负。

SEEK_END:将该文件的偏移量设置为文件长度加offset,offset可正可负。

APUE中给出了一种确定当前文件偏移量的方法:

off_t currpos;
currpos = lseek(fd,0,SEEK_CUR);

将偏移量设置为当前值+0的位置,由此获得当前文件的偏移量。

上述方法也可用于确定所涉及的文件是否可以设置偏移量。若文件描述符指向管道、FIFO(命名管道)、网络套接字,则lseek返回-1,并将errno设置为ESPIPE。

APUE中给出了用于测试标准输入能否设置偏移量的例子,源码如下:

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

int main(int argc,char* argv[])
{
    if( lseek(STDIN_FILENO,0,SEEK_CUR) == -1) perror(argv[0]);
    else printf("seek ok\\n");
    return 0;
}

运行结果如下,直接运行程序发现不能设置标准输入的偏移量。

./test_lseek 
./test_lseek: Illegal seek

通过查询”Illegal seek“对应的errno可以发现,恰好有如下定义,下述定义位于”/usr/include/asm-generic/errno-base.h“中。

#define	ESPIPE		29	/* Illegal seek */

结合之前的描述可以得到结论:标准输入是管道或FIFO。

通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的当前偏移量。对于普通文件,其偏移量必须是非负值(关于文件的类型之后会详细分析)。由于偏移量可能是负值,所以在比较lseek的返回值应当谨慎,不要测试它是否小于0,而要测试它是否等于-1。lseek仅将当前的文件偏移量记录在内核中,这个偏移量可用于下一个读写操作。文件的偏移量可以大于当前文件的长度,在这种情况下,对该文件的下一次写将加长该文件,并将这部分文件的内容填充为0,这部分没有内容的文件被称为“文件空洞”。“文件空洞”并不需要占据磁盘空间。

通过实验验证一下:

#include <fcntl.h>
#include <stdio.h>

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

int main(int argc,char* argv[])
{
	int fd;
	if( (fd = open("file.hole",O_RDWR|O_CREAT,S_IRUSR|S_IWUSR))<0 ) perror(argv[0]);
	if( write(fd,buf1,10) != 10 ) perror(argv[0]);
	if( lseek(fd,16384,SEEK_SET)==-1 ) perror(argv[0]);
	if( write(fd,buf2,10) != 10 ) perror(argv[0]);
	return 0;
}

运行结果如下:

gcc -o test_createhole test_createhole.c
./test_createhole
od -c file.hole 
0000000   a   b   c   d   e   f   g   h   i   j  \\0  \\0  \\0  \\0  \\0  \\0
0000020  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0
*
0040000   A   B   C   D   E   F   G   H   I   J
0040012 
 ls -ls file.hole
8 -rw------- 1 16394  6月 15 14:53 file.hole

再来看看没有空洞的文件的情况,源码如下:

#include <fcntl.h>
#include <stdio.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";
char buf3[] = "\\0";

int main(int argc,char* argv[])
{
	int fd;
	int i;
	if( (fd = open("file.nohole",O_RDWR|O_CREAT,S_IRUSR|S_IWUSR))<0 ) perror(argv[0]);

	i = 0; 
	while(i<16394){
		if( write(fd,buf3,1) != 1 ) perror(argv[0]);
		i++;
	}

	if( lseek(fd,0,SEEK_SET)==-1 ) perror(argv[0]);
	if( write(fd,buf1,10) != 10 ) perror(argv[0]);

	if( lseek(fd,16384,SEEK_SET)==-1 ) perror(argv[0]);
	if( write(fd,buf2,10) != 10 ) perror(argv[0]);
	return 0;
}
运行结果如下:

 gcc -o test_createnohole test_createnohole.c
./test_createnohole
od -c file.nohole 
0000000   a   b   c   d   e   f   g   h   i   j  \\0  \\0  \\0  \\0  \\0  \\0
0000020  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0  \\0
*
0040000   A   B   C   D   E   F   G   H   I   J
0040012
可以发现两个文件的内容完全相同,再来对比一下两个文件。

ls -ls file.hole file.nohole 
 8 -rw------- 1 16394  6月 15 14:53 file.hole
20 -rw------- 1 16394  6月 15 15:19 file.nohole

可以发现,两个文件的长度相同,但实际占据磁盘块,无空洞的文件较少。

3.7 read函数,先来看看函数原型:

#include <unistd.h>
ssize_t read(int fd,void* buf,size_t nbytes);
返回值:读操作从当前的偏移量开始读,返回读到的字节数,若已到文件尾,返回0;若出错,返回-1。此处要注意read函数存在实际读到的字节数少于要求读的字节数。

3.8 write函数,还是先看看函数原型与返回值:

#include <unistd.h>
ssize_t write(int fd,void* buf,size_t nbytes);

返回值:若成功,返回已写的字节数,对于普通文件,写操作从文件的当前偏移量处开始;若出错,返回-1。若返回值与参数nbytes不同则表示出错,代表此时有数据未能成功写入。write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。这里文件长度限制是指进程中存在一个“RLIMIT_FSIZE”常量,用于限定可以创建的文件的最大字节长度。

3.9节主要讨论I/O效率,在此就不给大家展开讲解了。

3.10节首先介绍了内核中I/O所用到的数据结构,在此给大家分享一篇blog吧,里面有些图,我就不盗用了。http://www.linuxidc.com/Linux/2015-01/111700.htm

在已有数据结构的基础上,结合之前介绍的操作做进一步说明:

  1. 在完成每个wirte后,文件表项中的当前文件偏移量就会增加所写入的字节数。若此时当前文件偏移量超过了文件的长度,则将当前文件长度设置为当前文件偏移量,也即文件被加长了。
  2. 如果用O_APPEND标志打开一个文件,在进行write操作前,首先将当前文件偏移量设置为文件长度,通过上述方法可将每次要写入的数据追加到当前文件的尾端处。
  3. 若一个文件用lseek定位到文件当前的尾端,则将文件表项中的当前文件偏移量设置为文件长度。这一过程看似与上一步的结果相同,在单进程的情况下也确实是相同的,但在多进程的情况下,操作就出现了不同。假设有进程A、B都执行“通过lseek定位到文件尾端,然后写入”的任务流(A、B对一个文件进行操作),此时由于进程的调度,上述任务的执行会出现多种情况,若由进程A完整执行这一过程或由进程B完整执行这一过程都不会出现问题,但若先由A使用lseek定位到文件的尾端,假设此时文件偏移量是100,再由B定位到文件的尾端,此时也定位到偏移量100;而后由A写入,A写入完成后再由B写入,此时问题出现了,B还从偏移量100的位置写入,A写入的内容也被覆盖。但若通过O_APPEND标志写入,在进行写入操作前,首先将当前文件偏移量设置为文件长度,则此时文件偏移量指向当前文件的尾端,也就不存在写入覆盖的问题。
  4. lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。

APUE中还讨论了文件描述符和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符(有可能是同一个进程中的不同文件描述符,若不同文件描述符共享文件表项,那么说明这些文件描述符共享文件状态标志、当前文件偏移量等)。

3.11节介绍了原子操作的相关概念,关于原子操作的例子,在上一小节的分析中已经给大家分享过了。

3.12节介绍了用于复制现有文件描述符的函数。函数原型如下:

#include <unistd.h>
int dup(int fd);
int dup2(int fd,int fd2);
返回值:若成功,返回新的文件描述符;若出错,返回-1。

由dup返回的新文件描述符一定是当前可用文件描述符的最小数值,参数fd代表被复制的描述符,文件描述符之间不共享FD_CLOEXEC。对于dup2,可以用fd2参数指定新描述符的值,若fd2已经打开,则先将其关闭(这一过程我在内核源码中没有找到)。此时如若fd等于fd2,则dup2直接返回fd2。 否则(fd不等于fd2的情况下),fd2的FD_CLOEXEC文件描述符标志就被清除,如此fd2在进程调用exec时是打开状态。首先来看看FD_CLOEXEC的含义:“close on exec, not on-fork, 意为如果对描述符设置了FD_CLOEXEC,使用execl执行的程序里,此描述符被关闭,不能再使用它,但是在使用fork调用的子进程中,此描述符并不关闭,仍可使用”。但如果通过dup2函数对fd进行复制,fd2的FD_CLOEXEC标志位就被清除,此时fd2在进程调用exec时是打开状态。还是通过实验简单验证一下,以下的验证程序来自于这篇blog:http://blog.csdn.net/ustc_dylan/article/details/6930189

结合新学到知识验证以下,先来看一个有问题的例子:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
        int fd,pid;
    int newfd;
        char buffer[20];
        fd=open("wo.txt",O_RDONLY);
        printf("%d\\n",fd);
        int val=fcntl(fd,F_GETFD);
        val|=FD_CLOEXEC;
        fcntl(fd,F_SETFD,val);

        pid=fork();
        if(pid==0)
        {
                //子进程中,此描述符并不关闭,仍可使用
                char child_buf[2];
                memset(child_buf,0,sizeof(child_buf) );
                ssize_t bytes = read(fd,child_buf,sizeof(child_buf)-1 );
                printf("child, bytes:%ld,%s\\n\\n",bytes,child_buf);

                //execl执行的程序里,此描述符被关闭,不能再使用它
                char fd_str[5];
                memset(fd_str,0,sizeof(fd_str));
                sprintf(fd_str,"%d",fd); //直接使用原来的文件描述符,但此时没有清除FD_CLOEXEC标志,因此使用execl函数无法打开该文件描述符
                int ret = execl("./exec1","exec1",fd_str,NULL);
                if(-1 == ret)
                        perror("ececl fail:");
        }        

        waitpid(pid,NULL,0);
        memset(buffer,0,sizeof(buffer) );
        ssize_t bytes = read(fd,buffer,sizeof(buffer)-1 );
        printf("parent, bytes:%ld,%s\\n\\n",bytes,buffer);
}

运行结果如下:

3
child, bytes:1,t

exe1: read fail:: Bad file descriptor
parent, bytes:14,his is a test

再来看看使用newfd的情况,同时newfd没有被初始化,源码如下:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(void)
{
        int fd,pid;
	int newfd;
        char buffer[20];
        fd=open("wo.txt",O_RDONLY);
        printf("%d\\n",fd);
        int val=fcntl(fd,F_GETFD);
        val|=FD_CLOEXEC;
        fcntl(fd,F_SETFD,val);

        pid=fork();
        if(pid==0)
        {
                //子进程中,此描述符并不关闭,仍可使用
                char child_buf[2];
                memset(child_buf,0,sizeof(child_buf) );
                ssize_t bytes = read(fd,child_buf,sizeof(child_buf)-1 );
                printf("child, bytes:%ld,%s\\n\\n",bytes,child_buf);

                //execl执行的程序里,此描述符被关闭,不能再使用它
                char fd_str[5];
                memset(fd_str,0,sizeof(fd_str));
		printf("newfd = %d\\n",newfd);
		if( (newfd = dup2(fd,newfd)) == -1 ) perror("dup2 fail:");
                sprintf(fd_str,"%d",newfd);
                int ret = execl("./exec1","exec1",fd_str,NULL);
                if(-1 == ret)
                        perror("ececl fail:");
        }        

        waitpid(pid,NULL,0);
        memset(buffer,0,sizeof(buffer) );
        ssize_t bytes = read(fd,buffer,sizeof(buffer)-1 );
        printf("parent, bytes:%ld,%s\\n\\n",bytes,buffer);
}

上述程序中newfd并没有初始化,此处比较凑巧,在子进程中被初始化为0(此处先存下一个疑问,每次运行的时候都是0,没有初始化的临时变量应该是随机值)。由于我的newfd初始值是0,根据dup2函数的功能:“如果fd2已经被打开,则先将其关闭”,此时文件描述符0就被关闭。而后调用dup2函数清除FD_CLOEXEC标志位,并将文件描述符0重定向至fd(wo.txt),调用execl函数后使用的是已经打开的文件描述符0。

运行结果如下:

3
child, bytes:1,t

newfd = 0
exe1: read 14,his is a test


parent, bytes:0,

若将newfd置为3,则虽然dup2函数返回fd2(此处为3,同时处于开启状态),但由于没有清除 FD_CLOEXEC标志位,则调用execl函数调用时,文件描述符3会被关闭。

改为4试试,程序再次正常运行。若改为使用dup函数,newfd值为4,同时程序正常运行。

有关于dup、dup2函数的源码分析请见:http://blog.csdn.net/u012927281/article/details/51711085

虽然两个不同的文件描述符具有相同的文件状态标志以及同一当前文件偏移量,但每个文件描述符都有它自己的一套文件描述符标志(close_on_exec),新描述符的执行时关闭(close_on_exec)标志总是由dup函数清除。

3.13 sync、fsync、fdatasync函数,先来看看这几个函数的原型:

#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
前两个函数的返回值为:若成功,返回0;若出错,返回-1。内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。由于内存与磁盘写入速度上的差距,当我们向文件写入数据时,内核通常先将数据写入缓冲区,然后排入队列,晚些时候再写入磁盘。上述方式被称为延迟写。在需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,就用到了上述三个函数。

sync的作用是将所有修改过的块缓冲区排入写队列,然后就返回,它不等待实际写磁盘操作结束。通常,由update(守护进程)周期性地调用sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。命令sync也调用sync函数。fsync还会同步更新文件的属性(metadata,包括size、访问时间st_atime&st_mtime等等)。

fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fdatasync函数类似于fsync,但它只影响文件的数据部分。

还是给大家分享一篇blog,书中的内容有些不全面:http://blog.csdn.net/cywosp/article/details/8767327

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

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

fcntl函数有以下5种功能。

  1. 复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)。两者之间的区别是F_DUPFD清除文件描述符标志(FD_CLOEXEC,貌似当前仅有这一个文件描述符标志),而F_DUPFD_CLOEXEC设置FD_CLOEXEC标志。
  2. 获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD)。
  3. 获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)。F_GETFL用于获取文件状态标志,由于3个访问方式标志并不各占1位,因此首先必须用屏蔽字O_ACCMODE获得访问方式位。
  4. 获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)。F_GETOWN用于获取当前接收SIGIO和SIGUSR信号的进程ID或进程组ID。F_SETOWN则用于设置上述内容。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID。
  5. 获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)。

关于文件描述标志与文件状态标志的区别,请见这篇blog:http://blog.csdn.net/hittata/article/details/8665892

最后两章就不给大家分享什么了,现在看可能还用不到,以后用到的时候再说吧。

这里关于习题3.5还要再谈一点,习题如下:

./a.out > outfile 2>&1
./a.out 2>&1 > outfile

 首先根据题目中的提示digit1>&digit2表示要将描述符digit1重定向至描述符digit2,通过前面的介绍我们已经知道要实现描述符的重定向,也就是让两个不同的文件描述符指向同一个文件表项,这一功能可通过dup/dup2函数实现。好了背景知识总结到这里,接下来详细看看这两条命令有什么区别。 

./a.out > outfile 2>&1
首先将标准输出重定向到outfile文件,接下来通过dup函数,使文件描述符2指向文件描述符1指向的文件表项,则此时文件描述2也指向outfile文件。

./a.out 2>&1 > outfile
首先通过dup函数,将文件描述符1复制到文件描述符2,则此时文件描述2指向文件描述1指向的文件表项,而后将文件描述符1重定向到outfile文件,这一操作过后,文件描述符2指向文件描述符1原来指向的文件表项,文件描述符1指向outfile的文件表项。






以上是关于文件I/O的主要内容,如果未能解决你的问题,请参考以下文章

java I/O流基础(知识+代码示例)

对文件 I/O,标准 I/O 的缓冲的理解

监控文件描述符的六种方式(进程监控selectpoll非阻塞轮询I/O异步I/O线程监控)

asyncio 是不是支持文件操作的异步 I/O?

Scala 文件 I/O

Linux I/O重定向