IO FILE 之fclose 详解
字数 1287 2025-08-05 08:19:22

IO FILE 之 fclose 详解

1. 概述

本教程详细分析 glibc 中 fclose 函数的实现原理,重点关注文件关闭流程和堆内存管理机制。fclose 函数主要完成以下工作:

  1. 将 FILE 结构体从 _IO_list_all 链表中移除
  2. 关闭文件并释放缓冲区
  3. 释放 FILE 结构体内存

2. 核心函数分析

2.1 _IO_new_fclose 主函数

位于 /libio/iofclose.c 的核心函数:

int _IO_new_fclose (_IO_FILE *fp) {
    int status;
    
    // 1. 从链表中移除
    if (fp->_IO_file_flags & _IO_IS_FILEBUF)
        _IO_un_link ((struct _IO_FILE_plus *) fp);
    
    // 2. 关闭文件和释放缓冲区
    if (fp->_IO_file_flags & _IO_IS_FILEBUF)
        status = _IO_file_close_it (fp);
    
    // 3. 最终确认
    _IO_FINISH (fp);
    
    // 4. 释放内存
    if (fp != _IO_stdin && fp != _IO_stdout && fp != _IO_stderr) {
        fp->_IO_file_flags = 0;
        free(fp);
    }
    return status;
}

2.2 _IO_un_link - 链表移除

位于 /libio/genops.c,负责将 FILE 结构体从全局链表 _IO_list_all 中移除:

void _IO_un_link (struct _IO_FILE_plus *fp) {
    if (fp->file._flags & _IO_LINKED) {  // 检查标志位
        if (_IO_list_all == NULL)        // 空链表
            ;
        else if (fp == _IO_list_all) {   // 链表头节点
            _IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
            ++_IO_list_all_stamp;
        }
        else {                           // 中间节点
            for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
                if (*f == (_IO_FILE *) fp) {
                    *f = fp->file._chain;
                    ++_IO_list_all_stamp;
                    break;
                }
        }
        fp->file._flags &= ~_IO_LINKED;  // 清除标志位
    }
}

关键点:

  • _IO_LINKED 标志位 (0x80) 表示结构体是否在链表中
  • 更新 _IO_list_all_stamp 防止并发问题
  • 修改 _flags 清除链接状态

2.3 _IO_file_close_it - 文件关闭和缓冲区释放

位于 /libio/fileops.c,核心功能函数:

int _IO_new_file_close_it (_IO_FILE *fp) {
    // 1. 刷新输出缓冲区
    if ((fp->_flags & _IO_NO_WRITES) == 0 && 
        (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
        write_status = _IO_do_flush (fp);
    
    // 2. 关闭文件描述符
    int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0 
                       ? _IO_SYSCLOSE (fp) : 0);
    
    // 3. 释放缓冲区
    _IO_setb (fp, NULL, NULL, 0);  // 释放并置空缓冲区
    _IO_setg (fp, NULL, NULL, NULL); // 清空输入缓冲区指针
    _IO_setp (fp, NULL, NULL);      // 清空输出缓冲区指针
    
    // 4. 确保从链表移除
    _IO_un_link ((struct _IO_FILE_plus *) fp);
    
    // 5. 设置关闭状态
    fp->_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS;
    fp->_fileno = -1;
    fp->_offset = _IO_pos_BAD;
    
    return close_status ? close_status : write_status;
}

2.3.1 _IO_do_flush 缓冲区刷新

宏定义实现:

#define _IO_do_flush(_f) \
    ((_f)->_mode <= 0 \
     ? _IO_do_write(_f, (_f)->_IO_write_base, \
                   (_f)->_IO_write_ptr-(_f)->_IO_write_base) \
     : _IO_wdo_write(_f, (_f)->_wide_data->_IO_write_base, \
                   ((_f)->_wide_data->_IO_write_ptr \
                    - (_f)->_wide_data->_IO_write_base)))

实际调用 _IO_do_write 执行系统调用写入数据。

2.3.2 _IO_SYSCLOSE 文件关闭

调用 vtable 中的 __close 函数:

int _IO_file_close (_IO_FILE *fp) {
    return close_not_cancel (fp->_fileno);
}
#define close_not_cancel(fd) __close (fd)  // 最终调用系统调用 close

2.3.3 _IO_setb 缓冲区释放

关键的内存释放点:

void _IO_setb (_IO_FILE *f, char *b, char *eb, int a) {
    if (f->_IO_buf_base && !(f->_flags & _IO_USER_BUF))
        free (f->_IO_buf_base);  // 释放缓冲区内存
    f->_IO_buf_base = b;
    f->_IO_buf_end = eb;
    if (a) 
        f->_flags &= ~_IO_USER_BUF;
    else 
        f->_flags |= _IO_USER_BUF;
}

2.4 _IO_FINISH - 最终确认

调用 vtable 中的 __finish 函数:

void _IO_new_file_finish (_IO_FILE *fp, int dummy) {
    if (_IO_file_is_open (fp)) {
        _IO_do_flush (fp);
        if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
            _IO_SYSCLOSE (fp);
    }
    _IO_default_finish (fp, 0);
}

_IO_default_finish 再次确认释放:

void _IO_default_finish (_IO_FILE *fp, int dummy) {
    if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) {
        free (fp->_IO_buf_base);
        fp->_IO_buf_base = fp->_IO_buf_end = NULL;
    }
    // 处理标记和保存缓冲区
    if (fp->_IO_save_base) {
        free (fp->_IO_save_base);
        fp->_IO_save_base = NULL;
    }
    _IO_un_link ((struct _IO_FILE_plus *) fp);
}

3. 关键数据结构变化

3.1 _IO_list_all 链表变化

关闭前:

_IO_list_all -> FILE1 -> FILE2 -> ... -> 当前FILE -> ...

关闭后:

_IO_list_all -> FILE1 -> FILE2 -> ... (当前FILE已移除)

3.2 FILE 结构体标志位变化

  • _flags: 清除 _IO_LINKED 标志
  • _fileno: 设置为 -1
  • _IO_buf_base/_IO_buf_end: 设置为 NULL

3.3 堆内存释放顺序

  1. 输出缓冲区内存 (通过 _IO_setb)
  2. 输入缓冲区内存 (如果有)
  3. FILE 结构体内存 (最后释放)

4. 安全注意事项

  1. 双重释放风险_IO_setb_IO_default_finish 都会检查并释放缓冲区,但通过 _IO_USER_BUF 标志避免
  2. 链表操作原子性_IO_list_all_stamp 用于检测并发修改
  3. 标准流特殊处理:不释放 stdin/stdout/stderr 的 FILE 结构体

5. 调用流程图

fclose()
|
├─ _IO_un_link()        // 从链表移除
│
├─ _IO_file_close_it()  // 主要关闭逻辑
│   ├─ _IO_do_flush()   // 刷新缓冲区
│   ├─ _IO_SYSCLOSE()   // 关闭文件描述符
│   └─ _IO_setb()       // 释放缓冲区
│
├─ _IO_FINISH()         // 最终确认
│   └─ _IO_default_finish()
│
└─ free()               // 释放FILE结构体

6. 总结

fclose 函数的完整执行流程体现了 glibc IO 系统的内存管理和资源清理机制,主要特点包括:

  1. 严格的链表管理确保全局状态一致
  2. 多重检查确保资源完全释放
  3. 通过标志位 (_IO_LINKED, _IO_USER_BUF 等) 控制流程
  4. 对标准流的特殊处理
  5. 缓冲区释放先于 FILE 结构体释放的安全顺序

理解这些机制对于分析 IO 相关的内存问题和安全漏洞至关重要。

IO FILE 之 fclose 详解 1. 概述 本教程详细分析 glibc 中 fclose 函数的实现原理,重点关注文件关闭流程和堆内存管理机制。 fclose 函数主要完成以下工作: 将 FILE 结构体从 _IO_list_all 链表中移除 关闭文件并释放缓冲区 释放 FILE 结构体内存 2. 核心函数分析 2.1 _IO_new_fclose 主函数 位于 /libio/iofclose.c 的核心函数: 2.2 _IO_un_link - 链表移除 位于 /libio/genops.c ,负责将 FILE 结构体从全局链表 _IO_list_all 中移除: 关键点: _IO_LINKED 标志位 (0x80) 表示结构体是否在链表中 更新 _IO_list_all_stamp 防止并发问题 修改 _flags 清除链接状态 2.3 _IO_file_close_it - 文件关闭和缓冲区释放 位于 /libio/fileops.c ,核心功能函数: 2.3.1 _IO_do_flush 缓冲区刷新 宏定义实现: 实际调用 _IO_do_write 执行系统调用写入数据。 2.3.2 _IO_SYSCLOSE 文件关闭 调用 vtable 中的 __close 函数: 2.3.3 _IO_setb 缓冲区释放 关键的内存释放点: 2.4 _IO_FINISH - 最终确认 调用 vtable 中的 __finish 函数: _IO_default_finish 再次确认释放: 3. 关键数据结构变化 3.1 _IO_list_all 链表变化 关闭前: 关闭后: 3.2 FILE 结构体标志位变化 _flags : 清除 _IO_LINKED 标志 _fileno : 设置为 -1 _IO_buf_base / _IO_buf_end : 设置为 NULL 3.3 堆内存释放顺序 输出缓冲区内存 (通过 _IO_setb ) 输入缓冲区内存 (如果有) FILE 结构体内存 (最后释放) 4. 安全注意事项 双重释放风险 : _IO_setb 和 _IO_default_finish 都会检查并释放缓冲区,但通过 _IO_USER_BUF 标志避免 链表操作原子性 : _IO_list_all_stamp 用于检测并发修改 标准流特殊处理 :不释放 stdin/stdout/stderr 的 FILE 结构体 5. 调用流程图 6. 总结 fclose 函数的完整执行流程体现了 glibc IO 系统的内存管理和资源清理机制,主要特点包括: 严格的链表管理确保全局状态一致 多重检查确保资源完全释放 通过标志位 ( _IO_LINKED , _IO_USER_BUF 等) 控制流程 对标准流的特殊处理 缓冲区释放先于 FILE 结构体释放的安全顺序 理解这些机制对于分析 IO 相关的内存问题和安全漏洞至关重要。