Apache中預創建Preforking
在預創建MPM中由于存在多個子進程偵聽指定的套接字,因此如果不加以控制可能會出現幾個子進程同時對一個連接進行處理的情況,這是不允許的。因此我們必須采取一定的措施確保在任何時候一個客戶端連接請求只能由一個子進程進程處理。為此Apache中引入了接受互斥鎖(Accept Mutex)的概念。接受互斥鎖是控制訪問TCP/IP服務的一種手段,它能夠確保在任何時候只有一個進程在等待TCP/IP的連接請求,從而對于指定的連接請求也只會有一個進程進行處理。為此MPM緊接著必須創建接受互斥鎖。 if (!is_graceful) { if (ap_run_pre_mpm(s->process->pool, SB_SHARED) != OK) { mpm_state = AP_MPMQ_STOPPING; return 1; } ap_scoreboard_image->global->running_generation = ap_my_generation; } 多數的MPM緊接著會立即創建記分板,并將它設置為共享,以便所有的子進程都可以使用它。記分板在啟動的時候被創建一次,直到服務器終止時才會被釋放。上面的代碼就是用于創建記分板,但是你可能很奇怪,因為你看不到我們描述的記分板創建函數ap_create_scoreboard()。事實上,創建過程由掛鉤pre_mpm完成,通過使用pre_mpm,服務器就可以讓其他的模塊在分配記分板之前訪問服務器或者在建立子進程之前訪問記分板。 ap_run_pre_mpm()運行掛鉤 pre_mpm,該掛鉤通常對應類似ap_hook_name之類的函數,對于pre_mpm掛鉤,對應的函數則是ap_hook_pre_mpm。在 core.c中的ap_hook_pre_mpm(ap_create_scoreboard, NULL, NULL, APR_HOOK_MIDDLE)設定掛鉤pre_mpm的對應處理函數則正是記分板創建函數ap_create_scoreboard。 ap_run_pre_mpm掛鉤也只有在進行重新啟動的時候才會調用,而在進行平穩啟動的時候,并不調用這個掛鉤,這樣做會丟失所有的仍然正在為長期請求提供服務的子進程的信息。掛鉤的引入是 Apache2.0版本的一個新的實現機制,也是理解Apache核心的一個重要機制之一,關于掛鉤的具體的實現細節我們在后面的部分會詳細分析。對于每次冷啟動,Apache啟動之后,內部的記分板的家族號都是從0開始,而每一次平穩啟動后家族號則是在原先的家族號加一。 set_signals(); 當分配了記分板之后,MPM就應該設置信號處理器,一方面允許外部進程通過信號通知其停止或者重新啟動,另一方面服務器應該忽略盡可能多的信號,以確保它不會被偶然的信號所中斷。正常情況下,父進程需要處理三種信號:正常的重新啟動、非正常的重新啟動以及關閉的信號。 SIGTERM:該信號用于關閉主服務進程。信號處理函數sig_term中設置shutdown_pending=true; SIGHUP:該信號用于重啟服務器,信號處理函數中設置restart_pending=true和graceful_mode=false SIGUSR1:該信號用于平穩啟動服務器,信號處理函restart數中設置restart_pending=true和graceful_mode=true 至于信號SIGXCPU、SIGXFSZ則由默認的信號處理函數SIG_DFL處理,SIGPIPE則會被忽略。在Apache主程序的循環中,程序不斷的檢測 shutdown_pending,restart_pending和graceful_mode三個變量的值。通常并不是外部程序一發送信號,Apache就立即退出。最差的情況就是信號是在剛檢測完就發送了,這樣,主程序需要將該次循環執行完畢后才能發現發送的信號。 if (one_process) { AP_MONCONTROL(1); make_child(ap_server_conf, 0); } 至此大部分準備工作已經完成,剩下的任務就是創建進程。進程的創建包括兩種模式:單進程模式和多進程模式。單進程模式通常用于Apache調試,由于不管多進程還是單進程,對HTTP請求處理以及模塊等的使用都是完全相同的,區別僅僅在于效率。而多線程的調試要比單進程復雜的多。如果是單進程調試模式,那么上面的兩句程序將被程序。我們首先解釋一下AP_MONCONTROL宏的含義。對于調試,一方面可能比較關心執行的正確與否,內存是否溢出等等,另外一方面就是能夠找出整個服務器的運行瓶頸,只有找到了運行的瓶頸才能進行改善,從而提高效率。例如,假設應用程序花了 50% 的時間在字符串處理函數上,如果可以對這些函數進行優化,提高 10% 的效率,那么應用程序的總體執行時間就會改進 5%。因此,如果希望能夠有效地對程序進行優化,那么精確地了解時間在應用程序中是如何花費的,以及真實的輸入數據,這一點非常重要。這種行為就稱為代碼剖析(code profiling)。 An executable program compiled using the -pg option tocc(1)automatically cally includes calls to collect statistics for thegprof(1)call-graph execution profiler. In typical operation, profiling begins at program startup and ends when the program calls exit. When the program exits, the profiling data are written to the file gmon.out, thengprof(1)can be used to examine the results. 一個可執行的應用程序可以在使用gcc編譯的時候利用-pg選項自動的調用相關函數收集一些執行統計信息以便gprof execution profiler使用。 moncontrol() selectively controls profiling within a program. When the program starts, profiling begins. To stop the collection of histogram ticks and call counts use moncontrol(0); to resume the collection of his-histogram togram ticks and call counts use moncontrol(1). This feature allows the cost of particular operations to be measured. Note that an output file will be produced on program exit regardless of the state of moncontrol(). Programs that are not loaded with -pg may selectively collect profiling statistics by calling monstartup() with the range of addresses to be pro-profiled. filed. lowpc and highpc specify the address range that is to be sampled; the lowest address sampled is that of lowpc and the highest is just below highpc. Only functions in that range that have been compiled with the -pg option tocc(1)will appear in the call graph part of the output; however, all functions in that address range will have their execution time measured. Profiling begins on return from monstartup(). 單進程的另外一個任務就是調用make_child。對于單進程,make_child非常的簡單: static int make_child(server_rec *s, int slot) { int pid; …… if (one_process) { apr_signal(SIGHUP, sig_term); apr_signal(SIGINT, sig_term); apr_signal(SIGQUIT, SIG_DFL); apr_signal(SIGTERM, sig_term); child_main(slot); return 0; } /*多進程處理代碼*/ } 從代碼中可以看出,單進程直接調用了child_main,該函數用于直接處理與客戶端的請求。在整個系統中只有一個主服務進程存在。 else { if (ap_daemons_max_free < ap_daemons_min_free + 1) /* Don't thrash... */ ap_daemons_max_free = ap_daemons_min_free + 1; remaining_children_to_start = ap_daemons_to_start; if (remaining_children_to_start > ap_daemons_limit) { remaining_children_to_start = ap_daemons_limit; } if (!is_graceful) { startup_children(remaining_children_to_start); remaining_children_to_start = 0; } else { hold_off_on_exponential_spawning = 10; } 對于多進程模式而言,處理要復雜的多。與多進程類似,上面的代碼負責創建子進程。在創建之前對其中使用到的幾個核心變量進行必要的調整,這幾個變量的含義分別如下: ap_daemons_max_free:服務器中允許存在的空閑進程的最大數目,一旦超過這個數目,一部分空閑進程就會被迫終止,直到最后的空閑進程數目降低到該值。 ap_daemons_min_free:服務器中允許存在的空閑進程的最小數目,一旦低于這個數目,服務器將創建新的進程直到最后空閑進程數目抵達這個數目。任何時候空閑進程的數目都維持在ap_daemons_max_free和ap_daemons_min_free之間。 ap_daemons_limit:服務器中允許存在的進程的最大數目。包括空閑進程、忙碌進程以及當前的記分板中的空余插槽。 ap_daemons_to_start:服務器起始創建的進程數目。這個值不能超出ap_daemons_limit。 remaining_child_to_start:需要啟動的子進程的數目。對于初始啟動,remaining_child_to_start的值就是ap_daemons_to_start的值。因此服務器是剛啟動,那么函數直接調用start_children創建remaining_child_to_start個子進程,同時將 remaing_child_to_start設置為零。對于平穩啟動,remaining_child_to_start的含義則要發生一些變化。 如果我們所進行的是平穩啟動,那么在我們進入下面的主循環之前應該可以觀察到相當多的子進程立即陸續退出,其中的原因則是因為我們向它們發出了AP_SIG_GRACEFUL信號。這一切發生的非常的快。對于每一個退出的子進程,我們都將啟動一個新的進程來替換它直到進程數目達到ap_daemons_min_free。因此 restart_pending = shutdown_pending = 0; mpm_state = AP_MPMQ_RUNNING; while (!restart_pending && !shutdown_pending) { int child_slot; apr_exit_why_e exitwhy; int status, processed_status; apr_proc_t pid; 至此,主服務進程則可以進入循環,它所作的事情只有兩件事情,一個是負責對服務器重新啟動或者關閉,另一個就是負責監控子進程的數目,或者關閉多余的空閑子進程或者在空閑子進程不夠的時候啟動新的子進程。restart_pending用于指示服務器是否需要進行重新啟動,為1的話表明需要重啟;shutdown_pending則指示是否需要關閉服務器,為1則表明需要關閉。除此之外,graceful用于指示是否進行平穩啟動。當外界需要對主進程進行控制的時候只需要設置相應的變量的值即可,而主進程中則根據這些變量進行相應的處理。 ap_wait_or_timeout(&exitwhy, &status, &pid, pconf); 如果restart_pending=0并且shutdown_pending=0的話意味著外部進程不需要服務器終止或者重新啟動,此時主服務進程將進入無限循環,監視子進程。對于平穩啟動而言,正常情況下,在每一輪循環中,主服務進程都會調用 ap_wait_or_timeout()等待子進程終止。通常情況下,子進程的退出有三種可能,分別枚舉類型apr_exit_why_e進行描述 1.正常退出,此時APR_PROC_EXIT=1,這種情況通常是進程所有任務完成后退出 2.信號退出,此時APR_PROC_SIGNAL=2,這種情況通常是進程在執行過程中接受到信號半途退出 3.非正常退出,此時APR_PROC_SIGNAL_CORE=4,通常是進程意外中斷,同時生成core dump文件。 if (pid.pid != -1) { processed_status = ap_process_child_status(&pid, exitwhy, status); if (processed_status == APEXIT_CHILDFATAL) { mpm_state = AP_MPMQ_STOPPING;uvwxy return 1; } 主進程通過ap_wait_or_timeout監視等待每一個子進程退出,同時在exitwhy中保存它們退出的原因。盡管如此,主進程并不會無限制的等待下去。主進程會給出一個等待的超時時間,一旦超時,主進程將會不再理會那些尚未結束的進程,繼續執行主循環的剩余部分。如果子進程在規定的時間內完成,那么即等待成功,此時該被終止的子進程的pid.pid將不為-1,否則pid.pid將為-1。一旦等待到子進程退出,那么進程退出的原因保存在processed_status中。如果processed_satus為APEXIT_CHILDFATAL,則表明發生了致命性的錯誤,這時候將導致整個服務器的崩潰,此時主進程直接退出,不對記分板做任何的處理,如u所示; child_slot = find_child_by_pid(&pid); if (child_slot >= 0) { (void) ap_update_child_status_from_indexes(child_slot, 0, SERVER_DEAD, (request_rec *) NULL);u if (processed_status == APEXIT_CHILDSICK) { idle_spawn_rate = 1;v } else if (remaining_children_to_start && child_slot < ap_daemons_limit) { make_child(ap_server_conf, child_slot); --remaining_children_to_start; } #if APR_HAS_OTHER_CHILD } 如果子進程發生的錯誤并不是致命性的,那么一切都得按部就班的處理——更新記分板中對應的插槽中的信息。首要的前提就是在記分板中找到該進程對應的插槽,這由函數find_child_by_pid()完成。如果能夠成功找到終止進程對應的插槽,那么直接在記分板中將該終止進程的狀態更新為SERVER_DEAD,這樣,該插槽將會再次可用,如u所示。如果進程退出是因為資源受限,比如磁盤空間不夠,內存空間不夠等等,此時Apache必須降低生成子進程的速度至最低。如v所示。如果Apache進行的是平穩重啟,那么在進入主循環之前,通過發送終止信號,很多的子進程都將被終止,這些被終止的進程在系統重啟后必須被新的進程替換,直到總的進程數目達到daemons_min_free。 remaining_children_to_start記錄了當前需要重啟的子進程的數目。 else if (apr_proc_other_child_alert(&pid, APR_OC_REASON_DEATH, status) == APR_SUCCESS) { #endif } else if (is_graceful) { ap_log_error(APLOG_MARK, APLOG_WARNING, 0, ap_server_conf, "long lost child came home! (pid %ld)", (long)pid.pid); } continue; } 如果進程在公告板中沒有找到相應記錄,此時檢查子進程是否是“其余子進程”(reap_other_child)。一些情況下,主服務進程會創建一些進程,這些進程并不是用來接受并處理客戶端連接的,而是用作其余用途,通常稱之為“其余進程”,并用一個單獨的列表進行登記。比如,一般情況下,Apache會將日志寫入到文件中,但是有的時候Apache則希望將數據寫入到一個給定的應用程序中。因此主服務器進程必須為該應用程序創建該進程,并且將該進程的標準輸入STDIN關聯道主服務進程的日志流中。這種進程并不是用來執行處理HTTP請求的子服務進程,因此稱之為 “其余進程”。任何時候只要服務器重啟,日志應用進程都會接受到SIGHU和SIGUSR1信號,然后終止退出,對應的模塊必須重新創建這種進程。對于其余進程,主服務進程不做任何事情,因為這不是主進程所管轄的范圍。如果既不是“其余子進程”,又沒有在公告板中找到相應記錄,同時管理員還設置了熱啟動選項,那么發生這種情況只有一個可能性:管理員減少了允許的子進程的數目同時強制執行了熱啟動。而一個正在忙碌的子進程擁有的入口記錄號比允許的值大。此時它終止的時候自然就不可能在公告板中找到相應的記錄入口。 else if (remaining_children_to_start) { startup_children(remaining_children_to_start); remaining_children_to_start = 0; continue; } 如果當所有的終止的子進程都被替換之后,remaining_children_to_start還不為零,此時意味著主服務進程必須創建更多的子進程,這個可以通過函數startup_children()實現。 perform_idle_server_maintenance(pconf); #ifdef TPF shutdown_pending = os_check_server(tpf_server_name); ap_check_signals(); sleep(1); #endif /*TPF */ } } /* one_process */ 一旦啟動完畢,那么主進程將使用perform_idle_server_maintenance進入空閑子進程維護階段,同時主進程還得監視相關的信號,比如關閉信號,重啟信號等等。空閑進程的維護在4.2.1.2中詳細描述。當主進程退出循環while (!restart_pending && !shutdown_pending) 的時候只有兩種情況發生,或者被通知關閉,或者被通知重啟。一旦如此,Apache將著手進行相關的清除工作。 mpm_state = AP_MPMQ_STOPPING; if (shutdown_pending) { if (unixd_killpg(getpgrp(), SIGTERM) < 0) { ap_log_error(APLOG_MARK, APLOG_WARNING, errno, ap_server_conf, "killpg SIGTERM"); } ap_reclaim_child_processes(1); /* Start with SIGTERM */ /* cleanup pid file on normal shutdown */ { const char *pidfile = NULL; pidfile = ap_server_root_relative (pconf, ap_pid_fname); if ( pidfile != NULL && unlink(pidfile) == 0) ap_log_error(APLOG_MARK, APLOG_INFO, 0, ap_server_conf, "removed PID file %s (pid=%ld)", pidfile, (long)getpid()); } ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, "caught SIGTERM, shutting down"); return 1; } 如果Apache需要進行關閉,那么Apache的清除工作工作包括下面的幾個方面: ■ 如果服務器被要求關閉,那么主服務進程將向整個進程組中的所有子進程發送終止信號,通知其調用child_exit正常退出。 ■ 調用ap_reclain_child_process回收相關的子進程。 ■ 清除父子進程之間通信的“終止管道”。 apr_signal(SIGHUP, SIG_IGN); if (one_process) { /* not worth thinking about */ return 1; } ++ap_my_generation; ap_scoreboard_image->global->running_generation = ap_my_generation; if (is_graceful) { ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, "Graceful restart requested, doing restart"); /* kill off the idle ones */ ap_mpm_pod_killpg(pod, ap_max_daemons_limit); /* This is mostly for debugging... so that we know what is still * gracefully dealing with existing request. This will break * in a very nasty way if we ever have the scoreboard totally * file-based (no shared memory) */ for (index = 0; index < ap_daemons_limit; ++index) { if (ap_scoreboard_image->servers[index][0].status != SERVER_DEAD) { ap_scoreboard_image->servers[index][0].status = SERVER_GRACEFUL; } } } 如果Apache被要求的是重新啟動,那么對于平穩啟動和非平穩啟動處理則不太相同。對于平穩啟動而言,主進程需要終止的僅僅是那些目前空閑的子進程,而忙碌的進程則不進行任何處理。空閑進程終止通過 ap_mpm_pod_killpg實現;同時由于記分板并不銷毀,因此對于那些終止的進程,必須更新其在記分板中的狀態信息為SERVER_DEAD;而對于那些仍然活動的子進程,則將其狀態更新為SERVER_GRACEFUL。 else { /* Kill 'em off */ if (unixd_killpg(getpgrp(), SIGHUP) < 0) { ap_log_error(APLOG_MARK, APLOG_WARNING, errno, ap_server_conf, "killpg SIGHUP"); } ap_reclaim_child_processes(0); /* Not when just starting up */ ap_log_error(APLOG_MARK, APLOG_NOTICE, 0, ap_server_conf, "SIGHUP received. Attempting to restart"); } 如果服務器被要求強制重啟,那么所有的子進程包括那些仍然在處理請求的都將被統統終止。
關鍵字:Apache 創建 Preforking
新文章:
- CentOS7下圖形配置網絡的方法
- CentOS 7如何添加刪除用戶
- 如何解決centos7雙系統后丟失windows啟動項
- CentOS單網卡如何批量添加不同IP段
- CentOS下iconv命令的介紹
- Centos7 SSH密鑰登陸及密碼密鑰雙重驗證詳解
- CentOS 7.1添加刪除用戶的方法
- CentOS查找/掃描局域網打印機IP講解
- CentOS7使用hostapd實現無AP模式的詳解
- su命令不能切換root的解決方法
- 解決VMware下CentOS7網絡重啟出錯
- 解決Centos7雙系統后丟失windows啟動項
- CentOS下如何避免文件覆蓋
- CentOS7和CentOS6系統有什么不同呢
- Centos 6.6默認iptable規則詳解