1. 程式人生 > >對Zebra的一點思考(Think of Zebra)

對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_fetchthread_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_readthread_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等的結果。通常這很簡單,但是如果我們需要新增一些步驟,比如使用者驗證過程呢?

ClientServerRequest AuthResponse PWD?Provide PWDAuth ResultSYS_LOGSYS_LOG_INFOClientServer

再考慮三次認證錯誤觸發黑名單事件!狀態機就是在處理完上一事件後,新增不同的事件執行緒。

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系統》