操作系统信号处理循环-阻塞还是非阻塞读取



我的应用程序有一个用于处理操作系统信号的线程,因此不会阻塞programLoop()。这个线程processOSSignals基本上一直在读取信号SIGINT、SIGTERM、SIGQUIT的文件描述符。在接收时,最初为真的loopOver被设置为假。

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。

然而,在许多情况下,为每个I/O操作生成一个线程是低效的。线程需要一个堆栈,它可能会消耗几兆字节,而且处理许多文件描述符(可能有许多不同类型)通常更高效,方法是将它们全部置于非阻塞模式,并等待其中一个准备就绪。

通常,这是使用poll(2)便携式完成的。select(2)是可能的,但在许多系统上,它被限制为一定数量的文件描述符(在Linux上,1024),许多程序将超过这个数量。在Linux上,也可以使用epoll(7)系列函数,如果您已经在使用signalfd(2)这样的不可移植结构,您可能更喜欢这样做。

例如,您可能希望将信号FD处理为主循环的一部分,在这种情况下,将该FD作为主循环使用poll(2)或其他函数处理的FD之一可能更可取。

你应该避免在循环中旋转或使用非阻塞插座睡觉。如果使用poll(2),则可以指定一个超时,在该超时之后,如果没有文件描述符准备就绪,则操作将返回0,因此您可以在不需要使用sleep的情况下控制超时。

与bk2204概述的建议相同:只需使用poll。如果您想要有一个单独的线程,那么向该线程发出信号的一种简单方法是将管道(或套接字)的读取端添加到轮询文件描述符集。然后,主线程在希望线程停止时关闭写入端。CCD_ 14随后将返回并发出信号表示可以从管道读取(因为它将发出信号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及其信号fd。

#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();
}

其他需要注意的事项:

  1. 考虑切换到std::jthread,这样即使程序循环抛出异常,它也会自动加入
  2. 根据您的后台线程所做的工作,您也可以通过调用std::thread::detach在程序端简单地放弃它
  3. 如果线程可能在长循环中保持忙碌(不调用poll),则可以将管道与std::atomic<bool>jthreadstd::stop_token配对,以发出停止事件的信号。这样线程就可以在循环迭代之间检查标志。顺便说一句,当您同时从不同线程读取和写入时,使用普通全局int是无效的
  4. 您也可以使用signalfd并向线程发送特定的信号,使其退出

相关内容

  • 没有找到相关文章

最新更新