文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

安全函数不安全-多线程慎用List.h

2024-12-02 12:41

关注

 本文转载自微信公众号「非典型技术宅」,作者无知少年。转载本文请联系非典型技术宅公众号。

前言

linux 开发应该多少都听过大名鼎鼎的 list.h ,其简洁优雅的设计,一个头文件完成了一个高可用的链表。

但是 list.h 并不是线程安全的,在多线程的情况下使用,必须考虑多线程数据同步的问题。

然而。。。。

我在使用互斥锁对链表的操作进行保护之后,还是被坑了!

下面是把我坑了的 list_for_each_entry 和 list_for_each_entry_safe 两个函数的详细分析。

list.h 单线程使用

在 list.h 这个文件中有非常多值得学习的地方,比如其最经典的 container_of 的实现。

在这里只介绍几个常用的函数,然后重点分析在多线程使用时的碰到的坑。

链表初始化及添加节点

首先定义一个链表和链表节点,定义一个产品,其属性为产品重量(weight)。

  1. typedef struct product_s 
  2.     struct list_head product_node; 
  3.     uint32_t index
  4.     uint32_t weight;   
  5. }product_t; 
  6.  
  7. //初始化链表头 
  8. LIST_HEAD(product_list); 

生产者在生产完产品后,将产品加入链表,等待消费者使用。

  1. void producer(void) 
  2.     product_t *product = malloc(sizeof(product_t)); 
  3.  
  4.     // 产品重量为 300 ± 10 
  5.     product->weight = 290 + rand() % 20; 
  6.      
  7.     printf("product :%p, weight %d\n", product, product->weight); 
  8.     list_add_tail(&product->product_node, &product_list); 

遍历链表

使用 list_for_each_entry 可以将链表进行遍历:

  1. // 遍历打印链表信息 
  2. void print_produce_list(void) 
  3.     product_t *product; 
  4.     list_for_each_entry(product, &product_list, product_node) 
  5.     { 
  6.         printf("manufacture product :%p, weight %d\n", product, product->weight); 
  7.     } 

其具体实现是使用宏将 for 循环的初始条件和完成条件进行了替换:

  1. #define list_for_each_entry(pos, head, member)                \ 
  2.     for (pos = list_first_entry(head, typeof(*pos), member);    \ 
  3.          &pos->member != (head);                    \ 
  4.          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 中提供了一个安全删除节点的函数

  1. // 删除重量小于300的节点 
  2. void remove_unqualified_produce(void) 
  3.     product_t *product, *temp
  4.     list_for_each_entry_safe(product, temp, &product_list, product_node) 
  5.     { 
  6.         // 移除重量小于300的产品 
  7.         if (product->weight < 300) 
  8.         { 
  9.             printf("remove product :%p, weight %d\n", product, product->weight); 
  10.             list_del(&product->product_node); 
  11.             free(product); 
  12.         } 
  13.     } 

其实现是使用一个中间变量,在开始每次开始执行循环体前,将当前节点的下一个节点保存到中间变量,从而实现"安全"遍历

  1. #define list_for_each_entry_safe(pos, n, head, member)            \ 
  2.     for (pos = list_first_entry(head, typeof(*pos), member),    \ 
  3.         n = list_next_entry(pos, member);            \ 
  4.          &pos->member != (head);                    \ 
  5.          pos = n, n = list_next_entry(n, member)) 

多线程中使用list.h

上面我们在主线程里面创建了产品,并放入到链表中并,并过滤了重量小于300的产品。

后面我们在多线程中对产品进行消费(两个线程同时消费链表的数据,使用完成后删除并释放结点)。

这里的逻辑和单线程中的差不多,同样是遍历链表,然后从链表中删除节点。不同的是,由于list.h自身没有带锁,所以需要使用互斥锁将链表的操作进行保护。

于是很自然的有了下面的代码

  1. void * consumer(void *arg) 
  2.     product_t *product, *temp
  3.  
  4.     // 使用互斥锁对链表进行保护 
  5.     pthread_mutex_lock(&producer_mutex); 
  6.     list_for_each_entry_safe(product, temp, &product_list, product_node) 
  7.     { 
  8.         list_del(&product->product_node); 
  9.         printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self()); 
  10.         pthread_mutex_unlock(&producer_mutex); 
  11.  
  12.         // 睡一会,防止太快了 
  13.         usleep(10*1000); 
  14.         free(product); 
  15.         pthread_mutex_lock(&producer_mutex); 
  16.     } 
  17.     pthread_mutex_unlock(&producer_mutex); 
  18.      
  19.     return NULL

在上面的这段代码中,在对链表操作时,使用互斥锁对链表进行了保护,使同时只能有一个线程访问链表。

不过你以为这样就好了嘛,如果时这样,这篇文章就没存在的必要了。。。

[[440989]]

在两个线程同时遍历时,即便是加了锁之后,数据访问也不安全。

在遍历使用的 list_for_each_entry_safe 宏中,使用了一个零时变量对保存着当前链表的下一个节点。

但是多线程访问链表时,有可能零时变量保存的节点,被另一个线程删除了,所以访问的时候又是 Segmentation fault

[[440990]]

后记

原因找到了,也就好办了。以至于解决方法嘛,我是使用一个全局的零时变量,将需要删除节点的下一个节点保存起来,手动实现多线程的"安全删除"。

  1. // 消费者 
  2. void * consumer(void *arg) 
  3.     product_t *product; 
  4.  
  5.     // 使用互斥锁对链表进行保护 
  6.     pthread_mutex_lock(&producer_mutex); 
  7.     list_for_each_entry(product, &product_list, product_node) 
  8.     { 
  9.         temp = list_next_entry(product, product_node); 
  10.         list_del(&product->product_node); 
  11.         printf("consume product :%p, weight %d, consumer :%p\n", product, product->weight, (void *)pthread_self()); 
  12.         pthread_mutex_unlock(&producer_mutex); 
  13.  
  14.         // 睡一会,防止太快 
  15.         usleep(10*1000); 
  16.         free(product); 
  17.  
  18.         pthread_mutex_lock(&producer_mutex); 
  19.         if(temp != NULL){ 
  20.             product = list_prev_entry(temp, product_node); 
  21.         } 
  22.     } 
  23.     pthread_mutex_unlock(&producer_mutex); 
  24.  
  25.     return NULL

一个晚上找到了这个bug,然后又花了一个晚上记录下来这个bug。

据说 klist.h 是 list.h 的线程安全版本,后面花时间在研究一下去,今天就先睡了。。。

 

来源:非典型技术宅内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯