面向資料流程式設計:理解環形緩衝區——Buffer Ring
阿新 • • 發佈:2020-12-14
面向資料流程式設計:理解環形緩衝區——Buffer Ring
所謂環形緩衝區是我們預留的一片用來儲存接收資料的區域(可以是外部的也可以是內部的),儲存資料的區域是有限的,而不斷接收的資料長度是未知的,甚至是無限的(理論上不斷電不破壞,感測器就能一直有資料傳回),鑑於硬體記憶體的限制我們無法建立一個無限大的儲存空間,只能在有限的資料儲存區域內,邊存邊寫,來減小資料的丟失。
環形緩衝區實際上就是一個邊存邊處理資料的模式,新儲存的資料在儲存區達到上限後將重回起點將已經處理完的資料重新覆蓋,達到一個重複利用的目的(這裡可以理解為鉛筆,橡皮檫與紙,你在打草稿時,一頁的計算量計算出結果後,你只需儲存答案,而草稿紙上的計算過程則可以擦除,用來進行下一次計算,環形緩衝區也是如此,處理過後的資料邊可以擦除,騰出來空間用以儲存新的資料,當然這裡沒有擦除的必要,重新覆蓋即可)。
a[50]
,在向儲存資料的同時,開始讀取資料進行處理,如果讀取資料的速度與儲存的速度相同,那麼存滿50個數據時,我同時讀取了50個數據,這個資料裡面儲存的資料對我已經沒有了作用,儲存資料的指標將會重新指向陣列第一個,讀取資料的指標同樣如此如此便達到了一片小區域無限儲存的目的。將陣列想象成一個首尾相連的環,這就是所謂的環形緩衝區。儲空間。
環形Buffer的特點:
通常包含一個讀指標(read_index)和一個寫指標(write_index)。讀指標指向環形 Buffer中第一個可讀的資料,寫指標指向環形Buffer中第一個可寫的緩衝區。通過移動讀指標和寫指標就可以實現Buffer的資料讀取和寫入。在 通常情況下,環形Buffer的讀使用者僅僅會影響讀指標,而寫使用者也僅僅會影響寫指標。
首先在記憶體裡開闢一片區域(大小為 buffer_size),對於寫使用者,順次往Buffer裡寫入東西,一直寫到最後那個記憶體(buffer_size)時再將寫指標指向記憶體區域的首地 址,即接下來的資料轉個環放到最開始處,只有遇到Buffer裡的有效儲存空間為0時,才丟掉資料;對於讀使用者,順次從Buffer裡讀出東西,一直寫到 最後那個記憶體(buffer_size)時再將讀指標指向記憶體區域的首地址,即接下來轉個環從最開始處取資料。
有效儲存空間與buffer_size的區別:
有效儲存空間是指那些沒有存放資料,或者以前存放過但已經處理過的資料,就是可用的空間大小;而buffer_size指的是總大小。
通過上面介紹可知,環形Buffer仍然是一長條區域,只不過其空間會被迴圈使用而已。示意圖如下,根據讀寫指標的位置可分為兩種情況,其中陰影填充部分為資料/已用空間,空白的為可用空間,即有效儲存空間。
struct rb
{
rt_uint16_t read_index, write_index; //讀指標和寫指標
rt_uint8_t *buffer_ptr; //環形buffer指標
rt_uint16_t buffer_size; //環形buffer大小
};
然後就是實現讀/寫操作了,對於寫操作:
/* 向環形buffer中寫入資料 */
static rt_bool_t rb_put(struct rb* rb, const rt uint8_t *ptr, rt_uint16_t length)
{
rt_size_t size;
/* 判斷是否有足夠的剩餘空間 */
//圖示的第一種情況,相減即為有效儲存空間,就是中間那段空白區域了
if (rb->read_index > rb->write_index)
size = rb->read_index - rb->write_index;
else // 圖示的第二種情況,兩段效儲存空間之和
size = rb->buffer_size - rb->write_index + rb->read_index;
/* 沒有多餘的空間,即空間不足無法完成寫入,直接返回錯誤 */
if (size < length) return RT_FALSE;
/* 以下均是指有效儲存空間滿足,可以完成寫操作 */
//圖示的第一種情況,所以直接將資料從write_index處寫入到buffer_ptr中即可
if (rb->read_index > rb->write_index)
{
/* read_index - write_index 即為總的空餘空間
* 關於memcpy, 指的是將prt中長度為length的資料拷貝到第一個引數所指的地址
* 其實每次寫入都必須是從write_index(寫指標)開始,
* 因為write_index是指向環形Buffer中的第一個可寫緩衝區
*/
memcpy(&rb->buffer_ptr[rb->write_index], ptr, length);
rb->write_index += length; //寫操作完成後需要將寫指標後移相應的長度
}
else //圖示的第二種情況,有兩段可用空間
{
//如果後半段空閒空間足夠容納,則直接拷貝即可
if (rb->buffer_size - rb->write_index > length)
{
memcpy(&rb->buffer_ptr[rb->write_index], ptr, length);
rb->write_index += length; //寫指標後移相應的長度
}
else //如果後半段空間不足,則先將後半段裝滿,然後再將剩餘的裝到前半段之中
{
/*
* write_index 後面剩餘的空間不存在足夠的長度,
* 需要把部分資料複製到前面的剩餘空間中
*/
//先塞滿後半段,注意大小不是length,而是後半段的長度
memcpy(&rb->buffer_ptr[rb->write index], ptr, rb->buffer_size - rb->write_index);
//此時只能從buffer_ptr的開始處進行寫入了,長度為總長減去上次已經寫入的
memcpy(&rb->buffer_ptr[0], &ptr[rb->buffer_size - rb->write_index], length - (rb->buffer_size - rb->write_index));
//寫指標後移相應的長度,其實就是第二次寫入的長度
rb->write_index = length - (rb->buffer_size - rb->write_index);
}
}
return RT_TRUE;
}
對於讀操作:
/* 從環形buffer中讀出資料 */
static rt_bool_t rb_get(struct rb* rb, rt_uint8 t *ptr, rt_uint16_t length)
{
rt_size_t size;
/* 判斷是否有足夠的資料 */
if (rb->read_index > rb->write_index) //圖示的第一種情況,有效資料為前後兩段的和
size = rb->buffer_size - rb->read_index + rb->write_index;
else // 圖示的第二種情況, 中間一段為資料,直接相減即可
size = rb->write_index - rb->read_index;
/* 沒有足夠的資料,如果剩餘資料不足,這直接返回錯誤 */
if (size < length) return RT_FALSE;
/* 以下均是指剩餘資料量滿足,可以完成讀操作 */
if (rb->read_index > rb->write_index) //圖示的第一種情況,有兩段資料
{
//如果後半段資料足夠,則直接取用
if (rb->buffer_size - rb->read_index > length)
{
//這裡是將buffer_ptr裡面取出長度為length的資料放到ptr之中;
//切記總是從read_index(讀指標)開始取
memcpy(ptr, &rb->buffer_ptr[rb->read_index], length);
rb->read_index += length; //讀指標後移相應的長度
}
else //如果後半段不夠用,則先取完後半段,然後從前半段取剩餘的部分
{
/* read index的資料不夠,需要分段複製 */
//注意資料長度
memcpy(ptr, &rb->buffer_ptr[rb->read index], rb->buffer_size - rb->read_index);
//注意資料長度和儲存的起始位置
memcpy(&ptr[rb->buffer_size - rb->read_index], &rb->buffer_ptr[0], length - rb->buffer_size + rb->read_index);
rb->read_index = length - rb->buffer_size + rb->read_index;
}
}
else //圖示的第二種情況,僅中間一段有資料,直接取用
{
memcpy(ptr, &rb->buffer_ptr[rb->read_index], length);
rb->read_index += length; //讀指標後移相應的長度
}
return RT_TRUE;
}c
本文在另一篇文章的基礎上加了點自己的見解,現附上原文連結理解RT-Thead中使用的環形緩衝區 — Buffer Ring