写点什么

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

作者:桑榆
  • 2022 年 10 月 08 日
    广东
  • 本文字数:4931 字

    阅读完需:约 16 分钟

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.h282 #ifdef __USE_LARGEFILE64283 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.h111 /* 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 for115    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_DEBUG866 # 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 的可靠性和复用做了特殊处理。

发布于: 刚刚阅读数: 6
用户头像

桑榆

关注

北海虽赊,扶摇可接;东隅已逝,桑榆非晚! 2020.02.29 加入

Android手机厂商-相机软件系统工程师 爬山/徒步/Coding

评论

发布
暂无评论
C++学习---cstdio的源码学习分析07-重新打开文件流函数freopen_源码刨析_桑榆_InfoQ写作社区