ความวิตกกังวลในการแยก: บทช่วยสอนสำหรับการแยกระบบของคุณด้วย Linux Namespaces

เผยแพร่แล้ว: 2022-03-11

ด้วยการถือกำเนิดของเครื่องมือต่างๆ เช่น Docker, Linux Containers และอื่นๆ การแยกกระบวนการของ Linux ออกจากระบบเพียงเล็กน้อยก็กลายเป็นเรื่องง่าย สิ่งนี้ทำให้สามารถเรียกใช้แอพพลิเคชั่นทั้งหมดบนเครื่อง Linux จริงเครื่องเดียว และทำให้แน่ใจว่าไม่มีสองแอพพลิเคชั่นสามารถรบกวนซึ่งกันและกัน โดยไม่ต้องใช้เครื่องเสมือน เครื่องมือเหล่านี้เป็นประโยชน์อย่างมากสำหรับผู้ให้บริการ PaaS แต่เกิดอะไรขึ้นภายใต้ประทุน?

เครื่องมือเหล่านี้อาศัยคุณลักษณะและส่วนประกอบหลายอย่างของเคอร์เนลลินุกซ์ ฟีเจอร์เหล่านี้บางส่วนเพิ่งเปิดตัวไม่นาน ในขณะที่ฟีเจอร์อื่นๆ ยังต้องการให้คุณแก้ไขเคอร์เนลเอง แต่หนึ่งในองค์ประกอบหลักที่ใช้เนมสเปซของ Linux เป็นคุณลักษณะของ Linux ตั้งแต่เวอร์ชัน 2.6.24 ออกสู่ตลาดในปี 2008

ใครก็ตามที่คุ้นเคยกับ chroot มีแนวคิดพื้นฐานอยู่แล้วว่าเนมสเปซ Linux ทำอะไรได้บ้างและวิธีใช้เนมสเปซโดยทั่วไป เช่นเดียวกับที่ chroot อนุญาตให้โปรเซสดูไดเร็กทอรีใดก็ได้ที่เป็นรูทของระบบ (โดยไม่ขึ้นกับกระบวนการที่เหลือ) เนมสเปซของ Linux ก็ยอมให้ส่วนอื่นๆ ของระบบปฏิบัติการสามารถปรับเปลี่ยนได้อย่างอิสระเช่นกัน ซึ่งรวมถึงแผนผังกระบวนการ อินเทอร์เฟซเครือข่าย จุดเชื่อมต่อ ทรัพยากรการสื่อสารระหว่างกระบวนการ และอื่นๆ

เหตุใดจึงต้องใช้เนมสเปซสำหรับการแยกกระบวนการ

ในคอมพิวเตอร์ที่มีผู้ใช้คนเดียว สภาพแวดล้อมระบบเดียวอาจใช้ได้ แต่บนเซิร์ฟเวอร์ที่คุณต้องการเรียกใช้บริการต่างๆ จำเป็นต้องรักษาความปลอดภัยและความเสถียรที่บริการต่างๆ จะแยกออกจากกันมากที่สุด ลองนึกภาพเซิร์ฟเวอร์ที่ใช้งานหลายบริการ ซึ่งหนึ่งในนั้นถูกบุกรุกโดยผู้บุกรุก ในกรณีเช่นนี้ ผู้บุกรุกอาจสามารถใช้ประโยชน์จากบริการนั้นและทำงานในลักษณะของเขาไปยังบริการอื่น ๆ และอาจถึงขั้นประนีประนอมกับเซิร์ฟเวอร์ทั้งหมด การแยกเนมสเปซสามารถจัดเตรียมสภาพแวดล้อมที่ปลอดภัยเพื่อขจัดความเสี่ยงนี้

ตัวอย่างเช่น การใช้เนมสเปซทำให้สามารถรันโปรแกรมตามอำเภอใจหรือโปรแกรมที่ไม่รู้จักบนเซิร์ฟเวอร์ของคุณได้อย่างปลอดภัย เมื่อเร็ว ๆ นี้มีการแข่งขันการเขียนโปรแกรมและแพลตฟอร์ม "hackathon" เพิ่มขึ้นเช่น HackerRank, TopCoder, Codeforces และอีกมากมาย ส่วนใหญ่ใช้ไปป์ไลน์อัตโนมัติเพื่อเรียกใช้และตรวจสอบโปรแกรมที่ส่งโดยผู้เข้าแข่งขัน มักจะเป็นไปไม่ได้ที่จะรู้ล่วงหน้าถึงลักษณะที่แท้จริงของโปรแกรมของผู้เข้าแข่งขัน และบางโปรแกรมอาจมีองค์ประกอบที่เป็นอันตราย ด้วยการรันโปรแกรมเหล่านี้เนมสเปซโดยแยกจากส่วนที่เหลือของระบบทั้งหมด ซอฟต์แวร์สามารถทดสอบและตรวจสอบได้โดยไม่ทำให้ส่วนที่เหลือของเครื่องตกอยู่ในความเสี่ยง ในทำนองเดียวกัน บริการการรวมออนไลน์อย่างต่อเนื่อง เช่น Drone.io จะดึงที่เก็บโค้ดของคุณโดยอัตโนมัติและเรียกใช้สคริปต์ทดสอบบนเซิร์ฟเวอร์ของตนเอง อีกครั้ง การแยกเนมสเปซเป็นสิ่งที่ทำให้สามารถให้บริการเหล่านี้ได้อย่างปลอดภัย

เครื่องมือเนมสเปซ เช่น Docker ยังช่วยให้ควบคุมการใช้ทรัพยากรระบบของโปรเซสได้ดียิ่งขึ้น ทำให้เครื่องมือดังกล่าวเป็นที่นิยมอย่างมากสำหรับผู้ให้บริการ PaaS บริการต่างๆ เช่น Heroku และ Google App Engine ใช้เครื่องมือดังกล่าวเพื่อแยกและเรียกใช้แอปพลิเคชันเว็บเซิร์ฟเวอร์หลายตัวบนฮาร์ดแวร์จริงเดียวกัน เครื่องมือเหล่านี้ช่วยให้พวกเขาเรียกใช้แต่ละแอปพลิเคชัน (ซึ่งอาจถูกปรับใช้โดยผู้ใช้หลายราย) โดยไม่ต้องกังวลเกี่ยวกับหนึ่งในนั้นที่ใช้ทรัพยากรระบบมากเกินไป หรือรบกวนและ/หรือขัดแย้งกับบริการที่ปรับใช้อื่น ๆ บนเครื่องเดียวกัน ด้วยการแยกกระบวนการดังกล่าว เป็นไปได้ที่จะมีซอฟต์แวร์พึ่งพา (และเวอร์ชัน) ที่แตกต่างกันโดยสิ้นเชิงสำหรับสภาพแวดล้อมที่แยกออกมา!

หากคุณเคยใช้เครื่องมืออย่าง Docker คุณรู้อยู่แล้วว่าเครื่องมือเหล่านี้สามารถแยกกระบวนการใน “คอนเทนเนอร์” ขนาดเล็กได้ การรันกระบวนการในคอนเทนเนอร์ Docker นั้นเหมือนกับการรันในเครื่องเสมือน เฉพาะคอนเทนเนอร์เหล่านี้เท่านั้นที่เบากว่าเครื่องเสมือนอย่างมาก โดยทั่วไป เครื่องเสมือนจะจำลองชั้นฮาร์ดแวร์ที่ด้านบนของระบบปฏิบัติการ แล้วเรียกใช้ระบบปฏิบัติการอื่นนอกเหนือจากนั้น สิ่งนี้ทำให้คุณสามารถเรียกใช้กระบวนการต่างๆ ภายในเครื่องเสมือน โดยแยกจากระบบปฏิบัติการจริงของคุณโดยสิ้นเชิง แต่เครื่องเสมือนนั้นหนักมาก! ในทางกลับกัน คอนเทนเนอร์ Docker ใช้คุณสมบัติหลักบางอย่างของระบบปฏิบัติการจริงของคุณ รวมถึงเนมสเปซ และตรวจสอบให้แน่ใจว่ามีการแยกในระดับที่ใกล้เคียงกัน แต่ไม่มีการเลียนแบบฮาร์ดแวร์และใช้งานระบบปฏิบัติการอื่นในเครื่องเดียวกัน ทำให้มีน้ำหนักเบามาก

ประมวลผลเนมสเปซ

ในอดีต เคอร์เนล Linux ได้รักษาแผนผังกระบวนการเดียว แผนผังมีการอ้างอิงถึงทุกกระบวนการที่ทำงานอยู่ในลำดับชั้นพาเรนต์-ชายด์ กระบวนการ ที่มีสิทธิ์เพียงพอและตรงตามเงื่อนไขบางประการ สามารถตรวจสอบกระบวนการอื่นโดยแนบตัวติดตามเข้ากับกระบวนการ หรือแม้กระทั่งสามารถฆ่ากระบวนการนั้นได้

ด้วยการเปิดตัวเนมสเปซ Linux มันเป็นไปได้ที่จะมีแผนผังกระบวนการ "ซ้อนกัน" หลายรายการ โครงสร้างกระบวนการแต่ละอันสามารถมีชุดของกระบวนการที่แยกได้ทั้งหมด สิ่งนี้สามารถรับประกันได้ว่าโปรเซสที่เป็นของโพรเซสทรีหนึ่งไม่สามารถตรวจสอบหรือฆ่าได้ - อันที่จริงไม่สามารถรู้ถึงการมีอยู่ของโปรเซสในโปรเซสทรีอื่นๆ หรือโปรเซสทรีอื่นๆ

ทุกครั้งที่คอมพิวเตอร์ที่มี Linux บูทขึ้น เครื่องจะเริ่มต้นด้วยกระบวนการเดียวโดยมีตัวระบุกระบวนการ (PID) 1 กระบวนการนี้เป็นรูทของแผนผังกระบวนการ และเริ่มต้นส่วนที่เหลือของระบบโดยดำเนินการบำรุงรักษาที่เหมาะสมและเริ่มการทำงาน daemons/บริการที่ถูกต้อง กระบวนการอื่นๆ ทั้งหมดเริ่มต้นที่ด้านล่างกระบวนการนี้ในแผนผัง เนมสเปซ PID ช่วยให้สามารถแยกต้นไม้ใหม่ได้ด้วยกระบวนการ PID 1 ของตัวเอง กระบวนการที่ทำสิ่งนี้ยังคงอยู่ในเนมสเปซพาเรนต์ ในทรีดั้งเดิม แต่ทำให้ชายด์เป็นรูทของทรีโปรเซสของตัวเอง

ด้วยการแยกเนมสเปซ PID โปรเซสในเนมสเปซลูกไม่มีทางรู้ถึงการมีอยู่ของโปรเซสพาเรนต์ อย่างไรก็ตาม โปรเซสในเนมสเปซพาเรนต์มีมุมมองที่สมบูรณ์ของโปรเซสในเนมสเปซย่อย ราวกับว่าเป็นโปรเซสอื่นในเนมสเปซพาเรนต์

บทช่วยสอนเนมสเปซนี้สรุปการแยกแผนผังกระบวนการต่างๆ โดยใช้ระบบเนมสเปซใน Linux

เป็นไปได้ที่จะสร้างชุดเนมสเปซย่อยที่ซ้อนกัน: กระบวนการหนึ่งเริ่มต้นกระบวนการย่อยในเนมสเปซ PID ใหม่ และกระบวนการย่อยนั้นวางไข่อีกกระบวนการหนึ่งในเนมสเปซ PID ใหม่ เป็นต้น

ด้วยการแนะนำเนมสเปซ PID โปรเซสเดียวสามารถมี PID หลายอันที่เชื่อมโยงกับโปรเซสนั้นได้ หนึ่งรายการสำหรับแต่ละเนมสเปซที่มันอยู่ภายใต้ ในซอร์สโค้ดของ Linux เราจะเห็นได้ว่า struct ชื่อ pid ซึ่งเคยใช้ติดตาม PID เพียงตัวเดียว ตอนนี้ติดตาม PID หลายตัวผ่านการใช้ struct ที่ชื่อ upid :

 struct upid { int nr; // the PID value struct pid_namespace *ns; // namespace where this PID is relevant // ... }; struct pid { // ... int level; // number of upids struct upid numbers[0]; // array of upids };

ในการสร้างเนมสเปซ PID ใหม่ ต้องเรียกการเรียกระบบ clone() ด้วยแฟล็กพิเศษ CLONE_NEWPID (C จัดเตรียม wrapper เพื่อแสดงการเรียกระบบนี้ และทำภาษายอดนิยมอื่น ๆ อีกมากมายเช่นกัน) ในขณะที่เนมสเปซอื่น ๆ ที่กล่าวถึงด้านล่างยังสามารถสร้างขึ้นได้โดยใช้การเรียกระบบ unshare() เนมสเปซ PID สามารถสร้างได้ในเวลาที่ใหม่เท่านั้น กระบวนการเกิดโดยใช้ clone() เมื่อ clone() ถูกเรียกด้วยแฟล็กนี้ กระบวนการใหม่จะเริ่มทันทีในเนมสเปซ PID ใหม่ ภายใต้แผนผังกระบวนการใหม่ สิ่งนี้สามารถแสดงให้เห็นได้ด้วยโปรแกรม C อย่างง่าย:

 #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { printf("PID: %ld\n", (long)getpid()); return 0; } int main() { pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL); printf("clone() = %ld\n", (long)child_pid); waitpid(child_pid, NULL, 0); return 0; }

คอมไพล์และรันโปรแกรมนี้ด้วยสิทธิ์ของรูท และคุณจะสังเกตเห็นผลลัพธ์ที่มีลักษณะดังนี้:

 clone() = 5304 PID: 1

PID ตามที่พิมพ์จากภายใน child_fn จะเป็น 1

แม้ว่าโค้ดการสอนเนมสเปซด้านบนนี้จะใช้เวลาไม่นานกว่าคำว่า “สวัสดี ชาวโลก” ในบางภาษา แต่ก็มีหลายอย่างเกิดขึ้นเบื้องหลัง ฟังก์ชัน clone() อย่างที่คุณคาดหวัง ได้สร้างกระบวนการใหม่โดยการโคลนกระบวนการปัจจุบันและเริ่มดำเนินการที่จุดเริ่มต้นของ child_fn() อย่างไรก็ตาม ขณะทำเช่นนั้น จะแยกกระบวนการใหม่ออกจากแผนผังกระบวนการเดิม และสร้างโครงสร้างกระบวนการแยกต่างหากสำหรับกระบวนการใหม่

ลองแทนที่ static int child_fn() ด้วยวิธีต่อไปนี้ เพื่อพิมพ์ parent PID จากมุมมองของกระบวนการที่แยกออกมา:

 static int child_fn() { printf("Parent PID: %ld\n", (long)getppid()); return 0; }

การรันโปรแกรมในครั้งนี้ให้ผลลัพธ์ดังต่อไปนี้:

 clone() = 11449 Parent PID: 0

สังเกตว่า PID พาเรนต์จากมุมมองของกระบวนการที่แยกออกมาเป็น 0 ได้อย่างไร ซึ่งบ่งชี้ว่าไม่มีพาเรนต์ ลองเรียกใช้โปรแกรมเดิมอีกครั้ง แต่คราวนี้ ให้ลบแฟ CLONE_NEWPID ออกจากภายในการเรียกใช้ฟังก์ชัน clone() :

 pid_t child_pid = clone(child_fn, child_stack+1048576, SIGCHLD, NULL);

คราวนี้ คุณจะสังเกตเห็นว่า PID หลักไม่ได้เป็น 0 อีกต่อไป:

 clone() = 11561 Parent PID: 11560

อย่างไรก็ตาม นี่เป็นเพียงขั้นตอนแรกในบทช่วยสอนของเรา กระบวนการเหล่านี้ยังคงมีการเข้าถึงทรัพยากรทั่วไปหรือทรัพยากรที่ใช้ร่วมกันอื่นๆ ได้ไม่จำกัด ตัวอย่างเช่น อินเทอร์เฟซเครือข่าย: หากโปรเซสลูกที่สร้างไว้ด้านบนเพื่อฟังบนพอร์ต 80 จะเป็นการป้องกันไม่ให้กระบวนการอื่นทั้งหมดบนระบบสามารถรับฟังได้

เนมสเปซเครือข่าย Linux

นี่คือจุดที่เนมสเปซเครือข่ายมีประโยชน์ เนมสเปซเครือข่ายอนุญาตให้แต่ละกระบวนการเหล่านี้เห็นชุดอินเทอร์เฟซเครือข่ายที่แตกต่างกันโดยสิ้นเชิง แม้แต่อินเทอร์เฟซแบบวนรอบก็แตกต่างกันสำหรับแต่ละเนมสเปซเครือข่าย

การแยกโปรเซสออกจากเนมสเปซเครือข่ายของตัวเองนั้นเกี่ยวข้องกับการแนะนำแฟล็กอื่นให้กับการเรียกฟังก์ชัน clone() : CLONE_NEWNET ;

 #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { printf("New `net` Namespace:\n"); system("ip link"); printf("\n\n"); return 0; } int main() { printf("Original `net` Namespace:\n"); system("ip link"); printf("\n\n"); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0; }

เอาท์พุท:

 Original `net` Namespace: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: enp4s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000 link/ether 00:24:8c:a1:ac:e7 brd ff:ff:ff:ff:ff:ff New `net` Namespace: 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

เกิดอะไรขึ้นที่นี่? อุปกรณ์อีเทอร์เน็ตที่มีอยู่จริง enp4s0 เป็นของเนมสเปซเครือข่ายทั่วโลก ตามที่ระบุโดยเครื่องมือ "ip" ที่เรียกใช้จากเนมสเปซนี้ อย่างไรก็ตาม อินเทอร์เฟซทางกายภาพไม่พร้อมใช้งานในเนมสเปซเครือข่ายใหม่ นอกจากนี้ อุปกรณ์ลูปแบ็คยังทำงานอยู่ในเนมสเปซเครือข่ายเดิม แต่จะ "ลง" ในเนมสเปซเครือข่ายย่อย

ในการจัดเตรียมอินเทอร์เฟซเครือข่ายที่ใช้งานได้ในเนมสเปซย่อย จำเป็นต้องตั้งค่าอินเทอร์เฟซเครือข่าย "เสมือน" เพิ่มเติมซึ่งครอบคลุมเนมสเปซหลายรายการ เมื่อเสร็จแล้ว จะสามารถสร้างอีเทอร์เน็ตบริดจ์ และแม้กระทั่งกำหนดเส้นทางแพ็กเก็ตระหว่างเนมสเปซ สุดท้าย เพื่อให้การทำงานทั้งหมด "กระบวนการกำหนดเส้นทาง" ต้องทำงานในเนมสเปซเครือข่ายทั่วโลกเพื่อรับปริมาณการใช้ข้อมูลจากอินเทอร์เฟซทางกายภาพ และกำหนดเส้นทางผ่านอินเทอร์เฟซเสมือนที่เหมาะสมไปยังเนมสเปซเครือข่ายย่อยที่ถูกต้อง บางทีคุณอาจเข้าใจว่าทำไมเครื่องมืออย่าง Docker ที่ทำหน้าที่ยกของหนักให้คุณถึงได้รับความนิยม!

เนมสเปซเครือข่าย Linux ประกอบด้วยกระบวนการกำหนดเส้นทางไปยังเนมสเปซเครือข่ายย่อยหลายรายการ

เมื่อต้องการทำสิ่งนี้ด้วยมือ คุณสามารถสร้างคู่ของการเชื่อมต่ออีเทอร์เน็ตเสมือนระหว่างพาเรนต์และเนมสเปซย่อยโดยการรันคำสั่งเดียวจากเนมสเปซพาเรนต์:

 ip link add name veth0 type veth peer name veth1 netns <pid>

ที่นี่ <pid> ควรถูกแทนที่ด้วย ID กระบวนการของกระบวนการในเนมสเปซย่อยตามที่ผู้ปกครองสังเกต การรันคำสั่งนี้จะสร้างการเชื่อมต่อแบบไพพ์ระหว่างเนมสเปซทั้งสองนี้ เนมสเปซพาเรนต์เก็บอุปกรณ์ veth0 ไว้ และส่งผ่านอุปกรณ์ veth1 ไปยังเนมสเปซย่อย สิ่งใดก็ตามที่เข้าสู่ปลายด้านใดด้านหนึ่ง จะออกมาทางปลายอีกด้านหนึ่ง เช่นเดียวกับที่คุณคาดหวังจากการเชื่อมต่ออีเทอร์เน็ตจริงระหว่างสองโหนดจริง ดังนั้น ทั้งสองด้านของการเชื่อมต่ออีเทอร์เน็ตเสมือนจะต้องกำหนดที่อยู่ IP

เม้าท์ เนมสเปซ

ลินุกซ์ยังรักษาโครงสร้างข้อมูลสำหรับจุดเชื่อมต่อทั้งหมดของระบบ ประกอบด้วยข้อมูลต่างๆ เช่น พาร์ติชั่นดิสก์ที่ติดตั้ง ตำแหน่งที่เมาท์ ไม่ว่าจะเป็นแบบอ่านอย่างเดียว และอื่นๆ ด้วยเนมสเปซ Linux เราสามารถโคลนโครงสร้างข้อมูลนี้ได้ ดังนั้นกระบวนการภายใต้เนมสเปซที่ต่างกันสามารถเปลี่ยนจุดเชื่อมต่อได้โดยไม่กระทบต่อกันและกัน

การสร้างเนมสเปซการเมาต์แยกกันมีผลคล้ายกับการทำ chroot() chroot() นั้นดี แต่ไม่ได้ให้การแยกที่สมบูรณ์ และผลกระทบของมันถูกจำกัดไว้ที่จุดต่อเชื่อมรูทเท่านั้น การสร้างเนมสเปซการเมาต์แยกกันทำให้แต่ละกระบวนการที่แยกออกมามีมุมมองที่แตกต่างอย่างสิ้นเชิงของโครงสร้างจุดเชื่อมต่อของระบบทั้งหมดจากแบบเดิม วิธีนี้ช่วยให้คุณมีรูทที่แตกต่างกันสำหรับแต่ละกระบวนการที่แยกได้ เช่นเดียวกับจุดเชื่อมต่ออื่นๆ ที่เฉพาะเจาะจงสำหรับกระบวนการเหล่านั้น ด้วยความระมัดระวังตามบทช่วยสอนนี้ คุณสามารถหลีกเลี่ยงการเปิดเผยข้อมูลใดๆ เกี่ยวกับระบบพื้นฐานได้

การเรียนรู้วิธีใช้เนมสเปซอย่างถูกต้องมีประโยชน์หลายประการดังที่อธิบายไว้ในบทช่วยสอนเนมสเปซนี้

ค่าสถานะ clone() ที่จำเป็นเพื่อให้บรรลุสิ่งนี้คือ CLONE_NEWNS :

 clone(child_fn, child_stack+1048576, CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | SIGCHLD, NULL)

ในขั้นต้น กระบวนการลูกจะเห็นจุดเชื่อมต่อที่เหมือนกันทุกประการกับกระบวนการหลัก อย่างไรก็ตาม ภายใต้เนมสเปซการเมาต์ใหม่ โปรเซสลูกสามารถเมาต์หรือเลิกเมาต์ปลายทางใดก็ได้ที่ต้องการ และการเปลี่ยนแปลงจะไม่ส่งผลต่อเนมสเปซของพาเรนต์ หรือเนมสเปซเมาต์อื่น ๆ ในระบบทั้งหมด ตัวอย่างเช่น หากกระบวนการหลักมีพาร์ติชั่นดิสก์หนึ่งติดตั้งอยู่ที่รูท กระบวนการที่แยกออกมาจะเห็นพาร์ติชั่นดิสก์เดียวกันถูกติดตั้งที่รูทในตอนเริ่มต้น แต่ประโยชน์ของการแยกเนมสเปซการเมาต์นั้นชัดเจนเมื่อกระบวนการแบบแยกพยายามเปลี่ยนพาร์ติชั่นรูทเป็นอย่างอื่น เนื่องจากการเปลี่ยนแปลงจะมีผลกับเนมสเปซการเมาต์ที่แยกออกมาเท่านั้น

ที่น่าสนใจ จริง ๆ แล้วนี่เป็นความคิดที่ไม่ดีที่จะวางไข่กระบวนการลูกเป้าหมายโดยตรงด้วยแฟ CLONE_NEWNS วิธีที่ดีกว่าคือการเริ่มกระบวนการ "init" พิเศษด้วยแฟ CLONE_NEWNS ให้กระบวนการ "init" เปลี่ยน "/", "/proc", "/dev" หรือจุดเชื่อมต่ออื่นๆ ตามต้องการ จากนั้นจึงเริ่มกระบวนการเป้าหมาย . มีการกล่าวถึงรายละเอียดเพิ่มเติมเล็กน้อยในช่วงท้ายของบทช่วยสอนเนมสเปซนี้

เนมสเปซอื่น ๆ

มีเนมสเปซอื่นๆ ที่สามารถแยกกระบวนการเหล่านี้ออกได้ กล่าวคือ ผู้ใช้ IPC และ UTS เนมสเปซผู้ใช้อนุญาตให้โปรเซสมีสิทธิ์รูทภายในเนมสเปซ โดยไม่ต้องให้สิทธิ์เข้าถึงโปรเซสภายนอกเนมสเปซ การแยกกระบวนการโดยใช้เนมสเปซ IPC จะทำให้มีทรัพยากรการสื่อสารระหว่างกระบวนการของตัวเอง เช่น ข้อความ System V IPC และ POSIX เนมสเปซ UTS แยกตัวระบุเฉพาะของระบบสองตัว: nodename และ domainname

ตัวอย่างด่วนเพื่อแสดงว่าเนมสเปซ UTS ถูกแยกออกมาแสดงด้านล่าง:

 #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/utsname.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static void print_nodename() { struct utsname utsname; uname(&utsname); printf("%s\n", utsname.nodename); } static int child_fn() { printf("New UTS namespace nodename: "); print_nodename(); printf("Changing nodename inside new UTS namespace\n"); sethostname("GLaDOS", 6); printf("New UTS namespace nodename: "); print_nodename(); return 0; } int main() { printf("Original UTS namespace nodename: "); print_nodename(); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWUTS | SIGCHLD, NULL); sleep(1); printf("Original UTS namespace nodename: "); print_nodename(); waitpid(child_pid, NULL, 0); return 0; }

โปรแกรมนี้ให้ผลลัพธ์ต่อไปนี้:

 Original UTS namespace nodename: XT New UTS namespace nodename: XT Changing nodename inside new UTS namespace New UTS namespace nodename: GLaDOS Original UTS namespace nodename: XT

ที่นี่ child_fn() พิมพ์ nodename เปลี่ยนเป็นอย่างอื่นแล้วพิมพ์อีกครั้ง โดยปกติ การเปลี่ยนแปลงจะเกิดขึ้นภายในเนมสเปซ UTS ใหม่เท่านั้น

ข้อมูลเพิ่มเติมเกี่ยวกับเนมสเปซทั้งหมดที่มีให้และแยกได้จากบทช่วยสอนที่นี่

การสื่อสารข้ามเนมสเปซ

บ่อยครั้งที่จำเป็นต้องสร้างการสื่อสารบางอย่างระหว่างเนมสเปซพาเรนต์และเนมสเปซย่อย ซึ่งอาจใช้สำหรับการทำงานการกำหนดค่าภายในสภาพแวดล้อมแบบแยกส่วน หรืออาจเป็นเพียงการรักษาความสามารถในการมองเข้าไปในสภาวะของสภาพแวดล้อมนั้นจากภายนอก วิธีหนึ่งในการทำเช่นนั้นคือการทำให้ SSH daemon ทำงานภายในสภาพแวดล้อมนั้น คุณสามารถมี SSH daemon แยกกันในแต่ละเนมสเปซเครือข่าย อย่างไรก็ตาม การมี SSH daemons หลายตัวทำงานอยู่นั้นใช้ทรัพยากรที่มีค่ามากมาย เช่น หน่วยความจำ นี่คือจุดที่กระบวนการ "เริ่มต้น" พิเศษพิสูจน์ให้เห็นว่าเป็นความคิดที่ดีอีกครั้ง

กระบวนการ "init" สามารถสร้างช่องทางการสื่อสารระหว่างเนมสเปซหลักและเนมสเปซย่อย แชนเนลนี้ใช้ซ็อกเก็ต UNIX หรือใช้ TCP ได้ ในการสร้างซ็อกเก็ต UNIX ที่ครอบคลุมเนมสเปซการเมาต์ที่แตกต่างกันสองแห่ง คุณต้องสร้างกระบวนการลูกก่อน จากนั้นจึงสร้างซ็อกเก็ต UNIX จากนั้นแยกชายด์ออกเป็นเนมสเปซการเมาต์แยกต่างหาก แต่เราจะสร้างกระบวนการก่อน แล้วแยกออกได้อย่างไร? ลินุกซ์ให้ unshare() การเรียกระบบพิเศษนี้ช่วยให้กระบวนการแยกตัวเองออกจากเนมสเปซเดิม แทนที่จะให้พาเรนต์แยกเด็กตั้งแต่แรก ตัวอย่างเช่น โค้ดต่อไปนี้มีผลเหมือนกันทุกประการกับโค้ดที่กล่าวถึงก่อนหน้านี้ในส่วนเนมสเปซเครือข่าย:

 #define _GNU_SOURCE #include <sched.h> #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> static char child_stack[1048576]; static int child_fn() { // calling unshare() from inside the init process lets you create a new namespace after a new process has been spawned unshare(CLONE_NEWNET); printf("New `net` Namespace:\n"); system("ip link"); printf("\n\n"); return 0; } int main() { printf("Original `net` Namespace:\n"); system("ip link"); printf("\n\n"); pid_t child_pid = clone(child_fn, child_stack+1048576, CLONE_NEWPID | SIGCHLD, NULL); waitpid(child_pid, NULL, 0); return 0; }

และเนื่องจากกระบวนการ "เริ่มต้น" เป็นสิ่งที่คุณคิดขึ้นเอง คุณสามารถทำให้มันทำงานที่จำเป็นทั้งหมดก่อน จากนั้นจึงแยกตัวเองออกจากระบบที่เหลือก่อนที่จะดำเนินการย่อยเป้าหมาย

บทสรุป

บทช่วยสอนนี้เป็นเพียงภาพรวมของวิธีใช้เนมสเปซใน Linux ควรให้แนวคิดพื้นฐานแก่คุณเกี่ยวกับวิธีที่นักพัฒนา Linux อาจเริ่มใช้การแยกระบบ ซึ่งเป็นส่วนสำคัญของสถาปัตยกรรมของเครื่องมือ เช่น Docker หรือ Linux Containers ในกรณีส่วนใหญ่ วิธีที่ดีที่สุดคือใช้เครื่องมือที่มีอยู่เหล่านี้ ซึ่งเป็นที่รู้จักและผ่านการทดสอบแล้ว แต่ในบางกรณี การมีกลไกการแยกกระบวนการที่ปรับแต่งได้เองอาจเป็นเรื่องที่สมเหตุสมผล และในกรณีนี้ บทแนะนำเนมสเปซนี้จะช่วยคุณได้อย่างมาก

มีอะไรเกิดขึ้นอีกมากภายใต้ประทุนมากกว่าที่ฉันได้กล่าวถึงในบทความนี้ และมีวิธีอื่นๆ ที่คุณอาจต้องการจำกัดกระบวนการเป้าหมายของคุณเพื่อเพิ่มความปลอดภัยและการแยกตัว แต่หวังว่านี่จะเป็นจุดเริ่มต้นที่มีประโยชน์สำหรับผู้ที่สนใจที่จะทราบข้อมูลเพิ่มเติมว่าการแยกเนมสเปซกับ Linux ทำงานอย่างไร