APR分析-總體篇node
1、何爲APR?程序員
Apache Server通過這麼多年的發展後,將一些通用的運行時接口封裝起來提供給你們,這就是Apache Portable Run-time libraries, APR。shell
2、APR的目錄組織apache
從www.apache.org上下載apr-1.1.1.tar.gz到本地解壓後,發現APR的目錄結構很清晰。編程
1) 全部的頭文件都放在$(APR)/include目錄中;數組
2) 全部功能接口的實現都放在各自的獨立目錄下,如threadproc、mmap等;緩存
3) 此外就是相關平臺構建工具文件如Makefile.in等。曾經看過ACE的代碼,ACE的全部源文件(.cpp)都放在一個目錄下,顯得很混亂。APR給個人第一印象還不錯。服務器
4) 進入各功能接口子目錄,以threadproc爲例,在其下面的子目錄有5個,分別爲beos、netware、os2、unix和win32。從APR的名字也能夠理解,每一個子目錄下都存放着各個平臺的獨特實現源文件。網絡
3、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。
4、應用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 -ladm
INCL = -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文件採用的是$(apr_path)/test目錄下的proc_child.c文件。編譯運行一切OK!
5、GO ON
分析APR的過程也是我學習Unix高級系統機制的過程,有時間我會繼續APR分析的。
APR分析-設計篇
APR附帶一個簡短的設計文檔,文字言簡意賅,其中不少的設計思想都值得咱們所借鑑,主要從三個方面談。
1、類型
1) APR提供並建議用戶使用APR自定義的數據類型,好處不少,好比便於代碼移植,避免數據間進行沒必要要的類型轉換(若是你不使用APR自定義的數據類型,你在使用某些APR提供的接口時,就須要進行一些參數的類型轉換);自定義數據類型的名字更加具備自描述性,提升代碼可讀性。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_int64_t;
typedef unsigned long long apr_uint64_t;
這些都是在apr.h中定義的,而apr.h在UNIX平臺是經過configure程序生成的,在不一樣平臺APR自定義類型的實際類型是徹底有可能不一致的。
2) 還有一點值得提的是在APR的設計文檔中,它稱「dso、mmap、process、thread」等爲「base types」。很難用中文理解之,估計是指apr_mmap_t這些類型吧。權且這麼理解吧^_^
3) 另外的一個特色就是大多APR類型中都包含一個apr_pool_t類型的字段,該字段用於分配APR內部使用的內存,任何APR函數須要內存均可以經過它分配。若是你建立一個新的類型,你最好在該類型中加入一個apr_pool_t類型的字段,不然全部操做該類型的APR函數都須要一個apr_pool_t類型的參數。
2、函數
1) 理解APR的函數設計對閱讀APR代碼頗有幫助。看了APR代碼你會發現不少相似APR_DECLARE(apr_hash_t *) apr_hash_make(apr_pool_t*pool)帶APR_DECLARE宏的函數聲明,究竟是什麼意思呢?爲何要加一個APR_DECLARE呢?在apr.h中有這樣的解釋:「APR的固定個數參數公共函數的聲明形式APR_DECLARE(rettype) apr_func(args);而非固定個數參數的公共函數的聲明形式爲APR_DECLARE_NONSTD(rettype) apr_func(args, ...);」。在Unix上的apr.h中有這兩個宏的定義:
#defineAPR_DECLARE(type) type
#define APR_DECLARE_NONSTD(type) type
在apr.h文件中解釋了這麼作就是爲了在不一樣平臺上編譯時使用「the most appropriate calling convention」,這裏的「calling convention」是一術語,翻譯過來叫「調用約定」。[注1]
常見的調用約定有:stdcall、cdecl、fastcall、thiscall和naked call,其中cdecl調用約定又稱爲C調用約定,是C語言缺省的調用約定。
2) 若是你想新增APR函數,APR建議你最好能按以下作,這樣會和APR提供的函數保持最好的一致性:
a) 輸出參數爲第一個參數;
b) 若是某個函數須要內部分配內存,則將一個apr_pool_t參數放在最後。
3、錯誤處理
大型的系統程序的錯誤處理是十分重要的,APR做爲一通用的庫接口集合詳細的說明了使用APR時如何進行錯誤處理。
1) 錯誤處理的第一步就是「錯誤碼和狀態碼分類」。APR的函數大部分都返回apr_status_t類型的錯誤碼,這是一個int型,在apr_errno.h中定義,和它在一塊兒定義的還有apr所用的全部錯誤碼和狀態碼。APR定義了5種錯誤碼類型,它們分別爲「0」[注2]、APR_OS_START_ERROR、APR_OS_START_STATUS、APR_OS_START_USEERR和APR_OS_START_SYSERR,它們每一個都擁有本身獨自的偏移量。
2) 如何定義錯誤捕捉策略?
因爲APR是可移植的,這樣就可能遇到這樣一個問題:不一樣平臺錯誤碼的不一致。如何處理呢?APR給咱們提供了2種策略:
a) 跨多平臺返回相同的錯誤碼
這種策略的缺點是轉換費時且在轉換時有錯誤碼損耗。好比Windows操做系統定義了成百上千錯誤碼,而POSIX才定義了50錯誤碼,若是都轉換爲規範統一的錯誤碼,勢必會有錯誤碼含義丟失,有可能得不到擁有真正含義的錯誤碼。執行流程如:
make syscall that fails
convert to common errorcode
return common errorcode
-------------------------------------------------------------------
decide execution based on common error code
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) 函數調用後,由誰來把堆棧恢復原來狀態
在像C/C++這樣的中、高級語言中,使用「調用約定」來講明這兩個問題。
[注2] 特殊「0」
每一個平臺都有0,可是都沒有實際的定義,0又的確是一個errnovalue的offset,可是它是「匿名的」,它不像EEXIST那樣有着能夠「自描述」的名字。
APR分析-進程篇
APR進程封裝源代碼的位置在$(APR_HOME)/threadproc目錄下,本篇blog着重分析unix子目錄下的proc.c文件內容,其相應頭文件爲$(APR_HOME)/include/apr_thread_proc.h。
1、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);[注1]
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);
2、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)調用後,咱們能夠用下面的圖來表示這些字段的狀態:[注3]
parent_in ----------------------------------------------
/|/
------------------------------------------
filedes[0] "in_pipe" filedes[1]
------------------------------------------
/|/
child_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,定義見[注2]
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,用於檢索容許該用戶所使用的權限。
3、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 stdouterr*/
apr_file_t *err;
} apr_proc_t;
結構中有很清晰明瞭的註釋,這裏就再也不說了。
建立一個新的進程的接口爲apr_proc_create,其參數也都很簡單。前面說過apr_proc_create先fork出一個子進程,衆所周知fork後子進程是父進程的複製品[注4],而後子進程再經過exec函數加載新的程序映像,並開始執行新的程序。這裏分析一下apr_proc_create的執行流程,其僞碼以下:
apr_proc_create
{
if (attr->errchk)
對attr作有效性檢查,讓錯誤儘可能發生在parentprocess中,而不是留給child process; ----(1)
fork子進程;
{ /* 在子進程中 */
清理一些沒必要要的從父進程繼承下來的描述符等,爲
exec提供一個「乾淨的」環境;------(2)
關閉attr->parent_in、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的設計,這裏僅僅做簡單說明。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後子進程繼承了這些。爲了關掉一些沒必要要的描述符和更好的和父進程通信,子進程做了一些重定向的工做,這裏用2副圖來表示重定向先後的差異:(圖中顯示的是子進程關閉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]
------------------|
爲了能更好的體現出「對話通道」的概念,這裏再畫出父進程再關閉ttr->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只是用另外一個新程序替換了當前進程的正文、數據、堆和棧段。這裏不詳述這幾種函數的差異,在參考資料中有相關描述[注5]。
4、總結
簡單分析了一下APR的進程封裝,APR的源代碼註釋很詳盡,不少細節能夠直接察看源碼。
[注1]
#define HANDLE_RTVAL(func, rv) do { /
if (rv != APR_SUCCESS) { /
printf("%s executes error!/n", #func); /
return rv; /
} /
} while(0)
[注2]
typedef enum {
APR_SHELLCMD, /*use the shell to invoke the program */
APR_PROGRAM, /* invoke the program directly, no copied env */
APR_PROGRAM_ENV, /* invoke theprogram, replicating our environment */
APR_PROGRAM_PATH, /* find program on PATH,use our environment */
APR_SHELLCMD_ENV /* use the shell toinvoke the program, replicating our environment */
} apr_cmdtype_e;
[注3]
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使用。
[注4]
fork後子進程和父進程的同和異
同:
子進程從父進程那繼承了
-- 父進程已打開的文件描述符;
-- 實際用戶ID、實際組ID、有效用戶ID、有效組ID;
-- 添加組ID;
-- 進程組ID;
-- 對話期ID;
-- 控制終端;
-- 設置用戶ID標誌和設置組ID標誌;
-- 當前工做目錄;
-- 根目錄;
-- 文件方式建立屏蔽字;
-- 信號屏蔽和排列;
-- 對任一打開文件描述符的在執行時關閉標誌;
-- 環境;
-- 鏈接的共享存儲段;
-- 資源限制。
異:
-- fork的返回值;
-- 進程ID;
-- 不一樣的父進程ID;
-- 子進程的tms_utime, tms_stime, tms_cutime以及tme_ustime設置爲0;
-- 父進程設置的鎖,子進程不繼承;
-- 子進程的未決告警被清除;
-- 子進程的未決信號集設置爲空集。
[注5]
這裏引用《Unix環境高級編程》中關於如何區分和記憶exec函數族的方法:「這六個exec函數的參數很難記憶。函數名中的字符會給咱們一些幫助。字母p表示該函數取filename做爲參數,而且用PATH環境變量尋找可執行文件。字母l表示該函數取一個參數列表,它與字母v互斥。v表示該函數取一個argv[]。最後,字母e表示該函數取envp[] 數組,而不使用當前環境。」
參考資料:
1、《Unix環境高級編程》
2、《Unix系統編程》
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)。
1、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_poll_create(pool, NULL);
for (i = 0; i < n; ++i) {
apr_pool_clear(subpool);
... ...
do_operation(..., subpool);
}
apr_pool_destroy(subpool);
2、深刻APR Pool
到目前爲止咱們已經知道了該如何「很好的」使用APR Pool,接下來咱們來深刻APRPool的內部,看究竟有什麼「奧祕」。
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_initialize又是APR program調用的第一個function,其在apr_general.h中聲明,在misc/unix/start.c中實現)。
apr_pool_initialize的僞碼以下(這裏先不考慮多線程的狀況):
static apr_byte_t apr_pools_initialized = 0;
static apr_pool_t *global_pool = NULL;
static apr_allocator_t *global_allocator = NULL;
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;
... ...[注1]
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大小內存時,allocaotr_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;
constchar *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;
也許如今你仍然不能看清楚APRPool的結構,無需着急,咱們繼續往下分析。
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。若是APR Pool中存在多個Pool,其節點結構關係以下:
/-child-->
/ --------Pool_level1-a
/ / parent /|/ |
/|/_ | | sibling
global_pool | |
/ | /|/
/-child-> Pool_level1-b
/|/ |
-parent------
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掉內存了。
3、總結
本文並未說明APR Pool有哪些優勢或缺點(除了概述中的一些Rules),僅是把其前因後果弄清。
[注1]
在本文中出現的"......"的符號表示與多線程相關的字段和代碼的省略。
APR分析-信號篇
1、信號介紹
1、Signal「歷史久遠」,在最初的Unix系統上就能看到它「偉岸」的身影。它的引入用來進行User Mode進程間的交互,系統內核也能夠利用它通知User Mode進程發生了哪些系統事件。從最開始引入到如今,信號只是作了很小的一些改動(不可靠信號模型到可靠信號模型)。
2、信號服務於兩個目的:
1) 通知某進程某特定事件發生了;
2) 強制其通知進程執行相應的信號處理程序。
2、基礎概念
1、信號的一個特性就是能夠在任什麼時候候發給某一進程,而無需知道該進程的狀態。若是該進程當前並未處於執行態,則該信號被內核Save起來,直到該進程恢復執行才傳遞給它;若是一個信號被進程設置爲阻塞,則該信號的傳遞被延遲,直到其阻塞被取消它才被傳遞給進程。
2、系統內核嚴格區分信號傳送的兩個階段:
1) Signal Generation : 系統內核更新目標進程描述結構來表示一個信號已經被髮送出去。
2) Signal Delivery : 內核強制目標進程對信號作出反應,或執行相關信號處理函數,或改變進程執行狀態。
信號的誕生和傳輸咱們能夠這樣理解:把信號做爲「消費品」,其Generation狀態就是「消費品誕生」,其Delivery狀態就是理解爲「被消費了」。這樣勢必存在這樣的一個狀況:「消費品誕生了,可是尚未被消費掉」,在信號模型中,這樣的狀態被稱爲「pending」(懸而未決)。
任什麼時候候一個進程只能有一個這樣的某類型的pending信號,同一進程的其餘同類型的pending信號將不排隊,將被簡單的discard(丟棄)掉。
3、如何消費一個signal
1) 忽略該信號;[注1]
2) 響應該信號,執行一特定的信號處理函數;
3) 響應該信號,執行系統默認的處理函數。包括:Terminate、Dump、Ignore、Stop、Continue等。
這裏有特殊:SIGKILL和SIGSTOP兩個信號不能忽略、不能捕捉、不能阻塞,而只是執行系統默認處理函數。
3、APR Signal封裝
APR Signal源代碼的位置在$(APR_HOME)//threadproc目錄下,本篇blog着重分析unix子目錄下的signals.c文件內容,其相應頭文件爲$(APR_HOME)/include/apr_signal.h。
1、apr_signal函數
Unix信號機制提供的最簡單最多見的接口是signal函數,用來設置某特定信號的處理函數。可是因爲早期版本和後期版本處理信號方式的不一樣,致使如今直接使用signal函數在不一樣的平臺上可能獲得不一樣的結果。
早期版本處理方式:進程每次處理信號後,隨即將信號的處理動做重置爲默認值。
後期版本處理方式:進程每次處理信號後,信號的處理動做不被重置爲默認值。
咱們舉例測試一下:分別在Solaris9 、Cygwin和RedHat Linux 9上。
例子:
E.G 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("insiguser1_handler, %d/n", sig);
}
input:
kill -USR1 9122
kill -USR1 9122
output:(Solaris 9)
in siguser1_handler, 16
用戶信號1 (程序終止)
output:(Cygwin and RH9)
in siguser1_handler, 30
in siguser1_handler, 30
...
..
E.G 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;
#ifdefSA_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一塊兒使用。關於信號集和sigprocmask函數將在下面詳述。
2、apr_signal_block和apr_signal_unblock
這兩個函數分別負責阻塞和取消阻塞內核傳遞某信號給目標進程。其主要利用的就是sigprocmask函數來實現的。每一個進程都有其對應的信號屏蔽字,它讓目標進程可以通知內核「哪些傳給個人信號該阻塞,哪些暢通無阻」。在《Unix高級環境編程》中做者有這麼一段說明「若是在調用sigprocmask後有任何未決的、再也不阻塞的信號,則在sigprocmask返回前,至少將其中之一遞送給該進程。」能理解這句我想信號屏蔽字這塊兒也就沒什麼問題了。在Unix高級環境編程》中做者舉了一個很不錯的例子,講解的也很詳細。這裏想舉例說明的是:若是屢次調用SET_BLOCK的sigprocmask設置屏蔽字,結果是什麼呢?
E.G 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)) {
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 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");
}
exit(0);
}
//output:
1st to wait 30 seconds
^/
SIGQUIT pending
SIGUSR1 unpending
2nd to wait 30 seconds -- 這以後發送kill -USR128821
SIGQUIT pending
SIGUSR1 pending
第一次輸出SIGUSR1unpending是由於並未發送USR1信號,因此天然爲unpending狀態;我想說的是第二次從新sigprocmask時咱們僅加入了SIGUSR1,並未顯示加入SIGQUIT,以後察看pending信號中SIGQUIT仍然爲pending狀態,這說明兩次SET_BLOCK的sigprocmask調用是"或"的關係,第二次SET_BLOCK的sigprocmask調用不會將第一次SET_BLOCK的sigprocmask調用設置的阻塞信號變爲非阻塞的。
4、總結
信號簡單而強大,若是想深刻了解signal的實現,參考資料中的第二本書會給你滿意的答案。
5、參考資料:
1、《Unix高級環境編程》
2、《深刻理解Linux內核》
[注1]
忽略信號和阻塞信號
前者至關於一個消費行爲,該信號的狀態爲「已消費」,然後者只是將信號作緩存,等待阻塞打開,再交給進程消費,其狀態爲「未消費」,也至關於處於pending狀態。
APR分析-文件IO篇
APR File I/O源代碼的位置在$(APR_HOME)/file_io目錄下,本篇blog着重分析unix子目錄下的相關.c文件內容,其相應頭文件爲$(APR_HOME)/include/apr_file_io.h和apr_file_info.h。
1、APR File I/O介紹
APR用了"不小的篇幅"來"描述"文件I/O,在$(APR_HOME)/file_io/unix目錄下,你會看到多個.c文件,每一個.c都是一類文件I/O操做。好比:
open.c -- 封裝了文件的打開、關閉、更名和刪除等操做;
readwrite.c -- 顧名思義,它裏面包含了文件的讀寫操做;
pipe.c -- 包含了pipe相關操做。
還有許多這裏很少說,因爲文件I/O操做複雜,咱們下面將僅挑出最經常使用的文件I/O操做進行分析。
2、基本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;
intbufpos; /* Read/Write position in buffer */
unsigned long dataRead; /* amountof valid data read into buffer */
intdirection; /*buffer being used for 0 = read, 1 = write */
unsigned long filePtr; /*position in file of handle */
#if APR_HAS_THREADS
struct apr_thread_mutex_t *thlock;
#endif
};
在這個數據結構中有些字段的含義一目瞭然,如filedes、fname、is_pipe等,而有些呢即便看了註釋也不可以立刻了解其真正的含義,這就須要在閱讀源碼時來體會。
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);
其中fname、flag和perm三個參數你應該很眼熟吧:)。每一個封裝都有自定義的一些標誌宏,這裏也不例外,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操做次數,會帶來性能上明顯的改善。這裏將用圖示說明緩衝區與文件的對應關係,以幫助理解APR緩衝I/O:
thefile->filePtr
|
0 /|/ 文件末尾
-----------------------------------------------
/////////////////// <---- thefile->filedes (文件)
-----------------------------------------------
/ /
/ /
/ /
0|/_ _/| APR_FILE_BUFSIZE
-----------------------------------------------
//////////////////////// (緩衝區)
//////////
-----------------------------------------------
/|/ /|/ /|/
| | |
| | thefile->dataRead
| thefile->bufpos
thefile->buffer
說明:"//////"-- 表示從文件讀到緩衝區的數據;
"//////" --表示從用戶已從緩衝區讀出的數據。
thefile->bufpos : 緩衝區中的讀寫位置
thefile->dataRead: 標識緩衝區從文件讀取的數據的大小
thefile->fileptr: 標識文件自己被讀到什麼位置
讀寫切換:若是先讀後寫,則每次寫的時候都要從新定位文件指針到上次讀的結尾處;若是先寫後讀,則每次讀前都要flush緩衝區。
(2)非阻塞I/O帶超時等待
這裏分析下面一段apr_file_read的代碼:
do {
rv = read(thefile->filedes, buf, *nbytes);
} while (rv == -1&& errno == EINTR); --------------(a)
#ifdef USE_WAIT_FOR_IO
if (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 = read(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中內存的工做,這裏不詳述了。
3、總結
複雜的文件I/O,讓咱們經過三言兩語就說完了。你們慢慢體會,看看世界著名開源項目的源代碼,收穫是頗豐的,不妨嘗試一下。
APR分析-高級IO篇
1、記錄鎖或(區域鎖)[注1]
我見過的對記錄鎖講解最詳細的書就是《Unix高級環境編程》,特別是關於進程、文件描述符和記錄鎖三者之間關係的講解更是讓人受益不淺,有此書的朋友必定不要放過喲。這裏將其中的三原則摘錄到這:
關於記錄鎖的自動繼承和釋放有三條規則:
(1) 鎖與進程、文件兩方面有關。這有兩重含意:第一重很明顯,當一個進程終止時,它所創建的鎖所有釋放;第二重意思就不很明顯,任什麼時候候關閉一個描述符時,則該進程經過這一描述符能夠存訪的文件上的任何一把鎖都被釋放(這些鎖都是該進程設置的)。
(2) 由fork產生的子程序不繼承父進程所設置的鎖。這意味着,若一個進程獲得一把鎖,而後調用fork,那麼對於父進程得到的鎖而言,子進程被視爲另外一個進程,對於從父進程處繼承過來的任一描述符,子進程要調用fcntl以得到它本身的鎖。這與鎖的做用是相一致的。鎖的做用是阻止多個進程同時寫同一個文件(或同一文件區域)。若是子進程繼承父進程的鎖,則父、子進程就能夠同時寫同一個文件。
(3) 在執行exec後,新程序能夠繼承原執行程序的鎖。
話歸正題談APR的記錄鎖,平心而論APR的提供的加索和解鎖接口並無什麼獨到的地方,APR之因此將之封裝起來,無非是爲了提供一個統一的跨平臺接口,而且不破壞APR總體代碼風格的一致性。APR記錄鎖源碼位置在$(APR_HOME)/file_io/unix目錄下flock.c,頭文件仍然是apr_file_io.h。apr_file_lock和apr_file_unlock僅提供對整個文件的加鎖和解鎖,而並不支持對文件中任意範圍數據的加鎖和解鎖。至於該鎖是建議鎖(advisory lock)仍是強制鎖(mandatory lock),須要看具體的平臺的實現了。兩個函數均利用fcntl實現記錄鎖功能(前提是所在平臺支持fcntl,因爲fcntl是POSIX標準,絕大多數平臺都支持)。代碼中有一處值得鑑賞:
while ((rc = fcntl(thefile->filedes, fc, &l)) < 0&& errno == EINTR)
continue;
這裏這麼作的緣由就是考慮到fcntl的調用可能被某信號中斷,一旦中斷咱們去要重啓fcntl函數。
2、I/O多路複用[注2]
在經典的《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)
做者同時對這5種I/O模型做了很詳細的對比分析,很值得一看。這裏所說的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的實現(That is 使用poll來實現apr_poll及apr_pollset_xx相關,與之對應的是使用select來實現apr_poll及apr_pollset_xx相關)來分析:在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 */
};
把數據結構定義貼出來便於後面分析時參照理解。
假設咱們像這樣apr_pollset_create(&mypollset,10, p, 0)調用,那麼在apr_pollset_create後,咱們能夠用圖示來表示mypollset變量的狀態:
mypollset
-------
nalloc ----> 10 /* 該mypollset的「容量」,在create的時候由參數指定 */
-------
nelts ----> 0 /* 剛初始化,mypollset中並無任何element,以後每add一次,nelts就+1 */
-------
---------------------------------------------
pollset ---------> pollset[0] | pollset[1] |...|pollset[nalloc-1]
---------------------------------------------
-------
-----------------------------------------------------
query_set ---------> query_set[0] | query_set[1] |...|query_set[nalloc-1]
-----------------------------------------------------
-------
---------------------------------------------------------
result_set ---------> result_set[0] | result_set[1] |...|result_set[nalloc-1]
---------------------------------------------------------
-------
pollset、query_set和result_set這幾個集合的關係經過下圖說明:
apr_pollfd_t *descriptor ---> [pollset_add]--------> query_set ------ [pollset_poll] -----> result_set (輸出)
| /|/
------------------->pollset ------ [pollset_poll] --------------------
apr_pollset_xx系列是改版後APR I/O複用新增的接口集,它以apr_pollset_t做爲其管理的基本單位,其中apr_pollset_poll用於監視pollset中的全部descriptor(s)。而apr_poll則是舊版的APR I/O複用接口,它一樣能夠實現apr_pollset_poll的功能,只是它的基本管理單位是apr_pollfd_t,其相關函數還包括apr_poll_setup、apr_poll_socket_add等在apr-1.1.1版中已看不到的幾個接口。新版本中建議使用apr_pollset_poll,起碼APR的測試用例(testpoll.c)是這麼作的。
select實現的思路與poll實現的思路是一致的,只是apr_pollset_t的結構不一樣,緣由不言自明。
3、總結
因爲APR對高級I/O的封裝很「薄」,因此基本上沒有太多很精緻的東西。
4、參考資料
1、《Unix高級環境編程》
2、《Unix網絡編程卷1、2》
[注1]
對於Unix,「記錄」這個定語也是誤用,由於Unix內核根本沒有使用文件記錄這種概念。一個更適合的術語多是「區域鎖」,由於它鎖定的只是文件的一個區域(也多是整個文件)-- 摘自《Unix高級環境編程》。
[注2]
在《Unix網絡編程卷1》譯者譯爲"多路複用",在《Unix高級環境編程》中譯者譯爲"多路轉接",我更傾向於前者。I/O多路複用其英文爲"I/OMultiplexing"。
APR分析-共享內存篇
APR共享內存封裝的源代碼的位置在$(APR_HOME)/shmem目錄下,本篇blog着重分析unix子目錄下的shm.c文件內容,其相應頭文件爲$(APR_HOME)/include/apr_shm.h。
1、共享內存簡單小結
共享內存是最快的IPC方式,由於一旦這樣的共享內存段映射到各個進程的地址空間,這些進程間經過共享內存的數據傳遞就不須要內核的幫忙了。Stevens的解釋是「各進程不是經過執行任何進入內核的系統調用來傳遞數據,顯然內核的責任僅僅是創建各進程地址空間與共享內存的映射,固然像處理頁面故障這一類的底層活仍是要作的」。相比之下,管道和消息隊列交換數據時都須要內核來中轉數據,速度就相對較慢。
Unix「歷史悠久」,因此在歷史上不一樣版本的Unix提供了不一樣的支持共享內存的方式,我想這也是Stevens在《Unix網絡編程第2卷》中花費三章來說解共享內存的緣由吧。你也不妨先看看shm.c中的代碼,代碼用條件宏分割不一樣Share Memory的實現。
2、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_ZERO
xxxx ---------- (1)
#elif APR_USE_SHMEM_MMAP_ANON
xxxx ---------- (2)
#endif
#endif /* APR_USE_SHMEM_MMAP_ZERO || APR_USE_SHMEM_MMAP_ANON */
#if APR_USE_SHMEM_SHMGET_ANON
xxxx ---------- (3)
#endif
} else { /* 建立有名shm */
#if APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM
#if APR_USE_SHMEM_MMAP_TMP
xxxx ---------- (4)
#endif
#if APR_USE_SHMEM_MMAP_SHM
xxxx ---------- (5)
#endif
#endif /* APR_USE_SHMEM_MMAP_TMP || APR_USE_SHMEM_MMAP_SHM */
#if APR_USE_SHMEM_SHMGET
xxxx ---------- (6)
#endif
}
}
apr_shm_create函數代碼很長,之因此這樣是由於其支持多種建立Share Memory的方式,在上面的僞代碼中共用條件宏分隔了6種方式,這6種方式將在下面分析。能夠看出shmem主要分爲"匿名的"和"有名的",其中"有名的"都是經過filename來標識(或經過ftok轉換filename而獲得的shmid來標識)。
其中不一樣版本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作標識。
3、總結
內核架起了多個進程間共享數據的紐帶--共享內存。經過上面的敘述你會發現共享內存的建立其實並不困難,真正困難的是共享內存的管理[注1],在正規的軟件公司像內存/共享內存管理這樣的重要底層功能都是封裝成庫形式的,固然內存管理的內容不是這篇blog重點涉及的內容。
4、參考資料:
1、《Unix網絡編程第2卷》
2、《Unix環境高級編程》
[注1] SIGSEGV和SIGBUS
涉及共享內存的管理就不能不提到訪問共享內存對象。談到訪問共享內存對象就要留神「SIGSEGV和SIGBUS」這兩個信號。
系統分配內存頁來承載內存映射區,因爲內存頁大小是固定的,因此存在多餘的頁空間空閒,好比待映射文件大小爲5000 bytes,內存映射區大小也爲5000 bytes。而一個內存頁大小4096,系統勢必要分配兩頁來承載,這時空閒的有效空間爲從5000-8191,若是進程訪問這段地址空間也不會發生錯誤。可是要超出8191,就會收到SIGSEGV信號,致使程序中止。關於SIGBUS信號的來歷,這裏也舉例說明:若待映射文件大小爲5000 bytes,咱們在mmap時指定內存映射區size = 15000 > 5000,這時內核真正的共享區承載體大小隻有8192(能包容映射文件大小便可),此時在[0,8191]內訪問均沒問題,但在[8192,14999]之間會獲得SIGBUS信號;超出15000訪問時會觸發SIGSEGV信號。
APR分析-環篇
在大學的時候學的不是計算機專業,但大三的時候我所學的專業曾開過一門好像叫「計算機軟件開發基礎」的課,使用的是清華的一本教材,課程的內容包括數據結構。說實話聽過幾節課,那個老師講的還不錯,只是因爲課程目標所限,沒講那麼深罷了。固然我接觸數據結構要早於這門課的開課時間。早在大一下學期就開始到計算機專業旁聽「數據結構」,再說一次實話,雖號稱名校名專業,可是那個老師的講課水平卻不敢恭維。
言歸正傳! 簡單說說環(RING):環是一個首尾相連的雙向鏈表,也就是咱們所說的循環鏈表。對應清華的那本經典的《數據結構》一書中線性表一章的內容,按照書中分類其屬於線性表中的鏈式存儲的一種。環是很常見也很實用的數據結構,相信在這個世界上環的實現不止成千上萬,可是APR RING(按照APR RING源代碼中的註釋所說,APR RING的實現源自4.4BSD)倒是其中較獨特的一個,其最大的特色是其全部對RING的操做都由一組宏(大約30個左右)來實現。在這裏不能逐個分析,僅說說一些讓人印象深入的方面吧。
1、如何使用APR RING?
咱們先來點感性認識! 下面是一個典型的使用APRRING的樣例:
假設環節點的結構以下:
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;
structelem_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)」節點的概念。咱們先看看哨兵節點在整個鏈表中的位置。
sentinel->next = 鏈表的第一個節點;
sentinel->prev = 鏈表的最後一個節點;
可是察看APR RING的源碼你會發現sentinel節點只是個虛擬存在的節點,這個虛擬節點既有數據域(虛擬出來的,不能引用)又有連接域,好似與普通節點並沒有差異。在APR RING的源文件中使用了下面這幅圖來講明sentinel的位置,同時也指出了sentinel和head的關係 -- head即爲sentinel虛擬節點的link域。
普通節點
+->+-------+<--
|struct |
|elem |
+-------+
|prev |
| next|
+-------+
| etc. |
. .
. .
sentinel節點
+->+--------+<--
|sentinel|
|elem |
+--------+
|ring |
| head |
+--------+
再看看下面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都指向了本身。因此判斷APRRING是否爲空只須要判斷RING的首個節點是否爲sentinel虛擬節點便可。APR_RING_EMPTY宏就是這麼作的:
#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)。看看APR RING源代碼就是這樣實現的:
#define APR_RING_SENTINEL(hp, elem, link) /
(struct elem *)((char *)(hp) -APR_OFFSETOF(struct elem, link))
至此APR RING使用一個虛擬sentinel節點分隔RING的首尾節點,已達到對節點操做一致的目的。
3、使用時注意事項
這裏在使用APR RING時有幾點限制:
a) 在定義RING的元素結構時,須要把APR_RING_ENTRY放在結構的第一個字段的位置。
b) 連接一種類型的元素就要使用APR_RING_HEAD宏定義該種類型RING的頭節點類型。學過C++或者瞭解泛型的人可能都會體味到這裏的設計有那麼一點範型的味道。好比:
模板:APR_RING_HEAD(T_HEAD,T) ---- 連接----> T類型元素
實例化:APR_RING_HEAD(elem_head_t,elem_t) --- 連接---->elem_t類型元素
4、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))
你們還能夠模仿寫出反向遍歷的接口APR_RING_REVERSE_TRAVERSE。
APR分析-進程同步篇
進程同步的源代碼的位置在$(APR_HOME)/locks目錄下,本篇blog着重分析unix子目錄下的proc_mutex.c、global_mutex文件內容,其相應頭文件爲$(APR_HOME)/include/apr_proc_mutex.h、apr_global_mutex.h。其用於不一樣進程之間的同步以及多進程多線程中的同步問題。
APR提供三種同步措施,分別爲:
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;
這幾種鎖機制,隨便拿出哪種都很複雜。APR的代碼註釋中強調了一點就是「只有APR_LOCK_DEFAULT」是可移植的。這樣一來用戶若要使用APR進程同步機制接口,就必須顯式指定一種同步機制。
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_SERIALIZE
pthread_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信號量是一個「計數信號量集」。因此咱們在注意的是在初始化時設定好信號量集的屬性以及在調用semop時正確選擇信號量集中的信號量。在APR的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進程同步機制的一個典型使用方法就是:「Createthe mutex in the Parent, Attach to it in the Child」。APR提供接口apr_proc_mutex_child_init在子進程中re-open themutex。
5、小結
APR提供多種鎖機制,因此使用的時候要根據具體應用狀況細心選擇。
APR分析-線程篇
APR線程的源代碼的位置在$(APR_HOME)/threadproc目錄下,本篇blog着重分析unix子目錄下的thread.c文件內容,其相應頭文件爲$(APR_HOME)/include/apr_threadproc.h。
1、線程基礎
《深刻理解計算機系統》(如下稱CS.APP)一書中對線程基礎概念的講解讓我眼前豁然開朗,這裏不妨引述一下:
(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) 線程不像進程那樣有着「父-子」體系,同一個進程內的線程都是「對等的」,主線程與其餘線程不一樣之處就在於其是進程建立的第一個線程。
2、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;
};
這個類型封裝了線程的屬性,不一樣的線程屬性會致使線程的行爲有所不一樣。Pthread提供多種線程屬性設置接口,但是APR並未所有提供,必要時我以爲能夠本身來調用Pthread接口。APR提供的屬性設置接口包括設置線程的可分離性、線程棧大小和棧Guard區域屬性。
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用來等待某個線程結束並釋放其資源。
3、小結
基本的線程管理接口相對較簡單,關鍵是對線程概念的理解。接下來的「線程同步」則是件比較有趣的話題。
APR分析-網絡IO篇
APR網絡I/O的源代碼的位置在$(APR_HOME)/network_io目錄下,本篇blog着重分析unix子目錄下的各.c文件內容,其相應頭文件爲$(APR_HOME)/include/apr_network_io.h。
以程序員的視角來看待網絡,這樣咱們能夠忽略一些網絡的基礎概念。下面將按部就班地接觸網絡,並說明APR是如何支持這些網絡概念的。
1、IP地址 -- 主機通訊
咱們熟知的而且天天工做於其上的因特網是一個世界範圍的主機的集合,這個主機集合被映射爲一個32位(目前)或者64位(未來)IP地址;而IP地址又被映射爲一組因特網域名;一個網絡中的主機上的進程能經過一個鏈接(connection)和任何其餘網絡中的主機上的進程通訊。
1、IP地址存儲
在現在的IPV4協議中咱們通常使用一個unsignedint來存儲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, soAPR 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地址使用的是一個unsignedint整形數來表示。這樣就存在着一個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,至於使用哪一個就看你的喜愛了^_^。
2、SOCKET -- 進程通訊
前面提到過經過一個鏈接(connection)能夠鏈接兩個internet不一樣或相同主機上的不一樣進程,這個鏈接是點對點的。而從Unix內核角度來看,SOCKET則是鏈接的一個端點。每一個SOCKET都有一個地址,其地址由主機IP地址和通信端口號組成。一個鏈接有兩個端點,這樣一個鏈接就能夠由一個SOCKET對惟一表示了。這個SOCKET對是這個樣子的(cliaddr:cliport, servaddr:servport)。
那麼在應用程序中咱們如何獲取和使用這一互聯網上的進程通信利器呢?每一個平臺都爲應用程序提供了一套SOCKET編程接口,APR又在不一樣平臺提供的接口之上進行了封裝,使代碼能夠在不一樣平臺上編譯運行,並且易用性也有所提升。
1、SOCKET描述符
SOCKET屬於系統資源,咱們必須經過系統調用來申請該資源。SOCKET資源的申請相似於FILE,在使用文件時咱們經過調用open函數獲取文件描述符,相似咱們也可經過調用下面的接口來獲取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;
#ifndef HAVE_POLL
int connected;
#endif
int 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 pollsetis used */
apr_pollset_t *pollset;
#endif
};
該結構中的socketdes字段實際上是真正存儲由socket函數返回的SOCKET描述符的,其餘字段都是爲APR本身所使用的,這些字段在Bind、Connect等過程當中使用。另外須要說起的就是要分清SOCKET描述符和SOCKET地址(IP地址,端口號),前者是系統資源,然後者用來描述一個鏈接的一個端點的地址。SOCKET描述符能夠表明任意的SOCKET地址,也能夠綁定到某個固定的SOCKET地址上(在後面有說明)。咱們若是不顯式將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 */
#defineAPR_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_RCVBUF 128 /**< Receive buffer */
#define APR_SO_DISCONNECTED 256 /**< Disconnected*/
... ...
另外從上面這些屬性值(都是2的n次方)能夠看出SOCKET也是使用一個屬性控制字段中的「位」來控制SOCKET屬性的。
再有APR提供一個宏apr_is_option_set來判斷一個SOCKET是否擁有某個屬性。
3、Connect、Bind、Listen、Accept -- 創建鏈接
這裏不詳述C/S模型了,只是說說APR支持C/S模型的一些接口。
(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) 注[1]
if (rc != APR_SUCCESS){
return rc;
}
if (rc == -1 && errno != EISCONN) {
returnerrno; --------- (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描述符佔用了。在服務器編程中Bind幾乎成爲了「必選」之調用,由於通常服務器程序都有本身的「名氣很大」的SOCKET地址,如TELNET服務端口號23等。apr_socket_bind也並未作太多的工做,只是簡單的調用了bind系統接口,並設置了apr_socket_t結構的幾個local_addr字段。
(3) apr_socket_listen
按照《Unix網絡編程 Vol1》的說法,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 = read(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可用。這裏我以爲好像有個問題,想象一下若是上一次SOCKET的狀態爲APR_INCOMPLETE_READ,那麼從新調用apr_socket_read後在SOCKET屬性中去掉APR_INCOMPLETE_READ,而後進入apr_wait_for_io_or_timeout過程,一旦apr_wait_for_io_or_timeout失敗,那麼就直接返回了。而實際上SOCKET仍然應該處於APR_INCOMPLETE_READ狀態,而下次再調用apr_socket_read就直接進入一輪完整數據的讀取過程了,不知道這種情形是否可否發生。
(d) 將(*len)設置爲實際從SOCKET Buffer中讀取的字節數,並根據這一實際數據與要求數據做比較來設置sock->options。
(2) apr_socket_send
apr_socket_send負責發送數據到SOCKET Buffer,其實現的方式與apr_socket_recv大同小異,這裏就不分析了。
3、小結
APR Network I/O中還有對Multicast的支持,因爲平時不常接觸,這裏不分析了。
注[1]:
/* in errno.h */
#define EISCONN 133 /* Socket is already connected */
#define EALREADY 149 /* operation already in progress */
#define EINPROGRESS 150 /* operation now in progress */
APR分析-線程同步篇
線程同步的源代碼的位置在$(APR_HOME)/locks目錄下,本篇blog着重分析unix子目錄下的thread_mutex.c、thread_rwlock.c和thread_cond.c文件的內容,其相應頭文件爲(APR_HOME)/include/apr_thread_mutex.h、apr_thread_rwlock.h和apr_thread_cond.h。
因爲APR的封裝過於「淺顯」,實際上也並無多少值得分析的「靚點」。因此本篇其實是在討論線程同步的3種運行模型。
1、互斥量
互斥量是線程同步中最基本的同步方式。互斥量用於保護代碼中的臨界區,以保證在任一時刻只有一個線程或進程訪問臨界區。
1、互斥量的初始化
在POSIX Thread中提供兩種互斥量的初始化方式,以下:
(1) 靜態初始化
互斥量首先是一個變量,Pthread提供預約義的值來支持互斥量的靜態初始化。舉例以下:
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
既然是靜態初始化,那麼必然要求上面的mutex變量須要靜態分配。在APR中並不支持apr_thread_mutex_t的使用預約值的靜態初始化(但能夠變通的利用下面的方式進行靜態分配的mutex的初始化)。
(2) 動態初始化
除了上面的狀況,若是mutex變量在堆上或在共享內存中分配的話,咱們就須要調用一個初始化函數來動態初始化該變量了。在Pthread中的對應接口爲pthread_mutex_init。APR封裝了這一接口,咱們能夠使用下面方式在APR中初始化一個apr_thread_mutex_t變量。
apr_thread_mutex_t *mutex = NULL;
apr_pool_t *pool = NULL;
apr_status_t stat;
stat =apr_pool_create(&pool, NULL);
if (stat !=APR_SUCCESS) {
printf("error in pool %d/n", stat);
} else {
printf("ok in pool/n");
}
stat =apr_thread_mutex_create(&mutex, APR_THREAD_MUTEX_DEFAULT, pool);
if (stat !=APR_SUCCESS) {
printf("error %d in mutex/n", stat);
} else {
printf("ok in mutex/n");
}
2、互斥鎖的軟弱性所在
互斥鎖之軟弱性在於其是一種協做性鎖,其運做時對各線程有必定的要求,即「全部要訪問臨界區的線程必須首先獲取這個互斥鎖,離開臨界區後釋放該鎖」,一旦某一線程不遵循該要求,那麼這個互斥鎖就形同虛設了。以下面的例子:
舉例:咱們有兩個線程,一個線程A遵循要求,每次訪問臨界區均先獲取鎖,而後將臨界區的變量x按偶數值遞增,另外一個線程B不遵循要求直接修改x值,這樣即便在線程A獲取鎖的狀況下仍能修改臨界區的變量x。
static apr_thread_mutex_t *mutex = NULL;
staticint x = 0;
staticapr_thread_t *t1 = NULL;
staticapr_thread_t *t2 = NULL;
static void * APR_THREAD_FUNC thread_func1(apr_thread_t *thd, void*data)
{
apr_time_t now;
apr_time_exp_t xt;
while (1) {
apr_thread_mutex_lock(mutex);
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadA]: own the lock, time[%02d:%02d:%02d]/n", xt.tm_hour,xt.tm_min,
xt.tm_sec);
printf("[threadA]: x = %d/n", x);
if (x % 2 || x == 0) {
x += 2;
} else {
printf("[threadA]: Warning: x變量值被破壞,現從新修正之/n");
x += 1;
}
apr_thread_mutex_unlock(mutex);
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadA]: release the lock, time[%02d:%02d:%02d]/n",xt.tm_hour, xt.tm_min,
xt.tm_sec);
sleep(2);
}
return NULL;
}
static void * APR_THREAD_FUNC thread_func2(apr_thread_t *thd, void*data)
{
apr_time_t now;
apr_time_exp_t xt;
while (1) {
x ++;
now = apr_time_now();
apr_time_exp_lt(&xt, now);
printf("[threadB]: modify the var, time[%02d:%02d:%02d]/n",xt.tm_hour, xt.tm_min, xt.tm_sec);
sleep(2);
}
return NULL;
}
int main(int argc, const char * const * argv, const char * const*env)
{
apr_app_initialize(&argc, &argv, &env);
apr_status_t stat;
//...
/*
* 建立線程
*/
stat =apr_thread_create(&t1, NULL, thread_func1, NULL, pool);
stat =apr_thread_create(&t2, NULL, thread_func2, NULL, pool);
//...
apr_terminate();
return 0;
}
//output
... ...
[threadA]: own the lock, time[10:10:15]
[threadB]: modify the var, time[10:10:15]
[threadA]: x = 10
[threadA]: Warning: x變量值被破壞,現從新修正之
[threadA]: release the lock, time[10:10:15]
固然這個例子不必定很精確的代表threadB在threadA擁有互斥量的時候修改了x值。
2、條件變量
互斥量通常用於被設計被短期持有的鎖,一旦咱們不能肯定等待輸入的時間時,咱們能夠使用條件變量來完成同步。咱們曾經說過I/O複用,在咱們調用poll或者select的時候實際上就是在內核與用戶進程之間達成了一個協議,即當某個I/O描述符事件發生的時候內核通知用戶進程而且將處於掛起狀態的用戶進程喚醒。而這裏咱們所說的條件變量讓對等的線程間達成協議,即「某一線程發現某一條件知足時必須發信號給阻塞在該條件上的線程,將後者喚醒」。這樣咱們就有了兩種角色的線程,分別爲
(1) 給條件變量發送信號的線程
其流程大體爲:
{
獲取條件變量關聯鎖;
修改條件爲真;
調用apr_thread_cond_signal通知阻塞線程條件知足了;------(a)
釋放變量關聯鎖;
}
(2) 在條件變量上等待的線程
其流程大體爲:
{
獲取條件變量關聯鎖;
while (條件爲假) {--------------------- (c)
調用apr_thread_cond_wait阻塞在條件變量上等待;------(b)
}
修改條件;
釋放變量關聯鎖;
}
上面兩個流程中,理解三點最關鍵:
a) apr_thread_cond_signal中調用的pthread_cond_signal保證至少有一個阻塞在條件變量上的線程恢復;在《Unix網絡編程 Vol2》中也談過這裏存在着一個race。即在發送cond信號的同時,該發送線程仍然持有條件變量關聯鎖,那麼那個恢復線程的apr_thread_cond_wait返回時仍然拿不到這把鎖就會再次掛起。這裏的這個race要看各個平臺實現是如何處理的了。
b) apr_thread_cond_wait中調用的pthread_cond_wait原子的將調用線程掛起,並釋放其持有的條件變量關聯鎖;
c) 這裏之因此使用while反覆測試條件,是防止「僞喚醒」的存在,即條件並未知足就被喚醒。因此不管怎樣,喚醒後我都須要從新測試一下條件,保證該條件的的確確知足了。
條件變量在解決「生產者-消費者」問題中有很好的應用,在我之前的一篇blog中也說過這個問題。
3、讀寫鎖
前面說過,互斥量把想進入臨界區而又試圖獲取互斥量的全部線程都阻塞住了。讀寫鎖則改進了互斥量的這種霸道行爲,它區分讀臨界區數據和修改臨界區數據兩種狀況。這樣若是有線程持有讀鎖的話,這時再有線程想讀臨界區的數據也是能夠再獲取讀鎖的。讀鎖和寫鎖的分配規則在《Unix網絡編程 Vol2》中有詳細說明,這裏不詳述。
4、小結
三種同步方式如何選擇?場合不一樣選擇也不一樣。互斥量在於徹底同步的臨界區訪問;條件變量在解決「生產者-消費者」模型問題上有獨到之處;讀寫鎖則在區分對臨界區讀寫的時候使用。