持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情
背景
_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.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中,但是注意了,这里做了一个参数的预处理,即
- 如果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.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函数实现。
- 首先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.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:
- 如果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进行了写入操作,之后根据写入的字节数量进行了文件流对象参数的调整,以便后续的使用。