[英]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.