文章目录
一、Sequetia API实现原理
前面介绍了RAW/Callback API编程,同时也谈到了RAW API编程的缺点:用户应用程序和协议栈内核处于同一进程中,两者间会出现相互制约的关系,影响协议栈接收、处理新数据包的效率,甚至出现丢包错误。本文将介绍协议栈在有操作系统模拟层下的编程方式,这时协议栈内核和用户程序就可以处在两个相互独立的进程中运行,两者间也就不存在相互制约的问题了。
在Linux中有BSD Socket API进行网络编程,它遵循了一套简单的编程步骤和规则,可以提高应用程序的开发效率。但由于BSD Socket函数实现上具有很高的抽象特性,所以并不适合在小型嵌入式TCP/IP协议栈中使用,更重要的是BSD Socket编程时,在应用程序进程和协议栈内核进程之间会出现数据拷贝的情况,拷贝消耗的时间和空间资源对嵌入式系统来说是非常宝贵的资源。
LwIP协议栈针对上述特点提供了Sequetia API,它的出发点是上层已经预知了协议栈内核的部分结构,API可以使用这种预知来避免数据拷贝的出现,Sequetia API与BSD Socket具有很大的相似性,但工作在更低的层次(Sequetia API操作的是一个网络连接,BSD Socket像操作普通文件那样来操作一个网络连接,充分贯彻linux一切皆文件的理念),用户进程可以直接操作内核进程中的数据包数据。
Sequetia API的实现由两部分组成:一部分作为用户编程接口函数提供给用户,这些函数在用户进程中执行;另一部分驻留在协议栈内核进程中,这两部分通过进程通信机制(IPC:Inter-Process Communication)实现通信和同步,共同为应用程序提供服务。被用到的进程通信机制包括以下三种:
- 消息邮箱,用于两部分API的通信;
- 信号量,用于两部分API的同步;
- 共享内存,用于保存用户进程与内核进程的消息结构。
Sequetia API设计的核心在于让用户进程负责尽可能多的工作,例如数据的计算、拷贝等;而协议栈进程只负责简单的通信工作,这点很关键,因为系统可能存在多个应用程序,它们都使用协议栈进程提供的通信服务,保证内核进程的高效性和实时性是提高系统性能的重要保障。Sequetia API的实现中,两部分API间的关系如下图示:
Sequetia API实现时,也为用户提供了数据包管理函数,可以完成数据包内存申请、释放、数据拷贝等任务,应用程序使用netbuf结构来描述、组装数据包,该结构只是对内核pbuf的简单封装,通过共享一个netbuf结构,两部分API就能实现对数据包的共同处理,避免了数据的拷贝。同时,由于RAW API针对RAW、UDP、TCP各有一套API,使用不够方便,协议栈内核对RAW API进行了更高抽象的封装,将RAW、UDP、TCP统一为一套接口(像上图中do_xxx格式的函数,本文称该接口函数为内核进程API),根据类型字段自动选择合适的调用对象,实现原理下文再介绍。
上图中用户进程API与内核进程API之间进行交互信息时,通常是由用户进程API函数调用时发出消息,用来告诉内核进程执行哪一个内核API函数(通过函数指针和参数指针的传递),各种类型内核消息的具体消息内容都可以直接包含在内核消息结构tcpip_msg中,两部分API互相配合共同完成一个完整的API功能。举个例子,如果用户程序中调用函数netconn_bind绑定了一个连接,该函数实现时是通过向内核进程发送一个TCPIP_MSG_API类型的消息,告诉内核进程执行do_bind函数;在消息发出后,函数阻塞在信号量上,等待内核处理该消息;内核在处理消息时,会根据消息内容调用do_bind,而do_bind会根据连接的类型调用内核函数udp_bind、tcp_bind或raw_bind;当do_bind执行完成后,它会释放信号量,这使被阻塞的netconn_bind得以继续执行,整个过程如下图所示:
二、协议栈消息机制
2.1 内核进程消息结构
内核进程tcpip_thread在前篇介绍协议栈内核定时事件时简单介绍过,该进程阻塞在邮箱上接收消息,系统消息是通过结构tcpip_msg来描述的,内核进程接收到消息后根据识别出的消息类型,调用相应的内核进程API函数处理这些消息。消息tcpip_msg的数据结构及内核进程tcpip_thread的实现代码如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcpip.h
enum tcpip_msg_type {
TCPIP_MSG_API,
TCPIP_MSG_INPKT,
TCPIP_MSG_NETIFAPI,
TCPIP_MSG_TIMEOUT,
TCPIP_MSG_UNTIMEOUT,
TCPIP_MSG_CALLBACK,
TCPIP_MSG_CALLBACK_STATIC
};
struct tcpip_msg {
enum tcpip_msg_type type;
sys_sem_t *sem;
union {
struct api_msg *apimsg;
struct netifapi_msg *netifapimsg;
struct {
struct pbuf *p;
struct netif *netif;
} inp;
struct {
tcpip_callback_fn function;
void *ctx;
} cb;
struct {
u32_t msecs;
sys_timeout_handler h;
void *arg;
} tmo;
} msg;
};
// rt-thread\components\net\lwip-1.4.1\src\api\tcpip.c
/* global variables */
static tcpip_init_done_fn tcpip_init_done;
static void *tcpip_init_done_arg;
static sys_mbox_t mbox;
/** The global semaphore to lock the stack. */
sys_mutex_t lock_tcpip_core;
/**
* 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;
if (tcpip_init_done != NULL) {
tcpip_init_done(tcpip_init_done_arg);
}
LOCK_TCPIP_CORE();
while (1) {
/* MAIN Loop */
UNLOCK_TCPIP_CORE();
LWIP_TCPIP_THREAD_ALIVE();
/* wait for a message, timeouts are processed while waiting */
sys_timeouts_mbox_fetch(&mbox, (void **)&msg);
LOCK_TCPIP_CORE();
switch (msg->type) {
case TCPIP_MSG_API:
msg->msg.apimsg->function(&(msg->msg.apimsg->msg));
break;
case TCPIP_MSG_INPKT:
if (msg->msg.inp.netif->flags & (NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET)) {
ethernet_input(msg->msg.inp.p, msg->msg.inp.netif);
} else {
ip_input(msg->msg.inp.p, msg->msg.inp.netif);
}
memp_free(MEMP_TCPIP_MSG_INPKT, msg);
break;
case TCPIP_MSG_NETIFAPI:
msg->msg.netifapimsg->function(&(msg->msg.netifapimsg->msg));
break;
case TCPIP_MSG_TIMEOUT:
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:
sys_untimeout(msg->msg.tmo.h, msg->msg.tmo.arg);
memp_free(MEMP_TCPIP_MSG_API, msg);
break;
case TCPIP_MSG_CALLBACK:
msg->msg.cb.function(msg->msg.cb.ctx);
memp_free(MEMP_TCPIP_MSG_API, msg);
break;
case TCPIP_MSG_CALLBACK_STATIC:
msg->msg.cb.function(msg->msg.cb.ctx);
break;
default:
LWIP_ASSERT("tcpip_thread: invalid message", 0);
break;
}
}
}
枚举类型tcpip_msg_type定义了系统中可能出现的消息类型,消息结构的msg字段是一个共用体union,共用体中定义了各类型消息的具体内容,每种类型的消息对应了共用体中的一个字段,其中注册定时事件和注销定时事件消息共用一个tmo结构、回调事件与静态回调事件消息也共用一个cb结构;API调用与NETIFAPI调用相关的消息具体内容比较多,不宜直接放在tcpip_msg中,系统用了专门的结构体api_msg和netifapimsg来描述对应消息的具体内容,tcpip_msg中只保存了一个指向api_msg和netifapimsg的指针。
2.2 用户进程消息结构
内核进程tcpip_thread负责接收被投递到邮箱中的tcpip_msg消息,那么这个消息是被哪个函数投递的呢?我们以上图为例,查看netconn_bind的代码可知,向邮箱中投递TCPIP_MSG_API类型消息的函数是tcpip_apimsg,函数代码如下:
// rt-thread\components\net\lwip-1.4.1\src\api\api_lib.c
/**
* Bind a netconn to a specific local IP address and port.
* Binding one netconn twice might not always be checked correctly!
* @param conn the netconn to bind
* @param addr the local IP address to bind the netconn to (use IP_ADDR_ANY
* to bind to all addresses)
* @param port the local port to bind the netconn to (not used for RAW)
* @return ERR_OK if bound, any other err_t on failure
*/
err_t netconn_bind(struct netconn *conn, ip_addr_t *addr, u16_t port)
{
struct api_msg msg;
err_t err;
msg.function = do_bind;
msg.msg.conn = conn;
msg.msg.msg.bc.ipaddr = addr;
msg.msg.msg.bc.port = port;
err = TCPIP_APIMSG(&msg);
return err;tcpip
}
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\tcpip.h
#define TCPIP_APIMSG(m) tcpip_apimsg(m)
// rt-thread\components\net\lwip-1.4.1\src\api\tcpip.c
/**
* Call the lower part of a netconn_* function
* This function is then running in the thread context
* of tcpip_thread and has exclusive access to lwIP core code.
* @param apimsg a struct containing the function to call and its parameters
* @return ERR_OK if the function was called, another err_t if not
*/
err_t tcpip_apimsg(struct api_msg *apimsg)
{
struct tcpip_msg msg;
if (sys_mbox_valid(&mbox)) {
msg.type = TCPIP_MSG_API;
msg.msg.apimsg = apimsg;
sys_mbox_post(&mbox, &msg);
sys_arch_sem_wait(&apimsg->msg.conn->op_completed, 0);
return apimsg->msg.err;
}
return ERR_VAL;
}
在tcpip_apimsg函数中完成将api_msg结构的消息封装为tcpip_msg结构的消息,并向邮箱中投递该消息,并阻塞等待信号量,直到内核进程tcpip_thread处理完该消息并释放信号量后,tcpip_apimsg函数获得信号量继续执行。
用户进程调用的API(比如netconn_bind)实际操作的是api_msg结构的消息,在内核进程tcpip_thread中处理TCPIP_MSG_API类型的消息时,也是调用api_msg结构消息中的函数指针,api_msg数据结构的描述如下:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\api_msg.h
/** This struct includes everything that is necessary to execute a function
for a netconn in another thread context (mainly used to process netconns
in the tcpip_thread context to be thread safe).
*/
struct api_msg_msg {
/** The netconn which to process - always needed: it includes the semaphore
which is used to block the application thread until the function finished. */
struct netconn *conn;
/** The return value of the function executed in tcpip_thread. */
err_t err;
/** Depending on the executed function, one of these union members is used */
union {
/** used for do_send */
struct netbuf *b;
/** used for do_newconn */
struct {
u8_t proto;
} n;
/** used for do_bind and do_connect */
struct {
ip_addr_t *ipaddr;
u16_t port;
} bc;
/** used for do_getaddr */
struct {
ip_addr_t *ipaddr;
u16_t *port;
u8_t local;
} ad;
/** used for do_write */
struct {
const void *dataptr;
size_t len;
u8_t apiflags;
} w;
/** used for do_recv */
struct {
u32_t len;
} r;
/** used for do_close (/shutdown) */
struct {
u8_t shut;
} sd;
#if LWIP_IGMP
/** used for do_join_leave_group */
struct {
ip_addr_t *multiaddr;
ip_addr_t *netif_addr;
enum netconn_igmp join_or_leave;
} jl;
#endif /* LWIP_IGMP */
} msg;
};
/** This struct contains a function to execute in another thread context and
a struct api_msg_msg that serves as an argument for this function.
This is passed to tcpip_apimsg to execute functions in tcpip_thread context.
*/
struct api_msg {
/** function to execute in tcpip_thread context */
void (* function)(struct api_msg_msg *msg);
/** arguments for this function */
struct api_msg_msg msg;
};
api_msg结构主要由两部分组成:一是希望内核进程执行的API函数do_xxx;另一个是执行相应函数时需要的参数api_msg_msg。api_msg_msg结构包含了三个字段:一是描述连接信息的conn,它包含了与该连接相关的信号量、邮箱等信息,do_xxx执行时要用这些信息来完成与应用进程间的通信和同步;二是内核回调执行结果err,记录内核do_xxx函数的执行结果;三是共用体msg,各个成员记录了各个函数执行时需要的详细参数。
2.3 内核进程API
内核进程tcpip_thread从邮箱中获取到tcpip_msg消息后,看到netconn_bind投递过来的消息类型是TCPIP_MSG_API,根据类型执行回调函数msg->msg.apimsg->function(&(msg->msg.apimsg->msg)),也即api_msg结构定义的函数指针和参数指针,这里的函数指针是do_bind,参数指针除了连接结构conn和执行结果err外还包括本地IP和本地port,实际内核进程执行的是do_bind(msg)。那么,do_bind最终又是如何调用RAW API的呢?或者说do_xxx形式的内核API是如何统一IP RAW、UDP RAW、TCP RAW API的呢?下面下看看do_bind的函数实现代码:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\api.h
/* Helpers to process several netconn_types by the same code */
#define NETCONNTYPE_GROUP(t) (t&0xF0)
/** Protocol family and type of the netconn */
enum netconn_type {
NETCONN_INVALID = 0,
/* NETCONN_TCP Group */
NETCONN_TCP = 0x10,
/* NETCONN_UDP Group */
NETCONN_UDP = 0x20,
NETCONN_UDPLITE = 0x21,
NETCONN_UDPNOCHKSUM= 0x22,
/* NETCONN_RAW Group */
NETCONN_RAW = 0x40
};
// rt-thread\components\net\lwip-1.4.1\src\api\api_msg.c
/**
* Bind a pcb contained in a netconn
* Called from netconn_bind.
* @param msg the api_msg_msg pointing to the connection and containing
* the IP address and port to bind to
*/
void do_bind(struct api_msg_msg *msg)
{
if (ERR_IS_FATAL(msg->conn->last_err)) {
msg->err = msg->conn->last_err;
} else {
msg->err = ERR_VAL;
if (msg->conn->pcb.tcp != NULL) {
switch (NETCONNTYPE_GROUP(msg->conn->type)) {
case NETCONN_RAW:
msg->err = raw_bind(msg->conn->pcb.raw, msg->msg.bc.ipaddr);
break;
case NETCONN_UDP:
msg->err = udp_bind(msg->conn->pcb.udp, msg->msg.bc.ipaddr, msg->msg.bc.port);
break;
case NETCONN_TCP:
msg->err = tcp_bind(msg->conn->pcb.tcp, msg->msg.bc.ipaddr, msg->msg.bc.port);
break;
default:
break;
}
}
}
TCPIP_APIMSG_ACK(msg);
}
在参数指针msg中有一个表示连接的字段conn,其中包含了类型字段conn_type,do_bind函数根据连接类型调用相应的RAW API,实现将多种RAW API抽象为统一的内核进程API(do_xxx的形式)的目的。下面列出内核进程API的其余接口函数:
// rt-thread\components\net\lwip-1.4.1\src\include\lwip\api_msg.h
void do_newconn ( struct api_msg_msg *msg); // * Create a new pcb of a specific type inside a netconn, Called from netconn_new_with_proto_and_callback.
void do_delconn ( struct api_msg_msg *msg); // * Delete the pcb inside a netconn, Called from netconn_delete.
void do_bind ( struct api_msg_msg *msg); // * Bind a pcb contained in a netconn, Called from netconn_bind.
void do_connect ( struct api_msg_msg *msg); // * Connect a pcb contained inside a netconn, Called from netconn_connect.
void do_disconnect ( struct api_msg_msg *msg); // * Disconnect a pcb contained inside a netconn, Only used for UDP netconns, Called from netconn_disconnect.
void do_listen ( struct api_msg_msg *msg); // * Set a TCP pcb contained in a netconn into listen mode, Called from netconn_listen.
void do_send ( struct api_msg_msg *msg); // * Send some data on a RAW or UDP pcb contained in a netconn, Called from netconn_send.
void do_recv ( struct api_msg_msg *msg); // * Indicate data has been received from a TCP pcb contained in a netconn, Called from netconn_recv.
void do_write ( struct api_msg_msg *msg); // * Send some data on a TCP pcb contained in a netconn, Called from netconn_write.
void do_getaddr ( struct api_msg_msg *msg); // * Return a connection's local or remote address, Called from netconn_getaddr.
void do_close ( struct api_msg_msg *msg); // * Close a TCP pcb contained in a netconn, Called from netconn_close.
void do_shutdown ( struct api_msg_msg *msg);
#if LWIP_IGMP
void do_join_leave_group( struct api_msg_msg *msg); // * Join multicast groups for UDP netconns, Called from netconn_join_leave_group.
#endif /* LWIP_IGMP */
#if LWIP_DNS
void do_gethostbyname(void *arg); // * Execute a DNS query, Called from netconn_gethostbyname.
#endif /* LWIP_DNS */
2.4 内核回调接口
使用Raw/Callback API编程时,用户编程的方法就是向内核注册各种自定义的回调函数,回调是与内核实现交互的唯一方式。协议栈Sequetia API是基于Raw/Callback API来实现的,它与内核交互的方式也只能通过回调,但协议栈Sequetia API的一个重要目的就是简化网络编程(Raw/Callback API编写并注册回调函数的方式没有明确规定用户编程的步骤和规则,使用这种API需要对协议栈内核有透彻的理解),因此协议栈实现了几个默认的回调函数,当为新连接创建内核控制块时,这些回调函数会被默认的注册到控制块中相关字段,这就为内核和协议栈Sequetia API的交互提供了保证。
这些默认的回调函数如下图所示,其中用于RAW的有1个函数(图中省略了),用于UDP的有1个函数,用于TCP的有6个函数:
这些默认的回调函数是在为新连接创建内核控制块时,被默认注册到控制块中相关字段的,默认回调函数注册过程的相关代码如下:
// rt-thread\components\net\lwip-1.4.1\src\api\api_msg.c
/**
* Create a new pcb of a specific type inside a netconn.
* Called from netconn_new_with_proto_and_callback.
* @param msg the api_msg_msg describing the connection type
*/
void do_newconn(struct api_msg_msg *msg)
{
msg->err = ERR_OK;
if(msg->conn->pcb.tcp == NULL) {
pcb_new(msg);
}
TCPIP_APIMSG_ACK(msg);
}
/**
* Create a new pcb of a specific type.
* Called from do_newconn().
* @param msg the api_msg_msg describing the connection type
* @return msg->conn->err, but the return value is currently ignored
*/
static void pcb_new(struct api_msg_msg *msg)
{
LWIP_ASSERT("pcb_new: pcb already allocated", msg->conn->pcb.tcp == NULL);
/* Allocate a PCB for this connection */
switch(NETCONNTYPE_GROUP(msg->conn->type)) {
case NETCONN_RAW:
msg->conn->pcb.raw = raw_new(msg->msg.n.proto);
if(msg->conn->pcb.raw == NULL) {
msg->err = ERR_MEM;
break;
}
raw_recv(msg->conn->pcb.raw, recv_raw, msg->conn);
break;
case NETCONN_UDP:
msg->conn->pcb.udp = udp_new();
if(msg->conn->pcb.udp == NULL) {
msg->err = ERR_MEM;
break;
}