UNIX再学习 -- 记录锁

APUE第 3 章,参看:UNIX再学习 -- 文件I/O  fcntl 函数它的记录锁功能我们当时没讲。接下来就详细说明下。

一、读写冲突

1、如果两个或两个以上的进程同时向一个文件的某个特定的区域写入数据,那么最后写入文件的数据极有可能因为写操作的交错而产生混乱。


2、如果一个进程写而其他进程同时在读一个文件的某个特定区域,那么读出的数据极有可能因为读写操作的交错而不完整。


多个进程同时读一个文件的某个特定区域,不会有任何问题,它们只是各自把文件中的数据拷贝到各自的缓冲区中,并不会改变文件的内容,相互之间也就不会冲突。
由此可以得出结论,为了避免在读写同一个文件的同一个区域时发生冲突,进程之间应该遵循以下规则:
如果一个进程正在写,那么其他进程既不能写也不能读。
如果一个进程正在读,那么其他进程不能写但是可以读。

二、读锁和写锁

为了避免多个进程在读写同一个文件的同一区域时发生冲突,UNIX/Linux 系统引入了文件锁机制,并把文件锁分为读锁和写锁两种,它们的区别在于:
读锁:共享锁,对一个文件的特定区域可以加多把读锁。
写锁,排它锁,对一个文件的特定区域只能加一把写锁。
基于锁的操作模型是:读/写文件中的特定区域之前,先加上读/写锁,锁成功了再读/写。读/写完成以后再解锁。

三、加锁和解锁

让我们重温一下 fcntl 函数。
#include <unistd.h>  
#include <fcntl.h>  
int fcntl(int fd, int cmd, ... /* arg */ );  
返回:若成功则依赖于 cmd(见下),若出错则为 -1

1、参数解析

对于记录锁,cmd 是 F_GETLK、F_SETLK 或 F_SETLKW。第三个参数是指向 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//加锁的进程号
                                   (F_GETLK only) */
               ...
           };
对 flock 结构说明如下:
所希望的锁类型: F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或 F_UNLCK(解锁一个区域)
要加锁或解锁的区域的起始地址,由 l_start 和 l_whence 两者决定。l_stat 是相对位移量(字节),l_whence 则决定了相对位移量的起点。
区域的长度,由 l_len 表示。
进程的 ID (l_pid)持有的锁能阻塞当前进程(仅由 F_GETLK 返回)。

关于加锁和解锁区域的说明还要注意下列各点:
指定区域起始偏移量的两个元素与 lseek 函数中最后两个参数类似。l_whence 可选用的值是 SEEK_SET、SEEK_CUR 或 SEEK_END。
该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件起始位置之前开始或越过该起始位置。
如若 l_len 为 0,则表示锁的区域从其起点(由 l_start 和 l_whence 决定)开始直至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。
为了锁整个文件,通常的方法是将 l_start 说明为 0, l_whence 说明为 SEEK_SET,l_len 说明为 0。

2、下面说明一下 fcntl 函数的 3 中命令

(1)F_GETLK

判断由 flockptr 所描述的锁是否会被另外一把锁所排斥(阻塞)。如果存在一把锁,它阻止创建由 flockptr 所描述的锁,则该现有锁的信息将重写 flockptr 指向的信息。如果不存在这种情况,则除了将 l_type 设置为 F_UNLCK 之外,flockptr 所指向结构中的其他信息保持不变

(2)F_SETLK 

设置由 flockptr 所描述的锁。如果我们试图获得一把读锁(l_type 为 F_RDLCK)或写锁(l_type 为 F_WRLCK),而兼容性规则阻止系统给我们这把锁,那么 fcntl 会立即出错返回,此时 errno 设置为 EACCES 或 EAGAIN。

(3)F_SETLKW

这个命令是 F_SETLK 的阻塞版本(命令中的 W 表示等待(wait)) 。如果所请求的读锁或写锁因另一个进程当前已经对所请求区域的某部分进行了加锁而不能被授予,那么调用进程会被置为休眠。如果请求创建的锁已经可用,或者休眠由信号中断,则该进程被唤醒。

应当了解,用 F_GETLK 测试能否建立一把锁,然后甩 F_SETLK 或 F_SETLKW 企图建立那把锁,这两者不是一个原子操作。因此不能保证在这两次 fcntl 调用之间不会有另一个进程插入并建立一把相同的锁。如果不希望在等待锁变为可用时产生阻塞,就必须处理由 F_SETLK 返回的可能的出错。

3、示例说明

想看更多示例,可参看下面的扩展
扩展: 文件锁

(1)从文件头10字节开始的20字节以阻塞模式加读锁

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd == -1)
    {
    perror ("open");
    exit (EXIT_FAILURE);
    }

    struct flock lock;
    lock.l_type = F_RDLCK;  //定义锁操作的类型为加读锁
    lock.l_whence = SEEK_SET;  //定义锁区偏移起点为文件头
    lock.l_start = 10;  //定义锁区从文件头开始计算的偏移 10 个字节
    lock.l_len = 20;  //定义锁区字节长度为 20 个字节,即只对文件中这 20 个字节进行区域加锁。
    lock.l_pid = -1;  //定义加锁进程标示为自动设置
    if (fcntl (fd, F_SETLKW, &lock) == -1) //F_SETLKW 为阻塞模式,是指进程遇锁,将被阻塞直到锁被释放。
    {
    perror ("fcntl");
    exit (EXIT_FAILURE);
    }

    if (close (fd) == -1)
    {
    perror ("close");
    exit (EXIT_FAILURE);
    }

    return 0;
}

(2)从当前位置10字节开始到文件尾以非阻塞模式加写锁

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>

int main()
{
	int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
	if (fd == -1)
	{
		perror ("open");
		exit (EXIT_FAILURE);
	}

	struct flock lock;
	lock.l_type = F_WRLCK;  //定义锁操作的类型为加写锁
	lock.l_whence = SEEK_CUR;  //定义锁区偏移起点为文件当前位置
	lock.l_start = 10;  //定义锁区从文件头开始计算的偏移 10 个字节
	lock.l_len = 0;  //定义锁区字节长度到文件结尾,即仅文件开头的 10 个字节不加锁
	lock.l_pid = -1;  //定义加锁进程标识为自动设置
	if (fcntl (fd, F_SETLK, &lock) == -1)  //F_SETLK 为非阻塞模式,是指进程遇锁,立即以错误返回,并设错误码为EAGAIN
	{
		if (errno != EAGAIN)
		{
			perror ("fcntl");
			exit (EXIT_FAILURE);
		}
		printf ("暂时不能加锁,稍后再试...\n");
	}

	if (close (fd) == -1)
	{
		perror ("close");
		exit (EXIT_FAILURE);
	}

	return 0;
}

(3)对整个文件解锁

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
	int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
	if (fd == -1)
	{
		perror ("open");
		exit (EXIT_FAILURE);
	}

	struct flock lock;
	lock.l_type = F_UNLCK;  //定义锁操作的类型为解锁
	lock.l_whence = SEEK_SET;  //定义锁区偏移起点为文件头
	lock.l_start = 0;  //定义锁区从文件头开始计算
	lock.l_len = 0;  //定义锁区字节长度到文件结尾,即整个文件
	lock.l_pid = -1;  //定义加锁进程标识为自动设置
	if (fcntl (fd, F_SETLKW, &lock) == -1)  //F_SETLKW 为阻塞模式,是指进程遇锁,将被阻塞直到锁被释放。
	{
		perror ("fcntl");
		exit (EXIT_FAILURE);
	}

	if (close (fd) == -1)
	{
		perror ("close");
		exit (EXIT_FAILURE);
	}

	return 0;
}

(4)测试锁

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

//打印不能加锁的具体原因
void why_not (struct flock* lock)
{
	printf ("%d进程", lock->l_pid);
	switch (lock->l_whence)
	{
		case SEEK_SET:
			printf ("在距文件头");
			break;
		case SEEK_CUR:
			printf ("在距当前位置");
			break;
		case SEEK_END:
			printf ("在距文件尾");
			break;
	}
	printf ("%ld字节处,为%ld字节加了", lock->l_start, lock->l_len);
	switch (lock->l_type)
	{
		case F_RDLCK:
			printf ("读锁。\n");
			break;
		case F_WRLCK:
			printf ("写锁。\n");
			break; 
	}
}

int main()
{
	int fd = open ("data.txt", O_RDWR, 0666);
	if (fd == -1)
	{
		perror ("open");
		exit (EXIT_FAILURE);
	}

	struct flock lock;
	lock.l_type = F_RDLCK;
	lock.l_whence = SEEK_SET;
	lock.l_start = 10;
	lock.l_len = 20;
	lock.l_pid = -1;
	//使用函数 fcntl 测试给定文件的特定区域是否可以加锁
	if (fcntl (fd, F_GETLK, &lock) == -1)
	{
		perror ("fcntl");
		exit (EXIT_FAILURE);
	}
	if (lock.l_type == F_UNLCK) //判断能否加锁,在不能加锁的情况下,打印原因
		printf ("此锁可加!\n");
	else
		why_not (&lock);

	if (close (fd) == -1)
	{
		perror ("close");
		exit (EXIT_FAILURE);
	}

	return 0;
}
输出结果:
此锁可加!

(5)示例解析

示例注释讲的很明白了,我现在主要想讲下。下面这两句话:
读锁:共享锁,对一个文件的特定区域可以加多把读锁。
写锁,排它锁,对一个文件的特定区域只能加一把写锁。
可用 示例一  和 示例二,添加延时,比如延时 20 秒,再另一个终端上再次执行加锁,可看到结果。
读锁,可以再加读锁;而写锁,不可再加锁了。

测试一下参数锁能否加上,如果能加上,则不会去加锁而是将锁的类型改成F_UNLCK 如果不能加上,则将文件中已经存在的锁信息通过参数锁带出来并且将 l_pid 设置为真正给文件加锁的进程号,所以可以使用 l_pid 判断能否加 上。

(6)扩展部分

为了避免每次分配 flock 结构,然后又填入各项信息,可写一个函数来处理这些细节。
#include <fcntl.h>
#include "apue.h"

int lock_leg (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, cmnd, &lock));
}

四、进阶

1、死锁

(1)死锁产生

讲线程互斥量的时候我们讲过死锁,当然这里的讲的是文件锁的死锁。
如果两个进程相互等待对方持有并且不释放(锁定)的资源时,则这两个进程就处于死锁状态。如果一个进程已经控制了文件中的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,那么它就会休眠,在这种情况下,有发生死锁的可能性。

(2)示例说明

#include "apue.h"
#include <fcntl.h>

static void
lockabyte(const char *name, int fd, off_t offset)
{
	if (writew_lock(fd, offset, SEEK_SET, 1) < 0)
		err_sys("%s: writew_lock error", name);
	printf("%s: got the lock, byte %lld\n", name, (long long)offset);
}

int
main(void)
{
	int		fd;
	pid_t	pid;

	/*
	 * Create a file and write two bytes to it.
	 */
	if ((fd = creat("templock", FILE_MODE)) < 0)
		err_sys("creat error");
	if (write(fd, "ab", 2) != 2)
		err_sys("write error");

	TELL_WAIT();
	if ((pid = fork()) < 0) {
		err_sys("fork error");
	} else if (pid == 0) {			/* child */
		lockabyte("child", fd, 0);
		TELL_PARENT(getppid());
		WAIT_PARENT();
		lockabyte("child", fd, 1);
	} else {						/* parent */
		lockabyte("parent", fd, 1);
		TELL_CHILD(pid);
		WAIT_CHILD();
		lockabyte("parent", fd, 0);
	}
	exit(0);
}
输出结果:
child:got the lock,byte 0
parent:got the lock,byte 1
child:writew_lock error:Deadlock situation detected/avoided
parent:got the lock,byte 0

(3)示例解析

上例中,子进程对第 0 字节加锁,父进程对第 1 字节加锁。然后,它们中的每一个又试图对对方已经加锁的字节加锁。所以出现死锁现象。

2、锁的隐含继承和释放

关于记录锁的自动继承和释放有三条规则:

(1)锁与进程、文件两方面有关。

这有两重含意:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。这就意味着如果执行下列四步:
fd1=open (pathname, ...);
read_lock (fd1, ...);
fd2 = dup ( fd1 ) ;
close ( fd2 ) ;
则在 close(fd2)后,在 fd1 上设置的锁被释放。如果将 dup 代换为 open,其效果也一样:
fd1=open (pathname, ...);
read_lock (fd1, ...);
fd2=open (pathname, ...);
close ( fd2) ;

(2)由 fork 产生的子程序不继承父进程所设置的锁。

这意味着,若一个进程得到一把锁,然后调用 fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程要调用 fcntl 以获得它自己的锁。这与锁的作用是相一致的。锁的作用是阻止多个进程同时写同一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。

(3)在执行 exec 后,新程序可以继承原执行程序的锁。

但是注意,如果对一个文件描述符设置了执行时关闭标志,那么当作为 exec 的一部分关闭该文件描述符时,将释放相关文件的所有锁。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

聚优致成

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值