- 作者:邹祁峰
- 邮箱:[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()实现的互斥锁、多写锁的作用域是进程级的,这种锁不能用来保证多线程中数据的安全性和一致性。