Unix daemons เป็นโปรแกรมที่ทำงานอยู่เบื้องหลัง Nginx, Postgres และ OpenSSH เป็นเพียงตัวอย่างบางส่วน พวกเขาใช้กลอุบายพิเศษบางอย่างเพื่อ "แยก" กระบวนการ และปล่อยให้ทำงานโดยไม่ขึ้นกับเทอร์มินัล
ฉันรู้สึกทึ่งกับ daemons มาโดยตลอด บางทีอาจเป็นชื่อนั้น และฉันคิดว่าน่าจะสนุกถ้าได้โพสต์เพื่ออธิบายวิธีการทำงานของมัน โดยเฉพาะวิธีสร้างมันใน Ruby
...แต่ก่อนอื่น
อย่าลองทำที่บ้าน!
คุณอาจไม่ต้องการสร้างภูต มีวิธีที่ง่ายกว่ามากในการทำงานให้สำเร็จ
คุณอาจต้องการสร้างโปรแกรมที่ทำงานอยู่เบื้องหลัง ไม่มีปัญหา. ระบบปฏิบัติการของคุณมีระบบเพื่อให้คุณเรียกใช้โปรแกรมปกติในพื้นหลังได้
บน Ubuntu สามารถทำได้ผ่าน Upstart ของ systemd บน OSX มันเปิดตัว มีคนอื่น. แต่พวกเขาทั้งหมดทำงานตามแนวคิดเดียวกัน คุณจัดเตรียมไฟล์การกำหนดค่าที่บอกระบบว่าจะเริ่มต้นและหยุดโปรแกรมที่รันเป็นเวลานานได้อย่างไร แล้ว...ก็ประมาณนั้น คุณสามารถเริ่มโปรแกรมโดยใช้คำสั่งของระบบ เช่น service my_app start
และทำงานในพื้นหลัง
กล่าวโดยย่อ การพุ่งพรวดนั้นเรียบง่ายและเชื่อถือได้ ในขณะที่ภูตเก่านั้นเป็นความลับและยากที่จะทำให้ถูกต้อง
...แต่ถ้าเป็นอย่างนั้น ทำไมเราควรเรียนรู้เกี่ยวกับภูตผี? เพราะสนุก! และเราจะได้เรียนรู้ข้อเท็จจริงที่น่าสนใจเกี่ยวกับกระบวนการ Unix ไปพร้อมกัน
ภูตที่ง่ายที่สุด
ตอนนี้มีคนบอกคุณแล้วว่าอย่าสร้าง daemon มาสร้าง daemon กันเถอะ! สำหรับ Ruby 1.9 มันง่ายอย่างเหลือเชื่อ สิ่งที่คุณต้องทำคือใช้วิธี Process.daemon
# Optional: set the process name to something easy to type<br>$PROGRAM_NAME = "rubydaemon"<br>
# Make the current process into a daemon
Process.daemon()
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
ตอนนี้ เมื่อฉันเรียกใช้สคริปต์นี้ การควบคุมจะส่งกลับไปยังคอนโซล ถ้าฉันแก้ไขบันทึกของฉัน ฉันจะเห็นว่ามีการเพิ่มการประทับเวลาทุกวินาทีเหมือนที่ฉันคาดไว้
นั่นเป็นเรื่องง่าย แต่ก็ยังไม่ได้อธิบายว่า daemons ทำงานอย่างไร ถึง จริงๆ เข้าใจว่าเราต้องทำการ daemonization ด้วยตนเอง
การเปลี่ยนแปลงกระบวนการหลัก
หากคุณใช้ bash เพื่อรันโปรแกรมปกติ กระบวนการของโปรแกรมนั้นเป็นลูกของ bash แต่สำหรับภูต ไม่สำคัญว่าคุณจะเปิดใช้อย่างไร กระบวนการหลักของพวกเขามักจะเป็นกระบวนการ "รูท" ที่ระบบปฏิบัติการจัดเตรียมไว้ให้
คุณสามารถบอกสิ่งนี้ได้โดยดูที่ id parent ของ daemon id parent ของ daemon จะเป็น 1 เสมอ ในตัวอย่างด้านล่าง เราใช้ pstree เพื่อแสดงสิ่งนี้:
$ pstree
-+= 00001 root /sbin/launchd
|--- 72314 snhorne rubydaemon
ที่น่าสนใจก็คือ นี่คือสิ่งที่ "กระบวนการกำพร้า" ดูเหมือน กระบวนการกำพร้าเป็นกระบวนการย่อยที่ผู้ปกครองได้ยุติ
ดังนั้น ในการสร้าง daemon เราต้องตั้งใจทำให้กระบวนการเป็นกำพร้า รหัสด้านล่างทำเช่นนี้
# Optional: set the process name to something easy to type
$PROGRAM_NAME = "rubydaemon"
# Create a new child process and exit the parent. This "orphans"
# our process and creates a daemon.
exit if fork()
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
การเรียก fork ส่งผลให้เกิดกระบวนการสองกระบวนการที่เรียกใช้โค้ดเดียวกัน กระบวนการดั้งเดิมคือพาเรนต์ของกระบวนการใหม่ Fork ส่งคืนค่าความจริงสำหรับผู้ปกครองและค่าที่เป็นเท็จสำหรับเด็ก ดังนั้น exit if fork()
ออกจากพาเรนต์เท่านั้น
กำลังถอดออกจากเซสชันปัจจุบัน
รหัส "daemonization" ของเรามีปัญหาเล็กน้อย แม้ว่ากระบวนการนี้จะสำเร็จแบบไร้ประสิทธิภาพ แต่ก็ยังเป็นส่วนหนึ่งของเซสชันของเทอร์มินัล นั่นหมายความว่า ถ้าคุณฆ่าเทอร์มินัล คุณจะฆ่าภูต ในการแก้ไขปัญหานี้ เราต้องสร้างเซสชันใหม่และแยกใหม่ ไม่คุ้นเคยกับกลุ่มเซสชันยูนิกซ์? นี่คือโพสต์ StackOverflow ที่ดี
# Optional: set the process name to something easy to type
$PROGRAM_NAME = "rubydaemon"
# Create a new child process and exit the parent. This "orphans"
# our process and creates a daemon.
exit if fork
# Create a new session, create a new child process in it and
# exit the current process.
Process.setsid
exit if fork
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
กำหนดเส้นทาง STDIN, STDOUT และ STDERR ใหม่
ปัญหาอื่นที่โค้ดด้านบนมีก็คือมันปล่อยให้ STDOUT ที่มีอยู่ ฯลฯ เข้าที่ นั่นหมายความว่าหากคุณเปิด daemon จากเทอร์มินัล สิ่งใดก็ตามที่ daemon เขียนไปยัง STDOUT จะถูกส่งไปยังเทอร์มินัลของคุณ ไม่ดี
แต่คุณสามารถกำหนดเส้นทาง STDIN, STDOUT และ STDERR ใหม่ไปยังเส้นทางใดก็ได้ ที่นี่เราเปลี่ยนเส้นทางไปยัง /dev/null
# Optional: set the process name to something easy to type
$PROGRAM_NAME = "rubydaemon"
# Create a new child process and exit the parent. This "orphans"
# our process and creates a daemon.
exit if fork
# Create a new session, create a new child process in it and
# exit the current process.
Process.setsid
exit if fork
STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen '/dev/null', 'a'
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
การเปลี่ยนไดเร็กทอรีการทำงาน
สุดท้าย ไดเร็กทอรีการทำงานของ daemon คือไดเร็กทอรีใดก็ตามที่เราอยู่เมื่อเรารันมัน นั่นอาจไม่ใช่ความคิดที่ดีที่สุด เนื่องจากฉันอาจตัดสินใจลบไดเร็กทอรีในภายหลัง งั้นเปลี่ยนไดเร็กทอรีเป็น / .
# Optional: set the process name to something easy to type
$PROGRAM_NAME = "rubydaemon"
# Create a new child process and exit the parent. This "orphans"
# our process and creates a daemon.
exit if fork
# Create a new session, create a new child process in it and
# exit the current process.
Process.setsid
exit if fork
STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen '/dev/null', 'a'
Dir.chdir("/")
# Once per second, log the current time to a file
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
ฉันไม่เคยเห็นมาก่อนหรือ
ลำดับของขั้นตอนนี้โดยพื้นฐานแล้วเป็นสิ่งที่ Ruby daemon ทุกตัวต้องทำก่อนที่จะเพิ่มวิธีการ Process.daemon ให้กับ Ruby core ฉันค่อนข้างคัดลอกบรรทัดจากส่วนขยาย ActiveSupport ไปยังโมดูลกระบวนการซึ่งถูกลบใน Rails 4.x ดูวิธีการได้ที่นี่