Làm cách nào để đọc và ghi cùng một tệp trong cùng một đường dẫn luôn luôn không thành công?


9

Nói rằng tôi có kịch bản sau đây:

#!/bin/bash
for i in $(seq 1000)
do
    cp /etc/passwd tmp
    cat tmp | head -1 | head -1 | head -1 > tmp  #this is the key line
    cat tmp
done

Trên dòng khóa, tôi đọc và viết cùng một tệp tmpmà đôi khi không thành công.

(Tôi đọc là do điều kiện chủng tộc vì các quy trình trong đường ống được thực thi song song, điều mà tôi không hiểu tại sao - mỗi headnhu cầu lấy dữ liệu từ lần trước, phải không? Đây không phải là câu hỏi chính của tôi, nhưng bạn cũng có thể trả lời nó.)

Khi tôi chạy tập lệnh, nó xuất ra khoảng 200 dòng. Có cách nào để tôi có thể buộc tập lệnh này luôn xuất ra 0 dòng không (vì vậy việc chuyển hướng I / O tmpluôn được chuẩn bị trước và vì vậy dữ liệu luôn bị hủy)? Để rõ ràng, tôi có nghĩa là thay đổi cài đặt hệ thống, không phải kịch bản này.

Cảm ơn ý tưởng của bạn.

Câu trả lời:


2

Câu trả lời của Gilles giải thích điều kiện cuộc đua. Tôi sẽ trả lời phần này:

Có cách nào để tôi có thể buộc tập lệnh này luôn xuất 0 dòng không (vì vậy chuyển hướng I / O sang tmp luôn được chuẩn bị trước và vì vậy dữ liệu luôn bị hủy)? Để rõ ràng, tôi có nghĩa là thay đổi cài đặt hệ thống

IDK nếu một công cụ cho việc này đã tồn tại, nhưng tôi có một ý tưởng về cách thực hiện một công cụ. (Nhưng lưu ý này sẽ không được luôn 0 dòng, chỉ là một thử nghiệm hữu ích mà sản lượng đánh bắt chủng tộc đơn giản như thế này một cách dễ dàng, và một số chủng tộc phức tạp hơn. Xem @Gilles' bình luận .) Nó sẽ không đảm bảo rằng một kịch bản là an toàn , nhưng sức mạnh là một công cụ hữu ích trong kiểm tra, tương tự như kiểm tra chương trình đa luồng trên các CPU khác nhau, bao gồm cả các CPU không x86 được đặt hàng yếu như ARM.

Bạn sẽ chạy nó như là racechecker bash foo.sh

Sử dụng cùng các phương tiện theo dõi / chặn cuộc gọi hệ thống strace -fltrace -fsử dụng để đính kèm với mọi quy trình con. (Trên Linux, đây là ptracelệnh gọi hệ thống tương tự được sử dụng bởi GDB và các trình gỡ lỗi khác để đặt điểm dừng, bước đơn và sửa đổi bộ nhớ / thanh ghi của một quy trình khác.)

Cụ sự openopenathệ thống các cuộc gọi: khi bất kỳ quá trình chạy dưới công cụ này làm cho một một open(2)cuộc gọi hệ thống (hoặc openat) với O_RDONLY, ngủ cho có lẽ 1/2 hoặc 1 giây. Để các opencuộc gọi hệ thống khác (đặc biệt là các cuộc gọi bao gồm O_TRUNC) thực hiện không chậm trễ.

Điều này sẽ cho phép người viết chiến thắng cuộc đua trong gần như mọi điều kiện cuộc đua, trừ khi tải hệ thống cũng cao hoặc đó là một điều kiện cuộc đua phức tạp khi việc cắt ngắn không xảy ra cho đến khi một số lần đọc khác. Vì vậy, biến thể ngẫu nhiên trong đó open()s (và có thể read()s hoặc ghi) bị trì hoãn sẽ làm tăng khả năng phát hiện của công cụ này, nhưng tất nhiên không cần kiểm tra trong một khoảng thời gian vô hạn với trình giả lập trễ sẽ bao gồm tất cả các tình huống có thể bạn gặp phải trong thế giới thực, bạn không thể chắc chắn kịch bản của mình không có chủng tộc trừ khi bạn đọc chúng cẩn thận và chứng minh rằng chúng không phải vậy.


Bạn có thể sẽ cần nó vào danh sách trắng (không trì hoãn open) cho các tệp trong /usr/bin/usr/libvì vậy quá trình khởi động không mất quá nhiều thời gian. (Liên kết động thời gian chạy phải có open()nhiều tệp (nhìn vào strace -eopen /bin/truehoặc /bin/lsđôi khi), mặc dù nếu chính lớp vỏ mẹ đang thực hiện cắt ngắn, điều đó sẽ ổn. Nhưng công cụ này sẽ vẫn không làm cho các tập lệnh chậm một cách vô lý).

Hoặc có thể liệt kê danh sách trắng mọi tệp mà quá trình gọi không có quyền cắt bớt ở vị trí đầu tiên. tức là quá trình theo dõi có thể thực hiện một access(2)cuộc gọi hệ thống trước khi thực sự tạm dừng quá trình muốn vào open()một tệp.


racecheckerbản thân nó sẽ phải được viết bằng C, không phải bằng shell, nhưng có thể sử dụng stracemã của nó làm điểm bắt đầu và có thể không mất nhiều công sức để thực hiện.

Bạn có thể có được chức năng tương tự với hệ thống tập tin FUSE . Có lẽ có một ví dụ FUSE về một hệ thống tập tin thông qua thuần túy, vì vậy bạn có thể thêm các kiểm tra vào open()chức năng làm cho nó ngủ để chỉ đọc mở ra nhưng hãy để việc cắt ngắn xảy ra ngay lập tức.


Ý tưởng của bạn cho một trình kiểm tra cuộc đua không thực sự hiệu quả. Đầu tiên, có một vấn đề là thời gian chờ không đáng tin cậy: một ngày, anh chàng kia sẽ mất nhiều thời gian hơn bạn mong đợi (đó là một vấn đề kinh điển với các tập lệnh xây dựng hoặc thử nghiệm, dường như hoạt động trong một thời gian và sau đó thất bại theo những cách khó gỡ lỗi khi khối lượng công việc mở rộng và nhiều thứ chạy song song). Nhưng ngoài này, mở thì bạn sẽ thêm một sự chậm trễ đến? Để phát hiện bất cứ điều gì thú vị, bạn cần thực hiện nhiều lần chạy với các mẫu độ trễ khác nhau và so sánh kết quả của chúng.
Gilles 'SO- ngừng trở nên xấu xa'

@Gilles: Phải, bất kỳ sự chậm trễ hợp lý nào cũng không đảm bảo việc cắt ngắn sẽ giành chiến thắng trong cuộc đua (trên một máy được tải nặng như bạn chỉ ra). Ý tưởng ở đây là bạn sử dụng điều này để kiểm tra kịch bản của bạn một vài lần, không phải là bạn sử dụng racecheckertất cả thời gian. Và có lẽ bạn muốn mở thời gian ngủ để đọc thành cấu hình vì lợi ích của mọi người trên các máy được tải rất nhiều, những người muốn đặt nó cao hơn, như 10 giây. Hoặc đặt nó thấp hơn, như 0,1 giây cho các tập lệnh dài hoặc không hiệu quả, mở lại các tệp rất nhiều .
Peter Cordes

@Gilles: Ý tưởng tuyệt vời về các mẫu độ trễ khác nhau, có thể cho phép bạn bắt được nhiều chủng tộc hơn chỉ là những thứ đơn giản trong cùng một đường ống mà "nên rõ ràng (một khi bạn biết vỏ hoạt động như thế nào)" như vỏ của OP. Nhưng "cái nào mở ra?" bất kỳ mở chỉ đọc, với danh sách trắng hoặc một số cách khác để không trì hoãn quá trình khởi động.
Peter Cordes

Tôi đoán bạn đang nghĩ về các cuộc đua phức tạp hơn với các công việc nền không cắt ngắn cho đến khi một quá trình khác hoàn thành? Vâng, sự thay đổi ngẫu nhiên có thể cần thiết để nắm bắt điều đó. Hoặc có thể nhìn vào cây quy trình và trì hoãn "sớm" đọc thêm, để cố gắng đảo ngược thứ tự thông thường. Bạn có thể làm cho công cụ ngày càng phức tạp hơn để mô phỏng các khả năng sắp xếp lại ngày càng nhiều hơn, nhưng đến một lúc nào đó bạn vẫn phải thiết kế chương trình của mình một cách chính xác nếu bạn đang thực hiện đa tác vụ. Kiểm tra tự động có thể hữu ích cho các tập lệnh đơn giản hơn trong đó các vấn đề có thể bị hạn chế hơn.
Peter Cordes

Nó khá giống với thử nghiệm mã đa luồng, đặc biệt là các thuật toán không khóa: lý luận logic về lý do tại sao nó đúng là rất quan trọng, cũng như thử nghiệm, bởi vì bạn không thể dựa vào thử nghiệm trên bất kỳ bộ máy cụ thể nào để tạo ra tất cả các thứ tự sắp xếp có thể là một vấn đề nếu bạn không đóng tất cả các sơ hở. Nhưng giống như thử nghiệm trên một kiến ​​trúc có thứ tự yếu như ARM hoặc PowerPC là một ý tưởng tốt trong thực tế, thử nghiệm một tập lệnh trong một hệ thống làm chậm trễ một cách giả tạo mọi thứ có thể phơi bày một số chủng tộc, vì vậy tốt hơn là không có gì. Bạn luôn có thể giới thiệu các lỗi mà nó sẽ không bắt được!
Peter Cordes

18

Tại sao có một điều kiện cuộc đua

Hai bên của một đường ống được thực hiện song song, không phải lần lượt từng bên. Có một cách rất đơn giản để chứng minh điều này: chạy

time sleep 1 | sleep 1

Điều này mất một giây, không phải hai.

Shell bắt đầu hai quá trình con và chờ cho cả hai hoàn thành. Hai quá trình này thực thi song song: lý do duy nhất tại sao một trong số chúng sẽ đồng bộ hóa với cái kia là khi nó cần chờ cho cái kia. Điểm đồng bộ hóa phổ biến nhất là khi các khối bên phải chờ dữ liệu đọc trên đầu vào tiêu chuẩn của nó và bị bỏ chặn khi phía bên trái ghi nhiều dữ liệu hơn. Điều ngược lại cũng có thể xảy ra, khi phía bên phải đọc dữ liệu chậm và các khối bên trái trong hoạt động ghi của nó cho đến khi phía bên phải đọc thêm dữ liệu (có một bộ đệm trong chính đường ống, được quản lý bởi kernel, nhưng nó có kích thước tối đa nhỏ).

Để quan sát điểm đồng bộ hóa, hãy quan sát các lệnh sau ( sh -xin từng lệnh khi thực thi):

time sh -x -c '{ sleep 1; echo a; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { cat; }'
time sh -x -c '{ echo a; sleep 1; } | { sleep 1; cat; }'
time sh -x -c '{ sleep 2; echo a; } | { cat; sleep 1; }'

Chơi với các biến thể cho đến khi bạn cảm thấy thoải mái với những gì bạn quan sát.

Đưa ra lệnh ghép

cat tmp | head -1 > tmp

quy trình bên trái thực hiện như sau (Tôi chỉ liệt kê các bước có liên quan đến giải thích của tôi):

  1. Thực hiện chương trình bên ngoài catvới các đối số tmp.
  2. Mở tmpđể đọc.
  3. Trong khi nó chưa đến cuối tập tin, hãy đọc một đoạn từ tệp và ghi nó vào đầu ra tiêu chuẩn.

Quá trình bên tay phải thực hiện như sau:

  1. Chuyển hướng đầu ra tiêu chuẩn đến tmp, cắt bớt tệp trong quy trình.
  2. Thực hiện chương trình bên ngoài headvới các đối số -1.
  3. Đọc một dòng từ đầu vào tiêu chuẩn và ghi nó vào đầu ra tiêu chuẩn.

Điểm duy nhất của đồng bộ hóa là phải 3 chờ trái 3 để xử lý một dòng đầy đủ. Không có sự đồng bộ giữa trái-2 và phải-1, vì vậy chúng có thể xảy ra theo thứ tự. Thứ tự chúng xảy ra không thể dự đoán được: nó phụ thuộc vào kiến ​​trúc CPU, vỏ, nhân, trên đó các lõi xảy ra theo lịch trình, vào những gì làm gián đoạn CPU nhận được trong khoảng thời gian đó, v.v.

Cách thay đổi hành vi

Bạn không thể thay đổi hành vi bằng cách thay đổi cài đặt hệ thống. Máy tính làm những gì bạn bảo nó làm. Bạn bảo nó cắt ngắn tmpvà đọc tmpsong song, vì vậy nó làm hai việc song song.

Ok, có một hệ thống cài đặt khác mà bạn có thể thay đổi: bạn có thể thay thế /bin/bashbằng một chương trình khác không phải là bash. Tôi hy vọng nó sẽ đi mà không nói rằng đây không phải là một ý tưởng tốt.

Nếu bạn muốn cắt ngắn xảy ra trước phía bên trái của đường ống, bạn cần đặt nó bên ngoài đường ống, ví dụ:

{ cat tmp | head -1; } >tmp

hoặc là

( exec >tmp; cat tmp | head -1 )

Tôi không biết tại sao bạn muốn điều này mặc dù. Điểm nào trong việc đọc từ một tệp mà bạn biết là trống?

Ngược lại, nếu bạn muốn chuyển hướng đầu ra (bao gồm cả cắt ngắn) xảy ra sau khi catđọc xong, thì bạn cần phải đệm đầy đủ dữ liệu trong bộ nhớ, ví dụ:

line=$(cat tmp | head -1)
printf %s "$line" >tmp

hoặc ghi vào một tệp khác và sau đó di chuyển nó vào vị trí. Đây thường là cách mạnh mẽ để thực hiện mọi thứ trong tập lệnh và có lợi thế là tập tin được viết đầy đủ trước khi nó hiển thị thông qua tên gốc.

cat tmp | head -1 >new && mv new tmp

Bộ sưu tập moreutils bao gồm một chương trình thực hiện điều đó, được gọi là sponge.

cat tmp | head -1 | sponge tmp

Cách phát hiện vấn đề tự động

Nếu mục tiêu của bạn là lấy các kịch bản được viết xấu và tự động tìm ra nơi chúng bị hỏng, thì xin lỗi, cuộc sống không đơn giản như vậy. Phân tích thời gian chạy sẽ không đáng tin cậy tìm thấy vấn đề bởi vì đôi khi catkết thúc việc đọc trước khi cắt ngắn xảy ra. Phân tích tĩnh về nguyên tắc có thể làm điều đó; ví dụ đơn giản hóa trong câu hỏi của bạn được Shellcheck nắm bắt, nhưng nó có thể không gặp vấn đề tương tự trong một tập lệnh phức tạp hơn.


Đó là mục tiêu của tôi, để xác định xem kịch bản có được viết tốt hay không. Nếu kịch bản có thể đã phá hủy dữ liệu theo cách này, tôi chỉ muốn nó phá hủy chúng mỗi lần. Thật không tốt khi nghe rằng điều này gần như không thể. Nhờ bạn, bây giờ tôi biết vấn đề là gì và sẽ cố gắng nghĩ ra giải pháp.
karlosss

@karlosss: Hmm, tôi tự hỏi liệu bạn có thể sử dụng cùng một công cụ theo dõi / chặn cuộc gọi hệ thống như strace(ví dụ Linux ptrace) để thực hiện các opencuộc gọi hệ thống đọc tất cả (trong tất cả các quy trình con) ngủ trong nửa giây không, vì vậy khi chạy đua với một cắt ngắn, cắt ngắn sẽ luôn luôn chiến thắng.
Peter Cordes

@PeterCordes Tôi là người mới làm điều này, nếu bạn có thể quản lý một cách để đạt được điều này và viết nó như một câu trả lời, tôi sẽ chấp nhận nó.
karlosss

@PeterCordes Bạn không thể đảm bảo rằng việc cắt ngắn sẽ thắng với sự chậm trễ. Nó sẽ hoạt động hầu hết thời gian, nhưng đôi khi trên một máy được tải nặng, tập lệnh của bạn sẽ thất bại theo những cách ít nhiều bí ẩn.
Gilles 'SO- ngừng trở nên xấu xa'

@Gilles: Hãy thảo luận về điều này dưới câu trả lời của tôi.
Peter Cordes
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.