PlusMagi's Blog By Pitt Phunsanit Backend อธิบายกลไกการทำ File Locking ด้วย fcntl() (Advisory locking) ตามมาตรฐาน POSIX

อธิบายกลไกการทำ File Locking ด้วย fcntl() (Advisory locking) ตามมาตรฐาน POSIX

การทำ 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 ชนิด

  1. Read Lock (F_RDLCK / Shared Lock)
    หลายโปรเซสสามารถถือ Read Lock บนตำแหน่งเดียวกันได้พร้อมกัน เหมาะสำหรับงานอ่านข้อมูล เพื่อป้องกันไม่ให้มีใครนำ Write Lock มาครอบทับ
  2. Write Lock (F_WRLCK / Exclusive Lock)
    ให้สิทธิ์โปรเซสเพียงตัวเดียวเท่านั้น ห้ามมีโปรเซสอื่นถือ Read Lock หรือ Write Lock ในช่วงไบต์ดังกล่าวพร้อมกัน เหมาะสำหรับการแก้ไขหรือเขียนข้อมูล
  3. 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 ทันที


Exit mobile version