สำรวจหน่วยความจำของโปรเซสบนลินุกซ์อย่างเป็นมิตร
(0xkato.xyz)- อธิบายโครงสร้างหน่วยความจำของโปรเซสบนลินุกซ์ในระดับการทำงานจริง พร้อมไล่ความสัมพันธ์ระหว่าง 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
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ชอบบทอธิบายสั้นๆแบบนี้มาก
ถึงจะเป็นเรื่องที่รู้อยู่แล้ว ก็ยังช่วยให้ได้ทบทวนอีกครั้งระหว่างอ่าน
พอเห็นคำอย่าง “mmap, without the fog” แล้วให้ความรู้สึกเหมือนเป็นบทความที่มี LLM มาช่วยเขียน เลยรู้สึกกังวลและหงุดหงิดขึ้นมาเฉยๆ
แถมมีสำนวนแปลกๆอย่าง “without the fog” อยู่ด้วย เลยยิ่งให้ความรู้สึกเหมือนมี chatgpt ช่วยเขียน
พอเห็นพูดถึง Instruction pipelining ก็ทำให้นึกอยากกลับไปยุคสถาปัตยกรรมเรียบง่ายแบบ 6502 สมัยก่อน
ตอนนั้นมันทำงานแบบ “ตรงไปตรงมา” โดยไม่มีการแมปหรือพร็อกซีที่ซับซ้อน
ถ้ามี interconnect ที่เร็วพอ ก็ดูเหมือนยังพอจะฝันถึงความเรียบง่ายแบบนั้นได้อีกครั้ง
แต่พอเห็นปัญหาอย่าง Meltdown กับ Spectre ก็ชัดเจนเหมือนกันว่าความซับซ้อนที่เพิ่มขึ้นต้องแลกมาด้วยอะไร
ตอนนี้ที่กฎของมัวร์เริ่มชนขีดจำกัดแล้ว ก็อดสงสัยไม่ได้ว่าการแลกเปลี่ยนด้านความซับซ้อน**แบบนี้ยังเป็นทางเลือกที่ดีที่สุดอยู่หรือเปล่า
ไม่คิดว่าความเรียบง่ายจะดีกว่าเสมอไป
มีข้อความขึ้นมาว่าเว็บไซต์นี้ถูกบล็อกเพราะเป็นโดเมนที่อันตรายหรือไม่ปลอดภัย
ดูจากผลการตรวจของ VirusTotalแล้วก็ไม่มีปัญหาอะไร
สงสัยว่าที่บอกว่ารายงานข้อผิดพลาดเป็นแค่ “noise” นั้นหมายความว่ายังไง