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

fgw7neuy  于 2022-12-22  发布在  Linux
关注(0)|答案(2)|浏览(121)

我的应用程序有一个处理操作系统信号的线程,所以不要阻塞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,只在那里有可用的东西时读取?

nue99wik

nue99wik1#

使用阻塞I/O还是非阻塞I/O取决于您希望如何处理I/O。
通常,如果您有一个线程专门用于阅读信号文件描述符,并且您只是希望它等待直到获得信号,那么您应该使用阻塞I/O。
然而,在许多情况下,为每个I/O操作生成一个线程是效率低下的。一个线程需要一个堆栈,这可能会消耗几兆字节,而处理许多文件描述符(可能是许多不同类型)通常更有效的方法是将它们都置于非阻塞模式,并等待其中一个就绪。
select(2)是可行的,但是在许多系统上,它被限制在一定数量的文件描述符(在Linux上,1024),并且许多程序会超过这个数量。在Linux上,epoll(7)家族的函数也可以使用,如果你已经使用了signalfd(2)这样的不可移植结构,你可能会更喜欢它。
例如,您可能希望将信号FD作为主循环的一部分来处理,在这种情况下,将该FD作为主循环使用poll(2)或其他函数之一处理的FD之一可能更合适。
应该避免的是在循环中旋转或使用非阻塞套接字休眠。如果使用poll(2),可以指定一个超时,在此之后,如果没有文件描述符准备就绪,操作将返回0,这样就可以控制超时,而无需求助于sleep

dgenwo3n

dgenwo3n2#

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

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

相关问题