Làm thế nào để đảm bảo chỉ có một phiên bản của tập lệnh bash chạy?


26

Một giải pháp không yêu cầu các công cụ bổ sung sẽ được ưa thích.


Một tập tin khóa thì sao?
Marco

@Marco Tôi đã tìm thấy câu trả lời SO này bằng cách sử dụng nó, nhưng như đã nêu trong một nhận xét , điều này có thể tạo ra một điều kiện cuộc đua
Tobias Kienzler

3
Đây là BashFAQ 45 .
jw013

@ jw013 cảm ơn! Vì vậy, có thể một cái gì đó giống như ln -s my.pid .locksẽ yêu cầu khóa (tiếp theo echo $$ > my.pid) và khi thất bại có thể kiểm tra xem liệu PID được lưu trữ .lockcó thực sự là một phiên bản hoạt động của tập lệnh hay không
Tobias Kienzler

Câu trả lời:


18

Gần giống như câu trả lời của nsg: sử dụng một thư mục khóa . Tạo thư mục là nguyên tử trong linux và unix và * BSD và rất nhiều hệ điều hành khác.

if mkdir $LOCKDIR
then
    # Do important, exclusive stuff
    if rmdir $LOCKDIR
    then
        echo "Victory is mine"
    else
        echo "Could not remove lock dir" >&2
    fi
else
    # Handle error condition
    ...
fi

Bạn có thể đặt PID của khóa sh vào một tệp trong thư mục khóa cho mục đích gỡ lỗi, nhưng đừng rơi vào cái bẫy nghĩ rằng bạn có thể kiểm tra xem PID đó để xem liệu quy trình khóa có còn thực thi không. Rất nhiều điều kiện cuộc đua nằm xuống con đường đó.


1
Tôi sẽ xem xét sử dụng bộ lưu trữ PID để kiểm tra xem phiên bản khóa có còn tồn tại không. Tuy nhiên, đây là một tuyên bố mkdirkhông phải là nguyên tử trên NFS (đây không phải là trường hợp của tôi, nhưng tôi đoán người ta nên đề cập đến điều đó, nếu đúng)
Tobias Kienzler

Có, bằng mọi cách, hãy sử dụng bộ lưu trữ PID để xem liệu quy trình khóa có còn thực thi không, nhưng đừng cố làm bất cứ điều gì ngoài việc ghi lại một thông báo. Công việc kiểm tra pid được lưu trữ, tạo một tệp PID mới, v.v., để lại một cửa sổ lớn cho các cuộc đua.
Bruce Ediger

Ok, như Ihunath đã nói , lockdir rất có thể là trong /tmpđó thường không được chia sẻ NFS, vì vậy điều đó sẽ ổn.
Tobias Kienzler

Tôi sẽ sử dụng rm -rfđể loại bỏ các thư mục khóa. rmdirsẽ thất bại nếu ai đó (không nhất thiết là bạn) quản lý để thêm tệp vào thư mục.
chepner

18

Để thêm vào câu trả lời của Bruce Ediger và được truyền cảm hứng từ câu trả lời này , bạn cũng nên thêm nhiều thông minh hơn vào việc dọn dẹp để bảo vệ chống lại việc chấm dứt kịch bản:

#Remove the lock directory
function cleanup {
    if rmdir $LOCKDIR; then
        echo "Finished"
    else
        echo "Failed to remove lock directory '$LOCKDIR'"
        exit 1
    fi
}

if mkdir $LOCKDIR; then
    #Ensure that if we "grabbed a lock", we release it
    #Works for SIGTERM and SIGINT(Ctrl-C)
    trap "cleanup" EXIT

    echo "Acquired lock, running"

    # Processing starts here
else
    echo "Could not create lock directory '$LOCKDIR'"
    exit 1
fi

Ngoài ra , if ! mkdir "$LOCKDIR"; then handle failure to lock and exit; fi trap and do processing after if-statement.
Kusalananda

6

Điều này có thể quá đơn giản, xin vui lòng sửa cho tôi nếu tôi sai. Không psđủ đơn giản ?

#!/bin/bash 

me="$(basename "$0")";
running=$(ps h -C "$me" | grep -wv $$ | wc -l);
[[ $running > 1 ]] && exit;

# do stuff below this comment

1
Đẹp và / hoặc rực rỡ. :)
Ma quái ngày

1
Tôi đã sử dụng điều kiện này trong một tuần và trong 2 lần, điều đó không ngăn cản quá trình mới bắt đầu. Tôi đã tìm ra vấn đề là gì - pid mới là một chuỗi con của cái cũ và bị ẩn đi grep -v $$. ví dụ thực tế: cũ - 14532, mới - 1453, cũ - 28858, mới - 858.
Naktibalda

Tôi đã sửa nó bằng cách đổi grep -v $$thànhgrep -v "^${$} "
Naktibalda

@Naktibalda bắt tốt, cảm ơn! Bạn cũng có thể sửa nó bằng grep -wv "^$$"(xem chỉnh sửa).
terdon

Cảm ơn đã cập nhật. Mô hình của tôi đôi khi thất bại vì các lỗ ngắn hơn được đệm bằng khoảng trắng.
Naktibalda

4

Tôi sẽ sử dụng một tập tin khóa, như được đề cập bởi Marco

#!/bin/bash

# Exit if /tmp/lock.file exists
[ -f /tmp/lock.file ] && exit

# Create lock file, sleep 1 sec and verify lock
echo $$ > /tmp/lock.file
sleep 1
[ "x$(cat /tmp/lock.file)" == "x"$$ ] || exit

# Do stuff
sleep 60

# Remove lock file
rm /tmp/lock.file

1
(Tôi nghĩ bạn đã quên tạo tệp khóa) Điều gì về điều kiện cuộc đua ?
Tobias Kienzler

ops :) Vâng, điều kiện chủng tộc là một vấn đề trong ví dụ của tôi, tôi thường viết công việc định kỳ hàng giờ hoặc hàng ngày và điều kiện cuộc đua là rất hiếm.
NSG

Chúng cũng không liên quan trong trường hợp của tôi, nhưng đó là điều người ta nên ghi nhớ. Có lẽ sử dụng lsof $0cũng không tệ?
Tobias Kienzler

Bạn có thể giảm bớt điều kiện cuộc đua bằng cách ghi $$vào tệp khóa của bạn . Sau đó sleeptrong một khoảng thời gian ngắn và đọc lại. Nếu PID vẫn là của bạn, bạn đã có được khóa thành công. Cần hoàn toàn không có công cụ bổ sung.
manatwork

1
Tôi chưa bao giờ sử dụng lsof cho mục đích này, tôi nó nên hoạt động. Lưu ý rằng lsof thực sự rất chậm trong hệ thống của tôi (1-2 giây) và rất có thể có nhiều thời gian cho điều kiện cuộc đua.
NSG

3

Nếu bạn muốn đảm bảo rằng chỉ có một phiên bản tập lệnh của bạn đang chạy, hãy xem:

Khóa tập lệnh của bạn (chống lại chạy song song)

Nếu không, bạn có thể kiểm tra pshoặc gọi lsof <full-path-of-your-script>, vì tôi sẽ không gọi chúng là các công cụ bổ sung.


Bổ sung :

thực sự tôi đã nghĩ làm việc đó như thế này:

for LINE in `lsof -c <your_script> -F p`; do 
    if [ $$ -gt ${LINE#?} ] ; then
        echo "'$0' is already running" 1>&2
        exit 1;
    fi
done

điều này đảm bảo rằng chỉ có quá trình với mức thấp nhất pidtiếp tục chạy ngay cả khi bạn thực hiện <your_script>đồng thời một vài trường hợp .


1
Cảm ơn liên kết, nhưng bạn có thể bao gồm các phần thiết yếu trong câu trả lời của bạn? Chính sách phổ biến tại SE để ngăn chặn thối liên kết ... Nhưng một cái gì đó [[(lsof $0 | wc -l) > 2]] && exitthực sự có thể là đủ, hoặc điều này cũng dễ bị điều kiện chủng tộc?
Tobias Kienzler

Bạn nói đúng, phần cốt yếu trong câu trả lời của tôi bị thiếu và chỉ đăng các liên kết là khá khập khiễng. Tôi đã thêm đề nghị của riêng tôi vào câu trả lời.
dùng114632

3

Một cách khác để đảm bảo một phiên bản bash script chạy:

#!/bin/bash

# Check if another instance of script is running
pidof -o %PPID -x $0 >/dev/null && echo "ERROR: Script $0 already running" && exit 1

...

pidof -o %PPID -x $0 nhận được mã PID của tập lệnh hiện có nếu nó đang chạy hoặc thoát với mã lỗi 1 nếu không có tập lệnh nào khác đang chạy


3

Mặc dù bạn đã yêu cầu một giải pháp mà không cần các công cụ bổ sung, đây là cách yêu thích của tôi bằng cách sử dụng flock:

#!/bin/sh

[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -en "$0" "$0" "$@" || :

echo "servus!"
sleep 10

Điều này xuất phát từ phần ví dụ của man flock, giải thích thêm:

Đây là mã soạn sẵn hữu ích cho các kịch bản shell. Đặt nó ở đầu tập lệnh shell mà bạn muốn khóa và nó sẽ tự động khóa trong lần chạy đầu tiên. Nếu env var $ FLOCKER không được đặt thành tập lệnh shell đang được chạy, thì hãy thực thi bầy và lấy một khóa không chặn độc quyền (sử dụng chính tập lệnh làm tập tin khóa) trước khi thực hiện lại chính nó với các đối số đúng. Nó cũng đặt var FLOCKER env đúng giá trị để nó không chạy lại.

Những điểm cần xem xét:

Xem thêm https://stackoverflow.com/questions/185451/quick-and-denty-way-to-ensure-only-one-instance-of-a-shell-script-is-ricky-at .


1

Đây là phiên bản sửa đổi của Câu trả lời của Anselmo . Ý tưởng là tạo ra một bộ mô tả tệp chỉ đọc bằng cách sử dụng chính tập lệnh bash và sử dụng flockđể xử lý khóa.

SCRIPT=`realpath $0`     # get absolute path to the script itself
exec 6< "$SCRIPT"        # open bash script using file descriptor 6
flock -n 6 || { echo "ERROR: script is already running" && exit 1; }   # lock file descriptor 6 OR show error message if script is already running

echo "Run your single instance code here"

Sự khác biệt chính đối với tất cả các câu trả lời khác là mã này không sửa đổi hệ thống tệp, sử dụng dấu chân rất thấp và không cần dọn dẹp vì bộ mô tả tệp được đóng ngay khi tập lệnh kết thúc độc lập với trạng thái thoát. Do đó, nó không thành vấn đề nếu kịch bản thất bại hoặc thành công.


Bạn phải luôn trích dẫn tất cả các tham chiếu biến shell trừ khi bạn có lý do chính đáng để không và bạn chắc chắn rằng bạn biết bạn đang làm gì. Vì vậy, bạn nên làm exec 6< "$SCRIPT".
Scott

@Scott Tôi đã thay đổi mã theo đề xuất của bạn. Cảm ơn nhiều.
John Doe

1

Tôi đang sử dụng cksum để kiểm tra tập lệnh của tôi thực sự đang chạy một cá thể, thậm chí tôi còn thay đổi tên tệp và đường dẫn tệp .

Tôi không sử dụng bẫy & khóa tệp, vì nếu máy chủ của tôi đột ngột ngừng hoạt động, tôi cần xóa tệp khóa thủ công sau khi máy chủ hoạt động.

Lưu ý: #! / Bin / bash trong dòng đầu tiên là bắt buộc đối với grep ps

#!/bin/bash

checkinstance(){
   nprog=0
   mysum=$(cksum $0|awk '{print $1}')
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(cksum /proc/$i/cwd/$cmd|awk '{print $1}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance 

#--- run your script bellow 

echo pass
while true;do sleep 1000;done

Hoặc bạn có thể mã hóa cksum bên trong tập lệnh của mình, vì vậy bạn không phải lo lắng nữa nếu bạn muốn thay đổi tên tệp, đường dẫn hoặc nội dung của tập lệnh .

#!/bin/bash

mysum=1174212411

checkinstance(){
   nprog=0
   for i in `ps -ef |grep /bin/bash|awk '{print $2}'`;do 
        proc=$(ls -lha /proc/$i/exe 2> /dev/null|grep bash) 
        if [[ $? -eq 0 ]];then 
           cmd=$(strings /proc/$i/cmdline|grep -v bash)
                if [[ $? -eq 0 ]];then 
                   fsum=$(grep mysum /proc/$i/cwd/$cmd|head -1|awk -F= '{print $2}')
                   if [[ $mysum -eq $fsum ]];then
                        nprog=$(($nprog+1))
                   fi
                fi
        fi
   done

   if [[ $nprog -gt 1 ]];then
        echo $0 is already running.
        exit
   fi
}

checkinstance

#--- run your script bellow

echo pass
while true;do sleep 1000;done

1
Vui lòng giải thích chính xác cách mã hóa tổng kiểm tra là một ý tưởng tốt.
Scott

không phải kiểm tra mã hóa cứng, nó chỉ tạo khóa nhận dạng của tập lệnh của bạn, khi một phiên bản khác sẽ chạy, nó sẽ kiểm tra quy trình tập lệnh shell khác và gửi tệp trước, nếu khóa nhận dạng của bạn nằm trên tệp đó, vì vậy có nghĩa là cá thể của bạn đã chạy.
arputra

ĐƯỢC; vui lòng chỉnh sửa câu trả lời của bạn để giải thích điều đó. Và, trong tương lai, vui lòng không đăng nhiều khối mã dài 30 dòng trông giống nhau (gần như) mà không nóigiải thích chúng khác nhau như thế nào. Và đừng nói những điều như mà bạn có thể mã hóa [sic] cksum trong tập lệnh của bạn, và đừng tiếp tục sử dụng tên biến mysumfsumkhi bạn không nói về tổng kiểm tra nữa.
Scott

Có vẻ thú vị, cảm ơn! Và chào mừng bạn đến với unix.stackexchange :)
Tobias Kienzler


0

Mã của tôi cho bạn

#!/bin/bash

script_file="$(/bin/readlink -f $0)"
lock_file=${script_file////_}

function executing {
  echo "'${script_file}' already executing"
  exit 1
}

(
  flock -n 9 || executing

  sleep 10

) 9> /var/lock/${lock_file}

Dựa trên man flock, chỉ cải thiện:

  • tên của tệp khóa, được dựa trên tên đầy đủ của tập lệnh
  • thông điệp executing

Nơi tôi đặt ở đây sleep 10, bạn có thể đặt tất cả các kịch bản chính.

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.