本文內容大致翻譯自 libevent-book, 但不是照本翻譯. 成文時, libevent最新的穩定版為 2.1.8 stable. 即本文如無特殊說明, 所有描述均以 2.1.8 stable 版本為準.
本文為系列文章的第一篇, 對應libevent-book的 chapter 0 + chapter 1 + R0 + R1
0. 前提條件
這個文件是對libevent的介紹與指導, 閱讀文件需要你具有以下的能力:
- 你精通C語言
- 你至少了解Unix網路程式設計.
- 你會安裝libevent
- 你大致知道libevent是幹什麼用的.
1. 基本概念: 阻塞/非阻塞/同步/非同步/回撥機制的討論
這裡首先要解釋四個名詞: 阻塞, 非阻塞, 同步, 非同步. 它們都是修飾"介面"的形容詞, 或者說的土一點, 它們都是修飾"函式"的形容詞.
同步, 還是非同步, 是從"訊息通訊"的視角去描述這個介面的行為. 而所謂的訊息通訊, 你可以簡單的把"函式"想象成一個淘寶客服, 把"呼叫方"想象成你自己. 呼叫函式的過程其實就是三步:
- "你"詢問"淘寶客服"一個問題. 比如, "在嗎?". 在這個場景中, 你就是"呼叫方", "淘寶客服"是函式, 而那句"在嗎?", 則是函式引數, 你把函式引數傳遞給函式.
- "淘寶客服"進行後臺處理. 這時淘寶客服接收到了你的詢問訊息, 如果他沒有在忙, 那麼他可以立即回覆你. 如果他現在正在忙, 比如正在吃飯, 比如正在和老婆吵架, 比如淘寶客服需要先看一下你之前的行為記錄, 然後再決定如何回覆你.(比如他看到你正在瀏覽一雙襪子,覺得你在潛在的買家, 他決定回覆你. 比如他看到你三天前下單買了一雙襪子, 但襪子還沒有發貨, 他覺得你有退貨的風險, 從而決定不理你, 假裝不在.) 這個客服思考決斷的過程, 就是函式內部進行處理運算的過程. 當然這個例子很簡單, 有些牽強.
- 最終, 淘寶客服回覆了你, "在的, 親". 這裡, 回覆這個動作, 就是函式返回, 而"在的, 親"這句話, 就是函式的返回值.
你從這個角度去看, 函式呼叫, 就是訊息通訊的過程, 你傳送訊息給函式, 函式經過一番運算思考, 把結果再回發給你.
所謂的同步, 非同步, 指的是:
- 這個淘寶客服很老實, 對於每個顧客發來的問題, 他都需要經過一番思考, 再進行答覆. 這個函式很老實, 對於每個函式呼叫, 都很老實的根據傳入引數進行計算, 再返回結果. 也是是說, 在淘寶客服思考結束之前, 這個客服不會向你傳送答覆, 你也收不到答覆. 也就是說, 在函式運算結束之前, 函式不會返回, 你也得不到返回值. 那麼, 這個客服是同步的, 這個函式呼叫的過程是同步呼叫, 這個函式是同步的.
- 假如這個淘寶客服很不老實, 他裝了一個自動答覆小程式. 對於每個詢問的顧客, 都先自動回覆一句"親, 現在很忙喲, 客服MM可能過一會才能給你答覆". 也就是說, 顧客在發出詢問之後, 立即就能得到一個答覆. 也就是說, 呼叫方在呼叫一個函式之後, 這個函式就立即返回了. 而真正的結果, 可能在過五分鐘之後才會給你. 即是五分鐘之後客服對你說"在的呢, 親". 這樣的函式, 就叫非同步函式.
非同步客服需要解決一個問題: 當真正的運算結果得出之後, 被呼叫的客服如何通知作為呼叫方的你, 取走答案. 在淘寶客戶端上, 是通過手機的震動訊息提醒, 是通過聊天框的紅點.
所以, 關於同步, 和非同步, 這裡做一個稍微正式一點的總結:
- 同步的過程: 呼叫方傳參->函式運算->函式返回運算結果.
- 非同步的過程: 呼叫方傳參->函式說我知道了, 然後過了五分鐘, 函式說我算出來了, 結果在這裡, 你來取.
這裡我們著眼於訊息的傳遞, 通訊方式, 也就是站在函式的角度去看, 結果是如何傳遞給呼叫方的. 同步介面, 運算結果在"函式呼叫"這個場景下就返回給了呼叫方. 非同步介面: 運算結果在"函式呼叫"這個場景之後的某個不定的時刻, 通過某種通知方式, 傳遞給呼叫方.
整個過程中我們忽略了一件事: 就是, 在函式執行運算的過程中, 呼叫方在幹什麼. 也是是, 在淘寶客服內心思考如何回覆你的時候, 你在幹什麼.
這就引出了阻塞與非阻塞:
- 阻塞: 在函式執行運算的過程中, 當前執行緒什麼也做不了. 在等待客服回覆的過程中, 你什麼也不做, 就在那乾等著, 直到他回覆了你.
- 非阻塞: 在函式執行去處的過程中, 當前執行緒可以去做其它事情. 在等待客服回覆的過程中, 你上了個廁所, 還順便洗了個澡.
換句話說:
- 同步與非同步, 描述的是 被呼叫的函式, 如何將結果返回給呼叫者
- 阻塞與非阻塞, 描述的是 呼叫方, 在得到結果之前能不能脫身
這是兩個維度上的邏輯概念, 這兩個維度互相有一定的干涉, 並不是完全正交的兩個維度, 這樣, 既然是兩個維度, 那麼就有四種組合.
- 同步, 且阻塞: 呼叫方發起呼叫直至得到結果之前, 都不能幹其它事情. 被調函式接收到引數直到運算結束之前, 都不會返回.
- 同步, 非阻塞: 呼叫方發起呼叫直至得到結果之前這段時間, 可以做其它事情. 但被調函式接收到引數直到運算結束之前, 都不會返回. 很顯然這個邏輯概念說得通, 但其實是反常理的. 因為: 如果呼叫方在發起呼叫之後, 得到結果(函式返回)之前, 要去做其它事情, 那麼就有一個隱含的前提條件: 呼叫方必須知道本次呼叫的耗時, 且被調方(函式)嚴格遵守這個時間約定. 一毫秒不多, 一毫秒不少. 這在程式碼的世界裡是很難達到的.
- 非同步, 且阻塞: 呼叫方發起呼叫直至得到結果之前, 都不能幹其它事情. 被呼叫函式接收到引數之後立即返回, 但在隨後的某個時間點才把運算結果傳遞給呼叫方. 之後呼叫方繼續活動. 這個邏輯概念依然說得通, 但是很彆扭. 這就相當於, 在你問淘寶客服問題的時候, 淘寶客服的自動回覆機器人已經給你說了"客服很忙喲, 可能過一會才能答覆你", 但你就是啥也不幹, 非得等到客服答覆你之後, 才去上廁所. 這種情景在程式碼世界裡可能發生, 但似乎很智障.
非同步, 非阻塞: 呼叫方發起呼叫直至得到結果之前這段時間, 可以做其它事情. 被調函式接收到引數後立即返回, 但在之後的某一個時間點才把運算結果傳遞給呼叫方. 這說起來很繞口, 舉個栗子, 還是客服:
- 你拿出手機, 向客服傳送訊息, "在嗎?". 然後把手機放桌子上, 轉向上廁所去了.
- 客服收到你的訊息, 機器人回覆你"不好意思, 客服現在很忙, 但我們會盡快答覆你的, 親!".
- 你上廁所回來了, 看手機沒訊息, 又去吃飯了.
- 客服開始處理你的訊息, 終於開始給你真正的回覆"親, 2333號客服為您服務, 你有什麼要了解的嗎?".
- 你吃飯的過程中, 手機震動, 你點開淘寶, 發現有了回覆. 整個流程結束.
可以看到
- 阻塞方式下, 呼叫方總是能第一時間拿到呼叫結果. 因為在阻塞期間, 呼叫方啥也不幹, 就等著函式返回結果. 非阻塞方式下, 呼叫方一般都是在函式返回了結果之後才去檢視運算結果.
- 非同步方式下, 被呼叫方可以推遲處理任務. 客服收到你的訊息後可以先把飯吃完, 函式收到你的呼叫後並不一定立即就開始運算.
- 同步且阻塞, 雙方都是槓精, 都是老實人. 理解起來比較自然.
- 非同步非阻塞, 呼叫方不在乎什麼時候能得到運算結果. 被呼叫方不在乎呼叫方著急不著急, 雙方都是佛系青年. 理解起來也比較自然.
還有一個點要給大家介紹到, 就是回撥函式. 在上面講過, 非同步呼叫, 需要函式以某種機制, 在運算結果得出之後, 將運算結果傳遞給呼叫方. 但回撥函式又繞了一個彎.
假設沒有回撥函式機制, 非同步流程就是:
- 顧客詢問客服, "你們家有沒有紅色36D的胸罩啊? 我想給我老婆買一件, 我老婆的胸是36D的". 然後去上廁所去了
- 自動機器人向顧客回覆"很忙喲, 請耐心等待"
- 客服開始處理顧客的詢問. 去庫房查貨.
- 庫房有貨, 客服要想辦法將這個資訊送到顧客手中. 他通過淘寶客戶端發表了答覆, 淘寶客戶端導致手機震動, 這個震動訊號通知了顧客.
- 顧客在廁所正拉屎, 看到手機上的訊息提醒, 思考了一分鐘, 顧客下單購買了這個胸罩.
這個流程裡顧客做了兩件事:
- 詢問客服"有沒有36D的紅色胸罩". 這是呼叫函式的行為
- 在得到肯定的答覆之後, 下單購買了這個胸罩. 這是得到函式返回的運算結果, 並根據運算結果進一步執行程式流程.(呼叫了另外一個函式: 購買)
而淘寶客服只做了一件事:
- 查詢庫房裡是否有貨
而有了回撥機制後, 非同步流程就是這樣的:
- 顧客詢問客服, "你們家有沒有紅色36D的胸罩?". 然後顧客把手機交給祕書, 叮囑道:"你盯著這個客服, 如果她說有, 你就下單買了, 地址寫我家, 如果沒有, 你就啥也不做". 然後顧客坐上了出差的飛機
- 自動機器人向顧客回覆"很忙喲, 請耐心等待"
- 客服開始處理顧客的詢問. 去庫房查貨.
- 庫房有貨, 客服要想辦法將這個資訊送到顧客手中. 他通過淘寶客戶端發表了答覆, 淘寶客戶端導致手機震動, 這個震動訊號通知了祕書.
- 祕書根據老闆的指示, 下單購買了這個胸罩.
這個流程裡, 顧客做了兩件事:
- 詢問客服"有沒有36D的胸罩". 這是呼叫函式行為.
- 向祕書叮囑. 這是向訊息監控方註冊回撥函式的行為. 訊息監控方負責接收函式的返回結果. 回撥函式則是: "如果有, 就買給老闆夫人, 如果沒有, 就什麼也不做"
淘寶客服只做了一件事:
- 查詢庫房裡是否有貨
而訊息監控方, 也就是祕書, 做了一件事:
- 根據客服的答覆選擇不同的行為. 即在函式呼叫結果得出之後, 呼叫回撥函式.
這就是回撥函式的一個生動的例子, 回撥函式機制中有了一個呼叫結果監控方, 就是祕書, 這個角色承擔著非常重要的職責: 即是在函式返回結果之後, 呼叫對應的回撥函式. 回撥機制一般都實現在非同步呼叫框架之中, 對於寫程式碼的人來說是透明的, 它簡化了呼叫方的職責與智力負擔, 一定程度上抽象了程式碼邏輯, 簡化了程式設計模型(注意: 是一定程度上!). 有了回撥機制:
- 呼叫方不必再去關心函式返回結果以及返回時機. 不必通過輪詢或其它方式去檢查非同步函式是否返回了結果.
- 呼叫方在呼叫時就向呼叫結果監控方註冊合適的回撥, 在呼叫函式那一刻, 將後續業務邏輯寫在回撥函式中, 只負責呼叫就行了. 程式碼越寫越像狀態機.
不過正所謂回撥一時爽, 除錯火葬廠. 寫過JavaScript的同學對這一點一定是深有體會. 當程式不能正確執行的時候, 除錯很蛋疼. 非同步框架本身由於函式返回時機不確定, 除錯就比較蛋疼, 再加上回撥機制, 那真是火葬廠了. 特別是回撥巢狀回撥, 裡面套個七八層的時候, 那真是把圖靈從墳裡挖出來也沒用的絕望場景.
2. 非同步IO與多路複用技術
我們先來看一段經典的同步且阻塞的HTTP客戶端程式:
#include <netinet/in.h> // for socketaddr_in
#include <sys/socket.h> // for socket functions
#include <netdb.h> // for gethostbyname
#include <sys/errno.h> // for errno
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(int argc, char ** argv)
{
const char query[] = "GET / HTTP/1.0\r\n"
"Host: www.baidu.com\r\n"
"\r\n";
const char hostname[] = "www.baidu.com";
struct sockaddr_in sin;
struct hostent * h;
const char * cp;
int fd;
ssize_t n_written, remaining;
char buf[4096];
/*
* Look up the IP address for the hostname.
* Watch out; this isn't threadsafe on most platforms.
*/
h = gethostbyname(hostname);
if(!h)
{
fprintf(stderr, "E: gethostbyname(%s) failed. ErrMsg: %s\n", hostname, hstrerror(h_errno));
return -__LINE__;
}
if(h->h_addrtype != AF_INET)
{
fprintf(stderr, "E: gethostbyname(%s) returned an non AF_INET address.\n", hostname);
return -__LINE__;
}
/*
* Allocate a new socket
*/
fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd < 0)
{
fprintf(stderr, "E: socket failed: %s\n", strerror(errno));
return -__LINE__;
}
/*
* Connect to the remote host
*/
sin.sin_family = AF_INET;
sin.sin_port = htons(80);
sin.sin_addr = *((struct in_addr *)(h->h_addr));
if(connect(fd, (struct sockaddr *)(&sin), sizeof(sin)) != 0)
{
fprintf(stderr, "E: connect to %s failed: %s\n", hostname, strerror(errno));
close(fd);
return -__LINE__;
}
/*
* Write the query
* XXX Can send succeed partially?
*/
cp = query;
remaining = strlen(query);
while(remaining)
{
n_written = send(fd, cp, remaining, 0);
if(n_written < 0)
{
fprintf(stderr, "E: send failed: %s\n", strerror(errno));
close(fd);
return -__LINE__;
}
remaining -= n_written;
cp += n_written;
}
/*
* Get an answer back
*/
while(1)
{
ssize_t result = recv(fd, buf, sizeof(buf), 0);
if(result == 0)
{
break;
}
else if(result < 0)
{
fprintf(stderr, "recv failed: %s\n", strerror(errno));
close(fd);
return -__LINE__;
}
fwrite(buf, 1, result, stdout);
}
close(fd);
return 0;
}
在上面的示例程式碼裡, 大部分有關網路與IO的函式呼叫, 都是阻塞式的. 比如gethostbyname
, 在DNS解析成功域名之前是不返回的(或者解析失敗了會返回失敗), connect
函式, 在與對端主機成功建立TCP連線之前是不返回的(或者連線失敗), 再比如recv
與send
函式, 在成功操作, 或明確失敗之前, 也是不返回的.
阻塞式IO確實比較土, 上面的程式編譯執行的時候, 如果你網路狀況不好, 可能會卡一兩秒才會讀到百度的首頁, 這卡就是因為阻塞IO的緣故. 當然, 雖然比較土, 但像這樣的場合, 使用阻塞IO是沒什麼問題的. 但假如你想寫一個程式同時讀取兩個網站的首頁的話, 就比較麻煩了: 因為你不知道哪個網站會先響應你的請求.. 你可以寫一些, 比如像下面這樣的, 很土的程式碼:
char buf[4096];
int i, n;
while(i_still_want_to_read())
{
for(i = 0; i < n_sockets; ++i)
{
n = recv(fd[i], buf, sizeof(buf), 0);
if(n == 0)
{
handle_close(fd[i]);
}
else if(n < 0)
{
handle_error(fd[i], errno);
}
else
{
handle_input(fd[i], buf, n);
}
}
}
如果你的fd[]
陣列裡有兩個網站的連線, fd[0]
接著百度, fd[1]
接著hao123, 假如hao123正常響應了, 可以從fd[1]
裡讀出資料了, 但百度的伺服器被李老闆炸了, 響應不了了, 這時, 上面的程式碼就會卡在i==0
時迴圈裡的n = recv(fd[0], buf, sizeof(buf), 0)
這條語句中, 直到李老闆把伺服器修好. 這就很蛋疼.
當然, 你可以用多執行緒解決這個問題, 多數情況下, 你有一個問題, 你嘗試使用多執行緒解決, 然後你多個了有問題.
上面是一個冷笑話, 多執行緒或多程式是一個解決方案, 通常情況下, 最簡單的套路是使用一個執行緒或程式去建立TCP連線, 然後連線建立成功後, 為每個連線建立獨立的執行緒或程式來進行IO讀寫. 這樣即使一個網站抽風了, 也只阻塞屬於它自己的那個讀寫執行緒或程式, 不會影響到其它網站的響應.
下面是另外一個例子程式, 這是一個服務端程式, 監聽40173埠上的TCP連線請求, 然後把客戶端傳送的資料按ROT13法再回寫給客戶端, 一次處理一行資料. 這個程式使用Unix上的fork()
函式為每個客戶端的連線建立一個獨立的處理程式.
#include <netinet/in.h> // for sockaddr_in
#include <sys/socket.h> // for socket functions
#include <sys/errno.h> // for errno
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define MAX_LINE 16384
char rot13_char(char c)
{
if(
(c >= 'a' && c <= 'm') ||
(c >= 'A' && c <= 'M')
)
{
return c+13;
}
else if(
(c >= 'n' && c <= 'z') ||
(c >= 'N' && c <= 'Z')
)
{
return c-13;
}
else
{
return c;
}
}
void child(int fd)
{
char outbuf[MAX_LINE + 1]; // extra byte for '\0'
size_t outbuf_used = 0;
ssize_t result;
while(1)
{
char ch;
result = recv(fd, &ch, 1, 0);
if(result == 0)
{
break;
}
else if(result == -1)
{
perror("read");
break;
}
if(outbuf_used < sizeof(outbuf))
{
outbuf[outbuf_used++] = rot13_char(ch);
}
if(ch == '\n')
{
send(fd, outbuf, outbuf_used, 0);
outbuf_used = 0;
continue;
}
}
}
void run(void)
{
int listener;
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(40713);
listener = socket(AF_INET, SOCK_STREAM, 0);
int one = 1;
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
if(bind(listener, (struct sockaddr *)(&sin), sizeof(sin)) < 0)
{
perror("bind");
return;
}
if(listen(listener, 16) < 0)
{
perror("listen");
return;
}
while(1)
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listener, (struct sockaddr *)(&ss), &slen);
if(fd < 0)
{
perror("accept");
}
else
{
if(fork() == 0)
{
child(fd);
exit(0);
}
}
}
}
int main(int argc, char ** argv)
{
run();
return 0;
}
你可以使用下面的命令列, 通過netcat
工具向本機的40713傳送資料, 來試驗一下上面的服務端程式碼:
printf "abcdefghijklmnopqrstuvwxyz\n" | nc -4 -w1 localhost 40713
多程式或多執行緒確實是一個還算是比較優雅的, 應對併發連線的解決方案. 這種解決方案的缺陷是: 程式或執行緒的建立是有開銷的, 在某些平臺上, 這個開銷還是比較大的. 這裡優化的方案是使用執行緒, 並使用執行緒池策略. 如果你的機器需要處理上千上萬的併發連線, 這就意味著你需要建立成千上萬個執行緒, 想象一下, 伺服器一般也就十幾個核心, 64個不得了了, 如果有五千併發連線, 5000個執行緒排除輪64個核心的大米, 執行緒排程肯定是個大開銷.
這個時候我們就需要了解一下非阻塞了, 通過下面的Unix呼叫, 可以將一個檔案描述符設定為"非阻塞"的. 明確一下: "非阻塞"描述的是IO函式的行為, 將一個檔案描述符設定為"非阻塞"的, 其實是指, 在這個檔案描述符上執行IO操作, 函式的行為會變成非阻塞的.
fcntl(fd, F_SETFL, O_NONBLOCK);
當這個檔案描述符是socket的檔案描述符時, 我們一般也會直接稱, "把一個socket設定為非阻塞". 將一個socket設定為非阻塞之後, 在對應的檔案描述符上, 無論是執行網路程式設計相關的函式, 還是執行IO相關的函式, 函式行為都會變成非阻塞的, 即函式在呼叫之後就立即返回: 要麼立即返回成功, 要把立即告訴呼叫者: "暫時不可用, 請稍後再試"
有了非阻塞這種手段, 我們就可以改寫我們的訪問網頁程式了: 我們這時可以正確的處理同時下載兩個網站的資料的需求了. 程式碼片斷如下:
int i, n;
char buf[1024];
for(i = 0; i < n_sockets; ++i)
{
fcntl(fd[i], F_SETFL, O_NONBLOCK);
}
while(i_still_want_to_read)
{
for(int i = 0; i < n_sockets; ++i)
{
n = recv(fd[i], buf, sizeof(buf), 0);
if(n == 0)
{
handle_close(fd[i]); // peer was closed
}
else if(n < 0)
{
if(errno == EAGAIN)
{
// do nothing, the kernel didn't have any data for us to read
// retry
}
else
{
handle_error(fd[i], errno);
}
}
else
{
handle_input(fd[i], buf, n); // read success
}
}
}
這樣寫確實解決了問題, 但是, 在對端網站還沒有成功響應的那幾百毫秒裡, 這段程式碼將會瘋狂的死迴圈, 會把你的一個核心佔滿. 這是一個很蛋疼的解決方案, 原因是: 對於真正的資料何時到達, 我們無法確定, 只能開個死迴圈輪詢.
舊式的改進方案是使用一個叫select()
的系統呼叫函式. select()
函式內部維護了三個集合:
- 有資料可供讀取的檔案描述符
- 可以進行寫入操作的檔案描述符
- 出現異常的檔案描述符
select()
函式在這三個集合有至少一個集合不為空的時候返回. 如果三個集合都為空, 那麼select()
函式將阻塞.
下面是使用select()
改進後的程式碼片斷:
fd_set readset;
int i, n;
char buf[1024];
while(i_still_want_to_read)
{
int maxfd = -1;
FD_ZERO(&readset);
// add all of the interesting fds to readset
for(i = 0; i < n_sockets; ++i)
{
if(fd[i] > maxfd)
{
maxfd = fd[i];
}
FD_SET(fd[i], &readset):
}
select(maxfd+1, &readset, NULL, NULL, NULL);
for(int i = 0; i < n_sockets; ++i)
{
if(FDD_ISSET(fd[i], &readset))
{
n = recv(fd[i], &readset);
if(n == 0)
{
handle_close(fd[i]);
}
else if(n < 0)
{
if(errno == EAGAIN)
{
// the kernel didn't have any data for us to read
}
else
{
handle_error(fd[i], errno);
}
}
else
{
handle_input(fd[i], buf, n);
}
}
}
}
使用select()
改進了程式, 但select()
蛋疼的地方在於: 它只告訴你, 三集合中有資料了, 但是: 哪個fd可讀, 哪個fd可寫, 哪個fd有異常, 這些具體的資訊, 它還是沒告訴你. 如果你的fd數量不多, OK, 上面的程式碼沒什麼問題, 但如果你持有著上千個併發連線, 那每次select()
返回時, 你都需要把所有fd都輪一遍.
下面是使用select()
呼叫對rot13服務端示例程式碼的重構
#include <netinet/in.h> // for sockaddr_in
#include <sys/socket.h> // for socket functions
#include <sys/errno.h> // for errno
#include <fcntl.h> // for fcntl
#include <sys/select.h> // for select
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define MAX_LINE 16384
char rot13_char(char c)
{
if(
(c >= 'a' && c <= 'm') ||
(c >= 'A' && c <= 'M')
)
{
return c+13;
}
else if(
(c >= 'n' && c <= 'z') ||
(c >= 'N' && c <= 'Z')
)
{
return c-13;
}
else
{
return c;
}
}
struct fd_state{
char buffer[MAX_LINE];
size_t buffer_used;
int writing;
size_t n_written;
size_t write_upto;
};
struct fd_state * alloc_fd_state(void)
{
struct fd_state * state = malloc(sizeof(struct fd_state));
if(!state)
{
return NULL;
}
state->buffer_used = state->n_written = state->writing = state->write_upto = 0;
return state;
}
void free_fd_state(struct fd_state * state)
{
free(state);
}
void make_nonblocking(int fd)
{
fcntl(fd, F_SETFL, O_NONBLOCK);
}
int do_read(int fd, struct fd_state * state)
{
char buf[1024];
int i;
ssize_t result;
while(1)
{
result = recv(fd, buf, sizeof(buf), 0);
if(result <= 0)
{
break;
}
for(int i = 0; i < result; ++i)
{
if(state->buffer_used < sizeof(state->buffer))
{
state->buffer[state->buffer_used++] = rot13_char(buf[i]);
}
if(buf[i] == '\n')
{
state->writing = 1;
state->write_upto = state->buffer_used;
}
}
}
if(result == 0)
{
return 1;
}
else if(result < 0)
{
if(errno == EAGAIN)
{
return 0;
}
return -1;
}
return 0;
}
int do_write(int fd, struct fd_state * state)
{
while(state->n_written < state->write_upto)
{
ssize_t result = send(fd, state->buffer + state->n_written, state->write_upto - state->n_written, 0);
if(result < 0)
{
if(errno == EAGAIN)
{
return 0;
}
return -1;
}
assert(result != 0);
state->n_written += result;
}
if(state->n_written == state->buffer_used)
{
state->n_written = state->write_upto = state->buffer_used = 0;
}
state->writing = 0;
return 0;
}
void run(void)
{
int listener;
struct fd_state * state[FD_SETSIZE];
struct sockaddr_in sin;
int i, maxfd;
fd_set readset, writeset, exset;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(40713);
for(i = 0; i < FD_SETSIZE; ++i)
{
state[i] = NULL;
}
listener = socket(AF_INET, SOCK_STREAM, 0);
make_nonblocking(listener);
int one = 1;
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
if(bind(listener, (struct sockaddr *)(&sin), sizeof(sin)) < 0)
{
perror("bind");
return;
}
if(listen(listener, 16) < 0)
{
perror("listen");
return;
}
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_ZERO(&exset);
while(1)
{
maxfd = listener;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_ZERO(&exset);
FD_SET(listener, &readset);
for(i = 0; i < FD_SETSIZE; ++i)
{
if(state[i])
{
if(i > maxfd)
{
maxfd = i;
}
FD_SET(i, &readset);
if(state[i]->writing)
{
FD_SET(i, &writeset);
}
}
}
if(select(maxfd + 1, &readset, &writeset, &exset, NULL) < 0)
{
perror("select");
return;
}
if(FD_ISSET(listener, &readset))
{
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listener, (struct sockaddr *)(&ss), &slen);
if(fd < 0)
{
perror("accept");
}
else if(fd > FD_SETSIZE)
{
close(fd);
}
else
{
make_nonblocking(fd);
state[fd] = alloc_fd_state();
assert(state[fd]);
}
}
for(i = 0; i < maxfd + 1; ++i)
{
int r = 0;
if(i == listener)
{
continue;
}
if(FD_ISSET(i, &readset))
{
r = do_read(i, state[i]);
}
if(r == 0 && FD_ISSET(i, &writeset))
{
r = do_write(i, state[i]);
}
if(r)
{
free_fd_state(state[i]);
state[i] = NULL;
close(i);
}
}
}
}
int main(int argc, char ** argv)
{
setvbuf(stdout, NULL, _IONBF, 0);
run();
return 0;
}
但這樣還不夠好: FD_SETSIZE
是一個很大的值, 至少不小於1024. 當要監聽的fd的值比較大的時候, 就很噁心, 遍歷會遍歷很多次. 對於非阻塞IO介面來講, select
是一個很粗糙的解決方案, 這個系統呼叫提供的功能比較薄弱, 只能說是夠用, 但介面確實太屎了, 不好用, 效能也堪優.
不同的作業系統平臺上提供了很多select
的替代品, 它們都用於配套非阻塞IO介面來使單執行緒程式也有一定的併發能力. 這些替代品有poll()
, epoll()
, kqueue()
, evports
和/dev/poll
. 並且這些替代品的效能都比select()
要好的多. 但比較蛋疼的是, 上面提到的所有介面, 幾乎都不是跨平臺的. epoll()
是Linux獨有的, kqueue()
是BSD系列(包括OS X)獨有的. evports
和/dev/poll
是Solaris獨有的. 是的, select()
屬於POSIX標準的一部分, 但就是效能捉急. 也就是說, 如果你寫的程式想跨平臺, 高效能, 你就得自己寫一層抽象, 把不同平臺對於IO多路複用的底層統一起來: 這也就是Libevent乾的事情.
libevent的低階API為IO多路複用提供了統一的介面, 其底層實現在不同的作業系統平臺上都是最高效的實現.
下面, 我們將使用libevent對上面的程式進行重構. 注意: fd_sets
不見了, 取而代之的是一個叫event_base
的結構體.
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
/* For fcntl */
#include <fcntl.h>
#include <event2/event.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#define MAX_LINE 16384
void do_read(evutil_socket_t fd, short events, void *arg);
void do_write(evutil_socket_t fd, short events, void *arg);
char
rot13_char(char c)
{
/* We don't want to use isalpha here; setting the locale would change
* which characters are considered alphabetical. */
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
struct fd_state {
char buffer[MAX_LINE];
size_t buffer_used;
size_t n_written;
size_t write_upto;
struct event *read_event;
struct event *write_event;
};
struct fd_state *
alloc_fd_state(struct event_base *base, evutil_socket_t fd)
{
struct fd_state *state = malloc(sizeof(struct fd_state));
if (!state)
return NULL;
state->read_event = event_new(base, fd, EV_READ|EV_PERSIST, do_read, state);
if (!state->read_event) {
free(state);
return NULL;
}
state->write_event =
event_new(base, fd, EV_WRITE|EV_PERSIST, do_write, state);
if (!state->write_event) {
event_free(state->read_event);
free(state);
return NULL;
}
state->buffer_used = state->n_written = state->write_upto = 0;
assert(state->write_event);
return state;
}
void
free_fd_state(struct fd_state *state)
{
event_free(state->read_event);
event_free(state->write_event);
free(state);
}
void
do_read(evutil_socket_t fd, short events, void *arg)
{
struct fd_state *state = arg;
char buf[1024];
int i;
ssize_t result;
while (1) {
assert(state->write_event);
result = recv(fd, buf, sizeof(buf), 0);
if (result <= 0)
break;
for (i=0; i < result; ++i) {
if (state->buffer_used < sizeof(state->buffer))
state->buffer[state->buffer_used++] = rot13_char(buf[i]);
if (buf[i] == '\n') {
assert(state->write_event);
event_add(state->write_event, NULL);
state->write_upto = state->buffer_used;
}
}
}
if (result == 0) {
free_fd_state(state);
} else if (result < 0) {
if (errno == EAGAIN) // XXXX use evutil macro
return;
perror("recv");
free_fd_state(state);
}
}
void
do_write(evutil_socket_t fd, short events, void *arg)
{
struct fd_state *state = arg;
while (state->n_written < state->write_upto) {
ssize_t result = send(fd, state->buffer + state->n_written,
state->write_upto - state->n_written, 0);
if (result < 0) {
if (errno == EAGAIN) // XXX use evutil macro
return;
free_fd_state(state);
return;
}
assert(result != 0);
state->n_written += result;
}
if (state->n_written == state->buffer_used)
state->n_written = state->write_upto = state->buffer_used = 1;
event_del(state->write_event);
}
void
do_accept(evutil_socket_t listener, short event, void *arg)
{
struct event_base *base = arg;
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listener, (struct sockaddr*)&ss, &slen);
if (fd < 0) { // XXXX eagain??
perror("accept");
} else if (fd > FD_SETSIZE) {
close(fd); // XXX replace all closes with EVUTIL_CLOSESOCKET */
} else {
struct fd_state *state;
evutil_make_socket_nonblocking(fd);
state = alloc_fd_state(base, fd);
assert(state); /*XXX err*/
assert(state->write_event);
event_add(state->read_event, NULL);
}
}
void
run(void)
{
evutil_socket_t listener;
struct sockaddr_in sin;
struct event_base *base;
struct event *listener_event;
base = event_base_new();
if (!base)
return; /*XXXerr*/
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(40713);
listener = socket(AF_INET, SOCK_STREAM, 0);
evutil_make_socket_nonblocking(listener);
#ifndef WIN32
{
int one = 1;
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
}
#endif
if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
perror("bind");
return;
}
if (listen(listener, 16)<0) {
perror("listen");
return;
}
listener_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
/*XXX check it */
event_add(listener_event, NULL);
event_base_dispatch(base);
}
int
main(int c, char **v)
{
setvbuf(stdout, NULL, _IONBF, 0);
run();
return 0;
}
總之:
- 注意連結的時候加上 -levent
- 程式碼量沒有減少, 邏輯也沒有簡化. libevent只是給你提供了一個通用的多路IO介面. 或者叫事件監聽介面.
evutil_socket_t
型別的使用, 與evutil_make_socket_nonblocking()
函式的使用, 均是為也跨平臺相容性. 使用這些型別名與工具函式, 使得在windows平臺上程式碼也能跑起來.
現在, 你看, 非同步IO+事件處理(或者叫多路IO複用), 是單執行緒單程式程式取得併發能力的最佳途徑, 而libevent則是把多平臺的IO多路複用庫給你抽象統一成一層介面了. 這樣代寫的程式碼不需要改動, 就可以執行在多個平臺上.
這樣就有了三個問題:
- 如果我的程式碼需要跨平臺, 或者只需要跨部分平臺(比如我只考慮Linux和BSD使用者, 完全不考慮Windows平臺), 我為什麼不自己把多路IO庫做個簡單的封裝, 為什麼要使用libevent呢? 典型的就是Redis, 用了很薄的一層封裝, 下面統一了
epoll
,kqueue
,evport
,select
等. 為什麼, 我需要使用libevent呢? - 如果將libevent作為一個黑盒去用, 不可避免的問題就是: 它的效能怎麼樣? 它封裝了多個多路IO庫, 在封裝上是否有效能損失?
- 現在是個輪子都說自己解決了跨平臺問題, 那麼libevent在windows上表現怎麼樣? 它能相容IOCP式多路IO庫嗎? 畢竟IOCP的設計思路和
epoll``select``evport``kqueue
等都不一樣.
答案在這裡:
- 你沒有任何理由非得使用libevent, redis就是一個很好的例子. libevent有不少功能, 但如果你只是跨小部分平臺, 並且只關注在多路IO複用上, 那麼真的沒什麼必要非得用libevent. 你完全可以像redis那樣, 用幾百行簡單的把多路IO庫自己封裝一下.
- 基本上這麼講吧: 你使用系統原生非同步IO多路複用介面的效能是多少, 使用libevent就是多少. 說實施libevent裡沒太多的抽象, 介面也沒有多麼好用, 封閉很薄, 和你使用原生介面基本一樣.
- libevent從版本2開始就能搞定windows了. 上面我們使用的是libevent很底層的介面, 其設計思路是遵循*nix上的事件處理模型的, 典型的就是
select
與epoll
: 當網路可讀寫時, 通知應用程式去讀去寫. 而windows上IOCP的設計思路是: 當網路可讀可寫時不通知應用程式, 而是先完成讀與寫, 再通知應用程式, 應用程式直接拿到的就是資料. 當在libevent 2提供的bufferevents
系列介面中, 它將*nix平臺下的設計, 改巴改巴改成了IOCP式的. 使用這個系列的介面不可避免的, 對*nix平臺有效能損失(這和asio封裝網路庫是一樣的做法), 但實話講, IOCP式的設計確實對程式設計師更友好, 程式碼可讀性高了不少.
總的來說, 你應該在如下的場合使用libevent
- 程式碼需要跨多個平臺, 甚至是windows
- 想在*nix平臺上使用IOCP式的事件介面程式設計
- 你不想自己封裝多個平臺上的多路IO介面, 並且自認為, 就算自己做, 做的也肯定沒有libevent好. libevent是一個久經考驗的很基礎的庫. 身經百戰.
下面是使用bufferevents
系列介面, 以IOCP式風格對之前例子程式碼的重構, 體驗一下更人性的事件處理方式:
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
/* For fcntl */
#include <fcntl.h>
#include <event2/event.h>
#include <event2/buffer.h>
#include <event2/bufferevent.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#define MAX_LINE 16384
void do_read(evutil_socket_t fd, short events, void *arg);
void do_write(evutil_socket_t fd, short events, void *arg);
char
rot13_char(char c)
{
/* We don't want to use isalpha here; setting the locale would change
* which characters are considered alphabetical. */
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
void
readcb(struct bufferevent *bev, void *ctx)
{
struct evbuffer *input, *output;
char *line;
size_t n;
int i;
input = bufferevent_get_input(bev);
output = bufferevent_get_output(bev);
while ((line = evbuffer_readln(input, &n, EVBUFFER_EOL_LF))) {
for (i = 0; i < n; ++i)
line[i] = rot13_char(line[i]);
evbuffer_add(output, line, n);
evbuffer_add(output, "\n", 1);
free(line);
}
if (evbuffer_get_length(input) >= MAX_LINE) {
/* Too long; just process what there is and go on so that the buffer
* doesn't grow infinitely long. */
char buf[1024];
while (evbuffer_get_length(input)) {
int n = evbuffer_remove(input, buf, sizeof(buf));
for (i = 0; i < n; ++i)
buf[i] = rot13_char(buf[i]);
evbuffer_add(output, buf, n);
}
evbuffer_add(output, "\n", 1);
}
}
void
errorcb(struct bufferevent *bev, short error, void *ctx)
{
if (error & BEV_EVENT_EOF) {
/* connection has been closed, do any clean up here */
/* ... */
} else if (error & BEV_EVENT_ERROR) {
/* check errno to see what error occurred */
/* ... */
} else if (error & BEV_EVENT_TIMEOUT) {
/* must be a timeout event handle, handle it */
/* ... */
}
bufferevent_free(bev);
}
void
do_accept(evutil_socket_t listener, short event, void *arg)
{
struct event_base *base = arg;
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listener, (struct sockaddr*)&ss, &slen);
if (fd < 0) {
perror("accept");
} else if (fd > FD_SETSIZE) {
close(fd);
} else {
struct bufferevent *bev;
evutil_make_socket_nonblocking(fd);
bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
bufferevent_setcb(bev, readcb, NULL, errorcb, NULL);
bufferevent_setwatermark(bev, EV_READ, 0, MAX_LINE);
bufferevent_enable(bev, EV_READ|EV_WRITE);
}
}
void
run(void)
{
evutil_socket_t listener;
struct sockaddr_in sin;
struct event_base *base;
struct event *listener_event;
base = event_base_new();
if (!base)
return; /*XXXerr*/
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = 0;
sin.sin_port = htons(40713);
listener = socket(AF_INET, SOCK_STREAM, 0);
evutil_make_socket_nonblocking(listener);
#ifndef WIN32
{
int one = 1;
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
}
#endif
if (bind(listener, (struct sockaddr*)&sin, sizeof(sin)) < 0) {
perror("bind");
return;
}
if (listen(listener, 16)<0) {
perror("listen");
return;
}
listener_event = event_new(base, listener, EV_READ|EV_PERSIST, do_accept, (void*)base);
/*XXX check it */
event_add(listener_event, NULL);
event_base_dispatch(base);
}
int
main(int c, char ** argv)
{
setvbuf(stdout, NULL, _IONBF, 0);
run();
return 0;
}
說實話也沒人性到哪裡去, 底層庫就是這樣, libevent還是太基礎了. 算不上十分友好的輪子.
3. Libevent 簡介
現在我們要正式介紹Libevent
3.1 libevent的賣點
- 程式碼跨平臺.
- 效能高. libevent在非阻塞IO+多路複用的底層實現上, 選取的是特定平臺上最快的介面. 比如Linux上用
epoll
, BSD上用kqueue
- 高併發可擴充套件. libevent就是為了那種, 需要維持成千上萬的活動socket連線的應用程式使用的.
- 介面友好. 雖然並沒有友好多少, 但至少比原生的
epoll
要好一點.
3.2 libevent下的各個子模組
evutil
通用型別定義, 跨平臺相關的通用定義, 以及一些通用小函式event and event_base
核心模組. 事件庫. *nix風格的事件模型: 在socket可讀可寫時通知應用程式.bufferevent
對核心事件庫的再一層封裝, IOCP式的事件模型: 在資料已讀已寫後通知應用程式evbuffer
這是bufferevent
模組內部使用的緩衝區實現.evhttp
簡單的HTTP C/S實現evdns
簡單的 DNS C/S實現evrpc
簡單的 RPC實現
總的來說, 作為使用者, 需要關心的是:
evutil
是需要關心的- 對於主在*nix平臺上寫後臺服務端程式的人: 只需要關心
event and event_base
核心庫的用法即可. - 對於跨平臺, 特別是包含win平臺的開發人員: 需要關注
bufferevent
和evbuffer
, 對於核心庫event and event_base
, 可以不關心 evhttp
,evdns
,evrpc
, 如無需要, 可以不用關心
3.3 libevent下的二進位制庫
以下是在連結你的程式碼的時候, 你需要了解的二進位制庫.
libevent_core
包含event and event_base
,evutil
,evbuffer
,bufferevent
中的所有函式libevent_extra
包含協議相關的函式. 包括 HTTP/DNS/RPC 等. 如果你用不到evhttp/evdns/evrpc
裡的函式, 那麼這個庫不用連結.libevent
滿清遺老, 包含了上面兩個庫裡的所有函式. 官方不建議在使用libevent 2.0以上的版本時連結這個庫. 這是個懶人庫.libevent_pthreads
如果你編寫多執行緒應用程式. 那麼這個庫裡包含了基於POSIX執行緒庫的相關函式實現. 如果你沒有用到libevent中有關的多執行緒函式, 那麼這個庫不用連結. 以前這些函式是劃分在libevent_core
中的, 後來被單獨割出來了.注意: 這個庫不是全平臺的.libevent_openssl
這個庫裡的與OpenSSL相關的函式實現. 如果你沒有用到libevent中有關OpenSSL的函式, 那麼這個庫不用連結. 以前這些函式也算在libevent_core
中, 最後也割出來了. 注意: 這個庫也不是全平臺的
3.4 libevent中的標頭檔案
libevent中的標頭檔案分為三類, 所有標頭檔案都位於event2
目錄下. 也就是說在程式碼中你應當這樣寫:
#include <event2/xxxx>
#include <event2/xxxx>
#include <event2/xxxx>
#include <event2/xxxx>
具體有哪些標頭檔案在後續章節會詳細介紹, 目前只介紹這個分類:
- API 標頭檔案. 這些標頭檔案定義了libevent對外的介面. 這些標頭檔案沒有特定字首.
- 相容性 標頭檔案. 這些標頭檔案是為了向前相容老版本libevent存在的, 它們裡面定義了老版本的一些廢棄介面. 除非你是在做老程式碼遷移工作, 否則不建議使用這些標頭檔案.
- 型別定義 標頭檔案. 定義了libevent庫中相關的型別. 這些標頭檔案有共同字尾
_struct.h
3.5 如何將老版本的程式碼遷移到libevent 2上
官方建議大家使用版本2, 但有時候這個世界就是不那麼讓人舒服, 如果你需要和版本1的歷史程式碼打交道, 你可以參照下面的對照表: 老標頭檔案與新標頭檔案的對照表
舊標頭檔案 | 新標頭檔案 |
---|---|
event.h | event2/event*.h, event2/buffer*.h, event2/bufferevent*.h, event2/tag*.h |
evdns.h | event2/dns*.h |
evhttp.h | event2/http*.h |
evrpc.h | event2/rpc*.h |
evutil.h | event2/util*.h |
在當前的2.0版本中, 老的舊標頭檔案其實是不需要替換的, 這些舊標頭檔案依然存在. 但還是建議將他們替換成新標頭檔案, 因為說不定50年後libevent升級到3.0版本, 這些舊標頭檔案就被扔了.
另外還有一些點需要你注意:
- 在1.4版本之前, 只有一個二進位制庫檔案.
libevent
, 裡面是libevent的所有實現. 如今這些實現被分割到了libevent_core
和libevent_extra
兩個庫中. - 在2.0之前, libevent不支援鎖. 也就是說, 2.0之前如果要寫出執行緒安全的程式碼, 你只能避免線上程間共享資料例項. 沒有其它辦法.
3.6 滿清遺老
官方對待老版本是這樣建議的:
- 1.4.7之前的版本被正式廢棄了
- 1.3之前的版本有一堆bug, 用的時候看臉吧.
- 推薦使用2.0後的版本
我對老版本的態度是這樣的: 能幹活就好. 沒有特殊原因, 我是不會做程式碼遷移的. 並且考慮到應用場景, 有時候用老版本也挺好的.
1.4.x版本的libevent被大量專案使用, 其實挺穩定的, 官方不建議使用, 只是官方不再在1.4版本上再加特性修bug了. 1.4版本最後的一個小版本號就停止在7上不動了. 而對於1.3版本, 確實不應該再碰了.
4. 使用libevent的正確姿勢
libevent有幾項全域性設定, 如果你需要改動這幾項設定, 那麼確保在程式碼初始化的時候設定好值, 一旦你的程式碼流程開始了, 呼叫了第一個libevent中的任何函式, 後續強烈建議不要再更改設定值, 否則會引起不可預知的後果.
4.1 libevent中的日誌
libevent預設情況下將把錯誤與警告日誌寫進stderr
, 並且如果你需要一些libevent內部的除錯日誌的話, 也可以通過更改設定來讓其輸出除錯日誌, 以在程式崩潰時提供更多的參考資訊. 這些行為都可以通過自行實現日誌函式進行更改. 下面是libevent相關的日誌介面.
// 以下是日誌級別
#define EVENT_LOG_DEBUG 0
#define EVENT_LOG_MSG 1
#define EVENT_LOG_WARN 2
#define EVENT_LOG_ERR 3
// 以下是已經被廢棄的日誌級別定義
/* Deprecated; see note at the end of this section */
#define _EVENT_LOG_DEBUG EVENT_LOG_DEBUG
#define _EVENT_LOG_MSG EVENT_LOG_MSG
#define _EVENT_LOG_WARN EVENT_LOG_WARN
#define _EVENT_LOG_ERR EVENT_LOG_ERR
// 這個是一個函式指標型別別名, 指向日誌輸出函式
// 日誌輸出函式應當是一個雙參, 無返回值的函式, 第一個引數severity為日誌級別, 第二個引數為日誌字串
typedef void (*event_log_cb)(int severity, const char *msg);
// 這是使用者自定義設定日誌處理函式的介面. 如果呼叫該函式時入參設定為NULL
// 則將採用預設行為
void event_set_log_callback(event_log_cb cb);
比如下面我需要改寫libevent記錄日誌的方式:
#include <event2/event.h>
#include <stdio.h>
// 丟棄日誌
static void discard_cb(int severity, const char * msg)
{
// 這個日誌函式內部什麼也不做
}
// 將日誌記錄至檔案
static FILE * logfile = NULL;
static void write_to_file_cb(int severity, const char * msg)
{
const char * s;
if(!logfile)
{
return;
}
switch(severity)
{
case EVENT_LOG_DEBUG: s = "[DBG]"; break;
case EVENT_LOG_MSG: s = "[MSG]"; break;
case EVENT_LOG_WARN: s = "[WRN]"; break;
case EVENT_LOG_ERR: s = "[ERR]"; break;
default: s = "[???]"; break;
}
fprintf(logfile, "[%s][%s][%s] %s\n", __FILE__, __func__, s, msg);
}
void suppress_logging(void)
{
event_set_log_callback(discard_cb);
}
void set_logfile(FILE * f)
{
logfile = f;
event_set_log_callback(write_to_file_cb);
}
注意: 在自定義的日誌輸出函式中, 不要呼叫其它libevent中的函式! 比如, 如果你要把日誌遠端輸出至網路socket上去, 你還使用了bufferevent來輸出你的日誌, 在目前的libevent版本中, 這會導致一些奇怪的bug. libevent官方也承認這是一個設計上沒有考慮到的點, 這可能在後續版本中被移除, 但截止目前的2.1.8 stable版本, 這個問題都還沒有解決. 不要作死.
預設情況下的日誌級別是EVENT_LOG_MSG
, 也就是說EVENT_LOG_DEBUG
級別的日誌不會呼叫至日誌輸出函式. 要讓libevent輸出除錯級別的日誌, 請使用下面的介面:
#define EVENT_DBG_NONE 0
#define EVENT_DBG_ALL 0xffffffffu
// 如果傳入 EVENT_DBG_NONE, 將保持預設狀態: 不輸出除錯日誌
// 如果傳入 EVENT_DEG_ALL, 將開啟除錯日誌的輸出
void event_enable_debug_logging(ev_uint32_t which);
除錯日誌很詳盡, 通常情況下對於libevent的使用者而言是沒有輸出的必要的. 因為要用到除錯級別日誌的場合, 是你百般無奈, 開始懷疑libevent本身有bug的時候. 雖然從巨集的命名上, 彷彿還存在著 EVENT_DGB_SOMETHING
這樣, 可以單獨控制某個模組的除錯日誌輸出的引數, 但實際上並沒有: 除錯日誌要麼全開, 要麼全關. 沒有中間地帶. 官方宣稱可能在後續的版本中細化除錯日誌的控制.
而如果你要控制其它日誌級別的輸出與否, 請自行實現日誌輸出函式. 比如忽略掉EVENT_LOG_MSG
級別的日誌之類的. 上面的介面只是控制"如果產生了除錯日誌, libevent呼叫或不呼叫日誌輸出函式"而已.
上面有關日誌的介面均定義在<event2/event.h>
中.
- 日誌輸出函式相關介面早在版本1.0時就有了.
event_enable_debug_logging()
介面在2.1.1版本之後才有- 日誌級別巨集名, 在2.0.19之前, 是以下劃線開頭的, 即
_DEBUG_LOG_XXX
, 但現在已經廢棄掉了這種定義, 在新版本中請使用不帶下劃線開頭的版本.
4.2 正確處理致命錯誤
當libevent檢測到有致命的內部錯誤發生時(比如踩記憶體了之類的不可恢復的錯誤), 預設行為是呼叫exit()
或abort()
. 出現這種情況99.99的原因是使用者自身的程式碼出現了嚴重的bug, 另外0.01%的原因是libevent自身有bug.
如果你希望在程式退出之前做點額外的事情, 寫幾行帶fxxk的日誌之類的, libevent提供了相關的入口, 這可以改寫libevent對待致命錯誤的預設行為.
typedef void (*event_fatal_cb)(int err);
void event_set_fatal_callback(event_fatal_cb cb);
注意, 不要試圖強行恢復這種致命錯誤, 也就是說, 雖然libevent給你提供了這麼個介面, 但不要在註冊的函式中試圖讓程式繼續執行. 因為這個時候libevent內部已經有坑了, 如果繼續強行恢復, 結果是不可預知的. 換個說法: 這個函式應該提供的是臨終遺言, 而不應該試圖救死扶傷.
這個函式也定義在 <event2/event.h>
中, 在2.0.3版本之後可用.
4.3 記憶體管理
預設情況下, libevent使用的是標準C庫中的記憶體管理函式, 即malloc()
, realloc()
, free()
等. libevent允許你使用其它的記憶體管理庫, 比如tcmalloc
或jemalloc
. 相關介面如下:
void event_set_mem_functions(void *(*malloc_fn)(size_t sz),
void *(*realloc_fn)(void *ptr, size_t sz),
void (*free_fn)(void *ptr));
介面的第一個引數是記憶體分配函式指標, 第二個引數是記憶體重分配函式指標, 第三個引數是記憶體釋放函式指標.
下面是一個使用的例子:
#include <event2/event.h>
#include <sys/types.h>
#include <stdlib.h>
union alignment
{
size_t sz;
void * ptr;
double dbl;
};
#define ALIGNMENT sizeof(union alignment)
#define OUTPTR(ptr) (((char *)ptr) + ALIGNMENT)
#define INPTR(ptr) (((char *)ptr) - ALIGNMENT)
static size_t total_allocated = 0;
static void * my_malloc(size_t sz)
{
void * chunk = malloc(sz + ALIGNMENT);
if(!chunk) return chunk;
total_allocated += sz;
*(size_t *)chunk = sz;
return OUTPTR(chunk);
}
static void * my_realloc(void * ptr, size_t sz)
{
size_t old_size = 0;
if(ptr)
{
ptr = INPTR(ptr);
old_size = *(size_t*)ptr;
}
ptr = realloc(ptr, sz + ALIGNMENT);
if(!ptr)
{
return NULL;
}
*(size_t *)ptr = sz;
total_allocated = total_allocated - old_size + sz;
return OUTPTR(ptr);
}
static void my_free(void * ptr)
{
ptr = INPTR(ptr);
total_allocated -= *(size_t *)ptr;
free(ptr);
}
void start_counting_bytes(void)
{
event_set_mem_functions(
my_malloc, my_realloc, my_free
);
}
上面這個例子中, 提供了一種記錄全域性記憶體使用量的簡單方案, 非執行緒安全.
對於自定義記憶體管理介面, 需要注意的有:
- 再次重申, 這是一個全域性設定, 一旦設定, 後續所有的libevent函式內部的記憶體操作都會受影響. 並且不要在程式碼流程中途更改設定.
- 自定義的記憶體管理函式, 在分配記憶體時, 返回的指標後必須確保在至少
sz
個位元組可用. - 自定義的記憶體重分配函式, 必須正確處理
realloc(NULL, sz)
這種情況: 即, 使之行為等同於malloc(sz)
. 也必須正確處理realloc(ptr, 0)
這種情況: 即, 使之行為與free(ptr)
相同且返回NULL. - 自定義的記憶體釋放函式, 必須正確處理
free(NULL)
: 什麼也不做. - 自定義的內在分配函式, 必須正確處理
malloc(0)
: 返回NULL. - 如果你在多執行緒環境中使用libevent, 請務必確保記憶體分配函式是執行緒安全的.
- 如果你要釋放一個由libevent建立來的記憶體區域, 請確認你使用的
free()
版本與libevent內部使用的記憶體管理函式是一致的. 也就是說: 如果要操作libevent相關的記憶體區域, 請確保相關的記憶體處理函式和libevent內部使用的內在管理函式是一致的. 或者簡單一點: 如果你決定使用某個記憶體管理庫, 那麼在整個專案範圍內都使用它, 這樣最簡單, 不容易出亂子. 否則應該盡力避免在外部操作libevent建立的記憶體區域.
event_set_mem_functions()
介面也定義在<event2/event.h>
中, 在2.0.2版本後可用.
需要注意的是: libevent在編譯安裝的時候, 可以關閉event_set_mem_functions()
這個特性. 如果關閉了這個特性, 而在專案中又使用了這個特性, 那麼在專案編譯時, 編譯將報錯. 如果要檢測當前引入的libevent庫是否啟用了這個功能, 可以通過檢測巨集EVENT_SET_MEM_FUNCTIONS_IMPLEMENTED
巨集是否被定義來判斷.
4.4 執行緒與鎖
多執行緒程式設計裡的資料訪問是個大難題. 目前的版本里, libevent支援了多執行緒程式設計, 但這個支援法呢, 怎麼講呢, 使用者還是需要知道不少細節才能正確的寫出多執行緒應用. libevent中的資料結構分為三類:
- 有一些資料結構就是非執行緒安全的. 這是歷史遺留問題, libevent在大版本號更新為2後才支援多執行緒, 這些資料結構是從版本1一路繼承下來的, 不要在多執行緒中共享這些例項. 沒辦法.
- 有一些資料結構的例項可以用鎖保護起來, 以在多執行緒環境中共享. 如果你需要在多個執行緒中訪問某個例項, 那麼你需要給libevent說明這個情況, 然後libevent會為這個例項加上適當的鎖保護, 以確保你在多執行緒訪問它時是安全的. 加鎖不需要你去加, 你需要做的只是告訴libevent一聲, 如何具體操作後面再講.
- 有些資料結構, 天生就是帶鎖的. 如果你帶
libevent_pthreads
庫連結你的程式, 那麼這些結構的例項在多執行緒環境中一定的安全的. 你想讓它不安全都沒辦法.
雖然libevent為你寫了一些加鎖解鎖的無聊程式碼, 你不必要手動為每個物件加鎖了, 但libevent還是需要你指定加鎖的函式. 就像你可以為libevent指定其它的記憶體管理庫一樣. 注意這也是一個全域性設定, 請遵循我們一再強調的使用規則: 程式初始化時就定好, 後續不許再更改.
如果你使用的是POSIX執行緒庫, 或者標準的windows原生執行緒庫, 那麼簡單了一些. 設定加解鎖函式只需要一行函式呼叫, 介面如下:
#ifdef WIN32
int evthread_use_windows_threads(void);
#define EVTHREAD_USE_WINDOWS_THREADS_IMPLEMENTED
#endif
#ifdef _EVENT_HAVE_PTHREADS
int evthread_use_pthreads();
#define EVTHREAD_USE_PTHREADS_IMPLEMENTED
#endif
這兩個函式在成功時都返回0, 失敗時返回-1.
這只是加解鎖. 但如果你想要自定義的是整個執行緒庫, 那麼你就需要手動指定如下的函式與結構定義
- 鎖的定義
- 加鎖函式
- 解鎖函式
- 鎖分配函式
- 鎖釋放函式
- 條件變數定義
- 條件變數建立函式
- 條件變數釋放函式
- 條件變數等待函式
- 通知/廣播條件變數的函式
- 執行緒定義
- 執行緒ID檢測函式
這裡需要注意的是: libevent並不會為你寫哪怕一行的多執行緒程式碼, libevent內部也不會去建立執行緒. 你要使用多執行緒, OK, 你用哪種執行緒庫都行, 沒問題. 但你需要將配套的鎖/條件變數/執行緒檢測函式以及相關定義告訴libevent, 這樣libevent才會知道如何在多執行緒環境中保護自己的例項, 以供你在多執行緒環境中安全的訪問.
- 如果你使用的是POSIX執行緒或者windows原生執行緒庫, 就方便了一點, 調一行函式的事情.
- 如果你在使用POSIX純種或windows原生執行緒庫時, 你不想使用POSIX配套的鎖, 那OK, 你在呼叫完
evthread_use_xxx_threads()
之後, 把你自己的鎖函式或者條件變數函式提供給libevent就好了. 注意這種情況下, 在你的程式的其它地方也需要使用你指定的鎖或條件變數. - 而如果你使用的是其它執行緒庫, 也OK, 只不過麻煩一點, 要提供鎖的相關資訊, 要提供條件變數的相關資訊, 也要提供執行緒ID檢測函式
下面是相關的介面
// 鎖模式是 lock 與 unlock 函式的引數, 它指定了加鎖解鎖時的一些額外資訊
// 如果呼叫 lock 或 unlock 時的鎖都不滿足下面的三種模式, 引數傳0即可
#define EVTHREAD_WRITE 0x04 // 鎖模式: 僅對讀寫鎖使用: 獲取或釋放寫鎖
#define EVTHREAD_READ 0x08 // 鎖模式: 僅對讀寫鎖使用: 獲取或釋放讀鎖
#define EVTHREAD_TRY 0x10 // 鎖模式: 僅在加鎖時使用: 僅在可以立即加鎖的時候才去加鎖.
// 若當前不可加鎖, 則lock函式立即返回失敗, 而不是阻塞
// 鎖型別是 alloc 與 free 函式的引數, 它指定了建立與銷燬的鎖的型別
// 鎖型別可以是 EVTHREAD_LOCKTYPE_XXX 之一或者為0
// 所有支援的鎖型別均需要被登記在 supported_locktypes 中, 如果支援多種鎖, 則多個巨集之間用 | 連結構成該欄位的值
// 當鎖型別為0時, 指的是普通的, 非遞迴鎖
#define EVTHREAD_LOCKTYPE_RECURSIVE 1 // 鎖型別: 遞迴鎖, 你必須提供一種遞迴鎖給libevent使用
#define EVTHREAD_LOCKTYPE_READWRITE 2 // 鎖型別: 讀寫鎖, 在2.0.4版本之前, libevent內部沒有使用到讀寫鎖
#define EVTHREAD_LOCK_API_VERSION 1
// 將你要用的有關鎖的所有資訊放在這個結構裡
struct evthread_lock_callbacks {
int lock_api_version; // 必須與巨集 EVTHREAD_LOCK_API_VERSION的值一致
unsigned supported_locktypes; // 必須是巨集 EVTHREAD_LOCKTYPE_XXX 的或組合, 或為0
void *(*alloc)(unsigned locktype); // 鎖分配, 需要指定鎖型別
void (*free)(void *lock, unsigned locktype); // 鎖銷燬, 需要指定鎖型別
int (*lock)(unsigned mode, void *lock); // 加鎖, 需要指定鎖模式
int (*unlock)(unsigned mode, void *lock); // 解鎖, 需要指定鎖模式
};
// 呼叫該函式以設定相關鎖函式
int evthread_set_lock_callbacks(const struct evthread_lock_callbacks *);
// 調該函式以設定執行緒ID檢測函式
void evthread_set_id_callback(unsigned long (*id_fn)(void));
// 將你要用的有關條件變數的所有資訊都放在這個結構裡
struct evthread_condition_callbacks {
int condition_api_version;
void *(*alloc_condition)(unsigned condtype);
void (*free_condition)(void *cond);
int (*signal_condition)(void *cond, int broadcast);
int (*wait_condition)(void *cond, void *lock,
const struct timeval *timeout);
};
// 通過該函式以設定相關的條件變數函式
int evthread_set_condition_callbacks(
const struct evthread_condition_callbacks *);
要探究具體如何使用這些函式, 請看libevent原始碼中的evthread_pthread.c
與evthread_win32.c
檔案.
對於大多數普通使用者來說, 只需要呼叫一下evthread_use_windows_threads()
或evthread_use_pthreads()
就行了.
上面這些函式均定義在 <event2/thread.h>
中. 在2.0.4版本後這些函式才可用. 2.0.1至2.0.3版本中使用了一些舊介面, event_use_pthreads()
等. 有關條件變數的相關介面直至2.0.7版本才可用, 引入條件變數是為了解決之前libevent出現的死鎖問題.
libevent本身可以被編譯成不支援鎖的二進位制庫, 用這種二進位制庫連結你的多執行緒程式碼, bomshakalaka, 跑不起來. 這算是個無用知識點.
另外額外注意: 多執行緒程式, 並且還使用了POSIX執行緒庫和配套的鎖, 那麼你需要連結libevent_pthreads
. windows平臺則不用.
4.5 小知識點: 有關鎖的除錯
libevent有一個額外的特性叫"鎖除錯", 開啟這種特性後, libevent將把它內部有關鎖的所有呼叫都再包裝一層, 以檢測/獲取在鎖呼叫過程中出現的錯誤, 比如:
- 解了一個沒有持有的鎖
- 對一個非遞迴鎖進行了二次加鎖
如果出現了上述錯誤, 則libevent會導致程式退出, 並附送一個斷言錯誤
要開啟這個特性, 呼叫下面的介面:
void evthread_enable_lock_debugging(void);
#define evthread_enable_lock_debuging() evthread_enable_lock_debugging()
注意, 這也是一個全域性設定項, 請遵循: 一次設定, 初始化時就設定, 永不改動的規則.
這個特性在2.0.4版本中開始支援, 當時介面函式名拼寫錯誤了, 少寫了一個g: evthread_enable_lock_debuging()
. 後來在2.1.2版本中把這個錯誤的拼寫修正過來了. 但還是相容了之前的錯誤拼寫.
這個特性吧, 很明顯是libevent內部開發時使用的. 現在開放出來估計是考慮到, 如果你的程式碼中出現了一個bug是由libevent內部加解鎖失誤導致的, 那麼用個特性可以定位到libevent內部. 否則你很難把鍋甩給libevent. 當然這種情況很少見.
4.6 小知識點: 排除不正確的使用姿勢
libevent是一個比較薄的庫, 薄的好處是效能很好, 壞處是沒有在介面上對使用者做過多的約束. 這就導致一些二把刀使用者經常會錯誤的使用libevent. 常見的智障行為有:
- 向相關介面傳遞了一個未初始化的事件結構例項
- 試圖第二次初始化一個正在被使用的事件結構例項
這種錯誤其實挺難發現的, 為了解決這個痛點, libevent額外開發了一個新特性: 在發生上述情況的時候, libevent給你報錯.
但這是一個會額外消耗資源的特性, libevent內部其實是追蹤了每個事件結構的初始化與銷燬, 所以僅在開發測試的時候開啟它, 發現問題, 解決問題. 在實際部署的時候, 不要使用這個特性. 開啟這個特性的介面如下:
void event_enable_debug_mode(void);
再不厭其煩的講一遍: 全域性設定, 初始化時設定, 一次設定, 永不更改.
這個特性開啟後, 也有一個比較蛋疼的事情: 就是如果你的程式碼裡大量使用了event_assign()
來建立事件結構, 可能你的程式在這個特性下會OOM掛掉..原因是: libevent可以通過對event_new()
和event_free()
的追蹤來檢測事件結構例項是否未被初始化, 或者被多次初始化, 或者被非法使用. 但是對於event_assign()
拷貝來的事件結構, 這追蹤就無能為力了, 並且蛋疼的是event_assign()
還是淺拷貝. 這樣, 如果你的程式碼裡大量的使用了event_assign()
, 這就會導致內建的的追蹤功能一旦追上車就下不來了, 完事車太多就OOM掛掉了.
為了避免在這個特性下由於追蹤event_assign()
建立的事件例項(或許這裡叫例項已經不合適了, 應該叫控制程式碼)而導致程式OOM, 可以呼叫下面的函式以解除對這種事件例項的追蹤, 以避免OOM
void event_debug_unassign(struct event * ev);
這樣, 除錯模式下, 相關的追蹤檢測就會放棄追蹤由event_assign
建立的事件. 所以你看, 這個特性也不是萬能的, 有缺陷, 湊合用吧. 在不開啟除錯模式下, 呼叫event_debug_unassign()
函式沒有任何影響
下面是一個例子:
#include <event2/event.h>
#include <event2/event_struct.h>
#include <stdlib.h>
void cb(evutil_socket_t fd, short what, void *ptr)
{
struct event *ev = ptr;
if (ev) // 通過判斷入參是否為NULL, 來確認入參攜帶的事件例項是event_new來的還是event_assign來的
event_debug_unassign(ev); // 如果是event_assign來的, 那麼就放棄對它的追蹤
}
/*
* 下面是一個簡單的迴圈, 等待fd1與fd2同時可讀
*/
void mainloop(evutil_socket_t fd1, evutil_socket_t fd2, int debug_mode)
{
struct event_base *base;
struct event event_on_stack, *event_on_heap; // 一個是棧上的事件例項, 一個是堆上的事件例項
if (debug_mode)
event_enable_debug_mode(); // 開啟除錯模式
base = event_base_new();
event_on_heap = event_new(base, fd1, EV_READ, cb, NULL); // 通過event_new來建立堆上的例項, 並把事件回撥的入參設定為NULL
event_assign(&event_on_stack, base, fd2, EV_READ, cb, &event_on_stack); // 通過event_assign來初始化棧上的例項, 並把事件回撥的入參設定為事件例項自身的指標
event_add(event_on_heap, NULL);
event_add(&event_on_stack, NULL);
event_base_dispatch(base);
event_free(event_on_heap);
event_base_free(base);
}
這個例子也寫的比較蛋疼, 湊合看吧.
另外, 除錯模式下的詳情除錯資訊, 只能通過在編譯時額外定義巨集USE_DEBUG
來附加. 即在編譯時加上-DUSE_DEBUG
來開啟. 加上這個編譯時的巨集定義後, libevent就會輸出一大坨有關其內部流程的詳情日誌, 包括但不限於
- 事件的增加
- 事件的刪除
- 與具體平臺相關的事件通知資訊
這些詳情不能通過呼叫API的方式開啟或關閉. 而開啟除錯模式的API, 在2.0.4版本後才可用.
4.7 檢測當前專案中引用的libevent的版本
介面很簡單, 如下:
#define LIBEVENT_VERSION_NUMBER 0x02000300
#define LIBEVENT_VERSION "2.0.3-alpha"
const char *event_get_version(void); // 獲取字串形式的版本資訊
ev_uint32_t event_get_version_number(void); // 獲取值形式的版本資訊
值形式的版本資訊由一個uint32_t
型別儲存, 從高位到低位, 每8位代表一個版本號. 比如 0x02000300
代表的版本號就是02.00.03.00
. 三級版本號後可能還有一個小版本號, 比如就存在過一個2.0.1.18
的版本
下面是一個在編譯期檢查libevent版本的寫法, 若版本小於2.0.1, 則編譯不通過. 需要注意的是, 編譯期檢查的是巨集裡的值, 如果你的專案構建比較混亂, 很可能出現標頭檔案的版本, 和最終連結的二進位制庫的版本不一致的情況. 所以編譯期檢查也不一定靠譜
#include <event2/event.h>
#if !defined(LIBEVENT_VERSION_NUMBER) || LIBEVENT_VERSION_NUMBER < 0x02000100
#error "This version of Libevent is not supported; Get 2.0.1-alpha or later."
#endif
int
make_sandwich(void)
{
/* Let's suppose that Libevent 6.0.5 introduces a make-me-a
sandwich function. */
#if LIBEVENT_VERSION_NUMBER >= 0x06000500
evutil_make_me_a_sandwich();
return 0;
#else
return -1;
#endif
}
下面是一個在執行時檢查libdvent版本的寫法. 檢查執行期的版本是通過函式呼叫檢查的, 這就保證了返回的版本號一定是連結進的庫的版本號. 這個比較靠譜. 另外需要注意的是, 數值形式的版本號在libevent2.0.1之後才提供. 所以只能比較蠢的用比較字串的方式去判斷版本號
#include <event2/event.h>
#include <string.h>
int
check_for_old_version(void)
{
const char *v = event_get_version();
/* This is a dumb way to do it, but it is the only thing that works
before Libevent 2.0. */
if (!strncmp(v, "0.", 2) ||
!strncmp(v, "1.1", 3) ||
!strncmp(v, "1.2", 3) ||
!strncmp(v, "1.3", 3)) {
printf("Your version of Libevent is very old. If you run into bugs,"
" consider upgrading.\n");
return -1;
} else {
printf("Running with Libevent version %s\n", v);
return 0;
}
}
int
check_version_match(void)
{
ev_uint32_t v_compile, v_run;
v_compile = LIBEVENT_VERSION_NUMBER;
v_run = event_get_version_number();
if ((v_compile & 0xffff0000) != (v_run & 0xffff0000)) {
printf("Running with a Libevent version (%s) very different from the "
"one we were built with (%s).\n", event_get_version(),
LIBEVENT_VERSION);
return -1;
}
return 0;
}
介面和巨集的定義位於 <event2/event.h>
中, 字串形式的版本號在1.0版本就提供了, 數值形式的版本號直至2.0.1才提供
4.8 一鍵釋放所有全域性例項
就算你手動釋放了所有在程式程式碼初始化時建立的libevent物件, 在程式退出之前, 也依然有一些內建的, 對使用者不可見的libevent內部例項以及一些全域性配置例項存在著, 並且存在在堆區. 一般情況下不用管它們: 程式都退出了, 釋放不釋放有什麼區別呢? 反正作業系統會幫你清除的. 但有時你想引入一些第三方的分析工具, 比如檢測記憶體洩漏的工具時, 就會導致這些工具誤報記憶體洩漏.
你可以簡單的調一下下面這個函式, 完成一鍵完全清除:
void libevent_global_shutdown(void);
注意哦: 這個函式不會幫你釋放你自己呼叫libevent介面建立出來的物件哦! 還沒那麼智慧哦!
另外, 很顯然的一點是, 當呼叫了這個函式之後, 再去呼叫其它libevent介面, 可能會出現異常哦! 所以沒事不要呼叫它, 如果你呼叫它, 那麼一定是自殺前的最後一秒.
函式定義在<event2/event.h>
中, 2.1.1版本後可用