Glibc---_IO_file_xsputn 函数逻辑分析
背景
_IO_file_xsputn 是 Glibc IO 库中的重要组成函数,主要作用是向指定的文件流对象中写入指定字节的数据,与_IO_do_write 的作用类似,一些系统函数的实现中就用到了这个函数,如 vfprintf 函数实现中的 PUT(F, S, N)宏就是对该接口的封装。接下来,我们就一起来看看这个函数的实现流程及其背后的原理。
函数入口分析
1.入参分析
注意这里我们将相关的一类函数都截取下来了,入参都基本一致:
FILE *:文件流对象
const char *:要写入的 buffer 地址,只读
size_t:要写入的字节数量
这里我们可以看到 libc_hidden_proto 宏,它的作用实际上是对外隐藏_IO_file_xsputn 函数原型;
与_IO_new_file_xsputn 相类似的其余三个函数通过其名字也能很好地进行区分:
_IO_new_do_write 是新版本实现;_IO_old_file_xsputn 是旧版本实现,_IO_wfile_xsputn 是针对宽字符的版本。
2.函数映射关系
从这里我们就能很好地看到 new 与 old 的作用,实际上是针对不同 GLIBC version 的操作。
这里我们优先分析 new 版本的代码,后续我们也是保持这个原则。
3._IO_new_file_xsputn 的函数入口
这里截取了其中关于数据处理的三个部分,分别是
向_IO_write_ptr 中拷贝数据(因为使用缓存的情况下,数据并不是直接写到物理文件中的);
调用 new_do_write 进行数据写入
调用_IO_default_xsputn 进行数据写入
函数逻辑分析
1.局部变量申请与特殊情况处理
这里申请了一些局部变量,后续使用;
针对输入 size 小于等于 0 的情况,直接返回 0,无需做其他处理。
2.计算剩余可以使用的缓存空间大小 count
注意,这里分成了两种情况进行计算:
第一种情况:如果当前使用_IO_LINE_BUF(输出操作中,数据在新的一行插入 FILE 流对象或 buffer 写满时触发写入物理文件;输入操作中,buffer 只有在 buffer 全为空时,写入新的一行到 buffer 中)或者当前是输出模式时,剩余空间等于 buffer 的末尾_IO_buf_end 减去当前写指针的位置_IO_write_ptr。
如果 count >= n,说明剩余的缓存空间足够写入数据;这时,从后往前遍历需要写入的 data,找到最后一个换行符'\n',标记当前位置,将剩余空间更新为第一个字符到倒数第一个换行符之间的字符数(p - s + 1),并将 must_flush 置为 true
第二种情况:使用缓存 buffer 的情况,剩余空间就等于_IO_write_end-_IO_write_ptr。
3.开始填充 buffer
(1).先调用__mempcpy 填充写缓冲 buffer
如果有剩余 buffer 空间大于要写入的 size n,那就将 count 更新为 to_do 大小,然后调用__mempcpy 将[s,s+count]范围内的字符拷贝到 f->_IO_write_ptr 处,同步更新 s 指针和 to_do 的大小。
(2).剩余空间不够或需要 flush 的情况
如果 todo 还有剩余(即剩余空间不够)或 must_flush 被置为 1 的情况(即上面有 flush 的情况),需要做如下的处理:
先调用_IO_OVERFLOW 将前面写满的 buffer 写入物理文件中,如果此时写入失败的话,那就需要做处理,如果 to_do == 0,即本次要写入的东西都写到缓冲 buffer 里面了,所以是写入失败的,需要返回 EOF,否则,说明 n - todo 字节的 buffer 被写入缓冲了。
计算当前文件流对象的 buffer 大小 block_size(即_IO_buf_end-_IO_buf_base),如果 block_size 大于 128,则计算剩余未写入字节的余数 to_do % block_size,否则置为 0,计算 do_write 为剩余字节数减去上面计算处出的对齐余数。所以作用是将剩余的未写入字节数规整为 m*block_size + 剩余未满 block_size 字节的部分。
调用 new_do_write 写入上面计算出的一整块数据(这些数据大小是 m 个 buffer 缓冲区大小),注意,这里返回的实际写入字节数 count 如果小于我们前面计算的 do_write 大小,那就直接返回已写入的字节数 n - to_do(说明有写入失败的情况存在)。
最后,如果还有字节没有写入,那就需要调用_IO_default_xsputn 进行剩余字节的写入。
最后的返回信息仍然是 n - to_do 字节
4._IO_default_xsputn 的逻辑分析
处理局部变量赋值,同时考虑写入 size 小于等于 0 的情况,直接返回 0
开始循环处理 data 数据
如果还有剩余缓存空间,计算剩余缓存空间数量 count
如果缓存空间比要写入的字节数量多,那就更新 count 为需要写入字节数;
如果需要写入字节数大于 20,那就调用__mempcpy 写入
否则就使用循环赋值的方式进行赋值(注意这里就是 Glibc 的精髓所在了,正常我们写代码可能就考虑循环赋值或者 memcpy 解决这个问题了,但是这里区分了情况,应该是考虑到了两者的性能差,为了达到最优情况,使用了分段处理的方式)
循环结束条件是剩余写入字符为 0,或调用_IO_OVERFLOW 写入 buffer 的同时写入下一个字符成功
总结
_IO_new_file_xsputn 函数主题部分大致分为三个部分(考虑写入字节比较多的情况):
第一个部分写入文件流对象中剩余缓冲 buffer 大小的数据:调用__mempcpy 实现;
第二个部分将之前的数据写入物理文件后,调用 new_do_write 写入 M*block_size 大小的数据,block_size 是缓冲 buffer 的大小;
第三个部分是将剩余的不足一个缓冲 buffer 大小的数据写入,调用_IO_default_xsputn 实现,这里根据写入字节的大小,小于 20 字节的使用循环赋值,大于 20 字节的使用__mempcpy 实现。
版权声明: 本文为 InfoQ 作者【桑榆】的原创文章。
原文链接:【http://xie.infoq.cn/article/2361eabee1617e65b67560c5b】。文章转载请联系作者。
评论