การทำ File Locking ด้วย fcntl() ตามมาตรฐาน POSIX เป็นกลไกการประสานงาน (Synchronization) ระหว่างหลายๆ โปรเซสที่ต้องการเข้าถึงไฟล์เดียวกัน โดยมีคุณลักษณะเด่น 2 ประการคือ เป็นแบบ Advisory Locking และรองรับ Record Locking (การล็อกเฉพาะช่วงไบต์ที่ต้องการ ไม่จำเป็นต้องล็อกทั้งไฟล์)
1. Advisory Locking vs Mandatory Locking
- Advisory Locking (แบบสมัครใจ/ตามข้อตกลง)
เคอร์เนลไม่ได้ขัดขวางหรือป้องกันคำสั่งread()หรือwrite()โดยตรง โปรเซสที่ต้องการอ่าน/เขียนไฟล์จะต้อง “ตกลงร่วมกัน” โดยเรียกคำสั่งตรวจสอบหรือขอล็อกผ่านfcntl()ก่อน หากมีโปรเซสอื่นไม่สนใจคำสั่งล็อกและสั่งwrite()ดื้อๆ เคอร์เนลก็ยังยอมให้เขียนทับได้ - Mandatory Locking (แบบบังคับ)
เคอร์เนลจะบล็อกทุกๆread()/write()ของโปรเซสอื่นทันทีหากติดล็อก (POSIX นิยาม Advisory เป็นหลัก ส่วน Mandatory มักไม่นิยมเนื่องจากเสี่ยงต่อ Deadlock ระดับทั้งระบบและทำให้ประสิทธิภาพลดลง)
2. ชนิดของการล็อก (Lock Types)
POSIX กำหนดชนิดของล็อกผ่านโครงสร้าง struct flock ไว้ 3 ชนิด
- Read Lock (
F_RDLCK/ Shared Lock)
หลายโปรเซสสามารถถือ Read Lock บนตำแหน่งเดียวกันได้พร้อมกัน เหมาะสำหรับงานอ่านข้อมูล เพื่อป้องกันไม่ให้มีใครนำ Write Lock มาครอบทับ - Write Lock (
F_WRLCK/ Exclusive Lock)
ให้สิทธิ์โปรเซสเพียงตัวเดียวเท่านั้น ห้ามมีโปรเซสอื่นถือ Read Lock หรือ Write Lock ในช่วงไบต์ดังกล่าวพร้อมกัน เหมาะสำหรับการแก้ไขหรือเขียนข้อมูล - Unlock (
F_UNLCK)
ใช้เพื่อปล่อยล็อกที่เคยครอบไว้
3. โครงสร้างข้อมูล struct flock
การควบคุมช่วงไบต์ของไฟล์ทำผ่านฟิลด์ใน struct flock ซึ่งกำหนดอยู่ใน <fcntl.h>:
C
struct flock {
short l_type; // F_RDLCK, F_WRLCK, หรือ F_UNLCK
short l_whence; // SEEK_SET (เริ่มไฟล์), SEEK_CUR (ตำแหน่งปัจจุบัน), SEEK_END (ท้ายไฟล์)
off_t l_start; // Offset เริ่มต้นเทียบจาก l_whence
off_t l_len; // ขนาดความยาวของช่วงที่ต้องการล็อก (ไบต์)
pid_t l_pid; // PID ของโปรเซสที่เป็นเจ้าของล็อก (ได้คืนมาเฉพาะตอนเรียก F_GETLK)
};
เทคนิคการล็อกทั้งไฟล์: หากกำหนด
l_whence = SEEK_SET,l_start = 0, และl_len = 0(ค่า 0 มีความหมายพิเศษใน POSIX คือ “ขยายไปจนถึงจุดสิ้นสุดของไฟล์เสมอ” แม้ไฟล์จะถูก append ข้อมูลยาวขึ้นในภายหลัง)
4. คำสั่งการทำงาน (Commands)
การเรียก System Call จะใช้รูปแบบ fcntl(fd, cmd, &flock_struct):
F_SETLK(Non-blocking): ขอจองล็อกทันที หากช่วงไบต์ดังกล่าวติดล็อกชนกับโปรเซสอื่น ฟังก์ชันจะส่งกลับ-1ทันที และเซ็ตerrnoเป็นEACCESหรือEAGAINโดยไม่รอF_SETLKW(Blocking / Wait): ขอจองล็อก หากมีโปรเซสอื่นถือล็อกที่ขัดแย้งอยู่ ตัวโปรเซสจะ Sleep รอจนกว่าล็อกจะว่าง หรือถูกขัดจังหวะด้วย Signal (EINTR)F_GETLK(Query Lock): ตรวจสอบว่าช่วงไบต์ที่ระบุสามารถจองล็อกได้หรือไม่ หากมีคนอื่นจองอยู่ ข้อมูลของเจ้าของล็อกเดิมจะถูกเขียนทับลงในflock_struct(รวมถึงฟิลด์l_pid) แต่ถ้าว่างอยู่ ฟิลด์l_typeจะเปลี่ยนเป็นF_UNLCK
5. ตัวอย่างโค้ด C: Exclusive Lock ทั้งไฟล์
C
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
int main(void) {
const char *filepath = "/tmp/shared_resource.lock";
int fd = open(filepath, O_RDWR | O_CREAT, 0666);
if (fd == -1) {
perror("open failed");
return 1;
}
struct flock fl;
fl.l_type = F_WRLCK; // ต้องการ Exclusive Lock
fl.l_whence = SEEK_SET; // นับจากจุดเริ่มต้นไฟล์
fl.l_start = 0; // เริ่มที่ไบต์ 0
fl.l_len = 0; // 0 หมายถึงล็อกคลุมทั้งไฟล์รวมถึงข้อมูลที่จะงอกเพิ่ม
printf("Waiting to acquire write lock on %s...\n", filepath);
// F_SETLKW: ถ้ามีโปรเซสอื่นถือล็อกอยู่ จะ Block รอตราบเท่าที่จำเป็น
if (fcntl(fd, F_SETLKW, &fl) == -1) {
perror("fcntl lock failed");
close(fd);
return 1;
}
printf("Lock acquired! Performing critical operations...\n");
sleep(5); // จำลองการทำงานใน Critical Section
// ปลดล็อก (Unlock)
fl.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &fl) == -1) {
perror("fcntl unlock failed");
} else {
printf("Lock released successfully.\n");
}
close(fd);
return 0;
}
6. พฤติกรรมสำคัญและข้อควรระวัง (Gotchas) ตามมาตรฐาน POSIX
1. ล็อกผูกกับคู่ (Process, Inode) ไม่ใช่ File Descriptor
นี่คือจุดที่ทำให้เกิดบั๊กบ่อยที่สุดใน POSIX locking
- หากโปรเซสเปิดไฟล์เดิม 2 ครั้ง (ได้
fd1และfd2) จากนั้นสั่งจองล็อกด้วยfd1 - หากมีการสั่ง
close(fd2)เคอร์เนลจะ ปลดล็อกของไฟล์นั้นทั้งหมดในโปรเซสทันที แม้ว่าจะยังถือfd1อยู่ก็ตาม
2. การสืบทอดข้าม fork() และ exec()
fork(): ล็อกของfcntl()ไม่ถูกสืบทอด ไปยัง Child Process (Child Process จะไม่มีล็อกใดๆ ติดไป)exec(): ล็อก ยังคงอยู่ ข้ามการเรียกexec()ยกเว้นกรณีที่ File Descriptor นั้นถูกเซ็ตแฟล็กFD_CLOEXECไว้ ซึ่งจะทำให้ FD ถูกปิดและล็อกหลุดไปอัตโนมัติ
3. การตรวจจับ Deadlock อัตโนมัติ
หากเกิดสถานการณ์ Deadlock ขึ้นระหว่างโปรเซส เช่น โปรเซส A ถือล็อกช่วงที่ 1 และรอช่วงที่ 2 ขณะที่โปรเซส B ถือช่วงที่ 2 และรอช่วงที่ 1 เมื่อเรียกใช้ F_SETLKW เคอร์เนลส่วนใหญ่ที่ได้มาตรฐาน POSIX จะตรวจจับได้และส่งคืนข้อผิดพลาด -1 พร้อมตั้ง errno = EDEADLK ทันที