C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括缓冲区和文件描述符。而文件描述符是文件描述符表的一个索引,也就是说c语言的文件指针是Linux系统中对文件描述符的一种封装。
FILE结构体
文件指针指向一个结构体
文件指针结构体包含文件描述符
文件指针是有C库函数使用,文件描述符由系统调用使用
FILE结构体里面还有缓冲区
优点:一、方便程序员使用;二、可以提高程序的移植性。
文件描述符
在Linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。
标准输入(stdin)的文件描述符是 0 STDIN_FILENO
标准输出(stdout)的文件描述符是 1 STDOUT_FILENO
标准错误(stderr)的文件描述符是 2 STDERR_FILENO
文件描述符的分配规则:从当前未被分配的最小整数处分匹配。
下面看文件file结构体
优点
兼容POSIX标准,许多Linux和UNIX系统调用都依赖于它。
缺点
文件描述符的概念存在两大缺点 :
一、在非UNIX / Linux操作系统上(如Windows NT),无法基于这一概念进行编程。
二、由于文件描述符在形式上不过是个整数,当代码量增大时,会使编程者难以分清哪些整数意味着数据,那些意味着文件描述符。因此,完成的代码可读性也就会变得很差。
文件指针和文件描述符
文件指针:
概念:一个指针遍历指向文件,通过文件指针获取该文件的信息,在C语言中,通常使用文件指针作为I/O句柄
一般形式:FILE* 指针变量标识符,常用FILE* fp
头文件:#include
文件指针本质是一个结构体,是由系统自定义的,所以结构体不需要自己定义,我们可以直接调用
例如:
//构建一个文件指针
FILE* fp =nullptr;
fp=fopen(file.c_str(),”rb”);
if(fp == nullptr) //判断文件是否打开
文件指针其实不难理解,而是涉及操作也很简单,主要是和I/O流中一些函数一起使用,那么接下来要看一看文件描述符
文件描述符:
实际文件描述符是文件指针结构体中的一员,也就是说在Linux系统中文件指针是文件描述符的封装,文件描述符是作为该结构体的一个系统调用接口。
虽然系统已经定义了文件描述符,哪有为什么给文件描述符定义结构体呢?
封装的好处:
1.方便使用
2.有利于提高代码的可植性
文件描述符在Linux下的使用原理:
在Linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB
中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。
文件描述符分配规则:
从最小一位没有被分配的整数开始分配
标准输入(stdin)的文件描述符是 0
标准输入(stdout)的文件描述符是 1
标准输入(stderr)的文件描述符是 2
注:文件描述符的有效范围是 0 到 OPEN_MAX。一般来说,每个进程最多可以打开64 个文件(0 — 63)。对于 FreeBSD 5.2.1、Mac OS X 10.3 和 Solaris 9 来说,每个进程最多可以打开文件的多少取决于系统内存的大小,int 的大小,以及系统管理员设定的限制。Linux 2.4.22 强制规定最多不能超过 1, 048, 576 。
文件描述符的优点以及缺点:
优点:兼容POSIX标准,在UNIX、Linux的系统调用中,大量的系统调用都是依赖于文件描述符。
缺点:
1、仅适用于Unix/Linux
2、文件描述符为整数表示,当代码量增大时,难以区分数据与文件描述符,因此会降低代码可读性
1.文件描述符fd的定义:文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
2.文件指针FILE定义说明文件指针的一般形式为:
FILE *指针变量标识符;
其中FILE应为大写,它实际上是由系统定义的一个结构,该结构中含有文件名、文件状态和文件当前位置等信息。在编写源程序时不必关心FILE结构的细节。
使用系统调用的时候用文件描述符的时候比较多,但是操作比较原始。C库函数在I/O上提供了一些方便的包装(比如格式化I/O、重定向),但是对细节的控制不够。
如果过度依赖其中的一种只会徒增麻烦,所以知道两者的转换是很有必要的。FILE*是对fd的封装
当然,有人会说知道文件路径的话重新打开就是了,但是这会产生竞争条件(Race Conditions),首先重新打开文件,相当于是2个fd指向同一文件,然后如果在打开的期间文件被删除了又被新建了一个同名文件,2个fd指向的便是不同的文件。
glibc库提供了两个转换函数fdopen(3)和fileno(3),都是
FILE *fdopen(int fd, const char *mode);
int fileno(FILE *stream);
PS:为了节省篇幅,还是继续忽略返回值的检查。
来看看测试吧,是不是我们想的那样。
#include
#include
#include
int main()
{
const char* filename = “new.txt”;
int fd = open(filename, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
FILE* fp = fdopen(fd, “w+”);
int fd2 = fileno(fp);
printf(“fd=%d | fd2=%d\n”, fd, fd2); |
fclose(fp);
close(fd);
return 0;
}
$ gcc test.c
$ ./a.out
fd=3 | fd2=3
参考fileno手册:
The function fileno() examines the argument stream and returns its integer descriptor.
FILE是对fd的封装,fileno()是直接取得被封装的fd,因此并未创建新的fd指向该文件。
参考fdopen手册:
The fdopen() function associates a stream with the existing file descriptor, fd. The mode of
the stream (one of the values “r”, “r+”, “w”, “w+”, “a”, “a+”) must be compatible with the
mode of the file descriptor.
fdopen()是讲流(FILE对象)与已存在的文件描述符fd进行关联,因此也是未创建新的fd。值得注意的是,FILE指针的模式(mode)必须与文件描述符的模式兼容。
关于mode参数先搁置会儿,目前我们知道的是,使用fileno和fdopen进行转换,都是在原有的fd上进行操作,并未产生新的fd。那么,再次审视刚才的代码,是否发现了问题?
我们来检查下close(fd)的返回值,把close(fd)改成下列代码
if (-1 == close(fd)) {
perror(“close”);
exit(1);
}
$ gcc test.c
$ ./a.out
close: Bad file descriptor
没错,fclose在关闭文件指针的时候,内部其实也关闭了文件描述符(否则资源就泄露了),既然这里fp内部的文件描述符和fd是同一个,当fp被关闭时,fd也被关闭了,再次关闭fd就会出现“损坏的文件描述符”错误。
OK,现在回顾下fopen的第2个参数,又r/r+/w/w+/a/a+一共6种设置(windows平台的rb/rb+/wb/wb+暂且不谈),对比Linux手册我将对应的open设置列出来
依然是进行测试,修改fd_mode和fp_mode,看看实验结果
#include
#include
#include
#include
const int security = S_IRUSR | S_IWUSR;
const int fd_mode = O_RDWR | O_CREAT | O_TRUNC;
const char* fp_mode = “r”;
int main()
{
int fd = open(“new.txt”, fd_mode, security);
FILE* fp = fdopen(fd, fp_mode);
if (fp == NULL) {
perror(“fdopen”);
exit(1);
}
close(fd);
return 0;
}
在fd_mode等价于”w+”时,fp_mode的6种设置(r/r+/w/w+/a/a+)均返回非空指针。
在fd_mode等价于”w”时,fp_mode6种设置只有”a”和”w”返回非空指针。
继续尝试”r”/”r+”/”a”/”a+”的设置,可以发现所谓“兼容”只与读写权限有关,O_RDWR兼容O_RDONLY和O_WRONLY,而后两者则只与自身兼容。
有意思的是O_APPEND(在末尾添加)和O_TRUNC(截断文件从头添加)也兼容。
The file position indicator of the new stream is set to that
belonging to fd, and the error and end-of-file indicators are cleared. Modes “w” or “w+” do
not cause truncation of the file. The file descriptor is not dup’ed, and will be closed when
the stream created by fdopen() is closed.
继续查看fdopen的手册内容,可以看到”w”和”w+”在这里不会导致文件截断。
后一句也印证了我们前面的实验结果:文件描述符不会被复制,文件指针被关闭时文件描述符也会被关闭。
PS:其实fdopen的手册上还有最后一句:The result of applying fdopen() to a shared memory object is undefined.
将fdopen用于共享内存对象的结果是未定义的。
文件指针和文件描述符的区别
在linux系统中把设备和普通文件也都看做是文件,要对文件进行操作就必须先打开文件,打开文件后会得到一个文件描述符,它是一个很小的正整数,是一个索引值。
内核会为每一个运行中的进程在进程控制块pcb中维护一个打开文件的记录表,每一个表项都有一个指针指向打开的文件,上边的索引值是记录表的索引值。
文件描述符的优点:兼容POSIX标准,许多系统调用都依赖于它;缺点是不能移植到unix之外的系统上去。
文件指针:c语言中使用的是文件指针而不是文件描述符来作为I/O的句柄,文件指针指向进程的用户空间中一个FILE结构的数据结构,FILE结构里主要包括一个I/O缓冲区和一个文件描述符,而文件描述符值是文件描述符表中的一个索引,从某种意义上将文件指针就是句柄的句柄(在Window中文件描述符被称为文件句柄)。
文件指针的优点:是c语言中的通用格式,便于移植。
既然FILE结构中含有文件描述符,那么可以使用fopen来获得文件指针,然后从文件指针获取文件描述符,文件描述符应该是唯一的,而文件指针却不是唯一的,但指向的对象是唯一的。
C语言文件指针域文件描述符之间可以相互转换
int fileno(FILE * stream)
FILE * fdopen(int fd, const char * mode)
FILE的结构
struct _iobuf {
char *_ptr; //缓冲区当前指针
int _cnt;
char *_base; //缓冲区基址
int _flag; //文件读写模式
int _file; //文件描述符
int _charbuf; //缓冲区剩余自己个数
int _bufsiz; //缓冲区大小
char *_tmpfname;
};
typedef struct _iobuf FILE;
open和fopen的区别
open | fopen |
---|---|
open返回一个文件描述符 | 返回FILE * |
无缓冲 | 有缓冲 |
与write()和read()配合使用 | 与fwrite()和fread()配合使用 |
int fd = open(“TAGS”, O_RDONLY);
FILE * fp = fopen(“TAGS”, “r”);
printf(“%d %d\n”, fd, fp->_file);
可以从文件指针中获取文件描述符
fileno(fp) 和fp->_file效果是一样的
FILE * fp = fdopen(fd, “r”) 根据文件描述符加上访问模式可以得到文件指针