Linux에서 프로세스가 처리되는 방식에 대한 오해로 인해 발생할 수 있는 일반적인 질문이 있습니다.
내 목적에 맞게 "스크립트"를 현재 사용자에게 실행 권한이 활성화된 텍스트 파일에 저장된 bash 코드 조각으로 정의하겠습니다.
서로 호출하는 일련의 스크립트가 있습니다. 단순화를 위해 이를 스크립트 A, B, C라고 부르겠습니다. 스크립트 A는 일련의 문을 실행한 다음 일시 중지하고 스크립트 B를 실행한 다음 일시 중지하고 스크립트 C를 실행합니다. 즉, 이 일련의 명령문 단계는 다음과 같습니다.
스크립트 A를 실행합니다.
- 시리즈 명세서
- 정지시키다
- 스크립트 B 실행
- 정지시키다
- 스크립트 C 실행
처음 일시 중지할 때까지 스크립트 A를 실행한 다음 스크립트 B에서 편집을 수행하면 코드 재개를 허용할 때 해당 편집 내용이 코드 실행에 반영된다는 것을 경험을 통해 알고 있습니다. 마찬가지로, 스크립트 A가 일시 중지된 동안 스크립트 C를 편집한 다음 변경 사항을 저장한 후 계속 진행하도록 허용하면 해당 변경 사항이 코드 실행에 반영됩니다.
따라서 실제 질문은 스크립트 A가 실행 중인 동안 편집할 수 있는 방법이 있느냐는 것입니다. 아니면 실행이 시작되면 편집할 수 없나요?
답변1
Unix에서 대부분의 편집자가 작업하는 방식은 편집된 내용이 포함된 새 임시 파일을 만드는 것입니다. 편집된 파일을 저장하면 원본 파일은 삭제되고 임시 파일의 이름은 원래 이름으로 변경됩니다. (물론 데이터 손실을 방지하기 위한 다양한 보호 장치가 있습니다.) 예를 들어 ("in-place") 플래그로 사용하거나 호출할 sed
때 의 스타일이지만 실제로는 전혀 "in-place"가 아닙니다. "새 장소와 옛 이름"이라고 불러야 합니다.perl
-i
이것은 유닉스가 (적어도 로컬 파일 시스템의 경우) 열린 파일이 "삭제"되고 동일한 이름의 새 파일이 생성되더라도 닫힐 때까지 계속 존재하도록 보장하기 때문에 잘 작동합니다. (파일을 "삭제"하기 위한 유닉스 시스템 호출이 실제로 "링크 해제"라고 불리는 것은 우연이 아닙니다.) 따라서 일반적으로 쉘 인터프리터에 일부 소스 파일이 열려 있고 위의 방법으로 해당 파일을 "편집"하면 쉘은 여전히 원본 파일이 열려 있기 때문에 변경 사항을 볼 수 없습니다.
[참고: 모든 표준 기반 주석과 마찬가지로 위의 내용도 해석의 여지가 있으며 NFS와 같은 다양한 코너 케이스가 있습니다. 학계의 추가 의견을 환영하지만 예외도 있습니다. ]
물론 파일을 직접 수정하는 것도 가능합니다. 파일의 데이터를 덮어쓸 수는 있지만 후속 데이터를 모두 이동하지 않고는 삭제하거나 삽입할 수 없기 때문에 편집 작업이 다소 불편해집니다. . 또한 이 변환을 수행하면 파일 내용을 예측할 수 없으며 파일을 여는 프로세스가 영향을 받습니다. 이 문제(예: 데이터베이스 시스템)를 제거하려면 복잡한 수정 프로토콜 세트와 분산 잠금이 필요합니다. 이는 일반적인 파일 편집 유틸리티의 범위를 훨씬 뛰어넘는 것입니다.
따라서 쉘이 파일을 처리하는 동안 파일을 편집하려면 다음 두 가지 옵션이 있습니다.
파일에 추가할 수 있습니다. 이는 항상 작동해야 합니다.
새 콘텐츠로 파일을 덮어쓸 수 있습니다.길이가 완전 똑같아. 이는 쉘이 파일의 해당 부분을 이미 읽었는지 여부에 따라 작동할 수도 있고 작동하지 않을 수도 있습니다. 대부분의 파일 I/O에는 버퍼 읽기가 포함되고 내가 아는 모든 쉘은 실행하기 전에 전체 복합 명령을 읽기 때문에 이 상황에서 벗어날 수 없을 것입니다. 이것은 확실히 신뢰할 수 없습니다.
나는 실행 시 스크립트 파일에 실제로 추가될 가능성을 요구하는 Posix 표준의 어떤 표현도 알지 못하므로 거의 때로는 posix는 물론 모든 Posix 호환 셸에서 작동하지 않을 수 있습니다. 현재 제공되는 쉘과 호환 가능합니다. 그래서 YMMV. 하지만 내가 아는 한, bash에서는 안정적으로 작동합니다.
증거로 여기에 덮어쓰기 및 추가를 사용하는 bash의 악명 높은 99병 맥주 프로그램의 "루프 없는" 구현이 있습니다 dd
(덮어쓰기는 항상 파일인 현재 실행된 줄을 전체 주석으로 대체하기 때문에 아마도 안전할 것입니다). 동일한 길이로, 자체 수정 동작 없이 최종 결과를 실행할 수 있도록 이 작업을 수행합니다.
#!/bin/bash
if [[ $1 == reset ]]; then
printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
exit
fi
step() {
s=s
one=one
case $beer in
2) beer=1; unset s;;
1) beer="No more"; one=it;;
"No more") beer=99; return 1;;
*) ((--beer));;
esac
}
next() {
step ${beer:=$(($1+1))}
refrain |
dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\ $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
printf "%-17s\n" "# $beer bottles"
echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
if step; then
echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
echo echo
echo next abcdefghijkl
else
echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
fi
}
####
next ${1:-99} #
답변2
bash
실행하기 전에 명령을 읽는 것이 큰 도움이 됩니다.
예를 들어:
cmd1
cmd2
셸은 스크립트를 덩어리로 읽으므로 두 개의 명령을 읽고 첫 번째 명령을 해석한 다음 스크립트의 끝으로 돌아가서 cmd1
스크립트를 다시 읽어 읽고 cmd2
실행할 수 있습니다.
다음과 같이 쉽게 확인할 수 있습니다.
$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo
(그렇지만 출력을 보면 strace
몇 년 전 동일한 작업을 시도했을 때보 다 좀 더 멋진 작업(예: 데이터를 여러 번 읽기, 뒤돌아보기 등)을 수행하는 것 같아서 위에서 뒤돌아보기에 대해 썼습니다. 새 버전에는 더 이상 적용되지 않습니다).
그러나 스크립트를 다음과 같이 작성하면:
{
cmd1
cmd2
exit
}
쉘은 끝까지 읽어서 }
메모리에 저장하고 실행해야 합니다. exit
쉘은 스크립트를 다시 읽지 않으므로 쉘이 스크립트를 해석하는 동안 안전하게 스크립트를 편집할 수 있습니다 .
또는 스크립트를 편집할 때 스크립트의 새 복사본을 작성해야 합니다. 쉘은 원본 파일을 계속해서 읽습니다(삭제되거나 이름이 바뀌더라도).
이렇게 하려면 이름을 바꾸고 the-script
복사 the-script.old
하고 the-script.old
편집하세요 the-script
.
답변3
실제로 스크립트가 실행되는 동안 스크립트를 수정하는 안전한 방법은 없습니다. 쉘이 버퍼링을 사용하여 파일을 읽을 수 있기 때문입니다. 또한 스크립트를 새 파일로 대체하여 수정하는 경우 셸은 일반적으로 특정 작업을 수행한 후에만 새 파일을 읽습니다.
종종 실행 중에 스크립트가 변경되면 셸에서 구문 오류를 보고하게 됩니다. 이는 쉘이 스크립트 파일을 닫았다가 다시 열 때 파일의 바이트 오프셋을 사용하여 반환 시 위치를 변경하기 때문입니다.
답변4
스크립트에 트랩을 설정한 다음 를 사용하여 exec
새 스크립트 콘텐츠를 가져오면 이 문제를 해결할 수 있습니다. 그러나 이 exec
호출은 실행 중 있던 위치가 아닌 처음부터 스크립트를 시작하므로 스크립트 B가 호출됩니다(계속해서).
#! /bin/bash
CMD="$0"
ARGS=("$@")
trap reexec 1
reexec() {
exec "$CMD" "${ARGS[@]}"
}
while : ; do sleep 1 ; clear ; date ; done
그러면 화면에 날짜가 계속 표시됩니다. 그런 다음 스크립트를 편집하여 date
로 변경할 수 echo "Date: $(date)"
있습니다. 스크립트를 작성한 후에도 스크립트를 실행하면 여전히 날짜만 표시됩니다. 그러나 캡처할 신호 세트를 보내면 trap
스크립트는 exec
(현재 실행 중인 프로세스를 지정된 명령으로 대체) 즉, 명령 $CMD
및 인수를 수행합니다 $@
. kill -1 PID
이를 실행하면(여기서 PID는 실행 중인 스크립트의 PID임) 출력이 명령 출력 Date:
앞에 나타나 도록 변경 됩니다 date
.
스크립트의 "상태"를 외부 파일(예: /tmp)에 저장하고 내용을 읽어 프로그램이 다시 실행될 때 "재개"되는 위치를 알 수 있습니다. 그런 다음 추가 트랩 종료(SIGINT/SIGQUIT/SIGKILL/SIGTERM)를 추가하여 "스크립트 A"를 중단한 후 다시 시작할 때 처음부터 시작되도록 tmp 파일을 지울 수 있습니다. 상태 저장 버전은 다음과 같습니다.
#! /bin/bash
trap reexec 1
trap cleanup 2 3 9 15
CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1
reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile; }
while [ "$state" != "stop" ] ; do
if [ -f "$statefile" ] ; then
state="$(cat "$statefile")"
else
state='starting'
fi
case "$state" in
starting)
run_scriptB
;;
scriptC)
run_scriptC
;;
esac
done
EXIT=0
cleanup