主要参考了http://blog.csdn.net/anonymalias/article/details/9197641(anonymalias的专栏)
记录锁相当于线程同步中读写锁的一种扩展类型,可以用来对有亲缘或无亲缘关系的进程进行文件读与写的同步,通过 fcntl 函数来执行上锁操作。尽管读写锁也可以通过在共享内存区来进行进程的同步,但是fcntl记录上锁往往更容易使用,且效率更高。
记录锁和读写锁一样也有两种锁:共享读锁(F_RDLCK)和独占写锁(F_WRLCK),使用规则也基本一致:
fcntl 函数有多种用途,我们这里只讨论关于记录锁的部分,具体如下:
int fcntl(int fd, int cmd, struct flock *lock); //需 #include <fcntl.h> /* cmd = F_GETLK,测试能否建立一把锁 cmd = F_SETLK,设置锁 cmd = F_SETLKW,阻塞设置一把锁 */ //POSIX只定义fock结构中必须有以下的数据成员,具体实现可以增加 struct flock { short l_type; /* 锁的类型: F_RDLCK, F_WRLCK, F_UNLCK */ short l_whence; /* 加锁的起始位置:SEEK_SET, SEEK_CUR, SEEK_END 分别表示文件头,当前位置和文件尾*/ off_t l_start; /* 加锁的起始偏移,相对于l_whence */ off_t l_len; /* 上锁的字节数,如果为0,表示从偏移处一直到文件的末尾*/ pid_t l_pid; /* 已经占用锁的PID(只对F_GETLK 命令有效) */ /*...*/ }; //Return value: 0表示成功,-1表示失败
F_SETLK (struct flock *) Acquire a lock (when l_type is F_RDLCK or F_WRLCK) or release a lock (when l_type is F_UNLCK) on the bytes specified by the l_whence, l_start, and l_len fields of lock. If a conflicting lock is held by another process, this call returns -1 and sets errno to EACCES or EAGAIN. F_SETLKW (struct flock *) As for F_SETLK, but if a conflicting lock is held on the file, then wait for that lock to be released. If a signal is caught while waiting, then the call is interrupted and (after the sig‐ nal handler has returned) returns immediately (with return value -1 and errno set to EINTR; see signal(7)). F_GETLK (struct flock *) On input to this call, lock describes a lock we would like to place on the file. If the lock could be placed, fcntl() does not actually place it, but returns F_UNLCK in the l_type field of lock and leaves the other fields of the structure unchanged. If one or more incompatible locks would prevent this lock being placed, then fcntl() returns details about one of these locks in the l_type, l_whence, l_start, and l_len fields of lock and sets l_pid to be the PID of the process holding that lock.
注意仔细阅读上面解释!
这里需要注意的是,用F_GETLK测试能否建立一把锁,然后接着用F_SETLK或F_SETLKW企图建立一把锁,由于这两者不是一个原子操作,所以不能保证两次fcntl之间不会有另外一个进程插入并建立一把相关的锁,从而使一开始的测试情况无效。所以一般不希望上锁时阻塞,会直接通过调用F_SETLK,并对返回结果进行测试,以判断是否成功建立所要求的锁。上面所阐述的规则只适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。即如果一个进程对一个文件区间已经有了一把锁,后来该进程又试图在同一文件区间再加一把锁,那么新锁将会覆盖老锁。
//在同一进程加锁的情况下,可以继续加锁(调用程序在文章后面给出) void *thread_test(void* fd_ptr) { int fd = * (int *)fd_ptr; flock lock; lock_init(&lock, F_WRLCK, SEEK_SET, 0, 0); if(writew_lock(fd) == 0) cout << "Got it!" << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl; } int main() { char *FILE_PATH = "a.txt"; int FILE_MODE = 0664; pthread_t pid; void *retVal; int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); readw_lock(fd); cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl; pthread_create(&pid, NULL, thread_test, (void *)&fd); pthread_join(pid, &retVal); unlock(fd); return 0; }
输出为:
0 0 Got it! 0 0
下面测试不同进程下情况:
//在父同进程里加读锁,看子进程能否继续加锁(调用程序在文章后面给出) int main() { char *FILE_PATH = "a.txt"; int FILE_MODE = 0664; int retVal; int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); readw_lock(fd); if(fork() == 0) { sleep(3); cout << "I'm child." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl; exit(0); } else { cout << "I'm father." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl; } wait(&retVal); unlock(fd); return 0; }
结果如下:
I'm father. 0 0 I'm child. 11331 0
注意父进程里加的是共享读锁,此时父进程是可以加任何锁的,而子进程只能加读锁。
将父进程里的共享读锁换成独占写锁:
//在父同进程里加写锁,看子进程能否继续加锁(调用程序在文章后面给出) int main() { char *FILE_PATH = "a.txt"; int FILE_MODE = 0664; int retVal; int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); readw_lock(fd); if(fork() == 0) { sleep(3); cout << "I'm child." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl; exit(0); } else { cout << "I'm father." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl; } wait(&retVal); unlock(fd); return 0; }
I'm father. 0 0 I'm child. 11393 11393此时的父进程加的是写锁,所以子进程什么锁都不能加。
2. 锁的粒度
这里要提到两个概念:记录上锁和文件上锁。
记录上锁:对于UNIX系统而言,“记录”这一词是一种误用,因为UNIX系统内核根本没有使用文件记录这种概念,更适合的术语应该是字节范围锁,因为它锁住的只是文件的一个区域。用粒度来表示被锁住文件的字节数目。对于记录上锁,粒度最大是整个文件。
文件上锁:是记录上锁的一种特殊情况,即记录上锁的粒度是整个文件的大小。
之所以有文件上锁的概念是因为有些UNIX系统支持对整个文件上锁,但没有给文件内的字节范围上锁的能力。
3. 记录锁的隐含继承与释放
关于记录锁的继承和释放有三条规则,如下:
(1)锁与进程和文件两方面有关,体现在:
//测试进程的结束是否影响该进程加的锁(调用程序在文章后面给出) int main() { char *FILE_PATH = "a.txt"; int FILE_MODE = 0664; int retVal; int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); if(fork() == 0) { writew_lock(fd); cout << "I'm child and I have a writew_lock." << endl; sleep(3); exit(0); } else { sleep(1); cout << "I'm father." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl; wait(&retVal); cout << "My child is over." << endl; cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl; } unlock(fd); return 0; }
I'm child and I have a writew_lock. I'm father. 12149 12149 My child is over. 0 0说明在子进程结束后,它所建立的锁失效。
//测试fd对锁的影响 #include <iostream> #include <fcntl.h> #include <pthread.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/shm.h> using namespace std; struct share_data{ pthread_cond_t cond; pthread_mutex_t mutex; pthread_mutexattr_t mutexAttr; pthread_condattr_t condAttr; }; int main() { char *FILE_PATH = "a.txt"; int FILE_MODE = 0664; int retVal; int shmid; struct share_data *shm; shmid = shmget(IPC_PRIVATE, sizeof(struct share_data), 0644 | IPC_CREAT); int fd_1 = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); int fd_2 = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE); if(fork() == 0) { shm = (struct share_data *) shmat(shmid, 0, 0); pthread_mutexattr_init(&(shm->mutexAttr)); pthread_mutexattr_setpshared(&(shm->mutexAttr), PTHREAD_PROCESS_SHARED); pthread_mutex_init(&(shm->mutex), &(shm->mutexAttr)); pthread_condattr_init(&(shm->condAttr)); pthread_condattr_setpshared(&(shm->condAttr), PTHREAD_PROCESS_SHARED); pthread_cond_init(&(shm->cond), &(shm->condAttr)); writew_lock(fd_1); cout << "I'm child and I have a writew_lock." << endl; sleep(5); close(fd_1); pthread_cond_signal(&(shm->cond)); sleep(3); exit(0); } else { shm = (struct share_data *) shmat(shmid, 0, 0); pthread_mutex_lock(&(shm->mutex)); sleep(1); cout << "I'm father." << endl; cout<<lock_test(fd_2, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd_2, F_RDLCK, SEEK_SET, 0, 0)<<endl; cout << "I'm waiting." << endl; pthread_cond_wait(&(shm->cond), &(shm->mutex)); cout << "My son's fd_1 is over." << endl; cout<<lock_test(fd_2, F_WRLCK, SEEK_SET, 0, 0)<<endl; cout<<lock_test(fd_2, F_RDLCK, SEEK_SET, 0, 0)<<endl; } wait(&retVal); return 0; }
结果如下:
I'm child and I have a writew_lock. I'm father. 17460 17460 I'm waiting. My son's fd_1 is over. 0 0证明了当关闭一个文件描述符时,则进程通过该文件描述符引用的该文件上的任何一把锁都将被释放。
(2)由fork产生的子进程不继承父进程所设置的锁。即对于父进程建立的锁而言,子进程被视为另一个进程。记录锁本身就是用来同步不同进程对同一文件区进行操作,如果子进程继承了父进程的锁,那么父子进程就可以同时对同一文件区进行操作,这有违记录锁的规则,所以存在这么一条规则。之前的代码已经证明了这点。
3)执行exec后,新程序可以继承原执行程序的锁。但是,如果一个文件描述符设置了close-on-exec标志,在执行exec时,会关闭该文件描述符,所以对应的锁也就被释放了,也就无所谓继承了。
int main() { int fd = open("./a.txt", O_RDWR | O_CREAT, 0664); readw_lock(fd); //child 1 if (fork() == 0) { cout<<"child 1 try to get write lock..."<<endl; writew_lock(fd); cout<<"child 1 get write lock..."<<endl; unlock(fd); cout<<"child 1 release write lock..."<<endl; exit(0); } //child 2 if (fork() == 0) { sleep(3); cout<<"child 2 try to get read lock..."<<endl; readw_lock(fd); cout<<"child 2 get read lock..."<<endl; unlock(fd); cout<<"child 2 release read lock..."<<endl; exit(0); } sleep(10); unlock(fd); return 0; }
child 1 try to get write lock... child 2 try to get read lock... child 2 get read lock... child 2 release read lock... child 1 get write lock... child 1 release write lock...
int main() { int fd = open("./a.txt", O_RDWR | O_CREAT, 0664); writew_lock(fd); int retVal; //child 1 if (fork() == 0) { cout<<"child 1 try to get write lock..."<<endl; writew_lock(fd); cout<<"child 1 get write lock..."<<endl; unlock(fd); cout<<"child 1 release write lock..."<<endl; exit(0); } //child 2 if (fork() == 0) { sleep(3); cout<<"child 2 try to get read lock..."<<endl; readw_lock(fd); cout<<"child 2 get read lock..."<<endl; unlock(fd); cout<<"child 2 release read lock..."<<endl; exit(0); } sleep(10); unlock(fd); return 0; }
child 1 try to get write lock... child 2 try to get read lock... child 2 get read lock... child 2 release read lock... child 1 get write lock... child 1 release write lock...结果表明还是读锁的优先级高啊。 与参考博文的结果FIFO不同。。。先这么着吧
int main() { int fd = open("./a.txt", O_RDWR | O_CREAT, 0664); writew_lock(fd); //child if (fork() == 0) { cout<<"child try to get write lock..."<<endl; writew_lock(fd); cout<<"child get write lock..."<<endl; unlock(fd); cout<<"child release write lock..."<<endl; exit(0); } sleep(5); close(fd); sleep(5); return 0; }结果:
child try to get write lock... child get write lock... child release write lock...其中1,2两句间,3句与结束之间有较长时间等待。表明就是sleep()函数的原因。当父进程里的fd回收后,子进程才拿到写锁。