예를 들어, 다음과 유사한 셸 스크립트가 있다고 가정합니다.
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/( timeout
GNU보다 몇 달 빠릅니다).
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 대신 파이프를 사용하는 것입니다.cmd
bash
ksh93
read -t
sleep
{
{
cmd 4>&1 >&3 3>&- &
printf '%d\n.' "$!"
} | {
read p
read -t 86400 || kill "$p"
}
} 3>&1
명령에 여전히 경쟁 조건이 있으므로 명령의 종료 상태가 손실됩니다. 또한 cmd
fd 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
프로세스를 시작할 때 프로세스의 시작 시간을 저장하십시오.
longrunningthing & p=$! stime=$(TZ=UTC0 ps -p "$p" -o lstart=) echo "Killing longrunningthing on PID $p in 24 hours" sleep 86400 echo Time up!
프로세스를 종료하기 전에 중지하십시오. (꼭 필요한 것은 아니지만 경쟁 조건을 피하는 방법입니다. 프로세스를 중지하면 해당 pid를 재사용할 수 없습니다.)
kill -s STOP "$p"
해당 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을 사용하고psutil
PID 재사용을 자동으로 처리합니다.
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 파일이렇게 하면 프로세스에 대해 최소한 어느 정도 제한된 제어가 가능해집니다. 래퍼를 포함하여 원본 바이너리를 수정하지 않고 이 작업을 수행하는 여러 가지 방법이 있습니다. 예를 들어:
백그라운드에서 필요한 작업을 시작하고(선택적 출력 리디렉션 사용) 프로세스의 PID를 파일에 쓴 다음 프로세스가 완료될 때까지 기다린 후( 사용
wait
) 파일을 삭제하는 간단한 래퍼 스크립트입니다. 대기 중에 프로세스가 종료되는 경우(예:kill $(cat pidfile)
래퍼는 pidfile이 삭제되었는지 확인만 합니다.
배치할 모니터 래퍼그것은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 시스템에서 사용하는 것 같습니다.