จะเกิดอะไรขึ้นถ้าคุณแก้ไขสคริปต์ระหว่างการดำเนินการ


31

ฉันมีคำถามทั่วไปซึ่งอาจเป็นผลมาจากความเข้าใจผิดว่ากระบวนการจัดการใน Linux อย่างไร

สำหรับวัตถุประสงค์ของฉันฉันจะกำหนด 'สคริปต์' เป็นตัวอย่างของรหัสทุบตีบันทึกไว้ในไฟล์ข้อความที่มีสิทธิ์ดำเนินการเปิดใช้งานสำหรับผู้ใช้ปัจจุบัน

ฉันมีชุดของสคริปต์ที่โทรหากันตามกัน ฉันจะเรียกพวกเขาว่าสคริปต์ A, B และ C. Script A ดำเนินการชุดคำสั่งแล้วหยุดชั่วคราวจากนั้นจะเรียกใช้สคริปต์ B จากนั้นหยุดสคริปต์แล้วเรียกใช้สคริปต์ C ในคำอื่น ๆ ซีรีส์ ขั้นตอนเป็นดังนี้:

เรียกใช้สคริปต์ A:

  1. ชุดคำสั่ง
  2. หยุด
  3. เรียกใช้สคริปต์ B
  4. หยุด
  5. เรียกใช้สคริปต์ C

ฉันรู้จากประสบการณ์ว่าถ้าฉันเรียกใช้สคริปต์ A จนกระทั่งหยุดการทำงานชั่วคราวจากนั้นทำการแก้ไขในสคริปต์ B การแก้ไขเหล่านั้นจะสะท้อนให้เห็นในการทำงานของรหัสเมื่อฉันอนุญาตให้ทำงานต่อ ในทำนองเดียวกันถ้าฉันทำการแก้ไขสคริปต์ C ในขณะที่สคริปต์ A ยังคงหยุดชั่วคราวแล้วอนุญาตให้ดำเนินการต่อหลังจากบันทึกการเปลี่ยนแปลงการเปลี่ยนแปลงเหล่านั้นจะสะท้อนให้เห็นในการเรียกใช้โค้ด

ต่อไปนี้เป็นคำถามจริงมีวิธีแก้ไขสคริปต์ A ในขณะที่ยังทำงานอยู่หรือไม่ หรือการแก้ไขเป็นไปไม่ได้เมื่อเริ่มดำเนินการ?


2
ฉันคิดว่ามันขึ้นอยู่กับเปลือก แม้ว่าคุณระบุว่าคุณกำลังใช้ bash ดูเหมือนว่ามันจะขึ้นอยู่กับวิธีที่เชลล์โหลดสคริปต์ภายใน
strugee

ลักษณะการทำงานอาจเปลี่ยนแปลงหากคุณแหล่งไฟล์แทนการดำเนินการ
strugee

1
ฉันคิดว่าทุบตีอ่านสคริปต์ทั้งหมดในหน่วยความจำก่อนที่จะดำเนินการ
w4etwetewtwet

2
@handuel ไม่ไม่ ไม่ต้องรอจนกว่าคุณจะพิมพ์ "exit" ที่พรอมต์เพื่อเริ่มตีความคำสั่งที่คุณป้อน
Stéphane Chazelas

1
@StephaneChazelas ใช่การอ่านจากเทอร์มินัลมันไม่ได้อย่างไรก็ตามที่แตกต่างจากการใช้สคริปต์
w4etwetewtwet

คำตอบ:


21

ใน Unix ผู้แก้ไขส่วนใหญ่ทำงานโดยสร้างไฟล์ชั่วคราวใหม่ที่มีเนื้อหาที่แก้ไข เมื่อบันทึกไฟล์ที่แก้ไขแล้วไฟล์ต้นฉบับจะถูกลบและไฟล์ชั่วคราวจะถูกเปลี่ยนชื่อเป็นชื่อเดิม (แน่นอนว่ามีมาตรการป้องกันต่าง ๆ เพื่อป้องกันดาต้าoss) ตัวอย่างเช่นรูปแบบที่ใช้โดยsedหรือperlเมื่อเรียกใช้ด้วยการ-iตั้งค่าสถานะ ("in-place") ซึ่งไม่ได้เป็น มันควรจะถูกเรียกว่า "สถานที่ใหม่ที่มีชื่อเก่า"

วิธีนี้ใช้งานได้ดีเพราะ unix มั่นใจ (อย่างน้อยสำหรับระบบไฟล์ในระบบ) ที่ไฟล์ที่เปิดอยู่ยังคงมีอยู่จนกว่าจะถูกปิดแม้ว่ามันจะ "ลบ" และไฟล์ใหม่ที่มีชื่อเดียวกันจะถูกสร้างขึ้น (ไม่ใช่เรื่องบังเอิญที่ระบบยูนิกซ์เรียกว่า "ลบ" ไฟล์จริงๆแล้วเรียกว่า "unlink") ดังนั้นโดยทั่วไปถ้าล่ามเชลล์มีไฟล์ต้นฉบับเปิดอยู่และคุณ "แก้ไข" ไฟล์ในลักษณะที่อธิบายไว้ข้างต้น เชลล์จะไม่เห็นการเปลี่ยนแปลงเนื่องจากมันยังคงเปิดไฟล์เดิมอยู่

[หมายเหตุ: เช่นเดียวกับความคิดเห็นตามมาตรฐานทั้งหมดข้างต้นอาจมีการตีความหลายครั้งและมีหลายกรณีเช่น NFS ยินดีต้อนรับคนเดินเท้าเพื่อกรอกความคิดเห็นด้วยข้อยกเว้น]

แน่นอนว่าเป็นไปได้ที่จะแก้ไขไฟล์โดยตรง มันไม่สะดวกสำหรับการแก้ไขเนื่องจากในขณะที่คุณสามารถเขียนทับข้อมูลในไฟล์คุณไม่สามารถลบหรือแทรกโดยไม่ต้องเลื่อนข้อมูลต่อไปนี้ทั้งหมดซึ่งจะเป็นการเขียนซ้ำมาก นอกจากนี้ในขณะที่คุณทำการขยับเนื้อหาของไฟล์จะไม่สามารถคาดการณ์ได้และกระบวนการที่เปิดไฟล์จะประสบ ในการที่จะหลีกเลี่ยงสิ่งนี้ (เช่นในระบบฐานข้อมูล) คุณจำเป็นต้องมีโปรโตคอลการดัดแปลงและล็อคแบบกระจาย สิ่งที่อยู่นอกเหนือขอบเขตของยูทิลิตี้การแก้ไขไฟล์ทั่วไป

ดังนั้นหากคุณต้องการแก้ไขไฟล์ในขณะที่กำลังประมวลผลโดยเชลล์คุณมีสองตัวเลือก:

  1. คุณสามารถต่อท้ายไฟล์ สิ่งนี้ควรใช้งานได้เสมอ

  2. คุณสามารถเขียนทับไฟล์ด้วยเนื้อหาใหม่ที่มีความยาวเท่ากัน สิ่งนี้อาจเป็นไปได้หรือไม่ขึ้นอยู่กับว่าเชลล์อ่านส่วนหนึ่งของไฟล์นั้นหรือไม่ เนื่องจากไฟล์ I / O ส่วนใหญ่เกี่ยวข้องกับการอ่านบัฟเฟอร์และเนื่องจากเชลล์ทั้งหมดที่ฉันรู้จักอ่านคำสั่งผสมทั้งหมดก่อนที่จะเรียกใช้งานมันไม่น่าเป็นไปได้เลยที่คุณจะได้รับสิ่งนี้ แน่นอนว่ามันจะไม่น่าเชื่อถือ

ฉันไม่รู้เกี่ยวกับถ้อยคำใด ๆ ในมาตรฐาน Posix ซึ่งจริง ๆ แล้วต้องการความเป็นไปได้ในการต่อท้ายไฟล์สคริปต์ขณะที่ไฟล์กำลังถูกประมวลผลดังนั้นจึงอาจไม่ทำงานกับเชลล์ที่สอดคล้องกับ Posix ทุกตัวซึ่งน้อยกว่าข้อเสนอปัจจุบันเกือบ และบางครั้ง -posix- เข้ากันได้เปลือก ดังนั้น YMMV แต่เท่าที่ฉันรู้มันทำงานได้อย่างน่าเชื่อถือด้วยการทุบตี

ตามหลักฐานแล้วนี่คือการดำเนินการ "วนฟรี" ของโปรแกรมเบียร์ที่น่าอับอาย 99 ขวดใน bash ซึ่งใช้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}   #

เมื่อฉันรันสิ่งนี้มันจะเริ่มต้นด้วย "ไม่มาก" จากนั้นก็ยังคงเป็น -1 และจะเป็นจำนวนลบอย่างไม่มีกำหนด
Daniel Hershcovich

ถ้าฉันทำexport beer=100ก่อนเรียกใช้สคริปต์มันทำงานได้ตามที่คาดไว้
Daniel Hershcovich

@DanielHershcovich: ถูกต้อง; การทดสอบเลอะเทอะในส่วนของฉัน ฉันคิดว่าฉันแก้ไขมัน; ตอนนี้รับพารามิเตอร์การนับที่เป็นตัวเลือก การแก้ไขที่ดีและน่าสนใจกว่าคือการรีเซ็ตโดยอัตโนมัติหากพารามิเตอร์ไม่ตรงกับสำเนาที่แคช
rici

18

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และแก้ไขมัน


4

ไม่มีวิธีที่ปลอดภัยในการแก้ไขสคริปต์ในขณะที่ทำงานอยู่เนื่องจากเชลล์สามารถใช้การบัฟเฟอร์เพื่ออ่านไฟล์ นอกจากนี้หากสคริปต์ถูกแก้ไขโดยแทนที่ด้วยไฟล์ใหม่เชลล์จะอ่านไฟล์ใหม่หลังจากดำเนินการบางอย่างเท่านั้น

บ่อยครั้งเมื่อมีการเปลี่ยนแปลงสคริปต์ขณะดำเนินการเชลล์จะสิ้นสุดการรายงานข้อผิดพลาดทางไวยากรณ์ นี่เป็นเพราะข้อเท็จจริงที่ว่าเมื่อเชลล์ปิดและเปิดไฟล์สคริปต์อีกครั้งจะใช้ไบต์ออฟเซ็ตลงในไฟล์เพื่อเปลี่ยนตำแหน่งของตัวเองเมื่อกลับมา


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) เพื่อล้างไฟล์ tmp นั้นดังนั้นเมื่อคุณรีสตาร์ทหลังจากขัดจังหวะ "สคริปต์ A" มันจะเริ่มจากจุดเริ่มต้น เวอร์ชั่นที่เป็นรัฐจะเป็นดังนี้:

#! /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

ฉันได้แก้ไขปัญหานั้นแล้วด้วยการจับภาพ$0และ$@ตอนเริ่มต้นสคริปต์และใช้ตัวแปรเหล่านั้นexecแทน
Drav Sloan
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.