1. 應用場景
網絡程式設計中有這樣一種場景:需要應用程式代碼一邊從TCP/IP協定棧接收資料(reading data from socket),一邊解析接收的資料。具體場景例如:使用者點選Youtube或優酷網站上的視訊内容,這時使用者PC上的播放軟體就是一邊接收資料一邊對資料進行解碼并播放的。這樣的場景的存在如下限制:
1. 必須邊接收資料,邊對資料進行解析,不能等待到資料全部接收完整後才解析(使用者等待的時間與體驗成反比)。
2. 資料為流式資料(如TCP承載),需對接收到的資料進行定界分析,将資料轉化為可被應用程式解析的結構化資料。
3. 資料的解析需要兼顧性能和記憶體空間的利用效率(如果減少記憶體拷貝,配置設定适當大小的緩存空間)。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsISPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsATOfd3bkFGazxCMx8VesATMfhHLlN3XnxCMwEzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5CNjNjYlVGNxYWOzEjZhJGNiNDNmdTZiRWZzMDO0IGO48CXxAzLchDMxIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjL5M3Lc9CX6MHc0RHaiojIsJye.png)
本文将設計一個适合上述場景的環形緩沖元件,提供友善的資料緩存與讀取接口,讓編碼專注于資料解析的邏輯,而不是将過多的精力消耗在緩沖區本身的處理上。本文讨論POSIX的一種優化的環形緩沖實作方式,并提出了進一步優化:
1. 高效的資料寫入與讀取接口,如應用程式可能對某段資料不感興趣,則可将其直接忽略掉。
2. 封裝了常見的整形資料讀取接口,解析程式可以直接讀數1~4位元組的整形資料。
- #ifndef _CIRCULAR_BUFFER_H
- #define _CIRCULAR_BUFFER_H
- typedef struct CircularBuffer {
- void *ptr;
- /* 必須為整數倍記憶體頁面大小*/
- unsigned long count;
- unsigned long read_offset;
- unsigned long write_offset;
- } CircularBuffer;
- /* 建立環形緩沖區 */
- CircularBuffer *cbCreate(unsigned long order);
- /* 銷毀環形緩沖區 */
- void cbFree(CircularBuffer *cb);
- /* 重置緩沖區,使之可用于新的業務資料緩存 */
- void cbClear(CircularBuffer *cb);
- int cbIsEmpty(CircularBuffer *cb);
- unsigned long cbUsedSpaceSize(CircularBuffer *cb);
- unsigned long cbFreeSpaceSize(CircularBuffer *cb);
- /* 向環形緩沖寫入len 位元組資料 */
- unsigned long cbPushBuffer(CircularBuffer *cb, void *buffer, unsigned long len);
- /* 從環形緩沖讀取len位元組存放到buffer中,
- buffer可以為NULL,忽略len位元組的資料*/
- void *cbReadBuffer(CircularBuffer *cb, void *buffer, unsigned long len);
- /* 從環形緩沖區讀取1個位元組 */
- unsigned char cbReadUINT8(CircularBuffer *cb);
- /* 從環形緩沖區讀取1個短整形數 */
- unsigned short cbReadUINT16(CircularBuffer *cb);
- short cbReadSINT16(CircularBuffer *cb);
- unsigned int cbReadUINT24(CircularBuffer *cb);
- int cbReadSINT24(CircularBuffer *cb);
- unsigned int cbReadUINT32(CircularBuffer *cb);
- int cbReadSINT32(CircularBuffer *cb);
- #endif
cbCreate接口建立并初始化一個環形緩沖區,實作如下:
- CircularBuffer *cbCreate(unsigned long order)
- {
- int fd = 0, status = 0;
- void *address = NULL;
- char path[] = "/dev/shm/circular_buffer_XXXXXX";
- CircularBuffer *cb = (CircularBuffer *)malloc(sizeof(CircularBuffer));
- if (NULL == cb) {
- return NULL;
- }
- order = (order <= 12 ? 12 : order);
- cb->count = 1UL << order;
- cb->read_offset = 0;
- cb->write_offset = 0;
- /* 配置設定2倍指定的緩沖空間 */
- cb->ptr = mmap(NULL, cb->count << 1, PROT_NONE, MAP_ANONYMOUS |MAP_PRIVATE, -1, 0);
- if (MAP_FAILED == cb->ptr) {
- abort(); |
- /* 根據path子產品建立一個唯一的臨時檔案 */
- fd = mkstemp(path);
- if (0 > fd) {
- abort();
- /* 删除檔案通路的目錄入口,程序仍可使用該檔案 */
- status = unlink(path);
- if (0 != status) {
- /* 将檔案大小精确指定為count位元組 */
- status = ftruncate(fd, cb->count);
- /* 将[ cb->ptr, cb->ptr + cb->count)位址空間映射到臨時檔案*/
- address = mmap(cb->ptr, cb->count, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, fd, 0);
- if (address != cb->ptr) {
- /* 将[ cb->ptr + cb->count, cb->ptr + 2 * cb->count)位址空間映射到臨時檔案*/
- address = mmap(cb->ptr + cb->count, cb->count, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, fd, 0);
- if (address != cb->ptr + cb->count) {
- status = close(fd);
- return cb;
該實作采用了一種精妙的處理方式,用2倍的緩存空間簡化資料的讀寫操作。
第1個mmap采用私有匿名的方式配置設定了一塊為指定緩沖區大小2倍的記憶體空間;第2個mmap将mkstemp建立的臨時檔案映射到[ptr,
ptr + count)位址,第3個mmap将mkstemp建立的臨時檔案映射到[ptr + count, ptr + 2 *
count)位址,這樣對ptr[i]的讀寫操作将等同于對ptr[i + count]的讀寫操作,進而達到簡化了環形緩沖區對于資料回繞的邏輯。
如下代碼為讀寫環形緩沖區及計算緩沖區已使用空間大小的例程。cbUsedSpaceSize函數可用于cbIsEmpty及cbFreeSpaceSize函數的實作。cbReadBuffer函數則可用于實作cbReadUINT8、cbReadUINT16、cbReadSINT16、cbReadUINT24、cbReadSINT24、cbReadUINT32及cbReadSINT32。cbReadBuffer函數的buffer參數若傳人為空,則忽略len指定長度位元組的資料。
- unsigned long cbPushBuffer(CircularBuffer *cb, void *buffer, unsigned long len)
- unsigned long write_offset = cb->write_offset;
- cb->write_offset += len;
- memmove(cb->ptr + write_offset, buffer, len);
- return len;
- void *cbReadBuffer(CircularBuffer *cb, void *buffer, unsigned long len)
- /* 忽略len位元組資料 */
- if (NULL != buffer) {
- address = memmove(buffer, cb->ptr + cb->read_offset, len);
- cb->read_offset += len;
- if (cb->read_offset > cb->count) {
- cb->read_offset -= cb->count;
- cb->write_offset -= cb->count;
- return address;
- unsigned long cbUsedSpaceSize(CircularBuffer *cb)
- return cb->write_offset - cb->read_offset;
3. 分析與讨論
1. 環形緩沖區特别适合于FIFO類型資料的處理,利用它可以不拷貝記憶體完成緩沖上資料的解析,提高資料解析效率。
2. 若資料讀取函數采用單位元組讀、取模數計算偏移的方式,則可能帶來性能上的損耗,該問題可以通過增加判斷或以做位運算等機制來解決,但同時也增加了實作邏輯的複雜度。
3. 其不足之處在于需要預先估計資料緩沖的大小,并配置設定比預估大小大一個數量級的緩存空間。一種可能的解決辦法是增加檢測機制,若發現緩沖太小,則動态調大緩沖的大小,但這同時又可能導緻頻繁的調整記憶體大小,帶來性能的下降。
(總結:根絕這樣寫的ringbuf 确實有點麻煩,暫時還沒有體會其中的要義,自己也嘗試着寫一個簡單的ringbuf,就是往這裡放東西,然後取定長資料)
【作者】張昺華
歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.