简述记录锁
前言:书中锁的部分快要结束了,接下来就是「信号量」了,加油干!
0X00 「记录锁」的感性认识
首先不要被他的名字误导了,「记录锁」本身的概念并不难,只是这个名字让他有点奇怪。。。
我一说它的常见用法你就能立马理解这个锁是干啥的,书上说:
「记录锁」可用于有亲缘关系或无亲缘关系的进程间共享某个文件的读写,被锁住的文件通过其文件描述符访问,由于是进程之间的共享,所以这种锁通常被内核维护。
Posix 的记录锁是一种「劝告锁」,劝告锁的意思是(摘自:https://www.ibm.com/developerworks/cn/linux/l-cn-filelock/index.html):
「劝告锁」是一种协同工作的锁。对于这一种锁来说,内核只提供加锁以及检测文件是否已经加锁的手段,但是内核并不参与锁的控制和协调。也就是说,如果有进程不遵守“游戏规则”,不检查目标文件是否已经由别的进程加了锁就往其中写入数据,那么内核是不会加以阻拦的。因此,劝告锁并不能阻止进程对文件的访问,而只能依靠各个进程在访问文件之前检查该文件是否已经被其他进程加锁来实现并发控制。
也就是说,如果你有一个进程正在编辑一个文件,此时有另一个进程也开始编辑这个文件,那么内核并不会拒绝此次操作,仅仅是提醒一下
大家可以用 nano 编辑同一文件试试
0X01 与记录锁相关的函数及举例
接下来我们用 fcntl 实现一个「文件锁」
struct flock {
short l_type; /* Type of lock: F_RDLCK,
F_WRLCK, F_UNLCK */
short l_whence; /* How to interpret l_start:
SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* Starting offset for lock */
off_t l_len; /* Number of bytes to lock */
pid_t l_pid; /* PID of process blocking our lock
(F_GETLK only) */
};
int fcntl (int fd, int cmd, struct flock *lock);
在 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 是从文件开始计算的。
而对于 fcntl 函数来说
其中,参数 fd 表示文件描述符;参数 cmd 指定要进行的锁操作,在这里只介绍与书上说的文件锁相关的三个取值 F_GETLK、F_SETLK 以及 F_SETLKW。这三个值均与 flock 结构有关
更多详细的内容在这里:
https://ibm.com/developerworks/cn/linux/l-cn-filelock/index.html
我就不重复了,少一点冗余信息
书上说记录锁有一个常见用途:确保某个程序(例如守护程序)在任何时候只有一个副本在运行
原理如下:
守护进程只维护一个只有 1 行文本的文件,其中包含它的进程 ID。它打开这个文件,必要的时候创建之,然后请求整个文件的一个写入锁,如果没有取得该锁,说明有已经存在一个守护进程的副本了,接着报错退出
接下来我们开始实现这个小程序:
#include "unpipc.h"
#define PATH_PIDFILE "./pidfile"
int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len)
{
struct flock lock;
lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */
// 后三个参数表示被加锁的位置,可以使整个文件,也可以是部分文件,l_whence 表示加锁的开始,l_len 表示加锁的长度
// whence 只能取三个值,SEEK_SET 表示文件开始,SEEK_CUR 表示文件中间部分,SEEK_END 表示文件结尾
lock.l_start = offset; /* byte offset, relative to l_whence */
lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* #bytes (0 means to EOF) */
return (fcntl(fd, cmd, &lock)); /* -1 upon error */
}
int write_lock(int fd, off_t offset, int whence, off_t len)
{
// F_SETLK 进程用它来对文件的某个区域进行加锁,如果有其他锁阻止该锁被建立,那么 fcntl() 就出错返回
// F_WRLCK 写锁
return lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len);
}
int main(int argc, char *argv[])
{
int pidfd;
char line[MAXLINE];
pidfd = open(PATH_PIDFILE, O_RDWR | O_CREAT, FILE_MODE);
// 请求写锁
if (write_lock(pidfd, 0, SEEK_SET, 0) != 0) {
// 为防止和正常的返回值混淆,系统调用并不直接返回错误码,而是将错误码放入一个名为 errno 的全局变量中。如果一个系统调用失败,你可以读出 errno 的值来确定问题所在
if (errno == EACCES || errno == EAGAIN) {
// 请求失败,说明已经存在了一个守护进程了
fprintf(stderr, "unable to lock %s, is %s already running?",
PATH_PIDFILE, argv[0]);
exit(-1);
} else {
fprintf(stderr, "unable to lock %s", PATH_PIDFILE);
exit(-1);
}
}
// 否则是第一次开启守护进程
// 给文件写入自己的 pid
sprintf(line, "%ld\n", (long)getpid());
write(pidfd, line, strlen(line));
// 阻塞
getchar();
return 0;
}
完整头文件在这里:https://github.com/TensShinet/learn_IPC/blob/master/my_code/fcntl/unpipc.h
书上锁的部分先告一段落