write不能满足要求,需要fsync
Linux、unix在内核中设有缓冲区、高速缓冲或页面高速缓冲,大多数磁盘I/O都通过缓冲进行,采用延迟写技术。
对于write函数,我们认为该函数一旦返回,数据便已经写到了文件中。但是这种概念只是宏观上的,一般情况下,对硬盘(或者其他持久存储设备)文件的write操作,更新的只是内存中的页缓存(page cache),而脏页不会立即更新到硬盘中,而是由操作系统统一调度,如flusher内核线程在满足一定条件时(一定时间间隔、内存中的脏页达到一定比例)将脏页面同步到硬盘上(放入设备的IO请求队列)。
因为write调用不会等到硬盘IO完成之后才返回,设想如果操作系统在write调用之后、硬盘同步之前崩溃,则数据可能丢失。虽然这样的时间窗口很小,但是对于需要保证事务的持久化(durability)和一致性(consistency)的数据库程序来说,write()所提供的“松散的异步语义”是不够的,通常需要操作系统提供的同步IO(synchronized-IO)原语来保证:
1)sync:将所有修改过的快缓存区排入写队列,然后返回,并不等待实际写磁盘操作结束;
2) fsync:只对有文件描述符制定的单一文件起作用,并且等待些磁盘操作结束,然后返回;
3)fdatasync:类似fsync,但它只影响文件的数据部分。fsync还会同步更新文件的属性;
4)fflush:标准I/O函数(如:fread,fwrite)会在内存建立缓冲,该函数刷新内存缓冲,将内容写入内核缓冲,要想将其写入磁盘,还需要调用fsync。(先调用fflush后调用fsync,否则不起作用)。
fsync的功能是确保文件fd所有已修改的内容已经正确同步到硬盘上,该调用会阻塞等待直到设备报告IO完成。
PS:如果采用内存映射文件的方式进行文件IO(使用mmap,将文件的page cache直接映射到进程的地址空间,通过写内存的方式修改文件),也有类似的系统调用来确保修改的内容完全同步到硬盘之上:
#incude
int msync(void *addr, size_t length, int flags)
msync需要指定同步的地址区间,如此细粒度的控制似乎比fsync更加高效(因为应用程序通常知道自己的脏页位置),但实际上(Linux)kernel中有着十分高效的数据结构,能够很快地找出文件的脏页,使得fsync只会同步文件的修改内容。
除了同步文件的修改内容(脏页),fsync还会同步文件的描述信息(metadata,包括size、访问时间等等),因为文件的数据和metadata通常存在硬盘的不同地方,因此fsync至少需要两次IO写操作,多余的一次IO操作,根据Wikipedia的数据,当前硬盘驱动的平均寻道时间(Average seek time)大约是3~15ms,7200RPM硬盘的平均旋转延迟(Average rotational latency)大约为4ms,因此一次IO操作的耗时大约为10ms左右。Posix同样定义了fdatasync,放宽了同步的语义以提高性能:
int fdatasync(int fd);
fdatasync的功能与fsync类似,但是仅仅在必要的情况下才会同步,因此可以减少一次IO写操作。
"fdatasync does not flush modifiedmetadata unless that metadata is needed in order to allow a subsequent dataretrieval to be corretly handled."
举例来说,文件的尺寸(st_size)如果变化,是需要立即同步的,否则OS一旦崩溃,即使文件的数据部分已同步,由于metadata没有同步,依然读不到修改的内容。而最后访问时间(atime)/修改时间(mtime)是不需要每次都同步的,只要应用程序对这两个时间戳没有苛刻的要求,基本没有问题。
补充:函数open的参数O_SYNC/O_DSYNC有着和fsync/fdatasync类似的含义:使每次write都会阻塞等待硬盘IO完成。
O_SYNC 使每次write等待物理I/O操作完成,包括由write操作引起的文件属性更新所需的I/O。
O_DSYNC 使每次write等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。
注意区别:
O_DSYNC和O_SYNC标志有微妙的区别:仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多数据)时,O_DSYNC标志才影响文件属性。而设置O_SYNC标志后,数据和属性总是同步更新。当文件用O_DSYN标志打开,在重写其现有的部分内容时,文件时间属性不会同步更新。于此相反,文件如果是用O_SYNC标志打开的,那么对于该文件的每一次write都将在write返回前更新文件时间,这与是否改写现有字节或追加文件无关。相对于fsync/fdatasync,这样的设置不够灵活,应该很少使用。
(来自http://blog.csdn.net/cywosp/article/details/8767327)
为了满足事务要求,数据库的日志文件是常常需要同步IO的。由于需要同步等待硬盘IO完成,所以事务的提交操作常常十分耗时,成为性能的瓶颈。在Berkeley DB下,如果开启了AUTO_COMMIT(所有独立的写操作自动具有事务语义)并使用默认的同步级别(日志完全同步到硬盘才返回),写一条记录的耗时大约为5~10ms级别,基本和一次IO操作(10ms)的耗时相同。
我们已经知道,在同步上fsync是低效的。但是如果需要使用fdatasync减少对metadata的更新,则需要确保文件的尺寸在write前后没有发生变化。日志文件天生是追加型(append-only)的,总是在不断增大,似乎很难利用好fdatasync。
Berkeley DB是处理日志文件的步骤:
1)每个log文件固定为10MB大小,从1开始编号,名称格式为“log.%010d"
2)每次log文件创建时,先写文件的最后1个page,将log文件扩展为10MB大小
3)向log文件中追加记录时,由于文件的尺寸不发生变化,使用fdatasync可以大大优化写log的效率
4)如果一个log文件写满了,则新建一个log文件,也只有一次同步metadata的开销
内存映射:内存映射文件,是由一个文件到一块内存的映射。Win32提供了允许应用程序把文件映射到一个进程的函数 (CreateFileMapping)。内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。
延迟写(delayed write): 传统的UNIX实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲进行。当将数据写入文件时,内核通常先将该数据复制到其中一个缓冲区中,如果该缓冲区尚未写满,则 并不将其排入输出队列,而是等待其写满或者当内核需要重用该缓冲区以便存放其他磁盘块数据时, 再将该缓冲排入到输出队列,然后待其到达队首时,才进行实际的I/O操作。这种输出方式就被称为延迟写。
延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,使得欲写到文件中的数据在一段时间内并没有写到磁盘上。当系统发生故障时,这种延迟可能造成文件更新内容的丢失。为了保证磁盘上实际文件系统与缓冲区高速缓存中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。
sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲洗内核的块缓冲区。命令sync(1)也调用sync函数。
标准IO函数(如fread,fwrite等)会在内存中建立缓冲,该函数刷新内存缓冲,将内容写入内核缓冲,要想将其真正写入磁盘,还需要调用fsync。(即先调用fflush然后再调用fsync,否则不会起作用)。
fflush以指定的文件流描述符为参数(对应以fopen等函数打开的文件流),仅仅是把上层缓冲区中的数据刷新到内核缓冲区就返回,
因此相对于fsync而言不是很安全,还需要再调用一下fsync来把数据真正写入硬盘。使用函数
fflush函数包含在stdio.h头文件中,用来强制将缓冲区中的内容写入文件。
(1)函数原型:int fflush(FILE *stream) ;
(2)函数功能:清除一个流,即清除文件缓冲区,当文件以写方式打开时,将缓冲区内容写入文件。也就是说,对于ANSI C规定的是缓冲文件系统,函数fflush用于将缓冲区的内容输出到文件中去。
(3)函数返回值:如果成功刷新,fflush返回0。指定的流没有缓冲区或者只读打开时也返回0值。返回EOF指出一个错误。
下面给出一个具体的例子来演示该函数使用的方法:
#include
#include
intmain(void){
FILE *fp;
if((fp=fopen("test","rb"))==NULL) {
printf("Cannot open file.\n");
exit(1);
}
char ch = 'C';
int i;
for(i=0; i<5; i++) {
fwrite(ch, sizeof(ch), 1, fp);
fflush(fp);
}
fclose(fp);
return 0;
}
注意:如果在写完文件后调用函数fclose关闭该文件,同样可以达到将缓冲区的内容写到文件中的目的,但是那样系统开销较大。
通常我们用fd表示,该fd可以是open、pipe、dup、dup2和creat等调用返回的结果,在linux系统中,设备也是以文件的形式存在,要对该设备进行操作就必须先打开这个文件,打开这个文件就会,就会获得这个文件描述符,它是个很小的正整数,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。文件描述符的优点:兼容POSIX标准,许多Linux和 UNIX系统调用都依赖于它。文件描述符的缺点:不能移植到UNIX以外的系统上去,也不直观。
fd只是一个索引.
在UNIX、Linux的系统调用中,大量的系统调用都是依赖于文件描述符。
此外,在Linux系列的操作系统上,由于Linux的设计思想便是把一切设备都视作文件。因此,文件描述符为在该系列平台上进行设备相关的编程实际上提供了一个统一的方法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include
#include <fcntl.h>
int main(void){ int fd; int numbytes; char path[] ="file"; char buf[256]; /*
* O_CREAT: 如果文件不存在则创建
* O_RDONLY:以只读模式打开文件
*/
fd = open(path, O_CREAT | O_RDONLY, 0644);
if(fd < 0){ perror("open()");
exit(EXIT_FAILURE); } memset(buf, 0x00, 256);
while((numbytes = read(fd, buf, 255)) > 0){ printf("%dbytes read: %s", numbytes, buf);
memset(buf, 0x00, 256);
} close(fd);
exit(EXIT_SUCCESS);}
C语言中使用的是文件指针而不是文件描述符做为I/O的句柄."文件指针(file pointer)"指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符值.而文件描述符值是文件描述符表中的一个索引.从某种意义上说文件指针就是句柄的句柄。文件指针的优点:是c语言中的通用格式,便于移植。
printf -> stdout -> STDOUT_FILENO(1) -> 终端(tty)
stdout/stdin -- 标准输出输入设备(printf("..")) 。
stderr-- 标准错误输出设备
两者默认向屏幕输出。但如果用转向标准输出到磁盘文件,则可看出两者区别。stdout输出到磁盘文件,stderr在屏幕。printf最后的输出到了终端设备,文件描述符1指向当前的终端可以这么理解:
STDOUT_FILENO = open("/dev/tty",O_RDWR);
stdout/stdin类型为 FILE*
STDOUT_FILENO类型为 int
使用stdout/stdin的函数主要有:fread、fwrite、fclose等,基本上都以f开头
使用STDIN_FILENO的函数有:read、write、close等
fwrite(buf,strlen(buf), 1,stdout);
write(STDOUT_FILENO,&buf,strlen(buf));
总结:
注释(1)的fclose 只关闭了stdout,而没有关闭STDOUT_FILENO,所以write() 仍然可以写到STDOUT_FILENO中.
但是把(1)处改为close(STDOUT_FILENO),程序输出变为:
./a.out
before vfork
pid = 28003, glob = 7, var = 89
32
C语言文件指针域文件描述符之间可以相互转换。
(1)FILE *fdopen(intfildes, const char *type);
这个函数很有用的,功能是将一个流关联到一个打开的文件号filedes上,该filedes可以是open、pipe、dup、dup2和creat等调用返回的结果。
type指定流打开方式,同fopen的打开方式,如"a", "r","w"等等
fdopen的流打开方式服从filedes的打开方式,比如filedes的open指定O_RDONLY,那么fdopen也只能指定"r"的打开方式了。
用fdopen的好处很明显,如果你不得已只能打开文件号,比如socket或者dup调用,但又想用fprintf,fscanf等流操作来进行读写,那么就再fdopen一次好了。
FILE的结构
struct _iobuf {
char *_ptr; //缓冲区当前指针
int _cnt;
char *_base; //缓冲区基址
int _flag; //文件读写模式
int _file; //文件描述符
int _charbuf; //缓冲区剩余自己个数
int _bufsiz; //缓冲区大小
char *_tmpfname;
};
typedef struct _iobuf FILE;
(2)intfileno(FILE stream);
用 fileno好处:你用fopen打开了文件,但是又想用flock或者lockf来给文件加锁,或者用fcntl来进行某些底层的操作,但上述这些函数只能对打开的文件号操作,而不能对打开流,这时候就用fileno再flock、lockf、fcntl好了。
既然FILE结构中含有文件描述符,那么可以使用fopen来获得文件指针,然后从文件指针获取文件描述符,文件描述符应该是唯一的,而文件指针却不是唯一的,但指向的对象是唯一的。