循環緩衝區 【ChatGPT】

循環緩衝區

作者:

Linux提供了許多功能,可用於實現循環緩衝區。有兩組這樣的功能:

  1. 用於確定2的冪大小緩衝區信息的便利函數。
  2. 當緩衝區中的對象的生產者和消費者不想共享鎖時,使用內存屏障。

要使用下面討論的這些功能,只需要一個生產者和一個消費者。可以通過串行化來處理多個生產者,並通過串行化來處理多個消費者。

什麼是循環緩衝區?

首先,什麼是循環緩衝區?循環緩衝區是一個固定大小的緩衝區,其中有兩個索引:

  • '頭'索引 - 生產者將項目插入緩衝區的位置。
  • '尾'索引 - 消費者在緩衝區中找到下一個項目的位置。

通常情況下,當尾指針等於頭指針時,緩衝區爲空;當頭指針比尾指針小1時,緩衝區爲滿。

當添加項目時,頭索引會遞增,而當移除項目時,尾索引會遞增。尾索引不應超過頭索引,並且當它們到達緩衝區末尾時,兩個索引都應該被包裝到0,從而允許無限量的數據流經過緩衝區。

通常,項目都是相同的單位大小,但並不嚴格要求使用下面的技術。如果要在緩衝區中包含多個項目或大小可變的項目,則可以逐漸增加索引,前提是兩個索引都不會超過對方。但是,實施者必須小心,因爲一個大於一個單位大小的區域可能會包裝到緩衝區的末尾,並被分成兩個段。

測量2的冪緩衝區

通常情況下,計算任意大小的循環緩衝區的佔用量或剩餘容量將是一個緩慢的操作,需要使用模數(除法)指令。但是,如果緩衝區的大小是2的冪,則可以使用更快的按位AND指令。

Linux提供了一組用於處理2的冪循環緩衝區的宏。可以通過以下方式使用這些宏:

#include <linux/circ_buf.h>

這些宏包括:

  • 測量緩衝區中剩餘的空間:

    CIRC_SPACE(head_index, tail_index, buffer_size);
    

    這將返回緩衝區中剩餘的空間,可以插入項目。

  • 測量緩衝區中最大的連續空間:

    CIRC_SPACE_TO_END(head_index, tail_index, buffer_size);
    

    這將返回緩衝區中最大的連續空間,可以立即插入項目,而無需回到緩衝區的開頭。

  • 測量緩衝區的佔用量:

    CIRC_CNT(head_index, tail_index, buffer_size);
    

    這將返回當前佔用緩衝區的項目數。

  • 測量緩衝區中不會包裝的佔用量:

    CIRC_CNT_TO_END(head_index, tail_index, buffer_size);
    

    這將返回可以從緩衝區中提取的連續項目數,而無需回到緩衝區的開頭。

這些宏中的每一個通常會返回一個介於0和buffer_size-1之間的值,但是:

  • CIRC_SPACE*() 用於生產者。對於生產者,它們將返回一個下限,因爲生產者控制頭索引,但是消費者可能仍在另一個CPU上耗盡緩衝區並移動尾索引。

  • 對於消費者,它將顯示一個上限,因爲生產者可能正在忙於耗盡空間。

  • CIRC_CNT*() 用於消費者。對於消費者,它們將返回一個下限,因爲消費者控制尾索引,但是生產者可能仍在另一個CPU上填充緩衝區並移動頭索引。

  • 對於生產者,它將顯示一個上限,因爲消費者可能正在忙於清空緩衝區。

  • 對於第三方,無法保證生產者和消費者對索引的寫入的可見性順序,因爲它們是獨立的,可能在不同的CPU上進行 - 因此,在這種情況下的結果只是一個猜測,甚至可能是負數。

使用內存屏障與循環緩衝區

通過在循環緩衝區中使用內存屏障,可以避免以下需求:

  • 使用單個鎖來管理對緩衝區兩端的訪問,從而允許緩衝區同時被填充和清空。
  • 使用原子計數操作。

這有兩個方面:填充緩衝區的生產者和清空緩衝區的消費者。在任何時候,只應該有一件事在填充緩衝區,而只應該有一件事在清空緩衝區,但是兩個方面可以同時操作。

生產者

生產者將類似於以下內容:

spin_lock(&producer_lock);

unsigned long head = buffer->head;
/* spin_unlock() 和下一個 spin_lock() 提供所需的排序。 */
unsigned long tail = READ_ONCE(buffer->tail);

if (CIRC_SPACE(head, tail, buffer->size) >= 1) {
        /* 將一個項目插入緩衝區 */
        struct item *item = buffer[head];

        produce_item(item);

        smp_store_release(buffer->head,
                          (head + 1) & (buffer->size - 1));

        /* wake_up() 確保在喚醒任何人之前,頭部已經提交 */
        wake_up(consumer);
}

spin_unlock(&producer_lock);

這將指示CPU必須在將新項目的內容寫入之前,將頭索引使其對消費者可用,並且指示CPU必須在喚醒消費者之前,必須寫入修訂後的頭索引。

請注意,wake_up() 不會保證任何類型的屏障,除非實際喚醒了某些內容。因此,我們不能依賴它進行排序。但是,數組中始終會留下一個元素爲空。因此,生產者必須在可能損壞消費者當前正在讀取的元素之前產生兩個元素。因此,在連續調用消費者之間的解鎖-鎖對提供了所需的排序,用於指示消費者已經空出了給定元素的索引的讀取,以及生產者對該相同元素的寫入。

消費者

消費者將類似於以下內容:

spin_lock(&consumer_lock);

/* 在讀取該索引的內容之前讀取索引。 */
unsigned long head = smp_load_acquire(buffer->head);
unsigned long tail = buffer->tail;

if (CIRC_CNT(head, tail, buffer->size) >= 1) {

        /* 從緩衝區中提取一個項目 */
        struct item *item = buffer[tail];

        consume_item(item);

        /* 在增加尾部之前完成讀取描述符。 */
        smp_store_release(buffer->tail,
                          (tail + 1) & (buffer->size - 1));
}

spin_unlock(&consumer_lock);

這將指示CPU在讀取新項目之前,必須確保索引是最新的,然後必須確保CPU在寫入新尾指針之前已經完成了讀取項目,這將擦除該項目。

請注意,使用READ_ONCE() 和smp_load_acquire() 讀取對立索引。這可以防止編譯器丟棄並重新加載其緩存的值。如果可以確保對立索引只會被使用一次,則不嚴格需要這樣做。此外,smp_load_acquire() 還強制CPU對後續內存引用進行排序。類似地,兩個算法中都使用了smp_store_release() 來寫入線程的索引。這說明了我們正在寫入可以同時被讀取的內容,防止編譯器破壞存儲,並強制對先前的訪問進行排序。

進一步閱讀

另請參閱Documentation/memory-barriers.txt,瞭解Linux的內存屏障功能的描述。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章