参考资料学习APR库
APR分析-整體篇
? ? 一、何為APR?
? ? ? ? ?Apache Server經(jīng)過這么多年的發(fā)展后,將一些通用的運行時接口封裝起來供給大家,這就是Apache Portable Run-time libraries,APR。
? ? 二、APR的目錄組織
? ? ? ? ? 1)所有的頭文件都放在$(APR)/include目錄中;
? ? ? ? ? 2)所有功能接口的實現(xiàn)都放在各自的獨立目錄下,如threadproc、mmap等;
? ? ? ? ? 3)此外就是相關(guān)平臺構(gòu)建工具文件如Makefile.in等。曾經(jīng)看過ACE的代碼,ACE的所有源文件(.cpp)都放在一個目錄下,顯得很混亂。
? ? ? ? ? ?4)進入各功能接口子目錄,以threadproc為例,在其下面的子目錄有5個,分別是beos、netware、os2、unix和win32.
? ? 三、APR構(gòu)建
? ? ? ? ? ?如果想要使用APR,需要先在特定平臺上構(gòu)建它,這里不考慮多個平臺的特性,僅針對Unix平臺進行分析。
? ? ? ? ? ?1) apr.h、apr.h.in、apr.h.hw和apr.h.hnw的關(guān)系
? ? ? ? ? ? ? ?在$(APR)/include目錄下,由于APR考慮移植性等原因,最基本的apr.h文件是在構(gòu)建時自動生成的,其中apr.h.in類似一模板作為apr.h生成程序的輸入源。其中apr.h.hw和apr.h.hnw分別是Windows和NetWare的特定版本。
? ? ? ? ? ?2) 編譯時注意事項
? ? ? ? ? ? ? ? 在Unix上編譯時,注意$(APR)/build下*.sh文件的訪問權(quán)限,應(yīng)該先chmod以下,否則Make的時候會提示ERROR。
? ? 四、應(yīng)用APR
? ? ? ? ? ? 我們首先make install一下,比如我們在Makefile中指定prefix=$(APR)/dist,則make install后,在$(APR)/dist下會發(fā)現(xiàn)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文件內(nèi)容如下:
# # 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文件內(nèi)容如下: 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分析-設(shè)計篇
? ? ? ? 5.1.1 類型
? ? ? ? ? ? ?1) APR提供的節(jié)本自定義數(shù)據(jù)類型包括:
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的設(shè)計文檔中,它稱"dso、mmap、process、thread"等為"base types"。
? ? ? ? ? ? ?3) 另外的一個特點就是大多APR類型中都包含一個apr_pool_t類型的字段,你最好在該類型中假如一個apr_pool_t類型的字段,否則所有操作該類型的APR函數(shù)都需要一個apr_pool_t類型的參數(shù)。
? ? ? ?5.1.2 函數(shù)
? ? ? ? ? ? 1) APR的固定個數(shù)參數(shù)公共函數(shù)的聲明形式APR_DECLARE(rettype) apr_func(args);而非固定個數(shù)參數(shù)的公共函數(shù)的聲明形式為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"是一術(shù)語,叫"調(diào)用約定".常見的調(diào)用約定有:stdcall、cdecl、fastcall、thiscall和naked call,其中cdecl調(diào)用約定又稱為C調(diào)用約定,是C語言缺省的調(diào)用約定。
? ? ? ? ? ? ?2)如果你想新增APR函數(shù),APR建議你最好按照如下做:
? ? ? ? ? ? ? ? ?a)輸出參數(shù)為第一個參數(shù):
? ? ? ? ? ? ? ? ? b)如果某個函數(shù)需要內(nèi)部分配內(nèi)存,則將一個apr_pool_t 參數(shù)放在最后。
? ? ? ? ? 5.1.3 錯誤處理
? ? ? ? ? ? ? ? APR作為一通用的庫接口集合詳細的說明了使用APR時如何進行錯誤處理。
? ? ? ? ? ? ? ?1) 錯誤處理的第一步就是"錯誤碼和狀態(tài)碼分類"。APR的函數(shù)大部分都返回apr_status_t類型的錯誤碼,這是一個int型,在apr_errno.h中定義,和它在一起定義的還有apr所用的所有錯誤碼和狀態(tài)碼。
? ? ? ? ? ? ? ? 2) 如何定義錯誤捕捉策略?
? ? ? ? ? ? ? ? ? ?由于APR是可移植的,這樣就可能遇到這樣一個問題:不同平臺錯誤碼的不一致。如何處理呢?APR給我們提供了2中策略:
? ? ? ? ? ? ? ? ? ? ?a) 跨多平臺返回相同的錯誤碼
? ? ? ? ? ? ? ? ? ? ?b)返回平臺相關(guān)錯誤碼,如果需要將它轉(zhuǎn)換為通用錯誤碼
? ? ? ? ? ? ? ? ? ? ? ? ?程序的執(zhí)行錄像往往要根據(jù)函數(shù)返回錯誤碼來定,這么做的缺點就是把這些工作推給了程序員。執(zhí)行流程如下:
? ? ? ? ? ? ? ? ? ? ? ? ?make syscall that fails
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?return error code
? ? ? ? ? ? ? ? ? ? ? ?----------------------------------------------------------------
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? convert to common error code (using ap_canonical_error)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? decide execution based on common error code
? ? ? ? ? ? ? ? ? ? ///
? ? ? ? ? ? ? ? ? ? 注1 調(diào)用約定
? ? ? ? ? ? ? ? ? ? ? ? ? ?我們知道函數(shù)調(diào)用是通過棧操作來完成的,在棧操作過程中需要函數(shù)的調(diào)用者和被調(diào)用者在下面的兩個問題上作出協(xié)調(diào),達成協(xié)議:
? ? ? ? ? ? ? ? ? ? ? ? ? ?a) 當參數(shù)個數(shù)多余一個時,按照什么順序把參數(shù)壓入堆棧
? ? ? ? ? ? ? ? ? ? ? ? ? ?b) 函數(shù)調(diào)用后,由誰來把堆棧回復(fù)原來狀態(tài)
? ? ? ? ? ? ? ? ? ? ? ? ? ?在像C/C++這樣的中、高級語言中,使用"調(diào)用約定"來說明這兩個問題。
? ? ? ? ? ? ? ? ? ?///
? ?5.2 APR分析-進程篇
? ? ? ? ?Apache Server的進程調(diào)度一直為人所稱道,Apache 2.0推出的APR對進程進行了封裝,特別是Apache 2.0的MPM(Multiple Precess Management)框架就是以APR封裝的進程為基礎(chǔ)的。
? ? ? ? ? ? ?APR進程封裝源代碼的位置在$(APR_HOME)/threadproc目錄下,本篇blog著重分析unix子目錄下的proc.c文件內(nèi)容,其相應(yīng)頭文件為$(APR_HOME)/include/apr_thread_proc.h。
? ? ? ? ?一、APR進程概述
? ? ? ? ? ? ? APR進程封裝采用了傳統(tǒng)的fork-exec配合方式(spawn),即父進程在fork出子進程后繼續(xù)執(zhí)行其自己的代碼,而子進程調(diào)用exec函數(shù)加載新的程序影響到其地址空間,執(zhí)行新的程序。我們先來看看使用APR創(chuàng)建一個新的進程的流程,然后再根據(jù)流程做細節(jié)分析:
apr_proc_t newproc; apr_pool_t *p; apr_status_t rv; const char *args[2]; apr_procattr_t *attr; /* 初始化APR內(nèi)部使用的內(nèi)存*/ rv = apr_pool_initialize(); HANDLE_RTVAL(apr_pool_initialize, rv); rv = apr_pool_create(&p, NULL); HANDLE_RTVAL(apr_pool_create, rv); /*創(chuàng)建并初始化新進程的屬性*/ 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); ... .../*其他設(shè)置進程屬性的函數(shù)*/ /*創(chuàng)建新進程*/ args[0] = "proc_child"; args[1] = NULL; rv = apr_proc_create(&newproc,"your_progname", args, NULL, attr, p); HANDLE_RTVAL(apr_proc_create, rv); /*等待子進程結(jié)束*/ rv = apr_proc_wait(&newproc, NULL, NULL, APR_WAIT); HANDLE_RTVAL(apr_proc_wait, rv);? ? ? ? ? ?二、APR procattr創(chuàng)建
? ? ? ? ? ? ? ? ?在我們平時的Unix進程相關(guān)編程時,我們大致會接觸兩類進程操作函數(shù):進程創(chuàng)建函數(shù)(如fork和exec等)和進程屬性操作函數(shù)(getpid、chdir等),APR將進程的相關(guān)屬性信息封裝到apr_procattr_t結(jié)構(gòu)體中,我們來看看這個重要的結(jié)構(gòu)體定義:(這里只列出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]
? ? ? ? 在上一篇關(guān)于APR的blog中說過,大部分的APR類型中都會有一個apr_pool_t 類型字段,用于APR內(nèi)部的內(nèi)存管理,此結(jié)構(gòu)也無例外。該字段用來標識procattr在哪個pool中分配的內(nèi)存。
? ? ? ? [PART 2]
? ? ? ? 進程不是孤立存在的,進程也是由父有子的。父子進程間通過傳統(tǒng)的匿名pipe進行通信。在apr_procattr_io_set(attr,APR_FULL_BLOCK,APR_FULL_BLOCK,APR_FULL_BLOCK)調(diào)用后,我們可以用下面的圖來表示這些字段的狀態(tài):
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調(diào)用apr_file_pipe_create創(chuàng)建pipe的時候,為相應(yīng)的in/out字段注冊了cleanup函數(shù)apr_unix_file_cleanup, apr_unix_file_cleanup在相應(yīng)的in/out字段的pool銷毀時被調(diào)用,在后面的apr_proc_create時還會涉及到這塊。
? ? ? ? [PART 3]
? ? ? ? 進程的一些常規(guī)屬性。
? ? ? ? currdir標識新進程啟動時的工作路徑(執(zhí)行路徑),默認時為何父進程相同;
? ? ? ? cmdtype標識新的子進程將執(zhí)行什么類型的命令;共5種類型,默認為APR_PROGRAM
? ? ? ? detached標識新進程是否為分離后臺進程,默認為前臺進程。
? ? ? ? [PART 4]
? ? ? ? 這4個字段標識平臺對進程資源的限制,一般我們接觸不到。struct rlimit的定義在/usr/include/sys/resource.h中。
? ? ? ? [PART 5]?
? ? ? ? errfn為一函數(shù)指針,原型為typedef void (apr_child_errfn_t)(apr_pool_t *proc, apr_status_t err, const char *description);這個函數(shù)指針如果被賦值,那么當子進程遇到錯誤退出前將調(diào)用該函數(shù)。
? ? ? ? ? ? errchk一個標志值,用于告知apr_proc_create是否對子進程屬性進行檢查,如檢查curdir的access屬性等。
? ? ? ? [PART 6]
? ? ? ? ? ? 用戶ID和組ID,用于檢索允許該用戶所使用的權(quán)限。
? ? ? ? 三、APR proc創(chuàng)建
? ? ? ? ? ? APR proc的描述結(jié)構(gòu)為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;? ? ? ? ? ? 創(chuàng)建一個新的進程的接口為apr_proc_create,其參數(shù)也都很簡單。前面說過apr_proc_create先fork出一個子進程,眾所周知fork后子進程是父進程的復(fù)制品,然后子進程再通過exec函數(shù)加載新的程序映像,并開始執(zhí)行新的程序。這里分析一下apr_proc_create的執(zhí)行流程,其偽碼如下:
apr_proc_create {if (attr->errchk)對attr做有效性檢查,讓錯誤盡量發(fā)生在parentprocess中,而不是留給child process; ---(1)fork子進程;{ /*在子進程中*/清理一些不必要的從父進程繼承下來的描述符等,為exec提供一個"干凈的"的環(huán)境 ---(2)關(guān)閉attr->parent_int、parent_out和parent_err,并分別重定向attr->child_in、child_out和child_err為STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO; ---(3)判斷attr->cmdtype,選擇執(zhí)行exec函數(shù); --(4)}/* 在父進程中*/關(guān)閉attr->child_in、child_out和child_err; }? ? ? ?下面針對上述偽碼進行具體分析:
? ? ? ? 1)有效性檢查
? ? ? ? ? ? attr->errchk屬性可以通過apr_procattr_error_check_set函數(shù)在apr_proc_create之前設(shè)置。一旦設(shè)置,apr_proc_create就會在fork子進程前對procattr的有效性進行檢查,比如attr->curdir的訪問屬性(利用access檢查)、progname文件的訪問權(quán)限檢查等。這些的目的就是一個"讓錯誤發(fā)生在fork前,不要等到在子進程中出錯"。
? ? ? ? 2) 清理"不必要的"繼承物
? ? ? ? ? ? ? ? ? ?由于子進程復(fù)制了父進程的地址空間,隨之而來的還包含一些"不必要"的"垃圾"。為了給exec提供一個"干凈的"環(huán)境,在exec之前首先要做一下必要的清理,APR使用apr_pool_cleanup_for_exec來完成這項任務(wù)。apr_pool_cleanup_for_exec做了哪些工作呢?apr_pool_cleanup_for_exec通過pool內(nèi)部的global_pool搜索其子節(jié)點,并逐一遞歸cleanup,這里的cleanup并不釋放任何內(nèi)存,也不flushI/O Buffer,僅是調(diào)用節(jié)點注冊的相關(guān)cleanup函數(shù),這里我們可以回顧一下apr_procattr_io_set調(diào)用,在創(chuàng)建相關(guān)pipe時就為相應(yīng)的in/out/err描述符注冊了cleanup函數(shù)。同樣就是因為這點,子進程再調(diào)用apr_pool_cleanup_for_exec之前,首先要kill掉(這里理解就是去掉相關(guān)文件描述符上的cleanup注冊函數(shù))這些注冊函數(shù)。防止相關(guān)pipe的描述符被意外關(guān)閉。
? ? ? ? 3) 建立起與父進程"對話通道"
? ? ? ? ? ?父進程在創(chuàng)建procattr時就建立了若干個pipe,fork后子進程繼承了這些。為了關(guān)掉一些不必要的描述符和更好的和父進程通訊,子進程作了一些重定向的工作,這里用圖來表示重定向前后的差別:(圖中顯示的是子進程關(guān)閉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]
? ? ? ? ? ? ? ? ? ? -----------|
? ? ? ? ? ? ? ? ? ?為了能更好的體現(xiàn)出"對話通道"的概念,這里再畫出父進程再關(guān)閉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) 啟動新的程序
? ? ? ? ? ? 根據(jù)APR proc的設(shè)計,子進程在被fork出來后,將根據(jù)procattr的cmdtype等屬性信息決定調(diào)用哪種exec函數(shù)。當子進程調(diào)用一種exec函數(shù)時,子進程將完全由新程序代換,而新程序則從其main函數(shù)開始執(zhí)行(與fork不同,fork返回后子進程從fork點開始往下執(zhí)行)。因為調(diào)用exec并不創(chuàng)建新進程,所以前后的進程ID并未改變。exec只是用另一個新程序替換了當前進程的正文、數(shù)據(jù)、堆和棧段。
? ? ? ? 四、總結(jié)
? ? ? ? ? ?xx_in/xx_out都是相對于child process來說的,xx_in表示通過該描述符child process從in_pipe讀出parent process寫入in_pipe的數(shù)據(jù);xx_out表示通過該描述符child process將數(shù)據(jù)寫入out_pipe供parent process使用;xx_err則是child process將錯誤信息寫入err_pipe供parent process使用。
? ? ? ? ? ?fork后子進程和父進程的同和異
? ? ? ? ? ? ? 同:
? ? ? ? ? ? ? ?--父進程已經(jīng)打開的文件描述符;
? ? ? ? ? ? ? ?--實際用戶ID、實際組ID、有效用戶ID、有效組ID;
? ? ? ? ? ? ? ?--添加組ID;
? ? ? ? ? ? ? ?--進程組ID;
? ? ? ? ? ? ? ?--對話期ID;
? ? ? ? ? ? ? ?--控制終端;
? ? ? ? ? ? ? ?--設(shè)置用戶ID標志和設(shè)置組ID標志;
? ? ? ? ? ? ? ?--當前工作目錄;
? ? ? ? ? ? ? ?--根目錄;
? ? ? ? ? ? ? ?--文件方式創(chuàng)建屏蔽字;
? ? ? ? ? ? ? ?--信號屏蔽和排列;
? ? ? ? ? ? ? ?--對任一打開文件描述符的在執(zhí)行時關(guān)閉標志;
? ? ? ? ? ? ? ?--環(huán)境;
? ? ? ? ? ? ? ? --連接的共享存儲段;
? ? ? ? ? ? ? ? --資源限制.
? ? ? ? ? ? ? ?異:
? ? ? ? ? ? ? ? ?--fork的返回值;
? ? ? ? ? ? ? ? ?--進程ID;
? ? ? ? ? ? ? ? ? --不同的父進程ID;
? ? ? ? ? ? ? ? ?--子進程的tms_utime,tms_stime,tms_cutime以及tme_ustime設(shè)置為0;
? ? ? ? ? ? ? ? ?--父進程設(shè)置的鎖,子進程不繼承;
? ? ? ? ? ? ? ? ? --子進程的未決告警被清除;
? ? ? ? ? ? ? ? ? ?--子進程的未決信號集設(shè)置為空集
? 5.3 APR分析-內(nèi)存篇
? ? ? ? 內(nèi)存管理一直是讓C程序員頭痛的問題,作為一個通用接口集,APR當然也提供其自己的內(nèi)存管理接口--APR Pool。APR Pool作為整個APR的一個基礎(chǔ)功能接口,直接影響著APR的設(shè)計風格。
? ? ? ? APR Pool源代碼的位置在$(APR_HOME)/memory目錄下,本篇blog著重分析unix子目錄下的apr_pools.c文件內(nèi)容,其相應(yīng)頭文件為$(APR_HOME)/include/apr_pools.h;在apr_pools.c中還實現(xiàn)了負責APR內(nèi)部內(nèi)存分配的APRallocator的相關(guān)操作接口(APR allocator相關(guān)頭文件為$(APR_HOME)/include/apr_allocator.h)。
? ? ? ? 一、APR Pool概述
? ? ? ? ? ? ?我們平時常用的內(nèi)存管理方式都是基于"request-style"的,即分配所請求大小的內(nèi)存,使用之,銷毀之。而APR Pool的設(shè)計初衷是為Complex Application提供良好的內(nèi)存管理接口,其使用方式與"request-style"有所不同。而$(APR_HOME)/docs/pool-design.htm文檔中,設(shè)計者道出了"使用好"APR Pool的幾個Rules,同時也從側(cè)面反映出APRPool的設(shè)計。
? ? ? ? ? ? ?1.任何Object都不應(yīng)該有自己的Pool,它應(yīng)在其構(gòu)造函數(shù)的調(diào)用者的Pool中分配。因為一般調(diào)用者知道該Object的聲明周期,并通過Pool管理之。也就說Object無須自己調(diào)用"Close" or "Free",這些操作在Object所在Pool被摧毀時被隱式調(diào)用的。
? ? ? ? ? ? ? 2.函數(shù)無須為了他們的行為而去Create/Destroy Pool,它們應(yīng)該使用它們調(diào)用者傳給它們的Pool。
? ? ? ? ? ? ? 3.為了防止內(nèi)存無限制的增長,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的應(yīng)用程序一般都會調(diào)用apr_app_initalize來初始化APR的內(nèi)部使用的數(shù)據(jù)結(jié)構(gòu),查看一下app_app_initialize的代碼,你會發(fā)現(xiàn)apr_pool_initialize在被apr_app_initialize調(diào)用的apr_initialize中被調(diào)用,該函數(shù)用來初始化使用Pool所需的內(nèi)部結(jié)構(gòu)(用戶無須直接調(diào)用apr_pool_initialize,在apr_app_initialize時它被自動調(diào)用,而apr_app_initailize又是APR? program調(diào)用的第一個function,其在apr_general.h中聲明,在misc/unix/start.c中實現(xiàn))。
? ? ? ? ? ? ? ? ?apr_pool_initialize {
? ? ? ? ? ? ? ? ? ? ? 如果(!apr_pools_initialized) {
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?創(chuàng)建global_allocator;? ?------(1)
? ? ? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ? ? ? ? 創(chuàng)建global_pool; ------(2)
? ? ? ? ? ? ? ? ? ? ? ? 給global_pool起名為"apr_global_pool";
? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? ? ? ? (1) Pool和Allocator
? ? ? ? ? ? ? ? ? ? ? ? ? ?每個Pool都有一個allocator相伴,這個allocator可能是Pool自己的,也可能是其ParentPool的。allocator的結(jié)構(gòu)如下:
/* 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)調(diào)用后,global_allocator的所有xx_index字段都為0,owner-->NULL,free指針數(shù)組中的指針也都-->NULL。這里的index是大小的級別,這里最大級別為20(即MAX_INDEX=20),free指針數(shù)組中free[0]所指的node大小為MIN_ALLOC大小,即8192,即2的13次冪。按此類推free[19]所指的node大小應(yīng)為2的32次冪,即4G byte。allocator_alloc中是通過index=(size >> BOUNDARY_INDEX) - 1來得到這一index的。allocator維護了一個index不同的memnode池,每一index級別上又有一個memnode list,以后用戶調(diào)用apr_palloc分配size大小內(nèi)存時,allocator_alloc函數(shù)就會在free memnode池中選和要尋找的size的index級別相同的memnode,而不是重新malloc一個size大小的memnode。另外要說明一點的是APR Pool中所有ADT中的xx_index字段都是大小級別的概念。
? ? ? ? ? ?(2) 創(chuàng)建global_pool
? ? ? ? ? ? ? 在APR Pool初始化的時候,唯一創(chuàng)建一個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的結(jié)構(gòu)如下:
/* 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用于創(chuàng)建Pool,但由于global_allocator尚未分配過任何node,所以global_allocator創(chuàng)建一個新的node,該node大小為MIN_ALLOC(即8192),該node的當前狀態(tài)如下:
node-->|----------|0| || ||----------|APR_MEMNODE_T_SIZE <----node->first_avail| || || ||----------size(一般為8192)<---node->endp? ? ? ? ? ? ?其他屬性值如下:
? ? ? ? ? ? ?node->next = NULL;
? ? ? ? ? ? ?node->index = (APR_UINT32_TRUNC_CAST)index; /*這里為1*/
? ? ? ? ? ? ?創(chuàng)建完node后,我們將在該node上的avail space劃分出我們的global_pool來。劃分后狀態(tài)如下(pool與node關(guān)系):
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之間關(guān)系)如下:
pool->allocator=global_allocator; pool->child = NULL; pool->sibling = NULL; pool->ref = NULL;? ? ? ? ? ? ? ? ?2.APR Sub_Pool創(chuàng)建(pool與pool之間關(guān)系)
? ? ? ? ? ? ? ? ? 上面我們已經(jīng)初始化了global_pool,但是global_pool是不能直接拿來就用的,我們需要創(chuàng)建其sub_pool,也就是用戶自己的pool。一般創(chuàng)建user的sub_pool我們都使用apr_pool_create宏,它只需要2個參數(shù),并默認sub_pool繼承parent_pool的allocator和abort_fn。在apr_pool_create內(nèi)部調(diào)用的還是apr_pool_create_ex函數(shù)。我們來看一下創(chuàng)建sub_pool后pool之間的關(guān)系:
? ? ? ? ? ? ? ? ? 例:
? ? ? ? ? ? ? ? ? ?static apr_pool_t *sub_pool = NULL;
? ? ? ? ? ? ? ? ? ?apr_pool_create(&sub_pool, NULL);
? ? ? ? ? ? ? ? ? ?這里sub_pool的創(chuàng)建過程與global_pool相似,也是先創(chuàng)建其承載體node,然后設(shè)置相關(guān)屬性,使其成為global_pool的child_pool。創(chuàng)建完后global_pool和該sub_pool的關(guān)系如下圖:
global_pool <------/ ----->sub_pool ---------- // -------- sibling --->NULL /--------parent ---------- / -------- child--------------/ sibling--->NULL -------- ---------child--->NULL---------? ? ? ? ? ? ? ? ?APR Pool是按照二叉樹結(jié)構(gòu)組織的,并采用"child-sibling"的鏈式存儲方式,global_pool作為整個樹的Root Node。
? ? ? ? ? ? ? ? ?3.從pool中分配內(nèi)存
? ? ? ? ? ? ? ? ? ? 上面我們已經(jīng)擁有了一個sub_pool,我們現(xiàn)在就可以從sub_pool中分配內(nèi)存了。APR提供了函數(shù)apr_palloc來做這件事情。
? ? ? ? ? ? ? ? ? ? ? 例如:apr_alloc(sub_pool,wanted_mem_size);
? ? ? ? ? ? ? ? ? ? ? ?apr_palloc在真正分配內(nèi)存前會把wanted_mem_size做一下處理。它使用APR_ALIGN_DEFAULT宏處理wanted_mem_size得到一個圓整到8的new_size,然后再在pool中分配new_size大小的內(nèi)存,也就是說pool中存在的用戶內(nèi)存塊的大小都是8的倍數(shù)。舉個例子,如果wanted_mem_size=30,apr_alloc實際會在pool中劃分出32個字節(jié)的空間。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? apr_palloc的工作流程簡單描述如下:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?a) 如果在pool->active node的avail space足夠滿足要申請的內(nèi)存大小size時,則直接返回active->first_avail,并調(diào)整active->first_avail= active->first_avail+size;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?b)如果a)不滿足,則查看active->next這個node滿足與否;如果滿足則將返回所要內(nèi)存,并將該node設(shè)為active node,將以前的active node放在新active node的next位置上;
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? c)如果b)也不滿足,則新創(chuàng)建一個memnode,這個node可能為新創(chuàng)建的,也可能是從allocator的free memnode池中取出的,取決于當時整個Pool的狀態(tài)。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 從上面我們也可以看出node分為2類,一種是作為pool的承載體,但pool結(jié)構(gòu)的空間不足以完全占滿一個node,所以也可以用來分配用戶內(nèi)存;另一種就是完全用于分配用戶內(nèi)存的了。每個pool有一個node list,當然這個list中包括它自己所在的node了。
? ? ? ? ? ? ? ? ? 4.apr_pool_clear和apr_pool_destroy
? ? ? ? ? ? ? ? ? ? 創(chuàng)建和分配結(jié)束后,我們需要clear或者destroy掉Pool。
? ? ? ? ? ? ? ? ? ? clear和destroy的區(qū)別在于clear并不真正free內(nèi)存,只是清理便于以后alloc時重用,而destroy則是真正的free掉內(nèi)存了。
? 5.4 APR分析-信號篇
? ? ? ? ?信號是Unix的重要系統(tǒng)機制。
? ? ? ? ?一、信號介紹
? ? ? ? ? ? ? ?1.Signal的引入用來進行User Mode進程間的交互,系統(tǒng)內(nèi)核也可以利用它通知User Mode進程發(fā)生了哪些系統(tǒng)事件。從最開始引入到現(xiàn)在,信號只是做了很小的一些改動(不可靠信號模型到可靠信號模型).
? ? ? ? ? ? ? ? ?2.信號服務(wù)于兩個目的:
? ? ? ? ? ? ? ? ? ?1) 通知某進程某特定事件發(fā)生了;
? ? ? ? ? ? ? ? ? ?2) 強制其通知進程執(zhí)行相應(yīng)的信號處理程序。
? ? ? ? ? 二、基礎(chǔ)概念
? ? ? ? ? ? ? ? ? 1.信號的一個特性就是可以在任何時候發(fā)給某一進程,而無需知道該進程的狀態(tài)。如果該進程當前并未處于執(zhí)行態(tài),則該信號被內(nèi)核Save起來,知道該進程恢復(fù)執(zhí)行才傳遞給它;如果一個信號被進程設(shè)置為阻塞,則該信號的傳遞被延遲,直到其阻塞被取消它才被傳遞給進程。
? ? ? ? ? ? ? ? ? ?2.系統(tǒng)內(nèi)核嚴格區(qū)分信號傳送的兩個階段:
? ? ? ? ? ? ? ? ? ? ?1)Signal Generation: 系統(tǒng)內(nèi)核更新目標進程描述結(jié)構(gòu)來表示一個信號已經(jīng)發(fā)送出去。
? ? ? ? ? ? ? ? ? ? ?2)Signal Delivery:內(nèi)核強制目標進程對信號做出反應(yīng),或執(zhí)行相關(guān)信號處理函數(shù),或改變進程執(zhí)行狀態(tài)。
? ? ? ? ? ? ? ? ? ? ?信號的誕生和傳輸我們可以這樣理解:把信號作為"消費品",其Generation狀態(tài)就是"消費品誕生",其Delivery狀態(tài)就是理解為"被消費了"。這樣勢必存在這樣的一個情況:"消費品誕生了,但是還沒有被消費掉",在信號模型中,這樣的狀態(tài)被稱為"pending"(懸而未決)。
? ? ? ? ? ? ? ? ? ? ? 任何時候一個進程只能有一個這樣的某類型的pending信號,同一進程的其他同類型的pending信號將不排隊,將被簡單的discard(丟棄)掉。
? ? ? ? ? ? ? ? ? ?3.如何消費一個signal
? ? ? ? ? ? ? ? ? ? ? 1) 忽略該信號;
? ? ? ? ? ? ? ? ? ? ? 2)響應(yīng)該信號,執(zhí)行一特定的信號處理函數(shù);
? ? ? ? ? ? ? ? ? ? ? 3)響應(yīng)該信號,執(zhí)行系統(tǒng)默認的處理函數(shù)。包括:Terminate、Dump、Ignore、Stop、Continue等。
? ? ? ? ? ? ? ? ? ? ? 這里有特殊:SIGKILL和SIGSTOP兩個信號不能忽略、不能捕捉、不能阻塞,而只是執(zhí)行系統(tǒng)默認處理函數(shù)。
? ? ? ? ? ?三、APR Signal封裝
? ? ? ? ? ? ? ? ?APR Signal源代碼的位置在$(APR_HOME)/threadproc目錄下,本篇blog著重分析unix子目錄下的signals.c文件內(nèi)容,其相應(yīng)頭文件為$(APR_HOME)/include/apr_signal.h。
? ? ? ? ? ? ? ? ? ?1.apr_signal函數(shù)
? ? ? ? ? ? ? ? ? ? ? ?早期版本處理方式:進程每次處理信號后,隨機將信號的處理動作重置為默認值。
? ? ? ? ? ? ? ? ? ? ? ?后期版本處理方式:進程每次處理信號后,信號的處理動作不被重置為默認值。
? ? ? ? ? ? ? ? ? ? ? ?我們舉例測試一下:分別在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結(jié)果表示在Solaris 9上,信號的處理仍然按照早期版本的方式,而Cygwin和RH9則都按照后期版本的方式。
? ? ? ? ? ? ? ? ? ? ? ? ? ? ?那么有什么替代signal函數(shù)的辦法么?在最新的X/Open和UNIXspecifications中都推薦使用一個新的信號接口sigaction,該接口采用后期版本的信號處理方式。在《Unix高級環(huán)境編程》中就有使用sigaction實現(xiàn)signal的方法,而APR恰恰也是使用了該方法實現(xiàn)了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(信號集)的概念,通過相關(guān)函數(shù)操作信號集以改變內(nèi)核傳遞信號給進程時的行為。Unix用sigset_t結(jié)構(gòu)來表示信號集。信號集總是和sigprocmask或sigaction一起使用。
? ? ? ? ? ? ? ? ? ? ?2、apr_signal_block和apr_signal_unblock
? ? ? ? ? ? ? ? ? ? ? ? ? 這兩個函數(shù)分別負責阻塞和取消阻塞內(nèi)核傳遞某信號給目標進程。其主要利用的就是sigprocmask函數(shù)來實現(xiàn)的。每個進程都有其對應(yīng)的信號屏蔽字,它讓目標進程能夠通知內(nèi)核"哪些傳給我的信號該阻塞,哪些暢通無阻"。
? ? ? ? ? ? ? ? ? ? ? ? ? ?這里想舉例說明的是:如果多次調(diào)用SET_BLOCK的sigprocmask設(shè)置屏蔽字,結(jié)果是什么呢?
? ? ? ? ? ? ? ? ? ? ? ? ?eg.3
int main(void) {sigset_t newmask, oldmask, pendmask;/* 設(shè)置進程信號屏蔽字,阻塞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狀態(tài)的信號*/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");}/*重新設(shè)置屏蔽字,阻塞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狀態(tài)的信號*/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 --這之后發(fā)送kill -USR128821 SIGQUIT pending SIGUSR1 pending? ? ? ? ? ? ? ? ? ? ? ?第一次輸出SIGUSR1 unpending是因為并未發(fā)送USR1信號,所以自然為unpending狀態(tài);我想說的是第二次重新sigprocmask時我們僅加入了SIGUSR1,并未顯示假如SIGQUIT,之后查看pending信號中SIGQUIT仍然為pending狀態(tài),這說明兩次SET_BLOCK的sigprocmask調(diào)用是"或"的關(guān)系,第二次SET_BLOCK的sigprocmask調(diào)用不會將第一次SET_BLOCK的sigprocmask調(diào)用設(shè)置的阻塞信號變成非阻塞的。
? 5.5 APR分析-文件IO篇
? ? ? ? ?文件I/O在Unix下占據(jù)著非常重要的地位。APR就是本著這個思想對Unix文件I/O進行了再一次的抽象封裝,以提供更為強大和友善的文件I/O接口。
? ? ? ? ? APR File I/O源代碼的位置在$(APR_HOME)/file_io目錄下,本篇blog著重分析unix子目錄下的相關(guān).c文件內(nèi)容,其相應(yīng)頭文件為$(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 --封裝了 文件的打開、關(guān)閉、改名和刪除等操作;
? ? ? ? ? ? ? ? readwrite.c -- 顧名思義,它里面包含了文件的讀寫操作;
? ? ? ? ? ? ? ? pipe.c -- 包含了pipe相關(guān)操作。
? ? ? ? ? ?二、基本APR I/O
? ? ? ? ? ? ? ?APR定義了apr_file_t類型來表示廣義的文件。先來看一下這個核心數(shù)據(jù)結(jié)構(gòu)的"模樣":
/* 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系統(tǒng)庫函數(shù)都提供對"打開文件"這個操作語義的支持。他們提供的接口很相似,參數(shù)一般都為"文件名+打開標志位+權(quán)限標志位",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參數(shù)都需要用戶傳入APR自定義的一些宏組合,這里介紹apr_file_open操作:
apr_file_open {"打開標志位"轉(zhuǎn)換; ----(1)"權(quán)限標志位"轉(zhuǎn)換; ----(2)調(diào)用Unix原生API打開文件;設(shè)置apr_file_t變量相關(guān)屬性值; ----(3) }? ? ? ? ? ? ? ? ? (1)由于上面說了,APR定義了自己的"文件打開標志位",所以在apr_file_open的開始需要將這些專有的"文件打開標志位"轉(zhuǎn)換為Unix平臺通用的"文件打開標志位";
? ? ? ? ? ? ? ? ? ?(2)同(1)理,專有的"權(quán)限標志位"需要轉(zhuǎn)換為Unix平臺通用的"權(quán)限標志位";
? ? ? ? ? ? ? ? ? (3) APR file I/O封裝支持非阻塞I/O帶超時等待以及緩沖I/O,默認情況下為阻塞的,是否緩沖可通過"文件打開標志位"設(shè)置。一旦設(shè)置為緩沖I/O,則apr_file_open會在pool中開辟大小為APR_FILE_BUFSIZE(4096)的緩沖區(qū)供使用。
? ? ? ? ? ? ? 2.apr_file_read/apr_file_write
? ? ? ? ? ? ? ? ?該兩個接口的看點是其緩沖區(qū)管理(前提:在apr_file_open該文件時指定了是Buffer I/O及非阻塞I/O 帶超時等待)。還有一點就是通過這兩個借口的實現(xiàn)我們可以了解到上面提到的apr_file_t 中某些"晦澀"字段的真正含義。
? ? ? ? ? ? ? ? ? (1) 帶緩沖I/O
? ? ? ? ? ? ? ? ? ? 這里的緩沖是APR自己管理的,帶緩沖的好處很簡單,即減少直接操作文件的次數(shù),提高I/O性能。要知道無論lseek還是read/write都是很耗時的,盡可能的減少直接I/O操作次數(shù),會帶來性能上明顯改善。
? ? ? ? ? ? ? ? ? ?讀寫切換:如果先讀后寫,則每次寫的時候都要重新定位文件指針到上次讀的結(jié)尾處;如果先寫后讀,則每次讀前都要flush緩沖區(qū)。
? ? ? ? ? ? ? ? ? ?(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) 一旦文件描述符設(shè)為非阻塞,(a)則瞬間返回,一旦(a)并未讀出數(shù)據(jù),則rv = -1并且errno被設(shè)置為errno = EAGAIN,這時開始帶超時的等待該文件描述符I/O就緒。這里的apr_wait_for_io_or_timeout使用了I/O的多路 復(fù)用技術(shù)Poll,在后面的APR分析中會詳細理解之。apr_file_t中的timeout字段就是用來做超時等待的。
? ? ? ? ? ? ?3.apr_file_close
? ? ? ? ? ? ? ? 該接口主要完成的工作為刷新緩沖區(qū)、關(guān)閉文件描述符、刪除文件(如果設(shè)置了APR_DELONCLOSE標志位)和清理Pool中內(nèi)存的工作。
5.6 APR分析-高級IO篇
? ? ?一、記錄鎖或(區(qū)域鎖)
? ? ? ? ? ? 我見過的對記錄鎖講解最詳細的書就是《Unix高級環(huán)境編程》,特別是關(guān)于進程、文件描述符和記錄鎖三者之間的關(guān)系的講解更是讓人受益匪淺。
? ? ? ? ? ? 關(guān)于記錄鎖的自動繼承和釋放有三條規(guī)則:
? ? ? ? ? ? (1) 鎖與進程、文件兩方面有關(guān)。這有兩重含義:第一重很明顯,當一個進程終止時,它所建立的鎖全部釋放;第二重意思就不很明顯,任何時候關(guān)閉一個描述符時,則該進程通過這一描述符可以存訪的文件上的任何一把鎖都被釋放(這些所都是該進程設(shè)置的)。
? ? ? ? ? ? ?(2) 由fork產(chǎn)生的子程序不繼承父進程所設(shè)置的鎖。這意味著,若一個進程得到一把鎖,然后調(diào)用fork,那么對于父進程獲得的鎖來說,子進程被視為另一個進程,對于從父進程處繼承過來的任一描述符,子進程要調(diào)用fcntl以獲得它自己的鎖。這與鎖的作用是一致的。鎖的作用是阻止多個進程同時寫同一個文件(或同一文件區(qū)域)。如果子進程繼承父進程的鎖,則父、子進程就可以同時寫同一個文件。
? ? ? ? ? ? ?(3) 在執(zhí)行exec后,新程序可以繼承原執(zhí)行程序的鎖。
? ? ? ? ? ? ? ? ? APR記錄鎖源碼位置在$(APR_HOME)/file_io/unix目錄下flock.c,頭文件仍然是apr_file_io.h。apr_file_lock和apr_file_unlock僅提供對整個文件的枷鎖和解鎖,而并不支持對文件中任意范圍數(shù)據(jù)的加鎖與解鎖。至于該鎖是建議鎖(advisory lock)還是強制鎖(mandatory lock),需要看具體的平臺實現(xiàn)了。兩個函數(shù)均利用fcntl實現(xiàn)記錄鎖功能。代碼中有一處值得借鑒:
while((rc = fcntl(thefile->filedes, fc, &l)) < 0 && errno == EINTR)continue;? ? ? ? ? ? ? ? ? ? ? ?這么做的原因就是考慮到fcntl的調(diào)用可能被某信號中斷,一旦中斷我們要去重啟fcntl函數(shù)。
? ? ? ? ?二、I/O多路復(fù)用
? ? ? ? ? ? ?在經(jīng)典的《Unix網(wǎng)絡(luò)編程第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多路復(fù)用就是第三種模型,它既解決了Blocking I/O數(shù)據(jù)處理不及時,又解決了Non-Blocking I/O采用輪詢的CPU浪費問題,同時它與異步I/O不同的是它得到了各大平臺的廣泛支持。
? ? ? ? ? ? ? APR I/O多路復(fù)用源碼主要在$(APR_HOME)/poll/unix目錄下的poll.c和select.c中,頭文件為apr_poll.h。APR提供統(tǒng)一的apr_poll接口,但是apr_pollset_t結(jié)構(gòu)定義和apr_poll的實現(xiàn)則根據(jù)宏P(guān)OLLSET_USES_SELECT、POLL_USES_POLL和POLLSET_USES_POLL的定義與否而不同。
? ? ? ? ? ? ? ?在poll的實現(xiàn)下,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; };? ? ? ? ? ? ? ? 統(tǒng)一的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分析-共享內(nèi)存篇
? ? ? ? ? ? ?共享內(nèi)存是一種重要的IPC方式。在項目中多次用到共享內(nèi)存,只是用而并未深入研究。
? ? ? ? ?APR共享內(nèi)存封裝的源代碼的位置在$(APR_HOME)shmem目錄下,本篇blog著重分析unix子目錄下的shm.c文件內(nèi)容,其相應(yīng)頭文件為$(APR_HOME)/include/apr_shm.h.
? ? ? ? ?一、共享內(nèi)存簡單小結(jié)
? ? ? ? ? ? 共享內(nèi)存時最快的IPC方式,因為一旦這樣的共享內(nèi)存段映射到各個進程的地址空間,這些進程間通過共享內(nèi)存的數(shù)據(jù)傳遞就不需要內(nèi)核的幫忙了。Stevens的解釋是"各進程不是通過執(zhí)行任何進入內(nèi)核的系統(tǒng)調(diào)用來傳遞數(shù)據(jù),顯然內(nèi)核的責任僅僅是建立各進程地址空間與共享內(nèi)存的映射,當然像處理頁面故障這一類的底層活還是要做的"。相比之下,管道和消息隊里交換數(shù)據(jù)時都需要內(nèi)核來中轉(zhuǎn)數(shù)據(jù),速度就相對較慢。
? ? ? ? ?二、APR共享內(nèi)存封裝
? ? ? ? ? ? ? APR提供多種創(chuàng)建共享內(nèi)存的方式,其中最主要的就是apr_shm_create接口,其偽碼如下:? ? ??
apr_shm_create {if (要創(chuàng)建匿名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 { /* 創(chuàng)建有名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創(chuàng)建匿名shmem的做法如下:
? ? ? ? ? ? ?(1) SVR4通過映射"/dev/zero"設(shè)備文件來獲得匿名共享內(nèi)存,其代碼一般為:
fd = open("/dev/zero", ..); ptr = mmap(..., MAP_SHARED, fd, ...);? ? ? ? ? ? ?(2) 4.4 BSD提供更加簡單的方式來支持匿名共享內(nèi)存(注意標志參數(shù)MAP_XX)
ptr = mmap(..., MAP_SHARED | MAP_ANON, -1, ...);? ? ? ? ? ? ?(3) System V匿名共享內(nèi)存區(qū)的做法如下:
shmid = shmget(IPC_PRIVATE, ...); ptr = shmat(shmid, ...);? ? ? ? ? ? ? ? 匿名共享內(nèi)存一般都用于有親緣關(guān)系的進程間的數(shù)據(jù)通訊。由父進程創(chuàng)建共享內(nèi)存,子進程自動繼承下來。由于是匿名,沒有親緣關(guān)系的進程是不能動態(tài)鏈接到該共享內(nèi)存區(qū)的。
? ? ? ? ? ? ? 不同版本Unix創(chuàng)建有名shmem的做法如下:
? ? ? ? ? ? ? (4) 由于是有名的shmem,所以與匿名不同的地方在于用filename替代"/dev/zero"做映射。
fd = open(filename, ...); apr_file_trunc(...); ptr = mmap(..., MAP_SHARED, fd, ...);? ? ? ? ? ? ? (5) Posix共享內(nèi)存的做法
fd = shm_open(filename, ...); apr_file_trunc(...); ptr = mmap(..., MAP_SHARED, fd, ...);? ? ? ? ? ? ? ? ? 值得注意的一點就是通過shm_open映射的共享內(nèi)存可以供無親緣關(guān)系的進程共享。apr_file_trunc用于重新設(shè)定共享內(nèi)存對象長度。
? ? ? ? ? ? ? ?(6) System V有名共享內(nèi)存區(qū)的做法如下:
shmkey = ftok(filename, 1); shmid = shmget(shmkey, ...); //相當于open orshm_open ptr = shmat(shmid, ...); //相當于mmap? ? ? ? ? ? ? ?有名共享內(nèi)存一般都與一個文件相關(guān),該文件映射到共享內(nèi)存段,而不同的進程(包括無親緣關(guān)系的進程)則都映射到該文件以達到目的。在APR中通過apr_shm_attach可以動態(tài)將調(diào)用進程連接到已存在的共享內(nèi)存上,前提是你必須知道該共享內(nèi)存區(qū)的標識,在APr中一律用filename做標識。
? ? ? ? 三、總結(jié)
? ? ? ? ? ? 內(nèi)核架起了多個進程間共享數(shù)據(jù)的紐帶--共享內(nèi)存。通過上面的敘述你會發(fā)現(xiàn)共享內(nèi)存的創(chuàng)建其實并不困難,真正困難的是共享內(nèi)存的管理,在正規(guī)的軟件公司像內(nèi)存/共享內(nèi)存管理這樣的重要底層功能都是封裝成庫形式的。
? ? ? ? 四、參考資料
? ? ? ? ? ? SIGSEGV和SIGBUS
? ? ? ? ? ? 涉及共享內(nèi)存的管理就不能不提到訪問共享內(nèi)存對象。談到訪問共享內(nèi)存對象就要留神"SIGSEGV和SIGBUS"這兩個信號。
? ? ? ? ? ? ? 系統(tǒng)分配內(nèi)存頁來承載內(nèi)存映射區(qū),由于內(nèi)存頁大小是固定的,所以存在多余的頁空間空閑,比如待映射文件大小為5000 bytes,內(nèi)存映射區(qū)大小也為5000bytes。而一個內(nèi)存頁大小4096,系統(tǒng)勢必要分配兩頁來承載,這時空閑的有效空間為從5000-8191,如果進程訪問這段地址空間也不會發(fā)生錯誤。但是要超出8191,就會收到SIGSEGV信號,導(dǎo)致程序停止。關(guān)于SIGBUS信號的來歷,這里也舉例說明:若待映射文件大小為5000 bytes,我們在mmap時指定內(nèi)存映射區(qū)size = 15000 > 5000,這時內(nèi)核真正的共享區(qū)承載體大小只有8192(能包容映射文件大小即可),此時在[0, 8191]內(nèi)訪問均沒問題,但在[8192,14999]之間會得到SIGBUS信號;超出15000訪問時會觸發(fā)SIGSEGV信號。
? 5.8 APR分析-環(huán)篇
? ? ? ? ? APR中少見對數(shù)據(jù)結(jié)構(gòu)的封裝,好像唯一例外的就是其對循環(huán)鏈表,即環(huán)(RING)的封裝。
? ? ? ? ? 簡單說說環(huán)(RING):環(huán)是一個首尾相連的雙線鏈表,也就是我們所說的循環(huán)鏈表。
? ? ? ? ? 1.如何使用APR RING?
假設(shè)環(huán)節(jié)點的結(jié)構(gòu)如下: struct elem_t { /* APR RING鏈接的元素類型定義 */APR_RING_ENTRY(elem_t) link; /*鏈接域*/int foo; /*數(shù)據(jù)域*/ }; 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的難點---"哨兵"
? ? ? ? ? ?環(huán)是通過頭節(jié)點來管理的,頭節(jié)點是這樣一種節(jié)點,其next指針指向RING的第一個節(jié)點,其prev指針指向RING的最后一個節(jié)點,即尾節(jié)點。但是通過查看源碼發(fā)現(xiàn)APR RING通過APR_RING_HEAD宏定義的頭節(jié)點形式如下:
#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; };? ? ? ? ? ? ? 通過對比可以看出頭節(jié)點僅僅相當于一個elem_t的link域。這樣做的話必然帶來對普通節(jié)點和頭節(jié)點在處理上的不一致,為了避免這種情況的發(fā)生,APR RING引入了"哨兵"節(jié)點的概念。我們先看看哨兵節(jié)點在整個鏈表中的位置。
? ? ? ? ? ? ? ? sentinel->next = 鏈表的第一個節(jié)點;
? ? ? ? ? ? ? ? sentinel->prev = 鏈表的最后一個節(jié)點;
? ? ? ? ? ? ? ? 但是查看APR RING的源碼你會發(fā)現(xiàn)sentinel節(jié)點只是個虛擬存在的節(jié)點,這個虛擬節(jié)點既有數(shù)據(jù)域(虛擬出來的,不能引用)又有鏈接域,好似與普通節(jié)點并無差別。
? ? ? ? ? ? ? ? ?再看看下面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)? ? ? ? ? ? ? ? ? 你會發(fā)現(xiàn):初始化RING實際上是將head的next和prev指針都指向了sentinel虛擬節(jié)點了。從sentinel的角度來說相當于其自己的link域的next和prev都指向了自己。所以判斷APR RING是否為空只需要判斷RING的首個節(jié)點是否為sentinel虛擬節(jié)點即可。APR_RING_EMPTRY宏就是這么做的:
#define APR_RING_EMPTY(hp, elem, link) /(APR_RING_FIRST((hp)) == APR_RING_SENTINEL((hp), elem, link))? ? ? ? ? ? ? ? ? ?那么如何計算sentinel虛擬節(jié)點的地址呢?
? ? ? ? ? ? ? ? ? ?我們這樣思考:從普通節(jié)點說起,如果我們知道一個普通節(jié)點的首地址(elem_addr),那么我們計算其link域的地址(link_addr)的公式就應(yīng)該為link_addr=elem_addr + offsetof(elem_t, link);前面我們一直在說sentinel虛擬節(jié)點看起來和普通節(jié)點沒什么區(qū)別,所以它仍然符合該計算公式。前面我們又說過head_addr是sentinel節(jié)點的link域,這樣的話我們將head_addr輸入到公式中得到head_addr = sentinel_addr + offsetof(elem_t, link),做一下變換即可得到sentinel_addr = head_addr - offsetof(elem_t, link)。看看APR RING源代碼就是這樣實現(xiàn)的:
#define APR_RING_SENTINEL(hp, elem, link) /(struct elem *)((char *)(hp) - APR_OFFSETOF(struct elem, link))? ? ? ? ? ? ? ? ? ? ?至此APR RING使用一個虛擬sentinel節(jié)點分隔RING的首尾節(jié)點,已達到對節(jié)點操作一致的目的。
? ? ? ? ? ? ? ? ?3、APR RING不足之處
? ? ? ? ? ? ? ? ? ? 1)缺少遍歷接口
? ? ? ? ? ? ? ? ? ? ? ?瀏覽APR RING源碼后發(fā)現(xiàn)缺少一個遍歷宏接口,這里提供一種正向遍歷實現(xiàn):
#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分析-進程同步篇
- 最新的統(tǒng)計數(shù)據(jù)顯示Apache服務(wù)器在全世界仍然占據(jù)著Web服務(wù)器龍頭老大的位置,而且市場占有率遙遙領(lǐng)先,所以學(xué)習Apache相關(guān)知識是完全正確的方向,這里我們繼續(xù)分析APR進程同步相關(guān)內(nèi)容。
? ? ? ? ?進程同步的源代碼的位置在$(APR_HOME)/locks目錄下,本篇blog著重分析unix子目錄下的proc_mutex.c、global_mutex文件內(nèi)容,其相應(yīng)頭文件為$(APR_HOME)/include/apr_proc_mutex.h、apr_global_mutex.h。其用于不同進程之間的同步以及多進程多線程中的同步問題。
? ? ? ? ? ? ?apr_thread_mutex_t - 支持單個進程內(nèi)的多線程同步;
? ? ? ? ? ? ?apr_proc_mutex_t - 支持多個進程間的同步;
? ? ? ? ? ? ?apr_global_mutex_t - 支持不同進程內(nèi)的不同線程間同步。
? ? ? ? ? ? ?在本篇中著重分析apr_proc_mutex_t。
? ? ? ? 1.同步機制
? ? ? ? ? APR提供多種進程同步的機制供選擇使用。在apr_proc_mutex.h中列舉了如下同步機制:
typedef enum {APR_LOCK_FCNTL, /*記錄上鎖*/APR_LOCK_FLOCK, /* 文件上鎖*/APR_LOCK_SYSVSEM, /*系統(tǒng)V信號量*/APR_LOCK_PROC_PTHREAD, /* 利用pthread線程鎖特性*/APR_LOCK_POSIXSEM, /*POSIX信號量*/APr_LOCK_DEFAULT /*默認進程間鎖*/ } apr_lockmech_e;? ? ? ? ?2.實現(xiàn)點滴
? ? ? ? ? ?APR提供每種同步機制的實現(xiàn),每種機制體現(xiàn)為一組函數(shù)接口,這些接口被封裝在一個結(jié)構(gòu)體類型中:
/* 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的出現(xiàn)也就在清理之中了
/* 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各個"成員函數(shù)"功能的"薄封裝",而真正干活的其實是apr_proc_mutex_t中的meth字段的"成員函數(shù)",它們的工作包括mutex的創(chuàng)建、獲取(加鎖)和清除(解鎖)等。以"獲取鎖"為例APR的實現(xiàn)如下:
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系統(tǒng)調(diào)用來完成鎖功能的,記得在以前的一篇關(guān)于APR文件I/O的Blog中談過記錄鎖,這里不再詳細敘述了。
? ? ? ? ? (2) 文件鎖
? ? ? ? ? ? ? 文件鎖是記錄鎖的一個特例,其功能由函數(shù)接口flock支持。值得說明的是它僅僅提供"寫入鎖"(獨占鎖),而不提供"讀入鎖"(共享鎖)。
? ? ? ? ? ?(3) System V信號量
? ? ? ? ? ? ? System V信號量是一種內(nèi)核維護的信號量,所以我們只需調(diào)用semget獲取一個System V信號量的描述符即可。值得注意的是與POSIX的單個"計數(shù)信號量"不同的是System V信號量是一個"計數(shù)信號量集"。? ?
? ? ? ? ? ?(4) 利用線程互斥鎖機制
? ? ? ? ? ? ? APR使用pthread提供的互斥鎖機制。原本pthread互斥鎖是用來互斥一個進程內(nèi)的各個現(xiàn)成的,但APR在共享內(nèi)存中創(chuàng)建了pthread_mutex_t,這樣使得不同進程的主線程實現(xiàn)互斥,從而達到進程間互斥的目的。截取部分代碼如下:
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信號量命名規(guī)則*/ 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分析-線程篇
? ? ? ? ? 并行一直是程序設(shè)計領(lǐng)域的難點,而線程是并行的一種重要的手段,而且現(xiàn)成的一些特性也能在進程并行時發(fā)揮很好的作用(“在線程同步篇”中詳細闡述)。
? ? ? ? APR線程的源代碼的位置在$(APR_HOME)/threadproc目錄下,本篇blog著重分析unix子目錄下的thread.c文件內(nèi)容,其相應(yīng)頭文件為$(APR_HOME)/include/apr_threadproc.h。
? ? ? ? ? 一、線程基礎(chǔ)
? ? ? ? ? ? ? (1) 在傳統(tǒng)觀點中,進程是由存儲于用戶虛擬內(nèi)存中的代碼、數(shù)據(jù)和棧,以及由內(nèi)核維護的"進程上下文"組成的,其中"進程上下文"又可以看成"程序上下文"和"內(nèi)核上下文"組成,可參見下圖所示:
進程--|- 進程上下文|-程序上下文|-數(shù)據(jù)寄存器|-條件碼|-棧指針|-程序計數(shù)器|- 內(nèi)核上下文|- 進程ID|- VM結(jié)構(gòu)|- Open files|- 已設(shè)置的信號處理函數(shù)|- brk pointer|- 代碼、數(shù)據(jù)和棧(在虛存中)|- 棧區(qū)<--SP|- 共享庫區(qū)|- 運行時堆區(qū) <--brk|- 可讀/寫數(shù)據(jù)區(qū)|- 只讀代碼/數(shù)據(jù)區(qū) <--PC? ? ? ? ? ? ? ?(2) 另一種觀點中,進程是由線程、代碼和數(shù)據(jù)以及內(nèi)核上下文組成的。下圖更能直觀的展示出兩種觀點的異同:
進程--+|- 線程|- 棧區(qū) <--SP|- 線程上下文|- 線程ID|- 數(shù)據(jù)寄存器|- 條件碼|- 棧指針|- 程序計數(shù)器|- 內(nèi)核上下文|- 進程ID|- VM結(jié)構(gòu)|- Open files|-已設(shè)置的信號處理函數(shù)|- brk pointer|- 代碼、數(shù)據(jù)(在虛存中)|- 共享庫區(qū)|- 運行時堆區(qū) <-- brk|- 可讀/寫數(shù)據(jù)區(qū)|- 只讀代碼/數(shù)據(jù)區(qū) <--PC? ? ? ? ? ? ? ?對比兩種觀點我們可以得出以下幾點結(jié)論:
? ? ? ? ? ? ?(a) 從觀點(2)可以看出進程內(nèi)的多個線程共享進程的內(nèi)核上下文和代碼、數(shù)據(jù)(當然不包括棧區(qū));
? ? ? ? ? ? ?(b) 線程上下文比進程上下文小,且切換代價小;
? ? ? ? ? ? ?(c) 線程不像進程那樣有著"父-子"體系,同一個進程內(nèi)的線程都是"對等的",主線程與其他線程不同之處就在于其是進程創(chuàng)建的第一個線程。
? ? ? ? ?二、APR線程管理接口
? ? ? ? ? ? ?如今應(yīng)用最廣泛的線程包就是PosixThread了。APR對線程的封裝也是基于Posix thread的。
? ? ? ? ? ? ?APR線程管理接口針對apr_thread_t 這個基本的數(shù)據(jù)結(jié)構(gòu)進行操作,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; };? ? ? ? ? ? ?這個結(jié)構(gòu)中包含了線程ID、線程函數(shù)以及該函數(shù)的參數(shù)數(shù)據(jù)。不過APR的線程函數(shù)定義與Pthread的有不同,“Pthread線程函數(shù)”是這樣的:
? ? ? ? ? typedef void *(start_routine)(void *);
? ? ? ? ? ?而"APR線程函數(shù)"如下:
? ? ? ? ? typedef void *(APR_THREAD_FUNC *apr_thread_start_t)(apr_thread_t *, void *);
? ? ? ? ?1.apr_thread_create
? ? ? ? ? ? apr_thread_create 內(nèi)部定義了一個dummy_worker的"Pthread線程函數(shù)",并將apr_thread_t結(jié)構(gòu)作為參數(shù)傳入,然后在dummy_worker中啟動"APR的線程函數(shù)"。在該函數(shù)的參數(shù)列表中有一項類型為apr_threadattr_t:
struct apr_threadattr_t {apr_pool_t *pool;pthread_attr_t attr; };? ? ? ? ? 2.apr_thread_exit
? ? ? ? ? ? ? 進程退出我們可以直接調(diào)用exit函數(shù),而線程退出也有幾種方式:
? ? ? ? ? ? ?(1) 隱式退出 - 可以理解為線程main routine代碼結(jié)束返回;
? ? ? ? ? ? ?(2) 顯式退出 - 調(diào)用線程包提供的顯示退出接口,在apr中就是apr_thread_exit;
? ? ? ? ? ? ?(3) 另類顯式退出 - 調(diào)用exit函數(shù),不僅自己退出,其所在線程也跟著退出了;
? ? ? ? ? ? ?(4)被"黑"退出 - 被別的"對等"線程調(diào)用pthread_cancel而被迫退出。
? ? ? ? ? ? ?apr_thread_exit屬于種類(2),該種類退出應(yīng)該算是線程的優(yōu)雅退出了。apr_thread_exit做了3個工作,分別為設(shè)置線程返回值、釋放pool中資源和調(diào)用pthread_exit退出。
? ? ? ? ? ? 3.apr_thread_join和apr_thread_detach
? ? ? ? ? ? ? 進程有waitpid,線程有join。線程在調(diào)用apr_thread_exit后,只是其執(zhí)行停止了,其占有的"資源"并不一定釋放,這里的"資源"我想就是"另種觀點"中的"線程上下文",線程有兩種方式來釋放該"資源",這主要由現(xiàn)成的"可分離"屬性決定的。如果線程是"可分離的",當線程退出后會自動釋放其"資源",如果線程為"非可分離的",則必須由"對等線程"調(diào)用join接口來釋放其資源。apr_thread_detach用來將其調(diào)用線程轉(zhuǎn)化為"可分離"線程,而apr_thread_join用來等待某個線程結(jié)束并釋放其資源。
? 5.11 APR分析-網(wǎng)絡(luò)IO篇
? ? ? ? ?APR網(wǎng)絡(luò)I/O的源代碼的位置在$(APR_HOME)/network_io目錄下,本篇blog著重分析unix子目錄下的各.c文件內(nèi)容,其相應(yīng)頭文件為$(APR_HOME)/include/apr_network_io.h。
? ? ? ?一、IP地址 -- 主機通信
? ? ? ? ? ?我們熟知的并且每天工作于其上的因特網(wǎng)是一個世界范圍的主機的集合,這個主機結(jié)合被映射為一個32位(目前)或者64位(將來)IP地址;而IP地址又被映射為一組因特網(wǎng)域名;一個網(wǎng)絡(luò)中的主機上的進程能通過一個連接(connection)和任何其他網(wǎng)絡(luò)中的主機上的進程通信。
? ? ? ? ? 1.IP地址存儲
? ? ? ? ? ? ?在如今的IPv4協(xié)議中我們一般使用一個unsigned int來存儲IP地址,在UNIX平臺下,使用如下結(jié)構(gòu)來存儲一個IP地址的值:
/* Internet address structure */ struct in_addr {unsigned int s_addr; /* network byte order(big-endian) */ };? ? ? ? ? ? ?這里值得一提的是APR關(guān)于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是以網(wǎng)絡(luò)字節(jié)序存儲的。如果你的IP地址不符合條件,可通過調(diào)用一些輔助接口來做轉(zhuǎn)換,這些接口包括:
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整形數(shù)來表示。這樣就存在著一個IP地址表示和IP地址存儲之間的一個轉(zhuǎn)換過程。APR提供這一轉(zhuǎn)換支持,我們用一個例子來說明:
#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將我們熟悉的點分十進制形式轉(zhuǎn)換成一個整形數(shù)存儲的IP地址;而apr_inet_ntop則將一個存整形數(shù)存儲的IP地址轉(zhuǎn)換為我們可讀的點分十進制形式。這兩個接口的功能類似于系統(tǒng)調(diào)用inet_pton和inet_ntop,至于使用哪個就看你的喜好了。
? ? ? ?二、SOCKET --進程通信
? ? ? ? ? ?1.SOCKET描述符
? ? ? ? ? ? ? 獲取SOCKET描述符:
int socket(int domain, int type, int protocol);? ? ? ? ? ? ? 從Unix程序的角度來看,SOCKET就是一個有相應(yīng)描述符的打開的文件。在APR中我們可以通過調(diào)用apr_socket_create來創(chuàng)建一個APR自定義的SOCKET對象,該SOCKET結(jié)構(gòu)如下:
/* 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 };? ? ? ? ? ? ? ? ? ? ? ?該結(jié)構(gòu)中的socketdes字段其實是真實存儲由socket函數(shù)返回的SOCKET描述符的,其他字段都是為APR自己所使用的,這些字段在Bind、Connect等過程中使用。我們?nèi)绻伙@示將SOCKET描述符綁定到某SOCKET地址上,系統(tǒng)內(nèi)核就會自動為該SOCKET描述符分配一個SOCKET地址。
? ? ? ? ? ? 2.SOCKET屬性
? ? ? ? ? ? ? ? 還是與文件對比,在文件系統(tǒng)調(diào)用中有一個fcntl接口可以從來獲取或設(shè)置已分配的文件描述符的屬性,如是否Block、是否Buffer等。SOCKET也提供類似的接口調(diào)用setsockopt和getsockopt。在APR中等價于該功能的接口時apr_socket_opt_set和apr_socket_opt_get。APR在apr_network_io.h中提供如下SOCKET的參數(shù)屬性:
#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
? ? ? ? ? ? ? ?客戶端連接服務(wù)器端的唯一調(diào)用就是connect,connect試圖建立一個客戶端進程與服務(wù)器端進程的連接。apr_socket_connect的參數(shù)分別為客戶端已經(jīng)打開的一個SOCKET以及指定的服務(wù)器端的SOCKET地址(IP ADDR:PORT)。apr_socket_connect內(nèi)部實現(xiàn)的流程大致如下:
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) 執(zhí)行系統(tǒng)調(diào)用connect連接服務(wù)器端,注意這里做了防止信號中斷的處理。
? ? ? ? ? ? ? (b) 如果系統(tǒng)操作正在進行中,調(diào)用apr_wait_for_io_or_timeout進行超時等待;
? ? ? ? ? ? ? (c) 錯誤返回,前提errno不是表示已連接上。
? ? ? ? ? ? ? 一旦apr_socket_connect成功返回,我們就已經(jīng)成功建立一個SOCKET對,即一個連接。
? ? ? ? ?(2) apr_socket_bind
? ? ? ? ? ? ? Bind、Listen和Accept這三個過程是服務(wù)器端用于接收"連接"的必經(jīng)之路。其中Bind就是告訴操作系統(tǒng)內(nèi)核顯示地位該SOCKET描述符分配一個SOCKET地址,這個SOCKET地址就不能被其他SOCKET描述符占用了。
? ? ? ? ?(3)apr_socket_listen
? ? ? ? ? ? ?SOCKET描述符在初始分配時都處于"主動連接"狀態(tài),Listen過程將該SOCKET描述符從"主動連接"轉(zhuǎn)換為"被動狀態(tài)",并告訴內(nèi)核接受該SOCKET描述符的連接請求。apr_socket_listen的背后直接就是listen接口調(diào)用。
? ? ? ? ? (4) apr_socket_accept
? ? ? ? ? ? ? Accept過程在"被動狀態(tài)"SOCKET描述符上接受一個客戶端的連接,這時系統(tǒng)內(nèi)核會自動分配一個新的SOCKET描述符,內(nèi)核為該描述符自動分配一個SOCKET地址,來代表這條連接的服務(wù)器端。注意在SOCKET編程接口中除了socket函數(shù)能分配新的SOCKET描述符之外,accept也是另外的一個也是唯一的一個能分配新的SOCKET描述符的系統(tǒng)調(diào)用了。apr_socket_accept首先在pool中分配一個新的apr_socket_t結(jié)構(gòu)變量,然后調(diào)用accept,并設(shè)置新變量的各個字段。
? ? ? ? ? ? 4.Send/Recv --數(shù)據(jù)傳輸
? ? ? ? ? ? ? 網(wǎng)絡(luò)通信最重要的還是數(shù)據(jù)傳輸,在SOCKET編程接口中最常見的兩個接口就是recv和send。在APR中分別有apr_socket_recv和apr_socket_send與前面二者對應(yīng)。下面逐一分析。
? ? ? ? ? ? ? ?(1) apr_socket_recv
? ? ? ? ? ? ? ? ?首先來看看apr_socket_recv的實現(xiàn)過程:
? ? ? ? ? ? ? ? ?
apr_socket_recv {if (上次調(diào)用apr_socket_recv沒有讀完所要求的的字節(jié)數(shù)) { ----------(a)設(shè)置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)設(shè)置(*len)和sock->options; -----(d)... ... }? ? ? ? ? ? ? ? ? ? ? ?針對上面代碼進行說明:
? ? ? ? ? ? ? ? ? ? ? ?(a) 一次apr_socket_recv調(diào)用完全有可能沒有讀完所要求的字節(jié)數(shù),這里做個判斷以決定是否繼續(xù)讀完剩下的數(shù)據(jù);
? ? ? ? ? ? ? ? ? ? ? ? (b) 調(diào)用read讀取SOCKET緩沖區(qū)數(shù)據(jù),注意這里做了防止信號中斷的處理。
? ? ? ? ? ? ? ? ? ? ? ? (c) 如果SOCKET操作正忙,我們調(diào)用apr_wait_for_io_or_timeout等待,直到SOCKET可用。
? ? ? ? ? ? ? ? ? ? ? ? (d) 將(*len)設(shè)置為實際從SOCKET Buffer中讀取的字節(jié)數(shù),并根據(jù)這一實際數(shù)據(jù)與要求數(shù)據(jù)作比較來設(shè)置sock->options.
? ? ? ? ? ? ? (2) apr_socket_send
? ? ? ? ? ? ? ? ? ? apr_socket_send負責發(fā)送數(shù)據(jù)到SOCKET Buffer,其實現(xiàn)的方式與apr_socket_recv大同小異。
? ? ? ? ? ? ? ? ?
總結(jié)
以上是生活随笔為你收集整理的参考资料学习APR库的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 读“ModSecurity配置关键字说明
- 下一篇: 摘录理解LDAP与LDAP注入