Tôi muốn đề xuất bằng cách bắt đầu so sánh với select()
và poll()
. Linux cũng cung cấp cả pselect()
và ppoll()
; và đối số thêm const sigset_t *
đối với pselect()
và ppoll()
(so với select()
và poll()
) có cùng tác dụng trên mỗi "biến thể p", giống như vậy. Nếu bạn không sử dụng tín hiệu, bạn không có chủng tộc để bảo vệ chống lại, do đó, câu hỏi cơ sở thực sự là về hiệu quả và dễ lập trình.
Trong khi đó, đã có câu trả lời stackoverflow.com tại đây: what are the differences between poll and select.
Đối với cuộc đua: một khi bạn bắt đầu sử dụng tín hiệu (vì lý do gì), bạn sẽ biết rằng nói chung, bộ xử lý tín hiệu chỉ cần đặt một loại volatile sig_atomic_t
để cho biết tín hiệu đã được phát hiện. Lý do cơ bản cho điều này là nhiều cuộc gọi thư viện không phải là re-entrant và tín hiệu có thể được gửi trong khi bạn đang "ở giữa" một thói quen như vậy. Ví dụ, chỉ cần in một thông báo đến một cấu trúc dữ liệu kiểu luồng như stdout
(C) hoặc cout
(C++) có thể dẫn đến các vấn đề tái nhập.
Giả sử bạn có mã mà sử dụng một biến volatile sig_atomic_t flag
, có lẽ để bắt SIGINT
, một cái gì đó như thế này (xem thêm http://pubs.opengroup.org/onlinepubs/007904975/functions/sigaction.html):
volatile sig_atomic_t got_interrupted = 0;
void caught_signal(int unused) {
got_interrupted = 1;
}
...
struct sigaction sa;
sa.sa_handler = caught_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGINT, &sa, NULL) == -1) ... handle error ...
...
Bây giờ, trong phần chính của mã của bạn, bạn có thể muốn để "chạy cho đến khi bị gián đoạn":
while (!got_interrupted) {
... do some work ...
}
Điều này là tốt cho đến khi bạn khởi động cần phải thực hiện cuộc gọi mà chờ đợi đối với một số đầu vào/đầu ra, chẳng hạn như select
hoặc poll
. Hành động "chờ" cần đợi cho I/O đó, nhưng nó cũng cần phải chờ cho việc ngắt SIGINT
. Nếu bạn chỉ cần viết:
while (!got_interrupted) {
... do some work ...
result = select(...); /* or result = poll(...) */
}
sau đó nó có thể là các ngắt sẽ xảy ra ngay trước khi bạn gọi select()
hoặc poll()
, chứ không phải sau đó. Trong trường hợp này, bạn đã bị gián đoạn — và biến số got_interrupted
được đặt — nhưng sau đó, bạn bắt đầu chờ. Bạn nên kiểm tra biến số got_interrupted
trước khi bắt đầu chờ đợi, không phải sau đó.
Bạn có thể thử viết:
while (!got_interrupted) {
... do some work ...
if (!got_interrupted)
result = select(...); /* or result = poll(...) */
}
này co lại cửa sổ "cuộc đua", bởi vì bây giờ bạn sẽ phát hiện ra sự ngắt nếu nó xảy ra trong khi bạn đang ở trong "làm một số công việc" mã; nhưng vẫn còn một cuộc đua, bởi vì gián đoạn có thể xảy ra ngay sau bạn kiểm tra biến, nhưng ngay trước lựa chọn hoặc bỏ phiếu.
Giải pháp là làm cho "kiểm tra, sau đó chờ đợi" chuỗi "nguyên tử", sử dụng các tính chất tín hiệu chặn của sigprocmask
(hoặc, trong POSIX ren mã, pthread_sigmask
):
sigset_t mask, omask;
...
while (!got_interrupted) {
... do some work ...
/* begin critical section, test got_interrupted atomically */
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
if (sigprocmask(SIG_BLOCK, &mask, &omask))
... handle error ...
if (got_interrupted) {
sigprocmask(SIG_SETMASK, &omask, NULL); /* restore old signal mask */
break;
}
result = pselect(..., &omask); /* or ppoll() etc */
sigprocmask(SIG_SETMASK, &omask, NULL);
/* end critical section */
}
(trên mã thực sự không phải là tuyệt vời, nó được cấu trúc để minh họa hơn là hiệu quả - nó hiệu quả hơn để làm thao tác mặt nạ tín hiệu hơi khác một chút, và đặt các bài kiểm tra "bị gián đoạn" một cách khác nhau).
Cho đến khi bạn thực sự bắt đầu cần phải bắt SIGINT
, tuy nhiên, bạn chỉ cần so sánh select()
và poll()
(và nếu bạn bắt đầu cần một số lượng lớn mô tả, một số các công cụ dựa trên sự kiện như epoll()
là hiệu quả hơn hoặc là một).
"Lý do cơ bản cho điều này là nhiều cuộc gọi thư viện không được tái nhập" Vì vậy, những gì sẽ xảy ra nếu một tín hiệu được giao trong khi chúng tôi đang ở giữa nói "đọc". Điều này có nghĩa là chúng ta không bao giờ có thể sử dụng đọc một lần nữa? – kptlronyttcna
@kptlronyttcna: không, nhưng cũng hơi có: bạn không thể gọi một cách an toàn, ví dụ: 'fread' trên stdin trong trình xử lý tín hiệu. Điều đó không có nghĩa là bạn có thể * không bao giờ * gọi 'fread', chỉ là bạn không thể làm điều đó trên một biến chia sẻ trong một trình xử lý tín hiệu. Các chi tiết chính xác về những gì bạn có thể làm một cách an toàn và những gì bạn không thể thay đổi tùy theo thư viện, hệ thống và các chi tiết khác. (Là một điểm phụ, khi 'đọc' là một cuộc gọi hệ thống, vì nó nằm trên Linux và BSD và Mac, hành vi chính xác của nó phụ thuộc vào đối tượng hệ thống tệp cơ bản." Các thiết bị chậm "có thể trả về lỗi EINTR hoặc đọc ngắn.) – torek