UNP卷二 chapter9 记录上锁
此处讲述的锁是读写锁的一种扩展类型,可用于亲缘关系或无亲缘关系的进程之间共享某个文件的读或写。被锁住的文件通过其描述符访问,执行上锁操作的函数是fcntl。此类型的锁通常在内核中维护,其属主是由属主的进程ID标识的。意味着此锁用于不同进程间的上锁,而不是用于同一进程内不同线程间的上锁。
1、一个例子讲明一个进程记录上锁
利用打印机假脱机处理系统使用的技巧,即给每台打印机准备一个文件,此文件只有一个单行的ASCII文本文件,其中含有待用的下一个***,需要给某个打印作业赋一个***的每个进程都得经历以下三个步骤:
i、读***文件;
ii、使用其中的***;
iii、给***加1并写回文件中。
于是一个程能够设置某个锁,以宣称没有其进程能够访问相应的文件,直到第一个进程完成访问为止。代码实现:
#include "unpipc.h"
#define SEQFILE "seqno" /* filename *///含有待用的下一个***文件
void my_lock(int), 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); /* lock the file */
//在程序中对文件进程读写操作,读写函数之间需要插入lseek函数以隔开才可成功
Lseek(fd, 0L, SEEK_SET); /* rewind before read */
n = Read(fd, line, MAXLINE);
line[n] = '\0'; /* null terminate for sscanf */
n = sscanf(line, "%ld\n", &seqno);//将***输入到seqno变量中
printf("%s: pid = %ld, seq# = %ld\n", argv[0], (long)pid, seqno);
seqno++; /* increment sequence number */
snprintf(line, sizeof(line), "%ld\n", seqno);
Lseek(fd, 0L, SEEK_SET); /* rewind before write */
Write(fd, line, strlen(line));
my_unlock(fd); /* unlock the file */
}
exit(0);
}
void
my_lock(int fd)
{
struct flock lock;
lock.l_type = F_WRLCK;//若改为F_RDLCK,则同一区段允许有多个读出锁,这与无锁是一样的,因为记录上锁允许同时有多个读出锁的存在
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0; /* write lock entire file */
Fcntl(fd, F_SETLKW, &lock);//F_SETLKW,此处W的含义是调用线程阻塞到该锁能够授予为止
}
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; /* unlock entire file */
Fcntl(fd, F_SETLK, &lock);
}
2、对比记录上锁与文件上锁
Posix记录上锁定义了一个特殊的字节范围以指定整个文件,它的起始偏移为0(文件的开头),长度也为0。本章的讨论集中于记录上锁,文件上锁只是它的一个特例。
术语粒度用于标记能被锁住的对象的大小。对于Posix记录上锁来说,粒度就是单个字节。粒度越小,允许同时使用的用户数就越多。
第一个真正的文件和记录上锁是由John Bass于1980年加到Version7中的,新增的一个系统调用名为locking。提供强制性记录上锁。后续将讲明强制性上锁和劝告性上锁的区别,以及记录上锁和文件上锁的区别。
3、Posix fcntl记录上锁
记录上锁的Posix接口是fcntl函数。
#include<fcntl.h>
int fcntl(int fd, int cmd, .../* struct flcok *arg */);
//返回:若成功则取决于cmd,若出错则为-1
a、第三个参数arg是指向某个flock结构的指针:
struct flock {
short l_type;/* F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence;/* SEEK_SEK, SEEK_CUR, SEEK_END*/
off_t l_start;/* relative starting offset in bytes */
off_t l_len;/* #bytes; 0 means until end-of-file */
pid_t l_pid;/* PID returned by F_GETLK */
};
b、第二个参数cmd如下:
F_SETLK 获取(l_type成员为F_RDLCK或F_WRLCK)或释放(l_type成为F_UNLCK)由arg指向的flock结构所描述的锁。如果无法将该锁授予调用进程 ,该函数返回一个EACCES或EAGAIN错误而不阻塞。
F_SETLKW 该命令可看成是上一个命令+wait,即阻塞到该锁能够授予为止。
F_GETLK 检查由arg指向的锁以确定是否有某个已存在的锁会妨碍将新锁授予调用进程。如果当前没有这样的锁存在,由arg指向的flock结构的l_type成员就被置为F_UNLCK。否则,关于这个已存在锁的信息将在由arg指向的flcok结构中返回(也就是说,该结构的内容由fcntl函数覆盖),其中包括持有该锁的进程的进程ID。
c、flcok结构描述锁的类型(读出锁或写入锁)以及待锁住的字节范围。跟lseek一样,起始字节偏移是作为一个相对偏移(l_start成员)伴随其解释(l_whence成员)指定的。
l_whence成员有以下三个取值(与lseek或fseek一样哈)。
i、SEEK_SET:l_start相对于文件的开头解释。
ii、SEEK_CUR:l_start相对于文件的当前字节偏移(即当前读写指针位置)解释。
iii、SEEK_END:l_start相对于文件的末尾解释。
l_len成员指定从偏移开始的连续字节数。长度为0意思“从起始偏移到文件偏移的最大可能值”。
因此锁住整个文件有两种方式:
i、指定l_whence成员为SEEK_SET,l_start成员为0,l_len成员为0。(此方式常用,只需调用一个)
ii、使用lseek把读写指针定位到文件头,然后指定l_whence成员为SEEK_CUR,l_start成员为0,l_len成员为0。
d、fcntl记录上锁既可用于读也可用于写,对于一个文件的任意字节,最多只能存在一种类型的锁(读出锁或写入锁)。而且,一个给定字节可以有多个读出锁,但只能有一个写入锁。
对于一个打开着某个文件的给定进程来说,当它关闭该文件的所有描述符或它本身终止时,与该文件关联的所有锁都被删除。锁不能通过fork由子进程继承。
e、调用fcntl获取或释放一个锁
/* include lock_reg */
#include "unpipc.h"
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 */
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 */
}
/* end lock_reg */
f、调用fcntl测试一个锁
/* include lock_test */
#include "unpipc.h"
pid_t
lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
struct flock lock;
lock.l_type = type; /* F_RDLCK or F_WRLCK */
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) */
if (fcntl(fd, F_GETLK, &lock) == -1)
return(-1); /* unexpected error */
if (lock.l_type == F_UNLCK)
return(0); /* false, region not locked by another proc */
return(lock.l_pid); /* true, return positive PID of lock owner */
}
/* end lock_test */
g、简化用的宏(针对记录上锁的简化用的宏)
#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)
3、劝告性上锁
Posix记录上锁称为劝告性上锁,其含义是内核维护着已由各个进程上锁的所有文件的正确信息,但是它不能防止一个进程写入一个已由另一个进程读锁定的某个文件。类似的,不以防止一个进程读取一个已由另一个进程写锁定的某个文件。一个进程能够无视一个劝告性锁而写一个读锁定文件,或读一个写锁定文件,前提是该进程有读或写该文件的足够权限。
以下这段话,最后一句没怎么理解到位。
劝告性锁对于协作进程是足够了。网络编程中守护程序的编写是协作进程的一个例子:这些程序访问诸如***文件之类的共享资源,而且都在系统管理员的控制之下。只要含有***的真正文件不是任何进程都可写,那么在该文件被锁住期间,不理会劝告性锁的随意进程无法写它。?????
4、强制性上锁
另一种类型的记录上锁(强制性上锁)。使用强制性锁后,内核检查每个read和write请求,以验证其操作不会干扰由某个进程持有的某个锁。对于通常的阻塞式描述符,与某个强制性锁冲突的read或write将把调用进程投入睡眠,直到该锁释放为止。对于非阻塞式描述符,与某个强制性锁冲突的read或write将导致它们返回一个EAGAIN错误。
对于某个特定文件施行强制性上锁,应满足:
i、组成员执行位必须关掉;
ii、SGID位必须打开。
在支持强制性记录上锁的系统上,ls命令查找权限位的这种特殊组合,并输出l或L以指示相应文件的强制性上锁是否启用。类似地,chmod命令接受l这个指示符以给某个文件启用强制性上锁。
5、读出者和写入者的优先级
i、某个写入锁待处理期间的额外读出锁(此时读出锁可以有多个,即读出锁先于写入锁给予)
相应代码:
#include "unpipc.h"
int
main(int argc, char **argv)
{
int fd;
fd = Open("test1.data", O_RDWR | O_CREAT, FILE_MODE);//打开文件
Read_lock(fd, 0, SEEK_SET, 0); /* parent read locks entire file */
printf("%s: parent has read lock\n", Gf_time());
if (Fork() == 0) {
/* 4first child */
sleep(1);
printf("%s: first child tries to obtain write lock\n", Gf_time());
Writew_lock(fd, 0, SEEK_SET, 0); /* this should block */
printf("%s: first child obtains write lock\n", Gf_time());
sleep(2);
Un_lock(fd, 0, SEEK_SET, 0);//释放写入锁
printf("%s: first child releases write lock\n", Gf_time());
exit(0);
}
if (Fork() == 0) {
/* 4second child */
sleep(3);
printf("%s: second child tries to obtain read lock\n", Gf_time());
Readw_lock(fd, 0, SEEK_SET, 0);//试图获取整个文件的读出锁
printf("%s: second child obtains read lock\n", Gf_time());
sleep(4);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child releases read lock\n", Gf_time());
exit(0);
}
/* 4parent */
sleep(5);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: parent releases read lock\n", Gf_time());
exit(0);
}
相应测试结果表明,第二个子进程请求的读出锁是立即给予的,第一个子进程的写入锁被排入请求队列。
ii、等待着的写入者是否比等待着的读出者优先
相应代码:
#include "unpipc.h"
int
main(int argc, char **argv)
{
int fd;
fd = Open("test1.data", O_RDWR | O_CREAT, FILE_MODE);
Write_lock(fd, 0, SEEK_SET, 0); /* parent write locks entire file */
printf("%s: parent has write lock\n", Gf_time());
if (Fork() == 0) {
/* 4first child */
sleep(1);
printf("%s: first child tries to obtain write lock\n", Gf_time());
Writew_lock(fd, 0, SEEK_SET, 0); /* this should block */
printf("%s: first child obtains write lock\n", Gf_time());
sleep(2);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: first child releases write lock\n", Gf_time());
exit(0);
}
if (Fork() == 0) {
/* 4second child */
sleep(3);
printf("%s: second child tries to obtain read lock\n", Gf_time());
Readw_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child obtains read lock\n", Gf_time());
sleep(4);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: second child releases read lock\n", Gf_time());
exit(0);
}
/* parent */
sleep(5);
Un_lock(fd, 0, SEEK_SET, 0);
printf("%s: parent releases write lock\n", Gf_time());
exit(0);
}
与第一个例子一起总结:当文件被取得写入锁时,其他进程对此文件的写入锁,读出锁统统都阻塞,此时FIFO顺序处理上锁请求的,当文件被取得读出锁时,其他进程对此文件可再次读出,但写锁阻塞。
以上知识点来均来自steven先生所著UNP卷二(version2),刚开始学习网络编程,如有不正确之处请大家多多指正。