linux程序设计——文件锁定(第七章)

7.2 文件锁定

这篇为linux的文件锁定,代码在文件锁定代码下载。文件锁定是多用户、多任务操作系统中一个非常重要的组成部分。程序经常需要共享数据,而这通常是通过文件来实现的。因此,对于这些程序来说,建立某种控制文件的方式就非常重要。只有这样,文件才可以通过一种安全的方式更新,或者说,当一个程序正在对文件进行写操作时,文件就会进入一个暂时状态,在这个状态下,如果另外一个程序尝试读这个文件,它就会自动停下来等待这个状态的结束。

linux提供了多种特性来实现文件锁定,其中最简单的方式就是以原子操作的方式创建锁文件,所谓“原子操作”就是在创建锁文件时,系统将不允许任何其他的事情发生。这就给程序提供了一种方式来确保它所创建的文件是唯一的,而且这个文件不可能被其他程序在同一时刻创建

第二种方式更高级一些,它允许程序锁定文件的一部分,从而可以独享这一部分内容的访问。有两种不同的方式可以实现第二种形式的文件锁定。我们将只对其中的一种做详细介绍,因为这两种方式非常相似——第二种方式只不过是程序接口稍微不同而已。

7.2.1 创建锁文件

许多应用程序只需要能够针对某个资源创建一个锁文件即可。然后,其他程序就可以通过检查这个文件来判断它们自己是否被允许访问这个资源。

这些锁文件通常都被放置在一个特定位置,并带有一个与被控制资源相关的文件名。例如,当一个调制解调器正在被使用时,linux通常会在/var/spool目录下创建一个锁文件。

注意,锁文件仅仅只是充当一个指示器的角色,程序间需要通过相互协作来使用它们。锁文件只是建议锁,而不是强制锁。

为了创建一个用于锁指示器的文件,可以使用在fcntl.h头文件中定义的open系统调用,并带上O_CREAT和O_EXCL标志。这样就能够以一个原子操作同时完成两项工作:确定文件不存在,然后创建它。

编写程序lock1.c

这个程序调用带有O_CREAT和O_EXCL标志的open来创建文件/tmp/LCK.test。第一次运行程序时,由于这个文件并不存在,所以open调用成功。但对程序的后续调用失败了,因为文件已经存在了。如果想让程序再次执行成功,必须删除那个锁文件。

至少在linux系统中,错误号17代表的是EEXIST,这个错误用来表示一个文件已存在。错误号定义在头文件errno.h或者它所包含的头文件中。

在本例中,这个错误号实际定义在头文件/usr/include /asm-generic/errno-base.h中:

#define EEXIST 17   /*File exitsts*/

这是一个适合与表示open(O_CREATE | O_EXCL)失败的错误号。

如果一个程序在它执行时,只需独占某个资源一段很短的时间——临界区,它就需要在进入临界区之前使用open系统调用创建锁文件,然后在退出临界区时用unlink系统调用删除该锁文件。

编写程序lock2.c

./lock2.exe & ./lock2.exe

这个命令运行程序的两份副本,在后台运行lock2的副本,在前台运行另一份副本。

这个程序使用while语句让程序循环10次,然后通过创建一个唯一的锁文件/tmp/LCK.test2来访问临界资源。如果因为文件已存在而失败,程序将等候一小段时间后再次尝试。如果成功,它就可以访问资源。在标记为“临界区”的部分,可以执行任何需要独占式访问的处理。

程序使用完资源后,通过删除锁文件来释放锁,然后可以在重新申请锁之前执行一些其他的处理(例如sleep(2))。这里锁文件扮演了类似二进制信号量的角色,就问题“可以使用这个资源吗?”给每个程序一个“是”或者“否”的答案,十四章进一步学习信号量。

这是进程间协调性的安排,必须正确地编写代码以使其正常工作,意识到这一点是非常重要的。当程序创建锁文件失败时,它不能通过删除文件并重新尝试的方法来解决此问题。或许这样可以让它创建锁文件,但是另一个创建锁文件的程序将无法得知它已经不再拥有对这个资源的独占式访问权了。

7.2.2 区域锁定

用创建锁的方法来控制对诸如串行口或者不经常访问的文件之类的资源的独占式访问,是一个不错的选择,但它并不适合用于访问大型的共享文件。假设有一个大文件,它由一个程序写入数据,但却由许多不同的程序同时对这个文件进行更新。当一个程序负责记录长期以来连续收集到的数据,而其他一些程序负责对记录的数据进行处理时,这种情况就可能发生。处理程序不能等待记录程序结束,因为记录程序将一直不停地运行,所以它们需要一些协调方法来提供同一个文件的并发访问。

可以通过锁定文件区域的方法来解决这个问题,文件中的某个特定部分被锁定了,但其他程序还可以访问这个文件的其他部分,这被成为文件段锁定或者文件区域锁定。linux提供了至少两种方式来实现这一功能:使用fcntl系统调用和使用lockf调用。我们将主要介绍fcntl接口,因为它是最常使用的接口。lockf和fcntl非常相似,在linux中,它一般作为fcntl的备选接口。但是,fcntl和lockf的锁定机制不能同时工作:它们使用不同底层实现,因此绝对不能混合使用这两种类型的调用,而应该坚持使用其中的一种。

第三章中讲过fcntl调用的定义如下:

#include <fcntl.h>

int fcntl(int filds, int command, ...);

fcntl对一个打开的文件描述符进行操作,并能根据command参数的设置完成不同的任务。它提供了3个用于文件锁定的命令选项:

F_GETLK

F_SETLK

F_SETLKW

当使用这些命令选项时,fcntl的第三个参数必须是一个指向flock结构的指针,所以实际的函数原型为:

int fcntl(int fildes, int command, struct flock* flock_structure);

flock(文件锁)结构依赖具体的实现,但它至少包含下述成员:

short l_type

short l_whence

off_t l_start

off_t l_len

pid_t l_pid

l_type成员的取值定义在头文件fcntl.h中

取值                    说明

F_RDLCK         共享(或读)锁,许多不同的进程可以拥有文件同意区域上的共享锁。只要任一进程拥有一把共享锁,那么就没有进程可以再获得该区域上的独占锁。为了获                                  得一把共享锁,文件必须以“读”或“读/写”方式打开。

F_UNLCK         解锁,用来清除锁

F_WRLCK         独占(或写)锁。只有一个进程可以在文件任意特定区域拥有一把独占锁。一旦一个进程拥有了写锁,其他进程无法在该区域获得任何类型的锁。为了                                             获得独占锁,文件必须以“写”或“读/写”方式打开。

l_whence、l_start和l_len成员定义了文件中的一个区域,即一个连续的字节集合。l_whence的取值必须是SEEK_SET、SEEK_CUR、SEEK_END中的一个。它们分别对应于文件头、当前位置和文件尾。l_whence定义了l_start的相对偏移值,其中,l_start是该区域的第一个字节。l_whence通常被设为SEEK_SET,这时l_start就从文件的开始计算。l_len参数定义了该区域的字节数。

l_pid参数用来记录持有锁的进程。

文件中的每个字节在任一时刻只能拥有一种类型的锁:共享锁、独占锁或解锁。fcntl调用可用的命令和选项的组合非常多。

1.F_GETLK命令

第一个命令是F_GETLK,它用于获取fildes打开的文件的锁信息。它不会尝试去锁定文件。调用进程把想创建的锁类型信息传递给fcntl,使用F_GETLK命令的fcntl就会返回将会阻止获取锁的任何信息。

flock结构中使用的值如下所示:

取值             说明

l_type          如果是共享(只读)锁则取值为F_RDLCK,如果是独占(写)锁则取值为F_WRLCK

l_whence    SEEK_SET、SEEK_CUR、SEEK_END中的一个

l_start          文件区域的第一个字节的相对位置

l_len            文件区域的字节数

l_pid            持有锁的进程的标识符

进程可能使用F_GETLK调用来查看文件中某个区域的当前锁状态,它应该设置flock结构来表明它需要的锁类型,并定义它感兴趣的文件区域。fcntl调用如果成功就返回非-1的值。如果文件已被锁定从而阻止锁请求成功执行,fcntl会用相关信息覆盖flock结构。如果锁请求可以成功执行,flock结构将保持不变。如果F_GETLK调用无法获得信息,它将返回-1表明失败。

如果F_GETLK调用成功,调用程序就必须检查flock结构的内容来判断其是否被修改过。因为l_pid的值被设置成持有锁的进程的标识符,所以通过检查这个字段就可以很方便地判断出flock结构是否被修改过。

2.F_SETLK命令

这个命令试图对fildes指向的文件的某个区域加锁或解锁。flock结构中使用的值如下所示:

取值            说明

l_type          如果是共享(读)锁则取值为F_RDLCK,如果独占(写)锁则为F_WRLCK,如果是解锁则为F_UNLCK

l_pid           不使用

要加锁的区域由flock结构中的l_start、l_whence和l_len的值定义。如果加锁成功,fcntl将返回一个非-1的值;如果失败,则返回-1.

3.F_SETLKW命令

F_SETLKW与F_SETLK命令相同,但在无法获取锁时,这个调用将等待直到可以为止。一旦这个调用开始等待,只有在可以获取锁或收到一个信号时它才返回

程序对某个文件拥有的所有锁都将在相应的文件描述符被关闭时自动清除。在程序结束时也会自动清除各种锁。

7.2.3 锁定状态下的读写操作

对文件区域加锁之后,必须使用底层的read和write调用来访问文件中的数据,而不要使用更高级的fread和fwrite调用,这是因为fread和fwrite会对读写的数据进行缓存,所以执行一次fread调用来读取文件中的头100个字节可能会读取超过100个字节的数据,并将多余的数据在函数库中进行缓存。如果程序再次使用fread来读取下100个字节的数据,它实际上将读取已缓存在函数库中的数据,而不会引发一个底层的read调用来从文件中取出更多的数据。

编写程序lock3.c

程序首先创建一个文件,并以可读可写方式打开它,然后再在文件中添加一些数据。接着在文件中设置两个区域:第一个区域为10~30字节,使用共享锁;第二个区域为40~50字节,使用独占锁。然后程序调用fcntl来锁定这两个区域,并在关闭文件和退出程序前等待10秒钟。

编写程序lock4.c

为了测试锁,需要首先运行程序lock3.exe,然后再运行程序lock4.exe来测试锁。

./lock3.exe & ./lock4.exe

程序中l_type的值为1对应的定义为F_WRLK,l_type的值为0对应的定义为F_RDLK。因此l_type的值为1表明锁失败的原因是已经存在一个写锁了,而l_type的值为0是因为已经存在一个读锁了。在文件中未被lock3程序锁定的区域上,无论是共享锁还是独占锁都将会成功。

可以看到10~30字节上可以设置一个共享锁,因为程序lock3在该区域上设置的是共享锁而不是独占锁。而在40~50字节的区域上,两种锁都将失败,因为lock3已经在该区域上设置了一个独占锁(F_WRLCK)。

7.2.4 文件锁的竞争

接下来测试两个程序争夺文件夹同一区域上的锁时会发生什么情况。再次用lock3.exe来锁定文件,然后用一个新的程序lock5.exe来尝试对它进行加锁。

lock5.exe的作用不再是测试文件中不同部分的锁状态,而是试图对文件中已经锁定的区域再次加锁。

顺便再提一下第三章就讲过的fcntl系统调用,fcntl系统调用对底层文件描述符提供了很多操作方法。

#include <fcntl.h>

int fcntl(int fields, int cmd);

int fcntl(int fileds, int cmd, long arg);

利用fcntl系统调用,可以对打开的文件描述符执行各种操作,包括对它们的复制、获取和设置文件描述符标志、获取和设置文件状态标志,以及管理建议性文件锁等。

7.2.5 其他锁命令

还有另外一种锁定文件的方法;lockf函数,它也通过文件描述符进行操作。其原型为:

#include <unistd.h>

int lockf(int fildes, int function, off_t size_to_lock);

function参数的取值如下所示:

F_ULOCK:   解锁

F_LOCK:     设置独占锁

F_TLOCK:    测试并设置独占锁

F_TEST:     测试其他进程设置的锁

size_to_lock参数是操作的字节数,它从文件的当前偏移值开始计算。

lockf有一个比fcntl函数更简单的接口,这主要是因为它在功能性和灵活性上都要比fcntl函数差一些。为了使用这个函数,必须首先搜寻想要锁定的区域的起始位置,然后以要锁定的字节数为参数来调用它。

与文件锁定的fcntl方法一样,lockf设置的所有锁都是建议锁,它们并不会真正阻止读写文件中的数据。对锁的检测是程序的责任。

7.2.6 死锁

假设两个程序都要更新同一个文件,它们需要同时更新文件中的字节1和字节2。程序A选择首先更新字节1,然后更新字节2,程序B选择首先更新字节2,然后更新字节1。

两个程序同时启动,程序A锁定字节1,程序B锁定字节2,然后A尝试字节2不成功,所以A需要等待,同样,B也需要等待。

这种两个程序都无法继续执行下去的情况,就被称为死锁(deadlok或者deadly embrace)。这个问题在数据库应用程序中很常见,当许多用户频繁访问同一个数据时就很容易发生死锁。大多数的商业关系型数据库都能够检测到死锁并且自动解开,但linux内核不行,这是需要采取一些外部干涉手段,例如强制终止其中一个程序来解决这个问题。

时间: 2024-10-14 21:05:17

linux程序设计——文件锁定(第七章)的相关文章

linux程序设计——数据管理(第七章)

第七章    数据管理 7.1    内存管理 这篇为linux的内存管理,代码在内存管理代码下载.在所有计算机系统中,内存都是一种稀缺资源.linux为应用程序提供了一个简洁的视图,它能反映一个巨大的可直接寻址的内存空间,此外,linux还提供了内存保护机制,它避免了不同的应用程序之间的互相干扰.如果机器被正确配置并且有足够的交换空间,linux还允许应用程序访问比实际物理内存更大的内存空间. 7.1.1 简单的内存分配 使用标准C语言函数库中的malloc调用来分配内存: #include

linux程序设计——数据库(第七章)

7.3 数据库 这篇为linux的dbm数据库,代码在dbm数据库代码下载.可以使用文件来存储数据,为什么还需要数据库呢?因为在有些情况下,数据库的特性提供了解决问题的更好的办法.与使用文件来存储数据相比,使用数据库有如下两方面的优势: 1.可以存储长度可变的数据记录,这对平面的.非结构化的文件来说实现起来有点困难 2.数据库使用索引来有效地存储和检索数据.这样做的一个显著优点是这个索引不必要非得是一个简单的记录号--这在平面文件中很容易实现,它可以是一个任意的字符串. 7.3.1 dbm数据库

linux程序设计——文件操作(第三章)

第三章    文件操作 3.1 linux文件结构 与UNIX一样,linux环境中的文件具有特别重要的意义,因为它们为操作系统服务和设备提供了一个简单而一致的接口.在linux中,一切都是文件. 这意味着,通常程序可以像使用文件那样使用磁盘文件.串行口.打印机等等. 目录也是文件,但它是一种特殊类型的文件.在现代UNIX(包括linux)版本中,即使是超级用户可能也不再被允许直接对目录进行写左操作了.所有用户通常都使用上层的opendir/readdir接口来读取目录,而无需了解特定系统中目录

【linux高级程序设计】(第七章)终端及串口编程 未完成

一.端口设备类型 1.显示设备基本信息 cat /proc/tty/drivers 里面包括了: 当前终端:/dev/tty 前台控制台终端:/dev/console 用于创建虚拟终端的:/dev/ptmx 虚拟终端从设备:pty_slave 虚拟终端主设备:pty_master 物理串口:serial 实际的物理串口

《Linux Device Drivers》 第七章 时间、延时及延缓操作——note

度量时间差 内核通过定时器中断来跟踪时间流 时钟中断由系统定时硬件以周期性的间隔产生,这个间隔由内核根据HZ的值设定,在常见的x86 PC平台上,默认定义为1000 <linux/param.h> <linux/timex.h> jiffies_64 unsigned long jiffies 使用jiffies计数器 <linux/jiffies.h> int time_after(unsigned long a, unsigned long b); int time

【linux程序设计4th】第三章1

makefile .PHONY:clean all CC=gcc CFLAGS=-Wall -g ###replace your bin BIN=simple_write simple_read copy_system all:$(BIN) %.o:%.c $(CC) $(CFLAGS) -c $< -o [email protected] clean: rm -f *.o $(BIN) simple_write.c /* #include <unistd.h> ssize_t writ

linux程序设计学习笔记(7-15)

七.数据管理 内存管理 malloc,free,realloc和windows都一样,都是ANSI C. 实际上,应用程序并没有直接访问到物理内存,也可以通过malloc获得比实际内存大得多的内存空间,因为系统会使用交换空间(swap space ,可以理解为windows的虚拟内存),如果申请的内存大于物理内存和交换空间,那么系统将提前终止这个进程.如果在申请的内存里进程指针操作,当移动比如++出了这段分配的内存范围之外,系统会自动报警.如果向一个空指针里复制数据,会发出越界报警. 分配的内存

linux程序设计——CD唱片应用程序(第七章)

7.4 CD唱片应用程序 这篇为第七章的CD唱片应用程序,代码在CD唱片应用程序代码下载.我们使用dbm数据库对数据存储,改进之前的CD唱片应用程序. 7.4.1 更新设计 虽然在文件中以逗号分隔变量来存储信息是一种在shell中很容易实现的方式,但是这样局限性很大,因为许多CD标题和曲目都包含逗号.可以通过使用dbm数据库来改进这种方法. 将CD资料分为标题和曲目两个部分,并用不同的文件来保存它们. 前面的实现存在一个问题,即将应用程序的数据访问部分和用户接口部分混在了一起,这与程序全实现在一

linux &nbsp; 第七章 磁盘和文件系统管理(一)

linux 第七章磁盘和文件系统管理(一) 享受生活 热爱挑战 明远分享 每章一句话: 在别人光鲜的背后有着太多太多,别人不知道的痛苦,自己不喜欢的人,以微笑面对,默默地为他祝福:对于喜欢的人,真情流露,真诚相待.人在做天在看,冥冥中自有因果安排,永远保持一颗善良的心,持续做对的事情,不断地提醒自己,低调做人,高调做事. 要求:    跟着做一下吧 看不清图片就点一下图片 一,关机后添加一块20GB的SCSI磁盘,重新开机进入RHEL 5系统 二,分区并格式化 1, 使用fdisk命令对新硬盘进