Linux中文件加锁

/proc/locks

andrew@andrew-Thurley:/dev$ cat /proc/locks 
1: POSIX  ADVISORY  WRITE 8968 08:01:11666907 1073741825 1073741825
2: POSIX  ADVISORY  READ  2433 08:01:11798469 128 128
...
35: FLOCK  ADVISORY  WRITE 1436 00:1a:7 0 EOF
51: FLOCK  ADVISORY  WRITE 1036 00:16:763 0 EOF

使用ps -p PID查看进程的相关信息

andrew@andrew-Thurley:/dev$ ps -p 8968
  PID TTY          TIME CMD
 8968 ?        00:00:02 chrome

从上面的输出可以看出持有的锁的程序是chrome,即 google浏览器

/dev我下搜索主设备号为8次设备号为1的设备,是/dev/sda1

andrew@andrew-Thurley:/dev$ ls -li /dev/sda1 |awk '$6=8'
351 brw-rw---- 1 root disk 8 1 12月 22 13:13 /dev/sda1

查看设备/dev/sda1的挂载点,并在该部分文件系统中搜索i-node节点为11666907的文件

andrew@andrew-Thurley:/dev$ mount |grep sda1
/dev/sda1 on / type ext4 (rw,relatime,errors=remount-ro,data=ordered)

从上面的输出可以看出,设备/dev/sda1是挂在在 /上的

andrew@andrew-Thurley:/dev$ sudo find / -mount -inum 11666907
/home/andrew/.config/google-chrome/Default/History

find -mount选项是防止find进入到/下的子目录进行搜索

最后显示被锁住文件的内容--google浏览器的历史记录

sudo vim /home/andrew/.config/google-chrome/Default/History

是一个google浏览器的SQLite数据库文件

在这里插入图片描述

这样就可以看出google浏览器持有文件sudo vim /home/andrew/.config/google-chrome/Default/History上的一把锁,而这个文件的内容就是google用于记录历史记录的数据库文件。

通过/proc/locks还能够获取被阻塞的锁请求的相关信息,如下图所示

在这里插入图片描述

其中锁号后面随即跟着->的行表示被阻塞的锁的请求

仅运行一个程序的单个实例

一些程序-特别是很多的daemon需要确保同一时刻只有一个程序实例在系统中运行。完成这项任务的一个常见的方法是让daemon在一个标准的目录中创建一个文件并在该文件上放置一把写锁。daemon在执行期间一直持有这个文件锁并在即将终止前删除这个文件。如果启用了daemon的另外一个实例,那么它在获取该文件的写锁时就会失败,其结果是它会意识到daemon的另一个实例肯定在运行,然后终止。

很多的网络服务器采用了另一种常规的做法,即当服务器绑定的众所周知的socket端口号已经被使用时就认为该服务器实例已经处于运行在状态了

/var/run目录通常是存放此类文件的位置,或者也可以在daemon的配置文件中加上一行,来指定文件的位置。

通常daemon会将其进程ID写入该文件,因此这个文件命名时通常将.pid作为扩展名。这对于那些需要找出daemon的进程ID的应用程序来讲是比较有用的。它允许执行额外的健全检查--效果可以像20.5节那样使用kill(pid,0)来检查进程ID是否存在。

文件create_pid_file.c展示了,用来创建和锁住一个进程ID锁文件的代码实现。可以使用下面的方式调用这个函数。

if(createPidFile("mydaemon", "/car/run/mydaemon.pid", 0) == -1)
	errExit("createPidFile");

createPidFile函数的精妙之处在于使用ftruncate()函数来清除文件中之前存在的所有的字符串。之所以这样做是因为daemon的上一个实例在删除文件时可能因系统崩溃而失败,在这种情况下,如果新的daemon实例的进程ID较小,那么可能就无法完全覆盖之前文件中的内容,从严格意义上来说清除所有的既有字符串不是必需的,但这样做显得更加简洁并且排除产生混淆的可能。

         
#include <sys/stat.h>
#include <fcntl.h>
#include "region_locking.h"             /* For lockRegion() */
#include "create_pid_file.h"            /* Declares createPidFile() and
                                                  defines CPF_CLOEXEC */
#include "tlpi_hdr.h"

#define BUF_SIZE 100            /* Large enough to hold maximum PID as string */
    
#define CPF_CLOEXEC 1
static int
lockReg(int fd, int cmd, int type, int whence, int start, off_t len)
{
    struct flock fl;

    fl.l_type = type;
    fl.l_whence = whence;
    fl.l_start = start;
    fl.l_len = len;

    return fcntl(fd, cmd, &fl);
}

int                     /* Lock a file region using nonblocking F_SETLK */
lockRegion(int fd, int type, int whence, int start, int len)
{
    return lockReg(fd, F_SETLK, type, whence, start, len);
}
int
createPidFile(const char *progName, const char *pidFile, int flags)
{
    int fd;
    char buf[BUF_SIZE];

    fd = open(pidFile, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
    if (fd == -1)
        errExit("Could not open PID file %s", pidFile);
	//< flags 设置为1的时候使用fcntl设置FD_CLOEXEC
    // 对于通过exec()来从起自己的进程比较有用,如果在exec()时文件描述符没有被关闭,那么重新启动的服务器就会认为服务器的另一个示例处于运行状态。
    if (flags & CPF_CLOEXEC) {

        /* Set the close-on-exec file descriptor flag */

        /* Instead of the following steps, we could (on Linux) have opened the
           file with O_CLOEXEC flag. However, not all systems support open()
           O_CLOEXEC (which was standardized only in SUSv4), so instead we use
           fcntl() to set the close-on-exec flag after opening the file */

        flags = fcntl(fd, F_GETFD);                     /* Fetch flags */
        if (flags == -1)
            errExit("Could not get flags for PID file %s", pidFile);

        flags |= FD_CLOEXEC;                            /* Turn on FD_CLOEXEC */

        if (fcntl(fd, F_SETFD, flags) == -1)            /* Update flags */
            errExit("Could not set flags for PID file %s", pidFile);
    }
	//设置一把写锁 SEEK_SET-开头  0 到 len=0代表到EOF
    //即对整个文件放置一把写锁,并且写锁的范围随着文件的增加而改变
    if (lockRegion(fd, F_WRLCK, SEEK_SET, 0, 0) == -1) {
        if (errno  == EAGAIN || errno == EACCES)
            fatal("PID file '%s' is locked; probably "
                     "'%s' is already running", pidFile, progName);
        else
            errExit("Unable to lock PID file '%s'", pidFile);
    }
	//将文件的大小截断为0
    if (ftruncate(fd, 0) == -1)
        errExit("Could not truncate PID file '%s'", pidFile);
	//将PID写如文件
    snprintf(buf, BUF_SIZE, "%ld\n", (long) getpid());
    if (write(fd, buf, strlen(buf)) != strlen(buf))
        fatal("Writing to PID file '%s'", pidFile);

    return fd;
}

老式加锁技术

在较老的不支持文件加锁的UNIX实现上,可以是会用一些特别的加锁技术。尽管这些技术都已经被fcntl()记录加锁计数所取代,但这里仍然需要介绍它们,因为一些较早的应用程序中,仍然存在他们的身影。所有的这些技术在性质上都是劝告式的

open(file, O_CREAT | O_EXCL,...)加上unlink(file)

SUSv3第三版的单一规范里,要求使用了O_CREATO_EXCL标记的open()调用原子执行检查文件的存在性,以及创建文件的两个步骤。这就意味着如果两个进程尝试在创建一个文件时指定这些标记,那么就保证其中只有一个进程能够成功。(另外一个进程从open中收到EEXIST错误。)这种调用与unlink()系统调用组合起来就构成一种加锁机制的基础。获取所可通过使用O_CREATO_EXCL标记打开文件后,立即跟着一个close()来完成,释放锁可通过使用unlink()来完成,尽管这项技术能够正常的工作,但它存在一些局限:

  • 如果open()失败了,即表示其他进程拥有了锁,那么就必须要在某种循环中重试open()操作,这种循环既可以是持续不断地,也可以是相邻两次尝试时间上加上一定的时间延时。有了fcntl()之后则可以使用F_SETTLKW来阻塞直到锁可用为止。

  • 使用open()uunlink()获取和释放锁涉及到的文件系统的操作,这比记录锁要慢得多,

  • 如果一个进程意外的终止并且没有删除锁文件,那么锁就不会被释放。处理这个问题存在特别的技术,包括检查文件的上次修改时间和让锁的持有者将进程ID写入文件,这样就能够检查进程是否存在,但这些计数中没有一项技术是安全可靠的,与之相反的是,在一个进程终止时记录锁的释放操作是原子的。

  • 如果放置多把锁,那么就无法检车出死锁,如果发生了死锁,那么造成死锁的进程就会永远的保持阻塞。

  • 第二版的NFS不支持O_EXCL语义。Linux 2.4NFS客户端也没有正确地实现O_EXCL,即使是第三版的NFS以及以后版本的也没能完成这个任务。

link(file, lockfile)加上unink()lockfile

link()系统调用,在新链接已经存在时,会失败的事实可用作一种加锁机制,而解锁功能则还是使用unlink()来完成。常规的做法是让需要获取锁的进程创建一个临时的文件名,一般来将需要包含进程ID。要获取锁则需要将这个临时文件链接到某个约定的标准路径上。如果link()调用成功,那么就获取了锁,如果调用失败EEXIST,那么就是另一个进程持有了锁,因此必须要在稍后的某个时刻重新尝试获取该锁。这项技术与上面介绍的open(file, O_CREAT|O_EXCL, 0)技术存在相同的局限。

open(file, O_CREAT|O_TRUNC|O_WRONLY, 0)

当指定O_TRUNC并且写权限被拒绝时在一个既有文件上调用open()会失败的事实,可作为一项几所技术的基础,要获取一把锁可以使用下面的代码,来创建新文件。

fd=open(file, O_CREAT|O_TRUNC|O_WRONLY, (mode_t)0);

close(fd);

如果open()调用成功,那么就获取了锁,如果因EACCES而失败(即文件存在但是没没有人拥有权限),那么其他进程持有了锁,还需要在后面某个时刻尝试重新获取锁。这项计数与前面介绍的技术存在相同的局限,还需要注意的是不能具备超级用户特权的程序使用这项技术,因为open()总是会成功,不管文件上设置的权限是什么。

总结

文件加锁使得进程能偶同步对一个文件的访问,Linux提供了两种文件加锁的系统调用

  1. BSD衍生出来的flock()
  2. system V衍生出来的fcntl()

尽管这两组系统调用在大多数UNIX实现上都是可用的,但只有fcntl()加锁是在SUSv3中进行了标准化。


flock()系统调用对整个文件加锁,可放置的锁有两种:一种是共享锁,这种锁与其他进程持有的共享锁是兼容的;另一种互斥锁,这种锁能偶阻止其他进程放置这两种锁。


fcntl()系统调用将一个文件的任意区域上放置一把锁(“记录锁”),这个区域可以是单个字节也可以是整个文件。可放置的锁有两种:读锁和写锁,他们之间的兼容性语义与flock()放置的共享锁和互斥锁之间的兼容语义类似。如果一个阻塞式(F_SETLKW)锁请求将会导致死锁,那么内核将让其中一个受影响的进程的fcntl()失败(返回EDADLK错误)。


除非系统中的flock()是使用fcntl()实现的,否则使用flock()fcntl()放置的锁之间是相互不可见的,通过flcok()fcntl()放置的锁在fork()中的继承语义和在文件描述符被关闭时的释放语义是不同的。

Linux特有的/proc/locks文件给出了系统中所有进程当期持有的文件锁。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章