• inux c编程:记录锁


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

    记录锁不仅仅可以用来同步不同进程对同一文件的操作,还可以通过对同一文件加记录锁,来同步不同进程对某一共享资源的访问,如共享内存,I/O设备。

    对于劝告性上锁,当一个进程通过上锁对文件进行操作时,它不能阻止另一个非协作进程对该文件的修改。

    即使是强制性上锁,也不能完全保证该文件不会被另一个进程修改。因为强制性锁对unlink函数没有影响,所以一个进程可以先删除该文件,然后再将修改后的内容保存为同一文件来实现修改。 

    1记录锁函数接口

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

    /* Do the file control operation described by CMD on FD.

       The remaining arguments are interpreted depending on CMD. */

    int fcntl (int __fd, int __cmd, ...);

     

    //根据cmd的不同有以下三种类型的调用

    int fcntl(int fd, int cmd);

    int fcntl(int fd, int cmd, long arg);

    int fcntl(int fd, int cmd, struct flock *lock);

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

    /*

    cmd = F_DUPFD,复制一个文件描述符;

    */

    int fcntl(int fd, int cmd);

     

    /*

    cmd = F_GETFD,获得文件描述符标志;

    cmd = F_SETFD,设置文件描述符标志;arg = 描述符标志的值,目前只定义了一个标志: FD_CLOEXEC

    int fcntl(int fd, int cmd);

    int fcntl(int fd, int cmd, long arg);

    */

     

    /*

    cmd = F_GETFL,获得文件状态标志;

    cmd = F_SETFL,设置文件状态标志;arg = 状态标志的值

    int fcntl(int fd, int cmd);

    int fcntl(int fd, int cmd, long arg);

    */

     

    /*

    cmd = F_GETOWN,获得当前接收SIGIOSIGURG信号的进程ID或进程组ID

    cmd = F_SETOWN,设置接收SIGIOSIGURG信号的进程ID或进程组IDarg = 进程ID或进程组ID

    int fcntl(int fd, int cmd);

    int fcntl(int fd, int cmd, long arg);

    */

     

    /*

    Return value:

     

    对于成功的调用,根据操作类型cmd不同,有以下几种情况:

           F_DUPFD  返回新的文件描述符

           F_GETFD  返回文件描述符标志

           F_GETFL  返回文件状态标志

           F_GETOWN 进程ID或进程组ID

           All other commands  返回0

    调用失败, 返回-1,并设置errno

    */

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

    int fcntl(int fd, int cmd, struct flock *lock);

     

    /*

    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;     /* 上锁的字节数*/

          pid_t l_pid;     /* 已经占用锁的PID(只对F_GETLK 命令有效) */

          /*...*/

    };

    //Return value: 前面已经说明;

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

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

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

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

     

    2记录锁规则说明

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

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

    如下表所示:

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

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

    void lock_init(struct flock *lock, short type, short whence, off_t start, off_t len)

    {

        if (lock == NULL)

            return;

        lock->l_type = type;

        lock->l_whence = whence;

        lock->l_start = start;

        lock->l_len = len;

    }

     

    int writew_lock(int fd)

    {

        struct flock lock;

        if (fd < 0)

        {

            return -1;

        }

        lock_init(&lock, F_WRLCK, SEEK_SET, 0, 0);

     

        if (fcntl(fd, F_SETLKW, &lock) != 0)

        {

            return -1;

        }

     

        return 0;

    }

     

    int main()

    {

        char *path="home/zhf/test1.txt";

        int fd;

        pid_t t,t1;

        fd = fopen(path, "r");

        writew_lock(fd);

        t=lock_test(fd, F_WRLCK, SEEK_SET, 0, 0);

        t1=lock_test(fd, F_RDLCK, SEEK_SET, 0, 0);

        printf("the pid_t is %d ",t);

        printf("the pid_t is %d ",t1);

        unlock(fd);

        return 0;

    }

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

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

    int main()

    {

        int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);

        writew_lock(fd);

        if (fork() == 0)

        {

            cout<<lock_test(fd, F_WRLCK, SEEK_SET, 0, 0)<<endl;

            cout<<lock_test(fd, F_RDLCK, SEEK_SET, 0, 0)<<endl;

     

            exit(0);

        }

        sleep(3);

        unlock(fd);

     

        return 0;

    }

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

    24791

    24791

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

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

    int main()

    {

        int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);

        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;

    }

    在Linux 2.6.18下执行结果如下:

    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...

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

  • 相关阅读:
    四校联考【20171001】
    C语言基础知识
    页表和TLB
    python
    Cache组织方式
    On the Spectre and Meltdown Processor Security Vulnerabilities
    latex-组织文本
    深入理解计算机系统
    深入理解计算机系统-计算机系统漫游
    逻辑地址到物理地址的转换
  • 原文地址:https://www.cnblogs.com/zhanghongfeng/p/9575714.html
Copyright © 2020-2023  润新知