Linux進程同步之記錄鎖(fcntl)


記錄鎖相當於線程同步中讀寫鎖的一種擴展類型,可以用來對有親緣或無親緣關系的進程進行文件讀與寫的同步,通過fcntl函數來執行上鎖操作。盡管讀寫鎖也可以通過在共享內存區來進行進程的同步,但是fcntl記錄上鎖往往更容易使用,且效率更高。

記錄鎖的功能:當一個進程正在讀或修改文件的某個部分是,它可以阻止其他進程修改同一文件區。對於這個功能闡述我認為有三點要解釋的:

  • 記錄鎖不僅僅可以用來同步不同進程對同一文件的操作,還可以通過對同一文件加記錄鎖,來同步不同進程對某一共享資源的訪問,如共享內存,I/O設備。
  • 對於勸告性上鎖,當一個進程通過上鎖對文件進行操作時,它不能阻止另一個非協作進程對該文件的修改。
  • 即使是強制性上鎖,也不能完全保證該文件不會被另一個進程修改。因為強制性鎖對unlink函數沒有影響,所以一個進程可以先刪除該文件,然后再將修改后的內容保存為同一文件來實現修改。具體可參考《APUEP367 

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的功能是對文件的控制操作,根據傳入不同的操作類型命令cmdfcntl會執行不同的操作,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,獲得當前接收SIGIO和SIGURG信號的進程ID或進程組ID
cmd = F_SETOWN,設置接收SIGIO和SIGURG信號的進程ID或進程組ID;arg = 進程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_typeF_RDLCKF_WRLCK)或釋放由lock指向flock結構所描述的鎖,如果無法獲取鎖時,該函數會立即返回一個EACCESSEAGAIN錯誤,而不會阻塞。

l F_SETLKW:F_SETLKWF_SETLK的區別是,無法設置鎖的時候,調用線程會阻塞到該鎖能夠授權位置。

l F_GETLK:IF_GETLK主要用來檢測是否有某個已存在鎖會妨礙將新鎖授予調用進程,如果沒有這樣的鎖,lock所指向的flock結構的l_type成員就會被置成F_UNLCK,否則已存在的鎖的信息將會寫入lock所指向的flock結構中

這里需要注意的是,用F_GETLK測試能否建立一把鎖,然后接着用F_SETLKF_SETLKW企圖建立一把鎖,由於這兩者不是一個原子操作,所以不能保證兩次fcntl之間不會有另外一個進程插入並建立一把相關的鎖,從而使一開始的測試情況無效。所以一般不希望上鎖時阻塞,會直接通過調用F_SETLK,並對返回結果進行測試,以判斷是否成功建立所要求的鎖。

2記錄鎖規則說明

前面我們說了記錄鎖相當於讀寫鎖的一種擴展類型,記錄鎖和讀寫鎖一樣也有兩種鎖:共享讀鎖(F_RDLCK)和獨占寫鎖(F_WRLCK)。在使用規則上和讀寫鎖也基本一樣:

  • 文件給定字節區間,多個進程可以有一把共享讀鎖,即允許多個進程以讀模式訪問該字節區;
  • 文件給定字節區間,只能有一個進程有一把獨占寫鎖,即只允許有一個進程已寫模式訪問該字節區;
  • 文件給定字節區間,如果有一把或多把讀鎖,不能在該字節區再加寫鎖,同樣,如果有一把寫鎖,不能再該字節區再加任何讀寫鎖。

如下表所示:


需要說明的是上面所闡述的規則只適用於不同進程提出的鎖請求,並不適用於單個進程提出的多個鎖請求即如果一個進程對一個文件區間已經有了一把鎖,后來該進程又試圖在同一文件區間再加一把鎖,那么新鎖將會覆蓋老鎖。

下面進行測試;第一個程序是在同一進程中測試能否在加寫鎖后,繼續加讀寫鎖。第二個程序是在在父進程中加寫鎖后,然后再子進程中測試能否繼續加讀寫鎖。

//調用的函數,在文章末尾貼出
int main()
{  
    int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);
    writew_lock(fd);

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

執行結果為:

0
0

表明同一進程可以對已加鎖的同一文件區間,仍然能獲得加鎖權限;

 

//調用的函數,在文章末尾貼出
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

表明不同進程不能對已加寫鎖的同一文件區間,獲得加鎖權限;

 

還有就是:加鎖時,該進程必須對該文件有相應的文件訪問權限,即加讀鎖,該文件必須是讀打開,加寫鎖時,該文件必須是寫打開。

3記錄鎖的粒度

這里要提到兩個概念:記錄上鎖和文件上鎖

記錄上鎖:對於UNIX系統而言,“記錄”這一詞是一種誤用,因為UNIX系統內核根本沒有使用文件記錄這種概念,更適合的術語應該是字節范圍鎖,因為它鎖住的只是文件的一個區域。用粒度來表示被鎖住文件的字節數目。對於記錄上鎖,粒度最大是整個文件。

文件上鎖:是記錄上鎖的一種特殊情況,即記錄上鎖的粒度是整個文件的大小。

之所以有文件上鎖的概念是因為有些UNIX系統支持對整個文件上鎖,但沒有給文件內的字節范圍上鎖的能力。

4記錄鎖的隱含繼承與釋放

關於記錄鎖的繼承和釋放有三條規則,如下:

1)鎖與進程和文件兩方面有關,體現在:

  • 當一個進程終止時,它所建立的記錄鎖將全部釋放;
  • 當關閉一個文件描述符時,則進程通過該文件描述符引用的該文件上的任何一把鎖都將被釋放。

對於第一個方面,可以建立如下測試代碼:

//調用的函數,在文章末尾貼出

//process 1
int main()
{
    int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);
 
    writew_lock(fd);
    cout<<"process 1 get write lock..."<<endl;
    
    sleep(10);

    cout<<"process 1 exit..."<<endl;
    return 0;
}

//process 2
int main()
{
    int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);

    writew_lock(fd);
    cout<<"process 2 get write lock..."<<endl;
    unlock(fd);

    return 0;
}

先啟動進程1,然后立即啟動進程2,執行結果如下:

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

 

對於第二個方面,可以進行如下測試:

//調用的函數,在文章末尾貼出
int main()
{
    int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);

    if (fork() == 0)
    {
        int fd_1 = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);

        readw_lock(fd_1);
        cout<<"child get read lock..."<<endl;

        sleep(3);

        close(fd_1);
        cout<<"close the file descriptor..."<<endl;

        pause();
    }

    sleep(1);

    writew_lock(fd);
    cout<<"parent get write lock..."<<endl;
    unlock(fd);

    return 0;
}

程序的執行結果如下:

child get read lock...
close the file descriptor...
parent get write lock...

 

可見,當關閉文件描述符時,與該文件描述符有關的鎖都被釋放,同樣通過dup拷貝得到的文件描述符也會導致這種情況;

 

(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)執行exec后,新程序可以繼承原執行程序的鎖。但是,如果一個文件描述符設置了close-on-exec標志,在執行exec時,會關閉該文件描述符,所以對應的鎖也就被釋放了,也就無所謂繼承了。

5記錄鎖的讀和寫的優先級

在讀寫鎖中,我曾經測試過Linux 2.6.18中提供的讀寫鎖函數是優先考慮等待讀模式占用鎖的線程,這種實現的一個很大缺陷就是出現寫入線程餓死的情況 那么在記錄鎖中是什么樣的規則呢,需要說明的是這在POSIX標准中是沒有說明的,要看具體實現。

具體進行以下2個方面測試:

  1. 進程擁有讀出鎖,然后寫入鎖等待期間額外的讀出鎖處理;
  2. 進程擁有寫入鎖,那么等待的寫入鎖和等待的讀出鎖的優先級;

測試1父進程獲得對文件的讀鎖,然后子進程1請求加寫鎖,隨即進入睡眠,然后子進程2請求讀鎖,看進程2是否能夠獲得讀鎖。

//調用的函數,在文章末尾貼出
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...

可知在有寫入進程等待的情況下,對於讀出進程的請求,系統會一直給予的。那么這也就可能導致寫入進程餓死的局面。

 

測試2父進程獲得寫入鎖,然后子進程1和子進程2分別請求獲得寫入鎖和讀寫鎖,看兩者的響應順序;

//調用的函數,在文章末尾貼出
int main()
{ 
    int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);
    writew_lock(fd);

    //child  1
    if (fork() == 0)
    {
        sleep(3);

        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)
    {
        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 2 try to get read lock...
child 1 try to get write lock...
child 2 get read lock...
child 2 release read lock...
child 1 get write lock...
child 1 release write lock...

將上面代碼該成child2 sleep 3schild1sleep

//調用的函數,在文章末尾貼出
int main()
{ 
    int fd = open(FILE_PATH, O_RDWR | O_CREAT, FILE_MODE);
    writew_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 1 get write lock...
child 1 release write lock...
child 2 get read lock...
child 2 release read lock...

由上可知在Linux 2.6.18下,等待的寫入鎖進程和讀出鎖進程的優先級由FIFO的請求順序進程響應。

6記錄鎖的使用封裝

 

void lock_init(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 readw_lock(int fd)
{
    if (fd < 0)
    {
        return -1;
    }

    struct flock lock;
    lock_init(&lock, F_RDLCK, SEEK_SET, 0, 0);

    if (fcntl(fd, F_SETLKW, &lock) != 0)
    {
        return -1;
    }
    
    return 0;
}

int writew_lock(int fd)
{
    if (fd < 0)
    {
        return -1;
    }

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

    if (fcntl(fd, F_SETLKW, &lock) != 0)
    {
        return -1;
    }

    return 0;
}

int unlock(int fd)
{
    if (fd < 0)
    {
        return -1;
    }

    struct flock lock;
    lock_init(&lock, F_UNLCK, SEEK_SET, 0, 0);

    if (fcntl(fd, F_SETLKW, &lock) != 0)
    {
        return -1;
    }

    return 0;
}

pid_t lock_test(int fd, short type, short whence, off_t start, off_t len)
{
    flock lock;
    lock_init(&lock, type, whence, start, len);

    if (fcntl(fd, F_GETLK, &lock) == -1)
    {
        return -1;
    }

    if(lock.l_type == F_UNLCK)
        return 0;
    return lock.l_pid;
}


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

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM