Linux下各种编程锁的比较 - 文件锁

  • 作者:邹祁峰
  • 邮箱:[email protected]
  • 博客:http://blog.csdn.net/qifengzou
  • 日期:2014.09.12
  • 转载请注明来自"祁峰"的CSDN博客

1 概述

  含有多个线程/进程的系统在运行过程中,往往会出现多个线程/进程在同一时间去读取或修改同一内存地址的数据,为了保证数据的完整性,最常用的方式是使用锁机制。编程锁有很多种,常见的有文件锁、互斥锁、读写锁、信号量、原子锁等等,虽然它们都起着保证数据完整性的作用,但是它们的作用范围、适用场景、时间开销各不相同。本篇将通过实验对各锁的适用场景、时间开销等方面做一个简单的比较和阐述,给今后工作过程中为使用哪种锁更合适而纠结时,提供一个参考依据。


2 文件锁

  在Linux系统中,文件锁的接口有3种方式:flock()、fcntl()、lockf()。以下将对这两种接口进行分别的说明。

2.1 flock()函数

表1 flock()函数说明

说明项 描述
头文件 #include <sys/file.h>
函数原型 int flock(int fd, int operation)
函数说明 1、此函数是从BSD中衍生出来的,但目前在大多数UNIX系统上都能找到。
2、此函数只能处理单个主机上的文件,不能处理NFS上的文件。

3、此函数按照operation所指定的方式对fd锁指向的文件进行加锁和解锁等相关操作。
4、此函数只能操作整个文件,不能操作此文件的部分区域。
5、此函数加的是建议性锁,当进程结束时,系统会自动释放该锁。
参数说明 参数operation有下列四种情况:

  LOCK_SH:建立共享锁定。多个进程可同时对同一个文件作共享锁定。

  LOCK_EX建立互斥锁定。一个文件同时只有一个互斥锁定。

  LOCK_UN解除文件锁定状态。

  LOCK_NB无法建立锁定时,此操作不会被阻塞,将立即返回操作结果。

    1、此选项与LOCK_SH或LOCK_EX 做OR(|)组合。

      如:fblock(fd, LOCK_NB | LOCK_EX) 或 fblock(fd, LOCK_NB | LOCK_SH)

    2、单一文件无法同时建立共享锁定和互斥锁定,而当使用dup()或fork()时文件描述词不会继承此种锁定。

    3、返回值  返回0表示成功,若有错误则返回-1,错误代码存于errno。


  其使用的参考代码如下:

图1 flock()参考代码

2.2 fcntl()函数

表2 fcntl()函数说明

说明项 描述
头文件 #include <fcntl.h>
函数原型 int fcntl(int fd, int cmd, ... /* arg */)
函数说明 在此只说明与锁相关的功能:
1、此函数符合POSIX标准的文件锁实现,所以是可移植的。
2、此函数不仅能处理本地文件,也能处理NFS上的文件。
  fcntl()请求会被递交给叫rpc.lockd的守护进程,然后由它负责和主机端lockd的对话。

3、此函数不仅能操作整个文件,也能操作此文件的部分区域。
4、此函数不仅可以加建议性锁,但也支持加强制性锁。
参数说明 1)当CMD为F_SETLKW时,阻塞加读锁或加写锁或解锁。
2)当CMD为F_SETLK时,非阻塞加读锁或加写锁或解锁。
3)当CMD为以上2值之一时,该函数第三个参数为:
  struct flock
  {
      short l_type;        /* 操作类型:F_RDLCK(读锁)、F_WRLCK(写锁)、F_UNLCK(解锁) */
      off_t l_start;         /* 起始偏移 */
      short l_whence; /* 相对位置:SEEK_SET(文件开始处)、CURR_SET(当前位置处)、END_SET(文件结束处) */
      off_t l_len;           /* 操作长度:0表示从start至文件结束处 */
      pid_t l_pid;          /* 进程ID:不必设置。当CMD为F_GETLK时,将返回该值 */
  }


  使用fcntl()实现的锁操作接口如下:


图2 fcntl()非阻塞锁操作


图3 fcntl()阻塞锁操作

以上2个函数虽然支持读锁、写锁、解锁等操作,但是参数过多,不太好使用。为了简化接口的使用,可以使用如下宏替代:(因考虑到有线程的读写锁,而fcntl()只支持进程级的锁,因此其命名以proc_开头)

图4 锁的宏定义

2.3 lockf()函数

表2 lockf()函数说明

说明项 描述
头文件 #include <unistd.h>
函数原型 int lockf(int fd, int cmd, off_t len)
函数说明 1、此函数符合POSIX标准的文件锁实现,所以是可移植的。
2、在Linux操作系统中,此函数是基于fcntl()的封装,在其他操作系统中,很多也是这么实现的。但是
在POSIX.1-2001中未指明lockf()和fcntl()之间的关系。

3、一个可移植的程序,应该避免将lockf()和fcntl()混搭使用。
参数说明 1、参数cmd:命令类型
 1)当CMD为F_LOCK时,给文件的指定段加互斥锁。(阻塞)
        1. 如果加锁的段的全部或部分已经被其他进程(而不是线程)加锁,此进程将会一直阻塞直到该段被释放;
        2. 如果加锁的端的与本进程之前加锁端有重叠,加锁段将进行合并;
        3. 当文件描述符被关闭或或进程退出后,锁将会自动释放;
        4. 孩子进程将不会继承父进程的锁;
 2)当CMD为F_TLOCK时,给文件的指定段加互斥锁。(非阻塞)
        该选项与F_LOCK一致,但是如果加锁段的全部或部分已经被其他进程占用时,将不会阻塞,而是立即返回错误。
 3)当CMD为F_ULOCK时,给文件的指定段解锁。
        1. 该选项可能导致加锁段被分割为2段加锁段。
        2. 比如:文件的第2~9 字节被加锁,可以通过此函数给第4~7字节先解锁,这样就造成第2~3和第8~9两端
还处于加锁状态。
 4)当CMD为F_TEST时,测试文件的指定段是否已加锁。(非阻塞)
        1. 当测试段没有被其他进程加锁,或者被本加锁时,返回0;
        2. 当测试段被其他进程加锁,将errno设置为EAGAIN(有些系统设置为EACCESS),同时返回-1.
2、参数len: 操作长度
 1)当LEN的值为正数时,操作的范围是:pos ~ pos+len-1(注:pos为文件当前偏移量)
 2)当LEN的值为负数时,操作的范围是:pos-len ~ pos-1
 3)当LEN的值为零时,操作的范围是:pos ~ 文件末尾(注:文件末尾指的是当前和未来的文件末尾)


  使用lockf()实现的锁操作接口如下:

图5 lockf()示例代码


  以上3个函数lockf()、fcntl()、flock()实现的互斥锁、多写锁的作用域是进程级的,这种锁不能用来保证多线程中数据的安全性和一致性。

  假设:有进程3个进程A、B和C同时抢一把进程级的互斥锁L(进程A有多个线程A1、A2、...)。如果进程A抢到了锁L,那么在进程A释放锁之前,进程B和C是无法再加锁成功的。如果A进程抢锁成功是通过线程A1,那么当线程A2、A3、...再执行抢锁L的操作时,依然会返回加锁成功;如果是由线程An抢到的锁L,可以由线程Ax来释放锁L。

  使用以上3个函数实现的读写锁的效果和互斥锁的效果,在多线程中是一致的,在此不再赘述。

  总之:以上3个函数lockf()、fcntl()、flock()实现的互斥锁、多写锁的作用域是进程级的,这种锁不能用来保证多线程中数据的安全性和一致性。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。