作答表單:
測驗 1
考慮我們想要實作一個 lock-free 的 single-producer/single-comsuer (SPSC) 並行程式,底層使用 ring buffer,且避免 false sharing。測試程式的參考輸出: (其中 4
指定 4 個處理器核)
程式碼可參見 SPSC (部分遮蔽)
其中利用到針對多核處理器的 spinlock: significant optimizations 技巧,對照 cserv 專案的 src/util/spinlock.h。
已知執行過程不會遇到任何 assert 錯誤,請補完程式碼,使其執行符合預期。作答規範:
DDDD
和 EEEE
皆為表示式
- 以最精簡的形式作答
測驗 2
考慮一個特別的 circular buffer 實作,嘗試透過 mmap 系統呼叫以化簡緩衝區邊界處理的議題。
A circular-buffer implementation may be optimized by mapping the underlying buffer to two contiguous regions of virtual memory. (Naturally, the underlying buffer‘s length must then equal some multiple of the system’s page size.) Reading from and writing to the circular buffer may then be carried out with greater efficiency by means of direct memory access; those accesses which fall beyond the end of the first virtual-memory region will automatically wrap around to the beginning of the underlying buffer. When the read offset is advanced into the second virtual-memory region, both offsets—read and write—are decremented by the length of the underlying buffer.
接下來 circular buffer 初始化時會呼叫三次 mmap,為什麼呢?
- 第一次呼叫
- 向作業系統請求配置一塊足以容納兩倍 circular buffer 空間的記憶體
- 第二次呼叫:
- 將稍早用 mkstemp 建立的暫存檔映射到第一次呼叫 mmap 所取得的記憶體中,映射大小為 buffer 的大小
- 第三次呼叫:
- 以第二次要求映射的記憶體位置 (即第一次得到的位置) 加上 buffer 大小的記憶體位置作為要求配置的位置,映射大小同樣為 buffer 的大小,並也是映射到同樣的 fd 上(注意,兩次呼叫傳入的 offset 均為0)
- 如此一來,第二次與第三次映射的記憶體位置即指向同一塊記憶體

mmap is used to mirror the buffer

the "mirrored" buffer is then placed beside the buffer. When the user polls the item it doesn't matter if the item crosses the buffer's boundary.
有了這個特性後,當我們使用 memcpy 在寫入/讀取 buffer 時即可不用考慮到邊界問題,進而改善存取效率,否則,我們必須考慮到目前 index 是否會超出邊界等等,這將對效能造成衝擊。
測試程式碼如下: (test.c
)
編譯上述 test.c
:
參考執行輸出:
其中 queue.h
程式碼列表如下:
#ifndef queue_h_
#define queue_h_
#include <pthread.h>
#include <stdint.h>
typedef struct {
uint8_t *buffer;
size_t size;
int fd;
size_t head, tail;
size_t head_seq;
size_t tail_seq;
pthread_cond_t readable, writeable;
pthread_mutex_t lock;
} queue_t;
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
typedef struct {
size_t len, seq;
} message_t;
static inline void queue_error(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
fprintf(stderr, "queue error: ");
vfprintf(stderr, fmt, args);
fprintf(stderr, "\n");
va_end(args);
abort();
}
static inline void queue_error_errno(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
fprintf(stderr, "queue error: ");
vfprintf(stderr, fmt, args);
fprintf(stderr, " (errno %d)\n", errno);
va_end(args);
abort();
}
void queue_init(queue_t *q, size_t s)
{
if (s % getpagesize() != 0) {
queue_error(
"Requested size (%lu) is not a multiple of the page size (%d)", s,
getpagesize());
}
if ((q->fd = memfd_create("queue_region", 0)) == -1)
queue_error_errno("Could not obtain anonymous file");
if (ftruncate(q->fd, s) != 0)
queue_error_errno("Could not set size of anonymous file");
if ((q->buffer = mmap(NULL, 2 * s, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS,
-1, 0)) == MAP_FAILED)
queue_error_errno("Could not allocate virtual memory");
if (mmap(q->buffer, s, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED,
q->fd, 0) == MAP_FAILED)
queue_error_errno("Could not map buffer into virtual memory");
if (mmap(q->buffer + s, s, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED,
q->fd, 0) == MAP_FAILED)
queue_error_errno("Could not map buffer into virtual memory");
if (pthread_mutex_init(&q->lock, NULL) != 0)
queue_error_errno("Could not initialize mutex");
if (pthread_cond_init(&q->readable, NULL) != 0)
queue_error_errno("Could not initialize condition variable");
if (pthread_cond_init(&q->writeable, NULL) != 0)
queue_error_errno("Could not initialize condition variable");
q->size = s;
q->head = q->tail = 0;
q->head_seq = q->tail_seq = 0;
}
void queue_destroy(queue_t *q)
{
if (munmap(q->buffer + q->size, q->size) != 0)
queue_error_errno("Could not unmap buffer");
if (munmap(q->buffer, q->size) != 0)
queue_error_errno("Could not unmap buffer");
if (close(q->fd) != 0)
queue_error_errno("Could not close anonymous file");
if (pthread_mutex_destroy(&q->lock) != 0)
queue_error_errno("Could not destroy mutex");
if (pthread_cond_destroy(&q->readable) != 0)
queue_error_errno("Could not destroy condition variable");
if (pthread_cond_destroy(&q->writeable) != 0)
queue_error_errno("Could not destroy condition variable");
}
void queue_put(queue_t *q, uint8_t *buffer, size_t size)
{
pthread_mutex_lock(&q->lock);
while ((q->size - (q->tail - q->head)) < (size + sizeof(message_t)))
pthread_cond_wait(&q->writeable, &q->lock);
message_t m = {.len = size, .seq = q->tail_seq++};
memcpy(&q->buffer[q->tail], &m, sizeof(message_t));
memcpy(&q->buffer[AAA], buffer, size);
q->tail += BBB;
pthread_cond_signal(&q->readable);
pthread_mutex_unlock(&q->lock);
}
size_t queue_get(queue_t *q, uint8_t *buffer, size_t max)
{
pthread_mutex_lock(&q->lock);
message_t m;
for (;;) {
while ((q->tail - q->head) == 0)
pthread_cond_wait(&q->readable, &q->lock);
memcpy(&m, &q->buffer[q->head], sizeof(message_t));
if (m.len > max) {
while (q->head_seq == m.seq)
pthread_cond_wait(&q->writeable, &q->lock);
continue;
}
break;
}
memcpy(buffer, &q->buffer[q->head + sizeof(message_t)], m.len);
q->head += m.len + sizeof(message_t);
q->head_seq++;
if (q->head >= q->size) {
CCC;
}
pthread_cond_signal(&q->writeable);
pthread_mutex_unlock(&q->lock);
return m.len;
}
#endif
請補完程式碼。
作答區
AAA = ?
(a)
q->head
(b)
q->size - (q->tail - q->head)
(c)
q->tail + sizeof(message_t)
(d)
sizeof(message_t)
(e)
size + sizeof(message_t)
BBB = ?
(a)
q->head
(b)
q->size - (q->tail - q->head)
(c)
q->tail + sizeof(message_t)
(d)
sizeof(message_t)
(e)
size + sizeof(message_t)
CCC = ?
(a)
q->head -= q->size; q->tail -= q->size;
(b)
q->head -= sizeof(message_t); q->tail -= sizeof(message_t);
(c)
q->head -= (q->tail - q->head); q->tail -= (q->tail - q->head);
延伸問題:
- 解釋上述程式碼運作原理,並探討效能表現
- 在 Linux 核心原始程式碼找出類似改善 ring buffer 效能的程式碼並解析
測驗 3
考慮以下透過 mmap 實作快速檔案複製的程式碼: mmap-filecopy.c
編譯方式:
假設原本已有檔名為 in
的檔案,且 out
不存在目前的路徑,可執行以下命令:
這樣即可達成快速的檔案複製。
請補完程式碼,使得符合預期。
作答區
PROP = ?
(a)
PROT_READ | PROT_WRITE
(b)
PROT_READ
延伸問題:
- 解釋上述程式碼運作原理,並指出其缺失
- 探討 sendfile 和 splice 等系統系統在上述程式的應用