6.3.3.5工作子进程管理
子进程通常被视为工作者,其组成了HTTP服务器的核心。它们负责处理对客户端的请求的处理。尽管多任务体系结构并不负责对请求的处理,不过他仍然负责创建子进程、对其进行初始化并且将客户端请求转交给它们进行处理。子进程的所有的行为都被封状在函数child_main()中。
6.3.3.5.1子进程的创建
在深入到子进程工作的内部细节之前,我们有必要了解一下主服务进程是如何创建子进程的。事实上,从主服务进程的最后的代码中也可以看出,主服务进程是通过调用make_child函数来创建一个子进程的,该函数定义如下:
static int make_child(server_rec *s, int slot)
该函数具有两个参数,slot是当前进程在记分板中的索引。
u的代码主要用于处理单进程。如前所述,单进程主要用于调试。对于单进程服务器而言,唯一的进程就是主服务进程,因此不需要创建任何额外的子进程,需要处理的就是转换主服务进程的角色为子服务进程,这种转变包括两部分:
1)、处理信号。
6.3.3.5.2初始化、配置以及服务重启
主服务进程使用fork()创建子进程。每个子进程都具有独立的内存区域并且不允许读取其余子进程的内存。因此由主服务器一次处理配置文件要比由各个子进程各自处理明智的多。配置文件的相关信息可以保存在共享内存区域中,该内存区域可以被每一个子进程读取。由于不是每一个操作系统平台都支持共享内存的概念,因此主服务进程在创建子进程之前处理配置文件。子服务进程通常是父进程的克隆,因此它们与主服务进程具有相同的配置信息而且从来不会改变。
任何时候,如果管理员想更改服务器配置。他都必须让主服务进程重新读取配置信息。当前存在的子进程所拥有的则是旧的配置信息,因此它们都必须被替换为新产生的子进程。为了避免打断正在处理的HTTP请求,Apache提供了一种平稳启动的方法,该模式下允许子进程使用旧的配置信息进行处理直到其退出。
子进程的初始化可以在相应的MPM中找到,对于预创建Preforking MPM而言就是child_main()。该函数包括下面的几个步骤:
调用ap_init_child_modules()重新初始化模块:每一个模块都由主进程预先进行初始化。如果模块中分配系统资源或者决定于进程号,那么模块重新进行初始化就是必须的。
建立超时处理句柄:为了避免子进程的无线阻塞,Apache对客户端请求使用超时处理。其中使用警告,警告的概念与信号的概念非常类似,通常将报警时钟设置为给定的时间,而当报警响起的时候系统将离开请求的处理。
循环内还有两种初始化:
清除超时设置:重置报警定时器
清除透明内存池:在请求响应循环中每个内存的分配都涉及到透明内存池。在循环的开始,内存池必须进行清理。
将公告板中的status设置为ready。
ptrans的创建,以及访问公告板,同时由于多个进程之间可能存在竞争,因此另外一个准备工作就是创建进程间的接受互斥锁。由于通常情况下,父进程都是使用fork生成子进程,此时子进程基本是父进程的克隆。一般情况下,Apache的启动都是使用超级用户进行的,因此子进程实际上也就具有与父进程等同的操作权限,父进程能够访问的资源子进程都能够访问。
Apache通常会将子进程的用户设置为一个普通的用户,比如nobody或者WWWRun之类从而来降低子进程的执行权限,原则上,子进程用户的权力应该尽可能的小。Unixd_setup_child将用户的ID从正在运行父进程的用户改变为在配置文件中规定的用户,如果不能改变用户的ID,子进程就立即退出。另外相关的初始化工作必须在unixd_setup_child()调用之前进行,因为一旦子进程权限降低,一些只能超级用户进行的初始化可能无法正常进行。
但是子进程具有与父进程相同的权限具有一定的潜在的危险。由于网络连接通常由子进程直接处理,因此如果黑客通过某种权限控制了子进程,那么他就能够任意的控制系统的。因此通常在进行了资源准备工作之后,
Ap_run_child_init 调用 child_init 挂钩进行子进程本身的初始化。
当所有的准备工作结束以后,子进程可以与客户进行会话。
80,但是在Apache中,则允许服务器在多个端口上同时进行侦听,这些侦听端口用结构ap_listen_rec进行描述:
一般的情况下,服务器只会侦听固定的端口,比如
描述了绑定到该端口的套接字,而bind_addr则描述了套接字必需关联的地址。Accept_func是一个回调函数,当从该侦听端口上接受到客户端连接的时候,该函数将被执行从而来处理连接。Active用以描述当前端口是否处于活动状态。
sd
对于服务器端的多个侦听端口,Apache使用链表进行保存,因此next用以指向下一个侦听套接字结构。整个链表的用ap_listen_rec全局变量记录,因此沿着ap_listen_rec可以遍历所有的侦听套接字。与此同时,侦听端口的数目也保存在全局变量num_listensocks中。
上面的代码所作的事情无非就是生成指定的需要逐一遍历的文件结果集合。
任何时候,子进程如果要正常退出,其都必须由主进程通过“终止管道”通知,另一方面,子进程也将不停的检查终止管道。一旦发现需要退出,子进程将die_now设置为1,这时候实际上就自动退出循环。相反,如果子进程不需要退出,那么它所作的事情只有一个,就是使用poll对所有的端口进行轮询,直到某个端口准备完毕,则调用相关的连结处理函数进行处理。
SAFE_ACCEPT(accept_mutex_on());
虽然多个子进程同属于一个父进程,但是多个子进程之间则是相互并行的,当多个子进程同时扫描侦听端口的时候,很可能发生多个子进程同时竞争一个侦听端口的情况。因此所有的子进程有必要互斥的等待TCP请求。
接受互斥锁能够确保只有一个子进程独占的等待TCP请求(使用系统调用accept())——这些都是侦听者所做的事情。接受互斥锁是控制访问TCP/IP服务的一种手段。它的使用能够确保在任何时候只有一个进程在等待TCP/IP的连接请求。
不同的操作系统有不同的接受互斥锁(Accept Mutex)的实现。有一些操作系统对于每一个子进程需要一个特殊的初始化阶段。它的工作方式如下:
调用过程accept_mutex_on():申请互斥锁或者等待直到该互斥锁可用
调用过程accept_mutex_off():释放互斥锁
prefork MPM中通过SAFE_ACCEPT(accept_mutex_on())实现子进程对互斥锁的锁定;而SAFE_ACCEPT(accept_mutex_off())则是完成互斥锁的释放。
if (num_listensocks == 1) {
offset = 0;
}
对于整个侦听套接字数组而言,任何时候只有一个侦听端口能被处理。Offset实际上描述了当前正在被处理的侦听端口在数组中的索引。如果当前服务器的侦听端口只有一个,那么几乎没有任何事情要做,也就没有所谓的轮询。
如果服务器配置使用多个侦听端口,那么Apache就必须使用poll()来确定客户正在连接哪个端口,然后我们就可以知道哪个端口正在受到访问,这样才能在这个端口上调用接受函数。如果轮询返回的值是EBADF,EINTR或者EINVAL之类的错误,那么轮询并不应该被终止,但是如果返回的不是这些错误,那么子进程应该调用clean_child_exit退出。
评论