วิธีป้องกัน SIGPIPE (หรือจัดการอย่างถูกต้อง)


260

ฉันมีโปรแกรมเซิร์ฟเวอร์ขนาดเล็กที่ยอมรับการเชื่อมต่อบนซ็อกเก็ต TCP หรือ UNIX ในพื้นที่อ่านคำสั่งง่าย ๆ และส่งคำตอบกลับขึ้นอยู่กับคำสั่ง ปัญหาคือไคลเอนต์อาจไม่สนใจคำตอบในบางครั้งและออกก่อนเวลาดังนั้นการเขียนลงในซ็อกเก็ตนั้นจะทำให้เกิด SIGPIPE และทำให้เซิร์ฟเวอร์ของฉันทำงานล้มเหลว การปฏิบัติที่ดีที่สุดในการป้องกันความผิดพลาดคืออะไร มีวิธีการตรวจสอบว่าอีกด้านของบรรทัดยังอ่านอยู่หรือไม่? (select () ดูเหมือนจะไม่ทำงานที่นี่เพราะมันบอกว่าซ็อกเก็ตเขียนได้เสมอ) หรือฉันควรจับ SIGPIPE ด้วยเครื่องจัดการและเพิกเฉย?

คำตอบ:


254

โดยทั่วไปคุณต้องการละเว้นSIGPIPEและจัดการข้อผิดพลาดในรหัสของคุณโดยตรง นี่เป็นเพราะตัวจัดการสัญญาณใน C มีข้อ จำกัด มากมายเกี่ยวกับสิ่งที่พวกเขาสามารถทำได้

วิธีที่พกพามากที่สุดที่จะทำคือการตั้งค่าการจัดการเพื่อSIGPIPE SIG_IGNการทำเช่นนี้จะช่วยป้องกันซ็อกเก็ตหรือการเขียนไปป์ใด ๆ ที่เป็นสาเหตุของSIGPIPEสัญญาณ

ในการเพิกเฉยSIGPIPEสัญญาณให้ใช้รหัสต่อไปนี้:

signal(SIGPIPE, SIG_IGN);

หากคุณใช้การsend()โทรตัวเลือกอื่นคือการใช้MSG_NOSIGNALตัวเลือกซึ่งจะปิดการSIGPIPEทำงานตามการโทรแต่ละครั้ง โปรดทราบว่าระบบปฏิบัติการบางระบบอาจไม่สนับสนุนการMSG_NOSIGNALตั้งค่าสถานะ

สุดท้ายคุณอาจต้องการพิจารณาการSO_SIGNOPIPEตั้งค่าสถานะซ็อกเก็ตที่สามารถตั้งค่าด้วยsetsockopt()ในระบบปฏิบัติการบางระบบ สิ่งนี้จะป้องกันไม่ให้SIGPIPEเกิดจากการเขียนไปยังซ็อกเก็ตที่ตั้งค่าไว้


75
ในการทำให้ชัดเจน: สัญญาณ (SIGPIPE, SIG_IGN);
jhclark

5
สิ่งนี้อาจจำเป็นถ้าคุณได้รับรหัสส่งคืนการออกที่ 141 (128 + 13: SIGPIPE) SIG_IGN เป็นตัวจัดการละเว้นสัญญาณ
velcrow

6
สำหรับซ็อกเก็ตการใช้ send () กับ MSG_NOSIGNAL ง่ายกว่ามากอย่างที่ @talash กล่าว
Pawel Veselov

3
จะเกิดอะไรขึ้นถ้าโปรแกรมของฉันเขียนไปที่ pipe เสีย (ซ็อกเก็ตในกรณีของฉัน) SIG_IGN ทำให้โปรแกรมไม่สนใจ SIG_PIPE แต่ส่งผลให้ send () ทำสิ่งที่ไม่พึงประสงค์หรือไม่?
sudo

2
เป็นมูลค่าการกล่าวขวัญตั้งแต่ฉันพบครั้งเดียวแล้วลืมมันในภายหลังและสับสนแล้วค้นพบมันเป็นครั้งที่สอง ตัวแก้จุดบกพร่อง (ฉันรู้ว่า gdb ทำ) แทนที่การจัดการสัญญาณของคุณดังนั้นสัญญาณที่ถูกละเว้นจะไม่ถูกละเว้น! ทดสอบรหัสของคุณนอกดีบักเกอร์เพื่อให้แน่ใจว่า SIGPIPE จะไม่เกิดขึ้นอีกต่อไป stackoverflow.com/questions/6821469/…
Jetski S-type

155

อีกวิธีหนึ่งคือการเปลี่ยนซ็อกเก็ตจึงไม่สร้าง SIGPIPE เมื่อเขียน () สะดวกกว่าในไลบรารีซึ่งคุณอาจไม่ต้องการตัวจัดการสัญญาณส่วนกลางสำหรับ SIGPIPE

สำหรับระบบที่ใช้ BSD (MacOS, FreeBSD ... ) ส่วนใหญ่ (สมมติว่าคุณใช้ C / C ++) คุณสามารถทำได้ด้วย:

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

เมื่อสิ่งนี้มีผลบังคับใช้แทนที่จะส่งสัญญาณ SIGPIPE สัญญาณ EPIPE จะถูกส่งคืน


45
มันฟังดูดี แต่ SO_NOSIGPIPE ไม่มีอยู่ใน Linux - ดังนั้นจึงไม่ใช่โซลูชันแบบพกพาที่กว้างขวาง
nobar

1
สวัสดีทุกคนคุณสามารถบอกฉันได้ว่าสถานที่ที่ฉันต้องวางรหัสนี้หรือไม่ ฉันไม่เข้าใจคำขอบคุณ
R. Dewi

9
ใน Linux ฉันเห็นตัวเลือก MSG_NOSIGNAL ในธงของการส่ง
Aadishri

ทำงานได้สมบูรณ์แบบบน Mac OS X
kirugan

116

ฉันไปงานเลี้ยงสุดช้า แต่SO_NOSIGPIPEไม่พกพาได้และอาจไม่สามารถใช้กับระบบของคุณได้ (ดูเหมือนจะเป็นเรื่อง BSD)

ทางเลือกที่ดีถ้าคุณใช้ระบบ Linux โดยที่ไม่SO_NOSIGPIPEต้องตั้งค่าMSG_NOSIGNALสถานะในการโทรส่ง (2)

ตัวอย่างการแทนที่write(...)โดยsend(...,MSG_NOSIGNAL)(ดูความคิดเห็นของnobar )

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

5
กล่าวอีกนัยหนึ่งให้ใช้ send (... , MSG_NOSIGNAL) แทนการเขียน () และคุณจะไม่ได้รับ SIGPIPE สิ่งนี้จะทำงานได้ดีสำหรับซ็อกเก็ต (บนแพลตฟอร์มที่รองรับ) แต่ send () ดูเหมือนจะถูก จำกัด ให้ใช้กับซ็อกเก็ต (ไม่ใช่ไพพ์) ดังนั้นนี่ไม่ใช่วิธีแก้ปัญหาทั่วไปสำหรับปัญหา SIGPIPE
สูงศักดิ์

@ Ray2k ใครในโลกที่ยังคงพัฒนาแอพพลิเคชั่นสำหรับ Linux <2.2? นั่นเป็นคำถามกึ่งจริงจัง distros ส่วนใหญ่ส่งอย่างน้อย 2.6
คู่ปรับ Shot

1
@Parthian Shot: คุณควรคิดใหม่คำตอบของคุณ การบำรุงรักษาระบบฝังตัวเก่าเป็นเหตุผลที่ถูกต้องในการดูแล Linux เวอร์ชันเก่า
kirsche40

1
@ kirsche40 ฉันไม่ใช่ OP- ฉันแค่อยากรู้
คู่ปรับ Shot

@sklnd สิ่งนี้ทำงานได้อย่างสมบูรณ์แบบสำหรับฉัน ฉันใช้QTcpSocketวัตถุQt เพื่อห่อการใช้ซ็อกเก็ตฉันต้องเปลี่ยนwriteการเรียกใช้เมธอดด้วยระบบปฏิบัติการsend(โดยใช้socketDescriptorวิธี) ใครรู้วิธีทำความสะอาดเพื่อตั้งค่าตัวเลือกนี้ในQTcpSocketชั้นเรียนหรือไม่
Emilio GonzálezMontaña

29

ในโพสต์นี้ฉันได้อธิบายวิธีแก้ปัญหาที่เป็นไปได้สำหรับเคส Solaris เมื่อไม่มี SO_NOSIGPIPE และ MSG_NOSIGNAL

แต่เราต้องระงับ SIGPIPE ชั่วคราวในเธรดปัจจุบันที่รันโค้ดไลบรารี ต่อไปนี้เป็นวิธีดำเนินการ: เพื่อระงับ SIGPIPE เราจะตรวจสอบก่อนว่ามันค้างอยู่หรือไม่ ถ้าเป็นเช่นนี้หมายความว่ามันถูกบล็อกในกระทู้นี้และเราไม่ต้องทำอะไรเลย หากห้องสมุดสร้าง SIGPIPE เพิ่มเติมก็จะถูกรวมเข้ากับห้องสมุดที่รอดำเนินการ หาก SIGPIPE ไม่รอดำเนินการเราจะบล็อกมันในเธรดนี้และตรวจสอบว่ามันถูกบล็อกอยู่แล้วหรือไม่ จากนั้นเรามีอิสระที่จะดำเนินการเขียนของเรา เมื่อเราจะกู้ SIGPIPE กลับสู่สถานะเดิมเราจะทำสิ่งต่อไปนี้: ถ้า SIGPIPE ค้างอยู่ในขั้นต้นเราจะไม่ทำอะไรเลย มิฉะนั้นเราจะตรวจสอบว่ามันค้างอยู่หรือไม่ หากเป็นเช่นนั้น (ซึ่งหมายความว่าการกระทำที่ได้สร้าง SIGPIPE ขึ้นไปหนึ่งรายการหรือมากกว่า) เราจะรอในหัวข้อนี้ ดังนั้นการล้างสถานะที่รอดำเนินการ (ในการทำเช่นนี้เราใช้ sigtimedwait () โดยไม่มีการหยุดพักชั่วคราวเพื่อหลีกเลี่ยงการบล็อกในสถานการณ์ที่ผู้ใช้ที่ประสงค์ร้ายส่ง SIGPIPE ไปยังกระบวนการทั้งหมดด้วยตนเอง: ในกรณีนี้เราจะเห็นมัน จัดการมันก่อนที่เราจะมีการเปลี่ยนแปลงที่จะรอ) หลังจากล้างสถานะที่รอดำเนินการเราจะปลดบล็อก SIGPIPE ในชุดข้อความนี้ แต่จะไม่ถูกบล็อก แต่เดิม

รหัสตัวอย่างที่https://github.com/kroki/XProbes/blob/1447f3d93b6dbf273919af15e59f35cca58fcc23/src/libxprobes.c#L156


22

จัดการ SIGPIPE ในเครื่อง

โดยปกติแล้วมันจะดีที่สุดในการจัดการข้อผิดพลาดในพื้นที่มากกว่าในตัวจัดการเหตุการณ์สัญญาณทั่วโลกเนื่องจากในเครื่องคุณจะมีบริบทมากขึ้นเกี่ยวกับสิ่งที่เกิดขึ้นและสิ่งที่ต้องใช้

ฉันมีเลเยอร์การสื่อสารในหนึ่งในแอพของฉันที่อนุญาตให้แอปสื่อสารกับอุปกรณ์เสริมภายนอก เมื่อมีข้อผิดพลาดในการเขียนเกิดขึ้นฉันโยนและยกเว้นในเลเยอร์การสื่อสารและปล่อยให้มันลอยขึ้นไปลองบล็อก catch เพื่อจัดการกับมัน

รหัส:

รหัสที่จะเพิกเฉยต่อสัญญาณ SIGPIPE เพื่อที่คุณจะสามารถจัดการกับสัญญาณนั้นได้:

// 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);

รหัสนี้จะป้องกันสัญญาณ SIGPIPE จากการเพิ่มขึ้น แต่คุณจะได้รับข้อผิดพลาดการอ่าน / เขียนเมื่อพยายามใช้ซ็อกเก็ตดังนั้นคุณจะต้องตรวจสอบว่า


ควรทำการโทรนี้หลังจากเชื่อมต่อซ็อกเก็ตหรือก่อนหน้า? อยู่ที่ไหนดีที่สุด ขอบคุณ
อาเหม็ด

@Ahmed ผมเองใส่ไว้ในapplicationDidFinishLaunching: สิ่งสำคัญคือมันควรจะเป็นก่อนที่จะโต้ตอบกับการเชื่อมต่อซ็อกเก็ต
แซม

แต่คุณจะได้รับข้อผิดพลาดการอ่าน / เขียนเมื่อพยายามใช้ซ็อกเก็ตดังนั้นคุณจะต้องตรวจสอบว่า - ฉันขอถามว่ามันเป็นไปได้อย่างไร สัญญาณ (SIGPIPE, SIG_IGN) ใช้งานได้สำหรับฉัน แต่ภายใต้โหมดแก้ไขข้อผิดพลาดจะส่งคืนข้อผิดพลาดเป็นไปได้หรือไม่ที่จะเพิกเฉยต่อข้อผิดพลาดนั้น
jongbanaag

@ Dreyfus15 ฉันเชื่อว่าฉันเพิ่งวางสายโดยใช้ซ็อกเก็ตในบล็อกลอง / จับเพื่อจัดการกับมันในพื้นที่ มันไม่นานมานี้ตั้งแต่ฉันได้เห็นสิ่งนี้
Sam

15

คุณไม่สามารถป้องกันกระบวนการที่ปลายสุดของไพพ์ไม่ให้ออกและถ้ามันจบก่อนที่คุณจะเขียนเสร็จคุณจะได้รับสัญญาณ SIGPIPE หากคุณ SIG_IGN สัญญาณการเขียนของคุณจะกลับมาพร้อมกับข้อผิดพลาด - และคุณต้องจดบันทึกและตอบสนองต่อข้อผิดพลาดนั้น เพียงแค่การจับและไม่สนใจสัญญาณในตัวจัดการไม่ใช่ความคิดที่ดี - คุณต้องทราบว่าตอนนี้ไพพ์หมดอายุแล้วและปรับเปลี่ยนพฤติกรรมของโปรแกรมดังนั้นมันจึงไม่ได้เขียนไปที่ไพพ์อีกครั้ง (เพราะสัญญาณจะถูกสร้างขึ้นอีกครั้ง อีกครั้งและคุณจะลองอีกครั้งและกระบวนการทั้งหมดอาจดำเนินต่อไปเป็นเวลานานและสิ้นเปลืองพลังงาน CPU มาก)


6

หรือฉันควรจับ SIGPIPE ด้วยเครื่องจัดการและเพิกเฉย?

ฉันเชื่อว่าถูกต้อง คุณต้องการที่จะรู้ว่าเมื่อสิ้นสุดอีกปิดตัวบ่งชี้ของพวกเขาและนั่นคือสิ่งที่ SIGPIPE บอกคุณ

แซม


3

คู่มือ Linux กล่าวว่า:

EPIPE จุดปลายโลคัลถูกปิดบนซ็อกเก็ตที่มุ่งเน้นการเชื่อมต่อ ในกรณีนี้กระบวนการจะได้รับ SIGPIPE ยกเว้นว่ามีการตั้งค่า MSG_NOSIGNAL

แต่สำหรับ Ubuntu 12.04 มันไม่ถูกต้อง ฉันเขียนบททดสอบสำหรับกรณีนั้นและฉันจะได้รับ EPIPE โดยไม่ต้อง SIGPIPE เสมอ SIGPIPE เป็น genereated ถ้าฉันพยายามที่จะเขียนไปยังซ็อกเก็ตที่ใช้งานไม่ได้ครั้งที่สอง ดังนั้นคุณไม่จำเป็นต้องละเว้น SIGPIPE หากสัญญาณนี้เกิดขึ้นนั่นหมายถึงข้อผิดพลาดทางตรรกะในโปรแกรมของคุณ


1
ฉันได้รับ SIGPIPE แน่นอนที่สุดโดยไม่เห็น EPIPE บนซ็อกเก็ตใน linux ฟังดูเหมือนข้อผิดพลาดของเคอร์เนล
davmac

3

การปฏิบัติที่ดีที่สุดในการป้องกันความผิดพลาดคืออะไร

ปิดใช้งาน sigpipes ตามทุกคนหรือตรวจจับและละเว้นข้อผิดพลาด

มีวิธีการตรวจสอบว่าอีกด้านของบรรทัดยังอ่านอยู่หรือไม่?

ใช่ใช้ select ()

select () ดูเหมือนจะไม่ทำงานที่นี่เพราะจะแจ้งว่าซ็อกเก็ตเขียนได้เสมอ

คุณต้องเลือกบิตอ่าน คุณอาจเพิกเฉยต่อบิตการเขียนได้

เมื่อปลายสุดปิดการจัดการไฟล์ให้เลือกจะบอกคุณว่ามีข้อมูลพร้อมที่จะอ่าน เมื่อคุณไปอ่านแล้วคุณจะได้รับกลับ 0 ไบต์ซึ่งเป็นวิธีที่ระบบปฏิบัติการบอกคุณว่าหมายเลขอ้างอิงไฟล์ถูกปิด

ครั้งเดียวที่คุณไม่สามารถเพิกเฉยบิตการเขียนได้คือถ้าคุณกำลังส่งวอลุ่มขนาดใหญ่และมีความเสี่ยงที่ปลายอีกด้านหนึ่งจะถูกแบ็คล็อกซึ่งอาจทำให้บัฟเฟอร์ของคุณเต็ม หากเป็นเช่นนั้นการพยายามเขียนลงในตัวจัดการไฟล์อาจทำให้โปรแกรม / เธรดของคุณถูกบล็อกหรือล้มเหลว การเลือกการทดสอบก่อนการเขียนจะช่วยปกป้องคุณจากสิ่งนั้น แต่มันไม่รับประกันว่าปลายอีกด้านจะแข็งแรงหรือข้อมูลของคุณกำลังจะมาถึง

โปรดทราบว่าคุณสามารถรับ sigpipe ได้จากระยะใกล้ () เช่นเดียวกับเมื่อคุณเขียน

ปิดล้างข้อมูลบัฟเฟอร์ใด ๆ หากปลายอีกด้านถูกปิดไปแล้วการปิดจะล้มเหลวและคุณจะได้รับ sigpipe

หากคุณใช้ TCPIP ที่บัฟเฟอร์การเขียนที่สำเร็จหมายความว่าข้อมูลของคุณได้รับการจัดคิวเพื่อส่งแล้วนั่นไม่ได้หมายความว่ามันถูกส่งไปแล้ว จนกว่าคุณจะปิดการโทรสำเร็จคุณจะไม่ทราบว่าข้อมูลของคุณถูกส่งไปแล้ว

Sigpipe บอกคุณว่ามีบางอย่างผิดปกติมันไม่ได้บอกอะไรคุณหรือสิ่งที่คุณควรทำ


Sigpipe บอกคุณว่ามีบางอย่างผิดปกติมันไม่ได้บอกอะไรคุณหรืออะไรที่คุณควรทำ เผง จุดประสงค์เพียงอย่างเดียวของ SIGPIPE คือการทำลายยูทิลิตีบรรทัดคำสั่งไพพ์เมื่อขั้นตอนต่อไปไม่ต้องการอินพุตอีกต่อไป หากโปรแกรมของคุณกำลังเชื่อมต่อเครือข่ายคุณควรปิดกั้นหรือเพิกเฉยต่อโปรแกรม SIGPIPE
DepaniDaniel

แม่นยำ. SIGPIPE มีไว้สำหรับสถานการณ์เช่น "find XXX | head" เมื่อหัวตรงกับ 10 บรรทัดแล้วจะไม่มีจุดต่อไปกับการค้นหา ดังนั้นการออกจากหัวและในครั้งต่อไปที่พบพยายามที่จะพูดคุยกับมันก็จะได้รับ sigpipe และรู้ว่ามันก็สามารถออกจาก
Ben Aveling

จริง ๆ แล้วจุดประสงค์ของ SIGPIPE คือการอนุญาตให้โปรแกรมมีความเลอะเทอะและไม่ตรวจสอบข้อผิดพลาดในการเขียน!
William Pursell

2

ภายใต้ระบบ POSIX ที่ทันสมัย ​​(เช่น Linux) คุณสามารถใช้sigprocmask()ฟังก์ชันได้

#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 ...
}

หากคุณต้องการกู้คืนสถานะก่อนหน้าในภายหลังตรวจสอบให้แน่ใจว่าได้บันทึกที่old_stateปลอดภัย หากคุณเรียกใช้ฟังก์ชันนั้นหลายครั้งคุณต้องใช้สแต็กหรือบันทึกครั้งแรกหรือครั้งสุดท้ายเท่านั้นold_state... หรืออาจมีฟังก์ชั่นที่ลบสัญญาณที่ถูกบล็อกโดยเฉพาะ

สำหรับข้อมูลเพิ่มเติมอ่านหน้าคน


ไม่จำเป็นต้องอ่าน - แก้ไข - เขียนชุดของสัญญาณที่ถูกบล็อกเช่นนี้ sigprocmaskเพิ่มในชุดสัญญาณที่ถูกบล็อกดังนั้นคุณสามารถทำสิ่งนี้ทั้งหมดด้วยการโทรเพียงครั้งเดียว
Luchs

ฉันไม่อ่าน - แก้ไข - เขียนฉันอ่านเพื่อบันทึกสถานะปัจจุบันที่ฉันเก็บไว้old_stateเพื่อที่ฉันจะสามารถเรียกคืนได้ในภายหลังถ้าฉันเลือก หากคุณรู้ว่าคุณไม่จำเป็นต้องกู้สถานะกลับคืนมาคุณก็ไม่จำเป็นต้องอ่านและเก็บรักษาด้วยวิธีนี้
Alexis Wilke

1
แต่คุณทำ: การโทรครั้งแรกเพื่อsigprocmask()อ่านสถานะเก่าการโทรเพื่อsigaddsetแก้ไขมันการโทรครั้งที่สองเพื่อsigprocmask()เขียน คุณสามารถลบต่อการเรียกร้องครั้งแรกเริ่มต้นด้วยและเปลี่ยนสายที่สองไปsigemptyset(&set) sigprocmask(SIG_BLOCK, &set, &old_state)
Luchs

อา! ฮ่า คุณถูก. ฉันทำ. ดี ... ในซอฟต์แวร์ของฉันฉันไม่ทราบว่าฉันได้ปิดกั้นสัญญาณใดบ้างหรือไม่ถูกบล็อก ดังนั้นฉันทำอย่างนี้ อย่างไรก็ตามฉันยอมรับว่าหากคุณมี "ชุดสัญญาณเพียงวิธีเดียว" ชุด + ล้างแบบง่ายก็เพียงพอแล้ว
Alexis Wilke
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.