[英]OS Signal handling loop - blocking or non-blocking read?
我的應用程序有一個用於處理操作系統信號的線程,因此不會阻塞programLoop()
。 這個線程,processOSSignals,基本上一直在讀取信號 SIGINT、SIGTERM、SIGQUIT 的文件描述符。 在他們的接待中, loopOver
最初是 true,被設置為 false。
int mSigDesc = -1;
void init()
{
// creates file descriptor for reading SIGINT, SIGTERM, SIGQUIT
// blocks signals with sigprocmask(SIG_BLOCK, &mask, nullptr)
...
mSigDesc = signalfd(mSigDesc, &mask, SFD_NONBLOCK); // OR 3rd param = 0?
}
void processOSSignals()
{
while (loopOver)
{
struct signalfd_siginfo fdsi;
auto readedBytes = read(mSigDesc, &fdsi, sizeof(fdsi));
...
}
}
int main()
{
init();
std::thread ossThread(processOSSignals);
programLoop();
ossThread.join();
}
我的問題是 - 應該將mSigDesc
設置為阻塞或非阻塞(異步)模式嗎?
在非阻塞模式下,這個線程總是很忙,但效率低下,一遍又一遍地讀取和返回EAGAIN
。
在阻塞模式下,它會一直等待,直到接收到其中一個信號,但如果從未發送過,ossThread 將永遠不會加入。
應該如何處理? 在非阻塞模式下使用 sleep() ,只是偶爾嘗試閱讀? 或者也許在阻塞模式下使用 select() 來監視mSigDesc
並只在某事時讀取。 那里有嗎?
使用阻塞還是非阻塞 I/O 取決於您希望如何處理 I/O。
通常,如果您有一個專用於從信號文件描述符讀取的線程,並且您只是希望它等待直到它收到信號,那么您應該使用阻塞 I/O。
然而,在許多情況下,為每個 I/O 操作生成一個線程是低效的。 一個線程需要一個堆棧,這可能會消耗幾兆字節,並且通過將它們全部置於非阻塞模式並等待其中一個准備就緒來處理許多文件描述符(可能具有許多不同類型)通常會更有效。
通常,這是使用poll(2)
可移植地完成的。 select(2)
是可能的,但在許多系統上,它被限制為一定數量的文件描述符(在 Linux 上,1024),許多程序會超過該數量。 在 Linux 上,也可以使用epoll(7)
系列函數,如果您已經在使用像signalfd(2)
這樣的不可移植的結構,您可能更喜歡它。
例如,您可能希望將信號 FD 作為主循環的一部分進行處理,在這種情況下,將該 FD 作為您的主循環使用poll(2)
或其他函數之一處理的 FD 可能更可取。
你應該避免做的是在循環中旋轉或使用非阻塞套接字休眠。 如果您使用poll(2)
,您可以指定一個超時,如果沒有文件描述符准備好,該操作在該超時之后返回 0,因此您已經可以控制超時而無需求助於sleep
。
與 bk2204 概述的建議相同:只需使用poll
。 如果你想有一個單獨的線程,一個簡單的方法來通知線程是將管道(或套接字)的讀取端添加到輪詢文件描述符集中。 當主線程希望線程停止時,它會關閉寫入端。 然后poll
將返回並發出可以從管道讀取的信號(因為它會發出 EOF 信號)。
下面是一個實現的概要:
我們首先為文件描述符定義一個 RAII 類。
#include <unistd.h>
// using pipe, close
#include <utility>
// using std::swap, std::exchange
struct FileHandle
{
int fd;
constexpr FileHandle(int fd=-1) noexcept
: fd(fd)
{}
FileHandle(FileHandle&& o) noexcept
: fd(std::exchange(o.fd, -1))
{}
~FileHandle()
{
if(fd >= 0)
::close(fd);
}
void swap(FileHandle& o) noexcept
{
using std::swap;
swap(fd, o.fd);
}
FileHandle& operator=(FileHandle&& o) noexcept
{
FileHandle tmp = std::move(o);
swap(tmp);
return *this;
}
operator bool() const noexcept
{ return fd >= 0; }
void reset(int fd=-1) noexcept
{ *this = FileHandle(fd); }
void close() noexcept
{ reset(); }
};
然后我們用它來構造我們的管道或套接字對。
#include <cerrno>
#include <system_error>
struct Pipe
{
FileHandle receive, send;
Pipe()
{
int fds[2];
if(pipe(fds))
throw std::system_error(errno, std::generic_category(), "pipe");
receive.reset(fds[0]);
send.reset(fds[1]);
}
};
然后該線程在接收端使用poll
及其 signalfd。
#include <poll.h>
#include <signal.h>
#include <sys/signalfd.h>
#include <cassert>
void processOSSignals(const FileHandle& stop)
{
sigset_t mask;
sigemptyset(&mask);
FileHandle sighandle{ signalfd(-1, &mask, 0) };
if(! sighandle)
throw std::system_error(errno, std::generic_category(), "signalfd");
struct pollfd fds[2];
fds[0].fd = sighandle.fd;
fds[1].fd = stop.fd;
fds[0].events = fds[1].events = POLLIN;
while(true) {
if(poll(fds, 2, -1) < 0)
throw std::system_error(errno, std::generic_category(), "poll");
if(fds[1].revents & POLLIN) // stop signalled
break;
struct signalfd_siginfo fdsi;
// will not block
assert(fds[0].revents != 0);
auto readedBytes = read(sighandle.fd, &fdsi, sizeof(fdsi));
}
}
剩下要做的就是按照這樣的順序創建我們的各種 RAII 類,即在線程連接之前關閉管道的寫入端。
#include <thread>
int main()
{
std::thread ossThread;
Pipe stop; // declare after thread so it is destroyed first
ossThread = std::thread(processOSSignals, std::move(stop.receive));
programLoop();
stop.send.close(); // also handled by destructor
ossThread.join();
}
其他注意事項:
std::jthread
,這樣即使程序循環拋出異常,它也會自動加入std::thread::detach
在程序結束時簡單地放棄它poll
),您可以將管道與std::atomic<bool>
或jthread
的std::stop_token
以發出停止事件信號。 這樣線程就可以在循環迭代之間檢查標志。 順便說一句,當您同時從不同線程讀取和寫入時,您對普通全局int
的使用無效
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.