前段时间我为自动S / MIME处理编写了一个简单的SMTP门,现在又进行了测试。对于邮件服务器而言,主进程会为每个传入连接分配一个子。限制创建的子进程的数量是一种很好的做法 - 所以我就这样做了。
在重负载(同时来自许多客户端的许多连接)期间,似乎子进程未正确计数 - 问题在于当子进程退出时减少计数器。几分钟的重负载计数器大于实际的子进程数(即5分钟后它等于14,但没有)。
我已经做了一些研究,但没有任何效果。所有僵尸进程都被收获,因此SIGCHLD
处理似乎没问题。我认为这可能是一个同步问题,但添加互斥锁并将变量类型更改为volatile sig_atomic_t
(就像现在一样)不会给出任何更改。这也不是信号屏蔽的问题,我尝试使用sigfillset(&act.sa_mask)
屏蔽所有信号。
我注意到waitpid()
有时会返回奇怪的PID值(非常大,如172915914)。
问题和一些代码。
init
)是否有可能收获其中一些流程?在main()
中分拣孩子:
volatile sig_atomic_t sproc_counter = 0; /* forked subprocesses counter */
/* S/MIME Gate main function */
int main (int argc, char **argv)
{
[...]
/* set appropriate handler for SIGCHLD */
Signal(SIGCHLD, sig_chld);
[...]
/* SMTP Server's main loop */
for (;;) {
[...]
/* check whether subprocesses limit is not exceeded */
if (sproc_counter < MAXSUBPROC) {
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
smime_gate_service(connfd); /* process the request */
exit(0);
}
++sproc_counter;
}
else
err_msg("subprocesses limit exceeded, connection refused");
[...]
}
Close(connfd); /* parent closes connected socket */
}
信号处理:
Sigfunc *signal (int signo, Sigfunc *func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM) {
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT; /* SunOS 4.x */
#endif
}
else {
#ifdef SA_RESTART
act.sa_flags |= SA_RESTART; /* SVR4, 44BSD */
#endif
}
if (sigaction(signo, &act, &oact) < 0)
return SIG_ERR;
return oact.sa_handler;
}
Sigfunc *Signal (int signo, Sigfunc *func)
{
Sigfunc *sigfunc;
if ( (sigfunc = signal(signo, func)) == SIG_ERR)
err_sys("signal error");
return sigfunc;
}
void sig_chld (int signo __attribute__((__unused__)))
{
pid_t pid;
int stat;
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
--sproc_counter;
err_msg("child %d terminated", pid);
}
return;
}
注意:所有以大写字母开头的函数(例如Fork()
,Close()
,Signal()
等)的行为与小写字母相同朋友(fork()
,close()
,signal()
等),但有更好的错误处理方式 - 因此我无需检查其返回状态。
NOTE2 :我使用kernel v3.10.11
在Debian Testing(gcc 4.8.2
)下运行并编译它。
答案 0 :(得分:0)
我会自己回答。
有几个理由不以这种方式计算子进程。首先,信号处理程序可能被另一个信号中断。我无法找到任何信息,这种情况发生时会发生什么。在libc手册页和this answer中有一些关于它的信息。但这可能不是问题。
volatile sig_atomic_t
变量上的操作似乎不是真正的原子,它依赖于系统架构。例如,在 amd64 上,递减sproc_counter
值的编译代码如下所示:
movl sproc_counter(%rip), %eax
subl $1, %eax
movl %eax, sproc_counter(%rip)
正如您所看到的,有多达三个汇编指令!它绝对不是原子的,因此必须同步对sproc_counter
的访问。
好的,但为什么添加互斥锁没有给出结果?答案在pthread_mutex_lock()
/ pthread_mutex_unlock()
:
ASYNC-SIGNAL SAFETY
互斥锁功能不是异步信号安全的。这意味着什么 不应该从信号处理程序中调用它们。特别是打电话 来自信号处理程序的pthread_mutex_lock或pthread_mutex_unlock可以 死锁调用线程。
这说清楚了。什么是更多的调用函数,打印日期(日志消息)也是一个坏主意 - 在那里使用fputs()
不是异步信号安全。
如何正确完成?
考虑到信号处理过程中可能发生的事情(即传递其他信号),很明显信号处理程序应该尽可能简洁。 set a flag in handler要好一些,并在主程序或专用线程中不时进行测试。我选择了第二种解决方案。
没有更多的话,让我们看一些代码。
信号处理将是这样的:
void sig_chld (int signo __attribute__((__unused__)))
{
sigchld_notify = 1;
}
main()
例程:
volatile sig_atomic_t sigchld_notify = 0; /* SIGCHLD notifier */
int sproc_counter = 0; /* forked child process counter */
pthread_mutex_t sproc_mutex = PTHREAD_MUTEX_INITIALIZER; /* mutex for child process counter */
/* S/MIME Gate main function */
int main (int argc, char **argv)
{
pthread_t guard_id;
[...]
/* start child process guard */
if (0 != pthread_create(&guard_id, NULL, child_process_guard, NULL) )
err_sys("pthread_create error");
[...]
/* SMTP Server's main loop */
for (;;) {
[...]
/* check whether child processes limit is not exceeded */
if (sproc_counter < MAXSUBPROC) {
if ( (childpid = Fork()) == 0) { /* child process */
Close(listenfd); /* close listening socket */
smime_gate_service(connfd); /* process the request */
exit(0);
}
pthread_mutex_lock(&sproc_mutex);
++sproc_counter;
pthread_mutex_unlock(&sproc_mutex);
}
else
err_msg("subprocesses limit exceeded, connection refused");
Close(connfd); /* parent closes connected socket */
}
} /* end of main() */
保护线程例程:
extern volatile sig_atomic_t sigchld_notify; /* SIGCHLD notifier */
extern int sproc_counter; /* forked child process counter */
extern pthread_mutex_t sproc_mutex; /* mutex for child process counter */
void* child_process_guard (void* arg __attribute__((__unused__)))
{
pid_t pid;
int stat;
for (;;) {
if (0 == sigchld_notify) {
usleep(SIGCHLD_SLEEP);
continue;
}
while ( (pid = waitpid(-1, &stat, WNOHANG)) > 0) {
pthread_mutex_lock(&sproc_mutex);
--sproc_counter;
pthread_mutex_unlock(&sproc_mutex);
err_msg("child %d terminated", pid);
}
sigchld_notify = 0;
}
return NULL;
}
答案 1 :(得分:0)
我认为信号方法可以修复,而创建一个线程会强迫你执行一个程序来处理连接。
有几个问题:
如果创建流程并同时结束,则sproc_counter
的更改可能会丢失。要解决此问题,请使用信号掩码(例如sigprocmask()
,pselect()
)以确保在主流操作sproc_counter
时不调用处理程序,或使信号处理程序设置标志并执行waitpid()
,计数器操作和登录主流程(但不在新线程中)。请注意,如果要在结束连接后直接避免为新连接或其他结束连接休眠,则flag方法仍需要信号掩码操作。
err_msg()
可能不是异步信号安全的。我看到三个选项:
SIGCHLD
取消屏蔽时调用异步信号不安全函数,或覆盖signal()
可能会导致其他代码调用您的版本而不是标准版本。这可能会导致奇怪的行为。
信号处理程序不保存和恢复errno
的值。
如果由于信号中断其他信号而出现问题,那就是sigaction
sa_mask
字段的用途。