• อธิบายโครงสร้างหน่วยความจำของโปรเซสบนลินุกซ์ในระดับการทำงานจริง พร้อมไล่ความสัมพันธ์ระหว่าง 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
    1. จัดการกฎของแคช
    2. แก้ไข page table
    3. ทำให้ 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

สถานการณ์ผิดพลาดและการวินิจฉัย

  • mmapEINVAL: จัดแนว file offset ผิด
  • mmapENOMEM: 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

ยังไม่มีความคิดเห็น

ยังไม่มีความคิดเห็น