ทำไม shell call fork ()?


32

เมื่อโปรเซสเริ่มต้นจากเชลล์เหตุใดเชลล์ฟอร์คเองจึงดำเนินการกระบวนการก่อน

ตัวอย่างเช่นเมื่อผู้ใช้อินพุตgrep blabla fooทำไมเชลล์ไม่สามารถเรียกexec()ใช้ grep โดยไม่มีเชลล์ย่อยได้

นอกจากนี้เมื่อเชลล์แยกตัวเองภายในตัวจำลองเทอร์มินัล GUI มันจะเริ่มต้นตัวจำลองเทอร์มินัลอื่นหรือไม่ (เช่นpts/13เริ่มต้นpts/14)

คำตอบ:


34

เมื่อคุณเรียกใช้execเมธอด family จะไม่สร้างกระบวนการใหม่execแทนที่หน่วยความจำกระบวนการปัจจุบันและชุดคำสั่งแทนด้วยกระบวนการที่คุณต้องการเรียกใช้แทน

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


เกือบจะโอเค --- ฉันแทนที่เทอร์มินัลด้วยทุบตี ;-)
Rmano

2
BTW คุณสามารถบอกทุบตีที่จะดำเนินการได้โดยไม่ต้องฟอร์ก grep exec grep blabla fooแรกโดยใช้คำสั่ง แน่นอนในกรณีนี้มันจะไม่เป็นประโยชน์มาก (เนื่องจากหน้าต่างเทอร์มินัลของคุณจะปิดทันทีที่ grep เสร็จสิ้น) แต่มันอาจจะมีประโยชน์ในบางครั้ง (เช่นถ้าคุณเริ่มเชลล์อื่นอาจผ่าน ssh / sudo / screen และไม่ต้องการกลับไปเป็นต้นฉบับหรือหากกระบวนการเชลล์ที่คุณใช้งานอยู่นั้นเป็น sub-shell ที่ไม่ได้หมายถึงการรันคำสั่งมากกว่าหนึ่งคำ)
Ilmari Karonen

7
ชุดคำสั่งมีความหมายที่เฉพาะเจาะจงมาก และมันก็ไม่ใช่ความหมายที่คุณกำลังใช้อยู่
Andrew Savinykh

@IlmariKaronen มันจะมีประโยชน์ในสคริปต์ wrapper ที่คุณต้องการเตรียมข้อโต้แย้งและสภาพแวดล้อมสำหรับคำสั่ง และกรณีที่คุณกล่าวถึงโดยที่ bash ไม่ได้ตั้งใจให้เรียกใช้มากกว่าหนึ่งคำสั่งนั่นคือการbash -c 'grep foo bar'เรียก exec จริง ๆ แล้วมีรูปแบบการเพิ่มประสิทธิภาพ bash สำหรับคุณโดยอัตโนมัติ
Sergiy Kolodyazhnyy

3

ตามที่ptsตรวจสอบด้วยตัวคุณเอง: ในเปลือกรัน

echo $$ 

เพื่อทราบ process-id (PID) ของคุณฉันมีตัวอย่าง

echo $$
29296

จากนั้นเรียกใช้ตัวอย่างsleep 60และจากนั้นในสถานีอื่น

(0)samsung-romano:~% ps -edao pid,ppid,tty,command | grep 29296 | grep -v grep
29296  2343 pts/11   zsh
29499 29296 pts/11   sleep 60

ดังนั้นโดยทั่วไปคุณมี tty เดียวกันที่เกี่ยวข้องกับกระบวนการ (โปรดทราบว่านี่เป็นของคุณsleepเพราะมันมีเชลล์เป็นพาเรนต์)


2

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()เรียกไปยังstdoutaka 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()ถ้าเป็นไปได้

ดูสิ่งนี้ด้วย


1

สำหรับแต่ละคำสั่ง (ตัวอย่าง: grep) ที่คุณออกในพรอมต์ bash คุณตั้งใจจะเริ่มกระบวนการใหม่แล้วกลับสู่พรอมต์ bash หลังจากดำเนินการ

หากกระบวนการเชลล์ (ทุบตี) เรียกใช้ exec () เพื่อเรียกใช้ grep กระบวนการเชลล์จะถูกแทนที่ด้วย grep Grep จะทำงานได้ดี แต่หลังจากการดำเนินการควบคุมไม่สามารถกลับไปที่เปลือกเพราะกระบวนการทุบตีถูกแทนที่แล้ว

ด้วยเหตุผลนี้ bash call fork () ซึ่งไม่ได้แทนที่กระบวนการปัจจุบัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.