프로세스를 종료하고 PID가 재사용되지 않도록 하는 방법

프로세스를 종료하고 PID가 재사용되지 않도록 하는 방법

예를 들어, 다음과 유사한 셸 스크립트가 있다고 가정합니다.

longrunningthing &
p=$!
echo Killing longrunningthing on PID $p in 24 hours
sleep 86400
echo Time up!
kill $p

그게 효과가 있겠지? 프로세스가 조기에 종료되었을 수 있고 해당 PID가 재활용되었을 수 있다는 사실 외에도 이는 일부 무고한 작업이 신호 대기열에서 폭탄을 받았다는 것을 의미합니다. 실제로 이것이 중요할 수도 있지만 여전히 걱정스럽습니다. 오래 실행되는 항목을 해킹하여 자체적으로 종료하거나 FS에서 PID를 유지/제거하는 것은 괜찮지만 여기서는 일반적인 경우를 생각하고 있습니다.

답변1

다음 명령을 사용하는 것이 더 좋습니다 timeout(사용 가능한 경우).

timeout 86400 cmd

현재(8.23) GNU 구현은 최소한 alarm()하위 프로세스를 기다리는 동안 또는 이와 동등한 기능을 사용하여 작동합니다. SIGALRM반환과 종료 사이의 전달을 막지는 못하는 것 같습니다 (효과적으로 취소waitpid()timeout경보). 이 작은 창 동안 timeout메시지는 stderr에 기록될 수도 있으며(예: 하위 프로세스가 코어를 덤프하는 경우) 경합 창은 더욱 넓어집니다(예: stderr이 전체 파이프인 경우 무한정).

개인적으로 이 제한 사항을 감수할 수 있습니다(향후 버전에서는 수정될 수 있음). timeout올바른 종료 상태를 보고하고, 기타 특수한 경우(예: 시작 시 SIGALRM이 차단/무시되고, 다른 신호를 처리하는 등)를 수동으로 수행하는 것보다 더 잘 처리하기 위해 특별한 주의가 필요합니다.

대략적으로 다음과 같이 작성할 수 있습니다 perl.

perl -MPOSIX -e '
  $p = fork();
  die "fork: $!\n" unless defined($p);
  if ($p) {
    $SIG{ALRM} = sub {
      kill "TERM", $p;
      exit 124;
    };
    alarm(86400);
    wait;
    exit (WIFSIGNALED($?) ? WTERMSIG($?)+128 : WEXITSTATUS($?))
  } else {exec @ARGV}' cmd

timelimit명령이 있습니다http://devel.ringlet.net/sysutils/timelimit/( timeoutGNU보다 몇 달 빠릅니다).

 timelimit -t 86400 cmd

이 방법은 유사한 메커니즘을 사용 하지만 자식의 죽음을 감지하기 위해 alarm()처리기(중지된 자식 무시)를 설치합니다 . SIGCHLD또한 실행하기 전에 경고를 취소하고 waitpid()(보류 중인 경우 전달을 취소하지 않지만 SIGALRM작성된 방식에서는 문제가 되지 않음) 종료합니다.앞으로호출됩니다 waitpid()(따라서 재사용된 PID는 종료될 수 없습니다).

네트워크 파이프명령이 하나 더 있습니다 timelimit. 다른 모든 방법보다 수십 년 앞선 이 방법은 대체 접근 방식을 취하지만 중지된 명령에 대해서는 제대로 작동하지 않으며 1시간 초과 시 종료 상태를 반환합니다.

귀하의 질문에 대한 보다 직접적인 답변으로 다음을 수행할 수 있습니다.

if [ "$(ps -o ppid= -p "$p")" -eq "$$" ]; then
  kill "$p"
fi

즉, 해당 프로세스가 여전히 자식 프로세스인지 확인하세요. 마찬가지로, 프로세스가 종료되고 해당 pid가 다른 프로세스에서 재사용될 수 있는 작은 경합 기간( ps프로세스 상태 검색과 kill종료 사이)이 있습니다 .

일부 쉘( zsh, bash, , mksh)을 사용하면 pid 대신 작업 사양을 전달할 수 있습니다.

cmd &
sleep 86400
kill %
wait "$!" # to retrieve the exit status

이는 하나의 백그라운드 작업만 생성하는 경우에만 작동합니다(그렇지 않으면 올바른 작업 사양을 안정적으로 얻는 것이 항상 가능한 것은 아닙니다).

이것이 문제라면 새로운 셸 인스턴스를 시작하세요.

bash -c '"$@" & sleep 86400; kill %; wait "$!"' sh cmd

이는 자식이 죽을 때 쉘이 할당 목록에서 할당을 제거하기 때문에 작동합니다. 여기서는 경합 창이 없어야 합니다. 왜냐하면 셸이 호출될 때 kill()SIGCHLD 신호가 아직 처리되지 않았고 pid를 재사용할 수 없거나(기다리지 않았기 때문에) 이미 처리되어 pid를 사용할 수 없기 때문입니다. 작업이 프로세스 테이블에서 제거되었습니다( kill오류가 보고됨). 확장을 위해 작업 테이블에 액세스하기 전에 최소한 SIGCHLD를 차단 bash하고 나중에 차단을 해제하세요.kill%kill()

사망 후에도 보류 중인 프로세스를 방지하는 sleep또 다른 옵션은 or 대신 파이프를 사용하는 것입니다.cmdbashksh93read -tsleep

{
  {
    cmd 4>&1 >&3 3>&- &
    printf '%d\n.' "$!"
  } | {
    read p
    read -t 86400 || kill "$p"
  }
} 3>&1

명령에 여전히 경쟁 조건이 있으므로 명령의 종료 상태가 손실됩니다. 또한 cmdfd 4를 닫지 않는다고 가정합니다.

다음과 같은 경쟁 없는 솔루션을 구현해 볼 수 있습니다 perl.

perl -MPOSIX -e '
   $p = fork();
   die "fork: $!\n" unless defined($p);
   if ($p) {
     $SIG{CHLD} = sub {
       $ss = POSIX::SigSet->new(SIGALRM); $oss = POSIX::SigSet->new;
       sigprocmask(SIG_BLOCK, $ss, $oss);
       waitpid($p,WNOHANG);
       exit (WIFSIGNALED($?) ? WTERMSIG($?)+128 : WEXITSTATUS($?))
           unless $? == -1;
       sigprocmask(SIG_UNBLOCK, $oss);
     };
     $SIG{ALRM} = sub {
       kill "TERM", $p;
       exit 124;
     };
     alarm(86400);
     pause while 1;
   } else {exec @ARGV}' cmd args...

(다른 유형의 코너 케이스를 처리하려면 개선이 필요하지만)

또 다른 경합 없는 접근 방식은 프로세스 그룹을 사용하는 것입니다.

set -m
((sleep 86400; kill 0) & exec cmd)

그러나 터미널 장치에 대한 I/O가 관련된 경우 프로세스 그룹을 사용하면 부작용이 있을 수 있습니다. 또한 에 의해 생성된 다른 모든 추가 프로세스를 종료할 수 있다는 추가 이점도 있습니다 cmd.

답변2

일반적으로 말하면 할 수 없습니다. 지금까지 제공된 모든 답변은 결함이 있는 경험적 방법입니다. pid를 사용하여 신호를 보내는 것이 안전한 경우는 단 하나뿐입니다. 대상 프로세스가 신호를 보낼 프로세스의 직접적인 하위 프로세스이고 상위 프로세스가 아직 신호를 기다리고 있지 않은 경우입니다. 이 경우, 종료되더라도 상위 프로세스가 기다릴 때까지 pid는 유지됩니다(이것이 "좀비 프로세스"입니다). 나는 쉘로 이것을 깨끗하게 수행하는 방법을 모른다.

프로세스를 종료하는 또 다른 안전한 방법은 마스터가 있는 의사 터미널에 설정된 컨트롤 tty를 사용하여 프로세스를 시작하는 것입니다. 그런 다음 터미널을 통해 신호를 보낼 수 있습니다(예: pty로 보내 SIGTERM거나 pty를 통해 문자를 쓸 수 있음).SIGQUIT

또 다른 더 편리한 스크립트 방법은 명명된 screen세션을 사용하고 screen 세션에 명령을 보내 종료하는 것입니다. 이 프로세스는 스크린 세션에 따라 명명된 파이프 또는 유닉스 소켓을 통해 발생하며 안전한 고유 이름을 선택하면 자동으로 재사용되지 않습니다.

답변3

  1. 프로세스를 시작할 때 프로세스의 시작 시간을 저장하십시오.

    longrunningthing &
    p=$!
    stime=$(TZ=UTC0 ps -p "$p" -o lstart=)
    
    echo "Killing longrunningthing on PID $p in 24 hours"
    sleep 86400
    echo Time up!
    
  2. 프로세스를 종료하기 전에 중지하십시오. (꼭 필요한 것은 아니지만 경쟁 조건을 피하는 방법입니다. 프로세스를 중지하면 해당 pid를 재사용할 수 없습니다.)

    kill -s STOP "$p"
    
  3. 해당 PID를 가진 프로세스의 시작 시간이 동일한지 확인하고, 그렇다면 종료하고, 그렇지 않으면 프로세스를 계속 진행합니다.

    cur=$(TZ=UTC0 ps -p "$p" -o lstart=)
    
    if [ "$cur" = "$stime" ]
    then
        # Okay, we can kill that process
        kill "$p"
    else
        # PID was reused. Better unblock the process!
        echo "long running task already completed!"
        kill -s CONT "$p"
    fi
    

이는 동일한 PID를 가진 프로세스가 하나만 있을 수 있기 때문에 작동합니다.그리고특정 운영 체제의 부팅 시간.

검사 중에 프로세스를 중지하면 경쟁 조건이 덜 문제가 됩니다. 분명히 여기에는 문제가 있습니다. 일부 무작위 프로세스가 몇 밀리초 동안 멈출 수 있습니다. 프로세스 유형에 따라 문제가 될 수도 있고 아닐 수도 있습니다.


개인적으로 저는 단순히 Python을 사용하고psutilPID 재사용을 자동으로 처리합니다.

import time

import psutil

# note: it would be better if you were able to avoid using
#       shell=True here.
proc = psutil.Process('longrunningtask', shell=True)
time.sleep(86400)

# PID reuse handled by the library, no need to worry.
proc.terminate()   # or: proc.kill()

답변4

당신의 longrunningthing행동을 좀 더 개선하고 데몬과 비슷하게 만드는 것을 고려해보세요. 예를 들어pid 파일이렇게 하면 프로세스에 대해 최소한 어느 정도 제한된 제어가 가능해집니다. 래퍼를 포함하여 원본 바이너리를 수정하지 않고 이 작업을 수행하는 여러 가지 방법이 있습니다. 예를 들어:

  1. 백그라운드에서 필요한 작업을 시작하고(선택적 출력 리디렉션 사용) 프로세스의 PID를 파일에 쓴 다음 프로세스가 완료될 때까지 기다린 후( 사용 wait) 파일을 삭제하는 간단한 래퍼 스크립트입니다. 대기 중에 프로세스가 종료되는 경우(예:

    kill $(cat pidfile)
    

    래퍼는 pidfile이 삭제되었는지 확인만 합니다.

  2. 배치할 모니터 래퍼그것은PID를 어딘가에 두고 전송된 신호를 포착하고 응답합니다. 간단한 예:

    #!/bin/bash
    p=0
    trap killit USR1

    killit () {
        printf "USR1 caught, killing %s\n" "$p"
        kill -9 $p
    }

    printf "monitor $$ is waiting\n"
    therealstuff &
    p=%1
    wait $p
    printf "monitor exiting\n"

이제 @R.. 및 @StéphaneChazelas가 지적했듯이 이러한 방법은 일반적으로 어딘가에 경쟁 조건이 있거나 생성될 수 있는 프로세스 수에 제한을 가합니다. 또한 하위 항목을 분기하고 분리할 수 있는 경우 longrunningthing(원래 질문에서는 문제가 되지 않았을 수 있음)를 처리하지 않습니다.

최근(몇 년 전) Linux 커널의 경우 이 문제는 다음을 사용하여 훌륭하게 처리될 수 있습니다.cgroup,지금 바로냉장고- 제 생각에는 이것이 일부 최신 Linux init 시스템에서 사용하는 것 같습니다.

관련 정보