`
liuzhaomin
  • 浏览: 207687 次
  • 性别: Icon_minigender_1
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

Apache中的进程剖析(3)

阅读更多

//转载请注明来源:http://blog.csdn.net/tingya
//版权声明:
//本书是《Apache源代码全景分析》的草稿部分,
//读者可以自由浏览和打印
//未经本文允许,不得以任何形式出现在盈利印刷品中,否则将追究法律责任!!!

6.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/shrun –n tingya”,所以对于shell应用而言不能像APR_PROGRAM直接将传入的args参数传递给exec函数,而需要进行额外的处理。这就是为什么要把SHELL单独辟出来成为两个类型的应用。
参数数组的转换非常的简单,只是在原有的数组的前面插入两个新的字符串“/bin/sh –c”,这样传入的所有的命令xxx都变为“/bin/sh –c xxx”,-c选项的用途是通知shell处理程序把-c后面的字符串作为一个参数处理

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

如果你觉得本文不错,请点击文后的“推荐本文”链接!!

分享到:
评论

相关推荐

    重启apache进程脚本

    这个脚本的主要功能是检测当前系统中 Apache 进程的数量,如果数量超过 60 个,则重启 Apache 服务器,以释放系统资源。同时,如果系统中可用内存小于 20%,则清理系统缓存,释放更多的内存资源。 下面我们来详细...

    Apache Server源代码分析(PDF)

    7. **错误处理和异常恢复**:Apache如何处理错误,如404找不到文件或500内部服务器错误,以及如何优雅地恢复异常状态,都是源代码分析中的重要部分。 8. **并发和线程模型**:Apache的多路复用模型允许高效地处理...

    Apache工作机制分析

    - **多处理模块(MPM)**:决定Apache如何处理客户端请求,包括使用进程或线程模型来处理并发连接。 - **可移植运行库(APR)**:提供一个平台独立的接口,使得Apache能够在多种操作系统上运行。 #### Apache+...

    Apache日志分析手册

    6. 查看进程和端口连接:手册中提到了使用ps和netstat命令来查看Apache进程和80端口的TCP连接数。这些命令有助于了解Apache服务的运行状态。 7. 分析日志中的异常行为:比如,通过过滤日志来查看特定IP地址在特定...

    Apache2中文使用手册

    3. **启动与管理Apache**:在系统服务中启动和停止Apache,例如在Linux上使用`systemctl start apache2`和`systemctl stop apache2`命令。还可以使用`apachectl`或`httpd`命令进行更复杂的控制,如重启、重载配置等...

    Apache源代码全景分析

    - **进程间通信**:在多进程模型中,Apache使用管道、信号和共享内存等机制实现进程间的通信与协调。 - **请求处理流程**:Apache接收到请求后,会经历接收、解析、路由、执行模块、响应等步骤,最后返回结果给...

    apache架构设计原理剖析

    1. 连接管理(监听):Apache监听指定端口的连接请求,一旦有新的连接到来,它会创建一个新的进程或线程来处理该连接。 2. 请求读取:Apache接收并解析HTTP请求,包括请求行、头和数据。 3. 找到表示形式:根据请求...

    apache中文帮助文档

    3. **日志管理**:理解如何配置访问日志和错误日志,分析日志数据以优化性能和排查问题。 4. **安全设置**:学习如何使用SSL/TLS进行加密通信,设置HTTPS,以及通过基本认证、摘要认证或证书认证保护网站。 5. **...

    apache源代码解析--基于Apache0.6.5

    在 Apache 0.6.5 版本中,守护进程的创建和管理是非常重要的。 知识点4: Apache 生命周期 Apache 生命周期是指 Apache 服务器从启动到关闭的整个过程。在这个过程中,Apache 服务器会经历多个阶段,包括初始化、...

    Apache Server源代码分析

    3. **配置解析器(Config File Parser)**:解析`httpd.conf`等配置文件,将配置指令转化为内存中的结构,供服务器在运行时使用。配置解析器的核心是`ap_config.h`和`config.c`等文件。 4. **请求处理(Request ...

    Apache官方中文文档教程

    6. **性能优化**:Apache有多种性能优化策略,如使用MPM(多进程模块)如prefork或worker,设置合适的MaxKeepAliveRequests和KeepAliveTimeout,以及开启缓存模块mod_cache等。 7. **HTTPS与SSL/TLS**:在当前网络...

    apache中文手册

    4. **性能优化**:Apache手册也会包含性能调优技巧,例如如何配置预加载进程(Prefork或Event MPM)、调整KeepAlive设置、使用缓存模块提高响应速度等。 5. **日志和错误处理**:了解如何解析和分析Apache的日志...

    Apache中文手册20

    5. **日志分析**:如何解读Apache的日志文件,以及如何使用工具进行日志分析,以监控服务器状态、发现潜在问题。 6. **多线程与事件模型**:Apache有 prefork 和 worker 两种多进程/多线程模型,手册会介绍它们的优...

    Apache中文手册

    3. **虚拟主机**:Apache支持在同一台服务器上运行多个网站,通过虚拟主机功能实现。这可以基于IP地址、端口号或者域名来区分不同的站点。 4. **安全与性能优化**:Apache提供SSL/TLS支持以实现HTTPS安全连接。此外...

    APACHE 2.2 中文参考手册

    2. **多线程支持**:通过MPM(Multi-Processing Module)机制,Apache 2.2 支持多线程和多进程模型,提高了并发处理能力。 3. **更好的性能**:相比之前的版本,Apache 2.2 在性能上有所提升,优化了内存使用,降低...

    apache 配置中文详解

    虚拟主机设置部分定义了 Apache 服务器进程中的不同 IP 地址和主机名。在这个部分,我们可以设置虚拟主机的各种参数,如 ServerName、ServerAdmin 和 DocumentRoot 等。 配置文件命名和路径 配置文件的命名和路径...

    Apache2.22源代码

    在提供的压缩包文件中,`httpd-2.2.22.tar.gz`是Apache2.22的源代码,通过解压这个文件,我们可以看到Apache的整个源代码结构,包括配置文件、核心代码、模块源代码、文档等。开发者可以通过阅读这些源代码来理解...

    apache中文文档

    2. **核心功能**:介绍Apache的核心特性,如多进程模型(MPM)、请求处理机制、模块加载等。 3. **安全设置**:讲解如何配置SSL/TLS以确保网站通信的安全,以及如何设置防火墙规则和权限控制来防止未授权访问。 4....

    apache启动不了的解决办法

    Apache 启动不了的解决办法 Apache 服务器是一种流行的 Web 服务器软件,然而,在实际应用中,Apache 服务器的启动问题是非常常见的。...Apache 服务器无法启动的解决办法有多种,需要根据具体情况进行分析和解决。

    APACHE COOKBOOK中文版

    9. **日志管理**:详细介绍如何配置和分析Apache日志,这有助于监控服务器健康状况和网站流量。 10. **平台特定的配置**:可能包含针对不同操作系统平台,如Linux、Windows的特定配置指导。 由于本书是由两位经验...

Global site tag (gtag.js) - Google Analytics