Unix daemons คือโปรแกรมที่รันในพื้นหลัง (background) ตัวอย่างเช่น Nginx, Postgres และ OpenSSH พวกมันใช้เทคนิคพิเศษเพื่อ "แยก" (detach) process ของพวกมัน ให้รันอิสระจาก terminal ใดๆ
ผมมักหลงใหลกับ daemons - อาจเป็นเพราะชื่อมัน - และคิดว่าจะสนุกถ้าเขียนบทความอธิบายการทำงานของมัน โดยเฉพาะวิธีสร้างใน Ruby
...แต่มาก่อน
อย่าลองทำที่บ้าน!
คุณอาจไม่ต้องการสร้าง daemon จริงๆ มีวิธีง่ายกว่านี้มาก
คุณอาจต้องการโปรแกรมที่รันในพื้นหลัง ไม่มีปัญหา OS ของคุณมีระบบสำหรับรันโปรแกรมทั่วไปในพื้นหลังอยู่แล้ว
บน Ubuntu ทำได้ผ่าน systemd (ก่อนหน้านี้คือ Upstart) บน macOS คือ launchd มีอื่นๆ อีก แต่ทุกตัวทำงานด้วยแนวคิดเดียวกัน: คุณให้ไฟล์ config บอกระบบวิธี start/stop โปรแกรมที่รันนาน แล้ว...แค่นั้นแหละ คุณเริ่มโปรแกรมด้วยคำสั่งอย่าง systemctl start my_app มันก็รันในพื้นหลัง
สรุป: systemd เรียบง่ายและน่าเชื่อถือ ส่วน daemons แบบเก่าๆ ซับซ้อนและยากมากที่จะทำให้ถูกต้อง
...แต่ถ้าเป็นเช่นนั้น ทำไมเราต้องเรียนรู้ daemons? เพราะสนุก! และเราจะเรียนรู้ข้อเท็จจริงน่าสนใจเกี่ยวกับ Unix processes
Daemon ที่เรียบง่ายที่สุด
ตอนนี้ที่คุณถูกเตือนแล้ว ให้สร้าง daemons กัน! ตั้งแต่ Ruby 1.9 นี้ง่ายมาก แค่ใช้เมธอด Process.daemon
# Optional: ตั้งชื่อ process ให้พิมพ์ง่าย
$PROGRAM_NAME = "rubydaemon"
# ทำให้ process ปัจจุบันกลายเป็น daemon
Process.daemon()
# ทุกวินาที log เวลาปัจจุบันลงไฟล์
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
ตอนรันสคริปต์นี้ ควบคุมจะคืนกลับมาที่ console หากผม tail log จะเห็น timestamp เพิ่มทุกวินาที ตามที่คาดหวัง

ง่ายมาก แต่ยังไม่ได้อธิบายว่า daemons ทำงานอย่างไร เพื่อเข้าใจจริงๆ เราต้องทำ daemonization เองด้วยมือ
การเปลี่ยน Parent Process
ถ้าคุณใช้ bash รันโปรแกรมทั่วไป process นั้นจะเป็น child ของ bash แต่ daemons ไม่ว่าจะ launch อย่างไร parent process จะเป็น "root" process ของ OS เสมอ
คุณบอกได้จาก parent ID ของ daemon ซึ่งจะเป็น 1 เสมอ ตัวอย่างด้านล่างใช้ pstree แสดง:
$ pstree -+=
00001 root /sbin/launchd
|--- 72314 snhorne rubydaemon
น่าสนใจที่นี่คือ orphaned processes (child process ที่ parent จบลงแล้ว) ก็มีลักษณะเหมือนกัน
ดังนั้น เพื่อสร้าง daemon เราต้องทำให้ process กลายเป็น orphan โดยเจตนา โค้ดด้านล่างทำได้:
# Optional: ตั้งชื่อ process
$PROGRAM_NAME = "rubydaemon"
# สร้าง child process ใหม่และออกจาก parent นี่คือการ "orphan" process ของเรา
exit if fork()
# ทุกวินาที log เวลาปัจจุบันลงไฟล์
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
fork สร้างสอง process รันโค้ดเดียวกัน Process เดิมคือ parent ของ process ใหม่ fork คืนค่า truthy ให้ parent และ falsy ให้ child ดังนั้น exit if fork() จะออกแค่ parent เท่านั้น
การแยกจาก Session ปัจจุบัน
โค้ด "daemonization" ข้างต้นยังมีปัญหา: มัน orphan process ได้ แต่ยังอยู่ใน session ของ terminal หมายความว่าถ้าคุณ kill terminal daemon ก็จะตาย ต้องสร้าง session ใหม่และ fork อีกครั้ง
# Optional: ตั้งชื่อ process
$PROGRAM_NAME = "rubydaemon"
# สร้าง child process และออกจาก parent
exit if fork
# สร้าง session ใหม่ สร้าง child process ใหม่ใน session นั้น และออกจาก process ปัจจุบัน
Process.setsid
exit if fork
# ทุกวินาที log เวลาปัจจุบันลงไฟล์
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
การเปลี่ยนเส้นทาง STDIN, STDOUT และ STDERR
ปัญหาอีกอย่างคือ STDIN/STDOUT/STDERR ยังคงอยู่เดิม ถ้า launch daemon จาก terminal output ของ daemon จะไปที่ terminal ของคุณ - ไม่ดี
แต่เราสามารถ re-route ได้ไปที่ path ใดก็ได้ ที่นี่ route ไป /dev/null:
# Optional: ตั้งชื่อ process
$PROGRAM_NAME = "rubydaemon"
exit if fork
Process.setsid
exit if fork
STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen '/dev/null', 'a'
# ทุกวินาที log เวลาปัจจุบันลงไฟล์
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
การเปลี่ยน Working Directory
สุดท้าย working directory ของ daemon คือ directory ที่เราอยู่ตอนรัน อาจไม่ดีถ้าต่อมาเราลบ directory นั้น ให้เปลี่ยนไปที่ / แทน:
# Optional: ตั้งชื่อ process
$PROGRAM_NAME = "rubydaemon"
exit if fork
Process.setsid
exit if fork
STDIN.reopen "/dev/null"
STDOUT.reopen "/dev/null", "a"
STDERR.reopen '/dev/null', 'a'
Dir.chdir("/")
# ทุกวินาที log เวลาปัจจุบันลงไฟล์
loop do
File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) }
sleep(1)
end
รู้จักลำดับขั้นตอนนี้มั้ย?
ลำดับขั้นตอนนี้คือสิ่งที่ทุก Ruby daemon ต้องทำก่อนที่ Process.daemon จะถูกเพิ่มเข้า Ruby core ผมคัดลอกมาจาก ActiveSupport extension ของ Process module ที่ถูกลบใน Rails 4.x