TL; DR : เพราะนี่เป็นวิธีที่ดีที่สุดสำหรับการสร้างกระบวนการใหม่และการควบคุมในเชลล์แบบโต้ตอบ
fork () เป็นสิ่งที่จำเป็นสำหรับกระบวนการและท่อ
ที่จะตอบเฉพาะส่วนของคำถามนี้ถ้าgrep blabla foo
จะถูกเรียกว่าผ่านexec()
โดยตรงในพ่อแม่ผู้ปกครองจะยึดอยู่และ PID grep blabla foo
กับทรัพยากรทั้งหมดจะถูกนำตัวไปจาก
อย่างไรก็ตามการพูดคุยให้ของทั่วไปเกี่ยวกับและexec()
fork()
เหตุผลสำคัญสำหรับพฤติกรรมดังกล่าวเป็นเพราะfork()/exec()
เป็นวิธีมาตรฐานในการสร้างกระบวนการใหม่บน Unix / Linux และนี่ไม่ใช่สิ่งที่ผิดพลาด วิธีนี้มีมาตั้งแต่เริ่มต้นและได้รับอิทธิพลจากวิธีเดียวกันนี้จากระบบปฏิบัติการที่มีอยู่แล้วของเวลา เพื่อให้ได้คำตอบของคำถามที่เกี่ยวข้องfork()
การสร้างกระบวนการใหม่นั้นง่ายกว่าเนื่องจากเคอร์เนลมีงานที่ต้องทำน้อยกว่าการจัดสรรทรัพยากรไปและคุณสมบัติมากมาย (เช่นตัวอธิบายไฟล์สภาพแวดล้อม ฯลฯ ) - ทั้งหมดสามารถ ถูกสืบทอดมาจากกระบวนการพาเรนต์ (ในกรณีนี้จากbash
)
ประการที่สองตราบใดที่เชลล์อินเทอร์แอคทีฟไปคุณไม่สามารถเรียกใช้คำสั่งภายนอกโดยไม่ต้องฟอร์ก ในการเรียกใช้งาน executable ที่อาศัยอยู่บนดิสก์ (ตัวอย่างเช่น/bin/df -h
) คุณจะต้องเรียกexec()
ฟังก์ชันหนึ่งในตระกูลเช่นexecve()
ซึ่งจะแทนที่ parent ด้วยกระบวนการใหม่ใช้ PID และตัวอธิบายไฟล์ที่มีอยู่เป็นต้น สำหรับเชลล์เชิงโต้ตอบคุณต้องการให้คอนโทรลกลับไปยังผู้ใช้และปล่อยให้เชลล์โต้ตอบหลักดำเนินต่อไป ดังนั้นวิธีที่ดีที่สุดคือการสร้างกระบวนการย่อยผ่านทางและปล่อยให้กระบวนการที่จะนำไปผ่านfork()
execve()
ดังนั้นเชลล์แบบโต้ตอบ PID 1156 จะวางไข่เด็กfork()
ด้วย PID 1157 จากนั้นเรียกexecve("/bin/df",["df","-h"],&environment)
ซึ่งทำให้/bin/df -h
ทำงานด้วย PID 1157 ตอนนี้เชลล์จะต้องรอให้กระบวนการออกจากระบบและกลับมาควบคุมอีกครั้ง
ในกรณีที่คุณต้องสร้างไพพ์ระหว่างคำสั่งตั้งแต่สองคำสั่งขึ้นไปdf | grep
คุณจำเป็นต้องมีวิธีในการสร้างไฟล์ descriptors สองไฟล์ (นั่นคือการอ่านและเขียนปลายpipe()
ไพพ์ซึ่งมาจากsyscall) จากนั้นให้กระบวนการใหม่สองอันสืบทอดกระบวนการเหล่านั้น นั่นคือการทำกระบวนการใหม่แล้วทำการคัดลอกส่วนการเขียนของไพพ์ผ่านการdup2()
เรียกไปยังstdout
aka fd 1 ของมัน(ดังนั้นถ้าการเขียน end เป็น fd 4 เราก็ทำได้dup2(4,1)
) เมื่อใดที่exec()
จะวางไข่df
เกิดขึ้นกระบวนการของเด็กจะไม่คิดอะไรเลยstdout
และเขียนลงไปโดยไม่รู้ตัว กระบวนการเดียวกันเกิดขึ้นgrep
ยกเว้นเราfork()
จะอ่านส่วนปลายของท่อด้วย fd 3 และdup(3,0)
ก่อนที่จะวางไข่grep
ด้วยexec()
. ทุกครั้งที่กระบวนการพาเรนต์ยังคงอยู่ที่นั่นรอการควบคุมอีกครั้งเมื่อไปป์ไลน์เสร็จสมบูรณ์
ในกรณีของคำสั่งในตัวโดยทั่วไปเชลล์จะไม่fork()
ยกเว้นsource
คำสั่ง subshells fork()
ต้อง
ในระยะสั้นนี่เป็นกลไกที่จำเป็นและมีประโยชน์
ข้อเสียของการฟอร์กและการปรับให้เหมาะสม
ตอนนี้เป็นที่แตกต่างกันสำหรับเปลือกหอยที่ไม่ใช่แบบโต้ตอบbash -c '<simple command>'
เช่น แม้ว่าจะfork()/exec()
เป็นวิธีการที่ดีที่สุดที่คุณต้องประมวลผลคำสั่งจำนวนมาก แต่ก็เป็นการสิ้นเปลืองทรัพยากรเมื่อคุณมีเพียงคำสั่งเดียว หากต้องการอ้างอิงStéphane Chazelasจากโพสต์นี้ :
Forking นั้นแพงในเวลา CPU, หน่วยความจำ, ตัวอธิบายไฟล์ที่จัดสรร ... การมีกระบวนการเชลล์โกหกเพียงแค่รอกระบวนการอื่นก่อนที่จะออกจากนั้นเป็นทรัพยากรที่สิ้นเปลือง นอกจากนี้มันทำให้ยากที่จะรายงานสถานะการออกของกระบวนการแยกต่างหากที่จะดำเนินการคำสั่งได้อย่างถูกต้อง (ตัวอย่างเช่นเมื่อกระบวนการถูกฆ่า)
ดังนั้นเชลล์จำนวนมาก (ไม่ใช่แค่bash
) ใช้exec()
เพื่ออนุญาตให้ใช้bash -c ''
คำสั่งง่าย ๆ นั้น และด้วยเหตุผลที่กล่าวมาข้างต้นการย่อเล็กสุดของท่อในเชลล์สคริปต์นั้นดีกว่า บ่อยครั้งที่คุณเห็นผู้เริ่มต้นทำสิ่งนี้:
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
แน่นอนว่านี่จะเป็นfork()
3 กระบวนการ นี่เป็นตัวอย่างง่ายๆ แต่ให้พิจารณาไฟล์ขนาดใหญ่ในช่วงกิกะไบต์ มันจะมีประสิทธิภาพมากขึ้นด้วยกระบวนการเดียว:
awk -F':' '$6~"/home"{print $6}' /etc/passwd
ความสิ้นเปลืองทรัพยากรจริง ๆ แล้วอาจเป็นรูปแบบหนึ่งของการโจมตีแบบปฏิเสธการให้บริการและโดยเฉพาะอย่างยิ่งการทิ้งระเบิดถูกสร้างขึ้นผ่านฟังก์ชั่นเชลล์ที่เรียกตัวเองว่าไปป์ไลน์ ทุกวันนี้สิ่งนี้ได้รับการลดลงโดย จำกัด จำนวนกระบวนการสูงสุดในกลุ่มcg บน systemdซึ่ง Ubuntu ก็ใช้ตั้งแต่รุ่น 15.04
แน่นอนว่าไม่ได้หมายความว่าการตีจะไม่ดี มันยังคงเป็นกลไกที่มีประโยชน์ตามที่กล่าวไว้ก่อนหน้านี้ แต่ในกรณีที่คุณสามารถหลีกเลี่ยงกระบวนการที่น้อยลงและใช้ทรัพยากรน้อยลงอย่างต่อเนื่องและมีประสิทธิภาพที่ดีขึ้นคุณควรหลีกเลี่ยงfork()
ถ้าเป็นไปได้
ดูสิ่งนี้ด้วย