Unix daemon là chương trình chạy nền. Nginx, Postgres và OpenSSH là một vài ví dụ. Họ sử dụng một số thủ thuật đặc biệt để "phát hiện" các quy trình của họ và cho phép chúng chạy độc lập với bất kỳ thiết bị đầu cuối nào.
Tôi luôn bị mê hoặc bởi daemon - có lẽ đó là cái tên - và tôi nghĩ sẽ rất vui khi thực hiện một bài đăng minh họa cách chúng hoạt động. Và cụ thể là cách bạn có thể tạo chúng trong Ruby.
... nhưng trước tiên.
Đừng thử cách này ở nhà!
Bạn có thể không muốn tạo một daemon. Có nhiều cách dễ dàng hơn để hoàn thành công việc.
Bạn có thể muốn tạo một chương trình chạy ở chế độ nền. Không vấn đề gì. Hệ điều hành của bạn cung cấp một hệ thống cho phép bạn chạy các chương trình bình thường trong nền.
Trên ubuntu, điều này được thực hiện thông qua Upstart của systemd. Trên OSX, nó khởi chạy. Co nhung nguoi khac. Nhưng tất cả chúng đều hoạt động theo cùng một khái niệm. Bạn cung cấp tệp cấu hình cho hệ thống biết cách khởi động và dừng chương trình đang chạy lâu dài. Vậy thì ... tốt, đó là khá nhiều. 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 nền.
Tóm lại, việc mới bắt đầu rất đơn giản và đáng tin cậy trong khi các daemon kiểu cũ rất phức tạp và rất khó để làm đúng.
... nhưng nếu đúng như vậy, tại sao chúng ta phải tìm hiểu về daemon? Chà, vì vui! Và chúng ta sẽ tìm hiểu một số thông tin thú vị về các quy trình Unix trong quá trình này.
Trình nền đơn giản nhất
Bây giờ bạn đã được yêu cầu không bao giờ tạo daemon, hãy tạo một số daemon! Đối với Ruby 1.9, điều này cực kỳ đơn giản. Tất cả những gì bạn phải làm là sử dụng phương thức 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
Bây giờ, khi tôi chạy tập lệnh này, điều khiển sẽ chuyển trở lại bảng điều khiển. Nếu tôi chỉnh sửa nhật ký của mình, tôi có thể thấy rằng dấu thời gian đang được thêm vào mỗi giây, giống như tôi mong đợi.
Vì vậy, điều đó thật dễ dàng. Nhưng nó vẫn không giải thích cách thức hoạt động của daemon. Để thực sự hiểu rằng, chúng tôi cần thực hiện daemonization theo cách thủ công.
Thay đổi quy trình chính
Nếu bạn sử dụng bash để chạy một chương trình bình thường, thì 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 chạy chúng như thế nào. Quy trình mẹ của chúng luôn là quy trình "gốc" do OS cung cấp.
Bạn có thể biết điều này bằng cách nhìn vào id cha của daemon. Id cha của daemon luôn là 1. Trong ví dụ dưới đây, chúng tôi sử dụng pstree để hiển thị điều này:
$ pstree
-+= 00001 root /sbin/launchd
|--- 72314 snhorne rubydaemon
Thật thú vị, đây cũng là "các quy trình mồ côi" trông như thế nào. Quy trình dành cho trẻ mồ côi là quy trình con có cha mẹ đã chấm dứt.
Vì vậy, để tạo một daemon, chúng ta phải cố tình bỏ qua một quá trình. Đoạn mã dưới đây thực hiện điều này.
# 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
Lời gọi fork dẫn đến hai quá trình chạy cùng một mã. Quy trình ban đầu là cha của quy trình mới. Fork trả về một giá trị true cho cha mẹ và một giá trị falsy cho con. Vì vậy, exit if fork()
chỉ thoát khỏi trang gốc.
Đang phát hiện từ phiên hiện tại
Mã "daemonization" của chúng tôi có một số vấn đề. Mặc dù nó đã thành công trong quá trình, nhưng nó vẫn là một phần của phiên làm việc của thiết bị đầu cuối. Điều đó có nghĩa là nếu bạn giết thiết bị đầu cuối, bạn sẽ giết daemon. Để khắc phục điều này, chúng ta cần tạo một phiên mới và phân tách lại. Không quen thuộc với các nhóm phiên unix? Đây là một bài đăng StackOverflow hay.
# 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
Định tuyến lại STDIN, STDOUT và STDERR
Một vấn đề khác mà đoạn mã trên gặp phải là nó để lại STDOUT hiện có, v.v. Điều đó có nghĩa là nếu bạn khởi chạy daemon từ một thiết bị đầu cuối, mọi thứ mà daemon ghi vào STDOUT sẽ được gửi đến thiết bị đầu cuối của bạn. Không tốt.
Nhưng bạn thực sự có thể định tuyến lại STDIN, STDOUT và STDERR đến bất kỳ đường dẫn nào. Ở đây chúng tôi định tuyến lại đến / 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
Thay đổi thư mục làm việc
Cuối cùng, thư mục làm việc của daemon là bất kỳ thư mục nào mà chúng ta tình cờ ở trong khi chạy nó. Đó có lẽ không phải là ý kiến hay nhất, vì tôi có thể quyết định xóa thư mục sau này. Vì vậy, hãy thay đổi thư mục thành /.
# 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
Tôi chưa thấy điều này bao giờ?
Chuỗi các bước này về cơ bản là những gì mọi trình nền Ruby phải làm trước khi phương thức Process.daemon được thêm vào lõi Ruby. Tôi đã sao chép khá nhiều dòng từ một phần mở rộng ActiveSupport sang mô-đun Process đã bị loại bỏ trong Rails 4.x. Bạn có thể xem phương pháp đó tại đây.