科技行者

行者学院 转型私董会 科技行者专题报道 网红大战科技行者

知识库

知识库 安全导航

至顶网软件频道基础软件Apache中的进程剖析(3)

Apache中的进程剖析(3)

  • 扫一扫
    分享文章到微信

  • 扫一扫
    关注官方公众号
    至顶头条

本文介绍了Apache中对Unix进程和Window进程的封装,在阅读这部分内容之前必须具有以下基本知识:

作者:tingya 来源:CSDN 2008年3月26日

关键字: 剖析 进程 Apache 开源

  • 评论
  • 分享微博
  • 分享邮件
.2 进程创建
6.2.1 Unix系统中进程创建
APR中通过apr_proc_create函数实现进程的创建,不过对于APR而言,创建进程并不仅仅是调用fork生成子进程就完毕了。整个创建可以用下面的伪码描述:
apr_proc_create
{
if (attr->errchk)
对attr做有效性检查,让错误尽量发生在parent process中,而不是留给child process; ----(1)
fork子进程;
{ /* 在子进程中 */
清理一些不必要的从父进程继承下来的描述符等,为exec提供一个“干净的”环境;------(2)
关闭attr->parent_in、parent_out和parent_err,
并分别重定向attr->child_in、child_out和child_err为STDIN_FILENO、
STDOUT_FILENO和STDERR_FILENO; -----(3)
判断attr->cmdtype,选择执行exec函数; ------(4)
}
/* 在父进程中 */
关闭attr->child_in、child_out和child_err;
}
下面我们将上面的部分展开详细描述。
{
    int i;
    new->in = attr->parent_in;
    new->err = attr->parent_err;
    new->out = attr->parent_out;
除了调用fork简单的生成子进程之外,创建进程的一个重要的任务就是创建父进程和子进程之间的管道、重定向并确保父进程和子进程之间能够通过管道通信。
    if (attr->errchk) {
        if (attr->currdir) {
            if (access(attr->currdir, X_OK) == -1) {
                return errno;
            }
        }
 
        if (attr->cmdtype == APR_PROGRAM ||
            attr->cmdtype == APR_PROGRAM_ENV ||
            *progname == '/') {
            if (access(progname, R_OK|X_OK) == -1) {
                return errno;
            }
        }
        else {
            /* todo: search PATH for progname then try to access it */
        }
    }
是否需要对子进程进行安全性检查由父进程的errchk成员决定。通常情况下推荐进行检查,这样一旦子进程有问题的话,该错误将被扼杀在“襁褓”之中,而不错遗留到子进程中。可以通过函数apr_procattr_error_check_set设置该成员。检查包括:
1)、检查子进程是否具有对当前父进程路径的更改权限。因为在子进程中需要调用chdir函数,如果没有权限,自然不成功。
2)、如果子进程任务是普通的应用程序,并且使用的路径名称是绝对路径,那么必须确保它具有读取和修改权限,因此子进程中需要调用exec()函数,如果权限不具备,该调用将不成功。
错误预处理的目的只有一个,就是让错误发生在fork前,不要等到在子进程中出错。
    if ((new->pid = fork()) < 0) {
        return errno;
    }
函数真正的调用fork产生子进程,此时程序将兵分两组执行。我们首先来看父进程中的工作:
6.2.1.1父进程中的处理
    /* Parent process */
    if (attr->child_in) {
        apr_file_close(attr->child_in);
    }
    if (attr->child_out) {
        apr_file_close(attr->child_out);
    }
    if (attr->child_err) {
        apr_file_close(attr->child_err);
    }
父进程在创建apr_procattr_t结构的时候创建了in、out和err三个管道共计六个描述符:parent_in、parent_out、parent_err、child_in、child_out和child_err,但是正如我们前面所言,对于父进程而言,与子进程通信仅仅需要parent_in、parent_out、parent_err三个,另外三个child_XXX则可以关闭。
6.2.1.2子进程中的处理
子进程中所作的工作与父进程类似:
/* Part 1 */
        if (attr->child_in) {
            apr_pool_cleanup_kill(apr_file_pool_get(attr->child_in),
                                  attr->child_in, apr_unix_file_cleanup);
        }
        if (attr->child_out) {
            apr_pool_cleanup_kill(apr_file_pool_get(attr->child_out),
                                  attr->child_out, apr_unix_file_cleanup);
        }
        if (attr->child_err) {
            apr_pool_cleanup_kill(apr_file_pool_get(attr->child_err),
                                  attr->child_err, apr_unix_file_cleanup);
        }
/*Part 2 */
        apr_pool_cleanup_for_exec();
/*Part 3 */
        if (attr->child_in) {
            apr_file_close(attr->parent_in);
            dup2(attr->child_in->filedes, STDIN_FILENO);
            apr_file_close(attr->child_in);
        }
        if (attr->child_out) {
            apr_file_close(attr->parent_out);
            dup2(attr->child_out->filedes, STDOUT_FILENO);
            apr_file_close(attr->child_out);
        }
        if (attr->child_err) {
            apr_file_close(attr->parent_err);
            dup2(attr->child_err->filedes, STDERR_FILENO);
            apr_file_close(attr->child_err);
        }
上面的代码可以分为三部分:
子进程清理
由于子进程中的大部分属性都是从父进程进程而来,这些属性中并不是全部有用,为此子进程首先清除从父进程中进程的与自己无关的垃圾信息,从而为exec提供一个干净的环境。清理工作由函数apr_pool_cleanup_for_exec实现。我们来看一下函数内到底对子进程进行了哪些清理:
APR_DECLARE(void) apr_pool_cleanup_for_exec(void)
{
    cleanup_pool_for_exec(global_pool);
}
static void cleanup_pool_for_exec(apr_pool_t *p)
{
    run_child_cleanups(&p->cleanups);
    for (p = p->child; p; p = p->sibling)
        cleanup_pool_for_exec(p);
}
从上面的代码中可以看出,清理的过程实际上是一个递归的过程。它从内存池根结点开始,逐一遍历内存池中的每一个结点,并调用结点内部对应的cleanup_t链表中的各个cleaup_t函数,对于管道而言,cleanup_t函数的注册是在使用apr_file_pipe_create函数的时候注册的:
apr_pool_cleanup_register((*in)->pool, (void *)(*in),
apr_unix_file_cleanup,apr_pool_cleanup_null);
apr_pool_cleanup_register((*out)->pool, (void *)(*out),
apr_unix_file_cleanup,apr_pool_cleanup_null);
因此,cleanup_pool_for_exec函数对于每一个内存池结点调用的实际上就是apr_unix_file_cleanup和apr_pool_cleanup_null函数。在apr_unix_file_cleanup中,对于普通文件描述符,如果该文件描述符进行了缓冲,那么首先要调用apr_file_flush进行缓冲刷新。由于管道是不使用缓冲的,因此缓冲的处理对管道不进行任何处理。事实上,对于管道描述符,清理操作所作的事情主要就是调用close关闭,如果文件的标志为APR_DELONCLOSE,意味着该文件在关闭后必须删除,那么同时调用unlink删除该文件。
        /*
         * If we do exec cleanup before the dup2() calls to set up pipes
         * on 0-2, we accidentally close the pipes used by programs like
         * mod_cgid.
         *
         * If we do exec cleanup after the dup2() calls, cleanup can accidentally
         * close our pipes which replaced any files which previously had
         * descriptors 0-2.
         *
         * The solution is to kill the cleanup for the pipes, then do
         * exec cleanup, then do the dup2() calls.
         */
建立子进程与父进程的通信管道
父进程在创建apr_procattr_t时就建立了若干个管道,fork后子进程继承了这些管道,因此子进程内部同时也具备了in、out和err三个管道共计六个描述符:parent_in、parent_out、parent_err、child_in、child_out和child_err,但是子进程仅仅需要child_in、child_out、child_err三个,另外三个parent_XXX则可以关闭,如下图的(1),(2)所示。整个子进程中的描述符变化如下图所示:
子进程中的管道描述符变化
关于子进程,另外的问题就是子进程所拥有的描述符。通常的进程都拥有三个最基本的描述符:标准输入描述符,标准输出描述符以及标准错误描述符,分别对应stdin,stdout和stderr三个标准设备。除此之外,APR中创建的进程还拥有child_XXX和parent_XXX六个描述符,共计九个描述符。当所有的parent_XXX描述符关闭后,子进程中还拥有六个描述符。
子进程中标准输入,标准输出以及标准错误三个描述符的存在,意味着子进程能够从标准输入接受数据,并向标准输出设备和标准错误设备输出数据。APR中并不希望子进程具有这种能力,它希望子进程所有的交互都来自父进程。如果需要从输入设备接受数据,也是父进程进程接受,然后通过管道传递给子进程;同样,如果子进程需要输出数据到屏幕,也必须首先将数据通过管道传递给父进程,然后由父进程输出。这样带来的好处就是避免了子进程的中可能遇到的错误,而由父进程统一管理。比如最简单的,如果子进程需要接受命令行,那么每个子进程必须对命令行进行预处理,这样无疑使得子进程变得臃肿和复杂。为此APR中对子进程中的STDIN_FILENO,STDOUT_FILENO和STDERR_FILENO使用管道描述符进行了重定向:
            dup2(attr->child_in->filedes, STDIN_FILENO);
            apr_file_close(attr->child_in);
上面的代码将child_in重定向到STDIN_FILENO,这样,由于STDIN_FILENO被覆盖,子进程所有的数据只能来自父进程;与此类似,子进程所有的数据只能输出到父进程,而不能输出到其余的输出设备。这样,即使在子进程中调用scanf或者printf,实际上也并不来自stdin和stdout,而是来自父进程。重定向后的父子进程的描述符和管道的关系如下:
启动程序前的准备工作
在执行应用程序之前,子进程进行一些启动相关的准备工作,包括:
1)、包括切换执行目录。子进程的工作目录必须与父进程相同。
2)、切换用户组Id和用户Id。Apache中子进程通常是实际的与客户进行通信的实体,为了防止可能潜在的黑客攻击,APR中希望子进程在正常运行的情况下,执行权限保持尽可能的低,这样即使黑客控制了子进程也对系统不会产生太大的影响。这种设置通常只有父进程使用root权限创建子进程的时候才需要设置。如果父进程本身的权限比较低,那么子进程继承的权限自然也很低,此时就不需要调整。
3)、设置进程极限值,包括CPU的极限,子进程使用内存的极限,启动的子进程的数目以及打开的文件描述符的数目。设置通过专门的limit_proc过程实现。函数内部无非调用的是setrlimit函数,比如:
setrlimit(RLIMIT_CPU, attr->limit_cpu);
setrlimit(RLIMIT_NPROC, attr->limit_nproc);
④ 启动应用程序
尽管子进程被fork后它就被处于活动状态,但是它到目前为止还没有获得实际的执行任务。Unix中通常通过exec系列函数来启动一个新的应用程序。
对于子进程最后的任务就是执行实际的任务。如果启动应用程序,由需要启动的程序类型即cmd_type决定。cmd_type的值以及含义如下表所示:
cmd_type类型
含义
是否需要指定程序路径
是否使用自定义环境变量
APR_PROGRAM
启动的是普通的应用程序
APR_PROGRAM_ENV
启动的是普通的应用程序
APR_PROGRAM_PATH
启动的是普通的应用程序
APR_SHELLCMD
启动的是Shell应用程序
APR_SHELLCMD_ENV
启动的是Shell应用程序
对于每一种类型,函数处理如下:
1)、普通的应用程序(cmdtype=APR_PROGRAM)
if (attr->cmdtype == APR_PROGRAM) {
     if (attr->detached) {
          apr_proc_detach(APR_PROC_DETACH_DAEMONIZE);
    }
     execve(progname, (char * const *)args, (char * const *)env);
}
APR_PROGRAM类型对应的是普通的应用程序,但是它并不使用全局的环境变量environ,而是使用自定义的环境变量数组。因此函数必须调用execve。由于APR中不支持参数列表,为此execle不再考虑。另外如果子进程需要与父进程脱离开成后后台进程,那么还需调用apr_proc_detach进行脱离操作。
2)、普通应用程序(cmdtype=APR_PROGRAM_ENV)
if (attr->cmdtype == APR_PROGRAM_ENV) {
     if (attr->detached) {
          apr_proc_detach(APR_PROC_DETACH_DAEMONIZE);
     }
     execv(progname, (char * const *)args);
}
对于APR_PROGRAM_ENV,它与APR_PROGRAM的唯一的区别就是它使用默认的全局环境变量,因此不需要在函数参数中明确传递环境变量数组。这可以由execv函数实现。
3)、普通应用程序(cmdtype=APR_PROGRAM_PATH)
if(attr->cmdtype == APR_PROGRAM_PATH)
{
     if (attr->detached) {
          apr_proc_detach(APR_PROC_DETACH_DAEMONIZE);
     }
     execvp(progname, (char * const *)args);
}
如果程序的类型为APR_PROGRAM_PATH,那么意味着这是一个普通的应用程序,但是与APR_PROGRAM和APR_PROGRAM_ENV不同,它允许仅仅指定应用程序的名称,而不需要指明完整的绝对路径,具体的路径则由操作系统在PATH目录下查找。这种情况使用execvp正好合适。
4)、普通Shell程序
    if (attr->cmdtype == APR_SHELLCMD ||
       attr->cmdtype == APR_SHELLCMD_ENV) {
            int onearg_len = 0;
            const char *newargs[4];
 
            newargs[0] = SHELL_PATH;
            newargs[1] = "-c";
            i = 0;
            while (args[i]) {
                onearg_len += strlen(args[i]);
                onearg_len++; /* for space delimiter */
                i++;
            }
            switch(i) {
            case 0:
                break;
            case 1:
                newargs[2] = args[0];
                break;
            default:
            {
                char *ch, *onearg;
                ch = onearg = apr_palloc(pool, onearg_len);
                i = 0;
                while (args[i]) {
                    size_t len = strlen(args[i]);
 
                    memcpy(ch, args[i], len);
                    ch += len;
                    *ch = ' ';
                    ++ch;
                    ++i;
                }
                --ch; /* back up to trailing blank */
                *ch = '\0';
                newargs[2] = onearg;
            }
        }
        newargs[3] = NULL;
        if (attr->detached) {
            apr_proc_detach(APR_PROC_DETACH_DAEMONIZE);
        }
        if (attr->cmdtype == APR_SHELLCMD) {
            execve(SHELL_PATH, (char * const *) newargs, (char * const *)env);
        }
        else {
            execv(SHELL_PATH, (char * const *)newargs);
        }
    }
对于Shell程序而言,它也是一个普通的应用程序,无非需要程序的名称,程序提供的参数等等,因此与前面的类似,可以通过execve和execv执行,具体调用哪一个,则取决于程序的类型。APR_SHELLCMD类型意味着应用程序是Shell,但是使用自定义的环境变量数组;而APR_SHELLCMD_ENV则意味着使用默认的environ数组。
不过与普通的应用程序不同的是,对于shell程序仅仅给定程序的名称和路径还不够,还必须给出shell执行程序的路径名称,对于Unix系统,通常执行shell通常是位于“/bin/sh”,而Window下则通常是“cmd.exe”。因此如果shell应用的用法是”run –n  tingya”,则真正执行的时候应该变换为”/bin/sh run –n  tingya”,所以对于shell应用而言不能像APR_PROGRAM直接将传入的args参数传递给exec函数,而需要进行额外的处理。这就是为什么要把SHELL单独辟出来成为两个类型的应用。
参数数组的转换非常的简单,只是在原有的数组的前面插入两个新的字符串“/bin/sh –c”,这样传入的所有的命令xxx都变为“/bin/sh –c xxx”,-c选项的用途是通知shell处理程序把-c后面的字符串作为一个参数处理

    • 评论
    • 分享微博
    • 分享邮件
    邮件订阅

    如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。

    重磅专题
    往期文章
    最新文章