3.2 文件I/O基本操作
Linux系统中文件操作主要有:不带缓存的I/O操作和带缓存的I/O操作。
3.2.1 不带缓存的I/O操作
不带缓存的I/O操作又称底层I/O操作。文件底层I/O操作的系统调用主要用到5个函数:open()、close()、read()、write()、lseek()。这些函数的特点是不带缓存,直接对文件进行操作。
虽然不带缓存的文件I/O操作程序不能移植到非POSIX标准的系统(如Windows系统)上去,但是在嵌入式程序设计、TCP/IP的Socket套接字程序设计、多路IO操作程序设计等方面应用广泛。因此,不带缓存的文件I/O程序设计是Linux文件操作程序设计的重点。
1.函数列表
不带缓存的文件I/O操作见表3-1。
表3-1 不带缓存的文件I/O操作
2.函数使用
函数使用见表3-2~表3-6。
表3-2 open()函数语法
open函数试图打开pathname指定的一个文件,flags指定访问该文件的方式,参数perms则指定了文件在创建时的模式。必须把flags设置为O_RDONLY、O_WRONLY或者O_RDWR,分别表示只读、只写和读/写访问。另外,可以按位方式设置多个表中列出的值。如果默认的文件模式能够满足要求,则使用open的第一种形式,如果希望像使用进程的模式那样也设置一个特定的文件模式,则使用open的第二种形式。两种形式调用成功后都返回一个文件描述符。如果失败则返回-1。
小知识:creat也能打开一个文件,如果该文件不存在,则创建它,和open一样,creat也在调用成功后返回文件描述符,失败返回-1,但本书没有介绍creat的原因有两个:一是creat拼写有错误,二是调用open函数更常见,而且使用open函数时O_CREAT标志和直接使用creat取得的结果一样。
表3-3 close()函数语法
为了在使用完某个文件后关闭它,可以调用系统调用close,close只有一个参数,即open函数返回的文件描述符。一旦调用了close,则该进程就不可以再使用该文件描述符进行文件操作了。
例如设计一个程序,以只读的方式打开系统配置文件“/etc/inittab”,则可以通过open、close函数进行文件的操作。程序代码如下:
/* open and close_/etc/inittab */ #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #defineSRC_FILE_NAME"/etc/inittab" /* 打开文件名 */ int main() { int fd; /* 以只读方式打开文件 */ fd = open(SRC_FILE_NAME, O_RDONLY); if (fd < 0) { printf("Open file error\n"); exit(1); } close(fd); return 0; }
表3-4 read()函数语法
系统调用read用于从文件描述符对应的文件中读取数据,fd必须是之前使用open函数调用返回的有效文件描述符,buf指定存储读出数据的缓冲区,count指定读取的字节数。如果调用成功,read函数返回成功读出的字节数,如果出错则返回-1,如果遇到EOF(文件末尾),则read返回0。
从刚才打开的系统配置文件“/etc/inittab”中读取100个字符并打印,则可以通过read函数进行文件的读取操作。程序代码如下:
/* read_/etc/inittab */ #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #defineSRC_FILE_NAME"/etc/inittab" /* 打开文件名 */ int main() { int fd, num; char buf[128] = {0} /* 以只读方式打开文件 */ fd = open(SRC_FILE_NAME, O_RDONLY); if (fd < 0) { printf("Open file error\n"); exit(1); } /* 读取文件前100个字符并打印 */ num = read(fd, buf, 100); if (num < 0) { printf("read file error\n"); exit(1); } printf("%s\n", buf); close(fd); return 0; }
表3-5 write()函数语法
系统调用write用于向文件描述符对应的文件写入数据,fd同样是之前使用open函数调用返回的有效文件描述符,buf是指向保存写入数据的缓冲区的指针,而count指定写入的字节数。如果调用成功,write返回成功写入的字节数,如果失败则返回-1。
小知识:read和write函数在进行文件读写后都会让文件的操作位置自动偏移到读取或写入之后的操作位置,例如用户连续调用多次读取操作,读取的内容是按照文件位置依次读取。
例如,打开的本目录文件“./test”写入字符串“helloworld”,则可以通过write函数进行文件的写入操作。程序代码如下:
/* write to ./test */ #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #defineDST_FILE_NAME"./test" /* 打开文件名 */ int main() { int fd, num; /* 以只读方式打开文件 */ fd = open(DST_FILE_NAME, O_RDWR); if (fd < 0) { printf("Open file error\n"); exit(1); } /* 把helloworld写入文件 */ num = write(fd, "helloworld", strlen("helloworld")); if (num < 0) { printf("write file error\n"); exit(1); } close(fd); return 0; }
表3-6 lseek()函数语法
函数lseek在用文件描述符fd对应的文件里,把文件指针设置到相对于whence值偏移offset的位置,文件指针是文件中执行读写操作的位置。其中whence可以有三个常量进行设置,SEEK_SET表示文件开头,SEEK_CUR表示文件当前操作位置,SEEK_END表示文件末尾,如果调用成功,返回新指针相对于文件头的偏移量,如果错误返回-1。
小知识:lseek函数经常用于获取文件的长度,即把文件的操作位置设置到SEEK_END位置后,lseek函数的返回值即为文件的大小。但要注意,如果后续还需要从文件读取或者写入内容,则先把文件的操作位置设置到文件头或者其他指定位置,否则后续的读取和写入操作会失败。
例如,计算/etc/inittab文件的长度时,可以通过lseek函数计算文件的长度并打印。程序代码如下:
/* get file length */ #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #defineDST_FILE_NAME"/etc/inittab" /* 打开文件名 */ int main() { int fd, len; /* 以只读方式打开文件 */ fd = open(DST_FILE_NAME, O_RDONLY); if (fd < 0) { printf("Open file error\n"); exit(1); } /* 把文件的位置移到最后,返回值即为文件大小 */ len = lseek(fd, 0, SEEK_END); if (num < 0) { printf("seek file error\n"); exit(1); } printf("file len = %d\n", len); close(fd); return 0; }
3.2.2 带缓存的I/O操作
前面所述的文件I/O操作是基于文件描述符的。这些都是基本的I/O控制,是不带缓存的。
本节说要介绍的I/O操作是基于流缓冲的,它是符合ANSI C的标准I/O处理,这里有很多读者已经非常熟悉的函数如:printf()、scanf()等。本节仅介绍最主要的函数。
带缓存的文件I/O操作是在内存中开辟一个“缓冲区”,为程序中的每一个文件使用。当执行读文件的操作时,从磁盘文件中将数据先读入内存“缓冲区”,装满后再从内存“缓冲区”依次读入接收的数据。反之亦然。
带缓存的文件I/O操作主要用到的函数如下。
1.函数说明
带缓存的文件I/O操作主要用到的函数见表3-7。
表3-7 带缓存的文件I/O操作主要用到的函数
2.函数格式
带缓存的文件I/O操作主要用到的函数格式及说明见表3-8和表3-19。
表3-8 fopen()函数语法
表3-9 mode取值说明
fopen函数以模式mode打开名为path的文件,需要注意Linux不区分文本模式和二进制模式。fopen返回一个文件指针,该指针可以传递给别的标准I/O函数来标识这个文件流,文件指针指向一个描述文件流的结构体,称之为文件句柄。出现错误的情况下,例如以只读方式打开一个不存在的文件时,fopen返回NULL。如果需要打开的文件不存在,则会以权限0666来创建该文件。
表3-10 fclose()函数语法
为了关闭文件流,调用函数fclose,fclose执行成功返回0,执行失败返回EOF。一旦文件由fclose关闭,任何试图对这个被关闭文件流的访问,包括其他fclose调用都会导致出现不可预料的结果。
例如以只读方式打开系统配置文件“/etc/passwd”文件,文件打开后打印文件句柄,并关闭文件。
/* fopen and fclose_sample */ #include <stdlib.h> #include <stdio.h> #define FILE_NAME"/etc/passwd"/* 源文件名 */ int main() { FILE *file; /* 以只读方式打开源文件 */ file = fopen(FILE_NAME, "r"); if (file == NULL) { printf("Open file error\n"); exit(1); } fclose(file); return 0; }
表3-11 fread()函数语法
函数fread允许从文件流读出数据,指针ptr指向的缓冲区保存fread从文件读出的数据,通常由stream指定要操作的数据流。size和nmemb分别控制读出的一条记录的大小和记录数。但是这里使用“记录”这个词对于Linux来说有点不太合适,因为Linux并不是以记录的方式完成读取操作的。可以理解为size指读取的字节数,而nmemb指读取多个单位的size。fread返回读入的记录数,返回值有可能比请求的值少,在出错的情况下,返回值通常比请求值小些,而且可能是负值。
例如,从刚打开的“/etc/passwd”文件中读取前100个字符并打印:
/* fread_sample */ #include <stdlib.h> #include <stdio.h> #define FILE_NAME"/etc/passwd"/* 源文件名 */ int main() { FILE *file; char buf[128] = {0}; int num; /* 以只读方式打开源文件 */ file = fopen(FILE_NAME, "r"); if (file == NULL) { printf("Open file error\n"); exit(1); } num = fread(buf, 100, 1, file); if (num < 0) { Printf("fread fail!\n"); exit(1); } printf("%s\n", buf); fclose(file); return 0; }
表3-12 fwrite()函数语法
函数fwrite允许向文件流写入数据,指针ptr指向的缓冲区保存写入文件的数据,通常由stream指定要操作的数据流。size和nmemb分别控制写入的一条记录的大小和记录数。但是同样这里使用“记录”这个词对于Linux来说也是不太合适,可以理解为size指写入的字节数,而nmemb指写入多个单位的size。fwrite返回写入的记录数,返回值有可能比请求的值少,在出错的情况下,返回值通常比请求值小些,而且可能是负值。
例如打开文件“./test2”,并在文件中写入“helloworld”字符串。
/* fwrite_sample */ #include <stdlib.h> #include <stdio.h> #define FILE_NAME"./test2"/* 文件名 */ int main() { FILE *file; char buf[128] = "helloworld"; int num; /* 以可写并可读方式打开源文件 */ file = fopen(FILE_NAME, "w+"); if (file == NULL) { printf("Open file error\n"); exit(1); } num = fwrite(buf, strlen(buf), 1, file); if (num < 0) { printf("fwrite fail!\n"); exit(1); } fclose(file); return 0; }
表3-13 fseek()函数语法
表3-14 ftell()函数语法
表3-15 rewind ()函数语法
文件定位函数设置文件内部的当前位置,这些函数对于没有指向普通文件的流,如套接口或者管道不起作用。fseek函数把当前位置设置到offset处,参数whence可以是SEEK_SET、SEEK_CUR和SEEK_END,这些值决定了是相对于文件的起始,还是文件的当前位置或者文件的末尾来计算偏移量offset。正常情况下,fseek返回值为0,错误时返回-1。而ftell函数只是简单地返回当前位置,rewind函数把文件指针位置设置为0,也就是说把文件指针设置到文件的起始位置。
例如打开“/etc/passwd”文件,计算文件长度,并一次性读取文件所有内容后打印该内容。
/* fseek/ftell/rewind_sample */ #include <stdlib.h> #include <stdio.h> #define FILE_NAME"/etc/passwd"/* 源文件名 */ int main() { FILE *file; char *buf; int num, len; /* 以只读方式打开源文件 */ file = fopen(FILE_NAME, "r"); if (file == NULL) { printf("Open file error\n"); exit(1); } fseek(file, 0, SEEK_END); len = ftell(file); if (len < 0) { printf("ftell fail!\n"); exit(1); } rewind(file); buf = malloc(len); num = fread(buf, len, 1, file); if (num < 0) { printf("fread fail!\n"); exit(1); } printf("%s\n", buf); fclose(file); return 0; }
表3-16 fgetc()函数语法
表3-17 fputc()函数语法
表3-18 fgets()函数语法
表3-19 fputs()函数语法
函数fgetc和fputc对文件进行读/写操作时每次读取或者写入一个字符,fgetc的返回值保存了读取的内容,注意此处使用整型数据存储这个字符,fputc函数写入的字符内容也存储在整型类型数据中,整型类型数据中的最后一个字节保存字符的值,使用过程中如果需要可以直接强制类型转换为字符型使用。函数fgets和fputs是基于行的输入/输出,以文件中'\n'字符作为行结束符,和Windows系统中文本文件的行结束符"\r\n"不同,是进行文本文件读/写操作的常用函数。
例如,打开“/etc/passwd”文件,依次读取每行内容并打印该内容。
/* fgets/fputs_sample */ #include <stdlib.h> #include <stdio.h> #define FILE_NAME"/etc/passwd"/* 源文件名 */ int main() { FILE *file; char buf[128], *plin; /* 以只读方式打开源文件 */ file = fopen(FILE_NAME, "r"); if (file == NULL) { printf("Open file error\n"); exit(1); } while (1) { plin = fgets(buf, 128, file); if (plin == NULL) { break; } printf("%s\n", buf); } fclose(file); return 0; }