對Zebra的一點思考(Think of Zebra)
此文並不是針對Zebra的應用,甚至不是一個架構的分析,只是對於Zebra的一點兒思考。
Zebra設計得是如此簡潔明快,每一種資料結構均對應於一定的應用。它們之間以一種鬆耦合的方式共存,而多種資料結構組成的功能模組幾乎完美的結合在一起,完成了非常複雜的功能。它的設計思想就在於對C語言面向物件式的應用。
雖然很多程式均借鑑面向物件設計方式,但是Zebra的程式碼風格是易讀的,非常易於理解和學習。與此同時,Zebra使用了豐富的資料結構,比如連結串列、向量、表和佇列等。它的鬆耦合方式使得每一種資料結構封裝的功能模組很容易被精簡、剝離出來,以備我們特殊的應用。這就是我寫下《Think Of Zebra》非常重要的原因!
1 Zebra中的thread
提起thread就會讓人想起執行緒,Linux中的執行緒被稱為pthread,這裡的thread不是pthread,因為它只是對執行緒的應用層模擬。Zebra藉助自己的thread結構,將所有的事件(比如檔案描述符的讀寫事件、定時器事件等)和對應的處理函式封裝起來,並取名為struct thread
。然後這些thread又被裝入不同的“執行緒”連結串列,掛載到名為thread_master
的結構中。這樣所有的操作只需要面向thead_master
。
/* Thread itself. */
struct thread
{
unsigned char type; /* thread type */
struct thread *next; /* next pointer of the thread */
struct thread *prev; /* previous pointer of the thread */
struct thread_master *master; /* pointer to the struct thread_master. */
int (*func) (struct thread *); /* event function */
void *arg; /* event argument */
union {
int val; /* second argument of the event. */
int fd; /* file descriptor in case of read/write. */
struct timeval sands; /* rest of time sands value. */
} u;
RUSAGE_T ru; /* Indepth usage info. */
};
/* Linked list of thread. */
struct thread_list
{
struct thread *head;
struct thread *tail;
int count;
};
/* Master of the theads. */
struct thread_master
{
struct thread_list read;
struct thread_list write;
struct thread_list timer;
struct thread_list event;
struct thread_list ready;
struct thread_list unuse;
fd_set readfd;
fd_set writefd;
fd_set exceptfd;
unsigned long alloc;
};
thread_master
執行緒管理者維護了6個“執行緒”佇列:read、write、timer、event、ready和unuse。
- read佇列對應於描述符的讀事件。
- write佇列對應於描述符的寫事件。
- timer通常為定時器事件。
- event為自定義事件,這些事件需要我們自己在適合的時候觸發,並且這類事件不需要對描述符操作,也不需要延時。
- ready佇列通常只是在內部使用,比如read、write或event佇列中因事件觸發,就會把該“執行緒”移入ready佇列進行統一處理。
- unuse是在一個“執行緒”執行完畢後被移入此佇列,並且在需要建立一個新的“執行緒”時,將從該佇列中取出,這樣就避免了再次申請記憶體。只有在取不到的情況下才進行新“執行緒”的記憶體申請。
1.2 執行緒管理者中的“執行緒”連結串列函式
struct thread_list
是一個雙向連結串列,對應的操作有:
// 新增thread到指定的連結串列中的尾部
static void thread_list_add (struct thread_list *list, struct thread *thread);
// 新增thread到指定的連結串列中指定的point前部,它在需要對連結串列進行排序的時候很有用
static void thread_list_add_before (struct thread_list *list,
struct thread *point,
struct thread *thread);
// 在指定的連結串列中刪除制定的thread
static struct thread *thread_list_delete (struct thread_list *list, struct thread *thread);
// 釋放指定的連結串列list中所有的thread, m 中的alloc減去釋放的“執行緒”個數
static void thread_list_free (struct thread_master *m, struct thread_list *list);
// 移除list中的第一個thread 並返回
static struct thread *thread_trim_head (struct thread_list *list);
1.3 thread中的read佇列
考慮這樣的應用:建立一個socket,並且需要listen該socket,然後讀取資訊,那麼使用read佇列就是不二選擇。下面是一個例子,這個例子將對標準輸入檔案描述符進行處理:
static int do_accept (struct thread *thread)
{
char buf[1024] = "";
int len = 0;
len = read(THREAD_FD(thread), buf, 1024);
printf("len:%d, %s", len, buf);
return 0;
}
int main()
{
struct thread thread;
// 建立執行緒管理者
struct thread_master *master = thread_master_create();
// 建立讀執行緒,讀執行緒處理的描述符是標準輸入0,處理函式為do_accept
thread_add_read(master, do_accept, NULL, fileno(stdin));
// 列印當前執行緒管理者中的所有執行緒
thread_master_debug(master);
// thread_fetch select所有的描述符,一旦偵聽的描述符需要處理就將對應的“執行緒” 的地址通過thread返回
while(thread_fetch(master, &thread))
{
// 執行處理函式
thread_call(&thread);
thread_master_debug(master);
// 這裡為什麼需要再次新增呢?
thread_add_read(master, do_accept, NULL, fileno(stdin));
thread_master_debug(master);
}
return 0;
}
編譯執行,得到如下的結果:
// 這裡readlist連結串列中加入了一個"執行緒",其他連結串列為空
-----------
readlist : count [1] head [0x93241d8] tail [0x93241d8]
writelist : count [0] head [(nil)] tail [(nil)]
timerlist : count [0] head [(nil)] tail [(nil)]
eventlist : count [0] head [(nil)] tail [(nil)]
unuselist : count [0] head [(nil)] tail [(nil)]
total alloc: [1]
-----------
// 輸入hello,回車
Hello
// thread_call呼叫do_accept進行了操作
len:6, hello
// 發現“執行緒”被移入了unuselist
-----------
readlist : count [0] head [(nil)] tail [(nil)]
writelist : count [0] head [(nil)] tail [(nil)]
timerlist : count [0] head [(nil)] tail [(nil)]
eventlist : count [0] head [(nil)] tail [(nil)]
unuselist : count [1] head [0x93241d8] tail [0x93241d8]
total alloc: [1]
-----------
//再次呼叫thread_add_read發現unuselist被清空,並且“執行緒”再次加入readlist
-----------
readlist : count [1] head [0x93241d8] tail [0x93241d8]
writelist : count [0] head [(nil)] tail [(nil)]
timerlist : count [0] head [(nil)] tail [(nil)]
eventlist : count [0] head [(nil)] tail [(nil)]
unuselist : count [0] head [(nil)] tail [(nil)]
total alloc: [1]
-----------
1.4 thread_fetch
和thread_process_fd
顧名思義,thread_fetch
是用來獲取需要執行的執行緒的,它是整個程式的核心。這裡需要對它進行重點的分析。
/* Fetch next ready thread. */
struct thread * thread_fetch (struct thread_master *m, struct thread *fetch)
{
int num;
int ready;
struct thread *thread;
fd_set readfd;
fd_set writefd;
fd_set exceptfd;
struct timeval timer_now;
struct timeval timer_val;
struct timeval *timer_wait;
struct timeval timer_nowait;
timer_nowait.tv_sec = 0;
timer_nowait.tv_usec = 0;
while (1)
{
/* 最先處理event佇列 */
if ((thread = thread_trim_head (&m->event)) != NULL)
return thread_run (m, thread, fetch);
/* 接著處理timer佇列 */
gettimeofday (&timer_now, NULL);
for (thread = m->timer.head; thread; thread = thread->next)
/* 所有到時間的執行緒均將被處理 */
if (timeval_cmp (timer_now, thread->u.sands) >= 0)
{
thread_list_delete (&m->timer, thread);
return thread_run (m, thread, fetch);
}
/* 處理ready中的執行緒 */
if ((thread = thread_trim_head (&m->ready)) != NULL)
return thread_run (m, thread, fetch);
/* Structure copy. */
readfd = m->readfd;
writefd = m->writefd;
exceptfd = m->exceptfd;
/* Calculate select wait timer. */
timer_wait = thread_timer_wait (m, &timer_val);
/* 對所有描述符進行listen */
num = select (FD_SETSIZE, &readfd, &writefd, &exceptfd, timer_wait);
xprintf("select num:%d\n", num);
if (num == 0)
continue;
if (num < 0)
{
if (errno == EINTR)
continue;
zlog_warn ("select() error: %s", strerror (errno));
return NULL;
}
/* 處理read中執行緒 */
ready = thread_process_fd (m, &m->read, &readfd, &m->readfd);
/* 處理write中執行緒 */
ready = thread_process_fd (m, &m->write, &writefd, &m->writefd);
if ((thread = thread_trim_head (&m->ready)) != NULL)
return thread_run (m, thread, fetch);
}
}
顯然,Zebra中的thread機制並沒有真正的優先順序,而只是在處理的時候優先處理一些佇列。他們的次序是:event、timer、ready、read和write。後面程式碼分析會得出read和write並沒有明顯的先後,因為它們最終都將被移入ready然後再被依次執行。而select同時收到多個描述符事件的概率是很低的。
thread_process_fd
對於read和write執行緒來說是另一個關鍵的函式。
int thread_process_fd (struct thread_master *m, struct thread_list *list,
fd_set *fdset, fd_set *mfdset)
{
struct thread *thread;
struct thread *next;
int ready = 0;
for (thread = list->head; thread; thread = next)
{
next = thread->next;
if (FD_ISSET (THREAD_FD (thread), fdset))
{
assert (FD_ISSET (THREAD_FD (thread), mfdset));
FD_CLR(THREAD_FD (thread), mfdset);
// 將偵聽到的描述符對應的執行緒移到ready連結串列中
thread_list_delete (list, thread);
thread_list_add (&m->ready, thread);
thread->type = THREAD_READY;
ready++;
}
}
return ready;
}
thread_process_fd
將偵聽到的描述符對應的執行緒移到ready連結串列中,並且進行檔案描述的清除操作,檔案描述符的新增在thread_add_read
和thread_add_write
中進行。
1.5 thread中的其他連結串列
write連結串列的操作類似於read連結串列,而event連結串列是直接操作的,timer連結串列只是新增對時間的比對操作。
在加入對應的連結串列時,使用不同的新增函式。
struct thread *thread_add_read (struct thread_master *m, int (*func) (struct thread *), void *arg, int fd);
struct thread *thread_add_write (struct thread_master *m, int (*func) (struct thread *), void *arg, int fd);
struct thread *thread_add_event (struct thread_master *m, int (*func) (struct thread *), void *arg, int fd);
struct thread *thread_add_timer (struct thread_master *m, int (*func) (struct thread *), void *arg, int fd);
1.6 thread機制中的其他函式
// 執行thread
void thread_call (struct thread *thread);
// 直接建立並執行,m引數可以為NULL
struct thread *thread_execute (struct thread_master *m,
int (*func)(struct thread *),
void *arg,
int val);
// 取消一個執行緒,thread中的master指標不可為空
void thread_cancel (struct thread *thread);
// 取消所有event連結串列中的引數為arg的執行緒
void thread_cancel_event (struct thread_master *m, void *arg);
// 類似於thread_call,區別是thread_call只是執行,不將其加入unuse連結串列。thread_run執行後會將其加入unuse連結串列。
struct thread *thread_run (struct thread_master *m,
struct thread *thread,
struct thread *fetch);
// 釋放m及其中的執行緒連結串列
void thread_master_free (struct thread_master *m);
1.7 一些時間相關的函式
static struct timeval timeval_subtract (struct timeval a, struct timeval b);
static int timeval_cmp (struct timeval a, struct timeval b);
當然也提供了簡單的DEBUG函式thread_master_debug
。
2 對Zebra中thread的應用
對thread的應用的探討是最重要的,也是最根本的。Zebra的thread機制,模擬了執行緒,便於平臺間的移植,使流水線式的程式編碼模組化,結構化。
執行緒列表間的組合很容易實現狀態機的功能,可以自定義應用層通訊協議。比如我們定義一個sysstat的遠端監控協議。Client請求Server,請求Code可以為SYS_MEM、SYS_RUNTIME、SYS_LOG等資訊獲取動作,也可以是SYS_REBOOT、SYS_SETTIME等動作請求,Server迴應這個SYS_MEM等的結果。通常這很簡單,但是如果我們需要新增一些步驟,比如使用者驗證過程呢?
再考慮三次認證錯誤觸發黑名單事件!狀態機就是在處理完上一事件後,新增不同的事件執行緒。
3 對Zebra的思考
Zebra由Kunihiro Ishiguro開發於15年前,Kunihiro Ishiguro離開了Zebra,而後它的名字被改成了Quagga,以至於在因特網上輸入Zebra後,你得到只有斑馬的註釋。Zebra提供了一整套基於TCP/IP網路的路由協議的支援,如RIPv1、RIPv2、RIPng、OSPFv2、OSPFv3、BGP等,然而它的亮點並不在於此,而在於它對程式架構的組織。你可以容易的剝離它,使它成為專用的CLI程式;也可以輕易的提取其中的一類資料結構;也可以借用它的thread機制實現複雜的狀態機。
編碼的價值往往不在於寫了多少,而在於對他們的組織!好的組織體現美好的架構、設計的藝術,可以給人啟迪,並在此基礎上激發出更多的靈感。如果一個初學者想學習程式設計的架構,無疑選擇Zebra是一個明智的選擇,你不僅可以學到各種資料結構,基於C的面向物件設計,還有CLI,以及各種網路路由協議,最重要是的Zebra條理清晰,程式碼緊湊,至少不會讓你焦頭爛額。
如果你不知道程式碼中的xprintf是怎麼一回事,那麼看看另一篇文章《一個通用的debug系統》!