【lwip】005-lwip內核框架剖析


前言

本筆記主要記錄lwip框架部分,目的是為了對lwip的源碼實現有初步了解,方便後面細節分析。
參考:

5.1 lwip初始化

協議棧初始化lwip_init()init.c文件中。

如果移植入帶系統的工程中,則調用tcpip_init()

  • 調用lwip_init()進行內核初始化。
  • 配置初始化後的鉤子函數,在新內核執行緒tcpip_thread中跑。
  • 創建一個tcpip_mbox郵箱,成員個數為TCPIP_MBOX_SIZE。主要用於接收從底層或者上層傳遞過來的消息。
  • 創建一個lock_tcpip_core內核鎖。
  • 創建一個tcpip_thread執行緒。這個執行緒就是LwIP在作業系統中作為一個獨立的執行緒運行,所有處理的數據都要這個執行緒去處理。
/**
 * @ingroup lwip_os
 * Initialize this module:
 * - initialize all sub modules
 * - start the tcpip_thread
 *
 * @param initfunc a function to call when tcpip_thread is running and finished initializing
 * @param arg argument to pass to initfunc
 */
void
tcpip_init(tcpip_init_done_fn initfunc, void *arg)
{
  lwip_init();

  tcpip_init_done = initfunc; /* 初始化後的鉤子函數 */
  tcpip_init_done_arg = arg; /* 初始化後的鉤子函數的參數 */
  /* 創建內核郵箱 */
  if (sys_mbox_new(&tcpip_mbox, TCPIP_MBOX_SIZE) != ERR_OK) {
    LWIP_ASSERT("failed to create tcpip_thread mbox", 0);
  }
#if LWIP_TCPIP_CORE_LOCKING
  /* 創建內核鎖 */
  if (sys_mutex_new(&lock_tcpip_core) != ERR_OK) {
    LWIP_ASSERT("failed to create lock_tcpip_core", 0);
  }
#endif /* LWIP_TCPIP_CORE_LOCKING */
  /* 創建內核執行緒 */
  sys_thread_new(TCPIP_THREAD_NAME, tcpip_thread, NULL, TCPIP_THREAD_STACKSIZE, TCPIP_THREAD_PRIO);
}

如果在裸機中,可以直接調用lwip_init()

lwip_init()init.c文件中。

lwip_init()源碼:

  • 在NO_SYS模式下才能直接調用lwip_init(),否則,用戶只能調用tcpip_init()來實現初始化,但是最終都是會調用到lwip_init()函數。
  • 通過該函數可以大概了解下lwip核心有什麼組件。
/**
 * @ingroup lwip_nosys
 * Initialize all modules.
 * Use this in NO_SYS mode. Use tcpip_init() otherwise.
 */
void
lwip_init(void)
{
#ifndef LWIP_SKIP_CONST_CHECK
  int a = 0;
  LWIP_UNUSED_ARG(a);
  LWIP_ASSERT("LWIP_CONST_CAST not implemented correctly. Check your lwIP port.", LWIP_CONST_CAST(void *, &a) == &a);
#endif
#ifndef LWIP_SKIP_PACKING_CHECK
  LWIP_ASSERT("Struct packing not implemented correctly. Check your lwIP port.", sizeof(struct packed_struct_test) == PACKED_STRUCT_TEST_EXPECTED_SIZE);
#endif

  /* Modules initialization */
  stats_init();
#if !NO_SYS
  sys_init();
#endif /* !NO_SYS */
  mem_init();
  memp_init();
  pbuf_init();
  netif_init();
#if LWIP_IPV4
  ip_init();
#if LWIP_ARP
  etharp_init();
#endif /* LWIP_ARP */
#endif /* LWIP_IPV4 */
#if LWIP_RAW
  raw_init();
#endif /* LWIP_RAW */
#if LWIP_UDP
  udp_init();
#endif /* LWIP_UDP */
#if LWIP_TCP
  tcp_init();
#endif /* LWIP_TCP */
#if LWIP_IGMP
  igmp_init();
#endif /* LWIP_IGMP */
#if LWIP_DNS
  dns_init();
#endif /* LWIP_DNS */
#if PPP_SUPPORT
  ppp_init();
#endif

#if LWIP_TIMERS
  sys_timeouts_init();
#endif /* LWIP_TIMERS */
}

5.2 內核超時

tcpip協議棧的超時機制也是很重要的一部分。

ARP快取表項的時間管理、IP分片數據報的重裝等待超時、TCP中的建立連接超時、重傳超時機制等都會用到。

LwIP為每個與外界網路連接的任務都有設定了timeout屬性。

其實現源碼主要在timeouts.ctimeouts.h

5.2.1 內核超時機制

內核只有一條超時鏈表static struct sys_timeo *next_timeout;

該鏈表的數據結構是一個有序單向非循環的非通用鏈表。

把需要超時處理的事件按喚醒時間升序插入到該鏈表中。

通過sys_timeouts_sleeptime()函數獲取下次喚醒的時間,到喚醒的時間後就會通過sys_check_timeouts()遍歷next_timeout超時鏈表。

這個只是底層的內核超時機制,另外lwip還基於這個機制再實現一套周期定時機制。

5.2.2 周期定時機制

周期定時機制時基於內核超時機制而實現的。

初始化內核超時機制時,把周期定時函數lwip_cyclic_timer()作為超時函數,lwip_cyclic_timers[]數組保存的回調函數作為lwip_cyclic_timer()的參數,讓其周期回調。

因為超時鏈表next_timeout中的事件超時後會出隊,但是lwip_cyclic_timers()函數裡面會將自己再次入隊,這樣實現周期回調。

5.2.3 內核超時鏈表數據結構

內核超時鏈表:

/** The one and only timeout list */
static struct sys_timeo *next_timeout;

內核超時鏈表數據結構:

struct sys_timeo {
  struct sys_timeo *next; /* 下一個節點 */
  u32_t time; /* 被喚醒的時間 */
  sys_timeout_handler h; /* 回調 */
  void *arg; /* 回調的參數 */
#if LWIP_DEBUG_TIMERNAMES
  const char* handler_name; /* 當前超時事件的描述。調試使用 */
#endif /* LWIP_DEBUG_TIMERNAMES */
};

5.2.4 內核超時初始化

初始化內核超時模組:

/** Initialize this module */
void sys_timeouts_init(void)
{
  size_t i;
  /* tcp_tmr() 不用在初始化時就插入延時鏈表,因為還沒用到 */
  for (i = (LWIP_TCP ? 1 : 0); i < LWIP_ARRAYSIZE(lwip_cyclic_timers); i++) {
    /* 把lwip_cyclic_timers數組保存的回調插入到超時鏈表中 */
    sys_timeout(lwip_cyclic_timers[i].interval_ms, lwip_cyclic_timer, LWIP_CONST_CAST(void *, &lwip_cyclic_timers[i]));
  }
}

lwip_cyclic_timers數組:

/* 這個數組包含所有堆棧內部的循環計時器 */
const struct lwip_cyclic_timer lwip_cyclic_timers[] = {
#if LWIP_TCP
  /* TCP計時器是一種特殊情況:它不必總是運行,初始化內核超時機制時就不會將其插入超時鏈表。可使用tcp_timer_needed()觸發從TCP啟動。*/
  {TCP_TMR_INTERVAL, HANDLER(tcp_tmr)},
#endif /* LWIP_TCP */
#if LWIP_IPV4
#if IP_REASSEMBLY
  {IP_TMR_INTERVAL, HANDLER(ip_reass_tmr)},
#endif /* IP_REASSEMBLY */
#if LWIP_ARP
  {ARP_TMR_INTERVAL, HANDLER(etharp_tmr)},
#endif /* LWIP_ARP */
#if LWIP_DHCP
  {DHCP_COARSE_TIMER_MSECS, HANDLER(dhcp_coarse_tmr)},
  {DHCP_FINE_TIMER_MSECS, HANDLER(dhcp_fine_tmr)},
#endif /* LWIP_DHCP */
#if LWIP_ACD
  {ACD_TMR_INTERVAL, HANDLER(acd_tmr)},
#endif /* LWIP_ACD */
#if LWIP_IGMP
  {IGMP_TMR_INTERVAL, HANDLER(igmp_tmr)},
#endif /* LWIP_IGMP */
#endif /* LWIP_IPV4 */
#if LWIP_DNS
  {DNS_TMR_INTERVAL, HANDLER(dns_tmr)},
#endif /* LWIP_DNS */
#if LWIP_IPV6
  {ND6_TMR_INTERVAL, HANDLER(nd6_tmr)},
#if LWIP_IPV6_REASS
  {IP6_REASS_TMR_INTERVAL, HANDLER(ip6_reass_tmr)},
#endif /* LWIP_IPV6_REASS */
#if LWIP_IPV6_MLD
  {MLD6_TMR_INTERVAL, HANDLER(mld6_tmr)},
#endif /* LWIP_IPV6_MLD */
#if LWIP_IPV6_DHCP6
  {DHCP6_TIMER_MSECS, HANDLER(dhcp6_tmr)},
#endif /* LWIP_IPV6_DHCP6 */
#endif /* LWIP_IPV6 */
};
/* lwip_num_cyclic_timers表示lwip_cyclic_timers數組有多少個成員 */
const int lwip_num_cyclic_timers = LWIP_ARRAYSIZE(lwip_cyclic_timers);

5.2.6 超時的溢出處理

看過freertos內核實現都知道,這個rtos是通過兩條延時鏈表(一條當前延時鏈表和一條溢出延時鏈表)來處理延時溢出的。

而lwip內核超時機制就只有一條超時鏈表,溢出,只能靠其它邏輯判斷了。

TIME_LESS_THAN()宏函數來處理溢出。

/* 首先限制超時最大的差值,這也是單鏈表溢出處理的局限 */
#define LWIP_MAX_TIMEOUT  0x7fffffff
/* t比compare_tocompare_to少就返回1,這個對時間節拍溢出也能正確判斷。原理我不會文字表達,自己找個例子驗證下吧 */
#define TIME_LESS_THAN(t, compare_to) ( (((u32_t)((t)-(compare_to))) > LWIP_MAX_TIMEOUT) ? 1 : 0 )

我還是舉個例子說明下吧:

例子1,正常情況下(時間軸t在c前):t和c比較,c還沒溢出,t-c為負,16進位表示為0x8xxxxxxx,比0x7fffffff大,返回1。

例子2,溢出情況下(時間軸t在c前):t和c比較,c溢出,由於限制了c到t的差距必須在LWIP_MAX_TIMEOUT內,t-c的差值一定大於0xFFFFFFFF-LWIP_MAX_TIMEOUTLWIP_MAX_TIMEOUT,還是比0x7fffffff大,返回1。

5.2.7 註冊超時事件

註冊超時事件使用sys_timeout(),內部會調用sys_timeout_abs()把超時事件插入到超時鏈表next_timeout中。

/**
 * Create a one-shot timer (aka timeout). Timeouts are processed in the
 * following cases:
 * - while waiting for a message using sys_timeouts_mbox_fetch()
 * - by calling sys_check_timeouts() (NO_SYS==1 only)
 *
 * @param msecs time in milliseconds after that the timer should expire
 * @param handler callback function to call when msecs have elapsed
 * @param arg argument to pass to the callback function
 */
#if LWIP_DEBUG_TIMERNAMES
void
sys_timeout_debug(u32_t msecs, sys_timeout_handler handler, void *arg, const char *handler_name)
#else /* LWIP_DEBUG_TIMERNAMES */
void
sys_timeout(u32_t msecs, sys_timeout_handler handler, void *arg)
#endif /* LWIP_DEBUG_TIMERNAMES */
{
  u32_t next_timeout_time;
  /* 確保當前處於TCPIP執行緒安全鎖內 */
  LWIP_ASSERT_CORE_LOCKED();
  /* 超時限制 */
  LWIP_ASSERT("Timeout time too long, max is LWIP_UINT32_MAX/4 msecs", msecs <= (LWIP_UINT32_MAX / 4));
  /* 計算喚醒時間 */
  next_timeout_time = (u32_t)(sys_now() + msecs); /* 由TIME_LESS_THAN宏處理溢出 */

#if LWIP_DEBUG_TIMERNAMES
  sys_timeout_abs(next_timeout_time, handler, arg, handler_name);
#else
  /* 把當前超時事件插入到超時鏈表,成註冊 */
  sys_timeout_abs(next_timeout_time, handler, arg);
#endif
}

超時事件插入超時鏈表sys_timeout_abs()

static void
#if LWIP_DEBUG_TIMERNAMES
sys_timeout_abs(u32_t abs_time, sys_timeout_handler handler, void *arg, const char *handler_name)
#else /* LWIP_DEBUG_TIMERNAMES */
sys_timeout_abs(u32_t abs_time, sys_timeout_handler handler, void *arg)
#endif
{
  struct sys_timeo *timeout, *t;

  /* 從記憶體池中獲取記憶體資源 */
  timeout = (struct sys_timeo *)memp_malloc(MEMP_SYS_TIMEOUT);
  if (timeout == NULL) {
    LWIP_ASSERT("sys_timeout: timeout != NULL, pool MEMP_SYS_TIMEOUT is empty", timeout != NULL);
    return;
  }
  /* 配置超時事件數據結構 */
  timeout->next = NULL;
  timeout->h = handler;
  timeout->arg = arg;
  timeout->time = abs_time;

#if LWIP_DEBUG_TIMERNAMES
  /* 當前超時事件的文本描述 */
  timeout->handler_name = handler_name;
  LWIP_DEBUGF(TIMERS_DEBUG, ("sys_timeout: %p abs_time=%"U32_F" handler=%s arg=%p\n",
                             (void *)timeout, abs_time, handler_name, (void *)arg));
#endif /* LWIP_DEBUG_TIMERNAMES */

  if (next_timeout == NULL) {
    next_timeout = timeout; /* 如果超時鏈表沒有超時事件,那當前超時事件就作為超時鏈表的首節點 */
    return;
  }
  /* 按喚醒時間有序插入 */
  if (TIME_LESS_THAN(timeout->time, next_timeout->time)) {
    /* 當前超時事件被喚醒的時間比超時鏈表中的所有超時事件都要早,那當前節點就做超時鏈表的頭兒 */
    timeout->next = next_timeout;
    next_timeout = timeout;
  } else { /* 按喚醒時間把超時事件有序插入到超時鏈表中 */
    for (t = next_timeout; t != NULL; t = t->next) {
      if ((t->next == NULL) || TIME_LESS_THAN(timeout->time, t->next->time)) {
        timeout->next = t->next;
        t->next = timeout;
        break;
      }
    }
  }
}

5.2.8 註銷超時事件

註銷超時事件的主要步驟:

  • 從超時鏈表中找出該事件,將其提除。
  • 釋放該事件的記憶體資源。
/**
 * Go through timeout list (for this task only) and remove the first matching
 * entry (subsequent entries remain untouched), even though the timeout has not
 * triggered yet.
 *
 * @param handler callback function that would be called by the timeout
 * @param arg callback argument that would be passed to handler
*/
void
sys_untimeout(sys_timeout_handler handler, void *arg)
{
  struct sys_timeo *prev_t, *t;
  /* 確保在tcpip執行緒安全鎖內 */
  LWIP_ASSERT_CORE_LOCKED();

  if (next_timeout == NULL) {
    return;
  }

  for (t = next_timeout, prev_t = NULL; t != NULL; prev_t = t, t = t->next) {
    if ((t->h == handler) && (t->arg == arg)) {
      /* 在鏈表中找到該事件 */
      /* 從超時鏈表中提除 */
      if (prev_t == NULL) {
        next_timeout = t->next;
      } else {
        prev_t->next = t->next;
      }
      /* 釋放記憶體資源 */
      memp_free(MEMP_SYS_TIMEOUT, t);
      return;
    }
  }
  return;
}

5.2.9 超時檢查處理

在裸機中,可以直接定時調用sys_check_timeouts()函數來實現超時檢查處理。

在系統中,超時檢查處理在tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg);函數被調用。

tcpip_timeouts_mbox_fetch()這個函數會在tcpip_thread()被一直調用。主要內容是等待tcpip_mbox消息,是可阻塞的,如果在等待tcpip_mbox的過程中發生超時事件,則會同時執行超時事件處理。

/**
 * @ingroup lwip_nosys
 * Handle timeouts for NO_SYS==1 (i.e. without using
 * tcpip_thread/sys_timeouts_mbox_fetch(). Uses sys_now() to call timeout
 * handler functions when timeouts expire.
 *
 * Must be called periodically from your main loop.
 */
void
sys_check_timeouts(void)
{
  u32_t now;
  /* 確保在tcpip執行緒安全鎖內 */
  LWIP_ASSERT_CORE_LOCKED();

  /* 獲取當前時間 */
  now = sys_now();

  do { /* 把所有已經超時的事件都處理一遍 */
    struct sys_timeo *tmptimeout;
    sys_timeout_handler handler;
    void *arg;
    /* 檢查釋放已收到的無序報文的資源,提高資源空閑空間 */
    PBUF_CHECK_FREE_OOSEQ();

    tmptimeout = next_timeout;
    if (tmptimeout == NULL) { /* 沒有事件就直接返回 */
      return;
    }

    if (TIME_LESS_THAN(now, tmptimeout->time)) { /* 沒有到期事件也不要處理 */
      return;
    }

    /* 處理已超時事件 */
    next_timeout = tmptimeout->next; /* 先從超時鏈表中移除 */
    handler = tmptimeout->h; /* 獲取回調函數 */
    arg = tmptimeout->arg; /* 獲取回調函數參數 */
    current_timeout_due_time = tmptimeout->time; /* 獲取當前時間 */
#if LWIP_DEBUG_TIMERNAMES
    if (handler != NULL) {
      LWIP_DEBUGF(TIMERS_DEBUG, ("sct calling h=%s t=%"U32_F" arg=%p\n",
                                 tmptimeout->handler_name, sys_now() - tmptimeout->time, arg));
    }
#endif /* LWIP_DEBUG_TIMERNAMES */
    /* 先釋放記憶體資源 */
    memp_free(MEMP_SYS_TIMEOUT, tmptimeout);
    if (handler != NULL) {
      handler(arg); /* 執行回調 */
    }
    LWIP_TCPIP_THREAD_ALIVE(); /* 該宏函數默認為空。其作用是用於類似看門狗的功能 */

  } while (1);
}

5.2.10 把超時鏈表的時間域更新到當前時間

就是超時鏈表首節點的喚醒時間從時間軸上移到當前時間,後續節點按差跟上。

/** Rebase the timeout times to the current time.
 * This is necessary if sys_check_timeouts() hasn't been called for a long
 * time (e.g. while saving energy) to prevent all timer functions of that
 * period being called.
 */
void
sys_restart_timeouts(void)
{
  u32_t now;
  u32_t base;
  struct sys_timeo *t;

  if (next_timeout == NULL) {
    return;
  }

  now = sys_now();
  base = next_timeout->time; /* 獲取超時鏈表首節點喚醒時間 */

  for (t = next_timeout; t != NULL; t = t->next) {
    /* 把超時鏈表上的所有事件的喚醒時間在時間軸上前移(base-now)個單位 */
    t->time = (t->time - base) + now;
  }
}

5.2.11 獲取到下次超時的時間差

/* 返回下一次超時到期前的剩餘時間。如果沒有加入超時隊列,則返回SYS_TIMEOUTS_SLEEPTIME_INFINITE */
u32_t
sys_timeouts_sleeptime(void)
{
  u32_t now;
  /* 確保在tcpip執行緒安全鎖內 */
  LWIP_ASSERT_CORE_LOCKED();

  if (next_timeout == NULL) {
    return SYS_TIMEOUTS_SLEEPTIME_INFINITE;
  }
  now = sys_now();
  if (TIME_LESS_THAN(next_timeout->time, now)) { /* 已經有事件到期了,返回0 */
    return 0;
  } else {
    u32_t ret = (u32_t)(next_timeout->time - now); /* 返回現在到下次喚醒的剩餘時間 */
    LWIP_ASSERT("invalid sleeptime", ret <= LWIP_MAX_TIMEOUT); /* 差值不能超過LWIP_MAX_TIMEOUT */
    return ret;
  }
}

5.2.12 周期定時機制實現

周期定時機制基於內核超時機制

利用lwip_cyclic_timer()做超時函數:

  • 函數的參數就是周期定時機制的數據結構,內含周期定時回調函數。
  • 函數的內容就是執行周期定時回調函數,計算下一個喚醒時間,重新把自己插回超時鏈表,以此實現周期定時回調。

周期定時機制的數據結構:

/* lwip周期定時機制數據結構 */
struct lwip_cyclic_timer {
  u32_t interval_ms; /* 周期值 */
  lwip_cyclic_timer_handler handler; /* 回調函數 */
#if LWIP_DEBUG_TIMERNAMES
  const char* handler_name; /* 周期定時事件文本描述 */
#endif /* LWIP_DEBUG_TIMERNAMES */
};

周期定時機制的基函數:

  • 注意:如果當前定時事件的下一個喚醒時間也過期了,那就重新計時,以當前時間為基準。
/**
 * 定時器回調函數,調用循環>處理程式()並重新調度自己
 *
 * @param arg unused argument
 */
#if !LWIP_TESTMODE
static
#endif
void
lwip_cyclic_timer(void *arg)
{
  u32_t now;
  u32_t next_timeout_time;
  /* 超時事件的回調參數就是周期定時事件 */
  const struct lwip_cyclic_timer *cyclic = (const struct lwip_cyclic_timer *)arg;

#if LWIP_DEBUG_TIMERNAMES
  LWIP_DEBUGF(TIMERS_DEBUG, ("tcpip: %s()\n", cyclic->handler_name));
#endif
  cyclic->handler(); /* 執行周期定時回調函數 */

  now = sys_now(); /* 獲取當前時間 */
  /* 計算下一個喚醒當前周期定時事件的時間,應該從上一個理應喚醒時間開始計 */
  next_timeout_time = (u32_t)(current_timeout_due_time + cyclic->interval_ms);
  if (TIME_LESS_THAN(next_timeout_time, now)) {
    /* 如果當前定時事件的下一個喚醒時間也過期了,那就重新計時,以當前時間為基準 */
#if LWIP_DEBUG_TIMERNAMES
    sys_timeout_abs((u32_t)(now + cyclic->interval_ms), lwip_cyclic_timer, arg, cyclic->handler_name);
#else
    /* 重新插入到超時鏈表,實現周期定時回調 */
    sys_timeout_abs((u32_t)(now + cyclic->interval_ms), lwip_cyclic_timer, arg);
#endif

  } else { /* 下一個喚醒時間還沒到期 */
#if LWIP_DEBUG_TIMERNAMES
    sys_timeout_abs(next_timeout_time, lwip_cyclic_timer, arg, cyclic->handler_name);
#else
    /* 重新插入到超時鏈表,實現周期定時回調 */
    sys_timeout_abs(next_timeout_time, lwip_cyclic_timer, arg);
#endif
  }
}

5.3 lwip中的消息

lwip消息就是其它執行緒把業務外包到lwip內核主執行緒tcpip_thread()去執行。

LwIP中必須存在的消息,整個內核的運作都要依賴他們:

  • 數據包消息。
  • API消息。

5.3.1 lwip消息數據結構

因為lwip中有多種消息類型,所以數據結構使用聯合體。

struct tcpip_msg {
  enum tcpip_msg_type type; /* msg類型 */
  union {
#if !LWIP_TCPIP_CORE_LOCKING
    struct {
      tcpip_callback_fn function; /* 需要內核執行的API */
      void* msg; /* API的參數指針,內含多個參數 */
    } api_msg; /* TCPIP_MSG_API。API消息 */
    struct {
      tcpip_api_call_fn function;  /* 需要內核執行的API */
      struct tcpip_api_call_data *arg;
      sys_sem_t *sem;
    } api_call; /* TCPIP_MSG_API_CALL。API回傳消息 */
#endif /* LWIP_TCPIP_CORE_LOCKING */
#if !LWIP_TCPIP_CORE_LOCKING_INPUT
    struct {
      struct pbuf *p; /* 收到的數據包 */
      struct netif *netif; /* 來自哪個網卡的 */
      netif_input_fn input_fn; /* 需要傳入哪個內核函數處理 */
    } inp; /* TCPIP_MSG_INPKT。網路數據包消息 */
#endif /* !LWIP_TCPIP_CORE_LOCKING_INPUT */
    struct {
      tcpip_callback_fn function;
      void *ctx;
    } cb; /* TCPIP_MSG_CALLBACK、TCPIP_MSG_CALLBACK_STATIC。回調消息 */
#if LWIP_TCPIP_TIMEOUT && LWIP_TIMERS
    struct {
      u32_t msecs;
      sys_timeout_handler h;
      void *arg;
    } tmo; /* TCPIP_MSG_TIMEOUT、TCPIP_MSG_UNTIMEOUT。註冊註銷超時消息 */
#endif /* LWIP_TCPIP_TIMEOUT && LWIP_TIMERS */
  } msg;
};

5.3.2 消息類型

enum tcpip_msg_type {
#if !LWIP_TCPIP_CORE_LOCKING
  TCPIP_MSG_API, /* API消息類型。如用戶調用應用層的介面時,需要tcpip_thread執行內核函數就用這個消息類型。需要往南供給協議棧。 */
  TCPIP_MSG_API_CALL, /* API回傳消息類型。比如用戶調用應用層有回傳的介面時,就用這個消息類型。需要往南供給協議棧。 */
#endif /* !LWIP_TCPIP_CORE_LOCKING */
#if !LWIP_TCPIP_CORE_LOCKING_INPUT
  TCPIP_MSG_INPKT, /* 網路數據包消息類型。即是網卡上收到的數據。需要往北供給協議棧。 */
#endif /* !LWIP_TCPIP_CORE_LOCKING_INPUT */
#if LWIP_TCPIP_TIMEOUT && LWIP_TIMERS
  TCPIP_MSG_TIMEOUT, /* 註冊超時定時器消息類型。 */
  TCPIP_MSG_UNTIMEOUT, /* 註銷超時定時器消息類型。 */
#endif /* LWIP_TCPIP_TIMEOUT && LWIP_TIMERS */
  TCPIP_MSG_CALLBACK, /* 回調消息類型。就是讓tcpip_thread這個執行緒幫忙執行這個回調函數,這個回調函數就能肆無忌憚地訪問內核函數了。 */
  TCPIP_MSG_CALLBACK_STATIC /* 靜態的回調消息類型。 */
};

5.3.3 API消息

API消息是有用戶執行緒發出,與內核進行交互。

用戶調用應用層介面,往南執行協議棧內核函數,需要tcpip_thread執行緒執行時,通過API消息告知tcpip_thread執行緒執行。

在執行緒間通訊中的api消息類型對應的數據結構偽程式碼如下:

struct {
      tcpip_callback_fn function; /* 需要內核執行的API */
      void* msg; /* API的參數指針,內含多個參數 */
    } api_msg; /* TCPIP_MSG_API。API消息 */

其中,void* msg;這個參數才是真正的API消息數據結構,對應是struct api_msg

要注意區分,前者是用於執行緒間通訊的API消息的數據結構。後者是API消息內容的數據結構。

5.3.3.1 API消息內容數據結構

struct api_msg包含3個欄位:

  1. 描述連接資訊的struct netconn *conn;
  2. 內核執行的結果err_t err;
  3. API介面數據結構union msg;

struct api_msg

/* IP addresses and port numbers are expected to be in the same byte order as in the corresponding pcb. */
/* 這個數據結構包含了在另一個執行緒上下文中執行netconn函數所需的所有內容(主要用於處理tcpip_thread上下文中的netconn以確保執行緒安全)。 */
struct api_msg {
  /* 當前需要執行的API對應的連接 */
  struct netconn *conn;
  /* 在tcpip_thread中執行的函數的返回值 */
  err_t err;
  /* 用戶調用不同的API,會使用不同的數據結構 */
  union {
    /* used for lwip_netconn_do_send */
    struct netbuf *b;
    /** used for lwip_netconn_do_newconn */
    struct {
      u8_t proto;
    } n;
    /** used for lwip_netconn_do_bind and lwip_netconn_do_connect */
    struct {
      API_MSG_M_DEF_C(ip_addr_t, ipaddr);
      u16_t port;
      u8_t if_idx;
    } bc;
    /** used for lwip_netconn_do_getaddr */
    struct {
      ip_addr_t API_MSG_M_DEF(ipaddr);
      u16_t API_MSG_M_DEF(port);
      u8_t local;
    } ad;
    /** used for lwip_netconn_do_write */
    struct {
      /** current vector to write */
      const struct netvector *vector;
      /** number of unwritten vectors */
      u16_t vector_cnt;
      /** offset into current vector */
      size_t vector_off;
      /** total length across vectors */
      size_t len;
      /** offset into total length/output of bytes written when err == ERR_OK */
      size_t offset;
      u8_t apiflags;
#if LWIP_SO_SNDTIMEO
      u32_t time_started;
#endif /* LWIP_SO_SNDTIMEO */
    } w;
    /* used for lwip_netconn_do_recv */
    struct {
      size_t len;
    } r;
#if LWIP_TCP
    /* used for lwip_netconn_do_close (/shutdown) */
    struct {
      u8_t shut;
#if LWIP_SO_SNDTIMEO || LWIP_SO_LINGER
      u32_t time_started;
#else /* LWIP_SO_SNDTIMEO || LWIP_SO_LINGER */
      u8_t polls_left;
#endif /* LWIP_SO_SNDTIMEO || LWIP_SO_LINGER */
    } sd;
#endif /* LWIP_TCP */
#if LWIP_IGMP || (LWIP_IPV6 && LWIP_IPV6_MLD)
    /* used for lwip_netconn_do_join_leave_group */
    struct {
      API_MSG_M_DEF_C(ip_addr_t, multiaddr);
      API_MSG_M_DEF_C(ip_addr_t, netif_addr);
      u8_t if_idx;
      enum netconn_igmp join_or_leave;
    } jl;
#endif /* LWIP_IGMP || (LWIP_IPV6 && LWIP_IPV6_MLD) */
#if TCP_LISTEN_BACKLOG
    struct {
      u8_t backlog;
    } lb;
#endif /* TCP_LISTEN_BACKLOG */
  } msg;
#if LWIP_NETCONN_SEM_PER_THREAD
  sys_sem_t* op_completed_sem;
#endif /* LWIP_NETCONN_SEM_PER_THREAD */
};

5.3.3.2 lwipAPI消息流圖

待畫:後續分析完socket API源碼實現後再補上

5.3.4 數據包消息

消息類型為TCPIP_MSG_INPKT

數據包消息是底層網卡接收到數據後需要往北交給協議棧處理時需要構造的消息。

tcpip_inpkt()函數中構造。

主要將收到的數據包傳遞到tcpip_thread執行緒執行。並告知要傳入哪個內核函數處理。

tcpip_inpkt()

  • 實現分兩種:

    1. 沒有tcpip內核鎖許可權,就需要把input的業務通過消息轉交給tcpip_thread處理。
    2. 如果有tcpip內核鎖許可權,那就獲取tcpip內核鎖,在本執行緒處理即可。
/**
 * 將接收到的數據包打包給tcpip_thread執行
 *
 * @param p the received packet
 * @param inp the network interface on which the packet was received
 * @param input_fn input function to call
 */
err_t
tcpip_inpkt(struct pbuf *p, struct netif *inp, netif_input_fn input_fn)
{
#if LWIP_TCPIP_CORE_LOCKING_INPUT /* 開放了tcpip內核鎖給input業務處理,就不需要外包到tcpip_thread處理 */
  err_t ret;
  LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_inpkt: PACKET %p/%p\n", (void *)p, (void *)inp));
  LOCK_TCPIP_CORE(); /* 獲取tcpip內核鎖 */
  ret = input_fn(p, inp); /* 直接在本執行緒處理收到的數據包 */
  UNLOCK_TCPIP_CORE(); /* 釋放tcpip內核鎖 */
  return ret;
#else /* 如果沒有開放tcpip內核鎖給input業務處理,就需要外包到tcpip_thread處理 */
  struct tcpip_msg *msg;

  LWIP_ASSERT("Invalid mbox", sys_mbox_valid_val(tcpip_mbox));
  /* 申請數據包消息資源 */
  msg = (struct tcpip_msg *)memp_malloc(MEMP_TCPIP_MSG_INPKT);
  if (msg == NULL) {
    return ERR_MEM;
  }
  /* 配置消息 */
  msg->type = TCPIP_MSG_INPKT; /* 數據包消息類型 */
  msg->msg.inp.p = p;
  msg->msg.inp.netif = inp;
  msg->msg.inp.input_fn = input_fn;
  if (sys_mbox_trypost(&tcpip_mbox, msg) != ERR_OK) { /* 投遞消息,非阻塞 */
    memp_free(MEMP_TCPIP_MSG_INPKT, msg); /* 投遞失敗,釋放資源 */
    return ERR_MEM;
  }
  return ERR_OK;
#endif /* LWIP_TCPIP_CORE_LOCKING_INPUT */
}

lwip數據包南向收包流圖:

5.4 tcpip_thread執行緒

LwIP內核是作為作業系統的一個執行緒運行的,在協議棧初始化的時候就會創建tcpip_thread執行緒。

該執行緒主要處理超時檢查和接收各種消息進行處理。

tcpip內核鎖是維護tcpip內核函數的原子性。

tcpip_thread內核執行緒和其它執行緒對內核tcpip的函數存在競爭關係。

5.4.1 lwip內核主執行緒流程圖

5.4.2 lwip內核主執行緒

lwIP主執行緒。

這個執行緒獨佔地訪問lwIP核心函數(除非對它們的訪問沒有被鎖定)。

其他執行緒使用消息框與該執行緒通訊。

該執行緒還啟動所有計時器,以確保它們在正確的執行緒上下文中運行。

/**
 * The main lwIP thread. This thread has exclusive access to lwIP core functions (unless access to them is not locked). Other threads communicate with this thread using message boxes.
 *
 * It also starts all the timers to make sure they are running in the right thread context.
 *
 * @param arg unused argument
 */
static void
tcpip_thread(void *arg)
{
  struct tcpip_msg *msg;
  LWIP_UNUSED_ARG(arg);
  /* 記錄當前執行緒 */
  LWIP_MARK_TCPIP_THREAD();
  /* tcpip內核上鎖 */
  LOCK_TCPIP_CORE();
  if (tcpip_init_done != NULL) {
    /* 執行用戶插入的lwip內核初始化鉤子函數 */
    tcpip_init_done(tcpip_init_done_arg);
  }

  while (1) {                          /* MAIN Loop */
    LWIP_TCPIP_THREAD_ALIVE(); /* 默認為空。這個可用於類似看門狗功能 */
    /* 等待消息或超時事件 */
    TCPIP_MBOX_FETCH(&tcpip_mbox, (void **)&msg);
    if (msg == NULL) {
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: invalid message: NULL\n"));
      LWIP_ASSERT("tcpip_thread: invalid message", 0);
      continue; /* 沒有消息,繼續等待 */
    }
    /* 處理收到的消息 */
    tcpip_thread_handle_msg(msg);
  }
}

5.4.3 等待消息或超時事件程式碼實現

#if !LWIP_TIMERS
/* 只等待消息 */
#define TCPIP_MBOX_FETCH(mbox, msg) sys_mbox_fetch(mbox, msg)
#else /* !LWIP_TIMERS */
/* 等待消息或超時事件 */
#define TCPIP_MBOX_FETCH(mbox, msg) tcpip_timeouts_mbox_fetch(mbox, msg)
/**
 * 永久等待一個消息,在等待過程中如果有超時事件發生,也會處理超時事件
 *
 * @param mbox the mbox to fetch the message from
 * @param msg the place to store the message
 */
static void
tcpip_timeouts_mbox_fetch(sys_mbox_t *mbox, void **msg)
{
  u32_t sleeptime, res;

again:
  /* 確保當前在tcpip內核鎖內 */
  LWIP_ASSERT_CORE_LOCKED();
  /* 獲取下一個需要喚醒的超時事件的剩餘時間 */
  sleeptime = sys_timeouts_sleeptime();
  if (sleeptime == SYS_TIMEOUTS_SLEEPTIME_INFINITE) {
    /* 沒有超時事件在等待 */
    /* 釋放tcpip內核鎖 */
    UNLOCK_TCPIP_CORE();
    /* 永久等待消息 */
    sys_arch_mbox_fetch(mbox, msg, 0);
    /* 拿到消息後,tcpip內核上鎖 */
    LOCK_TCPIP_CORE();
    return;
  } else if (sleeptime == 0) { /* 如果有事件超時了,就先處理超時事件 */
    sys_check_timeouts(); /* 處理所有超時了的事件 */
    /* 處理完畢後繼續等待消息或超時事件 */
    goto again;
  }

  /* 如果沒有消息,且存在超時事件在計時,就按需休眠 */
  /* 釋放tcpip內核鎖 */
  UNLOCK_TCPIP_CORE();
  /* 等待消息,直至有消息到來或者有超時事件需要喚醒處理,才解除阻塞 */
  res = sys_arch_mbox_fetch(mbox, msg, sleeptime);
  /* 有消息或者事件需要處理時,進入tcpip內核鎖 */
  LOCK_TCPIP_CORE();
  if (res == SYS_ARCH_TIMEOUT) {
    /* 如果等待消息超時了,說明超時鏈表中有超時事件需要喚醒了 */
    sys_check_timeouts(); /* 處理所有超時了的事件 */
    /* 處理完畢後繼續等待消息或超時事件 */
    goto again;
  }
}
#endif /* !LWIP_TIMERS */

5.4.4 tcpip_thread處理消息

其它執行緒需要發消息到tcpip_thread()執行緒來處理內核操作。

如果開啟了tcpip內核鎖,客戶端對lwip的操作也可以不用外包tcpip_thread()執行緒處理。

/* 處理單個tcpip_msg
 * This is in its own function for access by tests only.
 */
static void
tcpip_thread_handle_msg(struct tcpip_msg *msg)
{
  switch (msg->type) {
#if !LWIP_TCPIP_CORE_LOCKING
    /* 沒有開啟tcpip內核鎖,其它執行緒對lwip的操作需要外包到tcpip_thread執行緒操作 */
    case TCPIP_MSG_API:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: API message %p\n", (void *)msg));
      msg->msg.api_msg.function(msg->msg.api_msg.msg); /* 執行API */
      break;
    case TCPIP_MSG_API_CALL:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: API CALL message %p\n", (void *)msg));
      msg->msg.api_call.arg->err = msg->msg.api_call.function(msg->msg.api_call.arg); /* 執行API,並返回結果碼 */
      sys_sem_signal(msg->msg.api_call.sem); /* 釋放訊號量,告知用戶執行緒執行API完畢 */
      break;
    case TCPIP_MSG_CALLBACK_STATIC_WAIT:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: CALLBACK WAIT message %p\n", (void *)msg));
      msg->msg.cb_wait.function(msg->msg.cb_wait.ctx); /* 執行回調 */
      sys_sem_signal(msg->msg.cb_wait.sem); /* 釋放訊號量,告知插入回調的執行緒回調執行完畢 */
      break;
#endif /* !LWIP_TCPIP_CORE_LOCKING */

#if !LWIP_TCPIP_CORE_LOCKING_INPUT
    /* 沒有開啟LWIP_TCPIP_CORE_LOCKING_INPUT,所以tcpip_input()沒有許可權好的tcpip內核鎖,只能外包回tcpip_tread執行緒處理 */
    case TCPIP_MSG_INPKT:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: PACKET %p\n", (void *)msg));
      if (msg->msg.inp.input_fn(msg->msg.inp.p, msg->msg.inp.netif) != ERR_OK) { /* 處理收到的數據包 */
        pbuf_free(msg->msg.inp.p); /* 處理完畢後釋放這個pbuf的資源 */
      }
      memp_free(MEMP_TCPIP_MSG_INPKT, msg); /* 釋放消息資源 */
      break;
#endif /* !LWIP_TCPIP_CORE_LOCKING_INPUT */

#if LWIP_TCPIP_TIMEOUT && LWIP_TIMERS
    /* 支援在tcpip_thread執行計時器功能 */
    case TCPIP_MSG_TIMEOUT:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: TIMEOUT %p\n", (void *)msg));
      sys_timeout(msg->msg.tmo.msecs, msg->msg.tmo.h, msg->msg.tmo.arg); /* 註冊超時事件 */
      memp_free(MEMP_TCPIP_MSG_API, msg); /* 釋放消息資源 */
      break;
    case TCPIP_MSG_UNTIMEOUT:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: UNTIMEOUT %p\n", (void *)msg));
      sys_untimeout(msg->msg.tmo.h, msg->msg.tmo.arg); /* 註銷超時事件 */
      memp_free(MEMP_TCPIP_MSG_API, msg); /* 釋放消息資源 */
      break;
#endif /* LWIP_TCPIP_TIMEOUT && LWIP_TIMERS */

    case TCPIP_MSG_CALLBACK:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: CALLBACK %p\n", (void *)msg));
      msg->msg.cb.function(msg->msg.cb.ctx); /* 執行回調 */
      memp_free(MEMP_TCPIP_MSG_API, msg); /* 釋放消息資源 */
      break;

    case TCPIP_MSG_CALLBACK_STATIC:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: CALLBACK_STATIC %p\n", (void *)msg));
      msg->msg.cb.function(msg->msg.cb.ctx); /* 執行回調。靜態,不需要釋放消息資源 */
      break;

    default:
      LWIP_DEBUGF(TCPIP_DEBUG, ("tcpip_thread: invalid message: %d\n", msg->type));
      LWIP_ASSERT("tcpip_thread: invalid message", 0);
      break;
  }
}

5.5 lwip數據流圖(全棧)

後續分析完socket API源碼實現後再補上(還有最後一層 socket API源碼實現還沒分析,所以全局資料庫流圖還有一層未畫)