参考资料学习APR库
APR分析-整體篇
? ? 一、何為APR?
? ? ? ? ?Apache Server經過這么多年的發展后,將一些通用的運行時接口封裝起來供給大家,這就是Apache Portable Run-time libraries,APR。
? ? 二、APR的目錄組織
? ? ? ? ? 1)所有的頭文件都放在$(APR)/include目錄中;
? ? ? ? ? 2)所有功能接口的實現都放在各自的獨立目錄下,如threadproc、mmap等;
? ? ? ? ? 3)此外就是相關平臺構建工具文件如Makefile.in等。曾經看過ACE的代碼,ACE的所有源文件(.cpp)都放在一個目錄下,顯得很混亂。
? ? ? ? ? ?4)進入各功能接口子目錄,以threadproc為例,在其下面的子目錄有5個,分別是beos、netware、os2、unix和win32.
? ? 三、APR構建
? ? ? ? ? ?如果想要使用APR,需要先在特定平臺上構建它,這里不考慮多個平臺的特性,僅針對Unix平臺進行分析。
? ? ? ? ? ?1) apr.h、apr.h.in、apr.h.hw和apr.h.hnw的關系
? ? ? ? ? ? ? ?在$(APR)/include目錄下,由于APR考慮移植性等原因,最基本的apr.h文件是在構建時自動生成的,其中apr.h.in類似一模板作為apr.h生成程序的輸入源。其中apr.h.hw和apr.h.hnw分別是Windows和NetWare的特定版本。
? ? ? ? ? ?2) 編譯時注意事項
? ? ? ? ? ? ? ? 在Unix上編譯時,注意$(APR)/build下*.sh文件的訪問權限,應該先chmod以下,否則Make的時候會提示ERROR。
? ? 四、應用APR
? ? ? ? ? ? 我們首先make install一下,比如我們在Makefile中指定prefix=$(APR)/dist,則make install后,在$(APR)/dist下會發現4個子目錄,分別為bin、lib、include和build,其中我們感興趣的只有include和lib。
? ? ? ? ? ? ?下面是一個APR app的例子project。
? ? ? ? ? ? ?該工程的目錄組織如下:
$(apr_path)- dist- lib- include- examples- apr_app- Make.properties- Makefile- apr_app.c? ? ? ? ? ? ? 我們的Make.properties文件內容如下:
# # The APR app demo # CC = gcc -Wall BASEDIR =$(HOME)/apr-1.1.1/examples/apr_app APRDIR =$(HOME)/apr-1.1.1 APRVER = 1 APRINCL =$(APRDIR)/dist/include/apr-$(APRVER) APRLIB =$(APRDIR)/dist/lib DEFS = -D_REENTRANT -D_POSIX_PTHREAD_SEMANTICS -D_DEBUG_ LIBS = -L$(APRLIB) -lapr-$(APRVER) /-lpthread -lxnet -lposix4 -ldl -lkstat -lnsl -lkvm -lz -lelf -lm -lsocket -ladmINCL = -I$(APRINCL) CFLAGS =$(DEFS) $(INCL)Makefile文件內容如下: include Make.properties TARGET = apr_app OBJS = apr_app.o all: $(TARGET) $(TARGET): $(OBJS)$(CC) ${CFLAGS} -o $@$(OBJS) ${LIBS} clean:rm -f core $(TARGET)$(OBJS)? ? ?而apr_app.c文件采用的是$(par_path)/test目錄下的proc_child.c文件。
五、GO ON
? ? 5.1 APR分析-設計篇
? ? ? ? 5.1.1 類型
? ? ? ? ? ? ?1) APR提供的節本自定義數據類型包括:
typedef unsigned char apr_byte_t; typedef short apr_int16_t; typedef unsigned short apr_uint16_t; typedef int apr_int32_t; typedef unsigned int apr_uint32_t; typedef long long apr_uint32_t; typedef long long apr_int64_t; typedef unsigned long long apr_uint64_t;? ? ? ? ? ? ?這些都是在apr.h中定義的。
? ? ? ? ? ? ?2)在APR的設計文檔中,它稱"dso、mmap、process、thread"等為"base types"。
? ? ? ? ? ? ?3) 另外的一個特點就是大多APR類型中都包含一個apr_pool_t類型的字段,你最好在該類型中假如一個apr_pool_t類型的字段,否則所有操作該類型的APR函數都需要一個apr_pool_t類型的參數。
? ? ? ?5.1.2 函數
? ? ? ? ? ? 1) APR的固定個數參數公共函數的聲明形式APR_DECLARE(rettype) apr_func(args);而非固定個數參數的公共函數的聲明形式為APR_DECLARE_NONSTD(rettype) apr_func(args,...);".在Unix上的apr.h中有著兩個宏的定義:
#define APR_DECLARE(type) type #define APR_DECLARE_NONSTD(type) type? ? ? ? ? ? ? ? ? 在apr.h文件中解釋了這么做就是為了在不同平臺上編譯時使用“the most appropriate calling convention”,這里的"calling convention"是一術語,叫"調用約定".常見的調用約定有:stdcall、cdecl、fastcall、thiscall和naked call,其中cdecl調用約定又稱為C調用約定,是C語言缺省的調用約定。
? ? ? ? ? ? ?2)如果你想新增APR函數,APR建議你最好按照如下做:
? ? ? ? ? ? ? ? ?a)輸出參數為第一個參數:
? ? ? ? ? ? ? ? ? b)如果某個函數需要內部分配內存,則將一個apr_pool_t 參數放在最后。
? ? ? ? ? 5.1.3 錯誤處理
? ? ? ? ? ? ? ? APR作為一通用的庫接口集合詳細的說明了使用APR時如何進行錯誤處理。
? ? ? ? ? ? ? ?1) 錯誤處理的第一步就是"錯誤碼和狀態碼分類"。APR的函數大部分都返回apr_status_t類型的錯誤碼,這是一個int型,在apr_errno.h中定義,和它在一起定義的還有apr所用的所有錯誤碼和狀態碼。
? ? ? ? ? ? ? ? 2) 如何定義錯誤捕捉策略?
? ? ? ? ? ? ? ? ? ?由于APR是可移植的,這樣就可能遇到這樣一個問題:不同平臺錯誤碼的不一致。如何處理呢?APR給我們提供了2中策略:
? ? ? ? ? ? ? ? ? ? ?a) 跨多平臺返回相同的錯誤碼
? ? ? ? ? ? ? ? ? ? ?b)返回平臺相關錯誤碼,如果需要將它轉換為通用錯誤碼
? ? ? ? ? ? ? ? ? ? ? ? ?程序的執行錄像往往要根據函數返回錯誤碼來定,這么做的缺點就是把這些工作推給了程序員。執行流程如下:
? ? ? ? ? ? ? ? ? ? ? ? ?make syscall that fails
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?return error code
? ? ? ? ? ? ? ? ? ? ? ?----------------------------------------------------------------
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? convert to common error code (using ap_canonical_error)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? decide execution based on common error code
? ? ? ? ? ? ? ? ? ? ///
? ? ? ? ? ? ? ? ? ? 注1 調用約定
? ? ? ? ? ? ? ? ? ? ? ? ? ?我們知道函數調用是通過棧操作來完成的,在棧操作過程中需要函數的調用者和被調用者在下面的兩個問題上作出協調,達成協議:
? ? ? ? ? ? ? ? ? ? ? ? ? ?a) 當參數個數多余一個時,按照什么順序把參數壓入堆棧
? ? ? ? ? ? ? ? ? ? ? ? ? ?b) 函數調用后,由誰來把堆?;貜驮瓉頎顟B
? ? ? ? ? ? ? ? ? ? ? ? ? ?在像C/C++這樣的中、高級語言中,使用"調用約定"來說明這兩個問題。
? ? ? ? ? ? ? ? ? ?///
? ?5.2 APR分析-進程篇
? ? ? ? ?Apache Server的進程調度一直為人所稱道,Apache 2.0推出的APR對進程進行了封裝,特別是Apache 2.0的MPM(Multiple Precess Management)框架就是以APR封裝的進程為基礎的。
? ? ? ? ? ? ?APR進程封裝源代碼的位置在$(APR_HOME)/threadproc目錄下,本篇blog著重分析unix子目錄下的proc.c文件內容,其相應頭文件為$(APR_HOME)/include/apr_thread_proc.h。
? ? ? ? ?一、APR進程概述
? ? ? ? ? ? ? APR進程封裝采用了傳統的fork-exec配合方式(spawn),即父進程在fork出子進程后繼續執行其自己的代碼,而子進程調用exec函數加載新的程序影響到其地址空間,執行新的程序。我們先來看看使用APR創建一個新的進程的流程,然后再根據流程做細節分析:
apr_proc_t newproc; apr_pool_t *p; apr_status_t rv; const char *args[2]; apr_procattr_t *attr; /* 初始化APR內部使用的內存*/ rv = apr_pool_initialize(); HANDLE_RTVAL(apr_pool_initialize, rv); rv = apr_pool_create(&p, NULL); HANDLE_RTVAL(apr_pool_create, rv); /*創建并初始化新進程的屬性*/ rv = apr_procattr_create(&attr, p); HANDLE_RTVAL(apr_procattr_create, rv); rv = apr_procattr_io_set(attr, APR_FULL_BLOCK, APR_FULL_BLOCK, APR_NO_PIPE); /*可選*/ HANDLE_RTVAL(apr_procattr_io_set, rv); rv = apr_procattr_dir_set(attr, "startup_path");/*可選*/ HANDLE_RTVAL(apr_procattr_dir_set, rv); rv = apr_procattr_cmdtype_set(attr, APR_PROGRAM);/*可選*/ HANDLE_RTVAL(apr_procattr_cmdtype_set, rv); ... .../*其他設置進程屬性的函數*/ /*創建新進程*/ args[0] = "proc_child"; args[1] = NULL; rv = apr_proc_create(&newproc,"your_progname", args, NULL, attr, p); HANDLE_RTVAL(apr_proc_create, rv); /*等待子進程結束*/ rv = apr_proc_wait(&newproc, NULL, NULL, APR_WAIT); HANDLE_RTVAL(apr_proc_wait, rv);? ? ? ? ? ?二、APR procattr創建
? ? ? ? ? ? ? ? ?在我們平時的Unix進程相關編程時,我們大致會接觸兩類進程操作函數:進程創建函數(如fork和exec等)和進程屬性操作函數(getpid、chdir等),APR將進程的相關屬性信息封裝到apr_procattr_t結構體中,我們來看看這個重要的結構體定義:(這里只列出Unix下可用的屬性)
/* in $(APR_HOME)/include/arch/unix/apr_arch_threadproc.h */ struct apr_procattr_t {/* PART 1 */apr_pool_t *pool;/* PART 2 */apr_file_t *parent_in;apr_file_t *child_in;apr_file_t *parent_out;apr_file_t *child_out;apr_file_t *parent_err;apr_file_t *child_err;/* PART 3 */char *currdir;apr_int32_t cmdtype;apr_int32_t detached;/* PART 4 */struct rlimit *limit_cpu;struct rlimit *limit_mem;struct rlimit *limit_nproc;struct rlimit *limit_nofile;/* PART 5 */apr_child_errfn_t *errfn;apr_int32_t errchk;/* PART 6 */apr_uid_t uid;apr_gid_t gid; };? ? ? ?我這里講apr_procattr_t 包含的字段大致分為6部分,下面逐一說明:
? ? ? ? [PART 1]
? ? ? ? 在上一篇關于APR的blog中說過,大部分的APR類型中都會有一個apr_pool_t 類型字段,用于APR內部的內存管理,此結構也無例外。該字段用來標識procattr在哪個pool中分配的內存。
? ? ? ? [PART 2]
? ? ? ? 進程不是孤立存在的,進程也是由父有子的。父子進程間通過傳統的匿名pipe進行通信。在apr_procattr_io_set(attr,APR_FULL_BLOCK,APR_FULL_BLOCK,APR_FULL_BLOCK)調用后,我們可以用下面的圖來表示這些字段的狀態:
parent_in----------------------------------------------------------filedes[0] "in_pipe" filedes[1]------------------------ chiild_in ----- parent_out---------------------------filedes[0] "out_pipe" filedes[1]------------------------ child_out ---------------------- parent_err ---------------------------filedes[0] "err_pipe" filedes[1]------------------------ child_err ---------------------------? ? ? ? 還有一點指的注意的是apr_procattr_io_set調用apr_file_pipe_create創建pipe的時候,為相應的in/out字段注冊了cleanup函數apr_unix_file_cleanup, apr_unix_file_cleanup在相應的in/out字段的pool銷毀時被調用,在后面的apr_proc_create時還會涉及到這塊。
? ? ? ? [PART 3]
? ? ? ? 進程的一些常規屬性。
? ? ? ? currdir標識新進程啟動時的工作路徑(執行路徑),默認時為何父進程相同;
? ? ? ? cmdtype標識新的子進程將執行什么類型的命令;共5種類型,默認為APR_PROGRAM
? ? ? ? detached標識新進程是否為分離后臺進程,默認為前臺進程。
? ? ? ? [PART 4]
? ? ? ? 這4個字段標識平臺對進程資源的限制,一般我們接觸不到。struct rlimit的定義在/usr/include/sys/resource.h中。
? ? ? ? [PART 5]?
? ? ? ? errfn為一函數指針,原型為typedef void (apr_child_errfn_t)(apr_pool_t *proc, apr_status_t err, const char *description);這個函數指針如果被賦值,那么當子進程遇到錯誤退出前將調用該函數。
? ? ? ? ? ? errchk一個標志值,用于告知apr_proc_create是否對子進程屬性進行檢查,如檢查curdir的access屬性等。
? ? ? ? [PART 6]
? ? ? ? ? ? 用戶ID和組ID,用于檢索允許該用戶所使用的權限。
? ? ? ? 三、APR proc創建
? ? ? ? ? ? APR proc的描述結構為apr_proc_t:
typedef struct apr_proc_t {/** The process ID */pid_t pid;/** Parent's side of pipe to child's stdin */apr_file_t *in;/** Parent's side of pipe to child's stdout */apr_file_t *out;/** Parent's side of pipe to child's stderr*/apr_file_t *err; } apr_proc_t;? ? ? ? ? ? 創建一個新的進程的接口為apr_proc_create,其參數也都很簡單。前面說過apr_proc_create先fork出一個子進程,眾所周知fork后子進程是父進程的復制品,然后子進程再通過exec函數加載新的程序映像,并開始執行新的程序。這里分析一下apr_proc_create的執行流程,其偽碼如下:
apr_proc_create {if (attr->errchk)對attr做有效性檢查,讓錯誤盡量發生在parentprocess中,而不是留給child process; ---(1)fork子進程;{ /*在子進程中*/清理一些不必要的從父進程繼承下來的描述符等,為exec提供一個"干凈的"的環境 ---(2)關閉attr->parent_int、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; }? ? ? ?下面針對上述偽碼進行具體分析:
? ? ? ? 1)有效性檢查
? ? ? ? ? ? attr->errchk屬性可以通過apr_procattr_error_check_set函數在apr_proc_create之前設置。一旦設置,apr_proc_create就會在fork子進程前對procattr的有效性進行檢查,比如attr->curdir的訪問屬性(利用access檢查)、progname文件的訪問權限檢查等。這些的目的就是一個"讓錯誤發生在fork前,不要等到在子進程中出錯"。
? ? ? ? 2) 清理"不必要的"繼承物
? ? ? ? ? ? ? ? ? ?由于子進程復制了父進程的地址空間,隨之而來的還包含一些"不必要"的"垃圾"。為了給exec提供一個"干凈的"環境,在exec之前首先要做一下必要的清理,APR使用apr_pool_cleanup_for_exec來完成這項任務。apr_pool_cleanup_for_exec做了哪些工作呢?apr_pool_cleanup_for_exec通過pool內部的global_pool搜索其子節點,并逐一遞歸cleanup,這里的cleanup并不釋放任何內存,也不flushI/O Buffer,僅是調用節點注冊的相關cleanup函數,這里我們可以回顧一下apr_procattr_io_set調用,在創建相關pipe時就為相應的in/out/err描述符注冊了cleanup函數。同樣就是因為這點,子進程再調用apr_pool_cleanup_for_exec之前,首先要kill掉(這里理解就是去掉相關文件描述符上的cleanup注冊函數)這些注冊函數。防止相關pipe的描述符被意外關閉。
? ? ? ? 3) 建立起與父進程"對話通道"
? ? ? ? ? ?父進程在創建procattr時就建立了若干個pipe,fork后子進程繼承了這些。為了關掉一些不必要的描述符和更好的和父進程通訊,子進程作了一些重定向的工作,這里用圖來表示重定向前后的差別:(圖中顯示的是子進程關閉parent_in/out/err三個描述符后的文件描述符表)
? ? ? ? ? ? 重定向前:
? ? ? ? ? ? ? ?子進程文件描述符
? ? ? ? ? ? ? ? ? ?----------------------------|
? ? ? ? ? ? ? ? ? ?[0] STDIN_FILENO|
? ? ? ? ? ? ? ? ? ?-----------------------------|
? ? ? ? ? ? ? ? ? ?[1] STDOUT_FILENO
? ? ? ? ? ? ? ? ? ?-----------------------------|
? ? ? ? ? ? ? ? ? ?[2] STDERR_FILENO
? ? ? ? ? ? ? ? ? ?------------------------------|
? ? ? ? ? ? ? ? ? ?[3] child_in.fd |----> in_pipe的filedes[0]
? ? ? ? ? ? ? ? ? ?-------------------------|
? ? ? ? ? ? ? ? ? ?[4] child_out.fd|--->out_pipe的filedes[1]
? ? ? ? ? ? ? ? ? ?-------------------------|
? ? ? ? ? ? ? ? ? ?[5] child_err.fd|--->err_pipe的filedes[1]
? ? ? ? ? ? ? ? ? ?----------------------|
? ? ? ? ? ? ? ? ? ?重定向后:
? ? ? ? ? ? ? ? ? ?-----------|
? ? ? ? ? ? ? ? ? ?[0] child_in.fd |--->in_pipe的filedes[0]
? ? ? ? ? ? ? ? ? ?-----------|
? ? ? ? ? ? ? ? ? ?[1]child_out.fd |--->out_pipe的filedes[1]
? ? ? ? ? ? ? ? ? ?-----------|
? ? ? ? ? ? ? ? ? ?[2]child_err.fd |---->err_pipe的filedes[1]
? ? ? ? ? ? ? ? ? ? -----------|
? ? ? ? ? ? ? ? ? ?為了能更好的體現出"對話通道"的概念,這里再畫出父進程再關閉attr->child_in、child_out和child_err后的文件描述表:
? ? ? ? ? ? ? ? ? ? ?父進程文件描述表
? ? ? ? ? ? ? ? ? ? ?-----------------|
? ? ? ? ? ? ? ? ? ? ?[0] STDIN_FILENO |
? ? ? ? ? ? ? ? ? ? ?-----------------|
? ? ? ? ? ? ? ? ? ? ?[1] STDOUT_FILENO |
? ? ? ? ? ? ? ? ? ? ?------------------|
? ? ? ? ? ? ? ? ? ? ?[2] STDERR_FILENO |
? ? ? ? ? ? ? ? ? ? ? ------------------|
? ? ? ? ? ? ? ? ? ? ? [3]parent_in.fd | ---->in_pipe的filedes[1]
? ? ? ? ? ? ? ? ? ? ? -------------------|
? ? ? ? ? ? ? ? ? ? ? [4] parent_out.fd |---->out_pipe的filedes[0]
? ? ? ? ? ? ? ? ? ? ? -------------------|
? ? ? ? ? ? ? ? ? ? ? [5] parent_err.fd |---->err_pipe的filedes[0]
? ? ? ? ? ? ? ? ? ? ? -------------------|
? ? ? ? 4) 啟動新的程序
? ? ? ? ? ? 根據APR proc的設計,子進程在被fork出來后,將根據procattr的cmdtype等屬性信息決定調用哪種exec函數。當子進程調用一種exec函數時,子進程將完全由新程序代換,而新程序則從其main函數開始執行(與fork不同,fork返回后子進程從fork點開始往下執行)。因為調用exec并不創建新進程,所以前后的進程ID并未改變。exec只是用另一個新程序替換了當前進程的正文、數據、堆和棧段。
? ? ? ? 四、總結
? ? ? ? ? ?xx_in/xx_out都是相對于child process來說的,xx_in表示通過該描述符child process從in_pipe讀出parent process寫入in_pipe的數據;xx_out表示通過該描述符child process將數據寫入out_pipe供parent process使用;xx_err則是child process將錯誤信息寫入err_pipe供parent process使用。
? ? ? ? ? ?fork后子進程和父進程的同和異
? ? ? ? ? ? ? 同:
? ? ? ? ? ? ? ?--父進程已經打開的文件描述符;
? ? ? ? ? ? ? ?--實際用戶ID、實際組ID、有效用戶ID、有效組ID;
? ? ? ? ? ? ? ?--添加組ID;
? ? ? ? ? ? ? ?--進程組ID;
? ? ? ? ? ? ? ?--對話期ID;
? ? ? ? ? ? ? ?--控制終端;
? ? ? ? ? ? ? ?--設置用戶ID標志和設置組ID標志;
? ? ? ? ? ? ? ?--當前工作目錄;
? ? ? ? ? ? ? ?--根目錄;
? ? ? ? ? ? ? ?--文件方式創建屏蔽字;
? ? ? ? ? ? ? ?--信號屏蔽和排列;
? ? ? ? ? ? ? ?--對任一打開文件描述符的在執行時關閉標志;
? ? ? ? ? ? ? ?--環境;
? ? ? ? ? ? ? ? --連接的共享存儲段;
? ? ? ? ? ? ? ? --資源限制.
? ? ? ? ? ? ? ?異:
? ? ? ? ? ? ? ? ?--fork的返回值;
? ? ? ? ? ? ? ? ?--進程ID;
? ? ? ? ? ? ? ? ? --不同的父進程ID;
? ? ? ? ? ? ? ? ?--子進程的tms_utime,tms_stime,tms_cutime以及tme_ustime設置為0;
? ? ? ? ? ? ? ? ?--父進程設置的鎖,子進程不繼承;
? ? ? ? ? ? ? ? ? --子進程的未決告警被清除;
? ? ? ? ? ? ? ? ? ?--子進程的未決信號集設置為空集
? 5.3 APR分析-內存篇
? ? ? ? 內存管理一直是讓C程序員頭痛的問題,作為一個通用接口集,APR當然也提供其自己的內存管理接口--APR Pool。APR Pool作為整個APR的一個基礎功能接口,直接影響著APR的設計風格。
? ? ? ? APR Pool源代碼的位置在$(APR_HOME)/memory目錄下,本篇blog著重分析unix子目錄下的apr_pools.c文件內容,其相應頭文件為$(APR_HOME)/include/apr_pools.h;在apr_pools.c中還實現了負責APR內部內存分配的APRallocator的相關操作接口(APR allocator相關頭文件為$(APR_HOME)/include/apr_allocator.h)。
? ? ? ? 一、APR Pool概述
? ? ? ? ? ? ?我們平時常用的內存管理方式都是基于"request-style"的,即分配所請求大小的內存,使用之,銷毀之。而APR Pool的設計初衷是為Complex Application提供良好的內存管理接口,其使用方式與"request-style"有所不同。而$(APR_HOME)/docs/pool-design.htm文檔中,設計者道出了"使用好"APR Pool的幾個Rules,同時也從側面反映出APRPool的設計。
? ? ? ? ? ? ?1.任何Object都不應該有自己的Pool,它應在其構造函數的調用者的Pool中分配。因為一般調用者知道該Object的聲明周期,并通過Pool管理之。也就說Object無須自己調用"Close" or "Free",這些操作在Object所在Pool被摧毀時被隱式調用的。
? ? ? ? ? ? ? 2.函數無須為了他們的行為而去Create/Destroy Pool,它們應該使用它們調用者傳給它們的Pool。
? ? ? ? ? ? ? 3.為了防止內存無限制的增長,APR Pool建議當遇到unbounded iteration時使用sub_pool,標準格式如下:
subpool=apr_pool_create(pool,NULL); for(i=0;i<n;++i) {apr_pool_clear(subpool);... ...do_operation(..., subpool); } apr_pool_destroy(subpool);? ? ? ? ? ?二、深入APR Pool
? ? ? ? ? ? ? ?1.分析apr_pool_initialize
? ? ? ? ? ? ? ? ? 任何使用APR的應用程序一般都會調用apr_app_initalize來初始化APR的內部使用的數據結構,查看一下app_app_initialize的代碼,你會發現apr_pool_initialize在被apr_app_initialize調用的apr_initialize中被調用,該函數用來初始化使用Pool所需的內部結構(用戶無須直接調用apr_pool_initialize,在apr_app_initialize時它被自動調用,而apr_app_initailize又是APR? program調用的第一個function,其在apr_general.h中聲明,在misc/unix/start.c中實現)。
? ? ? ? ? ? ? ? ?apr_pool_initialize {
? ? ? ? ? ? ? ? ? ? ? 如果(!apr_pools_initialized) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?創建global_allocator;? ?------(1)
? ? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ? ? ? ? 創建global_pool; ------(2)
? ? ? ? ? ? ? ? ? ? ? ? 給global_pool起名為"apr_global_pool";
? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? (1) Pool和Allocator
? ? ? ? ? ? ? ? ? ? ? ? ? ?每個Pool都有一個allocator相伴,這個allocator可能是Pool自己的,也可能是其ParentPool的。allocator的結構如下:
/* in apr_pools.c*/ struct apr_allocator_t {apr_uint32_t max_index;apr_uint32_t max_free_index;apr_uint32_t current_free_index;... ...apr_pool_t *owner;apr_memnode_t *free[MAX_INDEX]; }; ? ? ? ? ?在(1)調用后,global_allocator的所有xx_index字段都為0,owner-->NULL,free指針數組中的指針也都-->NULL。這里的index是大小的級別,這里最大級別為20(即MAX_INDEX=20),free指針數組中free[0]所指的node大小為MIN_ALLOC大小,即8192,即2的13次冪。按此類推free[19]所指的node大小應為2的32次冪,即4G byte。allocator_alloc中是通過index=(size >> BOUNDARY_INDEX) - 1來得到這一index的。allocator維護了一個index不同的memnode池,每一index級別上又有一個memnode list,以后用戶調用apr_palloc分配size大小內存時,allocator_alloc函數就會在free memnode池中選和要尋找的size的index級別相同的memnode,而不是重新malloc一個size大小的memnode。另外要說明一點的是APR Pool中所有ADT中的xx_index字段都是大小級別的概念。
? ? ? ? ? ?(2) 創建global_pool
? ? ? ? ? ? ? 在APR Pool初始化的時候,唯一創建一個Pool --global_pool。apr_pool_t的非Debug版本如下:
/* in apr_pools.c */ struct apr_pool_t {apr_pool_t *parent;apr_pool_t *child;apr_pool_t *sibling;apr_pool_t **ref;cleanup_t *cleanups;cleanup_t *free_cleanups;apr_allocator_t *allocator;struct process_chain *subprocesses;apr_abortfunc_t abort_fn;apr_hash_t *user_data;const char *tag;apr_memnode_t *active;apr_memnode_t *self; /*The nodecontaining the pool itself */char *self_first_avail;... ... }? ? ? ? ? ? ? 而apr_memnode_t的結構如下:
/* in apr_allocator.h */ struct apr_memnode_t {apr_memnode_t *next; /*next memnode */apr_memnode_t **ref; /*reference to self*/apr_uint32_t index; /*size*/apr_uint32_t free_index; /*how much free*/char *first_avail; /*pointer to first free memory*/char *endp; /*pointer to end of free memory */ };? ? ? ? ? ? ? apr_pool_create_ex首先通過allocator尋找合適的node用于創建Pool,但由于global_allocator尚未分配過任何node,所以global_allocator創建一個新的node,該node大小為MIN_ALLOC(即8192),該node的當前狀態如下:
node-->|----------|0| || ||----------|APR_MEMNODE_T_SIZE <----node->first_avail| || || ||----------size(一般為8192)<---node->endp? ? ? ? ? ? ?其他屬性值如下:
? ? ? ? ? ? ?node->next = NULL;
? ? ? ? ? ? ?node->index = (APR_UINT32_TRUNC_CAST)index; /*這里為1*/
? ? ? ? ? ? ?創建完node后,我們將在該node上的avail space劃分出我們的global_pool來。劃分后狀態如下(pool與node關系):
node-->|----------|0 <---pool->self=pool_active| || ||-------------|APR_MEMNODE_T_SIZE <------global_pool| || ||--------------|APR_MEMNODE_T_SIZE+SIZEOF_POOL_T <------node->first_avail=pool->self_first_avail| || ||------------size(一般為8192) <------------node->endp? ? ? ? ? ? ?pool其他一些屬性值(pool與pool之間關系)如下:
pool->allocator=global_allocator; pool->child = NULL; pool->sibling = NULL; pool->ref = NULL;? ? ? ? ? ? ? ? ?2.APR Sub_Pool創建(pool與pool之間關系)
? ? ? ? ? ? ? ? ? 上面我們已經初始化了global_pool,但是global_pool是不能直接拿來就用的,我們需要創建其sub_pool,也就是用戶自己的pool。一般創建user的sub_pool我們都使用apr_pool_create宏,它只需要2個參數,并默認sub_pool繼承parent_pool的allocator和abort_fn。在apr_pool_create內部調用的還是apr_pool_create_ex函數。我們來看一下創建sub_pool后pool之間的關系:
? ? ? ? ? ? ? ? ? 例:
? ? ? ? ? ? ? ? ? ?static apr_pool_t *sub_pool = NULL;
? ? ? ? ? ? ? ? ? ?apr_pool_create(&sub_pool, NULL);
? ? ? ? ? ? ? ? ? ?這里sub_pool的創建過程與global_pool相似,也是先創建其承載體node,然后設置相關屬性,使其成為global_pool的child_pool。創建完后global_pool和該sub_pool的關系如下圖:
global_pool <------/ ----->sub_pool ---------- // -------- sibling --->NULL /--------parent ---------- / -------- child--------------/ sibling--->NULL -------- ---------child--->NULL---------? ? ? ? ? ? ? ? ?APR Pool是按照二叉樹結構組織的,并采用"child-sibling"的鏈式存儲方式,global_pool作為整個樹的Root Node。
? ? ? ? ? ? ? ? ?3.從pool中分配內存
? ? ? ? ? ? ? ? ? ? 上面我們已經擁有了一個sub_pool,我們現在就可以從sub_pool中分配內存了。APR提供了函數apr_palloc來做這件事情。
? ? ? ? ? ? ? ? ? ? ? 例如:apr_alloc(sub_pool,wanted_mem_size);
? ? ? ? ? ? ? ? ? ? ? ?apr_palloc在真正分配內存前會把wanted_mem_size做一下處理。它使用APR_ALIGN_DEFAULT宏處理wanted_mem_size得到一個圓整到8的new_size,然后再在pool中分配new_size大小的內存,也就是說pool中存在的用戶內存塊的大小都是8的倍數。舉個例子,如果wanted_mem_size=30,apr_alloc實際會在pool中劃分出32個字節的空間。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? apr_palloc的工作流程簡單描述如下:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?a) 如果在pool->active node的avail space足夠滿足要申請的內存大小size時,則直接返回active->first_avail,并調整active->first_avail= active->first_avail+size;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?b)如果a)不滿足,則查看active->next這個node滿足與否;如果滿足則將返回所要內存,并將該node設為active node,將以前的active node放在新active node的next位置上;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? c)如果b)也不滿足,則新創建一個memnode,這個node可能為新創建的,也可能是從allocator的free memnode池中取出的,取決于當時整個Pool的狀態。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 從上面我們也可以看出node分為2類,一種是作為pool的承載體,但pool結構的空間不足以完全占滿一個node,所以也可以用來分配用戶內存;另一種就是完全用于分配用戶內存的了。每個pool有一個node list,當然這個list中包括它自己所在的node了。
? ? ? ? ? ? ? ? ? 4.apr_pool_clear和apr_pool_destroy
? ? ? ? ? ? ? ? ? ? 創建和分配結束后,我們需要clear或者destroy掉Pool。
? ? ? ? ? ? ? ? ? ? clear和destroy的區別在于clear并不真正free內存,只是清理便于以后alloc時重用,而destroy則是真正的free掉內存了。
? 5.4 APR分析-信號篇
? ? ? ? ?信號是Unix的重要系統機制。
? ? ? ? ?一、信號介紹
? ? ? ? ? ? ? ?1.Signal的引入用來進行User Mode進程間的交互,系統內核也可以利用它通知User Mode進程發生了哪些系統事件。從最開始引入到現在,信號只是做了很小的一些改動(不可靠信號模型到可靠信號模型).
? ? ? ? ? ? ? ? ?2.信號服務于兩個目的:
? ? ? ? ? ? ? ? ? ?1) 通知某進程某特定事件發生了;
? ? ? ? ? ? ? ? ? ?2) 強制其通知進程執行相應的信號處理程序。
? ? ? ? ? 二、基礎概念
? ? ? ? ? ? ? ? ? 1.信號的一個特性就是可以在任何時候發給某一進程,而無需知道該進程的狀態。如果該進程當前并未處于執行態,則該信號被內核Save起來,知道該進程恢復執行才傳遞給它;如果一個信號被進程設置為阻塞,則該信號的傳遞被延遲,直到其阻塞被取消它才被傳遞給進程。
? ? ? ? ? ? ? ? ? ?2.系統內核嚴格區分信號傳送的兩個階段:
? ? ? ? ? ? ? ? ? ? ?1)Signal Generation: 系統內核更新目標進程描述結構來表示一個信號已經發送出去。
? ? ? ? ? ? ? ? ? ? ?2)Signal Delivery:內核強制目標進程對信號做出反應,或執行相關信號處理函數,或改變進程執行狀態。
? ? ? ? ? ? ? ? ? ? ?信號的誕生和傳輸我們可以這樣理解:把信號作為"消費品",其Generation狀態就是"消費品誕生",其Delivery狀態就是理解為"被消費了"。這樣勢必存在這樣的一個情況:"消費品誕生了,但是還沒有被消費掉",在信號模型中,這樣的狀態被稱為"pending"(懸而未決)。
? ? ? ? ? ? ? ? ? ? ? 任何時候一個進程只能有一個這樣的某類型的pending信號,同一進程的其他同類型的pending信號將不排隊,將被簡單的discard(丟棄)掉。
? ? ? ? ? ? ? ? ? ?3.如何消費一個signal
? ? ? ? ? ? ? ? ? ? ? 1) 忽略該信號;
? ? ? ? ? ? ? ? ? ? ? 2)響應該信號,執行一特定的信號處理函數;
? ? ? ? ? ? ? ? ? ? ? 3)響應該信號,執行系統默認的處理函數。包括:Terminate、Dump、Ignore、Stop、Continue等。
? ? ? ? ? ? ? ? ? ? ? 這里有特殊:SIGKILL和SIGSTOP兩個信號不能忽略、不能捕捉、不能阻塞,而只是執行系統默認處理函數。
? ? ? ? ? ?三、APR Signal封裝
? ? ? ? ? ? ? ? ?APR Signal源代碼的位置在$(APR_HOME)/threadproc目錄下,本篇blog著重分析unix子目錄下的signals.c文件內容,其相應頭文件為$(APR_HOME)/include/apr_signal.h。
? ? ? ? ? ? ? ? ? ?1.apr_signal函數
? ? ? ? ? ? ? ? ? ? ? ?早期版本處理方式:進程每次處理信號后,隨機將信號的處理動作重置為默認值。
? ? ? ? ? ? ? ? ? ? ? ?后期版本處理方式:進程每次處理信號后,信號的處理動作不被重置為默認值。
? ? ? ? ? ? ? ? ? ? ? ?我們舉例測試一下:分別在Solaris9、Cygwin和RedHat Linux 9上。
? ? ? ? ? ? ? ? ? ? ? ?例子:
? ? ? ? ? ? ? ? ? ? ? ?eg 1:
void siguser1_handler(int sig); int main(void) {if (signal(SIGUSR1, siguser1_handler) == SIG_ERR) {perror("siguser1_handler error");exit(1);}while(1) {pause();} } void siguser1_handler(int sig) {printf("in siguser1_handler,%d/n", sig); } input:kill -USR1 9122kill -USR1 9122 output(Solaris 9):in siguser1_handler, 16用戶信號1(程序終止) output:(Cygwin and RH9):in siguser1_handler, 30in siguser1_handler, 30.....? ? ? ? ? ? ? ? ? ? ? ? ? eg.1結果表示在Solaris 9上,信號的處理仍然按照早期版本的方式,而Cygwin和RH9則都按照后期版本的方式。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?那么有什么替代signal函數的辦法么?在最新的X/Open和UNIXspecifications中都推薦使用一個新的信號接口sigaction,該接口采用后期版本的信號處理方式。在《Unix高級環境編程》中就有使用sigaction實現signal的方法,而APR恰恰也是使用了該方法實現了apr_signal。其代碼如下:
APR_DECLARE(apr_sigfunc_t *) apr_signal(int signo, apr_sigfunc_t *func) {struct sigaction act, oact;act.sa_handler = func;sigemptyset(&act.sa_mask); ----(1)act.sa_flags=0; #ifdef SA_INTERRUPT /* SunOS */act.sa_flags |= SA_INTERRUPT; #endif... ...if (sigaction(signo, &act, &oact) <0) return SIG_ERR;return oact.sa_handler; }? ? ? ? ? ? ? ? ? ? ? ? (1)這里有一個Signal Set(信號集)的概念,通過相關函數操作信號集以改變內核傳遞信號給進程時的行為。Unix用sigset_t結構來表示信號集。信號集總是和sigprocmask或sigaction一起使用。
? ? ? ? ? ? ? ? ? ? ?2、apr_signal_block和apr_signal_unblock
? ? ? ? ? ? ? ? ? ? ? ? ? 這兩個函數分別負責阻塞和取消阻塞內核傳遞某信號給目標進程。其主要利用的就是sigprocmask函數來實現的。每個進程都有其對應的信號屏蔽字,它讓目標進程能夠通知內核"哪些傳給我的信號該阻塞,哪些暢通無阻"。
? ? ? ? ? ? ? ? ? ? ? ? ? ?這里想舉例說明的是:如果多次調用SET_BLOCK的sigprocmask設置屏蔽字,結果是什么呢?
? ? ? ? ? ? ? ? ? ? ? ? ?eg.3
int main(void) {sigset_t newmask, oldmask, pendmask;/* 設置進程信號屏蔽字,阻塞SIGQUIT */sigemptyset(&newmask);sigaddset(&newmask, SIGQUIT);if(sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {perror("SIG_BLOCK error");}printf("1st towait 30 seconds/n");sleep(30);/*第一次查看當前的處于pend狀態的信號*/if(sigpending(&pendmask) <0) {perror("sigpending error");}if (sigismember(&pendmask, SIGQUIT)) {printf("SIGQUIT pending /n");} else {printf("SIGQUIT unpending/n");}if (sigismember(&pendmask, SIGUSR1)) {printf("SIGUSR1 pending/n");} else {printf("SIGUSR1 unpending/n");}/*重新設置屏蔽字,阻塞SIGUSR1*/sigemptyset(&newmask);sigaddset(&newmask, SIGUSR1);if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {perror("SIG_BLOCK error");}printf("2nd to wait 30 seconds/n");sleep(30);/*再次查看當前的處于pend狀態的信號*/if (sigpending(&pendmask) < 0) {perror("sigpending error");}if (sigismember(&pendmask, SIGQUIT)) {printf("SIGQUIT pending/n");} else {printf("SIGQUIT unpending /n");}if (sigismember(&pendmask, SIGUSR1)) {printf("SIGUSR1 pending/n");} else {printf("SIGUSR1 unpending/n");}exit(0); }//output 1st to wait 30 seconds ^/ SIGQUIT pending SIGUSR1 unpending 2nd to wait 30 seconds --這之后發送kill -USR128821 SIGQUIT pending SIGUSR1 pending? ? ? ? ? ? ? ? ? ? ? ?第一次輸出SIGUSR1 unpending是因為并未發送USR1信號,所以自然為unpending狀態;我想說的是第二次重新sigprocmask時我們僅加入了SIGUSR1,并未顯示假如SIGQUIT,之后查看pending信號中SIGQUIT仍然為pending狀態,這說明兩次SET_BLOCK的sigprocmask調用是"或"的關系,第二次SET_BLOCK的sigprocmask調用不會將第一次SET_BLOCK的sigprocmask調用設置的阻塞信號變成非阻塞的。
? 5.5 APR分析-文件IO篇
? ? ? ? ?文件I/O在Unix下占據著非常重要的地位。APR就是本著這個思想對Unix文件I/O進行了再一次的抽象封裝,以提供更為強大和友善的文件I/O接口。
? ? ? ? ? APR File I/O源代碼的位置在$(APR_HOME)/file_io目錄下,本篇blog著重分析unix子目錄下的相關.c文件內容,其相應頭文件為$(APR_HOME)/include/apr_file_io.h和apr_file_info.h.
? ? ? ? ? ?一、APR File I/O介紹
? ? ? ? ? ? ? APR用了"不小的篇幅"來"描述"文件I/O,在$(APR_HOME)/file_io/unix目錄下,你會看到多個.c文件,每個.c都是一類文件I/O操作,比如:
? ? ? ? ? ? ? ? open.c --封裝了 文件的打開、關閉、改名和刪除等操作;
? ? ? ? ? ? ? ? readwrite.c -- 顧名思義,它里面包含了文件的讀寫操作;
? ? ? ? ? ? ? ? pipe.c -- 包含了pipe相關操作。
? ? ? ? ? ?二、基本APR I/O
? ? ? ? ? ? ? ?APR定義了apr_file_t類型來表示廣義的文件。先來看一下這個核心數據結構的"模樣":
/* in apr_arch_file_io.h */ struct apr_file_t {apr_pool_t *pool;int filedes;char *fname;apr_int32_t flags;int eof_hit;int is_pipe;apr_interval_time_t timeout;int buffered;enum {BLK_UNKNOWN, BLK_OFF, BLK_ON } blocking;int ungetchar; /* Last charprovided by an unget op.(-1=no char)*/ #ifndef WAITIO_USES_POLL/* if there is a timeout set, then this pollsetis used */apr_pollset_t *pollset; #endif/* Stuff for buffered mode */char *buffer;int bufpos; /*Read/Write position in buffer */unsigned long dataRead; /* a mount of valid data read into buffer */int direction; /*buffer being used for 0 = read, 1 = write */unsigned long filePtr; /*position in file of handle */ #if APR_HAS_THREADSstruct apr_thread_mutex_t *thlock; #endif };? ? ? ? ? ? 1.apr_file_open
? ? ? ? ? ? ? ?ANSI C標準庫和Unix系統庫函數都提供對"打開文件"這個操作語義的支持。他們提供的接口很相似,參數一般都為"文件名+打開標志位+權限標志位",apr_file_open也不能忽略習慣的巨大力量,也提供了類似的接口如下:
? ? ? ? ? ? ? ? APR_DECLARE(apr_status_t) apr_file_open(apr_file_t **new,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?const char *fname,?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?apr_int32_t flag,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?apr_fileperms_t perm,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?apr_pool_t *pool);
? ? ? ? ? ? ? ? ?每個封裝都有自定義的一些標志宏,這里也不例外,flag和perm參數都需要用戶傳入APR自定義的一些宏組合,這里介紹apr_file_open操作:
apr_file_open {"打開標志位"轉換; ----(1)"權限標志位"轉換; ----(2)調用Unix原生API打開文件;設置apr_file_t變量相關屬性值; ----(3) }? ? ? ? ? ? ? ? ? (1)由于上面說了,APR定義了自己的"文件打開標志位",所以在apr_file_open的開始需要將這些專有的"文件打開標志位"轉換為Unix平臺通用的"文件打開標志位";
? ? ? ? ? ? ? ? ? ?(2)同(1)理,專有的"權限標志位"需要轉換為Unix平臺通用的"權限標志位";
? ? ? ? ? ? ? ? ? (3) APR file I/O封裝支持非阻塞I/O帶超時等待以及緩沖I/O,默認情況下為阻塞的,是否緩沖可通過"文件打開標志位"設置。一旦設置為緩沖I/O,則apr_file_open會在pool中開辟大小為APR_FILE_BUFSIZE(4096)的緩沖區供使用。
? ? ? ? ? ? ? 2.apr_file_read/apr_file_write
? ? ? ? ? ? ? ? ?該兩個接口的看點是其緩沖區管理(前提:在apr_file_open該文件時指定了是Buffer I/O及非阻塞I/O 帶超時等待)。還有一點就是通過這兩個借口的實現我們可以了解到上面提到的apr_file_t 中某些"晦澀"字段的真正含義。
? ? ? ? ? ? ? ? ? (1) 帶緩沖I/O
? ? ? ? ? ? ? ? ? ? 這里的緩沖是APR自己管理的,帶緩沖的好處很簡單,即減少直接操作文件的次數,提高I/O性能。要知道無論lseek還是read/write都是很耗時的,盡可能的減少直接I/O操作次數,會帶來性能上明顯改善。
? ? ? ? ? ? ? ? ? ?讀寫切換:如果先讀后寫,則每次寫的時候都要重新定位文件指針到上次讀的結尾處;如果先寫后讀,則每次讀前都要flush緩沖區。
? ? ? ? ? ? ? ? ? ?(2) 非阻塞I/O帶超時等待
? ? ? ? ? ? ? ? ? ? ?這里分析下面一段apr_file_read的代碼:
do {rv = read(thefile->filedes, buf, *nbytes); }while(rv == -1 && errno == EINTR); ---(a) #ifdef USE_WAIT_FOR_IOif (rv == -1 &&(errno == EAGAIN || errno == EWOULDBLOCK) &&thefile->timeout != 0) {apr_status_t arv = apr_wait_for_io_or_timeout(thefile, NULL, 1); ----(b)if (arv != APR_SUCCESS) {*nbytes = bytes_read;return arv;}else {do {rv = rad(thefile->filedes, buf, *nbytes);}while (rv == -1 && errno == EINTR);}} #endif? ? ? ? ? ? ? ? ? ? (a) 第一個do-while塊:之所以使用do-while塊是為了當read操作被信號中斷后重啟read操作;
? ? ? ? ? ? ? ? ?(b) 一旦文件描述符設為非阻塞,(a)則瞬間返回,一旦(a)并未讀出數據,則rv = -1并且errno被設置為errno = EAGAIN,這時開始帶超時的等待該文件描述符I/O就緒。這里的apr_wait_for_io_or_timeout使用了I/O的多路 復用技術Poll,在后面的APR分析中會詳細理解之。apr_file_t中的timeout字段就是用來做超時等待的。
? ? ? ? ? ? ?3.apr_file_close
? ? ? ? ? ? ? ? 該接口主要完成的工作為刷新緩沖區、關閉文件描述符、刪除文件(如果設置了APR_DELONCLOSE標志位)和清理Pool中內存的工作。
5.6 APR分析-高級IO篇
? ? ?一、記錄鎖或(區域鎖)
? ? ? ? ? ? 我見過的對記錄鎖講解最詳細的書就是《Unix高級環境編程》,特別是關于進程、文件描述符和記錄鎖三者之間的關系的講解更是讓人受益匪淺。
? ? ? ? ? ? 關于記錄鎖的自動繼承和釋放有三條規則:
? ? ? ? ? ? (1) 鎖與進程、文件兩方面有關。這有兩重含義:第一重很明顯,當一個進程終止時,它所建立的鎖全部釋放;第二重意思就不很明顯,任何時候關閉一個描述符時,則該進程通過這一描述符可以存訪的文件上的任何一把鎖都被釋放(這些所都是該進程設置的)。
? ? ? ? ? ? ?(2) 由fork產生的子程序不繼承父進程所設置的鎖。這意味著,若一個進程得到一把鎖,然后調用fork,那么對于父進程獲得的鎖來說,子進程被視為另一個進程,對于從父進程處繼承過來的任一描述符,子進程要調用fcntl以獲得它自己的鎖。這與鎖的作用是一致的。鎖的作用是阻止多個進程同時寫同一個文件(或同一文件區域)。如果子進程繼承父進程的鎖,則父、子進程就可以同時寫同一個文件。
? ? ? ? ? ? ?(3) 在執行exec后,新程序可以繼承原執行程序的鎖。
? ? ? ? ? ? ? ? ? APR記錄鎖源碼位置在$(APR_HOME)/file_io/unix目錄下flock.c,頭文件仍然是apr_file_io.h。apr_file_lock和apr_file_unlock僅提供對整個文件的枷鎖和解鎖,而并不支持對文件中任意范圍數據的加鎖與解鎖。至于該鎖是建議鎖(advisory lock)還是強制鎖(mandatory lock),需要看具體的平臺實現了。兩個函數均利用fcntl實現記錄鎖功能。代碼中有一處值得借鑒:
while((rc = fcntl(thefile->filedes, fc, &l)) < 0 && errno == EINTR)continue;? ? ? ? ? ? ? ? ? ? ? ?這么做的原因就是考慮到fcntl的調用可能被某信號中斷,一旦中斷我們要去重啟fcntl函數。
? ? ? ? ?二、I/O多路復用
? ? ? ? ? ? ?在經典的《Unix網絡編程第1卷》 Chapter 6中作者詳細介紹了五種I/O模型,分別為:
? ? ? ? ? ? ?- blocking I/O
? ? ? ? ? ? ?- nonblocking I/O
? ? ? ? ? ? ?- I/O multiplexing (select and poll)
? ? ? ? ? ? ?- signal driven I/O(SIGIO)
? ? ? ? ? ? ?- asynchronous I/O (the POSIX aio_functions)
? ? ? ? ? ? ?這里所說的I/O多路復用就是第三種模型,它既解決了Blocking I/O數據處理不及時,又解決了Non-Blocking I/O采用輪詢的CPU浪費問題,同時它與異步I/O不同的是它得到了各大平臺的廣泛支持。
? ? ? ? ? ? ? APR I/O多路復用源碼主要在$(APR_HOME)/poll/unix目錄下的poll.c和select.c中,頭文件為apr_poll.h。APR提供統一的apr_poll接口,但是apr_pollset_t結構定義和apr_poll的實現則根據宏POLLSET_USES_SELECT、POLL_USES_POLL和POLLSET_USES_POLL的定義與否而不同。
? ? ? ? ? ? ? ?在poll的實現下,apr_pollset_t的定義如下:
/* in poll.c */ struct apr_pollset_t {apr_pool_t *pool;apr_uint32_t nelts;apr_uint32_t nalloc;struct pollfd *pollset;apr_pollfd_t *query_set;apr_pollfd_t *result_set; };? ? ? ? ? ? ? ? 統一的apr_pollfd_t定義如下:
/* in apr_poll.h */ struct apr_pollfd_t {apr_pool_t *p; /*associated pool*/apr_datatype_e desc_type; /*descriptor type*/apr_int16_treqevents; /*requested events*/apr_int16_trtnevents; /*returned events*/apr_descriptordesc; /* @see apr_descriptor*/void *client_data; /*allowsapp to associate context */ };? 5.7 APR分析-共享內存篇
? ? ? ? ? ? ?共享內存是一種重要的IPC方式。在項目中多次用到共享內存,只是用而并未深入研究。
? ? ? ? ?APR共享內存封裝的源代碼的位置在$(APR_HOME)shmem目錄下,本篇blog著重分析unix子目錄下的shm.c文件內容,其相應頭文件為$(APR_HOME)/include/apr_shm.h.
? ? ? ? ?一、共享內存簡單小結
? ? ? ? ? ? 共享內存時最快的IPC方式,因為一旦這樣的共享內存段映射到各個進程的地址空間,這些進程間通過共享內存的數據傳遞就不需要內核的幫忙了。Stevens的解釋是"各進程不是通過執行任何進入內核的系統調用來傳遞數據,顯然內核的責任僅僅是建立各進程地址空間與共享內存的映射,當然像處理頁面故障這一類的底層活還是要做的"。相比之下,管道和消息隊里交換數據時都需要內核來中轉數據,速度就相對較慢。
? ? ? ? ?二、APR共享內存封裝
? ? ? ? ? ? ? APR提供多種創建共享內存的方式,其中最主要的就是apr_shm_create接口,其偽碼如下:? ? ??
apr_shm_create {if (要創建匿名shm) { #if APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON #if APR_USE_SHMEM_MMAP_ZEROxxxx ---------(1) #elif APR_USE_SHMEM_MMAP_ANONxxxx ----------(2) #endif #endif /* APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON */ #if APr_USE_SHMEM_SHMGET_ANONxxxx ----------(3) #endif } else { /* 創建有名shm */ #if APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM #if APR_USE_SHMEM_MMAP_TMPxxxx --------(4) #endif #if APR_USE_SHMEM_MMAP_SHMxxxx ----------(5) #endif #endif /* APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM */ #if APR_USE_SHMEM_SHMGETxxxx -----------(6) #endif} }? ? ? ? ? ? ?其中不同版本Unix創建匿名shmem的做法如下:
? ? ? ? ? ? ?(1) SVR4通過映射"/dev/zero"設備文件來獲得匿名共享內存,其代碼一般為:
fd = open("/dev/zero", ..); ptr = mmap(..., MAP_SHARED, fd, ...);? ? ? ? ? ? ?(2) 4.4 BSD提供更加簡單的方式來支持匿名共享內存(注意標志參數MAP_XX)
ptr = mmap(..., MAP_SHARED | MAP_ANON, -1, ...);? ? ? ? ? ? ?(3) System V匿名共享內存區的做法如下:
shmid = shmget(IPC_PRIVATE, ...); ptr = shmat(shmid, ...);? ? ? ? ? ? ? ? 匿名共享內存一般都用于有親緣關系的進程間的數據通訊。由父進程創建共享內存,子進程自動繼承下來。由于是匿名,沒有親緣關系的進程是不能動態鏈接到該共享內存區的。
? ? ? ? ? ? ? 不同版本Unix創建有名shmem的做法如下:
? ? ? ? ? ? ? (4) 由于是有名的shmem,所以與匿名不同的地方在于用filename替代"/dev/zero"做映射。
fd = open(filename, ...); apr_file_trunc(...); ptr = mmap(..., MAP_SHARED, fd, ...);? ? ? ? ? ? ? (5) Posix共享內存的做法
fd = shm_open(filename, ...); apr_file_trunc(...); ptr = mmap(..., MAP_SHARED, fd, ...);? ? ? ? ? ? ? ? ? 值得注意的一點就是通過shm_open映射的共享內存可以供無親緣關系的進程共享。apr_file_trunc用于重新設定共享內存對象長度。
? ? ? ? ? ? ? ?(6) System V有名共享內存區的做法如下:
shmkey = ftok(filename, 1); shmid = shmget(shmkey, ...); //相當于open orshm_open ptr = shmat(shmid, ...); //相當于mmap? ? ? ? ? ? ? ?有名共享內存一般都與一個文件相關,該文件映射到共享內存段,而不同的進程(包括無親緣關系的進程)則都映射到該文件以達到目的。在APR中通過apr_shm_attach可以動態將調用進程連接到已存在的共享內存上,前提是你必須知道該共享內存區的標識,在APr中一律用filename做標識。
? ? ? ? 三、總結
? ? ? ? ? ? 內核架起了多個進程間共享數據的紐帶--共享內存。通過上面的敘述你會發現共享內存的創建其實并不困難,真正困難的是共享內存的管理,在正規的軟件公司像內存/共享內存管理這樣的重要底層功能都是封裝成庫形式的。
? ? ? ? 四、參考資料
? ? ? ? ? ? SIGSEGV和SIGBUS
? ? ? ? ? ? 涉及共享內存的管理就不能不提到訪問共享內存對象。談到訪問共享內存對象就要留神"SIGSEGV和SIGBUS"這兩個信號。
? ? ? ? ? ? ? 系統分配內存頁來承載內存映射區,由于內存頁大小是固定的,所以存在多余的頁空間空閑,比如待映射文件大小為5000 bytes,內存映射區大小也為5000bytes。而一個內存頁大小4096,系統勢必要分配兩頁來承載,這時空閑的有效空間為從5000-8191,如果進程訪問這段地址空間也不會發生錯誤。但是要超出8191,就會收到SIGSEGV信號,導致程序停止。關于SIGBUS信號的來歷,這里也舉例說明:若待映射文件大小為5000 bytes,我們在mmap時指定內存映射區size = 15000 > 5000,這時內核真正的共享區承載體大小只有8192(能包容映射文件大小即可),此時在[0, 8191]內訪問均沒問題,但在[8192,14999]之間會得到SIGBUS信號;超出15000訪問時會觸發SIGSEGV信號。
? 5.8 APR分析-環篇
? ? ? ? ? APR中少見對數據結構的封裝,好像唯一例外的就是其對循環鏈表,即環(RING)的封裝。
? ? ? ? ? 簡單說說環(RING):環是一個首尾相連的雙線鏈表,也就是我們所說的循環鏈表。
? ? ? ? ? 1.如何使用APR RING?
假設環節點的結構如下: struct elem_t { /* APR RING鏈接的元素類型定義 */APR_RING_ENTRY(elem_t) link; /*鏈接域*/int foo; /*數據域*/ }; APR_RING_HEAD(elem_head_t, elem_t); int main() {struct elem_head_t head;struct elem_t *el;APR_RING_INIT(&head, elem_t, link);/* 使用其他操作宏插入、刪除等操作,例如*/el = malloc(sizeof(elem_t);el->foo = 20051103;APR_RING_ELEM_INIT(el, link);APR_RING_INSERT_TAIL(&h, el, elem_t, link); }? ? ? ? ? ? 2.APR RING的難點---"哨兵"
? ? ? ? ? ?環是通過頭節點來管理的,頭節點是這樣一種節點,其next指針指向RING的第一個節點,其prev指針指向RING的最后一個節點,即尾節點。但是通過查看源碼發現APR RING通過APR_RING_HEAD宏定義的頭節點形式如下:
#define APR_RING_HEAD(head, elem) /struct head { /struct elem *next; /struct elem *prev; /}? ? ? ? ? ? ?如果按照上面的例子進行宏展開,其形式如下:
struct elem_head_t {struct elem_t *next;struct elem_t *prev; };? ? ? ? ? ? ? 而一個普通的元素elem_t 展開形式如下:
struct elem_t {struct { /struct elem_t *next; /struct elem_t *prev; /} link;int foo; };? ? ? ? ? ? ? 通過對比可以看出頭節點僅僅相當于一個elem_t的link域。這樣做的話必然帶來對普通節點和頭節點在處理上的不一致,為了避免這種情況的發生,APR RING引入了"哨兵"節點的概念。我們先看看哨兵節點在整個鏈表中的位置。
? ? ? ? ? ? ? ? sentinel->next = 鏈表的第一個節點;
? ? ? ? ? ? ? ? sentinel->prev = 鏈表的最后一個節點;
? ? ? ? ? ? ? ? 但是查看APR RING的源碼你會發現sentinel節點只是個虛擬存在的節點,這個虛擬節點既有數據域(虛擬出來的,不能引用)又有鏈接域,好似與普通節點并無差別。
? ? ? ? ? ? ? ? ?再看看下面APR_RING_INIT的源代碼:
#define APR_RING_INIT(hp, elem, link) do{ /APR_RING_FIRST((hp)) = APR_RING_SENTINEL((hp), elem, link); /APR_RING_LAST((hp)) = APR_RING_SENTINEL((hp), elem, link); / }while(0)? ? ? ? ? ? ? ? ? 你會發現:初始化RING實際上是將head的next和prev指針都指向了sentinel虛擬節點了。從sentinel的角度來說相當于其自己的link域的next和prev都指向了自己。所以判斷APR RING是否為空只需要判斷RING的首個節點是否為sentinel虛擬節點即可。APR_RING_EMPTRY宏就是這么做的:
#define APR_RING_EMPTY(hp, elem, link) /(APR_RING_FIRST((hp)) == APR_RING_SENTINEL((hp), elem, link))? ? ? ? ? ? ? ? ? ?那么如何計算sentinel虛擬節點的地址呢?
? ? ? ? ? ? ? ? ? ?我們這樣思考:從普通節點說起,如果我們知道一個普通節點的首地址(elem_addr),那么我們計算其link域的地址(link_addr)的公式就應該為link_addr=elem_addr + offsetof(elem_t, link);前面我們一直在說sentinel虛擬節點看起來和普通節點沒什么區別,所以它仍然符合該計算公式。前面我們又說過head_addr是sentinel節點的link域,這樣的話我們將head_addr輸入到公式中得到head_addr = sentinel_addr + offsetof(elem_t, link),做一下變換即可得到sentinel_addr = head_addr - offsetof(elem_t, link)??纯碅PR RING源代碼就是這樣實現的:
#define APR_RING_SENTINEL(hp, elem, link) /(struct elem *)((char *)(hp) - APR_OFFSETOF(struct elem, link))? ? ? ? ? ? ? ? ? ? ?至此APR RING使用一個虛擬sentinel節點分隔RING的首尾節點,已達到對節點操作一致的目的。
? ? ? ? ? ? ? ? ?3、APR RING不足之處
? ? ? ? ? ? ? ? ? ? 1)缺少遍歷接口
? ? ? ? ? ? ? ? ? ? ? ?瀏覽APR RING源碼后發現缺少一個遍歷宏接口,這里提供一種正向遍歷實現:
#define APR_RING_TRAVERSE(ep, hp, elem, link) /for ((ep) = APR_RING_FIRST((hp)); /(ep) != APR_RING_SENTINEL((hp), elem, link); /(ep) = APR_RING_NEXT((ep), link))? 5.9 APR分析-進程同步篇
- 最新的統計數據顯示Apache服務器在全世界仍然占據著Web服務器龍頭老大的位置,而且市場占有率遙遙領先,所以學習Apache相關知識是完全正確的方向,這里我們繼續分析APR進程同步相關內容。
? ? ? ? ?進程同步的源代碼的位置在$(APR_HOME)/locks目錄下,本篇blog著重分析unix子目錄下的proc_mutex.c、global_mutex文件內容,其相應頭文件為$(APR_HOME)/include/apr_proc_mutex.h、apr_global_mutex.h。其用于不同進程之間的同步以及多進程多線程中的同步問題。
? ? ? ? ? ? ?apr_thread_mutex_t - 支持單個進程內的多線程同步;
? ? ? ? ? ? ?apr_proc_mutex_t - 支持多個進程間的同步;
? ? ? ? ? ? ?apr_global_mutex_t - 支持不同進程內的不同線程間同步。
? ? ? ? ? ? ?在本篇中著重分析apr_proc_mutex_t。
? ? ? ? 1.同步機制
? ? ? ? ? APR提供多種進程同步的機制供選擇使用。在apr_proc_mutex.h中列舉了如下同步機制:
typedef enum {APR_LOCK_FCNTL, /*記錄上鎖*/APR_LOCK_FLOCK, /* 文件上鎖*/APR_LOCK_SYSVSEM, /*系統V信號量*/APR_LOCK_PROC_PTHREAD, /* 利用pthread線程鎖特性*/APR_LOCK_POSIXSEM, /*POSIX信號量*/APr_LOCK_DEFAULT /*默認進程間鎖*/ } apr_lockmech_e;? ? ? ? ?2.實現點滴
? ? ? ? ? ?APR提供每種同步機制的實現,每種機制體現為一組函數接口,這些接口被封裝在一個結構體類型中:
/* in apr_arch_proc_mutex.h */ struct apr_proc_mutex_unix_lock_methods_t {unsigned int flags;apr_status_t (*create)(apr_proc_mutex_t *, const char *);apr_status_t (*acquire)(apr_proc_mutex_t *);apr_status_t (*tryacquire)(apr_proc_mutex_t *);apr_status_t (*release)(apr_proc_mutex_t *);apr_status_t (*cleanup)(void *);apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *);const char *name; };? ? ? ? ? ?之后在apr_proc_mutex_t類型中,apr_proc_mutex_unix_lock_methods_t的出現也就在清理之中了
/* in apr_arch_proc_mutex.h */ struct apr_proc_mutex_t {apr_pool_t *pool;const apr_proc_mutex_unix_lock_methods_t *meth;const apr_proc_mutex_unix_lock_methods_t *inter_meth;int curr_locked;char *fname;... ... #if APR_HAS_PROC_PTHREAD_SERIALIZEpthread_mutex_t *pthread_interproc; #endif };? ? ? ? ?這樣APR提供的用戶接口其實就是對mech各個"成員函數"功能的"薄封裝",而真正干活的其實是apr_proc_mutex_t中的meth字段的"成員函數",它們的工作包括mutex的創建、獲取(加鎖)和清除(解鎖)等。以"獲取鎖"為例APR的實現如下:
APR_DECLARE(apr_status_t) apr_proc_mutex_lock(apr_proc_mutex_t *mutex) {return mutex->meth->acquire(mutex); }? ? ? ? ?3.同步機制
? ? ? ? ? 按照枚舉類型apr_lockmech_e的聲明,我們知道APR為我們提供了5中同步機制,下面分別說說:
? ? ? ? ? (1) 記錄鎖
? ? ? ? ? ? ?記錄鎖是一種建議性鎖,它不能防止一個進程寫已由另一個進程上了讀鎖的文件,它主要利用fcntl系統調用來完成鎖功能的,記得在以前的一篇關于APR文件I/O的Blog中談過記錄鎖,這里不再詳細敘述了。
? ? ? ? ? (2) 文件鎖
? ? ? ? ? ? ? 文件鎖是記錄鎖的一個特例,其功能由函數接口flock支持。值得說明的是它僅僅提供"寫入鎖"(獨占鎖),而不提供"讀入鎖"(共享鎖)。
? ? ? ? ? ?(3) System V信號量
? ? ? ? ? ? ? System V信號量是一種內核維護的信號量,所以我們只需調用semget獲取一個System V信號量的描述符即可。值得注意的是與POSIX的單個"計數信號量"不同的是System V信號量是一個"計數信號量集"。? ?
? ? ? ? ? ?(4) 利用線程互斥鎖機制
? ? ? ? ? ? ? APR使用pthread提供的互斥鎖機制。原本pthread互斥鎖是用來互斥一個進程內的各個現成的,但APR在共享內存中創建了pthread_mutex_t,這樣使得不同進程的主線程實現互斥,從而達到進程間互斥的目的。截取部分代碼如下:
new_mutex->pthread_interproc = (pthread_mutex_t *)mmap((caddr_t)0,sizeof(pthread_mutex_t),PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);? ? ? ? ? ? (5) POSIX信號量
? ? ? ? ? ? ? ?APR使用了POSIX有名信號量機制,下面代碼舉例說明:
/* in proc_mutex.c */ apr_snprintf(semname, sizeof(semname), "/ApR.%lxZ%lx", sec, usec); /* APR自定義一種POSIX信號量命名規則*/ psem =sem_open(semname, O_CREAT, 0644, 1);? ? ? ? ? ?4.如何使用
? ? ? ? ? ? ?我們知道父進程的鎖其子進程并不繼承。APR進程同步機制的一個典型使用方法就是:"Create the mutex in the Parent, Attach to in the Child"。APR提供接口apr_proc_mutex_child_init在子進程中reopen themutex。
? 5.10 APR分析-線程篇
? ? ? ? ? 并行一直是程序設計領域的難點,而線程是并行的一種重要的手段,而且現成的一些特性也能在進程并行時發揮很好的作用(“在線程同步篇”中詳細闡述)。
? ? ? ? APR線程的源代碼的位置在$(APR_HOME)/threadproc目錄下,本篇blog著重分析unix子目錄下的thread.c文件內容,其相應頭文件為$(APR_HOME)/include/apr_threadproc.h。
? ? ? ? ? 一、線程基礎
? ? ? ? ? ? ? (1) 在傳統觀點中,進程是由存儲于用戶虛擬內存中的代碼、數據和棧,以及由內核維護的"進程上下文"組成的,其中"進程上下文"又可以看成"程序上下文"和"內核上下文"組成,可參見下圖所示:
進程--|- 進程上下文|-程序上下文|-數據寄存器|-條件碼|-棧指針|-程序計數器|- 內核上下文|- 進程ID|- VM結構|- Open files|- 已設置的信號處理函數|- brk pointer|- 代碼、數據和棧(在虛存中)|- 棧區<--SP|- 共享庫區|- 運行時堆區 <--brk|- 可讀/寫數據區|- 只讀代碼/數據區 <--PC? ? ? ? ? ? ? ?(2) 另一種觀點中,進程是由線程、代碼和數據以及內核上下文組成的。下圖更能直觀的展示出兩種觀點的異同:
進程--+|- 線程|- 棧區 <--SP|- 線程上下文|- 線程ID|- 數據寄存器|- 條件碼|- 棧指針|- 程序計數器|- 內核上下文|- 進程ID|- VM結構|- Open files|-已設置的信號處理函數|- brk pointer|- 代碼、數據(在虛存中)|- 共享庫區|- 運行時堆區 <-- brk|- 可讀/寫數據區|- 只讀代碼/數據區 <--PC? ? ? ? ? ? ? ?對比兩種觀點我們可以得出以下幾點結論:
? ? ? ? ? ? ?(a) 從觀點(2)可以看出進程內的多個線程共享進程的內核上下文和代碼、數據(當然不包括棧區);
? ? ? ? ? ? ?(b) 線程上下文比進程上下文小,且切換代價小;
? ? ? ? ? ? ?(c) 線程不像進程那樣有著"父-子"體系,同一個進程內的線程都是"對等的",主線程與其他線程不同之處就在于其是進程創建的第一個線程。
? ? ? ? ?二、APR線程管理接口
? ? ? ? ? ? ?如今應用最廣泛的線程包就是PosixThread了。APR對線程的封裝也是基于Posix thread的。
? ? ? ? ? ? ?APR線程管理接口針對apr_thread_t 這個基本的數據結構進行操作,apr_thread_t的定義很簡單:
/* apr_arch_threadproc.h */ struct apr_thread_t {apr_pool_t *pool;pthread_t *td;void *data;apr_thread_start_t func;apr_status_t exitval; };? ? ? ? ? ? ?這個結構中包含了線程ID、線程函數以及該函數的參數數據。不過APR的線程函數定義與Pthread的有不同,“Pthread線程函數”是這樣的:
? ? ? ? ? typedef void *(start_routine)(void *);
? ? ? ? ? ?而"APR線程函數"如下:
? ? ? ? ? typedef void *(APR_THREAD_FUNC *apr_thread_start_t)(apr_thread_t *, void *);
? ? ? ? ?1.apr_thread_create
? ? ? ? ? ? apr_thread_create 內部定義了一個dummy_worker的"Pthread線程函數",并將apr_thread_t結構作為參數傳入,然后在dummy_worker中啟動"APR的線程函數"。在該函數的參數列表中有一項類型為apr_threadattr_t:
struct apr_threadattr_t {apr_pool_t *pool;pthread_attr_t attr; };? ? ? ? ? 2.apr_thread_exit
? ? ? ? ? ? ? 進程退出我們可以直接調用exit函數,而線程退出也有幾種方式:
? ? ? ? ? ? ?(1) 隱式退出 - 可以理解為線程main routine代碼結束返回;
? ? ? ? ? ? ?(2) 顯式退出 - 調用線程包提供的顯示退出接口,在apr中就是apr_thread_exit;
? ? ? ? ? ? ?(3) 另類顯式退出 - 調用exit函數,不僅自己退出,其所在線程也跟著退出了;
? ? ? ? ? ? ?(4)被"黑"退出 - 被別的"對等"線程調用pthread_cancel而被迫退出。
? ? ? ? ? ? ?apr_thread_exit屬于種類(2),該種類退出應該算是線程的優雅退出了。apr_thread_exit做了3個工作,分別為設置線程返回值、釋放pool中資源和調用pthread_exit退出。
? ? ? ? ? ? 3.apr_thread_join和apr_thread_detach
? ? ? ? ? ? ? 進程有waitpid,線程有join。線程在調用apr_thread_exit后,只是其執行停止了,其占有的"資源"并不一定釋放,這里的"資源"我想就是"另種觀點"中的"線程上下文",線程有兩種方式來釋放該"資源",這主要由現成的"可分離"屬性決定的。如果線程是"可分離的",當線程退出后會自動釋放其"資源",如果線程為"非可分離的",則必須由"對等線程"調用join接口來釋放其資源。apr_thread_detach用來將其調用線程轉化為"可分離"線程,而apr_thread_join用來等待某個線程結束并釋放其資源。
? 5.11 APR分析-網絡IO篇
? ? ? ? ?APR網絡I/O的源代碼的位置在$(APR_HOME)/network_io目錄下,本篇blog著重分析unix子目錄下的各.c文件內容,其相應頭文件為$(APR_HOME)/include/apr_network_io.h。
? ? ? ?一、IP地址 -- 主機通信
? ? ? ? ? ?我們熟知的并且每天工作于其上的因特網是一個世界范圍的主機的集合,這個主機結合被映射為一個32位(目前)或者64位(將來)IP地址;而IP地址又被映射為一組因特網域名;一個網絡中的主機上的進程能通過一個連接(connection)和任何其他網絡中的主機上的進程通信。
? ? ? ? ? 1.IP地址存儲
? ? ? ? ? ? ?在如今的IPv4協議中我們一般使用一個unsigned int來存儲IP地址,在UNIX平臺下,使用如下結構來存儲一個IP地址的值:
/* Internet address structure */ struct in_addr {unsigned int s_addr; /* network byte order(big-endian) */ };? ? ? ? ? ? ?這里值得一提的是APR關于IP地址存儲的做法,看如下代碼:
#if (!APR_HAVE_IN_ADDR) /*We need to make sure we always have an in_addr type, so APR will just define it ourselves, if the platform doesn't provide it. */ struct in_addr {apr_uint32_t s_addr; }; #endif? ? ? ? ? ? ? APR保證了其所在平臺上in_addr的存在。在in_addr中,s_addr是以網絡字節序存儲的。如果你的IP地址不符合條件,可通過調用一些輔助接口來做轉換,這些接口包括:
htonl: host to network long; htons: host to network short; ntohl: network to host long; ntohs: network to host short.? ? ? ? ? ? ? 2.IP地址表示
? ? ? ? ? ? ? ?我們平時看到的IP地址都是類似"xxx.xxx.xxx.xxx"這樣的點分十進制的。上面說過IP地址使用的是一個unsigned int整形數來表示。這樣就存在著一個IP地址表示和IP地址存儲之間的一個轉換過程。APR提供這一轉換支持,我們用一個例子來說明:
#include <apr.h> #include <apr_general.h> #include "apr_network_io.h" #include "apr_arch_networkio.h" int main(int argc, const char *const *argv, const char *const *env) {apr_app_initialize(&argc, &argv, &env);char presentation[100];int networkfmt;memset(presentation, 0, sizeof(presentation));apr_inet_pton(AF_INET, "255.255.255.255", &networkfmt);printf("0x%x/n", networkfmt);apr_inet_ntop(AF_INET, &networkfmt, presentation, sizeof(presentation));printf("presentation is %s/n", presentation);apr_terminate();return 0; }? ? ? ? ? ? ? ?APR提供apr_inet_pton將我們熟悉的點分十進制形式轉換成一個整形數存儲的IP地址;而apr_inet_ntop則將一個存整形數存儲的IP地址轉換為我們可讀的點分十進制形式。這兩個接口的功能類似于系統調用inet_pton和inet_ntop,至于使用哪個就看你的喜好了。
? ? ? ?二、SOCKET --進程通信
? ? ? ? ? ?1.SOCKET描述符
? ? ? ? ? ? ? 獲取SOCKET描述符:
int socket(int domain, int type, int protocol);? ? ? ? ? ? ? 從Unix程序的角度來看,SOCKET就是一個有相應描述符的打開的文件。在APR中我們可以通過調用apr_socket_create來創建一個APR自定義的SOCKET對象,該SOCKET結構如下:
/* apr_arch_networkio.h*/ struct apr_socket_t {apr_pool_t *cntxt;int socketdes;int type;int protocol;apr_sockaddr_t *local_addr;apr_sockaddr_t *remote_addr;apr_interval_time_t timeout; #ifdef HAVE_POLLint connected; #endifint local_port_unknown;int local_interface_unknown;int remote_addr_unknown;apr_int32_t options;apr_int32_t inherit;sock_userdata_t *userdata; #ifndef WAITIO_USES_POLL/* if there is a timeout set, then this pollset is used */apr_pollset_t *pollset; #endif };? ? ? ? ? ? ? ? ? ? ? ?該結構中的socketdes字段其實是真實存儲由socket函數返回的SOCKET描述符的,其他字段都是為APR自己所使用的,這些字段在Bind、Connect等過程中使用。我們如果不顯示將SOCKET描述符綁定到某SOCKET地址上,系統內核就會自動為該SOCKET描述符分配一個SOCKET地址。
? ? ? ? ? ? 2.SOCKET屬性
? ? ? ? ? ? ? ? 還是與文件對比,在文件系統調用中有一個fcntl接口可以從來獲取或設置已分配的文件描述符的屬性,如是否Block、是否Buffer等。SOCKET也提供類似的接口調用setsockopt和getsockopt。在APR中等價于該功能的接口時apr_socket_opt_set和apr_socket_opt_get。APR在apr_network_io.h中提供如下SOCKET的參數屬性:
#define APR_SO_LINGER 1 /*Linger*/ #define APR_SO_KEEPALIVE 2 /*Keepalive*/ #define APR_SO_DEBUG 4 /*Debug*/ #define APR_SO_NONBLOCK 8 /*Non-blocking IO*/ #define APR_SO_REUSEADDR 16 /*Reuse addresses*/ #define APR_SO_SNDBUF 64 /*Send buffer*/ #define APR_SO_DISCONNECTED 256 /*Disconnected*/ ......? ? ? ? ? ?另外從上面這些屬性值(都是2的n次方)可以看出SOCKET也是使用一個屬性控制字段中的"位"來控制SOCKET屬性的。
? ? ? ? ? ?再有APR提供一個宏apr_is_option_set來判斷一個SOCKET是否擁有某個屬性。
? ? ? ? ? ? 3.Connect、Bind、Listen、Accept-- 建立連接
? ? ? ? ? ? ? (1) apr_socket_connect
? ? ? ? ? ? ? ?客戶端連接服務器端的唯一調用就是connect,connect試圖建立一個客戶端進程與服務器端進程的連接。apr_socket_connect的參數分別為客戶端已經打開的一個SOCKET以及指定的服務器端的SOCKET地址(IP ADDR:PORT)。apr_socket_connect內部實現的流程大致如下:
apr_socket_connect {do {rc = connect(sock->socketdes,(const struct sockaddr *)&sa->sa.sin,sa->salen);} while (rc == -1 && errno == EINTR); ---(a)if ((rc == -1) && (errno == EINPROGRESS || errno == EALREADY)&& (sock->timeout > 0)) {rc = apr_wait_for_io_or_timeout(NULL, sock, 0); ----(b)if (rc != APR_SUCCESS) {return rc;}if (rc == -1 && errno != EISCONN) {return errno; ----(c)}初始化sock->remote_addr;... ... }? ? ? ? ? ? ? ? ? 對上述代碼進行若干說明:
? ? ? ? ? ? ? (a) 執行系統調用connect連接服務器端,注意這里做了防止信號中斷的處理。
? ? ? ? ? ? ? (b) 如果系統操作正在進行中,調用apr_wait_for_io_or_timeout進行超時等待;
? ? ? ? ? ? ? (c) 錯誤返回,前提errno不是表示已連接上。
? ? ? ? ? ? ? 一旦apr_socket_connect成功返回,我們就已經成功建立一個SOCKET對,即一個連接。
? ? ? ? ?(2) apr_socket_bind
? ? ? ? ? ? ? Bind、Listen和Accept這三個過程是服務器端用于接收"連接"的必經之路。其中Bind就是告訴操作系統內核顯示地位該SOCKET描述符分配一個SOCKET地址,這個SOCKET地址就不能被其他SOCKET描述符占用了。
? ? ? ? ?(3)apr_socket_listen
? ? ? ? ? ? ?SOCKET描述符在初始分配時都處于"主動連接"狀態,Listen過程將該SOCKET描述符從"主動連接"轉換為"被動狀態",并告訴內核接受該SOCKET描述符的連接請求。apr_socket_listen的背后直接就是listen接口調用。
? ? ? ? ? (4) apr_socket_accept
? ? ? ? ? ? ? Accept過程在"被動狀態"SOCKET描述符上接受一個客戶端的連接,這時系統內核會自動分配一個新的SOCKET描述符,內核為該描述符自動分配一個SOCKET地址,來代表這條連接的服務器端。注意在SOCKET編程接口中除了socket函數能分配新的SOCKET描述符之外,accept也是另外的一個也是唯一的一個能分配新的SOCKET描述符的系統調用了。apr_socket_accept首先在pool中分配一個新的apr_socket_t結構變量,然后調用accept,并設置新變量的各個字段。
? ? ? ? ? ? 4.Send/Recv --數據傳輸
? ? ? ? ? ? ? 網絡通信最重要的還是數據傳輸,在SOCKET編程接口中最常見的兩個接口就是recv和send。在APR中分別有apr_socket_recv和apr_socket_send與前面二者對應。下面逐一分析。
? ? ? ? ? ? ? ?(1) apr_socket_recv
? ? ? ? ? ? ? ? ?首先來看看apr_socket_recv的實現過程:
? ? ? ? ? ? ? ? ?
apr_socket_recv {if (上次調用apr_socket_recv沒有讀完所要求的的字節數) { ----------(a)設置sock->options;goto do_select;}do {rv = read(sock->socketdes, buf, (*len)); ------(b)} while (rv == -1 && errno == EINTR);if ((rv == -1) && (errno == EAGAIN || errno == EWOULDBLOCK) && (sock->timeout > 0)) { do_select:arv = apr_wait_for_io_or_timeout(NULL, sock, 1);if (arv != APR_SUCCESS) {*len = 0;return arv;}else {do {rv = rad(sock-》socketdes,buf, (*len));} while(rv == -1 && errno == EINTR);}} ---------(c)設置(*len)和sock->options; -----(d)... ... }? ? ? ? ? ? ? ? ? ? ? ?針對上面代碼進行說明:
? ? ? ? ? ? ? ? ? ? ? ?(a) 一次apr_socket_recv調用完全有可能沒有讀完所要求的字節數,這里做個判斷以決定是否繼續讀完剩下的數據;
? ? ? ? ? ? ? ? ? ? ? ? (b) 調用read讀取SOCKET緩沖區數據,注意這里做了防止信號中斷的處理。
? ? ? ? ? ? ? ? ? ? ? ? (c) 如果SOCKET操作正忙,我們調用apr_wait_for_io_or_timeout等待,直到SOCKET可用。
? ? ? ? ? ? ? ? ? ? ? ? (d) 將(*len)設置為實際從SOCKET Buffer中讀取的字節數,并根據這一實際數據與要求數據作比較來設置sock->options.
? ? ? ? ? ? ? (2) apr_socket_send
? ? ? ? ? ? ? ? ? ? apr_socket_send負責發送數據到SOCKET Buffer,其實現的方式與apr_socket_recv大同小異。
? ? ? ? ? ? ? ? ?
總結
以上是生活随笔為你收集整理的参考资料学习APR库的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 读“ModSecurity配置关键字说明
- 下一篇: 摘录理解LDAP与LDAP注入