- อธิบายขั้นตอนการสร้าง “ลินุกซ์ดิสโทรขนาดจิ๋ว” แบบเป็นลำดับ โดย คอมไพล์ลินุกซ์เคอร์เนลด้วยตัวเองและประกอบ user space ขั้นต่ำ
- กล่าวถึง บทบาทของเคอร์เนลระบบปฏิบัติการ องค์ประกอบของลินุกซ์ดิสโทร และความสัมพันธ์ระหว่างเคอร์เนลกับ user space ตั้งแต่พื้นฐาน
- ใช้สถาปัตยกรรม RISC-V (เครื่อง
riscv64 virt ของ QEMU) เป็นตัวอย่าง แต่ หลักการเดียวกันสามารถนำไปใช้กับสถาปัตยกรรมอื่นอย่าง x86 ได้เช่นกัน
- สร้าง สภาพแวดล้อมลินุกซ์ขั้นต่ำที่สามารถรันได้จริง ซึ่งมี
init process, initramfs และเชลล์อย่างง่ายที่เขียนด้วย Go
- ปิดท้ายด้วยการแนะนำวิธีสร้าง ไมโครดิสโทรที่ใช้งานได้จริง ด้วยโปรเจ็กต์
u-root และสรุปเป็น คู่มือเริ่มต้นที่ช่วยให้เข้าใจโครงสร้างโดยรวมของระบบลินุกซ์
เคอร์เนลของระบบปฏิบัติการคืออะไร
- เคอร์เนลคือองค์ประกอบหลักของระบบปฏิบัติการที่ทำหน้าที่ จัดการทรัพยากรฮาร์ดแวร์และควบคุมการทำงานของโปรแกรม
- แม้อยู่ในสภาพแวดล้อมแบบคอร์เดียว ก็มี ความสามารถในการจัดการ multitasking ที่ทำให้ดูเหมือนหลายโปรแกรมกำลังทำงานพร้อมกัน
- เคอร์เนลทำหน้าที่ abstract การควบคุมอุปกรณ์อินพุต/เอาต์พุต เพื่อให้แอปพลิเคชันไม่ต้องจัดการรายละเอียดอย่างที่อยู่ฮาร์ดแวร์หรือค่ารีจิสเตอร์โดยตรง
- ตัวอย่างเช่น โปรแกรมเพียงแค่สั่งว่า “ให้เขียนข้อความไปยัง standard output” แล้วเคอร์เนลจะจัดการการสื่อสารกับฮาร์ดแวร์จริงเอง
- เคอร์เนลมอบ อินเทอร์เฟซระบบไฟล์ เพื่อให้เข้าถึงข้อมูลในระดับสูง
- ไฟล์ไม่ได้เป็นแค่ข้อมูลบนดิสก์ แต่ทำหน้าที่เป็น logical interface สำหรับการสื่อสารกับเคอร์เนลด้วย
- เคอร์เนลมี โมเดลการแยกและการสื่อสารระหว่างโปรเซส ทำให้แต่ละแอปพลิเคชันทำงานแยกจากกันหรือทำงานร่วมกันได้
- Linux kernel เป็นโอเพนซอร์ส รองรับสถาปัตยกรรมหลากหลาย และเป็นหนึ่งในเคอร์เนลที่ถูกใช้งานแพร่หลายที่สุดในโลก
ลินุกซ์ดิสโทรคืออะไร
- ลินุกซ์เคอร์เนลเพียงอย่างเดียวไม่เพียงพอให้ผู้ใช้รันเว็บเบราว์เซอร์หรือแอป GUI ได้ และต้องมี โครงสร้างพื้นฐานซอฟต์แวร์หลายชั้นเหนือเคอร์เนล
- งานอย่างการตั้งค่าเครือข่าย การกำหนด IP หรือการจัดการ VPN เป็นหน้าที่ของ โปรแกรมระดับ user space ไม่ใช่ของเคอร์เนล
- ดังนั้นลินุกซ์ดิสโทรจึงนิยามได้ว่าเป็น การรวมกันของเคอร์เนล + โครงสร้างพื้นฐานใน user space
- ดิสโทรประกอบด้วย แพ็กเกจ เครื่องมือ การตั้งค่า และ process เริ่มต้น (
init) ที่อยู่บนความสามารถพื้นฐานที่เคอร์เนลมอบให้
- ระดับความซับซ้อนของดิสโทรมีได้หลากหลาย ตั้งแต่แบบมินิมอลอย่าง Arch Linux ไปจนถึงแบบใช้งานง่ายสำหรับผู้ใช้อย่าง Ubuntu
โครงสร้างพื้นฐานนอกเคอร์เนล: user space และ init process
- เมื่อเคอร์เนลบูตเสร็จ จะรัน
init ซึ่งเป็น process หมายเลข PID 1 เป็นตัวแรก
init เป็นบรรพบุรุษของทุก process ใน user space หลังจากนั้น และจะค่อย ๆ รันบริการและเครื่องมือต่าง ๆ ของระบบ
- ชุดของ process และเครื่องมือที่
init เรียกใช้งานคือ องค์ประกอบที่แท้จริงของลินุกซ์ดิสโทร
- เมื่อดิสโทรซับซ้อนขึ้น บางครั้งก็ถูกวิจารณ์ว่า “bloated” เพราะมีฟังก์ชันที่ไม่จำเป็นสะสมมากเกินไป
- ในทางกลับกัน หากสร้าง ไมโครดิสโทรแบบคัสตอม ก็สามารถได้ระบบน้ำหนักเบาที่มีเฉพาะความสามารถขั้นต่ำที่จำเป็น
การคอมไพล์ลินุกซ์เคอร์เนลสำหรับ RISC-V
- บนสภาพแวดล้อม
x86 สามารถคอมไพล์เคอร์เนลสำหรับ RISC-V โดยใช้ cross-compilation toolchain
- ดาวน์โหลดซอร์ส
linux-6.5.2.tar.xz จาก kernel.org แล้วรัน make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig
- สามารถแก้ไขค่าคอนฟิกเคอร์เนลแบบเห็นภาพได้ผ่าน
menuconfig
- หลังคอมไพล์แบบขนานด้วย
make -j16 จะได้ไฟล์ arch/riscv/boot/Image
- บูตใน QEMU ด้วย
qemu-system-riscv64 -machine virt -kernel arch/riscv/boot/Image
- ในบันทึกการบูตจะเห็นข้อความอย่าง การตรวจพบเลเยอร์ SBI, การเริ่มต้น UART และการเปิดใช้งาน printk
อุปสรรคแรก: ไม่มี root filesystem
- ระหว่างการบูตเคอร์เนลจะเกิด kernel panic พร้อมข้อผิดพลาด
VFS: Unable to mount root fs
- สาเหตุคือไม่มี root filesystem (
initramfs) ถูกจัดเตรียมไว้
- ระบบไฟล์สามารถสร้างอยู่บน RAM (
initramfs) ได้ ไม่ได้จำกัดเฉพาะดิสก์
initramfs ถูกแพ็กในรูปแบบ cpio และใน QEMU สามารถโหลดได้ด้วยออปชัน -initrd
สร้าง initramfs และรัน “Hello world”
- ข้อกำหนดขั้นต่ำคือจะต้องมีไบนารี
/init
- เขียน
init.c แล้วคอมไพล์แบบลิงก์สแตติก (-static)
- แพ็กด้วย
cpio -o -H newc < file_list.txt > initramfs.cpio
- เมื่อรัน QEMU จะพิมพ์ “Hello world” แล้วเกิด kernel panic อีกครั้งเพราะ
init จบการทำงาน
- วิธีแก้คือเพิ่มลูปไม่รู้จบเพื่อไม่ให้
init ออกจากโปรแกรม
เพิ่มเชลล์อย่างง่ายที่เขียนด้วย Go
init ใช้ fork และ execl เพื่อรัน /little_shell
little_shell.go เป็นเชลล์เรียบง่ายที่รับอินพุตจากผู้ใช้และ echo คำสั่งกลับออกมา
- คอมไพล์สำหรับ RISC-V ด้วย
GOOS=linux GOARCH=riscv64 go build little_shell.go
- ทั้ง
init และ little_shell ใช้ UART ร่วมกันในการแสดงผล
- standard input/output ถูกจัดการในรูปของ file handle และถูกสืบทอดต่อเมื่อ
fork
- ผลลัพธ์คือได้ สภาพแวดล้อมลินุกซ์พื้นฐาน ที่มี “Hello from init” และอินพุตของเชลล์แสดงสลับกันออกมา
สรุปบทบาทของเคอร์เนล
- abstract ฮาร์ดแวร์: โปรแกรมฝั่งผู้ใช้สามารถแสดงผลได้โดยไม่ต้องรู้รายละเอียดของ UART หรืออุปกรณ์
- มอบอินเทอร์เฟซระดับสูง: เข้าถึงไบนารีอื่นอย่าง
little_shell ผ่านระบบไฟล์
- แยกโปรเซสออกจากกัน:
init และเชลล์ทำงานอยู่ในหน่วยความจำคนละส่วน
- เคอร์เนลมอบ รากฐานการรันที่เสถียรและพกพาได้สูง บนฮาร์ดแวร์ที่ซับซ้อน
นิยามของระบบปฏิบัติการ
- บางมุมมองอาจนับเฉพาะเคอร์เนลว่าเป็นระบบปฏิบัติการ หรืออาจมองว่า ดิสโทรทั้งชุดคือระบบปฏิบัติการ
- สิ่งสำคัญคือการเข้าใจ ขอบเขตบทบาทและโครงสร้างปฏิสัมพันธ์ ระหว่างเคอร์เนลกับ user space
สร้างไมโครดิสโทรที่ใช้งานได้จริงด้วย u-root
- โปรเจ็กต์ u-root มอบ ชุดเครื่องมือ user space ที่สร้างด้วย Go
u-root มีทั้ง bootloader ฝั่ง user space และสภาพแวดล้อมเชลล์ ที่ทำงานอยู่บนลินุกซ์เคอร์เนล
- หลังติดตั้งแล้ว สามารถสร้าง
initramfs อัตโนมัติได้ด้วยคำสั่ง GOOS=linux GOARCH=riscv64 u-root
- ไฟล์
/tmp/initramfs.linux_riscv64.cpio ที่ได้สามารถนำไปรันบน QEMU ได้
- ตอนบูตจะมีแบนเนอร์ “Welcome to u-root!” พร้อม พรอมป์ต์เชลล์พื้นฐาน
- รองรับคำสั่งพื้นฐานอย่าง
ls, pwd, echo และมีฟีเจอร์ tab completion
ทดลองเชื่อมต่อเครือข่าย
- เพิ่มอุปกรณ์
virtio-net-device และ virtio-rng-pci ให้ QEMU
- ใช้ออปชัน
-device virtio-net-device,netdev=usernet -netdev user,id=usernet
- ใช้
dhclient ของ u-root เพื่อรับ IP อัตโนมัติผ่าน DHCP
- ตัวอย่างเช่น
eth0 ได้รับ 10.0.2.15/24
- เข้าถึงเครือข่ายภายนอกสำเร็จด้วย
wget http://google.com และยืนยันการดาวน์โหลด index.html ได้
ความสำคัญของ package manager และ init
- ดิสโทรทั่วไปใช้ package manager เพื่อติดตั้งและอัปเดตซอฟต์แวร์แบบไดนามิก
- แต่การทดลองนี้เป็นแนวทางแบบ embedded ที่ต้องรีบิลด์อิมเมจทั้งก้อนใหม่
init ไม่ใช่แค่ตัวรัน process ธรรมดา แต่เป็น องค์ประกอบสำคัญของการเริ่มต้นอุปกรณ์ การจัดการบริการ และการควบคุมการบูตของระบบ
- สามารถดูขั้นตอนการตั้งค่าอุปกรณ์ต่าง ๆ เช่น
/dev ได้จากซอร์สโค้ด init ของ u-root
ที่เก็บโค้ดบน GitHub
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ผมกำลังทำ ดิสโทร Linux ขนาดจิ๋ว ขึ้นมาเองมาหลายเดือนแล้ว
โหมดผู้ใช้ประกอบด้วยไบนารีแบบ static เพียงไฟล์เดียว และมีแค่ไฟล์ไม่กี่ไฟล์สำหรับรองรับ confidential microVM container
โดยเฉพาะโครงสร้างของ initramfs น่าสนใจมาก กระบวนการที่เคอร์เนลแตก cpio archive, เข้า tmpfs แล้วรัน /init นั้นให้ความรู้สึกราวกับเวทมนตร์
ยังสามารถนำ cpio archive หลายชุดมาต่อกันได้ แต่ละชุดบีบอัดได้ และถูก overlay ตามลำดับ
ด้วยการออกแบบที่เรียบง่ายแต่สง่างามนี้ ทำให้ผมได้เรียนรู้อะไรมากมายจากการเขียนโค้ด unpack ด้วยตัวเอง
ช่วงนี้ qemu เริ่มรองรับ uftrace บนสถาปัตยกรรมหลักแล้ว
เวลาผู้เชี่ยวชาญถามว่า “จะดีบักสิ่งนี้อย่างไร?” นี่แหละคือคำตอบ
ดูข้อมูลที่เกี่ยวข้องได้ในเธรดนี้
ผมก็กำลังทำโปรเจกต์คล้ายกันอยู่ — azathos
มี toy init, shell และยูทิลิตีอีกเล็กน้อยที่ทำขึ้นเอง
ใส่ GNU coreutils ไว้สำหรับดีบัก และตอนนี้กำลังโฟกัสกับ ความสามารถในการวาดหน้าต่าง ลงบน framebuffer
โปรเจกต์นี้เจ๋งมาก ทำให้นึกถึงสมัยปี 98 ที่ทำ “ดิสโทร” แบบใช้ฟลอปปีดิสก์เพื่ออิมเมจเครื่อง Windows PC ผ่าน UDP broadcast
“make bzimage”, ข้อผิดพลาดในสคริปต์ init, รีบูตไม่รู้จบ… มีความทรงจำเยอะมาก
น่าสนใจที่วิธีสมัยนี้ก็ไม่ได้ต่างกันมาก ถ้าพอร์ตไปลง Raspberry Pi น่าจะทั้งสนุกและได้ความรู้ อาจจะลองทำเองก็ได้
สุดท้ายเพื่อนต้องไรต์เนื้อหา sftp ลง CD ให้ถึงจะแก้ได้ ตอนนั้นเขียนได้แค่ 2x เท่านั้น
สงสัยว่าถ้าจะรันสิ่งนี้เป็น cloud image (เช่น Vultr, DigitalOcean) หรือเปิด GUI แล้วรัน Firefox จะยากแค่ไหน
ยังสามารถบูตด้วยดิสโทรอื่นก่อน แล้วใช้ kexec เพื่อรันเคอร์เนลของตัวเองและติดตั้งบนหน่วยความจำได้ด้วย
ดูตัวอย่างการทำจริงได้จาก nixos-anywhere
เป็นงานที่ง่ายกว่าที่คิด
ถ้ามีเวอร์ชันของโปรเจกต์นี้ที่ทำมาสำหรับ Raspberry Pi โดยเฉพาะก็น่าจะน่าสนใจมาก
ตอนแรกผมสงสัยว่าทำไมต้องสร้างอะไรแบบนี้เอง แล้วก็คิดว่าศึกษา Linux ผ่าน Gentoo อย่างเดียวไม่ได้หรือ
ปรับแต่ง user space ได้ก็จริง แต่ไม่เหมาะนักถ้าต้องการเรียนรู้ Linux เองจริง ๆ
แค่ดู stage3 tarball ก็อยู่ในระดับ “มินิดิสโทร” แล้ว
ถ้าเอาไว้เรียนรู้ถือว่าดีมาก และถ้าอยากทำให้เสร็จเร็ว buildroot เป็นตัวเลือกที่ดี
บทความนี้ทำให้ผมได้เรียนรู้อะไรมากจริง ๆ ขอบคุณสำหรับ โพสต์ที่ข้อมูลแน่นมาก