引*
glibc中有各种vtable,fileops,strops,wfileops等等
更多的的可以在glibc/libio/vtables.c查看,不同vtable中对应的函数实现也不同,不过都是为io服务,都与_IO_FILE有关
本文主要研究file虚表函数,其主要集中在fileops.c中
fileops.c文件开头这一段注释提供了不少信息
认真阅读能提供不少帮助
1 | /* An fstream can be in at most one of put mode, get mode, or putback mode. |
大致如下
- 文件流的模式:文件流可以处于put模式、get模式或putback模式中。Putback模式是get模式的一种变体。
- 文件缓冲区中的当前位置:在文件缓冲区中,只有一个当前位置,而不是分别有get指针和put指针。在get模式中,当前位置是gptr()的位置;在put模式中,当前位置是pptr()的位置。
- 缓冲区位置与外部文件系统位置的对应:通常情况下,缓冲区中与外部文件系统位置对应的位置是_IO_read_end,但在putback模式下,它是_IO_save_end,并且在文件处于附加模式时也是_IO_save_end。这是因为从读模式切换到写模式会自动将外部文件系统位置切换到文件的末尾。如果字段_fb._offset >= 0,则它表示与eGptr()对应的文件整体偏移。
- PUT模式:在put模式下,_IO_read_ptr、_IO_read_end和_IO_read_base都相等。它们通常等于_IO_buf_base,但如果从get模式切换到put模式,它们不一定相等。_IO_write_base不为空,通常等于_IO_buf_base。_IO_write_end等于_IO_buf_end,但只在完全缓冲模式下成立。未刷新的字符位于_IO_write_base和_IO_write_ptr之间。
- GET模式:在get或putback模式下,eback() != egptr()。在get模式中,未读字符位于gptr()和egptr()之间。操作系统文件位置对应于egptr()的位置。
- PUTBACK模式:putback模式用于记住已经通过sputbackc放回的“多余”字符,它们存储在特殊的putback缓冲区中。在putback模式中,get缓冲区指向特殊的putback缓冲区。未读字符包括putback缓冲区中gptr()和egptr()之间的字符,以及指向原始预留缓冲区的save_gptr()和save_egptr()之间的区域。操作系统位置对应于save_egptr()的位置。
- 行缓冲输出:在行缓冲输出期间,_IO_write_base等于base(),并且epptr()也等于base()。但是,ptr()可能位于base()和ebuf()之间。这会导致在每次放入字符时调用filebuf::overflow(int C)。如果缓冲区中还有更多空间(pptr<ebuf),并且C不是’\n’,则会插入C,并增加pptr(),否则刷新写入。
- 无缓冲流:如果文件缓冲区是unbuffered(),则_shortbuf[1]用作缓冲区。
相关系统调用
lseek
seek
是一个用于文件操作的系统调用,它的主要功能是用于改变文件指针的位置,从而实现对文件的随机访问。具体来说,seek
的功能包括:
- 定位文件指针:
seek
允许你将文件指针(读/写位置)移动到文件中的任意位置。这是对文件进行随机访问的关键操作。你可以指定要移动到的位置,通常是相对于文件开头的偏移量。 - 读取和写入特定位置:通过改变文件指针的位置,你可以在文件中的任何位置进行读取和写入操作,而不必按照顺序逐个字节进行操作。这对于访问大型文件或数据库非常有用。
- 支持文件的随机访问:
seek
是实现随机访问的关键,允许你在不必按照文件顺序读取数据的情况下,快速访问和处理文件的各个部分。 - 实现文件的截断和扩展:在某些情况下,
seek
可以用于截断文件(减小文件大小)或扩展文件(增大文件大小)。通过移动文件指针并写入数据,你可以实现这些操作。
其返回值表示成功执行操作后的文件偏移量,如果出现错误,返回值会是 -1
。
具体来说,lseek
的原型如下:
1 | off_t lseek(int fd, off_t offset, int whence); |
fd
是文件描述符,用于指定要进行定位操作的文件。offset
是一个偏移量,用于指定要移动的相对位置。可以为正数、负数或零,具体取决于whence
参数的值。whence
用于确定偏移量的基准位置,通常可以取以下值之一:SEEK_SET
:以文件开头为基准,offset
指定的位置。SEEK_CUR
:以当前文件位置为基准,增加offset
指定的位置。SEEK_END
:以文件末尾为基准,增加offset
指定的位置。
lseek
函数会根据 offset
和 whence
的指定值来移动文件描述符 fd
的偏移位置,并返回新的文件偏移位置。如果操作成功,返回值是新的偏移位置。如果出现错误,返回值是 -1
1 |
sync
sync
是一个系统调用,它的主要功能是将操作系统内核中尚未写入磁盘的缓冲区数据强制刷新到磁盘上的存储设备,以确保数据持久性和文件系统的一致性。sync
的主要功能包括:
- 数据持久性:通过执行
sync
,操作系统会将所有尚未写入磁盘的数据写入到物理存储设备中。这可以确保即使系统崩溃或断电,尚未写入磁盘的数据也不会丢失。 - 文件系统一致性:
sync
也有助于维护文件系统的一致性。在写入文件和目录信息时,文件系统通常会维护内部数据结构,这些数据结构需要及时写入磁盘以确保文件系统的一致性。sync
确保这些数据结构及其相关的数据被写入磁盘。 - 缓冲区刷新:
sync
还用于刷新内核中的缓冲区,以确保缓冲区中的数据被写入磁盘。这对于正在进行的文件操作和文件系统操作非常重要,因为数据通常首先存储在内存中以提高性能,然后定期刷新到磁盘上。 - 数据完整性:
sync
还有助于维护数据的完整性。它确保了所有写入的数据都已经被持久地存储在磁盘上,以免数据损坏或丢失。
stat
系统调用 stat
用于获取关于文件或目录的信息,如文件的大小、访问权限、所属用户和组、文件类型等。它返回一个包含文件信息的结构体,通常被称为 struct stat
。
stat
系统调用的功能包括:
获取文件的基本属性:
stat
可以用来获取文件的基本属性,如文件大小、创建时间、修改时间、访问时间等。获取文件的权限信息:
stat
可以提供文件的权限信息,包括文件的拥有者、所属组以及其他用户的权限。确定文件的类型:
stat
可以告诉您文件是普通文件、目录、符号链接还是其他类型的文件。获取文件的相关信息:
stat
可以提供有关文件系统的信息,如文件系统的块大小、设备号等。
1 | struct _stat64 { |
其他
其他还有用到read,write,open,close都较为熟悉就不记录了
IO虚表函数
1 | versioned_symbol (libc, _IO_new_do_write, _IO_do_write, GLIBC_2_1); |
finish
finish的主要功能是关闭缓冲区,解除文件流在_IO_list_all中的链接
1 _IO_new_file_finish
1 | void |
- 先是检查文件是否打开,是则调用_IO_do_flush (fp)并根据 _IO_DELETE_DONT_CLOSE标志位决定是否调用close关闭文件流
- _IO_default_finish (fp, 0);
2-1 _IO_do_flush
1 |
根据是是否是宽字节有两个分支
目前先看非宽字节分支,调用的是另一个虚表函数write,这里先不写
2-2 _IO_default_finish
1 | void |
- 若buf不为空且_IO_USER_BUF标志为0,释放free缓冲区,并置空buf指针
- 将文件流的所有marker的_sbuf字段清空
- 若文件流的_IO_save_base不为空,则将其free释放并置空
- 调用unlink将文件流解除_IO_list_all链
3 _IO_un_link
1 | void |
- 先确认文件流在_IO_list_all链中
- 寻找_IO_list_all链中的fp将其解链并清除_IO_LINKED标志位
overflow
overflow
主要负责将数据写入底层文件(或设备)
1 | int |
- 判断文件流是否设置_IO_NO_WRITES,是则标记错误并返回
- 如果文件不处于写入模式(_IO_CURRENTLY_PUTTING)或者_IO_write_base为空
- 如果是_IO_write_base为空的情况,先通过_IO_doallocbuf申请buf缓冲区,并设置read指针
- 如果文件流正在备份(_IO_IN_BACKUP)
- 调用_IO_free_backup_area (f)
- f->_IO_read_base减去f->_IO_read_end - f->_IO_read_ptr和f->_IO_read_base - f->_IO_buf_base中更小的那个
- f->_IO_read_ptr = f->_IO_read_base;
- 如果f->_IO_read_ptr == f->_IO_buf_end,将f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
- 将w-ptr和w-base设置为r-ptr,w-end设置为b-end,r-base和r-ptr设置为r-end(之后这些指针一般用简写)
- 设置_IO_CURRENTLY_PUTTING位
- 若文件流是行缓冲或无缓冲模式且非宽字符w-end=w-ptr
- 如果文件不处于写入模式(_IO_CURRENTLY_PUTTING)或者_IO_write_base为空
- 如果参数ch为EOF,调用_IO_do_write
- 如果w-ptr==b-end,调用_IO_do_flush如果返回EOF则直接return EOF
- *f->_IO_write_ptr++=ch
- 如果文件是无缓冲或者是行缓冲且ch为’\n’,调用_IO_do_write如果返回EOF则直接return EOF
- 返回ch
write
1 | int |
如果to_do==0直接返回1
否则调用new_do_write (fp, data, to_do)
1 | static size_t |
如果_IO_IS_APPENDING被置位,说明文件对象是以追加方式打开的,所以将fp->_offset赋值为_IO_pos_BAD,即定位到文件末尾;
如果不是追加模式,就要考虑读写buffer块地址的信息了,读的尾指针不等于写的基指针,说明之前读写过程不一致,现在我们需要写入信息,所以需要调用_IO_SYSSEEK进行调整,基于当前的位置(1表示SEEK_CUR)将两者调整到一致。
- 如果返回结果是异常的-1,那就直接返回0,表示写入字节数为0.
- 否则使用新的位置信息更新fp->_offset
- 调用系统调用_IO_SYSWRITE (fp, data, to_do);
- 如果当前列参数不等于0(即第一列),而且写入的字符数不等于0,此时需要更新列参数,调用_IO_adjust_column函数实现。
- 调用_IOsetg将读相关的base、ptr、end更新为_IO_buf_base;然后将写相关的base、ptr更新为_IO_buf_base。
注意最后的w-end会根据当前的模式选择是等于_IO_buf_base还是_IO_buf_end:
- 如果fp->_mode <= 0,说明是标准字符,fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)说明是按行为buffer单位或没有缓存buffer,这种情况将写end置为_IO_buf_base,即无法使用buffer,否则则是可以使用buffer的情况,置为_IO_buf_end,可以使用base到end这块空间作为写缓存。
看一下col调整函数
1 | unsigned |
就是更新最后一行的列
首先ptr指向真正写入的最后一个字符;
当ptr大于line,即从后向前遍历字符,如果找到换行符,则结束,说明之前遍历的位于写入的最后一行,此时line + count - ptr - 1表示最后一行的字符数,返回该值即可;
如果没有找到换行符,那就返回start + count,即之前的列号加真正写入的字符数。
最后在外层再加1得到当前行的列号,整体的逻辑就是要更新当前的列号。
read
1 | ssize_t |
就是调用系统调用
seek
1 | off64_t |
seek就是调用lseek
返回成功后的偏移地址如果错误返回-1
stat
1 | int |
调用stat系统调用
返回一个stat结构体
underflow
underflow主要负责从文件中读取数据到缓冲区
1 | int |
如果已经到达文件末尾返回EOF
如果文件不允许读,设置错误并返回EOF
如果r-ptr<r-end返回r-ptr指向的字符
如果buf为空
- 如果_IO_save_base不为空先将其释放,并取消_IO_IN_BACKUP标志位
- 申请buf
如果是行缓冲或无缓冲模式
- 给stdout上锁
- 如果stdout是行缓冲且在_IO_list_all链上且不禁止写,对stdout调用overflow
- 将stdout解锁
调用_IO_switch_to_get_mode (fp);
更新读写缓冲区所有指针为fp->_IO_buf_base
调用系统调用_IO_SYSREAD(fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base)
,返回值为count
如果count<=0
- 如果count为0,文件标志设置到达末尾
- 如果count小于0,设置错误标志,并将count置为0
r-end向后移动count
如果count为0,将fp->_offset设置为-1(文件末尾)并返回EOF
如果fp->_offset不为-1,fp->_offset移动到当前位置向后count字节
返回r-ptr指向的字符
看以下其中调用的_IO_switch_to_get_mode (fp);
1 | int |
- 如果w-ptr>w-base说明输出缓冲区还有数据尚未写入文件,调用_IO_OVERFLOW (fp, EOF)
- 如果处于备份模式fp->_IO_read_base = fp->_IO_backup_base;
- 否则fp->_IO_read_base = fp->_IO_buf_base;
- 如果w-ptr>r-end,r-end=w-ptr
- 否则fp->_IO_read_base = fp->_IO_buf_base;
- r-ptr被赋值为w-ptr,write的所有指针置为r-ptr
- 取消文件流的_IO_CURRENTLY_PUTTING标志位
感觉有些指针操作有些多余了,underflow外层中都会统一更新赋值
uflow
1 | int |
- 调用underflow
- 如果underflow返回值为EOF,返回EOF
- 否则返回fp->_IO_read_ptr处的字符
sync
sync负责平衡读写,将未写入的数据写入文件,将未读取的数据去除
1 | int |
- 如果write缓冲区有未写入的,调用_IO_do_flush不成功写入或写入不完全则直接返回EOF
- 平衡read指针
imbue
在2.31中是个空函数
showmanyc
在2.31中是个空函数
close
关闭文件流
1 | int |
- 如果文件不处于打开状态,直接返回
- 如果文件不禁止写且正处于写入模式,调用_IO_do_flush,否则设置write_status = 0
- 调用_IO_unsave_markers (fp);
- 如果文件未设置_IO_FLAGS2_NOCLOSE,调用sysclose关闭文件描述符
- 如果文件为宽字符模式,对宽字符缓冲进行处理
- 设置缓冲区指针为null
- 调用_IO_un_link解链文件
- 设置标志位,设置偏移基址为末尾设置文件描述符
- 返回close_status ? close_status : write_status;
doallocate
1 | int |
- 如果文件描述符大于等于0且文件返回的信息正常
- 如果文件_IO_IS_FILEBUF标志被设置,设置 _IO_LINE_BUF标志位
- malloc申请chunk
- 设置buf指针并返回
seekpos
1 | off64_t |
调用_IO_SEEKOFF
seekoff
出现频率不高,暂时先略过
1 | off64_t |
pbackfail
出现频率不高,暂时先略过
1 | int |
setbuf
1 | FILE * |
- 调用_IO_default_setbuf (fp, p, len)
- 调整pptr和gptr为_IO_buf_base
看_IO_default_setbuf (fp, p, len)
1 | FILE * |
- sync平衡失败直接返回NULL
- 缓冲区未指定或长度为0,将文件流设置为无缓冲模式,并设置缓冲区指针为shorbuf
- 否则取消无缓冲标志并设置缓冲区为指定区域
- 更新pptr和gptr为null
- 返回fp
xsgetn
1 | size_t |
如果buf为null
- 如果savebase不为null,先将其释放并取消’在备份’标志位
- 调用doallocbuf申请缓冲区
循环,条件为当需要的数据want大于0
如果want不多于read缓冲区中拥有的数据,直接将缓冲区中的数据转移到内存,并调整gptr
否则如果want>have
- 如果have大于0,先将have中的数据转移到内存
- 如果处于备份模式,调用_IO_switch_to_main_get_area (fp);并结束当次循环
- 如果buf不为空且want小于缓冲区容量,调用underflow,若返回EOF则跳出循环,否则结束此次循环
- 如果缓冲区异常或者want大于缓冲区容量
- 设置pptr和gptr
- 如果缓冲区存在且缓冲区大于128则count -= want % block_size;,即将超过缓冲区的部分直接调用系统调用读取,剩余部分则在下一次循环完成
- 调用系统调用read(fp, s, count),如果上一步没有修改count,那么这一步就可以直接调用系统调用read读取所有的内容,并且是直接读到目标区域不经过缓冲区,根据返回值有:
- 如果返回为0,则设置文件标志为到达文件末尾,否则设置为发生错误,返回值小于0则设置错误标志位,两种情况下,都会跳出循环
- 返回值大于0,则继续向下执行
- s += count;want -= count;如果文件偏移不在末尾则调整offset
- 再次开始循环,进行前面的操作
- 返回n-want(即读入的量)
xspuntn
1 | size_t |
- 如果n小于等于0直接返回
- 如果文件时行缓冲且正处于写入模式
- 如果b-end—w-ptr大于n,从要写入数据的末尾开始查找’\n’符,如果找到了设置count为’\n’字符前的数据长度,并将must_flush 置 1
- 否则count=w_end - w_ptr
- 如果count大于0
- 如果count>todo,count=todo
- 将内存中的数据转移到缓冲区
- s += count;to_do -= count;
- 如果to_do + must_flush > 0
- 调用overflow,若返回EOF,则返回to_do == 0 ? EOF : n - to_do;
- do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
- 如果do_write大于0,调用new_do_write (f, s, do_write);写多余的
- 如果写入数量小于do_write,返回n - to_do
- 如果to_do还有剩,调用_IO_default_xsputn (f, s+do_write, to_do);
看_IO_default_xsputn (f, s+do_write, to_do);
1 | size_t |
- 如果more小于等于0直接返回0
- 循环
- 如果f->_IO_write_ptr < f->_IO_write_end
- 如果count>more,count = more;
- 如果count>20,将内存中的数据移动到缓冲区
- 否则如果count不为0但小于20,将内存中的数据逐个复制到缓冲区
- 如果more=0或者_IO_OVERFLOW (f, (unsigned char) *s++) == EOF结束循环,more!=0才会执行_IO_OVERFLOW,原因是因为前面已经填满了缓冲区需要刷新,其会单独往缓冲区写入一个字符(原本是处理行缓冲的机制),所以下面要more—
- 否则more—
- 如果f->_IO_write_ptr < f->_IO_write_end
- 返回n-more
拾遗
- 综上可以看出缓冲区模式,对读取过程并没有什么影响,对写入过程的影响则要更大,不过都受到上层函数影响