分享
 
 
 

Apache中的进程剖析(3)

王朝system·作者佚名  2006-05-27
窄屏简体版  字體: |||超大  

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/sh run –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与之联系!

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有