• Linux文件锁的使用


            文件是一种共享资源,多个进程对同一文件进行操作的时候,必然涉及到竞争状态,因此引入了文件锁实现对共享资源的访问进行保护的机制,通过对文件上锁, 来避免访问共享资源产生竞争
    状态。

    一、文件锁的分类

            1.建议性锁

            建议性锁本质上是一种协议,程序访问文件之前,先对文件上锁, 上锁成功之后再访问文件,这是建议性锁的一种用法;但是如果你的程序不管三七二十一,在没有对文件上锁的情况下直接访问文件,也是可以访问的,并非无法访问文件。

            2.强制性锁

            如果进程对文件上了强制性锁,其它的进程在没有获取到文件锁的情况下是无法对文件进行访问的。

    Linux 系统中,可以调用 flock()、 fcntl()函数对文件上锁

    二、flock()函数进行上锁

          函数原型:flock()函数只能产生建议性锁

    1. #include
    2. int flock(int fd, int operation);

    fd: 参数 fd 为文件描述符,指定需要加锁的文件。
    operation: 参数 operation 指定了操作方式,可以设置为以下值的其中一个:
        LOCK_SH: 在 fd 引用的文件上放置一把共享锁。 所谓共享,指的便是多个进程可以拥有对同一个文件的共享锁,该共享锁可被多个进程同时拥有。
        LOCK_EX: 在 fd 引用的文件上放置一把排它锁(或叫互斥锁) 。 所谓互斥,指的便是互斥锁只能同时被一个进程所拥有。
        LOCK_UN: 解除文件锁定状态,解锁、释放锁。除了以上三个标志外,还有一个标志:
        LOCK_NB: 表示以非阻塞方式获取锁。默认情况下,调用 flock()无法获取到文件锁时会阻塞、 直到其它进程释放锁为止。可以指定 LOCK_NB 标志, 如果无法获取到锁应立刻返回(错误返回,并将 errno 设置为 EWOULDBLOCK) ,通常与 LOCK_SH 或 LOCK_EX
    一起使用,通过位或运算符组合在一起。
    返回值: 成功将返回 0;失败返回-1、并会设置 errno,

    使用实例:

            代码1

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. /*建议性锁*/
    10. static int fd; //文件描述符
    11. /* 信号处理函数 */
    12. static void sigint_handler(int sig)
    13. {
    14. if (SIGINT != sig)
    15. return;
    16. /* 解锁 */
    17. flock(fd, LOCK_UN);
    18. close(fd);
    19. printf("进程 1: 文件已解锁!\n");
    20. }
    21. int main(int argc, char *argv[])
    22. {
    23. if(argc != 2) {
    24. fprintf(stderr, "Usage: error!\n");
    25. exit(1);
    26. }
    27. fd = open(argv[1], O_WRONLY); /*只写的方式打开*/
    28. if(fd < 0) {
    29. perror("open()");
    30. exit(1);
    31. }
    32. if(flock(fd, LOCK_EX | LOCK_NB) == -1) { //以非阻塞的方式加锁
    33. perror("pid 1 lock failed");
    34. exit(1);
    35. }
    36. printf("pid 1 lock successed!\n");
    37. signal(SIGINT, sigint_handler);
    38. for(;;)
    39. sleep(1);
    40. return 0;
    41. }

    ①首先利用open函数获取文件描述符fd
    ②对文件描述符使用非阻塞的方式加一个互斥锁
    ③注册SIGINT信号处理函数:信号处理函数实现对文件进行解锁
    ④死循环进行睡眠

    代码2:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. int main(int argc, char *argv[])
    10. {
    11. char buf[100] = "Hello World!";
    12. int fd;
    13. int len;
    14. if (2 != argc) {
    15. fprintf(stderr, "usage: %s \n", argv[0]);
    16. exit(-1);
    17. }
    18. /* 打开文件 */
    19. fd = open(argv[1], O_RDWR);
    20. if (-1 == fd) {
    21. perror("open error");
    22. exit(-1);
    23. }
    24. /* 以非阻塞方式对文件加锁(排它锁) */
    25. if (-1 == flock(fd, LOCK_EX | LOCK_NB))
    26. perror("进程 2: 文件加锁失败");
    27. else
    28. printf("进程 2: 文件加锁成功!\n");
    29. /* 写文件 */
    30. len = strlen(buf);
    31. if (0 > write(fd, buf, len)) {
    32. perror("write error");
    33. exit(-1);
    34. }
    35. printf("进程 2: 写入到文件的字符串<%s>\n", buf);
    36. /* 将文件读写位置移动到文件头 */
    37. if (0 > lseek(fd, 0x0, SEEK_SET)) {
    38. perror("lseek error");
    39. exit(-1);
    40. }
    41. /* 读文件 */
    42. memset(buf, 0x0, sizeof(buf)); //清理 buf
    43. if (0 > read(fd, buf, len)) {
    44. perror("read error");
    45. exit(-1);
    46. }
    47. printf("进程 2: 从文件读取的字符串<%s>\n", buf);
    48. /* 解锁、退出 */
    49. flock(fd, LOCK_UN);
    50. close(fd);
    51. exit(0);
    52. }

    ①首先利用open函数获取文件描述符fd
    ②对文件描述符使用非阻塞的方式加一个互斥锁
    ③不管加锁成功还是失败都将buf总的内容写入该文件
    ④最后从文件中读取写入的内容,并进行打印

    运行效果:

    运行代码1

    运行代码2

    从打印信息可以看出,代码2对文件加锁失败了,是因为锁已经被代码1持有,尽管加锁失败,但是代码2对文件的读写仍然是成功的,这就是建议性锁

    对代码1发送SIGINT信号,会触发信号处理函数对文件进行解锁,再次运行代码2可以成功对文件进行加锁。 

    关于 flock()规则

    ①同一进程对文件多次加锁不会导致死锁。
    ②文件关闭的时候,会自动解锁。
    ③一个进程不可以对另一个进程持有的文件锁进行解锁。
    ④由 fork()创建的子进程不会继承父进程所创建的锁

    三、fcntl()函数进行上锁

            fcntl函数是一个多功能文件描述符管理工具箱,通过配合不同的 cmd 操作命令来实现不同的功能。

    1. #include
    2. #include
    3. int fcntl(int fd, int cmd, ... /* struct flock *flockptr */ );

    fd:要加锁的文件描述符

    cmd: F_SETLK、 F_SETLKW、 F_GETLK
    第三个参数 flockptr 是一个 struct flock 结构体指针 

    1. struct flock
    2. {
    3. short int l_type; /* Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK. */
    4. short int l_whence; /* Where `l_start' is relative to (like `lseek'). */
    5. #ifndef __USE_FILE_OFFSET64
    6. __off_t l_start; /* Offset where the lock begins. */
    7. __off_t l_len; /* Size of the locked area; zero means until EOF. */
    8. #else
    9. __off64_t l_start; /* Offset where the lock begins. */
    10. __off64_t l_len; /* Size of the locked area; zero means until EOF. */
    11. #endif
    12. __pid_t l_pid; /* Process holding the lock. */
    13. };
    14. l_type: 所希望的锁类型,可以设置为 F_RDLCK、F_WRLCK 和 F_UNLCK 三种类型之一, F_RDLCK
    15. 表示共享读锁, F_WRLCK 表示独占写锁, F_UNLCK 表示解锁一个区域。
    16. l_whence 和 l_start: 这两个变量用于指定要加锁或解锁区域的起始字节偏移量。
    17. l_len: 需要加锁或解锁区域的字节长度。
    18. l_pid: 一个 pid,指向一个进程,表示该进程持有的锁能阻塞当前进程,当 cmd=F_GETLK 时有效。
    19. 以上便是对 struct flock 结构体各成员变量的简单介绍,对于加锁和解锁区域的说明,还需要注意以下
    20. 几项规则:
    21. 锁区域可以在当前文件末尾处开始或者越过末尾处开始,但是不能在文件起始位置之前开始。
    22. 若参数 l_len 设置为 0,表示将锁区域扩大到最大范围,也就是说从锁区域的起始位置开始, 到文
    23. 件的最大偏移量处(也就是文件末尾)都处于锁区域范围内。而且是动态的, 这意味着不管向该文
    24. 件追加写了多少数据,它们都处于锁区域范围,起始位置可以是文件的任意位置。
    25. 如果我们需要对整个文件加锁,可以将 l_whence 和 l_start 设置为指向文件的起始位置, 并且指定
    26. 参数 l_len 等于 0

    测试代码: 

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main(int argc, char *argv[])
    8. {
    9. struct flock wr_lock = {0};
    10. struct flock rd_lock = {0};
    11. int fd = -1;
    12. /* 校验传参 */
    13. if (2 != argc) {
    14. fprintf(stderr, "usage: %s \n", argv[0]);
    15. exit(-1);
    16. }
    17. /* 打开文件 */
    18. fd = open(argv[1], O_RDWR);
    19. if (-1 == fd) {
    20. perror("open error");
    21. exit(-1);
    22. }
    23. /* 将文件大小截断为 1024 字节 */
    24. ftruncate(fd, 1024);
    25. /* 对 100~200 字节区间加写锁 */
    26. wr_lock.l_type = F_WRLCK;
    27. wr_lock.l_whence = SEEK_SET;
    28. wr_lock.l_start = 100;
    29. wr_lock.l_len = 100;
    30. if (-1 == fcntl(fd, F_SETLK, &wr_lock)) {
    31. perror("加写锁失败");
    32. exit(-1);
    33. }
    34. printf("加写锁成功!\n");
    35. /* 对 400~500 字节区间加读锁 */
    36. rd_lock.l_type = F_RDLCK;
    37. rd_lock.l_whence = SEEK_SET;
    38. rd_lock.l_start = 400;
    39. rd_lock.l_len = 100;
    40. if (-1 == fcntl(fd, F_SETLK, &rd_lock)) {
    41. perror("加读锁失败");
    42. exit(-1);
    43. }
    44. printf("加读锁成功!\n");
    45. /* 解锁 */
    46. wr_lock.l_type = F_UNLCK; //写锁解锁
    47. fcntl(fd, F_SETLK, &wr_lock);
    48. rd_lock.l_type = F_UNLCK; //读锁解锁
    49. fcntl(fd, F_SETLK, &rd_lock);
    50. /* 退出 */
    51. close(fd);
    52. exit(0);
    53. }

    一个进程可以对同一个文件的不同区域进行加锁,该程序对文件的 100~200 字节区间加了一个写锁,对文件的 400~500 字节区间加了一个读锁。

    读锁的共享性测试: 

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main(int argc, char *argv[])
    8. {
    9. struct flock lock = {0};
    10. int fd = -1;
    11. /* 校验传参 */
    12. if (2 != argc) {
    13. fprintf(stderr, "usage: %s \n", argv[0]);
    14. exit(-1);
    15. }
    16. /* 打开文件 */
    17. fd = open(argv[1], O_RDWR);
    18. if (-1 == fd) {
    19. perror("open error");
    20. exit(-1);
    21. }
    22. /* 将文件大小截断为 1024 字节 */
    23. ftruncate(fd, 1024);
    24. /* 对 400~500 字节区间加读锁 */
    25. lock.l_type = F_RDLCK;
    26. lock.l_whence = SEEK_SET;
    27. lock.l_start = 400;
    28. lock.l_len = 100;
    29. if (-1 == fcntl(fd, F_SETLK, &lock)) {
    30. perror("加读锁失败");
    31. exit(-1);
    32. }
    33. printf("加读锁成功!\n");
    34. for ( ; ; )
    35. sleep(1);
    36. }

    从打印信息可以发现,多个进程对同一文件的相同区域都可以加读锁,说明读锁是共享性的。

    写锁的独占性测试:

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. int main(int argc, char *argv[])
    8. {
    9. struct flock lock = {0};
    10. int fd = -1;
    11. /* 校验传参 */
    12. if (2 != argc) {
    13. fprintf(stderr, "usage: %s \n", argv[0]);
    14. exit(-1);
    15. }
    16. /* 打开文件 */
    17. fd = open(argv[1], O_RDWR);
    18. if (-1 == fd) {
    19. perror("open error");
    20. exit(-1);
    21. }
    22. /* 将文件大小截断为 1024 字节 */
    23. ftruncate(fd, 1024);
    24. /* 对 400~500 字节区间加写锁 */
    25. lock.l_type = F_WRLCK;
    26. lock.l_whence = SEEK_SET;
    27. lock.l_start = 400;
    28. lock.l_len = 100;
    29. if (-1 == fcntl(fd, F_SETLK, &lock)) {
    30. perror("加写锁失败");
    31. exit(-1);
    32. }
    33. printf("加写锁成功!\n");
    34. for ( ; ; )
    35. sleep(1);
    36. }

            由打印信息可知,但第一次启动的进程对文件加写锁之后,后面再启动进程对同一文件的相同区域加写锁发现都会失败,所以由此可知,写锁是独占性的。

    关于fcntl函数的规则:
            ①文件关闭的时候,会自动解锁。
            ②一个进程不可以对另一个进程持有的文件锁进行解锁。
            ③由 fork()创建的子进程不会继承父进程所创建的锁

    注意:

            当一个文件描述符被复制时(譬如使用 dup()、 dup2()或 fcntl()F_DUPFD 操作) ,这些通过
    复制得到的文件描述符和源文件描述符都会引用同一个文件锁, 使用这些文件描述符中的任何一个进行解锁都可以:

    例如:

    1. flock(fd, LOCK_EX); //加锁
    2. new_fd = dup(fd);
    3. flock(new_fd, LOCK_UN); //解锁
    4. lock.l_type = F_RDLCK;
    5. fcntl(fd, F_SETLK, &lock);//加锁
    6. new_fd = dup(fd);
    7. lock.l_type = F_UNLCK;
    8. fcntl(new_fd, F_SETLK, &lock);//解锁

    对于flock函数上的锁,如果不进行主动解锁的话,只有文件描述符fd和new_fd都关闭才会自动解锁。而fcntl函数上的锁,fd和new_fd任意一个关闭都会自动解锁。

  • 相关阅读:
    JS-语法-变量(声明、命名规范、一次性声明多个变量、使用)
    基于SSM的新闻网站浏览管理实现与设计
    docker:Untar exit status 1 archive/tar: invalid tar header 错误解决
    JAVA基础小结(项目三)
    golang关于channel
    jsp简单实现新闻发布系统中用户注册确认和用户模拟登录功能的开发
    安装Keras,tensorflow,并将虚拟环境添加到jupyter notebook
    2022年最新安徽建筑施工信号工(建筑特种作业)考试真题题库及答案
    GIS前端编程-Leaflet插件扩展
    纯手码优质JAVA面试八股文
  • 原文地址:https://blog.csdn.net/qq_42174306/article/details/127637931