Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

Ruby의 유닉스 데몬에 대한 이론적 소개

Unix 데몬은 백그라운드에서 실행되는 프로그램입니다. Nginx, Postgres, OpenSSH가 몇 가지 예입니다. 그들은 몇 가지 특별한 트릭을 사용하여 프로세스를 "분리"하고 모든 터미널과 독립적으로 실행되도록 합니다.

저는 항상 데몬에 매료되었습니다. 아마도 이름 때문일 것입니다. 그리고 데몬이 어떻게 작동하는지 설명하는 포스트를 작성하는 것이 재미있을 것이라고 생각했습니다. 특히 Ruby에서 생성하는 방법에 대해 설명합니다.

...하지만 먼저.

집에서 시도하지 마세요!

아마도 데몬을 만들고 싶지 않을 것입니다. 작업을 완료하는 훨씬 쉬운 방법이 있습니다.

백그라운드에서 실행되는 프로그램을 만들고 싶을 수도 있습니다. 문제 없어요. OS는 백그라운드에서 일반 프로그램을 실행할 수 있는 시스템을 제공합니다.

우분투에서 이것은 systemd의 Upstart를 통해 수행됩니다. OSX에서는 시작되었습니다. 다른 사람들이 있습니다. 그러나 그들은 모두 같은 개념에 따라 작동합니다. 장기 실행 프로그램을 시작하고 중지하는 방법을 시스템에 알려주는 구성 파일을 제공합니다. 그럼...음, 그 정도입니다. service my_app start와 같은 시스템 명령을 사용하여 프로그램을 시작할 수 있습니다. 백그라운드에서 실행됩니다.

요컨대, 신생은 간단하고 신뢰할 수 있지만 구식 데몬은 불가사의하고 제대로 하기가 매우 어렵습니다.

...그렇다면 왜 데몬에 대해 배워야 합니까? 글쎄, 재미 때문에! 그리고 그 과정에서 Unix 프로세스에 대한 몇 가지 흥미로운 사실을 배우게 됩니다.

가장 단순한 데몬

데몬을 만들지 말라고 들었으니 이제 데몬을 만들어 봅시다! 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

이제 이 스크립트를 실행하면 제어가 콘솔로 다시 전달됩니다. 로그를 추적하면 예상대로 타임스탬프가 1초마다 추가되는 것을 볼 수 있습니다.

Ruby의 유닉스 데몬에 대한 이론적 소개

그래서 쉬웠습니다. 그러나 여전히 데몬이 어떻게 작동하는지 설명하지 않습니다. 정말 수동으로 데몬화를 수행해야 합니다.

상위 프로세스 변경

bash를 사용하여 일반 프로그램을 실행하는 경우 해당 프로그램의 프로세스는 bash의 자식입니다. 그러나 데몬을 사용하면 데몬을 실행하는 방법이 중요하지 않습니다. 상위 프로세스는 항상 OS에서 제공하는 "루트" 프로세스입니다.

데몬의 부모 ID를 보면 알 수 있습니다. 데몬의 부모 ID는 항상 1입니다. 아래 예에서는 pstree를 사용하여 이를 표시합니다.

$ pstree
-+= 00001 root /sbin/launchd
 |--- 72314 snhorne rubydaemon

흥미롭게도 이것은 "고아 프로세스"의 모습이기도 합니다. 고아 프로세스는 부모가 종료된 자식 프로세스입니다.

따라서 데몬을 만들려면 의도적으로 프로세스를 분리해야 합니다. 아래 코드가 이 작업을 수행합니다.

# 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() 부모만 종료합니다.

현재 세션에서 분리

우리의 "데몬화" 코드에는 몇 가지 문제가 있습니다. 프로세스를 성공적으로 분리하지만  여전히 터미널 세션의 일부입니다. 즉, 터미널을 죽이면 데몬도 죽입니다. 이 문제를 해결하려면 새 세션을 만들고 다시 포크해야 합니다. 유닉스 세션 그룹에 익숙하지 않습니까? 다음은 좋은 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 등을 그대로 두는 것입니다. 즉, 터미널에서 데몬을 시작하면 데몬이 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

작업 디렉토리 변경

마지막으로 데몬의 작업 디렉토리는 실행했을 때 있었던 디렉토리입니다. 나중에 디렉토리를 삭제하기로 결정할 수 있으므로 이는 아마도 최선의 생각이 아닐 것입니다. 디렉토리를 / .

로 변경해 보겠습니다.
# 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

이전에 본 적이 없습니까?

이 일련의 단계는 기본적으로 Process.daemon 메서드가 Ruby 코어에 추가되기 전에 모든 Ruby 데몬이 수행해야 하는 작업입니다. ActiveSupport 확장에서 Rails 4.x에서 제거된 Process 모듈로 거의 한 줄씩 복사했습니다. 그 방법은 여기에서 볼 수 있습니다.