1.概述
1.flock() 给整个文件加锁
2.fcntl() 给一个文件区域加锁
2.flock()
常规方法:
1.给文件加锁
2.执行文件IO
3.解锁文件使得其他进程能够给文件加锁
混合使用加锁和 stdio 函数:
由于 stdio 库会在用户空间进行缓冲,因此在使用 stdio 函数与本章的加锁技术时要格外小心。这里的问题是一个输入缓冲器在被加锁之前
可能会被填满或者一个输出缓冲器在锁被删除之后可能会被刷新。可以采用如下方法避免:
1.使用 write() 和 read() 取代 stdio 库来执行 IO
2.在对文件加锁之后立即刷新 stdio 流,并且在释放锁之前再次刷新这个流
3.使用 setbuf() 来禁用 stdio 缓冲
劝告式和强制式加锁:
在默认情况下,文件锁是劝告式的,这表示一个进程可以简单的忽略另一个进程在文件上放置的锁。要使得劝告式加锁模型能够正常工作,所有访问
文件的进程都必须配合,即在执行IO之前首先需要在文件上放置一把锁。与之对应的是,强制式加锁系统会强制在一个进程执行IO时需要遵从其他进程持有的锁。
flock() 是锁在文件描述符上的,所以 fork() 之后,子进程继续引用。exec() 也机械化引用,除非 close-on-exec。
flock() 的限制:
1.只能对整个文件加锁
2.通过 flock() 只能放置劝告性的锁
3.很多 NFS 实现不识别 flock() 放置的锁。
flock() 锁转换的过程不一定是原子的。
锁继承与释放的语义:
锁会在相应的文件描述符被关闭之后自动释放。但问题更加复杂,通过 flock() 获取的文件锁是与打开的文件描述而不是文件描述符或者文件(i-node)本身相关联的
这意味着当一个文件描述符被复制时(通过dup, dup2, 或者一个 fcntl() F_DUPFD 操作)新文件描述符会引用同一个文件锁。
如果已经通过了一个特定的文件描述符获取了一个锁并创建了该文件描述符的一个或多个副本,那么---如果不显式的执行一个解锁操作---只有当所有的描述符副本都被
关闭之后才会被释放。
如果使用 open() 获得第二个引用同一个文件的文件描述符(以及关联的打开的文件描述)那么,flock 会将第二个描述符当成是一个不同的描述符。如,执行下面的代码
会在第二个 flock 上阻塞:
fd1 = open('a.txt', O_RDWR);
fd2 = open('a.txt', O_RDWR);
flock(fd1, LOCK_EX);
flock(fd2, LOCK_EX); // 阻塞
这样一个进程就能使得 flock() 将自己锁在一个文件之外。使用 fcntl() 返回的记录锁是无法取得这一种效果的。
当使用 fork() 创建一个子进程时,这个子进程会复制其父进程的文件描述符,并且与使用 dup() 调用之类的函数复制描述符一样,这些描述符会引用同一个打开的文件描述,
进而引用同一个锁。如,下面的代码会导致一个子进程删除父进程的锁。
flock(fd, LOCK_EX);
if ( fork() == 0 )
flock(fd, LOCK_UN);
通过 flock() 创建的锁在 exec() 中会得到保留。除非 close-on-exec.
3.fcntl(fd, cmd, &flockstr)
使用 fcntl() 能够在一个文件的任意部分放置一把锁,这个文件部分可以是一个字节,也可以是整个文件。
锁获取和释放的细节:
1.解锁一块区域总是会立即成功的。即使当前并不持有一块区域上的锁,对这块区域解锁也不是一个错误。
2.在任何时刻,一个进程只能持有一个文件在某个特定区域上的一种锁。在之前已经锁住的区域上放置一把新锁会导致不发生任何事情或原子的将既有锁转换成新模式。在后一种情况中,
当一个读锁转换成写锁时需要为调用返回一个错误(F_SETLK)或阻塞(F_SETLKW) 做好准备。这与 flock 是不同的,它的锁的转换不是原子的。
3.一个进程永远无法将自己锁在一个文件区域之外,即使通过多个引用同一文件的文件描述符放置锁也是如此。
4.在已经持有的锁中间放置一把模式不同的锁会产生三把锁:在新锁的两端会创建两个模式为之前模式的更小一点的锁。
死锁:
每个进程的第二个锁请求会被另外一个进程持有的锁阻塞,这种场景被称为死锁。
锁的限制和性能:
Linux 中没有为所能获得的记录锁的数量设置一个固定的上限,至于具体数量取决于可用的内存数量。
获取和释放记录锁的速度有多快呢?这个问题没有固定的答案,因为这些操作的速度取决于用来维护记录锁的内核数据结构和具体的某一把锁在这个数据结构中所处的位置。
每个打开着的文件都有一个关联链表,链表中保存着该文件上的锁。列表中的锁会先按照进程ID,再按其实偏移量来排序。
内核在一个打开着的文件相关联的锁链表中维护着 flock() 锁与文件租用。
锁继承与释放的语义:
1.由 fork() 创建的子进程不会继承记录锁。
2.记录锁在 exec() 中会得到保留,
3.一个进程中的所有线程会共享同一组记录锁
4.记录锁同时与一个进程和一个 i-node 关联。从这种关联关系中可以得出一个毫不意外的结果,就是当一个进程终止时,其所有的记录锁都会被释放。另外一个稍微出乎意外的是,
当一个进程关闭了一个而文件描述符之后,进程持有的对应文件上的所有锁都会被释放,不管这些锁是通过哪个文件描述符得到的。
锁定饿死和排队加锁请求的优先级:
在Linux 上的规则:
1.排队的锁请求被准许的顺序是不确定的。如果多个进程正在等待加锁,那么它们被满足的顺序取决于进程的调度。
2.写着并不一定比读者拥有更高的优先权,反之亦然
4.强制加锁
为了在 Linux 上使用强制式加锁,就必须要在包含待加锁的文件的文件系统以及每个待加锁的文件上启用这一项功能。通过在挂载文件系统时(Linux 特有的) -o mand 选项能够在该文件
系统上启用强制式加锁。
mount -o mand /dev/sda10 /testfs
在程序中可以通过调用 mount(2) 时指定 MS_MANDLOCK 标记来取得同样的结果。
通过查看不带任何选项的 mount(8) 命令的输出,就能够看出一个挂载文件系统是否启用了强制式加锁
mount | grep 'mand'
文件强制式加锁是启用是通过 set-group-ID 权限位和 group-execute 权限来完成的。这种权限组合在其他场景毫无意义。
在shell中可以通过下面方法再一个文件上启用强制式加锁: chmod g+s,g-x /testfs/file
在一个程序中可以通过使用 chmod() 和 fchmod() 来完成。
当现实一个启用了强制式加锁权限位的文件的权限时,ls(1) 会在 group-execute 权限中显示一个 S
如果存在进程持有一个文件任意部分上的强制式读锁或者写锁,那么就无法再该文件上创建一个共享内存映射(即在调用mmap()时指定了MAP_SHARED标记)。同样,如果一个文件参与了一个共享
内存映射,那么就无法在该文件的任意部分上放置一把强制锁。
强制式加锁的警告:
强制式加锁所起的作用并没有看起来那么大,它存在一些问题和缺陷:
1.在一个文件上持有一把强制式锁并不能阻止其他进程删除这个文件,因为只要在父目录上拥有合适的权限就能够与一个文件断开连接。
2.在一个可公开访问的文件上启用强制式锁之前需要经过深思熟虑,因为即使是特权进程也无法覆盖一个强制式锁。恶意用户可能会持续的持有该文件上的锁以制造拒绝服务攻击。
3.强制式锁加锁存在性能开销。在启用了强制式锁的文件上执行的每个IO系统调用中,内核都必须检查在文件上是否存在冲突的锁。如果文件上存在大量的锁,那么这种检查工作
会极大的降低IO系统调用的效率
4.强制式加锁还会在应用程序设计阶段造成额外的开销,因为需要处理每个IO系统调用返回EAGAIN(非阻塞)或EDEADLK(阻塞IO)错误的情况
5.因为在当前的Linux实现中存在一些内核竞争条件,因此在有些情况下执行IO操作的系统调用在文件上存在本应该拒绝这些操作的强制式锁也能成功
总的来说,避免使用强制式加锁。
5./proc/locks 文件
通过检查Linux特有的 /proc/locks 文件中的内容能够查看当前系统中存在的锁。文件显示了用 flock(),fcntl() 创建的锁的相关信息。
59: POSIX ADVISORY WRITE 1716 fd:00:35915058 0 EOF
1.锁在该文件上所有锁的序号
2.锁的类型:其中 FLOCK 表示 flock() 创建的,POSIX 表示 fcntl() 创建的
3.锁的模式:其值是 ADVISORY 或 MANDATORY
4.锁的类型:其值是 READ,WRITE(对应于 fcntl() 中的共享锁和互斥锁)
5.持有锁的进程的ID
6.3个冒号分割的数字,它们标识出了锁所属的文件。这写数字是文件所处的文件系统的主要和次要设备号,后面跟着文件的 i-node 号
7.锁的起始字节。对应 flock 来说,永远是0
8.锁的结尾字节。其中 EOF 表示锁延伸到文件结尾(即对 fcntl() 创建的锁来讲是将 l_len 指定为0)。对于 flock 锁来说,这一列的值永远是0
使用 /proc/locks 文件中信息能够查找到哪个进程持有了哪个文件上的锁。
1.ps -p PID // PID 已知
2.ls -li /dev/sda7 | awk '$6 == "3," && $7 == 10' // 已知设备ID 3:7
3.mount | grep sda7
4.find / -mount -inum i-node // 已经 i-node
最后找到文件
文件租用: /proc/locks 文件还显示了系统中进程持有的文件租用的相关信息。文件租用是 Linux 特有的机制,如果一个进程租用了一个文件,那么该进程
在其他进程尝试 open() 或 truncate() 该文件时会收到通知(通过发送信号)。(包括 truncate() 是有必要的,因为它是唯一一个在无需打开文件的情况下就
能够改变文件内容的系统调用),之所以提供文件租用是位了是的 Samba 能够支持 Microsoft SMB 的机会锁(oplocks)功能以及允许第4版的 NFS 支持委托。
6.仅运行一个程序的单个实例
一些程序---特别是很多 daemon --- 需要在同一时刻只有一个程序实例在系统中运行。完成这项任务的一个常见方法是让 daemon 在一个标准目录中创建一个
一个文件并在该文件上放置一把写锁。daemon 在运行期间一直持有这个文件锁并在即将终止之前删除这个文件。如果 daemon 启动了另外一个实例,那么它在获得
该文件上的写锁时就会失败,其结果是它会意识到 daemon 的另一个实例肯定在运行,然后终止。
很多网络服务器采用了另外一种做法,即当服务器绑定的众所周知的 socket 端口号已经被使用时就认为该服务器已经处于运行状态了。
/var/run 目录通常是放置此类锁文件的位置。或者也可以在 daemon 的配置文件中加一行指定文件的位置。
通常,daemon 会把 pid 写入到这文件中,因此这个文件在命名时通常将 .pid 作为扩展名(如 syslogd 会创建文件 /var/run/syslogd.pid)。这对于那些需要
daemon 的进程ID的应用程序来说是比较有用的。它还允许额外的健全检查---可以使用 kill(pid, 0)来检查进程ID是否存在。(在较早的提供文件加锁的UNIX实现上,
这是一种不完美但很是使用的方法,用于检查一个 daemon 实例是否在运行或前一个实例在终止之前是否没有成功删除这个文件)。
createPidFile() 函数中的一个精妙之处是使用 ftruncate() 来清楚锁文件中之前存在的所有字符串。之所以这么做是因为 daemon 的上一个实例在删除文件时,
可能因为系统崩溃 而失败,在这种情况下,如果新 daemon 实例的进程比较小,那么可能就无法完全覆盖之前文件中的内容。例如,如果进程ID 是 789,那么就会只向
文件写入 789\n,但之前的daemon 实例可能已经向文件写入了 12345\n,这时如果不截断文件的话就会得到内容是 789\n5\n。从严格意义上来讲,清除所有既有字符串
不是必须的,但这样做显得更加简洁。
7.老式加锁技术
1.open(file, O_CREAT|O_EXEC, ...) 加上 unlink(file)
能原子性的检查文件的存在性和创建文件两个步骤。这意味着如果两个进程尝试在创建一个文件时指定这些标记,那么就保证只有其中一个进程能够成功。这种调用与
unlink() 构成了一个加锁技术。获取锁可以通过 open,立即跟着一个 close() 。释放锁则可以通过 unlink() 完成。
lockf();