diff --git a/include/zephyr/linker/common-rom/common-rom-net.ld b/include/zephyr/linker/common-rom/common-rom-net.ld index 909d11c6d573..8b386ee8ce8b 100644 --- a/include/zephyr/linker/common-rom/common-rom-net.ld +++ b/include/zephyr/linker/common-rom/common-rom-net.ld @@ -25,3 +25,7 @@ #if defined(CONFIG_NET_MGMT_EVENT) ITERABLE_SECTION_ROM(net_mgmt_event_static_handler, 4) #endif + +#if defined(CONFIG_NET_SOCKETS_SERVICE) + ITERABLE_SECTION_RAM(net_socket_service_desc, 4) +#endif diff --git a/include/zephyr/net/socket_service.h b/include/zephyr/net/socket_service.h new file mode 100644 index 000000000000..16da94d238ba --- /dev/null +++ b/include/zephyr/net/socket_service.h @@ -0,0 +1,246 @@ +/** + * @file + * @brief BSD Socket service API + * + * API can be used to install a k_work that is called + * if there is data received to a socket. + */ + +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_INCLUDE_NET_SOCKET_SERVICE_H_ +#define ZEPHYR_INCLUDE_NET_SOCKET_SERVICE_H_ + +/** + * @brief BSD socket service API + * @defgroup bsd_socket_service BSD socket service API + * @ingroup networking + * @{ + */ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * This struct contains information which socket triggered + * calls to the callback function. + */ +struct net_socket_service_event { + /** k_work that is done when there is desired activity in file descriptor. */ + struct k_work work; + /** Callback to be called for desired socket activity */ + k_work_handler_t callback; + /** Socket information that triggered this event. */ + struct zsock_pollfd event; + /** User data */ + void *user_data; + /** Service back pointer */ + struct net_socket_service_desc *svc; +}; + +/** + * Main structure holding socket service configuration information. + * The k_work item is created so that when there is data coming + * to those fds, the k_work callback is then called. + * The workqueue can be set NULL in which case system workqueue is used. + * The service descriptor should be created at built time, and then used + * as a parameter to register the sockets to be monitored. + * User should create needed sockets and then setup the poll struct and + * then register the sockets to be monitored at runtime. + */ +struct net_socket_service_desc { +#if CONFIG_NET_SOCKETS_LOG_LEVEL >= LOG_LEVEL_DBG + /** + * Owner name. This can be used in debugging to see who has + * registered this service. + */ + const char *owner; +#endif + /** Workqueue where the work is submitted. */ + struct k_work_q *work_q; + /** Pointer to the list of services that we are listening */ + struct net_socket_service_event *pev; + /** Length of the pollable socket array for this service. */ + int pev_len; + /** Where are my pollfd entries in the global list */ + int *idx; +}; + +#define __z_net_socket_svc_get_name(_svc_id) __z_net_socket_service_##_svc_id +#define __z_net_socket_svc_get_idx(_svc_id) __z_net_socket_service_idx##_svc_id +#define __z_net_socket_svc_get_owner __FILE__ ":" STRINGIFY(__LINE__) + +extern void net_socket_service_callback(struct k_work *work); + +#if CONFIG_NET_SOCKETS_LOG_LEVEL >= LOG_LEVEL_DBG +#define NET_SOCKET_SERVICE_OWNER .owner = __z_net_socket_svc_get_owner, +#else +#define NET_SOCKET_SERVICE_OWNER +#endif + +#define NET_SOCKET_SERVICE_CALLBACK_MODE(_flag) \ + IF_ENABLED(_flag, \ + (.work = Z_WORK_INITIALIZER(net_socket_service_callback),)) + +#define __z_net_socket_service_define(_name, _work_q, _cb, _count, _async, ...) \ + static int __z_net_socket_svc_get_idx(_name); \ + static struct net_socket_service_event \ + __z_net_socket_svc_get_name(_name)[_count] = { \ + [0 ... ((_count) - 1)] = { \ + .event.fd = -1, /* Invalid socket */ \ + NET_SOCKET_SERVICE_CALLBACK_MODE(_async) \ + .callback = _cb, \ + } \ + }; \ + COND_CODE_0(NUM_VA_ARGS_LESS_1(__VA_ARGS__), (), __VA_ARGS__) \ + const STRUCT_SECTION_ITERABLE(net_socket_service_desc, _name) = { \ + NET_SOCKET_SERVICE_OWNER \ + .work_q = (_work_q), \ + .pev = __z_net_socket_svc_get_name(_name), \ + .pev_len = (_count), \ + .idx = &__z_net_socket_svc_get_idx(_name), \ + } + +/** + * @brief Statically define a network socket service. + * The user callback is called asynchronously for this service meaning that + * the service API will not wait until the user callback returns before continuing + * with next socket service. + * + * The socket service can be accessed outside the module where it is defined using: + * + * @code extern struct net_socket_service_desc ; @endcode + * + * @note This macro cannot be used together with a static keyword. + * If such a use-case is desired, use NET_SOCKET_SERVICE_ASYNC_DEFINE_STATIC + * instead. + * + * @param name Name of the service. + * @param work_q Pointer to workqueue where the work is done. Can be null in which case + * system workqueue is used. + * @param cb Callback function that is called for socket activity. + * @param count How many pollable sockets is needed for this service. + */ +#define NET_SOCKET_SERVICE_ASYNC_DEFINE(name, work_q, cb, count) \ + __z_net_socket_service_define(name, work_q, cb, count, 1) + +/** + * @brief Statically define a network socket service in a private (static) scope. + * The user callback is called asynchronously for this service meaning that + * the service API will not wait until the user callback returns before continuing + * with next socket service. + * + * @param name Name of the service. + * @param work_q Pointer to workqueue where the work is done. Can be null in which case + * system workqueue is used. + * @param cb Callback function that is called for socket activity. + * @param count How many pollable sockets is needed for this service. + */ +#define NET_SOCKET_SERVICE_ASYNC_DEFINE_STATIC(name, work_q, cb, count) \ + __z_net_socket_service_define(name, work_q, cb, count, 1, static) + +/** + * @brief Statically define a network socket service. + * The user callback is called synchronously for this service meaning that + * the service API will wait until the user callback returns before continuing + * with next socket service. + * + * The socket service can be accessed outside the module where it is defined using: + * + * @code extern struct net_socket_service_desc ; @endcode + * + * @note This macro cannot be used together with a static keyword. + * If such a use-case is desired, use NET_SOCKET_SERVICE_SYNC_DEFINE_STATIC + * instead. + * + * @param name Name of the service. + * @param work_q Pointer to workqueue where the work is done. Can be null in which case + * system workqueue is used. + * @param cb Callback function that is called for socket activity. + * @param count How many pollable sockets is needed for this service. + */ +#define NET_SOCKET_SERVICE_SYNC_DEFINE(name, work_q, cb, count) \ + __z_net_socket_service_define(name, work_q, cb, count, 0) + +/** + * @brief Statically define a network socket service in a private (static) scope. + * The user callback is called synchronously for this service meaning that + * the service API will wait until the user callback returns before continuing + * with next socket service. + * + * @param name Name of the service. + * @param work_q Pointer to workqueue where the work is done. Can be null in which case + * system workqueue is used. + * @param cb Callback function that is called for socket activity. + * @param count How many pollable sockets is needed for this service. + */ +#define NET_SOCKET_SERVICE_SYNC_DEFINE_STATIC(name, work_q, cb, count) \ + __z_net_socket_service_define(name, work_q, cb, count, 0, static) + +/** + * @brief Register pollable sockets. + * + * @param service Pointer to a service description. + * @param fds Socket array to poll. + * @param len Length of the socket array. + * @param user_data User specific data. + * + * @retval 0 No error + * @retval -ENOENT Service is not found. + * @retval -ENINVAL Invalid parameter. + */ +__syscall int net_socket_service_register(const struct net_socket_service_desc *service, + struct zsock_pollfd *fds, int len, void *user_data); + +/** + * @brief Unregister pollable sockets. + * + * @param service Pointer to a service description. + * + * @retval 0 No error + * @retval -ENOENT Service is not found. + * @retval -ENINVAL Invalid parameter. + */ +static inline int net_socket_service_unregister(const struct net_socket_service_desc *service) +{ + return net_socket_service_register(service, NULL, 0, NULL); +} + +/** + * @typedef net_socket_service_cb_t + * @brief Callback used while iterating over socket services. + * + * @param svc Pointer to current socket service. + * @param user_data A valid pointer to user data or NULL + */ +typedef void (*net_socket_service_cb_t)(const struct net_socket_service_desc *svc, + void *user_data); + +/** + * @brief Go through all the socket services and call callback for each service. + * + * @param cb User-supplied callback function to call + * @param user_data User specified data + */ +void net_socket_service_foreach(net_socket_service_cb_t cb, void *user_data); + +#ifdef __cplusplus +} +#endif + +#include + +/** + * @} + */ + +#endif /* ZEPHYR_INCLUDE_NET_SOCKET_SERVICE_H_ */ diff --git a/samples/net/sockets/echo_service/CMakeLists.txt b/samples/net/sockets/echo_service/CMakeLists.txt new file mode 100644 index 000000000000..fda31959a7db --- /dev/null +++ b/samples/net/sockets/echo_service/CMakeLists.txt @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(sockets_service_echo) + +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) + +include(${ZEPHYR_BASE}/samples/net/common/common.cmake) diff --git a/samples/net/sockets/echo_service/README.rst b/samples/net/sockets/echo_service/README.rst new file mode 100644 index 000000000000..3e1d7625d701 --- /dev/null +++ b/samples/net/sockets/echo_service/README.rst @@ -0,0 +1,51 @@ +.. zephyr:code-sample:: sockets-service-echo + :name: Echo server (service) + :relevant-api: bsd_sockets + + Implements a simple IPv4/IPv6 TCP echo server using BSD sockets and socket service API. + +Overview +******** + +The sockets/echo_service sample application for Zephyr implements a TCP echo +server supporting both IPv4 and IPv6 and using a BSD Sockets compatible API. + +The purpose of this sample is to show how to use socket service API. +The socket service is a concept where many blocking sockets can be listened by +one thread, and which can then trigger a callback if there is activity in the set +of sockets. This saves memory as only one thread needs to be created in the +system. + +The application supports IPv4 and IPv6, and both UDP and TCP are also supported. +The source code for this sample application can be found at: +:zephyr_file:`samples/net/sockets/echo_service`. + +Requirements +************ + +- :ref:`networking_with_host` +- or, a board with hardware networking + +Building and Running +******************** + +Build the Zephyr version of the sockets/echo_service application like this: + +.. zephyr-app-commands:: + :zephyr-app: samples/net/sockets/echo_service + :board: + :goals: build + :compact: + +After the sample starts, it expects connections at 192.0.2.1, or 2001:db8::1 +and port 4242. +The easiest way to connect is: + +.. code-block:: console + + $ telnet 192.0.2.1 4242 + +After a connection is made, the application will echo back any line sent +to it. The application implements a single-threaded server using blocking +sockets, and currently is only implemented to serve only one client connection +at time. After the current client disconnects, the next connection can proceed. diff --git a/samples/net/sockets/echo_service/overlay-e1000.conf b/samples/net/sockets/echo_service/overlay-e1000.conf new file mode 100644 index 000000000000..adcf29f904d7 --- /dev/null +++ b/samples/net/sockets/echo_service/overlay-e1000.conf @@ -0,0 +1,6 @@ +# Overlay for experimental TCP as qemu_x86 with E1000 + +CONFIG_PCIE=y + +CONFIG_NET_L2_ETHERNET=y +CONFIG_NET_QEMU_ETHERNET=y diff --git a/samples/net/sockets/echo_service/prj.conf b/samples/net/sockets/echo_service/prj.conf new file mode 100644 index 000000000000..c39159ea7e48 --- /dev/null +++ b/samples/net/sockets/echo_service/prj.conf @@ -0,0 +1,38 @@ +# General config +# The async method used in the sample needs more stack for the workqueue +CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=1500 +CONFIG_POSIX_API=y + +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=y +CONFIG_NET_TCP=y +CONFIG_NET_SOCKETS=y +CONFIG_NET_SOCKETS_POSIX_NAMES=y +CONFIG_NET_IPV4_MAPPING_TO_IPV6=y +CONFIG_POSIX_MAX_FDS=10 +CONFIG_NET_MAX_CONN=5 +CONFIG_NET_SOCKETS_SERVICE=y +CONFIG_NET_SOCKETS_POLL_MAX=20 + +# Network driver config +CONFIG_TEST_RANDOM_GENERATOR=y + +# Network address config +CONFIG_NET_CONFIG_SETTINGS=y +CONFIG_NET_CONFIG_NEED_IPV4=y +CONFIG_NET_CONFIG_NEED_IPV6=y +CONFIG_NET_CONFIG_MY_IPV4_ADDR="192.0.2.1" +CONFIG_NET_CONFIG_PEER_IPV4_ADDR="192.0.2.2" +CONFIG_NET_CONFIG_MY_IPV6_ADDR="2001:db8::1" +CONFIG_NET_CONFIG_PEER_IPV6_ADDR="2001:db8::2" + +# Network buffers +CONFIG_NET_PKT_RX_COUNT=16 +CONFIG_NET_PKT_TX_COUNT=16 +CONFIG_NET_BUF_RX_COUNT=64 +CONFIG_NET_BUF_TX_COUNT=64 +CONFIG_NET_CONTEXT_NET_PKT_POOL=y + +CONFIG_NET_SHELL=y diff --git a/samples/net/sockets/echo_service/sample.yaml b/samples/net/sockets/echo_service/sample.yaml new file mode 100644 index 000000000000..aee10686c0f0 --- /dev/null +++ b/samples/net/sockets/echo_service/sample.yaml @@ -0,0 +1,16 @@ +sample: + description: echo server using socket service API + name: socket_service_echo +common: + harness: net + depends_on: netif + filter: CONFIG_FULL_LIBC_SUPPORTED and not CONFIG_NATIVE_LIBC + # eventfd does not work properly with native_posix so exclude it here + platform_exclude: + - native_posix + - native_posix_64 +tests: + sample.net.sockets.service.echo: + tags: + - net + - socket diff --git a/samples/net/sockets/echo_service/src/main.c b/samples/net/sockets/echo_service/src/main.c new file mode 100644 index 000000000000..dbdde28cf302 --- /dev/null +++ b/samples/net/sockets/echo_service/src/main.c @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(net_echo_server_svc_sample, LOG_LEVEL_DBG); + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#define MY_PORT 4242 + +static char addr_str[INET6_ADDRSTRLEN]; + +static struct pollfd sockfd_udp[1] = { + [0] = { .fd = -1 }, /* UDP socket */ +}; +static struct pollfd sockfd_tcp[1] = { + [0] = { .fd = -1 }, /* TCP socket */ +}; + +#define MAX_SERVICES 1 + +static void receive_data(bool is_udp, struct net_socket_service_event *pev, + char *buf, size_t buflen); + +static void tcp_service_handler(struct k_work *work) +{ + struct net_socket_service_event *pev = + CONTAINER_OF(work, struct net_socket_service_event, work); + static char buf[1500]; + + /* Note that in this application we receive / send data from + * system work queue. In proper application the socket reading and data + * sending should be done so that the system work queue is not blocked. + * It is possible to create a socket service that uses own work queue. + */ + receive_data(false, pev, buf, sizeof(buf)); +} + +static void udp_service_handler(struct k_work *work) +{ + struct net_socket_service_event *pev = + CONTAINER_OF(work, struct net_socket_service_event, work); + static char buf[1500]; + + receive_data(true, pev, buf, sizeof(buf)); +} + +/* In this example we create two services, one with async behavior and one with + * sync one. The async is for TCP and sync is for UDP (this is just an arbitrary + * choice). + * This is an artificial example, both UDP and TCP sockets could be served by the + * same service. + */ +NET_SOCKET_SERVICE_SYNC_DEFINE_STATIC(service_udp, NULL, udp_service_handler, MAX_SERVICES); +NET_SOCKET_SERVICE_ASYNC_DEFINE_STATIC(service_tcp, NULL, tcp_service_handler, MAX_SERVICES); + +static void receive_data(bool is_udp, struct net_socket_service_event *pev, + char *buf, size_t buflen) +{ + struct pollfd *pfd = &pev->event; + int client = pfd->fd; + struct sockaddr_in6 addr; + socklen_t addrlen = sizeof(addr); + int len, out_len; + char *p; + + len = recvfrom(client, buf, buflen, 0, + (struct sockaddr *)&addr, &addrlen); + if (len <= 0) { + if (len < 0) { + LOG_ERR("recv: %d", -errno); + } + + /* If the TCP socket is closed, mark it as non pollable */ + if (!is_udp && sockfd_tcp[0].fd == client) { + sockfd_tcp[0].fd = -1; + + /* Update the handler so that client connection is + * not monitored any more. + */ + (void)net_socket_service_register(&service_tcp, sockfd_tcp, + ARRAY_SIZE(sockfd_tcp), NULL); + close(client); + + LOG_INF("Connection from %s closed", addr_str); + } + + return; + } + + p = buf; + do { + out_len = sendto(client, p, len, 0, + (struct sockaddr *)&addr, addrlen); + if (out_len < 0) { + LOG_ERR("sendto: %d", -errno); + break; + } + + p += out_len; + len -= out_len; + } while (len); +} + +static int setup_tcp_socket(struct sockaddr_in6 *addr) +{ + socklen_t optlen = sizeof(int); + int ret, sock, opt; + + sock = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); + if (sock < 0) { + LOG_ERR("socket: %d", -errno); + return -errno; + } + + ret = getsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, &optlen); + if (ret == 0 && opt) { + LOG_INF("IPV6_V6ONLY option is on, turning it off."); + + opt = 0; + ret = setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, optlen); + if (ret < 0) { + LOG_WRN("Cannot turn off IPV6_V6ONLY option"); + } else { + LOG_INF("Sharing same socket between IPv6 and IPv4"); + } + } + + if (bind(sock, (struct sockaddr *)addr, sizeof(*addr)) < 0) { + LOG_ERR("bind: %d", -errno); + return -errno; + } + + if (listen(sock, 5) < 0) { + LOG_ERR("listen: %d", -errno); + return -errno; + } + + return sock; +} + +static int setup_udp_socket(struct sockaddr_in6 *addr) +{ + socklen_t optlen = sizeof(int); + int ret, sock, opt; + + sock = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); + if (sock < 0) { + LOG_ERR("socket: %d", -errno); + return -errno; + } + + ret = getsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, &optlen); + if (ret == 0 && opt) { + LOG_INF("IPV6_V6ONLY option is on, turning it off."); + + opt = 0; + ret = setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, optlen); + if (ret < 0) { + LOG_WRN("Cannot turn off IPV6_V6ONLY option"); + } else { + LOG_INF("Sharing same socket between IPv6 and IPv4"); + } + } + + if (bind(sock, (struct sockaddr *)addr, sizeof(*addr)) < 0) { + LOG_ERR("bind: %d", -errno); + return -errno; + } + + return sock; +} + +int main(void) +{ + int tcp_sock, udp_sock, ret; + struct sockaddr_in6 addr = { + .sin6_family = AF_INET6, + .sin6_addr = IN6ADDR_ANY_INIT, + .sin6_port = htons(MY_PORT), + }; + static int counter; + + tcp_sock = setup_tcp_socket(&addr); + if (tcp_sock < 0) { + return tcp_sock; + } + + udp_sock = setup_udp_socket(&addr); + if (udp_sock < 0) { + return udp_sock; + } + + sockfd_udp[0].fd = udp_sock; + sockfd_udp[0].events = POLLIN; + + /* Register UDP socket to service handler */ + ret = net_socket_service_register(&service_udp, sockfd_udp, + ARRAY_SIZE(sockfd_udp), NULL); + if (ret < 0) { + LOG_ERR("Cannot register socket service handler (%d)", ret); + } + + LOG_INF("Single-threaded TCP/UDP echo server waits " + "for a connection on port %d", MY_PORT); + + while (1) { + struct sockaddr_in6 client_addr; + socklen_t client_addr_len = sizeof(client_addr); + int client; + + client = accept(tcp_sock, (struct sockaddr *)&client_addr, + &client_addr_len); + if (client < 0) { + LOG_ERR("accept: %d", -errno); + continue; + } + + inet_ntop(client_addr.sin6_family, &client_addr.sin6_addr, + addr_str, sizeof(addr_str)); + LOG_INF("Connection #%d from %s (%d)", counter++, addr_str, client); + + sockfd_tcp[0].fd = client; + sockfd_tcp[0].events = POLLIN; + + /* Register all the sockets to service handler */ + ret = net_socket_service_register(&service_tcp, sockfd_tcp, + ARRAY_SIZE(sockfd_tcp), NULL); + if (ret < 0) { + LOG_ERR("Cannot register socket service handler (%d)", + ret); + break; + } + } + + (void)net_socket_service_unregister(&service_tcp); + (void)net_socket_service_unregister(&service_udp); + + close(tcp_sock); + close(udp_sock); + + return 0; +} diff --git a/subsys/net/lib/shell/sockets.c b/subsys/net/lib/shell/sockets.c index 8be67fc5faf0..012c572d7770 100644 --- a/subsys/net/lib/shell/sockets.c +++ b/subsys/net/lib/shell/sockets.c @@ -9,6 +9,7 @@ LOG_MODULE_DECLARE(net_shell); #include "net_shell_private.h" #include +#include #if defined(CONFIG_NET_SOCKETS_OBJ_CORE) struct socket_info { @@ -54,7 +55,8 @@ int walk_sockets(struct k_obj_core *obj_core, void *user_data) * actual lifetime as calculated in close() */ lifetime = obj->create_time; - strncat(fd, "C", 1); + fd[0] = 'C'; + fd[1] = '\0'; count->closed++; } else { lifetime = k_ticks_to_ms_ceil32(sys_clock_tick_get() - @@ -78,6 +80,57 @@ int walk_sockets(struct k_obj_core *obj_core, void *user_data) } #endif /* CONFIG_NET_SOCKETS_OBJ_CORE */ +#if defined(CONFIG_NET_SOCKETS_SERVICE) + +#if CONFIG_NET_SOCKETS_LOG_LEVEL >= LOG_LEVEL_DBG +#define MAX_OWNER_LEN 32 +#else +#define MAX_OWNER_LEN sizeof("") +#endif + +static void walk_socket_services(const struct net_socket_service_desc *svc, + void *user_data) +{ + struct net_shell_user_data *data = user_data; + const struct shell *sh = data->sh; + int *count = data->user_data; + int len = 0; + static char pev_output[sizeof("xxx,") * CONFIG_NET_SOCKETS_POLL_MAX]; + static char owner[MAX_OWNER_LEN + 1]; + + NET_ASSERT(svc->pev != NULL); + + for (int i = 0; i < svc->pev_len; i++) { + len += snprintk(pev_output + len, sizeof(pev_output) - len, + "%d,", svc->pev[i].event.fd); + } + + if (len > 0) { + pev_output[len - 1] = '\0'; + } + +#if CONFIG_NET_SOCKETS_LOG_LEVEL >= LOG_LEVEL_DBG + len = strlen(svc->owner); + + int offset = len > sizeof(owner) ? + len -= (sizeof(owner) - 3) : 0; + + snprintk(owner, sizeof(owner), "%s%s", + offset == 0 ? "" : "...", + svc->owner + offset + 1); +#else + snprintk(owner, sizeof(owner), ""); +#endif + + PR("%32s %-6s %-5d %s\n", + owner, + svc->pev->work.handler == NULL ? "SYNC" : "ASYNC", + svc->pev_len, pev_output); + + (*count)++; +} +#endif /* CONFIG_NET_SOCKETS_SERVICE */ + static int cmd_net_sockets(const struct shell *sh, size_t argc, char *argv[]) { #if defined(CONFIG_NET_SOCKETS_OBJ_CORE) @@ -113,7 +166,39 @@ static int cmd_net_sockets(const struct shell *sh, size_t argc, char *argv[]) count.closed == 1 ? "" : "s"); } } -#else + +#if defined(CONFIG_NET_SOCKETS_SERVICE) + PR("\n"); +#endif +#endif + +#if defined(CONFIG_NET_SOCKETS_SERVICE) + struct net_shell_user_data svc_user_data; + int svc_count = 0; + + svc_user_data.sh = sh; + svc_user_data.user_data = &svc_count; + + PR("Services:\n"); + PR("%32s %-6s %-5s %s\n", + "Owner", "Mode", "Count", "FDs"); + PR("\n"); + + net_socket_service_foreach(walk_socket_services, (void *)&svc_user_data); + + if (svc_count == 0) { + PR("No socket services found.\n"); + } else { + PR("\n%d socket service%s found.\n", svc_count, + svc_count == 1 ? "" : "s"); + } + +#if !defined(CONFIG_NET_SOCKETS_OBJ_CORE) + PR("\n"); +#endif +#endif + +#if !defined(CONFIG_NET_SOCKETS_OBJ_CORE) ARG_UNUSED(argc); ARG_UNUSED(argv); @@ -121,6 +206,12 @@ static int cmd_net_sockets(const struct shell *sh, size_t argc, char *argv[]) "CONFIG_OBJ_CORE and CONFIG_NET_SOCKETS_OBJ_CORE", "socket information"); #endif +#if !defined(CONFIG_NET_SOCKETS_SERVICE) + ARG_UNUSED(argc); + ARG_UNUSED(argv); + + PR_INFO("Socket service not supported.\n"); +#endif return 0; } diff --git a/subsys/net/lib/sockets/CMakeLists.txt b/subsys/net/lib/sockets/CMakeLists.txt index 7ffd6dc476b9..253cb4a182f8 100644 --- a/subsys/net/lib/sockets/CMakeLists.txt +++ b/subsys/net/lib/sockets/CMakeLists.txt @@ -26,6 +26,7 @@ zephyr_library_sources_ifdef(CONFIG_NET_SOCKETS_SOCKOPT_TLS sockets_tls.c zephyr_library_sources_ifdef(CONFIG_NET_SOCKETS_OFFLOAD socket_offload.c) zephyr_library_sources_ifdef(CONFIG_NET_SOCKETS_OFFLOAD_DISPATCHER socket_dispatcher.c) zephyr_library_sources_ifdef(CONFIG_NET_SOCKETS_OBJ_CORE socket_obj_core.c) +zephyr_library_sources_ifdef(CONFIG_NET_SOCKETS_SERVICE sockets_service.c) if(CONFIG_NET_SOCKETS_NET_MGMT) zephyr_library_sources(sockets_net_mgmt.c) diff --git a/subsys/net/lib/sockets/Kconfig b/subsys/net/lib/sockets/Kconfig index b2da30c1e487..40d86b9498f0 100644 --- a/subsys/net/lib/sockets/Kconfig +++ b/subsys/net/lib/sockets/Kconfig @@ -73,6 +73,31 @@ config NET_SOCKET_MAX_SEND_WAIT The maximum time a socket is waiting for a blocked connection before returning an ENOBUFS error. +config NET_SOCKETS_SERVICE + bool "Socket service support [EXPERIMENTAL]" + select EXPERIMENTAL + select EVENTFD + help + The socket service can monitor multiple sockets and save memory + by only having one thread listening socket data. If data is received + in the monitored socket, a user supplied work is called. + Note that you need to set CONFIG_NET_SOCKETS_POLL_MAX high enough + so that enough sockets entries can be serviced. This depends on + system needs as multiple services can be activated at the same time + depending on network configuration. + +config NET_SOCKETS_SERVICE_STACK_SIZE + int "Stack size for the thread handling socket services" + default 1200 + depends on NET_SOCKETS_SERVICE + help + Set the internal stack size for the thread that polls sockets. + +config NET_SOCKETS_SERVICE_INIT_PRIO + int "Startup priority for the network socket service" + default 95 + depends on NET_SOCKETS_SERVICE + config NET_SOCKETS_SOCKOPT_TLS bool "TCP TLS socket option support [EXPERIMENTAL]" imply TLS_CREDENTIALS diff --git a/subsys/net/lib/sockets/sockets_service.c b/subsys/net/lib/sockets/sockets_service.c new file mode 100644 index 000000000000..d253ece629c5 --- /dev/null +++ b/subsys/net/lib/sockets/sockets_service.c @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(net_sock_svc, CONFIG_NET_SOCKETS_LOG_LEVEL); + +#include +#include +#include + +static int init_socket_service(void); +static bool init_done; + +static K_MUTEX_DEFINE(lock); +static K_CONDVAR_DEFINE(wait_start); + +STRUCT_SECTION_START_EXTERN(net_socket_service_desc); +STRUCT_SECTION_END_EXTERN(net_socket_service_desc); + +static struct service { + /* The +1 is for triggering events from register function */ + struct zsock_pollfd events[1 + CONFIG_NET_SOCKETS_POLL_MAX]; + int count; +} ctx; + +#define get_idx(svc) (*(svc->idx)) + +void net_socket_service_foreach(net_socket_service_cb_t cb, void *user_data) +{ + STRUCT_SECTION_FOREACH(net_socket_service_desc, svc) { + cb(svc, user_data); + } +} + +static void cleanup_svc_events(const struct net_socket_service_desc *svc) +{ + for (int i = 0; i < svc->pev_len; i++) { + ctx.events[get_idx(svc) + i].fd = -1; + svc->pev[i].event.fd = -1; + svc->pev[i].event.events = 0; + } +} + +int z_impl_net_socket_service_register(const struct net_socket_service_desc *svc, + struct zsock_pollfd *fds, int len, + void *user_data) +{ + int i, ret = -ENOENT; + + k_mutex_lock(&lock, K_FOREVER); + + if (!init_done) { + (void)k_condvar_wait(&wait_start, &lock, K_FOREVER); + } + + if (STRUCT_SECTION_START(net_socket_service_desc) > svc || + STRUCT_SECTION_END(net_socket_service_desc) <= svc) { + goto out; + } + + if (fds == NULL) { + cleanup_svc_events(svc); + } else { + if (len > svc->pev_len) { + NET_DBG("Too many file descriptors, " + "max is %d for service %p", + svc->pev_len, svc); + ret = -ENOMEM; + goto out; + } + + for (i = 0; i < len; i++) { + svc->pev[i].event = fds[i]; + svc->pev[i].user_data = user_data; + } + + for (i = 0; i < svc->pev_len; i++) { + ctx.events[get_idx(svc) + i] = svc->pev[i].event; + } + } + + /* Tell the thread to re-read the variables */ + eventfd_write(ctx.events[0].fd, 1); + ret = 0; + +out: + k_mutex_unlock(&lock); + + return ret; +} + +static struct net_socket_service_desc *find_svc_and_event( + struct zsock_pollfd *pev, + struct net_socket_service_event **event) +{ + STRUCT_SECTION_FOREACH(net_socket_service_desc, svc) { + for (int i = 0; i < svc->pev_len; i++) { + if (svc->pev[i].event.fd == pev->fd) { + *event = &svc->pev[i]; + return svc; + } + } + } + + return NULL; +} + +/* We do not set the user callback to our work struct because we need to + * hook into the flow and restore the global poll array so that the next poll + * round will not notice it and call the callback again while we are + * servicing the callback. + */ +void net_socket_service_callback(struct k_work *work) +{ + struct net_socket_service_event *pev = + CONTAINER_OF(work, struct net_socket_service_event, work); + struct net_socket_service_desc *svc = pev->svc; + struct net_socket_service_event ev = *pev; + + ev.callback(&ev.work); + + /* Copy back the socket fd to the global array because we marked + * it as -1 when triggering the work. + */ + for (int i = 0; i < svc->pev_len; i++) { + ctx.events[get_idx(svc) + i] = svc->pev[i].event; + } +} + +static int call_work(struct zsock_pollfd *pev, struct k_work_q *work_q, + struct k_work *work) +{ + int ret = 0; + + /* Mark the global fd non pollable so that we do not + * call the callback second time. + */ + pev->fd = -1; + + if (work->handler == NULL) { + /* Synchronous call */ + net_socket_service_callback(work); + } else { + if (work_q != NULL) { + ret = k_work_submit_to_queue(work_q, work); + } else { + ret = k_work_submit(work); + } + + k_yield(); + } + + return ret; + +} + +static int trigger_work(struct zsock_pollfd *pev) +{ + struct net_socket_service_event *event; + struct net_socket_service_desc *svc; + + svc = find_svc_and_event(pev, &event); + if (svc == NULL) { + return -ENOENT; + } + + event->svc = svc; + + /* Copy the triggered event to our event so that we know what + * was actually causing the event. + */ + event->event = *pev; + + return call_work(pev, svc->work_q, &event->work); +} + +static void socket_service_thread(void) +{ + int ret, i, fd, count = 0; + eventfd_t value; + + STRUCT_SECTION_COUNT(net_socket_service_desc, &ret); + if (ret == 0) { + NET_INFO("No socket services found, service disabled."); + goto fail; + } + + /* Create contiguous poll event array to enable socket polling */ + STRUCT_SECTION_FOREACH(net_socket_service_desc, svc) { + get_idx(svc) = count + 1; + count += svc->pev_len; + } + + if ((count + 1) > ARRAY_SIZE(ctx.events)) { + NET_WARN("You have %d services to monitor but " + "%d poll entries configured.", + count + 1, ARRAY_SIZE(ctx.events)); + NET_WARN("Consider increasing value of %s to %d", + "CONFIG_NET_SOCKETS_POLL_MAX", count + 1); + } + + NET_DBG("Monitoring %d socket entries", count); + + ctx.count = count + 1; + + /* Create an eventfd that can be used to trigger events during polling */ + fd = eventfd(0, 0); + if (fd < 0) { + fd = -errno; + NET_ERR("eventfd failed (%d)", fd); + goto out; + } + + init_done = true; + k_condvar_broadcast(&wait_start); + + ctx.events[0].fd = fd; + ctx.events[0].events = ZSOCK_POLLIN; + +restart: + i = 1; + + k_mutex_lock(&lock, K_FOREVER); + + /* Copy individual events to the big array */ + STRUCT_SECTION_FOREACH(net_socket_service_desc, svc) { + for (int j = 0; j < svc->pev_len; j++) { + ctx.events[get_idx(svc) + j] = svc->pev[j].event; + } + } + + k_mutex_unlock(&lock); + + while (true) { + ret = zsock_poll(ctx.events, count + 1, -1); + if (ret < 0) { + ret = -errno; + NET_ERR("poll failed (%d)", ret); + goto out; + } + + if (ret == 0) { + /* should not happen because timeout is -1 */ + break; + } + + if (ret > 0 && ctx.events[0].revents) { + eventfd_read(ctx.events[0].fd, &value); + NET_DBG("Received restart event."); + goto restart; + } + + for (i = 1; i < (count + 1); i++) { + if (ctx.events[i].fd < 0) { + continue; + } + + if (ctx.events[i].revents > 0) { + ret = trigger_work(&ctx.events[i]); + if (ret < 0) { + NET_DBG("Triggering work failed (%d)", ret); + } + } + } + } + +out: + NET_DBG("Socket service thread stopped"); + init_done = false; + + return; + +fail: + k_condvar_broadcast(&wait_start); +} + +K_THREAD_DEFINE(socket_service_monitor, CONFIG_NET_SOCKETS_SERVICE_STACK_SIZE, + socket_service_thread, NULL, NULL, NULL, + K_LOWEST_APPLICATION_THREAD_PRIO, 0, 0); + +static int init_socket_service(void) +{ + k_thread_name_set(socket_service_monitor, "net_socket_service"); + + return 0; +} + +SYS_INIT(init_socket_service, APPLICATION, CONFIG_NET_SOCKETS_SERVICE_INIT_PRIO); diff --git a/tests/net/socket/service/CMakeLists.txt b/tests/net/socket/service/CMakeLists.txt new file mode 100644 index 000000000000..1f85d6ad3729 --- /dev/null +++ b/tests/net/socket/service/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(socket_poll) + +target_include_directories(app PRIVATE ${ZEPHYR_BASE}/subsys/net/ip) +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/net/socket/service/prj.conf b/tests/net/socket/service/prj.conf new file mode 100644 index 000000000000..5e51a0f6a53e --- /dev/null +++ b/tests/net/socket/service/prj.conf @@ -0,0 +1,31 @@ +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=y +CONFIG_NET_UDP=y +CONFIG_NET_TCP=y +CONFIG_NET_SOCKETS=y +CONFIG_POSIX_MAX_FDS=10 +CONFIG_NET_PKT_TX_COUNT=8 +CONFIG_NET_PKT_RX_COUNT=8 +CONFIG_NET_MAX_CONN=5 +CONFIG_NET_SOCKETS_SERVICE=y +CONFIG_NET_SOCKETS_POLL_MAX=20 + +# We need to set POSIX_API and use picolibc for eventfd to work +CONFIG_POSIX_API=y +CONFIG_PICOLIBC=y + +# Network driver config +CONFIG_TEST_RANDOM_GENERATOR=y + +CONFIG_MAIN_STACK_SIZE=2048 +CONFIG_ZTEST_STACK_SIZE=1280 + +CONFIG_ZTEST=y + +CONFIG_NET_TEST=y +CONFIG_NET_DRIVERS=y +CONFIG_NET_LOOPBACK=y +CONFIG_NET_TCP_MAX_RECV_WINDOW_SIZE=128 +CONFIG_NET_TCP_TIME_WAIT_DELAY=50 diff --git a/tests/net/socket/service/src/main.c b/tests/net/socket/service/src/main.c new file mode 100644 index 000000000000..1eb10fff6f3a --- /dev/null +++ b/tests/net/socket/service/src/main.c @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2023 Nordic Semiconductor + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(net_test, CONFIG_NET_SOCKETS_LOG_LEVEL); + +#include +#include + +#include + +#include "../../socket_helpers.h" + +#define BUF_AND_SIZE(buf) buf, sizeof(buf) - 1 +#define STRLEN(buf) (sizeof(buf) - 1) + +#define TEST_STR_SMALL "test" + +#define MY_IPV6_ADDR "::1" + +#define ANY_PORT 0 +#define SERVER_PORT 4242 +#define CLIENT_PORT 9898 + +#define TCP_TEARDOWN_TIMEOUT K_SECONDS(3) + +K_SEM_DEFINE(wait_data, 0, UINT_MAX); +K_SEM_DEFINE(wait_data_tcp, 0, UINT_MAX); +#define WAIT_TIME 500 + +static void server_handler(struct k_work *work) +{ + struct net_socket_service_event *pev = + CONTAINER_OF(work, struct net_socket_service_event, work); + + ARG_UNUSED(pev); + + k_sem_give(&wait_data); +} + +static void tcp_server_handler(struct k_work *work) +{ + struct net_socket_service_event *pev = + CONTAINER_OF(work, struct net_socket_service_event, work); + + ARG_UNUSED(pev); + + k_sem_give(&wait_data_tcp); + + k_yield(); + + Z_SPIN_DELAY(100); +} + +NET_SOCKET_SERVICE_ASYNC_DEFINE(udp_service_async, NULL, server_handler, 2); +NET_SOCKET_SERVICE_ASYNC_DEFINE(tcp_service_small_async, NULL, tcp_server_handler, 1); +NET_SOCKET_SERVICE_ASYNC_DEFINE_STATIC(tcp_service_async, NULL, tcp_server_handler, 2); + +NET_SOCKET_SERVICE_SYNC_DEFINE(udp_service_sync, NULL, server_handler, 2); +NET_SOCKET_SERVICE_SYNC_DEFINE(tcp_service_small_sync, NULL, tcp_server_handler, 1); +NET_SOCKET_SERVICE_SYNC_DEFINE_STATIC(tcp_service_sync, NULL, tcp_server_handler, 2); + + +void run_test_service(const struct net_socket_service_desc *udp_service, + const struct net_socket_service_desc *tcp_service_small, + const struct net_socket_service_desc *tcp_service) +{ + int ret; + int c_sock_udp; + int s_sock_udp; + int c_sock_tcp; + int s_sock_tcp; + int new_sock; + struct sockaddr_in6 c_addr; + struct sockaddr_in6 s_addr; + ssize_t len; + char buf[10]; + struct zsock_pollfd sock[2] = { + [0] = { .fd = -1 }, + [1] = { .fd = -1 }, + }; + + prepare_sock_udp_v6(MY_IPV6_ADDR, CLIENT_PORT, &c_sock_udp, &c_addr); + prepare_sock_udp_v6(MY_IPV6_ADDR, SERVER_PORT, &s_sock_udp, &s_addr); + prepare_sock_tcp_v6(MY_IPV6_ADDR, CLIENT_PORT, &c_sock_tcp, &c_addr); + prepare_sock_tcp_v6(MY_IPV6_ADDR, SERVER_PORT, &s_sock_tcp, &s_addr); + + sock[0].fd = s_sock_udp; + sock[0].events = ZSOCK_POLLIN; + + ret = net_socket_service_register(udp_service, sock, ARRAY_SIZE(sock), NULL); + zassert_equal(ret, 0, "Cannot register udp service (%d)", ret); + + sock[0].fd = s_sock_tcp; + sock[0].events = ZSOCK_POLLIN; + + ret = net_socket_service_register(tcp_service_small, sock, ARRAY_SIZE(sock) + 1, NULL); + zassert_equal(ret, -ENOMEM, "Could register tcp service (%d)", ret); + + ret = net_socket_service_register(tcp_service, sock, ARRAY_SIZE(sock), NULL); + zassert_equal(ret, 0, "Cannot register tcp service (%d)", ret); + + ret = bind(s_sock_udp, (struct sockaddr *)&s_addr, sizeof(s_addr)); + zassert_equal(ret, 0, "bind failed"); + + ret = connect(c_sock_udp, (struct sockaddr *)&s_addr, sizeof(s_addr)); + zassert_equal(ret, 0, "connect failed"); + + /* Send pkt for s_sock_udp and poll with timeout of 10 */ + len = send(c_sock_udp, BUF_AND_SIZE(TEST_STR_SMALL), 0); + zassert_equal(len, STRLEN(TEST_STR_SMALL), "invalid send len"); + + if (k_sem_take(&wait_data, K_MSEC(WAIT_TIME))) { + zassert_true(0, "Timeout while waiting callback"); + } + + /* Recv pkt from s_sock_udp and ensure no poll events happen */ + len = recv(s_sock_udp, BUF_AND_SIZE(buf), 0); + zassert_equal(len, STRLEN(TEST_STR_SMALL), "invalid recv len"); + + ret = bind(s_sock_tcp, (struct sockaddr *)&s_addr, sizeof(s_addr)); + zassert_equal(ret, 0, "bind failed (%d)", -errno); + ret = listen(s_sock_tcp, 0); + zassert_equal(ret, 0, ""); + + ret = connect(c_sock_tcp, (const struct sockaddr *)&s_addr, + sizeof(s_addr)); + zassert_equal(ret, 0, ""); + + /* Let the network stack run */ + k_msleep(10); + + len = send(c_sock_tcp, BUF_AND_SIZE(TEST_STR_SMALL), 0); + zassert_equal(len, STRLEN(TEST_STR_SMALL), "invalid send len"); + + if (k_sem_take(&wait_data_tcp, K_MSEC(WAIT_TIME))) { + zassert_true(0, "Timeout while waiting callback"); + } + + new_sock = accept(s_sock_tcp, NULL, NULL); + zassert_true(new_sock >= 0, ""); + + sock[1].fd = new_sock; + sock[1].events = ZSOCK_POLLIN; + + ret = net_socket_service_register(tcp_service, sock, ARRAY_SIZE(sock), NULL); + zassert_equal(ret, 0, "Cannot register tcp service (%d)", ret); + + if (k_sem_take(&wait_data_tcp, K_MSEC(WAIT_TIME))) { + zassert_true(0, "Timeout while waiting callback"); + } + + len = recv(new_sock, BUF_AND_SIZE(buf), 0); + zassert_equal(len, STRLEN(TEST_STR_SMALL), "invalid recv len"); + + ret = net_socket_service_unregister(tcp_service); + zassert_equal(ret, 0, "Cannot unregister tcp service (%d)", ret); + + ret = net_socket_service_unregister(udp_service); + zassert_equal(ret, 0, "Cannot unregister tcp service (%d)", ret); + + ret = net_socket_service_unregister(tcp_service_small); + zassert_equal(ret, 0, "Cannot unregister tcp service (%d)", ret); + + ret = close(new_sock); + zassert_equal(ret, 0, "close failed"); + + ret = close(c_sock_tcp); + zassert_equal(ret, 0, "close failed"); + + ret = close(s_sock_tcp); + zassert_equal(ret, 0, "close failed"); + + ret = close(c_sock_udp); + zassert_equal(ret, 0, "close failed"); + + ret = close(s_sock_udp); + zassert_equal(ret, 0, "close failed"); + + /* Let the stack close the TCP sockets properly */ + k_msleep(100); +} + +ZTEST(net_socket_service, test_service_sync) +{ + run_test_service(&udp_service_sync, &tcp_service_small_sync, + &tcp_service_sync); +} + +ZTEST(net_socket_service, test_service_async) +{ + run_test_service(&udp_service_async, &tcp_service_small_async, + &tcp_service_async); +} + +ZTEST_SUITE(net_socket_service, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/net/socket/service/testcase.yaml b/tests/net/socket/service/testcase.yaml new file mode 100644 index 000000000000..74046d08274c --- /dev/null +++ b/tests/net/socket/service/testcase.yaml @@ -0,0 +1,17 @@ +common: + depends_on: netif + # FIXME: This test fails very frequently on mps2_an385 due to the system + # timer stability issues, so keep it disabled until the root cause + # is fixed (GitHub issue zephyrproject-rtos/zephyr#48608). + # eventfd API does not work with native_posix so exclude it here + platform_exclude: + - mps2_an385 + - native_posix + - native_posix_64 +tests: + net.socket.service: + min_ram: 21 + tags: + - net + - socket + - poll diff --git a/tests/net/socket/socket_helpers.h b/tests/net/socket/socket_helpers.h index 1d4d33f4bca4..9cecccc20b66 100644 --- a/tests/net/socket/socket_helpers.h +++ b/tests/net/socket/socket_helpers.h @@ -6,7 +6,13 @@ #include +#if defined(CONFIG_POSIX_API) +#include +#include +#include +#else #include +#endif #define clear_buf(buf) memset(buf, 0, sizeof(buf))