安全函式不安全-多執行緒慎用List.h

roc_guo發表於2021-12-27

安全函式不安全-多執行緒慎用List.h安全函式不安全-多執行緒慎用List.h

前言

linux 開發應該多少都聽過大名鼎鼎的 list.h ,其簡潔優雅的設計,一個標頭檔案完成了一個高可用的連結串列。

但是 list.h 並不是執行緒安全的,在多執行緒的情況下使用,必須考慮多執行緒資料同步的問題。

然而。。。。

我在使用互斥鎖對連結串列的操作進行保護之後,還是被坑了!

下面是把我坑了的 list_for_each_entry 和 list_for_each_entry_safe 兩個函式的詳細分析。

list.h 單執行緒使用

在 list.h 這個檔案中有非常多值得學習的地方,比如其最經典的 container_of 的實現。

在這裡只介紹幾個常用的函式,然後重點分析在多執行緒使用時的碰到的坑。

連結串列初始化及新增節點

首先定義一個連結串列和連結串列節點,定義一個產品,其屬性為產品重量(weight)。

typedef struct product_s
{
struct list_head product_node;
uint32_t index;
uint32_t weight;
}product_t;
//初始化連結串列頭
LIST_HEAD(product_list);

生產者在生產完產品後,將產品加入連結串列,等待消費者使用。

void producer(void)
{
product_t *product = malloc(sizeof(product_t));
// 產品重量為 300 ± 10
product->weight = 290 + rand() % 20;
printf("product :%p, weight %d\n", product, product->weight);
list_add_tail(&product->product_node, &product_list);
}
遍歷連結串列

使用 list_for_each_entry 可以將連結串列進行遍歷:

// 遍歷列印連結串列資訊
void print_produce_list(void)
{
product_t *product;
list_for_each_entry(product, &product_list, product_node)
{
printf("manufacture product :%p, weight %d\n", product, product->weight);
}
}

其具體實現是使用宏將 for 迴圈的初始條件和完成條件進行了替換:

#define list_for_each_entry(pos, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member); \
&pos->member != (head); \
pos = list_next_entry(pos, member))

其中for迴圈的第一個引數將 pos = list_first_entry(head, typeof(*pos), member); 初始化為連結串列頭指向的第一個實體連結串列成員。

第二個引數 &pos->member != (head) 為跳出條件,當pos->member再次指向連結串列頭時跳出for迴圈。

for的第三個引數透過pos->member.next指標遍歷整個實體連結串列,當pos->member.next再次指向我們的連結串列頭的時候跳出for迴圈。

但是 list_for_each_entry 不能在遍歷的迴圈體中刪除節點,因為在迴圈體中刪除連結串列節點後,當前節點的前驅結點和後繼結點指標會被置空。

在for迴圈的第三個引數中,獲取下一個節點時,會發生非法指標訪問

安全函式不安全-多執行緒慎用List.h安全函式不安全-多執行緒慎用List.h

“安全遍歷連結串列”

為了解決在遍歷連結串列過程中,無法刪除結點的問題,在 list.h 中提供了一個安全刪除節點的函式

// 刪除重量小於300的節點
void remove_unqualified_produce(void)
{
product_t *product, *temp;
list_for_each_entry_safe(product, temp, &product_list, product_node)
{
// 移除重量小於300的產品
if (product->weight < 300)
{
printf("remove product :%p, weight %d\n", product, product->weight);
list_del(&product->product_node);
free(product);
}
}
}

其實現是使用一箇中間變數,在開始每次開始執行迴圈體前,將當前節點的下一個節點儲存到中間變數,從而實現"安全"遍歷

#define list_for_each_entry_safe(pos, n, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member), \
n = list_next_entry(pos, member); \
&pos->member != (head); \
pos = n, n = list_next_entry(n, member))
多執行緒中使用list.h

上面我們在主執行緒裡面建立了產品,並放入到連結串列中並,並過濾了重量小於300的產品。

後面我們在多執行緒中對產品進行消費(兩個執行緒同時消費連結串列的資料,使用完成後刪除並釋放結點)。

這裡的邏輯和單執行緒中的差不多,同樣是遍歷連結串列,然後從連結串列中刪除節點。不同的是,由於list.h自身沒有帶鎖,所以需要使用互斥鎖將連結串列的操作進行保護。

於是很自然的有了下面的程式碼

void * consumer(void *arg)
{
product_t *product, *temp;
// 使用互斥鎖對連結串列進行保護
pthread_mutex_lock(&producer_mutex);
list_for_each_entry_safe(product, temp, &product_list, product_node)
{
list_del(&product->product_node);
printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self());
pthread_mutex_unlock(&producer_mutex);
// 睡一會,防止太快了
usleep(10*1000);
free(product);
pthread_mutex_lock(&producer_mutex);
}
pthread_mutex_unlock(&producer_mutex);
return NULL;
}

在上面的這段程式碼中,在對連結串列操作時,使用互斥鎖對連結串列進行了保護,使同時只能有一個執行緒訪問連結串列。

不過你以為這樣就好了嘛,如果時這樣,這篇文章就沒存在的必要了。。。

在兩個執行緒同時遍歷時,即便是加了鎖之後,資料訪問也不安全。

在遍歷使用的 list_for_each_entry_safe 宏中,使用了一個零時變數對儲存著當前連結串列的下一個節點。

但是多執行緒訪問連結串列時,有可能零時變數儲存的節點,被另一個執行緒刪除了,所以訪問的時候又是 Segmentation fault

後記

原因找到了,也就好辦了。以至於解決方法嘛,我是使用一個全域性的零時變數,將需要刪除節點的下一個節點儲存起來,手動實現多執行緒的"安全刪除"。

// 消費者
void * consumer(void *arg)
{
product_t *product;
// 使用互斥鎖對連結串列進行保護
pthread_mutex_lock(&producer_mutex);
list_for_each_entry(product, &product_list, product_node)
{
temp = list_next_entry(product, product_node);
list_del(&product->product_node);
printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self());
pthread_mutex_unlock(&producer_mutex);
// 睡一會,防止太快
usleep(10*1000);
free(product);
pthread_mutex_lock(&producer_mutex);
if(temp != NULL){
product = list_prev_entry(temp, product_node);
}
}
pthread_mutex_unlock(&producer_mutex);
return NULL;
}

一個晚上找到了這個bug,然後又花了一個晚上記錄下來這個bug。

據說 klist.h 是 list.h 的執行緒安全版本,後面花時間在研究一下去,今天就先睡了。。。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69901823/viewspace-2849484/,如需轉載,請註明出處,否則將追究法律責任。

相關文章