下面的问题可能听起来有点复杂,但实际上这是一个相当简单,普遍和共同的问题,即在同一个文件上工作的三个进程。 在下面的文本中,我试图用一些说明性的例子将问题分解成一组特定的需求。
有一个名为index的文本文件,其中包含一些元数据。
有一个应用程序( APP ),它理解文件格式并对其进行有意义的改变。
该文件存储在版本控制系统( VCS )下,这是其他用户在同一文件上执行的更改的来源。
我们需要devise一个应用程序( APP ),这个应用程序可以在一个合理的文件中使用这个文件,最好不要和VCS进行太多的交互,因为它假定VCS被用来保存一个大的项目, 索引文件只是一小部分并且用户可能希望在任何时候更新VCS ,而不考虑APP内的任何正在进行的操作。 在这种情况下, APP应该以防止任何可能的数据丢失的方式妥善处理这种情况。
请注意, VCS是未指定的,它可能是perforce,git,svn,tarballs,闪存驱动器或您最喜爱的WWII莫尔斯电台和文本编辑器。
文本文件可以是二进制文件,这并不会改变什么。 但是考虑到VCS存储,它很容易被合并,因此文本/人类可读的格式是最合适的。
可能的例子有:复杂的configuration(AI行为树,游戏对象描述),资源列表,其他不需要手工编辑的事物,与手头上的项目有关,但是历史很重要。
请注意,除非您急于实现您自己的版本控制系统,否则将大部分configuration“外包”到某种基于客户端 – 服务器的外部解决scheme并不能解决问题 – 您仍然需要在版本控制系统中保存参考文件在数据库中对所讨论的configuration版本的引用。 这意味着,你仍然有同样的问题,但规模有点小 – 一个文件中的一个文本行,而不是一打。
真空中的通用APP可能分三个阶段: 读取 , 修改 , 写入 。 读取阶段 – 读取和反序列化文件, 修改 – 更改内存中状态, 写入 – 序列化状态并写入文件。
这种应用程序有三种通用工作stream程:
第一个工作stream程适用于只读“用户”,如游戏客户端,它只读取一次数据并忘记文件。
第二个工作stream程用于编辑应用程序。 由于外部更新很less发生,而且用户不可能同时在几个编辑应用程序中编辑同一个文件,所以假设一个普通的编辑应用程序只想读取一次状态(尤其是如果它是一个资源消耗的操作)并且只在外部更新的情况下重新读取。
第三个工作stream程用于自动化的cli使用 – 构build服务器,脚本等。
考虑到这一点,分别威胁读取和修改 + 写入是合理的。 让我们调用一个只读阶段的操作,并准备一些读操作的信息。 写操作将是一个操作,从读操作 修改状态并将其写入磁盘。
由于工作stream1和2可能由不同的应用程序实例同时运行,因此允许同时运行多个读取操作也是合理的。 某些读取操作 (例如,用于编辑应用程序的读取操作)可能需要等到现有写入操作完成后才能读取最新状态和最新状态。 其他读取操作 (如游戏客户端中的这种操作)可能需要读取当前状态,不pipe它是什么,而不被阻塞。
另一方面, 写操作检测其他正在运行和中止的写操作是合理的。 写入操作还应该检测对索引文件所做的任何外部更改并中止。 理由 – 执行(和等待)任何工作都是没有意义的,因为它们是基于可能的过时状态而被抛弃的。
对于一个强大的应用,应该在应用的每一个点上假设一个星系尺度的严重故障的可能性。 在任何情况下都不应该使索引文件不一致。
读取操作 :
写操作 :
如果收到来自租约的信号 – 中止和清理,则不重命名。 重命名(2)没有提到它可能被打断,POSIX 要求它是原子的,所以一旦我们做到了 – 我们已经做到了。
我知道有共享内存互斥和命名信号量(而不是对应用程序实例之间的合作锁定),但我认为我们都同意,它们对于手头的任务来说是不必要的复杂,并且有其自身的问题。
读取操作 :
写操作 :
在修改过程中,使用WaitForSingleObject (零超时)检查OVERLAPPED结构中的事件。 如果索引有事件 – 中止操作。 否则 – 再次发射表,检查我们是否仍然是最新的,如果是的话 – 继续。
在Linux中,您也可以使用强制性文件锁定 。
请参阅“语义”部分:
如果一个进程用一个强制性的读锁定来锁定一个文件的一个区域,那么允许其他进程从该区域读取。 如果这些进程中的任何一个尝试写入该区域,它将阻塞直到锁被释放,除非进程已经用O_NONBLOCK标志打开文件,在这种情况下系统调用将立即返回错误状态EAGAIN。
和:
如果一个进程用一个强制性的写入锁定锁定了一个文件的区域,除非进程已经用O_NONBLOCK标志打开了文件,在这种情况下,系统调用将会立即返回错误状态EAGAIN。
通过这种方式, APP可以设置读取或写入锁定文件, VCS将被锁定,直到锁定被释放。
请注意,如果VCS可以unlink()
索引文件或使用rename()
将其替换,那么强制锁定和文件租约都不会起作用:
您也无法在目录上建立锁定或租约。 你可以在这种情况下做什么:
读取操作后, APP可以手动检查该文件是否存在,并具有相同的i节点。
但是写入操作是不够的。 由于APP不能自动检查文件i-node并修改文件,因此可能会无意中覆盖VCS所做的更改而无法检测到。 您可能可以使用inotify(7)
检测到这种情况。