写点什么

Glibc---_IO_file_xsputn 函数逻辑分析

作者:桑榆
  • 2022-10-22
    广东
  • 本文字数:3827 字

    阅读完需:约 13 分钟

背景

_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.ccompat_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.csize_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 实现。

发布于: 刚刚阅读数: 6
用户头像

桑榆

关注

北海虽赊,扶摇可接;东隅已逝,桑榆非晚! 2020-02-29 加入

Android手机厂商-相机软件系统工程师 爬山/徒步/Coding

评论

发布
暂无评论
Glibc---_IO_file_xsputn函数逻辑分析_源码刨析_桑榆_InfoQ写作社区