Skip to content

Architecture

Daniele Lacamera edited this page Dec 3, 2015 · 21 revisions

In this section the architecture of the stack is described, from the core data structures to the functional flow.

Main data types

Protocols

A TCP/IP stack is made of layers, each one represented by a protocol. The main structure representing a protocol in picoTCP is the structure pico_protocol defined in pico_protocols.h.

struct pico_protocol {
    char name[MAX_PROTOCOL_NAME];
    uint32_t hash;
    enum pico_layer layer;
    uint16_t proto_number;
    struct pico_queue *q_in;
    struct pico_queue *q_out;
    struct pico_frame *(*alloc)(struct pico_protocol *self, uint16_t size); /* Frame allocation. */
    int (*push)(struct pico_protocol *self, struct pico_frame *p);    /* Push function, for active outgoing pkts from above */
    int (*process_out)(struct pico_protocol *self, struct pico_frame *p);  /* Send loop. */
    int (*process_in)(struct pico_protocol *self, struct pico_frame *p);  /* Recv loop. */
    uint16_t (*get_mtu)(struct pico_protocol *self);
}

Here is a brief description of the fields:

  • name contains a unique name used to identify the protocol implementation. Values for this fields look like “ipv4” or “tcp”

  • hash is a signature obtained by hashing the name with the pico_hash function. It is used for fast matches in ordered structures and it is generated automatically when the protocol gets registered to the stack. layer is an identifier of the OSI layer where the protocol belongs to. For convention, all the protocols that connect directly to the networking core protocols (IPv4/IPv6), such as ICMP, must be grouped in the PICO_PROTO_TRANSPORT layer.

  • q_in and q_out refer to the queues of frames traversing the protocol in either direction. For a better understanding of the queuing mechanism, refer to the Queues section in this chapter.

  • alloc is a pointer to the function, provided by the protocol, that offers the capability of allocating outgoing frames at this level, which would take into account all the protocols overhead in the stack. This function is only mandatory if the top layers utilize a generic function for the frame allocation in the bottom layers. For example, in order to allocate an outgoing IP frame, the IPv4 protocol implementation associates this pointer to a function called pico_ipv4_alloc, which takes into account the overhead of the IPv4 header and the possible underlying datalink headers.

.

static struct pico_frame *pico_ipv4_alloc(struct pico_protocol *self, uint16_t size)
{
    struct pico_frame *f =  pico_frame_alloc(size + PICO_SIZE_IP4HDR + PICO_SIZE_ETHHDR);
    IGNORE_PARAMETER(self);
    if (!f)
        return NULL;
    f->datalink_hdr = f->buffer;
    f->net_hdr = f->buffer + PICO_SIZE_ETHHDR;
    f->net_len = PICO_SIZE_IP4HDR;
    f->transport_hdr = f->net_hdr + PICO_SIZE_IP4HDR;
    f->transport_len = size;
    f->len =  size + PICO_SIZE_IP4HDR;
    return f;
}
  • The push function pointer is the entry point for the upper layers to communicate that a new frame needs to be enqueued for transmission. The protocol can perform all the needed checks in this function, prepend information in the header, and decide whether or not it is the case to enqueue the packet for later transmission - or have the frame processed inline to its next destination.

  • process_in and process_out are the two entry point from the stack internal scheduler. These functions will be called at regular intervals when the protocol is registered to the stack. The goal of these functions is to get rid of the frames in the queues q_in and q_out respectively, and forward them to their next destinations.

  • The get_mtu hook is not mandatory, and can be used to enforce a maximum packet size for the outgoing packets that cross the layer. If your protocol works with a certain packet or frame size, this field might be useful to propagate this information along the stack.

In order to add a new protocol to the stack, a new instance of the structure pico_protocol needs to be filled in, and then the API function pico_protocol_init can be called. From that moment on, the protocol is part of the stack and can start to interact with the other components.

Device drivers

Similarly, the stack accepts the registration of another type of module, describing a device driver, that can be attached to the stack. The pico_device structure is described in pico_device.h as follows:

struct pico_device {
    char name[MAX_DEVICE_NAME];
    uint32_t hash;
    uint32_t overhead;
    uint32_t mtu;
    struct pico_ethdev *eth; /* Null if non-ethernet */
    struct pico_queue *q_in;
    struct pico_queue *q_out;
    int (*link_state)(struct pico_device *self);
    int (*send)(struct pico_device *self, void *buf, int len); /* Send function. Return 0 if busy */
    int (*poll)(struct pico_device *self, int loop_score);
    void (*destroy)(struct pico_device *self);
    int (*dsr)(struct pico_device *self, int loop_score);
    int __serving_interrupt;
    /* used to signal the upper layer the number of events arrived since the last processing */
    volatile int eventCnt;
  #ifdef PICO_SUPPORT_IPV6
    struct pico_nd_hostvars hostvars;
  #endif
};

Some of the fields here are in common with the protocol structure, the name, hash, and the two queues have exactly the same meaning as for the pico_protocol descriptor. The relationship with the internal scheduler to this type of module though, is a bit different. There are two different possible strategies to implement a device driver: a poll-based implementation and a IRQ-based mechanism, slightly more efficient but also more difficult to implement.

In both cases, the stack uses the send function pointer of the driver whenever a frame has to leave the device using a link associated to the device. The send function associated by the driver during the initialization phase must implement all the mechanisms to actually put the frame out on the desired physical medium.

The polling strategy of the device driver requires the driver developer to implement the poll function. The poll function will be called by the stack at regular interval, to give the driver the possibility to inject newly received packet into the stack. This means that the smallest set of operations required to implement a device driver consists of the send and poll operations.

An asynchronous driver, on the other hand, might not implement the poll function, and instead set the __serving_interrupt flag from the interrupt handler every time a new frame is available for possible reception. This will ensure that the dsr function is called at least once to process the incoming frame when the interrupt returns. The purpose of the dsr is then the same as the poll, except that the former will only be invoked when an external, asynchronous event such as an interrupt handler has set the __serving_interrupt flag before.

The eth field is very important for two reasons: in the first place, it provides a datalink address associated to the device, but it can also assume a NULL value to indicate that the specific device associated to the driver is exchanging pure IP packets instead of full frames, and if so the stack will react accordingly. This is pretty useful in point-to-point datalink drivers, where the two endpoints don’t need any layer 2 addressing. Two examples of this kind of devices can be found in the tun driver and the PPPoS (ppp over serial line) implementation.

By implementing the link_state function, the driver can provide information regarding the state of the physical channel before any use of the device, or during the device activity. This function is not mandatory to implement in a driver (the stack will assume that the device is permanently connected if the function is not defined), but it might help if the physical device can sense the activity of the link and react accordingly. Furthermore, a global function pico_dev_link_state is exported by the stack to the public API, making it possible for the application developer to know about the physical state of the device.

The destroy function is not mandatory either: it is in fact used to free possible additional entries related to the device when the device is destroyed.

In order to create a new device instance, the driver needs to export its own init function, which must perform the specific initialization needed before calling the generic pico_device_init from the stack API. From the moment the device is created, it becomes part of the stack itself.

Clone this wiki locally