Điều gì xảy ra nếu bạn chỉnh sửa một tập lệnh trong khi thực hiện?


31

Tôi có một câu hỏi chung, có thể là kết quả của sự hiểu lầm về cách xử lý các quy trình trong Linux.

Đối với mục đích của tôi, tôi sẽ định nghĩa một 'tập lệnh' là một đoạn mã bash được lưu vào một tệp văn bản với quyền thực thi được kích hoạt cho người dùng hiện tại.

Tôi có một loạt các kịch bản gọi nhau. Để đơn giản, tôi sẽ gọi chúng là các tập lệnh A, B và C. Tập lệnh A thực hiện một loạt các câu lệnh và sau đó tạm dừng, sau đó nó thực thi tập lệnh B, sau đó nó tạm dừng, sau đó nó thực thi tập lệnh C. Nói cách khác, chuỗi các bước là một cái gì đó như thế này:

Chạy tập lệnh A:

  1. Hàng loạt báo cáo
  2. Tạm ngừng
  3. Chạy tập lệnh B
  4. Tạm ngừng
  5. Chạy tập lệnh C

Tôi biết từ kinh nghiệm rằng nếu tôi chạy tập lệnh A cho đến lần tạm dừng đầu tiên, sau đó thực hiện các chỉnh sửa trong tập lệnh B, các chỉnh sửa đó được phản ánh trong quá trình thực thi mã khi tôi cho phép tiếp tục. Tương tự như vậy nếu tôi chỉnh sửa tập lệnh C trong khi tập lệnh A vẫn bị tạm dừng, sau đó cho phép nó tiếp tục sau khi lưu các thay đổi, những thay đổi đó được phản ánh trong quá trình thực thi mã.

Đây là câu hỏi thực sự, có cách nào để chỉnh sửa Script A trong khi nó vẫn đang chạy không? Hoặc là không thể chỉnh sửa một khi việc thực hiện của nó bắt đầu?


2
Tôi sẽ nghĩ rằng nó phụ thuộc vào vỏ. mặc dù bạn nói rằng bạn đang sử dụng bash. có vẻ như nó sẽ phụ thuộc vào cách shell tải các tập lệnh bên trong.
strugee

hành vi cũng có thể thay đổi nếu bạn lấy tệp thay vì thực thi nó.
strugee

1
Tôi nghĩ bash đọc toàn bộ tập lệnh vào bộ nhớ trước khi thực hiện.
w4etwetewtwet

2
@handuel, không, không. Giống như nó không đợi cho đến khi bạn gõ "exit" tại dấu nhắc để bắt đầu diễn giải các lệnh bạn đã nhập.
Stéphane Chazelas

1
@StephaneChazelas Có đọc từ thiết bị đầu cuối không, tuy nhiên điều đó khác với chạy tập lệnh.
w4etwetewtwet

Câu trả lời:


21

Trong Unix, hầu hết các trình soạn thảo hoạt động bằng cách tạo một tệp tạm thời mới chứa nội dung được chỉnh sửa. Khi tệp đã chỉnh sửa được lưu, tệp gốc sẽ bị xóa và tệp tạm thời được đổi tên thành tên gốc. (Tất nhiên, có nhiều biện pháp bảo vệ khác nhau để ngăn chặn dataloss.) Ví dụ, đây là kiểu được sử dụng bởi sedhoặc perlkhi được gọi với -icờ ("tại chỗ"), hoàn toàn không thực sự "tại chỗ". Nó nên được gọi là "nơi mới với tên cũ".

Điều này hoạt động tốt bởi vì unix đảm bảo (ít nhất là đối với các hệ thống tệp cục bộ) rằng một tệp đã mở tiếp tục tồn tại cho đến khi nó bị đóng, ngay cả khi nó bị "xóa" và một tệp mới có cùng tên được tạo. (Không phải ngẫu nhiên mà hệ thống unix gọi "xóa" một tệp thực sự được gọi là "hủy liên kết".) Vì vậy, nói chung, nếu trình thông dịch shell có một số tệp nguồn mở và bạn "chỉnh sửa" tệp theo cách được mô tả ở trên , shell thậm chí sẽ không thấy các thay đổi vì nó vẫn mở tệp gốc.

[Lưu ý: như với tất cả các nhận xét dựa trên tiêu chuẩn, phần trên có thể diễn giải theo nhiều cách hiểu và có nhiều trường hợp góc khác nhau, chẳng hạn như NFS. Trẻ em được chào đón để điền vào các ý kiến ​​với các ngoại lệ.]

Tất nhiên, có thể sửa đổi các tập tin trực tiếp; Nó không thuận tiện cho mục đích chỉnh sửa, bởi vì trong khi bạn có thể ghi đè lên dữ liệu trong một tệp, bạn không thể xóa hoặc chèn mà không thay đổi tất cả dữ liệu sau, điều này có nghĩa là sẽ viết lại khá nhiều. Hơn nữa, trong khi bạn đang thực hiện việc dịch chuyển đó, nội dung của tệp sẽ không thể đoán trước được và các quá trình mở tệp sẽ bị ảnh hưởng. Để thoát khỏi điều này (ví dụ như với các hệ thống cơ sở dữ liệu), bạn cần một bộ giao thức sửa đổi tinh vi và các khóa phân tán; những thứ vượt ra ngoài phạm vi của một tiện ích chỉnh sửa tập tin điển hình.

Vì vậy, nếu bạn muốn chỉnh sửa một tệp trong khi nó được xử lý bởi trình bao, bạn có hai tùy chọn:

  1. Bạn có thể nối vào tập tin. Điều này nên luôn luôn làm việc.

  2. Bạn có thể ghi đè lên tệp có nội dung mới có cùng độ dài . Điều này có thể hoặc không thể hoạt động, tùy thuộc vào việc shell đã đọc phần đó của tệp hay chưa. Vì hầu hết các tệp I / O liên quan đến bộ đệm đọc và vì tất cả các shell mà tôi biết đều đọc toàn bộ lệnh ghép trước khi thực hiện nó, nên rất khó để bạn có thể thoát khỏi điều này. Nó chắc chắn sẽ không đáng tin cậy.

Tôi không biết bất kỳ từ ngữ nào trong tiêu chuẩn Posix thực sự đòi hỏi khả năng nối thêm tệp tập lệnh trong khi tệp đang được thực thi, do đó, nó có thể không hoạt động với mọi trình bao tuân thủ Posix, ít hơn nhiều với việc cung cấp gần như hiện tại và vỏ đôi khi-posix. Vậy YMMV. Nhưng theo như tôi biết, nó hoạt động đáng tin cậy với bash.

Bằng chứng là đây là một triển khai "không có vòng lặp" của chương trình 99 chai bia khét tiếng trong bash, sử dụng ddđể ghi đè và nối thêm (phần ghi đè có lẽ an toàn vì nó thay thế dòng hiện đang thực thi, luôn là dòng cuối cùng của dòng tệp, với một nhận xét có cùng độ dài; tôi đã làm điều đó để kết quả cuối cùng có thể được thực thi mà không có hành vi tự sửa đổi.)

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

Khi tôi chạy cái này, nó bắt đầu bằng "Không còn nữa", sau đó tiếp tục đến -1 và vào các số âm vô thời hạn.
Daniel Hershcovich

Nếu tôi làm export beer=100trước khi chạy tập lệnh, nó hoạt động như mong đợi.
Daniel Hershcovich

@DanielHershcovich: hoàn toàn đúng; kiểm tra cẩu thả về phía tôi. Tôi nghĩ rằng tôi đã sửa nó; bây giờ nó có một tham số đếm tùy chọn. Một sửa chữa tốt hơn và thú vị hơn sẽ là tự động đặt lại nếu tham số không tương ứng với bản sao được lưu trong bộ nhớ cache.
rici

18

bash đi một chặng đường dài để đảm bảo nó đọc các lệnh ngay trước khi thực hiện chúng.

Ví dụ:

cmd1
cmd2

Shell sẽ đọc tập lệnh theo các khối, do đó có khả năng đọc cả hai lệnh, diễn giải lệnh đầu tiên và sau đó tìm kiếm đến cuối cmd1tập lệnh và đọc lại tập lệnh để đọc cmd2và thực thi tập lệnh.

Bạn có thể dễ dàng xác minh nó:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(mặc dù nhìn vào stracesản lượng vào đó, có vẻ như nó hiện một số ưa thích nhiều thứ (như đọc dữ liệu nhiều lần, tìm lại ...) so với khi tôi đã cố gắng cùng một vài năm trước đây, vì vậy tuyên bố của tôi ở trên về lại lseeking thể không áp dụng nữa trên các phiên bản mới hơn).

Tuy nhiên, nếu bạn viết kịch bản của bạn là:

{
  cmd1
  cmd2
  exit
}

Shell sẽ phải đọc đến khi đóng }, lưu nó trong bộ nhớ và thực thi nó. Do đó exit, shell sẽ không đọc lại từ script để bạn có thể chỉnh sửa nó một cách an toàn trong khi shell đang diễn giải nó.

Ngoài ra, khi chỉnh sửa tập lệnh, hãy đảm bảo bạn viết một bản sao mới của tập lệnh. Shell sẽ tiếp tục đọc bản gốc (ngay cả khi nó bị xóa hoặc đổi tên).

Để làm điều đó, đổi tên the-scriptđể the-script.oldvà sao chép the-script.oldvào the-scriptvà chỉnh sửa nó.


4

Thực sự không có cách nào an toàn để sửa đổi tập lệnh trong khi nó đang chạy vì trình bao có thể sử dụng bộ đệm để đọc tệp. Ngoài ra, nếu tập lệnh được sửa đổi bằng cách thay thế nó bằng một tệp mới, shell thường sẽ chỉ đọc tệp mới sau khi thực hiện một số thao tác nhất định.

Thông thường, khi một kịch bản được thay đổi trong khi thực thi, trình bao kết thúc báo cáo lỗi cú pháp. Điều này là do thực tế là, khi shell đóng và mở lại tệp script, nó sử dụng offset byte vào tệp để định vị lại chính nó khi trả về.


4

Bạn có thể khắc phục điều này bằng cách đặt bẫy trên tập lệnh của mình và sau đó sử dụng execđể chọn nội dung tập lệnh mới. Tuy nhiên, lưu ý, execcuộc gọi bắt đầu tập lệnh từ đầu chứ không phải từ nơi nó đã đạt được trong quá trình đang chạy và do đó tập lệnh B sẽ được gọi (cứ tiếp tục như vậy).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

Điều này sẽ tiếp tục hiển thị ngày trên màn hình. Sau đó tôi có thể chỉnh sửa tập lệnh của mình và thay đổi datethành echo "Date: $(date)". Khi viết ra, kịch bản đang chạy vẫn chỉ hiển thị ngày. Bao giờ nếu tôi gửi tín hiệu mà tôi đặt trapđể chụp, tập lệnh sẽ exec(thay thế quy trình đang chạy hiện tại bằng lệnh được chỉ định) là lệnh $CMDvà đối số $@. Bạn có thể làm điều này bằng cách phát hành kill -1 PID- trong đó PID là PID của tập lệnh đang chạy - và đầu ra thay đổi để hiển thị Date:trước dateđầu ra lệnh.

Bạn có thể lưu trữ "trạng thái" của tập lệnh của mình trong một tệp bên ngoài (nói / tmp) và đọc nội dung để biết nơi "tiếp tục" vào lúc chương trình được thực thi lại. Sau đó, bạn có thể thêm một chấm dứt bẫy bổ sung (SIGINT / SIGQUIT / SIGKILL / SIGTERM) để xóa tệp tmp đó để khi bạn khởi động lại sau khi ngắt "Tập lệnh A", nó sẽ bắt đầu lại từ đầu. Một phiên bản trạng thái sẽ là một cái gì đó như:

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

Tôi đã khắc phục vấn đề đó bằng cách nắm bắt $0$@khi bắt đầu tập lệnh và sử dụng các biến execđó thay thế.
Drav Sloan
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.