connectivity/networking/api/net_pkt.html · connectivity/networking/api/net_pkt.rst 공식 원문 ↗

Zephyr 3.7.0 · Connectivity · Networking API

Packet Management

Packet pool과 MTU 계산부터 cursor mode, 복사 없는 header access까지 packet 수명주기를 따라갑니다.

Official pathconnectivity/networking/api/net_pkt.html
Source fileconnectivity/networking/api/net_pkt.rst
Source versionZephyr 3.7.0
TranslationFull · reviewed

본문 기준: Zephyr 3.7.0 LTS3 · 이후 유지보수 패치는 별도입니다.

Part 1

요약·해설

TX/RX packet의 memory 관리, MTU 기반 buffer 할당, reference count, write·overwrite mode와 contiguous data access를 설명합니다.

Memory

Pool · MTU · refcount

Cursor

Write · Overwrite

Data

Contiguous access

Part 2

접을 수 있는 영어 원문 전체

영어 원문 전체 펼치기
원문 SHA-256 B1B1311237D47FEA69574A59C54D3CF9B091B3D43F2892D87CB0458E4CFEC669
.. _net_pkt_interface:

Packet Management
#################

.. contents::
    :local:
    :depth: 2

Overview
********

Network packets are the main data the networking stack manipulates.
Such data is represented through the net_pkt structure which provides
a means to hold the packet, write and read it, as well as necessary
metadata for the core to hold important information. Such an object is
called net_pkt in this document.

The data structure and the whole API around it are defined in
:zephyr_file:`include/zephyr/net/net_pkt.h`.

Architectural notes
===================

There are two network packets flows within the stack, **TX** for the
transmission path, and **RX** for the reception one. In both paths,
each net_pkt is written and read from the beginning to the end, or
more specifically from the headers to the payload.


Memory management
*****************

Allocation
==========

All net_pkt objects come from a pre-defined pool of struct net_pkt.
Such pool is defined via

.. code-block:: c

    NET_PKT_SLAB_DEFINE(name, count)

Note, however, one will rarely have to use it, as the core provides
already two pools, one for the TX path and one for the RX path.

Allocating a raw net_pkt can be done through:

.. code-block:: c

    pkt = net_pkt_alloc(timeout);

However, by its nature, a raw net_pkt is useless without a buffer and
needs various metadata information to become relevant as well.  It
requires at least to get the network interface it is meant to be sent
through or through which it was received. As this is a very common
operation, a helper exist:

.. code-block:: c

    pkt = net_pkt_alloc_on_iface(iface, timeout);

A more complete allocator exists, where both the net_pkt and its buffer
can be allocated at once:

.. code-block:: c

    pkt = net_pkt_alloc_with_buffer(iface, size, family, proto, timeout);

See below how the buffer is allocated.


Buffer allocation
=================

The net_pkt object does not define its own buffer, but instead uses an
existing object for this: :c:struct:`net_buf`. (See
:ref:`net_buf_interface` for more information). However, it mostly
hides the usage of such a buffer because net_pkt brings network
awareness to buffer allocation and, as we will see later, its
operation too.

To allocate a buffer, a net_pkt needs to have at least its network
interface set. This works if the family of the packet is unknown at
the time of buffer allocation. Then one could do:

.. code-block:: c

    net_pkt_alloc_buffer(pkt, size, proto, timeout);

Where proto could be 0 if unknown (there is no IPPROTO_UNSPEC).

As seen previously, the net_pkt and its buffer can be allocated at
once via :c:func:`net_pkt_alloc_with_buffer`. It is actually the most
widely used allocator.

The network interface, the family, and the protocol of the packet are
used by the buffer allocation to determine if the requested size can
be allocated.  Indeed, the allocator will use the network interface to
know the MTU and then the family and protocol for the headers space
(if only these 2 are specified).  If the whole fits within the MTU,
the allocated space will be of the requested size plus, eventually,
the headers space. If there is insufficient MTU space, the requested
size will be shrunk so the possible headers space and new size will
fit within the MTU.

For instance, on an Ethernet network interface, with an MTU of 1500
bytes:

.. code-block:: c

    pkt = net_pkt_alloc_with_buffer(iface, 800, AF_INET4, IPPROTO_UDP, K_FOREVER);

will successfully allocate 800 + 20 + 8 bytes of buffer for the new
net_pkt where:

.. code-block:: c

    pkt = net_pkt_alloc_with_buffer(iface, 1600, AF_INET4, IPPROTO_UDP, K_FOREVER);

will successfully allocate 1500 bytes, and where 20 + 8 bytes (IPv4 +
UDP headers) will not be used for the payload.

On the receiving side, when the family and protocol are not known:

.. code-block:: c

    pkt = net_pkt_rx_alloc_with_buffer(iface, 800, AF_UNSPEC, 0, K_FOREVER);

will allocate 800 bytes and no extra header space.
But a:

.. code-block:: c

    pkt = net_pkt_rx_alloc_with_buffer(iface, 1600, AF_UNSPEC, 0, K_FOREVER);

will allocate 1514 bytes, the MTU + Ethernet header space.

One can increase the amount of buffer space allocated by calling
:c:func:`net_pkt_alloc_buffer`, as it will take into account the
existing buffer. It will also account for the header space if
net_pkt's family is a valid one, as well as the proto parameter. In
that case, the newly allocated buffer space will be appended to the
existing one, and not inserted in the front. Note however such a use
case is rather limited.  Usually, one should know from the start how
much size should be requested.


Deallocation
============

Each net_pkt is reference counted. At allocation, the reference is set
to 1.  The reference count can be incremented with
:c:func:`net_pkt_ref()` or decremented with
:c:func:`net_pkt_unref()`. When the count drops to zero the buffer is
also un-referenced and net_pkt is automatically placed back into the
free net_pkt_slabs

If net_pkt's buffer is needed even after net_pkt deallocation, one
will need to reference once more all the chain of net_buf before
calling last net_pkt_unref. See :ref:`net_buf_interface` for more
information.


Operations
**********

There are two ways to access the net_pkt buffer, explained in the
following sections: basic read/write access and data access, the
latter being the preferred way.

Read and Write access
=====================

As said earlier, though net_pkt uses net_buf for its buffer, it
provides its own API to access it. Indeed, a network packet might be
scattered over a chain of net_buf objects, the functions provided by
net_buf are then limited for such case.  Instead, net_pkt provides
functions which hide all the complexity of potential non-contiguous
access.

Data movement into the buffer is made through a cursor maintained
within each net_pkt.  All read/write operations affect this
cursor. Note as well that read or write functions are strict on their
length parameters: if it cannot r/w the given length it will
fail. Length is not interpreted as an upper limit, it is instead the
exact amount of data that must be read or written.

As there are two paths, TX and RX, there are two access modes: write
and overwrite.  This might sound a bit unusual, but is in fact simple
and provides flexibility.

In write mode, whatever is written in the buffer affects the length of
actual data present in the buffer. Buffer length should not be
confused with the buffer size which is a limit any mode cannot pass.
In overwrite mode then, whatever is written must happen on valid data,
and will not affect the buffer length. By default, a newly allocated
net_pkt is on write mode, and its cursor points to the beginning of
its buffer.

Let's see now, step by step, the functions and how they behave
depending on the mode.

When freshly allocated with a buffer of 500 bytes, a net_pkt has 0
length, which means no valid data is in its buffer. One could verify
this by:

.. code-block:: c

    len = net_pkt_get_len(pkt);

Now, let's write 8 bytes:

.. code-block:: c

    net_pkt_write(pkt, data, 8);

The buffer length is now 8 bytes.
There are various helpers to write a byte, or big endian uint16_t, uint32_t.

.. code-block:: c

    net_pkt_write_u8(pkt, &foo);
    net_pkt_write_be16(pkt, &ba);
    net_pkt_write_be32(pkt, &bar);

Logically, net_pkt's length is now 15. But if we try to read at this
point, it will fail because there is nothing to read at the cursor
where we are at in the net_pkt. It is possible, while in write mode,
to read what has been already written by resetting the cursor of the
net_pkt. For instance:

.. code-block:: c

    net_pkt_cursor_init(pkt);
    net_pkt_read(pkt, data, 15);

This will reset the cursor of the pkt to the beginning of the buffer
and then let you read the actual 15 bytes present. The cursor is then
again pointing at the end of the buffer.

To set a large area with the same byte, a memset function is provided:

.. code-block:: c

    net_pkt_memset(pkt, 0, 5);

Our net_pkt has now a length of 20 bytes.

Switching between modes can be achieved via
:c:func:`net_pkt_set_overwrite` function. It is possible to switch
mode back and forth at any time.  The net_pkt will be set to overwrite
and its cursor reset:

.. code-block:: c

    net_pkt_set_overwrite(pkt, true);
    net_pkt_cursor_init(pkt);

Now the same operators can be used, but it will be limited to the
existing data in the buffer, i.e. 20 bytes.

If it is necessary to know how much space is available in the net_pkt
call:

.. code-block:: c

    net_pkt_available_buffer(pkt);

Or, if headers space needs to be accounted for, call:

.. code-block:: c

    net_pkt_available_payload_buffer(pkt, proto);

If you want to place the cursor at a known position use the function
:c:func:`net_pkt_skip`.  For example, to go after the IP header, use:

.. code-block:: c

    net_pkt_cursor_init(pkt);
    net_pkt_skip(pkt, net_pkt_ip_header_len(pkt));


Data access
===========

Though the API shown previously is rather simple, it involves always
copying things to and from the net_pkt buffer. In many occasions, it
is more relevant to access the information stored in the buffer
contiguously, especially with network packets which embed headers.

These headers are, most of the time, a known fixed set of bytes. It is
then more natural to have a structure representing a certain type of
header.  In addition to this, if it is known the header size appears
in a contiguous area of the buffer, it will be way more efficient to
cast the actual position in the buffer to the type of header. Either
for reading or writing the fields of such header, accessing it
directly will save memory.

Net pkt comes with a dedicated API for this, built on top of the
previously described API. It is able to handle both contiguous and
non-contiguous access transparently.

There are two macros used to define a data access descriptor:
:c:macro:`NET_PKT_DATA_ACCESS_DEFINE` when it is not possible to
tell if the data will be in a contiguous area, and
:c:macro:`NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE` when
it is guaranteed the data is in a contiguous area.

Let's take the example of IP and UDP. Both IPv4 and IPv6 headers are
always found at the beginning of the packet and are small enough to
fit in a net_buf of 128 bytes (for instance, though 64 bytes could be
chosen).

.. code-block:: c

    NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE(ipv4_access, struct net_ipv4_hdr);
    struct net_ipv4_hdr *ipv4_hdr;

    ipv4_hdr = (struct net_ipv4_hdr *)net_pkt_get_data(pkt, &ipv4_access);

It would be the same for struct net_ipv4_hdr. For a UDP header it
is likely not to be in a contiguous area in IPv6
for instance so:

.. code-block:: c

    NET_PKT_DATA_ACCESS_DEFINE(udp_access, struct net_udp_hdr);
    struct net_udp_hdr *udp_hdr;

    udp_hdr = (struct net_udp_hdr *)net_pkt_get_data(pkt, &udp_access);

At this point, the cursor of the net_pkt points at the beginning of
the requested data. On the RX path, these headers will be read but not
modified so to proceed further the cursor needs to advance past the
data. There is a function dedicated for this:

.. code-block:: c

    net_pkt_acknowledge_data(pkt, &ipv4_access);

On the TX path, however, the header fields have been modified. In such
a case:

.. code-block:: c

    net_pkt_set_data(pkt, &ipv4_access);

If the data are in a contiguous area, it will advance the cursor
relevantly. If not, it will write the data and the cursor will be
updated. Note that :c:func:`net_pkt_set_data` could be used in the RX
path as well, but it is slightly faster to use
:c:func:`net_pkt_acknowledge_data` as this one does not care about
contiguity at all, it just advances the cursor via
:c:func:`net_pkt_skip` directly.


API Reference
*************

.. doxygengroup:: net_pkt

Part 3

한국어 전문 번역

개요

Network packet은 networking stack이 다루는 핵심 data입니다. Zephyr는 packet, packet의 읽기·쓰기 상태, core가 필요로 하는 metadata를 struct net_pkt로 나타냅니다. 이 문서에서는 이 object를 net_pkt라고 부릅니다.

Data structure와 관련 API 전체는 include/zephyr/net/net_pkt.h에 정의되어 있습니다.

Architecture 참고 사항

Stack 안에는 두 packet flow가 있습니다. TX는 전송 경로이고 RX는 수신 경로입니다. 두 경로 모두 각 net_pkt를 처음부터 끝까지, 더 구체적으로는 header에서 payload 방향으로 쓰고 읽습니다.

Memory 관리

할당

모든 net_pkt object는 미리 정의한 struct net_pkt pool에서 나옵니다. Pool은 다음 macro로 정의합니다.

NET_PKT_SLAB_DEFINE(name, count)

하지만 core가 TX용과 RX용 pool을 이미 하나씩 제공하므로 application이 이 macro를 직접 사용할 일은 드뭅니다.

Buffer가 붙지 않은 raw net_pkt는 다음처럼 할당합니다.

pkt = net_pkt_alloc(timeout);

Raw net_pkt는 buffer가 없으면 쓸모가 없고, 실질적으로 사용하려면 여러 metadata도 필요합니다. 적어도 packet을 내보낼 network interface 또는 packet을 받은 interface를 지정해야 합니다. 매우 흔한 작업이므로 다음 helper가 있습니다.

pkt = net_pkt_alloc_on_iface(iface, timeout);

net_pkt와 buffer를 한 번에 할당하는 더 완전한 allocator도 있습니다.

pkt = net_pkt_alloc_with_buffer(iface, size, family, proto, timeout);

Buffer가 실제로 어떻게 할당되는지는 다음 절에서 설명합니다.

Buffer 할당

net_pkt object는 자체 buffer를 정의하지 않고 기존 struct net_buf object를 사용합니다. 자세한 내용은 net_buf 문서를 참고하십시오. 다만 net_pkt가 buffer 할당과 이후 operation에 network 의미를 더하기 때문에, 일반적으로 underlying net_buf 사용은 API 뒤에 감춰집니다.

Buffer를 할당하려면 net_pkt에 최소한 network interface가 설정돼 있어야 합니다. Buffer 할당 시 packet family를 아직 몰라도 됩니다. 이때는 다음처럼 호출합니다.

net_pkt_alloc_buffer(pkt, size, proto, timeout);

proto를 모르면 0을 사용할 수 있습니다. IPPROTO_UNSPEC은 없습니다.

앞에서 본 net_pkt_alloc_with_buffer()는 packet과 buffer를 함께 할당하며, 실제로 가장 널리 쓰이는 allocator입니다.

Buffer allocator는 network interface, packet family, protocol을 사용해 요청한 크기를 할당할 수 있는지 결정합니다. Interface에서 MTU를 얻고, family와 protocol이 지정됐다면 header 공간도 계산합니다. 전체가 MTU 안에 들어가면 요청 크기에 필요한 header 공간을 더해 할당합니다. MTU 공간이 부족하면 header와 새 payload 크기가 MTU 안에 맞도록 요청 크기를 줄입니다.

예를 들어 MTU가 1500 byte인 Ethernet interface에서 다음 호출은

pkt = net_pkt_alloc_with_buffer(iface, 800, AF_INET4, IPPROTO_UDP, K_FOREVER);

새 packet에 800 + 20 + 8 byte의 buffer를 성공적으로 할당합니다. 20 byte는 IPv4 header, 8 byte는 UDP header 공간입니다. 반면 다음 호출은

pkt = net_pkt_alloc_with_buffer(iface, 1600, AF_INET4, IPPROTO_UDP, K_FOREVER);

1500 byte를 할당하며, 그중 20 + 8 byte는 IPv4·UDP header에 쓰이므로 payload에는 사용할 수 없습니다.

수신 측에서 family와 protocol을 모를 때 다음 호출은

pkt = net_pkt_rx_alloc_with_buffer(iface, 800, AF_UNSPEC, 0, K_FOREVER);

800 byte만 할당하고 별도 header 공간은 더하지 않습니다. 그러나 다음처럼 1600 byte를 요청하면

pkt = net_pkt_rx_alloc_with_buffer(iface, 1600, AF_UNSPEC, 0, K_FOREVER);

MTU 1500 byte에 Ethernet header 공간 14 byte를 더한 1514 byte를 할당합니다.

net_pkt_alloc_buffer()를 다시 호출하면 기존 buffer를 고려해 할당 공간을 늘릴 수 있습니다. net_pkt의 family가 유효하면 proto와 함께 header 공간도 반영합니다. 새 buffer 공간은 기존 chain 앞이 아니라 뒤에 append됩니다. 다만 이 사용 사례는 제한적이며, 보통 처음부터 필요한 크기를 알고 요청해야 합니다.

해제

각 net_pkt는 reference count를 가집니다. 할당할 때 count는 1이고 net_pkt_ref()로 늘리며 net_pkt_unref()로 줄입니다. Count가 0이 되면 buffer도 un-reference되고 net_pkt는 자동으로 free net_pkt_slab에 돌아갑니다.

net_pkt를 해제한 뒤에도 buffer가 필요하다면 마지막 net_pkt_unref()를 호출하기 전에 chain의 모든 net_buf를 한 번 더 reference해야 합니다. 자세한 내용은 net_buf 문서를 참고하십시오.

Operation

net_pkt buffer에 접근하는 방법은 두 가지입니다. 하나는 기본 read/write access이고 다른 하나는 data access이며, 후자가 권장 방식입니다.

Read와 Write access

net_pkt는 buffer로 net_buf를 사용하지만 자체 access API를 제공합니다. Network packet은 여러 net_buf object의 chain에 흩어질 수 있어 net_buf 함수만으로는 이런 경우를 다루기 어렵습니다. net_pkt 함수는 잠재적으로 non-contiguous한 접근의 복잡성을 감춥니다.

Buffer 안의 data 이동은 각 net_pkt가 유지하는 cursor를 통해 이뤄지고 모든 read/write operation이 cursor에 영향을 줍니다. Read와 write 함수의 length는 엄격한 값입니다. 주어진 길이 전체를 읽거나 쓸 수 없으면 실패하며, length는 상한이 아니라 반드시 처리해야 하는 정확한 byte 수입니다.

TX와 RX 두 경로에 대응해 write와 overwrite라는 두 access mode가 있습니다. 이름은 다소 낯설지만 동작은 단순하고 유연합니다.

Write mode에서 buffer에 쓰면 실제 data length가 늘어납니다. Data length는 어떤 mode에서도 넘을 수 없는 한계인 buffer size와 다릅니다. Overwrite mode에서는 이미 유효한 data 영역에만 쓸 수 있고 buffer length는 바뀌지 않습니다. 새로 할당한 net_pkt의 기본값은 write mode이며 cursor는 buffer 처음을 가리킵니다.

이제 mode에 따라 각 함수가 어떻게 동작하는지 단계별로 살펴봅니다.

500 byte buffer와 함께 새로 할당한 net_pkt의 data length는 0이므로 아직 유효한 data가 없습니다. 다음 호출로 확인할 수 있습니다.

len = net_pkt_get_len(pkt);

이제 8 byte를 씁니다.

net_pkt_write(pkt, data, 8);

Buffer data length는 8 byte가 됩니다. 한 byte나 big-endian uint16_t·uint32_t를 쓰는 helper도 있습니다.

net_pkt_write_u8(pkt, &foo);
net_pkt_write_be16(pkt, &ba);
net_pkt_write_be32(pkt, &bar);

net_pkt의 data length는 논리적으로 15가 됩니다. 그러나 cursor가 이미 쓰기 끝 위치를 가리키므로 지금 바로 읽으려 하면 읽을 data가 없어 실패합니다. Write mode에서도 cursor를 초기화하면 이미 쓴 내용을 읽을 수 있습니다.

net_pkt_cursor_init(pkt);
net_pkt_read(pkt, data, 15);

Cursor를 buffer 처음으로 되돌린 뒤 실제 15 byte를 읽습니다. 읽기가 끝나면 cursor는 다시 data 끝을 가리킵니다.

넓은 영역을 같은 byte로 채우는 memset 함수도 제공됩니다.

net_pkt_memset(pkt, 0, 5);

net_pkt의 data length는 이제 20 byte입니다.

net_pkt_set_overwrite()로 mode를 언제든 오갈 수 있습니다. 다음은 overwrite mode로 설정하고 cursor를 초기화합니다.

net_pkt_set_overwrite(pkt, true);
net_pkt_cursor_init(pkt);

이제 같은 operator를 사용할 수 있지만 기존의 유효 data, 즉 20 byte 안에서만 동작합니다.

net_pkt에 남은 buffer 공간을 확인하려면 다음을 호출합니다.

net_pkt_available_buffer(pkt);

Header 공간까지 고려한 payload 가용 공간이 필요하면 다음을 호출합니다.

net_pkt_available_payload_buffer(pkt, proto);

net_pkt_skip()을 사용하면 cursor를 알려진 위치로 옮길 수 있습니다. 예를 들어 IP header 뒤로 이동하려면 다음처럼 합니다.

net_pkt_cursor_init(pkt);
net_pkt_skip(pkt, net_pkt_ip_header_len(pkt));

Data access

앞의 API는 단순하지만 data를 packet buffer 안팎으로 항상 복사합니다. 특히 고정 형식 header를 포함한 network packet에서는 buffer에 저장된 정보를 contiguous하게 직접 접근하는 편이 더 적합한 경우가 많습니다.

대부분의 header는 길이가 정해진 byte 집합이므로 header type을 나타내는 structure를 사용하는 것이 자연스럽습니다. Header 전체가 buffer의 contiguous 영역에 있다는 것을 안다면 현재 buffer 위치를 header type으로 cast하는 것이 훨씬 효율적입니다. 읽거나 쓰려는 header field에 직접 접근하면 별도 복사 memory도 절약합니다.

net_pkt는 앞서 설명한 API 위에 이 용도의 전용 API를 제공합니다. 이 API는 contiguous 접근과 non-contiguous 접근을 투명하게 처리합니다.

Data access descriptor를 정의하는 macro는 두 가지입니다. Data가 contiguous 영역에 있을지 알 수 없으면 NET_PKT_DATA_ACCESS_DEFINE, 반드시 contiguous하다고 보장되면 NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE을 사용합니다.

IP와 UDP를 예로 들겠습니다. IPv4·IPv6 header는 항상 packet 처음에 있고, 예를 들어 128 byte짜리 net_buf 안에 들어갈 만큼 작습니다. 64 byte를 선택할 수도 있습니다.

NET_PKT_DATA_ACCESS_CONTIGUOUS_DEFINE(ipv4_access, struct net_ipv4_hdr);
struct net_ipv4_hdr *ipv4_hdr;

ipv4_hdr = (struct net_ipv4_hdr *)net_pkt_get_data(pkt, &ipv4_access);

원문은 이어서 struct net_ipv4_hdr에도 같다고 설명합니다. UDP header는 예를 들어 IPv6 packet에서 contiguous 영역에 있지 않을 가능성이 있으므로 다음처럼 일반 data access descriptor를 사용합니다.

NET_PKT_DATA_ACCESS_DEFINE(udp_access, struct net_udp_hdr);
struct net_udp_hdr *udp_hdr;

udp_hdr = (struct net_udp_hdr *)net_pkt_get_data(pkt, &udp_access);

net_pkt cursor는 요청한 data의 시작을 가리킵니다. RX 경로에서는 이 header를 읽기만 하고 수정하지 않으므로, 다음 data로 진행하려면 cursor를 header 뒤로 전진시켜야 합니다.

net_pkt_acknowledge_data(pkt, &ipv4_access);

TX 경로에서는 header field를 수정했으므로 다음 호출로 변경 내용을 반영합니다.

net_pkt_set_data(pkt, &ipv4_access);

Data가 contiguous 영역에 있으면 cursor를 해당 길이만큼 전진시킵니다. 그렇지 않으면 data를 buffer에 쓰고 cursor를 갱신합니다. net_pkt_set_data()를 RX 경로에서도 사용할 수 있지만 net_pkt_acknowledge_data()가 약간 더 빠릅니다. 후자는 contiguity를 검사하지 않고 net_pkt_skip()으로 cursor만 직접 전진시키기 때문입니다.

API Reference

Doxygen group net_pkt가 전체 API reference를 제공합니다.

Source

출처

영어 원문에는 문단, 문서 지시문, 표, 코드, 코드 심벌, 경로를 그대로 보존했습니다.