文件I/O总结

前言

不积硅步无以至千里,不积小流无以成江河。继续。。。

 

一、概念:

      文件I/O被称为不带缓冲的I/O,指的是每个read和write都是调用内核中的一个系统调用。这些不带缓冲的文件I/O函数不是ISO C的组成部分。它们是POSIX.1和Single UNIX Specification的组成部分。

      不带缓冲的文件I/O函数常用的有:open、read、write、lseek、close。

      1、关于系统调用

      系统调用,英文名system call,每个操作系统都在内核里有一些内建的函数库,这些函数可以用来完成一些系统系统调用,把应用程序的请求传给内核,调用相应的的内核函数完成所需的处理,将处理结果返回给应用程序,如果没有系统调用和内核函数,用户将不能编写大型应用程序,及别的功能,这些函数集合起来就叫做程序接口或应用编程接口(Application Programming Interface,API),我们要在这个系统上编写各种应用程序,就是通过这个API接口来调用系统内核里面的函数。如果没有系统调用,那么应用程序就失去内核的支持。

     2、 关于缓冲区  

      linix对IO文件的操作分为不带缓存的IO操作和标准IO操作(即带缓存)要明确以下几点:
      1)不带缓存,不是直接对磁盘文件进行读取操作,像read()和write()函数,它们都属于系统调用,只不过在用户层没有缓存,所以叫做无缓存IO,但对于内核来说,还是进行了缓存,只是用户层看不到罢了。
      2)带不带缓存是相对来说的,如果你要写入数据到文件上时(就是写入磁盘上),内核先将数据写入到内核中所设的缓冲储存器,假如这个缓冲储存器的长度是100个字节,调用系统函数:
ssize_t write (int fd,const void * buf,size_t count);
写操作时,设每次写入长度count=10个字节,那么你几要调用10次这个函数才能把这个缓冲区写满,此时数据还是在缓冲区,并没有写入到磁盘,缓冲区满时才进行实际上的IO操作,把数据写入到磁盘上。

      那么,既然不带缓存的操作实际在内核是有缓存器的,那带缓存的IO操作又是怎么回事呢?
带缓存IO也叫标准IO,符合ANSI C 的标准IO处理,不依赖系统内核,所以移植性强,我们使用标准IO操作很多时候是为了减少对read()和write()的系统调用次数,带缓存IO其实就是在用户层再建立一个缓存区,这个缓存区的分配和优化长度等细节都是标准IO库代你处理好了,不用去操心,还是用上面那个例子说明这个操作过程:
上面说要写数据到文件上,内核缓存(注意这个不是用户层缓存区)区长度是100字节,我们调用不带缓存的IO函数write()就要调用10次,这样系统效率低,现在我们在用户层建立另一个缓存区(用户层缓存区或者叫流缓存),假设流缓存的长度是50字节,我们用标准C库函数的fwrite()将数据写入到这个流缓存区里面,流缓存区满50字节后在进入内核缓存区,此时再调用系统函数write()将数据写入到文件(实质是磁盘)上,看到这里,你应该明白一点,标准IO操作fwrite()最后还是要掉用无缓存IO操作write,这里进行了两次调用fwrite()写100字节也就是进行两次系统调用write()。
两条总结:
无缓存IO操作数据流向路径:数据——内核缓存区——磁盘
标准IO操作数据流向路径:数据——流缓存区——内核缓存区——磁盘

(以上参考:https://blog.csdn.net/scottly1/article/details/24186719)

      3、文件描述符:
      对于内核文件而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读或写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。按照惯例,UNIX系统shell使用文件描述符0与进程的标准输入相关联,文件描述符1与标准输出相关联,文件描述符2与标准错误相关联。在依从POSIX的应用程序中,幻数0、1、2应当替换成符号常量STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO。这些常量定义在头文件中。

 

二、常用文件I/O函数解析

      1、open函数:打开或创建一个文件。
      1)头文件:#include
      2)函数原型:int open(const char *pathname, int oflag, ... /*mode_t mode*/);
      3)返回值:若成功则返回文件描述符,若出错则返回-1;
      4)参数:
                    const char *pathname:要打开或创建文件的名字;
                    int oflag:用来说明此函数的多个选项;
                    O_RDONLY   只读打开
                    O_WRONLY   只写打开
                    O_RDWR     读、写打开
                    以上三个常量中必须指定一个且只能指定一个,以下常量则是可选择的:
                    O_APPEND   每次写时都追加到文件的尾端。
                    O_CREAT    若此文件不存在,则创建它。使用此文件时需要第三个参数mode,用其指定该新文件的访问权限。
                    O_EXCL     如果同时指定了O_CREAT,而文件已经存在,则会出错。用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作
                    O_TRUNC    如果此文件存在,而且为只写或读写成功打开,则将其长度截短为0;
                    O_NOCTTY   如果pathname指的是终端设备,则不将该设备分配作为此进程的控制终端。
                    O_NONBLOCK 如果pathname指的是一个FIFO、一个块特殊设备或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞模式。

                    第三个参数mode仅当创建新文件时才使用;

      5)mode:       

        mode代表文件权限标志也可以使用加权数字表示,这组数字被称为umask变量,它的类型是mode_t,是一个无符号八进制数。umask变量的定义方法下表所示。umask变量由3位数字组成,数字的每一位代表一类权限。用户所获得的权限是加权数值的总和。例如764表示所有者拥有读、写和执行权限,群组拥有读和写权限,其他用户拥有读权限。

加 权 数 值

1

2

3

4

所有者拥有

读权限

群组拥有读权限

其他用户拥

有读权限

2

所有者拥有

写权限

群组拥有写权限

其他用户拥

有写权限

1

所有者拥有

执行权限

群组拥有执行权限

其他用户拥

有执行权限

      mode 设置:

      文件权限 = 给定对的文件权限   &    本地掩码(取反)
      例如:设定权限     0777
      umask 出来的本地掩码是   0002

777  ----------------------------二进制                                          111 111 111
002  ----------------------------二进制  00  000 010    取反后得   111 111 101
                                                                                               &   (按位与)
                                                                              实际权限  111 111 101 

      即实际权限为 0775

     使用open 函数创建文件范例:open(pathname, ORDWR | 0_CREAT |O_TRUNC, 0777);

      2、creat函数:创建一个新文件。
      1)头文件:#include
      2)函数原型:int creat(const char *pathname, mode_t mode);
      3)返回值:若成功则返回为只写打开的文件描述符,若出错则返回-1;
      注意:creat是以只写方式打开所创建的文件。如果要创建一个临时文件,并要先写该文件再读该文件,则必须先调用creat、close然后再调用open。可以用以下调用open的方式替代:
      open(pathname, ORDWR | 0_CREAT |O_TRUNC, mode);

      3、lseek函数:显式地为一个打开的文件设置其偏移量。
      每个文件都有一个与其相关联的“当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读写操作都是从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0;
      1)头文件:#include
      2)函数原型:off_t lseek(int filedes, off_t offset, int whence);
      3)返回值:若成功返回新的文件偏移量,若出错则返回-1;
      4)参数:
      int filedes:文件描述符;
      int whence:若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
                           若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。
                           若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可为正或负。
      若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确定打开文件的当前偏移量:
      off_t currpos;
      currpos = lseek(fd, 0, SEEK_CUR);

      4、close函数:关闭一个打开的文件。
      1)头文件:#include
      2)函数原型:int close(int filedes);
      3)返回值:若成功返回0,若出错则返回-1;
      关闭一个文件时还会释放该进程加在该文件上的所有纪录锁。
      当一个进程终止时,内核会自动关闭它所有打开的文件。很多程序都利用了这一功能而不显式地用close关闭打开的文件;
      4)参数:
      int filedes:文件描述符;

      5、read函数:
      1)头文件:#include
      2)函数原型:ssize_t read(int filedes, void *buf, size_t nbytes);
      3)返回值:若成功则返回读到的字节数,若已到文件结尾则返回0,若出错则返回-1;
      有多种情况可使实际读到的字节数少于要求读的字节数:
      读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前还有30个字节,而要求读100个字  节,则read返回30,下一次再调用read时,他将返回0(文件尾端);
      当从终端设备读时,通常一次最多读一行。
      当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
      当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
      当从某些面向记录的设备读时,一次最多返回一个记录。
     4)参数:
     int filedes:文件描述符;
     void *buf:用于存储从文件中读到的内容;
     size_t nbytes:要求读取的字节数;

      6、write函数:
      1)头文件:#include
      2)函数原型:ssize_t write(int filedes, const void *buf, size_t nbytes);
      3)返回值:若成功则返回已写的字节数,若出错则返回-1;
      其返回值通常与nbytes的值相同,否则表示出错。write出错的原因通常是磁盘已写满,或者超过了一个给定进程的文件长度限制。
      对于普通文件,写操作从文件的当前偏移量处开始。如果在打开文件时,指定了O_APPEND选项,则在每次写操作之前,将文件的偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
      4)参数:
      int filedes:文件描述符;
      void *buf:用于存储待写入的数据;
      size_t nbytes:要求写入的字节数;

      7、size_t与ssize_t
     1)size_t是一些C/C++标准在stddef.h中定义的。这个类型足以用来表示对象的大小。size_t的真实类型与操作系统有关,在32位架构中被普遍定义为:
      typedef   unsigned int size_t;
      而在64位架构中被定义为:
      typedef  unsigned long size_t;
      size_t在32位架构上是4字节,在64位架构上是8字节,在不同架构上进行编译时需要注意这个问题。
      2)ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int32 位和 64 位C数据类型

      3)32和64位C语言内置数据类型,如下表所示:

      文件I/O总结_第1张图片

 

三、open、read、write、close函数应用举例:

          从源文件中读取指定长度的字节数,将读到的数据写入到目的文件中。

#include 
#include 
#include 
#include 

#define READ_MAX_SIZE 2048      //要求从源文件中读的字节数
#define WRITE_SIZE 100          //每次调用write时写入的字节数

int copy_info(char *str, char *dstr)
{
    int fp = -1;                      
    int fd = -1;                      
    char *buf = NULL;                  
    char *bufp = NULL;                  
    size_t max_bytes = READ_MAX_SIZE;  //要求读的字节数;max_bytes要做自减操作,必须定义为无符号整型;
	size_t total_read_bytes = 0;       //实际读到的字节数
	size_t total_write_bytes = 0; 	   //实际写入的字节数
    ssize_t read_bytes = 0;            
    ssize_t write_bytes = 0;  
	int count = 0;
	int i = 0;
	
	buf = (char *) malloc(sizeof(char) * (READ_MAX_SIZE + 1));
	memset(buf, '\0', sizeof(char) * (READ_MAX_SIZE + 1));
	
	bufp = buf;
	
	if((str == NULL) || (dstr == NULL))
	{
		printf("input parameter is NULL!\n");
		return -1;

	}
	
    fp = open(str, O_RDONLY);
    if ( 0 > fp )
    {
		printf("[%s]%s %d open %s file failed!\n", __FILE__, __func__, __LINE__, str);
		return -1;
    }

    while ( (0 != max_bytes) && ( read_bytes = read(fp, bufp, max_bytes)) != 0)
    {
        if ( -1 == read_bytes )  
        {
            if ( EINTR == errno )  
            {
                continue;
            }
            else   
            {
				printf("[%s]%s %d Read %s file failed!\n", __FILE__, __func__, __LINE__, str);
				close(fp);
                return -1;
            }
        }

        max_bytes -= read_bytes;
        bufp += read_bytes;
		total_read_bytes += read_bytes;
    }
	errno = 0;
	
	fd = open(dstr, O_WRONLY | O_CREAT |O_TRUNC, 0777);
	if(fd == -1)
	{
		printf("open file %s failed!\n", dstr);
		return -1;
	}
	
	bufp = buf;
	count = total_read_bytes / WRITE_SIZE;
	printf("[%s]%s %d, total_read_bytes = %u ,count = %d\n", __FILE__, __func__, __LINE__, total_read_bytes, count);

	for(i = 0; i < count; i++)
	{
		write_bytes = write(fd, bufp, WRITE_SIZE);
		if(WRITE_SIZE != write_bytes)
		{
			printf("[%s]%s %d write %s file failed!\n", __FILE__, __func__, __LINE__, dstr);
			close(fp);
			close(fd);
			return -1;	
		}
		
		bufp += write_bytes;
		total_write_bytes += write_bytes;
	}

	if(total_read_bytes % WRITE_SIZE != 0)
	{
		write_bytes = write(fd, bufp, total_read_bytes % WRITE_SIZE);
		if(total_read_bytes % WRITE_SIZE != write_bytes)
		{
			printf("[%s]%s %d write %s file failed!\n", __FILE__, __func__, __LINE__, dstr);
			close(fp);
			close(fd);
			return -1;	
		}
		
		bufp += write_bytes;
		total_write_bytes += write_bytes;
		printf("[%s]%s %d remainder = %d, total_write_bytes = %u\n", __FILE__, __func__, __LINE__, write_bytes, total_write_bytes);
	}

	close(fp);
    close(fd);

    return 0;

}


/* 从file1 READ_MAX_SIZE个字节的字符,将其写入写到file2中 */
int main(int argc, char *argv[])
{

	int ret = 0;

	printf("%s %d argc:%d\r\n", __FUNCTION__, __LINE__, argc );

	if((argv[1] == NULL) || (argv[2] == NULL) || (argc < 3))
	{
		printf("input parameter is NULL!\n");
		return -1;

	}

	printf("argv0 = %s\r\n", argv[0]);
	printf("argv1 = %s\r\n", argv[1]);
	printf("argv1 = %s\r\n", argv[2]);


	ret = copy_info( argv[1], argv[2]);
	
	if(ret != 0)
	{
		printf("copy_info error!\n");
	}

	return 0;
}

      1)源文件为二进制文件,且文件长度大于2048字节。输出结果如下:

 

      $ ./read_info test.bin result.bin

      main 120 argc:3

      argv0 = ./read_info

      argv1 = test.bin

      argv1 = result.bin

      [read_info.c]copy_info 73, total_read_bytes = 2048 ,count = 20

      [read_info.c]copy_info 103 remainder = 48, total_write_bytes = 2048

     $

      将源文件与目的文件做二进制对比查看是否符合要求。

      文件I/O总结_第2张图片

      按照要求拷贝了2048个字节的数据。前2048字节完全一致。

       2) 源文件为文本文件,且文件长度小于2048字节。输出结果如下:

       $ ./read_info ifconfig ifconfig.bin

      main 120 argc:3

      argv0 = ./read_info

      argv1 = ifconfig

      argv1 = ifconfig.bin

      [read_info.c]copy_info 73, total_read_bytes = 952 ,count = 9

      [read_info.c]copy_info 103 remainder = 52, total_write_bytes = 952

      $

      将源文件与目的文件做文本文件对比查看是否符合要求。

文件I/O总结_第3张图片

      要求读2048字节的数据,实际只读到了952字节的数据,实际写入到目标文件的字节数也是952字节。读出与写入的数据完全一致。

四、注意

      再次回顾一下read、write函数的定义:

       1、read函数

       1)头文件:#include
       2)函数原型:ssize_t read(int filedes, void *buf, size_t nbytes);
       3)返回值:若成功则返回读到的字节数,若已到文件结尾则返回0,若出错则返回-1;
       有多种情况可使实际读到的字节数少于要求读的字节数:读普通文件时,在读到要求字节数之前已到达了文件尾端。例如,若在到达文件尾端之前还有30个字节,而要求读100个字节,则read返回30,下一次再调用read时,他将返回0(文件尾端);
       4)参数:
       int filedes:文件描述符;
       void *buf:用于存储从文件中读到的内容;
       size_t nbytes:要求读取的字节数;  

       read函数可以根据函数的返回值来判断文件是否结束。即使nbytes大于filedes文件的实际长度,最后实际读到的数据长度还是文件的实际长度,不会出现越界读取数据的问题。

       2、write函数

       1)头文件:#include
       2)函数原型:ssize_t write(int filedes, const void *buf, size_t nbytes);
       3)返回值:若成功则返回已写的字节数,若出错则返回-1;
       其返回值通常与nbytes的值相同,否则表示出错。write出错的原因通常是磁盘已写满,或者超过了一个给定进程的文件长度限制。
       对于普通文件,写操作从文件的当前偏移量处开始。如果在打开文件时,指定了O_APPEND选项,则在每次写操作之前,将文件的偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
      4)参数:
      int filedes:文件描述符;
      void *buf:用于存储待写入的数据;
      size_t nbytes:要求写入的字节数;

      注意writr函数的返回值描述。write函数无法根据函数的返回值来判断文件是否已经读取结束。如果写入的长度nbytes大于buf的长度,write函数还是会往filedes文件中写入nbytes个字节的数据。所以使用write函数时,要注意控制写入数据长度nbytes,防止将Buf以外的数据写入到文件中。

      以下例子中,对于write和fwrite函数,要求写入的字节数nbyte大于buf长度。

      

#include 
#include 
#include 
#include 


int main()
{
	int fd = -1;
	FILE *sp = NULL;
	ssize_t write_bytes = 0; 
	char bufp[10] = {0};
	
	#if 1
	fd = open("111", O_WRONLY | O_CREAT |O_TRUNC, 0777);
	if(fd == -1)
	{
		printf("open file 111 failed!\n");
		return -1;
	}
	
	write_bytes = write(fd, bufp, 100);
	printf("[%s]%s %d remainder = %d\n", __FILE__, __func__, __LINE__, write_bytes);
	close(fd);
	
	#else
	sp = fopen("112", "r");
	fwrite(bufp, sizeof(char), 100, sp);
	fclose(sp);
	#endif
	return 0;
}

      编译没有报错,执行过程中也没有报错,但是输出的结果以二进制方式查看,write写入到目标文件111中的前0个字节的数据是buf中的数据,后90个数据应该是buf后面存储空间中的数据。这显然是有问题的。

      

上图中,使用fwrite函数写112文件,从二进制文件来看,好像fwrite很安全,并没有写入越界的字符。但是只要写入的数据不符合预期就是有问题的,至少在使用上是有问题的。代码和结果如下:

#include 
#include 
#include 
#include 

#define READ_MAX_SIZE 2048
#define WRITE_SIZE 100

int copy_info(char *str, char *dstr)
{
    int fp = -1;                      
    int fd = -1;                      
    char *buf = NULL;                  
    char *bufp = NULL;                  
    size_t max_bytes = READ_MAX_SIZE;  //max_bytes要做自减操作,必须定义为无符号整型
    ssize_t read_bytes = 0;            
    ssize_t write_bytes = 0;  
	int count = 0;
	int i = 0;
	
	buf = (char *) malloc(sizeof(char) * (READ_MAX_SIZE + 1));
	memset(buf, '\0', sizeof(char) * (READ_MAX_SIZE + 1));
	
	bufp = buf;
	
	if((str == NULL) || (dstr == NULL))
	{
		printf("input parameter is NULL!\n");
		return -1;

	}
	
    fp = open(str, O_RDONLY);
    if ( 0 > fp )
    {
		printf("[%s]%s %d open %s file failed!\n", __FILE__, __func__, __LINE__, str);
		return -1;
    }

    while ( (0 != max_bytes) && ( read_bytes = read(fp, bufp, max_bytes)) != 0)
    {
        if ( -1 == read_bytes )  
        {
            if ( EINTR == errno )  
            {
                continue;
            }
            else   
            {
				printf("[%s]%s %d Read %s file failed!\n", __FILE__, __func__, __LINE__, str);
				close(fp);
                return -1;
            }
        }

        max_bytes -= read_bytes;
        bufp += read_bytes;
    }
	errno = 0;
	
	//测试fwrite是否安全。
	FILE *sp = NULL;
	sp = fopen("113", "a");
	fwrite(buf, sizeof(char), 1024, sp);
	close(fp);
    fclose(sp);
	
    return 0;

}


/* 从file1 READ_MAX_SIZE个字节的字符,将其写入写到file2中 */
int main(int argc, char *argv[])
{

	int ret = 0;

	printf("%s %d argc:%d\r\n", __FUNCTION__, __LINE__, argc );

	if((argv[1] == NULL) || (argv[2] == NULL) || (argc < 3))
	{
		printf("input parameter is NULL!\n");
		return -1;

	}

	printf("argv0 = %s\r\n", argv[0]);
	printf("argv1 = %s\r\n", argv[1]);
	printf("argv1 = %s\r\n", argv[2]);


	ret = copy_info( argv[1], argv[2]);
	
	if(ret != 0)
	{
		printf("file_copy error!\n");
	}

	return 0;
}

文件I/O总结_第4张图片

      要求读2048字节的数据,实际只读到了952字节的数据;要求写入实际写入到目标文件的字节数1024字节,实际写入1024字节。其中952字节是实际有用的数据,后72字节是buf中的初始化数据。后72字节的数据虽然不是越界读取,但也不是我们想要的数据。所以,在使用fwrite时也要注意nbytes的长度。

 

引用以下资料,衷心感谢大家的分享:

https://blog.csdn.net/scottly1/article/details/24186719

UNIX环境高级编程

 

 

 

 

你可能感兴趣的:(linux)