zoukankan      html  css  js  c++  java
  • Linux进程同步之记录锁(fcntl)

    记录锁相当于线程同步中读写锁的一种扩展类型,可以用来对有亲缘或无亲缘关系的进程进行文件读与写的同步,通过fcntl函数来执行上锁操作。尽管读写锁也可以通过在共享内存区来进行进程的同步,但是fcntl记录上锁往往更容易使用,且效率更高。

    记录锁的功能:当一个进程正在读或修改文件的某个部分是,它可以阻止其他进程修改同一文件区。对于这个功能阐述我认为有三点要解释的:

    • 记录锁不仅仅可以用来同步不同进程对同一文件的操作,还可以通过对同一文件加记录锁,来同步不同进程对某一共享资源的访问,如共享内存,I/O设备。
    • 对于劝告性上锁(POSIX定义的记录锁即是这种类型的锁),当一个进程通过上锁对文件进行操作时,它不能阻止另一个非协作进程对该文件的修改。
    • 即使是强制性上锁(有些系统定义),也不能完全保证该文件不会被另一个进程修改。因为强制性锁对unlink函数没有影响,所以一个进程可以先删除该文件,然后再将修改后的内容保存为同一文件来实现修改。具体可参考《APUEP367 。

    1记录锁函数接口

    记录上锁的POSIX接口函数fcntl如下:

    1. /* Do the file control operation described by CMD on FD. 
    2.    The remaining arguments are interpreted depending on CMD. */  
    3. int fcntl (int __fd, int __cmd, ...);  
    4.   
    5. //根据cmd的不同有以下三种类型的调用  
    6. int fcntl(int fd, int cmd);  
    7. int fcntl(int fd, int cmd, long arg);  
    8. int fcntl(int fd, int cmd, struct flock *lock);  

    由函数名称可知fcntl的功能是对文件的控制操作,根据传入不同的操作类型命令cmdfcntl会执行不同的操作,fcnt根据cmd不同,接收可变的参数。具体有以下五种类型的操作:

    1. /* 
    2. cmd = F_DUPFD,复制一个文件描述符; 
    3. */  
    4. int fcntl(int fd, int cmd);  
    5.   
    6. /* 
    7. cmd = F_GETFD,获得文件描述符标志; 
    8. cmd = F_SETFD,设置文件描述符标志;arg = 描述符标志的值,目前只定义了一个标志: FD_CLOEXEC 
    9. int fcntl(int fd, int cmd); 
    10. int fcntl(int fd, int cmd, long arg); 
    11. */  
    12.   
    13. /* 
    14. cmd = F_GETFL,获得文件状态标志; 
    15. cmd = F_SETFL,设置文件状态标志;arg = 状态标志的值 
    16. int fcntl(int fd, int cmd); 
    17. int fcntl(int fd, int cmd, long arg); 
    18. */  
    19.   
    20. /* 
    21. cmd = F_GETOWN,获得当前接收SIGIO和SIGURG信号的进程ID或进程组ID 
    22. cmd = F_SETOWN,设置接收SIGIO和SIGURG信号的进程ID或进程组ID;arg = 进程ID或进程组ID 
    23. int fcntl(int fd, int cmd); 
    24. int fcntl(int fd, int cmd, long arg); 
    25. */  
    26.   
    27. /* 
    28. Return value: 
    29.  
    30. 对于成功的调用,根据操作类型cmd不同,有以下几种情况: 
    31.        F_DUPFD  返回新的文件描述符 
    32.        F_GETFD  返回文件描述符标志 
    33.        F_GETFL  返回文件状态标志 
    34.        F_GETOWN 进程ID或进程组ID 
    35.        All other commands  返回0 
    36. 调用失败, 返回-1,并设置errno。 
    37. */  

    上面四个功能都是fcntl提供的很常用的操作,关于记录锁的功能就是fcntl提供的第五个功能,具体使用如下:

    1. int fcntl(int fd, int cmd, struct flock *lock);  
    2.   
    3. /* 
    4. cmd = F_GETLK,测试能否建立一把锁 
    5. cmd = F_SETLK,设置锁 
    6. cmd = F_SETLKW, 阻塞设置一把锁 
    7.  
    8. */  
    9. //POSIX只定义fock结构中必须有以下的数据成员,具体实现可以增加  
    10. struct flock {  
    11.       short l_type;    /* 锁的类型: F_RDLCK, F_WRLCK, F_UNLCK */  
    12.       short l_whence;  /* 加锁的起始位置:SEEK_SET, SEEK_CUR, SEEK_END */  
    13.       off_t l_start;   /* 加锁的起始偏移,相对于l_whence */  
    14.       off_t l_len;     /* 上锁的字节数,如果为0,表示从偏移处一直到文件的末尾*/  
    15.       pid_t l_pid;     /* 已经占用锁的PID(只对F_GETLK 命令有效) */  
    16.       /*...*/  
    17. };  
    18. //Return value: 前面已经说明;  

    F_SETLK:获取(l_typeF_RDLCKF_WRLCK)或释放由lock指向flock结构所描述的锁,如果无法获取锁时,该函数会立即返回一个EACCESSEAGAIN错误,而不会阻塞。

    F_SETLKW:F_SETLKWF_SETLK的区别是,无法设置锁的时候,调用线程会阻塞到该锁能够授权位置。

    F_GETLK:F_GETLK主要用来检测是否有某个已存在锁会妨碍将新锁授予调用进程,如果没有这样的锁,lock所指向的flock结构的l_type成员就会被置成F_UNLCK,否则已存在的锁的信息将会写入lock所指向的flock结构中

    这里需要注意的是,用F_GETLK测试能否建立一把锁,然后接着用F_SETLKF_SETLKW企图建立一把锁,由于这两者不是一个原子操作,所以不能保证两次fcntl之间不会有另外一个进程插入并建立一把相关的锁,从而使一开始的测试情况无效。所以一般不希望上锁时阻塞,会直接通过调用F_SETLK,并对返回结果进行测试,以判断是否成功建立所要求的锁。

    2记录锁规则说明

    前面我们说了记录锁相当于读写锁的一种扩展类型,记录锁和读写锁一样也有两种锁:共享读锁(F_RDLCK)和独占写锁(F_WRLCK)。在使用规则上和读写锁也基本一样:

    • 文件给定字节区间,多个进程可以有一把共享读锁,即允许多个进程以读模式访问该字节区;
    • 文件给定字节区间,只能有一个进程有一把独占写锁,即只允许有一个进程已写模式访问该字节区;
    • 文件给定字节区间,如果有一把或多把读锁,不能在该字节区再加写锁,同样,如果有一把写锁,不能再该字节区再加任何读写锁。

    如下表所示:


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

    下面进行测试;第一个程序是在同一进程中测试能否在加写锁后,继续加读写锁。第二个程序是在在父进程中加写锁后,然后再子进程中测试能否继续加读写锁。

    1. //调用的函数,在文章末尾贴出  
    2. int main()  
    3. {    
    4.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    5.     writew_lock(fd);  
    6.   
    7.     cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl;  
    8.     cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;  
    9.   
    10.     unlock(fd);  
    11.   
    12.     return 0;  
    13. }  

    执行结果为:

    1. 0  
    2. 0  

    表明同一进程可以对已加锁的同一文件区间,仍然能获得加锁权限;

    1. //调用的函数,在文章末尾贴出  
    2. int main()  
    3. {  
    4.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    5.     writew_lock(fd);  
    6.   
    7.     if (fork() == 0)  
    8.     {  
    9.         cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl;  
    10.         cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;  
    11.   
    12.         exit(0);  
    13.     }  
    14.   
    15.     sleep(3);  
    16.     unlock(fd);  
    17.   
    18.     return 0;  
    19. }  

    执行结果为:

    1. 24791  
    2. 24791  

    表明不同进程不能对已加写锁的同一文件区间,获得加锁权限;

    还有就是:加锁时,该进程必须对该文件有相应的文件访问权限,即加读锁,该文件必须是读打开,加写锁时,该文件必须是写打开。

    3记录锁的粒度

    这里要提到两个概念:记录上锁和文件上锁

    记录上锁:对于UNIX系统而言,“记录”这一词是一种误用,因为UNIX系统内核根本没有使用文件记录这种概念,更适合的术语应该是字节范围锁,因为它锁住的只是文件的一个区域。用粒度来表示被锁住文件的字节数目。对于记录上锁,粒度最大是整个文件。

    文件上锁:是记录上锁的一种特殊情况,即记录上锁的粒度是整个文件的大小。

    之所以有文件上锁的概念是因为有些UNIX系统支持对整个文件上锁,但没有给文件内的字节范围上锁的能力。

    4记录锁的隐含继承与释放

    关于记录锁的继承和释放有三条规则,如下:

    1)锁与进程和文件两方面有关,体现在:

    • 当一个进程终止时,它所建立的记录锁将全部释放;
    • 当关闭一个文件描述符时,则进程通过该文件描述符引用的该文件上的任何一把锁都将被释放。

    对于第一个方面,可以建立如下测试代码:

    1. //调用的函数,在文章末尾贴出  
    2.   
    3. //process 1  
    4. int main()  
    5. {  
    6.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    7.    
    8.     writew_lock(fd);  
    9.     cout<<"process 1 get write lock..."<<endl;  
    10.       
    11.     sleep(10);  
    12.   
    13.     cout<<"process 1 exit..."<<endl;  
    14.     return 0;  
    15. }  
    16.   
    17. //process 2  
    18. int main()  
    19. {  
    20.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    21.   
    22.     writew_lock(fd);  
    23.     cout<<"process 2 get write lock..."<<endl;  
    24.     unlock(fd);  
    25.   
    26.     return 0;  
    27. }  

    先启动进程1,然后立即启动进程2,执行结果如下:

    1. process 1 get write lock...  
    2. process 1 exit...  
    3. process 2 get write lock...  

    对于第二个方面,可以进行如下测试:

    1. //调用的函数,在文章末尾贴出  
    2. int main()  
    3. {  
    4.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    5.   
    6.     if (fork() == 0)  
    7.     {  
    8.         int fd_1 = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    9.   
    10.         readw_lock(fd_1);  
    11.         cout<<"child get read lock..."<<endl;  
    12.   
    13.         sleep(3);  
    14.   
    15.         close(fd_1);  
    16.         cout<<"close the file descriptor..."<<endl;  
    17.   
    18.         pause();  
    19.     }  
    20.   
    21.     sleep(1);  
    22.   
    23.     writew_lock(fd);  
    24.     cout<<"parent get write lock..."<<endl;  
    25.     unlock(fd);  
    26.   
    27.     return 0;  
    28. }  

    程序的执行结果如下:

    1. child get read lock...  
    2. close the file descriptor...  
    3. parent get write lock...  

    可见,当关闭文件描述符时,与该文件描述符有关的锁都被释放,同样通过dup拷贝得到的文件描述符也会导致这种情况;

    (2)由fork产生的子进程不继承父进程所设置的锁。即对于父进程建立的锁而言,子进程被视为另一个进程。记录锁本身就是用来同步不同进程对同一文件区进行操作,如果子进程继承了父进程的锁,那么父子进程就可以同时对同一文件区进行操作,这有违记录锁的规则,所以存在这么一条规则。

    下面是测试代码(上面已经用过该代码进行测试):

    1. //调用的函数,在文章末尾贴出  
    2. int main()  
    3. {  
    4.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    5.     writew_lock(fd);  
    6.   
    7.     if (fork() == 0)  
    8.     {  
    9.         cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl;  
    10.         cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;  
    11.   
    12.         exit(0);  
    13.     }  
    14.   
    15.     sleep(3);  
    16.     unlock(fd);  
    17.   
    18.     return 0;  
    19.   
    20. }  

    我们知道在前面已经说过,同一个进程可以重复对同一个文件区间加锁,后加的锁将覆盖前面加的锁。那么再假设如果子进程继承了父进程的锁,那么子进程可以对该锁进行覆盖,那么在子进程内对该锁是否能获得权限的测试应该是可以,但测试结果为:

    1. 24791  
    2. 24791  

    表明已经进程24791已经占用该锁,所以假设不成立,子进程不会继承父进程的锁;

    (3)执行exec后,新程序可以继承原执行程序的锁。但是,如果一个文件描述符设置了close-on-exec标志,在执行exec时,会关闭该文件描述符,所以对应的锁也就被释放了,也就无所谓继承了。

    5记录锁的读和写的优先级

    在读写锁中,我曾经测试过Linux 2.6.18中提供的读写锁函数是优先考虑等待读模式占用锁的线程,这种实现的一个很大缺陷就是出现写入线程饿死的情况 那么在记录锁中是什么样的规则呢,需要说明的是这在POSIX标准中是没有说明的,要看具体实现。

    具体进行以下2个方面测试:

    1. 进程拥有读出锁,然后写入锁等待期间额外的读出锁处理;
    2. 进程拥有写入锁,那么等待的写入锁和等待的读出锁的优先级;

    测试1父进程获得对文件的读锁,然后子进程1请求加写锁,随即进入睡眠,然后子进程2请求读锁,看进程2是否能够获得读锁。

    1. //调用的函数,在文章末尾贴出  
    2. int main()  
    3. {  
    4.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    5.     readw_lock(fd);  
    6.   
    7.     //child  1  
    8.     if (fork() == 0)  
    9.     {  
    10.         cout<<"child 1 try to get write lock..."<<endl;  
    11.         writew_lock(fd);  
    12.         cout<<"child 1 get write lock..."<<endl;  
    13.   
    14.         unlock(fd);  
    15.         cout<<"child 1 release write lock..."<<endl;  
    16.   
    17.         exit(0);  
    18.     }  
    19.   
    20.     //child 2  
    21.     if (fork() == 0)  
    22.     {  
    23.         sleep(3);  
    24.   
    25.         cout<<"child 2 try to get read lock..."<<endl;  
    26.         readw_lock(fd);  
    27.         cout<<"child 2 get read lock..."<<endl;  
    28.   
    29.         unlock(fd);  
    30.         cout<<"child 2 release read lock..."<<endl;  
    31.         exit(0);  
    32.     }  
    33.   
    34.     sleep(10);  
    35.     unlock(fd);  
    36.   
    37.     return 0;  
    38. }  

    Linux 2.6.18下执行结果如下:

    1. child 1 try to get write lock...  
    2. child 2 try to get read lock...  
    3. child 2 get read lock...  
    4. child 2 release read lock...  
    5. child 1 get write lock...  
    6. child 1 release write lock...  

    可知在有写入进程等待的情况下,对于读出进程的请求,系统会一直给予的。那么这也就可能导致写入进程饿死的局面。

    测试2父进程获得写入锁,然后子进程1和子进程2分别请求获得写入锁和读写锁,看两者的响应顺序;

    1. //调用的函数,在文章末尾贴出  
    2. int main()  
    3. {   
    4.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    5.     writew_lock(fd);  
    6.   
    7.     //child  1  
    8.     if (fork() == 0)  
    9.     {  
    10.         sleep(3);  
    11.   
    12.         cout<<"child 1 try to get write lock..."<<endl;  
    13.         writew_lock(fd);  
    14.         cout<<"child 1 get write lock..."<<endl;  
    15.   
    16.         unlock(fd);  
    17.         cout<<"child 1 release write lock..."<<endl;  
    18.   
    19.         exit(0);  
    20.     }  
    21.   
    22.     //child 2  
    23.     if (fork() == 0)  
    24.     {  
    25.         cout<<"child 2 try to get read lock..."<<endl;  
    26.         readw_lock(fd);  
    27.         cout<<"child 2 get read lock..."<<endl;  
    28.   
    29.         unlock(fd);  
    30.         cout<<"child 2 release read lock..."<<endl;  
    31.   
    32.         exit(0);  
    33.     }  
    34.   
    35.     sleep(10);  
    36.     unlock(fd);  
    37.   
    38.     return 0;  
    39. }  

    Linux 2.6.18下执行结果:

    1. child 2 try to get read lock...  
    2. child 1 try to get write lock...  
    3. child 2 get read lock...  
    4. child 2 release read lock...  
    5. child 1 get write lock...  
    6. child 1 release write lock...  

    将上面代码该成child2 sleep 3schild1sleep

    1. //调用的函数,在文章末尾贴出  
    2. int main()  
    3. {   
    4.     int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);  
    5.     writew_lock(fd);  
    6.   
    7.     //child  1  
    8.     if (fork() == 0)  
    9.     {  
    10.         cout<<"child 1 try to get write lock..."<<endl;  
    11.         writew_lock(fd);  
    12.         cout<<"child 1 get write lock..."<<endl;  
    13.   
    14.         unlock(fd);  
    15.         cout<<"child 1 release write lock..."<<endl;  
    16.   
    17.         exit(0);  
    18.     }  
    19.   
    20.     //child 2  
    21.     if (fork() == 0)  
    22.     {  
    23.         sleep(3);  
    24.   
    25.         cout<<"child 2 try to get read lock..."<<endl;  
    26.         readw_lock(fd);  
    27.         cout<<"child 2 get read lock..."<<endl;  
    28.   
    29.         unlock(fd);  
    30.         cout<<"child 2 release read lock..."<<endl;  
    31.   
    32.         exit(0);  
    33.     }  
    34.   
    35.     sleep(10);  
    36.     unlock(fd);  
    37.   
    38.     return 0;  
    39. }  

    Linux 2.6.18下执行结果如下:

    1. child 1 try to get write lock...  
    2. child 2 try to get read lock...  
    3. child 1 get write lock...  
    4. child 1 release write lock...  
    5. child 2 get read lock...  
    6. child 2 release read lock...  

    由上可知在Linux 2.6.18下,等待的写入锁进程和读出锁进程的优先级由FIFO的请求顺序进程响应。

    6记录锁的使用封装

    1. void lock_init(flock *lock, short type, short whence, off_t start, off_t len)  
    2. {  
    3.     if (lock == NULL)  
    4.         return;  
    5.   
    6.     lock->l_type = type;  
    7.     lock->l_whence = whence;  
    8.     lock->l_start = start;  
    9.     lock->l_len = len;  
    10. }  
    11.   
    12. int readw_lock(int fd)  
    13. {  
    14.     if (fd < 0)  
    15.     {  
    16.         return -1;  
    17.     }  
    18.   
    19.     struct flock lock;  
    20.     lock_init(&lock, F_RDLCK, SEEK_SET, 0, 0);  
    21.   
    22.     if (fcntl(fd, F_SETLKW, &lock) != 0)  
    23.     {  
    24.         return -1;  
    25.     }  
    26.       
    27.     return 0;  
    28. }  
    29.   
    30. int writew_lock(int fd)  
    31. {  
    32.     if (fd < 0)  
    33.     {  
    34.         return -1;  
    35.     }  
    36.   
    37.     struct flock lock;  
    38.     lock_init(&lock, F_WRLCK, SEEK_SET, 0, 0);  
    39.   
    40.     if (fcntl(fd, F_SETLKW, &lock) != 0)  
    41.     {  
    42.         return -1;  
    43.     }  
    44.   
    45.     return 0;  
    46. }  
    47.   
    48. int unlock(int fd)  
    49. {  
    50.     if (fd < 0)  
    51.     {  
    52.         return -1;  
    53.     }  
    54.   
    55.     struct flock lock;  
    56.     lock_init(&lock, F_UNLCK, SEEK_SET, 0, 0);  
    57.   
    58.     if (fcntl(fd, F_SETLKW, &lock) != 0)  
    59.     {  
    60.         return -1;  
    61.     }  
    62.   
    63.     return 0;  
    64. }  
    65.   
    66. pid_t lock_test(int fd, short type, short whence, off_t start, off_t len)  
    67. {  
    68.     flock lock;  
    69.     lock_init(&lock, type, whence, start, len);  
    70.   
    71.     if (fcntl(fd, F_GETLK, &lock) == -1)  
    72.     {  
    73.         return -1;  
    74.     }  
    75.   
    76.     if(lock.l_type == F_UNLCK)  
    77.         return 0;  
    78.     return lock.l_pid;  
    79. }  

    Jun 28, 2013 PM16:06 @lab  困呀。。。


    原文:http://blog.csdn.net/anonymalias/article/details/9197641

  • 相关阅读:
    firefox打开链接自动跳转至新页面设置
    sql server 查询不为空的字段
    C# 判断ip地址是否正确
    Win7自带功能,刻录光盘遇到的问题
    【一起学源码-微服务】Nexflix Eureka 源码六:在眼花缭乱的代码中,EurekaClient是如何注册的?
    【一起学源码-微服务】Nexflix Eureka 源码五:EurekaClient启动要经历哪些艰难险阻?
    【一起学源码-微服务】Nexflix Eureka 源码四:EurekaServer启动之完成上下文构建及EurekaServer总结
    【一起学源码-微服务】Nexflix Eureka 源码三:EurekaServer启动之EurekaServer上下文EurekaClient创建
    【一起学源码-微服务】Nexflix Eureka 源码二:EurekaServer启动之配置文件加载以及面向接口的配置项读取
    【一起学源码-微服务】Netflix Eureka 源码一:Netflix Eureka 源码初探,我们为什么要读源码?
  • 原文地址:https://www.cnblogs.com/Zoran-/p/5819274.html
Copyright © 2011-2022 走看看