学习《apache源代码全景分析》之多任务并发处理摘录
1.如果要寫服務(wù)器程序,按照正常的思路,通常主程序在進(jìn)行了必要的準(zhǔn)備工作后會調(diào)用諸如fork之類的函數(shù)產(chǎn)生一個新的進(jìn)程或線程,然后由子進(jìn)程進(jìn)行并發(fā)處理。每個進(jìn)程偵聽某個端口,然后接受網(wǎng)絡(luò)連接,并處理這些了連接上的請求數(shù)據(jù)。
2.當(dāng)主程序調(diào)用了函數(shù)ap_mpm_run之后,整個主程序就算結(jié)束了。然后進(jìn)入多進(jìn)程并發(fā)處理狀態(tài),為了并發(fā)處理客戶端請求,Apache會產(chǎn)生多個進(jìn)程,每個進(jìn)程又產(chǎn)生一定數(shù)目的線程,等等。
3.MPM中所使用到的公共數(shù)據(jù)結(jié)構(gòu),主要包括兩種:記分板(ScoreBoard)和父子進(jìn)程的終止通信管道。記分板類似于共享內(nèi)存,主要用于父子進(jìn)程之間的數(shù)據(jù)交換。任何一方都可以將對方需要的信息寫入到記分板上,同時任何一方也可以到記分板上獲取需要的數(shù)據(jù)。記分板通常用于主進(jìn)程對子進(jìn)程方向的通信,子進(jìn)程再記分板中寫入自己的狀態(tài),主進(jìn)程則通過讀取記分板從而了解子進(jìn)程的狀態(tài)。
? ?終止通信管道則用于主進(jìn)程通知子進(jìn)程終止運(yùn)行,它也是單方向的。
4.MPM的功能定位在Apache的主循環(huán)中,服務(wù)器的主程序主要完成所有的初始化及配置處理。這些都是在調(diào)用函數(shù)ap_mpm_run()之前完成。一旦調(diào)用了ap_mpm_run(),函數(shù)的指揮權(quán)從主程序切換到MPM中了。
? MPM的主要任務(wù)就是創(chuàng)建進(jìn)程或線程并對它們進(jìn)行管理。另外一個職責(zé)就是在套接字上進(jìn)行偵聽客戶端請求。
5.prefork MPM示意圖
? ?
6.父進(jìn)程、子進(jìn)程及記分板之間關(guān)系
? ?
?記分板的數(shù)據(jù)結(jié)構(gòu)如下:
typedef struct {global_score *global; //描述全局信息的結(jié)構(gòu)process_score *parent; //進(jìn)程間相互通信結(jié)構(gòu)worker_score **servers; //記錄每個線程的運(yùn)行信息lb_score *balancers; } scoreboard;--------- typedef struct {int server_limit; //系統(tǒng)中存在的服務(wù)進(jìn)程的最大數(shù)目int thread_limit; //每個進(jìn)程允許產(chǎn)生的線程的最大數(shù)目ap_scoreboard_e sb_type; ap_generation_t running_generation;apr_time_t restart_time; int lb_limit; } global_score;---------- typedef struct process_score process_score; struct process_score {pid_t pid; //主進(jìn)程的進(jìn)程號ap_generation_t generation; /* generation of this child 家族號*/ap_scoreboard_e sb_type; int quiescing; //記錄將要被優(yōu)雅終止的進(jìn)程的進(jìn)程號 }; ---------- typedef struct worker_score worker_score; struct worker_score {/*第一部分,主要描述線程本身的狀態(tài)和信息*/int thread_num; //Apache識別該線程的唯一標(biāo)志apr_os_thread_t tid; //該線程的線程號unsigned char status; //當(dāng)前線程的狀態(tài)/*第二部分,主要描述線程被訪問的相關(guān)信息*/unsigned long access_count; //當(dāng)前線程在整個服務(wù)器運(yùn)行期間所處理的請求數(shù)目。apr_off_t bytes_served; //記錄當(dāng)前線程從客戶端請求中所讀取的所有字節(jié)總數(shù)unsigned long my_access_count; //當(dāng)前線程在本次連接處理中所讀取的請求字節(jié)總數(shù)apr_off_t my_bytes_served; apr_off_t conn_bytes; //當(dāng)前線程處理的最后一個連接中所處理的字節(jié)數(shù)目unsigned short conn_count; //當(dāng)前線程所處理的最后一個連接中的請求數(shù)目/*第三部分,主要描述線程運(yùn)行相關(guān)的時間信息*/apr_time_t start_time; //記錄線程的啟動時間apr_time_t stop_time; //記錄線程的停止時間 #ifdef HAVE_TIMESstruct tms times; #endifapr_time_t last_used;//記錄線程最后一次使用的時間/*第四部分,主要描述線程的連接信息*/char client[32]; //請求客戶端的主機(jī)名稱或IP地址char request[64]; //客戶端發(fā)送的請求行信息char vhost[32]; //當(dāng)前請求所請求的虛擬主機(jī)名稱 };7.創(chuàng)建記分板
? ? ? ?通過ap_creatge_scoreboard函數(shù)實(shí)現(xiàn)記分板的創(chuàng)建,位于scoreboard.c文件中。
? ? 記分板內(nèi)存大小計(jì)算
? ? ? ?通過函數(shù)ap_calc_scoreboard_size計(jì)算記分板所需要占用的內(nèi)存大小。
? ? 記分板初始化
? ? ? ?通過調(diào)用ap_init_scoreboard()對該內(nèi)存塊進(jìn)行初始化操作。
? ? 記分板內(nèi)存分配圖:
? ? ? ?
? ?記分板插槽管理
? ? ? 最頻繁的一項(xiàng)功能就是在記分板中查找指定進(jìn)程信息,函數(shù)find_child_by_pid(apr_proc_t *pid)用以實(shí)現(xiàn)該功能。
? ?記分板內(nèi)存釋放
? ? ? 通過ap_cleanup_scoreboard()完成內(nèi)存釋放,如果記分板沒有被共享,那么對它的釋放就非常簡單,記分板中的兩個內(nèi)存塊分別用ap_scoreboard_image和ap_scoreboard_image->global標(biāo)識,因此直接調(diào)用free即可。
8.管道具有幾個很鮮明的特點(diǎn):
? ? (1) 管道是半雙工的通信手段,數(shù)據(jù)只能在一個方向上流動。在進(jìn)行雙向數(shù)據(jù)通信時,需要建立兩個管道,分別用于不同方向。
? ? (2) 管道通常用于父子進(jìn)程或兄弟進(jìn)程等具有親緣關(guān)系的進(jìn)程間的通信。
? ? (3) 管道本質(zhì)上是一個文件,對于管道兩端的文件而言,實(shí)際上是對文件進(jìn)行操作。
9.終止管道定義在pod.h中,
typedef struct ap_pod_t ap_pod_t; struct ap_pod_t {apr_file_t *pod_in;apr_file_t *pod_out;apr_pool_t *p; };在終止管道中,pod_out用于父進(jìn)程向子進(jìn)程中寫入數(shù)據(jù),而pod_in則用于子進(jìn)程從管道中讀取信息。
? ? 9.1 終止管道的創(chuàng)建使用ap_mpm_pod_open進(jìn)行。
10.Inetd:通用的多任務(wù)處理結(jié)構(gòu)
? ? 分為兩個部分:主服務(wù)進(jìn)程和客戶服務(wù)進(jìn)程。主服務(wù)器(Master Server)進(jìn)程通常用于等待客戶端的連接請求。一旦客戶端發(fā)起一個請求,主服務(wù)器將建立連接,同時調(diào)用fork創(chuàng)建一個新的客戶服務(wù)進(jìn)程,并由客戶服務(wù)進(jìn)程處理客戶端的請求,而主服務(wù)進(jìn)程則繼續(xù)返回進(jìn)入等待狀態(tài),等待客戶端的請求。整個體系如下圖:
? ??
11.Leader/Follow模式
? ? Apache中使用最多的Prefork MPM就是基于Leader/Follower MPM模型的。
? ??
12.Prefork MPM模型示意圖
? ??
13.Prefork MPM的內(nèi)部數(shù)據(jù)流程
? ??
14.所有的MPM都是從ap_mpm_run()函數(shù)開始執(zhí)行的,預(yù)創(chuàng)建MPM也不例外。ap_mpm_run()函數(shù)通常由Apache核心在main()中進(jìn)行調(diào)用,一旦調(diào)用,運(yùn)行服務(wù)器的任務(wù)就從Apache核心移交給了MPM。這個函數(shù)是所有的MPM都必須實(shí)現(xiàn)的。通常情況下,ap_mpm_run的實(shí)現(xiàn)比較復(fù)雜。主服務(wù)進(jìn)程的功能主要包括下面幾部分:
? ? (1) 接受進(jìn)程外部信號進(jìn)行重啟、關(guān)閉及優(yōu)雅啟動等操作,外部進(jìn)程通過發(fā)送信號給主服務(wù)進(jìn)程以實(shí)現(xiàn)控制主服務(wù)進(jìn)程的目的。
? ? (2) 在啟動時創(chuàng)建子進(jìn)程或在優(yōu)雅啟動時用新進(jìn)程替代原有進(jìn)程。
? ? (3) 監(jiān)控子進(jìn)程的運(yùn)行狀態(tài)并根據(jù)運(yùn)行負(fù)載自動調(diào)節(jié)空閑子進(jìn)程的數(shù)目:在存在過多空閑子進(jìn)程時終止部分空閑進(jìn)程;在空閑子進(jìn)程較少時創(chuàng)建更多的空閑進(jìn)程以便將空閑進(jìn)程的數(shù)目維持在一定的數(shù)目之內(nèi)。
? ?
? ?15.Prefork MPM對各種信號的處理
? ? ?
16.主進(jìn)程對空閑子進(jìn)程的維護(hù)流程
? ??
17.整個子進(jìn)程中函數(shù)調(diào)用的層次如下圖:
? ??
? ? 子進(jìn)程的創(chuàng)建?
? ? ? ? ? 主服務(wù)進(jìn)程是通過調(diào)用make_child函數(shù)來創(chuàng)建一個子進(jìn)程的,函數(shù)定義如下:
static int make_child(server_rec *s, int slot)18.子進(jìn)程主體執(zhí)行流程圖
? ? ?
? ? 整個循環(huán)分為兩部分:對客戶端請求的等待及請求被接受后的處理。等待請求通過poll進(jìn)行輪詢。
? ? 對于所有的子進(jìn)程而言,通常情況下第一件事情就是調(diào)用child_init掛鉤,child_init掛鉤的主要目的就是允許子進(jìn)程初始化互斥鎖,以及可能會由多個子進(jìn)程共享的內(nèi)存塊。
19.在子進(jìn)程循環(huán)中,子進(jìn)程將通過poll來不斷地對輪詢進(jìn)行處理,判斷是否有客戶端連接到來,如果有則立即接受該連接并進(jìn)行處理。說道接受客戶端的連接,就不得不提Unix socket API的一個缺點(diǎn)。如果Apache開放了多個端口或多個地址供客戶端連接,Apache會使用select來檢測每個socket是否就緒,select將表明一個socket有0個或者至少1個連接正等候處理。Apache的模型是多子進(jìn)程的,所有空閑進(jìn)程會同時檢測新的連接。
for (;;) {for (;;) {fd_set accept_fds;FD_ZERO(&accept_fds);for (i = first_socket; i <= last_socket; ++i) {FD_SET(i, &accept_fds);}rc = select(last_socket+1, &accept_fds, NULL, NULL, NULL);if (rc < 1) continue;new_connection = -1;for (i = first_socket; i <= last_socket; ++i) {if (FD_ISSET(i, &accept_fds)) {new_connection = accept(i, NULL, NULL);if (new_connection != -1) break;}}if (new_connection != -1) break;}process the new_connection; }? ? ?這種設(shè)想的實(shí)現(xiàn)方法有一個嚴(yán)重的“饑餓”問題。如果多個子進(jìn)程同時執(zhí)行這個循環(huán),則在多個請求之間,進(jìn)程會被阻塞在select,隨即進(jìn)入循環(huán)并試圖accept此連接,只有一個進(jìn)程可以成功執(zhí)行(假設(shè)還有一個連接就緒),而其余的則會被阻塞在accept。這樣,只有那一個socket可以處理請求,而其他都被鎖住了,直到有足夠多的請求將它們喚醒。此“饑餓”問題在PR#467中有專門的講述。這里至少有兩種解決方法。
? ? ?(1) 使用非阻塞型socket,不阻塞子進(jìn)程并允許它立即繼續(xù)執(zhí)行。但是,這樣會浪費(fèi)CPU時間。
? ? ?(2) 使內(nèi)層循環(huán)的入口串行化:
for (;;) {accept_mutex_on();--------------------------for (;;) {fd_set accept_fds;FD_ZERO(&accept_fds);for (i = first_socket; i <= last_socket; ++i) {FD_SET(i, &accept_fds);}rc = select(last_socket+1, &accept_fds, NULL, NULL, NULL);if (rc < 1) continue;new_connection = -1;for (i = first_socket; i <= last_socket; ++i) {if (FD_ISSET(i, &accept_fds)) {new_connection = accept(i, NULL, NULL);if (new_connection != -1) break;}}if (new_connection != -1) break;}accept_mutex_off();-----------------------------process the new_connection; }20.工作者(Worker)MPM是混合了線程和進(jìn)程的MPM,內(nèi)部結(jié)構(gòu)如下圖:
? ??
? ? ? ?整個Worker MPM內(nèi)部被細(xì)分為3個功能模塊:主進(jìn)程、工作子進(jìn)程及工作線程。主進(jìn)程啟動后,它會建立一組數(shù)目不定的工作子進(jìn)程,子進(jìn)程的數(shù)目由主進(jìn)程進(jìn)行動態(tài)調(diào)整,這與Prefork MPM非常相似,每個子進(jìn)程又會建立固定數(shù)據(jù)的工作線程。
? ? ? ?每個子進(jìn)程產(chǎn)生的線程屬于一組,每組中的線程分為兩種角色:偵聽者線程和工作者線程。偵聽者線程用于偵聽網(wǎng)絡(luò)并接受客戶端連接,一旦接受完畢,就將它們放入連接隊(duì)列中。然后,工作者線程負(fù)責(zé)從隊(duì)列中獲取連接,為所有來自它的請求提供服務(wù)。偵聽線程和工作線程之間通過套接字隊(duì)列進(jìn)行異步通信。
? ? ? ?當(dāng)服務(wù)器繁忙時,通常需要產(chǎn)生更多的工作者線程。但是Worker MPM并不直接創(chuàng)建線程,它首先新創(chuàng)建一個進(jìn)程,然后由這個進(jìn)程再一次性地創(chuàng)建多個線程。因此Worker MPM中線程的創(chuàng)建總是批量的。與之類似,當(dāng)服務(wù)器空閑時,MPM也不是逐個地終止某個特定的線程,它仍然以進(jìn)程為單位,終止一個進(jìn)程,然后該進(jìn)程一次性地終止該進(jìn)程下的所有線程。
? ? ? ?關(guān)于函數(shù)的調(diào)用層次:
? ? ??
? ? ? 整個內(nèi)部數(shù)據(jù)流程如下圖:
? ? ??
? ? ?Worker MPM的apr_mpm_run()的層次更加請求,整個模塊可以被分割為3個功能部分,如下圖所示:
? ? ??
? ? ? ?Worker MPM使用server_main_loop函數(shù)使主進(jìn)程進(jìn)入循環(huán)處理:
server_main_loop(remaining_children_to_start);? ?
21.子進(jìn)程工作流程
? ??
? ?對于子進(jìn)程而言,它最重要的任務(wù)就是創(chuàng)建N個線程,其中包括一個偵聽線程及過個工作線程。
? ?創(chuàng)建線程通過start_threads函數(shù)實(shí)現(xiàn)
? ?偵聽線程和工作者線程之間通過連接套接字隊(duì)列進(jìn)行通信。偵聽線程接受所有的客戶端連接,并且將接收到的套接字放入隊(duì)列中。同時工作線程不斷地從隊(duì)列中獲取套接字并對其進(jìn)行處理。
? ?偵聽線程將接收到的連接放入連接套接字隊(duì)列中,而工作線程則不斷地監(jiān)視連接套接字隊(duì)列,一旦發(fā)現(xiàn)有可用的套接字,工作線程將對該連接進(jìn)行處理。
? ?Worker MPM中使用fd_queue_t描述套接字隊(duì)列:
struct fd_queue_t {fd_queue_elem_t *data; //記錄實(shí)際的套接字?jǐn)?shù)據(jù)int nelts;int bounds;apr_thread_mutex_t *one_big_mutex;apr_thread_cond_t *not_empty;int terminated; }; typedef struct fd_queue_t fd_queue_t;struct fd_queue_elem_t {apr_socket_t *sd; //套接字的描述符apr_pool_t *p; //該套接字所使用的內(nèi)存池conn_state_t *cs; //新版本中新引入的一個成員,用于記錄當(dāng)前套接字的連接狀態(tài) }; typedef struct fd_queue_elem_t fd_queue_elem_t;? ? ? 為了操作套接字隊(duì)列,Apache提供了一系列的操作哈數(shù),包括:
? ? ? (1) 套接字隊(duì)列初始化
apr_status_t ap_queue_init(fd_queue_t *queue, int queue_capacity, apr_pool_t *a);? ? ? ? ? ? queue_capacity是隊(duì)列初始化時的容量大小,創(chuàng)建所需要的所有的內(nèi)存均來自內(nèi)存池a,最終生成的隊(duì)列由queue返回。
? ? ? (2) 套接字入隊(duì)列
apr_status_t ap_queue_push(fd_queue_t *queue, apr_socket_t *sd,conn_state_t *cs, apr_pool_t *p);? ? ? ? ? ? ?queue是目的套接字隊(duì)列,sd是需要保存的套接字,cs則是當(dāng)前的連接裝填。
? ? ? ?(3) 套接字出隊(duì)列
apr_status_t ap_queue_pop(fd_queue_t *queue, apr_socket_t **sd,conn_state_t **cs, apr_pool_t **p);? ? ? ? ? ? queue是操作隊(duì)列,套接字的相關(guān)信息則由sd、cs以及p分別返回。
22.偵聽線程工作流程
? ??
23.調(diào)用過程accept_mutex_on():申請互斥鎖或等待直到該互斥鎖可用。
? ? 調(diào)用過程accept_mutex_off():釋放互斥鎖。
24.工作者線程的數(shù)據(jù)流程
? ??
? ?工作線程主要完成兩個方面的任務(wù):
? ? ?(1) 從套接字隊(duì)列中獲取一個可用的套接字;
? ? ?(2) 處理套接字。
? ? 工作線程的入口函數(shù)為worker_thread.
? ? 工作者線程將調(diào)用ap_queue_pop從套接字隊(duì)列中獲取一個可用的套接字,如果隊(duì)列為空,那么ap_queue_pop將被阻塞。對于獲取到的套接字,process_socket函數(shù)將被調(diào)用對其進(jìn)行處理。
25.對于Apache而言,WinNT MPM是Window平臺下唯一使用的MPM。
? ??
? ? 整個WinNT MPM內(nèi)部可以分割為3個重要的組成部分:兩個進(jìn)程及一組多個工作進(jìn)程。兩個重要的進(jìn)程分別是:監(jiān)控進(jìn)程和工作進(jìn)程。
? ??
? 監(jiān)控進(jìn)程通常稱為父進(jìn)程,主要任務(wù)是執(zhí)行master_main函數(shù),監(jiān)視子進(jìn)程的運(yùn)行情況并做出適當(dāng)?shù)奶幚?#xff0c;這些處理包括以下幾個部分:
? ? ? (1) 子進(jìn)程處理部分。父進(jìn)程主要負(fù)責(zé)創(chuàng)建子進(jìn)程、關(guān)閉子進(jìn)程等;
? ? ? (2) 事件響應(yīng)部分。事件主要包括三種:重啟事件、終止事件及子進(jìn)程處理。
? 工作進(jìn)程由父進(jìn)程創(chuàng)建,主要任務(wù)是創(chuàng)建工作線程并對工作線程進(jìn)行管理。內(nèi)部數(shù)據(jù)流程如下圖:
? ??
? ? 工作進(jìn)程創(chuàng)建的線程包括三大類:
? ? ?(1) 一個主線程。主要負(fù)責(zé)啟動一個或多個偵聽線程用于偵聽等待客戶端的連接請求;
? ? ?(2) 一個或多個偵聽線程,一旦發(fā)現(xiàn)客戶端的請求,它們將接受該請求并將該請求保存到隊(duì)列中。
? ? ?(3) 固定數(shù)目的工作線程。股則處理客戶端的連接請求并對其進(jìn)行響應(yīng)。
? ? 主進(jìn)程通過調(diào)用CreateProcess()創(chuàng)建子進(jìn)程。
? ? 工作隊(duì)列用于子進(jìn)程和工作線程之間進(jìn)行套接字傳遞,它基于生產(chǎn)者/消費(fèi)者模式,子進(jìn)程是生產(chǎn)者,它生產(chǎn)套接字,并寫入工作隊(duì)列;線程則是消費(fèi)者,它從隊(duì)列中讀取套接字。對工作隊(duì)列的操作必須保持互斥和同步:插入、刪除都必須鎖定;隊(duì)列中沒有套接字可讀取時,線程必須等待,一旦有新的套接字放入隊(duì)列,線程則被喚醒。在使用之前必須創(chuàng)建響應(yīng)的互斥鎖和信號燈。
? ? ?完整的偵聽過程包括創(chuàng)建套接字、調(diào)用Listen及accept三大步驟。
? ? Windows NT下的連接接受:
? ??
??
?
總結(jié)
以上是生活随笔為你收集整理的学习《apache源代码全景分析》之多任务并发处理摘录的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 学习《apache源代码全景分析》之模块
- 下一篇: 学习《apache源代码全景分析》之网络