在程序退出之前完成信号CGI输出

时间:2013-11-26 09:57:08

标签: c apache web-applications cgi

我正在努力使用C编写的CGI程序更可靠地进行优化。

应用程序对光盘进行同步写入,我希望在CGI输出完成后完成这些操作。我最初认为这就像关闭/重新打开stdin / stdout / stderr流一样简单,事实上这在许多服务器上运行得非常漂亮 - 即使有几秒钟的光盘写入,我也会在几毫秒内得到用户响应排队等。

不幸的是,我现在在几台服务器上遇到了问题。一旦CGI程序关闭stdout,它就会收到来自Apache的终止信号。几秒钟之后,它遭到了严重的杀戮。调用setsid()似乎对此没有影响。

是否有另一种方法告诉Apache它应该将输出发送到客户端,而不结束CGI程序?

5 个答案:

答案 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)

要考虑的选项是:

  1. CGI程序为Apache生成输出。
  2. 在关闭标准输出之前,它会分叉。
  3. 父进程关闭其标准输出,然后使用其中一个紧急退出函数(_exit()_Exit()或其中一个亲属)来挽救。这样可以避免刷新其他I / O流。
  4. 子进程使用close()关闭文件描述符1 - 它避免使用fclose(),因为父进程正在写入标准输出。
  5. 然后,子流程处理其余的写作工作。
  6. 您可能需要通过设置其进程组来将子进程与其父进程隔离。 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);
}

Adapted from this PERL code

答案 3 :(得分:0)

您可能还会尝试捕获TERM信号并忽略它,直到您完成处理。

答案 4 :(得分:0)

您可以查看FastCGI。它允许您密切关注CGI编程模型,但将请求生命周期与流程生命周期分离。然后你可以做这样的事情:

while (FCGI_Accept() >= 0) {
    // handle normal request
    FCGI_Finish();
    // do synchronous I/O outside of request lifetime
}