- อธิบายโครงสร้างหน่วยความจำของโปรเซสบนลินุกซ์ในระดับการทำงานจริง พร้อมไล่ความสัมพันธ์ระหว่าง virtual address space และหน่วยความจำจริงทีละขั้น
- อธิบายอย่างเป็นรูปธรรมว่าโปรเซสเป็นเจ้าของและเข้าถึงหน่วยความจำอย่างไร โดยเน้นกลไกสำคัญอย่าง page table, VMA, mmap, page fault, CoW
- แนะนำวิธีสังเกต สถานะหน่วยความจำรายโปรเซส ผ่านระบบไฟล์
/proc และบทบาทของเครื่องมือวิเคราะห์ขั้นสูงอย่าง pagemap, kpageflags
- ครอบคลุมการปรับแต่งประสิทธิภาพและเทคนิค dirty tracking ใน user space ผ่านความสามารถใหม่ของเคอร์เนลอย่าง Transparent Huge Pages(THP), userfaultfd, PAGEMAP_SCAN
- อธิบายหลักการออกแบบเคอร์เนลด้านความปลอดภัยและประสิทธิภาพ เช่น มาตรการรับมือ Meltdown ด้วย PTI, TLB flush, นโยบาย W^X เพื่อให้เข้าใจภาพรวมของการจัดการหน่วยความจำบนลินุกซ์
โครงสร้างพื้นฐานของหน่วยความจำโปรเซส
- เมื่อโปรแกรมเริ่มทำงาน จะดูเหมือนมีหน่วยความจำต่อเนื่องขนาดใหญ่อยู่ก้อนหนึ่ง แต่จริง ๆ แล้ว เคอร์เนลลินุกซ์จะประกอบมันขึ้นแบบไดนามิกเป็นหน่วยหน้า (page)
- CPU จะค้น page table เพื่อแปลง virtual address ไปเป็น physical frame
- ถ้าไม่มี mapping จะเกิด page fault และเคอร์เนลจะจัดสรรหน้าใหม่หรือคืนค่าข้อผิดพลาด
- หาก RAM จริงไม่พอ เคอร์เนลจะย้ายหน้าที่ไม่ได้ใช้งานไปยังดิสก์ หรือนำ file page ออกเพื่อคืนพื้นที่
/proc คือ ระบบไฟล์เสมือน ที่เคอร์เนลสร้างไว้ในหน่วยความจำ เพื่อเปิดเผยสถานะของโปรเซสและเคอร์เนลในรูปแบบไฟล์
Address space และ VMA
- แต่ละโปรเซสมี ออบเจ็กต์ address space หนึ่งตัว และภายในประกอบด้วย VMA (Virtual Memory Area) หลายส่วน
- VMA คือช่วง address ต่อเนื่องที่มีสิทธิ์เหมือนกัน (R/W/X) และใช้ backend เดียวกัน (anonymous memory หรือไฟล์)
- page table คือโครงสร้างที่ฮาร์ดแวร์อ้างอิง โดยเก็บ ข้อมูล mapping (PTE) ระหว่าง virtual page กับ physical page
- การเปลี่ยนแปลง address space ทำผ่าน system call 3 ตัว
mmap: สร้างพื้นที่ใหม่
mprotect: เปลี่ยนสิทธิ์
munmap: ลบ mapping
- page มี ขนาดพื้นฐาน 4KiB และบางระบบรองรับ page ขนาดใหญ่ 2MiB·1GiB ด้วย
ดูโครงสร้างหน่วยความจำผ่าน /proc/self/maps
- ใช้คำสั่ง
cat /proc/self/maps เพื่อตรวจสอบ memory map ของโปรเซสได้
- จะแสดงโค้ด/ข้อมูล/bss ของไฟล์ปฏิบัติการ, heap, anonymous mapping, shared library, stack เป็นต้น
- พื้นที่
[vdso] และ [vvar] คือ โค้ดและข้อมูลสำหรับ system call แบบความเร็วสูง ที่เคอร์เนล map เข้ามา
หลักการทำงานของ mmap
mmap ไม่ใช่การจองหน่วยความจำจริงทันที แต่เป็น การบันทึกคำมั่นสัญญาต่อ address space
- page จะถูกจัดสรรเมื่อมีการเข้าถึงครั้งแรก
- เมื่อต้อง map ไฟล์
offset ต้องจัดแนวตาม page และถ้าเข้าถึงเกินท้ายไฟล์จะเกิด SIGBUS
MAP_SHARED จะสะท้อนไปยังไฟล์โดยตรง ส่วน MAP_PRIVATE จะสร้าง page แยกผ่าน copy-on-write (CoW) เมื่อมีการเขียน
MAP_FIXED_NOREPLACE ช่วยให้ล้มเหลวหากมี mapping อยู่แล้วที่ address ที่ระบุ จึงปลอดภัยกว่า
การเข้าถึงครั้งแรกและ page fault
- เมื่อเข้าถึง mapping ใหม่เป็นครั้งแรก หาก CPU หา page table entry ไม่พบ จะเกิด page fault
- เคอร์เนลจะตรวจสอบความถูกต้องของ address, สิทธิ์การเข้าถึง และการมีอยู่ของมัน
- ถ้าเป็น anonymous mapping จะจัดสรร page ใหม่ที่เติมค่า 0 ไว้ ถ้าเป็น file mapping จะอ่านจาก page cache
- minor fault คือกรณีข้อมูลอยู่ใน RAM อยู่แล้ว ส่วน major fault คือกรณีต้องทำ disk I/O
- stack ได้รับการป้องกันด้วย guard page ดังนั้นถ้าเข้าถึงต่ำลงไปมากเกินจะเกิด
SIGSEGV
fork() และ Copy-on-Write ของ MAP_PRIVATE
- ตอน
fork โปรเซสแม่และลูกจะ แชร์ physical page เดียวกัน และถูกทำเครื่องหมายให้อ่านอย่างเดียว
- จะคัดลอก page ใหม่เพื่อแยกจากกันก็ต่อเมื่อมีการเขียนเท่านั้น
- file mapping แบบ
MAP_PRIVATE ก็ทำงานด้วยหลักการเดียวกัน
- ตัวเลือกที่เกี่ยวข้อง
vfork: แชร์ address space ของโปรเซสแม่
clone(CLONE_VM): สร้างเธรด
MADV_DONTFORK, MADV_WIPEONFORK: ไม่ส่งต่อ mapping ไปยังโปรเซสลูก หรือรีเซ็ตเป็น 0 ในโปรเซสลูก
การเปลี่ยนสิทธิ์และการทำให้ TLB เป็นโมฆะ
- เมื่อเปลี่ยนสิทธิ์ของ page ด้วย
mprotect เคอร์เนลจะ แยก VMA และแก้ไข page table จากนั้นจึง ทำให้ TLB เป็นโมฆะ
- ตาม นโยบาย W^X page จะเขียนได้และรันได้พร้อมกันไม่ได้
- TLB (Translation Lookaside Buffer) คือแคชของการแปลง address ล่าสุด ซึ่งการทำให้เป็นโมฆะอาจทำให้เกิดความหน่วงชั่วคราว
การสังเกตอย่างละเอียดผ่าน /proc
- ใช้
/proc/<pid>/maps, smaps, smaps_rollup เพื่อตรวจดูสิทธิ์, RSS และการใช้ HugePage ของแต่ละพื้นที่
/proc/<pid>/pagemap ให้สถานะแบบราย page (มีอยู่, swap, PFN ฯลฯ) แต่ PFN ถูกซ่อนไม่ให้ผู้ใช้ทั่วไปเห็น
/proc/kpagecount, /proc/kpageflags แสดงจำนวน mapping ต่อ PFN และคุณสมบัติของ page (anonymous, file, dirty ฯลฯ)
- ใช้
mincore, SEEK_DATA/SEEK_HOLE เพื่อระบุช่วงข้อมูล/รูของ sparse file ได้
- สามารถผสาน
PAGEMAP_SCAN กับ userfaultfd เพื่อทำ dirty tracking ใน user space ได้
Transparent Huge Pages (THP) และ mTHP
- THP จะรวมหน่วยความจำที่ถูกเข้าถึงบ่อยให้เป็น page ขนาดใหญ่โดยอัตโนมัติ (เช่น 2MiB) เพื่อ เพิ่มประสิทธิภาพของ TLB
- เธรด
khugepaged จะรวม page ที่อยู่ติดกันเข้าด้วยกัน
- mTHP รองรับ large page แบบขนาดยืดหยุ่น (folio) หลายขนาด เช่น 16KiB·64KiB
- ตรวจสอบการใช้งานได้จาก
AnonHugePages, FilePmdMapped ใน /proc/self/smaps
- จัดการค่าระดับทั้งระบบได้ที่
/sys/kernel/mm/transparent_hugepage/
- ควบคุมรายพื้นที่ได้ด้วย
MADV_HUGEPAGE, MADV_NOHUGEPAGE
Dirty tracking ใน user space
- ใช้
userfaultfd และ PAGEMAP_SCAN เพื่อ คัดลอกเฉพาะ page ที่เปลี่ยนแปลงแล้ว ได้
- เคอร์เนลจะสแกนและใส่การป้องกันการเขียนใน atomic operation ครั้งเดียว
- มีประสิทธิภาพสำหรับงานอย่าง snapshot และ live migration
กลไก TLB flush
- บน x86 การทำให้ TLB เป็นโมฆะมี 2 วิธี
INVLPG: ทำให้ page เดียวเป็นโมฆะ
- reload page table root เพื่อ flush ทั้งหมด
PCID และ INVPCID ใช้ จัดการ TLB tag แยกรายโปรเซส เพื่อลดการ flush ที่ไม่จำเป็น
tlb_single_page_flush_ceiling คือค่า threshold ที่เคอร์เนลใช้ตัดสินใจระหว่างการ flush ราย page กับ flush ทั้งหมด
การรับมือ Meltdown: Page Table Isolation (PTI)
- Meltdown คือช่องโหว่ที่ข้อมูลของเคอร์เนลอาจรั่วผ่านแคชได้ระหว่าง speculative execution
- ลินุกซ์รับมือด้วย PTI (Page Table Isolation) โดยแยก address space ของผู้ใช้และเคอร์เนลออกจากกัน
- ตอนเข้าสู่เคอร์เนลจะสลับ
CR3 เพื่อใช้ page table ที่มีเฉพาะเคอร์เนล
- ใช้
PCID เพื่อลดการ flush ของ TLB ให้น้อยที่สุด
- โดยปกติจะเปิดใช้งานอยู่แล้ว และปิดได้ด้วย
nopti
ขั้นตอนที่ปลอดภัยของเคอร์เนลเมื่อเปลี่ยน mapping
- ลำดับเมื่อมีการเปลี่ยน mapping
- จัดการกฎของแคช
- แก้ไข page table
- ทำให้ TLB เป็นโมฆะ
- แม้แต่ mapping ภายในเคอร์เนล (
vmap, vmalloc) ก็จะซิงก์แคชและ TLB ก่อนและหลัง I/O
- บางสถาปัตยกรรมต้อง flush instruction cache หลังคัดลอกโค้ด
โครงสร้าง stack และการเรียกฟังก์ชันบน x86
- ในโหมด 64 บิตจะใช้รีจิสเตอร์ RIP, RSP, RBP และ stack จะเติบโตลงด้านล่าง
- ตาม System V AMD64 ABI การส่งอาร์กิวเมนต์ใช้ RDI, RSI, RDX, RCX, R8, R9 และค่าที่คืนกลับอยู่ใน RAX
- user mode คือ ring 3 ส่วนเคอร์เนลคือ ring 0 โดย system call และ interrupt จะสลับผ่าน gate
สถานการณ์ผิดพลาดและการวินิจฉัย
mmap → EINVAL: จัดแนว file offset ผิด
mmap → ENOMEM: virtual space ไม่พอ หรือชนข้อจำกัด overcommit
- เข้าถึง file mapping แล้วได้
SIGBUS: เข้าถึงเกิน EOF
mprotect(PROT_EXEC) → EACCES: mount แบบ noexec หรือนโยบาย W^X
- RSS เพิ่มหลัง
fork(): เกิดจากการคัดลอก page แบบ CoW
- เขียนทับ mapping เดิมด้วย
MAP_FIXED → แนะนำ MAP_FIXED_NOREPLACE
เช็กลิสต์สำหรับใช้งานจริง
- ต้องการจองหน่วยความจำทันที:
mmap + PROT_READ|PROT_WRITE + MAP_PRIVATE|MAP_ANONYMOUS
- เวลาสร้างโค้ด: รักษา W^X และใช้
mprotect(PROT_READ|PROT_EXEC)
- เมื่อต้อง map ไฟล์: จัด
offset ให้ตรง page และห้ามเข้าถึงเกิน EOF
- ถ้า page fault เยอะ: ใช้
MADV_WILLNEED หรือ pre-touch
- วิเคราะห์การใช้หน่วยความจำ:
/proc/<pid>/smaps_rollup → /proc/<pid>/maps
fork โปรเซสขนาดใหญ่: คำนึงถึง CoW และใช้ exec ในโปรเซสลูก
- สภาพแวดล้อมที่ไวต่อ latency: สังเกต THP/mTHP,
mlock, และพฤติกรรมของ TLB
ยังไม่มีความคิดเห็น