笨辦法學C 練習44:環形緩衝區

飛龍發表於2019-05-09

練習44:環形緩衝區

原文:Exercise 44: Ring Buffer

譯者:飛龍

環形緩衝區在處理非同步IO時非常實用。它們可以在一段接收隨機長度和區間的資料,在另一端以相同長度和區間提供密緻的資料塊。它們是Queue資料結構的變體,但是它針對於位元組塊而不是一系列指標。這個練習中我打算想你展示RingBuffer的程式碼,並且之後你需要對它執行完整的單元測試。

#ifndef _lcthw_RingBuffer_h
#define _lcthw_RingBuffer_h

#include <lcthw/bstrlib.h>

typedef struct {
    char *buffer;
    int length;
    int start;
    int end;
} RingBuffer;

RingBuffer *RingBuffer_create(int length);

void RingBuffer_destroy(RingBuffer *buffer);

int RingBuffer_read(RingBuffer *buffer, char *target, int amount);

int RingBuffer_write(RingBuffer *buffer, char *data, int length);

int RingBuffer_empty(RingBuffer *buffer);

int RingBuffer_full(RingBuffer *buffer);

int RingBuffer_available_data(RingBuffer *buffer);

int RingBuffer_available_space(RingBuffer *buffer);

bstring RingBuffer_gets(RingBuffer *buffer, int amount);

#define RingBuffer_available_data(B) (((B)->end + 1) % (B)->length - (B)->start - 1)

#define RingBuffer_available_space(B) ((B)->length - (B)->end - 1)

#define RingBuffer_full(B) (RingBuffer_available_data((B)) - (B)->length == 0)

#define RingBuffer_empty(B) (RingBuffer_available_data((B)) == 0)

#define RingBuffer_puts(B, D) RingBuffer_write((B), bdata((D)), blength((D)))

#define RingBuffer_get_all(B) RingBuffer_gets((B), RingBuffer_available_data((B)))

#define RingBuffer_starts_at(B) ((B)->buffer + (B)->start)

#define RingBuffer_ends_at(B) ((B)->buffer + (B)->end)

#define RingBuffer_commit_read(B, A) ((B)->start = ((B)->start + (A)) % (B)->length)

#define RingBuffer_commit_write(B, A) ((B)->end = ((B)->end + (A)) % (B)->length)

#endif

觀察這個資料結構,你會看到它含有bufferstartendRingBuffer的所做的事情只是在buffer中移動startend,所以當資料到達緩衝區末尾時還可以繼續“迴圈”。這樣就會給人一種在固定空間內無限讀取的“幻覺”。接下來我建立了一些巨集來基於它執行各種計算。

下面是它的實現,它是對工作原理更好的解釋:

#undef NDEBUG
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <lcthw/dbg.h>
#include <lcthw/ringbuffer.h>

RingBuffer *RingBuffer_create(int length)
{
    RingBuffer *buffer = calloc(1, sizeof(RingBuffer));
    buffer->length  = length + 1;
    buffer->start = 0;
    buffer->end = 0;
    buffer->buffer = calloc(buffer->length, 1);

    return buffer;
}

void RingBuffer_destroy(RingBuffer *buffer)
{
    if(buffer) {
        free(buffer->buffer);
        free(buffer);
    }
}

int RingBuffer_write(RingBuffer *buffer, char *data, int length)
{
    if(RingBuffer_available_data(buffer) == 0) {
        buffer->start = buffer->end = 0;
    }

    check(length <= RingBuffer_available_space(buffer),
            "Not enough space: %d request, %d available",
            RingBuffer_available_data(buffer), length);

    void *result = memcpy(RingBuffer_ends_at(buffer), data, length);
    check(result != NULL, "Failed to write data into buffer.");

    RingBuffer_commit_write(buffer, length);

    return length;
error:
    return -1;
}

int RingBuffer_read(RingBuffer *buffer, char *target, int amount)
{
    check_debug(amount <= RingBuffer_available_data(buffer),
            "Not enough in the buffer: has %d, needs %d",
            RingBuffer_available_data(buffer), amount);

    void *result = memcpy(target, RingBuffer_starts_at(buffer), amount);
    check(result != NULL, "Failed to write buffer into data.");

    RingBuffer_commit_read(buffer, amount);

    if(buffer->end == buffer->start) {
        buffer->start = buffer->end = 0;
    }

    return amount;
error:
    return -1;
}

bstring RingBuffer_gets(RingBuffer *buffer, int amount)
{
    check(amount > 0, "Need more than 0 for gets, you gave: %d ", amount);
    check_debug(amount <= RingBuffer_available_data(buffer),
            "Not enough in the buffer.");

    bstring result = blk2bstr(RingBuffer_starts_at(buffer), amount);
    check(result != NULL, "Failed to create gets result.");
    check(blength(result) == amount, "Wrong result length.");

    RingBuffer_commit_read(buffer, amount);
    assert(RingBuffer_available_data(buffer) >= 0 && "Error in read commit.");

    return result;
error:
    return NULL;
}

這些就是一個基本的RingBuffer實現的全部了。你可以從中讀取和寫入資料,獲得它的大小和容量。也有一些緩衝區使用OS中的技巧來建立虛擬的無限儲存,但它們不可移植。

由於我的RingBuffer處理讀取和寫入記憶體塊,我要保證任何end == start出現的時候我都要將它們重置為0,使它們從退回緩衝區頭部。在維基百科上的版本中,它並不可以寫入資料塊,所以只能移動endstart來轉圈。為了更好地處理資料塊,你需要在資料為空時移動到內部緩衝區的開頭。

單元測試

對於你的單元測試,你需要測試儘可能多的情況。最簡單的方法就是預構造不同的RingBuffer結構,之後手動檢查函式和算數是否有效。例如,你可以構造end在緩衝區末尾的右邊,而start在緩衝區範圍內的RingBuffer,來看看它是否執行成功。

你會看到什麼

下面是我的ringbuffer_tests執行結果:

$ ./tests/ringbuffer_tests
DEBUG tests/ringbuffer_tests.c:60: ----- RUNNING: ./tests/ringbuffer_tests
----
RUNNING: ./tests/ringbuffer_tests
DEBUG tests/ringbuffer_tests.c:53: 
----- test_create
DEBUG tests/ringbuffer_tests.c:54: 
----- test_read_write
DEBUG tests/ringbuffer_tests.c:55: 
----- test_destroy
ALL TESTS PASSED
Tests run: 3
$

你應該測試至少三次來確保所有基本操作有效,並且看看在我完成之前你能測試到額外的多少東西。

如何改進

像往常一樣,你應該為這個練習做防禦性程式設計檢查。我希望你這樣做,是因為 liblcthw的程式碼基本上沒有做我教給你的防禦型程式設計檢查。我將它們留給你,便於你熟悉使用這些額外的檢查來改進程式碼。

例如,這個環形緩衝區並沒有過多檢查每次訪問是否實際上都在緩衝區內。

如果你閱讀環形緩衝區的維基百科頁面,你會看到“優化的POSIX實現”,它使用POSIX特定的呼叫來建立一塊無限的區域。研究並且在附加題中嘗試實現它。

附加題

  • 建立RingBuffer的替代版本,使用POSIX的技巧併為其執行單元測試。

  • 為二者新增一個效能對比測試,通過帶有隨機資料和隨機讀寫操作的模糊測試來比較兩個版本。確保你你對每個版本進行了相同的操作,便於你在操作之間比較二者。

  • 使用callgrindcachegrind比較二者的效能。

相關文章