点我查看操作系统秘籍连载


计算机领域中,锁机制使用的非常多。它主要是为了避免多个进程访问同一资源时,可能出现的数据不一致问题。

例如,cat命令输出一个比较大的文件内容,cat命令的特性是需要先将所有磁盘文件数据读取到内存后再输出,所以cat输出一个大文件可能需要花费一些时间。如果在cat在加载文件时,在另一个终端上向这个文件追加了一行数据,那么cat最终加载的数据会包含这行新追加的数据吗?

再更简单的一个示例是,两个用户同时用vim打开一个文件去修改数据,那么以谁修改的数据为准呢?所以,vim为了避免这种问题,每次打开一个文件的时候,都会在文件的同一个目录下创建一个隐藏的.swp临时文件(例如vim a.log时会生成一个.a.log.swp的文件),如果有其它用户用vim去打开同一个文件,会检查这个隐藏文件是否存在,如果存在说明有人正在编辑这个文件。

锁的内容非常多,这里简单介绍下它最基本也最实用的基础知识。

如果不使用锁,也就是允许多个进程同时更新、读取同一份数据,将可能出现以下问题:

1.脏读:读取到脏数据。例如用户A用vim将文件中的字母x修改为了y,用户B在vim保存之前通过cat读取,得到的这个字母将是x,这种现象称为脏读。但其实A已经将它修改为了y,只不过该修改只存在于A的vim缓存中,还没有保存到磁盘文件中。通常,将缓存中修改后但没有保存的数据称为脏数据

2.更新丢失:某用户的数据更新操作被其它用户覆盖。例如,用户A和用户B同时修改同一文件中的最后一个字符a,A将a修改为x,B将a修改为y,那么A先保存的话,B的修改将覆盖A的修改,最终保存的结果是y,如果B先保存的话,A的修改将覆盖B的修改,最终结果得到字符x。所以,同时修改数据时,有一个进程的更新被覆盖了,也就是丢失了。

除这两个问题之外,在多进程同时更新、读取同一份数据时,还可能会出现其它现象,这里不再多做描述。下面介绍一下锁的机制。

当需要读数据时,将申请读锁,当需要修改数据时,将申请写锁。

申请锁的时候,需要先检查该资源上是否已经有锁,如果有锁,检查已存在的锁与待申请的锁是否可以兼容共存。

读锁和写锁的兼容性如下表:

S X
S YES NO
X NO NO

这个兼容性是很容易理解的:

  • 1.当多个进程都只是读取同一份资源(即都申请S锁),因为没有修改数据,所以可以允许它们同时读取,所以S锁与S锁是可以共存的
  • 2.如果有一个进程修改数据,它将申请X锁,这时显然不能让其它进程读取或写入数据,所以X锁与S锁、X锁和X锁都是互斥的
  • 3.如果一个进程正在读取数据(即已申请S锁),其它进程想修改数据,也是不允许的,所以S锁和X锁是互斥的

这就不难理解,为什么写锁被称为排它锁或互斥锁的缘故:排除异己。

此外,使用锁需要考虑锁的粒度,即对多少资源量上锁。例如,对于一个文件来说,可以直接对整个文件上锁,也可以只对文件中想要访问的那部分数据上锁。如果直接对整个文件上锁,其它进程申请该文件的互斥锁将总是被阻塞,而如果只是锁定该文件中的前10K数据,那么其它进程如果申请的互斥锁从第20K开始的数据,就不会收到影响。

所以,锁的粒度越大,阻止其它进程的可能性就越大,多进程并发的能力就越差。锁的粒度越小,阻止其它进程的可能性就越小,并发的能力就越强。

但是,锁的粒度太小也不一定好,因为每个锁都是需要额外管理的,粒度越小,需要维护的锁数量越多。比如频繁创建锁和频繁释放锁的开销并不一定小,甚至在极端的时候比维护单个粗粒度的锁效率更低。

在shell命令行下,提供了一个flock命令,它可以通过某个文件来实现锁机制:运行某个命令时在某个文件上申请锁(读锁或写锁),另外一个命令运行时也申请该文件上的锁(读锁或写锁),如果锁可以共存,则第二个命令可以执行,否则默认阻塞。

下面是shell命令行下flock命令的简单用法,更详细内容可man flock自行探索。

1
2
3
4
5
6
7
8
9
# 以下代码在终端1上执行
# 在/tmp/a.lock上申请共享锁(-s),申请成功就运行sleep 10命令
# 因为此时/tmp/a.lock上还没有任何锁,所以申请成功
$ flock -s /tmp/a.lock sleep 10

# 以下代码在终端2上执行
# 在/tmp/a.lock上申请互斥锁(-x),申请成功就运行cat /etc/passwd命令
# 因为/tmp/a.lock上已经有共享锁,所以阻塞,直到10s后共享锁释放
$ flock -x /tmp/a.lock cat /etc/passwd