持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第14天,点击查看活动详情
背景
_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是针对宽字符的版本。
// glibc/libio/libioP.h
extern size_t _IO_file_xsputn (FILE *, const void *, size_t);
libc_hidden_proto (_IO_file_xsputn)
extern size_t _IO_new_file_xsputn (FILE *, const void *, size_t);
extern size_t _IO_old_file_xsputn (FILE *, const void *, size_t);
extern size_t _IO_wfile_xsputn (FILE *, const void *, size_t);
libc_hidden_proto (_IO_wfile_xsputn)
2.函数映射关系
从这里我们就能很好地看到new与old的作用,实际上是针对不同GLIBC version的操作。
这里我们优先分析new版本的代码,后续我们也是保持这个原则。
// glibc/libio/fileops.c
versioned_symbol (libc, _IO_new_file_xsputn, _IO_file_xsputn, GLIBC_2_1);
// glibc/libio/oldfileops.c
compat_symbol (libc, _IO_old_file_xsputn, _IO_file_xsputn, GLIBC_2_0);
3._IO_new_file_xsputn的函数入口
这里截取了其中关于数据处理的三个部分,分别是
- 向_IO_write_ptr中拷贝数据(因为使用缓存的情况下,数据并不是直接写到物理文件中的);
- 调用new_do_write进行数据写入
- 调用_IO_default_xsputn进行数据写入
// glibc/libio/fileops.c
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
...
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
...
count = new_do_write (f, s, do_write);
...
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
...
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
函数逻辑分析
1.局部变量申请与特殊情况处理
这里申请了一些局部变量,后续使用;
针对输入size小于等于0的情况,直接返回0,无需做其他处理。
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
size_t count = 0;
if (n <= 0)
return 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。
/* 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. */
3.开始填充buffer
(1).先调用__mempcpy 填充写缓冲buffer
如果有剩余buffer空间大于要写入的size n,那就将count更新为to_do大小,然后调用__mempcpy将[s,s+count]范围内的字符拷贝到f->_IO_write_ptr处,同步更新s指针和to_do的大小。
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
(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 字节
if (to_do + must_flush > 0)
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
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;
/* 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;
4._IO_default_xsputn的逻辑分析
- 处理局部变量赋值,同时考虑写入size小于等于0的情况,直接返回0
-
开始循环处理data数据
- 如果还有剩余缓存空间,计算剩余缓存空间数量count
- 如果缓存空间比要写入的字节数量多,那就更新count为需要写入字节数;
- 如果需要写入字节数大于20,那就调用__mempcpy 写入
- 否则就使用循环赋值的方式进行赋值(注意这里就是Glibc的精髓所在了,正常我们写代码可能就考虑循环赋值或者memcpy解决这个问题了,但是这里区分了情况,应该是考虑到了两者的性能差,为了达到最优情况,使用了分段处理的方式)
- 循环结束条件是剩余写入字符为0,或调用_IO_OVERFLOW写入buffer的同时写入下一个字符成功
size_t
_IO_default_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (char *) data;
size_t more = n;
if (more <= 0)
return 0;
for (;;)
{
/* Space available. */
if (f->_IO_write_ptr < f->_IO_write_end)
{
size_t count = f->_IO_write_end - f->_IO_write_ptr;
if (count > more)
count = more;
if (count > 20)
{
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
}
else if (count)
{
char *p = f->_IO_write_ptr;
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_new_file_xsputn函数主题部分大致分为三个部分(考虑写入字节比较多的情况):
第一个部分写入文件流对象中剩余缓冲buffer大小的数据:调用__mempcpy 实现;
第二个部分将之前的数据写入物理文件后,调用new_do_write写入M*block_size大小的数据,block_size是缓冲buffer的大小;
第三个部分是将剩余的不足一个缓冲buffer大小的数据写入,调用_IO_default_xsputn实现,这里根据写入字节的大小,小于20字节的使用循环赋值,大于20字节的使用__mempcpy实现。