写在前面:SQLite封锁机制的实现需要底层文件系统的支持,不管是Linux,还是Windows,都提供了文件锁的机制,而这为SQLite提供了强大的支持。本节就来谈谈SQLite使用到的文件锁——主要基于Linux和Windows平台。

  • Linux的文件锁

Linux 支持的文件锁技术主要包括建议锁(advisory lock)和强制锁(mandatory lock)这两种。此外,Linux 中还引入了两种强制锁的变种形式:共享模式强制锁(share-mode mandatory lock)和租借锁(lease)。在这里,主要讨论建议锁(advisory lock)。
建议锁并不由内核强制实行,也就是说如果有进程不遵守“游戏规则”,不检查目标文件是否已经由别的进程加了锁就往其中写入数据,那么内核是不会加以阻拦的。因此,建议锁并不能阻止进程对文件的访问,而只能依靠各个进程在访问文件之前检查该文件是否已经被其他进程加锁来实现并发控制。进程需要事先对锁的状态做一个约定,并根据锁的当前状态和相互关系来确定其他进程是否能对文件执行指定的操作。而强制锁是由内核强制采用的文件锁——由于内核对每个read()和write()操作都会检查相应的锁,所以会降低系统性能。
对于建议锁,Linux提供两种实现方式:锁文件(lock files)和记录锁( record locking)。

(1)锁文件(lock files)
锁文件是最简单的对文件加锁的方法,每个需要加锁的数据文件都有一个锁文件(lock file)。当锁文件存在时,就认为该数据文件已经被加锁,别的进程不应该访问(但是你非要访问,Linux也不会阻止)。当锁不存在,进程就可以创建一个锁文件,然后访问相应的数据文件。只要创建锁的过程是原子的,就能保证某一时刻只有一个进程拥有该锁,这种方法保证某一时刻只有一个进程访问文件。
这种想法很简单,当一个进程想访问文件时,可以按如下方式对文件加锁:


fd = open("somefile.lck", O_RDONLY, 0644);
if (fd >= 0) {
    close(fd);
    printf("the file is already locked");
    return 1;
} else {
    /* the lock file does not exist, we can lock it and access it */
    fd = open("somefile.lck", O_CREAT | O_WRONLY, 0644");
    if (fd < 0) {
        perror("error creating lock file");
        return 1;
    }
    /* we could write our pid to the file */
    close(fd);
}

当一个进程处理完文件后,就可以调用unlink("somefile.lck")释放锁了——本质上是删除somefile.lck文件。
上面这段代码实际上存在竞争情况,原因在于if语句块不是原子性的,进入if语句块,内核可能调度别的进程运行。更好的方式如下:


fd = open("somefile.lck", O_WRONLY | O_CREAT | O_EXCL, 0644);
if (fd < 0 && errno == EEXIST) {
    printf("the file is already locked");
    return 1;
} else if (fd < 0) {
    perror("unexpected error checking lock");
    return 1;
}

/* we could write our pid to the file */
close(fd);

O_EXCL标志保证open()创建锁文件的过程是原子性的。
注意以下几点:
1、任何时刻只有一个进程可以拥有锁。
2、O_EXCL标志只对本志文件系统是可靠的,对于网络文件系统并不能很好的支持。
3、锁仅仅只是建议性的。
4、如果一个持有锁的进程不正常结束,锁文件仍然存在。如果加锁进程的pid存储在锁文件中,其它进程可以检查锁进程是否存在,当它结束时就可以删除锁。但是,在检查的时候,如果pid被其它进程使用了,此时就无能为力了。

(2)记录锁(Record Locking)
为了克服锁文件的缺点,System V和BSD4.3引入了记录锁,相应的系统调用为lockf()和flock()。而POSIX对于记录锁提供了另外一种机制,其系统调用为fcntl()。Linux提供三种接口,在这里仅讨论POSIX的接口。
记录锁和锁文件有两个很重要的区别:首先,记录锁可以对文件的任何一部分加锁——这对于DBMS这样的应用程序,有极大的帮助,SQLite当然没有放过这样的好处。其次,记录锁的另一个优点就是它由内核持有,而不是文件系统持有。当进程结束时,所有的锁也随之释放。
和锁文件一样,POSIX锁也是建议性的。记录锁有两种锁:读锁(read locks)和写锁(write locks)。读锁也就是共享锁(shared lock),写锁也就是排它锁(exclusive lock)。对于一个记录,只能有一个进程持有写锁,读锁不能存在。
对于一个进程本身而言,多个锁绝不会冲突。如果一个进程对文件的200-250字节持有读锁,然后对200-225字节数据加写锁,是会成功的。此时,200-225为写锁,而226-250字节数据为读锁,该规则主要是防止进程本身发生死锁(尽管多进程之间仍然可能发生死锁)。

POSIX锁通过fcntl()系统来实现,如下:

#include <fcntl.h>

int fcntl(int fd, int command, long arg);

arg为指向flock结构体的指针:


#include <fcntl.h>

struct flock {
    short l_type;
    short l_whence;
    off_t l_start;
    off_t l_len;
    pid_t l_pid;

};

在 flock 结构中,l_type 用来指明创建的是共享锁还是排他锁,其取值有三种:F_RDLCK(共享锁)、F_WRLCK(排他锁)和F_UNLCK(删除之前建立的锁);l_pid 指明了该锁的拥有者;l_whence、l_start 和l_end 这些字段指明了进程需要对文件的哪个区域进行加锁,这个区域是一个连续的字节集合。因此,进程可以对同一个文件的不同部分加不同的锁。l_whence 必须是 SEEK_SET、SEEK_CUR 或 SEEK_END 这几个值中的一个,它们分别对应着文件头、当前位置和文件尾。l_whence 定义了相对于 l_start 的偏移量,l_start 是从文件开始计算的。

可以执行的操作包括:

* F_GETLK:进程可以通过它来获取通过 fd 打开的那个文件的加锁信息。执行该操作时,lock 指向的结构中就保存了希望对文件加的锁(或者说要查询的锁)。如果确实存在这样一把锁,它阻止 lock 指向的 flock 结构所给出的锁描述符,则把现存的锁的信息写到 lock 指向的 flock 结构中,并将该锁拥有者的 PID 写入 l_pid 字段中,然后返回;否则,就将 lock 指向的 flock 结构中的 l_type 设置为 F_UNLCK,并保持 flock 结构中其他信息不变返回,而不会对该文件真正加锁。
    * F_SETLK:进程用它来对文件的某个区域进行加锁(l_type的值为 F_RDLCK 或 F_WRLCK)或者删除锁(l_type 的值为F_UNLCK),如果有其他锁阻止该锁被建立,那么 fcntl() 就出错返回
    * F_SETLKW:与 F_SETLK 类似,唯一不同的是,如果有其他锁阻止该锁被建立,则调用进程进入睡眠状态,等待该锁释放。一旦这个调用开始了等待,就只有在能够进行加锁或者收到信号时才会返回

需要注意的是,F_GETLK 用于测试是否可以加锁,在 F_GETLK 测试可以加锁之后,F_SETLK 和 F_SETLKW 就会企图建立一把锁,但是这两者之间并不是一个原子操作,也就是说,在 F_SETLK 或者 F_SETLKW 还没有成功加锁之前,另外一个进程就有可能已经插进来加上了一把锁。而且,F_SETLKW 有可能导致程序长时间睡眠。还有,程序对某个文件拥有的各种锁会在相应的文件描述符被关闭时自动清除,程序运行结束后,其所加的各种锁也会自动清除。

  • Windows中的文件锁

Windows中的锁都是强制锁(mandatory locks),Windows中的共享文件通过以下几个机制来管理:
(1)    通过共享访问控制方式,应用程序可以指定整个文件进行共享读,写或者删除。
(2)    通过字节范围锁(byte range locks)可以对文件的某一部分进行读写访问。
(3)    Windows文件系统不允许正在执行的文件被打开用来进行写或删除操作。
文件的共享方式由WIN32 API中的打开文件函数CreateFile()中的sharing mode参数确定:


HANDLE CreateFile(
  LPCTSTR lpFileName,
  DWORD dwDesiredAccess,
  DWORD dwShareMode,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes,
  DWORD dwCreationDisposition,
  DWORD dwFlagsAndAttributes,
  HANDLE hTemplateFile
);

dwShareMode的取值通常为:
FILE_SHARE_DELETE:
   Enables subsequent open operations on an object to request delete access. 
Otherwise, other processes cannot open the object if they request delete access.
If this flag is not specified, but the object has been opened for delete access, the function fails.
FILE_SHARE_READ:
     Enables subsequent open operations on an object to request read access. 
Otherwise, other processes cannot open the object if they request read access. 
If this flag is not specified, but the object has been opened for read access, the function fails.
FILE_SHARE_WRITE:
Enables subsequent open operations on an object to request write access. 
Otherwise, other processes cannot open the object if they request write access. 
If this flag is not specified, but the object has been opened for write access, the function fails.

具体的实现函数:


BOOL LockFile(
  HANDLE hFile,
  DWORD dwFileOffsetLow,
  DWORD dwFileOffsetHigh,
  DWORD nNumberOfBytesToLockLow,
  DWORD nNumberOfBytesToLockHigh
);
  • SQLite封锁机制的几个注意点

SQLite的lock byte的定义如下:

Code

(1)PENDING_BYTE为何设置为0X4000 0000(1GB)?
在Windows文件中,被加锁的区域不要求有数据,并且它会阻止所有的进程写文件的该区域,包括第一个持有该锁的进程.为了防止出现由于对含有mandatory lock的页面进行读写操作而出现错误(这在Windows中是不允许的),SQLite完全忽略包含pending byte的页面,所以pending byte在数据库文件上产生一个”文件洞”。PENDING_BYTE设置得那么高,则大部分数据库文件不会遇到由于PENDING_BYTE产生”文件洞”引起的空间损失(除非文件特别大,超过1GB)。

(2)对于Windows来说,文件中加锁的区域不能重叠,为了使两个读进程可以同时访问文件,对于SHARED LOCK选择一个SHARED_FIRST——SHARED_FIRST+ SHARED_SIZE范围内的随机数,所以有可能两个进程取得一样的lock byte,所以对于Windows,SQLite的并发性就受到限制。

SQLite入门与分析(六)---再谈SQLite的锁的更多相关文章

  1. SQLite入门与分析(七)---浅谈SQLite的虚拟机

    写在前面:虚拟机技术在现在是一个非常热的技术,它的历史也很悠久.最早的虚拟机可追溯到IBM的VM/370,到上个世纪90年代,在计算机程序设计语言领域又出现一件革命性的事情——Java语言的出现,它与 ...

  2. SQLite入门与分析(二)---设计与概念(续)

    SQLite入门与分析(二)---设计与概念(续)   写在前面:本节讨论事务,事务是DBMS最核心的技术之一.在计算机科学史上,有三位科学家因在数据库领域的成就而获ACM图灵奖,而其中之一Jim G ...

  3. SQLite入门与分析(四)---Page Cache之事务处理(2)

    写在前面:个人认为pager层是SQLite实现最为核心的模块,它具有四大功能:I/O,页面缓存,并发控制和日志恢复.而这些功能不仅是上层Btree的基础,而且对系统的性能和健壮性有关至关重要的影响. ...

  4. SQLite入门与分析(三)---内核概述(1)

    写在前面:从本章开始,我们开始进入SQLite的内核.为了能更好的理解SQLite,我先从总的结构上讨论一下内核,从全局把握SQLite很重要.SQLite的内核实现不是很难,但是也不是很简单.总的来 ...

  5. SQLite入门与分析(五)---Page Cache之并发控制

    写在前面:本节主要谈谈SQLite的锁机制,SQLite是基于锁来实现并发控制的,所以本节的内容实际上是属于事务处理的,但是SQLite的锁机制实现非常的简单而巧妙,所以在这里单独讨论一下.如果真正理 ...

  6. SQLite入门与分析(四)---Page Cache之事务处理(1)

    写在前面:从本章开始,将对SQLite的每个模块进行讨论.讨论的顺序按照我阅读SQLite的顺序来进行,由于项目的需要,以及时间关系,不能给出一个完整的计划,但是我会先讨论我认为比较重要的内容.本节讨 ...

  7. SQLite入门与分析(三)---内核概述(2)

    写在前面:本节是前一节内容的后续部分,这两节都是从全局的角度SQLite内核各个模块的设计和功能.只有从全局上把握SQLite,才会更容易的理解SQLite的实现.SQLite采用了层次化,模块化的设 ...

  8. SQLite入门与分析(二)---设计与概念

    写在前面:谢谢各位的关注,没想到会有这么多人关注.高兴的同时,也感到压力,因为我接触SQLite也就几天,也没在实际开发中用过,只是最近项目的需求才来研究它,所以我很担心自己的文章是否会有错误,误导别 ...

  9. SQLite入门与分析(八)---存储模型(2)

    3.页面结构(page structure) 数据库文件分成固定大小的页面.SQLite通过B+tree模型来管理所有的页面.页面(page)分三种类型:要么是tree page,或者是overflo ...

随机推荐

  1. ASP保存远程图片文件到本地代码

    <% Function SaveRemoteFile(LocalFileName,RemoteFileUrl) SaveRemoteFile=True dim Ads,Retrieval,Get ...

  2. java多线程总结六:经典生产者消费者问题实现

    这是个线程同步的经典例子,源代码如下: <span style="font-size:16px;">package demo.thread; /** *经典生产者与消费 ...

  3. IOS 高级开发 KVC(二)

    前一篇博客最后介绍了KVC 再json 转模型时遇到一些问题.今天接着来介绍KVC 的其他用法.其实我们在一开始的时候就一直再强调命名的重要性.命名规范是KVC 存活的基础.如果没有这个条件支撑,那么 ...

  4. JavaScript基础-面向对象编程<1>

    1.1 函数与对象  1.定义函数的方式定义类 定义类的方法: function class1(){ //类成员的定义及构造函数部分 } class1既是一个函数,也是一个类. 使用 new 操作符获 ...

  5. 正确理解javascript的this关键字

    javascript有this关键字,它和javascript的执行上下文有着密切的关系,就是说this具体指代什么要根据它的上下文来判断. 一.this和对象的关系    var Person={ ...

  6. 点击UserControl中的按钮将值传给winform页面

    源码下载地址:http://download.csdn.net/detail/dora_zhh/7456521 1.如图所示,点击选择按钮弹出用户控件UserControl 2.点击确定按钮,将值传给 ...

  7. AMD规范与CMD规范

    JavaSript模块化   在了解AMD,CMD规范前,还是需要先来简单地了解下什么是模块化,模块化开发?       模块化是指在解决某一个复杂问题或者一系列的杂糅问题时,依照一种分类的思维把问题 ...

  8. python正则的中文处理

    因工作需要,要查找中文汉字分词,因为python正则表达式\W+表示的是所有的中文字就连标点符号都包括.所以要想办法过滤掉. 参考博客:http://log.medcl.net/item/2011/0 ...

  9. 针对目前高校移动App的火热,哥决定点一把火

    最近正在做市场调研,还请众位大哥大姐们帮忙投个票,求扩散 http://user.qzone.qq.com/717010686/vote/00000000feb6bc2a3ebd1e53

  10. Python3.4 多线程

    线程安全和全局解释器锁 Thread State and the Global Interpreter Lock 总结: 通过使用GIL后, Python多线程安全, 并且数据保持同步. Python ...