1.文件I/O操作概述
在Linux下,文件I/O操作可分为两类,一类是基于文件描述符的I/O操作,另一类是基于数据流的I/O操作。我们可以先来了解文件描述符和数据流这些基本概念。
1.1 文件描述符简介
所谓的文件描述符,就是进程与打开的文件的一个桥梁。通过这个桥梁,才可以在进程中对这个桥梁进行操作。
在Linux环境下,每打开一个磁盘文件,都会在内核中建立一个文件表项,文件表项里存储着文件的状态信息、存储文件内容的缓冲区和当前文件的读写位置。如果同一个磁盘文件打开了三次,就会创建3个这样的文件表项(a,b,c),读写该文件时,只会改变文件表项中的读写位置。这3个文件表项存储在一个文件表数组table[3]中,其中table[0] = a, table[1] = b, table[2] = c。这个文件表的下标就称之为文件描述符。将这个文件描述符存储在一个数组des[3] = {0,1,2},那么,在进程中就可以通过这个des数组下标引用文件表项。也就是说,通过文件描述符就可以访问到这个磁盘文件。
概括地说,文件描述符就是一个小整数:分别是标准输入0,标准输出1,标准错误输出2.它们对应的物理设备是键盘、显示器、显示器。
画个图帮大家理解:
1.2 数据流概述
从数据操作方式这个角度来说,Linux系统中的文件(普通文件与设备文件)可以看做是数据流。对文件操作之前,必须先调用标准I/O库函数fopen()将数据流打开。打开后,就可以对数据流进行输入和输出操作。
要对数据流进行读写操作,需要标准I/O库函数和FILE类型的文件指针一起来实现。这个文件指针是打开数据流时返回的指针,该指针用来表示要操作的数据流。当执行程序时,有3个数据流不需要进行特定的函数进行打开操作,它们会自动打开。这3个数据流是标准输入、标准输出、和标准错误输出。它们自动打开,当不使用时,也会自动关闭。
然而,调用标准I/O库函数fopen()打开数据流,在对数据流进行操作后,需要调用fclose()函数将其关闭。fclose()函数在关闭数据流之前,会清空在操作过程中分配的缓冲区并保存数据信息。
2.基于文件描述符的I/O操作
基于文件描述符的这些I/O操作,都是Linux操作系统提供的一组文件操作的接口函数,如open(),close(),read(),write(),lseek()等。
2.1 文件的打开与关闭
要对一个文件进行操作,前提是它已经存在,然后才能打开。打开后就可以对其操作或控制。在操作完成后,需要将其关闭,如果不及时关闭,就可能造成文件中数据的丢失。
在Linux中,提供了系统调用函数open(),close()用于打开和关闭一个已经存在的文件。
2.1.1 open()函数
该函数可以打开或创建一个文件(包括设备文件),其定义如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
int creat(const char* pathname, mode_t mode);
//函数具体使用哪个,和具体应用场景有关。
//如果目标文件不存在,需要open()创建,则第三个参数创建文件的默认权限。
//否则使用有两个参数的open()函数。
上述的三个函数在调用成功时,都会返回其新分配的文件描述符;否则返回值为-1,并设置适当的errno值。
参数:
pathname均代表要打开或创建的这个文件的路径名称;
flags代表文件打开方式的宏定义;
(O_RDONLY: 只读打开;)
(O_WRONLY: 只写打开;)
(O_RDWR: 读写打开;)
(O_CREAT: 若文件不存在,则创建。需要使用mode选项来指明新文件的访问权限;)
(O_APPEND: 追加写)
mode均代表文件的访问权限。
2.1.2 close()函数
该函数用于关闭一个已经打开的文件,其定义如下:
#include <unistd.h>
int close(int fd);
如果调用成功,则返回0;失败,返回-1,并设置适当的errno值。
参数fd是要关闭的文件描述符。
2.2 文件的读写操作
在Linux系统中,提供了系统调用函数read()和write(),用于实现文件的读写操作。
2.2.1 read()函数
该函数从打开的文件(包括设备文件)中读取数据,该函数定义如下:
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t count);
参数:
fd代表的是要进行读写的文件的文件描述符;
buf代表的是读取的数据存放在buf指针所指向的缓冲区中;
count代表的是读取的数据的字节数。
读取文件数据时,文件的当前读写位置会向后移动。
注意:这个读写位置和使用C标准I/O库时的读写位置有可能不同。这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区的位置。
如果调用成功,返回值为读取的字节数;否则返回值-1,并设置适当的errno值。
2.2.2 write()函数
该函数向打开的设备或文件中写入数据,其定义如下:
#include <unistd.h>
ssize_t write(int fd, const void* buf, size_t count);
参数:
fd代表想要写入数据的文件的文件描述符;
buf指向写入文件的数据的缓冲区;
count代表写入文件的数据的字节数。
调用成功返回写入的字节数,否则返回-1,并设置适当的errno值。
说明:当向常规文件写入数据时,返回值是字节数count; 但是当向终端设备或者网络中写入时,返回的不一定为写入的字节数。
2.2.3 文件的定位
每个打开的文件都记录着当前的读写位置,打开文件时读写位置是0,表示文件开头,通常读写多少个字节就会将读写位置往后移动多少个字节。
lseek()函数可以移动当前的读写位置,通常也叫做偏移量,定义如下:
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int fileds, off_t offset, int whence);
参数:
fileds代表文件名描述符;
offset代表偏移量;
whence代表用于偏移时的相对位置,其取值如下:
(SEEK_SET: 从文件开头计算偏移量;)
(SEEK_CUR: 从当前位置计算偏移量;)
(SEEK_END: 从文件末尾计算偏移量;)
偏移量允许超过文件末尾,这种情况下对该文件的下一次写操作将延长文件,未写入内容的空间用’\0’填满。
函数调用成功返回新的偏移量,否则返回-1,并设置新的errno值。
例:通过调用上述的几种系统调用函数,对文件进行简单的读写操作。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
char* path = "/root/file/oldfile.txt"; /*进行操作的文件路径*/
int fd;
char buf[40], buf2[]="hello mrcff"; /*自定义读写用的缓冲区*/
int n, i;
if((fd = open(path, O_RDWR))<0)
{
perror("open file failed!");
return 1;
}
else
printf("open file successful!\n");
if((n = read(fd, buf, 20))<0)
{
perror("read failed!");
return 1;
}
else
{
printf("output read data:\n");
printf("%s\n", buf); /*将读取的数据输出到终端控制台*/
}
if((i = lseek(fd, 11, SEEK_SET))<0) /*定位到从文件开头处到第11个字节处*/
{
perror("lseek error!");
return 1;
}
else
{
if(write(fd, buf2, 11)<0) /*向文件中写入数据*/
{
perror("write error!");
return 1;
}
else
{
printf("write successfully!\n");
}
}
close(fd); /*关闭文件的同时保存对文件的改动*/
if((fd = open(path, O_RDWR))<0) /*打开文件*/
{
perror("open file failed!");
return 1;
}
if((n = read(fd, buf, 40)) <0)
{
perror("read 2 error!");
return 1;
}
else
{
printf("read the changed data:\n");
printf("%s\n", buf); /*将数据输出到终端*/
}
if(close(fd)<0) /*关闭文件*/
{
perror("close failed!");
return 1;
}
else
printf("close successfully! Bye~\n");
return 0;
}