How does copyout() in xv6 avoid race condition in the page table? - operating-system

// Copy from kernel to user.
// Copy len bytes from src to virtual address dstva in a given page table.
// Return 0 on success, -1 on error.
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
The function used in xv6 kernel for copying out data from the kernel to user-space process accepts a pagetable as argument and uses that pagetable to access the physical address behind that user-space address. However, neither copyout() nor walkaddr() performs any locking operation on the page table. How are race conditions avoided in this case? Is it possible for the page table to change after getting the physical address and before the actual reading and cause issues? And how is it avoided in xv6?

If I am not wrong, the copyout() is invoked by the kernel in the
piperead() and pipewrite() located in pipewrite.c function. Prior to calling this
function the calling process's spinlock is already held.

Related

Unaligned access performance on Intel x86 vs AMD x86 CPUs

I have implemented a simple linear probing hash map with an array of structs memory layout. The struct holds the key, the value, and a flag indicating whether the entry is valid. By default, this struct gets padded by the compiler, as key and value are 64-bit integers, but the entry only takes up 8 bools. Hence, I have also tried packing the struct at the cost of unaligned access. I was hoping to get better performance from the packed/unaligned version due to higher memory density (we do not waste bandwidth on transferring padding bytes).
When benchmarking this hash map on an Intel Xeon Gold 5220S CPU (single-threaded, gcc 11.2, -O3 and -march=native), I see no performance difference between the padded version and the unaligned version. However, on an AMD EPYC 7742 CPU (same setup), I find a performance difference between unaligned and padded. Here is a graph depicting the results for hash map load factors 25 % and 50 %, for different successful query rates on the x axis (0,25,50,75,100): As you can see, on Intel, the grey and blue (circle and square) lines almost overlap, the benefit of struct packing is marginal. On AMD, however, the line representing unaligned/packed structs is consistently higher, i.e., we have more throughput.
In order to investigate this, I tried to built a smaller microbenchmark. In this microbenchmark, we perform a similar benchmark, but without the hash map find logic (i.e., we just pick random indices in the array and advance a little there). Please find the benchmark here:
#include <atomic>
#include <chrono>
#include <cstdint>
#include <iostream>
#include <random>
#include <vector>
void ClobberMemory() { std::atomic_signal_fence(std::memory_order_acq_rel); }
template <typename T>
void doNotOptimize(T const& val) {
asm volatile("" : : "r,m"(val) : "memory");
}
struct PaddedStruct {
uint64_t key;
uint64_t value;
bool is_valid;
PaddedStruct() { reset(); }
void reset() {
key = uint64_t{};
value = uint64_t{};
is_valid = 0;
}
};
struct PackedStruct {
uint64_t key;
uint64_t value;
uint8_t is_valid;
PackedStruct() { reset(); }
void reset() {
key = uint64_t{};
value = uint64_t{};
is_valid = 0;
}
} __attribute__((__packed__));
int main() {
const uint64_t size = 134217728;
uint16_t repetitions = 0;
uint16_t advancement = 0;
std::cin >> repetitions;
std::cout << "Got " << repetitions << std::endl;
std::cin >> advancement;
std::cout << "Got " << advancement << std::endl;
std::cout << "Initializing." << std::endl;
std::vector<PaddedStruct> padded(size);
std::vector<PackedStruct> unaligned(size);
std::vector<uint64_t> queries(size);
// Initialize the structs with random values + prefault
std::random_device rd;
std::mt19937 gen{rd()};
std::uniform_int_distribution<uint64_t> dist{0, 0xDEADBEEF};
std::uniform_int_distribution<uint64_t> dist2{0, size - advancement - 1};
for (uint64_t i = 0; i < padded.size(); ++i) {
padded[i].key = dist(gen);
padded[i].value = dist(gen);
padded[i].is_valid = 1;
}
for (uint64_t i = 0; i < unaligned.size(); ++i) {
unaligned[i].key = padded[i].key;
unaligned[i].value = padded[i].value;
unaligned[i].is_valid = 1;
}
for (uint64_t i = 0; i < unaligned.size(); ++i) {
queries[i] = dist2(gen);
}
std::cout << "Running benchmark." << std::endl;
ClobberMemory();
auto start_padded = std::chrono::high_resolution_clock::now();
PaddedStruct* padded_ptr = nullptr;
uint64_t sum = 0;
for (uint16_t j = 0; j < repetitions; j++) {
for (const uint64_t& query : queries) {
for (uint16_t i = 0; i < advancement; i++) {
padded_ptr = &padded[query + i];
if (padded_ptr->is_valid) [[likely]] {
sum += padded_ptr->value;
}
}
doNotOptimize(sum);
}
}
ClobberMemory();
auto end_padded = std::chrono::high_resolution_clock::now();
uint64_t padded_runtime = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(end_padded - start_padded).count());
std::cout << "Padded Runtime (ms): " << padded_runtime << " (sum = " << sum << ")" << std::endl; // print sum to avoid that it gets optimized out
ClobberMemory();
auto start_unaligned = std::chrono::high_resolution_clock::now();
uint64_t sum2 = 0;
PackedStruct* packed_ptr = nullptr;
for (uint16_t j = 0; j < repetitions; j++) {
for (const uint64_t& query : queries) {
for (uint16_t i = 0; i < advancement; i++) {
packed_ptr = &unaligned[query + i];
if (packed_ptr->is_valid) [[likely]] {
sum2 += packed_ptr->value;
}
}
doNotOptimize(sum2);
}
}
ClobberMemory();
auto end_unaligned = std::chrono::high_resolution_clock::now();
uint64_t unaligned_runtime = static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(end_unaligned - start_unaligned).count());
std::cout << "Unaligned Runtime (ms): " << unaligned_runtime << " (sum = " << sum2 << ")" << std::endl;
}
When running the benchmark, I pick repetitions = 3 and advancement = 5, i.e., after compiling and running it, you have to enter 3 (and press newline) and then enter 5 and press enter/newline. I updated the source code to (a) avoid loop unrolling by the compiler because repetition/advancement were hardcoded and (b) switch to pointers into that vector as it more closely resembles what the hash map is doing.
On the Intel CPU, I get:
Padded Runtime (ms): 13204
Unaligned Runtime (ms): 12185
On the AMD CPU, I get:
Padded Runtime (ms): 28432
Unaligned Runtime (ms): 22926
So while in this microbenchmark, Intel still benefits a little from the unaligned access, for the AMD CPU, both the absolute and relative improvement is higher. I cannot explain this. In general, from what I've learned from relevant SO threads, unaligned access for a single member is just as expensive as aligned access, as long as it stays within a single cache line (1). Also in (1), a reference to (2) is given, which claims that the cache fetch width can differ from the cache line size. However, except for Linus Torvalds mail, I could not find any other documentation of cache fetch widths in processors and especially not for my concrete two CPUs to figure out if that might somehow have to do with this.
Does anybody have an idea why the AMD CPU benefits much more from the struct packing? If it is about reduced memory bandwidth consumption, I should be able to see the effects on both CPUs. And if the bandwidth usage is similar, I do not understand what might be causing the differences here.
Thank you so much.
(1) Relevant SO thread: How can I accurately benchmark unaligned access speed on x86_64?
(2) https://www.realworldtech.com/forum/?threadid=168200&curpostid=168779
The L1 Data Cache fetch width on the Intel Xeon Gold 5220S (and all the other Skylake/CascadeLake Xeon processors) is up to 64 naturally-aligned Bytes per cycle per load.
The core can execute two loads per cycle for any combination of size and alignment that does not cross a cacheline boundary. I have not tested all the combinations on the SKX/CLX processors, but on Haswell/Broadwell, throughput was reduced to one load per cycle whenever a load crossed a cacheline boundary, and I would assume that SKX/CLX are similar. This can be viewed as necessary feature rather than a "penalty" -- a line-splitting load might need to use both ports to load a pair of adjacent lines, then combine the requested portions of the lines into a payload for the target register.
Loads that cross page boundaries have a larger performance penalty, but to measure it you have to be very careful to understand and control the locations of the page table entries for the two pages: DTLB, STLB, in the caches, or in main memory. My recollection is that the most common case is pretty fast -- partly because the "Next Page Prefetcher" is pretty good at pre-loading the PTE entry for the next page into the TLB before a sequence of loads gets to the end of the first page. The only case that is painfully slow is for stores that straddle a page boundary, and the Intel compiler works very hard to avoid this case.
I have not looked at the sample code in detail, but if I were performing this analysis, I would be careful to pin the processor frequency, measure the instruction and cycle counts, and compute the average number of instructions and cycles per update. (I usually set the core frequency to the nominal (TSC) frequency just to make the numbers easier to work with.) For the naturally-aligned cases, it should be pretty easy to look at the assembly code and estimate what the cycle counts should be. If the measurements are similar to observations for that case, then you can begin looking at the overhead of unaligned accesses in reference to a more reliable understanding of the baseline.
Hardware performance counters can be valuable for this case as well, particularly the DTLB_LOAD_MISSES events and the L1D.REPLACEMENT event. It only takes a few high-latency TLB miss or L1D miss events to skew the averages.
The number of cache-line accesses when using 24-byte data structures may be the same as when using 17-byte data structure.
Please see this blog post: https://lemire.me/blog/2022/06/06/data-structure-size-and-cache-line-accesses/

Getting "*** buffer overflow detected ***: terminated Aborted" after calling recvfrom( ) function for UDP communication

I am facing an error whenever I call a recvfrom() function for UDP in server side:
buffer overflow detected ***: terminated Aborted
What is the meaning of this error? I am not able to understand.
unsigned int len;
int rv,i;
int tmp;
//char msg[200],command=0;
unsigned short *Fptr;
float Float_Temp;
//make socket blocking
FD_ZERO(&readnbs);
FD_SET(g_iUDP_datalogger_soc, &readnbs);
g_UDP_Blocktimervalue.tv_sec = 0;
g_UDP_Blocktimervalue.tv_usec = UDP_REC_BLOCKTIME;
rv = select(g_iUDP_datalogger_soc + 1, &readnbs, NULL, NULL, &g_UDP_Blocktimervalue);
len = sizeof(g_UDP_ClientAddr);
if (rv == 1) {
printf(" \n\n\n\n\n rv=%d\n\n\n\n",rv);
tmp = recvfrom(g_iUDP_datalogger_soc, &tmp, SIZE_UDP_MSG, 0,(struct sockaddr *) &g_UDP_ClientAddr, &len);
tmp = recvfrom(g_iUDP_datalogger_soc, [YOUR BUFFER ?] , SIZE_UDP_MSG, 0,(struct sockaddr *)...
You're trying to buffer the received data into a single int (tmp) object, which you are also using as the return value from the recvfrom(), and even not checking in any way - either for error, nor for received data.
You are asking recvfrom() to read SIZE_UDP_MSG number of bytes into tmp, which is an int. You did not show what SIZE_UDP_MSG is defined as, but if SIZE_UDP_MSG > sizeof(int), you are going to be writing bytes past the bounds of tmp into surrounding memory, corrupting the memory. That is a buffer overflow.
Perhaps you meant to receive the bytes into your (commented out) msg buffer instead?
char msg[SIZE_UDP_MSG];
tmp = recvfrom(..., msg, SIZE_UDP_MSG, ...);

stm32f429, spi dr register not write data

code_1
code_2
register on debug
logic analyzer
void SPI_SendData(SPI_RegDef_t *pSPIx,uint8_t *pTxBuffer , uint32_t Len)
{
while(Len > 0)
{
// 1 . wait until TXE is set ,
while(SPI_GetFlagStatus(pSPIx, SPI_TXE_FLAG) == FLAG_RESET);
// 2. check the DFF bit in CR1
if( (pSPIx->CR1 & (1 << SPI_CR1_DFF) ) )
{
// 16 BIT DFF
pSPIx->DR = *((uint16_t*)pTxBuffer); // dereferencing(typecasting );
Len--;
Len--;
(uint16_t*)pTxBuffer++; // typecasting this pointer to uint16 type and incrementing by 2.
/* The buffer is a uint8_t pointer type. When using the 16-bit data frame,
* we pick up 16-bits of data, increment pointer by 2 bytes,*/
}else
{
// 8 BIT DFF
pSPIx->DR = *pTxBuffer;
Len--;
pTxBuffer++;
/*
*(( uint8_t*)&hspi->Instance->DR) = (*pData);
pData += sizeof(uint8_t);
hspi->TxXferCount--;
*/
}
}
}
i see, MOSI always send 255 on logic analyzer but wrong data.
(uint16_t*)pTxBuffer++; increments the pointer by 1 byte, not two that you say you hope it will in the comment.
If you want to do it by converting to halfword pointer and incrementing, then you need to do something like:
pTxBuffer = (uint8_t*)(((uint16_t*)pTxBuffer) + 1);
But that is just a silly way of saying pTxBuffer += 2;
Really it doesn't make sense to have the if inside the loop, because the value of the DFF bit doesn't change unless you write to it, and you don't. I suggest you write one loop over words and one loop over bytes and have the if at the top level.

DPDK implementation of MPSC ring buffer

While going through the implementation of the DPDK MPSC (multi-produce & single-consumer) Ring Buffer API, i found the code to move the head of the producer for inserting new elements in the Ring buffer. The function is as follows :
static __rte_always_inline unsigned int
__rte_ring_move_prod_head(struct rte_ring *r, unsigned int is_sp,
unsigned int n, enum rte_ring_queue_behavior behavior,
uint32_t *old_head, uint32_t *new_head,
uint32_t *free_entries)
{
const uint32_t capacity = r->capacity;
uint32_t cons_tail;
unsigned int max = n;
int success;
*old_head = __atomic_load_n(&r->prod.head, __ATOMIC_RELAXED);
do {
/* Reset n to the initial burst count */
n = max;
/* Ensure the head is read before tail */
__atomic_thread_fence(__ATOMIC_ACQUIRE);
/* load-acquire synchronize with store-release of ht->tail
* in update_tail.
*/
cons_tail = __atomic_load_n(&r->cons.tail,
__ATOMIC_ACQUIRE);
/* The subtraction is done between two unsigned 32bits value
* (the result is always modulo 32 bits even if we have
* *old_head > cons_tail). So 'free_entries' is always between 0
* and capacity (which is < size).
*/
*free_entries = (capacity + cons_tail - *old_head);
/* check that we have enough room in ring */
if (unlikely(n > *free_entries))
n = (behavior == RTE_RING_QUEUE_FIXED) ?
0 : *free_entries;
if (n == 0)
return 0;
*new_head = *old_head + n;
if (is_sp)
r->prod.head = *new_head, success = 1;
else
/* on failure, *old_head is updated */
success = __atomic_compare_exchange_n(&r->prod.head,
old_head, *new_head,
0, __ATOMIC_RELAXED,
__ATOMIC_RELAXED);
} while (unlikely(success == 0));
return n;
}
The load and compare exchange of the producer's head is done using __ATOMIC_RELAXED memory ordering. Isn't this a problem when multiple producers from different threads produce to the queue. Or am I missing something?
https://doc.dpdk.org/guides/prog_guide/ring_lib.html describes the basic mechanism that DPDK uses for implementing the Ring buffer.

How to ensure recv() to have all data send() in tcp

I am implementing the recvall() function to be sure that the data is completely sent. Also I modified the send() function to sendall() like this:
int sendall (int consocket, char* buf, int* len)
{
int total = 0;
int bytesleft = *len; // how many we have left to send
int n;
while(total < *len) {
n = send(consocket, buf+total, bytesleft, 0);
if (n == -1) { break; }
total += n;
bytesleft -= n;
*len = total; // return number actually sent here
return n==-1?-1:0; // return -1 on failure, 0 on success
}
}
How can I implement recvall()? Say I sent from the server a struct of 14 bytes and I check in the client and get 12 bytes.. now in an unreliable situation how should I manage to get the other two bytes... I have spent time trying... any help welcomed.
You may loop over a select with an appropriate timeout . Select will wait until new data is received or until time is elapsed or an error occurs. This gives you a greater control than just blocing on a recv.