标准IO与文件IO

  对于操作系统而言,I/O操作可以分为两类,一类是带缓存的IO,又称为标准IO(C标准库中提供了标准IO库,即stdio),它实现了跨平台的用户缓存解决方案。另一类是Unix/Linux下的文件IO,又称直接IO,即文件访问机制不经过操作系统内核的缓存,数据直接在磁盘和应用程序地址空间进行传输。相对而言,直接IO效率更高。本篇文章将从文件的打开、读写等方面来介绍这两类IO操作。

一.标准IO

    标准IO在系统调用的上一层多加了一个缓冲区,也因此引入了流的概念,在UNIX/Linux下表示为FILE*(并不限于UNIX/Linux,ANSI C都有FILE的概念),FILE实际上包含了为管理流所需要的所有信息:实际I/O的文件描述符,指向流缓存的指针(标准I/O缓存,由malloc分配,又称为用户态进程空间的缓存,区别于内核所设的缓存),缓存长度,当前在缓存中的字节数,出错标志等。标准I/O对每个I/O流自动进行缓存管理,它提供了三种类型的缓存:

          1) 全缓存。当填满标准I/O缓存后才执行I/O操作。磁盘上的文件通常是全缓存的。
          2) 行缓存。当输入输出遇到新行符或缓存满时,才由标准I/O库执行实际I/O操作。stdin、stdout通常是行缓存的。
          3) 无缓存。相当于read、write了。stderr通常是无缓存的,因为它必须尽快输出。

    在linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓存区中,然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间。标准I/O库在关闭流的时候自动释放缓存。另外,也可以使用函数fflush()将流所有未写的数据送入(刷新)到内核(内核缓冲区),fsync()将所有内核缓冲区的数据写到文件(磁盘)。第一次调用带缓存的文件操作函数时,标准库会自动分配内存并且读出一段固定大小的内容存储在缓存中。所以以后每次的读写操作并不是针对硬盘上的文件直接进行的,而是针对内存中的缓存的。何时从硬盘中读取文件或者向硬盘中写入文件有标准库的机制控制。实际上,标准IO最终还是通过调用系统提供的不带缓存的IO实现的(每次read/write都进行一次系统调用),标准IO的引入,避免了频繁的系统调用,减少了系统资源消耗,提高了IO效率。

   以fgetc/fputc 为例,当用户程序第一次调用fgetc 读一个字节时,fgetc 函数可能通过系统调用进入内核读1K字节到I/O缓冲区中,然后返回I/O缓冲区中的第一个字节给用户,把读写位置指 向I/O缓冲区中的第二个字符,以后用户再调fgetc ,就直接从I/O缓冲区中读取,而不需要进内核了,当用户把这1K字节都读完之后,再次调用fgetc时,fgetc函数会再次进入内核读1K字节到I/O缓冲区中。在这个场景中用户程序、C标准库和内核之间的关系就像在“Memory Hierarchy”中CPU、Cache和内存之间的关系一样,C标准库之所以会从内核预读一些数据放在I/O缓冲区中,是希望用户程序随后要用到这些数据,C标准库的I/O缓冲区也在用户空间,直接从用户空间读取数据比进内核读数据要快得多。另一方面,用户程序调用fputc通常只是写到I/O缓冲区中,这样fputc函数可以很快地返回,如果I/O缓冲区写满了,fputc 就通过系统调用把I/O缓冲区中的数据传给内核,内核最终把数据写回磁盘或设备。有时候用户程序希望把I/O缓冲区中的数据立刻传给内核,让内核写回设备或磁盘,这称为Flush操作,对应的库函数是fflush,fclose函数在关闭文件之前也会做Flush操作

    标准IO具有以下优点:(1)使用了操作系统内核缓冲区,在一定程度上分离了应用程序空间和实际的物理设备。(2)减少了直接读盘次数,提高性能。当应用程序尝试读取某块数据的时候,如果这块数据已经存放在了页缓存中,那么这块数据就可以立即返回给应用程序,而不需要经过实际的物理读盘操作。当然,如果数据在应用程序读取之前并未被存放在页缓存中,那么就需要先将数据从磁盘读到页缓存中去。对于写操作来说,应用程序也会将数据先写到页缓存中去,数据是否被立即写到磁盘上去取决于应用程序所采用的写操作机制:如果用户采用的是同步写机制( synchronous writes ), 那么数据会立即被写回到磁盘上,应用程序会一直等到数据被写完为止;如果用户采用的是延迟写机制( deferred writes ),那么应用程序就完全不需要等到数据全部被写回到磁盘,数据只要被写到页缓存中去就可以了。在延迟写机制的情况下,操作系统会定期地将放在页缓存中的数据刷到磁盘上。与异步写机制( asynchronous writes )不同的是,延迟写机制在数据完全写到磁盘上的时候不会通知应用程序,而异步写机制在数据完全写到磁盘上的时候是会返回给应用程序的。所以延迟写机制本身是存在数据丢失的风险的,而异步写机制则不会有这方面的担心。

    为了说明标准IO中缓存区的存在,参考下面代码的执行和输出:

#include 
#include 
#include 

int main(int argc, char *argv[])
{
	char temp[100];
	FILE *myfile =stdin;
	printf("before reading----------------\n");
	printf("file buffer base: %p\n",myfile->_IO_read_base);
	printf("read buffer length %ld\n", myfile->_IO_read_end - myfile->_IO_read_base);
	printf("write buffer base %p\n", myfile->_IO_write_base);
	printf("write buffer length %ld\n", myfile->_IO_write_end - myfile->_IO_write_base);
	printf("buf buffer base %p\n", myfile->_IO_buf_base);
	printf("buf buffer length %ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base);
	printf("\n");
	printf("Please Enter: ");
	fgets(temp, 100, myfile);
	fputs(temp, myfile);
	printf("\n");
	printf("after reading-----------------\n");
	printf("read FILE buffer base %p\n", myfile->_IO_read_base);
	printf("read FILE buffer length %ld\n", myfile->_IO_read_end - myfile->_IO_read_base);
	printf("write FILE buffer base %p\n", myfile->_IO_write_base);
	printf("write FILE buffer length %ld\n", myfile->_IO_write_end - myfile->_IO_write_base);
	printf("FILE buffer base %p\n", myfile->_IO_buf_base);
	printf("FILE buffer length %ld\n", myfile->_IO_buf_end - myfile->_IO_buf_base);
	
	exit(0);	
}


输出:

标准IO与文件IO_第1张图片
可以看出,在执行gfets之前,myfile缓存区是没有分配内存的,执行完fget后,myfile的缓存区才被分配,缓存区的长度是1024Byte。从而说明了标准IO中缓存区的存在。

    下面再补充一个标准IO文件操作的例子,代码如下:

#include 
#include 
#include 

int main(int argc, char *argv[])
{
	FILE *filp = NULL;
	char fileDir[] = "/home/yangzhiyuan/Documents/test.txt"; 
	char dataPtr[] = "Helloworld";
	filp = fopen(fileDir,"w+");  /* 可读可写,不存在则创建 */
	if(fwrite(dataPtr,1,sizeof(dataPtr),filp) != sizeof(dataPtr)) {  /* 写数据到文件中 */
		perror("fwrite error");
		exit(1);
	}
	fclose(filp);
	
	FILE *fp = NULL;
	fp = fopen(fileDir,"r");
	char buffer[256];
	fread(buffer,1,sizeof(buffer),fp);  /* 读取文件中的内容 */	
	fclose(fp);

	printf("%s\n",buffer);
	exit(0);
}

二.文件IO

  Linux中一切皆文件,对于Linux文件的操作使用直接IO,应用程序中调用open,write,read,ioctl,close等函数操作linux中的文件。linux下文件IO的操作要包含下面4个头文件:

#include 
#include 
#include 
#include 

下面主要介绍文件的读写,其他的参考man help。

1.read读文件


  默认情况下,文件都是以阻塞方式打开的,文件的读也是默认采用阻塞方式。因此关于read的返回值,返回小于len的非零正整数也是合法的。关于read的返回值说明如下:

  (1)当采用默认方式打开文件时(阻塞读)

  read的返回值:a. 读取失败,返回值小于零,错误码在errno中有定义。

                         b.读取成功:返回实际读取字节个数,当到达文件末尾EOF时,调用read,会返回0,表示没有读取到任何字节。如果调用read读取指定len个字节,但是没有一个字节可读时,调用会被阻塞(sleep),直到有数据可读,程序才会继续执行。关于读取指定len个字节数据,可采用下面方法实现:

ssize_t ret;
while(len != 0 && (ret = read(fd,buf,len)) != 0) {  /* 一直读完len个字节或者读到EOF */
  if(ret == -1) {
    if(errno == EINTR)  /* 表示读取数据之前接收到信号 */
      continue;
    perror("read");
    break;
  }
  len -= ret;
  buf += ret;
} 
  (2)采用非阻塞方式读

  这种情况需要使用非阻塞方式打开文件,open调用如下:

fd = open(filename,O_NONBLOCK);
char buf[BUFSIZ];
ssize_t nr;

start:
nr = read(fd,buf,BUFSIZ);
if(nr == -1) {
  if(errno == EINTR)
    goto start;
  if(errno == EAGAIN)
    /* resubmit later */
  else
    /* error */
}
非阻塞读的意义在于捕获EAGAIN。

参考代码1:blockread.c

#include 
#include 
#include 
#include 
#include 
#include 

#define SIZE 1024

int main(int argc,char * arg[])
{
	char buf[SIZE];
    int fd;
	char *file = "/bin/blockreadfile";
	ssize_t nr;
	if((fd = open(file,O_RDONLY|O_CREAT,0777)) < 0) 
		printf("open %s failed\n",file);
	else {
		printf("Before reading! \n");
		if((nr = read(fd,buf,SIZE))<0) { //sleep
			perror("read");		
		}
		else 
			printf("The content is %s\n",buf);
		printf("Reading finished!\n");
	}
	return 0;
}


2.write写文件

注意:与read相比,write()调用不太可能出现部分写的情况,也不存在EOF场景,因此一般不会出现循环调用write(),但是对于socket类型,需要循环来保证写了所有的字节请求。

ssize_t ret, nr;
while(len != 0 && (ret = write(fd,buf,len)) != 0) {
  if(ret == -1) {
    if(errno == EINTR)
      continue;
    perror("write");
    break;
  }
  len -= ret;
  buf += ret;
}
此外,写文件的方式还与open()调用有关,可以有追加写(Append)模式、同步写模式等。

参考代码2:write_read.c

#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_SIZE 1000

int main(int argc,char* arg[])
{
	int fd;
	ssize_t length_w,length_r = MAX_SIZE,ret;
	char *testwrite = "/bin/testwrite";
    ssize_t count;
	char buffer_write[] = "Hello Write Function!";
	char buffer_read[MAX_SIZE];

	if((fd = open(testwrite,O_RDWR|O_CREAT,0777))<0) {
		printf("open %s failed\n",testwrite);    
	}
    //check partial write
    count = sizeof(buffer_write);
    length_w = write(fd,buffer_write,count);
    if(length_w == -1)
		perror("write");
	else if(length_w != count)
        printf("It's partial write!\n");
	else
        printf("Write Function OK!\n");
	close(fd);

    if((fd = open(testwrite,O_RDWR|O_CREAT,0777))<0) {
		printf("open %s failed!\n,testwrite");
	}
	if((ret = read(fd,buffer_read,length_r))<0) {
		perror("read");
	}
	printf("Files Content is %s \n",buffer_read);
	close(fd);

	return 0;
}


参考:

1. 《Linux系统编程》—— 第二版

2.  http://blog.csdn.net/carolzhang8406/article/details/7227761

3.  http://blog.sina.com.cn/s/blog_6592a07a0101gar7.html

你可能感兴趣的:(Linux系统编程)