C++学习---cstdio的源码学习分析08-重新打开文件流函数freopen

时间:2022-12-18 13:00:30

cstdio中的文件访问函数

stdio.h中定义了一系列文件访问函数(fopen,fclose,fflush,freopen,setbuf,setvbuf),接下来我们一起来分析一下freopen对应的源码实现。

- fopen:打开文件

- fclose:关闭文件

- fflush:刷新文件流

- freopen:重新打开文件流(不同的文件或访问模式)

- setbuf:设置stream buf

- setvbuf:改变stream buf

重新打开文件流函数freopen

重新打开一个文件,文件名为filename,模式为mode,文件流对象为stream;

如果filename为空,那说明,将原有的流使用新的mode打开,比如,原来只读,现在修改为可读可写。

FILE * freopen ( const char * filename, const char * mode, FILE * stream );

这个函数常用来重定位标准流​​stdin​​, ​​stdout​​ and ​​stderr​​到特定的文件中,如下的例子:

将标准输出定位到myfile.txt中,然后调用printf之后,实际打印的结果也是到了myfile.txt文件中。

/* freopen example: redirecting stdout */
#include <stdio.h>
int main ()
{
freopen ("myfile.txt","w",stdout);
printf ("This sentence is redirected to a file.");
fclose (stdout);
return 0;
}

函数入口分析

这里我们只截取了freopen64的函数定义,其实与freopen的实现基本一致,我们就对freopen进行分析

//glibc/libio/stdio.h
282 #ifdef __USE_LARGEFILE64
283 extern FILE *fopen64 (const char *__restrict __filename,
284 const char *__restrict __modes)
285 __attribute_malloc__ __attr_dealloc_fclose __wur;
286 extern FILE *freopen64 (const char *__restrict __filename,
287 const char *__restrict __modes,
288 FILE *__restrict __stream) __wur;
289 #endif

实现逻辑在对应的.c文件glibc/libio/freopen64.c

35 FILE *                                                                                                               |  bug-ungetc4.c
36 freopen64 (const char *filename, const char *mode, FILE *fp) | bug-ungetwc1.c
37 {

函数逻辑分析

1.局部变量定义与参数检查

38   FILE *result = NULL;                                                                                               |  bug-wfflush.c
39 struct fd_to_filename fdfilename; | bug-wmemstream1.c
40 | bug-wsetpos.c
41 CHECK_FILE (fp, NULL);

fd_to_filename被定义为如下的字符数组,FD_TO_FILENAME_PREFIX是"/proc/self/fd/"字符串的长度,INT_STRLEN_BOUND(int)是表示整数类型的或表达式T的字符串长度限制,两者加起来即是最长的fd文件路径。

//glibc/sysdeps/generic/fd_to_filename.h
25 struct fd_to_filename | fputc_u.c
26 { | fputwc.c
27 /* A positive int value has at most 10 decimal digits. */ | fputwc_u.c
28 char buffer[sizeof (FD_TO_FILENAME_PREFIX) + INT_STRLEN_BOUND (int)]; | freopen.c
29 };

//glibc/sysdeps/unix/sysv/linux/arch-fd_to_filename.h
19 #define FD_TO_FILENAME_PREFIX "/proc/self/fd/"

//glibc/include/intprops.h
111 /* Bound on length of the string representing an integer type or expression T.
112 T must not be a bit-field expression.
113
114 Subtract 1 for the sign bit if T is signed, and then add 1 more for
115 a minus sign if needed.
116
117 Because _GL_SIGNED_TYPE_OR_EXPR sometimes returns 1 when its argument is
118 unsigned, this macro may overestimate the true bound by one byte when
119 applied to unsigned types of size 2, 4, 16, ... bytes. */
120 #define INT_STRLEN_BOUND(t) \
121 (INT_BITS_STRLEN_BOUND (TYPE_WIDTH (t) - _GL_SIGNED_TYPE_OR_EXPR (t)) \
122 + _GL_SIGNED_TYPE_OR_EXPR (t))
//细节实现部分可以查看如下的代码:
//glibc/include/intprops.h中相关宏的实现
//这里就不细节展开了。

CHECK_FILE检查文件流指针的合法性,不合法返回NULL

865 #ifdef IO_DEBUG
866 # define CHECK_FILE(FILE, RET) do { \
867 if ((FILE) == NULL \
868 || ((FILE)->_flags & _IO_MAGIC_MASK) != _IO_MAGIC) \
869 { \
870 __set_errno (EINVAL); \
871 return RET; \
872 } \
873 } while (0)
874 #else
875 # define CHECK_FILE(FILE, RET) do { } while (0)
876 #endif

2.调用_IO_SYNC将文件流对象中未写入的信息写回

_IO_SYNC的调用细节可以参考前一篇文章fflush的分析

43   _IO_acquire_lock (fp);
44 /* First flush the stream (failure should be ignored). */
45 _IO_SYNC (fp);

3.原有文件流指针如果不是FILEBUF,那么直接返回结果

47   if (!(fp->_flags & _IO_IS_FILEBUF))
48 goto end;
...
92 end:
93 _IO_release_lock (fp);
94 return result;
95 }

4.获取当前真正要打开的文件名

50   int fd = _IO_fileno (fp);
51 const char *gfilename
52 = filename != NULL ? filename : __fd_to_filename (fd, &fdfilename);

首先通过_IO_fileno获取当前文件流的fd,实际上也就是获取文件流对象的_fileno成员值;

69 #define _IO_fileno(FP) ((FP)->_fileno)

根据前面我们说的函数的作用,需要判断当前传入的filename是否等于空,如果不等于空,那就打开filename,否则我们需要通过__fd_to_filename得到当前文件流对象的filename。

__fd_to_filename的作用就是生成fd对应的文件名,前缀是FD_TO_FILENAME_PREFIX,后面的fd number调用_fitoa_word转换为字符串,最后填写'\0',即如果fd为33,那么filename为”/proc/self/fd/33“

//glibc/sysdeps/generic/fd_to_filename.h
31 /* Writes a /proc/self/fd-style path for DESCRIPTOR to *STORAGE and
32 returns a pointer to the start of the string. DESCRIPTOR must be
33 non-negative. */
34 char *__fd_to_filename (int descriptor, struct fd_to_filename *storage)
35 attribute_hidden;

//glibc/misc/fd_to_filename.c
25 char *
26 __fd_to_filename (int descriptor, struct fd_to_filename *storage)
27 {
28 assert (descriptor >= 0);
29
30 char *p = mempcpy (storage->buffer, FD_TO_FILENAME_PREFIX,
31 strlen (FD_TO_FILENAME_PREFIX));
32 *_fitoa_word (descriptor, p, 10, 0) = '\0';
33
34 return storage->buffer;
35 }

5.关闭旧文件流对象,打开新文件流对象

  • 调用_IO_file_close_it关闭原有的fp,在此之前将_IO_FLAGS2_NOCLOSE置位;
  • 将fp的操作虚函数表置为&_IO_file_jumps,如果是宽字节操作的fp,那就使用&_IO_wfile_jumps初始化fp->_wide_data->_wide_vtable;
  • 调用_IO_file_fopen打开新的file,filename为我们上面生成的gfilename,这里包含了两个意思,如果gfilename更新了,那么就是打开新的文件,否则就是用不同的模式打开原有的文件,然后将_IO_FLAGS2_NOCLOSE置0。
54   fp->_flags2 |= _IO_FLAGS2_NOCLOSE;
55 _IO_file_close_it (fp);
56 _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
57 if (_IO_vtable_offset (fp) == 0 && fp->_wide_data != NULL)
58 fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
59 result = _IO_file_fopen (fp, gfilename, mode, 0);
60 fp->_flags2 &= ~_IO_FLAGS2_NOCLOSE;

6.打开成功的话,调用__fopen_maybe_mmap

__fopen_maybe_mmap之前有提到过,针对一些特殊情况,如只读,那就直接将文件数据mmap到内存,不需要加载到缓存buffer中。

61   if (result != NULL)
62 result = __fopen_maybe_mmap (result);

7.做一些打开后的处理工作

  • 首先将结果文件流对象的_mode置为0(未绑定流方向);
  • 如果原有文件流的fd不等于-1,而且新开启的文件流的fd与之前也不一样,调用__dup3将新开启的fd拷贝到原有fd上,并关闭原有fd,如果失败返回-1,说明出现了EINVAL,即新开启的fd有问题,那此时就需要关闭文件,直接返回NULL;
  • 如果成功的话,那我们就有两个fd同时指向同一个文件了,此时我们调用__close关闭_IO_fileno (result),然后用fd填充新的结构体,然后返回result;
  • 如果原有文件流的fd不等于-1,而且新开启的文件流的fd与之前完全一样,那我们关闭旧有的那一个就好,不用使用__dup3检查新打开fd的可靠性。
63   if (result != NULL)
64 {
65 /* unbound stream orientation */
66 result->_mode = 0;
67
68 if (fd != -1 && _IO_fileno (result) != fd)
69 {
70 /* At this point we have both file descriptors already allocated,
71 so __dup3 will not fail with EBADF, EINVAL, or EMFILE. But
72 we still need to check for EINVAL and, due Linux internal
73 implementation, EBUSY. It is because on how it internally opens
74 the file by splitting the buffer allocation operation and VFS
75 opening (a dup operation may run when a file is still pending
76 'install' on VFS). */
77 if (__dup3 (_IO_fileno (result), fd,
78 (result->_flags2 & _IO_FLAGS2_CLOEXEC) != 0
79 ? O_CLOEXEC : 0) == -1)
80 {
81 _IO_file_close_it (result);
82 result = NULL;
83 goto end;
84 }
85 __close (_IO_fileno (result));
86 _IO_fileno (result) = fd;
87 }
88 }
89 else if (fd != -1)
90 __close (fd);

23 /* Duplicate FD to FD2, closing the old FD2 and making FD2 be
24 open the same file as FD is which setting flags according to
25 FLAGS. Return FD2 or -1. */
26 int
27 __dup3 (int fd, int fd2, int flags)

总结

freopen遵循了先关闭后打开的原则,分别调用_IO_file_close_it关闭,_IO_file_fopen打开,在此过程中对打开的文件名进行了计算,对fd的可靠性和复用做了特殊处理。