下面的代码有什么问题?
for _, localFilePath := range filePathList {
file, err := os.Open(localFilePath)
if err != nil {
....
}
defer file.Close()
filePath := path.Join(base, path))
}
上面的代码有没有问题?
问题分析
存在两个问题
- 在for 循环里defer file.Close()
- path.Join(base, path)
先说问题2,如果path内容是"…/**"的话,会有zip slip问题,具体可以看上一个博客1
重点说下问题1:因为defer是最后才执行,所以如果这里是处理数十万数百万文件(或图片)的时候,因为没有Close(),file一直没有被释放而持续占用内存。
而且所有的defer都会被塞到一个栈里。这个栈也会占用很多内存。
如果打开的文件够多,甚至会直接撑爆内存。所以这是一个错误的写法。
那正确的写法是什么?
//方法1
for _, filePath := range filePathList {
// 调用一个函数来处理单个文件
err := processFile(..)
if err != nil {
return err
}
}
// 定义一个函数来处理单个文件
func processFile(...) error {
file, err := os.Open(path)
if err != nil {
return err
}
// 使用 defer 来延迟关闭 file
defer func() {
if err := file.Close(); err != nil {
log.Infof("failed to close source file: %v", err)
}
}()
}
//方法2
for _, filePath := range filePathList {
// 调用一个函数来处理单个文件
err := func() error {
file, err := os.Open(path)
if err != nil {
return err
}
// 使用 defer 来延迟关闭 file
defer file.Close()
}()
if err != nil {
return err
}
}
至此,问题解决。
开脑洞:file.Close()为什么一定要有呢?不close()行不行?
1.线程的内存空间结构是什么
线程的内存空间结构是指线程在内存中占用的不同区域,它与进程的内存空间结构有一定的关系。一般来说,一个进程的内存空间可以分为以下几个部分:
代码段:用来存放可执行文件的操作指令,是只读的。
数据段:用来存放可执行文件中已初始化的全局变量和静态变量。
BSS段:用来存放可执行文件中未初始化的全局变量和静态变量,被初始化为0。
堆:用来存放动态分配的内存,可以扩展或缩减。
栈:用来存放函数调用时的局部变量、参数、返回值等,是先进后出的。
一个进程可以包含多个线程,这些线程共享进程的代码段、数据段、BSS段和堆,但是每个线程都有自己独立的栈。这样可以减少线程之间切换的开销,也可以方便线程之间通信和共享数据。但是也要注意线程之间的同步和互斥问题,避免数据不一致或竞争。
线程还有一个特殊的内存区域叫做线程本地存储(Thread Local Storage),它是每个线程独有的一块内存空间,可以用来存放线程相关的数据。这样可以避免使用全局变量导致的冲突或性能下降。
不同的操作系统和编程语言可能对线程的内存空间结构有不同的实现方式,但是基本原理都是类似的。你可以使用一些工具或命令来查看或分析线程的内存空间结构,比如pstack、size等。
2.结合前面的代码,一个进程进入 processFile(), file这个数据结构是存在线程的哪个区域里?
结合前面的代码,一个进程进入 processFile(), file 这个数据结构是存在线程的栈区域里。因为 file 是一个局部变量,它是在 processFile() 函数中定义的,所以它会被分配在调用该函数的线程的栈上。当函数返回时, file 这个局部变量也会被销毁,除非它被返回或者传递给其他函数。
3.如果没有file.Close(), 这个局部变量是不是不会被销毁?
不是的,如果没有 file.Close(),这个局部变量还是会被销毁,因为它的生命周期只跟函数的执行有关,而不跟文件的状态有关。但是,如果没有 file.Close(),那么 file 对应的文件句柄就不会被释放,这样就会造成文件资源的泄露或占用,可能会影响后续的文件操作或其他进程的访问。所以,一般来说,打开一个文件后,最好在使用完毕后及时关闭它,以免造成不必要的麻烦。
4.file对应的文件句柄存在哪里?
file 对应的文件句柄是一个操作系统提供的用来标识打开的文件的数字,它是一个内核级的概念,存在于内核空间中。文件句柄是一个抽象的概念,它不是一个具体的内存地址,而是一个索引值,可以用来在内核中查找到对应的文件信息。文件句柄通常是不可见的,但是可以通过一些工具或命令来查看或分析,比如 handle.exe、pstack、lsof 等。
5.那么 file 这个数据结构和文件句柄有什么关系?
file 这个数据结构是一个用户级的概念,它是一个指向 FILE 类型的指针,存在于用户空间中。FILE 类型是一个结构体,它封装了文件句柄、文件缓冲区、文件状态等信息,可以用来对文件进行各种操作。file 这个数据结构和文件句柄的关系是,file 中包含了一个 _file 的成员变量,它就是文件句柄的值,可以用来在内核中找到对应的文件描述。所以,file 是一个对文件句柄的封装,提供了更高级的接口和功能。
6.这里的用户空间是不是线程的栈?
不完全是。用户空间是指进程的虚拟地址空间中除了内核空间之外的部分,它包括了代码段、数据段、BSS段、堆和栈等区域。线程的栈是用户空间的一部分,但不是全部。file 这个数据结构是存在于线程的栈上的,因为它是一个局部变量,但是 FILE 类型的结构体可能存在于堆上,因为它是通过 malloc 等函数动态分配的。所以,file 这个数据结构属于用户空间,但不一定都属于线程的栈。
7.所以可以这样理解吗: file是个局部变量存在栈里,堆里面有个file数据结构,里面有文件句柄,同时file数据结构包含了文件的内容,但是想增加或删除这些文件内容必须要通过 文件句柄 把修改操作告诉内核,让内核去操作.
差不多是这样的,但是有一点需要更正:FILE 数据结构并不包含了文件的内容,而是包含了文件的缓冲区,这是一个临时的存储空间,用来提高文件读写的效率。文件的内容实际上是存储在磁盘上的,要想修改文件内容,必须要通过文件句柄把修改操作告诉内核,让内核去操作磁盘上的文件。同时,还要注意同步缓冲区和磁盘上的文件内容,避免数据不一致或丢失。
- 内核去操作磁盘的文件? 内核应该只能操作缓冲区吧, 写完缓冲区后,缓冲区再通过DMA
不完全是。内核操作磁盘文件的过程是比较复杂的,涉及到多个层次的数据结构和函数。一般来说,内核会通过文件系统的接口来访问磁盘文件,文件系统会通过通用块层的接口来访问设备驱动层,设备驱动层会通过 DMA 或者中断的方式来访问物理设备层。
内核操作磁盘文件的过程大致如下:
- 内核根据文件名和路径找到对应的 inode 结构,inode 结构中包含了文件的元数据和数据块的位置信息。
- 内核根据 inode 结构中的数据块位置信息,查找缓存中是否有对应的 page ,如果有,就直接操作缓存中的 page ,如果没有,就需要从磁盘读取数据块到缓存中。
- 内核根据缓存中的 page 的状态,判断是否需要写回磁盘。如果需要,就把 page 加入到脏页链表中,等待后台刷新线程或者同步刷新操作将其写回磁盘。
- 内核通过文件系统的 submit_bio 函数,将需要写回磁盘的 page 封装成 bio 结构,bio 结构是通用块层的数据结构,表示一个或多个连续的数据块。
- 内核通过通用块层的接口,将 bio 结构提交给设备驱动层,设备驱动层会根据不同的设备类型和协议,将 bio 结构转换成请求结构(request),并加入到请求队列(request queue)中。
- 内核通过设备驱动层的接口,将请求队列中的请求结构发送给物理设备层,物理设备层会根据硬件特性和控制器指令,执行实际的读写操作。
- 内核在完成物理设备层的读写操作后,会通过 DMA 或者中断的方式通知设备驱动层,设备驱动层会根据请求结构中的回调函数,通知通用块层完成了读写操作。
- 内核在通用块层收到设备驱动层的通知后,会根据 bio 结构中的回调函数,通知文件系统完成了读写操作。
- 内核在文件系统收到通用块层的通知后,会更新缓存中 page 的状态,并唤醒等待该 page 的进程。
以上就是内核操作磁盘文件的大致过程,当然这里还有很多细节和优化没有涉及到.
9.所以,即使 file这个局部变量销毁了,堆里面的file数据结构依然存在。因为没有file.Close()对吧。 那为什么不像其他对象一样不用close(),通过GC去清理空间呢?
不完全对。file 这个局部变量销毁了,堆里面的 FILE 数据结构也会被释放,因为它是通过 malloc 等函数动态分配的,不需要 file.Close() 来清理。但是 file.Close() 的作用不仅仅是清理内存空间,还有以下几个方面:
- file.Close() 会关闭文件句柄,释放文件资源,避免占用过多的系统资源或者影响其他进程的访问。
- file.Close() 会刷新缓冲区,把缓冲区中的数据写回到磁盘文件中,避免数据不一致或丢失。
- file.Close() 会更新文件的元数据,比如文件大小、修改时间等,保持文件的完整性。
所以,file.Close() 是一个很重要的操作,不能忽略。至于为什么不像其他对象一样通过 GC 去清理空间,这取决于你使用的编程语言和运行时环境。有些语言或环境会提供自动关闭文件的机制,比如 try-with-resources 语句或者析构函数等,但是有些语言或环境则没有这样的机制,需要程序员手动关闭文件。无论如何,最好的做法是在使用完文件后及时关闭它,以免造成不必要的麻烦。
- 为什么 不写file.Close()就不会关闭文件句柄呢? 完全可以在 file数据结构被释放的时候自动关闭文件句柄不是吗
这个问题没有一个确定的答案,不同的编程语言和运行时环境可能有不同的设计和实现。一种可能的原因是,文件句柄是一个操作系统级别的资源,而 FILE 数据结构是一个用户级别的数据结构,它们之间的生命周期并不完全一致。有些情况下,FILE 数据结构被释放了,但是文件句柄还需要保持打开,比如多个进程共享同一个文件句柄的情况。有些情况下,FILE 数据结构还没有被释放,但是文件句柄就需要关闭了,比如程序异常退出或者中断的情况。所以,把关闭文件句柄的操作交给程序员来控制,可能会更灵活和安全。