简体   繁体   English

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

[英]OS Signal handling loop - blocking or non-blocking read?

My application has a thread for handling OS signals, so to not block the programLoop() .我的应用程序有一个用于处理操作系统信号的线程,因此不会阻塞programLoop() This thread, processOSSignals, basically keeps on reading the file descriptor for signals SIGINT, SIGTERM, SIGQUIT.这个线程,processOSSignals,基本上一直在读取信号 SIGINT、SIGTERM、SIGQUIT 的文件描述符。 On their reception, loopOver being initially true, is set to false.在他们的接待中, 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();
}

My question is - should mSigDesc be set to blocking or non-blocking (asynchronous) mode?我的问题是 - 应该将mSigDesc设置为阻塞或非阻塞(异步)模式吗?

In non-blocking mode, this thread is always busy, but inefficiently reading and returning EAGAIN over and over again.在非阻塞模式下,这个线程总是很忙,但效率低下,一遍又一遍地读取和返回EAGAIN

In blocking mode, it waits until one of the signals is received, but if it is never sent, the ossThread will never join.在阻塞模式下,它会一直等待,直到接收到其中一个信号,但如果从未发送过,ossThread 将永远不会加入。

How should it be handled?应该如何处理? Use sleep() in the non-blocking mode, to attempt reading only occasionally?在非阻塞模式下使用 sleep() ,只是偶尔尝试阅读? Or maybe use select() in the blocking mode, to monitor mSigDesc and read only when sth.或者也许在阻塞模式下使用 select() 来监视mSigDesc并只在某事时读取。 is available there?那里有吗?

Whether you use blocking or non-blocking I/O depends on how you want to handle your I/O.使用阻塞还是非阻塞 I/O 取决于您希望如何处理 I/O。

Typically, if you have a single thread which is dedicated to reading from the signal file descriptor and you simply want it to wait until it gets a signal, then you should use blocking I/O.通常,如果您有一个专用于从信号文件描述符读取的线程,并且您只是希望它等待直到它收到信号,那么您应该使用阻塞 I/O。

However, in many contexts, spawning a single thread for each I/O operation is inefficient.然而,在许多情况下,为每个 I/O 操作生成一个线程是低效的。 A thread requires a stack, which may consume a couple megabytes, and it's often more efficient to process many file descriptors (which may be of many different types) by putting them all in non-blocking mode and waiting until one of them is ready.一个线程需要一个堆栈,这可能会消耗几兆字节,并且通过将它们全部置于非阻塞模式并等待其中一个准备就绪来处理许多文件描述符(可能具有许多不同类型)通常会更有效。

Typically, this is done portably using poll(2) .通常,这是使用poll(2)可移植地完成的。 select(2) is possible, but on many systems, it is limited to a certain number of file descriptors (on Linux, 1024), and many programs will exceed that number. select(2)是可能的,但在许多系统上,它被限制为一定数量的文件描述符(在 Linux 上,1024),许多程序会超过该数量。 On Linux, the epoll(7) family of functions can also be used, and you may prefer that if you're already using such non-portable constructions as signalfd(2) .在 Linux 上,也可以使用epoll(7)系列函数,如果您已经在使用像signalfd(2)这样的不可移植的结构,您可能更喜欢它。

For example, you might want to handle signal FDs as part of your main loop, in which case including that FD as one the FDs that your main loop processes using poll(2) or one of the other functions might be more desirable.例如,您可能希望将信号 FD 作为主循环的一部分进行处理,在这种情况下,将该 FD 作为您的主循环使用poll(2)或其他函数之一处理的 FD 可能更可取。

What you should avoid doing is spinning in a loop or sleeping with a non-blocking socket.你应该避免做的是在循环中旋转或使用非阻塞套接字休眠。 If you use poll(2) , you can specify a timeout after which the operation returns 0 if no file descriptor was ready, so you can already control a timeout without needing to resort to sleep .如果您使用poll(2) ,您可以指定一个超时,如果没有文件描述符准备好,该操作在该超时之后返回 0,因此您已经可以控制超时而无需求助于sleep

Same advise as bk2204 outlined: Just use poll .与 bk2204 概述的建议相同:只需使用poll If you want to have a separate thread, a simple way to signal that thread is to add the read side of a pipe (or socket) to the set of polled file descriptors.如果你想有一个单独的线程,一个简单的方法来通知线程是将管道(或套接字)的读取端添加到轮询文件描述符集中。 The main thread then closes the write side when it wants the thread to stop.当主线程希望线程停止时,它会关闭写入端。 poll will then return and signal that reading from the pipe is possible (since it will signal EOF).然后poll将返回并发出可以从管道读取的信号(因为它会发出 EOF 信号)。

Here is the outline of an implementation:下面是一个实现的概要:

We start by defining an RAII class for file descriptors.我们首先为文件描述符定义一个 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(); }
};

Then we use that to construct our pipe or socket pair.然后我们用它来构造我们的管道或套接字对。

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

The thread then uses poll on the receive end and its signalfd.然后该线程在接收端使用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));
    }
}

All that remains to be done is create our various RAII classes in such an order that the write side of the pipe is closed before the thread is joined.剩下要做的就是按照这样的顺序创建我们的各种 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();
}

Other things to note:其他注意事项:

  1. Consider switching to std::jthread so that it joins automatically even if the program loop throws an exception考虑切换到std::jthread ,这样即使程序循环抛出异常,它也会自动加入
  2. Depending on what your background thread does, you can also simply abandon it on program end by calling std::thread::detach根据您的后台线程的作用,您也可以通过调用std::thread::detach在程序结束时简单地放弃它
  3. If the thread may stay busy (not calling poll ) for long loops, you can pair the pipe up with an std::atomic<bool> or jthread 's std::stop_token to signal the stop event.如果线程可能在长循环中保持忙碌(不调用poll ),您可以将管道与std::atomic<bool>jthreadstd::stop_token以发出停止事件信号。 That way the thread can check the flag in between loop iterations.这样线程就可以在循环迭代之间检查标志。 Incidentally, your use of a plain global int was invalid as you read and write from different threads at the same time顺便说一句,当您同时从不同线程读取和写入时,您对普通全局int的使用无效
  4. You could also use the signalfd and send a specific signal to the thread for it to quit您还可以使用 signalfd 并向线程发送特定信号以使其退出

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM