詢問關於系統程式作業三的內容

94 views
Skip to first unread message

廖翊廷

unread,
Mar 15, 2024, 2:37:46 AM3/15/24
to 中正資工,系統程式設計
老師您好:

我想請問一下關於系統程式作業三的內容。

作業基本上分成兩部分:flock 和 lockf,我有問題的是 lockf 的部分。接下來我會先提一下指令的意義,再說明 flock.c 的執行結果,然後是對於 lockf.c 執行結果的疑惑,以及我的猜想。最後是問題。

1. 指令
  • int flock(int fd, int operation); 上鎖的時候是把整個檔案一起上鎖,
  • int lockf(int fd, int cmd, off_t len); 會是從 fd 的 file position 往後鎖住 len 個 byte,而解鎖時也是從 fd 的 file position 開始往後解鎖 len 個 byte。
2. flock.c 的執行結果

如果同時執行 4 個使用 flock() 對同一個檔案上鎖並讀寫,假如程式碼內迴圈寫的順序是:

const size_t sleepTime = 100000;
for(size_t I=0;i<1000;i++){
flock(fd, LOCK_EX);
//…
flock(fd, LOCK_UN);
usleep(sleepTime);
}
或是
const size_t sleepTime = 100000;
for(size_t I=0;i<1000;i++){
usleep(sleepTime);
flock(fd, LOCK_EX);
//…
flock(fd, LOCK_UN);
}

只要等待是在上鎖前或是開鎖後,4 個程式一起執行的時間就會非常接近只有 1 個程式自己執行的時間。因為在等待的時候所是打開的狀態,其他程式就能趁這個空檔去讀寫檔案。

2.png
3-5.png

但是如果把等待放在上鎖期間,那執行時間就會接近於 4 倍的「 1 個程式自己執行的時間」。因為其他程式要先等待成功上鎖的程式把鎖解開後才能去讀寫。

const size_t sleepTime = 100000;
for(size_t I=0;i<1000;i++){
flock(fd, LOCK_EX);
//…
usleep(sleepTime);
flock(fd, LOCK_UN);
}

3.png

3. lockf.c 的執行狀況


而如果同時執行 4 個使用 lockf 對同一個檔案上鎖並讀寫,假如程式碼內迴圈寫的順序是:

const size_t sleepTime = 100000;
for(size_t I=0;i<1000;i++){
lockf(fd, F_LOCK, 0);  // len=0 means that the section extends from the current file position to infinity, encompassing the present and future end-of-file positions.
//…
lockf(fd, F_ULOCK, 0);
usleep(sleepTime);
}
或是
const size_t sleepTime = 100000;
for(size_t I=0;i<1000;i++){
usleep(sleepTime);
lockf(fd, F_LOCK, 0);  // len=0 means that the section extends from the current file position to infinity, encompassing the present and future end-of-file positions.
//…
lockf(fd, F_ULOCK, 0);
}

那想像中其他程式應該有辦法在等待的時候趁這個空檔去讀寫檔案,也就是跟 flock 一樣。但是實際跑出來的結果卻是一個程式花 1 倍的時間,另一個程式花 2 倍的時間,另一個程式花 3 倍的時間,另一個程式花 4 倍的時間。看似他們好像排隊做完。

在往年的作業中,大部分的解釋為:因為 lockf 的關係,lockf.db會被鎖起來,不能同時對 lockf.db 寫入資料,而第二次 lockf 要先等第一次 lockf 執行完後才能被執行。但是在程式碼中,卻是把等待放在開鎖後,這個時候檔案應該是處於未上鎖的狀態,那怎麼會說「要等前一個解鎖」呢?

4. 對於 lockf.c 結果的猜想
我認為 lockf 會有這樣的結果並不如往年作業裡所說的,因為flock跟lockf差異所造成的。純粹是因為程式寫錯了!

以去年 408530036 潘巧書的作業為例:(圖片為他的程式碼)
4.png

在他的 lockf.c 中,分別在 line 24 跟 line 36 上鎖和解鎖,雖然都是上鎖、解鎖 lock_size 個 bytes。但是 file position 在這兩個時間並不一樣,所以上鎖跟解鎖的檔案區域並不一樣。也就是說,會有一段是沒有被解鎖到,就永遠在那邊。而那些沒被解鎖的區段會在什麼時候被解鎖呢?是在檔案被關閉的時候,也就是一個程式跑完後。這有就是為什麼同時執行 4 個程式,會看起來像是「等待一個做完,領一個程式才能開始做」。

所以整個過程大概是:
4 個程式同時開始執行,但一定還是有一點點時間差,所以只有一個程式第一個成功用 lockf 把檔案鎖起來。然後讀檔 -> 寫檔 -> 解鎖 -> 睡覺(但是解鎖又沒有解乾淨,有一小段還是上著鎖的,就開始睡覺)。在睡覺的這段期間,其他程式嘗試去讀檔案,結果因為那一小段沒解乾淨的區段,因此檔案還是無法讀寫。直到那個程式整個執行完後,那些沒解乾淨的區段才被解鎖。這時候另外三支程式就會有其中一個成功用 lockf 把檔案鎖起來。接著其他兩支程式只能在等他全部做完後才會再去搶第三個…...

綜上所述,會有「用 lockf 程式要排隊執行」的錯覺,只是程式寫錯了,沒有解鎖解完全。

比如說,下面這段程式碼(我寫的)。在 lne 40 上鎖前先在 line 39 將 file position 回到檔案起始位置;在 l​ine 5​0 開鎖前先在 line 49 將 file position 回到檔案起始位置。

01: #include <stdio.h>
02: #include <stdlib.h>
03: #include <string.h>
04: #include <sys/file.h>
05: #include <unistd.h>
06: #define BUFFER_SIZE 4096
07: #define MAX_COMMAND_LENGTH 150
08: #define DEBUG_MODE 0
09:
10: int main() {
11:     const size_t sleepTime     = 100000;  // in microsecondj(10^-6 s)
12:     const size_t iterativeTime = 1000;
13:     const size_t readDigits    = 4;
14:     //     const size_t systemCommandMaxLength = 150;
15:     const char *fileDir                              = "lockf.db";
16:     char        systemCommand[3][MAX_COMMAND_LENGTH] = {"sudo mount -oremount,mand /", "chmod g+s ", "chmod g-x "};
17:     int         fileDescriptor                       = 0;
18:     off_t       offset                               = 0;
19:     char        buffer[BUFFER_SIZE];
20:
21:     fileDescriptor = open(fileDir, O_RDWR | O_CREAT, S_IRUSR | S_IWUSR);
22:     if (fileDescriptor == -1) {
23:         perror("Error: Can't not open the file");
24:         exit(EXIT_FAILURE);
25:     }
26:     if (strlen(systemCommand[1]) + strlen(fileDir) + 1 > MAX_COMMAND_LENGTH) {  // concat the base command with file directory
27:         perror("Error: file director too long");
28:         exit(EXIT_FAILURE);
29:     } else {
30:         strcat(systemCommand[1], fileDir);
31:         strcat(systemCommand[2], fileDir);
32:     }
33:     system(systemCommand[0]);
34:     system(systemCommand[1]);
35:     system(systemCommand[2]);
36:
37:     for (size_t i = 0; i < iterativeTime; i++) {
38:         usleep(sleepTime);
39:         lseek(fileDescriptor, 0, SEEK_SET);
40:         lockf(fileDescriptor, F_LOCK, 0);                     // lock the file
41:         lseek(fileDescriptor, (off_t)-readDigits, SEEK_END);  // go back -digitsNum bytes
42:         read(fileDescriptor, buffer, readDigits);             // read the number in the end of the file into the buffer
43:         sscanf(buffer, "%ld", &offset);                       // sscanf the data in buffer into offset
44:         lseek(fileDescriptor, 0, SEEK_END);
45:         lseek(fileDescriptor, offset, SEEK_CUR);
46:         offset += 1;
47:         sprintf(buffer, "%ld", offset);
48:         write(fileDescriptor, buffer, readDigits);
49:         lseek(fileDescriptor, 0, SEEK_SET);
50:         lockf(fileDescriptor, F_ULOCK, 0);
51:     }
52:
53:     close(fileDescriptor);
54:     return (EXIT_SUCCESS);
55: }
56:

這樣跑出來的時間就也就會跟 flock 跑出來的結果相似,4​ 程式執行完成的時間差不多。

5.png

5. 問題

這個作業的目的是什麼?期待學生做出怎麼樣的結果?如果是希望展示 flock 和 lockf​ 的「鎖定效果」,那想像中只要正確解鎖,那同時執行多個相同的程式,結果應該是幾乎一樣的;如果是要體現兩者個強度差異,那像中不應該有「解鎖」這一步,而是在維持鎖定的情況下用不同的方法嘗試去讀寫他,看看哪個方法可以、哪個不行讀寫。

我實在很疑惑,還請老師您解答,謝謝!

魯智深

unread,
Mar 15, 2024, 5:52:39 AM3/15/24
to 中正資工,系統程式設計
1. 你寫的沒錯,之前的報告是不對的,因為量測的方法(每個測試程式的寫法)不一樣,因此不能合理的比較lockf和flock
2. 我們的作業比較像是「開放性問題」,使用不同版本的 Linux 可能會有不一樣的執行結果,只要「論述正確」,像你這樣去寫報告,我一定會給很高分,也會很開心
3. 我用底下的例子說明系統程式會有很多「開放性問題」的原因

getpid()的glibc實作:
第一版:真的使用system call呼叫getpid,因此要很小心overhead
第二版:呼叫vdso中的函數,這個會有二次的function call的overhead,但好過於system call
第三版:__pid初始值為0,如果getpid()發現__pid為0則呼叫system call,並設置__pid。否則直接回傳 __pid
這三種實現方式都是對的,但overhead來說,第三版除了第一次呼叫以外,他的成本只是function call,因此不用特別小心

結論:
現在我教的東西跟我當初學的東西幾乎都不一樣,系統程式要教大家的是「作業系統等級的全盤思考」,答案只要合理助教就會給分
考卷給我改,有些題目也很難有正確答案,例如 gcc --static,編譯出來的程式碼通常會比較快,但有反例。

請繼續加油



習五

Reply all
Reply to author
Forward
0 new messages