对Zebra的一点思考(Think of Zebra)

转自:netwalker.blog.chinaunix.net
http://blog.chinaunix.net/uid-20608849-id-2103544.html

此文并不是针对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等的结果。通常这很简单,但是如果我们需要添加一些步骤,比如用户验证过程呢?

Client Server Request Auth Response PWD? Provide PWD Auth Result SYS_LOG SYS_LOG_INFO Client Server

再考虑三次认证错误触发黑名单事件!状态机就是在处理完上一事件后,添加不同的事件线程。

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值