【问题标题】:C: select() - Signal interruptC: select() - 信号中断
【发布时间】:2019-06-25 11:04:23
【问题描述】:

我正在用 C 语言编写一个与 AF_UNIX 套接字一起工作的多线程服务器程序。 服务器的基本结构是:

  • 主线程初始化数据结构并创建一个“工作”线程池。
  • 工作线程开始等待空线程安全队列上的新请求
  • 主线程使用select() 调用监听各种套接字(新连接和已连接的客户端)。
    • select() 揭示了连接套接字上可能的读取:主线程调用 accept() 并将返回的文件描述符放入 fd_set(读取集)。
    • select() 揭示了已连接套接字上可能的读取:主线程从 fd_set(读取集)中删除准备好的文件描述符并将它们放入线程安全队列中。
  • 工作线程从队列中提取文件描述符并开始与链接客户端通信以服务请求。在服务工作线程结束时,将套接字文件描述符放回fd_set(我编写了一个函数来使此操作线程安全),然后它返回在队列中再次等待新请求。

此例程在无限循环中重复,直到引发 SIGINT。 必须在不退出循环的情况下对 SIGUSR1 执行另一个功能。

我对此表示怀疑,因为如果我使用EINTR = Interrupted system call 提出 SIGINT 我的程序退出。 我知道pselect() 调用和“self pipe”技巧,但我不知道如何让这些东西在多线程情况下工作。

我正在寻找一种(与 POSIX 兼容的)信号管理,以防止在主线程等待 pselect() 时出现 EINTR 错误。

我贴一些代码来澄清一下:

在这里我设置了信号处理程序(忽略errorConsolePrint函数)

if(signal(SIGINT, &on_SIGINT) == SIG_ERR)
{
    errorConsolePrint("File: %s; Line: %d; ", "Setting SIGINT handler", __FILE__, __LINE__);
    exit(EXIT_FAILURE);
}
if(signal(SIGTERM, &on_SIGINT) == SIG_ERR)
{
    errorConsolePrint("File: %s; Line: %d; ", "Setting SIGINT handler", __FILE__, __LINE__);
    exit(EXIT_FAILURE);
}
if(signal(SIGUSR1, &on_SIGUSR1) == SIG_ERR)
{
    errorConsolePrint("File: %s; Line: %d; ", "Setting to SIGUSR1 handler", __FILE__, __LINE__);
    exit(EXIT_FAILURE);
}

if(signal(SIGPIPE, SIG_IGN) == SIG_ERR)
{
    errorConsolePrint("File: %s; Line: %d; ", "Setting to ignore SIGPIPE", __FILE__, __LINE__);
    exit(EXIT_FAILURE);
}

这里我为pselect设置了信号掩码

sigemptyset(&mask);
sigemptyset(&saveMask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGUSR1);
sigaddset(&mask, SIGPIPE);

这里我打电话给pselect

test = saveSet(masterSet, &backUpSet, &saveMaxFd);
CHECK_MINUS1(test, "Server: creating master set's backup ");

int test = pselect(saveMaxFd+1, &backUpSet, NULL, NULL, &waiting, &mask);
if(test == -1 && errno != EINTR)
{
    ...error handling...
    continue;
}

希望能得到一些帮助! 提前谢谢大家。

【问题讨论】:

  • 我不太了解您真正想知道的内容。如何对SIGINTSIGUSR1 的情况进行不同的处理?如何终止多线程进程?如何优雅地 终止多线程进程?还有什么?
  • EINTR 错误发生在你程序的什么地方?
  • 我的问题是:如何配置信号管理以防止在进程等待select() 调用时出现错误“系统调用中断”
  • 基本计划存在严重缺陷。强烈建议:在main()设置线程池,设置信号处理程序,调用socket(),调用任意setsockopt()s,调用bind()调用listen()然后,在循环中调用accept()然后通过socket从accept() 返回到可用线程。然后线程处理与客户端的所有通信,包括最终关闭套接字
  • @user3629249 为什么你认为这个结构有缺陷?您建议的那个不合时宜,因为客户端(即给定,我无法修改它)可以在两个请求之间等待很长时间,这样一个线程可以无限期地被占用。

标签: c sockets signals multiplexing


【解决方案1】:

先阅读signal(7)signal-safety(7);您可能想要使用 Linux 特定的 signalfd(2),因为它非常适合(对于 SIGTERMSIGQUITSIGINT)进入围绕 poll(2) 或旧的 select(2) (或较新的 pselectppoll)

另请参阅this 对一个非常相似的问题的回答(以及那里提到的pipe(7) to self 技巧,它与 POSIX 兼容)。

另外,signal(2) 文档:

  The effects of signal() in a multithreaded process are unspecified.

所以你真的应该使用sigaction(2)(这是 POSIX)。

【讨论】:

  • 是的,我已经阅读了有关 signalfd() 的信息,这将是完美的,但我不能使用 linux 特定的调用,因为我在问题中写道,我需要一个 POSIX 兼容的解决方案。
  • 是的,我也读过它,这是一个非常好的解决方案,但现在我解决了,正如我在我自己的问题的最后一个答案中所写的那样。这样,一切似乎都很好。你能读一下,告诉我你的想法吗?
【解决方案2】:

好的,我终于有办法了。

我的问题的核心在于我的服务器的多线程特性。 经过长时间的搜索,我发现如果我们从其他进程发出信号(以异步方式),哪个线程捕获信号并不重要,因为行为保持不变:信号被捕获并且先前注册的处理程序是执行。 也许这对其他人来说是显而易见的,但这让我发疯,因为我不知道如何解释执行期间出现的错误。

在那之后,我发现我解决的另一个问题是关于过时的signal() 调用。 在执行期间,我第一次升起SIGUSR1,程序按预期捕获并管理它,但第二次它以User defined signal 1 退出。

我发现signal() 调用为特定信号设置了“一次性”处理程序,在第一次处理该信号后,该信号的行为返回默认值。

这就是我所做的:

这里是信号处理程序:

注意:我在处理程序本身内部为 SIGUSR1 重置了处理程序

static void on_SIGINT(int signum)
{
    if(signum == SIGINT || signum == SIGTERM)
        serverStop = TRUE;
}

static void on_SIGUSR1(int signum)
{
    if(signum == SIGUSR1)
        pendingSIGUSR1 = TRUE;

    if(signal(SIGUSR1, &on_SIGUSR1) == SIG_ERR)
        exit(EXIT_FAILURE);
}

这里我在服务器初始化期间设置了处理程序:

 if(signal(SIGINT, &on_SIGINT) == SIG_ERR)
    exit(EXIT_FAILURE);
if(signal(SIGTERM, &on_SIGINT) == SIG_ERR)
    exit(EXIT_FAILURE);
if(signal(SIGUSR1, &on_SIGUSR1) == SIG_ERR)
    exit(EXIT_FAILURE);
if(signal(SIGPIPE, SIG_IGN) == SIG_ERR)
    exit(EXIT_FAILURE);

这里是服务器的监听周期:

while(!serverStop)
{
    if (pendingSIGUSR1)
    {
        ... things i have to do on SIGUSR1...
        pendingSIGUSR1 = FALSE;
    }


    test = saveSet(masterSet, &backUpSet, &saveMaxFd);
    CHECK_MINUS1(test, "Server: creating master set's backup ");

    int test = select(saveMaxFd+1, &backUpSet, NULL, NULL, &waiting);
    if((test == -1 && errno == EINTR) || test == 0)
        continue;
    if (test == -1 && errno != EINTR)
    {
        perror("Server: Monitoring sockets: ");
        exit(EXIT_FAILURE);
    }

    for(int sock=3; sock <= saveMaxFd; sock++)
    {
        if (FD_ISSET(sock, &backUpSet))
        {
            if(sock == ConnectionSocket)
            {
                ClientSocket = accept(ConnectionSocket, NULL, 0);
                CHECK_MINUS1(ClientSocket, "Server: Accepting connection");

                test = INset(masterSet, ClientSocket);
                CHECK_MINUS1(test, "Server: Inserting new connection in master set: ");
            }
            else
            {
                test = OUTset(masterSet, sock);
                CHECK_MINUS1(test, "Server: Removing file descriptor from select ");
                test = insertRequest(chain, sock);
                CHECK_MINUS1(test, "Server: Inserting request in chain");
            }
         }
    }
}

【讨论】:

  • signal 在多线程程序中没有很好的定义。你想使用sigaction
  • 是的sigaction() 肯定更好,但它在某些平台上给我带来了编译问题,因为我必须使用-std=c99 选项进行编译。
  • 那么这些平台不是POSIX
  • 是的,这些平台是 POSIX,但是(我不知道为什么),当我使用像 sigaction() 这样的调用或像 sigset_t 这样的类型时,编译在 linux 上会出错(因为标准 c99)但是在 Mac OS 上运行没有问题。这让我有点沮丧和困惑了很长时间
  • 只是需要添加additional编译选项。阅读feature_test_macros(7) for Linux
【解决方案3】:

我建议以下策略:

  • 在初始化期间,像您一样设置信号处理程序。
  • 在初始化期间,阻塞所有(可阻塞)信号。参见例如Is it possible to ignore all signals?
  • 在您的主线程中使用 pselect 在调用期间解除阻塞线程,与您一样。

这样做的好处是所有系统调用,包括所有工作线程中的所有系统调用,将永远返回 EINTR,除了单个 pselect主线程。例如,参见Am I over-engineering per-thread signal blocking?pselect does not return on signal when called from a separate thread but works fine in single thread program 的答案。

此策略也适用于select:只需在调用select 之前立即取消阻塞主线程中的信号,然后再重新阻塞它们。如果您的select 超时时间很长或无限,并且您的文件描述符大多处于非活动状态,您只需要pselect 来防止挂起。 (我自己从未使用过pselect,主要是在没有它的旧 Unix 上工作。)

我假设您的信号处理程序是合适的:例如,它们只是自动设置一个全局变量。

顺便说一句,在您的示例代码中,您是否需要 sigaddset(&amp;mask, SIGPIPE),因为 SIGPIPE 已被忽略?

【讨论】:

    【解决方案4】:

    您可能应该做的是将一个线程专用于信号处理。这是一个草图:

    main 中,在产生任何线程之前,阻塞除 SIGILL、SIGABRT、SIGFPE、SIGSEGV 和 SIGBUS 之外的所有信号(使用 pthread_sigmask)。

    然后,生成您的信号处理程序线程。此线程循环调用 sigwaitinfo 以获取您关心的信号。它采取适合每个人的任何行动;这可能包括向主线程发送消息以触发干净关闭 (SIGINT)、将要在工作池 (SIGUSR1) 中处理的“另一个函数”排队等。您确实安装处理程序对于这些信号。

    然后你生成你的线程池,它根本不需要关心信号。

    【讨论】:

    • 是的,我读到了 signalfd()sigwaitinfo() ,但它们仅适用于 linux。我忘记在问题中写了,对不起。还有其他解决方案吗?
    • @zwol 你是对的。我收到编译错误,因为我正在使用 -std=c99 选项进行编译
    • @AlessandroMeschi 供将来参考,在使用 GCC 的几乎所有情况下,-std=gnuXX -Wall -Wpedantic 是比-std=cXX 更好的避免不需要的扩展的方法。 -std=cXX 模式禁用的扩展被广泛使用,以至于我通过用它们编译完全普通的代码一再发现系统头文件中的错误。
    • @AlessandroMeschi 在这种情况下,#define _XOPEN_SOURCE 700 在所有 C 源文件的顶部(在包括 any 系统头文件之前)以访问当前版本标准化的所有内容POSIX 的。 (注意:在某些系统上,这将删除对您可能想要的旧内容的访问,例如gettimeofdaysetitimer(正式被clock_* 接口取代,但有几件事@ 987654336@可以做到他们做不到的)。)
    猜你喜欢
    • 2012-08-30
    • 2020-03-05
    • 1970-01-01
    • 2020-07-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多