简单回答:是的,确实就是一个设计失误。
Linux的文件实体本身,与文件名是解耦的,也就是说,一个文件可以拥有很多个文件名,删除掉其中一个文件名不影响访问到文件实体本身。
在内存中打开一个Linux文件,大致相当于在内存中创建了一个临时的新文件名用于访问这个文件实体,此时,硬盘上所有的文件名当然都可以删除掉。关闭内存中的文件,意味着删除了这个文件实体对应的最后一个文件名,此时引用计数为零,才会被删除。
——用程序员的话来讲,Linux的文件名就是一个共享指针,文件实体的删除其实类似于内存垃圾回收机制。删除其中一个指针不应当受到指针指向的实体所限制,删除掉所有指针才会导致实体释放。
而Windows最初设计为一个文件的实体只能依赖文件名来访问,文件名与文件实体之间强耦合,因此删除掉文件名之后缺乏有效的手段访问到一个文件的实体。想要访问到这个文件就必须提供某种,根据文件ID或者编号之类的来访问文件的方法。而目前Windows并没有提供类似的API。
我相信后来微软也意识到了这个问题,但大概是因为兼容性的原因,一直没有解决掉,也就延续了几十年之久。不知道未来会不会解决它,大概是不会了吧。
要回答这个问题,先要分析一下:如何让文件系统允许删除正在打开的文件?
这个问题看上去简单,其实设计起来比较复杂的。比如,在Linux上删除正在打开的文件,那么打开的文件FD是仍然可用的,文件的全部内容都是可见的,直到最后一个FD关闭了为止(引用计数为0)。文件系统只是把对应的文件设成“待删除”的状态,等待引用计数归零以后才真正在后台删除。这期间,“待删除”文件仍然占用着磁盘空间。这需要文件系统和操作系统支持:
1. 在内存中维护一个待删除的队列
为什么不在硬盘上?理论上可以在硬盘上,但会增大文件系统设计的复杂度。这跟FAT的文件名删除标记是不同的,待删除的文件,整个数据结构在磁盘上都是存在的,只有名字不存在。而FAT的文件名删除标记打上以后,FAT链表也会跟着释放掉。
2. 文件系统内部有一个清理任务(线程?)去完成真正的删除动作
这个比较容易理解,就是有个任务专门负责清理待删除文件。
3. 操作系统的cache管理能针对一个删除的名字(甚至是重名)文件做管理,并正确区分。
比如,现在有1.txt文件正在被使用,然后被删掉了,又创建了1.txt文件,又被打开了,那么此时操作系统内部,以文件名作为索引的话,是有两个1.txt文件的,一个是被删除的状态,但文件内容,以及对应的cache块都是存在的。这个看上去比较简单的事情,才是最麻烦的,要操作系统的cache管理层负责维护两个重名文件,而cache管理又往往是操作系统最核心的部分。
现在来看Windows的实现,虽然Windows提供了FILE_SHARE_DELETE,但是如果看一下WRK的代码就知道了,这个玩意仅仅只是打了个标记,根本不涉及任何内核上限制性的内容,所以这个flag不能解释任何原因。
有兴趣的可以看一下IoCheckShareAccess的实现:
那么Windows不能这么做的原因,只能到文件系统驱动里去找,微软开源了FAT的驱动源码(FASTFAT)通过对比WRK源码可以发现,Windows内核标记一个文件对象的,是一个叫FILE_OBJECT的数据结构:
typedef struct _FILE_OBJECT { CSHORT Type; CSHORT Size; PDEVICE_OBJECT DeviceObject; PVPB Vpb; PVOID FsContext; PVOID FsContext2; PSECTION_OBJECT_POINTERS SectionObjectPointer; PVOID PrivateCacheMap; NTSTATUS FinalStatus; struct _FILE_OBJECT *RelatedFileObject; BOOLEAN LockOperation; BOOLEAN DeletePending; BOOLEAN ReadAccess; BOOLEAN WriteAccess; BOOLEAN DeleteAccess; BOOLEAN SharedRead; BOOLEAN SharedWrite; BOOLEAN SharedDelete; ULONG Flags; UNICODE_STRING FileName; LARGE_INTEGER CurrentByteOffset; ULONG Waiters; ULONG Busy; PVOID LastLock; KEVENT Lock; KEVENT Event; PIO_COMPLETION_CONTEXT CompletionContext; } FILE_OBJECT;
对于Linux来说是一个叫file的数据结构
struct file { union { struct llist_node fu_llist; struct rcu_head fu_rcuhead; } f_u; struct path f_path; struct inode *f_inode; /* cached value */ const struct file_operations *f_op; /* * Protects f_ep, f_flags. * Must not be taken from IRQ context. */ spinlock_t f_lock; enum rw_hint f_write_hint; atomic_long_t f_count; unsigned int f_flags; fmode_t f_mode; struct mutex f_pos_lock; loff_t f_pos; struct fown_struct f_owner; const struct cred *f_cred; struct file_ra_state f_ra; u64 f_version; #ifdef CONFIG_SECURITY void *f_security; #endif /* needed for tty driver, and maybe others */ void *private_data; #ifdef CONFIG_EPOLL /* Used by fs/eventpoll.c to link all the hooks to this file */ struct hlist_head *f_ep; #endif /* #ifdef CONFIG_EPOLL */ struct address_space *f_mapping; errseq_t f_wb_err; errseq_t f_sb_err; /* for syncfs */ } __randomize_layout __attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
虽然两边差异很大,但可以看到相似的成员有两个:
FILE_OBJECT->FileName <===> file->f_path //文件路径 FILE_OBJECT->FsContext2 <===> file->private_data //驱动私有信息
而Linux还多了一个很关键的结构:file->f_inode
inode结构对应的有点类似于Windows里的FCB,但FCB结构是各个文件系统自己定义的,比如FAT的定义:
然后各自的文件系统驱动再通过file --> inode或者FILE_OBJECT --> FCB结构关联各自操作系统内核的cache管理部分,形成一套完整的cache管理机制(Windows通过FILE_OBJECT->SectionObjectPointer找到对应的cache内容)。
对比二者的区别的话,可以发现Linux在文件系统公共层(VFS)上有定义inode结构,而Windows没有,同时Linux的inode结构不是以名字索引的,而是以一个序号i_ino:
struct inode { /*.....*/ /* Stat data, not accessed from path walking */ unsigned long i_ino; };
问题就出在这里:
Windows的文件系统驱动里只能通过名字来索引FILE_OBJECT结构的,不像Linux那样可以通过序号(i_ino)来找到inode,那么一旦一个文件的文件名处于无效的状态,那么内核就没办法检索出来一个文件名对应的FILE_OBJECT,以及它对应的cache节点。而一旦cache管理出了问题,那么内核是必然要崩溃的。
所以Windows不能这么做的原因是:
受限于FILE_OBJECT数据结构的设计(可能最早是因为FAT的设计问题),导致内核/文件系统驱动中不能正确处理重名FILE_OBJECT问题,重名可能导致内核cache管理部分混乱,所以Windows不允许删除一个正在打开的文件。这是一个内核设计的问题,上层的各种行为只是表面现象。
那么Windows的FILE_SHARE_DELETE是怎么回事呢?其实FILE_SHARE_DELETE并没有把文件真的删除。如果你创建了一个带FILE_SHARE_DELETE标记的文件,然后删除它,再创建一个重名的文件,Windows会提示你创建失败:
所以,Windows仍然是以文件名的方式管理cache的。
因为Linux使用i_ino在这个序号来管理文件cache,不存在重名问题,所以自然可以删除文件不受影响。
顺便补充一下,各种杀毒软件的文件粉碎机,本质上就是干掉FILE_OBJECT->SectionObjectPointer里的数据结构,释放内核中对应的cache节点,这样FILE_OBJECT重名也不会波及到内核的cache管理导致蓝屏了。
其实Windows是支持FILE_OPEN_BY_FILE_ID的动作的,但可能是因为兼容性的问题(FILE_OBJECT改动会影响很多驱动),所以微软只能保持现状,而Linux则不考虑兼容性问题,灵活性自然要好很多。
那么,这个算不算是Windows的设计失误?我个人认为勉强算是,非要说bug是feature也是可以接受的。Windows的这个行为导致了其文件系统与POSIX规范不完全兼容,对于一些开源软件来说,不算友好。当然了,微软为了兼容性做了很大的让步,甚至包括bug的兼容性,比如Win9x里内存free以后仍然可用的bug,跳过Windows9这个版本号等等,所以为了文件系统的兼容性而保持这种行为也是可以理解的。毕竟Windows是要考虑兼容性问题的,每个大版本的改动都会导致一些驱动不能正常工作,这对于用户来说是很难接受的。
Linux的fs.h变化很大,不过Linux里大部分软件都是开源的,所以只要重新编译一下就可以了,不过对于某些闭源软件来说,Linux这种频繁变动的设计也是很不友好的。
补充:
其它回答和评论里提到,我这个只是描述不能删除的原因,没有说为什么这么设计。我个人感觉是已经说的很清楚了,就是兼容性的问题。展开来说就是:
Windows使用文件名作为内核对象(FILE_OBJECT)的唯一标识符 -> 文件名不能重名 -> 文件不能在打开状态下被删除。
如果继续向上追溯,是因为DOS时代的文件系统FAT就没有inode的概念,FAT的文件属性保存在目录里而不是单独的一个区域,Win9x只支持FAT。
inode的概念最早是从UNIX-like的文件系统中出现的,属于文件系统的另一个发展方向。Windows也试图在文件系统中引入类似inode的定义,比如NTFS的$MFT里就有类似inode的定义。
技术发展到今天,文件系统的底层数据结构已经不是影响文件系统行为的决定性因素,比如FAT32没有inode,但在Linux上一样可以获得inode号。所以到了今天,Windows不能删除一个打开的文件,本质上不是技术能力问题,也不是文件系统数据结构问题,纯粹是为了保持兼容性而做出的牺牲。
还有的答案提到,把FILE_OBJECT的里的filename清空,不影响使用,这个确实不影响,但又不是只有这一个地方保存了文件名。以FAT为例,至少在FCB里,在Name splay tree里都有文件名(FASTFAT: FCB_STATE_NAMES_IN_SPLAY_TREE)。核心的问题是Windows内核中,不具备类似Linux那种的inode结构,inode结构是一个与文件名无关的设计,没有这个前提,要删除打开的文件就变得麻烦了。
断开一切FILE_OBJECT里的引用确实没问题,甚至都不会蓝屏,但是如果是移动设备(U盘),你就会发现这个盘怎么也不能安全弹出了,因为有一个data cache一直释放不掉。因为操作系统内核(NTOSKRNL的Cache Manager,就是WDK里一堆CC开头的API)跟这一整套东西紧紧的绑定在一起。
最后,为什么我用WRK,因为WRK是公开的。兼容性又不是指代码完全不变,API行为才是最大的兼容性考验。况且WIN10 20H2的FILE_OBJECT也就比WRK上新增了三个成员,都是在结尾扩展的,前面的TYPE/SIZE完全可以覆盖这种兼容性的缺陷。猜猜WRK是基于哪个版本的?
这就是个历史问题,历史上这么做没什么不对的,因为允许文件在打开的时候被删除,对于很多软件来说是灾难性的事情。
但是既然这么实现了,就有软件依赖于这个Feature(譬如说直接用文件做跨进程锁),结果依赖了几十年到了今天就成了个严重的问题。事实上微软自身也被这个问题搞得焦头烂额,Windows更新过程中必须要重启很大程度上就是因为这个原因。这个事情被客户吐槽了不知道多少次,看看微软做出的努力也能知道这个Feature要移除会带来多大的问题(否则十几年前就移除了)……更有很多恶意软件利用这一点来规避杀毒软件的清除……
但是现代的操作系统都开始弱化文件系统的存在感,我认为这个问题在未来随着操作系统给软件的隔离和限制越来越多,会逐步的淡化……
回答 why 的问题能不能不要有那么多长篇大论 what/how 的答案?
Windows 的独占强制文件锁是一个设计失误。它的问题在于:
首先说第二点,独占强制锁要解决的不是数据丢失问题。这个很好理解,如果没有 process 在访问一个文件,用户还是想怎么删就怎么删。它要解决的也不是 app 私有文件的 integrity 问题。私有文件放到用户看不到的私有 container 里就好。
回到第一点,它要解决的是早期 PC 的一个性能问题。首先上面说了,它面对不是 app 私有文件的问题,所以这里是用户文件,也就是诸如 .docx, .pptx, .psd 这样的问题。而且上面说了,它要解决的并不是这类文件被误删的问题,那是什么问题呢?考虑这种情况:
一个用户打开了三个 .doc 文件。都做了编辑。然后他删掉了其中一个,回到 Word 之后,Word 因为处理不好文件被删除 crash 了。结果另外两个文件的编辑也丢失了。
其实这个情况今天很好解决。一个普遍方案是,app 在打开文件的时候把文件 copy 到私有 container 中,编辑结束再 copy 回原来位置。但是早期 PC 时代这样做还是比较消耗资源,所以 Microsoft 就搞了一个 performance hack。
所有你看起来奇怪的设计,大都是 performance hack。