《UNIX网络编程 卷2》 笔记: 记录上锁

来源:互联网 发布:软件开发企业收入确认 编辑:程序博客网 时间:2024/05/20 04:11

本节讲述记录上锁,它是读写锁的一种扩展类型,它可用于有亲缘关系或无亲缘关系的进程之间共享某个文件的读与写。使用记录上锁,应用可以指定文件中需要上锁或解锁的字节范围。Posix记录上锁定义了一个特殊的字节范围以指定整个文件,这就是文件上锁,它只是记录上锁的一个特例。

Linux提供的记录上锁功能的函数是fcntl

#include <unistd.h>#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */ );
如果该函数用于记录上锁,那么它要求第三个参数arg是一个指向flock结构体的指针。flock结构体定义如下:

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   (set by F_GETLK and F_OFD_GETLK) */   ...};

它描述锁的类型(读出锁或写入锁)以及待锁住的字节范围。指定l_whence为SEEK_SET,l_start为0,l_len为0可锁住整个文件。

用于记录上锁功能的参数cmd的三个值如下:

    F_SETLK:获取(l_type成员为F_RDLCKF_WRLCK)或释放(l_type成员为F_UNLCK)由arg指向的flock结构描述的锁。

    F_SETLKW:与上个命令类似,区别是获取不到锁进程会阻塞。

    F_GETLK:检查由arg参数指向的锁是否被进程占用。如果锁被进程占用,可获取该进程的PID和锁的类型。

我们先看一个例子,看看如果没有记录上锁,多个进程间共享文件可能会出现什么现象。

在这个例子中,我们首先创建一个名为“sqeno”的ASCII文件,文件只有一行,包含一个字符“1”,表示初始的序列号。然后写一个名为locknone的小程序,这个程序做如下的工作:

(1)读序列号文件

(2)打印进程PID和文件中的序列号

(3)给序列号加1并写回文件中

其中步骤(2)和步骤(3)重复20遍。

程序代码如下:

#include "unpipc.h"#define SEQFILE "seqno"void my_lock(int);void my_unlock(int);int main(int argc, char **argv){int fd;long i, seqno;pid_t pid;ssize_t n;char line[MAXLINE + 1];pid = getpid();fd = Open(SEQFILE, O_RDWR, FILE_MODE);for (i = 0; i < 20; i++) {my_lock(fd);Lseek(fd, 0L, SEEK_SET);/*读出序列号字符串形式*/n = Read(fd, line, MAXLINE);line[n] = '\0';/*转成数值形式*/n = sscanf(line, "%ld\n", &seqno);printf("%s: pid = %ld, seq# = %ld\n", argv[0], (long)pid, seqno);/*序列号加1*/seqno++;/*转成字符串形式*/snprintf(line, sizeof(line), "%ld\n", seqno);Lseek(fd, 0L, SEEK_SET);/*写入文件中*/Write(fd, line, strlen(line));my_unlock(fd);}exit(0);}
my_lock和my_unlock是给文件上锁的函数,现在为空函数。

然后几乎同时执行同一个程序两次,那么可能会出现如下的情况:

liu@ubuntu:~/work$ ./locknone: pid = 93426, seq# = 1
./locknone: pid = 93426, seq# = 2
./locknone: pid = 93426, seq# = 3
./locknone: pid = 93426, seq# = 4
./locknone: pid = 93426, seq# = 5
./locknone: pid = 93426, seq# = 6
./locknone: pid = 93426, seq# = 7
./locknone: pid = 93426, seq# = 8
./locknone: pid = 93426, seq# = 9
./locknone: pid = 93426, seq# = 10
./locknone: pid = 93426, seq# = 11
./locknone: pid = 93426, seq# = 12
./locknone: pid = 93426, seq# = 13
./locknone: pid = 93426, seq# = 14
./locknone: pid = 93426, seq# = 15
./locknone: pid = 93426, seq# = 16
./locknone: pid = 93426, seq# = 17
./locknone: pid = 93426, seq# = 18
./locknone: pid = 93426, seq# = 19
./locknone: pid = 93426, seq# = 20
./locknone: pid = 93425, seq# = 1
./locknone: pid = 93425, seq# = 2
./locknone: pid = 93425, seq# = 3
./locknone: pid = 93425, seq# = 4
./locknone: pid = 93425, seq# = 5
./locknone: pid = 93425, seq# = 6
./locknone: pid = 93425, seq# = 7
./locknone: pid = 93425, seq# = 8
./locknone: pid = 93425, seq# = 9
./locknone: pid = 93425, seq# = 10
./locknone: pid = 93425, seq# = 11
./locknone: pid = 93425, seq# = 12
./locknone: pid = 93425, seq# = 13
./locknone: pid = 93425, seq# = 14
./locknone: pid = 93425, seq# = 15
./locknone: pid = 93425, seq# = 16
./locknone: pid = 93425, seq# = 17
./locknone: pid = 93425, seq# = 18
./locknone: pid = 93425, seq# = 19
./locknone: pid = 93425, seq# = 20

可以看到,当发生进程切换后,第二个进程读取到的序列号是1而不是21,它好像完全不知道另一个进程也在修改该文件。

现在我们把my_lock和my_unlock函数功能修改为文件上锁和解锁,代码如下:

void my_lock(int fd){struct flock lock;/*写入锁*/lock.l_type = F_WRLCK;/*整个文件范围*/lock.l_whence = SEEK_SET;lock.l_start = 0;lock.l_len = 0;Fcntl(fd, F_SETLKW, &lock);return;}void my_unlock(int fd){struct flock lock;/*释放锁*/lock.l_type = F_UNLCK;/*整个文件范围*/lock.l_whence = SEEK_SET;lock.l_start = 0;lock.l_len = 0;Fcntl(fd, F_SETLK, &lock);return;}

我们把修改后的程序改名为lockfcntl,再同时执行这个程序两次,结果如下:

liu@ubuntu:~/work/unpipc/lock$ ./lockfcntl: pid = 93474, seq# = 1
./lockfcntl: pid = 93474, seq# = 2
./lockfcntl: pid = 93474, seq# = 3
./lockfcntl: pid = 93474, seq# = 4
./lockfcntl: pid = 93474, seq# = 5
./lockfcntl: pid = 93474, seq# = 6
./lockfcntl: pid = 93474, seq# = 7
./lockfcntl: pid = 93474, seq# = 8
./lockfcntl: pid = 93474, seq# = 9
./lockfcntl: pid = 93474, seq# = 10
./lockfcntl: pid = 93474, seq# = 11
./lockfcntl: pid = 93474, seq# = 12
./lockfcntl: pid = 93474, seq# = 13
./lockfcntl: pid = 93474, seq# = 14
./lockfcntl: pid = 93474, seq# = 15
./lockfcntl: pid = 93474, seq# = 16
./lockfcntl: pid = 93474, seq# = 17
./lockfcntl: pid = 93474, seq# = 18
./lockfcntl: pid = 93474, seq# = 19
./lockfcntl: pid = 93474, seq# = 20
./lockfcntl: pid = 93475, seq# = 21
./lockfcntl: pid = 93475, seq# = 22
./lockfcntl: pid = 93475, seq# = 23
./lockfcntl: pid = 93475, seq# = 24
./lockfcntl: pid = 93475, seq# = 25
./lockfcntl: pid = 93475, seq# = 26
./lockfcntl: pid = 93475, seq# = 27
./lockfcntl: pid = 93475, seq# = 28
./lockfcntl: pid = 93475, seq# = 29
./lockfcntl: pid = 93475, seq# = 30
./lockfcntl: pid = 93475, seq# = 31
./lockfcntl: pid = 93475, seq# = 32
./lockfcntl: pid = 93475, seq# = 33
./lockfcntl: pid = 93475, seq# = 34
./lockfcntl: pid = 93475, seq# = 35
./lockfcntl: pid = 93475, seq# = 36
./lockfcntl: pid = 93475, seq# = 37
./lockfcntl: pid = 93475, seq# = 38
./lockfcntl: pid = 93475, seq# = 39
./lockfcntl: pid = 93475, seq# = 40

可以看到,发生进程切换后,第二个进程读取到的序列号是21,这正是两个进程使用文件锁进行同步后的结果。

Posix记录上锁也称为劝告性上锁。其含义是内核维护着已由各个进程上锁的所有文件的正确信息,但是它不能防止一个进程写已由另一个进程读锁定的某个文件,也不能防止一个进程读已由另一个进程写锁定的某个文件。所以,劝告性锁都在协作进程间使用。执行如下命令后的一个输出结果就能说明Posix记录上锁是劝告性的。

liu@ubuntu:~/work$ ./lockfcntl & ./locknone &

liu@ubuntu:~/work$ ./lockfcntl: pid = 62641, seq# = 1
./lockfcntl: pid = 62641, seq# = 2
./lockfcntl: pid = 62641, seq# = 3
./lockfcntl: pid = 62641, seq# = 4
./locknone: pid = 62642, seq# = 1
./locknone: pid = 62642, seq# = 2
./locknone: pid = 62642, seq# = 3
./locknone: pid = 62642, seq# = 4
./locknone: pid = 62642, seq# = 5
./locknone: pid = 62642, seq# = 6
./locknone: pid = 62642, seq# = 7
./locknone: pid = 62642, seq# = 8
./lockfcntl: pid = 62641, seq# = 5
./lockfcntl: pid = 62641, seq# = 6
./locknone: pid = 62642, seq# = 9
./locknone: pid = 62642, seq# = 10
./lockfcntl: pid = 62641, seq# = 7
./lockfcntl: pid = 62641, seq# = 8
./locknone: pid = 62642, seq# = 11
./locknone: pid = 62642, seq# = 12
./lockfcntl: pid = 62641, seq# = 9
./lockfcntl: pid = 62641, seq# = 10
./locknone: pid = 62642, seq# = 13
./locknone: pid = 62642, seq# = 14
./locknone: pid = 62642, seq# = 15
./lockfcntl: pid = 62641, seq# = 11
./lockfcntl: pid = 62641, seq# = 12
./lockfcntl: pid = 62641, seq# = 13
./locknone: pid = 62642, seq# = 16
./locknone: pid = 62642, seq# = 17
./locknone: pid = 62642, seq# = 18
./locknone: pid = 62642, seq# = 19
./lockfcntl: pid = 62641, seq# = 14
./lockfcntl: pid = 62641, seq# = 15
./locknone: pid = 62642, seq# = 20
./lockfcntl: pid = 62641, seq# = 16
./lockfcntl: pid = 62641, seq# = 17
./lockfcntl: pid = 62641, seq# = 18
./lockfcntl: pid = 62641, seq# = 19
./lockfcntl: pid = 62641, seq# = 20

可以看到发生进程切换时,locknone进程获取到的序列号不是lockfcntl进程写入到文件中的序列号,说明lockfcntl进程持有的劝告性记录锁对locknone进程没有影响。

从上述my_lock和my_unlock函数可以看到请求或释放一个锁都需要填充flock结构体,我们可以使用如下的宏简化程序:

/*以非阻塞的方式获取读出锁*/#define read_lock(fd, offset, whence, len) \lock_reg(fd, F_SETLK, F_RDLCK, offset, whence, len)/*以阻塞的方式获取读出锁*/#define readw_lock(fd, offset, whence, len) \lock_reg(fd, F_SETLKW, F_RDLCK, offset, whence, len)/*以非阻塞的方式获取写入锁*/#define write_lock(fd, offset, whence, len) \lock_reg(fd, F_SETLK, F_WRLCK, offset, whence, len)/*以阻塞的方式获取写入锁*/#define writew_lock(fd, offset, whence, len) \lock_reg(fd, F_SETLKW, F_WRLCK, offset, whence, len)/*释放锁*/#define un_lock(fd, offset, whence, len) \lock_reg(fd, F_SETLK, F_UNLCK, offset, whence, len)/*判断读出锁是否被进程占用*/#define is_read_lockable(fd, offset, whence, len) \!lock_test(fd, F_RDLCK, offset, whence, len)/*判断写入锁是否被进程占用*/#define is_write_lockable(fd, offset, whence, len) \!lock_test(fd, F_WRLCK, offset, whence, len)int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len){struct flock lock;lock.l_type = type;lock.l_start = offset;lock.l_whence = whence;lock.l_len = len;return fcntl(fd, cmd, &lock);}pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len){struct flock lock;lock.l_type = type;lock.l_start = offset;lock.l_whence = whence;lock.l_len = len;if (fcntl(fd, F_GETLK, &lock) == -1)return -1;/*没有进程占有该文件锁*/if (lock.l_type == F_UNLCK)return 0;/*有进程占用该文件锁*/return lock.l_pid;}

记录上锁的一个常见用途是确保某个守护程序在任何时刻只有一个副本在运行,一个例子如下:

#include "unpipc.h"#define PATH_PIDFILE"pidfile"int main(int argc, char **argv){int pidfd;char line[MAXLINE];/*打开或创建一个pid文件*/pidfd = Open(PATH_PIDFILE, O_RDWR | O_CREAT, FILE_MODE);if (write_lock(pidfd, 0, SEEK_SET, 0) < 0) {if (errno == EACCES || errno == EAGAIN)err_quit("unable to lock %s, is %s already running?",PATH_PIDFILE, argv[0]);elseerr_sys("unable to lock %s", PATH_PIDFILE);}/*将进程pid写入文件*/snprintf(line, sizeof(line), "%ld\n", (long)getpid());Ftruncate(pidfd, 0);Write(pidfd, line, strlen(line));pause();}

最后,关于读出锁和写入锁的优先级,我们还需知道如下两点:

    1. 如果某个资源已经被读锁定,那么后续请求获取读出锁的进程优先级比请求获取写入锁的进程优先级高。
    2. 如果某个资源已经被写锁定,那么后续先发起锁请求的进程先获得锁,而不区分锁请求是读出锁还是写入锁。

原创粉丝点击