流和文件描述符
当需要向文件输入或输出时,有两种基本机制表示程序和文件之间的连接:文件描述符(file descriptor)和流(stream)。文件描述符表示为int类型的对象,流表示为 FILE * 类型的对象。
文件描述符为输入和输出操作提供了原始的低级接口。文件描述符和流都可以作为终端与设备进行连接,也可以用于普通文件与进程之间的通信,通信方法包括管道和socket。针对某种特定类型的设备,如果要执行某个具体的控制操作,则必须使用文件描述符,这种情况下设备不提供流的工作方式。如果程序需要在某种特殊模式下输入输出,比如非阻塞(或轮询)输入,同样需要使用文件描述符。
相比于文件描述符,流提供了更高层次的接口。流接口对各种文件的处理非常相似,唯一的例外是三种缓冲(buffer)样式有所不同。三种缓冲样式分别为:无缓冲流、行缓冲流和全缓冲流。无缓冲流的特点是字符写入文件时独立传输,字符之间没有联系;当遇到换行符时,写入行缓冲流的字符会以块的形式传输到文件;全缓冲流的字符能够组成任意大小的块写入文件。
流接口的主要优点是,用于输入输出的函数集比文件描述符的相应功能更丰富,更强大,如格式化输入输出功能(printf 和 scanf函数)、面向字符和面向行的输入输出功能。文件描述符接口仅提供用于传输字符块的简单功能,而且文件描述符的可移植性不如流,非GNU系统可能根本不支持文件描述符。
流和线程
由于历史原因,C数据结构中表示流的类型被称为FILE,而不是stream。FILE类型在头文件stdio.h中被定义。
流在多线程应用程序中的使用方式与在单线程应用程序中的使用方式相同。POSIX标准要求缺省情况下流操作是原子的,体现在:当两个线程同时向一个流发出操作时,操作会依次执行,就像有先后顺序一样;当执行读或写操作时,缓冲区受保护,不执行同一流的其他操作。但函数本身将仅确保其自身操作的原子性,而不确保所有函数调用的原子性,如果程序需要原子调用多个流函数,需要在应用程序代码中执行流锁定。
文本流和二进制流
打开一个流时,可以指定使用文本流(text stream)或二进制流(binary stream)。
FILE * fopen (const char *filename, const char *opentype)
opentype中的字符“b”表示请求二进制流而不是文本流,但在POSIX系统(包括GNU系统)中没有区别。fopen函数默认以文本流形式打开文件。
文本流和二进制流在几个方面有所不同:
- 从文本流读取的数据会被换行符 ‘\n’ 分成不同行,而二进制流则只是一长串字符。在某些系统上,文本流可能无法处理多于254个字符的行。
- 在某些系统上,文本文件只能包含打印字符、水平制表符和换行符,因此文本流可能不支持其他字符。但是,二进制流可以处理任何字符。
- 再次读入文件时,在文本流中紧接换行符之前写入的空格字符可能会消失。
- 一般而言,从文本流读取或写入文本流的字符与实际文件中的字符之间不需要一对一的映射。
二进制流总是比文本流更强大,但文本流依然有自己存在的价值。在一些操作系统上,文本流与二进制流具有不同的文件格式,当面向文本的程序需要读写一个普通文本文件时,文本流依然是唯一的选择。
通过一个简单的实验,研究Linux上文本IO流和二进制IO流的区别。
文本流程序内容如下:
#include<stdio.h>
int main(void)
{
int num=12345;
FILE * fp;
fp=fopen("file.txt","w");
fprintf(fp,"%d",num);
fclose(fp);
return 0;
}
二进制流程序内容如下:
#include<stdio.h>
int main(void)
{
int write_var=12345,read_var;
FILE * fp;
fp=fopen("binary.txt","w+");
fwrite(&write_var,sizeof(int),1,fp);
rewind(fp);
fread(&read_var,sizeof(int),1,fp);
printf("%d\n",read_var);
fclose(fp);
return 0;
}
分别运行两个程序,文本IO流程序生成file.txt文件;二进制IO流程序生成binary.txt文件,同时在命令行显示“12345”。两个程序都是将整数12345写入文件,但实际上是不一样的。
(1)生成文件的内容不同。
# cat file.txt
12345
# cat binary.txt
90
cat命令查看文件时,以ASCII字符的格式显示内容。因为fprintf函数是文件IO函数,存储整数12345的时候是分别把字符 ‘1’、‘2’、‘3’、‘4’、'5’的二进制编码写入文件中,所以读取的时候还是12345。而fwrite将整数12345的二进制编码0011 0000 0011 1001写入文件,因为本实验的主机使用大端存储的方式,所以二进制的低8位被放到内存的高地址上,高位放到了内存低地址上,以字符格式读取的时候,0011 1001被解析成ASCII的字符9,0011 0000被解析成ASCII的字符0,最后显示90。
(2)生成文件的大小不同。
# ll file.txt binary.txt
-rw-r--r--. 1 root root 4 May 29 15:39 binary.txt
-rw-r--r--. 1 root root 5 May 29 09:48 file.txt
文件IO生成的file.txt存储的内容为5个字符,因为在本实验中,每个char字符占用1个字节,每个int型整数占用4个字节,所以file.txt大小为5字节。
二进制编码0011 0000 0011 1001需要占用2字节的存储空间,但在fwrite函数中,明确定义了使用sizeof(int)大小的空间来存放变量write_var,所以binary.txt的大小为4字节,binary.txt后面的两个字节使用字符^@进行填充,该字符在ASCII表中的值为0,表示NULL。
# cat -A binary.txt
90^@^@
(3)生成文件的格式不同。将二进制程序fopen函数的 w+ 参数改为 a+,以便追加内容,重新编译并执行两次,此时,binary.txt的内容为:
# cat -A binary.txt
90^@^@90^@^@
使用file命令查看格式,file命令一般将文件分为3种类型:文本文件、可执行文件和数据。binary.txt的文件类型为data,可以很清楚的看到二进制流与文本流的区别。
# file binary.txt file.txt
binary.txt: data
file.txt: ASCII text, with no line terminators
参考文献
[1]Ian Darwin等.file(1) - Linux man page[EB/OL].https://linux.die.net/man/1/file,1973-11-01.
[2]GNU.The GNU C Library[EB/OL].https://www.gnu.org/software/libc/manual/html_node/index.html,2020-01-01.
[3]Stephen Prata.C Primer Plus(第五版)中文版[M].人民邮电出版社:北京,2014:354.