底层I/O之文件锁

概述

当同一个文件被多进程同时打开,操作时,为防止一致性的问题,需要使用文件锁。Unix系统使用fcntl函数来操作文件锁。文件锁与线程锁不同,线程锁是把文件作为临界资源,防止多个线程同时打开一个文件,文件锁是防止多个并行进程同时访问同一个文件的同一块内容,《linux程序设计》中将其称为区域锁。

如果一个数据块被加了读锁(read lock),那其他进程不能再对该块加写锁(write lock),但是与读有关的锁操作可以进行,所以读锁也称为共享锁。而一个数据块被加了写锁(write lock),那其他进程不能再对该块加任何锁,写锁也称为独占锁。注意这里说的是锁之间的互斥关系,而不是读写操作的互斥关系,为什么说的这么拗口呢,这是因为Unix关于读写操作的函数并不去检查文件块是否加锁,文件锁只能限制锁操作而不限制读写操作,所以为了达到保证文件内容一致性的目的,对于文件内的临界数据,在读写之前要用锁操作去限制后面的读写函数的调用,也就是说加锁和检测锁是程序猿的工作:(。比如写一个临界数据,先用锁操作检查有没有锁(因为写操作本身不检查锁),如果有锁就不写。而且使用了文件锁后就必须用内核态的读写函数read , write 而不能用fread , fwrite,因为fread属于buffered I/O 会在用户态内存中缓存数据,影响锁的正确使用。

一个进程一次对一个文件的某一个字节内容只能加一种类型的锁,锁随进程退出而被释放,并且不会被其子进程继承。

锁操作

文件锁操作通过函数:fcntl (fd, cmd, lock_p)来完成。

struct flock

其中第三个参数lock_p是描述文件锁的 , lock_p结构类型为struct flock,定义在fcntl.h中。

1
2
3
4
5
6
7
8
9

struct flock{
short int l_type;
short int l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;

}

short int l_type: 指明锁类型,值为:F_RDLCK(读锁), F_WRLCK(写锁), F_UNLCK(释放锁)。

short int l_whence: 文件指针从何处开始,类似seek函数,值为:SEEK_SET, SEEK_CUR, SEEK_END

off_t l_start: 指明文件块的起始偏移地址(相对于l_whence)。

off_t l_len: 指明文件块大小。0表示从l_start到文件结尾。

pid_t l_pid: 指明拥有锁的进程ID。当cmdF_GETLK时会返回拥有锁的进程号。

Macro

fcntl (fd, cmd, lock_p)第二个参数cmd是一个宏定义,代表具体的锁操作,值为:F_GETLKF_SETLKF_SETLKW

int F_GETLK: 获取指定文件区域的锁信息,调用形式为fcntl(fd, F_GETLK, lock_p),如果该区域存在与要检查类型冲突的锁,则该锁信息会被更新到lock_p中。例如你想测试某一块区域能否加一个F_RDLCK,若该区域存在一个F_RDLCK锁,此时加锁是可行的,因此fcntl调用返回非-1,表示可加锁,且lock_p不会被更新,若你想加的是F_WRLCK,那fcntl返回-1,表示不能加锁,lock_p的信息被更新为与F_WRLCK相冲突的那个锁的信息(五个字段全部被更新)。如果该区域没有锁,则l_type被更新为F_UNLCK。根据上面的描述,F_WRLCK可以检测读锁和写锁,F_RDLCK只能检测写锁。

其他的错误信息,在errno中,其值为:

EBADF:参数fd不正确。

EINVAL:参数lock_p不正确或者文件不支持锁。

int F_SETLK:用来进行加锁或解锁(解锁时l_typeF_UNLCK)操作,调用形式为fcntl(fd, F_SETLK, lock_p)。如果有上述类型的锁冲突,加锁会失败,函数会立即返回-1。进程只能解自己的锁,在读写操作结束后,要有释放锁的习惯。

errno信息为:

EAGAIN 或者 EACCES:有其他的锁阻碍加锁操作

EBADF: 参数fd不正确。

EINVAL:参数lock_p不正确或者文件不支持锁

ENOLCK:锁资源不足。

int F_SETLKW:该命令与F_SETLK功能一样,只不过在加锁失败时不是立即返回,而是阻塞等待冲突的锁被释放并且加锁成功。

errno信息为:

EINTR:阻塞过程被中断。

EDEADLK:出现死锁。

例子

请参考《linux程序设计(第四版)》P228。