【Linux多进程同步】记录锁

主要参考了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,并对返回结果进行测试,以判断是否成功建立所要求的锁。

1. 记录锁只能用于进程间同步

上面所阐述的规则只适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求即如果一个进程对一个文件区间已经有了一把锁,后来该进程又试图在同一文件区间再加一把锁,那么新锁将会覆盖老锁。

//在同一进程加锁的情况下,可以继续加锁(调用程序在文章后面给出)

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;
}

这个例子较复杂一些,除了记录锁,还有互斥锁和条件变量。互斥锁和条件变量的目的是为了在子进程close(fd_1)之后再唤醒主进程,看看当子进程用 fd_1 加过锁再释放掉 fd_1 后对父进程的影响。

结果如下:

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时,会关闭该文件描述符,所以对应的锁也就被释放了,也就无所谓继承了。



4. 记录锁的读和写的优先级


我们知道,读写锁函数是优先考虑等待读模式占用锁的线程,这种实现的一个很大缺陷就是出现写入线程饿死的情况。那么对于记录锁呢,具体进行以下2个方面测试:
  1. 进程拥有读出锁,然后写入锁等待期间额外的读出锁处理;
  2. 进程拥有写入锁,那么等待的写入锁和等待的读出锁的优先级;
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...

此处是利用到了writew_lock() ,readw_lock()以及unlock()函数里面 F_SETLCKW 参数,该参数使得当前进程不能获得锁时会陷入阻塞,等待直到可以获得锁(返回0)或者收到某种信号(返回-1)。
值得注意的是子进程1获得写锁是在子进程2释放锁后还过了较长时间,明显是在父进程unlock(fd)之后。
可知在有写入进程等待的情况下,对于读出进程的请求,系统会先满足读进程(即是)。那么这也就可能导致写入进程饿死的局面。

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不同。。。先这么着吧

最后要说的是文件描述符fd在fork()之后的子进程里是与父进程里的fd虽然指向同一个文件,但已经是两个不同的fd了。见下面的例子:
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回收后,子进程才拿到写锁。

你可能感兴趣的:(【Linux多进程同步】记录锁)