当前位置: 首页 > 科技观察

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

时间:2023-03-16 02:22:26 科技观察

本文转载自微信公众号《非典型技术宅》,作者是懵懂少年。转载本文请联系非典型技术宅公众号。前言Linux开发应该或多或少听过大名鼎鼎的list.h,它的设计简洁大方,一个头文件就完成了一个高可用的链表。但是list.h不是线程安全的。在多线程环境下使用时,必须考虑多线程数据同步的问题。然而。...我用互斥锁保护链表运行后,还是被骗了!下面详细分析骗我的list_for_each_entry和list_for_each_entry_safe这两个函数。list.h单线程使用list.h有很多值得学习的地方,比如最经典的container_of实现。这里我们只介绍几个常用的函数,然后着重分析在使用多线程时遇到的陷阱。链表初始化和添加节点首先定义一个链表和链表节点,定义一个商品,它的属性是商品重量(weight)。typedefstructproduct_s{structlist_headproduct_node;uint32_tindex;uint32_tweight;}product_t;//初始化链表头LIST_HEAD(product_list);生产者生产完产品后,将产品添加到链表中,等待消费者使用。voidproducer(void){product_t*product=malloc(sizeof(product_t));//产品重量为300±10product->weight=290+rand()%20;printf("product:%p,weight%d\n",产品,产品->重量);list_add_tail(&product->product_node,&product_list);}遍历链表使用list_for_each_entry遍历链表://遍历并打印链表信息voidprint_produce_list(void){product_t*product;list_for_each_entry(product,&product_list,product_node){printf("manufactureproduct:%p,weight%d\n",product,product->weight);}}具体实现是用一个宏来代替for循环的初始条件和完成条件:#definelist_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中提供了一个安全删除节点的函数//删除权重小于300的节点voidremove_unqualified_produce(void){product_t*product,*temp;list_for_each_entry_safe(product,temp,&product_list,product_node){//移除重量小于300的产品if(product->weight<300){printf("removeproduct:%p,weight%d\n",product,product->weight);list_del(&product->product_node);free(product);}}}是通过中间变量实现的,每次开始执行循环体之前,保存当前节点的下一个节点给一个中间变量,从而实现“安全”遍历#definelist_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上面我们创建了pro主线程中的ducts,放入链表中,过滤出权重小于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("consumeproduct:%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);returnNULL;}在上面的代码中,在对链表进行操作时,被链接的列表受互斥锁保护,因此一次只有一个线程可以访问链表。但是你觉得这样就可以了吗?如果是这样的话,这篇文章就没有存在的必要了。..当两个线程同时遍历时,即使加了锁,数据访问也不安全。在用于遍历的list_for_each_entry_safe宏中,用一个零时变量对保存当前链表的下一个节点。但是,当多个线程访问链表时,有可能零时变量保存的节点被另一个线程删除,所以访问时很容易找到Segmentationfault后记的原因。至于解决方法,我是用一个全局的零时间变量保存下一个需要删除的节点,手动实现多线程的“安全删除”。//Consumervoid*consumer(void*arg){product_t*product;//使用mutex保护链表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("consumeproduct:%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);returnNULL;}一个晚上找到错误,另一个晚上记录它。据说klist.h是list.h的线程安全版本,后面会花点时间研究一下,今天先睡了。..