/static/codemoomoo2.png

1. Introduction of Operating System

บทความนี้นำเสนอแนวคิดพื้นฐานของระบบปฏิบัติการ (Operating System) ตั้งแต่ประวัติ วิวัฒนาการ สถาปัตยกรรม Kernel การจัดการโพรเซส หน่วยความจำ ไฟล์ระบบ และ I/O พร้อมตัวอย่างโค้ดที่ใช้งานได้จริงบน Linux เพื่อเป็นพื้นฐานสำหรับการพัฒนาซอฟต์แวร์ระดับระบบ


1.1 แนวคิดพื้นฐานของระบบปฏิบัติการ (OS Fundamental Concepts)

ระบบปฏิบัติการ (Operating System: OS) คือซอฟต์แวร์ระบบ (system software) ที่ทำหน้าที่เป็นตัวกลางระหว่างฮาร์ดแวร์คอมพิวเตอร์ (computer hardware) และผู้ใช้งาน (user) รวมถึงโปรแกรมประยุกต์ (application program) ต่าง ๆ โดย OS จะบริหารจัดการทรัพยากรของระบบ เช่น หน่วยประมวลผลกลาง (CPU) หน่วยความจำ (memory) อุปกรณ์ต่อพ่วง (I/O device) และระบบไฟล์ (file system) ให้ผู้ใช้งานสามารถใช้งานทรัพยากรเหล่านี้ได้อย่างสะดวก มีประสิทธิภาพ และปลอดภัย

1.1.1 นิยามและบทบาทของระบบปฏิบัติการในฐานะ Resource Manager และ Extended Machine

ระบบปฏิบัติการมีมุมมองสองด้านที่สำคัญ ซึ่ง Andrew S. Tanenbaum ได้อธิบายไว้ในหนังสือ Modern Operating Systems

มุมมองที่ 1: OS ในฐานะ Resource Manager (ผู้จัดการทรัพยากร)

OS ทำหน้าที่จัดสรรและควบคุมทรัพยากรของคอมพิวเตอร์ให้กับโปรแกรมต่าง ๆ ที่ร้องขอใช้งานพร้อมกัน โดยพิจารณาถึง:

มุมมองที่ 2: OS ในฐานะ Extended Machine (เครื่องจักรขยาย)

OS ซ่อนความซับซ้อนของฮาร์ดแวร์และนำเสนอ อินเทอร์เฟซ (interface) ที่เป็น abstraction ให้แก่โปรแกรมเมอร์ เช่น แทนที่นักพัฒนาจะต้องเขียนโค้ดควบคุม disk controller ด้วยตนเอง OS นำเสนอแนวคิดของ "ไฟล์ (file)" ที่ใช้งานง่ายผ่าน system call เช่น open(), read(), write(), close()

flowchart TB
    User["ผู้ใช้งาน
(User)"] App["โปรแกรมประยุกต์
(Application)"] OS["ระบบปฏิบัติการ
(Operating System)"] HW["ฮาร์ดแวร์
(Hardware)"] User --> App App -->|"System Call
API"| OS OS -->|"Device Driver
Instruction"| HW HW -->|"Interrupt
Signal"| OS OS -->|"Return Value
Event"| App

1.1.2 ประวัติและวิวัฒนาการของ OS (Batch → Multiprogramming → Time-sharing → Modern)

วิวัฒนาการของ OS สะท้อนการเปลี่ยนแปลงของฮาร์ดแวร์และความต้องการของผู้ใช้งานในแต่ละยุค

flowchart TB
    subgraph Gen1["Gen 1 (1945-1955) Vacuum Tube"]
        A1["ไม่มี OS
Plugboard
Manual Operation"] end subgraph Gen2["Gen 2 (1955-1965) Transistor"] A2["Batch System
IBM 7094
FMS, IBSYS"] end subgraph Gen3["Gen 3 (1965-1980) IC"] A3["Multiprogramming
Time-sharing
OS/360, MULTICS, UNIX"] end subgraph Gen4["Gen 4 (1980-2000) Personal Computer"] A4["PC OS
MS-DOS, Windows
Mac OS, Linux"] end subgraph Gen5["Gen 5 (2000-ปัจจุบัน) Mobile & Cloud"] A5["Mobile OS
iOS, Android
Cloud OS, IoT"] end Gen1 --> Gen2 --> Gen3 --> Gen4 --> Gen5

ยุคที่ 1 (1945-1955) Vacuum Tube: ไม่มีแนวคิด OS เลย นักวิทยาศาสตร์เขียนโปรแกรมด้วย machine language แล้วป้อนเข้าเครื่องโดยใช้ plugboard หรือบัตรเจาะรู (punched card) ตัวอย่างเครื่องในยุคนี้เช่น ENIAC, EDVAC

ยุคที่ 2 (1955-1965) Transistor และ Batch System: เกิดแนวคิด Batch Processing (การประมวลผลแบบกลุ่ม) เพื่อลดเวลาว่างของ CPU ระหว่างรอผู้ใช้งาน โปรแกรมหลาย ๆ งานถูกรวมเป็นชุด (batch) แล้วให้ OS (เช่น FMS - Fortran Monitor System) รันทีละงานต่อเนื่องกันไป

ยุคที่ 3 (1965-1980) IC และ Multiprogramming: เทคโนโลยี IC (Integrated Circuit) ทำให้คอมพิวเตอร์เล็กลง เร็วขึ้น และเกิดแนวคิด Multiprogramming ที่ให้หลายโพรเซสอยู่ในหน่วยความจำพร้อมกัน เมื่อโพรเซสหนึ่งรอ I/O CPU จะสลับไปทำงานอื่น ต่อมาเกิด Time-sharing (เช่น CTSS, MULTICS, UNIX) ที่ให้ผู้ใช้งานหลายคนใช้เครื่องเดียวพร้อมกันผ่าน terminal

ยุคที่ 4 (1980-2000) PC Era: Intel 8080/8086 และการกำเนิดของ personal computer ทำให้เกิด OS สำหรับผู้ใช้รายบุคคล เช่น CP/M, MS-DOS, Windows 3.x/9x/NT, Mac OS และ Linux (1991 โดย Linus Torvalds)

ยุคที่ 5 (2000-ปัจจุบัน) Mobile, Cloud และ Embedded: iOS (2007), Android (2008), ChromeOS, รวมถึง OS สำหรับ cloud infrastructure เช่น Hypervisor, container runtime, และ IoT OS อย่าง FreeRTOS, Zephyr


1.1.3 ประเภทของ OS: Batch, Time-Sharing, Real-Time (Hard/Soft), Distributed, Embedded, Mobile, Cloud OS

ประเภท OS ลักษณะเด่น ตัวอย่าง Use Case
Batch OS รันงานเป็นชุด ไม่มี interaction IBM OS/360, z/OS Payroll, Billing
Time-Sharing OS หลายผู้ใช้ใช้พร้อมกัน UNIX, Multics Mainframe, Server
Hard Real-Time OS มี deadline เข้มงวด ห้ามพลาด VxWorks, QNX, RTEMS Aerospace, ABS รถยนต์
Soft Real-Time OS มี deadline แต่พลาดได้บ้าง RTLinux, Windows CE Video streaming, Gaming
Distributed OS หลายเครื่องทำงานร่วมกัน Plan 9, Amoeba, LOCUS HPC Cluster
Embedded OS รันในอุปกรณ์เฉพาะ FreeRTOS, Zephyr, TinyOS IoT, MCU
Mobile OS อุปกรณ์พกพา จัดการพลังงาน iOS, Android, HarmonyOS Smartphone, Tablet
Cloud OS รันบน virtualized infrastructure OpenStack, Kubernetes* Data center, SaaS
Network OS เน้นบริการเครือข่าย Cisco IOS, Junos Router, Switch

*Kubernetes ไม่ใช่ OS ในความหมายดั้งเดิม แต่ถูกเรียกว่า "Cloud OS" เพราะทำหน้าที่จัดการทรัพยากรระดับ cluster

Real-Time OS (RTOS) แบ่งเป็น 2 ประเภทตามความเข้มงวดของ deadline


1.1.4 สถาปัตยกรรม Kernel: Monolithic, Microkernel, Hybrid, Exokernel, Unikernel

Kernel คือส่วนหลักของ OS ที่ทำงานใน privileged mode และเป็นส่วนที่ต้องทำงานให้ได้ตลอดเวลาเพื่อจัดการทรัพยากรของระบบ

flowchart TB
    subgraph Mono["Monolithic Kernel"]
        M1["User App"] --> M2["System Call"]
        M2 --> M3["Kernel
FS + MM + Sched + Driver"] end subgraph Micro["Microkernel"] U1["User App"] --> U2["IPC"] U2 --> U3["FS Server"] U2 --> U4["MM Server"] U2 --> U5["Driver Server"] U3 & U4 & U5 --> K1["Minimal Kernel
IPC + Sched"] end Mono ~~~ Micro

Monolithic Kernel: Kernel ทั้งหมดทำงานใน address space เดียวและใน kernel mode เดียว ส่วนต่าง ๆ (scheduler, memory manager, file system, device driver) เรียกกันได้โดยตรงเป็น function call ทำให้มีประสิทธิภาพสูงแต่ซับซ้อนและบั๊กในส่วนหนึ่งอาจทำให้ทั้งระบบล่มได้ ตัวอย่าง: Linux, FreeBSD, OpenBSD

Microkernel: Kernel มีขนาดเล็กมาก ทำเฉพาะงานที่จำเป็น (IPC, scheduling, basic memory management) ส่วนอื่น ๆ เช่น file system, device driver ทำงานเป็น user process สื่อสารกันผ่าน IPC ข้อดีคือเสถียรภาพสูง (crash แยกส่วน) ข้อเสียคือ overhead จาก context switch และ IPC ตัวอย่าง: MINIX 3, QNX, seL4

Hybrid Kernel: ผสม Monolithic และ Microkernel โดยเก็บ subsystem สำคัญไว้ใน kernel space แต่แยก service บางส่วนออก ตัวอย่าง: Windows NT kernel, XNU (macOS/iOS)

Exokernel: ลดชั้น abstraction ให้น้อยที่สุด เปิดให้ application เข้าถึง hardware resource ได้โดยตรง โดย kernel ทำหน้าที่เพียง multiplex และ protect เท่านั้น ตัวอย่างงานวิจัย: MIT Exokernel

Unikernel: รวม application + library OS เป็น single-address-space image รันบน hypervisor โดยตรง ขนาดเล็ก boot เร็ว เหมาะกับ cloud/microservice ตัวอย่าง: MirageOS, IncludeOS, Unikraft


1.1.5 User Mode vs Kernel Mode และ Privilege Ring

CPU สมัยใหม่ (x86, ARM) มีระดับสิทธิ์การทำงาน (privilege level) หลายระดับ เพื่อป้องกันไม่ให้โปรแกรมผู้ใช้งานเข้าถึงฮาร์ดแวร์หรือหน่วยความจำของ kernel โดยตรง

flowchart TB
    R0["Ring 0 (Kernel Mode)
Kernel + Driver
สิทธิ์สูงสุด"] R1["Ring 1
Device Driver (optional)"] R2["Ring 2
Driver (optional)"] R3["Ring 3 (User Mode)
Application
สิทธิ์จำกัด"] R3 -->|"System Call
(trap/syscall)"| R0 R0 -->|"Return
(iret/sysret)"| R3

บน x86 มี Ring 0–3 โดยปกติ Linux และ Windows ใช้เพียง Ring 0 (kernel) และ Ring 3 (user) ส่วน Ring 1 และ Ring 2 ไม่ถูกใช้งานทั่วไป (เว้นแต่ในบาง hypervisor เดิม)

การเปลี่ยนจาก User Mode → Kernel Mode เกิดได้ 3 ทาง

  1. System Call: โปรแกรมเรียกใช้บริการของ kernel เช่น read(), write()
  2. Interrupt (Hardware Interrupt): อุปกรณ์แจ้งเหตุ เช่น keyboard press, timer tick
  3. Exception/Trap: โปรแกรมเกิดข้อผิดพลาด เช่น division by zero, page fault

เมื่ออยู่ใน Kernel Mode CPU สามารถ:


1.1.6 System Call และ API (POSIX)

System Call คือกลไกที่ user process ใช้ร้องขอบริการจาก kernel โดยจะ trap เข้าสู่ kernel mode ผ่านคำสั่ง syscall (x86_64) หรือ svc (ARM)

sequenceDiagram
    participant App as User App
    participant Libc as C Library
(glibc) participant Kern as Kernel participant HW as Hardware App->>Libc: printf("Hello") Libc->>Libc: format string Libc->>Kern: syscall write(1, buf, len) Note over Kern: เปลี่ยนเป็น Kernel Mode Kern->>HW: ส่งข้อมูลไป terminal HW-->>Kern: เสร็จ Kern-->>Libc: return bytes written Note over Libc: กลับเป็น User Mode Libc-->>App: return

POSIX (Portable Operating System Interface) คือมาตรฐานที่กำหนด API และ behavior ของ OS ตระกูล Unix ให้โปรแกรมสามารถ port ข้าม OS ได้ เช่น Linux, macOS, FreeBSD

ตัวอย่างการเปรียบเทียบ System Call

หมวด Linux System Call Windows API
Process creation fork(), execve() CreateProcess()
File open open() CreateFile()
File read read() ReadFile()
File write write() WriteFile()
File close close() CloseHandle()
Wait child waitpid() WaitForSingleObject()
Memory map mmap() MapViewOfFile()
Exit exit() ExitProcess()

ตัวอย่างโค้ด C: เรียก system call โดยตรง (ไม่ผ่าน libc)

// syscall_direct.c - แสดง "Hello POSIX!" โดยเรียก write() syscall โดยตรง
// คอมไพล์: gcc syscall_direct.c -o syscall_direct
#include <unistd.h>      // สำหรับ syscall และ STDOUT_FILENO
#include <sys/syscall.h> // สำหรับ SYS_write

int main(void) {
    const char msg[] = "Hello POSIX!\n";
    // syscall(SYS_write, fd, buffer, length)
    // fd = 1 คือ stdout (STDOUT_FILENO)
    syscall(SYS_write, STDOUT_FILENO, msg, sizeof(msg) - 1);
    
    // เรียก exit syscall เพื่อจบโปรเซสอย่างสมบูรณ์
    syscall(SYS_exit, 0);
    return 0; // ไม่มีวันถึงบรรทัดนี้
}

ตัวอย่างการใช้งาน:

$ gcc syscall_direct.c -o syscall_direct
$ ./syscall_direct
Hello POSIX!
$ strace -e write,exit_group ./syscall_direct
write(1, "Hello POSIX!\n", 13Hello POSIX!
)          = 13
exit_group(0)                           = ?
+++ exited with 0 +++

1.1.7 Boot Process และ Init System

ลำดับการบูต (boot sequence) บน Linux x86_64 สมัยใหม่ (UEFI) มีขั้นตอนดังนี้

flowchart TB
    A["Power On"] --> B["UEFI Firmware
POST + Init HW"] B --> C["EFI System Partition
อ่าน bootloader"] C --> D["Bootloader
GRUB / systemd-boot"] D --> E["Load Kernel + initramfs
vmlinuz + initrd.img"] E --> F["Kernel Init
mount rootfs, probe HW"] F --> G["pid=1: init
systemd / OpenRC / runit"] G --> H["User Space
Services + Login"]

ขั้นตอนโดยละเอียด

  1. POST (Power-On Self-Test) - UEFI/BIOS ตรวจสอบฮาร์ดแวร์พื้นฐาน CPU, RAM, GPU
  2. Bootloader - UEFI อ่าน .efi จาก EFI System Partition (ESP, มักเป็น /boot/efi) แล้วโหลด GRUB หรือ systemd-boot
  3. Kernel Loading - Bootloader โหลด kernel image (vmlinuz-*) และ initial ramdisk (initramfs-* หรือ initrd.img)
  4. Kernel Initialization - Kernel unpacks initramfs, probe driver, mount root filesystem จริง แล้วเรียกโปรเซส pid=1
  5. Init System - Process แรก (pid=1) เริ่มต้น service, mount filesystem เพิ่มเติม, สร้าง getty/login

Init System ที่พบบ่อย

ตัวอย่าง systemd service file:

# /etc/systemd/system/hello.service
# ตัวอย่าง service ง่าย ๆ ที่รัน shell script ทุก ๆ ครั้งที่บูต
[Unit]
Description=Hello World Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/hello.sh
Restart=on-failure
User=nobody

[Install]
WantedBy=multi-user.target

ตัวอย่างการใช้งาน:

# เปิดใช้งานและเริ่ม service
sudo systemctl daemon-reload
sudo systemctl enable hello.service
sudo systemctl start hello.service

# ตรวจสอบสถานะ
systemctl status hello.service
journalctl -u hello.service -f

1.2 การจัดการโพรเซส (Process Management)

1.2.1 แนวคิดของ Process และ Process Control Block (PCB)

Process (โพรเซส) คือโปรแกรมที่กำลังทำงาน (program in execution) ซึ่งต่างจาก program ที่เป็นเพียงไฟล์บน disk โพรเซสประกอบด้วย:

flowchart TB
    subgraph Mem["Process Memory Layout"]
        direction TB
        S["Stack
(เติบโตลง)"] G["↓ ↓ ↓"] F["... Free ..."] U["↑ ↑ ↑"] H["Heap
(เติบโตขึ้น)"] B["BSS
(uninitialized data)"] D["Data
(initialized global)"] T["Text
(โค้ด)"] S ~~~ G ~~~ F ~~~ U ~~~ H ~~~ B ~~~ D ~~~ T end

Process Control Block (PCB) คือโครงสร้างข้อมูลใน kernel ที่เก็บข้อมูลทุกอย่างที่ต้องใช้ในการจัดการ process แต่ละตัว บน Linux เรียกว่า struct task_struct (ในไฟล์ include/linux/sched.h)

ข้อมูลสำคัญใน PCB:

1.2.2 สถานะของ Process (New, Ready, Running, Waiting, Terminated)

stateDiagram-v2
    [*] --> New: fork()
    New --> Ready: admit
    Ready --> Running: scheduler dispatch
    Running --> Ready: timer interrupt
(preemption) Running --> Waiting: I/O request
wait for event Waiting --> Ready: I/O done
event occurs Running --> Terminated: exit() Terminated --> [*]

คำอธิบายแต่ละสถานะ

บน Linux สามารถดูสถานะของ process ได้ด้วย ps ตัวอักษรที่ปรากฏ:

1.2.3 Process Scheduling Queues (Ready Queue, Device Queue, Job Queue)

Job Queue: รวมทุกโพรเซสในระบบ

Ready Queue: โพรเซสที่อยู่ในหน่วยความจำหลักและพร้อมทำงาน รอ CPU

Device Queue: แต่ละ I/O device มี queue ของตนเอง โพรเซสที่รอ I/O จะเข้า queue นี้

flowchart LR
    RQ["Ready Queue"] -->|"dispatch"| CPU["CPU"]
    CPU -->|"time slice expired"| RQ
    CPU -->|"I/O request"| DQ1["Disk Queue"]
    CPU -->|"I/O request"| DQ2["Network Queue"]
    CPU -->|"fork()"| RQ
    CPU -->|"wait for child"| WQ["Wait Queue"]
    DQ1 -->|"I/O done"| RQ
    DQ2 -->|"I/O done"| RQ
    WQ -->|"child exit"| RQ
    CPU -->|"exit"| END["Terminate"]

1.2.4 การสร้างและยุติ Process (fork, exec, wait, exit)

บน UNIX/Linux การสร้าง process ใหม่ใช้ fork() ซึ่ง copy ทุกอย่างจาก parent สร้างเป็น child (ในทางปฏิบัติใช้ Copy-on-Write) จากนั้นมักตามด้วย exec*() เพื่อโหลดโปรแกรมใหม่ทับ memory image

sequenceDiagram
    participant P as Parent
    participant C as Child
    P->>P: fork()
    Note over P,C: Kernel สร้าง PCB ใหม่
copy address space (CoW) P->>P: pid = child_pid C->>C: pid = 0 C->>C: execve("/bin/ls", ...) Note over C: โหลดโปรแกรม ls ทับ P->>P: waitpid(child_pid, ...) C->>C: ทำงาน ls แล้ว exit(0) C-->>P: SIGCHLD + exit status

ตัวอย่างโค้ด C: fork + exec + wait

// fork_exec.c - แสดงตัวอย่าง fork() + execvp() + waitpid()
// คอมไพล์: gcc fork_exec.c -o fork_exec
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

int main(void) {
    printf("[Parent] pid=%d กำลังจะ fork()\n", getpid());
    
    pid_t pid = fork();  // สร้าง child process
    
    if (pid < 0) {
        // fork ล้มเหลว (เช่น resource เต็ม)
        perror("fork");
        exit(EXIT_FAILURE);
    } else if (pid == 0) {
        // --- Child Process ---
        printf("[Child] pid=%d ppid=%d กำลังจะ exec ls\n",
               getpid(), getppid());
        // แทนที่ memory image ด้วยโปรแกรม ls
        char *args[] = {"ls", "-l", "/tmp", NULL};
        execvp("ls", args);
        // ถ้า exec สำเร็จ โค้ดบรรทัดนี้ไม่มีวันรัน
        perror("execvp");
        exit(EXIT_FAILURE);
    } else {
        // --- Parent Process ---
        int status;
        printf("[Parent] fork สำเร็จ child pid=%d\n", pid);
        // รอ child จบ เพื่อป้องกัน zombie process
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
            printf("[Parent] child จบด้วย exit code %d\n",
                   WEXITSTATUS(status));
        }
    }
    return 0;
}

ตัวอย่างผลลัพธ์:

$ gcc fork_exec.c -o fork_exec && ./fork_exec
[Parent] pid=12345 กำลังจะ fork()
[Parent] fork สำเร็จ child pid=12346
[Child] pid=12346 ppid=12345 กำลังจะ exec ls
total 12
drwxrwxrwt 10 root root 4096 Apr 20 14:30 .
-rw-r--r--  1 user user  123 Apr 20 14:29 test.txt
[Parent] child จบด้วย exit code 0

ตระกูลฟังก์ชัน exec: execl(), execlp(), execle(), execv(), execvp(), execve() ต่างกันที่การส่ง argument (list vs vector) การค้นหาใน PATH (p) และ environment (e)

1.2.5 Thread, Multithreading, User-level vs Kernel-level Thread

Thread คือหน่วยการทำงาน (unit of execution) ภายใน process เดียวกัน ที่แชร์ address space, open file, signal handler แต่มี stack, register, และ program counter ของตัวเอง

ประโยชน์ของ Multithreading:

User-level Thread (ULT): OS ไม่รู้จัก thread ห้องสมุดใน user space เป็นคนจัดการ เช่น green thread ข้อดีคือเร็ว (ไม่ต้อง syscall) ข้อเสียคือถ้า thread หนึ่งเรียก blocking syscall ทั้ง process จะ block

Kernel-level Thread (KLT): Kernel เป็นคนจัดการและ schedule ข้อดีคือถ้า thread หนึ่ง block อันอื่นยังทำงานได้ และใช้ประโยชน์ multi-core ได้ ข้อเสียคือ switching ช้ากว่า ULT

1.2.6 Thread Model (Many-to-One, One-to-One, Many-to-Many)

flowchart TB
    subgraph M21["Many-to-One"]
        U1A["ULT"] --> K1A["KLT"]
        U1B["ULT"] --> K1A
        U1C["ULT"] --> K1A
    end
    subgraph O2O["One-to-One"]
        U2A["ULT"] --> K2A["KLT"]
        U2B["ULT"] --> K2B["KLT"]
        U2C["ULT"] --> K2C["KLT"]
    end
    subgraph M2M["Many-to-Many"]
        U3A["ULT"] --> K3A["KLT"]
        U3B["ULT"] --> K3A
        U3C["ULT"] --> K3B["KLT"]
        U3D["ULT"] --> K3B
    end
Model ข้อดี ข้อเสีย ตัวอย่าง
Many-to-One เร็ว, ไม่ต้อง syscall block ทั้ง process, ไม่ใช้ multi-core Green Thread (เดิม)
One-to-One true parallelism, ไม่ block สร้าง thread เยอะ overhead สูง Linux NPTL, Windows
Many-to-Many ยืดหยุ่น, ประสิทธิภาพดี ซับซ้อนในการ implement Solaris เดิม, Windows ThreadPool

Linux ใช้โมเดล One-to-One ผ่าน NPTL (Native POSIX Thread Library) โดย pthread แต่ละตัวถูก map เป็น kernel task (lightweight process) ผ่าน clone() syscall

1.2.7 Inter-Process Communication (IPC): Shared Memory, Message Passing, Pipe, FIFO, Socket, Signal

flowchart LR
    P1["Process A"]
    P2["Process B"]
    SM["Shared
Memory"] MQ["Message
Queue"] PI["Pipe / FIFO"] SK["Socket"] SG["Signal"] P1 <--> SM P2 <--> SM P1 -->|send| MQ -->|recv| P2 P1 -->|write| PI -->|read| P2 P1 <-->|bi-dir| SK SK <--> P2 P1 -->|kill| SG --> P2

Shared Memory (หน่วยความจำร่วม): สอง process map หน่วยความจำส่วนเดียวกันเข้าสู่ address space ของตน การสื่อสารเร็วมาก (ไม่มี copy) แต่ต้องจัดการ synchronization เอง API: POSIX (shm_open, mmap), SysV (shmget, shmat)

Message Passing: ส่งข้อความ (message) ผ่าน kernel ง่ายกว่า shared memory แต่ช้ากว่าเพราะมี copy ผ่าน kernel API: POSIX mqueue, SysV msgget/msgsnd/msgrcv

Pipe (|): unidirectional byte stream ระหว่าง parent-child (anonymous pipe) ใช้มากบน shell เช่น ls | grep txt

FIFO (Named Pipe): เหมือน pipe แต่มีชื่อใน filesystem สามารถใช้ระหว่าง process ที่ไม่มีความสัมพันธ์กันได้ สร้างด้วย mkfifo

Socket: bidirectional, ใช้ได้ทั้งในเครื่อง (UNIX domain socket) และข้ามเครื่อง (TCP/UDP) เป็นพื้นฐานของการสื่อสารเครือข่ายและไมโครเซอร์วิส

Signal: ส่งเหตุการณ์แบบ asynchronous เช่น SIGINT (Ctrl+C), SIGTERM, SIGKILL, SIGCHLD เหมาะกับ notification สั้น ๆ

ตัวอย่าง pipe ระหว่าง parent-child

// pipe_demo.c - ตัวอย่าง anonymous pipe ส่งข้อมูลจาก child กลับ parent
// คอมไพล์: gcc pipe_demo.c -o pipe_demo
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(void) {
    int fd[2];           // fd[0] = read end, fd[1] = write end
    char buffer[128];
    
    // สร้าง pipe ก่อน fork เพื่อให้ทั้งสอง process แชร์ pipe กัน
    if (pipe(fd) < 0) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // Child: เป็นฝั่งเขียน
        close(fd[0]);  // ปิด read end ที่ไม่ใช้
        const char *msg = "สวัสดีจาก child process!\n";
        write(fd[1], msg, strlen(msg));
        close(fd[1]);  // ปิด write end เมื่อเขียนเสร็จ
        exit(0);
    } else {
        // Parent: เป็นฝั่งอ่าน
        close(fd[1]);  // ปิด write end ที่ไม่ใช้
        ssize_t n = read(fd[0], buffer, sizeof(buffer) - 1);
        if (n > 0) {
            buffer[n] = '\0';  // terminator
            printf("[Parent] รับข้อความ: %s", buffer);
        }
        close(fd[0]);
        wait(NULL);  // รอ child จบ
    }
    return 0;
}

ผลลัพธ์:

$ gcc pipe_demo.c -o pipe_demo && ./pipe_demo
[Parent] รับข้อความ: สวัสดีจาก child process!

1.3 อัลกอริทึมการจัดตารางเวลา CPU (CPU Scheduling Algorithm)

1.3.1 เกณฑ์การวัดประสิทธิภาพ: CPU Utilization, Throughput, Turnaround Time, Waiting Time, Response Time

เมื่อออกแบบ CPU scheduler เราต้องพิจารณาเป้าหมายหลายอย่างพร้อมกันซึ่งอาจขัดแย้งกัน

สมการความสัมพันธ์พื้นฐาน

T = tcompletion - tarrival

โดย T คือ turnaround time, t_completion คือเวลาที่ process จบ และ t_arrival คือเวลาที่ process เข้าระบบ

W = T - tburst

โดย W คือ waiting time, t_burst คือเวลาที่ process ต้องการ CPU (CPU burst time)

W¯ = i=1 n Wi n

โดย คือ average waiting time, W_i คือ waiting time ของ process i, และ n คือจำนวน process ทั้งหมด

1.3.2 Preemptive vs Non-preemptive Scheduling

1.3.3 First-Come First-Served (FCFS)

FCFS เป็นอัลกอริทึมที่ง่ายที่สุด process เข้าก่อนได้ CPU ก่อน เป็น non-preemptive ใช้ FIFO queue

ตัวอย่าง: มี 3 process ที่มาถึงพร้อมกัน ที่เวลา 0

Process Burst Time
P1 24
P2 3
P3 3

ถ้าลำดับคือ P1 → P2 → P3

| P1       |P2|P3|
0         24 27 30

หากสลับเป็น P2 → P3 → P1

|P2|P3|P1        |
0  3  6         30

ปัญหา Convoy Effect: process สั้น ๆ ต้องรอ process ยาวตัวหน้า

1.3.4 Shortest-Job-First (SJF) และ Shortest-Remaining-Time-First (SRTF)

SJF เลือก process ที่มี burst time สั้นที่สุดก่อน พิสูจน์ได้ว่าให้ average waiting time ต่ำสุด (optimal) แต่ปัญหาคือเราไม่รู้ burst time ล่วงหน้า ต้อง predict จากประวัติ

τn+1 = α tn + ( 1 - α ) τn

โดย τ_(n+1) คือค่าคาดการณ์ burst time ครั้งถัดไป, t_n คือ burst time จริงครั้งล่าสุด, τ_n คือค่าคาดการณ์ครั้งก่อน, และ α (0 ≤ α ≤ 1) คือน้ำหนักถ่วง (exponential averaging)

SRTF (Shortest Remaining Time First): เวอร์ชัน preemptive ของ SJF ถ้ามี process ใหม่มาและ remaining time ของมันน้อยกว่า process ที่กำลังทำงานอยู่ จะ preempt ทันที

ตัวอย่าง SRTF:

Process Arrival Burst
P1 0 8
P2 1 4
P3 2 9
P4 3 5
|P1|P2      |P4     |P1           |P3           |
0  1        5       10            17            26

1.3.5 Priority Scheduling และปัญหา Starvation / Aging

กำหนด priority number ให้แต่ละ process (ตัวเลขเล็ก = priority สูงในหลายระบบ เช่น Linux nice -20 ถึง +19) process priority สูงสุดได้ CPU ก่อน

ปัญหา Starvation (Indefinite Blocking): process priority ต่ำอาจไม่ได้ทำงานเลยหาก process priority สูงมาเรื่อย ๆ

แก้ด้วย Aging: เพิ่ม priority ของ process ที่รอนานขึ้นเรื่อย ๆ เช่น ทุก 1 วินาที เพิ่ม priority +1

1.3.6 Round Robin (RR) และการเลือก Time Quantum

Round Robin กำหนด time quantum (time slice) เท่า ๆ กัน มักอยู่ที่ 10-100 ms แต่ละ process ได้ quantum หนึ่งชุด เมื่อหมด preempt ไปต่อท้าย ready queue

การเลือก Time Quantum

ตัวอย่าง RR quantum = 4:

Process Burst
P1 24
P2 3
P3 3
|P1 |P2 |P3|P1 |P1 |P1 |P1 |P1 |
0   4   7  10 14  18  22  26  30

1.3.7 Multilevel Queue Scheduling

แบ่ง ready queue เป็นหลายระดับตามประเภทของ process เช่น

  1. System processes (priority สูงสุด)
  2. Interactive processes (foreground)
  3. Interactive editing
  4. Batch processes
  5. Student processes (ต่ำสุด)

แต่ละ queue ใช้อัลกอริทึมของตัวเอง (เช่น foreground = RR, background = FCFS) และมีนโยบายระหว่าง queue (fixed priority / time slicing)

1.3.8 Multilevel Feedback Queue Scheduling

พัฒนาจาก Multilevel Queue ตรงที่ process สามารถ ย้ายระหว่าง queue ได้ตามพฤติกรรม

flowchart TB
    NEW["New Process"] --> Q0["Queue 0 (RR q=8ms)
Highest Priority"] Q0 -->|"ใช้ quantum หมด
ยังไม่เสร็จ"| Q1["Queue 1 (RR q=16ms)"] Q1 -->|"ใช้ quantum หมด
ยังไม่เสร็จ"| Q2["Queue 2 (FCFS)
Lowest Priority"] Q0 -->|"done/block"| EXIT["Exit/Wait"] Q1 -->|"done/block"| EXIT Q2 -->|"done/block"| EXIT Q2 -.->|"aging"| Q1 Q1 -.->|"aging"| Q0

Process ที่ใช้ CPU สั้น ๆ (I/O bound) จะอยู่ Q ระดับสูง ส่วน process ที่กิน CPU ยาว ๆ (CPU bound) จะค่อย ๆ ถูก demote ลง queue ล่าง

1.3.9 Multi-processor Scheduling (Load Balancing, Processor Affinity)

บนระบบ multi-core มีประเด็นเพิ่มเติม

1.3.10 ตัวอย่างจริง: Linux CFS, O(1) Scheduler, Windows Scheduler

Linux CFS (Completely Fair Scheduler) ใช้ตั้งแต่ kernel 2.6.23 (2007) ใช้ red-black tree เก็บ process ตาม virtual runtime (vruntime) โดย process ที่ vruntime น้อยที่สุดจะได้ CPU ก่อน ให้ "ความยุติธรรม" ในแง่ของ proportional share

vruntime += Δ t × Wnice0 Wprocess

โดย Δt คือเวลาที่ process เพิ่งใช้ CPU, W_nice0 คือน้ำหนักของ nice=0 (1024), และ W_process คือน้ำหนักของ process ตาม nice value

O(1) Scheduler (kernel 2.6.0-2.6.22) ใช้ 140 priority bitmap ให้การเลือก process เป็น O(1) ไม่ขึ้นกับจำนวน process

EEVDF (Earliest Eligible Virtual Deadline First) - ตั้งแต่ Linux 6.6 (2023) ได้ทยอยแทนที่ CFS

Windows Scheduler ใช้ priority-based preemptive scheduling มี 32 priority level แบ่งเป็น real-time (16-31) และ variable (0-15) มีกลไก priority boost สำหรับ foreground window และ I/O completion


1.4 การซิงโครไนซ์โพรเซส (Process Synchronization)

1.4.1 Critical Section Problem และเงื่อนไข Mutual Exclusion, Progress, Bounded Waiting

Critical Section คือส่วนของโค้ดที่เข้าถึง shared resource (เช่น ตัวแปรร่วม, ไฟล์, database) การ execute พร้อมกันหลาย thread ใน critical section อาจทำให้ข้อมูลผิดพลาด

โครงสร้างทั่วไปของโค้ดที่มี critical section

while (true) {
    // entry section (ขอเข้า)
    
    //  --- critical section ---
    
    // exit section (ออก)
    
    // remainder section (งานอื่น ๆ)
}

เงื่อนไขที่โซลูชันต้องมีครบ 3 ข้อ:

  1. Mutual Exclusion: ณ เวลาหนึ่ง ต้องมี process ไม่เกิน 1 ตัวใน critical section
  2. Progress: ถ้าไม่มีใครอยู่ใน CS และมีหลาย process อยาก enter การตัดสินใจต้องไม่ถูกเลื่อนอย่างไม่มีที่สิ้นสุด
  3. Bounded Waiting: ต้องมีขอบเขตบนของจำนวนครั้งที่ process อื่นเข้าก่อนได้ หลังจากที่ process หนึ่งขอเข้า CS

1.4.2 Race Condition

Race Condition เกิดเมื่อผลลัพธ์ขึ้นกับลำดับการ execute ของ thread ที่ไม่คาดเดาได้

ตัวอย่างคลาสสิก: counter++ ซึ่งดูเหมือนเป็นคำสั่งเดียว แต่จริง ๆ เป็น 3 คำสั่ง:

LOAD  R1, [counter]
ADD   R1, R1, 1
STORE [counter], R1

ถ้า 2 thread interleave กัน ค่าอาจผิดได้

Thread A: LOAD R1 = 5
Thread B: LOAD R1 = 5
Thread A: ADD R1 = 6
Thread B: ADD R1 = 6
Thread A: STORE counter = 6
Thread B: STORE counter = 6   ← ควรเป็น 7!

1.4.3 Software Solution: Peterson's Algorithm, Bakery Algorithm

Peterson's Algorithm (สำหรับ 2 process) - ใช้ตัวแปรร่วม flag[2] และ turn

// Peterson's algorithm สำหรับ 2 process
bool flag[2] = {false, false};
int turn = 0;

// Process i (i = 0 หรือ 1)
void enter_cs(int i) {
    int j = 1 - i;
    flag[i] = true;  // ประกาศว่าต้องการเข้า
    turn = j;        // ให้สิทธิ์อีกฝ่ายก่อน
    while (flag[j] && turn == j) 
        ;  // busy-wait
}

void exit_cs(int i) {
    flag[i] = false;
}

หมายเหตุ: Peterson's algorithm ต้องการ memory ordering ที่ถูกต้อง ในฮาร์ดแวร์สมัยใหม่ต้องใช้ atomic หรือ memory barrier

Bakery Algorithm (Lamport, 1974) ขยาย Peterson ให้รองรับ N process โดยใช้ "บัตรคิว" (number) แบบร้านเบเกอรี่ คนที่ได้เลขน้อยสุดได้เข้าก่อน

1.4.4 Hardware Solution: Test-and-Set, Compare-and-Swap, Atomic Instruction

CPU สมัยใหม่มี atomic instruction ที่อ่าน-แก้-เขียนในคำสั่งเดียวโดยไม่ถูกขัด

Test-and-Set (TAS):

// pseudo-code ของ atomic test-and-set
bool TestAndSet(bool *target) {
    bool old = *target;
    *target = true;
    return old;  // return ค่าเดิม
}

Compare-and-Swap (CAS): พื้นฐานของ lock-free data structure

int CompareAndSwap(int *value, int expected, int new_value) {
    int old = *value;
    if (old == expected)
        *value = new_value;
    return old;
}

บน x86 คำสั่งคือ LOCK CMPXCHG บน ARM ใช้ LDXR/STXR

1.4.5 Semaphore (Binary / Counting) และ Wait/Signal

Semaphore (Dijkstra, 1965) คือจำนวนเต็ม S ที่เข้าถึงได้ด้วยสอง atomic operation

wait(S):
    while (S <= 0) ;    // รอ (จริง ๆ block ไม่ใช่ busy-wait)
    S--;

signal(S):
    S++;

Binary Semaphore (0 หรือ 1) ≈ mutex

Counting Semaphore ใช้ควบคุมจำนวน resource ที่มีจำกัด เช่น connection pool ขนาด 10

1.4.6 Mutex, Spin Lock, Read-Write Lock

Mutex (Mutual Exclusion): เหมือน binary semaphore แต่มี ownership - ผู้ที่ lock ต้องเป็นผู้ unlock (pthread_mutex)

Spin Lock: แทนที่จะ sleep ตอน wait จะ busy-wait (loop) เหมาะเมื่อ critical section สั้นมาก และ context switch แพงกว่าการรอ

Read-Write Lock (RWLock): แยก lock สำหรับอ่าน (shared) และเขียน (exclusive) อ่านพร้อมกันได้หลาย reader แต่เขียนได้ทีละคน และขณะเขียนห้ามอ่าน เหมาะกับข้อมูลที่อ่านบ่อย เขียนน้อย

1.4.7 Monitor และ Condition Variable

Monitor (Hoare, 1974) คือ high-level abstraction ที่รวม shared data + procedure เข้าไว้ด้วยกัน โดย รับประกันว่ามี process เดียวเท่านั้นที่อยู่ใน monitor ในเวลาหนึ่ง

Condition Variable ใช้สำหรับรอเงื่อนไขบางอย่างภายใน monitor

Java synchronized + wait()/notify() และ Python threading.Condition เป็น implementation ของแนวคิดนี้

1.4.8 ปัญหาคลาสสิก: Producer-Consumer, Readers-Writers, Dining Philosophers, Sleeping Barber

Producer-Consumer (Bounded Buffer): producer ใส่ข้อมูล consumer นำออก buffer ขนาดจำกัด ใช้ 3 semaphore

// Producer-Consumer ด้วย semaphore (pseudo-code)
semaphore mutex = 1;     // ป้องกัน buffer
semaphore empty = N;     // จำนวนช่องว่าง
semaphore full = 0;      // จำนวน item

// Producer
while (true) {
    item = produce();
    wait(empty);         // รอช่องว่าง
    wait(mutex);
    put_into_buffer(item);
    signal(mutex);
    signal(full);        // แจ้งว่ามี item
}

// Consumer
while (true) {
    wait(full);          // รอ item
    wait(mutex);
    item = remove_from_buffer();
    signal(mutex);
    signal(empty);       // แจ้งว่ามีช่องว่าง
    consume(item);
}

Readers-Writers: หลาย reader อ่านพร้อมกันได้ แต่ writer ต้อง exclusive มีตัวแปร First/Second/Third problem ตามการจัดลำดับความสำคัญ

Dining Philosophers: 5 นักปราชญ์นั่งล้อมโต๊ะกลม ระหว่างนักปราชญ์มีตะเกียบ 1 คู่ ต้องการ 2 อัน (ซ้าย+ขวา) เพื่อกิน — ตัวอย่างคลาสสิกของ deadlock ถ้าทุกคนหยิบตะเกียบซ้ายพร้อมกัน

Sleeping Barber: ช่างตัดผมและห้องรอที่มีเก้าอี้จำนวนจำกัด ใช้ synchronize การมาของลูกค้ากับการทำงานของช่าง

1.4.9 Deadlock: เงื่อนไข 4 ข้อ (Mutual Exclusion, Hold & Wait, No Preemption, Circular Wait)

Deadlock เกิดเมื่อกลุ่ม process ติดค้างกันโดยแต่ละตัวถือ resource และรอ resource ที่อีกตัวหนึ่งถืออยู่ ต้องเกิดเงื่อนไข 4 ข้อพร้อมกัน (Coffman conditions)

  1. Mutual Exclusion: resource ใช้ได้ทีละคนเท่านั้น
  2. Hold and Wait: process ถือ resource อยู่ขณะรอ resource อื่น
  3. No Preemption: resource ถูกแย่งคืนไม่ได้ ต้องปล่อยเอง
  4. Circular Wait: มีวงกลมของ process ที่รอซึ่งกันและกัน
flowchart LR
    P1["Process P1
ถือ R1, รอ R2"] P2["Process P2
ถือ R2, รอ R1"] R1["Resource R1"] R2["Resource R2"] P1 -->|hold| R1 P2 -->|hold| R2 P1 -.->|request| R2 P2 -.->|request| R1

1.4.10 การจัดการ Deadlock: Prevention, Avoidance (Banker's Algorithm), Detection & Recovery, Ignore (Ostrich)

1) Prevention: ทำลายเงื่อนไข 1 ใน 4 ข้อ

2) Avoidance (Banker's Algorithm) - ตัดสินใจจัดสรรโดยรับประกันว่าระบบจะอยู่ใน safe state เสมอ ต้องรู้ maximum resource need ล่วงหน้า

Need [ i , j ] = Max [ i , j ] - Allocation [ i , j ]

โดย Need[i,j] คือจำนวน resource j ที่ process i ยังต้องการเพิ่ม, Max[i,j] คือความต้องการสูงสุด, และ Allocation[i,j] คือจำนวนที่จัดสรรแล้ว

3) Detection & Recovery - ปล่อยให้ deadlock เกิด แล้วตรวจจับ (resource allocation graph cycle) และแก้ไข (kill process, rollback)

4) Ignore (Ostrich Algorithm) - ทำเป็นไม่รู้ไม่เห็น เหมือนนกกระจอกเทศซุกหัวในทราย! UNIX/Linux/Windows ส่วนใหญ่ใช้วิธีนี้สำหรับ application-level เพราะ deadlock เกิดไม่บ่อย และการ prevent/avoid มี cost สูง


1.5 การจัดการหน่วยความจำ (Memory Management)

1.5.1 Address Binding (Compile-time, Load-time, Execution-time)

Address Binding คือกระบวนการแมป "logical address" ในโปรแกรมไปเป็น "physical address" ในหน่วยความจำจริง เกิดได้ 3 เวลา

  1. Compile-time Binding: รู้ physical address ตั้งแต่คอมไพล์ (absolute code) - ไม่ยืดหยุ่น ใช้ใน embedded เท่านั้น
  2. Load-time Binding: คอมไพล์เป็น relocatable code รู้ physical address ตอน load - ต้อง load ตำแหน่งเดิมทุกครั้ง
  3. Execution-time Binding (Runtime): การแมปเกิดระหว่างรัน ต้องมี hardware ช่วย (MMU) วิธีที่ OS สมัยใหม่ใช้

1.5.2 Logical vs Physical Address และ MMU (Memory Management Unit)

MMU เป็นฮาร์ดแวร์ที่แปลง logical → physical address ใน runtime โดยใช้ page table

flowchart LR
    CPU["CPU
Virtual Address
0x1234"] -->|"VA"| MMU["MMU
+ Page Table"] MMU -->|"PA 0xA234"| RAM["Physical RAM"] TLB["TLB Cache"] <-->|"lookup"| MMU

1.5.3 Contiguous Allocation: First Fit, Best Fit, Worst Fit

การจัดสรรหน่วยความจำแบบ contiguous - process แต่ละตัวอยู่ในบริเวณ memory ต่อเนื่องกัน

Strategy การเลือก hole (ช่องว่าง) ที่จะใส่ process ขนาด n:

ผลการทดลองพบว่า First Fit และ Best Fit ใช้งานดีกว่า Worst Fit

1.5.4 Fragmentation: Internal vs External และการแก้ไข (Compaction)

Compaction: ย้าย process เข้ามาชิดกันเพื่อรวม hole เป็นก้อนใหญ่ แต่ cost สูง (ต้อง dynamic relocation ด้วย)

1.5.5 Paging: Page, Frame, Page Table, TLB (Translation Lookaside Buffer)

Paging แบ่งหน่วยความจำออกเป็นชิ้นเท่า ๆ กัน

Virtual Address = ( p , d ) Physical Address = ( f , d )

โดย p คือ page number, d คือ offset ภายใน page, f = PageTable[p] คือ frame number ส่วน offset d คงเดิม

TLB (Translation Lookaside Buffer) เป็น cache ของ page table ใน MMU เก็บ (page → frame) ที่ใช้บ่อย เพื่อหลีกเลี่ยงการอ่าน page table จาก memory ทุกครั้ง

Effective Access Time (EAT):

EAT = h ( t + m ) + ( 1 - h ) ( t + 2 m )

โดย h = TLB hit rate, t = TLB lookup time, m = memory access time (TLB miss ต้องอ่าน page table 1 ครั้ง แล้วอ่าน data อีก 1 ครั้ง รวม 2m)

1.5.6 Hierarchical Page Table, Hashed Page Table, Inverted Page Table

สำหรับ 64-bit address ถ้าใช้ single-level page table จะมีขนาดมหาศาล จึงใช้โครงสร้างอื่น

1.5.7 Segmentation และ Segmentation with Paging

Segmentation แบ่งโปรแกรมตามหน่วยตรรกะ (segment) ขนาดไม่เท่ากัน เช่น code, data, stack, heap โดย address เป็น (segment_id, offset)

Segmentation with Paging: ใช้ segment กำหนดขอบเขตตรรกะ แล้วแต่ละ segment paging ย่อยภายใน — x86 แบบเดิมใช้วิธีนี้ แต่ x86_64 ยกเลิก segmentation แล้ว (ใช้ flat paging)

1.5.8 Virtual Memory: Demand Paging, Page Fault, Copy-on-Write

Virtual Memory ให้ process เห็น address space ใหญ่กว่า RAM จริงได้ โดย

Effective Access Time สำหรับ demand paging:

EAT = ( 1 - p ) × tma + p × tpf

โดย p คือ probability ของ page fault, t_ma คือ memory access time (เช่น 100 ns), t_pf คือ page fault service time (เช่น 8 ms = 8,000,000 ns) แสดงให้เห็นว่าแม้ page fault rate น้อยมาก ก็ทำให้ระบบช้าลงอย่างมีนัยสำคัญ

1.5.9 Page Replacement Algorithms: FIFO, Optimal, LRU, LFU, Clock (Second Chance)

เมื่อ RAM เต็ม ต้องเลือก page ใดที่จะถูก swap ออก

ตัวอย่าง reference string: 7 0 1 2 0 3 0 4 2 3 0 3 2 มี 3 frames

FIFO: ไล่ page ที่อยู่มานานสุด - ง่ายแต่มี Belady's Anomaly (เพิ่ม frame กลับทำให้ fault มากขึ้น)

Optimal (OPT): ไล่ page ที่จะไม่ถูกใช้นานที่สุดในอนาคต - ทำไม่ได้จริงเพราะต้องรู้อนาคต แต่ใช้เป็น benchmark

LRU (Least Recently Used): ไล่ page ที่ไม่ได้ใช้นานที่สุดในอดีต - ใกล้เคียง OPT implement ด้วย counter หรือ stack

LFU (Least Frequently Used): ไล่ page ที่ใช้น้อยครั้งสุด

Clock (Second Chance): ใช้ reference bit วนดูแบบ circular - ถ้า bit=1 ตั้งเป็น 0 แล้วข้าม ถ้า bit=0 ไล่ตัวนั้น เป็นที่นิยมเพราะใกล้ LRU และ implement ง่าย

Algorithm Page Faults (3 frames) ลักษณะ
FIFO 15 ง่ายสุด มี Belady's Anomaly
Optimal 9 Benchmark ที่ทำไม่ได้
LRU 12 ใกล้ optimal, overhead สูง
Clock 12-13 ใกล้ LRU, overhead ต่ำ

1.5.10 Thrashing, Working Set Model, Page Fault Frequency

Thrashing เกิดเมื่อ process เกิด page fault บ่อยมากจน CPU ใช้เวลาส่วนใหญ่ไป swap pages แทน execute จริง CPU utilization ต่ำ แต่ disk I/O สูงผิดปกติ

flowchart LR
    A["CPU Util ต่ำ"] --> B["OS คิดว่าต้องเพิ่ม
degree of multiprog."] B --> C["โหลด process
เพิ่มเข้า RAM"] C --> D["RAM ไม่พอ
Page Fault ทะลัก"] D --> E["Disk I/O ท่วม
CPU ว่างมากขึ้น"] E --> A

Working Set Model (Denning, 1968) - ในหน้าต่างเวลา Δ page ที่ถูก access คือ working set ของ process นั้น OS ควรเก็บทั้ง working set ไว้ใน RAM ถ้าผลรวมของ working set > RAM จะ thrashing

Page Fault Frequency (PFF): ตรวจ fault rate ถ้าสูงเกินเพดานบน เพิ่ม frame ให้ process ถ้าต่ำเกินเพดานล่าง ลด frame


1.6 การจัดการระบบไฟล์ (File System Management)

1.6.1 File Concept, Attributes, Operations, Type

File คือชุดข้อมูลต่อเนื่องที่มีชื่อและถูกเก็บบน secondary storage

Attributes: name, identifier (inode number), type, location (pointer to data), size, protection (permission), time & date, user/group

Operations: create, write, read, reposition (seek), delete, truncate, open, close

Type: ระบุด้วย extension (.txt, .exe) หรือ magic number ต้นไฟล์ (เช่น ELF = 0x7F 45 4C 46, PNG = 89 50 4E 47)

1.6.2 Access Methods: Sequential, Direct, Indexed

1.6.3 Directory Structure: Single-level, Two-level, Tree, Acyclic Graph, General Graph

flowchart TB
    R["/"] --> H["home"]
    R --> E["etc"]
    R --> V["var"]
    H --> U1["user1"]
    H --> U2["user2"]
    U1 --> D1["docs"]
    U1 --> P1["pics"]
    D1 --> F1["report.pdf"]
    U2 --> L1["link-to-report"] -.->|"symlink"| F1

1.6.4 Mounting และ File Sharing

Mount คือการแปะ filesystem บน device เข้าไปเป็นส่วนหนึ่งของ directory tree ที่มีอยู่

# Mount disk partition /dev/sdb1 เข้าที่ /mnt/data
sudo mount -t ext4 /dev/sdb1 /mnt/data

# ดูทุก filesystem ที่ mount อยู่
mount | column -t
findmnt

# Unmount
sudo umount /mnt/data

ไฟล์ /etc/fstab กำหนด mount อัตโนมัติตอนบูต

# <device>        <mount>   <type> <options>       <dump> <pass>
UUID=xxxx-xxxx    /         ext4   defaults        0      1
UUID=yyyy-yyyy    /home     ext4   defaults        0      2
/dev/sda2         swap      swap   defaults        0      0
tmpfs             /tmp      tmpfs  defaults,size=2G 0     0

1.6.5 File Allocation Methods: Contiguous, Linked, Indexed

Method Allocation ข้อดี ข้อเสีย
Contiguous blocks ต่อเนื่อง random access เร็ว external fragmentation, ขยายยาก
Linked แต่ละ block มี pointer ไปถัดไป ไม่เกิด external frag random access ช้า, ใช้พื้นที่เก็บ pointer
Indexed มี index block เก็บ pointer ของ data block random access เร็ว, ไม่ frag overhead ของ index block

FAT ใช้ linked allocation โดยเก็บ pointer รวมไว้ใน File Allocation Table

ext2/3/4 ใช้ indexed allocation ด้วยโครงสร้าง inode ที่มี direct pointer 12 ตัว + indirect + double indirect + triple indirect

1.6.6 Free Space Management: Bit Vector, Linked List, Grouping, Counting

1.6.7 File System Implementation: Inode, Superblock, Block Group

โครงสร้างของ ext2/3/4 บน Linux

Inode ไม่มีชื่อไฟล์ (ชื่อเก็บใน directory entry) มีเฉพาะ

# ดู inode ของไฟล์
$ stat /etc/passwd
  File: /etc/passwd
  Size: 2856       Blocks: 8          IO Block: 4096   regular file
Device: 259,2  Inode: 131073      Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)  Gid: (    0/    root)

# ดูรายละเอียด inode เต็ม (ext4)
$ sudo debugfs -R "stat <131073>" /dev/nvme0n1p2

1.6.8 Journaling, Copy-on-Write, Log-structured File System

Journaling: เขียน "สมุดบันทึก (journal/log)" ไว้ก่อนว่าจะทำอะไร เมื่อ crash ระหว่างเขียน สามารถ replay หรือ rollback ได้ - ext3/4, NTFS, XFS

Copy-on-Write (CoW): ไม่ overwrite ข้อมูลเดิม แต่เขียนลง block ใหม่ แล้วอัปเดต pointer atomically - Btrfs, ZFS, APFS

Log-structured File System: เขียนทั้ง filesystem เป็น log ต่อเนื่อง - NILFS, F2FS (Flash-friendly)

1.6.9 ตัวอย่างระบบไฟล์: ext4, XFS, Btrfs, ZFS, NTFS, APFS, FAT32/exFAT

Filesystem ประเภท Max File Max Volume Feature เด่น ใช้กับ
FAT32 Legacy 4 GB 2 TB compat. กว้าง USB, SD Card
exFAT Legacy+ 16 EB 128 PB รองรับ flash SD Card ใหญ่
NTFS Journaling 16 EB 256 TB ACL, compression Windows
ext4 Journaling 16 TB 1 EB mature, เร็ว Linux ทั่วไป
XFS Journaling 8 EB 8 EB scalable, parallel RHEL, SUSE
Btrfs CoW 16 EB 16 EB snapshot, subvolume, RAID Fedora, openSUSE
ZFS CoW 16 EB 256 ZB pool, RAID-Z, checksum, dedup FreeBSD, TrueNAS
APFS CoW 8 EB 8 EB snapshot, encryption macOS, iOS
F2FS Log-struct 3.94 TB 16 TB flash-optimized Android

1.7 ระบบ I/O และอุปกรณ์ต่อพ่วง (I/O System)

1.7.1 I/O Hardware: Port, Bus, Device Controller

1.7.2 Polling, Interrupt-driven I/O, DMA

Polling: CPU วนถาม device ว่าพร้อมหรือยัง - เปลือง CPU แต่ latency ต่ำ

while (!(device_status & READY)) 
    ;  // busy-wait
data = device_data;

Interrupt-driven I/O: CPU ทำงานอื่น เมื่อ device พร้อม จะส่ง interrupt CPU จึง handle

sequenceDiagram
    participant CPU
    participant Dev as Device Ctrl
    participant ISR as Interrupt Handler

    CPU->>Dev: start I/O
    Note over CPU: ทำงานอื่นต่อ
    Dev->>Dev: กำลังทำงาน...
    Dev->>CPU: INT (interrupt)
    CPU->>ISR: jump to ISR vector
    ISR->>Dev: read status + data
    ISR-->>CPU: iret (return)
    Note over CPU: ทำงานเดิมต่อ

DMA (Direct Memory Access): สำหรับถ่ายข้อมูลปริมาณมาก DMA controller ย้ายข้อมูล RAM ↔ device โดยไม่รบกวน CPU เสร็จแล้วค่อย interrupt - ใช้ใน disk, network, GPU

1.7.3 Device Driver และ Kernel I/O Subsystem

Device Driver คือโมดูลใน kernel (หรือ user space สำหรับ microkernel) ที่รู้จักรายละเอียดของ device แต่ละรุ่น ทำหน้าที่แปลง generic I/O request ของ kernel ให้เป็นคำสั่งเฉพาะของ device

Linux แบ่ง device เป็น

ตัวอย่าง Kernel Module Hello World:

// hello_mod.c - ตัวอย่าง kernel module ง่าย ๆ
// คอมไพล์ด้วย Makefile สำหรับ out-of-tree module
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Moo");
MODULE_DESCRIPTION("ตัวอย่าง Hello World kernel module");

// ฟังก์ชันที่เรียกเมื่อ load module
static int __init hello_init(void) {
    printk(KERN_INFO "[hello_mod] โหลดโมดูลเรียบร้อย\n");
    return 0;  // 0 = สำเร็จ
}

// ฟังก์ชันที่เรียกเมื่อ unload module
static void __exit hello_exit(void) {
    printk(KERN_INFO "[hello_mod] ถอนโมดูลเรียบร้อย\n");
}

module_init(hello_init);
module_exit(hello_exit);

Makefile:

obj-m += hello_mod.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

การใช้งาน:

make
sudo insmod hello_mod.ko    # load
sudo dmesg | tail -5        # ดู log: [hello_mod] โหลดโมดูลเรียบร้อย
lsmod | grep hello_mod
sudo rmmod hello_mod        # unload
sudo dmesg | tail -5        # [hello_mod] ถอนโมดูลเรียบร้อย

1.7.4 Buffering, Caching, Spooling

1.7.5 Disk Scheduling: FCFS, SSTF, SCAN, C-SCAN, LOOK, C-LOOK

ดิสก์แบบจานหมุนมี seek time (เวลาเลื่อนหัวอ่าน) เป็น bottleneck การจัดลำดับ request สำคัญมาก

ตัวอย่าง: queue = 98, 183, 37, 122, 14, 124, 65, 67 เริ่มที่ head = 53

FCFS: ไล่ตามคิว → total head movement = 640

SSTF (Shortest Seek Time First): เลือก request ใกล้ head ปัจจุบันสุด → 236 (อาจเกิด starvation)

SCAN (Elevator): ไปทางเดียวจนสุดแล้วกลับ → 236

C-SCAN: คล้าย SCAN แต่พอถึงสุดทาง jump กลับต้น ไม่ service ขากลับ

LOOK / C-LOOK: เหมือน SCAN/C-SCAN แต่ไม่จำเป็นต้องไปจริง ๆ ถึงสุดทาง พอไม่มี request ก็กลับเลย

Algorithm Head Movement ลักษณะ
FCFS 640 ง่าย fair
SSTF 236 เร็วเฉลี่ย starvation
SCAN 236 fair ดี ใช้มากใน HDD
C-SCAN 386 fair มากกว่า SCAN
LOOK 208 เร็วกว่า SCAN
C-LOOK 322 fair + ไม่ไปเกิน

สำหรับ SSD ที่ไม่มี seek time อัลกอริทึมเหล่านี้ไม่สำคัญเท่า HDD แต่ OS ยังใช้ scheduler (mq-deadline, kyber, bfq, none) เพื่อ merge request และควบคุม fairness ระหว่าง process

# ดู I/O scheduler ปัจจุบันและเปลี่ยน
$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq

# เปลี่ยนชั่วคราว
$ echo mq-deadline | sudo tee /sys/block/nvme0n1/queue/scheduler

# เปลี่ยนถาวรผ่าน udev rule
$ sudo tee /etc/udev/rules.d/60-ioschedulers.rules <<EOF
ACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"
ACTION=="add|change", KERNEL=="sd[a-z]|mmcblk[0-9]*", ATTR{queue/scheduler}="bfq"
EOF

สรุปบทที่ 1

บทที่ 1 ได้ปูพื้นฐานของระบบปฏิบัติการครบทุกหัวข้อสำคัญ โดยเริ่มจากนิยามและบทบาทของ OS ในฐานะ resource manager และ extended machine วิวัฒนาการ 5 ยุค ประเภทและสถาปัตยกรรม kernel แบบต่าง ๆ ตามด้วยการจัดการโพรเซสและ thread การจัดตารางเวลา CPU (FCFS, SJF, RR, MLFQ, CFS) การซิงโครไนซ์ (semaphore, mutex, monitor, deadlock) การจัดการหน่วยความจำ (paging, virtual memory, page replacement) ระบบไฟล์ (inode, journaling, CoW) และระบบ I/O (DMA, device driver, disk scheduling) ทั้งหมดนี้จะเป็นรากฐานสำหรับเนื้อหาในบทถัดไปซึ่งจะเจาะจงไปที่ Linux และเครื่องมือพัฒนาซอฟต์แวร์ต่าง ๆ