LwIP提供了三种编程接口,分别为RAW/Callback API、NETCONN API、SOCKET API。它们的易用性从左到右依次提高,而执行效率从左到右依次降低。下面对这三种 API 进行介绍:
RAW/Callback API RAW/Callback API是LwIP的一大特色,在没有操作系统支持的裸机环境中,只能使用这种API进行开发,同时这种API也可以用在操作系统环境中。
NETCONN API 在操作系统环境中,可以使用NETCONN API或者Socket API进行网络应用程序的开发。NETCONN API是基于操作系统的IPC机制(即信号量和邮箱机制)实现的,它的设计将LwIP内核代码和网络应用程序分离成了独立的线程。如此一来,LwIP内核线程就只负责数据包的TCP/IP封装和拆封,而不用进行数据的应用层处理,大大提高了系统对网络数据包的处理效率。
SOCKET API Socket,即套接字,它对网络连接进行了高级的抽象,使得用户可以像操作文件一样操作网络连接。它十分易用,许多网络开发人员最早接触的就是Socket编程,Socket已经成为了网络编程的标准。在不同的系统中,运行着不同的TCP/IP协议,但是只要它实现了Socket的接口,那么用Socket编写的网络应用程序就能在其中运行。可见用Socket编写的网络应用程序具有很好的可移植性。
信号量与互斥量的实现为内核提供同步与互斥的机制,比如当用户想要发送一个数据的时候,就会调用上层 API 接口,API 接口就会去先发送一个数据给内核去处理,然后尝试获取一个信号量,因为此时是没有信号量的,所以就会阻塞用户线程;内核在知道用户想要发送数据后,就会调用对应的网卡去发送数据,当数据发送完成后就释放一个信号量告知用户线程发送完成,这样子用户线程就得以继续执行。
/* Declare the first network device as default interface */ if (is_first_nf) { uk_pr_info("%c%c%u: Set as default interface\n", nf->name[0], nf->name[1], nf->num); netif_set_default(nf); is_first_nf = 0; } netif_set_up(nf);
/* call user specified initialization function for netif */ if (init(netif) != ERR_OK) { returnNULL; } #if LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES /* Initialize the MTU for IPv6 to the one set by the netif driver. This can be updated later by RA. */ netif->mtu6 = netif->mtu; #endif/* LWIP_IPV6 && LWIP_ND6_ALLOW_RA_UPDATES */
#if !LWIP_SINGLE_NETIF /* Assign a unique netif number in the range [0..254], so that (num+1) can serve as an interface index that fits in a u8_t. We assume that the new netif has not yet been added to the list here. This algorithm is O(n^2), but that should be OK for lwIP. */ { structnetif *netif2; int num_netifs; do { if (netif->num == 255) { netif->num = 0; } num_netifs = 0; for (netif2 = netif_list; netif2 != NULL; netif2 = netif2->next) { LWIP_ASSERT("netif already added", netif2 != netif); num_netifs++; LWIP_ASSERT("too many netifs, max. supported number is 255", num_netifs <= 255); if (netif2->num == netif->num) { netif->num++; break; } } } while (netif2 != NULL); } if (netif->num == 254) { netif_num = 0; } else { netif_num = (u8_t)(netif->num + 1); }
/* add this netif to the list */ // 链表插入部分 netif->next = netif_list; netif_list = netif; #endif/* "LWIP_SINGLE_NETIF */ mib2_netif_added(netif);
LWIP_ASSERT("uknetdev needs an input callback (netif_input or tcpip_input)", nf->input != NULL);
/* Netdev has to be in unconfigured state */ if (uk_netdev_state_get(dev) != UK_NETDEV_UNCONFIGURED) { LWIP_DEBUGF(NETIF_DEBUG, ("%s: Netdev %u not in uncofigured state\n", __func__, uk_netdev_id_get(dev))); return ERR_ISCONN; }
/* Interface name, the interface number (nf->num) is assigned by lwip */ nf->name[0] = UKNETDEV_NETIF_NAME0; nf->name[1] = UKNETDEV_NETIF_NAME1;
/* * Bring up uknetdev * Note: We use the default allocator for setting up the rx/tx queues */ /* TODO: In case the device initialization should happen manually before * attaching to lwip, we require another init function that skips * this initialization steps. */ a = uk_alloc_get_default(); if (!a) return ERR_MEM;
/* Get device information */ uk_netdev_info_get(dev, &lwip_data->dev_info); if (!lwip_data->dev_info.max_rx_queues || !lwip_data->dev_info.max_tx_queues) return ERR_IF; #if CONFIG_LWIP_UKNETDEV_POLLONLY /* Unset receive interrupt support: We force polling mode */ lwip_data->dev_info.features &= ~UK_FEATURE_RXQ_INTR_AVAILABLE; #endif/* CONFIG_LWIP_UKNETDEV_POLLONLY */ lwip_data->pkt_a = a;
/* * Device configuration, * we want to use just one queue for each direction */ dev_conf.nb_rx_queues = 1; dev_conf.nb_tx_queues = 1; ret = uk_netdev_configure(dev, &dev_conf); if (ret < 0) { LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Failed to configure netdev %u\n", __func__, nf->name[0], nf->name[1], nf->num, uk_netdev_id_get(dev))); return ERR_IF; }
/* * Receive queue, * use driver default descriptors */ rxq_conf.a = a; rxq_conf.alloc_rxpkts = netif_alloc_rxpkts; rxq_conf.alloc_rxpkts_argp = lwip_data; #ifdef CONFIG_LWIP_NOTHREADS /* * In mainloop mode, we will not use interrupts. */ rxq_conf.callback = NULL; rxq_conf.callback_cookie = NULL; #else/* CONFIG_LWIP_NOTHREADS */ rxq_conf.callback = uknetdev_input; rxq_conf.callback_cookie = nf; #ifdef CONFIG_LIBUKNETDEV_DISPATCHERTHREADS rxq_conf.s = uk_sched_get_default(); if (!rxq_conf.s) return ERR_IF;
#endif/* CONFIG_LIBUKNETDEV_DISPATCHERTHREADS */ #endif/* CONFIG_LWIP_NOTHREADS */ ret = uk_netdev_rxq_configure(dev, 0, 0, &rxq_conf); if (ret < 0) { LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Failed to configure rx queue of netdev %u\n", __func__, nf->name[0], nf->name[1], nf->num, uk_netdev_id_get(dev))); return ERR_IF; }
/* * Transmit queue, * use driver default descriptors */ txq_conf.a = a; ret = uk_netdev_txq_configure(dev, 0, 0, &txq_conf); if (ret < 0) { LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Failed to configure tx queue of netdev %u\n", __func__, nf->name[0], nf->name[1], nf->num, uk_netdev_id_get(dev))); return ERR_IF; }
/* Start interface */ ret = uk_netdev_start(dev); if (ret < 0) { LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Failed to start netdev %u\n", __func__, nf->name[0], nf->name[1], nf->num, uk_netdev_id_get(dev))); return ERR_IF; }
/* Maximum transfer unit */ nf->mtu = uk_netdev_mtu_get(dev); UK_ASSERT(nf->mtu); LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: MTU: %u\n", __func__, nf->name[0], nf->name[1], nf->num, nf->mtu));
#ifndef CONFIG_LWIP_NOTHREADS /* * We will use the status update callback to enable and disabled * receive queue interrupts */ netif_set_status_callback(nf, uknetdev_updown); #endif/* !CONFIG_LWIP_NOTHREADS */
/* * Initialize the snmp variables and counters inside the struct netif. * The last argument is the link speed, in units of bits per second. */ NETIF_INIT_SNMP(nf, snmp_ifType_ethernet_csmacd, UKNETDEV_BPS); LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Link speed: %"PRIu32" bps\n", __func__, nf->name[0], nf->name[1], nf->num, UKNETDEV_BPS));
#define NETIF_FLAG_UP 0x01U /** If set, the netif has broadcast capability. * Set by the netif driver in its init function. */ #define NETIF_FLAG_BROADCAST 0x02U /** If set, the interface has an active link * (set by the network interface driver). * Either set by the netif driver in its init function (if the link * is up at that time) or at a later point once the link comes up * (if link detection is supported by the hardware). */ #define NETIF_FLAG_LINK_UP 0x04U /** If set, the netif is an ethernet device using ARP. * Set by the netif driver in its init function. * Used to check input packet types and use of DHCP. */ #define NETIF_FLAG_ETHARP 0x08U /** If set, the netif is an ethernet device. It might not use * ARP or TCP/IP if it is used for PPPoE only. */ #define NETIF_FLAG_ETHERNET 0x10U /** If set, the netif has IGMP capability. * Set by the netif driver in its init function. */ #define NETIF_FLAG_IGMP 0x20U /** If set, the netif has MLD6 capability. * Set by the netif driver in its init function. */ #define NETIF_FLAG_MLD6 0x40U
typedefenum { /** pbuf data is stored in RAM, used for TX mostly, struct pbuf and its payload are allocated in one piece of contiguous memory (so the first payload byte can be calculated from struct pbuf). pbuf_alloc() allocates PBUF_RAM pbufs as unchained pbufs (although that might change in future versions). This should be used for all OUTGOING packets (TX).*/ PBUF_RAM = (PBUF_ALLOC_FLAG_DATA_CONTIGUOUS | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_HEAP), /** pbuf data is stored in ROM, i.e. struct pbuf and its payload are located in totally different memory areas. Since it points to ROM, payload does not have to be copied when queued for transmission. */ PBUF_ROM = PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF, /** pbuf comes from the pbuf pool. Much like PBUF_ROM but payload might change so it has to be duplicated when queued before transmitting, depending on who has a 'ref' to it. */ PBUF_REF = (PBUF_TYPE_FLAG_DATA_VOLATILE | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF), /** pbuf payload refers to RAM. This one comes from a pool and should be used for RX. Payload can be chained (scatter-gather RX) but like PBUF_RAM, struct pbuf and its payload are allocated in one piece of contiguous memory (so the first payload byte can be calculated from struct pbuf). Don't use this for TX, if the pool becomes empty e.g. because of TCP queuing, you are unable to receive TCP acks! */ PBUF_POOL = (PBUF_ALLOC_FLAG_RX | PBUF_TYPE_FLAG_STRUCT_DATA_CONTIGUOUS | PBUF_TYPE_ALLOC_SRC_MASK_STD_MEMP_PBUF_POOL) } pbuf_type;
LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Poll receive queue...\n", __func__, nf->name[0], nf->name[1], nf->num)); do { // 调用函数接收数据包并判断设备的状态 ret = uk_netdev_rx_one(dev, 0, &nb); if (unlikely(ret < 0)) { uk_pr_crit("%c%c%u: Receive error %d. Stopping interface...\n", nf->name[0], nf->name[1], nf->num, ret); // 收包出现错误时关闭接口并跳出循环 netif_set_down(nf); break; } // 如果没有数据包则停止 if (uk_netdev_status_notready(ret)) { /* No (more) packets received */ break; } // 将接收到的 netbuf 转化为 pbuf 供 lwip 使用 p = lwip_netbuf_to_pbuf(nb); p->payload = nb->data; p->tot_len = p->len = nb->len; err = nf->input(p, nf); if (unlikely(err != ERR_OK)) { // 错误处理 #if CONFIG_LWIP_THREADS && CONFIG_LIBUKNETDEV_DISPATCHERTHREADS /* At this point it is possible that lwIP's input queue * is full or we run out of memory. In this case, we * return to the scheduler and hope that lwIP's main * thread is able to process some packets. * Afterwards, we try it once again. */ if (err == ERR_MEM) { LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: lwIP's input queue full: yielding and trying once again...\n", __func__, nf->name[0], nf->name[1], nf->num)); // 内存满,令当前线程挂起 uk_sched_yield(); err = nf->input(p, nf); if (likely(err == ERR_OK)) continue; } #endif
/* * Drop the packet that we could not send to the stack */ uk_pr_err("%c%c%u: Failed to forward packet to lwIP: %d\n", nf->name[0], nf->name[1], nf->num, err); uk_netbuf_free_single(nb); } } while (uk_netdev_status_more(ret)); }
if (unlikely(p->tot_len > uk_netbuf_tailroom(nb))) { LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Cannot send %"PRIu16" bytes, too big (> %"__PRIsz")\n", __func__, nf->name[0], nf->name[1], nf->num, p->tot_len, uk_netbuf_tailroom(nb))); uk_netbuf_free_single(nb); return ERR_MEM; }
/* * Copy pbuf to netbuf * NOTE: Unfortunately, lwIP seems not to support zero-copy transmit, * yet. As long as we do not have this, we have to copy. */ // 由于目前 lwip 库不支持零复制传输,故需要使用 memcpy 进行复制 wpos = nb->data; for (q = p; q != NULL; q = q->next) { memcpy(wpos, q->payload, q->len); wpos += q->len; } nb->len = p->tot_len; // 开始发包,并判断是否仍然有待处理的数据包 /* Transmit packet */ do { ret = uk_netdev_tx_one(dev, 0, nb); } while (uk_netdev_status_notready(ret)); if (unlikely(ret < 0)) { LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Failed to send %"PRIu16" bytes\n", __func__, nf->name[0], nf->name[1], nf->num, p->tot_len)); /* * Decrease refcount again because in * the error case the netdev did not consume the pbuf */ uk_netbuf_free_single(nb); return ERR_IF; } LWIP_DEBUGF(NETIF_DEBUG, ("%s: %c%c%u: Sent %"PRIu16" bytes\n", __func__, nf->name[0], nf->name[1], nf->num, p->tot_len));
return ERR_OK; }
与用户交互
前文中提到 unikraft 接入了两套 lwip 的 API,分别是 NETCONN API 和 socket API。socket部分不用多说,用户调用 sys/socket.h 中提供的各种函数即可。下面简单介绍一下用户如何调用 NETCONN API 以及在调用时都发生了什么。
netconn_getaddr 函数的作用很简单,就是获取一个 netconn 连接结构的源 IP 地址、端口号与目标IP地址、端口号等信息, 并且 IP 地址保存在 addr 中,端口号保存在 port 中,而 local 指定需要获取的信息是本地IP地址(源 IP 地址) 还是远端 IP 地址(目标 IP 地址),如果是 1 则表示获取本地 IP 地址与端口号,如果为 0 表示远端 IP 地址与端口号。 同样的,该函数会调用 netconn_apimsg 函数构造一个 API 消息,并且请求内核执行lwip_netconn_do_getaddr 函数, 然后通过 netconn 连接结构的信号量进行同步。
#if LWIP_IPV4 && LWIP_IPV6 /* "Socket API like" dual-stack support: If IP to bind to is IP6_ADDR_ANY, * and NETCONN_FLAG_IPV6_V6ONLY is 0, use IP_ANY_TYPE to bind */ if ((netconn_get_ipv6only(conn) == 0) && ip_addr_cmp(addr, IP6_ADDR_ANY)) { addr = IP_ANY_TYPE; } #endif/* LWIP_IPV4 && LWIP_IPV6 */
#if LWIP_TCP #if (LWIP_UDP || LWIP_RAW) if (NETCONNTYPE_GROUP(conn->type) == NETCONN_TCP) #endif/* (LWIP_UDP || LWIP_RAW) */ { structpbuf *p =NULL; /* This is not a listening netconn, since recvmbox is set */
/* pcb->state LISTEN not allowed here */ LWIP_ASSERT("don't call tcp_recved for listen-pcbs", pcb->state != LISTEN);
rcv_wnd = (tcpwnd_size_t)(pcb->rcv_wnd + len); if ((rcv_wnd > TCP_WND_MAX(pcb)) || (rcv_wnd < pcb->rcv_wnd)) { /* window got too big or tcpwnd_size_t overflow */ LWIP_DEBUGF(TCP_DEBUG, ("tcp_recved: window got too big or tcpwnd_size_t overflow\n")); pcb->rcv_wnd = TCP_WND_MAX(pcb); } else { pcb->rcv_wnd = rcv_wnd; }
wnd_inflation = tcp_update_rcv_ann_wnd(pcb);
/* If the change in the right edge of window is significant (default * watermark is TCP_WND/4), then send an explicit update now. * Otherwise wait for a packet to be sent in the normal course of * events (or more window to be available later) */ if (wnd_inflation >= TCP_WND_UPDATE_THRESHOLD) { tcp_ack_now(pcb); tcp_output(pcb); }
LWIP_ERROR("netconn_write: invalid conn", (conn != NULL), return ERR_ARG;); LWIP_ERROR("netconn_write: invalid conn->type", (NETCONNTYPE_GROUP(conn->type) == NETCONN_TCP), return ERR_VAL;); dontblock = netconn_is_nonblocking(conn) || (apiflags & NETCONN_DONTBLOCK); #if LWIP_SO_SNDTIMEO if (conn->send_timeout != 0) { dontblock = 1; } #endif/* LWIP_SO_SNDTIMEO */ if (dontblock && !bytes_written) { /* This implies netconn_write() cannot be used for non-blocking send, since it has no way to return the number of bytes written. */ return ERR_VAL; }
/* sum up the total size */ size = 0; for (i = 0; i < vectorcnt; i++) { size += vectors[i].len; if (size < vectors[i].len) { /* overflow */ return ERR_VAL; } } if (size == 0) { return ERR_OK; } elseif (size > SSIZE_MAX) { ssize_t limited; /* this is required by the socket layer (cannot send full size_t range) */ if (!bytes_written) { return ERR_VAL; } /* limit the amount of data to send */ limited = SSIZE_MAX; size = (size_t)limited; }
API_MSG_VAR_ALLOC(msg); /* non-blocking write sends as much */ API_MSG_VAR_REF(msg).conn = conn; API_MSG_VAR_REF(msg).msg.w.vector = vectors; API_MSG_VAR_REF(msg).msg.w.vector_cnt = vectorcnt; API_MSG_VAR_REF(msg).msg.w.vector_off = 0; API_MSG_VAR_REF(msg).msg.w.apiflags = apiflags; API_MSG_VAR_REF(msg).msg.w.len = size; API_MSG_VAR_REF(msg).msg.w.offset = 0; #if LWIP_SO_SNDTIMEO if (conn->send_timeout != 0) { /* get the time we started, which is later compared to sys_now() + conn->send_timeout */ API_MSG_VAR_REF(msg).msg.w.time_started = sys_now(); } else { API_MSG_VAR_REF(msg).msg.w.time_started = 0; } #endif/* LWIP_SO_SNDTIMEO */
/* For locking the core: this _can_ be delayed on low memory/low send buffer, but if it is, this is done inside api_msg.c:do_write(), so we can use the non-blocking version here. */ err = netconn_apimsg(lwip_netconn_do_write, &API_MSG_VAR_REF(msg)); if (err == ERR_OK) { if (bytes_written != NULL) { *bytes_written = API_MSG_VAR_REF(msg).msg.w.offset; } /* for blocking, check all requested bytes were written, NOTE: send_timeout is treated as dontblock (see dontblock assignment above) */ if (!dontblock) { LWIP_ASSERT("do_write failed to write all bytes", API_MSG_VAR_REF(msg).msg.w.offset == size); } } API_MSG_VAR_FREE(msg);
#if LWIP_SO_SNDTIMEO if ((conn->send_timeout != 0) && ((s32_t)(sys_now() - conn->current_msg->msg.w.time_started) >= conn->send_timeout)) { write_finished = 1; if (conn->current_msg->msg.w.offset == 0) { /* nothing has been written */ err = ERR_WOULDBLOCK; } else { /* partial write */ err = ERR_OK; } } else #endif/* LWIP_SO_SNDTIMEO */ { do { dataptr = (constu8_t *)conn->current_msg->msg.w.vector->ptr + conn->current_msg->msg.w.vector_off; diff = conn->current_msg->msg.w.vector->len - conn->current_msg->msg.w.vector_off; if (diff > 0xffffUL) { /* max_u16_t */ len = 0xffff; apiflags |= TCP_WRITE_FLAG_MORE; } else { len = (u16_t)diff; } available = tcp_sndbuf(conn->pcb.tcp); if (available < len) { /* don't try to write more than sendbuf */ len = available; if (dontblock) { if (!len) { /* set error according to partial write or not */ err = (conn->current_msg->msg.w.offset == 0) ? ERR_WOULDBLOCK : ERR_OK; goto err_mem; } } else { apiflags |= TCP_WRITE_FLAG_MORE; } } LWIP_ASSERT("lwip_netconn_do_writemore: invalid length!", ((conn->current_msg->msg.w.vector_off + len) <= conn->current_msg->msg.w.vector->len)); /* we should loop around for more sending in the following cases: 1) We couldn't finish the current vector because of 16-bit size limitations. tcp_write() and tcp_sndbuf() both are limited to 16-bit sizes 2) We are sending the remainder of the current vector and have more */ if ((len == 0xffff && diff > 0xffffUL) || (len == (u16_t)diff && conn->current_msg->msg.w.vector_cnt > 1)) { write_more = 1; apiflags |= TCP_WRITE_FLAG_MORE; } else { write_more = 0; } err = tcp_write(conn->pcb.tcp, dataptr, len, apiflags); if (err == ERR_OK) { conn->current_msg->msg.w.offset += len; conn->current_msg->msg.w.vector_off += len; /* check if current vector is finished */ if (conn->current_msg->msg.w.vector_off == conn->current_msg->msg.w.vector->len) { conn->current_msg->msg.w.vector_cnt--; /* if we have additional vectors, move on to them */ if (conn->current_msg->msg.w.vector_cnt > 0) { conn->current_msg->msg.w.vector++; conn->current_msg->msg.w.vector_off = 0; } } } } while (write_more && err == ERR_OK); /* if OK or memory error, check available space */ if ((err == ERR_OK) || (err == ERR_MEM)) { err_mem: if (dontblock && (conn->current_msg->msg.w.offset < conn->current_msg->msg.w.len)) { /* non-blocking write did not write everything: mark the pcb non-writable and let poll_tcp check writable space to mark the pcb writable again */ API_EVENT(conn, NETCONN_EVT_SENDMINUS, 0); conn->flags |= NETCONN_FLAG_CHECK_WRITESPACE; } elseif ((tcp_sndbuf(conn->pcb.tcp) <= TCP_SNDLOWAT) || (tcp_sndqueuelen(conn->pcb.tcp) >= TCP_SNDQUEUELOWAT)) { /* The queued byte- or pbuf-count exceeds the configured low-water limit, let select mark this pcb as non-writable. */ API_EVENT(conn, NETCONN_EVT_SENDMINUS, 0); } }
if (err == ERR_OK) { err_t out_err; if ((conn->current_msg->msg.w.offset == conn->current_msg->msg.w.len) || dontblock) { /* return sent length (caller reads length from msg.w.offset) */ write_finished = 1; } out_err = tcp_output(conn->pcb.tcp); if (out_err == ERR_RTE) { /* If tcp_output fails because no route is found, don't try writing any more but return the error to the application thread. */ err = out_err; write_finished = 1; } } elseif (err == ERR_MEM) { /* If ERR_MEM, we wait for sent_tcp or poll_tcp to be called. For blocking sockets, we do NOT return to the application thread, since ERR_MEM is only a temporary error! Non-blocking will remain non-writable until sent_tcp/poll_tcp is called */
/* tcp_write returned ERR_MEM, try tcp_output anyway */ err_t out_err = tcp_output(conn->pcb.tcp); if (out_err == ERR_RTE) { /* If tcp_output fails because no route is found, don't try writing any more but return the error to the application thread. */ err = out_err; write_finished = 1; } elseif (dontblock) { /* non-blocking write is done on ERR_MEM, set error according to partial write or not */ err = (conn->current_msg->msg.w.offset == 0) ? ERR_WOULDBLOCK : ERR_OK; write_finished = 1; } } else { /* On errors != ERR_MEM, we don't try writing any more but return the error to the application thread. */ write_finished = 1; } } if (write_finished) { /* everything was written: set back connection state and back to application task */ sys_sem_t *op_completed_sem = LWIP_API_MSG_SEM(conn->current_msg); conn->current_msg->err = err; conn->current_msg = NULL; conn->state = NETCONN_NONE; #if LWIP_TCPIP_CORE_LOCKING if (delayed) #endif { sys_sem_signal(op_completed_sem); } } #if LWIP_TCPIP_CORE_LOCKING else { return ERR_MEM; } #endif return ERR_OK; }