从0实现基于Linux socket聊天室-多线程服务器一个很隐晦的错误-2
根据 《0 基于socket和pthread实现多线程服务器模型》所述,server创建子线程的时候用的是以下代码:
pconnsocke = (int *) malloc(sizeof(int));
*pconnsocke = new_fd;
ret = pthread_create(&tid, NULL, rec_func, (void *) pconnsocke);
if (ret < 0)
{
perror("pthread_create err");
return -1;
}
为什么必须要malloc一块内存专门存放这个新的套接字呢?
要讲清楚这个问题的原因需要一些背景知识:
Linux创建一个新进程时,新进程会创建一个主线程; 每个用户进程有自己的地址空间,系统为每个用户进程创建一个task_struct来描述该进程, 实际上task_struct 和地址空间映射表一起用来,表示一个进程; Linux里同样用task_struct来描述一个线程,线程和进程都参与统一的调度; 进程内的不同线程执行是同一程序的不同部分,各个线程并行执行,受操作系统异步调度; 由于进程的地址空间是私有的,因此在进程间上下文切换时,系统开销比较大; 在同一个进程中创建的线程共享该进程的地址空间。
明白这些基础知识后,下面我来看下,当进程创建一个子线程的时候,传递的参数情况:
直接传递栈中内存地址
我们首先分析下如果创建子线程传递的是局部变量new_fd的地址这种情况。
由上图所示:
创建一个线程,如果我们按照图中传递参数方法,那么new_fd是在栈中的,创建子线程的时候我们把new_fd地址传递给了thread1,线程回调参数arg的地址是new_fd地址。
因为主函数会一直循环不退出,所以new_fd一直存在栈中。用这种方法的确可以把new_fd的值3传递到子线程的局部变量fd,这样子线程就可以使用这个fd与客户端通信。
但是因为我们设计的是并发服务器模型,我们没有办法预测客户端什么时候会连接我们的服务器,假设遇到一个极端情况,在同一时刻,多个客户端同时连接服务器,那么主线程是要同时创建多个子线程的。
多个客户端同时连接服务器
如上图所示,所有新建的的thread回调函数的参数arg存放的都是new_fd的地址。如果客户端连接的时候时间间隔比较大,是没有问题的,但是在一些极端的情况下还是有可能出现由于高并发引起的错误。
我们来捋一下极端的调用时序:
第一步:
如上图所示:
T1时刻,当客户端1连接服务器的时候,服务器的accept函数会创建新的套接字4; T2时刻,创建了子线程thread1,同时子线程回调函数参数arg指向了栈中new_fd对应的内存。 假设,正在此时,又有一个客户端要连接服务器,而且thread1页已经用尽了时间片,那么主线程server会被调度到。
第二步:
如上图所示:
T3时刻,主线程server接受了客户端的连接,accept函数会创建新的套接字5,同时创建子线程thread2,此时OS调度的thread2; T4时刻,thread2通过arg得到new_fd了的值5,并存入fd; T5时刻,时间片到了,调度thread1,thread1通过arg去读取new_fd,此时栈中new_fd的值已经修5覆盖了; 所以出现了2个线程同时使用同一个fd的情况发生。
这种情况的发生,虽然概率很低,但是并不代表不发生,该bug就是一口君在解决实际项目中遇到过的。
传递堆内存地址
如果采用传递堆的地址的方式,我们看下图:
T1时刻,当客户端1连接服务器的时候,服务器的accept函数会创建新的套接字4,在堆中申请一块内存,用指针pconnsocke指向该内存,同时将4保存到堆中; T2时刻,创建了子线程thread1,同时子线程回调函数参数arg指向了堆中pconnsocke指向的内存。 假设,正在此时,又有一个客户端要连接服务器,而且thread1页已经用尽了时间片,那么主线程server会被调度到。 T3时刻,主线程server接受了客户端的连接,accept函数会创建新的套接字5,在堆中申请一块内存,用指针pconnsocke指向该内存,同时将5保存到堆中,然后创建子线程thread2; T4时刻,thread2通过arg指向了堆中pconnsocke指向的内存,此处值为5,并存入fd; T5时刻,时间片到了,调度thread1,thread1通过arg去读取fd,此时堆中数据位5; 就不会出现了2个线程同时使用同一个fd的情况发生。
这个知识点有点隐蔽,希望读者在使用的时候多加小心。下一章,我们要讲解如何利用我们现有的代码实现登录注册的功能。
相关阅读:
《从0实现基于Linux socket聊天室-多线程服务器模型-1》
一口君个人微信
添加一口君个人微信即送Linux、嵌入式等独家入门视频
→ 精选技术资料共享
→ 高手如云交流社群
推荐阅读
【1】基于ARM UART裸机驱动详解
【2】Linux 自旋锁spinlock,教你如何把ubuntu弄死锁
【3】看了这几个C语言例子,你一定和我一样连说5个卧槽,声音一次比一次大
【4】手把手教Linux驱动7-内核互斥锁
【5】Linux信号量(1)-SYSTEM V
【6】I2C干货-基于Cortex-A9(重新整理)【7】一文搞懂ADC裸机和基于Linux驱动编写方法
【8】22张图详解浏览器请求数据包如何到达web服务器(搞懂网络可以毕业了)
【9】手把手教Linux驱动5-自旋锁、信号量、互斥体概述
【10】从0实现基于Linux socket聊天室-多线程服务器模型-1
本公众号全部原创干货已整理成一个目录,请在公众号里回复「m」获取!或者关注进入后台点击左下角「干货」!
后台回复「进群」,即可加入技术交流群,进群福利:免费赠送Linux学习资料。