背景
_IO_do_write 是 GlibcIO 库中的重要组成函数,负责向指定的文件流对象中写入指定字节的 buffer,是很多上层函数的调用基础,通过这个函数构建起了上层 C 函数与底层系统调用(汇编语言操作)的联系,那么 Glibc 中是如何实现这个函数的呢?接下来,我们就一起来看看这个函数的实现流程及其背后的原理。
函数入口分析
1.入参分析
注意这里我们将相关的一类函数都截取下来了,入参都基本一致:
这里我们可以看到 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.c
versioned_symbol (libc, _IO_new_do_write, _IO_do_write, GLIBC_2_1);
// glibc/libio/oldfileops.c
compat_symbol (libc, _IO_old_do_write, _IO_do_write, GLIBC_2_0);
复制代码
3._IO_new_do_write 的函数入口
可以看到,这个函数的实际实现还是在 new_do_write 中,但是注意了,这里做了一个参数的预处理,即
其余情况均返回 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 位置。
#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.c
off64_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 函数实现。
最后在外层再加 1 得到当前行的列号,整体的逻辑就是要更新当前的列号。
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
// glibc/libio/genops.c
unsigned
_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:
_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 进行了写入操作,之后根据写入的字节数量进行了文件流对象参数的调整,以便后续的使用。
评论