写点什么

C++ 学习 ---cstdio 的源码学习分析 05- 打开文件函数 fopen

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

    阅读完需:约 17 分钟

cstdio 中的文件访问函数

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

  • fopen:打开文件

  • fclose:关闭文件

  • fflush:刷新文件流

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

  • setbuf:设置 stream buf

  • setvbuf:改变 stream buf

打开文件函数 fopen

给定指定的文件名和对应的访问 mode,返回对应的文件流对象 FILE 指针。

FILE * fopen ( const char * filename, const char * mode );
复制代码

函数入口分析

对应代码位置:glibc/include/stdio.h

可以看到实际上内部的实现实际都是委托到__fopen_internal,这里有两种版本的函数,一种是 32 位,一种是 64 位,两者的调用区别在于__fopen_internal 的第三个参数。

183 extern FILE *_IO_new_fopen (const char*, const char*);184 #   define fopen(fname, mode) _IO_new_fopen (fname, mode)
//glibc/libio/iofopen.c 83 FILE * 84 _IO_new_fopen (const char *filename, const char *mode) 85 { 86 return __fopen_internal (filename, mode, 1); 87 } 93 # if !defined O_LARGEFILE || O_LARGEFILE == 0 94 weak_alias (_IO_new_fopen, _IO_fopen64) 95 weak_alias (_IO_new_fopen, fopen64) 96 # endif//glibc/libio/iofopen64.c 34 FILE * 35 _IO_fopen64 (const char *filename, const char *mode) 36 { 37 return __fopen_internal (filename, mode, 0); 38 }
复制代码

int is32 标识是否是 32 位,所以在_IO_new_fopen 中调用时默认传 1,在_IO_fopen64 中调用时默认传 0。

 55 FILE * 56 __fopen_internal (const char *filename, const char *mode, int is32) 57 {
复制代码

__fopen_internal 函数

基本的函数逻辑与https://xie.infoq.cn/article/ebc2af6fa385c99400d2c9c2e基本一致,对创建的局部数据结构体 new_f 分配数据,调用_IO_no_init,_IO_JUMPS,_IO_new_file_init_internal 初始化其中的变量。

因为只知道当前的文件名,不知道 fd,所以需要调用_IO_file_fopen 进行后续的操作,如果失败,则需要 unlink,释放内存,然后返回 NULL。

 55 FILE * 56 __fopen_internal (const char *filename, const char *mode, int is32) 57 { 58   struct locked_FILE 59   { 60     struct _IO_FILE_plus fp; 61 #ifdef _IO_MTSAFE_IO 62     _IO_lock_t lock; 63 #endif 64     struct _IO_wide_data wd; 65   } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); 66  67   if (new_f == NULL) 68     return NULL; 69 #ifdef _IO_MTSAFE_IO 70   new_f->fp.file._lock = &new_f->lock; 71 #endif 72   _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps); 73   _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; 74   _IO_new_file_init_internal (&new_f->fp); 75   if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL) 76     return __fopen_maybe_mmap (&new_f->fp.file); 77  78   _IO_un_link (&new_f->fp); 79   free (new_f); 80   return NULL; 81 }
复制代码

_IO_file_fopen 函数

_IO_file_fopen ((FILE *) new_f, filename, mode, is32)

注意:上面的函数调用过程中将 new_f 指针从 locked_FILE 转为了 FILE,这样做是合法的,因为 locked_FILE 中第一个变量_IO_FILE_plus 的首个变量即是 FILE,实际上这样做使得 new_f 指针的访问被截断,只能访问前面 FILE 中的内容。

//glibc/libio/libioP.h324 struct _IO_FILE_plus                                        325 {326   FILE file;327   const struct _IO_jump_t *vtable;328 };
复制代码

这里做了符号映射,实际调用_IO_file_fopen 被映射为_IO_new_file_fopen

//glibc/libio/fileops.c1422 versioned_symbol (libc, _IO_new_file_fopen, _IO_file_fopen, GLIBC_2_1);
复制代码

_IO_new_file_fopen 函数

1.入参及局部变量准备

不多赘述,准备与 fopen 相关的变量

 210 FILE * 211 _IO_new_file_fopen (FILE *fp, const char *filename, const char *mode, 212             int is32not64) 213 { 214   int oflags = 0, omode; 215   int read_write; 216   int oprot = 0666; 217   int i; 218   FILE *result; 219   const char *cs; 220   const char *last_recognized;
复制代码

2.如果文件已经打开,则返回 0

判断方式也很简单,查看 fp 的_fileno 是否被赋值,正常打开一次之后,该值将被赋值为对应的 fd

 222   if (_IO_file_is_open (fp)) 223     return 0;  565 #define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)
复制代码

3.解析文件打开的 mode

omode 记录 File access modes:只读/只写/读写

oflags 记录文件 open 的参数:

  • O_CREAT:Create file if it doesn't exist

  • O_TRUNC:Truncate file to zero length

  • O_APPEND:Writes append to the file

  • O_EXCL:Fail if file already exists

  • O_CLOEXEC:Set close_on_exec

read_write 记录读写参数:

  • _IO_NO_READS:Reading not allowed

  • _IO_NO_WRITES:Writing not allowed

  • _IO_IS_APPENDING:追加模式

fp->_flags2 记录第二个 flags 信息:

  • _IO_FLAGS2_MMAP:使用 mmap

  • _IO_FLAGS2_NOTCANCEL:不取消模式

  • _IO_FLAGS2_CLOEXEC:lose_on_exec

last_recognized 记录最后检测到的模式。

 224   switch (*mode) 225     { 226     case 'r': 227       omode = O_RDONLY; 228       read_write = _IO_NO_WRITES; 229       break; 230     case 'w': 231       omode = O_WRONLY; 232       oflags = O_CREAT|O_TRUNC; 233       read_write = _IO_NO_READS; 234       break; 235     case 'a': 236       omode = O_WRONLY; 237       oflags = O_CREAT|O_APPEND; 238       read_write = _IO_NO_READS|_IO_IS_APPENDING; 239       break; 240     default: 241       __set_errno (EINVAL); 242       return NULL; 243     } 244   last_recognized = mode;  245   for (i = 1; i < 7; ++i) 246     { 247       switch (*++mode) 248     { 249     case '\0': 250       break; 251     case '+': 252       omode = O_RDWR; 253       read_write &= _IO_IS_APPENDING; 254       last_recognized = mode; 255       continue; 256     case 'x': 257       oflags |= O_EXCL; 258       last_recognized = mode; 259       continue; 260     case 'b': 261       last_recognized = mode; 262       continue; 263     case 'm': 264       fp->_flags2 |= _IO_FLAGS2_MMAP; 265       continue; 266     case 'c': 267       fp->_flags2 |= _IO_FLAGS2_NOTCANCEL; 268       continue; 269     case 'e': 270       oflags |= O_CLOEXEC; 271       fp->_flags2 |= _IO_FLAGS2_CLOEXEC; 272       continue;                                                                                                                                      273     default: 274       /* Ignore.  */ 275       continue; 276     } 277       break; 278     }
复制代码

4.调用_IO_file_open 打开文件

注意,这里大部分参数都是传入的,或者刚解析出来的,这个 oprot 是前文定义的局部变量int oprot = 0666,表示

该文件拥有者对该文件拥有读写的权限但是没有操作的权限该文件拥有者所在组的其他成员对该文件拥有读写的权限但是没有操作的权限其他用户组的成员对该文件也拥有读写权限但是没有操作的权限

调用_IO_file_open 的流程中大致可以分为如下几步:

  • 根据 flags2 决定是调用__open_nocancel 还是__open;

这里的__open 实际上就是系统函数__libc_open,参考https://xie.infoq.cn/article/446098c2245671be70a22aff2

47 weak_alias (__libc_open, __open)

__open_nocancel 的原理类似,参见:glibc/sysdeps/unix/sysv/linux/open_nocancel.c

  • 调用_IO_mask_flags 设定对应的 flags;

  • 针对 append 模式,移动文件指针到_IO_seek_end;

  • 将打开后的 fp link 到_IO_list_all 上

 280   result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write, 281               is32not64);  179 FILE * 180 _IO_file_open (FILE *fp, const char *filename, int posix_mode, int prot, 181            int read_write, int is32not64) 182 { 183   int fdesc; 184   if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL)) 185     fdesc = __open_nocancel (filename, 186                  posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot); 187   else 188     fdesc = __open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot); 189   if (fdesc < 0) 190     return NULL; 191   fp->_fileno = fdesc; 192   _IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING); 193   /* For append mode, send the file offset to the end of the file.  Don't 194      update the offset cache though, since the file handle is not active.  */ 195   if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS)) 196       == (_IO_IS_APPENDING | _IO_NO_READS)) 197     { 198       off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end); 199       if (new_pos == _IO_pos_BAD && errno != ESPIPE) 200     { 201       __close_nocancel (fdesc); 202       return NULL; 203     } 204     } 205   _IO_link_in ((struct _IO_FILE_plus *) fp); 206   return fp; 207 }
复制代码

5.查看打开的文件是否需要特殊转换

这里主要是针对宽字符进行相关的处理和模式设置,详细的内容就不赘述了,具体细节与正常的打开流程基本一致,最后设置宽字符的字符处理虚函数表_wide_vtable。

 283   if (result != NULL) 284     { 285       /* Test whether the mode string specifies the conversion.  */ 286       cs = strstr (last_recognized + 1, ",ccs="); 287       if (cs != NULL) 288     {                                                                                                                                                289       /* Yep.  Load the appropriate conversions and set the orientation 290          to wide.  */... 347       /* From now on use the wide character callback functions.  */ 348       _IO_JUMPS_FILE_plus (fp) = fp->_wide_data->_wide_vtable; 349  350       /* Set the mode now.  */ 351       result->_mode = 1; 352     } 353     } 
复制代码

6.返回 result,即 FILE*指针

 210 FILE * 211 _IO_new_file_fopen (FILE *fp, const char *filename, const char *mode, 212             int is32not64) 213 { ... 280   result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,    281               is32not64); ... 355   return result;                                               356 }
复制代码

__fopen_maybe_mmap 函数

针对 flags2 为 mmap 且 flags 设定为"r"的模式,可以直接使用 mmap 内容的方式,因为不需要修改原文件内容,所以需要替换 fp 中字符操作的虚函数表,使用 maybe_mmap 类型的函数

 33 FILE * 34 __fopen_maybe_mmap (FILE *fp) 35 { 36 #if _G_HAVE_MMAP 37   if ((fp->_flags2 & _IO_FLAGS2_MMAP) && (fp->_flags & _IO_NO_WRITES)) 38     { 39       /* Since this is read-only, we might be able to mmap the contents 40      directly.  We delay the decision until the first read attempt by 41      giving it a jump table containing functions that choose mmap or 42      vanilla file operations and reset the jump table accordingly.  */ 43  44       if (fp->_mode <= 0) 45     _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_maybe_mmap; 46       else 47     _IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps_maybe_mmap; 48       fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_maybe_mmap; 49     } 50 #endif 51   return fp; 52 }
复制代码

总结

通过一层层函数调用和参数检查准备,最后调用_IO_file_open 打开文件,返回文件流指针,在此过程中也会针对一些特殊情况(如追加模式,只读模式,做参数调整或虚函数表的重新赋值)做处理,尽可能提升程序的效率。

发布于: 2022 年 10 月 05 日阅读数: 38
用户头像

桑榆

关注

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

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

评论

发布
暂无评论
C++学习---cstdio的源码学习分析05-打开文件函数fopen_c++_桑榆_InfoQ写作社区