.
.
.
.
.
最近在学习 APUE,所以顺便将每日所学记录下来,一方面为了巩固学习的知识,另一方面也为同样在学习APUE的童鞋们提供一份参考。
本系列博文均根据学习《UNIX环境高级编程》一书总结而来,如有错误请多多指教。
APUE主要讨论了三部分内容:文件IO、并发、进程间通信。
文件IO:
标准IO:优点是可移植性高,缺点是性能比系统 IO 差,且功能没有系统 IO 丰富。
系统IO:因为是内核直接提供的系统调用函数,所以性能比标准 IO 高,但是可移植性比标准 IO 差。
并发:
信号 + 多进程;
多线程;
进程间通信:
FIFO:管道;
System V:又称为 XSI,支持以下三种方式:
msg:消息队列;
sem:信号量;
shm:共享存储;
Socket:套接字(网络通信);
本系列博文就是围绕着这些内容进行学习和总结出来的,但是APUE一书讲述的主要是 Unix 环境,而 LZ 用的是 Linux 环境,所以本系列博文的所有内容都是基于 Linux 环境的,仅供各位童鞋参考。LZ 尽量多讲原理,少讲函数的具体使用,在使用 LZ 提到的函数的时候,如与各位开发环境的 man 手册冲突,则以 man 手册为准。
==============================华丽的分割线==============================
好了,通过简单的介绍相信大家对 APUE 的结构已经有了一个大致的了解了,接下来就开始今天的正题:文件 IO。
今天讲的所有的 IO 操作都是标准 IO,如果是方言我会单独标识出来。
首先要了解的一个概念是文件位置指针。
当我们打开一个文件要对它进行读写的时候,我们怎么能知道要从哪里开始读(写)文件呢?其实标准库准备了一个工具辅助我们读写文件,它就是文件位置指针。当我们使用标准库函数操作文件的时候,它会自动根据文件位置指针找到我们要操作的位置,也会随着我们的读写操作而自动修改指向,而不用我们自己手动记录和修改文件的操作位置。它使用起来非常方便,以至于你完全感觉不到它的存在,但是为了更好的理解文件 IO,你必须知道它的作用。
1.fopen(3)
1 fopen - stream open functions 2 3 #include <stdio.h> 4 5 FILE *fopen(const char *path, const char *mode);
这是今天要学习的第一个函数,在操作文件之前,我们需要通过 fopen() 函数将文件打开,通过这个函数我们可以告诉操作系统我们要操作的是哪个文件,以及用什么样的方式操作这个文件。
参数列表:
path:要操作的文件路径。
mode:文件的打开方式,这个打开方式一共分为6种。
r:以只读的方式打开文件,并且文件位置指针会被定位到文件首。如果要打开的文件不存在则报错。
r+:以读写的方式打开文件,并且文件位置指针会被定位到文件首。如果要打开的文件不存在则报错。
w:以只写的方式打开文件,如果文件不存在则创建,如果文件已存在则被截断为 0 字节,并且文件位置指针会被定位到文件首。
w+:以读写的方式打开文件,如果文件不存在则创建,如果文件已存在则被截断为 0 字节,并且文件位置指针会被定位到文件首。
a:以追加的方式打开文件,如果文件不存在则创建,且文件位置指针会被定位到文件最后一个有效字符的后面(EOF,end of the file)。
a+:以读和追加的方式打开文件,如果文件不存在则创建,且读文件位置指针会被初始化到文件首,但是总是写入到最后一个有效字符的后面(EOF,end of the file)。
返回值:
FILE 是一个由标准库定义的结构体,各位童鞋不要企图通过手动修改结构体里的内容来实现文件的操作,一定要通过标准库函数来操作文件。
这个函数返回一个 FILE 类型的指针,它作为我们打开文件的凭据,后面所有对这个文件的操作都需要使用这个指针,而且使用之后一定不要忘记调用 fclose(3) 函数释放资源。
如果该函数返回了一个指向 NULL 的指针,则表示文件打开失败了,可以通过 errno 获取到具体失败的原因。
2.fclose(3)
1 fclose - close a stream 2 3 #include <stdio.h> 4 5 int fclose(FILE *fp);
这个函数是与 fopen(3) 函数对应的,当我们使用完一个文件之后,需要调用 fclose(3) 函数释放相关的资源,否则会造成内存泄漏。当一个 FILE 指针被 fclose(3) 函数成功释放后,这个指针所指向的内容将不能再次被使用,如果需要再次打开文件还需要调用 fopen(3) 函数。
参数列表:
fp:fopen(3) 函数的返回值作为参数传入即可。
3.fgets(3)
1 fgets - input of strings 2 3 #include <stdio.h> 4 5 int fgetc(FILE *stream); 6 7 char *fgets(char *s, int size, FILE *stream);
从输入流 stream 中读取一个字符串回填到 s 所指向的空间。
这里出现了一个 stream 的概念,这个 stream 是什么呢,它被成为“流”,其实就是操作系统对于可以像文件一样操作的东西的一种抽象。它并非像自然界的小河流水一样潺潺细流,而通常是要么没有数据,要么一下子来一坨数据。当然 stream 也未必一定就是文件,比如系统为每个进程默认打开的三个 stream:stdin、stdout、stderr,它们本身就不是文件,就是与文件有着相同的操作方式,所以同样被抽象成了“流”。
这个函数并没有解决 gets(3) 函数可能会导致的数组越界问题,而是通过牺牲了获取数据的正确性来保证程序不会出现数组越界的错误,实际上是掩盖了 gets(3) 的问题。
该函数遇到如下四种情况会返回:
1.当读入的数据量达到 size - 1 时;
2.当读取的字符遇到 \n 时;
3.当读取的字符遇到 EOF 时;
4.当读取遇到错误时;
并且它会在读取到的数据的最后面添加一个 \0 到 s 中。
返回值:
成功时返回 s。
返回 NULL 时表示出现了错误或者读到了 strem 的末尾(EOF)。
4.fread(3)、fwrite(3)
1 fread, fwrite - binary stream input/output 2 3 #include <stdio.h> 4 5 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream); 6 7 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
这两个函数使用得最频繁,用来读写 stream,通常是用来读写文件。
参数列表:
ptr:fread(3) 将从 stream 中读取出来的数据回填到 ptr 所指向的位置;fwrite(3) 则将从 ptr 所只想的位置读取数据写入到 stream 中;
size:要读取的每个对象所占用的字节数;
nmemb:要读取出多少个对象;
stream:数据来源或去向;
返回值:
注意这两个函数的返回值表示的是成功读(写)的对象的个数,而不是字节数!
例如:
read(buf, 1, 10, fp); // 读取 10 个对象,每个对象 1 个字节
read(buf, 10, 1, fp); // 读取 1 个对象,每个对象 10 个字节
当数据量充足的时候,这两种方式是没有区别的。
但是!!当数据量少于 size 个字节的整倍数时,第二种方法的的最后一个对象会读取失败。比如数据只有 45 个字节,那么第二种方法的返回值为 4,因为它只能成功读取 4 个对象。
所以通常第一种方式读写数据使用得比较普遍。
5.atoi(3)
1 atoi, atol, atoll, atoq - convert a string to an integer 2 3 #include <stdlib.h> 4 5 int atoi(const char *nptr); 6 long atol(const char *nptr); 7 long long atoll(const char *nptr); 8 long long atoq(const char *nptr);
atoi(3) 函数族在这里提一下,主要是为了下面的 printf(3) 函数族做一个铺垫。
这些函数的作用是方便的将一个字符串形式的数字转换为对应的数字类型的数字。
上面这句话可能有点坳口,给你看个例子就懂了,下面是伪代码。
1 char *str = "123abc456"; 2 int i = 0; 3 i = atoi(str);
i 的结果会变成 123。这些函数会转换一个字符串中地一个非有效数字前面的数字。如果很不幸这个字符串中的第一个字符就不是一个有效数字时,那么它们会返回 0。
6.printf(3)
1 printf, fprintf, sprintf, snprintf - formatted output conversion 2 3 #include <stdio.h> 4 5 int printf(const char *format, ...); 6 int fprintf(FILE *stream, const char *format, ...); 7 int sprintf(char *str, const char *format, ...); 8 int snprintf(char *str, size_t size, const char *format, ...);
printf(3) 函数大家一定不会陌生了,应该从写 Hello World! 的时候就接触到了的吧,所以我也不多介绍了,主要介绍两个内容。
一个是面试常考的一个问题,用了这么久的 printf(3) 函数,大家有没有注意过它的返回值表示什么呢?
printf(3) 的返回值表示成功打印的有效字符数量,不包括 \0。
另一个要说的就是刚才我们提到了 atoi(3) 函数族,它们负责将字符串转换为数字,那么有没有什么函数可以将数字转换为字符串呢,其实通过 sprintf(3) 或 snprintf(3) 就可以了。
有了这两个函数,不仅可以方便的将数字转换为字符串,还可以将多个字符串任意拼接为一个完整的字符串。
这里直接讲解一下 snprintf(3) 函数。
参数列表:
str:拼接之后的结果会回填到这个指针所指向的位置;
size:size - 1 为回填到 str 中的最大长度,数据超过这个长度的部分则会被舍弃,然后会在拼接的字符串的尾部追加 \0;
format:格式化字符串,用法与 printf(3) 相同,这里不再赘述;
...:格式化字符串的参数,用法与 printf(3) 相同;
这个函数与 fputs(3) 一样,只是掩盖了 sprintf(3) 可能会导致的数组越界问题,通过牺牲数据的正确性来保证程序不会出现数组越界的错误。
7.scanf(3)
1 scanf, fscanf, sscanf - input format conversion 2 3 #include <stdio.h> 4 5 int scanf(const char *format, ...); 6 int fscanf(FILE *stream, const char *format, ...); 7 int sscanf(const char *str, const char *format, ...);
scanf(3) 函数族相信也不用过多的介绍了,这里唯一要强调的就是:scanf(3) 函数支持多种格式化参数,唯独 %s 是不能安全使用的,可能会导致数组越界,所以当需要接收用户输入的时候可以使用 fgets(3) 等函数来替代。
8.fseek(3)
1 fgetpos, fseek, fsetpos, ftell, rewind - reposition a stream 2 3 #include <stdio.h> 4 5 int fseek(FILE *stream, long offset, int whence); 6 7 long ftell(FILE *stream); 8 9 void rewind(FILE *stream);
fseek(3) 函数族的函数用来控制和获取文件位置指针所在的位置,从而能够使我们灵活的读写文件。
介绍一下 fseek(3) 函数的参数列表:
stream:这个已经不需要多介绍了吧,就是准备修改文件位置指针的文件流;
offset:基于 whence 参数的偏移量;
whence:相对于文件的哪里;有三个宏定义可以作为它的参数:SEEK_SET(文件首), SEEK_CUR(当前位置), or SEEK_END(文件尾);
返回值:
成功返回 0;失败返回 -1,并且会设置 errno。
单独看参数列表也许你还有所疑惑,那么我写点简单的伪代码作为例子:
1 fseek(fp, -10, SEEK_CUR); // 从当前位置向前偏移10个字节。 2 fseek(fp, 2GB, SEEK_SET); // 可以制造一个空洞文件,如迅雷刚开始下载时产生的文件。
ftell(3) 函数以字节为单位获得文件指针的位置。
fseek(fp, 0, SEEK_END) + ftell(3) 可以计算出文件总字节大小。
还有一个值得大家注意的问题:
fseek(3) 和 ftell(3) 的参数和返回值使用了 long,所以取值范围为 -2GB ~ (2GB-1),而 ftell(3) 只能表示 2G-1 之内的文件大小,所以可以使用 fseeko(3) 和 ftello(3) 函数替代它们,但它们只是方言(SUSv2, POSIX.1-2001.)。
由于这两个函数比较古老,所以设计的时候认为 +-2GB 的取值范围已经足够用了,而没有意识到科技发展如此迅速的今天,2GB 大小的文件已经完全不能满足实际的需求了。
rewind(3) 函数将文件位置指针移动到文件起始位置,相当于:
1 (void) fseek(stream, 0L, SEEK_SET)
9.getline(3)
1 getline - delimited string input 2 3 #include <stdio.h> 4 5 ssize_t getline(char **lineptr, size_t *n, FILE *stream); 6 7 Feature Test Macro Requirements for glibc (see feature_test_macros(7)): 8 9 getline(): 10 Since glibc 2.10: 11 _POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700 12 Before glibc 2.10: 13 _GNU_SOURCE
这个函数是一个非常好用的函数,它能帮助我们一次获取一行数据,而无论这个数据有多长。
参数列表:
lineptr:一个一级指针的地址,它会将读取到的数据填写到一级指针指向的位置,并将>该位置回填到该参数中。指针初始必须置为NULL,该函数会根据指针是否为 NULL 来决定是否需要分配新的内存。
n:是由该函数回填的申请的内存缓冲区的总大小,长度初始必须置为0。
虽然很好用,但是各位童鞋别高兴得太早了,该函数仅支持 GNU 标准,所以是方言,大家还是自己封装一个备用吧。
另外,想要使用这个函数必须在编译的时候指定 -D_GNU_SOURCE 参数:
1 $> gcc -D_GNU_SOURCE
当然如果不想在编译的时候添加参数,也可以在引用头文件之前 #define _GNU_SOURCE,只是比较丑陋而已。
还有一个办法,是在 makefile 中配置 CFLAGS += -D_GNU_SOURCE,这样即省去了编译时手动写参数的麻烦,也避免了代码中的丑陋。
好了,时间不早了,今天先写到这里。