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