ปี: 2017

อธิบายกลไกการทำ 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 ทันที