Unix daemon là các chương trình chạy trong background. Nginx, Postgres và OpenSSH là một vài ví dụ. Chúng sử dụng một số thủ thuật đặc biệt để "tách" tiến trình của mình, cho phép chạy độc lập với bất kỳ terminal nào.
Tôi luôn hơi say mê với daemon - có lẽ vì cái tên - và nghĩ viết bài minh họa cách chúng hoạt động sẽ vui. Và cụ thể, cách tạo chúng trong Ruby.
...nhưng trước tiên.
Đừng thử tại nhà!
Bạn có thể không muốn tự tạo daemon. Có cách dễ dàng hơn để làm việc.
Bạn có thể muốn chương trình chạy trong background. Không vấn đề gì. Hệ điều hành cung cấp hệ thống để chạy chương trình thông thường trong background.
Trên Ubuntu, điều này thực hiện qua Upstart hoặc systemd. Trên OSX là launchd. Có những cái khác. Nhưng tất cả hoạt động theo cùng khái niệm. Bạn cung cấp file cấu hình nói cho hệ thống cách khởi động/dừng chương trình chạy lâu. Sau đó... chỉ thế thôi. Bạn có thể khởi động chương trình bằng lệnh hệ thống như service my_app start và nó chạy trong background.
Tóm lại, upstart đơn giản & tin cậy trong khi daemon kiểu cũ là huyền bí và rất khó làm đúng.
...nhưng nếu thế, tại sao chúng ta nên học về daemon? Vì vui thôi! Và chúng ta sẽ học được những thực tế thú vị về tiến trình Unix.
Daemon đơn giản nhất
Sau khi đã được khuyên đừng tự tạo daemon, hãy tạo một vài daemon! Từ Ruby 1.9, điều này cực kỳ đơn giản. Chỉ cần dùng phương thức Process.daemon.
# Tùy chọn: đặt tên tiến trình dễ gõ $PROGRAM_NAME = "rubydaemon" # Biến tiến trình hiện tại thành daemon Process.daemon() # Mỗi giây, ghi thời gian hiện tại vào file loop do File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) } sleep(1) end
Khi chạy script này, điều khiển trả về console. Nếu tôi tail log, thấy timestamp được thêm mỗi giây, đúng như mong đợi.

Vậy là dễ. Nhưng vẫn chưa giải thích daemon hoạt động như thế nào. Để thực sự hiểu, cần làm daemon hóa thủ công.
Thay đổi tiến trình cha
Nếu bạn dùng bash chạy chương trình thông thường, tiến trình của chương trình là con của bash. Nhưng với daemon, không quan trọng bạn khởi động thế nào. Tiến trình cha của chúng luôn là tiến trình "root" do OS cung cấp.
Bạn có thể thấy điều này bằng cách nhìn parent id của daemon. Parent id của daemon luôn là 1. Trong ví dụ dưới dùng pstree để chứng minh:
$ pstree -+= 00001 root /sbin/launchd |--- 72314 snhorne rubydaemon
Thú vị là, đây cũng chính là hình dáng của "tiến trình mồ côi" (orphaned processes). Tiến trình mồ côi là tiến trình con mà tiến trình cha đã kết thúc.
Vì vậy, để tạo daemon, chúng ta phải cố tình làm mồ côi một tiến trình. Code dưới đây làm điều đó.
# Tùy chọn: đặt tên tiến trình dễ gõ $PROGRAM_NAME = "rubydaemon" # Tạo tiến trình con mới và thoát tiến trình cha. Điều này "làm mồ côi" # tiến trình của chúng ta và tạo daemon. exit if fork() # Mỗi giây, ghi thời gian hiện tại vào file loop do File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) } sleep(1) end
Lệnh fork tạo ra hai tiến trình chạy cùng code. Tiến trình gốc là cha của tiến trình mới. Fork trả về giá trị truthy cho cha và falsy cho con. Vì vậy exit if fork() chỉ thoát tiến trình cha.
Tách ra khỏi phiên hiện tại
Code "daemon hóa" của chúng ta còn vài vấn đề. Mặc dù nó làm mồ côi tiến trình thành công, nhưng nó vẫn thuộc phiên của terminal. Nghĩa là nếu bạn kill terminal, bạn kill daemon. Để sửa, cần tạo phiên mới và fork lại. Không quen với nhóm phiên Unix? Đây là bài StackOverflow hay.
# Tùy chọn: đặt tên tiến trình dễ gõ $PROGRAM_NAME = "rubydaemon" # Tạo tiến trình con mới và thoát tiến trình cha. Điều này "làm mồ côi" # tiến trình của chúng ta và tạo daemon. exit if fork # Tạo phiên mới, tạo tiến trình con trong đó và thoát tiến trình hiện tại. Process.setsid exit if fork # Mỗi giây, ghi thời gian hiện tại vào file loop do File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) } sleep(1) end
Điều hướng lại STDIN, STDOUT và STDERR
Vấn đề khác của code trên là nó để STDIN, STDOUT, STDERR nguyên chỗ. Nghĩa là nếu bạn khởi động daemon từ terminal, bất cứ thứ gì daemon ghi ra STDOUT sẽ gửi tới terminal của bạn. Không tốt.
Nhưng bạn thực sự có thể điều hướng lại STDIN, STDOUT, STDERR đến bất kỳ đường dẫn nào. Ở đây điều hướng tới /dev/null.
# Tùy chọn: đặt tên tiến trình dễ gõ $PROGRAM_NAME = "rubydaemon" # Tạo tiến trình con mới và thoát tiến trình cha. Điều này "làm mồ côi" # tiến trình của chúng ta và tạo daemon. exit if fork # Tạo phiên mới, tạo tiến trình con trong đó và thoát tiến trình hiện tại. Process.setsid exit if fork STDIN.reopen "/dev/null" STDOUT.reopen "/dev/null", "a" STDERR.reopen '/dev/null', 'a' # Mỗi giây, ghi thời gian hiện tại vào file loop do File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) } sleep(1) end
Thay đổi thư mục làm việc
Cuối cùng, thư mục làm việc của daemon là thư mục nào chúng ta đang ở khi chạy nó. Có thể không phải ý tưởng hay, vì tôi có thể quyết định xóa thư mục sau. Vì vậy hãy đổi thư mục thành / .
# Tùy chọn: đặt tên tiến trình dễ gõ $PROGRAM_NAME = "rubydaemon" # Tạo tiến trình con mới và thoát tiến trình cha. Điều này "làm mồ côi" # tiến trình của chúng ta và tạo daemon. exit if fork # Tạo phiên mới, tạo tiến trình con trong đó và thoát tiến trình hiện tại. Process.setsid exit if fork STDIN.reopen "/dev/null" STDOUT.reopen "/dev/null", "a" STDERR.reopen '/dev/null', 'a' Dir.chdir("/") # Mỗi giây, ghi thời gian hiện tại vào file loop do File.open("/tmp/rubydaemon.log", "a") { |f| f.puts(Time.now) } sleep(1) end
Chẳng lẽ tôi đã thấy cái này trước?
Chuỗi bước này về cơ bản là những gì mọi Ruby daemon phải làm trước khi phương thức Process.daemon được thêm vào Ruby core. Tôi sao chép gần như từng dòng từ một extension ActiveSupport cho module Process bị xóa trong Rails 4.x. Bạn có thể thấy phương thức đó ở đây.