Cách ngăn chặn SIGPIPE (hoặc xử lý chúng đúng cách)


260

Tôi có một chương trình máy chủ nhỏ chấp nhận các kết nối trên ổ cắm TCP hoặc UNIX cục bộ, đọc một lệnh đơn giản và tùy theo lệnh sẽ gửi trả lời. Vấn đề là đôi khi khách hàng không có hứng thú với câu trả lời và thoát ra sớm, do đó, việc ghi vào ổ cắm đó sẽ gây ra SIGPIPE và khiến máy chủ của tôi gặp sự cố. Cách tốt nhất để ngăn chặn sự cố ở đây là gì? Có cách nào để kiểm tra xem phía bên kia của dòng vẫn đang đọc không? (select () dường như không hoạt động ở đây vì nó luôn nói rằng ổ cắm có thể ghi được). Hay tôi chỉ nên bắt SIGPIPE bằng một trình xử lý và bỏ qua nó?

Câu trả lời:


254

Bạn thường muốn bỏ qua SIGPIPEvà xử lý lỗi trực tiếp trong mã của bạn. Điều này là do bộ xử lý tín hiệu trong C có nhiều hạn chế về những gì họ có thể làm.

Cách di động nhất để làm điều này là đặt SIGPIPEtrình xử lý SIG_IGN. Điều này sẽ ngăn chặn bất kỳ ổ cắm hoặc đường ống ghi gây ra SIGPIPEtín hiệu.

Để bỏ qua SIGPIPEtín hiệu, sử dụng mã sau đây:

signal(SIGPIPE, SIG_IGN);

Nếu bạn đang sử dụng send()cuộc gọi, một tùy chọn khác là sử dụng MSG_NOSIGNALtùy chọn, điều này sẽ SIGPIPEtắt hành vi trên cơ sở mỗi cuộc gọi. Lưu ý rằng không phải tất cả các hệ điều hành đều hỗ trợ MSG_NOSIGNALcờ.

Cuối cùng, bạn cũng có thể muốn xem xét SO_SIGNOPIPEcờ ổ cắm có thể được đặt setsockopt()trên một số hệ điều hành. Điều này sẽ ngăn không SIGPIPEbị gây ra bởi chỉ ghi vào ổ cắm được đặt.


75
Để làm rõ điều này: signal (SIGPIPE, SIG_IGN);
jhclark

5
Điều này có thể cần thiết nếu bạn nhận được mã trả về thoát là 141 (128 + 13: SIGPIPE). SIG_IGN là trình xử lý tín hiệu bỏ qua.
khóa dán

6
Đối với ổ cắm, việc sử dụng send () với MSG_NOSIGNAL thực sự dễ dàng hơn, như @talash nói.
Pawel Veselov

3
Điều gì chính xác xảy ra nếu chương trình của tôi ghi vào một đường ống bị hỏng (một ổ cắm trong trường hợp của tôi)? SIG_IGN làm cho chương trình bỏ qua SIG_PIPE, nhưng sau đó điều đó có dẫn đến việc send () làm điều gì đó không mong muốn không?
sudo

2
Đáng nói hơn, vì tôi đã tìm thấy nó một lần, rồi quên nó sau và bối rối, rồi phát hiện ra nó lần thứ hai. Trình gỡ lỗi (tôi biết gdb không) ghi đè xử lý tín hiệu của bạn, vì vậy các tín hiệu bị bỏ qua không được bỏ qua! Kiểm tra mã của bạn bên ngoài trình gỡ lỗi để đảm bảo SIGPIPE không còn xảy ra nữa. stackoverflow.com/questions/6821469/
Mạnh

155

Một phương pháp khác là thay đổi ổ cắm để nó không bao giờ tạo SIGPIPE trên write (). Điều này thuận tiện hơn trong các thư viện, nơi bạn có thể không muốn xử lý tín hiệu toàn cầu cho SIGPIPE.

Trên hầu hết các hệ thống dựa trên BSD (MacOS, FreeBSD ...), (giả sử bạn đang sử dụng C / C ++), bạn có thể thực hiện việc này với:

int set = 1;
setsockopt(sd, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));

Với hiệu ứng này, thay vì tín hiệu SIGPIPE được tạo, EPIPE sẽ được trả về.


45
Điều này nghe có vẻ tốt, nhưng SO_NOSIGPIPE dường như không tồn tại trong Linux - vì vậy nó không phải là một giải pháp di động rộng rãi.
tộc

1
chào tất cả, bạn có thể cho tôi biết nơi tôi phải đặt mã này không? tôi không hiểu cảm ơn
R. Dewi

9
Trong Linux, tôi thấy tùy chọn MSG_NOSIGNAL trong các cờ gửi
Aadishri

Hoạt động hoàn hảo trên Mac OS X
kirugan 8/03/2015

116

Tôi đến bữa tiệc rất muộn, nhưng SO_NOSIGPIPEkhông di động và có thể không hoạt động trên hệ thống của bạn (có vẻ như đó là một điều BSD).

Một giải pháp thay thế tuyệt vời nếu bạn sử dụng hệ thống Linux mà không cần SO_NOSIGPIPEđặt MSG_NOSIGNALcờ cho cuộc gọi gửi (2) của bạn.

Ví dụ thay thế write(...)bằng send(...,MSG_NOSIGNAL)(xem bình luận của quý tộc )

char buf[888];
//write( sockfd, buf, sizeof(buf) );
send(    sockfd, buf, sizeof(buf), MSG_NOSIGNAL );

5
Nói cách khác, sử dụng send (..., MSG_NOSIGNAL) để thay thế cho write () và bạn sẽ không nhận được SIGPIPE. Điều này sẽ hoạt động tốt cho các socket (trên các nền tảng được hỗ trợ), nhưng send () dường như bị giới hạn sử dụng với socket (không phải ống), vì vậy đây không phải là giải pháp chung cho vấn đề SIGPIPE.
tộc

@ Ray2k Ai trên trái đất vẫn đang phát triển ứng dụng cho Linux <2.2? Đó thực sự là một câu hỏi nửa nghiêm túc; hầu hết các tàu distro với ít nhất 2,6.
Bắn Parthian

1
@Parthian Shot: Bạn nên suy nghĩ lại câu trả lời của bạn. Bảo trì các hệ thống nhúng cũ là một lý do hợp lệ để chăm sóc các phiên bản Linux cũ hơn.
kirsche40

1
@ kirsche40 Tôi không phải là OP- Tôi chỉ tò mò thôi.
Bắn Parthian

@sklnd điều này làm việc cho tôi hoàn hảo. Tôi đang sử dụng một QTcpSocketđối tượng Qt để bọc cách sử dụng ổ cắm, tôi phải thay thế writecuộc gọi phương thức bằng HĐH send(sử dụng socketDescriptorphương thức). Bất cứ ai cũng biết một tùy chọn sạch hơn để đặt tùy chọn này trong một QTcpSocketlớp?
Emilio González Montaña

29

Trong bài viết này, tôi đã mô tả giải pháp khả thi cho trường hợp Solaris khi cả SO_NOSIGPIPE và MSG_NOSIGNAL đều không khả dụng.

Thay vào đó, chúng ta phải tạm thời loại bỏ SIGPIPE trong luồng hiện tại thực thi mã thư viện. Dưới đây là cách thực hiện việc này: để chặn SIGPIPE, trước tiên chúng tôi kiểm tra xem nó có đang chờ xử lý không. Nếu có, điều này có nghĩa là nó bị chặn trong chuỗi này và chúng ta không phải làm gì. Nếu thư viện tạo SIGPIPE bổ sung, nó sẽ được hợp nhất với thư đang chờ xử lý và đó là điều không có. Nếu SIGPIPE không chờ xử lý thì chúng tôi sẽ chặn nó trong chuỗi này và kiểm tra xem nó đã bị chặn chưa. Sau đó, chúng tôi được tự do thực hiện các bài viết của chúng tôi. Khi chúng tôi khôi phục SIGPIPE về trạng thái ban đầu, chúng tôi sẽ làm như sau: nếu SIGPIPE đang chờ xử lý ban đầu, chúng tôi không làm gì cả. Nếu không, chúng tôi kiểm tra nếu nó đang chờ xử lý. Nếu có (nghĩa là các hành động bên ngoài đã tạo ra một hoặc nhiều SIGPIPE), thì chúng ta sẽ đợi nó trong chuỗi này, do đó xóa trạng thái chờ xử lý của nó (để thực hiện điều này, chúng tôi sử dụng sigtimedwait () với thời gian chờ bằng 0; điều này là để tránh chặn trong một kịch bản mà người dùng độc hại đã gửi SIGPIPE theo cách thủ công cho toàn bộ quá trình: trong trường hợp này chúng tôi sẽ thấy nó đang chờ xử lý, nhưng luồng khác có thể xử lý nó trước khi chúng tôi có một sự thay đổi để chờ đợi nó). Sau khi xóa trạng thái chờ xử lý, chúng tôi bỏ chặn SIGPIPE trong chuỗi này, nhưng chỉ khi nó không bị chặn ban đầu.

Mã ví dụ tại https://github.com/kroki/XPcoat/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156


22

Xử lý SIGPIPE tại địa phương

Thông thường tốt nhất là xử lý lỗi cục bộ thay vì xử lý sự kiện tín hiệu toàn cầu vì tại địa phương, bạn sẽ có nhiều bối cảnh hơn về những gì đang diễn ra và những gì cần phải xử lý.

Tôi có một lớp giao tiếp trong một trong những ứng dụng của mình cho phép ứng dụng của tôi giao tiếp với một phụ kiện bên ngoài. Khi xảy ra lỗi ghi, tôi ném và ngoại lệ trong lớp giao tiếp và để nó nổi lên thành một khối thử bắt để xử lý nó ở đó.

Mã số:

Mã để bỏ qua tín hiệu SIGPIPE để bạn có thể xử lý nó cục bộ là:

// We expect write failures to occur but we want to handle them where 
// the error occurs rather than in a SIGPIPE handler.
signal(SIGPIPE, SIG_IGN);

Mã này sẽ ngăn tín hiệu SIGPIPE được tăng lên, nhưng bạn sẽ gặp lỗi đọc / ghi khi cố gắng sử dụng ổ cắm, vì vậy bạn sẽ cần kiểm tra xem.


Cuộc gọi này nên được thực hiện sau khi kết nối một ổ cắm hoặc trước đó? nơi nào là tốt nhất để đặt. Cảm ơn
Ahmed

@AHmed Cá nhân tôi đưa nó vào applicationDidFinishLaunching : . Điều chính là nó nên được trước khi tương tác với một kết nối ổ cắm.
Sam

nhưng bạn sẽ gặp lỗi đọc / ghi khi cố gắng sử dụng ổ cắm, vì vậy bạn sẽ cần kiểm tra xem. - Tôi có thể hỏi làm thế nào điều này là có thể? signal (SIGPIPE, SIG_IGN) hoạt động với tôi nhưng ở chế độ gỡ lỗi, nó trả về một lỗi có thể bỏ qua lỗi đó không?
jongbanaag

@ Dreyfus15 Tôi tin rằng tôi chỉ thực hiện các cuộc gọi bằng cách sử dụng ổ cắm trong khối thử / bắt để xử lý cục bộ. Đã được một lúc kể từ khi tôi nhìn thấy điều này.
Sam

15

Bạn không thể ngăn quá trình ở đầu xa của một đường ống thoát ra và nếu nó thoát ra trước khi bạn viết xong, bạn sẽ nhận được tín hiệu SIGPIPE. Nếu bạn SIG_IGN tín hiệu, thì ghi của bạn sẽ trở lại với một lỗi - và bạn cần lưu ý và phản ứng với lỗi đó. Chỉ bắt và bỏ qua tín hiệu trong trình xử lý không phải là ý hay - bạn phải lưu ý rằng đường ống hiện không còn tồn tại và sửa đổi hành vi của chương trình để nó không ghi lại vào đường ống (vì tín hiệu sẽ được tạo lại và bỏ qua một lần nữa, và bạn sẽ thử lại, và toàn bộ quá trình có thể diễn ra trong một thời gian dài và lãng phí rất nhiều năng lượng CPU).


6

Hay tôi chỉ nên bắt SIGPIPE bằng một trình xử lý và bỏ qua nó?

Tôi tin rằng điều đó là đúng. Bạn muốn biết khi nào đầu kia đã đóng mô tả của họ và đó là những gì SIGPIPE nói với bạn.

Sam


3

Hướng dẫn sử dụng Linux cho biết:

EPIPE Đầu cuối cục bộ đã bị tắt trên ổ cắm hướng kết nối. Trong trường hợp này, quy trình cũng sẽ nhận được SIGPIPE trừ khi MSG_NOSIGNAL được đặt.

Nhưng đối với Ubuntu 12.04 thì không đúng. Tôi đã viết một bài kiểm tra cho trường hợp đó và tôi luôn nhận được EPIPE mà không có SIGPIPE. SIGPIPE bị phát sinh nếu tôi cố gắng ghi vào cùng một ổ cắm bị hỏng lần thứ hai. Vì vậy, bạn không cần bỏ qua SIGPIPE nếu tín hiệu này xảy ra, điều đó có nghĩa là lỗi logic trong chương trình của bạn.


1
Tôi chắc chắn nhận được SIGPIPE mà không cần nhìn thấy EPIPE trên ổ cắm trong linux. Điều này nghe có vẻ như một lỗi kernel.
davmac

3

Cách tốt nhất để ngăn chặn sự cố ở đây là gì?

Hoặc vô hiệu hóa sigpipes theo mọi người, hoặc bắt và bỏ qua lỗi.

Có cách nào để kiểm tra xem phía bên kia của dòng vẫn đang đọc không?

Có, sử dụng select ().

select () dường như không hoạt động ở đây vì nó luôn nói rằng socket có thể ghi được.

Bạn cần chọn trên các bit đọc . Bạn có thể bỏ qua các bit ghi .

Khi đầu xa đóng xử lý tệp của nó, chọn sẽ cho bạn biết rằng có dữ liệu sẵn sàng để đọc. Khi bạn đọc và đọc nó, bạn sẽ nhận lại 0 byte, đó là cách HĐH cho bạn biết rằng phần xử lý tệp đã bị đóng.

Lần duy nhất bạn không thể bỏ qua các bit ghi là nếu bạn đang gửi khối lượng lớn và có nguy cơ đầu bên kia bị tồn đọng, điều này có thể khiến bộ đệm của bạn bị đầy. Nếu điều đó xảy ra, thì việc cố gắng ghi vào xử lý tệp có thể khiến chương trình / luồng của bạn bị chặn hoặc không thành công. Kiểm tra chọn trước khi viết sẽ bảo vệ bạn khỏi điều đó, nhưng không đảm bảo rằng đầu kia khỏe mạnh hoặc dữ liệu của bạn sẽ đến.

Lưu ý rằng bạn có thể nhận được một sigpipe từ close (), cũng như khi bạn viết.

Đóng tuôn ra bất kỳ dữ liệu đệm. Nếu đầu kia đã bị đóng, thì đóng sẽ thất bại và bạn sẽ nhận được một sigpipe.

Nếu bạn đang sử dụng TCPIP được đệm, thì việc ghi thành công chỉ có nghĩa là dữ liệu của bạn đã được xếp hàng để gửi, điều đó không có nghĩa là nó đã được gửi. Cho đến khi bạn gọi thành công, bạn không biết rằng dữ liệu của mình đã được gửi.

Sigpipe nói với bạn điều gì đó đã sai, nó không cho bạn biết điều gì hoặc bạn nên làm gì với nó.


Sigpipe nói với bạn điều gì đó đã sai, nó không cho bạn biết điều gì hoặc bạn nên làm gì với nó. Chính xác. Mục đích duy nhất của SIGPIPE là phá bỏ các tiện ích dòng lệnh được xử lý khi giai đoạn tiếp theo không còn cần đầu vào. Nếu chương trình của bạn đang kết nối mạng, bạn thường chỉ nên chặn hoặc bỏ qua toàn bộ chương trình SIGPIPE.
DepressionDaniel

Đúng. SIGPIPE dành cho các tình huống như "tìm XXX | đầu". Khi đầu đã khớp với 10 dòng của nó, không có điểm nào tiếp tục với tìm kiếm. Vì vậy, đầu thoát ra, và lần sau khi tìm cách nói chuyện với nó, nó nhận được một sigpipe và biết rằng nó cũng có thể thoát ra.
Ben Aveling

Thực sự, mục đích duy nhất của SIGPIPE là cho phép các chương trình được cẩu thả và không kiểm tra lỗi khi ghi!
William Pursell

2

Trong hệ thống POSIX hiện đại (tức là Linux), bạn có thể sử dụng sigprocmask()chức năng.

#include <signal.h>

void block_signal(int signal_to_block /* i.e. SIGPIPE */ )
{
    sigset_t set;
    sigset_t old_state;

    // get the current state
    //
    sigprocmask(SIG_BLOCK, NULL, &old_state);

    // add signal_to_block to that existing state
    //
    set = old_state;
    sigaddset(&set, signal_to_block);

    // block that signal also
    //
    sigprocmask(SIG_BLOCK, &set, NULL);

    // ... deal with old_state if required ...
}

Nếu bạn muốn khôi phục trạng thái trước đó sau đó, hãy đảm bảo lưu trạng thái old_statean toàn. Nếu bạn gọi hàm đó nhiều lần, bạn cần sử dụng ngăn xếp hoặc chỉ lưu lần đầu tiên hoặc lần cuối old_state... hoặc có thể có chức năng loại bỏ tín hiệu bị chặn cụ thể.

Để biết thêm thông tin đọc trang người đàn ông .


Không cần thiết phải đọc-sửa đổi-ghi tập hợp các tín hiệu bị chặn như thế này. sigprocmaskthêm vào tập hợp các tín hiệu bị chặn, vì vậy bạn có thể thực hiện tất cả điều này chỉ với một cuộc gọi.
Luchs

Tôi không đọc-sửa-ghi , tôi đọc để lưu trạng thái hiện tại mà tôi giữ old_stateđể sau đó tôi có thể khôi phục nó sau này, nếu tôi chọn. Nếu bạn biết bạn sẽ không cần phải khôi phục lại trạng thái, thực sự không cần phải đọc và lưu trữ nó theo cách này.
Alexis Wilke

1
Nhưng bạn làm: cuộc gọi đầu tiên để sigprocmask()đọc trạng thái cũ, cuộc gọi để sigaddsetsửa đổi nó, cuộc gọi thứ hai để sigprocmask()viết nó. Bạn có thể xóa cuộc gọi đầu tiên, khởi tạo sigemptyset(&set)và thay đổi cuộc gọi thứ hai thành sigprocmask(SIG_BLOCK, &set, &old_state).
Luchs

Ah! Ha ha! Bạn đúng. Tôi làm. Chà ... trong phần mềm của tôi, tôi không biết tín hiệu nào tôi đã chặn hoặc không bị chặn. Vì vậy, tôi làm theo cách này. Tuy nhiên, tôi đồng ý rằng nếu bạn chỉ có một "bộ tín hiệu theo cách này" thì một bộ + xóa đơn giản là đủ.
Alexis Wilke
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.