LYYL' Blog

勿忧拂意,勿喜快心,勿恃久安,勿惮初难。

0%

IO FILE结构与file ops源码分析

结构体

stat

stat结构体是用来保存文件或者文件夹信息的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
struct stat
{
//文件所在的设备的ID
__dev_t st_dev; /* Device. */
#ifndef __x86_64__
unsigned short int __pad1;
#endif
//inode节点号
#if defined __x86_64__ || !defined __USE_FILE_OFFSET64
__ino_t st_ino; /* File serial number. */
#else
__ino_t __st_ino; /* 32bit file serial number. */
#endif
#ifndef __x86_64__
__mode_t st_mode; /* File mode. */
//文件链接数
__nlink_t st_nlink; /* Link count. */
#else
__nlink_t st_nlink; /* Link count. */
__mode_t st_mode; /* File mode. */
#endif
__uid_t st_uid; /* User ID of the file's owner. */
__gid_t st_gid; /* Group ID of the file's group.*/
#ifdef __x86_64__
int __pad0;
#endif
//设备号,针对设备文件
__dev_t st_rdev; /* Device number, if device. */
#ifndef __x86_64__
unsigned short int __pad2;
#endif
#if defined __x86_64__ || !defined __USE_FILE_OFFSET64
//文件大小,以字节为单位
__off_t st_size; /* Size of file, in bytes. */
#else
__off64_t st_size; /* Size of file, in bytes. */
#endif
//系统块的大小
__blksize_t st_blksize; /* Optimal block size for I/O. */
//文件所占用的系统块的个数
#if defined __x86_64__ || !defined __USE_FILE_OFFSET64
__blkcnt_t st_blocks; /* Number 512-byte blocks allocated. */
#else
__blkcnt64_t st_blocks; /* Number 512-byte blocks allocated. */
#endif
#ifdef __USE_XOPEN2K8
/* Nanosecond resolution timestamps are stored in a format
equivalent to 'struct timespec'. This is the type used
whenever possible but the Unix namespace rules do not allow the
identifier 'timespec' to appear in the <sys/stat.h> header.
Therefore we have to handle the use of this header in strictly
standard-compliant sources special. */
//最后一次存取时间
struct timespec st_atim; /* Time of last access. */
//最后一次修改时间
struct timespec st_mtim; /* Time of last modification. */
struct timespec st_ctim; /* Time of last status change. */
# define st_atime st_atim.tv_sec /* Backward compatibility. */
# define st_mtime st_mtim.tv_sec
# define st_ctime st_ctim.tv_sec
#else
__time_t st_atime; /* Time of last access. */
__syscall_ulong_t st_atimensec; /* Nscecs of last access. */
__time_t st_mtime; /* Time of last modification. */
__syscall_ulong_t st_mtimensec; /* Nsecs of last modification. */
__time_t st_ctime; /* Time of last status change. */
__syscall_ulong_t st_ctimensec; /* Nsecs of last status change. */
#endif
#ifdef __x86_64__
__syscall_slong_t __glibc_reserved[3];
#else
# ifndef __USE_FILE_OFFSET64
unsigned long int __glibc_reserved4;
unsigned long int __glibc_reserved5;
# else
__ino64_t st_ino; /* File serial number. */
# endif
#endif
};

该结构体中有很多针对64位的填充和结构体成员变量类型的变化,这些和我们理解该结构体并不存在影响,主要是为了适应64位的系统。

FILE

FILE文件结构体在程序执行fopen函数的时候会进行创建,并分配在堆中,这里需要注意的是,stdin,stdout,stderr这三种标准输入输出的文件结构体的存储位置在libc.so中。FILE结构体的定义在libio.h中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
//输入输出缓冲区
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

在这个结构体中,所有的FILE结构体会通过chain域彼此形成一个链表,链表的头部是全局变量_IO_list_all表示,通过这个全局变量我们就可以遍历所有的FILE结构体。在每个程序启动的时候,stdinl,stdout,stderr这三个文件流是自动打开的,此时的_IO_list_all就已经指向了由这三个文件流构成的链表。

但是实际上,FILE结构体的外面还包含着IO_FILE_PLUS结构,该结构体中包含了一个重要的指针vtable,该指针指向了一系列的函数指针。在lib2.23 32版本中的vtable的偏移为0x94,64位下的偏移为0xd8

1
2
3
4
5
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

vtable

vtableIO_JUMP_t类型的指针,在其中保存了一些函数指针宫IO函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
}

fopen

1
2
3
4
5
6
#include<stdio.h>
#include<stdlib.h>
int main(){
FILE * fp = fopen("./code.txt", "r+");
fclose(fp);
}
1
2
gcc test.c -g
patchelf --set-rpath ~/Desktop/glibc/x64/glibc2.23/lib/ fopentest

首先我们先编译一个具有fopen操作的c程序,使用gdb调试

我们看到在调用fopen的时候实际上调用的是_IO_new_fopen函数,该函数位于iofopen.c文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
_IO_FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
struct locked_FILE
{
struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
_IO_lock_t lock;
#endif
struct _IO_wide_data wd;
} *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

if (new_f == NULL)
return NULL;
#ifdef _IO_MTSAFE_IO
new_f->fp.file._lock = &new_f->lock;
#endif
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
_IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
#else
_IO_no_init (&new_f->fp.file, 1, 0, NULL, NULL);
#endif
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);
#if !_IO_UNIFIED_JUMPTABLES
new_f->fp.vtable = NULL;
#endif
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)
return __fopen_maybe_mmap (&new_f->fp.file);

_IO_un_link (&new_f->fp);
free (new_f);
return NULL;
}

_IO_FILE *
_IO_new_fopen (const char *filename, const char *mode)
{
return __fopen_internal (filename, mode, 1);
}

函数又调用了__fopen_internal函数来实现fopen的具体操作。从函数的执行流程来看,__fopen_internal总共分为四步操作

malloc分配locked_FILE结构体

这里比较简单,不在赘述。需要注意的是分配的是locked_FILE结构体,其中_IO_wide_data结构体是针对宽字节设计的,其中存储了起始指针等信息,和IO_FILE结构体类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Extra data for wide character streams.  */
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */
__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;
wchar_t _shortbuf[1];
const struct _IO_jump_t *_wide_vtable;
};

分配之后的结果如下

调用_IO_no_init对结构体进行全null初始化

我们进入IO_no_init函数中

函数的位置位于genops.c中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void
_IO_old_init (_IO_FILE *fp, int flags)
{
fp->_flags = _IO_MAGIC|flags;
fp->_flags2 = 0;
fp->_IO_buf_base = NULL;
fp->_IO_buf_end = NULL;
fp->_IO_read_base = NULL;
fp->_IO_read_ptr = NULL;
fp->_IO_read_end = NULL;
fp->_IO_write_base = NULL;
fp->_IO_write_ptr = NULL;
fp->_IO_write_end = NULL;
fp->_chain = NULL; /* Not necessary. */

fp->_IO_save_base = NULL;
fp->_IO_backup_base = NULL;
fp->_IO_save_end = NULL;
fp->_markers = NULL;
fp->_cur_column = 0;
#if _IO_JUMPS_OFFSET
fp->_vtable_offset = 0;
#endif
#ifdef _IO_MTSAFE_IO
if (fp->_lock != NULL)
_IO_lock_init (*fp->_lock);
#endif
}

void
_IO_no_init (_IO_FILE *fp, int flags, int orientation,
struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
{
_IO_old_init (fp, flags);
fp->_mode = orientation;
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
if (orientation >= 0)
{
fp->_wide_data = wd;
fp->_wide_data->_IO_buf_base = NULL;
fp->_wide_data->_IO_buf_end = NULL;
fp->_wide_data->_IO_read_base = NULL;
fp->_wide_data->_IO_read_ptr = NULL;
fp->_wide_data->_IO_read_end = NULL;
fp->_wide_data->_IO_write_base = NULL;
fp->_wide_data->_IO_write_ptr = NULL;
fp->_wide_data->_IO_write_end = NULL;
fp->_wide_data->_IO_save_base = NULL;
fp->_wide_data->_IO_backup_base = NULL;
fp->_wide_data->_IO_save_end = NULL;

fp->_wide_data->_wide_vtable = jmp;
}
else
/* Cause predictable crash when a wide function is called on a byte
stream. */
fp->_wide_data = (struct _IO_wide_data *) -1L;
#endif
fp->_freeres_list = NULL;
}

函数首先调用了_IO_old_init函数,即将结构体中的成员变量设置为默认值。并且将_wide_data结构体中的成员变量也设置为默认值。

调用_IO_file_init函数对结构体进行初始化

我们进入IO_file_init函数中,该函数位于fileops.c文件中

程序实际上调用的是IO_new_file_init函数

1
2
3
4
5
6
7
8
9
10
11
12
13
void
_IO_new_file_init (struct _IO_FILE_plus *fp)
{
/* POSIX.1 allows another file handle to be used to change the position
of our file descriptor. Hence we actually don't know the actual
position before we do the first fseek (and until a following fflush). */
fp->file._offset = _IO_pos_BAD;
fp->file._IO_file_flags |= CLOSED_FILEBUF_FLAGS;

_IO_link_in (fp);
fp->file._fileno = -1;
}
libc_hidden_ver (_IO_new_file_init, _IO_file_init)

函数的主要作用就是将该文件结构体加入到IO_list_all链表中。我们跟进该函数,函数位于genops.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void
_IO_link_in (struct _IO_FILE_plus *fp)
{
if ((fp->file._flags & _IO_LINKED) == 0)
{
fp->file._flags |= _IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
//将结构体加入链表中的主要操作
fp->file._chain = (_IO_FILE *) _IO_list_all;
_IO_list_all = fp;
++_IO_list_all_stamp;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
libc_hidden_def (_IO_link_in)

首先是判断该结构体是否已经加入到链表中。接着将该结构体的_chain指针指向_IO_list_all指针当前指向的结构体,也就是下一个结构体,更新IO_list_all

我们可以看到分配完毕之后的结构体。注意到其排列的方式是stderr->stdout->stdin。至此新分配的结构体已经加入到IO_list_all链表中。

调用_IO_file_fopen执行系统调用打开文件

在将结构体加入到链表之后,接着程序尝试调用IO_file_open函数按照相应的模式打开文件

我们进入该函数

函数位于fileops.c文件中,我们看一下函数代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
_IO_FILE *
_IO_new_file_fopen (_IO_FILE *fp, const char *filename, const char *mode,
int is32not64)
{
int oflags = 0, omode;
int read_write;
int oprot = 0666;
int i;
_IO_FILE *result;
#ifdef _LIBC
const char *cs;
const char *last_recognized;
#endif

if (_IO_file_is_open (fp))
return 0;
switch (*mode)
{
case 'r':
omode = O_RDONLY;
read_write = _IO_NO_WRITES;
break;
case 'w':
omode = O_WRONLY;
oflags = O_CREAT|O_TRUNC;
read_write = _IO_NO_READS;
break;
case 'a':
omode = O_WRONLY;
oflags = O_CREAT|O_APPEND;
read_write = _IO_NO_READS|_IO_IS_APPENDING;
break;
default:
__set_errno (EINVAL);
return NULL;
}
#ifdef _LIBC
last_recognized = mode;
#endif
for (i = 1; i < 7; ++i)
{
switch (*++mode)
{
case '\0':
break;
case '+':
omode = O_RDWR;
read_write &= _IO_IS_APPENDING;
#ifdef _LIBC
last_recognized = mode;
#endif
continue;
case 'x':
oflags |= O_EXCL;
#ifdef _LIBC
last_recognized = mode;
#endif
continue;
case 'b':
#ifdef _LIBC
last_recognized = mode;
#endif
continue;
case 'm':
fp->_flags2 |= _IO_FLAGS2_MMAP;
continue;
case 'c':
fp->_flags2 |= _IO_FLAGS2_NOTCANCEL;
continue;
case 'e':
#ifdef O_CLOEXEC
oflags |= O_CLOEXEC;
#endif
fp->_flags2 |= _IO_FLAGS2_CLOEXEC;
continue;
default:
/* Ignore. */
continue;
}
break;
}

result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
is32not64);
//...
}

函数实际上调用的是IO_new_file_fopen函数,首先判断了文件是否已经被打开,即判断_fileno(文件描述符)是否被设置。

1
2
> #define _IO_file_is_open(__fp) ((__fp)->_fileno != -1)
>

如果文件未被打开,则根据用户的文件打开方式设置相应的文件打开模式,并调用IO_file_open函数打开文件。我们进入函数看一下

函数位于fileops.c文件内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
_IO_FILE *
_IO_file_open (_IO_FILE *fp, const char *filename, int posix_mode, int prot,
int read_write, int is32not64)
{
int fdesc;
#ifdef _LIBC
if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
fdesc = open_not_cancel (filename,
posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
else
fdesc = open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
#else
fdesc = open (filename, posix_mode, prot);
#endif
if (fdesc < 0)
return NULL;
fp->_fileno = fdesc;
_IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
/* For append mode, send the file offset to the end of the file. Don't
update the offset cache though, since the file handle is not active. */
if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
== (_IO_IS_APPENDING | _IO_NO_READS))
{
_IO_off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
if (new_pos == _IO_pos_BAD && errno != ESPIPE)
{
close_not_cancel (fdesc);
return NULL;
}
}
_IO_link_in ((struct _IO_FILE_plus *) fp);
return fp;
}
libc_hidden_def (_IO_file_open)

首先是系统调用open函数打开文件,获取文件描述符,并写入到fileno成员变量中。接着根据用户的输入模式例如a方式打开的文件,则设置文件的指针指向文件的末尾。接着再次调用了IO_link_in函数,确保结构体已经连接到IO_list_all链表中。

总结

  1. malloc分配locked_FILE结构体
  2. 调用_IO_no_init对结构体进行全null初始化
  3. 调用_IO_file_init函数对结构体进行初始化
  4. 调用_IO_file_fopen执行系统调用打开文件

fread

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#include<stdlib.h>
int main(){
FILE * fp = fopen("./code.txt", "r+");
char buf[10];
fread(buf, 1, 19, fp);
printf("%s", buf);
fclose(fp);
}
1
2
gcc test.c -g
patchelf --set-rpath ~/Desktop/glibc/x64/glibc2.23/lib/ freadtest

调用fread实际上调用的是IO_fread函数,该函数位于iofread文件内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_IO_size_t
_IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_size_t bytes_read;
CHECK_FILE (fp, 0);
if (bytes_requested == 0)
return 0;
_IO_acquire_lock (fp);
bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
_IO_release_lock (fp);
return bytes_requested == bytes_read ? count : bytes_read / size;
}
libc_hidden_def (_IO_fread)

真正读取文件的是调用了IO_sgetn函数,函数返回读取的字节数,CHECK_FILE在当前的文件中未被定义。我们跟如函数中看一下

函数位于getops.c文件内

1
2
3
4
5
6
7
_IO_size_t
_IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n);
}
libc_hidden_def (_IO_sgetn)

_IO_XSGETN

函数调用的是_IO_XSGETN调用的是虚函数表中的__xsgetn,默认调用的是IO_file_xsgetn函数,该函数位于fileops.c文件中,该函数就是fread实现的核心函数

初始化输入输出缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
_IO_size_t want, have;
_IO_ssize_t count;
char *s = data;

want = n;

if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
//...
}
libc_hidden_def (_IO_file_xsgetn)

首先判断输入输出缓冲区是否为空,也就是判断_IO_save_base是否含有堆地址。如果输入输出缓冲区为空,则调用_IO_doallocbuf初始化输入输出缓冲区。同时释放存储备份的缓冲区。该函数位于genops.c文件中。

IO_doallocbuf

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_doallocbuf (_IO_FILE *fp)
{
if (fp->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0)
//分配堆区
if (_IO_DOALLOCATE (fp) != EOF)
return;
_IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
libc_hidden_def (_IO_doallocbuf)
IO_DOALLCOATE

检查结构体中的flag如果flag不是IO_UNBUFFEREDmodel>0,则调用_IO_DOALLOCATE,该函数调用的是虚函数表中的__doallocate函数,默认调用的_IO_file_doallocate函数,该函数位于filedoalloc.c文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int
_IO_file_doallocate (_IO_FILE *fp)
{
_IO_size_t size;
char *p;
struct stat64 st;

#ifndef _LIBC
/* If _IO_cleanup_registration_needed is non-zero, we should call the
function it points to. This is to make sure _IO_cleanup gets called
on exit. We call it from _IO_file_doallocate, since that is likely
to get called by any program that does buffered I/O. */
if (__glibc_unlikely (_IO_cleanup_registration_needed != NULL))
(*_IO_cleanup_registration_needed) ();
#endif

size = _IO_BUFSIZ;
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
{
if (S_ISCHR (st.st_mode))
{
/* Possibly a tty. */
if (
#ifdef DEV_TTY_P
DEV_TTY_P (&st) ||
#endif
local_isatty (fp->_fileno))
fp->_flags |= _IO_LINE_BUF;
}
#if _IO_HAVE_ST_BLKSIZE
if (st.st_blksize > 0)
size = st.st_blksize;
#endif
}
p = malloc (size);
if (__glibc_unlikely (p == NULL))
return EOF;
_IO_setb (fp, p, p + size, 1);
return 1;
}
libc_hidden_def (_IO_file_doallocate)
_IO_SYSSTAT

首先是调用_IO_SYSSTAT获取文件的信息,并保存在stat结构体中。_IO_SYSSTAT调用的是虚函数表中的__stat函数,获取文件信息,修改相应的需要申请的size

IO_setb

在获取了blksize之后,按照blksize的大小申请相应的内存空间,接着就是调用了_IO_setb函数

该函数位于genops.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//_IO_setb (fp, p, p + size, 1);
void
_IO_setb (_IO_FILE *f, char *b, char *eb, int a)
{
if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF))
//起释放输出缓冲区的作用
free (f->_IO_buf_base);
f->_IO_buf_base = b;
f->_IO_buf_end = eb;
if (a)
f->_flags &= ~_IO_USER_BUF;
else
f->_flags |= _IO_USER_BUF;
}
libc_hidden_def (_IO_setb)

看到其主要的作用就是设置IO_buf_baseIO_buf_end,也就是设置结构体中输入输出缓冲区的位置。执行完毕之后函数返回,接着alloc相关函数返回,这样输入输出缓冲区就初始化完毕了。

读取文件数据

这里的读取文件数据分为两个阶段

  1. 输入缓冲区存在,且剩余文件的读取数据小于用户需要读取的文件数据,则直接拷贝剩余数据。
  2. 否则调用系统调用读取文件数据。

直接拷贝数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
//初始化输入输出缓冲区
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (want <= have)
{
memcpy (s, fp->_IO_read_ptr, want);
fp->_IO_read_ptr += want;
want = 0;
}
//else...
}

return n - want;
}

这里want是用户想要读取的字节数。若输入缓冲区存在,且当前文件剩余读取的字节数小于用户想要读取的文件字节数,那么直接将需要读取的内容拷贝到返回值s中。

have表示的是当前的输入缓冲区的大小,若have=0,则表示当前输入缓冲区没有待读数据,需要将文件中的数据读取到输入缓冲区中

系统调用读取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
_IO_size_t
_IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
{
//初始化输入输出缓冲区
while (want > 0)
{
have = fp->_IO_read_end - fp->_IO_read_ptr;
if (want <= have)
{
//直接拷贝数据
}
else
{
//如果当前输入缓冲区存在待读数据,则现将当前输入缓冲区中的数据拷贝到返回值中。
if (have > 0)
{
#ifdef _LIBC
s = __mempcpy (s, fp->_IO_read_ptr, have);
#else
memcpy (s, fp->_IO_read_ptr, have);
s += have;
#endif
want -= have;
fp->_IO_read_ptr += have;
}

/* Check for backup and repeat */
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
continue;
}

/* If we now want less than a buffer, underflow and repeat
the copy. Otherwise, _IO_SYSREAD directly to
the user buffer. */
//输入缓冲区存在且用户需要读取的数据小于输入缓冲区的大小
if (fp->_IO_buf_base
&& want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
{
//读取数据到输入缓冲区
if (__underflow (fp) == EOF)
break;
//进入下一个循环
continue;
}

/* These must be set before the sysread as we might longjmp out
waiting for input. */
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
_IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);

/* Try to maintain alignment: read a whole number of blocks. */
count = want;
if (fp->_IO_buf_base)
{
_IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
if (block_size >= 128)
//count表示的最大block块数的数据大小
count -= want % block_size;
}

count = _IO_SYSREAD (fp, s, count);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN;

break;
}

s += count;
want -= count;
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
}
}

return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

若当前的输入输出缓冲区中不存在可读数据就需要系统调用从文件中读取数据了。

  • 用户需要读取的数据大于blksize即一个block的大小,则调用IO_SYSREAD将最大的block数的数据直接读取到s中。
  • 否则调用IO_UNDERFLOW从文件中读取最大一个block的数据。
_underflow

因为按照执行流程,此时是第一次读取数据,也就是_IO_read_ptr_IO_read_end等指针均为空值,输入缓冲区中不存在待读数据,且我们读取的数据的大小小于一个block size,因此会进入到__underflow函数中。该函数位于genops.c文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int
__underflow (_IO_FILE *fp)
{
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
if (_IO_vtable_offset (fp) == 0 && _IO_fwide (fp, -1) != -1)
return EOF;
#endif

if (fp->_mode == 0)
_IO_fwide (fp, -1);
if (_IO_in_put_mode (fp))
if (_IO_switch_to_get_mode (fp) == EOF)
return EOF;
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (_IO_in_backup (fp))
{
_IO_switch_to_main_get_area (fp);
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
}
if (_IO_have_markers (fp))
{
if (save_for_backup (fp, fp->_IO_read_end))
return EOF;
}
else if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
return _IO_UNDERFLOW (fp);
}
libc_hidden_def (__underflow)

首先会对FILE结构体的model进行检查,保证其处于get模式。如果fp->_IO_read_ptr < fp->_IO_read_end 成立则表示还存在未被读取的数据,直接返回,否则就调用_IO_UNDERFLOW读取文件中的数据。该函数调用的是虚函数表中的__underflow函数,默认调用的是IO_file_underflow函数,位于fileops.c文件内。

_IO_UNDERFLOW
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
int
_IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
#if 0
/* SysV does not make this test; take it out for compatibility */
if (fp->_flags & _IO_EOF_SEEN)
return (EOF);
#endif
//检查文件是否允许读
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
//存在未被读取的数据
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
//输入输出缓冲区未被初始化,则初始化缓冲区
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
//输出stdout中未被输出的数据
/* Flush all line buffered files before reading. */
/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
#if 0
_IO_flush_all_linebuffered ();
#else
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (_IO_stdout);

if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (_IO_stdout, EOF);

_IO_release_lock (_IO_stdout);
#endif
}

_IO_switch_to_get_mode (fp);

/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
//初始化读位置等指针
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
//调用系统调用读取文件数据
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
//更新指针,处理错误
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}
libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

函数的执行流程如下

  1. 首先是检查文件是否允许读,若不允许则直接返回EOF
  2. 检查输入输出缓冲区是否被初始化,如果未被初始化则初始化输入输出缓冲区(分配block size大小的内存空间)
  3. 输出stdout中未被输出的内容,即调用_IO_OVERFLOW表示的虚函数,这里在fwrite部分的分析
  4. 调用_IO_SYSREAD读取文件数据,函数返回读取的文件的字节数
  5. 设置读取的末尾指针。
_IO_SYSREAD

我们看一下程序调用读操作的函数_IO_SYSREAD,函数调用的是虚函数表中的__read,默认调用的是_IO_file_read函数,位于fileops.c文件中

1
2
3
4
5
6
7
8
_IO_ssize_t
_IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
{
return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
? read_not_cancel (fp->_fileno, buf, size)
: read (fp->_fileno, buf, size));
}
libc_hidden_def (_IO_file_read)

函数在进行一定的检查之后直接调用系统调用read读取文件内容到buf缓冲区中。在执行完毕_IO_UNDERFLOW之后,FILE结构体中的读位置指针已经指向了输入输出缓冲区

图片无法显示,请设置GitHub代理

我们看到输入输出缓冲区中已经存储了文件中的数据,而文件的读位置指针也指向了输入输出缓冲区。continue进入下一个循环

  • 若用户需要读取的数据小于输入输出缓冲区中可读数据的大小,则直接memcpy将数据拷贝到返回值中
  • 否则将当前缓冲区中的数据memcpy到返回值中
    • 若剩余需要读取的数据大于一个blksize则调用_IO_SYSREAD将最大block数的数据直接读取到s
    • 否则调用__underflow读取剩余的数据

进入到此时说明输入输出缓冲区中存在数据,因此调用_IO_SYSREAD函数将输入输出缓冲区中的数据读取到返回值s中。

总结

  1. 若输入输出缓冲区未初始化则初始化输入输出缓冲区
  2. 若用户需要读取的数据的大小小于当前的输入输出缓冲区的可读取数据的大小,则直接memcpy将数据拷贝到返回值中
  3. 否则先将当前输入输出缓冲区的可读数据拷贝到返回值中。若剩余需要读取的数据的大小小于block size则调用__underflow读取最大一个block size的数据保存在缓冲区中,返回第2步。也就是直接memcpy
  4. 若剩余需要读取的数据大于一个block size则调用IO_SYSREAD将最大block数的数据直接读取到s中,返回第2步,也就是下一步通过__underflow读取剩余的数据。

fwrite

1
2
3
4
5
6
7
8
#include<stdio.h>
#include<stdlib.h>
int main(){
FILE * fp = fopen("./test.txt", "w");
char buf[]="This is a test";
fwrite(buf, 1, sizeof(buf), fp);
fclose(fp);
}
图片无法显示,请设置GitHub代理

文件打开后的FILE结构体如上图所示,注意到这里和我们上面的FILE结构体存在差异,是因为我们才用的是64位的程序,但是总体上的是相同的。

图片无法显示,请设置GitHub代理

接着函数调用的是IO_fwrite函数,该函数位于iofwrite.c文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_IO_size_t
_IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t request = size * count;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request);
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned. The latter is a special case where we
simply did not manage to flush the buffer. But the data is in the
buffer and therefore written as far as fwrite is concerned. */
if (written == request || written == EOF)
return count;
else
return written / size;
}
libc_hidden_def (_IO_fwrite)

函数主要是调用了_IO_sputn的虚函数,我们跟进入看一下,函数默认调用的是_IO_new_file_xsputn函数,该函数位于fileops.c文件内

计算输出缓冲区可用空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;

if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */

/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

//...
return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

这里是计算了当前的缓冲区中可用的空间的大小,保存在count变量中。

写入数据

输出缓冲区空间存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
//计算可用缓冲区的大小
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
#ifdef _LIBC
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
#endif
s += count;
to_do -= count;
}
//...
}

to_do表示剩余需要写入的数据的大小,若当前的缓冲区中存在剩余,且满足用户的需求则使用memcpy将数据拷贝到输出缓冲区中。如果此时的to_do仍存在数值的话,表示输出缓冲区的空间不足,存在剩余的数据

未建立输出缓冲区或空间不足

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
//计算当前的输出缓冲区的可用空间
//如果存在可用的剩余空间则将数据拷贝到输出缓冲区中

//这里是判断数据是否存在剩余,若数据存在剩余则表示的是输出缓冲区未建立或者已经被用尽
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
//返回值为EOF的时候表示的是函数调用出错
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
//...
}
return n - to_do;
}

若果仍然剩余需要被写入的数据,则表示当前输出缓冲区已经用尽,或者未建立输出缓冲区。此时调用_IO_OVERFLOW所表示的虚函数进行输出缓冲区的初始化,或者刷新输出缓冲区。由于我们的程序是第一次读数据,因此未建立输出缓冲区,首先会调用该函数。

IO_OVERFLOW

该函数调用的是虚函数表中的__overflow,默认调用函数_IO_new_file_overflow函数,该函数的主要作用就是初始化,刷新输出缓冲区

注意这里的刷新就体现了延时写入的思想,即先将数据写入到内存的缓冲区中,当缓冲区用尽或者文件关闭等其他情况的时候再将数据写入到文件中,以减少对磁盘的读写,降低延迟

图片无法显示,请设置GitHub代理

_IO_new_file_overflow函数,位于fileops.c文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
int
_IO_new_file_overflow (_IO_FILE *f, int ch)
{
//判断是否具有可写权限
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
//对当前处于读模式或者输入输出缓冲区未初始化的情况进行处理
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
//初始化输入输出缓冲区
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
//设置读位置指针
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output. g that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}

if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
//传入的是EOF,所以直接调用IO_do_write
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

首先判断了当前的FILE结构体指向的文件是否具有可写权限,然后若当前的输入输出缓冲区未被初始化,则调用IO_doallocbuf初始化输入输出缓冲区。在初始化缓冲区之后对FILE结构体中的读指针进行了赋值

1
2
#define _IO_setg(fp, eb, g, eg)  ((fp)->_IO_read_base = (eb),\
(fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg))
图片无法显示,请设置GitHub代理

我们看到在经过赋值之后,读位置指针都指向了输入输出缓冲区。后面对于读指针的操作是针对之前FILE结构体处于写模式进行了。接着对当前的写指针进行了赋值,主要是将_IO_write_ptr赋值为了_IO_read_ptr。在执行完毕之后调用了_IO_do_write来进行写入操作。设置完毕之后的FILE结构体如下

图片无法显示,请设置GitHub代理
IO_do_write
图片无法显示,请设置GitHub代理

函数调用的是IO_new_do_write函数,若需要写入的字节数为0则直接返回,否则调用new_do_write函数,注意到此时我们是第一次调用,由于刚刚赋值了写指针,此时的to_do值为0,因此这里会直接返回0

如果此前存在输入输出缓冲区或者此前处于写模式,且存在未被刷新的输出缓冲区数据,那么就会调用new_do_write函数,将当前未被写入的数据写入到文件中,即刷新输出缓冲区。

分块数据写入

注意到我们的程序在执行时IO_OVERFLOW的返回值为0,也就是此时没有写入任何的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
_IO_size_t
_IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
//计算当前的输出缓冲区的可用空间
//如果存在可用的剩余空间则将数据拷贝到输出缓冲区中

//这里是判断数据是否存在剩余,若数据存在剩余则表示的是输出缓冲区未建立或者已经被用完
if (to_do + must_flush > 0)
{
//初始化输入输出缓冲区,并进行刷新
//返回EOF则表示出错。
if (_IO_OVERFLOW (f, EOF) == EOF)
//return to_do == 0 ? EOF : n - to_do;
//判断需要写入的数据是否为大块的数据
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);

if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}

/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}

如果需要写入的数据是大块的数据(block_size>128且待写入数据to_do大于一个block),那么首先将do_write赋值为整块的数据,调用new_do_write函数进行文件数据写入,我们知道new_do_write函数返回值是写入的文件的字节数,如果count < do_write则表示写入出错,那么直接返回。我们看一下new_do_write函数

new_do_write

函数位于fileops.c文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
_IO_SYSWRITE

首先函数对写指针进行了一个判断,防止读写冲突,接着就是调用_IO_SYSWRITE进行写入操作,该函数调用的是虚函数表的__write函数,默认调用的是IO_new_file_write函数,也是最终执行系统调用的地方,位于filepos.c文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
_IO_ssize_t
_IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n)
{
_IO_ssize_t to_do = n;
while (to_do > 0)
{
_IO_ssize_t count = (__builtin_expect (f->_flags2
& _IO_FLAGS2_NOTCANCEL, 0)
? write_not_cancel (f->_fileno, data, to_do)
: write (f->_fileno, data, to_do));
if (count < 0)
{
f->_flags |= _IO_ERR_SEEN;
break;
}
to_do -= count;
data = (void *) ((char *) data + count);
}
n -= to_do;
if (f->_offset >= 0)
f->_offset += n;
return n;
}

最终调用系统调用将数据写入到文件中,更新文件的偏移位置。

将整block的数据写入完成之后,就剩下了小块的数据,此时调用_IO_default_xsputn函数进行写入。由于我们写入文件的字节数较少,那么直接进入的是该函数,我们跟进看一下

_IO_default_xsputn

将整block的数据写入之后,调用_IO_default_xsputn将剩余的数据写入,由于我们的程序写入的数据较少,因此直接进入该函数,我们跟进看一下

图片无法显示,请设置GitHub代理

函数位于genops.c文件中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
_IO_size_t
_IO_default_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (char *) data;
_IO_size_t more = n;
if (more <= 0)
return 0;
for (;;)
{
/* Space available. */
//输出缓冲区存在可用空间
if (f->_IO_write_ptr < f->_IO_write_end)
{
_IO_size_t count = f->_IO_write_end - f->_IO_write_ptr;
if (count > more)
count = more;
if (count > 20)
{
#ifdef _LIBC
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
#else
memcpy (f->_IO_write_ptr, s, count);
f->_IO_write_ptr += count;
#endif
s += count;
}
else if (count)
{
char *p = f->_IO_write_ptr;
_IO_ssize_t i;
for (i = count; --i >= 0; )
*p++ = *s++;
f->_IO_write_ptr = p;
}
more -= count;
}
//此时输出已经完成 或者输出缓冲区不存在空间或空间已经用尽
if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
break;
more--;
}
return n - more;
}
libc_hidden_def (_IO_default_xsputn)

这里先判断了输出缓冲区是否存在剩余空间,如果不存在则调用IO_OVERFLOW刷新输出缓冲区。

若缓冲区存在剩余的空间

  • 若写入的数据大于20字节,则依次赋值
  • 否则通过memcpy拷贝数据

完成之后若more即数据仍然存在剩余则表示输出缓冲区已经用尽则调用IO_OVERFLOW刷新输出缓冲区。到此用户的数据已经全部写入到文件中。

总结

  1. 首先计算当前的输出缓冲区存在的剩余空间的大小,若存在空间剩余则将数据拷贝到输出缓冲区中
  2. 若此时仍然存在剩余的数据,则表示的是输出缓冲区未建立或者是空间已经用尽,则此时调用IO_OVERFLOW函数进行输出缓冲区的建立或者刷新
  3. 进入到这一步表示输出缓冲区已经准备完毕,则首先判断写入的数据是否大于整块的数据,若大于则调用IO_do_write函数进行整块数据写入
  4. 调用_IO_default_xsputn函数将剩余部分写入文件

fclose

分析fclose函数的时候直接使用[fwrite](# fwrite)函数的程序

图片无法显示,请设置GitHub代理

函数调用的是位于ioflcose.c文件中的IO_new_fclose函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
int
_IO_new_fclose (_IO_FILE *fp)
{
int status;

CHECK_FILE(fp, EOF);

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
/* We desperately try to help programs which are using streams in a
strange way and mix old and new functions. Detect old streams
here. */
if (_IO_vtable_offset (fp) != 0)
return _IO_old_fclose (fp);
#endif

/* First unlink the stream. */
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
_IO_un_link ((struct _IO_FILE_plus *) fp);

_IO_acquire_lock (fp);
if (fp->_IO_file_flags & _IO_IS_FILEBUF)
status = _IO_file_close_it (fp);
else
status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
_IO_release_lock (fp);
_IO_FINISH (fp);
if (fp->_mode > 0)
{
#if _LIBC
/* This stream has a wide orientation. This means we have to free
the conversion functions. */
struct _IO_codecvt *cc = fp->_codecvt;

__libc_lock_lock (__gconv_lock);
__gconv_release_step (cc->__cd_in.__cd.__steps);
__gconv_release_step (cc->__cd_out.__cd.__steps);
__libc_lock_unlock (__gconv_lock);
#endif
}
else
{
if (_IO_have_backup (fp))
_IO_free_backup_area (fp);
}
if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr)
{
fp->_IO_file_flags = 0;
free(fp);
}

return status;
}

根据函数的代码其执行流程如下

  1. 调用_IO_un_link函数将当前的FILE结构体从IO_list_all链表中卸下
  2. 调用_IO_file_close_it函数关闭文件并刷新缓冲区
  3. 释放FILE结构体内存

在分析之前我们先看一下经过fwrite调用之后的FILE结构体

图片无法显示,请设置GitHub代理

注意到此时的FILE结构体中存在待刷新的输出缓冲区,也就是还存在数据未被写回到文件中。

函数默认调用的是位于genops.c文件中的_IO_un_link函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void
_IO_un_link (struct _IO_FILE_plus *fp)
{
if (fp->file._flags & _IO_LINKED)
{
struct _IO_FILE **f;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
run_fp = (_IO_FILE *) fp;
_IO_flockfile ((_IO_FILE *) fp);
#endif
if (_IO_list_all == NULL)
;
else if (fp == _IO_list_all)
{
_IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
++_IO_list_all_stamp;
}
else
for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
if (*f == (_IO_FILE *) fp)
{
*f = fp->file._chain;
++_IO_list_all_stamp;
break;
}
fp->file._flags &= ~_IO_LINKED;
#ifdef _IO_MTSAFE_IO
_IO_funlockfile ((_IO_FILE *) fp);
run_fp = NULL;
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
}
}
libc_hidden_def (_IO_un_link)

函数首先是判断了当前的FILE结构体是否在链表中,如果在链表中则找到该结构体所在的位置,并将其摘下。最后设置文件的flag表示该链表不在链表中。

_IO_file_close_it关闭文件并刷新缓冲区

在将FILE结构体从当前的IO_list_all链表中摘下之后,就开始调用_IO_file_close_it函数关闭文件

图片无法显示,请设置GitHub代理

函数调用的是IO_new_file_close_it函数,位于fileops.c文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int
_IO_new_file_close_it (_IO_FILE *fp)
{
int write_status;
//判断文件是否处于open状态只需要判断fileno是否存在即可

if (!_IO_file_is_open (fp))
return EOF;

if ((fp->_flags & _IO_NO_WRITES) == 0
&& (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
write_status = _IO_do_flush (fp);
else
write_status = 0;

_IO_unsave_markers (fp);

int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
? _IO_SYSCLOSE (fp) : 0);

/* Free buffer. */
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
if (fp->_mode > 0)
{
if (_IO_have_wbackup (fp))
_IO_free_wbackup_area (fp);
_IO_wsetb (fp, NULL, NULL, 0);
_IO_wsetg (fp, NULL, NULL, NULL);
_IO_wsetp (fp, NULL, NULL);
}
#endif
//输入输出缓冲区指针置空,释放输入输出缓冲区
_IO_setb (fp, NULL, NULL, 0);
//置空输入缓冲区
_IO_setg (fp, NULL, NULL, NULL);
//置空输出缓冲区
_IO_setp (fp, NULL, NULL);

_IO_un_link ((struct _IO_FILE_plus *) fp);
fp->_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS;
fp->_fileno = -1;
fp->_offset = _IO_pos_BAD;

return close_status ? close_status : write_status;
}
libc_hidden_ver (_IO_new_file_close_it, _IO_file_close_it)

首先判断了当前的FILE结构体表示的文件是否位于打开状态。接着判断当前的输入输出缓冲区是否为输出缓冲区,若是则调用_IO_do_flush函数刷新缓冲区

_IO_do_flush

注意到我们当前的FILE结构体的输入输出缓冲区是输出缓冲区,且存在待写入文件的数据,因此会进入到该函数。

1
2
3
# define _IO_do_flush(_f) \
_IO_do_write(_f, (_f)->_IO_write_base, \
(_f)->_IO_write_ptr-(_f)->_IO_write_base)

该函数直接调用的是_IO_do_write函数,此时的to_do数据不为0,因此会直接调用new_do_write函数,该函数最终调用系统调用write将数据写入文件中。

图片无法显示,请设置GitHub代理

数据写入完毕之后我们看到写指针已经被刷新了。说明数据已经写入到文件中了。

IO_SYSCLOSE

刷新完毕缓冲区之后,其接着调用了_IO_SYSCLOSE函数,该函数调用的是虚函数表中的__close函数,默认调用的是_IO_file_close函数

图片无法显示,请设置GitHub代理

函数调用了close_not_cancel函数,该函数则是直接调用了close系统调用,关闭了文件描述符

1
2
#define __close_nocancel(fd) \
__close (fd)
图片无法显示,请设置GitHub代理

在关闭系统文件描述符之后对FILE结构体内的指针进行了清空的操作,并释放了输入输出缓冲区。即调用了_IO_setb,_IO_setg,_IO_setp三个函数。

至此FILE结构体中的指针全部清空,缓冲区也释放完毕了。

确认文件关闭并释放FILE结构体内存

IO_FINISH

在清空FILE结构体之后,函数调用了IO_FINISH函数,该函数调用的是虚函数表中的__finish函数,默认调用的是_IO_new_file_finish函数

图片无法显示,请设置GitHub代理

该函数位于fileops.c文件中

IO_new_file_finish

该函数起确认文件关闭的作用。

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_new_file_finish (_IO_FILE *fp, int dummy)
{
if (_IO_file_is_open (fp))
{
_IO_do_flush (fp);
if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
_IO_SYSCLOSE (fp);
}
_IO_default_finish (fp, 0);
}
libc_hidden_ver (_IO_new_file_finish, _IO_file_finish)

首先判断文件是否关闭成功,若失败则清空缓冲区,执行IO_SYSCLOSE调用关闭文件描述符

IO_default_finish

最终执行_IO_default_finish函数

图片无法显示,请设置GitHub代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void
_IO_default_finish (_IO_FILE *fp, int dummy)
{
struct _IO_marker *mark;
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
{
free (fp->_IO_buf_base);
fp->_IO_buf_base = fp->_IO_buf_end = NULL;
}

for (mark = fp->_markers; mark != NULL; mark = mark->_next)
mark->_sbuf = NULL;

if (fp->_IO_save_base)
{
free (fp->_IO_save_base);
fp->_IO_save_base = NULL;
}

_IO_un_link ((struct _IO_FILE_plus *) fp);

#ifdef _IO_MTSAFE_IO
if (fp->_lock != NULL)
_IO_lock_fini (*fp->_lock);
#endif
}
libc_hidden_def (_IO_default_finish)

函数再次确认输入输出缓冲区已经释放,确认FILE结构体从当前链表中摘除。

IO_FINISH函数返回之后即表明FILE文件结构体已经清除完毕,即调用free将文件结构体释放,这样fclose函数就完成了。