ความวิตกกังวลในการแยก: บทช่วยสอนสำหรับการแยกระบบของคุณด้วย 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 โปรเซสในเนมสเปซลูกไม่มีทางรู้ถึงการมีอยู่ของโปรเซสพาเรนต์ อย่างไรก็ตาม โปรเซสในเนมสเปซพาเรนต์มีมุมมองที่สมบูรณ์ของโปรเซสในเนมสเปซย่อย ราวกับว่าเป็นโปรเซสอื่นในเนมสเปซพาเรนต์
เป็นไปได้ที่จะสร้างชุดเนมสเปซย่อยที่ซ้อนกัน: กระบวนการหนึ่งเริ่มต้นกระบวนการย่อยในเนมสเปซ 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 ที่ทำหน้าที่ยกของหนักให้คุณถึงได้รับความนิยม!
เมื่อต้องการทำสิ่งนี้ด้วยมือ คุณสามารถสร้างคู่ของการเชื่อมต่ออีเทอร์เน็ตเสมือนระหว่างพาเรนต์และเนมสเปซย่อยโดยการรันคำสั่งเดียวจากเนมสเปซพาเรนต์:
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 ทำงานอย่างไร