cstdio 中的文件访问函数
stdio.h 中定义了一系列文件访问函数(fopen,fclose,fflush,freopen,setbuf,setvbuf),接下来我们一起来分析一下 fopen 对应的源码实现。
打开文件函数 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.h
324 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.c
1422 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 记录读写参数:
fp->_flags2 记录第二个 flags 信息:
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 的流程中大致可以分为如下几步:
这里的__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 打开文件,返回文件流指针,在此过程中也会针对一些特殊情况(如追加模式,只读模式,做参数调整或虚函数表的重新赋值)做处理,尽可能提升程序的效率。
评论