C: select() - 信号中断
C: select() - Signal interrupt
我正在用 C 语言编写一个使用 AF_UNIX 套接字的多线程服务器程序。
服务器的基本结构是:
- 主线程初始化数据结构并创建一个 "worker" 线程池。
- 工作线程开始在空线程安全队列上等待新请求
- 主线程通过
select()
调用侦听各种套接字(新连接和已连接的客户端)。
select()
显示连接套接字上可能的读取:主线程调用 accept()
并将返回的文件描述符放入 fd_set
(读取集)。
select()
显示已连接套接字上的可能读取:主线程从 fd_set
(读取集)中删除就绪文件描述符并将它们放入线程安全队列中。
- 工作线程从队列中提取文件描述符并开始与链接的客户端通信以服务于请求。在服务工作线程结束时,将套接字文件描述符放回
fd_set
(我写了一个函数使该操作线程安全)并且它 returns 再次在队列中等待新请求。
此例程无限循环重复,直到发出 SIGINT。
必须在不退出循环的情况下对 SIGUSR1 执行另一个功能。
我对此表示怀疑,因为如果我发出 SIGINT,我的程序会以 EINTR = Interrupted system call
退出。
我知道 pselect()
调用和 "self pipe" 技巧,但我不知道如何让它们在多线程情况下工作。
我正在寻找一种(POSIX 兼容)信号管理,它可以在主线程等待 pselect()
时防止 EINTR
错误。
我post一些代码来说明:
这里我设置了信号处理程序(忽略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;
}
希望得到一些帮助!
提前谢谢大家。
您可能应该做的是将一个线程专用于信号处理。这是一个草图:
在 main
中,在生成任何线程之前,阻止除 SIGILL、SIGABRT、SIGFPE、SIGSEGV 和 SIGBUS 之外的所有信号(使用 pthread_sigmask
)。
然后,生成您的信号处理程序线程。此线程循环调用 sigwaitinfo
以获取您关心的信号。它采取适合每个人的任何行动;这可能包括向主线程发送消息以触发干净关闭(SIGINT),将 "another function" 排队到工作池(SIGUSR1)中进行处理,等等。你做 not 为这些信号安装处理程序。
然后你生成你的线程池,它根本不需要关心信号。
我建议采用以下策略:
- 在初始化期间,像您一样设置信号处理程序。
- 在初始化期间,阻止所有(可阻止的)信号。例如参见 [=19=].
- 在您的主线程中使用
pselect
以在调用期间解除线程阻塞,同样如此。
这样做的好处是 所有 系统调用,包括所有工作线程中的所有系统调用,永远不会 return EINTR,主线程中的单个 pselect
除外。例如,参见 Am I over-engineering per-thread signal blocking? and 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(&mask, SIGPIPE)
,因为 SIGPIPE
已被忽略?
好的,终于找到解决办法了。
我的问题的核心是我的服务器的多线程特性。
经过长时间的搜索,我发现如果我们有其他进程发出的信号(以异步方式),哪个线程捕获信号并不重要,因为行为保持不变:信号被捕获并且之前注册的处理程序是执行。
也许这对其他人来说是显而易见的,但这让我发疯,因为我不知道如何解释执行过程中出现的错误。
在那之后我发现了另一个我解决的问题,是关于过时的 signal()
调用。
在执行过程中,我第一次上升 SIGUSR1
,程序按预期捕获并管理它,但第二次它以 User defined signal 1
.
退出
我发现 signal()
为特定信号调用设置 "one time" 处理程序,在第一次处理该信号后,该信号的行为 return 为默认行为。
所以这就是我所做的:
这里是信号处理程序:
N.B.: 我在处理程序本身
中重置了 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(7) and signal-safety(7); you might want to use the Linux specific signalfd(2) since it fits nicely (for SIGTERM
& SIGQUIT
and SIGINT
) into event loops around poll(2) or the old select(2)(或较新的 pselect
或 ppoll
)
另请参阅 this answer (and the pipe(7) 那里提到的自我欺骗,它 POSIX 兼容)到一个非常相似的问题。
此外,signal(2) 文档:
The effects of signal() in a multithreaded process are unspecified.
所以你真的应该使用 sigaction(2)(即 POSIX)。
我正在用 C 语言编写一个使用 AF_UNIX 套接字的多线程服务器程序。 服务器的基本结构是:
- 主线程初始化数据结构并创建一个 "worker" 线程池。
- 工作线程开始在空线程安全队列上等待新请求
- 主线程通过
select()
调用侦听各种套接字(新连接和已连接的客户端)。select()
显示连接套接字上可能的读取:主线程调用accept()
并将返回的文件描述符放入fd_set
(读取集)。select()
显示已连接套接字上的可能读取:主线程从fd_set
(读取集)中删除就绪文件描述符并将它们放入线程安全队列中。
- 工作线程从队列中提取文件描述符并开始与链接的客户端通信以服务于请求。在服务工作线程结束时,将套接字文件描述符放回
fd_set
(我写了一个函数使该操作线程安全)并且它 returns 再次在队列中等待新请求。
此例程无限循环重复,直到发出 SIGINT。 必须在不退出循环的情况下对 SIGUSR1 执行另一个功能。
我对此表示怀疑,因为如果我发出 SIGINT,我的程序会以 EINTR = Interrupted system call
退出。
我知道 pselect()
调用和 "self pipe" 技巧,但我不知道如何让它们在多线程情况下工作。
我正在寻找一种(POSIX 兼容)信号管理,它可以在主线程等待 pselect()
时防止 EINTR
错误。
我post一些代码来说明:
这里我设置了信号处理程序(忽略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;
}
希望得到一些帮助! 提前谢谢大家。
您可能应该做的是将一个线程专用于信号处理。这是一个草图:
在 main
中,在生成任何线程之前,阻止除 SIGILL、SIGABRT、SIGFPE、SIGSEGV 和 SIGBUS 之外的所有信号(使用 pthread_sigmask
)。
然后,生成您的信号处理程序线程。此线程循环调用 sigwaitinfo
以获取您关心的信号。它采取适合每个人的任何行动;这可能包括向主线程发送消息以触发干净关闭(SIGINT),将 "another function" 排队到工作池(SIGUSR1)中进行处理,等等。你做 not 为这些信号安装处理程序。
然后你生成你的线程池,它根本不需要关心信号。
我建议采用以下策略:
- 在初始化期间,像您一样设置信号处理程序。
- 在初始化期间,阻止所有(可阻止的)信号。例如参见 [=19=].
- 在您的主线程中使用
pselect
以在调用期间解除线程阻塞,同样如此。
这样做的好处是 所有 系统调用,包括所有工作线程中的所有系统调用,永远不会 return EINTR,主线程中的单个 pselect
除外。例如,参见 Am I over-engineering per-thread signal blocking? and 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(&mask, SIGPIPE)
,因为 SIGPIPE
已被忽略?
好的,终于找到解决办法了。
我的问题的核心是我的服务器的多线程特性。 经过长时间的搜索,我发现如果我们有其他进程发出的信号(以异步方式),哪个线程捕获信号并不重要,因为行为保持不变:信号被捕获并且之前注册的处理程序是执行。 也许这对其他人来说是显而易见的,但这让我发疯,因为我不知道如何解释执行过程中出现的错误。
在那之后我发现了另一个我解决的问题,是关于过时的 signal()
调用。
在执行过程中,我第一次上升 SIGUSR1
,程序按预期捕获并管理它,但第二次它以 User defined signal 1
.
我发现 signal()
为特定信号调用设置 "one time" 处理程序,在第一次处理该信号后,该信号的行为 return 为默认行为。
所以这就是我所做的:
这里是信号处理程序:
N.B.: 我在处理程序本身
中重置了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(7) and signal-safety(7); you might want to use the Linux specific signalfd(2) since it fits nicely (for SIGTERM
& SIGQUIT
and SIGINT
) into event loops around poll(2) or the old select(2)(或较新的 pselect
或 ppoll
)
另请参阅 this answer (and the pipe(7) 那里提到的自我欺骗,它 POSIX 兼容)到一个非常相似的问题。
此外,signal(2) 文档:
The effects of signal() in a multithreaded process are unspecified.
所以你真的应该使用 sigaction(2)(即 POSIX)。