When modeling computer systems it is often necessary to send chunks of data around. This is especially common when modeling network devices where one device model may pick up a network packet from target memory, attach a header or a CRC and pass it on to another device that sends it out on the simulated network. When receiving a network packet, the device will get a network packet, examine it, optionally strip parts from it and pass it on to other devices or the target memory.
Models of more advanced devices will need to do additional processing of the packets, adding, removing or updating headers on the way. And a more abstract model of a network node may want to model the whole network stack.
The frags_t
data type is designed to facilitate the efficient implementation of functions passing these chunks around. It represents the data as a list of fragments, where the individual fragments references pieces of the actual data. The data is not stored in the frags_t
structure, nor is it owned by it. Instead, it can be seen as a pointer that points to multiple memory locations in sequence.
The typical use is a function that is passed some block of data. Without frags_t
, it might have taken a plain pointer and a size:
void old_receive_data(const uint8 *data, size_t data_len);
But with frags_t
it would instead take a pointer to a constant frags_t
:
void receive_data(const frags_t *data);
The usage is very similar. Instead of passing the data as a single, contiguous block of memory, it is passed as a list of references to fragments of the data. The only important difference is that the receive_data()
function will need another way to access the data itself.
To complete the introduction, this is how a frags_t
is created before calling this function:
frags_t data;
frags_init_add(&data, header, header_len);
frags_add(&data, payload, payload_len);
receive_data(&data);
A function that receives a const frags_t *
argument can use it to read the data it references in two ways.
The first, and most common, way is to extract it to another buffer, using the frags_extract()
function, or using the frags_extract_slice()
function to only extract parts of it.
void receive_data_extract(const frags_t *data)
{
uint8 buf[frags_len(data)];
frags_extract(data, buf);
// buf now contains all the data
// ...
}
Or
void receive_data_slice(const frags_t *data)
{
uint8 buf[HEADER_LEN];
frags_extract_slice(data, buf, 0, HEADER_LEN);
// buf now contains the header
// ...
}
To avoid copies, it is sometimes useful to use the iterator functions. This is an example of a simple checksum function:
int checksum(const frags_t *data)
{
int chksum = 0;
for (frags_it_t it = frags_it(data, 0, frags_len(data));
!frags_it_end(it);
it = frags_it_next(it))
chksum = partial_checksum(chksum,
frags_it_data(it),
frags_it_len(it));
return chksum;
}
The iterator will provide the pointer to the data in one fragment at a time. These iterator functions are usually not very useful to do common things like read the header of a network packet, since there is no guarantee that any fragment contains the entire header.
To avoid the cost of heap allocation, the preferred way to allocate data to be referenced by a fragment list, and the fragment list itself, is on the stack. Allocating on the stack is extremely cheap, and with variable-sized arrays in C99 (and DML), it is also very simple to use. A typical network device that reads a packet using DMA and attaches a CRC before sending it to the link could look like this:
void send_packet(nic_t *dev)
{
dma_desc_t desc = get_next_dma_desc(dev);
uint8 data[desc.len];
dma_read(dev, desc.addr, desc.len, data);
uint8 crc[4];
calculate_crc(data, crc);
frags_t packet;
frags_init_add(&packet, data, desc.len);
frags_add(&packet, crc, 4);
send_to_link(dev, &packet);
}
One advantage of stack allocation is that there is no need for any destructors; the memory is automatically released when the stack frame is removed.
This works since the frags_t
type has no external allocation. Adding fragments will not cause any dynamic allocations to be made. This also means that the size of the fragment list fixed, so there is a limit to the number of fragments that can be added. The size of the list is eight, which should be enough for most cases, while still being manageable.
Stack allocation also means that there is never any doubt about the ownership of the data. The pointers to the data can not be taken over by anyone else, so the ownership remains with the function that allocates it.
The references to the data in the fragment list is read-only. It is not possible to use a frags_t
reference to modify any data that it points to. There could be other, writeable, references to the same data, such as the local variables data
and crc
in the example above, but when those are not available to a function it has no way of modifying the data.
Since ownership of a fragment list, or of any of its fragments, can not be passed in a function call, there is no way to simply store a fragment list for later use. Instead, the data must be copied if it is going to be needed later.
A network link model that receives a network packet in a frags_t
will typically need to hold on to the data for a while before delivering it to all the recipients. This means that it should extract the data into a buffer that it allocates on the heap. And when it sends the packet to one of the recipients, it can simply create a frags_t
that references the heap-allocated data and pass that pointer to the receiving device.
Here is some pseudo-code for a link model:
void send_a_packet(link_t *link, const frags_t *packet)
{
link->packet_buffer = MM_MALLOC(frags_len(packet), uint8);
link->packet_buffer_len = frags_len(packet);
frags_extract(packet, link->packet_buffer);
// ... defer delivery to a later time ...
}
void deliver_a_packet(link_t *link)
{
frags_t packet;
frags_init_add(&packet, link->packet_buffer,
link->packet_buffer_len);
for (link_dev_t *dev = link->recipients; dev;
dev = dev->next)
deliver_to_dev(link, dev, &packet);
MM_FREE(link->packet_buffer);
link->packet_buffer = NULL;
}
As a convenience, there is a function frags_extract_alloc()
that does the allocation and extracts to the allocated buffer, so the send function can be written like this instead:
void send_alloc(link_t *link, const frags_t *packet) {
link->packet_buffer = frags_extract_alloc(packet);
// ... defer delivery to a later time ...
}
The memory management of the packet buffer in the above code is rather straightforward, but in other cases may be more complex and require reference counting, etc. The frags library does not attempt to solve any such problem; it is only intended to be used for passing data in function calls.
Since the fragment list and the data it points to are only valid as long as the stack frame they live in is live, it is almost never possible to pass references to them between threads. It is possible to do it and block until the other thread is finished using it before returning, but there are very few occasions where this makes sense. Simply copying the data, as described in the previous section, is usually the best solution.
This is a summary of the rules and conventions that should be adhered to when using this library. Any exception to these rules should be clearly marked in the code.
frags_t
pointer passed to a function is read-only. This means that you should always declare them as const frags_t *
in function prototypes. frags_t
are read-only. They are declared as const uint8 *
, and can not be used to modify the data. frags_t
pointer passed to a function can not be stored and reused after the function returns. Neither can a copy of the frags_t
structure be stored and reused. There are a few common use cases that often occur, and this section outlines some of the more important ones, showing how to best use frags_t
to handle them.
Call a function that expects a frags_t
pointer, when there is only a data pointer available.
This is the simplest case. Just create a frags_t
containing a single fragment referencing the data.
frags_t frags;
frags_init_add(&frags, data, data_len);
// fn(..., &frags, ...);
Call a function that expects a frags_t
pointer, when there is only a dbuffer_t
available.
The dbuffer_t
type is used by previous network interfaces in Simics. To create a frags_t
from a dbuffer, the data needs to be extracted from the dbuffer first.
uint8 buf[dbuffer_len(dbuf)];
memcpy(buf, dbuffer_read_all(dbuf), dbuffer_len(dbuf));
frags_t frags;
frags_init_add(&frags, buf, dbuffer_len(dbuf));
// fn(..., &frags, ...);
Add a header.
To add a header to a message, create a new frags_t
that contains the header and the contents of the old message.
frags_t msg;
frags_init_add(&msg, header, header_len);
frags_add_from_frags(&msg, payload, 0, frags_len(payload));
There is a utility function called frags_prefix()
that does exactly this:
frags_t new_msg = frags_prefix(header, header_len, payload);
Similarly, there is a frags_suffix()
for adding at the end.
Remember that any frags_t
received in a function call is read-only, so the only way to add a header is to create a new frags_t
.
Strip a header.
To remove a header, use frags_add_from_frags()
to pick the non-header part of the old message.
frags_t payload;
frags_init_add_from_frags(&payload, msg,
HEADER_LEN, frags_len(msg) - HEADER_LEN);
Change a field in a packet.
To change the value of a specific field, use frags_add_from_frags()
to pick the part of the message you want to keep, and frags_add()
to add the new value where it should be:
frags_t new_packet;
uint8 new_field[6] = { 0, 1, 2, 3, 4, 5 };
// copy everything before the field
frags_init_add_from_frags(&new_packet, msg,
0, OUR_FIELD_OFFSET);
// add the new field
frags_add(&new_packet, new_field, 6);
// copy the rest of the packet
frags_add_from_frags(&new_packet, msg,
OUR_FIELD_OFFSET + 6,
frags_len(msg) - (OUR_FIELD_OFFSET + 6));
Extract integer values
To read a multi-byte integer value from a message, care needs to be taken to handle byte order portably. There are macros in simics/util/swabber.h
that helps with this:
uint32 buf[1];
frags_extract_slice(msg, buf, val_offs, 4);
uint32 val = LOAD_BE32(buf);
There are a number of utility functions that facilitates this, so the code above can be written using frags_extract_be32()
.
uint32 new_val = frags_extract_be32(msg, val_offs);
Print the contents of a packet
A simple loop over the fragments via an iterator will do the job:
void print_packet(const frags_t *packet)
{
printf("packet: ");
for (frags_it_t it = frags_it(packet, 0, frags_len(packet));
!frags_it_end(it);
it = frags_it_next(it)) {
const uint8 *it_data = frags_it_data(it);
for (int i=0; i<frags_it_len(it); i++)
printf("%x ", (unsigned)it_data[i]);
}
printf("\n");
}
The complete API documentation for all frags_t
-related functions is available in the
API Reference Manual, in the Device API Functions section.