Thêm một Heisenbug vượt qua con cá sấu

Thêm một Heisenbug vượt qua con cá sấu

$> set -o pipefail

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Повезло!

$> fortune | head -1 > /dev/null && echo "Повезло!" || echo "Вы проиграли"
Вы проиграли

Здесь fortune chương trình có điều kiện không có exit(rand()).

Bạn có thể giải thích? có chuyện gì ở đây vậy?

Lạc đề trữ tình-lịch sử

Lần đầu tiên tôi làm quen với Heisenbug này cách đây một phần tư thế kỷ. Sau đó, đối với cổng vào FaxNET, cần phải tạo một số tiện ích thông qua đường ống "chơi cờ đam" trong FreeBSD. Đúng như dự đoán, tôi tự coi mình là một lập trình viên tiên tiến và khá giàu kinh nghiệm. Vì vậy, tôi định làm mọi thứ một cách cẩn thận và cẩn thận nhất có thể, đặc biệt chú ý đến việc xử lý lỗi...

Kinh nghiệm xử lý lỗi trong sendmail và uucp/uupc trước đây của tôi đã bổ sung thêm sự chuyên cần của tôi trong việc “xử lý lỗi kỹ lưỡng”. Không có ích gì khi đi sâu vào chi tiết của câu chuyện đó, nhưng tôi đã vật lộn với Heisenbug này trong hai tuần trong 10-14 giờ. Vì vậy, người ta nhớ tới, hôm qua người quen cũ này lại ghé qua thăm.

TL;DR Trả lời

Tính thiết thực head có thể đóng kênh từ fortune сразу ngay khi anh ấy đọc dòng đầu tiên. Nếu như fortune xuất ra nhiều hơn một dòng, sau đó lệnh gọi tương ứng write() sẽ trả về lỗi hoặc báo cáo rằng đầu ra có ít byte hơn yêu cầu. Ngược lại, viết với việc xử lý lỗi cẩn thận fortune có quyền phản ánh tình trạng này trong trạng thái thoát của nó. Sau đó do cài đặt set -o pipefail sẽ làm việc || echo "Вы проиграли".

Tuy nhiên, head có thể không đến kịp đóng trước fortune sẽ kết thúc việc xuất dữ liệu. Sau đó nó sẽ hoạt động && echo "Повезло!".

Trong một ngày hôm nay của tôi GNUMakefile có như vậy một mảnh:

echo '#define MDBX_BUILD_COMPILER "$(shell set -o pipefail; $(CC) --version | head -1 || echo 'Please use GCC or CLANG compatible compiler')"'

Dịch sang tiếng người

Nó phổ biến ở đây đối với GNU tạo и bash theo cách trình biên dịch bằng cách sử dụng tùy chọn --version nó hỏi anh ta là ai và nếu tùy chọn này không được hỗ trợ thì một đoạn sơ khai sẽ được chèn vào "Vui lòng sử dụng trình biên dịch tương thích GCC hoặc CLANG".

Tương tự như bản mẫu можно встретить где угодно. В этом месте он появился давно и прекрасно везде работал (Linux, Solaris, OSX, FreeBSD, WSL vân vân.). Nhưng hôm qua ở KhácLinux trên nền Elbrus 2000 (E2K) Tôi nhận thấy:

#define MDBX_BUILD_COMPILER "lcc:1.23.20:Sep--4-2019:e2k-v3-linux Please use GCC or CLANG compatible compiler"

Thành thật mà nói, tôi không nhận ra ngay “người quen” cũ của mình. Hơn nữa, dự án đã được thử nghiệm nhiều lần trên Elbrus và dưới nhiều bản phân phối khác nhau, bao gồm cả Alt. Với nhiều trình biên dịch, phiên bản GNU Make và bash khác nhau. Vì vậy, tôi không muốn nhìn thấy sai lầm của mình ở đây.

Khi cố gắng tái hiện vấn đề và/hoặc hiểu chuyện gì đang xảy ra, nhiều điều kỳ lạ hơn bắt đầu xảy ra.
Chính tả dòng lệnh:

echo "#define MDBX_BUILD_COMPILER '$(set -o pipefail; LC_ALL=C cc --version | head -1 || echo "Please use GCC or CLANG compatible compiler")'"

Thỉnh thoảng nó sẽ tạo thêm văn bản, sau đó thì không... Thường thì một trong các tùy chọn sẽ tồn tại khá lâu, nhưng nếu bạn chọc lâu hơn, bạn luôn có cả hai!

Tất nhiên, strace tất cả mọi thứ của chúng tôi! Và sau khi gõ một dòng chữ strace, nhưng chưa kịp nhấn Enter, tôi nhận ra người bạn cũ của mình, ông Heisenbug, và các nhà phát triển trình biên dịch bản thân tôi 25 năm trước, Hoài cổ… Và tôi quyết định buồn và viết lời nhắn này 😉

Nhân tiện, giống như bất kỳ lòng tự trọng nào Heisenbug, Dưới strace không thích sinh sản.

Vì vậy những gì đang xảy ra?

  • Tính thiết thực head có quyền (hay nói đúng hơn là bị buộc) đóng kênh đang đọc ngay khi đọc đủ số dòng được yêu cầu.
  • Người viết chương trình tạo dữ liệu (trong trường hợp này cc) có thể in nhiều dòng và miễn phí thực hiện việc này thông qua nhiều cuộc gọi write().
  • Nếu đầu đọc sẽ có thời gian để đóng kênh bên mình trước khi kết thúc quá trình ghi bên bên ghi, khi đó bên ghi sẽ nhận được lỗi.
  • Chương trình viết được quyền cả hai đều bỏ qua lỗi ghi kênh và phản ánh nó trong mã hoàn thành của bạn.
  • Do cài đặt set -o pipefail mã hoàn thành đường ống sẽ khác XNUMX (có lỗi) nếu kết quả khác XNUMX từ ít nhất một phần tử và sau đó nó sẽ hoạt động || echo "Please use GCC or CLANG compatible compiler".

Có thể có các biến thể tùy thuộc vào cách chương trình người viết làm việc với tín hiệu. Ví dụ: chương trình có thể chấm dứt bất thường (với việc tự động tạo trạng thái chấm dứt khác XNUMX/có lỗi) hoặc write() sẽ trả về kết quả ghi ít byte hơn yêu cầu và đặt errno = EPIPE.

Ai là người có tội?

Trong trường hợp được mô tả Chút chút của tất cả mọi thứ. Xử lý lỗi trong cc (lcc:1.23.20:Sep—4-2019:e2k-v3-linux) không phải dư thừa. Trong nhiều trường hợp, tốt hơn là nên thận trọng, mặc dù điều này có thể bộc lộ những sai sót bất ngờ trong một khuôn mẫu được thiết kế cho hành vi truyền thống.

Phải làm gì?

Sai lầm:

fortune | head -1 && echo "Повезло, но вы рискуете!" || echo "WTF?"

Chính xác:

  1. (fortune && echo "Успешно" || echo "Ошибка") | head -1

    Ở đây, việc đóng sớm một đường ống sẽ được trình thông dịch lệnh xử lý khi phục vụ đường ống lồng nhau ("trong" dấu ngoặc đơn). Theo đó, nếu fortune sẽ báo cáo lỗi khi ghi vào kênh đã đóng ở trạng thái, sau đó đầu ra || echo "Ошибка" nó sẽ không đi đến đâu vì kênh đã bị đóng.

  2. fortune | cat - | head -1 && echo "Успешно" || echo "Ошибка"

    Đây là tiện ích cat hoạt động như một bộ giảm chấn vì nó bỏ qua lỗi EPIPE khi rút tiền. Bây giờ kết luận như vậy là đủ fortune nhỏ (vài dòng) và vừa với bộ đệm kênh (từ 512 byte đến ≈64K, trong hầu hết các hệ điều hành ≥4K). Nếu không vấn đề có thể quay trở lại.

Cách xử lý chính xác EPIPE và các lỗi ghi âm khác?

Không có giải pháp đúng duy nhất nhưng có những khuyến nghị đơn giản:

  • EPIPE yêu cầu phải được xử lý (và được phản ánh ở trạng thái thoát) khi xuất dữ liệu yêu cầu tính toàn vẹn. Ví dụ, trong quá trình hoạt động của trình lưu trữ hoặc tiện ích sao lưu.
  • EPIPE tốt hơn nên bỏ qua khi hiển thị thông tin và tin nhắn phụ trợ. Ví dụ: khi hiển thị thông tin về các tùy chọn --help hoặc --version.
  • Nếu mã đang được phát triển có thể được sử dụng trong quy trình trước | headsau đó EPIPE Tốt hơn là bỏ qua, nếu không thì tốt hơn là xử lý và phản ánh trong trạng thái thoát.

Tôi muốn nhân cơ hội này để bày tỏ lòng biết ơn của mình đến các đội MCST и KhácLinux cho công việc có năng suất cao. Quyết tâm của bạn thật tuyệt vời!
Cố lên các đồng chí, cố lên cuộc gặp gỡ vào mùa thu!

Cảm ơn berez để sửa lỗi chính tả và lỗi.
KDPV từ Georgy A.

Nguồn: www.habr.com

Mua dịch vụ lưu trữ đáng tin cậy cho các trang web có bảo vệ DDoS, máy chủ VPS VDS 🔥 Mua dịch vụ hosting website đáng tin cậy với bảo vệ DDoS, máy chủ VPS VDS | ProHoster