我正在努力使用C编写的CGI程序更可靠地进行优化。
应用程序对光盘进行同步写入,我希望在CGI输出完成后完成这些操作。我最初认为这就像关闭/重新打开stdin / stdout / stderr流一样简单,事实上这在许多服务器上运行得非常漂亮 - 即使有几秒钟的光盘写入,我也会在几毫秒内得到用户响应排队等。
不幸的是,我现在在几台服务器上遇到了问题。一旦CGI程序关闭stdout,它就会收到来自Apache的终止信号。几秒钟之后,它遭到了严重的杀戮。调用setsid()
似乎对此没有影响。
是否有另一种方法告诉Apache它应该将输出发送到客户端,而不结束CGI程序?
答案 0 :(得分:2)
在那里抛出想法。当前设置如下所示
+------------+ +------------+
| CGI |----------->| Apache |
| Program |<-----------| Server |
+------------+ +------------+
这样的事情
+------------+ +------------+ +------------+
| Daemon |--->| CGI |--->| Apache |
| Program |<---| Passthru |<---| Server |
+------------+ +------------+ +------------+
基本上,将当前程序的所有功能移动到启动时启动一次的守护程序。然后为Apache创建一个微小的passthru程序,通过CGI启动。 passthru程序使用IPC(共享内存或套接字)将自身附加到守护程序。 passthru程序在stdin上收到的所有东西,都转发给守护进程。 passthru程序从守护进程接收的所有东西,它都在stdout上转发给Apache。这样,Apache就可以随心所欲地启动/终止passthru程序,而不会影响你在后端尝试做什么。
答案 1 :(得分:2)
要考虑的选项是:
_exit()
或_Exit()
或其中一个亲属)来挽救。这样可以避免刷新其他I / O流。close()
关闭文件描述符1 - 它避免使用fclose()
,因为父进程正在写入标准输出。您可能需要通过设置其进程组来将子进程与其父进程隔离。 Apache只能通过进程组向子进程发送信号(因为它不知道子进程的PID),因此通过取消与原始进程组的关联,您的子进程将不受Apache发送信号的影响。
值得考虑的另一个选择是父母做的工作,然后分叉。父进程不刷新标准输出;它只是使用紧急出口功能。子进程将自己与父进程隔离,然后使用fclose()
关闭标准输出,刷新任何挂起的输出。 Apache看到该文件已关闭,并继续以其快乐的方式。子进程然后进行清理。同样,将孩子设置为自己的过程组至关重要。这样做的好处是输出直到孩子自己隔离后才关闭,所以Apache不应该使用计时巧合向孩子发送信号(Apache的孙子)。您甚至可以在分叉之前将流程隔离在其自己的流程组中...这也消除了孩子的漏洞窗口。但是,在您即将分叉和退出之前,您不应该做那种隔离;否则,你会破坏Apache提供的保护机制。
这是在编译到程序playcgi.c
的源文件playcgi
中模拟您可能会执行的操作:
#include <errno.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
static void err_exit(const char *fmt, ...);
static void be_childish(void);
static void be_parental(pid_t pid, int fd[2]);
int main(void)
{
int fd[2];
if (pipe(fd) != 0)
err_exit("pipe()");
pid_t pid = fork();
if (pid < 0)
err_exit("fork()");
else if (pid == 0)
{
dup2(fd[1], STDOUT_FILENO);
close(fd[0]);
close(fd[1]);
be_childish();
}
else
be_parental(pid, fd);
return 0;
}
static void be_parental(pid_t pid, int fd[2])
{
close(fd[1]);
char buffer[1024];
int nbytes;
while ((nbytes = read(fd[0], buffer, sizeof(buffer))) > 0)
write(STDOUT_FILENO, buffer, nbytes);
kill(-pid, SIGTERM);
struct timespec nap = { .tv_sec = 0, .tv_nsec = 10 * 1000 * 1000 };
nanosleep(&nap, 0);
int status;
pid_t corpse = waitpid(pid, &status, WNOHANG);
if (corpse <= 0)
{
kill(-pid, SIGKILL);
corpse = waitpid(pid, &status, 0);
}
printf("PID %5d died 0x%.4X\n", corpse, status);
}
static void be_childish(void)
{
/* Simulate activity on pipe */
for (int i = 0; i < 10; i++)
printf("Data block %d from child (%d)\n", i, (int)getpid());
fflush(stdout);
/* Create new pipe to coordinate between child and grandchild */
int fd[2];
if (pipe(fd) != 0)
err_exit("child's pipe()");
pid_t pid = fork();
if (pid < 0)
err_exit("child's fork()");
if (pid > 0)
{
char buffer[4];
fprintf(stderr, "Child (%d) waiting\n", (int)getpid());
close(fd[1]);
read(fd[0], buffer, sizeof(buffer));
close(fd[0]);
fprintf(stderr, "Child (%d) exiting\n", (int)getpid());
close(STDOUT_FILENO);
_exit(0);
}
else
{
/* Grandchild continues - with no standard output */
close(STDOUT_FILENO);
pid_t sid = setsid();
fprintf(stderr, "Grandchild (%d) in session %d\n", (int)getpid(), (int)sid);
/* Let child know grandchild has set its own session */
close(fd[0]);
close(fd[1]);
struct timespec nap = { .tv_sec = 2, .tv_nsec = 0 };
nanosleep(&nap, 0);
for (int i = 0; i < 10; i++)
{
fprintf(stderr, "Data block %d from grandchild (%d)\n", i, (int)getpid());
}
fprintf(stderr, "Grandchild (%d) exiting\n", (int)getpid());
}
}
static void err_vexit(const char *fmt, va_list args)
{
int errnum = errno;
vfprintf(stderr, fmt, args);
if (fmt[strlen(fmt)-1] != '\n')
putc('\n', stderr);
if (errno != 0)
fprintf(stderr, "%d: %s\n", errnum, strerror(errnum));
exit(EXIT_FAILURE);
}
static void err_exit(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
err_vexit(fmt, args);
va_end(args);
}
函数main()
和be_parental()
模拟Apache可能做的事情。有一个管道成为CGI子进程的标准输出。父对象从管道中读取,然后向子进程发送终止信号,然后暂停10毫秒,然后查找尸体。 (这可能是代码中最不具说服力的部分,但是......)如果它找不到,它会发送一个SIGKILL信号并收集死亡孩子的尸体。它报告了孩子如何死亡和返回(在此模拟中,成功退出)。
函数be_childish()
是CGI子进程。它将一些输出写入其标准输出,并刷新标准输出。然后它创建一个管道,以便子孙可以同步他们的活动。孩子叉。幸存的孩子报告它正在等待孙子,关闭管道的写入端,读取管道的读取端(对于永远不会到达的数据,因此读取将返回0表示EOF)。它关闭管道的读取端,报告(在标准错误上)它将退出,关闭其标准输出,并退出。
同时,孙子关闭标准输出,然后使自己成为会话领导者
setsid()
。它报告其新状态,关闭管道的两端,从而释放其父级(原始子进程),以便它可以退出。然后需要2秒钟的睡眠时间 - 父母和祖父母要离开的时间充足 -
然后将一些信息写入标准错误,并退出&#39;消息和退出。
$ ./playcgi
Data block 0 from child (60867)
Data block 1 from child (60867)
Data block 2 from child (60867)
Data block 3 from child (60867)
Data block 4 from child (60867)
Data block 5 from child (60867)
Data block 6 from child (60867)
Data block 7 from child (60867)
Data block 8 from child (60867)
Data block 9 from child (60867)
Child (60867) waiting
Grandchild (60868) in session 60868
Child (60867) exiting
PID 60867 died 0x0000
$ Data block 0 from grandchild (60868)
Data block 1 from grandchild (60868)
Data block 2 from grandchild (60868)
Data block 3 from grandchild (60868)
Data block 4 from grandchild (60868)
Data block 5 from grandchild (60868)
Data block 6 from grandchild (60868)
Data block 7 from grandchild (60868)
Data block 8 from grandchild (60868)
Data block 9 from grandchild (60868)
Grandchild (60868) exiting
您可以按返回以输入空命令行并获取另一个命令提示符。
在#60; PID 60867死亡之前有一个明显的停顿0x0000&#39;消息(以及出现的提示)和来自孙子(60868)&#39;的数据块0的输出信息。这表明尽管父母死亡等,孩子仍在继续。你可以在孩子打盹的同时(因此发出信号),或者父进程向进程组发送信号(kill(-pid, SIGTERM)
和{{1}但是我相信孙子将会继续写作。
答案 2 :(得分:1)
使用mod_cgi和单个进程时无法做到这一点。它在资源上很重要,但实现这一目标的普遍接受的方法称为双叉。它是这样的:
pid_t kid, grandkid;
if ((kid = fork())) {
waitpid(kid, null, 0);
}
else if ((grandkid = fork())) {
exit(0);
}
else {
// code here
// do something long lasting
exit(0);
}
答案 3 :(得分:0)
您可能还会尝试捕获TERM
信号并忽略它,直到您完成处理。
答案 4 :(得分:0)
您可以查看FastCGI。它允许您密切关注CGI编程模型,但将请求生命周期与流程生命周期分离。然后你可以做这样的事情:
while (FCGI_Accept() >= 0) {
// handle normal request
FCGI_Finish();
// do synchronous I/O outside of request lifetime
}