
บทความนี้นำเสนอแนวคิดพื้นฐานของระบบปฏิบัติการ (Operating System) ตั้งแต่ประวัติ วิวัฒนาการ สถาปัตยกรรม Kernel การจัดการโพรเซส หน่วยความจำ ไฟล์ระบบ และ I/O พร้อมตัวอย่างโค้ดที่ใช้งานได้จริงบน Linux เพื่อเป็นพื้นฐานสำหรับการพัฒนาซอฟต์แวร์ระดับระบบ
ระบบปฏิบัติการ (Operating System: OS) คือซอฟต์แวร์ระบบ (system software) ที่ทำหน้าที่เป็นตัวกลางระหว่างฮาร์ดแวร์คอมพิวเตอร์ (computer hardware) และผู้ใช้งาน (user) รวมถึงโปรแกรมประยุกต์ (application program) ต่าง ๆ โดย OS จะบริหารจัดการทรัพยากรของระบบ เช่น หน่วยประมวลผลกลาง (CPU) หน่วยความจำ (memory) อุปกรณ์ต่อพ่วง (I/O device) และระบบไฟล์ (file system) ให้ผู้ใช้งานสามารถใช้งานทรัพยากรเหล่านี้ได้อย่างสะดวก มีประสิทธิภาพ และปลอดภัย
ระบบปฏิบัติการมีมุมมองสองด้านที่สำคัญ ซึ่ง 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
วิวัฒนาการของ 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
| ประเภท 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
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
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 ทาง
read(), write()เมื่ออยู่ใน Kernel Mode CPU สามารถ:
CLI, STI, HLT, การเขียน CR0-CR4IN, OUT)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 +++
ลำดับการบูต (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"]
ขั้นตอนโดยละเอียด
.efi จาก EFI System Partition (ESP, มักเป็น /boot/efi) แล้วโหลด GRUB หรือ systemd-bootvmlinuz-*) และ initial ramdisk (initramfs-* หรือ initrd.img)Init System ที่พบบ่อย
.service, .timer, .socket, .target)/etc/rc.d/ (ปัจจุบันแทบไม่ใช้แล้ว)ตัวอย่าง 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
Process (โพรเซส) คือโปรแกรมที่กำลังทำงาน (program in execution) ซึ่งต่างจาก program ที่เป็นเพียงไฟล์บน disk โพรเซสประกอบด้วย:
malloc(), new
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:
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 --> [*]
คำอธิบายแต่ละสถานะ
wait() รับค่า exit statusบน Linux สามารถดูสถานะของ process ได้ด้วย ps ตัวอักษรที่ปรากฏ:
R = Running/RunnableS = Interruptible Sleep (waiting)D = Uninterruptible Sleep (มัก block ที่ I/O disk)Z = ZombieT = Stopped (ถูก SIGSTOP)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"]
บน 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)
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
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
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!
เมื่อออกแบบ CPU scheduler เราต้องพิจารณาเป้าหมายหลายอย่างพร้อมกันซึ่งอาจขัดแย้งกัน
สมการความสัมพันธ์พื้นฐาน
โดย T คือ turnaround time, t_completion คือเวลาที่ process จบ และ t_arrival คือเวลาที่ process เข้าระบบ
โดย W คือ waiting time, t_burst คือเวลาที่ process ต้องการ CPU (CPU burst time)
โดย W̄ คือ average waiting time, W_i คือ waiting time ของ process i, และ n คือจำนวน process ทั้งหมด
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 ยาวตัวหน้า
SJF เลือก process ที่มี burst time สั้นที่สุดก่อน พิสูจน์ได้ว่าให้ average waiting time ต่ำสุด (optimal) แต่ปัญหาคือเราไม่รู้ burst time ล่วงหน้า ต้อง predict จากประวัติ
โดย τ_(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
กำหนด priority number ให้แต่ละ process (ตัวเลขเล็ก = priority สูงในหลายระบบ เช่น Linux nice -20 ถึง +19) process priority สูงสุดได้ CPU ก่อน
ปัญหา Starvation (Indefinite Blocking): process priority ต่ำอาจไม่ได้ทำงานเลยหาก process priority สูงมาเรื่อย ๆ
แก้ด้วย Aging: เพิ่ม priority ของ process ที่รอนานขึ้นเรื่อย ๆ เช่น ทุก 1 วินาที เพิ่ม priority +1
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
แบ่ง ready queue เป็นหลายระดับตามประเภทของ process เช่น
แต่ละ queue ใช้อัลกอริทึมของตัวเอง (เช่น foreground = RR, background = FCFS) และมีนโยบายระหว่าง queue (fixed priority / time slicing)
พัฒนาจาก 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 ล่าง
บนระบบ multi-core มีประเด็นเพิ่มเติม
taskset, sched_setaffinity())Linux CFS (Completely Fair Scheduler) ใช้ตั้งแต่ kernel 2.6.23 (2007) ใช้ red-black tree เก็บ process ตาม virtual runtime (vruntime) โดย process ที่ vruntime น้อยที่สุดจะได้ CPU ก่อน ให้ "ความยุติธรรม" ในแง่ของ proportional share
โดย Δ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
Critical Section คือส่วนของโค้ดที่เข้าถึง shared resource (เช่น ตัวแปรร่วม, ไฟล์, database) การ execute พร้อมกันหลาย thread ใน critical section อาจทำให้ข้อมูลผิดพลาด
โครงสร้างทั่วไปของโค้ดที่มี critical section
while (true) {
// entry section (ขอเข้า)
// --- critical section ---
// exit section (ออก)
// remainder section (งานอื่น ๆ)
}
เงื่อนไขที่โซลูชันต้องมีครบ 3 ข้อ:
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!
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) แบบร้านเบเกอรี่ คนที่ได้เลขน้อยสุดได้เข้าก่อน
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
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
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 แต่เขียนได้ทีละคน และขณะเขียนห้ามอ่าน เหมาะกับข้อมูลที่อ่านบ่อย เขียนน้อย
Monitor (Hoare, 1974) คือ high-level abstraction ที่รวม shared data + procedure เข้าไว้ด้วยกัน โดย รับประกันว่ามี process เดียวเท่านั้นที่อยู่ใน monitor ในเวลาหนึ่ง
Condition Variable ใช้สำหรับรอเงื่อนไขบางอย่างภายใน monitor
wait(c) - ปล่อย monitor แล้วรอsignal(c) - ปลุก process หนึ่งที่รอ cbroadcast(c) - ปลุกทุก process ที่รอJava synchronized + wait()/notify() และ Python threading.Condition เป็น implementation ของแนวคิดนี้
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 การมาของลูกค้ากับการทำงานของช่าง
Deadlock เกิดเมื่อกลุ่ม process ติดค้างกันโดยแต่ละตัวถือ resource และรอ resource ที่อีกตัวหนึ่งถืออยู่ ต้องเกิดเงื่อนไข 4 ข้อพร้อมกัน (Coffman conditions)
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) Prevention: ทำลายเงื่อนไข 1 ใน 4 ข้อ
2) Avoidance (Banker's Algorithm) - ตัดสินใจจัดสรรโดยรับประกันว่าระบบจะอยู่ใน safe state เสมอ ต้องรู้ maximum resource need ล่วงหน้า
โดย 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 สูง
Address Binding คือกระบวนการแมป "logical address" ในโปรแกรมไปเป็น "physical address" ในหน่วยความจำจริง เกิดได้ 3 เวลา
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
การจัดสรรหน่วยความจำแบบ contiguous - process แต่ละตัวอยู่ในบริเวณ memory ต่อเนื่องกัน
Strategy การเลือก hole (ช่องว่าง) ที่จะใส่ process ขนาด n:
ผลการทดลองพบว่า First Fit และ Best Fit ใช้งานดีกว่า Worst Fit
Compaction: ย้าย process เข้ามาชิดกันเพื่อรวม hole เป็นก้อนใหญ่ แต่ cost สูง (ต้อง dynamic relocation ด้วย)
Paging แบ่งหน่วยความจำออกเป็นชิ้นเท่า ๆ กัน
โดย 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):
โดย h = TLB hit rate, t = TLB lookup time, m = memory access time (TLB miss ต้องอ่าน page table 1 ครั้ง แล้วอ่าน data อีก 1 ครั้ง รวม 2m)
สำหรับ 64-bit address ถ้าใช้ single-level page table จะมีขนาดมหาศาล จึงใช้โครงสร้างอื่น
Segmentation แบ่งโปรแกรมตามหน่วยตรรกะ (segment) ขนาดไม่เท่ากัน เช่น code, data, stack, heap โดย address เป็น (segment_id, offset)
Segmentation with Paging: ใช้ segment กำหนดขอบเขตตรรกะ แล้วแต่ละ segment paging ย่อยภายใน — x86 แบบเดิมใช้วิธีนี้ แต่ x86_64 ยกเลิก segmentation แล้ว (ใช้ flat paging)
Virtual Memory ให้ process เห็น address space ใหญ่กว่า RAM จริงได้ โดย
fork() ไม่ copy page จริง แต่ให้ parent/child share pages แบบ read-only เมื่อใครเขียน จะเกิด page fault แล้วค่อย copyEffective Access Time สำหรับ demand paging:
โดย 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 น้อยมาก ก็ทำให้ระบบช้าลงอย่างมีนัยสำคัญ
เมื่อ 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 ต่ำ |
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
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)
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
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
| 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
โครงสร้างของ 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
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)
| 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 |
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
Device Driver คือโมดูลใน kernel (หรือ user space สำหรับ microkernel) ที่รู้จักรายละเอียดของ device แต่ละรุ่น ทำหน้าที่แปลง generic I/O request ของ kernel ให้เป็นคำสั่งเฉพาะของ device
Linux แบ่ง device เป็น
/dev/tty0/dev/sdaตัวอย่าง 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] ถอนโมดูลเรียบร้อย
ดิสก์แบบจานหมุนมี 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 ได้ปูพื้นฐานของระบบปฏิบัติการครบทุกหัวข้อสำคัญ โดยเริ่มจากนิยามและบทบาทของ 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 และเครื่องมือพัฒนาซอฟต์แวร์ต่าง ๆ