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的值以及含义如下表所示:
关于作者 张中庆,目前主要的研究方向是嵌入式浏览器,移动中间件以及大规模服务器设计。目前正在进行Apache的源代码分析,计划出版《Apache源代码全景分析》上下册。Apache系列文章为本书的草案部分,对Apache感兴趣的朋友可以通过flydish1234 at sina.com.cn与之联系!