flock 实现细节
能够实现进程的互斥,主要是依靠了内核里统一的资源入口控制
在flock 的 man page 中有关于 flock 细节的一些描述。其中说明了flock 是与打开文件的文件
表项相关联的。根据《Unix 环境高级编程》对打开文件的介绍,打开的文件在进程表和操作系统
中的对应的结构如下图所示:
每个进程在进程表中都一个对应的项目,叫做进程表项,上图是最左边展示了进程表中两进程表项,
分别对应两个独立的进程。在进程表项中,有一个文件描述符表,其中存储了所有本进程打开的文件描述符
信息及指向对应文件表项的指针。而操作系统中,还另外有一张表,叫做文件表,其中存储的是系统中所有进
程打开的文件的相关信息,其中的项目叫做文件表项(上图中间蓝色部分)。
在进程表项的文件描述符表中每个描述符都对应一个文件表项指针,指向文件表中的一项。v 节点表中的项
目称为 v 节点表项,可以认为其中存储了真正的文件内容。
从图中可以看出,进程1对同一个文件打开了两次,分别对应本进程中的文件描述符 fd0 和 fd2。而下面的
进程对这个文件又打开了一次,对应此进程中的 fd1描述符。要注意的是,不论是同一进程还是不同的进
程,对同一文件打开时,都建立了与各fd 对应的独立的文件表项。
2. 在flock 的man page 中关于文件表项有如下描述:
Locks created by flock() are associated with anopen file table entry
这说明进程使用 flock 对已打开文件加文件锁时,是加在了上图中间蓝色部分的文件表项上。假如图中位于下
方的进程对fd1 加上了排他锁,实际就是加在了fd1 指向的文件表项上,这时上方的进程对 fd0 或 fd2 再加任
何类型的文件锁都会失败。这是因为操作系统会检测到上方的两个文件表项和下方的文件表项都指向了相
同的 v 节点,并且下方的文件表项已经加了排他锁,这时如果其他指向相同v 节点的文件表项再想尝试加
上与原有锁类型不相容的文件锁时,操作系统会将此文件表项对应的进程阻塞。
3. 调用dup 、 fork、execve 时的文件锁
如果要了解用 dup 复制文件描述符时和使用 fork 产生子进程时的文件锁表现,就要了解在调用这两个函数时
,描述符对应的数据结构发生了哪些变化。
用 dup 复制文件描述符时,新的文件描述符和旧的文件描述符共享同一个文件表表项,示意图如下:
调用 dup 后,两个描述符指向了相同的文件表项,而flock 的文件锁是加在了文件表项上,因而如果对
fd0 加锁那么 fd1 就会自动持有同一把锁,释放锁时,可以使用这两个描述符中的任意一个。
4. execve函数族中的文件锁
在fork 产生子进程后,一般会调用 execve 函数族在子进程中执行新的程序。如果在调用 execve 之前,
子进程中某些打开的文件描述符已经持有了文件锁,那么在执行execve 时,如果没有设置 close-on-exec
标志,那么在新的程序中,原本打开的文件描述符依然会保持打开,原本持有的文件锁还会继续持有。
文件锁的解除可以通过将 flock 的 operation 参数设置为 LOCK_UN常量来实现。这时如果有多个fd 指向
同一文件表项,例如给 fd0 加文件锁后,用 dup 复制了fd0 的情况下,用 LOCK_UN对fd0 解锁后,所有
和 fd0 指向同一文件表项的 fd 都不再持有文件锁。fork 子进程复制父进程文件描述符的情形也是如此。
对描述符fd加了文件锁后,如果没有显式使用LOCK_UN 解锁,在关闭 fd 时,会自动解除其持有的文件锁。
但是在为 fd 加锁后如果调用了dup 复制了文件描述符,这时关闭fd 时的表现和调用 LOCK_UN 是不一样的。
如果未显式使用 LOCK_UN解锁,在关闭文件描述符后,如果还有其他的fd 指向同一文件表项,比如之前调用
了dup 的情形,这时加在文件表项上的文件锁并不会解除,其他指向此文件表项的文件描述符依然持有锁,并且
锁的类型也不会发生变化。
使用fork 产生子进程时同样如此。父进程和子进程的描述符指向同一文件表项且已经加了文件锁时,如果用
LOCK_UN将其中一个fd 解锁,那么指向同一表项的所有其他fd 都会自动解锁。但是如果未使用 LOCK_UN
解锁,只是通过 close(fd)关闭了某个文件描述符,那么指向同一文件表项的其他描述符,依然会持有原有的锁。
出于方便考虑,在没有出现多个fd 指向现一文件表项的情况下,可以直接使用close(fd) 的默认解锁功能,
而不用显式的使用LOCK_UN。在有多个 fd 指向同一文件表项的情形下,如果要完全解锁,一定要使用
LOCK_UN 解锁,不能再使用 close(fd) 的默认解锁功能。