写点什么

Glibc---_IO_do_write 函数逻辑分析

作者:桑榆
  • 2022-10-21
    广东
  • 本文字数:3683 字

    阅读完需:约 12 分钟

背景

_IO_do_write 是 GlibcIO 库中的重要组成函数,负责向指定的文件流对象中写入指定字节的 buffer,是很多上层函数的调用基础,通过这个函数构建起了上层 C 函数与底层系统调用(汇编语言操作)的联系,那么 Glibc 中是如何实现这个函数的呢?接下来,我们就一起来看看这个函数的实现流程及其背后的原理。

函数入口分析

1.入参分析

注意这里我们将相关的一类函数都截取下来了,入参都基本一致:

  • FILE *:文件流对象

  • const char *:要写入的 buffer 地址,只读

  • size_t:要写入的字节数量

这里我们可以看到 libc_hidden_proto 宏,它的作用实际上是对外隐藏_IO_do_write 函数原型;

与_IO_do_write 相类似的其余三个函数通过其名字也能很好地进行区分:

_IO_new_do_write 是新版本实现;_IO_old_do_write 是旧版本实现,_IO_wdo_write 是针对宽字符的版本。

// glibc/libio/libioP.h
extern int _IO_do_write (FILE *, const char *, size_t); libc_hidden_proto (_IO_do_write)extern int _IO_new_do_write (FILE *, const char *, size_t);extern int _IO_old_do_write (FILE *, const char *, size_t);extern int _IO_wdo_write (FILE *, const wchar_t *, size_t);libc_hidden_proto (_IO_wdo_write)
复制代码

2.函数映射关系

从这里我们就能很好地看到 new 与 old 的作用,实际上是针对不同 GLIBC version 的操作。

这里我们优先分析 new 版本的代码,后续我们也是保持这个原则。

// glibc/libio/fileops.cversioned_symbol (libc, _IO_new_do_write, _IO_do_write, GLIBC_2_1);
// glibc/libio/oldfileops.ccompat_symbol (libc, _IO_old_do_write, _IO_do_write, GLIBC_2_0);
复制代码

3._IO_new_do_write 的函数入口

可以看到,这个函数的实际实现还是在 new_do_write 中,但是注意了,这里做了一个参数的预处理,即

  • 如果 to_do == 0,需要写入的字节数为 0,那就直接返回 0;

  • 如果调用 new_do_write 返回的值与要写入的字节数相等,说明写入成功,返回 0。

其余情况均返回 EOF,表示写入失败。

// glibc/libio/fileops.c/* Write TO_DO bytes from DATA to FP.   Then mark FP as having empty buffers. */int_IO_new_do_write (FILE *fp, const char *data, size_t to_do){  return (to_do == 0       || (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF; }libc_hidden_ver (_IO_new_do_write, _IO_do_write)
复制代码

函数逻辑分析

1.更新 fp->_offset

这里有两段逻辑,根据 fp 文件流对象的 mode 决定如何更新 fp->_offset

  • 如果_IO_IS_APPENDING 被置位,说明文件对象是以追加方式打开的,所以将 fp->_offset 赋值为_IO_pos_BAD,即定位到文件末尾;

  • 如果不是追加模式,就要考虑读写 buffer 块地址的信息了,读的尾指针不等于写的基指针,说明之前读写过程不一致,现在我们需要写入信息,所以需要调用_IO_SYSSEEK 进行调整,基于当前的位置(1 表示 SEEK_CUR)将两者调整到一致。

    如果返回结果是异常的-1,那就直接返回 0,表示写入字节数为 0.

    否则使用新的位置信息更新 fp->_offset

  if (fp->_flags & _IO_IS_APPENDING)    /* On a system without a proper O_APPEND implementation,       you would need to sys_seek(0, SEEK_END) here, but is       not needed nor desirable for Unix- or Posix-like systems.                                                                                                Instead, just indicate that offset (before and after) is       unpredictable. */    fp->_offset = _IO_pos_BAD;  else if (fp->_IO_read_end != fp->_IO_write_base)    {      off64_t new_pos    = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);      if (new_pos == _IO_pos_BAD)    return 0;      fp->_offset = new_pos;    }    /* _IO_pos_BAD is an off64_t value indicating error, unknown, or EOF.  */#define _IO_pos_BAD ((off64_t) -1)
复制代码

_IO_SYSSEEK 逻辑

查看函数调用关系后可知,_IO_file_seek->__lseek64->INLINE_SYSCALL_CALL,最后走到系统调用,通过该函数调整 offset 位置。

  • 入参中 offset 为调整的位置,可以是正或负,代表向后或向前;

  • int dir 表示当前基于的位置:

#define SEEK_SET 0 /* Seek from beginning of file. */

#define SEEK_CUR 1 /* Seek from current position. */

#define SEEK_END 2 /* Seek from end of file. */

/* The 'sysseek' hook is used to re-position an external file.   It generalizes the Unix lseek(2) function.   It matches the streambuf::sys_seek virtual function, which is   specific to this implementation. */typedef off64_t (*_IO_seek_t) (FILE *, off64_t, int);#define _IO_SYSSEEK(FP, OFFSET, MODE) JUMP2 (__seek, FP, OFFSET, MODE)#define _IO_WSYSSEEK(FP, OFFSET, MODE) WJUMP2 (__seek, FP, OFFSET, MODE)
off64_t_IO_file_seek (FILE *fp, off64_t offset, int dir){ return __lseek64 (fp->_fileno, offset, dir);}libc_hidden_def (_IO_file_seek)
// glibc/sysdeps/unix/sysv/linux/lseek64.coff64_t__lseek64 (int fd, off64_t offset, int whence) {#ifdef __NR__llseek loff_t res; int rc = INLINE_SYSCALL_CALL (_llseek, fd, (long) (((uint64_t) (offset)) >> 32), (long) offset, &res, whence); return rc ?: res;#else return INLINE_SYSCALL_CALL (lseek, fd, offset, whence);#endif}
复制代码

2.调用_IO_SYSWRITE 写入信息

最后调用了 SYSCALL_CANCEL 实现,这里就不做展开了,使用了系统调用。返回写入的字节数或者-1(失败情况)。

  count = _IO_SYSWRITE (fp, data, to_do);    /* The 'syswrite' hook is used to write data from an existing buffer   to an external file.  It generalizes the Unix write(2) function.   It matches the streambuf::sys_write virtual function, which is   specific to this implementation. */typedef ssize_t (*_IO_write_t) (FILE *, const void *, ssize_t);#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)#define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN)
// glibc/sysdeps/unix/sysv/linux/write.c/* Write NBYTES of BUF to FD. Return the number written, or -1. */ssize_t__libc_write (int fd, const void *buf, size_t nbytes){ return SYSCALL_CANCEL (write, fd, buf, nbytes);}libc_hidden_def (__libc_write)
复制代码

3.调用_IO_adjust_column 调整列参数

如果当前列参数不等于 0(即第一列),而且写入的字符数不等于 0,此时需要更新列参数,调用_IO_adjust_column 函数实现。

  • 首先 ptr 指向真正写入的最后一个字符;

  • 当 ptr 大于 line,即从后向前遍历字符,如果找到换行符,则结束,说明之前遍历的位于写入的最后一行,此时 line + count - ptr - 1 表示最后一行的字符数,返回该值即可;

  • 如果没有找到换行符,那就返回 start + count,即之前的列号加真正写入的字符数。

最后在外层再加 1 得到当前行的列号,整体的逻辑就是要更新当前的列号。

  if (fp->_cur_column && count)    fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;    // glibc/libio/genops.cunsigned_IO_adjust_column (unsigned start, const char *line, int count){  const char *ptr = line + count;  while (ptr > line)    if (*--ptr == '\n')      return line + count - ptr - 1;  return start + count;}libc_hidden_def (_IO_adjust_column)
复制代码

4.对读写 buffer 指针进行调整

先调用_IO_setg 将读相关的 base、ptr、end 更新为_IO_buf_base;

然后将写相关的 base、ptr 更新为_IO_buf_base。

注意这里写相关的 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 这块空间作为写缓存。

  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);  fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;  fp->_IO_write_end = (fp->_mode <= 0               && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))               ? fp->_IO_buf_base : fp->_IO_buf_end);
复制代码

总结

_IO_do_write 通过调用 new_do_write 的具体实现,考虑了文件打开的模式(写与追加模式),调佣系统函数 write 进行了写入操作,之后根据写入的字节数量进行了文件流对象参数的调整,以便后续的使用。

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

桑榆

关注

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

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

评论

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