Glibc---_IO_file_xsputn函数逻辑分析

504 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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实现。