首先感谢keyword1983 童鞋,感谢看出了文章RTEMS 4.9.5:QEMU MINI2440 BSP 中的网络驱动开发(下)文章中的错误。

关于RTEMS中的网络同步问题,一直是纠结我的一个大问题。以太网的协议栈的代码庞大复杂,很难剥茧抽丝,获得精髓。加之本人愚钝,工作繁忙,更是难上加难。我在11年3月份做了一些QEMU MINI2440 BSP的移植工作,顺便撰写了部分的关于网络同步的一篇文章。然而这篇文章对网络驱动的同步问题分析的有误。

同步,RTEMS中,无非就是信号量、事件等元素。RTEMS的网络是从BSD中移植过来的,在 rtems_bsdnet_internal.h 中这三个的宏定义为:

#define splnet()    0

#define splimp()    0

#define splx(_s)    do { (_s) = 0; } while(0)

比如说

#define    MGET(m, how, type) { \
      int _ms = splimp(); \
      if (mmbfree == 0) \
        (void)m_mballoc(1, (how)); \
      if (((m) = mmbfree) != 0) { \
        mmbfree = (m)->m_next; \
        mbstat.m_mtypes[MT_FREE]--; \
        (m)->m_type = (type); \
        mbstat.m_mtypes[type]++; \
        (m)->m_next = (struct mbuf *)NULL; \
        (m)->m_nextpkt = (struct mbuf *)NULL; \
        (m)->m_data = (m)->m_dat; \
        (m)->m_flags = 0; \
        splx(_ms); \
    } else { \
        splx(_ms); \
        (m) = m_retry((how), (type)); \
    } \
}

简单的说,BSD中的splimp相当于关闭所有的中断,而splx相当于恢复到splimp执行以前的状态。可是splimp在rtems中被定义成空。啥都没有。所以,这段代码中的splimp和splx实际上起不到任何保护作用。

networking.pdf第2.3 Understand the network scheduling conventions中描述道:

When writing code for the driver transmit and receive tasks, take care to follow the network scheduling conventions. All tasks which are associated with networking share various data structures and resources. To ensure the consistency of these structures the tasks execute only when they hold the network semaphore (rtems_bsdnet_semaphore). The transmit and receive tasks must abide by this protocol. Be very careful to avoid ‘deadly embraces’ with the other network tasks. A number of routines are provided to make it easier for the network driver code to conform to the network task scheduling conventions.

  • void rtems_bsdnet_semaphore_release(void) This function releases the network semaphore. The network driver tasks must call this function immediately before making any blocking RTEMS request.
  • void rtems_bsdnet_semaphore_obtain(void) This function obtains the network semaphore. If a network driver task has released the network semaphore to allow other network-related tasks to run while the task blocks, then this function must be called to reobtain the semaphore immediately after the return from the blocking RTEMS request.
  • rtems_bsdnet_event_receive(rtems_event_set, rtems_option, rtems_interval, rtems_event_set *) The network driver task should call this function when it wishes to wait for an event. This function releases the network semaphore, calls rtems_event_receive to wait for the specified event or events and reobtains the semaphore. The value returned is the value returned by the rtems_event_receive.

(我粗略的翻译如下:)

当为驱动程序编写发送和接收任务时,应注意遵循以下网络调度的约定。所有与网络相关的任务共享各种数据结构和资源。只有当他们持有的网络信号量(rtems_bsdnet_semaphore),才能确保这些任务执行的数据结构的一致性。发送和接收任务必须遵守此协议。要非常小心,以避免和其他网络任务抱死(deadly embraces)。系统提供了一些例程,使网络驱动程序代码编写更容易符合网络任务调度的约定。

  • void rtems_bsdnet_semaphore_release(void):这个函数释放网络信号量。网络驱动程序任务在任何阻塞RTEMS请求之前必须立即调用该函数;
  • void rtems_bsdnet_semaphore_obtain(void):此函数获取网络信号量。如果一个网络驱动任务已释放网络信号量以允许其他被阻塞的网络相关任务执行,那么这个函数必须立即被调用以重新获取信号量以从阻塞的RTEMS请求中返回;
  • rtems_bsdnet_event_receive(rtems_event_set, rtems_option, rtems_interval, rtems_event_set *):网络驱动任务要等待事件时,应调用此功能。这个函数将释放网络信号量,调用rtems_event_receive等待指定的单个或多个事件,并重新获取信号量。返回值是由rtems_event_receive返回的值。

但这段文字,和通篇文章并未教我们如何使用rtems_bsdnet_semaphore_release、rtems_bsdnet_semphore_obtain 和 rtems_bsdnet_event_receive

这段文字似乎告诉我们,信号量(rtems_bsdnet_semaphore)用于网络内部的同步方案。通过阅读相关代码:

rtems_dhcp.c中,我可以看到:

static void
dhcp_task (rtems_task_argument _sdl)
{
  unsigned long       count;
  struct dhcp_packet  call;
  struct sockaddr_dl  *sdl;
  rtems_event_set     event_out;
  unsigned int        timeout = 0;
  int                 error;
  struct proc *procp = NULL;
  int                 disconnected;
 
  sdl = (struct sockaddr_dl *) _sdl;
 
  count = dhcp_elapsed_time;
  disconnected = 0;
 

  while (TRUE)
  {
    /*
     * Sleep until the next poll
     */
    timeout = TOD_MILLISECONDS_TO_TICKS (1000);

    /*这里使用的是rtems_event_receive,并不是rtems_bsdnet_event_receive*/

    rtems_event_receive (RTEMS_EVENT_0,

                         RTEMS_WAIT | RTEMS_EVENT_ANY,
                         timeout, &event_out);


    if(event_out & RTEMS_EVENT_0) break;

    count++;

    if (count >= (dhcp_lease_time / 2))
    {
      rtems_bsdnet_semaphore_obtain ();
      
      dhcp_request_req (&call, &dhcp_req, sdl, TRUE);

      /*
       * Send the Request.
       */
      error = bootpc_call (&call, &dhcp_req, procp);
      if (error) {
        rtems_bsdnet_semaphore_release ();
        printf ("DHCP call failed -- error %d", error);
        continue;
      }

      /*
       * Check for DHCP ACK/NACK
       */
      if (memcmp (&dhcp_req.vend[0],
                  dhcp_magic_cookie,
                  sizeof (dhcp_magic_cookie)) != 0)
      {
        rtems_bsdnet_semaphore_release ();
        printf ("DHCP server did not send Magic Cookie.\n");
        continue;
      }
 
      process_options (&dhcp_req.vend[4], sizeof (dhcp_req.vend) - 4);
 
      if (dhcp_message_type != DHCP_ACK)
      {
        rtems_bsdnet_semaphore_release ();
        printf ("DHCP server did not accept the DHCP request");
        continue;
      }
      
      rtems_bsdnet_semaphore_release ();
      
      count = 0;
    }
  }

  dhcp_task_id = 0;
  printf ("dhcpc: exiting lease renewal task.\n");
  rtems_task_delete( RTEMS_SELF);
}


/*rtems_glue.c 658行*/

rtems_status_code rtems_bsdnet_event_receive (
  rtems_event_set  event_in,
  rtems_option     option_set,
  rtems_interval   ticks,
  rtems_event_set *event_out)
{
    rtems_status_code sc;

    rtems_bsdnet_semaphore_release ();
    sc = rtems_event_receive (event_in, option_set, ticks, event_out);
    rtems_bsdnet_semaphore_obtain ();
    return sc;
}

实际上网络三个任务(发送、接收、服务)的抽象写法是:

void TxDaemon(void *arg)
{
    rtems_event_set events;
    for (;;)
    {
        rtems_bsdnet_event_receive(
            START_TRANSMIT_EVENT,
            RTEMS_EVENT_ANY | RTEMS_WAIT,
            RTEMS_NO_TIMEOUT,
            &events);

        SendPkg();/*这里的SendPkg实际上是被rtems_bsdnet_semaphore_obtain和rtems_bsdnet_semaphore_release所包围*/

                  /*

                  for (;;)

                  {

                      rtems_status_code sc;

                   rtems_bsdnet_semaphore_release ();
                   sc = rtems_event_receive (event_in, option_set, ticks, event_out);
                   rtems_bsdnet_semaphore_obtain ();

                      SendPkg();

                  }

                  */

    }

}


void RxDaemon(void *arg)
{
    rtems_event_set events;
    for (;;) {
        rtems_bsdnet_event_receive(
            START_RECEIVE_EVENT,
            RTEMS_EVENT_ANY | RTEMS_WAIT,
            RTEMS_NO_TIMEOUT,
            &events);

        RecvPkg();/*这里的RecvPkg实际上是被rtems_bsdnet_semaphore_obtain和rtems_bsdnet_semaphore_release所包围,同上*/
    }
}

void NetworkDaemon(void *task_argument)
{
    rtems_status_code sc;
    rtems_event_set events;
    TEMacBuf *m;
    for (;;) {
        sc = rtems_bsdnet_event_receive (NETISR_ETH_EVENT,
                        RTEMS_EVENT_ANY | RTEMS_WAIT,
                        RTEMS_NO_TIMEOUT,
                        &events);

        if (sc == RTEMS_SUCCESSFUL) {/*下面这段代码也被rtems_bsdnet_semaphore_obtain和rtems_bsdnet_semaphore_release所包围,同上*/
            /*其他代码*/

            ......

        }
    }
}


SendPkg和RecvPkg代码中包含些什么呢?主要包含Ethernet对于内存的管理。BSD是自己管理内存的,内存分为两种,一种是mbuf, 另外一个是mbuf cluster, mbuf用来存储一些小数据,而cluster用来存储一些大数据。mbuf相当于一个传送带上的位置。我们的数据必须放到这个传送带上才能发送走。对于整个系统来说,这部分肯定是临界资源,所以访问mbuf及其mbuf cluster必须用信号量保护起来。防止出现意外情况。

除了BSD自己管理的内存,包括原始套接字的一些重入数据结构,全局数据结构等。都需要使用rtems_bsdnet_semaphore_obtain和rtems_bsdnet_semaphore_release保护起来。从这些分析可见,rtems_bsdnet_semaphore_obtain和rtems_bsdnet_semaphore_release就是用来实现网络内部资源的同步的。


RTEMS的设计者们为了防止用户弄不清楚哪些资源是需要保护的,哪些内部资源是不需要保护的,已经把该信号量和rtems_bsdnet_event_receive结合到一起。用户在撰写网络驱动的时候,不再需要了解什么样的资源需要保护。直接按照框架代码去开发就好。

rtems_id
rtems_bsdnet_newproc (char *name, int stacksize, void(*entry)(void *), void *arg)
{
    struct newtask *t;
    char nm[4];
    rtems_id tid;
    rtems_status_code sc;

    strncpy (nm, name, 4);
    sc = rtems_task_create (rtems_build_name(nm[0], nm[1], nm[2], nm[3]),
        networkDaemonPriority,
        stacksize,
        RTEMS_PREEMPT|RTEMS_NO_TIMESLICE|RTEMS_NO_ASR|RTEMS_INTERRUPT_LEVEL(0),
        RTEMS_NO_FLOATING_POINT|RTEMS_LOCAL,
        &tid);

再谈谈三个网络任务是同优先级别(都是调用rtems_bsdnet_newproc创建),又不允许时间轮转调度。这是为什么?现在看来,主要有以下2点:

1. 三个任务都不允许时间轮转调度,任务只能被抢占。三个任务都是按需要执行,避免了任务切换带来的时间开销;由于信号量的存在,如果发生了任务切换,任务切换过去又得切换回来。比如说,Tx任务正在发送,切换到Rx任务,Rx任务得不到信号量,又进入沉睡态,只得又进入Tx任务;这一反一复,就凭空多了两次任务切换。所以不允许时间轮转调度。

2.网络信号量是二值信号量,在发生优先级反转时,采用的是优先级继承算法。大多数时候,网络的三个任务处于沉睡态并按需要执行,如果三个网络任务不是同一优先级别,很容易发生优先级别反转,浪费更多的时间。



int rtems_bsdnet_semaphore_create(void)
{
    rtems_status_code sc;
    sc = rtems_semaphore_create (rtems_build_name('B', 'S', 'D', 'n'),
                    0,
                    RTEMS_PRIORITY |
                        RTEMS_BINARY_SEMAPHORE |
                        RTEMS_INHERIT_PRIORITY |
                        RTEMS_NO_PRIORITY_CEILING |
                        RTEMS_LOCAL,

                    0,
                    &networkSemaphore);
    if (sc != RTEMS_SUCCESSFUL) {
        printf ("Can't create network seamphore: `%s'\n", rtems_status_text (sc));
        return (-1);
    }
    rtems_bsdnet_semaphore_release();
    return (0);
}


用信号量,而不用关闭中断的方式,还是基于实时性的考虑。RTOS一大特性就是对中断快速响应。关闭中断时间太长,实时性会变差。也许有些朋友不这么认为,的确,现在有很多RTOS的应用都不是要求实时性的领域,而是要求从硬件上获取最大的性能。实时性的一个副作用就是最大的利用了硬件的性能。所以,在RTEMS中,应多用二值信号量同步,而不要采用关闭中断的方式。那样代价太高。


最后,做学问讲求的是“踏实 探索”,为我的草率向朋友们道歉,以后写东西一定谨慎谨慎再谨慎。