用Linux和Windows控制超出我们控制范围的多个进程的Conwarent状态文件操作

下面的问题可能听起来有点复杂,但实际上这是一个相当简单,普遍和共同的问题,即在同一个文件上工作的三个进程。 在下面的文本中,我试图用一些说明性的例子将问题分解成一组特定的需求。

任务序言

有一个名为index的文本文件,其中包含一些元数据。

有一个应用程序( APP ),它理解文件格式并对其进行有意义的改变。

该文件存储在版本控制系统( VCS )下,这是其他用户在同一文件上执行的更改的来源。

我们需要devise一个应用程序( APP ),这个应用程序可以在一个合理的文件中使用这个文件,最好不要和VCS进行太多的交互,因为它假定VCS被用来保存一个大的项目, 索引文件只是一小部分并且用户可能希望在任何时候更新VCS ,而不考虑APP内的任何正在进行的操作。 在这种情况下, APP应该以防止任何可能的数据丢失的方式妥善处理这种情况。

优先评论

请注意, VCS是未指定的,它可能是perforce,git,svn,tarballs,闪存驱动器或您最喜爱的WWII莫尔斯电台和文本编辑器。

文本文件可以是二进制文件,这并不会改变什么。 但是考虑到VCS存储,它很容易被合并,因此文本/人类可读的格式是最合适的。

可能的例子有:复杂的configuration(AI行为树,游戏对象描述),资源列表,其他不需要手工编辑的事物,与手头上的项目有关,但是历史很重要。

请注意,除非您急于实现您自己的版本控制系统,否则将大部分configuration“外包”到某种基于客户端 – 服务器的外部解决scheme并不能解决问题 – 您仍然需要在版本控制系统中保存参考文件在数据库中对所讨论的configuration版本的引用。 这意味着,你仍然有同样的问题,但规模有点小 – 一个文件中的一个文本行,而不是一打。

任务本身

真空中的通用APP可能分三个阶段: 读取修改写入读取阶段 – 读取和反序列化文件, 修改 – 更改内存中状态, 写入 – 序列化状态并写入文件。

这种应用程序有三种通用工作stream程:

  1. 阅读 – > <呈现信息>
  2. 阅读 – > <呈现信息并等待用户input> – > 修改 – > 写入
  3. – > 修改 – >

第一个工作stream程适用于只读“用户”,如游戏客户端,它只读取一次数据并忘记文件。

第二个工作stream程用于编辑应用程序。 由于外部更新很less发生,而且用户不可能同时在几个编辑应用程序中编辑同一个文件,所以假设一个普通的编辑应用程序只想读取一次状态(尤其是如果它是一个资源消耗的操作)并且只在外部更新的情况下重新读取。

第三个工作stream程用于自动化的cli使用 – 构build服务器,脚本等。

考虑到这一点,分别威胁读取修改 + 写入是合理的。 让我们调用一个只读阶段的操作,并准备一些读操作的信息。 写操作将是一个操作,从读操作 修改状态并将其写入磁盘。

由于工作stream1和2可能由不同的应用程序实例同时运行,因此允许同时运行多个读取操作也是合理的。 某些读取操作 (例如,用于编辑应用程序的读取操作)可能需要等到现有写入操作完成后才能读取最新状态和最新状态。 其他读取操作 (如游戏客户端中的这种操作)可能需要读取当前状态,不pipe它是什么,而不被阻塞。

另一方面, 写操作检测其他正在运行和中止的写操作是合理的。 写入操作还应该检测对索引文件所做的任何外部更改并中止。 理由 – 执行(和等待)任何工作都是没有意义的,因为它们是基于可能的过时状态而被抛弃的。

对于一个强大的应用,应该在应用的每一个点上假设一个星系尺度的严重故障的可能性。 在任何情况下都不应该使索引文件不一致。

要求

  1. 文件读取是一致的 – 在任何情况下,我们都不应该在文件被更改之前读取一半的文件,或者读取另一半的文件。
  2. 写操作是独占的 – 不允许同一个文件同时进行其他的写操作
  3. 写操作是可靠的 – 我们应该能够等待写操作完成或失败。
  4. 写操作是事务性的 – 在任何情况下,文件都不应该保留在部分更改或其他不一致的状态或基于过时的状态。 应该检测到在操作之前或操作过程中对索引文件的任何改变,并尽快中止操作。

Linux的

读取操作

  1. 获取共享锁,如果请求 – 打开(2) ( O_CREAT | O_RDONLY )和flock(2) ( LOCK_SH )“锁定”文件。
  2. 打开(2) ( O_RDONLY )索引文件。
  3. 创建内容快照并解析它。
  4. 关闭(2)索引文件。
  5. 解锁 – 解锁(2) ( LOCK_UN )并关闭(2) “锁定”文件

写操作

  1. 获得排他锁 – 打开(2) ( O_CREAT | O_RDONLY )和flock(2) ( LOCK_EX )“锁”文件。
  2. 打开(2) ( O_RDONLY )索引文件。
  3. fcntl(2) ( F_SETLEASEF_RDLCK )索引文件。 – 我们只对写这些RDLCK租约感兴趣。
  4. 检查状态是否是最新的,做事情,改变状态,把它写到附近的一个临时文件中。
  5. 将临时文件重命名为索引 – 这是原子性的,如果我们还没有到目前为止的租赁中断,我们根本不会 – 这将是一个不同的文件,而不是我们已经得到租约上。
  6. fcntl(2) (* F_SETLEASE, F_UNLCK )索引文件。
  7. 关闭(2)索引文件(“旧的”,在文件系统中没有引用)
  8. 解锁 – 关闭(2) “锁定”文件

如果收到来自租约的信号 – 中止和清理,则不重命名。 重命名(2)没有提到它可能被打断,POSIX 要求它是原子的,所以一旦我们做到了 – 我们已经做到了。

我知道有共享内存互斥和命名信号量(而不是对应用程序实例之间的合作锁定),但我认为我们都同意,它们对于手头的任务来说是不必要的复杂,并且有其自身的问题。

视窗

读取操作

  1. 获取共享锁,如果请求 – CreateFile ( OPEN_ALWAYSGENERIC_READFILE_SHARE_READ )和LockFileEx (1字节)的“锁”文件
  2. CreateFile ( OPEN_EXISTINGGENERIC_READFILE_SHARE_READ )索引文件
  3. 阅读文件内容
  4. 关闭处理索引
  5. 解锁 – 关闭处理“锁定”文件

写操作

  1. 获得排他锁 – CreateFile ( OPEN_ALWAYSGENERIC_READFILE_SHARE_READ )和LockFileEx ( LOCKFILE_EXCLUSIVE_LOCK ,1个字节)的“锁”文件
  2. CreateFile ( OPEN_EXISTINGGENERIC_READFILE_SHARE_READ | FILE_SHARE_WRITE )索引文件
  3. 在索引文件目录中的ReadDirectoryChanges (FALSE, FILE_NOTIFY_CHANGE_LAST_WRITE ),带有OVERLAPPED结构和一个事件
  4. 检查状态是最新的。 修改状态。 写一个临时文件
  5. 用临时替换索引文件
  6. 关闭处理索引
  7. 解锁 – 关闭处理“锁定”文件

在修改过程中,使用WaitForSingleObject (零超时)检查OVERLAPPED结构中的事件。 如果索引有事件 – 中止操作。 否则 – 再次发射表,检查我们是否仍然是最新的,如果是的话 – 继续。

备注

  1. Windows版本使用锁定而不是Linux版本的通知机制,这可能会干扰写入的外部进程,但在Windows中似乎没有其他方式。

在Linux中,您也可以使用强制性文件锁定 。

请参阅“语义”部分:

如果一个进程用一个强制性的读锁定来锁定一个文件的一个区域,那么允许其他进程从该区域读取。 如果这些进程中的任何一个尝试写入该区域,它将阻塞直到锁被释放,除非进程已经用O_NONBLOCK标志打开文件,在这种情况下系统调用将立即返回错误状态EAGAIN。

和:

如果一个进程用一个强制性的写入锁定锁定了一个文件的区域,除非进程已经用O_NONBLOCK标志打开了文件,在这种情况下,系统调用将会立即返回错误状态EAGAIN。

通过这种方式, APP可以设置读取或写入锁定文件, VCS将被锁定,直到锁定被释放。


请注意,如果VCS可以unlink()索引文件或使用rename()将其替换,那么强制锁定和文件租约都不会起作用:

  • 如果您使用强制锁定,则VCS不会被阻止。
  • 如果您使用文件租赁, APP不会收到通知。

您也无法在目录上建立锁定或租约。 你可以在这种情况下做什么:

  • 读取操作后, APP可以手动检查该文件是否存在,并具有相同的i节点。

  • 但是写入操作是不够的。 由于APP不能自动检查文件i-node并修改文件,因此可能会无意中覆盖VCS所做的更改而无法检测到。 您可能可以使用inotify(7)检测到这种情况。